doomsday-stable-1.15.7/0000775000175000017500000000000012641367725014175 5ustar jaakkojaakkodoomsday-stable-1.15.7/.gitmodules0000664000175000017500000000016112641367670016347 0ustar jaakkojaakko[submodule "doomsday/external/assimp"] path = doomsday/external/assimp url = https://github.com/skyjake/assimp doomsday-stable-1.15.7/doomsday/0000775000175000017500000000000012641367671016014 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/0000775000175000017500000000000012641367671017156 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/tests.pro0000664000175000017500000000055612641367671021050 0ustar jaakkojaakko# The Doomsday Engine Project # Copyright (c) 2012-2013 Jaakko Keränen include(../config.pri) TEMPLATE = subdirs !deng_notests:deng_tests: SUBDIRS += \ test_archive \ test_commandline \ test_bitfield \ test_info \ test_log \ test_record \ test_script \ test_string \ test_stringpool \ test_vectors doomsday-stable-1.15.7/doomsday/tests/test_record/0000775000175000017500000000000012641367671021473 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_record/main.cpp0000664000175000017500000000446712641367671023136 0ustar jaakkojaakko/* * The Doomsday Engine Project * * Copyright (c) 2009-2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include using namespace de; int main(int argc, char **argv) { try { TextApp app(argc, argv); app.initSubsystems(App::DisablePlugins); Record rec; LOG_MSG("Empty record:\n") << rec; rec.add(new Variable("hello", new TextValue("World!"))); LOG_MSG("With one variable:\n") << rec; rec.add(new Variable("size", new NumberValue(1024))); LOG_MSG("With two variables:\n") << rec; Record rec2; Block b; Writer(b) << rec; LOG_MSG("Serialized record to ") << b.size() << " bytes."; String str; QTextStream os(&str); for(duint i = 0; i < b.size(); ++i) { os << dint(b.data()[i]) << " "; } LOG_MSG(str); Reader(b) >> rec2; LOG_MSG("After being deserialized:\n") << rec2; Record before; before.addRecord("subrecord"); before.subrecord("subrecord").set("value", true); DENG2_ASSERT(before.hasSubrecord("subrecord")); LOG_MSG("Before copying:\n") << before; Record copied = before; DENG2_ASSERT(copied.hasSubrecord("subrecord")); LOG_MSG("Copied:\n") << copied; } catch(Error const &err) { qWarning() << err.asText(); } qDebug() << "Exiting main()..."; return 0; } doomsday-stable-1.15.7/doomsday/tests/test_record/test_record.pro0000664000175000017500000000015412641367671024532 0ustar jaakkojaakkoinclude(../config_test.pri) TEMPLATE = app TARGET = test_record SOURCES += main.cpp deployTest($$TARGET) doomsday-stable-1.15.7/doomsday/tests/test_info/0000775000175000017500000000000012641367671021150 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_info/main.cpp0000664000175000017500000000233312641367671022601 0ustar jaakkojaakko/* * The Doomsday Engine Project * * Copyright (c) 2009-2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, see . */ #include #include #include #include #include using namespace de; int main(int argc, char **argv) { try { TextApp app(argc, argv); app.initSubsystems(App::DisablePlugins); ScriptedInfo dei; dei.parse(app.fileSystem().find("test_info.dei")); } catch(Error const &err) { qWarning() << err.asText(); } qDebug("Exiting main()..."); return 0; } doomsday-stable-1.15.7/doomsday/tests/test_info/test_info.pro0000664000175000017500000000051212641367671023662 0ustar jaakkojaakkoinclude(../config_test.pri) TEMPLATE = app TARGET = test_info SOURCES += main.cpp SCRIPTS = test_info.dei OTHER_FILES += $$SCRIPTS deployTest($$TARGET) scripts.files = $$SCRIPTS scripts.path = $$DENG_DATA_DIR macx { scripts.path = Contents/Resources QMAKE_BUNDLE_DATA += scripts } else { INSTALLS += scripts } doomsday-stable-1.15.7/doomsday/tests/test_info/test_info.dei0000664000175000017500000000350212641367671023625 0ustar jaakkojaakko# Fonts for the default UI style # - size can be "pt" or "px", defaults to "pt" # - weight: normal bold light # - style: normal italic script { # Version module will be available in embedded scripts here. import Version def joinArray(a) result = '' for i in a: result += i return result end } script "condition testing" condition "Version.OS == 'macx'" { print "Running on %s!" % Version.OS for i in [0, 2, 4] print 'i =', i end } # Nameless groups are allowed. group { condition: False script test2 condition True { print "WILL NOT BE RUN" } } thing A { member: value from A } thing B inherits A { member2: value from B script { print "Contents of B:"; print self } } group { # All members inherit A. inherits = A thing C { member2: value from C script { print "Contents of C:"; print self } } thing D inherits B { # Inherits A from group, then B. member3: value from D script { print "Contents of D:"; print self } } } type1 first-block { key = value } type2 example-block inherits first-block {} group font { condition: Version.OS == 'macx' preferences < font.default, $" 'font'+'.'+'title' " > font Default { family $= 'Lucida ' + "Grande" size $: joinArray(['16', 'pt']) weight = normal style: normal script { print "Default font:" print self } } # Identifiers retain case in script namespaces (and are case # sensitive), however group types are forced lower case (FoNt==font). FoNt "Title" inherits font.Default { SIZE: 24pt weight $: if self.weight == "normal": "bold"; else: "normal" } } script { print "Title font:\n", font.Title } doomsday-stable-1.15.7/doomsday/tests/test_log/0000775000175000017500000000000012641367671020776 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_log/main.cpp0000664000175000017500000000444712641367671022437 0ustar jaakkojaakko/* * The Doomsday Engine Project * * Copyright (c) 2012-2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, see . */ #include #include #include using namespace de; int main(int argc, char **argv) { try { TextApp app(argc, argv); app.initSubsystems(App::DisablePlugins); for(int j = 0; j < 2; ++j) { bool devMode = j > 0; app.logFilter().setAllowDev(devMode); for(int i = LogEntry::LowestLogLevel; i <= LogEntry::HighestLogLevel; ++i) { LogEntry::Level level = LogEntry::Level(i); app.logFilter().setMinLevel(level); LOG_AT_LEVEL(level, "Enabled level %s with dev:%b") << LogEntry::levelToText(level) << devMode; for(int k = LogEntry::LowestLogLevel; k <= LogEntry::HighestLogLevel; ++k) { for(int d = 0; d < 2; ++d) { duint32 other = k | (d? LogEntry::Dev : 0); LOG_AT_LEVEL(other, "- (currently enabled %8s) entry at level %8s (context %3s): visible: %b") << LogEntry::levelToText(level) << LogEntry::levelToText(other) << LogEntry::contextToText(other) << LogBuffer::get().isEnabled(LogEntry::Generic | other); } } } } } catch(Error const &err) { qWarning() << err.asText(); } qDebug() << "Exiting main()..."; return 0; } doomsday-stable-1.15.7/doomsday/tests/test_log/test_log.pro0000664000175000017500000000015212641367671023336 0ustar jaakkojaakkoinclude(../config_test.pri) TEMPLATE = app TARGET = test_log SOURCES += main.cpp deployTest($$TARGET) doomsday-stable-1.15.7/doomsday/tests/test_appfw/0000775000175000017500000000000012641367671021332 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_appfw/test_appfw.pro0000664000175000017500000000124612641367671024233 0ustar jaakkojaakko# Using Doomsday. DENG_CONFIG = gui appfw symlink include($$DENG_SDK_DIR/doomsday_sdk.pri) # Package the application resources. dengPackage(net.dengine.test.appfw, $$OUT_PWD) win32: INCLUDEPATH += C:/SDK/OpenGL QT += gui opengl network CONFIG += c++11 SOURCES += \ src/approotwidget.cpp \ src/appwindowsystem.cpp \ src/globalshortcuts.cpp \ src/main.cpp \ src/mainwindow.cpp \ src/testapp.cpp HEADERS += \ src/approotwidget.h \ src/appwindowsystem.h \ src/globalshortcuts.h \ src/mainwindow.h \ src/testapp.h !macx: dengDynamicLinkPath($$denglibs.path) dengDynamicLinkPath($$[QT_INSTALL_LIBS]) dengDeploy(test_appfw, $$PREFIX) doomsday-stable-1.15.7/doomsday/tests/test_appfw/README.md0000664000175000017500000000105412641367671022611 0ustar jaakkojaakko# Test for Doomsday SDK and libappfw This test application is a minimal GUI application based on *libappfw* and the rest of the Doomsday SDK. It shows a simple UI with a single label widget. The application supports VR modes. A virtual mouse cursor is displayed if the native mouse cursor cannot be used to interact with window contents. Command line options: - **--ovr** Use the Oculus Rift VR mode. ## Instructions Before compiling you must have the Doomsday SDK installed somewhere. The qmake variable `DENG_SDK_DIR` must point to the SDK folder. doomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/0000775000175000017500000000000012641367671026537 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/shaders/0000775000175000017500000000000012641367671030170 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/shaders/fx-blur.dei0000664000175000017500000000557512641367671032246 0ustar jaakkojaakkogroup fx.blur { # The same vertex shader is used for both blur steps. vertexShader = """ uniform highp mat4 uMvpMatrix; uniform highp vec4 uColor; uniform highp vec4 uWindow; attribute highp vec4 aVertex; attribute highp vec2 aUV; attribute highp vec4 aColor; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { gl_Position = uMvpMatrix * aVertex; vUV = uWindow.xy + aUV * uWindow.zw; vColor = aColor * uColor; }""" shader horizontal { vertex $= fx.blur.vertexShader fragment = " uniform sampler2D uTex; uniform highp vec2 uBlurStep; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { highp vec4 sum = vec4(0.0); sum += texture2D(uTex, vec2(vUV.s - 4.0 * uBlurStep.s, vUV.t)) * 0.05; sum += texture2D(uTex, vec2(vUV.s - 3.0 * uBlurStep.s, vUV.t)) * 0.09; sum += texture2D(uTex, vec2(vUV.s - 2.0 * uBlurStep.s, vUV.t)) * 0.123; sum += texture2D(uTex, vec2(vUV.s - uBlurStep.s, vUV.t)) * 0.154; sum += texture2D(uTex, vUV) * 0.165; sum += texture2D(uTex, vec2(vUV.s + uBlurStep.s, vUV.t)) * 0.154; sum += texture2D(uTex, vec2(vUV.s + 2.0 * uBlurStep.s, vUV.t)) * 0.123; sum += texture2D(uTex, vec2(vUV.s + 3.0 * uBlurStep.s, vUV.t)) * 0.09; sum += texture2D(uTex, vec2(vUV.s + 4.0 * uBlurStep.s, vUV.t)) * 0.05; gl_FragColor = sum; gl_FragColor.a = 1.0; }" } shader vertical { vertex $= fx.blur.vertexShader fragment = " uniform sampler2D uTex; uniform highp vec2 uBlurStep; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { highp vec4 sum = vec4(0.0); sum += texture2D(uTex, vec2(vUV.s, vUV.t - 4.0 * uBlurStep.t)) * 0.05; sum += texture2D(uTex, vec2(vUV.s, vUV.t - 3.0 * uBlurStep.t)) * 0.09; sum += texture2D(uTex, vec2(vUV.s, vUV.t - 2.0 * uBlurStep.t)) * 0.123; sum += texture2D(uTex, vec2(vUV.s, vUV.t - uBlurStep.t )) * 0.154; sum += texture2D(uTex, vUV) * 0.165; sum += texture2D(uTex, vec2(vUV.s, vUV.t + uBlurStep.t )) * 0.154; sum += texture2D(uTex, vec2(vUV.s, vUV.t + 2.0 * uBlurStep.t)) * 0.123; sum += texture2D(uTex, vec2(vUV.s, vUV.t + 3.0 * uBlurStep.t)) * 0.09; sum += texture2D(uTex, vec2(vUV.s, vUV.t + 4.0 * uBlurStep.t)) * 0.05; gl_FragColor = sum * vColor; }" } } doomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/shaders/generic.dei0000664000175000017500000000635112641367671032274 0ustar jaakkojaakkogroup generic { # Simple shader with untextured vertices. There is an additional constant # color applied to all vertices. Uses a combined model-view-projection # matrix. shader color_ucolor { vertex = " uniform highp mat4 uMvpMatrix; uniform highp vec4 uColor; attribute highp vec4 aVertex; attribute highp vec4 aColor; varying highp vec4 vColor; void main(void) { gl_Position = uMvpMatrix * aVertex; vColor = uColor * aColor; }" fragment = " varying highp vec4 vColor; void main(void) { gl_FragColor = vColor; }" } shader texture { vertex = " uniform highp mat4 uMvpMatrix; attribute highp vec4 aVertex; attribute highp vec2 aUV; varying highp vec2 vUV; void main(void) { gl_Position = uMvpMatrix * aVertex; vUV = aUV; }" fragment = " uniform sampler2D uTex; varying highp vec2 vUV; void main(void) { gl_FragColor = texture2D(uTex, vUV); }" } group textured { # Simple shader with one texture plus a color per vertex. Uses a # combined model-view-projection matrix. shader color { vertex = " uniform highp mat4 uMvpMatrix; attribute highp vec4 aVertex; attribute highp vec2 aUV; attribute highp vec4 aColor; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { gl_Position = uMvpMatrix * aVertex; vUV = aUV; vColor = aColor; }" fragment = " uniform sampler2D uTex; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { gl_FragColor = vColor * texture2D(uTex, vUV); }" } # Simple shader with one texture plus a color per vertex. There is # an additional constant color applied to all vertices. Uses a # combined model-view-projection matrix. shader color_ucolor { vertex = " uniform highp mat4 uMvpMatrix; uniform highp vec4 uColor; attribute highp vec4 aVertex; attribute highp vec2 aUV; attribute highp vec4 aColor; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { gl_Position = uMvpMatrix * aVertex; vUV = aUV; vColor = aColor * uColor; }" fragment = " uniform sampler2D uTex; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { gl_FragColor = vColor * texture2D(uTex, vUV); }" } } } ././@LongLink0000644000000000000000000000015300000000000011602 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/shaders/oculusrift-barrel.fshdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/shaders/oculusrift-barr0000664000175000017500000000431012641367671033234 0ustar jaakkojaakko#version 120 uniform sampler2D texture; uniform float distortionScale = 1.714; uniform vec2 screenSize = vec2(0.14976, 0.0936); uniform float lensSeparationDistance = 0.0635; uniform vec4 chromAbParam = vec4(0.996, -0.004, 1.014, 0.0); uniform vec4 hmdWarpParam = vec4(1.0, 0.220, 0.240, 0.000); varying highp vec2 vTexCoord; const float aspectRatio = 1.0; const vec2 inputCenter = vec2(0.5, 0.5); // We render center at center of unwarped image const vec2 scaleIn = vec2(2.0, 2.0/aspectRatio); void main() { vec2 scale = vec2(0.5/distortionScale, 0.5*aspectRatio/distortionScale); vec2 lensCenter = vec2(1.0 - lensSeparationDistance/screenSize.x, 0.5); // left eye // vec2 tcIn = vTexCoord; vec2 uv = vec2(tcIn.x*2, tcIn.y); // unwarped image coordinates (left eye) if (tcIn.x > 0.5) // right eye uv.x = 2 - 2*tcIn.x; vec2 theta = (uv - lensCenter) * scaleIn; float rSq = theta.x * theta.x + theta.y * theta.y; vec2 rvector = theta * ( hmdWarpParam.x + hmdWarpParam.y * rSq + hmdWarpParam.z * rSq * rSq + hmdWarpParam.w * rSq * rSq * rSq); // Chromatic aberration correction vec2 thetaBlue = rvector * (chromAbParam.z + chromAbParam.w * rSq); vec2 tcBlue = inputCenter + scale * thetaBlue; // Blue is farthest out if ( (abs(tcBlue.x - 0.5) > 0.5) || (abs(tcBlue.y - 0.5) > 0.5) ) { gl_FragColor = vec4(0, 0, 0, 1); return; } vec2 thetaRed = rvector * (chromAbParam.x + chromAbParam.y * rSq); vec2 tcRed = inputCenter + scale * thetaRed; vec2 tcGreen = inputCenter + scale * rvector; // green tcRed.x *= 0.5; // because output only goes to 0-0.5 (left eye) tcGreen.x *= 0.5; // because output only goes to 0-0.5 (left eye) tcBlue.x *= 0.5; // because output only goes to 0-0.5 (left eye) if (tcIn.x > 0.5) { // right eye 0.5-1.0 tcRed.x = 1 - tcRed.x; tcGreen.x = 1 - tcGreen.x; tcBlue.x = 1 - tcBlue.x; } float red = texture2D(texture, tcRed).r; vec2 green = texture2D(texture, tcGreen).ga; float blue = texture2D(texture, tcBlue).b; gl_FragColor = vec4(red, green.x, blue, green.y); } doomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/images/0000775000175000017500000000000012641367671030004 5ustar jaakkojaakko././@LongLink0000644000000000000000000000014600000000000011604 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/images/deng-logo-256.pngdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/images/deng-logo-256.pn0000664000175000017500000023067612641367671032546 0ustar jaakkojaakkoPNG  IHDR:= pHYs  *iTXtXML:com.adobe.xmp 2013-05-28T16:05:45 Adobe ImageReady 1 72 5 1 72 256 65535 232 |@IDATxyeYu_^YWwWLXcki[bhB`!$ƀE0Ba?䱰x3tttWuK^{=q^FEfג u\V*y߃߇eA)|$3G@1}af `?跏[13}#tm,?t^y|k;3x 6wyG|?pqݛiqyyo}O^e!d<7@?׽_{3u_~u_uuw?躿?Slg4C=g9 zGP5zmw~z?/?{=v' c7|@W=houw_}[ͷ syAǻ?;m4G>g{CwW`k5hk굽^ {Y^z/W_>yg^3P C5׽7W׽ z}{>|\ {~gg4O#99g4O>+(+cV{ >'P6<11p4#i}|Kn\]7QH琼zsHcRS^&M2e|½JBR[^cV A{s{_ng5>x-v }%~@W`V}o%XU/K+Ijnh3LiPicjxoj0zݦh:vGnEҦ?I6p߽ ߋ!k޳W2ڮulL>&w@!Tu!Jll:ϐ&)t337L}`p6Vo+/=gxGwg~( ޷i:RԬ,/5xZ~*w3LeulV V 3:!/k_ <9GI{nJCBp`C 6_[z=.VmA{ZPݦRC(1HG$ 7ua j zxNs֭֍MZCI9~6Hj ntNZ,z{칔g>۾ '_bēq݋&TTMЛ~mIppsfjzU~@}W+]Crm j808`.DGݎ$ļ'tt$yt3[,_ܸz\o6(<)orfV ěǞپh ~@u_@%\yҷH_'M޷8r9BCʏOG}"y?(Cأ d-zj3#0 2M@.0(ڛݍ\@w Ȩ <@@=~5#DpLCh*WŻ͵wmnopm!Kzd[y|}~vAdEb_8/Hl5U{^ݿ%:ٕOΐ޷9y9zx3=; m$ICHa,8#@s/72:S@ 6o!7;[L"m`6n{r/3fo3N@uݘ [MgӴzj[M!̦ e2 OKf#A-ʢ"cQ;tBs~= \:I8K,he5UWPMcn?Xb{tǻZ?/qdӤ_&"itvٳ8f ޣH=<0wpR$!'Lj0ލ~[ ZGKtx·WV5z9p]s^z24 ˕@`<QWfnŋ͍˗S]r[5`W&P_;YCߝ; |aIP~{\_ 9ҿH16O6A:GPJgm&{d &&e Mf?ehO@+`ӣ4>?x4+޿t@Iڪo*&v8(J^7q#B4 <-dX0:dlֵŷnV0vm:tD~~&P}?3 ~srs |SG7I@{^su/?A$SSi?=7 B8zx!)֫Ͽ~LL^?8g>EIS+0ƭ |MWQN2u$NƥK we! E!-e[`|&:Ia(L ,;4VwxzBHU#TY{qMy> `Kbnu_Re%S$mO7'NiQZWDcc#1i=qy뵷@DX[ZnJݧ UDư#*2=lP2"G00w\~ZǝM? $Mnc>Ww__[ݗϙ6e?u/9$}F[ű'haZLS1?6/|~_{6iFxi9 OM4kK0̋.w,D2͕02ɽj0]v->_ng ;ٴlؕ&nsVc+K$v9vhEz#bEDOB]yjb4072$RN@;E\S&X.d*HQuĄm!egź]$7uF*5[d&O># 8GFiqR~9ӎ oABw1+ ޺̟8,4ʳviP.LR>Q~M*\c n#I32L{xu/'3U3\R; ~ۣOuҟ!~Kͩ3OD tʏ&"!s0A00{lx$0a a?-O\.% KJܞF8Ǭ/nCJŅ.8^t6ylx5ڨshL&Hf?57p\Qy(O=sd$Ƣfo_є "˴LSB#JǕ! vx-YR?lUCQP*=Ifo4R~3Th;nN  U\(2/h6;u8wUIS 0gmuEE W׶G RU4ʍAQU|HxL 嫔sfszo}MߝWx5{)hQ75(4 ʐ"Gh ~:}$I#F ؽf&8̱lU&g{kv0^df8A8S803`W՟lLlqؽBU|Z |47?1;ė1w)_~</Ĺĥt Mj<@nԫߡ\HgHsgi;mܵUqZ.ΖψBM?an~aD#:q4sV=xB)157`~{&3u'CըC0X1-oNWs"#05jjVNG rɊ2nF`S.grx7;c5 t+j~!e;{gk[k0QQO]bPocIM~/mVxfL;gM2]ை{q~7=*4vd5+_|*1N[6܁1&(PDZcljK8/L@Gf28 }τ])gT' ԷcPSO`kHu%Zxp^^ZSV9޿+Wb[?# <#fȢ!(\Z\mݻ,7D$.Trn3εV͏߃vCDS2.A&I544 4/"%(Zw˨-!ÿh*殌 ys> C/W۹_"e/sSH'B)?|Hs{H}Lم;_^| <_4*Gs?u(Z _d-ʱ때MsWy@X0 `9.sW @5 S۳:0\,3_~G+C-7H^-uM@`k.64,`79ad@&s0,=QɦU }s)xĚ{eJza*=iA/O SGG'mN0?ȡCC 'qh헀B*ݽn:}qzo~ByuB>[- u<ß8 XLAJfEC̜~b 4)n2̷&EwL>:(P!@x _C|f B Yeᨴs!GC|tҩpa$ K{9EB|hN=bdPJgHP>a& S2 К2Z8!iBHyiv@+#3ӜS} @ϑ=Rf-9y9 4$+!Eb.!/7f\ ݫK.ۦfA|]zҶ6qopNq Eo5x;~9o-|=H6@BɼH[9{zܧO"zܽ{YbD pF P:k858/%6b3OD"׉7 ȏ9ޜKG䅖G}q|2]å2-~zn cjQ ^ɬ@(wjɂW jGmL@Hc͇ B,׹ڂ5AA&:EhftR&' `d2:4c($xzш͊.FC$I TF@zn'{η/"O%m ;B׽&%noz' A9Nԙ橳Cf଻%lW~a+K~"%@ Вn>;2f@23su,0W)2\JCAfخo% WCF1~ѹh5>*7LC9b%u^Cc Cz7u*uj68 3 )3}bei[Aߍd:6Dd,f虴3+,"+!P_M2|9j{W.^n^cT/T~7ؽ[ݗ+)> Eį{$%x'uH:wLgÌ;j1J2h^hrxdn77jZ-g}l 0 26R|e"zkxoq=*:qSN =jRW#wnNuy.a@K~v6k?f +>-`LEM3th^g2.2} Vj SMcZH~!xˊ9h3N41Mb:̐bQh/}&rHldn|(*T#3-Stq{c~ĶO%H7"|<4 D)jv[y 7g=ќls|n!>BWjڒ;R_ ";j窶v[_ש6Ͽ5BޯoSۮ!:H%Zn60!@-A<|.Hxe̔`5 Fw`6<"zJ4AD+Bf|EdR2b NJsj= gdI#e8kUFF|_"- \&O߿BR#pۋ Tb*cAߟ+X<_ f_Fzk;Nb>{yJ/7.\iLǦN?R ./CBW b -!暄I2_7T]gH%akU{Q>,zbuڭc@10" scc 6n L}}Jekj5zԭ`tX6 He‡-)@0 K^Q5Դ43qھyTF-3G;8 ESRVjvX:e$ɅK~H27w3ߟl~m -̵N;\ۧ)k| {<4D:sQh/m>q3gU(4,纑mg{}ζݷRǦr_3q+z]KxЌ~^s-4 **Ku eJgNwuJ$XTQtB2%!؅@dd*(jwq O+].CLr{|NBqFj`r㯑ohFi8jpBh+Z?R}8F)O" mABs]̣>&{\wI6(=vo<6Q{e$4n_/ү~m~o;t4F3w| 7F=+einb@~WE/J9N;Z<셯HL5N_g|#r)W}NH/LRǝw6=u'6 ٨5$ u# Q"#e.CV'ЖYl$v o` g]\$thaofH` VQ1+!0,Ihy%LJL3_roҟjtZ'YO|CZ0u}\Ͳڸ{%Ơ24^xE*i'&gGO6g|9q@32T_^ TKzeyCܣ}G/A{VP0)yrWƽmʐU 5ǍcHy|yN~t{qwq`]e"_0nD#=%h^[(!O}bxM H[Z_q_x|آH=%p _:$K?D?Xw'qrړԙF űۮd]}_ޏ#)3TP4[8 u?xҧ7IҜӼ}?KJ$Im4;ǞKn7;f(]o|l8أ6hM6DžKV[v&bo g΄@8.:/[|:/ETiڱU[C*"gF% ͕&i(fXc반=xR.\NT-Lc/\S_x\ܔrqf啐ȧN\GGcBo1GÉҵ#cTy mVb|-XFVȻqo"er3J#?R]/DFa띮k~r}-G3ye"[0E5,N&ad/vۯ.~$-VPښ89Y_ok?MrJ~kBX@?tR 2G񓧚㧘ҋMs DW$b\ABL~[zt7{,j+-(f=ʣ֚o݌m9\ ?!Pw+WnP?` qiC;) ޘ/]~= m:;3~+dYU_`r Tԥu`m__-DL td21?l::5C\H8@[)caN2 9VeM||lgwuۤˤFݶK}= ~ܿ l{g2-;N5gu%\;ǀI$rq0SЖWr Cu>)1T'[O%dE0)@*Z{edUFm#ķˬ5u7N4'qrj"ܺ ah.Ϳ9bp^l;wE6-Y{Uo%u?ʢ}ẮC-R>،wHT3^ SWQK{aq=Cf>Jy24h8l$`}C #qihAYt0ħt"Xε_Q S¨KrQgP`vUyV =yk Hy Ez؃ITH3̗So"OF96ޫ#6R oYNoSoE"~,w.^h8RY ]dF㣥΂lT&a(on,AQc4*b,Pk`4 2:T\Sľʈ ije._B):TH[`=;ig&Da6,apo%~!A| ; |Lϙf!37o*!PUzO6^'gG=vG-T@yձ'Ih_ʳ3H>5wj~Ԕ`OkCP:.سN0h|}{G4("|Q5I E&1#8s\r& e^@=,aGBύk7I-3׾mbnxuFf |dM0Ab h_YDix_ZKo;5@I\D"3<7^ * fqbXTi|wmU_Bg|" Z+ԞhYu9`Di.V Gξ3$+СE]=ŁW4 _d>Iup˯ńy02|woG[:HACXs /BdlZ~*fc0` AtFaރbY5SKr2&4civY6m,oT$)duM+ca{pZٺ_9feB*/@!&p3G;' NQ/iV~;]3RTe } ?or77{(!ܤw>@X7qpqU|!Ux* `F[v^&Q $A=6#C$ɧ80qիW2*p͛FsQ,I-2CCl}&C0eQ wk16hZa!|drI#Ѵb vgQDx`LF+5wjn'PgB-;/eH2un pubR= /  QoJQ˪G=|S5?v~Dg Y..8ίR@IDATX@jQʥ^֭/upʿJ:x <|BI:Fd?o Oʏߟsh^2T{$qNyD[kuo$˕?VcNC yUPңGbquwki"Q$DkYCaU'1L<Zu rOYGڃo1W mj,ә2ޮ7PjYp\' FLy*WF]RS (瞶X&Dzٯ3Syw;j,_Ct5GAٜs`~BKmߘtPD߃9dA31[pMYO:q~&_ G$:aknirh>rp/حIW 9}tĝt]ztSNspl9%P^l$Z;02XIO E!O@ݶ/)[$ugǙ:AԳN&Z̀&kl][L@]>@*zLr szR9?@ټ4-г4#VV_E{v2;JM+ qG/`4жYZ4Iޓܧy0?2]S2نC:~)h/ "QDzٶ. Ҭ?@'~Dy4y^~*}a,?#0\( 7SLHx?FHAuҶP¤J%^Ɉ#gTn&uo%= P{A4ԋ/KP,Cc:oL@y.uH+F疳N8\J*RGM2zH&h"%Qf6 CGB9GTOTE!\( n+|d|(@Vsd\%X1ga5 S6mmŵkMͅ;ii_5 Ã629 X 7Ɉ3pdï5yݲJ 20wڊV7&ϊ&n4%e瞀e2M"$Yɘk |_~e#g=As׹gixeAI%d7Mb/9<{H}OVƨ{"4 _`(oGh3~|:cEj3!J#B/2#s[%l;Y)PnJ \ J&,. 0lWZ?Cu<0⨡J[K/-vU@W}7F_F{Z/3ΰg'( `9P2᧩Hzm~4%M* Yqz5 SƸWڃo] L\7Տ (h '@k8!ߠUeNXPKHg Ck0Caa97O^Dj=h쭛)f9[Q֖#${SH2P>9F0u?N߼$Me*PS/=L] tc"nu{=v_ b1_ON# v\9*,3OЇRcpShcHl;X"ܼK\xJtIqv h O@gkz;6c|y I{jsrf ](LJP$TUl+h)t"*TkWz)}1u%bDH Du)v! H%R0 CMgjCg g\JHG]Pf* S pc(4 dX N&tƈ8ui1>2MD4( U;d~knk ʠ_|6``(::tx9'˲QvLJ^=vحCkA&ڕV}mnNx9.)era|[$B 0Lk,hBgՂJ28{%5 5_VQ%{70 hY~ e"hׯal ]k U}+o A3smYAp s+B6͖.|Ÿ^dD Sv*Plزġy0}d77IbNNb%+J#˔߳ўU xd p{+걩VvJ6W?Uzms !A1DroqE+9=D/U1o2.6&v gJ@;R7 Sԯm"PF!XxC\WK0<_ MuWX$k RqE$}H*3_U{df=l ns!cqd,Z4a{_QyL,`sxyL[ƢjFl>\8iHKȻ2cԄe&`}ef9;yF!L!vLlAc]. ab iE;^ !h0Bߜ: b3KLgFД$7;9|x`B wMV%N>y<}<ܪ[B(,F5\Ȧۄ܋Ç渋J+ DMM%2)ܳB+T<-B,"V 1lU{Cu?U7U }#w 1OPyJxi_ǜTD򜪲10C@i0dPߕSp6O"V": Ҙ~4^]a 2PgTT?í^bVÊ] :pli->WF~ds:CgJFT^cyy$ YVT9{+t_v+%@ 3ckK0Cp.`SB 6!L_TbջwpBOUpp4PwiH/pQC * yߕ7 Dя55<[[m}P-k!d4PY 2_wbP?eRvSe~RJ# %LE%v\'CЩh \a+WCaX@j  Ӥ2:@|t 2g(y57Ү5:oqoF3?G!ctRJMvt0ВL@R.XKo. ̀D])g?aԉKhW,P:84 La.^<@_H+#]3U&`[iJܽwHӑᗸ;GVoq Qar-s 3w^䣙pMr1Vpi@)h<7A>8yw!E'^֢ x:j{~C\eQ;K O[;J 87A8;C~cدKhm_h͛,lNTb%Lvpԛ7n(5z-ER1_rls(pSrjZ2P`Ϛԏ4w?}rFPf4>-bYg;n0 (E19{_I 3Oүt}C߃}Bg!pز_ڔS,ڡd Tlk;9)dt׹2g?`0g\׽yL=?2Bo}{rLg:PKP/F\&XXkm:ZyjuV5,ǵq*؎oz4p?E`nG3lXg u}}L=>]UXݹam\F6"q|'Oaxxo>Eui2HqJ dgf&P8䜦T3$61H0ensKC~S9v}+07f'^f]Z`q cՏsF.Lk!2Tޣԇ@#ji32Gj0j2Uiy\XYXg2A5b2P?a0f(hO}&z0oPVσk\- #Ua+(=p1C44wm|i0"7Ȅ$r5 /q3$C-I"5T=?kr݈=TW +arǵR{+'t 5*?f>3 3 8Wih%  hp M"q6Q%'ՈĻ"w""7Zm^yر p?5yo*p9f1V]³v c)30LGOͰ Z΢M5d] *5-!Ë4ܖz!v[hF: 606XfQ΋ s@싦Atʣiel x3PLm{ǘ9d_ئ+um2]J$ITԅ, Q-u2߹N$$@GED {ikFpqgZls EHMÎ֣EJ&-lm@}m(m'hFd3yiBpzI.g}gD7˗.773gy~H W}d %f[o7O=.쯮 K^O oW+m(~T!G &*^F2 +nNHh]P駔W%g()83}Q6nQ܈>Y׌(wJ̐P~'6 0-܅D$O~(|nϸw~?k D&%ksר}νfhe<4P ľҽ{+RRoۑB &@FmXt4m&`#y7]暑tK*6P׉L'"Ug -WuZN6!IڬrջڇoA@2zjFD(:9 )e$ZcBuQImSO7'O@@{4?#͜3tW"eXgaZpj/.Ƿb`-?8i!ҎeMV'\q--'W \#F)Qg)\gz}!)Y6Uc&X/< ap}a+SafU|B楣>=C[ŷ> 3Wf(ز s̫S4ʢa0~i87L&dn;~ŠXXSX[oē6n  (۠IFPis#/v4kЇ~Fɮ8%+w،zyg")3Dt7= h?ڳ]rP TseL^BR)$ʈDͪTS7o{]ǔB5Y"L\ 0(}fK|䝃]Fnf eEQkgYXw)56ڒtZön}%tqE c"ap\3v; 0?/ |!{ϑ#>퓉 ,A$2n.f0,h#QLhX0~YgѠ'2lqM zE㛝{x[d3ٮM!!h7LwۿPı`vW-Re Թns8WqG#VJ i A UV{`$.osv>7h24W#M-)9v8s 1F|J?b͵)ѺRڪ޽4ַ{̳L #:Ʀ&(F- PSw}NҮUJ 8F0hFwJƴN'3lr0Uڡh\9ߊ rC =G9~ihM]GH@|a$v }؞d"> ]`V9P+Wbt[#۾afɘɂrmrBQxb?Z.Wz x?1̀HǦ+#*}0N[Wn%j3Wak./jz pd{ Y%"fѷBN"uԹҽ՜,=</ |f]F\.5@L c?ދ60 ub6C5@$3Q Eü hs2yb̀cr+2'Yz aָ1>?qY80\yy p*dLK' qZp)"H9L_o>RLAӜ8ľ84gzhg8GM#HJ]X{ !8@\!91 xf/*2f0qKFAHC5+s67`uY: \LH70d !}Y :UY# S}F:OUJaP&'v ~~f p}t?Cak,dfWکړ^o(UD<ŹZ=r\?*\CL)싑qJ5D$rzb jVbV%buVURbKTxA੢z맳$DxR5=+RG mݥt s|kuV%a3>J}՜dB>{,߰g22Iں Ϟ??s9(~rޥņ`V\HQ-f~S> ܺ!->((YLUe2[a,>d8eG=C.lS-[?F̔pbjxvdSS%;UgU^^G =Gf)mO1"|10GI3vW{lVk\9ۣ`d^$:j5J/`h7|[7O=qsqA-AXЦշZ23n#Pq RiI861aFõmz|q6Tš7 N]i͙Id*3|K2"b(222Y &nאd.=&j8g q␛= Md((@ hj~_!&P,:~MW],ϟu<+ wX&i2?{M2LW4kY`V1׾i>6~ݩVI_]F5%E(C'tĢ=ViY)e Z/g)7t$P]mᗎT` K2EL>lu{OCCz;K}boE pnԗkiO)r4xL=ҢuV\L& pA7_{)yHO7:U5t:Cb8?d?f0n k 餫RY T!S6MJ۠v` iey7Q -zyTfvN'( Q`d?)\Q[4 P'q"M& rY#[FD!ÎI>_S$WeŒޗ/_l~~!o޾HHCDʹ&XdtPub۞n>ʐx} Nޡ <~7KjOyq I-oqie\Xⵕ4C5(8 %}C ,˘d򥋗yÄ.Ynr#LĽ2Z> ~@Y@\0-NK? |![Y~O{p`_g ܯd@MXĄqڧNˬLcK%]JZ-" tjHhq&PYJ_[+pmٯL>I-fe>yMXfp"_qm ƴJKJ!rߍNyͲOsUslJMbdr3 6$\"_%H sa#Hd#ӎ-۲HJ&{*:i|lPʩz k^kCQ.;} qBnI%[U$]̣_Vy IbD9jxB< C^!dy)=u5Nv:B'0J-*`H4{wHhTћnE>^,ҥ`5gv̀E|Ә1&I+2xk5Zho=$\52q: wQ@[;2L:ŏgth@ֳZhHVԉ[aDVT'B f/9D 0MRMHصDR1y&`a*R޳7^+͓{xU| `O#~^N={Ú{1,y K`H-'Xr""@_ܾ~cs$@ G`_o O77_6~q̄J*RXHiPD!pv6i=`1ILT]3 ǟ%iL$CYXx|wu~`1p-j"1c>{븦ln4` )M[$ny1kH1;j;?F ̈5;!پ`a?S춻$C!14F& Ux\8gf;=8^|Y1p8loQݗ_:uqGyvߓMZUv;͛6bHSSKVSe#ruΞ^^٥t@4ƐYi.m:zp]BR 2gB ԈZ޹ygΛooL*_?ٜ8tէ|ܸ{csw ~a̷'HNP G{CYiǀ"\OQi\&3x?Sش2Ṟ$`T%Xo2lMo<9ze1WZ{riCt`D8ϻ?vycQIP%>)LJ@kbq[bm< i|$D2LAm֜Ptav`̭JsL UM@F;;l.{ kŮퟗs_Dl񇮝\`@s%=b 'i&RjĴ /|l<  Xޘ={fp㆛s`d܇w޵|S>?J/nOgj3_xmo>;;^u #@2#=23G n"8u$6fl3E2I3p/A^Z?δm"oXzM8o3PRKx<s2eeK'^#?[TG @W\|6?6gˆ;#` }~vT^N9&wpI)$,IJߒރ9N?׿|y?Q-:!bTTbF=Ӯq#SmȤ+¤Ty]7\f94 fnmS%v{&u^i>BhRx9v ;iSͨhAᜥT|~Y۱7BO;-b49gsaʘG1cFĝ$H*p21߶t8$0~9V$,~ F0z^~/t(aNpK9ܧ4`-E;0_i}Wm_y՘ùHy t6 π?アyW?g/_t>`i]1/´Ld5&7v4y#<持71367M!2WS<<&"cS&0Zґ`6afCԧ^J#cj>sƞ>[4DaXNFգ3յD5ӴpaLY\v߮G(s 9i ÄXWiehhr={zgNb]O^DF_\͇XxuݲҋY~n=7< 뱾_;ʡlz(7l{!X^VAzZ xyq 0kt:5ً)`"bi,HL"-jg.jaq[{6sY.C[ kvE%6gt01?gc{U&N1FNkoN{w )g|Ggi>^r!15x0,5|ͼ6gO7d^}”7l-dP lPYlפrgp?&D NBGo>̓y~ƌ/rhǍG9Q#|}l} {&l]%Lϡqf_Q8?6HX.ªŠ\\7[> ,kS!ўh Dȶk ۪s8$ݾqC.@N|pM|򬏵MrelN*i}`L3Gςn[OEKT؁ݏ-BY"% ^z0u;6ǎT/qY8$d1!7zՅ\*0>hч;<ϰ&7էTѐ[x<08^ٌ"AqHĞH!ehҊb⟿;b_t8WYvc&ֺ qkWDbRqT\Wp3nQ*$-A~o^e#C_ 79Eͳrf*W63Dv瘄ڱw1cK#yH:|5:LERky[%l]r^ ،9!؟2ڞU4ǜ7g@jH ?y^w^/||F"^\_sGTƚ+2?9F;4h ԉCp0lDHyTi]_ VR!KUWҍoES|b9xMWXl] Hت J1cY9Xb xF8õeiy!|1;G:&@3_{ZI>ϳ0vvh=*^ ! W+Bkշg z'N|tR!CϥAۤĐ{$C[7CbOmw|Y2X 8fƠlDHO 5\u|0ǭlJ^),ѹL!Kdz~YBqz' S8ekd{X+yB,`5 ڲgQg;4$// B8p)V꺺86Dr˾p|T[Lg *~nJ_#vxR\eZ9~ȝHk=#!zQti.u*yq"q6je)@(Wpj=4\y Ц0FC2] +x5j3x7$$RsT>yHo \z.sIx9 ¿1!cֱl+I~6ں<:iYXtv_[f0׃]-}7B?^wI_ f8{`pbi !`8a^]UujIlk^ϑhsmN88B F0߉T`b<)CPyI S* QI)Gs^`ݢdNB?x7skb>g D cL]ԛXDH^-;DЊC;_Ouɏ0hL$%uFоZ,DO Np$?9=5x<1zi0``>ƧR*"eq ]$ f:R>0%nTUtA=Ec'=^o>(iHFilJiyi d7F^1c秮/1@rE!7aq;KER-zJGt/~<+vl{O|X[ў% Fl#t.ZG 3z/N[9c[$8z2 /CʁP^t]^8#4Ԥ6L ZYtlTm2$MM&55MS|}edܻabGOkWӅK1 eR!q0+\ H[7!%fO&X;sP "q4B4/) *1V/NBTF J rPzI*` Vҧl#x>Ώ&I۷'si_(b gBȕ&ǿKé$kZpE8aW0w1Y 49/ݥ&6c)dɄ*$#|Uw~!V(g X0˭?gz,ڱ;=Ζ %UpB3 e!*!D #Tw\ܶ_B>0 ]lb<:lZlB0b"6%O%z1`&ƒJ/6oK/|/[ e.\j(t{jcKܺg `jgyq q!Rb牓$Syџ/X& J)GD/*ŗ_¢ ,>Q ϳKɋ|[ʄ%E._]81k]N l3'#f͑G b*+~x.H0LX2a #భKAHtp!5mm5!|q$1W^~qxb ~kp)m`8ǧ4 SP̈́ǞL&KnC$9t{NKϚY;vi/҅ &K0^G2Iy4CN?Jq\up&4;Uf498ilc>Cv Dg>LCޑJ1y"TZAW_D oATCIHPl,1`XDc"߸HRH(8Y&Ѓ G$[PŗFcL:|N,@x;SI.x7]2_J?):v L_@vs=n \(B׻~" ]_xlӾ7i`W^ L1wr5w7ovLXهYq*{b}ƂQ &hibĕ70õAp;Fyz8.&j=zgbmw\grIKSk'׳A7)[ǡE@YLbLh Aװw'غ?{pO*BJxT._(y>T57AL^m*Sb ]v^\/q%X}6ؽi5>ǣӗ/^ *h;C9>3KPhTjW|s!s=`ـ*Hr?XA5Udl~~sX;vtvy z#c a-rE"zFPX0G`'ɟ6y҆ȤP+z~|^MXHMl7&bJ/ N0|+owooݩ t2/hup |vNq [qŗcv#bѢuNI̼8Q'FXxZ{gz^>}ς|׎Ұ7 DMr[ !:O$9Rxz\]0DJSy G r!HD#1)ߓ$3ܻ7$m!rdM6;AX,i>a(Kǣ Ph$5ٵ0D|J6aBrN!&^ͷ"?}Sm}},$he$쉹( ɟ ug].RjI|("Rݣ_6+"E%ThG*<1 lDq?ɿ/AӲG`?eEfEs@1Y' "cz +/͌D^̹{D9<2҄֯o p>lQK~. h*-σ9S O/r*.re|,%+fgqTW^cP|z^>_@\ugǶw`m=om5jy1 ~<ęx& ȯry7C hn}i&V` o[fh1nft&0e&q`QO%Ԙ6c?fy j .>pnJ3=m1=֚_{K9u/-E ;h$0"Kνs-&OE3 <SѤmy ?h,/~kŦ=jsgl~/0_UXØ@DH!@M.e Chhf?NɓO."\ƼQC [M`iߝt~^;.G/g3_lkwzOҡV*k_!i2F*m y^<0x[W8P*ŏ!M5lEd:Ni+X\rھ$f@N:XŞgv~V[@?F ֺ{-.n Nj4Jm ؛HYh"@ZvuxWfBåPh<NEWJD"LbeC/cf#1!yj=T[qQ8vrPI/-؆̦kImfYz4I˞O=gmGy=Ԧړi(0&Bv#|vOOpϼ/OտnWEĀ4S]IgEZz9l91+3R'_ǃG65z*jf co{N#Y4z?[:|Y;1 M@: *iw N@Ĺ:'u4xwԙ)w%CD8Յk){}s0@7AQU}Sm%ysH^ֻd*﹓/{m!I+o}L=>/& T/dg2Hб WρhPw!bXhu|$ݝ0Tk'%pp2eX@Շoa  Of(Dc+r7k!zr >ܰ'By8GC!_zi_߼;T)CA u̲NۘB11at)֍D׾2"Ǔ^[^'A4 fiG&6iuuaF(G-MH/eXX4E0Iv\]بv|d!,FlÈCݿ>L!i7YN`Vlme1 BsDL.}ps=a&?b $ZfS !{~M 6> #, t <iU9O!K׍T Ybѣ6"AF8l5ҵT[K~<2gʫQ'$4,+ _??U~ƈ_x`ʑ:R7f4vV1ZZ0lp0PUqYITN[񵯽ymʿL䇟h5K<-Lǫ/73pJm( m<;S68 dyzm+ h&/l1ڼ q/M} => a&He_3$#eMd9sӜ-Y7ET'jgA X0]hØl4 ɁJB[Iyո8[0zoу}O=8D!>D?{֝RĨ[ $|x@"fƆ{XB(ީY=HbEImel pD0@,GpME@J@0WHl0 H=hr˞@3LV>{9)U9HV`B: >ؘki<`vB/^cw2tvohφ $(&%0/`bPOsM0] `l|_Gۍ?s Qٗ 4{O#iåiL[:ǼOx ,:)?M|8 L/4kí!DM\gmM" MB@z4BIBPϤqѽn Nߚ]zII7D1@ΑPV"gNF:@z?] f#cv #0*b|ن8B }{㹾HQLqYXE2%l6#@1kN(~Qhhqxңr UCSC Bq<>yKl1LRma|5&E}dѶ&ֵi13| Cfǣb^k_7(ni_i)I#R #h^s!YĿHtEREɑ\[,9WA1 fi5}[7P&jßpc=//OBݼ @9}1y9z&d@.5rBiߙ$jwa8m^!{c& &GN=iX!x:clw}-3ViWy)Q‹/3j4] Y)v}!$)l/_g$||jl8̃:~M #3#a\@̤9DEP]qJh!8Q98SH-q$ͭZ]0 EZT1s5 if~OZy8*Q46+A9ss,Ѕq`G?zcϢB… YEhb?k|aB[}Wη|g inji`tx}ǞOu@M1{Ƙ4 4rzzg:iPډ"2.-v,Ɂ ֲ`9Le慤6 L u8|ʄ eLTvTX[ xt{6iz(_»O.m隽6|G5$4^<|0NGImt~Y,G(6+x߸[2%Ipe$#Ĵ;X'aTןt .5٭n j90'&(4Ḋ I6Ќ؞ &j9lÉ-ࡽQ<,upZ3ʾ6nU":F߀[ jd)7fӄkhi ^=)cG(|E hN`[Q9{2>*iitit50єW ?>X˧m綏 1Om 5q޿=\WRt]8,0 #Sn3&5'߯ȥ~RI G1N,}.&q/XudVnzfWv0,$c |z :7/CD\ 1M$>Q]㹇qVBfa MoCHPb.FӏU|I8̡"a,=sL:DHW pkS͟8ծI'v/U2 "SssRkT\ҝ~JSi*lYD)Gfȼx2u璜 ?B:v(ɾ*؂H`q,yu"˜w(v|T6R^9%'aaq{z,]x[`WIUeDӸB865eȓ \g`39[.`cS[˳\|qP64j 㣑4^<0.y\v 5CW>> `o|1-&iMZ1Mm[nVx[k]9>kJ0҄&L.'}֣F>ۙisPh !% m/rq0RD7DTKN23}VrT:odrF`Bqo` و«STfzMt y:b_q;Ǩb eMϤEs[2˭Rmiaxr][&ryY1 ϥc(LCS@IXEKW*>x& s ,Lk_Yfo!1%_\$avI lSx66χ$").0 >M@cg{ZGKG AC>隝|g40V;BD@hm '6`䛊8DWA D)I:RkCcܫZDը$e&2P?pRv&p<Nz D~|! ?E1 5RV4f/hng.q4d%T/+$ 1<#ޔ^N!N߿98X8g8T+v%4^dӎ&1%A,WFV.Z}_)#>/u2#kXUǢ9JG ~"'+ rN:BH0o!of|? z.LfN\ q\i5'♘*8m  d3 zĕ^='z,r^_#-4#Q:LV?pky]:SLHW!\&g6 s/Nhg-Rϻ6vO `k}^[V64 f>wb=ŒP[Vo;Пn/t~ @;_y K80}PT౟l_!lB[F/ΛŮ_Mʼnٖ&:<1,B1#RdQ8ؓ7*u-֍4”!Tv&B=Yu[ A:ZCD '*_gϵRWӷa' QRvc!1E 7IoxLeå*~Ydc+Hٓ savFFd an.yg1tLPh} 0 98{ٜ}Lӳ!NĻ1EL|jt>Lrȑ"R؏휈86k!& \k$<mYtg?~/$)QK"ҋmbJY3&`i˙'Ug0&AaI/ q͇٫>zsςxX;fP A{^]0\ע d $rBNK4 r?;|BmQSž مryj%;SsA۳U!nؚ& n50 K=Z0ONc1(E`:V& ɤCD;bJP"$/FıC>GGjEo4R}$fKCmpqx'G={dpD%Q1R~\p8CQ$1tV!r:D'`VF`HLu:K8hs_87‡8gŗ^YRVRC&p~`;x{/˷m3xڙO:˽ݽuQ 7*tL0f2lP" q08k' }VVS%\\M][v$&8wqB1 x@x!x!ԏ9H6 1WU}ڒ.ۛ:=rN5.$9lp[S'=IC=gW"}0{=S'@$$648O7mċ3I ݓut[Wܽp|s"ػli/f& ߈ѲRڞ3>*SFJmL'87s7/c3c km6,~܇m#hϕDzI뗈ơLU\ii=)̑o7E!H/l}I~Ǘe:?ano 8\ql9ٶp2i}d*;Dťk`^Jv!='ƅ]dqr`*É5ڴG3pAUh OZ-ٚ1Ć%|w8&b]+bba ax5^kR?m$K׶˼W> R{N`9XHP3}`{mկf1.Dh>itx|Gw}k~)/?:&0c&Ae̓(LfN}BTqiz_ѕY2Y+sw2˅ $+Ţy.Mh.靷߭ͽo|󛛳1jvH1h.O  ?1>޻JWo?~~gvvbs>O%u& !8zoY*6.^<ɸzsD4qJc""i6L0Ʀ$uMҸvm$Q P٣~iȉ8,:u&zg]Q^Cmaj>\峳1?|P82U~BwٝT,î]:`c^1%vIę$1x-h{$C,UmK^R#h,7;Ms8*楗_xE39U7oo€ĴnÒ #n۞׿-mb3V)( @05;c|L̟ BwrdubpSmSWqs0L7H$u=3qeLB<:l2LW~[sς |~םGv\b_IpS{:ت]?*:v8A5_w1Y " BC̈ciD|NX 9%%.WO^#w6Rϻjs8oPH6!H:]6!PÐCOۘiqpug{MԌrY0RX1˅.:K۠J11 2#U9QAbƘ vxx;"iwǎUs' &1HQ>×I.C { 6zY_~=|^^gևZ:k6z]uqt `#IMZSp6Tg 2~KrlI3AI֬8`~MmF$ DW(IVVc2laE4Pr#!X׫[`PTR'ˏ߇aa Z TFS w& g-K3BRW5{x6Si%7!]|O;&gfw4KO 9!f[w!b&s+FiKrH}{Gri/A>&6X}ƒ: ySƋoH.}ty54xr~ D_SN f[_׹oibEo:Vz~6e?{/"KwLέv^saRNj{ ]ڊ4Z̆ǿr!&9n?YL,!!TNrOR~%d1Q)PվO:)lkǦ>i9҄9x9t} r=JԀ(1[WaOJL̓b$ƣPZi, N鷍?L4֡h#u-` oz^"|4YO*՞#ɇFE{"9,Lk=gGL"ʉ%̉F~*(iLqEU0ܽ-j~2>bB֣ϖN3qfϘ:X/F߼ o>? {?;[ q}Up#esy&[\!B.QyR噴ZL9ַ`G;Iw_  靯ZY{Zq|a)q,zAT d 2I@&Ф='"~efAKܹsum!2&A" L+&" vl#]-o@IDAT MŦr3{nDsK3_C]CsZvydƴ`HN㩯qIӬTM[ }Nn}Sm-0ɚ _8qMMC&LH/80Y&k$1?>xS6V_eٵ>;\gt(ϱvn{ww|L"Y!/;&x6V{G*!Jow I>d;}(zIH5d3%¹bg0n̥Ns$BX"DKA$GQ+]⤯c>pD޻R!mN6o6DH`C;x$qڳŽ?s*y"b#8Ӗ0=N^>2欦I >/k|l#&w&1X'bB<~5"ߖ㗐HvZa/Ȏ&"LZWv 2.k許q{UW=O||p>ПIY['#h໚HN0) WvH$pH^[xS?/ivg w/n^$d![H#3K9߂XxĂx3yedAjx%XDŽ87ݷUYB&wLbI6dx!1PIl=ߜW5|D zO⤝PhIrvVa^?d naqj(Ϥ~s&k!*ԝ7/(GڹSKzg[ce 7~P߀gan}taPo޹1јǼ:MDQ698D+8jk'\# wh`cG;6 |c(0(K_XHGkrF Y:{ v@R&s ymn۾@D=;;}`3$9/ wfA75nN!"'@.` F oKC`0ҒSgRM6Ubs mRM0dP̤޳jл!&wA ?dqD ^TTshABBO*s>p ms*!3n01Ƶ7ZQ/=Ƀ̓{;,{n xB ! o h^$}RLDv2HT1 ",Ux1*OisS!.B[%xO'iyØQד0B9CcU1 CFͫ,mM 5S>u8"5:$"ӣ; (׾yg^߼K3?| d.DV#FJgN4e;;s`(z￿W^nu[R4%w 4쨑MxCZ GFē}bBcTG1]$h 6TKЄ_IDhݿjܽFW{| ZO 6/4wC bR|) ){"Bv+fic,WDD0#'ڠ`9Ū9 I?*/+QݒX"5CPX$Ю( @^ 7LjϤ.l' aR$5퉶5Q u&DPExwS:Kg#&5b 7 SV5~ 1R.gP3dK%R}>ܝLP?G{rNRa q,Leq2eK|,­_wF IWںuNJzɝ z]ͫ|$EE& ф&4 I$JS0 Hotͅ;fSf*?=.Fa(No΁W d$jÛ B@,%#q( AkWÙH!θ?HKR]@ xr ԕֱ+"2lT#CE;8pYI#j lIϕNQ:݂yg0&đM'M<8to !]0jE8s| ޢs1~̟gt le b"_)ii(" WH(@k s֙b X!6/ҟ·=kdz=v$[̑P>/n/)ٺ9lU8:bEICt!ux&='YlY !1 I460fk l?nB,yE{֟>!{Ru??@H0kUi~k>'8No BǶͅk浆r,%dH0&@LJx&W?X8L;Heή弒KZ*a SYF݈^ B"*J2U }Mu"$eh@p]lyhUQNLJ~cW%^mD?Tjs0!xi{{=xy`RLDh=RΨ+ |d`?[pU0(p m߆iGᚇ99G>mp+dfTKJS/uj̹W릻%"lᤆI9p0M0}h#d_:6|0qoO*`BH{$c9B:BDYu-:оT/V@zFHᆈbňw_UA _Rj\K[ ^G՝Vc=u$:ۢw)5R~a2*`bZ`O ?4! 8&uxʵ+hパ3;̩ƨ͡8OjMNDNp`p`Q?bW]NshMi3у=wfG̲mÆI?iaͣ4OGo<`p냛wvҊ@}n~B8wv{}o$뾻R )J 3q e ԢB%z 3jM>Wfܫ9f (IZ \/½0IA֣{,D^pDwR3t?Ʀјa$l P`//pA Ө1.kN4Y~^rhKʏ͵ݏx̃5&s"*~UDx=OE4 k<ca9cs|O8Z]1eTLjy0ltr/LcYGXupYK89P,:~B o9l'ã!G&CAV)@ ί&z/8˧/w/ȧܮkZvMF粤ZK:؄Te&_?D"]Iߙ𮅘@!b҈߯TKUSٲ0nׯ\@7FDHn5= Yb&Soz$ 8q$=G; 8ز|B.8D㐪K?;$I-3OSK;^s:)L ko42S˅ļ`ӑS?P<ʰ76ۏH\`|rC>x؜%4a~eN0Q+|_ y`vňBE7s\wŪ] R32~>0aޘ[iEOIg*|lovέp7s;-xJ/:wz631vtEG |a6:p17[88"۷k"HNG;CžnzpCdhZ<܇Tu7ш17 b&QC!Te@a#qvl|S&eޏSkh0-d '-H5OH1+0Rޞ~,}GoYMmgZd| /aA(C}gO9:$ZTOXHhs #Ld"U(,"3kL簡(_},V 'HjI@wc_l}섌`e;`))>\x='? i|/|^0 ;臵yz6{ 睐*xTHlS iF:!Nl ]y!QٖoN&6ĉՌK}T!pBp,@u2aLzfe5 JXIc>$tb [Ӓd38pKN>]-`˳%#X1N̾1񙐾%ʃ =sHn Ώd' 9^[Sϧ#KQ,>>yf q ?P9>;io@PFÇ>4 ^8V& L[^h(Pv ha0S?QyUVuBAi'G:} e֠CvӂrO=.8kL:~v1-3>lRChwI~98Ӻ/%cB3h,4.[I~F1.v\}7B7kSGI3@=o*+ ., ^jo{yDkq`ŠИ:n1vOZF: 21S@"bO*XkB V9gү:0hJ5eU7i${Y0i2Tld;yj9@h t2kL|g- Ɛ)0E|W5uwHa <`_y?ƣmF9wu BPVNct(韺cIVڈBRaiӔ;M)~ocHopejK@TrFp>O>Yq4 *)υ{:3UؐЮUL% J6#-o.{(4v>P# 3#=<b}:uLt_ϖPDW3r\/sXUNhlcF`&"R"Y|@ ?:jɑy5H.HG\Ʊԏ,oLgk#cLnj2w xb7Kk'9 Ys_~'^~mbO8^kg>AX'2ݧU Y,ܝ!O|OCy"gSĵ QMyV=`Do;Zv[kU0: XIt_ٛzhk_rރa8Ֆ2CVGΘh ٔVߴnI?'^Z6oN)Nԧa;we+@Ӽok9:;tו(KC TETh1#>F! HoFU?Ld$:#ve[ȉTZ%H7'nzhw1<}t:qۮ@zRt,&*J*/}>Zŀfc=sC%ICZꄙaM "6Z]0"Ĩ@[5URw}é%@Fhj s&HHfFk 8Ѣ.\1s2PvJHpљ| 31('2bHE>{._7cؚ/LK7Yõ3lb1'51c*zh9>e̿RfP6oC^xOwYsd<J[` :c3vX]0.32K&1m[lqH^F%\ZQfs!sY; :Wb`ǛjeCy}`Bu ضk@VX5)m{Kks6ey,Ɨx!S $wnX@'[ gH&} 1b-&D}mM !>g [yׄTdۖ|ᔑb/drԜ-YK;v0@_N6kIf^g[ᨎ!~q6"U!F)*mRssʥvT.i6^Z 0~®3V?VkJ6FllȯEdDtP)m@?'1:CBj3VP{-FM)#Zx]i N|~9&R-h|rJ_}>k0~##؄25&鼅OWo?l7ͅ+ӕTa`5-C~>Q>}Cx:/iCV_ t07?RkgIuܻYQ!isޞ齐^r RsWSWɗi=!mO0&r_|LIx=W./T-Ö洑RQs̫oٖ 8VFJwH(ShJ }"'xGy>or_ǘbT}S_Cϻ^ͺ$oi-VFz A&iN{էk1NF gYTC1+MvO;k LB[Ic {kdʥNݘv6Æ`aq*+ R휲%i6m&c(K Cw U~ǠM L>#9&`S?Q/;ͻ*&>~OY( uE; AXozsKoθqpQU'X~ =)߲?_V6'hu%P5c?, %>E-lXZJ!2DkB@ KrFsd2<<'9o 4+CX v1Fc$`>QvFShr?䭁 Dzl ڹzL PWbdz@ M c ;]@rL@c %jA݈6LgMiH\=hmZ`(G$` tMOG~ h+gR.,]\$n3#B7l@8-B=MN~x< 7+\כf a+򓸊a]js4wäC|_]/~1MGRp~'p!iAbZ!B1OJ n H_~*ޥs)ZkH3`)%E@欝jA "&+M:``b ˱ |My9v C8)DK`76frqq[Rn`U(G"Yj0 \ S۝m5$u-b"e\;>pss)6 us"I޸RQr_6F)!P,miG̦+`OʨϊI6%tYdObʐv$l? $-Xj|(y -"y ;Rl֑ zaowؘ0]w3 hvCa퍡SO>UU<`IYBG q @ej1FiXa?C m ZC) +δŸvqގ,+ujm_^It5RI`cPt!2a3rfװ;R*/P_1gmr:/*slT^B8%, {d>30 *)s u{Hcj;W0jZ LYǼ>ۃ3̚Ħivd1cӹ,} ^4e5 CHbfVK=V^;8Y?'$ BjCS/RL/WZGeyu(h'3;ڏģy/*ū& zůa < )ʦ Z@_ܷf{H _./#1TPuu} o}._nBws1:nRZO'"͗w^Z#-3-KtHƿ LOD[_$yV!b3fQIbSy ð Z|P}Uܛ|$kQG{:9{s\zЂ JC+qϾFVCߢVLV:\f;X8XƸRLf*F}(C[;1(i#tlbY Z`ie`;)3McFe0gz 4Zy6~OU伟YN]&}.z<$+ijEi߬0LP}+_^5?(:^LqиH2*. nUOSLU^#`FsZ ~%ڀ *׎<'?]e|RzeI ҉6϶`v B5D~xAƂm@ 2MR7qV#JZc픎Ɂ|VIT,4OxI` ʍ[ߕI4柵S1[u4 ?#F3ȦnRoX煛b_HK+OSC$2;K{} Ùh:{WHiZH0ǏIȱaLj˳8b FL#͖ Jݗ鸀VTe2slI1ocMpx  ])QFc`y_ 3s_=4&0SǯWxCR!w®; hָ/iC"Rn\xFlv3v`-EϤoҙd5LI0B 6EBfH3On9sKw}+K);hE 3 O@?]61!LԌ2ZVZXsi=*1tH RBaeٴ: εHfnO"z` Ð`^'i_icAV~޽%: x5g܉,%М ȻII_H^qCl {!ЀL=c?T~3V/7mwi~k¥o^Fb-1n<ǔһm QU7^>J"\$dڈ /)Ae E^vi8-Yr&4\K؀SR#)͠Kd(|H3J!`-wS 'f!f3,\fd4$^: C@ի ^@21^ˀA϶i{-Ea̟[âֳ?FF>p`Ij2DUKrX{h,ܩǎgxp˻I?=ga<^vl0hU_=u!G=hrơ]IH 6y)ϣ)+?W?Oި3>ZYa60 F1(/1XWT/QE5hF9P>k}.8}"ؒ,݄7%e/ՔE9:z1E4F`cQh`j6ٷh_b(WU80ޕwW#MۓyW$C6JleqwtͮZV}M*+~K(oc!pyh60ZYϳ* Ek;z}d M9m,{/0d9xiNcD0oj練}qpT,v_ڟק%8ZOTj7UWy~=/'Dž>}5urG q7,\e+!k a Ham~5 2#kC`%pMYхs-AVmʘ#Gc4s;D?0M8mc1 dADRY_ ECc9ތa~ )bf4ċJHbBYsMNPZB+s02C@0 0Z·CHpMퟲ h/ ~ժG#+SqL%ɵߩҤ3^JZ#mJNpqf1 P_0I fam -l:ud*3&4 Mo"Wт=Z6˔Cl34C}:c)]l;ɭ*,NdyVb1eNP. CHQ>rtoƿ^3po{>I s Y#pG?[eߋlVbQ;?x5Rƭ1]̪?8[e3%[V|!Dzvg)=۲ {QcG+U<JNR@7R41Ѻک%%@=@jrdY5}7&@B:$^_Ko8gVВ,׽$,:0qysK 1Zk"|1>]fgvPvp.yP=[LkfJ0qU)6ߔx:SmG~v;^~Tx':%ܘ~O^4<㛋Yy. &0c9'S.x%~B-ƅw{}7{Z- IDATTTX\uhI+Y~-S=^YC,Χk^Vn ` m2*dvᅲ:7ϸ}wjW${}|1aH_km@rhy<2ujac_hDE B5/{1M@֯d*OkS&v_06׀ME:?ihj6xkGyޚ?d Ua=w|u&{SfbO @rcCgO.~o3#>qVxgl4<ҁګ4dSC #vB{ҭj'pNL4}k" 2#dMsf_Z*-[/)1k Yۉ+׾,Ûq/Sjj}g*zbBc+MC(! qʦ9C[ rM6À2B ޴/#5z8MD\L;a{}ONv}S'H8ќSXKŗU!yT04ܣJAg݂b'+xZPq!L5:#f`P{CMgj [ 4` |!wN~//|wIlE@ 5NS`8ATIeTӀMa$,VvE5]f!2dʽr9SfRȼ}OI2Z9lU ,;@ߞ%njvV 2y@5)+ɞJf6W`D򲵙m`W'a"à8O~+!ܘcwFzRhM5ToBwN[a-cu{:huY$/Rw)SI}Z&ESʼwL:W⑯|%O-& =&~_f@>\&&{P/<KeY1fF |0!ܧhYZWi5x ( ЊiDU`wN6ԌA0ֽA!"/elY C8B,EN8?pfȁ.|PS,QUboz0ư3¯B4C} sPiξB AII3:#}c?PKUSiȓ݂04 FJpGx|_aq]ByZPbvK]iyԓ5+?m-W㹔$o?O3s}4/4w y-<"Be 4C??_&`30P[()F50<ȓE@~3ftsƜHc,.%/߭;3 X3N~9[/Sf!J'*+YÅ- Pߖ%ƀ ,= bLƐ|GC CL`H L󞣲ƐlIJ^ ɐ2&EҀL th_CflͦV>{LuBO m$ݻ`$j)r1/{F cK_ # 0ʋ@[=ǣoTHE32]{Bol$U)iwCf}5&`8/|f`6Pk|O#EJk@BRG 8IM9zwÑǞ?VD ("` )la|;5YhKr/zmsLJh^IǀNQOIfG=K: r-coL#!]~]Q7vl2ʻk$1DquZHW`63`l" gr¨0+&- .+c{gY"y3}48DU_վpCz)ufh<auvb1q";apH ˹@@?K?7<g$T%*zS @ Ynf!@#' /'?vw>OK a[:< xeO^\ql'6?Z[b2< FqiXaA|h=o[|8yVbYonGLSTUvUN i1cgň7`d3<%-W&x5QW{qLe1n=ds!ߴflYΤ a<(а`Gi%=m%J/ F-MqJoJ^DS秌ꧤskO~QO{:?ހC6<+/>QUUnEv(}iLgFYÀ`|>e:@ݐ! #,uv|$>&(!w ؔ>E@%8-!m<ܟ5%^ެib@1?إl,2*/> w&L Ҹ& P}ģ6>mdȐF%Q[1Lֻs?ջll6+d&3'O` Zo8zWc 1?ac: {\Mf!J>[뉛۞g ]h#?3FSK~}3z54CI\٩N2=慀wut=ٱ`k\4@IeoP66PbЋc Qk;&9 ι;~ׯhM#-hH~.eY# ouCo |\ s?ݜPLh@3 ~mx0q?_no~9Z Om.=y%C4pr!5Kg X;F!K“;~s-՞93*8M:CmG1!0Ma -4+v,#2uI-'%./?=H:Q݃<4mAP^TYmעԍѵ0"22l0^c-hJ3& U=&/S@<Py(O1WL*!sR_AXt=迉:^`fBoE0\C[Tٔo `f>fzL H1 v+R"SN%# Mk3041c 8ʓkXMZ+}y8I\=ɟv@ 7(S17ҳh^PKrI˘+ElT~co5ß2\D#CHf(Ps3є0>H6 ([HѶ jAPTs~i$`pB1̉S{7.r?OXpo3*O@̋7;eM@Z3g P"_8ʗdLwt![IEZXF¤Y@ƗMfyGe Ub(a#ƷK:"h@\# ~"fMR~ϕMWbݷ>g 1;4KTo&Ya ,5~0(OG׹~cu~:T>~ך/㨣GGYWUf~}ط]H;rhZ<)~Y_~||h:|0@aML@R[v&0Cޏ(r]nK$g>Xk8&@u!@#dSL E$ 0 u0 XW[ mg*Ç\6;A($=P5vLl`b7+@7Vjtmj1 ?rK*= khBBec)*WBIq+rܷ4dOKPo;LTmhc%,,Чp@{4FROɽ]LR';}dˠow}<+5r a?霁we&Y(4 `fζB<#4g?[6Kӕ;r>0GX%P96U|$5!T 1>y.1u 33l1Sp+Tsߜ2N)e1D aoW;c^HWD){@e)LWǒ_ q5C@R3$&;g Kgx=S0K Fk0;))WkT"yU~# ч|? |zb| |jTa^?[Rvx L1@3f3Z/oCsEENWƫ3i: ֹ[HY`d2l5XX{CYg#]# ea{K҇)PH i&axyZ`r.RTg -S/Eg$Q3S<hݮ@'>ώ帰t)/NbI=^JvL`f=Gs'=Q+2w) P4ˮGJL@k6CCH?F@n[O}*{ [fިheLƀ_[h_j8wkkH!mB]hɬ_ N b:ed>i )m2 >?c|5҆(4R1ZB,?p2cTNV7/EN\L dN~i0myXC:x|0^qlsi#>JV 16uƎ~8[.h'DB!d֠ހ﴾J,.lhŅzs3@x 7h-C sL@c}!Fg֟\>q4IN13`t"H<z3sac+مFEH5uW ƒc&eZx#Go LQF0!wI՞_}Ҙs'r&D}aG|Wkkh-h62j >1X}-i^Rp!hOeCCod:4 z! _?o෴P~:̷[dzHЌgL;T#'op?`ߗ2*u M9!1 < (CX䞡rc㐏11Cf9mnck۲gZK2F TeH~VQ ע$IĹz>q&ӈ OO;v14@Q )y+RR*{JHyr-Ʃ跟}M2۹42 ߿ |~C: Ծ@ ,3׳&zS(ѬEC H'}ψ2(Eu舁pAB;(iVZ-G2MECu{j%^N2= bTsқ@|ش&~j؅HB3 YaHYza$9ea/$c0oQv1= j+ HB|e7e%wiÐ/dAc)H=oRϮ7b˿nߠP!+s^[ bUpu\8f8dסܒUob!-'ڡYhq*xTnq_cJ ^ +X] j/@,9k_ !YGB ՚s!4  ޤ_96Hjܟx$n^k'Q$!TuNk%X1*g߶[y3oZ.Šjѣѯ _b͠u_؀ﰙ@JX \C-u@hx 9\Az>`/")C1GrbAI r-%NHp I [B#Jz4VϽ V)]3)N-i&ud+) ȞɿO"Z@ w3 }HM0&}L OUXBh'Cȋ0ƻ'Yp>w>/z ~2*@%H\ߵxeFL@3B HT M;` vb#HGsHg 4GP2F6à, ZAQ3E$:%' H1u{EgW7Z i/4> ǒٗ7Y]/қ={3_{47oI y@?/liqxD+.,6ۻҽgu!b5#Br;A?"@v}AJ`:!xO=h%p5NI}2k=VFfj>;ze$u;{Ff0=;."L<-` o_ϡx_o*{?~4rmKW4 6E/"5;l! A.69mU\>ksƹ?31r}W{V垷e[1c[Hh,.: e$La-'Zr{S0;*/ڒHdXt 2W{<@w\uŅ OrӸkW<PI.jЏi=A@(m@zgw&y߮;HƇďG~, 9«s I`ၱ<&%텽{>GO8aLTovi{9o@P:p\*a L~v˒U@z߀_uO癬;|/tHDI?`p fLpXD9.LJ " ҖA>_{.&{/JEy_1_*ٟ8`fF} o̜yv.Op,A>ݑzNxo0;v٣Em 4-kryG pb۶W3y׉dB+ ~9*}.e@Π[_ Vyo}auDžr8Rvníy1k` 封ـ||opv/ܲ7 f$0 #}aYeF[M5l }W~iysg#-9ӹ`>3Yyz ܾp3: Ҏ s\^ם%[/~~}4U@ |p gwC/_w>k!!b{ki]GZo 9\bӅsFx|o)}wVN *fWaMUH7X6kwiv`\jmP 4_U^N[_2qsu8$-.G `{λCծӅ\1f t\}oם6`9˰vyP] [b CF` wWϿu\4!םahu6 wߪLzv9m.Wv\JT/`=r,H7LF?-S8Ai}}o=UiE;MÌpu@9sg۪~績rH]{6+chmFN׫~[uOu8?Ua^(gg\&mrܽsZ_/:>_s}N_phE6X4ŕ#d 㝶*}-qyʳӺ ~V׀r?q&o}UaoxV-m6b[`Yc_/ U^|{^V7AUNR8}Wzv{q{-$oJ$מA%Pڵv|Ϝw͠@up~[^U7FЙ-|i%g;.{FʵA ܜ*>] x^ގ/_4[fo~[d U ؎mWȾ ntsgvrrg}^.wo?~]kdi^)|<:mZzpyIooiǫrfe }a?|v}OVegosr^}o~=r\&zWa2X|t|OlUZ΀:t[sڪ;m)nh6{m U@ Q} U~ִE~hnjs_ˮ)Jo^wxy`z k.52U]fpU`zmj['6u⺔;`o{9y4IENDB`doomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/shaders.dei0000664000175000017500000000163312641367671030656 0ustar jaakkojaakko# Core Set of Shaders # # In each "shader" block, there can be: # - path: path to both the .vsh and .fsh files (omit extension: # "shaders/test" => shaders/test.vsh, shaders/test.fsh) # - path.vertex: path to the vertex shader file # - path.fragment: path to the fragment shader file # - vertex: source of the vertex shader # - fragment: source of the fragment shader @include @include group vr { group oculusrift { shader barrel { vertex = " #version 120 attribute highp vec4 aVertex; attribute highp vec2 aUV; varying highp vec2 vTexCoord; void main() { gl_Position = aVertex; vTexCoord = aUV; }" path.fragment = "shaders/oculusrift-barrel.fsh" } } } doomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/Info0000664000175000017500000000014012641367671027350 0ustar jaakkojaakkotitle: Application Framework Test version: 1.0 license: GPL 3+ tags: test importPath doomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/0000775000175000017500000000000012641367671032161 5ustar jaakkojaakko././@LongLink0000644000000000000000000000015100000000000011600 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/rules.deidoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/rules0000664000175000017500000000321012641367671033232 0ustar jaakkojaakko# Length rules for the default UI style script { import DisplayMode UNIT = 4.0 * DisplayMode.DPI_FACTOR } rule unit { constant $= UNIT } rule halfunit { constant $= UNIT / 2 } rule gap { constant $= UNIT * 3 } rule glow { constant $= UNIT * 25 } group label { rule gap { constant $= gap.constant / 2 } } rule scrollarea.bar { constant $= UNIT } group document { rule progress { constant $= UNIT * 30 } rule popup.width { constant $= UNIT * 120 } } group editor { rule width { constant $= UNIT * 55 } rule completion.height { constant $= UNIT * 100 } } group progress { rule textgap { constant $= gap.constant } } group slider { rule width { constant $= UNIT * 55 } rule label { constant $= UNIT * 20 } rule editor { constant $= UNIT * 20 } } group dialog { rule gap { constant $= UNIT * 2 } rule about.width { constant $= UNIT * 80 } rule message.width { constant $= UNIT * 140 } rule download.width { constant $= UNIT * 115 } rule multiplayer.width { constant $= UNIT * 80 } } group alerts { rule width { constant $= UNIT * 100 } } group sidebar { rule width { constant $= UNIT * 80 } } group console { rule width { constant $= UNIT * 125 } group commandline { rule width.min { constant $= UNIT * 25 } rule width.max { constant $= UNIT * 75 } } } group gameselection { rule max.width { constant $= UNIT * 215 } rule max.height { constant $= UNIT * 215 } } group coloradjustment { rule slider { constant $= slider.width.constant * 1.36 } } group rendererappearance { rule width { constant $= UNIT * 100 } } ././@LongLink0000644000000000000000000000015100000000000011600 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graphics/doomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graph0000775000175000017500000000000012641367671033203 5ustar jaakkojaakko././@LongLink0000644000000000000000000000017400000000000011605 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graphics/toggle-onoff@2x.pngdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graph0000664000175000017500000001052512641367671033210 0ustar jaakkojaakkoPNG  IHDR8 G AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs\|iTXtXML:com.adobe.xmp 1 5 1/2 1 1/2 56 1 32 2013-08-23T13:08:09 Pixelmator 2.2 7 JIDATX Y;N@ _%g;p$Eo5Gd%=΄qVt+Yòpo}[tof;858wu8|Y,$ѵ*!V&*3%Jm 7=DLl(st8(~0gMm %b TR[~h-\F=8NZ8rbbmBB25\@fUØSu<.3N`ܨVt3H,Q6u6yLW'PX΄K]KjAa54D,&Kn#ߠJM]G[?)*Gkrڐ&=@jlr Z$ :qZ@}Q_ʸ9S7u|ށA_à+Ԡd73u6Ivc-IENDB`././@LongLink0000644000000000000000000000017100000000000011602 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graphics/toggle-onoff.pngdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graph0000664000175000017500000000763012641367671033213 0ustar jaakkojaakkoPNG  IHDR AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  "iTXtXML:com.adobe.xmp 2013-08-10T16:08:99 Pixelmator 2.2 1 72 5 1 72 28 1 16 `*IDAT8핁 0EMq; 5@clNbtPDsB9CDsKHQ\/أB|2)T2ČfkzD >4c&6hq~LC )WN*e[6"[GF-zZ$=Epzm˖-\_[k_v)}̽IENDB`././@LongLink0000644000000000000000000000017200000000000011603 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graphics/progress-mini.pngdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graph0000664000175000017500000001167712641367671033221 0ustar jaakkojaakkoPNG  IHDR((m AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  7t"iTXtXML:com.adobe.xmp 1 5 85 1 85 40 1 40 2014-03-01T13:03:93 Pixelmator 3.1 v]IDATX ͘[UU=cݽfI"L((zn"^}Q PiC]]H2#FS3F5{}nC׿׿.{ʬHZțT*Sy]7J72v!,ҋIχspd$᳤KGz`,AOQ>1milƖcu[A⥭mv՝=9t;)87mp[9EïQX4.rYO2`N/x^pk fZɺ#*j@>]:9s/_Jh{O`UFRGy4q3Ih&oIb%pϕ{Ȏ}i)oc\ށ!Hr^7B| S^k1 ǷpOHPP(Apjra ,mlvۘtW* )X  Zo??8jK0~fV]@X R/Nfw*geOa]`dQ&ѨDG^ D3ҿs(^> ,5ȇxx(h*K  y{^$CShT82&f`9 k>U [`'xeC:[J`343';0~+հ -cA\`eH=ބIh&v-eG1{`-/mIޅ&F6)C j %kT:sƠAg΅hѳ\f%3曉{:ιI'`?Zr4g7xFιI52@E80x <[YeXmF彰|󖁷BgÐ?-}iґZI1թUG:i^IkD(#`;u u2gu6/sS87aFz%IENDB`././@LongLink0000644000000000000000000000017200000000000011603 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graphics/progress-gear.pngdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graph0000664000175000017500000002237112641367671033212 0ustar jaakkojaakkoPNG  IHDR\rfgAMAaLAiCCPICC ProfilexmOhAƿшV""sM/IVbL[Q,fdmAD,śVzQAAXB-Mvc ;}͛ ,ha@H;aM'S:6f pHYs  niTXtXML:com.adobe.xmp ? A IDATx fSǓ$dfuFn\rdtQD{tPr)F%rH\¸۸ s~_3y}߽~߽=okg{z=̜953`diV۵6f@ $l|W  Hp0 3`]u3`6`f afm $̀@ẁۀH;瀞;30v U7vnf a6n@ $l|W  Hp0 3`]u3`6`f afm $̀@ẁۀH;瀞;30v U7vnf a6n@ JMhU mXV˵.뇄_ ?}K 3s̔_׺/:~aQktw?'\'>Kπ@a)YGX/ t_RW l%_ӽW + 8G˄kKEaƨX tv:BLTCJs Hy^7 [ƭ#" T*CM-MA63_أ`:FW C\* XȀW k (N)!N~Tapp`0v&-t}y۽a\T'u=m6sŽgO mZO6vc)=Yx}#SW_xXQ`!INc);z ݧ/%ܷg1.FO̺c3k8v^7=Ο`j zmDY6_<Y=g`mDn-8N@W#jTJ );~5JK X09? D!KUӔv5Sʎ=oK8I[oI;KO`lΉ+2\XS"TQ%9 l/ ͹UKB# -ܣNN(,<] Xeɀ@>x':#'xwu5akanOWk9%XI)^\b3 )aeydS΁C 'OӋBKVŗBL! -A*\n Aԓy^-!l((!w "s-zߤ𭄻."T8GiPu᩼Qa) !n&-}0`Й4:y7ē~ ;'xo^(8:v"B,NR;->NN;띙 kht\$&L㕤= Iˣ"\L,x5aџv1rITD**Ȁ5yZ؈%'vse>'9W3Ҡ3vrWcU/ kKz%vH~+ b*121[p_U ?-_)莑+/IIW/3Khv-R')7"k+ oXSC`7!II1_<N@^M❪ *t } k<XINRvlFcU~K1xb&!,],+չdW}snT@O~:;OLˈ]C}BQaK!d$EBwQ%Y{ ?e[ge,q3bcꐄ>.>iKRLpe<$Wʨw2(P(.2: /Jg2E]P\5#/83UzwX ¸e`aR +2@TX'K6´t[e>84s0_BP /F>`TOBp C#I OV\}Mv䣅J F1AsZ3v-IB?ۏ]_'36D}V?ڿ)wSW8k,],0%/I^Txj|(s1D0sV`Rؙ %},6/4BbSBUA֏\I-Yn1e)c]iTLD~9{M`G`Q^nMt#ŪY~1m᭱SOC"9{\($XvĞdI)ⅲXe+ObB,yn֒ `G]c o&NlXz@TF!+{bWKq CO<\Ѵ `W식cX):d=}r"l3αvQ,eă@2X:_i^ sC^v1dM)Y-AhNm͟Rae=S0g:?$F Đb(&o0Yr"}kë_R +-X Eɟi#T}sTU 9J  CV6l:brufX0vяܩD'u [Ix^ExD` aKpڣ"9,IWY\@oP\ MemIg^ۅS%*;Ki.hyMZv~5u|A=B趷^/6NxW$%~PSQ2V %tϝ&,}] õ:ؼa /(=z Uz=p`aVa ,&,Y Nh/Y"J{d=VM|PdhxbPH n UI1@{dZ0>+RUë6q+E :"=k)Uw2O i2@dV?/ZxEl=VO,Vk'Uv5#0g57B6VPQ/QYō2*XXڇʐ.͊p3ЃKF;ZG.),, Ty\?U"9 `snV ? SǷ,(Ñ\@tpk@Z6RɁ: p+)!! oVJ踗 !S';< 8о_JTF:x ̓ߝ&, ЎBGO7Y,'K )@i ($@ZsUnSj%1)Kf Fb$*X7}ĭ`3@u !x9ms';`[A!=I) ђV Ȋ2Ɋ$H,p6.';Ս/TMNZ3@}ns: ^VR^C` : Nm%Ng]e=4W++djLrRRu; \ yn0;/ySwV|}yί(o{Z TH?@(,(< ᯭB!:oP;Dxkw} 1]]`SW0( bDʒ?fERxUց$: 3@{ozZ׬/GI6y<#VC%|-4O:+bε [U#:]U@rU\?ҭTq= (@v%W)BVV>QëPӥcz"]h?^t^eV:Ⱦ"ȮtXE i?ppP Īno%]7 Mxf`6w<8RDhk_.hq<YL@ *HMK8RexE$jb $*;H`%T>$*qnBJqHAiX;_=VM#e`1Q@ a5wl Exh'yVkODeb$]Vlh'P`JL_mVYL@-=eq 3Љ19UXS`{w*>ZT'zF/uH >bt~9[l秀uPUZ/2MW c`qUVaU.γTV0ۅ_EbI4TV~!E.OH^Έ &F 1lH\*p. P 1bgv@{O~K9X9=\+6n勔biRT2 [ kn>,Vz6Nޛ>ҽbbsN#dK#Ϫ=m@_'𵠷G).**XkA|d#aR>#])xVUD6PpS^̈i u#zv=-ER6(=umo$ycO6K6WdErx#;)epc1woMoQ w0*b ]'Y?%#?f {7 xezȻK=*jZ11)o,"_K 4s>@'T]r=Ycݣ |m:P>S u5lEd=` .T BbU2dU,oH#.KгJȔ_YBca[Rc+ T 5@S!XÎRTjҗVЬeqZ `P$>&Ye%Uюioq b`2`) Ń֙"H~׍3Gt݅ 'tj1_P_24O7X2GpG#&!F>E>e_q-QSwkB <sJNΛ' ]p%/(#;")<)N2O%4؅JǺ I`d9G(wOf Lv:%|/U^u,8˶F7-+Kc`1iƖȁ`a4SXYOxZ`zv_I?^ ,5J+WEg5Şq9':w73a!'AS\XUF}Kh|+"x,ދtWWˇXkt秢)65Ty|D%]~#,QU3|{^9IMc):"\+–b;RQ~9ja'&J%^ඒ˫ɿxQy%xOx߼"#̫Rt7rl=_x됭SLB >P|KRtO ˖+wY<,0rXhTQ42U ' 0:f?_"÷"] Ͱ>o8߄7bd);aM'S:6f pHYs  niTXtXML:com.adobe.xmp ? A IDATxU % %E @Z(B4 i"E@ P)"#DiB E@$ Z7ݻef;3<)w^SLp:S;^jW3 V`vr/@>s0 罻0}kva}9>- Ǘi1X  y*io Es+2 =, +E@ix2x<9wG-# h X,']%pkI. 2@Ox;BB RR@:BM2jί 6zһuW@rrt[SWOa{`e&[44N_03>݌p`+lЭ ۂ[2 32p;p3Rlִt h@@ n pкx[ 4&[z #+Z*hQww񅛇(#@ L"+u7 BTsU[BѣyȔЌno& h"W?je_g9n۾Z?^ j\ &/sƃ!_C#h&Kw&'X`e?*SӀ tiΎ0-nW6x Em\Nw"A'.ީ._pwXN/t ԩ@l#6kE>UA/=p}~>d>U 0+4'PH,uܟO͈3$zcQ։@hhs@O,pc*09a~ g:, Z/׏"ǀ{$$r:+.TYa@.E @ʯ B,m#4Ȓ:I],M#wX:+~C2̇f4Fh-BxC>r1ΌɉZIr$"X |Γ5#Cz3 ޭ&q|-(n@nVˇmت`:j5Mگ>\"+,a gG$ֿĹ׹]) 4ڰP%z+|8MJbuwNs/ET`:ɰ`?n0l%:q ueW \NFn#& 8rY.R<@%jQ[<չ]+w a!wk``8Mj*&a+Pd4~X 8!0OCA* H-vBղ_=/Q T²% |CjhO?N`e7@#Vj:OP}! NQ:y6 no\eX`u#-ӏao Eq9+P4#6Ѩ0+P(dh`J$4 A Eq ? Y05_GWp ts\ |_`BLlTׯ) n+M=4l}&!N=z@[^ q<+ ܌Pa^ٰQ8+I Aa6*ᙵW=<"ssku+ W@cg1VZ `~qN_~\W # eİfZ@k㞚0V[c$A]B(ـJh2jOn+КoMkh,lHf&迦F0v6%͋wcF\\ՁZ<ԟU,x4W 2(p,ZS`aʸB8+WTԚ0YEhĪz<+4Q1WT;ouGϣ@w=VV;I{ĺ\T3 tVZm&? ՛|@HpPY`1bV;cHOc+P_envx:9!9 +Q T-ԵgJ*[q +#69~} =xÐ+V:5ݬ Xo4#u\ wy>h + )0ވJ{}K[k2kTfqrޯWkU:y S-tW`jU[+iy#W0RI~ʋ*[seW 4@X4׷[, p\n LlR+"Z [ػ$ =x/HU +`UݪtBpj+`U@WTD+Ё Xխr]tzY TX9\)`UuX@V4ս<@F[^,~ L:ip[5@b|V^*A6<+PH,H]ńYe2a6<+P8}e@?ea| _+`Uuܻ  YuhU&ѣSHqweD8u @:gp Xխ-2rګUݪ*/ +o4 sW(+`e\+;WPZjU&-sWH aTK{*@+$L@B=+PGY\{f`"q ]+ЏZ8Mht+^>W %N} ӿݕ}!A=+ T@-kH*^mRrn+`~>(eLۥ >| UA"]P}Lj!PpXЈJuv`, n+`KPMmA-jA+0UFZt>f%[͝ΣT+I+K7/F\Y@a\b*@7sc[i0p+|r Z;``RӾe&K|x|W jTnOqV;׹f'`QtV1jjWj|y4WXhNtyKK2]+VkQxV-fW`J۠/m 8k9MZ jjByus:MJF~{vh*Չ[[ϣuꐅ|QMThDp")46HzE\觃V[!M)J$ܬW+ NM(+KT2̹v/NQ#>a!q>5+8 4mpoEW`~ #BjOlqy@mZy0 RM/dT2-ֻ h 0uJD Zl f sW G '1-.Y\ 7W)e]zqj^O@a+P$P= 4X[q{xDb9 xȃGI~WtZuZlp6И|Vd dT-0og_m:%tx9v#,- EU@o>XC:5^Wt%q%,8"O QIٕ 3)\'KZcd*n!T(kՔIDJ4$w8vq/yU@d0aO<<'t`h17W hoエE(^S @"W4J{iMժQ\ e.]̢ \H7M\*ЗXV~iq*H\EdԌg"5ZՐϩ\4r jVV@ \i.m: Xg\ |?y'œ{$J`tx,wNei\ ,Uuy7uKҪZZįcm{C5Ц6-͵ryJv>wN ? ך|N PpH @O[ZwJۀ> ԱȚ-G\| L:=AUJ%k<`k~ dAӺNN̊dpXpY/?6̞Su(S)CuJUf`:psڨ} éٴ_u+pAB 9] bt*2( 9-6T 5P%BVH1N]R^VV$HupsRR+4i|3{Y\p]1t*Wp3T&@ʯy@$!lr+ wNWBm9P[6J( JP kP8N[:B,1-ʼn| _kgN  (Ípo >F[ʀ7?_$% /inLN!+R*u(2 ꍩwP`FPR}]@< %`o4 hz2)5|^$SQRPMCr6ڧ DQ`ak-Q&1Xq  h7UH򟀉!q+ %8,$ZB|5)Yp2`!;Vp* YWQ09d"Q߳H31 ,t^qOJ @ym$i8JO,SW` Q:~1Fc V영"dq诓vy-p:NM![*@NbYTq'Vy1ոf6 %[G@_]\02UOSvL/+iJR >[ 3܊iau7ӰIDgH,nYT!M'tqN ǂ}$1TjdҲ$<he_3o@Y4i|D(㤥/fT(}q@Z$ 4+OT-:-'" OV"4a`9{L + .dS[ pZ5C@@V62-+?b@59h2 VdhC(`.˘fzt\8+З5=ѱ`ȢM SL ƀhBu -VT!Oi`҅ >M4nnc]>ѾAԯW~e[zǃQuV3<^2 h N-V?Z:pˎ_>-"T#\vֳOHjp%1*Bk`%0+p  Z(+r]ˋ5sn Lnp3 n pPѵ|v ̀0D =.0)H:4Z3.hqYXcxm֞  j\_ u0 fn_oT!Ty- \~fk-64w`꿿 4V yi;4ծ,ܒ 4ŨEk<w)/<Ywm$/ʿ(9q k݇OD*M8ެG,;,}҇ ZxXYJy?`m㻠iwhb'nyP@y jh|A 40WHIzW+5^~*'uAntm p˱ry] L@~>I٭4IENDB`././@LongLink0000644000000000000000000000016700000000000011607 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graphics/borderglow.pngdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graph0000664000175000017500000003135712641367671033216 0ustar jaakkojaakkoPNG  IHDR\rfbKGDC pHYs  tIME +/J IDATx}r뺮$H;ԼΜg.oPU8,ۊh`i߹} M{6mo{m=%[0&{>&M{a<8&vBAsž6l_€Lfۄ `g)6k!4`L%0 Ha+MeIg¾i77m 6}_{7`[E9U@[t&M݀6lx ?:W."oMi ewk ,bhc&ˁ?WE}op[N 6LKyf{qHڍ`B }g}Dgm +otms6mkf6"sDݕQ 6lOSִ=ڤ׌FlE6>__;r1PܟA.- P&FBMCQ3^ (R𒃵r\ _kT(|++WQfHϔw6$l0$-*%zk212V2B FR.L"$`䏨2 rxv3nG|4%(I\6v"mx?;\9TxU 0vE$ůJ2K#I2|Gs_ #Dp$ՓV̜/;[~Uī2K? 3 aEk}WޙЊQb-J t!p!U2,)i `췂tid*lo`TdTEDeՀd*$ѡt63tȾ*H ,>@,2'l$0ϭ$bX[n9m"f<;fHQI,`(W4~ [D~dBKdGUQL"(Q&ʼ> ?6 d#~GH#x?k0 0xf#*_LK Ѵ`}Fo*F܊F}~U4'1X+3G]Ѩ?6dL&ΛM^M"<Z2Fj%~#v3z*FQR^ x=S TYM?fx"m$ǏG<#3q^DlJn3ÑXG@g/н&K9*ڣ|$U庲${kJn&_f3HKOYQWpT*gtg)Yj #$o>r omRRLx_,&h |U!#)έH`\ =,DWV9l}}y"Q1-y|36wi4Ri0>ݸ@lAIQ}o8׀nȮ* {~kMMS,Q0wG32%Q{G+ uVrlw'E| $UpKI~+>o uˆv"xF9j%0L@x GHxg6Z6ŐC! D=jZA=F[jR+>O,g$!* Q7ux%GYG`F*cOg 4b5##q4dTy1=,c։@g߈*j H[n1HsS\*F0faoFh9ץM\;1 #d0VP߷!2.Rq2@{C$u:$cK`-͹ ,HZV?:1ј *[Ce2a| {3MSv#ֺߖk`}Q~5F&x2HY떔B* iF0;3X[oEm< k`i$a Sd@ݴMpu2\Qޒ_J"轍/<|W[>6X 8"ޣmaR ؠDk+ H@&x ^E5 -N 2ДKSLCP[(_CjtA!XL  `DusoGϣ.<=4>.{NkhŨ G.UuG k8ze~@}֪ϪZD ^W&Mࢹ.@3=։BSDoH*`gZZy;FPK_*h$`/n3g{ߪ&zpJ} -粐 )/W'80SySRb÷#Ĭa2D,`ȧCs։H%U Tֹ^GxJT CLUQ C{¡*Kxj 5*[m.@l1HѸyЗ#̙1Td2\C@ٝ3DGJ8?O%H <L?7^VD鬆љT0 ^S1<ʠٛ(C$̼F){`M")Bhs. ݻ&5'1@!JR Ht's"@ T:PH ,ix#LBV%5*㤑:HeҔ"}Jp*u3W:+' cZvzԁy- h[YOi.(wUTDUeȦ 0;e}=apaD7'CVo09XHDVP btȪweȡ D,U0A[сlj#T9/"L8-ˈeʊٍ@MǮ,*IM֔ #s{(aQ 9 #IgпA\@@.@+ߑe5(6NR o 9%*j$ ^+}хyM|l9,?Gtiz 3:\_ #8 PaHcJ'*&8bfXD ;kGR)$Fz(`V2 T!x_J +n%̭Ӻh"p)cB94F4YxUOGM3![}~}aFO>r8ߋJ8?{RS[/Do+ =^"*"#U9A`?Q'BpioEpT>O@ўE?*tދVϞ>|&-kt"7} d=D0"?6{?z|!j;z0$p*)5o{99 1xJ cF k!r F @&^)pD)'tDo4[" X=T+%)wb)Z,f!OHX ɨN '"C1#`9ҭW}n<+W4weވѧ:OFhÂ]}$ILJ$LJ|DRN "ߋhv͛X:9(CV(Y)'b[_Ox]W;S'z%R.{?>>x)/'|] S)BzX!uGF=KE y2L eQPA.=é]_ b_PFAu/pHܧ |v< ׿ge)b_cy,A7 9gqSqh~ܸO_?H |x ࣋R(B*2_#*d^S`kAn23¥FٽtFC(o@_DP`2{ 瞚r"aB p0?m(zU4>## @z=@$/ TH$d=xv@(=?@R|*KTJ3*"cLyL]; f֎=3.f(R}%Ce!"Ey! |qI{tK\uy1x!5Ȕ+ Lqyd׺uM@QqkP}9n)1jxKq 'q0@A!\_ntR#f`v?qalZe }f`PX9$|_Ѿ}΁u'"?} ;q][ -G_$LB `Fdn9_&5w~W#x ߃1]" 8pۧO"=@ &A % X!ý;{a` 3H C3upΎ >:"@dT ǿ !D,_'f\yԢ RCg7 B˂\qYjM.\]?={>6+8 9a? + IjA#!h0W yr{Z]+~PE"Ce_p2&)(4r٭+xH4/PgW^TA.?R`{@p]~^T#l>IH\T !!?@'A9 u\ҞM^_ Vh~ = }1 8_pt?&wPf?EBi0ՈG5HOmf@șU5,s7yb0M<*}cT߃̽)ykeLpoA'k HΌ~ | #/Qj?T(*!)?C(.C>C_?Q>pI#9#r^QRo0P{H'܈V"1& 01?رdRP

B~)W5's) U{Y0 <̣xL=m8\ 뗖F߫NHeU̠\R0LC7{Oگ0y?E8G'Tހ[ҔWsپJk u:W \WSAVڧߧ ~RwNHD&.Xֈ&Fpib&A ƍ7sT|@:$ ۠zFghAF6H#Dޣ9҂O]h*"3)%q:;gF0hQXdSUoED۾' CׁA'sQ.]*s/ SY nD*Z{D7"op: O ?(ģ'cc;9of[A)2 ϚTŊ y@+s./=Q*>)r7_rԶ( d|%$gQ|{2{ ߗZ*v2$[DK >g6W$MhN#)@e''=EgCH O&9G"a#~4U.#%sW=fV~vQn} i d'}^!M0'1:yr)Pԥ,y((5Hs6wD7tz< ETE'|7 fNR(BRiGt]IY𙈑$ [#*+}.Я?#n |od{K GOB|"Z^i%@7!:KyG1)g"@2m6zE0` " wR_?(~D([ӈ?_'/b׿i/?wst hޡ[cM+j<6kDlk"zRaIcD*DQ1`rs$Q<҅bx"#yj)DҁF;"y> pՁ0 8VAMPrV`An)|a@ E}//J/HS7?T@E':ß* ~֑FCr!S F4%5(8 Lh>e1pU~V)Ku9B]o_Ry@{  "c)h;X!hD06ٌS@9nd(p% CwJ `C=>O89_Gqv*dfQ)RD1ȩA(ߪ,qk! 69ܿR}0pA"cN>"pdp*ҟh|*23IH:2z&& yXGk &nf`FAPDž7yZj@*#;"gC$#&4|^Ў#E=3"ޚv}*؏dˉRPsO;'f 0OJz6~pIDAT"v[ QƎ|qyP6 Ȳf9t6"W NlrدOP:00Ĺ~PH4<M 4)G 0M|Ww:p 8ER07߄&SO`OC'p>׃ ? 05DEs-j*"=KA4ex':j̟"0r/f}(DoPuԐ_c? DBI1hgo:p$z1H~rR +)W"V3}t/D.GLAHU6l 94~#ҁj@ħ[|k@֏0:xس_/aoP ݠ@S]ȧ7>U}ؘ4r ޟ/R' q^x˨Č>BTX[jE~th mUڼu|I_3G/XA^jMWVjܟO! ɞ-ۤ@T[ 9XԇT:%H Q7sEP&/.ƇhLQQ_1O$ĩ'X鯥vNl4f6q?2"k.~hOИG~o;7^uUy$@R_}>E{0S!D f(& = DxO p,) bKW4d(c^ ߺ_Q'/w9?(Y-bm }b/HWFVS'>o(Exh=9L.N$giP9n5EbH#jzQV.%@S~]_}}@|i8 ҅]9E{k@(.6`PaF)2( \@1=Ks|V<Bz*B^`- VmM"4Jn2y22.98Ru~-Q @ J?L 8 ) M<@yO3LC حm2x4r/] "=~JA` :m8~aT5QE?@OM(J:p<kZO=}*۴4B绲@3"@ru-xeF.#"}?b H~)jS݌aA )օJ*ar+.=%R nF |_d #|oEf!8mA%%ZSbY&iBQzL@?<PS> m(B|Xk)e@ߠRjF$,R龧p5?')H!س & ZP1܈R x"\ȧ%uuٯH6/,VHz3^#.-'PqO%>WE)`RY|J!h`K0_`B*hr 3F1|"%Հ>8&dI(:4f4<^o 6Y*""95k l]} > s` C<1De"H~0Hf)xR)5o DͿf3[]b &"TOc~08\sOCrit])gV> $yo ޮ H, :@_ZM9$d8%9@ kZ %amPRZZ`%`E%hǃ!u),V>[,q~o;qW R@^_wy~!\ j՛"HSлkngߒRxgf ['M'; _9I D0gT~Vӌ+TQ uꑌCfdNj f \B*os >jXM69e`7,*s G`vU%xf F0~0(j\j$NPWYo$H<ֹ~^ Q0#LD\x-Sz-y<X?(\ۼki3Gh n #PjE*_EZ$B`![oPoUa?(LV\/"W &h^-8v{wyrr h Q<;J`$эd>E5# Ho_dp|np<7 1}Ux 5.DDof{ Pϟ[2 -QZ-vӁ#^Y)᧵%"`dFܢ D^gTԂ\3s F{i5gZ X%.MLm?i`X"u5%Y+G̷Ÿ%ﹼ~͌PTߡ/)h,i5 -@R/IXs|OKZg-<ƳO:F*s,K#K2׫2ײ(f_F{vtnz1#?CD>e>'k@SHJq2y2."ІƤOG? ޣ 2{FdQߪ Έmy]5b/]c)ǍoQHylU^%-[y 0'Ŏ<>W"&X[:%y)txH:4R3:Qie]Qh@M.&Y$2%zFS,ϊ#ϔ~+0j F¨R|OIADі-ZxےLr|s:5GL@2"[G `4EW"Xr!L/[!d3Ku=_EW@0y7 x^_ .w̵Y9ljXE:c~o%>Q Ѳb++7$ 3=S෼&YֺxYkeFYSM,ˆk7uiւkA0YJ*zDӁlO -6ߕ@-]&f%t;I(#FuwU "Y=jntbH6$3H;6˽L[>rf."LAJ"ߏx35L 2҂Tmd@6brPwF6H Y$N*By SqTGg)oAXEo/ߝfFl=DF#weu0-H`K̐+Izȱ+S a3W%h2H{ K{ 3o'd# hF@lfHHߕ\@'Va@?GӀ? O!U)_4pDt/3 6 "qc3s@63Ay&Br>s!kr#eTnLʄE mfWH@3 2p-VXIQI_n٩Aׂȝ\WI^wPT`/SVy.C7m"!sMy}$QٿJlox"Bk`ĐY`&D:{#moNN3MhI& CC_&lod@JM m`X>߬m!w3@ אAx}~@/Rm&m=%m\Tn>=ـ `oU»mp m䰷/ `o{&momFcIENDB`././@LongLink0000644000000000000000000000016200000000000011602 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graphics/mouse.pngdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graph0000664000175000017500000001134212641367671033206 0ustar jaakkojaakkoPNG  IHDR @{u :iCCPPhotoshop ICC profilexwTTϽwz0)C 7Da`(34!EDA"""` `QQy3Vt彗g}k=g}ֺtX 4Jc `23B=ÀH>nL"7w+7tI؂dPĩق }F1(1E";cX| v[="ޚ%qQ-["LqEVaf"+IĦ"&BD)+Rn|nbң2ޜT@`d0l[zZ ?KF\[fFf_nM{H? }_z=YQmv|c34 )[W%I Ȱ316rX7(ݝ ⺱SӅ|zfšyq_0sxpєqyv\7GSa؟8"Q>j1>s@7|8ՉŹ,߳e%9-$H*P*@#`l=p0VHiA>@ vjP @h'@8 .:n``a!2D UH 2!y@PAB&*: :]B=h~L2 p"΃ p\ u6<?g! DCJiA^&2L#PEGQި(j5jU:jGnFQ3Oh2Z mC#щlt݈nC_BF`0FcDa1k0Vy f 3bXl `{ǰCq[3yq<\ww7Zx;| ŗ]8~ M!8Ʉ*B !HT'\b8 q$C'bHBvay=+2Mv&G&Ec[ [bDDĐ I* Zc0&8(&iYH~Ho(%46h0װu wKDŽ7EGGDDōFG7FϮX{xULQ̝:+sV^]*uՙXXf8t\DѸ@f=s6'~_ ˍ̮`Oq8圉D]SINII\7n5ewrm\J`ᔅԈ4\Z\) /ד>aQ1n3|?~c&2S@L uYY5YoóOHrrsNy};_-cZuuk/\?kÑ)*0-(/x)bSWr±^$E[nEmnfmOk%%%JY׾1ꛅ ˬir]+wZiYYGgʿs{?T'U߮qiݧo۾C*זԾ?=xΫ^P֡ 2mjTl,ixwxHȑ&JG˚faԱc7sŨZr}wN>8(mP{nLGRHgT)S]]m?x3g]8wn| ƺc\x'ߥ+=/_u=wvWO]c\n}Ϫ'l:o\:xviMoܺ~{;˾;y/Ylx~XHQc?:b=rf}Icda)iDӤ)ϩV<|~W_}oοDΌ\«ï-_w>~f~#zGPQc'O6gAMA|Q cHRMz%u0`:o'IDATxb diip]#v0220R 0 oCb0CTT@G:"^ &P<???deeG8 ///իWRhG#Ξ= rl#ri´ .^Ƞ? & 3};$ @,߾} /_-/JGxt r  ]\\ Rr #be-X8AC&L`8rb`2!8 9@>%a 9Xq0qg@}@a[@  x]+zA jA511a*/@A@U>,A =,\p:9ݻw`yP<1'1(/ 17 bj;u <޸q!)) 8('*CXXfA ='ԁ,Py &\J 9re{ܻ;vxj.((w ĿaDD3Cp: 8A@`|3P!ЛSN7@!J Pi3@GH9x-׆ ϝ;ǐ` PvXs :77D_ ~I ?POۃ2,7G .jjj  r@+T/Ȝ@?(-/%(2@! :Ç @HF`Gbz# T/>B}/Xl9(˂r 2JI]!J  @a*5dcfoC18wipaz p;._#bAq^-]5sLˁ 1B@1B}*Ʉ4A t!ǏAӧOAP-ub@ bHؠ|*^/]^an ,@3 J?@BTs0@Ov8, J9V:Plƭ>|08;TNy ӑ "4D`EDfPd9ȎkJ'X !&./!J P@Ҁ,l%K744r$+<"-1 FDm\faP 3@l<q1 r[hhj@?h} )P:b 2C (/ P%\Pŗ X J_>Yu(i@ ,X jdDx@PN`5  RB5@ Z*: Hr4`mP40B-bZ+Q0"5@ P ~#QA(99 0!!DE@1Yij 0%>p#,@d9!(j"P5@ h@pЀ; 4w@ h@pЀ; &eDIENDB`././@LongLink0000644000000000000000000000016100000000000011601 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graphics/fold.pngdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/graph0000664000175000017500000000726412641367671033216 0ustar jaakkojaakkoPNG  IHDR@@iq AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  iTXtXML:com.adobe.xmp 5 1 2 O8IDATxINA9 w"(.< 5py6pk{ G&MwSUM飻.d" " " " " " " " " " "@=@h6C9P%ǰC"K;Jr-sd"&ewvx5E\QF. OF bwZ *2ެ`w__]$Eow(璘{ =&zU1Gռj!VXoWLq(d?Z87~ mg_%'7;|@z3&55͠lZ>q6Gt=P_̀AwS=h3iQ=zKⰃฝ{7. >-4e#gӌw.ؙ"{s[y`K̸J;TFVPɵ=du* _`l&s7 +!{u34:&Q]ރ`Rhqh BSHEWD@D@D@D@D@D@D@D@D@DKtB)`OIENDB`././@LongLink0000644000000000000000000000015100000000000011600 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/fonts.deidoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/fonts0000664000175000017500000000605612641367671033244 0ustar jaakkojaakko# Fonts for the default UI style # # - size can be "pt" or "px", defaults to "pt" # - weight: normal bold light # - style: normal italic script { import Version, DisplayMode, gui # Load the Open Sans font contained in this pack. def loadOpenSans() import App, Path fontDir = __file__.fileNamePath() / "fonts" for style in ['Regular', 'Bold', 'Italic', 'BoldItalic', 'Light', 'LightItalic'] App.loadFont(fontDir / ("OpenSans-%s.ttf" % style)) end end } group { condition: Version.OS == 'windows' script { loadOpenSans() } font default { family: Open Sans size: 12pt weight: normal style: normal } font monospace inherits default { family: Courier New size: 10pt } } group { condition: Version.OS == 'macx' script { # Define mappings for native font styles and weights. import App App.addFontMapping("Helvetica Neue", { ['regular', 25]: 'HelveticaNeue-Light', ['regular', 50]: 'HelveticaNeue', ['regular', 75]: 'HelveticaNeue-Bold', ['italic', 25]: 'HelveticaNeue-LightItalic', ['italic', 50]: 'HelveticaNeue-Italic', ['italic', 75]: 'HelveticaNeue-BoldItalic' }) App.addFontMapping("Menlo", { ['regular', 25]: 'Menlo-Regular', ['regular', 50]: 'Menlo-Regular', ['regular', 75]: 'Menlo-Bold', ['italic', 25]: 'Menlo-Italic', ['italic', 50]: 'Menlo-Italic', ['italic', 75]: 'Menlo-BoldItalic' }) } font default { family: Helvetica Neue size $: gui.scale('16pt', DisplayMode.DPI_FACTOR) weight: normal style: normal } font monospace inherits default { family: Menlo size $: gui.scale('13pt', DisplayMode.DPI_FACTOR) } } group { condition: Version.OS == 'unix' font default { family: Liberation Sans size: 13pt weight: normal style: normal } font monospace inherits default { family: FreeMono size: 12pt } } font title inherits default { size $: gui.scale(self.size, 1.75) weight: light } font heading inherits title { size $: gui.scale(default.size, 1.2) } font small inherits default { size $: gui.scale(self.size, 0.75) } group editor { font plaintext inherits default {} font hint inherits default { style: italic weight: light } } group separator { font empty inherits default { size $: gui.scale(self.size, 0.5) } font label inherits small { weight: bold } } group choice { font selected inherits default { weight: bold } } group tab { font label inherits small { weight: bold } font selected inherits tab.label {} } group slider { font label inherits small {} font value inherits slider.label { weight: bold } } group log { font normal inherits default {} } doomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/Info0000664000175000017500000000013212641367671032773 0ustar jaakkojaakkotitle: Application Framework Test: UI Style version: 1.0 license: GPL 3+ tags: test style ././@LongLink0000644000000000000000000000015200000000000011601 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/colors.deidoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/color0000664000175000017500000000461112641367671033224 0ustar jaakkojaakko# Colors for the default UI style script { import gui, Version } # # THE BASIC COLOR PALETTE # color text { rgb <1.0, 1.0, 1.0> } color background { rgb <0.0, 0.0, 0.0, 0.75> } color accent { rgb <1.0, 0.8, 0.4> } color altaccent { rgb <0.63, 0.75, 0.67> } color glow { rgb <1.0, 1.0, 1.0, 0.14> } group inverted { color text { rgb <0.0, 0.0, 0.0> } color background { rgb <1.0, 1.0, 1.0, 0.75> } color accent { rgb <0.5, 0.4, 0.2> } color glow { rgb <0.0, 0.0, 0.0, 0.15> } } # # COLORS FOR SPECIFIC WIDGETS # group label { color highlight { rgb <1.0, 1.0, 1.0> } color dimmed { rgb <0.72, 0.72, 0.68> } color accent { rgb $= accent.rgb } color dimaccent { rgb <0.85, 0.68, 0.34> } color altaccent { rgb $= altaccent.rgb } } group popup { group info { color background { rgb <1.0, 1.0, 1.0> } color glow { rgb $= inverted.glow.rgb } } } group choice { color popup { rgb $= gui.colorAlpha(background.rgb, 1.0) } } group tab { color selected { rgb $= accent.rgb } } group progress { group light { color wheel { rgb <1.0, 1.0, 1.0, 0.25> } color shadow { rgb <0.0, 0.0, 0.0, 0.45> } } group dark { color wheel { rgb <0.0, 0.0, 0.0, 0.25> } color shadow { rgb <1.0, 1.0, 1.0, 0.54> } } } group dialog { color background { rgb $= gui.colorAlpha(background.rgb, 0.9) } color default { rgb $= altaccent.rgb } } group editor { color cursor { rgb $= gui.colorAlpha(accent.rgb, 0.7) } color hint { rgb $= altaccent.rgb } } group log { color normal { rgb <0.85, 0.85, 0.8> } color highlight { rgb <1.0, 1.0, 1.0> } color dimmed { condition: Version.OS != 'windows' rgb <0.72, 0.72, 0.68> } color dimmed { # Apply some extra black because we don't have a Light style # font on Windows. condition: Version.OS == 'windows' rgb <0.65, 0.65, 0.62> } color accent { rgb $= accent.rgb } color altaccent { rgb $= altaccent.rgb } color dimaccent { rgb <0.85, 0.68, 0.34> } } group document { color normal { rgb $= gui.colorMix(inverted.text.rgb, [1, 1, 1], 0.2) } color highlight { rgb $= inverted.text.rgb } color dimmed { rgb <0.5, 0.5, 0.5> } color accent { rgb $= inverted.accent.rgb } color dimaccent { rgb <0.85, 0.68, 0.34> } } ././@LongLink0000644000000000000000000000015200000000000011601 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/images.deidoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/defaultstyle.pack/image0000664000175000017500000000077312641367671033175 0ustar jaakkojaakko# Images for the default UI style script { import gui } group window { image borderglow { path = "graphics/borderglow.png" } image cursor { path = "graphics/mouse.png" } } image fold { path = "graphics/fold.png" } image toggle.onoff { path $= gui.dpiScaledImagePath("graphics/toggle-onoff.png") } group progress { image wheel { path = "graphics/progress-wheel.png" } image gear { path = "graphics/progress-gear.png" } image mini { path = "graphics/progress-mini.png" } } doomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/images.dei0000664000175000017500000000006112641367671030464 0ustar jaakkojaakkoimage logo { path = "images/deng-logo-256.png" } doomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/modules/0000775000175000017500000000000012641367671030207 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/modules/appconfig.de0000664000175000017500000000053512641367671032472 0ustar jaakkojaakko# Config for test_appfw import gui def setDefaults(d) # Applies the apps's defaults. # - d: Record where to set the values. gui.setDefaults(d) # Use a windowed mode instead of going to fullscreen. d.window.main.fullscreen = False d.window.main.maximize = False # TODO: Other default values set here. end doomsday-stable-1.15.7/doomsday/tests/test_appfw/net.dengine.test.appfw.pack/modules/bootstrap.de0000664000175000017500000000006412641367671032536 0ustar jaakkojaakko# test_appfw bootstrap print "Running bootstrap..." doomsday-stable-1.15.7/doomsday/tests/test_appfw/src/0000775000175000017500000000000012641367671022121 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_appfw/src/main.cpp0000664000175000017500000000260712641367671023556 0ustar jaakkojaakko/** @file main.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "testapp.h" #include "mainwindow.h" #include #include using namespace de; int main(int argc, char **argv) { TestApp app(argc, argv); try { app.initialize(); return app.execLoop(); } catch(Error const &er) { qWarning() << "App init failed:\n" << er.asText(); QMessageBox::critical(0, "test_appfw", "App init failed:\n" + er.asText()); return -1; } #ifdef DENG2_DEBUG // Check that all reference-counted objects have been deleted. DENG2_ASSERT(Counted::totalCount == 0); #endif return 0; } doomsday-stable-1.15.7/doomsday/tests/test_appfw/src/globalshortcuts.cpp0000664000175000017500000000257112641367671026051 0ustar jaakkojaakko/** @file globalshortcuts.cpp Global keyboard shortkeys. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "globalshortcuts.h" #include "testapp.h" #include using namespace de; DENG2_PIMPL_NOREF(GlobalShortcuts) {}; GlobalShortcuts::GlobalShortcuts() : Widget("shortcuts"), d(new Instance) {} bool GlobalShortcuts::handleEvent(Event const &event) { if(event.isKeyDown()) { KeyEvent const &key = event.as(); if(key.modifiers().testFlag(KeyEvent::Control) && key.qtKey() == Qt::Key_Q) { TestApp::app().quit(); return true; } } return false; } doomsday-stable-1.15.7/doomsday/tests/test_appfw/src/appwindowsystem.cpp0000664000175000017500000000266112641367671026107 0ustar jaakkojaakko/** @file appwindowsystem.cpp Application window system. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "appwindowsystem.h" #include using namespace de; DENG2_PIMPL(AppWindowSystem) { Instance(Public *i) : Base(i) { self.style().load(App::packageLoader().load("net.dengine.test.appfw.defaultstyle")); } }; AppWindowSystem::AppWindowSystem() : d(new Instance(this)) { setAppWindowSystem(*this); } MainWindow &AppWindowSystem::main() { return WindowSystem::main().as(); } bool AppWindowSystem::rootProcessEvent(Event const &event) { return main().root().processEvent(event); } void AppWindowSystem::rootUpdate() { main().root().update(); } doomsday-stable-1.15.7/doomsday/tests/test_appfw/src/appwindowsystem.h0000664000175000017500000000226612641367671025555 0ustar jaakkojaakko/** @file appwindowsystem.h Application window system. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef APPWINDOWSYSTEM_H #define APPWINDOWSYSTEM_H #include "mainwindow.h" #include class AppWindowSystem : public de::WindowSystem { public: AppWindowSystem(); static MainWindow &main(); bool rootProcessEvent(de::Event const &event); void rootUpdate(); private: DENG2_PRIVATE(d) }; #endif // APPWINDOWSYSTEM_H doomsday-stable-1.15.7/doomsday/tests/test_appfw/src/approotwidget.cpp0000664000175000017500000000331212641367671025514 0ustar jaakkojaakko/** @file approotwidget.cpp Application's root widget. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "approotwidget.h" #include "mainwindow.h" #include "testapp.h" using namespace de; DENG2_PIMPL(AppRootWidget) { Instance(Public *i) : Base(i) {} }; AppRootWidget::AppRootWidget(CanvasWindow *window) : GuiRootWidget(window), d(new Instance(this)) {} MainWindow &AppRootWidget::window() { return GuiRootWidget::window().as(); } void AppRootWidget::addOnTop(GuiWidget *widget) { // The window knows what is the correct top to add to. window().addOnTop(widget); } void AppRootWidget::dispatchLatestMousePosition() { TestApp::windowSystem().dispatchLatestMousePosition(); } void AppRootWidget::handleEventAsFallback(Event const &/*event*/) { // Handle event at global level, if applicable. } void AppRootWidget::update() { GuiRootWidget::update(); // Tell the window to redraw itself as soon as possible. window().draw(); } doomsday-stable-1.15.7/doomsday/tests/test_appfw/src/globalshortcuts.h0000664000175000017500000000214412641367671025512 0ustar jaakkojaakko/** @file globalshortcuts.h Global keyboard shortkeys. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef GLOBALSHORTCUTS_H #define GLOBALSHORTCUTS_H #include class GlobalShortcuts : public de::Widget { public: GlobalShortcuts(); // Events. bool handleEvent(de::Event const &event); private: DENG2_PRIVATE(d) }; #endif // GLOBALSHORTCUTS_H doomsday-stable-1.15.7/doomsday/tests/test_appfw/src/approotwidget.h0000664000175000017500000000254512641367671025170 0ustar jaakkojaakko/** @file approotwidget.h Application's root widget. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef APPROOTWIDGET_H #define APPROOTWIDGET_H #include #include class MainWindow; /** * GUI root widget tailored for the application. */ class AppRootWidget : public de::GuiRootWidget { public: AppRootWidget(de::CanvasWindow *window = 0); MainWindow &window(); void addOnTop(de::GuiWidget *widget); void dispatchLatestMousePosition(); void handleEventAsFallback(de::Event const &event); void update(); private: DENG2_PRIVATE(d) }; #endif // APPROOTWIDGET_H doomsday-stable-1.15.7/doomsday/tests/test_appfw/src/mainwindow.cpp0000664000175000017500000001524112641367671025004 0ustar jaakkojaakko/** @file mainwindow.cpp The main window. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "mainwindow.h" #include "testapp.h" #include "approotwidget.h" #include "globalshortcuts.h" #include #include #include #include #include #include using namespace de; DENG2_PIMPL(MainWindow) , DENG2_OBSERVES(Canvas, GLResize) { AppRootWidget root; bool needRootSizeUpdate; VRWindowTransform contentXf; GlobalShortcuts *shortcuts; CompositorWidget *compositor; LabelWidget *test; // Faux mouse cursor for transformed VR mode. LabelWidget *cursor; LabelWidget *camPos; ConstantRule *cursorX; ConstantRule *cursorY; Instance(Public *i) : Base(i) , root(&self) , needRootSizeUpdate(false) , contentXf(self) , compositor(0) , test(0) , cursor(0) , cursorX(new ConstantRule(0)) , cursorY(new ConstantRule(0)) { self.setTransform(contentXf); self.canvas().audienceForGLResize() += this; } ~Instance() { releaseRef(cursorX); releaseRef(cursorY); } void setupUI() { Style const &style = Style::get(); shortcuts = new GlobalShortcuts; root.add(shortcuts); compositor = new CompositorWidget; root.add(compositor); test = new LabelWidget; test->setText("Doomsday Application Framework Test"); test->setImage(TestApp::images().image("logo")); test->setTextAlignment(ui::AlignBottom); test->rule().setRect(root.viewRule()); compositor->add(test); // Mouse cursor. cursor = new LabelWidget; cursor->setBehavior(Widget::Unhittable); cursor->margins().set(""); // no margins cursor->setImage(style.images().image("window.cursor")); cursor->setAlignment(ui::AlignTopLeft); cursor->rule() .setSize(Const(48), Const(48)) .setLeftTop(*cursorX, *cursorY); compositor->add(cursor); // The mouse cursor is only needed with OVR. if(!VRConfig::modeAppliesDisplacement(TestApp::vr().mode())) { cursor->hide(); } } void glInit() { GLState::current() .setBlend(true) .setBlendFunc(gl::SrcAlpha, gl::OneMinusSrcAlpha); contentXf.glInit(); self.raise(); self.activateWindow(); self.canvas().setFocus(); } void updateMouseCursor() { Vector2i cp = TestApp::windowSystem().latestMousePosition(); if(cp.x < 0 || cp.y < 0) return; cursorX->set(cp.x); cursorY->set(cp.y); } void updateRootSize() { DENG2_ASSERT_IN_MAIN_THREAD(); needRootSizeUpdate = false; Vector2ui const size = contentXf.logicalRootSize(self.canvas().size()); // Tell the widgets. root.setViewSize(size); } void updateCompositor() { DENG2_ASSERT_IN_MAIN_THREAD(); if(!compositor) return; VRConfig &vr = TestApp::vr(); if(vr.mode() == VRConfig::OculusRift) { compositor->setCompositeProjection( vr.projectionMatrix(OVR_FOV, root.viewRule().size(), OVR_NEAR_CLIP, OVR_FAR_CLIP) * Matrix4f::scale(Vector3f(.5f, -.5f / vr.oculusRift().aspect(), 1)) * Matrix4f::translate(Vector3f(-.5f, -.5f, -1))); } else { // We'll simply cover the entire view. compositor->useDefaultCompositeProjection(); } } void canvasGLResized(Canvas &canvas) { LOG_AS("MainWindow"); Canvas::Size size = canvas.size(); LOG_TRACE("Canvas resized to ") << size.asText(); // Update viewport. GLState::current().setViewport(Rectangleui(0, 0, size.x, size.y)); updateRootSize(); } }; MainWindow::MainWindow(String const &id) : BaseWindow(id), d(new Instance(this)) { if(App::commandLine().has("--ovr")) { // Go straight into Oculus Rift mode. VRConfig &vr = TestApp::vr(); vr.setMode(VRConfig::OculusRift); vr.setRiftFramebufferSampleCount(App::commandLine().has("--nofsaa")? 1 : 2); vr.setPhysicalPlayerHeight(1.8f); vr.setScreenDistance(.5f); vr.setEyeHeightInMapUnits(vr.physicalPlayerHeight() * .925f); setCursor(Qt::BlankCursor); } setWindowTitle("test_appfw"); d->setupUI(); } AppRootWidget &MainWindow::root() { return d->root; } Vector2f MainWindow::windowContentSize() const { // Current root widget size. return d->root.viewRule().size(); } void MainWindow::drawWindowContent() { GLState::current().target().clear(GLTarget::ColorDepth); d->updateCompositor(); d->root.draw(); } void MainWindow::canvasGLReady(Canvas &canvas) { BaseWindow::canvasGLReady(canvas); // Configure a viewport immediately. GLState::current() .setViewport(Rectangleui(0, 0, canvas.width(), canvas.height())) .setDepthTest(true); LOGDEV_MSG("MainWindow GL ready"); d->glInit(); } void MainWindow::preDraw() { // NOTE: This occurs during the Canvas paintGL event. BaseWindow::preDraw(); DENG2_ASSERT_IN_MAIN_THREAD(); d->updateMouseCursor(); if(d->needRootSizeUpdate) { d->updateRootSize(); } } void MainWindow::postDraw() { if(TestApp::vr().mode() != VRConfig::OculusRift) { swapBuffers(); } BaseWindow::postDraw(); Garbage_Recycle(); } void MainWindow::addOnTop(de::GuiWidget *widget) { d->compositor->add(widget); // Keep the mouse cursor on top. d->compositor->moveChildToLast(*d->cursor); } bool MainWindow::handleFallbackEvent(Event const &) { // Handle event at a global level, if appropriate. return false; } doomsday-stable-1.15.7/doomsday/tests/test_appfw/src/mainwindow.h0000664000175000017500000000256612641367671024457 0ustar jaakkojaakko/** @file mainwindow.h The main window. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef MAINWINDOW_H #define MAINWINDOW_H #include #include "approotwidget.h" class MainWindow : public de::BaseWindow { Q_OBJECT public: MainWindow(de::String const &id = "main"); AppRootWidget &root(); de::Vector2f windowContentSize() const; void canvasGLReady(de::Canvas &canvas); void addOnTop(de::GuiWidget *widget); void drawWindowContent(); void preDraw(); void postDraw(); protected: bool handleFallbackEvent(de::Event const &event); private: DENG2_PRIVATE(d) }; #endif // MAINWINDOW_H doomsday-stable-1.15.7/doomsday/tests/test_appfw/src/testapp.cpp0000664000175000017500000000465612641367671024320 0ustar jaakkojaakko/** @file testapp.cpp Test application. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "testapp.h" #include "appwindowsystem.h" #include using namespace de; DENG2_PIMPL(TestApp) { QScopedPointer winSys; ImageBank images; Instance(Public *i) : Base(i) {} void loadAllShaders() { // Load all the shader program definitions. FS::FoundFiles found; self.findInPackages("shaders.dei", found); DENG2_FOR_EACH(FS::FoundFiles, i, found) { LOG_MSG("Loading shader definitions from %s") << (*i)->description(); self.shaders().addFromInfo(**i); } } }; TestApp::TestApp(int &argc, char **argv) : BaseGuiApp(argc, argv), d(new Instance(this)) { setMetadata("Deng Team", "dengine.net", "Application Framework Test", "1.0"); setUnixHomeFolderName(".test_appfw"); } void TestApp::initialize() { DisplayMode_Init(); addInitPackage("net.dengine.test.appfw"); initSubsystems(App::DisablePlugins); // Create subsystems. d->winSys.reset(new AppWindowSystem); addSystem(*d->winSys); d->loadAllShaders(); // Also load images. d->images.addFromInfo(rootFolder().locate("/packs/net.dengine.test.appfw/images.dei")); // Create the window. MainWindow *win = d->winSys->newWindow("main"); scriptSystem().importModule("bootstrap"); win->show(); } TestApp &TestApp::app() { return *static_cast(DENG2_APP); } AppWindowSystem &TestApp::windowSystem() { return *app().d->winSys; } MainWindow &TestApp::main() { return windowSystem().main(); } ImageBank &TestApp::images() { return app().d->images; } doomsday-stable-1.15.7/doomsday/tests/test_appfw/src/testapp.h0000664000175000017500000000252212641367671023753 0ustar jaakkojaakko/** @file testapp.h Test application. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef APPFW_TEST_APP_H #define APPFW_TEST_APP_H #include #include #include "appwindowsystem.h" #define OVR_FOV 122.f #define OVR_NEAR_CLIP .1f #define OVR_FAR_CLIP 1300.f class TestApp : public de::BaseGuiApp { public: TestApp(int &argc, char **argv); void initialize(); static TestApp &app(); static AppWindowSystem &windowSystem(); static MainWindow &main(); static de::ImageBank &images(); private: DENG2_PRIVATE(d) }; #endif // APPFW_TEST_APP_H doomsday-stable-1.15.7/doomsday/tests/test_script/0000775000175000017500000000000012641367671021521 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_script/main.cpp0000664000175000017500000000316612641367671023157 0ustar jaakkojaakko/* * The Doomsday Engine Project * * Copyright (c) 2009-2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, see . */ #include #include #include #include #include #include using namespace de; int main(int argc, char **argv) { try { TextApp app(argc, argv); app.initSubsystems(App::DisablePlugins); Script testScript(app.fileSystem().find("kitchen_sink.de")); Process proc(testScript); LOG_MSG("Script parsing is complete! Executing..."); LOG_MSG("------------------------------------------------------------------------------"); proc.execute(); LOG_MSG("------------------------------------------------------------------------------"); LOG_MSG("Final result value is: ") << proc.context().evaluator().result().asText(); } catch(Error const &err) { qWarning() << err.asText(); } qDebug("Exiting main()..."); return 0; } doomsday-stable-1.15.7/doomsday/tests/test_script/test_script.pro0000664000175000017500000000053212641367671024606 0ustar jaakkojaakkoinclude(../config_test.pri) TEMPLATE = app TARGET = test_script SOURCES += main.cpp SCRIPTS = kitchen_sink.de sections.de OTHER_FILES += $$SCRIPTS deployTest($$TARGET) scripts.files = $$SCRIPTS scripts.path = $$DENG_DATA_DIR macx { scripts.path = Contents/Resources QMAKE_BUNDLE_DATA += scripts } else { INSTALLS += scripts } doomsday-stable-1.15.7/doomsday/tests/test_script/kitchen_sink.de0000664000175000017500000004752012641367671024514 0ustar jaakkojaakko# The Doomsday Engine Project # # Copyright (c) 2009-2013 Jaakko Keränen # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, see . # ================ # THE KITCHEN SINK # ================ # This script tests *all* features of the Doomsday Script Language. # Behold its power and glory. import sections # --------------------------------------------------------------------------- sections.begin('BASIC EXPRESSIONS') sections.subsection('Basic types (number, text, array, dictionary, time).') print 5, 5.5, -3.141592657 print 0x100, 0X123 print "Hello", 'World' print """I can span newlines.""" print [1, 2, 3] print [1, [2, 3], 4] print {'a': 'b', 1: ['b', {5:6, 6:7}], ['array', 'as', 'key']: 'oh my'} print 'The time is now:', Time() sections.subsection('Constants: True, False, None, Pi.') print True, False, None, Pi sections.subsection('Built-in functions: len(), dictkeys(), dictvalues().') print 'len("abcd") =', len("abcd") print 'len([1, 2, 3, 4]) =', len([1, 2, 3, 4]) print 'len({1:2, 3:5, 8:13}) =', len({1:2, 3:5, 8:13}) print 'dictkeys({1:2, 3:5, 8:13}) =', dictkeys({1:2, 3:5, 8:13}) print 'dictvalues({1:2, 3:5, 8:13}) =', dictvalues({1:2, 3:5, 8:13}) sections.subsection('Built-in functions: Text(), Number(), Time(), timedelta().') print Text(123), Number("123") print "Beginning of March 2011:", Time("2011-03-01") print Time("2000-01-02 03:04:05.678") print 'Invalid time:', Time("123-123"), Time("") print 'Thousand seconds later:', timedelta(Time(), Time() + 1000) sections.subsection('Built-in functions: serialize() and deserialize().') a = [1, 'One', {1:'One'}] print 'a =', a b = serialize(a) print 'serialize(a) =', b print 'deserialize(serialize(a)) =', deserialize(b) del a, b sections.subsection('Built-in function: eval().') a = eval(""" 'arg' + 'ument' """) eval("""print "Printed from eval(): %s" % a""") # --------------------------------------------------------------------------- sections.begin('OPERATOR EXPRESSIONS') sections.subsection('Operators: +') print 'Numbers:', 1 + 1, -.5 + .5 print 'Larger than 32-bit:', 0xffffffffff + 0xffffffffff print 'Text:', 'Hello' + 'World' print 'Array:', [1, 2, 3] + ['a', 'b', 'c'] print 'Dictionary:', {1:2, 3:4} + {'a':'b', 'c':'d'} print 'Time:', Time() + 3600 sections.subsection('Operators: -') print 'Numbers:', 1 - 1, -.5 - .5 print 'Larger than 32-bit:', 0xffffffffff - 0xffffffffff print 'Dictionary:', {'a':'A', 'b':'B'} - 'a' print 'Time:', Time() - 3600, timedelta(Time(), Time() - 3600) sections.subsection('Operators: *') print 'Numbers:', 1 * 2, 2.5 * 4, (2 + 6) * 45 print 'Text:', 'Word' * 3, 'Longword' * 3.5 sections.subsection('Operators: /') print 'Numbers:', 2 / 2, 10 / 4, 1 / 0 print 'Path concatenation:', 'folder' / 'filename.ext', 'folder/' / 'filename.ext' sections.subsection('Operators: %') print 'Numbers:', 7 % 3 print 'String formatting:', \ '%s with "%3i" arguments. Hex %x, capital hex %X, float %f, precision %.10f.' % [ 'Formatted', 2, 240, 240, Pi, Pi] print 'More formatting: Width and precision: "%10.5f"' % Pi sections.subsection('Operators: +=, -=, *=, /=, %=') a = 1 print 'a =', a print 'a =', a += 3 print 'a =', a -= 3 print 'a =', a *= 100 print 'a =', a /= 10 print 'a =', a %= 3 print 'a =', a path = '/some/path/' print 'path =', path print 'path =', path /= 'filename.ext' sections.subsection('Operators: []') transports = ['planes', 'trains', 'automobiles', 'bicycles'] print transports[2] xlat = {'hi': 'hello', 'globe': 'world'} print xlat['hi'], xlat['globe'] matrix = [[1, 2], [3, 4]] print 'Two level indexing:', matrix[0][1], matrix[1][1] matrix2 = [matrix, matrix] print 'Three level indexing:', matrix2[1][1][0], 'from', matrix2 sections.subsection('Operators: slice.') fullArray = [1, 2, 3, 4, 5, 6, 7] print 'Full:', fullArray print 'Sliced:', fullArray[2:4], fullArray[3:], fullArray[:3] print 'Nega-sliced:', fullArray[-4:-2] print 'Reversing:', fullArray[::-1] print 'Reverse slice:', fullArray[4:2:-1] print 'Stepping:', fullArray[::2], fullArray[1:6:2] print 'Text can be sliced as well...' fullString = "abcdefg" print 'Full:', fullString print 'Sliced:', fullString[2:4], fullString[3:], fullString[:3] print 'Nega-sliced:', fullString[-4:-2] print 'Reversing:', fullString[::-1] print 'Reverse slice:', fullString[4:2:-1] print 'Stepping:', fullString[::2], fullString[1:6:2] sections.subsection('Operators: not.') print not True print not False print not 'Is this true?' sections.subsection('Operators: and/or.') print True and True print True and False print True or True print True or False print False or False print "'and' has higher preference than 'or':", True and False or True sections.subsection('Operators: comparisons.') print 'Numbers:', 1 > 3, 1 < 3, 2 >= 2, 4 >= 2, 2 <= 2, 4 <= 2 print 'Text:', "hello" > "world", "hello" < "world", 'hello' == 'hello' print 'True/False:', True == True, True != False print 'Arrays:', [1, 0] == [1, 0], [1, 0] < [1, 1], [1, 0] > [1, 1], [2, 0] > [1, 0], [2, 0] < [1, 0] sections.subsection('Operators: in.') print 'transports =', transports if 'bicycles' in transports: print "Got a bicycle." if not 'jet' in transports: print "No jet." # --------------------------------------------------------------------------- sections.begin('STATEMENTS & COMPOUNDS') sections.subsection('Separating statements with semicolon.') print 'Statement 1.'; print 'Statement 2.' sections.subsection('if/elsif/else statements.') if True end if True if True if True if True end end end end if True print 'Regular compound.' else print "You won't see this." end if True: print "One statement compound." else: print "Not printed." if True print "Compound A" if False print "Compound B" else: print "Compound C" end if False: print "Lots of useless stuff here" elsif True print "From the elsif" end if False print 'a)' elsif False print 'b)' elsif True > False print 'c)' end print 'After the a), b), c)' if True: print 'Everything on one line.'; else: print 'Goes unseen.' if False; print 'Goes unseen again.'; else; print 'And with semicolons (requires "end").'; end sections.subsection('Assign statement.') print 'Assigning 10 to i...' i = 10 print '...and i ==', i i = 'abc' print "Now i ==", i sections.subsection('Assign statement: assigning into elements.') array = [1, 2, 3, 4] originalArray = array # by value print array array[1] = 100 print array array[1] = ['a', 'b', 'c'] print array array[1][1] = 'Bee' print array array[1] = {1:2, 'Three':4} print array array[1][4] = 'Four' print array array[2] = array[1] print array array[2]['Three'] = '300' print array print originalArray sections.subsection('Assign statement: weak assignment with ?=.') print "'z' in locals():", 'z' in locals() z ?= 3 print "'z' in locals():", 'z' in locals() print 'z =', z print 'Trying weak assignment: z ?= 10' z ?= 10 print 'z =', z sections.subsection('Assign statement: const assignment to make read-only variables.') z = 1 print 'z =', z const z = 10 print 'z has been consted to', z try z = 10 print 'It is allowed to reassign the same value...' z = 5 catch ReadOnlyError print 'Have to delete z before its value can change.' end del z z = 2 print 'Now z =', 2 sections.subsection('Delete statement: deleting identifiers.') def funky() i = 1 k = 2 print locals() del i print 'After deletion:' print locals() end funky() del funky sections.subsection('while statement.') i = 5 while i > 0 print 'Still looping because %i > 0' % i i -= 1 end sections.subsection('break statement.') while True print "I won't stay here." break print "Jumped over me." end print "Out of the while." while False: print "Never going in." while True while True print "Uh-oh, now I'm in trouble!" break 2 end end print "Guess again!" sections.subsection('for statement.') for i in ['planes', 'trains', 'automobiles'] print "I would like:", i end print 'Value of i after the "for" loop is', i sections.subsection('continue statement.') i = 3 while i > 0 print 'i =', i i -= 1 if i == 1: continue print 'Bottom of loop.' end for i in ['planes', 'trains', 'automobiles'] if i == 'trains': continue print i, 'are cool!' end sections.subsection('pass statement: no operation.') if True: print "'Tis true."; else: pass # --------------------------------------------------------------------------- sections.begin('EXCEPTIONS') sections.subsection('Simple try/catch.') try: print 'Nothing thrown yet.' catch: print 'Skipped.' try: throw "OMG!" catch: print 'Whoa there.' try throw "OMG!" catch print 'Not so fast!' end try: throw "OMG!" catch Error print 'Hold your horses.' end try throw "OMG!" catch Error: print "Whatcha tryin' to pull?" sections.subsection('Minimal catch.') try: throw 'OMG!' catch: pass sections.subsection('Assigning exception message to local variable.') try: throw "OMG!" catch Error, er print 'Caught:', er end try: throw "OMG!" catch Error, er: print 'Caught:', er sections.subsection('Nesting try/catch statements.') try try try throw 'Deep.' catch BogusError end catch BogusError end catch Error, er: print 'Got it:', er try try try throw 'Deep 2.' catch BogusError end catch BogusError: pass try catch Error, er print 'Should *not* be caught here.' end catch Error, er print 'Should be caught here:', er end sections.subsection('Throwing exception in a function.') def erroneous() throw "I can't handle the truth!" end try print "Calling erroneous()" erroneous() try print 'This will be skipped!' catch print 'I am on the wrong catch level.' end catch print 'Fumbled it...' end sections.subsection('Catching a specific type of exception.') try record temp reference = temp del temp print reference catch NullError, er print 'Oh noes:', er end sections.subsection('Multiple catch statements.') try throw 'For the 2nd catch.' catch NullError: print 'Got NullError' catch Error, er: print er catch: print 'Never here.' sections.subsection('Exception thrown in another module.') try sections.throwError() catch Error, er print 'Got error from another module:', er end sections.subsection('Exception thrown in recursive call.') def recursive(i) print i if i == 10: throw 'Reached 10!' recursive(i + 1) end try recursive(1) catch Error, er print er end sections.subsection('Exception from trying to change a read-only variable.') const SOME_CONSTANT = 3.1415 print 'SOME_CONSTANT =', SOME_CONSTANT try SOME_CONSTANT = 3 catch ReadOnlyError, er print 'Cannot do it.', er end try const SOME_CONSTANT = 3 catch ReadOnlyError, er print 'Will not work either:', er end # --------------------------------------------------------------------------- sections.begin('RECORDS') sections.subsection('Creating a record with a statement.') record myrec print len(myrec) print "Alternative way using Record()." del myrec myrec = Record() print len(myrec) sections.subsection('Creating a record using an expression.') del myrec (record myrec).expressionCreated = True myrec.(record subexp).alsoExpCreated = True print myrec del myrec.subexp sections.subsection('Creating variables into a record.') myrec.newMember = 100 print len(myrec) print 'myrec.newMember =', myrec.newMember print "Here's the record:" print myrec sections.subsection('Creating a subrecord.') record myrec.subrec print len(myrec), len(myrec.subrec) const myrec.subrec.something = 200 try myrec.subrec.something = 201 print 'THIS IS AN ERROR -- EXCEPTION SHOULD HAVE BEEN THROWN! --' catch ReadOnlyError, er print 'Cannot change const member.' print er end print 'myrec.subrec.something =', myrec.subrec.something print "Here's the record:" print myrec sections.subsection('Checking whether members exist in record.') print 'subrec' in myrec, 'newMember' in myrec, 'not-there' in myrec.subrec sections.subsection('Built-in functions: members(), subrecords().') print 'members of myrec:', members(myrec) print 'subrecords of myrec:', subrecords(myrec) sections.subsection('Alternative ways to access members and subrecords.') print myrec['newMember'] print myrec['subrec'] print myrec['subrec']['something'] print members(myrec)['newMember'] sections.subsection('Creating new members using [].') myrec['assignedElement'] = 3000 print myrec sections.subsection('Copying members using recutil.') import recutil record copyTarget recutil.copyMissingMembers(myrec, copyTarget) print "Copied:\n", copyTarget del recutil sections.subsection('Having two variables reference the same record.') reference = myrec print len(reference) reference.otherMember = 150 print "Here's the record:" print myrec reference.memberViaRef = 'added via ref' print "Added a member via a reference:" print reference sections.subsection('Accessing members through a record reference.') record other other.refd = reference print 'The other is:' print other other.refd.newMember = 300 print 'myrec after modifications:' print myrec sections.subsection('Making a copy of a record.') def recCopyTest() print "Record() can be used to make a copy of a record." reference = Record(myrec) print reference print "Now we can modify the new record without affecting myrec." reference.superGreat = 'wow' del reference.subrec print reference print "Original:" print myrec print "Locals:" print locals() end recCopyTest() sections.subsection('Deleting members from record.') print 'Before deleting:' print myrec del myrec.subrec.something print 'After deleting:' print myrec print "subrec isn't gone, though: len(myrec.subrec) =", len(myrec.subrec) sections.subsection('Deleting a record pointed to by two variables.') record soonDeleted reference = soonDeleted del soonDeleted try: print reference catch Error, er: print "Can't print reference:", er del reference sections.subsection('Serializing records.') print 'serialize(myrec) =', serialize(myrec) print 'deserialize(serialize(myrec)) =' print deserialize(serialize(myrec)) # --------------------------------------------------------------------------- sections.begin('FUNCTIONS') sections.subsection('Defining a function (empty body).') def func1(): return func1() def func1b() end func1b() sections.subsection('Returning values from the function.') def func2(): return 'World' print 'Hello', func2() def func2b(a) if a return "It is true!" else return "Nay, it is false." end end print func2b(1), func2b(''), func2b(True), func2b(False) sections.subsection('Returning a record from a function.') def recFunc() record a a.value = "recFunc" return a end try: print recFunc() catch NullError print "Records are passed by reference; the returned record went out of scope." end del recFunc def recFunc() record a a.value = "recFunc" return Record(a) end print "Let's try again, making a copy for the return value." print recFunc() del recFunc sections.subsection('Passing arguments.') a = 'At process scope.' def func3(a, b, c) print 'The arguments were:', a, b, c return b end print 'returned:', func3(1, 2, 3) print a sections.subsection('Passing record as argument.') myrec.passed = 'Original' print 'myrec.passed =', myrec.passed def recArg(r) r.passed = 'Changed' end recArg(myrec) print 'myrec.passed =', myrec.passed sections.subsection('Default values to function arguments.') def func4(a, b='default', c=100) print 'Given:', a, b, c end func4(1) func4(1, 2) func4([1,2], c='world', b='hello') print 'func4 =', func4 sections.subsection('Defining function in a record.') def myrec.func5(a, b) return a + b end print myrec.func5("The record is\n", Text(myrec)) sections.subsection('Defining a function within a function.') def func6() print 'Inside func6.' print 'func2() =', func2() def func2(): return 'Shadows the global level.' print 'func2() =', func2() end func6() print 'func2() =', func2() sections.subsection("Accessing function's locals within a subfunction.") def func7() a = 100 print 'Inside func7: a =', a def func() print 'Inside func: a =', a end func() end func7() sections.subsection('Reference to function.') reference = func2 print 'func2() =', func2() print 'reference() =', reference() print 'reference =', reference sections.subsection('Callback function as argument.') def doCallback(a, b, cb) print cb(a, b) end doCallback(10, 15, myrec.func5) sections.subsection('Returning a local function from a function.') a = 'Process scope.' def testfun() a = 'testfun() scope.' def returningFunc() print 'I come from another place.', a end returningFunc() # 'a' found in local namespace return returningFunc end testfun()() # 'a' found in global namespace! (testfun()'s local scope does not exist here) ar = [testfun] ar[0]()() # --------------------------------------------------------------------------- sections.begin('SCOPES') sections.subsection('Global assignment with :=.') a = 1 print 'a =', a def normalAssign() a = 2 print 'in normalAssign(), a =', a end normalAssign() print 'a =', a def globalAssign() a := 2 print 'in globalAssign(), a =', a end globalAssign() print 'a =', a sections.subsection('Exporting variable from a function.') def exporting() abc = 'Value from exporting()' export abc end abc = 'Value in global namespace' print 'Before: abc =', abc exporting() print 'After: abc =', abc sections.subsection('Exporting a record.') def exporting2() record bunny bunny.ear = 'value' export bunny end exporting2() print 'bunny =', bunny sections.subsection('Exporting while assigning.') del abc def exporting3() export abc = 'Value from exporting3()' end exporting3() print 'abc =', abc sections.subsection('Built-in function locals() returns the local namespace as a record.') print 'At global level:' print locals() print print 'Within a function:' def func8() justMe = "Function's local namespace." print locals() end func8() sections.subsection('Importing modules: built-in module Config.') import Config print "Here's the Config:" print Config sections.subsection('Serializing namespaces.') sns = serialize(locals()) print 'sns =', sns print 'That deserializes into:' dsns = deserialize(sns) print dsns print 'Calling deserialized function...' print dsns.func3(1, 2, 3) print 'Calling deserialized function that throws an exception...' try: dsns.erroneous(); catch: print 'Caught.' del dsns sections.subsection('Serializing record references.') def recordRefTest() record hello hello.entry = 'World' record hello.subrec hello.subrec.another = 123 hello.recref = hello.subrec print 'Original record:' print hello hello.subrec.another = 456 print 'Modified:' print hello print 'After serialization:' print deserialize(serialize(hello)) end recordRefTest() sections.subsection('Importing modules: import record.') print 'Getting rid of the imported sections module (which is just a reference).' print "Note that this has no effect on sections's own namespace." del sections print 'import record sections' import record sections print 'The sections namespace has become a local record.' print locals() # --------------------------------------------------------------------------- sections.begin('SCRIPT RETURN VALUE') sections.subsection('Returning value from the script: the final expression.') "I am the return value." doomsday-stable-1.15.7/doomsday/tests/test_script/sections.de0000664000175000017500000000215412641367671023664 0ustar jaakkojaakko# The Doomsday Engine Project # # Copyright (c) 2009-2013 Jaakko Keränen # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, see . # Module for printing and numbering sections. secNum = 0 subSecNum = 0 def printSection(msg): print "\n" + "=" * 78 + "\nSECTION %i: %s" % [secNum, msg] def printSubsection(msg): print "\n" + "_" * 78 + "\n%i.%i: %s\n" % [secNum, subSecNum, msg] def begin(msg) secNum += 1 subSecNum := 0 printSection(msg) end def subsection(msg) subSecNum += 1 printSubsection(msg) end def throwError() throw 'Thrown from the sections module.' end doomsday-stable-1.15.7/doomsday/tests/test_bitfield/0000775000175000017500000000000012641367671021777 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_bitfield/main.cpp0000664000175000017500000000572512641367671023440 0ustar jaakkojaakko/* * The Doomsday Engine Project * * Copyright (c) 2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, see . */ #include #include #include using namespace de; int main(int, char **) { try { BitField::Elements elems; elems.add(1, 1); BitField pack(elems); pack.set(1, duint(1)); qDebug() << pack.asText().toLatin1().constData(); elems.clear(); elems.add(1, 1); elems.add(2, 1); pack.setElements(elems); pack.set(2, true); qDebug() << pack.asText().toLatin1().constData(); pack.set(1, true); qDebug() << pack.asText().toLatin1().constData(); elems.add(3, 3); pack.set(1, false); qDebug() << pack.asText().toLatin1().constData(); pack.set(3, 6u); qDebug() << pack.asText().toLatin1().constData(); elems.add(10, 8); pack.set(10, 149u); qDebug() << pack.asText().toLatin1().constData(); qDebug() << "Field 1:" << pack[1]; qDebug() << "Field 2:" << pack[2]; qDebug() << "Field 3:" << pack[3]; qDebug() << "Field 10:" << pack[10]; DENG2_ASSERT(pack[10] == 149); BitField pack2 = pack; qDebug() << "Copied:" << pack2.asText().toLatin1().constData(); qDebug() << "Equal:" << (pack2 == pack? "true" : "false"); qDebug() << "Delta:" << pack.delta(pack2); pack2.set(3, 3u); qDebug() << "Modified:" << pack2.asText().toLatin1().constData(); qDebug() << "Equal:" << (pack2 == pack? "true" : "false"); qDebug() << "Delta:" << pack.delta(pack2); pack2.set(3, 6u); pack2.set(10, 128u); qDebug() << "Modified:" << pack2.asText().toLatin1().constData(); qDebug() << "Field 10:" << pack2[10]; qDebug() << "Equal:" << (pack2 == pack? "true" : "false"); qDebug() << "Delta:" << pack.delta(pack2); pack2.set(1, true); qDebug() << "Modified:" << pack2.asText().toLatin1().constData(); qDebug() << "Equal:" << (pack2 == pack? "true" : "false"); qDebug() << "Delta:" << pack.delta(pack2); qDebug() << "Delta (reverse):" << pack2.delta(pack); } catch(Error const &err) { qWarning() << err.asText() << "\n"; } qDebug() << "Exiting main()...\n"; return 0; } doomsday-stable-1.15.7/doomsday/tests/test_bitfield/test_bitfield.pro0000664000175000017500000000015612641367671025344 0ustar jaakkojaakkoinclude(../config_test.pri) TEMPLATE = app TARGET = test_bitfield SOURCES += main.cpp deployTest($$TARGET) doomsday-stable-1.15.7/doomsday/tests/config_test.pri0000664000175000017500000000226312641367671022201 0ustar jaakkojaakko# Let deployers know this is only a test app. CONFIG += deng_testapp include(../config.pri) include(../dep_core.pri) macx { defineTest(deployTest) { # Fix the dynamic linker paths so they point to ../Frameworks/ inside the bundle. fixInstallName($${1}.app/Contents/MacOS/$${1}, libdeng_core.2.dylib, ..) deployPackages($$DENG_PACKAGES, $$OUT_PWD/../..) export(QMAKE_BUNDLE_DATA) export(dengPacks.files) export(dengPacks.path) } # In an SDK build, we can access the libraries directly from the built SDK. deng_sdk { QMAKE_LFLAGS += \ -Wl,-rpath,$$DENG_SDK_LIB_DIR \ -Wl,-rpath,$$[QT_INSTALL_LIBS] } } else:win32 { CONFIG += console target.path = $$DENG_BIN_DIR defineTest(deployTest) { deployPackages($$DENG_PACKAGES, $$OUT_PWD/../..) export(INSTALLS) export(dengPacks.files) export(dengPacks.path) } } else { target.path = $$DENG_BIN_DIR defineTest(deployTest) { deployPackages($$DENG_PACKAGES, $$OUT_PWD/../..) export(INSTALLS) export(dengPacks.files) export(dengPacks.path) } } deployTarget() doomsday-stable-1.15.7/doomsday/tests/test_string/0000775000175000017500000000000012641367671021523 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_string/main.cpp0000664000175000017500000000470512641367671023161 0ustar jaakkojaakko/* * The Doomsday Engine Project * * Copyright (c) 2009-2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, see . */ #include #include #include using namespace de; int main(int argc, char **argv) { try { TextApp app(argc, argv); app.initSubsystems(App::DisablePlugins); LOG_MSG("Escaped %%: arg %i") << 1; LOG_MSG("Escaped %%: arg %%%i%%") << 1; //LOG_MSG("Error: %") << 1; // incomplete formatting //LOG_MSG("Error: %i %i") << 1; // ran out of arguments LOG_MSG("More args than formats: %i appended:") << 1 << 2 << 3 << "hello"; LOG_MSG("String: '%s'") << "Hello World"; LOG_MSG(" Min width 8: '%8s'") << "Hello World"; LOG_MSG(" Max width .8: '%.8s'") << "Hello World"; LOG_MSG(" Left align: '%-.8s'") << "Hello World"; LOG_MSG("String: '%s'") << "Hello"; LOG_MSG(" Min width 8: '%8s'") << "Hello"; LOG_MSG(" Max width .8: '%.8s'") << "Hello"; LOG_MSG(" Left align: '%-8s'") << "Hello"; LOG_MSG("Integer (64-bit signed): %i") << 0x1000000000LL; LOG_MSG("Integer (64-bit signed): %d") << 0x1000000000LL; LOG_MSG("Integer (64-bit unsigned): %u") << 0x123456789abcLL; LOG_MSG("Boolean: %b %b") << true << false; LOG_MSG("16-bit Unicode character: %c") << 0x44; LOG_MSG("Hexadecimal (64-bit): %x") << 0x123456789abcLL; LOG_MSG("Hexadecimal (64-bit): %X") << 0x123456789abcLL; LOG_MSG("Pointer: %p") << &app; LOG_MSG("Double precision floating point: %f") << PI; LOG_MSG("Decimal places .4: %.4f") << PI; LOG_MSG("Decimal places .10: %.10f") << PI; } catch(Error const &err) { qWarning() << err.asText() << "\n"; } qDebug() << "Exiting main()...\n"; return 0; } doomsday-stable-1.15.7/doomsday/tests/test_string/test_string.pro0000664000175000017500000000015412641367671024612 0ustar jaakkojaakkoinclude(../config_test.pri) TEMPLATE = app TARGET = test_string SOURCES += main.cpp deployTest($$TARGET) doomsday-stable-1.15.7/doomsday/tests/test_vectors/0000775000175000017500000000000012641367671021702 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_vectors/main.cpp0000664000175000017500000001225012641367671023332 0ustar jaakkojaakko/* * The Doomsday Engine Project * * Copyright (c) 2009-2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, see . */ #include #include #include #include #include #include using namespace de; int main(int, char **) { try { Vector2f a(1, 2.5); Vector3f b(3, 5, 6); Matrix3f ma; Matrix4f mb; Matrix4d mc; // Note: Using QDebug because no de::App (and therefore no log message // buffer) is available. qDebug() << "Sizeof Vector2f:" << sizeof(a); qDebug() << "Sizeof Vector2f.x:" << sizeof(a.x); qDebug() << "Sizeof Vector3f:" << sizeof(b); qDebug() << "Sizeof Matrix3f:" << sizeof(ma); qDebug() << "Sizeof Matrix4f:" << sizeof(mb); qDebug() << "Sizeof Matrix4d:" << sizeof(mc); qDebug() << "Direct access to members:"; qDebug() << a.x << a.y; qDebug() << b.x << b.y << b.z; qDebug() << "First operand defines type of result:"; qDebug() << "Vector2f + Vector3f:" << (a + b).asText(); qDebug() << "Vector3f + Vector2f:" << (b + a).asText(); Vector2i c(6, 5); // This would downgrade the latter to int; won't do it. //qDebug() << "Vector2i + Vector2f (converted to int!): " << (c + a).asText(); qDebug() << "Vector2i:" << c.asText(); qDebug() << "Vector2f + Vector2i:" << (a + c).asText(); a += b; b += a; qDebug() << "After sum:" ; qDebug() << "a:" << a.asText() << "b:" << b.asText(); qDebug() << "a > b: " << (a > b); qDebug() << "b > a: " << (b > a); Vector2f s(1, 1); Vector3f t(2, 2, 2); qDebug() << "s: " << s.asText() << " t:" << t.asText(); qDebug() << "s > t: " << (s > t); qDebug() << "t > s: " << (t > s); qDebug() << "s < t: " << (s < t); qDebug() << "t < s: " << (t < s); t.z = -100; qDebug() << "t is now: " << t.asText(); qDebug() << "s > t: " << (s > t); qDebug() << "t > s: " << (t > s); qDebug() << "s < t: " << (s < t) << " <- first operand causes conversion to Vector2"; qDebug() << "t < s: " << (t < s); Vector2d u(3.1415926535, 3.33333333333333333333333); qDebug() << "u:" << u.asText(); Block block, block2; //Writer writer(block); writer << u; Writer(block) << u; Writer writer(block2); writer << u; Vector2d w; Reader(block) >> w; Vector2d y; Reader reader(block2); reader >> y; qDebug() << "w:" << w.asText(); qDebug() << "y:" << w.asText(); qDebug() << "Matrix operations:"; qDebug() << "Identity" << ma.asText(); qDebug() << "Identity" << mc.asText(); qDebug() << "Rotation 45 degrees" << Matrix4f::rotate(45).asText(); qDebug() << "Rotation 90 degrees" << Matrix4f::rotate(90).asText(); qDebug() << "Rotation 45 degrees, X axis" << Matrix4f::rotate(45, Vector3f(1, 0, 0)).asText(); qDebug() << "Translation" << Matrix4f::translate(Vector3f(1, 2, 3)).asText(); qDebug() << "Scale" << Matrix4f::scale(Vector3f(1, 2, 3)).asText(); t = Vector3f(1, 2, 3); Matrix4f scaleTrans = Matrix4f::scaleThenTranslate(Vector3f(10, 10, 10), Vector3f(-5, -5, -5)); qDebug() << "Scale and translate with" << scaleTrans.asText() << "result:" << (scaleTrans * t).asText(); qDebug() << "Seperate matrices (translate * scale):" << (Matrix4f::translate(Vector3f(-5, -5, -5)) * Matrix4f::scale(10) * t).asText(); qDebug() << "Seperate matrices (scale * translate):" << (Matrix4f::scale(10) * Matrix4f::translate(Vector3f(-5, -5, -5)) * t).asText(); qDebug() << "Inverse" << scaleTrans.inverse().asText(); t = scaleTrans * t; qDebug() << "Result" << (scaleTrans.inverse() * t).asText(); qDebug() << "X axis rotated to Z" << (Matrix4d::rotate(90, Vector3d(0, -1, 0)) * Vector3d(1, 0, 0)).asText(); qDebug() << "Look at (10,10,10) from (1,1,1)" << Matrix4f::lookAt(Vector3f(10, 10, 10), Vector3f(1, 1, 1), Vector3f(0, 0, 1)).asText(); qDebug() << "Cross product" << Vector3f(1, 0, 0).cross(Vector3f(0, 1, 0)).asText(); } catch(Error const &err) { qWarning() << err.asText() << "\n"; } qDebug() << "Exiting main()...\n"; return 0; } doomsday-stable-1.15.7/doomsday/tests/test_vectors/test_vectors.pro0000664000175000017500000000015512641367671025151 0ustar jaakkojaakkoinclude(../config_test.pri) TEMPLATE = app TARGET = test_vectors SOURCES += main.cpp deployTest($$TARGET) doomsday-stable-1.15.7/doomsday/tests/test_commandline/0000775000175000017500000000000012641367671022503 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_commandline/main.cpp0000664000175000017500000000262412641367671024137 0ustar jaakkojaakko/* * The Doomsday Engine Project * * Copyright (c) 2014 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, see . */ #include #include #include using namespace de; int main(int argc, char **argv) { try { TextApp app(argc, argv); app.initSubsystems(App::DisablePlugins); CommandLine cmd; #ifdef UNIX cmd << "/bin/ls"; cmd << "-l"; #endif String output; if(cmd.executeAndWait(&output)) { LOG_MSG("Output from %s:\n") << cmd.at(0) << output; } else { LOG_WARNING("Failed to execute!"); } } catch(Error const &err) { qWarning() << err.asText(); } qDebug() << "Exiting main()..."; return 0; } doomsday-stable-1.15.7/doomsday/tests/test_commandline/test_commandline.pro0000664000175000017500000000016212641367671026551 0ustar jaakkojaakkoinclude(../config_test.pri) TEMPLATE = app TARGET = test_commandline SOURCES += main.cpp deployTest($$TARGET) doomsday-stable-1.15.7/doomsday/tests/test_stringpool/0000775000175000017500000000000012641367671022415 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_stringpool/main.cpp0000664000175000017500000000612212641367671024046 0ustar jaakkojaakko/** * @file main.cpp * * StringPool unit tests. @ingroup tests * * @author Copyright © 2010-2013 Daniel Swanson * @author Copyright © 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include using namespace de; int main(int, char **) { try { StringPool p; String s = String("Hello"); DENG2_ASSERT(!p.isInterned(s)); DENG2_ASSERT(p.empty()); // First string. p.intern(s); DENG2_ASSERT(p.isInterned(s) == 1); // Re-insertion. DENG2_ASSERT(p.intern(s) == 1); // Case insensitivity. s = String("heLLO"); DENG2_ASSERT(p.intern(s) == 1); // Another string. s = String("abc"); String const &is = p.internAndRetrieve(s); DENG2_ASSERT(!is.compare(s)); DENG2_UNUSED(is); String s2 = String("ABC"); String const &is2 = p.internAndRetrieve(s2); DENG2_ASSERT(!is2.compare(s)); DENG2_UNUSED(is2); DENG2_ASSERT(p.intern(is2) == 2); DENG2_ASSERT(p.size() == 2); //p.print(); DENG2_ASSERT(!p.empty()); p.setUserValue(1, 1234); DENG2_ASSERT(p.userValue(1) == 1234); DENG2_ASSERT(p.userValue(2) == 0); s = String("HELLO"); p.remove(s); DENG2_ASSERT(!p.isInterned(s)); DENG2_ASSERT(p.size() == 1); DENG2_ASSERT(!p.string(2).compare("abc")); s = String("Third!"); DENG2_ASSERT(p.intern(s) == 1); DENG2_ASSERT(p.size() == 2); s = String("FOUR"); p.intern(s); p.removeById(1); // "Third!" // Serialize. Block b; Writer(b) << p; qDebug() << "Serialized stringpool to" << b.size() << "bytes."; // Deserialize. StringPool p2; Reader(b) >> p2; //p2.print(); DENG2_ASSERT(p2.size() == 2); DENG2_ASSERT(!p2.string(2).compare("abc")); DENG2_ASSERT(!p2.string(3).compare("FOUR")); s = String("hello again"); DENG2_ASSERT(p2.intern(s) == 1); p.clear(); DENG2_ASSERT(p.empty()); } catch(Error const &err) { qWarning() << err.asText() << "\n"; } qDebug() << "Exiting main()...\n"; return 0; } doomsday-stable-1.15.7/doomsday/tests/test_stringpool/test_stringpool.pro0000664000175000017500000000016012641367671026373 0ustar jaakkojaakkoinclude(../config_test.pri) TEMPLATE = app TARGET = test_stringpool SOURCES += main.cpp deployTest($$TARGET) doomsday-stable-1.15.7/doomsday/tests/test_glsandbox/0000775000175000017500000000000012641367671022176 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_glsandbox/main.cpp0000664000175000017500000000236212641367671023631 0ustar jaakkojaakko/* * The Doomsday Engine Project * * Copyright (c) 2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, see . */ #include #include #include #include "testwindow.h" using namespace de; int main(int argc, char **argv) { try { GuiApp app(argc, argv); app.addInitPackage("net.dengine.test.glsandbox"); app.initSubsystems(App::DisablePlugins); TestWindow win; win.show(); return app.execLoop(); } catch(Error const &err) { qWarning() << err.asText(); } qDebug("Exiting main()..."); return 0; } doomsday-stable-1.15.7/doomsday/tests/test_glsandbox/test_glsandbox.pro0000664000175000017500000000070012641367671025735 0ustar jaakkojaakko# Using Doomsday. DENG_CONFIG = gui symlink include($$DENG_SDK_DIR/doomsday_sdk.pri) # Package the application resources. dengPackage(net.dengine.test.glsandbox, $$OUT_PWD) win32: INCLUDEPATH += C:/SDK/OpenGL QT += gui opengl CONFIG += c++11 SOURCES += \ main.cpp \ testwindow.cpp HEADERS += \ testwindow.h !macx: dengDynamicLinkPath($$denglibs.path) dengDynamicLinkPath($$[QT_INSTALL_LIBS]) dengDeploy(test_glsandbox, $$PREFIX) doomsday-stable-1.15.7/doomsday/tests/test_glsandbox/testpic.png0000664000175000017500000024322512641367671024367 0ustar jaakkojaakkoPNG  IHDR:= AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  ~iTXtXML:com.adobe.xmp 1 5 25410/353 1 25410/353 256 1 232 2013-04-28T19:04:52 Pixelmator 2.1.4 @IDATxyeiywfgff:h$`HH`)NJIʮWJ\IT*+)$#l+N%$ b1;o_yMYe>wyﳾNnߚ͞z0y=0[xM40[p =p n7}sn-<{`s-sy`N{ mz;4;}.O:S dW;2|,on5ɯd+5V}:m2{sspgns]3.wxJXlJ ߕGQy8VB)K4I.WM&i>Kc˟I>Q2)/|n0֥^g}}/|!@rp0릛u7٘\l3f5}{߱凇ܽ(9>8'd0 6+#x<8uK&I~i'  FAkKNPl~1_ܩW˕R);;$Om>%i2~p91RϘ<9O|5ZH{8<^IXH\) |(8$O1 (pN~2MyjnRaҟdצ4!#PfKaR9)~7Vzh ?d<pxO& NA@9菆hQR(2,;Q d0N^o4y<_cǞ=h|s<?g(VMavc?Xw:SGCۘ۽؟NW`쇸hxzL)9E'r%suyT0( M` SB caf47 pLBO0H @2;@c,{A9'Q_n0=/_/[+/{/׾i <%A+aZ՘ޯ9 Ї,5; )" E::>oW@O \Cm\?#aٯEeTWϏ?_vU=,>ODMk!-7Lt&w@V o`T)h{ C-K3i.C22]PH<^ȼ^HsL7@-窍 a5 ]?>9c5,̈́ zo881K~ؑϞ:v[ <>C͏v]2,{߾pMk!M=Tӽ5fv wm9J2Oh HRd!4?N(KU _fǁ>AꫦH[ MC>̪0XL`d'as+CwJj@ { B `x.Ƒd P @btHh2DZbΓOs?jI1\]u]Y&B׮WSh"yV29~'vlf;=/j)πstƟǸ*_뮻?GcTxCZCy %x1M +R&^PU  `)@vJa&jvHYtN'^} 6 u4*}x\ud;;qj!>J|ZXI>ޠaŐ1ؐD +m zAk?u|gj9:=]?`+e0-~7G1( WðūO?<j%Q߹_鷥w}?/W )1Liey ĿޜiGHQ8PFz0T#n,^#__Y#_sE0kHd*!cFRH}ƼN@"Ac̅ ?)@gwF 2e`ܜ  bfe#{D[lu'A=tSi1Gg}Z줠n]9|羹æIk I+rFg 1OзH2z k0=̮_|9)o*:RU0@4q7L1Bb&;,ĄPdFUo5_e"8^# r%ExPB }ʔpv0ix2s yC<%Jx}jN?  Y7INuR(e'`]^n~Zyg_-k/e{ϹF@'\y9|WL?~m\.r^2v/1pDxTUt%w Ш$bkJ\ LxYt Wp2ke 0=Dnz}&i0 maP et0 )O2ˋ,ǨU-g<0"ﭫjW̏*~? oT?ρ@Wc?|o0WXT~DBA5%q^*o$@^Kjv>`$2khhfjp"`jr9T4DkL鐢8'ca-Lbra=,!8~ws!%x2/'VusJykF3փd@ht!5O34lgb‡a7c3Es AE-\&=^]Y\^!FQE(U VEhP+TdKBXj&M&TPS|S4c%t<@L}wb7Ѓ_@%rmj#F5i4jIϞT&a}pN>}Ց D=i"1P "JԽ^'KKd3/\ѻ+ڿ7GoA*@N>b-k\Mg?soɗ w6Z T04:)aZ5Yh6fҨT"EVn$:  }^S?`fӗqti SICImQ2L2D_dCʀXU!Z噑7<`>?6)XH2ZY#HsIBԁd`ZkaQxPh/~}tRl,%6}V'q|Ԯ"\5/1:\4jP„ rЫ}'?)ZKڀ9nI UeiT?L0?J>ć FKUubx a6_ 5^M@i]_*LeH'2pBKᵘ בx HKB>_$ dk3\qhe!N3yNLdr/j2<~|2Lm b` Xp :j79$h)½#j[]lme2񽦃 0jD! N L)XYSq Q$˷:wpzӯ}QJǭ-s\&_bfYz+ӴѥZc!qґf`fRoaj \H:da0/~=B:PCC,ێo^=\SWEgV{?nh]z?xZ}㋋x7DY"%Zs7PQ*v}~ ۿQDJq8')@:A8 (כRS-<$idB$W3DU_O~&YPjR~0*:hyQW8,=Hgm/6fG\+0M7Ϥ$vvڑIL15vE{84HCz9UىIDa2dYuaeL1XUzwgp p@`Ӄ\Oז3x<*!=dTC_J!&6>יSv@/wR.4smL0c00v0P=7T]c~)%R8zχ0;MƎz5 Bpxnu#!{H=1mW>Y!l\#i)`ԥ *7%g'$䴿ʊ;ajg0ἃ]6<|ePx[%l xv#B-~ێI_WUWծEx g'.r]5"-b㛲[-K8RW_=GWT  kg/bb'YfxM"'`Q%*oPaMש*ǖm%x&&S1Ae\(zc^3iT^Z $[OLꣅ(\.ldg*C]4Fd^2Bp-rj?_Fq C 'm}3gǎʍb2aNG>cˋ*|$հ+ 2 UJ񀆙o0;$9ԏ!exx_t 1W|)?cH#GIdPsru:OCם$]g .|y'+Ո+~{@hĦN|9#@-OFcLYXf!gסbY 4hA0FS_> ` nj_?zeCcB+/˘_S6w]n,Jyo1_/&k++xЫ&G廥daTx?q&yB CƻzY~.ێtuV*;} XDO .&Uo#O? m =a&EaB0?ĜBRS:Ῐ$8ͅ%_`0vDT,@vZ2$|4#L0%;꫙cfR䞨Md.UOq|z_׿MHtW\+`~u TpH$ !<9uË +v~ċOοR!WT@YםYt8`ȃA0[W$7σj2< ?*vp8^b|.b =,CWKP A DԀ<͒<& )q`w%!j\+ ^Qp.c=vzru ̭3r/$G֎$G9 S|/B-R{|`@"9;B +Qf^D!LSTZ3G0i*<(A+.F}4)y#%M CVP`d.kVc#$;<IG0&R^սz> Ð|e'Z1+؞2̋c8Eg26jX: ߂ =Zfs5JNmKg?s۝/Gă?H!~!KV8WYZK+/Ȇ1ƴ3򪵕6Ԍ"5$B :ؽBX*V:󺤖!8uTtdWDR!UùF#bF`MtSmDaj3QkÔ^" QE6bdBόUCASѶqTw/r09ctb$HϪ\ jk8EDLbaQE?GH|%3tX)jc[.?|ts_ZPEa2š@td,7XuO (.=Hq[ ^P7MB鬉S<2,a6pEDL@a9D:yų_zO>~֟Ye /{{0?HuS"&kHBNu4}k;j!BvsUa<;}{<0֐ &i!>ޗZ|@̻wX F˄zc47(=a4Nࡃa7:\7iLO|o@p:Zh`﷨9EFGS;F!O| Q$}A!rrZBp*5 `TNjO} P^^jo|Po: WW9Kw_S5\c//,ݧKXR#uq-'kkLjd*yD.1c~y(`I5P&?h%s 0D%`.*wt\c}.3~ ?V&k)߹ǟ@1 } d,F#Ǣ"eMI0h*`җ`a{8\<JL\}H#F]ʪ3*g1I`%puIU0~q3AUX\} ~v$H | @IA~oa.pgFtAe Wc7?W-?OV?]g G0o8kxHel2c`@F1$PA+bMf֣l6;$-˺N6f0ڀf-NTj os/3Qat cz܅V jT!1LU6Ag2`&B3 ;\:P3"80]]Bǯ1Fq/ݒL<#tf*12:u0)'3Ճ`yN8`΁}w4奤^>tm}g)W-,jߨ-gZ̞t:_5He %UH"SቐA6/G% H==Ї3wua$gw8Ϻx΍UЗ%ePVsAúրCkȬj ysy,2gt:s5exC.=a+KD[#/}>xgIĞ  xYeoWOU;FMn~[W3)T`~e"9꿶BN3Fvch !ǜ\hi}#?@ p}cKOz|2z02@#s3AMuJreQARC-+^Jj5Mb7!QuߠTJ\[0bnʯJn|> 5;-3$JRg",0`E21 c1׎C%/V;,qNX\U@ԯ}llu|6R_jPXm[[P/&OeW0.U*smWA!޴O'&wDh"#!_0)N""Оv6R_փMW/#EnrhC`2F_trrZp.K" .p :@s Ԫ|.oxb^5?4.!Rltlĝ# ÄET6J̜e"q~,6ia{LXP.4ع c"Hi!,AFԂff֊ڃm"3_+@@xYPQe=wBi"R@vpO؂N6;os~(a\I<wU>v^׌;zc6WjӫCm+38z/yB㪻9#%D]aZUR?S+9G`8hW<Ǿy) g=+UVOYy2$uѱe!8Z) 0TLFG*L8 DI6O"R1hoRDN?GJ'XΌB`?lsKOn]3f<^1KN}燦Gʕ2r-eȩizjR-V"!.AMVȘ-re8"+iQG!ȰaN30E 75T~$l>A@zDL=n{ 6(̘_?wkʵ)(b]Ћḱ:K|5YƋ,Cp!sy|? u3 P~OR상xΐ6mSI>NA%q6ཾHݕɵUb[cd {hh7=a;d: 1 M>V>;99[/oh/xc0m#);Igah L_gL9H_Y9ƶ:H="KJMt Dv,K ~a~|a9j<ķ2 2~L }QWQT5ɬ\z/|' gݨB.>|)mwp*;]YYUז?STP-PHoiT)ĖԙS !g!_ex/uy.f2rGTe28D3q}3*‹4ـ0j?Z+ hjkGUa $(Q?n;G67¨֑Tr 5VSFG*P'& mmBʇó5/)\[YM^<{|q.?3tsS ߴp̯gZG#a#8pL2זcNQ} PX?[qVU~TC nz!t?B^ߦiVޘ L='(6W…|Cp=7 jr6slAa2WDz:Pme8&CtL@[7J"m4MJC } Y)ucmBYP3@:zF"ɋ=6@v3a݁<ۖ8=)WNHW ɭˀ6wYCѱz ?]جϽ=Ţ(|Xgjg|{`.Lj\PsUhL74,ʟQs;$;JڽiÃ7-ivhg֑mG`ڨ~\J2 46dzō!/J)`ZzOp.҂yܸ©nv?LVZus:`T?PU#W_;e?5#9W%w7Щh嶦Çq SߣϬ/o+2_0eV!` d0[껺M)e_M\?Dw2 ̠W+b" Ry3ac0C [b'm՟+6Y p֥W6†9gA3PJD,vΝdAv~vf8nJ8KIWK+IGPlP׉wju]{8ye?!|?e 'ZgJu6<0k]Ri{JNv#ծ)92@{Y?&"kp_0&D Jڴ%Opшr#=' WtI1XȽ!Y!x]E0>[j2P晢2KQgZh?lYv#M̜8qdXid]4X{;դ 02T[ixu>pй:UI_K 1iӆQ.J wa:@EP}G/!\P#*$ <# Y 7,?j-z5Hׇh Bi&K/nBp鼋r &`HW eHҪG~Sr\ @<{NfhCs lD텲a9ۓ!aD'%;!V}TCMLLI&t4`rC٧}d5gsba\IKSLT%C8!H>x '9C9an`4Y:EfWNJH=}6<e )Ċđ01}=:B[H$**Ljpɘل*Zb9P@ {xBsgY6_ &~s :0ZmpA.`z KJqac=rZ6lh ڏL,|.{gFFhLwIBwAX+*IDqW5Al]+os :{<T&%ħ@ !HR65JF\īR^ '[ % s3)n֠ZїҲD^zX6D[YPU>X*p;ßmSEg&%|"(Z̀dAEvhV#0â)*reHh| 4Rh'l ILm<g'}l$j}Fp44Cz 9^ct\~{sO= 7索+o!P[Lpi}\.8~*ɰXњVu0jNj fgmoo:UajYLO=Lp91(G`Ӱj=H=\iSl_;0̯Ɵ{67 @1gVמ8rBf*ZDeP}!e%UQd@RqT, űq!Bc#ۇ,' 5!kMpG'Qd jb5m 1J2e*"6`y0'3w z(1Pb=pVeP^~]W/m>zt g*IH |pE"ZIoMoOOR+ bVcHɂ&x(tܧzĉ]pOJ6 +usZ_29XwMP؁Tw``[A|ΛPs==h-ڷj*>u^B [%S8[+?n2d,թZ|ӏF»Rc@+|"Y,lO"ZlNQ.G֛3Zk5-q`b9RMaP~x י;?L# . PCՐwV?2^k !X7ބBPW7Xv8`\,]XPy&D #@c U,/ m2j;F?eS_JY)f(ap]f*\%֗ :n'`Ui'W:&#f!;Q@h .Ve5 D=;HyCP'hC]Jw?kF<&`'OE776 @הPq|˧!mNҊg O?BcμH2Z?Oo7.fWi?k \oa@%':;{T (qt8G$׆Ub 7_\'K?/ȐDdz|7ehh)8ٌED$qPc :a}p>5| | "J@Qw"|z;,R)}3PPg{1$<;E),3lTq]7m=SLo`2b2.3 l /꣘ ,>kt [cꃁ[aY5p#:K3;NOVu w䌍<Xo@.pe^d(¤RZR v(Sg<Ӑ>2s6Dh8)")BD)i Yu L3c^SR0T秦xyw k.W2;טА\]S98%Z/'NމWL}9RL' mT]:'n'Y-5dS-P!궆rmMAVFDgf}YK+GU9L^0 ڡ TT=yuڼ0c(xIcTF%҇;WU--:WG1O{ ==V}$Ze@YwKL+.tHBfHDj(42JI`jHh8>Qz!P'jRi}J-fڎD&M@rR פjW:ҕzmo6>?$Qhgw>SFM՛J0NZoh}4}Lp1뫶 P3A12doﵠTa+E#>\PgӲ}`"idOftf|PkUHzt rwAtQìȃ>?~Kfٗ q ?3҃AQmǘkUMip7M4Tcl$₫ՓEj$w\, '~:h$LX 2cKSps ϘT6mFB֗Nu.xj,֤"x؛JLbA#ɑw V[nݟ:.!>#4y'f#.z90Kz7+¦;~/2R2(#FkR&p u Vdޮt?`& ۂ$YڏZ1#L&2d/a \=2+]S"aAj(˜wcќ 6::Klϐ2Cg|-G }hi..DLRR1Sq^Pu= tS}p:x_˭9e|^>Tѹ4P;>?euACnm B(&@tv2f@3/ڃa|tO$vsWW<Ϫ/HK%iC_i%8cJ(ڀgcHSE+sٵm =QۉtH~MIEV j0g$&Q8|F{UĤ|y$pDH:\ZouS%Gj8~|4Gj;GWn pCcVښ <5&s`ʉ_}&0!<=]'1N>\<gfFj+{r0t5BA{oB#8ʜt}% }. 9q)-ZvK(A`<9]Ͼe1eNM47 vKYɫh{Tx5Vx~{u O^ |ჯqD;_'Ojo[ZAө-f-D0Nګn1BGά} M3%X6rN-4+ںSAT]Y@P7Uif p)fɄ^_fW %sݼBS@N؋O^&v:*]᷏kz8 [W 7c9΂- 2Y6Z#Ft/-Vd!q#}<'2& pW {d:*}H4~V&~DŽ`Dpm$:d"8`XW\7@tc!ft#T{*ye d7:Hm'Mّ%tpT^Bċ~HcĬ@s,ϊ5(3W. 2WeKo_*8;8y8]r 4n>wvַ;E${( pė 7EV `?$h0j&0xO([ ́aMxR '2'΀|u*X_" 8æPBU-+pvUQc{Edir:zP0Qhj+1( ͆4WU2qpo*, =G5d>[<0*`k'P:$T0 󴏕+.sk ꜳ_ʌ'(u :AI=>9s+`B;0c0.|7YVo'R7;xF >! e4PnO]<&Cojhq!wܱfmvHƺx¹wsW8--dhՄN=t w`o/Xbp˨V1+R\m]b d >b TOM Ef!yiHKƌu+,d YRf<لNz`xF5 L;2Ԏ(H%~[jm@|&Nf)9|d}EU?%t/)QFx<3( TCz+!}4v8u;X2T̤ aue3YK0R-w %0MsvPlymy]`b[ߪ8n -Î!f"GJz$k"\}AH߿T1T (%UAjz "l,GGu]?fùA'SJWU1a*:wT5K*@RJC} I \(!,8|&\:IP m9)x|ڤd(l AV~Q * ~XAeguvת٤-1ʜj}Ör6Ԍl;xG-!N@$0'}eh7$:ޚ( 4eڂ X<$[@ А]O_I^-ԏQ7qDM vJqtyU:wtG).)<0 f*]`kGz*{6'4)Q /&g#33iWI#ݏX8Dn*UXYecrck"ct`,x@!QPwJ<'2uµC/gLw(ESΎӃ.(  եh:φ~;[Ozi={{W@#`6`2ʑ-H$ɮ}ܡ+n:a"gTudՀr|^lZ2ʣìO`%SL'S{ up<1tC ʎ>CEyk_N[y"0oY)j%- k.^"f9f/'λ=׵!k.`9goZh./9Mehf$ 1؄\<@<{2[$X1 A Pbhs#h-Xkg?>,DcmJة9ogLV`pJJ<Ui{d R WC ^"sh׀iN @Ѓt yozR3b}PL.6>="!P*7@#o5Mz-o[6GaI T 7 Snm3׼)z,j Oߢ.DZ 8җG"22ϠEO_M&)i[;>?FʥByр0v]\m@ѽW@ch723H @B?9w~=ݦMmvVfIbE(34=j >pܻV7yp-3|mr=gcXa)²]ieH hc?ه8K8^ )U"@m`,A5Hpͨ3Up| o|u b8 k-4bxKht20ГpQ `Q "cT zUiCTED”VD4LrY;6ZM.gfdADQ@3D5EhQ6Rq@7 S16g7tmkt>kZˠW y&fs* ]݈2/ eg&d{$q=‹D3OGRM}!k$E@ 1*4yd\pc!@gi@[s  è]Jڗ,3Z6J:cǘ쐳 a})ڈT'sqER_L_8lo+f!Rav3L`]{h5M/`CDWx?@ V ҁ`S$'2uE2sl5 v~j,5ie\=/ǪfCD]{ ]cKrL^IOhfjɈF21鈺NO:GNE)y}wj3z}\tm,R ڋJ~5/;HFh4A̒;#0Au5Oz$uAaߺdSI5)x flgh[}ĩ;&{ 6Cm.Jd<{Q_Y$,,Cv@Z!F; [`MTTͺ&lׄy6vv#paT%BX4kE$ ̘:#@N&7-nd 30~HTj_LkhVs$5娊ǎgK#[hדdFy")&$b@y9xMͳ'!'p[ɢ6<[(a ʔ{+Mx#V^Ե} KKD p{b xgCi5T+6Jȩ|5*y)DrĝyM& CCx]Vv@`}WФ@jnBpIq}tcfDtH{@晴cm"0:@l'1Z/r ГxYm Jz\S#hr4$ RC]\mKX,-GG9 ZI}0x_SCKJ2T7=U$ !x@Aئ?u?%61[c U $!ˬ2dD"Mۢ:rcF&5e0w2g7Y*xΐddDtEڸ] T'D WYKA` R/J$Y[I"$acc:l%|3y'$% o~Aߡo6w:n~P~˚f=34DERH_;ҩʍLzhaDN?P( kJwǣQ8Ȯiϓh<0觥CiD`l"QٙF2?JF{:l]ڈ|mqr˄)JTdZھ9GBV*ӂae~6󱨉+G-HW~⟑4tL]Da1?A871|h;L `6gI}wfkN%otLVs@A\HBKhh'*jj:HƒR,ZN):))gμaM6"~7zTREwY۔Cfi?μMl-T.&<F!P^D#:bIoUZMPPBZ1*B"WؑhSiϚfcH3׺[HXFmO'~ &޺t&YG3Qu9{ aoTL:h-JwV5p֟.[~۬!}yՙb j3py8kCB2 ~Vt9L. b R&\Z2g.G5/1(&ԳES:<Ϡ- 'EvzU Ɉ$SrH}j_$L5m lS =@S@ CD" #0mMCFu?{?7\O[MN~<.Egij- 4fp6^՗}F=Sӈ́4VugU5$ $:m(Ct}m yg y=IM/kGV4 :Zfl0tSM#RfygX-̦ÚᗒDﲏNןx:H*iMa l\?Ĵݩ:/uFE7ehHRq!R*L˦խ{yӜ|)[ .65+Ƥ/]|}ˬ= Im"ΙƂ2sz#1Х jEV(E9$tة]Xj}Z|0{4pȱHYGhҟvmu-fIVp#$(e$bSǨ0C_Y[ \*۾\c8B!j宪 +C锁ϏIHMM?rt#LHSwwcbp{+ޏ2> o@YPU8]?!/h[e;! F!~ՉM RFFz.m3l(.5j$`ay) hb\Ӂz~)Їi!7!iP[,us "c.J1vS3*x#qL 92ܤ)| =>,Èf8f͟@.W򤧼HhzMtY#w5kg>bzha>93݇(j\MI\#E@5؉:5?DU\Zo۴Z{ֆ-qR!ya'| 'X*(ˀ 1U8,x_!Nj'!ګ%R%*%bؿ:O VhI GH 6-*R#Xpd Jǔb:o@ҏ9X8sR8֎@090Ҏ@,;:$"@)Añ]C*efQHπhoM$ҩкc0BP>{hnbR!Z+sk#:+F:ν]Ƌ yjT4Z-$O?t7Ϡ=b ?i*uJeWN7ʥ{7.^ga4k4q?{-WoWF euRq'`fQiSOr.WYer$A`/ ~yASļ9sOakٺuj56|I< ^& bf'Q'0/hFA"7d$YEG{EmDx}(bsyHfWn8eS{OS vV?^Ր ﷩IUY#.Ĉ>MvsNZ3SȻWD;[ʆtveoB$H2}nNT c Ҍ#n7Ccj1y;_Ë'VOeKUԢF:Hp}fn 2Ous8 {a~a'F \2f:/4Ӹ̌񠥣ڹUe'M)@o!. T{僯V߾W?ۋ00U[E+q0ێ7=tfُj۟SMe*i#欘ij4XM,Fp8Uq04+KH@~_o Է`~Zۻotl{U7{٬LR85*CS`m5ڀ]j@) i61lؽ{;j䖻0O RשpGQ[n߾ o/zlٛtMX-2|PPOk`0vP:8ǏhJ6} kZ})7֍WOGbqhDOä{Pїmyyi?n$svD-u铵WL4ML Q>yqfETV!0ԉw0ڰҔac;/X0L7^Yj Y) IynDc/FQCTM=nIǿNٷ c$luer"ΛBȻIZ&_ %6MBRۑ6Q!~;iig1t㲝yA\@ALvyel̜`:D{fw縔`w{I8z^c9M>(\]x jw6|#E!Ï?(87  L F}^j0iy- \wގXkSob xSi7s:AJaN&/z[>~MkGG~\:ú7yߛPf2P)O& ms!u:ȇD A:W#@v@CP<5u8[5'URN2 cHlsyxRFb9 l\/#31$P IZdb>vc80y.O% g^4ONa)\ލL^l\~DU93M }$ɼQf=Coof_luDVZЌ8.WT{fxw}^w0C~/dA<+/?-J˰O1r7ݛ9[y4oϙGv&Eb(_0,zr>Vj ,4Db]S tzY%N7ߴaxT_ ВewZ ]_#$, hĭs}=>:i9*&LM7$P*Pa7ilq'ضu]OM"\|R;h 4-ZAvGCev pd5sDn)EmGDqRTqS]$DRP5ްOv26<+YLlRID#(f l*iR)Iwm~wֻVƵ·]WCă@_fo7~=/N3b1;ͻ̣A88"ʚ` X͉z3Ʉ̓bZ= RW +j^Gtc %U9D?>U$~53Abm8l͆$lWPZ' P<ע΅Elh5y*"#8͑Ƨ3+F8xe#}@bH]|5鿡U>_voſ 2yi7tGcu`:GGPf A& MV!U@r,cJQcsaBsO/k 갎! wjý«sv艸NV>N^VxT\Ī:){IOv#9Ӏ.9kj>\(dzZ rDHGIQa urA>N0懸Zf$)2'?Z}P;=@ۏ;zY΍j( kɷo#%Rɂ,Ѭ{sN_NpiiE nFjRf(vt0ޤobp}wEs/rkx9L?BfFᴛRzb~~􆚻C=wazo0yaUy<џi/ԋp\*Z5^{!6X"]Āw5Ba6n _LعK:)H&3f l%/KUǙ\>+G{,M6+^܅FksifI8ZGj|bqL @`45[DpN 7c ImY1:2S{wgia+o>&AXݘy3y5 F`j_s7}33u6H{q݃|8=1ur3MJ?DIL cN Y{z閟O_޾w'.!8/׎~ZH&)^k4߯=[7R7ma ȼe jGz"^u;њ >u.#fnRջEd'EU5xrQ$"F!$uWT\.kqߏ`4}_6G]٢;s%C,FEqZqؤuLT48[-x~9Źe}vvrA1G@QwO[ (pxz|V:^# |q/2.LNA!τVm Lk[tmQZp‘lU{ه1i/z0ٱ6f ~"q<6a$Msx~_>GJw +-H$2 iZ1Gq`V$X~<}4n,L7%5y6J«z0 rJ} ŒDm,u͢J0pVvpq3-][m׎69OO.&\ۏ=V\0,iQ\x$ \ !&5#:?2N]&W80N 7ئԸ1h,17޺[ rܙwFz4kP *inG⃧֨7!3'01 ,o;Şkf4oΫ_I$@=o/flH+yL.W$CZ[ÎP"ݒrp~Yr["i- .{ߛق空ok3C$j\6F /Xtf35 `քœva}E_̒1o^25A 9bq,¸Ker-VHm^?OږӠ)9$YnΤ2)3GnDn@E6Rͩ[E "wk&Q|$dmo?iJ]>Ԁ$qLXCxL+*ZWq jg- 6͜y-m~sIam?ң*kMCl%=IKAJ0TΩ+4W3G L);xSqc)n +t/|}v _K.-|MhAlo<΍O$M'dG<7[ r3uD|=>Q 1ՁKZ=8S7OmٳTgje{T`DhA)4fi050=HJK"$9<:@+ᇻ_7so"^YM%Ί?q!~6 UlPI?2 Fa,/?䭣bꮌ@YN "3(m']=[=H=iP7Z>"Ug$8_VXsq*^1(DձhcisjZw74LCPϲ-t`lpSQ1v76ѡ-?-"٧/|cJ˚;b-Q6`n!oݽ4h<-XDڂ#ra<~<3[qrTmOgB9?(/w>+|tm(ܧ'滯Wο֡K9кu7KPΈwHB2ΓroS9P  qdyƊ. aKSE5nDBmG:ɾ4&3Q-x0TB ΤDN$Fh,e ĨM0Iˍ\Sd-د z=g]X]rz@IDATjG]M4Hᛓdxt `:|itB*T'j>dOtAywԟ^9KCO`S9PtkpcОbC0IwVL<"E} f_ci?ym =7p9lC:[a(m?L659`4;kq2ñqpƆö_}Am4FFwYU" L\)J{u>v}143-U癙 6 s ZJib 4u&E#\_4Z4z}Lw! bӿډ{ А?!cFfG*H0i$Y2-Gel2M춘㇫?4V ;'cnCH oԮWI v%DF%|HUi˝a ~}wW~0olvȑ!C=g>?>SdTeto #"|9z?~ G#A 6 ? ٬;kL$$&qZN.cZ=!)(FP#VT -_d cZ7K|)EN^0 peK”M<` <*$1f1wzgk)MՂo X;T^bl‹' YR:Kkg=vz7 6d>7Zjx~я~+x^^q `(X[jvsj2.?h#)D* 0Y/8m+v$hi7WKgY(CWZDB?,QN1pBF[[1g [-zTN[MxRFkҕD`D)93]?3w T0.Qy6z٥GΦavƦ[ߋG;Ģ &_owAЇh!ҩ'9`o#RAqD D ˴YPy2l6`[fn~5t9prxm& C!s<]e},0xSDcSaT+17Z_D)*=8Bgyag:pAxVa.͢H~l? HI9[`FKTh6+afLó٘1䲹zb,->bu#՛rsc|,mp 'v ?@ FhQ 9uLcő9F*wE?diI{fzܩNI4y0m28׿Y\,JG;I'8$ie<cSBZ]@!GaBT[ǯ\QߏJ7'{[-a*H[4dj tC?Ѝ@QESc6y`6#\G,Feje|H_7[aC4&|A{/3f* =`>Q1_0[vehUcnyv݄L3ffMC\X{ LaW)mZ0΁w=3kW=C}]fCGoe_vFK? BHc7[u!Px2 <퉦R'4ɖijԓ&Qu9Âz8'4β3#Se\r2\%a,IV󜨦יhH5Y]iν2|Ampح2ߊVU05_וLͶvֱ_(RKa!i,FoVUdpp+mDž;1A9̈ j%2XdDvj]Eơ9pfDæu 1 ؈uѿa]oEX$һtSEOMIu 1|%Dw-R 4ղ]`H&`fTe̤~!ypCbZN2SHӐj01(0U+sh5c, Ɠ4km,"1mF*[I_LÂb#%PG[ ,е,4Al!1FFkwB1Ob F*ԙWd3),jf_Wg=6`.9F0M18˴v!::DZœ/f5W×rS_,Xk<+L;ܑ,1>ܣрrsq־ Ka57tLc֢vYݥ9Ҫq87y6zԀ0kTH,һ3&s!E s4'UG*V[X2jŕo@JN3G$/RWqjSPF+ ӆ} 5j1s4IJm(kҌ簔tcYTNYf*B'2G^ր'~?EEۭب?~g`@^ y3H\ْz[е303'iͼh}_JSY L3j2HqLBp>VD*H4 1ϲ]̡0b6Vh6JS=L6ڬPECnc3C3sSR7rg '"dw/;˯M:M=y=@4 Dj8^jH# 'MB [P%, 3jF-} ѳT`DdK&BMfM}uX<;ǘ Ϥ9cl|o4d5=+cLv Aj-xP?⩀!?   }ol_IyS/(F!=6e%'Zw0e|{dZ&&iM1ݹ?zm66xяSv_YXfa1u)z6'$r8>"w.0z %L`SU#GO ̯7q5מޡV!N'!Զj8Dj-PLqaXlvLRgd3k;frI:"Rh738Hj:esL P`mڜ_A93WHp=86nm`~LBYvŸt0ۃ/~EIqPVy2 Dm6&f\Iu☍:Q~k75%mFpe+!{@xYNϜd$[x1iC5(RBe1u|>-% :~AZAΣ!8jYHewDf:€T]D0 7oX[tvw88 -s"$}Zu[Dc@0x bfI"ZuwX;30}NZL)0dIO'BЍvHcA$x=8X]1e瀼Ѭ`fW ҕV;!"ew9"IF 9raE12ܕL5"*ۨ #'Lk6Ͽӊ?~3 e6M7 ƽn`0AlkR{鵉/@:.)Mfi]m/,GڦJf:) ]0x3@BQgʙyIn< Vr8[}iWĮϴwlbmt;q:S8%ĄTO~҆=rRؤ4Eʕ剘in yyRӢa(V9`sޢrjz}ے2\Ǭ-C.i$laҖ t.Sa767KTI?9.ʹgg:7&5{qJ՗Ӝ2alπy6BNһ1jZB#IUۮmU8CJȢ`,桍>mj}lS~^?v5>Zl_9ZaJ>L,e1% I2<յiזּ?E셅)^$?p|8v=p 4 zpv1e(X9.j|T&d6w{CG #3TИ8Cidќ6ѡ!TL핬,\{1ПPDEWS"J 93J/}~Ԭьx'fE˰f]tijUWնѮni84Oj; )*hpb$]̜ixE91\_/nn_~Q~O#8*љfyI @nnv%`\4.qtv!'b 9c )5"'B܁bHl)mucod# ݴ1-RH =jނiM:ު~xGX>‘W;i!g90`cVT\ӏ-A**%clޑU+~bH`ђ KvۥbVOb21*ךXlEK*g)T^ŵzMMa:t)u.}O.J&"t$9] C@>3<7 0(T$y@͛yT&">6N6!3[j-S.@3T]RꝱkŴ} ߖ GTa"jqRw6{|$.6B&JjʐL1-+fcL_4Gt3DG_Qa$_ZI};A8 {>Fv#brhA˷%%9?:hdeR76F ՎnDpjp αxu#e`sR̳Ë<ʡAͰY9ϪA#vdL#&L^L58XyƔRj2hU > d,pGC=Pomp޴pN尘,F`EՕ|.Iߎ׿kv|ms {բ)P~sQBfjޢէs2س!`5ÃpܼժjG'J˶\J rL 0 3v%xra2h$|2!sV4!hݒ 8/2GaM z%e?!5.ʹ dvXJ,gg޻`k/"omg{Mr0D`mո!*Oڰ˞Vo^V8rKIxD~!nBv^oڹ,Cwf4`)i+&O+Ț|8jqIRHM`cE>)3L'c9Xfm((wܬ- qu_~<3jgP_om D1XLA!`ܼ@GgAd[`@J o6D? jTrͥM1_TMU폳𽑞ya -5ӻ V@ ns,D oB!]6Ma0!D ېӤD);4rÅ- Nd.Hų5M;pxCsvMQ|AFI4 '@#y|LhjiŬ~iguُK |U\FDpk6A ̴Bn'fƤ:/N"#dY1!, ʩٲd~luA G$Hnʻz-"0?ٺ| ?[@4࡭r:38i&|->*F; VӴJK[^Wӟy31JeCFAι_<^Uۘihwx}84ھ9N,Y<>p5O=.^&mM@ IufI7E $Q t17=Yt[OD͔i ҅49QHpwG1`:0$N}\1A`8RV3 x _Ľv穻1I9% 5\xrs$cy/WXi,+>>0~H *S 4a(;a0 ǞyA e+6O2ĈMLDߴPϘN|oicx3 *6}w#{:F9g\r*2$7vG/Aad|&=b/Sf u'RP mJSFTˢS֖1.eMMa=q}Z301i6 3"MQ$&vgSO-p ~1}(L|qi(vb/gip<N`USGmQ0*Ĭ҈LυOW0v\5<;iPpƼT73XԟfiD@Zyp5bi,͘a}~0ļ{OoXsڳ(|7̈́޽T挩UV+B E`1?rdn;l|mzORYY:썧b')ZkFZ6D*@hOmR?̍7aq )߇ Ƴ>Y}Tx*rb#҆ԁ)umĸ¿ӫ[WĆ;ܽ(t~_ohx`V_NVfKc&U40SxPٵ%s7v!Y:,{5LN<ڇt dpc X1W!WYXG%Exa paPԘQvv{9^FTؐs9CW` +W1%9iSj/R9f4{2 w@5; 9Sܻ_ O^KIS7nM>b>i[gb yhLuxQCڌa-s6oi3L%as ON: xNjT>L imV.7NďgVb2czNS-LuR{ial S–2,qm^bu"$D1kF17g?6Qԙh@BZlf#d.'ߠ7II!Kx0 bF0}F$!:DC $ Hu=~$'c9{0 #xl}!)wwpKI r\d]6Mis|>-uM1a]r^jQ}YAw<쿦Է_?.0/~_ֽ o>&NR g>7$S^{]yؐ?+]'OVow<,OjTrԱxʗb#Lt>s Bv%_IDԶ*B#apo@q^tI N5<竗+DlVA+]2 avC;H/mAؘxj z#{%>69]Κ#T$`){߻5۫_}Isǫ['L1Qh$?5Q80mqEk (6|EJo?DJ7Ig2g<&Ief*}0 &OF]ҡ b?f"Cfсv(>sTI> Hik F AN[DΑI!4lFVڄ4ӌREzkT؊3 ĒVaRN;W<\ xs{p+Vpe"(Oغw֛eْ=--y*(UtjT/:t:"q!L7lv?BK$bo/ẐX8 {]t ܾwe>._o҅乃L-f[fDϟCۥ'7VWsz3~L`1!6^ GqG,stdˤ@<^?`/3k)6rm$uT*bbp`10=1}Jc섇FFY 12m=Kn |ΰ6L eb3'0-l_$ .#ke2u|ݹsap/e." I-UE4WqN1A0@,B,6pȟD|! Mi:vOA۳QjQ{{/ti`"4ۙWŜ9B>o"wZR<#jƉ3$|{=߫1xW |:p4FA0[6Y0Zc&:5c fAjɝ,^C?r>l4mr֭zr= f9U#1ZY AgZLD`U@tdBPԿ)֠40V=A@ʵV͢M``eȫOH3UV=myG?$N3}V[h L#nuy:ݧ4Q{޸}gOTm !@YowyLÂmȻRg`CkXfRm2ND[ V'^T)WM>x ӣ|?ܡ 8 8n*L}lL[ |NqӢI_;^kW^_zY `yQV߮;Jngސ0|Ŏo|sUx'oqY `݀g~铤G/='Ԣ+!'?`deAX$9F56 7ją*=z 4w!p;Ҙ%Mu/{vz "MNR\Ć+oÔdİnZ<ǐLYHF(pe*Һ#T:~A*G|?75 RigjYﭛwVos_>\=$3kiUPjjL=Oqh҆bj;(J^,"ʜ-l  ,ci ,^.3p9=&:1`7"] Y0, !P){zc1F5=&>_2Ԩ %#ݻf"ջ_/[3f0Es2z׮73kuT{cڏqu1H-62{.t:< gV^|;w}\& \7\ֿZ~|͟(IpָF9}H` T @#%"T҄q>A430g ݟ!˚5xy+YfaVX@-6.MB@f~?~[^\C^泙2!>INFȷp\Xx`KW8(+=F`3v? E_Ο/ޜ܎0۴=U[-C"DfL1-.&CA{&ZY;au,#n3{R9m)1~1t@Z+xU0⸩3 JYu,"~B_k)NK7DxؽY6,T6+bf)iiOӸuIi^ٚ!tҖ0R]Uo^݊z`h};ʫkοq @C zΧIػz RI]jQt,hQ)g_#:Aا{f}aU0 Ӆ^p؀1y^)7zo<F݄T e-L(Ɖn[9OJo%!m.4VLT5@,~12xiCV)޽Ou!H$݀{KHHӼ>B x1vϽ@={$qأT.מWDwjBmkb1%aȘ^1}Γޘ Gza#Mh$iO+w}wbQ6j &8LI B"5 Ey}ݱ[-XTr%[q4|mǶb4T='xx^TEw8:ǥ0:Z7V֍=lܿxl&Ej6/Ө<pqdAeg f$ld.jA7y}]oAE0[1l|#"&I'a{hǿ?W"t٭Hl!63H)(R-7m9u Ybl'a8xN!N@IDATķ&tT_CLC$D㘥 Qo#k^PLovDc6o=}O$9 = f;m{H jiqdԖiH*L CĄܢ˄BIΙ!y=E ?owYs?IvU֝‚R;.GMqI'jsL9▒_>D!v,~#{֛%gZ3-!p5@u`0a c)kp#{ޠ!5i+}c6٥OҐq՝ˌ,>yX&@Ba[ď1*(Edj_~yKժ5MZe3Ѱu^^sօqșu7J;M&Bm^O_5+R8$sΖ#%%.\jh.R~}9o<Ŀݫ ќE{9$>|lvؤ)у4!e 4Rd#ب /&=qtC%]HTl@d!- -GhfoHvU~`k 1t'/H0Thnݨ$~}`/~BGl1j a;@J|0 tiF0@ص9]h`:^-c;tuZ5^hֺŷϿ_<wA*g˂s,5iuFbAs7N?pB׺23Dt#?I:NtSqLD,I#Fڤ!rՉ ;񬤞RS׼+Uf0Ķ_1SP&H$2UsC=\LѤWϒxd T o4X'laJiKZ{{4'94Yu?GN+-zLeSmt7z-IB05ˉ3I"YN\S.dCJţ2E0!ǜ#(O%-KӀg^m|v8.Y+{?Z?Ol%d}[/F Vm^erͿl6x^i{woyį5>kzYuk:ǥ1nC+OZe|Iv/d,!0\͖=ϙY{8f4#ȗڻŻc˅輻;yC֛پft;;Ox)<]\ vgZڟж{vrDQW4AK 03RLdY=^ȳDn{U9V{_g Yy|8qd> s]TVX@ɨ$z\1m&FKi<Mb4fJ| uJ@^C) W4V1ZnkҬ^qߜƴ+̅\k=/IE5 Ù} mTYu?d0Hlipʮ1S-Zhv2KMÉi`S#gW(ґWtoӐ.RfnsX7$a*}!a7'h[!lVmGɱv4%gspAlʧr߻~NC~Z čF[ Mg8*ˡl8 FPfMy;DMQ" Qap<Sf&)C/$>[5zfӛD޻в6?OVϽ$ Ly:;'e٭vujў^{w3v\%6DT_)bLrb Xpst1GSU qiU3!`1!@S.g̀Tјڹw*=c<1f3 tV-`[X@P«A/V7t5A5cÄa\}f%8NI:iHs֭Pu^Ioc}i"jS{IɌӔf=,s$OuQx KzlI R 5 v|~NnPO{}3A.hHf& 1Rzqr~Nk3I>>2&U4})Iǔ!Ͻ\mR5Ib4[yeي<|ǟ߽9hA/F}6mE/fxSvD+"Tm1Y(T>"Fb6/3 vCl *yޞ4*qO ݭZmɃ@1]tAO!~vQm3L͛ņٲ6y6ϯf%)X?ޟjb|*ͼ>*yWR$x7(9C'vw=g Ci.36S/ăI0kcIMͼ>)$7{}ǫ>4bk|i3[͓\4F@}*ʶr @ɕ>N# AL7`##QI Z5œ#-i ù^J P;A,s%=d*G1eT P[fE!ؙ3lһwd0bp:U9v|JL9bkMiK{`j]{t-",pz.΃ֵUȕ 2&5| E3/D m9u3y{b%ԾliSm'O s"z3ލ.kۢ`,cg?ڽ,l^`3hL=UI{[`*2˖4Qگm_()PůI&߯>h_tڥ4Ng4xںs|ɳo?{ߛ9+{~;ܸ~u;k`b޲zv^rt8Jl=cp7IOR*l{yHs wERKmax`{?S+^kJh-vi74ppw|Jay駮?17Z2$$Hcd:"F҅ *z>ڲ]ANZn6l{mE$EZ*T(p:i-DXpyuX&b QU}5 AL"K6Nm rd5\o! DN۱6>:QGUb˿t6a@ :_0VW;joQQpDԓ)e{P#B7HۈfCHϑF[ fs>oqE}f8no8U>,٤%f1:p0;b^%?CH0L" a8_}~#t.u5/Tm v!K1>wNR"~məJ>>ӧ:E_F*[2/s\:ucםXɥC뎞>{;]kɿ3-c-lm/rzUw?IZYd$#L HNzz^TSғ /t̞o "S;ZC (VI Yن:Qo=4Ԙ k>5^LmXT흙BVFRQ.%Qm$ /a+N>K>`EF-JRVk˞H̐rP<35;IS, i83I7ӯ n![YİG @ K҄=i r k8$~C#3+1TDPח ?XŷzKg 6/%OxZrg? b}mwSܤTaPjtKa$cZ᳾˽*"O5$LJ 8޽TBfl b0,ʷHk;y3cH =I'^杍oЄp(-rn!H {KѠ"ifǂo-Ѵ4;=*0hle"2k&OIL?%.>`2s`$*se3I0$+[ FH]ug:G 8:bv R " V8Zh%\ܪQ+kUY TjwaSiڜ'$cs J>up1V \F 8(uBd[HirS1k2NzATb'~<f42[hL}_fbj}VpVg(hpСvRsr`t-DcwVKOP>N5ͷ |CC8CeN(Ly4]a&=HbେB! :}$T>UB1nCUmVVH/5,Yݑw∓T) ۼ"}Nd#`uADD -HWΚw!Z.V@"ō].hmӘnj]27EݻD*ֽ8 GFE K5 l/% c@ Z%J!&bX蝙yF{$m~Ysƴ0 >H7:[A]٭bP_gE;(S;̨^txyoo;VG؇^rFZ#_}̊Fk9\U#_ 8J^U+ϸ1$M%EjwTBf@=gl2'wW۱9)\C@A|X" 7:)WHH[(BGMUy'e>/*W0ufAB bI$2_Bhagu*ԱߒX 1Y]cn`y]gGdT9gIz/#EToFD˫c3B9ȴI!ԩ^DCb'1"-I#- 2FR*0EQSK :cYZ j(@^Px8ɄZ|uTwm9X7v*ro^34Px>>">[8tK]͘iy`qs;<(=:YA8f Lp lRr#֦Tq$.F@!9jJm$ą|w5ES0?kej $3.9z۰!pI"ԣN7uCeіg9:n/ $Bw~b'z<50˥oY(?+@sp[_@nε^: ȩEճ4hqbjF$+Ap2r-3X#soy;ցhŠP : nti(:cuaV`8ٌ⊳s2ι\%Jv4\ȕvmИ^rgS^XWފhsD"P # ?8/pRPNu*$gO¥,pfg%L@Q?Ea\tB}ʴG$2f[]*3nfj9[`˟b%#*=P:ʝ*"mz\bK-f$4glgbk ;[es$R,Wa Pge9ȥ4"2jwOV_WfTP\ip47 _u\K'Y;?6pDAu "ApWyQ܅40#42K$"[s(\a1f՟ܠI|k:C7sp85{&KtpDߐss 5[`QS-͵sfX$M4sP @iPt-Uc L߹~NԆ$Z֣[޴s@r†%jSMƊnev_ ,\Hlik^LK8"5?P<V)K.UH]"yXCcx$ JY^aeѡå8UU){Ɛ!U!TN%T uh,#>}}}φ (,YHU)0ÃNjJw&.A|N}?}NAFޠ#.,HSK:֕9hLVn=[:߅3j.WQ4 ݴY@jͻ H@8OG4?hg| DU8{W%$~T 8vtСbIngk! ZqD::D)&ɔ.wcAZ/T[AP#ŠU W ˠe.%7Ո-ڀC%c3*-GDej 0DgeHmP3L&.Fy}YJc onN N\Pt^a'~ 6[J_Glf.:C!iD%.yK ##!Q˿w|yJYqwYrFtnwT +ڦψAǠqN џNLsN[H[ڦ!',cqb1'aDx%XNŭt(igV;1~_f&F-C|zY[aۡ(,\A p? !H|A][8_5 bS~ƭ8g=a=E.K<1{@^ ĚRijgzuشpK?+X>axX@e]ޣS47D?HA(eDtOe&t8EDm#uNuO*QiW]0 fF$.\p+ g.q'?)L 4Y ZT6<~sogY5UZcáv .~'&猓{8hx F0v@+@,AaifNFS1t,|BpjЫҵcyﺂMғ16WH͓EVxtG@O'M=Vw8T nn=$>|pU/Uzb#pcÖ!۳3Je?ay]`qO™ GuZHT"4 $o*& ⰅCSmlkw^`5)/a5s1} 3ulUTDej^EN35$lN.ȿkd6']^<MR#J3v6~YNͲ냷r; VOx&`{ b"Zb:h9y}ʎ!2DgNmgUpM];l)!P'u" @@Z4:"C݀ʪZK&v&{jڹ VWp37yC<**CPuS_= y hx-}lXF 6_Gvv=)&IHi]6$3ґ;kS,uE4U89z}N^`| 9qh mrAlSx/@KpQ7]TKP#cyA«> Aw,<Ұl$⵶!>H[:;wH#$ g Go[dD0\}sas@? +OO =CJ֑,l+^WHth677`wgS\#Z;__AD}YmU.sPd .ۗ`p|vhW U ]0cl@(0KV<^BA` D NJ lUExF$F!6E% 'HnvV]W#ہuGSock'`.R sg eh2:z,@xa )u oP\'m8FgڊIUNd[ν ! Q"C&]\ N.5ݏRPa% >Ջ = Qf|m诵_]$K&Q:뜾=.;%KxKKxƢXG*;&"AT*?mo\!OgGHVW8++̫fVE{.ĥpuMF4=F/0t-9T-tbxsဝFU,#FJUw7v\;cVMސ|.1v<~m9"r~`Tj \} +2@jăJ~Յzy[VG*$NYݿ C;)C^W!2^YOX`R^?d*O&LMš0ԱSD͌2ӖG@ W1j&D9=FLkI7kLϘr8 |R $N#-݃=IW].)PrPaɄtq=/t5)JE`VsZ4}7 uti@D:<<;.ej+"ܹ?yz]+ƞ ,"pNCP}W)&it6ܘ8]B,ZvurM|b\qXZoI(9 FN1>ɧCWn㼒δ՛^XPn9un $p8Tln h]jbf k!DRH"qo@l mS\ah'U[umbQ?+/)B7 T[kVAϕ2H2@_|(B&A&C 9!a?}B, }q:4&=S+ǼX19K Wݝș3. B\}Y4\nbq&`mvf gPI<GIm#LG% Zx=d|xV]0Ǝ0K1ñ'om*-3b0О|ĐB^J5(3u!Y1\ :]mW7NǠŔF?Nj[L]PgA]:E6o`d~wܯ*q$n%*ԡҖ i@ %8a""h1;\^WZPU{'dcHDŽ ݳO։L9Ux<:Qm$`*i-#@ǡA3ɯôKѷJ?4b? rIƱ)PiYd(Y9{RW^:0?! 8C;a1NHj}|近u]@z5ߏ)m Ű8݇_@@; }_[x`n'JK 6Ο.~f5(-.# 1z8@QlPiBL#1Ǘ)@lKJv %BAW/;a 0Y!ug~ϔ݀>ck҂ӵE E3ϡT[ QypN*J)zr*Y1غ`WT (Px"'FnHx|>#fEӇpL 0vƅzҩ/%FC >q#~K9~ AX-;2Ù[k7bBoleLzuJ"LW273iI,Y&\c u&}ʈfeRCULZ׹&C#D!tA7!֡ E1C,4+MjH\ݽ忟ϟ}ۭWCfo2U+z=0-mpD@\N8cڊƴSRu5.r*iNO>zxut_@,=Nų:3?椀n%Rqu#>%H";;;;5xESn9[ ec$DSF܃jAB T ?7\_+ʐH b, %CNB^$"27\A|܀a>5cL* ,5]7P#zoB ;xQ:R ;rH {ذ 0Lg:o }J_]*@( ,:͐Sfm x$pb:HfCN?Wҙ6'9I4]8#\x^P~߫ uSw-+_S}g^4^@8^G:͎줾3iw `O1>>:v_`''s50w=?\ۍ6 h3mg)R%Z_@}թ\BDä́.l B+*.j40EC~~? 5nf1[\KR3:fc@ wLA\sqSj:Lt`j;G>aG Q&xtKkrDg*VQd]{.0X,ǸN<'$6!6PQ.~6dIc6IDAT\`_ ѐh71; sJ-> AJtHT8ť=Sڇ(TrQC4. |&2V H-(?k [13t"qp:~­iViXɌ_`VD[4F=cfK~p.1kwTj(trB:ǧ۽&ϦTvg߶ xϤzugz<70'X)PHi>aX>{rpsh~l[0;@Kt%@i ©B E4xrAQI͉WoɩF;tE I`-=:C,!ک\"wZJ!b =f .@i 絻|{{ȶJ8(nsu:H+ z=Ru|\B@ˆa_?(RԘ TE@qm_uQeˊЅpW`ƽSaػ5Fq! w$8q\$TA^29rqAyX4inxuN E\ڎ$'-_vù G 8)h@6:>wr{\>s\>Ym />*}ڟe1ZBږYsq?;=C:F/txoYKҠG'! {* o[[wФc a"S[.̚st8=ϖDq-9QHg\Ớ.+.@o0( olp_eř>ڴ)Wf$*3L rCK!KԀ,-!L3$ʈdKX!8Z! x8nʕ21\ֳ\-v+~_2 Yp}~g:=HtYόO d? 8%X%!-3?+)qXV-m 1b.g'^_4 !2,ehtc@l\7\eHu @m@ +7X K9h5Ίn%}>b jؕOt.=p@s3;,:˰ siP&Liaco-!&˷c"S|,ߩ#FMHhiIY ;hտ&6'zH۩;f^p DP"5B9d9,匨M&O.y,̀ri˥D <z51\)p_'J>2X-[+ߓ˓7Nw@qDXev2l:>߿j3 E%vuAGh/$5ڶm}!=0cY`k$ 6`,xԠ" 6CXasaQ*D ! vѼ+E݅ V tT" l6;bnC.72B(А7P퀕C!XWr]x7m % i8?̜p~NK &`<~[_=KU]~/IRC+ƣ@_OE xv5ז4b]+7#S~Dm!uhbqNDc'Z pnCƦXm-8-ƽ-$Q#pk YFʇ(K#_DJ15`B\Z:.\4ޒޑXԣs=&h@V(a A"fɓo@Hvb^]GG $!4ѰJA?wAk=-k"B5Azt>ј҇ e*JmGm:2;ۘ&,C :}֞'ZpG. "xt9C\ܾ{'vtGBoE@S)ZYE-g]/ApsonG㸜,:ngxFy hh%<:(ҩԸN5Wtv<H6_`i+ oV&5F0uC]:l*,0|5]Aa} Ui:V~7>c et#$h`@MWX-g#:3pW.ͩߤ[?]wGWV7xRbZ7yT0m 5S z$Bhwԕ~' t9zgy.(+0iY9BQ~ %> 79:CM|8g CWmЛ ~r%yoJ$`5H1 R[ȋ %A wqBn> "i'z%;0}4fh@tX.C7/1<`{ }UVխgnefBqfҐkNFu } GiG^PlkD@* [X+AphᜃnI-:R~ JP+]yFF_Co2{ UW*R2pas8o#H.A "GOH0gG'9{1<:{n;tpHz tI(Ds9e]M_{ CgOpM[8|W"κ ib룑S^D+=篾활<ㅑ M) <짶9;J_<\5<ypbTEL&TL7Sot~ T,&kw3vCEBPX_7?`EL$xkKr@V1'wDBp)L ehFBTj?+*l't/I@`?% H, 7.m RY# nnkEf%hϝ. pF_Jz-:c +wue@G?ALA%37Rya6Sڐ=zFh 8`<:wq$i0 Ih(V͖+w \^ sia<=:Ƅ$QZGNMɁkg9)]f8\]&Pc[)>%|e尋{Su@P()ZLאT>ɠyCO5dyHB@K*Kek)dIւ s fx$" 8ela-:- {w25W#LEapb$]i׸JQ^'$ٻR ]uD@= hߥ_$Vj ͏y,%ϓYh=Q?ts# $h0gvr q:+Cp2̮{]ag;(~˭yVC#H=r1bG|/bxVoW96ܗ{;[q vUpr)d`FV)t88U層 GEokR5Xz̽W :2N~}kF$rCR/aRB!%nRoGԂvaWpD ?WA:^9CIvsܠ]j!M6qkֆQ!J3f2"|(Y+CsSuk''1kPj DQa !Mضˋ6㝽|.wUu!@q}gINl/ L ZC= `1 ?jgKU};}Z苨@FLYMP(; *rf@LC;ٓP:5h3@!KeŽ쎻X8ZO >zZҞ{ '5DBwZ=fkSJ@WUP qO%EvNyg0VZکsV4cx*u)!&U*0ӎ6W X$5ru+a;k m,wFm2b܍r}>-w%%σ__Rd^<<ȇ a%ڜVl[V^Gyz }p!w'cwܷʘm&A%6> -F1$Qt}沝o$1`У.sVGmR T3 ̟6N5}F>Z4 + (TK@\gKaJXҕv2$OcĊO7rJv'\ұZfhH'A_$Ԕ0T~/ ^'ܰ 6go9\l.B JKNB"@Nc_b ''E\qx%kL9X,ݻwwܟN[ǣ0c8 +:Fk %([Y zW吣pFzp^ + $ (-CdH_gi$M<\EswVp{r^As0S ^O5&p-\d,#Mh9FthLdSa:MnÌ EF(W͒P軑*H4|O{$pG exgvaB/<֬XBggWx|97ٸ޼d}1u;9?D"Jn2Qj†ڜ>p 7HG0.4 6ݸS$ 2Ρ.="LG?y {;RGjP: ~ڃBĢ兼g{ߴ7A?_m`^ܐgk^ 8nԑD6crK4I xX}I}#;x?D Q 4|žCmt4m9ϋtu.پj%#/Rϊ9FobiO9 hg[MX٦XBgY[88/NY@ O'{h 0b h +]@;'p8xq8;2< Fh@::> =yu:0X4w[i''lH8P;Q둿6UhÐ|[F{3_ggWb<>#)Ӕ987]%w$XK VJ͵Ĥg~$~gD<%7yQCT׶ T!J#7i8簅=OO񾥕=wԘo4RG9% yo!v9,zOa޻p썶'C,z4a` Jߤ3("\bNa*7X9׊Y wzV.-L>#@'-MӃy A `=3qH 8A:8 \G7X%ha 5n[\ `'.?o\ Ō@_ XMG%Uh+vʔĐ@/z#r4"{V<wz%6Wj=σz9|O~_ %:!W.fKv] ,LOPg N''k}c(&.ȇ, .!GK~ DvwPppKli/Z"Sx&0v♖4ę{)bR"֩/HHyS>r2 @cAl#'Բ=|4KKHD">o;Ի350>, gq^g A ̴ǻ=^z lFJOc7 xeI ny&{c1Mm}6N8_:p&-><9]\"VtE}߳_/)Kg14aux]t:0~|j/ƛ8YK=Ts18d M1CbQХʹ1z7D"h@))wnsC(B8f,uu`wȋPS0* g}8Sñ"+)H@m8e| d`x.RRP⢧c `Vѳ#|$&2%gIFki@y)/ҋ ̓䍻7;1RBp?Ű;Mgl)ǿ,10{6wCn5SS=BB蜽̆?ۿoc3(ϦkC_?(JO%yxPk%h^37O@ fO1`@w C _90IC #dmr \ao)A[D9(2#R Q[ _who 4y7 x?$4L3;NTShs/]S*{/kC{D@|D1!zߧʬ\zlV޿KOu03au{]g^5O`Lo( У`XA+. D:Ȇ4F.=ېE缞ω#_d*:3@ E$4Ps`uCmͼ rF bLr)ϟSl{ e &BB Us'ާ29Ϙ"?_rgQ,۴hcզ%F 4L+BBir+)/-z_~KH'{]"[DR'WU|wMߦ 6&:vO =k<3'>gd?E',"quġ,*>oӄ 01ir ( 6<0`w?z,= R#̓N B qMZыqۗ8P8`Y__P! IQX4C+a8׳DuU}%*2WRD*H|_#PԘ-F_]^Z}J>?P۳_»x xOo=JúD:Գb(= *} 5p>^.,N': hDe/VD`5S568{$a EJSMWuۄX)yW5j9`Xcm/@U@5l2)+%sfQ|(e."/+4@`Rn30LQ0}+јqz u #P{TŶ:.k_D/)}~#)wG؂xsqdP/i`%q}7 1H XBq|F~-Б(I\A $#10Ƌ5Iz=郔L\9m#Ӄ՝31{\v#%( (M 2nЀ)ʚmU%q+ N Y x4@goq@*S+?n/.>߾oNRdi wtAu}')S}uIluO͵oHGd7޸?X`kP3Z)znwhɖDġqP&N5>@"f #QH޴P g2}p5ǻQ!1,^DVe_pD=Yʹeͯ :\BR߉RP@ps_ > \J Y[[cj#O34X9ܴS>K6!r(0-A3u-LXws(gY$,:M^RI(]h1hըlu{nq"F.Dfv\63иt\!\< 6lP @ 1~_BҬp`ח//1]k0qq"ݽ|IsCfiOޟC VGx_Y0WggI1$b+A.=;8Dk4!>@@繽[3K1H欳 % $L)[{ T 8cJi `^ s7{}+_i>z0_oJ+|KCV]K! {{0< loP!9`^ ! $m޳/5! Ф+D4š B_D^m]E r A<27)>\_b!]^| ?ZY~g_(Qn]aE +=_[2>r|%u/û]wM\|>bG? >gz]`v KKLNn/6"Tk<4x ztM>j|hL, 4IB-лLڸb=| q=b}cmyϷVWQ! AX\۽v,1 }|@5'_P K/$PG"  6Nlgs@@RpJ D $<垸f̙P$lt RP)]74=Pg%o+&hyS2= uu{6=EL=F@X`vpi9uU p]k%#c\!PoeLvUaHA |Fe9fK$4Vُ醨IkĴwf4GJ =m2a>_`\uKqYzo |K.Mן0S֫M/"P-W*z{}}_?8x UJ˵ R^h0j{}b:,슸xc^RI0gӖ`8}}ye6cziՀs8}Y:7EF7"&^=)qCfU !]ֵႷ@DPqx~\"6w^yE|LJoem'9_['* TBWCgA8$PN 3`)Y:䛻X흝Q@l *Yz-(=( u6\@w+~ Z p|Ϗ*4'Nڳ=_81BS`A+YBm-nJ.\7CRnxQ:aK\1]ci|WJQZy=0<Eq<{z^u"n;t!]{_\D+Oϳ,{.@tüô1P[=i "uR@]nxJl^*~/S'u朗AWl=0 ú^] u_9Ѯƻ|nx8^ ]@:X ^ yWXy>QaJxqCW3s3"`hvςHzm+K u^1n{MpcB=w 3^=ܽ6lu_ ]v[מ+n7pկ>3_^p}*75q!M!(p><V|֯~޳G\D?ATu=Wu_V>ymzu7<א >7{kv] b u]gߩ_z^g}mu^I^{UGqV^ul}^7zw*y'"{=V?祂 ú7Er8c^Pu/p |םS s߭p}{Mn 𩰿qC5xsc4[yz_qY+{0u ѽyлu]`<8EH<FOU testpic.png doomsday-stable-1.15.7/doomsday/tests/test_glsandbox/net.dengine.test.glsandbox.pack/0000775000175000017500000000000012641367671030247 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_glsandbox/net.dengine.test.glsandbox.pack/testpic.png0000664000175000017500000024322512641367671032440 0ustar jaakkojaakkoPNG  IHDR:= AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  ~iTXtXML:com.adobe.xmp 1 5 25410/353 1 25410/353 256 1 232 2013-04-28T19:04:52 Pixelmator 2.1.4 @IDATxyeiywfgff:h$`HH`)NJIʮWJ\IT*+)$#l+N%$ b1;o_yMYe>wyﳾNnߚ͞z0y=0[xM40[p =p n7}sn-<{`s-sy`N{ mz;4;}.O:S dW;2|,on5ɯd+5V}:m2{sspgns]3.wxJXlJ ߕGQy8VB)K4I.WM&i>Kc˟I>Q2)/|n0֥^g}}/|!@rp0릛u7٘\l3f5}{߱凇ܽ(9>8'd0 6+#x<8uK&I~i'  FAkKNPl~1_ܩW˕R);;$Om>%i2~p91RϘ<9O|5ZH{8<^IXH\) |(8$O1 (pN~2MyjnRaҟdצ4!#PfKaR9)~7Vzh ?d<pxO& NA@9菆hQR(2,;Q d0N^o4y<_cǞ=h|s<?g(VMavc?Xw:SGCۘ۽؟NW`쇸hxzL)9E'r%suyT0( M` SB caf47 pLBO0H @2;@c,{A9'Q_n0=/_/[+/{/׾i <%A+aZ՘ޯ9 Ї,5; )" E::>oW@O \Cm\?#aٯEeTWϏ?_vU=,>ODMk!-7Lt&w@V o`T)h{ C-K3i.C22]PH<^ȼ^HsL7@-窍 a5 ]?>9c5,̈́ zo881K~ؑϞ:v[ <>C͏v]2,{߾pMk!M=Tӽ5fv wm9J2Oh HRd!4?N(KU _fǁ>AꫦH[ MC>̪0XL`d'as+CwJj@ { B `x.Ƒd P @btHh2DZbΓOs?jI1\]u]Y&B׮WSh"yV29~'vlf;=/j)πstƟǸ*_뮻?GcTxCZCy %x1M +R&^PU  `)@vJa&jvHYtN'^} 6 u4*}x\ud;;qj!>J|ZXI>ޠaŐ1ؐD +m zAk?u|gj9:=]?`+e0-~7G1( WðūO?<j%Q߹_鷥w}?/W )1Liey ĿޜiGHQ8PFz0T#n,^#__Y#_sE0kHd*!cFRH}ƼN@"Ac̅ ?)@gwF 2e`ܜ  bfe#{D[lu'A=tSi1Gg}Z줠n]9|羹æIk I+rFg 1OзH2z k0=̮_|9)o*:RU0@4q7L1Bb&;,ĄPdFUo5_e"8^# r%ExPB }ʔpv0ix2s yC<%Jx}jN?  Y7INuR(e'`]^n~Zyg_-k/e{ϹF@'\y9|WL?~m\.r^2v/1pDxTUt%w Ш$bkJ\ LxYt Wp2ke 0=Dnz}&i0 maP et0 )O2ˋ,ǨU-g<0"ﭫjW̏*~? oT?ρ@Wc?|o0WXT~DBA5%q^*o$@^Kjv>`$2khhfjp"`jr9T4DkL鐢8'ca-Lbra=,!8~ws!%x2/'VusJykF3փd@ht!5O34lgb‡a7c3Es AE-\&=^]Y\^!FQE(U VEhP+TdKBXj&M&TPS|S4c%t<@L}wb7Ѓ_@%rmj#F5i4jIϞT&a}pN>}Ց D=i"1P "JԽ^'KKd3/\ѻ+ڿ7GoA*@N>b-k\Mg?soɗ w6Z T04:)aZ5Yh6fҨT"EVn$:  }^S?`fӗqti SICImQ2L2D_dCʀXU!Z噑7<`>?6)XH2ZY#HsIBԁd`ZkaQxPh/~}tRl,%6}V'q|Ԯ"\5/1:\4jP„ rЫ}'?)ZKڀ9nI UeiT?L0?J>ć FKUubx a6_ 5^M@i]_*LeH'2pBKᵘ בx HKB>_$ dk3\qhe!N3yNLdr/j2<~|2Lm b` Xp :j79$h)½#j[]lme2񽦃 0jD! N L)XYSq Q$˷:wpzӯ}QJǭ-s\&_bfYz+ӴѥZc!qґf`fRoaj \H:da0/~=B:PCC,ێo^=\SWEgV{?nh]z?xZ}㋋x7DY"%Zs7PQ*v}~ ۿQDJq8')@:A8 (כRS-<$idB$W3DU_O~&YPjR~0*:hyQW8,=Hgm/6fG\+0M7Ϥ$vvڑIL15vE{84HCz9UىIDa2dYuaeL1XUzwgp p@`Ӄ\Oז3x<*!=dTC_J!&6>יSv@/wR.4smL0c00v0P=7T]c~)%R8zχ0;MƎz5 Bpxnu#!{H=1mW>Y!l\#i)`ԥ *7%g'$䴿ʊ;ajg0ἃ]6<|ePx[%l xv#B-~ێI_WUWծEx g'.r]5"-b㛲[-K8RW_=GWT  kg/bb'YfxM"'`Q%*oPaMש*ǖm%x&&S1Ae\(zc^3iT^Z $[OLꣅ(\.ldg*C]4Fd^2Bp-rj?_Fq C 'm}3gǎʍb2aNG>cˋ*|$հ+ 2 UJ񀆙o0;$9ԏ!exx_t 1W|)?cH#GIdPsru:OCם$]g .|y'+Ո+~{@hĦN|9#@-OFcLYXf!gסbY 4hA0FS_> ` nj_?zeCcB+/˘_S6w]n,Jyo1_/&k++xЫ&G廥daTx?q&yB CƻzY~.ێtuV*;} XDO .&Uo#O? m =a&EaB0?ĜBRS:Ῐ$8ͅ%_`0vDT,@vZ2$|4#L0%;꫙cfR䞨Md.UOq|z_׿MHtW\+`~u TpH$ !<9uË +v~ċOοR!WT@YםYt8`ȃA0[W$7σj2< ?*vp8^b|.b =,CWKP A DԀ<͒<& )q`w%!j\+ ^Qp.c=vzru ̭3r/$G֎$G9 S|/B-R{|`@"9;B +Qf^D!LSTZ3G0i*<(A+.F}4)y#%M CVP`d.kVc#$;<IG0&R^սz> Ð|e'Z1+؞2̋c8Eg26jX: ߂ =Zfs5JNmKg?s۝/Gă?H!~!KV8WYZK+/Ȇ1ƴ3򪵕6Ԍ"5$B :ؽBX*V:󺤖!8uTtdWDR!UùF#bF`MtSmDaj3QkÔ^" QE6bdBόUCASѶqTw/r09ctb$HϪ\ jk8EDLbaQE?GH|%3tX)jc[.?|ts_ZPEa2š@td,7XuO (.=Hq[ ^P7MB鬉S<2,a6pEDL@a9D:yų_zO>~֟Ye /{{0?HuS"&kHBNu4}k;j!BvsUa<;}{<0֐ &i!>ޗZ|@̻wX F˄zc47(=a4Nࡃa7:\7iLO|o@p:Zh`﷨9EFGS;F!O| Q$}A!rrZBp*5 `TNjO} P^^jo|Po: WW9Kw_S5\c//,ݧKXR#uq-'kkLjd*yD.1c~y(`I5P&?h%s 0D%`.*wt\c}.3~ ?V&k)߹ǟ@1 } d,F#Ǣ"eMI0h*`җ`a{8\<JL\}H#F]ʪ3*g1I`%puIU0~q3AUX\} ~v$H | @IA~oa.pgFtAe Wc7?W-?OV?]g G0o8kxHel2c`@F1$PA+bMf֣l6;$-˺N6f0ڀf-NTj os/3Qat cz܅V jT!1LU6Ag2`&B3 ;\:P3"80]]Bǯ1Fq/ݒL<#tf*12:u0)'3Ճ`yN8`΁}w4奤^>tm}g)W-,jߨ-gZ̞t:_5He %UH"SቐA6/G% H==Ї3wua$gw8Ϻx΍UЗ%ePVsAúրCkȬj ysy,2gt:s5exC.=a+KD[#/}>xgIĞ  xYeoWOU;FMn~[W3)T`~e"9꿶BN3Fvch !ǜ\hi}#?@ p}cKOz|2z02@#s3AMuJreQARC-+^Jj5Mb7!QuߠTJ\[0bnʯJn|> 5;-3$JRg",0`E21 c1׎C%/V;,qNX\U@ԯ}llu|6R_jPXm[[P/&OeW0.U*smWA!޴O'&wDh"#!_0)N""Оv6R_փMW/#EnrhC`2F_trrZp.K" .p :@s Ԫ|.oxb^5?4.!Rltlĝ# ÄET6J̜e"q~,6ia{LXP.4ع c"Hi!,AFԂff֊ڃm"3_+@@xYPQe=wBi"R@vpO؂N6;os~(a\I<wU>v^׌;zc6WjӫCm+38z/yB㪻9#%D]aZUR?S+9G`8hW<Ǿy) g=+UVOYy2$uѱe!8Z) 0TLFG*L8 DI6O"R1hoRDN?GJ'XΌB`?lsKOn]3f<^1KN}燦Gʕ2r-eȩizjR-V"!.AMVȘ-re8"+iQG!ȰaN30E 75T~$l>A@zDL=n{ 6(̘_?wkʵ)(b]Ћḱ:K|5YƋ,Cp!sy|? u3 P~OR상xΐ6mSI>NA%q6ཾHݕɵUb[cd {hh7=a;d: 1 M>V>;99[/oh/xc0m#);Igah L_gL9H_Y9ƶ:H="KJMt Dv,K ~a~|a9j<ķ2 2~L }QWQT5ɬ\z/|' gݨB.>|)mwp*;]YYUז?STP-PHoiT)ĖԙS !g!_ex/uy.f2rGTe28D3q}3*‹4ـ0j?Z+ hjkGUa $(Q?n;G67¨֑Tr 5VSFG*P'& mmBʇó5/)\[YM^<{|q.?3tsS ߴp̯gZG#a#8pL2זcNQ} PX?[qVU~TC nz!t?B^ߦiVޘ L='(6W…|Cp=7 jr6slAa2WDz:Pme8&CtL@[7J"m4MJC } Y)ucmBYP3@:zF"ɋ=6@v3a݁<ۖ8=)WNHW ɭˀ6wYCѱz ?]جϽ=Ţ(|Xgjg|{`.Lj\PsUhL74,ʟQs;$;JڽiÃ7-ivhg֑mG`ڨ~\J2 46dzō!/J)`ZzOp.҂yܸ©nv?LVZus:`T?PU#W_;e?5#9W%w7Щh嶦Çq SߣϬ/o+2_0eV!` d0[껺M)e_M\?Dw2 ̠W+b" Ry3ac0C [b'm՟+6Y p֥W6†9gA3PJD,vΝdAv~vf8nJ8KIWK+IGPlP׉wju]{8ye?!|?e 'ZgJu6<0k]Ri{JNv#ծ)92@{Y?&"kp_0&D Jڴ%Opшr#=' WtI1XȽ!Y!x]E0>[j2P晢2KQgZh?lYv#M̜8qdXid]4X{;դ 02T[ixu>pй:UI_K 1iӆQ.J wa:@EP}G/!\P#*$ <# Y 7,?j-z5Hׇh Bi&K/nBp鼋r &`HW eHҪG~Sr\ @<{NfhCs lD텲a9ۓ!aD'%;!V}TCMLLI&t4`rC٧}d5gsba\IKSLT%C8!H>x '9C9an`4Y:EfWNJH=}6<e )Ċđ01}=:B[H$**Ljpɘل*Zb9P@ {xBsgY6_ &~s :0ZmpA.`z KJqac=rZ6lh ڏL,|.{gFFhLwIBwAX+*IDqW5Al]+os :{<T&%ħ@ !HR65JF\īR^ '[ % s3)n֠ZїҲD^zX6D[YPU>X*p;ßmSEg&%|"(Z̀dAEvhV#0â)*reHh| 4Rh'l ILm<g'}l$j}Fp44Cz 9^ct\~{sO= 7索+o!P[Lpi}\.8~*ɰXњVu0jNj fgmoo:UajYLO=Lp91(G`Ӱj=H=\iSl_;0̯Ɵ{67 @1gVמ8rBf*ZDeP}!e%UQd@RqT, űq!Bc#ۇ,' 5!kMpG'Qd jb5m 1J2e*"6`y0'3w z(1Pb=pVeP^~]W/m>zt g*IH |pE"ZIoMoOOR+ bVcHɂ&x(tܧzĉ]pOJ6 +usZ_29XwMP؁Tw``[A|ΛPs==h-ڷj*>u^B [%S8[+?n2d,թZ|ӏF»Rc@+|"Y,lO"ZlNQ.G֛3Zk5-q`b9RMaP~x י;?L# . PCՐwV?2^k !X7ބBPW7Xv8`\,]XPy&D #@c U,/ m2j;F?eS_JY)f(ap]f*\%֗ :n'`Ui'W:&#f!;Q@h .Ve5 D=;HyCP'hC]Jw?kF<&`'OE776 @הPq|˧!mNҊg O?BcμH2Z?Oo7.fWi?k \oa@%':;{T (qt8G$׆Ub 7_\'K?/ȐDdz|7ehh)8ٌED$qPc :a}p>5| | "J@Qw"|z;,R)}3PPg{1$<;E),3lTq]7m=SLo`2b2.3 l /꣘ ,>kt [cꃁ[aY5p#:K3;NOVu w䌍<Xo@.pe^d(¤RZR v(Sg<Ӑ>2s6Dh8)")BD)i Yu L3c^SR0T秦xyw k.W2;טА\]S98%Z/'NމWL}9RL' mT]:'n'Y-5dS-P!궆rmMAVFDgf}YK+GU9L^0 ڡ TT=yuڼ0c(xIcTF%҇;WU--:WG1O{ ==V}$Ze@YwKL+.tHBfHDj(42JI`jHh8>Qz!P'jRi}J-fڎD&M@rR פjW:ҕzmo6>?$Qhgw>SFM՛J0NZoh}4}Lp1뫶 P3A12doﵠTa+E#>\PgӲ}`"idOftf|PkUHzt rwAtQìȃ>?~Kfٗ q ?3҃AQmǘkUMip7M4Tcl$₫ՓEj$w\, '~:h$LX 2cKSps ϘT6mFB֗Nu.xj,֤"x؛JLbA#ɑw V[nݟ:.!>#4y'f#.z90Kz7+¦;~/2R2(#FkR&p u Vdޮt?`& ۂ$YڏZ1#L&2d/a \=2+]S"aAj(˜wcќ 6::Klϐ2Cg|-G }hi..DLRR1Sq^Pu= tS}p:x_˭9e|^>Tѹ4P;>?euACnm B(&@tv2f@3/ڃa|tO$vsWW<Ϫ/HK%iC_i%8cJ(ڀgcHSE+sٵm =QۉtH~MIEV j0g$&Q8|F{UĤ|y$pDH:\ZouS%Gj8~|4Gj;GWn pCcVښ <5&s`ʉ_}&0!<=]'1N>\<gfFj+{r0t5BA{oB#8ʜt}% }. 9q)-ZvK(A`<9]Ͼe1eNM47 vKYɫh{Tx5Vx~{u O^ |ჯqD;_'Ojo[ZAө-f-D0Nګn1BGά} M3%X6rN-4+ںSAT]Y@P7Uif p)fɄ^_fW %sݼBS@N؋O^&v:*]᷏kz8 [W 7c9΂- 2Y6Z#Ft/-Vd!q#}<'2& pW {d:*}H4~V&~DŽ`Dpm$:d"8`XW\7@tc!ft#T{*ye d7:Hm'Mّ%tpT^Bċ~HcĬ@s,ϊ5(3W. 2WeKo_*8;8y8]r 4n>wvַ;E${( pė 7EV `?$h0j&0xO([ ́aMxR '2'΀|u*X_" 8æPBU-+pvUQc{Edir:zP0Qhj+1( ͆4WU2qpo*, =G5d>[<0*`k'P:$T0 󴏕+.sk ꜳ_ʌ'(u :AI=>9s+`B;0c0.|7YVo'R7;xF >! e4PnO]<&Cojhq!wܱfmvHƺx¹wsW8--dhՄN=t w`o/Xbp˨V1+R\m]b d >b TOM Ef!yiHKƌu+,d YRf<لNz`xF5 L;2Ԏ(H%~[jm@|&Nf)9|d}EU?%t/)QFx<3( TCz+!}4v8u;X2T̤ aue3YK0R-w %0MsvPlymy]`b[ߪ8n -Î!f"GJz$k"\}AH߿T1T (%UAjz "l,GGu]?fùA'SJWU1a*:wT5K*@RJC} I \(!,8|&\:IP m9)x|ڤd(l AV~Q * ~XAeguvת٤-1ʜj}Ör6Ԍl;xG-!N@$0'}eh7$:ޚ( 4eڂ X<$[@ А]O_I^-ԏQ7qDM vJqtyU:wtG).)<0 f*]`kGz*{6'4)Q /&g#33iWI#ݏX8Dn*UXYecrck"ct`,x@!QPwJ<'2uµC/gLw(ESΎӃ.(  եh:φ~;[Ozi={{W@#`6`2ʑ-H$ɮ}ܡ+n:a"gTudՀr|^lZ2ʣìO`%SL'S{ up<1tC ʎ>CEyk_N[y"0oY)j%- k.^"f9f/'λ=׵!k.`9goZh./9Mehf$ 1؄\<@<{2[$X1 A Pbhs#h-Xkg?>,DcmJة9ogLV`pJJ<Ui{d R WC ^"sh׀iN @Ѓt yozR3b}PL.6>="!P*7@#o5Mz-o[6GaI T 7 Snm3׼)z,j Oߢ.DZ 8җG"22ϠEO_M&)i[;>?FʥByр0v]\m@ѽW@ch723H @B?9w~=ݦMmvVfIbE(34=j >pܻV7yp-3|mr=gcXa)²]ieH hc?ه8K8^ )U"@m`,A5Hpͨ3Up| o|u b8 k-4bxKht20ГpQ `Q "cT zUiCTED”VD4LrY;6ZM.gfdADQ@3D5EhQ6Rq@7 S16g7tmkt>kZˠW y&fs* ]݈2/ eg&d{$q=‹D3OGRM}!k$E@ 1*4yd\pc!@gi@[s  è]Jڗ,3Z6J:cǘ쐳 a})ڈT'sqER_L_8lo+f!Rav3L`]{h5M/`CDWx?@ V ҁ`S$'2uE2sl5 v~j,5ie\=/ǪfCD]{ ]cKrL^IOhfjɈF21鈺NO:GNE)y}wj3z}\tm,R ڋJ~5/;HFh4A̒;#0Au5Oz$uAaߺdSI5)x flgh[}ĩ;&{ 6Cm.Jd<{Q_Y$,,Cv@Z!F; [`MTTͺ&lׄy6vv#paT%BX4kE$ ̘:#@N&7-nd 30~HTj_LkhVs$5娊ǎgK#[hדdFy")&$b@y9xMͳ'!'p[ɢ6<[(a ʔ{+Mx#V^Ե} KKD p{b xgCi5T+6Jȩ|5*y)DrĝyM& CCx]Vv@`}WФ@jnBpIq}tcfDtH{@晴cm"0:@l'1Z/r ГxYm Jz\S#hr4$ RC]\mKX,-GG9 ZI}0x_SCKJ2T7=U$ !x@Aئ?u?%61[c U $!ˬ2dD"Mۢ:rcF&5e0w2g7Y*xΐddDtEڸ] T'D WYKA` R/J$Y[I"$acc:l%|3y'$% o~Aߡo6w:n~P~˚f=34DERH_;ҩʍLzhaDN?P( kJwǣQ8Ȯiϓh<0觥CiD`l"QٙF2?JF{:l]ڈ|mqr˄)JTdZھ9GBV*ӂae~6󱨉+G-HW~⟑4tL]Da1?A871|h;L `6gI}wfkN%otLVs@A\HBKhh'*jj:HƒR,ZN):))gμaM6"~7zTREwY۔Cfi?μMl-T.&<F!P^D#:bIoUZMPPBZ1*B"WؑhSiϚfcH3׺[HXFmO'~ &޺t&YG3Qu9{ aoTL:h-JwV5p֟.[~۬!}yՙb j3py8kCB2 ~Vt9L. b R&\Z2g.G5/1(&ԳES:<Ϡ- 'EvzU Ɉ$SrH}j_$L5m lS =@S@ CD" #0mMCFu?{?7\O[MN~<.Egij- 4fp6^՗}F=Sӈ́4VugU5$ $:m(Ct}m yg y=IM/kGV4 :Zfl0tSM#RfygX-̦ÚᗒDﲏNןx:H*iMa l\?Ĵݩ:/uFE7ehHRq!R*L˦խ{yӜ|)[ .65+Ƥ/]|}ˬ= Im"ΙƂ2sz#1Х jEV(E9$tة]Xj}Z|0{4pȱHYGhҟvmu-fIVp#$(e$bSǨ0C_Y[ \*۾\c8B!j宪 +C锁ϏIHMM?rt#LHSwwcbp{+ޏ2> o@YPU8]?!/h[e;! F!~ՉM RFFz.m3l(.5j$`ay) hb\Ӂz~)Їi!7!iP[,us "c.J1vS3*x#qL 92ܤ)| =>,Èf8f͟@.W򤧼HhzMtY#w5kg>bzha>93݇(j\MI\#E@5؉:5?DU\Zo۴Z{ֆ-qR!ya'| 'X*(ˀ 1U8,x_!Nj'!ګ%R%*%bؿ:O VhI GH 6-*R#Xpd Jǔb:o@ҏ9X8sR8֎@090Ҏ@,;:$"@)Añ]C*efQHπhoM$ҩкc0BP>{hnbR!Z+sk#:+F:ν]Ƌ yjT4Z-$O?t7Ϡ=b ?i*uJeWN7ʥ{7.^ga4k4q?{-WoWF euRq'`fQiSOr.WYer$A`/ ~yASļ9sOakٺuj56|I< ^& bf'Q'0/hFA"7d$YEG{EmDx}(bsyHfWn8eS{OS vV?^Ր ﷩IUY#.Ĉ>MvsNZ3SȻWD;[ʆtveoB$H2}nNT c Ҍ#n7Ccj1y;_Ë'VOeKUԢF:Hp}fn 2Ous8 {a~a'F \2f:/4Ӹ̌񠥣ڹUe'M)@o!. T{僯V߾W?ۋ00U[E+q0ێ7=tfُj۟SMe*i#欘ij4XM,Fp8Uq04+KH@~_o Է`~Zۻotl{U7{٬LR85*CS`m5ڀ]j@) i61lؽ{;j䖻0O RשpGQ[n߾ o/zlٛtMX-2|PPOk`0vP:8ǏhJ6} kZ})7֍WOGbqhDOä{Pїmyyi?n$svD-u铵WL4ML Q>yqfETV!0ԉw0ڰҔac;/X0L7^Yj Y) IynDc/FQCTM=nIǿNٷ c$luer"ΛBȻIZ&_ %6MBRۑ6Q!~;iig1t㲝yA\@ALvyel̜`:D{fw縔`w{I8z^c9M>(\]x jw6|#E!Ï?(87  L F}^j0iy- \wގXkSob xSi7s:AJaN&/z[>~MkGG~\:ú7yߛPf2P)O& ms!u:ȇD A:W#@v@CP<5u8[5'URN2 cHlsyxRFb9 l\/#31$P IZdb>vc80y.O% g^4ONa)\ލL^l\~DU93M }$ɼQf=Coof_luDVZЌ8.WT{fxw}^w0C~/dA<+/?-J˰O1r7ݛ9[y4oϙGv&Eb(_0,zr>Vj ,4Db]S tzY%N7ߴaxT_ ВewZ ]_#$, hĭs}=>:i9*&LM7$P*Pa7ilq'ضu]OM"\|R;h 4-ZAvGCev pd5sDn)EmGDqRTqS]$DRP5ްOv26<+YLlRID#(f l*iR)Iwm~wֻVƵ·]WCă@_fo7~=/N3b1;ͻ̣A88"ʚ` X͉z3Ʉ̓bZ= RW +j^Gtc %U9D?>U$~53Abm8l͆$lWPZ' P<ע΅Elh5y*"#8͑Ƨ3+F8xe#}@bH]|5鿡U>_voſ 2yi7tGcu`:GGPf A& MV!U@r,cJQcsaBsO/k 갎! wjý«sv艸NV>N^VxT\Ī:){IOv#9Ӏ.9kj>\(dzZ rDHGIQa urA>N0懸Zf$)2'?Z}P;=@ۏ;zY΍j( kɷo#%Rɂ,Ѭ{sN_NpiiE nFjRf(vt0ޤobp}wEs/rkx9L?BfFᴛRzb~~􆚻C=wazo0yaUy<џi/ԋp\*Z5^{!6X"]Āw5Ba6n _LعK:)H&3f l%/KUǙ\>+G{,M6+^܅FksifI8ZGj|bqL @`45[DpN 7c ImY1:2S{wgia+o>&AXݘy3y5 F`j_s7}33u6H{q݃|8=1ur3MJ?DIL cN Y{z閟O_޾w'.!8/׎~ZH&)^k4߯=[7R7ma ȼe jGz"^u;њ >u.#fnRջEd'EU5xrQ$"F!$uWT\.kqߏ`4}_6G]٢;s%C,FEqZqؤuLT48[-x~9Źe}vvrA1G@QwO[ (pxz|V:^# |q/2.LNA!τVm Lk[tmQZp‘lU{ه1i/z0ٱ6f ~"q<6a$Msx~_>GJw +-H$2 iZ1Gq`V$X~<}4n,L7%5y6J«z0 rJ} ŒDm,u͢J0pVvpq3-][m׎69OO.&\ۏ=V\0,iQ\x$ \ !&5#:?2N]&W80N 7ئԸ1h,17޺[ rܙwFz4kP *inG⃧֨7!3'01 ,o;Şkf4oΫ_I$@=o/flH+yL.W$CZ[ÎP"ݒrp~Yr["i- .{ߛق空ok3C$j\6F /Xtf35 `քœva}E_̒1o^25A 9bq,¸Ker-VHm^?OږӠ)9$YnΤ2)3GnDn@E6Rͩ[E "wk&Q|$dmo?iJ]>Ԁ$qLXCxL+*ZWq jg- 6͜y-m~sIam?ң*kMCl%=IKAJ0TΩ+4W3G L);xSqc)n +t/|}v _K.-|MhAlo<΍O$M'dG<7[ r3uD|=>Q 1ՁKZ=8S7OmٳTgje{T`DhA)4fi050=HJK"$9<:@+ᇻ_7so"^YM%Ί?q!~6 UlPI?2 Fa,/?䭣bꮌ@YN "3(m']=[=H=iP7Z>"Ug$8_VXsq*^1(DձhcisjZw74LCPϲ-t`lpSQ1v76ѡ-?-"٧/|cJ˚;b-Q6`n!oݽ4h<-XDڂ#ra<~<3[qrTmOgB9?(/w>+|tm(ܧ'滯Wο֡K9кu7KPΈwHB2ΓroS9P  qdyƊ. aKSE5nDBmG:ɾ4&3Q-x0TB ΤDN$Fh,e ĨM0Iˍ\Sd-د z=g]X]rz@IDATjG]M4Hᛓdxt `:|itB*T'j>dOtAywԟ^9KCO`S9PtkpcОbC0IwVL<"E} f_ci?ym =7p9lC:[a(m?L659`4;kq2ñqpƆö_}Am4FFwYU" L\)J{u>v}143-U癙 6 s ZJib 4u&E#\_4Z4z}Lw! bӿډ{ А?!cFfG*H0i$Y2-Gel2M춘㇫?4V ;'cnCH oԮWI v%DF%|HUi˝a ~}wW~0olvȑ!C=g>?>SdTeto #"|9z?~ G#A 6 ? ٬;kL$$&qZN.cZ=!)(FP#VT -_d cZ7K|)EN^0 peK”M<` <*$1f1wzgk)MՂo X;T^bl‹' YR:Kkg=vz7 6d>7Zjx~я~+x^^q `(X[jvsj2.?h#)D* 0Y/8m+v$hi7WKgY(CWZDB?,QN1pBF[[1g [-zTN[MxRFkҕD`D)93]?3w T0.Qy6z٥GΦavƦ[ߋG;Ģ &_owAЇh!ҩ'9`o#RAqD D ˴YPy2l6`[fn~5t9prxm& C!s<]e},0xSDcSaT+17Z_D)*=8Bgyag:pAxVa.͢H~l? HI9[`FKTh6+afLó٘1䲹zb,->bu#՛rsc|,mp 'v ?@ FhQ 9uLcő9F*wE?diI{fzܩNI4y0m28׿Y\,JG;I'8$ie<cSBZ]@!GaBT[ǯ\QߏJ7'{[-a*H[4dj tC?Ѝ@QESc6y`6#\G,Feje|H_7[aC4&|A{/3f* =`>Q1_0[vehUcnyv݄L3ffMC\X{ LaW)mZ0΁w=3kW=C}]fCGoe_vFK? BHc7[u!Px2 <퉦R'4ɖijԓ&Qu9Âz8'4β3#Se\r2\%a,IV󜨦יhH5Y]iν2|Ampح2ߊVU05_וLͶvֱ_(RKa!i,FoVUdpp+mDž;1A9̈ j%2XdDvj]Eơ9pfDæu 1 ؈uѿa]oEX$һtSEOMIu 1|%Dw-R 4ղ]`H&`fTe̤~!ypCbZN2SHӐj01(0U+sh5c, Ɠ4km,"1mF*[I_LÂb#%PG[ ,е,4Al!1FFkwB1Ob F*ԙWd3),jf_Wg=6`.9F0M18˴v!::DZœ/f5W×rS_,Xk<+L;ܑ,1>ܣрrsq־ Ka57tLc֢vYݥ9Ҫq87y6zԀ0kTH,һ3&s!E s4'UG*V[X2jŕo@JN3G$/RWqjSPF+ ӆ} 5j1s4IJm(kҌ簔tcYTNYf*B'2G^ր'~?EEۭب?~g`@^ y3H\ْz[е303'iͼh}_JSY L3j2HqLBp>VD*H4 1ϲ]̡0b6Vh6JS=L6ڬPECnc3C3sSR7rg '"dw/;˯M:M=y=@4 Dj8^jH# 'MB [P%, 3jF-} ѳT`DdK&BMfM}uX<;ǘ Ϥ9cl|o4d5=+cLv Aj-xP?⩀!?   }ol_IyS/(F!=6e%'Zw0e|{dZ&&iM1ݹ?zm66xяSv_YXfa1u)z6'$r8>"w.0z %L`SU#GO ̯7q5מޡV!N'!Զj8Dj-PLqaXlvLRgd3k;frI:"Rh738Hj:esL P`mڜ_A93WHp=86nm`~LBYvŸt0ۃ/~EIqPVy2 Dm6&f\Iu☍:Q~k75%mFpe+!{@xYNϜd$[x1iC5(RBe1u|>-% :~AZAΣ!8jYHewDf:€T]D0 7oX[tvw88 -s"$}Zu[Dc@0x bfI"ZuwX;30}NZL)0dIO'BЍvHcA$x=8X]1e瀼Ѭ`fW ҕV;!"ew9"IF 9raE12ܕL5"*ۨ #'Lk6Ͽӊ?~3 e6M7 ƽn`0AlkR{鵉/@:.)Mfi]m/,GڦJf:) ]0x3@BQgʙyIn< Vr8[}iWĮϴwlbmt;q:S8%ĄTO~҆=rRؤ4Eʕ剘in yyRӢa(V9`sޢrjz}ے2\Ǭ-C.i$laҖ t.Sa767KTI?9.ʹgg:7&5{qJ՗Ӝ2alπy6BNһ1jZB#IUۮmU8CJȢ`,桍>mj}lS~^?v5>Zl_9ZaJ>L,e1% I2<յiזּ?E셅)^$?p|8v=p 4 zpv1e(X9.j|T&d6w{CG #3TИ8Cidќ6ѡ!TL핬,\{1ПPDEWS"J 93J/}~Ԭьx'fE˰f]tijUWնѮni84Oj; )*hpb$]̜ixE91\_/nn_~Q~O#8*љfyI @nnv%`\4.qtv!'b 9c )5"'B܁bHl)mucod# ݴ1-RH =jނiM:ު~xGX>‘W;i!g90`cVT\ӏ-A**%clޑU+~bH`ђ KvۥbVOb21*ךXlEK*g)T^ŵzMMa:t)u.}O.J&"t$9] C@>3<7 0(T$y@͛yT&">6N6!3[j-S.@3T]RꝱkŴ} ߖ GTa"jqRw6{|$.6B&JjʐL1-+fcL_4Gt3DG_Qa$_ZI};A8 {>Fv#brhA˷%%9?:hdeR76F ՎnDpjp αxu#e`sR̳Ë<ʡAͰY9ϪA#vdL#&L^L58XyƔRj2hU > d,pGC=Pomp޴pN尘,F`EՕ|.Iߎ׿kv|ms {բ)P~sQBfjޢէs2س!`5ÃpܼժjG'J˶\J rL 0 3v%xra2h$|2!sV4!hݒ 8/2GaM z%e?!5.ʹ dvXJ,gg޻`k/"omg{Mr0D`mո!*Oڰ˞Vo^V8rKIxD~!nBv^oڹ,Cwf4`)i+&O+Ț|8jqIRHM`cE>)3L'c9Xfm((wܬ- qu_~<3jgP_om D1XLA!`ܼ@GgAd[`@J o6D? jTrͥM1_TMU폳𽑞ya -5ӻ V@ ns,D oB!]6Ma0!D ېӤD);4rÅ- Nd.Hų5M;pxCsvMQ|AFI4 '@#y|LhjiŬ~iguُK |U\FDpk6A ̴Bn'fƤ:/N"#dY1!, ʩٲd~luA G$Hnʻz-"0?ٺ| ?[@4࡭r:38i&|->*F; VӴJK[^Wӟy31JeCFAι_<^Uۘihwx}84ھ9N,Y<>p5O=.^&mM@ IufI7E $Q t17=Yt[OD͔i ҅49QHpwG1`:0$N}\1A`8RV3 x _Ľv穻1I9% 5\xrs$cy/WXi,+>>0~H *S 4a(;a0 ǞyA e+6O2ĈMLDߴPϘN|oicx3 *6}w#{:F9g\r*2$7vG/Aad|&=b/Sf u'RP mJSFTˢS֖1.eMMa=q}Z301i6 3"MQ$&vgSO-p ~1}(L|qi(vb/gip<N`USGmQ0*Ĭ҈LυOW0v\5<;iPpƼT73XԟfiD@Zyp5bi,͘a}~0ļ{OoXsڳ(|7̈́޽T挩UV+B E`1?rdn;l|mzORYY:썧b')ZkFZ6D*@hOmR?̍7aq )߇ Ƴ>Y}Tx*rb#҆ԁ)umĸ¿ӫ[WĆ;ܽ(t~_ohx`V_NVfKc&U40SxPٵ%s7v!Y:,{5LN<ڇt dpc X1W!WYXG%Exa paPԘQvv{9^FTؐs9CW` +W1%9iSj/R9f4{2 w@5; 9Sܻ_ O^KIS7nM>b>i[gb yhLuxQCڌa-s6oi3L%as ON: xNjT>L imV.7NďgVb2czNS-LuR{ial S–2,qm^bu"$D1kF17g?6Qԙh@BZlf#d.'ߠ7II!Kx0 bF0}F$!:DC $ Hu=~$'c9{0 #xl}!)wwpKI r\d]6Mis|>-uM1a]r^jQ}YAw<쿦Է_?.0/~_ֽ o>&NR g>7$S^{]yؐ?+]'OVow<,OjTrԱxʗb#Lt>s Bv%_IDԶ*B#apo@q^tI N5<竗+DlVA+]2 avC;H/mAؘxj z#{%>69]Κ#T$`){߻5۫_}Isǫ['L1Qh$?5Q80mqEk (6|EJo?DJ7Ig2g<&Ief*}0 &OF]ҡ b?f"Cfсv(>sTI> Hik F AN[DΑI!4lFVڄ4ӌREzkT؊3 ĒVaRN;W<\ xs{p+Vpe"(Oغw֛eْ=--y*(UtjT/:t:"q!L7lv?BK$bo/ẐX8 {]t ܾwe>._o҅乃L-f[fDϟCۥ'7VWsz3~L`1!6^ GqG,stdˤ@<^?`/3k)6rm$uT*bbp`10=1}Jc섇FFY 12m=Kn |ΰ6L eb3'0-l_$ .#ke2u|ݹsap/e." I-UE4WqN1A0@,B,6pȟD|! Mi:vOA۳QjQ{{/ti`"4ۙWŜ9B>o"wZR<#jƉ3$|{=߫1xW |:p4FA0[6Y0Zc&:5c fAjɝ,^C?r>l4mr֭zr= f9U#1ZY AgZLD`U@tdBPԿ)֠40V=A@ʵV͢M``eȫOH3UV=myG?$N3}V[h L#nuy:ݧ4Q{޸}gOTm !@YowyLÂmȻRg`CkXfRm2ND[ V'^T)WM>x ӣ|?ܡ 8 8n*L}lL[ |NqӢI_;^kW^_zY `yQV߮;Jngސ0|Ŏo|sUx'oqY `݀g~铤G/='Ԣ+!'?`deAX$9F56 7ją*=z 4w!p;Ҙ%Mu/{vz "MNR\Ć+oÔdİnZ<ǐLYHF(pe*Һ#T:~A*G|?75 RigjYﭛwVos_>\=$3kiUPjjL=Oqh҆bj;(J^,"ʜ-l  ,ci ,^.3p9=&:1`7"] Y0, !P){zc1F5=&>_2Ԩ %#ݻf"ջ_/[3f0Es2z׮73kuT{cڏqu1H-62{.t:< gV^|;w}\& \7\ֿZ~|͟(IpָF9}H` T @#%"T҄q>A430g ݟ!˚5xy+YfaVX@-6.MB@f~?~[^\C^泙2!>INFȷp\Xx`KW8(+=F`3v? E_Ο/ޜ܎0۴=U[-C"DfL1-.&CA{&ZY;au,#n3{R9m)1~1t@Z+xU0⸩3 JYu,"~B_k)NK7DxؽY6,T6+bf)iiOӸuIi^ٚ!tҖ0R]Uo^݊z`h};ʫkοq @C zΧIػz RI]jQt,hQ)g_#:Aا{f}aU0 Ӆ^p؀1y^)7zo<F݄T e-L(Ɖn[9OJo%!m.4VLT5@,~12xiCV)޽Ou!H$݀{KHHӼ>B x1vϽ@={$qأT.מWDwjBmkb1%aȘ^1}Γޘ Gza#Mh$iO+w}wbQ6j &8LI B"5 Ey}ݱ[-XTr%[q4|mǶb4T='xx^TEw8:ǥ0:Z7V֍=lܿxl&Ej6/Ө<pqdAeg f$ld.jA7y}]oAE0[1l|#"&I'a{hǿ?W"t٭Hl!63H)(R-7m9u Ybl'a8xN!N@IDATķ&tT_CLC$D㘥 Qo#k^PLovDc6o=}O$9 = f;m{H jiqdԖiH*L CĄܢ˄BIΙ!y=E ?owYs?IvU֝‚R;.GMqI'jsL9▒_>D!v,~#{֛%gZ3-!p5@u`0a c)kp#{ޠ!5i+}c6٥OҐq՝ˌ,>yX&@Ba[ď1*(Edj_~yKժ5MZe3Ѱu^^sօqșu7J;M&Bm^O_5+R8$sΖ#%%.\jh.R~}9o<Ŀݫ ќE{9$>|lvؤ)у4!e 4Rd#ب /&=qtC%]HTl@d!- -GhfoHvU~`k 1t'/H0Thnݨ$~}`/~BGl1j a;@J|0 tiF0@ص9]h`:^-c;tuZ5^hֺŷϿ_<wA*g˂s,5iuFbAs7N?pB׺23Dt#?I:NtSqLD,I#Fڤ!rՉ ;񬤞RS׼+Uf0Ķ_1SP&H$2UsC=\LѤWϒxd T o4X'laJiKZ{{4'94Yu?GN+-zLeSmt7z-IB05ˉ3I"YN\S.dCJţ2E0!ǜ#(O%-KӀg^m|v8.Y+{?Z?Ol%d}[/F Vm^erͿl6x^i{woyį5>kzYuk:ǥ1nC+OZe|Iv/d,!0\͖=ϙY{8f4#ȗڻŻc˅輻;yC֛پft;;Ox)<]\ vgZڟж{vrDQW4AK 03RLdY=^ȳDn{U9V{_g Yy|8qd> s]TVX@ɨ$z\1m&FKi<Mb4fJ| uJ@^C) W4V1ZnkҬ^qߜƴ+̅\k=/IE5 Ù} mTYu?d0Hlipʮ1S-Zhv2KMÉi`S#gW(ґWtoӐ.RfnsX7$a*}!a7'h[!lVmGɱv4%gspAlʧr߻~NC~Z čF[ Mg8*ˡl8 FPfMy;DMQ" Qap<Sf&)C/$>[5zfӛD޻в6?OVϽ$ Ly:;'e٭vujў^{w3v\%6DT_)bLrb Xpst1GSU qiU3!`1!@S.g̀Tјڹw*=c<1f3 tV-`[X@P«A/V7t5A5cÄa\}f%8NI:iHs֭Pu^Ioc}i"jS{IɌӔf=,s$OuQx KzlI R 5 v|~NnPO{}3A.hHf& 1Rzqr~Nk3I>>2&U4})Iǔ!Ͻ\mR5Ib4[yeي<|ǟ߽9hA/F}6mE/fxSvD+"Tm1Y(T>"Fb6/3 vCl *yޞ4*qO ݭZmɃ@1]tAO!~vQm3L͛ņٲ6y6ϯf%)X?ޟjb|*ͼ>*yWR$x7(9C'vw=g Ci.36S/ăI0kcIMͼ>)$7{}ǫ>4bk|i3[͓\4F@}*ʶr @ɕ>N# AL7`##QI Z5œ#-i ù^J P;A,s%=d*G1eT P[fE!ؙ3lһwd0bp:U9v|JL9bkMiK{`j]{t-",pz.΃ֵUȕ 2&5| E3/D m9u3y{b%ԾliSm'O s"z3ލ.kۢ`,cg?ڽ,l^`3hL=UI{[`*2˖4Qگm_()PůI&߯>h_tڥ4Ng4xںs|ɳo?{ߛ9+{~;ܸ~u;k`b޲zv^rt8Jl=cp7IOR*l{yHs wERKmax`{?S+^kJh-vi74ppw|Jay駮?17Z2$$Hcd:"F҅ *z>ڲ]ANZn6l{mE$EZ*T(p:i-DXpyuX&b QU}5 AL"K6Nm rd5\o! DN۱6>:QGUb˿t6a@ :_0VW;joQQpDԓ)e{P#B7HۈfCHϑF[ fs>oqE}f8no8U>,٤%f1:p0;b^%?CH0L" a8_}~#t.u5/Tm v!K1>wNR"~məJ>>ӧ:E_F*[2/s\:ucםXɥC뎞>{;]kɿ3-c-lm/rzUw?IZYd$#L HNzz^TSғ /t̞o "S;ZC (VI Yن:Qo=4Ԙ k>5^LmXT흙BVFRQ.%Qm$ /a+N>K>`EF-JRVk˞H̐rP<35;IS, i83I7ӯ n![YİG @ K҄=i r k8$~C#3+1TDPח ?XŷzKg 6/%OxZrg? b}mwSܤTaPjtKa$cZ᳾˽*"O5$LJ 8޽TBfl b0,ʷHk;y3cH =I'^杍oЄp(-rn!H {KѠ"ifǂo-Ѵ4;=*0hle"2k&OIL?%.>`2s`$*se3I0$+[ FH]ug:G 8:bv R " V8Zh%\ܪQ+kUY TjwaSiڜ'$cs J>up1V \F 8(uBd[HirS1k2NzATb'~<f42[hL}_fbj}VpVg(hpСvRsr`t-DcwVKOP>N5ͷ |CC8CeN(Ly4]a&=HbେB! :}$T>UB1nCUmVVH/5,Yݑw∓T) ۼ"}Nd#`uADD -HWΚw!Z.V@"ō].hmӘnj]27EݻD*ֽ8 GFE K5 l/% c@ Z%J!&bX蝙yF{$m~Ysƴ0 >H7:[A]٭bP_gE;(S;̨^txyoo;VG؇^rFZ#_}̊Fk9\U#_ 8J^U+ϸ1$M%EjwTBf@=gl2'wW۱9)\C@A|X" 7:)WHH[(BGMUy'e>/*W0ufAB bI$2_Bhagu*ԱߒX 1Y]cn`y]gGdT9gIz/#EToFD˫c3B9ȴI!ԩ^DCb'1"-I#- 2FR*0EQSK :cYZ j(@^Px8ɄZ|uTwm9X7v*ro^34Px>>">[8tK]͘iy`qs;<(=:YA8f Lp lRr#֦Tq$.F@!9jJm$ą|w5ES0?kej $3.9z۰!pI"ԣN7uCeіg9:n/ $Bw~b'z<50˥oY(?+@sp[_@nε^: ȩEճ4hqbjF$+Ap2r-3X#soy;ցhŠP : nti(:cuaV`8ٌ⊳s2ι\%Jv4\ȕvmИ^rgS^XWފhsD"P # ?8/pRPNu*$gO¥,pfg%L@Q?Ea\tB}ʴG$2f[]*3nfj9[`˟b%#*=P:ʝ*"mz\bK-f$4glgbk ;[es$R,Wa Pge9ȥ4"2jwOV_WfTP\ip47 _u\K'Y;?6pDAu "ApWyQ܅40#42K$"[s(\a1f՟ܠI|k:C7sp85{&KtpDߐss 5[`QS-͵sfX$M4sP @iPt-Uc L߹~NԆ$Z֣[޴s@r†%jSMƊnev_ ,\Hlik^LK8"5?P<V)K.UH]"yXCcx$ JY^aeѡå8UU){Ɛ!U!TN%T uh,#>}}}φ (,YHU)0ÃNjJw&.A|N}?}NAFޠ#.,HSK:֕9hLVn=[:߅3j.WQ4 ݴY@jͻ H@8OG4?hg| DU8{W%$~T 8vtСbIngk! ZqD::D)&ɔ.wcAZ/T[AP#ŠU W ˠe.%7Ո-ڀC%c3*-GDej 0DgeHmP3L&.Fy}YJc onN N\Pt^a'~ 6[J_Glf.:C!iD%.yK ##!Q˿w|yJYqwYrFtnwT +ڦψAǠqN џNLsN[H[ڦ!',cqb1'aDx%XNŭt(igV;1~_f&F-C|zY[aۡ(,\A p? !H|A][8_5 bS~ƭ8g=a=E.K<1{@^ ĚRijgzuشpK?+X>axX@e]ޣS47D?HA(eDtOe&t8EDm#uNuO*QiW]0 fF$.\p+ g.q'?)L 4Y ZT6<~sogY5UZcáv .~'&猓{8hx F0v@+@,AaifNFS1t,|BpjЫҵcyﺂMғ16WH͓EVxtG@O'M=Vw8T nn=$>|pU/Uzb#pcÖ!۳3Je?ay]`qO™ GuZHT"4 $o*& ⰅCSmlkw^`5)/a5s1} 3ulUTDej^EN35$lN.ȿkd6']^<MR#J3v6~YNͲ냷r; VOx&`{ b"Zb:h9y}ʎ!2DgNmgUpM];l)!P'u" @@Z4:"C݀ʪZK&v&{jڹ VWp37yC<**CPuS_= y hx-}lXF 6_Gvv=)&IHi]6$3ґ;kS,uE4U89z}N^`| 9qh mrAlSx/@KpQ7]TKP#cyA«> Aw,<Ұl$⵶!>H[:;wH#$ g Go[dD0\}sas@? +OO =CJ֑,l+^WHth677`wgS\#Z;__AD}YmU.sPd .ۗ`p|vhW U ]0cl@(0KV<^BA` D NJ lUExF$F!6E% 'HnvV]W#ہuGSock'`.R sg eh2:z,@xa )u oP\'m8FgڊIUNd[ν ! Q"C&]\ N.5ݏRPa% >Ջ = Qf|m诵_]$K&Q:뜾=.;%KxKKxƢXG*;&"AT*?mo\!OgGHVW8++̫fVE{.ĥpuMF4=F/0t-9T-tbxsဝFU,#FJUw7v\;cVMސ|.1v<~m9"r~`Tj \} +2@jăJ~Յzy[VG*$NYݿ C;)C^W!2^YOX`R^?d*O&LMš0ԱSD͌2ӖG@ W1j&D9=FLkI7kLϘr8 |R $N#-݃=IW].)PrPaɄtq=/t5)JE`VsZ4}7 uti@D:<<;.ej+"ܹ?yz]+ƞ ,"pNCP}W)&it6ܘ8]B,ZvurM|b\qXZoI(9 FN1>ɧCWn㼒δ՛^XPn9un $p8Tln h]jbf k!DRH"qo@l mS\ah'U[umbQ?+/)B7 T[kVAϕ2H2@_|(B&A&C 9!a?}B, }q:4&=S+ǼX19K Wݝș3. B\}Y4\nbq&`mvf gPI<GIm#LG% Zx=d|xV]0Ǝ0K1ñ'om*-3b0О|ĐB^J5(3u!Y1\ :]mW7NǠŔF?Nj[L]PgA]:E6o`d~wܯ*q$n%*ԡҖ i@ %8a""h1;\^WZPU{'dcHDŽ ݳO։L9Ux<:Qm$`*i-#@ǡA3ɯôKѷJ?4b? rIƱ)PiYd(Y9{RW^:0?! 8C;a1NHj}|近u]@z5ߏ)m Ű8݇_@@; }_[x`n'JK 6Ο.~f5(-.# 1z8@QlPiBL#1Ǘ)@lKJv %BAW/;a 0Y!ug~ϔ݀>ck҂ӵE E3ϡT[ QypN*J)zr*Y1غ`WT (Px"'FnHx|>#fEӇpL 0vƅzҩ/%FC >q#~K9~ AX-;2Ù[k7bBoleLzuJ"LW273iI,Y&\c u&}ʈfeRCULZ׹&C#D!tA7!֡ E1C,4+MjH\ݽ忟ϟ}ۭWCfo2U+z=0-mpD@\N8cڊƴSRu5.r*iNO>zxut_@,=Nų:3?椀n%Rqu#>%H";;;;5xESn9[ ec$DSF܃jAB T ?7\_+ʐH b, %CNB^$"27\A|܀a>5cL* ,5]7P#zoB ;xQ:R ;rH {ذ 0Lg:o }J_]*@( ,:͐Sfm x$pb:HfCN?Wҙ6'9I4]8#\x^P~߫ uSw-+_S}g^4^@8^G:͎줾3iw `O1>>:v_`''s50w=?\ۍ6 h3mg)R%Z_@}թ\BDä́.l B+*.j40EC~~? 5nf1[\KR3:fc@ wLA\sqSj:Lt`j;G>aG Q&xtKkrDg*VQd]{.0X,ǸN<'$6!6PQ.~6dIc6IDAT\`_ ѐh71; sJ-> AJtHT8ť=Sڇ(TrQC4. |&2V H-(?k [13t"qp:~­iViXɌ_`VD[4F=cfK~p.1kwTj(trB:ǧ۽&ϦTvg߶ xϤzugz<70'X)PHi>aX>{rpsh~l[0;@Kt%@i ©B E4xrAQI͉WoɩF;tE I`-=:C,!ک\"wZJ!b =f .@i 絻|{{ȶJ8(nsu:H+ z=Ru|\B@ˆa_?(RԘ TE@qm_uQeˊЅpW`ƽSaػ5Fq! w$8q\$TA^29rqAyX4inxuN E\ڎ$'-_vù G 8)h@6:>wr{\>s\>Ym />*}ڟe1ZBږYsq?;=C:F/txoYKҠG'! {* o[[wФc a"S[.̚st8=ϖDq-9QHg\Ớ.+.@o0( olp_eř>ڴ)Wf$*3L rCK!KԀ,-!L3$ʈdKX!8Z! x8nʕ21\ֳ\-v+~_2 Yp}~g:=HtYόO d? 8%X%!-3?+)qXV-m 1b.g'^_4 !2,ehtc@l\7\eHu @m@ +7X K9h5Ίn%}>b jؕOt.=p@s3;,:˰ siP&Liaco-!&˷c"S|,ߩ#FMHhiIY ;hտ&6'zH۩;f^p DP"5B9d9,匨M&O.y,̀ri˥D <z51\)p_'J>2X-[+ߓ˓7Nw@qDXev2l:>߿j3 E%vuAGh/$5ڶm}!=0cY`k$ 6`,xԠ" 6CXasaQ*D ! vѼ+E݅ V tT" l6;bnC.72B(А7P퀕C!XWr]x7m % i8?̜p~NK &`<~[_=KU]~/IRC+ƣ@_OE xv5ז4b]+7#S~Dm!uhbqNDc'Z pnCƦXm-8-ƽ-$Q#pk YFʇ(K#_DJ15`B\Z:.\4ޒޑXԣs=&h@V(a A"fɓo@Hvb^]GG $!4ѰJA?wAk=-k"B5Azt>ј҇ e*JmGm:2;ۘ&,C :}֞'ZpG. "xt9C\ܾ{'vtGBoE@S)ZYE-g]/ApsonG㸜,:ngxFy hh%<:(ҩԸN5Wtv<H6_`i+ oV&5F0uC]:l*,0|5]Aa} Ui:V~7>c et#$h`@MWX-g#:3pW.ͩߤ[?]wGWV7xRbZ7yT0m 5S z$Bhwԕ~' t9zgy.(+0iY9BQ~ %> 79:CM|8g CWmЛ ~r%yoJ$`5H1 R[ȋ %A wqBn> "i'z%;0}4fh@tX.C7/1<`{ }UVխgnefBqfҐkNFu } GiG^PlkD@* [X+AphᜃnI-:R~ JP+]yFF_Co2{ UW*R2pas8o#H.A "GOH0gG'9{1<:{n;tpHz tI(Ds9e]M_{ CgOpM[8|W"κ ib룑S^D+=篾활<ㅑ M) <짶9;J_<\5<ypbTEL&TL7Sot~ T,&kw3vCEBPX_7?`EL$xkKr@V1'wDBp)L ehFBTj?+*l't/I@`?% H, 7.m RY# nnkEf%hϝ. pF_Jz-:c +wue@G?ALA%37Rya6Sڐ=zFh 8`<:wq$i0 Ih(V͖+w \^ sia<=:Ƅ$QZGNMɁkg9)]f8\]&Pc[)>%|e尋{Su@P()ZLאT>ɠyCO5dyHB@K*Kek)dIւ s fx$" 8ela-:- {w25W#LEapb$]i׸JQ^'$ٻR ]uD@= hߥ_$Vj ͏y,%ϓYh=Q?ts# $h0gvr q:+Cp2̮{]ag;(~˭yVC#H=r1bG|/bxVoW96ܗ{;[q vUpr)d`FV)t88U層 GEokR5Xz̽W :2N~}kF$rCR/aRB!%nRoGԂvaWpD ?WA:^9CIvsܠ]j!M6qkֆQ!J3f2"|(Y+CsSuk''1kPj DQa !Mضˋ6㝽|.wUu!@q}gINl/ L ZC= `1 ?jgKU};}Z苨@FLYMP(; *rf@LC;ٓP:5h3@!KeŽ쎻X8ZO >zZҞ{ '5DBwZ=fkSJ@WUP qO%EvNyg0VZکsV4cx*u)!&U*0ӎ6W X$5ru+a;k m,wFm2b܍r}>-w%%σ__Rd^<<ȇ a%ڜVl[V^Gyz }p!w'cwܷʘm&A%6> -F1$Qt}沝o$1`У.sVGmR T3 ̟6N5}F>Z4 + (TK@\gKaJXҕv2$OcĊO7rJv'\ұZfhH'A_$Ԕ0T~/ ^'ܰ 6go9\l.B JKNB"@Nc_b ''E\qx%kL9X,ݻwwܟN[ǣ0c8 +:Fk %([Y zW吣pFzp^ + $ (-CdH_gi$M<\EswVp{r^As0S ^O5&p-\d,#Mh9FthLdSa:MnÌ EF(W͒P軑*H4|O{$pG exgvaB/<֬XBggWx|97ٸ޼d}1u;9?D"Jn2Qj†ڜ>p 7HG0.4 6ݸS$ 2Ρ.="LG?y {;RGjP: ~ڃBĢ兼g{ߴ7A?_m`^ܐgk^ 8nԑD6crK4I xX}I}#;x?D Q 4|žCmt4m9ϋtu.پj%#/Rϊ9FobiO9 hg[MX٦XBgY[88/NY@ O'{h 0b h +]@;'p8xq8;2< Fh@::> =yu:0X4w[i''lH8P;Q둿6UhÐ|[F{3_ggWb<>#)Ӕ987]%w$XK VJ͵Ĥg~$~gD<%7yQCT׶ T!J#7i8簅=OO񾥕=wԘo4RG9% yo!v9,zOa޻p썶'C,z4a` Jߤ3("\bNa*7X9׊Y wzV.-L>#@'-MӃy A `=3qH 8A:8 \G7X%ha 5n[\ `'.?o\ Ō@_ XMG%Uh+vʔĐ@/z#r4"{V<wz%6Wj=σz9|O~_ %:!W.fKv] ,LOPg N''k}c(&.ȇ, .!GK~ DvwPppKli/Z"Sx&0v♖4ę{)bR"֩/HHyS>r2 @cAl#'Բ=|4KKHD">o;Ի350>, gq^g A ̴ǻ=^z lFJOc7 xeI ny&{c1Mm}6N8_:p&-><9]\"VtE}߳_/)Kg14aux]t:0~|j/ƛ8YK=Ts18d M1CbQХʹ1z7D"h@))wnsC(B8f,uu`wȋPS0* g}8Sñ"+)H@m8e| d`x.RRP⢧c `Vѳ#|$&2%gIFki@y)/ҋ ̓䍻7;1RBp?Ű;Mgl)ǿ,10{6wCn5SS=BB蜽̆?ۿoc3(ϦkC_?(JO%yxPk%h^37O@ fO1`@w C _90IC #dmr \ao)A[D9(2#R Q[ _who 4y7 x?$4L3;NTShs/]S*{/kC{D@|D1!zߧʬ\zlV޿KOu03au{]g^5O`Lo( У`XA+. D:Ȇ4F.=ېE缞ω#_d*:3@ E$4Ps`uCmͼ rF bLr)ϟSl{ e &BB Us'ާ29Ϙ"?_rgQ,۴hcզ%F 4L+BBir+)/-z_~KH'{]"[DR'WU|wMߦ 6&:vO =k<3'>gd?E',"quġ,*>oӄ 01ir ( 6<0`w?z,= R#̓N B qMZыqۗ8P8`Y__P! IQX4C+a8׳DuU}%*2WRD*H|_#PԘ-F_]^Z}J>?P۳_»x xOo=JúD:Գb(= *} 5p>^.,N': hDe/VD`5S568{$a EJSMWuۄX)yW5j9`Xcm/@U@5l2)+%sfQ|(e."/+4@`Rn30LQ0}+јqz u #P{TŶ:.k_D/)}~#)wG؂xsqdP/i`%q}7 1H XBq|F~-Б(I\A $#10Ƌ5Iz=郔L\9m#Ӄ՝31{\v#%( (M 2nЀ)ʚmU%q+ N Y x4@goq@*S+?n/.>߾oNRdi wtAu}')S}uIluO͵oHGd7޸?X`kP3Z)znwhɖDġqP&N5>@"f #QH޴P g2}p5ǻQ!1,^DVe_pD=Yʹeͯ :\BR߉RP@ps_ > \J Y[[cj#O34X9ܴS>K6!r(0-A3u-LXws(gY$,:M^RI(]h1hըlu{nq"F.Dfv\63иt\!\< 6lP @ 1~_BҬp`ח//1]k0qq"ݽ|IsCfiOޟC VGx_Y0WggI1$b+A.=;8Dk4!>@@繽[3K1H欳 % $L)[{ T 8cJi `^ s7{}+_i>z0_oJ+|KCV]K! {{0< loP!9`^ ! $m޳/5! Ф+D4š B_D^m]E r A<27)>\_b!]^| ?ZY~g_(Qn]aE +=_[2>r|%u/û]wM\|>bG? >gz]`v KKLNn/6"Tk<4x ztM>j|hL, 4IB-лLڸb=| q=b}cmyϷVWQ! AX\۽v,1 }|@5'_P K/$PG"  6Nlgs@@RpJ D $<垸f̙P$lt RP)]74=Pg%o+&hyS2= uu{6=EL=F@X`vpi9uU p]k%#c\!PoeLvUaHA |Fe9fK$4Vُ醨IkĴwf4GJ =m2a>_`\uKqYzo |K.Mן0S֫M/"P-W*z{}}_?8x UJ˵ R^h0j{}b:,슸xc^RI0gӖ`8}}ye6cziՀs8}Y:7EF7"&^=)qCfU !]ֵႷ@DPqx~\"6w^yE|LJoem'9_['* TBWCgA8$PN 3`)Y:䛻X흝Q@l *Yz-(=( u6\@w+~ Z p|Ϗ*4'Nڳ=_81BS`A+YBm-nJ.\7CRnxQ:aK\1]ci|WJQZy=0<Eq<{z^u"n;t!]{_\D+Oϳ,{.@tüô1P[=i "uR@]nxJl^*~/S'u朗AWl=0 ú^] u_9Ѯƻ|nx8^ ]@:X ^ yWXy>QaJxqCW3s3"`hvςHzm+K u^1n{MpcB=w 3^=ܽ6lu_ ]v[מ+n7pկ>3_^p}*75q!M!(p><V|֯~޳G\D?ATu=Wu_V>ymzu7<א >7{kv] b u]gߩ_z^g}mu^I^{UGqV^ul}^7zw*y'"{=V?祂 ú7Er8c^Pu/p |םS s߭p}{Mn 𩰿qC5xsc4[yz_qY+{0u ѽyлu]`<8EH<FOU) 2tDM>4u,*=/?:/; 0Y*C&1/&*d/C2K^^gJu|g|*{uA/?QTwonuMZLl@u@BFGOKOOk^iii{h;`l^6[feji~`zO!^Z^s^z`}cR`1|`#`^?, "+ ,"^%G^`{sneF;;DGnsboeF;Fn? !   "! #                 "!"#  $  %#  &""#'  (& ) *  "+  ,-./ 0  0  )0 1 #1) #"23.  ,$%+4,+4'3-% $,5&2"'.3/* 3    #! " !6!"6!"  ## D&$+'*'%(1.$ /+* D,&&21,'+*'-%',1$&($) )$.  /,+ /4, 9,41;5*-015.  .5=  (%C!'(3"()7 #/*8 $9&,08*29:7;123<-"&(73#"2&95;6'-<0&(A:)0) =56 *-'3">94+@4/, ?2A.-A=?0 /=A. 0 .A) 0 7)># 1):> )1@372"#?B2.38@/$, >:9+7@74,47>44+;B63':A27-<80<@85,$3@<"2&B?63.'=6? */B;23%,C (CD!6C,D (D$6GEI89:EFI9;:FHI<=>GIJ?>@IHK>=<LJIA@>IKL><ANEGB98NMEBC9MFEC;9MHFCD;GON8EBJOG@F?JPO@GFQPJHG@JRQ@IHJLR@AILKRA<INUSBJKSMNKCBSTMKLCTVMLMCUNOJBEMVHCMDOZUENJWX\OPQXY\PRQY]\RSQPZOGTFH^K=U<]_\SVQ^uRUWIRK^I<UVTaMLXbXWYZ[fY(\]^_`aVeiMbc(YX^]ZkgddefdhkfgdViHMcDeWlb[hf]Y\i]fmn\jkfn]\kiieolmnelomonngkkedHp^=pUHip=lpioplnplW[oOqq_nrVs{l[toqW\sOQu_]nviktPQwGHQutHWwRuQIWH^puUpW[WsqOu_v`Vxy_qvVrxvqrxrzrqkzr{`|\y|QwXb}ZYdx~febWbY[y(^gxdefgnmekjPtZGwTpoupnWol{nots\|uQ|{[~tq~[q[squ`\_yQVaVXMeVbMeb*ebbe*Ybw)X}Z)wy}f(\^xjmfj\jmj}mj}m}jzuoWno{nt`vyxv}xvz}xzvxtuwW~{taTXLaXyw}yZNa`jf\||||`ysusus|u||`|y{tsu~SKTSLK**jjtwtwuWuWxx~nkqs{rrkz{hdgf~xvrxzz}kh{hrhhzrzrxgegmej:98:;9>=<@>?<=>>@AA<>89B9CB9;C;DCB8?F@FG@@GHHI@IA@I<AKJBBCKCLKCMLBJDMCJEQPOQRPQSRFTG<U=QVSIWUU<IJ_`XLM[ZY'^]\cbM'Z]^feddgfDcMh[b]i\kj\ik\nmlnomdekUp=pl=pnlqOosVrtonuQOkivHGwwWHHWIWpUuOqyxVxrVzrx{rzQ|yK_YZ}f~[Yb'^fejkeTwGWnptqo|QuqtquqVQyMXMbb+b+bY&Z}&}'^\\jjjjnWtnxyxxxWwtLXX}\||yuu|uy|tuKL++}wWW~r{s{z~fgzx{ejeNJETTwtZwTZTSK_U`_J_a_aN``_a                           #"%#"$$#!  !  ! ""##$!!  %#%%SUKJ_ZUN`J_JKNEJ`NJKwa__ ##&Z&'^Z'^Xc(Z^(cy^)cXZyc)*wb}Y*w}+Y}+$"";J==N>c(XV*stand01@ߒUpƣv{{Z͝p}wнsάjޣmܹ" ^"bٯެb2d-ݯܯI8򳂟αӷ=쯁 ާhڑNyїGXz8 x &· no̐=$94 >fC;"&<_Op!(/2%1>V4A"Z7R>(Hd1g a͌eӈ'\ hу}Xzw-d(ό&ه3q6|q Ė;8Ce1;5bu3ssn6Gid:?gB ^9vNs`g2&&=ky2zCZK:EanAxwBr"I#xIuTN}{ZhRT$O[?KR 0yeE(mYI}K]M(d#!SR{L\ZE^F2a$4d)"bb|ia˒q?og'imJEhA"dhpdh{VqNpq?qAqzTrq_qH`IQ:dWP3[0dN"<:^+%&M|tqL/ rАnɍĦ5K1Iv P,H' gD>'z,4K*؎V4 -,ѐA3Ϛ2x=pĢ !w"΄6ƚ,nwgp{hƌt9 ;zSpsqQF{zOK=>~ m{#sbiˎpqmsQhw:s~|krpsc+7{]ɣMӀ q\L؇RۉJ͌Piq?f7.{?2V^qUŌHqᛛRqWޠ$ޝqCq5qֆXVB+stand02@ߑUpŢv{{Z͝p}wϼsͬjޢm۹" ^"bدެb2d-ݯܯI8򳂟αҷ=믁 ާhڑNyЗG]z8 x '· noː>#94 ?eC;"&<`Op(/2%1>V4A"Z7R>(Hd1g a͌e҈&\ hЃ|Xyw-d(ό&ه3p6|p Ö;8Ce1;5bu3ssn6Fid:?gB ^9vNs`g2&&=ky2zCZK:EaoAxwBr"H#xIuTN}zZhRS$O[?KR 0yeE(mZH}K]M(c#!SQ{L\YE^F2a$4c("ba|ia˒pFog'imJEhA"dgpdh{VpOpp?qApzUqp_pH`IQ:dWP3Z0dN"<:]+%&M{tqL/rϐnȍĥ5L1Iv P,G' gD>'z,4L*؎V4 -,Ґ?3ϙ2w:XV/+stand03@ߑUpĢv{{Z̝p}vμs̬jݢmڹ" ^"bׯެp2d-ܮܮK8򲂟ͱҶ=믁'ާkِNyϖGXh8 x '· noː?#94 ?DD;"&<`Oq(/2%1>W4?"Z6S>(Ie1g aoe҈&\ g~Ѓ|Xxu,d'ό&ه~2o5Вp!Ö;8Ce1;5bu2rsn6Fid:?gA ^9vN 4pnN/B6F6;7[>s`f2&&<kz1zCYK:EaoAxwAr"H#xHuUM}yY~hQS$OZ?KR 0yeE(mZH}K\M(c#!TQ{L\YE^F2`$4c("ca|h`ɒp?ng'hlJEgA"dgpdg{Vp~OopCpApzUpp_pH_IQ:dWP3Z0cN"<:]+%&M{tqL/qΐnȍĤ5L1Iu P,G'hD>'z,4L*؍V4 -,ҏ?3ϙ2w} mz#sc~hɎoqmrQhv:w~{krq~rc+7z]ɢM p[K؇QۉIΌOip4e7.z3/U^pUËHpᛙQrWߠ'ߝpCp5pJ=!=*O>sXV!+stand04@ޑUpâv{|Z̝p}vͼsˬjݢmڹ" f"bׯ߬b2d-ۮۮI8򲂟ͱѶ=믁 ާkِNyϖG]h8 x '· noː>#94 ?eD;"&<`Op!(/2%1>W4?"Z6S>(Ie1g aoe҈&\ h~Ѓ|Xxu,d'Ό&؇~2o5|p!Ö:8Ce1ԋ5bu2rsn6Fid:?gAۆ^9vN 4pnN/B6F6;7[=s`f2&&<kz1zCYK:Eao?xwAr"H#xHuUM}yY~hQS$OZ?KQ 0ydE(lZH}K\M(c#!TQ{L\YE]F2`$4c("ca|h`ɒp?ng'hlJEgA"dgpdg{Vp~OopCpApzUpp_pH_IQ:dWO3Z0cN"<:]+%&M{tqL/qΐnȍĤ5L1Iv P,G'hD>'z,4L*؍V4 -,ҏ?3Ϙ2w| mz#sc~hɎoqmrQhv:s~{krq~r~c+7z]ɢM p[K؇QۉIΌOip4e7.z3/U^pUÊHpᛙQrVߠ'ߝpCp5pvXV+stand05@ޒUpâv{|Z̝p}wͼsˬjݣmڹ" ^"bׯ߬b2d-ۯۯI8򳂟ͱѷ=믁 ާhّNyϗGXz8 x &͇ noː>#94 >eC;"&<_Op(/2%1>V4?"Z6R>(Hd1g aoe҈&\ hЃ|Xyw-e'Ό%؇2p5|p Ö:8Be0ԋ5bu2ssn6Fid:?gA ^8vN;IxU7 4pnN/B6F6;7[=s`f2&&<ky1zCYK:Dao?xwAr"H#xHTM}zYhQS$N[?KQ 0ydE(lZH}K\M(c#!SQL\YE]F2`$4b("ba|i`˒p?ng'ilJEgA"dgpdg{VpOop?pApzUqp_pH_IQ:dWO3Z0cN"<:]+%&M|tqL/rϐnȍĤ.L1Iv P,G' gD>'z,4L*؍V4 -,ҏ?3Ϙ2ww`UtH[Ga9iLnXoQ\V錹*Sȃ.„ &'%ۇ*ΎG͉/pUӍa5׎I?|O2@PHC[38J==zN>cWV*stand06@ޒUpâvt|Z̝p}vͽsˬjݣmڹ" ^"bׯ߬b2d-ۯۯI8򳂟ͱѷ=믁 ިkّNyϗG]z8 x &͇ noː=#94 =eC;"&<^Op!(/2%1>!U4?"Y6R>(Gd1g aoe҈&\ hЃ}Xyw-d(Ό%؇2p5|p Ö:8Be0Ӌ5bu2ssn6Gid:?iA ^8vN;IxU7 4pnN/B6F6;7[=s`f2&&<ky1zCYK:Dan?xwAr"H#xHTM}{YhQS$N[?KQ 0ydE(lYH}K\M(c#!SQ{L[YE]F2`$4b)"ba|i`˒p?ng'ilJEgA"dgpdg{VpNop?pApzUrp_pH_IQ:dWO3Z0cN"<:]+%&M{tqL/ rАnɍĤ5K1Iv P,G' gD>'z,4K*؎V4 -,ѐA3Ϙ2w=pĠ !w"΄8ƚ,nwgp{hƋt9 ;ySprqQFzzOK=>| m{#sb~iˎoqmsQhw:s~{krp~r~c+7z]ɢM~ pZJ؇PۉI͌Oip?e7.z32U^pUŊHpᛛQrVޠ$ޝpCp5pw`⚀U쑑H[Ga9iLnXoQ\V錺*NɃ.♀썑Ä &'%܇*ώGΉ/pUӍa5׏I?|O2@PIC[38~==1@>8VO=run1܋UqÜtt}Zʗp{v͸t˧~jܜmٶ" ^"i֫ݦp2d-۪۪K8񭂢̭г=멁 ݡk׊NzΐG]z8 w %ɇ noŐ:xP#90U8N>,Da gaȌe·%^i˃] r.E):#Շ,,|A9He1;5b,gs+8i.3g> i1|ux2Ayx,5h*XV1AycJHRD1yoE,BB/o3p}? `85)^.);guZ'$-'k-zAK!=5&q|:x;s095y8-vcEmRvJt,E9IU4GC3[4( ])"`C}uTB4U2 ZLTX}YEZF/R",FV1"n[|vWǒu?h\'b9m]=x``ral_mvWmq?m{AmqT|g~hi8R M>,^DBKK0'U-;N($Ӿ&M|wrmƍ(/ q͐lďǫ.ƤL2&m'ТG,̝<& uuB7'|0=|L/K4 ,ن-A)n1i !"ycIu{wycn؆p:ł ;rKpfsZ|DtqkWTy=>r mq"zlyqvǒiimlIhpw`UtGX[Ga8iLnXoP\VƖ*×SՃ.N΅ &'ǔ%*ېGي/pU[<4CBUG=FX}3|;k==ÒR>|j,;>run2Td򨍙 fŒfݥޞgzvnryۢz$͇ߜ]$xʴg-çgշ;٨i0䰉ۣKoޱ߲޷?寁,Τw'MqI [zxx# ІĊ nu&̎0P*9A 0e5;~**{h>XmBQiObIa){lNmBd,+%&չMz"sgÜfL/ wfѐaʎ.L1IfuUJ&MD&T1I>='lź3>Lz(xCCP -,^6>6.EMP8i'!rÓ"AE7(enIo][CU ;bJbusR.N}! kJB=C5 m*'zX]ˎwkiR|Ph,~AzWvxZRc\+b77~](;o xk~Yه_܉Wό]i4u7.3/c^ǀUHᛐ_reߚ,ߝ؀C܀9р<Ā< qn퐲YoYXm8bMwvptdtU࠻kUaGiZng8_\e댲*SÔ.vtN &'%ך)ɡGǜ/рUKCk:@VDB\kG=H>NL7rLS@run3T Ðߡۗg ˃՘z,ȅב]$βs-⹞sϬ<ڢg. 㧇ҘAɼp٦᫉C᥄+ƛx&NH [zz!}#+Ԇl nx2ώ*P.:M'e2;2+9UP=!HIL:W sg%n#b,׉6\~zu1Ԁv1q*u=b8Ҋ=܆|SrZӐn;ė?oIcA֋EaIvhLTmPKiW uF}rRDrNBm{DS,rm2ZcYh^9TGpeH9]K\TsŁC,Rz}Izr&.Z#mt[xy]r^r]XxYm|rukny0h X,̅bZ{vE}"`e^xleyfy$}&pw͎yFr$S'9~g}{of}`DfxTowIN6e5q{ HP\w-*I-ڵM œӺK. ­4Aw(u3@ 8Iǘ,O6QJHBM?qR Б")GX)K}à¯(\ fc}(a SC6_mAaaylYV*Yme_?CLi5Sks}cieJhSUjmpwp{;w41Yk KtQ I\1x[؇h܆S͌`ixFnE9F;f^UHbrhޚQ̄Cτ5DŽ<< 㛅쐲[o[Xo8eOwsᚉf둖WߠnWaHi\nioa\g録*N.ᙉN &'%˞*¦G/DŽU?CY0RgQ4kh<}/EJs=P =e6C>nn|yn,;)>run4Uc _ؼdߚܖgssm"hƃכz%Ʌٔ]$sέg-gд;ڠi0 㩉ԞAǽnڬૉ߱F,ȟw&OrI[ztv""̆l no)Ǝ({P,:E'{d-;}-+9O"JXgdX0E[99F8b :zK{C7=mGB{=?)-iI9zWVÎ_i;gvBjq^rei'q)O- _C~%+6/a xo~xo~]ևc؁[̌bi4z7032h^UHvߛcr{iݠ$|ݝ҅Cօ9˅<< |gd됫]o]Yq8f}Qwlukht~YޠoYaKi^nl6c\i茫*S.}lk~N &'%џ*§G/ʅUŨD4 Up3ClZtjYrun5UhУi‘uZգprmڽkحujk f"iݺb ɩp-ᵉL8yմٺ=$ߴhOwܙI]z8xx 'цĊnu%AQ%9> ?eE}'%<`Or!(52$ >V4G"]5U>(Jf g aόe ԇ+\hs!Ҁq!mw5d1Ќ0نs=bAВc)ŖF8Me;֋?bt?vsgJLa_PEaL\EvBQLxONAgt2)V8w&kDYe=}bdECO] ?ppe7B?c6BU[Is`~:'+[k|;zJu=?_ilKxrL!c#wfQZld~^\o$^_?Up 0vyM(YR}>qQ)%!Q[{HfcEhF8$4},"^l\lɒ{4}n'dQ ;~E"bvvctTzKz{?{A{~Ub{~Z{Q~I\Xdbn3x5N'ZC{*%&ƹMvogcL/ scΐ`ȏů.ŨL I|'Զ[$ͲSmY"R qS[L.\4,-,ҠCJ6+%MņGas4!y"4]7&lrtBM0srl^mD~ܗ:,:\pZ.sMY}{AhKH=>2mDz^\Ɏ{xm̀[iWynztX#\!L FH1(Xs*6p v{~e~Vڇ[܁TьZi{4q7032_^{UH~{⛐[ra࠭${C{5{<{<{ {{o{k{퐴VoUXg8^Jwtlr_tRXeRaDiWnco[\a댴*QƎ.trN &'%ے*̚Gʔ/{Um]=L>'tR\5?run6ܓUyǡr[˟psv˻yͫvyݣlԾ!Є^"bճ߫b2d ۲ܰHʌ򲂚˶ͽ.믄 ާkדNЙG&Yh6x +ˇ noʐJ$97 KfN;"%5kOz'10$1Ba4B"d4\=(Ro1Zc8eϋ&^ iςw4E2:+օ/ׄ-|tO9SD<;>e~2qzi5Fg_9DgEt=Jh2z*XAO)DFZ=y=q&GVj G^kPP׀}It%Q[ LQCV0AqcB& kYtI}[ZW)aS$nQ{g\_F\?aD'4`O!w[|zT{~3ka'hiJSeTtfowgznn{hlv?n>nwT{b~shd3B,XW)O WbfC>Iv]AI#&}MtnrQiÓ,-un˒k/( &u R, J'~{EA'|,4L/׊[5,֌D3ٔ2Gu?pҝ ""ׁ"8Җ,y{k|y~rćw:wVppqiFxzhe~=>z mx%stwtpǒmpmpRht:szf}m|c+7x^מM|}g~WـS׊RىX:SZ~3u4/) X`sUHsTqZޠ$ޝsCs5s4WunZVattack1@śSԝ&ӧ)Ǜ-*ԧ*`|<_d=Ĭ;tot ˢ1x;`};L1lZ;C9~Ս>¼HΘ&yJB Y!1hF<03+.ȉ y&P]M4mFE1\ [k N.}V#5%9'E(8.L+ ziJK+~3 7.~D+Ck(% 7k4q4(ʂ*=X )v#C~KFƒK+aP*l<9Љ.Y%l^s.Ɋz2bT9ΈZ8Z=HptD3DwB:bBI Z3zU=+c)gi3GMT]e*>c x9V{H^8.5F-ĴCL!V41 |s򲞮:? =w,ԾS.ԽH) H ?*ˮS0 8F=mI>Þ TVattack2@ƞT,ϫ5  00l:g[4ۨ)lžˆij=`t6~A gZ-DF̊FŌA+u_&RIzbc%H&+Cȉ3 ;x,eFM*%hDN͉S;qCJntN!EwN(~Az6{(>iF?;sfoh[ ;x4DFA'7ZhB N/<F|vCitHbU *Qz,OFRoxad3tPC=^r7,_a(YkGYxNUɌQpX}cps`PWmoKd8irO7,ZV>]U.<ʽF ͯA)I'7< ":F Cw*ҬT.ӭJ)~'I$мB*O1 8|E:mTo 25ڇ;Eش<IL{7q %Ǐ%ўbԾ.җ$ҨI!lMbgزgpn oMgt!Ewma{k6fdϠ z!cę"YϾpcrzIr9_|fZ?oҙXG˙h rZPZ]ՁRugK͈qSsbo[nχo8Ԇ~R\p`VYߕZkۖSyݛSvhNdDb8k:tz i]OLqNmoOX\n^mK[tLFOdߚ~^lKqd:hrOlGnF[Pl3O5vr X\pM I+.5K5h`h`zDEh0\t RntG8em=>c=٢H>鎳Vattack3@ӂ@ʰ*ϋ;,ͦO 1ƹ1ZŇXQG9ӭQhPr~1`$*wCD8o7ܡFڹ.^f$J~RuLz"u&(o‰a} gx)#CI~O(Sh?zF| wVz|^w@KqSJC4WmCHG5coqu ;RAFK'SjA Y/W_~Xm]p_ *],aJͳhnwb?lA=l5,ta(plGqxNm֌epm~ct`RhɒtKmUiyi78dVIw_0 8 ƶe.#>(8 Gϋ7 7w*S.G) 'I$?*׫J2 :7:Qo 258E<IŪ)Ύ5q %ԏ%ֳb)ݝ$ީ(!laozon ςLg!Fwy||8zdѵ z!sі"_crHr:_gZ?oԮXGͯhylXIPӁI]uςe~w{ˊr}ϊӉχքp|\TSQڝPءIP~RmG`NE䌅xqXOF۝B`}mbzYoM~=tBP;CWh^xqUlz_jpnvo[r}l5Vq=G> &ybVattack4@·Uנ(ї5'ҙN ۹/ҫ0T;NWG9ϨjƉß s%^t%~1 zIE.vFҭ>ǚ=_D2'}RH'&zfc+S&K8x j-Ú%z"[7Ɉe6iBN̉MEqFPltP'BwO.:u=p4yEOAE<6v2g0F~:lFi_9kv>bUQ T=z_.'\AphINIbkT'>2l w?k{Mg@9_=;/?g883z$bobd ;{<>F7'9[o>!H/>GzxKm{QbM *L,N<[o}h`3wY>=^z5,b^&\gG[uNW͉Zpa{cup`EUexK]CilY7"SV5fM.<7 űD)!K(7; {:F Fu*ݳS.޴H){'I$A*ըV2 :E:vTo 259Fּ<I+́6g %ь%Ԩb)ؘ$|ا(!gMWaޮrqn uMgv!Ewocxt6qd̨ z"uaɔ"Q׸wcqIr:\cPZ?oСXGʣh v eNCMnӁE\Y͈dcs}oouhnЅ|ˌԆrb\~_UK۝L{ءDٚ}ViG[NtEu8}:{sUJ>ܝ:aama_YrrK~9nt;P4>v۠kv^\qUNh_djXnyV[vcl.Slmattack5@U(č5&Q-ӹ..M;~IPF5 j e$fxÒ#0yE7(s?>L_CY T)Dh JDDD6#x"zUt2G8#m5R v"Z{& W:('J<52H';8<#[< iBZ%K C> 5M<4{>(' <[*%k,&Z!3ǃ*IX3u LŐTNC8i>6l(Fρev dc9Ɗk=bAEΉJCg2MluC0Aw?88r9aՄtz0SA2D5wfPh/,:lNbQ;kp&=b>S J>zT>'\&qi0Q<]u\c(>m x'`{9pA$c'w{*t#i*$r3aoMe ;oP??'Ev?"-/%|rUpnXd5 *5,3'k6lsa)fh?+I5$J_&BiG:v5Šhdzl|f_rU19QxKHFiX[7;Vg50<7-B> ݬ@H41 lc}O9? ð+v%T/J*ur{ I B*|ƭT1 9Fw]V͊gbhv6wmo;8Ӆwd\_JAݕIpڡ>{ۚv&^NN~D~9:~;u PB2ߙ-e^ne]XxrJu7^v2P,;gݠosbYZ[Ihe`lan[\}en{)zO0YS";Q;SN{ yIz,y/.H.UֈcEE`1+^s(MonV7|m=z=SJ>Dob Sattack6@{YGɒ=Mxt#۳+Ʀ)G|5AG31tt 1g^|#4 jEF q>JIY@R T Ah:4>2#&āxzGqM:m=B t Zx"H:J1006'B&:;Y* pGK*; C, %N*Fv+! 5 [1k1Z*!Ȃ-8X!u;~E =I'aG%l14ρ#VgWk)NJr-bJ4ΉR3g7?muD%BwA.9rASՄh|1JB395wX`0,z:s?bW*krH R.zU4'c&fi0FDO\\(>b x/R{9f@%Y.k{1gx(~m1*u%`6M[ ;pH>?'!FpA"-/%+}yGpuKd5 *5,9.^6pka)gb?+I}5$NaGlAx;\d_}fesU7?QoKG9iWP7;V]505ӾF-LL ޭUG31 rh}N9? +v,ҽT/ӼI*{x| I A*~ĮO1 9FZrOattack7@ȈTМҡ)t#,Ӧ*T|5NW31tt ȥ1v*^}1D lS-C->ĭJŖIg@ER7h; 550'%Ɖxz GfM8 mB?1kZo !H5J-0*5'D#6 S& pIH*~6>* |!K'Cm& )4k3Z/ ɂ,7X!v;~D>’K%aL$l73Ё(T fWo(Ɋv+bO3ωW0g<>ntD(CwA0:wFRք&f~1MC3;6wXb0,}:xG X+zT5(c&ei0EJL\^)>b x6N{9gA%Y5g{7dx{/~o50x$^6L[ ;pK>?'*FsB"-/-2Cp|Gd4 *4,A3[6tha=gdA+H6$Sc'NnIyCXd[~emtU<GPoKG8iWO7;V]500F-´CL!V41 wn񱞪:? ?w,ֺS.ֹH*~ I A*ȭS1 9F<T6 2.;FѲ<I(ǁ8g %͏%Тd)tј$qѨ$"{lNUϲwrn yOgbߋ"FwZكN|v7oEƛ z"uTEsfsKr;t`shPZ?oʓXGėmt aKDWU։JhiHϊtUsi8_ˌыx8ՆY\|UVLVauKmޚSgMXݝvDv9z:~; `R>:uOprOWd[eGLv>;pJWࠁdpKZk8iwQnVoP\Yn)O0h]"FH\N I,/.H.xU{aCEa)-^vJd{D7mmY=@=!M>o+OVattack8@ëTй)ؙ**r|Ǹ5pv=̪;.t+nuŅǴ1ˊL`}N^2mmMCKԟ>΍H&ԊJ:R/h?7..$*ljx yK[M3m?B1^[k J/~~V-.%5'A&0M#-~bFI+w3"9* yG'Ce" 1k/q,%ʂ(9X%v?~GCÒG'aI'l47Ё%XhZl,͊v-bL8ЉW1g>A [)zS5*c&_i1>LK[]*>\ x;I{:bA%T8c{8cs}3{r21y#^6MU ;oL??'0DtC",%27?pAd5 *4,D2[8wdb=feBLF6$Ve'RpNzITdUeryvU:KQgKH3iXI7;}VV5z05ٽF-ʲA(!H41 wn}7fm>='@>/ocX7 pain101@V$`RϠq[v!!R鬗ʾx Wս|]֪%rե[ܸ 뙪s:Ψgf˳糣@bûx뻲ȷ˷ Ȳů+ܨv(I؟Ku YFhk%9J1Sl/&"2}5wEzIOK'ƈ/QqAI\& U(.\;'9<΄f'-pAY!.i6!\?0""Q^q1+Έ;*p#4΃BAa&(s;*<]5DEn7FՉL~\ƒ]HSXۅbSn1V؄MFmB7ptD'AwA/8w"l2Z4EC485w6QXX1.m;gXo]zk8b9E X2yO4%\$^h-CFNsVU&>[ x>D{5^J#S9Wt3[rl{w9lr%v{u \dFU ;kC;?'5rFcA!+}/8{=~wMn}LGo5$Xnd'VupGRvNI`Lesfw_DIdKA9iOL76wHW1t0f-q.Ysٰ?c'OcKYMl0c N;<ܫTNo2~5W uô3z$ѨX/ӪM*A} M!͵E%gҢU4";51zW8 2.=Fֱ<Ip(8g #$ӡdѺ*h XH"|m:Nlt sPird"Gw`Wzy<\8Π h"rN&dfqwKq;ayhPZ=6қ]G˚mH8,"ˎ0v$~ҐFo-v@`:YՋY2^;P 7rcRfVC SX`O*?I3HWwoJ~<%  S |y)s^јTΙ Y"tqnz>ij=ea)zC1SA0 6'#V :K(P=N/62Y1`?F XyDsdFF[))^qGd~88[nM>%=w%E>_hpbٖpain102@ԍV%hP—ri$#[Ȩxߺ2cӾ~˨X޹*vԣӰ xwȡrv֮ └@mx隣骨l׬=֋{ї+I’KcY/X k;20NY,&!1,x2zKOF l4MZ 4HR U).L7'=6ŅWs%-pDT%/[1 [65!<L 2%l8$b%1Ƀ9=\ (s>AX;H:p=<͈%H]ȐbO[L҇gIp:N҄NBi<9ktD%=wA,5w,g*]{3D?463w+SWW/.l9kRpY$}km6b8G U2zP2"\#`,EDOrWT$>\ x7H{4_ J"T3]{0_m{x2olŎ$||s![6EW ;lB9?',wFc=!*//4yuLnwBd/{ *2v,: s:qZ^=cW;LGo3$Sr_'O{iGJsNB_oR~eknq_ >IgKA:iON75zHX/w0y23k{׭Cq,[iHb{5t2^IFYN}14g|3v%ϱW/ѳL*Pvz L"˾E%iӢV4":41W8 2.=Fּ<Gr(̆7g #}$өd*d ZßG"wg)<ts zOir]"EwX}Nuy9c6Ω h"rJ*lcg}Jq:u\tcPZ?6Ҥ]GˤmXH8*'̎;oՄ-{~PeӉAozl:dϋh2k<_2NmceffC)Q)ѡ"R\+I&:Opy6e}WA7Q2qk=k]8{ؖR ֛X.7ig(`z*vj[elUa\ErjT0`JJ1/= )S&AHB]+[*K2q1s>\1n{B{cFF\'&^rFdw>7bl9=L=K>jiW`e|͵L܋I&ТJKRAk<1>+%,ā#xzJiN=m=F mZw &G?{V,)53'C,A\#2pHL*1H+ S*Fs! 14k5g,)Ȃ09[%w>~C>K-iH/l1<ρ"YeUh8ˊq9bI?ωT8g:08w=[&a}0JA1:4w"UP]/*v9sGbY%}hm :b;F V/zQ6&c$bi.DGNrX['>^ w4K{7cA#V2b{3br}.vn+*t%\6IX ;kI+Ex4$P{a'KlFu?\6W}ehwrU0AMiKD9iSN78~HZ2{00?-ѭA(w"{Hy41} c[l󭞘Iƫxq {Oi\χ"EwVKxw7k6ʢ z"uM8pdqJq:^qdPZ?oΜ]GǞmt bQ=9ѐNaՉ?qaUχ``h~w8wnˌ19{1obcBA=ؙDqס:|Wr+[&LNxEz:;mbB7/ٙ)Y^m^\Yer;y/au/P&6iX\tYYZPKhUbl]nyX\odr.vVr0NO9Q7QOitLs,m/5K0z_~aEE]++^r HdwB7hlh'=S߫=UM>mOVpain104@ŭTѼ)ݘ**s|Ƕ5qw=Ϊ;.t+ouŅȸ1ˍM`}N_2mnMCLա>ьH&Պ@:R/h<6--(&ȉy z"GZM5 mC=1^[i H0}~V-.%4'C"80 M#2}bHF+v3"9( xG%Ce" 5k2q1 ʂ)7X!v:~D=ŒJ$aM#l91Љ+R"dVq&Ɋx*bQ1ψX0Z:AotB-Dw?5;wIOք&h/QD1?7wWd0*:zb?E X,zT8*c(ci2CJM[`+>` x5O{;fA&X4g{7dw{.q81w'_6NX ;nOA?')DwC"-%,2Dp|Gd6 *5,A5Z6tic=egB+F6$Re'MpIzCXd[fmu`>HRlKI6iYM7<VZ6~.5ؽ?2˱A(!H4< |q:F ?w,ӸS.ӸH) H ?*ӭO0 8F<~S6 2.ޑ;FѮ<I(À8g %ː%Οp)uϙ$sΪ&"zmQV̳vqn yNgbݍ"Fw[ׅN}u7nEƘ z"uTGtfsKr:tbqiPZ?oʐXGÓm x dNG|ZSւMfmFΊwQhf8]oЋu8ՅU\wUVOX^ܡNkޚSl[ݝqDq9u:z;} cTA=wLptLX`]bIKvB}PG=J>Uk pain201@ØU'š5 ۩N /ٺ0];~[PR5ѶmӉv0_t(~|4 hL"F4Ƀ?Ž>թ+nB:7wTf%8sz 1e(W KvVv{T8G g1+r_(niIpzM`]phey_8[јiKaOiof7'WH9uQ0;7 C)K'8مqp"֋7݆4~.лS5ϾI3l$I%A3۷I0'87:{Op 2<̖;d;I°)8r %ќ/ʯi3#zޱ+${G^smm MrHu|"vtoqp͵ s"fכ"I߼kL9mvQZ?oѭXGƪhk `R=07h̀0~GUɂ/uTigm`amt׋nsnoۆ4bTOUUdbDH.ԙ2Oh{w2RW-VvdsomwJN^U p.UI[$lKMinyFv|8^s^>nhRDZVPF`vZp͇Ccm/.\|$Nm~XorhbH>!j=_L>*NZVXpain202@”U'ƣ5 ٢Q /۲/Z;XPR5αmΆ w-_t(~5 hL#F0ɂ?ü>ա+lB:|TI'z cc1K(C 3}:vgU$G$&h2Q V[Yk'$.uJ)0$U,/5")A6 ni;W.i9 /Bd"79EZ-x4~#(|X )z!,[8"UYBt[h c75a52k#HÀo{P*j],a7?łH2k xDlOk@;`?9~P0Ki-53z!_oac ;|D>F7'?_|> G/GRzvDm|JpM *M,P5Rng`?z`>=`2,k_(giIgxM[ϊRpZew_:WdwK\?ikW7#RH5eM0<7 ƺC)K'7 tp"ҋ7 4{*ҭS5үI3q$H$оA)ײH0'8}7:kQd 2<҄;d֯;I+8r %ɗ%͝iҽ3ם#y֬( MuJ]ݱenm wMq|!Huv"nl8fd͟ s"cə"IԺzhrK9gqZ?oї]GƖhq eVB5?^}6w~OLy=^Ҡqbng[mzՋupΌu HP]bS`tnBGM4՛4Ns\5^֠&Zu2\xP1zI>XEGxs8xVch\ o%PGV kIaty?5o:xLߌ|aCpc:fE`f\nwDei#-\zLp~Gofm>T=SM>8>Rpain203@T'Ǯ.ٜQ */d|<ab3ͭ;tnɆ :`v9I m[6C<ˌ?Kښ(vB< T0*z SL08&8%}'x[VG)m7J1Y[_ Y'' vJ*.#C(5-&D, si>O)n4 14k :/D]&$( k'"q$0ă$GX4tOX T<.a=.j+Aʁ dwlZ,ʈe-i?=ˉM7ZFGmtK,CwJ3:u7]6t|>SC>?7vh*g0<8l=i^'~zr)b x?X{Ge@4Y?.'8SvA =/=F|yAm}CpD *D,J4Wo{ca3p`?LU4,`b']mG\wNRRoUex}v_:R\oKT9ibP7JH,]D0G@wGOpain204@ʤTһ)ܘ%*r|ʷ5ou3Ϫ1.t*nuŅǽ ʐL`xM^2mmLCJ؛>ʍK&؄@CY0kB52*&(+ȉ x"L^M3 mCA1aZm I-V,.%2&B$- L# bHE+y2"7({C%Ch"'[4k1q0%˂)8X%vA~GCĒI%aL'l77Ё)W"gZo,͊x-bO7ЉZ1gBH{?dA*W;b{ëj6=pain301@ƐN!ĥ)䭓 //ԐNJ}ʱ9҉Ő䏎=Ļȴ:QtIbΌ϶;ΔjrSwfr/{N'{3g('~ B3I ? j+zw;) ]9? aY$:^0yXq"k.B!6k{EsMA3,A$Td6S0 }+v^Z6)FA/+N(An8 *p.$.WKgD XJk71P3u9Á)-+|Z^e iU jE%ˁ9=-6p$^lll,];puur eDqPl/c&i1pZPX.> w1I, !?< À9?ً=݀*T3K3ᆎ$I$C.0'6?8ĢRd 23¶?6;I,:s %֠/m.Э  M~hfnp OqtHugUFC s"_ZnrN9srl|Z?oʹXGhs } }|eh`G~v&kg3Ӂ%ms%PLE>dg9T6k<T*UAKreaC؛_RNU)i,tfp n nx:qNisfpmsn#Qh`)~J9^dRXSE۝{iӠZrl zgWhD7UI8Nz%zz|z&!U=Y59:8:>'8!U\oĬGe\a4^pVNd}v7h=} =$J>«j+y=pain302@ƋS ͠)"󣕵 //ːȇ}ƻ9́NJ勏=ճ®;Dt;dǦʋĭ;͛fTx^m1i{bA`|ֵ>I'ԡJA}S 2 g .=2= 7%xz23 ^);iR+;_}Xql(D 3l}0vO63);'O63Q( |cT9)~==)"K#Am-n$XDi=[Bj21W, u/!ƀ!4"(|U\]mKLj:40L%Cn#cblf+\>`ouDOCwAX;wY0Љ(2vD3c8w'N1-:;cbGhqB=b>g c8sTZ*c&i1eVO])> x;gv;A'|=q{Cb{/rFm<|zH_6M| ;qp??'.HC",%07ȊSb~be5 *5,IAY6vc=iBLJ5,Te'MqLӅMɛhe}yfmwSLOQKHYiXp7;V~505?2͸K,G ?< z9?;={*S.I)$I$A*ַM1 9?:ڒR6 2.֫;E<I­,8s %ݘ%ʾp){ܣ#{ϳ u]_ļ‰pn NgnHwcSFDż z"u[RhrLr:~gsqZ?oʵXGh rZU\~k5j\GԀ}/lq2џXdKd|9c7<-`DUcKb|ZPؘT_۝y)'z1e%o+n.4mXbiWSl"hm/RmZ>[n7[=pain303@ǯTœ)噙 %*x|ʶ5w{3Ҫ;2t-ouŅ ʓS`xQa1mpRAP|֣>ՋH&Ս@BT0h851-,É x x'A^M5iG51`[n!D/j~V+.%2'F8. M! bK=+{2"8%}E!Ci" %9 k5 q5ł+2X!v#2~=6’MaRl=(̉.I%\Ou!n}'bU*ˈ^,ZAEltI/?wF78wKGӄ+k}7TA8C5wRi/299bb,}hv#:bAO _+zX<&\(oi2MQI`d'>k x;Q{LN4$Wa'RlNzJYd`zfqrU?LSvKIAiYW7=He705ٽ?2αA(G4< {q~:F ?w*۹R.۹G) H!?*֭O1 :F<~R6 2.9FѸ<I(Ʉ6g %̐%Чb)xЙ!uͩ&"lQXȲvpn zMgiFwa܅S~w7qEƣ z"uXHweqJr;t`xgPZ?oʜXGßh w dNHt\KӂN^Ԁp?̈nHsc8YomkDRInSX>KtAQ8rDWWxKg>[^-hl=k:p<]Al.SyE@wGO=pain304@ʤTһ)ܘ%*r|ʷ5ou3Ϫ1.t*ouŅƽ ʐL`xM^2mmLCJכ>ʍKIׄ@CT0kB52*&(+ȉ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'4k1q0%ʂ)8X%vA~GCĒI%aL'l77Ё)W"gZn,͊x-bO7ЉZ1gB7wXb0/~9{_ x>H{?eA+W;b{;bu{6zr54|#^6RX ;rM??#'3~HuC"2/6;?pAd: *9,G6Z8ydb=ieB+K6$Ye'UqQzKTdTeuxvU=NVkKM6i\M7 BH!Z:}.0ڽ?-ͱA(!H41 yp~:F ?w,׳R.׳G) H!?*ԬO0 9F AȝWAjump14@ӑV ܦ.ʚ --{ŽЦ{ؼ<ǁӧW.ޭձ turoդȇױ ˷卓UwƯ p䠥>׉{ɛ=I'߸A7|2 TƐ,&!N%T 6ɉ$e"P728e%ShgM2);hY/L3O?P<4% I-,qp;`Ii167$u-DZxHASwBnք 8XU;HOvZ%bG7yNfiaAomt4]p>L k=ds\\x}C`'D.V*s{%o.{yn3# }.hV}:MZ 9tVY3'OsQ 5,* ǀcvcYc:x*?t2,)+Ecr)mhV=SxI,ChG5oO)~N#ҒvffBLzU80IoI=H\NXD 8} M^Bp3Ӌ,Ҷ%ղ{,~ i'y$˺v,OnsCl ␏:IJ80Ř+ޛlCJ42Gt̨(LJlgE%\%PcR+[ޠ!_ذV!D{sVLHһW8DŽkvBuA"vVx</{ChBBz]v<ˤ<s~su}t]qqY/ɴ,>Þ+erE6W9Im s_HCSoԂH}bflqmgE{z6:7Ӆp\UVKSwrIޚ}OfWND9:; ^P<9rhpohXxXzDgv=8Gp}xmeZfXhsklmoh\pn)S0f\"EG[N I,/.H.UŘtEh4Hcr?l[9hzhr=F#=%8>'F.1Ajump24@ӄV ܝ.ʎ --}軏Ѧ{ض<ԧW.ޤթ;tro՚‡ר ˷升Uw򊖁Ƨ p堛>؉{ɏ=I'߸A:|6T,)!N)U #6É$e"SQ:2;e%VhkMN5) kzY/L#3MCP<7% L-$tp>`Im197!w-G7#D+0*z{&I+|J`P9&V(b[;M* :uE\4e&T^L&2W%*xr^}\T^8I*=N-()/Eal=pUU>U]C(D\H4_O%rQوq_axfHmU=/ŞM;ICaS%F]9KO- ~,r%h, bu'c a,OQ"ZCkŖٞ:0ˋ+[F'5 GsϠ(ʔWg!%W%Ȭ5d6*Y!_ک'4!rcORJԷ2ޥuk;}"_4x7t*o|Be@z?u;ϝAvsksZt{bXhaRϝ >bWå'm weMHXmςM{ӁeenvkqE|zd87͋n]UJPܕXuءN~ښOl\ND9:; cUCߕ>wfpsfWv]xJevCޚ>MnܠvqcZlVhwhlkof\nn)S5ka"KޝL`N I,/.H.Uf7dOim;uV.ez>=bU>2U!@jump34@ԒV ݧ.˛ %%Îө}۾<ԫ)ᯘֲ zuxo֧ɇײ Ϲ䒒Uw򏟁ȱ p礤>ٍ|˛=I'޼JG.|B#b9<'O8c1F̄%3Á.s`QG=ƊLo%bzt -WND2;wY%>6$(WCDo$;"v2 zGTЊDMkB`҆8Tq0Yσ.n#bt"hʐVXiW^ɊEgӉ90!tÍt]nvdaYeψYkZ5t{?4r}=3l{Tyׄ 7Mk4@Ekzy?gLd>Q^[(OoHErO>ԂQ9ٖeuf^~`LDɧ7=*A +g3&*d }2.U-VJ0-R?F IC–9ʤb6 0͛3ůJE9Gӫ(Hs%v%*p*+k!o߲) mu^Zھ'~ů ]hUT)xOBDDz3uPҧPtsYrKtqmdsP>ɮe W m oZUdyւZفqrl~xqEo:7ԇz]_J\dݡ[ߚRugND9:; naPLtp|tWhWsvQLY{᠊zqZvchulxot\zn)S5ul"XYkN I,/3H.Uo˩QER_gh3k4m6 >=qh$>ƫjJ=jump44@tT̎.ᇙú %/ⴎ՛~}ͮ<Ǿ֜v~)ҜƟ;lvuk}oŐȽǝ өrUx􂀁 m薈y>y|޻}L{I'߭wJQX'+|N/['9H)OCY<>2/:k"hQS8Wg/is}:TMP/~oY#y>C;GZR v&}*S3|?$WYCccdA;d_6?'H5$+#w$mix]`~`, %0*?MebU=_r3>J1%GLG=TO6iN1yvUlq_UcU\GǟDK;riJ72V,<շ? ϥ@+HF: }7>:ȡ>t,^%R+ kn'Q!K,מO<"IAx̏Ԩk: 2Ҁ(ѳ?C5Iҙ(>g %e%c+i#rڧ$!uXOpaѸƳ _hKxMxGo;hx@qV z)uPѝ\hsVsHv}UXnURZ?ЮdXGm цԆkermjiyρ{gnkqe}d8E:m]`Vlؙstӡk|֚H&vRD9:; |p_ܕ]fpfXuwvfev`P\hmؠucZVhglioe\mn)S5{"fڝgzN I,/.H.UzڬKENJ\dLOnay4q}z1r==i6>īj=jump54@|SΓ)厙%*﷎є}Ҳ<ї~위)آȥ;`~t_dƘuȢ1Ѧ|zTxy1m菍}>w|ںLI'۪~JFW'{Bj"W:=,M7P04%(.f"`NH0Mc-a}sv.UME*w nR({=8 v)}0 V|2{;&^G9kRaE ;iY8?'G|8!-- {'rrychc0 *3,>@dhX=co6LJ2%ISGA\G;nN6܄fttf\hUMDǛIK@giPy76V0 <޹?2ժ@+HýF< v9C;ʨ>s,X/L+qr'L!E,ܤO7"CCÌ1_8 2͈+ܰ;F5Iҝ(Ş:g %p%b+l!qަ'"w]N_[س{ձ ƘVhQ~HxKu@nB}@ z%uPњQesOsBvWWoZR̥Z?oXGm p[VenςZ}sdllsEzo:7΅o]UV]ܕewء\ښOvhND9:; obPߕMgp~gXxizWfvQޚMZoܠx|dZwUhhllog\on)S5vn"XݝYlN I,/.H.UϡnFEVA"^k=Lniz7|~zF=8=4M>E@wGO=jump64@ʤTһ)ܘ%*r|ʷ5ou3Ϫ1.t*ouŅƽ ʐL`xM^2mmLCJכ>ʍKIׄ@CT0kB52*&(+ȉ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'4k1q0%ʂ)8X%vA~GCĒI%aL'l77Ё)W"gZn,͊x-bO7ЉZ1gB7wXb0/~9{_ x>H{?eA+W;b{;bu{6zr54|#^6RX ;rM??#'3~HuC"2/6;?pAd: *9,G6Z8ydb=ieB+K6$Ye'UqQzKTdTeuxvU=NVkKM6i\M7 BH!Z:}.0ڽ?-ͱA(!H41 yp~:F ?w,׳R.׳G) H!?*ԬO0 9FE@wGO=flip01@ʤTһ)ܘ%*r|ʷ5ou3Ϫ1.t*ouŅƽ ʐL`xM^2mmLCJכ>ʍKIׄ@CT0kB52*&(+ȉ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'4k1q0%ʂ)8X%vA~GCĒI%aL'l77Ё)W"gZn,͊x-bO7ЉZ1gB7wXb0/~9{_ x>H{?eA+W;b{;bu{6zr54|#^6RX ;rM??#'3~HuC"2/6;?pAd: *9,G6Z8ydb=ieB+K6$Ye'UqQzKTdTeuxvU=NVkKM6i\M7 BH!Z:}.0ڽ?-ͱA(!H41 yp~:F ?w,׳R.׳G) H!?*ԬO0 9Fǫj` u x7k{5{A"oq:x{Bq{/rH;pG^6Fn ;ei??&,>C"(%/4`bwce0 */,C>Ŏx6mb=^BLA6$Oe'LpIzCŎuEufivUHHJ~KAQiPd76Vp0004ҶC("J41 rh}󲞪9? ɲ?w,R.G+z H ?+ѮT0 9C<ԐS6۷ 2.Ρ:F<Iʦ(8g %֐%b)oٛ!nլ!"unWU̷qn֠ Ng`"FwYM~FE z#uSJgsKr;tzbmk͓Z?oXGk y hXW}~o_Ԉ`lׁZˊ`q7u6<7;c^_J]dmr[wޚ)whݝ|D|9:; oaPM\p~\WnipWZvQMzZcߠn}XZxIh]l`o[\cn)S5wn"XYmN I,/.H.~Ǔ_ԙBEXK/^lBKdw`7mI>o=*3H>ūjUj)wWӇ++{F-k:wK]2&:icXYhaT?b5g eXhIc,c!i*f_iuO+>~ x9{}2B!zqD}Pu{1uc{LlY`6Bx ;axA?&/<̛T6μ 2Θ)ũ:F<Iҩ 9s %ߓ%b)oߡ!pز! spk\snʧ Pg_HxYNCB z$uVŤYksMr=twdlmZڹ?աoXGm {otw؀bl~iւdocqFE540f^UHuߔ|ortxݚ,ݝ}D}9:; yhe_p_Xprp^vie{rfߠp[ZMh`lco^\fn)S0"pQqN I,/ę.H.UŘaˢCET[1^gTLdur7m>%y=QS>ǫj)jflip04@eުO,߬SȪ\w'ܲ{#Ői*ƴsxĻz8Ƀɐ"φ.)ε?٭K4I:APzdTATQ=&v:^/I5wV%cyh#hC9]wux ~bW&^(lnfQnCXFI-o]$o)5qngF>$w$i40} u-^y}%sn32:'&ix*-lgHq+xw9bGi<>^>d;~JÉmVgQ~WE`_JV]?smv>aDw;g;wU‡1~-C/o8w`[0'7n^^]{heX;b7h r[hLf*c"i+gnirR)>~ xA~}4@"{qN}^r{6p{t\r]ZdDy ;d|=?&5?A"'%7;}wc}wf. */'SfFp^=]>L?3'Tb'RmPvKЊf{_pr`sVGK>]iMp74Vz/.0ֹ4%>&;zٸN̳4ŤI1ը(r,N.C+ { D ;+ӫ@- 8K<ԛO6Ի 2э*̩6F<Iҟ 6s %%b=wޙ!{թ! {i~i¶mnϧ KgfDx`TzAB z"u_ÝkdsIr:t~_rgƛZ߸?ڡoXGm rɁbfƇhob[FF.?0e`ǀUƠHϛmrv͚,Q|D{9~:Ă: }ҕ{^p^Wo猗p逄]v~КzzّeϠoZZLh_nbo]\en)Sǘ5"НN I,/ڗ.ТI͝.}U͙[Ӣ>eW_/^lXHd|u}7lU>u=)N>ƫjƐ6flip05@e߫O,߯Sȫ\w'ܵ{#ɐg*ƶsvĽz8̃ː"х5=г>۬H4C9CLzcT?VM?&t:\0G6uV%[{e#hB9Vyup `X&Z)lcgQnCTHI}-oY%i)5goqE>#x$e50~1q-^r%sp056)~&it*,leIo*xv8aGh;>]=d:}IƉlUfPWEa_IV]~ x<}2B!{sK}\rt{3rzrZn]]6By ;a|>>&2=B"&*48ŀxcxyf- *-'PeŐCl`=[AL=4'Pd'NoLyGҍ_~_ku`sTEK<]iKp72Vz-.0 ֻ?%>&;͉wټN̵4ŦI;ի(u,P.E+ {}'F <+Ӯ@. 9K<͜Q6ͻ 2ҏ*Ū8F<IӢ 7s %%b+sߝ!w֭ vl~g»pnɧ NgbFx[P|A@ z#u\áifsJr;tyaniZٹ?ԡoXGk цṕaneȈgoaZFF.?0d]UßHқlruК,SzDz9}:; |Օy]p\Xm쌔o[v}ӚyxܑdҠnYZKh^lao\\cn)Sė5"ӝN I,/ז.̡Hɜ.|Uƛ^̣AeT^0^hXJdww7l>I=>aR>ūj|`ͼflip06@eߪO,ݬSɪ\zڲt#Ɛi*Ƶswżz8ʃɐ"φ5)Ҵ>ܬK4C9DLzdT?VM?&u9]0G7wU%\ze#hA9Vwup ~aX&Z)ldfQmCUHI~,pY%i)5hnqE>#v$e50}1q-^r}%rp056*%iu*+neIp*xw8bGi;>]=d:~H‰mThPWE`_IV]=tn{~ x<}3A"zqK}]rt{3p|q[n][dBx ;b|=?&2=A"&*48~xcxxf- *.'QfCm_=[?L>3'Pb'NmLwGҊ_|_ks`tTFK=]iKo73Vz-.0ֹ3%L';ɄyظN˲4ģI;ը(s,O.D+ |{ E ;+ӫ@- 8K<ΜP6ϻ 2ҍ*ƪ7F<IҠ 7s %%b=tޚ!yժ! vihnnʦ LhbEx\QzA@ z"u]ÞkesIr:tz`ng›Zڸ?աdXGk ͆oɁandćgo`ZCF.?0c]~UŞHΛkru̚,GRzDy9|:Á: ~ѕ{\p\Xm猖n耄[v~ϚzxؑcΠm퇨XZJh]l`o[\cn)SƖ5"ϝN I,/ٔ.ϠI̛.{Uǚ\͢?eU^/^iXIdxv~7lFq>'*=T>īj03Xflip07@EߪV,ܫSɪ\zٰt#Îi*ƴs߰wǼzӐ8ȃȒ"Ά0)Ե?ݭK4G9CNzeT?VO>&v9]/H6xU%`yf#hA9[vut ~aW&[)kfQnCVGI,p[%m)5nngE>$w$f5/} r-^v}%sn148(%iv*+lfHq*xx7cFk;>]=d9HnTiPWE`_IU]>smv=`Cw:f;wS/},~C.n7wa[/'7n^\]{hdW;b6g o[hKe*c!i*flirQ(>} x=}3@"zqL}^rt{4o}qɁ\p\Z6Cx ;c{=?&3>?"'%59|xczxf. *.'RgCn]=\>L>3'Ra'PlMvHщ_{_mq`uUFK=\iLo73Vy.00ָ3%L'Džǁz׶Nʰ4ĢL;Ԧ(r,N.C+ }z'D ;+Ԫ@- 7K<ћO6Ѻ 2ы*ɩ6F<IҞ 6s %%b+uޘ!zԨ! xhiln̦ KgdDx]RyA@ z"u^œldsHr:t|_pfĚZݸ?ؠoXGk ˆoǁ`dÇfo`[CF.?0c`~UƞV̛krtʚ,QzDy9|:ŀ: ϕ|\p\Wl䌗n怅[v͚{x֑c̠m釩XZJh]l`o[\cn)SȖ."QNš I,/۔.РH͛.{Uʚ[Ϣ>eV].^kWHdzv|7l>=S>ëjVXflip08@EV,ެSɪ\w'۲t#Őg*ƴsvŻzӐ8ɃȐ"φ5)ҵ>ܭK4L:AQzdSATR=&v9].H5wU%fzg"hB9bx{ aW&](thQrCWF&,o\$p).wpgE>%z$h5/ t-^|#wn31<%%iw)+lgHq*xw8bFi;>]=d:~I‰mThP~VE`_IU]?smv>`Dw:f;wT0~-~C.n8w`[0'7n^^\{hdW;b6g qZhKe*c!i*emirQ)>} x?~}3@"zqM}^r{5p{s\q\ZdCx ;c{=?&5>A"'%7;}wc|wf. *.'SfFo^=]>L?3'Sa'QmOvJЊB{_or`tVGK>\iLn74Vy..0ֹ3%>&ɅɁzطN˲4ĤL;էLr,N.C+ z D ;+ӫ@- 8K<қO6Һ 2э*ʩ6F<Iҟ 6s %%b+vޙ!zթ! ziimnͦ LheDx_TyAB z"u_kdsIr:t}_qfŚZ޸?ؠoXGk ͆pɁaneňgoa[FF.?0d`UşHΛlru͚,RzDz9}:ā: ~ѕ{]p\Wm猖o逄[v~КzxؑdΠn퇨YZKh^lao\\cn)SǗ5"ϝN I,/ږ.ϡH͜.|U˙\ѡ>eV]/^kWHd{u}7lj>^=qH>ūj&Xflip09@e߭O,Nɬ[۱w"''⼗{-g*Ǻrv z d҃ΐ!Շ4=ɳ>حIFVP99Uz^Q>NV9&q8Ƈ[+G/qT#qMg hC5n r ^T^%rS?WC&~,o]!t)0zgF>( h3- t-_ # n7)C~$iu(+ldEm)xr8ʉ]Ec;ɀ<^=d;NJxJωfUaPV6__IŒU]?rtv<`Gw9e?wUχ1,~G-m;w\Y2&:m^]ZhbW>b5g oYhJe-c!i)ekhrO+>} x?}}2B!zqL~}[r{6vtuXo[_dAw ;a{A>&6=E"&%7;Ävc{ue- *-'QaːFmc=[CL=6'Rg'QsO}JБf_nx`mTEK<[iKn72Vy-0 5 ?,K&;|tN5ůL1ֲ(y,R.F+ ~ H >+ѴU0 ;K<ϚS6ͻ 2ї)Ʃ9F<IӨ 9s %%b=tߡ!vײ!"xoxdrnȦ OgeHx^SCB z$u]ådisLr=t|dpmZظ?ӠoXGk }rցb̊f҉gobZFF.?0e]UHܛnrwښ,Q|D|9:; wߕt^p^Xoq~]vxPtz摀eܠoZZLh_lbo^\en)S5"~QN I,/ј.ǣHğ.~UȘ`͡BeT]1^hWKdzs7l>u=FKJ>ƫjɄXflip10@ݤ^ޤMȵ*쳞ʢWʤu',輏ڊ|/ށ)ȳr̾ avpߋwϾѬpc̃~#ʊ~Fs͎婴?AԡI锶DP R$,I kDJ)CI1&W3ʉE"2$yUP#lMUmnAR&xU9΄DD‘H;|o4\{5nn8͇[LӄMZGQɐIoQfCɊvL\wvSԆ-+tF-b:wDW1&:`cZOhbK>b6` fOhJ[,c!zi*^^brP}+>w x6'Pf'NrL|Gʐ~e|fkw`XNFK=TiLf73Vs..24ѻC("J4 ulN5> гLx,R.G+{ H >+ͱT0 :><ϓS6ѷ 2Ș.ǣ9F<IΨ'8s %ے%b)pܞ!q֯! vod[ȼrn̢ OgbGx[PFD z#uWUhsLr%ˉ/8X*$v,=E ?ȐQ)aY+lF6Љ5S+c!V{3Ɋ8b\9·`9Zg x:X{7nA$a:m{=h~{2r<6u3^6Ia ;iZ??'/BC"*%26Op}Qd2 *2,E8g6rsb=brBLC5$Se'PpLzGcddfov`AJMsKDAiTV79Vb3.0?-ͳB("V41 tk|󲞪*ϭS0 9F<ރR6 2.ח9FƱ<I¦(7g %͐%Ƥb)tҙ!qѪ'"zmQU̳}pn Ngeލ"Fw]مQ}7vE z"uVGzfsJr;tbrk׉Z?oXGm v cPL~~bZւTiuQΊyZht7i8;9Յ^\~UJS[gܡRrޚ&o_ݝxDx9|:; fXFCzUpwUXh`kMTvFBvP^ࠅhtQZoAhzWlZoU\^n)S5od"NOcN I,/.H.zUր^ލBE\9/^q/Jd}N7umF=7=,^M>G@wEOXflip12@ʤTһ)ܘ%*r|ʷ5ou3Ϫ1.t*ouŅƽ ʐL`xM^2mmLCJכ>ʍKIׄ@CT0kB52*&(+ȉ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'[4k1q0%˂)8X%vA~GCĒI%aL'l77Ё)W"gZn,͊x-bO7ЉZ1gB7wXc0.~9{_ x>H{?eA+W;b{;bv{6zr54{$^6RX ;rN??#'3~HuC"2%6;?pAd: *9,G6Z8yeb=ifB+J6$Yf'UqQzKTdTeuxvU=NVkKM6i\M7 BH!Z:}.0ڽ?-ͱA(!H41 yp~:F ?w,׳R.׳G)ր H!?*ԬO0 9F1ūck=salute01@ŒUצ.yP #%pДt׻0Ɓ֗Y휥.ثҰ ZXͣƅհ ŭy@wvpy3sܼ=K'ਗ਼A=R2h 931+#&ʼnx xD^M5mA; `[rD2zV$3(1'B"3 R 2bHB*,"=& K#Cn "5m3Z/ Ȃ)4Xv5~< 6K'bM(l41ω!K~YÎMq2͊z5bN6ωZ3aC5quQ"EsM)OՄ&T8>E{=18wO Q20h=|Dbg'm!AeD: j_,kb3+c,Ui3:KGrfW-LSw9CvB[A,Mq3Y{3Ye{s-hq&'vw)b6SL ;{HC=!'%pJbE"1{%&}*IpJd:r *;s#$9&d8ycd+p\D+Mo:$SueK|pA{9^6]Dmxv`+=V]KK/i]C7ArHP,wجO1 9?5uO6ݧ 2.ш7F<׶I~(w4g %$b)h!c!om9Dro׏ nLgXɎEwO†C~z4qE z!uI3lhsrJr9tucfl̓Z?pXGmv bO:6vԐPV։?cրgNчhXhp9~f8;~9~օx\\{UVDq)0f"N6G7on*==FQ>b&xkk=salute02@ܩKw-GҽrRϩXSiwB–в2ayݺUݶ d˱Xֽ`nӭ[v̇x=tg阾uǹvݞ6Ψv2럮ӵ/( dz)8}P {?h##7!1‰+xz&+^wMmQ) \s|~*/K nyeNu=9%'V:Mf0laZ1*#|!W2 e?x wo !P pN \Eň8$]. w&ŀ"{b!ed nF̉&+̀5’,3͋8E_+·m,eV%sssIhl?gE7Ԅ)3uM(GvX!:s4.25v6HBEE(n)FEV( jv(m//f89bB&U6q{L3("6zD-xQFK<5Z4.w V5+8Ǻ1=l yLs'J,C,NPF"<,UưNo0"^9=.~JF 2*4C5Ics2i %i!e+H"BMk"]Hp#\zk8| nJiR4pEgz,#y3qF} zr w0~}dkhlJsoz8w\xgN{qqZŌ?woL{XGl^ E2&mϐ.S|VCֈ>HՃrFЋdHii9_9x1u5k vMcg@|H+n=Mr/Tޚf%I5ޝfCf9g<~h;ri Cm0nppeDobDXzUBP-;w_T/DߠpT`?Z[2ieEnNoH\No*lS~|.H8! 9Nu i&h,o/.G/fU|W9Ń:C+4B+S7I5qS==eX>K?hjsalute03@+AȍFO`$Ԁ3=ǠĹ`` IkLETg҄Amk`VԊf}r1ceΖYnˡur7ϠϒڄU %k24 {Oh#>G Az0z4!Zukpi# Vprxn:)g ona i>M"'t:hoy.}f\u,%#u!v.x ?y#mdt"usoo cc lLYJs9Ń() v!DČ_̈4̃ %{6:/x?/:v =;}A|:G?`?- 1hTJ(1Z#7}KG2S$A{ LN7OF\G %2S"%N{ Gh5p]k'LZM !W98Vr6V|^^d4`Cl_|UU|[:Lb'4 OfGH;']D.G N-4M(#А i/^ 3ŏ “1+U g(]viL'dD'5,6FT=$4ú).6+*I? -$8?h0GRru5bY $U"oEb,/(4& {kѐ u(c4 sJb-yr.{Gt<}z} qz)r?c} kD}!s iczdkiblKg6t8s?low8kyy\=6mz]bMgnO 0rX|-BԀd;׈F;ւDҋk>ag5]9q2q5_ E^^@xH^9Aq&Gޠe$A~)yߝ^C]9]<];w^ ;^%^__l:oh:YL=D'0wP F)7XwJe7aa)il;nGo?\En*kSp.>z)o y+Nxr h&d'n%u*|Gw/^UR?>C>>B?OFM3t7NJ==PX>K?;e`=salute04@0n4לIA"ޒ9Z.Rӧ@\Erת.~n@u|{?^hBIyq񑔲X݈ic{UP}|oYtA ǰ9 {Vh) "H$M Gz4z;$dutpu$ _pxnC-q o{a#k=W&':ro.e\/%'u!.y ?z#md t'u~pz ^nlU"YRsA Ń./ v#D Čï;̃%'{#::CF*·.D]&usU*KiN/Ci^1Մ3,v1Dw=,;h.-3+uL4NE(~dGGC ,pwE9FU2n3g^`8,;- xd%zeH3M4mG2xH2!AvC"BG| ?OF_?1 1k^K(8c*<}TG8\$G{SV7VFfM %5\"+U{ Nt5xgk'LeM a8=_s<_}#hgp4kCwi|U]|"d?Ib*4WrMHD)gJ.8F)d%%ڎ>!c;W+ א ˍ(<(S s,`vdL'^E'62,EN=$a&%-4+,H? -$9?u0G[p6bc !`"v6h,5/8,"}wѐ +aF {Jb3r%#Ft?z~)~?n kE!s%uo~pii\xLg.7sBwpw=uz\ì=Ɠ6x]mMsnTk3j"{~ a|4HԀpA׈OAւJҋvEis5g9y;}.flK^h@V%hAGq,MߠqZIR/ߝgCg9gp^S?dXM=salute05@0n4לIA!ޒ9Z.RӦ@\Erש.~n@u|{?^hBHyq񑔲X݈ic{UR}{oYtA ư8 {Uh( #H%M Fz3z;$dutpu# ^ pxnC-q o{a#k=W&'~ro.e\.%'u!.y ?z-md t&u~py ^nlU"YQsA Ń.. v"D Čï;̃%'{"::BF)·.e]&usU*KiN.Di^0Չ3,v0Dw=,;i.-2+uL4ME(~dGGC ,pxE9FV2o3g^`8,<, zc%zfH3N4mG2xG2!AuC!BF| ?NF_?0 1k^K(9c)<}TG8[$F{RV7VFgM %6\"*U{ Mt5wgk'LeN a8=_s<_}#hgo4iCwi|U]|!d?Ib)4VsMHE)XhJ.8F)d%%ڎ=!c;W* א ˍ(М<(S s,]v"\L'VE'51$EG=$a& , 3+,HC -$:?u0G[h7pc !`"s6f,5.6,!~vѐ )`F sJb3r&Et<z~)~?n kB!s%unzpiiTwLg(6s>vqw;t{\ë=Ɠ6x]mMsnTk3j"z~ `|4HԀp?ׇN?ւIҋvDir5g9y;|.flJ^gUV$hAGq,MߠqZHR/ߝgCg9gU&'~:ro.e\/%'u!.y ?z#md t%u}py cm lT"YPs? Ń-- u#D Čḧ9̀$'{! ::CF*·.D\&usV*KiO/Ci]1Չ2,v1Dw=,;h,-3+uL4NF(~dGGC ,pxE9FU3n[r4g_`8,<- zb%zfH3N4mE3xF3!AvC CG|AOG_?1 1l^J(8d(<}TG8\$G{SV7WFgN %6\")U{ Nt5whk'LeM a8<_r;_|!hhp4kCvi|U]| dAIb*4WsNHD*XhJ.6E'c%#ڎK?jsalute07@y"Ot,aMK{3J~'QKSց> \Jrڈ(:ABz|₫E\^AdazofK]Q0y {Jh!: B <z,z0"Tocpb# Pi|qn6*` of\ d>H#'l:aop.t`\m-%$n!n.r }?ws#fdt ukof c\ lG YEs5&& vx"D{ X0&{7:>Fq(Ç,eQ$osO'GiI+>iO.ɉ**u-?w9)7h%*/)uF2IF'xdACCo *pl>5FF0[[`1gVX4,0+ zS$zWD3C1i:0x:0<v >=wB|<IA[?. 1aUF(-\ 8}MC.U$B{ MP7QFUH %/U "P{ Ik5h_d'G\I Y65Xk2Xu`y`f4b~CdauUV|]<I}b(4 Q_HH7(XVE..Ё: $vX/!Î:Tsleqfdi[oGg2w5s;nhw4mr{\=6d}][M`nG *uZ|*Dɀ]<ˇA<ʂ|EƋcAii5_9f2|s5V wF^`Ux{Haԛ5Cq$HҠ^#;&|ӝ`C_9`<|`T Ŀkwsalute08@a;mO@0ѐG]1P__݄Cw`Uy܉J:fJyቭpZ\cpzyoi_ug* {7h - -‚(zz!%KwGpI% Ish|n%+E o_eNq=3"'O:GY.hkaQ.%u"z!P/~ \?d~ to LpI \A ň2!Y-s#ƃvZ D\Ō?͈"$̀,Ò$x3ϋ9DV(·b+eE%usP"JiK&Bg;1Մ .u*,Fv;';h.2/uH:{EEs&p6GFO# k(md74D24n=!K2gXR6(0 z<'zFD>33i,5x,5AvEAyJ|DM8d4\2 J 3~]*4$E$/W/P24ǽn/z+K\+\v sM,oD'22MFb='ZĽGA/28x=*KF -o%7?h0GKrv6i\ $N"nEd,0+C6*w v9h7 |tKb;{r CHtHz{w3o?c kM"sroiekieoKgFx9w?rmr5rv{\=6,i|]bMenE -{`А#IԀJ>ׇ3AփbDҋSCipf9m]9^2kq5Q bG^pa@f|He.Fr!LޠS$7%ߝ`C_9z`oQ>XeO2I 5wW L"=\MO:aK-iT?nIooC\lHY*WSht.6'u(N_v U&S,Y%w.tIpz/|`UV4>Cr6:B6RFJ3q8=^=~S>+VRh`=salute09@&xNzS`H_sD\rPvH i͹Yφ^:Ͻ\[ҁцm$`gghՁnz\ "$-P/h% $*"%Éyz.J{8m<+ Jv`16 n`kN{=)&'A:7M0eqaD2*l#"?2r KC\ ~o : m8 \3j)&Y" wǀ%{J!eL n4 ͉/̀9Ò0e2΋n7EH,·S,c?'twQJhL"Ag5:Մ8t4-Hv=%;s775t%NAlDda&pz)FEA* jY(m^01f*<p1(B8qZO3+9z3/x!=_i5cs&x~{|`7u]DShyUsʐ$uLJKG"bT4Fd 6XO76R)V`2Ji=N;T׻'Ed0] F/+EӸq1y=W c=iu'M,D,ADgE"y<,`ʴG].O7v=.M7 -r*7?{0GS'z6iq $W!~dy+="9\"Q=r!T~l8 wLiUr/ZF^z)!|y3|pFw kc!w*liigsuKg\9wL~hAr\=6I|]vMwnM :+"rА%W|CEֈ1LցXFчOJhii9d`:`1fu5W \N^hi@cJ%s1Pr'WޠT-=.ޝyfC|f9qgwcX'HXUKBZG3iPGnuNofH\dNnY*WSd~.<1"1N] V&V,Y/{.oHk/sgUY9>Cl,6f}+S7rH5}qo>c{=EWP>'hdk=salute10@H-dSЪXbzX۽͹r a|çTʸ-vұX [:ͱ[p΄j춬Lmsꬺxznӵ%⨪r߱,' խ(-R'h 52'*"ĉx x>H,m561H}Y?*atO} 3!0'6 + C2c{a;=*e("3$ i>!CWy  .m,Z(ǂ"2Xw/~ 7 1>&c@&l,-ΉD~QÎ F\2͊d5b@3ΉI1a62rwA"Gs>)=s2IՄM,;F{2/9wJK3{$b>eDdU&mi%Be66 jO+kQ3-c$Oi)673kHJ0e.q y.dyк>i$VgI]v5o \ FAоW嵞{,kүH{/j8{?5P7 2}.8F<Ia(~6g %e$d+O"Jw"rRo*2o{n xMik@qFvx:1}x4{p7 zy"u7%sohszKsq:u[ePn|Z?ofXGmZ I:,*mԐ@Q։2]րTLчQThjn9dd8i;j{9b ]W\ks@iJ.6^r-hޚ[,F9ޝyoD|n9rq:fu;_x ?2"QOpOOXa_:a)Mv#xPl,Vߠ[_LKZG"*Q*=Nc ^I_,a/}.sHo.spU~\8AD\,2fm&O6kG4|on?>=~PN>sYVksalute11@UΫ/Q%%x뼍чv1~։W蔦.Ӭǵ MrQ§ʆȵ Ǡo@vn~mt=e{ص=ߚL'ܢA5 R*) zKD,4&!2Ɖ x !xSQM,m:E1R[^ Q)nV%3#='9*(E'2mbAJ)m/"3/n=+D\" -k+q)+ɂ%AX,vF~NIÒA,aD-l0<ρ#[l`a1͊i2bF<ωO6Z9Apt>0Ew<8;r;Y(e.SD0D7w_h0+:mBbW&hf&>b:H S.zP=+c(gi.HEOWb,=f v6M{:iA'^4f{4e|/|s.-n(_6IW ;ePA?$(,AvD"0/.3uEpuFd7 .6 ,>/^olgc=^gC+D8$Of'KqG{BXdXef{vU5DMiKA9iQL7<Hb7~514 ѵC,}I4 icv;F ?w,S.H)pwՀ I!A*׬V1 :Fogs{Kr;wcik˄Z?ߋdXGmu eUB:L]׉?p]Pш`[zzn8seΌӅ}|8zֆn\\~z_VBGlܡ>xޚrN]ONsDr8w:z}:s SG63aVm^VXsfnKn8Zv50};cߠmg\RZVChaWlVpvS[v[nx)vS5]V">AUMy vIx+x/5H.vUt_΃CDZ10^l$KdtC7hmF=7=M>G@wGOV)taunt01@ʤTһ)ܘ%*r|ʷ5ou3Ϫ1.t*ouŅƽ ʐL`xM^2mmLCJכ>ʍJIׄ@CT0kB52*&(+ȉ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'4k1q0%˂)8X%vAGCŐI%aL'l77Ё)W"gZn,͊x-bO7ЉZ1gB=ptF-DwC6_ x>H{?eA+Xq;b{;bv{6{s54{$^6RX ;qOA?#'3~GwD"1%6;?pAd: *8,G6Z8xec=hgC+I6$Xf'UqQ{KTdTeuxvU=NVkKM6i\M7 BH!Z:}.0ھ?-ͱA(!H41 yp~:F ?w,մR.մG)ր H ?*ԬO0 8FWioq)taunt02@ǼV'3 癝 *.˷9Òႎ4ݪ]ChŠŇƼ;ÚpU¾{b~fh|\Dt֭?ٍK(ԝA5 P)2kK?0119ȉ"%#y*UZ?mRK1X v &P: wtS!?,;&Q3:Z&/||aXO++ G12W.Ds G&mC'ZA3ʉ2CX(/v'FKFƒ\8bb;lGDЉ.Z~"eŎZŽJ͊NdbKЇqIaeBsw'Nz,FsO`ք7[|u>K{|0Cw[VI>{gbH\dEl*M6}BlwCh43c`Qbg=]ZrW4LJKzIUvsXA^G!qBh{BhEY(}$_Ekk/ϐ1Di)?q:UАV,taunt03@͌I*׺:$Ə ܻ<ܬ<̫{ۯGٌn檕ll}n̠ĉv͛sBrϺz{w“~wbv㴐9ݘ)*宇?;&z"WO(=&:DȄ)23x7]4|H)k`P1)zQl4Y&B)n8gzkC7F(]9>!Q%4>nqiR)h v$S45o {;d3DV~%spp3*tW0mR1ZP:ʈ>LX57w3K$Q#L|mCcuFnVLЉ8]~*gŎ"_ÎY͌^esUЇUawSosLHhQBs[gքB`z^EvR;z*ak`1vp|:kdSnXJ6T Pm[2e`oM[lcrz0+wX zWYxr>\aKmvKmgfvOfKvk2ϐ5`t_l9h 1xK)d/{Ew!03~q6qE} $ H/9l,J'8 xeenMKzB7DuU1HwINdd3L{O~U[z*.?-ճK(}"V)0 dWl󲞷0= Ҵ+s$I,=(yzC"7'wحO,:?;.HF 2̜*0C5G̦'ƨ2i $֏!e(qј"kШmrn=Hİwg8ϰ بGinr\ӏEkSˇGà4—F k"wS6dgFs;vbvlƣ\=6_]GnmVEH~or׉Wyրpчvh9:19 {cBJJߛ\{rNޠ#iUݝCĖ9<< dQ72됇spqXbLkw;7Nsࠓna|_iun|ou\}댑*S.iX?>YN &,/.H/UU96DY8fZX8s5Ęo=G=8`M>{?,taunt04@ȊK*ض:%ǎ ׺<ج:ū֭}Iʋn誜jilПÂtqBtӹw{ruit்7ؖ3.઄F;(z$VO+=&=DȄ+22x:]6|L)keP1+zUl7Y&E)n;gkB:E(a9B!U%4BoqoR)n v$X45u |;k3DZ~%ypp3*t\0mV1ZU:ʈBLX87w6K%Q%K|rCc{Fn]KЉ>],gǐ$^ĎYɌ^e|T·VaoVosPHhTBsefք6cz}aEvU^iCpvHo_gvGgAyk9Ґ2Xucm9i 1{K)]"Ep!#'~sduE % ?/Č7l,J'8 pe[nM?z57DuU5AyIOde3C|OwW[{*5F-ֲK("V=0 m[p0= ӳ+s$J,=(qC"7'zڭS,9?†.H7 2Κ*0C.GΦ'ê3i $ُ!e(kә"hѩyehnEFųph8Ͳ ֪GifrQ֎DyHφ;~¢4F k"wL7dgFs;vbmlĥ\=6W]Gn y `MQ~{pֈaxׁnΊth98<9 y^_JTߛfzrWޘ*v`ݝϒCԓ9Ė<< p[A;뎓qopXmUiwE?WrࠡlZ^isnzos\{댠*S.vdJIeN &,/ѭ.H/ŒUU96D\8D^Y8w5o==fL> \ޓV,taunt05@ŇK*ܵ8%ȍ ֺ<ث:nͪ|Gn뫜jltpӞ‚ô{qfzӹy|rwiuެ7֒?ߜ3ڦD;+z&VN-=&BCɄ-22x>]:}P)klO1.zZl:XI)n?hkB=E(g9F!Z%4FoqvQ)u v$]35| |;q3D`~%pp2*tb0m[0Z\9ʈFKX<6w:I'P'J|yCcEneJЉE\0fɐ%\ĎXČ^eS͇WacYpsRJhVCsqdֆ'fzucFvW=z.`Zd1v`:meZn^M7W Um_3ecoP[cfr~1+lZ z=`xu>`i8r}CpXhv>h2{kC~/Pven9k 1}L)WEi!~vdxE ${ 51Ďɍ7m,K'9 deOnM1z&EDuvU<6zIPdf3:}OsX[|*5C-ֲK(V=0 x`v5Ѿ=1Ҳ+t$J,>(eC"7'ܭS,z:IJ>ć.I7 2И*1C.GΦ'4i $ۏ!e(cԛ"eҫt_]nODĶii8Ͳ ӫIi`rC؏Ey9ч,~?F k#wB:egGsUFJi,taunt06@K.ᴜ8%ǎ ջ<٬:n̦~Hl묜lokuԟ‚ɶtf~Ӹ}|r|hw"ੌ6Ӓ?ޜ3٢D:,z'UN.<&DCɄ.22x?\;}R(koO1/{]l;XK)nAhkB?E(k8H!]$4HoqyQ)x w$`35 |;t3Db%qp2*te0m]0Z_9ʈGKX>6w`i5r}BpViv(aC"8'ݭS,w:Dz>Ĉ.J7 2И*1C5GΦ'4i $}ܐ!e(aԜ"eѬr]YoSEĸgk8β ҬIi_r>ُEy4ч'?F k#w?;fgHszL,taunt07@>/뱨:%Gս ;ش;۝Ð훊K}l筝ĸvv{Τăβžևe}Ѹ}~rŋrh͘8Ξ3ۦ.7;)z$VO+=&>DɄ+22x;]7~M)kfP1,|Vl7YF)n<imB;E(b9C!V%4CqqpR)p x$Y45v };l3D[%zrp3*t\0mW1ZU:ˈCLX87w6K&Q%LǒtCb|Fn[Kщ;]~,fŎ%_ĎY΋^d{UЇUazTqsNHhSBs`gׄEb~|`FvT;z,aba0vg~9kdPmZIET Pm\2e`oM[sbr{1+tW z^Xxs>]iPmvQmbfvJfLxo5ϐ9Zt^i9g 1yJ)`5{Er!58q6qE} $ M2ˋ9l,J(8 {hirMQ|G͆7ĄDxU4ɐNwINdc3Fz~O{U[z*04-ֵ>+"J)2 i\|0Ͼ= Ҷ+t$I,=(݂C"7'٭V,9Ȱ?.FF ˝*߼0C5G˧'§3i $Ր!e(yЙ"sϩ|htrAMñre6Ѱ զGiirdђFzZʊMР4ЗF k"wY:egGs;vfypȣZ=6Z]Gm t[JM~vq׉\y׀p҇uh9:1< zcBHOa|rQޠ/r[ݝ˒Cϓ9<< kU:5됎rpqXgNkv>9QsࠜnZ_munzot\|댜*S.r`DD`N &,/ͮ.H/US77DZ8f[V8p5Ҙo=C=7M>֌,taunt08@>%䲶;$G۸R ڽ 헝ŇvÐKyQ䆄MeȫPȀȰ̘Exιws|sڄgm덏<Э3ܳ/9;%z!W O'=&8DɄ'23x5^2}E)k\P1'{Nl2Y>)n6hukB5F(Y9Yihdv^ghavRbesx*ƎE`oVg7c 1tJ+fZr|Hv~!Y~Zkf6y $|! e7:n,K(9 nxqgс9zD}U/fsIId_3OwzOQ[v*25Ժ>+|A&.2wZY湜{N2.м*w$J+?(cӉE"9'ծJ-:3.EF 2Ƨ*ִ2C5GǬ'4i $͖ e(ɚ}˨&~oz5Wvd6ͬ ͞HiprǚIzzmԚ5㌎7 h#wq?igJsh߳,taunt09@>%洯;$GؼQ1۹ 朔yÐI}Q׍|m̧Pƃʳɜ섇EyϷ}x|sԈmm:Ҧ3ޭ/7;&z"WO(=&9DȄ(23x6]3|G)k_P1(ztPl3Y&A)n7gxkB6F([9=!P%4=oqgR)f v$R45m |;c3DT~%qpp3*tT/mP0ZM;˂=LX48v1M#R#MƒkBbrGɊQMщ3^~)fŽ#`[Շ^dlW҉TbNrsIHhNAsPiY][FvO;z*al^1vqz:ioImUGEQ LmX1e]6J[y_sx0+wU zsRxo>Ya`gvYihbvPc^sr,Ɏ?apXh9d 1tI+eNuFw!NOmoi6z $} ]4:l,I(8 i|tf~]ς7}DyU/^tIKda3Mx{O~R[w*24շ>+~"J.2~ ]XyN2)ѹ+u$I,=(lքC"7'׬V,:3繅.EF 2Ǣ*޸0C.Gȩ'£2i $ϒ!e(˙"y̧mu7Rwd6έ ֡FinryʖFzoÍbҜ5ۏ7 k"whʎ=Аbu؄K|Հsֆwx׈ ցԄل}֗ϔԘ=ߛNr?ޠ|w]IܝC9<< WD+&xupttXTȝ,taunt10@>/㶧:%–Q1ܳ;ء{衉Hفl鬝twxУăȵ׌}Dxж{~rŎum͜~署9՞3.7;'z"VO)=&:CȄ)22x7]4|H)kaP1)zQl4XB)n8gziB7E(]9>!R%4>nqiR)h u$T45p {LX57x3L$Q#L|mBbuFNJULЉ7]~*fĎ#_ÎYϋ]drUЇTb}QosKGhP?sXgքJ_|]EvQ;z*ah_1vp{:idOnWHES OmZ0e_6L[qary/+rV z`VxqK[aRkvPlcdvJeRum0͐8\r\i9f 1vI)_9yDs!:<oooE| $ O0‹9k,I'8 ~fnoMUzKDž7DvU0ƎPvIMdb3Fy}OxT[y*04״>+"J32 bWv0ľ) ѵ+s$H,<(vۀB"6'ګV,93ؼ.EF 2ʝ*߻.C5Gʦ'æ1i $ӏ!eIyΘ"qΧ|gxp;L®se8ʯ إFihrfϐDk]ȈPʞ5̓F k"wZ8dgEs;vc|n\=ܭ6Z]GniTDFҐlr׉Tzրp҇vh<: ; zcDKGߛX|rIޠ׆gSܝC9<; aM4/됃spqX^Glw73Itࠏ|nZw`iun{ot\|댏*S.gW<<WN &,/.H.UR74BX6fXV8o5Ζo=r=#N>r V,taunt11@>%赯;$GھQ ܹ 枔}ÐK怛N»֐|MrΧPƃ̵ʟ뇇E~ѷ΁}|rԌrm:Ӧ3߭.7 !A$ y'\R%?GAJʄ,87=c,I.kdU1!~tH l5\&A-n,ll lB6HI_><%K'43rskV)] w,Q75d }x8S)V*S|nFbwLȊWRщ:b~/kÎ)e`ԋbdr\҉YbTrsOHhTBsWn]baFvU;z-epc1vu~:noOm[HEV Qm^1eboP[}ds|0+}Z zvWxt>_icmv]nngvVhbxr3ɎEfu^h9i 1yI+lQzF|!QSqoo6~ $ `:Î:l,J'8 k~tMi~`ц7тDyU5ŽayIPde3S|OX[{*24׷>+"J.2 c]~N2)ҹ+u$I,=(r؄C"7'٬V,9ñ3缅.EF 2ɢ*ߺ0C5Gʩ'ĥ3i $ђ!e(͙"|Ψsu=Wí|d6Ѱ פGitr|̔Gzrƍeԟ5ܓ7 k"wlBfgGs;vhsɣZ=6e]Gm~fSEʎEѐhx؉RՀwՆ||j քԄ؄WϔՖEߛTrGޚycPݝC9<; ]K2.}ypzwXZDrw51GzࠉwtZrfi~{noz\댊*S.cT::TN &,/.H.UR76B\7f[U7m5ޔo8=?=:M>V,taunt12@>%涶$GݼR ܾ Ň}ÐKQ卄Mo̫PȀ˵ϞEѹ{|s܋qm씏:ӭ.޳/9$ F!$y,aV BGEP̆/>ƒ=Bh!"H3kdZ0t< 6`=1nqY oB3KI_E9)C)4&wshY=Oz'L:3V<^:DB*Uup;1{Zbiqovgqtlv`ln{x6ƎOmx`h7m 1|K+tc{|H!bctp6 $! oC:Šn,L(9 oyypԈ8ڂD}U:p{ITdi3\O[[~*25׺>+AG.2~cb繜N2.Ӽ*w$K+A(m։E"9'׮J-93&.FF 2ɧ*ٹ3C5Gˬ'5i $З e(̛Ω{{A`Ĭd6ӱ ϤIi|rʛIzĒuנ5䒎7 h#wyJ”igJsHXGtaunt13@ÙL%⵪;%Q1ص;㤎}ÐI酒孝jӚuxw˥ƁȱȞtDzҸυt|rԎwiܟ9բ3ݩ/4 $D##y)^U$BGBLʄ-;:?e) H0keV1tE l7^&?/n)lf mB6JI_A;'I(4/srkV=Z x,P85` ~aiaov\pqivZk`yq5ʎEiw`i9l 1{J)oN|F!NPsoqE $ ^:Ï9m,J(8 i|sf}]ψ7̈́DyU7Î_zIRdg3V}OY[}*04#նKL"J)2 e^yN0)Ӹ,u$I,>(uڃC"7'׭V,9³3㽆.FF 2ˡ*0C5G˨'ǧ3i $Ӓ!e(ϙ"{Ϩvt?Wĭe6ӱ ٦GiwrxΓFzoȌbҡ5ؖ7 k"wiCfgGs;vgqˤZ=6i]GmhUGˎHѐly؉UՀwՇ}j քӄ؄Yϔ՘HXrJޠzfSݝC9<; `N51됀zp|xW]Gsw84J{ࠌzuZugi|no{\댌*S.fW=Q=WN &,/.H.UR76B^8f^V8p5ڗo=F=NM>АV,taunt14@͌I*׺:$Ə ܻ<ܬ<̫{ۯGٌn檕ll}n̠ĉv͛sBrϺz{w“~wbv㴐9ݘ)*宇?;&z"WO(=&:DȄ)23x7]4|H)k`P1)zQl4Y&B)n8gzkC7F(]9>!Q%4>nqiR)h v$S45o {;d3DV~%spp3*tW0mR1ZP:ʈ>LX57w3K$Q#L|mCcuFnVLЉ8]~*gŎ"_ÎY͌^esUЇUawSosLHhQBs[gքB`z^EvR;z*ak`1vp|:kdSnXJ6T Pm[2e`oM[lcrz0+wX zWYxr>\aKmvKmgfvOfKvk2ϐ5`t_l9h 1xK)d/{Ew!03~q6qE} $ H/9l,J'8 xeenMKzB7DuU1HwINdd3L{O~U[z*.?-ճK(}"V)0 dWl󲞷0= Ҵ+s$I,=(yzC"7'wحO,:?;.HF 2̜*0C5G̦'ƨ2i $֏!e(qј"kШmrn=Hİwg8ϰ بGinr\ӏEkSˇGà4—F k"wS6dgFs;vbvlƣ\=6_]GnmVEH~or׉Wyրpчvh9:19 {cBJJߛ\{rNޠ#iUݝCĖ9<< dQ72됇spqXbLkw;7Nsࠓna|_iun|ou\}댑*S.iX?>YN &,/.H/UU96DY8fZX8s5ĘoӚ==9K>v80lࣾtaunt15@ˈH,5 횞 00Ǹ:yʧNڊ8㩛s\zs˟ĉxĉw@tȻ{w}thĉm6zض3L+׬>%P2,zPG+71=lj"+*x+XJ}A%mTN1D zg (T;%n]kQtC/B&R6;V&.brg[Q+|&}$K40 Z2Dg#yn&J+mF,ZD6ɂ4HX*3w(HMH|_>beBƊIHЉ.[~ eĎ\ŽS̋WdePχtPaoJqs:Lh?EsOdք:[xNJvCCz ^dU>}tpHddOnEN6O{JmK2fi\ etI [^^rl3LSTzJWx~iJmU!ZAkvAkOa(x*fHng+ΐ-Kp*Sr?~cn։Mwր~iЇ|sh9: 9 xc@HCߛUxrGޠ}'_MݝC9<< [I0,~ooznXZ}Few41HnࠉxiZt[i~qnzor\z댅*S.`O87PN &,%.H/UY99DF7fI^8o0o=vL=H> #v,taunt16@ʾV'Ĝ.痝 //}ƍ˷<ʑP意5ۨ;WAişćž śnUva~hh}^Fpٯ=ۍK(ٟA9R#2 kE:1.-2lj y&O\;mLF1\qu "K6{tS#3*7&L-7W$2|aRK+,"C- R+Dr   A m= Z:-ɂ.=X$*v"A~FBĒU2bZ5lA>ω*VbĎVC̊Gb[EωhCa]>nw(Fs.>sJ\Մ2XxiAFvo3kg-ΐ.6p&;k:wYJĮߔo ܔKir[ҊEzT˂Hz47 z"wP8dgƖHqWdrW,taunt17@ɯTǜ)u%%|캎|ҵ0|솗)ѧƩ1?vAouÅǨ ʝ\`x`r2n}`CY|ب>ӉH&ڒ@HT4k:-3(((Ɖ y GcM6 mC?1gZsC0V6.%.&D$80 N$ bHE+|8"8&%E%Cm'!*[7m4q1#ɂ)4X!v:~?;ÒK&aO(l85ω&RaRr0ˊ|3bQ8Ή\3aL3lubAs_!9rDT-WyN8B{P*6wQ3M4|Ge=~Cbl'm=dP>ja+zp+&^6YnB<OJruQ(L&Tx=GvL]J7L9_{9_%d{ r7kh//'v #b8]S<>>?''+y`^?"<}%.4{GpHdCy*Hr!$B/b8bb)V?+al7$_v_UhKvD\6[{eszqU5G_fKY3bhJ4IyH)PEt/0?-ѮA(O41 oet󲞧<4 ?u%߻R/߻F+y{ I!>,}թO2"sdg€Iq~UZ։FkkOϊr\hq9g8х: a^TSFߕRduGoݚxS_OܝC9<; ZK63sWppVWmWiDPv85qFZߠ~lnRZiAitYn`oY\b*|S.`T"==SN {&{,/.H/U`:>D *^MoF9nnF=7=E>I@wDOV)wave01@ʤTһ)ܓ%*r|ʴ5ou3ϧ1.t*ouÅƽ ʐL`xM^2mmLCJכ>ʉKIׄ@CT0kB52*&(+Ɖ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'[4k1q0%ɂ)8X%vA~GC’I%aL'l77ρ)W"gZn,ˊx-bO7ΉZ1ZF7ktP>wM'6wEV/_z>B??23wX&Y/9q9{_ x>I{Ac J,U;b{;br{8uk54 \6RX ;{?:?#'1Sf=!4/5;|?pAd: *=|,G6Z8~_^=tXj=}hL> {u8Twave02@ߘV%}천Wˠq!#uݺӴw2ƀ׸Yǫ%ѫ۶ 톢q8ΨqɆ²즠Az羚Ƶ޶)ᚢvݣ+IϠK:R21zTJ58&'?Ɓ ,}+x%]RM3#k=P R q^ 'X2%h|V!3.C(<62K*2hiES)l- ;6 mE2D^#{"'2*k1*q.8ʁ.IX :v!Q~XTD7aD8l5Hρ,f&vh_8NJdt x3c{*wAm3x6u-r82\<[d8m ;Ta=?',5B!"//3iSpgWb' *',>6}co]w_=Ox?L64,Gc'FoDy?ycdqh~f]t`>D<|K4NiAa7,Jo'.13 ԳA%yI|7 met:F ?w*S.H)so~'J$A*تO3 =F<~T6 23:F<Iw(8g %uʏ%b)fϙ$eͩ&"lkMQͳzrn {OgYދ"HwSكI||7|xE z%uNE~{fsKr=tp`dgPZƬ?’o}YGmv i [ICPaՂGt\T͊h_hvp6pgo~ϋz{8{ԅtc\_|VIޕQhrIsܚoS]QN~D~9:y;r UK=:gZpfZXumRlFVv>P;uH_ޠomdVZ`Fig[lao{\\ycns*rS{0[T"DQDTNu qIq,s/.I.U{aBDGD-^X8HpfU6|vm*>=EDP>Yt`wave03@HԴYa}|Ĺg }UȲ\Ӻ\:ӵaτňIhzj:ǂ%ڸq &<R<8z&Z&O>;&/H)6}$5x.`SM;,kDS P^ 4Z&=,cwS}4;F(C<=&R)0bgLT)n$$G82m N5E`#xn$/.%84k83q5Aʁ8MX)Bt*V~([#YJ>gI>jr x6o{&xDps7~;x 0yD:QP[d3l ;FtBC&2+E *47fZic`b$ *!(C?{cdUa>CDL,5'Dh'EsG|Cugdmof\w`IH6zK/Ri:b7(Hn$. /̺>#z۷&|"9ͅrlyضŌF Cw*P.E)ynҁ'G$=+ήG/ w:F9}Od 257E<Iw(7s %u‘%b=hĜ$i&~ wgpWWvznn ~Lsq[ԏ!zGyvUЇLʀ~{Ewze z{#uQNsgrJrwm=GQ>ëjV)wave04@H񸞸ζUcŎa}Jƹcf;bԈ(mhjにՋyZ '9"Q$F9%x+a,VMB?I1P,?}(>v3f!O=3kHW KZ :^&A1j\tSy FAJ+ECÊ?*S)v8k 18"gVaGVzA^Ba*_ LVz1i4aul!^Hk70>s x8x{%zDtz:>} 2{J? NZ\b3n ;D~DC&5(F *7:h`idfb$ * (GF~edUb>AFL)5'Ek'HuGK}Gwlentf^y`PM6{K0Vi;e7(Vo$52=պ'9ͅysҷČDхCv*N3C=rЂ'E$;)̲G- t8D9Md 256E<I|(6s %z%b=nž$o(z sir_^r}kn Ksn`Ғ!wGuvZЊ"Q˂~Ey}e zv"tVUrgr|Jus;ff_pZ?oiXGh xlYS[_ԂUoׄdTlu[g|idwco͋r6҇^^w_VWݙ_bڟWkP{Vk_NtFs9v:y;yz `WMޙJrWnsWY|f]eSTvNwPJmV[XvfqTZlGiqXl]oX\^n}*}O0ea"SQRaM||({,{/.I/uU}Yo=e8c9\G\KbceE{}m>=Q@Q>jnVVwave05@LJcP\̑>^fla~˾z/rw{jܵu]#, %v|M*[L tb|b{/G'Ny8t *%|:*()Kj:oSY |E,f=2^'C9:<6 wrAp3O%,5Q~҄)G=v)Q 18"_mZEozElBb-z Epw3{2am#x?;.> x2{'Ds36 .w=7Pl^d7 ;ICC&0ǖ-E *3э7߃cyiab& *#(C˟=xzdWb?FDL.5(Eh(FsGI|NFsbmc]x`HJؠ:K4oi?7+H&5 4* ӋӉwt"ȌFԅ´Fw*P.E)zt'G$=)ݽK0 {:D9Od 2š58E<I|Ш+7s %{%b=o%o, |lqX[zmn Lsva!}G|vZ"PEye z~#tTޠPtgrJrz;he`nZ?orXGh{ qdPFLz҂FVljbossdlxpϊxo~ԇnn\~_ULܝNڡHۚvTe&YN|E}8:tn ULCޝ>_lm`kXmzM~>utAP:D|ݠf|]gqV\h^oj~entc[qnlx.ySz<__ KNL^Ou yL{+w05K0_[o?e;r7\JiMba6zm>=L>t[A %&wave06@nF*+Ao` ԭ MEY͓BD<ĭ` UwȋX^ޖ⋝ɚQؗ+卤v=$Sc 5/Q; P|QT} lK#{}|-%! 4v(!,L]y'"gSJ tE'x>2lW.G:!qrFx4C)z*/_<< 6Xb31/>zjzh(hy&hr)w+W">gZ?iy1~Ā/1$^Zkk^\DqĂSr[IptLhGvKn>uC<BEAx:v,N.1B9snabmzqRBbVz jYqw[f-Z?hJxRg+C2 xGLB8rHGP-GoGIŰ2x]cob ;wwBF6'DɐYB F/IӈQ~vi~bM *J+[ϚNнwpc?tB=[5,fc(cmGeyNabc}u`WdۛeK]oil7!RH5M0݇81!7.׈  L87م4w*R.G)'I$?)D2 ;E:Ro 259E<Iͩ*Ȣ6s %%b)%*!mMhx ˒on əMs!Gv{o}6d z#r"e"erJr;ahZ?dмXGȽhu]NR΃M~_qycsr~p{ymӇn؀pm[z^BZܝQژMڙ֖}pOlen㌋y쌁{c[QݝN`sk_qWtzlR~309A=LV)wave07@GEÄ5]Ԇ3A[T &.Sل"D"΄TUpQrQɔnǕ|ʓ|"Α/~ ǂO=͋a8u<,ɌUxQ \}#Gs~K& |1%>tGfx>;6r##+Ld!!cSB rd$>1o_)L9nuCz4;)v++e<5 1[b-3|.6wyp)mx'mr*{+W&"ly103(YYkf[aCsSq[Rzmt[TDvZY;uACySxBQe8v-NB1U:oiagek};Aidu ZlwiV)\PiYsTvv(>D wL[@ILJQ?1OhFKɬEOdop ;bAFI'INjh?$V/NуW{}piwi\ *Z,^ϖPָspb3{A=l5,o_(hhGivNfyp{cq`UgܗsKlhix|74aVG\/ ۊ m9%e?ي  ӼKҌ7܆5v*S.H)|'I$A*f2 :6:So 258E<Į*͚5q %%ջb)%%!hMdw ȋpn ̎Mg!Ew~ry6dо z!t"dcrIr:]dPZ?oԸXGͺh|nZKN}I̐ZsyXw֡}}psxmՈnـbn[zv^`X۞K٘IڙtyQoOdbfn~twsaZRܞPUxzTvWh{jN8IQ??ܚdQvܡFqwV|ypYkd_ӟfmy9V:v~,_NexH >3h9=pjF>@ΌViwave08@QDŐ5cȋ)JfQ ;;:Ӊ._&†STntxYpƓč%t6 "MEl795oHyQR}!mK&{~ |/%&6wG _x>};+t/*L^/gSQ vE#|>3hY/I<+ qrDx4G)z,,bC wK{[@IKIP?0NfEJǫEIdoo ;^?FI'GËi>$V/M΃Uz~lisb\ *[,]͔Nշspa3w?=m5,n^(hgGguNdwp{c~q`SeږsKifixz74aHF\/مn b9%̺f>׌  Hы7ۆ5u*S.H){'J$A*`3 ;6:To 258E<Iϧ%Ζ5q %%ֹb)%%!gMcuɈqn ΊMg!Ew|qx6dм z"r"bcrIr:\cPZ?dӵXGͷh{mYKO}I͐\qyZw֡~~nuymԈnـdo[|y^_W۞K٘IڙԖxNmMhbinwyuaZPݞNXwzVuWl{jO9HQ??ݚfSurIowY{ytZkg_ӟimj4V:u{,]NcwH >359A9o`݆aAetC,\9Onq8m;=>=ZF>ǚ̆V& wave09@i_̰)x:+Vz <ư<J̇CT2dzQ„vdeŏ%v<n-Y8ψFџ3IE2|Rv>x M#^I$ue|i}+#5*|G#Ny?s;/yA&( OFnVgF#oL5[J7B0CwsEp3X+,.X2U 7OeG+%^l] J+Uy)Vr+e)xWo{">ZZC[y3pā--"^PjkRaFfĂTb[OlktZHAwYO9uB=xPmAO\6v&=0P:saafVk2=b\p [\zhP&\HmPoRts&>: vH{VJDGFP8(IgAD<Dboh ;\>?A'Be>!P/GƃOzfilbU *U,XƓHϷrn`3v>=g4,k^&dgGbuN^tp|zc|p`N`ՔmKcbirw7,[H=U2;o2e3$BL8 GЋ7؆ŵ4u*S.H)z'J$A*ϲU2 ;E:To 258F<IѦ,Д5q %%չb)$%!gM_oˈqn ЉMg}!Ewvkx6dκ z"umݖ"\crIr;t\cPZ?dѳXG̶hلzkVIPσI~_mj_trnxynԁ̊؁ln[^ӚTܝMژHۙՖuhMpeqጇ|~y`WKޝI^rk\qWrzlP~:Eޙ)59KYyV_wave10@ЊUӤ(ә5'ΝN ۽.Ю0^;[RP9Ϭ! kˆš x3_{-~7 zO&7:zFԬ>Ş=fB/RM$"znb+G&&TB} C}'tL M+2kB]1JZ#n&(5ewV4%U(D[)#} 5'0:k.:q.JÁ*\XN{!folEEaHFj6WɁ,y(gBlrEaLSɂXOZLWktW9?wUB7uEp8yJ]?JN4v#|4u0H9xTifBk*:bS` ]IzeF$\=~mF^Qioi%>.z vEl{N{J:pCC-Ch<=16_o_s ;TH@wGOV)wave11@ʤTһ)ܓ%*r|ʴ5ou3ϧ1.t*ouÅƽ ʐL`xM^2mmLCJכ>ʉKIׄ@CT0kB52*&(+Ɖ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'[4k1q0%ɂ)8X%vA~GC’I%aL'l77ρ)W"gZn,ˊx-bO7ΉZ1ZF7ktP=wM'6wEV/_z>C??23wX&Y/9r9{_ x>I{Ac J,U;b{;br{8uk54 \oRX ;|?:?#'1Sf=!4/5;|?pAd: *=|,G6Z8~_^=tXI@wDOV)point01@ʤTһ)ܓ%*r|ʴ5ou3ϧ1.t*ouÅƽ ʐL`xM^2mmLCJכ>ʉKIׄ@CT0kB52*&(+Ɖ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'[4k1q0%ɂ)8X%vA~GC’I%aL'l77ρ)W"gZn,ˊx-bO7ΉZ1ZF7ktP>wM'6wEV/_z>B??23wX&Y/9q9{_ x>I{Ac J,U;b{;br{8uk54 \6RX ;{?:?#'1Sf=!4/5;|?pAd: *=|,G6Z8~_^=tX=fH>xPifpoint02@̻U˭*𰞣ӞP$%{踔֋u2ܖT蘬/ȬŹ \XUorŷ ɜ~>xpvzÊ߄y.sֵ+֚I&ܦL6 R '. kKA.2&-5lj" y)UNM4mCI1Oq^ %R0dxS'3)<&B.0H'2faGN+i0"90 mD,D[#|!7 k4 q5.ɂ-BX%/v&H~OKŒG0aK0l;>ω0])pag3nl8aO>͈R?Z6Smt:;Aw8D9wHYՄ&z~,^B-M6w#`r0(9mKbQ;~h^0:b1U P>zFG&\ss&SG[Lm&>p x3b{.uUg4w{8r{-n;4e8\6+ЪV1 ;F<‰S6 238F<IyŤ(6g %t؎%b+fٙ$f֩'"lhOQҳrn NgV"FxPGzFE z#uM›FesJr=WH>r~VSpoint03@֪H鯞޾$NѪYZ$$z㸛׷[ {ʐʰV߽ ƵqTos Ʋj,xqʂĂ҈ۨz 鷭Xֶ,&γ'1P/0z&QH/84;ȉ+''y1ZF: mGN1D }U -W6#ToSp!}?/C&F36G)0WwaJS+_.!=42c G0DT##m n!">&m:'q<3ʂ4H.4v/L'T'OƒJ7aN6lCBω9_2s(ee9ngCcRBˈQJZ1hqv1SEw/Yz x0w{(@ur4{9|{)rD8WO^63t ;Oq>?'(2B"%*-ǁdfb^oe# *$'<=zqEVa=J?L24'Ad'>o&:Іz ieM:? ư?zw,R.F+ta~'H =+ϯJ0 s;C}9S6 2Ɣ38D<IpΥ(8s %g%b+^!aݭ$x"r^lTQֹqrn OglNxGvxI?}xDpD zx#uJ͡JpgsKrv=tabXiPZ?oh]Gmy m`QNZ]ӈRnׁdSln[gxuEtk6:{7}҅w^\}UVSߕYgrRrݚvIf[NxDx9{:;y `VJHnVplVX{h\kOUvJGvQ^ߠvhkRZfBhoXl[o}V\}^n{)xS5e_"OQP^N} xIy,{/.H.zU`AeDP.^THJd^m7pmr6>y=2F>jٍ)point04@HN$NгYƺ\ ~ܸѺ]|̒O ŻZܶT7h l#x[Ԃyى۽hݹY$GM!0P22z,RJ1:9<ɉ0)%)y6[C~>"mKO1A {rR 2X:$nMkQkv?3D&I5:H).QsgMS+\+!B50` K1DR!#g {n%$C(m?)qB4ʈ8I35v5L,U,OȐN9aQ8lHBψ>^7r-dg;ngG\VBʊRN[0sst/]Gw,b>wSXՆ%#~G#k;w0e2:e[cFZhKR>b'd JXs7b,cibFm;*>} x0}&Byq5{< {)uJ<SY_d0w ;Jz?>&(/D"%*-΃aoc[xc! *"'=CyuERb=EBL/5'=f' ư>vx,R.F+v^'H =+ͳAy0 m;Cy9S6 2{˓39D<IoЧ,9s %d%b=^#bݰ$q"lZnYTս"ksn PheLrHoxG>uClf{ zr$uJϥNkiszLro>t]dTlZ?ob~XG{k{ qeVS^[jWlׁfSnqXqyvEukd:}7;z\]zUVXߕ]dۡWoݚyIi`NuDt9x:};| d[OMrTppTW~e`gTRvOMsV\ߠyfnPZk?hrUlXoS\[n}){S5ic"TUcN {I|,~/.H.wUaAe?Y0^OSKd[v7mm9>I =x\E>Φp\Vpoint05@ݴHM$޵O̳Y\ ظ̻]~߽̒MZ׶V7h Ȼl#x[Ԃkډܹh عY$GMݾ!2P15z.P"I49<;ʉ2)')y9YE~A!mMM1B zT 4V<$OkQlu?5C&L4<J(.RrgOQ+]*~ D40a M0BS!#h zn%&F'mB)qD3ʈ:H64v8J/S/MȒP9aS7mJ?ψC[:p0bi:phG\X?ʊSO[/vuv._Hw+eAwVUՆ%"H#m | x/}%Bys6|=~ {(vM>R\`d/v ;I|A>&(.E"%),υ`rcZ{c! *"'>FyueQc=DCL.6';į>tz,S.G+y ]'H >+ʳAx0 k<Cw9T6 2z̓39E<IoΧ,:s~ %c%b=^#cڱ$p iYp]V"htn QhdKpInxF=sBk_z zp%uKͧQhlsxMrn>t\eSnZ?d`|XGyk~ thZWzaWjZgׁhQntUqztEwhd8~}EЋ}X]vUV[ߕ``ۡZkݚ|LmcQqDp9t:y:{ g^SPtPpsPWbddWOvS{PPoYXޠ|bqMZn[2^NVMdZy7kmvl:>(=C>Trl0point06@ڴHM$۵OɳY\ Ըɻ]̒۽M ZԷV7h ȸl#x[Ղkډܶh չY$߳Gھ!3P 16z0O#H58=:ʉ4()(y:XF}B!mNL1C zrU 6U=#PiQlu?7B&M3=K'.SrgQP+^)~!E30b N0DT #i yo $'G'mD(qF2ʈψEYɊSO[.wvv-`Iw+fAwXSՆ$!H"n | x/}%_yq6|>~ {(wO?Q^a6/v ;H}B?&'.E"%),φ_t\Y}^! *!'?GyuePd=DCL.6';ï>s{*S.G+{ \'}I >+dzAv0 i<Cv\2^MWNdY{7ilw:>'=qB>rtpoint07@شH޶M$ٵNdzY\ ҹǻ]̒ٽM ZҷV7h ȶl#x[Ղyۉܴh ҹY$ݳM׾!3P!06z0O$G58>9ʉ4()(y;WF}C!mOK1D zrU 6T>#PiQmt?7A&M2>L'.SqgQO+^)}!F30b N/DU #i yn!$(H'mD(qG1ʈ | x.}%Uyq5|>} {'xP? Q^c6/v ;H}C?&&-E"%(+Ї]u^X~^! *!'>HxveOe=CDL-6';i&9t8׀5ݔh___O{`SD1K,Yi5l7%Vw!014%~L'ҋфpkM7>;¯>r{*T.H){ ['|I ?+ƳAv0 h<~Au’\3^MXOdX|7hkĊ:>=x B>:npֈ֣point08@״HݶN$صNƳY\ ѹƻ]͒ؽM ZѷV7h ȶl#x[Ղjۉܳh ҹY$ܳGߴM׾!3P 06z0N#G57=9ʉ4')(y:VF}B mOK1D yU 6T=#PhQmt?7A&M2=L'0SqgQO+^)}!F22b N/DU #i yn!$(G&mD'qF1ʈ | x,}%Uyq5|=} {&xP?P^c6/v ;H|C?&%.E"%&*Ї\u^W~^! *!'>HxveOe=CDL.7':i&9t7׀4ݖh`^_N{`SC1K,Yi6l7%Vw!01?%}߷='ҋс~pkM6>;>q|,U.I+{ Z'|J ?+ųAv1 h<~At’\3^MWOdW|7gkF8> =܋B>«j2)point09@شH߶N$ٵOdzY\ ӸȻ]}̒ڽM ZҶV7h ȷl#x[Ղkډܴh ӹY$޳GMؾ!2P05z.O"G47;9ʉ2('(y9WE}A!mMK1C yrT 4T<#OiQlu?5A&K2<J'.SqgOO+^)}!D3/a M/DT #i yo$&F'mB(qD2ʈ:F52v8G.Q/KȐP8ZS6mJ=ψCXՄ:m/_Ŏh:pfH\X=ɊQO[,wvv-^Iw+dAwVRԉ!"~H#l | x+}%Uyq3|<} {%vN=Q]c6/v ;I{B?&$.E"%%)φ\t^W~^! *"'<FwueOe=DDL.6':h&8s63ݔg_^_Mz`QB2K,Yi6l7%Vx!014%|='ҋф}nhM6>;~¯>q{,U.I+z Y'}J A+ƳAw1 i<}As’5W6 2w̓+;F<Imͨ,;s~ %_%b=[#a׳$p iVp^U"hwn ShdGnJmxC:p@fUz zo%uI̪QemsvOrl?tYfPoZ?oa}XGyk t iZXvaTjZcׁhOnuRqyrEvfd8~{Eϋ~U]rUJ\ޙa]r[fܚ|=n cQmDl9p:t;w h_SQuMpsMX^d`XLvSwQkZUޠ|^rIZn9hvOlQoM\Tn)S5ng"XQYfN I,/.H.oUeEe>Z2^NVOdV{7fhQb/>=#HF>gt%Cpoint10@ԫHM۾$NΫYZ$$|޸ص[ }ːȱV߻ õqTosǯjᄉ,xqʂł҈ۦz 鵮Xӷ$&̴$4P-4z)NF3778ɉ.%#%y4WI=mIK1H }X 0U9!lWoSs!~?2A&H19J(0ZwaLP+b-!A32e J.DW"#p n &A$m=%q?0ʂ6F01v2H*Q)KǒM6aP4mF=ψ<[5o+ag7pfC\T=ʊQK[.ort0XFw._=wRTՆ"$zF%g:w.b3 ;eX\EXhNN>b)g HUs9`+\i eDi>*> x,~{'Uzr2|:|{&sH:TU`62z ;Mw??'$1C"%&*̓`lcZvc# *$';AyrESc=HBL16'=f':q8|4ۑmfb_PwUKA¦5K/Zi9o7'H{#0< ̾?%|>':І}mfM8>;ů>vy*S.H+w]'I ?+̯J}0 o;Cx9U6 2{ʓ3:F<Inϧ,:s %c%b=\#aܰ$v pZnYRӽ"oun QhkJtHsxE<tBl_ zv$uIΥMkks}Mrr>t]dTlZ?dfXGk| qeUR|]WjVgׁfOnqUqysEvgd:}}7ы{X]wUJWߕ]`ۡVlݚy(i_NrDq9u:z;}| dZNLrPppPW~b`dSOvN|PKoUXޠybnLZk;hrQlUoP\Xn~)|O5ic"STbN |I},~/.H.tUcCECV/^ROLdZs7lm>ϱ=58J>|dV!Ipoint11@̾Uή%𱞣֞P$%{織׎u2~ܙT目/ɭǹ _XXrǸ ɞ>xrv|Ê߆|.uֹ+ٛI&ݩL;R$3hG>31&//lj%x+PSM8mEE1T[b (O4kzS'3,:'D)4K&2lbIJ+m0"<-pG*C_"' 9k7q7)ʂ0>X'*v)C~ KFŒJ,aM+l=8ω2X+l!]h.nm4aP8͈S;Z5Unt9CBw7L:wKTք&{+fC,V7w']z1&:mHaP:~h\9:b1a jPy x3a{.Jr4v{9p{,p=5c<\69c6a}a=U}?L;5$Hc'EoBy<ƍ|fesn~f]tUCD>K8TiEi7.Hu)014 ͶC,K9 og|:? ?v,R.G+wl} H >+ЫV0 :CH@wFOϾpoint12@ʤTһ)ܓ%*r|ʴ5ou3ϧ1.t*ouÅƽ ʐL`xM^2mmLCJכ>ʉKIׄ@CT0kB52*&(+Ɖ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'[4k1q0%ɂ)8X%vA~GC’I%aL'l77ρ)W"gZn,ˊx-bO7ΉZ1ZF7ktP>wM'6wEV/_z>B??23wX&Y/9q9{_ x>I{Ac J,U;b{;br{8uk54 \6RX ;{?:?#'1Sf=!4/5;|?pAd: *=|,G6Z8~_^=tXI=&n>y$֋ crstnd01@biRϋ{IdFʱIzf}ׯ.ܙ?֊4ƺ"B}ӄ8© Bt3vFt}}h|Ιh:N%rR~l:x/s_*}SsBxn@τtKHo`E |Q7eHG} .rtI,/;}O5^jXy5-/zu<42G*_G*nTd64$tHNgo)[8S5D!|hSuAQF'4e.O; ?^\AD$\+eVtIdk?ewNö6}e*xH{?a\/m MU7%/ALPy^B##`pDMZAgoP=DhU`O+N| 9K]WW|u]zx]Lq9Mo*?cΒs~cr|cE`LZE[bK.^K_\GTiZSfw"t^b^zT`mהORD0BrD?p=Ol@*rJo8ܝ:ٗbګ=X>\fV crstnd02@biRϋ{IdFʱIzf}ׯ.ܙ?֊4ƺ"B}ӄ8© Bt3vFt}}h|͙h:N%rR~l:x/s_*}SsBxn@τtKHo`E {Q7eHG} .rtI,/;}O5^jXy5-/zu<42G*_G*nTd54$tHNgo)[8S5D!|hSuAQF'4e.O;?^\AD$\+eVtIdk?ewNö6}bh=>P|֖ crstnd03@biRϋ{IdFʱIzf}ׯ.ܙ?Պ4ƺ"B}ӄ8© Bs3vFt}|h}͙h:N%rY~l:x/s_*}SsBxn@τtKHo`E |Q7eHG} .rtI,/<}N5^jXy9-/zu<42G*_G*mTd64$tHNho)[8T3D!|hSvAQG'4e.O;?^c?D$\,eVtIdk?ewNö6}<= >ix֠ crstnd04@biRϋ{IdFʲIzf}ׯ.ܙ?Պ4ƺ"B}ӄ8© Bs3vFt||h}͚h:N%rY~l:x/s_*}SsBxn@τtKHo`E |Q6fHG} .stI,/<}O5^jXz5-0zu<41G*_G*mTd64$uHNho)\8T3D!{hSvAQG'4e.O< ?_c?D$\,fWtIek?fwNȯ6};x΀H4:OhqXgkVr̈YՄV=o_hrlXbhk_z?o/Sf/P\ltΉ:`4dX-[V53{HJuK-zxX^D3x2NY{4;Er5nw*WZMD?e*wHz=a\.m MU7$/AKPy^B##^pDMWAgoP=EgR`O+N{ 9K\WV|u[yw]Lq9Lo+?cΑs~cq{cE]KZEYbL.]K_[GTiZSfw"s^a^zS`mדPRD0BrD?p=Ok@*rJo8ܞ:ٗbګ=>j9r֢ crstnd05@biRϋzIdFʲIzf}ׯ.ܙ?Պ4Ż"B}ӄ8© Bs3vFt||h}͚h:N%qY}l:x/s_*|SsBxm@τtKHo`F |Q6fHG}Ά.stI,/<|O4_jXz9-0yu<41G*`F*mTd64$uHNhn)\8T3D!{hSvAQG'4e.O<?_\?E$\,fWtIek?fwNpˉ6~;ỳH4:OhqXgkVŝYՄV=o_hrlXbhk_z?o/Sf/P\ltΉ:_5dX-[V53{HJuK-zxX^D2x3NYz5:Er5nw*WZND>e+wHy>a\.m MU7$/AKPx^B##^pDMWAgoQ=EgS`O+N{ 9K\WV}t]zw]Lq9Lo+?c͑r~cq{cE]KZEYbL.]K_[GTiYSfv"s^a^zS`mדPRD0BrD?p=Ok@*rJo8ܞ:ٗbګ8=>&OSe֡ crstnd06@biRϋzIdFʲIzf}ׯ.ܙ?Ջ4ŻB}ӄ8 Bs3vFt||h}͚h:N%qY}l:y/s_*|SsBxm@τsKHn`F |R6fHG~ .suI,/<|O4_jXz5-0yu<41G*`F*mTd64$uHNhn)\7T3D!{hSvAQG'4f.O<?_c?E$\,fWtIek?fwNpˉ6~;ỳH4:OhqXgkVŝYV=o^hrlXbhk_zAn0Sf0O\luΉ;_5dW.[V54{HJuK-zxX^D2w3NYz5:Er5nw*WZND>e+wHy>`\.m NU7$/AKPx^C##^pDMXAgoQ?EgS`O+N{ 9K\WU}t[zv]Lq9Lo*?c͑r}cq{cE^KZEYbL.]K_[FTiYSfv"s^a^zS`nגPRD0BrE?p>Ok@*rJo8ܞ:ٗbڬ=V=>t`'ZV crstnd07@biRόzIdFʲIzf}ׯ.ܙ?Ջ4Ż"B}ӄ8 Bs3vFt||h}͚h:N%qY}l:y0s_*|SsBxm@τsKHn`F }R6fHG~ .suI,/<|O4_jXz5-0yu<51G*`F*lTd64$uHNho)\7T3D"{hSvAQG'4f.O< ?_\?E$\,fWtIek?fwNpˉ6~;ỳH4:OhqXgkVŝYԄV=o^hskXbhk_zAn0Sf0O\luΉ;_5cW.[V54{HKtK-zxX^E2wj3NYz5:Er5nw*WZNC>e+wHy>`\.m NU7$/AJPx^C##^pDMXAgoQ?EfS_O+N{ 9K[WU}t[zv]Mq9Lo*?c͐r}cq{cE^KZEZaK.]J_\FTiYSfv"r^`^zR`nגPRD0BrE?p>Ok@*rJo8ܞ:ٗbڬļ={X>lQV crstnd08@biRόzIdFʲIzf}ׯ.ۙ?Ջ4Ż"B}ӄ8 Cs3uFt||h}͚h:N%qY}l:y0s_*|SsBxm@τsKHn`F }R6fHG~ .suI,/<|O4_jXz9,0yu<51G*`F*lTd74$uHNho)\7T3D"{hSwAQG'4f.O< >_\?E$\,fWtIek?fwNpˇ6~;ỳH4:OiqXhkVs˂YԄU=o^hskXbhk_zAn0Rf0O\luΉ;_6cW.[V54{HKtK-zxX_E2wj3NYy6:Dr5nw*WZNC>e+wHy>`\.m NU7$/AJPx_C##_pDMXAfoQ?EfS_O+N{ 9K[WU}s]zv]Mq9Mo*?c͐r}cq{bE^KZEZaK.^J_\FTiYSfu"r^`^zR`nגPRD1BrE?p>Ok@*rJo8ܞ:ٗbڬ==d>;rYNV crstnd09@ciRόzIdFʲIzf|ׯ.ۙ?Ջ4Ż"B|ӄ8 Cs3uFt||h}͚h:N%qY}l:y0s_*|SsBxm@τsKHn`F }R6fHG~ͅ.suI,/=|N4_jX{9,0yu<51G*`F+lTd74$vHNio)\7U3D"{hSwAQH'4f.O< >_\?E$\,fWtIek?fwNpˇ6~;ỳH4:OiqXhkVs˂YԄU=o^hskXbhk_zAn1Rf1O\luΉ;_6cW/[V54zHKtJ-zxX_E2w3NYy6:Dr5nw*WZNC>e+wHy>`\.m NU7$/BJPx_C##_pDMYAfoQ?EfT_O+N{ 9K[WU}s]zv]Mq9Mo*?c͐r}cr{bE^KZEZaK.^J_\FTiYSfu"r^`^zR`nגPRD1BrE?p>Ok@*rJo8ܞ8ٗbڬT=\>&olP֢ crstnd10@biRόzIdFʲIzf}ׯ.ۙ?Ջ4Ż"B}ӄ8 Cs3uFt||h}͚h:N%qY}l:y0s_*|SsBxm@τsKHn`F }R6fHG~ .suI,/<|N4_jX{9,0yu<51G*`F+lTd74$vHNio)\7U3D"{hSwAQH'4f.O< >_\?E$\,fWtIek?fwNpˇ6~;ỳH4:OiqXhkVs˂YԄU=o^hskXbhk_zAn1Rf1O\luΉ;_6cW/ZV54zHKtJ-zxX_F2wj3NYy6:Dr5nw+WZOC>e+wHy>`\/m NU7$/BJPx_C##_pEMYAfoQ?EfT_O+N{ 9K[WU}s[zv]Mq9Mo*?c͐r}cr{bE_KZEZaK.^J_\FTiYSfu"r^`^zR`nגPRD1BrE?p>Ok@*rKo8ܞ:ٗbڬ=L>=hUcrstnd11@biRόzIdFʲIzf}ׯ.ۙ?Ջ4Ż"B}ӄ8 Cs3uFt||h}͚h:N%qY}l:y0s_*|SsBxm@τsKHn`F }R6fHG~ .suI,/<|O4_jXz5,0yu<51G*`F*lTd64$uHNho)\7T3D"{hSwAQG'4f.O< >_\?E$\,fWtIek?fwNpˇ6~;ỳH4:OiqXhkVs˂YԄU=o^hskXbhk_zAn1Rf1O\luΉ;_6cW/ZV55zHKtJ-zxX_F2wj3NYy6:Dr5nw+WZOC>e+wHy?`\/m NU7$/BJPx^C##_pEMYAfoQ=EfT_O+O{ 9K[WU}t[zv]Mq9Mo*?c͐r}cr{bE_KZE[aK.^J_\FTiYSfv"r^`^zR`nגPRD1BrE?p>Pk@+rKo8ܞ:ٗbڬs=7>s_Zcrstnd12@biRόzIdFʲIzf}ׯ.ۙ?Ջ4Ż"B}ӄ8 Bs3vFt||h}͚h:N%qY}l:y0s_*|SsBxm@τsKHn`F }R6fHG~ .suI,/<|O4_jXz5,0yu<51G*`F*lTd64$uHNhn)\7T3D"{hSvAQG'4f.O< >_\?E$\,fWtIek?fwNpˉ6~;ỳH4:OhqXgkVŝYԄU=o^hrkXahk_zAn1Rf1O\luΉ:_6cW/ZV55zHLtK-zxX^F2wj3NYz7:Dr5nw+WZOC>e+wHy?`\/m NU8$0BJPx^C##_pEMYAfoQ=EfT_O+O{ 9K[WU}t[zv]Mq9Mo*?c͐r}cr{cE_KZE[aK.^J_\FTiYSfv"r^`^zR`nגPRD1BrE?p>Pk@+rKo8ܞ:ٗbڬC=( >QU^a3crstnd13@biR΋zIdFɲIzf}֯.ڙ?ԋ4ĻB}҄8 Bs3vFt||h}̚h:N%qY}l:y/sf*|SsBxm@΄sKHn`F |R6fHF~ .suH,/<|O4_jXz5,0yu<51G*`E*lTd64$uHNhn)\7T3C"{hSvAQG'4f.O< >_\?E$\,fWtHek?fwNpˉ6~;ỳG4:OhqXgkVŝYԄV=n^hqkXahk_z@n1Rf2O\kuΉ:_7cW/ZV55zHKtK-yxX^E2w4NYz7:Dr6nw+WZPC>e,wGy?`^/l MU8$0AJPw^B##_pDMZAgoQ=DfU_O+O{ 9J[WU{t[yv]Mq9No*?c͐q}cq{cE_KZE[aK.^J_\FTiYSfv"r^`^yS`lגQRD1BrD?p>Pk@+rJo8۞:ؗbجx=>Ih#ͽcrstnd14@biRϋzIdFʲIzf}ׯ.ۙ?Ջ4Ż"B}ӄ8 Bs3vFt||h}͚h:N%qY}l:y/s_*|SsBxm@τsKHn`F |Q6fHG~ .suI,/<|O4_jXz5,0yu<41G*`F*mTd64$uHNhn)\7T3C!{hSvAQG'4f.O<>_\?E$\,fWtIek?fwNpˈ6~;ỳH4:OhqXgkVŝYՄV=o^hrlXahk_zAn2Rf2N\luΉ:_7cW0ZV55zHLtK-zxX^G2wj4NYz7:Dr6nw+WZPC>e,wHyA`^0l MU8$0BJPx^D##_pEMZAgoR?EgU_O=O{ 9K\WU|t[zv]Nq9No*?c͑r}cr|cE`KZE[aK.^K_\FTiYSfv"s^a^zS`mגQRD2BrF?p?Pk@+rKo8ܞ:ٗb٬=&>;fq0crstnd15@biRϋzIdFʲIzf}ׯ.ۙ?Պ4Ż"B}ӄ8 Bs3vFt||h}͚h:N%qY}l:x/s_*|SsBxm@τtKHn`F |Q6fHG}Ά.stI,/<|O4_jXz9,0yu<41G*`F*mTd64$uHNhn)\7T3D!{hSvAQG'4f.O<>_\?E$\,fWtIek?fwNpˉ6~;ỳH4:OhqXgkVŝYՄV=o_hrlXahk_zAn2Rf2N\ltΉ:_7cW0ZV55zHLtK-zxX^G2wj4NYz8:Dr6nw,WZPC>e,wHyA`^0l MU8$0BKPx^D##_pFMZAgoR=EgU_O+O{ 9K\WU|t]zw]Nq9No*?c͑r~cr|cE`KZE[bK.^K_\GThYSfv"s^a^zS`mגQRD2BrF?p?Qk@,rLo8ܞ:ٗb٫4.=>V+f{0̳crstnd16@biR΋zIdFɲIzf}֯.ڙ?Ԋ4ĻB}҄8 Bs3vFt||h}̚h:N%qY}l:x/sf*|SsBxm@΄tKHo`F |Q6fHF}Ά.stH,/<|O4_jXz5,0yu<41G*_E*mTd64$uHNhn)\7T3C!{hSvAQG'4e.O<>_\?D$\,fWtHek?fwNoˈ6};x΀G4:OhqXgkVr̂YՄV=m_hqlXahk_z@o2Rf2N\ktΉ:_7cW0ZV55zHKtK-yxX^F2w4NXz8:Dr6nw,WZQD>e,wGz@`^0l MU8$0AKPw^C##_pEMZAgoR=CgU`O+P{ 9J\WV{t[yw]Nq9No*?bΑq~cq|c7`KZE\bK.^K_\GThYSfv"s^a^yS^lדQRD2BrE?p?Qk@,rKo8۞:ؗbثL=g>/0crstnd17@biRϋ{IdFʱIzf}ׯ.ۙ?Պ4źB}ӄ8 Bs3vFt||h}͚h:N%rY~l:x/s_*}SsBxn@τtKHo`E |Q6eHG} .rtI,/<}O4^jXz5,0zu<41G*_F*mTd64$uHNho)\;7T3D!{hSvAQG'4e.O< ?_c?D$\,fVtIdk?fwNȯ6};x΀H4:OhqXgkVr̈YՄV=n_hrmXahk^zAo2Rf2N\ltΉ:`7cW0ZV55zHLtK-zxX^H2wj4NX{8:Dr6nw,WZQD>e,wHzA`^0l MT8$0BKPx^D##_pFM[AgoR=DgU`O+P{ 9K\WV|u[zw]Nq9No*?bΑr~cr|cE`KZE\bL.^K_\GThZSfw"s^a^zS`mדQRD2BrF?p?Qk@,rLo8ܞ:ٗb٫k={w>#0Խcrstnd18@biRϋ{IdFʱIzf}ׯ.ۙ?Պ4źB}ӄ8 Bs3vFt||h|͙h:N%rY~l:x/s_*}SsBxn@τtKHo`E |Q6eHG} .rtI,/<}N4^jXy9-/zu<41G*_F*mTd64$tHNho)[8T3D!|hSvAQG'4e.O;?^c?D$\,eVtIdk?ewNö6};x΀H4Ž:OhqXgkVr͂ZքV=n_hrmXbgk^zAo2Rf2N\lsΉ:`7cW0ZV55zHLuK-zxX^H2wj4OX{8:Dr6nw+WZQD>e,wHzAa^0m MT8$0BKPy^C##`pEM[AgoR=DhU`O+O| 9K]WV|u]zw]Nq9No*?bΒs~cr|cE`LZE\bK.^K_\GThZSfw"t^b^zT`mדQRD1BwF?p?Pk@+rKo8ܞ:ٗb٫J=m>y$c crstnd19@biRϋ{IdFʱIzf}ׯ.ܙ?֊4ƺ"B}ӄ8© Bt3vFt}}h|Ιh:N%rR~l:x/s_*}SsBxn@τtKHo`E |Q7eHG} .rtI,/;}O5^jXy5-/zu<42G*_G*nTd64$tHNgo)[8S5D!|hSuAQF'4e.O; ?^\AD$\+eVtIdk?ewNö6}e*xH{?a\/m MU7%/ALPy^B##`pDMZAgoP=DhU`O+N| 9K]WW|u]zx]Lq9Mo*?cΒs~cr|cE`LZE[bK.^K_\GTiZSfw"t^b^zT`mהORD0BrD?p=Ol@*rJo8ܝ:ٗbګ{95;crwalk1@dfRwIeFܴIΐwhz첦.?錄4ؽCz熥8ӫ Cp4sFǞvyyh~k:O&nYƞzm}:z0pf*yTpBzi@pKIk`J V<kHM҆2xyP,2BN9cj`9/5~uC86G.dJ-oTn84(zHUnn-a=Y5H$~h[{CYJ'9k.W?EccGH$d.l[tOkkFlwUuψ;B~ҀO9ŎAWnr`mk^wЂc_DvchzoWkmkf|:q!WgT_wxЉ6c&hX_Y:!J:xI/zX^53uP]u>GuovX`4E;cyF}b[p#P[% 9Mh'!%Zk$NJ?bo1>BlDaK>1|5H`UXx[yw\-r91p'?gϔckx_EOJUeHbD?UI_UGThZSex"t^b^{T]rٔ0SDCr$Ap'0m@t*p::bﭳ<ӚgCè*ײ`潓Kڔpޘ⭽ 2ε#đGԌbɗ@ܗʵ?g*P.B)НŅ_(F%>*êR<ǰA_xXo۱1t>&B%ھKˎ*ى*q*{/Ʋ YL%ٟ/"#HHص!#ɑzϢzNzm$ϛAvauYr_w_x$tސYrԀErƖ>|DYDRÜFؙiXIxʍśɈЈՆքrkщmh`X|iubŃm}iɆldỲYʙSוeˠlЙ̚QQ|Cw7v9r:rx|ܕ萷Vp\W`o]Rwvؙh⑍\Ϡ^W[HiSnbd`]^oĞ)N҅5ܝ۞̆ &,Ǚ/.ԝGՏ.Vn.e 0RwGK{p7qxf >+=>sٔv[0crwalk2@`nR~IbFڮI΋~d謭.햞?爋4ַA䁬8Ҧ Aw2yFǘrhzޗf:M$uYƘi:v/w_)QwBuq@߁wKFr`$L,XG"-mHW˅?x)zZ,?CyOEejh9<6wuMWBRܭFќmXH}Ljj͈jԄqz}΁mh^X|iubĀm{iǂkbY|YŔSוbˠiЙ̚PRyCt9s9o:ouxܕ萶TpZW^o[Pwtיf⑏ZϠ[U[FiQn_d^]\o*Ń5ܝ۞Ȃ &,Ô/ڌ.ΘGϋ.{Vac/>ov^Yn7px%>>C>nҘb$3crwalk3@]vR܄I^F֨IɆ`䦵.鐧?⃔4Ұ>}8Π >0F’nhvڐŅc:J#}Ye:r-f'NBqy@}KDz`O} Z9oHJ 0y{L,1EqS6fj\9-8qu~ÀJ6<Sqq\pkVxYYAuhh}uYcoziWVqI[hJW^lvłOaNkYGbW7MIc{M-~Yu_={NUitRDGrPrwF]hkMCeF{\{[fcJq!^`R%JWLPyh\##wy`SsLl8m=YhnhR+i"9`]nWs]y]hu9hu,3uБxcf7zV^EukO.vM_sIT[N}x"u^c\U]זlXDLIr`GpZlp@Evfs8ċ䋺p1͏gퟰF峻/֦_宦>ЊŊҌƇ֠ DZ #صGхdˑCыn̫3`)95%>Ðч^(0+"K¡R)$θ5B8y?pɯ ̺?`ɻKÜ/΋$w*{/ Hޔ-ϫ2$&EOͽ#$ړlWCPөFǙmXK}yyy}сxpzx{Ƀ~nh\X{iub}hzh~y`YyYN}ו`ˠfϙ˚PRvCq9p9m:mrvwܕu}萫SpXW\oYOwxqיwd⑂XϠZT[EiPn]d\]Zo*N~5"}ܝ۞ &,/҈.ƓILj.yVXcT7SshXs9qzS>&> >X&Hjecrwalk4@[zRԁI]FϥI„_ܢ.?ڀ4ʭ<z8Ɲ =/Flhtҍʅa:I"Yc:pƒ,_'MBo}@zKB~`Qx [7p|HG .z|I,/EkS5hjX9+9mu9D2G*hF*f]c;}9$|HNqn)e8^5?+wyS}ARLtI4o.NI?hcAOw'X7o`tInkAowMv6<G4:PsYqkRxUU=rig|wY_pzhW[rP]kQY_evTaUmZOdX4TIi}N-YxeB}VVloZFHrXswN^hrPDeN|`||chcRr!``Y%R\MPrec$#{|gUyPn8t=]islT+q"9d^rXr]z]pv9pw-3vАrch7Y`E{nQ.{N_xJT\Syu^d\U]זsYDTKrgIpasq@Nwnt8܎ɋڈpۛ;ƌg㛶Fޯ/Ѣ_ߪ>LJɊɉʇ͜ԅ#ӲIʄdƐCȈnǨ3a)<5(>Ӈ_(2*%LR+$и7B8{Dpͯ1ʸ4`ͻK2ы&w*|/ Hۚ-̯2$&FO#ږo¢?ztp!צ6}d\|Dyfx%|٘TwՋ8wˠ8?WDRשF˙mXHxyyyՄwπspzrzǃzng[X{hub|hyh|z_YxRQ}|֕`ʠ}eϙ˚PRuCp7o9l:lqtq}ܔp{萣RpXW[oX|Nwrpיqc|XϠYSZEiPn]d[]Yo)N}5w۝yڞ~ &,/ʇ.G.wV\c[9Uso[t9sz=u ><>QSF crwalk5@XvR}IYF矡I[.?|4⧯:v8ޗ :-FҊghpƅ]:F!~YЊ_:l*_%JBkz@vK?{`E vO> bHPw 4moS,4:S;[jdt50/uC78G/\M#v`q17)oHXdn.Y?R5I!k^pA\B&;a.Z<H[cIC'f+bTtS`kIbwYh͈=wEqрR|:ƎCZfceh`ijf{لdFygg~wRldhkMlĉ΅٣ г#묣G݅eӓAnצ?s)D3/>ׄΊw(;,+LјR-$AƗ@:HpƵ1Ҽ?ޙ^Kü0ȗ.w*/ H׬-ļ0$&\O #ڙxڍGw}#γ? u@Vx(|ұcu̘BuBSPZPѯFĠmXHy{k̂kkۉu׉ezmςggRXzcrcyixawhVYnTτNrיVˠ\ϙ˙PRkCe7d9a:afisܕq萼IpOWRoOFweיY⑑OϠPJ[P^=m>>^\>D'[-eVtHdk>ewMö5};xπG3Ž9NhqWgkUr͂XքU<l`hpmX`gk]z?o1Rf1O\ksΉ9`6cW/ZV44{IJuK-xyX]F2wj1OW{6:Dr3nw)XXND>e*xF{>a\.m LS6%.?LPw]A##^pCMXAgoO=ChS`O+M| 9I]UWzu]wx]Kq9Lo*?aΒq~cp|cE^LZEYbK.\K_ZGTgZSdwt^b^xT`kהNRD/BrC?p<Nl@)sIo8؞:՗b֫e<=o>a^ؐb crattak1@^m`夂+k:Ɯ,اxOqwŷ֧0ࠐ<ǥIΈD}26ЀkɌ{_%X~l>$]|B Ldr҉4_>FHf?y]KBLTGNNs!L=JPpWW|#`R_3e"_8k=Σ+p; Ҵ#ӛ'p@Bi*ڥf-ܢ_%ą[(ͫU׭YHGE_xx 1x>M4<Kō+߀>a*t*ߩc%.,ٚ+$!GH ܮ!nehzc$ŠIƱwuXhSMwfrfҬxͧ'p݉!^soWqˆAG]CTFmԤWIٮhȇφtքohjp{҉p_hVŸomfbzmpmZXzRQyڕ\ϡxdԛ˙QQvBq7p9m9ntyoo쐜KpQWXoUxFvpqܛoaxRԠVL[9iInXdU]To)N5 "tߕwܞ1&,/3G.yTY7XDiZ&}rp9kw>~=m[>qa crattak2@bYLs7է(wɻ254ٴPJ76p&y† h~*X$';x>cy8Ve|{_}CI\=bW{L|x{hhI&wt($;6aV$aj<1:%ar:6w G aTgf];<zyI5t[4Yy0Isk>?PLu&*m/FD3feBHq Q0_q Oiu+qy'ow+%|$9qq;ry102!S]k^c\>uyMx[LZB+KD*BAJOHCBC:>#Kf:]Z< fp\ay~PZyVD WutrN)XkOOPc%,eOwPh^X_OeMnGUSS;RHG͗_!bJueCcQop=PZc>?*it0ms>_k;UsTOkn~p[aN`UexpIDT4re1p` og@Oqkl<8e1]ɳ?ݾ%ҤJޭL6ȋ !&oḊ4l/^2S/d,ܳQ#N/ǡIBȴCD mc 15;F9Kϖ%1[ /z/^ )%-"NHfo |wnRgl!͙Bw`|Vood xж"q{"`TqwG[ʖ;EYESFiշXHs͉|oZMMyJUqkrm[v{jnuny{yɂ|r]SQPӝZrʠRzPuPbNWSC9:;y\R~E֕BqapqgY}o[kOZwGPDwܑQgWwloa\kNiq^npom^lnx*xO0`YJ֝JYמ{ w(v,x/.I0JqpEEM1qW mf9hsW>X=J>z>Әb crattak3@c}WKt7آ&wζ034ݰP8J7E;z$j;h)X#&c7We}x_zAI\=pWN!iuH/{ x0$94lV)ejD<6$nr;7$G"cQqfa6<|ISH{I-o%JE6heCHy'T0a Pyu3uy.sw4)%|,!#?tqDvy:::'[`kee\IyySzL`:-R:,ILńJVAEH9U[>2orZf{K]yDC Zuyr>/Fit_g=VtTOonp[aN`aky^GDB2rT0pN ]f@ɮ=D~;iY 13/C5KҒ%)[ *v/ٻ`+%-!HHu y {riKsh!В<u]~UondǾ xɴ v{"l"QrrAqʏ8{?YASסFiɷWHw̉~gYXÃU`uks[yznyyɂy]RQ^וhxϠ_ҚΚqNdQC99< pcSܕQ搂hpnWyons_`wUؚS|_mӠv~h\|Uignxot]to*N0sgX۝Xfڞ &,/.G.Unh7D?7rVnf8gs!k> =/>b crattak4@c[Ƿ}Gډ4۝|vܰ-͟+˽)ܫVέ9KM4̞1)"ʇt ў2|0Z:G/lW /e;dDсy_ϟvKRc'3wXp!EyN$_zG*u l)$H3uV,ajB0H"wsK7"G']fyftC5!tI=y'X >X/\jE=bO&1k%ZI[.GK&=I%-hAB]?2msZa}<^y'? Tt}r(3)ftKP@$-fsKs0TT"bGwF.!#AFQVIџ9 -]Cu8=OPdD?HdJB?+Bq9NYYRxvhz}mi@k 9@`"?\yVڹzpmXSET#EeQE;3Zs?U[?VkSNhn"~pZa{N`eezCDD%/r8-p2 Cc@m>g<Ƚ:ȷEξ1\׬?%ٜJ(d !&nDƇe_*I/>* Z,A-/=G>gQ3b crattak5@d~[ºzGӇ4ܝzxذ-ʝ)Ƚ)۬U̩9MJFȜ<"l͜2z.[2=0kQ 'e8cDɃs_ȢqKQc,.wXf$?yxN'ZzG&o d$!N1vV,]j?2O!wsR4 G(Xnw_{E5"oI;w(S BU/dlD=fP3f%`H>dEUG r8I;|r,sj)nw+({!?rs?uj3ā54W^k`d[DyLy;d$.W#,NIʄ8[)GL!]?2kqZ]}7_y"= Pt}u$4$dtIL<#-fqGs+RTaCxC)"a@k9f<ɹ:ȴeɼ1\Ԫ?%ٜJ(d !&oDŋD`.J/?* Z,B-=/ʘO:ĩ9~D{;eOo߶ 14%C*Kя)܂"[ *s/̸ TL}%% #CHy w ymԤeDse$̍7u[sSdmd xup|"l~Nrn:Ŋ4{=>hb crattak6@exXxI˂F۞yyү/ś)ĺ=٭TȢ9NEF˜< lǘ2v+]*3os=qj2Ł32V[k^a[BuJv7e!.X+OH˄4[&FL%לV(d !oDËD`.L/A* Z,C#>%ʙO;ç:~D|;dQߴ 14&C.KϏ*܀#[ *s/˶ TL~%/ #DHy v yoԢcFhe!ˋ8u~[pSdmd xtm}"l~Orl;qÈ5{RțFܗiXHvɉΆiYVǂǗ\oko[zlytpk~yłv]Pș`ڕnpСcvԚΚrNeQC49rEqv}g7fzr>n=>-5b crattak7@V[ͪ{G{4͛{hί-о+ѯ)ΪIԠ5=A4ҏ0ʊtū א2o#Z-Ƈ9/DŽlJ "e/WDtw_ՑtKEc('zXb"8ytN%S}G+g _*!J'xV-TjD0JzsN+"G'Qh|fx55"fI>l'L AL/^jG=dB&3]%\<=ZeS<l+D5u1hj-dw2xĄ*y ~|(CgsEkj:~Ɓ<:!\Vkc][InOr;f&-Y%*PP̄8[+EM#:L%*dBAZB2nk[\:cy$7 Qq|u'7&_uCN? 0flFt.MT [D{E+z" C=Ak9IYURwr~gwoi>b 9>Z#?YY˾rbkYVES!JeOB>3WqBUWAVgTNdo"zb[axN`ic݀B<D"(r6%p/ B[@d<`<ΰ:ΪEǾ1\ɫ?ٴ%˚Jۤ(p !ܠnBËea*N/D* Z,E#A%V<ĥ;~f};cT 14)C.Kː*}&[*s/β Y+%%!DH { zq֞bIhe$̉:u[sSdnd xuq"r}Qri>qņ7{>Y>R˗FޒiXIøwȉ qa]wy\΄bhkf[ul~opkxjÂnYRQgڕugϡknԚΚyNlQC49u=>Ka crattak8@ZuYϚ{InF{d/ج=՜3Ʀ?ב9:3FҀ:l̤ zZD&3e/V?>`cIC'd-Z Jt?kj8iwCxɉ00|:)-KlqQmkK{ʉMJ0f_hmiXWojYz?i-F_-CU`~Ή:]2YR+PP,0tEGlF-uu[^B!my-HV|u0.8p8 Ig@$oDl:֨:աdѶ1Z׺?ͯ%UʝKdĝŵ յ!ΚGdBDf*V/L*ʅ\(߭K#I/TBūA~B u` 1~?1C4Kʑ/ޅ-[*v/ұ`L%ܝ/"#FHޯ!!zۡsShf$̒Bu[tUMzerex#tvXrxHqŏ>{CYBRЛFmWIȷzĈ̇щrӄmvymωpmmfXzmtpk|mjmYRQwٕgΠynҚΚQ|QCƅ49~<}~kޛi鐢]dbWk6a|Rwnwښnh{]Ҡg]\LiZnn6i]ho*N.oޕp~ܞ &$/)G/V}r:E2HsO;{{l7kw4m>_=m>?"b crattak9@biRϋ{IdFʱIzf}ׯ.ܙ?֊4ƺ"B}ӄ8© Bt3vFt}}h|Ιh:N%rR~l:x/s_*}SsBxn@τtKHo`E |Q7eHG} .rtI,.9~S5^jXy5*-u<62G*_BtYc.7$tHNgo)[8T3?kSuAQB&4e.Q; ?^\AC'[(eVtIdk?ewNö6}e*xH{?a\.m MU7%.ALPy^B##_pDMZAgoP=DhU`O+N| 9K]WW|u]zx]Lq9Mo*?cΒs~cr|cE`LZE[bK.^K_\GTiZSfw"t^b^zT`mהORD0BrD?p=Ol@*rJo8ܝ:ٗbګχ=>0crpain1@S`=Y8+Od <:tCʊv>Q(oqY `xքh}Rk#`wɍmuv{yvv.r bd/EdeDwF:^yURypv taL |{-0gZKz-;-YX-ap(AF^fJSu=$l*v15Dgz24Clt,"26d'e6on%=Na [xW›*q*y$')αΤC~zP~]0yEXRiK7bM3V5L[OLSKAP"ǎOdD_aI-Z\g]tjT4QrVAqUWwM>N~i+?EMbUu^JcPUSmK˄Us"QSHPAM]z"#l_a3m*f8l=SbgLO+gi!5XXbRig~tqg[9f[+3exM"oi{ob7q6ZEnOL.l{J_hETqWSor"yi^Z~Q`Qo|"k8DR&ra$p\kU@M\fY8ڇp Z7/B?m؉~ #ƺLmE}ق8m/ٞg-ڜc%q_,ΧXר_ UNFdqv<9R4#<Kơ2?\/w/%c0//ޤ2/KH\s$)lbh g!ŌH#r\yUMpqpڽwӱ,x!_"[rpTZǎ>GYDTFiٹW I߼!gmԉh؄`QC?>Ek8VchZholfhmЈ6TD|^Tsf[IEOLJ^~ofDiJ;m6mv8x}:}<LS aL`-0ُ6O0{<<WHO'CO1~3ǝ*є.,ȑ0Q/ڒYzm_yBF{y\ s9hKm.pCHayegspV|lG`kSfcKpj?s`x[7WMsb6m9kzf,>\=t>MqX[Ucrpain2@TT=W8LOa199 xAyŠy>)6l_izzІaMp#`tm~t}},y ei-EefBtC<^s`Rz|v lL!||(-+b_Hz5;'U]+\v!(;C__JN:L+l*z|<1Blz7?Bdt+$07^ *c4hq%=Irh"x%""03z.11#IwhT|]9yHXOiNCbP=U?ƁHZPUROJM%RlAalJ]]c`(vj\APr_,Bq^`wVJNqox{KYwHiPHfvtvox|Jcenl ~.hIo%zKdky|F|7qslUptdBlbn^``Ä[7_OgcG|n5lzс>l=>zcrpain3@YxT>X7INa0‰4}7?x ?,Ekuzxņ_J%Ygr}+f,IDk|@wC?~`eslx,|I5ņ!;%*ViO'qzI:%Ji0P,I!v;8d`SB8!KpLJ #IpCl4n*e/]*ц`(ٰV#W-RHŭHB‡ v14HF9K/=Z*y/c#)ݎ%Ѧ2$!KHԷ#!|dzg!ʗLȵw]{VM}eueԯxϫ'}ގ{ar΀WqǔEJYGTFmթXHڰhɈ҉քufavxádpsr`˜za|ua|myby|[WgT{VoٞdvΛbӝQGNZcQpVnXn]zxrhfqUso^QOmhg~Tnݑa۝YZvәQl[ɠbQrrPzIh~RXNh4Oe8%tMz"iL3{E=>`# crpain4@biRϋzIdFʱIyf|ׯ.ܙ?֊4ƺ"B|ӄ8© Bs3uFt||h|Ιh:N%qR}l:x/r_*|SrBxm@τsKHn`D |Q7eHG} .rtI,.9}S5^jXy5,,}u<62G*_E!q`d44$tHNgn)[8T3CkSuAQE&4e.P>?^\?D']-eVtIdk?ewNoˈ6}e*xHz?a\/m MU7%.AKPy^B##`pDMZAgoP=DhU`OLN| 9K]WW|t]zw]Lq9Mo*?cΒs~cr|cE`LZE[bJ.^J_\FTiZSfv"s^b^zT`mהORD0BrD?p=Ol@*rJo8ܝ:ٗbګ1>횎.=death301@Q^ٚ"?cʓЎaCUԵaڥ;uރirϼVā~W`Zѯ2m]z=àba ۪ ٛ:wimpȫ 9莌!ugmbݖlvlO&tc#^)āqjulK3v)gCxz{a4(<a_A0z8 !;',kirJ &k4$ t/?) w g8q w|su(Ɗ{(db+ʉn4f^ hJ+}Q+[H&u=6w<̇X7ρ><|;@A3RۅY8gDԉJ6>kz.;m.3hKKӀkI{u5>,4kU9Yd28oI?c9Go?.De0 In6^Q4p[(ZPgNGA)mGz[=RU-qoPwhO\A5&-uhUg+\KZ:?(OX4gA 0T1(LZX{Q9HK!X_Uiuk9k4FI$XJ"]k7c1lW([2~W3"gY{lddynPRy~5z?zq@(oXw`KGt.Ev@);DJN=( PH%Xb,HdIFͺ4FO7ðo*_ A,/7n *Sv,u"S!G :nwMCSV>J*-R3 #%1K+Mu׶3e"y"B eZM(`{b!?)v< Ti,Ik]X`_Yd8b8-TK$ %!UN2KM+>$%` i=BrqFd06K7_FA4i.: =\=g1>մsP7sAf2_:*Sk adtˇ6aʂ}'ee hNvb\Ur18t.ЇU&΁=-{>2P FۅN8e5ԄIY9?0>F] eI=(>?7YK{HAp=5$XS}W`ra9a4V;"':2_a8Yz1e`'Q<M<"\X_aNesnPTsx5t?tq@(fYpH? O]N3H=+AC>E"uh:"I[dϽ,JaIFѹ4BO5űr*bC-Ž/4o *Uw,w"U!K";mySL"NSRS*-M=Y&%2Oe%IBw4cS+{",@s6fiEAAxd"|AkEskWLz|;pW{Wq.3`(pGX߶#`ެ(H׹\ǙIE3Ь>˖N4ӟlų*]A,.2h *Qp,V)":e],$IS}1,*q-=N$N4O p^1vu"` TYazWMY,A2lC">[,I9]fI2Tm hepS_SR.q38iwiWy0y5c^n0X:XyL8 +#?cG5~-/i4RtOG΂>I}fLЉHJbt{1po;] l1P ROEekFZ=&tT0PZ(W^N#<(1Jkkl*4U85death304@hQ{胓 c͟|~vZjִx""avhi#zbWq!{qޚ2O[\?Ɋi͹ ܄YMlfnΘs Ǩ9ue Xgrs|hQLi*qVBN j{aV /f5]҉ N!G+)}=2>iƚ.ax3%]Ř{"679 Ň8 ƒ#Eiu ucq,ƌ%ˉtŃd%{g+}XAՅJ8,΄6o'Uz#mkl7~2cu*#l*_29- W86r%D1$X6n]S%]3&=i6MV#g3{z*Ss=w4]fM=}|+kb&dzpx`ɔ.Ҋv46&iq"i|Zy8c\(n>KX)y!#uQA ,1&0;\n_WYi]RuGтgI}LӉoJi{1o; 1w xOElF=QuTZPZSW`t#d(ZJka~eCiV5kEO{SId`V`-wAO 9PMUAN[KDq+e$P =hiF'?O^2,4_q.Ԍo7e >=W>2UFdeath305@nQ臓 p͢҂zZwֶ""nvmi#fWu!qޞ2T[a?Ɏiͼ ܈^MllnΛw ǫ9yk ]gvwnQ=i*qVBN jaV /f5]ԉ N!G+)}=2>iȚ.ax3%]ǘ{"679 LJ8 ă#Ei ucq,:%̇ǃq%{t+XAׅJ8,Є6o'Yz"mhy7̀6mu+ k*_-62 W90nn'D)$]6n]H$`&x=i<MGg3zp, Sog=w4)9OCIPD.J{A'i?$Su_ea9a{4CB{; amW~1bB&MI"!]FcTu\Ptu{5vsCv_@`fr$H":Y,3 5E9J?G߽#޴({ǢIxiдtˠNkӨź*udeh *,a/$& pv8)"|Sg3y:*n%KN$ O-O \*V`OMK+6/a[tL+;?]^=.2yUpwgΔ.׉y40mdigy V`1$F_s3ًr74$>l=B(>OOPHQdпdeath306@k{!ЩՇpqٶ !ô}Wr%ǻWV*r.5.@˄gϯ0/kdḎmBsуoI8ɖETp?+Dwh z]^I {Z58rxQN n oh%<0o=_')鎩|5,2&B|*B"g5t_y8,^tw 4wfICE )fi x]r/E'%){F9ND.7e'Phii9Ȇ0mx!,jm/Yx-|43ZD3Ud5_$ [6wp$]= ^!x=gA ;i/uk[$S^ g;y;qY)lG3C^NǀHtU- F*>$!OOGE10EF*V1$ N||[rcsFb]C8+c2"^|U{킚3h9W}I$_IbWrer~s?wV@uTTe~o1 # K6 .01#>' G%IڻɝOһְͦȪLȷ/ í߻.ⴣLҺ2ͼ&[1$, ƪu{>t0 ^<kBङLX,J νz$vO&O $s[$)"yJTH'C6 1>eB6.AY^74lz9H(i}sݗt=Ջ\>)aU(hx7\%Zq1kLrWz9>!#y02\Ѡ܈˄k~TɀJƇKɉO:Na3t5ݝ/䊶3ן-SfnBAyYTg[]ۙ,˛B_n7u1y Ђ Ȅ䎮{R͔QI\ld|FwhY`ҘK[mHi;mXXMb]ե/ޝUҗ2T˝¦Vʜ ۣIշ,ͩ#/Kߗ-mBAS)Y=C`V>ӍUD \>X=;=]LEdeath307@ys$μљ|tuּ-!$ʼnV}ݎ(˸:U:Kw|>; Ɓqˢ3pLT\8&Ȯp3kc2i|_Dh%JG~d$['h^TqA]Dw(^Xgzbael.tC2 syH^E.2k>܌r?l03/goF働LFb|']}o9F?YWKJE_o0YZ j w4^,E!8)ć+Á{IxDOm]378\'Xhn%#ag&"g?<(ye(?ja+i5SClU8NYo`4IcrDU\4cb\4Y( ^&;gUM'-i(~m;1N>a6l8k;:m. O:|RԆF9E LJ SmZ+5G;aLF92.+`:xZ!)]ۣ5מ9ԍ7m5XG==Pr9B9>death308@|ٚ T'Šq}ǚ.ׇ$ۈ$ʳMYIϩ79 UxMHuBm?lsͺgCCH6q~%Sǟmi|=<\>k=]>y'X>O'ݛHIZ6u5^ })SP8cbAZ5[ Z."(U$p9aNI)c*q)*\x=X`<:I "I>&%m8El$K8h%M_3CBFm2C&JGSDDBOQF^?W`b'[grlx&AXF^lE 8#7 `<+$JB)Vq%s\2\qV6_g2dF΂U8jO3og/}A7jHQH:-V]_OT 20a&\ZMK3Y8K^,R}.hJeL0nim.[SM.CSZ-l.m+:m>6H:<ω)C {L-V F%V_eJe2V[ -X$=NOCYNQRWU&4]uPօLȅn0Shu'_lk Jf|[Wbc h=RbVb]<_߆W9/0F%QR \]1xQCGpImDž+Қ IJ/ȽʱYNд_Kط=ک0Ѣ ĶǨEOHի`Kx$d_'\^'ݝx?YUn EɒIL[HdVF,o&S,O#LIi"M=[!w-$3=Y4W'#/ǃ !x0},~*U,V`=[ls W.lswJNwQQC[7bzp m ywfq]\sL}zwxz} 1 M/!It/]ړ^?ޏ}FDaCM>?NBDlLB5_IdZo9Vd4UяFHz;T֑OIMP`%W*PsWQrXNKu("LQOL^XoVY QTM&PkN TPRHxQ]pW==MH>ījs=punch01ΣC锝$}޳Ζ[Žh}־{Ἲg*͹CĬse߆Ƥh2˸}%lsѰ|ҷ{ո8Ѽvaܰ,ږAGӵ,DQqz0>X+H((//|<wJw/MLTjCI p}OE&_3> g/'I-ˆ^~t*lVL-Ad*~.ڍc+<.)s\b?jFi4,̓O8c/#r$<Ē5AY&8U4iO3Έ9AӁ0`~)pƎ(Oo2pn>\Q9ЇPEZ*bsu6HGw5O>wNT׆&kF)X:wFST{1";lScFOh`=Dd8[ FMzIQ-c#yi,Y>euPw-=v w!u{4|B!oq'{0y{{qF/bIfoFo ;_hE?'8E"'%Ȃhcb`me0 *. ,2:nE]g)WF+<8$>f'8q2}-בywelfSxUJ9¤IKANiOc76Hq0..>-Ա@'OF< {nx8C;űAz,V.K* d I C*ӭN0 8C9X8ڽ 2ǘ3?7<IxϨ(;g %m%p)a!eݰ# M`o]Qվ"unե RgJGwD7DyD z#uEΣKyksNr;edXm̘Z?d]Gk nXSz`SԈVe؄lHn~PgpEdd8{EыS]sUVZߕb\rYgݚHteNnDm9q:v;y m_NKKp|KX]f_TIvNyJlWSߠ^zGZu5hLlPoJ\Sn)S0tl"UVkN I,/.H.pUdãIETF3cc?Ro`k7ymC>~@=VD>«ja~=punch02ӰIüʫ-ϸGTɓ]##̹䌽YɎIXyKPpq|z2ueXx^wՂsrcφTئ%Ǿ'Ļ'-teR!_ xC]Z5Kp(+O*yKfz ml {VV(W%|4tF b7+KJ̈́V"e3yWa/uD&]8h9\;;q2m4~d[miCÂH9h>R̀RMa;Hr6eÎE\]9]TMhOOЉEcED>pb9mcEqQQшSZr9w)xNv&|G}Tiׄ<"JB}RgT58"fXZLhz=XCg%d Nfw.h3gkbN5,C } xC e~wFI ?wXMM[^p-w ;D?B&DЖ1C *GڍLfigdua * (TɢU|bbX]CDA>.2(Mc(QoGY{Wurcn^ev``[գ/K*Wi3l7#Vy0;F/͹=,ϊ"۵&nCыCs)G49> ,?,3=A+'v:D8pFp 24-cy9I+|3ww %ݐ*yZ~>y%{װ,p$cwmo mڽ hwim Gw^q#{Dgvk aeycs zl#{cڢdrfu{Fts=hC>j'=punch03ֱKЯ̷-ֳMTɛ[$ӹ#й䉵] ʐ뙸KlUHnq͇z}2vaXǃ_vψuub |Y۪%Ŭ(έ&㟹%teN"] xL&yG_{ik{RT(T"|4u< ^7,GD̄Sc/zjT[/u=&Z5g2Y6:q-m+a[ka>9ʂD3h9K̀NJa5Br0^Î>YY2XPGiLIω?\=}<7l`8maD[NLЈOVq4u{+aKv)eCvPeׄ2$I!p?}MdT5#9!cWZIczCCBi'c Jaw0W/ghaI{9w)C| x9"{_wu=B 6rRFPOap0v ;Hk?D(9ʓ5B *<ԋCdha`ta! *#(LĢNzdbX_?H?>32(JbIKmGQzNssck^_u`YSң3K-Wi7k7%Vw"0 <F/״L'͌Ά{"oCЋCu)L3>= y'D$7=J. y<D8wKp 240e9IĦ+4w %}/a>s%uد,w!lrlMi eٽ"n}ml Iseg!~EovaX~}Dve{ zs#t\آ^sfrGux=maeiZ?ob~]GysӅ~ saY[\ԂXr؄aMmvOqzfdv^pʊndЇQ]pTS`c\ڡ]fݚQshNhDd7g:i:lpzeWVxFptJWSokXZHvWxUh\SޠRsEZp3hzDmJpI[Ln)N5v o"]_mឋ I,/3I.mTwZn7c7L4ZIENp`r6wk)>]=DC>?k=punch04ٷAMа%ްʛXǩq $պ㖳[/줴A rͶufbЊsзƦ/y{ȟ||Œَu]ۻ*͛KܙG䭳,xDF\z0RO6Hn((,C|4't?w/_>Gy>Uio}JP(P4{5 [5,A8ʉOc+jOS/y8&W0k*V0:t(s#_[la7-y<)i0=̀GC\+6r$QÒ3QY%ML=iF>Ή6P1q.*a`5paBZIDЈIOq+qrt/LGw-Q?vI]ׄ%qF$\;vC_R3%;aV\B[zO5Cb/a BYz:L+Z~h%_>rtDo)>z x({)z@r.4{'nG6VHeo9t ;S^BF'#8B #/'ȉ.׀`fa[rc( *(,;A{idYc?PxB=95,Cb&>mG?zN;qvcffUu`NCˣ<K5TiBh7-Vu(014 ȴB,H7ͅ{roCЋCx*T.H) m'J$?+̫O2 =C:Uo 249E<Ixʦ(8s %q%b=g$kۯ$ hlM_Yؽ"tn PgzV!HwPF}|Dse z$uNԢQvhrLr>ia_hPZ?pvXGh zmXRXXԂRm؄`KlsPq{ldvadʌt6Ї~R]p`VX]^ۡViݚOocNhDf8k:p:s h]NLuIpqKWXnb^QLvN{JnTVߠYpGZl5hvJlLpI[On)O5qi"UWgM I+/3H.lTcBeD?0\S9QpZo6smlW==hjD>«js=punch05ΡC钝$}ޱΓ[Žh }ֽ}Ἰg*ͷCĪseމƢh2˷}%lsѮ|ҷ{ն8Ѽvaܮ,ړAGӴ,DQqz0>X+H((//|<wJw/MLTjCI p}OE&_3> g/'I-ʆ^~t*lVL-Ad*~.ٍc+<.)s\b?jFi4,̃O8c/#r$<Ò5AY&8U4iO3͈9Aҁ0`~)pŎ(Oo2pn>\Q9χPEZ,`pueuTr*>v w!uv4{A"n'{0y{{mF/eGf8Fo ;daC?'>B"(/hcb`me0 *0,2:nE`e)]zC+B7$Ac&9mG2z-׏ywelfSuUJ9¢IKANiOc76Vq00.>-ԯ@(OF< {nx8C;ůCy,W/L* d K C*ӬN1 :C9ɞ[8ڽ 2ǖ3ǭ?F<IxϦ(X*=Ǝ>>40=^|Tf392eR twODgH%}0VIW@mig.6D+-l#pL8(^A# , z/hJZ-I>xS#x+WS$A q^-yp+hM+}[AM|?ۉl5v].u,ZF3?<_O>LCI|BWK l)$?U=eQB|CB#`**g+ WT~rA}>g]d(V^D'=fA!QcmPfyIxD{n@~X@gf{UwQ܄Ue|2S I.%a7:Fr$cO=&Z#(u#u&ptĺGd`a[ɍ(-ti`Ӑ[Ɨ*&2zwy M,?( ]xF"8'ki/e=G0KF {$0Cr*|n4if !h!yfq&\a_SZswn^{Ȏ\n8 ;Ji[{sOrHfkKFKnJq mk$zVxha||nigIgq?xff]pc+7Pu]iMqp ~\ۄJ؉J̌FӊW7YbL~>,L%f`TNf㖘LgMޡ ᛳK3.0 tl`薀_WES[t4T4kQ堊Dv<ߟo;Oc?dZ8wFf^n7'ԙ,}t|㕫  $,M%TY97BW3*_o-EE|??oo.>=2@>/?rMv|_{GہfDvt)t7ZF4R<_]>LA[|AiJ!%$>h=yVD|MD%u*+{) UemN|>hkd+WmD(=y>!QsmPvxIDrBa@hszUs]܄Tw4fI.8a8MF&x OT 'o(%(x}̹GnghɻbƓ+/{ncѐaÔ)I2&}y$M,A( aDŽE"9'qr/l=I0L7 ,2C{.s6io !n!e{(`dgZ^rvy^ǎbo8 ;KicsRwHnkMGKw@y mr$zWfq|vigJsv?xif`pc+7W~]sMyp eڄO؉IΌHԈR7Va=|?*=/b^VOl◔NgPޡ A350 uo}g}f|S6QXn9T5zV䘆Iv?ߟi;Mc_w=D>ER]^|뚕 uϸҚ\٬iywxb$Є>ıiF醞өm ܾx#Ɍi޵vv5se$IM _Zl T1 }kk~:Ȯ/3d?ω+M"džVKx#1$Ȅ?J:Bdɜ8XӐ<-Lf a b·ʌ́'EmxvfZ/9,τp.̀g5{?FHc,͋6aHGrvHCDsCL<$Pl OwH~CuBn@hzUlrQ;K3Ra?eF +Or+L%LնHuwܸNq=0uil׾5L1ǴIy,Q*D+ kۃG"<(|ij/zDYR,_oFHE{V4n[>T=nG>Ge@n0ڽpunch09C똝 ڵЙZ̫i}i,CƯhE牴ͨh2ֽ}$Ȋgڴ{{޼9wc,KO, Bkh O*&d}zzz_.*HLjm:d'҃4<>Y,x&/+Ռ}G!Pў,>ڎ029Dㄮ\bt͈|oǵ+eh hV{m$^^2o}.φf-фWF~OQÒ\)76C\}/υw=a>\ruDTEs?]Tc\DhkR=e6o jfChQf+c#a)n_VwU,L w:bv6J%Cn}Pbx{0wdmLwsO_6C ;gyB='-?G"'%/1aexee/ *0"'ITΐxFmf)^E+A:$Nk'KvE~>ÓxDyfgzUaLFK1ʳ>z,U*I+ r I A,α1;><͙W6 2Ș)Ƨ=F<Gʩ';g ${ޔ$d+m٢!rѳ"npo_uѯ RirXIzRH؁CD h%uU\lsOs>tufhn˟a=ܭ6]Mm rtiKӈzXׁHoPgzFmE9Fqjr=punch10ϣC锝$}޳Ζ[žh|־{ὺg*ιCĬs!D߉Ǥh2˸}%lgҰ{Ҹ{ո8ѽvcݰ,ږAOԵ,DRrz1=X+H((/.|=wK w/LLUjDH p}PE&`} 3? g/'J,ˆ^u*lVL-Ad*~-ڍd*<.)t\b?jGi5+̓O7c0"r$;Ē5?Y&7V3iO3Έ:?Ӂ0_~*oƎ(Np1pn=\Q8ЇPEZ*aru7HGw5O>wOS׆&kF*X:wFST{1";mS\GNha=Dd9[ FLhJQ-c#yi,Y>duPw-=u w!s{4|A!oq'{0x{{qF0bIfoFo ;_hE?'8E"'%ǂhcbamc0 *. ,2:nE^g)WF+<8$>f&8q2}-֑zwemfSxUJ8IKANiPc76Hp0.0>-Ա@(OF< znw8C;űAz,V.K* d I C*ӭN0 8Cj>:=crpunch1n޳ }k֣YֺsWҶ2oOݝ Yڮ wTmכY5۲Ԟhׇ._Ǐѣsƚ}ͤLՔrř ~-"t%]^&=R 1VQK$L5|)})\ ~J5(GQ ir4EI7#C2->-LB=Dž5![&# ÊUH3.G-!r'N/6v!&Մ+ As:9ʈ64h6Dτ6?[@}!TNQS>iRBÊBLщ;c/p\iBpgOXWDŊLS[ao* f|,]{\TЇ}`(/X$%W,R?I4ELm\]7xQ wlJ)?Z{r; Eq0<h;':huR =C9v{|31f4 },tN$OlLW^s1#V#RI Y5;`!g9%f'hK9O(8N /]Wgju\\yx^?Q%9B+),Llea\c?b,ZFP>K*E`K@8qGT'ZQ!w|wt_l}bfNT_f3]AKS id.7 _GPH#6?N/ý=ŧ'İ5-  VF< DU8Hm$߯_%U%;h`'ɱRնQ#GG~@tm:׶2|L;>3I'ڒ6a%]{#^(a#m $}JO\#ѥ݅^mr?i"Its;^-XvBqw@k'rAb`g΄RgFvH]vFRǜZ?ߡd|]Gh~ɇчt~kkՆnRlh^ЇrLpU[hd_eupldxn]]wYQreכO˟|QК̚}Pp~Rz>x3t5nbLod7]^\8,˝~/|ocpܙbxoۜ}  %ō*M/zV؏~FFDPgR'_xq5qqz@/>@W=>.crpunch2յ$ù ݽ!دħTعᷤYʼn¦I宼 ̩SjUoY}Τg/񘰟q锺xgࣰr WĽ پŶ-&W_ 0}-vhO*AK4a#W~X~:u%?V+AyGY~.xUZbRkWSRpDk=m|9b{Z^Ԋ}R_B[|.x ]IlR#UyY36%ul)<7z%'HgRh :81;?DOv|Gf L$u&|3 h%fd^~:r"?^2J;>;e7(lj-\R( f/|tO]H`#g* ^+=0Ph^EzmF=JZC/^I+1[J,]V'tQ#ZTQ|`>n`b 4Ȳ5WK/2i9B7x'fHK#d0ǃČ|ð? ܺ. fƅ q DzH7wƦKg{,Z%I+:Ny'O!C,ڧ(q>nJrV_Μ_82kԎ(0Al)|Ibҧ,4gg%M%r]qIP#[ֻ$_#QS`Os UX$|VhLu6xJ^ z3}+ubJXVbkf)w<Ѱ^ejsNqsI}[XWN\RvZ?oHeXkGgk8nyni݋cw܇a`maiԈ_`ixXrl^iycesfoYXpRSgݚhdudnܛ@yWpWifhEm7r9u;xacTiwUǠ`o|fdX}_|`rc_ߘaxRqxFhUnTpRXp=3z"fߕmtԞ0G()?O3mRt8D+Ug:;I4Wze >s=dK>.Rcrpunch3ޥ,ǹ ΜSؿڶ㻔W{ŕG믩 դSjTpȡYϭq.󘚠r얤a⦟r;Pˬ2%ɤ/)XS("s]O)7K*aS~O|0u$DV%o߄4"xQXbNhXLSn>k~8j|4_{SaӊuL\A7x+eHJ&c0Ĉ8ƀ˫> *emą} n !e{H7vƣKfy%^%P,:Kt'R!I,&vArJ~qJZ͘f82iӊ(6Cu3I_ң'7gq%H%{^z(K#Vص'f#YS[On N^#[hSr1}Ke z-v$naJTVlkn)w5ҩWfigRgxI}]UYNWRZ?dOoXuGqknvncۆ\zڇ\al[mӈ[`iy[qifgxcerfn^XxRSaۛfcu`k؛ÚrPgPuBtFv9x9y;}pYZVpxWŠeoxdbSxYwZm`[ژdyTqzGiWn]dX[^o+3 p"]ߕblמ1(*=G)vRy?D,Sg99F5Tz| >nC=>|3crpunch4(۪ ݎRѳąŸx˄H󵘅۟PͬoSiڙWjήҼw3󝇘áw윒}b寐t;x؜0%#ё.;]8'dDO5&K!YC| 8|&l$X J'0CEv |%;f> |&[#\VPR+e"?T;B?;H,d7w(w4JP(([/l dQ~}]G].]%)Q+)$BofJnh?I9YC8NK*2vSU)RVfQՂ`{`So_<``W*FMK='iK77o3\HA.Z/9ÇҧK2%Do‹ s eJ8~Kpt%^%R,;Pl'R!M$CI~yJ^i: 2sȄ+9C3Idɝ(7Z%K#^(Mܘ#Xѭw#i\TOmLn"\mbr1xKvz-m#ei}@YUk'w3ɟSqegRgH}gPYVQRZ?d]XGmˊxԊaنZw؆[\lYhӉ\Xp[[qyenoepy}enaY~TQ_xڛmZϟc^՚ʚmWaPA}4|5y.J|5Y{z}=S>>>f:crpunch5Kݡ2α׆WԻ 軁ɱ1kH󯖅ϛWӦoRhّXԩϽw4Zȝt噒|»e⫏wӒ0,#ɋ.S^'7U2VHK O7|*}$` pJ/)>T vid.HI1$C~3-7.L9?ȅ/"Q'# ŊLK3q0>.!d(E06h"w&ք& 9s3;̈06h0GЄ0B[C}WQTIAiIEŊ:P҉4f*v`]Ep\R[MHNJDV[dr{% d|&Z{RX҇e!1W%T{'UPBF,JK`_]2{H slB*j8^r5 Cq*>m4(3nuI"=C;v|-3f6{'zQ#RtPPTx+Z"JJ;O7;U"d7!k(o?=N'1Q/c\\oy\R~{^8S%2F*)'C}qeV_d?V.XFEBJ*;eNU1yLV#_Q{n}x__gfEY`Z-SDKJ!iY/7 c>RH88P/93;ʣI2Ѵ!C8;  ˺EU8Jo$ƹ^%ȸS,;\e'R!N$ѣDG~@iܗi:2Lԫ:>3Iq(6Z%T#^(Vɐ#`#{lNOsQ"Ë\mtr8p"Iz4d(]yzBe{@k&w8WagQgFvzKYeJRZ˼?ƩooXGhˊӇf ^p׆aUl]b҉dPpX[~mdzce|pqdnaY}TQdi؛zR͟nTњ͚oPcR>~3z5sTaB<Cskin1(䒔.騎c05ΧvVЖsTTF"B˃O~s$ ?||,[Ƕh+Sb_BzϦp/,EbmŌnhޠ7u8F~nl+NnwM#'F+-`s.#40U1S ɂSwE zA)Nm% ?{u }pZ7 dN j'jKy/,ƌp?a m[2/-z#3y<?"/lb&'tpczψm']ZsC΃6/u#e!όw؉@+ր%.˒#$Ƒ<:EE3ׅ5bm/us}/Liv3DirC-6u76Dw`/=h=7*3-wR5UE6oPIF7 4pK9F>,\w@sk7,m0 ke0zV)z;p>CxADr=>w):N{c~Y-LSd?O 2kK(A=){-ڬdFа Kb<q+FtHz ؜3ԗ? mQ!s# ÍkipLg88wEpw;zȞc=7]xMn cC1ӐD`؀TۇcWہWӌVaˀ4s9/Ǎ5-]^р@H>aXqH_ࠚ'hIC5<ˀ< jJ쐨QoPXh8l\KBw)l&^tLLXeLa:iRnaoX\_ꌨ*Qɘ.lK)&LN &'%*ըGҡ/UԭR??CN?CRRFf3֙78e=q>mBX>$r0uskin2&Ǎ,{`.ő3S;SQρQD@ŸLR m<ɍ~ YT1P+v@΋ >^fTFD3_1KPJ!ɐ%|C(!ʗ+8ʎ]+-Nō28R*TPVPHC;?WÞKPE Y'<|~ XET9;Jàeu7@S// F]͂RKH1?)B7Kɋ$3ʎ%-ʐ{R ~L҈fUoGiGPЀ8Vː7Lv)QǒPoW dW҄6\|+_)S`:dE\ԇ]ej[vsy[Lir]Dibbׄ,^u5]Ew][=mLI(]-wi6jE]phIF[ ]pf:F_WZtagr7,e[ kb[zj)w_p=bx?cq?=?<%?X=(?h=3?=%? =1?>&?=4?=> <><>h=>=>=>=>`<>! <>" <>x=> => =>=>=>=?= ?=> =?X=(?=%? =?<F?!=B?`<@?"h=3?`<7?<%?<>;J>(="> h=>=`?=B?X=M? 8=Y?#h=w?=u?>l?=_?=O?= =>=>=1?h=3?=??=4?;z? ;z? ;{?;z?;z?=> = ?X=?=?>?>>h=3?=B?=(="> =B>>>=>=.> =B>(="> >>=?=>=>=> >>=>=?=%? >&?=?=??=u?h=w?>?>l?=B?&<R?$<F?Dh=3?,=??>=R?:=>(<>' <>% <>C`<>D=>$=>)=>7h=>3<>'x=>.=>$=>1X=?5=?==>A=>)=>$<%?'X=(?+h=3?,`<7?%;{?';z?-;z?*;z?0;z?8;z?<h=w?;H=?5<^?1=`?2=u?B>?6H=?5=?*=?8=%?/>&?@=1?4=4?7>??>=`?2=l??=u?Bh=>3=B><(=">-<>'(=">-=B><=.>0=>)=>A=>: >>>=>7=?=X=?5>?6>>?=>Ah=>3=>7>>@=B><=?<>&?@=?8=u?B>l??>?6`<@?C`<7?%h=3?,<F?D ==I8=X=G==E==N">*>Mn> >S>^>T>Z>>z>a>>j>z>VB>>e*>>i==I==E<>F">*>M*>F>Hj>z>V*>>i#?*>F?^>H7?">I#?*>K"?>L?2>R?=J ?>Q?=P>=t?(=Z>=?=>=>=3?=G7?">I?=J.?H=O"?>L?=J7?">I 8=X=G==O==N>=Un> >Sj>=>>>=>=.?H=O?=J?=P?(=Zj>z>V">*>M>^>T>z>a==O&>=U>>h=j>=n>X=>=?2?X???\??W>?s?>[>>>>~>>>>{>>?>o>>u>>p?J>^?^>H#?*>K???\?2?X?F?`>??|>?s?J>^>>u?2>R#?*>K2>1?X*>?WR>>bB>>e>>*>>>>>>>>>>>>>Z>>z>>^>j>z>">*>*>F><> v>B?f>>;?Y~>1?X~>1?c>(?)>4?y> ?w>?>>*>> >>s?d>s?kF>l?gJ>g?n>g?mv>B?f>I?j>9?>K?>~?h>s?k>>s?d>>~?B>>e*>?W*>>l>>B?]>>;?Yv>B?fJ>g?n*>J?_ ?>i?>e?>o?>l>>{?>[>>~>>p?^>H ?>i?>o?>l??W?>[?f?q ?J?_ ?f?n?n?k?~?r?b?v ?J?_ ?>Q>>u>=t>n>>F>>>>n>>>>= ?>?=?=.?H=3?=?2>R>>u ?>Q>F?` ?J?_?b?v>W?>K?>??|> ?w2>1?XR>>b>>*~>s?x>>~?>>s?dF>l?g>l?>p?>~?~>~?>>~?>4?y>9?~>1?c>>B>>e>>>>>>> ?w>(?)2>1?Xv>B?f~>9?>~?>~?>p?>~?z>g?m>I?j>Z?>h?}>l?F>l?g>p?>l?>h?}>~?z>i?}>W??b?v>~?z>~?>~?z?b?v?~?r>?>9?>4?y&>X=>>h=>K?>9?>??|> ?>?s>>>>>>>>> ?>?>?????>F? ?J??b??f??~??n? ?~??}?>>>?s>>>>>Z>n> >S>>>>>>n> >>^>>Z>>>>>> ?> ?>>>>> ?>9?> ?>I?j>K?>Z?>=t>F>>=>>>=>=>=>=>>>n>>>u>>>>>>>> ?~??~?r?n?k?}?h>?8=>? >P?2>R?v?X=rf?=R?r?2>z{? >v?X=rb?.> ====8=X====h=>===&><>= ==<>==">*>==n> >>=j>=>>h=n>X=&><#?*>7?">?^>#?*>?J>?2>>> ?>3?=?=7?">"?>#?*>?2>?= ?>?2>"?>">*>>^>n> >????2???>??=??? ?J??D?R>>*>?2>1?> ?>>+B>>*>?v>B?~>>;?>>B?J>g?>g?>I?>9?~>z>*>>B>>>>>z> >>;?~>1?~>1?>(?&>4?> ?>?>>+>>>>F>l?>s?>>s?~>s?>l?>g?J>g?>s?>s?>~?>>s?>>~?~>s?~>~?>~?*>F>*>>j>z>*>>*>?B>>?>?> ?>>>>>>>>>?>?>>>?^>?J>>>>> ?>?^> ???>?>>>>>>>>>>>>?>> ?f? ?J??f??n?J>g?>>B?*>J?>????>>>j>=n> >>>>=n>X=~>9?>4?~>1?>>?>>>>>>>B>>>>+2>1?>(?&> ? ~>s?>p?>l?>h?>g?>Z?>I?>K?>9?>~?>~?>p?~>s?>p?>~?>~?>h??b?>F?>W?>i?>~?>~??~?>>>>>>>4?>9?>?>K?>??>9?> ?> ?>>>K?>W?>F?>??>F>>n>>=>=>>>? >>?8=P?2>R?<b?.>f?=v?X=v?X={? >r?2>b?.>?(=?=>=>=>=>=>>>>>>>=>=>=>=>=>=?=>=u?>c??f??r??~?>r?>^?> f?>c??a??m??f??K?$? ^?n>K?n>K?>5?n>5?>"?>!#?>?> #?? X?)?X?@? J?7?J?S?:?@?J?k?;?k?J?v?;?v?J??;??X?k? X?@? i?j? h?t?X?v? J?v?J?k?X?@? J?k?J?S?X?@? J?)?X?)?J?7?:?)?f??m??l? ?X?v? h?t?X??J??J?v?z?X?i?j? X?@? p?9?h?t?i?j? z?X?~?`?i?y?X??r?>^?n>^?> K?>K?>5?>-?>$#?>4??"2??#K??K??%f??K?$?K?>K??c??K?>4??"K??K??f??c??f?>^?> K?>c??p?9?X?@? X?)?:?)?J?7?:?@?$?8?!*?k?:?@?;?k?*?t??X? :?@?*?t?;?k?;?v?;??(?x??`??X? 2??#(? ?+??6??K?$?K??%?h??`?(?x?#??2??##?>$?8?!:?@??X? ~?`?~?h?i?y?>=? < ?`<?=+??6??(? ?>>h=&><>=>=?= ?`<? <l? ?a??m??K?>-?>$4??"././@LongLink0000644000000000000000000000015400000000000011603 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_glsandbox/net.dengine.test.glsandbox.pack/models/guard1_body.pngdoomsday-stable-1.15.7/doomsday/tests/test_glsandbox/net.dengine.test.glsandbox.pack/models/guard1_b0000664000175000017500000126247712641367671033164 0ustar jaakkojaakkoPNG  IHDR{CIDATx[Iv}W==`M&)MJu,R~ï ~r8#-G۴hG2-% җwUW}N>3==sNԮ]\ro}e֧l+n Oug7c{pisy1tzY'G=׼_ tE?ˬ^ܑ?|f C$IWz B|~]u(>c|g=G!UkC3o6y^>o~++}cN?qx΋\?W|q)z'*tXv7eY!ޚ(9E$"21?W>柪k~M:r_\{i0ypN|z듼iσ^:N @?'yuw??f5'Y/lRqQ'.w|Шуu$Nt~9v]_޻fDf gb4KKG{fCV;ޯw[k|:KDD$cwy]_ >f}|xL>ßjhzU>o-~0~86=QOq|d@︶W{xD[DT/ΓGg~?~1p[(2%IP< Lͻw߿~w~w~w=^ϯ2GwLej헶uD26齋J0+3@Ly<w ʗ ^`fgLID`z̰df0݉n uYDZSxj$Nb%3wF\&L7?<=< Є8 "D"Vp|jo>?h&`Q=Y\etSe6U Ӣ3ܑ̜ eT+"GB^3(:ǯWS?pJS&Ϩ6/KB:ï 7W;qn><8Ns|'z~zپ(*̲Αǟ>`+"r>53hM9,#-"zq&1v=3T8a3qgf* s@fz||{㚙dfLj{fxf<)"Ifi{wq,{u|~ 63@&(IUDT̜av~T1G|Df {q(GZt94bDDxΙI}13$'S-rI$~g i_*֚ׯ<=#18׷A._z C~駿+9n?%5t\2,"*'2( 9x|<_o[7Izb>Sݹ|&s-1'<%qm3kc"JpDkѣG̼E RDU9"j"bZD RUAIfuTSyXēGt\Ě ,H-4"̌L:Ƶh"{?[$Ńg:K"3ĔHR@DIit"gD$D\/4^ʹ1D&@T7 {y+yNp/D>;pq>y γ[eKH:f-T3SA8*umi Uψ(??>pqsRRDQ/m;/}朞g[7C[@xfVAfз=BT aiL |i?w?ڃp3S'`1p3"$@ <=\~ G¹)0}@dK1)r"#RdDrFZ:&*D49qDHWCAt  1fQ-.&N9D̲f6cv‚.㷿y=$m'4z%@$2]@΢@D0h&0973(D@! !vwb7ad9B,hC=X$ "I3"( )%㸞‘LcNc ܝY0D"mID&| 3#=&13skf$0Zv{}o],dfZ&1" A$M[On><{>=>U3<(%v<$X+#w BFϚL `iЛf kT^kReJ0I&(y@#A0mp3% $fB`0[ ]iQQ&ZQęL[DLDL#./C' quabN2LXD$14iH d݉v^z KS>﾿i|snW?'75"$TzD^$T& {0%6m5D,L/uj[??ٿʇr/Y_M:1W~U5i9kJpz?O"\sB2Q"c+; $-K(Ĕ@ ҥi][aΊz9L$!=+aJELJv%{"+3 U$\&a# r ؘV9(u2ގKTSHԆ̬yx(j)}UnƋ,~MS|]_T uxu?ZOHu]+ĿIy::ʡE!W  㺮ì~bc4sFȼ pOըL @&26RP3sEL!eo373c?# 0h}ff˕9Okֶ@2Kr233+MIu+S]DDd4 swI3#Q814": J" s$ mjm%L[*uHb@0_G#"̵Ú3L#i6HHfvZ?Th|R/&5*frd=m-.VjJ^{hגSٞ{xo]7>⚯}׬w2h'?:2Q~xN߱BR"D', 춡k*Q "Ca"j,WaX'͂u b0Yp-ȈgS{0$Q-c Jf8+ eHBW83ӆ%pn#BȈ٭`YT *y k3CGxλ+YFߴ5uxIɌa Jh0;4?zNtb_6oEoROj: G;1HThH\* f- *I=L`&7JN 4Jg(E[TpDd$Z;[gݢ/ː~ub(͟[-5"Ae#9Zm6nD¢̬ܓ(mRaSCIx$a'?xG={W&0Lu]Om3XD8e$՝fC' 7gZߎ)>]6o$L>r "e }e& 9c~sV 5M J޼fkɑITDo<*[PnNDdHX)#mg)eyum,_zkmogw}5C@[1=^Hgˆ[BĈ3"Bs S<}~dfeb"t뺲\G4Ad2Q9[ZO7d펈Pa@rYH ѻw†{77JO<POnpz\p I)D[,bkw&Uyx& ؄GS26IR-\%W RE/SQ͇~CpW`fn3@R۪sĻ%̝X ߻nޤgiѰDl*3#CDyu0;ٺe0( ,[i(lsFDK *]/(#wbJ3¬MxY8"#¦pP 2ݳ2a`gvTm,L&R!dSfw7 '7HHW7"t@BLM”aʄeYda 0"s&J'K?"I| D,34'|pi>:ŷ`4H,eUUD9)Eәi#0 A ѕTH$MY/\3XA-4xYbvV޺|K4%2 QBl($\"D2 \uL/JX 33DSf Dҙ)6ed|KfJA _?˿?yyM_O/K~ڗEUe}v/OC&lNkfVx>my9lI9 YD Ţ얬Hfd"ID2 L [ͅ%qM 3xHfV7$Og&8Vwӧ 3JDQ"N,aLgxdx2. |;K $3 \:с5"&UU(mFĖ$ LaV:Ob)RR`pMY#"SS`xF7a vfo_yty$e'{"#`NNiܶ= 2!K8iy9|= ginYeùG) T(h uP!2D{G7ѿoɖwc_).-TȱJ\%^XLDTL/'rk1'.+:pR_kD0n !pDL _(%yXTWf lNM5,+^ҩj2$ j;HWT/Q%.׊9*Kr?$yz8Oȣ}OrC a&䪠11#0P|Kdda:U6|/%P"f>jL`wn}u7~[2"5 b>i]j*YEcHboMuUAT]_d z~w;n]I9_(__J+1DҴ $9@`tOtO'ZlIu"2-f`Qf0Hq[ flYT輎$HH5<Oy= Ăp[n-U@]|AL^%k1.F>{tox<{Vd=5`O:cas~v<㺎 w !X{ Wsqcg *9}xP=9|^mfɀ}O/o'E2k]:kl:] TYr4h*3Gzf:IxBlDlb :Χ1a<c9-p,w:Vil!).뺎ʣK%]*EAX{zqtQ1u,*8_yӛ((93Q'òX &a wR2 1ޘAT erл&hzH[g{CWw|kuB,t0WnB>~ܛ Q6w6f#XBXzTEXܭVeX3[kdscyN Z= y+s~x_/4Հ_WŸX DIcƘ HИfc1|zLs?fYQ3mp0!,-|aaUf*taT@Uc pt9*?&BJ;oc_ѿ76ܶWOW8F:̼9ܴ֗ݾ/="D?=c{wϟ?`IWe3 |" 'O"f&B|L3H PD8 l5eeH"vicNp9tcFDMwJ33^1UFMX猈/TD5զYD)[^: \lw %x@Ddf.B"Ԛt iNwxFƠOKQZ E$́Ғ3)X*` 18ʕw-Xtu*+YbY s4=M<<Vi>9< 3IcN_cZhFNl~<~:Q({'_~o}HJyjӯD\b٥/"=[ZkB^W%|wLki~}ה ?ns =ֹ"s]z>뺊p<FITUYҽwfN9<"cFG1%6ֶrL",@j,& s"zWRqwsNBUХhi} B0ك̨R#kB4U!<fx'VVwؘEfH4y'3gici/={Va[S&:Gϛ s|`?jO|? 7BWH 6yNDF nYsW`BPvm2+f͍%"sZ81̦y81t :}P.w?m{O(#Tme="Tu!,Hrcm9s"TDt yB(qA;_>/1ʈ] $&o3 ⫐\Y38"XEH\Nb*U-;F;CXǨB^r$%+ 2XW: 7 ֊`*asЏx@3R#Bl (eQq]I <y0sƥF$[ZzTvI^z~|)|hj{ؓI Ըgì /]ф"|$ =-4A=ıQD3 F6l{a{~\ۏ/?{AN&[kmLt;ļ4A6ƺNG7f Dp;P\O&BD*<2f6ݓȑ-r kzGESfE sN^G paR[ď>y֏PLn]MeG[[2Sz-uD]X'yH`2q"wDU2* M%HƀeG̈$h1oD_#:=eQ;eBm{zR{{ wLAZ%-\m("0n @2TI͢B6'鹎u#6P"@"Ht'(3!FBD'3$2B.v'آ+΅F&@DJÐV_Σw=&KH!UM,cY[cdr*uHUv7wchc 6nHՒxͲ 0Nr&m9v'1?+G?O#O<}%럀#HXZ)p7$d츞Vx8sD&v> DIvRǰM@RfuI`G"# sNHU*\DW+HTaQDvY-*y> vz4ӽd$lL3/d1))\)Ed'Yf ♑a9܆lL*ykk*">y^"EΫG kkI,]1}G'7ޝ`cW??/~>Ԗ3<"&-*(6swt3pw'JVzlMRmj1uH_KLcyQƈivKyvp $Au.˟ $*U&NȸO3Rn_FM.Ž\IcO0, I1Fv%"*Y! 3ff27f P-" Z>-PnLf@.^3Ksl>#,ufv,&2Z+˻@hNgVwK+6Z21E[6Б{.Xt.d¥d;U6,3L3zK [ njs0=p^uHdFDl"(ڲMT&FBc룉rb}!sSyE9o1x-)Yp9[SXz&LJ~s:zתBU&ƳO`bW!؊!fR@EvթwΟ_34:}T92z%oZsZĜ~96  lE!@JffLB`ƪ.Ry{#J{$8њL3 qaRIJd\/BC>ym{0ed "06w' cqW]q^*.B7f̤*c #ih]E-t.!Hº[t0fRncdm\"cy+T)DLI*Z̲k5msQ32OpI"UNbJiW[MBk-ӧ3֓boG"0 cDvNDs1<~Dd^y{% Ixd ̶[ں΀oxz1ncJBrR|wpHbjn{KA"0ӧ(vD^(XWfj̖ ɶ> evIߟŸ'7^4/ue3sJɔf*Lٛ18 -esE+ IRD[ $^n00k6G 075pD +Sߘ/?xLx[ް@[xWO/"EȤu]uն'ȒϥM(`HK v V77YȆwmIHIT1sW:6/r&i%5mFkv$Ĺ3-gTX?"$ODX(!s>rL݁(k$en;oMbyLX83w+/'2 clp>UM_9,2bI³wjCSZ_Vj ȟmKbu}eMCC|'cX:V"̇-MoZ{9$Bڃ ,H.PV&Jsr'H4$ݲ, %E#If." 6Mo1"2UXܽ#2lʢl^_C] !i?_׾~S&w7{߁fғG\m|:NݮМ"L&Upvc%NN=twHekD2Ȧ ƭ$JA2]ZKl꾩Û22; G;e1HUyEyXNc6PU̪51jCqgf#)2<)3aޥIm'7enN %y63&nv&\[RQkPhQvmж5mcɔ]]G/as=O'-*Onn("I|zd&&I[LfS]i)Dxl x "fѭ8vHl3 <:?3 x,"4~]~)J.7JRU魒jKӥ+Q'Gψ6tc ՖfQ;}9XCT99(2HAL`bvw\6ߓI;ы ChIOZq"ZV^S n9 fQ%!DUZu!DUchUĺe 6=fƜ9d =UU{!z\$%TUN.9n{ff3)Jn!R4Y"hs17f1I '|M&I=L NOl>T|>h NRfBQxP w%8Sd;an :}t~%36u`/ |=?="1fA]>x~tϻx})p^)9="*n̤#lDs qw<1DƢ-$`#!ns t`oWz>?s1qZsQBDZ̼֚|JI&=S00URw'^kYUUXe Æk%#+?o?|__*0d'xEY':{}%7e("R9+e.Uy16^|hXiå sͱTYyvn3LD[p{ 6e0+BilIRƴY19a9#ipUn7z$QN/v_L瑙B-P*Xlξi+NSn֟%Ps@œȄV{Ae&zޫV ^ Q4z HP0Fo ҘyFnu+9o]8+1J|N?(U4%k ҅9+ԖeYו Qވ3W(ݾqOno>Wv_o?o)csTm@)2]w^D"ZZ/z Nawsss8e 06Jʻ"\aHȴ>Ni"V.Tɒ,$2$7iOyLerЯۥr3svchm*Zu^ <!)_pIx\Tfϻgl+9եAlf4L *-EJ`PJR>._HdL$kںRQ&mɧmGI&2${ΙuߨvD3HbddஅTc@"-xo_!.V bUO\cƥ 9;o?XUwyr=L60#.HJJ 93XYݑ\]AA*"s&Ku1dY~1j%Ouk!CgQcz<ggK"j]@ 7G5 h_2dhQ }n.HTt\e @rS$yjs=cg #Yg9#<. ?ʸ_rod:U0;`Vf*y!z mt7^,g(-wX,m(K-| ĭjrY fi'qi`1D :,3fa˸rQp35ʜW [Zl2rq)j73kИ[f bPZDT\фDF$ee "(#w> Ю>3|~ى4"~=L0&IXWSi\E4nܧ=w.f6=?`)>h u%Aq9Lj\NnoG D>]v2"?r?}xWbֱpyslR~?N)tDDX&ZDG&ٵ(C+D^?=Mޯ팗TE#6"=x^m3DX(P$;Pԕ?/obȋ \3Ϗ4(qҺR%2#@tD%m9&|-[%"Dp_\ņAq{8z'E4m9N 0 g]U,E{p>@Hx;w&ӎsmsESq \dY^Sq"FᰏyHIШư "",Gܴ=+OI WF|ƴwcߙzM5{=D@kv'7}𥷟p,ʷv=Z3m2]gyFx!F"|i/)!)ј]vJ>VA.A0 a ǹ^(0-2J 9@ћQD>~)"s}rw7~MbFD/L\ {&TUkһe6LF I8MoEAH&K6O\Q<`f)1 ႘8UcrmJYqz]/+:6 8z>Q/G\J*CmaKR/6.['T*Z{%IDÛ2DUFMNkx"LȨdJc2S.Ȫ,a@\Y(C uv9UYV?AME4۔(za'[׮]k'fLXLBs: x7ܴF%,,D==t:i֊,9-a)~[W8[Qןw9>EZDbfaea1L'슝G\Hj€_~kLAi|yrZ(u]Y[SGkmb.% $QoE13୘?+$ ȋr SRDVQXM7,U(#GA/^1؎YB%T"R2d3lD3d "bu J}N;%JvK8kK,'2,bMYXP؇-@o NJ4N ]2C7"{ah?:C[&gJ D)d`7oBS"@z` B鑥 715YAxOЕߤb`F2?(MX]ۏpw߽w>}|5>OV$J&tz`DYOox~Ě6}M 爀y9ony=qXy`Gx:v/"iuOS=_~7~mhO*ZLBmOD̽wfhfqW5t{-L%,L[ ٜ3<Ҹ52x9lf渊BR!"ȘaisYXp@A=䝯?y%qaRnZb3M*=Ge~$]efs#;Δy<)Qweii: QK׶)4t*TMf ,n"#^Yj?B|4T<9#EURJE¦ cX3ၛ0SL;p(bwDal e(ŭ}B(z[3t`BxU.BcDDxZzC0%3Dc(%AaB>C9zq˦&KIꂊP/rm_LJ~N4" ,H"Q0O%\T_T1(ZsN6=@$ e'IЖ bUAs0KG鬲,Hb/]f#Έ >v}?ۇE|醴%mEwxݸu zl;kDfwG* pK)~MCT#'9McEXΑq'"F~O?iѕO.i@CmK1L< E$.ZzsQ.g+}$I#S!+N Q ]^anf[0JHe#Ddոmf2 "vV¹nR!)yU7ޤWu?b*\DR1w{0ј(E}f6aek;Va<:PD6tB0Ս=Dp#(y*޳A:^@|_Ÿg_iu,"^o]"ihLL IDdm!‘ r[x=ybak) f'ki]j Ew/$Yb, e139ލLa#'jEJ|S0y2]!6in/E1gڴekACr7Ww /̊CȊ> o,R kAni:ș &&)ݱ9B6GuQxy$ SP°([_!61A9rfVR5k*Z J%V]Dd32IAlإÊBj1Bl0D, `bE)q.aϟ{t$m]X粴ǘI=Q,JD"{Bn43\,Җ< O}7j?ApS-"Lv"|%8 p"Ks QVJ]zm.yxP쀯>95q8 q8(<9ڙҲ5NէU X44A˲d4G 'LDR:{>{n/o?~1b0zkzi^5<N .3Y9assTU(A( 2-ZVQ}XZdqOc[ϟd%" a"B)wexΧ/p/|8@:9H(hDϹ)Uvno[O'9L}Č֖94Vf9 *> ֘ f: 7tUV |]׭,@.TXp3E5b!0{#naڍv^V"(]L|^[kJ ^=`#.EX2DMgݎ1,zDz{phfcz/`Gcꍛc-Y=ds2󝯭,~Ddq:ާHkDf@\jfQqeRP5,gOyړ0ixSEi7K珣5-DdDz:+Ea&2x^h7 "o һ `@="wHhrC -$ǜU&-҂pE+e1h `f.]&WfKo.xeI"/tƽ KӼFJVnˌV7ƘWç*""#-):i]:;(@T\ۉ"z^P{L`c]K `fj2$in` 4"3ݧ2131EE)i£ vPSnq=<ԎSDLOf~c1ySz`Rw||w}y1-??:/v:\Fi:2=}#!0e X}Sphh!X8r+W_E͛bQ +ԩ+d^Cb:ʹg"P[C%tyDF'*G|DnR/y#2Xiߞf֗%sS"df,7"Bl@DBohҙsk R [6?37w Юra1RD !&n ufv$]뭩5 Ez8K ;颔.71m ǏD´HO?/B[al%<^n 5/Z"á]&۸={dlnE%A"V9aLdbkg,ߋq|nP~wME,)/^^V7o6?6QdL7niIk\n`&/"<>,Mf0_39l$`qEGM㺮8@ƨ]mN9וc2 % lIB1|zT֪EY꭪㦨3RUyЃ Цt{;7y3ebL`圅71Х٢^ ]2:rEfkT2jwowEd=MT!":|%^s8(3G:iIV 3 >Fd1x~U~ǝHjGQϞѰgOWݿ3ϭ]owڜsS򋹮#=̌U'=2V <}dse۝@.;m=ǤX,KI)X)c %v]r> wiо#޻ ?}t]3(㛻{EѾzziF[={*/G2=Ҵ$- ܂eEөQ쪊p8E3(O"BZDy> y0IA(xsi#"H8%, f >L*tDt6JT]P3v@EzffN]mѮR,de\x7f| j (OyMUw ۗGeS?x^D9۾,~pwE-nnv$ifXQ,̂XN9X eYٳ'8c`%ziU L:ݯH&sDq&N9 G;({|t.77w:&7$f~:ߏjc0_yezmݿ5K]ط.{G9'3+ (\;I#e$1$MR@(Qp(ݍƥT2\"}u*@nʭ*9~{o}_P(*ng-1N 9{9/MDʤ\tE; )HoV]ښݡ3<}TK-w)'By? lfo>ƇD~Tݞf n`P Wn X),$ aԊKRzPdJpK)xzeDA2 Gw۲cr3e] 5-Gdqn7Ed̢0I9" j݀=ܗüwy#Nk m^}D;uMDRz}6"1q\be:YWAY G/v?~|u`؝^7a?T$]2f]-U'J\v<;P_Djmy!|bԺk7h-켴5V+˲h-pP"=)jLLC2VJmsa\ ,o?T~k4k倞gޯvY02 16F#o$LH[Zw#LmأM!04=ef1 Hfн@,EBek,2v~ףB#| $DInOCD0ľkS.{\Wp̘AyM9a#ri r*YmVbge,.ij0D(u҈ؤc{#Qo{0ï?#K$"Bʈiң3oT Y p˵qZ*(: YfYlJ( =V$'@ɌRX Z>{aDA1Bu-'E=4d֒ͭϱ, xVnc6s_7xcv_ND!޵ywXfֵ3>&%Jgg?>o0f|?z[OJr;L^D|nL@2EZim=3ƙԎG% PkI\hbm͞wg< nRUHK=ZDD{{N̓Nj&ai<˳uy޿! [2 oafUeX4zZ@8eUtuoDVeqJͺr[+ؚ.!J3k&% 7=DBDsyFWDuS>^3&8/ˋN3N.=>bp.L_|Eʘ\F<ˡMȨjkrf#_“GJU3#0J<X[#6rAYg=M=F*d"36EUzDt(U/zoJNR2-}ܯg_Rx@NӾ&aY ,D=-]oPkpFjA-EܳHd,""JUbx9] Z_(EPeDϋ#ʴ,6r]zGMDUY[ E8/˙ RӭZ=k%L9vSq~J寧t:)!A5k~GDR>x]vef'*:rvYe4E7t Z ]0ݾ ezgaeR=Ӳh-t3=_r__cU-/"}9[7=yyQyr!:rKf-ã< =_WR>Lskc֚Yck4Kȸib^dZk[XBD(7!4L=8? X$u `h3l ƿDDxZF]>t iw$}iʬđ J#2=IpI17Q -Y"<UUD.16HH'¼6r`21rT7 e!ux2dZ& Τr<0yl{ )5/p R$GOTiNu һ$0ˈYP -twsh}}xݑ)Cgv{T7!V&YaiI[eCZAn}8{D`|W'?s**N'9LN}k2ngoْ cln1C"uO*"YJn+ Yd"Rf8k?]Aͺĵ rTI1BDfO_0_yy5x#},2Vy)t'EKvu,֪*$"c%|S$%s1o#e{Շ2Zkm 뙌YS<MXԭolzf.<8q\T [v"=BKHr,Bk^VIHbֽj !ŠhDx\|6n990yvb `` :jMf!aSư"xf& M%_LGCI$@0[[Lj8  3c-^D1ѹj,fpWhl*eF$yf TDF 6f .>p=NԄ`B jFVՑ13#O8"`"}OR.YAukfaͶ2ׁPd s&~<I8SKRk?>u cmR c8 1tjfS L(S%fn#37X}[''co| Gf)uiDtRkaG)Jب!>;,_{|[>|~8޿~=LEjdSF-h8>?mlFRzf-u0NBT+ٮMsXrEw5MO=|,E۲Ne7R=v9+/q:?Czv'_߆O9GEN %VKWo7$L?r2c~ZTuՍ.2HuE ADGZZnk)nZ;}Y{%R$cA`&rйwwLIVě[բgpTpͯ(LX׵V VUK*_[%ÀK zt_,rECBN#cQcGX:DDsUW`+ TeDQfh+gcw21T[pz 3f-YKQ&;,ۿywOC*Om9M4ޚw-1G,wGG'$[Rk)%z{e3%Cys_4H)ԛJ0CD= @]Ÿ={rn_u7ZTnnE"G =>`VnE1w7XX/˲:+r*/n>}{o_}0\dL!(&ccUuO*Rf /ę5!^"غ.}} JjMP k ˁ`xF[֧f=IjRLbʇ!ۯ뺮MDl]m"Rq@YQ3{~y4_wClZi;Xm{@Qz{~{LU#12,d*k"Jf2C6̌вasۛ$C!Xt -f9pٟJ̻<NJE%̆)$#&|R.D9ϕyp8bY3" !O{OxD 6pSh)t`-_unT ]CxtCgh"龎cZ CȤcEYCJu5fv< \>?CmKvڱ?֋JHGJF'r -(wӔDɉEk9L"P->}O,폦ۛRKe*vIk$G$IA4NGVa>ie07E(O$fӣb歙0n\@7Nef7ܐ^ -MI%s?J<ctX }c_'Rd2w=6vވ([kE"]t:슈/6Xf-C+"www¥֙x; l v&Rk?>>672i"Jw<( ۦKvnN=cH_+5A 6HE uZ* im1""(Hr1c+,tWE*#U$=ň3 RZ4AfVFg4F$&cT3 C<!mۅAz2bpH%®6y)eS^pБ%Y0O,+Tbniy"x\2Q3{I7ˑԒN(?^Z33;W//?i6ķ_ݏ =֚pf"2k)FP-"r37|_J\G_~ÓHY{s)l͡ G`.,==-$:Zk-FUh2vEܽ5ݬVK=ϵ2 H˭HK&h9weRZȵʴkkQa ׌^OO/)aؒ,ۦL{bpB fn?[DD"WLghXy=Rgϧsdt ލݺLN|8tz:IDATZ{""f*"y*WuO`R ȩ:LOFD޾}iӼeYTah̵ᐙo߼a{Ԅ6zY`FkoRkeV3i h2{fP@ehk?Nt>83Gfs;0(yx&Yk%9Y gȊT$2F\y &) /(E,"\ o챻gzoq^ބ9y*3nD I_Z-mG[Q B:3ƱyHE 6]) 9 ۝O}ot(0sU =IR㺮f!"HdD_A_>x/؍6s#+wAx#.D4x}@CJq}hlEc6{)[x`}x62MNZ r䯺T¦.3oFxcP`K}˭r2p`20̙q<[u:lP{U\* wօyN#Q x/@ʤD;gk>OmBItDDe1A$Zkr뾴d 7@Jwr.#AֵiYnOi)Bo@n1h5ąGy&8nJ( TD Tcwk"17m,۞ |k 5u|z~SԺ[xZDsbopü;M4jۻ\ZͬkdRl4U3;=KRJoM K@;"GrwOow"@,p{# 8)|:L(9g=?||8ȔS9Of7篽2KGָHxd,<)"#F_uf|nV^׾c@<#ol""2csRg#071(E'"DfDB@@$jSV"BݽwswpiY Jږ M8QXVkMB w=0ȝ)>R )sJ$>!CX֒$$% @1u j2Ty'f.oG,,֡͡P9/iyTeۙ{FAeZUDڹI-7V_MDefك^l0QZnVBfZ^,!ZK57&OR: . |I58iB[7ve?k|/>}?ɏ:k^~-Oޜ V.2)"tjY2e5IwE &!i.Sܐ HJ*o]IDnN@_Dq73ߟh3%yYUaWU9MMSy@Z׵HKBf'sѣ%XulϞ=R݌v;f3z/SUU]kk”pqO|MZZal* HeYӓNu7WenX[k>:._dffAs.DnY_bPbuȶ)F%b] %ϔ,,95;A.+6p[2HF/"E[ ݭ䉘z~,n%3_?(L༘ZeKx^֪ʄXpssL,C_jfuڝ\"g&s[뉄wez>RM5$[u&<UjJ/Ct^Նi)ufr}ֲen8IbфHKA'"+AHG2K>AV.1E蘐TQPRE.Rɝ (1 [kne%~r3wаX{?Us=2zw`ieY0_̜t:Zc6۷tIhD(@[4M Ɉetox1_F=%ffîH %}"]Ch0ILNjf()ȑ,'!ARնvLf֊%dfL2߯sIT;7w]e]{)hzYڬ `MݾGt TK 7TV!“7~47qO Ҳdk/=<7eYH0K7sRg?2ֽbRk b-YEf7sa9\gX4ߠ|>77M;y]FL#Z/ tːdRaڴec :nWDE !yn"oE8h#S{2G_֚ ?海{,F(XgO/fhx^DxCHu˭1#RF"u6fpO$[tf\?|e+YWR`T$"FK&pblfqU60Rę"ydZa(.S fynv:_Ǐ33eD2a=ЅYg_ -M#D9=^J/s9SPQ\W+ya (QzZK`*z\yHʠuw!;2tEW<O-ֱ{ya[.OZtXȋW7W _.oq I }#qYGn7 AU½O}fK[î7_yxf?pXz!2sIŻr];t]>"$_x"NGukۛQaHFiicVr=4預9ZnN*bZ?akϠfwM}}ji?5[`?#cM9Tx# %5d IW!XdK_D#|+CDo˽eLYE(q]dd ,:A%:CΓ{F-2Mu$kxO!s_@:՛a7Z&wEAh~Pԩ -lP!{CUuQ&w֘GDaM.3Җ5ӏ˲p2q gpkY@A a$QB=Bu;R3y;l P3AcYy_m77{"ZH8#'6!A(EXaD(D,K̃U5q^df2",S% ` 9;?}];R֚@U{[\ڲ;K TYp3M2zTTlsH7Qvї5\pQdN~y5+[-H|yoߜke|37)z"J \KUZ癄5HPR}]+q$ͺܼթz:L0EM`#A_nZJQ7f~?l1{s.:+}f֚"uk;yiO>\/w3Xy%%jf*90\)0a}- d eD!"#Q6/2dbzh#o 4T%Ei?A y_{㛻1̘s`+*[dk-"FH}eލgVk./>x)%40c.U>ų]-UnWSGO"bw{$6Xȓ#:/;H},Vz#禵fv!"c~aFQ m"A/RU;*in#DEdX!ݢM`1k$wxdBH%^_0J 7Z[IXVͤ8N :Q J^`T94s (qi\b'au4"0z";2Dzn<©yKImMk{OrX)S b 4R&x;bh7'aw('4Mݸ$~~³Cos T$*` ZNoIJ`DDkpwמa ѷF)VK{F ,$!()3{H <'ȝ -Jfpo=J֩s @GJ-DM%Xזût {w$=cP-):uѺ:; D@|n` &KU TE#"XDөTw)dHsa"*Z,%A̓8Yz>-N 3pɬgPa9·áonSZd>wn*AEUֵv33+!a3}8.7Β7E2I"\fxk;N7ш+Ln>Hxw.֐f6"`˲<mL}9J)4ML\XNBD+4W#}+wx1:x_&VM7 f,:If?j֍a1Puf6QL D"%c%Rc "y0+KE`Lf0$›EY4 ԰|i @̿V$t:I fH.+GDPRB 9,.ftG& 22T. 6CuYk"jdC-ҟR⼬9y`F$d[-*VGG=@gr`{n]User4Yy:T` JylZ>_10w_+8|eoMr$Nd$:u'41cC7$,=fD"2l4٬uoK>y:/:ht7~^= (˱鸔R(Dl~(nKW":Q\(1(fÃ1 vv gKx{(r{n >  dʔH  B0mh2ZIthY{0'0 n2V&fov:[U/^J<3ݯ=b  8t~|ʹDP/nwE@cAlED[TJ)J6s)%"Xx(ڲ6D$2~m7Und@FK5,K-23KH ꞩmY1,T@kMיy71$pnL9`M"6Oҙ "xp80])Q_@UjAeuO<ܯ̘g99gz_ "XD\wׯv O:7r! y;qA Əĺݾ 0"ܻYxP3<-4c"A茘x<1ԓ\֟|{;JK n&~ӟTtXvOeǵCŸ'PTu9wn:n9"nwSwwׇ'bY3?Ӳ:0@:uV"<.ycvSEirf9L/n/7dGֽ/C  33x `Z.潖Qa$%)="%ͯ7qu (D "Fr[  '>:dB`V/R@9Q\K4, ۝K棓CΜg4uYC}~_< FQM8==%aB"ʥLd%Uta,xcA|>/빅 EkdkF Joܤ(CAvsyt緜 -Tw:'s޷T<"<_А{%H`ZZg^TEYżݦi 8 Pqa'vjf6Hj}h)Ĕ,[O3ڭI2ȽQHdc#=c9#EV0JD=I"$706GK!"r]C4"893|K(H3_2þŤE~{:S&{F)28K$ѥUvD^1pF)c虣ՠ06sVw"k9a2~G#"b%0~8.*O%[fBDcIȆ@˫,(J̈lgDE2±6 !;´sԙiP!Q9#\`I'g c܉$WZUF7s]u2H^A[WxR& 谟RH^ő譅*^?}ῘXo7oʋCCTUlIU"t\Up>yuve|SBHX>G"":O~~\u]~nIX#(yYgn<3,$ p[֧vwsӲj̱NKݽ88|*fz_zG/O?~t "JGG &vahl+ᘵ^3DBULfF0\# _8&gT3#$!RRQ!N )qMfm!Z)S$(K ,3+0}D,M^1&IdIpzou߇/.Ꟙ9CU[QP$ J!f4(1YW8Z"֚I10σ!ę""BÄ/VnKH}~}4bz `ӹaTaf0Q[ nWJB0$E( >V#| VsW5z aς 2<T3FR6f TE^s-,#fz =H}R %G3!1O$&rVGxyuc@GZ-J"S@V>t@n ed^u=R<պ @ؠu;[ ei,""EcC;&"".J$ srt')Z!nቭ*EFx>.:0Ⓘ,ޚCHJlSܼ8%-GlyIU14@a,U7 Yk7~yS> /K+@~#:/"JΫBac WZm]pؙźۋh IBJy^@|s'Xv;n}pd0%MA[d vFjln$糔dڻ`>==|8þ|ͷ^<>>#3UA'ǧ^ϞV_[|+n#$*?׿fϬ۲,?w8Lb:ϑ>z&"?kNE^zߨy%3;7Oqgw;T[ I*k_t^z>Vͳ>|qW=~Ltsx%ejpGUyMQ4ۛ[p9D:Z>ALDTK)c=' iwZB $AAø`\`ii l(vțc#" 3₱Eb7xjjv94xpb[XOD )E7nˠbᥔuBIB \\pz,#T}rLifotQ"?*0r]R ,!~]߿M;/uIL*ѷA--sNsU &3=@}Yke aD4܄~1S-G̔ \ڋmԑZg]׾lfHn}m1"2̚۵iJ)B<|k߼y=<$3lYvix<"1Y~oUչRd*OsRY2Oe5:??ݧǧX ga g_|;%o_p"Bo0 MRyAUjOou/^H >{O>ǟnp| }([VZK2%U>~/7hT. v&eY̺ٜQ._>sA\T Lc{1~ݾMnop?}oƸݳg7wFć/~=Q 3i'+ݷoPK!"-!?{7Uk i4b?g/?_b` ٵ %vfй~~<<.ox|nڿ}ŷ_}*]km8=?ܶfkoE\/K<ݷq՗㸼PҦ؂ >YSjsK.0 "ǫ tYn֕uF%3G&0)l Ԛ=>-a&,<;yLS]  qwdz|Í2ld443A|>JDEj\J-˟wO?_yl>w󮭆·ܞ_ӏ~zo:@$K>o~7\k sSJ)%Ͼx>7ChXj~EY1suo3& D$@?/}= Z|?}}f" $vrnF2iXqf=@=?x~.LaN1 sk]:ub0IT̜@L.=okmJFUXD}뤪} Ò<=/+_W X8 8ى-gZ4&dFs|1DBw\"r;+0 gb؜;SUdDą;uX4Mf"j@Fddl!La ̗z1bE_rK.0M;1lE^X5F L7 樝iAL™~5tE@ߢ at,<39G`gz[)"<.Dݓ4|\/E_4>H`f2!:ّIL: ykvږOS'n7)q9Ww>:͌}8ں<{:>|'vO1iIT^3J\f=3Zxju_Uhmt4՝025wy#;斣ݽ1z{m9ǧe~W?;"$}OۛNܻUP0Y=cz(DxE/#ۜmYDNU9K>p1b.|ܔ9Ys.Uw-Brto<{$ C)  ]GnN.6(2a=J)D oOҶ92qz=\+ e]/ڗ* : ffu59NBه,DT-U;)lfS5TV"bD&$c.:xfiKac;Q&\/趉~={kYG(Dz p_ד5B80RbчTYd#D@o#>DMD1 &@zCd&EWf<؛%qn^f33s!^r!w$$S;*("]_oe[[_}|HLe.`5AxfkwN"bHG'k!k=xX6sayU4Du?sVi2h!ξ6&~$"Q] OE0I[f$j-L?MK)Z"w E.`Lh#Fr(ǘBP TA*ʴ:υJ z\X$9Hz XTP+ Q-e`Dռ w?R'"ԩ֩y>{6/ ѷbr)oL,<<&Zɻ"20OAJU K-RK1usa*Z@NH<#2jMr%ijfݜ&1X'`mM?_GR,Z31*PkiOGKZͼ<lLLO?7_8ʗmd"< hFUL"21_M|HRA,hfH&03RR_[X\"*),9ѱly#g8HFV-#D6=0HFZ5!<c|ݾfN&̔`n#ZkyiyYZ꼛3**Q"<*ARłUDJ]DH@&1'g_ ǯii矽=-7?"6n7MsT0Of8D=nI4Mq:ztʋ G*I?$rByQg溮0?H/x^OOOk OS)S4M0'wr=Hwկ")ff"2f{"۰%-(.DRԲhsˋݼs0|H-dL@m+5 hU.,ymwpnb_*870*e[91wб}vʭ Ƹ:o"ᲩG8ac6Ì;~犰][kScᴆK'G ַTbn6э-DY'wT4"8" Ѳ#r)Hɱ$3"e(ݿ=Ieط.{GDfVo@7@r Ii$1=Aԋ'_Ld3Q3! 9ϭ23}:4ؠ ]*322}k}#G%$@ж?p/A@w[k}nI2YUvs<>VDJ5/|Q&3޸F?-~,:<`uV{.=fV Wǘ\3^k 0rZZ9t3"SUw+.|~S[G8/ˌX&"\_eZ7{ΓA]{_=nO'.: fY&yfN5' "8c/Y# LF9lyRY(lHpt:Ek2mK9 ~n|.?u#H򊍈~kcP-ʢjl i1L 2Mu*y1MA=-)F@B`xgݻq9 ª$cLBe* {P7i" }nu=:.ŨpQnCU3J›Y4?<Ӵ{^XhLVR4Z<,.eYrÔbGNGu5|t ý RR"xLa LtJ>q8`Ľsgm3s+yy*ΫE΄ _M^AU%(s- a1l[51uR Q kYB1YsيmAopJҁs%@[ϭ>p!p٤LZ<nN~q6WPTuc@aL nwwwBHy<5_ tiXV)!9V lO)ʦ# fA$ k6lD\>ӈn3]]]}kkm|֥T$y`'Dxk7ahpņ9_敗_ Oo͕Ax{:q?hx8$nhZk=K,*I?UU[k$̬ Nµ|FT)n>!ĬL$u*3nϷ؀ဖ jU7^{w}*ON&גiS<ۻ~;%T{>=hy!A/ ^̈H9懩QdI0Cl@F!v:Y)%ȩ<9[wb3rۀI SwHMU'\ݽ]mҖenLl-`&EFR>9\@?SJ@%S=1aI-9SZ[[¨aUf$P }9 +̈ȭ3e(Y5#@ F zNj#X/r|}s;YTf]d>616jR.mMb*<NKn/0⹔"qsIhA"lHCDP/ۦ#nB޻M*̙ I @#*,8bzu)pe]fƢ77 _%BZg)H'p=ǻC3rzڥ#SzZJ̥f#|aAN6YxN!ϳ?"aU}8PyaSIGo1sOih#بe7wNvUK߶3QN|npF ^}>DHjab'Ev Rc{ۈwMw{cZчy5{ [{NU2X{iä*}ރiܐ­E?-y繋7_ oG gZyQo<~d8j݆"Ʋ^{o|djҰ_zQx80HU}X8B5srl1(HdcKQ#(r>#ܝEh scT+' ^DaC~$<̥e_lϰ 79_>8'|t|?d@\@qSN'&![cza&VtR> GvZnYAR1 Q~`"P|i 0S2+l\?3`l^ˬ3\ 8({uLf]#֎n[^rI.ĵ<~?;L;%c!]x,éF0[},ۯc&ss8ym% 4ZUcF!rRz8/u-e0f[YBlT΄n&~d\?$z^!^i;^>w )ÌҚ~Rg)Awmj8 Q4»8DUDo"˝av Qiwˁ,fflH_ >[Y9F;q70!|y ^HDyΖSsof6Ypgre䑭Z9rrT%e!>/>mj|/? 6Gd{kc($ލJ|w_.N< $#"CD@N˼CwOAAEp\6.u|w答Bv gV;eT=C&hm3Nu71N&8."$G)Kb@),jl_R#aiicU?CDKQC=<@4EU 3Sfk/pZ׵_η?) \3P+biȦ#JH<s0EބٿCfea}Gdyǟz?*zbe{v>: |k7{F3¥ϭiZuLN. 7dVLfn "91T 6`AJqy&Xbxo "Ͻ LTE0je2JH>NDvVmZMl걖wwwwբn@hYf=RiV==oVt ›7{1O~\ZVbf{׿:{R֙%} ;|dU=h @ߦ Z*H,Z#N6,E֓0-5w>u_[ɳgwez\_( Dݣ$bf[>==.W?Ư/VE}G8Cwzdvg]r atؙvvGW"LIle,J, E0V w""*LJ%ˌ8-F#37za·e-Z3)0O{ƿ<C7_ w4[NbX+V% l"E"؃#rknLawww˲E  :"U@nnq1ncIHeQj|cXOYwN| ;0V^9fAUm8|=D1" wOIeYG;|E=v]Jmj$ "rADHnh5Սֳed+;@P"|)9Eȴa n$$|&Wyx9hx-4Zk*4Ұ@BiDjLD b* 0-4j`t eabùץ ɴ}{B|qf0yfW8y.,0g$l>}?>z*__gn{&㺈/8fVr`g3E$7{Q.U=@xQk[U1{Uv3ٝ_zfbOƣ c07ewcnN*%Oo_ku 4s~>9oMkI#ib-X`.[C,:' fx*kGZ\>'oOMH/l'" z%CYp2PQ^"|0]l (xxs" iNڈ*4 ;J ֺҔ!TuT QmiTu>"2pS̐-ftn'M(^\#[4 $5gt6 Re ew~#U"g)b }6M=QϏ%bcy]F~q|yx 8Bγfʜl{)4%!]!ۓ "uR",ˢeR*7l#@>qj?R繶j ~ֹtJzv;ZNo"XG_/ @a@ 77|y!L]#zޘyYC]%Jqa Mȓu`ݬ08v>Bwz+Wھ$t%wy\<Ϟx}4?xz;BIq^J(K]NQm-3HX8.?ϩsmuОsk8[jER/C.?7U+=BŠl>0K 1sME2j<)""7 3[3.8˄.\Fp_tA B<|9 %Zknh1/ŭ!|#=08udww9Hȷ aDpH!^<9Dbdp,ƙ}Wj#J6K.4ð@pۯFL˜TJTnl}ýh!1w(RĬcx TK-$SeGi]hZInm??!8(>5M*4O2)UKW)[w`[ |s(ŻއLE!׾o?Z̅̇v=<[Yeg-fnm2ݲ9/6x[o2~n=ax+0){nBlOz?\Ύ3@TCd3&d^TA@8T2{p>3*h%ғi;_|%a)1!aL ×2L*ʢ(K)a,;>R,c߆ay,`bD1p"N-ay"Dg֕b͓.w|m CJ=dpnӒ?q@ LwjȖpf=йO\ ??JXdzSMUINg&0 k/@K3k¼i"0NRN)@tMgOǣNU窥*(|?}¶#f?Kp u벚ZZ/yc_嗿[r=4[u%zǏ? K)dXk}:-ww徝ьvoַO_导ʼpU 2ZJQ>¼;={ٳgq{$Yp誔ѓ]/| {Ng=77T>K9"ù0FKO<*|n"|`p$<+"㕘ofќ])UxsZ-EVϿ]AA.ԗ^zW^sZ ,Ett?lt{$2RbײՖ.\Lek_f,k䟓vsc#? %ǖ8hv}n[&΅?^ KYgU";qu(° v*v@,6Vm6G󦐬)`!byF|>"sJ.\hӥs"(͏VbJQ H 41:שjqԹ{ !II%˩2ϻCpLDT>o?g?=kwdMY{1a\'K7\I3sQ _7n Aw_wW[_? `8iEfv}(}}Sc_ꗯfk; Y~%kH0wd5U[$TSͺ F_~|}5晉9{0*뺞N|\w*,{}:{{?@."PFx 7Fn"nnvErd  P9 4e֛>~_7Hky-C=֢η1)ժS!*`1Fv8Zdܟ"(ZE}Dh&N;ɫC%~}2so?x套}Rq )3a6DW6sHBZ P?Z<_Szհ$y\.ӠxƬoGFbH6%[0K> ",VY pZҢ"[RNRtR,>!l8,Zs"mBHsYW/?R.mtTHاR*n.2ֺ5Q_{N>zw˯~]RPJ)Ň~+/R^uQpw_/Wg0־җ_oNY˿{?/_ <׾};ceUs7m RR_ֺ$ "H!ls59-q#ی3!&#N-=>+>]>& 2YEH}7ᢪDZhGi3 <z{gC7^e%0ZD."$JBN\nnnF|گ}"@QJ03kRݳ i6 T8(67vpQo|s@?y8gW BX:Ո`3nq8ÆIwuoo_ygQX"ʉ e$L,(FxN ~sw>w{{?7bx8-ksº_?yWS}G_Wo-Fr*#~soKgr~\=Dޖ2q9>e}KǏo[[G0}7Rʣi#}! oWsK#;޷%SE-S]%ۯ|e~o~ Ј* -JLDE)Uq9/J|oO{Cb ,-$죎1-@.w;-mmmtVy㶷a6"$Ҁ޾8 0\`.xa6tVXms37%D۷mv;!(Y_$; pESw\.2C9>G]O!㹱:4Tq{gruu0emram>K** '{ f@^,d#%#ܶ-3]?}Rz9-mhT$/fF8T #,{"ZImSJ%rRJyot7sGO}z_ @cPѩrU EG7|iT++}W6,"YWwHeBhJZ#R*ODp&iL̘~YG7I6̓%3̉f&ThRٮ&}h4`)Hvϣ#g /b<xWz8j%B|'&M*\.UKFkAܐ#j/) ~n:1FIUvmD✤{v3eyS?v;uj/j& ѻv3*3U*Xc623"#C8A".6uNHH8zUdm"`&KZZPûSb~ )*)ި2FL$i҆م㉀y#w V9D9mIQTMOJc(TyW' 7ׇu]kZe9uRCNks1FIɎ*Gr%"EDc"UXf*R(`*ٹtSQ!qEyo=G6KsUimUF/*kkWo_y]-Nk,U1i}텥 4,nZe֫{q|:$ |fJyN^`ɢlA$4o;nV,cQb SQ;Gx:skivV|{$=DPpyHEo47CG|C8RvS ~׻A F<υ sWQ)!H-b/LdɻfD P-!}X9cfCD>78(g91I6axN,/?,9~wͮVRyf@WU plik1#eE !aX3 vf!P!a(\2*b3ʼSE,ZsXwD8i&Q噼 ynEֺ+K%K]9Dd&elf1OQ-Nӟʣ+F窵aRn"am]Dd (DDHD&N3J$T Ssa#\soaz0R֚+xUaIdE0Uw=NTKu^yqEXYmYAN>:"Q9?yvj҇VĽ lBlgZec7{7ySyh}C/ B0~ɞ%"1=/xg;[mn_c 7H. irupW :q)݂/OΪy|2ROtfbF6 1SD$:BtIKDltĤ?" Ec%w+랙3fiFjn#*Q"t(BX?R0^V̜189/Ėo~h/q@tgr$`n ٞgQfb˾ݰզz3Hϡ zZx9Da䶭$+$/E#7I/lvdbމ*[V&uuu(KRTŦ W=rFa&DzOx U*sa²SP&MB 7XV,#ظuE*OJTJ2jQTSQ*"EiDfUyR0Oݴ͙γ@0#PkwUU碥NK룔-6 _[R2lmcc' D=I 14@d;ں4KQ2w0OE6}0\5 F`tcHۺN`~7ޅxu]MKa7*Pa!b,~ts{<wNJ_zoDy?3 3A,҇@k#f }sbTWbs3V-LIV6 <\<({f0TZR3H b0"P"| J)u* ͱ6Wc_|uP:33<ˬW]),Úmѻcw TݗVYb/m-qbi^z_d8aR8kK/Ej- s[ͺU 7PIٖZDڈ])LXו"@cD#U5i0OўcûͻY?Z+NN(_z4q**[6Om msv?9 {W0ű|86g>".rw!J;6.^U88+z`<_'godzLDrDѴ>]S@Dۅ[(孞'X?z℟Byħ T?~06z'zI=LS"$lM{y'`j)69}3"̜BqöZfXxHԴH>8|O3yG>;j=͔VnJPŀie&2 HuPh]ZewcϑZrkj}Tt`=U'O1WD+,@r7ojHDXYvUNE=E͌c։\k%1, Ŭ5u113;ݿgڿ{+*^LEf[M{ݷx9a6cT '&bP/k.pà]9#c36ϳl`4փ A]nvӦR4-0Q]uw3DՃsH.3b։hDL1rnlS-}A({G !VyxQdR45!D3?8II qsM*J#X-|?ۏۻuﶮBC1z ??G=|'NNS!Cb"`!uuf^͙a^-JO~"P x3UwFu" StfVH*u :i5,0Kv#t 8G_Vu]Wۿ߿_5c)wN:  4A,Ӝs&V&Vh 'ΌR5<%`\y0NSE2+Ey*{v- 2,YF[@KMm4Wn<"LϞݶ֮;%^__>E5bTdԲE㥗nO^~{[ܵvYa\ m=͌`[9B*2)+"xG߈j5Y! 'Rb" p 'ՐWIY7Pa C__|W;Q+wRnDDX0# -'/]ᩖhQLc~?<64 rH̬p1xtD<ϻ:}7T4`GZe)}zj+`A9kV[nhvR)4_[f#ֺ1iej6F_z*wvoLTʕatVERKQ"ȇ EaY;NN@0YGPzܝbS NQiƶ*%wgάcl^%AP8ňL!̄⁠ 8鳟?Wn_W_zٳeYfnʬ1%q)Y/5<,Ψ_^onnڟ>1rutklģy%~of*%B EUD|u關zZ"1ؽ@>2~r,}!R@-&YA!Sk=NM'Yϻ~4}|罓X(֎SG*#g[=r}\:aݶ|p{bW&ʓ~?iaa * {H]{:@/;X}=dp/'T#B2 bPvyA4ݮwrª2Lf s)eڶ>D'(ym뢪$>{Ւ{n8̓sxm=p:•s-f}"3(vύa?[(,7"rҚZ$RݝM#OcUi v:G^㻷ՙ_gҹ-sSϞpa<2g`ri8|S n{a~uGܼѢ%8m`UyMp7+o9jN~ɥ~\TJ}=~$3\_\яaT̵pk}íYI[8bD[2 0Œ(*e~?s`g(Si01Zzk}T[kw{ Px)B7_,u (+2uuDaf6BDLmYvD,wl)fa"JVZ4/D 0dp1&} ;xwq~BE3>?fl ۉRBy\dE1r]tO]Fv3Ĵ$Y~g'1*\jZ~Ozln:z_/~}-e*8AEX<Ț M}YR K>Y2ɂ\cL̼VXRI>^0aLœ(KYQ3>c'栟U[vdF?n4XUutJ) *fvߚJX[U qiiXh.D˺NASp&i k||o/]w*Kihe5ZD(ѨۙL. HHUƖeD,i x}#78x!0yÎ\;<2I( ó;̾%]T}q,Oyy/W/W"i|ژH./멖EDD %њTD k$ͬXS[k.9~wWo @14ep)#"B$UJ)H_en7kLxGz anB̬FwpJypcUR$( i7Ѱ~{,HR4OZnccg$ySuº::{.{7C-)୍2UT,2qK.;K5ֺ̽vLquSONïO￟3H9` Xo;Wi*4FtXIH`3R32pAUoG Spo˺* +c֫3aUi*GX)n˚!|׌^p m+!2+F0BGusTZdY{DG׳t ozZQ2Zo!c׼̭2ټe,S-lLu+ZkZkѱrwi7mi9} 3y4Q湚`];GPjM`\un>z)%gUѳ81l̉ch9$.A#rý̢|Y>HO$?xQ?Ƌ|eoOP`oc hfx{:7†*#iUU qb/^uI YyT9D^]0ee*u,t\D"=bY!g"ÓZlª*eYD1z?݂XDwsRTH烈OeZn־Y(k!I0C||1){y?z)ӑn;[_={2⪠O~RN@bDûu~iQ 3AAO՟뿸 A}w!;!V-ceX"zo`Kp2A,_vـ.8xW8W]DIr,?-kvSr`i޻I"R AU5b}a#x]i@$1,-@a>&fTaC-Kyw\ a."$tuvIjU"hiu'd8Acc^'5"fuޝ "ZS5g9Kf.N:)X T|vcw˧Q^?|6ph}D>l,f^JNDži~<# *!zv9nfS$AS.$ӧ?oƐy8(/3&cfUKNfldq`j ԩwƟl)gNE0S` ͇U+6I {]S򝈨Dy0/ZfDԈy}&Sq$㲴WՈe$W'Hѵa*r۷D 31lijc|l m+{ƠyWNLOim9*+#noAi}z}} __o}ᇟnoo_/?=`l''+Nı#Ce!m(3"LSG3$;I#z|nE-9䜽_O@u/} { T˹N |XRsL~L)eIKd>{*޷ݕ#B2 fG)ͻv#>Y>:-{)YZ@@ ?NtPo{w?-k{ ։vرbq%x<8q8eJ ."8BU`NU*=tYS~=L q?j@%%"6vR/fZw?Gq/X~x`wn?wy`x.:<f6Z4E K??s:./쭭DS*.EIEmˈn‡ZAɊ$mL/1\ tae} `yQ'L90Y̕ Ee>[kXzZNan`-ГYO ч¬}cUus"ٓi݄Tu*LypQR":nt̂Zkw'ex7_ELDplKLOpDrxQ܅Y=62(4·~ HMH ̜I0`+)ΕဒăT$)c(%B^͋Ͳgz0? -ꋑia7vj7o)pLdYge)UiC(giprIݽSk %MUK}]D,F~gDXxZ]yKͼ/9˺>"6p>b=dctouѲ{ !&Sb1Fm-2{Ǧ_n~bmePFuAHKĸo_''-~: g_yG⩺%3Ks0"at*_ooGt@6 R fB뺺gb8=XOuZz;2q wM(Cİvy>$a,#l 3w_Nn&vw̄D\[[#(yaTj ћZu2i[[T׵un^X}AhD-kUvLnYږG},e:K-ruunfũ1/_Ƌ,~7o7*gGF`"8b$f=M$B3%iB{K9v9 B,v)#,؇ }X*SJC< &?H%VRs`C6wonL JK`+Q*ZDDT4M"ϵg^aލnZ$XgͣǠxYDԲH-kI28N)ZXED_ݧ"P{V{ڏn!KD<7}DN 6SC0o&E1Fkmq8jr`a:I2K'J,&i"g BWݿE/qyEsp'GUV~~mşc~>O<!!|y`E-hF[UcV¢Ey1dEއ4h㴎udˎ+{_šB˴֪,mܯmiׁ֝mMfo[>9#b>6dIDDƖ viTF'?yǰ_.T5dH)DJ("I I2{=Y+"?|޷\c{׊_LtUN h[֚jxWg.]DFHuiJE0gMԔcTնn `Cp4uYn꺮D"bhn#iaJ{kcA@Ѻq\NǏC$ ?{Sz<#8!ÈLss` g3a<44lm"Bt֚ZW`NGQKR"&< _uoy}'>?>4A -1!M az ص<0!P^ P躮 h[B7|R/!UR:Fx( k-I@e''T5`ӜuѷҼ˹9) =C F!<`><f'ie`as8""ޓ wn <U6SjE涴I87 jOyi݉Ép,B;sAbеRZ߼S"Kaݺ8RdU ZumḿzАVZXzy.qi'ZKڑbSuaޏ-j͕v߸{ ;oROby:R ]F bce"s$d|͏>"01⨹rg,R̺B{P0_) {`Rd6,C*?̾HqGnP֏˜c 4370eD*@!.ĵ u(A{7U2nw;:Y[aRh 0O̥ތ+":)C:.*QeEB\!G(.ϫ_ym6tT ݏ(P :rK)TJ~(,|Zu]I 3ik+`1\= ͛/fIA[NAHqFojSķ73m.FiiLӔ}]WeWWwgRY~g'?c{XKo~gT"B ]ΝQݟ[et (ODPlALeR' G7ow6R;"6qb@ȞBc]5S(k,} sQCZkHq$36qGPw?ƽ" j6Ͻ^>]_}kj|=i/MS`PHZк?–!?'{.?X8<}j~awCMg\cʌn/]ÇaymB?ۛ4ӼPjD,0KEDhmՊ5ͳP6 eb4m"R0KPs"Lc"P cX4sfܓJd5sg9TϞ_k8AQ gI;?iEzk*je\YwA?=J)4M_܊6=}v}{NO1. r"3yx # AvhiI H*'zQmHq#G칧\欙{Ff=J?$L4>Yntewu$ލ5r{N:/);# A#Y=:dO6Ml*Z<0Pɝ6U{[mqFEIJ,"bV3nipʹ_F1vu!ZÊY#`eXQZq(هӾ?}}~?;]_4W$:6tA]\axxi2Tni֡tU@Twwj +6Xr-a`m Dhx?"hMqmlfjaaDRQ @jU0A 9J}EjJmZ8VUrzg~?0x0cAtp&FD$})ۺV@k$ngAZR.Bݖz k_vÇN,VӲݚj6{OXPe("Ϯ]OJ% „0-#5 d@6u!/]S!aTmLn3JPDR$enj0ܸ{k]JvsS&ߍ0 ˲7&XywϞ/%" [SQskQ*HcH,Ap֘SȇuͶ_zg0"=ԅzʼnXkŷ}g[C*)D$ `@0}YV/uzyyMvnnQhBBSa5f^zsL٭H-OæId#nqwdB 93oǡEh<y: =.z|941zx% /Y;PďX'r-݋[f."A[#3`X׵L#3m4>`^~>qZ3~=ޏćk?6 @W Z6VF htV Tkk/˺OTjDtSDԬ 8iw效ɋ[L(8B'ttbŜ[!c]ZO,8uk"EjBX~o~aw:Wjֱka!iNKDՂІqN؜XaƇyeMm˄kg>;w/p$D3'ps tZ1w z#"$D1)1ϲdfΌ̒ĹeOٿuDݡljWqpC7wǑ,-07cIw~Ƿ*a[m1"bа0.ʂڼ&BOl5"^{P)v$V/ 2NhjOC: i6 }m/Rˀu p%(GncInjD#b)挴]U"zaD{'/I1EMZZ&K)֦iaaQ˘HZ"BOW;7m>*0@[7-olB} IfgMP+#?l?DГK Vdګ7>̈n&̨ٺ@@& 8-kS iCy^O[#q]zo֚lEwp !xĖ/  )gzztTOOr\[[_ʕvS(Hqs}O;"4ֺWz>YoUX1ZY7#@MYri+JYݛ*Z64CD!,kWR{ܔ5o?ԯڃ@B@\2i0ȄL|NR' !x_o!tsnESWx[zWϮ!1o[‹W ĹrO{G tq7)e\H@֋خvPzlJ.ebHA=j=8wXmf= J x"pP G-p:E&IT!JdhjizF5t Dj0N"HPC"Y@j{ ʒfӦa0@unVVyxxPDw !<:& Z C)ګ7ˣ- a([ߍ{W)0u-! #i}w|Z]we]ֵ/^# ן_=x¯|?x{?~3B|hɀNbGfDěf1swG&gŤ:RPRܝ<= !ZfnnQ PҨ°o1=ipNsL@uZ o}7\ݧ+1"Se曛 )P̧)+K)%%W*H8.KT p{#eA$Lj 湆+%Z8`k 2\zn?ϳjf8֓5y>:Dd4xV'0Gu׵G0DBߠGΈӽ3s{ 33P{.-_*뺪9%Q,uťq{wT Z6$,[{̺]Jmq‰[z߈ 3^;Dja5{_HvuI]=.sKڔc+~=m/ϟ }]y_j]e(|jќjk֛0[kn CM75oQ bk.#2 hkR"3F*, uT"f\Ձ]DqsEUikEb@u:?LM}guX1|?V"=6uBK<R+8u(VE [w=a`,"*5zvq=~W 0޶yΏ|: =}{gG@hClѥǞ#Ekl2i(HӾ )m.D(\@ѝ7WNT0s'P彀AHMfy (, Tոd\WFDxuHCO©5&%G DjE6p]U%Mkb68st/7ݓEu%%"X,zD66'6曈Хb.X#gJᖼevڃ0"" pk ,\{'F@흈XmvZO;vlvcf Cat F Qsɕp`t`'dϦ46th}w炂nFu`뀸;tu/&2uܷ=`һ~M[绻;B97]͵T[P7I鴰RIY[j@hUC .b ׹x DMA*J4H;!#c3910n`Z*Zl.c a,͝O}a]݂\Q zeYZc.PtmO#3Z;8[}Qp:5gL ɵc|<@ h1hl]Ÿ}a>W"8L &[?͟w o&i_[P7nn=(t@N@t@3fio)p G Putx:~;9$_枊_W @pӞ.x~*)H>eCRJ<K_ <"IضGVʮ`MDJaU-e.R<38"=!04U;k!| 7Zm c '=[vUDfݬ03_>~]! ;[녁 33kZ"C !*}'V.34ݴ[I" !e7x"U-y#MmTEF,pTց@ ւḪ=0UG{x ͫC;xe0KkjBJP@p]uw!b`YU#`7$HKI:!@ [{2 8ax[Bgn0I)4"TtF" &p"ΩצLkhk@&LG:A@ dN9 y`S%FW#M QbSd;~u`vpb>-ܝ@>Ngf,`m3.9Wb"4NC)3f@4brHp5"(,ffሸ,K=I[5ܽu/ovYU=' ,ź9ZwU]]TL|YZ$"T-% pwx2ѐDYi` t rH\,"f%^y6P=LDڒgK ȇntMfk׵) 2 k 3!v\`AgtkCA~?yX !\jn< w#  28CXcr Ei=QTT%Ga @ 4 hS  (9&R*8iޡHF gSW#f? nD$iO#*<rA]#cӰջ{I^De=[y"P>ѩ\Upiq(R8"UHi`fiA&B\Kɋv ԑƱôMX-jS \Դ2#bk 3#0X*#]. J9=ܟ۩?NI~}|>8UI[0X2FsئIŷs4 1]R-̍&2&ްf),c) 8œ#4O1ΌE JxvCc`<'681PsL K67 >J  L3>)66s"s彽q 7#.z% kK1a>#҅3#6{eKj @@N9bXA$&JLv~^41 e!@#|o4wnHݢ :sF$2д D"{ScfFrGDgʧZ;.uL%:FvcWkԈ`JD`x(wޭ Uz7O DQ6\ E);0Cv[ݎYLY0 hC0} 8mL-K\A` 楅x61N{"DCnh;fۈTNH 9 g=i @\&{`(42[f4MC@a^מ3I#d`6n`\`Wz:ˊ4vf"wֵn"U#bxs^xD˗/BDRPa8sʗ?} 5Og_M{mAA(@tO-O ] - pKze \#0R%k+ U,  6 L;=0gixk!~⍱TaygϟG2pr $PuC==A33R}:Et V{ əH:0pZj\#zHuȻf* Rjo=xl9c5"kD Ә i׈fVd0f5 JHo8Lp B0u@ %s̏~C_4HwDw`mBD@f:v G"6m[2w*Lc@ky22aD'[O'D u=N0`rR1 v`Pgi{f ( U@tuU"\5k ƉCfStW=_5-!`R1 z/"Ʊ"jQM}v)65s0`(B,XF$L4a7pL0+0ḡW1j-HoD"2xxx(eMkfV1p^Z](>Tfhf =",bnov?ݭ7bufS#IU+ ~wuVRb=~- pOƏ~շ}S+.ݍ t$)G?0Ȕѹ!{v@g8B!d$`FGB $ t=lf77/o/6 ך @` 2δjE8Aƶ?Ij""%cc5o$t t "%Rᘚىf/a dw/B&HzڸܗDV K\Zìlb#y]FH$yA:-}d߱vFO|׷-Ep?#Ko+#2 YBR܍ȵ5s M7-a(9 ""F~G)= Cfn!a_p: P9"RCNylAe)]5@ 1iwH #8 G< 6 ɳ9>Ļ>"@]M*EtY (DJwDXC ,;>E[3(U\fAݢ?# Y"[8@`P̢N2ա z83.Co8MUJRQwF8g7{!Ǚ@-qӃL8TZxY[w' UUB\D K)a "I_a8"Bav 3x #/o;dǘM~i}W%x9bzLxl .$  au02V&AiPl0 Q޲ND}r0/}w޺lwH9_Ov;DBMr8NI@Fu5iϗRt#dX%y!<ۃcl@W?AHxXjNg0LQe߮#Q/sL8U3 5[DDn="\e"*,)hjf\ ^!hY[V3ɍK[,amY:' k1qsOI޾ly՛=k88mi$lgr 6G@tn̜ʊ r8MˡˢBe;Tm`Y +3\ua&@$'$4E VY-}+4UUEvD22HTs4u L 6 Z\a >8i h~<ݏ,`ixC!33WD*B Sn8$U%@ 뺮k7X'Aƽ:tPJ 1TC[[nCyn{bv~x[L!ܿ9/~aYy$:8JDOgHizKFy>J~dV~K=`dw%x$o+,Z,~͗_WR-uA2p''#J)`sV % 6wE7@@jMf"\w uVR$M=.j3RXUӓ F-T#%}N˺oZ@[)b}u"EZ< J"'*\5ZR}=nafh8v4R4CёBm."/Og  ?z ϝqL!l4K?Z[ 10v(1 ݧfmDvDJ8[d35S-C9PRR̉l]Aon0\TU{ Ņxi,"XM'irv.!5JAwOa U@` њ"IBk{dg L 3Ðvn`>Ln,iMA˹wVمuZ&!F̦[ꖬrpWoR0#Wz/[)aPF} 5jo۲UǚRtmJQ/KcZdT7^`d:78i gfF~Ju8.RcYD0RY^e&$PKR+_ϙ~xX3޽xx?ɟ7||VG@pz4t}!$bC@tnGMϪ-ޑpAS~ŭOA0im t-d;Nǂ( ћPIUu%.$LmIndy)\4Pu^Q:1H: z8vk8T9{kmF0Tu)C*݃QQ5dO,u({I\>bua`;3"BH`ň1 )1~?٫ܿv~w{{ Te?A:7K_«}Koy~xA4۵߭^_?G~|$Y7&g%DdyZ24/\&HH@[`e-(_L}"*w>Mz0?_q(!^{ 3mI@::Rp1 nq?&tql"bWKDh7D|e ̼O`x׶ZFfkDxWUM ehYg5"5ԲJBEGDs03j Yn,rJ؞ c=MX3&y;DUKe}%VA4玲JDj4E 3pxD4M1/kUrz'f3f^b|ڢ+FhwCˆP3r4v LAD ꪶ,˲,?-'cqV 7W۬ED3<(HLA̓#oko̯|>}ß1)HTb}CXN !2"@6 hhKpۜ?y KN|kl8Epꎛ3qngR%yOO fR#GN#¼-.N} F;R$pp23جACӴ9u$:i쎫MtUXìlF`L 1%(*qԁ$Da)HAy4WB󭊀Kh2,bD]Bdcлn+RD@9mc=YGZkaN$Ґ65rDA- 6mZ|J}r0j)S-P0K"q<4sZZ+Yx'u83VE5yY9{"h`f?kSa!{4jvJ݀dx\q꺮H< v%@ 2k-QFBRǫ+|>U]K's7&J|tꍺsz;%L@RQ;aG9o w//`߹|?o~>S+sz9mJDwvEt&̄{ %+ 7Oo.Zv;P2^Z0 4eGd*2wE RIjQ{@BA4Մ r0L9bF WRB(stqDƭd1qe#BE6rݽƐ㘮2=݋H$"izBDBB֬>v,h=}e0"qa1j҂b!f X|޺,5vJ2xnf)L A23@`M#Ӎ^6M>G %<T5MYHl{eqVĖ[Sֻ#8V0 nOUԣF7  s,r<55(U#`$e5"Z{P/"RS #L=OAx-J(=b((H$騽7+" 08NE(nW 00Kqk(nHJ)(jEBZQ޷n7{kvj 086"Pxh8>Xs, 03G> DU RY^Zj^]BNˀ+6o7ߙ%!U|?܏O<8rpv @(Ė=DdȤX S ଻a03tg~k s_7}0' (˹4 B16"sHiT[E$0"R !7 !jyKGDF WV,ͬ#ي ּ4:;q'/" bEÀK}p""@ ˏ斵6ɅEj0lh1] /$YcVG3RJad:kak ^T ;bGr/`4V o0)F)wi ۊ_[pA8oDxZ}(u"[3 (b (t!hf͸) @T751 ָ#Aa"Ppjn\k=^a?മemVq(±< Sn:ySi@7( Z@7G "i hkqֺQ*˾ު@Fpw Pz?i1IS&w/iZZ˽AJDBt}Z`ԒBPx>>p2< u=_Y9'iftg7Dz5 #ֈS%7ϟe9uSZO2W? Z1{tw|ʳ'ׯzxj?ۿ[k3?}ȇā*9N5"BwEqA]Kb'3=ovp֡Gِz_?K;^ A*Ei/^A\]'@jZ8 úl2V5uX_Qį PʑKc$?#_Jw~fg6lٿxz!{3u+C :mWH(g{bYxJ"6*X^d&3cF9i+"P/$d<uF:\yl##Kl>4r&"P]"[.}pbf7sͮh =`XҕQDDi'BZ*=ZCP6yR) "6Tʇ0gڱ ^'$k@CCiApOM7OϮa]۲`0c`&50TƱ N!-5 Hi(8 OEjapۍC-\6Fq8k_܈)3Q]ռ9 !{ka@͔I`2w8 aBDՊq}}p{xxHdXdnܬ4"|jnʨf0ހqc`" hDKgj32`v P[UILZIDATӮy(" )\4Ŵ#Qfn$4I)9\]vzo^1)0Vy:@n麗)jirb4Uej5ww/{oLZ[#hn0N4ZqnĒ3RkkDa8Q5R![’lP}("54 E$:1UK8ݮw~!`q%!|uj3br-p]9矉WUO1SAٌi_e*B.ʥ9'!cXK@P@R咘l?.$f͜=6Sߏ MhiIx-Ùk]@$-p 7%a& |$谟6e*8P ‰qJH޻nÈ#2B B.Ad(<0xWRݔ( \aw:=N']~cf0sH."pz{R @4zWgb W `H:e ܮQ 3)kZeô @3X_]ti4P~qNo''͏W_yz_=. O NvhZfYZWȄ{=Z#bS"*u]:vn>;ӃXs&#.ސCDd)pmsO:H%7"e&Pkjc:Ib\A,Q S 2R@ $״D,uM09Lx6ÈLSXaQn-[8$p6"RC;l0/GY 'B3`F 'Σm\jgB8c[Rf٠t7h42 К^MVFD81*R\__J椊nVkF!m`:wDl̷oݍH6/Az'"Xc]J""Tu>z5~3`#-{k- fhj G̭__=™Y74%]t:jwD4Y*U{wyRQg^"JswpZk۫u8_z_#bgH<}!zsW1 )H\0z-pߛٲ4pډh]Y0- GT%i̭w"P8fVwÖ,1HG7 ⥌Hv BPmVCmh]27CwRC `sn! !BBP4cxn`9"ܶv8Hٌ D2b3Ȁ~fyI6Z`PeNsmOu1ҭ-9tx1!QHp'e,4 ]֙"rrr`pMU Q*ща]S%T(̄n݇:뒏iiBtL9) Um="'` \Cbnn9zU8=n=i"aku&J45[g/^ĥy^~vg€_~/x‘|Wҟ?|EEw!?ʟ(`vmUlU@p?_/qJ_E< oL"bsbDGw9BtL42s1)\5fœO`DcS B@8g ~Ipi ]ܵt0st2((wZ2$_PgPED*-K |S$8>ܩz-U$J-$ ԌR6ᴬ cI}/CA@!&GWʼPD8r|p-"K[y(t\@̥J^Ϧ̵ WUD,q])A^º?a^ܾH)0 z/e`ޖi|8-&+x>ɓX4`@q9 |)Qϰv=Oo6AR! 6NxDMB{Ty+63~?nޚ*i1x_Ŝ+3:ZS3+A<܉\ a]-=s9NF@T ֖~g\ܕ %t:-^t-v_4SU"!8Z۲>>˷0DW7JӮ}mXSK-okK~*~Vׅo_/] @PkBqĨeTU^ы(<2~5a]" a^Tۻӛ/oykp ; /?xqԅx A|__^ڲ`8G]ݳV kEݙ97Ͽe01,'yi cMc5P Tdq~0R]!ԭkgXqDYj&:ԑ)Nͻ*"z貜zc_Whc*e{~ !~w8Ȫzd.ǶVxZW^{_~wIGp8+???s&SiMsi_:Osg=m1<#jrT.-eGwRZkONQ͈B ?jb#IlQiKVQ9"ּ5jݳedyEt,"ő'f p}C /Հ{ ?E7SDk"v @:&[!X7NLۇ(]}Y{[Wbܸ HؖT8VvP"yoԃqAHaf caaf a7k7# \`B@ yYOb@j0xt~I\sYm*R. 8жLq7oz}$ _S^l U"t}}]n):"š~큠#u>SyXmV9>ܩ2;6SJ5iY'Gp^1iDb6Mq<Edۺ|8f-Zk-@Hyjnq@&fN3Z_W;;0X B"s#:QN)SwtfuP sݎ"rZ;MX۝Fj\k%֥z Kk;W\D$B#,>mʽrn̙)pDw{TH%7{iD^\P} mb*7p>~R~{ۖ}Gg5]0szSbޓ_zf`sH8(i蝂 зRJk2Ee,b` 8ݟfgzxKw 0Y0OvicB01r*]\ԨpwQWu%uoTRԉ b.HyDU!kwo3QM)ep#*ԭ% !CDFAgRL$ Q0 ]M(ὗx4sj60ܣʏ;0ZXV5׮>س~uih F,5D>>v{a5KBð;ˆ4Ԃ8H)ץ/j ּ`6&gljfff:?>9\nnoj?l26sP$PJgIlsk`o=܇hKewpxzÐ"j)L""&,Γ0c.w/o$lDzY*mX0DW:^aTk[[_"fN|p.rO?BqW|w:Xg>1>,106)oМ})0P~d6kIpwٳ­ i$~ti8yy03)"Gt^yS"jgVTuG ybYInIC9=/qq!kYF&LWW7vk2pks],CSr_p ΋33BJwnm^|(s&aff!a⾋|I -!{&NI6喥HBId!'0,Dsb}Gl u.Mښ,*Cxה dXw p<>TfN޺[smT?MK r\f5t.bf6Mgo6wgkNOUY}Tqssssa,|Ë4XMZcmv}<"eiW 62˻v@F9Ex4y&3yv;~؜NiZr.ww"yۥږexRlSG)`JLe ݁JrZal6QWߊmŧD>^-8b3/*%#W )Rص/#;ƺ] j ĴVQ܉(KPHc'RFD 0s_U端H`g@%27sNE^lEt.W TLNr_#Dg.ж2%(zK7yJ={! )EЁ7!̝ZKoeRézyr9Z昦i*9]5,DBLq?_,ٹǶbo:LwPo%lc P5(r(lGts 0ϕg5d vy4MG?;J>U$`j*'HK^jbK GR8 7w ,aaMDZgvgY0gs5mr.Ʌ|Qݮ<JG]Ӑv7C E6e8I58 yYv+iڔG%ߺd3[6=Nq}xл~,{8opuu 7L0))GJ̜h{WD&62n1v'"^TpVmyW^} YpɏGq$k8DX+>q_D>_w>νh?x}o'0 DʜJk}"D[Osl`cṊ-G;*[v^Z)pjyK5HUuƜfzXՒUa:MnXBDK[͵Ϋ u4ZuwDfBDf 4U5SJĔ%9bݺСڼR'"E]vbgwsgWk|r ҷϾRJZK[#3 6F=xIdHC;۾w!wZJ9Zȉ5ukdu٤z:5!\" v BHm׈vfhv&Eq!^=gOj}y)ϭ=˙s&Փ,Gs"xC wcjԴ0jS U[n: &`B o N FLfț9z8e:A@\5^nR)u\$yvX> YD*xIBOK7C/rNkSu2"$j!u<E˘cBBTl0ϭE0ڬMOW2gA<*<k#٤p&'a [H?_/OWA? ON$.]=XDH+ؤtYmu,>sպ{ĺ22iQMDNDgPW_n菬ǚ5*iYJ_. -;:뛓u5qG"ue7! k&j9gPwz(>k9g3_A/q'ck|n|=4=\v\a!` pD.2JiVy%~wR81Nf m odPqCXS # ҽHw6yvDW ^aؓ _Eֹ]h ~ .E.yz*RR9kד(a}_# wG?z,U42PJrO'sVGu?) g.~r̠㱱$Uhmq҈ F8&.A>֚8~T?Iplpynڄ8~{N6Z<;45rZ5L׍ŁWNJGmcf1OHbIROWWIع/2#"„2ePRxZqERD]Ð7ͫݘ)%`<.7*j!zwZ̸~rٖi8I"!#0+MwЈOS)Ip8h) SX?ΉY‰ҐV^{.9wS@DZlڌHvP.Dԥ9I2ef~NzLի#bf w[ Ǒ"Z bM<hJmIz39Ԏ Ͷ[Pmjشx0Nc'WfD6/=]袶榛09g O޺Y8{}m.U{>=NCC9p͐NH^5HTYڶIQI&Hdi(W|8[ ]yFJ8D U[8GX7F. dď#o/#H I9gm6\YJ!e>ZW^VxR KvWE*#'85NO:Rr),ͮtak0Mw~G`u^WmKkV="8N'Wk͂0 C'^x#Pf)1E))W/^پqgIyh $w)-:"tkKʹ[s.t63W`ۖR3ǹN-. 6%%HD$v΂+Ms@#1 ȉs& <|^NXi8±G=LN]Wu}' UNu_Y;iY:8Ɲ#']jZmnHBA.?<{Ym8O 3J 9iZ0\?{5HəNKʇ<^a =9!iRfXJq2Hq {ݾtzV'{$5eH@ 9<+ k~Z ]wq(抈m]5p:]~nBҦEUd63m`Df:FYҹatYaWM^KM!b1N$3$$s5COg6G8ǭrKDW~eIVN65AD5 2HkKtSn)emPٺYf1vg FPb~&Y57Cm5 |2w>WG.]6tF$&oe{ ">>X~p0ǙGDtg燁a ןsX TIV+s הR8EUe\#"QVrbkjּ4 3'| QU),4'r^  XK&ud$ [(Í֝e} Q,3 ܝ$$EFY=n|Af?vgWgߗgn̒/LSJ6n2a$A-r _1@yyYZHp"1[mX!t:]P}_NSGI:b 5mh$VF9c(M2#bn)`貵awi6-!JDJ"c)TݫM6ȓ$oS͈h:S^/6SvKrSD"ĚvPE71Ҵ{K)[ɇ8'wD3," sK="lc7kDsvvs0r.1(e}a֨($Y9޽qSpn _ļ8W_~/~omzVi:<}/|t\Ns<=8?DNC3#6/¼o)ށOY-LTJ1[#,ɬ=<<㨫&h@| oqbDآ$)yJ:[n٘Y2/t%tBMAz;}AVn/_|R=.YBsJ _ʖA`_s+1«йRy qʉ{pD$lJJK 0 \j`LC (ylڼ G 7r侺=4­u<}9N0QXN8&`ᎰXCԗK"ʜw\_9bPYVS"uXΥ"i9Q0Eh￶^Y;*5;_%*K/i&i~8b:`]Lw3 %%BQr㐇7ہQJR]b35~ݡ>n yZ~Xe'ARëLԮKzW`pjnȝP~Djz?oy}sw6w&|P^ֶv}}{_8xT`՚G>$#E򬻧T1b֑0 IT,GO<9_G{n~m,t{/wn|@D)gwqy{ŸiQwIgitC%͔Cֺ0C$SHDʹm9FMǃ8RaGP4hm=mLGw7D y 9963,n> sZ ҈((RJu{p]a(#,9 bt0-Ֆ0 U/3^ 3(eL֔RaRDDZTܛDcY O^W)sE&?TkϞik4GDpp*j:\*}l !9M9 Q$$n[+blf"iw8PcܝF[ViBu)DϞqTa~OQ X!\ӫW4%vzZ6)zrގylh~ IQ-"HrXpx8rIcɛ!4Ms1i\FA}\ƹE )R՛FivgOxmvp/n "׋p(Aryx_Syj-]Es9(H%$VB%"4< 8"rwPf`AxrU᣼0s%!9T$9܃8u PR!yF$BЎwLݪE,IRz\mYH:[{=pʪg0s!x@} )*3m=D 7oLZpZ"B^}\/箆]Pva0Sj d @tf:-IJ1ZrN,y6YW{nRH"yp,mdoR|< !zS0 ЕV YwyfNc}rޒ{PP'{p^k4 Y(Ꭶ( Ar$hsjԯE Xj0jc_s״Gݯ#Gv_{n/?od{Nwvŷݫc7 -cǪva:9rY,xD$q:>:O*ٯ}=M/J ܯuwH\[Gijd={?zgwWJ~7zoJ1m>RJg gy&ZW1L`LA")GYixZ׆sS;޷Ɯt<CnZ͔6%h=lc9MnnnZnf%$s<s0 uļLl#0lYA:1p>-G"Rmvʌ]AyZ]kGP̬NVD|wHB!IXBTX7PBD]Bf7OuUiN;4_X(#H?nSWE=SJ)q,gH\} J)fShnKk-HRH&3"$IzM?e;ηkSx"̼R2.z8MzA:5vx$Nfʗk29=W&Oo5U3CAA rfZMC&fvP2,fB1s"úx@ߜ_{ 짟oNǻx6e9M^@b<ޟnv ;F-b,ˢ3`)gnEuH"ɅTyyv憇SU' 9jj` q8fw7M>?~ՑOK.Ϳg~ZWQy$ Yy#9!SnLE.uf#LV~G7WZgwfV dI}[aͭWL5#DX_O|5%qf5 fK#Apmji<圗: t3yD t`3s2]]"ֶ͒Y΃Y?R+ɛk~DC7! f戞YH;)|a8<8nc]Δ Љgg6Dt}U:2d*kL׸&Xvp*˼O C$"Ik[<٤nԐ wM DaAF~W]{RDtY^$C 7C"`Mֺ E]z@eDeU#o.ӟ=^?'_ ^捥cOZ<ր|'??'v1JTyl_{qjF|n"1[s[cʹHUuD)O\͘s2ayV/Q8Wcôhd$r{[RzۼKݝ޻|/c!uߕO[Y /?K{*$sJvIUả! ̜)0oHlYzI%! `%Ӊ(R(`^e;p*9fcRXAU0S)c.ݖa̪nfLr\Y=-(gZZRMNӲ,%D, ˲VWO.n-& ͌B<"Ggf9ZZZZ." 9@R0OQ*Q3eP=<[ikEXD%Ƚg,4:-eM K){{AJJ8gڸ;uAY0z$y t"dIܳnZ6HLF,(dEq""j kg CEGYi)]xPbHa atγVI4˃I &#?f0_o}㻇5}H,]"x a~S;gzpv`:]Tvyi+աݻ@G@,E"ysέ-t $%rXI.\Ȏ._',D"[Qkf5C"BC9wCn@JW]Ņ\ GP 9|'xK: oaL4͜7xt ZeXrJ,j)Ey^`ln،ehL--@,9$1^AL˲ֈ""M 3HBɋ2UKf}uw(ƒ[IKF).4[/NSVvJ]+~'3ʵ.2xSs_n3^|5WKo_goۻٲl?qkw9!d@jG$٬]]pwjipiZr.fn88y>^m<Ԁo?÷_޿O q^?v7ԋO8 K.7Q]>=_(1z;%R^o1+.gAWU $<L$/~&B$#{W(D,R$m ;G80EZ[r~<>ЉyɻZ됄9 y[S5wW]*Q)%8ZkS?M$"9M'H[֖Jר|,ZbwK)$đJne"2k;7OSl<" i$@)% ;RJl\HrbD-2QRY2n$IB9u ff&1"KW[PY"% FUωJIa̜V\(Pw!RZ8T!@e8q2fphf]5[]5+fb)c(]:䠰@ 8pCz%P5$^w*͠f0dֹ=`BY_+FO}i7괜S?a`e5gaf)鴸ҠaV=6K Ge{b p9?'_{[/V8G:1o $ҟ byz/֪%aq҈P#:d-x8Ĕdw=!3&? }D'Cb<@p]IR[G[ |SzYSs NL+toNhpav g;Vs&A]]VKCZOڶ \ܵ?!LD][WudiYLB=O̫Σ~k_|&cAls֖aLU[?$ܛ 0@A$%8zK$1VK[p07M2[M,mmgNsJTrn-Sd(jAhn}s^Z6c2W1t/yt]!8ffkbFS*T(ayP`07Syws[g;?{'?g_| ~0z960%Ӑfoua3֓FybiOx,~a{ǠGşܯ_HZ(5wGk}-5'5RnT8[{wp&z;Ɉ 9Gu^YpFzԿv"֌HR2kD+铱95ZSu6]<.L"Ҽ%!s 4L|wb3+|g3bk2_=଑u.gmGLx-ܩZ'Z LNqH AV\4GM`EYU9ݺ;en<،\J0%BZ΀\Ի5Xͻ.&CMj4s' ̪1iَAReif933B3#朓0-Q]sBerREɎ'Mf@ $^|:)0uN tDfxK|^c NOj~Qb9MeܕDnݿ朧J&Xu ;Ͳ2]]a[ȶ>֋o??ԋkWH^I/ٿa|/ 9vL+ݱgng Sw !&! 1Q wx!z[,((BYT'D@D 1lF3kaܽ2fP̛ƱoooR%"ͦT-2;1qL‰i6BI2l.v{v0@Nv`9 F8nIS/G&I ɷ+UZ̉Pu1c<0 "٤Bf8<5Mi7CEl˂qĐxjq)Q9`КgR RyvjBsm7R""YFS0Ƒ[UzgEp[]k*9~}O؏??.N5 #C9}k_ ʐTuZj.7i,U{CﮞWzf{W/Μ^;w*KgW¨ 궯;av;Dzi!]S"="v sy/Y/]vd?Qwc~RH"Gz a^RYcDg- wXݲ0Xkk!VyyUCeϗE/VP? ]n%NgRo|m-'Y,D_T5ψ/^_녻3(?_,͌sG ~y7=}kBD:81`fF. 76w,@a,ͬlgoa9v{}}}ue$[kܦEAGw'YT 3zZ3/'XT# Wh430MLԺ|[[ێ]؉93z?Xm#KIj1/3,̃$ :X溨Aa8#]F &}]_/?^xW8|8f87nz}o5?ʷ_|p_*{/7Y`lSIePV$|YR2QbotǧB[?0 nPO/&Us4n`_x,a߿WlAWOֵO6!#AݶœhAb{bqJ51u:9arX ,e, yЩ0 Z."ֱp4s>YXk J,  p۶~ acf/kiynYKr ;h>=+`st""{ef<~i.+kӥjh: DlD: ե)y av$:(DT݁( %"$q8|in6+N5x}$R }ѯ{s4%k=! %Lq:hùXSu q|?Lfq̜4.], 0vqH8u)<:: s3hnv0S %8As_h1'#Ň?~w&4~ Ⱦ~㽗whmWhfq<Ӣ1lQUBh-K:9 2N@Zdf6c~W\H`B2Q6ӾYGPWr~o/ ex :JGS]Hf7xY! ZkJGD0Yxh yuMtU+VAlW\r5ڵ9,K]aGU&|fAf^j[U" &>ŬAz]Dr.,NJBN"qqW{m<DXԕ%{okHgO`2$WX綝[++ % )728M)aU%)c/59CD߀t)@׻M.hfvPbaa`X-%PZJ"WՎSɷwl9^f]5Ϣs]5?W(b}zg?/g?{gԿԏtEDkj`C^b!)ADي%:[Ĥ I\":e KkH,ERZ]Vkb%Liz.Z/h"n3YkU{d)fFB967"f*Ddu V*~?͆a4sڨn@p{O+ ]E'HԔu9tSW)qߨ:6lRRJinyxd,y}8h[s86<@ $q;jX|s.D!"zyz-^^wA^<{1^tl^Z`@fqtaXw#6>CU-ei"%H! {1:Nsgʄ-hO\0LXxF'Yb ??Ή`waڭ$ج*(-̉H +hԫ%Q$eNc%wM]3L`}rSZ-}WtS3U7"iEt/v8"ܙġS8]{xYKY]s:^! OC>#"V#fڡh6й Ugؕp&uqi"CE "S$RU3cڰYr63wsLjFЀ^n?>I JG3yI0_jPEk7X)J7w Fy^9udQX4Ӂ݋Na$ %okCZpnj/O$K$GJP $v5R^Mޥ`$z:JHa#('qeb!g=͙_zKP 蒟9lG@Nۥ^Y9i/~fֈ]]ά7-N̉\7W ']Q%⠦qϫ޲)޵ s`*skK[*3w_aؚqp{{KD P՗^Q [.ὲs@ Ue5l1s[Z @sXpVtyw@<3bS4s}8B4'c79NB HCsԷ=[sܒd99S8ZwrGwiͼ{#{5 5tbVkWeUuafnպ].@%qz>Ì0Q/N+.\ʹ-#1QSER拜"XvsWqV_-ByxU_NM~v͙Xr]?mb+1 @pqnovӲ,/|:\賝~ d&Pai*◈(HdzZ;t`G BI83G׀43 g~,3Ҋe>:p6Q\zfsEZpjg p꺖]3SXDتr XB"BE2Eta|U%_^U%d Fν:5l83?bfs+9x7uUZe_Be`f&Uk { Eـ|T2n;wMC:JUM]Tyx0*-cVU!a溨:ww8:]>m_,RW>07W~OO!x<\__uNCƖ0n2$ W&/9`:YE_ݾj`q3cif n aNx*{|\__?JCl6Ϟ=K)MӤ-p8|D""owI}lrp<Km `P 4tj~-$)#%`"I," wxDHYnnv\___ppAޝ{o럺^w#(O6=%5 g0JpB2` 3g)%aoH١B"'9>s>O>c7qnfqTkuij̦DpR^Z! c0q@D 2Xx9["( p_߯MTqlf"iݹn[3+Hj43!V~{RmpU w; _1uWEֿG}H5"ut#CY#  MAp C$4̍Uppo 8<`j2$I9uOU)ޖx ^aRRJ]6Idk8n_N<M,Ce"V@מXVRkZkNk6FDDZjFz[۝wޚW/D}N~yN"!Q1(ϸ<7-fomrwM)a;&qj޿;(^cȃ`?{>& JaQEJ8݆ ڬi!7Hjbi&LaJR 7 | nZæ\__R2MuF/t\^ BIRJf1ef:\D.uwwj,DJ)D4|}'"3ݽ#L"}k툹D!\2vF*)ޝw0C滻y^S">GVO/EeY#H:#"e! I`;KGF 0HD:%Bg"\#/Fm#`Y7b GԪᤪP3wW U/״V^O#Yx))7N"!f) 8$E݈""ITeji2eYOo]݌IKm)ƿ Yr}8r}+jg &1tj7@;œNsU&跾_j{/->.L%яW; Vgb.sJ)Iaxk)'PU ?=SS.$ig=ڳ`DZZC]D3jDl (R {CZ3< CB)Y̪wtĿq )%l,BѴ:=ٍj7>HDUulw"R{NIUPZJ $쯟\-LDfADʹWl|x<ZkE373 l6ιx̒Rz8O,޼gUSeWOF ϙ ~ۿ`"V" ̴E"z #Vwn BԌsȽhnR247gNBL,8N? ({iP;0׾Y#>V1 cD]ӁLm8xE)S~o77Zk2)p0S*C뒄aeY,,_yʗxu5nKՐ\Iݍ}Y]]]wPisө~%Z2_Ů50nЛ %-lT| m q@#{0 룖dbٿg3P<(Iʉ{QD_6L/k}]}&3)cU#xBJ$\U@f FI)N:·8ֺRxaHd@ktc<> qlnW6c.2J)DU纬5Mm9<z5W7-^Bmr!]@t}rs5lƔ8"&"|nƱN9Rr|/M f%f߽0jiO,!4Mt=^W$,9ru^Zk0 f>m"z8t:\kD gSTջbIK?Fz]8!=g'"!pp&W8PQҰVkDU 3:SXSf4D$o~?8I/*s2uqH"wuOĉu2sv$q3MƲ,yܚ/4MSU_̰ۛws⃗ˋ 􅷮WMe4"IDjQsב9d+[0s KekfoEtv9afRAj3ŽxM:y;Ό$8$g ?ƇN9ںڭ+?GWߓ;ۏ)P&"nb)akssSSD40lZ;<4]7(E̺u%ksUp"ХBRJaٜ{<7 ryۍMwS)84O8pbTS)tygUUqe1H0#(K) Cv,lvhC˲(W޳W$`xdq;lۈ`HY6aY`T zk-e߾Im&/fs}eD3,Y`t,d,ܷ%hX %,KE#@4踁@c$$LZ@(J$̈̌nqך_1h=z}ϳk_ >(~TJڜ*RbyVu]C`HLBIͬZ]Zjo5& <>,jgm*FS̬8330P!bf$Cdl]?='9826'! PkR&bJg8̓ ㌌QTk) l _jR;wW~iip/gG#|pN?u t RU'~#*x^ %Fn㨇M;90WU_ϞLљf_z\p4Lˌ|~W@?~_V*ʈW5y0L(u'9%l*Z"Sv5xt䀄U4rfh=Df$Kyx>S<[})eD]|[* >}_/À.9!o{ö*Mj d-֐A*vx~$&pPW`?{z_|̯DĐ8yyk{ٛB9$N[kz BH)QY8D[` @K)iL.?3UjGpSFi XSZzֻSj1Ɯޏp3]٥*)¾)0eYDBk1\Nm) SZr7xn 4Hn{O) @XbngD-p2S0ryϳ{*kIJ_OZoýQ !RmuLP[ ^^p&(GuSmMn-\bC LjW@&̌JC*?CyZ 88iF&O/JÀ^8baK)I)yL;K)߿UC#"L=i9ƨ`0 a6}SDsnA0 t:10M!߿{H9k{⅙R^xq:M8m"Nu]4M̸,K+u}] z>Z4ah~z8fPlֆa3x;ڲoڶ.:!Xn@4=ќ^:1yO z-u`N|F&_Ea@E.Oht]͋Luu&Go@YM](TVJYF"vpD A KKx:a f"vR۲+k ̾:k}k{w3|˗/M.@JiH[+Ľt3o98evmG")k=<,M8s*os2;wp'_ tފm])ƀ!qٌ*b)BDMyj`Xok S`SضV m?mm5ksΊ0]|+*bf"MDL`wn Ԧa8#B8O!<90adn[Ɣ*8a]Mc"6MB 9%^Z!0/rZ!2jk}9 y'mz=8{!EׂH:o4o~F1L{OC~ZHʌD{"1 ᭴a49Hf6 R$W6ޑ(G@(> w=c5iPSLCp "C֟uO׎B*_Z]-l`MY>}@@wwwiv#su۶H|:<2BVSs.2ރ 1rLe\y3Mx,a65#q9j4B0  xVT{MUR(q8*\:gk,}rC'BL!9 }J*Q82]QHR DW '񚙇?_#׎_zfoSr!S<^:QZDZ,뾬FDҺ! CܝVc9lRJ9?fM7o>y.KvHe>ʗeǷo~L˺(hߪ?{gKkA!{x~g%~q._ g ?mZ {3mlS>zwџtw?$lm8䔢MU:xu V)  TATTö3p}uw`p{x{\^ȡ]BtWK4 C)0j#^ܽM12QmGQ@"z!PZCmӼ,i<uW 1s.Ntww,4M֐?N4M]_WVjQA?S>L^N^η-W[22y`g~W{"DdDe!@!nig=纀h@ +YW{ tyֶRܥƈ O&E^AV!\.'|{yw#񢵦&5',}_eY`yAUlf̸{)evߝM|P{}K ߿_ ]֙G7ٗ:"7'o^޿i?sdJyUuwU@&# <$Dt4LQ/d;gB! _ܶ+Ť!o 43h `uQ45K#J^mӰ.{7E01wikCo>}?/rGDñiR9y/&D<Ͼ=N "l>) ~s 8z8 "ZZ1u9+oɵfֱyW04v[v(q DRJ˶ ⽦l/殹K#k5HAUWdmY}-aCew  Uj8Oӄֺ.d! *fFOT ఁgfF#8m,`qjD($"E@FSU?Ҥit@aj@n_q?o◿FaO1494i iW[ܖ+"RnREGNѫ4 Ðz#\tUWO2cC@3iH^裏梁z{||,m 0=_/>A=^[k;N<$CD  [ٯWuu_i2,L=@8^Ő6`QPTNLUܵ!"(sΞMR$63m4hSwsL)DTD!^e7aLTs˲uCFjgU%|j_TDk)eګ b$cE9i}){ @^2ӾH\ TLЀS Pp0q叇Ӎ|pPN)XDig#[Ľ~b sy132Q_D};l"9[)0/ic9<ŋhL^> ҆!$'5.`YnRR.Gˬ&_h.Vkm_V 8>~-×s# _?\NËs (h`bLp;u5ޏd;Y2GN#sߺ! J Nᄎ"ZWij9JmֵOi)^vji H"B`hM}5ܥcx~F?_1 m-8L1|m۶xA0yrf&۶ ڐ)D4_"nۖs^/4 h]44ϳF[3 1`JcqdU1!*]C)cҦxxJ޻DgCrh̑ND{B1!Bf6uZ[S@nݻc/^d҇=C'MԄv j J"#EED23QS3"R MULEȍM#{~8wQs,xDHsbacS1t:]Be]Ӑ^Dݓbz}쥂Ȍ))ΧVZι2MHiCk58vY%8 k\JowwwDa۶Zu% feumc:cS@͗՚~~+_W##8Ԁݯtj ?upHv,d !zӸRJ yZ>81u^0 Ik-Pk 9Kt:@w߼#^sTէ~mmW4,DJ^[k NshݮuI$pg&DMz||CE&TJvRJxwVE:έ|ηۭ>xc10=2߿\N1",/2QtUU2 h5 gDlf"u' "~j)!21' \+bV_V?xp*|i0 8MS2Ő9<fjF4Y732]@;<,qOS9&N)sZJy1oM1Zk0 C44FDӔyJq} 1R*'O,/RQDTf{k{_Uj3|qtl [+v{xW9>aeÏ_&HVP0f㿚RT"һ:M(OT1m(Z?'{&Ȇ" `)`66nl8LYԖ0|J۱rW6230yg{/_)YZkaOi޴wފnpR1}G{h;X5se5ФSiKɘj91wC\e L-B@pow"ɑǜs@J9(PkmUMcD4h20 CJQ !Rjn9R@RD}#hpݤ',C&} RKn.Tm_qݢ K;k7 3s r8"b|)eTH"PJ圪DJV={&~CKE8R1rCgbԺm0"ֲ"FK%mۼD@."2m]7Zk+]LxE7olDF3eVDWRUDu3}ǐ5OVd tF`F@#D18c DL8RP|e'b\1a8`]HkMQE]s^۶_kJ2My{0LC"x^{^}4Iry]H10lY"ڶ9ҘK[ִU8_iB}N{UH~JޤY GAѺlδVD:_.]Z)K ^݁Zgu]eߡ۵\D!\7Y't )ֺE;JRڟ ;NUw] Zz ˯ ~ߗ@(@jOQC.jbc&07(&ajL1Aۀ%u{ɧq =W}nE׷iJlz9M^'g-8ُ8me]WCn[e9"SToK9Ɯz^Cʞgf!P@gB" PzH"8"-pykMTjnYK"0=UQ]SZO4 CJv]5CorH)%d{9X=i}{-hM{\TQ4K9ǜD!FD-hPP18$3=4/dR䜃"UPTB1<ZLj_ƈ/`qR cMUl3 +-"kUt߶:)482/ŋ+3W!,kvOcf"4f0mt[kΠ)-B4$_oK)şC82f}Q<}WZDZHoۏSH!Ydfs݈B/uVjk ~6t @[=[ϯu/?[:mYj"3zBA!~e:x "6h 4*Ԫr!e-G^׊1᥽ůWd LO~Zߺ!ِ'Xe>bJ_?Ld nZ7E^why,yΗ9tE{ S0涷Z~}f"yL)G D)8OD$rJoy"vf>'l iQ޻tMq0 <9vt}-www/^p@P󶄪)`] s䘈k H=;7bf6@8M;(v}azu5VDPg"(y$:x֠ "T Ԁ?@_%=u 0p"R`QU9Eh%ƜzA,f04GRt瀊 DRs8ϧt>1އidlÑ RJi]y8v1)^{69 a6-e1 ƺ{!4sq)_RJiӻeʾD4M"14#\PzJ)lVaT| j/+7~}ߍ z3=a ^@]$uh͛j [1""jzfab癜w1 Q[5 LDbuK_D&C@yEdB(Rk{uv1XJQUf SMG-$"E{8'4iDd@ЅDM*eСj1{"1PNWU"RPUwq<:稜Rmla h"DOt޺vA&bBӔ#2;C HBm)o|ud ޮ@SBBSB0#F d\330mLffx:[B0U23&ijVQ {h\G(4<0$f nÐru˗/Ej΁s˖LYCLWe/%}9Vw{kJYX9wc.f}CN)lM!C{b-JofPⰧnha40N}!Rkmk^v9+ cZ׶ '~c 3EDn@K)0ooy<7 ~~4CmL; I-\@6MiߚP0>L]ēp8j!O3v=~x@M t,RBk-R*g~օN{ !@xn4$f1TLqr9MhP[Ef̀C'5-hSmF9X*vwRNTw5F꽷‘[[/y^,7ϛ]B8(K}* AZSf`dTځW[!Y/^|9S7f2W;T˗;Kwޥrη-S1~]s!ԭ}sZ۷o/8:Ub.}#"in4b|q1n ɼuu;NS14Ͻkiu+!c183Ɣsaizлc1[OS녑8qIC.y *0}[SsY!BB.UP0Ȫj]chR)6t: Dt!\qD 0 7vRJ1e_8p3O|*:ZEOM)G("uw@P6gc`!Cf h;#81$߅#Hh&F x`GJ2E`F@D25_=iGw5l.=޻^RDq7%"8!nzىizaãy~|9x:2MSΙÑ^ D4yߵ}rN'H j[-Olijk)Ckl!JD-H1$"RJ2v#ĉ#)ce齛j{-~b i8KXQEҐVa1Z-x܄cO\MsV.7-8t#(Sܘ =@:@P .f`ǿC6}y5'F%|q pӈ |~Cx5R61i|xxX5}{m0OygD )na&#!ӟJ@ӟT{k ![v}6s."1E9n@XJD]|z1HRRvP|,kuK-cNIѯ/^[-ĔQTC87pwwm*AzGWنq0>x,E1.63&|zPUݫW~ɛ_D'׏_𐃴N aR#u*B`Қ)Ƹ@e9.8PJP0(d1-.?R+Cu_|ɧaanۣ,v[ovpZޚ b{#zգq4C~7ao"񱯄c G9ZCқֽ8{1`Yr:V Z^ǁZT/݃ƴuضN&wVCIUK)rVzBjUZ7TugZJ@O?!21Ug{D AD@C $#3Ӫ}`t!!bW!0;;ƨvh&ٶ3qBZ!].w!bqr ̖e]-u_VHR^za)pbmݵ{ k>\o6>]Eru+W! ͛7޿{¾1Uٶ۶q4MӼ,@xf̥|yfo$4uaö-4GD~=§Dc]}c϶,N`9lND B`f0Q3$/j3&$Rx*  g7~k@4a@r﫻xN3#<:`wF覭"awǤjZGib;W}Rʋ `ykkqd]b)9yޥZ)ajm0MSd"$S4L>pR)|\.#" x-սUQizބ*("C7} 0lLDp[ NOy}':Faj?,'rcU,< g]|Lo;οxb0U(D$Q=p|"$* к ) պ}ι{Ɓ0mM)/t9Q{L?/KJ'?8~駟1ݝM1a]4MLw[sDj^q}q6#qD9|bRyVk")%f~勻{Ua {-3Yޙi\NU:|O>7繵b@ kR)|>{̀60LGiBN8}./ht7TEbf"D>m|ywGT?Ww+b֚2I5f&#T+>2:uFBEC$`SeI"Z}!)(3 "K_? M㋏?HsUݤsAJtvs$ 3mND_rr}1eg"/Kk-ٺ8v8M|^e]~]{5'h b(e+4bÐy]y/뾧4 iHىj;"c]9].0L$9Z?.iv*|eYDCu{!)2 n)n^t2r{^Υ5&}>T5 G˧~ j8D懇wl޽6|'|>O!RnχT+?-g0s߮ PJƼ^0#ڷ L`݀)s6U-#\#_AC#M O?#F͛74ݻ. LVь=qoc!T~պHӐu"jkslk9ED4}-xZJۭ{_#\}3lpȦC)bxxnho S03gzKHD_{t:Y.m]^(@)%xWQyYQzv5) H[C"c(F+y~_(a݄Dps%33Q &b D%)ZS7BxgC1zBC0?;K<ͥ#DaapKrmT}齋麮4mj!>;u\1߿ OIJo޼gUap1ĈSMSJu/u/NJ~|>kT p]W7i~{F;a۶w)XrTc-m6CeYulf"ֻ`` S vZ|Rz:)pjrubGcym7LJþeLG߼{}3Hca[?\Pzt4.8xti̕q`Ӷhׂfl5fL1)A>wLx+1BYqү3LtOTuY4<]?v}t>_ƘR x__h ( 1' LGc5'אc+E|}/i&պUtBO?4Anjsk 8妔BDt\޿y @bfdD[+l& h1237"hD>M|̀bLޟ! @]"; bC { 2IDIT|ik[s>\ir!V8O)$nhcaw4^O0 p>8Wrw$k0U%O<5n=pPTyb@<K)ׅC oyRfvmØRmqk-2s̋WxmBTEm^1rxSqw )VvV#*9/n4Mի׷[cӂiun9^Z1Lpy[W__?\OVS)}hH nkw_. b "x*@α^v5Dmo}3db@kG+1 r:WyN<ݙa^m4aqJiƺ\m|\ !t1j;&h1pUbp'bly2j-D4DQbDAHq۶^;8}/Mϻja\;:|X Cv׀m_=-@![6E=N{m/^ȈzP!xbz7ocG@ b9t|Q֛1435# FH~3Q2C2Zk!#4T04E0LUrvr}ɥsiTMtwږe0q8|{۲8A=8}ZCysOӴokr'"ں]BNLq+d`+d6јdy߷Rjpd!b-ĜGL9,b zCĔNð,!s2aTu4?}-.L|www)aO7 B `7@L)g>ݛA۾o9@8O=^װ.{MPh~ ?E8d?3.Ə~֧lEn?z5hSKNL&*!ĭ4"̌E,90tiʷX8"!ZR1B)tR??53d7]xwixqEU UnUUqwZJj;8CTn^NSWQ%`}Q<<~ww!sDƔ7[<׽ 0Σj))V+HÐi>\RxAۿ/}7*^J̇nĤ M(b z?4D 9 ;+bνwB63+e۶s$>ܙ8qLh8Om8ڲ,!#bݷm+BhT"ԛƽw n}{ӭ슠}R*@^ #aphj଒!%_w[khE_>>I&O4ܲiH[)\oڤAfR&n#1s61H )EMR}wֺmqoSoj4* 0u5Bh`*2Msjnlf#)3NT1ś֥i?82S@DHfhP*2%??? LIjz)% L!"B7*"d@|O<ֵ7Ԣ҈\fe73u0 CR6y8ƘPJiu۵msDhCwٲx;_U!lV3]c8O OSͶLB ji}mbjd"6 r&{nN`]QnRn,=C58RZʣĔNy !'vڻN׶'NٴRmq"|_/o}ivװ\所Vk%YIb{>UwYzD"0D#@fY^"Of۵! 2"MjRHz>Sm?BpJhx'|ʠ$r "<(YӾ2j"*61!jbԋ*H] AH H0_9$qQfr^K5y*aJydA =4J-"mf<^aHRh޿8f"ΥajW[m<[q0OĜnmBH)IU[7NӾ\w{3}8 Bڥoo^%I1R<=c4Mׇk)ζ qLwn[7}2ƘR !\.B9%n!-kG{(f/D8x]W5\N B?"GUM_[G?hsGZkk]=*U,V~9ݑkO.rK5**j]Dh|aP0 Z* Nh`yPPF! 2z>L Z޾͛yכY4}x4W^圷۶mZOӠ:X3rSL)*-sLٌ;:O337;9S֒8=9Mx0zȆZUst+ɀWJa&D#n׎Mb]ܷYU"2Rcp+gwn>4326S7n^m.P25w)e qL!#7Q3"Vq:}9+כȖx[.ںN@4z^SJ!4۶R"3si54skM˗Q1ƣw!R)u8F?Ƭ"Uc|rÜ/.^uoD|yYDr xSq\uuEBqmn+B8ߝ^~D)9T.MDr"bҚXo]+XR ˗Hmb[ۚx[>8~M 'Gwkf叽?~(:waC7wdTn[2HTAIb`|{Qc5ckD@5~lyLJrX19e+j+ezcj]e{P[uC8ֵ6 487^&J.QUdVj$^)(^#1ckiVD}YD$@D1'$p)j'++>ʺ18K70M!ֆi )VnlѵD)Rh *y{|YҤ֟3+kۭ3-cV`K<5 dbe? q>+ !0 |A D&œЀ!?w >d%4MJǀH!Rs )"rZ$P# սn{YZءҾW0_N-"W)D{Ua]w|% /^ޯC\.&ou-{}q ^Xt2>N9 ԓ0q\u6Mfwy=Hn83mֶͬ<^djmoS̼웏aB^k-)*뺟NӾ"JU "7<@Uʾ,"<ߗ;ǯiS{,O*FRMg-XSDa+ɬ` Ukbo%9H1= )q7hMX%R0D*)R>)'#!軾n^۶ú@{zJaۊ/ 9ޫHEޟ7Zu/" IR0Uaٷ݋K=G4/ ٺ퉴~Q?$Y>ЇO3) o\ό H@u9lN s: ,p?P טv[rRl{bv}Hm(P`3xxxj:^ww yj]_t>@ﵔBB{i?1w(a<^\nujdsP լ i&JSJ0=g*@)>.M1v9z~s[km۲]g@K5e0 VKcM4ٺ\ gP׽ջˋ֮ޣTó;<eҽY[YxL8&0Wxvwift?tۿ.J,{m C&5۫3ܼYW^;ԝo1z%/ag ־J!C`+S!0>/_U|[{{ߋ1e]~->UqG*xmcϓU/^4N,Q$S)81 ,9g]jU5q&b)b Q{#n(e[bj=tɆTk@[W1q>%交m[͔(Z]of1,m]NvٶMB&|wO~j1(b1D i$çU7w?Lngcl@BCDdL@ UT1z:1" g!#02S!"AƺZ! ̌18O]) 1%DBalsJE'@t?"->嘦U>s3 y<1оoa)7oy>yt\B$ư{ C&sv40@@b]ubBӓͻw5P,OAP^!sVun:fR3I/kSmjyNPJ+]X[ZZy3Zu|^eQAwm!ِb uK/3!oGXm#1s |g=\=yi9?RϿl쫢{*jm\{9#^A !PDE ћ+d>.3O&O8bBJDD4HIUCcq!UNr>].۶9B9;dH9܁ݜm)tZ"0=K@K#x3zU@aFR3"ҎTaGsx 6"ckaӐ%]0(yŋfsJ),6o~ZRJn'U1xw7.B$0sFYs^o8~+7_z@G6SD{+7Fбs@ st&xެ; 2Ey..G$BPDT`Qe$HqAk-t\/^*Q3iھW t:ͧ)1eu.ikZki͛yRJiR L]}>(<˕s>6U;N&+pvlXpswPAf3Omo޸sP~"zB!LӔC9!|j[<9gb6kte9TЅ"m_W/VAJ?wu,?яpb<#G<-GDta!3; pRvD'j E <@t-JߖHϧnj;K7M!@knLU\mR 3/UU#|k;M!(mn%"0ڑcjaL` H*tĦhO6AUsc!x+6A-0j <^[:MiZyFSkZK)d ]4mGd  tLP ܖJUW[k0?OS6Mw. #Z$DdSHO<"$#DF@S4X8T8 dȈP5L_Ϳ"4f6LOϻs5C^a83>'" lmdR8);85™3fvDHi:u}VKmۖB IL9DnsY n*l;3ZE[b..]uMD駟~A^#L(l+2G8`{1׶0~nbf R:RlɼCTwu{tї?xۈ)< 3bc 6F44 &$BCO,HUM[Oc !pMp0cz\nyGR`]qGG+ C:kihr]JSI|ġ]~ O^nݣn>cgm]q9<&,u!Bk-S$)lrfjm2Sャ"Fwww{ϪjJiǟe+1_ I~vZ+w28G҆B@4 L9'D`m^eJ#3úR~5үc˿_ #znJ-:mY9C@} ݶƆ!c-Z.D`&A? 9pށ0!Ro > 40{|۶iTe:ͭhpBi͛7yd$t$|^U[k<?9AYOHNlPTOdΚhԥq Ūyy G-Vl- 8T^=pt1P.DBls)p7;m=(gszmsJpSn#<!DZ;bRV^zO~WOC[ɿYMIݑր4~Ժ!)W|N2d !/3Q`HG^" "((ĐUЄbHz70M]Ҵ'&M9F牥LzDE1*df Һ.<"@Ô,R:ϾSYz>={\|o{!y>nl&a`gFjJeW<0s'˶#j9PkBC{[k˲cޠsQkaZk=s)1O#f4y>by$CTZ>nf8۲-0 zays?suoH~۫&/BY3:#O?w>7PbqW&Rbَ&0lTr!apww'G>Uxܒidqio2qyi#3 "N3T)L~z)cG/)/)^}O: Su1үW4M{8{s-0 Y6!ð!`~_`6`2`=l,-Rؤę3;ʛs"bߴV7ʪnFfs΍#ַ߰ W5V bWh/˶Ϋ8t!ED+v<Cp6@Sy[_QDyε֚NXBDi7*!nkn]˘W)\m rBoq7 hg 0Nj0 ]2/-Wnzv(P̀JKr>D{~0=Лf3pfF("ԚU#`QMS@\ѽwkYE1!̓wpr$ ;mڙ?m^1 \E_<{oy$]y~]ZRJ) C]A^xWtm[֛2qJP0qع;Z%`TvAOo#|ȸ0q/RWS r{4 4䢋#"65vضM^z?B"t齏1y]5J1-˶-!b!sԳ}8gϞiE( f[Vkwkk miۍұ"#D&JS$AAKB{#۲D|#x8D$<&6'[N(]eάxš*p{{+{0Ԥ8\Bj^8^c0fQB!h.(Y?#Xa!jIna LAvА""9"435"%.]jZd]TZaFØ8:U!ZιZVwl} q)%P1Z\J q(9gjkm&qm9[p^f՚JlGfTmD|uZ- $Dąl~`F1Ssi|w׻$_RJf<\mږC.mOx< 'nN.m;\"N朵!h 1س8>1hŠv;;c ;鯩`Dɱ3@GB%k\gH`3?(Jo""H J4AdARBwzˤA$$Ca=K)qÐ du]Mxw&qLm[joWWWkvN$+ !wB9!j-50D{/MٹZ4Mma9烧N<@LbaYq7Ld7-Zc`b"20 cI)h&Zxw!fS9Ui˅;M=l!m^j{xݝv1 J5y )%cP:qJ{p8XPb mHevF A׿лcwR`L=ޞ$& `=A@TT޻<|p*tEs^\8C&zhF:P2c1XJ 7VoLwЖ{L^DO Ϟ=TEp=UDzH ƶݻ{[O)M佟aIËu;pM`r!Dp&9^g&FfN!!ZR}{3qD!Fճ32G`\PV;WkRt^<{VtAIsu.s2o!^"\ɔ>.e}d]J[pV03!{rLlE{3 [ sf9Dt`t dVEcU vL#D␬۾RfGl,{!ށ0ԩ:}?k7̞ݺe|0ֳ*%Z&Ć"3*Ztw!Zs3~ b1%;$6˜=t lƑ3"1qFU=s8ōUqu q>.<ڙ1$> EZ1h ~o!dÏ<_N2gap[Qc D Z|a\8D@"ڌZ1u1)@or? H̢Rk'fh~^K=n圙{_K9n[9u-@ˎj%4uY&R777cڿ>xz:ek*)4 @轷v;&Cd[f>-ǯ0 ]!/R[+9ԕUu7MIHG9J;/u<ݏ,0\}T͖-Yϳ˺5뛃ה ycD[=777ɨ5!}k!]f4> yZ뼭뫖mB+Dj*4Uy.^ m&_ -#.M-o]jw 3R?/9˘xYVK =YkhJ-5[Rjhkl%[9kqD\K]o~w^Npɓ'R}d!j>msL&UoR ||y%?~^@x֭׆ "å߶*Rڶm Q" m[)EmnBdP+Uyip[s.PchtwE\X>WKSUQYAZҫE C.--glږ;PaYvv>^+whkot[JfP4XH!^$#AyOyxx:@kŒf#Zs6"3oNFu"RhzR~ ߫RVZQ{oy[*eَ(=qYP%HڻtiAنqRrQ)81umvԵy]b?qsñօkc!; Ov_|+M^pb^fP5J TC~ a&Bv=8gZrbH߶͍jQ*"! swS7/&עaH1G!}wz|ss5^{u {j5HBhV``49f$b"f>\_9Go\]]}_1*ѡ9إbk-窪̵ܲt@qM[ޚ̰0Ĥb;\*jS,u{z\̾;L2 ~_Ћ  Mz؈b&RŎ1#* 94 +aS! E0-EPK&X۶dM;2n@DDR lΥwSBm-Z˚{ZY |۶]`bLC)%Rڶޝ?NzoGwsj!Ǟp[XZ3* PUiyZ50H`-܆`!8fKn_jumM\`ceR|mmۊ" |xXr !{4|f8{CTrDW?j{pH2_n|3< ~T~:d 0GP&BFPRBB!DZ F2|X_^DJo&rR<; 4^]tL"m;N{k_!!)'O Cqη>}=_|W^yjy9___& [UTQK݂OYuۙd1xw\aiƧww}JV H\MB|bk-# ޤgs6œB'aJ)],j)tPIQbrxaj;6όvw!%/ T$@ 圻vdR *YJ{>FM"`4kŖRjMwiռ8(GT =9&>}Xx;picfB=vwu Kn]ZϹ0x*DPAEX#iЏ=*yOꊈZǁXJ).^Eg;vݝ ګMJzѺ 0c^]iՑۿ۹W^a76|:5@Rʼ5Z1DfDTno{qOǣg_@q c)wyOnw;0[o^O|:ikDRw1b9#`v﵊33`=y9 LdUUィ4œO|gҴ_Հs#FS6r ؘ*0`DT҇hfeFe"Be SF+.-ܤ)}ZC{#n7ySe;Fs:[kۼᰫu~4'BxuءtўsCk,i.?H"^PFY@9WkCh8u]Nz>+"Der_׵&"]i̜Rr 6Ӳme%xomc<}Ôؓ9>B#"j;KP o:ReB~]skmu]U8Dъ4M aYzo@gOM?/_@c?ᎻV@z+\{Bkᎈ4feq7Z=4U[w$r\uQ6%DMQDnwpt15fXd] LD4nL4M˲|~C@}ׇl65b^[!@mZcڬQ&.6jt";O~{뫫7xC}gXjKs DI!{/ PDT!|5}ɗκjָ0';@~ɦizq<.isA5ݿ~lR:uB%t33)%@Q t1SZ[lhag)Q ǔle'm@뢘,:jzPxSPf3X9R,܄z+ImpGהY}j4vGhxc߻=R[o*/kkfA-BJɤLk~oJ)|wwg֚usU:f-isvT؄ /^;g(Dd$1xr;R#RJl;6ysn看L1o~Ca}c^n=@m ~F#Oq:RKoԬj>xRx_2k/y*[)7=@JХsZT+Jѿyn;^^{G<ڧ^iMZ]TtwLD}g;o?x:OQ +O\~ -ݰP{Qű1Н!/z| _1HRڲz|p~|"utTu&,m,̊UdgB]E;Hk(hjYs h"ƀHB8 QwmhUMjW3  pOMIUPKi]nUyyP-朋>ic`a= s]mjIuZRJ3,3/|iٶMEyWkC.BAU5dJ k)\Ji*1Z3\t"qܛuR H \LD,QDZhh2Ji˲(LεyK'[9[p=ϭuY0[BqmB<~@:ouzm1 :v[B b7DzZkc@Ґ"_]{dsrۏ=8_kM) Н1:H!=1y$꽟pPw7}+W^_CgS5gJ)KDPJOo_OS|,3\+o?_=ro+jBѣi7;o=tk=ywڜsJܥ * tca{_?KJN:3:ꗆ4֔{!KлX҇6@TG'g6"*{ JuUa"; \S 8DҰm͔ɻKO(ڀ!M %YZ]׍si̮4.ٳQި&*{@\ 9oܛs@T1%(fimm<:ô !V ܬ{t֊!y;KnDRo҄XL gS7J)yY[VN1*a)i.ø׻Ԋ0쬑aC5oUZ{xփyi`P_ W} 2Uy .&Q"ND˲L޹֪\@y58뺾ݷzw͍Q߶Mr !)K0o> ]tW";i"&y֮ޝy5M_ ~I5u/ko>yѓi/x̫eX6&ݶқޛћ (2v{cRĠJ@JCn7%GH|a#\y[)$E8u{׿3!u @].}GDd0SU#!#jz ~)Awd(>}*&.D@82Yk?bݶ XY5HSJZ5K$}bp&؂mZFLYÐ c!eP&!缈0xo f8DtwwWOK)/noE$Pk=-$b1>ۉbCYk}V1hRi1itxFl̑#29GvVFrs>jo+hP=;E8 ceqvKC]>fQhTDINT|֓~wVa|dՌc=l:);!mjo\[{ԆK޴T{*Zu]*Twm*)%djyVC\P1XK't|X|Or!UE벀`x<Ѳ㐜w.u[Dr^`!\՘!m4s"meYQ ,v;7vvq-܏r;8nts-y^Ml ==}n[J8D84y!y6+;셬[k>ߝ^Xa7NvHkMזDK "eC/wo^^{!% t @ZZEW#!d",Y**cv0wVJѣGG~aGK߆aЇ0xkVˀTDtonluiJEĔu?z8gB٪v|O""{gҺzG[rhF3kn`jH ʄi|>{l!jMY dn0П'uS PK5=h$DQ; 9G& uŋ0:BTth#P |ka=l=XիHmJh.i֒.6DΧŘiZ{wh)zM{V˒7Uzq{k!7}u^!1,'8Ga \Z,|aw/EU<~ik2U c>̆9eesq1k9jϭNh #ضm[/G®嚥rMyq|dw6 TkmeR7T"rm<~x^?'n}g>2S2f?L:{p[|m(",gTiSΙb&F1ź9uמz{77^)%.i7(^8ϔ֯n;_}C\9MhC>:NCMaV[s 64YVSJ۶vS;Npi~ݳ;`b^)&R[]rO!2c9_mq@e"Lnnh0="Bh zWR0?#W}y׉*h:Vh.0JnQpHM@Uȴ?!xvH $(Ĥ"j2tYႪx$jQε[k!ƀx1:Ji\b uij RH y]LמR2w ) J!L}@^,ҵ*"^4RaJ777:%@%wT=!x%dE9W2cΕ^j97L.vfN)1s91#2!/b;6~|*zqܕ-=-LDz"6yj^VM\Hf#P2o'?Ǹ Cʭsļ,~J8MCkn8Hm5iz?z~[64 H^ [}7_,PJ2]a4D'z~7l\;o꽊:iXUOR !zvÐ@4 m.]JcRJ!༈0r)o+_b{/Ve9ה_Ԝ^{}k>W^{u^m~Қ #tbu,M`I_enku*oSg V+m_'/M" ΑU 8w}ߖ8f@H(HU4G&-kBQ>7UrT@!Q cB"r;>:̵0`O*tZL~J1<$tNSx|a鴖,{Lp~q7Zkk{<]N11XOcFQPZ^[m)%PDjoqH|{{!f˨r:!bom1ycpϖIw)֗\Lqu8|U=}rDZךRr泅>fZ%Wv!3t]s)sN)_Gg9Dueg(3cFdz~{ ܚ#2YL}] IȌGA"+t/){`ٿϡT@{ EA@ $H93v)#!@@ϽsiEme.¥Ѐ*q?y2[{_Ւ #{'NsND#gm73Zk!עWWWx:ދ0MbPe޹V53Fm7S~{5|肢oekFvwJ9hkb`ܪ,+ޏP=a7HTz3j%CǏS /.r"B9<~!j^P@/Ԅ8U>zN@v:#qHmkt.qL]k-n[w3" O)UDonn?~:|;ڶ __xenWH""wkq79B 84ޙݸ1nV"?'?QF앴^#Z fo)"ѫ?{]l)9йѣǯ>έjG {myGn ,a/ZA]j Ǧ|@d7a۶RjJR^_sʿgHӋM* ua.RR+3wGH $AID|옙qj쉥ZJZ!H\djBGh6):+BÝs]-;Ry9Kk(O쪨 4{ѕV3kRMׇ8ݨj^V1wЪ2&]pΥq=mvssZk*>aJɔy>/4i^}v[9/Bu>Sml,|t)zלa[3͟Ѵ^KiN)ٸ+LzDEI(/̈3 sL ;ih!K Q"Rr3P5Be]a=޸T|b ??3ᠪ1CM˴PRJ:ZA@UkFvC5s[adUDkι{{p}_1~:s] wwwO=z\xD"ӮbQ/ŭs_6ONiDy[Zk۲ўu `s{{^ܫa:M1ocb=`pOK7?z"XuE4J>믿hvM_ ΧŇ/:yë>Ye73nsV-!]H?|"Py̼m9ww>]]]1s9oYJ!r>#<~ӹO=?˕ͧ\+CDѱTB>8&dlE˶z4`NO*sR !u]Uq]g 5uDJ)&ػ<}7ۭiѕ{/"Y?{u]=zɵUefCx](a*/}{?/_Ot׼u"]طe[;$j6(2Si"2bk>yӺ|ϟ?!@"2:Xf\+b\ g%]Jcw9,:ZJ. bfkX*?'_{͑դj)4HӶUSE&@G):au"״1 (E)2>?@Ph_3lYpUlHEmJF[ԗe1]R 2q!Rqw,ӐbuYQr~_nY;,R\`>cÐ0 殾me]u];pX3":tRe[7Ĕ2ǫ90V!\Ս"7%T3_MWZkӀs٧x^{9sY7s%[E* {,{H)aS&Drayc̠.YS Tp۶d=Kga.,5AzMZr?Gm#? &]?3E?m !vf_G̪kjB=BVCֺizχnvss1zN5 mۮUu>V\UkyGι#k):MSXMje~H0)n7NG0@[6*\&`H x5@Q[Qv%"p8L/ܤV9Gl=roFWwS] ̬/Hod̅%~8*jyvPO΅y8BkMJoXDHRmv"T.S%ԄӔp}xwinǺv<+Qm)ko۶2snuGEؖL]z[owOϵVbSlUȥ@k.LnkJĹAaA no~ǭemPk=]]y{կE tG$M56n*^bhAC,G򩮪"mȿ+ɟ\5laf@TZskJf#.c&"jBm13an7uC\D=uWlC] p*sZKx w9{xK<jB&VJ9ֺHս8-ל3Z2kq{~p:]d8m[B)q^;jR*ybNt]`>x{{J]H8CkMړW۹|48*ۛ 0{${[3(;`{@1e?Ҏ>}> JLf J jS{ tHV{2gz鍢[~4HdUvn"w N@S5P&WaW^s6A1K +ɓغ%?6Qw!">|o|Ku^s-"R eo<&y݂D&"DL]ʫ-"@ӆJW^ATkrTj~J8ϳ"E1MEcJk}^O8r/窆Y~JPi_~* vwSP3q!2 iGxyoi?YMbyR Q!QRJ]s[nw_y]TwDeB3ض%J 31ͧxdy10'Dqͣ7a־_}[@n_GTUx&te@Mz/Wv>{RZ9ThR Jl+:v읣jzW\VJ5PsA$@jkɽϞ}Zk8i^WC;gUi9&a79Ԉ>jh7j0\b>FKz\TJu1-?,oGis*b{HػJ deU%% %"E4ZQԾGP8_LK?\Vfu$*Z+T ޛ]Meb!Őv1$ۭRJZ۳gWmYA%8?ۖz[k)%Bh{_q>.lkf+Z")Z݌cjImf 1P~gzv<{*R:3\EuBa[K4k@$1 uca5*s-!agn&*Lݓ'OemHΑ{>n.p؍8y>à(XZjy hg8Da*e{㏡xO P:+~ih6c܌gÃ51n@}pIf>bDD<$cd@!DVC4_z",˺o1_eT;GpƽOc0777/^ؤ1C9\E4WMc)炈y1 9>ϒc\jC scJ)eQ዇5#w0ϵywm`v.HA<Êtbstl['A`4.E6Cwàrzeua~ ү?4vmqG,@P."n?RA;D(?gZ5E"=Wlk8 B7^k./^hexU(9Vu&U$ffi :ߖb10S^Q)*1DPaH_^PIUmlrU{{>4 "Gi?|vduZHl9]b</ZbJ^r777/;av?^__/ۺ^x!yrIuY7ݞCm-FD":"5FJL0թo|W@kCdKLT TPhk)@-io>?x?uLA4*민ע).*Rn4o\U>![kHU Dfm*Qwv.Ƹkk)}~寢?)%lnH!S @#o_s ܢ/'V#vFgܫeYa` l\̭3CDt|\ȩwf$Fu4ZZ-gU]9Xk%e۶9xV[UJ{v0B PJ!$Mߑx۶5kY|J4RJJvDaZGvRJ57QclOy"1o[65qDws=YlKf]D۶vmjo|}8Zm^{ZVnjR "BK.%7.2ִ_=>*+)8c~A ")gVN*ޗ}`v϶ 10)~4<1?z_39:sJ@BHbĘZ3^Iz,]5RBkq8uˌv>/k0D/foqyژ9DyW+?xo|ٻ8EQGb΄er1Q"QgBѾUci o~x]U{罏iG-#朻b޾}?K_蝏[uιL4OuqS==z0 .8mY}tXm]tέ[!nZ;TWU]yS ΂@Uj.պ+/ſ]-(_3Pȁ{\/ʀ )J!g_sW:@@7>]-"{M9Υ!IV :G!Ҳ ɻVw&vgbVUx{IR󹔒| !% ?s C@DB@dծǔB%n7ܪQ@ju)%foTl[ϻݎCLuZ/p|6Bp'v]Dm^b<4M%oT"/^Zw;>)8My޲A||Hy[Lt*PmLd*xBi>ED@sVؔ4?:._^'"|| /5'=hܿݳ RP.3u4ЍkcJ#h]`[jkU[ku]١UhR!ǯ>᭷ !\]]57n93aw|w>`1HuQe˹!pR leB؇{ C^DEz7{K>w"%By^ɹVZpQW~k<R I47x^{kŝfcy!Tk*(zGSTm9!GtlHT I'>t q (KU;Xhn_H?"(W~Rϛ)1w0c;FPU;Kɀ1F&`r.:5okS ۶^e65dD4: ޜs(8gfr19qOgfo\WWW\kUu]wN襷;t񞗼Rt rklIPͨ0fD1 {)}xbC4yq[kuYB iF˲\cwwwөWi֦mgaYxUTLݎlTR>8f.!z3:_ L&w ~zQɃ'} r~;@>E{] @ h/E~Ỳ}}z#" bHSFgU !XVs!:Hs?τ]Z6K붲䜻{wT:`ȊࢵQܾ7Eg}Ҵ⽹6"ri̬ jafDWVVl"r֪E @bu Q=h`[ZkϾ[['Vcz5wjowwSq߿]"6]iZkf3Vղn ѲeĪvLU=rZZk[q}.sckB佔@ /C.Xx`h ԑ_~nL f)KMJ՚Bw*e^b"Zy&rJUЭtfmnnVe];nce,TUh>mv݉85YNDcZMUA+2oϞ-4xKAQމx=`2 ,cVEUok_Qk✫9BB)EJ~A:L*݁P>)%v>aS{fe {z6Q@D57sdpU@pȂE0?|?? ?Y[˹P[k|E S5C`T!"s*sי97j$|)%Pk3)n` ٻx1bkң ]N|1`t:!jJ ՙd&0B E /D䣻GeYzq[x80s)Cf=ϳX))"RG3늈y8\yOdg1)ijGUԦ!8SwPާqm۲!zz~@}gr/(+O uE{W6y؟""/AT(# u+gT_z sރsG9f89&y4iůʿ~@iJ)@\K%\Xc0cX\#"3j1GZn>ݦ1Zw~۶+)ħw" 󎵵mAr}~{CTb,L>1Bkم@ rB Zst櫗<<\nT[kMVqEPZ~־ߺۍӐB›GWt"3Bm K"2wC_|lӴKJwʹp}4 С^s0Q5hk-[8N]p|͠{D:gEkqWKMeFR@Dd7EUՊ89ZkzOOöλq_y㛛i[׹v>XI!"7WBHιe9QҲ,CL"<χ8b~>îbC"9( 4E@aJǏө55K9g*3n#yg6WWW4n%nni2~Ia}Yuu}ΛsnH٪k^nF9kðn3 Gٿ%KkD:t̞@I"x7MƖ~p)\Joq㐐H@{~K)4[vZs3ҋME+ ?~+\*ʜj4E #¶eUuD|J)"NwG4aHbd}4!Vs9&AahUH +B /K݈O _Dz.9`f-:~ѣGWøuDDW$זs.[{V+(6؛Z~2nf&ZآO+5PK"P85ď.6ft !c ~ߧ,^fHdL<4&8˲VJ]cJcRJއZ&tLKHɓ'Vۉ[5;CXG,R۲,[-k> {/u8blqeM)%abu# )Qs8u9i7Gb0868klӬ9"K)wwwJ"{]+Ky^XU[j&jvLiH.#;..!ZB7'!\? bew)~B0M?jVgjس|ї=κ(@Gj)@_᭙3U[^&|a'cl5Rtf]۲zmNZTJ˲|>ΥRZcJmyr=o|{GN|:;&*& 3@]3nR[.ݲqЄq߯#WJ.#BfG{m*J*UJW@i9-Wx!be>:Z<(h[Oøk/k&uOkmcE|֐:Q%b)uCTo]TouEI;Rٻ&Juu]SeGy5|lJ˲LӤ|w{s3cO4M"PJ׭'^< C5Foc*fFǖN7E%#Ð[/lQx Χk|Z,3Wi&Dkc$ CJѣG@AQD򒥋u^ Vk|<; 1lm ɺ\fBDXOtbTQ )/e+>av^b݊^oډjyϗc7]nG"ET%D 8yS{0}cBDQCѨw繵4Ma׺cȽ0\sly] eImSCDG><;{f躝g\m9GD絋"(s7;o[YP?_?_7+wg!J?Ǐ{V/hkRښ1;^[k(k1u^ֆ1v;$5nj'O<}zv1 b {]Z;l׵d1:6xzl9Zk,3$eFW*tKk-ګRea/"An^֎ǭLD?ת.:UE$oss4 ˲ 0F_snv4`bԽ;7Ϳ[CHp <D2dRBEf|_?y:h/NN^^xߩap 拯wM?p*D*Ms6hqaxwnn^yxۚO1aYW_{ɓm[!W7 cIz%B1$GfE%j s\8_MAOhkH+} -BL>̻U`G] yi3=˞RJ)VeQg3ocIpXyRKs&p:&ؕRV`v_]DT}˼Z2`0~&e|M^")(B8\mQx0UdGKyÃ*po`q} @䞛f(27VE " JJ6&қnYݳg><޾9T21 aMD^uc1zw6B+YDZ͌x{G ({/R۶b90Zk97({G&Dc3~޻!K$5io N-79$ nmv|)}yJWnꗾwyG"9.=FuH*fhj?y5;}߯;{=<32JuIJ*&U *30S1`1F 0X U$!ef(Kwimvbs="LI)g~k؆`_mi1=  f@wB_W*bera~߿{{~޾_Z~_N"0e;Tr{ob0Zkh3q&F{+j't T{L DdV7PutYmmqvaY{o`JGDjuJǛ hzɫQio?~7WG?lS&үo7/ 5_˔}tb$-Bf[kJC/ng8sDy s΀bJ[/Fmխ.5O%aHj+]Sk΁4FFi{+ S(=^k6_"s !\챔fr8Z|Z[ fڏzYzhW`**,K5DMt~x||4E?ѷݘ.fV֍tQY>8;vyvem[u|vl[)4u>cYh_ e^uM1PJ(s: ëW//g]WW uȼ-^҄00|y<#m@qu̮9D "-Yl[T A _}|?Gd ϵBk: RBN?lց  BR`:X[h Z?Gpw?1l p-ŗP>>t:qx8/!xssZ3EIK 1Ϳgwwwb( H*j8#1o͇ן??~3oQ2ݼz{y]ztP1VQy $իWw!M>իW^~NeR̽!>"6>!U}h֍{m4\ΜCS!XH! HW1*bgI(֪`&`$ݚhAm38cL8#@ݾzŔR1D!w͛4"!,"Cx<n' 1vqv$1ɿD:(4߿x񂉶m;L;WAnN^ky~?GW3YN~snDv˲2Nx~8+ضBq۹zww\Ml{Sc9ZrJv}@DnB`ܥHs;ݨc?@AYB."Xo.1X0-4bHBH@ڡ Qul]?O^G oCh?o?_4eշy۴ } ~ۿq70€~zO]+7wϚRõA c:繕)&nҘ̐5ԭMy/~''ֆݠR l*R[nWD4PzkW7f{&^ݭ*m5yeq\ \%Ŝ. . { ̀$ly*c'j}@) bzѫJ￿ "߿_W?)h(,ɏզ1[1Ɵdtu]ZR80<>^޾}; ۷oE@u&U !L1*@\WIġ2xh!32Wɱ9,Wav/N/q7p8Ld3Z R0sZAVqضm7>F8].E{#W LTku֋ӍrH8~_y]e{nze+/˒RDz,bZJ!fD9,K۪Rەsė˥s!y% o!2̗*Q#֚R tck U ԤVխ\K"H~hu_@\JS}cjf?EyHJ[?K|{Gۗ} b+/wÏwWO~KbgkY"݈Ӵ yZ֭:ϲ.e>?~!h\JKDe[u!ڶ'sHYVUS HM""rwwdΫR o;t:87=?j9| q031|_Ad!Z޻ZOxܱ~Ͼ$rzrGܱ+}ͧ~[:USaYb 1Ymb fGOj~){gi/N|8rmND*L۶)&Vzy,* "tݶUUCHy Ɣu&BQ0aV)u-c Pn}jBǛ˗/)lekn[ 8 "Cd" f"R\;V:=hWOaΏgrM|joQ>_vf-.!ޜN=B{AA8ØmC˺9Sـ48 jYאҘy]2O^zUݿzz|p8ضn˼Bv>]3Zeqnj#x[c{:`2=<<8K ``\..pع#l` *~yoU] %fֵq|\=w agP`@3⓮xDo??[_m^Jg4|&"Pe`mmy[k*__ZW`!H/z+|N["B̹ĘBbȈzj41N"B킎F,uVooo\=,[UibFխ4şwEׯ]0w_?DH ULK Lvn??>6Q߿@~|Y\}ۯ{ޛ Ejoڵb'!+0R ! Ӵ;Su-w19 Ө]7 ic Ǹ&@D U:!~?`ZLbJYR"DTDJ)Sj뿫1זqx9PzmґiGS1q`N1R4RBS9Uwul)qۯkݘu i$A'yȗ76)kL!weݦ|n[Pkއ7)03#c!8- Sml+2!yB?tUÜb¶.. &Grݞ̭w-}eOc_hZm'Ng̪ L ZbSQ+oD)G H sq1Ci}5 P2MbNovibj1ZjfBտ~O!,B+VאOQ2Z:n;iõonnms]J_0ye @!((:"B@@ ~GzEP/ˏZRH 跍ȁcT|QDbCQ]-`dDhNËT{XJv1mo^q#}a3p攓3[MH`mj#!AZj!eDD z<;U-]ZtbD֑ED֭{܄@1Ƈʦ1~G!(6'n>Vmyq{[k-uUi]>0324"y]m )<"b")f1oCn}<iw޽yyQl1C^~Kà8 C)nE> T{{8.??I G))2Ƞnwy3CœS䀈#8[!Ҥuh]i&lV-ʄ iuFsk6y&w62tpoLUUjS33wDNuza+үLo|Si$B@ &:3 !13a V՘%Fd5| }$hݡlngfW|)fs:ѠS:cJqFlRNV1Z˲I/(p}aqgryK C.ۅ BS{!Aiڻ| +?b %8M^R6Eյf(אQM5жu7|hSkM@E+Èhs̩7%3PTa]WO90'ں+FA7} ðMAӘNJY5;Ѷ-kvhkmHc(|;?+?҉_7\#kcT8E1c LzkB!0R{ BDj7BDbBkM8ŭ7D M{NMk?<_Qhx?LyڏP@ji0V wj3TC5E񭜁QͳJcDdd"˺ZCp9#m̀bc+hWyryt\Dc@(:! 68̑^yqu̼.raۖe^h+%("@sxCJ~k ɠbQjSUAU 7U@Hheݐ ,\]i߸(ZR"bgp!^sctT`{*;m(:gښ[kB~AC6FMSz4+X7W,YZK!&W~ tZ*M) T5agD|(km4-y-*?O¶gQzWqTcXUͰ*"}ZZ+Cm*!\n@Mj53H0_@ӴWaKۇ۲ܚdn3݈SRUFD1ܚ9SHvV/<>-˅,H(l|G{זR^N]zu艵j=?D8=Sbs:ORr8p_?G0:#4qCjZ{oM mYYQ$"=迼оq&ֺbs-#a;D7xb*35J^Ir޾= 5 x3xR'"$(D:!h]|ʑA:.LDi)01 9&U˺ \o<C tH#m a!B0LSZkmtu @Uq z)Ʈ{wC3ɔSJyb̂L!pp|2!Zۏ6鵮C|Z@^zczJj B T pQ_ ̖ )pzDc`ɧտ0Gi7xpŋҚ܎_vݔOצ"Ґ]GFP:z)KD+]FD!hDl[/cHQ{p)({,E ^BXVrJ`Fl5 “$NDD`jD[WW|M!!qzDsFuۖy>7Z)8_Um+!W/`Ǜ{COλWஈyIq͋q̗Cb˲.")% !xN\]Jƽw8Ð8ϾG!G!8{#R}L{@z/T(5Eg9uݾnܫ4ȫW|RCeAiBdDcFd]̌zMsȿ֤[Z;q|oȭ TB|$&gG5z =i;3Qi#Ed[W:r S\J@}ݻZ'{ߋqZcZ^ÿ~g_Yl]=<0!m &c(CՋ TRo5QFIUe 9Rmׁj@oݿ.7,kݞ4뺪><|O[kገ) <#2lM{1If`&eOdRs`W 1!0=RD>]R>ܼ8}/nz[oCo[ 0@/"gqBbhblenw݌[Yф {1[5Qk!EE>^K70zb>d%s㡴johLi77K\%CJ}f?MȥUAu1H<cLx͵vہ]+kc'kӠf0q \JI#b 8z$K t+6W^9n),қYZATB!$g[^!=KJR7PI)[̩n%D0i]~)%WkUs64 [kR%L631& 6Q0 82s]jhfn*јrA~}v}۷i΋wlq{|">>>^.nn+qjYmD0vCy6/ݞJ C8񉔵mۺnkZfrOu6ݙr/!9h ^>"bwDDΫ "빔rr/'_J\/^Kldj Ů !("|]]@C aK[-Aqi͈(7UkhC6$153k-pvbbfOT! )/{! ' !c"~hLMԦ1m|3.Th_:?| bJ91.mWi HjFrm0("l[kF=StbFb 93Gi|"p!&t8 ̀ Pr4NÃ{.,1edo3*VZzZo !4RyZD!t<9E/e]IjN1ZrJpz~C!aȥl]F[k)q" y ֫H>MaMLw-H.#G3]Lb64[4A<DSD0 `)\gE:-oksĠyH3MWIfW0Z/!"]<9)ª^#m&r禿(/dT^/ ̵&W̠:yv])E 0o^~ӟ.ˢbFhAP`D1w's]?K9!0ǀhH4M圻VzH1}ksJ8h&ϬwH)7D"4Im9q^ `ߧL:q\b/պLØR2fd"-_B<ϗm;}ۺ)~~yqDV9;Hkm˲8i#G&\um~v#]2mRJ!iij^4M].0l~! +v;S\%"1RLbl4yxGmӴ1aBz63bkomh`h@/|}Ĩ H8ٴ5pA à.7sj[ҪJKo=pe%^;' !U O|c+2!]/<0{:VjV 7BD EekI)rcJ){]n۶7882wIF NUZ %Pեla͐RJoT{+  4+&@`t:N~L)e3pPTm߿@Jt:??;l[q#1n6X{k/퇢M Y4+D7A ,ƠM݃Ze|H)uU0r )>.}IC H9&a@Nm6o[* |廷_~9e|V{zou^nbڛ4ij7TN\ Ym ǡ{ /Z/8D*u]`5י@ti "Pk~n"ym9ٲm̼Gmlۖr(r.bO4o2@.Orj0CCTUU0A5yџ82ۦ xpΗ͉uLݮ1e V$H9˺-zn1pPԘG"vq RͱnK<\t=MöuٌJ'Dڶup8)VrAW-5FBSHCJn1""EZ `&dR#qڴ6h_6o BjҮiWJp3 >U:̼EFCкߚx:JۜsܭDZ0 ?Oe&I@kjқ+mKvUdT0YHoW\jݔT)!!{aCry6^6Uem>3=N)EȻE?ǝ"bI>njЫdPUxb)UrPJr?4"HͤF@&u۶nthRL!jk-\UAd %fHDĦ.o)!S`"q 6b7^!]^hy@TC rl> LHb!@ YUED_8nVZYcﵔ's۶TRIs;)#!fgJ1z;D[-D v#mcv[%ɷcނOM| >mx~AGc̻|Z[J!iKJbm+eqws( `1,esmX HF'`$گ6olbasj&{zvCY PKR]*}=N,i/bٞ!ޕ1R+] `&L;\5[[$\Pa`B0E& .^=@LC ICDFDʶx ,3rG{9 "r"-̌1 Bm׮2]9w̼FfeE4>4!f`&3A˲c.q2oP zsPLcSo #@h@"99A=?wCP31%T TUR QV@j8 11? |Ød'3L)&JYUO YH(M{o z{YaLD2OCvVg Ȁ9cd& )1i„La&AD̄( bU;NiHr&һ`HrΠH U`fNѶ)FFf$1G4ewɦoX:3rDu]:0`Rz;!;8,Wy۶B|/k"T!a ( ZxS ӕ'].`ibXK!:)ͧNJop8ynTJ1RJ_|yq\YU(HY4M`VWރ'"4c y5nɛHeYv;ʺ!5a޽Ǒ1 @n#PJ!$39(c\k5-0{CW5TkBOӼ@n7SR|ㆀ"ΓGx폧hj񐓁t8OLV;i"vS󒇈Vc)e/=\zkjJDR".QRJ$QHa&?<#+Y̮{݊5Zkbp8|[~7:Lp(,*áDu)Zٶ QMWpwO~) 1RLIU]T.|!9$靐O/Nx%Ÿm˲!C! ^I@jc8Cd@BJ<4 U}14{?я;߿>]T3"ڮ O)iqH)Q0=On.keR63B fPsUC&0M#n[ N)kR7f1Vw㴛q7폇aB80t4E ]rJ"] svĩY r1:"IDaR"BL)0NHf&`JU3vӶmѶ\m-0M#v͋/i)nl)5 39ݲ,gQ^ Z%Bf>N) 9^$!ji7MS0sDܶZcџǜ=O/rȲ.!.zsKd z̗uvj-eYZ98Mn=? U$`B!Z˶DMcJ0rs#R )e` W(pzA=1۲\kRD'=y <̐ MӐDžASqS夫Kٞ3 p|+.Һ+ԬnEZ¯fA33m}zeFv=c4ZL! ,3n =-iwchupfKٮ׫U q]7m xE@#B/ǫX[eUt@<,!`T "];N?lOm@!Me6tVr m[kӍ'dں~ob&]ZkiitKkmq۶!e"Z33|TWaDb)vef8~C5ǹ}y:왹ok/u:",J9A֊JZɠ検qlZ=z7in炡@8Z[GR D*"OPo֪ŘZ+ D ]Hͷ9FsjK&bBj^ZKpki@j}aVLq634w@WSRQԶZ<%\E9Ea1i "|Fg"봁L{kɌ8vO1&v=ےAMUEgQ% p4R6%yei^.u˶-V-"Dԥ()qqC׾oB˺馗!7oG?6["{xx@Zok^^Jq۔_!ѵΪ[Wâ4 ADZi5p b)EUmS1UmAј2/UgvpMJ)q ]: Ɯպ}{Rbf)0YZ[{r͖h#H ͟S$Q;VnVމٲ5ՃJcu+m%5^{+[)/>`O;<<< Y4/ZC_T?|n[ ˲Զ1{C@$ڶIq5B{'t5Z=VLϏ󼴦MDt]-K! @^;z)B몢*Vku~Uz`k=W_v l%s:Ɯ幁2 u5gs FWNJ~W5zo +4#0Dgwb!%J/ @1Dqwh&ȔBЪ]%pW@S""0gDU1ƈ_Ӷ49RISZ+E9!CJ)c`|1.˂&"W纮Ӵπ2h[q\ْRN)~?y8ÙRJi[ޝA˺{nav?~ZLRN"H]Kq$`X T03'`& XZ]c硅@)i $1UUֿpwWkiM[cDB Bw>g^剘t!g\׵-89_~E4&9gn -oJ)Z/ekURK~#ᰫ]D/%xi@Z uۗ1RB`!Mn{9 Xz DSyR\/zD d3Ç4/^޾3-0aY"4{뺙֚H|A~7#?wZr_z}uBoRbBE([ !pZ;i,Ǯ&Ȼvݽr$ƮZKȊ !"~S[^h &UA94k]ߘoyHD!MI3׳WQ*fFz˓5U?iG)W0]}:D\k-8s  D\\#/8!Oө,(h6B1&]x] |< ZkTQkv Dp>9Y(kP 1JYEz4 }>[Ny6 RQ%H< C[m˺x"㑙uSZt2S>v>Һ( a- {[h"#D$q"Ddj.#"0Ŝ(\izĘ!bC@. Dq~8oH7kE߷RzЈ DIZ*z0&L9]B˺0n XjiCD#tӖb0sY8ϳwq3E8 "=R||<+Xch??"-18} OTu^V]лVS֯ ba׭ûy W+O#ΏmΏޡA WP.@tyҶ3K0d& aHr0붖m^.Gb @av]f"LiȀ*S^bHrV.z_mCyȃwӨ"l\9\[w5U|JqYqyz]וq-rne+2sz !ۺ.)%ܶ BzVk-j;c'8wm2jA !\] TZMdvnNG"'ǘB2RUi=gRR6";D)-|8Tmℵe]Eq)岕sy]j dsL"TC875#u:tRAzg jIzcJ.4 yzxxH|9)Xq:MBr0s){'HD@nw82 d ŮmY0 Tr13!q[.a:Neq/˺C€J)v6_<y8tJ,pضګ>0r bm 5/Ǜc/u6/^D?aǜ?|,OCD#cUU]kNqk7W"tOi !\.q5FtoQdU}NUw~ߖ87/o{m+]|u53VUwx8Ն! (0Z!Br0&h kmnrnWo@8A\Zg TRJѣG7q9g #Fe~z%"zpLy]U5 W8wmeRr3= OϦ4рπ^2yRcݚTcTzM[O@=LgKﭵ!ד0=SR׵D9'!a oj\ackwqJi(No}[n|7o޸Ȑb)e- U|9a<̆JUCTdo8QԃUl= m*0iHUC׉_HF.6H۶R0B3׶ߵ.v:@S5mU2s޶MDRh6 öm~ y5:)IJxm+yK@:4UwȬ؟C)Id~Bwi_[.4IքY.ʼ]}j"2 gf־?DAֶj"ncJ!=k7RR8tޥ>4_}m*K4Mqn,UEV/`SvCQ 7Z}? F;@V;\S\j.𫔠hr.*QDDA4ZWMCCh&6i"@bh&j;|4Ǹ+#kS]D |6 Ch*u~wAiC'Y3mbme˼A-lz0&@@~4SXVZlneb0RV[mU@Z齴.eZjbUtUMZVUm;iy[V[/ձz֟ũfJ"JD~7n^|YM j%.jo>10.pXk"sm!ӫ׷Lxs{]. 7!`b":~LZuuD2¶mbY䏾0M7/?+Ck Di❷rtU"a2/K9?.պk5WMSr=]{k&>hV [oJ_͋LzWzyURjh%.`Jtmu] BW:[ZPҫǰjVv."W;o}_uvKe]KT1m[{齷{R<'e]emkmZ ^W15CM5PS@e ׭, )z9mWAe["֭><LzGar|a[[)o߽-Zѷ-k 五a߽֫~73 !#!{33#Hhhq~~k!Z%<`o<?g?I$IU53wȈ:zzA산_hʌ?>U#knw|z|zܶ9gUsļC)voTfzNDU˔Hs5?:qa^rN7s[7SX5@$2"R)MWKfHC)q)tϧsdDDI(nj> i:朘S- ^Jv0R甿̛Ea #ȎO`t)BK[eQ-y@ r#!'V9<CO4kݶmk]ܾ벬)qjs+1QbKa1lsZE{k^7ʥpf4. Ku9E+pq J]y"@iÐI,.*AJ̧i 1R "w.Ro;l nnS:m]!RӔ U$H.bqG!?!wCDZbR 1s 0Rr7Ch<05JLӧ#ԠwQTr8ER܁9#ra(0 Cs*9oĉ)xX)1 ?È.wfrl[UUM9N4ڥmu׷as.>}ەPIؐ>Hnd7/|9YWS-%L.O?|qRԴiD)*R8yz?}Ç'p@L%)%K Dm[|v0 URbb ܰxqAbN iqTsʥ pԚZMr  h7dJUUǵa̦"&վ.KJ)qtKNL6#3q(ÐKĜ搆2NS =Ԉ8Qk]p:SN=8>>^qȥ<<>X"ae,Ą@˺lĻq]_='fLLȷUU0N fDxq7S[@S[JWirJhC)!&@aؖuSp/)fZз-2#1"Z3NiFHsB艑4M%4 %+ %|vy0eh(oP.۲Z4Ӊ 'k/ RH߾?"0M#fjpmwB1d*Ȝ m:rVd*{V<$xڶ0M}(OӔ[҅)q]LP}LJ4E+!2-Exh[w"j%N ;)$x^yA^DE38R"="1ʅS$1laT#N)%r7bq_(t>qM1Ly!7~|>?}x rbTBV-,whsNtiOOO?,ۓT4Nt\rN_~+}?}|0_o;AJ̜"/ֻ,ˢj)]L^[kj0 AffC(PU@#3n$"¶֢è4N@˺iΙUk7󈎱cpwpC@QCZ̋kՌ.lkkfS:bB4NJܔ̉OSιI)k#1\?.: Ǐ9'@{||\Nλ=<<~݇2R e2s81O?躬p\s#y[6z咆uYZm3rC@Ĝ` Gt2XT\- 0Ҟ駟J)zz0Qvw@tȳ ]Aۼ88 H"\rV|yYa<.4 @)98[GD0oBܻ4%"e&= y~?zÏK`c5BRVi}ZT[)ne3sٵhnhLDy`tLYp &ض ?ޤ3R)e~8Z31uE&2 {GV)T2twD0=`mf>~4Ѫ=e(A (0 Y5v-;^0QgoR &]䜥r[i]Wf430UD^=hmq׿_?s~no_===nIRu5Ĕ ae0̜2Z:۲8]u4YZkϯ/[/Ǐ__Tb{ RJZėe w] SaDժtGeG%BeGM|)n`Ucj6T7SuP3DmuW84N DQڱhFRj@-K9ݗyC`1'^j&28ZdU5[kx7W3DvB$;1SYG%?SppE*"+37G \mbT%oߙ󐇔h+ޮ鲭xkB[S~s*!T#w~zzb@wQPXSr`]@./ʜa/J a)H9ܮK8&mtk+@j8өmI*/(責*۶hӘRF3##mu [ "XX 'A;G Mԍr=fG;g9`OݔqpwX?]^_ZÀlf]><=!FGK8*ͬn[XX.)[,jP :"Rخu0^dHB˜uEb&a2)R0tiJ ÎhHpLdN1=/Z{@ WR [LڪJںZ}vv bk(8SM:R7Ep zCDD̬j P֛ij$]wұn7upPR8 @).@t3a]65K䮢@MAZny(iLui saXy]WWS.t:@౑Ci&ƌZnb#<0zGD)1"2p1=b 8QJi79K$Gw=?m8۾ʌVec.jNbf %0;źJíwSΔ2jU8# hkֶJff4xki_A-& xi;nַ&2̜#b`=2fav_1eNa лƜ999r@Dz)9 1"d,R8Gݰ֮< Hasz001@uyt:DNP2MLkEyFrZkq2=ouM).0Vw{ooofm7qp4!< R4SD*̯+y?<믽4 RR7| |h &hʪRw qPR8lbZTa"8r {HPK8101f!H{ԫ ǐy]Id:xwzb2dvVWN) RJ)'n[J|9MBWϡu y9~OƐ$2e2 - CyyyLOOO0ޖTr}ǁK __>>n٧OJf nUb`Rαy8S*yۛm[3kis)2Mq O /yDհ)Tp]<{CÇ궠(] -#b, ɀ4S,yUߞ/gQ='r$Wgr 9siØ8s@} SJM8#).w⎰ǙU]ېS8hm_%$f " (%ĉUp; gZg6D"J9wPu&+=ofݐfn&AqwW) %L)cI[Vz@uPJo7Mw>{P@!m&f4Pw7xJʱxxz*{$l)ef,u]׏?>==QΏR*mYEf.<<!\.[S?1:uUYns3.FL*)&}YȊ 0JJ1(w($" 괟m672RJC.\mYV58ۆDԵ۬;ćLDÖϽS7E? ;qծ w%<큉9dRnt[SwrOe¿7FEs"m^yVJ6ُ K睈j ˰Pr-rcDuODi`XZPSZU}HN L8S)K|jǪJ$"ʑbDZ>@>}Cwu 2c5LЈs2^[|y<}7""8[kDvoS)U}iOOOy)q!Q.˕7358аi0 :r[ K O)R 7:6<R0[ " kǁ#&(eW@5fzw>v$bșnLhay8v^!] (lwԲmcA{8M})pu]: 2^tswbqTF)&q3p&RʜapW3@n! Z/[.$Wk.y!`"rgI.4><=l:úe0۲,ۭpoo7wk۶ nmW月L|j7Zql*S"@Qdɩֵ0杖-̙$FsqfG@ 7C럪mmc]z&6zCtĢ]A0o߾=<<@7dwG1%#MU׾o{Fri@Zql][$/Jmsءswdc@]͚QlvSn H~3E2 WS7H"VkZ sܠHPU=uk 12:u DEdH'k_: ̽׻7A/l+aBCD(9;y) T)g03˲*"(W>f0.`klJ[ݽ.ss.9g 'nfV{[Ç|c)k w*b@< 8bV`ȥ!sꔥi-1DL9g{yyۖegD1DͲ6o&3SV ǂCEpH9f/"v&{2݄[R4 iH'=vOfG6@g-NyC`)Q]E8I̋wcȸaJΦFRrpD& a4KDTs+{PPrΈ^:۲$~bQqh5x:;Xkq t0k Zagx|Jv'[ۮk2mSo:IGCz5 99CIuzkaXжXԧJ H>DZf [|n&c.x[*jxtic)0 tӧ?_5PL|\}ӼnFw##50q*]u5p&rT":t<a)Z] ݃p5,%lc;4;Ҥ\=~\=D:."ES L/)}@w8.o\LaL*X脪{WH"EFC2t{SkMJyKQyÃts|KyH)NzS^JYRr>ui0RJ")aJ zX]n8灭z%NxNt:?\8_T?!F?aBDi(f65$.%P*}~RJ׼}cP'dmMT2xw_-BxPDW..fa@%Q7%GD@8N9 \5s.pd۵߿a8}xZ[2i 6^'ĶmK)*5BmёvvGC43x=](:; `<̜+@BTQU%LXDN4|Vkou&uQg{ゥ Ø0eb${@^7yN)wS^l2lPq)c)%um'"d3sDF#bw94xXD>̽Jyos^++~ot9{&߶acGLD[#f;cNJ̺;!$Dd]W^J)e=`Gb L޼n"{ZkWL*i=n%"j=B (xf~W SBSuUɌ>o8= a8Rz~~M[k8eg㲭njGmzp撲އ0X<<.7ZCG@fUMa(i3 ez -.DvHf=ЂY<qַZ)~uPC.k5. |ˇ[8X.%rrA#@+yX#isp. 8"v6FJG&ixy_N 4mY8'R3'=Z݌8Bsm[fK޶ ʐ,%^{#"z*st."w$WpW M{JLDVU57o`ijGDK_vуHJ)a"+-]$t޶M Ht2+As@G񔭊)p*oo9!ͷ5zﵶ1 SKqD.Yփƣ.}H0@@PW8%t#dD赅,)aBD0PUB]Z 3)P)îg@ ZP,: ){tPo#Z5u#LcZR7F{Pl(`eXx19gnEw3;|v'ǚGSm۴ }ep%{<_ڑ< 3 k)p7 ]SmvAC)1j]kkr:zHOK.D*5ansS7.֪n[v^Ӈ Kb;w5u4 DNeDǑSH6aSJD SB,0avo:}y}5ỷhkkdH/Cmi^{Yj%ȴIjN!cz4Ml1 6p:U{ ޣg,)YI1eM42v%ȾA}{x!!!csRf4*ʐMö- `z])q"6L>)GrfvzMHmU vj[=:' 54?LJu!TAw2@\LbƒmdO #[Aj4F:ңȡQwq-mDˮ{n];]֢cyA$c; D @Zں! ipq`U51P}wkhkNH0\-ԂAy(&8;`K YRiq$.ޝi/!u[j ھT///ooobD<_)Cm~۵mU okcM:"Z.=ѣ[fpUQ-i^x 5C%Nնm5zƤ#<ڣhA!o7X-߾==^~-auw*r8Ʃ#71Ka'l Oh*l>=&9mYm5п;ļ0avKS+ &׊p|׷۶m4 lMU,#vbhE>j}6"jMSdM)Nv9_QU߮=ކ1_miDZ%p#HW (h` ā|?· LdwuG;vtD8x ڧ࡙92!4pSJ M!{k ?Oy(npBhۼS.RփX\RnDDWv{; `=s]#2qFіRx0)ĸ,.`aNR*)mY1̤ycM#b*|>˒ gwZ݆!}۷tVT=.L!{v0 @U1PGC% / |сfmMzwQ1J Rƈ3Or9sVvv~Nӈ=z楮u,çO1_N˯9!1!]J@w]<<\d@oϙ9X7)ͷt:u]ת[*ϭmyOB V3`)dgTs1ܻhH:M 0&KED\53 )k9D$"Fs"D| ,*Wf[Q Qi@PS[&@J)"wmM ,˂ [k)~i^Juʇ.SHpLݺHιaטJ)Jk3srko[k- xmRJ;@UE;",JOg_DLAG]!T[ߖ!6"fcMmK u5}몽u۶>wm}npLzM}xB1$b޵j&xaDoeyoU v.[B\J>xɉDZ@#εR@& 9_.p؜BjmZ톈>f5[ DGUG'"@&wϜj ȷ17$} I*3 0#24۶$Mz*uENO?}pf2ۜRyLmCʆ2;kcJ_~?~zjXIzZy\MږKx*]y)Noߞ󺆵eN5\! ĭ+k6o"6KD hdv&];Du4hޞWcN?r- ID e ¦87pBU5l\),] ~׵vf֙Saf$fZaȃ9>>~nԇ TkE3͸HNzW,*_`]8ST7ݶmȹn<M5Xe*ٍyn*DҚ\>9A|Cs ]LTrvDCpB)Q7 p92VJ80c韾IP?lTwW+G0b=:Vݦ "=ܻmm"Sw~)#Hs![-0tDߣy,a9 u춷HY8F)3bIrEE/i;hh)YJf;{EݭIZb!! sBf|}X[ym[{ )G~}{CJ2 y篿})&UVfJ ~N_ֱ"[n=>MLk4es1Ҷ[k R^_qDDrNۦ^[D0/yH*\`kfx|8m853Sen.Ubü,v `rDh OAoW(嫊Hol" XQ!F m CzOju1|)Qajxj]?}|[֧:WwKrRR_5=/ߟ33jܻnn*b`a72`NrtA29A8ŅpW@Kbp!+~)qS6cqhpg:y^k2L˲ 颶γH6 3A¡=xXu]2ÐiJjxľjK* b\0qyc e2Q0RHֆ1;f>Nۼ03E PDؠľTza{O`_J/%?ϟZkImQ݅uדּCrN"єV83s3 cqd_W&ѶCտ@wM@.*brN豐@Ủ)x BaZ] /U5̐\UaAsOܚwJ "m+[s<̘1tu0"Wm\]n9gqϧUS~z| yy1O`fү9@/&o[@ϷN/jKWlߖǒ׶ "n)mksNHc&Ԛ Cf9]_zC"'6(bx<+ voPVR߭ @ku3U%GnéeY=%SŽzSƼnͫ2M0A&bDyRo7{|,mu݂c˦K#&3]3ܺ` [mÀLVp$H __ȧ @7ouj: -eׯ(ӐݮI2כ?ZkcV0+leYe>>}lØZk7-e0֙oҷRHr 7UלnĮOO?-]OQźoyiu:Ej#ژ5j @\ѶBdʔ!U@R*bQMHb9fJwsa4t*X,r5rGJRBN$E@miߙ`(4q`mmy(K)6 $"߾}zk4bw z/B,sz}{af䀈N`*"20 &fTpG32dlJٓX0N~&46Nܽļwso7pdV1ENȏv>3-+BLyZ'a2?Vΐ1d3LԠ S_o`sYs9aupHx㤵ytֱ PDL}]?6F:7w IݪNM;l[ W.`kt tRG:cCxD>9t@g"hD u D "#߮=7*3 AtQhT{ۈ?DԠI4Ô˔D Si;auDo7&kυ˭@ΐ2oM˼=> "ޚooD)QS-ԗG`6)Cm눸xfoeVK)z{e^+f`~xՙY)y,NI׬\Ny6X`ty~׿i2t>JvpV齯Uu5f@C*"fLL:kD cᄁ~nǾwIi̼,kb|[JuRbv)e^w;Qθw2d*qb@EtnDMWGqqIyR.GuFRt:-fN4魵mYhQq~tO2"KVUQ(+ cs"|N#,)(De0Q\{*"*kFdaV:0Gd⸼DO??>^O9Dh˺km*"df 6!ؤ5DdLD20dp@9jK+ GDj\A,kÐs^ݖĉrbVPFCkDȅA rOA_2wnW rPMU{0|ytu^+foj#N8P&P u^ip*fp)@f0Ar~lMUfA% p J&]]m['DVx~c{)w0dg:^49*qǬ{9dp;cp?"?'tPZ9^\)NB@@F@?6/ wl^bdBhFoxkA m_ywn'GayShuzGRbJmsN4 ϯh1u :NߞZOy@U-ZuY߾nKL(CJ%ZTFpPر%j&0q,Ð`/GmsCOD7sD82䜆!6Ϫ2 u4‡ AtO w@W[%*7Q"FhZ٧`ylۖ2z$Z-;ʻ7(6MҺΘ`9< \߻dBl[uO?v05y^;RVs T7EBEW<7" e5-;B&w;՘ͤ3NC{38BΉRbUPwm}2dڵשo+쏢N;?*Nc|> %4|}K)Yr{幻r%@T`Yk)orq:&04RoBaTvC:A\y\׵lBZB$bPB9?c`twj~P?@C92: b p~8<{/<"{')tCo+a(3"ӻkD!60W8qպvCuCP)̎qx/AUn DӠk )MCGhTtn^qZZ h[o/0%wg._ڗooouѸ *U7#D pYrwʵVMr ; R FSN N2$nRpYmm \]PoeIUBkvN'aq)yQ_jazIyya@"1eV=EB"Fܻt5Wb'E EқjkM!PuS)TMv5wSf&Nm[I)m7W1Dpyd3w,ŷV5o}k DÚ{T00R< )zbi2ͷ*Zo.??}M-?~ڶy894#`XoepuۘEmo4spy9EڀszWQ٫wq']-"ͪw$fbp}mꈤ ʫ3;(8g#uxj]G5GrkŏoA @;Q\E N]9:~١F/;YL@bDT W uFňstIDAT+do8Nf)<- C+kvsmBT<[k8t뺮qJ/W77pvqc-8LJ뭜ҿ^^ځ(OG9RD%#J@92Ls.y uu5L{kI z08m ޒ/1]B.ND0 ޽ -^YDǀxͮ&Sح4\u]v߻,?8 j\өnD͠7u_XS*0P|6SB:?>\oۺe.{R @UK)*?!Ey$"37D̜`5Cc1  ua[RV8"913=1%rуќE׶(t׳pJZ|w17n//߉42lSJyA2NX@kӮF~c͗/ץBZ`F!UvL 9CQvp>ay{uu(D*Ah $pB@j;v[m@tiN)7xKU   !{_{}A5O:;Ǭa8ܻN1L=DKqc2q=\N#0ڶFS YrnwkHBitŏdDX- 3!x LSkwЄ3q6Lt>u`vĜuK)].CoT rQJƜL4j8 n׫030sO]wwo]S;ADכe]O@LXMRRLڄsN $23a!֥&&)޶{\m;ܭrEI);ZӃ֠T;-Ԓh5;ʷ3"v<@L]=}'ڕ>y"8r!ac {?)u$ LǓDn!Qu)x!n5@  C,}[_p UL]#6 n bEG)kw,#T;wEJ!Gs^$n9yi@F)KG3MN *yUBk͐rtBm 'ק |y( TE.;8z8B  E5A SD¸՘{nۦM\jm.fn;z&*=~"3h(&UIgbUe8M\^GB&&uu771tZݔ-jv֙pC92'8M3\mTCh]uWfKk[]tϯ.U/TT R8w;@uu؍D{޻ovpkHlDHaK*LqҞ$5]n5?Tp޾4$k]t10uהXԚxt ؚ"@ nq nد"w oy!{Lު?Ԇ{:v|#Ak.Z~hUhE{wG98'K4ac5yG;}44Q]KLW1s@Ļ`"/ԍ}d]w& ݷHHMa_B![  O420& G|Jg6>?;.!Ĩ"Aaz\qBym֥qe:Db{U^pONL lpQn:m1ƣN\rNRVWwGX 1P2mf"Va @ an=6{3Nja"ts3 < :ֿ ,T?ihxC3#χ"+JBS*DBFfD̜³֖h93Pjv>][ŋtگCAp7t2p?N^AN|:΃mr$0S與8P"xՑQ՜OF8k;oDwxqwqTZy0]ok AB Lny(mb6 m: U2޷uDۗ_?>~2׫U_ޮk$9Tdf]A YJ=6+Лf}}ekkLZ#/3]{3 0W0fTpSqƑ"g ]<_Q\]!':[HwM8H{ܕ/E,>w~O 'u"RNnffwn PLܬؑ{q 9SfbØ Ky-[ddTK:2q3 P-gFXmJkbf狙Ēܻ#Ѭ}RlUN%5$hn _-D _~#ee6($ j9{ _.1G`0=:dw_4R"XwJ8< ̯v[`A u8l@H"֝%93*xTX<.! k=)5ga߯'Q ~0sf[w:80{=ywȎe#t^޿Bݿ'""u_`DhwM|/ݱ7G?Zu$WuGpT 1T<}F ?b>+sv})1QwE HMSqy< et`mq&a@-%Lq:Mx)sM^mV]+  nӸm*l>?Ậ_^-)#s"vgXBD@({8֢w6S}ޛNOHt)EwW^Ggz#w_]v1G3!TVkw~@3%ݝb03Ea;K%Q[ч,;lȑǚFaHR f?LaH_=>^.'҇Ono_ u]\mHJu:?==~"a`U=6H?9a̧y[6Y=vvDf~L{93!{Gn9wn4ܥ38`;\V8{m c"&ڻ-El <4ekNeق X89bnUvOUm/a b;3$ E k kI8@Iu'G{l[I?0>k5)+8Y`#y,Zi32}.0߻ip͈wN4D4XR9:{#v!5l?a.:2]sw3 %n;"[C ~ikP컊x>_.f`ݚy]D=SA䜐i9۬?}[uN|.˯[=LJpy_޶L@<@oő}Iw}{_#H/6]ݥ+Yqj8{ Q[Ec஠< LRwɢSo}oX;Fe73p^Ue7`pGUPinC =k#$=x@ q4۶M|l!o圾~}ap΅ubۯS|5~pz&N[WGZ{kR?tiZ׷{WkLJb3c*[HcȡrZrX1sBFr*iϱe];A2/ j, CpKwh7}kmr |ΥEDJ@(b^3hH`wKߑĝ҃tDĂ7Zo vਕnp$~w¢Gx wwS!9%]AO*hNw!F7OׇDŽecw ;t|LwO< vD4-_=ޖy$% yԠ 6ID^^2\c9M$"ֺ,3!/fpk==L?ze$.9Qy^{JOΜ[ۆoVߪa@mhLI0Kٝvk.;s0خ_&`u3K>"z)9Z䧟>ef" O>}"r>}qo|y m2&U(!뼜r\.e)%W;?|\҈ ~ Vk[UHm ;S^W R! `nDi0i{'|#@H( HD̊Ry:ޮǦeS̉0[zF0|#k{ :\5x{_cuHN0s1{19qIW`8+N}? O1 qPHQ56f;^Mc3À\=[X5@SC P?x(\)q8FIw}}e<;Lq]z'@9 vtCsW9>>݇z>nwmpk23?;q? Yʓĭ8ҝ;WiFH6 4y7f i:3skDruQ|۵ ·Tt ? mmsD6Ui۶onn:__e:|Xz)51oꌈzh~?%s8խߖ9ui*ԭs(TcJ6 cs?4Mòկ_aa8Mӗ/f?_yq3Ŭoۿ`ݚ?h-l..JWbJɅ0'3!߾ a%FKPLxUH a:9e՞2z Sο|8MVqUs@6QHqX%j'<pܻuqfh@[3pCp9ko lL31C88umi*,.ADsysr xܙv%+ 1Y#zC1dÏ!USP яtHd0W)t%%@D{6 8x xm 88#颫g}`$p CGWG A$G`F>n{C ~?3 P\`M#73]D]/"HǯTG#+cBt|\k4_12:[31,p&G;Ax;TT54/'Ni.~4s| 47zwcB*C 4MCښ͋֠)p_2o2/k}[۲֯^uׯV [m_2߮v}}</㇇qÐ91?>/]_˷z{]kI4j]|ʏm\Nz{x魂].4n>B۲?}۷ @8Ӽۦ]ml_My4sw̹0JӺi0RJC!$pPZwwS11Zsbt`L4N9xe^  ]p>-0H%a(|VڮŁK\:;N9qovXJΉwKѠ tC0p3BiDz,/&AM1ܻ~B&q/wߟCT̏w8le$py&Ac`C;U_lJ`ET= cwD桷:J+"z87Q܀aqH:Bt? fPsrUpsH AS] pw2RΌM] Qs ヰ`H9/]%_ E{t;@L΄.Dhn>ZsTo Fr3$uwr] ?d}Zqh fs-LQnۭúVfBחnO3s}rNO9;3A. PJdzx[WD9y8)x :Q\; ZS"eN\[{{mTnoo}yy}{i[}Y^֚lmjZ̈́zZko7w0@Tut>_~!.)ym(b@]*SRo)lM+ּ //ֺ&HbBRU`]-]\ća2(l[C@^7hU 1<4aa)hj6QtOӺLjO?W@9gN&rin][snvNy \JuBa|z=]e|ND`@V1]{{-1aqoxh;3^r.azw;i>\8C}?T %fI9s{%wؽ @FD,f`l{)̱O*@ r7x_6~|p T vx F8mw8R?L pCA /Uw3dOT8~ 0@#,#{qvLo$b|Bw~a"5Ii'}SCһ`;>n=>K!&Zw_e]kA=}7Ƙs޻]ݎvb;$bcqmr .@ prc)A@4 !$%8cN^9=<.XkYk9G25e{k,)"".`-m!s.giZ֭W<ﭧsnr)0 ܚB KC|7=ӻw_?]~u!w٧)\CBpL3er0_Cڶ 7Q[ Ko̾£>wD3~U}6>&"5iRP݉t!@Oùmɻ] :;itHqs=]r{|O~wgi./oAoRO_?[ܝ'"Obv!蟷JC&Ol{-֟{B0Uf?~a-,{vB)u0 VzbLVJiUq[dtK)/{$gε6E2=#5^j󁫴VaeEZW[Lc k HDy墨S9nuSߔbww~wR:(=?Cv\b\"vk|&5 $D0P ር)3ܴ66"Ѥ{v;yʠI H>{87y w *-p>u 48u3=3cg8T7,}&Z.==nu͞*!VAr!f[Gi9_}/_}[PrVpnYH7>2cƔB?>Ag'P׹ZW=@x4Хnx3i!s( )9u{lx}gL(ܦrv~* QDy,I ^!{T3!{{ #z`/爁2@w­ݗ>G؇'`"C1tzJњJ5p:WEڶRRt8Bx0s`#jc`*ܟ3"zYtPU4mTǧ]pгUuE1-#fJ){bO8@M!ɗ4Rp8\ׅ`OO7u~p:M#|h):_y JJ>>^;<8 "(Ak:ִT!r]j#{]G]y[PnbM`vSoV g^ط7Ql7d=f ߓn.UCoF23@aHfv]s?D#⺔^Ƥ@0@ c4?"k&?6kG΍| Y6a0 8ف4R @23:O| VdЪ*/OU^/O_ ?+].t QUO3%DH8 nj)9$:_.SD>m3<-ku;; i^AA,EOC@yye&0 Q8i`~LS``k~6_ 0ZrA"26 c!r b[ɪr`@fb*d+`qۏu # 1@'yG$7KS}`Qɷa}{lҥ&ۗn* aמu8xVogE3T8!"!`Y.3+m{pr}|sz:qC3-.Ckz_O1OgoDB\aP+ǣWyc>p|YrKlf?tt)y^{qC%gD( v7*#o\/0`_%,81Z[f & `sV-y1#R$gxx9Wv<#Hp7(!Z\ ""i ~GWJ~뵊㈦|:zY|i۶#:~:oOkkv):=0v]TUN!O*(zVDLy]_M(o[ܸKK>8}TErt1¾)! BJq[)֚:,0cTm;]]Ӊ#}Yf*會lMar޾}p<=}z:o^zU(L o!&د.N([m*9ye'AqlOvn '70~Y}lt6hp'#"{R`ay5rۋh2b23{@ַ"[ ǍwSj\ fi{51F6)'z&Vf͔J۱M{Zl[ޓ9͸v{=`ϔJ[۝Y~Z;}s+ܭ6ݾ]sH΁sY bRH3#~17&;1s^2|:L9uF`Iaԥ5&W/~|||Rؖee#'֧O# [*݇C"mE@kf@͐eZhXS7C# r?"s.0x D`  1ښR Uʺ#(\dWJ0y4uUEv(45E_faOrܶ|AZ.yt|ͫޘ\'ĘrY*JV1V|7nznpulLq^uS#Zsv5H[x`4D Jo Bbf]vA7vcO7W [w _"Yn!M2g9zγwAfF"\+"LLƽAV[~~I#p~&[ ݛׯuYOò[kYD؇*Ojv{ B!mdA.3RZ A>w}D;T-v& =ם^q|O~lJۃ7P;]/וnl1 H -=:3pX`7 ժf9Bt9`"އEZR؋Zncp=Mu)ZDuۚjιbtJ˨ n+k xn>{) )8i^^:'{nL)9o/^Lo^8ʎizamc-oOC)k-Ǐe<p;s18澑}^2APwƛD]δg{YC<BӀL!"2D߿g30e%hUّb> Vu5ѭ 1u;ԯ6Dtۀrj.֚k㔒Nk-۲>>>nv5Ҏ$g&1!s)5p ֱ> Ǿ\\왦L*КcjɖJQ4ݛ~kvow$3]E}ހ/^4EDH71"WpX湚 M$:w]D9^\7"4iUSC*U9'5Zߦy6 )QmUZ3Zh+UAM@wt 3AI ?%w{@apw^G<4%j j,N=u5\[.zsir\,ƾOC1mE+( lY bT]*fiUM Yhٲ5ؤ0M(j j&@nm+HVfo7j*t/kT[TVVjg@݊lUvM4̓o"WjcSִ Tڬ45#jb ;wkq0aXrݲ#5*YV0D"6`1E f~+r&[tݚ uT.׼e 5KmnZVښۖ4;_mquy:sc՛}4Xպw+ tП`0=OೳlG3 %}obn3QL!!V ^q4),ޓso?7}kSΰ!xB5>s(/ҌGDǥ4"ET1&U1 f)gKmJq{z? I{G8cvn\`na&#lǻ$O)>.XW12;:90PVI>阽*{P" j_ vq yIa=5`]R[Y'ϣgsϥ.?Ą!xݻCHRrݽTݺ"c$\V pui+%Ws0XuE$8 )y05ZE65 QTJ_K` n &(%; Axt 2̧U ٶM ]9eZEm!n}JD=N*~.>E~ȎtdSJ4mR YUe&qH}l pq!V1.ϙJ# w526,8OR?K륨'ǧȹ4B\7pr͎ |pjTEBl*_|Ɏ۷sU`^ʻwKggs \҃gH%Dzd1|'[$x9^b:R(NK3DF4nZ4[t[3IJ}%B뒜'UQm*})UZm˵ݏK-V;lU|䢦@ sVj+. 2;U+qU*|էIFzS|WoIo9ÉZ743o]W)M3~nv11~XJ(TPɐw#j9}c|KO؛Vn=T*@Qf"LSҊ\<_ދԏWv~}pT3eV"&[E^ZZS9"prmIEJ3ATCЦvi'>3 Tv@>xG)p &9O]|ݏGJb݅9H=uňsw:B/AjHYD?u("sZpJUR@0q@Uui7K b =/h ^E{`kw> ?3g眏!s&jPjG:YSkŽBĥz]!v<;WE:E km.U !p RrBƈ`am 3y>|cbs?uB̘M-|> GJAm X#}#~Ү&=s@ ֔ew΅\[GZoYf4s1,ky)Ӑ4DX7K-4Am//RcjnYoG;0xlAm_am})VJWٷjwDDp\3q>u& /v]( 㰮+3iBkog@`V0":a:ǟm}z Z+C@ň ̘]`Uso&X""DJjg6yhmyy64Qky{K j! !MR9Fu=! apc1ww\^]mّKV5^7Q?)b< -j\)p|7#3JkDȖkp:25xwwgft|8| !GMS DaDDa7^T?> GIg옊@P XZmpT""cE01k7ٶm]8_k!P|t]F$Zm%A@b3ϐ@Z[j\Mt[HDŐ곥b `)p1V&kJE_SJR,2(&)mk8ukfPo^6CF0U=t˅QIڽFr%[랴ӽ7M'V);s;# h# ӡqY/yD,'r˺0Ta2^>c!"tn$xx헿񛿵jX2>9hI}nvtۦk۟n݀o{/˯~Rnlvt'Zv- {3ag/֚5ik:x uL\EUڤAj` F. 0& DiqiD@fZL3𾏒r%_e;NFRJkFeK^7h92 .y3t%dt'Sifι*T[˥5uN{v j;N9$‹Bp.sn(Muljq&; 1M0xDV#6%mc[vjyݶ-)TGv:N[&]b.ו,ԲкD㔶m<>|x !ڻo>||ID)m8ᐦa\DǔbkrL{[~RLء8D}qN㔦i2y_k9B3ZYoGe<cs@hਵ&fjW0TɻTcHe|FI>&ryPk}$Rs!S\{i D[ι#]kF۬HE⢪EaUt-i`n98?-|]Jf ³t >YiY~:?I ?3QnFlw>w<TٯL H=zu 8ɇ~b{#*њm/Mz9¼,[Nbui`ݸȷQM!{^J~7}!O],| g.ywi^PF^nl; /URk;b"T `}\mr(PC;#Zs6PCO_{̥j?OݯO/oƺ ^8'˿|;?~w~CkGhJk7QcL`l[(ʞ&8O~O)7h`' k{})v pUD ʾcS ޡg7T͂3m[8ޝiQe`IJi`d{N)|{ߋ1oo#Yx~ҞTŜoo?Zϳj; ;n}IR'Kl""πjUsĭWRDnIHTٚ>Jsn{~~c8Kaׯ˻<;;?ϯ4^~R߼y=`i|{oO__"9R3!4l[i?<9 * CQ|Ga0 }TJaYDTj}]+)bW眔Jq3$;Mw^Lo_^ ? m4CuYRZ#\4iEl8sHuRCp{jU$5y^ipj*0d|piۺ1z3[6co&bf׵2Lm[ފC*i#m焆֊JHS!X[㚷b8 }PsfDU[VUiRkі aju݋;*֜\̚T"[/۔֚Bx+Bm}LM*YB:$$қ}b > 9,pٲ S D^ѝ?RY朹5Na"jyZ+M u̽&%KLVֺJ˷_^׼eYmY'g&#lsE>'#mٿ}/%|NcPmiTݳ4* Ѝ{l&{ceU55[5_ $"o^vY AbĆI9#Vq_3qUG{*Yݶ|7Нto]4@}ٹG}s@D؞`cإ>&Ag,"fI׿3{fSQmDD1ƛĵޠl[׼m!S?z~o/:0g1v_d3Ze !kmd!ZJA,p33q1 RyuAZx<*b1mLa9 {F*:G`v̭-M#e^ELaz||V| Ø0R 5EffC\bpia]T q43G.4Mfv>J|p1x HV2$B<$Ȱmҽ"Zґ]bNz>jLp HTE9#H+V5iLj`$;u{iz|%+ҧp!x@sn/!v ?x{FO|\ v_/b AuV1v}ʝ;%;^mJH뚻FJ/mԺ|h8|GDf8*{1~NfƎ;R PJ#Fa֧{D_~O? ¶;__|/_ro|?{+̿;yPTDp-;R5+xrbB5'"a8̬c uW 6Yh1Z)|Z[؍9gBAU@kPz]LzY#PBH@z8rkRJ`Aԭ5"0|>2a43"dZl]Soynmi-㫗'"ow,] rr*7)˲\'fғju۶e^J)0 ڥ"{bTV8Ƙ.1*}wjjc)[U=>>>>>LJ?HW+]A[׵9gDDfCTULOVX׶;JL4Ms14umF JqH!\eY@49.vVf@ öm [RdH\j%o֊\_zw\\rε 115Db"RmE l'vC罨ײS~1S,BkF67p~߿|ʅm3y]@mF4cZKrB؛uళ.Sg\ϳݖP?q?/&!bhU dAZUEHCi_ou\߄j[wMh*45Cvn< .t=lV3+0]0 ֐|U!!;b<|b63!㺮[-Cf6DR&䝈R?#RZ=)rePGfD;r&-*eE:;mvwwdf4EDŸ5 y/ץKS߿i"]}"ǡmJ)R/۪0pS}sJٺ4 )KlgR`UZ}VsN@C Ujt9hڜ7ZV3[׵um3E._)v֪ c E@/Lc8,G{!mEEO?C?q7!|y?9?^߿whi"8 0 8&~0Rp8w"by]UEq ΕRqdZsW@Qq>!4Hyd1^tE}GcnӔJ)/^ȹnH)gU5#Ugﺞ"W5*@Cf؊Qi UXuyb/?J@Ӑ_~[O ߶mo߾z&ׅZOÁ'hR݈Em%譵a9{jUZiҳ"ȳ&E-:oMDJi$Tkeb&ε}kKve{51WuCJ9. !g>ǻ2Vmڻ"t:5D,lH4af>V8Um "ק̺bٹ[7iwCd-D'"bf6ZrkͤZUeFl"sJTS(X 4)4<.M*<>eODEϥ}RitcwSs̞BoU)؃DzfjD*[-iLLd @QU 2/ҡo8?Qw-&I;bb{ m]i݁ /yFtwڦiRAT:v״" Ui&ZqBlӶlwwq۲u]s)djtm&fBVV0" 1۶, 1q;NiH>;WюDǎ!ĘsV5wGU5S`#B0@(ms0 9Ӛ= {SJqD;tx)`~(y)ƔaҘZSJ5fBdaQNwG{HHҶukεⶭ9g)'>I]PkKQ/1*0 !α `mV6*|LP@ EiY<ڊs ?_|wr8$p\ Eu>D([VQ(A"73rT vE05>h7o0Rj+jMڊIUr1Ėx`CM@;_OqEnxyڷ*SE>= Uw7/⫇!94xsĬ2ju5(/Y!*BlTB f֬ `Y{!"*!C,BS8h&#IU wN3"2 SM:^__|u{Vn3=H#%| o!N{!q03qnڶmLw}mn d+;6i8Esj=kU 1\UDk2MCLa[s)n$1~vie@=[9z,Y;WLl`M0N:wzkMxGws)1D2*ZJ~H{_Z4-`E@_=ei0h~{{? C*RE$QĚBM4뚷uk ~YUl P yS)EE~=躎f+Tk.ouaրX +X330]s>:4 )!@9$l[.1rDlDAę?y7.SHΫ_޶|C"-0Nݾ0߾a0a";*4$/WȘMD4U\ riuOTnGD iyl1~`H Cim1U@?K/7/N!R[[q.*˼ժ8qLC4]ο EpqS}C Bt8[˥dRkjo" k%H<c> S6LRc ,"6M1HSP&H>96CSTmH˶XBh*"lTEJ)y]T49 w1އ 1}1 i9D\0$%BV5NAͨ6p}̾ahҐh1Kv,Urқyapo^<14i뗇/ߜCcϽ5_W) 4tx{R;w:7O18r9*4:DIq}.{8_I~wŽ&^?.uΎpKA5E}jLA 5o11zw7ֶȬ0ZaH1L&9˜ x^[mxɥe&a w% Cj/_8L?o;i ;!Ca1x7 ϗsFnE] SL9ByHqL;b؉o,usOS_iU{`G ߼{8.|%fB[mulEj<jU}z:\Z-ۺ\yWU1-_#1SۼOO֕H(՚m!DzyrLʲۺ>ߝt8L޿828$?_|q *RbLbkuC0x:Y˗"Co޼yM)ATc|Րc;q/^Ki]WyQ|'Am۶ek*B(6y˺&}DZn DED292\h4r|8LιBAA䐼0ǧNWǻ! Rbb[YUDnICq/p<2;D@p#u;Yfb؇ç84BBt5ִ[VU Yi"|ȥ >ךc265gQ5-גk'.B"mr)|-V֧:g ]zǏrUhb`i|YU\΅CλZZgi8XKi~>bBK] y۶ygD35RZs^y^=hͅSm.sJjs0yQr8ViڄwKb浕eY8aiekj3{SZIHMm3tK˲a:jLIȹRJ:_}k7ta/q{PkۺVe1JBر]F(f|%ADyRJ*g;Hm+КT嶚j_v) ԏKJIJ!efUBHPC\UA!_Wj)PbSVϥO0.y߼祬U&ZP*Vڲ+Z{@\-FuiݾZDJt->^Z):,"[C"zMܶn"yJc=E'd*@f5m'wy$`=F}h LfH)"nlH}CQP 3IZ-ҺAj'Z>QUUbF n!9 d[ ^#3k?HzA+ȇW_x]/ئo-oSQ@lQٙ }U4Ey̼55Ɓ}ir+9׮1HJe<l]sv}E*S\A_OŋKihٛ<\$R[S:˥弽}68sǶ,߼Tiׯ_#B2R sQ몠>9o~5KǡVj vS nYཿ̫R><^`jcj^r'HwAm)[!~DvD0pg{9B`Kεqbpu@̢1Vp!_1)B20Bn#Et.5ڱQkP[kɇgQĖecfCuݜAD圥֚'.\V[MqO"ubbb̬5ͬkHH HhAԐŌ=;DvEs@oC\ƸMYZLѶ;cm9_  Cيfd yi$ ~[+4c&Rifu~`f5 BD BZ0bD4F""a:",Ma!#F "vj!3C")bUctQnR*u+Wg16iL w1=WCb,Z[3ubrRb%ALs!6v'hsPn?7snz_SRk]I ] 8!HZ)zmM1xxv4<)iJ!kٖ|8ީjHښZjbFĈc_lfUJMiRJm0MnexpaܶmY@}^:`Oq^%mRKЌNw`^&z{O۶9v$O10m[iqs|<ssg=[Eѧi۶*>|J)>qyv 6h6iMIB9 D$NrT#cJz=w4tkpCUE>$"".N[k669b31:z]nPJ& TUO<DNuIk͇F.sT#bO[V]cCP0tޠZ"#9V@Z!t{Naz \OlbB@j씔4ׇғC'j)qk:NS@ xaعުRTs0+XWZl"|xsu(9,ք{=i;"f9e셼R t-RۥU5Cfgf͘KG~FOA Hιק9>Ԛuy۬ɲ.\V;ǪM R`o#$왙Z3dVn\!a:sD8=Uj.4\T5ÑkibRwyV3SG>朋)t Z}伯tc>2ij$r!ӷO>9 tΗ&F>wf fq [η/U5@]G8LA9{PVCsO"V rC`·^|6^uӿwTU.QRsk_i{1!MSKVHcbf"1y[>xV2-p8tQJ{'A/w#0!wAUPN@TUxk-nk):"L\Lt0 \(/W mB0iL\gdiVKm?RJ0DUx<si&`1v&d4ZKɊ(fyVL1XzQfWk%Df<>"ݠDDɖs)EZRښjkv$0`\+zv[k9m"r^˺a)jfӇD)ώ80QoOS0 伏7i)awuc<{t}TD#Z͛fm;=/muK_J uRmrwtR/͗9i љ i 9ĨjD0) t0ćqbHcC01 (;n)q:Ntwh1wiHtpwp?8'&r#1RBBa BiTbF;xڀȹpl"ځl:T\ @)fUMԜc1+Jk֭nǻ<iS3%gm+""2 J u.h(Ʊ|RJZZcL+¿/|._g@s/?/``᱇  G!59'ZVyעjpCCzaH6;b`Ǐ@tRyMԭ@doq8xu}1dZFhkJ u^rY[XSH12ĘxȮk9ߵtl@xĐS _|ݻyt:E/s>L`/_ԭ?я1CH/ADj&2L㺮fRD>ox|KUArӑyZrYnVMJgɥA6$ 1Cu8c {~qSy5?~|{| C*eu]m+xu]_yZkC1yS{m@€чWgBSJ1yt丛:M9Nγ1n%VGKW0LJ-uuRCDМ^z?^J\J}`bTײnpOw4 .Kw:L0 m]W1ߺxrYV3,H4ޮ^8\.3ۋe)ajj̜ϏODp8O/ou^g<1uiMVy-ׂ=4XUZ-cׇ]^ im[wlkse.(E>njtt}\ )r1*MԭN1QO祓MZ5^RH&"eݙf)%c | їc+[+U8?狟 0xo*L9L䂻\/ݜ5jC]wг qm#iBĚ !hy^>tA)U0e!uY[wz9_>"DlVr]egiRK55ijjy:z͔]ۖ͠Ŭl UTt6P f|kMT:J);4X׭z>_[ϾԲKk^גIujS(cL B7!rP5ղJO|^!:0]2!:CO~.Ͷ>l9DU*&l&6`=g6KLGeX<@m eE]Ȉsv-]^|"sZ{LckQ[-j&j-1\yY)mxTJI)q7//>|ղmRjL<]^zu>qH) !ֹׯ_N^>@y nLo,ߏ TK>J|:)d;GLԸB̉JnA@m )c.)$ ^*)4F1Ulv)wx ]NDIQB{yyZZon޿=f]W-r1Rx?>=-aG%L|iRX3˶Pw[HDHg 0 Qim)6Znoo1#]9+InD*ZaB%*lZ\3Քka"ҠHYuRJw4Y )B4Ѹ{6i[(N)1Zk#6E@\b aPl7Ȧa$DTP)%-%?y_?5~3֊#ULkT2+!Q@܂FggRkAhc-5U(̈́P2+{ )B/qvB HT:jw+gVM$ B ZuNB(-nm[X〪OrW P#WM%a%ܫ$z"!(i)!LJZ Jz7O4,ڙ1ƘKk9gMm獵弆T+,Pw7ƾe I _HULaUN;#Pr""0vʈs1Fit*$:mQj= QZK[sEC=OX..󼭁JF4Ku&żxwl'jvsΧZa2~wd#O|K)qlh6 ///(`y߯묔QV~ba,Θ,2U=r./շJ,,vN)ń-߸vjaDtU2WipBXP ^PJ̺`Bl*f-d%!PiI57!6&eq93T-e[ئZkY 0sNUkkKm*9D"0 ޿yٴnl1ߡ̰,㫷o^^~l~47.Kߏ˲o-gJr1Ww7g_՛Bfb9tԪdulvS Nhr[()m*ƨlUWey4/!DDy]n !`!!4H Ga݈XHkQ++ f cF n*>9]}B 3 (|sMhw ߞ~jʚzy+*%n LF]7Ԓ&2XH+y/sT+C%Z2a @)~swɿ;=pg/g\k[X5ox+J )ri%e%T+)ηb)]N{T\ryy9\jmZ֙m{5aEbqk)9RZR Uo- JeJ)jv6/J59r( $2FS !R:)U%{||TyǏqws[J__~ewY Tb|4J11j]J9K)MN[;'AY &'nBAN32JtA8_J8R\$_9Z5۬be[s.RJkMܶǐRJȰyBoK(Ti%"P+Λ*LHZkfVĈPJ-03֖ 4w~w!l f@s>*{g1J u}b*%BBZ;*]r ?aMI'a@s19&Dq-dZv&J)sDm!|w8`.5HyaqR3"\j)F"v/kINmr7ʼyVy%I}uf Xօse؍ymq6dAT?{}1B[TBJN9f꼷Fgm)9lykB{Bhg9+Y)u:_ߍu{95 RqceD2X`q~w\rJFRuOy/P\iR\; mL;`!ĕbTJ5"e[}wݮzBBλƽRUbXJc!F[ͲY;u1x&JZ%\1ׄ%w̬PD= *yc5ZDڶo7Rd [&l c _.Z}S|q3/5vY9Vֻ@k-79IJ:+0 ֹZIJ^EH1 ;;"qwv7ߧi7_+c̻w_v49캆ׯ߾NǛۇǧeZ )|<?Rj)i]ϧ^/0w\ o:!Ddk &fRRmZcE1X(л+)cJJ `^bjR)M:M5w#T3_X?<\(퍌i;$&mn~І[kBRi8a 1:?}s+ 眕pM%B̉{)1(¥\ԂͻJ) !4P$ :'sCuB*J*OA(c zy[G<+)okV3oyd(> 0ƭ lYkڔ ,b]Xs]ӡiBnoá9ؙ1R@b5 fDjYT=sa&m_!kTH9P\R٬|5(8)v af)4#{8Pk]2\ka"n[nWR!: 2LW0҂HƔ Xk]w] w|aJBaWJя~w_m\n]8s)p/nۖNimrnښm7_7fޞ/R!BHMݏ&/n;mM\sr-cӶ8蜭TA1&DRa?Ԛ"E)r-Nf]jcF2zaDǔ0 ݮXTfvͅ R mRB䜬sl; rAQ@ I*ZhD lZ-MK4_,Z')FKy<_ڤEPTC vt΅m˥JsV-cZo  )o %ww$NPJB/scK\眉kcBZ5P)56em!Z3\CZ{ssnUIQ QR-l r މcgnGt]e7@zM!N>J:?xmJ)PZHi0yn'uəx+Dk,H\ 5 ywK&s5Ÿ!D.om;tZ%};Ъ%1faYCSrQΙ*4J)ȂJI9ģG w7q<||RgwF:k^_u%~PJYme~fʌ///Mk ~?Yg?υƻ#]"EhfwGBZ&JJIV\Zk5ĕڴF$mu]A )t̩rAruވ(,DumɱH)wZfx,a?_iB"Vk1*BXvmv_mL RBZ+H!p_;0־WbL̂6޺u@OḲcVJTSi9 a1k@Wj)BEPF;s3(1ƘcٶMU+ |zRr m[\YJ UI(6Vc !JmP \)}osa`"r}MPZ2סJb-yf[cӴ%ezQ§mk|TR ԺyRRJR*!$ӴH{Rй^)u,!mRRyAJ )Q9RɌ_<,yJfߏQjs̵K)wnd4MWV붍JcU}{b[}?ƒSӴ4㨔B__\)Nij @-eez~>q<<@r1JI$$KRAifRk)2HڜzG<ϗJRh1'2RF+[J1qdw~Jf/7wBa?v6v֚~Ͽg}8}#4M_o}x<)ɜW>q;Q ZK4R,WeQEM5 *p, FY3H}HiR5ה:TR:_K;cTjgciI%wc7MSge T@6FR!sJݺDp+o[9~>όFi[*kDfJ) UIEl[dJ),T֌ U?A[!uDD5'h3)D%쬕R]fbR{5o߾`$n(=g̜c!1Fp= !hUzL()KɌ4#jmUܺZ+cgTR~zxTJp&eYbHtsM1|{{{r-bI5sj|"!|>5ۻ/psnIJYbj~nUqKRP*^׽PRL{gt[MYkBa;/3]OƄnooۣUS޶|vRI^.ówY\BPI%sΈ,.q1{wcIa+u>Z8F&l0,8?M1&ZۻwCDU)Ek)@&"~\öwu]-:oiF(}c| Q֠F0-9&u^r.(iPP)n<5221dmݲ,\9'+e)xT8,\lI;\C\PpU'd!w\VR(BfFd%nכpRMpt]WJ2FT(*/D\c@!dJYkB1QI~^cj3Ϝs qBj L5Pku9\Ee!DG%/y]n^==qp>?~t:}/R?Gvm1ޞNo. ";ms|Y2]S$ .v7J[LDT- aPo=1F&ZyﵔB2H%P{7FeSyw<ݿ>_΍VRNxsmw"p>/TbqqVۛqq!*.אPۻZ)f`V;vb9O!,TRJm۞Ois-@*e2CHD<ͥdnQH!u]/s Aih+y^b}oa6}Q RV)J 7RJ)㔽ϗ<z(|/"D%c!Tv6F)!;KJko1^s!Z҅Vi( jv-B>#j!K7ۛnכs]c6͋&RZnoQ µ/-ŘRB(Rr+Q2+ԚrNVRSɵ־}JI׭W 6RJPY+$  T]IuR+2F rfy:[-{/ K))et$))S%ӲŒr{@N) yk>,9i%Rq^C.vwV+A\sXg |9OTB`kw].er95'T-˖I.-I!ŗIkȹJr-mb1Z %XJᬕBc۶Ŝq>s}g͵wN5~ݶ~Gl?L8OKNef`1в.ky˹wrJ(A);Okm RJFkM 3"rZ)*bRp+J!JVS!}T+Rr*PY nBΩhI\ڎ<|<4c.Č(Z0w_nn~p p> ðuݤJ 4OO}i9}ss;/W^LD:7AB+)*S%e^/7i;)i[@4B 1Ɯ"RJZkܒw  )THD9dbFR1̗efbT.T5y"2*(L2{&%^YsNqۘjKOAMͦRKido$1m@T]u )DetrR #Aw"(TB@UXb9ZŰ;EMg:/1,7ַbZk;KDlYV-98mYwffuJIHBB 5-JQ AP}S)s: ! # s<)%o4-1f TY)!:oGmv[͙*x//K5;r/BRB+ca1\s.{ea%bU㢔N9g*`Am*TVwjF<7xjIMLKJ|ν_z|~ ӳs~۷;{.m6Q}!(Ƹ֕cݒ+MwM+S"*̜baFlr/B2,Up !v4$kjs$%2~L)lMubx>]J)~襔hi԰19uۜZ fb@yLuޔ\)d q@kIVJWG@"zgkKw~3^Ĝ[(s-Ki5ZBRA,K"4B`(W:yPed īGJPiipĜz--)%*dsN5R N3P B!0JQ3cJq<_NHdf"X7 H!4OMyY퍔r aRhu]eZsj 9nm7O[*賓EׂTRJ`*nJIFy:?cRR"#bRRq:)k^הbٶu\~@w:lki؍BBq[c9m۶qhSʤi*]jayKX3W/K}˶nZ*RRJmeXU`f\q@J.c*i>+:WP2RRJe2Tbn5T0N#Ȧt1TI*lJXh܏ e1ennnPeb5ݲmJ5-t> 1Ҁ PHXjn[ [ϝ޽SJ}|xjZaW?'?oC nx<yu)OOOn&ctp؅9g޽;>oªmYmXR*̬^J ) ǽJRyNF9B|se:'0 uHϧuZֺWqz5yYS=վ>T x8 7J)˄lL{DL)T6BTȌi2JLM/X[g;u)CqпAбz1 BTuR %@4R9c@"hm,#CR PH̵|W H77)mS7nnn7 uCl?SB`dk~9W"-`*5CLB 9l1L)eg"ZM-%0kUt%AM D qt Jq[|s: !,VfBpA֐RZ:o_v2r. T(x8R-ckw\2oKe$uy:}7Z)JD8c^EJzL&|IOS=͑YX38S3Jjabdnmtk-"LӴ.˺BJYsn.11l1载LS˭13Yk ˲6Ann1L,Z.sw*{/""!Tz YikwcJ֪<Yo}apyon1Z])U(;)8cSR!J *Y촳HS!XYa:&h9: ]N[&4M(B@9`֥QÄ,PHz\Ji&)@,r9-UfLJJJ!j9wӲnw_v=QM9mc\u[rJV:Om.|o ̕}uIUbİV8in36g9a>Ķ̜RΥĀ!y] ;ۣ͛b JdΗC0 A΅a9-ӉG(RSjarM1}ݗZY{{sWfk/7)Fh(e4UVsI!H })DBȭPiHLlJSߩz{vƻ;YF?MˀA3SA)M\06%ǸU%5\.Z \r9DT2 c:BJ9S˻h \;gmΑ"b¼\Tk @z (h#\Ln e!D֭Ѣ6;!@vü,Dp@*첬}wNHRRR5Ũ9I(Jr˶%OK]#iZXy۶HSrtY]9l۶u\ y#5lvD\ju~Gg@1ZJBF1s,ZmvP:W—Ӓ )muRK6 JJEJu]G(\16W.R Z\6)|nX\TRiB*Q-)S6bRu [(Đu)7A@ 9glU`vVS)ŔRTJuJ{ҲABBfJ*!֥fc,3#"\R0uZ5}wןܾΥoT؍㼬8L23Cuxsîo|<޼}B |y~ޯy?ϳҺ}2a<]Z"?!f)EI%R!ki^u}+*TRBz{BNVZ *m"HRl1n˺,Xoi^ . K* RBR*"+qekq֚k)}rZhYCJ8gq!c6|e8"SJ)sL9*ZA ([i4qFR)kߝCZ*(Y*sef!f<Ԓ1BH!R̓0Œ+JH B^(UP`7xwܿս7Crvv?FeY^_҇!un70sE)U6M? QiuC3V֜#< av 0UZB.~0C߭C0t֥͗wFk0x@J {ܶŔ:kn74fX]TJwͫcT{ks6R.ĕJ*h&Υ SIR)sZ+*~4^oJ)"MG|xٶC-]Șe gw dkxtotP|5{k4/ 83;'aqqe_~sJ^+-)F!duzkmyT(rə.S FrsɵD)۶si:}jKĜKBH//69u]wssƫ% EknRÕsι bJ )kVt=;6COZ uYrN~/fu0&Gfc-"3Z<=|w&J>|xt</V*S>vk>u`%O?ܴ32_EZI)1Lt9eWKmYs)ΚK J)yk9B`k⏻QiRH0߼~= p޹i^S3 ͛[ 9Zj)Ckm$+FRj*4/ZI}zc\ڭ-k{<B0x\t:)R<=Ohwۺ ejMf-0!l)RRʖqI!D\ XR)jR[][-zp9՝s?O`{T ^V"!mYu)-Sq00vSJ<_JR"LJ211Rj)BswB`%:NBȱiZY 9m]K00U*%+QR*0/+3=%tS3hc֔RZs-D!Z9mSR8  poe?Jm2m nZIkdCV5%{ew^L1jyyy9)۶!2HPj%mYg4/JiXr9rM9!2.r:/̕k*Q@TJT(AxRS=)S%"Bƶ|-kֺۺ )uB9b9lXmlkQ(D,JR-Pڜ6(1m{+LeiJymkT#)ǔ#W 1( \I)9]B_|ǧo߼<MJ:^Www۶*%w/OT|g?a^Rx>|OtwWO///Oo|r:Bk "ZSJTIk[ ѻ9SkB4;eyrz:)rLQI).軮r!sC\hS)x1Jo6]iv(Tk\8|:-\DۖZI!ęQ|7yr!Rʶ-˲S9&2ȕ?|W"!qy&mTfǰnvƶ-H!j}[ǵDu˱2@SB2 'G8nW[-J0?S0~/cZk0(NZBT:M9^$944!@ι6<@,saRTD\5+9b-$0].e;(0#V)YJAmkpc RI)Skm&OK{\j xPJ+j.1F)zV&8k- :,T $8w9g`6Ɔw]BY"ߵkvr)y!D%)L[cuM%ѭN_bz||m@&fa1 %oY6bPJYk@kJ1uۼյҲ˲6@{\l1*0Vmҵr+ѕ uIVRRѦu1FcBtLFkc}"VFjaj)%RCRJǔҭlm_~ߤ R\"!k!8r%6:snr8) rZ*15s%RmM0>͛4 rݞSXZʶ͛W1r ՖԑKQK#QPTF" dx~9l[RGْ Y#uQ]e;ghUK !UyZRZqw10O~?K9!%Z׭F)u=Hx^B~ZrYJDJiJς,jʌ޻OJz凜BncJQc3My|jDM!8w@" C9QI[JJm]@k:m[ï_۞RcԩPy[Ds [^M+;^*aݶc/ihͅSJ%7[,۲T5F1PڔDs)%-DA%S##J(Zͥ* )FuYwݺ16ҿ640ָKBm nxssׯ@ViݿzcI}:mUJZї̳札TRJ|e煀<_&'otbVrFDDrشE#\Vu}?YsFu 9;mԸnn RR \C i~x|RZ;O'fNjSι\bR$ URk5BXĴ!@ej 9WZ;l7Zi(% TofZSjnoo¶r {!% h|xw{uR+UR8/˶˴0Urzc<)g`Z}/P-K+}o )ʒ J8v{rv cvF ]ciۂPE&u.ly6TXrVJ/zY־vc{{}LsDo;nl8Os1W"r[0s-5Ƥ@Ba7}u~쇒ZB)AIZS[#kLlQ{c{ctӉk9yݬ5댳\N*hZJ} JI&L0RZ8RZD!D\5%iD)m2Řr&D\JL0DV1LZketQʹ|k.Q۶V!TJiZ99vSZZkQ)ku:وu I)Z]ʅnO//ֻw àP?~2Ϳ{y9ÈB}??ðm9|e_N/wwwa[6ޘqw{ps{Osx]yf: qa Z!d|:LjK/$z=S)4+%wY)DKBm !ϧs;uß(Fm 9e.K1y]VRIxp`RJe3_ J,PHl\vhR-E 0QKZZK.T!b9nL~=FD4J ୖR*1²,}7zj23x51@`Z!H!ZQƔR@ojS=UGtBP)G-Mmփ京R 5UI)k"cˢOel$%Ƹkj땾ƀ(9sKi'd3ZK)g Jvco)~,D(TAR9,KN%|s t h4+ \9%oV*{˔cr_Z(e)ϕLSK'Ç8:s|8cRJ䇈 $SJm.V+,պQ!R9 P %Cc֚Jv춍j1Fg,˕A lc*ӄi: ev5>i8!C%qY_տ9~sww7ׯ^].@\.-泷Ӵ~?''s)J[|||lN) |>zTvor:???ݿyi?-@_$Nx9GgDP tK)y&XR,b'kuBo[\.8#{կnnXSIP)TѲT뺮=ǸR%H): % x XjafQkFT B c0kc]7 RéJ)⋰n%ƴ;Y0J)m95u.UiW.[BVDu~?v 91ƴnm[r`t\P)SGe*- RQ9bNziO/i7 VK p1r*BRy뭕j62!u=x%Zs:*5nA"*MݢlZk9ٜܶ"&XuB@rsH!IW1s\jPhuYVfc- Ԟi4XS֬?Wo_ TB?ǧ^lI JڪC݇n^uG{?Ͼ{qܿ< }:=c(yr9}fR )9{xN 0s.b!MxJ~IL$):g5laF)r!!*b>XJFc̺ZxG%BP7a؍ݮ"\r:RRιc,J %,%@5ѺR몗VJVT{˥F&! DbsՕTQja%4JVj 0 ejي,% Ѳ[ RR+\qsn/A@\oOfRT_b&R9Ө]cjeF)%)󙙕4FdlPVQ.Rb.xq+L<@̈BT~s_J aˏn<ϯ_N% !wΟ|ƾg?*j|z~4M-fǛ\R~p[E)32;Έ߹6wF*u]䜭6ML 9g$Ck@by&O//Ƙ~v}SyúM狷9WsL(ݫW9Fj 9('\!ПN{.JD;r2QZ !mSJѵRعwsURZ*ֶ]"y]ۑDeb0JB5_w% @ r=J~oXkճ.Lkݍsl[ZלUP!rk *r\k!xoS̸*v8ʶtÒtkI)SJ}GD9Ė"˅nqmJ)%ol!8_V0zⶖ, '\VV%s7!̌hT !xb/K& }okܶ! iKr[6nŏ3ʼNjc!dF wjk -@J)%t0&Զ&l1}A!qs)uR8!1sRJY}A)%mZBQY)% *vl ܔRƨqc&*u-˒sJ}UzcĶ2Z pZJN!꺎y6?#h a\?m4Wrx0ƔZ)?\1s)n[5U!^irJ|*QJ_E9ZkK%G+XJ?=Ij崅2ψk _|m[\~0 |.8/s g?՛?{?O߷??50u qիWRjJ&h }עs/jT1kTP9ؖoBN))mK%b!pu2aHƘnyJ%$LSvC9 ɡʼnNm)|WB"ˤˆܢl}vU:nַhoK k@Ќ"VZk Q\@Mޠ954з{o# rFwfj΀@W 30Wk[5b1m(eΧ# hbFdbTIPY#JuJ)B@KײJ9gBPjm1T _Sh9eYrMm1-蜳b*rY33d̗e.qVBTO/gD)%}sݲ,)J VHx9=Qqy:D!i^Cyafo8t/uc@:}s^8- ̏z9$˲`hy&T* %9ZJ@I۶y%%65n1m |mJ9)aĶm̘svyl2*t2s|]-9C>]'IhDcJ///tup)E7M+SSRAPC ĕ%k+B,ejVk&,XR ۏt(&df^HED ?m Zv e }ʿ%Ʈ뤔J2 T.jf~a<䘶mt{{[XTmݻw777yrOGտK-ۼy՚0N/Ç"n!"RJߑ67([WJ"2Ehñ;wdƞp?,c!a*g1ZwîsVٖ]ꇡ~)o0K)i jfG܂MDXs)qi ;Ikp9g=""PNR̵xLnu }9Z3)cS΀ξylݰH0Q5( f:1tffa֘d!eOE;-?O=M+Q?ck%S<1vv܍@8p\ΗYe/@hUZ4RJm{SE| s֊@ q\I/~ߏFC\0޺{"Z%Hu)Xu@ٮι"P`uY0F)"D0hR{G7}kYfȌR6Ɩ39o0[?ZqaGm4Lӹk. "43#Q8(ܧE",%Yom&pV"H@@J`)ȵ\]aWn_6c?o9u1F7eǏկ윙]6|zrzXIN)w.׺NmEO"M7w7Z))BЧr J"9euŰ?"7w7X1h)*ZB4ZB!\\á;)5uC. J9V!? 5 "LJ ,\IiRZ1DR E+CV]Mk7iQJ" w;h^;xA@j.kEYѧ񃣻U.()Ьy}G?&z)qڸ#Q$fKEP)]rɠ3k'F;=H~9Z'TaͥR{=рrۢ@ZkyUTh_JNa,8o߇_v}YsSJrn`o%}C!2/)-*àxzSnhC{uG@/4oׯn-p`]8E;7D4yﻮke :A ، 6۲Rⶍ],*7#bXjgs*]׵jFAyƘp:=mXK)NkFCiK86@4Qضf";.PJR3ՙ=n!稭z\gu}R*1[8pg?Yc#6&JED;H[cu^i; ,d95"EP7xctr1RyQ)spw 3(y-YU*k"%Ĵ+3 331y4qeyo뺧| 8tꫯ>>K$q܇_#4NrQ<Ͽ?|0zs<™n}qRJ)s* 1M@>GVY \Xk3nR1t>O{eSƒRAmH9g晌Elk4Ku,U)\Y) 9j9XeIKTko-!s廻]?hM`Ϥog-rH T.3(?wocWk )]%io=sU[(y [du 0Ȳ9g1J3|n& 1/_jmzooB.-o@f~n""JRTjI)Admۀa WAg !˻ZQlۆݮ [P\* B? @hw;2Vz>e"ŒZpJ)|^M)N^ ,)ƮyŠJy$k*d-b<7)@+[mveZ+# )TJ>oy^rNJfR_RcPH4 9mQ5=a]%{Exۻ4]J?tޅJeӛ7oOSuBK$>޽{<ϧ疎pwwpp8m14R2cyR%ע~z~ [~x?Ӻ%,¼DBMǿ_'b:\N+u\i\Q+cu)-rN9SJ)i*۷oJI7k;L1RD5Xn\[ }r*۶mk1me]eYnv>U*>ϧ?xޙ\:\k@H(+֚Q0Zڧ5G N@TRT3iSz+ڑ;oA#h*vPq(J_y ]B[&b) tS2m9""(DtTSn#n6ڵBocZD!`> aФrئb4T4ix.k7~eu~QNt^A2rooۿ˯go8k0 r<~wsYUOS~xܞO@~]7annƍ:N)eHcJ͵VTpLcLh爨Rմ 9m487Gkm= ~|>)kNHut8777ui]]_xo7J7qC˼̻~]pnnnvݺq]mk\u6Dw;"]kݶyߺ=$F-o:Dp, ^S! OO:i7lDZ^}P>BHDl"ӕ҈:D!o)qwmakbdΟ!i5mjI1RCg016{%}՗ }?_xLJW~w>l3J/~w?}c|w_AaJm 8^nv;vw[p8ְZdw)VPΊ~(?Mǿ^̢m|R t1&jRLu]ۍ`޿R,\ vcqvAdx>ݝWjY?n'~~~)k4 k29mJSLp."Fغ:" klkx{R*0Js׍Z븥n ޙaa̤8>"*E 5R2\"=fVQ=-I~l9")\0Ēsu6 IDhZKʙ* #q",͆\N5Zvͱ=)p!g0=ۈє(\)kwnza3۲ y+U$}>yӔ\s!e݌y)Ëq_˿[~'eP5Zӗ !A8uK[Tx\j)KU2k"\6#G.4Xs%d L)Zonov] !BDZmۜιIkj,mZ 0 !cneq)S{##7w)Tc|m}kٮ#Z0qr,6HkJaTzhmsZPsAjM- }9g.-9QZk2Z7:m,"@6ݒB)EkBR ܘSrmXFWR Z^~|w/_݋_|9/ML)?/Owww_Rku=??]ZwRj~w~>{~xN_%!Z%"j薨?hǗ^]}I+ƈ۶'qZ+@bɹS+C9q9XkƴisnNOmURʶuUٶ-*HTWaiDPR5awR 6V5t:! 0CwSh #KK@)RJiz?Z54B;-H lrL$G?lt5--RR<0r,{U)EJRJ l˪4jԩͥ @,swC;(DDaZf$a~6)EXZ2vvVm[˥a9sU"6Oq]*1*LU1fDJDTR H:$rmwK Ǐ6'"PsFJ)O/O˗/ ð77|իWOOO_\.?_\.?øY]yoyg?1։iu]SΤp.>?3~R|>W˲"X) [(14(ݮb%n˼Mˬac.4jVӥ&-EOcmd O-uJLJ\ҕjOgY$v)|gY?\cVFwqio=9SJAiSkPAveYriIRPJaVm4\ r:)&Dj.p? 󼊀:Qvo@cwU*M9ט*34qԮa {/"D]#!bS] $c,9"1fTdK;h(b6IDATNK(8?Է//_\ 2 5hP5UmuvD໗ Vtfgm7l9vmۀVY˕TYr~Z9ZHjD)E^V˲TaDk;g޶*,|\@kmQւ,Bٮs^[k9qpgq Y*QBj5eTS]rXk %sERJ9ט8ymX_j! q?KS)`ܷR}NRcuhNshqXVP4>e.ED#H6F"ߛZK9&R1*2\k[)=?FV/.9}͛7gxݶm?>yt:v|r<Ԗƕ;9ی.x'j4վO8~sImEYzZ2.%z ZzeY:oU;]VDD ym]yka1p89#RJPZ_~LKвCiú$!.܍7kE.L0Bsʭ b7؍I9WXIOfr. n]ѰU~pC!\b5\JaLr M ᇾK+33jlS\u^!0jp*x5HjM!cv%g"r!$iåu^8& TMvL暃BS;-g7}<.sJEi\囗wavi盛W1Fcᰋ1 icBk-Goo>*#!ۭFupwJ)kL[/޿-i7_<>>~W<~~~O~rih~7Z3׺m[.rni}qS6 ĒJQCd"Al9r|4MM3lp}G3KLIJ;RݰG%LD $b!R*8ÐB ڸpbFT17I u0H5c(1F>ƘcBadf^X叻 FdF*̈HJq|>#!~SG)%jSH!g8Kڶ&AN2NmF_XCPJY[-Z'kqY˺,,X+R!RJ1Cf.;u32bΥֺXk s;}UkY""c,N;5uM!RFDFh2)PqoFZSaF~ӟ~7291/*Ӥ~ND׭|z|J)1^Q Pd:Mɴz pZee.eniTsnr6^.af7|3_.ifr(" (1t^ sx9>],6ƵM,r۶P)Ie3ƌ>nqL)jV}:z2@T ywˌ̬5ۖe 6KMij43|ٞb1["c-ιZ$rH?>`W=|.[J X77wߎ5~q틻aNsί|wwǕmOt\^zOϯ^ðkxwu3zt9k77*|Yr^8`-5ÏE 6{iݐƾظFd .˒`VJ].jMK!溆h|RBιu 9gTs״Lsh$I4Mٖ6HIhBRd:Qf[Z}9n[Ixnnm^xT R6@J9bF jl"*5X2|=r9b4϶s QDr%eYBOHeuވHA\kmRRf޷ةB$DdH溮~ 9kJg+ZCPhmJyi]9JphRkVu" <></r[AkRvo7w]ם/Cuy>Kz(93r%J)E[9oQ HRJ]ǒ[o=ςV^.nhĩ DTjmZ3aJu`3;L7ιu]97hk*ܪ*vgA&y "*[ )Bfeٴm9Ք6o1ƾטե&Zl=DX]Y%J֌kJ).Un VzMc*15W)?>-'~Qsp@fuAD0laM)u A3%|{J)fwPJ9~]7o޼꫟.R3t9M󿜦?y3 ݶmYo~Z{g_mmO۷{sss9=Nӥ:ﻻ[|r+R@ѲdD4oo b-ƖEpO):\wXsR q@0Ԋ}eҭoPR; RJ!}yYtҚ*jl>B1Ֆ23UIWKJJM)"REP12s)li0pB\LY*Y @Õ=yMVmo7Q!P. ۿcι"֮Jc-rкޟNmHi qkY۬lHŒ\R 0R+3k-Skf.HZ !dZ CsƛuZElX#fЪ1֐FRaRrs\?q71ȺMDm6/߼$nhakڏ'Z mlej\m^.懮i"Z8\^{7/R%F|>zߧ蔲3f.|y(Uw}לѶ#Az'P0s)Ͳ,˲pjlmyӧD*zZF1ZAZ\BK)?6XlN9p1fզY7zN秸F͍XOe޶-`bjOkA"}lhS27d?mavkmBQLw#xos4qZ ZtߌCN_ymq#4|}_>N>|v_#RZ_~FrԦCȻjDZ?]dDJ~y^ö:Hsv\֒RHDM{M{ʠjNph~W'0 hzYm۬1mu]b)[Rz9'* Vw-9+* SEikOSM)L16g;p@Y/C@P2JJ ׸8hbo]BJI^?H+c p>R/y}%q]qZJ;#"!De%sML+oG,0Bdī&dwq ;(fA<ҵRu S!OJA12aXRz_Ӎls/bsƘqo*OVm0 òpye{sAD)c5l9g$rJc.8OTPJqV`G| j9˥t]҄EǣsIk^Mkj bukPcw]-aYDڶ'm/}qCD1'FH)=>>{^zi5s~Yu]Ǜ}J"1{[RPG3"3'u p8X nϹv](,׶i/^iL* ?<<TBJRJZԧުݕ!k-B?6-d$FBLE)vjJ)og9lV48OS)y:0p77ۯrJJ/~!ny:k?~v^,S޾7TsoC?7T mSJP+\eZGrΌCKt:2W J)a蘓QZyZ&RqT(9綆1:䢝QF[B ӥM}/4[FU뜘j@' \RJ% @b !2,kltBkb3K&}Rf\~sb.{ou]n֤XQt~M"E@JUtΙKM9,JۡlED,%RZ<Zk27v<"*1nhcL bmtZx]hYu uց"012W}!|^&@]]U[,BB Rj6ZN `fbq R"-www),"@)f[jDRU9AK*\Zk&„ҟn+HsV+elGaj-=-RJ6.n)Ųm<ͽ'@J%T*SXr眭aU+ĸ!Zal{C!l]{a :/$5?I{4P6ZҺ9l  5Hͥ RS܌JkE巴x֨UeF8ejJ9ˆuH)@Wr9Ase۶nNΙyjϧWW_|%\7O?WۻWw0rx9˿z:^Dq|z>_慹yW_q:Ւ;oRm[uf]ҟ$\YR$Jv]f))/RTJڴ*$)2M :csLRX!6Gm>7B?, 23Ve [*H:O"UK5҇{IZT,Ik͜k ͡o)\/߃~:;k֔K5: 6,]cȴq+H)X@}QD͉a H6"9M"r'"9lavw1n-NAXEHT[9SkmאV\Eۧ"2smo@AlbV"uYb]9x1nTs-pE+96bAu|>{u+y-qumm1#"Fii u]uZf۶={gq1y6U֬|U}2ΦRbJaj"-֚(:\M7@ZWRi]WTh&V~"Xk9yY 2e"m^SExZ !nKsZu7a].3":+H0-[>ƘJ/3}Y˲,0+='Z1}k `4refnZak4Zm[[qs#s !us$"%tuNOi-"nt qW _?w7_}o*U}Zouy˷(0Ԛ[30)%c" 3/F`SJ| 3OPIIfHsD9{cN`hos.mgﯳ*9Z-wey]/˺nlRR$v12sNu G]SaTqcJAa$BFc@2֨fԤ-M)_|߯c 4M77"NeYq޵g^6$RZc׎ p!GutuœC,یݫ!4`{6/. Y w]B1Fn a[{tz^psǁݾk}7tB7o,Vwln˧NiHZ\^)KLmo9=?kcU:eAn7t]9G~L:wc?uJR{AX"F$JD!ǔ;ߏyRqVJؠjavaKN+A;~w˗/r1nwy{ś7^z[8oovu1ƼR_yX5t#)וr9DTuegX&URtEMp{]Llm&9w:RJ{ H*xG) ?I{&,"cJ{k!~6ʞʹDZ"JXezDy۶uD(4[""~k0UDnynP%sfBf,$m_|ьHZ4(9xJZf[~ZK QjFUeYRMsDDnv}㶞Ka\.̅PQYkJYipPsfeYZRJ\'aDL4F0_ԤƮ[ƫ&EDwJi:жҋ/mmcu]Xv8}v]g@qhtZurR㺑šk:DlƿxcA9'E HH)L3DF)\JZ{8Iiݲ!m54 v|zkv55׍cgga"IZ&BDmI==<2HR"Q9m%&r[@m OƸ)?"]khߝPsk QJ2Bm[-r!p9ip|R۶u%9kmPi5Vc,wpcF!ԵR @[cRN1C I# b7Oyo 8o.9y}5~ׯ_mۖmJݮcU\rئoוz4}kZ C)$TĹ,ԂƘ*xwwJDRn:2ڐJ) *FTJim#>>:bspw[ KooM*R&HZàus,sz9,HЎATjE31sKW-^D7&!"䜹kM0Ki^A{ )mnZsH[͹E& !1VUЌhQb )ώӆjӹpM4$o,*YkM;uJ)j͵01öy^9RpAĜ8oPy!!98Ɣ% '?\yaH941947{d\s&s}~~U*suX5BV{ZͤaQJk طy#Xd4_at#smU.)AJέ [Ac"@3e5Qݾ| 5ZV@sơw]NDw?4~*%NTJٶB@Ac073O?yi䈶,K)v/"{.Rq[E uU*)=<<)FA-_A:o)%tUº5yRQDfS5"KXbfBRJ\u<-s~8ûRI1DH"MEBpGeLV!P(]K"MNhQJ97GDYv{C!rXfc)ָwAbgƨ5/.ۺ۲mu뜵v&,2túϏڔ$]QDaZi.ZyoOh4Qؗ_2D0sW7HUJnƹ<RZfuǏݰٷWRXl I)캱RJyN zy>xcn? 7;cRw il0xX QwM޼ySJn8޽K)" W?il?nDrʎ^}mYIٗoޜS ZL cFeN)X" k̕Oql!bmk˲Xmc?攬H]?6 6":?=7& q V}7Jy[Z%EYi28J)RJɩ4(ePXpY'@v{B\N4MK SUJp:aas?\7p8=Ҽu]7gYoooϾ[t8(274?==Mv}'"q"l[ '{fp9r1^k^/emHkb%l]_:dܻN֟\bU6F Q맇{km-W(p$Bx~~֎XSUD\ijeۡsӴ~|8*DH.{.su"XR>6Pz윿΍Pw0t"(>||E1iMYR6CH߿??RPkiyZ9˨G<(R!ֺNs 5aXeʚVf&dǵZK(m2*Eb?GYDiCm[RͬLtBَ=1uO b ' w+W-zYr9Vιn?r=0몗R85< #ye6h(s%臵d,Ӭ%-vZj [.De5mY.N53s1b)e K0 @Bq`%aRƥ:ߝE9"lm[lZumu ڪR<̗Baʲltݶm*4,-FubȌƶ=moq2.9z묵/ӥhRDPZkѵJ C?GrjsiEdWJ r9WTn9eY}+ZSCHdqntcRR%z%yeZ4(sQ-^臾2lظfXPS\6-u]ۈYj0<<~_}t,`+ݲڗj@+Z4vY|86!5WL<< #јȵ{p&n|"-nDvy]~?|~~nhphn%rֱG뷛8dH%$C59&.ڦq*u4܆0H6sjOk}<Y:_\.{/9WA ѾK 6  T)\RvF|\ ޯ˒YT9C"^\kͥ:[\s*\Jcawu !jjƘ*mD@sֺhKe)rS*DZ7*=qSXSJ"Kl-BV"aW2jը8X.s]Xk9Fkk,JwQ5I7#0eᚮ.mM)Vm,m[Rga"圡4=ڔRֺ̩FĥVReffШֵ\ quREXw"RJ9@]*+ V秧?fs)B2ֺ}ki[bn" }JT ZewDy Zs9FɵƐaB1X賳omR 85b;9c,˲ůPj%M0׆KKQ9W5׸2cw^)WJ&\sv}qvhMRj)&)dm]g{?gKfUYWSc,5g fk"mH)AZk,j2jl3Ka׏c*5!ڗJ9s>9܍>\ 5Z׵TK;?|>;g|;iQRC7 2rSAYc;HY5R).M~)ۛ9`񽳊2XҊkgqeqcф t?<8dlȭ9hޛEy ;rΙƼUp)ETmMK !ߙ8c֜K.h-o2ʩa1ymO15 tR!?K>?1J;*%0;aDMTW8Hw(.ֺ9燞fLyMF!F/Z<_ڲΦ)%UijV1nJfnpRTUbl̟$%C {-<"t]wss,w{0J_ ItSCε B!iM(.D]S-^.Av( > +*-gPDU&,Zᜫ(Y. *;VEk^ֵZ.%,۲ΗS,QKZp8Xm3>-#u]sU'.VVRjۥ`&d1cJPu\d[˖s17B^J.˲4e2(μyW Ӳ`])_[(hefoR1Td唚ZYZ/+k}+k+ "E'9z2sI,۲՟R:ϗqrtqC@&u [m6@<5P㝯Ҡa0-ak| ,@D}/ UimBc m|>3QY:k99l[U)[l{V7uRQj)rJ)tεY6l Qkff/UkDp[ms]'b~7 3J> ZkU˼*DMu]CGWenKdE̓"J*qm1*m41C,,[Qci$6p?>`Ԥ[rT>t+LE0@:oLy.9Zgy73[@SCHeٶ-nADH;YPj̀y¥0TJ9D4_i~u]o_ܭ̻ZយַϧS)E)<cLrQF29x{5 c<.< )1m\m]y(tHC>-h-ĥ6ͱ"u1kBKK/psHjdSf5ZbJSSߍ>ƘC ;[ҏrb͋jG 9'y=1mcx\)%@VF+hȋ7DD923Rj]z^e[5 c\Z#2Ud6(kmefM3Cm[jX7T?xf9 yotFo[|~|b_~]WVv$T12/ݻR6HR8U ;LSJZF)7%P1li Nݰ%j,2ϥr-)?R1Fps$X RE&Zyjz0Q)U)9W[0P(K)Z9cΥ&k+`)C7hmMxkqT֞?A|Z EXAvfL)19!\3$B85ƼmQ)r~|M`DEQXr֔k"n?P:0TĜ 0`RBlku-b "nsi/9sU*PΩ:@!(\A HY4XemCFbm5kt@RIͦeZ#9{۶ve?md<ۇpham#[{ncw=RkL@IӾȌ_i~?K*AL)o]?"^+s>M Z} EvbMoo!|k :J"r5zXT\c639g㝵jY2۶)eYݾzh\/rCD QcRxE:s}J)YVH`^"WrKu DD\5u(3{I7{ ݲN fd\sk:P=1 {U#]J[r V"eޓ}?u֚^v=zxlJ2jO Coa۶ͫTÃ1jovaYp8Z6Sc /B1v{dnT5?Jߖk-<*]J 91 W9iVjRcs)Tv۶5F, w9jADgM iLTr溿=DֵehksFi|JyXw]ט’RZ+uRsRf&Qj+R%_M)EaR5ߕ<5.':@m[r.,(3Z%҄s͙_JcN۲N\<a0ƈs& R\1rZ3VѷAWh=EJRx>ZP{ ]@ 1VmL?G2BFhNA/nu[Wm[ufA|9Ruε\ j]GD )C[5\m|0]u]u|RΙChZ y۔w9734eYr#n8;@n"QB > UERR"}?ɌSږ +HZ+*QJMWK&Z+cDDiFS)k)rQJ @cGU#V̈P u]ʙ 3Ԕ)4-\RN23B{eK+bS u !Rb*@~ZsNl1Z&˥C&!mWk~ڠOyK|$Rf\(TJf.*zOjL85; <ϙ<[J&y^cDj<-67} $iYi,VdY:,9@j֚u`@\*#TʂJ1(ЏCwUJ\T2e|Uj۽k K*"Жtŀp已t]m1sں8Qk3Ҿ (DZҷTqCTJ\Vnj>)q)*pe&)OZuNhM/ jUJAFMZ۸Rj bn6D156u%nZ}g2]xb@0t)mYhďs85p.Z{B}w8j)86\.!EaECߵ8.nԒs;B5R-X*SV}5{\-ŽZ;ϫ25亰V ~sFE?~8/Ff\rI0CJij+49 nB󌨜rK)Djv4Ԝ?eր!yaH) 4ik,;SkJs!TFmE:SsimvuQ Ak)-U! QitMO)AAPkFb"⽝l9d}ժEu];æ֍~j:}0Vuk3qc+P˸-ag{mM+7̵rX Tw42X0_ %*~y( ‡nr-9U":+nk4[e&kkK)$dnK}7@H).@%)%by7c1w]Q*收͜sIQZE?lQo|@nRa e Tj}:47WAH!q. ^W{J):e4hRdMH~~Y3s1JY =4cCRsih]mVe7n{`?kJ ld9ԫ_/z*sAm_#*T$^U1cGfoSڶ-:{>].F%\f֊vw|֜cgs"ry9}{IskaO>%!E+vRJ-xn 0harYܡZkއq"۫/}@p\G60GU^}'g9`3y.%Mq`Ju ~u]ks?o,5jz!j NSQ]˲w-nNYrޣʇϫC{Y]E\ E ɇ0fv| \"<ΞeY^>e˯)u]']Kf@9I"g]˗/n !ZtcrϑyvZaYfpD۱!]B58?3*:[+(`1F@ 5S\"03N!O|8J11] $Ї W[de)}UUi BD yIzB !֝y }#=xLs9,~ejy=ߏ"ΟJ<9K$uc`m~C|fq%W@]D\/ZP̾x*믿|]M ^_b qoo߾O\qRzy{}ak>v޻5zvH))6ppq8O Қ0cV}Gp, Ǩ u !Ҧunq1&}^^/*H%D>o;3_Q,mB.(Xk~6M<_Z1fSׯ& ^וP{//KJ_??K>}"pחb˲|lfiE11iBLÖUƵ?20pZfaIj6-봼Ge̷hחk|wfu40<{]~~ 1n/*~?Bjpnt6hSYPuN{! :҃CqKG !{kdz5ƘFe4+e(++M<:|ZJW$"$"OQ93ɝ񟱂=o`D$@ZvJ))DO< lᣡ^?y߿WfBR*R\0Ą\@uw%\bTpMַCD~V+" 4j_D! " vyOPd>{?JBj9RQK)ޙ uCXk]U~ +H-Gvj*CG3][k|\̹dx#efir~oO:yg fsέ֓:j`9<(}3\DrƬ4~QlĵCZ̜R_4X""C\p4Zmyjy?lp5sFX۰ׯ̣qRٟ:QLsVޙAӺ/fZkiL~ά"x&HȜs@̼mZ׵^l>t(6L!h"" 7!)3!`` `\s?(K)e[RA9(v>L5ouy )ƔR"42 "*!* 朷Ǯ4j{Ǹ#Q ,8,V%|I x?{Vf}{2%]jv>c˄>|n7DOtx墲V*N7J!-5CވȵVlޥ9^n0R^o/qcsvyCHD)|nD*DDl?1gwJ sJ*![/Po{߫b#ݷMUӒh*vJMRk͗zc[e7SS#7D镀@C.Yh8&tcyh%/1FwhqiFm2qr2z$sסi ToR>Z>tad!"r &ض<:HcqA+"Gι۳F9eiҜ 8Zi#:Z b8D@E)kr>6RMK/<9[.zefO\FraJ9KasN^^^}:{t\j1(8gG 97xR+@E (y?{.(N){m5,~i!"u!VorQsC| D\J̸8G!%RL \'s>23t^g0O?|nz5[>}HbJz݉nag/Y£2s ʽGݚ4٥\| VP51Lл\X-ޕ#y!QUc`t*'1{kC{$җeIa .spz%~0ϋ=~t\|R{[%1Fr0de?_A&c9~8Ց\>}εѵ z\JzJ!~uy^YLf^.sAqJIIJ4-ιlz'DMT);sn6f&r &5b2\;BpE0ZXUm1;qs- ;ޟy VEExV@ [ʾ?NGg^NRnz}i@H)EZ Y.Zy]b &s͑xBfv:({|fm[Hs:x,#?zUTtZk( c pB[rY?#[krUdnzmǶTUA$MCdJjqBH5eTrY"cYm"bER]^/q|P geZk)şJ=c 384r)ڹ񺮏hȻ󗗗˞߿oۦ@%gߗyN/˒RZ~|eyyd>e8R`#^hyy"xr]kz߾|CҪ O,3+??bzGn>q.?t,16qɕLe? @ι;̈/2|Y21H⣆Q!mcƬle}s@B:N> ̓j w;m6`ufX:!* شe6޺Cc/V~~wrɭMD1AUQ,}8QUG1=fiL|u]ǐ,ι\}O4؄3cYIʨ>f29[ƽfՓ{L3 9jje—/_챿3:bmx#{>rBYzeɞFM-sr}y 1Nݖy Tzrٶ֒R0-(""R*9peu83b]̯!Riѧsm&={c՜HDZF:lZ}ctUrέ}ߏ ׄ%8[.Yl&U+K9TDDX:,GM@};kD9z2$D6 8c]לJ7m1nCUbl ls>qoowl-a!EMLGvF2[k;PUG(q^ Pk1L ^@p1NS=0ZR2wfyRs&֞\UU?Q5` \Ϟ| 3KDgIP<%> "ރH@ Sc# ށ6E<ϕHEA[hr| -KŷGr晹ף3Np "HVsCُ֚'Jѝ"BO朙:A8Ӓ8/ʠ9WN|TqAr<7A4.4߾6gĠ1E#H.9fa DǾ뷷GI9$ `V" aIz1u%Dr>E`1>{C8L9j)x)cVɍ575CUE4.cӶizOߟά V2""wVDLӴߎJSŗl简:ib>6نC ~Zy^GE83*~dhιM΢{xJ^e :/ 4UDЁ*܄YD$vwCnLx0]_noCz֚^g-P!s7R"lہ 4ǽd{&R !2z&|]\HID@` 97Oq)qJ=JkF/ābfVzUpӨ./yPoF )^rH*ɻB/6 o{3PU]VzHc44M']Y}(4@kM]\]Y(QcfBe^ꜦBDF]۰PndOx AX ˲iڶmז4r>,׷>}6])z.u.H[Dޟϧ NP0d9 \k0`!#SDė;K),ݚ=#Cguw{?▹h_R~eg)>-u) 'C5D/Cx ].ۧOr.4"sy~K_V2㾛Kqu< |RhF8c ĘR>DRkZk3z !䤵yw|OSD; S``8#Ccȥ":`>R*xn R(oN߼H^Z.JxrYs@P*_|.j˥rJGg3yLzVZkx8JΙ12s3#<ZG]rPeY>x;#i]RO[.Kۭ"qJ>ѥӏQe)-V0iĘut7G-YN7-˴M2}.YrDQق8(O/W:RʶmV7>z=oׂ} l$c@˲*N='U̹8<+AaenBYszq6)ZZ/"f'}{Rhwy;,˲N#bл0I.]l#2[@u@Yjˢ&ɟ~{_b甦٧o:!cvQzsCD|9KWJI)D{y/~Q>6R "b訍.}v@֊'[!\q?”΅Dj TQRJӔZ> @e1F|F2'OP}T T} ra+{X"{ޮ$pr$Vpއy1:.ءsB8C#)1Yk0eyN|R={vxOC3!1ڜG ف fny3Cfr<"UceŨ݈Q,,X:y/"k8)0/;}")2Y<`mvm4<;Lj`⃗~37A5:*8r]irM]dm9g1֜R9eF5xJv1"b/q~b \këɇZkf Dr>O1Bm5DKnx{H߽Jc ~谬9Bگx?]J {P3EufԆ{Գ5f0ck[=<_VCxm^.O\o1c X\Ny&c4"f-U[8Z*n.^/t){cꑷ)Dh0Z>ަٺr_kywFua~KS ,۶*>c)54 }]d:*$t){)Y *ϟ=RJ! m[e͵e~{놨8GCs΍wzKUD#x?M<;m"=>۶=yQm۷m׷W;D-x!E@UY_*sګ)BD5w^Ⱦ{A0Z ,f9YPV0U睟 QŽ;EG-Vt,9׹FTAQwQCup僈| Umaa.a8_i~st i{۶#~Rk1NgI߾!~^WfUDg,KrYf~^k}s)eiD§ODuY\z`I|n֨dv snYWoX'ݲĚK<ϭUXګˡ^JSr @<@|lBPz-MjQXrkmLt\G~5qΕ'ZPI{yy1X֚^Qߍ'S|7~{Q4M1Ni%g9{MDdaD VP%cqN:aW)l$Z,2:~øѷo߬#$9 V(vZRsnIӺ!et )8dM;q]Ump42sVmS䍌N9Q8Iurzŋ~ &Hh1Z) P|AFk」щTe8IqFM AĹ?c$o}7Dd>G>RJөF OwHiC$N(0ۨu!]rX[Gw#Qr휦đHi䋅cy!:&ƐJr]b$iIJ{cf!3]У:b@eJEE`R;oɅic0%U%{59|޸x{𣺉ϗEUy/e cp`&_7v=O^rc%M)f"?J_W}5C8r%J(Ū4#D!:3r0a%Qw6`ޓBJDĵ~tmI;1(\ޚG2p1Fm`cRC<ǔFھ o4M>MGKqrwiY[r 2FWep9f1Ƶer^W P18iVAd)ˆ[3X0y!"x|&YX6sS> "T9 iB@:7 t]V3WwV3K' :M;Uf[EOnx%Pժ &+Y߃j) 6fScB=^_ֿp!Ci,eUUAp]ܩ!46d.%h$#co)@uTEq2u]SJ9?Ϸ@n]לûs"2X`Ϫzs6sw"O6Op";6yޚAmH.:W`9gagKޓ7J9UQ'R2 y34_1Bi ,}6E!6{FG8cS@W qw]J3e)c 1@&D>^bU-R9?o\1)":fGFDMlr_\}C,2'DQ5 <p)9e߸7ټbw{7;UYoWU#xOCtU26"|!\?mC7MpV kkm~۾+5/_fh^rܟ{f]O!o֚^K.dFD?tbg 1Nsj)s9-DӔ[^P=2Zѻ "E 6@t1M%7۽pnz_YX ~pʠCFpat&{r.(BٶqJ PֿTx~֋Or[SJd4O2rw2!2hCp(ն{yezeQl#- ח؆<˚'6Ox2zӦ"¹\bcl kiync\n_TQ`3b?6zq]c8| r y3:/V6c)²,x03^.UO kZAv3}sn<~g21R4՟~z}iZ.}ϟ8ڞ)vZ=nV[ι1f ! ajJ)ۼ\X%zRP\1PY/B.mNUD[k[SJ4Uܷ.˻ׯH=͟H)ǠGD$j1FQcǷ*+")"?#1_V3i! !K J4qiS~6zC1NW)wih1ͦ؝zyZf 71)瑐"Qt: 1p$f?4Ml9l3[|C"98\0U! @\n[Ǿ[k%"#Ƹ壖Rv sg>:j4RU )~{;_xG eYTΠO'""8ʑSsnm0wzl~iQ}0!pk}B9`88һёh\Jێ:;Cp6/ɚ}0ܛNnck!iRp8"`z9p^J|P!S${}_jp6++.zkmL) \t)-"=ir>NZoreHv:~wyΙPN0Ĕ~6̠UC`o_,p]*dIDAT] "YUsٟ:γ?"oğH;8 0iۼL_iR֚Oa(YeVU"wC˥0kuXg ~Sp!.NR44iވ4{̖?Qc: 1,Q{/AEc#V@8i-SSzރ ИRG몘s Nks-h\o! @+uaƩ\r dv"`U I"Z z5kL41 FogMG4sVHoҀm4 p^6Z>ʴ̽ cfy^]q/aAư޸2_z?s0#wL2.,˒[n9ч^jyuY}}z<9g+{ŏ)EED\/69@}f\,M)T}VeGƶ|E`q^.IdL} 1ibsnqQzmM yk_iSO?pDtn~].mmR>OtrsO?><J+uK1|߾j1T ;e\қI授Rq噟/7;B"Z1j>#Vpν~qC b򋚋28Rה{G ca8)|8̛b3?1") :)Mʢ8R#Bk5އFJ)!:8u[%:Wrmfƀ hJ^{?P߿ReSGf(0K)v OaڶM___M)y]RLrϵ.A^eRɻpzG03˯q1z[\om۶y_bYF_/s]U9Qix {)eg=gS @ǁ'8 {V_x؛.2fBQ9g:e8r^s޿a?PBP-mC-1e]Z;j1-:@hz?[܆Ce"kһ~ci3~ιPK!mJ0ZӤ".2 ZDpgEi޾}o'%&к )͵֖mV!477i1E#0M?jw"ꭼ@MmhKaxnU7U ^'{҅~~}=~?53Ԝ˱}S8-Nmߓw:KҾɼrԙyo"rX.3tC}!<-)< p.P|Nx(Qz γȷsB@ȪNd0fD4GUUƱ=)|#Nd&'" RZP@g03"Vvr\VlCr!j| !p`EtJIsğ>!uv/4[]Yd콧y"RR+9HыR:FCRJi%?7޽.AQݍ?," rf1)3~ mQ }ψ L`㟶ֶmdD}/˂k as{GTDZsȕV!v|~yqqv.lo$R8j@}iJ)ATcKcXk}{{e!@DO~K)fTfGZ/t{}ћ)jcyy7a䛛c]Wp1|ҲLSȝU%\!c4"ʻEqJyU-ŲgOBh{^Db Ee&qưeYg~VTAEp @ڇJù, n9|lNx1"5?*<Ȩ 3 38ͮZ36Wq+_cHk"g"*sa鈅VZD=S<4PΕ@z=@a  s%L"cH2!}*p#:Q&RpxѲK)x#%:Kjo!d،m)%{7\tzT8CN"!$iŜN 1 O})TyLb _lSLrt!q^b7nzچ8Z!y@rt+J)2)`~".ii\wBԡq\uZ(!CĔz uZ ۷oe{`cj) K3h,K˗/>x͇|o>}~1# =1)9$Yk]9&F#0-;FoJi v_4M<cvsShiJn9Zxd"^c$JD$w0޹fS>UTJ2O'v8ZrήG^ʩE$G1yqfR"uOD˴RM*(RP@Q=j:3iBiIV+'4$yۏZliJ)1esofp^y4Kuȟہ.)9xʍk2"֣w%ovvGuCd4O<:p G9 >6Á9, F.Tl־p Ay<]ӲZ偢s褏Hvr2wE;1:Prt $: t;fFG^!yvqQ}H)wN'pv X\)>1pƎ5|c3jg胧s*\!F")^.N6261džH^.{qZQdRpS۷O8B)΅Ίr1i6p|!{};Vqu] xn]J)e-RmbDcq^ZGB|>朝#v$a&V}ߧIۇ B`&>aMjeUdYo=p$Y{ygj/{~/,І%令ާi0}!8Yz= 4];kP#j u}yZ4Mo74Zϟ>b!.WV)࢈{޿^Y /:Mӻ"<G<]h'4i;0w@H/K=mMs;;_;]Ҫe(ϟ26NBu6~ܷO_`tUo)+\z . ݷ0x,䂑Ȱt^1N (!퍝ӧWёk"5*YJȔ)1>rt@Dn]3b/kHˬʹTc -y$h^*SwVC/1Dɇ#|{Zr̲+;>TہАT@CDGq 1\bH!sd*hتJιޝC[>I&Dqbq2ϳ"jdTDJŶi~z律 ^n Di.pzAaa>ȾRq5$O<:F, ]fc[0(yyze+ LRpȬb~szo =40jΠymoV\֭H !&KmUsQppT9\wj a*/U.xOܸ|m?J#32Lޜ:5iv 8YWIkcVYTs2TD7TOĭef EU"pY^[G$X{Cs#6bd:QΙHMV<)"׹+]IPqPvƃF(ƈk*=nzv0c \6d{:93j󿔃 3s/VU-g~y7=*Ez/N1Z2lMY۹u52mjДRkb[kj`˾ٌDQ1}{}5!B)b콏)E# 1 3"NL`נཟ~9gLDfGmFH,pt c&~}]y CB:L1مh[4MCc`3=m ,*0yeq.]48%B?I@2̥ >9O+Jy/;3/0YY![n磖sx cb@}7VZzrk6`ښsx\O5g]"p1ޟ}ǐ6-t:o7죝cֹwwI j.YdNZgo:Lr;::evnX1CP+Y9gO>`"d 2O6۷/)͈jmBpߟιO/&+ !VG9:nCWu 1F}lMi7P]  >Ff'm 'B^J1P;׃',߿_Cn/_^nooo{O'"ss.Ʃj_}D/\樋"C -tiB:=}SJ!f:3uYU˾;9NWH{m$"N۷}Fh>x]7Ɏ;~~c#Y0K}O&10%_nK1z4@?}<Du")߿RB߿ݵ'fYeخ?[|?2 "K/O1- KҔ֚^wt\8'2m ޿߉`;URl{Gt:]?y2/e ܧk!MAhb&6UDf1Fa!Eec2lnD؀r^1|c14`4& ")1jǔRmP(عП ȲJ]Hy4#5r!BzZ3[Du&6tfm~`&34^#yg)%A;"ś\lhvYZ):tNAD%B's$/CG:q@tۭcnޟ[]#ޑJ1Wf !oD0yNFM]e՗M9¸q61Ä["}{+8pޓnWUK)ۿ/CN\[Ij[͈7s_Ři g X܇s}CDK-06cM'X:ӯ߶>QΓ`4M'h `5Di#A%C&a -Z=QDZs 9J&ϟ?ύcZ/: T۷of6At:MUGJȨ>Z$Y4-63t2sGF Y#g#_לs9"c>*)#+BNGRZkvc 8lOW%8eb#x=8<1"mRv?f˲9 8x.c0CmS߳E3<2iC4y]Dtv۷O ol;=|<.ض͡VkU: f®*c!# vT!hчq}9Dޓ=[k'@ ;D \G5J y00Pje4U?CQЀ%4+*Tm!QQDZ, ew4xoAs7^;nS-hh]+zȝ ģ\U1p=eo9 o7Qz痗`!my{MsnĠAIW]DQVm:JicH1aRJ)* "zm>(b!™;Ϡ2ؾo7ZL.xm y?>wGN0Fplf2ߞVcRً' )=k۟4y<$^fOΆҁ¾?>8'~aݷȵS쒿yT"l߯rAZ"!xDRC "G,ǜ@|9 !$B?jloDfRZfor0(3EG>\ѻ1{ϽLVJQ6`|TA͎/sb>PsNh{j B1+)6%1e?uJ#oE; cI9pkmU%Zm(sZ p9]t4{|>WdfU"b9DT[k۶ٿrBt9dڽsdj+m]gS5eww~6BrԐ&LtQzG1Fm^>?Z1!%PbIL)vmJ)_||Zr{*"i{#1|>13l*dW8RJ)xcJɓSp zQakuh*PUˑ zc=J^mC8;ƈi!0C6,mCUs#L4y!yǑ]u>c\/"BLVk5"u4Mz1- ?7+bE;STeJ2לѹg&W#RJ 3:jpRf?M_73Vfާ9q:0E8xsw<Ϸ->х["l@wD>~}{Q=nܑTm&Dd#DNOf]tF-yُݖ[\.9Db}E, 8?Rm;BA4m@ y JKއbv\lV@b\NQ\UEp`l(Km83vh<<=E jǿQÚ,9JG'/F$u܇MŁ?Qؑw۾yc5bB魳Km~ȷr^z[eyޑ+L' s~',x{{Hoֆi;!_|Y0Ze;۾^-nH d.˲sߏ%rG6zc%$"nNYb)MC9ƨ(Ρ098UuVKzgJiY.('/ff}$ufJe~[B(cMUTD  Rg5?; ?Zwu9j;6;RJi'ιuO3SՒ+L4/1Z/m96Ddq ֚# |CxH`KJdOm3#lh^iF(\.gMl=I(M r4hMjV|'4rR.8s4q5 M>ٹ0d@m̎>$X/LMwkC@U9=*zKq>ObǙ;@Zz]?gx,9cwuf688GXDLzpN08NH`s7SU<=$)˺j_|L4O!8bVSkJSlbd`b g2ϧ1"Q=db(a|.+nUPUPxߧZkGr(?v#8eT|{myc5u&5~yݞRJ]Lgfsye>VQV꠮ 2؇@|.ι6sh u}GO^9:Z)`_t%NS6TƞiC9{MJC}ج֬*Zku e n.*(e QU;$ݶ\"9w]U D;28n)C{DRR!&PNiyy< Ș鲬mԜqo!^Rf'G ۣNDe/sO;)Xg#Skkw+@mxJBzl{6D  l}9OGDԯ뺮~R:?R%Cb:$FW0ّADl4^vn7snj1fq,q>-|ծGNK ArDHd4M֚ ÇjRj֎})I},k^1Msx\|mGcCzb2t9|x>i2޳-BKT^i }|hbJoXk.{=Rl)-$hMUs1Ng-27-,)6FUjovPAp~Ú ]-5URz}}OӴm[kIC} #?RKb) $`2p RKv#P҉P`ywއڱAۛཏi$UEB Ja_{r"xlF)!r598*N|"bL<uQ;kx %荙E2`UA`+}+^_ hp]^|>Aȗ_~EJ0++"{xcWB sӾ.{w{>_ሐZK1rl1p\)R " l?cJ\j:%O)QpξѶmk˜;cg.Go;" 5:!Qn1̞|٤{G>S2gj;T|:ZJ 6YՏ!׺":8sPMg.֭!F/,1y"rjqtiq88O3#ڵVGB@G"28}=Q@o}shʥwj'e>}9m& ):b_`߿R}Ck[U>3n9 U'e^Fjȴ{߿{$!"cN^QwQbj~ئdo???Gw"Aa>qwuD@ ,{|/~_ u7y^F^;fSDȡ@>2A20\IISe#UNPkc+%@STp0JU[[sVUMo%Xƶ(wߍ+@&"a/&"czZ`m69v&]r0h}u{rTB=^?{225K7S_> ~tg^1U5= O75]~{9˱7oV]߽{WEJx~23{1rж{1E¼fPٖ-<%yEtQYIL>ךSVSIqj,m ;Rׅ l,MW@&i=nrT DDP3W̞Hk+ K}]tc]1v,mz]?~m}#Hlw%UE.c}{ndUU63fjm66X ]K Eڃ-Kg]]CU-fB;"IncdЖYj}[fm}.DÇI㦋m۶`f, cD~[΃8Z7jof^/`Cfvz10vj=i]Jf.KC1:^HD=mzFtAFI [k7߽?;Q ?; WU$.NKٗryoM,V(%_^&̧ );t !AϘ'G-r>W) \pӖl}\I@> SHDSi1֧g"}}T˗Ek?>9JS)PG lt]&Z̄*ά:<8#bFH%=\eU'&:DPǏӺ1gVSbdžLbF{!ɌڽN"FD}d뇻3}1DB+n2jjR2hê(r_vSDw#-R2aTU%c.[RFd1vx-TP 55GqÍ܅x{_>Oh36R%.QJDe" }RUq(Eھm @@) O沬 Pɕ9 ҹt{Y{S[*ض02a0gLüx138I8M[ LYi.JVW~~WD|,? ,"nwdNDdi\if6HX[+j ys?]Q\bԅE^/SNUG+""<}grDt+n,u-fQ*-ݍ{xXqnڇxʡ[P[/-*dGB3p}i"87ZOt2Ӈq׋","O:jTg>IS 3Tu~z䇗sx+IT\Qҡz]PgX }z*}P;rYTV+(Bմ]3rf1/ˑ6ڈ-*fffAD޽?M."ZSPՆ𺮅{)T\8]Eݬ/M;^ikd1/3+\Pgƍy=/7,iawpv$Fr]4୵1⺬̎DA,kr-2"leӽ〘YQo{8 Ȍ岪,x}קUU'cA3Tfo@@D˺Xg\^朝L+/UCsmS||<rZ>J=F43# ;#efDa{5sZ!̴T7m?5|edJ{$3>Ilj]!dRdƲl͛7˔RU- eY8F4"wSuY W\Kd}7$<౮}G:uu)hkfAH𲰪* ^I&,"O(J`QXNK{%H`i#{(û)VTV"cF݈P{٘i(*pwʩ$af@¥)DQ۲w Kow`~ƭr"ҡ[swwcYeYDuJ$me[d6o/wv @mzx=,2^gLy$*yX61gn9 .EfR Ϥh!"w^{U`>Ƕu"c0}30w(GDRz9JP:F|"Xwq) (#"U,>'^"By pwf_fff̮<IZ3LcۿahU}Oh e3 $PNf"}7e9 'ѭ( ϖD1Rl-S6OhzCHg}ӥ5]+QҖxzmMJx/IoGRUIwRm˒D)jDDI{U3Gf$`fg$&fasuR/#`̼j%XA2 06'Գ$AHûYYf!bR k!KR23w5!ó3G 1'ܬYJa)Qu)E$ѲnH$Huˈ̸"BFdWq̠J:K,3E/ zI^qI4|*J(K90jJ8 QsjffUT-O񴛑ٌl"EU_#9Υ,!ULfh5IH镦@eJ,J\U-c=eiuLe(m7@48YiYlorvGeJ_j`h̶#c&T5̌i/Z2]##Ehl XO̥֚1$cZ2m5"kSs:Da_j3M02%hn#gpE~D?]~{@ܑg=R"K9AqT?CWڭwoMNd=G*.AskUgIy\Tgk[Ҽ]UIf &1Sm`RUM'lܴv7Q=΁^ă _}ݽNz2઺Kr=3Eu@>fv/Ϊ93OԺs9ˉSAu`XFY{G̸8~m'lI|Qh0#}|=f30}m>vżB LVd 3rYm s:4p ia25ZfnEe~\щ3%|k a '&ϗYEm$?>jpX`w]d}\/U,G2/rwҦ1lQ&?X?1%W;fRPQ=EN 1+$(0n3 "kok++*`*-y,Rq@-"Djp%1Ffeqxxe۸ LyL0 #¦:SUD] 8$a]gP"1&s%U~ieߑUS*<(}/gzޗoefG()tX[ffΏ}>|y4g՜z!$2|f4M:\Ce@U=~Ui :ED*S\^m 9tY*%^#99_ȁKD:- O CaM=ũq.ׅ}Oo?YTDt6G(>V#C|!"Z L8"G]y86r+ #Ƥ tzdTSj:,W~`L*0ev*,8(7ϭg˺SYDTi+}{~}aRh1~yӆ4a /!,iAJj!=гl,:*f#y={4dg>i}23D$Wy>շT`fNancQ e,@8A8>U.GKSF#0hv2(7a-1J/of1&7\|ghU{ʜ5Yl%4qIx#bx:뜩qBGfˉQ9b8Iw+#R3R,,`9s9klDd&q&i2W]A:msJuc- ?p:Zhvx2+<Ȝ-8G~7\TAH,˪۷m6[ڏw 7w6Cc J?/"՗x*$c$͘)"j%QY׵ A>GU5$*{XGON=B7'1цqn3o6ȔfIDWZY# gQT)ֽVbp5Y!fKu#8lkꡪ2lD$ fytLj")ʕ)"{n%hdi"riK%`4mY< nN \%{'Ӧ|̳ 4>21= RUbW1Tuk[ =G_?&|ƠJl [6cfy3 ó µ<^ ErNZr ">^Oz%2H,CaN )\vG_\/]f3Ӷ=u9\^>~xyzKS]vY6طMDnbٮ^tZrUe}^s2˧c U[MV9,fhUUz{|͛ͺsSgfz)J } ,tyUP !6l_/v:b3-ܼ3WRcYD ݪo6R5ț̫R0 `4;hj_N!DI QjFPegDTߟ ^Raf;0 JJoµh'i&"z_O> nx_:[kˢ[^wfȖȂ*}}WMUFx%#1ˑ[%&(2[^W[\4˲0LFć/r'@YRtV܉i,B{t~'xME{yjbn+㏏7`mdfУ+4;IJsۜT= Sw>L.\`nxfqй׬lt&#BAUJ}txkj"f&dG2CD*133D=F&@8"z`5⠃6><<䘙gb"L,E8ݔ8zdwpGI ?Vקәx?T:0 j"d5 A8Gn,H Ӽ2%% Z,qN3,1 9'./S5(#v8W"uxL!wd":?1|QM0.IIAninQ_=W]>~ݿf1۶m6y 7eD HpnJHr3\q!CE$;XOf(FX8sq3?==u.@dӏߧz^Uu4,v+Гzi0>7.ַJD|+<Y(3WsJ7a1̢KbbmSœҭW"DVIUEgJ LW `XRY2T!IQ؉L9Gh"̓Mh"(T r r3Mqxzzᆿ"Q5z{T,5u)f{O@8OT=SED5,,`**ebp$*"|ZCy,QfLG>T _T$Js)%8$diyLՊ$pH&&H\ATݞHx*1̺CT2>@ĉ=F!9IUuںc3GP3sw+2 imw ^~[uwo7O*™ܕRvH"&^ay/h*(gD2 Iqc{NmLp&>?Dw9o,f&*ѮY Q;CY<#&pj0cj V SrR`$Ap&5+<>!r}sJPXY,̜=m=SGp9ݗֈ9#mT=".wgbYZ1edƬģo/s48/Je x8ꈫba,<Ù{ 0/OnjNvUb("SmTE\*Dijhek{M1eBTvK5y/e*9)G#MD6)4rPƃ}<QyY1eIcA6 y}FtwzJ秶,ڸ鵵+-9 (q|;SU swυ,K (8A~TL^BDi#N+GTNMY_PbGҚ S///23Ӌ%xXQZ~$()`{D3^JQ֖[B884qL 8efJD!0s,WH"Kިc]W#M2!"k:?6g ;%~!7I7OELsNL6WfR#]!BAxHdga5%DUmRsYG"dgPVq"u'U!vs?*DY~]'(%:9}H"z#/Rv>ޭei|yҤ4')yd 7[k",m1r] AG)*q܊ReKy&%4-if(LII}t"pv4"LORbc/E.^#D(Z6I#JS k[z]KiR@&/mɜGiuYpw;j N>NU,Z""aw'3knVr%4`ȇFL0 xw//%̓ၤqւ=UF4 /Q #0UvT",Z$"-´εU.HfyzG?F|se(??{nrYO~Yumo߾}kfҖ//=-F[Ch P$oz4~ ǩG߶GBWϦ|󕯍Ə0{w{8Ŀ~'_ O?w=wT9`="<#.W1Uv yBU5 KMO5fT׫ZH3}}M:Hi>25cH* ֚t3OK(+-z1LD3 cCK͛7ۭA(c!+.zM_ҿɕwۜ?2ntI~,y tOv<,C3O0|QX)"BjE"zڶm}Dt}&F|7]>W}),2y8Qڨ<|PpfdC>r>Kg O Kឥkf5veZ1OOofƕ-*p4aT3PGmm!Ŀc>^ϕ@~[x踐_O]VhCQgcy|}shKߋ/OswT23R 'Dx"%ɳQw."\Dvl/%Ԧ=|^\.:jo(yY呹<2#ˇ?HQm-3 "}f\Mr˚3$Tx@fQ_0nSz|p*"in [T)ee.I?suPa4mָpC/GH'*Ř͌[kb*VAzmuçWi}6Oȯ#W7É_S_U.緫I}_%{  `z=B_~q Z(Gݯ~GH/X(F(*$auuS28ݓ=i^ +ّhƶUYw7q;?+%h1^?~/Ztx O?Kץ ZPU w)jBE.o.>JjY8NEL Jbl+}O?yFZ']m /M7C/?g__ϵ]?1h [5hD UKyGE*B|9[Z=3,rv};;Y_?&ћ'.gCLT rIENDB`././@LongLink0000644000000000000000000000015300000000000011602 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_glsandbox/net.dengine.test.glsandbox.pack/models/iron_grill.pngdoomsday-stable-1.15.7/doomsday/tests/test_glsandbox/net.dengine.test.glsandbox.pack/models/iron_gri0000664000175000017500000017772612641367671033311 0ustar jaakkojaakkoPNG  IHDR\rfbKGDIDATxڴYmI߇bX9y3rꮦmɰj؆ aɖ, X HE˂G%IdfA$,(XՕUyNg^c ~uNVuu+DߊX"wcYն:$E`;0`|hDKdiLxcAIT r$U?Qx&0LDB q%%:sXcvAy+@@ 06s` h CaL&ж-Y+˫`)0>}z淿,w)`Ǔx qe 1O?j1"R$B_wt]飷v %W,rr{go@||FUUjΧԛkƧzSd2MӐ)'nqҕKv=Ikn?H "wDJ1)Gxsxŗ;ٙIBJ E0Gs8߿)%z9 hŎl:d4Z;:>|?}u(Yl`4,F|紝Apl6"e4/+lDbzvwpQVl;5Qà(8a߿ߏOR<kIWCOg05O׵/SP5itcd>b\c<(X׸#wli )%eU4-APu#,5EɃH)ɦ%(H>Z)uMB'k,=~ ۛYJ$q(`C/JHwȇ^ypi>d\bI]7dYF4k^,wzPnnV֚Śx1W/!`8,9oG9R\.I7l6F7:xBM 8V+NO^ G!*Q.@m۲8c ÷XNu˒QgGᛓ<}zoyOv<~:;akS?4MK86 Cч8%eP-hkCry&zzS)ׯm{p{~O&"C=\087t:E)Ig(HlY>=B j$9y&]1Qa 9@I}9seX%RI>($eU)Zx "t۶M#?mt3@pƀ1ZHEJU$ d‹O@> ,$<ZRl_ MRK8$$|;oKCE8(IP"`|Ph}1u]c!p RmI3Ʉjb]unEkQ 577|z!D@&B)нiۣ Tn=m!Mס$I֊jBHU%1!ޔ/͎1n7uc?Innu!BkIHxEݻΡFm.`BU< $XcAG8"ߕW4mGg yќAR%aQ@؀Zk_|kBpjhSʪBKu'rDQD۶t&xttr>l`0U#l@B01GW>C)p~xxz4cWt]d8"2(WpΓ)E~wքzpt8e{#@>,Ҵ>dΐQBܺ?!Dk mg;CGGǴmdL'<:}д-!N|'G14=F$rs`8[kY\|N2㚚, %9.@)E䫣܀$Ih64_0:yȃՊ 4ưzcTųy<8;9a8R.^Qz}y- 2 n_a:^o0G"de)JIR 8N%/գ"*pu%d}<ã5 C꺡n:/Z޿ {6(EY{r8hMHt8kC$:Nk(SD: |!d_5pqufQ1$q4 FGƬ`+͂( tZm(@ ,MHOF'[k٘bvѐ4M;,myANdYx<0!ł$Ymz{RR"Bд-IP y!΅$A8H1[= C!%`2&Ibk"-GssƣI`8D IBu+b@۶ln^Nb[)E4MC1G|M6ɳ,vQDQ\\^,#Iz$Ge#^~[}mkIF|7 zh4@G5U)%Zɐd:NS"Ͳ4<͆Pt)%ZjbIc}׆so> uJ2+m#'Kc\{qڶ%RH !X %mR] чRJ<ӰfpzurŽ4 1n``FUUMM&6=5ruI4 ({1 o.t>N@eF[K@@Zs dŹ@ݼ I q h1s}FkSG4hhU:^_ccr.Q{?|CJs\~qG;Z7xB$7;%>.D*u?H+c]zk,Q-8Z zGL/~Z1($1A\6 ;rS(M^9yMP.XסSQt|m[>[.039ڦtѪ077KXg)&gPEB)Ou໿AB'`44k!$I'0POXV=IQ5-ѐIg suM>zyĒ}<J*W gk ev-Z#k-y۽C$W4r}Da3=g]%v1 WXov(ѐkVm_vΑf).fC[R%א8rMqs/#24= 4 ;3,W&m<3!>kuG'L\ X[7#c4 k QOu MgΓf MmWb00 د4e;Th|za4-:B3'gxprXR gih9 fgy,+V8R(qEQPGo,iruޣttm6[\<^# J?g6,!b;Nzk5s.붻v6wGA\=AR|QBe{).Py(`svn4i20ƱBݬ39s,D1 Ҏ{|jłfs֚:F 㱆c8(BRLI/h֗< )-%/^$1Oβ/><|+)%P Oۙ8XBawRda Z}{'Of÷j_ҚmvpOaU(=qҏGHE&X Ug\/kNH8X{,|@G%"noJ\myA=miCϘ?xrIB3P#RCKX~]1sEF\]/J1 Cs4\׷:R_xIdYJ ?{ߓ%"_i" %Ļp?zUUu #:0î,n\__r2f8X<>yx$8 zObng6uk`FTL..ʇߤ,Kʲ$׎,97`d6e<TבqPE( #\H9>"G)qK6 +)th> GrAaݒ)Mpݾ$RΏ k:GY6,W[>x+ 鄓dzGuq]n#IT>e{+rUwXkJw}UcE lT#!q(pֆ216 C<ػ\uTu{\r!2ŧ3fg67!, v=J $ I3R)UUveZ3 yu~Et# Lcc_V#Ak :}5ٌfbt-GJi!Ze9Yܺ/dP%R)6h)ɳg4К6 }PeH)RHX1;A q_!"Pә[6Onmn~=o[ⅶ5xaMlhH_ ~?q=yu]vWEg3Ϳo/_,j "c:2N&*EY8v! ˓h ?[iSÿgmKV(-{APLߔĝDx۶N/|g_o1ﺎ,^2ʺe1NJ4eݳoa Kw?<ﺎ*R'>YʫWHW~?G[?]m8xNODZ1(EF%)J>s?RgOiQn Oi M⬥3 )ә;gOmyxFW&NdyApw[/?ٟ_v;5HI/N+n;m_泩o~ğy4d"ձDZxo++X_NOK?OR4M>U6X Z1tW'k__|_>I)fzi65EQZ 2'?Sܟ+'EQl s"g0X8g,%MSʪd0ʲt_OgoO$"⃯<>˳4 0!Bl+ݎ4;?=?/?֟K'Il8gTux8`bZ3vun0#Ҋك?)/ogs$д-К.㈔dۢg=~~7_zgOnepkEAWI$1;5h=z?OA& - ?#G}ofi"23.DgUx[9`)1֣1aacuӒHAcʪk"-{K\n=Zܡ BVXGkq޻@"%yaږ #MbʠnGoSR>a<ra E -%^xz}yã1u`{b-q:%*Nf)WWAFMY) mI ,(AH&g=qyo/<>9Eݶ0A`^;rxcM~GSiێildiBX$qqVе ^x}Pu$f+XQq 0dYB,c٫ }"Me!`"$w8PZK'IDk7x$I(%"~)Mhf)5`@Խ*_yr/{SaLoI::cRKJa;0.T"Yo42EGk>zccv£A< ^#͆7}لrH ö2!@8(;lWnxH4C[RonR$yJ @}T-iSW%qĒX'Qb4q} დR`]ՒE)v}/*QZIW pdI4Xow$fAgBMȾqA:4JRQH$*zH;,ϐ*i@g|{WBߩ? *J% %/giZK'7< xӡUf8]`e~2fكNvh  t`+^ѶZ 0!+RV5U?ݡX()Jtt]ưI"Io@pXc+bD_vJv|(DRP ǽQ ^Q;B i/*)dP+ޖR]mx0H]#!tX5: xO;(Y(q#^ƻ!sV_tyJ$Cث)$Zi<C8n"ڪ , '4KZ/Ah+$Ixy]AӠ6.x^JeIG]G8*HmIk;yH!zwX\R)T})4= (:Akdp"}6, Iq;i3B\n(f2lg !cLۄ -߃<gqAE-ٖ+pzC$a2c ޻1Ѹ/ؠW}_'~tg^d~_a4NQBiNZ4E 0a:C(S]!aS%pIjo.d1NFy$RZ g?lK5QDDu"Lߔ0BTbB' J$Pe8g$5Ea\2i)eAq$-W:?rcMGDK΢ē a OnS$c/1^G}k~Dv4"bڦ >m:4ky ͣa徤(rRThURALJA? 1Z΀ #(-k,JTh0ƐgIP6 e9YÚ'M~OQ%Nh 5~K 9lvOPnw 'ӠC ,P`I\@c퐄*KkCkGE+& AޫEXZZG)nn֡>G2ïw8ĕ[l@9!I5!M4i x",:(˒,A$aQܱM@<2tρiZZ2 % 9 Y.7CE]xQHt(FqL&J-c>W mgX6 UbSX sF:R!{, 8Z,QpyuDYibᝣ g)sb6 q n$=PkADmע,(^)-uP]eh늢(bk,iP:&u@1R3l8(B'1hqmJt}㖿0h5diFr4m8i6N@ XQVU*hkdf\xTyے4ٯW)xP:"b5aY@4O@qIC }iG+EewmʵaGxOQܿnPLFY$8VDx 9J |]xEJ }TYk(CLZ4`AH9'-6ڒCJ)x"=LX@J=:R/&8(]зnvxI4|zǽ ס֢iP>Ѱ`2!#4Y^Qvz{b@`E Ͼ *7t,.^ݑVk; Tؗ/Z7O)H$$A&4eoR %?O(Qd:躖۶J)5sFdӛϗѠ`<F %-JGخ(8_4|zFDq4 ݝms?Pӓ ‡\N/Q1M̧CHQ HIy :P ͹^uݾd: MX[-?}臆'e40ΐF=e10Y;KtC8v#ĻNY"M:gEE|TqƆN?y5Lx1W/؎isc,ep:D:C+#Kcmh!“gYl}lW3dbDG1I{&F q2IRcuٳ,ZA@KƞM.S34m+8"XR& NCPiZ*p, ,It$"dm!=뻚'tiHUx5hg]|@94C˞JKBÌqPt}Mk[vij\ B0?,Uӱݕ9}M1fP1:J}֦Vo2Hqx~^K]g>w(?c s$ku֐$)kp>03DZt>c:n8ڦ%Kv 6Lzb-2, fg rs(#UҶ-3LU>tˠȩEnk-yt$IBPw8QWZI[ϲo we xnZCu.7Mtމt6[= !V/!%VBg{j lzW<@R 5ɉ}YryD4+nT- &ܖ5M+!tLFC$X+>Π,XYZ[ݶ$K6qiZ 2J@]׳n|>gV2,q8dmvǫu/DN mzVFdXnۏ!æzq)I,AFTn|6#ՁBA]Wܬ k/uT%=QɲFki~; mFLc"XwGb܇5օ,-$ =ށt`lpMK+x;{}`g[$@4i3PJsqyrCa zvॕU $UhjWb<E6 } ?~u' Xӄ??K_&7~?>l4IRQ lc8.p]ѵ-?_/ ꇟ?T dz:1]2B45-K/ڇϞ= ?q$ܰOUo>_G_??^^^R$փilKX1ÔO}_}?ܛ(7UCrv2+ts\aُ/G\_O?ar2ZZRwpK&ILSU<~?oO7 5tmߨێD<(,yp6O՗^_|dZā|Y(i;d@Z9:}[c?2GКލ$ֆipY\A3zRh18$dzuO7>??#'GZ]͚T\|>zK|R |pOP5ZH"%"XSK>|9q o aNN4dP 2Ⓩ?棏}y,d6歧HmscY/r%_?Y|D׃ 9$sVԭBsZd7p8 .2xzV`{ge}'Itc#(,!AXQ,m-s{)^ ZuqFgH%BDw̿(i5-,aZ Ǒ$yJD|oľ1 N l:tRmBCJFEAw ?N8kY7Xp{^|~jjn֡f=]|( ћ}UIf8bjGq{ Dqd2f8FdgEf#YL(:~]J!0XKt JA53=im]"uh89KT T^~׵ NT :Lue.Z^!QSmJT: Px5tco:`zmLK2,p@>KFVDlK,1(`\y1펗1>W=64Ĵ]`om(Ձd\1u`[ 4̾)%q |HFD> w*ZӢH,-;6 k rY(%{@>?|}odq"ˆu(=,Mf0HAB(^q@8T7`"%"Xa=p$o@9j"b:7. !54]FڶU'@r4c:dȶ*I>4H |W\\ E[ۡGiQHB[TdiBݶxkSA#%4'7=MՄ=}8x &F*o!x:[6i%3KV(M3,eX2JӃٿEEf$$UFλzC4̾%8(,Ix0%&tmV;N?bBi0bs9dє; *N(oB_)`4 $G8t2&Ofd<&.ɲ\tBE"T:0/o-"M٘W;Kڮ#Ib'pXu@b (u?,n~'MmnHAXExO㸧H@ zp[#uF^FXDXi(&Ug42iprv>Riv $%U٨81:;$b0}HSU h( Gǃ %Cה$4{u$ ` $^?mhvG)EJwHʄj=b0CȹY2uoPM(cNO'_=F [dFd4MG' DeB `X8KՀa"jGLSFqBN(P!NR,CKlv Q>!&)a|6e*,%MwM Q=<bסf:1* 4`X$IpMg##?o`g+$0TuhzbZ!&IF'p:qih0+{J8&Q(C)16I(;jb QFU T4mHGí'v2ᜣm Mљ F*V*8pQć{0̯V1(O&,AG=S%0 kyݷfީ +8 qҊQQ_=߮<|l㠃|)dm [m΅(Rp͞hNg:J-,y}.F|Imi[$Jt껇;UWUJbHגDI2Y`rʰ{{]F#vG2tgXk6 DWn0ޑD[m P`k;|wv!IOc'-~ﭥ*V4Mn%~wfÏ_iJ&;h6خcri=KYʃZkdRU;LA*]~uu[U5)Q񵯽{0?ʵHLwH34bќ3l[:]I (2,ԻkC+e<;c0HJoSDZ3:܌=B)$BߌsY|j㝧 ٶw]U!Ah{ezyw0otcPdf2mܯA)>~yw77^xA4yk{T>"ʧ̦{[OVUUaxNuxȾ=o~v5~x骬Kg,>`m[W/y9r~cU5Tu8{;5˗Tu~x8OxZVA\OŃ9=^uFE(Ṻx}+OWM݀}Uq*Hc̝< xッ^>ڇ/8:Pmf3lDJuh;(=}[hxɣdEQC5RqP_Yqןo: $y$ vw1?֚Pn$)v:=LSHn?a [Re`MDZ3> 9J+ځ !EW!)YѨ]~F4wj=5>pH}F8,;y=i ReWZ1㻇zvB)Is%rY^v)89kgfrAf !K^Yo6!K=zDuܼm?|7HPss AoEA90t]P)>QRxHrqtSE3@H0M"4OR',cJS"y8-(f98b PsJG쫆C?z۪ uc[3uui_l6hͯyoa:8vGUo[Ɣk2{U][ yVpY9RJ>8Z,qL&`H۶ԡ(/u-AA:\\ʫWZS/O B)]`Hbд^2jHy*@cvBvxPł`{X^QJ̀ƖKnG\^_vu# *08Pg:z>89;Q/Iɨ[Q1E)~['DvGes}NdI0ڏOu'8BsԾub"I :kE/^.mL>vi|#: Db`4pPϟ?'cpA$71kD%Ō;98 N9Rp4n:dJAeP.#Nq*M`=NcOR0l&aL1|ۿ)|3Xkw~=D2yq)U({uBi"x8'e:>BUk5OlnOHy}yŢtTU^4^N' ~`7g-Hi7+a^Cէ%B K>8vOԌFlbnm;4 o= ګ =<˃I"P Χ{t6i>|Ç]uGo8%N q8H$fquLw @$: rOVD\4Bќ4M999!˲ 4l6; HC栣*~W!Z HgPd["B麻!Guxh;͂Rh>*YktǼ"t!9Z\'Z#.v"B@G8~4*!%IKbs޶Ri XoBaz)oR.Y,ȯa]/PHID ]P%uǸCUe#.5ł];F#t>gޡh4d:+_C RBH5C/=R f Of1eU`|>Ȳg3u>@ۗF#u4Oyֻ#\A!쵢Șf^VyUݗ7;`0t؆ V+ZQ{a0ݖ4Ϩ8>XV\^߰8ɲ,P QI)Ct`T>丽UR$_9UUX,c9cLo730 f75n.yI~W<4MKwl[>49$2gOf5DZ_5A#R֫%eUifyOU4M_iXmvTuC>9#re`r"fC6MSn;:p%bv>nw^R$: tIg-8k[XDUYloH8}6Z ~ 1*89aB`<syu+NNtmG ]~Ys?6D%)Bq(:GŤ9擇 zqCc >Y_do4$#=Wlv;$E shF*v7ΒgRh,+m;t,aѴ:R I1!GPyMCvLS5qoi?PJl AUe񚲬ꚲ ݀&;3 /:bpd~e:Qd6FXovC7\H٫dJUGq1ɐ,O9qSPbt6w(7e憮Bw3nIr=b3Tͯ~fsR)S_~@RJTu.̕םCA@)fsr}Wo ׯ͆?65_ZF:b3Ͷ$RF ]_?F?cp~ubM2{6]=1F 'g}k>%/ (o7O$4mC4F{Ojg?H:Ѭ bM1N:P٠tPJdH:y1Q1#рt|#ه֚xt*fLSVՂr_$ 'iK)bb}}3W &Yk2eb:"HF'y~cA}EQo~ $ۯ} ӵ \| MC-c-Cݚ5U]ZoFOb/R2 ᰠk)AQ;NqcYw׿s2ιvu?>Mb!v{M]\^\^cx}i!cQqMSҶt͎՚'MN^a!$ 4%;&x2*y(6~vnt2g/06䅤he[M)%@ NfE Ͻs i\z$IB۴ C AAvT=SIh4<Q@8y4MYWt֐eys0F{j}`]`= ϑfd<*^CfÜxns6"l9)`<ln{OfjCӶq>=Ϟ`677 ..13$#(q8DfC$tv%>`yQpBfu%H!c@XGӶ/\aV ,q4`0( O ~Xp2"d4 .Xw>cy8,%J:U0 <˒`HibFkI۴}cX AT1 !89sm_e8#Mׇ_2XBG>׮4|EG`W-#&,3Xchێ(̦C.P1UufnSv;4 E\1_TJ)R E1`W&C,!>QZdqS3_Z riq  frntFQ&5iGBu]ӏI:g>2>{gt2Oh ( ë#Q.ZӺp!$ńfIĔKn^ Ӷ-O8ո$&2Fq_7GEHI&p뺦.kH8SJqy^YYgy qՋdTM'=gPd%]\\^VFsA$"+s:=}!McG9h'9WQ*&T6Ɣ|RJ_|%o ʉ_ܾR5mbORvstE, &!iK(btdtJs}*D۶8{Ou#iq՚Q8TۮAOѣш XQbP!g-ə \>:O+K4ɡ?پXَ 9F6nU)N:G*@0'"\_EzJh7Y|{G]m : Rƺ'ӆtM`KO_td:R7(BWo|bZWo󜻻;ڶ#1߽sݩ9>x(E@k-eQR'G( Np\_>nGyIbn;Y5imNq旯L&gAt}n;_r1 onE9BZVhۖ|Λ/ϘNo}w#_4 U2x|aq1xzֶ-]x#aG$nH,<\!Z{us@I݉N4 M#9mr1CZ8b9_jM;N##\@I/a !R @g& Cڶd=;JaR[?~OovαϸX|cɧ m披$Q>@XZ uD`}3ɲ4Mi?MlA8]A@0YVp]ull6#2R7`J6 3l0ٔtq3,KS,E! jb c#S!\@ktE:i:`7h\nZ>ofp"M" ,8;Hm0>xAd!bEI$,@#*ߓOQD֬kaExg3e1$Nd<#8KDQv5*Mh1R' c,{a9jtmłO1ZQw8:{RXΜcIr]G:'( r&:n7[ƌG m; a6MCMKݶLWd()GNWkb>9Mۡ%@ e5=z&OqzxG6дqLu$]9_:X^uD(pUU>> 7I8I(ڣq@x]s\0$ xP)5V }W?k~@K|>3 {)⼸Z3b CڦcX`AiɘYdB8Y"G\/חx=Y9G۶tp̙ϧa0#)k1QzzYt]i/? Ǐ 5>gzP֔%GEfYxkEH۶p8RM3I(&Ϗ;mH/T ;w#Bϸ7{g&S:Ҟ6e5 I;ݡϽ8bW $(Eh!QR$ky8 ư?Dq 8IǢ#zVDRJ4/˱Ovmqifk ǏX3LږnA F!!m| ݚR(\.;UUrq:qF!Ŕ,k d8kфŒ|{G0`ㆱ_|9X>ݡxA8E<:m(d2-o(ċKb;/5777l!v=Qߣ矴mK(rA5Yq ; ~ՊP+ gnY7?='@v C4ܣi 2% CK%O'WD ;$s8Lӄm닅7X瀡/NٱXIØ(Q1>=P9Qy0b|2Iy.b>|K&1*]b ru`!hٿ;X&]}?~z!^m{` 1FuP%ł( I(Jxx!,G)ZkdiLe/pɄwv{[#ktAQUvtl~9ʻw6?I1?zu1NU|j RJe:%JBk(0`2I GDZs<hۏh-Yf$qD^M˥dv+v(" $꒼lHfkay!ߎd@]wq@0(+ѐjl`>˼nLZRa$hZCoZYo&<ߚǻ_}A(*Q`zG^U~pbmX.(%̲OK~#riR"`2I};9Yia6!ٌ,KN2-=Hi( *H7HA!(o-/pQؐ^Xa(DJv iFU<,]ol@gm^|A`k"_Rʒ(?aߏfyu`6ˊ0',kKʞo^!CW9C&/+|~>T'-~ <fm[ji<#zIcw8޼$ ~|Px5UJF}nd(H8Qݑ(B& #eUoIvXh5dtZ!( )eӣr1!v^\O/<%8 8>nd Z+md)EY㜣kVk8pOYl㘦HcAdA#NR︼?PU%B*v ֚d?Ngq$Ak*|vIDUDQ'?pV 4>n&O'Q1Ƒ$IAsHklă.$mz(Ο#u>|bp"/s@Xk9<|ѾOZI{sj휣(ck,  !]=='K}[:6#zP%m@OXkIp6jQzsD:M5藿g G7{jNPC(hB LoM3x|)8d{t]|6cr /| mzv("+.+yQ2MH/Y-@) c' ç{_ Q`A$|t ?|,5I8 3N9eYѶ-Mrܹqь1gΏ^LHM>SۚQr@Zf9\^WCDh}W/o8:8J˚$# Tt# W=' PJoJ)' CVd>/J) b?6 |FYn€ƭfb39m>.Y_,((1qR5Q<]x#i2᲍$Sg-JKz.!(t%1Xc< Ԛ` cY59;bAjjZ$ 9NOS5iHi2Obtb fWM/st)p nHG9Es1MSa{@ j>{鐲i& ZI#˛8ލKfdiJ"}Ky?"/85jI۵TUCdiJH| ";JzM&1I4mKۍ[ Vwu Ne$ω6Ơ$Qȷ?F@q8H4 dՅzNWޓޕi :hڞ4Y_,xx܂sX|>k[, NgzcH%F@eC'{,KX/f' `84x^aIDATMD5Zz!' 5iѶ=ih;{(EZK2u 떶w4MY v=1\ %4A)b6n:'I"ʲyϓ*/HXfY?#~/FFR%2YFF*IEP}g/Z qeYQ7ZI./disۖ0JS^faڶpз-w;g~A4Yu/v-R f~=uWdi|$N2./\^mKgzd/!QEQ`{ YÑ$i[(+kX-|{~gOԴ3nP9'ң!*-b|I4PYVƱf+@o2)q1N9,Y<\WG '&Y)@Hc6FsǭO?~u]$)ir"774Hc#G@1s@&^TkV >5h)kgݡB)EYԻ$~Lw=@1r8Lx"xr`@i?&zoL8%EYѴ YP ͱgH7BuUu=Q(%)c Mo (5)%S~";~ 5`1]?0 9t6 4k&IzIzE4>WOkzC >ngk}-?~DaHo;`>hێ !{@&!4AIEu򂢪J2IB,3C` aqd%,ij/ p8aTb7b-{ Q@P<fSk~IQ-#?n-4Ik8ң#i75UuiZS]4D)a-Ip8^߰%%#[k_}ICH,$!?#!K-w8 81mi;! p >%78,uely`%8$_Bl|Juh\Y̧0o>tU5ԕ7 Mဿf#Iv@Ȓ*hɷ̧ac>I)v%au=i_ў" \ w^zVKb>*f˗{Dsś.c^ 70#eY %15+TmKw?͞i`Ō8IhD2 P%5i3?|0$n`'sMŚpa֑$ ,AjXkjXxysޡtxnZڶb~5AIIhcGqf֒4qu98Gx'TbE\QR{}ߏ⍧:Wګk:;l I 9#]-Iuc-NR)hPpt(0<;kmTA PJ?=x֑A㉺Ipsx9-1}l>g{}j9݇;Cwt"h@}XH@*iM S|֍^rJ)4Sy'm'W0J8Qׯn>w-UUᬡ,+$4˨]4,10b¶xS2*d=sE/oh8d abVU2Ľpɒ8NXg}wGV)m]?BA:1@kt4'qkGC dCg:F ֞ m{z`:b@)1=AF*A+?0_,U;=wo$q 8k:D]O'nI8.-ęCࡑeӡͫQtمTMtb\2|7 by{vI;tdiLEhe@(xhc갡j1L')Ji vsbqp6hXzá(X-b`yC%4mᘳ?`1޷G$!mk3`zTތs\)G# u\ hz 858GIÄG@mjr`+h@Yo rZٙ9Tu㖲-Ŋ<f1GnϞHSi{>~dH%ͧhۆz*\){&t4ow>=w ]eΫ$Qdǹp&˚QVblGYE=?m59ƅ (Yq*JdYAJI#$%Qrpc @k~?l~!Qp4y$H-ͯ:E!f."b%1dyOH֊,shf1U1Ձ?ݯNjM<ڶ-}w"(` 8 :c,xo-Z*lUk&H) C ǜY߰O;uYh7=Mh'ƞ/_Z}3k?-w%a_!Rz2G+b:4(K(فn^^dn~,MpRY$19TO/}ci"M^sqQ>s?Rz%RJZ!׿L{;|1o׿7U?H5F UM|*Кn҄"Rgy}9uuk"A(q;AHOEuoZzW9jR@]w&{~t$MB2flt]$x\\Eެ& 86_jᄀiStw"JjPhD/xx74 uq{fr1*nV9jHOk FNAF$)+OXw :ڶ9L&ZnV $>%~,_/Tkk N黎w;}ѲZ1 c/4}}*+o:ӖHBB;D ݖ| %cΫ=Xʲ&o!h4M#"TW_ם["#`Jp*s`[߰hwuO6_1q*y RW:Tuy:`kcRS MVa{da%CoDQ@a'STMK]ׄAxT9҄jw=e3_3hՕĮŗazhMwJ3'Ԓ6RBS?ps@!eۛeGjob-SMM ח!Ԛ,x}OoNj/y/e}PyE+#|8Kkn_B@G軎3e{]ABm7H[qkfm;S%iBtU {ߥQQNiJz Ԛi|yN8#"NqO_}1jq+lwE.=JLׁazlJЧQ*ʺF AmM}7T}wI|A*s(Pv3"Aj>JzO2ns:@IG$^RL#ɿŒ]s +'׃H1c8MgvLdCт/jR/HMYo󾄡V^uX|ׇp ZڮCꀮizz5ceQ0_8nm(Pw>n(P)X'^'||jIky!vYeMk܃@kՠezQUZFI9âbʿ5//25(+^^Ɛ%!Aa):\hOcz RӀw?|ǡ(O&͔͡"jpJMFYtʦo;k I,M<%Ih:!隊Y(b::4!Њh $t82N}GCB4w7}5eU!B`% CnK"~#o0]]a(0C BtZM#IZںa6`LPv=hOts=Qsr^%jNyl6j(PCs<G1ioSp}`rtFӴCa|! B%ACL~c\]_c{ǩ1ET6P0 $fW(ӗjAg%릡*:6x8H"g%: ܆b2෿Ip(,Ö/_09oj!g*AIA5Yl}=+N'T?B߷0J?S^_D|O,EUi1}طP[)(K ^ġ}ݖ4#ɔ~#oWNaE RAxl;埠} 7KC"X.(76i$"\],GO_-q"T\]^rȰÀrZJzuT$MyX̽b/=鄛/_-)SQ6 TZiT}RT%N" AWDaGSLfsڦ*uXy{?WsNyyF z\WBE(qC KH?Q }k/f4xiWyJaLő(~Q}.U9Mksg0T($&1_1_;~j!exq L3LӐʖ85Rj AIS 8&M$|-_^7;9C jMTu0]ׂ $ }nTa-dKOWj`w5b'!8UPT d9y8$#sTudpx"2#7W,]yA:?:?S7 5 V"" fmlNy:p* t$N3hrɜ#B)MRp xpNM磘IS6C2t~u,x \N'jJy9x!x}򂺷 c󷪔B0DcHy[ȦS01,' p:䷿G^锎*[z~A(%{{dAHciZ_PàÐ0P|3>9o~F+ߠ[Og8ivIJ]5UA40$P =¶m:YŜOk_̹Xtiڞ,`TFz'~!`eՐ&!mgŒ, m9GQ[Eu=ij<HYl阪,Ё^{3h%x_3c3IcƷqˆ;xua>˨!x,Aț\r,0 P~q5*i5t]dBX1/qED)SAtCP/^?Ӵ5ugˆq*d[߲mAPB@98wXI[GTm:wd z<軞v (zo=-b("?PsIt@~7O>@Tu;NFa=+LHxQ7{RN'(zg_i? 2 pu>oYKƘB!JztRm &η(*@b}}Hi$8QV @Y6@+%IJ)(= /]H||1JecM2Jp ϋP8JzEcLGO !7Mg([Ktar>##tp<z#8v/&Ic%MObo[2ֱnlvByHc]` <;YBTeC*݁8\,LajNEjElDj`!QS'PoB<1"QR {?C߶/{aT7-MښS !=艼azg{zcCu5?io]IL]w<0fЎ;MsqbBRI= rn4f%8ۡ3Д9tfAZO`Pz!ř(~xJ9Ŋ}]cS%uC1LhnՊ@M1ƲlHIb {fG}v>R5psshE"Ԋ$XZ?kzL7q== m37" tNر}GkNk~. C7YY!q t]O=z,֘╒ƕV>s=M RJxvukX$& 7t6|6f=W?"um{{Get:pBy҃:K7TSNhD9Y˷_*ȪEN$8cÍvKg^cIo'lIǬkDH D(1Ň\__EO(wrx#, NRF"}$SNfߴL҈IҌk{?xIiii;^`% rU%b{3}?O(BM(8".i$#ԒTpx{gymn$gFc h҄'g0ϯ6 Σ״S. 4e0OGmJDb ;g#@Q5>4PGyF \@Y A RI)O)^g4M h)Ǣ!]mJRUѥ$Chޏ`H1o <ÿb5{߰ %0cCsW/iu8q3D,mtxb\ې2?/*wh qCXdʲd1y2Ik t8XkY-fp82y0XEY-b@=٪):8{vzX@0Y݁ -ԇiJÆ>0Qw- df CTM=rENj}Q$0Nsw?<`*Rb8n8A <X;X;,fؤa<>Fסt@>m *V@kW0x:aq"@KVmi!Dx~Gv]KuH(K͐& eU_۾E _1o:[$MYH@ԝ`΋XR%:qP* HZzJ4 % ZK _m 6!m+p:>Fv@G%A?@\I4 Pk[T5EMZ0,ڦ7 ( yd)zOӋ3~Ji;Ph@G"J#$m7^b+17f tQH8|76kXvonl4KB1XCY֗t}TeUqyq/H7뿣*Kp0mHc{:ΦxBHg(v+%c f?E$JYJq=jzM β @!jI4g^|U?UNVP@9SPYo$PC(4͂ -W TOf /%>>6s{cHטj9-_T؃dBow;TC}?#OknP~ddykN ڊx0՞7_R7._~|Jk'@G's=aؓ[\^udKrGQVDaу< h= Ok ze\VKZL1u]?*›7o|%) /|x@!m?_vB~1黕|vIq<<<<}Þd⋨ٜS䅧1Wճ矻-??2eٌ̦}y p4:Օ!̧(Bk|tp|Kg=S҅$t &o=-gׯ_%ګ"5#Qz/t=߿}///6d]M,8P Ab|DMvVSĽ1Ap~dNDTCUYuѝȮj<{cǏ? Z޾ar =r :ߑ^P5qQYEk- |mw899o9V%uz|)=iN2^=hQXcI|vYҜY^j4_hv u]Rxp M_ih-6cp&Z =R t ]=<g)%|+\tn]~cHO_8shk_p(o;!Ik\珔+&zHG!Mg=m{L˜ PuCz0( 2|+din;4ftg"ᜣ,K `XPUW>?~`ey1GAN)$k46S c  ds4{x[f3eh`y!\B KSL%֒PKHR4MGO}i: 9%|nN`=1zAgzrG::<`a΂c}{G^Ⱦ8Gm[9׈Bp)%9P8 GUI1ApAnۖ񅗑 y?m;gk,:Os \ <53- mFxx/jGYXq *u3G8Ц=]Ax8 9Rd*I1pY# ?<<BP7Db~gڧߊ#"6]rM9ŵoڏ4|s4Ep7n.ĂOl>zxRS|E7zhW ÀݻAtax|@aLg3)d;(1(i^"RToι@Pg/"kGIAɊ0 E3ED"}% CmHAM~w:ܓS"G GI\{P7Q$ -N̞rXciOcDvPԉŸ$A;`H_'fgwi_=H\W>U $lp$$"P hΘQj OƯ}C8J0g7cp |T8Niz8@lqhj)UIU5L'(ZKjRÓGVUq|qB~OQgک -n8c,U4wz =Bu%;oJ0MɏGsݏAÖ?q=uӉXe$zg/{|R0='E8- @H $#ٶ-X BqZ{B 0c@Om&r<4,K:7Nfl75/{f)>zsHoHӄhzi}"KcʪBK5;sUg S姂xvk>jd2p܀]q?~QJ1!^raHHu,3$!Bkf1Y OFvK>Y4MS= (~Iv(M|Hpx.v{Y̧<ߏ:AKS%Ӌs@)%W%nx#mmcwޓN,o>78cd`:i+?# ?a8U'7qd]8ŅSBt:Cw{g?ㇻѾ7#Go(_ky<!XEH?{!(()$mA)= )$H))?w`u"Vd9u]Ы쏸UDGo=n RG4đ#cɜluf%ZsOӴ|ڷ,t;URJ~6v%BuzG p8Qjcm`Z?ep 5|¸;})v՞tJ REƻ7hI8w1M}~8L2躎mۖ'8t\i4 jd& Yy|~2 EeUE$С{1ϟ*)pf3o#Ipb6hvMw| D@ZI_d:./UE${H~ʟ}~ՄZL] ǓYh7rwLA}f!YG)a-{|m[u躎X|dupzAd CJI&_So|'P"Ic/N|`;{V矑8|J6pVr{4 y%X]NR iQ@nB^\?cD}ZzNro,]۟sd%|V[+1`|q(Mco< 1;G珟:o.㹷2\sDn`}g&߼>VMuМJWX6kY\扞ܣm[.^~C'_ c?λZͮGt]՚oGcv_S%Ri6o#6`/^fz<}= sJ#-Гl+?>Jɳ{'b_}fk2֯iA G۶?08&Ӊ[Kt];Zq^G?[\hC)^=1u]K<mӆtt"BŵFo8r=EYxӦe>Ҷ5QpsgkO1@ RC@ 횶-yמ{cH=U }m7!ohw|te.wZ| ˫TFeW4c%j{?`Æ4tcI &~5G|Ge{n60: hsP֯h4MIW/=!c:G֫%D=)ḮbM OxRC.)"v̐·)dHӔ@Yf Jy,q8a"ߏŠTJ!ޓfqC5upoFx8sYz:mr"Kbbc )w=Qp:8NL҈bia6p2۪-s;` ۻ{OkB*6~Xcɫ}0I*1w9H 7xXuYv.2N ޯopR@WgG AɊi^ }+p8iQRp faPV5BicKյwVH7xZ7oISoy|`~I<[ I(di4c?N_~uwQR :I%`,FA!'Cq[Pnu݆a axrhcqԽ {& Ʉ0oz{tmrH7TU#c>x̅f2.qvGU7|qE:zv#Zk&iBf>Ҩ*Ty Y4ay(-Ч/?R%/vŎwkq΍9gQRd-6hQ0p#1ojʇ5􆲬x@AHQ3-x6MXPÁ'@Xk$Iηq]GC kxN O,@{6`v:z? za?xct!{7oHé ^b U/GB;LXk$/*N h8f6(F)B !8X.Lח`-op2BnOxZv9nZ;ڈ~35N20D)Iz1us?E!7P ~u1"躮}/|6]?J,T#cX7$jמɌ1oyůS%m@۶]GpkLp,&c<ޛʂnKQgۻzcF tОQR!47Y|8__'5"+MOq),!B6;R,fSE1{r=SY&;KQH0g:؀qq1ξ 煇 m˿#A^J \ u!͘!/KN 4ٜn?|eٛ !5W@]j-ɋjIuOɊ [ooo9m_ʲ,KRmGQozdTšwEȲ?ٟZ`+H9Nq·,#\RzoW?y$c`>.Z3]^^j5Ջ7S/ʚ0G(bpw ^l4UIvQk(^Ԑ1ݽ71KRT{c)],[zP M^8c@7 `{vTuvpʽoHqFkis`8?{ibhAN㐑w=lIfi%GQ# Áx4Uܳi;0T|k,Yr4U0甗h%υf&_iҕ8]<.&QnC-ZGߛsa:=;b'wR)~CUw{nvn,e1OR&,wO<ë@ c|~c u!%?hIA79Gn. VH8EoiG2:OhpK޽sꂢ,x 8c``isoXQt!FRlI㈢AJuꯘ-mi6c,qOmqG]U8`:ͼho~ (c,,f6lK(Q1ՐZ"o j?Pc8C%Ǥ[[m:TT`CHV|l$eyb}3$U۶|q80x|X@cK)#b ;4qwx*8`1\Wda)qr|xGYV4]l6eݎV]Kư\ɒ Z%Ǽ QR0;X%I%дm{7;_2n1HE`c Bz737AyMOΜ1,,k5(!d Cl2!$ͨ& K>*c)M]S UiڎI$12УRJʺ"8z#b6aqL۶lw/Ҷ-u#cIa4;1@с"]r߰Z.hSj:0x JHQR H]oXΧTe1?SA7%4֎~CiAIkRr=GIԛĎpJ)9AT1jILfxHLIv=aHnJG!AGG;RJʼZ;RmR Bk!$y֙wz,!#~#i: Il>X-8k}մq `eGdB]X-֯hOm//fw1b4_@-Y!(m`jgKo((Z0: \Q%ϼƐ)~3v@Ki[v,.!L G#M&Ւhz<ZKsI÷{ew8qS֡sn{xd^i5ԾW*2h9N\ 淳IbAE0^*UA@gH Aj>~SYUғ=i ) p^:Ow@fN9oV1}u$ -/H6X{1M!a2_,}K;C9fL)O Ask$qH[XΧR\/8w%|Z 6w^zYpûkDW EG Mٴ,ۻ&YJS7=:ItdYJ+a#1dE8Biՠ&t&>9faOzͅH$A3PHadLoIo9Rti[3B 9Ō.BIUUXk "u9ٛWJAQ7 AS+C$QCE`Qr||ЇDq֐ыC48V/ּ=ua=H88m[0ڞөZɔ.p:|sm+֞iDIfq }1Ek)CDB Aմ"P»4-uӠ(J"rO,B`9ޮY*魣…8ۖ$KqBŲ\؝*,#cTQCcȫ?cIc^)M1f(x8 4m Ut;9MY a아HÈ('Jk(b\.mŷ߿41'"ꦦ,rDeSPH%nL')|BjO Z }+?8@jޡjAߵTUKg iEB((fvc:Daaۃ(8#&8CU59U#FHIG` !AyaApXIe M3ڶR#Gubm|J54דRZءߒO8 3__E?h;\8~t`I,=nWR7>dBJbuLJG^_.hQ7 Ma$qbMOke>IZ ٌ掺9uP0jݠ ~zt=#}bLT|ưy1 LN4Ø$!B&G3<'=ķ T?MyuCêjзiP'.8 +5aжBH$$IӉt 5Nd Y6eE0P\q}kz%,(!Jd 6RS3%"3~'"A`lnX2}X{;DբEi˖U21LU&x4Zl|&-abw3##7+w|uig 6OD$J)S^DX3]kn6t\v9kr؎L&X,Y/Ě[M47 >mgB̌ۯn6 )\Ŵ&eiY1|"dqi$ںmYJy-7o0Nta ޾ǟ>R;v~$94N>q{{K^ƠUD ·v{hE4NCjcb8NQ?{Vmq0E$ǀa9(BR8kJ};:;x<[B6P1I~pƸq$5id mR5=_cIo25X*sZ^yf腆sdxT59->~m*z〫5Ϻ%@WƱ447˞37-3?|xۯn03~fy{feqչ G9 1iY1Q͚<~O]7(mH][7]OBlvZ}"#'Hb^3KӴ/EמG?g軖vK/J4c*'w]%D;GRM'$zuU3dmu0 ˿;隆-<,xzBU??K@YvOvR4m[J9NlA~w`Zrf!rZfN?wlyBhiJ2>+f0tEflX >Dhbds`y//\#hٓlY4gN)Θ'2i1zMj_~e?#٩Sy/m]Sr {AR~chjno|EشY&%R؇aDk7?@zj^?⪚軎y8vR_j1V}9kڮc=U7_qϛ{ljH(\b=IQD[d@H`(k L6$:o߿o{)dYR{[K'/Bҍ/k9+Yw2Hl1g_ `2 2Z@GK3 IB%;vHII}-wT%:5MCe (3@R[7-?}x Z)qM'NJm߼}oߡM0OL4r85ͧ*`c,i"3LrQ߮4KQ,aY.?b9'\ՐDIW42ϏOK|Hԕ]ZQنOOLӟ=<1ѯ>Br*MDy1Z<յ>}C0T8ж5W?a")MV ӄAx-=t.{ A筳#UUԎ{|h]M2K)BnjjkZCfE2u45. Dm_'m#e4$T뜳v[К-Hvunx O4 U|_=7z!gL6![Մ!dENpl$m0&'K?Wxx m"R!y}|arps9''fPYҚ+QEG9=*q'땈h[3nڐ]{S"e>ƙ#U]SWaJ aX "GL4S2b*10Gφ9SJi[6=uemTCPn3ω4h0I#ؿ38ڮE4yi)ኜEVݧe 9!zI8)M[;ǙdynXzU?r#ʺdlj"kؚ~բ/RndX0g<{U> /;C ~`l/n<*Gb ';jg*1#n,E~$ UNa,cr]܎t!$@@\RliҚE W|>j 1gc7buR+eO6bw %l]GrUbQ9K,Du9IL&TkP)wm9E c,7Fd~3~`86)Y/"r*5rgYzZS2 Αl4}<H酛;v//cJkv-Yi8XTuG]Ap{2xspig6)ƉǫmbevB!U*52U[A2Œ?R/ V8kh^|( y/kI3Tuz:Iyb=j=)&U,5!i,5W"|NUoN%777V4"jqDVRU54nh^kQʀV81` ew NK8Vk,&0&qk[Ms֠,C]7hlkτ k cHyJ3Q!zOSVn?/iD8i>b L͂OJ>6M >?Kg, @7Kd~]['@4M%}gFiafa=?o 4rss~$%v//XWIVy+/Ia8YKvr4Rw r1ȒQY{b0lUt#+i˔ q@K0yO 2Gp<%J)nk]p<}=?.$23n=?ެzN#DI)˅J@V]ےRMUY&͚iʳmqv&Z@+)5m%81x|R%kb1sgz,Xk8w誡[#!k*'3+3_GV,>%fGByuֲZXUr<k qb"e2b_~5V)b#n8 TiyȍM@Z5x¯2iێ҅>iq{n,i*%emmijj$DVBjZ!8uZ&)J)}V)IHyق6BE}PW4T9-u JKX)U(<iZ==QUNR^)*ƎARm|vFvݺ端gr8 EAVb3jGvk֡sd?qr2':k-!c+ K9h M)RbB@۴ʈʁy~<ϲc BQt8 Bm5 5"k)d֫4maB :" c:kb)MV )T(b+{`fg)fy!r;޽{wx 7m 9Is Z}RvM}+%Q !TK 1"D[ؚ8vejbZֆeKH4OrRX)g1 Grr*sG'ŢJ_$vAL Qz8uUZb2by9 D-JU2]6~G֋XWQ7D}_8ʑIu}MexN )Ub]cP:QMJiˤ.}i~}x]SY\uLs"#DMX,W*bLxQdGtvig%{hkslɌmm,(s1ERdV_`ܲu`i^Yն8$ZfzJLR (кjA2RJ1MoR>@Ng|5H&˘r)쐛EC"bHB)X6sB]1( mJ0<(3WEbfڦxY (mX-{rٱ?}G*m۠ )F)< gjaf033n lȢ!ꊻ%u] uURiZT ? Ajg&M$Dijq<4N2njںF,%Kvqo$vUi.R81-:pvZeˣ.#gΗ~:EgmSP' z)RW_|F^ga!&YV7E#][Of#u]4 u]<\f¹8.Ps\N)HE{x?ç-"U E\C]7mC]U4όޣ +͇J:r.L6] ;?>c: 9mlb~~oNYXjzc9o&q*~8?~@V;N3y0 |zް#MYw\tʗerдڳ<+ .djgAJ+m1bEH{7*c[evl`˟7o睏q[n1ǧ-9vEIˋ<cdY28m6|,宼Tv.f* i*M_eU5,,ZTUDa*'^\0 z.,F+8ҷ5SNx|[yi2C!kq_˾c'ٹqi*-c#?rъwoQE^oP%nM%mX]Y\c ?~|]H]92J}EFTAus`Ե5{4!ɲ)gKx\r)M%cղc<) pz}_is2F3VCx˘Y#w%/,=OJ aXY#A0&n q .k{4]Wrtmu”q Q✳0+bçŝkB)%/xSHM]qFѬNK C:;|Vq21T5WF*͢8Ɇuβj*BHRSu"EѦ9qhu ۇ[|ᑶY/cBO/|>U1aj"Z5:,f סկoOc#WO9N.!H tME&&6 :Kc-_FP+JNty!]?Y$ L)ѵ 8kp2y_ʈDwuNI{S$)Q"9gq>~O8k͝t.pG_^t>)S @J墣-]pjgfs ?EƄ`᳞i94B(}rPN^[ _1F- "TZ]a**5ƤC_h9>0#sQb ^|Xar^ V{euv+#]'`Iϱ+if˸zY~ef8ͤj{qUu*t^9c1ŴO黽&Nxג }~6Ĝ.*/tn*%UUe)meQ+E b˖)JG :K՟s1}5[w @EEܠŷ _dq%7TSj`cdO L`Dp%Llw"g%충g"w8?YG`YX"`]0Dܹ's&A ێR8ɑp-Qi֙30YTz^'星#UNbSf/31ۈ.4V2B'(^v_<3cW9ՂʹqN^D1/R03/)IxO 4SNB"O3/RJQ\c JgC^:嵺0TF~1"x. A4 7cq g~,*#pi3U'ROI˘2Q=/ >if+&c|zPf&HrwSȰ`[WwwxwqZ<}OZ##QotnqnDbf^D4;Al/,YDԛ2Q32e+;0xlXEs)w/(Q9Ij@,g!"_{Y "b0z}iܘHΣ/1'bfW:81\Ad+rMܖFDǂ`v@2`}8h֔Ehoaр%v2K),B8Rq{s}ܶ1e#t83Ǝ$"dw>t~ذNDr[W'Վ#gZ91XG)pU#>l0T}6wWqbW9 "}0^X% pP__#g8XC ]'dM8xAQՋk`]~qWf$%nJue}/Gki!R?x˗ǻn#\׹&{wq^dD̎^jD?+9Ӯ`V[ض tDDjE,D=a8cw5$!"aM)̍{-K&pf hg&aNʄPEӆȲQ۲؃{ou]a^0S/9ueD˒a8W8c㾔\oQϯ/^k jfvL1 @kg1Fea,v '&ܝL*DA8diLf.D \msFW&{pal8~vD8J]ѴSk0$ dL,bav0գ呙3sXf󞝀kU ?wy;މ5t\)] Lr[#?=DF9LdRU7:v+ȸ1a"qaWG7 g9S\ii.00(glYC{b6"һۺˈ逓bHl$JfDhELɡ9gw[;u]S '21su6baq$"ۍч$"GT1ԉa?s~]aa٬jDDF0x ݵwgR3sg"s=ڙ5,-"Dp0FϷ "A6Hx]9{Gjfoے(;H-϶xWV  Rcj'Ӻ'H"-R1` rJs&,|۲,l 0+AFDa)^}?PUuKJeq0N}ىʶnQ! +6'ɻ],a.N8_><>,E,e.7ѰwS/>PLܧ'B m0|e fynWRvƤdhhQfƒ/\I4}x+?2 Xw@+"3s919s$ DDtuXX83L麓>jd3LgdgVn/gcf)`vx;ex(fޟ_1ԕI\.{GxI"M#~ƅk9_N '8s}omwJ|/vEv&];mLf&^W"..UwKq?~m[\[]nP8$1ϒCná1u+^J-|j`M,K7W9™ pBl?^wueVU'2S3;RGP)K ZpPN'zFDDy]0"lKISѝ䭵,V/\k,^J&"3'g}__ﭵ2Af"c[?sl>@n wcP;v7u\JDnp3gI n”K?> s$N:eQGXE#D gw뽷>އF(=P#a_P;jSs!fUwL9V6IaOy^24 j9^sLSJ,n U*iA bf@c\XYH'ߘO|N,3 @|:2sweVn1@NFL a8Wf1Skb剅f|ǜRFhd39vwkmp9Z{"053%bcWS7\(6̔㨭njZrJB{=}"r(ȅIp+pWΑKZ[$=_+'II#5Ý5UVJvG,o)MM aƺ$RJ)Dk1TuDzVu:!yjd9kY05waafbN)BET˲Pl f'0;oo۶1}M{?H}Gz*4>'tFoKQ )8Ή{J)'Jg1D(9eYN3aFBP6'8n7as[Rbs#Z$YធR0aV5CMSsnJP ?޴w7KYgEMjLijk)\֔CYJ 4籭%"~{}=jKz98 'q8fr̽>r:jp=Q"NvU 4t4/ό 7tG4̘ .aάD\RN~Dg1qDKLn10\'/8(3j(%7‰}>$-"%H%pyqa+z;elD< Id^zgf1Y8P2׹+R~f_Klq:4Y5&R<9vU`5ny UɈTdn4| OSfs_WY\B_.3|)E݆rS'W%yYmj뺂h6m`dRL+,,Ľ>2lg"dn5,]ʥ^£8gt`ɋ Kۏ ⼔n`w?uỶÍ}°0q]z65ޏU`,몪c0/|K$om]J)˲8xV֪N%mD`6*,c޳Yۏu]iYD,0"6kT-Rp:+'fܻ:]k̴5aQܙY-||7Ϊ.98v_h4W`G; @{^OQ`,D2+DDw3+L6bآBDOYjvw;EH9uD|Qgq>lDF >F`W}Ȅs^6Qy{1ɰPff87za⚵0k#ovMД3ཝ r]W|>Ɂq7Kaf*"}4'ޠK:K.E\zS:hs$ PL0 esp #Hf۲8Z%XU5#v J7#'f`R u{m,mL NJ~-G98˺j.^0W"UhX3`r5YݏJ~n@ϧ,kl ."d^E'Lܝ@n6`F7 i8Nʐ04e[՘CQi^RJ8zjM 0nܺq w?~uRE5^n7%wo[4icLn[`F)"u?{RH ?#JXu6='lQZMp6yE%@Ɖ2쓏0fLSt"E;ZfvmDԻQś E*n}7q{mOWKi\#NYh&")a|(` PI]cpegJ4GQ< _D׫5 3tR(Ny }RwcC$JRzW@#6PLј¬D#_< F n5>ra>G!"Z 0we8:a)"־Qɦ#-teE뺚Ydձ";k CG<[s##1yry4KWcq"191FڲʒKq"Ug& :`cݞG#E8QcR\2Hbfm1,D( "bɢ3vn{E 2AKGHD^nKY^/0MtU3;gbVSFěx +NoJDj|B,ƉOFĉ[#"7rwIo<&3^`j]ḊpfU\O1x" vj]dt )%~$I|7m؊qPm\'NV"}v'p0=&9/L40I̅!dq|[?^`]Ԕ "or' +40"IDDDLgAϧt<}a "u;%V"YU#ضŽp7s{tpC-=!bF=.[~@Xyٶqvc@zՇ#F~h-PJj=܈hR*f|-o @m?kXW;R*c&W' K)dD\D$.q-b>4βJ|J~"]GJ8ɷ~;O'0mIu!Jee>ONj`d3ޓ'K.Z[smJTFZ D N)JeY,6zuꊪ|[Jke?6l1/)a=K&Ffs)9uYeY,%3.{G!~ƀr!9ev80pOٕv 1zpe<(̌D@pS3 d1x}yII$'=O"1j!Rcȹ f!zŜRZJ:q>RgJ4W03sH&|ンOzGu^IX$"ffQ>mZU$)X$h'l-<^xN,s>tBBQY%xz(/֟ܝX_0‹\7WJL< >dP [1Zo6<9Yo-`z)ABo? KJ) R f>50=1R2%eSy܆jY}>?|3JYmR)a]A>Sg!6j͒@#0 oI1Qe]T ?x>zNGVEDSB)'!:0%818#1F | ,- >}RF7Uo:tn;0lۺۺm &[S{k-oG;eB0tO^_P<-]UoG5@hkѺ80U]ED8T]E@b~GT̢*Iyo g_Œ q"u9 ˱[q,/p"!eI~G"# 8f!nIuY8}[EBfn}'"`Dž݃83/h[Owda̾<Ю# uľ4?+}b<'W|b/O3r.wqㄏ,.U`5,G#;$w|2hXUgafZ&hpv 4\~Lj5| Y&,I3ٕH# |Ϫf13<$%o|]bVrD~]TQm[jQ`D$>KN}? O5 (my)wͭ{".۪ l=n",k*hպ3q.G} 8$z]/uYwۍ|!tc = E>"R(̍k㚿򾢧Xs-Rr}^">D3s5]HJi>Y|LY_v߃`_1mO•D,G؍.JwȈoW  ü0Z^k"`Ε>#sr~g"&o$%D_qm; N@ܣjx'EMw ~I-P~D]7͆Uy`6_=f:0bOFZRDi-+=鍮Ʒ*2˅9%_.#, l6IVw"*i 6 ׮{u-"rnZ׭D-gbؤ᪾dX1-N( 0T0m?=a6UeJ`R]Gl1ۂye < fokv0s}?nǿqH 17r')NJn݁fF1 QyI n:RO_jSf%ڈH܁( {m]hE痻̳ t;/ܭj4=15zzSW%ԇ>#)'z>8'AZ/%3pOlaY)щh-ş_nwfxec&c:N Ѥ3s@4яʏ}OjL%%,\:1#? QD \`*9.yP8f }!2Laj"gt3g-k C5Tefu$"GZw\g13gEDdQn]BD" ZS*B#V`iͬ,_^D@b90"23f'@+eEq۶J$NbJ|ө}+>0 :.Y҇ j -M+JǽGG1ٶ0v_i^th3o>#ɹu͒ &fYxQP79['ળ{ΆP65-bS8mcGU{[ו ʜW m a"icp hc(ɒ6zT FiY03E>G^gUcf `Nӆm[ė JH"u]?~U7}}1e!9grJ)cR<dwckCRNXotFl8Vk}Sx Nz~"FR~leQu]~<:Lkm=!r~Iw;,ə蕏K¬}1D?-}5䄜 %Of23Ga<@&"R $b^vfvƯ_'vRHϣs3-6̣rŭ1ffn=,>&J~VMҡ#2`47] U'>l4tNp $fWRx؏Rn_uŇc7U.ɧ֓w 3RN.vy8+vy6pݵw1xƋ8gr4)@EI۸+m2NX]l">* )8o)Jh>¶!<Q$).'d 70ѺwFZN*nduG1z]<|¬c&l@݈Նw}Ͽ),yuQ7"z۟F3lqNյc2\8 FYe]>IJ֎qus᷶;`MvZ1)<%FD(˒rvO"e97|(Uӡm>m})`&G 8}*ăZ;_i; 5l(" ǂŀPd$"UyIX+Szo;pfVIhj%/7Щl3ӄ9Tp&0CuoIM%@9m%`,ҝ$D)>T fad/QSgY "_6<}Ui#pGURǺ-z<"ͼ7-f1s= Np aVl)eę,n┸ꖈ(ZšOyLN&BJ}sG73n>΅=" 8t=KҺX0hw wR$ L3"&Ruw;F[+tZz'"&z{:ܩ>|1z8b u]8TFHR˗ovK)e1BݨoC$9W\߸˄Eg"': XS՜RbO/GIBBi6Em!hQJֿX+0cmsmI$C ~?uijA0hc'9KJ%6f.H)їe ucDډ2H$x~qe y&Ǖ3L觡Q\r^kk#d&XjSJΤc(%3~|Z`>L9K qG'Ŵ{YuUuy9<3z~bf)ѸGaffS8LEєv Bگ,Qs\z*9ATdSǴq~"bxh}AP4? TPϟ>)PI S\LK%@%_FJDxF[rCi2?wyzX 9.1{tB\Cظ8r|zmWOH+nEբ}DZdBL{=ܝ)ޑ(ce0"C#}]Mj\M8N c%*]^Bh=ݔ1^.~¼yZkݵ?BxGA\Tj??ýH/|RJK>2趖 9ނ%㹃lYs4Z o|>u}axV-ԛ̞ɂI{~򅄛8Ti7zZ'G朤DW` pq,֗q*!%q)\a۶{JGm 6Nnhm6},kf0T8ؑw e"OjfCU>qw/KaBp) B'%J wdf,XUw\)s}݊iDF4aVѰLS`nBΧT8ȂVHD }+R?#+ɭE  jFnf7,Sk/nJ| ;!SʉAp^໇ 5AS|6o:f> ӥGl ݇C`Q(i Ql68rbTPHTkRJc%/t"@uJDT{K^qQjMd :E{cɁ$"6;HSvC)G;ę9$%qؖmyrށfs/l^u'Y Zb)IإRr]6겼rJ6R߄ӱLu)!cenNH퇷/~er%xtqp^r ,ɳZs9\ zShRGOBPmꗏiPItcf#ȗ|XcY o~njޞQG. %aGᜓ m$>dcT)Iklܥs㧒e}4ZtBaA<جH˃GށoոRBlL 31s/p?%$b\ t Ğp&">va#տ>ɬR_+Btwu[z ta3a!LSs)0Mzŭ5μO :3!^?A:"b=+RzygwiٱIdoo=+WήJfL;bu +*%0pDZrݧh7U{$MGWgS:\{ '[5leYۋ &v?u÷~d/?|;W$f3!"f.cxJvncT2#-eߨ-p3YiYsyYwbdZs?#cgmvHP[.JI?Rw}r)8臷uۜhF$ +u#s @]K) f _߮`vdI$pw?77l'r NhN8Na,\vPpT̗ADH,<>+PdrBoyf'ۺc&DC*M"|.%*&S'g}Wթ/P1 Bt9ȘryB^,!u̼?+0&k|2F"aޞ08Ht ԓ ` j'<BdDbU cjtjDma?R!rÝW:}@+"3rkk-\@.' Cr*"$w(Qը]IK3 #ganfe`kw&οsmnW{|@kG&(D,TnkT,}߳ٺ.!3sIj徑N93', ̪REH4(Q R%g_պ/?ᆵG70K8;(ؓ RFqB Р@ D)eY>0h!Wpݖq[o˲1HC 2῏i۶,˲Џ#bRJq${n7NYjL8Ut@j'`JĮff68Zn _R66'ivؾeYF~z%aL?9$沮$,Pӗ θc(+ ;<<Ƿ~1̘)}r?>v@w뮨3Wʭ̢̳Y,J)]1DrbÁjd f~{{œ;Sң23{o|e$9F)~S`NI8VYId;dӻi~^9Gf6;jQ,'\!7|>;[klХ5 zҩypK\B]Y*z$WDW3^/< N~v^g@8NHnh5-|̨.D1inRd ZoBUD#r k!N*"˲؉ֶ')6Fo]b>c,IXrfP4OJc#k)[y{{o[ Ė?S朘y)ͯ'/PJF=1 ;qמJftMKJgK.-3޷*51J)֚,$m1s% Nm"(A&Sm)R5D"cDDj ';<b.E`dM~ϯ%[',P/eY:<1 : za!*"uq챭_ux u7Oߤ>ķ.ˈل>>Zj?g&p{3+'5bX*WQ[N Q73Hʺ%vMk.G˺Y.Ar :zdF?}l[k= s&f=) Cvj,=cwǯVlL.ͥL|mw~eF (Xτ☥?ǹ,˾u(PA˒Bl|S3/:OU]ohݷ 1mqH4y 2g8RJh$^nf!d5Bfg^씫j=`r:|Zd0%:Bj6Rh8vKI~kL/mpYe]@8s۵.g.z;ƩEؙ)ApQQSJBv9lbD qdCv&N'{BrZ$qzm_?Wsú5ܷeY@Z -{1 }}V0sNP~17e|VJv'" Yo׿? )_78<Ú8utvD@o:kky)GQ6;&VJgHN6eZ)-%Dt֡Z5dp0/F @O"W"& bBK nqTq[c|̼d!6ae]<%1 Zxcfk&"N`{2W,K6Ӝj`C <8\O>{ }\=s,w? Itca"*Ih-?>b %N 9KN,`W,kyYֵQ1\x c(,̶m ᡗmcf=1tbcP $V syw|@C[-Hd^ I;2 [J?n |:M]NY ;RJ;'HeM숒Yt={gEohz]p%89)4|iO)6=Dl<3>.p ` )+QYD6:yɏДXChC.<'IDs{]M|Q,n ۹?Lfj۶Y#8~F3*>5]3sSHc*cśuǰ@S*,~vP`g:C/]_ۿXr~hݗKp*E^{?ݖk.BdDZ,K*qHNpB"mJ ,֚,pfHN>}Q┉t48dsaCobf,גR|?Dl)"0Cmɓ4NzN0pMmmYeo 1NJnLR^EXf7R:٨99| 7fhU} Sut@E{Zjnh S 1zK͒pP~Dprm,IRR30^ʒeZIc]aZr ٘C,ooo_~)1c3X䖳x*YT)ۖKCd*|֖sv`֜X͗/g{s>RJ, w3OjjK^8L"ޠcsu:gfE9{Y@0KmHeK)Y [0Tԛr{ ,H GL6Z r3DH9KJ6Ѹ6G4!Nn0XLRF\P* af0S 9N"f, wp4Nwm]ة,Utڎ) q?C؜G fqB:F5ƈKEܙwaYR^Y b<àO$9횓;-誺mAdnD^r¥qԜ ]Rr^އ%|Änv5Ȓǯ}͗Qh#%Qr -n'Q7KJ\rMB҇H8Ra9z yNL)3PE/Ϸ0{k^mO_?֣ކY=FKJ1^RVzjf lb7RRs)9f[~ׂ+D1D׵CU{nZdS%86zgo,Yzp,?&S/r*+J(ՒtvŖ±:zJiB@eLH23ILn.Z4Q($朒Z[%Qfnj?A @5GThc Qo r_*9MDbJQbu]5,"IRc!>gjN ɍjRJ Ib8$l\G7u'ಝGOs =(\Gm4wjɽ3˹i*\.axޔRNG{xj N)x(TN۶-f"\JDcc!7;+ ۺ^qM9^IC}ԕ]l";ª朣 m1E3Yv9pO RJ+\ZͦqvW\29Ks^źs/S 3\8YmQq"q9'ʅÉF(sIPh?_>ɷw_O^^?}Gi'4k%dz{{k>z㥸A~c 5'RտۿO~jD1]޾0z+9~oo벴[y)'RsΉ%|ݶׯ_I⬳1)Lc(/Ot "">SMRcA ] !"݁Im8` 9 cWIAn𜒃X{]EhcO Qo[.9ceYPrZsa$i&u)̬ _J)K޶7F׉ L~_{mfv/Vp\С'Uݶ$968qZͼcs~O":1g];ٺnoD?}+,L~Tg)zh>G\ź XJN{=Z{m}FɄ`SJ  j%$r 3H9BøA"wD5'eKv<F@b0ѷ5KRq?ל~|}K{. ahF`Hk-b1:୎1Tͱ4isrsfb"9뺦a"$1C 3-e9;4% xV:uiPɋ0D!C23S.)%a!I@ $A(rokܦ;Sv}m[؟}tExs8^^^9P%fUf2|sSӉi'0Lw췿=G?v핁ϟ^>}_ܶ~琩WL,Ąm]3 ۨMBǷw?D޺05zTa"ϾKy`M-L؏h`":|63H@9L]F\Zt1t Zr>-޾%" n."9r7%g0Aec^9-Zj tFi✗ynUXBb)aOؑ$f˚Km+nۺ.RZe nL\5 $|>,̀ǩ,0[Nˡ"x>O,-oQQ= hLa~"O0p0?wr<#ף́,K9"Č(q2vhNb ŜS.(6bI)zʒC 9k볥,%9R0qt]\&~ot{D>HYG{D3Ib!&F)EU ?_ 6}/F՚R"f I$ØS)e6>x?3~J~}}g?ůʐ 2#k{;~'"{}[ P;2Gf̢<.16Gښ}Z?_2s8RfɹO 3>krJmMJYEW}Ji!,˶?ESFD[)1l[W0۶Z~[m[S},{䜗NL$,3e/s؁c[of8Dn%Ȳc;!bԁٟ{=x{_Nf~>1F.p8rHPc"{8}r H[f9$ܠj8,r&Dw_E xdf9!I>%S"ZpԪ:hsըXxLӲ,!ZM^'SThrX+ iGp6x\ruYEmυߘNf=DLDOc|V"zP%YpԨnHWnVJ!d'Zż|h`:YX%s:3 .L'_[kmtzm9ۈ Y",) e]nRĪTrϵv0HHo% jN{aYrS=->x+Ky)ZDKdEHIÏoxpNl$Ej=@$l56#mrkYR}73x{BՈc$1߁Z}4%f> 3"v1\U:u5ᇯB?Sj>Seso1PGB ДSJ̡;͢{ZFx%&9DJ澮RHےuicnK$TJJ JBI0|]go.k.9k̾R|zy]lpVǛ%y}hCk1"6zf[_rf7.ktzt4a Mǘ0m6 8dv/+|ȓ;tS'vՑ%M?FTԩrN 3bR,IzW3#EbCufmv\$OG 8B\"g%""FTJΑ9͎brC`Y!D9l  nkY$`>N1~2j2Op钱cf4*RR5 ufiý"8ꉣYnLBTr*9 / yTO+z\E/#Pݏ.W؟%ZxF6:th˒H?կ$>N__2ƽp D n_s%HR"vf']'*փSJy)/?}v_mYmےu]&_Я_~˟;KSTpw'9ta2L g߃ ǩkK0 1sWRB-#cYW6|YD~2>5  ͊h-=P~DGG+Myh+@@.Y7'p"ItLʉ=xqͯ'gLrNi"HN#3XϾIWo/|Gݛ ל0K Ru3֖ĤO @>59Q*C2$ N!;$ 6FωڕXS0Ne[%_2۳^c^MZ,%9L:\9Ƕ-X6Zf(dI 9)zeaRrDYsbS*vUB`T5:C*?NW}:sL Ik("5CN:̬|}4Ib9~=ğu\[HFJD\b L|_^^DDS2T{܏넩p3'!i2Z!jꘪW+` f`a}OiS41nb&rJ&>KQkoYRHʼnj1֔E)$RLnRDvqN} g2P~kp7bzDŻ{>&͔޾朜 1 j}ԗ5HQJC[k|%FOzav$V 4TSJs4)L [TgdmHq$l6Zk!i$!NTB矏ZuvٜEDj'ѹڥhU 䀜4'nm(1OOZR."W Lfs{ƌ7sOݍ 5IɣeOIш$I2#$%fI"):}"ŤVsѓTQJy}}]퍀R\-1*hJ:RFcx}}]~[fnaS)N0w>zs8zCڒѺ̞i) ǒ8=^}~2,rV?ܜH}?#Pޏ w}\ŨFcmJQs64g!yH^SjF>Jp`Ùmra3}oϙf,<|>q^n7wܶ7'<Ƕϟem7RGBN^SQe%=8%\ԧ5.m{*3>mLhe:W.Rg㡔M͒`? Uf=OH<A$@>yDԆ } w]B,r(Z 8+ ;4,~5FC+4{(37JbFS*9V/x{ |Z˚UvGu~{D.F8?uΧjY#^DT[7u+"|HR s4><{J<# bRiA2gŭ^>bOj8@D’U V5r"lnHڛ?+L9x>lݜe[ۺۭؗ I̙,rN3Ez)ɰa6r.$"g ac-.9ZdH hkזsΒTwmts:ϟ'ӷ>}}РJ' i [ܶukEz{yLrbJS;@9BZQQOsc^;qZוKE@'DשAq%>_CXSJscU: <0GDS*}Y)E;|kWգ؈,&‡ Sn yHF@}ێ9)P<o149v4:*HcRP3,vuT{_;uNQk`pNwdQnro|ْ ֊1.˒__U-`Y94Ï= |psJ?g2ҊWk5 ŝ8="쳼jt 3򰐣% H48`F`B89ǿ9fMi9&"_$(sb/!;TF3#?S/}3RjqU"z<޶m@r }>vSwZ0s|8zk%|/?_ݿ֧b֔Gc-%.{j2cnw0%ӄBA5EJ)Gm1 qLH䛙ffy~ؖU%7P>)q1}W7z}]J`+X8ɒ7fq_‡S&Aw/Շ:TBybgGu4`u&HJC;۟|{۶mBn4眄] njªׯc0%pf t2 3sޖe-KN6x<1?; "jmt)+w~zud6zEX֭as`*\ۍthZk̴,+IJ9%IBwHZc?=L1.C (bPRJηmej^TmJQר""m+3Pb[m]Ѧ8*SytX唢+A[rYR[q?'d9!l|r6Y1SdC44p&tZЛ 8خ8zZkYJJc},|mA yaC$v5|)K)Hbojo@%y=T `[,Go@rVwش3.E\juِF`mYTe":8:H/Y{#u]jJpt s u7#Zl!L\#xuL9 S [hu۶m7D%gw_<ùsCw0$h7W7ЫKBUI69eR*)3 1WFDf/ej{4G`ctLH%x8?>>< ,rswWG[fK?p5 Xh尖̬1{0h3@,m^C z6~u]\!dfo}q[8WZb 腱ةC'BSg&~̏7@^6(obOSp(p4I_?<=Nڈ }M<5$8nil=BD)Ȧi J7Mݾ^2[}s0ó;#>W7оQfb@mf s@RDFz"ZHEBO.kzVNL3{:ZqgcZ$;_H)fTG > ";\ , rW֪U<7Ŝ[~G&>oV+z$Bl@v) ;0` MD^_@`Fmsݟw M"O(% )#+} PF'cS@彬kY3uSeYkݳI}c]׈DYuu[W} ~ ~"@2@ݟe]OIqY$!8c9#R" udpkbh`2?]\@8=۶!Bwc۶`:, txb9sc+l_su?ƛWoT@q^xOg'\{-I1r6RSTP$)6١{nˉ ha/[ N$~}^dD)SCTK8o/8x<ͽVSh}LٝG`frb?vVG*95ڰeI$щs18! "⎁d L]!h:agY1&,dnqJ=?UmYl/wկ'IKﵵ^1~~J_^n 49a`<"r49gP;sYy#J<p;ׅcAGRk L\(j@_0(g2Ko{?eP bBC[TKcnZh]W6SpaJvۂu;8?G]z`Q@eІI1k#*lfm^ q%`"9@ĺ۶ \뚋T~U"h=pY |>oooחy!Ea(r^~>|D {vB> ߁08-A Yr?qGkF%3Bbp No^;+r0R~{4Fl"?)W#bWң^7ޢL3Is8unxG g4T8ZX&5t9 O({~nF'Ke_?s%D'ǯWE2m)G0ccץ1BAԾ]k"wohkgQ.$4d1Es<㄰0k6_5*T=e۶:TB{/i!jAF"8}t:!:33:_k+)n7'8꺮L7p3P0Aj0WEvU7"Mu[pQ8ԭn> ͹'yG*HvJp˾}KѽQ@(~k<bY^璉Ñq58|Hl]W4n > 9HmEb8$ m,+aI̖"S}Yd>iM^hΓ ^k]BKa;'kplbtRY6bb@6Lpz8sWg й=n7P{e3S$3tH)?t/NpXԟp"ww"z<,b_9aD dY1b$Q53c3A8ffd؅4\q*Y&N D5R t7MD}#B]1d.,SAb_q"$O'?*Qo[ ^fp0i!'Bwhcf4Zw*% sݺǀd'шxֺHrw3̘ay䐃) !yY)L|ԣ/: /X{0San3Zqa$UEyFT(\SJueMLi)D~qY$bөGYyѳiqd$(5"?WDHF<&W@ĜKbZf77 : R9To3FNUSJ~i7lpOIDx])K.=jĜKvuDѣBh6uB{k5  (91GE)qwdfV=[oU:Ԣrwb{|<8Hj+N0?w}gf@.%DB0; }-[MHk|s)QX(ҝb3:"R7s( 6&#t ?aIz\eY˒ChQӉ s.^hYc">t 噺eL8%)ig9 JN9&WG~8iCu.Re'-|isG, Dm?|õb]R|Я/?KfnKtQ"r6"#&N+:pRIA+| ,>\ a.L'933J>i$y<3\$Iz@DhIkw~'vZS$Hjc)%F3Ix ZpAZc??w@Fy.94=57׮^?^v V#1SΌ1~;a>;IXD%O,ϣ] ؙ!54f4-#n%W+Qxy234w: @ 2eZJ'hK@ւ ee⮶+q$)q]PYOK)3Kx!!"N ZxSsZiXZ+%;"ܐؼDY]糔 8 ."rWDХ%U#r'0CE_q0Ju|_p*C#ўy}Ih\rZՆ8u]ќI D>F Ylץ\6H 〪xj:IZnV@<ѨigәFMZ5@>M;1T5Bj6NjJ)0}HzdU2mŴY""PtҐErDr#A(zkAj1 LǾ?km񷉨f}MBnc(E 0=rA:[k\݇$VFT|\ NEDZժe=̙OutbAf 9y&B䡳")7c5"h2wmAOy)}}뒅 鮦s"4Ԁΐ]u\yL/aTKq?cZ[4dm[b)\9p@w F80Ъ8Z]JA+f+2 eHRrf6I .Y)SJ"]X!); $0M0`!DD7#wf&"Gk-L+)AFsnXU$ ueψ<@>1ZJUݑ㋙"ak_^_$' -`tuįPVB,Iڬ鴑Qݏ#ymD"1SNB3}Ym]=6]seYh#pZ{D%>;B7lAxD$u^}kmux>(PTtGd`]=1 XF ;S“~u1 o0v6qFki]{i6˺!gCSUpq j6FJDSGfݾpN%epȉ:sX.Ǻv+|E8[JB`^^^L{& ADpZ$ናUP"W=.aw()%VuҤOW2$ȫY wG1c,sK_ߨ(蔑O@px>u܅£d_FpDxF2#!/ke-qT#)somO"3f 'KDlcDSu?oK~~׿:DDEKӚMxM[X),wI)9 N$* Лs`-NmDW!X#x3 ikl۱^ |-!hcq_~<y)^Һ(ж."N  k3D3{1+?MLaYȧ8i.q8?T5F3,0)'Ab'I['=7`f`?9huD`Ŭ,n>T}fڶ  DĮc?eY/h|2I|Y#A8C#,3EDvdtFg)P~"11RH){2taSJ)EMIIRnL/iWseR,n1H*9R=ɿ]BDN4z1 !88"')+!8X /V+NأMu`vmm!!BVB>U7w:s 7tzu!bwcxo`Ý ͬ3Q2S3ď, 9`Ƹp:C@=I+БR2SaJ%1KJi)em΃С:‰ʟ#nm")Bdbes`c3Ƕ&6U1^#?#N%,k<"?{]4 ͡AyEZkr)] 1܃P6A󛻮@ڻMamʹFqֈ%#p}zޙDy j0a\Uܮ BNw;ܘ91L2 #qAxO {)ߘ?>>~RUBE2kK))eG07^qf`?Q8L=`I%Ԋ {z1=Tdp&Z-f&,unf۶2RI%fD瓈1u@8ԐHk`gV[b 6փ{׮$~0 's0؟twL,Q0HGqGv] <{m,Nt#'m]RJf fERfF`rJ{׮.%9?;J) 0|0R==%Aد#"0H}I?L>{hU Zo} difx(0#_joۺ֠C݌1` w`!f\5 zD#3އkmM;I33@:cI1du S/ٶeY<Ƙ(gƧc0cZ}:AK!07b)Ctsju麮F#|3s.91U!lpEmO$Z@'&A`f$a f)IJ%q@,Œ(!mKy^Aߪΰ/}?RsXH"$S& HNP(>Y_Jf&EDmnQyFL^=t^s M5G tbF6Dh`wl?!zU lTNL{Iueit#H:/߿ V伬 3[$@<&Ӛ'%aI0YM؉E?9y9K>T t^խזJnwb-ROM.1F\{Z8aw!v 0v a˲8Bz} GNH"b>B_$˫"FMt~PB0_![ R)Ø=3z=D"k,x:nR'C#wI )99 ~-axJI  Nx&sa W>w<+F%%Fp֚ #&M`bx<+X5&YC׌|h[`I4J 3ހnljA|`1k_ F$˙{_d݊ܦ:hߘz毶%*yـh}.K۲i{Fg*ZۺO X3 KI~[7i v5E.\n6~~GȹQk)tg YHae}ձjlܘ#NIݜ$}M͛!b7u2i8N~d1D$S`+!(p᫸41 囏$#]<3F]ȳ`Zk`Y1" Dbk 16pY$IdHDž\;W7DfH[ZF"p$j;BY LUD$X~̊qBg q 9=CmD?ǿgABHvGVL# 81kp,<| RYOb]7:Z_p#(DT֎eY ]d:,! [p f E:)]yɞ;ieY"#"C@pqC Xn[t9pM\Ҳ{3€aD,):ly $m?j3i3!bN)9W3Gk3g>Ƶ>1TixeY0Fa*xlAC8>yQ`(~evA35==4<}|9; 8ާ}Jg- 1>T5,"DAi&oZ[U(EV-.s?[Tm[%,@UȾxK)۲l]L wnB{A1u=07ݽڧdJ)%(D`Ykt }iDuu\H?1GA @gNC 10Oɝ0@T1Ԧ;_Nh!MxmUM՟Rr, !%xc"0.$ߴ63'i D)(SGk=Vr)Q)%_b[5BLgPfWZjއ140wsh{>~zV[?juh;P܁! EF405p7B"Ēg, H8THTn[0ܼ>kcLf&ϐd&,0q8>ں&g.ED(PB쮢b"(" {NWKjabZւqiO_6c]VỈhݶ?ZJ✳ObȞ305ȧ`m#:A=,o| H"7'w3q)6 Yox&+i4>qc=DV0d_Pr^W!!󹮫D%miF,pl !" Uy<.TGǾHP|2p9LA {tL hZG'D~,D(Ay,q@DLQ.{/eE6:"kUl3rD"nۊHLSP}p\'/ p$8;<Af8%2m!HhH$#.p5۶MϔȽl1/u0!"{xM@7pJ2tB' GȒ 05M֘g,;-RDn`nF|]^4OG>}H1&Ϛ K0JI [s, N>$BL7 \ '03A&'+kr.Mמ_H-Y♳h1mf~)U=4)k}?(BCQk F)pژ_<#/8=J8mဥ  8e>LO5P~ǿ?}<:>zbfq޴ι9ō8:E:EZ.=ZDb`'=Hz>;*0ッz:MD#QJIІ:nϒۧZww7j(Yd6RJa5©Ӕp%'ڪ s21De)Ђ"V#D'BZNbY:%sUG'k:^'r:4cn4&j q!btC@clxcYtp˲|9iHHxȵ‘r 7[^m‚Ist>7[ơ%*9n36Ȉkjkd#xfN9, rLVZGcT -k޶ܕ\,{Aۉ#.o>x :нsagreB4e1%g0Eq=yY=Ir} hջ7x$]{C$[/8zt$azUM<弾ZsYU52E0 G+d1 : 22ӺnG]r9fa2 GW_.K$z Dc HxG)MN|_Wuo<\-"sպP o`SDN6nl>Q0yܷJ>ZU?`δiQD2=]gcW% U10mqJ[o~:` b}9vHNmZDumR h x<%gf8){⪸~Q™%89JApt۶~,Kd9+r~06C///,"S~("]q{@MºO1=n,lމI;f9f̎13̞ϧ,414H,%eBtЅLIeh:bK߷ "bT1F>j`c mh@h8-kLXS!0cD~/׹F@UK ERptqæ_Rg Axi7ݗ`wBx0caSVn:\NoRg˅SsDx}zERJVc h|~9̔"JDl(zOox<[k^J:8%x)`tm}|^{s~;re37#b83ˤnAv8ih|>MW_~p1/ErAwSS=ޯ{2Έh;__޷m=b?w_K2 Ei*B0YU<^1Y -^@ /*48ܡB} 2|tM\[lJX$DHp{y3pUn h[k+"FY?G)gu;#fv_7w`)ކ!b ZAĸ,mӾz侽}z GP.cFȏY萗}C13câUs=\(H6ZkXK0[U4UZ0af=4KN%Kݻ pb9AyǗiF8BCPu 0Îߑ)焈}p0b50ڵpd $H,Ecޝym5@\5R2)vId"<1uFDn+q 8ʒ=9$dfV3T'/3s19 qs&#bYGYFL$d/xEŎ0fh&r*vBYk-%&e1su]!xuxL-VwSIJ_Uyk=܇x"9 cۈȇvSD'ahd`:?珣e]I$kGb"Ͻ ]8v&vGFDv(aDa'"S; )kP [N;u:YvA6Mj͌(B83y)EZ; PND__.nfIA벎1al SxD Yڏ֧6pTLd* 3RҒgkGE>O@ @ZT~ER2rvw'J{ED=utUBp2}ͱc8Tf6LGjuoܟB˲{F?v|1Q=sIOT Jh f0G2Ms6h-#Gm 2 ?3|{{Ǔ''| <[k)q h^Jb$鰏fCErw U!im]WC(Q-`t F3_?\bV~~fJ Wc#8ZfIu^Դu$ {߲lfX;)8GEe"&]!b"ehz<Ip.E\-g9L"`wUʼ%ՙSuo^^[f KBD_EO8^u.)#bv ̀etL1V$Hj1p:G+{Y6p"14TuC$YR1e"$BO9}&.^'f8Ƙau<<Û8OA 1$`_Ͷ;cZy :89P7kjkݏ!d|:࢛q7,F*_R;Pm)dv49YNL"Ѳ1Fv8˶;EVo zZ3)V(ʺN0A*N 7{;q@샲;c2]`۶k(쵧0q=/ $Qfmwu()bwDC _Pփh @ؘYO9汪i˲8a?D rBjuoi))1 uT ;:M.?r̯A 7~{y)GL u-#j/G|v^zDpͯ~ 38MX"O8?"՞gbapV}<8LVPN"Z;1Tʭ1BwK#pcjD#Ab81%'aNH .eHԡaǦ<)- DSE==#GtwCz|5f~OBLC73rp9r T}?h8#غ$ڱ_~Y2b"-K!`D@5іҖe[䗭Kmo"%bbJD)toe’ I"$7 z0 QLt$̄9$BȌ”u-I})Eh΄Q2a8#nk߶,YoǾwT1A[P3UI T0UĒ$$@U3BwT>]|D 3WiEs)q\{, uDYfr0 {ȓ8r"%gf굚ZNqo]3C90QN)u)%ϟϧN˹ganhD @Ё9xIX `n9/n8" |n*INRCu)mnےSX|||:\G;Zqa7L3C[ܭ0gp-ۺݷuyo/.uyݖ%uEO8θB{4uyJSHzDD<MRɄn:z#BafeYr$YD P0~ݿ{QP5V{7MIXD/ER ,bS뱮3 ǭKDLeLvYK b@ w_K ^b<-D'b uq0nu 97(I5WXchHJ A͌a6Ơ2v$Qq#cQaHR[eNfZ#55>4:F4PQr[(cIe",283/}BLq `B.þUo/͟34щlV:֥ 0d"&5ܢS|iTϕeq8?7sĢ [qՆBLj8eYg^LLLtf !}M>:3Z+"IƩ ?!ǁ1m8n6,K Q͒TH4G|r(\1z׋쪽w0]=}uV!o ʲ(}o79¾A{Q7ol ;:FJ8wF!ef5VO{(ERI" .uZRN\1"}٤ 3ݵ%PRb{cRJ<8|YKL.&6.'s&1.,QԠj{<`ZU{lHDDrI7c1N?x"8 V0q${ tӀ#oOm6 #CJh/|F!J씾:#Ciz"ONFt{/_~/sʜdyODjL7sfjOIx<S1I O4 !p *@u!!z=lD jL<0$,]PSnQ,{L_l\Jl:vuĚ|:C)WgQdE$KΦz۶R1Z 1}ۘ}ؠ1a$}`I!$&pyJ̀$1}tGNzah #7GyR/[Q$J l8sZ ¡#nAf1"=I$PƪjmGGk>~n71L;tiww#aF"яfðE|rۘ Mu[>ݷO^˶$KYb3FER:Qc4_E8\!,(sNPFhu$3 @L}ny 9K┘$uR-yɲaT78jk~6"!R` qiϧ m ΔEisMDk$ADL̵!:L6ƈ-s#ڙBP) .q-Ky㴍 H.)Fjm-l!S-/%G&D8|.o!\]af`,!*ze9#|aD]!# Fo~F'&0Q,0s@d#} #:6Zk[I0gBK"NStbMZ/)D% 'hD4/ ؼ&7 *_s`67Ss; 117ZQDB'-ܝpO=p2IgѾiab 飂0גߵ֟~R",LX㨵0FNvS/BhOa>N;#&I)z 9%a7 *:ӹ f)6vDZ =91?`'C.tR&4dRxxY>tDubᅮLLL@>\ ǾD$cZkF-"/%sz]OmuI !p3ZHTuN ?5oI#>wDLI- 1qs:WmE B`N49pZͯ|0"v1YJY(0n%my%1ު^[k*l˒D\\z/P5|<0䴔b94|mw?=̍BNϼ̸rLRj8>yn#Qg?"K !ƨvkVN21=jǿ fI N<3RM \c`Is_E8'N (D%š$# M9xӘЏZS?Q=fTa1,*pgy׮8t%ۺIJW໙>>]X2#z8@Ȳ 6dë} 3P 8"CAD"$lT-C喫sZ%'"aFxzb"50}Q?lDԡuYu'!#&5Xk=ZJRc4!t!ff>Fޚ0# 9 !;64hDFj!:XuS%n 9˶C3RpuK@hߥa,P`C='!Ssuj|>|0m߷{L2v>ݿ{}eyY\xO2Ǝf!$p'j c)̥.""0GsE#uQlHg6#ob"OCՆy&gL3kARIpfN, Koe͂C{k1k;fC{n罅uB5$rٰծ#R̔LڜWB=ЛzdQq.s[W lRkr"0{YѶ,ۺg`Apoc>T;1m*9TQFm5řE(,.y?t.4.ˆH?}y7{=D1JNi1Ԝ)/8f'0B)!LicU$E$g#lĆ=缬o/Oq*,CGı;x.bx<:z)Elɿ/or!"fs,%,6[km sRk\a9Ń[wcGU5@@UM*I)ft0!jPUj0S`f:,KH][3 '$IѤ? ,yWk ٭;:T܆Z[a 9R c1sE /LJ!] ,i{mV6pet[}[RJIr$sr{є)'_O6dNZV&ɈnAMDD:!6UJfCRW @$$6xniNݞFb\}@pp'pR 4 5ɂ*h\M12_=_#pĵB$rcu oFJIko]JN)3AIu505Unz`bI)!NZ( R"'7`6psx &H[)2:gdhf$Z)nӧ rћ<O3D$]=!gx<|;QʙZ~Hz]4?Y+oyݽ1ɤSgvG}H.2Q퇘}`F1rwD48B 01:܉B3x+⻫GiXm6%!3v"wHdDwwK m,˶nX$?>NGr[hD)8IH`B@`\{7@MLf7{25b>@Pďt}zŚ>uuqBx8Qav<~|~1>?$mYs9%S4Fw7 ?FY[US}Op {1y' acmfCo(n)2v )L+ 1pv!DjE$ g!d^Lfx&\ל{hu-۲z۶Q1ڶr̭1TiiPk Vis( 2hj1I1$U^--`\a>"Zt z 3 0foDz)%b:m{hzޖU$!tv%@؎z]$ Z33ݖâBwR.KLjBV#TU~F:Gj.B`Ca&L8㳙IJ,)ga#0K.?8tt/}swFc>n_uYy>{旭ϟ^-۶&%eafccHU@6AЂFco ͇RO~E<Μn@+Tec s4-LÈY Dt }SeB0%b&dtf̄}Z{`@%F:|]I#ֺ+B1P6!>9{]V$N1x~# Sh', zuf.Tz.)!O?; Ib'ŐH}Lל- p5p&&fa0;!  WDB :` }h%G G' q\RJ~FD<6gx~?/?#q@-me154I`fW <*(aOz!$p(~ӫ%gD 13G4hsaP"R7tFeFVHN7Aw q8|x4ASj$]S" OOgx[WC i-M˒&j!9?>Qv n [J7vH}uĥxxǰ1۶7f̻E矗e Dd]}u0[w/ZraL&}gj}ruq쬭&"k՘ezn.5ZP-M!I<؈1'0U?'*fqm~*vp>i~`L^c`sc Ӱ P$ @!62rt%d%mhL˦0&q1TUf۶9z$%espz ;6}55 6 8B.͌qtK)lNO?nmYr*13Is{pˋSK|5`4/"MXImӃ2)?Ehx  J4~}'&>iiY GNEWϣɲ گVt)2A>]!Y׹J=G3Gӎc;3H8Xnkya:  .kC9t<`_[C,?fw3c&ԣe](SלK22l=}<.whqruSC[~]v4p䜃Ϙ0r~&SUejI3#p C>̥z9 1| c<8"b>mR8lѢXpb5j8?܎&@K~Yw/o}+kq(Wwv."IrZox9 y\9t_4*\bQeI.vBe_X%CD5"O]K$}P!^:t뽻)|a&f"w9 $$H%ɺޘG5B E~*f G3nR9ct$}?1ƾZ.~N~9$9R1"s٦h%\'x{U$9圈IDL`1" so-VLq9k#!V{?ӟ4U-a&03.eRzWDDD{d;IRox}?) ~ 1 8fxڒ||M)cb8b8(a ٹya w@WUS9]4Z["VIĒq baj5wIISj sއѺm^ln^C$wuɟVۧVrL0cdE9X/K$F7hS@c ,fP̝@HFgr 9:0?)8 :3DG\mLE@,h0{ߡq7$Z-CZGG&f`1bE v'D 3C[<0 #$KY$'&h[VZl",KZs΄I8W(HA9[3#bIRJf"7t,8B-V ,ZDhjKSwDgP\Shg7s.byf)4yJi>CVdFOP0:DĒuz,b:2s9,I$^Jm[8}?j 0BLrZk;xhA8]ua 4/c)>MY$:\SۏAȾޯ.8x4gS Fg\"~H"A.ќQsˆ(z+&Ib,'h&"8R$!H&`Q2$:3E1" ʙ<|bTTї/K* 8ؙ|=u9. ޏ}_^L!>:BaPN5""V:ag\xO`]qeHHI{)8"IJD$)E"3dmGGSv<8HL,|ڻJ$ S6%K f2 ɘ0*N) %@RN1Ů&D90견D!IC1>;P|Oiw!~H~ v[,ctt=V8} xh?okp^rZKYsƔ$#IcepOf&0 "Gg-2OQ]LE/SC;!ȕ$)rs@@qÉ`!m[?!/Q C5[.JlnO-#"|P 4"JL@HL(SX|B)y)9$L9pY$ Yuq}?ZaLT SnRJm-tD,9Dj; !"rN BK4`\OuX`tGc 8k\y$&a3jHlƉWI9\Z13k7>Dk0W31IXoJ-B191F '&@x 7KQ1$ʒ"Մ2/Zޟ;JO hRfKa$`XJ澇RΒRb$|Ψ :Ch$ ],4,эƜLDcB VlG+8_h%vFfÇ9>Ô(^n׮x督zH-Zt4xT׽RL7 }߶OxKDvDS걩,o WӧOѧ8pt%G=8 f.Bke?sF SnYR3GĢlx{" הʦd-Z:!hxM4F^/&#UsA@g'0C&P͔AD8q]I)P$Ԗֻyn~:@n+.t ȱ̹C3˶l~tw!Yι4O_LLy˗/, PEtWө#/`@&BZڃc=x9g Įw%I~I̡͠M8De)c4<5YVD..ר}c^]|jA*ܜU=޿|OO}Bq\|=p׊q}";W@D47?aH3f:TK.¬nhܭcoKL#:j~ZK*i>#j 6fX#@{)"!EEDUP}< P˻9Q[ '1֕}%f ZP<2O`[aL$\JNI˨MNL0o,R9L1qRP @Z$ GyZId;ϦfkUݺ dw/24i $ܥnUeFYyf%)%L,HҷJ7wR)DhN,N{+5m74)5UDg @ I9UXjL\3M 6L(RtCou ϊ2Y@kZ",:i4D9ew7Mq~_v]'2 dB2wlgĈBf9Y{$%!z^smS|5̟3ecpRT5yZIy̭rLR}bk5Vk)EbLlƣr$!HMD7"G\*1t"Jyo-%W|FJmYdRaFmu%"&8aqw<`CLY`,iGJ>l9vU=r^ZAa"75,'e7o׵0\r̔):<:-,3ֆ\8G _If;DSCfYxL+q^'=uDDA3ΤIu&`X=#<_Es[Ns+oL}x 1|Gf>3s{̓nT$zQZ-(K[=KդǾlӒ^# Y>sc+Ci1 ;BLP)%Qy('y}:4o71YYDc xҀ6kUb|3˴q$jvc"F"8W OqZ) D:=tcav D DT"3ޣk+n"Q߯ D0Qe9̙LY*#3*Nk<ɿw b+U\ͩ>WD$z^tҷ3S 0c, dK?Պө˜|8 `SX5\(3N)ٸɞ$2;%L0G & .>ܶ 3xƄ( m35o A#>~pvDa1{\}ۈ3r޶{wsO;;xR}x 97M 2 `Yl)blXɩ/',m:2׶e8;KDo.5G7p c,{heH~LjZEں>nLLsDk,mRb]@JAtY7hY$uA Ubu#֚G=#6y>#@u8$9s+# Z S o#cB0}U*:B0DzX01%agyQ==&W QJ>W D!ȏjs&z6FH8"L-]Q 7 tYɑ;NLB3 tuˆHji&;.еD pkMog`BfZ rXosiUuB/KHjKjZd{2|?}` H|I2=F# 9J60e0;PZ5ˑl. mےvb2_R\-A2GX9"U/6A;DD "Xwtl`ܶo~mR+  =]Nh#~JNcŸ,K⩴!wHaʿ7q +އ[ -N @qǶH)6tUVO1a2?#OXQp;y\#?B¥{n"5CP`=J B"0#3*"#g 艃C}9HF Bڇ6vD) KkRm&d3}m'GY`dW˳"s$ 3#ytYWfB"Ү:X___^m/i)Lpf SSF}\-=ֹR305ġJټ:aaAP w7e&9k8Du~90 &5RoE€HOs^p΀dr6"NK[Yt+˲Pm1quBHQ[?ɚZ+Qlhn"鈦Gy;ok,vU5zY2gCmfZLy,?؈(.YWDn#R2'KZ&(ۙ(xD aaEDRJC uZŇ/ɚ}QK JXjnyOݮ.IᱫC˒|,J:`C} ͉mtZ c0縯Ě nVjED7"s/!\l}v4i_ۢ6z\~8뮽_9-R1uDc]`d~3&׶>&)q2ȔgՌSrDܳ֗rK 9SʫHý:VDN,R;4i}ćTt<LBʺV DsSJt8A숀4d2yMWELEZY!ܬ0"b50k) jjԌ8tj=+u]dWkpa2#HRtL) A9[.Ro\.K)R0Mw_ZS;WYI&eƉR=,,Cwuu}l{P3l{10,"(Q G$Dt8². ̄TJCv*w45*c 2b5kj0UX%"E! }LZƁ@} XpsjT@LNezǸ?A,/#"ocT|wacLdɄ*eY 1knJ-Ӓj۾!"K&[>IO۶MO=B}m{hkۦcx8 U^J}< {B鼦uypc\۾}:ƾN7,k) ĈLN$ u{$gu HZbǷ1 чۇ {۽އh(Ccj1cjTx灇k-?\2zvݑ"h 8&csa<6"B)0elNX;j ;4'@CX03zLmY,b湎")RxJE9~`s]W us$anS1|cY˹1wxyUC߽_~T 5f$(B fPdZ8L60ݶ=]#orX߷}:FMsIW2S?y/{8C+2)FZ0pC88U>-ߡp'Cי # x0M jcX = '^p)*DtGjfN9%p`^ r S?nًեf->T8ZQN}>#OD&&Jbq[K!ZkuUG̈A)µ*(LX<ۋSE٪N&0 qm\knϯ?}kϧӛsmwUp plF@7wLsrHDh9%F*"ILRXnjNNJ u|\Y>pȔH hR %4N\gH}:La<$IWn3Q^^wY\;48:cT8MGu1 `/8/㜺NdD.sdqcB JMd[Gxd`}߅X=hejFCgorm=k ICB=Xq^{@88'Vs ͡H1-1O=z0bH̡$)v# st|"ԄOU4V!0%]- j+֛,Lyу?ض "mm{ﺍ1{EA<< Y3H!™1_XCրifu8iȹ n xLcj&,/rVP6UlNG-Ui}=Ѷozu}0!BsD,t`US( ̘(eCh5{eXt9Y0f\%!^ČyBHNb*,U᮵VN{[' {"P 4 .Dp9#iEd~pBBmpy:q91!U'%`] 4q:ch] Y͹bRkMy_Jf 9's̠T2ޮ*[)U !b?̵| 5*ErMjLZC=}l-"(<0w5o[ @>;kjW6c'[s'ptjIk^B{eJWI8- (i%2ɿ, RR-:;º.㡩}pR,V$?_l~>-WD x:Ey]o6m_ϧ<9٢d(R؋b1/C@ ffE<==}?_ODͷJ)>tݐ!AGȒY0B mbo "RapO* t< P`0f@Ĝgh b!9b[CkSJ˂FU׶$Q`.HJLņ#l<\J7_/ysWԉ`>rx\2&dZԯ+TqL+ oKCCr}(v.H3s&C?:z{WE*Z>}N}2<ǣp<|$<<Ƙ%Ej: "p51[c5}6li|}$&6e*3)1k>S^jiHv;f_&"$2th##tiKz݆BDL8O[Dfyo][Юj]coC1kf:}IЄe#9@E@RZ@(^gwo?#K1H1RrSMLL9b//Q*|DԒ0RJ3S)bi+TqG@k]N鲮'*85م B+#h&112;͓$$@kB.bJ A2=>v,8w\^Km"Hy,Jfy I}ۆjBR3:8LB%~3nKj␊L[T!b;)9EdbsdCu,^T f(G_ݛsU2V* 뱼q,-Y:C=jxH\x}C#zNއcdv:q,'0>8p0u9tRjR,Eb{\kHm8$uQSGx﬒ g#yj1̷}â3n/ ̝2Ϡ<$ ј캆Taa$b'F&*DcĎvIDAT:IIO}~:8GkmEZK(5K#XjEUc%Cܲ7q d"sTyd~xdln\%`3#!;8pCcPB8lJ@MD` ':x#92GaȆh Z`nfS%%2v3O_LH: L'BIt(~55yhKۼc c~|v%CR"R19̄{nJf&M78Fg.A8^K'EsZ(f<MIҴgyz@mv"!KU xx}i>NLL"0~v0@kZ""_BpNIaDܷC}nϟ_lȂZc{o~KS&Ҥ4em=L#:"ydz>X|ڇ1Re2|=T"4{PcCn@ƈh:I 0KS E;"BM!XJ%'nL3"d#Bm} 5RnD[PUO!ZEtGOl}sɑd*)z'RC>? Lvjm.-m jCĶei-82,(gbWA+܄e?Z\.3 0ڤ~= oz:Hx\yӗ0yD&R4݁|oR䑊p M""Dž24Qt;F_GA2/INN,.`L5㘀_*ϙ',ۜ/&|17'HhjĒu#'^8,}iMϭmk"Բ:z~"vքFKI%A Cݭo}޿ V@dd@(R0U}׷_WBVӲ,@SLj\U܆n1bfǾ1g$% ޷އNLѮ@dчMn$(e#9dHvc03sw"a$ m9UfNA>A)R8t }yDhcws7wP>4'۽+}]5eȭ}u0,foSTv`}Hfi) T3@¥|Xt:/ cپϳ/;|M˧Ϸ}P#HXJ-$RYH13,EPͥT$ߗ JcV "pCDA]ptwNA0PK7?n@ؖEDR)̅e3 V*W9!@"CH=|## @*F)c&\y6r˙|NP"Aу AO4>c '"dwB㻇Zks"2dY+}Uܭ`Cg$T1[Lα̱ ^?}=_/~{)06*"J$7`c޻;ԥj1N<#IPU4*r~^"r^/o.9Vk%B`fZ9}o>u-нՆm_ZTT"V̸y@׽+C3D>bX[Ȯ!1?B܄KT̓!<[ "kxkŬ bVX*WwJ9גfhBߐ `@*r@QRn 1z"n",TQ!$̪1 q!.5) 3sF11W~wEħZ0ҦO,"BPIxۉ7ݿ]o7m˲ªj+% T`>P8 B$.cjDf "ڬ',633dL:"39RTA^ YFy$(OW~ߞ.D|{ AvF qr13~D;:?qǑ9;gFc#Ofy0"0BMb a48{sh>S kG#2ӑs>?m]%w{T{)˨ i|f}ϿݺΈX|Ӳ+8[́vj2c.ܝ9) Cp$4azv@8])D9LZءf":mz1ֺԄʹ[>uֳGn?ӳY 1= QaS1TanXDHHaCZ! PH} ۤkRwpRآضr1X}"}̩TAZF}l}}\{׭@$2',UDME4za)D6H)__op>©5GH)Eo[gigDw} 9oљ|¦!V,Zĥǽo{߶?ͯ~oRxᦜAT5[?7'q=Kpf_ |=Ǥ>3fϔKZ[Ꞹ֞Q6]|Ҧ,ݹ\ CxC9DTw0rx ܝy=+ T0ZKk !D~=tɷ?Ma\' v;d#7wP r6vYCe [a%ClF FD{ln7SyCUwmK)Ӳ|c<h y" >9VRij@X_!!v5 0;IʥɊ:FۈX@*o.OoTն^kϧImbý|>/k!O v%'Br7E<.}b G~g"G95?EdY|>N, vm82nH k^RL׹KL#S"0uˀu*%S=EF-G~);3#<,S?S]}730P; " (& W`dC CIgxEV-"aBj XRJcZ[َU d#uB2 0 T[?<_~_겊dX|!FJ}y=1(y9km~G@m$DL|&i'wOOo}c0-2czty\Z"ȍ\JV A,깎zTJٟ!"$f#_7":eLa:1~KE.5)xgbZJwU]^^^^މRp/0ieZ c C@2_?ߩ5^^3e[MAӧoucϟ? fN@,˓ O;d8^-Yѧ*oVHȅ{>_ gR׾eYS.c$he ?6FPf>^̆B)Psqִ:RlIgϐ*7P^q ;G@ɒd \C@+5`}8s>Yp^ +iT{y(Dh B_p99؀8ťUw_jCĮ;Z%Zz_N_."gN?A uN'U}:[%GRHA\x޼yP+֮۶%n/wk/W!1mLc~,HFB$F$,E" 19SH(.\ﯯXwom"tY1#b)\ry9 47&Ðrj,K%"""2q)2p~}߅òT=M6H 3  H<%:-Ծ'ws۟N7oΕet:U)K+c )Vfum]D ę19 Ff)m#7 F W/Ϸ{+Xh1՚O("Vw׮/1##b) 0#`gU >o߾E߿%~F ^>_PmYBsͰ秋Z~VvwΠ$!3-I\p! 0d j5U HH}0D0ƨm=-jo}Y0RQjNmV3uA̚'PIUwx>c8A};Gi%iΪQ~zm+5C<`.(1zSIzd~yE'@͊f+=C1G]6#"#mkm 0܃{$R"<-VH)|>sTI[i|>?=%r\."",miUD$\ Z I=,ZMBE,ƸnZFp7sՈe͞?y--:mLPu5}AN"I؄Yxֺ̘V3۶˫o?~XJOϟ?C5{ԺAĵJ!r.lTic)s7A*U.SY>@~Rt*­K[j:TDde][`\YV!Yӈe o@2xDP ҕ+4j&KBD1%iv{x+mi"L[]MW|D0/0I}18&dLF[g0' 2,NO?t- [ĬǶm鿙SSNgP(""4Y1.Ƣނ|_e=#')Jvߙy]00rJmH0}S p>KX$ 1y"/[8!Cs֝9Bc@grl1뭵2zN„Q1pfIa U-`Jɸe 0gz oD~} 7BS8!a,edb2w@34f- HGE^}6{Y2T~BHvFlABIKᶥ;= )EX 0k" O X?}:rp9)ql'K}GĤLg*L$PJ|." a«ץW$ޫ &Kq9+//~?eҊ&)0nDjU3Ə|R"`APӮ}IJn~ 7|xzyvu@n 5Rj[Zk)N_r9=Hˆ #"P<7OҪRK[X@vyK[E@aV옆<ݷmxf0q̇oNK98 1G|T\Icv6l6/c>"DxmRʲZt?0If|-ڗC,"fL,?R!d~B Tm 0Vb΃FD벬˪2"A3Ƌƨ1??^~x΀ $"r0$.D}ab|DR[|?34]DPLjH8\8Sې~L $-e84ѡH5R<@oN€`G9U[.\p)Re"$arPZ1e#@ap[emnF{&ED p<ȴ5]ss7Jm%0ѐd=P9`eDL"A6 JDUb?1͢2U= /SW=[Y&_&Ч4Ոޒc,]-DKR $ Jic}_C0nn3 (C_^f^UM_>F7ok-?wO_:"pkal2>z] .IDFnnYDZsY2*R=?Ua"jfgcZHfDmiLҪH1wɉl"nw/ziZBoz<̜qZ6wK;D 0bLb {$̀v7})Ek)i BɈubdu]ZkdↇǑF@~lJ i]")'>wZ8Db>9A+@@U]ץLZ~,mUP>==REY( u 1K=ܶ>|H^,"ӑmۀ0I1rX噗it)2NGKlR723!t|\L "A>I1Hg1E9\};%ZIZ[Ϝ|0KO!0Sm7ݽ=\ӫ Dط&\r9G10"ˆ3 NjC$ "fHJ4,&d0@MlfL^kvg D}kYj"961=XKCn2M7+DR$iAd@LskM]秵VJn02}}s\K,Ē8ƨ&"gpu@DB!=..$mY z߶~~w.E /t9?]9\/B@aIʩo<=9y1)H׵1A!BF,[Bqjs-^HRJp^\ 3 0RМ4R0"eY;l{?_oiKԕ{xR-zfw"o. BytlF@*7i]0)1Q+ gri]Ha)"ei@i5;511 i[eϯF"yC |}ۥf<5)ZHIYW9xs.Uڜ29=^`L9E$H*޵|4,yG BZ&T IE1tʱAD[ q$)|d<%sZ'#Ѷ1yYϩ8$L2CfDYF$sMT!,R~kC2M-[0N^v!d0%0go %ot3AKfm03>zȐ4̏]yY g4'D^۶Z[k^[(w)%F8J Tm߻.=~~z2Us}yݶ7ӇNҖk!#Q9!$F<-˻秥m:TU' Zwo Z0r^Z>*I@)yF\PaRI;gV$e#L6drvI6p@VTc%q^wVDYqCdaC =b]o=ZR ٩VkxN $Xv:Nr>OtuRE ܇)7Ĩ7]Ow'pei|*߂2 l{d "qL6u]%笚+~{N򝭭 j6 `j?淿{G&T&$E0 䜧s+ 9pȀ3ʚ wi$ɣiDcsZmvbfxB+p, RsI$2{{wFjua!}hd%hĜ?eSCdYqrC0-p\Xhi{ wZ&0q.'K:=7sj! 83)1L"uix#st:uՈBDU=_\W P%OOlW@DLS7Ő<> TWIKDm7OaSSat^(gZ8-WE$5"cmk 42.| SF\1 cΌp5ua>?9 ѯ"QZ "Z1fI,HGaaֵ0UvJqt^;CuDRSUX8k3"s$3Z(21Idc:RCDLP zy]^<0a.r߶q ,JA|:B̔p$p}`{^u Ā~}yqi6po.m+t" 0d1|13֊ &aCW7~H @03⌨,wVw hRJ)vU#bY֢η#η/) @)q8nf˲PC޿+9z_׵*KpnTLߥ">eĐc4!C3G f6ƣ0g` n P[5M9/WĶ \<蜎sZYl,Zi hATD1fP˃ Wdf<;MZM3QDRL#N<ķq>I1w)Gnזp<8* -"!{ƖMM'XdByZ[ /˒Z=y]1}wa5+6smP8@$1RgC,­wcl[Os|^ oח0u 7}pi ?wVNȌu8~YnБ| 3,:}dZӺ$m?|ޖǏ璻p{jmEsbvjݻs"}10yTaXj9_zRxZOk%ðGT6̌HN64S TRD,~RSͮ7Pjg">Kei,`aZ$/5wRYʤ1 lͬf)a֑}~߶|ͮZKepס5r"/OkB_y1Fgb7U7IB8EUIp:vɧkqLz" &8<<L5U}m:I8]LIRMq@ֵԢ0|3\KKkthw}-"d^kd1`֖y8Q:PJItX<ډft#1F˨1ˈShZ B*}j`/*pfə!ayPB=Ǽ"p%nG沙ZF!YBptFRxvgLDjRә*h[)n{FUD8m€@ZF?T_"e.Ag*bcv׾mZ>^=($jVKSZ,r9bz͇wo,k[j#0BH.>Fw !-iBBO.۶oe]"%3I֞O"@\NԶ`97sqH;p\q!Kp clBn*"Ǹ^M өό2սU!2banuѡnABHzY/>wn0MK|>4Gun 1Vj%lP8g>#l; ] B0A;*%GsJQJ!Sk"C- Za D&f(LmzsS "eOU1F-U}>[U+GoR5?}v}w9Y@V8~7Gm$ga`$d)Hd("g\"2K -<2 (cthdp^) rvv0DGq3톈b}'a5379&_z"TBs4Me؁;g1C:&{ƒ#F_^XoX_v scmۻZ //Wiu!r卒_R)zk勏!Ea^f<8 KF-Cz:S⮆\JvoV[ށcϟG׿zZEfEN[#"^D1ME [-iD̏4$>==}ò־l`0 ]^ EZr:{|nb}Ee R0saY׵a"TB ʤ$'=<ե 4u#bi(.x8x tRym#31WZV1, #Lw@:tW!Bo/z dߖ"Rt6fBL!J$0"<c ;!w.}r T3_A#aU{>/?)D:"bRjtjglZ0 uWucKJDc%v}Lﻻ2-4p$U{S7o{&R2F8`oy:]>~GmRnˋ7ID́lNx(?SY4k{+X t6tDr" YTT9:swdlZ+E՛"$IugJF;1۶f\ǏK|^|)"o46rn2:Ac ZED*cNņ#րpÇz(sc" =D"E!B%Qf!"B 3X/.umm)c Kzߺmyirl&έԑTa|den7ZkG˧J=? 2)eV4ÆvHBR&Md;z˥1}?)Bi"DNa5o~]&L yxPB!Táy9-l/2C֞e.^o[m\$"z|>#FP2v}}5w2>݇ 1pS?_7B6jVr]oO/~i_]﷜ڛYNH{oI )-k#*6)h#T@CD!us=fͤa$ x}w_i>~tKkVI[_­GXUv+yf tͩдPPDy8=]a&A2f;n,~t'"a}sܟĞy=Z~/Z޷TL鎁 = -"iNF! z]w"*MzZknyDhxcJ%$}pt N{XNԜQx2>iEDaJ;kiȘ"VkM*a?|f˲[+>{#C"/eqw>-ַ\McevyY&~ 5L czZ27T 簏&Bp0 Ee9/)7 Lc /Ba<%ܙT0%,y[jj!1 "Y'N3&HJZ}i'A"ȞR 90f@tvsˉg>jBTEC,YPWOT33w7a_EDc1 #b̷C~ gTVy7|t[cSf<-q]>(VGt Zfy:`Va? HD~_o_ZEsA7G¼TYja&F@)RZDĥVfcXkI'ȮR8S 1"!"eb,3wJ@j.EJ0hfCf|}G1]]mJvt:o߾9O~]GKK2s!ts]VbƙjLhLZOGn׭wնT5f^'K"EG } 55>` 3R2B vn^?]NR%eMi+,FohR:p@|vg)awEj+IDExofI?+z qDO3P P`V[.49,*j"]SC}DXx]-߭4)׶sRgﻻgLVj9A1G(cȟ;+zQRJ3#˾X.8w"S80ݹ)0i͔(fζ#w!~ڶz"/늄+HϑmÌ(;N33m=BRΔ^^^,{~uR4aD1:ԯ[Vc~(TUkإI=" DҀaeYH,{zn0˜RRaR$ 'DK~8."85≈ :FO) ڒjiG=g"$0{dԸ"xqx%Հ-/miΧseZ !|]&eYOǎTm|("ufdUr;?mnLmީpY3R RܷnˁGDa U$#r>ZӶµk9'n>YQJ}-Zf~lq0k8~_TՅݛ77Qc]ڲ6?ݻw$2&&D8JKD?8i:AF_{3'4,PsUdjL>bf!dH/; o>~󭚧"~Vt׈Xqdτt=( |z'Ҽ ( <. 0XgaT'SXXHT"L1tKnTatb7Xp5!>].9-eDp+r, p!pm\#o[U7JkCD OW|ݙU{nhxm DBfVQRUrA齋秲O?}땤ZH;"ֆYcBks:Ғ""}.%T{:44@gf$d|y\eY(\ !X8<8--[D E>.vzY>}wO0 uz:z, $vBZH!p v<2S \J18R@l9w _ۊR [0Ej-ZhF` A[~\ot\ў\r:HwًH@6Qsҭ"E<AsJ L9'3=͒?rSq_yv8eTݦy[)<}ϣ$9Ɇ~|Ԥ+`{Wt;TI(@  xg6"x%`ϧSkkYzٺ,}mߧ[X\__޷۷5 UkNs9 !i3V,̳Me2ISZK|Bݘ|AbO7oΧS)mg @eYNSK)ʥ⯯6hTq]kZjmmi9a!`9L«! H"(STR0 xDjan nshF0c5}۾}ޮmwuȈ}Dnﻹof*̭յxTmw"LQP5i\T]g<s\IDvp5߇$%)D%m:!_RJZK۱4<|S2}Hp̽rm۲H%}"bijO>>(,L"i+HRq&?mt1PĄ07__ßSzek}XfdT-ґa GV5Fts@$l,˂n}חϟ#BV"EZkeӚqbݯxt?nL#YS]PGψnb V3(0tOx5K秊k"2夎@nQ1匈|Z P#>>ͻ\ خ7AXZ_o 3ǵ4+aa1Ó@(a:"T|m}9}Km ³# r~o۽@8n}>t,""&=݄,KkK,sgM$Y&v7fQ53_"2rB4D P<0%LUˢ=AIIfw9;R+b"1PȄ,H+I-3":f:})fN#!Pwǜ\57 t@D0}jG6:"J)$D  ,\)uuu),^Ed)d '`]"t`t)w'@<Ԓa8"U%3E`f +'Z@ė[Y* ݏmeZ ?=]iYfd] py}CDH"RV6G9u2S"wZ1"ƈ.+MXXR~UJ|_m>̾?\W*f$HO^+sai>?WJ ?H} " ,a?}|PUXc@]T]I?h!ZDTYc`ӟ."Q"#&b`HT NPD8819mx >5?cN`.T#:-Bj\jqeJ.b~121x~^hhcYI֚^rI2R#$m]WˆN\k;_|y8߾}>qe@u]Uy07YC|Gv˺Zy0`D`Fr#)גCY*pFADq fL\ey}Wv׷K꒙Im"n69ah}wx],oX0KE@\¹P*En;>9<_~eqw,(`*"fD2#Aq-#?~~O@ a,<*w5%B(%m߳Ul(HPI`@۶Eef!"e۶e"Gd~_ZJq\?|xmK?\p!#8ʩ:?ϟ>#:Tt u&`u]0 ~sIzpWD2E^y J׹x:fZi<)c,+`$)οJGd6o蹦6";È0#b;r9 d@u0D 0e$(4RRib; I֭0 m?m;zĶ^J):3wuF3 Rqoy\9^.hu{<5#*۶NnicTԡ6T} Zx5 ԫfRYltWHX~%Uj,SZ)[CnǝmEm]|[8vKWFx =z!B/?D('K6k2 e\t0"f$qsr?A$yÁeP !HTa1MZh ̕@1UÄ`k]{~Y Fۭz٘i{wTJZTIXzưHBl;b2dJRs-6bڙY4o! aV<]ڲSGNm$DLlA@ eYw3#bߏRJppW; GZ8\m0kmAH#Z+!S hXqP>ڱ,?o4޴{õ+":F)j}omYl? B)u*Sz8il˶~^ߑ5DTSqowfRJF GGġQ!"}\KWcmKB=|H9Y_ERJxM#ǣY34 {!0?a<^|NB$8[H+XkT煉qOwsMM*]5e[1:knH!5beYn:ܠ.˒ `*"?Go޵1\@;~߶\d:?7w0U>e)yxDHQY/JHz^)jqd6L23"RUuR.KrZkIB뺈,&ץgqqNa̻uO\ݝ3ClD$=,Hā@˶գxanX`Vͬ sB,%5=-3e/_2u[}Q>uMG$QNOXYha e^*,ݜbT (hYч,\^/Z#.O#=mlE~,5f,R ጃKvܷm@&Ru_м.=cPXX'̓@H-E#siHKl}s/ n}۾ ^J1B\P~6z @7EĶӇ׿5O/k.˺Us y*2!Rx&0bDmz¥fYy, Ml"J]7W 2 Ff+eY__~332 0#:qȈfv""Y/?rq}#P<պ a8v˿Sy^9sN bfN'f20)+ZxSEp$Fٙ+F:y~I="CԘ%'@AG<Ӳ3'0R0<= "s+s>Js>c"H3b"!K*S3!)A{hGo$SjnD`.f:luj,e@ hPv qӴMV/Rjڍ1{SJ)cmF@"0Y!`Ss 3 HE$}hB=X=< G(eD" ps7!n붬ۺl벜KH-"EkywYrXJ)R="cv1 C8 e沬].Dp zxĄ~#scw DX0YH"y.h]TO V akK-" ą˲lp`B&AYr;{n 烏hN"v"pol#ކz|}ڻy>aC }6,K]___Q"A%9HT {]Z+t3-`-%GՈ`0":voH"µV譫j]{oj"8A NHvzʜœ$pc&@"3>}뺬 +:RGH!^?+9 @DD5 )Ie,eإlpukɀ=RoZ'?x)NZy1t #!|*GГԱAIMNЌDoR,-b~YJgUg؅@Z#pϔ5 8pY֡6l[=LA`;?}<9?XNm2؀ih}K]n@,6ip)ض C5\-]$ bkT~缡rA Dn㾔,Cޗ*ݥedpgYt>amv4&)L34" ޻dYN1"jLq :e#e 2pXXc&@08eY{w)-#L'f7#8guYH$Dd:^rי,uasbcCXX8vҖtX{eB𜎚f\(+5I @cp-~b6gf)K;d\ 0P+"^qRPanj}lח~{ϗ+Q( \K]гG/T !H MJ9z{th}/lāǾgB>u"V2#P'Ȁ>?&mYJgԝ<1 w ".VR0(Bi,_)b\+ SDv5f ?rA@֒[F`6wȋg8`~  gʜB}7W-ƉsJIƧ<;qz|aޜ6hS7̕Ls0= K@̼,*!2:'"fsȈRR-R Cf観@H~j4^J~)E0!Dpl0 SU.zY˺nDR*rXJusF;=hR E2җ_n뽏vuEo{̼^Q35MUuq U c]j1squ&6oVrYS>2)@UDZ/R+ 0 7GT 8ZC `c'TO4y{>3H= .*a/ha)w'N]Rꐜ`Dʉ Y¬cRg&H0F 0]]Ku%@ĦF" "3A].Rk wʈ\MRNU5"HWoLxw͏[OMY1}Qxx)eI<">wmn(o YGb=  " ]}w9C4s1J؇LQa<#`".0׈$zeLL;}J$}grQ)LU=>̷u!ws#́!q{G?3*~&5ˬRe[~?x? SFKF]!@!2iDHL̮VKqJ!Qa1hυ#@n1z:o0}FRZ:Fk@DzȂRJfPq~RZX2a)03]p~.(!=cO( BP%m, C-K]-"JNEHdp: 'fD-#ioM5|뀎^`jJDsu#s&"Pޗu:T,uw7sh{0.R̙ՙ8Ȳ,LKa լAg<G꙾pHceTc!jUڄ䭊_5 juA Qu[D25g!ϔlF̰mwn!f93g3%R %DrTb&"LR fPͱz.HC&t !G@;iy(kT&0! g>D3w09 tnH) S quD? Kk0!~u_~gbRS5]J$ģc+EaAa,n K@Ζ3JS ̃͢utˑ%Bi Ғi93d]M G#Z&Ò1KA^j#@LI9 B#D}VJ!b U@Yω0'1% Dϡx"`des, i}!Ey[sU./?ﭑȺ,w2wĚ:p7_nק'9c.CvhRKe"%A1zu6DdDGgGIqFu-R/2 B}($8R%0/ Sގ{#ܼGOHE >5p5ˣg!!AL3ZY{;8χ"DqFR+Ez Q^ "E0R|Q#tP1]qV  0$:?m0ގ֚YS S]ꏅC!)yj!=/99Pz3Щ(1eP,z|)H">fc{{%>[ХVdaz*7,lo.>B́w.2fbΠLJ01|&$gNc!QNeZX pO}fb@ތ%?۾Hln"`}3lleexASX&磏EYxCMSX}@&\."D =N>NmߗR."RDl]9FֵmzlZ;C͏c'u]_^^۾rEUߏ}t82V .eYJ 4RM 0J)"eZ4slb.Bh3!eEpqmߏcOj|jer G.6T@|޵kk]Uւz}Z5,\9"2H 3&#)RB ddHwHӘB@|1Tu42L{0@03 # 1jfN磙mS{PRDHsɒ $oJ^q[;Ft޻X1j۶nzٮ?0",?7Ցij޻z "l3<<"zIJHa37!hp虿f >'$jч"sPBsRf)7fW pM,(HB8ZO;N8GKv?ѣ-za$=z5R0ֺ)+P S005&h~.BzQ:Pjmo9UIy4e|$IGv𡝙{J^"tܢy})Tt؝h?W{qDL>DxF.Yz$B?1jZ7?~S-"B>F&ou]~ۏ~f.U $ uYkZ KN}=ǣ) ba K"Pu)4P4)ݰ>4R(FsSC`d.ӵ[Wchٮ?"zXDꚗ73/0z'TՒ32WbH@Kf֏cS )[3"Xia i<6pf8 g'xK`!8e&__.[Jq|y^^_Yߊ 5R,њc[1Hr d-` *"G]WũN̄D\J=+:|ȱ 4#ijNv֧ &C,#IL)+iZ~gS~ qB[e$'ɒ+,ltGD3s]JJLmL[>#s9HAp_nF̭e"r^k>F?*"E$Mg@(`㺮,}76'P]1 4< sCu 4P{REL8qlBCvN0P[䬔R3&?s򍇓tr\"<3ڇ*/uZAsH̀s~2^_9kߎISEP%ȥ3#1-h}Nc].XlO0l>~D˺mz}z6^_mkؖEU__ߗe-\~Fx0q)xyy.~y~V  ;39c`)D"HEnǜ+B’mfBmnC͔:պ̡% -&gx+d0֎Zuٞ>DRXJbm Ua4wӑ9Ą{Z pa6fT)2-<,t ?jI$BH @tb ^&Nùv;Ԅ0MGiary IMz0q\M5U;M}oMR1_>|&{Cv矟?|].}{5˜8j@KoRJFzm61`91x@apK #` $"$CNA 2D 3 8x&CT\j%wRriژvOOU-`pR}ṰUKv3O6I+<[@Ȁ487dt}N;¬x&Fx ӱ3 rKYJ͟]{)}8u<-*_.׍S!qz>]r-R",0 "F9ӱ,T`ܖglʯr4%84=aيJu]R8\e:HXd]SCUUq., pYק?l0V6̖f>#=%u)9&Z*E`]R$c4i?-rt <ϥL#"n+GzR. x}t*uϼbL15':<#M)G͹,{Lm]ײ.:`]/V8~зoÊT{~"@$ zDgXr)i,%]DO%X"l,f8@ޅ~N'7&n> HWD;ZVFl k<7ddȘ8H08ES@4Tn˶fGBDzhK&u d"(L m[lW8O|]s'"1? f 2g LKa᭏O(1#;@:\n&l\uLkM>sQ8=sRa9hRc1֊4w!´Y0 1͛!] sWl3LP5@7S ! #ez?r+ntZDH;Z;:JRR_$"}&~sSQGk:4jmP;cxG#pR><_&5 ` !C!p#7>@>2TKͼH 4<1YHK$, Clfސk}iٶZ("P8$r!@S5cBf =UD @PjUWwSMO4)4#Zו"3q(_S(ܔ IJ#RbNKb xL#d"0+CZ+~U}!Qe뺿\ a6R #.m*OnKx@WU۽օ ~\*{@k]%p&'Ya@/Kg#̝Ob;@ϗ=|^K 𔙧]k.#(%=hG 3QYN:FaJlVo[("B,\ Bҡ@rٮK1I2Ū~̭rN-¡s)ɡv4kE("j)e]@9 3ͼBZ@<|YX"],K&K>fnwR20H~?/pGeJq72ᔥN5։1ջs#<qr\әUI, nβ?^DReLt{zdl:cbd{n[{]Hׯzd'Eey~~[_ں""%gmyo_[kӕ#|hVky\{/㔅kp7H1X-wՀ֤e#RJsKOrLpnkF?hC'Ze]?~f5 w9l  <ˤ܁098[XjR9l)}C9-E~sɔ3 >3Q`ݓoDyn83=OD#{w =g c @\я-Jķ۷d*,m%nf˺~q"5If&Rr8F\~Vk%m:Fmۦ0 eL7"DR8̌\}% L P)Ĝ]1 0]u5߭$&/L+#fvqN`x.|bʢ,F63en4gt&jݔÝNFlfw)Ds3x,kYEݘ?Ҕi,BFCӢ2OtbB=Bd\0ߜ

_W0:Ƕlgixd(CSLn'r0󿣷ޛ#7fQ*BD:l.O)c"Huh@6?4zОyX8:kf5\F_]=?#%77ec9onR >FwߟMF#K` z!f`rSL+Dq HRNq̅DffQ^\q h,ENCP33 CpYԆ# ccnt )L|J,JN3EeFs}?f%nƠ !O7 &"Tίˢ;z\ jΗZpτ@i_D?<*3%"윸yVj8LY9NpǾ'3O}\$&ti>S< w\"@n >H z8G򰋀Cxg~.遐; ^IDATcq_),YFĶm<1KQiJ tv>,i׺"bSݏFT ֊ȗ/_}o///R})…xh<@Us*,?||ӇtMe.뚩j%ift\]fp5!*.Kg&4bݒZ;SHHYdukk-Y#Tͬ޻Dr}z#z)q@4>HJ B5c )' j 35,eZ7hL^Dz)YHk{y3 'F"f3.=2:1цn.A<m]z_[%EeY>.GW5\.mz{R%\agЎN3a0KO&I>%z4P!ꌇ̝0Lі{n| pFs嚺kgVZx~#ÙN*;p)BzpJ5SsY9iRxXx8^YN RNG;^UU۾ۦo[^f_R#`]8ܭ.EuO?EDJ${P,d 9&2-N,Id~Qah6~9)!+%DnfRK^@CÝ1zsZUP ]OJHBnBLJa!Ҟa7Kf < "a1q1:!7HRMzR*F<|cB;2D."DĹQ*$Tj9zC]sn붦OeuiGK N"Y]W5{{_J9,9\흋d4Tm:Gڽ30 G!z1̲ߴtIiTghH.i<X'7(鶐"%nx~嗼Y#"%iWBԥʈJ`6N 23 )LL6z ]ha&AHk #nl 22-_"wS?[s6^cB ʜh}xjoOݮuYm.2BDzY[M qVvlAs{zl>ADdzeNPLs1FPr*1RwU͋!ٵ?\~bv3Mwoƿ\k]͔w~r>zap7 @8hn7/G SRڎ#þ2x}LDC!==?f~{1w&Xkn&s#Afh&Pyv!0YlpS}t ͷ0e ) ﯯ!"x||)r۶{U]î|lGP$).b}x &=ԇU.Uo?0tdB~yR]St1$""cOODM 1>ޤ,)"aXJ@gZ{{Lpb⬜No6eGymxJhS\1`ƈ0  Cu:';K]x 8!6UIO9*afT"(oDL$""q1.͆^. xi]Oi'"SHi ?;/h-uDl;"r)BBa; $pFjPxY3u ¿^=uryfSk@9pz X'+bY. [4WIDrkA"ȥ$ Б e9RgLۥQ*mV($x1lP#q-APn(q,b~}/o߾vnXDZ ~x\_|EຬL@]>+"q\Ӟ *K]mE0 k6RRsOD$M[@ \_b6h~$-a!GL% 8Iw>Rq0aCjGG9욞ۺBW' яXJD,Q A$~n0#bo!("6ضMCHLy#ZH֮tUUy|}GSVZ֭:k%nRȤ<<}at 'zt .e@~lQg<]C%lwm~ٮ&4+88xe~DY )cP]0.8&DnkəݶK뽖zy^^}{~yZj}{lZ}]Ja$~}p-zo/{e>}xO@uY OF$ㆈNq2Lc:,YM6q\.؏=.j.[WgY,hN玐t>"2&m¤dobt~gs'&?d8ŸS^0Sy]~̆TuԥWjpY˷o//rPs叿 e] s3 Os.32œ^YO!iaH7qza'gpJu]>|\\ !Z?﷾C ,rbpdy(W@sM/xt6T|L6u{ooc pPbdTr[///-Tc eY-RoߞtK-ˢ:7}v-uhǁ,oHN5<|# :n3~%)րܗ>H4qf`y8U*e{rX&cGJ: XT$Rٌ% I&FKvtjl܊p= ӌ[^m"dLFٺrNAxbܽ vJ*GZO,;15͔"$BœE@E{|Q~MS1ES+pʿ|@ZjM^J sMA@4|4u%na3f*'hA}.h:X.UJ=(wGvMpom2`Ysj:zknwuss6qwe:t Uuqh8iR-(,}RXU3O6C`3I-?ؑ媔Ufr53O;APOI)h9|~%{J&Am6ƺ˺ ~" /_zeڇnI*&8Y^{7)ˏ~I)o:ObY4s"(*&Xl8w|0QS(:uay۾hmVGhˆ2ME8X9DSŔw#+ ~=$EZ=z?;RR܇*nC)gA:c cD0!kGuYR zC.M)SH)+yiJ(ɉJ#0 D Ys>Um/,D{MUf~G".irε'p!X Ti$ DT 7 ե'"Yhu}$~q쭵}0I35'"( ;=룷fc@BsC )~߿~i0OP!&gh@Ln&<+n$lf,gQ @ખ}:"e`ǩ*1~׷ޕxėߖe~_Zd!˿0-jCش_ T&͕Gyo_a[R "㇏ {.{EC`Ij:Ծ bՒm930 3<<4ҼiĜLCeGSh'DEXfq,ER3fRzH)$Ē} ϥ.οxM`c烘m~ >zt>ߛxl.N{WTNt|N̗ٔ VR~zlV뺮zn9łY%Wr T7No]fϼ>kHn᳜⹡# .xS8O 8Z/nku]WbQ˿mv~w{oM~i=ER{kY%pR elI)DiLdC5*f͔49y&Нoeڡ{^VK߯#9d 1e~6zkhم0晣bsUȥ@<`a}vc_APJ!R/tR+yfMW+ǫ&Qff"'x]36"0,v,~u-~orYE=]DtYn 3קs1 O_C\vZ=bvlixa>S@#,;5,!ytĜٶ |T+ur0|#nWf~?fd3#?Y1)b\zk˲L!`-,ts˲ RJnDRؚZk>N[`:XbCOYYJ)Rrn#-nEFϴe"S 'L3ҟ(p#s2 XLSD8C:2m!/EWDD#$=Ƒ1M;\Y-#rI3(&qmD ET53cS ZfV`*L,6鲵v`Pz^__q],Qk%b?mN[>}ˇ?P\]ze]^EDx:tfdd /82ԂD̅3qj38svUՆ֏ct35$Ñ2m]FoqǾ}o~Gvw uGkGDvjXM0̏>Pݿ~{}rߏ֎r~$*;f#DP3q4=dHAGԕ-H2#Kal\uVMx ѿlx{.]Ƕae~[ @XvBH:z;n)pKaP֍J=Te|e[E8?c⡣uBBbtPQ)ȯ'3eW-EPG=!Cg̻( tdfd:A*7 20[J }쓈i"DֵyrFsFNI<\/ic^L|oƑgu0 `۶e"K"8!K'e[t7<] #eS""˶S0Xkyy~"~>d .zn/iuYT/_~۶].zvr+%Ch9$%";e-kŇ\*m%i3|TAzbL=7i 73Uf v~[jSkj`hC[CY fm1{@P%_d M1~)RR:B91-Nt"ls%~|fBg:7@RSL2_ZraZ+3ZLE$ !\Kރ)|&fCGʞ.ʈBď?#Dm?|,u2bdzю_~k}r>]یԏ>gcȲu|ze[.Y-w3%)I$pRh;ҬIW#+Mt\Չ([򙉇xh]}W;><|$ z}Gk:Lvvw<,_c}uM9!RkA"K9 Qx~:S(Ef(V3& AXjj YjuQ(JLUs8:lnu۾TkuO紮 )"^__z͍Ǐy26Zjٶ s"Ho]cyُ_t8ϋW<0!$.OYdyLR誚4"zRF-5{-"4̙{WBXl &DT$/B]|L3!p*05iyDrhni}u] |ՌZkϟ""[*bTf檖P 2)|`.kCGw$i! Z \fCfE{5EOC&DѲ͇(?>yQ{YX.2ueRny!RueA&lR9<LjeWB(® ٚyG؏-K]ƌѻyoMOO :>}󕈎֎)v~܇^?~De]۾uYR.׍X$Vȶm9NsqRW$V7 PB,˺! A >F]0Tt:`eYuI/H{c{߇ͼ;tdFFmh{Cu1CSWᮎQ|{/ߺLJ1msʥۅRzuaF^HBhn̅&8=))3z&%T$B j->Z?˷o?N氙U).kv$R.F,__՚GÒIaH,k)ӧx{}?)` En. 3Lj%'[W {4uyh1!@p0=81=kT?,KDj]XztՒޯsn<ƐRXON]✒~@01yCc*OY|0"#i1G^m?e ¼n!p'A("Gkj:!0J>. E_ Nm?KlyM!D"PӡwvxknUSb D_SDl^+nm]WI_(F`.8SqU\b l۶IS6&ʑ;R{@oCj*i>?x{{˷. qYӇz!{{DUKmoR8\1D'P)_PZkIDyҍZk株Lh cPk-pk{{ݬc`CW]8LSD_}j>#ҐDDlmmg}^;eݶИKu9,4f7H2 $ LjpZ VYm{ݯOOozno-ܲ<==Ocvf݋HׯfDdWsxn1F|>$g돽}|T#BA>9%p9вsE?GF)A6eA G@HgL ) L1RK{;02C[~`~e\!E{ 怈D]6-,׫ADO!8 "e?w3q"1=LD"\""쓽0~*Tϰ"ƹȆ˹ӟ.Aa?Α#e7"f'hD9 2q”ịu+L{_/[J J=mE@F!K @ba)Rաmk."BvYץ<_ KDv|jJ%&ض-5`vKvi,ew]|b=w].M"DZJaP"f&?tb=P3/uQu5'$hFXzC$RkB!H$2̆GwH?  i<b0`m.Zsei{Nb  Z J-H}җuy 2 U[OIMO[9WA~}}{z0ݶK-;BY˅ҍ,ws}~y*o<܇rEHRe][ S;LAy" ~\!"b) LЌX<8UA'l\i cy9?DRcfpF&); T* E}ȤOkRkFe~{$iXS}4ӕ o*JofYzSW) :x;ɾH3̊=vs4>9 0qf:CJ2Eb /J!2Nt&pc1H?uY<b'aS`vF,n9OG*A$035µԼWc֍qReFe]zaZJuw@|lyazzCSk\??}/R_-ӆ#]./oߘXdҥttpUݶI \s"O735nD\PUSbԡa>7{M iBk*RkE>F&l f$gp pDBPL.R?uo~Y8v81cd%5ļLHuL%O܂4/Y뎈bmt Jvա<8F)ߨt^z5CRJߏ1Foo߮קe[gٱO=,B}g[]qm%Jt#"32>֥TyLzOw9KI͜O9Tp=M4jb]siɓ8ϱԼR "Lf݄%MM^d2(|Ӻ;1'Հ|*YDJ#J&2#!uiuYLV{J "0 dG!"?i{7M{3e޾7fQ*DRb^1ٜ_~Q[8@V+;s\ t\2hAd$nFOi&@Z(I3ۤwbMU41lY#SD04 <<<#J;F&t<\MvY v1nw{R.ZR<_~K)+! a]ݏu\e]}}o~{.^6fZYa^ RJEZbw*8 wbz'S@<Cp˂$DݢRGTQu{M )?BtlED(Â1 F{` KY DTJ-ERDl[;²"u0 0 5RKF͸R "RP$&_̐$Ժ.i$@p3?n:Y 3pgfunfv{z~[o"~<,~12ܗa+QJÏ}4Sgf(86񏯯2Gb{ǽ姏w8#"a).GNP038dͮyxO}zo[?R>1c3(M%aD[Zi֔'2&)yx̹{~b0{敀xa楈n*E 2z D1ƾ$\jbȂ&` M0eIUd\%()>0Loa|?S_ɹqo5 wyS=m;'뻳N"c /P\Fɩ*[H!B;ө=,$ cc3,k9bDkM?]KV$eLnD4.Bx&ɜ[?M@ܡiˑ$eRU31dVVzݧ^}Ϻ}2cpLQC:pw`**?ބգH\03c ?Fo"c~߾~og}Öp_/ޭG"z?_rIeYׯ^H孭$&“X}C08>w_1;@.pT`}G@șowrYQkuNҴpG?>G|}Sלݝ oZ!/f!2E$$ã:Dۣ5ܵ8;P Qh?ZoI^pև1gZA$"YGq hUL253{QۻڂjMǾ?{9gq?.K>v%Lx{uY9j9m?tt/_MEd%\>FrN,Ǿ ELxkc ,z!h㓻D mNN,e?\B6c$Dn‚~d@E 2ka̢>?N}8R|O`C9 :!~hH`I@>F@Mh2pwDHDZ3912㳯 ֐3(dA\.Ls9(3J ZʂL" |]!If>D$ q' j?Fo#1\޷oo֮Cmc߻jWU@sx{_׽֣vNr>Do 3~]G)DNr*  \B̭a8'$5}?o''2Ǿ1 ph>`'LgG TVLe.0*. ~ױ\9z1"Fk _~ew"Jdh}jG,^JQ3_,% ߿|_Wyn}Y$Eej=p% s(c^2;0aSb #jsrgz)d]fS8+`gnzL^t#$"I%ԂhGϸss RLa=l^kaFNO2NRш0LIuLMqsbGԣ͋ƓS'6f@ l٥Ha lSx(i)eͧGO6`,q؈Ɂw9 54 #31FL`f$D^6 RGv11."3{9rJ>" rYlaFlDlPrz> z^s9%I9 4j;Rm-N9H9 Cn!;!yNZ]-$|9Ϭo/Akm)O{km'6OƱ/LeD ŌyJ=lڝAobK϶RrJi]u]Wur{{d^P 1 uh 멍D Ǜ?%>8炶H1sn;ӀEb#RsRdy pw i!&"v0˗o"3I63D/.et5{ooomWnV42!3\Ha_?Puu}ZkM]u,%!V*-DBa) VZ*9 jƜM-@J pZDǏ}3eS.?䔆v˗_>}seI f:Fm\{nEERʹ9jCXRΥ,Ĵm[Ǹ?ވԮG|\.#%q=XᏣ֣41̘U3/D=r鏡n4bpzkITZ___-&X31a0֮%hD{~'"0 ~]'Xj#RTÇm6ܬu0F1kkp>{NE )'FZfLѠ6ٯDXhѓxX@>" )ebQʕ$=;6~z ,KYR6[tdz!A"&Fg,$L鋸zYN˲D7nL;d&iSJ=Q US3zɋ㈤د茣?E`c^@ Ńnfr0G) tU^$O'wngtQJ ^+euGu` XsTA՟ #g,1rZweNYҺm?jէ[;uUZ+gZm6_ƀETˇW_ֺ,9_I| sɵVp צ] 7w_VGkա>cK)f$0f}[z`{Lȵׯ 'dח$WA`9g'ER#fÒAQ$RhGII,* %N0ȴ_J nݺG6Flfu1V²&rBRUI=cT99{*Yc;l݌q-чq˲$zl/mrݬ#Z!?J)pҦr^5rC†ČDSܑbO̦! я7p.[P' j=1g%gضux)% {CHW;G TVUt9akY"^eF=F|CzQH~sڵ^YK)mt-歵8Kpn"qzonQ=#(-qQB#11M8jMiV8$ba>ڑX(@D+trC; 0e%c8%Pm sNY[WuYbg*gMy,F0FMU.D&YAEv$uP59p1"{?mnR};R«,~kGm^%똰Ox<w-nJHRxT ²3'=f4@5WCĦ%'&6ZkkΉ@!5j#vJv߶1bމLL)˺ }|YݖoyuI _o˺N`yMfok!J@r:.˲=Ľ1Ԇ,#ЁDc1A^[wuYS" Hλ嗥Ĵ-{<ƨ ƑD$2ӔfIA1SB\5~g/ח+3\=#2=t^7B)T-fs6̴rvʄ ) !9]P_%q@u\5 (-jS8|!sUaz<{Ql2 8=thG:.뚘A{Nj@D[kRԴP] &fs۶M$? XF:8qV<H!oBӥ5a-ruG3;'Ĉݛ/8W1(a>ΞO08ޘR'\]u6LCk3byhQJ V=)gdl(< {< ڻ$j:ƺ,9 !֣պOey}>_>ڿ'\/k ?rP%7jVJoo߾WKmmDMmIOu""Ƕx}$ hzgUen1( uǾ+B^>17׏kk~PJ>u傎$9!Z҇Z`? #,9h*k) "%2@a^,Q߷YL͌e &ƦMDk)I(#r Gt0L9QގQ+޺1TmtֵZmc].+!2;|ZcC("{J=/$v0Fvu]uY|om,) Och$fBoc47aZY{IIfǾGpcۅK}3)$*naF$\[#fScpTͰxtD0znk=f)fG19Ű7|)ĩ>#sb!L,E"+# s\ptG9RLqO y#0F 6X/J@oMX U0f \eS# 'l|w<@$ Ck"5D, BLc;ʄ?GA!ѣ<&1J<`pWتLڶprr@ Eyr& ky1wX +"N;:%gw>(l#D~n#L%緷>ڇwx{{csűպ\8~a[9u]u*|Rqr}ZO?K21Q[jn9,IQ:"P~ߺ,03>Dڀh^Ӳ|uDQ%'a,Y\Rb^o0L$=s91'jΒz^. n$iYWs3Ru/ܦ<oP]֋ )㱻;y.K77udV0}j~IÚ/_QjEї07v{y?Z#\לh.LY$^֗u-)}~6у\-'aD!ZsĴ$)9ˌ3/'RĪ\nנ~Y!!giqW3G4u}2"T$Fe-ō$ 810xN~DYchz>9)!`k5J:jvEX!Rjq Sd)ln(`EJLv-*2m|6ZC/u6$~Q P凜hH)ÜL3 Q,Q#ehb3Fz$ P=KK F\^)PU]%hE"J. 34ngS=̄m:Oն/ˊ!b8zYv0?}WYd۶~{?y8RrJkbYkQk̜RaY"rВvmMJrF 1 y8V''1؆m{ݎ{6{0 PmCtG(/K)KN KN^ۧ "BLDbK˺kakÇ-C9$z#G G7uX4=o^>|r<-DPG7njC `tED&{(͐H2wGba1u]W]ݠzϒ $[^oسyK)RJ%IdZ Nak8ZC c v 8o:8MEϓ41#huFDVL=N*9XUnގ+hƖ5~3O<:ױOr)P Dx[k>,1qoW"e!0לqJ> <6Fb {!-|SGwo'(֚ qH1>HE  LHLOWp$0j #F3#(lٵ֒!89Vz~ h.9A;p;"#՝ʲmu TANĬb?"ǶLnmWm^WYEԣ÷ߗ%YmVrv_z^.K)3cyd39E ^DL5?“xlm,뭵|L'3\bTxu]ʒRb3yN@9f9PJ\.G0hռqJq^n뺾>lXm&d>m]^r+yɒ˒AKJIT{oHK|Y .9Y ve%e]וײv$T gy{6'&", б$뺔T)P$/j$X'ݰѱښ:9~?F!7"?\m5ʲ흉?~`,7x5jm3/|qt[2Z/9=Կ=6,e EY3,E$@=BLnVr.",'9X IPV9x ;! 228 FA{xB{bx;5d(u33DtG %U" f}ڷz,,\5,|H$N9DX HN6 U#/fOՋ}-zY'}9C!~7 uJ C'C@~ԍf,kL}0܏# ѧkRF8E?kfqjS0Y"Fq)%8/"_UIxP joGzg7534Z%e]׏D_󜠤߿?~8ZO=05HRRJ߾}yj_TU'?v^x|{8JQp",!s} oehC0()_^^DhΗa!,B91y- &6DSJqRbޚxJs@ц庘iAI"  $tɩ MגKI(V,uy *tŁL,9aH)Ӊ:G"}۶"+px{׮L[){򊈽3KYbOhhꈨ*"I) =q//_|/뺽R0v o8 OE 6?QՔRq#{$BOB">>OgR" lJ.ߣ φ34rИ7="6d.%c8ƄBw.K.a0W0 @rBŒמНqKJE<0snq\ЁЌ6uӈO2L7oM%@rDO8"0zڙۘ!x<L%$ D$N ~,p%HRL!"M>L:9RQ#d ptC8,94 ft #8E`˄ZM"H-$Hחm)u^m?2T_oWG`s}}}}\rJ"Ckk%$xyz{Ͽ2TmGĽ߾ .k Lv~Ce9]e ۸\6p1t}?^SKɈ?.DkkG_^[nqS: /eoQ߾Z]UȌ 予0Dq;Ǿo{o}?:F|v^DZ,0 #d5ఘD&ɹ=v|0f@DdjjAo0|R3jZ=w V#8’=zKHJ<˓`D!œdRbfY"´{Jb 9 Qgy}}mEZ;'UM)ZmK2#T?O?^^LJ""jy4$|(j:,icۺ;L3 rh@s}̛sPtSՃZ?DVJ,B3#B\kmG R_֚J&d4<c5UCgߓ\Lfwpъ9dREY֢콷$Q9:3SuI/Zc Zdz=?Z[kex<{\v_]׿~򽎈CSkZNM<v?F:#Z- 90U~}lQ[U:FuUֺn1z5Hd@@gt1KٶRN؇ ,ΔeYhR,E4hnGGj" 䪄6T5%!DfݺuYrZUQ1mk.r[K,,9kov5jGY%τ}LDR! jkPr;%qձ1`"Y #[5N4۶s")C4=X (:; ƈ)" I8@ тZ7Ӝ3Q% !3s"aF1ݜGm¸0H7A4 ó#b)̄u'b"D^J 2㲮8ԣ#6L"N  !j,n[]tp³ă@?磏(fg iL."vn'q/DD|V@߃K>"Eֈ9g N-<3 gԣ${L"a!@c_)%Y8hncc~|}c21~2h岎1t~DUs۶RJZGz?~p{fr\KfkBL),)%X5摮uԚ2 у3Q!@;$8ps0Gj! B"h tu$pfZk;#‚"!FRd't\ȝZ7;[=jct0O J^@./˧ ai/Uv)~5^Xms @Dc?ucI!}>,ƿ>TA(%SZ[k>x\뺮Lm[ݏIs%-f:l$3[֋#Ƕݘ c!IQH/%hMKOOU p"z@F\Q$qS]{k)0Eco-Kfȩ)OU9a(L)|K +`p6 P$!H.$8gOTsCD,%rLM5iN4,QRO4g?Ζ0uwf8)KD%Ĉ)iY!=Y=;~)aNrȹo1xrJ{=a]e˥Ȫz Zޓh_㨵rcIf<4g$Y-]o۱Ij_aMLjg x\C=!P0ggd>h 50C0p`r!Fwp8g0`u+8?a'!0",I8fx[u%{ r5YbZI HzuDUU /_TSιhlֺ+1zS : 1fփ9 ٍ$4gJh႘Lpf7S;P1RxG*goK^# u ma-#K.ZkLCDY{\"kD3%KzD:cL}F`@t b^E{C&3sjd`rB8ٛfv+Ddez'w=V,%2kJ)v>Rᬍu0 ad9,5?ӸL2O@Wu|NVӈ4k ~ΣP3Ys#N[PR177" #DClH>Հ`k]o7A'"ʒtZRz-窺rYjf`M-oe`$qow_KVaU$r[S/zcF'hMgj@N"1H2x 3KeYD(homӯ1* 1ZkQf!~y; ˚Rnq*Z81R)Mp @ Tu$?ZejfЈ2coqAǷo~!BuP^.ER??~Hn+'8~>FmC[ƅ%Ni&tcdt%@w,p |0.[ ΜJ X"ݬ2 $:Qb0 Z[.kK#/s{XZBr ݅ XzyCfiΊ}#qjyeEwOݙ#=?(ueY"Cc"2#G)eYǾ+8ӫhf@7 A^ 3E7`IK sj%pM,)P}tQ~Hep%Um:b'cfw~kΙȆ۝HzD]ttс 3JF%cJBF?s24}|p2ĉda#IU t:X#Z0$Tbq襨ROi!Ds F6=ףN6GS>sڏq5ϳ uD}sg93yG_~1ƺ?c]xUQ^0PF %+N_΅խnrYO::@F 1/@hQMgOȻ)ޠ䂂\Zoa1y;ѺzS* 3Ӓ,r} hf:yqDJ )'HwF U>Z B;(0k]\\j ܀a)׸Bt7jfr ɲ۔FٹDۋ[dGb:7^ΧFDd 5Bnf*%56ۚ@n,8Sl-~}{̳7ӰN]]qԖsZOru;]ގJY#%X@Dp6(hI.ڎ @GNhUz]},sY}gG=RD4)/^m3P NH_lo@DF?lk~]Rw3Ki_%Č.i'4Hꀵ~\F %K1@B5 p&ȌnȀ~EDy9kޏԎa#DaBJ0N]|Q2M>H9#m]z[{wE"vÄ)tǣz9 .g>֣:1^,3@m[t@R ؗeY( Nuuq|re*v/$}~\0%!KmO>14bNcCEz][{շCDR˒lK*#;*YD!@dوʩHSoH~8Ja[$@HAZ bfҎ8(xDDGOtyo ">ԑӈ@rep% 18wQ(,1{`DSwJpq-~ &9s;>?}։`Dp,O_ϒ6v@",`j0(?9+Lj?(片Δ!tٚ8+{8Oo SJnyQ/Bp h!ڏBF#::j;ָcZRjj_]$9E]j_L"}jKYD$IKJ)>q- fmhvg1 .OieUL)'P%2 RD񴽇}H f”0H3S,"՟a }ێ6ƾ]̺3b!,)-K)Yq(ٿ@C`~}!AuEU7 #Q˲DJ նm]E޷=qT!~^.﵍c?:t6"zhtQ[<6&Hオ,_˲Znbgos{{D~:jmD9RHDcqtxkx M 2 0mԝGP[/ #cK)rG0xrRO bte,s :Zx ۺ#Y9'4քX=:Usiu?EDjGSswNѳ)TS}^!)S 㳪fϮsb?z?qBG 4 ׮(jE@."LV'm\p 3tD ")fЎ8Ķ3%2%~X.VGmCr8:8U~~*',ޝS"b\jyޑ&D%*tD|hݶOAu]?RZaNHvԜ믿?_я~\u%jߪ#!\8?c )qQ{}/}Wc0B:81FIɑ^#(&%jK){ɡ'&$V) Ku64G`΂-`QT~Bs:P\B$EBYzWΜR 4pSB &2[S[N.Z+'FSlFG'vh(!k> _߾ob]M>ڴv ~&0a;3}zqAc;K!œ~<IDAT~c|һ&KmXP[eY>~}}Bs&0Y"rYG%XFOiX Czcf6THٓ:,Yع{7%1Z)e:~KE2"v0!8ufqwG58!# $ ik1O՜nǑRT= w@^kIXע[jD4р~\FfUBsJB)8~칬' gA[l.C? 3pPx{'Sjf1t} ,ci)U3 |9WIvs> " y8{_1{2ؾu6zDJ~*xʖkc^$r,Lqlor^۷į dfv_E$6/_喎scć,ϟGsD\ŽzVGǏ/f^sArEU=N&"֍D҄* 'D %XNE1 ¥"̉EĵbS {3ގRJtKNHbC]3[ZvT-8AOL쮆殨6:mhΒddE#zE^:z$),3In:GZ㮪fQ{FՇc?uDFx> F"Dڻ-lYϯRr)JeYZkM~,pz{LG7IiT tRz3/B"6Ѫ'eYR*OBL$]x7ըxLRMzh͡q؁9 31Y"=u0bJzYBa4 W1ϪZ;JY|D#6,$r:P%͘)Frw15͒PuD":- tXKmSJJ)1#X}/)dB2b1_ Pp$, "tjL1ztF$0Qh@9ݻ1Q+H+dDN71Sõ;x21G! \! i.ĵB9jg3!?!wUz[,/HqL9TѪ6F_/r)EX?H0_v l.!ږr:\~a[3Bo#k班ݚjNk=bH벤,IX{IWcڢeYkf=эkY afdz,)u 1SI{qp7I 2& ^$v{}]eab"N9r.%PQmo8DvUD$aOD36,, @ʲ\޵Bf~s[W156qT`6htU݂m'$N&Gt5 y=wS{]Ǐq|Ϸm[~yyc,63˲¨PSrYIZ룤e-΄At(!U R HfD D:X(!%D*3k>8mщFn=S8c_Y$ &bB6fњgւF) K Bfh55誣藜l B'8B (9IKN) ? pYXBo)3?cdD37T: Ĥu"R)O*#i&l\UC4GAٮyfi<_Ohpj>_5OM !A'C}Ff&Xfӈ:"h;bM误Du´ՖKCcI\h{<ܵrF㇏D,c4f~ܽ}n//Ƕx[$˯Vhx<>~0`b˲nۖ fx˺[! Ĉr$chA)ID`k/zYrbYHJ,\0SJ"Dr攢lny 㘛 'Dd^t*A$6C{6z}ߘ)ʄHa#BAHJ1P">Z;N)岘}eQx?VmcLxvt< P O3)@,Q0&9KJi߶r,04oooe~-/˪{335UZcH ip`9Dy> â͍p033N* C~x Q3wW &!Ue%g1DrOk-HD|H?]nI^IJXrn=c{ ݐ80RVοsTvCĩ!R7u5:YO+^A<)ؙ8QyVs=MgSO j=F{C) %3G??2&"Rvn7zEpUgzXUR9zD&~{۶]ɼs^?Q?|Gr!X㇏^[\ڻ#mǾ![NÇfO*$vf @ךC)dr[K),Kb"X2$YD⠆,\qX|ch::)[Ƕajf]Unc[;vlGݷ][OY>|xvԸ9݇a[ ]]sZGwr <"sFlfۺ۾caߎxv558Z5 =a':£.„8Q1SJ!Y>~2F?z,e}<h?,Q?in;,EQkzYRˌFk}8L\"3=sN)}?rcú*D;gG;jѓrFR?8& D`&mMՔ mX3>@0aD$DBAǘt-sUl{P2TUѵ{BS#1 nnB7m_&5۞Sc>X6Ov[9ȏÎk=PbnR2Wb)n_nfG>ضcUb~FQݷ, 6fvSf4$}t7;m{%lwwqvJP$F%QBY"pKeFh6\uaǾlt6w=>Zmvhq譎zk`~.?}jR:h $TcI%x't>. hutGC"IiWz&vb8\ O! 8 KsI8|fV򢦗uAB!ߒp}~YrAD@Bt^kke =p"4!0ch'H8m""cA4fC?+k@Ŝjpb#f݆\%,~̩5`fBC?$)(h 9G™, %V[& 5I$1=JAwrAow~3;#;ւdj`VL3~!J)_~c; ΠnhQ-8FןvG$fTZS͡0f#cc~+骊$ޞj0 !2ul܆;0a}~!Di8U$`6HUڵ]/kd[^{KJ/KӧOHJڷ_*8o˺r~؏HjHum]%mvo}^||SWh.G6b)FI4‘ݽ:i朄Qgd,Ъ5j>cA\{&1 BeROuX4 Wܵ!`sʪĈy) l {c{@,[[k9Va +#yN) %H[5V$I  $L$#,fVS̜Pdy=g%'9V%>kޟJx;p Yy<=oI)>'O}Oy8׳Fw(~Y"(^<.j?Hl@ݡYhJ)|)6^lM tn~}-K,/y5ffrڢ4w w%5]~Q7?mD]uݶ .K4%5r{hFZJGYKWns*_NTfCwSV[zYV,/{=H[4݅(k'ʒu+B|T}ErkxR4ʄj%"1ƈp`Cia!"I#"~3m]Rc,N 'qobS9:Z>)RB-KyIF܇G>>PWhkL`'M@Ͷ}O,ԥ_}y-b/ZWu~ߞeV1^R.5]3K9)ZŘ`#S!;R' TGP9""2i0H{,E')=I9AkyJ4}y19({IRJI wȓ t ڮ5T^.Eo A+NFơAs+(e:~ؾ`GeHc@1L-Ą03PabbU>o_?f&\?^3"NDO\a 5sD:?#iZ͐d L<%@>lw01Skm$?Ax192qƑƙU&½ֈض},F{*)cSck&eY_~Q&ֺ.Hz<8Tz,5޶M^bfm~t]_,߾|Ñz]1Iyl80?86Pb~ov5].<A[#c9'p'a˲$*OjO,?,RV%)$<ӯ`:EqR^[6Al$(wh <-KvQ 9=]D>|F^?>.`5ևA˪G^EFъ.S|[T0˒9I I@hq"hƒ.}^RyYںH//F"I}eA𷷷R޷c?vѕ_?| m;FdPH|<(7咄u$F`ƜQgu"K^_<[gf Q[8mkk-:³Tg1j?jo>1yuTg !(GSHGca$NKVt$i1t,9G7C{އꐔSֵ5hT@Y*`/9/Rk"C&a N$RJIMH.///em}_Wd90 ĔѠ aDQa"I$DW_rb$Ȁn6ƐĈd:ܛ%'$Dbf*"B.k9$G'hK!tH t&-F3Ps v؞81 ng؜14Ͼ'⤸9cWus#n 1tȜIB}&-9r}S,D94nCs*01^ޮV1la*)'";v|譾 Qj߾}{}Xk񃩕R}Wwbz{nK=W08^ntY/"9>8b߾}3h}tz8Q,?R,rUO>e1\oID *.);zh&.„G%8N x~XDbHx#$uգ>~cVkxL͆đ$(Gچ?`Z_^_~Vk\.CZo>8n/{(4zXGTǪXjaHo@l#8!I q!1Ǐ/_ۿ|ێ>tR$-~ eNk>("@aŁp48n`7탐': ygBF>%1M<3@JdDg)+<{>zS\rb 7! G Fa!1P08#"PPEὬ%Xњ$&"~Ԥs:u":Ά*_9xhvb BH)A[a~6|5yΘs?ٍٛ#IhO=OQKўW ;8^͎VDS.BgFjA#8rS#`]X@Fs34u#Or4Q|$)*9TVu:sOWUVVD($tw3fNFVLI9}0>|0v{mǔNZULQE~|VwIL|>ZSn^[d)䶽*EK=ai0x8v+F֧O1׷"sJøo_EǏ)!)yJ) }r+=! C/> jZZ+KotήtiֺRkiMժ)+D5һReI9#D>4m*Nǥ&( yeͽwC�b12/X:-yM4aBgf@{-z`MؐAIr V][K)p4aGH1覫0d8t7CD݃滴@d)jg Oh-%~<Η˥˧OҐo[=zq8HmRfqt>Ҫy y~qa_jDl?7Gjx RNdpGVs3Db86`h@"}ȉcî4;0C `E61Hc̑)l]* Z! 0Lċ9gꭢ|p?BS@B5-<̀@aQ\jjzSDɵ{Ty5xfv(h!4 b Ć1{9 Y) sB E HU"CCD6akmiNӍD J T ޱt-N#^Wqb;цߡpC< ܉XG_nI~"1\PUE`Bv\{ 6?NSQkM9;[wK'FpŬG>)q~U)Kbw3!ˏ{oߍ80o{&ny(M[ku؍l~U>?aYıM/L)祕חRp:\9HoinoKR[?NR[w Lv\|`΁SJ<C1Cډm8!k1m nDIԠ65b&R{MVR Rj2އKw!VqZ9e5+wO<,[9#XzWեV!{GZQ&ni[tS-uCu7n78nnz `d9փ-眇X[։YNRJqqB1bD8ڡDFB50!bY}~䋨+T֛S 4 *b`%~oEMaM ~"3nf)Fqc 1FiSK)i7[gˁXX56*XK9M2/AʴZ*(=cvOu|dY`nԱɵ^XՈh$BD8#BVGhtEX^F٥n܌1[;{^ߝinpx XoW9"jbŻ,z$2[H jő"s 4 faCa ՜H]ZJ&WqL)ڗRjq9NC(dm6VLJkKUwpKYJU ~oש>cN[Eeyig)r~8}7y)p b9@VB!%!r&>v WL.j}*}e15]0{ Z N9R!2_fL@"b@1*DZRyja~k8˗/{yyvKYQq&wp+\Ϥ[._7֢q]M)ZsL*-N9@Ud)eO}[o,XJ,eQ(]ޮ4 )gt. NK]$g'|PūYNBK٠[L)=} `""qX[t2M]R`Oh&""x仡)sbF?FRWZo9ETk651UwfF8Bti4:_;ZkW)A[)E$~JUUVi3l7_^ }dPN%uVw7up_ ;q_`p%XMZ=rbbzg=Ç?~vG!c22ozb2[D7yW0fzV]clZ8SiS@2ecK+]RՖmz:~m!Zm`& EUDNSkeYPkl_RN QBDrQJIHwpH‡O߿G!PJDs-OOOf?1j9E0u)yz;V-ΪFfckbb̥u1@ḼD\5!V%6>|]84dJ@Zu1Xjmvmz MO>٪(qЩ,g="e1矾EU)m#  PM1Gf]Xٷf7S(tm$7@AJ)QwT!Q]Z}zW RJG3bFSm@[+BN-6*Ÿ%ڜq@D=n6?K)2"yZV[!+4mX ZjYH+Wywu0k[?²̌8vɸ'oK3Q2[bN92/RҦ qBї 5 ?% m ײ>qÖa ]9GDUYw** UQW#{w7Aj92/)y&7OP=o`u0 n3|2cqq@*Y1(_YZ@K)nU'mDbs%`l4,X#pB$Q@U`)0s8-k+)0xJ+cΧezoo"퇱Z LK)!Ů >8@R),p(v}}K8wiWM~1^ )8 )7Pzin_oWr<vp؍څ)E(pl<S\@|׮c@~a0! fN%L9"8SѝAS !0{2b 5R5!ڥԊX#K}܍O4x>v4o_)NeRayx^KiyD)pkIH;kn`8.Km] TZ3&!{5 $ ,:lHflHѽ:pi}-u㨢JУV{oMOffjewV`w">P Έd!(ʊ D:6GIJHėcJڅUBLNL1!6kL MLe01LD)"bj 8c32i QԴ E]ecѪE/?==s<$Cj.a.n> _4pR7C ۢe\N_;\X#Gf+=w$$X݃w>} Nl]m&bEsk?D`}oA|M] nS("0x+ V)H:&RvDd@~̻q֚Z^ǡ{v Ft^RvvYJCy8}J!㧇iJmRNb`3z4{Ov8Մx$,ŌL`Fd=^HX׮q xn491zCL#iя)3p8b  -Ƹ(v{ QHx۴1 lY<+1ӴQx4a|D4 ~J?^_}وqm}U/z԰H\k a֚IW'´\5ֻv"6ddں40st PJb~_+R6a:"7f:1 ~+n۱zf5`?bL(+=Y̞hoJ1`s .q܇e9'ۏn7g"qp:a!$|FM*spS9W) FF9Zjw)' aDO+@}s!VU-] f"^̘ K÷/6ZRn9o\O_zKC>ooo6]d44Eq3F!YSJQQm֌)HTZ1 &5LKD]e%+ܝ7|[kL]1RƜ_ bT ^J̘?Q}~kg?okL C(^iT4[7ȫL|z̫`"ik)K CRDQ9\WQ!ӧaJ#ZCq <R}Lx<ݘCJZjDnz)y^"׷V/˲0Q9,ːq.NarOr/Щ(o̓|i) ~]U#;XDB1ګy {޻vcp8M:"”ÐR]?4 ! ŹҚ~0?GG\]n)e1@DcV*uϭ^m70R8nب M-gyWU{kw2oPm`5=#+} 0c a/_K)yOӲUCJ}p#V~yB.! \Lxnw -@V|y]A9pgL/)tY}URJq?GsLDPk^P$rc1!Jv^ĖR ?N$L$[[j$_itrH$`btWlgTJ$&͇{ݭ?x huj[kG7P-=|Y4n ZUfx'>ۀ#:xbݡu돩*Huٽ$hy`@fJHjXs`M"iQpXE nJ,1a 6_fK@˥~]ugDn9J] ^.5F&Ɯإsi qe[։B)9|{ywoa2nZZt}|\vݸey|x`?^\(Miȃ,x4qj&֘bCLzSĐۤ`DqU楄[_u9?RN[caqHqrp>tpSkf LK)Ds0itYր81x#SH>r1e)K-j)e4tS,E糩VݕanZTMɥr?qLB iƀ1|\fm;- LDLz!S(A)p C.ݦ[)v-J@ {ﵩt1f.QeB0mS0$G)AhH{S]tͦ0wzm{oSkUD(%* AAi;tG}5.j)%jpʎ+(Z#sP@QCbCښr|4%yv[9fXelL^ ӽ,‡?e{ f"u_PtBWP:3}|5֧;F |ZM9>[wF/8ö%|h ͇mw^;ɷqm݉@FDp_Q+b7&qbv\=tl^fb.rN!#S`90RHN <'8s%lJK߿wҪM86w;J)?8M8ΙH̜bލn7{9^^^<)0x CDcp~xTUS@8L[_2w1F楋4pMԽޜrGDr [ŏ+))x:Df"0B[*q4w&8cD(5NTADn v]`$bN!~>lI ! @%0{H/?,C*n\ZtH 6o(NڰA)e_1fU@ȑ<׭f /s# uުnV[SuP [fѳBi7js- HꮚΩOKxАԔ}&Xߊjy~~&/RN69"E㶵_ sW[+Hv~Z|͂!{1ݰx{jkbDŽ5w/*UY7?őq@H6g'/ ̀,.+S05[m5.K5FcivqːͷsJouYfҰnɌ qLKYJQ?^Zont} iw<^oǧ'$Rph9CyZo߿z?Nn?ͷq7NBHeqΡr=641DS"]o˂b< PKs̹8ܭgFr0fR#0 "՗2+m-ZO ]s|||}|u9jǡp4֚! CFe&vnmï6-8~|:]ݐSiae[yTpH)C>R`ݦ8wӏ> R`79T=`SΙ|ыk4512zCm' bo;L޻V Y UF BpK1Xх}y ]zԆĞ8}!Ep9+"'Ҩjin z75CaHM{k< #-1 ZK-*1ōwg|BBZZ0Khղ|ɫba%s`Th`:,LN_vn柼tw{_GhP.6D~|ixVbLn 5Dk$0u=5DTg\QiFL,̡uqs nUTUB!b+eOO1Hzzx<뭋G8 3yYeQ"˲B|>] 9bL)Sn#KTvq9 ,GMUlVާeqcv۫YvZxvZ/g?9\.f6 ~wJ`)%0Mokܦ9;ayY Ƽ;mK߾}o])QMC&Dt0u8z[i2)N)/yX*]Btnކ!LNv&3!Xrjʲ*qfLjxV&?pQD0q^H`^fk&Tbo 26DN_{򭳱EEjSe]TZYw!&6Q)C{"1s)EB)&AoME̳ ~wU C@̗B2/c*R1]Kmm.I4n|UvQ4ʙRThzQ 5|$n\D(*ze }-߇aϵ6K~х!rve8j+O?\JwֻwV77îLSo?MrZqKY?`D)nK7RJWEn қ4&D0j`kuM0 4&㑘Eo1%9avJm/oDJ16qQ1+Qvۭjf1[] 9-Ƅ`ġz9&0SE[zD.ͯp `U*Lf׸Yb.N.>4QuvN]sFRdPDD9Ĉ+޻ikC={6־gtVJBݛq9›i-MmZ$%jE+[ 9UHh`b_jOv9HKD`8cYw)ޜE HVC ^Bi}9*ne)LLh.X-5W tUm9:3#"*!lޡ}?UV.5p?<^[]G&ىpwvo;n`OK!p1lj>"xUjN,Bm'Lul۟ฆ""f^B40@!}pAtiu-]S>a5ɘY{&dۅɔZ.C Vk .pL!R[SL|8R1d a):8s_t\n1ӗU;Aֈt:y]/9i}u΁9NӄLK)9 х8v߿~j)j6-S`եRD1Qn범RZ'q^/BuAP0m\% f8^.7d"X{GDgBWN0ͥqs2y~Wea.SyY6]c?~23V ǽRHT]%k@1E7u`UָD`kccN!e)+Q3rlAU9%`H8cm03(jobHVbN>ú C]"B2.jm6rϲ-?FN8rnDZv]ơ K[ށB9 뱲^ܭ[vCιA$hdV$ާӐ^{4f^j3O ̵VU 1 m/DTSc܉g RY'AK+(Y"jV/;q+L \ksss>Rzu(8r_I>ݝR6y*}tJ ~K 7t뎉)a;wK;5jx غ`~k1Fb2fj Ff>:k-*?pn sΥpvm@-$IPb*x25"vCZkM,;w˗?M$`)嗗%çq߻~vv 1EW9cmx:.ץ,10r)*9*@m)PNp8q/L: C@!eB@"Ew_A aVtMzח/KDt:?xrg Nߦ)R*z,Kø^oC8t<,0dRZəV05.U3xgD@VC"9Km"P︁q,U~IZ"4W%H""/Kq`;қb~-˂LX5?''C<c`@LVHLԵꮫ?]Š޺sэFt 9i3Bya,) %g9v\{S1V}q?|b} y"DWD m҆Cu}:@.qtITD5hR {n?BB!e^ v;DT|:ib???o[k~9hf<ۿ-˲RJ3bL!<ϭ q?u׷WZmre1|ݣ}{wS,jCKq+wO^J񋩫Ɯpi ,ei]I?=d"ƛ;Od~0^.*DD`*X'վWb1s@9!o=D@bfCmUCUTtM6ƀъNmz-;j,ād= }%쀙OVF$Fo bbЭ *>n8lX Vӷdb]~`z=lߍa[;|qfjqwPwlk=䜹ٟ+! "gJ,4kK?#|c@X}"䘶9j9Uٵ6 9 #Iw\[8d#RһRnri.+-Sj("}!U$yKK9 UϏO1 ~߾_~>9?r<>}ׯ_1'R<2i{Dyv)N|=a<[)xeY_~n|6ODDӼrJt^ "#"p` aѐinpc@TZFv^)ITC$^9kNcz5] 8.()! MMOf]_pb*L{뽋)$UumiÜȌ|oAcLt70ZĄnd2R;2&bVѬ5[YDFQaq1ibB !)j.?yaN{ܚkѝU}|mC؀pnBOGxXya'{ sw 41V5/eG!ݷ>n0`9]ɯZ[u:Ø #v# 9_hrm)EDƜUk"^i嘘?=>D|:rJVpn9?=眉VJiߗRv!x>v"] Oz\i61|!'U/59|z|@z^k9= *5ѥԔ3vy[Y{?Now8Ci(eY8D5<π~G tXjxZV0 \Db0.ڤ-Z ̓k햇{nx)OLDo-VsmZD"֤|gחO>ɧϷ4XYܿ/ipkq} >ԇ&|0嶇fsVv2y* u$h0CrC"^JnQptPڲ,9@)"n<O33YoR~.b SNO?X߫[sFd4Joכ>ϗr䔞]Os6s:]_8nw8R~xy{}q,[SZjLiFD,]/1'|^0:nsi~.Dt>Uu. ===Ř#"LZry{{ !Or]}.ږe&b[JA0-sozvڦ{Y/_~jeV>eY^_~p>!~_iYVK aަۍS !הS)SJooxey}}}z8$GB-C1p_. OJYJ-E՚ZڦiƱRR+5l؍9]v8n$ s)"b^?^^CXU/MKY{RßS{!ڐ"] >òCN~!ihcHyR3cIff0lD.߻բ!zL3š"!jJ)}Бuѹ; rֿH]Vph^tϯ5!hX}OLġDOf$Q0  E}<)LnmbyUށk)GK%Mٶ}03$u5T5uFUsD$` OnCk]aBmHY@O)!3Rpͼ{Tda )``ƈezϟ n~/oeUG~/廨X;廜OLw-Wwv, =~8>%c=a@_@n#KTJvj9PJyx|a1Qe82/ 9OX[\n|>ߏәS+ "&bs->ʏ?\Hϟi6ꡄK0D`xʰ9Dacz.kRavHZ%eYCcle圿~ja7 .u/0O7f~y{9?N̼ZE$Ðy_\zU$?NZ5TJ9&ߛUz&&"]rҐB!FXmݧm&֍.H2!2#$He/ f&*nIĪDƑUE^Vך3 V3gaʺd7R 18G^ONoRDp>L 0f`k)n5[.bH̞y;5QՔRIL5]C@b6]w]ܔ QoqҐ3!䮯:M!;VsGBg^1>BOphM$4oWj=#w]|}CEC=LwF8œ\B#3{ω5k$]/75|ՑuMDN&f }{^2`@LnÂf``BJiH͈ô)n]VgAaL)R겔8JY]ќbRaqQD8ӧ'bnt~8^w^b(4cL~-ŨO?_oSzϟw&xݮ׷":noFD)pܮ'0uZ  ,EMח&ۏ{3ā40[kkmw0˲sm;taҦi΋LӼ>==Z[kTD1ya)KfQDH5B`ObN~<e1o+BU~Uu4K:xnͅ+vhR qwsm3?~V!eqNm!܏eY8&@܂]! ( rvYw5ugL^ޞ?y^zkv CJM^j&U.Đb!ӧ?u]5%0ׯ_/{Y 6^~}Joݮsr.A@TuǷn,p\q;8N!׷x< lA.RJ/Ӳ,Ȕb u%AxÐcB5ZkǏ)R${h߲UNӤD4T[Ӽ !]ox3-uwV $DNyn8!D1Z'In^X\N55B5bC Q3tZOq#6&v oV]4sj"7333O-Hb(7 rI<9.U_SgeqX|ZjNzC 1Č@6HN"b0Kk#,Z픠íOO6@7mMa,1$،dI;ԶOj7 t?v}n%mxc ):ƝGDN ?PwٵzEvWoz*[rx!JnQ4SY#k))5ʙ!@c1" BfWW"B1>ƽ ʼ8Dd^JeƜ`NSǠbSt<<)PDӴ,nY"&OOHO?=[oVG=#!^_^p?^U+X۷oo?ΏӼLo_{0ր16O>yPjE&0M9}}ݍ4OϟӯZ[ 1.ۤfCz),{k!"uMTUb] t'ux<@ò,ڻ&UU}{{!nckp8&vhHxؿ]=M$ȁLAlf NؠV`ȂR h Ž6)L\[C q&[*Xog` /.FCDw RP "aJi땐Lq {Bk Q.n)VJsܿ+2xJpA*`ug>iYhI lfX[4"puߗFH7I)2S]DW<@! ψzg*,}}IJ_jP#׸>;ë łK*mf~ ^^< 0Yw'1DûOܳ,+.e@0e"3%D&϶. ?1Ey],NC1]bJ҈v.6eh_7D ́ ԚҐsJ93RRND.,F"bղ3;`+vpےV.2MŴ eqj(:~^}=/#^ѮZ?dx jIw\ޟWܻﺿ1mٮsvb۟C!dy{l/GED.0"jj[JC zWi0)Ҋgz!CV 9^/fmooo qK)p}kU7G;N˲Hn\*q^SpTx#b$];GoZ<뀻:-!&nkzeY2a4mh+'](ٶwWxCZ߉uYLdf]Ƹ_khD#|wj[½];/lHJk55єJnw#=^w} \Z1rqS <PvL"+"ssή]{mz?q"| C@lfK9*4ޒTDEe؞hWd+ `"DLȲYk0U٬nm(@xyxlcv@$p0 Lj [Jgbo ="Ci;cmYf zӐ9qDfS^Y\MKY<9f`E jfVc0%ߡ6>zr}?돟t>?`ކGD;kL׿%,a#qM5+,C* Z) N |GV t!f{2hRG@HɑH`{mۍn :B(l.6.zR?j66~.k'|J֣3amǮGw;EZMOD$F"]zBH[=}6 뭛)6l(zPrdGm ,G޺"F43Ю)F"F5߽B`3C&*}MÐy.ei=>Ĝq7eYB8~?_y7LZk6xxx8?JMD>??Zk|>3w` Uwn{?N?Rz;N16;"Lҕe)):p88矿|\86_CCjmK!b4.jb60ܦ~8 x<-r^.f "]Z7d|zxzx8s昮}J^rx|zo}8D^~z:j //VO.Z*ؘnr -$ֻ857jx:0tZj3y:=bFV̋2O" Pm=D3g@d֪W>YU~`7"Vp':O-Vi Eg̻ӛ"3s;7e[* Uϻ W( }v*[J/W9Jj.|siHOo7;"4!5bf)H,5=J)FZ$c2T[\nLAZDϟ.ۯfvX{{[r:h!!m ˗/|K)9D4E0t3Q'DPreܮ1 G.ki[6 ;Rt8k-n\zO%cme^o@1)rHcF0ۼTlR(].oȁe)^^_Ot\.9z3k-HЪaԔs Zalvinz  ZMqPAzO7|M>Zu#Rk.}CͤIfq sb8R]$.R{MCD)FD)9cڥãC?W+2cN ]g2Z2 kTjbvJf]]fј)n XANDNvK.1D5,hITJ!&U&ihxu 3iꞌNdZW%3s@fTvވC""RR/!=g.jR&bPC[mZ2OSJ)ĔbByaH)/BN"<lA"7ieO.wc]`wY$<}+}y?|qpއؤwxGy%7"3^oaݠ2Pػ8[*sx>>L*"1%&cL]d%~ ,f)EK6ys !n\ED!VZBj$; Cƥvs)^f61efR;k~ jN-]{q?:EXw6=C1ZJ9`1E&}0:P`?t2>==}R{!wqad-D"h+-Moqc78sD!d&&THbs\]jhmX<cJYa^)ڼ2@ULlenj5TVřu,**(IWi7Y#"U'9)@#=Z)N3׈圃x p D"z`)"HiA1Zc;[p)Y} _T zt.%nZi^q?JTUPI)6QC X#9q1HK{h0dϟX,0 pKq|!xhzV+Uktö1Y]qUlz꺡8KÝ@<>|aҍAQq_aD"܆g^Qlm;GrT@Zoм7k,̇>mHG4dw^kU:R11q ;c*V":)ijq[<_j<___4M_|!Z+2zYD=rY0l]{q[]z~xly Do720"TaʼIESQﯷz81wi[kv:>nR44/]9qzͦt>_?Ի\Ӳ|^{asKS0bB)6C=d&9g?DdߧRcy7D{98RUyTC1[R6sm@t1a!;W+.Ӳ֗,f9g 8FfYIR1!bn$Rhj k}uگҚ]ZQZ-)h[)FH1"VMWr "n"DDR!7E^RPU՘Wu}syKOBRشD5ߙ? `&ۿ|A$ar u7!ySl͌ـ ܜWmg!m*/S8i`@O(~5nWy$D ev3Mgh827E{!ltFjk~Ҋ 2IAW/"ۧUKB$$qgt0'0p%6l]ܴ5mrWaj!X^S-1B?x<ǜ)r!???!Q1=eo߾Ki.|~6nڻO{BH),>߮Ӵ̵qEo1&^p{omM_~,sDB >mpP^V0niJm`qHbىwM%8OsŕJ?C׷r:{OG S wRD.mv6,i΃ C?~xx8Q4-]zkӯ}]b]t<䜧iVx<Ӕ ǯp[ ݐ7[s$94eSU3+dUwUϼ;uUU5xpb&9a$y]U{,fV[3s*9B׻ 70s|޵@d3NU$)0$-& 9w%eH" C I"jdqe,Pd$O%3ںvE&'Pm1jr0j:H {)cį\=}hFUkS֭7LBL2Ma@ss0!E G rdDrue23/߃!`J<>f>/cj?AdF|\UH/ i`}#M֏N%;\ȑ6)!؏/[?~g#$|0G o|Ik1\x]m|z0s&Im_.uyC=Ě׷'fjn/{ϩz|>Mtj4~0O￁rI9y*%m4͈kJǟZTAMclzgd[Lt9ֺz$OfgɗoJ,e%8Yq2^[`*Y$ȩ>qY(Mq8`N HURW"!ġ&27]UP݄$.qʘeG)Y"[5ҒELG< Y,h$'d N" Ppx ǵDdH#h)]B3Ў;{%;!$ yԠڇ_U阁3sPZQRҮF[GD$"t,NrX]6MC'ѾmS4851Tcl0cfD W𮻹;921P|jL_aD,=4Ӝ2BX59 5l\,N]lN ibݗ-Oˇ׷/\.oϗsry~zz{}]?ϗm۞jx}jOfVJ3+Ӵ[N%I@!ʴЧ\|us_GqSΧi[׷VSy}{i_VtS6UpܶM$1rMW?GUyr}NӲn)zZO󶭭2M.o">^W3{׶U^׵?hHMrss0IlcK~RےSD|Bnz-"Q?rNiz4ͷn")gsΗۺ#qr)IF59 |k@LE8 PM0;}M6003$*|>l]5DtG'INxTla-ȳr DJt lh R%L9s8RJ,,9]V0(>{kNTQȵn n/<]^^^Rg{sݷϟ?ǗZ7ò,-?$|6S"ϷmwTڷu۶ӹ9>~yyi!QX QvS痗d]݆!%AZkk5m[ ׵Vrt/?Eee  sk{)4Mr\DJRڶ~K9k7f]Umöל~Ħri/"DeYΗ, ""akun@2qӴ%IT# b][q/Yz_צUsJ)GjHx-ăǨZB aj7f~0]Hҏ1tYqݏ`zL{2%@*:7x.62q-҉^psS#,6 Jh9329^e;)ںnOt2ӧe[)m[U˗τoۺ[)4͙Lrt9"Ï>}bNMo"뫹uj'圄z}eY[|zɓ>R[뽷_>~\JN!z>iVݍ u5s8eYPݘÇZCˆM)j; !V>|/kkm&f"y*붤$f:{}]q[wf,B1)/OoooOg"ߗR|v]y>V{euj`-%eI˗91NSkjH\yf$SaiS Tuo`w $Ai$^b28}~,^fZN8Q?\ '1NsCwIoC!lŨ<(u}sxl#T{|7j2Z"L&&F;%GeR P(xhcE`SW X6ĭ޻$a&5ڟ?|`NL|_y.mkœN秧Zw"]si{yyٶ~$ڧ?? =.{S9LFudQA$75i3zB]CDlJ)5^( wq+ \U$t**<=w3mhh.FxAC)"HdkᯔM])Zb8.֛;()0b1voi""&Ԃ P#{tW7 JXJ9@kJ:ٺA0q}?@H"pAz8[L]Hqnt Ǧڶ%YΉ sY=1ֈ}xbLG Axgߗ%fS9g5%,2sk5TR)NӼ,Hݮ|ݞ/v#O_/Og"콟.1Z{o??ķ<`Y/׻HZוN3?==GU~B$4R4%FAA y A!F#ݬ|Pm䄌!7:80O9%ܲ!jSp,9% K;7 ^0[0j{ԇ ӾWUMBD߂E02#YoLW771G7`[kZ/ Վ"T )&a$)NPRMeV퉸 nIon+*'&la 94MHdQcǺþ[aF7f^wd]%v)G75~0{߶MK4S  9X) ѮEO%'vݝ"gBP^Jb>'?Qd3= |L$|p`vn0v~LaPz'htHD R$L{0>G !c<"!ږM젓𣏝w%AIDB$pM ;lZ[+Syi$TR:ϟ?}<]~ǨzTUZ ^tZdf?~CrDL)RQ][\h-?PY8%'̜ry,(P"MZ{kmݶ$0ԙf!J6?jC 74Trcx>1sJ2g8SAۏ}'`w3K9!#jdhC'=%.{yER01,!zHvcv**IBȨ{ f*L!]r=(wF<'BhY$jEtú5FJ9'@ڷ R $br јL) ;T*80Sb.4%Dza5%=}]{IUrV˔~} G,}D+fi⾓} {J%4EA#S6zP 0pPOIb* D=n."(Β oGGqo{) %@w$>X\3)uVkN1jŰ{7"R@#jƻHR52 vSsG7H"즦ڵp^^C# M xqk  ސ L>_"BLV<}<mk(THdo!$BFn)/>cDzGb:CNhNDK#fs3:Cnψ:twp JzJP;Fc!L8'-p Q s"0It:"#vu)l 8q#xL|4 #4C]1B`x=Vܬҋ"F6.QձH̩-.Kޛ^OvnϧeYz;!|z\m%//?ߚi~G5$m&q7s[B+]Df,k<.Ogf]߮π(9<_N)ڴ);~zΗyO-_|ixY˹oDҶmZ޼drbi{'̉l"-ۺ]R#c'DŒ̇,,8 (UA$VEXCn<عP y,jnܴiyo5w!Gk1{K3K<h;Šİ,,HT:nuM)9:2 xⳈHk{2"9 ZPGm4b1bqԍF" C8 Dm02LBLhqZkp|1c88fn[ruAI.K*EׁeY܃3;.?Dq^|>~>]$IЁ km$&jM,D>9D^렿n73g&W#}ߞk_M9[J2SkYy>m#of&.OD,~}M)}~)g R>\^}9sk6buk3')%N"}irwI{om]HDhy4aGl;Q j6z&I[1{Q)&z>b@cƄ08`3G($`^EѐsގsNIPYbk{3@ɋ$p[kiyzÁ9rέX$8)"zBĐ2 `$zGDL"mHJ!V@G@s[`oW0y?}S$ _c z$j]^} 3GI(B̸om_&)n}cIM8Z9gt[e#H,@8ſ4͢3{7C 8昊4g{)&.L1t֬j)[%B;[QhOb7 GL{rDG{!314du]sji@LFDbSv+]=l2_>bOOo|ޖ"ӗ"2'SK9ERannjfiDmP"<=_Tϧi~z=" Z}z^ߖ2kԚU>̽n[<ݩF\FW۶sN.˾f|?u Bw+(}kn y*%8rt:Eavso]kנ1PRX6cDa떧,^M$bXaX\}G,Y AgfB| &c${C jL fZ%I8b挸~;8[%GR<2>8h w< X1%Ff)bƈY\bwvpFc1@U5uffJ%{9$fy(]0wC n=Š[eJ3/t`ˆQ5R[0|>xdw"9#P@T]p$g jX~GD{Gy&W ijQqhDnO(U$ }Fvm)>G[uM)7g DL7;:oWFy &L8$t_K‛mYsy~SnPS53D{sV~}d-;u#P===a<JΡC>).u]r"֯׷yk/u^+ iuӹ!_.gfk `M ijuO)m#`S)lҷiWoMӼ#bz緷t:Noi3!t}{> rct>/rݐ?ʩI}k̉߯i>]\}Yޮ .~/eyDoo73;_.4kIzo/˽wE$=䡵uI n (^׽ĜHm{N[< *qII% }*%"YkWppW3p>FA8qscmfy*P,eIEޣ.t r@c>LdͬCzDLHLD¼of&82{(w@'ZQ B&#^ tEG kF%'"kedm. 8茈w!:Me]Wouݪ:1m]our.ey5i]__8dB(|>Y(=mOZC&t[9t~Ww/sq)˶Z2Rεԯ~y}ysWsG)t:aB(9+ m޻֮ 3n>K7G=KL޶Kwr):t$ڡЍeYb>  "0D ǿ.v]M[8QIrZsx~{u]SIZWHR;;R"qHj7(Pf圈ػŒ۰b(=u Li-A+e|bŨY8r#0~aCyNBDb*"]#N_iʽuYNSoܗ]rܼkkm۶|:ݮS֚nvl[붯I:RI P}Ywݶ-je۫D^rN)Vjd/˗/ ٴc̯Ǘu5{ϟ?s:{,JL벪smoUSۧ۶9,[w75}xHڎy>! pHl!0GzKƀ[O\+pSs0fDo{-%FXL;ɱY<>!#|ty*yZu[7,UU)Pt%nEaf9Im js muۦ~_DmYmgwJJ#YDΧy]3;NZ|YX(+>k+Ll/˝Η3^/OO)Vv-rOo[R^ġI]Jv]_&e*i'FHq"0HÍ[8{7faIbaLc1ND)*cF80߳mYZEd]C byԨBM[msoML1.6XPd8,Bn?HH }Ji:͒׷׽X#0'Dx޺6aɥXQqp1'I[<',iԻƒ(j_η|o??}jr IR.붇Tcۛ!?MӔ׽׽qHPJ9RuZU䒄ub{ge]&Ȯۊ޴w֔%zCYJnt#8$,˔|Z|:4ie.{W2z_oo8Z.Ֆs^-OE$#[e~۽V44Me1O{x¨Z`DNscx 9}W9"E=j׈wT[)w?ݵ[k){krGfĬ(P[HR)Ԝ^4稨bje`*) f='a Zy9z&Ihaj}u7O圁0h@svRћ2RʙuRVV s$!VkoJyjpT˲HQȇԺ44ͪ-*~`c0X"#668s.O@FiPο'52j7R&=^:BQs.~ľn8 7 H#8BDǥFH0RN#"6U3K'Mv[F*x`nTLc EUq8E<^1zjc󔏴gPNKIFaE"˗ ̷D4]o7C6 T5O\_~}ۖjκ)i1|9 [l5f9/6M4M뺶4M)uUY32Ѳ?O>|_~2z8Dr{UDjr+oiVm '9Gn{9 ?m끐[kPsN]=tʅjo>\Mrf"s@!;|d:Qj-I)͈0`h\7}bp44=Їt!>4h觑޻f),IUI□)4&52F07tpp$`ܷ=JTmnm[ǬrRZ@G`w)3C!TTbcCblJ]E)N'̓d$ wej#0e!H Z nu-%$1D/~ #qƯc:8"hSSD7SDq!  6- #8'aX ^ İ>!֞RX MS׽Lr[\Ҷ<__jRDʴ6 oyuW@bIyKΉh܈Q " "pĵv!N)!Hrдo zo1d؝Zo]y]W1Sα 3w f"2rJ@wQoLFg#=Ԑ! by;vT8t1 $F/vBg"k!ZwjMє8 !Z pMc\3x82Z1tGldZr0 v%ǡDɧ2Gﻛ)Cz4#tCfP1hV1}ۣ$]uKI @ >YA7g1s'aK$lf aYB Œ!,šm/%7U290{ff>@wqW$`)t@y:,V[c#TfPBr%#(yDpD>s$,"̙9!й}+Sqvﭯ=Ʀf6凞ާO931YSk<##sz\m=Op{Χut>n뾇#|>T}|9uV4Mz|v?im\Zק(=^r"e+$ןЍ~cIq?i$>ʂ"J{ϹPPu$zo0zTS{)9l#}x04ͰF" a*%{n,,0ut{o:^A`tweW K 霧",D*m\"c:{8[CINJLSk꺹)!LSxͽi%mG!)S"wV 5`&2tif&& ;" pFƼE랄i_;TM"oTm0$?|1}G]U{'u% xP}; DHvHjL;3fcͬnA SH.[U[Ȓ%'5M _߮뺻3!nj`&waL݈"?u۶}/j9kڶmo$RKNӺLS)D)e_7y>II?>wz[;#ү|bɭ{W7V[zZ7'bNRrIQxN[ksaf0G@f!☀92GJIZk5G`4MBBʜkG '? v@#We 05]{ZERC3NUmR$ᇁz{1}ݸзpGlX$v5ښ:b<R;hL5D;^QHN!X)n"VmܻNSS4ш[SUcݷ#*(!:?RBan]SlSL1%J99O{/Y%QT: EA'Ю"z1܉SX_iYpf^C2䨩u^6KJ[Cb/)\)}%4 |C@x{+HLDऻBǰqfDk` ?0LWuH~O?Rk]%|y:Oe᧧~yHKnPvr]DbSM?|x^kkf {ivJng B`3" e 8¸H![2)h5 h! C]E$AO)0 2sP[ǿHK%ssH׷9'@誵R@DzooTyYsEt rmE4mxob;1 h33q3$QR 3b?Ss*) ˹ 㺭DUkm)ei-n[}{" IȾ}ߧӌhimNkOsYu&zz~kT{$zmz~#[k_-G3#IeՉ9Rn QRJ"9,"%9#qجGGu{[zW=h,v`ըLUm"6N,A ؙs\3.3ރ爀#@oȚ譆OUHk-nѮ.6atx0jx$!@H9SԔ% xծ󴨆$?M.Ș"z0)Diy"ˬ *,x&ҖőChjI13{ԛjXd$D`fi5p ( cCLޕ]X rajB͜sS(Rq3,;/n`@8B!$c<@Ud#bf"mҁ&#B0l0Ra5AFnwI3cNy*i|IJRseݖmkX:B%~L$v5jkdžC58:[xN8)h7R;oWU|HܣZ7U?_<"w)aˎ`L Ѹ`8pU5W`It>/y>͗ˇ2͙âKέҮ$W۶52rR~%eu#~!8yY|ZʹweЀg_ˇgǟuIRڵri?~㏗)(u}="m49Q'_nx01p2jm{ mG%:<1"!*;|cD/ϣfdM@x!KCP.ܢ!?9 5X;-lH(C;PpO&*DD "0E1wt$FB2[Fx&rppėR`*+" ,Rw7wC0Dt?IENDB`././@LongLink0000644000000000000000000000015400000000000011603 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_glsandbox/net.dengine.test.glsandbox.pack/models/round_grill.pngdoomsday-stable-1.15.7/doomsday/tests/test_glsandbox/net.dengine.test.glsandbox.pack/models/round_gr0000664000175000017500000021421112641367671033275 0ustar jaakkojaakkoPNG  IHDR\rfbKGDIDATxyԯY xOTSR )AD@E$k787fjݺlD%6򂈊(! :s7Þ?}N P*ԵVV;(^ߪ!f?]@ z!}⾄>??ݸzEn~/셸Bfk//3ׯ_7|G޸<=6i^H[+//S]ۙcدZU}?$/$2Bf>~Bŵ5Ħn?nW{! o0/U~TJ?8>D釁~x7E'y! | ?|>u}?G+T<AF)k?]aq^HB5?}'O0O3D [n0)%ۈ,1^HB~/׋٤ki:Jk5(XZC09H%I1! m]Ϝ~ x"]1<%?]Yco(@SDJA =uUզhEBDb7O\;lgM^1^1╯;9:| 5 Z3RNBRh4#c'!x*7om>w{{Z]|+fp|kk{㚟WBxvjv-D %J&Ę" wH)v's^Zt2{XI`|CbDUy޿>,,>6mO2 D1EbJ >\_[+M|K+i)wh[' kB՟>}?׺O!G!Rj)WgD{ %%hAiPea^>?t}bރ ˛8hOob$H;bL~VE|H)!aU{nsib'v} )BDIR"bobZ7cx!<󨦓KfWq!)Օ* R1&hPJIo !\L& r-m J+YrI, ,N*L}&U¼\A?ȉU5%%X^-kR #g/$gJ?$Ѥm׏>!m )rRBH J,~\sxX^6kט4%)`>i1,,$G|J)B|!* Xk 7R7GK9/$guQ*ѡks )7Ri*RdH["0R7 TSYX_H;FiMJ;vB78tC!îwRd49j-"mדX$<+)_|3 Zk )@]IB &%ADwH) RJϽ뾱la͆f%nsJT̙{9 .@#ezH[D*Jt}cZQTEa? ڟwBxQŗuTDKąf&]@JI!"2%å{MӶ-}3=}?PZ9f2V7[H HpO+%"e!4BH9@Jc]EALj\Fk]/L~x=򉲴2BO@*L$- Rb"Db#\o.?܋?~_V95CI*V_iҭJ ;q E=C) !ORrlO%GuWe%m{[Pm!(;Zn6ϭڟBxOP%aB& #F>V$t5GfT.y̩1!uDM5gT>Xp*PE=|CS_8x/ScH!R BD+ke16]Oy,"A? ?q'IZx#M*' 81]GR@)5kj*{S}b~c a}D}m6'׈)bd{k¼!B x}a˔RZ*@*omH2U [0'~{{ex~ʕ)xmK4 >B A ID$Ę>o5?عM)0lNn`%#(X^a{kc8m1Z{\ʾ1(%- %b Dc̉  Mw=QL͛7]? ?1v/$g׻'''Z1 1FʺԳml=hvP*"<>R{D:snXrjr ,7-kkHcBpv1Sw&zM:'̘rE"I9 KG4h[ۀ4ʕњ?!ŕ'x{=%1Fj#:N0Q=gt]¾DJ{aZkkZYsrIjvF=!Ϻh8S+T HNJIf[OgԓnrrZrr%Y}xBb[6O~pjLY 4UF $R*(2d4juQ]^*h2n"[6r+z=!(H)^T%]ק)Ui1AJH) hloSI]ONp!MC;D1C)ۜ&Ikk/ u4[Te1e6HG.zB߃~BsziJ\P:&w*!@ DS!ևc1_ZbX|AiMS#bMPR$ %X%%熘~n1x+ߦ; w홪;@"nNQd6TrxPF剁ҍVsCw-XYR"/FITC<; )=A*)>W)y)uGhe H1VRfgX|h^}k%<& I!DRK!4$6$6Bps|XE9LSR=K/CH. Z&!|J ./8-^bk~jZ}(6F*p )zУ%O!x&Uqa\= \.ͰcLפ Q3C²>5a{ X\tCe,IN$"̲SPo0e._~kh=cWU3/JmwRU3qUu3+{eYKcͿwpQBR>"'HI@ IRRъ?8[n٥ć/_/^4 >SA"eZRDHRӀV $5ο"ƻVv')OtX|O!)"H["RlHiB@p4tiS}@MY .P6.pΏM]]I)^QXP;!@Ber-Hh V/_)aZi|6V IHB >9щ{>J$PZk#RB Akt #.k+V}([;?wtxG DO7,R$Rp4m&9꓀wim֚Efkn jnj ӦjJR4bTW S!!.oEi_*}sQ6!G x$|XdTCm~/L⹘IZlSB)MZ)@faΘwR.L+zg.@*|=QW8\18v BP{Wʯ q ) jӒH V,V+FOf&9lR$P6'ʡ K] G79r½'a4_\ABvYiÒXs#"%& by&\LMJQu}DH !`<|Zp D C(W ԎOD$e@]7آ1Bjf/.%szdpuegLI]R:ݯTuUCvEV"+H`qvK枺,k%/OOR T $(S $WR:I)P㡴Y/z)\3bАDu1ĬV#$HHLCzRB( 1 k(ο(/::<2xED7FsEE]گ[ɍ]`g v2+it] k[tc|5u J˜@]przpP+.m}qa )Hzn )CĻ0xO뻘RTCWgp&DkmgpC/xD+IRe*gI$R>tqZ{CruӄjEc JMVzܥs;mBi; kD c!ZȺ_]b-T@N"EBhSptxϋ~͹7b"FFJg %au98u{.&MD:璒D1!rG#1%b9!>?{y?\h V4e)ېW@ VQmw6@ (wFw.m(%I1"Ĩ]TUC4a\>"jk’Ά5gMA<Ś0GFTܸ~3wB+iͻseaJRP gA@b>fͽ 6 㓳Zݼ} /n\GL (Aa5B3t B4AHڮ#lk^=$nM k zD7$$VY)Jc1"FKJkp8BTYVtK=i]~S=ݦh4JJbLYޫ8:[ެB2nA/x]UدԓՓymYM V|[F'}uѠTI;wL3<twsIl󞢙%LJHSMYb>m^5y~0O*fmB'Ĉ[RY/&T!U_z֠fRHZ4U5YS.%ZKeʧx)AHp.~6u7rչ_ܚ<\+RHeQXå,ʂԠkiHCl>]g9x׻~HSG!"~I[' 0hɥٸEoAHTfuW{$qĔ$Beq6 Ƚeݰ5kX30d"k~ U9nX.T2;ViUbGiLaL1FBH%0 ﻋ_ٚc>m^ZV0FSZ T 4 ͜}6UjOwS=?szz;Bw颞)u?# 1xno `Uc|hge]*̷ws?ZI"D7Vڒa櫥/YciHpz…|֬nzbAr`LB(D`ʜ0n_mUioY gV~zR(M+gggߑx$x_v?S;)iP$S5YW TrJ6C@ IUz5sׯ_.a3_ly- wC^c5MeH/ DBlPWEa)f4N%@ ҚĦIF(^ZM$|V.~Ua7hg>z~!L>C2tkvM Đ=V%~M }{w]\U{~ciƞ %e3AJ@t3H)2%W$$"%MU~V˹M7n|-lΡ&{F*EU4U-Ӧ#׀=R$V 9;m$XKLҝ4}/# k2ZSe–\~n֨?<7V=/$EUŔSTXch/a$SC<;L}?mMym&c^7t絔3+L R$&0F#E؊w#eDH)`95{l߻|w|RZEtR&! 0h4"R2>_xK.}k< !A#AH;L)Eic,)% xup4Z1ޚ.Pe,Jd™|F 0e IըTR:Y<_Vܿ;9[/\ǴV$עm!n CO׭/= 羬n?H+ڵن;H%Z M9H#mc@ItRݑkV"MGYXTM?xn\b$Q} Ą@cF)6BP{B7£^7m߽F]M3,;$|8QJeȹ ʪ?R!}kW7{šz<_+cBxbX(ꡪ:mVY7F(n1"'@QMrfPjͦ>@9D3|<=SBd _®Ɛ2 g1f<*t!x… /w#blzOUSXeZup-R dn_!|IJqEFDh91dpqא.x?{?v\jO>x^x87ijR2#D б#)uZ klٰ\w]y6r;xα>1n?+1N:s*gl]p知oI !$]}yL䕫eo 봯}kMUb!zLQtmգO^!Z7hJ(5ȊRb m|OU+R)|+sۣk7 GU@&[ID)s0c9)_ KuY7?b;ʜzُyՔO]NNN\UW1nV#5 J)#:,o+bXSWB闁wjQ!ċدj !A-4y6KN !ʼ}b,3h7波F]nRus1!D1|t:t`]׍-Ji!o4V: zA#H[s^}S>y=,7kfJ͒ 70$XZZ`Qi JJY1fwʢB87Pج @̉*ۤ-[OЇ8R/))5~hQɻl8*")$D ctW^c]}a|%%fH HiRNΥ#AL@dlP?t!`͈~Hh G, >C@5Gǧ߾X,|Ӧ|UUӇ1`F& w賸l;w;88[/O]xtɤyq"1*"R>侁xv) \$BQ%1D8%ݦC %"RJLŤ&%D!eQR;[s.cw{Fe ³Y/YTDzœG(KH>Rd^***&MlV3mH&Ces!D" ̧H! h&Ia'xiۍIp> !opC"{RHmDӓ%so|&[9W*]G@YZ6(G4vZ[&қ|e?ܹ@~uZRDUPjp~: @ߵbg v%# +NNNMv҉BBqtxu;Gk,1IJh{zfZҮD? SUfg3&&wC * &3+(ѻY;:rbÛN=i^Mq] 3Z$aB(BYZ~-VwX=/pmҥKwL)JT9EŞw(%G!$$Ȥ*ђ;_loUSh! =JW+&eɬ]Ѷ=1Hjf+CF&R s,NHm?:ow{ʤ #EFO&FB xn`Z|O(f,+t /^!RD).*>ԟ&;woޚ9 93h5B|))ep'~6]yiVH)d۶[,˗}+ЮJM!n2D=7)ěRe%s~woIp'2tV 擒Y;ΖK )BK %%*&H >f |7C%nX=@J$قrւɌ#Ng9)]߿%p|QdkRco{l֚kq}JN @]zzzzh7n=xa0I~ *Ca7(Vk>F|$IBD|S|v#—K)vYn:>Tc!]QȁzՓ w>JmFK*Ca 5D7i}dS{MtUdAUЬkqqyyh XRRJ -Xwcfc%d:kTuu.ߙRzѳB;.\{z\1y)T9%(FӶ(ع?_M|n$xHŤ.Njb)8ZXc0d_bdf.rMn"PuݼhUއe=cmG'uN$XJwwX[_k=9u; gznc",4b>wzS u1_UL&hz>&;~XrecydCփGYB( DJe7nlXk 1nlƥ  GZBn(-$p3ktFYZf'gvH&eܸ \K?kXk)H2# 8[R(024&)PM|`"B !,$`RNʒfK0", f95,W@J] !w2gMۭY3AHdkFJҚsAbfLJxmϋ_ZM]|3ǘwO$JN'ם;G.\ -/3xTRRZ+yQI)yqL1~bZ{?(1nk_naIcV)51=MM4 NT<fU7X=RAWEEe-Z lFjZl0m̥)':7PHF;r swyXG?Q =uj6P|*_nU!28wn<ڸ 6=FfM|:c>Q5[{9qjDJfBQVږi-@Wu';~l>ug>}16I)ƌ6,!DR@kEL=>vߩJ󇶧wmϛoN&;EUa?!^'2}6 |sK_w4ꕮLK%Ž"{I!=|1sS⒌yt^W;C |K;Bx >ev4R@aRR4i˲Ѯb:wC%npL!$ʌZP蚺*>|@HA~Re1n1w1"Fg+"1DϔaRh2w14^ ؂3!P5H(E58lFcB$ XA]@ jB%իK#|%EQܾ0Zc(ӬE))K< 6g*GbޥKt .Cdzxwv>d^kH)ѭ61[ !DRʛ0ݚSyB YgRsXuj:XƞE `:ebpΡEdSk ${! 1* ']{o~sq %Z˷ԥIS~jYVjHA %|2j{g3~)=CH)gA@a=KU՟ܶJeu[9}䦙 UԤQ.fIk0ARY]ohb7Q'^rFM=#eY)+Q'T]|jCpv|B+̏\F>F799:ƅImmrK)&א=vzs}?P L7ҋ^nC]Kss}ж*Pʃlѓ)3ZP윓nHBM-՟M>xI}طkFe])xt IDZSK )%5F̶vI)_S|hRJ\ܗm۶1._xK^O@^Hh[#V֖~*s%$ (7_e}z;xw1Ls[DȐߘHhἧsB`l{srtF 9[,II0k*V MjJX,*#~V$79eQ}\G8rG׮RMG 2[ݺAi$LuIm%/TQrF7 6lo;>>~Nm~ʾmo>iy]R 9f0 )TCk96>5$ -"͛Gg0vVS1 ΝwN(%жukdB Q1_T Qh2BO֚ )yȚs"Xt/wϽ%QgnY\ܩyoʱW{Ё([>@U7p2o~%>kٹ # gPrˇx;񺐝lpvF<%h ?J:a>=\Hs5vKNV,tÆQV"G=zɯo 7ϧ^žRvE>xT%PE'+d/jS"7ߏ?~gpp>1`KIv)x--^XI3AwuMF %t:yMSr?|8>t?fNr{g-)|jYKlE 򄫇k#n-IICZK7NF?jh aHPTXk6!GKT0 4R VZe;cKN 랭YCL ThbUO7ÅEAVHNP`<;Qt{Jȉ%&X/7HBl{LBulrӳq \d(*WS[c˗͛υ_/UJ~AS߰=LM^@ar 2OIM3c_rvzoZU|J!x!nUQhCk5oJxa܀ ?1-eF4Cc%]ԳQ1Fg]T)IzmpȂ?uvzSB BT%"+$GmNB<ݬUMid   Q'RʉQjk/޴gmY3>E+ŋ_G*;p+#w})`{PZñRV?d VplljD*IY_<6NXfOl?65M~ٙOt B CO~˕=f :%%~<1 N8>+2''sb)$< a>DN]NJOmueq{;˺h)6mJwa#R%E2?*DcĔEM$ewIOob,qb|k)@J.QT@C xȲLO¯aT>ς[0fӵkjtH|cQ/B:uMB=D?|!1J 6Æm#qJ$gZ1M^.^J)>[k MS}T%$ IUk Z Qe+ңWAWHBi0資!"{\Mc*٢X<{6{cM{v6 Bpi@*{"Iz|H̶pCKZ| }G4) hDWH]CfLQՓ𑒪/O+{/*'Mcʎ,;A] brrr*%LKczRS sOf@O(5*[~ȵzD Ltyv.]m !M8C׳\s\Dr~6uU'qv?YC$5\ a'{;TUE?d(rWD6q2R%cR$mlgU?h)%d>|e"Jkȼƙ} 3j*ѶeEҖ;pa>~*)T" !%V lQQͶ2rqΡGLpJdGJ(stݚ"!ޑb$L>ÄnM [gU޼)0O-1._֚L&M]efV޼4iW9[0ZRW) O#4X@?I1sO{/^}bVg֝߮T!QJc۴ÅG^ @&-1\N,iDjZgqO\YlmM1dӏ{cԈV*;)%l=~qOӈFllֹ58O"VR WI]5k(@]g3&sK]|U!B"782I)r>i̋8'vϿn޹$eSdli&HSYFߓ#j)#mѦ@ E_4[H)OzxC^nG߻SI-̶4yP2s*3*Flz6sDLDU0 eR!ޚ0bJg`-M3[)%UYsG7oR躖̶f͌UdR7Xh [g0F+1*J"bd~}U]dj5rbKJQ 8[!$x^DkR°yPǷ1fLX(M{O0۞3Lr2zʷi0jõFEw{gdg{66AqĔD|̿)!.j9|ṯؚe5ݷ¾mG|Ž;%E= 1x֋S)P qh[!# #N Yp6 kBm֛<-*j8pCOoHNC]zTrպ0<8;}dq!v.]LGܚBD؊0ϐҹ@޹, (] SO)"EQf2(n,D݋4x˛1 >+1gZ"*dB*5B/:"z*QBD){<)if͔J&(> Ĥh 1'3 BqE)[Xo>B0 p*qE/fsvbOa5'ў[]|FnXV` ͈D5Ξ *'fiYg7ݟ<4)MSSŸ3%ƈP*7#0QՎ@kK |*4Z TUź3Qk'93ݰFq>ug|/= ";"vĻ ) #\DSSLY%!dL$4%%R+if9WI{!{SzK!wp';r'mwlo+u߭|a kDp(SEvj$DV d>cIBl]R=Rۮ RI]UƨϙVŷe}Cy滛f2+ "hBTi=u9 hDJQ_js-EYOlI)},bTۓuno/־, QU%ݤeBHp'\f\BQOf߷$)_t9GǍ5O nZflc>_]v>=[eA0m۾;#{7}ߝ*j[rq1f0|/M.+Ȃ(_Gh3Gw埴ң@$ƀsYT3//]UAXO{:}fm@4Lk!VT6VUjCuYkR*LE!3;f!fXH& E{6  IR7\FkMt퀱fʐࢴ=6C0xBs!+*4%8GN0\clgی"nU")j6l iOSy4LEOSw[Z%Nh]y![IVhELwH3&Q*] jm.m<ëV򕳦;5ݻ۳YVq2csC+BJ2\>*GuQ!ߏ fG 5hBԶlãst揯כ8]b~<{n˂d36˛uvzFLg[nڶ̅w`=!5JJB0!F HB x>Д,Kk޼5mަx4!+ʲsd1YGIw,O)>Ԇj0* /]G qUa3LXg0PYB9 `nЋ -!df2'V%J٦ nKq&a C ]N9~@8;;CDi5z!1%JiaHe}׽'e-_ݛOl|*˒$ 0W!b8/&(SA GHEZ-BH$eJ_3L9wX1uQ:'/_'m{R0XWk㓓}H=Z5S-G+ЈP(k(6EN=X*H*SJU+bh[f[Qn˳C׭W~{o3wŕ'c%_hloͿpuvv^O/ƖJ&\r,@" ewamzЭ.u QcH11 ~twNc eU*|}vSV":g *kJ!bqp?DxWV#9ɵ|.A)HBe1Q)2N|R\9UUsঀ%u|ah(ÖdAin@,Xbk; 1q+*Ѻ,K {w 3Oj-+ms2ٴ=Ud/Sqr|L9vwE\\$U%ٷ M9_BzHuH* fmgw"C%|sپ\EM}ukԛgM٤:!rmHY|V)摭Jdwk,,  ?thUDkuL{N߽lH)opgB\qȼU=Mtj_j(]Ab^erB׮q.SGԏ*3mI/(Y=ʘI%|PwVJd zxAzc227\J잻Ht!PXMMߞKƑ(jЦڎFB)%lZXٚW򕴫S ՄUCbh-6KHboC̨4!7F&:t=ꕧ軎)5cp:;GcJ }eWPLS7M~bL+k,'AV$q.cZߚ6oSѷ|d789Fi*%;fL5^Eb gtl2eYSaXOA8C?j ){*w?+ᗼm7=첔p>;-pϐrF,]t.?&ͨ>["5Xʺ‡H$ێRZw4T<_L1Ƭ`5Bj]?dDb!:5EێVKBIRuӓ޹ONߐaRiLRFဃBbãٴyl&{] ~Z{ rq!d~5!?ʪ92з"yP%iߵC"> U8g s:\)!d̕H1sOZ+Ru'i1dţ~Kp*G1rR4UYR1ٽSM薧L*7%]#Eɦγ'qt*gk&mo!cʏQ#v@\IjƍCJxalF׶F`h;jG6~S,nP\Gb]e=u-ٙP2"gclKJ&e3 jnaPZS3|bvԓu3e\ʍ=yëGwV3Zp7!|d8NWo#A0 kGgQi%捗RŘ!%!unb @QH$B[/#vnj$)zx3i)]igk{S@ 5#"ت1%[/N&6 Cx[k m籤Pꄵc,6w'gU>=&uքsZ FĠd&[0dA$Q6;^- ù310 ,!gAIRjύW(hO"Ia(q}gXnڶ09֠f>iFe=irgB⽻}. 4[{_|Wwsi<GF_~Bjrm&\)q>Sg#*/W=Vgen xxǞ< zŦo) /l|dZ3$hcFw[!`\zt{#7D?V Brc)129%<"˳Q|4R);GzHz3j;VI0zBY)59Q?DHÛ7؝5g3"ױbSZC<>d +O>6rGCbVS*',ʊXozO5*Mѣ(ң5)+* b̍[vmF+0Lsڶ{{~W{b!_Lǀ.M^~stS|@e|:!nE6kH :n&\Ȋ[[%[9Ea3^ ]z捷i O0iu{#*-6{@b60 z,!RYEQڴǻkne-Tlex^v8<8Հ:e!1Q5=R$Ca9Z ]ghztN<_/Fn5vˇ>A7oP|}>S?)I35 688[Esxz>r?}Z&>Aś> MF!uy|YEga>F Iȃ7ڵk; sb#j24g @R0>xWxgъ'έHDQje3L)h~ }|L zm VgK:ػp:جd,Bv1FgL'{i B){qrtc|Đ]0E>{L7 1E%9Ύ dҰCHdB={0|ZWwm2b!_&M9V'cl5$7>W0יوDͽHF"134 lBUU| I݇bA}4e,UhѢD6)=TdqXԕ+Wןx9ep't۶O߾\~Fζv^% Z E.0PơQ"sۋbmLJf;8OYoWg\|rUi"kl@MGa{k 5łÓS?7:; nu_ϷY6hBfg&ǧBCw}$2~"xOʊCױٷf\H?Ls3QXCԬDYb`s~ߵ3$+d]ff趤0RLM:ClmT$Hst_*ϩ-Y1LT% g'y##a(1hAbx9Gruo?Z~L@9? !G7_WnO|mwWJo!P 4s_S=6c,Q]t%։y Dz\r, ߃㞱QbjXTvϾ8xD̍]H)!H{Ir3o *{=V)c+ʱlJLlF+ɆjEbgUšD=JΈdۏ ,H1꾡vՆ>x8ӓ#6P4 1i8z`P;(\Ci h~&˲jx1=D-j\Y{~A`O`RgZ}GU lunZ0(ZnzYaYPcV4u42fgRoVk%3pה8bxcqW/;իqe#DOӔtmKiuS,7bu48a9%5!P2nqե cK3ncҠ}0WZ>_DZlQ(#˪,p%M:e1?W[nnnia:7)7z97Wu%e)2˺/o@T7 KcEY:6TU)pPJb&u/@ H:( 0zbuL跌(Jnm?B<'y z"fVuZ6 .!҆)*6DvoԉIk\)9c-CגILc=1qt|BJgw\owTEd*R18_*WWXk,J%DL44GISZa.Kb5G|+Epɹhҙ:•(ūW/w?$_hקPJE6JiRuŪ0L@]h-UB(ZAc -@"AI ffTn2wOCwVrVG UY1Nȓgo45/^ Mƪ#WW73a4QK*hmV%}r8t00 #v0f xNɬW > 4$7l41?/ss}ђNN8>9ǁ >k-lRuI?8ܝ"1":~LYf5j3㔹z4j1I)'.?Sc'w˜3G+)M*)^ݰe{}EȊY,$ɳP=FB1o$1FɠS$F鴻Cb|mwt]5 S>EږE-LĊ:L ib"bteU/+XoVL>̩ ]{ HiK5Q*فzxxq)RovII.9;Qq9yI9ρ}gfbQjA>m9^L23$BO|> \pjz@ >%b:n̉0p{XVQ%vý͐^5E0ܳ$Daُ(c5 mKۍ^Kbq9ibCP/|U닺FiE,^P%.2$t%9Ogߎ3_~>` VkPkkbSNPՒᰥRWV,Ə](RsSBQAҺ{BP~%0C9r 9E) ~&k$ж1^ sEQΪڈy MnM 3lo %fFNiJ;Rd?ͥphA0q]܁c, Ja/5i\ah=зk\#TEIߏXvhW+bV\^ݠU:]?1o<ss}G0nXW,ghW0((0kmus~I/oR{UWn*m8+臁™{*FκZRm_ #b?x H:aytJ&vrv68KkC1 Fw?1J|vB8t#ʂF#Jb*~"@7z&er->x>|aҏNOb@Pj&$Q(A U(g+g`+@D9 +H$FbYlc$Ģݘ]7PU PEM:aoyC>4 TPT v60 ̡rE '>קn Ygu2]Kaisf<>f-y j2>tUs|"6{8X+H1@UrȂ)S4o>9D9a>C,2"D4Ǒqg1N|t~1hDܺYbcN ?NCz`ݭKfqdlhMU20ӬK="ML?u6([pчܾtZht|wLn#Ff!'[Uw>>m@i6=Bό ,*S:&/cv;mfvp a =}KTyϞQTkÖz2Bnr.܃6~`w(nh7]'寱 3\+е,KlQܒ~*K&)  3gtm8EKs 8%Ix8r-*b 4h~Ys8@k>"4 b${zVfa8phݯ^PXggHKYiT=u9KH3E (>mM93Z[yi,h v3Z2_')3nV6V*9?9f#O`[Կ͆I0kcmŢ$A ըyPnX0Q-l|`}${;XWXc[B)c9WȶuSv#Dib8 m+~m^2Α_T9EY\ZƖ%vl\qãZpVf3N6ѿ2,4itJTkrm̒NUW՛JOz~#Opj]K◟fxHZKuJ=kc4T:A J aݎ_,OY`&'Ҝc™,:z9=?`-cc Q!UYkipǧ9=^#H0ML~mOH<}iHI?%)01Zd7kvo)EuQZě AptDy?cLc4Z_(WS,7y^=Gep!#ϰ*z69RIQR[53 3Z\h/b (ܲ,0a"O}>ePZ%$ހF^D<뵝+zG;p_}ќsz`„XqzC7h*88Y/+!r6>~iJKXWOtc"M$OU7@Fg9OruI>-iǑf[&/f0yBENQYsna?y٨s !R( -@>e,V9m1I|ҋydvR u)D)W)|_f&z)2R)1R53F1Gu{̯qTwO~t70?֫:Ph6ӄi{77<1~K=C۲ZZn(8 #YkՆ1ɬV+{-XC? |g%^%/$Պ8B>44s)3cacdĸ2U$Dl8I,*RhW`]C/_kd( 4,R77b)LLukEH빯>Eߛ 8ZT8?o/ RZck,UhxB_ûC/׹z۬ߡCgR*(rv)GuY>J9R2y(mYqbL81n_Ɣޏ1RlMBA%`bl&/l'yu[# 3'8g&w{tL}K__"# SqcңǏ>pFf0nkfc^'^\6g L] gJJq^DCqĐ*J 99=g&N7|/Hq$D0),sSIJ#A][0 F8Z&,fnf_;A8ȝNV0DM`F,96!9h[xw)` F+1aUڋO6O>9MӔ4zmFml|1?P]Y~M'fz# +i\+airָMX몢jyj})QÚScLav)CeG912vzv_NݾeZV[M]%vi~7󂚟! .ԧdsh| g6p}}-˦S?bcp]2TYi{T2oRmbHCsx\-+W{ʲ8q:ni '@5˫WLDzʙ vD邜FQk645/^_`ꓼ2Y{w#?K{w"I?GM-Y氧]5 #ckٲj,6'JEQ+eYE9+k,"MUWx&jsa+oxwon\|;_)(O$LrKb%j)X$M(6,ϟ+nw-MS9&>BDŽ`@90rM p KV2~p,◢,}-IDAT4$(ŷVH]$GɩsN)[JrD>Ͱ4yN6KCohe!)j,̧T21Ytľ el v[8#kWV8nwh9tE@NmRU%1ɜ(0yv-fE7I<6w Qҩ1g 0xϼq,|#*niHʒ~ uSSWW7k@q8!\Bt{Qwtmb ~* &a*QUJ+EuرHP9|XB%[)' 'W<W/AY~ |+fu4M)Zcʆ)&m3LO ʲm[st~NJ1uxugY%X8! cƁQE]Cz?I8]p3/IJ8V~KvÖeSvV!ҧ?yʌ!w4gLJv?ےܶ8ެQҟŜجWWv{ڃD-- /K~I9${{%`2eY2L#GlV :BG/Įm]3M`iO@(,Y'.RHՆ00rOXg鮟#+9Jk\ ۗ( Zn~o0dGKf·|#&LNi 2AWXW3Ukqa~eT`FUH~ F&K|s(4ܱꅈ[aI1Ǟ9U0;7|ʙZ1:ͥX@ULft|5ᄌ=N4!+8Imʙ~pNmV EGP4MmG"ÁqSOzho^ҵ[~#h- IY.Đ }зTYƑբzd(X-V+6=OٶU, ]GÌE!ǑiMh%~ΰ\->`5L>R%u]g?Le*DUc(EFY.9Sau\?^0 Lf!&.N8 [^^fRd /?[jTq8)u@kfE KɑYRB(mX-.NBHRpM9Ƿרr3 '.hFm&xD&Oе-J֩a{7okȾ*jt*ł}H=}oyy5yNMw=;9W?oߣ%-Q,OEJ}uAvbPo9Z/%;C $E o`LE)1Ϩb)iow^ub\iԌSC;bY#MSZ4tpivNJhJɣmjǟ?ȫgn+Bn85ueݵ,h0c N?H{*0Eb_unDQ89;/xed1Uuq5d 4·`w8#GgLLwGG ==9f{՜) ~b;QO)V>3DU;4ܰ?n$*-DZw#Fy*\tL]U𣕞^2:I9ϕa#$꽪K 'y3d/ ɻ0礚2ruBc3x=|TЗB ΊwD+%82F'Lm"zqALcd`:\c+q-N=rMf}=o)t AմdigISOѬِŚNE Uv8ptH K?|v!*v__7/GZ+rUSahwr)#NF6HZI&ID$֝sy aa@%k7ıCkI-˂{ kg[bAvb4lvgU`߬>o9/w[rq;@`}7fh(TZ#Qʴr^DŌjiN#fAL(=5x4%(++?<8p1L$(Ѫ'1a't9pQ%˺&La-/DzrLɍI&n@Y Dw^n^bS P҉habTX#3˦f;0k, Kڰ=NO?q{^ m>i.pge:`}]ˡ1\4X+jQ>0NRAF?R,9LķTGR4kb{I-EnE90% ը4f\nJwI[7۷9=^˿յhfc')sfwp,agXkXt@ ͌U JA}4urɞ$Ls $2k.\!W/=8F4КcR3UW$X;}Wx 3tCj<;84׋a臉j;LKpƲ5e(g0yb8;|ai; -1HC*Z಴l8llYxۉI*REQHUXr,h:DqHssshS'~DEUQ"OÀ}۳YoSr<9^s?R)+Zp1OM3eIQ8ZbʢDIR)ķ(vl57x/GaZ-:&btD+%"17MA ʕ SF¢c+r\\M2 4MM6 3XDE:[-=e#/]?/|/p7EcqDk WPv=°/ V)Yt"-!$fE,dƁx&' [b  ~gT,):rz C+ZmbL9mF|UUPه/}x~xWl63B9 'JkgoeӜ}iM]W#eQWgGҜ^Ym?>:"EY( sh97 tnɓh(f̰FxuiiCYTS΢.899( 8Vgxӓ#'H Z;mb @4>hnpvfGHa4弡$~Y5+T =m'ahM+#f0[c銖t%c(e񵂋 Ͻjrpr`4MB,Ι&yquG~#NEA+%9 DS\VMccX=#] )"gBg}MFS.8>>N_n^_UmҰ\xիc@B-#9kńrkf]Pg-cTtSd'lZQT+%:knVcOL< xj(z{(${4e BJ=0J1n_8G˜u0 \*޾(i<(YVYBʳŊʍ̾a|+3vO]Z&)Zc(*g޶u8EaŒܶie9hC7 65Fb&e5 [LQrv~F#SZG9s{sEa5S`Nnx'&\p"gbGK l"˦VQжY[,CiyŔA*%Y %!%q3~JY2rʤ)џSe_x\\v, dd nfloe7g4~BN Т'45:O4'SP쯞^4zE:ˢFV;g!PmɾGxiUI9wu ۮ3|_/3n.+UYI^f)$]Sj98PŒ!'5 #9bFuPL`qlSk6TN4EY`䩣)^-AƒE1GVG9Y;xQ,ɓ%O-e!.=GH/ %ͷ,ܽiEe EszsV^  UI7lNPZu=9%F)ˊjQT&IіW׷r%qr.  }O6tvvJe8۬qՂp}@6 ^FMހ%UU2#7|u82  mKG糵f,ص=eJ1ZkN |QOY*$>. qn ,sYI4d+8Z1 g hâ% ;ˡ _Z)m/,U]?ȍ9WuG<뾵U$!+%z 'rr:yTvvvR9+M2m1nAdO;F~wSS,Qs\0|bÞ iNտ򿽼x7H{_J_f땾|pr&KK?HG6M(S-M]-Zb&)&?٩3%MTf-vߒZq92k~\E@E8:MO}xs>Gq?6~hʂatrQA34U)߿h%ԋ衪< v\b/PiS3uAr}N GSfq֧iV-ĉ=Z($mXTd\)DLcTpF2!{i;Qakv0k>Hes||˪f/_8IaXU9z@KsRzt')1f߿T ʢXlIzr0.qBٷ2i.K:W nR8I5FIL$7ԝ@$[,Ha4'@&in~2L.vLUTi*YcRf8Z4sЊfє?Ё$ <ʍ.,>$ı7E Bz+jMR)(QBt Jib}XCst?E>&q_߻?GY/a)%HϞEg9fqttD:#. ŚZK`3*y%Xpt^Jzyy1QϱwiY2몾mKhqPJhCZ)%% v1M, ВD\H)$[\^ʊ(ee7ۧ*PtLYqg Y;6݁F7GtDQ5WnhJ4h Yqv~N?IS!fUIӔbcQɱ %1vV?ʳKzCsc. Ѩ9Nli6 I@IU(|mHA̟{SL$6ddop9OE9v=ȸ2%Swfd3m- AM׍)WS*YR P%<{Y2<G~o폑x;٬?hm>9 ;)5nQ/paGL"!;Y^1#8MOi9^J)B1s倫?Blu0iG1*<}H1Qxs3-q r5H$eq;a;zb^ѣ i,ΉymG1Cs8qY2q|$ՙ*?"xcTͪaGeE]S U5j<O<5N8gX4 WrA֖aG[JS3 a1BZ>Y^rBYG&_f?:15ZScSCzdz~R 2ZW>DumqΩeSJUI*J1Gq$,9W#p?f 请߭~ݷ|w"譏R.9NzŲƦox^{)r[^ہ f.eY:,S⸑s/ y٣n؀5W_3% H9;-x -cҶ= hGX'Y}~)(BmE+볬y cϨr( ͣ10CI81\~4f_Ϝgn,+Z=6F?5Z?\4{czZnc(|H3icB .fd\|}?Ÿ7mJ)W?§"~ucgOW&BRGZz@),7?f&@ɼ!(X6!"{Yz8=#ʪ0iqsaX4Ka`;SB#μ9tYc^  /tAyve08K?֫867WH-18,枀z}ȑr;9cBY=Ω7~2EtiTBqp'A^yͿ?iX;Đ G񳳬NϹ؜PLoofpۛ[Jf~.uiQJsmQI4veEeE4EI̭hCL0IF@l!rِr`a @㩪[bbjgpYgIł-ygi~]އqs(0l~I9ѻ}>Jw!IP)1'tN)Ozu.|T1\~觍 ^NL\,0zVUO)cҐD໧Fia1ɍzf#OOg+k0Ļ:1%ӪOIt僌֖ i2}W,KZxD?rDi8˲2۱Y-=u]X*1 qbVbFb-~EJHQi}=(p|@I"H(ԣ@5p}sËSXmhB\]p^`u#(} E1L5\ow<<;h6W7{-]m1d%:{keԔb.*K3( WLӈ;"_4'ٍ)p[겚^$jq8\ ;`)G)5+$,x1Ioi^_:F`іYrcDB,YR lN'<9xjL($ݗ0.hL,aglZ>h)Q 3nGN+iz%(uT%%q%)fAI|!3GˆaI)sz^,:h#ȣc4%}7v/!u-gEI(ѢuJ UXcHJH95$L4#12eM :&^>GMA>/%O~J,G>KedRߜƧTmmL髊-by'49GDjETlI@c?ʯc1/\.SG9 B+qCL?4e9+5a(ePtGbvrTE#jq9'B1EUJF0Q,,Q6e,hEh1pU7ι*N$@]c`'Bkx|{5dB?TR$є$3q"䙩Y \Y?tTvKTfwa{}IQ!ߛ}F" ~t?Uק-4jwvZGRj.cבkd>B!hc}XVL;K2r3OF친 4?+L~ڕҠ, ΏlV  <)F }OJCN$41kuɃ5x(~s1FX1D1gU˶8m !N՚Y3ak9x8 ~y7$XrӞg1sýja~VKꊊvavٌ8_F_ 9.78gqgb,arSεm{ 9=vYnEž)f!TY46Ȫp?T-Y% qf9fG3S@93)M^Pzm{IUqo434WHJ^['jDʢdy򐾗ra]A^ MҖ*Pɸ EHz>( :a>+'Ɲ,9g4Rzcf]N"|y{{sΟp`0,(stZ1)3ah8GeA#iQ-h (MBƈ_[ğ;SNhYU)Їpj. r`S/פAn~o(?PS {=9U(cTfIђ;@VgK(vц=1z@?軖#d95 \peF/Y" #]EÃ'TN}1f؂V.t3FڊQs$ƀhm\&^,7hߡq +CU?Gi{L!sݣxp-USrQQEiJdcN )b?(kM8F1(Jq_ Y$ > ݨF@%QWaFs QX%5n@i~ry}GnX3ڛWi8Ă)+,8ྂgYʊuәC;PVRKqjYH^쮙2q0@U F?̚opc; #)%7 (KRr´Fb(AUYHl`HIngRs,99aTUA]̙{ʪ)2 p^ .g~8H^)IBK6zR(-‘Swl-4 d+7?#ǀ&7G'Ç~gQcOOz~#OpZggVkNy>v?!24<ՌSݍFb30IV ?tC͖kۖ8g]<|[onw9p K|ֲ(-Զx~y ,pUCHIZa\8 M+vaiogv^>0"8Rj* OO(lLIYtic}rA7 ];VWa0Zv0ֻ2 !^VnL)xu|ѶA 'O[Ⱦb[~Ç~7a_O\.գG~o!iY؉ջcQ)OSfTEC 9fy!Ԭ5OI⭗'vZ\ސ o<{^ >9ZkF IJY0Mo !DAZ-"} (+rb=ۛ[GG("m'Ǜ.!ќP}scsb!f";Lov"0R$Hn$%=0WeWӟbJmI0.:=ON7ڊViy?Q9${'~קbPJONN7z?z{5[8Hci~F/vOkEc٬i5UiqXcH9 ow\]^ zJX_\cb#БmL$Oq3Mж;4(KA0N$IJ&fҴ}+9m!VUjf\qv|B&q}}+,OfM!1 LJ@8J1'=a v/|3|F4tqN,[w=> cGǎ>ąj?:bu6wۿn#Kz]7i˟gf:VFe9aHibQ;ˎnR`Ts}lcl5;O^+|cs_vVxgh>Κy%d٥DdWiսp׶(t*WT/)d;0Nl6kœlASWkNON$y9 _EI4 <|:)P=eB;G\<‡a83No^5CdǙŘqFZi ݏ+(mxo?ZЬrt23-ƻf=Κ}oc_\ е7(O{02ZacsWrF6G':Sܹa \m;*躁iEptNp|a@ 9Co;n;nonMN*龧bg-KQVF)c*X{ʨGORSX&Qcx)虽`%3 #{aPZLcbZ`cNQnGDI$F}E&Zԉ㇖R89=o<{!|1yR?U=z[/<*8w=ragƻxEjT_=? ]jYM]qqajj)x~@P!Kw2'գǼMυ (%6]/(dsɷ_y°8#'BQsU{2Gm'AlĮ;cZS0se _lą'a 3{:' WROʏW/(TfY x9vfL 2PXRxn1͋5s%̮/{vH[/jy)93F-A]?ϻ?BX´y}Z[􏎎;Ѫbv% cﮜ\ow;5N-*eC7I krdqv-/oPaQrͬ;R͝`q~p>&-YAwbG2-W[I7sDY°Z6DyM"mqq$^]QΞ) =˥ !Sp熁!h//)KK9C$ i3ȑ.cArCZv'oz4s~\$ӄRe‘Ќ4GyR%'tqӌ*̬ qF9YH+-:% md1 DSgz{[a]y j/Oea6ۿοE ߡgƹ|E0ȼ`ϔD? ~LLIrin eUSiv}~{k;P <k^Sn~>)&FOUJ0*qi-=[ohwW\]rur!Q=CSWLDUQh P=}&*Xkxpqzբ#8٬5X''ǨbvJLGrS 2rv_W1RĢFh5E0K4Zq8rx{C{!X3 ykM],Wk Uza,LfD q 6m1&ˆcSyKgA"ڛ}NN雟)ŷ_>bJ'~4JoZ}9=z^|lLrQ6s"‡ HIeX658J7s e jv‰|L;ϊƢ^bx-B臉=8>i 5猶c~dI?A*6kgIJٔL5㓳,ܷ4UwUwL_^~m5Zů.u}?~Յ|t=%(c@H8I¬3?uG }EZRXW2~LFsԆEUpn N;v[|Dfv@}v$[ޡT὏w'qM cKW7!Mȳ]"l5&!`PYGV\$pO.|V6)7}@};dlv[TTuCLլ8Ӽ9eĬC~K%GY:|DɩE`gN} zѠDOHViѓcDim^lNΞZ>::;NNNKBH)ʙ67,pzrVtʋŮ(ON`+8_?L\rjplG/^rC`rM$lvvA w}_)-7g:Z}[ouKZ8o~oVkJ/cfO>''ǿfX~;Z勜M~"EO4RĹBp,6 JJeTӢuIsBD@qg0LAYsk-p0ƌaInH?d8Lxgoԝ;^@QH羟wIJwg樱//-74GGJi tv{Y q٬%I>Jܔa?xݰX%mq( ̶(1EΑY0>FbC-F9AY*7\ "435sV ΨHJrRJi}Ǣi}jQ)__O>ڔ `33_7|0FS>~o}睷0cXZ"&b81hYu& "JV-1Ba_V7˿(ZK3(D+Y)xҘ)gƻ H:xNιxT(gxfNsu"fqjƱի]K=Ke| Cp՜Fk@kE\v()q!:КǹC/,ˣS}=cQFݻ͕,,鮡V|!V u%Z}2u%8X#p򑪖!u+JrY^0L)V-uD'81D?J/B&$_z?LWG>o1֥WolԲiH14H6i@iKΉt8cd%58u|raT,KN~7Xk-(h}Rΰyh`6c|t|g߃ߤ߶L=EA2J!h(8=s1Li@9qD[ )Fڮgh}O>~׼}bF2byTWDQ̒sssauzO-EQEw3Ik^9'l8akçNq@Up咾?U#Ն\բA+DIZϖ/!ij ]{'eD\U[?ẋ_}W&$|w'[P5!DGIM"ξpEWkRCK{8سrYP-$X6KBkKQŋ/Tp0rwq ޟ8=MQBH!GqiD,ahIr(Kj9i=ؓR’LуYm :u,捋^,(k2O'hWekyoor\b*fS([P8 !'Z4*g !sY~+Tڡ~ #qN䑆ZYb<Znwvw'Sۊgw(`(2 ǽ<͛ pL@E4W(Kū+N[0]t|;mZ f]ί^n$?oz/nV,NPDSUGRr&֫qp͚ϱJ]u)ޘ"y6J/G ɲUr;ׯt.Kd`s8YDt3D)1g4X6,ʹ,jzn)C]Um>~fCn? }F8e(\"F8(䦏c 2M-.*B&+Z 9lA>V)xakr<:>9AfEWr-y_KeS{CieJʩPVJ:4sjBSIMan^Yi4\l77 S?ٕ.˿_kQB1g1)gŔ꒛}Yl8>=i~(ď)Ȃg-snwYed5؜Y/}n6@<O<:[77[54Q6ݢ(P3ر9}Ki`բ*2"~4Yӯ=-D?Lp{M9I\edxjml?="0eRB2(8|M@؜E~ 4OCs(h}+^?ǡ}_393P EoA[PDlQrmqIY 3Yi+9ZB[B΄&Iڜ+-+[̻ߨ }ͺ+6< Z[TSܐC:I{tԑb&{n+o[BlQRkrD%嬛c!jOՇG_u]ژ*d,˜p7vdyr'O ݎD#?QhZ㿵V->CRgwu d3nH1sr嬑2s ,]+:c%_9Aܸ)5 CUJ=Z9rlNnfG]}}?1Oqo$Q''˱cso&N"CRXzf;yCUUw_loӟ۵ݷ'l ؚ֚4?HEb )EuJlzjo Bvlځu0>W4zoZ[^[4GN0v`)kN2٧eEHs@REFN)ZfAƃnw?܏?)cYuU~p3V9Lɼxr}njL4L,4=\=uUc,61,D9t[={_a'$3ga`? lzIuԙ*w7Dzvm")GsS)`n/^z;"?W pz{x_єŷ1F++om*Q$*)OOQ 0 z/8䧸L+/<~_,_ho^e]ߡu&Y99A2PN z}2C ƾH At9tχn~oOɮ4M?Y-APy|c5Uf{˾-V&Ʋ\J=ce>oaHRopk-1IUZXlח[ #M㥸BK(`9t#g1De_Kn}V4U! D,a> (ei`|+ 9lj̈А4pu=rhg'u۶%ge9|B _'rJ>!(VZiW8\Q) S2JPBVSX(59ErBRhq"Z0(Ĝ+b}vеU ^/ur tHSD+fvI{g%NJf1.]鏦$>?V˔YCN*0 k5޵#v&㽢 5cu繾⻞FJnd*<} Cֆ1&AXCPw0M\^oI$'Gb1'Hf3#G^ߴ7+%L9Spkz}G%rDg9SrlIK?8>^BhO]lpY.տus&~+x׏c,,ZRJ)9g-A{VN6v$Uc4  9?= e)^s9/T^dxrzzێۏ ]C$(G ӄ2=@g4ZfIZg:Io1jRT K !v0ə'u|.Ÿc̷M~v>}7rWܙsU4UΊ":.n|;zET䳟PQ6Lqsꪔ!-]únƞ+QjG ʢILZ6Z=td4m?]M{(.G{\ (~,E:@=Emw0Q%2!DHH݀ყv/Ul'K7M춻?53!ݡS0DH8^V0: g+21XpEE榧5z?Sy~~߾\/JCqE#2\?AN EZ!LJVRML>)4}wyO_ 1\WL9bzƓ'}G&sW?zN׼ݢxkĘ,v@!b{NFjKN/g} h|ĔED7{ 5!9fѣ#1D|jG7XRrA]VĔi7n9q8iCa\IRJʊ9 B#ZQq8tûW?qki W'ysvv>{!)˛c7c ł)FmKgq5͠ ãga:4BX2_qf?fG]|LBtCQLH7N,ꊺxu}KSá=< tgyw[[A HbI a1vW0$"1.W@Pʉ-mac!,4HH2mBuz}wy;#fVMUTys!xXҀ娪^}{WkCEUkE9"F|"BC1ǛT" M{vuY8ԩӧ_{UL2*Kƅʡ"{0"!6xA ʒM& 6g(/ n`HJ߁bJqP4mru(C8p:V!ө >Jt51.͙G/iɯ\?jcn[UyT)w28:_'E) _炪Pyik۴(4eYbF|v{KD\[nГ=>[5VME7f) ! Y`꣆2!'t`TdY'TREiߛw}q<ٗ.s+R_x:'Ԛ`ZxgE܅ !x\wfoŪ'u!F$˗/=ѠD ,/NEo[r-(H2*"=U6#&UIQ,ùZ!N_𛞾70@ҋ fLG/z^$g<(:Y2)>WMn>D 4N1k<TŦRjxE!"WIgzG8*"(0.= ֦3&+gN5l67Uxz uSۯ6,۶vE>vb=j8B)A)=ŏ`TƼ(S[}_}2d_(/URDFјKqM!."QF zo1S;7_kϟ?`sp{ql iTUq)6h>}kN U#LshY%JoQV]x^b:RGJŃb.ZyHGYxdTl [t!3eqi{)#/0Q!}#L ֤Jġ΁ÐeGOU}tL&'BH"x9i{t\2=7V=wd6Jga $t.|҃{{{wNtnwݾ{Ų2/!3td"l2I󃫿T7;n~1?Er<ϫj*R 1F!|CG%ЭzoQއnpwm˗.=eMȏR4ӱ{Pssn9h (DE׬JyFYcp("K]c!&1 \`\t"Uf:Z"RƑ$QAgȱ>*فXNػy\ r`bL7#yU2UXKƣRWS*csx1@+шhL*bD@{XRb0GU&!.C5yKmlzqYݼ!~:ֹ c:tPK7n?'y ~`]Vsk,A_S-!ύНY6Mg_0ػ>˗.>k0ӶUHqSnpJ"B߰jZ̑Zb2 ]!A8ʪyPeų%Ц"E,4qDiZRjQ JɘjTYD!?yZ|uV Ɠ EJk@2ͨ{z:# p*Y.\):>C8{HVaJ%q Μ3˺^^񽽱or-eBzn7oU]9p86?B:ޘ_^,X-qy9 R*byLWMUho쏅 8cՙ3g_ש$p 1Tf4:RibHV˺Ԑ"$S1(u&ER)&D I^h1a -QzJI<#.D T,ưw j#>r5&Q2ԏE25-1BYٽ~B?u^J];RmB`s}vl9M& N%e5lh]88d.m]ϔV)vؙƣh UҞ}l}1jXZ{μo|H/i9i޷p˽}iZ̓ſkϝ;K/>Ő {=2Uŀw!%&HSk e姲<BLO5'MS1R|Y_B_U;먻Q?lÕ``Qě32Q{c? aN4sġb/0c܋ 6UCִKcSdRR%h޲a!Y@NgiwȔz@8$!Kܒt^^qW;痫|ho[|;$SDjce vΝ;{->VDK^ Ag`XMo؛^wlBQ*Ї()R\ 1"TD $k-6%7Ŝi>fM2َG2LtC3dsY[31gBWD֑Zyߣx*Ye5ֲjVQwc{33 g3l!RR Lnww]{[, !S!ϲd呃[zPqW[kSktGSw 3·}ypk>=iX,y'dwŗB`a`E}7UJC>X$;lb˲_,Y, }A9.K**^V<%s,Sx1.$nQüu<˨Y$Sq{yec0k1HR?"u&k׮G"zFeQ=5Rց(>s~=K5Yu۶ˮ랊qQ/Fr-q/, EfbmWB,,xVy ÅtP:R,:"1݈p7nP ~),پ$]gR:Ւ+}BP}W=ja1M?d|(q,5c΢iq184Vd}ի3m>2QUtK1ZKݦC۳AG8g~ﺊ ܙӯ9yuGܴ L#Y^ uE۬{\S7-WwuY뮎F˄SH3Ѵ2NhES@%oY\3DxR0x\6>ᷲ,c쓩"ou&7^%,ͅHFJIVEb |aY55Ǯgcs>u<G\Y#n_vwjVy\?cHd)eb"pwV1x } @9?1c Uw pqh5S(] Tr`ozw}?}uLR!C!`9ϥ+W#[,~W&y><*, ×TB +s@2,YKc̊I>cmmt#1D1'c»眫sΉY!mb(XTEʽg9Rg*jDYdY1٢l\fXJ=b\P9dz*r4"*$ >!LHTM ȡL ڤSTMSSVU >ƍ=zogۜeeYeY:TCSH|Gd}7,w=I6LZ$p޾)"biWl?8\书* BV5;_w^G)xk]o>e㺵=[NiL O,0|"sI! ֦Th!˒Ye:H@(̦c櫎͡&B8-R]O5]_0F-0)2PMny1bTUTb{Dnh1q!rvL'O1}wwsyndJ>PBjd>oQ Te'鄠r1}GsYu]׹U3<g_7^vYRRҴ=J(L|4K$#"Œ45Ԋ-$;Ir2->bo<.OcUw|@,dE\-ް(|Ĺ5^39ړԚ<ή{E5|1{[ Y LۢT1$΃(D}`ͭ/# I<^Ƌ}f^[_gΞj1 ;&pX|,ۆCBWXeESmEK.~_߬ң k)&TF\Kg bsQPOY`~A54ni!ru:Z= 4m܌ygz6Oƍrh},2@Y0*yTl aږvW/jV§P[NzStm~WB lrq6}LǏ:ښXߙzQha,:\wHa+plKY1˲\iUH!呱&R})F 4Ƣ^9XXg;GoZv{9yښkNHqn~j`uֺL;`ٴ'{m%SUT{{8quNrP:kdf4Re@HDta,e5zjht>ZkfɗK ЃMZڞ&%uۥx`EƼpX>|mmB֕JY/i#wy' `݄?/A{>Xַ)ʂlǜs׼st}{i!е-W&dYA+OP˺Hģe\Bfpop#k;+y&mAV;i.|V+HZJ0}67_$u_}؟p})zig⅋߸vڣC߼q mB' @@e6vv39#;w5U&V +uBB=U/W1r9w9wts¬( (~5%(88֚i^[uˢdyFqw Y8ӲǿN%h_^qkY'eQE8ĈΓwB9@V:p3ιŋ߼{Ye-F$YO#@g_>|ٳg_=ιe::@6Z~s:ˏ=IG%mng$c*u+jʸ1Nt1&cL:廞~^y`9sۥJO)Gx*JBDDwޏRQQ5:+궳 !|`|œ"6g?|?ʄRt={q/p_찜n7G'"P5TĻ׷t RoC)Ml٨F/| Lk1멸lk/|U~c͆(4lǺz[dQ@(BD$E|O<ٳ=tr,Y,b])8ּj0a(BJBIRf1'x\kwϞ~ʳ4] @۶c#m<#A0ƶKux!YlB!uwsH~)(Hn@tCj!5Uݿ'r,rfIki6+vG.Oit?,%J#S)S3VYlƄp}0=q,V?cWYIUh }^ΐA8DSEVf6F3*Z1^;}zoXǤrX}v*bFRT-4֧vÚ(sL*0u#w6!x4fkmFYχp%reݑ0, <]o$ˉVmr,W-^]"&y+8QC xMΜ~k;;ϣUz_7YqޱmBC>c5owD- r(OZ$r(b\ u*Jop}PݙFm;4qށቈm4yjVk{oB{£5Z͙ۯݹI 3H@F)I X21^}aUw(%ӎ>&7Ʋ6M_;eݽ!zP#sxY[YZGg,8}z[I=(wXn Q"AT:Ʀ1X, PdwFk=ǢqkG;I(L0ZDk"Qdbayw)/o͗;~#OYaawi/HD2,)R|z csq̦KVgHRV]Ӗ:ϓ˲.GZ-HO^jD"/_+9q?u`֚7BE"UI hX}G4 Ax]AVRHpozBw.l!w 1t,ܠJi}7XsV(|zXJ{5/_E߮DŽd-D&F|DnR5 ,M݈eIG<ԍ?LʊD'ӗZK׷mMSj6}V7w߯B"@ W=J*<'}rSRr Le">ݸ8$!2C@*Ş"Q2ljq`IJDV"TbT鯵7w}xI_VV}[%ڴ,2+ghDxJ1UU!=>q܎t'M,b]- H g{Nʢȿף BQD! D@AAD$:ݒ=Z'IJ,pΙi턯ݸ%g2^h%V?!&IDAT:ˈHyT+ G:"!|?3ֽhNB"S>U4AkAeGH,3?N&@ R#Nuܸ>E8sq|`Ѽ=G9D#L455rpA@Q_Wl}o<1FcTe)R!"e)xb@Fʈ !-R1aE7],V9bԆ B @hd*9##SrNxM|UZA:xJlV' 2;Ox2B"HڻJwP Oq4 9mCHDBkb ךYO? W˷NcHu:4 4Ub *Ջ}V9u|LxD*F094Y#Ʋn?.;T sWy GaZ?=Y|x YV6zuEQum[xo^'sZ\t囔@q`:kQaEJ :wǸBL'@SRPC| K:׹BPyQB 9{d lO^fϞb\},c}G;?U ! J!HbI!4#(4BpX<BrU_B btB 1v"|=iR`2llcU)"ѱn ĹLiF "ٚLI6zTթ6c5=$c1yh ؖj;lYON@Mױ΢lu׿|'?ߵcKwMVR7^uҗ&MU]êihwTum)qf1!Z\rcLQm.x)dN4T'P`gw|G L#0*ޢuyQ!H.J3^4O頧pWk<*l6{V )RA^TGoxΗv8&?)E ht R*MTF dy1߶Eۙ_BBo|@ʈcEj C7\;j{5S{ k/stӶy)@TdbF\H-mg~ eϲ3-tKӭP4|{ڮ5ՊiX]1|8lBbO>$L'rTS9k0Ƒy6.E`M'iQ1.dE]'rt\ib\l4_1| zLc\WJΤ^O5ZG3b\;S  y!Ov,tmts3pX/:il&M\-qV<1]rhBUVS46K!! yӵuʲ]o~'czg*ӌ4ʭŌhh_q2Uh yO9[{sBi$ᨢ0͒fOj:<ϒ'荍eǁcia|u,?b@J`-uoxZ Hl3"Xr-Ʉ'Ę&. k{Z"w[>8"6dZ^dTb?3>u<BI"υ[:oMF Ddc0E`<8}Wyu)hd8$`;}W~%1:+w+"iJ6+d3k15?cUJ^J"d^2F88اݿoaBEQ 8~{vNZOoE`t A('׽q(; kzLxOP6 vNXX4MO]l5 Bt ;h;vr,rr c-Z8T^7[s,SCRV $5":20]=>7ݬ.9.oO`9~iFGo B $Z7βLƔEm,uQQpaXkJɓ56;!mdbK љ&KpdN}cVѨz*ѽ1j\4U/7iߧ^dΐ%:*20M;— A4޻ws̬Q] xMjr rQ%#3Tɓ{boR$8X,wj?| :'SUXg/2. !2.ZFǨ3[1.ċ,MDc>iHZ$bYdu,>\Z6=!H]X1"2Lcc,7dYFUUh)D1r77#ɱxj >5D^3`ilBNmUUUx6]GEJuR6q_;7ת*?R)1"3wC'(2 TDE lN W_9?uox,JיY!BL|^ #*'J lZJXgcۚkk$kidB@" i(Ĭ߻iyƍ78Rʒi7okNg|`kke_Z"tA. zyLZ׃h! e{h{O;<]sMS,K/ h)CȔ`4'RBG.SIHIbD| y*\> shbzcWxL`<65}o~oeUj1Vb<* }&p:Fx'`!δe^c30+,KF.P۔AOw#1Iܶ#g1@g>$UY (6W^}X*n`R>s^}| '5ާ-]rUW?}<`i@G1&>}?GB\Z=;ɪ5BMlX[w7N̳m1F}īe8N&i{ CzʶY ŇxQD]n~w]ٝMy%)ҙYk~-"O^vΣG}KlH`05'<'媬n}}U/"DxUq?vc{T׽7ƾC۟}ߗJ%E bU]ޯ,vC1i (RJ=@7H%A}BRݒ 8ă#כ}oӶ~woWmnm~ڤ:_V]RC'=ȧ-S1ҹ_VMYW[7޳\-$k'\B B()Pz$<#"CZ#dʁ7ݷ?{m6ۧ_S'?uox o]Vʕg}n{XBu䀔R !uk-R%sP hJJ&fB4xkFbhMB0bO3/ZE§=UugL'w΋ޘ!~Ktܩ(D)lu2*AvdZ#U!H!n9{ 7~׏sTOw'{Oq8?#׸RPSS_^hd ъ_d[9M3m;\DQ~?{Ԣ' g-wFwD?mqއFe%a8wc}X]]=Y[ kV^r|/%JQd XY [g/3|Zɮ[ 1okW_5K!$EZVf#x>qP}{׬W{ F @ʣ`s~h\}[9 ]o~^˳*H(8} eQwC.V^?3FgBߛwm](Bey֩-7,{?tox)4mK]◾(»,1^{g{g^[3V?KJ!LIENDB`././@LongLink0000644000000000000000000000016100000000000011601 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_glsandbox/net.dengine.test.glsandbox.pack/models/boblampclean.md5animdoomsday-stable-1.15.7/doomsday/tests/test_glsandbox/net.dengine.test.glsandbox.pack/models/boblampc0000664000175000017500000106321512641367671033244 0ustar jaakkojaakkoMD5Version 10 commandline "" numFrames 140 numJoints 33 frameRate 24 numAnimatedComponents 198 hierarchy { "origin" -1 63 0 // "sheath" 0 63 6 // origin "sword" 1 63 12 // sheath "pubis" 0 63 18 // origin "pelvis" 3 63 24 // pubis "spine" 4 63 30 // pelvis "neck" 5 63 36 // spine "head" 6 63 42 // neck "upperarm.L" 5 63 48 // spine "forearm.L" 8 63 54 // upperarm.L "wrist.L" 9 63 60 // forearm.L "thumb.L" 10 63 66 // wrist.L "thm_end.L" 11 63 72 // thumb.L "fingers.L" 10 63 78 // wrist.L "fingerstip.L" 13 63 84 // fingers.L "lamp" 13 63 90 // fingers.L "upperarm.R" 5 63 96 // spine "forearm.R" 16 63 102 // upperarm.R "wrist.R" 17 63 108 // forearm.R "thumb.R" 18 63 114 // wrist.R "thm_end.R" 19 63 120 // thumb.R "fingers.R" 18 63 126 // wrist.R "fingerstip.R" 21 63 132 // fingers.R "thigh.R" 3 63 138 // pubis "shin.R" 23 63 144 // thigh.R "ankle.R" 24 63 150 // shin.R "toe.R" 25 63 156 // ankle.R "tiptoe.R" 26 63 162 // toe.R "thigh.L" 3 63 168 // pubis "shin.L" 28 63 174 // thigh.L "ankle.L" 29 63 180 // shin.L "toe.L" 30 63 186 // ankle.L "tiptoe.L" 31 63 192 // toe.L } bounds { ( -16.340680 -13.251548 -0.285210 ) ( 16.321431 70.077741 66.474757 ) ( -16.343818 -13.249535 -0.286453 ) ( 16.317874 70.094876 66.479983 ) ( -16.341916 -13.246621 -0.287754 ) ( 16.313486 70.125240 66.489477 ) ( -16.335548 -13.242885 -0.289112 ) ( 16.308388 70.167721 66.502786 ) ( -16.325233 -13.238372 -0.290508 ) ( 16.302700 70.221201 66.519418 ) ( -16.311446 -13.233151 -0.291947 ) ( 16.296514 70.284557 66.538854 ) ( -16.294616 -13.227257 -0.293404 ) ( 16.289921 70.356682 66.560578 ) ( -16.275123 -13.220754 -0.294892 ) ( 16.282997 70.436486 66.584101 ) ( -16.253326 -13.213671 -0.296387 ) ( 16.275798 70.522844 66.608911 ) ( -16.229554 -13.206061 -0.297894 ) ( 16.268393 70.614664 66.634588 ) ( -16.204108 -13.197965 -0.299401 ) ( 16.260831 70.710822 66.660700 ) ( -16.177280 -13.189433 -0.300903 ) ( 16.253164 70.810154 66.686854 ) ( -16.149325 -13.180512 -0.302399 ) ( 16.245430 70.911514 66.712770 ) ( -16.120496 -13.171244 -0.303875 ) ( 16.237680 71.013641 66.738138 ) ( -16.091024 -13.161699 -0.305344 ) ( 16.229938 71.115223 66.762743 ) ( -16.061136 -13.151920 -0.306786 ) ( 16.222246 71.214820 66.786413 ) ( -16.031040 -13.141967 -0.308201 ) ( 16.214620 71.310834 66.809072 ) ( -16.000953 -13.131911 -0.309593 ) ( 16.207094 71.401460 66.830688 ) ( -15.971074 -13.121815 -0.310959 ) ( 16.199672 71.484528 66.851315 ) ( -15.941604 -13.111735 -0.312279 ) ( 16.192358 71.557343 66.871004 ) ( -15.910506 -13.101773 -0.313580 ) ( 16.185167 71.612428 66.890121 ) ( -15.876421 -13.091981 -0.314839 ) ( 16.178068 71.726124 66.908698 ) ( -15.840468 -13.082445 -0.316055 ) ( 16.171008 71.989818 66.926322 ) ( -15.803612 -13.073249 -0.317234 ) ( 16.163954 72.244008 66.942701 ) ( -15.766702 -13.064477 -0.318367 ) ( 16.156899 72.477756 66.957631 ) ( -15.730523 -13.056225 -0.319466 ) ( 16.149855 72.683741 66.970949 ) ( -15.695778 -13.048571 -0.320511 ) ( 16.142858 72.857924 66.982636 ) ( -15.663166 -13.041627 -0.321526 ) ( 16.135961 72.998815 66.992779 ) ( -15.633358 -13.035465 -0.322483 ) ( 16.129246 73.106118 67.001581 ) ( -15.607028 -13.030195 -0.323395 ) ( 16.122793 73.188480 67.009149 ) ( -15.584880 -13.025908 -0.324261 ) ( 16.116718 73.256762 67.015443 ) ( -15.567669 -13.022705 -0.325077 ) ( 16.111143 73.312780 67.020578 ) ( -15.556215 -13.020675 -0.325836 ) ( 16.106187 73.357428 67.024512 ) ( -15.551442 -13.019914 -0.326542 ) ( 16.101991 73.391220 67.027283 ) ( -15.554410 -13.020508 -0.327183 ) ( 16.098694 73.414279 67.028852 ) ( -15.566357 -13.022561 -0.327770 ) ( 16.096430 73.426524 67.029171 ) ( -15.588756 -13.026144 -0.328285 ) ( 16.095324 73.427703 67.028184 ) ( -15.623368 -13.031337 -0.328728 ) ( 16.095505 73.417409 67.025785 ) ( -15.672326 -13.038212 -0.329090 ) ( 16.097050 73.395137 67.021856 ) ( -15.701522 -13.046825 -0.329357 ) ( 16.098220 73.360254 67.016230 ) ( -15.852245 -13.235599 -0.329537 ) ( 16.098293 73.312027 67.008715 ) ( -16.188511 -14.198901 -0.329601 ) ( 16.098629 73.249611 66.999040 ) ( -16.809161 -15.166696 -0.329548 ) ( 16.100361 73.172065 66.986914 ) ( -17.784731 -16.109243 -0.329346 ) ( 16.104523 73.078328 66.971960 ) ( -19.219563 -17.049730 -0.328987 ) ( 16.112074 72.967180 66.953721 ) ( -21.123245 -18.029491 -0.328449 ) ( 16.123989 72.837313 66.931672 ) ( -23.121393 -18.974523 -0.327713 ) ( 16.141300 72.687227 66.905165 ) ( -24.821818 -19.646140 -0.326737 ) ( 16.165131 72.515307 66.873474 ) ( -25.965707 -19.995502 -0.325497 ) ( 16.196787 72.319664 66.835680 ) ( -26.553075 -20.069814 -0.323940 ) ( 16.237830 72.098309 66.790767 ) ( -26.794724 -19.969871 -0.322033 ) ( 16.290230 71.848929 66.737494 ) ( -27.095587 -19.755434 -0.319153 ) ( 16.358585 71.514856 66.661644 ) ( -27.853889 -19.506769 -0.369999 ) ( 16.437701 71.063014 66.551461 ) ( -28.830154 -19.219202 -0.434204 ) ( 16.516771 70.520985 66.408710 ) ( -29.580326 -18.910144 -0.506447 ) ( 16.587428 69.971013 66.235662 ) ( -29.918144 -18.587730 -0.576355 ) ( 16.644058 69.434544 66.036010 ) ( -29.991529 -18.267112 -0.640469 ) ( 16.684107 68.873486 65.815444 ) ( -29.857283 -17.966620 -0.696018 ) ( 16.840717 68.304445 65.582013 ) ( -29.908004 -17.709158 -0.740734 ) ( 17.011300 67.745599 65.346336 ) ( -29.967597 -17.523010 -0.772602 ) ( 17.153157 67.217370 65.122013 ) ( -30.030994 -17.442892 -0.789601 ) ( 17.261487 66.743276 64.926081 ) ( -30.055995 -17.510727 -0.789200 ) ( 17.330412 66.351749 64.779940 ) ( -29.965432 -17.701704 -0.777535 ) ( 17.370527 66.022896 64.675422 ) ( -29.826560 -17.954569 -0.763714 ) ( 17.397281 65.713368 64.587231 ) ( -29.642978 -18.262686 -0.748764 ) ( 17.411103 65.421871 64.515123 ) ( -29.564958 -18.619791 -0.733761 ) ( 17.412823 65.147264 64.458761 ) ( -29.448977 -19.019868 -0.719769 ) ( 17.403280 64.915457 64.417739 ) ( -29.452071 -19.457281 -0.707943 ) ( 17.383122 64.823041 64.391651 ) ( -29.353580 -19.926702 -0.699562 ) ( 17.352624 64.757473 64.380016 ) ( -29.008358 -20.420604 -0.693497 ) ( 17.315301 64.717684 64.382296 ) ( -28.508599 -20.930702 -0.687641 ) ( 17.274663 64.807675 64.397888 ) ( -27.894280 -21.450933 -0.681950 ) ( 17.231008 64.902090 64.426117 ) ( -27.194858 -21.975504 -0.676399 ) ( 17.185029 65.001566 64.466276 ) ( -26.434729 -22.498784 -0.670921 ) ( 17.137645 65.106534 64.517536 ) ( -25.726063 -23.015488 -0.665482 ) ( 17.089864 65.217191 64.579048 ) ( -25.097222 -23.520572 -0.660011 ) ( 17.042670 65.333542 64.649868 ) ( -24.445982 -24.009427 -0.654453 ) ( 16.999408 65.455327 64.728966 ) ( -23.772702 -24.477818 -0.648766 ) ( 16.969141 65.582078 64.815311 ) ( -23.399312 -24.922005 -0.642876 ) ( 16.947238 65.713100 64.907775 ) ( -23.139479 -25.338771 -0.636742 ) ( 16.935617 65.847533 65.005255 ) ( -22.857728 -25.725458 -0.630303 ) ( 16.936532 65.984193 65.106554 ) ( -22.555840 -26.080020 -0.623520 ) ( 16.952512 66.121816 65.210562 ) ( -22.235920 -26.401066 -0.616337 ) ( 16.986200 66.258889 65.316133 ) ( -21.900456 -26.687829 -0.608723 ) ( 17.043637 66.393761 65.422158 ) ( -21.552203 -26.940178 -0.600628 ) ( 17.125161 66.696350 65.527611 ) ( -21.194222 -27.158643 -0.592030 ) ( 17.225499 67.042598 65.631499 ) ( -20.829785 -27.378385 -0.582897 ) ( 17.340150 67.382305 65.732927 ) ( -20.462365 -27.566763 -0.573212 ) ( 17.465174 67.713076 65.831108 ) ( -20.095599 -27.724339 -0.562969 ) ( 17.597047 68.032648 65.925326 ) ( -19.733253 -27.853517 -0.552143 ) ( 17.732527 68.340507 66.014986 ) ( -19.379168 -27.957119 -0.540756 ) ( 17.868590 68.795418 66.099606 ) ( -19.037251 -28.038224 -0.528790 ) ( 18.002324 69.226804 66.178813 ) ( -18.711473 -28.100176 -0.516281 ) ( 18.130863 69.632250 66.252322 ) ( -18.405813 -28.146416 -0.503239 ) ( 18.251317 70.009550 66.319919 ) ( -18.124299 -28.180368 -0.489693 ) ( 18.360690 70.356725 66.381512 ) ( -17.870989 -28.205377 -0.475689 ) ( 18.455793 70.671923 66.437030 ) ( -17.649976 -28.224562 -0.461270 ) ( 18.533132 70.953429 66.486483 ) ( -17.465411 -28.240591 -0.446480 ) ( 18.588773 71.199494 66.529808 ) ( -17.321544 -28.257466 -0.433235 ) ( 18.618156 71.408381 66.567030 ) ( -17.260242 -28.274943 -0.420014 ) ( 18.615845 71.578193 66.598048 ) ( -17.254539 -28.290145 -0.406601 ) ( 18.572209 71.717005 66.626396 ) ( -17.301722 -28.300070 -0.393070 ) ( 18.489044 71.835148 66.654863 ) ( -17.392238 -28.303922 -0.379513 ) ( 18.375280 71.934103 66.682377 ) ( -17.517895 -28.300191 -0.366028 ) ( 18.238075 72.015430 66.708093 ) ( -17.671465 -28.286887 -0.352715 ) ( 18.083488 72.080711 66.731348 ) ( -17.846417 -28.261761 -0.339703 ) ( 17.916901 72.131419 66.751680 ) ( -18.036747 -28.222462 -0.327107 ) ( 17.743375 72.168881 66.768780 ) ( -18.236829 -28.171225 -0.319660 ) ( 17.567900 72.194175 66.782470 ) ( -18.441291 -28.108764 -0.320549 ) ( 17.395656 72.208081 66.792688 ) ( -18.644905 -28.034074 -0.321240 ) ( 17.232280 72.210948 66.799440 ) ( -18.842459 -27.965784 -0.321724 ) ( 17.084235 72.202603 66.802797 ) ( -19.028578 -27.873317 -0.322006 ) ( 16.959304 72.181991 66.802669 ) ( -19.199691 -27.754619 -0.322086 ) ( 16.852844 72.149644 66.799984 ) ( -19.354932 -27.602527 -0.321972 ) ( 16.753618 72.107584 66.795945 ) ( -19.492517 -27.407548 -0.321697 ) ( 16.660964 72.056030 66.790652 ) ( -19.610511 -27.160641 -0.321263 ) ( 16.574299 71.995223 66.784258 ) ( -19.706873 -26.853611 -0.320694 ) ( 16.493093 71.925359 66.776834 ) ( -19.818227 -26.479313 -0.320000 ) ( 16.416863 71.846616 66.768427 ) ( -20.080488 -26.031929 -0.319183 ) ( 16.345178 71.759307 66.759138 ) ( -20.197225 -25.567815 -0.318272 ) ( 16.277639 71.663696 66.748977 ) ( -20.380001 -25.045906 -0.317257 ) ( 16.213866 71.560204 66.738029 ) ( -20.485673 -24.431503 -0.316155 ) ( 16.153518 71.449300 66.726332 ) ( -20.525100 -23.724174 -0.314964 ) ( 16.096265 71.331546 66.713934 ) ( -20.515985 -22.925953 -0.313698 ) ( 16.041801 71.207614 66.700883 ) ( -20.480254 -22.041296 -0.312363 ) ( 15.989822 71.078281 66.687255 ) ( -20.403621 -21.076976 -0.310955 ) ( 15.940046 70.944411 66.673107 ) ( -20.264187 -20.041879 -0.309490 ) ( 15.892193 70.806975 66.658506 ) ( -20.088580 -18.969906 -0.307968 ) ( 15.845988 70.721234 66.643553 ) ( -19.874942 -17.909063 -0.306393 ) ( 15.801160 70.670595 66.628318 ) ( -19.556926 -16.812055 -0.304772 ) ( 15.757440 70.616783 66.612916 ) ( -19.124449 -15.692378 -0.303105 ) ( 15.714557 70.560491 66.597447 ) ( -18.610263 -14.564094 -0.301403 ) ( 15.672228 70.502547 66.582060 ) ( -18.049492 -13.441409 -0.299667 ) ( 15.630176 70.443816 66.566868 ) ( -17.472337 -13.254941 -0.297905 ) ( 15.746903 70.385299 66.552057 ) ( -17.193474 -13.255202 -0.296120 ) ( 15.863894 70.328048 66.537781 ) ( -16.966579 -13.254949 -0.294308 ) ( 15.967922 70.273151 66.524200 ) ( -16.764480 -13.254365 -0.292488 ) ( 16.060696 70.221824 66.511545 ) ( -16.593932 -13.253609 -0.290665 ) ( 16.141918 70.175319 66.500025 ) ( -16.461806 -13.252817 -0.288838 ) ( 16.211822 70.134921 66.489882 ) ( -16.375059 -13.252103 -0.287014 ) ( 16.271204 70.101947 66.481366 ) } baseframe { ( -0.000000 0.016430 -0.006044 ) ( 0.707107 0.000242 0.707107 ) ( 31.228901 6.251943 9.236629 ) ( -0.022398 0.133633 0.852233 ) ( 0.003848 -11.026810 0.100900 ) ( 0.001203 -0.000819 0.001677 ) ( 26.002844 -2.030195 0.014076 ) ( 0.000000 -0.000000 -0.750740 ) ( 0.000000 4.130581 0.000000 ) ( -0.008569 -0.043541 0.136531 ) ( 0.000000 7.977538 -0.000000 ) ( 0.001867 -0.044560 -0.030738 ) ( -0.000001 13.487594 -0.000000 ) ( 0.030480 0.081639 0.353221 ) ( 0.000000 4.943967 0.000000 ) ( 0.000004 -0.000009 0.403507 ) ( -1.626972 11.080726 -7.581193 ) ( -0.972402 0.054889 0.001102 ) ( 0.000000 12.910863 -0.000000 ) ( -0.299251 0.030900 0.085823 ) ( -0.031254 11.193578 0.010376 ) ( 0.060311 0.000230 0.004107 ) ( 0.054595 0.018457 -0.015135 ) ( -0.176653 0.062347 0.335345 ) ( 0.000000 3.968525 0.000000 ) ( -0.039798 -0.055173 -0.355365 ) ( 0.000000 5.348644 -0.000000 ) ( -0.333183 -0.042087 -0.017112 ) ( 0.000000 2.438549 0.000000 ) ( -0.530821 -0.101863 0.096338 ) ( -0.655277 1.265096 -0.625148 ) ( -0.192019 -0.907991 -0.305115 ) ( 0.975809 10.864569 7.905795 ) ( 0.942812 0.268265 0.080217 ) ( 0.000000 13.379274 -0.000000 ) ( 0.179574 -0.160291 0.616037 ) ( 0.028222 10.991364 0.020885 ) ( -0.009560 0.110497 0.242448 ) ( 0.000004 -0.125166 -0.066536 ) ( 0.332332 -0.124874 0.331347 ) ( -0.000000 4.224101 0.000000 ) ( -0.226414 0.016245 -0.268104 ) ( -0.000000 5.160579 -0.000000 ) ( 0.363836 0.004691 0.037130 ) ( 0.000000 2.493865 -0.000000 ) ( 0.619859 -0.137444 -0.141287 ) ( -3.265696 3.709081 5.241207 ) ( -0.026046 0.012720 -0.997340 ) ( 0.000000 13.074334 0.000000 ) ( 0.020356 0.084646 0.038299 ) ( -0.035381 14.148866 0.000001 ) ( 0.000204 -0.010442 -0.504513 ) ( 0.000000 4.097309 -0.000000 ) ( 0.000147 0.001216 -0.253475 ) ( -0.000000 3.425298 -0.000000 ) ( -0.004317 0.002866 -0.069741 ) ( -3.261971 3.695171 -5.223284 ) ( -0.023142 -0.020279 -0.997340 ) ( -0.000000 13.074365 0.000000 ) ( -0.026086 -0.005701 0.039174 ) ( -0.035380 14.148879 0.012529 ) ( -0.000089 0.008070 -0.504150 ) ( -0.000000 4.097290 0.000000 ) ( 0.000000 0.000002 -0.256032 ) ( -0.000000 3.425305 -0.000000 ) ( 0.005016 -0.002226 -0.069715 ) } frame 0 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.228901 6.251943 9.236629 0.022398 -0.133633 -0.852233 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008569 0.043541 -0.136531 0.000000 7.977538 -0.000000 -0.001867 0.044560 0.030738 -0.000001 13.487594 -0.000000 -0.030480 -0.081639 -0.353221 0.000000 4.943967 0.000000 -0.000004 0.000009 -0.403507 -1.626972 11.080726 -7.581193 0.972402 -0.054889 -0.001102 0.000000 12.910863 -0.000000 0.299251 -0.030900 -0.085823 -0.031254 11.193578 0.010376 -0.060311 -0.000230 -0.004107 0.054595 0.018457 -0.015135 0.176653 -0.062347 -0.335345 0.000000 3.968525 0.000000 0.039798 0.055173 0.355365 0.000000 5.348644 -0.000000 0.333183 0.042087 0.017112 0.000000 2.438549 0.000000 0.530821 0.101863 -0.096338 -0.655277 1.265096 -0.625148 -0.192019 -0.907991 -0.305115 0.975809 10.864569 7.905795 -0.942812 -0.268265 -0.080217 0.000000 13.379274 -0.000000 -0.179574 0.160291 -0.616037 0.028222 10.991364 0.020885 0.009560 -0.110497 -0.242448 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.265696 3.709081 5.241207 0.026046 -0.012720 0.997340 0.000000 13.074334 0.000000 -0.020356 -0.084646 -0.038299 -0.035381 14.148866 0.000001 -0.000204 0.010442 0.504513 0.000000 4.097309 -0.000000 -0.000147 -0.001216 0.253475 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.223284 0.023142 0.020279 0.997340 -0.000000 13.074365 0.000000 0.026086 0.005701 -0.039174 -0.035380 14.148879 0.012529 0.000089 -0.008070 0.504150 -0.000000 4.097290 0.000000 -0.000000 -0.000002 0.256032 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 1 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.222026 6.236773 9.264546 0.024988 -0.134426 -0.851489 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008541 0.043391 -0.136426 0.000000 7.977538 -0.000000 -0.001779 0.044549 0.030951 -0.000001 13.487594 -0.000000 -0.030764 -0.082259 -0.353265 0.000000 4.943967 0.000000 0.000361 0.000378 -0.403283 -1.626874 11.081237 -7.581213 0.972424 -0.054681 -0.001101 0.000000 12.910863 -0.000000 0.296768 -0.029812 -0.089659 -0.031254 11.193578 0.010376 -0.060908 -0.000267 -0.004304 0.054595 0.018457 -0.015135 0.177324 -0.062355 -0.334922 0.000000 3.968525 0.000000 0.039691 0.055327 0.355585 0.000000 5.348644 -0.000000 0.334266 0.042215 0.017963 0.000000 2.438549 0.000000 0.530197 0.102228 -0.096753 -0.655277 1.265096 -0.625148 -0.193708 -0.908580 -0.302376 0.978884 10.880526 7.905150 -0.943543 -0.265304 -0.080549 0.000000 13.379274 -0.000000 -0.180379 0.161234 -0.612912 0.028222 10.991364 0.020885 0.008257 -0.110219 -0.243008 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.261482 3.709407 5.247099 0.026051 -0.012855 0.997330 0.000000 13.074334 0.000000 -0.020504 -0.084629 -0.037977 -0.035381 14.148866 0.000001 -0.000213 0.010438 0.504591 0.000000 4.097309 -0.000000 -0.000146 -0.001213 0.253449 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.218260 0.023873 0.020160 0.997333 -0.000000 13.074365 0.000000 0.025905 0.004986 -0.039393 -0.035380 14.148879 0.012529 0.000088 -0.008070 0.504136 -0.000000 4.097290 0.000000 -0.000000 -0.000001 0.256038 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 2 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.215008 6.221424 9.293080 0.027615 -0.135230 -0.850725 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008462 0.042959 -0.136125 0.000000 7.977538 -0.000000 -0.001672 0.044535 0.031175 -0.000001 13.487594 -0.000000 -0.031527 -0.083926 -0.353388 0.000000 4.943967 0.000000 0.001402 0.001432 -0.402644 -1.626597 11.082675 -7.581272 0.972484 -0.054098 -0.001097 0.000000 12.910863 -0.000000 0.294122 -0.028704 -0.093535 -0.031254 11.193578 0.010376 -0.061256 -0.000289 -0.004419 0.054595 0.018457 -0.015135 0.177714 -0.062359 -0.334675 0.000000 3.968525 0.000000 0.039628 0.055417 0.355713 0.000000 5.348644 -0.000000 0.334895 0.042290 0.018458 0.000000 2.438549 0.000000 0.529833 0.102440 -0.096994 -0.655277 1.265097 -0.625148 -0.195311 -0.909263 -0.299383 0.981969 10.896532 7.904502 -0.944289 -0.262231 -0.080920 0.000000 13.379274 -0.000000 -0.181182 0.162258 -0.609644 0.028222 10.991364 0.020885 0.006879 -0.109889 -0.243673 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.256169 3.709961 5.254688 0.026058 -0.013027 0.997316 0.000000 13.074334 0.000000 -0.020696 -0.084608 -0.037496 -0.035381 14.148866 0.000001 -0.000222 0.010433 0.504669 0.000000 4.097309 -0.000000 -0.000144 -0.001206 0.253373 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.211812 0.024811 0.020008 0.997321 -0.000000 13.074365 0.000000 0.025677 0.004070 -0.039613 -0.035380 14.148879 0.012529 0.000087 -0.008070 0.504122 -0.000000 4.097290 0.000000 0.000000 -0.000001 0.256055 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 3 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.207887 6.205945 9.322063 0.030270 -0.136042 -0.849945 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008336 0.042270 -0.135643 0.000000 7.977538 -0.000000 -0.001548 0.044520 0.031410 -0.000001 13.487594 -0.000000 -0.032655 -0.086386 -0.353578 0.000000 4.943967 0.000000 0.003044 0.003094 -0.401634 -1.626164 11.084918 -7.581362 0.972577 -0.053196 -0.001087 0.000000 12.910863 -0.000000 0.291352 -0.027580 -0.097441 -0.031254 11.193578 0.010376 -0.061388 -0.000297 -0.004462 0.054595 0.018457 -0.015135 0.177863 -0.062361 -0.334582 0.000000 3.968525 0.000000 0.039605 0.055451 0.355762 0.000000 5.348644 -0.000000 0.335134 0.042318 0.018646 0.000000 2.438549 0.000000 0.529695 0.102520 -0.097086 -0.655277 1.265097 -0.625148 -0.196850 -0.910011 -0.296204 0.985061 10.912575 7.903854 -0.945044 -0.259076 -0.081320 0.000000 13.379274 -0.000000 -0.181983 0.163342 -0.606268 0.028222 10.991364 0.020885 0.005446 -0.109519 -0.244415 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.249868 3.710721 5.263806 0.026066 -0.013232 0.997296 0.000000 13.074334 0.000000 -0.020929 -0.084584 -0.036872 -0.035381 14.148866 0.000001 -0.000231 0.010429 0.504748 0.000000 4.097309 -0.000000 -0.000140 -0.001193 0.253251 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.204082 0.025934 0.019826 0.997304 -0.000000 13.074365 0.000000 0.025405 0.002971 -0.039835 -0.035380 14.148879 0.012529 0.000086 -0.008070 0.504108 -0.000000 4.097290 0.000000 0.000001 0.000001 0.256082 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 4 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.200696 6.190378 9.351347 0.032944 -0.136860 -0.849151 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008168 0.041350 -0.134998 0.000000 7.977538 -0.000000 -0.001409 0.044504 0.031654 -0.000001 13.487594 -0.000000 -0.034045 -0.089413 -0.353825 0.000000 4.943967 0.000000 0.005220 0.005297 -0.400292 -1.625597 11.087862 -7.581481 0.972697 -0.052022 -0.001070 0.000000 12.910863 -0.000000 0.288496 -0.026444 -0.101367 -0.031254 11.193578 0.010376 -0.061336 -0.000294 -0.004445 0.054595 0.018457 -0.015135 0.177804 -0.062360 -0.334619 0.000000 3.968525 0.000000 0.039614 0.055438 0.355743 0.000000 5.348644 -0.000000 0.335039 0.042307 0.018571 0.000000 2.438549 0.000000 0.529749 0.102488 -0.097050 -0.655277 1.265097 -0.625148 -0.198344 -0.910796 -0.292900 0.988158 10.928642 7.903204 -0.945801 -0.255865 -0.081738 0.000000 13.379274 -0.000000 -0.182781 0.164463 -0.602815 0.028222 10.991364 0.020885 0.003978 -0.109123 -0.245208 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.242681 3.711664 5.274293 0.026076 -0.013468 0.997273 0.000000 13.074334 0.000000 -0.021197 -0.084556 -0.036120 -0.035381 14.148866 0.000001 -0.000240 0.010425 0.504827 0.000000 4.097309 -0.000000 -0.000134 -0.001177 0.253089 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.195203 0.027224 0.019618 0.997282 -0.000000 13.074365 0.000000 0.025095 0.001710 -0.040057 -0.035380 14.148879 0.012529 0.000085 -0.008071 0.504094 -0.000000 4.097290 0.000000 0.000002 0.000002 0.256119 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 5 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.193465 6.174762 9.380800 0.035627 -0.137680 -0.848347 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.007961 0.040219 -0.134204 0.000000 7.977538 -0.000000 -0.001256 0.044486 0.031906 -0.000001 13.487594 -0.000000 -0.035605 -0.092806 -0.354120 0.000000 4.943967 0.000000 0.007869 0.007980 -0.398650 -1.624912 11.091417 -7.581625 0.972840 -0.050617 -0.001044 0.000000 12.910863 -0.000000 0.285584 -0.025301 -0.105308 -0.031254 11.193578 0.010376 -0.061127 -0.000281 -0.004376 0.054595 0.018457 -0.015135 0.177570 -0.062358 -0.334767 0.000000 3.968525 0.000000 0.039651 0.055384 0.355666 0.000000 5.348644 -0.000000 0.334661 0.042262 0.018274 0.000000 2.438549 0.000000 0.529968 0.102361 -0.096905 -0.655277 1.265097 -0.625148 -0.199811 -0.911596 -0.289527 0.991257 10.944722 7.902553 -0.946554 -0.252620 -0.082167 0.000000 13.379274 -0.000000 -0.183576 0.165605 -0.599314 0.028222 10.991364 0.020885 0.002489 -0.108712 -0.246029 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.234705 3.712772 5.285999 0.026087 -0.013730 0.997245 0.000000 13.074334 0.000000 -0.021497 -0.084525 -0.035254 -0.035381 14.148866 0.000001 -0.000249 0.010420 0.504906 0.000000 4.097309 -0.000000 -0.000128 -0.001157 0.252890 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.185301 0.028661 0.019386 0.997255 -0.000000 13.074365 0.000000 0.024751 0.000304 -0.040281 -0.035380 14.148879 0.012529 0.000084 -0.008071 0.504080 -0.000000 4.097290 0.000000 0.000003 0.000004 0.256165 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 6 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.186224 6.159133 9.410300 0.038313 -0.138499 -0.847533 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.007720 0.038901 -0.133277 0.000000 7.977538 -0.000000 -0.001092 0.044466 0.032166 -0.000001 13.487594 -0.000000 -0.037251 -0.096380 -0.354456 0.000000 4.943967 0.000000 0.010938 0.011086 -0.396741 -1.624124 11.095505 -7.581790 0.973000 -0.049017 -0.001007 0.000000 12.910863 -0.000000 0.282649 -0.024153 -0.109255 -0.031254 11.193578 0.010376 -0.060787 -0.000260 -0.004264 0.054595 0.018457 -0.015135 0.177188 -0.062353 -0.335007 0.000000 3.968525 0.000000 0.039713 0.055296 0.355540 0.000000 5.348644 -0.000000 0.334044 0.042189 0.017789 0.000000 2.438549 0.000000 0.530324 0.102154 -0.096669 -0.655277 1.265097 -0.625148 -0.201267 -0.912390 -0.286135 0.994357 10.960806 7.901903 -0.947297 -0.249364 -0.082597 0.000000 13.379274 -0.000000 -0.184367 0.166750 -0.595790 0.028222 10.991364 0.020885 0.000995 -0.108296 -0.246856 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.226033 3.714024 5.298781 0.026099 -0.014016 0.997214 0.000000 13.074334 0.000000 -0.021825 -0.084492 -0.034288 -0.035381 14.148866 0.000001 -0.000259 0.010416 0.504986 0.000000 4.097309 -0.000000 -0.000121 -0.001134 0.252658 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.174498 0.030230 0.019133 0.997222 -0.000000 13.074365 0.000000 0.024376 -0.001230 -0.040506 -0.035380 14.148879 0.012529 0.000083 -0.008071 0.504065 -0.000000 4.097290 0.000000 0.000004 0.000007 0.256220 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 7 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.178999 6.143524 9.439730 0.040993 -0.139315 -0.846713 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.007448 0.037416 -0.132231 0.000000 7.977538 -0.000000 -0.000918 0.044446 0.032431 -0.000001 13.487594 -0.000000 -0.038907 -0.099964 -0.354827 0.000000 4.943967 0.000000 0.014375 0.014566 -0.394591 -1.623247 11.100056 -7.581974 0.973174 -0.047257 -0.000958 0.000000 12.910863 -0.000000 0.279718 -0.023005 -0.113203 -0.031254 11.193578 0.010376 -0.060339 -0.000232 -0.004116 0.054595 0.018457 -0.015135 0.176685 -0.062348 -0.335324 0.000000 3.968525 0.000000 0.039793 0.055181 0.355375 0.000000 5.348644 -0.000000 0.333233 0.042093 0.017152 0.000000 2.438549 0.000000 0.530792 0.101881 -0.096358 -0.655277 1.265097 -0.625148 -0.202727 -0.913158 -0.282773 0.997455 10.976885 7.901252 -0.948027 -0.246118 -0.083022 0.000000 13.379274 -0.000000 -0.185155 0.167883 -0.592269 0.028222 10.991364 0.020885 -0.000490 -0.107887 -0.247671 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.216753 3.715403 5.312502 0.026112 -0.014322 0.997180 0.000000 13.074334 0.000000 -0.022177 -0.084456 -0.033233 -0.035381 14.148866 0.000001 -0.000268 0.010411 0.505067 0.000000 4.097309 -0.000000 -0.000112 -0.001107 0.252396 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.162906 0.031911 0.018862 0.997184 -0.000000 13.074365 0.000000 0.023975 -0.002875 -0.040731 -0.035380 14.148879 0.012529 0.000082 -0.008071 0.504051 -0.000000 4.097290 0.000000 0.000005 0.000010 0.256283 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 8 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.171816 6.127967 9.468980 0.043662 -0.140127 -0.845889 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.007150 0.035782 -0.131077 0.000000 7.977538 -0.000000 -0.000734 0.044425 0.032702 -0.000001 13.487594 -0.000000 -0.040497 -0.103394 -0.355231 0.000000 4.943967 0.000000 0.018133 0.018370 -0.392226 -1.622293 11.105005 -7.582175 0.973359 -0.045366 -0.000894 0.000000 12.910863 -0.000000 0.276821 -0.021860 -0.117145 -0.031254 11.193578 0.010376 -0.059806 -0.000199 -0.003940 0.054595 0.018457 -0.015135 0.176086 -0.062341 -0.335702 0.000000 3.968525 0.000000 0.039889 0.055043 0.355179 0.000000 5.348644 -0.000000 0.332266 0.041978 0.016392 0.000000 2.438549 0.000000 0.531349 0.101555 -0.095987 -0.655277 1.265097 -0.625148 -0.204207 -0.913883 -0.279485 1.000551 10.992949 7.900603 -0.948740 -0.242900 -0.083435 0.000000 13.379274 -0.000000 -0.185939 0.168988 -0.588776 0.028222 10.991364 0.020885 -0.001952 -0.107493 -0.248454 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.206950 3.716890 5.327032 0.026126 -0.014647 0.997143 0.000000 13.074334 0.000000 -0.022550 -0.084419 -0.032103 -0.035381 14.148866 0.000001 -0.000278 0.010407 0.505147 0.000000 4.097309 -0.000000 -0.000103 -0.001078 0.252109 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.150635 0.033691 0.018575 0.997139 -0.000000 13.074365 0.000000 0.023552 -0.004616 -0.040957 -0.035380 14.148879 0.012529 0.000082 -0.008072 0.504036 -0.000000 4.097290 0.000000 0.000007 0.000013 0.256354 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 9 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.164699 6.112494 9.497941 0.046311 -0.140930 -0.845063 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.006827 0.034020 -0.129830 0.000000 7.977538 -0.000000 -0.000544 0.044403 0.032978 -0.000001 13.487594 -0.000000 -0.041949 -0.106510 -0.355664 0.000000 4.943967 0.000000 0.022168 0.022455 -0.389672 -1.621274 11.110295 -7.582389 0.973552 -0.043372 -0.000815 0.000000 12.910863 -0.000000 0.273984 -0.020720 -0.121075 -0.031254 11.193578 0.010376 -0.059207 -0.000162 -0.003742 0.054595 0.018457 -0.015135 0.175413 -0.062333 -0.336126 0.000000 3.968525 0.000000 0.039996 0.054888 0.354958 0.000000 5.348644 -0.000000 0.331181 0.041849 0.015541 0.000000 2.438549 0.000000 0.531975 0.101190 -0.095570 -0.655277 1.265097 -0.625148 -0.205721 -0.914548 -0.276318 1.003643 11.008990 7.899954 -0.949432 -0.239731 -0.083828 0.000000 13.379274 -0.000000 -0.186720 0.170050 -0.585334 0.028222 10.991364 0.020885 -0.003378 -0.107124 -0.249186 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.196703 3.718469 5.342248 0.026141 -0.014986 0.997104 0.000000 13.074334 0.000000 -0.022942 -0.084379 -0.030909 -0.035381 14.148866 0.000001 -0.000287 0.010402 0.505228 0.000000 4.097309 -0.000000 -0.000093 -0.001047 0.251799 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.137790 0.035553 0.018275 0.997089 -0.000000 13.074365 0.000000 0.023109 -0.006438 -0.041183 -0.035380 14.148879 0.012529 0.000081 -0.008072 0.504022 -0.000000 4.097290 0.000000 0.000009 0.000016 0.256431 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 10 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.157675 6.097137 9.526503 0.048935 -0.141724 -0.844237 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.006485 0.032146 -0.128500 0.000000 7.977538 -0.000000 -0.000346 0.044380 0.033257 -0.000001 13.487594 -0.000000 -0.043192 -0.109156 -0.356126 0.000000 4.943967 0.000000 0.026438 0.026778 -0.386951 -1.620199 11.115870 -7.582614 0.973749 -0.041302 -0.000717 0.000000 12.910863 -0.000000 0.271236 -0.019590 -0.124986 -0.031254 11.193578 0.010376 -0.058562 -0.000122 -0.003529 0.054595 0.018457 -0.015135 0.174688 -0.062324 -0.336582 0.000000 3.968525 0.000000 0.040112 0.054721 0.354720 0.000000 5.348644 -0.000000 0.330012 0.041711 0.014624 0.000000 2.438549 0.000000 0.532648 0.100797 -0.095122 -0.655277 1.265097 -0.625148 -0.207284 -0.915136 -0.273316 1.006728 11.024999 7.899306 -0.950099 -0.236630 -0.084194 0.000000 13.379274 -0.000000 -0.187497 0.171056 -0.581969 0.028222 10.991364 0.020885 -0.004755 -0.106790 -0.249849 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.186091 3.720124 5.358028 0.026156 -0.015337 0.997062 0.000000 13.074334 0.000000 -0.023347 -0.084338 -0.029663 -0.035381 14.148866 0.000001 -0.000297 0.010398 0.505309 0.000000 4.097309 -0.000000 -0.000082 -0.001014 0.251469 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.124470 0.037483 0.017964 0.997033 -0.000000 13.074365 0.000000 0.022650 -0.008326 -0.041409 -0.035380 14.148879 0.012529 0.000080 -0.008072 0.504007 -0.000000 4.097290 0.000000 0.000011 0.000020 0.256516 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 11 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.150769 6.081928 9.554554 0.051526 -0.142506 -0.843415 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.006126 0.030179 -0.127100 0.000000 7.977538 -0.000000 -0.000144 0.044356 0.033539 -0.000001 13.487594 -0.000000 -0.044153 -0.111170 -0.356616 0.000000 4.943967 0.000000 0.030903 0.031298 -0.384087 -1.619080 11.121678 -7.582849 0.973948 -0.039182 -0.000599 0.000000 12.910863 -0.000000 0.268606 -0.018473 -0.128873 -0.031254 11.193578 0.010376 -0.057890 -0.000080 -0.003307 0.054595 0.018457 -0.015135 0.173933 -0.062315 -0.337057 0.000000 3.968525 0.000000 0.040233 0.054548 0.354472 0.000000 5.348644 -0.000000 0.328794 0.041566 0.013670 0.000000 2.438549 0.000000 0.533349 0.100387 -0.094655 -0.655277 1.265097 -0.625148 -0.208911 -0.915632 -0.270526 1.009806 11.040966 7.898661 -0.950738 -0.233617 -0.084526 0.000000 13.379274 -0.000000 -0.188271 0.171989 -0.578707 0.028222 10.991364 0.020885 -0.006069 -0.106499 -0.250423 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.175188 3.721840 5.374259 0.026172 -0.015698 0.997019 0.000000 13.074334 0.000000 -0.023765 -0.084296 -0.028375 -0.035381 14.148866 0.000001 -0.000306 0.010393 0.505390 0.000000 4.097309 -0.000000 -0.000071 -0.000979 0.251124 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.110774 0.039467 0.017644 0.996970 -0.000000 13.074365 0.000000 0.022178 -0.010267 -0.041636 -0.035380 14.148879 0.012529 0.000079 -0.008072 0.503992 -0.000000 4.097290 0.000000 0.000013 0.000024 0.256606 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 12 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.144008 6.066902 9.581977 0.054077 -0.143274 -0.842598 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.005753 0.028135 -0.125642 0.000000 7.977538 -0.000000 0.000061 0.044333 0.033824 -0.000001 13.487594 -0.000000 -0.044757 -0.112388 -0.357135 0.000000 4.943967 0.000000 0.035523 0.035975 -0.381102 -1.617925 11.127669 -7.583091 0.974146 -0.037037 -0.000459 0.000000 12.910863 -0.000000 0.266124 -0.017372 -0.132729 -0.031254 11.193578 0.010376 -0.057208 -0.000038 -0.003082 0.054595 0.018457 -0.015135 0.173167 -0.062306 -0.337538 0.000000 3.968525 0.000000 0.040356 0.054371 0.354220 0.000000 5.348644 -0.000000 0.327560 0.041420 0.012703 0.000000 2.438549 0.000000 0.534058 0.099971 -0.094181 -0.655277 1.265097 -0.625148 -0.210619 -0.916019 -0.267996 1.012873 11.056883 7.898017 -0.951346 -0.230714 -0.084817 0.000000 13.379274 -0.000000 -0.189043 0.172835 -0.575574 0.028222 10.991364 0.020885 -0.007306 -0.106264 -0.250890 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.164067 3.723601 5.390826 0.026189 -0.016067 0.996974 0.000000 13.074334 0.000000 -0.024191 -0.084252 -0.027055 -0.035381 14.148866 0.000001 -0.000316 0.010389 0.505472 0.000000 4.097309 -0.000000 -0.000059 -0.000943 0.250765 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.096794 0.041491 0.017318 0.996903 -0.000000 13.074365 0.000000 0.021697 -0.012247 -0.041863 -0.035380 14.148879 0.012529 0.000078 -0.008073 0.503978 -0.000000 4.097290 0.000000 0.000015 0.000028 0.256703 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 13 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.137421 6.052094 9.608648 0.056580 -0.144024 -0.841791 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.005369 0.026030 -0.124135 0.000000 7.977538 -0.000000 0.000270 0.044309 0.034110 -0.000001 13.487594 -0.000000 -0.044925 -0.112633 -0.357683 0.000000 4.943967 0.000000 0.040261 0.040772 -0.378018 -1.616745 11.133792 -7.583339 0.974343 -0.034893 -0.000294 0.000000 12.910863 -0.000000 0.263822 -0.016292 -0.136548 -0.031254 11.193578 0.010376 -0.056534 0.000004 -0.002860 0.054595 0.018457 -0.015135 0.172410 -0.062297 -0.338013 0.000000 3.968525 0.000000 0.040477 0.054197 0.353972 0.000000 5.348644 -0.000000 0.326341 0.041275 0.011748 0.000000 2.438549 0.000000 0.534759 0.099560 -0.093713 -0.655277 1.265097 -0.625148 -0.212425 -0.916278 -0.265776 1.015929 11.072739 7.897376 -0.951920 -0.227943 -0.085058 0.000000 13.379274 -0.000000 -0.189812 0.173577 -0.572598 0.028222 10.991364 0.020885 -0.008451 -0.106093 -0.251227 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.152797 3.725393 5.407622 0.026205 -0.016441 0.996928 0.000000 13.074334 0.000000 -0.024622 -0.084208 -0.025714 -0.035381 14.148866 0.000001 -0.000325 0.010384 0.505553 0.000000 4.097309 -0.000000 -0.000047 -0.000906 0.250395 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.082623 0.043542 0.016987 0.996830 -0.000000 13.074365 0.000000 0.021210 -0.014254 -0.042089 -0.035380 14.148879 0.012529 0.000077 -0.008073 0.503963 -0.000000 4.097290 0.000000 0.000018 0.000033 0.256805 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 14 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.131038 6.037544 9.634434 0.059026 -0.144755 -0.840995 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.004977 0.023881 -0.122591 0.000000 7.977538 -0.000000 0.000479 0.044285 0.034397 -0.000001 13.487594 -0.000000 -0.044570 -0.111716 -0.358259 0.000000 4.943967 0.000000 0.045080 0.045650 -0.374859 -1.615549 11.139997 -7.583590 0.974535 -0.032775 -0.000103 0.000000 12.910863 -0.000000 0.261735 -0.015235 -0.140321 -0.031254 11.193578 0.010376 -0.055886 0.000044 -0.002646 0.054595 0.018457 -0.015135 0.171682 -0.062289 -0.338470 0.000000 3.968525 0.000000 0.040593 0.054030 0.353733 0.000000 5.348644 -0.000000 0.325168 0.041136 0.010831 0.000000 2.438549 0.000000 0.535432 0.099165 -0.093263 -0.655277 1.265097 -0.625148 -0.214347 -0.916391 -0.263921 1.018971 11.088524 7.896737 -0.952456 -0.225328 -0.085242 0.000000 13.379274 -0.000000 -0.190579 0.174198 -0.569812 0.028222 10.991364 0.020885 -0.009488 -0.106000 -0.251413 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.141449 3.727201 5.424541 0.026222 -0.016817 0.996881 0.000000 13.074334 0.000000 -0.025057 -0.084164 -0.024362 -0.035381 14.148866 0.000001 -0.000335 0.010380 0.505634 0.000000 4.097309 -0.000000 -0.000035 -0.000868 0.250018 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.068349 0.045607 0.016654 0.996752 -0.000000 13.074365 0.000000 0.020720 -0.016274 -0.042316 -0.035380 14.148879 0.012529 0.000076 -0.008073 0.503948 -0.000000 4.097290 0.000000 0.000020 0.000037 0.256912 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 15 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.124895 6.023293 9.659188 0.061408 -0.145463 -0.840215 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.004580 0.021702 -0.121020 0.000000 7.977538 -0.000000 0.000689 0.044260 0.034684 -0.000001 13.487594 -0.000000 -0.043599 -0.109427 -0.358862 0.000000 4.943967 0.000000 0.049944 0.050574 -0.371647 -1.614348 11.146232 -7.583842 0.974721 -0.030711 0.000120 0.000000 12.910863 -0.000000 0.259902 -0.014207 -0.144042 -0.031254 11.193578 0.010376 -0.055281 0.000081 -0.002446 0.054595 0.018457 -0.015135 0.171002 -0.062281 -0.338896 0.000000 3.968525 0.000000 0.040702 0.053873 0.353509 0.000000 5.348644 -0.000000 0.324073 0.041007 0.009975 0.000000 2.438549 0.000000 0.536061 0.098795 -0.092842 -0.655277 1.265097 -0.625148 -0.216405 -0.916336 -0.262492 1.021997 11.104226 7.896102 -0.952950 -0.222895 -0.085358 0.000000 13.379274 -0.000000 -0.191345 0.174677 -0.567249 0.028222 10.991364 0.020885 -0.010398 -0.105996 -0.251422 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.130087 3.729012 5.441480 0.026238 -0.017194 0.996833 0.000000 13.074334 0.000000 -0.025492 -0.084119 -0.023009 -0.035381 14.148866 0.000001 -0.000344 0.010375 0.505716 0.000000 4.097309 -0.000000 -0.000023 -0.000829 0.249636 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.054058 0.047673 0.016320 0.996670 -0.000000 13.074365 0.000000 0.020229 -0.018296 -0.042542 -0.035380 14.148879 0.012529 0.000075 -0.008073 0.503933 -0.000000 4.097290 0.000000 0.000023 0.000042 0.257023 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 16 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.119028 6.009390 9.682744 0.063714 -0.146145 -0.839455 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.004181 0.019508 -0.119432 0.000000 7.977538 -0.000000 0.000898 0.044236 0.034971 -0.000001 13.487594 -0.000000 -0.041903 -0.105530 -0.359485 0.000000 4.943967 0.000000 0.054815 0.055505 -0.368406 -1.613151 11.152442 -7.584093 0.974901 -0.028730 0.000376 0.000000 12.910863 -0.000000 0.258365 -0.013213 -0.147701 -0.031254 11.193578 0.010376 -0.054736 0.000115 -0.002266 0.054595 0.018457 -0.015135 0.170390 -0.062273 -0.339280 0.000000 3.968525 0.000000 0.040800 0.053732 0.353308 0.000000 5.348644 -0.000000 0.323088 0.040890 0.009205 0.000000 2.438549 0.000000 0.536626 0.098463 -0.092464 -0.655277 1.265097 -0.625148 -0.218623 -0.916089 -0.261555 1.025005 11.119831 7.895471 -0.953400 -0.220674 -0.085397 0.000000 13.379274 -0.000000 -0.192111 0.174995 -0.564948 0.028222 10.991364 0.020885 -0.011161 -0.106096 -0.251227 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.118779 3.730811 5.458336 0.026255 -0.017569 0.996786 0.000000 13.074334 0.000000 -0.025925 -0.084075 -0.021663 -0.035381 14.148866 0.000001 -0.000354 0.010371 0.505797 0.000000 4.097309 -0.000000 -0.000010 -0.000791 0.249252 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.039836 0.049728 0.015989 0.996584 -0.000000 13.074365 0.000000 0.019740 -0.020308 -0.042768 -0.035380 14.148879 0.012529 0.000074 -0.008074 0.503918 -0.000000 4.097290 0.000000 0.000025 0.000047 0.257139 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 17 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.113481 5.995889 9.704917 0.065933 -0.146798 -0.838718 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.003781 0.017315 -0.117837 0.000000 7.977538 -0.000000 0.001105 0.044212 0.035256 -0.000001 13.487594 -0.000000 -0.039361 -0.099752 -0.360120 0.000000 4.943967 0.000000 0.059657 0.060407 -0.365160 -1.611971 11.158567 -7.584341 0.975071 -0.026863 0.000669 0.000000 12.910863 -0.000000 0.257174 -0.012259 -0.151288 -0.031254 11.193578 0.010376 -0.054269 0.000144 -0.002112 0.054595 0.018457 -0.015135 0.169865 -0.062267 -0.339609 0.000000 3.968525 0.000000 0.040884 0.053611 0.353135 0.000000 5.348644 -0.000000 0.322243 0.040789 0.008546 0.000000 2.438549 0.000000 0.537110 0.098178 -0.092139 -0.655277 1.265097 -0.625148 -0.221025 -0.915621 -0.261186 1.027991 11.135325 7.894844 -0.953799 -0.218698 -0.085346 0.000000 13.379274 -0.000000 -0.192876 0.175125 -0.562951 0.028222 10.991364 0.020885 -0.011756 -0.106316 -0.250795 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.107589 3.732587 5.475010 0.026271 -0.017940 0.996738 0.000000 13.074334 0.000000 -0.026353 -0.084030 -0.020335 -0.035381 14.148866 0.000001 -0.000363 0.010366 0.505878 0.000000 4.097309 -0.000000 0.000002 -0.000752 0.248869 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.025767 0.051760 0.015660 0.996494 -0.000000 13.074365 0.000000 0.019257 -0.022297 -0.042993 -0.035380 14.148879 0.012529 0.000073 -0.008074 0.503904 -0.000000 4.097290 0.000000 0.000028 0.000053 0.257259 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 18 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.108304 5.982852 9.725492 0.068052 -0.147416 -0.838012 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.003385 0.015136 -0.116246 0.000000 7.977538 -0.000000 0.001309 0.044189 0.035539 -0.000001 13.487594 -0.000000 -0.035829 -0.091775 -0.360750 0.000000 4.943967 0.000000 0.064433 0.065243 -0.361935 -1.610819 11.164543 -7.584582 0.975231 -0.025145 0.001006 0.000000 12.910863 -0.000000 0.256383 -0.011350 -0.154792 -0.031254 11.193578 0.010376 -0.053898 0.000167 -0.001989 0.054595 0.018457 -0.015135 0.169448 -0.062262 -0.339870 0.000000 3.968525 0.000000 0.040950 0.053515 0.352998 0.000000 5.348644 -0.000000 0.321573 0.040710 0.008022 0.000000 2.438549 0.000000 0.537495 0.097952 -0.091881 -0.655277 1.265097 -0.625148 -0.223642 -0.914898 -0.261473 1.030952 11.150689 7.894223 -0.954143 -0.217007 -0.085191 0.000000 13.379274 -0.000000 -0.193643 0.175040 -0.561307 0.028222 10.991364 0.020885 -0.012156 -0.106675 -0.250091 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.096580 3.734324 5.491404 0.026287 -0.018304 0.996691 0.000000 13.074334 0.000000 -0.026774 -0.083986 -0.019034 -0.035381 14.148866 0.000001 -0.000373 0.010362 0.505960 0.000000 4.097309 -0.000000 0.000014 -0.000714 0.248488 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.011934 0.053757 0.015338 0.996402 -0.000000 13.074365 0.000000 0.018781 -0.024252 -0.043218 -0.035380 14.148879 0.012529 0.000072 -0.008074 0.503889 -0.000000 4.097290 0.000000 0.000031 0.000058 0.257383 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 19 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.103555 5.970353 9.744214 0.070056 -0.147996 -0.837341 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.002995 0.012987 -0.114666 0.000000 7.977538 -0.000000 0.001509 0.044166 0.035820 -0.000001 13.487594 -0.000000 -0.031136 -0.081222 -0.361347 0.000000 4.943967 0.000000 0.069107 0.069974 -0.358758 -1.609710 11.170296 -7.584815 0.975380 -0.023616 0.001390 0.000000 12.910863 -0.000000 0.256059 -0.010494 -0.158200 -0.031254 11.193578 0.010376 -0.053641 0.000183 -0.001904 0.054595 0.018457 -0.015135 0.169160 -0.062258 -0.340050 0.000000 3.968525 0.000000 0.040997 0.053449 0.352903 0.000000 5.348644 -0.000000 0.321109 0.040655 0.007660 0.000000 2.438549 0.000000 0.537760 0.097795 -0.091703 -0.655277 1.265097 -0.625148 -0.226506 -0.913876 -0.262518 1.033884 11.165905 7.893607 -0.954425 -0.215646 -0.084917 0.000000 13.379274 -0.000000 -0.194412 0.174707 -0.560075 0.028222 10.991364 0.020885 -0.012330 -0.107194 -0.249071 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.085815 3.736011 5.507420 0.026302 -0.018661 0.996645 0.000000 13.074334 0.000000 -0.027184 -0.083943 -0.017768 -0.035381 14.148866 0.000001 -0.000382 0.010357 0.506041 0.000000 4.097309 -0.000000 0.000026 -0.000676 0.248113 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.998417 0.055708 0.015022 0.996309 -0.000000 13.074365 0.000000 0.018316 -0.026161 -0.043442 -0.035380 14.148879 0.012529 0.000071 -0.008074 0.503874 -0.000000 4.097290 0.000000 0.000034 0.000064 0.257510 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 20 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.098596 5.957820 9.761416 0.071980 -0.148538 -0.836689 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.002612 0.010880 -0.113109 0.000000 7.977538 -0.000000 0.001703 0.044143 0.036098 -0.000001 13.487594 -0.000000 -0.024225 -0.065733 -0.361896 0.000000 4.943967 0.000000 0.073639 0.074562 -0.355655 -1.608724 11.175412 -7.585022 0.975533 -0.022124 0.001682 0.000000 12.910863 -0.000000 0.256305 -0.009800 -0.161100 -0.031254 11.193578 0.010376 -0.053519 0.000191 -0.001864 0.054595 0.018457 -0.015135 0.169022 -0.062256 -0.340136 0.000000 3.968525 0.000000 0.041019 0.053417 0.352858 0.000000 5.348644 -0.000000 0.320887 0.040629 0.007487 0.000000 2.438549 0.000000 0.537887 0.097720 -0.091618 -0.655277 1.265097 -0.625148 -0.229588 -0.912431 -0.264748 1.036812 11.181094 7.892993 -0.954643 -0.214633 -0.084491 0.000000 13.379274 -0.000000 -0.195246 0.174104 -0.559301 0.028222 10.991364 0.020885 -0.012300 -0.108042 -0.247657 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.075358 3.737634 5.522961 0.026317 -0.019007 0.996600 0.000000 13.074334 0.000000 -0.027582 -0.083902 -0.016546 -0.035381 14.148866 0.000001 -0.000391 0.010353 0.506122 0.000000 4.097309 -0.000000 0.000038 -0.000639 0.247746 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.985298 0.057600 0.014716 0.996214 -0.000000 13.074365 0.000000 0.017865 -0.028013 -0.043666 -0.035380 14.148879 0.012529 0.000070 -0.008075 0.503860 -0.000000 4.097290 0.000000 0.000037 0.000070 0.257640 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 21 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.092965 5.944811 9.777611 0.073865 -0.149050 -0.836038 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.002241 0.008831 -0.111584 0.000000 7.977538 -0.000000 0.001891 0.044121 0.036372 -0.000001 13.487594 -0.000000 -0.014862 -0.044781 -0.362307 0.000000 4.943967 0.000000 0.077989 0.078966 -0.352657 -1.607893 11.179723 -7.585196 0.975704 -0.020524 0.001790 0.000000 12.910863 -0.000000 0.257107 -0.009324 -0.163254 -0.031254 11.193578 0.010376 -0.053551 0.000189 -0.001874 0.054595 0.018457 -0.015135 0.169058 -0.062257 -0.340114 0.000000 3.968525 0.000000 0.041013 0.053426 0.352870 0.000000 5.348644 -0.000000 0.320945 0.040635 0.007532 0.000000 2.438549 0.000000 0.537854 0.097740 -0.091640 -0.655277 1.265097 -0.625148 -0.232822 -0.910527 -0.268293 1.039757 11.196375 7.892375 -0.954807 -0.213925 -0.083914 0.000000 13.379274 -0.000000 -0.196185 0.173260 -0.558945 0.028222 10.991364 0.020885 -0.012118 -0.109292 -0.245854 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.065270 3.739181 5.537932 0.026332 -0.019340 0.996557 0.000000 13.074334 0.000000 -0.027966 -0.083861 -0.015378 -0.035381 14.148866 0.000001 -0.000401 0.010348 0.506202 0.000000 4.097309 -0.000000 0.000050 -0.000604 0.247390 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.972658 0.059422 0.014422 0.996120 -0.000000 13.074365 0.000000 0.017430 -0.029798 -0.043889 -0.035380 14.148879 0.012529 0.000070 -0.008075 0.503845 -0.000000 4.097290 0.000000 0.000040 0.000075 0.257773 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 22 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.086961 5.931582 9.792780 0.075704 -0.149534 -0.835395 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.001883 0.006852 -0.110100 0.000000 7.977538 -0.000000 0.002072 0.044100 0.036641 -0.000001 13.487594 -0.000000 -0.003904 -0.020274 -0.362458 0.000000 4.943967 0.000000 0.082117 0.083145 -0.349795 -1.607166 11.183493 -7.585349 0.975884 -0.018873 0.001782 0.000000 12.910863 -0.000000 0.258374 -0.009006 -0.164887 -0.031254 11.193578 0.010376 -0.053759 0.000176 -0.001943 0.054595 0.018457 -0.015135 0.169292 -0.062260 -0.339967 0.000000 3.968525 0.000000 0.040975 0.053480 0.352947 0.000000 5.348644 -0.000000 0.321322 0.040680 0.007826 0.000000 2.438549 0.000000 0.537638 0.097867 -0.091785 -0.655277 1.265097 -0.625148 -0.236202 -0.908238 -0.272862 1.042715 11.211722 7.891754 -0.954922 -0.213486 -0.083212 0.000000 13.379274 -0.000000 -0.197195 0.172208 -0.558947 0.028222 10.991364 0.020885 -0.011790 -0.110844 -0.243728 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.055613 3.740638 5.552236 0.026345 -0.019659 0.996516 0.000000 13.074334 0.000000 -0.028331 -0.083822 -0.014273 -0.035381 14.148866 0.000001 -0.000410 0.010344 0.506283 0.000000 4.097309 -0.000000 0.000061 -0.000569 0.247047 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.960577 0.061163 0.014140 0.996028 -0.000000 13.074365 0.000000 0.017014 -0.031502 -0.044112 -0.035380 14.148879 0.012529 0.000069 -0.008075 0.503830 -0.000000 4.097290 0.000000 0.000043 0.000081 0.257909 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 23 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.080849 5.918362 9.806893 0.077486 -0.149992 -0.834765 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.001540 0.004959 -0.108666 0.000000 7.977538 -0.000000 0.002245 0.044080 0.036905 -0.000001 13.487594 -0.000000 0.007936 0.006204 -0.362275 0.000000 4.943967 0.000000 0.085978 0.087054 -0.347101 -1.606499 11.186954 -7.585489 0.976069 -0.017220 0.001723 0.000000 12.910863 -0.000000 0.260027 -0.008793 -0.166194 -0.031254 11.193578 0.010376 -0.054167 0.000150 -0.002078 0.054595 0.018457 -0.015135 0.169750 -0.062265 -0.339681 0.000000 3.968525 0.000000 0.040902 0.053585 0.353097 0.000000 5.348644 -0.000000 0.322060 0.040768 0.008402 0.000000 2.438549 0.000000 0.537216 0.098116 -0.092068 -0.655277 1.265097 -0.625148 -0.239719 -0.905627 -0.278201 1.045680 11.227109 7.891132 -0.954995 -0.213283 -0.082408 0.000000 13.379274 -0.000000 -0.198246 0.170973 -0.559258 0.028222 10.991364 0.020885 -0.011323 -0.112609 -0.241337 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.046449 3.741992 5.565778 0.026358 -0.019961 0.996477 0.000000 13.074334 0.000000 -0.028677 -0.083785 -0.013239 -0.035381 14.148866 0.000001 -0.000420 0.010339 0.506363 0.000000 4.097309 -0.000000 0.000071 -0.000536 0.246720 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.949136 0.062811 0.013873 0.995937 -0.000000 13.074365 0.000000 0.016619 -0.033116 -0.044333 -0.035380 14.148879 0.012529 0.000068 -0.008075 0.503816 -0.000000 4.097290 0.000000 0.000047 0.000087 0.258047 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 24 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.074874 5.905360 9.819918 0.079203 -0.150426 -0.834155 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.001217 0.003163 -0.107292 0.000000 7.977538 -0.000000 0.002408 0.044061 0.037164 -0.000001 13.487594 -0.000000 0.020016 0.033220 -0.361741 0.000000 4.943967 0.000000 0.089527 0.090646 -0.344612 -1.605852 11.190315 -7.585625 0.976252 -0.015610 0.001668 0.000000 12.910863 -0.000000 0.261995 -0.008636 -0.167354 -0.031254 11.193578 0.010376 -0.054800 0.000111 -0.002287 0.054595 0.018457 -0.015135 0.170461 -0.062274 -0.339235 0.000000 3.968525 0.000000 0.040788 0.053749 0.353331 0.000000 5.348644 -0.000000 0.323204 0.040903 0.009296 0.000000 2.438549 0.000000 0.536560 0.098502 -0.092508 -0.655277 1.265097 -0.625148 -0.243370 -0.902751 -0.284082 1.048649 11.242512 7.890509 -0.955030 -0.213290 -0.081523 0.000000 13.379274 -0.000000 -0.199311 0.169578 -0.559835 0.028222 10.991364 0.020885 -0.010719 -0.114505 -0.238732 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.037842 3.743230 5.578460 0.026370 -0.020245 0.996441 0.000000 13.074334 0.000000 -0.029000 -0.083750 -0.012285 -0.035381 14.148866 0.000001 -0.000429 0.010335 0.506443 0.000000 4.097309 -0.000000 0.000081 -0.000505 0.246411 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.938416 0.064355 0.013622 0.995851 -0.000000 13.074365 0.000000 0.016248 -0.034627 -0.044554 -0.035380 14.148879 0.012529 0.000067 -0.008075 0.503802 -0.000000 4.097290 0.000000 0.000050 0.000094 0.258186 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 25 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.069264 5.892774 9.831813 0.080848 -0.150837 -0.833568 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000914 0.001480 -0.105988 0.000000 7.977538 -0.000000 0.002560 0.044042 0.037416 -0.000001 13.487594 -0.000000 0.031712 0.059385 -0.360902 0.000000 4.943967 0.000000 0.092713 0.093872 -0.342367 -1.605185 11.193772 -7.585764 0.976429 -0.014084 0.001669 0.000000 12.910863 -0.000000 0.264216 -0.008490 -0.168533 -0.031254 11.193578 0.010376 -0.055685 0.000056 -0.002579 0.054595 0.018457 -0.015135 0.171456 -0.062286 -0.338612 0.000000 3.968525 0.000000 0.040629 0.053978 0.353658 0.000000 5.348644 -0.000000 0.324805 0.041093 0.010547 0.000000 2.438549 0.000000 0.535641 0.099042 -0.093123 -0.655277 1.265097 -0.625148 -0.247154 -0.899666 -0.290300 1.051616 11.257909 7.889886 -0.955031 -0.213484 -0.080575 0.000000 13.379274 -0.000000 -0.200365 0.168043 -0.560640 0.028222 10.991364 0.020885 -0.009982 -0.116461 -0.235957 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.029853 3.744341 5.590185 0.026381 -0.020507 0.996409 0.000000 13.074334 0.000000 -0.029299 -0.083718 -0.011421 -0.035381 14.148866 0.000001 -0.000438 0.010330 0.506522 0.000000 4.097309 -0.000000 0.000091 -0.000476 0.246122 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.928498 0.065783 0.013391 0.995769 -0.000000 13.074365 0.000000 0.015905 -0.036025 -0.044774 -0.035380 14.148879 0.012529 0.000066 -0.008076 0.503787 -0.000000 4.097290 0.000000 0.000053 0.000100 0.258327 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 26 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.064236 5.880794 9.842532 0.082410 -0.151227 -0.833011 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000635 -0.000076 -0.104762 0.000000 7.977538 -0.000000 0.002701 0.044026 0.037661 -0.000001 13.487594 -0.000000 0.042386 0.083267 -0.359864 0.000000 4.943967 0.000000 0.095482 0.096675 -0.340406 -1.604465 11.197511 -7.585916 0.976595 -0.012683 0.001779 0.000000 12.910863 -0.000000 0.266632 -0.008314 -0.169887 -0.031254 11.193578 0.010376 -0.056853 -0.000016 -0.002965 0.054595 0.018457 -0.015135 0.172768 -0.062302 -0.337788 0.000000 3.968525 0.000000 0.040419 0.054280 0.354089 0.000000 5.348644 -0.000000 0.326919 0.041344 0.012201 0.000000 2.438549 0.000000 0.534427 0.099754 -0.093935 -0.655277 1.265097 -0.625148 -0.251071 -0.896425 -0.296666 1.054578 11.273278 7.889264 -0.955001 -0.213844 -0.079581 0.000000 13.379274 -0.000000 -0.201384 0.166386 -0.561638 0.028222 10.991364 0.020885 -0.009112 -0.118407 -0.233054 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.022547 3.745309 5.600856 0.026391 -0.020746 0.996380 0.000000 13.074334 0.000000 -0.029570 -0.083688 -0.010655 -0.035381 14.148866 0.000001 -0.000448 0.010326 0.506602 0.000000 4.097309 -0.000000 0.000099 -0.000449 0.245857 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.919465 0.067083 0.013179 0.995693 -0.000000 13.074365 0.000000 0.015591 -0.037298 -0.044994 -0.035380 14.148879 0.012529 0.000065 -0.008076 0.503773 -0.000000 4.097290 0.000000 0.000056 0.000106 0.258470 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 27 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.060006 5.869606 9.852020 0.083882 -0.151597 -0.832490 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000381 -0.001493 -0.103625 0.000000 7.977538 -0.000000 0.002829 0.044010 0.037898 -0.000001 13.487594 -0.000000 0.051327 0.103280 -0.358803 0.000000 4.943967 0.000000 0.097775 0.098997 -0.338777 -1.603655 11.201711 -7.586085 0.976748 -0.011446 0.002044 0.000000 12.910863 -0.000000 0.269191 -0.008067 -0.171567 -0.031254 11.193578 0.010376 -0.058338 -0.000108 -0.003455 0.054595 0.018457 -0.015135 0.174436 -0.062321 -0.336740 0.000000 3.968525 0.000000 0.040153 0.054663 0.354637 0.000000 5.348644 -0.000000 0.329607 0.041663 0.014306 0.000000 2.438549 0.000000 0.532882 0.100660 -0.094966 -0.655277 1.265097 -0.625148 -0.255124 -0.893083 -0.303001 1.057531 11.288600 7.888645 -0.954943 -0.214352 -0.078557 0.000000 13.379274 -0.000000 -0.202346 0.164623 -0.562798 0.028222 10.991364 0.020885 -0.008110 -0.120278 -0.230062 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.015987 3.746122 5.610374 0.026399 -0.020960 0.996355 0.000000 13.074334 0.000000 -0.029811 -0.083661 -0.009997 -0.035381 14.148866 0.000001 -0.000457 0.010322 0.506680 0.000000 4.097309 -0.000000 0.000107 -0.000425 0.245618 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.911400 0.068243 0.012991 0.995625 -0.000000 13.074365 0.000000 0.015309 -0.038435 -0.045212 -0.035380 14.148879 0.012529 0.000064 -0.008076 0.503759 -0.000000 4.097290 0.000000 0.000060 0.000113 0.258613 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 28 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.056787 5.859398 9.860214 0.085253 -0.151947 -0.832009 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000157 -0.002755 -0.102586 0.000000 7.977538 -0.000000 0.002944 0.043996 0.038127 -0.000001 13.487594 -0.000000 0.057682 0.117509 -0.357955 0.000000 4.943967 0.000000 0.099526 0.100769 -0.337530 -1.602723 11.206551 -7.586281 0.976884 -0.010412 0.002515 0.000000 12.910863 -0.000000 0.271840 -0.007708 -0.173724 -0.031254 11.193578 0.010376 -0.060176 -0.000222 -0.004062 0.054595 0.018457 -0.015135 0.176501 -0.062345 -0.335440 0.000000 3.968525 0.000000 0.039822 0.055138 0.355315 0.000000 5.348644 -0.000000 0.332937 0.042057 0.016919 0.000000 2.438549 0.000000 0.530963 0.101781 -0.096244 -0.655277 1.265097 -0.625148 -0.259319 -0.889697 -0.309134 1.060471 11.303855 7.888028 -0.954859 -0.214992 -0.077518 0.000000 13.379274 -0.000000 -0.203226 0.162768 -0.564091 0.028222 10.991364 0.020885 -0.006975 -0.122009 -0.227019 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.010239 3.746767 5.618636 0.026407 -0.021146 0.996334 0.000000 13.074334 0.000000 -0.030019 -0.083637 -0.009456 -0.035381 14.148866 0.000001 -0.000466 0.010317 0.506759 0.000000 4.097309 -0.000000 0.000114 -0.000404 0.245407 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.904387 0.069252 0.012826 0.995565 -0.000000 13.074365 0.000000 0.015063 -0.039423 -0.045429 -0.035380 14.148879 0.012529 0.000063 -0.008076 0.503745 -0.000000 4.097290 0.000000 0.000063 0.000119 0.258758 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 29 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.054796 5.850361 9.867043 0.086515 -0.152278 -0.831575 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000036 -0.003848 -0.101654 0.000000 7.977538 -0.000000 0.003043 0.043983 0.038347 -0.000001 13.487594 -0.000000 0.061885 0.126927 -0.357365 0.000000 4.943967 0.000000 0.100901 0.102162 -0.336547 -1.601632 11.212210 -7.586510 0.976998 -0.009620 0.003240 0.000000 12.910863 -0.000000 0.274531 -0.007197 -0.176509 -0.031254 11.193578 0.010376 -0.062411 -0.000360 -0.004800 0.054595 0.018457 -0.015135 0.179012 -0.062374 -0.333856 0.000000 3.968525 0.000000 0.039421 0.055716 0.356139 0.000000 5.348644 -0.000000 0.336989 0.042538 0.020105 0.000000 2.438549 0.000000 0.528622 0.103144 -0.097797 -0.655277 1.265097 -0.625148 -0.263665 -0.886327 -0.314898 1.063395 11.319024 7.887414 -0.954752 -0.215747 -0.076478 0.000000 13.379274 -0.000000 -0.204003 0.160835 -0.565490 0.028222 10.991364 0.020885 -0.005704 -0.123538 -0.223960 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.005369 3.747229 5.625542 0.026413 -0.021303 0.996319 0.000000 13.074334 0.000000 -0.030192 -0.083617 -0.009042 -0.035381 14.148866 0.000001 -0.000475 0.010313 0.506837 0.000000 4.097309 -0.000000 0.000120 -0.000386 0.245227 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.898512 0.070098 0.012688 0.995515 -0.000000 13.074365 0.000000 0.014854 -0.040251 -0.045646 -0.035380 14.148879 0.012529 0.000062 -0.008077 0.503731 -0.000000 4.097290 0.000000 0.000067 0.000125 0.258902 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 30 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.054262 5.842696 9.872425 0.087656 -0.152591 -0.831194 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000194 -0.004757 -0.100841 0.000000 7.977538 -0.000000 0.003127 0.043973 0.038558 -0.000001 13.487594 -0.000000 0.065240 0.134450 -0.356878 0.000000 4.943967 0.000000 0.102123 0.103398 -0.335673 -1.600348 11.218874 -7.586779 0.977087 -0.009113 0.004272 0.000000 12.910863 -0.000000 0.277214 -0.006492 -0.180078 -0.031254 11.193578 0.010376 -0.065091 -0.000527 -0.005686 0.054595 0.018457 -0.015135 0.182023 -0.062408 -0.331951 0.000000 3.968525 0.000000 0.038940 0.056407 0.357125 0.000000 5.348644 -0.000000 0.341853 0.043114 0.023939 0.000000 2.438549 0.000000 0.525806 0.104779 -0.099661 -0.655277 1.265097 -0.625148 -0.268173 -0.883036 -0.320124 1.066298 11.334088 7.886805 -0.954624 -0.216604 -0.075452 0.000000 13.379274 -0.000000 -0.204653 0.158838 -0.566969 0.028222 10.991364 0.020885 -0.004294 -0.124799 -0.220921 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.001445 3.747495 5.630986 0.026417 -0.021428 0.996309 0.000000 13.074334 0.000000 -0.030328 -0.083601 -0.008764 -0.035381 14.148866 0.000001 -0.000484 0.010309 0.506914 0.000000 4.097309 -0.000000 0.000124 -0.000371 0.245081 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.893863 0.070767 0.012578 0.995477 -0.000000 13.074365 0.000000 0.014687 -0.040906 -0.045861 -0.035380 14.148879 0.012529 0.000062 -0.008077 0.503718 -0.000000 4.097290 0.000000 0.000070 0.000132 0.259047 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 31 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.055422 5.836614 9.876265 0.088665 -0.152886 -0.830874 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000316 -0.005468 -0.100156 0.000000 7.977538 -0.000000 0.003193 0.043964 0.038757 -0.000001 13.487594 -0.000000 0.067933 0.140492 -0.356480 0.000000 4.943967 0.000000 0.103195 0.104484 -0.334904 -1.598832 11.226737 -7.587097 0.977146 -0.008935 0.005664 0.000000 12.910863 -0.000000 0.279838 -0.005549 -0.184594 -0.031254 11.193578 0.010376 -0.068273 -0.000724 -0.006737 0.054595 0.018457 -0.015135 0.185596 -0.062447 -0.329683 0.000000 3.968525 0.000000 0.038368 0.057228 0.358295 0.000000 5.348644 -0.000000 0.347632 0.043799 0.028508 0.000000 2.438549 0.000000 0.522448 0.106720 -0.101873 -0.655277 1.265097 -0.625148 -0.272858 -0.879892 -0.324644 1.069177 11.349028 7.886201 -0.954477 -0.217550 -0.074455 0.000000 13.379274 -0.000000 -0.205151 0.156789 -0.568503 0.028222 10.991364 0.020885 -0.002742 -0.125726 -0.217938 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -2.998536 3.747550 5.634861 0.026419 -0.021518 0.996304 0.000000 13.074334 0.000000 -0.030422 -0.083588 -0.008632 -0.035381 14.148866 0.000001 -0.000493 0.010304 0.506991 0.000000 4.097309 -0.000000 0.000128 -0.000361 0.244972 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.890531 0.071248 0.012498 0.995451 -0.000000 13.074365 0.000000 0.014564 -0.041377 -0.046075 -0.035380 14.148879 0.012529 0.000061 -0.008077 0.503704 -0.000000 4.097290 0.000000 0.000073 0.000138 0.259192 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 32 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.058535 5.832345 9.878455 0.089529 -0.153163 -0.830620 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000398 -0.005964 -0.099609 0.000000 7.977538 -0.000000 0.003241 0.043957 0.038946 -0.000001 13.487594 -0.000000 0.070094 0.145342 -0.356161 0.000000 4.943967 0.000000 0.104125 0.105426 -0.334237 -1.597046 11.236007 -7.587472 0.977168 -0.009135 0.007477 0.000000 12.910863 -0.000000 0.282351 -0.004322 -0.190233 -0.031254 11.193578 0.010376 -0.072022 -0.000956 -0.007976 0.054595 0.018457 -0.015135 0.189806 -0.062492 -0.327000 0.000000 3.968525 0.000000 0.037695 0.058195 0.359672 0.000000 5.348644 -0.000000 0.354447 0.044607 0.033915 0.000000 2.438549 0.000000 0.518471 0.109006 -0.104480 -0.655277 1.265097 -0.625148 -0.277739 -0.876966 -0.328277 1.072029 11.363825 7.885602 -0.954313 -0.218570 -0.073503 0.000000 13.379274 -0.000000 -0.205470 0.154702 -0.570066 0.028222 10.991364 0.020885 -0.001041 -0.126247 -0.215049 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -2.996712 3.747380 5.637057 0.026420 -0.021572 0.996306 0.000000 13.074334 0.000000 -0.030473 -0.083580 -0.008656 -0.035381 14.148866 0.000001 -0.000502 0.010300 0.507067 0.000000 4.097309 -0.000000 0.000130 -0.000353 0.244902 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.888606 0.071526 0.012451 0.995439 -0.000000 13.074365 0.000000 0.014488 -0.041649 -0.046289 -0.035380 14.148879 0.012529 0.000060 -0.008077 0.503691 -0.000000 4.097290 0.000000 0.000077 0.000144 0.259337 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 33 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.063882 5.830137 9.878872 0.090232 -0.153423 -0.830442 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000437 -0.006231 -0.099212 0.000000 7.977538 -0.000000 0.003269 0.043952 0.039122 -0.000001 13.487594 -0.000000 0.071814 0.149206 -0.355911 0.000000 4.943967 0.000000 0.104918 0.106228 -0.333668 -1.594945 11.246910 -7.587913 0.977145 -0.009766 0.009775 0.000000 12.910863 -0.000000 0.284693 -0.002759 -0.197184 -0.031254 11.193578 0.010376 -0.076417 -0.001229 -0.009429 0.054595 0.018457 -0.015135 0.194739 -0.062543 -0.323842 0.000000 3.968525 0.000000 0.036907 0.059327 0.361283 0.000000 5.348644 -0.000000 0.362441 0.045553 0.040283 0.000000 2.438549 0.000000 0.513783 0.111686 -0.107536 -0.655277 1.265097 -0.625148 -0.282836 -0.874334 -0.330832 1.074850 11.378461 7.885010 -0.954134 -0.219653 -0.072611 0.000000 13.379274 -0.000000 -0.205583 0.152588 -0.571632 0.028222 10.991364 0.020885 0.000815 -0.126287 -0.212293 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -2.996048 3.746968 5.637463 0.026419 -0.021586 0.996314 0.000000 13.074334 0.000000 -0.030478 -0.083577 -0.008848 -0.035381 14.148866 0.000001 -0.000511 0.010296 0.507143 0.000000 4.097309 -0.000000 0.000131 -0.000351 0.244875 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.888185 0.071589 0.012439 0.995441 -0.000000 13.074365 0.000000 0.014462 -0.041710 -0.046501 -0.035380 14.148879 0.012529 0.000059 -0.008078 0.503678 -0.000000 4.097290 0.000000 0.000080 0.000151 0.259480 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 34 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.071774 5.830271 9.877369 0.090759 -0.153664 -0.830348 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000431 -0.006252 -0.098975 0.000000 7.977538 -0.000000 0.003276 0.043949 0.039286 -0.000001 13.487594 -0.000000 0.073163 0.152239 -0.355723 0.000000 4.943967 0.000000 0.105577 0.106895 -0.333193 -1.592481 11.259694 -7.588430 0.977066 -0.010888 0.012633 0.000000 12.910863 -0.000000 0.286799 -0.000804 -0.205655 -0.031254 11.193578 0.010376 -0.081549 -0.001548 -0.011126 0.054595 0.018457 -0.015135 0.200499 -0.062599 -0.320136 0.000000 3.968525 0.000000 0.035986 0.060648 0.363161 0.000000 5.348644 -0.000000 0.371782 0.046659 0.047763 0.000000 2.438549 0.000000 0.508270 0.114814 -0.111105 -0.655277 1.265097 -0.625148 -0.288175 -0.872079 -0.332099 1.077636 11.392917 7.884425 -0.953944 -0.220785 -0.071797 0.000000 13.379274 -0.000000 -0.205458 0.150461 -0.573177 0.028222 10.991364 0.020885 0.002835 -0.125761 -0.209711 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -2.996618 3.746301 5.635960 0.026416 -0.021559 0.996329 0.000000 13.074334 0.000000 -0.030433 -0.083579 -0.009217 -0.035381 14.148866 0.000001 -0.000520 0.010292 0.507218 0.000000 4.097309 -0.000000 0.000130 -0.000352 0.244892 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.889363 0.071422 0.012464 0.995460 -0.000000 13.074365 0.000000 0.014489 -0.041547 -0.046712 -0.035380 14.148879 0.012529 0.000058 -0.008078 0.503665 -0.000000 4.097290 0.000000 0.000083 0.000157 0.259623 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 35 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.082563 5.833060 9.873776 0.091091 -0.153888 -0.830349 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000376 -0.006009 -0.098909 0.000000 7.977538 -0.000000 0.003261 0.043949 0.039436 -0.000001 13.487594 -0.000000 0.074194 0.154561 -0.355589 0.000000 4.943967 0.000000 0.106109 0.107434 -0.332810 -1.589600 11.274638 -7.589035 0.976918 -0.012571 0.016133 0.000000 12.910863 -0.000000 0.288593 0.001607 -0.215880 -0.031254 11.193578 0.010376 -0.087534 -0.001919 -0.013105 0.054595 0.018457 -0.015135 0.207212 -0.062661 -0.315789 0.000000 3.968525 0.000000 0.034913 0.062187 0.365348 0.000000 5.348644 -0.000000 0.382676 0.047949 0.056537 0.000000 2.438549 0.000000 0.501791 0.118461 -0.115266 -0.655277 1.265097 -0.625148 -0.293784 -0.870287 -0.331839 1.080383 11.407173 7.883849 -0.953743 -0.221954 -0.071080 0.000000 13.379274 -0.000000 -0.205060 0.148334 -0.574671 0.028222 10.991364 0.020885 0.005031 -0.124575 -0.207348 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -2.998500 3.745360 5.632430 0.026411 -0.021487 0.996352 0.000000 13.074334 0.000000 -0.030336 -0.083586 -0.009775 -0.035381 14.148866 0.000001 -0.000528 0.010288 0.507292 0.000000 4.097309 -0.000000 0.000128 -0.000359 0.244959 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.892243 0.071011 0.012528 0.995495 -0.000000 13.074365 0.000000 0.014573 -0.041144 -0.046922 -0.035380 14.148879 0.012529 0.000057 -0.008078 0.503652 -0.000000 4.097290 0.000000 0.000087 0.000163 0.259765 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 36 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.096652 5.838863 9.867895 0.091205 -0.154093 -0.830457 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000268 -0.005486 -0.099027 0.000000 7.977538 -0.000000 0.003222 0.043951 0.039572 -0.000001 13.487594 -0.000000 0.074950 0.156267 -0.355503 0.000000 4.943967 0.000000 0.106517 0.107847 -0.332516 -1.586243 11.292061 -7.589739 0.976680 -0.014895 0.020373 0.000000 12.910863 -0.000000 0.289987 0.004546 -0.228120 -0.031254 11.193578 0.010376 -0.094511 -0.002353 -0.015414 0.054595 0.018457 -0.015135 0.215035 -0.062729 -0.310687 0.000000 3.968525 0.000000 0.033662 0.063980 0.367891 0.000000 5.348644 -0.000000 0.395370 0.049450 0.066833 0.000000 2.438549 0.000000 0.494170 0.122710 -0.120117 -0.655277 1.265097 -0.625148 -0.299695 -0.869048 -0.329779 1.083088 11.421209 7.883281 -0.953535 -0.223145 -0.070481 0.000000 13.379274 -0.000000 -0.204348 0.146224 -0.576084 0.028222 10.991364 0.020885 0.007416 -0.122623 -0.205255 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.001775 3.744131 5.626747 0.026403 -0.021368 0.996382 0.000000 13.074334 0.000000 -0.030183 -0.083599 -0.010535 -0.035381 14.148866 0.000001 -0.000537 0.010284 0.507365 0.000000 4.097309 -0.000000 0.000124 -0.000371 0.245076 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.896928 0.070341 0.012633 0.995547 -0.000000 13.074365 0.000000 0.014718 -0.040488 -0.047130 -0.035380 14.148879 0.012529 0.000057 -0.008078 0.503640 -0.000000 4.097290 0.000000 0.000090 0.000170 0.259905 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 37 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.114507 5.848101 9.859486 0.091075 -0.154279 -0.830684 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000106 -0.004664 -0.099340 0.000000 7.977538 -0.000000 0.003158 0.043956 0.039693 -0.000001 13.487594 -0.000000 0.075465 0.157433 -0.355462 0.000000 4.943967 0.000000 0.106807 0.108141 -0.332307 -1.582336 11.312332 -7.590559 0.976326 -0.017955 0.025467 0.000000 12.910863 -0.000000 0.290871 0.008098 -0.242674 -0.031254 11.193578 0.010376 -0.102661 -0.002859 -0.018112 0.054595 0.018457 -0.015135 0.224164 -0.062801 -0.304684 0.000000 3.968525 0.000000 0.032201 0.066073 0.370855 0.000000 5.348644 -0.000000 0.410172 0.051200 0.078939 0.000000 2.438549 0.000000 0.485177 0.127667 -0.125780 -0.655277 1.265097 -0.625148 -0.305944 -0.868459 -0.325594 1.085747 11.435004 7.882723 -0.953323 -0.224344 -0.070025 0.000000 13.379274 -0.000000 -0.203278 0.144146 -0.577383 0.028222 10.991364 0.020885 0.010006 -0.119781 -0.203489 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.006526 3.742594 5.618783 0.026393 -0.021199 0.996421 0.000000 13.074334 0.000000 -0.029971 -0.083617 -0.011506 -0.035381 14.148866 0.000001 -0.000545 0.010280 0.507438 0.000000 4.097309 -0.000000 0.000119 -0.000388 0.245249 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.903527 0.069396 0.012783 0.995618 -0.000000 13.074365 0.000000 0.014927 -0.039562 -0.047337 -0.035380 14.148879 0.012529 0.000056 -0.008078 0.503627 -0.000000 4.097290 0.000000 0.000093 0.000176 0.260043 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 38 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.136681 5.861269 9.848263 0.090670 -0.154446 -0.831047 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000116 -0.003524 -0.099861 0.000000 7.977538 -0.000000 0.003067 0.043963 0.039797 -0.000001 13.487594 -0.000000 0.075767 0.158122 -0.355459 0.000000 4.943967 0.000000 0.106983 0.108318 -0.332180 -1.577797 11.335884 -7.591511 0.975819 -0.021865 0.031553 0.000000 12.910863 -0.000000 0.291112 0.012359 -0.259885 -0.031254 11.193578 0.010376 -0.112216 -0.003453 -0.021276 0.054595 0.018457 -0.015135 0.234855 -0.062877 -0.297584 0.000000 3.968525 0.000000 0.030488 0.068524 0.374320 0.000000 5.348644 -0.000000 0.427468 0.053244 0.093227 0.000000 2.438549 0.000000 0.474511 0.133472 -0.132414 -0.655277 1.265097 -0.625148 -0.312569 -0.868618 -0.318892 1.088355 11.448537 7.882176 -0.953109 -0.225534 -0.069740 0.000000 13.379274 -0.000000 -0.201793 0.142118 -0.578529 0.028222 10.991364 0.020885 0.012820 -0.115902 -0.202116 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.012840 3.740733 5.608402 0.026381 -0.020978 0.996468 0.000000 13.074334 0.000000 -0.029696 -0.083642 -0.012704 -0.035381 14.148866 0.000001 -0.000554 0.010276 0.507510 0.000000 4.097309 -0.000000 0.000111 -0.000411 0.245479 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.912153 0.068160 0.012980 0.995706 -0.000000 13.074365 0.000000 0.015203 -0.038351 -0.047543 -0.035380 14.148879 0.012529 0.000055 -0.008079 0.503615 -0.000000 4.097290 0.000000 0.000096 0.000182 0.260179 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 39 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.167990 5.878447 9.833464 0.089966 -0.154567 -0.831518 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000401 -0.002046 -0.100604 0.000000 7.977538 -0.000000 0.002949 0.043974 0.039884 -0.000001 13.487594 -0.000000 0.075878 0.158384 -0.355492 0.000000 4.943967 0.000000 0.107048 0.108384 -0.332133 -1.572204 11.364903 -7.592685 0.975393 -0.026517 0.039808 0.000000 12.910863 -0.000000 0.290119 0.018210 -0.283104 -0.031254 11.193578 0.010376 -0.125587 -0.004286 -0.025707 0.054595 0.018457 -0.015135 0.249790 -0.062965 -0.287539 0.000000 3.968525 0.000000 0.028089 0.071950 0.379155 0.000000 5.348644 -0.000000 0.451515 0.056082 0.113357 0.000000 2.438549 0.000000 0.459366 0.141576 -0.141683 -0.655277 1.265097 -0.625148 -0.321589 -0.869201 -0.308366 1.090909 11.461786 7.881640 -0.952919 -0.226669 -0.069689 0.000000 13.379274 -0.000000 -0.199425 0.140006 -0.579550 0.028222 10.991364 0.020885 0.015760 -0.110386 -0.201230 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.020808 3.738528 5.595465 0.026366 -0.020700 0.996524 0.000000 13.074334 0.000000 -0.029355 -0.083673 -0.014140 -0.035381 14.148866 0.000001 -0.000562 0.010272 0.507581 0.000000 4.097309 -0.000000 0.000102 -0.000441 0.245772 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.922923 0.066615 0.013226 0.995813 -0.000000 13.074365 0.000000 0.015551 -0.036838 -0.047747 -0.035380 14.148879 0.012529 0.000054 -0.008079 0.503603 -0.000000 4.097290 0.000000 0.000100 0.000188 0.260312 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 40 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.209943 5.899361 9.814881 0.088969 -0.154631 -0.832080 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000752 -0.000209 -0.101582 0.000000 7.977538 -0.000000 0.002801 0.043988 0.039954 -0.000001 13.487594 -0.000000 0.075819 0.158265 -0.355556 0.000000 4.943967 0.000000 0.107006 0.108342 -0.332163 -1.565452 11.399938 -7.594102 0.974838 -0.032454 0.050509 0.000000 12.910863 -0.000000 0.287705 0.025776 -0.312722 -0.031254 11.193578 0.010376 -0.143369 -0.005393 -0.031604 0.054595 0.018457 -0.015135 0.269599 -0.063051 -0.273987 0.000000 3.968525 0.000000 0.024896 0.076505 0.385561 0.000000 5.348644 -0.000000 0.483078 0.059802 0.140281 0.000000 2.438549 0.000000 0.438838 0.152313 -0.153976 -0.655277 1.265097 -0.625148 -0.332700 -0.870429 -0.293106 1.093402 11.474725 7.881117 -0.952757 -0.227761 -0.069871 0.000000 13.379274 -0.000000 -0.196027 0.137730 -0.580503 0.028222 10.991364 0.020885 0.018786 -0.103102 -0.200799 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.030525 3.735960 5.579825 0.026348 -0.020363 0.996589 0.000000 13.074334 0.000000 -0.028943 -0.083711 -0.015828 -0.035381 14.148866 0.000001 -0.000570 0.010268 0.507651 0.000000 4.097309 -0.000000 0.000090 -0.000477 0.246130 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.935961 0.064744 0.013524 0.995938 -0.000000 13.074365 0.000000 0.015975 -0.035005 -0.047949 -0.035380 14.148879 0.012529 0.000054 -0.008079 0.503592 -0.000000 4.097290 0.000000 0.000103 0.000193 0.260443 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 41 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.258863 5.924092 9.792896 0.087681 -0.154655 -0.832759 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.001173 0.002010 -0.102809 0.000000 7.977538 -0.000000 0.002621 0.044005 0.040004 -0.000001 13.487594 -0.000000 0.075606 0.157800 -0.355648 0.000000 4.943967 0.000000 0.106862 0.108196 -0.332267 -1.557858 11.439340 -7.595695 0.973522 -0.040711 0.062793 0.000000 12.910863 -0.000000 0.284224 0.034273 -0.345624 -0.031254 11.193578 0.010376 -0.163741 -0.006664 -0.038368 0.054595 0.018457 -0.015135 0.292197 -0.063104 -0.258199 0.000000 3.968525 0.000000 0.021229 0.081723 0.392873 0.000000 5.348644 -0.000000 0.518405 0.063958 0.171157 0.000000 2.438549 0.000000 0.414801 0.164538 -0.167992 -0.655277 1.265097 -0.625148 -0.343097 -0.873136 -0.272919 1.095832 11.487331 7.880607 -0.952597 -0.228873 -0.070231 0.000000 13.379274 -0.000000 -0.191960 0.135376 -0.581390 0.028222 10.991364 0.020885 0.022002 -0.094505 -0.200725 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.042090 3.733008 5.561328 0.026326 -0.019963 0.996663 0.000000 13.074334 0.000000 -0.028457 -0.083756 -0.017784 -0.035381 14.148866 0.000001 -0.000578 0.010264 0.507720 0.000000 4.097309 -0.000000 0.000077 -0.000520 0.246557 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.951395 0.062527 0.013878 0.996080 -0.000000 13.074365 0.000000 0.016480 -0.032834 -0.048149 -0.035380 14.148879 0.012529 0.000053 -0.008079 0.503581 -0.000000 4.097290 0.000000 0.000106 0.000199 0.260570 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 42 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.311669 5.952790 9.767776 0.086097 -0.154651 -0.833578 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.001670 0.004632 -0.104302 0.000000 7.977538 -0.000000 0.002408 0.044026 0.040034 -0.000001 13.487594 -0.000000 0.075253 0.157021 -0.355765 0.000000 4.943967 0.000000 0.106619 0.107950 -0.332442 -1.549684 11.481751 -7.597410 0.970775 -0.052412 0.076016 0.000000 12.910863 -0.000000 0.280013 0.043073 -0.379359 -0.031254 11.193578 0.010376 -0.185294 -0.008010 -0.045531 0.054595 0.018457 -0.015135 0.315971 -0.063108 -0.241201 0.000000 3.968525 0.000000 0.017336 0.087251 0.400586 0.000000 5.348644 -0.000000 0.554485 0.068192 0.203607 0.000000 2.438549 0.000000 0.388795 0.177364 -0.182719 -0.655277 1.265097 -0.625148 -0.350219 -0.878190 -0.247200 1.098192 11.499576 7.880112 -0.952416 -0.230062 -0.070722 0.000000 13.379274 -0.000000 -0.187527 0.133019 -0.582210 0.028222 10.991364 0.020885 0.025505 -0.084964 -0.200928 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.055609 3.729651 5.539813 0.026302 -0.019497 0.996746 0.000000 13.074334 0.000000 -0.027892 -0.083808 -0.020022 -0.035381 14.148866 0.000001 -0.000586 0.010260 0.507788 0.000000 4.097309 -0.000000 0.000060 -0.000570 0.247058 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.969361 0.059944 0.014291 0.996237 -0.000000 13.074365 0.000000 0.017069 -0.030304 -0.048347 -0.035380 14.148879 0.012529 0.000052 -0.008079 0.503570 -0.000000 4.097290 0.000000 0.000108 0.000204 0.260694 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 43 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.365640 5.985668 9.739703 0.084208 -0.154632 -0.834565 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.002246 0.007684 -0.106075 0.000000 7.977538 -0.000000 0.002160 0.044050 0.040043 -0.000001 13.487594 -0.000000 0.074773 0.155957 -0.355905 0.000000 4.943967 0.000000 0.106280 0.107607 -0.332687 -1.541157 11.526000 -7.599200 0.965641 -0.069000 0.089585 0.000000 12.910863 -0.000000 0.275482 0.051585 -0.411689 -0.031254 11.193578 0.010376 -0.206723 -0.009350 -0.052662 0.054595 0.018457 -0.015135 0.339449 -0.063058 -0.224011 0.000000 3.968525 0.000000 0.013446 0.092761 0.408241 0.000000 5.348644 -0.000000 0.588706 0.072198 0.235375 0.000000 2.438549 0.000000 0.362386 0.189983 -0.197232 -0.655277 1.265097 -0.625148 -0.351130 -0.886609 -0.214787 1.100476 11.511431 7.879632 -0.952195 -0.231378 -0.071306 0.000000 13.379274 -0.000000 -0.182997 0.130729 -0.582965 0.028222 10.991364 0.020885 0.029387 -0.074801 -0.201344 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.071190 3.725865 5.515109 0.026274 -0.018962 0.996839 0.000000 13.074334 0.000000 -0.027244 -0.083868 -0.022559 -0.035381 14.148866 0.000001 -0.000594 0.010256 0.507855 0.000000 4.097309 -0.000000 0.000042 -0.000628 0.247637 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.990002 0.056974 0.014766 0.996409 -0.000000 13.074365 0.000000 0.017748 -0.027396 -0.048543 -0.035380 14.148879 0.012529 0.000052 -0.008079 0.503559 -0.000000 4.097290 0.000000 0.000111 0.000210 0.260814 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 44 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.418241 6.023010 9.708788 0.081998 -0.154608 -0.835743 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.002906 0.011190 -0.108146 0.000000 7.977538 -0.000000 0.001875 0.044079 0.040029 -0.000001 13.487594 -0.000000 0.074175 0.154631 -0.356065 0.000000 4.943967 0.000000 0.105850 0.107171 -0.332997 -1.532480 11.571020 -7.601020 0.956419 -0.092567 0.102781 0.000000 12.910863 -0.000000 0.271201 0.059157 -0.440225 -0.031254 11.193578 0.010376 -0.226573 -0.010593 -0.059275 0.054595 0.018457 -0.015135 0.361032 -0.062962 -0.207843 0.000000 3.968525 0.000000 0.009821 0.097884 0.415328 0.000000 5.348644 -0.000000 0.618667 0.075696 0.264122 0.000000 2.438549 0.000000 0.337464 0.201534 -0.210538 -0.655277 1.265097 -0.625148 -0.341676 -0.899617 -0.173613 1.102680 11.522865 7.879170 -0.951913 -0.232874 -0.071949 0.000000 13.379274 -0.000000 -0.178622 0.128572 -0.583661 0.028222 10.991364 0.020885 0.033742 -0.064308 -0.201918 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.088951 3.721626 5.487036 0.026243 -0.018352 0.996942 0.000000 13.074334 0.000000 -0.026508 -0.083935 -0.025411 -0.035381 14.148866 0.000001 -0.000602 0.010253 0.507921 0.000000 4.097309 -0.000000 0.000020 -0.000695 0.248298 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.013468 0.053595 0.015306 0.996592 -0.000000 13.074365 0.000000 0.018522 -0.024087 -0.048736 -0.035380 14.148879 0.012529 0.000051 -0.008080 0.503549 -0.000000 4.097290 0.000000 0.000114 0.000215 0.260929 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 45 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.466983 6.065179 9.675083 0.079447 -0.154587 -0.837142 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.003656 0.015180 -0.110532 0.000000 7.977538 -0.000000 0.001551 0.044111 0.039992 -0.000001 13.487594 -0.000000 0.073470 0.153064 -0.356243 0.000000 4.943967 0.000000 0.105330 0.106645 -0.333371 -1.523851 11.615792 -7.602831 0.939509 -0.126957 0.115658 0.000000 12.910863 -0.000000 0.267376 0.065698 -0.464721 -0.031254 11.193578 0.010376 -0.244881 -0.011741 -0.065382 0.054595 0.018457 -0.015135 0.380780 -0.062831 -0.192730 0.000000 3.968525 0.000000 0.006455 0.102630 0.421869 0.000000 5.348644 -0.000000 0.644633 0.078718 0.289874 0.000000 2.438549 0.000000 0.314113 0.212055 -0.222674 -0.655277 1.265097 -0.625148 -0.316564 -0.918298 -0.117462 1.104796 11.533845 7.878725 -0.951548 -0.234603 -0.072618 0.000000 13.379274 -0.000000 -0.174650 0.126617 -0.584303 0.028222 10.991364 0.020885 0.038671 -0.053763 -0.202602 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.109014 3.716908 5.455405 0.026207 -0.017664 0.997054 0.000000 13.074334 0.000000 -0.025679 -0.084011 -0.028597 -0.035381 14.148866 0.000001 -0.000609 0.010249 0.507986 0.000000 4.097309 -0.000000 -0.000004 -0.000770 0.249047 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.039920 0.049781 0.015915 0.996784 -0.000000 13.074365 0.000000 0.019396 -0.020354 -0.048926 -0.035380 14.148879 0.012529 0.000050 -0.008080 0.503539 -0.000000 4.097290 0.000000 0.000117 0.000220 0.261040 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 46 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.509303 6.112637 9.638585 0.076524 -0.154580 -0.838792 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.004501 0.019682 -0.113252 0.000000 7.977538 -0.000000 0.001185 0.044148 0.039929 -0.000001 13.487594 -0.000000 0.072667 0.151276 -0.356437 0.000000 4.943967 0.000000 0.104724 0.106032 -0.333807 -1.515468 11.659292 -7.604590 0.913537 -0.169412 0.128193 0.000000 12.910863 -0.000000 0.263733 0.071641 -0.486837 -0.031254 11.193578 0.010376 -0.262503 -0.012848 -0.071266 0.054595 0.018457 -0.015135 0.399628 -0.062666 -0.178012 0.000000 3.968525 0.000000 0.003192 0.107221 0.428172 0.000000 5.348644 -0.000000 0.667986 0.081427 0.313831 0.000000 2.438549 0.000000 0.291333 0.222045 -0.234215 -0.655277 1.265097 -0.625148 -0.278107 -0.937731 -0.048392 1.106817 11.544333 7.878301 -0.951078 -0.236624 -0.073281 0.000000 13.379274 -0.000000 -0.171339 0.124936 -0.584899 0.028222 10.991364 0.020885 0.044289 -0.043446 -0.203353 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.131509 3.711686 5.420014 0.026168 -0.016894 0.997176 0.000000 13.074334 0.000000 -0.024751 -0.084095 -0.032136 -0.035381 14.148866 0.000001 -0.000617 0.010246 0.508049 0.000000 4.097309 -0.000000 -0.000031 -0.000855 0.249887 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.069526 0.045509 0.016598 0.996981 -0.000000 13.074365 0.000000 0.020377 -0.016172 -0.049113 -0.035380 14.148879 0.012529 0.000050 -0.008080 0.503529 -0.000000 4.097290 0.000000 0.000119 0.000224 0.261145 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 47 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.542426 6.165965 9.599230 0.073194 -0.154597 -0.840731 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.005446 0.024727 -0.116326 0.000000 7.977538 -0.000000 0.000776 0.044190 0.039840 -0.000001 13.487594 -0.000000 0.071771 0.149282 -0.356644 0.000000 4.943967 0.000000 0.104036 0.105335 -0.334301 -1.507538 11.700439 -7.606254 0.879976 -0.214050 0.139448 0.000000 12.910863 -0.000000 0.260324 0.076962 -0.506517 -0.031254 11.193578 0.010376 -0.278945 -0.013881 -0.076761 0.054595 0.018457 -0.015135 0.417061 -0.062478 -0.164132 0.000000 3.968525 0.000000 0.000124 0.111530 0.434066 0.000000 5.348644 -0.000000 0.688244 0.083770 0.335350 0.000000 2.438549 0.000000 0.269830 0.231236 -0.244849 -0.655277 1.265097 -0.625148 -0.231468 -0.952489 0.024997 1.108736 11.554289 7.877899 -0.950474 -0.239002 -0.073906 0.000000 13.379274 -0.000000 -0.168966 0.123610 -0.585458 0.028222 10.991364 0.020885 0.050726 -0.033648 -0.204128 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.156577 3.705929 5.380647 0.026124 -0.016037 0.997307 0.000000 13.074334 0.000000 -0.023718 -0.084187 -0.036048 -0.035381 14.148866 0.000001 -0.000624 0.010242 0.508111 0.000000 4.097309 -0.000000 -0.000061 -0.000949 0.250825 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.102466 0.040750 0.017358 0.997177 -0.000000 13.074365 0.000000 0.021469 -0.011515 -0.049296 -0.035380 14.148879 0.012529 0.000049 -0.008080 0.503520 -0.000000 4.097290 0.000000 0.000121 0.000229 0.261245 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 48 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.563205 6.225907 9.556896 0.069407 -0.154648 -0.843001 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.006498 0.030348 -0.119774 0.000000 7.977538 -0.000000 0.000321 0.044236 0.039723 -0.000001 13.487594 -0.000000 0.070791 0.147098 -0.356864 0.000000 4.943967 0.000000 0.103268 0.104558 -0.334852 -1.500292 11.738032 -7.607774 0.842186 -0.255953 0.148774 0.000000 12.910863 -0.000000 0.257225 0.081607 -0.523597 -0.031254 11.193578 0.010376 -0.293714 -0.014811 -0.081702 0.054595 0.018457 -0.015135 0.432584 -0.062281 -0.151551 0.000000 3.968525 0.000000 -0.002654 0.115424 0.439375 0.000000 5.348644 -0.000000 0.705138 0.085717 0.353926 0.000000 2.438549 0.000000 0.250326 0.239377 -0.254280 -0.655277 1.265097 -0.625148 -0.182250 -0.960296 0.094252 1.110544 11.563669 7.877519 -0.949706 -0.241814 -0.074456 0.000000 13.379274 -0.000000 -0.167849 0.122732 -0.585989 0.028222 10.991364 0.020885 0.058142 -0.024693 -0.204886 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.184365 3.699607 5.337076 0.026076 -0.015087 0.997447 0.000000 13.074334 0.000000 -0.022575 -0.084288 -0.040355 -0.035381 14.148866 0.000001 -0.000631 0.010239 0.508171 0.000000 4.097309 -0.000000 -0.000095 -0.001054 0.251866 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.138933 0.035476 0.018200 0.997367 -0.000000 13.074365 0.000000 0.022680 -0.006354 -0.049475 -0.035380 14.148879 0.012529 0.000049 -0.008080 0.503511 -0.000000 4.097290 0.000000 0.000124 0.000233 0.261338 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 49 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.567913 6.293425 9.511378 0.065100 -0.154745 -0.845654 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.007663 0.036582 -0.123618 0.000000 7.977538 -0.000000 -0.000183 0.044288 0.039576 -0.000001 13.487594 -0.000000 0.069732 0.144738 -0.357094 0.000000 4.943967 0.000000 0.102424 0.103703 -0.335458 -1.494001 11.770674 -7.609095 0.805492 -0.290975 0.155874 0.000000 12.910863 -0.000000 0.254549 0.085478 -0.537761 -0.031254 11.193578 0.010376 -0.306211 -0.015598 -0.085886 0.054595 0.018457 -0.015135 0.445612 -0.062092 -0.140823 0.000000 3.968525 0.000000 -0.005022 0.118739 0.443880 0.000000 5.348644 -0.000000 0.718454 0.087247 0.369053 0.000000 2.438549 0.000000 0.233694 0.246174 -0.262164 -0.655277 1.265097 -0.625148 -0.136560 -0.962053 0.151183 1.112231 11.572424 7.877165 -0.948733 -0.245156 -0.074892 0.000000 13.379274 -0.000000 -0.168362 0.122415 -0.586500 0.028222 10.991364 0.020885 0.066734 -0.016952 -0.205581 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.215031 3.692687 5.289056 0.026022 -0.014039 0.997595 0.000000 13.074334 0.000000 -0.021314 -0.084398 -0.045080 -0.035381 14.148866 0.000001 -0.000638 0.010236 0.508230 0.000000 4.097309 -0.000000 -0.000132 -0.001170 0.253017 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.179133 0.029655 0.019129 0.997542 -0.000000 13.074365 0.000000 0.024017 -0.000660 -0.049649 -0.035380 14.148879 0.012529 0.000048 -0.008080 0.503503 -0.000000 4.097290 0.000000 0.000126 0.000237 0.261425 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 50 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.551920 6.369793 9.462373 0.060188 -0.154903 -0.848756 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008949 0.043464 -0.127882 0.000000 7.977538 -0.000000 -0.000740 0.044345 0.039397 -0.000001 13.487594 -0.000000 0.068600 0.142214 -0.357332 0.000000 4.943967 0.000000 0.101505 0.102773 -0.336115 -1.488994 11.796655 -7.610145 0.777072 -0.315298 0.160740 0.000000 12.910863 -0.000000 0.252457 0.088406 -0.548427 -0.031254 11.193578 0.010376 -0.315579 -0.016189 -0.089025 0.054595 0.018457 -0.015135 0.455310 -0.061938 -0.132735 0.000000 3.968525 0.000000 -0.006809 0.121236 0.447266 0.000000 5.348644 -0.000000 0.727842 0.088322 0.380022 0.000000 2.438549 0.000000 0.221155 0.251212 -0.268014 -0.655277 1.265097 -0.625148 -0.100948 -0.961181 0.187515 1.113787 11.580497 7.876839 -0.947503 -0.249147 -0.075162 0.000000 13.379274 -0.000000 -0.170970 0.122799 -0.586991 0.028222 10.991364 0.020885 0.076758 -0.010882 -0.206161 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.248746 3.685133 5.236325 0.025964 -0.012888 0.997751 0.000000 13.074334 0.000000 -0.019929 -0.084516 -0.050249 -0.035381 14.148866 0.000001 -0.000645 0.010232 0.508287 0.000000 4.097309 -0.000000 -0.000173 -0.001297 0.254284 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.223284 0.023255 0.020149 0.997694 -0.000000 13.074365 0.000000 0.025487 0.005601 -0.049817 -0.035380 14.148879 0.012529 0.000048 -0.008081 0.503495 -0.000000 4.097290 0.000000 0.000127 0.000240 0.261505 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 51 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.509196 6.460926 9.406602 0.054276 -0.155148 -0.852555 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.010605 0.052696 -0.133529 0.000000 7.977538 -0.000000 -0.001377 0.044411 0.039173 -0.000001 13.487594 -0.000000 0.067400 0.139536 -0.357578 0.000000 4.943967 0.000000 0.100515 0.101771 -0.336823 -1.484869 11.818057 -7.611011 0.757873 -0.330637 0.164122 0.000000 12.910863 -0.000000 0.250858 0.090584 -0.556334 -0.031254 11.193578 0.010376 -0.322292 -0.016612 -0.091275 0.054595 0.018457 -0.015135 0.462223 -0.061821 -0.126915 0.000000 3.968525 0.000000 -0.008096 0.123033 0.449698 0.000000 5.348644 -0.000000 0.734257 0.089055 0.387683 0.000000 2.438549 0.000000 0.212134 0.254791 -0.272173 -0.655277 1.265097 -0.625148 -0.080834 -0.962638 0.192379 1.115260 11.588137 7.876530 -0.945888 -0.254198 -0.075311 0.000000 13.379274 -0.000000 -0.175969 0.123848 -0.587653 0.028222 10.991364 0.020885 0.089181 -0.005934 -0.206478 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.292712 3.675438 5.167735 0.025888 -0.011389 0.997942 0.000000 13.074334 0.000000 -0.018126 -0.084667 -0.056910 -0.035381 14.148866 0.000001 -0.000651 0.010229 0.508342 0.000000 4.097309 -0.000000 -0.000223 -0.001452 0.255823 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.280737 0.014916 0.021478 0.997827 -0.000000 13.074365 0.000000 0.027403 0.013755 -0.049974 -0.035380 14.148879 0.012529 0.000047 -0.008081 0.503487 -0.000000 4.097290 0.000000 0.000129 0.000244 0.261579 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 52 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.442645 6.567477 9.343272 0.047313 -0.155466 -0.857018 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.012765 0.065110 -0.141039 0.000000 7.977538 -0.000000 -0.002117 0.044487 0.038892 -0.000001 13.487594 -0.000000 0.066136 0.136717 -0.357830 0.000000 4.943967 0.000000 0.099457 0.100699 -0.337579 -1.480957 11.838356 -7.611832 0.741976 -0.342706 0.166922 0.000000 12.910863 -0.000000 0.249449 0.092468 -0.563154 -0.031254 11.193578 0.010376 -0.327958 -0.016970 -0.093175 0.054595 0.018457 -0.015135 0.468034 -0.061718 -0.121987 0.000000 3.968525 0.000000 -0.009186 0.124555 0.451755 0.000000 5.348644 -0.000000 0.739471 0.089650 0.394017 0.000000 2.438549 0.000000 0.204498 0.257792 -0.275661 -0.655278 1.265097 -0.625148 -0.070526 -0.966476 0.177076 1.116703 11.595628 7.876227 -0.943880 -0.260316 -0.075408 0.000000 13.379274 -0.000000 -0.182960 0.125401 -0.588528 0.028222 10.991364 0.020885 0.104065 -0.001442 -0.206524 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.350648 3.662822 5.077531 0.025788 -0.009415 0.998175 0.000000 13.074334 0.000000 -0.015755 -0.084861 -0.065605 -0.035381 14.148866 0.000001 -0.000657 0.010226 0.508394 0.000000 4.097309 -0.000000 -0.000285 -0.001645 0.257735 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.356316 0.003932 0.023226 0.997892 -0.000000 13.074365 0.000000 0.029928 0.024493 -0.050113 -0.035380 14.148879 0.012529 0.000047 -0.008081 0.503480 -0.000000 4.097290 0.000000 0.000131 0.000247 0.261652 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 53 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.358734 6.684172 9.275178 0.039646 -0.155818 -0.861863 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.015288 0.079645 -0.149831 0.000000 7.977538 -0.000000 -0.002952 0.044574 0.038561 -0.000001 13.487594 -0.000000 0.064813 0.133764 -0.358086 0.000000 4.943967 0.000000 0.098333 0.099561 -0.338380 -1.477250 11.857592 -7.612610 0.728470 -0.352548 0.169328 0.000000 12.910863 -0.000000 0.248179 0.094139 -0.569190 -0.031254 11.193578 0.010376 -0.332881 -0.017281 -0.094827 0.054595 0.018457 -0.015135 0.473064 -0.061625 -0.117695 0.000000 3.968525 0.000000 -0.010137 0.125881 0.453545 0.000000 5.348644 -0.000000 0.743851 0.090149 0.399422 0.000000 2.438549 0.000000 0.197847 0.260383 -0.278676 -0.655278 1.265097 -0.625148 -0.063544 -0.970146 0.158452 1.118117 11.602965 7.875930 -0.941575 -0.267115 -0.075427 0.000000 13.379274 -0.000000 -0.191555 0.127446 -0.589415 0.028222 10.991364 0.020885 0.120522 0.002262 -0.206433 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.418363 3.648169 4.972205 0.025671 -0.007109 0.998424 0.000000 13.074334 0.000000 -0.012984 -0.085080 -0.075720 -0.035381 14.148866 0.000001 -0.000663 0.010224 0.508443 0.000000 4.097309 -0.000000 -0.000357 -0.001868 0.259947 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.444580 -0.008911 0.025266 0.997810 -0.000000 13.074365 0.000000 0.032876 0.037043 -0.050232 -0.035380 14.148879 0.012529 0.000046 -0.008081 0.503473 -0.000000 4.097290 0.000000 0.000133 0.000250 0.261724 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 54 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.262686 6.806654 9.204642 0.031563 -0.156174 -0.866866 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.018058 0.095420 -0.159423 0.000000 7.977538 -0.000000 -0.003872 0.044669 0.038182 -0.000001 13.487594 -0.000000 0.063435 0.130686 -0.358346 0.000000 4.943967 0.000000 0.097145 0.098359 -0.339225 -1.473740 11.875803 -7.613346 0.716808 -0.360760 0.171445 0.000000 12.910863 -0.000000 0.247017 0.095645 -0.574620 -0.031254 11.193578 0.010376 -0.337237 -0.017556 -0.096288 0.054595 0.018457 -0.015135 0.477499 -0.061540 -0.113888 0.000000 3.968525 0.000000 -0.010981 0.127057 0.455130 0.000000 5.348644 -0.000000 0.747610 0.090576 0.404127 0.000000 2.438549 0.000000 0.191952 0.262663 -0.281329 -0.655278 1.265097 -0.625148 -0.054397 -0.971765 0.152036 1.119501 11.610145 7.875639 -0.939054 -0.274275 -0.075346 0.000000 13.379274 -0.000000 -0.201448 0.129983 -0.590139 0.028222 10.991364 0.020885 0.137814 0.004871 -0.206323 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.492440 3.632199 4.857051 0.025542 -0.004587 0.998667 0.000000 13.074334 0.000000 -0.009951 -0.085308 -0.086752 -0.035381 14.148866 0.000001 -0.000668 0.010221 0.508489 0.000000 4.097309 -0.000000 -0.000436 -0.002116 0.262399 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.541088 -0.022966 0.027495 0.997527 -0.000000 13.074365 0.000000 0.036098 0.050772 -0.050331 -0.035380 14.148879 0.012529 0.000046 -0.008081 0.503466 -0.000000 4.097290 0.000000 0.000134 0.000253 0.261793 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 55 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.158970 6.931093 9.133719 0.023318 -0.156511 -0.871842 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.020973 0.111655 -0.169401 0.000000 7.977538 -0.000000 -0.004869 0.044773 0.037760 -0.000001 13.487594 -0.000000 0.062004 0.127493 -0.358609 0.000000 4.943967 0.000000 0.095897 0.097096 -0.340112 -1.470421 11.893026 -7.614042 0.706629 -0.367724 0.173339 0.000000 12.910863 -0.000000 0.245945 0.097018 -0.579556 -0.031254 11.193578 0.010376 -0.341138 -0.017802 -0.097598 0.054595 0.018457 -0.015135 0.481460 -0.061462 -0.110472 0.000000 3.968525 0.000000 -0.011739 0.128113 0.456553 0.000000 5.348644 -0.000000 0.750886 0.090948 0.408279 0.000000 2.438549 0.000000 0.186662 0.264695 -0.283696 -0.655278 1.265097 -0.625148 -0.040719 -0.970393 0.165585 1.120853 11.617162 7.875356 -0.936393 -0.281516 -0.075142 0.000000 13.379274 -0.000000 -0.212390 0.133026 -0.590540 0.028222 10.991364 0.020885 0.155290 0.006077 -0.206314 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.569913 3.615535 4.736658 0.025407 -0.001949 0.998889 0.000000 13.074334 0.000000 -0.006778 -0.085536 -0.098262 -0.035381 14.148866 0.000001 -0.000674 0.010219 0.508532 0.000000 4.097309 -0.000000 -0.000522 -0.002382 0.265035 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.641992 -0.037664 0.029821 0.997013 -0.000000 13.074365 0.000000 0.039461 0.065124 -0.050408 -0.035380 14.148879 0.012529 0.000045 -0.008081 0.503459 -0.000000 4.097290 0.000000 0.000136 0.000256 0.261861 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 56 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.051635 7.053909 9.064347 0.015153 -0.156808 -0.876637 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.023944 0.127629 -0.179390 0.000000 7.977538 -0.000000 -0.005937 0.044884 0.037299 -0.000001 13.487594 -0.000000 0.060525 0.124191 -0.358872 0.000000 4.943967 0.000000 0.094590 0.095773 -0.341038 -1.467286 11.909292 -7.614700 0.697679 -0.373696 0.175055 0.000000 12.910863 -0.000000 0.244949 0.098277 -0.584080 -0.031254 11.193578 0.010376 -0.344663 -0.018025 -0.098781 0.054595 0.018457 -0.015135 0.485029 -0.061389 -0.107381 0.000000 3.968525 0.000000 -0.012426 0.129069 0.457840 0.000000 5.348644 -0.000000 0.753772 0.091275 0.411980 0.000000 2.438549 0.000000 0.181876 0.266523 -0.285825 -0.655278 1.265097 -0.625148 -0.025158 -0.966783 0.189419 1.122174 11.624013 7.875078 -0.933673 -0.288577 -0.074793 0.000000 13.379274 -0.000000 -0.224173 0.136599 -0.590463 0.028222 10.991364 0.020885 0.172346 0.005558 -0.206530 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.648061 3.598751 4.615248 0.025270 0.000711 0.999081 0.000000 13.074334 0.000000 -0.003578 -0.085752 -0.109845 -0.035381 14.148866 0.000001 -0.000678 0.010216 0.508574 0.000000 4.097309 -0.000000 -0.000612 -0.002662 0.267807 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.743753 -0.052481 0.032160 0.996271 -0.000000 13.074365 0.000000 0.042846 0.079585 -0.050465 -0.035380 14.148879 0.012529 0.000045 -0.008081 0.503453 -0.000000 4.097290 0.000000 0.000137 0.000259 0.261927 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 57 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.944574 7.171548 8.998464 0.007305 -0.157049 -0.881113 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.026884 0.142638 -0.189037 0.000000 7.977538 -0.000000 -0.007069 0.045001 0.036803 -0.000001 13.487594 -0.000000 0.059001 0.120788 -0.359136 0.000000 4.943967 0.000000 0.093228 0.094393 -0.342003 -1.464330 11.924633 -7.615321 0.689769 -0.378860 0.176624 0.000000 12.910863 -0.000000 0.244021 0.099440 -0.588246 -0.031254 11.193578 0.010376 -0.347867 -0.018228 -0.099857 0.054595 0.018457 -0.015135 0.488265 -0.061322 -0.104566 0.000000 3.968525 0.000000 -0.013052 0.129940 0.459011 0.000000 5.348644 -0.000000 0.756334 0.091565 0.415303 0.000000 2.438549 0.000000 0.177519 0.268178 -0.287753 -0.655278 1.265097 -0.625148 -0.008473 -0.961237 0.218461 1.123461 11.630693 7.874808 -0.930977 -0.295197 -0.074272 0.000000 13.379274 -0.000000 -0.236609 0.140738 -0.589749 0.028222 10.991364 0.020885 0.188385 0.002954 -0.207107 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.724222 3.582411 4.496943 0.025136 0.003303 0.999237 0.000000 13.074334 0.000000 -0.000459 -0.085952 -0.121107 -0.035381 14.148866 0.000001 -0.000683 0.010214 0.508613 0.000000 4.097309 -0.000000 -0.000705 -0.002952 0.270671 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.842913 -0.066905 0.034432 0.995332 -0.000000 13.074365 0.000000 0.046134 0.093656 -0.050503 -0.035380 14.148879 0.012529 0.000045 -0.008081 0.503446 -0.000000 4.097290 0.000000 0.000139 0.000262 0.261991 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 58 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.841754 7.280267 8.938128 0.000024 -0.157219 -0.885147 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.029708 0.155959 -0.197990 0.000000 7.977538 -0.000000 -0.008256 0.045125 0.036274 -0.000001 13.487594 -0.000000 0.057435 0.117290 -0.359400 0.000000 4.943967 0.000000 0.091812 0.092960 -0.343003 -1.461546 11.939078 -7.615905 0.682758 -0.383350 0.178070 0.000000 12.910863 -0.000000 0.243152 0.100516 -0.592100 -0.031254 11.193578 0.010376 -0.350793 -0.018413 -0.100840 0.054595 0.018457 -0.015135 0.491213 -0.061260 -0.101993 0.000000 3.968525 0.000000 -0.013624 0.130736 0.460082 0.000000 5.348644 -0.000000 0.758624 0.091823 0.418303 0.000000 2.438549 0.000000 0.173537 0.269684 -0.289507 -0.655278 1.265097 -0.625148 0.008696 -0.954273 0.248651 1.124715 11.637198 7.874545 -0.928402 -0.301105 -0.073550 0.000000 13.379274 -0.000000 -0.249527 0.145495 -0.588230 0.028222 10.991364 0.020885 0.202787 -0.002153 -0.208199 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.795640 3.567095 4.386014 0.025009 0.005734 0.999355 0.000000 13.074334 0.000000 0.002464 -0.086128 -0.131645 -0.035381 14.148866 0.000001 -0.000687 0.010212 0.508650 0.000000 4.097309 -0.000000 -0.000799 -0.003246 0.273584 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.935893 -0.080407 0.036554 0.994258 -0.000000 13.074365 0.000000 0.049207 0.106823 -0.050527 -0.035380 14.148879 0.012529 0.000044 -0.008081 0.503440 -0.000000 4.097290 0.000000 0.000140 0.000265 0.262053 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 59 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.747474 7.375889 8.885652 -0.006414 -0.157305 -0.888614 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.032327 0.166807 -0.205879 0.000000 7.977538 -0.000000 -0.009495 0.045253 0.035717 -0.000001 13.487594 -0.000000 0.055830 0.113703 -0.359661 0.000000 4.943967 0.000000 0.090345 0.091475 -0.344037 -1.458929 11.952654 -7.616454 0.676532 -0.387270 0.179410 0.000000 12.910863 -0.000000 0.242338 0.101516 -0.595673 -0.031254 11.193578 0.010376 -0.353473 -0.018582 -0.101740 0.054595 0.018457 -0.015135 0.493907 -0.061202 -0.099633 0.000000 3.968525 0.000000 -0.014150 0.131467 0.461064 0.000000 5.348644 -0.000000 0.760679 0.092055 0.421020 0.000000 2.438549 0.000000 0.169886 0.271057 -0.291109 -0.655278 1.265097 -0.625148 0.025741 -0.946824 0.276296 1.125934 11.643523 7.874289 -0.926057 -0.305998 -0.072592 0.000000 13.379274 -0.000000 -0.262759 0.150942 -0.585715 0.028222 10.991364 0.020885 0.214862 -0.010270 -0.209983 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.859286 3.553446 4.287156 0.024896 0.007900 0.999437 0.000000 13.074334 0.000000 0.005067 -0.086276 -0.141016 -0.035381 14.148866 0.000001 -0.000691 0.010210 0.508684 0.000000 4.097309 -0.000000 -0.000894 -0.003542 0.276505 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.018755 -0.092415 0.038438 0.993145 -0.000000 13.074365 0.000000 0.051935 0.118530 -0.050540 -0.035380 14.148879 0.012529 0.000044 -0.008082 0.503434 -0.000000 4.097290 0.000000 0.000142 0.000267 0.262113 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 60 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.666676 7.453489 8.843780 -0.011687 -0.157292 -0.891377 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.034640 0.174275 -0.212286 0.000000 7.977538 -0.000000 -0.010777 0.045387 0.035135 -0.000001 13.487594 -0.000000 0.054187 0.110035 -0.359921 0.000000 4.943967 0.000000 0.088829 0.089940 -0.345103 -1.456475 11.965386 -7.616969 0.671001 -0.390701 0.180655 0.000000 12.910863 -0.000000 0.241574 0.102446 -0.598993 -0.031254 11.193578 0.010376 -0.355931 -0.018738 -0.102566 0.054595 0.018457 -0.015135 0.496374 -0.061148 -0.097465 0.000000 3.968525 0.000000 -0.014633 0.132139 0.461965 0.000000 5.348644 -0.000000 0.762529 0.092264 0.423490 0.000000 2.438549 0.000000 0.166533 0.272314 -0.292574 -0.655278 1.265097 -0.625148 0.042052 -0.940334 0.297600 1.127118 11.649666 7.874041 -0.924071 -0.309518 -0.071352 0.000000 13.379274 -0.000000 -0.276133 0.157175 -0.581978 0.028222 10.991364 0.020885 0.223786 -0.022043 -0.212671 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.911632 3.542209 4.205840 0.024802 0.009680 0.999489 0.000000 13.074334 0.000000 0.007206 -0.086392 -0.148714 -0.035381 14.148866 0.000001 -0.000695 0.010208 0.508717 0.000000 4.097309 -0.000000 -0.000989 -0.003835 0.279396 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.086912 -0.102272 0.039981 0.992120 -0.000000 13.074365 0.000000 0.054170 0.128136 -0.050550 -0.035380 14.148879 0.012529 0.000043 -0.008082 0.503428 -0.000000 4.097290 0.000000 0.000143 0.000270 0.262171 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 61 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.605399 7.506923 8.815950 -0.015395 -0.157161 -0.893267 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.036528 0.177229 -0.216691 0.000000 7.977538 -0.000000 -0.012097 0.045524 0.034530 -0.000001 13.487594 -0.000000 0.052511 0.106290 -0.360177 0.000000 4.943967 0.000000 0.087266 0.088358 -0.346199 -1.454179 11.977299 -7.617451 0.666093 -0.393704 0.181818 0.000000 12.910863 -0.000000 0.240857 0.103313 -0.602081 -0.031254 11.193578 0.010376 -0.358190 -0.018881 -0.103325 0.054595 0.018457 -0.015135 0.498637 -0.061097 -0.095471 0.000000 3.968525 0.000000 -0.015078 0.132757 0.462795 0.000000 5.348644 -0.000000 0.764200 0.092452 0.425738 0.000000 2.438549 0.000000 0.163450 0.273464 -0.293916 -0.655278 1.265097 -0.625148 0.056946 -0.936836 0.308002 1.128265 11.655620 7.873800 -0.922604 -0.311219 -0.069771 0.000000 13.379274 -0.000000 -0.289461 0.164329 -0.576730 0.028222 10.991364 0.020885 0.228507 -0.038331 -0.216523 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.948317 3.534308 4.148822 0.024736 0.010928 0.999517 0.000000 13.074334 0.000000 0.008705 -0.086469 -0.154113 -0.035381 14.148866 0.000001 -0.000699 0.010207 0.508747 0.000000 4.097309 -0.000000 -0.001081 -0.004121 0.282217 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.134699 -0.109169 0.041060 0.991343 -0.000000 13.074365 0.000000 0.055733 0.134856 -0.050563 -0.035380 14.148879 0.012529 0.000043 -0.008082 0.503422 -0.000000 4.097290 0.000000 0.000144 0.000272 0.262228 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 62 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.569181 7.527489 8.807201 -0.017673 -0.156893 -0.894423 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.038107 0.176561 -0.219558 0.000000 7.977538 -0.000000 -0.013450 0.045664 0.033905 -0.000001 13.487594 -0.000000 0.050803 0.102474 -0.360429 0.000000 4.943967 0.000000 0.085660 0.086732 -0.347324 -1.452037 11.988414 -7.617900 0.661746 -0.396332 0.182906 0.000000 12.910863 -0.000000 0.240184 0.104120 -0.604955 -0.031254 11.193578 0.010376 -0.360265 -0.019012 -0.104022 0.054595 0.018457 -0.015135 0.500712 -0.061050 -0.093638 0.000000 3.968525 0.000000 -0.015487 0.133326 0.463557 0.000000 5.348644 -0.000000 0.765710 0.092622 0.427786 0.000000 2.438549 0.000000 0.160615 0.274519 -0.295146 -0.655278 1.265097 -0.625148 0.069758 -0.939434 0.299672 1.129376 11.661382 7.873567 -0.921792 -0.310416 -0.067691 0.000000 13.379274 -0.000000 -0.303628 0.173458 -0.568864 0.028222 10.991364 0.020885 0.227012 -0.062692 -0.222504 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.973441 3.528864 4.109737 0.024691 0.011784 0.999532 0.000000 13.074334 0.000000 0.009732 -0.086520 -0.157822 -0.035381 14.148866 0.000001 -0.000702 0.010205 0.508776 0.000000 4.097309 -0.000000 -0.001169 -0.004396 0.284926 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.167452 -0.113889 0.041797 0.990783 -0.000000 13.074365 0.000000 0.056800 0.139455 -0.050584 -0.035380 14.148879 0.012529 0.000043 -0.008082 0.503417 -0.000000 4.097290 0.000000 0.000146 0.000275 0.262282 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 63 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.553429 7.518814 8.815646 -0.019068 -0.156505 -0.895166 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.039596 0.174193 -0.221808 0.000000 7.977538 -0.000000 -0.014829 0.045807 0.033264 -0.000001 13.487594 -0.000000 0.049066 0.098593 -0.360677 0.000000 4.943967 0.000000 0.084011 0.085063 -0.348475 -1.450045 11.998754 -7.618318 0.657910 -0.398627 0.183924 0.000000 12.910863 -0.000000 0.239552 0.104872 -0.607629 -0.031254 11.193578 0.010376 -0.362171 -0.019133 -0.104663 0.054595 0.018457 -0.015135 0.502614 -0.061007 -0.091953 0.000000 3.968525 0.000000 -0.015863 0.133849 0.464258 0.000000 5.348644 -0.000000 0.767076 0.092776 0.429652 0.000000 2.438549 0.000000 0.158010 0.275485 -0.296273 -0.655277 1.265097 -0.625148 0.080900 -0.946269 0.277198 1.130449 11.666948 7.873342 -0.921526 -0.307357 -0.065121 0.000000 13.379274 -0.000000 -0.318808 0.184495 -0.558432 0.028222 10.991364 0.020885 0.219730 -0.094899 -0.230350 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.994768 3.524222 4.076533 0.024652 0.012510 0.999542 0.000000 13.074334 0.000000 0.010605 -0.086562 -0.160978 -0.035381 14.148866 0.000001 -0.000705 0.010204 0.508802 0.000000 4.097309 -0.000000 -0.001252 -0.004656 0.287481 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.195273 -0.117894 0.042422 0.990290 -0.000000 13.074365 0.000000 0.057705 0.143356 -0.050608 -0.035380 14.148879 0.012529 0.000042 -0.008082 0.503412 -0.000000 4.097290 0.000000 0.000147 0.000277 0.262334 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 64 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.552258 7.489040 8.836735 -0.019697 -0.156024 -0.895551 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.041000 0.170258 -0.223486 0.000000 7.977538 -0.000000 -0.016231 0.045953 0.032609 -0.000001 13.487594 -0.000000 0.047302 0.094650 -0.360919 0.000000 4.943967 0.000000 0.082323 0.083354 -0.349651 -1.448197 12.008337 -7.618706 0.654540 -0.400622 0.184880 0.000000 12.910863 -0.000000 0.238961 0.105571 -0.610115 -0.031254 11.193578 0.010376 -0.363919 -0.019243 -0.105250 0.054595 0.018457 -0.015135 0.504356 -0.060967 -0.090406 0.000000 3.968525 0.000000 -0.016209 0.134329 0.464901 0.000000 5.348644 -0.000000 0.768311 0.092915 0.431351 0.000000 2.438549 0.000000 0.155620 0.276368 -0.297305 -0.655277 1.265097 -0.625148 0.090665 -0.954065 0.248825 1.131482 11.672313 7.873125 -0.921674 -0.302677 -0.062189 0.000000 13.379274 -0.000000 -0.333973 0.196243 -0.546559 0.028222 10.991364 0.020885 0.208477 -0.131242 -0.238821 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.012522 3.520335 4.048869 0.024620 0.013114 0.999549 0.000000 13.074334 0.000000 0.011331 -0.086596 -0.163615 -0.035381 14.148866 0.000001 -0.000708 0.010202 0.508826 0.000000 4.097309 -0.000000 -0.001329 -0.004895 0.289837 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.218450 -0.121227 0.042942 0.989867 -0.000000 13.074365 0.000000 0.058457 0.146602 -0.050637 -0.035380 14.148879 0.012529 0.000042 -0.008082 0.503407 -0.000000 4.097290 0.000000 0.000148 0.000279 0.262384 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 65 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.561410 7.443999 8.867258 -0.019662 -0.155472 -0.895621 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.042323 0.164879 -0.224630 0.000000 7.977538 -0.000000 -0.017648 0.046100 0.031943 -0.000001 13.487594 -0.000000 0.045513 0.090652 -0.361155 0.000000 4.943967 0.000000 0.080597 0.081607 -0.350850 -1.446493 12.017183 -7.619063 0.651602 -0.402347 0.185776 0.000000 12.910863 -0.000000 0.238408 0.106222 -0.612423 -0.031254 11.193578 0.010376 -0.365519 -0.019344 -0.105788 0.054595 0.018457 -0.015135 0.505949 -0.060929 -0.088990 0.000000 3.968525 0.000000 -0.016526 0.134769 0.465490 0.000000 5.348644 -0.000000 0.769428 0.093040 0.432896 0.000000 2.438549 0.000000 0.153430 0.277175 -0.298247 -0.655277 1.265097 -0.625148 0.099163 -0.961989 0.215894 1.132477 11.677472 7.872916 -0.922157 -0.296832 -0.058984 0.000000 13.379274 -0.000000 -0.348277 0.207733 -0.534223 0.028222 10.991364 0.020885 0.194749 -0.168612 -0.246934 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.026908 3.517160 4.026424 0.024593 0.013605 0.999553 0.000000 13.074334 0.000000 0.011921 -0.086623 -0.165763 -0.035381 14.148866 0.000001 -0.000710 0.010201 0.508848 0.000000 4.097309 -0.000000 -0.001398 -0.005110 0.291943 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.237250 -0.123928 0.043363 0.989516 -0.000000 13.074365 0.000000 0.059066 0.149233 -0.050669 -0.035380 14.148879 0.012529 0.000042 -0.008082 0.503402 -0.000000 4.097290 0.000000 0.000149 0.000281 0.262432 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 66 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.577430 7.388359 8.904709 -0.019052 -0.154867 -0.895409 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.043569 0.158165 -0.225275 0.000000 7.977538 -0.000000 -0.019077 0.046248 0.031268 -0.000001 13.487594 -0.000000 0.043702 0.086603 -0.361385 0.000000 4.943967 0.000000 0.078836 0.079824 -0.352071 -1.444927 12.025309 -7.619392 0.649061 -0.403826 0.186616 0.000000 12.910863 -0.000000 0.237892 0.106825 -0.614563 -0.031254 11.193578 0.010376 -0.366979 -0.019437 -0.106279 0.054595 0.018457 -0.015135 0.507401 -0.060895 -0.087696 0.000000 3.968525 0.000000 -0.016815 0.135171 0.466028 0.000000 5.348644 -0.000000 0.770434 0.093153 0.434297 0.000000 2.438549 0.000000 0.151431 0.277910 -0.299105 -0.655277 1.265096 -0.625148 0.106586 -0.969326 0.179877 1.133430 11.682420 7.872716 -0.922933 -0.290198 -0.055578 0.000000 13.379274 -0.000000 -0.360927 0.218066 -0.522409 0.028222 10.991364 0.020885 0.179993 -0.204102 -0.253852 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.038120 3.514657 4.008899 0.024573 0.013987 0.999556 0.000000 13.074334 0.000000 0.012382 -0.086644 -0.167451 -0.035381 14.148866 0.000001 -0.000713 0.010200 0.508868 0.000000 4.097309 -0.000000 -0.001457 -0.005293 0.293744 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.251925 -0.126035 0.043691 0.989237 -0.000000 13.074365 0.000000 0.059540 0.151285 -0.050705 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503397 -0.000000 4.097290 0.000000 0.000150 0.000283 0.262479 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 67 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.597129 7.326347 8.946893 -0.017945 -0.154227 -0.894944 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.044742 0.150215 -0.225449 0.000000 7.977538 -0.000000 -0.020512 0.046397 0.030587 -0.000001 13.487594 -0.000000 0.041869 0.082508 -0.361608 0.000000 4.943967 0.000000 0.077042 0.078007 -0.353311 -1.443496 12.032731 -7.619692 0.646892 -0.405078 0.187403 0.000000 12.910863 -0.000000 0.237412 0.107384 -0.616541 -0.031254 11.193578 0.010376 -0.368306 -0.019521 -0.106725 0.054595 0.018457 -0.015135 0.508719 -0.060864 -0.086519 0.000000 3.968525 0.000000 -0.017079 0.135536 0.466518 0.000000 5.348644 -0.000000 0.771340 0.093254 0.435564 0.000000 2.438549 0.000000 0.149613 0.278577 -0.299884 -0.655276 1.265096 -0.625148 0.112938 -0.974772 0.147134 1.134343 11.687154 7.872525 -0.924001 -0.283129 -0.052038 0.000000 13.379274 -0.000000 -0.371066 0.226283 -0.512233 0.028222 10.991364 0.020885 0.165769 -0.234713 -0.258795 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.046339 3.512789 3.996013 0.024557 0.014268 0.999557 0.000000 13.074334 0.000000 0.012720 -0.086658 -0.168704 -0.035381 14.148866 0.000001 -0.000715 0.010199 0.508885 0.000000 4.097309 -0.000000 -0.001504 -0.005439 0.295175 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.262710 -0.127582 0.043932 0.989030 -0.000000 13.074365 0.000000 0.059888 0.152792 -0.050744 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503393 -0.000000 4.097290 0.000000 0.000151 0.000285 0.262522 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 68 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.617072 7.262413 8.991596 -0.016414 -0.153568 -0.894249 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.045841 0.141122 -0.225177 0.000000 7.977538 -0.000000 -0.021949 0.046545 0.029903 -0.000001 13.487594 -0.000000 0.040018 0.078370 -0.361824 0.000000 4.943967 0.000000 0.075217 0.076160 -0.354570 -1.442198 12.039467 -7.619965 0.645069 -0.406123 0.188139 0.000000 12.910863 -0.000000 0.236967 0.107899 -0.618364 -0.031254 11.193578 0.010376 -0.369507 -0.019597 -0.107129 0.054595 0.018457 -0.015135 0.509910 -0.060835 -0.085454 0.000000 3.968525 0.000000 -0.017317 0.135868 0.466961 0.000000 5.348644 -0.000000 0.772150 0.093345 0.436704 0.000000 2.438549 0.000000 0.147968 0.279179 -0.300587 -0.655276 1.265095 -0.625147 0.118121 -0.977743 0.124889 1.135213 11.691668 7.872342 -0.925398 -0.276007 -0.048429 0.000000 13.379274 -0.000000 -0.377606 0.231186 -0.505108 0.028222 10.991364 0.020885 0.153940 -0.256939 -0.260900 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.051737 3.511519 3.987503 0.024547 0.014453 0.999558 0.000000 13.074334 0.000000 0.012945 -0.086667 -0.169549 -0.035381 14.148866 0.000001 -0.000717 0.010198 0.508900 0.000000 4.097309 -0.000000 -0.001536 -0.005539 0.296161 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.269827 -0.128602 0.044090 0.988893 -0.000000 13.074365 0.000000 0.060116 0.153785 -0.050787 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503389 -0.000000 4.097290 0.000000 0.000152 0.000287 0.262564 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 69 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.637346 7.195519 9.040102 -0.014523 -0.152905 -0.893343 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.046870 0.130975 -0.224482 0.000000 7.977538 -0.000000 -0.023382 0.046694 0.029218 -0.000001 13.487594 -0.000000 0.038151 0.074194 -0.362031 0.000000 4.943967 0.000000 0.073363 0.074283 -0.355844 -1.441030 12.045529 -7.620210 0.643573 -0.406974 0.188825 0.000000 12.910863 -0.000000 0.236557 0.108372 -0.620036 -0.031254 11.193578 0.010376 -0.370586 -0.019665 -0.107492 0.054595 0.018457 -0.015135 0.510980 -0.060809 -0.084496 0.000000 3.968525 0.000000 -0.017532 0.136165 0.467359 0.000000 5.348644 -0.000000 0.772872 0.093426 0.437724 0.000000 2.438549 0.000000 0.146489 0.279720 -0.301218 -0.655276 1.265095 -0.625147 0.122307 -0.978614 0.116045 1.136040 11.695958 7.872169 -0.927266 -0.268678 -0.044528 0.000000 13.379274 -0.000000 -0.379659 0.231727 -0.501796 0.028222 10.991364 0.020885 0.145139 -0.268476 -0.259897 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.054474 3.510815 3.983118 0.024542 0.014548 0.999559 0.000000 13.074334 0.000000 0.013061 -0.086672 -0.170009 -0.035381 14.148866 0.000001 -0.000718 0.010198 0.508913 0.000000 4.097309 -0.000000 -0.001557 -0.005604 0.296798 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.273485 -0.129126 0.044172 0.988822 -0.000000 13.074365 0.000000 0.060232 0.154296 -0.050831 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503385 -0.000000 4.097290 0.000000 0.000153 0.000289 0.262604 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 70 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.660430 7.122568 9.093852 -0.012332 -0.152252 -0.892245 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.047828 0.119858 -0.223386 0.000000 7.977538 -0.000000 -0.024807 0.046841 0.028534 -0.000001 13.487594 -0.000000 0.036268 0.069986 -0.362230 0.000000 4.943967 0.000000 0.071482 0.072379 -0.357134 -1.439988 12.050933 -7.620428 0.642384 -0.407645 0.189464 0.000000 12.910863 -0.000000 0.236181 0.108804 -0.621564 -0.031254 11.193578 0.010376 -0.371548 -0.019726 -0.107816 0.054595 0.018457 -0.015135 0.511933 -0.060786 -0.083641 0.000000 3.968525 0.000000 -0.017724 0.136431 0.467715 0.000000 5.348644 -0.000000 0.773511 0.093497 0.438629 0.000000 2.438549 0.000000 0.145169 0.280201 -0.301781 -0.655276 1.265095 -0.625147 0.125873 -0.978486 0.114870 1.136822 11.700018 7.872004 -0.929551 -0.260873 -0.040303 0.000000 13.379274 -0.000000 -0.378318 0.229046 -0.501187 0.028222 10.991364 0.020885 0.138012 -0.272521 -0.256733 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.054704 3.510643 3.982624 0.024541 0.014558 0.999559 0.000000 13.074334 0.000000 0.013075 -0.086671 -0.170105 -0.035381 14.148866 0.000001 -0.000720 0.010197 0.508926 0.000000 4.097309 -0.000000 -0.001572 -0.005649 0.297245 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.273882 -0.129183 0.044180 0.988816 -0.000000 13.074365 0.000000 0.060242 0.154351 -0.050878 -0.035380 14.148879 0.012529 0.000040 -0.008082 0.503381 -0.000000 4.097290 0.000000 0.000154 0.000291 0.262641 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 71 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.686085 7.045030 9.151158 -0.009901 -0.151623 -0.890971 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.048717 0.107853 -0.221907 0.000000 7.977538 -0.000000 -0.026218 0.046987 0.027854 -0.000001 13.487594 -0.000000 0.034372 0.065747 -0.362420 0.000000 4.943967 0.000000 0.069577 0.070451 -0.358436 -1.439071 12.055691 -7.620621 0.641485 -0.408146 0.190055 0.000000 12.910863 -0.000000 0.235838 0.109196 -0.622950 -0.031254 11.193578 0.010376 -0.372398 -0.019780 -0.108102 0.054595 0.018457 -0.015135 0.512774 -0.060765 -0.082887 0.000000 3.968525 0.000000 -0.017893 0.136666 0.468028 0.000000 5.348644 -0.000000 0.774070 0.093560 0.439426 0.000000 2.438549 0.000000 0.144004 0.280625 -0.302276 -0.655276 1.265095 -0.625147 0.128867 -0.977684 0.119176 1.137559 11.703843 7.871850 -0.932090 -0.252818 -0.036054 0.000000 13.379274 -0.000000 -0.374720 0.224462 -0.502512 0.028222 10.991364 0.020885 0.132006 -0.272017 -0.251902 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.052571 3.510975 3.985795 0.024544 0.014487 0.999559 0.000000 13.074334 0.000000 0.012993 -0.086666 -0.169858 -0.035381 14.148866 0.000001 -0.000721 0.010196 0.508939 0.000000 4.097309 -0.000000 -0.001580 -0.005677 0.297517 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.271205 -0.128798 0.044120 0.988871 -0.000000 13.074365 0.000000 0.060153 0.153976 -0.050927 -0.035380 14.148879 0.012529 0.000040 -0.008082 0.503377 -0.000000 4.097290 0.000000 0.000155 0.000292 0.262676 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 72 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.714251 6.963917 9.210765 -0.007282 -0.151033 -0.889537 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.049537 0.095041 -0.220065 0.000000 7.977538 -0.000000 -0.027612 0.047131 0.027181 -0.000001 13.487594 -0.000000 0.032466 0.061484 -0.362601 0.000000 4.943967 0.000000 0.067650 0.068500 -0.359750 -1.438276 12.059815 -7.620788 0.640864 -0.408486 0.190600 0.000000 12.910863 -0.000000 0.235528 0.109550 -0.624197 -0.031254 11.193578 0.010376 -0.373137 -0.019827 -0.108350 0.054595 0.018457 -0.015135 0.513505 -0.060747 -0.082230 0.000000 3.968525 0.000000 -0.018040 0.136871 0.468302 0.000000 5.348644 -0.000000 0.774555 0.093614 0.440117 0.000000 2.438549 0.000000 0.142990 0.280994 -0.302707 -0.655276 1.265095 -0.625147 0.131281 -0.976366 0.127386 1.138250 11.707429 7.871705 -0.934770 -0.244695 -0.032039 0.000000 13.379274 -0.000000 -0.369701 0.218992 -0.505215 0.028222 10.991364 0.020885 0.126700 -0.269199 -0.245704 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.048213 3.511781 3.992419 0.024551 0.014341 0.999559 0.000000 13.074334 0.000000 0.012821 -0.086657 -0.169289 -0.035381 14.148866 0.000001 -0.000723 0.010195 0.508951 0.000000 4.097309 -0.000000 -0.001584 -0.005688 0.297625 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.265634 -0.127999 0.043994 0.988981 -0.000000 13.074365 0.000000 0.059969 0.153197 -0.050977 -0.035380 14.148879 0.012529 0.000040 -0.008083 0.503374 -0.000000 4.097290 0.000000 0.000156 0.000294 0.262708 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 73 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.745020 6.879979 9.271611 -0.004531 -0.150496 -0.887957 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.050287 0.081502 -0.217879 0.000000 7.977538 -0.000000 -0.028984 0.047273 0.026516 -0.000001 13.487594 -0.000000 0.030550 0.057200 -0.362772 0.000000 4.943967 0.000000 0.065703 0.066528 -0.361074 -1.437601 12.063319 -7.620929 0.640505 -0.408675 0.191099 0.000000 12.910863 -0.000000 0.235250 0.109865 -0.625309 -0.031254 11.193578 0.010376 -0.373770 -0.019867 -0.108563 0.054595 0.018457 -0.015135 0.514130 -0.060732 -0.081668 0.000000 3.968525 0.000000 -0.018167 0.137046 0.468535 0.000000 5.348644 -0.000000 0.774966 0.093660 0.440706 0.000000 2.438549 0.000000 0.142122 0.281310 -0.303076 -0.655276 1.265095 -0.625147 0.133074 -0.974634 0.138231 1.138894 11.710769 7.871569 -0.937501 -0.236681 -0.028523 0.000000 13.379274 -0.000000 -0.363975 0.213546 -0.508866 0.028222 10.991364 0.020885 0.121739 -0.266057 -0.238346 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.041761 3.513035 4.002294 0.024562 0.014123 0.999558 0.000000 13.074334 0.000000 0.012563 -0.086644 -0.168414 -0.035381 14.148866 0.000001 -0.000724 0.010195 0.508963 0.000000 4.097309 -0.000000 -0.001583 -0.005684 0.297581 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.257338 -0.126808 0.043808 0.989144 -0.000000 13.074365 0.000000 0.059697 0.152038 -0.051029 -0.035380 14.148879 0.012529 0.000040 -0.008083 0.503371 -0.000000 4.097290 0.000000 0.000156 0.000295 0.262738 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 74 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.778651 6.793829 9.332661 -0.001699 -0.150028 -0.886248 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.050967 0.067317 -0.215366 0.000000 7.977538 -0.000000 -0.030328 0.047411 0.025862 -0.000001 13.487594 -0.000000 0.028626 0.052898 -0.362933 0.000000 4.943967 0.000000 0.063738 0.064539 -0.362406 -1.437043 12.066212 -7.621046 0.640399 -0.408719 0.191552 0.000000 12.910863 -0.000000 0.235006 0.110142 -0.626288 -0.031254 11.193578 0.010376 -0.374298 -0.019900 -0.108741 0.054595 0.018457 -0.015135 0.514652 -0.060719 -0.081199 0.000000 3.968525 0.000000 -0.018272 0.137192 0.468731 0.000000 5.348644 -0.000000 0.775308 0.093698 0.441197 0.000000 2.438549 0.000000 0.141397 0.281573 -0.303383 -0.655276 1.265095 -0.625147 0.134165 -0.972602 0.150553 1.139490 11.713859 7.871445 -0.940208 -0.228971 -0.025809 0.000000 13.379274 -0.000000 -0.358248 0.209074 -0.513102 0.028222 10.991364 0.020885 0.116779 -0.264664 -0.229980 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.033341 3.514709 4.015224 0.024577 0.013839 0.999557 0.000000 13.074334 0.000000 0.012226 -0.086627 -0.167252 -0.035381 14.148866 0.000001 -0.000725 0.010194 0.508975 0.000000 4.097309 -0.000000 -0.001576 -0.005665 0.297395 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.246481 -0.125248 0.043565 0.989355 -0.000000 13.074365 0.000000 0.059342 0.150519 -0.051081 -0.035380 14.148879 0.012529 0.000040 -0.008083 0.503368 -0.000000 4.097290 0.000000 0.000157 0.000296 0.262766 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 75 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.815634 6.706026 9.392735 0.001159 -0.149644 -0.884426 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.051578 0.052566 -0.212547 0.000000 7.977538 -0.000000 -0.031641 0.047547 0.025222 -0.000001 13.487594 -0.000000 0.026697 0.048582 -0.363084 0.000000 4.943967 0.000000 0.061757 0.062534 -0.363745 -1.436601 12.068505 -7.621139 0.640534 -0.408624 0.191959 0.000000 12.910863 -0.000000 0.234793 0.110382 -0.627134 -0.031254 11.193578 0.010376 -0.374723 -0.019927 -0.108884 0.054595 0.018457 -0.015135 0.515072 -0.060709 -0.080820 0.000000 3.968525 0.000000 -0.018357 0.137309 0.468888 0.000000 5.348644 -0.000000 0.775583 0.093729 0.441592 0.000000 2.438549 0.000000 0.140814 0.281784 -0.303631 -0.655276 1.265095 -0.625147 0.134423 -0.970444 0.163127 1.140036 11.716693 7.871330 -0.942816 -0.221811 -0.024306 0.000000 13.379274 -0.000000 -0.353340 0.206736 -0.517578 0.028222 10.991364 0.020885 0.111431 -0.267537 -0.220727 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.023073 3.516780 4.031025 0.024595 0.013492 0.999555 0.000000 13.074334 0.000000 0.011813 -0.086606 -0.165820 -0.035381 14.148866 0.000001 -0.000727 0.010193 0.508986 0.000000 4.097309 -0.000000 -0.001566 -0.005632 0.297076 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.233217 -0.123343 0.043267 0.989608 -0.000000 13.074365 0.000000 0.058909 0.148664 -0.051133 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503366 -0.000000 4.097290 0.000000 0.000158 0.000297 0.262792 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 76 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.857216 6.616469 9.452676 0.003989 -0.149362 -0.882507 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.052119 0.037333 -0.209442 0.000000 7.977538 -0.000000 -0.032917 0.047678 0.024598 -0.000001 13.487594 -0.000000 0.024763 0.044258 -0.363225 0.000000 4.943967 0.000000 0.059763 0.060515 -0.365088 -1.436273 12.070208 -7.621208 0.640902 -0.408396 0.192320 0.000000 12.910863 -0.000000 0.234613 0.110585 -0.627849 -0.031254 11.193578 0.010376 -0.375048 -0.019948 -0.108993 0.054595 0.018457 -0.015135 0.515392 -0.060701 -0.080532 0.000000 3.968525 0.000000 -0.018422 0.137399 0.469008 0.000000 5.348644 -0.000000 0.775791 0.093752 0.441892 0.000000 2.438549 0.000000 0.140368 0.281946 -0.303819 -0.655276 1.265095 -0.625147 0.133509 -0.967994 0.176949 1.140531 11.719265 7.871226 -0.945399 -0.215434 -0.024397 0.000000 13.379274 -0.000000 -0.349012 0.207079 -0.522914 0.028222 10.991364 0.020885 0.105615 -0.275855 -0.209965 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.011073 3.519224 4.049517 0.024616 0.013086 0.999553 0.000000 13.074334 0.000000 0.011329 -0.086582 -0.164134 -0.035381 14.148866 0.000001 -0.000728 0.010193 0.508996 0.000000 4.097309 -0.000000 -0.001552 -0.005587 0.296634 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.217698 -0.121112 0.042919 0.989899 -0.000000 13.074365 0.000000 0.058402 0.146491 -0.051185 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503363 -0.000000 4.097290 0.000000 0.000158 0.000298 0.262814 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 77 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.902850 6.524810 9.513315 0.006734 -0.149201 -0.880511 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.052591 0.021699 -0.206070 0.000000 7.977538 -0.000000 -0.034151 0.047805 0.023992 -0.000001 13.487594 -0.000000 0.022827 0.039927 -0.363355 0.000000 4.943967 0.000000 0.057757 0.058484 -0.366436 -1.436057 12.071330 -7.621253 0.641494 -0.408039 0.192635 0.000000 12.910863 -0.000000 0.234465 0.110752 -0.628435 -0.031254 11.193578 0.010376 -0.375272 -0.019962 -0.109069 0.054595 0.018457 -0.015135 0.515614 -0.060695 -0.080332 0.000000 3.968525 0.000000 -0.018467 0.137462 0.469091 0.000000 5.348644 -0.000000 0.775936 0.093768 0.442100 0.000000 2.438549 0.000000 0.140060 0.282058 -0.303950 -0.655276 1.265095 -0.625147 0.131547 -0.965011 0.192839 1.140975 11.721569 7.871133 -0.948014 -0.209628 -0.025722 0.000000 13.379274 -0.000000 -0.344647 0.209135 -0.529301 0.028222 10.991364 0.020885 0.099619 -0.287523 -0.197653 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.997451 3.522016 4.070529 0.024640 0.012625 0.999549 0.000000 13.074334 0.000000 0.010779 -0.086555 -0.162209 -0.035381 14.148866 0.000001 -0.000729 0.010192 0.509006 0.000000 4.097309 -0.000000 -0.001533 -0.005531 0.296078 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.200067 -0.118576 0.042522 0.990224 -0.000000 13.074365 0.000000 0.057826 0.144021 -0.051236 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503361 -0.000000 4.097290 0.000000 0.000159 0.000299 0.262835 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 78 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.951043 6.431133 9.573434 0.009334 -0.149180 -0.878457 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.052993 0.005750 -0.202454 0.000000 7.977538 -0.000000 -0.035339 0.047928 0.023407 -0.000001 13.487594 -0.000000 0.020891 0.035595 -0.363474 0.000000 4.943967 0.000000 0.055743 0.056445 -0.367785 -1.435951 12.071880 -7.621275 0.642303 -0.407556 0.192902 0.000000 12.910863 -0.000000 0.234349 0.110881 -0.628891 -0.031254 11.193578 0.010376 -0.375399 -0.019970 -0.109111 0.054595 0.018457 -0.015135 0.515739 -0.060692 -0.080220 0.000000 3.968525 0.000000 -0.018492 0.137497 0.469138 0.000000 5.348644 -0.000000 0.776016 0.093777 0.442217 0.000000 2.438549 0.000000 0.139886 0.282120 -0.304023 -0.655276 1.265095 -0.625147 0.128930 -0.961646 0.209452 1.141367 11.723599 7.871051 -0.950568 -0.204121 -0.027842 0.000000 13.379274 -0.000000 -0.340615 0.212294 -0.535929 0.028222 10.991364 0.020885 0.093503 -0.301197 -0.184557 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.982317 3.525135 4.093894 0.024667 0.012112 0.999543 0.000000 13.074334 0.000000 0.010167 -0.086524 -0.160060 -0.035381 14.148866 0.000001 -0.000730 0.010192 0.509014 0.000000 4.097309 -0.000000 -0.001512 -0.005463 0.295414 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.180464 -0.115754 0.042082 0.990578 -0.000000 13.074365 0.000000 0.057185 0.141272 -0.051287 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503360 -0.000000 4.097290 0.000000 0.000159 0.000300 0.262852 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 79 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.000652 6.335391 9.631949 0.011725 -0.149325 -0.876370 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053326 -0.010430 -0.198618 0.000000 7.977538 -0.000000 -0.036475 0.048045 0.022846 -0.000001 13.487594 -0.000000 0.018955 0.031265 -0.363582 0.000000 4.943967 0.000000 0.053721 0.054398 -0.369135 -1.435954 12.071866 -7.621275 0.643322 -0.406952 0.193122 0.000000 12.910863 -0.000000 0.234266 0.110974 -0.629217 -0.031254 11.193578 0.010376 -0.375427 -0.019972 -0.109121 0.054595 0.018457 -0.015135 0.515767 -0.060691 -0.080194 0.000000 3.968525 0.000000 -0.018497 0.137504 0.469149 0.000000 5.348644 -0.000000 0.776035 0.093779 0.442243 0.000000 2.438549 0.000000 0.139848 0.282135 -0.304040 -0.655276 1.265095 -0.625147 0.125973 -0.958098 0.225656 1.141704 11.725349 7.870980 -0.952986 -0.198700 -0.030418 0.000000 13.379274 -0.000000 -0.337264 0.216102 -0.542126 0.028222 10.991364 0.020885 0.087324 -0.315846 -0.171316 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.965772 3.528560 4.119452 0.024696 0.011552 0.999535 0.000000 13.074334 0.000000 0.009498 -0.086489 -0.157702 -0.035381 14.148866 0.000001 -0.000731 0.010191 0.509022 0.000000 4.097309 -0.000000 -0.001487 -0.005385 0.294652 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.159024 -0.112666 0.041599 0.990955 -0.000000 13.074365 0.000000 0.056483 0.138264 -0.051335 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503358 -0.000000 4.097290 0.000000 0.000159 0.000301 0.262867 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 80 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.050668 6.237421 9.687731 0.013836 -0.149660 -0.874273 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053591 -0.026753 -0.194584 0.000000 7.977538 -0.000000 -0.037556 0.048156 0.022311 -0.000001 13.487594 -0.000000 0.017023 0.026941 -0.363679 0.000000 4.943967 0.000000 0.051694 0.052347 -0.370484 -1.436064 12.071296 -7.621252 0.644545 -0.406228 0.193293 0.000000 12.910863 -0.000000 0.234216 0.111030 -0.629414 -0.031254 11.193578 0.010376 -0.375358 -0.019968 -0.109097 0.054595 0.018457 -0.015135 0.515699 -0.060693 -0.080256 0.000000 3.968525 0.000000 -0.018484 0.137485 0.469123 0.000000 5.348644 -0.000000 0.775991 0.093775 0.442179 0.000000 2.438549 0.000000 0.139942 0.282100 -0.304000 -0.655276 1.265095 -0.625147 0.122974 -0.954641 0.240356 1.141986 11.726811 7.870921 -0.955204 -0.193165 -0.033157 0.000000 13.379274 -0.000000 -0.334978 0.220173 -0.547251 0.028222 10.991364 0.020885 0.081137 -0.330579 -0.158543 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.947920 3.532270 4.147046 0.024727 0.010946 0.999525 0.000000 13.074334 0.000000 0.008774 -0.086452 -0.155148 -0.035381 14.148866 0.000001 -0.000732 0.010191 0.509029 0.000000 4.097309 -0.000000 -0.001459 -0.005298 0.293797 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.135877 -0.109329 0.041077 0.991351 -0.000000 13.074365 0.000000 0.055725 0.135012 -0.051382 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503357 -0.000000 4.097290 0.000000 0.000160 0.000301 0.262880 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 81 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.100060 6.136908 9.739425 0.015588 -0.150216 -0.872197 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053788 -0.043134 -0.190380 0.000000 7.977538 -0.000000 -0.038574 0.048260 0.021805 -0.000001 13.487594 -0.000000 0.015095 0.022626 -0.363765 0.000000 4.943967 0.000000 0.049666 0.050293 -0.371831 -1.436280 12.070176 -7.621207 0.645967 -0.405387 0.193414 0.000000 12.910863 -0.000000 0.234198 0.111049 -0.629480 -0.031254 11.193578 0.010376 -0.375192 -0.019957 -0.109042 0.054595 0.018457 -0.015135 0.515535 -0.060697 -0.080403 0.000000 3.968525 0.000000 -0.018450 0.137439 0.469062 0.000000 5.348644 -0.000000 0.775884 0.093763 0.442026 0.000000 2.438549 0.000000 0.140170 0.282018 -0.303903 -0.655276 1.265095 -0.625147 0.120257 -0.951650 0.252318 1.142211 11.727981 7.870873 -0.957152 -0.187305 -0.035762 0.000000 13.379274 -0.000000 -0.334235 0.224132 -0.550579 0.028222 10.991364 0.020885 0.075007 -0.344520 -0.146916 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.928857 3.536243 4.176525 0.024761 0.010300 0.999513 0.000000 13.074334 0.000000 0.008001 -0.086411 -0.152413 -0.035381 14.148866 0.000001 -0.000732 0.010191 0.509035 0.000000 4.097309 -0.000000 -0.001428 -0.005203 0.292858 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.111151 -0.105761 0.040518 0.991761 -0.000000 13.074365 0.000000 0.054915 0.131536 -0.051426 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503356 -0.000000 4.097290 0.000000 0.000160 0.000302 0.262889 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 82 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.147598 6.033324 9.785197 0.016893 -0.151028 -0.870173 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053919 -0.059486 -0.186031 0.000000 7.977538 -0.000000 -0.039526 0.048358 0.021329 -0.000001 13.487594 -0.000000 0.013173 0.018325 -0.363839 0.000000 4.943967 0.000000 0.047637 0.048239 -0.373173 -1.436600 12.068515 -7.621139 0.647583 -0.404430 0.193485 0.000000 12.910863 -0.000000 0.234214 0.111030 -0.629414 -0.031254 11.193578 0.010376 -0.374929 -0.019940 -0.108953 0.054595 0.018457 -0.015135 0.515275 -0.060704 -0.080637 0.000000 3.968525 0.000000 -0.018398 0.137366 0.468964 0.000000 5.348644 -0.000000 0.775715 0.093744 0.441783 0.000000 2.438549 0.000000 0.140531 0.281887 -0.303750 -0.655276 1.265095 -0.625147 0.118218 -0.949648 0.259951 1.142379 11.728850 7.870838 -0.958739 -0.180856 -0.037881 0.000000 13.379274 -0.000000 -0.335699 0.227539 -0.551158 0.028222 10.991364 0.020885 0.069013 -0.356661 -0.137294 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.908681 3.540461 4.207741 0.024797 0.009615 0.999498 0.000000 13.074334 0.000000 0.007181 -0.086368 -0.149509 -0.035381 14.148866 0.000001 -0.000733 0.010190 0.509039 0.000000 4.097309 -0.000000 -0.001395 -0.005099 0.291840 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.084970 -0.101980 0.039927 0.992182 -0.000000 13.074365 0.000000 0.054055 0.127852 -0.051468 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503355 -0.000000 4.097290 0.000000 0.000160 0.000302 0.262896 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 83 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.193674 5.925032 9.824945 0.017769 -0.152243 -0.868110 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053985 -0.075722 -0.181566 0.000000 7.977538 -0.000000 -0.040406 0.048449 0.020888 -0.000001 13.487594 -0.000000 0.011259 0.014041 -0.363901 0.000000 4.943967 0.000000 0.045610 0.046187 -0.374510 -1.437023 12.066317 -7.621050 0.649390 -0.403359 0.193502 0.000000 12.910863 -0.000000 0.234263 0.110974 -0.629216 -0.031254 11.193578 0.010376 -0.374569 -0.019918 -0.108832 0.054595 0.018457 -0.015135 0.514920 -0.060712 -0.080958 0.000000 3.968525 0.000000 -0.018326 0.137267 0.468831 0.000000 5.348644 -0.000000 0.775483 0.093718 0.441449 0.000000 2.438549 0.000000 0.141025 0.281708 -0.303541 -0.655276 1.265095 -0.625147 0.117021 -0.948655 0.263540 1.142487 11.729412 7.870815 -0.959982 -0.173642 -0.039519 0.000000 13.379274 -0.000000 -0.339686 0.230561 -0.548853 0.028222 10.991364 0.020885 0.063095 -0.367542 -0.129593 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.887483 3.544904 4.240549 0.024834 0.008896 0.999480 0.000000 13.074334 0.000000 0.006320 -0.086321 -0.146450 -0.035381 14.148866 0.000001 -0.000733 0.010190 0.509043 0.000000 4.097309 -0.000000 -0.001359 -0.004988 0.290750 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.057455 -0.098003 0.039304 0.992607 -0.000000 13.074365 0.000000 0.053151 0.123976 -0.051506 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503355 -0.000000 4.097290 0.000000 0.000160 0.000302 0.262900 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 84 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.239633 5.811946 9.860784 0.018337 -0.153904 -0.865914 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053988 -0.091757 -0.177013 0.000000 7.977538 -0.000000 -0.041209 0.048531 0.020484 -0.000001 13.487594 -0.000000 0.009354 0.009778 -0.363952 0.000000 4.943967 0.000000 0.043587 0.044139 -0.375840 -1.437549 12.063589 -7.620940 0.651382 -0.402175 0.193466 0.000000 12.910863 -0.000000 0.234346 0.110879 -0.628882 -0.031254 11.193578 0.010376 -0.374111 -0.019889 -0.108678 0.054595 0.018457 -0.015135 0.514467 -0.060724 -0.081365 0.000000 3.968525 0.000000 -0.018235 0.137140 0.468662 0.000000 5.348644 -0.000000 0.775187 0.093685 0.441023 0.000000 2.438549 0.000000 0.141654 0.281480 -0.303275 -0.655276 1.265095 -0.625147 0.116385 -0.948201 0.265000 1.142535 11.729658 7.870805 -0.960996 -0.165836 -0.040991 0.000000 13.379274 -0.000000 -0.345583 0.233677 -0.544645 0.028222 10.991364 0.020885 0.057147 -0.378408 -0.122871 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.865358 3.549554 4.274807 0.024874 0.008144 0.999459 0.000000 13.074334 0.000000 0.005420 -0.086272 -0.143249 -0.035381 14.148866 0.000001 -0.000734 0.010190 0.509045 0.000000 4.097309 -0.000000 -0.001321 -0.004871 0.289595 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.028726 -0.093846 0.038652 0.993035 -0.000000 13.074365 0.000000 0.052206 0.119926 -0.051541 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503355 -0.000000 4.097290 0.000000 0.000160 0.000302 0.262901 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 85 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.285373 5.695444 9.893481 0.018646 -0.155910 -0.863611 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053930 -0.107504 -0.172403 0.000000 7.977538 -0.000000 -0.041927 0.048605 0.020120 -0.000001 13.487594 -0.000000 0.007461 0.005539 -0.363992 0.000000 4.943967 0.000000 0.041571 0.042098 -0.377161 -1.438176 12.060335 -7.620809 0.653557 -0.400878 0.193375 0.000000 12.910863 -0.000000 0.234464 0.110746 -0.628412 -0.031254 11.193578 0.010376 -0.373555 -0.019853 -0.108491 0.054595 0.018457 -0.015135 0.513918 -0.060737 -0.081859 0.000000 3.968525 0.000000 -0.018124 0.136986 0.468456 0.000000 5.348644 -0.000000 0.774826 0.093645 0.440506 0.000000 2.438549 0.000000 0.142417 0.281203 -0.302951 -0.655276 1.265096 -0.625147 0.116168 -0.948222 0.264651 1.142520 11.729583 7.870809 -0.961795 -0.157593 -0.042323 0.000000 13.379274 -0.000000 -0.352941 0.236812 -0.538881 0.028222 10.991364 0.020885 0.051192 -0.389105 -0.116968 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.842394 3.554390 4.310375 0.024914 0.007364 0.999434 0.000000 13.074334 0.000000 0.004485 -0.086219 -0.139917 -0.035381 14.148866 0.000001 -0.000734 0.010190 0.509046 0.000000 4.097309 -0.000000 -0.001282 -0.004747 0.288380 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.998900 -0.089528 0.037975 0.993460 -0.000000 13.074365 0.000000 0.051224 0.115716 -0.051571 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503355 -0.000000 4.097290 0.000000 0.000160 0.000302 0.262898 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 86 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.330801 5.576724 9.923691 0.018738 -0.158172 -0.861224 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053814 -0.122880 -0.167765 0.000000 7.977538 -0.000000 -0.042557 0.048670 0.019798 -0.000001 13.487594 -0.000000 0.005581 0.001330 -0.364020 0.000000 4.943967 0.000000 0.039564 0.040067 -0.378473 -1.438903 12.056563 -7.620656 0.655912 -0.399468 0.193226 0.000000 12.910863 -0.000000 0.234616 0.110573 -0.627802 -0.031254 11.193578 0.010376 -0.372899 -0.019812 -0.108270 0.054595 0.018457 -0.015135 0.513270 -0.060753 -0.082442 0.000000 3.968525 0.000000 -0.017993 0.136805 0.468214 0.000000 5.348644 -0.000000 0.774399 0.093597 0.439894 0.000000 2.438549 0.000000 0.143317 0.280876 -0.302569 -0.655276 1.265096 -0.625147 0.116246 -0.948652 0.262757 1.142442 11.729176 7.870825 -0.962391 -0.149048 -0.043536 0.000000 13.379274 -0.000000 -0.361372 0.239894 -0.531858 0.028222 10.991364 0.020885 0.045251 -0.399500 -0.111751 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.818682 3.559396 4.347116 0.024956 0.006559 0.999405 0.000000 13.074334 0.000000 0.003518 -0.086164 -0.136469 -0.035381 14.148866 0.000001 -0.000734 0.010190 0.509045 0.000000 4.097309 -0.000000 -0.001240 -0.004618 0.287111 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.968093 -0.085063 0.037274 0.993879 -0.000000 13.074365 0.000000 0.050209 0.111364 -0.051597 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503355 -0.000000 4.097290 0.000000 0.000160 0.000302 0.262893 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 87 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.375827 5.456857 9.951987 0.018649 -0.160615 -0.858773 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053643 -0.137802 -0.163132 0.000000 7.977538 -0.000000 -0.043091 0.048725 0.019523 -0.000001 13.487594 -0.000000 0.003716 -0.002847 -0.364036 0.000000 4.943967 0.000000 0.037569 0.038047 -0.379773 -1.439730 12.052274 -7.620483 0.658444 -0.397945 0.193017 0.000000 12.910863 -0.000000 0.234804 0.110359 -0.627050 -0.031254 11.193578 0.010376 -0.372143 -0.019764 -0.108016 0.054595 0.018457 -0.015135 0.512522 -0.060772 -0.083113 0.000000 3.968525 0.000000 -0.017842 0.136596 0.467934 0.000000 5.348644 -0.000000 0.773903 0.093541 0.439187 0.000000 2.438549 0.000000 0.144353 0.280498 -0.302128 -0.655276 1.265096 -0.625148 0.116506 -0.949431 0.259541 1.142298 11.728431 7.870855 -0.962799 -0.140325 -0.044646 0.000000 13.379274 -0.000000 -0.370531 0.242860 -0.523840 0.028222 10.991364 0.020885 0.039345 -0.409469 -0.107104 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.794309 3.564553 4.384893 0.024999 0.005731 0.999372 0.000000 13.074334 0.000000 0.002524 -0.086106 -0.132915 -0.035381 14.148866 0.000001 -0.000733 0.010190 0.509043 0.000000 4.097309 -0.000000 -0.001197 -0.004484 0.285793 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.936418 -0.080470 0.036553 0.994288 -0.000000 13.074365 0.000000 0.049163 0.106885 -0.051619 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503356 -0.000000 4.097290 0.000000 0.000160 0.000301 0.262885 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 88 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.420365 5.336832 9.978883 0.018411 -0.163166 -0.856278 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053419 -0.152188 -0.158535 0.000000 7.977538 -0.000000 -0.043522 0.048769 0.019296 -0.000001 13.487594 -0.000000 0.001868 -0.006987 -0.364040 0.000000 4.943967 0.000000 0.035587 0.036040 -0.381060 -1.440655 12.047474 -7.620288 0.661151 -0.396309 0.192747 0.000000 12.910863 -0.000000 0.235027 0.110105 -0.626153 -0.031254 11.193578 0.010376 -0.371286 -0.019710 -0.107728 0.054595 0.018457 -0.015135 0.511673 -0.060792 -0.083875 0.000000 3.968525 0.000000 -0.017671 0.136359 0.467618 0.000000 5.348644 -0.000000 0.773337 0.093478 0.438383 0.000000 2.438549 0.000000 0.145529 0.280070 -0.301627 -0.655276 1.265096 -0.625148 0.116846 -0.950499 0.255200 1.142087 11.727339 7.870899 -0.963033 -0.131537 -0.045670 0.000000 13.379274 -0.000000 -0.380100 0.245650 -0.515074 0.028222 10.991364 0.020885 0.033492 -0.418893 -0.102929 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.769363 3.569843 4.423573 0.025043 0.004883 0.999336 0.000000 13.074334 0.000000 0.001506 -0.086046 -0.129269 -0.035381 14.148866 0.000001 -0.000733 0.010190 0.509040 0.000000 4.097309 -0.000000 -0.001153 -0.004346 0.284433 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.903988 -0.075763 0.035814 0.994685 -0.000000 13.074365 0.000000 0.048091 0.102296 -0.051635 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503357 -0.000000 4.097290 0.000000 0.000159 0.000301 0.262873 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 89 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.464328 5.217582 10.004853 0.018050 -0.165761 -0.853757 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053146 -0.165958 -0.154006 0.000000 7.977538 -0.000000 -0.043845 0.048802 0.019122 -0.000001 13.487594 -0.000000 0.000038 -0.011087 -0.364034 0.000000 4.943967 0.000000 0.033622 0.034051 -0.382332 -1.441678 12.042167 -7.620074 0.664031 -0.394558 0.192413 0.000000 12.910863 -0.000000 0.235288 0.109808 -0.625106 -0.031254 11.193578 0.010376 -0.370325 -0.019649 -0.107405 0.054595 0.018457 -0.015135 0.510722 -0.060815 -0.084727 0.000000 3.968525 0.000000 -0.017480 0.136093 0.467263 0.000000 5.348644 -0.000000 0.772698 0.093407 0.437478 0.000000 2.438549 0.000000 0.146846 0.279589 -0.301066 -0.655276 1.265096 -0.625148 0.117168 -0.951804 0.249906 1.141808 11.725890 7.870958 -0.963113 -0.122791 -0.046623 0.000000 13.379274 -0.000000 -0.389786 0.248208 -0.505804 0.028222 10.991364 0.020885 0.027711 -0.427662 -0.099136 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.743931 3.575249 4.463020 0.025088 0.004018 0.999295 0.000000 13.074334 0.000000 0.000467 -0.085983 -0.125543 -0.035381 14.148866 0.000001 -0.000732 0.010191 0.509034 0.000000 4.097309 -0.000000 -0.001107 -0.004204 0.283035 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.870917 -0.070960 0.035058 0.995066 -0.000000 13.074365 0.000000 0.046997 0.097612 -0.051645 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503359 -0.000000 4.097290 0.000000 0.000159 0.000300 0.262859 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 90 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.507627 5.100014 10.030344 0.017594 -0.168339 -0.851230 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.052826 -0.179031 -0.149580 0.000000 7.977538 -0.000000 -0.044053 0.048824 0.019005 -0.000001 13.487594 -0.000000 -0.001772 -0.015141 -0.364015 0.000000 4.943967 0.000000 0.031675 0.032080 -0.383589 -1.442798 12.036356 -7.619839 0.667083 -0.392692 0.192012 0.000000 12.910863 -0.000000 0.235586 0.109468 -0.623905 -0.031254 11.193578 0.010376 -0.369260 -0.019581 -0.107046 0.054595 0.018457 -0.015135 0.509665 -0.060841 -0.085673 0.000000 3.968525 0.000000 -0.017268 0.135799 0.466870 0.000000 5.348644 -0.000000 0.771984 0.093327 0.436469 0.000000 2.438549 0.000000 0.148307 0.279055 -0.300442 -0.655276 1.265096 -0.625148 0.117377 -0.953296 0.243818 1.141459 11.724076 7.871031 -0.963061 -0.114195 -0.047518 0.000000 13.379274 -0.000000 -0.399310 0.250483 -0.496271 0.028222 10.991364 0.020885 0.022021 -0.435661 -0.095643 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.718099 3.580752 4.503100 0.025134 0.003139 0.999250 0.000000 13.074334 0.000000 -0.000589 -0.085918 -0.121749 -0.035381 14.148866 0.000001 -0.000731 0.010191 0.509027 0.000000 4.097309 -0.000000 -0.001061 -0.004059 0.281606 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.837316 -0.066077 0.034290 0.995429 -0.000000 13.074365 0.000000 0.045884 0.092850 -0.051650 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503361 -0.000000 4.097290 0.000000 0.000159 0.000299 0.262841 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 91 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.550168 4.985030 10.055787 0.017068 -0.170837 -0.848716 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.052464 -0.191330 -0.145289 0.000000 7.977538 -0.000000 -0.044137 0.048833 0.018947 -0.000001 13.487594 -0.000000 -0.003559 -0.019147 -0.363986 0.000000 4.943967 0.000000 0.029750 0.030131 -0.384828 -1.444014 12.030043 -7.619584 0.670304 -0.390709 0.191542 0.000000 12.910863 -0.000000 0.235922 0.109083 -0.622547 -0.031254 11.193578 0.010376 -0.368087 -0.019507 -0.106652 0.054595 0.018457 -0.015135 0.508502 -0.060869 -0.086713 0.000000 3.968525 0.000000 -0.017035 0.135476 0.466437 0.000000 5.348644 -0.000000 0.771191 0.093238 0.435355 0.000000 2.438549 0.000000 0.149914 0.278467 -0.299755 -0.655276 1.265096 -0.625148 0.117379 -0.954931 0.237084 1.141037 11.721887 7.871120 -0.962906 -0.105851 -0.048368 0.000000 13.379274 -0.000000 -0.408400 0.252425 -0.486725 0.028222 10.991364 0.020885 0.016438 -0.442778 -0.092373 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.691954 3.586334 4.543682 0.025180 0.002250 0.999201 0.000000 13.074334 0.000000 -0.001658 -0.085851 -0.117900 -0.035381 14.148866 0.000001 -0.000730 0.010192 0.509018 0.000000 4.097309 -0.000000 -0.001013 -0.003912 0.280150 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.803297 -0.061131 0.033511 0.995771 -0.000000 13.074365 0.000000 0.044757 0.088025 -0.051648 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503363 -0.000000 4.097290 0.000000 0.000158 0.000298 0.262819 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 92 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.591850 4.873552 10.081612 0.016494 -0.173195 -0.846236 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.052063 -0.202775 -0.141167 0.000000 7.977538 -0.000000 -0.044090 0.048828 0.018953 -0.000001 13.487594 -0.000000 -0.005322 -0.023099 -0.363945 0.000000 4.943967 0.000000 0.027848 0.028205 -0.386049 -1.445327 12.023232 -7.619308 0.673695 -0.388608 0.190999 0.000000 12.910863 -0.000000 0.236296 0.108653 -0.621024 -0.031254 11.193578 0.010376 -0.366805 -0.019426 -0.106221 0.054595 0.018457 -0.015135 0.507228 -0.060899 -0.087850 0.000000 3.968525 0.000000 -0.016781 0.135123 0.465964 0.000000 5.348644 -0.000000 0.770315 0.093140 0.434131 0.000000 2.438549 0.000000 0.151669 0.277823 -0.299003 -0.655276 1.265096 -0.625148 0.117080 -0.956668 0.229844 1.140541 11.719314 7.871224 -0.962680 -0.097868 -0.049185 0.000000 13.379274 -0.000000 -0.416788 0.253987 -0.477426 0.028222 10.991364 0.020885 0.010982 -0.448890 -0.089255 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.665583 3.591979 4.584630 0.025226 0.001352 0.999147 0.000000 13.074334 0.000000 -0.002737 -0.085782 -0.114007 -0.035381 14.148866 0.000001 -0.000729 0.010192 0.509008 0.000000 4.097309 -0.000000 -0.000965 -0.003762 0.278673 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.768973 -0.056138 0.032725 0.996091 -0.000000 13.074365 0.000000 0.043618 0.083154 -0.051640 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503365 -0.000000 4.097290 0.000000 0.000158 0.000297 0.262794 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 93 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.632564 4.766545 10.108254 0.015898 -0.175350 -0.843813 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.051627 -0.213284 -0.137249 0.000000 7.977538 -0.000000 -0.043905 0.048809 0.019026 -0.000001 13.487594 -0.000000 -0.007060 -0.026994 -0.363894 0.000000 4.943967 0.000000 0.025973 0.026307 -0.387249 -1.446735 12.015924 -7.619013 0.677253 -0.386386 0.190381 0.000000 12.910863 -0.000000 0.236711 0.108174 -0.619333 -0.031254 11.193578 0.010376 -0.365411 -0.019338 -0.105752 0.054595 0.018457 -0.015135 0.505842 -0.060932 -0.089085 0.000000 3.968525 0.000000 -0.016505 0.134739 0.465451 0.000000 5.348644 -0.000000 0.769353 0.093032 0.432792 0.000000 2.438549 0.000000 0.153577 0.277121 -0.298183 -0.655276 1.265096 -0.625148 0.116380 -0.958470 0.222239 1.139969 11.716344 7.871344 -0.962423 -0.090356 -0.049983 0.000000 13.379274 -0.000000 -0.424205 0.255123 -0.468651 0.028222 10.991364 0.020885 0.005672 -0.453866 -0.086213 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.639073 3.597668 4.625809 0.025273 0.000450 0.999090 0.000000 13.074334 0.000000 -0.003823 -0.085712 -0.110085 -0.035381 14.148866 0.000001 -0.000728 0.010193 0.508995 0.000000 4.097309 -0.000000 -0.000916 -0.003611 0.277180 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.734458 -0.051115 0.031933 0.996387 -0.000000 13.074365 0.000000 0.042472 0.078252 -0.051625 -0.035380 14.148879 0.012529 0.000040 -0.008083 0.503368 -0.000000 4.097290 0.000000 0.000157 0.000296 0.262765 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 94 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.672187 4.665037 10.136167 0.015301 -0.177236 -0.841470 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.051158 -0.222777 -0.133571 0.000000 7.977538 -0.000000 -0.043571 0.048775 0.019172 -0.000001 13.487594 -0.000000 -0.008769 -0.030827 -0.363831 0.000000 4.943967 0.000000 0.024126 0.024437 -0.388427 -1.448239 12.008121 -7.618697 0.680978 -0.384040 0.189683 0.000000 12.910863 -0.000000 0.237167 0.107647 -0.617467 -0.031254 11.193578 0.010376 -0.363903 -0.019242 -0.105245 0.054595 0.018457 -0.015135 0.504340 -0.060967 -0.090421 0.000000 3.968525 0.000000 -0.016206 0.134324 0.464895 0.000000 5.348644 -0.000000 0.768300 0.092913 0.431335 0.000000 2.438549 0.000000 0.155642 0.276360 -0.297295 -0.655277 1.265096 -0.625148 0.115173 -0.960306 0.214407 1.139318 11.712968 7.871481 -0.962178 -0.083432 -0.050776 0.000000 13.379274 -0.000000 -0.430367 0.255783 -0.460697 0.028222 10.991364 0.020885 0.000530 -0.457560 -0.083176 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.612513 3.603383 4.667084 0.025319 -0.000454 0.999028 0.000000 13.074334 0.000000 -0.004911 -0.085639 -0.106145 -0.035381 14.148866 0.000001 -0.000726 0.010194 0.508980 0.000000 4.097309 -0.000000 -0.000867 -0.003458 0.275676 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.699864 -0.046079 0.031138 0.996657 -0.000000 13.074365 0.000000 0.041322 0.073338 -0.051602 -0.035380 14.148879 0.012529 0.000040 -0.008083 0.503372 -0.000000 4.097290 0.000000 0.000156 0.000295 0.262733 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 95 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.710581 4.570158 10.165834 0.014730 -0.178784 -0.839234 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.050662 -0.231167 -0.130168 0.000000 7.977538 -0.000000 -0.043081 0.048725 0.019395 -0.000001 13.487594 -0.000000 -0.010447 -0.034593 -0.363758 0.000000 4.943967 0.000000 0.022311 0.022600 -0.389581 -1.449838 11.999825 -7.618361 0.684871 -0.381569 0.188903 0.000000 12.910863 -0.000000 0.237664 0.107068 -0.615417 -0.031254 11.193578 0.010376 -0.362276 -0.019139 -0.104698 0.054595 0.018457 -0.015135 0.502719 -0.061004 -0.091860 0.000000 3.968525 0.000000 -0.015884 0.133878 0.464297 0.000000 5.348644 -0.000000 0.767151 0.092784 0.429755 0.000000 2.438549 0.000000 0.157866 0.275538 -0.296335 -0.655277 1.265096 -0.625148 0.113344 -0.962146 0.206494 1.138587 11.709175 7.871634 -0.961995 -0.077224 -0.051576 0.000000 13.379274 -0.000000 -0.434971 0.255914 -0.453886 0.028222 10.991364 0.020885 -0.004421 -0.459804 -0.080066 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.585992 3.609106 4.708318 0.025366 -0.001358 0.998963 0.000000 13.074334 0.000000 -0.005998 -0.085566 -0.102200 -0.035381 14.148866 0.000001 -0.000724 0.010195 0.508963 0.000000 4.097309 -0.000000 -0.000818 -0.003306 0.274168 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.665307 -0.041047 0.030343 0.996901 -0.000000 13.074365 0.000000 0.040173 0.068426 -0.051572 -0.035380 14.148879 0.012529 0.000040 -0.008082 0.503375 -0.000000 4.097290 0.000000 0.000155 0.000293 0.262696 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 96 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.747588 4.483164 10.197786 0.014211 -0.179913 -0.837135 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.050140 -0.238364 -0.127079 0.000000 7.977538 -0.000000 -0.042424 0.048658 0.019699 -0.000001 13.487594 -0.000000 -0.012094 -0.038288 -0.363674 0.000000 4.943967 0.000000 0.020530 0.020797 -0.390711 -1.451532 11.991037 -7.618006 0.688929 -0.378969 0.188036 0.000000 12.910863 -0.000000 0.238205 0.106436 -0.613178 -0.031254 11.193578 0.010376 -0.360528 -0.019029 -0.104111 0.054595 0.018457 -0.015135 0.500974 -0.061044 -0.093405 0.000000 3.968525 0.000000 -0.015539 0.133398 0.463654 0.000000 5.348644 -0.000000 0.765900 0.092644 0.428044 0.000000 2.438549 0.000000 0.160255 0.274652 -0.295302 -0.655277 1.265096 -0.625148 0.110765 -0.963961 0.198650 1.137773 11.704952 7.871805 -0.961928 -0.071872 -0.052400 0.000000 13.379274 -0.000000 -0.437679 0.255456 -0.448573 0.028222 10.991364 0.020885 -0.009156 -0.460402 -0.076800 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.559600 3.614818 4.749370 0.025412 -0.002257 0.998894 0.000000 13.074334 0.000000 -0.007081 -0.085491 -0.098264 -0.035381 14.148866 0.000001 -0.000722 0.010196 0.508944 0.000000 4.097309 -0.000000 -0.000769 -0.003153 0.272660 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.630905 -0.036036 0.029552 0.997118 -0.000000 13.074365 0.000000 0.039028 0.063535 -0.051534 -0.035380 14.148879 0.012529 0.000040 -0.008082 0.503379 -0.000000 4.097290 0.000000 0.000154 0.000291 0.262656 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 97 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.783023 4.405492 10.232615 0.013772 -0.180537 -0.835207 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.049596 -0.244270 -0.124344 0.000000 7.977538 -0.000000 -0.041590 0.048573 0.020090 -0.000001 13.487594 -0.000000 -0.013706 -0.041906 -0.363580 0.000000 4.943967 0.000000 0.018787 0.019032 -0.391814 -1.453320 11.981756 -7.617631 0.693154 -0.376236 0.187078 0.000000 12.910863 -0.000000 0.238790 0.105749 -0.610740 -0.031254 11.193578 0.010376 -0.358655 -0.018910 -0.103481 0.054595 0.018457 -0.015135 0.499102 -0.061087 -0.095060 0.000000 3.968525 0.000000 -0.015169 0.132885 0.462965 0.000000 5.348644 -0.000000 0.764540 0.092491 0.426199 0.000000 2.438549 0.000000 0.162814 0.273701 -0.294192 -0.655277 1.265096 -0.625148 0.107288 -0.965725 0.191044 1.136874 11.700286 7.871993 -0.962038 -0.067536 -0.053264 0.000000 13.379274 -0.000000 -0.438102 0.254333 -0.445155 0.028222 10.991364 0.020885 -0.013642 -0.459119 -0.073285 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.533428 3.620501 4.790100 0.025458 -0.003149 0.998822 0.000000 13.074334 0.000000 -0.008155 -0.085416 -0.094349 -0.035381 14.148866 0.000001 -0.000719 0.010197 0.508922 0.000000 4.097309 -0.000000 -0.000721 -0.003001 0.271158 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.596776 -0.031065 0.028766 0.997307 -0.000000 13.074365 0.000000 0.037892 0.058682 -0.051488 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503384 -0.000000 4.097290 0.000000 0.000153 0.000289 0.262612 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 98 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.816665 4.338816 10.271002 0.013445 -0.180552 -0.833487 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.049034 -0.248777 -0.122003 0.000000 7.977538 -0.000000 -0.040568 0.048468 0.020574 -0.000001 13.487594 -0.000000 -0.015281 -0.045443 -0.363476 0.000000 4.943967 0.000000 0.017083 0.017307 -0.392888 -1.455203 11.971984 -7.617236 0.697546 -0.373367 0.186024 0.000000 12.910863 -0.000000 0.239420 0.105004 -0.608094 -0.031254 11.193578 0.010376 -0.356653 -0.018783 -0.102808 0.054595 0.018457 -0.015135 0.497097 -0.061132 -0.096828 0.000000 3.968525 0.000000 -0.014775 0.132336 0.462230 0.000000 5.348644 -0.000000 0.763066 0.092325 0.424210 0.000000 2.438549 0.000000 0.165548 0.272682 -0.293003 -0.655277 1.265096 -0.625148 0.102740 -0.967410 0.183858 1.135887 11.695166 7.872201 -0.962386 -0.064401 -0.054185 0.000000 13.379274 -0.000000 -0.435772 0.252447 -0.444080 0.028222 10.991364 0.020885 -0.017844 -0.455665 -0.069418 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.507572 3.626137 4.830362 0.025504 -0.004030 0.998746 0.000000 13.074334 0.000000 -0.009217 -0.085341 -0.090469 -0.035381 14.148866 0.000001 -0.000716 0.010198 0.508897 0.000000 4.097309 -0.000000 -0.000672 -0.002850 0.269667 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.563042 -0.026151 0.027988 0.997469 -0.000000 13.074365 0.000000 0.036769 0.053884 -0.051433 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503389 -0.000000 4.097290 0.000000 0.000152 0.000287 0.262564 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 99 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.848249 4.285129 10.313747 0.013270 -0.179838 -0.832020 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.048456 -0.251768 -0.120103 0.000000 7.977538 -0.000000 -0.039345 0.048343 0.021156 -0.000001 13.487594 -0.000000 -0.016816 -0.048894 -0.363362 0.000000 4.943967 0.000000 0.015422 0.015625 -0.393933 -1.457182 11.961720 -7.616821 0.702105 -0.370358 0.184869 0.000000 12.910863 -0.000000 0.240097 0.104199 -0.605230 -0.031254 11.193578 0.010376 -0.354516 -0.018648 -0.102090 0.054595 0.018457 -0.015135 0.494955 -0.061179 -0.098713 0.000000 3.968525 0.000000 -0.014355 0.131752 0.461446 0.000000 5.348644 -0.000000 0.761468 0.092144 0.422071 0.000000 2.438549 0.000000 0.168463 0.271591 -0.291731 -0.655277 1.265096 -0.625148 0.096914 -0.968983 0.177308 1.134810 11.689577 7.872427 -0.963038 -0.062688 -0.055186 0.000000 13.379274 -0.000000 -0.430111 0.249670 -0.445862 0.028222 10.991364 0.020885 -0.021715 -0.449680 -0.065074 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.482127 3.631704 4.870009 0.025549 -0.004898 0.998668 0.000000 13.074334 0.000000 -0.010262 -0.085265 -0.086639 -0.035381 14.148866 0.000001 -0.000713 0.010200 0.508870 0.000000 4.097309 -0.000000 -0.000624 -0.002701 0.268194 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.529827 -0.021314 0.027222 0.997604 -0.000000 13.074365 0.000000 0.035663 0.049159 -0.051369 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503394 -0.000000 4.097290 0.000000 0.000151 0.000285 0.262511 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 100 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.877960 4.247571 10.364341 0.013249 -0.178106 -0.830862 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.047829 -0.253266 -0.118577 0.000000 7.977538 -0.000000 -0.037562 0.048160 0.022015 -0.000001 13.487594 -0.000000 -0.018310 -0.052252 -0.363239 0.000000 4.943967 0.000000 0.013807 0.013990 -0.394947 -1.459255 11.950964 -7.616386 0.706830 -0.367204 0.183608 0.000000 12.910863 -0.000000 0.240822 0.103330 -0.602137 -0.031254 11.193578 0.010376 -0.352240 -0.018504 -0.101326 0.054595 0.018457 -0.015135 0.492669 -0.061229 -0.100718 0.000000 3.968525 0.000000 -0.013908 0.131131 0.460612 0.000000 5.348644 -0.000000 0.759739 0.091949 0.419774 0.000000 2.438549 0.000000 0.171565 0.270426 -0.290373 -0.655277 1.265096 -0.625148 0.089698 -0.970486 0.171088 1.133640 11.683505 7.872672 -0.964113 -0.062825 -0.056336 0.000000 13.379274 -0.000000 -0.419626 0.245461 -0.451496 0.028222 10.991364 0.020885 -0.025257 -0.439857 -0.060075 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.457193 3.637184 4.908888 0.025592 -0.005748 0.998588 0.000000 13.074334 0.000000 -0.011288 -0.085190 -0.082872 -0.035381 14.148866 0.000001 -0.000710 0.010202 0.508841 0.000000 4.097309 -0.000000 -0.000577 -0.002555 0.266744 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.497259 -0.016571 0.026471 0.997713 -0.000000 13.074365 0.000000 0.034578 0.044527 -0.051297 -0.035380 14.148879 0.012529 0.000042 -0.008082 0.503400 -0.000000 4.097290 0.000000 0.000150 0.000282 0.262454 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 101 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.906067 4.225570 10.423315 0.013341 -0.175361 -0.829993 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.047124 -0.253462 -0.117313 0.000000 7.977538 -0.000000 -0.035020 0.047898 0.023248 -0.000001 13.487594 -0.000000 -0.019760 -0.055512 -0.363107 0.000000 4.943967 0.000000 0.012240 0.012405 -0.395927 -1.461423 11.939713 -7.615931 0.711724 -0.363900 0.182234 0.000000 12.910863 -0.000000 0.241597 0.102394 -0.598801 -0.031254 11.193578 0.010376 -0.349820 -0.018351 -0.100513 0.054595 0.018457 -0.015135 0.490233 -0.061281 -0.102849 0.000000 3.968525 0.000000 -0.013433 0.130471 0.459726 0.000000 5.348644 -0.000000 0.757867 0.091738 0.417308 0.000000 2.438549 0.000000 0.174862 0.269183 -0.288924 -0.655277 1.265096 -0.625148 0.081218 -0.971957 0.164793 1.132373 11.676934 7.872938 -0.965575 -0.064778 -0.057634 0.000000 13.379274 -0.000000 -0.404131 0.239638 -0.460804 0.028222 10.991364 0.020885 -0.028511 -0.425880 -0.054479 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.432870 3.642555 4.946842 0.025635 -0.006578 0.998507 0.000000 13.074334 0.000000 -0.012289 -0.085115 -0.079183 -0.035381 14.148866 0.000001 -0.000706 0.010203 0.508808 0.000000 4.097309 -0.000000 -0.000531 -0.002411 0.265324 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.465469 -0.011942 0.025737 0.997797 -0.000000 13.074365 0.000000 0.033520 0.040005 -0.051215 -0.035380 14.148879 0.012529 0.000042 -0.008082 0.503406 -0.000000 4.097290 0.000000 0.000148 0.000280 0.262393 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 102 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.932386 4.216469 10.487675 0.013520 -0.171853 -0.829359 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.046348 -0.252459 -0.116290 0.000000 7.977538 -0.000000 -0.031914 0.047579 0.024757 -0.000001 13.487594 -0.000000 -0.021163 -0.058669 -0.362965 0.000000 4.943967 0.000000 0.010726 0.010871 -0.396873 -1.463687 11.927967 -7.615456 0.716787 -0.360440 0.180742 0.000000 12.910863 -0.000000 0.242424 0.101388 -0.595210 -0.031254 11.193578 0.010376 -0.347248 -0.018189 -0.099649 0.054595 0.018457 -0.015135 0.487640 -0.061335 -0.105111 0.000000 3.968525 0.000000 -0.012930 0.129771 0.458785 0.000000 5.348644 -0.000000 0.755844 0.091509 0.414664 0.000000 2.438549 0.000000 0.178362 0.267859 -0.287381 -0.655277 1.265096 -0.625148 0.071560 -0.973348 0.158505 1.131008 11.669849 7.873225 -0.967268 -0.068178 -0.059015 0.000000 13.379274 -0.000000 -0.384682 0.232509 -0.472760 0.028222 10.991364 0.020885 -0.031467 -0.408631 -0.048471 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.409264 3.647796 4.983709 0.025677 -0.007384 0.998424 0.000000 13.074334 0.000000 -0.013261 -0.085042 -0.075588 -0.035381 14.148866 0.000001 -0.000702 0.010205 0.508772 0.000000 4.097309 -0.000000 -0.000486 -0.002271 0.263939 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.434593 -0.007447 0.025025 0.997857 -0.000000 13.074365 0.000000 0.032492 0.035614 -0.051123 -0.035380 14.148879 0.012529 0.000042 -0.008082 0.503412 -0.000000 4.097290 0.000000 0.000147 0.000277 0.262327 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 103 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.956721 4.218201 10.554905 0.013769 -0.167785 -0.828915 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.045506 -0.250355 -0.115491 0.000000 7.977538 -0.000000 -0.028400 0.047216 0.026461 -0.000001 13.487594 -0.000000 -0.022515 -0.061715 -0.362815 0.000000 4.943967 0.000000 0.009267 0.009394 -0.397782 -1.466047 11.915723 -7.614960 0.722019 -0.356819 0.179124 0.000000 12.910863 -0.000000 0.243304 0.100307 -0.591348 -0.031254 11.193578 0.010376 -0.344519 -0.018016 -0.098732 0.054595 0.018457 -0.015135 0.484883 -0.061392 -0.107508 0.000000 3.968525 0.000000 -0.012398 0.129030 0.457787 0.000000 5.348644 -0.000000 0.753655 0.091261 0.411830 0.000000 2.438549 0.000000 0.182072 0.266449 -0.285738 -0.655277 1.265096 -0.625148 0.060773 -0.974616 0.152294 1.129540 11.662234 7.873533 -0.969060 -0.072733 -0.060420 0.000000 13.379274 -0.000000 -0.362176 0.224339 -0.486500 0.028222 10.991364 0.020885 -0.034109 -0.388844 -0.042214 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.386484 3.652886 5.019323 0.025717 -0.008163 0.998341 0.000000 13.074334 0.000000 -0.014200 -0.084970 -0.072101 -0.035381 14.148866 0.000001 -0.000697 0.010208 0.508733 0.000000 4.097309 -0.000000 -0.000443 -0.002136 0.262596 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.404772 -0.003108 0.024336 0.997895 -0.000000 13.074365 0.000000 0.031499 0.031374 -0.051021 -0.035380 14.148879 0.012529 0.000043 -0.008082 0.503419 -0.000000 4.097290 0.000000 0.000145 0.000274 0.262256 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 104 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.978853 4.229102 10.622778 0.014070 -0.163333 -0.828628 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.044604 -0.247234 -0.114897 0.000000 7.977538 -0.000000 -0.024614 0.046825 0.028291 -0.000001 13.487594 -0.000000 -0.023815 -0.064645 -0.362656 0.000000 4.943967 0.000000 0.007866 0.007976 -0.398653 -1.468503 11.902980 -7.614445 0.727422 -0.353029 0.177373 0.000000 12.910863 -0.000000 0.244238 0.099148 -0.587199 -0.031254 11.193578 0.010376 -0.341624 -0.017833 -0.097761 0.054595 0.018457 -0.015135 0.481953 -0.061452 -0.110047 0.000000 3.968525 0.000000 -0.011834 0.128244 0.456730 0.000000 5.348644 -0.000000 0.751288 0.090993 0.408792 0.000000 2.438549 0.000000 0.186003 0.264948 -0.283989 -0.655277 1.265097 -0.625148 0.048883 -0.975719 0.146232 1.127967 11.654070 7.873863 -0.970842 -0.078200 -0.061796 0.000000 13.379274 -0.000000 -0.337446 0.215380 -0.501270 0.028222 10.991364 0.020885 -0.036415 -0.367178 -0.035861 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.364643 3.657801 5.053508 0.025755 -0.008909 0.998258 0.000000 13.074334 0.000000 -0.015101 -0.084900 -0.068740 -0.035381 14.148866 0.000001 -0.000692 0.010210 0.508691 0.000000 4.097309 -0.000000 -0.000401 -0.002005 0.261303 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.376153 0.001055 0.023675 0.997913 -0.000000 13.074365 0.000000 0.030547 0.027305 -0.050909 -0.035380 14.148879 0.012529 0.000043 -0.008082 0.503427 -0.000000 4.097290 0.000000 0.000143 0.000270 0.262180 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 105 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.998533 4.247787 10.689212 0.014410 -0.158653 -0.828472 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.043648 -0.243174 -0.114492 0.000000 7.977538 -0.000000 -0.020679 0.046418 0.030184 -0.000001 13.487594 -0.000000 -0.025059 -0.067450 -0.362489 0.000000 4.943967 0.000000 0.006527 0.006620 -0.399483 -1.471056 11.889733 -7.613909 0.732996 -0.349064 0.175481 0.000000 12.910863 -0.000000 0.245230 0.097906 -0.582743 -0.031254 11.193578 0.010376 -0.338556 -0.017639 -0.096731 0.054595 0.018457 -0.015135 0.478840 -0.061513 -0.112734 0.000000 3.968525 0.000000 -0.011237 0.127413 0.455611 0.000000 5.348644 -0.000000 0.748728 0.090703 0.405537 0.000000 2.438549 0.000000 0.190164 0.263351 -0.282130 -0.655277 1.265097 -0.625148 0.035894 -0.976613 0.140388 1.126284 11.645339 7.874216 -0.972523 -0.084366 -0.063095 0.000000 13.379274 -0.000000 -0.311311 0.205892 -0.516414 0.028222 10.991364 0.020885 -0.038359 -0.344262 -0.029559 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.343860 3.662517 5.086082 0.025792 -0.009620 0.998175 0.000000 13.074334 0.000000 -0.015960 -0.084833 -0.065521 -0.035381 14.148866 0.000001 -0.000687 0.010212 0.508645 0.000000 4.097309 -0.000000 -0.000360 -0.001880 0.260066 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.348888 0.005020 0.023046 0.997913 -0.000000 13.074365 0.000000 0.029641 0.023430 -0.050786 -0.035380 14.148879 0.012529 0.000044 -0.008082 0.503435 -0.000000 4.097290 0.000000 0.000141 0.000267 0.262100 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 106 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.015471 4.273069 10.752158 0.014776 -0.153893 -0.828424 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.042641 -0.238247 -0.114261 0.000000 7.977538 -0.000000 -0.016711 0.046006 0.032083 -0.000001 13.487594 -0.000000 -0.026243 -0.070124 -0.362315 0.000000 4.943967 0.000000 0.005253 0.005331 -0.400271 -1.473706 11.875978 -7.613353 0.738745 -0.344915 0.173438 0.000000 12.910863 -0.000000 0.246281 0.096575 -0.577961 -0.031254 11.193578 0.010376 -0.335305 -0.017434 -0.095640 0.054595 0.018457 -0.015135 0.475534 -0.061578 -0.115578 0.000000 3.968525 0.000000 -0.010606 0.126535 0.454427 0.000000 5.348644 -0.000000 0.745956 0.090388 0.402049 0.000000 2.438549 0.000000 0.194568 0.261653 -0.280154 -0.655277 1.265097 -0.625148 0.021793 -0.977254 0.134840 1.124488 11.636022 7.874593 -0.974031 -0.091041 -0.064268 0.000000 13.379274 -0.000000 -0.284610 0.196156 -0.531351 0.028222 10.991364 0.020885 -0.039909 -0.320726 -0.023461 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.324257 3.667010 5.116856 0.025827 -0.010291 0.998095 0.000000 13.074334 0.000000 -0.016772 -0.084769 -0.062461 -0.035381 14.148866 0.000001 -0.000681 0.010215 0.508596 0.000000 4.097309 -0.000000 -0.000322 -0.001762 0.258893 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.323136 0.008762 0.022451 0.997898 -0.000000 13.074365 0.000000 0.028786 0.019772 -0.050653 -0.035380 14.148879 0.012529 0.000044 -0.008081 0.503444 -0.000000 4.097290 0.000000 0.000139 0.000263 0.262013 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 107 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.029323 4.303892 10.809499 0.015156 -0.149202 -0.828470 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.041588 -0.232521 -0.114189 0.000000 7.977538 -0.000000 -0.012825 0.045603 0.033926 -0.000001 13.487594 -0.000000 -0.027364 -0.072659 -0.362133 0.000000 4.943967 0.000000 0.004048 0.004111 -0.401015 -1.476456 11.861712 -7.612776 0.744668 -0.340574 0.171236 0.000000 12.910863 -0.000000 0.247394 0.095149 -0.572829 -0.031254 11.193578 0.010376 -0.331861 -0.017216 -0.094484 0.054595 0.018457 -0.015135 0.472023 -0.061644 -0.118585 0.000000 3.968525 0.000000 -0.009940 0.125606 0.453174 0.000000 5.348644 -0.000000 0.742955 0.090047 0.398310 0.000000 2.438549 0.000000 0.199226 0.259847 -0.278052 -0.655277 1.265097 -0.625148 0.006545 -0.977593 0.129672 1.122575 11.626097 7.874994 -0.975312 -0.098044 -0.065267 0.000000 13.379274 -0.000000 -0.258217 0.186480 -0.545579 0.028222 10.991364 0.020885 -0.041033 -0.297216 -0.017724 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.305966 3.671253 5.145629 0.025859 -0.010918 0.998017 0.000000 13.074334 0.000000 -0.017531 -0.084708 -0.059579 -0.035381 14.148866 0.000001 -0.000675 0.010218 0.508542 0.000000 4.097309 -0.000000 -0.000287 -0.001651 0.257792 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.299066 0.012259 0.021896 0.997870 -0.000000 13.074365 0.000000 0.027987 0.016353 -0.050508 -0.035380 14.148879 0.012529 0.000045 -0.008081 0.503453 -0.000000 4.097290 0.000000 0.000137 0.000259 0.261922 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 108 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.039669 4.339282 10.858936 0.015537 -0.144732 -0.828595 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.040493 -0.226057 -0.114264 0.000000 7.977538 -0.000000 -0.009141 0.045219 0.035653 -0.000001 13.487594 -0.000000 -0.028418 -0.075046 -0.361944 0.000000 4.943967 0.000000 0.002916 0.002965 -0.401713 -1.479305 11.846930 -7.612178 0.750769 -0.336031 0.168862 0.000000 12.910863 -0.000000 0.248571 0.093621 -0.567322 -0.031254 11.193578 0.010376 -0.328214 -0.016986 -0.093261 0.054595 0.018457 -0.015135 0.468295 -0.061713 -0.121765 0.000000 3.968525 0.000000 -0.009236 0.124624 0.451848 0.000000 5.348644 -0.000000 0.739702 0.089676 0.394300 0.000000 2.438549 0.000000 0.204153 0.257927 -0.275818 -0.655277 1.265097 -0.625148 -0.009905 -0.977573 0.124982 1.120541 11.615542 7.875421 -0.976328 -0.105200 -0.066039 0.000000 13.379274 -0.000000 -0.233055 0.177199 -0.558654 0.028222 10.991364 0.020885 -0.041692 -0.274423 -0.012517 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.289122 3.675219 5.172192 0.025890 -0.011496 0.997943 0.000000 13.074334 0.000000 -0.018233 -0.084652 -0.056895 -0.035381 14.148866 0.000001 -0.000668 0.010221 0.508484 0.000000 4.097309 -0.000000 -0.000254 -0.001548 0.256771 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.276854 0.015484 0.021383 0.997833 -0.000000 13.074365 0.000000 0.027252 0.013200 -0.050351 -0.035380 14.148879 0.012529 0.000046 -0.008081 0.503463 -0.000000 4.097290 0.000000 0.000135 0.000254 0.261825 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 109 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.045991 4.378297 10.897860 0.015906 -0.140653 -0.828793 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.039361 -0.218916 -0.114471 0.000000 7.977538 -0.000000 -0.005791 0.044871 0.037194 -0.000001 13.487594 -0.000000 -0.029401 -0.077275 -0.361748 0.000000 4.943967 0.000000 0.001861 0.001897 -0.402362 -1.482254 11.831626 -7.611559 0.757048 -0.331276 0.166306 0.000000 12.910863 -0.000000 0.249814 0.091985 -0.561410 -0.031254 11.193578 0.010376 -0.324349 -0.016742 -0.091965 0.054595 0.018457 -0.015135 0.464336 -0.061784 -0.125127 0.000000 3.968525 0.000000 -0.008491 0.123585 0.450445 0.000000 5.348644 -0.000000 0.736172 0.089274 0.389997 0.000000 2.438549 0.000000 0.209363 0.255883 -0.273442 -0.655277 1.265097 -0.625148 -0.027634 -0.977129 0.120885 1.118381 11.604335 7.875874 -0.977050 -0.112327 -0.066524 0.000000 13.379274 -0.000000 -0.210119 0.168689 -0.570185 0.028222 10.991364 0.020885 -0.041840 -0.253098 -0.008027 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.273869 3.678879 5.196323 0.025917 -0.012021 0.997873 0.000000 13.074334 0.000000 -0.018870 -0.084601 -0.054429 -0.035381 14.148866 0.000001 -0.000661 0.010225 0.508422 0.000000 4.097309 -0.000000 -0.000224 -0.001454 0.255839 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.256685 0.018411 0.020918 0.997789 -0.000000 13.074365 0.000000 0.026586 0.010338 -0.050183 -0.035380 14.148879 0.012529 0.000046 -0.008081 0.503473 -0.000000 4.097290 0.000000 0.000133 0.000250 0.261722 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 110 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.047629 4.419982 10.923168 0.016249 -0.137158 -0.829056 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.038194 -0.211153 -0.114799 0.000000 7.977538 -0.000000 -0.002929 0.044573 0.038472 -0.000001 13.487594 -0.000000 -0.030309 -0.079338 -0.361545 0.000000 4.943967 0.000000 0.000886 0.000910 -0.402961 -1.485306 11.815794 -7.610919 0.763508 -0.326296 0.163554 0.000000 12.910863 -0.000000 0.251126 0.090233 -0.555062 -0.031254 11.193578 0.010376 -0.320254 -0.016484 -0.090592 0.054595 0.018457 -0.015135 0.460128 -0.061857 -0.128684 0.000000 3.968525 0.000000 -0.007705 0.122487 0.448960 0.000000 5.348644 -0.000000 0.732338 0.088836 0.385375 0.000000 2.438549 0.000000 0.214875 0.253708 -0.270913 -0.655277 1.265097 -0.625148 -0.046752 -0.976185 0.117524 1.116091 11.592450 7.876355 -0.977463 -0.119228 -0.066652 0.000000 13.379274 -0.000000 -0.190512 0.161372 -0.579795 0.028222 10.991364 0.020885 -0.041422 -0.234087 -0.004468 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.260361 3.682202 5.217785 0.025942 -0.012487 0.997808 0.000000 13.074334 0.000000 -0.019438 -0.084555 -0.052202 -0.035381 14.148866 0.000001 -0.000653 0.010229 0.508356 0.000000 4.097309 -0.000000 -0.000197 -0.001370 0.255006 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.238759 0.021012 0.020505 0.997741 -0.000000 13.074365 0.000000 0.025995 0.007795 -0.050002 -0.035380 14.148879 0.012529 0.000047 -0.008081 0.503484 -0.000000 4.097290 0.000000 0.000130 0.000245 0.261613 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 111 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.043724 4.463304 10.931006 0.016548 -0.134483 -0.829381 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.036996 -0.202825 -0.115237 0.000000 7.977538 -0.000000 -0.000741 0.044345 0.039390 -0.000001 13.487594 -0.000000 -0.031136 -0.081223 -0.361336 0.000000 4.943967 0.000000 -0.000004 0.000009 -0.403507 -1.488460 11.799426 -7.610257 0.770152 -0.321080 0.160591 0.000000 12.910863 -0.000000 0.252511 0.088354 -0.548240 -0.031254 11.193578 0.010376 -0.315912 -0.016210 -0.089136 0.054595 0.018457 -0.015135 0.455654 -0.061933 -0.132446 0.000000 3.968525 0.000000 -0.006873 0.121325 0.447387 0.000000 5.348644 -0.000000 0.728167 0.088360 0.380407 0.000000 2.438549 0.000000 0.220708 0.251391 -0.268221 -0.655277 1.265097 -0.625148 -0.067409 -0.974642 0.115081 1.113665 11.579861 7.876864 -0.977553 -0.125677 -0.066337 0.000000 13.379274 -0.000000 -0.175518 0.155747 -0.587075 0.028222 10.991364 0.020885 -0.040363 -0.218390 -0.002099 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.248761 3.685155 5.236329 0.025964 -0.012888 0.997750 0.000000 13.074334 0.000000 -0.019929 -0.084516 -0.050238 -0.035381 14.148866 0.000001 -0.000644 0.010233 0.508284 0.000000 4.097309 -0.000000 -0.000173 -0.001297 0.254282 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.223285 0.023255 0.020149 0.997694 -0.000000 13.074365 0.000000 0.025488 0.005601 -0.049807 -0.035380 14.148879 0.012529 0.000048 -0.008081 0.503496 -0.000000 4.097290 0.000000 0.000127 0.000240 0.261497 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 112 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.035101 4.508420 10.924330 0.016813 -0.132473 -0.829768 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.035772 -0.193984 -0.115773 0.000000 7.977538 -0.000000 0.000901 0.044175 0.040013 -0.000001 13.487594 -0.000000 -0.031856 -0.082875 -0.361108 0.000000 4.943967 0.000000 -0.000783 -0.000780 -0.403984 -1.491958 11.781278 -7.609523 0.777460 -0.315224 0.157177 0.000000 12.910863 -0.000000 0.254023 0.086265 -0.540637 -0.031254 11.193578 0.010376 -0.311136 -0.015909 -0.087536 0.054595 0.018457 -0.015135 0.450718 -0.062013 -0.136575 0.000000 3.968525 0.000000 -0.005960 0.120051 0.445659 0.000000 5.348644 -0.000000 0.723453 0.087820 0.374860 0.000000 2.438549 0.000000 0.227109 0.248829 -0.265246 -0.655277 1.265097 -0.625148 -0.091714 -0.972036 0.114684 1.110998 11.566025 7.877424 -0.977381 -0.131837 -0.065516 0.000000 13.379274 -0.000000 -0.164273 0.151515 -0.592593 0.028222 10.991364 0.020885 -0.038049 -0.207634 -0.001436 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.239017 3.687765 5.252054 0.025982 -0.013227 0.997699 0.000000 13.074334 0.000000 -0.020347 -0.084483 -0.048520 -0.035381 14.148866 0.000001 -0.000635 0.010237 0.508206 0.000000 4.097309 -0.000000 -0.000153 -0.001235 0.253666 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.210181 0.025153 0.019848 0.997648 -0.000000 13.074365 0.000000 0.025061 0.003744 -0.049594 -0.035380 14.148879 0.012529 0.000048 -0.008080 0.503508 -0.000000 4.097290 0.000000 0.000124 0.000234 0.261367 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 113 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.023496 4.556073 10.909729 0.017069 -0.130767 -0.830218 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.034525 -0.184683 -0.116397 0.000000 7.977538 -0.000000 0.002285 0.044032 0.040496 -0.000001 13.487594 -0.000000 -0.032456 -0.084266 -0.360852 0.000000 4.943967 0.000000 -0.001438 -0.001443 -0.404385 -1.495987 11.760370 -7.608678 0.785786 -0.308394 0.153126 0.000000 12.910863 -0.000000 0.255701 0.083902 -0.532014 -0.031254 11.193578 0.010376 -0.305785 -0.015571 -0.085743 0.054595 0.018457 -0.015135 0.445169 -0.062099 -0.141190 0.000000 3.968525 0.000000 -0.004941 0.118626 0.443726 0.000000 5.348644 -0.000000 0.718013 0.087197 0.368546 0.000000 2.438549 0.000000 0.234263 0.245944 -0.261897 -0.655277 1.265097 -0.625148 -0.119626 -0.968120 0.116181 1.108012 11.550530 7.878051 -0.977038 -0.138032 -0.064257 0.000000 13.379274 -0.000000 -0.154789 0.147949 -0.597318 0.028222 10.991364 0.020885 -0.034469 -0.201074 -0.002422 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.230854 3.690102 5.265396 0.025999 -0.013513 0.997652 0.000000 13.074334 0.000000 -0.020703 -0.084456 -0.047002 -0.035381 14.148866 0.000001 -0.000625 0.010242 0.508119 0.000000 4.097309 -0.000000 -0.000136 -0.001182 0.253142 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.199085 0.026760 0.019594 0.997604 -0.000000 13.074365 0.000000 0.024702 0.002172 -0.049357 -0.035380 14.148879 0.012529 0.000049 -0.008080 0.503523 -0.000000 4.097290 0.000000 0.000121 0.000227 0.261214 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 114 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.009186 4.606026 10.887940 0.017316 -0.129336 -0.830726 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.033259 -0.174974 -0.117098 0.000000 7.977538 -0.000000 0.003435 0.043914 0.040850 -0.000001 13.487594 -0.000000 -0.032946 -0.085420 -0.360572 0.000000 4.943967 0.000000 -0.001980 -0.001991 -0.404716 -1.500479 11.737062 -7.607735 0.794947 -0.300671 0.148494 0.000000 12.910863 -0.000000 0.257518 0.081283 -0.522426 -0.031254 11.193578 0.010376 -0.299904 -0.015201 -0.083774 0.054595 0.018457 -0.015135 0.439049 -0.062190 -0.146246 0.000000 3.968525 0.000000 -0.003825 0.117064 0.441605 0.000000 5.348644 -0.000000 0.711845 0.086489 0.361488 0.000000 2.438549 0.000000 0.242102 0.242754 -0.258196 -0.655277 1.265097 -0.625148 -0.148752 -0.963157 0.118247 1.104736 11.533532 7.878738 -0.976543 -0.144250 -0.062635 0.000000 13.379274 -0.000000 -0.146900 0.144992 -0.601360 0.028222 10.991364 0.020885 -0.030224 -0.196931 -0.004695 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.224125 3.692191 5.276578 0.026013 -0.013752 0.997611 0.000000 13.074334 0.000000 -0.021003 -0.084434 -0.045666 -0.035381 14.148866 0.000001 -0.000614 0.010247 0.508026 0.000000 4.097309 -0.000000 -0.000122 -0.001138 0.252702 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.189808 0.028102 0.019383 0.997564 -0.000000 13.074365 0.000000 0.024406 0.000859 -0.049098 -0.035380 14.148879 0.012529 0.000050 -0.008080 0.503538 -0.000000 4.097290 0.000000 0.000117 0.000220 0.261042 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 115 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.992418 4.658068 10.859621 0.017555 -0.128155 -0.831286 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.031978 -0.164909 -0.117867 0.000000 7.977538 -0.000000 0.004374 0.043818 0.041088 -0.000001 13.487594 -0.000000 -0.033339 -0.086360 -0.360272 0.000000 4.943967 0.000000 -0.002419 -0.002436 -0.404984 -1.505371 11.711678 -7.606709 0.804772 -0.292131 0.143333 0.000000 12.910863 -0.000000 0.259454 0.078423 -0.511921 -0.031254 11.193578 0.010376 -0.293536 -0.014800 -0.081642 0.054595 0.018457 -0.015135 0.432397 -0.062283 -0.151703 0.000000 3.968525 0.000000 -0.002620 0.115377 0.439311 0.000000 5.348644 -0.000000 0.704940 0.085695 0.353706 0.000000 2.438549 0.000000 0.250563 0.239279 -0.254166 -0.655277 1.265097 -0.625148 -0.177075 -0.957588 0.119788 1.101196 11.515167 7.879481 -0.975910 -0.150476 -0.060719 0.000000 13.379274 -0.000000 -0.140455 0.142589 -0.604809 0.028222 10.991364 0.020885 -0.025784 -0.193836 -0.008044 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.218700 3.694055 5.285797 0.026025 -0.013947 0.997574 0.000000 13.074334 0.000000 -0.021252 -0.084416 -0.044494 -0.035381 14.148866 0.000001 -0.000603 0.010252 0.507926 0.000000 4.097309 -0.000000 -0.000110 -0.001101 0.252337 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.182185 0.029204 0.019210 0.997527 -0.000000 13.074365 0.000000 0.024166 -0.000219 -0.048821 -0.035380 14.148879 0.012529 0.000051 -0.008080 0.503555 -0.000000 4.097290 0.000000 0.000112 0.000212 0.260853 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 116 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.973416 4.712011 10.825363 0.017787 -0.127202 -0.831895 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.030685 -0.154539 -0.118694 0.000000 7.977538 -0.000000 0.005119 0.043743 0.041220 -0.000001 13.487594 -0.000000 -0.033642 -0.087106 -0.359954 0.000000 4.943967 0.000000 -0.002766 -0.002788 -0.405196 -1.510607 11.684514 -7.605610 0.815098 -0.282847 0.137693 0.000000 12.910863 -0.000000 0.261487 0.075337 -0.500544 -0.031254 11.193578 0.010376 -0.286718 -0.014371 -0.079361 0.054595 0.018457 -0.015135 0.425247 -0.062378 -0.157524 0.000000 3.968525 0.000000 -0.001335 0.113577 0.436858 0.000000 5.348644 -0.000000 0.697289 0.084814 0.345218 0.000000 2.438549 0.000000 0.259587 0.235535 -0.249827 -0.655277 1.265097 -0.625148 -0.202566 -0.952106 0.119703 1.097417 11.495559 7.880274 -0.975151 -0.156698 -0.058570 0.000000 13.379274 -0.000000 -0.135317 0.140689 -0.607740 0.028222 10.991364 0.020885 -0.021606 -0.190509 -0.012349 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.214460 3.695714 5.293230 0.026035 -0.014102 0.997541 0.000000 13.074334 0.000000 -0.021455 -0.084403 -0.043473 -0.035381 14.148866 0.000001 -0.000590 0.010258 0.507820 0.000000 4.097309 -0.000000 -0.000101 -0.001071 0.252041 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.176066 0.030088 0.019072 0.997494 -0.000000 13.074365 0.000000 0.023978 -0.001084 -0.048526 -0.035380 14.148879 0.012529 0.000052 -0.008079 0.503573 -0.000000 4.097290 0.000000 0.000107 0.000203 0.260650 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 117 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.952380 4.767683 10.785702 0.018012 -0.126456 -0.832547 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.029386 -0.143916 -0.119570 0.000000 7.977538 -0.000000 0.005690 0.043686 0.041256 -0.000001 13.487594 -0.000000 -0.033864 -0.087677 -0.359622 0.000000 4.943967 0.000000 -0.003029 -0.003054 -0.405357 -1.516132 11.655843 -7.604451 0.825774 -0.272888 0.131624 0.000000 12.910863 -0.000000 0.263595 0.072038 -0.488336 -0.031254 11.193578 0.010376 -0.279484 -0.013915 -0.076941 0.054595 0.018457 -0.015135 0.417630 -0.062472 -0.163675 0.000000 3.968525 0.000000 0.000023 0.111672 0.434260 0.000000 5.348644 -0.000000 0.688881 0.083844 0.336041 0.000000 2.438549 0.000000 0.269121 0.231536 -0.245195 -0.655277 1.265097 -0.625148 -0.222760 -0.947762 0.116628 1.093420 11.474820 7.881113 -0.974275 -0.162906 -0.056245 0.000000 13.379274 -0.000000 -0.131364 0.139249 -0.610215 0.028222 10.991364 0.020885 -0.018230 -0.185470 -0.017559 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.211299 3.697186 5.299039 0.026044 -0.014221 0.997512 0.000000 13.074334 0.000000 -0.021615 -0.084393 -0.042588 -0.035381 14.148866 0.000001 -0.000577 0.010265 0.507709 0.000000 4.097309 -0.000000 -0.000093 -0.001048 0.251807 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.171314 0.030773 0.018965 0.997466 -0.000000 13.074365 0.000000 0.023836 -0.001754 -0.048215 -0.035380 14.148879 0.012529 0.000054 -0.008079 0.503592 -0.000000 4.097290 0.000000 0.000102 0.000193 0.260434 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 118 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.929495 4.824928 10.741127 0.018231 -0.125898 -0.833239 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.028084 -0.133092 -0.120487 0.000000 7.977538 -0.000000 0.006100 0.043646 0.041204 -0.000001 13.487594 -0.000000 -0.034014 -0.088088 -0.359278 0.000000 4.943967 0.000000 -0.003217 -0.003244 -0.405471 -1.521900 11.625916 -7.603240 0.836656 -0.262327 0.125174 0.000000 12.910863 -0.000000 0.265761 0.068538 -0.475334 -0.031254 11.193578 0.010376 -0.271867 -0.013436 -0.074395 0.054595 0.018457 -0.015135 0.409576 -0.062563 -0.170124 0.000000 3.968525 0.000000 0.001448 0.109672 0.431527 0.000000 5.348644 -0.000000 0.679707 0.082784 0.326190 0.000000 2.438549 0.000000 0.279115 0.227296 -0.240288 -0.655277 1.265097 -0.625148 -0.235726 -0.945540 0.110096 1.089225 11.453054 7.881993 -0.973289 -0.169087 -0.053795 0.000000 13.379274 -0.000000 -0.128486 0.138226 -0.612289 0.028222 10.991364 0.020885 -0.015654 -0.178326 -0.023440 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.209119 3.698489 5.303371 0.026051 -0.014307 0.997487 0.000000 13.074334 0.000000 -0.021738 -0.084388 -0.041827 -0.035381 14.148866 0.000001 -0.000564 0.010271 0.507593 0.000000 4.097309 -0.000000 -0.000087 -0.001030 0.251630 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.167807 0.031278 0.018888 0.997441 -0.000000 13.074365 0.000000 0.023737 -0.002248 -0.047889 -0.035380 14.148879 0.012529 0.000055 -0.008079 0.503612 -0.000000 4.097290 0.000000 0.000097 0.000183 0.260209 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 119 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.904930 4.883601 10.692087 0.018444 -0.125512 -0.833968 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.026784 -0.122119 -0.121436 0.000000 7.977538 -0.000000 0.006366 0.043621 0.041073 -0.000001 13.487594 -0.000000 -0.034098 -0.088356 -0.358925 0.000000 4.943967 0.000000 -0.003335 -0.003364 -0.405543 -1.527864 11.594972 -7.601989 0.847608 -0.251240 0.118393 0.000000 12.910863 -0.000000 0.267964 0.064850 -0.461578 -0.031254 11.193578 0.010376 -0.263896 -0.012935 -0.071731 0.054595 0.018457 -0.015135 0.401111 -0.062652 -0.176841 0.000000 3.968525 0.000000 0.002933 0.107585 0.428671 0.000000 5.348644 -0.000000 0.669759 0.081633 0.315687 0.000000 2.438549 0.000000 0.289521 0.222829 -0.235122 -0.655277 1.265097 -0.625148 -0.243664 -0.944913 0.101155 1.084850 11.430353 7.882911 -0.972202 -0.175230 -0.051270 0.000000 13.379274 -0.000000 -0.126581 0.137583 -0.614006 0.028222 10.991364 0.020885 -0.013373 -0.170117 -0.029729 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.207829 3.699638 5.306361 0.026056 -0.014364 0.997466 0.000000 13.074334 0.000000 -0.021826 -0.084385 -0.041178 -0.035381 14.148866 0.000001 -0.000549 0.010278 0.507472 0.000000 4.097309 -0.000000 -0.000083 -0.001018 0.251505 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.165430 0.031618 0.018837 0.997421 -0.000000 13.074365 0.000000 0.023677 -0.002582 -0.047550 -0.035380 14.148879 0.012529 0.000056 -0.008078 0.503633 -0.000000 4.097290 0.000000 0.000092 0.000173 0.259974 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 120 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.878842 4.943569 10.639000 0.018653 -0.125280 -0.834729 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.025489 -0.111049 -0.122409 0.000000 7.977538 -0.000000 0.006501 0.043610 0.040870 -0.000001 13.487594 -0.000000 -0.034123 -0.088494 -0.358565 0.000000 4.943967 0.000000 -0.003392 -0.003422 -0.405578 -1.533980 11.563234 -7.600706 0.858506 -0.239703 0.111333 0.000000 12.910863 -0.000000 0.270188 0.060987 -0.447106 -0.031254 11.193578 0.010376 -0.255599 -0.012414 -0.068960 0.054595 0.018457 -0.015135 0.392263 -0.062735 -0.183798 0.000000 3.968525 0.000000 0.004474 0.105419 0.425701 0.000000 5.348644 -0.000000 0.659033 0.080390 0.304550 0.000000 2.438549 0.000000 0.300292 0.218148 -0.229711 -0.655277 1.265097 -0.625148 -0.248787 -0.945160 0.090045 1.080312 11.406805 7.883864 -0.971020 -0.181324 -0.048716 0.000000 13.379274 -0.000000 -0.125558 0.137284 -0.615406 0.028222 10.991364 0.020885 -0.011346 -0.161088 -0.036397 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.207347 3.700647 5.308134 0.026061 -0.014394 0.997447 0.000000 13.074334 0.000000 -0.021882 -0.084385 -0.040632 -0.035381 14.148866 0.000001 -0.000535 0.010285 0.507348 0.000000 4.097309 -0.000000 -0.000081 -0.001010 0.251425 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.164078 0.031810 0.018810 0.997404 -0.000000 13.074365 0.000000 0.023651 -0.002770 -0.047200 -0.035380 14.148879 0.012529 0.000058 -0.008078 0.503654 -0.000000 4.097290 0.000000 0.000086 0.000162 0.259734 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 121 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.851377 5.004709 10.582254 0.018857 -0.125189 -0.835518 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.024204 -0.099935 -0.123398 0.000000 7.977538 -0.000000 0.006516 0.043612 0.040602 -0.000001 13.487594 -0.000000 -0.034095 -0.088516 -0.358201 0.000000 4.943967 0.000000 -0.003394 -0.003423 -0.405579 -1.540209 11.530917 -7.599399 0.869233 -0.227797 0.104047 0.000000 12.910863 -0.000000 0.272414 0.056960 -0.431958 -0.031254 11.193578 0.010376 -0.247003 -0.011875 -0.066090 0.054595 0.018457 -0.015135 0.383058 -0.062813 -0.190967 0.000000 3.968525 0.000000 0.006064 0.103181 0.422627 0.000000 5.348644 -0.000000 0.647531 0.079055 0.292804 0.000000 2.438549 0.000000 0.311386 0.213265 -0.224071 -0.655277 1.265097 -0.625148 -0.252795 -0.945653 0.076792 1.075626 11.382491 7.884847 -0.969751 -0.187358 -0.046179 0.000000 13.379274 -0.000000 -0.125331 0.137295 -0.616521 0.028222 10.991364 0.020885 -0.009535 -0.151453 -0.043419 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.207596 3.701530 5.308808 0.026064 -0.014399 0.997431 0.000000 13.074334 0.000000 -0.021910 -0.084388 -0.040178 -0.035381 14.148866 0.000001 -0.000520 0.010292 0.507219 0.000000 4.097309 -0.000000 -0.000080 -0.001006 0.251388 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.163653 0.031868 0.018805 0.997390 -0.000000 13.074365 0.000000 0.023657 -0.002827 -0.046838 -0.035380 14.148879 0.012529 0.000059 -0.008078 0.503676 -0.000000 4.097290 0.000000 0.000080 0.000151 0.259488 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 122 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.822673 5.066902 10.522214 0.019057 -0.125224 -0.836334 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.022935 -0.088831 -0.124396 0.000000 7.977538 -0.000000 0.006425 0.043625 0.040275 -0.000001 13.487594 -0.000000 -0.034020 -0.088435 -0.357834 0.000000 4.943967 0.000000 -0.003346 -0.003375 -0.405550 -1.546509 11.498225 -7.598077 0.879686 -0.215607 0.096588 0.000000 12.910863 -0.000000 0.274625 0.052784 -0.416174 -0.031254 11.193578 0.010376 -0.238134 -0.011318 -0.063131 0.054595 0.018457 -0.015135 0.373521 -0.062884 -0.198322 0.000000 3.968525 0.000000 0.007698 0.100878 0.419458 0.000000 5.348644 -0.000000 0.635258 0.077628 0.280478 0.000000 2.438549 0.000000 0.322758 0.208193 -0.218217 -0.655277 1.265097 -0.625148 -0.257293 -0.945746 0.061238 1.070808 11.357488 7.885858 -0.968401 -0.193321 -0.043705 0.000000 13.379274 -0.000000 -0.125820 0.137587 -0.617383 0.028222 10.991364 0.020885 -0.007912 -0.141406 -0.050776 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.208502 3.702300 5.308491 0.026066 -0.014383 0.997418 0.000000 13.074334 0.000000 -0.021912 -0.084394 -0.039807 -0.035381 14.148866 0.000001 -0.000504 0.010299 0.507088 0.000000 4.097309 -0.000000 -0.000080 -0.001006 0.251388 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.164062 0.031805 0.018819 0.997380 -0.000000 13.074365 0.000000 0.023692 -0.002766 -0.046467 -0.035380 14.148879 0.012529 0.000060 -0.008077 0.503699 -0.000000 4.097290 0.000000 0.000074 0.000140 0.259239 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 123 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.792858 5.130040 10.459224 0.019253 -0.125371 -0.837172 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.021685 -0.077791 -0.125395 0.000000 7.977538 -0.000000 0.006238 0.043648 0.039895 -0.000001 13.487594 -0.000000 -0.033904 -0.088261 -0.357466 0.000000 4.943967 0.000000 -0.003255 -0.003282 -0.405494 -1.552844 11.465359 -7.596747 0.889772 -0.203221 0.089013 0.000000 12.910863 -0.000000 0.276807 0.048470 -0.399800 -0.031254 11.193578 0.010376 -0.229016 -0.010746 -0.060090 0.054595 0.018457 -0.015135 0.363676 -0.062947 -0.205837 0.000000 3.968525 0.000000 0.009373 0.098516 0.416201 0.000000 5.348644 -0.000000 0.622228 0.076111 0.267604 0.000000 2.438549 0.000000 0.334367 0.202946 -0.212165 -0.655277 1.265097 -0.625148 -0.264153 -0.944634 0.042993 1.065870 11.331868 7.886895 -0.966979 -0.199202 -0.041337 0.000000 13.379274 -0.000000 -0.126951 0.138130 -0.618017 0.028222 10.991364 0.020885 -0.006450 -0.131128 -0.058452 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.209997 3.702967 5.307287 0.026068 -0.014347 0.997407 0.000000 13.074334 0.000000 -0.021891 -0.084402 -0.039511 -0.035381 14.148866 0.000001 -0.000489 0.010307 0.506953 0.000000 4.097309 -0.000000 -0.000081 -0.001009 0.251421 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.165217 0.031634 0.018850 0.997372 -0.000000 13.074365 0.000000 0.023752 -0.002598 -0.046087 -0.035380 14.148879 0.012529 0.000062 -0.008077 0.503723 -0.000000 4.097290 0.000000 0.000069 0.000129 0.258988 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 124 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.762058 5.194015 10.393614 0.019447 -0.125619 -0.838030 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.020460 -0.066869 -0.126389 0.000000 7.977538 -0.000000 0.005966 0.043680 0.039469 -0.000001 13.487594 -0.000000 -0.033751 -0.088007 -0.357100 0.000000 4.943967 0.000000 -0.003125 -0.003151 -0.405415 -1.559174 11.432514 -7.595419 0.899411 -0.190729 0.081379 0.000000 12.910863 -0.000000 0.278943 0.044034 -0.382880 -0.031254 11.193578 0.010376 -0.219675 -0.010161 -0.056977 0.054595 0.018457 -0.015135 0.353551 -0.063000 -0.213487 0.000000 3.968525 0.000000 0.011083 0.096101 0.412866 0.000000 5.348644 -0.000000 0.608460 0.074505 0.254220 0.000000 2.438549 0.000000 0.346172 0.197537 -0.205931 -0.655277 1.265097 -0.625148 -0.275115 -0.941437 0.020971 1.060826 11.305700 7.887953 -0.965493 -0.204988 -0.039122 0.000000 13.379274 -0.000000 -0.128652 0.138896 -0.618447 0.028222 10.991364 0.020885 -0.005128 -0.120788 -0.066434 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.212014 3.703544 5.305296 0.026069 -0.014293 0.997398 0.000000 13.074334 0.000000 -0.021850 -0.084411 -0.039280 -0.035381 14.148866 0.000001 -0.000473 0.010314 0.506816 0.000000 4.097309 -0.000000 -0.000083 -0.001015 0.251483 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.167036 0.031366 0.018897 0.997367 -0.000000 13.074365 0.000000 0.023834 -0.002337 -0.045700 -0.035380 14.148879 0.012529 0.000063 -0.008076 0.503747 -0.000000 4.097290 0.000000 0.000063 0.000118 0.258737 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 125 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.730391 5.258728 10.325701 0.019637 -0.125954 -0.838904 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.019264 -0.056121 -0.127371 0.000000 7.977538 -0.000000 0.005620 0.043720 0.039002 -0.000001 13.487594 -0.000000 -0.033567 -0.087683 -0.356738 0.000000 4.943967 0.000000 -0.002962 -0.002986 -0.405315 -1.565462 11.399883 -7.594100 0.908536 -0.178225 0.073744 0.000000 12.910863 -0.000000 0.281020 0.039488 -0.365464 -0.031254 11.193578 0.010376 -0.210135 -0.009564 -0.053798 0.054595 0.018457 -0.015135 0.343171 -0.063044 -0.221249 0.000000 3.968525 0.000000 0.012825 0.093640 0.409459 0.000000 5.348644 -0.000000 0.593982 0.072815 0.240369 0.000000 2.438549 0.000000 0.358132 0.191979 -0.199529 -0.655277 1.265097 -0.625148 -0.288457 -0.936413 -0.004299 1.055690 11.279049 7.889031 -0.963953 -0.210669 -0.037105 0.000000 13.379274 -0.000000 -0.130857 0.139858 -0.618697 0.028222 10.991364 0.020885 -0.003925 -0.110554 -0.074713 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.214491 3.704042 5.302613 0.026069 -0.014224 0.997391 0.000000 13.074334 0.000000 -0.021791 -0.084423 -0.039108 -0.035381 14.148866 0.000001 -0.000456 0.010322 0.506677 0.000000 4.097309 -0.000000 -0.000085 -0.001024 0.251571 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.169438 0.031014 0.018957 0.997364 -0.000000 13.074365 0.000000 0.023937 -0.001993 -0.045305 -0.035380 14.148879 0.012529 0.000065 -0.008076 0.503771 -0.000000 4.097290 0.000000 0.000057 0.000107 0.258488 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 126 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.697972 5.324081 10.255789 0.019825 -0.126366 -0.839792 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.018102 -0.045601 -0.128334 0.000000 7.977538 -0.000000 0.005208 0.043767 0.038499 -0.000001 13.487594 -0.000000 -0.033357 -0.087299 -0.356380 0.000000 4.943967 0.000000 -0.002771 -0.002792 -0.405199 -1.571673 11.367659 -7.592796 0.917094 -0.165802 0.066167 0.000000 12.910863 -0.000000 0.283025 0.034847 -0.347603 -0.031254 11.193578 0.010376 -0.200419 -0.008956 -0.050564 0.054595 0.018457 -0.015135 0.332561 -0.063078 -0.229097 0.000000 3.968525 0.000000 0.014593 0.091138 0.405990 0.000000 5.348644 -0.000000 0.578831 0.071043 0.226099 0.000000 2.438549 0.000000 0.370209 0.186286 -0.192978 -0.655277 1.265097 -0.625148 -0.301936 -0.930149 -0.031432 1.050473 11.251981 7.890126 -0.962370 -0.216231 -0.035336 0.000000 13.379274 -0.000000 -0.133498 0.140990 -0.618788 0.028222 10.991364 0.020885 -0.002822 -0.100593 -0.083280 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.217368 3.704471 5.299329 0.026068 -0.014142 0.997386 0.000000 13.074334 0.000000 -0.021716 -0.084436 -0.038986 -0.035381 14.148866 0.000001 -0.000440 0.010330 0.506535 0.000000 4.097309 -0.000000 -0.000089 -0.001035 0.251682 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.172346 0.030588 0.019030 0.997362 -0.000000 13.074365 0.000000 0.024057 -0.001577 -0.044905 -0.035380 14.148879 0.012529 0.000066 -0.008076 0.503796 -0.000000 4.097290 0.000000 0.000051 0.000096 0.258241 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 127 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.664913 5.389977 10.184178 0.020011 -0.126843 -0.840691 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.016980 -0.035366 -0.129273 0.000000 7.977538 -0.000000 0.004741 0.043820 0.037966 -0.000001 13.487594 -0.000000 -0.033126 -0.086867 -0.356030 0.000000 4.943967 0.000000 -0.002557 -0.002575 -0.405068 -1.577768 11.336034 -7.591518 0.925044 -0.153558 0.058706 0.000000 12.910863 -0.000000 0.284945 0.030127 -0.329353 -0.031254 11.193578 0.010376 -0.190553 -0.008339 -0.047280 0.054595 0.018457 -0.015135 0.321749 -0.063101 -0.237008 0.000000 3.968525 0.000000 0.016383 0.088602 0.402466 0.000000 5.348644 -0.000000 0.563052 0.069196 0.211462 0.000000 2.438549 0.000000 0.382362 0.180474 -0.186294 -0.655277 1.265097 -0.625148 -0.313639 -0.923352 -0.059305 1.045188 11.224557 7.891235 -0.960754 -0.221662 -0.033863 0.000000 13.379274 -0.000000 -0.136513 0.142268 -0.618740 0.028222 10.991364 0.020885 -0.001802 -0.091075 -0.092132 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.220583 3.704841 5.295534 0.026067 -0.014048 0.997382 0.000000 13.074334 0.000000 -0.021628 -0.084450 -0.038907 -0.035381 14.148866 0.000001 -0.000423 0.010338 0.506392 0.000000 4.097309 -0.000000 -0.000093 -0.001048 0.251810 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.175684 0.030100 0.019112 0.997361 -0.000000 13.074365 0.000000 0.024192 -0.001100 -0.044499 -0.035380 14.148879 0.012529 0.000068 -0.008075 0.503821 -0.000000 4.097290 0.000000 0.000045 0.000085 0.257998 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 128 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.631324 5.456324 10.111159 0.020195 -0.127373 -0.841598 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.015902 -0.025474 -0.130182 0.000000 7.977538 -0.000000 0.004229 0.043877 0.037408 -0.000001 13.487594 -0.000000 -0.032879 -0.086396 -0.355690 0.000000 4.943967 0.000000 -0.002324 -0.002340 -0.404926 -1.583709 11.305205 -7.590271 0.932359 -0.141587 0.051420 0.000000 12.910863 -0.000000 0.286771 0.025344 -0.310770 -0.031254 11.193578 0.010376 -0.180561 -0.007714 -0.043957 0.054595 0.018457 -0.015135 0.310763 -0.063112 -0.244959 0.000000 3.968525 0.000000 0.018193 0.086036 0.398893 0.000000 5.348644 -0.000000 0.546696 0.067279 0.196515 0.000000 2.438549 0.000000 0.394555 0.174558 -0.179495 -0.655277 1.265097 -0.625148 -0.321593 -0.916996 -0.086843 1.039846 11.196838 7.892356 -0.959119 -0.226947 -0.032741 0.000000 13.379274 -0.000000 -0.139837 0.143666 -0.618573 0.028222 10.991364 0.020885 -0.000848 -0.082174 -0.101268 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.224079 3.705163 5.291316 0.026065 -0.013945 0.997379 0.000000 13.074334 0.000000 -0.021529 -0.084465 -0.038864 -0.035381 14.148866 0.000001 -0.000406 0.010346 0.506247 0.000000 4.097309 -0.000000 -0.000098 -0.001063 0.251953 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.179378 0.029561 0.019202 0.997362 -0.000000 13.074365 0.000000 0.024339 -0.000572 -0.044090 -0.035380 14.148879 0.012529 0.000070 -0.008075 0.503846 -0.000000 4.097290 0.000000 0.000040 0.000075 0.257762 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 129 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.597316 5.523028 10.037025 0.020377 -0.127946 -0.842511 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.014874 -0.015981 -0.131054 0.000000 7.977538 -0.000000 0.003680 0.043939 0.036830 -0.000001 13.487594 -0.000000 -0.032619 -0.085896 -0.355361 0.000000 4.943967 0.000000 -0.002078 -0.002091 -0.404776 -1.589459 11.275372 -7.589064 0.939024 -0.129986 0.044367 0.000000 12.910863 -0.000000 0.288491 0.020514 -0.291915 -0.031254 11.193578 0.010376 -0.170466 -0.007084 -0.040602 0.054595 0.018457 -0.015135 0.299630 -0.063111 -0.252927 0.000000 3.968525 0.000000 0.020016 0.083447 0.395281 0.000000 5.348644 -0.000000 0.529824 0.065299 0.181320 0.000000 2.438549 0.000000 0.406748 0.168552 -0.172600 -0.655277 1.265097 -0.625148 -0.323323 -0.912485 -0.112840 1.034459 11.168886 7.893487 -0.957478 -0.232071 -0.032027 0.000000 13.379274 -0.000000 -0.143410 0.145159 -0.618309 0.028222 10.991364 0.020885 0.000056 -0.074078 -0.110689 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.227798 3.705446 5.286762 0.026063 -0.013835 0.997376 0.000000 13.074334 0.000000 -0.021422 -0.084481 -0.038848 -0.035381 14.148866 0.000001 -0.000389 0.010354 0.506102 0.000000 4.097309 -0.000000 -0.000103 -0.001078 0.252108 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.183356 0.028980 0.019300 0.997363 -0.000000 13.074365 0.000000 0.024495 -0.000004 -0.043676 -0.035380 14.148879 0.012529 0.000071 -0.008074 0.503872 -0.000000 4.097290 0.000000 0.000035 0.000065 0.257532 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 130 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.562997 5.589996 9.962065 0.020559 -0.128549 -0.843428 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.013901 -0.006948 -0.131883 0.000000 7.977538 -0.000000 0.003104 0.044003 0.036237 -0.000001 13.487594 -0.000000 -0.032353 -0.085377 -0.355045 0.000000 4.943967 0.000000 -0.001824 -0.001834 -0.404621 -1.594977 11.246740 -7.587906 0.945033 -0.118850 0.037604 0.000000 12.910863 -0.000000 0.290099 0.015653 -0.272852 -0.031254 11.193578 0.010376 -0.160294 -0.006449 -0.037223 0.054595 0.018457 -0.015135 0.288381 -0.063098 -0.260890 0.000000 3.968525 0.000000 0.021851 0.080840 0.391637 0.000000 5.348644 -0.000000 0.512505 0.063264 0.165941 0.000000 2.438549 0.000000 0.418906 0.162475 -0.165626 -0.655277 1.265097 -0.625148 -0.315971 -0.911223 -0.136955 1.029039 11.140761 7.894624 -0.955847 -0.237019 -0.031784 0.000000 13.379274 -0.000000 -0.147166 0.146722 -0.617969 0.028222 10.991364 0.020885 0.000928 -0.066988 -0.120402 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.231682 3.705701 5.281960 0.026061 -0.013719 0.997375 0.000000 13.074334 0.000000 -0.021308 -0.084497 -0.038854 -0.035381 14.148866 0.000001 -0.000372 0.010362 0.505955 0.000000 4.097309 -0.000000 -0.000108 -0.001095 0.252270 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.187542 0.028369 0.019402 0.997364 -0.000000 13.074365 0.000000 0.024659 0.000593 -0.043261 -0.035380 14.148879 0.012529 0.000073 -0.008074 0.503897 -0.000000 4.097290 0.000000 0.000029 0.000055 0.257312 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 131 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.528478 5.657134 9.886572 0.020739 -0.129173 -0.844346 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.012989 0.001566 -0.132665 0.000000 7.977538 -0.000000 0.002512 0.044069 0.035635 -0.000001 13.487594 -0.000000 -0.032084 -0.084851 -0.354746 0.000000 4.943967 0.000000 -0.001567 -0.001573 -0.404463 -1.600222 11.219523 -7.586806 0.950393 -0.108275 0.031188 0.000000 12.910863 -0.000000 0.291587 0.010777 -0.253644 -0.031254 11.193578 0.010376 -0.150071 -0.005811 -0.033828 0.054595 0.018457 -0.015135 0.277044 -0.063073 -0.268825 0.000000 3.968525 0.000000 0.023691 0.078221 0.387968 0.000000 5.348644 -0.000000 0.494814 0.061184 0.150445 0.000000 2.438549 0.000000 0.430991 0.156343 -0.158595 -0.655277 1.265097 -0.625148 -0.301914 -0.912093 -0.159880 1.023597 11.112525 7.895766 -0.954242 -0.241771 -0.032082 0.000000 13.379274 -0.000000 -0.151043 0.148331 -0.617574 0.028222 10.991364 0.020885 0.001786 -0.061126 -0.130417 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.235672 3.705937 5.276999 0.026059 -0.013599 0.997374 0.000000 13.074334 0.000000 -0.021190 -0.084515 -0.038873 -0.035381 14.148866 0.000001 -0.000355 0.010370 0.505808 0.000000 4.097309 -0.000000 -0.000113 -0.001111 0.252436 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.191863 0.027738 0.019507 0.997366 -0.000000 13.074365 0.000000 0.024827 0.001210 -0.042844 -0.035380 14.148879 0.012529 0.000074 -0.008073 0.503923 -0.000000 4.097290 0.000000 0.000025 0.000046 0.257103 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 132 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.493870 5.724348 9.810845 0.020920 -0.129805 -0.845262 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.012144 0.009497 -0.133394 0.000000 7.977538 -0.000000 0.001911 0.044135 0.035028 -0.000001 13.487594 -0.000000 -0.031819 -0.084326 -0.354464 0.000000 4.943967 0.000000 -0.001311 -0.001314 -0.404307 -1.605152 11.193945 -7.585771 0.955116 -0.098355 0.025177 0.000000 12.910863 -0.000000 0.292950 0.005905 -0.234359 -0.031254 11.193578 0.010376 -0.139821 -0.005172 -0.030427 0.054595 0.018457 -0.015135 0.265652 -0.063036 -0.276709 0.000000 3.968525 0.000000 0.025534 0.075596 0.384284 0.000000 5.348644 -0.000000 0.476829 0.059066 0.134902 0.000000 2.438549 0.000000 0.442969 0.150174 -0.151527 -0.655277 1.265097 -0.625148 -0.284488 -0.913731 -0.181557 1.018145 11.084239 7.896910 -0.952679 -0.246307 -0.032995 0.000000 13.379274 -0.000000 -0.154976 0.149960 -0.617147 0.028222 10.991364 0.020885 0.002648 -0.056741 -0.140747 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.239709 3.706165 5.271966 0.026056 -0.013478 0.997373 0.000000 13.074334 0.000000 -0.021070 -0.084532 -0.038898 -0.035381 14.148866 0.000001 -0.000338 0.010378 0.505661 0.000000 4.097309 -0.000000 -0.000119 -0.001128 0.252603 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.196243 0.027098 0.019613 0.997367 -0.000000 13.074365 0.000000 0.024997 0.001835 -0.042426 -0.035380 14.148879 0.012529 0.000076 -0.008073 0.503949 -0.000000 4.097290 0.000000 0.000020 0.000037 0.256906 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 133 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.459290 5.791538 9.735190 0.021100 -0.130433 -0.846174 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.011371 0.016783 -0.134063 0.000000 7.977538 -0.000000 0.001313 0.044202 0.034423 -0.000001 13.487594 -0.000000 -0.031562 -0.083815 -0.354204 0.000000 4.943967 0.000000 -0.001061 -0.001061 -0.404154 -1.609720 11.170243 -7.584813 0.959222 -0.089187 0.019626 0.000000 12.910863 -0.000000 0.294185 0.001053 -0.215066 -0.031254 11.193578 0.010376 -0.129572 -0.004534 -0.027028 0.054595 0.018457 -0.015135 0.254236 -0.062987 -0.284521 0.000000 3.968525 0.000000 0.027374 0.072971 0.380592 0.000000 5.348644 -0.000000 0.458638 0.056922 0.119381 0.000000 2.438549 0.000000 0.454803 0.143987 -0.144442 -0.655277 1.265097 -0.625148 -0.266404 -0.915186 -0.201811 1.012697 11.055969 7.898054 -0.951178 -0.250604 -0.034613 0.000000 13.379274 -0.000000 -0.158896 0.151583 -0.616715 0.028222 10.991364 0.020885 0.003534 -0.054124 -0.151414 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.243731 3.706396 5.266956 0.026054 -0.013358 0.997372 0.000000 13.074334 0.000000 -0.020950 -0.084549 -0.038921 -0.035381 14.148866 0.000001 -0.000320 0.010387 0.505513 0.000000 4.097309 -0.000000 -0.000124 -0.001145 0.252768 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.200605 0.026461 0.019719 0.997367 -0.000000 13.074365 0.000000 0.025167 0.002457 -0.042008 -0.035380 14.148879 0.012529 0.000078 -0.008073 0.503975 -0.000000 4.097290 0.000000 0.000016 0.000029 0.256723 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 134 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.424855 5.858604 9.659923 0.021281 -0.131047 -0.847080 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.010677 0.023355 -0.134667 0.000000 7.977538 -0.000000 0.000727 0.044267 0.033824 -0.000001 13.487594 -0.000000 -0.031318 -0.083328 -0.353967 0.000000 4.943967 0.000000 -0.000824 -0.000821 -0.404009 -1.613878 11.148668 -7.583940 0.962734 -0.080868 0.014593 0.000000 12.910863 -0.000000 0.295290 -0.003762 -0.195835 -0.031254 11.193578 0.010376 -0.119351 -0.003897 -0.023640 0.054595 0.018457 -0.015135 0.242829 -0.062926 -0.292239 0.000000 3.968525 0.000000 0.029208 0.070353 0.376902 0.000000 5.348644 -0.000000 0.440328 0.054762 0.103953 0.000000 2.438549 0.000000 0.466461 0.137800 -0.137363 -0.655277 1.265097 -0.625148 -0.250618 -0.915681 -0.220342 1.007264 11.027780 7.899194 -0.949756 -0.254633 -0.037033 0.000000 13.379274 -0.000000 -0.162734 0.153173 -0.616302 0.028222 10.991364 0.020885 0.004469 -0.053614 -0.162443 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.247676 3.706640 5.262062 0.026051 -0.013240 0.997371 0.000000 13.074334 0.000000 -0.020834 -0.084566 -0.038934 -0.035381 14.148866 0.000001 -0.000303 0.010395 0.505366 0.000000 4.097309 -0.000000 -0.000129 -0.001160 0.252925 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.204869 0.025839 0.019823 0.997367 -0.000000 13.074365 0.000000 0.025333 0.003066 -0.041591 -0.035380 14.148879 0.012529 0.000079 -0.008072 0.504001 -0.000000 4.097290 0.000000 0.000012 0.000022 0.256557 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 135 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.390692 5.925440 9.585377 0.021462 -0.131632 -0.847977 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.010068 0.029144 -0.135199 0.000000 7.977538 -0.000000 0.000165 0.044329 0.033239 -0.000001 13.487594 -0.000000 -0.031093 -0.082877 -0.353756 0.000000 4.943967 0.000000 -0.000605 -0.000599 -0.403875 -1.617575 11.129489 -7.583165 0.965676 -0.073498 0.010137 0.000000 12.910863 -0.000000 0.296263 -0.008523 -0.176737 -0.031254 11.193578 0.010376 -0.109187 -0.003265 -0.020273 0.054595 0.018457 -0.015135 0.231467 -0.062854 -0.299841 0.000000 3.968525 0.000000 0.031031 0.067747 0.373223 0.000000 5.348644 -0.000000 0.421991 0.052597 0.088687 0.000000 2.438549 0.000000 0.477907 0.131633 -0.130312 -0.655277 1.265097 -0.625148 -0.238410 -0.915167 -0.236804 1.001860 10.999742 7.900328 -0.948490 -0.258005 -0.041300 0.000000 13.379274 -0.000000 -0.166417 0.154701 -0.615938 0.028222 10.991364 0.020885 0.005400 -0.057169 -0.174400 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.251477 3.706909 5.257384 0.026049 -0.013126 0.997369 0.000000 13.074334 0.000000 -0.020723 -0.084582 -0.038929 -0.035381 14.148866 0.000001 -0.000286 0.010403 0.505220 0.000000 4.097309 -0.000000 -0.000134 -0.001175 0.253071 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.208950 0.025242 0.019923 0.997366 -0.000000 13.074365 0.000000 0.025493 0.003649 -0.041177 -0.035380 14.148879 0.012529 0.000081 -0.008072 0.504026 -0.000000 4.097290 0.000000 0.000008 0.000015 0.256409 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 136 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.356932 5.991933 9.511900 0.021645 -0.132176 -0.848862 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.009552 0.034074 -0.135654 0.000000 7.977538 -0.000000 -0.000362 0.044388 0.032672 -0.000001 13.487594 -0.000000 -0.030893 -0.082475 -0.353575 0.000000 4.943967 0.000000 -0.000409 -0.000402 -0.403755 -1.620753 11.112996 -7.582498 0.968072 -0.067181 0.006321 0.000000 12.910863 -0.000000 0.297106 -0.013211 -0.157845 -0.031254 11.193578 0.010376 -0.099109 -0.002639 -0.016936 0.054595 0.018457 -0.015135 0.220186 -0.062771 -0.307306 0.000000 3.968525 0.000000 0.032838 0.065161 0.369564 0.000000 5.348644 -0.000000 0.403720 0.050438 0.073650 0.000000 2.438549 0.000000 0.489109 0.125508 -0.123312 -0.655277 1.265097 -0.625148 -0.227953 -0.914249 -0.251685 0.996500 10.971927 7.901453 -0.947366 -0.260611 -0.047661 0.000000 13.379274 -0.000000 -0.169867 0.156137 -0.615654 0.028222 10.991364 0.020885 0.006276 -0.064997 -0.187430 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.255065 3.707215 5.253029 0.026048 -0.013020 0.997366 0.000000 13.074334 0.000000 -0.020621 -0.084598 -0.038897 -0.035381 14.148866 0.000001 -0.000269 0.010411 0.505075 0.000000 4.097309 -0.000000 -0.000138 -0.001188 0.253202 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.212761 0.024685 0.020016 0.997364 -0.000000 13.074365 0.000000 0.025644 0.004193 -0.040765 -0.035380 14.148879 0.012529 0.000083 -0.008071 0.504052 -0.000000 4.097290 0.000000 0.000005 0.000010 0.256282 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 137 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.323714 6.057964 9.439868 0.021829 -0.132664 -0.849733 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.009136 0.038068 -0.136022 0.000000 7.977538 -0.000000 -0.000843 0.044442 0.032131 -0.000001 13.487594 -0.000000 -0.030725 -0.082135 -0.353427 0.000000 4.943967 0.000000 -0.000244 -0.000234 -0.403654 -1.623353 11.099506 -7.581952 0.969939 -0.062032 0.003211 0.000000 12.910863 -0.000000 0.297822 -0.017810 -0.139232 -0.031254 11.193578 0.010376 -0.089148 -0.002020 -0.013640 0.054595 0.018457 -0.015135 0.209023 -0.062678 -0.314611 0.000000 3.968525 0.000000 0.034623 0.062603 0.365937 0.000000 5.348644 -0.000000 0.385609 0.048296 0.058911 0.000000 2.438549 0.000000 0.500034 0.119445 -0.116389 -0.655277 1.265097 -0.625148 -0.218577 -0.913037 -0.265561 0.991198 10.944415 7.902566 -0.946292 -0.262732 -0.055316 0.000000 13.379274 -0.000000 -0.173001 0.157450 -0.615483 0.028222 10.991364 0.020885 0.007116 -0.075447 -0.201079 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.258364 3.707570 5.249107 0.026046 -0.012924 0.997362 0.000000 13.074334 0.000000 -0.020530 -0.084612 -0.038828 -0.035381 14.148866 0.000001 -0.000252 0.010419 0.504931 0.000000 4.097309 -0.000000 -0.000142 -0.001200 0.253313 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.216206 0.024181 0.020101 0.997360 -0.000000 13.074365 0.000000 0.025782 0.004686 -0.040358 -0.035380 14.148879 0.012529 0.000084 -0.008071 0.504077 -0.000000 4.097290 0.000000 0.000003 0.000005 0.256177 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 138 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.291191 6.123405 9.369680 0.022016 -0.133081 -0.850588 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008827 0.041040 -0.136298 0.000000 7.977538 -0.000000 -0.001263 0.044490 0.031623 -0.000001 13.487594 -0.000000 -0.030594 -0.081871 -0.353316 0.000000 4.943967 0.000000 -0.000117 -0.000105 -0.403576 -1.625307 11.089368 -7.581542 0.971289 -0.058172 0.000880 0.000000 12.910863 -0.000000 0.298414 -0.022303 -0.120973 -0.031254 11.193578 0.010376 -0.079338 -0.001411 -0.010395 0.054595 0.018457 -0.015135 0.198018 -0.062575 -0.321735 0.000000 3.968525 0.000000 0.036383 0.060079 0.362353 0.000000 5.348644 -0.000000 0.367754 0.046183 0.044534 0.000000 2.438549 0.000000 0.510650 0.113467 -0.109567 -0.655277 1.265097 -0.625148 -0.209776 -0.911585 -0.278848 0.985970 10.917289 7.903663 -0.945204 -0.264581 -0.063665 0.000000 13.379274 -0.000000 -0.175730 0.158606 -0.615463 0.028222 10.991364 0.020885 0.007935 -0.087278 -0.215003 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.261295 3.707989 5.245740 0.026046 -0.012840 0.997356 0.000000 13.074334 0.000000 -0.020453 -0.084625 -0.038713 -0.035381 14.148866 0.000001 -0.000236 0.010427 0.504790 0.000000 4.097309 -0.000000 -0.000145 -0.001208 0.253399 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.219183 0.023745 0.020175 0.997355 -0.000000 13.074365 0.000000 0.025904 0.005112 -0.039956 -0.035380 14.148879 0.012529 0.000086 -0.008071 0.504102 -0.000000 4.097290 0.000000 0.000001 0.000001 0.256099 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 139 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.259527 6.188116 9.301774 0.022206 -0.133410 -0.851422 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008635 0.042898 -0.136471 0.000000 7.977538 -0.000000 -0.001610 0.044529 0.031156 -0.000001 13.487594 -0.000000 -0.030510 -0.081700 -0.353246 0.000000 4.943967 0.000000 -0.000034 -0.000021 -0.403525 -1.626541 11.082965 -7.581283 0.972117 -0.055739 -0.000589 0.000000 12.910863 -0.000000 0.298888 -0.026672 -0.103144 -0.031254 11.193578 0.010376 -0.069713 -0.000813 -0.007213 0.054595 0.018457 -0.015135 0.187213 -0.062465 -0.328654 0.000000 3.968525 0.000000 0.038110 0.057600 0.358824 0.000000 5.348644 -0.000000 0.350247 0.044109 0.030580 0.000000 2.438549 0.000000 0.520923 0.107598 -0.102874 -0.655277 1.265097 -0.625148 -0.201093 -0.909909 -0.291906 0.980834 10.890640 7.904741 -0.944056 -0.266352 -0.072166 0.000000 13.379274 -0.000000 -0.177956 0.159566 -0.615633 0.028222 10.991364 0.020885 0.008744 -0.099371 -0.228898 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.263771 3.708487 5.243059 0.026046 -0.012771 0.997349 0.000000 13.074334 0.000000 -0.020394 -0.084636 -0.038541 -0.035381 14.148866 0.000001 -0.000220 0.010434 0.504650 0.000000 4.097309 -0.000000 -0.000146 -0.001214 0.253455 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.221583 0.023393 0.020235 0.997348 -0.000000 13.074365 0.000000 0.026007 0.005456 -0.039561 -0.035380 14.148879 0.012529 0.000087 -0.008070 0.504126 -0.000000 4.097290 0.000000 -0.000000 -0.000001 0.256050 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } ././@LongLink0000644000000000000000000000015500000000000011604 Lustar rootrootdoomsday-stable-1.15.7/doomsday/tests/test_glsandbox/net.dengine.test.glsandbox.pack/models/Marine_green.pcxdoomsday-stable-1.15.7/doomsday/tests/test_glsandbox/net.dengine.test.glsandbox.pack/models/Marine_g0000664000175000017500000017500012641367671033201 0ustar jaakkojaakko ._y~:__x..x xl«s .NVV.´..OOs_x.´x x.C=;´ xs _sҫ_x.x . _lNV=.x_l.xOlx.x_..OĴ.. =b;٦CV;Ĵ´ s«55_ ._5O5s . .xô.N.b =ٴ =;.´..´.x llO5x. _sOs_. _x.5Osx.;V.٦b bbAD=...ô. x_OOl._s5Ol._x.xOlxx x_x . ..V==;»V=b C.bD=.N´;..xs5ll5Oӫll5sx._sOOl_x._ sOO__l«l_x..V=b=;.V .= ;.VC...;;. sl5OOl_ _OOs_ . x_. sӭ5s__l5lls_ .V==.٦‰D=.=* C..VCvV.VCVٴsӭӃOO5Oll. __lO_. _.xOll55Ol55O5l_x.NV=b;.;= =. ´»VCV.N.VV;.lӭlO55 _O_x. _ x_lҭls_lOOO55 .»٦=V.vb .C=..C=.VVٴ._lO5ӃOOl__sOOOx.xs _sOӭ__lOlҫ5l5x.V==Vôb b.N...V=V;¦=V;´._5ll5ܭ5Ӄ5_ x_sOӤ_s ._Ol_«lOOӭ5lOҫ5l .;VŦV..CV.ô´;V¦VV¦b=V;.;.;.O«l­OOa_a_.N.x_ls_x._5Ols_5lӭlO5l5O.;VvC¦C.´´.V;.V==´.VV;xܭOO†OҫӭOl4x. OOҫ_x´.xO5_«5O­ҫsô..;V»..Ŵô.;.VCN;V=..5­OOOܫ :x._alӭO_x´._l5_l5«OƒOl..;.N;...N..NV;¦b=. Ӥk†ӭOOsxN A´._ayOOlsxôslOܫlOlOll5s..N´....N..»ٻVvb=ٴ´xslӭӃ­Oҫlx.DD=.Nxayllx ...5Ols_x_slO¤5lOO ........»N.ٻæb=;;.l5Oӭ¤5l5_ (b ADb N x:yOOl_ .._5O5_x_5OOOOll_;٦=.´´.V .;V».CV.Ĵ 5OOҫ5s_ vV V x aylOls_x ´´´´.x_5O55 _ s5OOO҃O .VVb NVVVVV;VV=VN.N.ٻ..;.xOOs_a_ NN.V=D*axxl5OܭOҫlx.´.. x x x x.x x x. _5Ol5. _OOOOĭO5 VC .V=æ=V.VVV *b..VV» O­OOOx_x.CA:.l5_xx..s__5OӭO5ls_x» *DAAC.s5OOӭOO5.ٻ;٦Db´;C=¦=b;.V¦= D vNô.´= =V;5OOl_4n*s(..=Dn:a_. slO­O5lsxxssss5ܭƒҫ_x  ڦx_OOO5sl ..VV==.;æ.V=bDô;´Ĵ.vAD=_OOsanD ».´ADMsx. x5O5sl«l5l5«llOOӭ_ .bNb _sl5lOҫl5_;´.V=¦=b=.¦V.= D.V.´...DAb=V´_sOOOOҫ5laJAC bV ´.D«x l5OslOOOOOlxbDV.CxalOӭOOOOlx..٦=¦=.;¦V.; DvV.´.N.b=sOӭOl b;*DAbV; (;Vb%x5OOl_ _sOOOkíĭOOlss_nD.N(__lOӃOOlsx.Ĵ.ô;Cæ=VVC== .´=b=.´.N»;V bC;xslll5Ols_.=VvbD _4CvlOOOls_l5llOOOOO5llssb»x_xslOOOOӭO5l ..Cæ==VCV=*V.v bVN.;b ´xl5ū55l5O5x.N.bAa:Yy5OOl_s_.lll«l5ll _MA;An_aOOOOlxVV..Ŧ=æ=Ŧb.CV;.;.VC==. 5ҫOӏ/Ol5_xa .xAA~:MO­ҫx x_ ._llsys_xD.´xbں_s5l5OOls ;V==V;.;VvvƦæ=....;..V¦=b. 5l­/OOs__ax.ôCDnM5­ŭO_x..x _ x_sss__ssxsl5zDvx.bD* sllOOOOOls.;b=¦V;..;.ٻVCVVCVV.VVVb .OӤҫl_xssax.´A%lӤOlx .ô x x x. x._x_sl5l~* v.xVbDAba_slOO5lsx.;V=b b=;ٻ...V»V´N٦VC¦bA*=.l5OӤOOs_sa_N.(N A=ƫOӭӤ¤O5l_x x x_  .x x___5lls:y=Jx_AADx_x x5OOOҫ55l5llsx;= =Cٻ;...ٻ.NCVٴVV= A=._lӭO†OOO5sss_N.N_ V%O†ƒOOl5__sl5lllya_aA.x.xl5OOO55 .;V=V».N....»V;.VV=DڵA ._lO¤O55«5s_s:a_.x_ _b_†ӤƤO5ll_5ë5llM~5MlAb´.x_x_ssl5OOOOҫ5lx.٦CV¦V;N.ŴĴ.´´N.V¦ A^>A ssl5OOOҫ_a ´x x x†ņƒOsl5OlOl5OM%._ssӭӭlsx.»VC¦=¦V;..´ôN.;...V¦=bs5ls5OO5l5ls___ .. xslĭ¤äÆäƒӭO5Oë5O܃5ܫ-ډA x.x_ss_5í¤†«l55x..¦=b=C»V..;...N.٦= ^bVsO«ll5lls_x.x._slOӭӆŭ†Ƥ­lOOOOӭl5OҺڦbx . __­OĆOO5_xV¦=V=b=;...N.´.;.´.V=¦=D=v.¤Ol55«_s_x..xlOOƤ†­†ӭOӃOOO5ȎaAbx. sO«OӭäOO5s_l.=C٦= V.ٻ.»V.´;V¦=CVN.s܃Ćҫ5ll55s_x_s_ .. _lOOܭƆ­OOӃ­ӃOlO­OOOOha4:WR .xs_sl5­5s_O.»¦=٦b A V.Vٻ..vV..V=ٻ»..s†O_l5l5 _x.´. lOƒ­kkíOOOӭ¤«Oílys4a_ .x__sslOlsl..¦=¦٦= .VV.ô.V.´.¦=¦VV;..lܭ­Ol_sl5_x. .._ܤÆkӃOO««O¤­OOl5lx´ __sl5l55lllO´٦CV;Cb V.;Vٻ.;.´..´C=.ô. .5ҫs__ll. .xsl†ӭ¤­ܭ­Ol5lӭ­­OO5l5.. _sl555l5ƒl_.».»= Abٴ.VV;....æ=V;. 5s_.lx. x . _5O­܃ƒӃӃllOO5O«55.x_xlë5lOӭäO_ ´.»..= =...;;´.x_s_ »x .x_x ._lOOӃOӃ5ӭ­Ӄ­OOO5555 .x__x.x_l«5lk¤O´.´..»C=C´...;´´.. _s_.__._x .x_lӃ­Ӄҫ5l55llOĭƒOO«OOҫҫ5x. .l_x_O¤O5~_xô.Ĵ.Ĵ´;´a8a#R8R#as . x.x__x._OOĤíOO5555lOӭOO5 .5___­5sx..´.;.ŴR۪H6rSrSms_ ..___x_5OӭӭӃO5lOOӭӃOO5OOOҫ .lls_sOҫls׻.´.´...ô;:IH6900ÞMs_.´.__l5ӃӭƒӤӭҫ5lOOOܭOOOOOx. l_x__ssls_ .;ŴǴǴ.ɴ.ɴ´´..;´´(IH6\9S6fa_s sxxl5lOӭOӃܭӤíӃO5OlOOO­ƒOOOOO. __x.. _s_´CADADAD n n*n*nWf****Dbn ADADAKDN#II6r۪۾r`S:(_a_x.x_x_l5¤OOlO5ƒOOOl_ .xx._s x».´A°ő?Db¦=b b*«___svB)`m77IR(Y#JnfJf"7f"fJ f7˜)SB7K7%"7%"7I7%#(f"%7"zm &v88IIª"._yya_x xVCCbC K*J۾60wwB8f b b bæV= b=bs5ls___ mSm77K7IfRN4(( #R##RaRJR#R#R*#4(»(RfI77r`).#"H۪f2288Iª8f.xV=bb=b b=DK=AI60+XooEXpb AA>^ b¦=bb=b*s~55s_ ___s_x%SB7&7"fa(Rf.(#nf"ڟ%7%7AWn%&7z"7%7"7f4vf(Rf"77p` v(pS6882R(.*b=b=A^' b =٦AJIHEEEXbbDDADbv=b D* =:Ols_x _ v&S&7"##H`R(=D>^'j^''DZj''^YBS7R4Rf"Km"7`&v(vS9`H2IH(.D*bÌ' =AIH+X+u©rpJb *^D b=b DA =b=:y«5ls_ _.4mSm7&mB&7fvf`0SR¼j11±jjjj1Ìjj^J\9f##"pm&7S9mRvCS9S¾HI2I6r9SfV b=b=ü^^^b =bv% =b^%2I60+uX+0ž\H*A =V¦b‰Ab b¦ b_:~lOҫOҫs_.NfS&mdm7"4(4f9&R^''j1³³1ññ1Œ1j-@&#*7mS)`SK7H9`vS6þBHR26r0u0S(.bV¦=^‰µD bCb bbH6rr–r6S`SI: AbD> =Vb DA b=VVb=V x__s~lO5s_ASrmdm&7nRv((#9*'j1󓳌1jıj1Œ1j^f7GR(vKS`m&9`&VvSr6rS6Hª88ĪIHr0u+u7; b b^>Db b= =b >A#IHp¾IpH" D>AbDA b D D b=y_O«l vS`m7%4(4(#p9pA''j1Ó±j1“1j790R(Y(v"S9)7pB)9m=vH6`r6R8IH6r90@+X+0rv AAV=A'A¼ =bA>ADAD 'D%27H7z۟IrS=D^'ּ>^ ^''^>b:a_l5.&S`m7f(Rf#NRm`f%jñ1“{1±1³11`@r##"f#JrKS9`*v(I6r9`rH8:8IH6\0u+Xu+`;bADAVbAv b>j1Œ^'j'-A‰-'1>b>jjj^ * AjjjDb bn .fm)9S&7b((#2p7R(#S0*7'jj±1{111“ó1-J0`R(7RY7`Sm`\.(6r9\96I8#8IHS90@+X+u09 bv=b^‰b=Aej'j։A^€j^'AA 1j^AbA'^>¼^'^b D '^ü'D µ‰n:(pS)`7f4 #f&f90&*%jj1{{1ŒijŒ1Rm9f(BSIR(N*`m&`9mC(N8I`9\96I28#8IHr9\0+X+u9S´===vbA˦v=bd=F>^^>> b=¦A' =b ^> >>  bA^-bD>^Ab bADD‰A VvmS`&f(#fpS)"9BR%j11{Ô{1³³1jD&\"RB9`I#7`m)9S#N.8Ir9\9r8#8IHr90u+@0\S4´=VVæA‰ A* ڦvAF>ډ^>>AbV'^¼=¦٦ b¼D bb¦=b= AA V¦=b=b=V.JS)dSB*C I9`#"9\7#A'j±1{Ĕ11Čų1j#I9#`S7WR)m9..NRIS`908RI6r0+9B#´x D Db==ډAAD ACDj^A^AµAb=b=։ b=bA b DADAb= bD bD DAD =b b b bvm`Smfv(Rn7S#p9 %#A'j±1Ŕ11ȳ1^ Vf9\p47])7f#YISm`\#.N#IHS9\@0H88IH6r90+u@9S&4 D*b=VAAډډAdAV -ĉDA^A=* ֗j b=b‰D=bDAA =VA‰DA= DA.Jr`m`(7)\9747`#A'j11³“Ô{111Œj Jr97Y]9Y(fmBS@Kٴ.4"H`\096۪88H6r`90+u\9fC.b bVA AAԉ= ¦=bD DAD b=bD=VC=b—։=ĦAԼ>%bVbA>A=v=bbٴNI`&`R4f%9\`%v`f4j1Ì{11111>C*r\#4p939m%JfS`S@X.N:690SHR86r9@u0\r7(R4bvADAAֵVAAADb DVCb—^ D b=DA^Ab=bV ڵA¼> b D DCvS9``7#fB\@`f(#79SJRj1Ì“{{111jñ1³>D=9@Yf`3@7*YJS``u).#۾r90\rHI8R۾6r909`#.4x_:4CA%DA V=DAAAڵAڵ‰>D¦=b>A V= bD‰v C=b A첵 V*D DAA R`@9`S9S"43SfY#p9fj1Čē1ıj1Œó1ּbJS9R2)u`"4(4H9`0X9N(#IHS9r8IH6r9\ɞ9Sf#N(#_slya:b =b=bCbDA‰ڵډAAA¦b=b * bnbC=DԼ‰Db*=vAD¼>=b Ab˦.f9\9`9m(N#"3f(RSmJ*j1ij1±j1Œó1^JS&R(JG7R4(fS\`@X\7ٙ´(S90\`I28R86r909`7fR(slO5y v¦=צAAAڵADADV4#((v4RbD* b* ¯= "%b=bb bb nJn*:4C ´C9\\`BfNR'JYf%fRb1Œ³1jj1Č1^=R"fY(e]7((9 \u@&(.#RIr9\`6Ia8RHS6r9SH#´5a~5-*vD‰Aډ%A>ACNaaRyf:*4v*n=44%-ݎ˜4:n%n"n:Mzx.vS9\\7fvN(pS9mf(#R b>րjŒ³ÌŒ1±jj1Ō1j'^=(#4v(f)G9B:(pS]@XuSR.(#RHS\96I28R69`S7I(í5܎yb b b *D%bCx_sl55Ž55ܫl5«5O5lOs #S9\`7fYNB)].vfC='j1Ȍ1jj1Č1ּvCR4.v)]u\f (7S3X X`fV(R#Rr9\S28R8۾6r`SHf.l¤OӃyW a=vCvV¦v4= ba=4(a«O«ӭܭӭOO .JS9\@\`pf#J&`]`&#m#v1Ō11±jjj1³Œ1j¼DC#I&JVv7u0fYNISuZwu\7.(R#IHr9S288I6rHIיO¤OӃO~n%J:a4R:*4v (v(NxslOOņӆkOOOOOӤs .`\\9)JC(f&)39RC4(^'j1Ì111j'1ij1'ff(4B939fv("rZXu\7(Rf:f69`688IH66HI:.lOܭOOO55l:4:a4xOOOO†ӃĆOOҫOks7939SHJ(fS9\m#(v(4'j1Œ1±1j1ó1^^A=(#Yv(4B9@9Sf4("` wu`7N#fI69628#88I6HfNlOӭOOOOӭҫsssOOӤk†Ć¤¤ÆOOӆkx49\9`)7J(4fKmS&fv.fR(Jj11±jjj''1jRNfS99"4NRSuwu9S7Y.7"f96882IH664ҫӭOӭӭOOO5O5lÆĆ††OӃƒӭk R)9`7:(#f7JfR(R7pR#%^j11jj'j1ó1'¼A Y7R(#R*fSr4N#R0wu9`7N#J"f6`rH8IþHY. 5OӭOO_5ҫ5O¤̆¤†܃­kk´vJ`S7N*RJ7f#(RpS#*'j11±'^ü^Œ1'ּ Y"`mR7:fR#N#HXu`H*v;ٻ.f"I6r6۪R2۾6þH"4.ҫOӭO܃OӭO5s5l5OܭĆʆOƐk5sx´#fn`r"R( #fS9"R#fSJv%^1'>AD%ډ&KA>'1Œ1'4R974fS0SJY (NR`9fn"7.N7p7f2r6H82H¾66S7(OӃӃO­ӭҫl5OӭƆ¤ĆO†ĤkÆs Ny)&f*J7IJ2RN79@\"RB9#(J'j1j4RJfW*-^j1j''^#f`Sf#S3wNN#"&7f7`)SN.R%H۾r6H2H66S7 5OOӭ­ӭ¤†k­O«ҫlƆkĆ­O¤Ɇ†ӫ´R"m7J:NR"7S9@`&*7r`f#%'jñ^YY#J"7&7JRJj''R`B#9uXGS7R.N#"fJfSm.NJ7I66ªIH6rSJOOĆkO«O†ņOkƤ†äs (#"mJ*R#.(m`@\S%S9SJ#*>jv pK3`)`f'''ֲ#79S"#Ju@\`7JC.J7)`&fR7m6HI۾r6H۪I2Iľ6Hv.lOÆkkkǐk5ss«Oӭ¤­ÃO܃¤†äƒӃsx#fm.N47pm\3]9&J7`7R*%''^%RWp)\GQt]3P9"'' 4#S`7JR7`]PuS7f(.Jm`9`7J#(.*mSII6H۪IHH۾S7vOO܃†kkkȐkOl_s5ƒӭíӃņĭ܃ܭӫxI&SvN."7m) ]#J&9`*#AR#fHr9\C#7\`74#7)@Χ]9`H%fRN%r9S׻N(N#v&SrI"6IIHzIH4 lO†ÐkkĐk¤­l_x_s_l5l­íӭ¤††ƒ55ӭ5.׻N.4"&7.(RfI7`3\Sv7S`((#"9`S`9uXu@9uZEHRbR 4"r`fB9u}e9`SJ#()9`:R.N (v*7mS`96IH6۪`rI۾Hf(.OO߭ӤÐkkl_x_slӃƒӃ¤¤†­OllO NN.N(4Rz(Rf"7&S ]3]`BJ#7r`"v((RHS`r`S`\Xu0+ 4RJ7S7RR`9PG9`m:Rf"R(N.N(.Yf7mr\S۾Hr9mIHf( ҫOOӤĐkkOsx _sss5ƒ5.(N.N4#NRf7Hm)]G3\)7#(#79`#fp-)`9\@Xww@9S4C(4J7`r7R(Rr93u@`Sp"R4 RNN(..CR-9m7I"fp`)77m`SOOOӭkkÆl_x __s_5ӃO5OܭO5´.(»NNaf"pm)\]]\S"#C4m7R(R"p7K7r`@wPuXZEZu0`SB%(Rpm%#(#f`]XP@09SIRNNN(()-A-&)-%-É- J..lOOOk†kO5_x. x_slOOOO5slO´.a4(N(.N#I7mS`\3@9SpR(R"2pmS`)0@wEX@`SRYfR(7S\uX@\9pfRN™(#>¼^>ü^^Av OO«OܭÆO5s_x_x x_ss5«ҫҫO5llsOOls_.V4R#(4(N.(R"p7)9@]03`&fR ת"BmS`\`)`9`9r9PZ@`& RfYv2)`u`S"R(N(#4##.bDDAA‰‰ډ-b;lOO5OÆO5s __x__sllllssO_.44R#N4I79\]Xwu9mfv(#f7m)`)m)`9`BS9uwX\S7f(fH`\@Xu@)7J#(.(R4 4=b b b bٴOOOO5OܭOO__x_x x_sss5OOl_.(aRN""7)\u^ ..lOOOO5l5O­Os_x_x.xss_slOl N((#(N"7&m`9P@\9#fS-BmS9\9`SSB0@`SR(Rp9u!^>Db=bD¼>bV. OOl5Ol_s__ x ._lsҫO«OҫO܃Ӄlx.´ .N(( (J%R(#RfI9\)SmfRf7SC»CNCYC(Cv=Br9S"RfS¹`\ur7I7R#RIf((N.b A¼>D=VC‰DA^'։=܃55O5ƒO5_s_sx.x__x._ss55OO­...(f7fNRaf7`&Af7B#RJ"pSmfvVN.(Cû(JS\9)IRJm%f%mS`IfJR#׻(m(ô..= A> =V= bD>ր'b; Ol5OOҫOO5s_ x _ . _s_«OOOOӃOl_.(۟#.(4RfSm%R(R7%R7)S7f*4v#4=4(4*f\`SYf7*C#SfJ#S4( (V= A¼b.Vb D^'=V l5lOOӃO5lsx_x. xssl5OOOOӭl.7J .N(R:%mf(vf#f&SmB7&&7&d\`SIJRfK4Jm"fR#.fY( .VbDÉ.´;V^'=V OlO5Oӭ5lë«55ls_x.x ..xslss5OOOƒ5 7f..#fSf#Yf#R"BmmmS` `)SmS&m9\9SJ#&JRmfR׻fm74N ׻٦ Db;.=^։V.lӭ5lO5ӃOO5ls_x.x.x_sss5OOOOӭӃO .(R7JN.#"`7fRIJ8JI7SmS`9`9`SSm\09S7f4f7ImJ4(´NfBmf(.٦b=.´.;.VbVlӭOsO5lӃOOO_x.x_._ssslO«O­ӭO5_ (#p%(N(n9S7%"&*(Rf`S)`)`9@@ɞ\9`S\u9S7RS9mSJ(N.N"#(N;V;V´..VVV= AC. lOlҫlO­ӃO__x. xs_ss5O5ӃlsxN7J((NRS\9SmS&R#Jf`rmB` 3\ `3`9\SIY#\9`&R(.(N"7f#N..VV;.;¦.Vb AD>A 5OOܭӭ­ls. _ ´ x_ssss5OO«OOӭӭl_.RJ4R4("S`p4f`S7*v=v=*C=-&S0@rf49\9)%(#4("..V;CvV;V;.b AÉA>V; 5ҫlO5lO¤¤¤lx _s_ .xsOlOӃӭ5x.4N4fJ#N(R`\9"fIB`7=.N.(.N=&S0`H47 ]9`7R(N4"fR(4(´;;V=.V٦ *A>>> .x55llOӭƆ­Os__ .x_sslOO5OӭO.´.N#(fJ#"m&Jf9`Bfv4#Y(YY4#"m)\@9pRJ)m%R(NHp7"R(4R(.;V=V;CVb*D>^>bV.x5ӃO5l5l5ҫk†5_sx´xs_ssOOOҫOOOlsx´.N(p7mf.N(#J(#fB`&7777pHBS\`pYRJ#׻.m&7fR(.;.;=;V٦ bAډD blOӭOl55ӭkkO5.´. ssssl5OO5OOO .(R"m( .R8#9SB)`9`9`SrSm9\S7((N(f(N.Vb;VƦ= b= bA lҫlllƒk¤s.._s_ssss5OlOO5´.N(7f R# $NR8"m9`H9 `9\9\`9`9S&9`BJR(Y#R#(K4((N.V bVb b=bAb D‰DxO5ll5«Ok¤Ol5_s_xssssO5l5OO«5ls .(N(fRfpfN#`Sm&"%f"%fnf"7)`m`Sp8J(fI"R(7JN(.;bA¦A>A bD>A >Dv 5ܫll5OӃ†Oll__Ĵ.sssslOl«O_.NN#"(#R")7NR"S9&v(C(V(v-mB`SJ#Y7za(f#NN$N.. ¼b=Ab >DbA‰% 5OO55Oӭ†l_s_ . x_s5OOl«Ol5llsx (N(4Rf&BvN"97J(v((f7`SIf2#4m&"4R#Nb>D= >^bADbbDAbC lOҫO5sl««l__.´x_5_slOë5llx..(((RK&R#797%fR#RfnfJ##RJ7SI4(*&dz(a׻.(..v‰Db=A v vbv. «O5lOӃOl__..5l_l«5l5«5s ((׻NN#p(2Im9r7&7&7Bm&7%7SmfR(&%RN..(b D == DbV=.٦.xl5OlӃӭOO5_x. __ë5s_O5l5O´  N a-n(pr9mr9`9`mp7&K7mIfR(f7IR(N. ..CvCVN(vC.Ĵ.´.«Oҫ5l†OOlx.´x_s_sl5_Ol55s_..((NN#R(aam9`r&m`939`p"I7&IfR:*N(($.(Y#R#4fJfR#C 5l5OOl܃†Ol _x .___s5O5__«lӃӃx.( 4(N((Na#7S9`S`9097%"p7fR4N(N(4((7HBHS9S`\97N llOO5Oҫ x__ ô.x_sOO5s_l5ƒx.(NN(Na"7p7"I7m7f%f"I"%#.((4(.4`r6\0`XPu`J. l5Oӭ5sO¤Ӄ ._s__ .´x__slsl55­O5l5..N(#NN.4RfJ*R#JfRf#׻..N( (NRIH6r0\6r@9HR´ 5llOlOӭܫl_ _s__.´x__sO5s«lOӤ5.N#NNN.(R*R4RJ*JR#R*#4(.NN( ((..R"HSr9\9r69\`H27RsllOÆ55l._sss_.xsӭssl5lOOx.(#(..N ((Y#J*#((NN.N$N#(N.("6r\9SrI`r" 5lll†lOҫ_ 5s_xô.x_ssOs_5lOOO´N#a#(N.NN.(N#R(»N.N. #R#(..JH6r9\9rS6HS9r6JN.«5l5Æl5OOܫsx_5Ol_.slܫs5OӤܫ.N(#N4#fJ# $N((RR(»´CHSH۾6r9rHIIr99` 5«ܭ¤ÆlsllsxlOsx´x _5O5l5lOO5s_ N(#a#R#$N(IfI7Bmp"fN(##RN.v`rH۪Hr9SIr9\09m 5OOlO†ss5«__sO5x´x5OO5lOӭOҫl.N(4#RR#aN(N(aRf"7&m7na#$( #R4..rSBI۾rSI"ɚu@u\J OҫOOk†l_lOx.._sssslOӃOll5l†Ox.N(#R#8(N(*JR4(»N$(#R4$ 7S66f6\0u+B lOOOĭӫssl5lOӭOls .xss5«5ll5O†ҫ_ RR((NNaN(fR#(NaS6IIr@u+u©9fx5Oҫ5O__5O­†Osx´._s5OO5l¤Ol_x´.(4a2:R#N4(N(( #R:#´#S6r"ª690@+u+u4x555«llOܫsOOĆO_x´.xs_slOӆ†O5Oӭ†O5ls..N(#a#R(###(#R#R*R(.R66I"Hr@u+uX`R 5l5~5ӃO5lӭņO_.´xsl܃¤äÆ5sx´N((4#:RR#R:R:RR#4#Y((f66II6r\0u+XuXrYx5l55܃O5OӃ†kĆ­O5~yx.´ xs_ss5ӭ¤†k5sx .N4R#R:JfJfRJ:RRa $.RH6/6IªHSž@u+X+wX`vN55ls«l5OOOOOƒ†kk†l~x.x_s_ss5¤OOkÆO5ls_ N RfJ:JJ*JR#($™C66۪Br90u+XqX\J_l5ҫ5lOOҫOӭdžܫ_´_ss5k†ҫl .(4R2JffJfRR(™.#۾r\0u+uXuXX@(_sl5OOO5l5ll5ҫll5ӭĆa_..x_ssslO«OܭÆҫ_$(4:RJJJ*R4 $NNf¾H¾IIS9\0uX+X+`#´sOlasy~ays܃­__. slssOҫl†¤s__x.N( 4#4YN´N(7rSI۶۪6@@0uX+X+w9R.s5Oҫya:_a__a~5ƒOl_axNslҫ5s_slOlӭ¤ܫs_..N(NNN(N.C4S\9IHI8S`90@+u+Xw9R.sOO5s"f"%"n:4a:~ޏs_a_a ´.xsO5sO†O«l.NCv.´.N(N.#f9u9HI۶IIBr90u+qXw9R´ ҏ6`9`)9 rr)Sdp"yƿzlla_ax´x5OOs5O5lOl55sx.´.´.N4J7 (#(N.Ĵf`u`6۾I2I`90@0@uX9 slҎ609r90]30@30@0]\S"f:__axsOOҫOO5lӃҫ55ls.´™´V;N ٻ(RISP?GKJ#f9fJ#=vCN.N´fJ`0@0\rSH"êIS9\@0u0quSY´5lH@0H¾mm)`)9]@rH"zn~yx(x..x҃ëOOl5ҫ5O5s N(R#R#R4Jf"%77&r\XZ u3\9\w +0uPG3S&%fJfJRv.J9\ɞ`SIIIpr9\00@+uv.sz6S\0r۪f7&-&m&mSmS90\rm:a(xa_N..xsO¤O55OOҫ.N#"f"H)]90\\0uXwu@XwZwZE,i`m-7K7&7N:#7Sr909`rIHIpB6r9žu0Rayf696Sr`r`9@09\]0@0u9SB&S9H"Ra4a_xN .´.sOŤO5sOӃӭӃO«_.NvR"If""fnH`\ɚ]\9\030@\@XZ oiH-B&7Bm%v.#YIHSrr6IHpIpH6r990u0`SI:fH6p69@\SH9`rS9]3@9\P`)m7I6H"Rax.N.Ns5ӤkÆҫls_sOӃӃOl.(#"7"fJf"r9\\9\9@0@\9\u0uPXX\z"7K&BKmS7R´v(#RªHII۾SpIH6r90@307%BRIHISrfRJp7&m&m&m`uX9"fRR׻..*sܤņ†~ss5OOOl(RfIf"IJfRfr9\@09\ɚ0@\0XP9f%-m&m)dm(8#8I"I۾rHIH¾6r90##JpIpmBBHS9)`)9&\u\HY $ô(sӆk†äO~ays_y5O5O_N(RfI*J"ru\\3\9\0\ɞ@XZ7J%7&Bm&7 N#I28Ir9SpI۾6r9YV##RJf`0@9+wZwX^' ٴ(^mB%*vN..(#J%7%7Sm&%=ٴ*%mSB%%JR(N.N..NVRIBm^(a.Nm&J8"&mSmS9`9`RJmdSmmSS`9`)SmfRIS9`mm&7"7S)^&f(N##R(4>^''‰AbVV*-Ÿ%J#(.(R#v4(.´. R4J%7K%b;4*%7K7f*R4.NCv4:#4(.vf%Ÿ&-*N(4.R)7Rf&m)\9`7fmSmSmù)S`99`99SJRpS9`m-7f%pr\`m(.44R4((V=bA^'>Db=* #v(vJ`)mA=..N»(v#R*W=..=*4(N.NvԷGi7C(vY=.#((4NR7r)BRJ7&mS`9`R"mSm`\39\9\9SmS%R%dS&7"9\`7RN##(#; A>A7mm> ´Vvٻ.(#7m9\]3^4 ..Vv=vV..V#=Y...)G<t 7f#(.CvC.N(a84"rS7J8f7&m`)S7*#S&7dmB&)`9\99\9`9S7%8J7&m&m&7f7\9`%vN$#R#N. 7S99rm&-D=...;.(:WK7S\@] 9&º=Nô.»VCV..CVC.ôCR%-SG!i}7 ..;.NaN(4.(I&KffI7p7&7%`)m`9`9\9`9`&J"7I7ff7`]9%(N #J ׻V= A7)0@0@\r&>A..4R A)3G9u9S&A%*Y.N..V..V..N.N.("7SuPi! <3&%A4.´(4#N44N 7m7Ifnf"f2` `S)S`]@]\]\]9`KJŸ•I`)Kn(N(RR (N;=A90@u0@0S¼^¼=..NvCR"m'j93u9m7%7v.»;..C.CJ7%`@Pi}PmJ*#v(.N(R(xR8#"mSSmBmSS9]'`)]uPu=.´(v=R4#R*f1\)mK"fn*R4(...´N*R"JfuG<`JR*RY= ׻. 4R(N4aR(#f&mS` r`9\š]\ ` mS93ǥ\]\\\9r`9`)f=CN ((R#(#NV= %9@\09\r^>^.»(=4R"7 Gj S&ffJ*vN..vf%J7nJ%u' nJ*(V* .((#׻N4(N»v*Jf"I7Hmm7p%B7&m&&p7K7&7&7êI7%JCN$(N#4(4 (;=* % 09099`)^A^ ..(4RJ7Km 3'^&7"%f*J%b(.V A-ԼCR"%f%7-]G'j<}G7f%f#V=CN(#(a((N.N((4#R444Rf"fJR4#4#v4#R#Y(v((.((N(##(($ b`9–099)'^>AD¼DV.NYRJJ=R* W=bD%J*J*J* =b=V..٦= -v4fAPG}P)&7fv V((#(((N$(N.»(((4RJ*J#R((((YY##RR(#(N. -`r09ɢ%>A񵉵NNv4= CCVv4=*#RJ* b=.´.b Dn4(*nmK PGiP K%fvVb(#R#(($N(N(NN(C#RJR#(($N(aR#R4R4(R(4aRN;= ډH9 rmz'>D b b.NN»YR=#4#R(Vv# *#b* *V= b *vWf& iG!i\m&K7JVb(N´™$#((N $N($N($(#RR (((#4#RR#(4(N.b -%nz67A>Abb;´N.(#R*f7H&K7m&R.R*b *b;٦* nYR*f7&K7&)i!iG S&77vV=CN(a4R4( #RNN  # $#R*:#4 ( #R#R#fR4((a#4(N.;b >->>'bb.N.N(#Jf7`9]\3\S7J4 (vbR**=.V=b% Cv7K7m3 , ,i!oP)BI%7"JYC..N4R4:4 ( ((#RY 4RJ#R##4a#( ( 4( #RR(R#4N.V= A>A^^>'> v=A .N.N.(4#J%`uZ !XwX9JRN(**bC= RV4f%7"Ÿ7)t, `S%JfJ*vN´..(R:R(4aJ*8#((4R#RR:J8*R#($4#4#4#R#a4N=bA‰A>^‰>^¦Dv»N4YR*Jf79wT,wXS7fJR4(NCvC=C.v=v(CRf%%J7&\XTϋuS&7JR´N. (4#R((4R##RN(R:fR##RJRfRa( ((((#a(N=A>D>'= DbN(#R*m@EQwXPu9`7fn2fRvN.٦CvC.´(#7fJfwZQt 7nR4(.N.(4:R#N4##4R$(#JR*R#R#4(4aR(N.;=A>‰D^^>>bVb =NN#f&9uwZwP00`m7f&B7f=C.V.V.R7%f%SwZuPu3XwZEX9#.NN# (N(44R#RJRJ%"fRa4a44#a(N((#4(.V=D>A‰'A>^=bV.(Rfm9]3@ɞ9`S9@9`p7& Vٻ.;»;. 7m&Km @Pw `r)`) \]Gu &fR#Y.(4R#(((((R#R#( ((((Y4#4N;VD^>A'D^'D> b=û#*Wf7B \9`mBHp777S\@`SmK^A C.VC.ڼ&S`\@u\`&77m-m)\¥`IJRN..(4RR:R# (( $ R#RR#R( ( RJRN..;VD^>A^DAډA‰ b=¦((4#J7S)9-"J*R4*Im`\ɚ9`m>*CV bV ^&S9u\9`m*RRf&\])J#.N4#RJfJR:#4#8#Rf"fJR###4#RfJR#4(..D^DA‰D ÉAD=V((#*mS)%#(vY(YRf7`9`mBډKڵ:.%bC mS`9S&7fv(CvC4%mS)7%f4.N(4a#:fJR#R#R#Rf7I"fR8R4R#R#a##RJfJJ#4(N..D>Ab A‰A=CC(#f%B7%"*#(RYR#Y(vRJI&mSm7"%b..b%(YJ&&-I(4#R#b(4f%7&K"fR(N´N(#:R:#R*RaJ*RJ*fI7I":R:RR:#RR#(NV bDD A‰Db;v#JRJnJR4(*fJn#YJff" #R=; %(R*"f%*JR#bJfJ7J(v4RfJR׻.´N44R#a#a#aR:RRR*fJIfJRRaRR#aR#4#a#R:#(N.= D=ډ Abb;(C4(Y4((Cv%K%RJJR#vR=#4Y(#R*bv.*R(4##RRf%f*%-%#Y(vYv(.(#(#48a#Ra*Jf"ffJ:R#44#((NbD=>b=A=-D.((YY(YvY#J%JR*J#J*Y(#Y( v4v#R (Yv#=J*#RJf%f%J4(#v(44(N.N#a#4Ra#RJfI"fJ*Ra#Ra44#(N.=D > =٦ CNv#RvC*YRJ#(4R#YCf4C#.((((.(#*4C#Jf= 4*J%*J(.YR(##Y(N(a( a#R*f%JJR#R4 ((.;=Db>Db =bb.»CY#RvC*(R*#(4#Y(Yf4(#Rٻ(((((.(#J4(YJ(4Jf%R*(R=(#R(.´N(4((4#R:JfJJR4R#R4 ((NCˉ>A=AAb=b=CC=V.NV(4v(.Cv..V(ٻ(.4#(4Y.(.N(N(. v4((4N(vRbC(C(´(CN.(v#(N((RffJf"fI"f##4(($N´ оO%biD$cBb)&0ohbetHU3aE-y]M_d3FT,% D4ʦWѥ}O-ݳشxiu|ws<{]<<?<7i\mL/WdPjT4l&kPNK2Ǒ\>임{R#M׮Ŝw7, ֫-EL"k\3ﴜԽ@T#B Ã:xTD\οdL\tD|d_^^s9]T<ɜTĮ᳄~lA4O?H$s.Nw2 4أ*ҥWX,u}<([e_h"27!"Ghve/տ f"bPie04M VfxUI"*"D9"C QUQ6M@u8Sۗꓪ}|HC1iT X?hOַzGZJ"mt]ri̷*ob1F;8 扨<]iafa QmJm;+d70/iB[ZWWg/0+nQ1׾o&ŷ2۽]eq3͛7*`IYhEDLrF*^))*ANS29Um}B`4Ik{E!(L85|/[`)u4;/k|e)X\cfR횦y~:O$" "PʝQyq1#d}- b'rSo(-$"4!svq97&-daIJ?$.1Clpߐ[u`.7dʣa\Rr\=vn{Bbp}T B;6FFF^D^CRȱ}dc4C u=hkR\ᕬtI|;}ihtf]HJL/6jTjx*S$o_3ߘ.fɈk}m2c|yfrl[_et!ڧ0CsX:6Tuc-h/8⏢Z!kbXVɷMq;8Ʌfkh$Ue,[Y_K2KXU6~}r:]DĻkk ٿDٙ]腶w}b7ljUbil&[T9www爟(aZi*(n0ODٻ,HI0mx>UåXkqM-h\o:L嗪&u#όP !h9g0s<: FT 'G!ND4No{;$^.8nw-I0&tC!h=xF۶KQ'[ CfW;?*UYJRWȺŽ"绮;0K{c0i̥2MSA%*E~0%J%UxƮfGN'{iuZ4ٽr(P#.b2/EdpHA`\O{gWT͋/V]MJ 0ȗ #*R!N.dZ2BLD)`dt8LTc߰o}S1ޛidW_}`Jlq}3sd׻zmے_L/yo\.'"v{G}Cӹv"Y?k|]CBfXȾ_Φ9_~ U5"֏lIC`GiM GJ&%ư[7k6UV{o)cp Sǎ[ 9yIKZLMs0 !Hp6C諯J`1b ΀)*yF R>frLsC-2n7ݻp:E5JTqMv᪘:of(Qr?aJ0^Uɩ@P; b# QH>,PGJ@FD\SFYn  &Jjf"M (Q]Q vQQsAX!  ;RqvΫF䚞4ND zAm'偟#%xDṗáۻ}۶mۖ.Ux>D5FK/2qUD8p1,EfL IB-5jsޯ@K4+ s@LRXٙ"Z,q-LQUC`5RLv< SW!HKPQIJSf+ a/?yPQQ11s0UE2c2tyV@DJ&b; L#18EĜ+9#haf#A͊Dgv b *aǔBU{eW"(@ *1a'HjǦmonnOݮ/l`p{wW_4H:ȕO~ -oIg`f&M4b΅(OO ǧx$F#wɆ>i2dR}`~lDDVٵNj/H!ܪ7XIH-P:B2h* {Mf̫ fE>*"br@ȗ α[(H*͑HαDQ4wB% _iT윭~ @ETi#}f*==>BQ F< `Ab#9)Š|$Q foH4V~ILH8 Y:B JlxJbk R8%8X2PFc1$T)ԋ`$*e‚x8mTbU1LbyOsnb 2Av{s,Rv})yoxi~fvLoi`I)5ͱm4Ϫb%0~QVJ `Gsn݌̦s Ob Z1D)D̔,g60hPKI  3Ɍ֎j I$8QJP2r*PXYvDݬ\*C3|>=9rgtN8 8*$nu '䁿1½LD[Nq,wnキJi xxxhv~.a0{B=Vf(9윹BqmXЊIZXâTɸqI>+U 2̂is qP 8 %HJN%s%*)P@>K1Ó ]HDbι A!ahs]׀Qvb_|Eʼnཿ?8pLBeT9{Uel)Ɓqyea&G ͨԂ21e=\>;)җeTL*g M|l58&g*)!] ,%OvSKȜ%&5!!jfƍt8ބ۶)v}xef{[fԯy`#Sbc}Yg_v{f}fft]ٛ7p/_O4cP a 1Hd^eɵ }V5<(5i='&p Vp`YfTRHr.)4a$Q~Ky 3 xa)`t$`6Pf>#I0U B "_`p"9bUeZ3qMav. >lI  ;Z>쳦۶e<<9e(1¿%(F[eCJL!S̚~s+-L^IPX ltwS",ܓN$FL!T4mc,mHۚɎIh1Ɯc&y9GH9NΉ]n`op8bVKyo \\B |%wm۶7i iƲ/矽q>Ox hDaOMPDRTD׌mRV\HI ! %D'W+rUpaѮߝ.0~'"$H-\L0g훦mێCi84Mf9NTHሣI U$P6|(Ѫ1Uܴ4%voiB 5#? z$g7`kw~?o, Wr|:_5D("c HU|3ޞ͛*''jK6+ELUievx\-&D2ylJ}?MrquLe`y1xcwz. qɫrJiYl^2V8 C}sOS}ikpwݻϮV}+v|`K h;O`s<8g͟,S*Qd X%Fhjw](@)>e3妺02Ȳ 1k 犕]Bs M1ڶ=/Q4[:N#;jolbE1 (xD9?)B1KP w,*ٓI?43bshCKTUf<]9ε*\z@[WeuD$~_p:mBկw.!Lr&UAUSI~hiuĤbɹb &ves\!' "˂C9%T7:#OBLcLmqHn.h#[ye}FQ"jGED,cv졘(S3| !`j yL;rblNKbvΑJ$Bxӵ}Ȏ%үI77Nf R-_ԿR{"/IΪjŇ_|Evp?Os&L1BIDf3(QFgU Ί ̴rMNQazClaBDBh|Bu}׶8'y8la3e6]S$A\+Gʍ( 0e-kBli)D%F"ԚeDh4Uڻ#^oi p8G/ҪĴp yv4A$%Q$a* "!bI$Y+K x!% PA-Zn) d4[Qon=>{H52'M 3'*bPi"R*GTV`xKspㅲsR )@7pOmғLQnnoumn?nooƱd^Ec?׌{0#v/޽{ww:o=30??p8Pd ~~C ?ۑlYaXvֈŨ`(J$e%kf/0oy|^8@szs_{<[XF")l hI\VXڣ®!h5^C'e>[!%'r*%e@1DJ&"mwrbn}o޼ڶUX*K|_%2Uˇ;1?~~F3绻;O.c D *Q%XbsT@p<⛑5Ccv lD{)UbRꗟ3}_/=}_E^\Fu9qA3l$0]8JRy=EuX,Bb( T !M !w,\d {|~r6ߠȋۿ/$SWy{O[,|>=4躶{͍?5 Ih`jQg|D$Լh4K,A ghOfb,,WvŞ cXFFә _孒#%|Q@ ι!`^q1xXVHHV%JUyRrlv:IkdCu|ݷߚ2%a{zz}:giK?!ٍLϝ3"~!o/m}8o.ItGcgdwPQ T R)ZfN.a^QgfFIt 2wR6fEsyNo}, R5Β;\oUC2p""Ds,&9w(S9m!e8{A@I994^x{T4Mz(8rfN}q2 Xʦ2KBFB(h ײw4 MjϤXC$icq3;7WZ˖n#Jz,Q/tpptznƼ^D.KWٻI`R J|9a״}1QuV}t:=?1 1?@)7g< "vH]#Q-O"g`'!a2UclsJ20lw; %pJ~ %0AQ7*c\Fm"*,]* W[I]*&KR鶐uV+~D(c4_ȹ BU$޽9O] ;E;[$}Lg z"nd>߳sMr79ό0Nx9I&YS|LT  4 >|8UљlL%ah 8g$4M4" tyWe-;CyYOzn@Yۍqy9qs FMjeڅ>%#zqJLyyߚm4m߷z:=4MV1 e5r3B)wB@SLv@ o(~6-*uL^Ṷ#%sIjt9iu]6(Y/YģD% U`Y;,ǎzSlUMBʕldSVx1iO/{o|18h[%BBQ8{,*~Ӈs}_kqLr_%VFU811`Yaヴ]ҸTP t_8?sx1hоFs>r&w#jL:r.Zf2{"7J`WأM:S5!%q|<`KRIPpb^U*|P{}'M=M=|DFKdy/|Q;A?Ƕk1B</YSCLBTkLFBABdHf瀬|CMӌc*(w~Bȁ%NYҩm3Cj=Mה^ |+?N'Q8+eStScacCHLM {EO`ֽ%!aJĄ%Z-$ t>F수>L8bJOin |E7?>HFݡo޼yzz:nafrnƿ\NiHU5"U%jݬTIR4\Pl2V]p@<+}1LSW:cŹ\(^5(W, UK*CUrLcUnRU!L:C jCx-bq" v\:=+Sh5T%{S *P:ۻ~Զ]i|Au^D.E$?Xت՟KOb\.]oڶDz`4 r"8N3D d5s DUϛ! (+Ѩb.dZ*aa@jGȌ}w1ZL 5Z7?G1kAA qWPrSߢjHd7|T&9(I,Lm1\gX--7Bf ̟27Hd8˳aaBr8cp"Ab((F}MӶrs"onny/e8D"uZ#rr,85gǣJU,LOIqE{OBD]>wV8LRQ%_> ._7" YRK3LcZ]:]*p8P%1{Cv1׊\7[)nb=-!yUM(Ӝx2+.眙V]׷ jm4~CېJ(߇?7Mîqαw 1Ǩ&Qb Ug?D51ҤPrݍ* v.m^,p&9u]&"2#v;{|if6˧]o4H69~? bb)[,sLXյZۙr*1jJ'"8ݡq(CjW,w|>T}^/Pɥ9_2-F]EExj)oB61Tg԰ݛ7o.Nd/| /BPUs4E:9uL`U Io#%N5/2>vݮHB70 p.'k0M ,[a $:XoFHR3K$sy3N8߼֞}*Laۘ)aFS5qaΒrm3EV,ŴѳA)eI,g^~9ƩwN`R][( AQQD/޿߶kޏez ̶|noo smG۶A8bN"~^Kk)1@ޙc̾L tudKK4Gf"f{"4´`j G.$LQY8D$ZBJ$j*+cIy#7d IP6JF̢6,1Є5XYs"֧y5y~YZ0/<( au~߶2LӘD9>o߾m™MZp$.ݦu;l-\fxt!dCO3Skl pRy/e_b5]ToUlgB:SvB:KM0z_u.̩"|b_M(8P"ΩFYN'<>wH_ /]Z[ʼS@؊x2rmM"D WҺEZ=@4y?t]oO4 ޺~||\M25̦`Ҷi1hW WoƁD.8L 0]REeEQ#edUS7r*Uqcz4ɉU{+Z7.O)E2+tZ AsK2IgvLuKX,˸bT,CWZCITNqYC@ kF!J~a$.|RQJ3&(eIeJJ2rwx\D5L{o!v`2P oeIc%,(yUU)eTY5M&4湽9{9%" ]t_ ԈPIy|KS9'܁ձ(=x #a;u:4Ng͝m̯+tHWwKJRkM éDuv Y&PM1s]'ZD0\5~hIoHR4k|o4Jy<]TQĜ L.4Wel(/#dU(s$-6R 1V85751:N8l`Afjj.."xD3?W t)G Qաl:2;9k'+p-vh.?[TTBɱлƣDb}(ͰanooGŐڂ0yb rf( plޮW G" oG3/!bP Fޞj3Mg !4P@EVLX0@\mjEg/ U4 f耊PLtf9WKWL*TqOyց?ay*:3R&b9n;}I%&ͬr877߿#C0B?83J"O*Ab q8v t !)~ "\BȚh2ZfeG`fwCJ)\#0UAYM '$"ʶ*o-8dZ{FhJFZmJaTV ԡ<6oϤT']$IDK; < E258iDcL&FMW>k 6#"l]Ҩx>vr <&5h9U1{q^̟Bʴ53 N/xzCN`c)ĉJ9 Udja#'\_UkNR߸EDݡt/1KIJc)kUKe8wu 0M.j\>f"y(%kgz3 Ʊu$a8cFvde\RrKcT&C~*Js=?xdBtlr6KKL0;uc@dEn&b{ci+^6~HQdn3**/HI "S1TQ%;?CTCo[LYd&kXI-d.%If>O)*`Cot\. 3sHUA*c|@ט8uxʓči4@V$SXSPT*:\j o(Bf@,u,EauM#0SOsj3o_e0lz;[;ܑ%b#XRlBRćϝPj{8^UDBN#Z@L#*ih΢# Ԑ|9BS`oKj2#u}"׶L+oizj|Ed 4R*fѸZ8%T { ~UqOrt9RœTs]2`Rd T&J\>{|.=܅ͯjwJs c,uvK/&@9¾6U}R H44MS6/,yֵVry &[~%DEa,b }aA{wZL+s"/+T9 f_Z2MACd@XгX?%pN]STH檷P_~4孋s u|!M*77]kЪJME ))üDgQ=SϑUTF bHm{Q4sl]$3OEs4U(u ;J:9Syf@f3ۊQ"l ZB&3̟ʟvyxPAZ-{c,+|ݕG,`]OJ[cp-<&Ѩi|zQH4n^H gɪbtj5JbfeTs˥Zkgs.T!ۋNdQUgɔ)lCٸsefy}Uԧ;Uk4lM+X YjrL) UU9EZ89%h5( Sq1tK1^Z4{9Nfh`4NJjQjs;,fI)[-J]y7I`# `3]~̪3KZ?ԹߪAT+a1XgVVxMM^[nV2~Z_N+v4&C=ƘZAϚ.׍14 mٗgߔ֧'VƱ2o]EsD41jIeE bX׈#O:+Vfuk[[~/1FRM\JWH-?98RΐSe3猆eZFh%;@B*hN<3=~,mF3M;U_EI4i΁i!s&֮&wqK`B +&K$mch~ Z Q{Tػ m-iՀ ZerBYtxTd,U=X^ڸ_IV.M|>l^.-ZPU[VV-DZzWE,jIMH%kP'|nӡ sT\r^K(JE*W2i1 |1zbO n2ٯ|٠ D ?`DS,IYw"uQQ"L*0>eMR#*G$vhe%"6 Cj͠$Цؼ\Po KOo&3s?u>! $]$*o:tg)UAL9`kvUyȄu߮зog3R3xWT>-~AoJƆݏ*d]K-Dգ6I~zm=x %%xo%WgMxAr2"qc.%W]ZKpآ9(6 VXiI[Ϣ9.z5UnjhR>l0S8`WZnDa6YhBjtmRrSr6/NTSZEnjTݘBn' !5*V !|8@y(rнW!eyUӓ.\1hNHιc}o۶/|/ݕL+S[]ߊ|.w2=ESh|q_aV. X53'λ)8/Fs~iꌭR Ec׌("8NhDr\;NX@\FU^lsdNX(m[34(-;i401Ƥ^DbR!SuS8q 4~ߗBc$mrz9}KGv!pŦmb71gu-[ȶ¦QF ʝ` $G%49QSح+yk"2Q%/ض~ UpUAuXX1kW[ GI R8XOǏq){5R6uл +_2jT8 3+u1 w΍`c솧im]-U(Ixy.K8E^܇펬ddy^Mzʡ9+i2ӳC1).B9yqǂ%+\aD(~ǾtL 3:V&KIŐwتw+bC 皔X9r(.!2;  oSۉ|yejsh4WA1dF9Bv3__NfH &fm)E!E g8%R+ 1;bsgLQvEi׌9%,Ei9zygCqx8 Bh8_jˑ~G7-İ2J聈HL*EsLH(Ez~?'Aj<% 4!i:[d~KpEԢrs$ ^R5xBh7Ms{WخUj"f!{X^<>=ۻۇۦ!@mzKKĶڔWإN5+(![1 bmhﺦi ijk"u=Iu<"K43CPA5,bi4} Jѡw~/oIrgMA<eDĹ 47dASjzS$v+Qeno*yLy^ z.^#ߡ;IO J }pZ|J!څG!f޲hu578.3<]c !cC+E,1B|zʬ5~+6xqYDX3*`YW^c4jVsw}npM۶cߐ8?.!Wwk^!WL,R9)IWZrV*xZr[@bwݡCwy|:};EaYp\};$TȶQsD5iU y33Op0^JC|o"r m4L."2ȉi( x\EAԗVbknl# }crOoi ST4L k0JRaX=M(z[^b+̩?6辅tPBE-rͳ"Zu&Qpi a rqu()$֌SM՚Ĵܲ@;Ve윶}=?=QG ݷ߆)f`5fRr|.4Nb3@մͯ3]Ե/^(EUUE$b;LOu޲*8Fph%yavq?&QR!3(^Ya~XqW-('9heN֧|ݗl>pm|&Z"K|Hp5Cf9"$*A5:Aoo80 kJ9t\Y=R/-1E|RIoeY sEfyBY?Et/e/'G*s:tʔ\v<) m~Rk}&egUTevHs*e _c (R,:CD&`\c?$n6khhJn)5kZGZYm oy&RY)_V \ĕ@MW%0]U%%Ezmh uZyHX!u_dC^@aqKw(i:Ю~%JPt:*q+@s֡5hd"\ Ohffl͝ OZ$z݉}*8pjVU33RTbuV %](vW>V[reRP2y b@>W!ȭ, 0`ʬ@ kb}+!Hd2-ɎZʆmY.a:|JvA'n-,-/Wg~}^ᄕ~m*ڰ^}Vll~ï<[o]y!%!XHdQ#TdyߛX<*x w3>ެqUC^bQݮKrk54[^묈i,J2#p:c+bWI9t=TUsL QZ CBYмhq%iK:+tUM>b%ō ?T4Χ":k?;x?DYWD$QIr em9?F^ҍPY Z^ϕ k[R+LhHwBZ}|}+KߗRhW>k ^ʤq*މttt}?N5cH g~LՀիBAcA g; s9 MG$i\Z")Hd2g@^S@sքs/ zHp[\=[MXK5 !}u#O=-uLp%dn뜔!:o*R*|ӒF,6L཰kݙs2O E9;5]$irsi'IvqMci vΰ *G5|P>vFhEլ>[Y$B# ֤Gָz<(.S^1d""-`愱cג{]*L!ͷ+6 gM% uΉJn;TݤWeXt<`\6Fid•DQp\ Bu(1m5K\ڔހ2X ̜I #|EMm ڀ#}Zf! -EY8ĽRKhb($-E"* 7}{lӋq^-&PM7,SyKf$걯96d,Dj}ߙ1yo[j&F&1Ι-ܹ̍&cRBSּm sS4`j Wqʿ^yY+}XK rPg"rK>̽MKҋ;_f;-\y W}Z[M|D- "W1({G<]1NV8nNry,JEY|,"PveNܛ9V1{q0Mh7enǛ}6M{ezBnôQ5ŏ^ SÂFEu;uk 򽛺~_%dcڗkYmv>^R%f~8@F| h2^ڈ |JRSD;Ni4"(#vl`Y :F;컦."e²L}-򅘘SW+է2a[ ]8TOzd&Oeʕ^0꽷C6-_%3P֚K^b (udh[pχlCcL%Q=WB)͟=`4|Hp LJP2 z{DnL:拈uo\un˄]s |3TvbhO!"4w'SI)P!aSasJSGk{X+mQeX K)Bk&[_e՟5}/$5-r5rMfJ_ ^eQGW)D)7_WS r{[3 "6HezE824O'+*V㽏o\bo.7Io49u296d4Mo߾N֦%/|!Zkͭ(P}#εδ:ukZ/_OX^VKQ}eBonZtNJFgү8JI4J\^_qҠ9`H!ex|\(Pyb|RyC,XA{u_s#e-PNsig ACk+[{'\g͒8 Ldjp ,i LjO)Smգ 7AZ̼?X* F@[CPZEj%')HijW-V4sd*41A8OC8]mr%X2VWd̵*Ì7RVl:opT{~]RJٺ%\_FyXY"ϧۑ_ޠWA%wem0iBs1yr|%RG4,U})iK7ڶ=9\譛()%IĻ^K@q Vv|XDj:uftZ-\}mUQG?)vJ_|Y6W=:"kN!LR-f*Υu#A{a.A @Irϊ0/k8wa-ڋ堛-nJ__lPԲz)&vk29N@nx+P " nҕ괊XryC$jnN @XfdgV|jyq7p^vgf{Eb&MĿ|eK{%Bpd4o]La"ڿ0=?)`U.k.^rqI\sXtTQZ'F966.@A~,Nz: _i?$rҜMUiyذH|z"s6L6>u2)E .A--E_}ڮ68OAL*2hr GRvV|{TKn%iZ%IV-[ffBT`n9L5*&rr\g2_r KSa5o*X^^Jr}*u5XAq!ѷJC$",c*-±(C+5}Wl+vIJwpМ_cf(rY='cI7tE\"6ZM 5iԒ5}Ҟ ;#Eֶѯj&{2u|9[f+W:dN&M$7KܙV^%cH75Lbi4*C5YYkհIHl '>rz"v6=vŽ8iʼ+o&ɷTSUB֤t +>|LA_xE'\!4l>z#9 ϳe,ګұj6qb^s{R2^[WlW9ODdNnazuFm޵su !Ę՘=3D?&s" @u5ղU*Xɕ7faۧ<Ǭj(Pėz7Oj:'!͆YQ$ )$߹b| 0Om+QREQfGܰ/ T]POԟ9% 8(XȋL?S ؆6n?]uAYpo6 jC|zR~LJt#u5\LKv]yQ[fiODi>{0ꈗ LO#8f&+ҡC~ZpsnbPGXlU}T.z-WI跗8mɫ>w6[Q+P霣9~Re"Y%Z>CEˮ3itMf 0{ Maw'PŬO>eEgKYd ȋRyTQ%屪5蓪2) T!j|3^Cs-Ņ`TB1DdU\++zQeW"I8Bs7-Ơ+ciHm[Y3 1ӣn ?˛xJ:js>^Нoa3@@]|^#mH˱ x* JQh}5{-_ޞn|?y2rvn_ ܁>P ;[-s5t6?N af.ߒƧg3gH %fVP4\$P?1>3'H'_$2;,HE6hX(_enK*ṫ\sŁK^׽W|$P3jT&);Y),l@S踶Nf6Fh ]%a`c۶m[[ύ?nX/~ЭI4UR24pf":zk,h}Kڛm!K|tˢۗV7A[%x_|Y5m'ϕݡЀ;+_;ZG 'lNWLuI4+[RÉJFU-.=j DS%]XJDݽ(({%3d(-Ij]')s7M9l:H$KW$D\cѨk[*i얼kgLL%J:+XpPmC=8 g5UY&:*?(YO .=(,jEE`ӵ I  Mg=S>˺4 VA)ܚo]=W mKGOV ^vm[nΑyRy>v\͹t:Y  ~3{1yJ ڶIΦz%MCsv,iD5I!1sJ3"+ȩyO\~̨mӫOiRM7YD+]Wf^4? /#hUem4n^2py4N8ʕ:d/@DJSC=\=lpy~M/A1uiƾecIWȬk'ax躮[FS3t3Uv|g-skQu`uhKKXZ\/vş/6"m\)=.*{fok2I,._}(]>no$THIz|2Xa&@*r䈛)J gaN9A4WeSem)&3 $+2(],gZ$ĝ$(`+%QJ'$R! ˚"62skA3Bw ̈́n] kd5a26wL#- bS$hVD2&m`Gf6e5 .(Y3IOEUK9-|vX"$F0si@vr:qk0F94Y+Z'dК;K 9dZn|65"5U,%) I"k0B,]j5Gnǯ˷]n(˾+jLu+RzCW<w3~Ιsbi6xWU?huZ )c՝;L9x&NCmߩj㻑/a\GbGD*ƄXg.es˜FQx>Op "hs,7EH,E^jwn  $I5ͮtxyv펁EF.IlgB% UU”%v 42Nӄ:޶lHD$RI#Ԁ[t]=_Ʃ!=3\g֫p4t:w޾ɷ@ DAҹh f'O+-&9~>Oa|E)Jk>UU1~eZS U*Zez ҚJs]vtvw nGk'̡:P^K<%|X H躮SLFYM U81Q6FHP9W3=j+iOOOI|'Uh9Fs؆TEo, D- ׬r!Wu/_nLevßvG$T"O[{CZBe䫪Gr8t7'QUeW=_],$]\ 3L'+ݡZ Q8g.ZE|9*+ ^y `.DoQkcD(hiּiiDD==FGin@{UviT4Wonr9"$B)(1N|^7gbb߲NCysΚjZ#hoF^Z2mhGӵ]yvqeooa:}{5b'1$2IA$"I|vUwW!XPJHjmTR˼FϚv]{uaFv/LqBHSU I}ۼy8_=>CJbHvL4#Z+c;y$ٯ@ptgiTis0E}L+c}um۶m>psscaȢz!1.j$Q Q\u`߫ L VsID Wy"*tl/7?U Mko秧 1041tYcqGJ]88ZeU'eZ_ ~{kvݮv1jABE$ܗ@9%RDU (/N5`.M9 tH%(oxձhfK|QY]&I0?j1)VJL Z䬠\ 0|ΞPkW,n\ ~{PmwK !X~*;y"uJ|Wʌ!qit HXH)77d22dipuU#JlD,~I؜/CT֮Bn%uCsf s%$ՙm3m>jv]df^tnﻔ˜h=8*͎0Â_/)i|Ǩ]Qbqڄ+zL"}(M>3AzztQxLa1Lag0bմs0X:sA4$ "ƹApVHM \nISHMlHXBiVB EJU2] lWhe+XRS û2LdyV}'"X\>J$rq!ڴ6EK|9"(ሉ13%Ďu i68 CA:WRubۦ5T|9y֜u4SBӔ$v h)&JiaU9q4obF<$Pxȉ?%0[_ĕԕOU$ ^Aq4gI'QQ3ER̓R:gAfb;bWH3lIb\<dH]o$~G@uAbAb/Kɭ Pkݶmuq.r(LFYM4Km;LѦ RzbīXM T(*"*`0E*Pf()1{kcs']W2,)%fA&uZv/E,ɾ)O@)KGix ؽU0[کAX뚅hJPEj^=\ Gir !@buhm@Vg@<;FN51u] +[}5{ug%gsD+6 "f_SW$Nq+=fJpP"Blw[ƑiB_뭆WICX1yo$VD秧'081e{T\8=T*t QWiZ6Sxʣ工1lO/DcZ}"" YzN|}%Mf_zM/̝]ݿl R5:AeCƒuGDؙFD<))snCۛcAU??΂[a1R%kZ$1:׈w_+J0{CE]_woBULp\x(RA%V{Z Y%~ZV%YhbnYd&4DZ))9L/,*/69طyT5k?e]ʻ1%SUcjfAEXE^2ATސ:K%".9̻ݮn&F dH rsfEKk.Ixs.sh_ݮ?s1f1BEizjbaPhkjW'fU)W1 uʗ_?:j\hF]7kKMˍwk3d?cI}[=jIi4@<|K%(02d(X(ΗA}+94~&ۛS5Jc $ԊfB9I# Xҕm}$9:E!QK 50gk\j@)}$ qTB`RlJe-`ȕVuUڰ(rBvҤw9"xݾooq*^baa& +m[yc}IY 5-=dpo߾co~s8ܐk{g'7 Y6[k=H[ovHuXj{KhWwXu3M"Gksh"Xrk,|b2 \i5}߽͛{g^vgaJqbm@,4@tevMu}ߧTU 6??=)?X0a$&f4>hf=;VhMq 1]U2fɼ1F̎bgoI3"bt>|*I.&;{CvL)lUh9jrN UԺ2ElndgWXYm |Mw3@P_&4MZ"ggP}QJO9IA@!nc3,n׏΍HJQ"ɱaJfu)^ I0dtϭruHR&y5LȉJ6ur5ƢRN`[9sbl ir_ܾ[0(C,P̥LJ$i(EXϹ^n|ssÔ4m!ܕ3qԏZ9wc)ɰQj0JNa1*ATR_.vTL!(MzQuD S̀UavqwXGC0ˡOIcF`fϾck: mla$vQgt⥌B[H@6)6*Q@AnN 0@P%!ayRj';ik\ n_} 9Ǡob V; w9亨0KHߌD777.Ub ϣ1F(5MDDꛫbN3|Jp9_9ĉS!Sܸ̚3S|piOH$SRJϢ%է/+P^?-ʟ_y0U{[_7fFUu(4C.v 9:/*?m+ r~;9Uev!&Uw]Fk?>X򙑿k:DDnRxd>` 1!.0\ڮ0w~۝ hzz2^sIq2%(M$Z,/fM(4AˢtP+Aҥϲ#W\ol1L 2N$?GTfDx4s3q4̩\8믿>˥o;$\X•[ÏOH `wjf8rL1(J{jg%wnmq-VBD}7M# LѼ7!A婔B #Uz b/-n<)L暤~B*@ Ylcyn? KDa/gg`Ib 7IKFmϾ꫻[":1a,ѿWF]hՏ9>5{xxPJILʧQQ߸9>\4M~p8>mێ(I&sz RWU2,p*jQRK:֢*q.V@7Ƕ8_"u$b?-d Wv/r2ӳ45Hqq)b#sT}.J̚թ8.dqoA{[ڮ}=]Ev΢rsIuOL~ &1 (9XRqf%-ϐnj*%$@ZSM֋iKΊ&kl͕EW e ARu^/Or3UMA1skGK)FQb+X~kV+; ֓c&jnƘ]g.4ϧ)_E> 0H~I6K9H%vm{:_VQZ6_0 E a~ 2 a8$p6|hTA1D)(%6 EƇBo&W nuGog[HAAw+gfnwǻ;taW%2//VUPٿ۞aPMn55g.p+G2{\0/JpLo߽="hE)p{%SNSPN ̋u]mXgT)mc9gWR< 7ݥ9Y KµRS.X Y=p̉ETRPWtRO掰rW7}ή$E(14E(l]J d8Eg-A@ d-[ШߺETl3{%Uཿ߱(VSPqqv S]wJO 0wSч$_۰AAUߜN{ r2Px<M Ȁ;j&D Y鸩xV+0C$E, U4g&;lVsݨ\+RFSѺpB%Ԡ*@^y[ +~ 5ioOa*z{} c՗ϧӮ'ưf΢!??ӨeUg%/"L|ssST*0]Qu(x~~fWtj PVEͭs޻ "DY5bK@LߺIj/3'8v:5iGyݗ72GׄJczFL5'- 86"b ̝@U2^k9GEJYԋ}od `{Gm0#T]C !)D~a ,Կ2~VKɍ;> @v2(jՖ%L: Ӹ:Y9Tb4a!1;w o_mURrT%府aYv]x>=Op[bzDk4Vߵ7m<ܫ߸î9޼yxxzzLZ1$Hk_ zT)fS2P0Sz,J (1з7xs5ov ӛ7o~TXIJpbm );R*Po|y:O1|\I,%gv|L_rATa2yU\,󐘈T n9Hd%AHQ22HAh_t}nBqiSΐ{׀д-P95mv~O8]RLq/zm#IH$EY{9NwWOleY2#Cd %ru\`S$xܸ!^}y~FD`T{26?3FAEU)?TA=]xp*7t  f^$mۚn à*!4øzuu16Rru#Ȧ!yRCL2FcIIb}S̐Y? NSGl*o)i_qd`TtTMЉzgAȈWnwo}?\$b.Lin7HhTVH|äin\,w3o |y p@2J"%UńՒ ^{AeML`%F>B|J 8 #V+lDC퍈"Ad>)Fh x^q?vKFDIZZ#A݂7C^a$*^( %9 F PNhmovu$PbkH* -1#!c۶3,˦b-ˆRi>.}ҏm׉0cUHB?DD .*!w>Rzݏ?h ۔z.f.g8E?H#7J@<#QP|r/ ؎ ɯnG t% RX RSYU&4TIhg(Ռ6pWWW:T9P*<Ǔw:-By/Fl~G\gTHvq~>vЩv-~u*[,PcDǨ BJETX(JIJA%(& Tj$LPM|,a4 \K"H*JpuuoiEmb*ÅĹ9)bZZ.|yvsg8D~bJy@RGVj U L9$"( 4d/R;$f?L18\kIq\.~\8Ɣڶۛ< Hl-!RRXEDt4D̄| Tj|\OHjG@KRn*14McfK蒈 Tժ^fc9< j&RPUx?k$ |3hVH  Q & ed7MgSS գZ͕1bi*ȴdJJqulV$:wtYtPeS sêB]"hp4%B⠖ 3KVyTCKu] Hf`2j<9m @25(Sh}/") u]u5  Dm.7C߽{4rB)IJ_fS3N^׿F^q07#*ST #2Kg_` (v[U],Jy̢|}#_ 9dUyw]gfuӶ*à(c:iL;U\*N;Lg]K`nh9uP̆8Ǫ7~ :#hM[}oOXή?|/v0/Z Z$ǐET?@^$f umG6ۢin\.Aڦ\غD AJpՂWl3JUE/R ""1w$ b=Tإ *z}T\3M (Q,꽩FD 80Dȥ҅W)Єiv w5Bo?+( pU; Dn RQ%>%fƘmvbщ}Jč{Db" ,2Yz;NFzgBAəkhzU _?緯_CnuJfc/803[V{BD #1u*eOވ.!ժjy\,gv{U `"2#o~F'@~+(. n_=*5gATb[m6QX sxU].WmZ0Z$1cmZͣab; S򢎈Ȉ}N!pRBJ*2+!⋒waQЖ뷗|~FvSѬKbEE:*@ 4` my?CӴm۵m\.30 &@Y~pw P͙*QMif孎@}KDcVplb XwӴmۂ8F`REDBha5b{"b)XR'n; ~oGf| :32 6r,A"RȈ6 ^ygn: >_V s#bSJ?e) s~|`O#Aw eʈ[D@UZmy)KH]>\_\2DUaEu]kܰf5Mc8 3 YU(g3`ވ-\(!2_כMuL87oTBMJެ, rJs (eʄ]ӒKr27zivٵ]VkB~*!EnOVju0l5ýGpyTV I eWRE Z@pB{Nх?0DmMoWqR;INk@j|AGM(Rq8qqLc1 픋r-+WDybȚZ'hŌd]s4"o~ݐ53jt-rXٺ)b##0PԅT%EZ[SHP+b]VVH&DyL2K嗓U?mgv T3&PuC1 m_Z@J DDY@aeP]{S9!Ky!8 ̐a̚pDU ! V|6H8/#~e\)dɞoӂ #! GRvщsZ q ~73 %DXOdYI?|@B`Y ^=hj^1R.;yclbvxyycu4'E\ի5QH)M10 DM.GSM:(1zc]l;%Nnj2riZJ3NrTN Ci9E -/VIUaT~c>ֹ smJJdss~x iz :#('uzHZ}VJFS +JT>^J(Ww&P(nRҮJxB{UU"5Y8!lVd+~_b)EDl6I A B%!zu eBDRQmr@ꉂ>4p{^B$~lCDQ< ;)}B [ (QeOܘU*8u(6~L:n?Of5*wYGCmۚ%0#CRw4I1Q2`ΞE L>Tt]cJ.nooMIUӹ)e5y4snvYs71`ԹGÙk)p/Ps`8pZ07D!jfLOD4F aJɓŠ> l/ûu+h1z(|pXR1a1{ D h_Ni4֊@A`%" v)W^nS)2(ȋ<̙Rt#v\.SλEBh)S-$ J`h,v)t5F'"t?2gxPgК]@P.?/ew1~ob"Th % BC@=>i~ԙ8(DU Wcꫯ(9V~fX{0m۴ %Ք̷10 )18HL5X,0#q hI+db|""q߽~T%m-{XHkMBm% 14Yj;3+\tb1 'HPsmjEgeP='}(͌iŰUTADu'MdAX-8vqrQFZβ"V0J"f>U4qqSJ~vkt,sy|BDnooS6EP:@Y|2}E?Kœ[|꜈bDL +Y`4R(Y8r zE?~EQ/KW(>nnnnT͛7H$ְ}:-`Ӑ1Ŷ4imm[d}T~$&3Mv$U\#(o&4]m[+qmnnn̩Hc&ԱqYEwssc"~@ǜ9ſ<_- "lU59Xx Bz[N{yK(~_\\x@Ea<#|ى !L*})qEbTRFsLF1ЄiiWb@ ޏxs{= &iƜڼ@Fe)Rl6.>Qf,X<7o.l49P=j'7V>Q ;C/[O \ ˛pXP_obsE="r4mmJJDWW!&o: K(+r~8ð}ۖe[ᤙK T`xJd#} ~xx p8$CHc [^@D3+)GQ ۅjN~6Er3XUqZY...J k8b9cyVf D %eڹm(Sf#"8+I\CSٮ)}W߇~1]WK`+%Ss;0Ki3T4Ϭ8 o|v'P\Xf/~>~ "bN,(݅T}߮©u r[(n*.R~n&k_Ów"+Jzܡ \b9 u+*( ݺ(!&߿^UI^jpߋZh7TQTfPM֤~37'&`gp1VUK,CL7!Kl~3 ^dD%u#=Ԟ}S~~O;2LDG5Uv6y`j5"S܎Po"wQ9rO?-s}ui SPP'93UasHPE+LQKMjZZ{vk*jVx*%fࣆ6NB~5`szUBHT ?.*'*@}'o'uor66MrB2` ]K-vf3SJDnȏoP3ۜT&kz6~aXȟ,~ _r 5s J p68=>EDԐEUR.UdI!=@ٟf]LBQn4ZOnƽ}*ޚyۑV,ZS$G/(zϞNxQ%/:DA$ JDu-Vn?9Gu~x=h-&L+&mv]kqXm!?U^Wr]9Vo6~,ϙg8>^<_UvAV%!+iO e&17ZJK bX#RI0u,y >n#۴ }1b?WS}}QZ_֎Gj>^z8OS]וdΐsMjbs5_Ceܸzf7_jYvP͢Z6ٳKh\}-Z{ONqX(7gT}ߗdu!E~:f8 bg}TUf)h]~9i&P6ۯ>v|Db@x1 ?uff dUSTooI;}B(^xN4᤮ڶ৚hgz2i,||#g7> 9@`yDheJJ9Ϧz˓OI#U~' [55:wjqF[_>ߨPWó9q-#cbò}+$;%zK_>GbfJxl:֩cmhO{ԯOrFh78sFL(Odꭦ=}QQ kJDD_u|yd~}s۲k<8tr|*EM["̯:jC p9lv7fuNJ(1[N;Ǿx-gNxHTVGH,UWqZ+˘Ĕ֞e^_m[xBb-+bZ$%`@F!FpHMoug%)%C[" ?52~p߷Ml9w{pUG6*pL] ֋Zxp>S2^( PT+ȭL@VňMo-D1"ZCVR$Ei |?#fKO8B5flz}}}so/l JD45Ke5 FDi9G"BE䀔`2i#s: sB5ū8'9lj P@R_V) \?k18PrَW l!*)*]"ET6 hn^IAzt؋2NvJz㴏f c2NaQJ!<6fUq/bB^oY9V a;%^/1/cndWxSꆍuE[_H"vE(* Dҡ#l۲lS_6e~iY.Esjz޺LA~ GDGp#k7ȳ! l琍4f7WMe l,YN5}H:HӒrμX$ Sb>) ;it· y}߃}Tc^e<8_En0NP8 Dh$sұ:Gn7LJ0淞җi ه}̉?ErܗvQaIENDB`doomsday-stable-1.15.7/doomsday/tests/test_glsandbox/testwindow.cpp0000664000175000017500000004417012641367671025117 0ustar jaakkojaakko/* * The Doomsday Engine Project * * Copyright (c) 2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, see . */ #include "testwindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace de; DENG2_PIMPL(TestWindow), DENG2_OBSERVES(Canvas, GLInit), DENG2_OBSERVES(Canvas, GLResize), DENG2_OBSERVES(Clock, TimeChange), DENG2_OBSERVES(Bank, Load) { QToolBar *modelChoice; enum Mode { TestRenderToTexture, TestDynamicAtlas, TestModel }; Mode mode; ImageBank imageBank; Drawable ob; Drawable atlasOb; Matrix4f modelMatrix; Matrix4f projMatrix; GLUniform uMvpMatrix; GLUniform uColor; GLUniform uTime; GLUniform uTex; GLTexture frameTex; GLTexture testpic; ModelDrawable model; ModelDrawable::Animator modelAnim; QScopedPointer modelAtlas; GLUniform uModelTex; GLProgram modelProgram; QScopedPointer atlas; QScopedPointer frameTarget; Time startedAt; Time lastAtlasAdditionAt; bool eraseAtlas; typedef GLBufferT VertexBuf; typedef GLBufferT Vertex2Buf; Instance(Public *i) : Base(i), mode (TestRenderToTexture), //imageBank (0), uMvpMatrix("uMvpMatrix", GLUniform::Mat4), uColor ("uColor", GLUniform::Vec4), uTime ("uTime", GLUniform::Float), uTex ("uTex", GLUniform::Sampler2D), modelAnim (model), uModelTex ("uTex", GLUniform::Sampler2D), atlas (AtlasTexture::newWithRowAllocator(Atlas::AllowDefragment | Atlas::BackingStore | Atlas::WrapBordersInBackingStore)) { // Use this as the main window. setMain(i); self.canvas().audienceForGLInit() += this; self.canvas().audienceForGLResize() += this; Clock::get().audienceForTimeChange() += this; uColor = Vector4f(.5f, .75f, .5f, 1); atlas->setTotalSize(Vector2ui(256, 256)); atlas->setBorderSize(2); atlas->setMagFilter(gl::Nearest); imageBank.add("rtt.cube", "/packs/net.dengine.test.glsandbox/testpic.png"); //imageBank.loadAll(); imageBank.audienceForLoad() += this; //model.load(App::rootFolder().locate("/data/models/marine.md2")); //boblampclean.md5mesh")); modelAtlas.reset(AtlasTexture::newWithKdTreeAllocator(Atlas::DefaultFlags, Atlas::Size(2048, 2048))); model.setAtlas(*modelAtlas); uModelTex = *modelAtlas; } ~Instance() { model.glDeinit(); } void canvasGLInit(Canvas &cv) { try { LOG_DEBUG("GLInit"); glInit(cv); } catch(Error const &er) { qWarning() << er.asText(); QMessageBox::critical(thisPublic, "GL Init Error", er.asText()); exit(1); } } void glInit(Canvas &cv) { // Set up the default state. GLState &st = GLState::current(); st.setBlend(true); st.setBlendFunc(gl::SrcAlpha, gl::OneMinusSrcAlpha); //st.setCull(gl::Back); st.setDepthTest(true); // Textures. testpic.setAutoGenMips(true); imageBank.load("rtt.cube"); //testpic.setImage(imageBank.image("rtt/cube")); //testpic.setImage(QImage(":/images/testpic.png")); testpic.setWrapT(gl::RepeatMirrored); testpic.setMinFilter(gl::Linear, gl::MipLinear); uTex = testpic; // Prepare the custom target. frameTex.setUndefinedImage(Vector2ui(512, 256), Image::RGBA_8888); frameTarget.reset(new GLTarget(frameTex)); // 3D cube. VertexBuf *buf = new VertexBuf; ob.addBuffer(buf); VertexBuf::Type verts[8] = { { Vector3f(-1, -1, -1), Vector2f(0, 0), Vector4f(1, 1, 1, 1) }, { Vector3f( 1, -1, -1), Vector2f(1, 0), Vector4f(1, 1, 0, 1) }, { Vector3f( 1, 1, -1), Vector2f(1, 1), Vector4f(1, 0, 0, 1) }, { Vector3f(-1, 1, -1), Vector2f(0, 1), Vector4f(0, 0, 1, 1) }, { Vector3f(-1, -1, 1), Vector2f(1, 1), Vector4f(1, 1, 1, 1) }, { Vector3f( 1, -1, 1), Vector2f(0, 1), Vector4f(1, 1, 0, 1) }, { Vector3f( 1, 1, 1), Vector2f(0, 0), Vector4f(1, 0, 0, 1) }, { Vector3f(-1, 1, 1), Vector2f(1, 0), Vector4f(0, 0, 1, 1) } }; buf->setVertices(verts, 8, gl::Static); GLBuffer::Indices idx; idx << 0 << 4 << 3 << 7 << 2 << 6 << 1 << 5 << 0 << 4 << 4 << 0 << 0 << 3 << 1 << 2 << 2 << 7 << 7 << 4 << 6 << 5; buf->setIndices(gl::TriangleStrip, idx, gl::Static); ob.program().build( ByteRefArray::fromCStr( "uniform highp mat4 uMvpMatrix;\n" "uniform highp vec4 uColor;\n" "uniform highp float uTime;\n" "attribute highp vec4 aVertex;\n" "attribute highp vec2 aUV;\n" "attribute highp vec4 aColor;\n" "varying highp vec2 vUV;\n" "varying highp vec4 vColor;\n" "void main(void) {\n" " gl_Position = uMvpMatrix * aVertex;\n" " vUV = aUV + vec2(uTime/10.0, 0.0);\n" " vColor = aColor + vec4(sin(uTime), cos(uTime), " "sin(uTime), cos(uTime)*0.5) * uColor;\n" "}\n"), ByteRefArray::fromCStr( "uniform sampler2D uTex;\n" "varying highp vec2 vUV;\n" "varying highp vec4 vColor;\n" "void main(void) {\n" " highp vec4 color = texture2D(uTex, vUV);\n" " if(color.a < 0.05) discard;\n" " gl_FragColor = color * vColor;\n" "}")) << uMvpMatrix << uColor << uTime << uTex; // Require testpic to be ready before rendering the cube. ob += testpic; // The atlas objects. Vertex2Buf *buf2 = new Vertex2Buf; Vertex2Buf::Type verts2[4] = { { Vector2f(0, 0), Vector2f(0, 0) }, { Vector2f(100, 0), Vector2f(1, 0) }, { Vector2f(100, 100), Vector2f(1, 1) }, { Vector2f(0, 100), Vector2f(0, 1) } }; buf2->setVertices(gl::TriangleFan, verts2, 4, gl::Static); atlasOb.addBuffer(buf2); atlasOb.program().build( ByteRefArray::fromCStr( "uniform highp mat4 uMvpMatrix;\n" "attribute highp vec4 aVertex;\n" "attribute highp vec2 aUV;\n" "varying highp vec2 vUV;\n" "void main(void) {\n" " gl_Position = uMvpMatrix * aVertex;\n" " vUV = aUV;\n" "}\n"), ByteRefArray::fromCStr( "uniform sampler2D uTex;\n" "varying highp vec2 vUV;\n" "void main(void) {\n" " gl_FragColor = texture2D(uTex, vUV);\n" "}\n")) << uMvpMatrix // note: uniforms shared between programs << uTex; cv.renderTarget().setClearColor(Vector4f(.2f, .2f, .2f, 0)); modelProgram.build( ByteRefArray::fromCStr( "uniform highp mat4 uMvpMatrix;\n" "uniform highp vec4 uColor;\n" "uniform highp mat4 uBoneMatrices[64];\n" "attribute highp vec4 aVertex;\n" "attribute highp vec3 aNormal;\n" "attribute highp vec2 aUV;\n" "attribute highp vec4 aBounds;\n" "attribute highp vec4 aColor;\n" "attribute highp vec4 aBoneIDs;\n" "attribute highp vec4 aBoneWeights;\n" "varying highp vec2 vUV;\n" "varying highp vec4 vColor;\n" "varying highp vec3 vNormal;\n" "void main(void) {\n" " highp mat4 bone =\n" " uBoneMatrices[int(aBoneIDs.x + 0.5)] * aBoneWeights.x + \n" " uBoneMatrices[int(aBoneIDs.y + 0.5)] * aBoneWeights.y + \n" " uBoneMatrices[int(aBoneIDs.z + 0.5)] * aBoneWeights.z + \n" " uBoneMatrices[int(aBoneIDs.w + 0.5)] * aBoneWeights.w;\n" " highp vec4 modelPos = bone * aVertex;\n" " gl_Position = uMvpMatrix * modelPos;\n" " vUV = aBounds.xy + aUV * aBounds.zw;\n" " vColor = aColor;\n" " vNormal = (bone * vec4(aNormal, 0.0)).xyz;\n" "}\n"), ByteRefArray::fromCStr( "uniform sampler2D uTex;\n" "varying highp vec2 vUV;\n" "varying highp vec3 vNormal;\n" "void main(void) {\n" " gl_FragColor = texture2D(uTex, vUV) * " "vec4(vec3((vNormal.x + 1.0) / 2.0), 1.0);" "}\n")) << uMvpMatrix << uModelTex; model.setProgram(modelProgram); } void bankLoaded(DotPath const &path) { LOG_RES_NOTE("Bank item \"%s\" loaded") << path; if(path == "rtt.cube") { DENG2_ASSERT_IN_MAIN_THREAD(); self.canvas().makeCurrent(); testpic.setImage(imageBank.image(path)); //self.canvas().doneCurrent(); imageBank.unload(path); } } void canvasGLResized(Canvas &cv) { LOG_GL_VERBOSE("GLResized: %i x %i") << cv.width() << cv.height(); GLState &st = GLState::current(); //st.setViewport(Rectangleui::fromSize(cv.size())); st.setViewport(Rectangleui(0, 0, cv.width(), cv.height())); updateProjection(cv); } void updateProjection(Canvas &cv) { switch(mode) { case TestRenderToTexture: // 3D projection. projMatrix = Matrix4f::perspective(40, float(cv.width())/float(cv.height())) * Matrix4f::lookAt(Vector3f(), Vector3f(0, 0, -5), Vector3f(0, -1, 0)); break; case TestDynamicAtlas: // 2D projection. uMvpMatrix = projMatrix = Matrix4f::ortho(-cv.width()/2, cv.width()/2, -cv.height()/2, cv.height()/2) * Matrix4f::scale(cv.height()/150.f) * Matrix4f::translate(Vector2f(-50, -50)); break; case TestModel: // 3D projection. projMatrix = Matrix4f::perspective(40, float(cv.width())/float(cv.height())) * Matrix4f::lookAt(Vector3f(), Vector3f(0, -3, 0), Vector3f(0, 0, 1)); break; } } void setMode(Mode newMode) { mode = newMode; updateProjection(self.canvas()); modelChoice->hide(); switch(mode) { case TestDynamicAtlas: lastAtlasAdditionAt = Time(); uMvpMatrix = projMatrix; break; case TestModel: modelChoice->show(); break; default: break; } } void draw(Canvas &) { switch(mode) { case TestRenderToTexture: // First render the frame to the texture. GLState::push() .setTarget(*frameTarget) .setViewport(Rectangleui::fromSize(frameTex.size())); drawRttFrame(); GLState::pop(); // Render normally. drawRttFrame(); break; case TestDynamicAtlas: GLState::push().setBlend(false); drawAtlasFrame(); GLState::pop(); break; case TestModel: drawModel(); break; } } void drawRttFrame() { GLState::current().target().clear(GLTarget::ColorDepth); // The left cube. uTex = testpic; uMvpMatrix = projMatrix * Matrix4f::translate(Vector3f(-1.5f, 0, 0)) * modelMatrix; ob.draw(); // The right cube. uTex = frameTex; uMvpMatrix = projMatrix * Matrix4f::translate(Vector3f(1.5f, 0, 0)) * modelMatrix; ob.draw(); } void drawAtlasFrame() { GLState::current().target().clear(GLTarget::ColorDepth); uTex = *atlas; atlasOb.draw(); } void initModelAnimation() { modelAnim.clear(); modelAnim.start(0); } void drawModel() { GLState::current().target().clear(GLTarget::ColorDepth); uMvpMatrix = projMatrix * modelMatrix; if(!modelAnim.isEmpty()) { modelAnim.at(0).time = startedAt.since(); } model.draw(&modelAnim); } void timeChanged(Clock const &clock) { if(!startedAt.isValid()) { startedAt = clock.time(); } uTime = startedAt.since(); switch(mode) { case TestRenderToTexture: modelMatrix = Matrix4f::rotate(std::cos(uTime.toFloat()/2) * 45, Vector3f(1, 0, 0)) * Matrix4f::rotate(std::sin(uTime.toFloat()/3) * 60, Vector3f(0, 1, 0)); break; case TestModel: modelMatrix = Matrix4f::translate(Vector3f(0, std::cos(uTime.toFloat()/2.5f), 0)) * Matrix4f::rotate(std::cos(uTime.toFloat()/2) * 45, Vector3f(1, 0, 0)) * Matrix4f::rotate(std::sin(uTime.toFloat()/3) * 60, Vector3f(0, 1, 0)) * Matrix4f::scale(3.f / de::max(model.dimensions().x, model.dimensions().y, model.dimensions().z)) * Matrix4f::translate(-model.midPoint()); break; case TestDynamicAtlas: if(lastAtlasAdditionAt.since() > 0.2) { lastAtlasAdditionAt = Time(); nextAtlasAlloc(); } break; } self.canvas().update(); } void nextAtlasAlloc() { if(eraseAtlas) { atlas->clear(); eraseAtlas = false; return; } #if 1 if((qrand() % 10) <= 5 && !atlas->isEmpty()) { // Randomly remove one of the allocations. QList ids; foreach(Id const &id, atlas->allImages()) ids << id; Id chosen = ids[qrand() % ids.size()]; atlas->release(chosen); LOG_DEBUG("Removed ") << chosen; } #endif // Generate a random image. QSize imgSize(10 + qrand() % 40, 10 + 10 * (qrand() % 2)); QImage img(imgSize, QImage::Format_ARGB32); QPainter painter(&img); painter.fillRect(img.rect(), QColor(qrand() % 256, qrand() % 256, qrand() % 256)); painter.setPen(Qt::white); painter.drawRect(img.rect().adjusted(0, 0, -1, -1)); Id id = atlas->alloc(img); LOG_DEBUG("Allocated ") << id; if(id.isNone()) { lastAtlasAdditionAt = Time() + 5.0; // Erase the entire atlas. eraseAtlas = true; } } }; TestWindow::TestWindow() : d(new Instance(this)) { qsrand(Time().asDateTime().toTime_t()); setWindowTitle("libgui GL Sandbox"); setMinimumSize(640, 480); QToolBar *tools = addToolBar(tr("Tests")); tools->addAction("RTT", this, SLOT(testRenderToTexture())); tools->addAction("Atlas", this, SLOT(testDynamicAtlas())); tools->addAction("Model", this, SLOT(testModel())); d->modelChoice = addToolBar(tr("Models")); d->modelChoice->addAction("MD2", this, SLOT(loadMD2Model())); d->modelChoice->addAction("MD5", this, SLOT(loadMD5Model())); //addToolBar(Qt::TopToolBarArea, d->modelChoice); //d->modelChoice->hide(); } void TestWindow::canvasGLDraw(Canvas &canvas) { LIBGUI_ASSERT_GL_OK(); d->draw(canvas); canvas.swapBuffers(); CanvasWindow::canvasGLDraw(canvas); } void TestWindow::testRenderToTexture() { d->setMode(Instance::TestRenderToTexture); } void TestWindow::testDynamicAtlas() { d->setMode(Instance::TestDynamicAtlas); } void TestWindow::testModel() { d->setMode(Instance::TestModel); } void TestWindow::loadMD2Model() { d->model.load(App::rootFolder().locate("/packs/net.dengine.test.glsandbox/models/marine.md2")); d->initModelAnimation(); } void TestWindow::loadMD5Model() { d->model.load(App::rootFolder().locate("/packs/net.dengine.test.glsandbox/models/boblampclean.md5mesh")); d->initModelAnimation(); } doomsday-stable-1.15.7/doomsday/tests/test_glsandbox/testwindow.h0000664000175000017500000000224412641367671024560 0ustar jaakkojaakko/* * The Doomsday Engine Project * * Copyright (c) 2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, see . */ #ifndef GLSANDBOX_TESTWINDOW_H #define GLSANDBOX_TESTWINDOW_H #include class TestWindow : public de::CanvasWindow { Q_OBJECT public: TestWindow(); void canvasGLDraw(de::Canvas &canvas); public slots: void testRenderToTexture(); void testDynamicAtlas(); void testModel(); void loadMD2Model(); void loadMD5Model(); private: DENG2_PRIVATE(d) }; #endif // GLSANDBOX_TESTWINDOW_H doomsday-stable-1.15.7/doomsday/tests/test_archive/0000775000175000017500000000000012641367671021636 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/tests/test_archive/main.cpp0000664000175000017500000001256612641367671023300 0ustar jaakkojaakko/* * The Doomsday Engine Project * * Copyright (c) 2009-2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include using namespace de; int main(int argc, char **argv) { try { TextApp app(argc, argv); app.initSubsystems(App::DisablePlugins); Block b; Writer(b, littleEndianByteOrder) << duint32(0x11223344); duint32 v; Reader(b, littleEndianByteOrder) >> v; LOG_MSG("%x") << v; Folder &zip = app.fileSystem().find("test.zip"); LOG_MSG("Here's test.zip's info:\n") << zip.info(); LOG_MSG("Root folder's info:\n") << app.rootFolder().info(); LOG_MSG ("General description: %s") << zip.description(); LOG_VERBOSE("Verbose description: %s") << zip.description(); LOGDEV_MSG ("Developer description: %s") << zip.description(); File const &hello = zip.locate("hello.txt"); File::Status stats = hello.status(); LOG_MSG("hello.txt size: %i bytes, modified at %s") << stats.size << Date(stats.modifiedAt); String content = String::fromUtf8(Block(hello)); LOG_MSG("The contents: \"%s\"") << content; try { // Make a second entry. File &worldTxt = zip.newFile("world.txt"); Writer(worldTxt) << FixedByteArray(content.toUtf8()); } catch(File::OutputError const &er) { LOG_WARNING("Cannot change files in read-only mode:\n") << er.asText(); } // test2.zip won't appear in the file system as a folder unless // FS::refresh() is called. newFile() doesn't interpret anything, just // makes a plain file. File &zip2 = app.homeFolder().replaceFile("test2.zip"); zip2.setMode(File::Write | File::Truncate); ZipArchive arch; arch.add(Path("world.txt"), content.toUtf8()); Writer(zip2) << arch; LOG_MSG("Wrote ") << zip2.path(); LOG_MSG("") << zip2.info(); LOG_MSG ("General description: %s") << zip2.description(); LOG_VERBOSE("Verbose description: %s") << zip2.description(); LOGDEV_MSG ("Developer description: %s") << zip2.description(); // Manual reinterpretation can be requested. DENG2_ASSERT(zip2.parent() != 0); Folder &updated = zip2.reinterpret()->as(); DENG2_ASSERT(!zip2.parent()); // became a source // This should now be a package folder so let's fill it with the archive // contents. updated.populate(); LOG_MSG("After reinterpretation: %s with path %s") << updated.description() << updated.path(); LOG_MSG("Trying to get folder ") << updated.path() / "subtest"; Folder &subFolder = App::fileSystem().makeFolder(updated.path() / "subtest"); Writer(subFolder.replaceFile("world2.txt")) << content.toUtf8(); Writer(subFolder.replaceFile("world3.txt")) << content.toUtf8(); Writer(subFolder.replaceFile("world2.txt")) << content.toUtf8(); Writer(subFolder.replaceFile("world2.txt")) << content.toUtf8(); Writer(subFolder.replaceFile("world3.txt")) << content.toUtf8(); try { File &denied = subFolder.locate("world3.txt"); denied.setMode(File::ReadOnly); Writer(denied) << content.toUtf8(); } catch(Error const &er) { LOG_MSG("Correctly denied access to read-only file within archive: %s") << er.asText(); } LOG_MSG("Contents of subtest folder:\n") << updated.locate("subtest").contentsAsText(); LOG_MSG("Before flushing:\n") << app.homeFolder().contentsAsText(); TimeDelta(0.5).sleep(); // make the time difference clearer // Changes were made to the archive via files. The archive won't be // written back to its source file until the ArchiveFolder instance // is deleted or when a flush is done. updated.flush(); LOG_MSG("After flushing:\n") << app.homeFolder().contentsAsText(); App::fileSystem().copySerialized(updated.path(), "home/copied.zip", FS::PlainFileCopy); LOG_MSG("Plain copy: ") << App::rootFolder().locate("home/copied.zip").description(); App::fileSystem().copySerialized(updated.path(), "home/copied.zip"); LOG_MSG("Normal copy: ") << App::rootFolder().locate("home/copied.zip").description(); } catch(Error const &err) { qWarning() << err.asText(); } qDebug() << "Exiting main()..."; return 0; } doomsday-stable-1.15.7/doomsday/tests/test_archive/test_archive.pro0000664000175000017500000000050112641367671025034 0ustar jaakkojaakkoinclude(../config_test.pri) TEMPLATE = app TARGET = test_archive SOURCES += main.cpp OTHER_FILES += hello.txt deployTest($$TARGET) archived.files = test.zip hello.txt archived.path = $$DENG_DATA_DIR macx { archived.path = Contents/Resources QMAKE_BUNDLE_DATA += archived } else { INSTALLS += archived } doomsday-stable-1.15.7/doomsday/tests/test_archive/test.zip0000664000175000017500000000027212641367671023342 0ustar jaakkojaakkoPKr:`$ hello.txtUT elJelJUxHWHIMI,IMQ(/IQ( n:DPKr:`$ hello.txtUTelJUxPKD`doomsday-stable-1.15.7/doomsday/tests/test_archive/hello.txt0000664000175000017500000000141012641367671023476 0ustar jaakkojaakkoHello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! Hello deflated world! doomsday-stable-1.15.7/doomsday/libappfw/0000775000175000017500000000000012641367670017617 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libappfw/libappfw.pro0000664000175000017500000002021712641367670022147 0ustar jaakkojaakko# The Doomsday Engine Project -- GUI Application Framework # Copyright (c) 2014 Jaakko Keränen # # This program is distributed under the GNU Lesser General Public License # version 3 (or, at your option, any later version). Please visit # http://www.gnu.org/licenses/lgpl.html for details. include(../config.pri) TEMPLATE = lib TARGET = deng_appfw VERSION = $$DENG_VERSION CONFIG += deng_qtgui deng_qtopengl include(../dep_core.pri) include(../dep_shell.pri) include(../dep_gui.pri) include(../dep_opengl.pri) include(../dep_ovr.pri) DEFINES += __LIBAPPFW__ INCLUDEPATH += include win32 { # Keep the version number out of the file name. TARGET_EXT = .dll } PRECOMPILED_HEADER = src/precompiled.h HEADERS += src/precompiled.h # Public headers. publicHeaders(root, \ include/de/AtlasProceduralImage \ include/de/AuxButtonWidget \ include/de/BaseGuiApp \ include/de/BaseWindow \ include/de/BlurWidget \ include/de/ButtonWidget \ include/de/ChildWidgetOrganizer \ include/de/ChoiceWidget \ include/de/CommandWidget \ include/de/CompositorWidget \ include/de/DialogContentStylist \ include/de/DialogWidget \ include/de/DocumentWidget \ include/de/DocumentPopupWidget \ include/de/FoldPanelWidget \ include/de/FontLineWrapping \ include/de/framework \ include/de/GLTextComposer \ include/de/GridLayout \ include/de/GridPopupWidget \ include/de/GuiRootWidget \ include/de/GuiWidget \ include/de/InputDialog \ include/de/IPersistent \ include/de/LabelWidget \ include/de/LineEditWidget \ include/de/LogWidget \ include/de/MenuWidget \ include/de/MessageDialog \ include/de/NotificationAreaWidget \ include/de/OculusRift \ include/de/PanelWidget \ include/de/PersistentState \ include/de/PopupMenuWidget \ include/de/PopupWidget \ include/de/ProceduralImage \ include/de/ProgressWidget \ include/de/RelayWidget \ include/de/ScriptCommandWidget \ include/de/ScrollAreaWidget \ include/de/SequentialLayout \ include/de/SignalAction \ include/de/SliderWidget \ include/de/Style \ include/de/StyleProceduralImage \ include/de/TabWidget \ include/de/TextDrawable \ include/de/ToggleWidget \ include/de/Untrapper \ include/de/VRWindowTransform \ include/de/WindowSystem \ include/de/WindowTransform \ include/de/VariableChoiceWidget \ include/de/VariableLineEditWidget \ include/de/VariableSliderWidget \ include/de/VariableToggleWidget \ include/de/VRConfig \ \ include/de/libappfw.h \ ) publicHeaders(ui, \ include/de/ui/ActionItem \ include/de/ui/Data \ include/de/ui/ImageItem \ include/de/ui/Item \ include/de/ui/ListData \ include/de/ui/Margins \ include/de/ui/Stylist \ include/de/ui/SubmenuItem \ include/de/ui/SubwidgetItem \ include/de/ui/VariableToggleItem \ \ include/de/ui/defs.h \ ) publicHeaders(framework, \ include/de/framework/actionitem.h \ include/de/framework/atlasproceduralimage.h \ include/de/framework/baseguiapp.h \ include/de/framework/basewindow.h \ include/de/framework/childwidgetorganizer.h \ include/de/framework/data.h \ include/de/framework/dialogcontentstylist.h \ include/de/framework/fontlinewrapping.h \ include/de/framework/gltextcomposer.h \ include/de/framework/gridlayout.h \ include/de/framework/guirootwidget.h \ include/de/framework/guiwidget.h \ include/de/framework/guiwidgetprivate.h \ include/de/framework/imageitem.h \ include/de/framework/item.h \ include/de/framework/ipersistent.h \ include/de/framework/listdata.h \ include/de/framework/margins.h \ include/de/framework/persistentstate.h \ include/de/framework/proceduralimage.h \ include/de/framework/sequentiallayout.h \ include/de/framework/signalaction.h \ include/de/framework/style.h \ include/de/framework/styleproceduralimage.h \ include/de/framework/stylist.h \ include/de/framework/submenuitem.h \ include/de/framework/subwidgetitem.h \ include/de/framework/textdrawable.h \ include/de/framework/untrapper.h \ include/de/framework/variabletoggleitem.h \ include/de/framework/vrwindowtransform.h \ include/de/framework/windowsystem.h \ include/de/framework/windowtransform.h \ ) publicHeaders(vr, \ include/de/vr/oculusrift.h \ include/de/vr/vrconfig.h \ ) publicHeaders(widgets, \ include/de/widgets/auxbuttonwidget.h \ include/de/widgets/blurwidget.h \ include/de/widgets/buttonwidget.h \ include/de/widgets/choicewidget.h \ include/de/widgets/commandwidget.h \ include/de/widgets/compositorwidget.h \ include/de/widgets/dialogwidget.h \ include/de/widgets/documentwidget.h \ include/de/widgets/documentpopupwidget.h \ include/de/widgets/foldpanelwidget.h \ include/de/widgets/gridpopupwidget.h \ include/de/widgets/labelwidget.h \ include/de/widgets/lineeditwidget.h \ include/de/widgets/logwidget.h \ include/de/widgets/menuwidget.h \ include/de/widgets/notificationareawidget.h \ include/de/widgets/panelwidget.h \ include/de/widgets/popupmenuwidget.h \ include/de/widgets/popupwidget.h \ include/de/widgets/progresswidget.h \ include/de/widgets/relaywidget.h \ include/de/widgets/scriptcommandwidget.h \ include/de/widgets/scrollareawidget.h \ include/de/widgets/sliderwidget.h \ include/de/widgets/tabwidget.h \ include/de/widgets/togglewidget.h \ include/de/widgets/variablechoicewidget.h \ include/de/widgets/variablelineeditwidget.h \ include/de/widgets/variablesliderwidget.h \ include/de/widgets/variabletogglewidget.h \ ) publicHeaders(dialogs, \ include/de/dialogs/inputdialog.h \ include/de/dialogs/messagedialog.h \ ) # Sources and private headers. SOURCES += \ src/baseguiapp.cpp \ src/basewindow.cpp \ src/childwidgetorganizer.cpp \ src/data.cpp \ src/dialogcontentstylist.cpp \ src/dialogs/inputdialog.cpp \ src/dialogs/messagedialog.cpp \ src/fontlinewrapping.cpp \ src/gltextcomposer.cpp \ src/gridlayout.cpp \ src/guirootwidget.cpp \ src/guiwidget.cpp \ src/item.cpp \ src/listdata.cpp \ src/margins.cpp \ src/persistentstate.cpp \ src/proceduralimage.cpp \ src/sequentiallayout.cpp \ src/signalaction.cpp \ src/style.cpp \ src/textdrawable.cpp \ src/untrapper.cpp \ src/vrwindowtransform.cpp \ src/vr/oculusrift.cpp \ src/vr/vrconfig.cpp \ src/widgets/auxbuttonwidget.cpp \ src/widgets/blurwidget.cpp \ src/widgets/buttonwidget.cpp \ src/widgets/choicewidget.cpp \ src/widgets/commandwidget.cpp \ src/widgets/compositorwidget.cpp \ src/widgets/dialogwidget.cpp \ src/widgets/documentwidget.cpp \ src/widgets/documentpopupwidget.cpp \ src/widgets/foldpanelwidget.cpp \ src/widgets/gridpopupwidget.cpp \ src/widgets/labelwidget.cpp \ src/widgets/lineeditwidget.cpp \ src/widgets/logwidget.cpp \ src/widgets/menuwidget.cpp \ src/widgets/notificationareawidget.cpp \ src/widgets/panelwidget.cpp \ src/widgets/popupmenuwidget.cpp \ src/widgets/popupwidget.cpp \ src/widgets/progresswidget.cpp \ src/widgets/relaywidget.cpp \ src/widgets/scriptcommandwidget.cpp \ src/widgets/scrollareawidget.cpp \ src/widgets/sliderwidget.cpp \ src/widgets/tabwidget.cpp \ src/widgets/togglewidget.cpp \ src/widgets/variablechoicewidget.cpp \ src/widgets/variablelineeditwidget.cpp \ src/widgets/variablesliderwidget.cpp \ src/widgets/variabletogglewidget.cpp \ src/windowsystem.cpp \ src/windowtransform.cpp # Installation --------------------------------------------------------------- macx { xcodeFinalizeBuild($$TARGET) linkDylibToBundledLibcore(libdeng_appfw) doPostLink("install_name_tool -id @rpath/libdeng_appfw.1.dylib libdeng_appfw.1.dylib") # Update the library included in the main app bundle. doPostLink("mkdir -p ../client/Doomsday.app/Contents/Frameworks") doPostLink("cp -fRp libdeng_appfw*dylib ../client/Doomsday.app/Contents/Frameworks") } deployLibrary() doomsday-stable-1.15.7/doomsday/libappfw/appfw.doxy0000664000175000017500000000206712641367670021646 0ustar jaakkojaakko# Public API documentation for libappfw @INCLUDE = ../doomsday.doxy PROJECT_NAME = "libappfw" PROJECT_NUMBER = 1.15.7 PROJECT_BRIEF = "Application Framework" OUTPUT_DIRECTORY = ../apidoc/appfw/ INPUT = include src FILE_PATTERNS = * EXCLUDE_PATTERNS = .DS_Store PREDEFINED = __cplusplus __LIBAPPFW__ \ "DENG2_PIMPL(ClassName)=typedef ClassName Public; struct ClassName::Instance : public de::Private" \ "DENG2_PIMPL_NOREF(C)=struct C::Instance : public de::IPrivate" \ "DENG2_PRIVATE(Var)=struct Instance; Instance *Var;" \ "DENG2_ERROR(N)=class N : public de::Error {};" \ "DENG2_SUB_ERROR(B,N)=class N : public B {};" INCLUDED_BY_GRAPH = NO COLLABORATION_GRAPH = NO REFERENCED_BY_RELATION = NO OPTIMIZE_OUTPUT_FOR_C = NO MACRO_EXPANSION = YES EXPAND_ONLY_PREDEF = NO INTERNAL_DOCS = NO doomsday-stable-1.15.7/doomsday/libappfw/include/0000775000175000017500000000000012641367670021242 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libappfw/include/de/0000775000175000017500000000000012641367670021632 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libappfw/include/de/ToggleWidget0000664000175000017500000000004312641367670024137 0ustar jaakkojaakko#include "widgets/togglewidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/vr/0000775000175000017500000000000012641367670022261 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libappfw/include/de/vr/oculusrift.h0000664000175000017500000001017712641367670024637 0ustar jaakkojaakko/** @file oculusrift.h * * @authors Copyright (c) 2014 Jaakko Keränen * @authors Copyright (c) 2013 Christopher Bruns * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_OCULUSRIFT_H #define LIBAPPFW_OCULUSRIFT_H #include "../libappfw.h" #include #include namespace de { /** * Oculus Rift configuration and head tracking. * * @ingroup vr */ class LIBAPPFW_PUBLIC OculusRift { public: enum Eye { LeftEye, RightEye }; enum Screen { DefaultScreen, PreviousScreen, HMDScreen }; public: OculusRift(); /** * Checks if the Oculus Rift functionality is available. This returns * @c true if LibOVR is enabled in the build. */ bool isEnabled() const; void glPreInit(); /** * Checks if a HMD is connected at the moment. */ bool isHMDConnected() const; /** * Initialize Oculus Rift rendering. The main window must exist and be visible. * This should be called whenever the Oculus Rift rendering mode is enabled. */ void init(); void deinit(); void beginFrame(); void endFrame(); /** * Sets the currently rendered eye. LibOVR will decide which eye is left and which * is right. * * @param index Eye index (0 or 1). */ void setCurrentEye(int index); Eye currentEye() const; /** * Checks if Oculus Rift is connected and can report head orientation. */ bool isReady() const; /** * Applies an offset to the yaw angle returned from Oculus Rift. This can be used to * calibrate where the zero angle's physical direction is. * * @param yawRadians Offset to apply to the yaw angle (radians). */ void setYawOffset(float yawRadians); void resetTracking(); /** * Sets a yaw offset that makes the current actual Oculus Rift yaw come out as zero. */ void resetYaw(); // Called to allow head orientation to change again. //void allowUpdate(); //void update(); /** * Returns the current head orientation as a vector containing the pitch, roll and * yaw angles, in radians. If no head tracking is available, the returned values are * not valid. */ Vector3f headOrientation() const; /** * Returns the current real-world head position. */ Vector3f headPosition() const; Vector3f eyeOffset() const; /** * Returns the eye pose for the current eye, including the head orientation and * position in relation to the tracking origin. * * @return Eye pose matrix. */ Matrix4f eyePose() const; Matrix4f projection(float nearDist, float farDist) const; float yawOffset() const; /** * Returns a model-view matrix that applies the head's orientation. */ //Matrix4f headModelViewMatrix() const; //float predictionLatency() const; /** * Returns the IPD configured in the Oculus Rift preferences. */ //float interpupillaryDistance() const; float aspect() const; Vector2ui resolution() const; /*Vector2f screenSize() const; Vector4f chromAbParam() const; float distortionScale() const;*/ float fovX() const; // in degrees //float fovY() const; // in degrees //Vector4f hmdWarpParam() const; //float lensSeparationDistance() const; void moveWindowToScreen(Screen screen); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_OCULUSRIFT_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/vr/vrconfig.h0000664000175000017500000001507612641367670024260 0ustar jaakkojaakko/** @file vrconfig.h Virtual reality configuration. * * @authors Copyright (c) 2014 Jaakko Keränen * @authors Copyright (c) 2013 Christopher Bruns * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_VRCONFIG_H #define LIBAPPFW_VRCONFIG_H #include #include namespace de { /** * Virtual reality configuration settings. */ class LIBAPPFW_PUBLIC VRConfig { public: /** * Stereoscopic 3D rendering mode. This enumeration determines the integer value in * the console variable. */ enum StereoMode { Mono, // 0 GreenMagenta, RedCyan, LeftOnly, RightOnly, TopBottom, // 5 SideBySide, Parallel, CrossEye, OculusRift, RowInterleaved, // 10 // NOT IMPLEMENTED YET ColumnInterleaved, // NOT IMPLEMENTED YET Checkerboard, // NOT IMPLEMENTED YET QuadBuffered, NUM_STEREO_MODES }; enum Eye { NeitherEye, LeftEye, RightEye }; public: VRConfig(); /** * Sets the current stereoscopic rendering mode. * * @param newMode Rendering mode. */ void setMode(StereoMode newMode); /** * Sets the distance from the eye to the screen onto which projection is being * done. This used when calculating a frustum-shifted projection matrix. * This is not used with Oculus Rift. * * @param distance Distance. */ void setScreenDistance(float distance); /** * Sets the height of the eye in map units. This is used to determine how big an * eye shift is needed. * * @param eyeHeightInMapUnits Height of the eye in map units, measured from the * "ground". */ void setEyeHeightInMapUnits(float eyeHeightInMapUnits); /** * Sets the currently used physical IPD. This is used to determine how big an * eye shift is needed. * * @param ipd IPD in mm. */ void setInterpupillaryDistance(float ipd); /** * Sets the height of the player in the real world. This is used as a scaling * factor to convert physical units to map units. * * @param heightInMeters Height of the player in meters. */ void setPhysicalPlayerHeight(float heightInMeters); /** * Sets the eye currently used for rendering a frame. In stereoscopic modes, * the frame is drawn twice; once for each eye. * * @param eye Eye to render. In non-stereoscopic modes, NeitherEye is used. */ void setCurrentEye(Eye eye); /** * Enables or disables projection frustum shifting. * * @param enable @c true to enable. */ void enableFrustumShift(bool enable = true); /** * Sets the number of multisampling samples used in the offscreen framebuffer * where Oculus Rift frames are first drawn. This framebuffer is typically some * multiple of the Oculus Rift display resolution. * * @param samples Number of samples to use for multisampling the Oculus Rift * framebuffer. */ void setRiftFramebufferSampleCount(int samples); /** * Sets the eyes-swapped mode. * * @param swapped @c true to swap left and right (default: false). */ void setSwapEyes(bool swapped); void setDominantEye(float value); /** * Currently active stereo rendering mode. */ StereoMode mode() const; float screenDistance() const; /** * Determines if the current stereoscopic rendering mode needs support from * the graphics hardware for quad-buffering (left/right back and front * buffers stored and drawn separately). */ bool needsStereoGLFormat() const; float interpupillaryDistance() const; float eyeHeightInMapUnits() const; float mapUnitsPerMeter() const; float physicalPlayerHeight() const; /** * Local viewpoint relative eye position in map units. */ float eyeShift() const; /** * Determines if frustum shift is enabled. */ bool frustumShift() const; bool swapEyes() const; float dominantEye() const; /// Multisampling used in unwarped Rift framebuffer. int riftFramebufferSampleCount() const; float viewAspect(Vector2f const &viewPortSize) const; /** * Calculates a vertical field of view angle based on a horizontal FOV angle, * view port size, and the current VR configuration. * * @param horizFovDegrees Field of view horizontally, in degrees. * @param viewPortSize Size of the viewport in pixels. * * @return Vertical field of view, in degrees. */ float verticalFieldOfView(float horizFovDegrees, Vector2f const &viewPortSize) const; /** * Produces a projection matrix suitable for the current VR configuration. * * @param fovDegrees Horizontal field of view angle. * @param viewPortSize Size of the viewport in pixels. * @param nearClip Distance of the near clipping plane. * @param farClip Distance of the far clipping plane. * * @return Projection matrix. */ Matrix4f projectionMatrix(float fovDegrees, Vector2f const &viewPortSize, float nearClip, float farClip) const; de::OculusRift &oculusRift(); de::OculusRift const &oculusRift() const; public: /** * Determines if the VR mode will be applying a transformation to window contents * that displaces the content from its "actual" location. * * @param mode Mode. * * @return @c true, if contents will change position on screen. */ static bool modeAppliesDisplacement(StereoMode mode); static bool modeNeedsStereoGLFormat(StereoMode mode); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_VRCONFIG_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/PopupWidget0000664000175000017500000000004212641367670024020 0ustar jaakkojaakko#include "widgets/popupwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/GridPopupWidget0000664000175000017500000000004612641367670024632 0ustar jaakkojaakko#include "widgets/gridpopupwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/DocumentPopupWidget0000664000175000017500000000005212641367670025520 0ustar jaakkojaakko#include "widgets/documentpopupwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/WindowSystem0000664000175000017500000000004512641367670024230 0ustar jaakkojaakko#include "framework/windowsystem.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/VariableSliderWidget0000664000175000017500000000005312641367670025607 0ustar jaakkojaakko#include "widgets/variablesliderwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/WindowTransform0000664000175000017500000000005012641367670024713 0ustar jaakkojaakko#include "framework/windowtransform.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ScriptCommandWidget0000664000175000017500000000005212641367670025461 0ustar jaakkojaakko#include "widgets/scriptcommandwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/LabelWidget0000664000175000017500000000004212641367670023734 0ustar jaakkojaakko#include "widgets/labelwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/libappfw.h0000664000175000017500000000272512641367670023615 0ustar jaakkojaakko/** @file libappfw.h Common definitions for libappfw. * * @authors Copyright © 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_MAIN_H #define LIBAPPFW_MAIN_H /* * The LIBAPPFW_PUBLIC macro is used for declaring exported symbols. It must be * applied in all exported classes and functions. DEF files are not used for * exporting symbols out of libappfw. */ #if defined(_WIN32) && defined(_MSC_VER) # ifdef __LIBAPPFW__ // This is defined when compiling the library. # define LIBAPPFW_PUBLIC __declspec(dllexport) # else # define LIBAPPFW_PUBLIC __declspec(dllimport) # endif #else // No need to use any special declarators. # define LIBAPPFW_PUBLIC #endif namespace de { } // namespace de #endif // LIBAPPFW_MAIN_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/ButtonWidget0000664000175000017500000000004312641367670024171 0ustar jaakkojaakko#include "widgets/buttonwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/TabWidget0000664000175000017500000000003712641367670023427 0ustar jaakkojaakko#include "widgets/tabwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ProgressWidget0000664000175000017500000000004512641367670024524 0ustar jaakkojaakko#include "widgets/progresswidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/AtlasProceduralImage0000664000175000017500000000005412641367670025604 0ustar jaakkojaakko#include "framework/atlasproceduralimage.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/CommandWidget0000664000175000017500000000004412641367670024275 0ustar jaakkojaakko#include "widgets/commandwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/TextDrawable0000664000175000017500000000004412641367670024141 0ustar jaakkojaakko#include "framework/textdrawable.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/Style0000664000175000017500000000003612641367670022654 0ustar jaakkojaakko#include "framework/style.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/GLTextComposer0000664000175000017500000000004612641367670024434 0ustar jaakkojaakko#include "framework/gltextcomposer.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/OculusRift0000664000175000017500000000003412641367670023651 0ustar jaakkojaakko#include "vr/oculusrift.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/CompositorWidget0000664000175000017500000000004712641367670025060 0ustar jaakkojaakko#include "widgets/compositorwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/Untrapper0000664000175000017500000000004212641367670023531 0ustar jaakkojaakko#include "framework/untrapper.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/PersistentState0000664000175000017500000000005012641367670024711 0ustar jaakkojaakko#include "framework/persistentstate.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/SliderWidget0000664000175000017500000000004312641367670024140 0ustar jaakkojaakko#include "widgets/sliderwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/LineEditWidget0000664000175000017500000000004512641367670024415 0ustar jaakkojaakko#include "widgets/lineeditwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ui/0000775000175000017500000000000012641367670022247 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libappfw/include/de/ui/VariableToggleItem0000664000175000017500000000005512641367670025700 0ustar jaakkojaakko#include "../framework/variabletoggleitem.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ui/SubwidgetItem0000664000175000017500000000005012641367670024741 0ustar jaakkojaakko#include "../framework/subwidgetitem.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ui/Item0000664000175000017500000000003712641367670023070 0ustar jaakkojaakko#include "../framework/item.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ui/Margins0000664000175000017500000000004212641367670023566 0ustar jaakkojaakko#include "../framework/margins.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ui/Data0000664000175000017500000000003712641367670023043 0ustar jaakkojaakko#include "../framework/data.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ui/ListData0000664000175000017500000000004312641367670023674 0ustar jaakkojaakko#include "../framework/listdata.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ui/ImageItem0000664000175000017500000000004512641367670024032 0ustar jaakkojaakko#include "../framework/imageitem.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ui/SubmenuItem0000664000175000017500000000004612641367670024427 0ustar jaakkojaakko#include "../framework/submenuitem.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ui/defs.h0000664000175000017500000000705312641367670023346 0ustar jaakkojaakko/** @file ui/defs.h Common de::ui namespace definitions. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_UI_DEFS_H #define LIBAPPFW_UI_DEFS_H #include "../libappfw.h" #include #include namespace de { namespace ui { /** * Basic directions. */ enum Direction { Left, Up, Right, Down, NoDirection }; inline Direction opposite(Direction dir) { switch(dir) { case Left: return Right; case Right: return Left; case Up: return Down; case Down: return Up; case NoDirection: return NoDirection; } } inline bool isHorizontal(Direction dir) { return dir == ui::Left || dir == ui::Right; } inline bool isVertical(Direction dir) { return dir == ui::Up || dir == ui::Down; } /** * Flags for specifying alignment. */ enum AlignmentFlag { AlignTop = 0x1, AlignBottom = 0x2, AlignLeft = 0x4, AlignRight = 0x8, AlignTopLeft = AlignTop | AlignLeft, AlignTopRight = AlignTop | AlignRight, AlignBottomLeft = AlignBottom | AlignLeft, AlignBottomRight = AlignBottom | AlignRight, AlignCenter = 0, DefaultAlignment = AlignCenter }; Q_DECLARE_FLAGS(Alignment, AlignmentFlag) Q_DECLARE_OPERATORS_FOR_FLAGS(Alignment) template typename RectType::Corner applyAlignment(Alignment align, SizeType const &size, RectType const &bounds) { typename RectType::Corner p = bounds.topLeft; if(align.testFlag(AlignRight)) { p.x += int(bounds.width()) - int(size.x); } else if(!align.testFlag(AlignLeft)) { p.x += (int(bounds.width()) - int(size.x)) / 2; } if(align.testFlag(AlignBottom)) { p.y += int(bounds.height()) - int(size.y); } else if(!align.testFlag(AlignTop)) { p.y += de::floor((double(bounds.height()) - double(size.y)) / 2.0); } return p; } template void applyAlignment(Alignment align, RectType &alignedRect, BoundsRectType const &boundsRect) { alignedRect.moveTopLeft(applyAlignment(align, alignedRect.size(), boundsRect)); } /** * Flags for specifying content fitting/scaling. */ enum ContentFitFlag { OriginalSize = 0, FitToWidth = 0x1, FitToHeight = 0x2, OriginalAspectRatio = 0x4, FitToSize = FitToWidth | FitToHeight }; Q_DECLARE_FLAGS(ContentFit, ContentFitFlag) Q_DECLARE_OPERATORS_FOR_FLAGS(ContentFit) /** * Policy for controlling size. */ enum SizePolicy { Fixed, ///< Size is fixed, content positioned inside. Filled, ///< Size is fixed, content expands to fill entire area. Expand ///< Size depends on content, expands/contracts to fit. }; } // namespace ui } // namespace de #endif // LIBAPPFW_UI_DEFS_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/ui/ActionItem0000664000175000017500000000004612641367670024226 0ustar jaakkojaakko#include "../framework/actionitem.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ui/Stylist0000664000175000017500000000004212641367670023641 0ustar jaakkojaakko#include "../framework/stylist.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/GuiWidget0000664000175000017500000000004112641367670023440 0ustar jaakkojaakko#include "framework/guiwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/RelayWidget0000664000175000017500000000004112641367670023770 0ustar jaakkojaakko#include "widgets/relaywidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ChildWidgetOrganizer0000664000175000017500000000005412641367670025624 0ustar jaakkojaakko#include "framework/childwidgetorganizer.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/BaseGuiApp0000664000175000017500000000004312641367670023532 0ustar jaakkojaakko#include "framework/baseguiapp.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/dialogs/0000775000175000017500000000000012641367670023254 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libappfw/include/de/dialogs/inputdialog.h0000664000175000017500000000257112641367670025751 0ustar jaakkojaakko/** @file dialogs/inputdialog.h Dialog for querying a string of text from the user. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_INPUTDIALOG_H #define LIBAPPFW_INPUTDIALOG_H #include "../MessageDialog" #include "../LineEditWidget" namespace de { /** * Dialog for querying a string of text from the user. */ class LIBAPPFW_PUBLIC InputDialog : public MessageDialog { public: InputDialog(String const &name = ""); LineEditWidget &editor(); protected: void preparePanelForOpening(); void panelClosing(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_INPUTDIALOG_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/dialogs/messagedialog.h0000664000175000017500000000266112641367670026236 0ustar jaakkojaakko/** @file messagedialog.h Dialog for showing a message. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_MESSAGEDIALOG_H #define LIBAPPFW_MESSAGEDIALOG_H #include "../DialogWidget" namespace de { /** * Dialog for showing a message. */ class LIBAPPFW_PUBLIC MessageDialog : public DialogWidget { public: MessageDialog(String const &name = ""); void useInfoStyle(); LabelWidget &title(); LabelWidget &message(); /** * Derived classes should call this after they add or remove widgets in the * dialog content area. */ void updateLayout(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_MESSAGEDIALOG_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/ScrollAreaWidget0000664000175000017500000000004712641367670024751 0ustar jaakkojaakko#include "widgets/scrollareawidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/NotificationAreaWidget0000664000175000017500000000005512641367670026140 0ustar jaakkojaakko#include "widgets/notificationareawidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/SequentialLayout0000664000175000017500000000005012641367670025060 0ustar jaakkojaakko#include "framework/sequentiallayout.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/VRWindowTransform0000664000175000017500000000005212641367670025165 0ustar jaakkojaakko#include "framework/vrwindowtransform.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/IPersistent0000664000175000017500000000004412641367670024024 0ustar jaakkojaakko#include "framework/ipersistent.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/PopupMenuWidget0000664000175000017500000000004612641367670024651 0ustar jaakkojaakko#include "widgets/popupmenuwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ChoiceWidget0000664000175000017500000000004312641367670024110 0ustar jaakkojaakko#include "widgets/choicewidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/FontLineWrapping0000664000175000017500000000005012641367670024776 0ustar jaakkojaakko#include "framework/fontlinewrapping.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/BlurWidget0000664000175000017500000000004112641367670023620 0ustar jaakkojaakko#include "widgets/blurwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/VariableChoiceWidget0000664000175000017500000000005312641367670025557 0ustar jaakkojaakko#include "widgets/variablechoicewidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/GridLayout0000664000175000017500000000004212641367670023634 0ustar jaakkojaakko#include "framework/gridlayout.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/VariableLineEditWidget0000664000175000017500000000005512641367670026064 0ustar jaakkojaakko#include "widgets/variablelineeditwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/DialogWidget0000664000175000017500000000004312641367670024115 0ustar jaakkojaakko#include "widgets/dialogwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/VariableToggleWidget0000664000175000017500000000005312641367670025606 0ustar jaakkojaakko#include "widgets/variabletogglewidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/StyleProceduralImage0000664000175000017500000000005412641367670025640 0ustar jaakkojaakko#include "framework/styleproceduralimage.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/PanelWidget0000664000175000017500000000004212641367670023754 0ustar jaakkojaakko#include "widgets/panelwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/0000775000175000017500000000000012641367670023627 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/guiwidget.h0000664000175000017500000004034712641367670026000 0ustar jaakkojaakko/** @file guiwidget.h Base class for graphical widgets. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_GUIWIDGET_H #define LIBAPPFW_GUIWIDGET_H #include #include #include #include #include #include "../Style" #include "../ui/defs.h" #include "../ui/Margins" #include "../framework/guiwidgetprivate.h" namespace de { class GuiRootWidget; class BlurWidget; /** * Base class for graphical widgets. * * Each GuiWidget has one RuleRectangle that defines the widget's position in * the view. However, all widgets are allowed to draw outside this rectangle * and react to events occurring outside it. In essence, all widgets thus cover * the entire view area and can be thought as a (hierarchical) stack. * * GuiWidget is the base class for all widgets of a GUI application. However, a * GuiWidget does not necessarily need to have a visible portion on the screen: * its entire purpose may be to handle events, for example. * * The common features GuiWidget offers to all widgets are: * * - Automatically saving and restoring persistent state. Classes that implement * IPersistent will automatically be saved and restored when the widget is * (de)initialized. * * - Background geometry builder. All widgets may use this to build geometry for * the background of the widget. However, widgets are also allowed to fully * generate all of their geometry from scratch. * * - Access to the UI Style. * * - GuiWidget can be told which font and text color to use using a style * definition identifier (e.g., "editor.hint"). These style elements are then * conveniently accessible using methods of GuiWidget. * * - Opacity property. Opacities respect the hierarchical organization of * widgets: GuiWidget::visibleOpacity() returns the opacity of a particular * widget where all the parent widgets' opacities are factored in. * * - Hit testing: checking if a view space point should be considered to be * inside the widget. The default implementation simply checks if the point is * inside the widget's rectangle. Derived classes may override this to adapt * hit testing to their particular visual shape. * * - Logic for handling more complicated interactions such as a mouse pointer * click (press then release inside or outside), and passing received events * to separately registered event handler objects. * * QObject is a base class for the signals and slots capabilities. * * @note Always use GuiWidget::destroy() to delete any GUI widget. It will * ensure that the widget is properly deinitialized before destruction. * * @ingroup gui */ class LIBAPPFW_PUBLIC GuiWidget : public QObject, public Widget { Q_OBJECT public: /** * Properties of the widget's background's apperance. * GuiWidget::glMakeGeometry() uses this to construct the background * geometry of the widget. * * @todo Refactor: it should be possible to apply any combination of these * in a single widget; use a dynamic array of effects. Base it on ProceduralImage. */ struct Background { enum Type { None, ///< No background or solid fill. GradientFrame, ///< Use the "gradient frame" from the UI atlas. BorderGlow, ///< Border glow with specified color/thickness. Blurred, ///< Blurs whatever is showing behind the widget. BlurredWithBorderGlow, BlurredWithSolidFill, SharedBlur, ///< Use the blur background from a BlurWidget. SharedBlurWithBorderGlow, Rounded }; Vector4f solidFill; ///< Always applied if opacity > 0. Type type; Vector4f color; ///< Secondary color. float thickness; ///< Frame border thickenss. GuiWidget *blur; Background() : type(None), thickness(0), blur(0) {} Background(GuiWidget &blurred, Vector4f const &blurColor) : solidFill(blurColor), type(SharedBlur), thickness(0), blur(&blurred) {} Background(Vector4f const &solid, Type t = None) : solidFill(solid), type(t), thickness(0), blur(0) {} Background(Type t, Vector4f const &borderColor, float borderThickness = 0) : type(t), color(borderColor), thickness(borderThickness), blur(0) {} Background(Vector4f const &solid, Type t, Vector4f const &borderColor, float borderThickness = 0) : solidFill(solid), type(t), color(borderColor), thickness(borderThickness), blur(0) {} inline Background withSolidFill(Vector4f const &newSolidFill) const { Background bg = *this; bg.solidFill = newSolidFill; return bg; } inline Background withSolidFillOpacity(float opacity) const { Background bg = *this; bg.solidFill.w = opacity; return bg; } }; typedef Vertex2TexRgba DefaultVertex; typedef GLBufferT DefaultVertexBuf; /** * Handles events. */ class IEventHandler { public: virtual ~IEventHandler() {} /** * Handle an event. * * @param widget Widget that received the event. * @param event Event. * * @return @c true, if the event was eaten. @c false otherwise. */ virtual bool handleEvent(GuiWidget &widget, Event const &event) = 0; }; enum Attribute { /** * Enables or disables automatic state serialization for widgets derived from * IPersistent. State serialization occurs when the widget is gl(De)Init'd. */ RetainStatePersistently = 0x1, AnimateOpacityWhenEnabledOrDisabled = 0x2, /** * Prevents the drawing of the widget contents even if it visible. The texture * containing the blurred background is updated regardless. */ DontDrawContent = 0x4, /** * Visible opacity determined solely by the widget itself, not affected by * ancestors. */ IndependentOpacity = 0x8, DefaultAttributes = RetainStatePersistently | AnimateOpacityWhenEnabledOrDisabled }; Q_DECLARE_FLAGS(Attributes, Attribute) public: GuiWidget(String const &name = ""); /** * Deletes a widget. The widget is first deinitialized. * * @param widget Widget to destroy. */ static void destroy(GuiWidget *widget); /** * Deletes a widget at a later point in time. However, the widget is immediately * deinitialized. * * @param widget Widget to deinitialize now and destroy layer. */ static void destroyLater(GuiWidget *widget); GuiRootWidget &root() const; Widget::Children childWidgets() const; Widget *parentWidget() const; Style const &style() const; /** * Returns the rule rectangle that defines the placement of the widget on * the target canvas. */ RuleRectangle &rule(); Rectanglei contentRect() const; /** * Returns the rule rectangle that defines the placement of the widget on * the target canvas. */ RuleRectangle const &rule() const; ui::Margins &margins(); ui::Margins const &margins() const; Rectanglef normalizedRect() const; Rectanglef normalizedRect(Rectanglei const &viewSpaceRect) const; /** * Normalized content rectangle. Same as normalizedRect() except margins * are applied to all sides. */ Rectanglef normalizedContentRect() const; void setFont(DotPath const &id); void setTextColor(DotPath const &id); void set(Background const &bg); Font const &font() const; DotPath const &textColorId() const; ColorBank::Color textColor() const; ColorBank::Colorf textColorf() const; /** * Determines whether the contents of the widget are supposed to be clipped * to its boundaries. The Widget::ContentClipping behavior flag is used for * storing this information. */ bool isClipped() const; Background const &background() const; /** * Sets the opacity of the widget. Child widgets' opacity is also affected. * * @param opacity Opacity. * @param span Animation transition span. * @param startDelay Starting delay. */ void setOpacity(float opacity, TimeDelta span = 0, TimeDelta startDelay = 0); /** * Determines the widget's opacity animation. */ Animation opacity() const; /** * Determines the widget's opacity, factoring in all ancestor opacities. */ float visibleOpacity() const; /** * Sets an object that will be offered events received by this widget. The * handler may eat the event. Any number of event handlers can be added; * they are called in the order of addition. * * @param handler Event handler. Ownership given to GuiWidget. */ void addEventHandler(IEventHandler *handler); void removeEventHandler(IEventHandler *handler); /** * Sets, unsets, or replaces one or more widget attributes. * * @param attr Attribute(s) to modify. * @param op Flag operation. */ void setAttribute(Attributes const &attr, FlagOp op = SetFlags); /** * Returns the current widget attributes. */ Attributes attributes() const; /** * Save the state of the widget and all its children (those who support state * serialization). */ void saveState(); /** * Restore the state of the widget and all its children (those who support state * serialization). */ void restoreState(); // Events. void initialize(); void deinitialize(); void viewResized(); void update(); void draw() final; bool handleEvent(Event const &event); /** * Determines if the widget occupies on-screen position @a pos. * * @param pos Coordinates. * * @return @c true, if hit. */ virtual bool hitTest(Vector2i const &pos) const; bool hitTest(Event const &event) const; /** * Checks if the position is on any of the children of this widget. * * @param pos Coordinates. * * @return The child that occupied the position in the view. */ GuiWidget const *treeHitTest(Vector2i const &pos) const; /** * Returns the rule rectangle used for hit testing. Defaults to a rectangle * equivalent to GuiWidget::rule(). Modify the hit test rule to allow * widgets to be hittable outside their default boundaries. * * @return Hit test rule. */ RuleRectangle &hitRule(); enum MouseClickStatus { MouseClickUnrelated, ///< Event was not related to mouse clicks. MouseClickStarted, MouseClickFinished, MouseClickAborted }; MouseClickStatus handleMouseClick(Event const &event, MouseEvent::Button button = MouseEvent::Left); /** * Requests the widget to refresh its geometry, if it has any static * geometry. Normally this does not need to be called. It is provided * mostly as a way for subclasses to ensure that geometry is up to date * when they need it. * * @param yes @c true to request, @c false to cancel the request. */ void requestGeometry(bool yes = true); bool geometryRequested() const; bool isInitialized() const; GuiWidget *guiFind(String const &name); GuiWidget const *guiFind(String const &name) const; public slots: /** * Puts the widget in garbage to be deleted at the next recycling. */ void guiDeleteLater(); public: /** * Normalize a rectangle within a container rectangle. * * @param rect Rectangle to normalize. * @param containerRect Container rectangle to normalize in. * * @return Normalized rectangle. */ static Rectanglef normalizedRect(Rectanglei const &rect, Rectanglei const &containerRect); static float toDevicePixels(float logicalPixels); inline static int toDevicePixels(int logicalPixels) { return int(toDevicePixels(float(logicalPixels))); } inline static duint toDevicePixels(duint logicalPixels) { return duint(toDevicePixels(float(logicalPixels))); } template static Vector2 toDevicePixels(Vector2 const &type) { return Vector2(typename Vector2::ValueType(toDevicePixels(type.x)), typename Vector2::ValueType(toDevicePixels(type.y))); } /** * Immediately deletes all the widgets in the garbage. This is useful to * avoid double deletion in case a trashed widget's parent is deleted * before recycling occurs. */ static void recycleTrashedWidgets(); protected: /** * Called by GuiWidget::update() the first time an update is being carried * out. Native GL is guaranteed to be available at this time, so the widget * must allocate all its GL resources during this method. Note that widgets * cannot always allocate GL resources during their constructors because GL * may not be initialized yet at that time. */ virtual void glInit(); /** * Called from deinitialize(). Deinitialization must occur before the * widget is destroyed. This is the appropriate place for the widget to * release its GL resources. If one waits until the widget's destructor to * do so, it may already have lost access to some required information * (such as the root widget, or derived classes' private instances). */ virtual void glDeinit(); /** * Called by GuiWidget when it is time to draw the widget's content. A * clipping scissor is automatically set before this is called. Derived * classes should override this instead of the draw() method. * * This is not called if the widget's visible opacity is zero or the widget * is hidden. */ virtual void drawContent(); void drawBlurredRect(Rectanglei const &rect, Vector4f const &color, float opacity = 1.0f); /** * Extensible mechanism for derived widgets to build their geometry. The * assumptions with this are 1) the vertex format is Vertex2TexRgba, 2) * the shared UI atlas is used, and 3) the background is automatically * built by GuiWidget's implementation of the function. * * @param verts Vertex builder. */ virtual void glMakeGeometry(DefaultVertexBuf::Builder &verts); /** * Checks if the widget's rectangle has changed. * * @param currentPlace The widget's current placement is returned here. * * @return @c true, if the place of the widget has changed since the * last call to hasChangedPlace(); otherwise, @c false. */ bool hasChangedPlace(Rectanglei ¤tPlace); /** * Called during GuiWidget::update() whenever the style of the widget has * been marked as changed. */ virtual void updateStyle(); private: DENG2_PRIVATE(d) }; Q_DECLARE_OPERATORS_FOR_FLAGS(GuiWidget::Attributes) template struct GuiWidgetDeleter { void operator () (WidgetType *w) { GuiWidget::destroy(w); } }; template class UniqueWidgetPtr : public std::unique_ptr> { public: UniqueWidgetPtr(WidgetType *w = nullptr) : std::unique_ptr>(w) {} }; } // namespace de #endif // LIBAPPFW_GUIWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/fontlinewrapping.h0000664000175000017500000000733312641367670027374 0ustar jaakkojaakko/** @file fontlinewrapping.h Font line wrapping. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_FONTLINEWRAPPING_H #define LIBAPPFW_FONTLINEWRAPPING_H #include "../libappfw.h" #include #include #include #include namespace de { /** * Line wrapper that uses a particular font and calculates widths in pixels. * Height is still in lines, though. The wrapper cannot be used before the * font has been defined. * * Supports indentation of lines, as marked in the RichFormat. * * @par Thread-safety * * FontLineWrapping locks itself automatically when any of its methods are * being executed. Instances can be used from multiple threads. */ class LIBAPPFW_PUBLIC FontLineWrapping : public Lockable, public shell::ILineWrapping { public: FontLineWrapping(); void setFont(Font const &font); Font const &font() const; bool hasFont() const; /** * Clears the wrapping completely. The text is also cleared. */ void clear(); /** * Resets the existing wrapping (isEmpty() will return @c true) but does not * clear the text. */ void reset(); void wrapTextToWidth(String const &text, int maxWidth); void wrapTextToWidth(String const &text, Font::RichFormat const &format, int maxWidth); bool isEmpty() const; String const &text() const; shell::WrappedLine line(int index) const; int width() const; int height() const; int rangeWidth(Rangei const &range) const; int indexAtWidth(Rangei const &range, int width) const; /** * Calculates the total height of the wrapped lined in pixels. If there are * multiple lines, takes into consideration the appropriate leading between * lines. */ int totalHeightInPixels() const; /** * Returns the maximum width given to wrapTextToWidth(). */ int maximumWidth() const; /** * Determines the coordinates of a character's top left corner in pixels. * * @param line Wrapped line number. * @param charIndex Index of the character on the wrapped line. Each * wrapped line is indexed starting from zero. * * @return XY coordinates of the character. */ Vector2i charTopLeftInPixels(int line, int charIndex); struct LineInfo { struct Segment { Rangei range; int tabStop; int width; Segment(Rangei const &r, int tab = 0) : range(r), tabStop(tab), width(0) {} }; typedef QList Segments; Segments segs; int indent; ///< Left indentation to apply to the entire line. public: int highestTabStop() const; }; /** * Returns the left indentation for a wrapped line. * * @param index Wrapped line number. */ LineInfo const &lineInfo(int index) const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_FONTLINEWRAPPING_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/data.h0000664000175000017500000000746212641367670024722 0ustar jaakkojaakko/** @file data.h UI data context. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_UI_DATA_H #define LIBAPPFW_UI_DATA_H #include #include #include "../GuiWidget" namespace de { namespace ui { class Item; /** * UI data context containing an enumerable collection of items. ui::Data and * ui::Item are pure content -- they know nothing about how the data is * presented. There may be multiple simultaneous, alternative presentations of * the same context and items. * * Modifying Data will automatically cause the changes to be reflected in * any widget currently presenting the data context's items. * * Data has ownership of all the items in it. * * @see ChildWidgetOrganizer */ class LIBAPPFW_PUBLIC Data { public: typedef dsize Pos; static dsize const InvalidPos; /** * Notified when a new item is added to the data context. */ DENG2_DEFINE_AUDIENCE2(Addition, void dataItemAdded(Pos id, Item const &item)) /** * Notified when an item has been removed from the data context. When this * is called @a item is no longer in the context and can be modified at * will. */ DENG2_DEFINE_AUDIENCE2(Removal, void dataItemRemoved(Pos oldId, Item &item)) DENG2_DEFINE_AUDIENCE2(OrderChange, void dataItemOrderChanged()) public: Data(); virtual ~Data() {} virtual Data &clear() = 0; inline bool isEmpty() const { return !size(); } inline Data &operator << (Item *item) { return append(item); } inline Data &append(Item *item) { return insert(size(), item); } /** * Insert an item into the data context. * * @param pos Position of the item. * @param item Item to insert. Context gets ownership. * * @return Reference to this Data. */ virtual Data &insert(Pos pos, Item *item) = 0; virtual void remove(Pos pos) = 0; virtual Item *take(Pos pos) = 0; virtual Item &at(Pos pos) = 0; virtual Item const &at(Pos pos) const = 0; /** * Finds the position of a specific item. * * @param item Item to find. * * @return The items' position, or Data::InvalidPos if not found. */ virtual Pos find(Item const &item) const = 0; /** * Finds the position of an item with a specific data. * * @param data Data to find. * * @return The items' position, or Data::InvalidPos if not found. */ virtual Pos findData(QVariant const &data) const = 0; enum SortMethod { Ascending, Descending }; virtual void sort(SortMethod method = Ascending); typedef bool (*LessThanFunc)(Item const &, Item const &); virtual void sort(LessThanFunc func) = 0; virtual void stableSort(LessThanFunc func) = 0; /** * Returns the total number of items in the data context. */ virtual dsize size() const = 0; LoopResult forAll(std::function func) const; private: DENG2_PRIVATE(d) }; typedef Data::Pos DataPos; } // namespace ui } // namespace de #endif // LIBAPPFW_UI_DATA_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/childwidgetorganizer.h0000664000175000017500000001364512641367670030221 0ustar jaakkojaakko/** @file childwidgetorganizer.h Organizes widgets according to a UI context. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_CHILDWIDGETORGANIZER_H #define LIBAPPFW_CHILDWIDGETORGANIZER_H #include "../libappfw.h" #include "../ui/Data" #include "../GuiWidget" namespace de { /** * Utility class that observes changes in a ui::Context and updates a parent * widget's children to reflect the UI context's contents. This involves * creating the corresponding widgets, updating them when the context items * change, and reordering them when the items' order changes. * * The concrete task of creating widgets is done by an object that implements * the ChildWidgetOrganizer::IWidgetFactory interface. Also, third parties * may observe widget creation and updates and alter the widget as they choose. * * @todo Virtualization: it is not required that all the items of the context * are represented by widgets on screen at the same time. In contexts with * large numbers of items, virtualization should be applied to keep only a * subset/range of items present as widgets. */ class LIBAPPFW_PUBLIC ChildWidgetOrganizer { public: /** * Constructs widgets for the organizer. */ class IWidgetFactory { public: virtual ~IWidgetFactory() {} /** * Called when the organizer needs a widget for a context item. This allows * the specialized organizers to choose the widget type and customize it * appropriately. * * After construction, the widget is automatically updated with * updateItemWidget(). * * @param item Item that has the content. * @param parent Future parent of the widget, if any (can be @c NULL). */ virtual GuiWidget *makeItemWidget(ui::Item const &item, GuiWidget const *parent) = 0; /** * Called whenever the item's content changes and this should be reflected * in the widget. * * @param widget Widget that represents the item. * @param item Item that has the content. */ virtual void updateItemWidget(GuiWidget &widget, ui::Item const &item) = 0; }; /** * Filters out data items. */ class IFilter { public: virtual ~IFilter() {} /** * Determines whether an item should be ignored by the organizer. * * @param organizer Which organizer is asking the question. * @param data Data context. * @param pos Position of the item in the context. * * @return @c true to accept item, @c false to ignore it. */ virtual bool isItemAccepted(ChildWidgetOrganizer const &organizer, ui::Data const &data, ui::Data::Pos pos) const = 0; }; /** * Notified when the organizer creates a widget for a context item. Allows * third parties to customize the widget as needed. */ DENG2_DEFINE_AUDIENCE2(WidgetCreation, void widgetCreatedForItem(GuiWidget &widget, ui::Item const &item)) /** * Notified when the organizer updates a widget for a changed context item. * Allows third parties to customize the widget as needed. */ DENG2_DEFINE_AUDIENCE2(WidgetUpdate, void widgetUpdatedForItem(GuiWidget &widget, ui::Item const &item)); public: ChildWidgetOrganizer(GuiWidget &container); /** * Sets the object responsible for creating widgets for this organizer. The * default factory creates labels with their default settings. The factory * should be set before calling setContext(). * * @param factory Widget factory. */ void setWidgetFactory(IWidgetFactory &factory); IWidgetFactory &widgetFactory() const; /** * Sets the object that decides whether items are accepted or ignored. * * @param filter Filtering object. */ void setFilter(IFilter const &filter); /** * Sets the data context of the organizer. If there was a previous context, * all widgets created for it are deleted from the container. The widgets * are immediately constructed using the current factory. * * @param context Context with items. */ void setContext(ui::Data const &context); void unsetContext(); ui::Data const &context() const; GuiWidget *itemWidget(ui::Data::Pos pos) const; GuiWidget *itemWidget(ui::Item const &item) const; GuiWidget *itemWidget(de::String const &label) const; ui::Item const *findItemForWidget(GuiWidget const &widget) const; private: DENG2_PRIVATE(d) }; /** * Simple widget factory that creates label widgets with their default * settings, using the label from the ui::Item. */ class DefaultWidgetFactory : public ChildWidgetOrganizer::IWidgetFactory { public: GuiWidget *makeItemWidget(ui::Item const &item, GuiWidget const *parent); void updateItemWidget(GuiWidget &widget, ui::Item const &item); }; } // namespace de #endif // LIBAPPFW_CHILDWIDGETORGANIZER_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/guiwidgetprivate.h0000664000175000017500000001030412641367670027361 0ustar jaakkojaakko/** @file guiwidgetprivate.h Template for GUI widget private implementation. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_GUIWIDGETPRIVATE_H #define LIBAPPFW_GUIWIDGETPRIVATE_H #include #include "../GuiRootWidget" namespace de { class Style; /** * Base class for GuiWidget-derived widgets' private implementation. Provides * easy access to the root widget and shared GL resources. This should be used * as the base class for private implementations if GL resources are being * used (i.e., glInit() and glDeinit() are being called). * * Use DENG_GUI_PIMPL() instead of the DENG2_PIMPL() macro. * * Note that GuiWidgetPrivate automatically observes the root widget's atlas * content repositioning, so derived private implementations can just override * the observer method if necessary. */ template class GuiWidgetPrivate : public Private, DENG2_OBSERVES(Atlas, Reposition), DENG2_OBSERVES(Asset, Deletion) { public: typedef GuiWidgetPrivate Base; // shadows Private<>::Base public: GuiWidgetPrivate(PublicType &i) : Private(i) {} GuiWidgetPrivate(PublicType *i) : Private(i) {} virtual ~GuiWidgetPrivate() { forgetRootAtlas(); /** * Ensure that the derived's class's glDeinit() method has been * called before the private class instance is destroyed. At least * classes that have GuiWidget as the immediate parent class need to * call deinitialize() in their destructors. * * @see GuiWidget::destroy() */ DENG2_ASSERT(!Base::self.isInitialized()); } void forgetRootAtlas() { if(_observingAtlas) { _observingAtlas->audienceForReposition() -= this; _observingAtlas->audienceForDeletion() -= this; _observingAtlas = nullptr; } } void observeRootAtlas() const { if(!_observingAtlas) { // Automatically start observing the root atlas. _observingAtlas = &root().atlas(); _observingAtlas->audienceForReposition() += this; _observingAtlas->audienceForDeletion() += this; } } bool hasRoot() const { return Base::self.hasRoot(); } GuiRootWidget &root() const { DENG2_ASSERT(hasRoot()); return Base::self.root(); } AtlasTexture &atlas() const { observeRootAtlas(); return *_observingAtlas; } GLUniform &uAtlas() const { observeRootAtlas(); return root().uAtlas(); } GLShaderBank &shaders() const { return root().shaders(); } Style const &style() const { return Base::self.style(); } void atlasContentRepositioned(Atlas &atlas) { if(_observingAtlas == &atlas) { // Make sure the new texture coordinates get used by the widget. Base::self.requestGeometry(); } } void assetBeingDeleted(Asset &a) { if(_observingAtlas == &a) { _observingAtlas = nullptr; } } private: mutable AtlasTexture *_observingAtlas = nullptr; }; #define DENG_GUI_PIMPL(ClassName) \ typedef ClassName Public; \ struct ClassName::Instance : public de::GuiWidgetPrivate } // namespace de #endif // LIBAPPFW_GUIWIDGETPRIVATE_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/windowsystem.h0000664000175000017500000000675612641367670026572 0ustar jaakkojaakko/** @file windowsystem.h Window management subsystem. * * @todo Deferred window changes should be done using a queue-type solution * where it is possible to schedule multiple tasks into the future separately * for each window. Each window could have its own queue. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_WINDOWSYSTEM_H #define LIBAPPFW_WINDOWSYSTEM_H #include #include #include #include #include "../Style" #include "../BaseWindow" namespace de { #undef main /** * Window management subsystem. * * The window system processes events produced by the input drivers. In * practice, the events are passed to the widgets in the windows. * * @ingroup gui */ class LIBAPPFW_PUBLIC WindowSystem : public System { public: /// Required/referenced Window instance is missing. @ingroup errors DENG2_ERROR(MissingWindowError); public: WindowSystem(); /** * Sets a new style for the application. * * @param style Style instance. Ownership taken. */ void setStyle(Style *style); template WindowType *newWindow(String const &id) { DENG2_ASSERT(!find(id)); WindowType *win = new WindowType(id); addWindow(id, win); return win; } void addWindow(String const &id, BaseWindow *window); /** * Returns @c true iff a main window is available. */ static bool mainExists(); /** * Returns the main window. */ static BaseWindow &main(); /** * Returns a pointer to the @em main window. * * @see hasMain() */ inline static BaseWindow *mainPtr() { return mainExists()? &main() : 0; } /** * Find a window. * * @param id Window identifier. "main" for the main window. * * @return Window instance or @c NULL if not found. */ BaseWindow *find(String const &id) const; /** * Closes all windows, including the main window. */ void closeAll(); /** * Returns the window system's UI style. */ Style &style(); /** * Dispatches a mouse position event with the latest mouse position. This * happens automatically whenever the mouse has moved and time advances. */ void dispatchLatestMousePosition(); Vector2i latestMousePosition() const; // System. bool processEvent(Event const &); void timeChanged(Clock const &); public: static void setAppWindowSystem(WindowSystem &winSys); static WindowSystem &get(); protected: virtual void closingAllWindows(); virtual bool rootProcessEvent(Event const &event) = 0; virtual void rootUpdate() = 0; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_WINDOWSYSTEM_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/stylist.h0000664000175000017500000000227212641367670025516 0ustar jaakkojaakko/** @file stylist.h Widget stylist. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_UI_STYLIST_H #define LIBAPPFW_UI_STYLIST_H #include "../libappfw.h" namespace de { class GuiWidget; namespace ui { /** * Widget stylist. */ class LIBAPPFW_PUBLIC Stylist { public: virtual ~Stylist() {} virtual void applyStyle(GuiWidget &widget) = 0; }; } // namespace ui } // namespace de #endif // LIBAPPFW_UI_STYLIST_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/subwidgetitem.h0000664000175000017500000000375412641367670026665 0ustar jaakkojaakko/** @file subwidgetitem.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_UI_SUBWIDGETITEM_H #define LIBAPPFW_UI_SUBWIDGETITEM_H #include "../ui/defs.h" #include "../ui/ImageItem" #include #include namespace de { class PopupWidget; namespace ui { /** * UI context item that opens a widget as a popup. */ class LIBAPPFW_PUBLIC SubwidgetItem : public ImageItem { public: typedef std::function WidgetConstructor; public: SubwidgetItem(String const &label, ui::Direction openingDirection, WidgetConstructor constructor) : ImageItem(ShownAsButton, label) , _constructor(constructor) , _dir(openingDirection) {} SubwidgetItem(Image const &image, String const &label, ui::Direction openingDirection, WidgetConstructor constructor) : ImageItem(ShownAsButton, image, label) , _constructor(constructor) , _dir(openingDirection) {} PopupWidget *makeWidget() const { return _constructor(); } ui::Direction openingDirection() const { return _dir; } private: WidgetConstructor _constructor; ui::Direction _dir; }; } // namespace ui } // namespace de #endif // LIBAPPFW_UI_SUBWIDGETITEM_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/style.h0000664000175000017500000000473112641367670025145 0ustar jaakkojaakko/** @file style.h User interface style. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_STYLE_H #define LIBAPPFW_STYLE_H #include "../libappfw.h" #include #include #include #include namespace de { class GuiWidget; class Package; /** * User interface style. */ class LIBAPPFW_PUBLIC Style { public: Style(); virtual ~Style(); /** * Loads a style from a resource pack. * * @param pack Package containing the style. */ void load(Package const &pack); RuleBank const &rules() const; FontBank const &fonts() const; ColorBank const &colors() const; ImageBank const &images() const; RuleBank &rules(); FontBank &fonts(); ColorBank &colors(); ImageBank &images(); // Partial implementation for Font::RichFormat::IStyle. void richStyleFormat(int contentStyle, float &sizeFactor, Font::RichFormat::Weight &fontWeight, Font::RichFormat::Style &fontStyle, int &colorIndex) const; Font const *richStyleFont(Font::RichFormat::Style fontStyle) const; /** * Determines if blurred widget backgrounds are allowed. */ virtual bool isBlurringAllowed() const; virtual GuiWidget *sharedBlurWidget() const; public: /** * Returns the current global application UI style. * @return */ static Style &get(); /** * Sets the current global application UI style. * * @param appStyle New application style. */ static void setAppStyle(Style &appStyle); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_STYLE_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/baseguiapp.h0000664000175000017500000000344712641367670026130 0ustar jaakkojaakko/** @file baseguiapp.h Base class for GUI applications. * * @authors Copyright © 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_BASEGUIAPP_H #define LIBAPPFW_BASEGUIAPP_H #include "../libappfw.h" #include "../PersistentState" #include #include #include /** * Macro for conveniently accessing the de::BaseGuiApp singleton instance. */ #define DENG2_BASE_GUI_APP (static_cast(qApp)) namespace de { class VRConfig; /** * Base class for GUI applications. * * Contains all the shared resources and other data that is needed by the UI framework. */ class LIBAPPFW_PUBLIC BaseGuiApp : public GuiApp { public: BaseGuiApp(int &argc, char **argv); void initSubsystems(SubsystemInitFlags flags = DefaultSubsystems); public: static BaseGuiApp &app(); static PersistentState &persistentUIState(); static GLShaderBank &shaders(); static WaveformBank &waveforms(); static VRConfig &vr(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_BASEGUIAPP_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/textdrawable.h0000664000175000017500000000675512641367670026503 0ustar jaakkojaakko/** @file textdrawable.h High-level GL text drawing utility. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_TEXTDRAWABLE_H #define LIBAPPFW_TEXTDRAWABLE_H #include "gltextcomposer.h" #include "fontlinewrapping.h" namespace de { /** * High-level GL text drawing utility. * * The task of drawing text is rather complicated. There are several components * involved: * * - EscapeParser parses style escape codes. * - Font understands rich text formatting and performs the actual * rasterization of text onto bitmap images. * - FontLineWrapping takes rich-formatted ("styled") text and wraps it onto * multiple lines, taking into account tab stops and indentation. * - GLTextComposer allocates the text lines from an Atlas and generates the * triangle strips needed for actually drawing the text on the screen. * * TextDrawable is a high-level utility for controlling this entire process as * easily as possible. If fine-grained control of the text is required, one can * still use the lower level components directly. * * TextDrawable handles wrapping of text using a background thread, so content * length is unlimited and the calling thread will not block. TextDrawable is * an Asset, and will not be marked ready until the background wrapping is * done. One is still required to call TextDrawable::update() before drawing, * though. */ class LIBAPPFW_PUBLIC TextDrawable : public GLTextComposer { public: TextDrawable(); void init(Atlas &atlas, Font const &font, Font::RichFormat::IStyle const *style = 0); void deinit(); void clear(); /** * Sets the maximum width for text lines. * * @param maxLineWidth Maximum line width. */ void setLineWrapWidth(int maxLineWidth); void setText(String const &styledText); void setFont(Font const &font); /** * Sets the range of visible lines and releases all allocations outside * the range. * * @param lineRange Range of visible lines. */ void setRange(Rangei const &lineRange); /** * Updates the status of the composer. This includes first checking if a * background wrapping task has been completed, and if so, the results of * background wrapping become the visible data to be drawn. * * @return @c true, if the lines have changed and it is necessary to remake * the geometry. @c false, if nothing further needs to be done or text is * still being preprocessed for drawing. */ bool update(); FontLineWrapping const &wraps() const; Vector2ui wrappedSize() const; String text() const; bool isBeingWrapped() const; Font const &font() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_TEXTDRAWABLE_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/ipersistent.h0000664000175000017500000000267012641367670026356 0ustar jaakkojaakko/** @file ipersistent.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_IPERSISTENT_H #define LIBAPPFW_IPERSISTENT_H #include "../libappfw.h" namespace de { class PersistentState; /** * Interface for objects whose state can be stored persistently using PersistentState. * * GuiWidget instances that implement IPersistent will automatically be saved and * restored when the widget is (de)initialized. */ class LIBAPPFW_PUBLIC IPersistent { public: virtual ~IPersistent() {} virtual void operator >> (PersistentState &toState) const = 0; virtual void operator << (PersistentState const &fromState) = 0; }; } // namespace de #endif // LIBAPPFW_IPERSISTENT_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/atlasproceduralimage.h0000664000175000017500000000513412641367670030173 0ustar jaakkojaakko/** @file atlasproceduralimage.h Procedural image for a static 2D texture. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_ATLASPROCEDURALIMAGE_H #define LIBAPPFW_ATLASPROCEDURALIMAGE_H #include "../libappfw.h" #include "../ProceduralImage" #include "../GuiWidget" #include "../GuiRootWidget" #include #include #include namespace de { /** * Procedural image that draws a simple 2D texture stored on an atlas. */ class LIBAPPFW_PUBLIC AtlasProceduralImage : public ProceduralImage { public: AtlasProceduralImage(GuiWidget &owner) : _owner(owner), _atlas(0), _needUpdate(false) {} ~AtlasProceduralImage() { release(); } Atlas &ownerAtlas() { return _owner.root().atlas(); } bool hasImage() const { return !_image.isNull(); } void alloc() { release(); _atlas = &ownerAtlas(); _id = _atlas->alloc(_image); } void release() { if(_atlas) { _atlas->release(_id); _atlas = 0; } } void setImage(Image const &image) { _image = image; _needUpdate = true; setSize(image.size()); } bool update() { if(_needUpdate) { alloc(); _needUpdate = false; return true; } return false; } void glInit() { alloc(); } void glDeinit() { release(); } void glMakeGeometry(DefaultVertexBuf::Builder &verts, Rectanglef const &rect) { if(_atlas) { verts.makeQuad(rect, color(), _atlas->imageRectf(_id)); } } private: GuiWidget &_owner; Atlas *_atlas; Image _image; Id _id; bool _needUpdate; }; } // namespace de #endif // ATLASPROCEDURALIMAGE_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/variabletoggleitem.h0000664000175000017500000000260412641367670027650 0ustar jaakkojaakko/** @file variabletoggleitem.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_UI_VARIABLETOGGLEITEM_H #define LIBAPPFW_UI_VARIABLETOGGLEITEM_H #include #include "../ui/Item" namespace de { namespace ui { /** * Represents a toggle for a boolean variable. */ class LIBAPPFW_PUBLIC VariableToggleItem : public Item { public: VariableToggleItem(String const &label, Variable &variable) : Item(ShownAsToggle, label), _var(variable) {} Variable &variable() const { return _var; } private: Variable &_var; }; } // namespace ui } // namespace de #endif // LIBAPPFW_UI_VARIABLETOGGLEITEM_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/vrwindowtransform.h0000664000175000017500000000320712641367670027615 0ustar jaakkojaakko/** @file vrwindowtransform.h Window content transformation for virtual reality. * * @authors Copyright (c) 2013 Christopher Bruns * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_VRWINDOWTRANSFORM_H #define LIBAPPFW_VRWINDOWTRANSFORM_H #include "../libappfw.h" #include "../WindowTransform" namespace de { class GLFramebuffer; /** * Window content transformation for virtual reality. */ class LIBAPPFW_PUBLIC VRWindowTransform : public WindowTransform { public: VRWindowTransform(BaseWindow &window); void glInit(); void glDeinit(); Vector2ui logicalRootSize(Vector2ui const &physicalCanvasSize) const; Vector2f windowToLogicalCoords(Vector2i const &pos) const; void drawTransformed(); GLFramebuffer &unwarpedFramebuffer(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_VRWINDOWTRANSFORM_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/imageitem.h0000664000175000017500000000307412641367670025745 0ustar jaakkojaakko/** @file imageitem.h Data item with an image. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_UI_IMAGEITEM_H #define LIBAPPFW_UI_IMAGEITEM_H #include "item.h" #include namespace de { namespace ui { /** * UI context item that represents a user action. */ class LIBAPPFW_PUBLIC ImageItem : public Item { public: ImageItem(Semantics semantics, String const &label = "") : Item(semantics, label) {} ImageItem(Semantics semantics, Image const &image, String const &label = "") : Item(semantics, label), _image(image) {} Image const &image() const { return _image; } void setImage(Image const &image) { _image = image; notifyChange(); } private: Image _image; }; } // namespace ui } // namespace de #endif // LIBAPPFW_UI_IMAGEITEM_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/listdata.h0000664000175000017500000000315712641367670025613 0ustar jaakkojaakko/** @file listdata.h List-based UI data context. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_UI_LISTDATA_H #define LIBAPPFW_UI_LISTDATA_H #include "../ui/Data" #include "../ui/Item" #include namespace de { namespace ui { /** * List-based UI data context. */ class LIBAPPFW_PUBLIC ListData : public Data { public: ListData() {} ~ListData(); dsize size() const; Item &at(Pos pos); Item const &at(Pos pos) const; Pos find(Item const &item) const; Pos findData(QVariant const &data) const; Data &clear(); Data &insert(Pos pos, Item *item); void remove(Pos pos); Item *take(Pos pos); void sort(LessThanFunc lessThan); void stableSort(LessThanFunc lessThan); private: typedef QList Items; Items _items; }; } // namespace ui } // namespace de #endif // LIBAPPFW_UI_LISTDATA_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/sequentiallayout.h0000664000175000017500000000607012641367670027413 0ustar jaakkojaakko/** @file sequentiallayout.h Widget layout for a sequence of widgets. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_SEQUENTIALLAYOUT_H #define LIBAPPFW_SEQUENTIALLAYOUT_H #include "../ui/defs.h" #include "../GuiWidget" namespace de { /** * Widget layout for a sequence of widgets. * * Layouts are utilities that modify the placement of widgets. The layout * instance itself does not need to remain in memory -- widget rules are * modified immediately as the widgets are added to the layout. */ class LIBAPPFW_PUBLIC SequentialLayout { public: SequentialLayout(Rule const &startX, Rule const &startY, ui::Direction direction = ui::Down); void clear(); /** * Sets the direction of the layout. The direction can only be changed * when the layout is empty. * * @param direction New layout direction. */ void setDirection(ui::Direction direction); ui::Direction direction() const; /** * Defines a width for all appended widgets. If the widget already has a * width defined, it will be overridden. * * @param width Width for all widgets in the layout. */ void setOverrideWidth(Rule const &width); /** * Defines a height for all appended widgets. If the widget already has a * height defined, it will be overridden. * * @param height Height for all widgets in the layout. */ void setOverrideHeight(Rule const &height); SequentialLayout &operator << (GuiWidget &widget) { return append(widget); } SequentialLayout &operator << (Rule const &emptySpace) { return append(emptySpace); } enum AppendMode { UpdateMinorAxis, ///< Layout total length on the minor axis is updated. IgnoreMinorAxis ///< Does not affect layou total length on the minor axis. }; SequentialLayout &append(GuiWidget &widget, AppendMode mode = UpdateMinorAxis); SequentialLayout &append(GuiWidget &widget, Rule const &spaceBefore, AppendMode mode = UpdateMinorAxis); SequentialLayout &append(Rule const &emptySpace); WidgetList widgets() const; int size() const; bool isEmpty() const; Rule const &width() const; Rule const &height() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_SEQUENTIALLAYOUT_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/signalaction.h0000664000175000017500000000242212641367670026453 0ustar jaakkojaakko/** @file signalaction.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_SIGNALACTION_H #define LIBAPPFW_SIGNALACTION_H #include "../libappfw.h" #include #include namespace de { class LIBAPPFW_PUBLIC SignalAction : public QObject, public Action { Q_OBJECT public: SignalAction(QObject *target, char const *slot); void trigger(); signals: void triggered(); private: QObject *_target; char const *_slot; }; } // namespace de #endif // LIBAPPFW_SIGNALACTION_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/dialogcontentstylist.h0000664000175000017500000000367612641367670030302 0ustar jaakkojaakko/** @file dialogcontentstylist.h Sets the style for widgets in a dialog. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_DIALOGCONTENTSTYLIST_H #define LIBAPPFW_DIALOGCONTENTSTYLIST_H #include "../libappfw.h" #include "../ui/Stylist" #include namespace de { class DialogWidget; /** * Sets the style for widgets in a dialog. */ class LIBAPPFW_PUBLIC DialogContentStylist : public ui::Stylist, DENG2_OBSERVES(Widget, ChildAddition) { public: DialogContentStylist(); DialogContentStylist(DialogWidget &dialog); DialogContentStylist(GuiWidget &container); ~DialogContentStylist(); void clear(); void setContainer(GuiWidget &container); /** * Adds a new container without detaching from the existing one(s). * * @param container New container to style. */ void addContainer(GuiWidget &container); void setInfoStyle(bool useInfoStyle); void setAdjustMargins(bool yes); void applyStyle(GuiWidget &widget); // Observes when new children are added. void widgetChildAdded(Widget &child); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_DIALOGCONTENTSTYLIST_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/basewindow.h0000664000175000017500000000652512641367670026152 0ustar jaakkojaakko/** @file basewindow.h Abstract base class for application windows. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_BASEWINDOW_H #define LIBAPPFW_BASEWINDOW_H #include "../libappfw.h" #include #include #include namespace de { class WindowTransform; /** * Abstract base class for application windows. * * All windows have a Canvas where the contents of the window are drawn. Windows may * additionally specify a content transformation using a WindowTransform object, which * will override the built-in transformation. The built-in transformation specifies an * "identity" transformation that doesn't differ from the logical layout. */ class LIBAPPFW_PUBLIC BaseWindow : public PersistentCanvasWindow { public: BaseWindow(String const &id); /** * Sets a new content transformation being applied in the window. The provided * object must remain in existence as long as the BaseWindow instance uses it. * * @param xf Content transformaton. */ void setTransform(WindowTransform &xf); /** * Changes the window transformation to the default one that applies no actual * transformation. */ void useDefaultTransform(); /** * Returns the current content transformation being applied to the content of the * window. */ WindowTransform &transform(); /** * Returns the logical size of the window contents (e.g., root widget). */ virtual Vector2f windowContentSize() const = 0; /** * Causes the contents of the window to be drawn. The contents are drawn immediately * and the method does not return until everything has been drawn. The method should * draw an entire frame using the non-transformed logical size of the view. */ virtual void drawWindowContent() = 0; virtual bool shouldRepaintManually() const; /** * Request drawing the contents of the window as soon as possible. */ virtual void draw(); void canvasGLDraw(Canvas &); void swapBuffers(); protected: /** * Called when a draw request has been received. This method should carry out any * preparations necessary before the frame can be drawn. It can also cancel the * frame is needed. * * @return @c true to continue drawing the frame, @c false to abort the frame. */ virtual bool prepareForDraw(); virtual void preDraw(); virtual void postDraw(); virtual bool handleFallbackEvent(Event const &event) = 0; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_BASEWINDOW_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/actionitem.h0000664000175000017500000000441612641367670026141 0ustar jaakkojaakko/** @file actionitem.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_UI_ACTIONITEM_H #define LIBAPPFW_UI_ACTIONITEM_H #include "imageitem.h" #include #include namespace de { namespace ui { /** * UI context item that represents a user action. */ class LIBAPPFW_PUBLIC ActionItem : public ImageItem { public: ActionItem(String const &label = "", RefArg action = RefArg()) : ImageItem(ShownAsButton | ActivationClosesPopup, label) , _action(action.holdRef()) {} ActionItem(Semantics semantics, String const &label = "", RefArg action = RefArg()) : ImageItem(semantics, label) , _action(action.holdRef()) {} ActionItem(Semantics semantics, Image const &img, String const &label = "", RefArg action = RefArg()) : ImageItem(semantics, img, label) , _action(action.holdRef()) {} ActionItem(Image const &img, String const &label = "", RefArg action = RefArg()) : ImageItem(ShownAsButton | ActivationClosesPopup, img, label) , _action(action.holdRef()) {} Action const *action() const { return _action; } void setAction(RefArg action) { _action.reset(action); notifyChange(); } private: AutoRef _action; }; } // namespace ui } // namespace de #endif // LIBAPPFW_UI_ACTIONITEM_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/windowtransform.h0000664000175000017500000000416512641367670027251 0ustar jaakkojaakko/** @file windowtransform.h Base class for window content transformation. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_WINDOWTRANSFORM_H #define LIBAPPFW_WINDOWTRANSFORM_H #include namespace de { class BaseWindow; /** * Base class for window content transformation. */ class WindowTransform { public: WindowTransform(BaseWindow &window); BaseWindow &window() const; /** * Called by the window when GL is ready. */ virtual void glInit(); virtual void glDeinit(); /** * Determines how large the root widget should be for a particular canvas size. * * @param physicalCanvasSize Canvas size (pixels). * * @return Logical size (UI units). */ virtual Vector2ui logicalRootSize(Vector2ui const &physicalCanvasSize) const; /** * Translate a point in physical window coordinates to logical coordinates. * * @param pos Window coordinates (pixels). * * @return Logical coordinates inside the root widget's area. */ virtual Vector2f windowToLogicalCoords(Vector2i const &pos) const; /** * Applies the appropriate transformation state and tells the window to draw its * contents. */ virtual void drawTransformed(); DENG2_AS_IS_METHODS() private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_WINDOWTRANSFORM_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/proceduralimage.h0000664000175000017500000000374412641367670027153 0ustar jaakkojaakko/** @file proceduralimage.h Procedural image. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_PROCEDURALIMAGE_H #define LIBAPPFW_PROCEDURALIMAGE_H #include #include #include "../libappfw.h" namespace de { /** * Base class for procedural images. * * A procedural image can be used instead of a static one to generate geometry * on the fly (see LabelWidget). */ class LIBAPPFW_PUBLIC ProceduralImage { public: typedef Vector2f Size; typedef Vector4f Color; typedef GLBufferT DefaultVertexBuf; public: ProceduralImage(Size const &size = Size()); virtual ~ProceduralImage(); Size size() const; Color color() const; void setSize(Size const &size); void setColor(Color const &color); /** * Updates the image. * * @return @c true, if the geometry has changed and it should be remade. Otherwise * @c false if nothing has been changed. */ virtual bool update(); virtual void glInit(); virtual void glDeinit(); virtual void glMakeGeometry(DefaultVertexBuf::Builder &verts, Rectanglef const &rect) = 0; private: Size _size; Color _color; }; } // namespace de #endif // LIBAPPFW_PROCEDURALIMAGE_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/submenuitem.h0000664000175000017500000000326712641367670026345 0ustar jaakkojaakko/** @file submenuitem.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_UI_SUBMENUITEM_H #define LIBAPPFW_UI_SUBMENUITEM_H #include "../ui/defs.h" #include "imageitem.h" #include "listdata.h" namespace de { namespace ui { /** * UI context item that contains items for a submenu. */ class LIBAPPFW_PUBLIC SubmenuItem : public ImageItem { public: SubmenuItem(String const &label, ui::Direction openingDirection) : ImageItem(ShownAsButton, label), _dir(openingDirection) {} SubmenuItem(Image const &image, String const &label, ui::Direction openingDirection) : ImageItem(ShownAsButton, image, label), _dir(openingDirection) {} Data &items() { return _items; } Data const &items() const { return _items; } ui::Direction openingDirection() const { return _dir; } private: ListData _items; ui::Direction _dir; }; } // namespace ui } // namespace de #endif // LIBAPPFW_UI_SUBMENUITEM_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/item.h0000664000175000017500000000574612641367670024752 0ustar jaakkojaakko/** @file item.h Data context item. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_UI_DATAITEM_H #define LIBAPPFW_UI_DATAITEM_H #include "../libappfw.h" #include #include #include namespace de { namespace ui { class Data; /** * Data item. * * Items are pure content -- the exact presentation parameters (widget type, * alignment, scaling, etc.)ares determined by the container widget and/or * responsible organizer, not by an Item. This allows one item to be presented * in different ways by different widgets/contexts. * * @see ui::Data */ class LIBAPPFW_PUBLIC Item { public: /** * Determines the item's behavior and look'n'feel. This acts as a hint for * the containing widget (and the responsible organizer) so it can adjust * its behavior accordingly. */ enum SemanticFlag { ShownAsLabel = 0x1, ShownAsButton = 0x2, ShownAsToggle = 0x4, ActivationClosesPopup = 0x100, Separator = 0x200, Annotation = 0x400 | Separator, DefaultSemantics = ShownAsLabel }; Q_DECLARE_FLAGS(Semantics, SemanticFlag) DENG2_DEFINE_AUDIENCE2(Change, void itemChanged(Item const &item)) public: Item(Semantics semantics = DefaultSemantics); Item(Semantics semantics, String const &label); virtual ~Item(); Semantics semantics() const; void setLabel(String const &label); String label() const; void setDataContext(Data &context); bool hasDataContext() const; Data &dataContext() const; /** * Returns a text string that should be used for sorting the item inside a * context. */ virtual String sortKey() const; /** * Sets the custom user data of the item. * * @param d Variant data to be associated with the item. */ void setData(QVariant const &d); QVariant const &data() const; DENG2_AS_IS_METHODS() protected: /** * Notifies the Change audience of a changed property. */ void notifyChange(); private: DENG2_PRIVATE(d) }; Q_DECLARE_OPERATORS_FOR_FLAGS(Item::Semantics) } // namespace ui } // namespace de #endif // LIBAPPFW_UI_DATAITEM_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/untrapper.h0000664000175000017500000000276712641367670026034 0ustar jaakkojaakko/** @file untrapper.h Mouse untrapping utility. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_UNTRAPPER_H #define LIBAPPFW_UNTRAPPER_H #include "../libappfw.h" #include namespace de { /** * Utility for untrapping the mouse. The mouse is untrapped from the specified window for * the lifetime of the Untrapper instance. When Untrapper is destroyed, mouse is * automatically trapped if it originally was trapped. */ class LIBAPPFW_PUBLIC Untrapper { public: /** * Mouse is untrapped if it has been trapped in @a window. * @param window Window where (un)trapping is done. */ Untrapper(CanvasWindow &window); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_UNTRAPPER_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/persistentstate.h0000664000175000017500000000256512641367670027251 0ustar jaakkojaakko/** @file persistentstate.h Persistent UI state. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_PERSISTENTSTATE_H #define LIBAPPFW_PERSISTENTSTATE_H #include "../libappfw.h" #include #include namespace de { class IPersistent; /** * Stores and recalls persistent state across running sessions. */ class LIBAPPFW_PUBLIC PersistentState : public Refuge { public: PersistentState(String const &name); PersistentState &operator << (IPersistent const &object); PersistentState &operator >> (IPersistent &object); }; } // namespace de #endif // LIBAPPFW_PERSISTENTSTATE_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/guirootwidget.h0000664000175000017500000000766612641367670026713 0ustar jaakkojaakko/** @file guirootwidget.h Graphical root widget. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_GUIROOTWIDGET_H #define LIBAPPFW_GUIROOTWIDGET_H #include #include #include #include #include #include #include "../libappfw.h" namespace de { class GuiWidget; /** * Graphical root widget. * * @ingroup gui */ class LIBAPPFW_PUBLIC GuiRootWidget : public RootWidget { public: GuiRootWidget(CanvasWindow *window = 0); /** * Sets the window in which the root widget resides. * * @param window Client window instance. */ void setWindow(CanvasWindow *window); /** * Returns the window in which the root widget resides. */ CanvasWindow &window(); AtlasTexture &atlas(); GLUniform &uAtlas(); Id solidWhitePixel() const; Id roundCorners() const; Id boldRoundCorners() const; Id borderGlow() const; Id tinyDot() const; /** * Gets the identifier of a style image allocated on the shared UI atlas texture. * * @param styleImagePath Path of the style image in the style's image bank. * * @return Id of the texture allocation. */ Id styleTexture(DotPath const &styleImagePath) const; static GLShaderBank &shaders(); /** * Returns the default projection for 2D graphics. */ Matrix4f projMatrix2D() const; void routeMouse(Widget *routeTo); bool processEvent(Event const &event); /** * Finds the widget that occupies the given point, looking through the entire tree. The * returned widget is the one that will first handle a received event associated with this * position. * * @param pos Position in the view. * * @return Widget, or @c NULL if none were found. */ GuiWidget const *globalHitTest(Vector2i const &pos) const; GuiWidget const *guiFind(String const &name) const; // Events. void update(); void draw(); /** * Draws until the widget @a until is encountered during tree notification. * * @param until Widget to stop drawing at. @a until is not drawn. */ void drawUntil(Widget &until); /** * Adds a widget over all others. * * @param widget Widget to add on top. */ virtual void addOnTop(GuiWidget *widget); /** * Reorders the children of the root widget so that @a widget is added to the top. * * @param widget Widget to move to the top. */ void moveToTop(GuiWidget &widget); /** * Sends the current mouse position as a mouse event, just like the mouse would've * been moved. */ virtual void dispatchLatestMousePosition(); /** * If the event is not used by any widget, this will be called so the application may * still handle the event for other, non-widget-related purposes. No widget will be * offered the event after this is called. * * @param event Event. */ virtual void handleEventAsFallback(Event const &event); protected: virtual void loadCommonTextures(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_GUIROOTWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/gridlayout.h0000664000175000017500000001105312641367670026163 0ustar jaakkojaakko/** @file gridlayout.h Widget layout for a grid of widgets. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_GRIDLAYOUT_H #define LIBAPPFW_GRIDLAYOUT_H #include "../ui/defs.h" #include "../GuiWidget" namespace de { /** * Widget layout for a grid of widgets. * * Layouts are utilities that modify the placement of widgets. The layout * instance itself does not need to remain in memory -- widget rules are * modified immediately as the widgets are added to the layout. */ class LIBAPPFW_PUBLIC GridLayout { public: enum Mode { ColumnFirst, ///< Widgets added to a column until the column gets full. RowFirst ///< Widgets added to a row until the row gets full. }; public: GridLayout(Mode mode = ColumnFirst); GridLayout(Rule const &left, Rule const &top, Mode mode = ColumnFirst); void clear(); void setMode(Mode mode); void setGridSize(int numCols, int numRows); void setModeAndGridSize(Mode mode, int numCols, int numRows); void setColumnAlignment(int column, ui::Alignment cellAlign); /** * Sets the alignment for an individual cell. Overrides column/row alignment. * * @param cell Cell position. * @param cellAlign Alignment for the cell. */ void setCellAlignment(Vector2i const &cell, ui::Alignment cellAlign); void setColumnFixedWidth(int column, Rule const &fixedWidth); void setLeftTop(Rule const &left, Rule const &top); void setOverrideWidth(Rule const &width); void setOverrideHeight(Rule const &height); void setColumnPadding(Rule const &gap); void setRowPadding(Rule const &gap); GridLayout &operator << (GuiWidget &widget) { return append(widget); } GridLayout &operator << (Rule const &empty) { return append(empty); } GridLayout &append(GuiWidget &widget, int cellSpan = 1); /** * Appends a widget to the layout as the next cell. Instead of using the * widget's actual widget, @a layoutWidth is treated as the widget's width * in the layout. This allows specifying a custom width that may include * additional space needed by the widget. * * @param widget Widget. * @param layoutWidth Width of the widget, overriding the actual width. * @param cellSpan How many cells does the widget span. * * @return Reference to this GridLayout. */ GridLayout &append(GuiWidget &widget, Rule const &layoutWidth, int cellSpan = 1); GridLayout &append(Rule const &empty); /** * Appends an empty cell according to the override width/height, * which must be set previously. */ GridLayout &appendEmpty(); WidgetList widgets() const; int size() const; bool isEmpty() const; /** * Returns the maximum grid size. These values were given to setGridSize(). */ Vector2i maxGridSize() const; /** * Returns the actual grid size in columns and rows. This depends on how * many widgets have been added to the grid. */ Vector2i gridSize() const; /** * Determines the cell coordinates of a particular widget. The widget * must be among the widgets previously added to the layout. * * @param widget Widget to look up. * * @return Cell coordinates. */ Vector2i widgetPos(GuiWidget &widget) const; GuiWidget *at(Vector2i const &cell) const; Rule const &width() const; Rule const &height() const; Rule const &columnLeft(int col) const; Rule const &columnRight(int col) const; Rule const &columnWidth(int col) const; Rule const &rowHeight(int row) const; Rule const &overrideWidth() const; Rule const &overrideHeight() const; Rule const &columnPadding() const; Rule const &rowPadding() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_GRIDLAYOUT_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/margins.h0000664000175000017500000000504612641367670025445 0ustar jaakkojaakko/** @file margins.h Margin rules for widgets. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_UI_MARGINS_H #define LIBAPPFW_UI_MARGINS_H #include #include #include #include "../ui/defs.h" namespace de { namespace ui { /** * Margin rules for a widget. */ class LIBAPPFW_PUBLIC Margins { public: DENG2_DEFINE_AUDIENCE2(Change, void marginsChanged()) public: Margins(String const &defaultMargin = "gap"); Margins &setLeft (DotPath const &leftMarginId); Margins &setRight (DotPath const &rightMarginId); Margins &setTop (DotPath const &topMarginId); Margins &setBottom(DotPath const &bottomMarginId); Margins &set (ui::Direction dir, DotPath const &marginId); Margins &set (DotPath const &marginId); Margins &setLeft (Rule const &rule); Margins &setRight (Rule const &rule); Margins &setTop (Rule const &rule); Margins &setBottom(Rule const &rule); Margins &set (ui::Direction dir, Rule const &rule); Margins &set (Rule const &rule); Margins &setAll (Margins const &margins); Rule const &left() const; Rule const &right() const; Rule const &top() const; Rule const &bottom() const; /** * The "width" of the margins is the sum of the left and right margins. */ Rule const &width() const; /** * The "height" of the margins is the sim of the top and bottom margins. */ Rule const &height() const; Rule const &margin(ui::Direction dir) const; /** * Returns all four margins as a vector. (x,y) is the left and top margins * and (z,w) is the right and bottom margins. */ Vector4i toVector() const; private: DENG2_PRIVATE(d) }; } // namespace ui } // namespace de #endif // LIBAPPFW_UI_MARGINS_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/gltextcomposer.h0000664000175000017500000001032312641367670027056 0ustar jaakkojaakko/** @file gltextcomposer.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_GLTEXTCOMPOSER_H #define LIBAPPFW_GLTEXTCOMPOSER_H #include #include #include #include #include "../ui/defs.h" #include "../FontLineWrapping" namespace de { /** * Allocates and releases lines of text on an atlas and produces geometry for * drawing the text. * * Relies on a pre-existing FontLineWrapping where the text content has been * wrapped onto multiple lines and laid out appropriately. */ class LIBAPPFW_PUBLIC GLTextComposer : public Asset { public: typedef Vertex2TexRgba Vertex; typedef GLBufferT VertexBuf; typedef VertexBuf::Builder Vertices; public: GLTextComposer(); void setAtlas(Atlas &atlas); void setWrapping(FontLineWrapping const &wrappedLines); void setText(String const &text); void setStyledText(String const &styledText); void setText(String const &text, Font::RichFormat const &format); /** * Sets the range of visible lines. * * @param visibleLineRange Visible range of lines. */ void setRange(Rangei const &visibleLineRange); Rangei range() const; /** * Makes sure all the lines are allocated on the atlas. After this all the * allocated lines match the ones in the wrapping. This must be called * before makeVertices(). * * @return @c true, if any allocations were changed and makeVertices() * should be called again. */ bool update(); /** * Forces all the text to be rerasterized. */ void forceUpdate(); /** * Releases all the allocations from the atlas. */ void release(); /** * Releases the allocated lines that are outside the current range. */ void releaseLinesOutsideRange(); void makeVertices(Vertices &triStrip, Vector2i const &topLeft, ui::Alignment const &lineAlign, Vector4f const &color = Vector4f(1, 1, 1, 1)); /** * Generates vertices for all the text lines and concatenates them onto the existing * triangle strip in @a triStrip. * * @param triStrip Vertices for a triangle strip. * @param rect Rectangle inside which the text will be placed. * @param alignInRect Alignment within @a rect. * @param lineAlign Horizontal alignment for each line within the paragraph. * @param color Vertex color for the generated vertices. */ void makeVertices(Vertices &triStrip, Rectanglei const &rect, ui::Alignment const &alignInRect, ui::Alignment const &lineAlign, Vector4f const &color = Vector4f(1, 1, 1, 1)); /** * Returns the maximum width of the generated vertices. This is only valid after * makeVertices() has been called. * * This may be larger than the maximum width as determined by FontLineWrapping if * tabbed lines are used in the text. This is because text segments are aligned * with tab stops only during makeVertices(). * * @todo Ideally tap stop alignment should be done in FontLineWrapping, so that the * maximum width would be known prior to generating the vertices. * * @return Maximum width of the generated vertices. */ int verticesMaxWidth() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_GLTEXTCOMPOSER_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/framework/styleproceduralimage.h0000664000175000017500000000431112641367670030223 0ustar jaakkojaakko/** @file styleproceduralimage.h Procedural image that uses a common UI texture. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_STYLEPROCEDURALIMAGE_H #define LIBAPPFW_STYLEPROCEDURALIMAGE_H #include "../ProceduralImage" #include "../GuiWidget" #include namespace de { class StyleProceduralImage : public ProceduralImage { public: StyleProceduralImage(DotPath const &styleImageId, GuiWidget &owner, float angle = 0) : _owner(owner), _imageId(styleImageId), _id(Id::None), _angle(angle) { if(_owner.hasRoot()) { // We can set this up right away. alloc(); } } GuiRootWidget &root() { return _owner.root(); } void setAngle(float angle) { _angle = angle; } void alloc() { _id = root().styleTexture(_imageId); setSize(root().atlas().imageRect(_id).size()); } void glInit() { alloc(); } void glDeinit() { _id = Id::None; } void glMakeGeometry(DefaultVertexBuf::Builder &verts, Rectanglef const &rect) { if(!_id.isNone()) { Matrix4f turn = Matrix4f::rotateAround(rect.middle(), _angle); verts.makeQuad(rect, color(), root().atlas().imageRectf(_id), &turn); } } private: GuiWidget &_owner; DotPath _imageId; Id _id; float _angle; }; } // namespace de #endif // LIBAPPFW_STYLEPROCEDURALIMAGE_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/SignalAction0000664000175000017500000000004412641367670024126 0ustar jaakkojaakko#include "framework/signalaction.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/ProceduralImage0000664000175000017500000000004712641367670024621 0ustar jaakkojaakko#include "framework/proceduralimage.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/0000775000175000017500000000000012641367670023300 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/variablesliderwidget.h0000664000175000017500000000323212641367670027645 0ustar jaakkojaakko/** @file variablesliderwidget.h Slider that changes a Variable. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_VARIABLESLIDERWIDGET_H #define LIBAPPFW_VARIABLESLIDERWIDGET_H #include #include "../SliderWidget" namespace de { /** * Widget for changing the value of a Variable using a slider. */ class LIBAPPFW_PUBLIC VariableSliderWidget : public SliderWidget { Q_OBJECT public: /// Thrown when the variable is gone and someone tries to access it. @ingroup errors DENG2_ERROR(VariableMissingError); public: VariableSliderWidget(Variable &variable, Ranged const &range, ddouble step = 0, String const &name = ""); Variable &variable() const; public slots: void updateFromVariable(); protected slots: void setVariableFromWidget(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_VARIABLESLIDERWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/dialogwidget.h0000664000175000017500000001666312641367670026130 0ustar jaakkojaakko/** @file widgets/dialogwidget.h Popup dialog. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_DIALOGWIDGET_H #define LIBAPPFW_DIALOGWIDGET_H #include "popupwidget.h" #include "scrollareawidget.h" #include "menuwidget.h" namespace de { class GuiRootWidget; class DialogContentStylist; /** * Popup dialog. * * The content area of a dialog is scrollable. A menu with buttons is placed in * the bottom of the dialog, for the actions available to the user. * * The contents of the dialog should be placed under the scroll area returned * by DialogWidget::content() and positioned in relation to its content rule. * When the dialog is set up, one must define the size of the content scroll * area (width and height rules). * * Note that when a widget is added to the content area, the dialog * automatically applies certain common style parameters (margins, backgrounds, * etc.). * * @par Widget Structure * * Dialogs are composed of several child widgets: *

 * DialogWidget    (PopupWidget)
 *  +- container   (GuiWidget; the popup content widget)
 *      +- heading (LabelWidget; optional)
 *      +- area    (ScrollAreaWidget; contains actual dialog widgets)
 *      +- buttons (MenuWidget)
 *      +- extra   (MenuWidget; might be empty)
 * 
* * Scrolling is set up so that the dialog height doesn't surpass the view * rectangle's height. Contents of the "area" widget scroll while the other * elements remain static in relation to the container. */ class LIBAPPFW_PUBLIC DialogWidget : public PopupWidget { Q_OBJECT public: /** * Modality of the dialog. By default, dialogs are modal, meaning that * while they are open, no events can get past the dialog. */ enum Modality { Modal, NonModal }; enum Flag { DefaultFlags = 0, WithHeading = 0x1 ///< Dialog has a heading above the content area. }; Q_DECLARE_FLAGS(Flags, Flag) enum RoleFlag { None = 0, Default = 0x1, ///< Pressing Space or Enter will activate this. Accept = 0x2, Reject = 0x4, Yes = 0x8, No = 0x10, Action = 0x20, IdMask = 0xff0000, Id1 = 0x010000, Id2 = 0x020000, Id3 = 0x030000, Id4 = 0x040000 }; Q_DECLARE_FLAGS(RoleFlags, RoleFlag) /** * All buttons in a dialog must be ButtonItem instances or instances of * derived classes. * * The DialogButtonItem typedef is provided for convenience. */ class LIBAPPFW_PUBLIC ButtonItem : public ui::ActionItem { public: /** * Button with the role's default label and action. * @param flags Role flags for the button. * @param label Label for the button. If empty, the default label will be used. */ ButtonItem(RoleFlags flags, String const &label = ""); /** * Button with custom action. * @param flags Role flags for the button. * @param label Label for the button. If empty, the default label will be used. * @param action Action for the button. */ ButtonItem(RoleFlags flags, String const &label, RefArg action); ButtonItem(RoleFlags flags, Image const &image, RefArg action); ButtonItem(RoleFlags flags, Image const &image, String const &label, RefArg action); RoleFlags role() const { return _role; } private: RoleFlags _role; }; /// Asked for a label that does not exist in the dialog. @ingroup errors DENG2_ERROR(UndefinedLabel); public: DialogWidget(String const &name = "", Flags const &flags = DefaultFlags); Modality modality() const; /** * If the dialog was created using the WithHeading flag, this will return the * label used for the dialog heading. */ LabelWidget &heading(); ScrollAreaWidget &area(); /** * Sets the rule for the minimum width of the dialog. The default is that the * dialog is at least as wide as the content area, or all the button widths * summed together. * * @param minWidth Custom minimum width for the dialog. */ void setMinimumContentWidth(Rule const &minWidth); MenuWidget &buttonsMenu(); /** * Additional buttons of the dialog, laid out opposite to the normal dialog * buttons. These are used for functionality related to the dialog's content, * but they don't accept or reject the dialog. For instance, shortcut for * settings, showing what's new in an update, etc. * * @return Widget for dialog's extra buttons. */ MenuWidget &extraButtonsMenu(); ui::Data &buttons(); ButtonWidget &buttonWidget(String const &label) const; ButtonWidget *buttonWidget(int roleId) const; /** * Sets the action that will be triggered if the dialog is accepted. The action * will be triggered after the dialog has started closing (called from * DialogWidget::finish()). * * @param action Action to trigger after the dialog has been accepted. */ void setAcceptanceAction(RefArg action); /** * Shows the dialog and blocks execution until the dialog is closed -- * another event loop is started for event processing. Call either accept() * or reject() to dismiss the dialog. * * @param root Root where to execute the dialog. * * @return Result code. */ int exec(GuiRootWidget &root); /** * Opens the dialog as non-modal. The dialog must already be added to the * widget tree. Use accept() or reject() to close the dialog. */ void open(); ui::ActionItem *defaultActionItem(); // Events. void update(); bool handleEvent(Event const &event); public slots: void accept(int result = 1); void reject(int result = 0); signals: void accepted(int result); void rejected(int result); protected: void preparePanelForOpening(); /** * Derived classes can override this to do additional tasks before * execution of the dialog begins. DialogWidget::prepare() must be called * from the overridding methods. The focused widget is reset in the method. */ virtual void prepare(); /** * Handles any tasks needed when the dialog is closing. * DialogWidget::finish() must be called from overridding methods. * * @param result Result code. */ virtual void finish(int result); private: DENG2_PRIVATE(d) }; typedef DialogWidget::ButtonItem DialogButtonItem; Q_DECLARE_OPERATORS_FOR_FLAGS(DialogWidget::Flags) Q_DECLARE_OPERATORS_FOR_FLAGS(DialogWidget::RoleFlags) } // namespace de #endif // LIBAPPFW_DIALOGWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/compositorwidget.h0000664000175000017500000000425012641367670027054 0ustar jaakkojaakko/** @file compositorwidget.h Off-screen compositor. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_COMPOSITORWIDGET_H #define LIBAPPFW_COMPOSITORWIDGET_H #include "../GuiWidget" namespace de { /** * Off-screen compositor. * * All children of the compositor are drawn to an off-screen render target * whose size is equal to the default render target. The default behavior of * the compositor is to then draw the composited off-screen target back to the * default target. * * @todo Allow optionally requesting more target attachments beyond the default * color buffer. */ class LIBAPPFW_PUBLIC CompositorWidget : public GuiWidget { public: CompositorWidget(String const &name = ""); GLTexture &composite() const; /** * Sets the matrix that is used when drawing the composited contents * back to the normal render target. * * @param projMatrix Projection matrix. */ void setCompositeProjection(Matrix4f const &projMatrix); /** * Sets the projection used for displaying the composited content to the * default matrix (covering the full view). */ void useDefaultCompositeProjection(); // Events. void viewResized(); void preDrawChildren(); void postDrawChildren(); protected: void glInit(); void glDeinit(); void drawComposite(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_COMPOSITORWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/togglewidget.h0000664000175000017500000000413612641367670026142 0ustar jaakkojaakko/** @file togglewidget.h Toggle widget. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_TOGGLEWIDGET_H #define LIBAPPFW_TOGGLEWIDGET_H #include "../ButtonWidget" namespace de { /** * Toggle is a specialized button that maintains an on/off state in addition to * the state of a ButtonWidget. */ class LIBAPPFW_PUBLIC ToggleWidget : public ButtonWidget { Q_OBJECT public: enum ToggleState { Active, Inactive }; /** * Audience to be notified whenever the toggle is toggled. */ DENG2_DEFINE_AUDIENCE2(Toggle, void toggleStateChanged(ToggleWidget &toggle)) public: ToggleWidget(String const &name = ""); /** * Sets the toggle state of the widget. */ void setToggleState(ToggleState state, bool notify = true); void setActive(bool activate) { setToggleState(activate? Active : Inactive); } void setInactive(bool deactivate) { setToggleState(deactivate? Inactive : Active ); } ToggleState toggleState() const; bool isActive() const { return toggleState() == Active; } bool isInactive() const { return toggleState() == Inactive; } signals: void stateChanged(ToggleWidget::ToggleState active); void stateChangedByUser(ToggleWidget::ToggleState active); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_TOGGLEWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/relaywidget.h0000664000175000017500000000402712641367670025774 0ustar jaakkojaakko/** @file relaywidget.h Relays drawing and events. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_RELAYWIDGET_H #define LIBAPPFW_RELAYWIDGET_H #include "../GuiWidget" namespace de { /** * Relays drawing and events to another widget. * * @ingroup gui */ class RelayWidget : public GuiWidget { public: RelayWidget(GuiWidget *target = nullptr, String const &name = ""); /** * Sets the widget that will be drawn when the relay widget is supposed to be * drawn, and that gets all events received by the relay widget. * * The @a target can be deleted while being a target of a relay. * * @param target Relay target. Ownership not taken. */ void setTarget(GuiWidget *target); GuiWidget *target() const; void initialize(); void deinitialize(); void viewResized(); void update(); bool handleEvent(Event const &event); bool hitTest(Vector2i const &pos) const; void drawContent(); public: /** * Notified when the target of the relay is about to be deleted. The target * still exists when this method is called. */ DENG2_DEFINE_AUDIENCE2(Target, void relayTargetBeingDeleted(RelayWidget &)) private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_RELAYWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/variablechoicewidget.h0000664000175000017500000000315512641367670027621 0ustar jaakkojaakko/** @file variablechoicewidget.h Choice widget for Variable values. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_VARIABLECHOICEWIDGET_H #define LIBAPPFW_VARIABLECHOICEWIDGET_H #include #include "../ChoiceWidget" namespace de { /** * Widget for choosing the value of a variable from a set of possible values. */ class LIBAPPFW_PUBLIC VariableChoiceWidget : public ChoiceWidget { Q_OBJECT public: /// Thrown when the variable is gone and someone tries to access it. @ingroup errors DENG2_ERROR(VariableMissingError); public: VariableChoiceWidget(Variable &variable, String const &name = ""); Variable &variable() const; public slots: void updateFromVariable(); protected slots: void setVariableFromWidget(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_VARIABLECHOICEWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/notificationareawidget.h0000664000175000017500000000577512641367670030212 0ustar jaakkojaakko/** @file notificationareawidget.h Notification area. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_NOTIFICATIONAREAWIDGET_H #define LIBAPPFW_NOTIFICATIONAREAWIDGET_H #include "../RelayWidget" namespace de { /** * Notification area. * * Notification widgets are expected to size themselves and allow unrestricted, * automatical positioning inside the area. Notifications can be added and removed * dynamically. The notification area is dismissed if there are no visible notifications. * * Notification widgets should not be part of the normal widget tree (no add/remove * called). Internally, NotificationAreaWidget uses RelayWidget to link the notifications * to the widget tree. * * The client window owns an instance of NotificationAreaWidget. Other widgets and * subsystems are expected to retain ownership of their notifications, and delete them * when the widget/subsystem is destroyed/shut down. * * Owners of notifications can use the UniqueWidgePtr template to automatically * delete their notification widgets. */ class LIBAPPFW_PUBLIC NotificationAreaWidget : public GuiWidget { Q_OBJECT public: NotificationAreaWidget(String const &name = ""); /** * Places the notification widget in the top right corner of @a area. * * @param area Reference area. */ void useDefaultPlacement(RuleRectangle const &area); Rule const &shift(); /** * Adds a notification to the notification area. If the notification widget is * destroyed while visible, it will simply disappear from the notification area. * Widgets are initialized before showing. * * @param notif Notification widget. */ void showChild(GuiWidget ¬if); /** * Hides a notification. The widget is deinitialized when dismissed (could be * after a delay if the entire notification area is animated away). * * @param notif Notification widget. */ void hideChild(GuiWidget ¬if); void showOrHide(GuiWidget ¬if, bool doShow) { if(doShow) showChild(notif); else hideChild(notif); } bool isChildShown(GuiWidget ¬if) const; public slots: void dismiss(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_NOTIFICATIONAREAWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/foldpanelwidget.h0000664000175000017500000000450312641367670026623 0ustar jaakkojaakko/** @file foldpanelwidget.h Folding panel. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_FOLDPANELWIDGET_H #define LIBAPPFW_FOLDPANELWIDGET_H #include "../PanelWidget" #include "../ButtonWidget" namespace de { /** * Folding panel. * * You should first set the container of the folding panel with setContent(). This * ensures that widgets added to the panel use the appropriate stylist. * * When the fold is closed, the panel contents are GL-deinitialized and removed from the * widget tree entirely. * * @note When the fold is closed, the content widget receives no update() notifications * or events because it is not part of the widget tree. * * If needed, FoldPanelWidget can create a title button for toggling the panel open and * closed. It is the user's responsibility to lay out this button appropriately. */ class LIBAPPFW_PUBLIC FoldPanelWidget : public PanelWidget { Q_OBJECT public: FoldPanelWidget(String const &name = ""); /** * Creates a title button widget for toggling the fold open and closed. * The method does not add the title as a child to anything. * * @param text Text. * * @return Button widget instance. Caller gets ownership. */ ButtonWidget *makeTitle(String const &text = ""); ButtonWidget &title(); void setContent(GuiWidget *content); GuiWidget &content() const; public slots: void toggleFold(); protected: void preparePanelForOpening(); void panelDismissed(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_FOLDPANELWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/popupwidget.h0000664000175000017500000000625112641367670026024 0ustar jaakkojaakko/** @file popupwidget.h Widget that pops up a child widget. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_POPUPWIDGET_H #define LIBAPPFW_POPUPWIDGET_H #include "../PanelWidget" #include "../ui/defs.h" namespace de { /** * Popup anchored to a specified point. Extends PanelWidget with positioning * and an anchor graphic. * * Initially a popup widget is in a hidden state. */ class LIBAPPFW_PUBLIC PopupWidget : public PanelWidget { Q_OBJECT public: PopupWidget(String const &name = ""); /** * Determines how deeply nested this popup is within parent popups. * * @return 0, if parents include no other popups. Otherwise +1 for each popup * present in the parents (i.e., number of popups among ancestors). */ int levelOfNesting() const; void setAnchorAndOpeningDirection(RuleRectangle const &rule, ui::Direction dir); void setAnchor(Vector2i const &pos); void setAnchorX(int xPos); void setAnchorY(int yPos); void setAnchor(Rule const &x, Rule const &y); void setAnchorX(Rule const &x); void setAnchorY(Rule const &y); Rule const &anchorX() const; Rule const &anchorY() const; /** * Replace the anchor with rules of matching constant value. */ void detachAnchor(); /** * Tells the popup to delete itself after being dismissed. The default is that * the popup does not get deleted. * * @param deleteAfterDismiss @c true to delete after dismissal. */ void setDeleteAfterDismissed(bool deleteAfterDismiss); /** * Lets the popup be closed when the user clicks with the mouse outside the * popup area. This is @c true by default. * * @param clickCloses @c true to allow closing with a click. */ void setClickToClose(bool clickCloses); /** * Sets the style of the popup to the one used for informational popups * rather than interactive (the default) ones. */ void useInfoStyle(); bool isUsingInfoStyle(); Background infoStyleBackground() const; // Events. bool handleEvent(Event const &event); protected: void glMakeGeometry(DefaultVertexBuf::Builder &verts); void updateStyle(); virtual void preparePanelForOpening(); virtual void panelDismissed(); private: DENG2_PRIVATE(d) }; template PopupWidget *makePopup() { return new ClassName; } } // namespace de #endif // LIBAPPFW_POPUPWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/panelwidget.h0000664000175000017500000000740112641367670025756 0ustar jaakkojaakko/** @file panelwidget.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_PANELWIDGET_H #define LIBAPPFW_PANELWIDGET_H #include "../ui/defs.h" #include "../ScrollAreaWidget" namespace de { /** * Panel that can be opened or closed. * * A panel always has a single child as the content widget. This content widget * may in turn contain several widgets, though. The size of the content widget * determines the size of the panel. The user must define the position of the * panel. * * Initially panels are in the closed state. They can be opened once the * content widget has been set. */ class LIBAPPFW_PUBLIC PanelWidget : public GuiWidget { Q_OBJECT public: /** * Audience to be notified when the panel is closing. */ DENG2_DEFINE_AUDIENCE2(Close, void panelBeingClosed(PanelWidget &)) public: PanelWidget(String const &name = ""); /** * Sets the size policy for the secondary dimension. For instance, for a * panel that opens horizontally, this determines what is done to the * widget height. * * - ui::Expand (the default) means that the widget automatically uses the * content's size for the secondary dimension. * - ui::Fixed means that the user is expected to define the panel's secondary * dimension and the panel does not touch it. * * @param policy Size policy. */ void setSizePolicy(ui::SizePolicy policy); /** * Sets the content widget of the panel. If there is an earlier content * widget, it will be destroyed. * * @param content Content widget. PanelWidget takes ownership. * @a content becomes a child of the panel. */ void setContent(GuiWidget *content); GuiWidget &content() const; GuiWidget *takeContent(); /** * Sets the opening direction of the panel. * * @param dir Opening direction. */ void setOpeningDirection(ui::Direction dir); ui::Direction openingDirection() const; bool isOpen() const; bool isOpeningOrClosing() const; void close(TimeDelta delayBeforeClosing); // Events. void viewResized(); void update(); void preDrawChildren(); void postDrawChildren(); bool handleEvent(Event const &event); public slots: /** * Opens the panel, positioning it appropriately so that is anchored to the * position specified with setAnchor(). */ virtual void open(); /** * Closes the panel. The widget is dismissed once the closing animation * completes. */ void close(); /** * Immediately hides the panel. */ void dismiss(); signals: void opened(); void closed(); void dismissed(); protected: void glInit(); void glDeinit(); void drawContent(); void glMakeGeometry(DefaultVertexBuf::Builder &verts); virtual void preparePanelForOpening(); virtual void panelClosing(); virtual void panelDismissed(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_PANELWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/buttonwidget.h0000664000175000017500000000617312641367670026177 0ustar jaakkojaakko/** @file buttonwidget.h Clickable button widget. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_BUTTONWIDGET_H #define LIBAPPFW_BUTTONWIDGET_H #include #include #include "../LabelWidget" namespace de { /** * Clickable button widget. * * @ingroup gui */ class LIBAPPFW_PUBLIC ButtonWidget : public LabelWidget { Q_OBJECT public: enum State { Up, Hover, Down }; /** * Notified when the state of the button changes. */ DENG2_DEFINE_AUDIENCE2(StateChange, void buttonStateChanged(ButtonWidget &button, State state)) /** * Notified immediately before the button's action is to be triggered. Will * occur regardless of whether an action has been set. */ DENG2_DEFINE_AUDIENCE2(Press, void buttonPressed(ButtonWidget &button)) /** * Notified when the button's action is triggered (could be before or after * the action). Will not occur if no action has been defined for the * button. */ DENG2_DEFINE_AUDIENCE2(Triggered, void buttonActionTriggered(ButtonWidget &button)) public: ButtonWidget(String const &name = ""); enum HoverColorMode { ReplaceColor, ModulateColor }; void useInfoStyle(bool yes = true); void useNormalStyle() { useInfoStyle(false); } bool isUsingInfoStyle() const; /** * Text color to use in the Hover state. The default is to use the normal text * color of the button (label). * * @param hoverTextId Style color identifier. * @param mode Color hover behavior. */ void setHoverTextColor(DotPath const &hoverTextId, HoverColorMode mode = ModulateColor); void setBackgroundColor(DotPath const &bgColorId); /** * Sets the action of the button. It gets triggered when the button is * pressed. * * @param action Action instance. Widget holds a reference. */ void setAction(RefArg action); Action const *action() const; /** * Triggers the action of the button. */ void trigger(); State state() const; // Events. void update(); bool handleEvent(Event const &event); signals: void pressed(); protected: void updateModelViewProjection(GLUniform &uMvp); void updateStyle(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_BUTTONWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/variabletogglewidget.h0000664000175000017500000000321312641367670027643 0ustar jaakkojaakko/** @file variabletogglewidget.h Toggles the value of a variable. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_VARIABLETOGGLEWIDGET_H #define LIBAPPFW_VARIABLETOGGLEWIDGET_H #include #include "../ToggleWidget" namespace de { /** * Widget for toggling the value of a variable. */ class LIBAPPFW_PUBLIC VariableToggleWidget : public ToggleWidget { public: /// Thrown when the variable is gone and someone tries to access it. @ingroup errors DENG2_ERROR(VariableMissingError); public: VariableToggleWidget(Variable &variable, String const &name = ""); VariableToggleWidget(String const &styledText, Variable &variable, String const &name = ""); Variable &variable() const; void setActiveValue(double val); void setInactiveValue(double val); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_VARIABLETOGGLEWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/labelwidget.h0000664000175000017500000002124212641367670025735 0ustar jaakkojaakko/** @file widgets/labelwidget.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_LABELWIDGET_H #define LIBAPPFW_LABELWIDGET_H #include #include #include #include "../ui/defs.h" #include "../GuiWidget" #include "../ProceduralImage" #include "../ui/Data" namespace de { /** * Widget showing a label text and/or image. * * LabelWidget offers several parameters for controlling the layout of the text * and image components. The widget is also able to independently determine its * size to exactly fit its contents (according to the LabelWidget::SizePolicy). * * The alignment parameters are applied as follows: * * - LabelWidget::setAlignment() defines where the content of the widget is * aligned as a group, once the relative positions of the image and the text * have been determined. * * - LabelWidget::setTextAlignment() defines where the text will be positioned * in relation to the image. For instance, if the text alignment is AlignRight, * the text will be placed on the right side of the image. If there is no * image, this has no effect. * * - LabelWidget::setImageAlignment() defines how the image is aligned in * relation to text when both are visible. For instance, if text is aligned to * AlignRight (appearing on the right side of the image), then an image * alignment of AlignTop would align the top of the image with the top of the * text. AlignBottom would align the bottom of the image with the bottom of the * text. This value must be on the perpendicular axis when compared to text * alignment (otherwise it has no effect). * * - LabelWidget::setTextLineAlignment() defines the alignment of each individual * wrapped line of text within the text block. * * Additionally, LabelWidget::setImageFit() defines how the image will be * scaled inside the area reserved for the image. * * LabelWidget is an Asset because the preparation of text for drawing occurs * asynchronously, and meanwhile the content size of the text is (0,0). * Observing the asset state allows others to determine when the label is ready * to be drawn/laid out with the final dimensions. * * @ingroup gui */ class LIBAPPFW_PUBLIC LabelWidget : public GuiWidget, public AssetGroup { public: LabelWidget(String const &name = ""); void setText(String const &text); void setImage(Image const &image); /** * Sets the image drawn in the label. Procedural images can generate any * geometry on the fly, so the image can be fully animated. * * @param procImage Procedural image. LabelWidget takes ownership. */ void setImage(ProceduralImage *procImage); /** * Sets an overlay image that gets drawn over the label contents. * * @param overlayProcImage Procedural image. LabelWidget takes ownership. * @param alignment Alignment for the overlaid image. */ void setOverlayImage(ProceduralImage *overlayProcImage, ui::Alignment const &alignment = ui::AlignCenter); String text() const; /** * Returns the actual size of the text in pixels. */ Vector2ui textSize() const; Rule const &contentWidth() const; Rule const &contentHeight() const; /** * Sets the gap between the text and image. Defaults to "label.gap". * * @param styleRuleId Id of a rule in the style. */ void setTextGap(DotPath const &styleRuleId); DotPath const &textGap() const; enum AlignmentMode { AlignByCombination, AlignOnlyByImage, AlignOnlyByText }; /** * Sets the alignment of the entire contents of the widget inside its * rectangle. * * @param align Alignment for all content. * @param alignMode Mode of alignment (by combo/text/image). */ void setAlignment(ui::Alignment const &align, AlignmentMode alignMode = AlignByCombination); void setTextAlignment(ui::Alignment const &textAlign); void setTextLineAlignment(ui::Alignment const &textLineAlign); void setTextModulationColorf(Vector4f const &colorf); Vector4f textModulationColorf() const; /** * Sets the maximum width used for text. By default, the maximum width is determined * automatically based on the layout of the label content. * * @param pixels Maximum width of text, or 0 to determine automatically. */ void setMaximumTextWidth(int pixels); void setMaximumTextWidth(Rule const &pixels); /** * Sets an alternative style for text. By default, the rich text styling comes * from Style. * * @param richStyle Rich text styling. */ void setTextStyle(Font::RichFormat::IStyle const *richStyle); /** * Sets the alignment of the image when there is both an image * and a text in the label. * * @param imageAlign Alignment for the image. */ void setImageAlignment(ui::Alignment const &imageAlign); void setImageFit(ui::ContentFit const &fit); /** * The image's actual size will be overridden by this size. * @param size Image size. */ void setOverrideImageSize(Vector2f const &size); Vector2f overrideImageSize() const; void setOverrideImageSize(float widthAndHeight); void setImageScale(float scaleFactor); void setImageColor(Vector4f const &imageColor); bool hasImage() const; /** * Sets the policy for the widget to adjust its own width and/or height. By default, * labels do not adjust their own size. * - ui::Fixed means that the content uses its own size and the widget needs to be * sized by the user. * - ui::Filled means content is expanded to fill the entire contents of the widget * area, but the widget still needs to be sized by the user. * - ui::Expand means the widget resizes itself to the size of the content. * * @param horizontal Horizontal sizing policy. * @param vertical Vertical sizing policy. */ void setSizePolicy(ui::SizePolicy horizontal, ui::SizePolicy vertical) { setWidthPolicy(horizontal); setHeightPolicy(vertical); } void setWidthPolicy(ui::SizePolicy policy); void setHeightPolicy(ui::SizePolicy policy); enum AppearanceAnimation { AppearInstantly, AppearGrowHorizontally, AppearGrowVertically }; /** * Sets the way the label's content affects its size. * * @param method Method of appearance: * - AppearInstantly: The size is unaffected by the content's state. * This is the default. * - AppearGrowHorizontally: The widget's width is initially zero, but when the * content is ready for drawing, the width will animate * to the appropriate width in the specified time span. * - AppearGrowVertically: The widget's height is initially zero, but when the * content is ready for drawing, the height will animate * to the appropriate height in the specified time span. * @param span Animation time span for the appearance. */ void setAppearanceAnimation(AppearanceAnimation method, TimeDelta const &span = 0.0); // Events. void viewResized(); void update(); void drawContent(); struct ContentLayout { Rectanglef image; Rectanglei text; }; void contentLayout(ContentLayout &layout); public: static LabelWidget *newWithText(String const &label, GuiWidget *parent = 0); protected: void glInit(); void glDeinit(); void glMakeGeometry(DefaultVertexBuf::Builder &verts); void updateStyle(); /** * Called before drawing to update the model-view-projection matrix. * Derived classes may override this to set a custom matrix for the label. */ virtual void updateModelViewProjection(GLUniform &uMvp); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_LABELWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/auxbuttonwidget.h0000664000175000017500000000255512641367670026715 0ustar jaakkojaakko/** @file auxbuttonwidget.h Button with an auxiliary button inside. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_AUXBUTTONWIDGET_H #define LIBAPPFW_AUXBUTTONWIDGET_H #include namespace de { /** * Button with another small auxiliary button inside. */ class LIBAPPFW_PUBLIC AuxButtonWidget : public ButtonWidget { public: AuxButtonWidget(String const &name = ""); ButtonWidget &auxiliary(); void useNormalStyle(); void invertStyle(); protected: void updateStyle(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_AUXBUTTONWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/variablelineeditwidget.h0000664000175000017500000000313512641367670030162 0ustar jaakkojaakko/** @file variablelineeditwidget.h Edits the text of a variable. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_VARIABLELINEEDITWIDGET_H #define LIBAPPFW_VARIABLELINEEDITWIDGET_H #include #include "../LineEditWidget" namespace de { /** * Widget for editing the value of a text variable. */ class LIBAPPFW_PUBLIC VariableLineEditWidget : public LineEditWidget { Q_OBJECT public: /// Thrown when the variable is gone and someone tries to access it. @ingroup errors DENG2_ERROR(VariableMissingError); public: VariableLineEditWidget(Variable &variable, String const &name = ""); Variable &variable() const; public slots: void updateFromVariable(); protected slots: void setVariableFromWidget(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_VARIABLELINEEDITWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/popupmenuwidget.h0000664000175000017500000000271312641367670026710 0ustar jaakkojaakko/** @file popupmenuwidget.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_POPUPMENUWIDGET_H #define LIBAPPFW_POPUPMENUWIDGET_H #include "../PopupWidget" #include "../MenuWidget" namespace de { /** * Popup widget that contains a menu. */ class LIBAPPFW_PUBLIC PopupMenuWidget : public PopupWidget { public: PopupMenuWidget(String const &name = ""); MenuWidget &menu() const; ui::Data &items() { return menu().items(); } // Events. void update(); protected: void glMakeGeometry(DefaultVertexBuf::Builder &verts); void preparePanelForOpening(); void panelClosing(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_POPUPMENUWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/gridpopupwidget.h0000664000175000017500000000436212641367670026673 0ustar jaakkojaakko/** @file * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_GRIDPOPUPWIDGET_H #define LIBAPPFW_GRIDPOPUPWIDGET_H #include "../PopupWidget" #include "../LabelWidget" namespace de { class GridLayout; /** * Popup with a grid layout for children. * * The default layout is 2 columns with unlimited rows, with the leftmost * column aligned to the right. * * Used for instance in the settings dialogs. */ class LIBAPPFW_PUBLIC GridPopupWidget : public PopupWidget { public: GridPopupWidget(String const &name = ""); /** * Returns the layout used by the popup's contents. */ GridLayout &layout(); LabelWidget &addSeparatorLabel(String const &labelText); /** * Adds a widget to the popup grid. The widget becomes a child of the * popup's container and is added to the grid layout as the next item. * * @param widget Widget to add. * * @return Reference to this widget (fluent interface). */ GridPopupWidget &operator << (GuiWidget *widget); /** * Adds an empty cell to the popup grid. * * @param rule Amount of space to add in the cell. * * @return Reference to this widget (fluent interface). */ GridPopupWidget &operator << (Rule const &rule); /** * Finalizes the layout of the popup. Call this after all the layout items * have been added to the widget. */ void commit(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_GRIDPOPUPWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/commandwidget.h0000664000175000017500000000476312641367670026305 0ustar jaakkojaakko/** @file commandwidget.h Abstract command line based widget. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_COMMANDWIDGET_H #define LIBAPPFW_COMMANDWIDGET_H #include "../LineEditWidget" namespace de { class PopupWidget; /** * Base class for text editors with a history buffer. Entered text is interpreted * as commands. Supports a Lexicon and a popup for autocompletion. * * @ingroup gui */ class LIBAPPFW_PUBLIC CommandWidget : public LineEditWidget { Q_OBJECT public: CommandWidget(String const &name = ""); PopupWidget &autocompletionPopup(); // Events. void focusGained(); void focusLost(); bool handleEvent(Event const &event); public slots: /** * Moves the current contents of the command line to the history. The * command line contents are then cleared. */ void dismissContentToHistory(); void closeAutocompletionPopup(); protected: /** * Determines if the provided text is accepted as a command by the widget. * This gets called whenever the user presses Enter in the widget. * * @param text Text to check. * * @return @c true, if the text is an executable command. */ virtual bool isAcceptedAsCommand(String const &text) = 0; virtual void executeCommand(String const &text) = 0; /** * Shows the popup with a list of possible completions. * * @param completionsText Styled content for the popup. */ void showAutocompletionPopup(String const &completionsText); void autoCompletionEnded(bool accepted); signals: void gotFocus(); void lostFocus(); void commandEntered(de::String const &command); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_COMMANDWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/lineeditwidget.h0000664000175000017500000000533612641367670026461 0ustar jaakkojaakko/** @file widgets/lineeditwidget.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_LINEEDITWIDGET_H #define LIBAPPFW_LINEEDITWIDGET_H #include "../GuiWidget" #include #include namespace de { /** * Widget showing a lineedit text and/or image. * * As a graphical widget, widget placement and line wrapping is handled in * terms of pixels rather than characters. * * @ingroup gui */ class LIBAPPFW_PUBLIC LineEditWidget : public GuiWidget, public shell::AbstractLineEditor { Q_OBJECT public: LineEditWidget(String const &name = ""); /** * Sets the text that will be shown in the editor when it is empty. * * @param hintText Hint text. */ void setEmptyContentHint(String const &hintText); /** * Enables or disables the signal emitted when the edit widget receives an * Enter key. By default, no a signal is emitted (and the key is thus not * eaten). * * @param enterSignal @c true to enable signal and eat event, @c false to * disable. */ void setSignalOnEnter(bool enterSignal); /** * Determines where the cursor is currently in view coordinates. */ Rectanglei cursorRect() const; // Events. void viewResized(); void focusGained(); void focusLost(); void update(); void drawContent(); bool handleEvent(Event const &event); public: static KeyModifiers modifiersFromKeyEvent(KeyEvent::Modifiers const &keyMods); signals: void enterPressed(QString text); void editorContentChanged(); protected: void glInit(); void glDeinit(); void glMakeGeometry(DefaultVertexBuf::Builder &verts); void updateStyle(); int maximumWidth() const; void numberOfLinesChanged(int lineCount); void cursorMoved(); void contentChanged(); void autoCompletionEnded(bool accepted); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_LINEEDITWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/documentwidget.h0000664000175000017500000000571412641367670026502 0ustar jaakkojaakko/** @file documentwidget.h Widget for displaying large amounts of text. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_DOCUMENTWIDGET_H #define LIBAPPFW_DOCUMENTWIDGET_H #include "../ScrollAreaWidget" #include "../ui/defs.h" namespace de { /** * Widget for displaying large amounts of text. * * Based on ScrollAreaWidget for scrolling. Only the visible portion of the * source text is kept ready for drawing -- the length of the source document * is thus unlimited. Only vertical scrolling is supported at the moment. * * DocumentWidget can be configured to expand horizontally, or it can use a * certain determined fixed width (see DocumentWidget::setWidthPolicy()). * * By default, the height of the widget is determined by its content size. * * The assumption is that the source document is largely static so that once * prepared, the GL resources can be reused as many times as possible. */ class LIBAPPFW_PUBLIC DocumentWidget : public ScrollAreaWidget { public: DocumentWidget(String const &name = ""); /** * Sets the text content of the widget. Style escapes can be used. * * @param styledText Text content. */ void setText(String const &styledText); /** * Sets the policy for managing the widget's width. * - ui::Fixed means that the widget's width must be defined externally, * and the width is also used as the content's width. * - ui::Expand means that the widget's width automatically expands * to fit the entire content, but the maximum line width is never * exceeded. * * The default policy is ui::Expand. * * @param policy Size policy for the widget's width. */ void setWidthPolicy(ui::SizePolicy policy); /** * Sets the maximum line width when using ui::Expand as the width policy. * * @param maxWidth Maximum width of a text line. */ void setMaximumLineWidth(int maxWidth); // Events. void viewResized(); void drawContent(); protected: void glInit(); void glDeinit(); void glMakeGeometry(DefaultVertexBuf::Builder &verts); void updateStyle(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_DOCUMENTWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/documentpopupwidget.h0000664000175000017500000000253312641367670027562 0ustar jaakkojaakko/** @file documentpopupwidget.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_DOCUMENTPOPUPWIDGET_H #define LIBAPPFW_DOCUMENTPOPUPWIDGET_H #include "../DocumentWidget" #include "../PopupWidget" namespace de { /** * Utility widget that has a document inside a popup. */ class LIBAPPFW_PUBLIC DocumentPopupWidget : public PopupWidget { Q_OBJECT public: DocumentPopupWidget(String const &name = ""); DocumentWidget &document(); DocumentWidget const &document() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_DOCUMENTPOPUPWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/tabwidget.h0000664000175000017500000000437012641367670025427 0ustar jaakkojaakko/** @file tabwidget.h Tab widget. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_TABWIDGET_H #define LIBAPPFW_TABWIDGET_H #include "../GuiWidget" #include "../ui/ImageItem" #include "../ui/Data" namespace de { /** * Tab buttons. Behaves like radio buttons, with one of the buttons being selected at a * time. One of the tabs is always selected. * * The widget sets its own height based on the height of the tab buttons. Tab buttons * are centered in the width of the widget. */ class LIBAPPFW_PUBLIC TabWidget : public GuiWidget { Q_OBJECT public: class TabItem : public ui::ImageItem { public: TabItem(String const &label, QVariant const &userData = QVariant()) : ImageItem(ShownAsButton, label) { setData(userData); } TabItem(Image const &img, String const &label) : ImageItem(ShownAsButton, img, label) {} }; public: TabWidget(String const &name = ""); void useInvertedStyle(); /** * Items representing the tabs in the widget. * * @return */ ui::Data &items(); /** * Returns the currently selected tab index. One of the tabs is always selected. */ ui::Data::Pos current() const; TabItem ¤tItem(); void setCurrent(ui::Data::Pos itemPos); // Events. void update(); signals: void currentTabChanged(); private: DENG2_PRIVATE(d) }; typedef TabWidget::TabItem TabItem; } // namespace de #endif // LIBAPPFW_TABWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/blurwidget.h0000664000175000017500000000237012641367670025623 0ustar jaakkojaakko/** @file blurwidget.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_BLURWIDGET_H #define LIBAPPFW_BLURWIDGET_H #include "../GuiWidget" namespace de { /** * Utility widget for drawing blurred widget backgrounds. Many widgets can * share the same blurred background texture, assuming they don't overlap each * other. */ class LIBAPPFW_PUBLIC BlurWidget : public GuiWidget { public: BlurWidget(String const &name = ""); }; } // namespace de #endif // LIBAPPFW_BLURWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/scrollareawidget.h0000664000175000017500000001173112641367670027007 0ustar jaakkojaakko/** @file scrollareawidget.h Scrollable area. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_SCROLLAREAWIDGET_H #define LIBAPPFW_SCROLLAREAWIDGET_H #include "../GuiWidget" namespace de { /** * Scrollable area. * * ScrollAreaWidget does not control its own position or size. The user must * define its rectangle. The content rule rectangle is defined in relation to * the widget's rectangle. * * The user must always define the size of the content area. * * ScrollAreaWidget can optionally independently draw a scroll indicator. * However, the default behavior is that the user must call * glMakeScrollIndicatorGeometry() to include the indicator geometry as part of * the derived/owner widget. */ class LIBAPPFW_PUBLIC ScrollAreaWidget : public GuiWidget { Q_OBJECT public: enum Origin { Top, ///< Scroll position 0 is at the top. Bottom ///< Scroll position 0 is at the bottom. }; public: ScrollAreaWidget(String const &name = ""); void setScrollBarColor(DotPath const &colorId); void setOrigin(Origin origin); Origin origin() const; void setIndicatorUv(Rectanglef const &uv); void setIndicatorUv(Vector2f const &uvPoint); void setContentWidth(int width); void setContentWidth(Rule const &width); void setContentHeight(int height); void setContentHeight(Rule const &height); void setContentSize(Rule const &width, Rule const &height); void setContentSize(Vector2i const &size); void setContentSize(Vector2ui const &size); void modifyContentWidth(int delta); void modifyContentHeight(int delta); int contentWidth() const; int contentHeight() const; RuleRectangle const &contentRule() const; ScalarRule &scrollPositionX() const; ScalarRule &scrollPositionY() const; Rule const &maximumScrollX() const; Rule const &maximumScrollY() const; bool isScrolling() const; Rectanglei viewport() const; Vector2i viewportSize() const; /** * Returns the current scroll XY position, with 0 being the top/left corner * and maximumScroll() being the bottom right position. */ Vector2i scrollPosition() const; virtual Vector2i scrollPageSize() const; /** * Returns the maximum scroll position. The scrollMaxChanged() signal * is emitted whenever the maximum changes. */ Vector2i maximumScroll() const; /** * Scrolls the view to a specified position. Position (0,0) means the top * left corner is visible at the top left corner of the ScrollAreaWidget. * * @param to Scroll position. * @param span Animation time span. */ void scroll(Vector2i const &to, TimeDelta span = 0); void scrollX(int to, TimeDelta span = 0); void scrollY(int to, TimeDelta span = 0); /** * Determines if the history view is at the bottom, showing the latest entry. */ bool isAtBottom() const; /** * Enables or disables scrolling. By default, scrolling is enabled. * * @param enabled @c true to enable scrolling. */ void enableScrolling(bool enabled); /** * Enables or disables scrolling with Page Up/Down keys. * * @param enabled @c true to enable Page Up/Down. */ void enablePageKeys(bool enabled); /** * Enables or disables the drawing of the scroll indicator. * * @param enabled @c true to enable the indicator. The default is @c false. */ void enableIndicatorDraw(bool enabled); void glMakeScrollIndicatorGeometry(DefaultVertexBuf::Builder &verts, Vector2f const &origin = Vector2f(0, 0)); // Events. void viewResized(); void update(); void drawContent(); void preDrawChildren(); void postDrawChildren(); bool handleEvent(Event const &event); public slots: void scrollToTop(TimeDelta span = .3f); /** * Moves the scroll offset of the widget to the bottom of the content. */ void scrollToBottom(TimeDelta span = .3f); void scrollToLeft(TimeDelta span = .3f); void scrollToRight(TimeDelta span = .3f); protected: void glInit(); void glDeinit(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_SCROLLAREAWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/choicewidget.h0000664000175000017500000000612012641367670026106 0ustar jaakkojaakko/** @file widgets/choicewidget.h Widget for choosing from a set of alternatives. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_CHOICEWIDGET_H #define LIBAPPFW_CHOICEWIDGET_H #include "../ButtonWidget" #include "../PopupMenuWidget" #include "../ui/ActionItem" namespace de { /** * Widget for choosing an item from a set of alternatives. * * The items of the widget should be ChoiceItem instances, or at least derived * from ActionItem/ChoiceItem. * * The default opening direction for the popup is to the right. */ class LIBAPPFW_PUBLIC ChoiceWidget : public ButtonWidget { Q_OBJECT public: /** * The items of the widget are expected to be instanced of * ChoiceWidget::Item or derived from it (or at least ui::ActionItem). */ class Item : public ui::ActionItem { public: Item(String const &label, Image const &image = Image()) : ui::ActionItem(image, label) {} Item(String const &label, QVariant const &userData, Image const &image = Image()) : ui::ActionItem(image, label) { setData(userData); } }; public: ChoiceWidget(String const &name = ""); void setOpeningDirection(ui::Direction dir); ui::Data &items(); /** * Sets the data model of the choice widget to some existing one. The data must * remain in existence until the ChoiceWidget is deleted. * * @param items Ownership not taken. */ void setItems(ui::Data const &items); void useDefaultItems(); PopupMenuWidget &popup(); void setSelected(ui::Data::Pos pos); ui::Data::Pos selected() const; ui::Item const &selectedItem() const; /** * Returns a rule that determines what is the maximum width of the widget. This is * the length of the longest item plus margins. * * A choice widget keeps changing its size depending on the selected item. Also, only * the selected item uses the "choice.selected" font, so the maximum width depends on * what is the widest item using that font. */ Rule const &maximumWidth() const; public slots: void openPopup(); signals: void selectionChanged(uint pos); void selectionChangedByUser(uint pos); private: DENG2_PRIVATE(d) }; typedef ChoiceWidget::Item ChoiceItem; } // namespace de #endif // LIBAPPFW_CHOICEWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/sliderwidget.h0000664000175000017500000000457512641367670026152 0ustar jaakkojaakko/** @file sliderwidget.h Slider to pick a value within a range. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_SLIDERWIDGET_H #define LIBAPPFW_SLIDERWIDGET_H #include "../GuiWidget" #include namespace de { /** * Slider to pick a value within a range. * * The value can also be entered as text by right clicking on the slider. */ class LIBAPPFW_PUBLIC SliderWidget : public GuiWidget { Q_OBJECT public: SliderWidget(String const &name = ""); void setRange(Rangei const &intRange, int step = 1); void setRange(Rangef const &floatRange, float step = 0); void setRange(Ranged const &doubleRange, ddouble step = 0); void setPrecision(int precisionDecimals); void setStep(double step); void setValue(ddouble value); void setMinLabel(String const &labelText); void setMaxLabel(String const &labelText); /** * Displayed values are multiplied by this factor when displayed. * Does not affect the real value of the slider. * * @param factor Display multiplier. */ void setDisplayFactor(ddouble factor); Ranged range() const; ddouble value() const; int precision() const; ddouble displayFactor() const; // Events. void viewResized(); void update(); void drawContent(); bool handleEvent(Event const &event); public slots: void setValueFromText(QString text); signals: void valueChanged(double value); void valueChangedByUser(double value); protected: void glInit(); void glDeinit(); void updateStyle(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_SLIDERWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/logwidget.h0000664000175000017500000000454312641367670025444 0ustar jaakkojaakko/** @file widgets/logwidget.h Scrollable widget for log message history. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_LOGWIDGET_H #define LIBAPPFW_LOGWIDGET_H #include #include #include #include "../ScrollAreaWidget" namespace de { /** * Scrollable widget for log message history. * * You must specify a log entry formatter using setLogFormatter() after creating the * widget. Otherwise the widget won't be able to show any entries. * * @ingroup gui */ class LIBAPPFW_PUBLIC LogWidget : public ScrollAreaWidget { Q_OBJECT public: LogWidget(String const &name = ""); /** * Sets the formatter that will be used for formatting log entries for the widget. * * @param formatter Formatter. Must exist as long as the LogWidget exists. */ void setLogFormatter(LogSink::IFormatter &formatter); /** * Returns the log sink that can be connected to a log buffer for receiving * log entries into the widget's buffer. */ LogSink &logSink(); /** * Removes all entries from the log. */ void clear(); void setContentYOffset(Animation const &anim); Animation const &contentYOffset() const; // Events. void viewResized(); void update(); void drawContent(); bool handleEvent(Event const &event); signals: //void scrollPositionChanged(int pos); //void scrollMaxChanged(int maximum); void contentHeightIncreased(int delta); protected: void glInit(); void glDeinit(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_LOGWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/menuwidget.h0000664000175000017500000001145212641367670025624 0ustar jaakkojaakko/** @file widgets/menuwidget.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_MENUWIDGET_H #define LIBAPPFW_MENUWIDGET_H #include "../ui/Data" #include "../ui/ActionItem" #include "../ui/SubmenuItem" #include "../ui/VariableToggleItem" #include "../ChildWidgetOrganizer" #include "../GridLayout" #include "../ScrollAreaWidget" #include "../ButtonWidget" #include "../PanelWidget" namespace de { /** * Menu with an N-by-M grid of items (child widgets). * * One or both of the dimensions of the menu grid can be configured to use ui::Expand * policy, in which case the child widgets must manage their size on that axis by * themselves. * * A sort order for the items can be optionally defined using * MenuWidget::ISortOrder. Sorting affects layout only, not the actual order of * the children. * * MenuWidget uses a ChildWidgetOrganizer to create widgets based on the * provided menu items. The organizer can be queried to find widgets matching * specific items. */ class LIBAPPFW_PUBLIC MenuWidget : public ScrollAreaWidget { Q_OBJECT public: MenuWidget(String const &name = ""); /** * Configures the layout grid. * * ui::Fixed policy means that the size of the menu rectangle is fixed, and * the size of the children is not modified. * * ui::Filled policy means that the size of the menu rectangle is fixed, * and the size of the children is adjusted to evenly fill the entire menu * rectangle. * * If a dimension is set to ui::Expand policy, the menu's size in that dimension is * determined by the summed up size of the children. * * If the number of columns/rows is set to zero, it means that the number of * columns/rows will increase without limitation. Both dimensions cannot be set to * zero columns/rows. * * @param columns Number of columns in the grid. * @param columnPolicy Policy for sizing columns. * @param rows Number of rows in the grid. * @param rowPolicy Policy for sizing rows. * @param layoutMode Layout mode (column or row first). */ void setGridSize(int columns, ui::SizePolicy columnPolicy, int rows, ui::SizePolicy rowPolicy, GridLayout::Mode layoutMode = GridLayout::ColumnFirst); ui::Data &items(); ui::Data const &items() const; /** * Sets the data context of the menu to some existing context. The context * must remain in existence until the MenuWidget is deleted. * * @param items Ownership not taken. */ void setItems(ui::Data const &items); void useDefaultItems(); bool isUsingDefaultItems() const; ChildWidgetOrganizer &organizer(); ChildWidgetOrganizer const &organizer() const; template WidgetType &itemWidget(ui::Item const &item) const { return organizer().itemWidget(item)->as(); } /** * Returns the number of visible items in the menu. Hidden items are not * included in this count. */ int count() const; /** * Determines if a widget is included in the menu. Hidden widgets are not * part of the menu. * * @param widget Widget. * * @return @c true, if the widget is laid out as part of the menu. */ bool isWidgetPartOfMenu(Widget const &widget) const; /** * Lays out children of the menu according to the grid setup. This should * be called if children are manually added or removed from the menu. */ void updateLayout(); /** * Provides read-only access to the layout metrics. */ GridLayout const &layout() const; GridLayout &layout(); // Events. void update(); bool handleEvent(Event const &event); public slots: void dismissPopups(); signals: /** * Called when a submenu/widget is opened by one of the items. * * @param panel Panel that was opened. */ void subWidgetOpened(de::PanelWidget *panel); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_MENUWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/progresswidget.h0000664000175000017500000000564612641367670026534 0ustar jaakkojaakko/** @file progresswidget.h Progress indicator. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_PROGRESSWIDGET_H #define LIBAPPFW_PROGRESSWIDGET_H #include "../LabelWidget" #include namespace de { /** * Progress indicator. * * Implemented as a specialized LabelWidget. The label is used for drawing the * static part of the indicator wheel and the status text. The ProgressWidget * draws the dynamic, animating part. * * @par Thread-safety * * The status of a ProgressWidget can be updated from any thread. This allows * background tasks to update the status during their operations. * * @todo Needs a bit of cleanup: the visual style (large gear, small gear, dots) * and the range setup should be separate concepts. */ class LIBAPPFW_PUBLIC ProgressWidget : public LabelWidget { public: enum Mode { Ranged, Indefinite, Dots ///< One dot per range unit, no label. }; public: ProgressWidget(String const &name = ""); void useMiniStyle(DotPath const &colorId = "text"); void setRotationSpeed(float anglesPerSecond); Mode mode() const; Rangei range() const; bool isAnimating() const; void setColor(DotPath const &styleId); void setShadowColor(DotPath const &styleId); /** * Sets the text displayed in the widget. Thread-safe. * * @param text New text for the progress. */ void setText(String const &text); void setMode(Mode progressMode); /** * Sets the range of values that can be given in setProgress(). * Automatically switches the widget to Ranged mode. * * @param range Range of valid values for setProgress(). * @param visualRange Range to which @a range maps to (within 0...1). */ void setRange(Rangei const &range, Rangef const &visualRange = Rangef(0.f, 1.f)); void setProgress(int currentProgress, TimeDelta const &transitionSpan = 0.5); // Events. void update(); protected: void glInit(); void glDeinit(); void glMakeGeometry(DefaultVertexBuf::Builder &verts); void updateStyle(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_PROGRESSWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/widgets/scriptcommandwidget.h0000664000175000017500000000373312641367670027526 0ustar jaakkojaakko/** @file scriptcommandwidget.h Interactive Doomsday Script command line. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBAPPFW_SCRIPTCOMMANDWIDGET_H #define LIBAPPFW_SCRIPTCOMMANDWIDGET_H #include "../CommandWidget" namespace de { /** * Interactive Doomsday Script command line. * * The widget has its own script Process in which all commands gets executed. * The namespace of this process persists across entered commands (but not * across engine shutdown). * * An entered command is not accepted until it parses successfully as a * Doomsday Script statement. In other words, it is possible to enter * multi-line scripts when there are open braces etc. If there is a true syntax * error in the entered script, a popup will open showing the exception from * the parser. * * @ingroup gui */ class LIBAPPFW_PUBLIC ScriptCommandWidget : public CommandWidget { public: ScriptCommandWidget(String const &name = ""); bool handleEvent(Event const &event); protected: bool isAcceptedAsCommand(String const &text); void executeCommand(String const &text); void autoCompletionBegan(String const &prefix); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBAPPFW_SCRIPTCOMMANDWIDGET_H doomsday-stable-1.15.7/doomsday/libappfw/include/de/MenuWidget0000664000175000017500000000004112641367670023620 0ustar jaakkojaakko#include "widgets/menuwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/LogWidget0000664000175000017500000000004012641367670023434 0ustar jaakkojaakko#include "widgets/logwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/DocumentWidget0000664000175000017500000000004512641367670024476 0ustar jaakkojaakko#include "widgets/documentwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/BaseWindow0000664000175000017500000000004312641367670023614 0ustar jaakkojaakko#include "framework/basewindow.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/MessageDialog0000664000175000017500000000004412641367670024257 0ustar jaakkojaakko#include "dialogs/messagedialog.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/VRConfig0000664000175000017500000000003212641367670023225 0ustar jaakkojaakko#include "vr/vrconfig.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/FoldPanelWidget0000664000175000017500000000004612641367670024565 0ustar jaakkojaakko#include "widgets/foldpanelwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/GuiRootWidget0000664000175000017500000000004512641367670024310 0ustar jaakkojaakko#include "framework/guirootwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/AuxButtonWidget0000664000175000017500000000004512641367670024651 0ustar jaakkojaakko#include "widgets/auxbuttonwidget.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/DialogContentStylist0000664000175000017500000000005412641367670025702 0ustar jaakkojaakko#include "framework/dialogcontentstylist.h" doomsday-stable-1.15.7/doomsday/libappfw/include/de/InputDialog0000664000175000017500000000004212641367670023770 0ustar jaakkojaakko#include "dialogs/inputdialog.h" doomsday-stable-1.15.7/doomsday/libappfw/src/0000775000175000017500000000000012641367670020406 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libappfw/src/listdata.cpp0000664000175000017500000000565312641367670022730 0ustar jaakkojaakko/** @file listdata.cpp List-based UI data context. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/ui/ListData" #include #include namespace de { using namespace ui; ListData::~ListData() { // Delete all items. qDeleteAll(_items); } dsize ListData::size() const { return _items.size(); } Item &ListData::at(Data::Pos pos) { DENG2_ASSERT(pos < size()); return *_items[pos]; } Item const &ListData::at(Pos pos) const { DENG2_ASSERT(pos < size()); return *_items.at(pos); } Data::Pos ListData::find(Item const &item) const { for(Pos i = 0; i < size(); ++i) { if(&at(i) == &item) return i; } return InvalidPos; } Data::Pos ListData::findData(QVariant const &data) const { for(Pos i = 0; i < size(); ++i) { if(at(i).data() == data) return i; } return InvalidPos; } Data &ListData::clear() { while(!isEmpty()) { remove(size() - 1); } return *this; } Data &ListData::insert(Pos pos, Item *item) { _items.insert(pos, item); item->setDataContext(*this); // Notify. DENG2_FOR_AUDIENCE2(Addition, i) { i->dataItemAdded(pos, *item); } return *this; } void ListData::remove(Pos pos) { delete take(pos); } Item *ListData::take(Data::Pos pos) { DENG2_ASSERT(pos < size()); Item *taken = _items.takeAt(pos); // Notify. DENG2_FOR_AUDIENCE2(Removal, i) { i->dataItemRemoved(pos, *taken); } return taken; } struct ListItemSorter { Data::LessThanFunc lessThan; ListItemSorter(Data::LessThanFunc func) : lessThan(func) {} bool operator () (Item const *a, Item const *b) const { return lessThan(*a, *b); } }; void ListData::sort(LessThanFunc lessThan) { qSort(_items.begin(), _items.end(), ListItemSorter(lessThan)); // Notify. DENG2_FOR_AUDIENCE2(OrderChange, i) { i->dataItemOrderChanged(); } } void ListData::stableSort(LessThanFunc lessThan) { qStableSort(_items.begin(), _items.end(), ListItemSorter(lessThan)); // Notify. DENG2_FOR_AUDIENCE2(OrderChange, i) { i->dataItemOrderChanged(); } } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/gltextcomposer.cpp0000664000175000017500000003736212641367670024204 0ustar jaakkojaakko/** @file gltextcomposer.cpp * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GLTextComposer" #include namespace de { using namespace ui; static Rangei const MAX_VISIBLE_RANGE(0, 0x7fffffff); DENG2_PIMPL(GLTextComposer) { Font const *font = nullptr; Atlas *atlas = nullptr; String text; FontLineWrapping const *wraps = nullptr; Font::RichFormat format; bool needRedo = false; Rangei visibleLineRange { MAX_VISIBLE_RANGE }; ///< Only these lines will be updated/drawn. int maxGeneratedWidth = 0; struct Line { struct Segment { Id id; Rangei range; String text; int x; int width; bool compressed; Segment() : id(Id::None), x(0), width(0), compressed(false) {} int right() const { return x + width; } }; QList segs; }; typedef QList Lines; Lines lines; Instance(Public *i) : Base(i) {} ~Instance() { releaseLines(); } void releaseLines() { if(atlas) { for(int i = 0; i < lines.size(); ++i) { releaseLine(i); } } lines.clear(); } void releaseOutsideRange() { if(!atlas) return; for(int i = 0; i < lines.size(); ++i) { if(!isLineVisible(i)) { releaseLine(i, ReleaseButKeepSegs); } } } enum ReleaseBehavior { ReleaseFully, ReleaseButKeepSegs }; void releaseLine(int index, ReleaseBehavior behavior = ReleaseFully) { Line &ln = lines[index]; for(int i = 0; i < ln.segs.size(); ++i) { if(!ln.segs[i].id.isNone()) { atlas->release(ln.segs[i].id); ln.segs[i].id = Id::None; } } if(behavior == ReleaseFully) { ln.segs.clear(); } } bool isLineVisible(int line) const { return visibleLineRange.contains(line); } String segmentText(int seg, FontLineWrapping::LineInfo const &info) const { return text.substr(info.segs[seg].range); } bool matchingSegments(int lineIndex, FontLineWrapping::LineInfo const &info) const { if(info.segs.size() != lines[lineIndex].segs.size()) { //qDebug() << "line" << lineIndex << "number of segs changed"; return false; } for(int i = 0; i < info.segs.size(); ++i) { if(info.segs[i].range != lines[lineIndex].segs[i].range) { // Range has changed. //qDebug() << "line" << lineIndex << "seg" << i << "range change"; return false; } if(segmentText(i, info) != lines[lineIndex].segs[i].text) { // Text has changed. //qDebug() << "line" << lineIndex << "seg" << i << "text change"; return false; } if(lines[lineIndex].segs[i].id.isNone() && info.segs[i].range.size() > 0) { // This segment has previously failed allocation. //qDebug() << "line" << lineIndex << "seg" << i << "not alloced before, len" << // info.segs[i].range.size(); return false; } } return true; } bool allocLines() { bool changed = false; for(int i = 0; i < wraps->height(); ++i) { FontLineWrapping::LineInfo const &info = wraps->lineInfo(i); if(i < lines.size()) { // Is the rasterized copy up to date? if(/*!isLineVisible(i) ||*/ matchingSegments(i, info)) { // This line can be kept as is. continue; } // Needs to be redone. releaseLine(i); } changed = true; if(i >= lines.size()) { // Need another line. lines << Line(); } DENG2_ASSERT(i < lines.size()); DENG2_ASSERT(lines[i].segs.isEmpty()); Line &line = lines[i]; for(int k = 0; k < info.segs.size(); ++k) { Line::Segment seg; seg.range = info.segs[k].range; seg.text = segmentText(k, info); if(isLineVisible(i) && seg.range.size() > 0) { // The color is white unless a style is defined. Vector4ub fgColor(255, 255, 255, 255); if(format.hasStyle()) { fgColor = format.style().richStyleColor(Font::RichFormat::NormalColor); } // Set up the background color to be transparent with no // change of color in the alphablended smooth edges. Vector4ub bgColor = fgColor; bgColor.w = 0; seg.id = atlas->alloc(font->rasterize(seg.text, format.subRange(seg.range), fgColor, bgColor)); } line.segs << seg; } DENG2_ASSERT(line.segs.size() == info.segs.size()); } // Remove the excess lines. while(lines.size() > wraps->height()) { releaseLine(lines.size() - 1); lines.removeLast(); changed = true; } DENG2_ASSERT(wraps->height() == lines.size()); return changed; } void updateLineLayout(Rangei const &lineRange) { if(lineRange.isEmpty()) return; Rangei current = lineRange; forever { int end = updateLineLayoutUntilUntabbed(current); if(end == lineRange.end) { break; // Whole range done. } current = Rangei(end, lineRange.end); } } /** * Attempts to update lines in the specified range, but stops if an * untabbed line is encountered. This ensures that each distinct tabbed * content subrange uses its own alignment. * * @param lineRange Range of lines to update. * * @return The actual end of the updated range. */ inline int updateLineLayoutUntilUntabbed(Rangei const &lineRange) { bool includesTabbedLines = false; int rangeEnd = lineRange.end; // Find the highest tab in use and initialize seg widths. int highestTab = 0; for(int i = lineRange.start; i < lineRange.end; ++i) { int lineStop = wraps->lineInfo(i).highestTabStop(); if(lineStop >= 0) { // The range now includes at least one tabbed line. includesTabbedLines = true; } if(lineStop < 0) { // This is an untabbed line. if(!includesTabbedLines) { // We can do many untabbed lines in the range as long as // there are no tabbed ones. rangeEnd = i + 1; } else { // An untabbed line will halt the process for now. rangeEnd = de::max(i, lineRange.start + 1); break; } } highestTab = de::max(highestTab, lineStop); // Initialize the segments with indentation. for(int k = 0; k < lines[i].segs.size(); ++k) { lines[i].segs[k].width = wraps->lineInfo(i).segs[k].width; } } DENG2_ASSERT(rangeEnd > lineRange.start); // Set segment X coordinates by stacking them left-to-right on each line. for(int i = lineRange.start; i < rangeEnd; ++i) { if(lines[i].segs.isEmpty() || i >= visibleLineRange.end) continue; lines[i].segs[0].x = wraps->lineInfo(i).indent; for(int k = 1; k < lines[i].segs.size(); ++k) { Instance::Line::Segment &seg = lines[i].segs[k]; seg.x = lines[i].segs[k - 1].right(); } } // Align each tab stop with other matching stops on the other lines. for(int tab = 1; tab <= highestTab; ++tab) { int maxRight = 0; // Find the maximum right edge for this spot. for(int i = lineRange.start; i < rangeEnd; ++i) { if(i >= visibleLineRange.end) break; FontLineWrapping::LineInfo const &info = wraps->lineInfo(i); DENG2_ASSERT(info.segs.size() == lines[i].segs.size()); for(int k = 0; k < info.segs.size(); ++k) { Instance::Line::Segment &seg = lines[i].segs[k]; if(info.segs[k].tabStop >= 0 && info.segs[k].tabStop < tab) { maxRight = de::max(maxRight, seg.right()); } } } // Move the segments to this position. for(int i = lineRange.start; i < rangeEnd; ++i) { if(i >= visibleLineRange.end) break; int localRight = maxRight; FontLineWrapping::LineInfo const &info = wraps->lineInfo(i); for(int k = 0; k < info.segs.size(); ++k) { if(info.segs[k].tabStop == tab) { lines[i].segs[k].x = localRight; localRight += info.segs[k].width; } } } } return rangeEnd; } }; GLTextComposer::GLTextComposer() : d(new Instance(this)) {} void GLTextComposer::release() { d->releaseLines(); d->visibleLineRange = MAX_VISIBLE_RANGE; setState(false); } void GLTextComposer::releaseLinesOutsideRange() { d->releaseOutsideRange(); } void GLTextComposer::setAtlas(Atlas &atlas) { d->atlas = &atlas; } void GLTextComposer::setWrapping(FontLineWrapping const &wrappedLines) { if(d->wraps != &wrappedLines) { d->wraps = &wrappedLines; forceUpdate(); } } void GLTextComposer::setText(String const &text) { setText(text, Font::RichFormat::fromPlainText(text)); } void GLTextComposer::setStyledText(String const &styledText) { d->format.clear(); d->text = d->format.initFromStyledText(styledText); setState(false); } void GLTextComposer::setText(String const &text, Font::RichFormat const &format) { d->text = text; d->format = format; setState(false); } void GLTextComposer::setRange(Rangei const &visibleLineRange) { d->visibleLineRange = visibleLineRange; } Rangei GLTextComposer::range() const { return d->visibleLineRange; } bool GLTextComposer::update() { DENG2_ASSERT(d->wraps != 0); // If a font hasn't been defined, there isn't much to do. if(!d->wraps->hasFont()) return false; if(d->font != &d->wraps->font()) { d->font = &d->wraps->font(); forceUpdate(); } if(d->needRedo) { d->releaseLines(); d->needRedo = false; } setState(true); return d->allocLines(); } void GLTextComposer::forceUpdate() { d->needRedo = true; } void GLTextComposer::makeVertices(Vertices &triStrip, Vector2i const &topLeft, Alignment const &lineAlign, Vector4f const &color) { makeVertices(triStrip, Rectanglei(topLeft, topLeft), AlignTopLeft, lineAlign, color); } void GLTextComposer::makeVertices(Vertices &triStrip, Rectanglei const &rect, Alignment const &alignInRect, Alignment const &lineAlign, Vector4f const &color) { if(!isReady()) return; DENG2_ASSERT(d->wraps != 0); DENG2_ASSERT(d->font != 0); Vector2i const contentSize(d->wraps->width(), d->wraps->totalHeightInPixels()); // Apply alignment within the provided rectangle. Vector2f p = applyAlignment(alignInRect, contentSize, rect); DENG2_ASSERT(d->wraps->height() == d->lines.size()); // Align segments based on tab stops. d->updateLineLayout(Rangei(0, d->lines.size())); // Compress lines to fit into the maximum allowed width. for(int i = 0; i < d->lines.size(); ++i) { Instance::Line &line = d->lines[i]; if(!d->isLineVisible(i) || line.segs.isEmpty()) continue; /* if(!d->wraps->lineInfo(i).segs.last().tabStop) continue; */ Instance::Line::Segment &seg = line.segs.last(); int const leeway = 3; if(seg.right() > d->wraps->maximumWidth() + leeway) { // Needs compressing (up to 10%). seg.compressed = true; seg.width = de::max(int(seg.width * .9f), d->wraps->maximumWidth() + leeway - seg.x); } } d->maxGeneratedWidth = 0; // Generate vertices for each line. for(int i = 0; i < d->wraps->height(); ++i) { Instance::Line const &line = d->lines[i]; if(d->isLineVisible(i)) { FontLineWrapping::LineInfo const &info = d->wraps->lineInfo(i); Vector2f linePos = p; for(int k = 0; k < info.segs.size(); ++k) { Instance::Line::Segment const &seg = line.segs[k]; // Empty lines are skipped. if(seg.id.isNone()) continue; Vector2ui size = d->atlas->imageRect(seg.id).size(); if(seg.compressed) { size.x = seg.width; } // Line alignment. /// @todo How to center/right-align text that uses tab stops? if(line.segs.size() == 1 && d->wraps->lineInfo(0).segs[0].tabStop < 0) { if(lineAlign.testFlag(AlignRight)) { linePos.x += int(contentSize.x) - int(size.x); } else if(!lineAlign.testFlag(AlignLeft)) { linePos.x += (int(contentSize.x) - int(size.x)) / 2; } } Rectanglef const uv = d->atlas->imageRectf(seg.id); auto const segRect = Rectanglef::fromSize(linePos + Vector2f(seg.x, 0), size); triStrip.makeQuad(segRect, color, uv); // Keep track of how wide the geometry really is. d->maxGeneratedWidth = de::max(d->maxGeneratedWidth, int(segRect.right() - p.x)); } } p.y += d->font->lineSpacing().value(); } } int GLTextComposer::verticesMaxWidth() const { return d->maxGeneratedWidth; } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/vr/0000775000175000017500000000000012641367670021035 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libappfw/src/vr/vrconfig.cpp0000664000175000017500000001545112641367670023364 0ustar jaakkojaakko/** @file vrconfig.cpp Virtual reality configuration. * * @authors Copyright (c) 2014 Jaakko Keränen * @authors Copyright (c) 2013 Christopher Bruns * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/VRConfig" #include "de/math.h" namespace de { DENG2_PIMPL(VRConfig) { StereoMode mode; de::OculusRift ovr; float screenDistance; float ipd; float eyeHeightInMapUnits; float eyeShift; float playerPhysicalHeight; bool swapEyes; int riftFramebufferSamples; // Multisampling used in unwarped Rift framebuffer /** * Unlike most 3D modes, Oculus Rift typically uses no frustum shift. (or if we did, * it would be different and complicated) */ bool frustumShift; float dominantEye; ///< Kludge for aim-down-weapon-sight modes Instance(Public *i) : Base(i) , mode(Mono) , screenDistance(20.f) , ipd(.064f) // average male IPD , eyeHeightInMapUnits(41) , eyeShift(0) , playerPhysicalHeight(1.75f) , swapEyes(false) , riftFramebufferSamples(1) , frustumShift(true) , dominantEye(0.0f) {} float mapUnitsPerMeter() const { // 0.925 because eyes are not at top of head return eyeHeightInMapUnits / (0.925 * playerPhysicalHeight); } }; VRConfig::VRConfig() : d(new Instance(this)) {} void VRConfig::setMode(StereoMode newMode) { d->mode = newMode; } void VRConfig::setScreenDistance(float distance) { d->screenDistance = distance; } void VRConfig::setEyeHeightInMapUnits(float eyeHeightInMapUnits) { d->eyeHeightInMapUnits = eyeHeightInMapUnits; } void VRConfig::setInterpupillaryDistance(float ipd) { d->ipd = ipd; } void VRConfig::setPhysicalPlayerHeight(float heightInMeters) { d->playerPhysicalHeight = heightInMeters; } void VRConfig::setCurrentEye(Eye eye) { float eyePos = (eye == NeitherEye? 0 : eye == LeftEye? -1 : 1); d->eyeShift = d->mapUnitsPerMeter() * (eyePos - d->dominantEye) * 0.5 * d->ipd; if(d->swapEyes) { d->eyeShift *= -1; } } void VRConfig::enableFrustumShift(bool enable) { d->frustumShift = enable; } void VRConfig::setRiftFramebufferSampleCount(int samples) { d->riftFramebufferSamples = samples; } void VRConfig::setSwapEyes(bool swapped) { d->swapEyes = swapped; } void VRConfig::setDominantEye(float value) { d->dominantEye = value; } VRConfig::StereoMode VRConfig::mode() const { return d->mode; } float VRConfig::screenDistance() const { return d->screenDistance; } bool VRConfig::needsStereoGLFormat() const { return modeNeedsStereoGLFormat(mode()); } bool VRConfig::modeNeedsStereoGLFormat(StereoMode mode) { return mode == QuadBuffered; } float VRConfig::interpupillaryDistance() const { return d->ipd; } float VRConfig::eyeHeightInMapUnits() const { return d->eyeHeightInMapUnits; } float VRConfig::mapUnitsPerMeter() const { return d->mapUnitsPerMeter(); } float VRConfig::physicalPlayerHeight() const { return d->playerPhysicalHeight; } float VRConfig::eyeShift() const { return d->eyeShift; } bool VRConfig::frustumShift() const { return d->frustumShift; } bool VRConfig::swapEyes() const { return d->swapEyes; } float VRConfig::dominantEye() const { return d->dominantEye; } int VRConfig::riftFramebufferSampleCount() const { return d->riftFramebufferSamples; } float VRConfig::viewAspect(Vector2f const &viewPortSize) const { /*if(mode() == OculusRift) { // Override with the Oculus Rift's aspect ratio. return oculusRift().aspect(); }*/ // We're assuming pixels are squares. return viewPortSize.x / viewPortSize.y; } float VRConfig::verticalFieldOfView(float horizFovDegrees, Vector2f const &viewPortSize) const { // We're assuming pixels are squares. float const aspect = viewAspect(viewPortSize); if(mode() == OculusRift) { // A little trigonometry to apply aspect ratio to angles float x = std::tan(.5f * degreeToRadian(horizFovDegrees)); return radianToDegree(2.f * std::atan2(x / aspect, 1.f)); } return horizFovDegrees / aspect; } Matrix4f VRConfig::projectionMatrix(float fovDegrees, Vector2f const &viewPortSize, float nearClip, float farClip) const { if(mode() == OculusRift && oculusRift().isReady()) { // OVR will calculate our projection matrix. float const mapUnits = d->mapUnitsPerMeter(); return oculusRift().projection(nearClip, farClip) * Matrix4f::translate(oculusRift().eyeOffset() * mapUnits); } float const yfov = verticalFieldOfView(fovDegrees, viewPortSize); float const fH = std::tan(.5f * degreeToRadian(yfov)) * nearClip; float const fW = fH * viewAspect(viewPortSize); /* * Asymmetric frustum shift is computed to realign screen-depth items after view point has shifted. * Asymmetric frustum shift method is probably superior to competing toe-in stereo 3D method: * - AFS preserves identical near and far clipping planes in both views * - AFS shows items at/near infinity better * - AFS conforms to what stereo 3D photographers call "ortho stereo" * Asymmetric frustum shift is used for all stereo 3D modes except Oculus Rift mode, which only * applies the viewpoint shift. */ float shift = 0; if(frustumShift()) { shift = eyeShift() * nearClip / screenDistance(); } return Matrix4f::frustum(-fW - shift, fW - shift, -fH, fH, nearClip, farClip) * Matrix4f::translate(Vector3f(-eyeShift(), 0, 0)); } OculusRift &VRConfig::oculusRift() { return d->ovr; } OculusRift const &VRConfig::oculusRift() const { return d->ovr; } bool VRConfig::modeAppliesDisplacement(StereoMode mode) { switch(mode) { case Mono: case GreenMagenta: case RedCyan: case LeftOnly: case RightOnly: case QuadBuffered: return false; default: break; } return true; } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/vr/oculusrift.cpp0000664000175000017500000004732312641367670023751 0ustar jaakkojaakko/** @file oculusrift.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * @authors Copyright (c) 2013 Christopher Bruns * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/OculusRift" #include "de/BaseWindow" #include "de/VRWindowTransform" #include #include #include #include #include #include #ifdef DENG_HAVE_OCULUS_API # include # include using namespace OVR; #endif namespace de { #ifdef DENG_HAVE_OCULUS_API Vector3f quaternionToPRYAngles(Quatf const &q) { Vector3f pry; q.GetEulerAngles(&pry.z, &pry.x, &pry.y); return pry; } #endif DENG2_PIMPL(OculusRift) , DENG2_OBSERVES(Canvas, KeyEvent) #ifdef DENG_HAVE_OCULUS_API , DENG2_OBSERVES(Variable, Change) #endif , public Lockable { #ifdef DENG_HAVE_OCULUS_API ovrHmd hmd; ovrEyeType currentEye; ovrPosef headPose[2]; ovrEyeRenderDesc render[2]; ovrTexture textures[2]; ovrFovPort fov[2]; float fovXDegrees; ovrFrameTiming timing; #endif Matrix4f eyeMatrix[2]; Vector3f pitchRollYaw; Vector3f headPosition; Vector3f eyeOffset[2]; float aspect = 1.f; BaseWindow *window = nullptr; QRect oldGeometry; bool inited = false; bool frameOngoing = false; bool needPoseUpdate = false; bool densityChanged = false; //Vector2f screenSize; //float lensSeparationDistance; //Vector4f hmdWarpParam; //Vector4f chromAbParam; float eyeToScreenDistance; //float latency; //float ipd; float yawOffset; Instance(Public *i) : Base(i) //, screenSize(0.14976f, 0.09360f) //, lensSeparationDistance(0.0635f) //, hmdWarpParam(1.0f, 0.220f, 0.240f, 0.000f) //, chromAbParam(0.996f, -0.004f, 1.014f, 0.0f) , eyeToScreenDistance(0.041f) //, latency(.030f) //,ipd(.064f) , yawOffset(0) { #ifdef DENG_HAVE_OCULUS_API hmd = 0; /* ovr_Initialize(); hmd = ovrHmd_Create(0); if(!hmd && App::commandLine().has("-ovrdebug")) { hmd = ovrHmd_CreateDebug(ovrHmd_DK2); } if(hmd) { LOG_INPUT_NOTE("HMD: %s (%s) %i.%i %ix%i pixels") << String(hmd->ProductName) << String(hmd->Manufacturer) << hmd->FirmwareMajor << hmd->FirmwareMinor << hmd->Resolution.w << hmd->Resolution.h; } */ #endif } ~Instance() { DENG2_GUARD(this); deinit(); #ifdef DENG_HAVE_OCULUS_API /* if(hmd) { ovrHmd_Destroy(hmd); }*/ ovr_Shutdown(); #endif } #ifdef DENG_HAVE_OCULUS_API /// Returns the offscreen framebuffer where the Oculus Rift raw frame is drawn. /// This is passed to LibOVR as a texture. GLFramebuffer &framebuffer() { DENG2_ASSERT(window); return window->transform().as().unwarpedFramebuffer(); } void variableValueChanged(Variable &, Value const &) { densityChanged = true; } void resizeFramebuffer() { Sizei size[2]; ovrFovPort fovMax; float density = App::config().getf("vr.oculusRift.pixelDensity", 1.f); for(int eye = 0; eye < 2; ++eye) { // Use the default FOV. fov[eye] = hmd->DefaultEyeFov[eye]; size[eye] = ovrHmd_GetFovTextureSize(hmd, ovrEyeType(eye), fov[eye], density); } fovMax.LeftTan = max(fov[0].LeftTan, fov[1].LeftTan); fovMax.RightTan = max(fov[0].RightTan, fov[1].RightTan); fovMax.UpTan = max(fov[0].UpTan, fov[1].UpTan); fovMax.DownTan = max(fov[0].DownTan, fov[1].DownTan); float comboXTan = max(fovMax.LeftTan, fovMax.RightTan); float comboYTan = max(fovMax.UpTan, fovMax.DownTan); aspect = comboXTan / comboYTan; LOGDEV_GL_MSG("Aspect ratio: %f") << aspect; // Calculate the horizontal total FOV in degrees that the renderer will use // for clipping. fovXDegrees = radianToDegree(2 * atanf(comboXTan)); LOGDEV_GL_MSG("Clip FOV: %.2f degrees") << fovXDegrees; framebuffer().resize(GLFramebuffer::Size(size[0].w + size[1].w, max(size[0].h, size[1].h))); uint const w = framebuffer().size().x; uint const h = framebuffer().size().y; framebuffer().colorTexture().setFilter(gl::Linear, gl::Linear, gl::MipNone); framebuffer().colorTexture().glApplyParameters(); LOG_GL_VERBOSE("Framebuffer size: ") << framebuffer().size().asText(); for(int eye = 0; eye < 2; ++eye) { ovrGLTexture tex; zap(tex); tex.OGL.Header.API = ovrRenderAPI_OpenGL; tex.OGL.Header.TextureSize = Sizei(w, h); tex.OGL.Header.RenderViewport = Recti(eye == 0? 0 : ((w + 1) / 2), 0, w/2, h); tex.OGL.TexId = framebuffer().colorTexture().glName(); textures[eye] = tex.Texture; } } #endif void init() { if(inited) return; inited = true; #ifdef DENG_HAVE_OCULUS_API //ovr_Initialize(); hmd = ovrHmd_Create(0); if(!hmd && App::commandLine().has("-ovrdebug")) { hmd = ovrHmd_CreateDebug(ovrHmd_DK2); } if(hmd) { LOG_INPUT_NOTE("HMD: %s (%s) %i.%i %ix%i pixels") << String(hmd->ProductName) << String(hmd->Manufacturer) << hmd->FirmwareMajor << hmd->FirmwareMinor << hmd->Resolution.w << hmd->Resolution.h; } // If there is no Oculus Rift connected, do nothing. if(!hmd) return; App::config("vr.oculusRift.pixelDensity").audienceForChange() += this; DENG2_GUARD(this); // Configure for orientation and position tracking. ovrHmd_ConfigureTracking(hmd, ovrTrackingCap_Orientation | ovrTrackingCap_MagYawCorrection | ovrTrackingCap_Position, 0); LOG_GL_MSG("Initializing Oculus Rift for rendering"); // We will be rendering into the main window. window = &CanvasWindow::main().as(); DENG2_ASSERT(window->isVisible()); DENG2_ASSERT(QGLContext::currentContext() != 0); // Observe key events for dismissing the Health and Safety warning. window->canvas().audienceForKeyEvent() += this; // Set up the rendering target according to the OVR parameters. auto &buf = framebuffer(); buf.glInit(); // Set up the framebuffer and eye viewports. resizeFramebuffer(); uint distortionCaps = hmd->DistortionCaps & ( ovrDistortionCap_Chromatic | ovrDistortionCap_TimeWarp | ovrDistortionCap_Vignette | ovrDistortionCap_Overdrive ); // Configure OpenGL. ovrGLConfig cfg; cfg.OGL.Header.API = ovrRenderAPI_OpenGL; cfg.OGL.Header.RTSize = hmd->Resolution; cfg.OGL.Header.Multisample = buf.sampleCount(); #ifdef WIN32 cfg.OGL.Window = (HWND) window->nativeHandle(); cfg.OGL.DC = wglGetCurrentDC(); #endif if(!ovrHmd_ConfigureRendering(hmd, &cfg.Config, distortionCaps, fov, render)) { LOG_GL_ERROR("Failed to configure Oculus Rift for rendering"); return; } for(int i = 0; i < 2; ++i) { eyeOffset[i] = Vector3f(render[i].HmdToEyeViewOffset.x, render[i].HmdToEyeViewOffset.y, render[i].HmdToEyeViewOffset.z); } /* for(int i = 0; i < 2; ++i) { qDebug() << "Eye:" << render[i].Eye << "Fov:" << render[i].Fov.LeftTan << render[i].Fov.RightTan << render[i].Fov.UpTan << render[i].Fov.DownTan << "DistortedViewport:" << render[i].DistortedViewport.Pos.x << render[i].DistortedViewport.Pos.y << render[i].DistortedViewport.Size.w << render[i].DistortedViewport.Size.h << "PixelsPerTanAngleAtCenter:" << render[i].PixelsPerTanAngleAtCenter.x << render[i].PixelsPerTanAngleAtCenter.y << "ViewAdjust:" << render[i].ViewAdjust.x << render[i].ViewAdjust.y << render[i].ViewAdjust.z; }*/ /* float clearColor[4] = { 0.0f, 0.5f, 1.0f, 0.0f }; ovrHmd_SetFloatArray(hmd, "DistortionClearColor", clearColor, 4); */ // Move the window to the correct display. //if(ovrHmd_GetEnabledCaps(hmd) & ovrHmdCap_ExtendDesktop) { //LOG_GL_MSG("Using extended desktop mode"); } /* else { LOG_GL_MSG("Using direct-to-HMD rendering mode"); }*/ ovrHmd_AttachToWindow(hmd, window->nativeHandle(), NULL, NULL); moveWindow(HMDScreen); #endif } #ifdef DENG_HAVE_OCULUS_API QRect screenGeometry(Screen which) const { foreach(QScreen *scr, qApp->screens()) { #ifdef WIN32 bool isRift = String(hmd->DisplayDeviceName).startsWith(scr->name()); if((which == HMDScreen && isRift) || (which == DefaultScreen && !isRift)) { LOG_GL_MSG("HMD display: \"%s\" Screen: \"%s\" Geometry: %i,%i %ix%i") << String(hmd->DisplayDeviceName) << scr->name() << scr->geometry().left() << scr->geometry().top() << scr->geometry().width() << scr->geometry().height(); return scr->geometry(); } #else DENG2_UNUSED(scr); DENG2_UNUSED(which); #endif } // Fall back the first screen. return qApp->screens().at(0)->geometry(); } #endif void deinit() { if(!inited) return; inited = false; frameOngoing = false; #ifdef DENG_HAVE_OCULUS_API DENG2_GUARD(this); LOG_GL_MSG("Stopping Oculus Rift rendering"); App::config("vr.oculusRift.pixelDensity").audienceForChange() -= this; if(hmd) { ovrHmd_ConfigureRendering(hmd, NULL, 0, NULL, NULL); framebuffer().glDeinit(); moveWindow(PreviousScreen); window->canvas().audienceForKeyEvent() -= this; window = 0; ovrHmd_Destroy(hmd); } #endif } #ifdef DENG_HAVE_OCULUS_API bool isWindowOnHMD() const { if(!window) return false; QRect const hmdRect = screenGeometry(HMDScreen); return hmdRect.contains(window->geometry()); } void moveWindow(Screen screen) { if(!window) return; if(screen == HMDScreen) { if(isWindowOnHMD()) return; // Nothing further to do. #ifdef WIN32 oldGeometry = window->geometry(); window->setGeometry(screenGeometry(HMDScreen)); window->showFullScreen(); #endif } else if(screen == PreviousScreen) { if(!isWindowOnHMD()) return; #ifdef WIN32 window->showMaximized(); window->setGeometry(oldGeometry); #endif } else { if(!isWindowOnHMD()) return; #ifdef WIN32 window->showMaximized(); window->setGeometry(screenGeometry(DefaultScreen)); #endif } } #endif /** * Observe key events (any key) to dismiss the OVR Health and Safety warning. */ void keyEvent(KeyEvent const &ev) { if(!window || ev.type() == Event::KeyRelease) return; #ifdef DENG_HAVE_OCULUS_API if(isHealthAndSafetyWarningDisplayed()) { if(dismissHealthAndSafetyWarning()) { window->canvas().audienceForKeyEvent() -= this; } } #endif } #ifdef DENG_HAVE_OCULUS_API bool isReady() const { return hmd != 0; } bool isHealthAndSafetyWarningDisplayed() const { if(!isReady()) return false; ovrHSWDisplayState hswDisplayState; ovrHmd_GetHSWDisplayState(hmd, &hswDisplayState); return hswDisplayState.Displayed; } bool dismissHealthAndSafetyWarning() { if(isHealthAndSafetyWarningDisplayed()) { return ovrHmd_DismissHSWDisplay(hmd); } return true; } void dismissHealthAndSafetyWarningOnTap() { if(!isHealthAndSafetyWarningDisplayed()) return; ovrTrackingState ts = ovrHmd_GetTrackingState(hmd, ovr_GetTimeInSeconds()); if(ts.StatusFlags & ovrStatus_OrientationTracked) { OVR::Vector3f rawAccel(ts.RawSensorData.Accelerometer.x, ts.RawSensorData.Accelerometer.y, ts.RawSensorData.Accelerometer.z); // Arbitrary value and representing moderate tap on the side of the DK2 Rift. if(rawAccel.LengthSq() > 250.f) { ovrHmd_DismissHSWDisplay(hmd); } } } void updateEyePoses() { if(!frameOngoing) return; needPoseUpdate = false; ovrVector3f hmdEyeOffsets[2] { render[0].HmdToEyeViewOffset, render[1].HmdToEyeViewOffset }; // Pose for the current eye. ovrHmd_GetEyePoses(hmd, 0, hmdEyeOffsets, headPose, nullptr); pitchRollYaw = quaternionToPRYAngles(headPose[0].Orientation); headPosition = Vector3f((headPose[0].Position.x + headPose[1].Position.x)/2, (headPose[0].Position.y + headPose[1].Position.y)/2, (headPose[0].Position.z + headPose[1].Position.z)/2); for(int i = 0; i < 2; ++i) { // TODO: Rotate using provided quaternion. // Note that Doomsday doesn't currently use this matrix! eyeMatrix[i] = Matrix4f::translate(Vector3f(headPose[i].Position.x, headPose[i].Position.y, headPose[i].Position.z)) * Matrix4f::rotate(-radianToDegree(pitchRollYaw[1]), Vector3f(0, 0, 1)) * Matrix4f::rotate(-radianToDegree(pitchRollYaw[0]), Vector3f(1, 0, 0)) * Matrix4f::rotate(-radianToDegree(pitchRollYaw[2]), Vector3f(0, 1, 0)); //* //Matrix4f::translate(-eyeOffset[i]); } } void beginFrame() { DENG2_ASSERT(isReady()); DENG2_ASSERT(!frameOngoing); if(densityChanged) { densityChanged = false; resizeFramebuffer(); } frameOngoing = true; needPoseUpdate = true; timing = ovrHmd_BeginFrame(hmd, 0); } void endFrame() { DENG2_ASSERT(frameOngoing); ovrHmd_EndFrame(hmd, headPose, textures); dismissHealthAndSafetyWarningOnTap(); frameOngoing = false; } #endif }; OculusRift::OculusRift() : d(new Instance(this)) {} bool OculusRift::isEnabled() const { #ifdef DENG_HAVE_OCULUS_API return true; #else return false; #endif } void OculusRift::glPreInit() { #ifdef DENG_HAVE_OCULUS_API LOG_AS("OculusRift"); LOG_VERBOSE("Initializing LibOVR"); ovr_Initialize(); #endif } bool OculusRift::isHMDConnected() const { #ifdef DENG_HAVE_OCULUS_API if(d->isReady()) return true; return ovrHmd_Detect() > 0; #endif return false; } void OculusRift::init() { LOG_AS("OculusRift"); d->init(); } void OculusRift::deinit() { LOG_AS("OculusRift"); d->deinit(); } void OculusRift::beginFrame() { #ifdef DENG_HAVE_OCULUS_API if(!isReady() || !d->inited || d->frameOngoing) return; // Begin the frame and acquire timing information. d->beginFrame(); #endif } void OculusRift::endFrame() { #ifdef DENG_HAVE_OCULUS_API if(!isReady() || !d->frameOngoing) return; // End the frame and let the Oculus SDK handle displaying it with the // appropriate transformation. d->endFrame(); #endif } void OculusRift::setCurrentEye(int index) { #ifdef DENG_HAVE_OCULUS_API if(d->hmd) { d->currentEye = d->hmd->EyeRenderOrder[index]; } #else DENG2_UNUSED(index); #endif } OculusRift::Eye OculusRift::currentEye() const { #ifdef DENG_HAVE_OCULUS_API return (d->currentEye == ovrEye_Left? LeftEye : RightEye); #else return LeftEye; #endif } Vector2ui OculusRift::resolution() const { #ifdef DENG_HAVE_OCULUS_API if(!d->hmd) return Vector2ui(); return Vector2ui(d->hmd->Resolution.w, d->hmd->Resolution.h); #else return Vector2ui(); #endif } void OculusRift::setYawOffset(float yawRadians) { d->yawOffset = yawRadians; } void OculusRift::resetTracking() { #ifdef DENG_HAVE_OCULUS_API if(d->isReady()) { ovrHmd_RecenterPose(d->hmd); } #endif } void OculusRift::resetYaw() { d->yawOffset = -d->pitchRollYaw.z; /* #ifdef DENG_HAVE_OCULUS_API DENG2_GUARD(d); if(isReady()) { d->yawOffset = -d->oculusDevice->yaw; } #endif */ } // True if Oculus Rift is enabled and can report head orientation. bool OculusRift::isReady() const { #ifdef DENG_HAVE_OCULUS_API return d->isReady(); #else return false; #endif } Vector3f OculusRift::headOrientation() const { #ifdef DENG_HAVE_OCULUS_API if(d->needPoseUpdate) d->updateEyePoses(); #endif Vector3f pry = d->pitchRollYaw; pry.z = wrap(pry.z + d->yawOffset, -PIf, PIf); return pry; } Matrix4f OculusRift::eyePose() const { #ifdef DENG_HAVE_OCULUS_API if(!isReady()) return Matrix4f(); if(d->needPoseUpdate) d->updateEyePoses(); return d->eyeMatrix[d->currentEye]; #else return Matrix4f(); #endif } Vector3f OculusRift::headPosition() const { #ifdef DENG_HAVE_OCULUS_API if(d->needPoseUpdate) d->updateEyePoses(); #endif return d->headPosition; } Vector3f OculusRift::eyeOffset() const { #ifdef DENG_HAVE_OCULUS_API return d->eyeOffset[d->currentEye]; #else return Vector3f(); #endif } Matrix4f OculusRift::projection(float nearDist, float farDist) const { #ifdef DENG_HAVE_OCULUS_API DENG2_ASSERT(isReady()); return Matrix4f(ovrMatrix4f_Projection(d->fov[d->currentEye], nearDist, farDist, true /* right-handed */).M[0]).transpose(); #else DENG2_UNUSED2(nearDist, farDist); return Matrix4f(); #endif } float OculusRift::yawOffset() const { return d->yawOffset; } float OculusRift::aspect() const { return d->aspect; } float OculusRift::fovX() const { #ifdef DENG_HAVE_OCULUS_API return d->fovXDegrees; #endif return 0; } void OculusRift::moveWindowToScreen(Screen screen) { #ifdef DENG_HAVE_OCULUS_API d->moveWindow(screen); #else DENG2_UNUSED(screen); #endif } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/signalaction.cpp0000664000175000017500000000217712641367670023574 0ustar jaakkojaakko/** @file signalaction.cpp * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/SignalAction" namespace de { SignalAction::SignalAction(QObject *target, char const *slot) : Action(), _target(target), _slot(slot) { connect(this, SIGNAL(triggered()), target, slot); } void SignalAction::trigger() { Action::trigger(); emit triggered(); } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/vrwindowtransform.cpp0000664000175000017500000003026412641367670024732 0ustar jaakkojaakko/** @file vrwindowtransform.cpp Window content transformation for virtual reality. * * @authors Copyright (c) 2013 Christopher Bruns * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/VRWindowTransform" #include "de/VRConfig" #include "de/BaseGuiApp" #include "de/BaseWindow" #include "de/GuiWidget" #include #include namespace de { DENG2_PIMPL(VRWindowTransform) { VRConfig &vrCfg; GLFramebuffer unwarpedFB; Instance(Public *i) : Base(i) , vrCfg(DENG2_BASE_GUI_APP->vr()) {} ~Instance() { vrCfg.oculusRift().deinit(); } Canvas &canvas() const { return self.window().canvas(); } GLTarget &target() const { return canvas().renderTarget(); } int width() const { return canvas().width(); } int height() const { return canvas().height(); } float displayModeDependentUIScalingFactor() const { if(GuiWidget::toDevicePixels(1) == 1) return 1.0f; // Not enough pixels for good-quality scaling. // Since the UI style doesn't yet support scaling at runtime based on // display resolution (or any other factor). return 1.f / Rangef(.5f, 1.0f).clamp((self.window().width() - GuiWidget::toDevicePixels(256.f)) / GuiWidget::toDevicePixels(768.f)); } void drawContent() const { LIBGUI_ASSERT_GL_OK(); self.window().drawWindowContent(); LIBGUI_ASSERT_GL_OK(); } /** * Draws the entire UI in two halves, one for the left eye and one for the right. The * Oculus Rift optical distortion effect is applied using a shader. * * @todo unwarpedTarget and unwarpedTexture should be cleared/deleted when Oculus * Rift mode is disabled (or whenever they are not needed). */ void vrDrawOculusRift() { OculusRift &ovr = vrCfg.oculusRift(); vrCfg.enableFrustumShift(false); // Use a little bit of multisampling to smooth out the magnified jagged edges. // Note: Independent of the window FSAA setting because this is beneficial even // when FSAA is disabled. unwarpedFB.setSampleCount(1); //vrCfg.riftFramebufferSampleCount()); // Set render target to offscreen temporarily. GLState::push() .setTarget(unwarpedFB.target()) .setViewport(Rectangleui::fromSize(unwarpedFB.size())) .apply(); unwarpedFB.target().unsetActiveRect(true); GLFramebuffer::Size const fbSize = unwarpedFB.size(); // Left eye view on left side of screen. for(int eyeIdx = 0; eyeIdx < 2; ++eyeIdx) { ovr.setCurrentEye(eyeIdx); if(ovr.currentEye() == OculusRift::LeftEye) { // Left eye on the left side of the screen. unwarpedFB.target().setActiveRect(Rectangleui(0, 0, fbSize.x/2, fbSize.y), true); } else { // Right eye on the right side of screen. unwarpedFB.target().setActiveRect(Rectangleui(fbSize.x/2, 0, fbSize.x/2, fbSize.y), true); } drawContent(); } unwarpedFB.target().unsetActiveRect(true); GLState::pop().apply(); vrCfg.enableFrustumShift(); // restore default } void draw() { switch(vrCfg.mode()) { // A) Single view type stereo 3D modes here: case VRConfig::Mono: // Non-stereoscopic frame. drawContent(); break; case VRConfig::LeftOnly: // Left eye view vrCfg.setCurrentEye(VRConfig::LeftEye); drawContent(); break; case VRConfig::RightOnly: // Right eye view vrCfg.setCurrentEye(VRConfig::RightEye); drawContent(); break; // B) Split-screen type stereo 3D modes here: case VRConfig::TopBottom: // Left goes on top // Left eye view on top of screen. vrCfg.setCurrentEye(VRConfig::LeftEye); target().setActiveRect(Rectangleui(0, 0, width(), height()/2), true); drawContent(); // Right eye view on bottom of screen. vrCfg.setCurrentEye(VRConfig::RightEye); target().setActiveRect(Rectangleui(0, height()/2, width(), height()/2), true); drawContent(); break; case VRConfig::SideBySide: // Squished aspect // Left eye view on left side of screen. vrCfg.setCurrentEye(VRConfig::LeftEye); target().setActiveRect(Rectangleui(0, 0, width()/2, height()), true); drawContent(); // Right eye view on right side of screen. vrCfg.setCurrentEye(VRConfig::RightEye); target().setActiveRect(Rectangleui(width()/2, 0, width()/2, height()), true); drawContent(); break; case VRConfig::Parallel: // Normal aspect // Left eye view on left side of screen. vrCfg.setCurrentEye(VRConfig::LeftEye); target().setActiveRect(Rectangleui(0, 0, width()/2, height()), true); drawContent(); // Right eye view on right side of screen. vrCfg.setCurrentEye(VRConfig::RightEye); target().setActiveRect(Rectangleui(width()/2, 0, width()/2, height()), true); drawContent(); break; case VRConfig::CrossEye: // Normal aspect // Right eye view on left side of screen. vrCfg.setCurrentEye(VRConfig::RightEye); target().setActiveRect(Rectangleui(0, 0, width()/2, height()), true); drawContent(); // Left eye view on right side of screen. vrCfg.setCurrentEye(VRConfig::LeftEye); target().setActiveRect(Rectangleui(width()/2, 0, width()/2, height()), true); drawContent(); break; case VRConfig::OculusRift: vrDrawOculusRift(); break; // Overlaid type stereo 3D modes below: case VRConfig::GreenMagenta: // Left eye view vrCfg.setCurrentEye(VRConfig::LeftEye); GLState::push().setColorMask(gl::WriteGreen | gl::WriteAlpha).apply(); // Left eye view green drawContent(); // Right eye view vrCfg.setCurrentEye(VRConfig::RightEye); GLState::current().setColorMask(gl::WriteRed | gl::WriteBlue | gl::WriteAlpha).apply(); // Right eye view magenta drawContent(); GLState::pop().apply(); break; case VRConfig::RedCyan: // Left eye view vrCfg.setCurrentEye(VRConfig::LeftEye); GLState::push().setColorMask(gl::WriteRed | gl::WriteAlpha).apply(); // Left eye view red drawContent(); // Right eye view vrCfg.setCurrentEye(VRConfig::RightEye); GLState::current().setColorMask(gl::WriteGreen | gl::WriteBlue | gl::WriteAlpha).apply(); // Right eye view cyan drawContent(); GLState::pop().apply(); break; case VRConfig::QuadBuffered: if(canvas().format().stereo()) { // Left eye view vrCfg.setCurrentEye(VRConfig::LeftEye); drawContent(); canvas().framebuffer().swapBuffers(canvas(), gl::SwapStereoLeftBuffer); // Right eye view vrCfg.setCurrentEye(VRConfig::RightEye); drawContent(); canvas().framebuffer().swapBuffers(canvas(), gl::SwapStereoRightBuffer); } else { // Normal non-stereoscopic frame. drawContent(); } break; case VRConfig::RowInterleaved: { // Use absolute screen position of window to determine whether the // first scan line is odd or even. QPoint ulCorner(0, 0); ulCorner = canvas().mapToGlobal(ulCorner); // widget to screen coordinates bool rowParityIsEven = ((ulCorner.x() % 2) == 0); DENG2_UNUSED(rowParityIsEven); /// @todo - use row parity in shader or stencil, to actually interleave rows. // Left eye view vrCfg.setCurrentEye(VRConfig::LeftEye); drawContent(); // Right eye view vrCfg.setCurrentEye(VRConfig::RightEye); drawContent(); break; } case VRConfig::ColumnInterleaved: /// @todo implement column interleaved stereo 3D after row intleaved is working correctly... case VRConfig::Checkerboard: /// @todo implement checker stereo 3D after row intleaved is working correctly ... default: // Non-stereoscopic frame. drawContent(); break; } // Restore default VR dynamic parameters target().unsetActiveRect(true); vrCfg.setCurrentEye(VRConfig::NeitherEye); LIBGUI_ASSERT_GL_OK(); } }; VRWindowTransform::VRWindowTransform(BaseWindow &window) : WindowTransform(window), d(new Instance(this)) {} void VRWindowTransform::glInit() { //d->init(); } void VRWindowTransform::glDeinit() { //d->deinit(); } Vector2ui VRWindowTransform::logicalRootSize(Vector2ui const &physicalCanvasSize) const { Canvas::Size size = physicalCanvasSize; switch(d->vrCfg.mode()) { // Left-right screen split modes case VRConfig::CrossEye: case VRConfig::Parallel: // Adjust effective UI size for stereoscopic rendering. size.y *= 2; size *= .75f; // Make it a bit bigger. break; case VRConfig::OculusRift: // Adjust effective UI size for stereoscopic rendering. size.x = size.y * d->vrCfg.oculusRift().aspect(); //size.y *= d->vrCfg.oculusRift().aspect(); size *= GuiWidget::toDevicePixels(1) * .75f; break; // Allow UI to squish in top/bottom and SBS mode: 3D hardware will unsquish them case VRConfig::TopBottom: case VRConfig::SideBySide: default: break; } size *= d->displayModeDependentUIScalingFactor(); return size; } Vector2f VRWindowTransform::windowToLogicalCoords(Vector2i const &winPos) const { // We need to map the real window coordinates to logical root view // coordinates according to the used transformation. Vector2f pos = winPos; Vector2f const size = window().canvas().size(); Vector2f viewSize = window().windowContentSize(); switch(d->vrCfg.mode()) { // Left-right screen split modes case VRConfig::SideBySide: case VRConfig::CrossEye: case VRConfig::Parallel: case VRConfig::OculusRift: // Make it possible to access both frames. if(pos.x >= size.x/2) { pos.x -= size.x/2; } pos.x *= 2; break; // Top-bottom screen split modes case VRConfig::TopBottom: // Make it possible to access both frames. if(pos.y >= size.y/2) { pos.y -= size.y/2; } pos.y *= 2; break; default: // Not transformed. break; } // Scale to logical size. pos = pos / size * viewSize; return pos; } void VRWindowTransform::drawTransformed() { d->draw(); } GLFramebuffer &VRWindowTransform::unwarpedFramebuffer() { return d->unwarpedFB; } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/windowtransform.cpp0000664000175000017500000000320312641367670024353 0ustar jaakkojaakko/** @file windowtransform.cpp Base class for window content transformation. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/WindowTransform" #include "de/BaseWindow" namespace de { DENG2_PIMPL_NOREF(WindowTransform) { BaseWindow *win; }; WindowTransform::WindowTransform(BaseWindow &window) : d(new Instance) { d->win = &window; } BaseWindow &WindowTransform::window() const { DENG2_ASSERT(d->win != 0); return *d->win; } void WindowTransform::glInit() { // nothing to do } void WindowTransform::glDeinit() { // nothing to do } Vector2ui WindowTransform::logicalRootSize(Vector2ui const &physicalCanvasSize) const { return physicalCanvasSize; } Vector2f WindowTransform::windowToLogicalCoords(Vector2i const &pos) const { return pos; } void WindowTransform::drawTransformed() { return d->win->drawWindowContent(); } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/basewindow.cpp0000664000175000017500000001160212641367670023254 0ustar jaakkojaakko/** @file basewindow.cpp Abstract base class for application windows. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/BaseWindow" #include "de/WindowTransform" #include "de/WindowSystem" #include "de/BaseGuiApp" #include "de/VRConfig" #include namespace de { DENG2_PIMPL(BaseWindow) , DENG2_OBSERVES(KeyEventSource, KeyEvent) , DENG2_OBSERVES(MouseEventSource, MouseEvent) { WindowTransform defaultXf; ///< Used by default (doesn't apply any transformation). WindowTransform *xf; Instance(Public *i) : Base(i) , defaultXf(self) , xf(&defaultXf) { // Listen to input. self.canvas().audienceForKeyEvent() += this; self.canvas().audienceForMouseEvent() += this; } ~Instance() { self.canvas().audienceForKeyEvent() -= this; self.canvas().audienceForMouseEvent() -= this; } void keyEvent(KeyEvent const &ev) { /// @todo Input drivers should observe the notification instead, input /// subsystem passes it to window system. -jk // Pass the event onto the window system. if(!WindowSystem::get().processEvent(ev)) { // Maybe the fallback handler has use for this. self.handleFallbackEvent(ev); } } void mouseEvent(MouseEvent const &event) { MouseEvent ev = event; // Translate mouse coordinates for direct interaction. if(ev.type() == Event::MousePosition || ev.type() == Event::MouseButton || ev.type() == Event::MouseWheel) { ev.setPos(xf->windowToLogicalCoords(event.pos()).toVector2i()); } if(!WindowSystem::get().processEvent(ev)) { // Maybe the fallback handler has use for this. self.handleFallbackEvent(ev); } } }; BaseWindow::BaseWindow(String const &id) : PersistentCanvasWindow(id) , d(new Instance(this)) {} void BaseWindow::setTransform(WindowTransform &xf) { d->xf = &xf; } void BaseWindow::useDefaultTransform() { d->xf = &d->defaultXf; } WindowTransform &BaseWindow::transform() { DENG2_ASSERT(d->xf != 0); return *d->xf; } bool BaseWindow::shouldRepaintManually() const { // By default always prefer updates that are "nice" to the rest of the system. return false; } bool BaseWindow::prepareForDraw() { // Don't run the main loop until after the paint event has been dealt with. DENG2_GUI_APP->loop().pause(); return true; // Go ahead. } void BaseWindow::draw() { if(!prepareForDraw()) { // Not right now, please. return; } // Initialize Oculus Rift if needed. auto &vr = DENG2_BASE_GUI_APP->vr(); if(vr.mode() == VRConfig::OculusRift) { if(canvas().isGLReady()) { canvas().makeCurrent(); vr.oculusRift().init(); } } else { canvas().makeCurrent(); vr.oculusRift().deinit(); } if(shouldRepaintManually()) { DENG2_ASSERT_IN_MAIN_THREAD(); // Perform the drawing manually right away. canvas().makeCurrent(); canvas().updateGL(); } else { // Request update at the earliest convenience. canvas().update(); } } void BaseWindow::canvasGLDraw(Canvas &cv) { preDraw(); d->xf->drawTransformed(); postDraw(); PersistentCanvasWindow::canvasGLDraw(cv); } void BaseWindow::swapBuffers() { DENG2_ASSERT(DENG2_BASE_GUI_APP->vr().mode() != VRConfig::OculusRift); PersistentCanvasWindow::swapBuffers(DENG2_BASE_GUI_APP->vr().needsStereoGLFormat()? gl::SwapStereoBuffers : gl::SwapMonoBuffer); } void BaseWindow::preDraw() { auto &vr = DENG2_BASE_GUI_APP->vr(); if(vr.mode() == VRConfig::OculusRift) { vr.oculusRift().beginFrame(); } } void BaseWindow::postDraw() { auto &vr = DENG2_BASE_GUI_APP->vr(); if(vr.mode() == VRConfig::OculusRift) { vr.oculusRift().endFrame(); } // The timer loop was paused when the frame was requested to be drawn. DENG2_GUI_APP->loop().resume(); } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/baseguiapp.cpp0000664000175000017500000001012712641367670023233 0ustar jaakkojaakko/** @file baseguiapp.cpp Base class for GUI applications. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/BaseGuiApp" #include "de/VRConfig" #include #include #include #include #include namespace de { static Value *Function_App_LoadFont(Context &, Function::ArgumentValues const &args) { try { // Try to load the specific font. Block data(App::rootFolder().locate(args.at(0)->asText())); int id; id = QFontDatabase::addApplicationFontFromData(data); if(id < 0) { LOG_RES_WARNING("Failed to load font:"); } else { LOG_RES_VERBOSE("Loaded font: %s") << args.at(0)->asText(); //qDebug() << args.at(0)->asText(); //qDebug() << "Families:" << QFontDatabase::applicationFontFamilies(id); } } catch(Error const &er) { LOG_RES_WARNING("Failed to load font:\n") << er.asText(); } return 0; } static Value *Function_App_AddFontMapping(Context &, Function::ArgumentValues const &args) { // arg 0: family name // arg 1: dictionary with [Text style, Number weight] => Text fontname // styles: regular, italic // weight: 0-99 (25=light, 50=normal, 75=bold) NativeFont::StyleMapping mapping; DictionaryValue const &dict = args.at(1)->as(); DENG2_FOR_EACH_CONST(DictionaryValue::Elements, i, dict.elements()) { NativeFont::Spec spec; ArrayValue const &key = i->first.value->as(); if(key.at(0).asText() == "italic") { spec.style = NativeFont::Italic; } spec.weight = roundi(key.at(1).asNumber()); mapping.insert(spec, i->second->asText()); } NativeFont::defineMapping(args.at(0)->asText(), mapping); return 0; } DENG2_PIMPL_NOREF(BaseGuiApp) { Binder binder; QScopedPointer uiState; GLShaderBank shaders; WaveformBank waveforms; VRConfig vr; }; BaseGuiApp::BaseGuiApp(int &argc, char **argv) : GuiApp(argc, argv), d(new Instance) { // Override the system locale (affects number/time formatting). QLocale::setDefault(QLocale("en_US.UTF-8")); d->binder.init(scriptSystem().nativeModule("App")) << DENG2_FUNC (App_AddFontMapping, "addFontMapping", "family" << "mappings") << DENG2_FUNC (App_LoadFont, "loadFont", "fileName"); } void BaseGuiApp::initSubsystems(SubsystemInitFlags flags) { GuiApp::initSubsystems(flags); double dpiFactor = 1.0; #ifdef DENG2_QT_5_0_OR_NEWER dpiFactor = devicePixelRatio(); #endif // The "-dpi" option overrides the detected DPI factor. if(auto dpi = commandLine().check("-dpi", 1)) { dpiFactor = dpi.params.at(0).toDouble(); } scriptSystem().nativeModule("DisplayMode").set("DPI_FACTOR", dpiFactor); d->uiState.reset(new PersistentState("UIState")); } BaseGuiApp &BaseGuiApp::app() { return static_cast(App::app()); } PersistentState &BaseGuiApp::persistentUIState() { return *app().d->uiState; } GLShaderBank &BaseGuiApp::shaders() { return app().d->shaders; } WaveformBank &BaseGuiApp::waveforms() { return app().d->waveforms; } VRConfig &BaseGuiApp::vr() { return app().d->vr; } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/guirootwidget.cpp0000664000175000017500000002375112641367670024016 0ustar jaakkojaakko/** @file guirootwidget.cpp Graphical root widget. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GuiRootWidget" #include "de/GuiWidget" #include "de/BaseGuiApp" #include "de/Style" #include "de/BaseWindow" #include #include #include #include #include #include #include namespace de { // Identifiers for images generated by GuiRootWidget. static DotPath const ID_SOLID_WHITE = "GuiRootWidget.solid.white"; static DotPath const ID_THIN_ROUND_CORNERS = "GuiRootWidget.frame.thin"; static DotPath const ID_BOLD_ROUND_CORNERS = "GuiRootWidget.frame.bold"; static DotPath const ID_DOT = "GuiRootWidget.dot"; #ifdef DENG2_QT_5_0_OR_NEWER # define DPI_SCALED(x) ((x) * qApp->devicePixelRatio()) #else # define DPI_SCALED(x) (x) #endif DENG2_PIMPL(GuiRootWidget) , DENG2_OBSERVES(Widget, ChildAddition) { /* * Built-in runtime-generated images: */ struct SolidWhiteImage : public TextureBank::ImageSource { Image load() const { return Image::solidColor(Image::Color(255, 255, 255, 255), Image::Size(1, 1)); } }; struct ThinCornersImage : public TextureBank::ImageSource { Image load() const { QImage img(QSize(DPI_SCALED(15), DPI_SCALED(15)), QImage::Format_ARGB32); img.fill(QColor(255, 255, 255, 0).rgba()); QPainter painter(&img); painter.setRenderHint(QPainter::Antialiasing, true); painter.setBrush(Qt::NoBrush); painter.setPen(QPen(Qt::white, DPI_SCALED(1))); painter.drawEllipse(DPI_SCALED(QPointF(8, 8)), DPI_SCALED(6), DPI_SCALED(6)); return img; } }; struct BoldCornersImage : public TextureBank::ImageSource { Image load() const { QImage img(QSize(DPI_SCALED(12), DPI_SCALED(12)), QImage::Format_ARGB32); img.fill(QColor(255, 255, 255, 0).rgba()); QPainter painter(&img); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(QPen(QColor(255, 255, 255, 255), DPI_SCALED(2))); painter.setBrush(Qt::NoBrush); painter.drawEllipse(DPI_SCALED(QPointF(6, 6)), DPI_SCALED(4), DPI_SCALED(4)); return img; } }; struct TinyDotImage : public TextureBank::ImageSource { Image load() const { QImage img(QSize(DPI_SCALED(5), DPI_SCALED(5)), QImage::Format_ARGB32); img.fill(QColor(255, 255, 255, 0).rgba()); QPainter painter(&img); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(Qt::NoPen); painter.setBrush(Qt::white); painter.drawEllipse(DPI_SCALED(QPointF(2.5f, 2.5f)), DPI_SCALED(2), DPI_SCALED(2)); return img; } }; struct StyleImage : public TextureBank::ImageSource { StyleImage(DotPath const &id) : ImageSource(id) {} Image load() const { return Style::get().images().image(id()); } }; CanvasWindow *window; QScopedPointer atlas; ///< Shared atlas for most UI graphics/text. GLUniform uTexAtlas; TextureBank texBank; ///< Bank for the atlas contents. bool noFramesDrawnYet; Instance(Public *i, CanvasWindow *win) : Base(i) , window(win) , atlas(0) , uTexAtlas("uTex", GLUniform::Sampler2D) , noFramesDrawnYet(true) { self.audienceForChildAddition() += this; } ~Instance() { GuiWidget::recycleTrashedWidgets(); // Tell all widgets to release their resource allocations. The base // class destructor will destroy all widgets, but this class governs // shared GL resources, so we'll ask the widgets to do this now. self.notifyTree(&Widget::deinitialize); // Destroy GUI widgets while the shared resources are still available. self.clearTree(); } void initAtlas() { if(atlas.isNull()) { atlas.reset(AtlasTexture::newWithKdTreeAllocator( Atlas::BackingStore | Atlas::AllowDefragment, GLTexture::maximumSize().min(GLTexture::Size(4096, 4096)))); uTexAtlas = *atlas; texBank.setAtlas(*atlas); // Load a set of general purpose textures (derived classes may extend this). self.loadCommonTextures(); } } void initBankContents() { // Built-in images. texBank.add(ID_SOLID_WHITE, new SolidWhiteImage); texBank.add(ID_THIN_ROUND_CORNERS, new ThinCornersImage); texBank.add(ID_BOLD_ROUND_CORNERS, new BoldCornersImage); texBank.add(ID_DOT, new TinyDotImage); // All style images. Style const &st = Style::get(); ImageBank::Names imageNames; st.images().allItems(imageNames); foreach(String const &name, imageNames) { texBank.add("Style." + name, new StyleImage(name)); } } void widgetChildAdded(Widget &child) { // Make sure newly added children know the view size. child.viewResized(); child.notifyTree(&Widget::viewResized); } }; GuiRootWidget::GuiRootWidget(CanvasWindow *window) : d(new Instance(this, window)) {} void GuiRootWidget::setWindow(CanvasWindow *window) { d->window = window; } CanvasWindow &GuiRootWidget::window() { DENG2_ASSERT(d->window != 0); return *d->window; } void GuiRootWidget::addOnTop(GuiWidget *widget) { add(widget); } void GuiRootWidget::moveToTop(GuiWidget &widget) { if(widget.parentWidget()) { widget.parentWidget()->remove(widget); } addOnTop(&widget); } AtlasTexture &GuiRootWidget::atlas() { d->initAtlas(); return *d->atlas; } GLUniform &GuiRootWidget::uAtlas() { d->initAtlas(); return d->uTexAtlas; } Id GuiRootWidget::solidWhitePixel() const { d->initAtlas(); return d->texBank.texture(ID_SOLID_WHITE); } Id GuiRootWidget::roundCorners() const { d->initAtlas(); return d->texBank.texture(ID_THIN_ROUND_CORNERS); } Id GuiRootWidget::boldRoundCorners() const { d->initAtlas(); return d->texBank.texture(ID_BOLD_ROUND_CORNERS); } Id GuiRootWidget::borderGlow() const { d->initAtlas(); return d->texBank.texture("Style.window.borderglow"); } Id GuiRootWidget::tinyDot() const { d->initAtlas(); return d->texBank.texture(ID_DOT); } Id GuiRootWidget::styleTexture(DotPath const &styleImagePath) const { d->initAtlas(); return d->texBank.texture(String("Style.") + styleImagePath); } GLShaderBank &GuiRootWidget::shaders() { return BaseGuiApp::shaders(); } Matrix4f GuiRootWidget::projMatrix2D() const { RootWidget::Size const size = viewSize(); return Matrix4f::ortho(0, size.x, 0, size.y); } void GuiRootWidget::routeMouse(Widget *routeTo) { setEventRouting(QList() << Event::MouseButton << Event::MouseMotion << Event::MousePosition << Event::MouseWheel, routeTo); } void GuiRootWidget::dispatchLatestMousePosition() {} bool GuiRootWidget::processEvent(Event const &event) { if(!RootWidget::processEvent(event)) { if(event.type() == Event::MouseButton) { // Button events that no one handles will relinquish input focus. setFocus(0); } return false; } return true; } void GuiRootWidget::handleEventAsFallback(Event const &) {} void GuiRootWidget::loadCommonTextures() { d->initBankContents(); } GuiWidget const *GuiRootWidget::globalHitTest(Vector2i const &pos) const { Widget::Children const childs = children(); for(int i = childs.size() - 1; i >= 0; --i) { if(GuiWidget const *w = childs.at(i)->maybeAs()) { if(GuiWidget const *hit = w->treeHitTest(pos)) { return hit; } } } return 0; } GuiWidget const *GuiRootWidget::guiFind(String const &name) const { return find(name)->maybeAs(); } void GuiRootWidget::update() { if(window().canvas().isGLReady()) { // Allow GL operations. window().canvas().makeCurrent(); RootWidget::update(); // Request a window draw so that the updated content becomes visible. window().as().draw(); } } void GuiRootWidget::draw() { if(d->noFramesDrawnYet) { // Widgets may not yet be ready on the first frame; make sure // we don't show garbage. window().canvas().renderTarget().clear(GLTarget::Color); d->noFramesDrawnYet = false; } #ifdef DENG2_DEBUG // Detect mistakes in GLState stack usage. dsize const depthBeforeDrawing = GLState::stackDepth(); #endif RootWidget::draw(); DENG2_ASSERT(GLState::stackDepth() == depthBeforeDrawing); } void GuiRootWidget::drawUntil(Widget &until) { NotifyArgs args(&Widget::draw); args.conditionFunc = &Widget::isVisible; args.preNotifyFunc = &Widget::preDrawChildren; args.postNotifyFunc = &Widget::postDrawChildren; args.until = &until; notifyTree(args); } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/style.cpp0000664000175000017500000001276312641367670022263 0ustar jaakkojaakko/** @file style.h User interface style. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/Style" #include #include #include #include #include #include namespace de { DENG2_PIMPL(Style) { Record module; RuleBank rules; FontBank fonts; ColorBank colors; ImageBank images; Instance(Public *i) : Base(i) { // The Style is available as a native module. App::scriptSystem().addNativeModule("Style", module); } void clear() { rules.clear(); fonts.clear(); colors.clear(); images.clear(); module.clear(); } void load(Package const &pack) { if(CommandLine::ArgWithParams arg = App::commandLine().check("-fontsize", 1)) { fonts.setFontSizeFactor(arg.params.at(0).toFloat()); } rules .addFromInfo(pack.root().locate("rules.dei")); fonts .addFromInfo(pack.root().locate("fonts.dei")); colors.addFromInfo(pack.root().locate("colors.dei")); images.addFromInfo(pack.root().locate("images.dei")); // Update the subrecords of the native module. module.add(new Variable("rules", new RecordValue(rules.names()), Variable::AllowRecord)); module.add(new Variable("fonts", new RecordValue(fonts.names()), Variable::AllowRecord)); module.add(new Variable("colors", new RecordValue(colors.names()), Variable::AllowRecord)); module.add(new Variable("images", new RecordValue(images.names()), Variable::AllowRecord)); } }; Style::Style() : d(new Instance(this)) {} Style::~Style() {} void Style::load(Package const &pack) { d->clear(); d->load(pack); } RuleBank const &Style::rules() const { return d->rules; } FontBank const &Style::fonts() const { return d->fonts; } ColorBank const &Style::colors() const { return d->colors; } ImageBank const &Style::images() const { return d->images; } RuleBank &Style::rules() { return d->rules; } FontBank &Style::fonts() { return d->fonts; } ColorBank &Style::colors() { return d->colors; } ImageBank &Style::images() { return d->images; } void Style::richStyleFormat(int contentStyle, float &sizeFactor, Font::RichFormat::Weight &fontWeight, Font::RichFormat::Style &fontStyle, int &colorIndex) const { switch(contentStyle) { default: case Font::RichFormat::NormalStyle: sizeFactor = 1.f; fontWeight = Font::RichFormat::OriginalWeight; fontStyle = Font::RichFormat::OriginalStyle; colorIndex = Font::RichFormat::OriginalColor; break; case Font::RichFormat::MajorStyle: sizeFactor = 1.f; fontWeight = Font::RichFormat::Bold; fontStyle = Font::RichFormat::Regular; colorIndex = Font::RichFormat::HighlightColor; break; case Font::RichFormat::MinorStyle: sizeFactor = .8f; fontWeight = Font::RichFormat::Normal; fontStyle = Font::RichFormat::Regular; colorIndex = Font::RichFormat::DimmedColor; break; case Font::RichFormat::MetaStyle: sizeFactor = .9f; fontWeight = Font::RichFormat::Light; fontStyle = Font::RichFormat::Italic; colorIndex = Font::RichFormat::AccentColor; break; case Font::RichFormat::MajorMetaStyle: sizeFactor = .9f; fontWeight = Font::RichFormat::Bold; fontStyle = Font::RichFormat::Italic; colorIndex = Font::RichFormat::AccentColor; break; case Font::RichFormat::MinorMetaStyle: sizeFactor = .8f; fontWeight = Font::RichFormat::Light; fontStyle = Font::RichFormat::Italic; colorIndex = Font::RichFormat::DimAccentColor; break; case Font::RichFormat::AuxMetaStyle: sizeFactor = .8f; fontWeight = Font::RichFormat::Light; fontStyle = Font::RichFormat::OriginalStyle; colorIndex = Font::RichFormat::AltAccentColor; break; } } Font const *Style::richStyleFont(Font::RichFormat::Style fontStyle) const { switch(fontStyle) { case Font::RichFormat::Monospace: return &fonts().font("monospace"); default: return nullptr; } } bool Style::isBlurringAllowed() const { return true; } GuiWidget *Style::sharedBlurWidget() const { return nullptr; } static Style *theAppStyle = nullptr; Style &Style::get() { DENG2_ASSERT(theAppStyle != 0); return *theAppStyle; } void Style::setAppStyle(Style &newStyle) { theAppStyle = &newStyle; /// @todo Notify everybody about the style change. -jk } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/gridlayout.cpp0000664000175000017500000004316612641367670023307 0ustar jaakkojaakko/** @file gridlayout.cpp Widget layout for a grid of widgets. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GridLayout" #include "de/SequentialLayout" #include namespace de { DENG2_PIMPL(GridLayout) { typedef QMap CellAlignments; WidgetList widgets; Mode mode; int maxCols; int maxRows; Rule const *initialX; Rule const *initialY; Rule const *baseX; Rule const *baseY; Vector2i cell; Rule const *fixedCellWidth; Rule const *fixedCellHeight; QMap fixedColWidths; CellAlignments cellAlignment; Rule const *colPad; Rule const *rowPad; Rule const *zeroRule; struct Metric { Rule const *fixedLength; ///< May be @c NULL. Rule const *current; ///< Current size of column/row (replaced many times). IndirectRule *final; ///< Final size of column/row (for others to use). Rule const *accumulatedLengths; ///< Sum of sizes of previous columns/rows (for others to use). Rule const *minEdge; ///< Rule for the left/top edge. Rule const *maxEdge; ///< Rule for the right/bottom edge. ui::Alignment cellAlign;///< Cell alignment affecting the entire column/row. Metric() : fixedLength(0), current(0), final(new IndirectRule), accumulatedLengths(0), minEdge(0), maxEdge(0), cellAlign(ui::AlignLeft) {} ~Metric() { releaseRef(fixedLength); releaseRef(current); releaseRef(final); releaseRef(accumulatedLengths); releaseRef(minEdge); releaseRef(maxEdge); } }; typedef QList Metrics; Metrics cols; Metrics rows; Rule const *totalWidth; Rule const *totalHeight; SequentialLayout *current; bool needTotalUpdate; Instance(Public *i, Rule const &x, Rule const &y, Mode layoutMode) : Base(i), mode(layoutMode), maxCols(1), maxRows(1), initialX(holdRef(x)), initialY(holdRef(y)), baseX(holdRef(x)), baseY(holdRef(y)), fixedCellWidth(0), fixedCellHeight(0), colPad(0), rowPad(0), zeroRule(new ConstantRule(0)), totalWidth(new ConstantRule(0)), totalHeight(new ConstantRule(0)), current(0), needTotalUpdate(false) {} ~Instance() { releaseRef(initialX); releaseRef(initialY); releaseRef(baseX); releaseRef(baseY); releaseRef(fixedCellWidth); releaseRef(fixedCellHeight); releaseRef(colPad); releaseRef(rowPad); releaseRef(zeroRule); releaseRef(totalWidth); releaseRef(totalHeight); foreach(Rule const *rule, fixedColWidths.values()) { releaseRef(rule); } fixedColWidths.clear(); clearMetrics(); delete current; } void clear() { changeRef(baseX, *initialX); changeRef(baseY, *initialY); delete current; current = 0; needTotalUpdate = true; widgets.clear(); setup(maxCols, maxRows); } void clearMetrics() { qDeleteAll(cols); cols.clear(); qDeleteAll(rows); rows.clear(); cellAlignment.clear(); } void setup(int numCols, int numRows) { clearMetrics(); maxCols = numCols; maxRows = numRows; if(!maxCols) { mode = RowFirst; } else if(!maxRows) { mode = ColumnFirst; } // Allocate the right number of cols and rows. for(int i = 0; i < maxCols; ++i) { addMetric(cols); } for(int i = 0; i < maxRows; ++i) { addMetric(rows); } cell = Vector2i(0, 0); } Vector2i gridSize() const { return Vector2i(cols.size(), rows.size()); } void addMetric(Metrics &list) { Metric *m = new Metric; int pos = list.size(); // Check if there is a fixed width defined for this column. if(&list == &cols && fixedColWidths.contains(pos)) { DENG2_ASSERT(fixedColWidths[pos] != 0); m->fixedLength = holdRef(*fixedColWidths[pos]); } for(int i = 0; i < list.size(); ++i) { sumInto(m->accumulatedLengths, list[i]->fixedLength? *list[i]->fixedLength : *list[i]->final); } list << m; } void updateMaximum(Metrics &list, int index, Rule const &rule) { if(index < 0) index = 0; if(index >= list.size()) { // The list may expand. addMetric(list); } DENG2_ASSERT(index < list.size()); Metric &metric = *list[index]; if(!metric.fixedLength) { changeRef(metric.current, OperatorRule::maximum(rule, metric.current)); // Update the indirection. metric.final->setSource(*metric.current); } else { // Fixed lengths are never affected. metric.final->setSource(*metric.fixedLength); } } Rule const &columnLeftX(int col) { if(!cols.at(col)->minEdge) { Rule const *base = holdRef(initialX); if(col > 0) { if(colPad) changeRef(base, *base + *colPad * col); sumInto(base, *cols.at(col)->accumulatedLengths); } cols[col]->minEdge = base; } return *cols.at(col)->minEdge; } Rule const &columnRightX(int col) { if(col < cols.size() - 1) { return columnLeftX(col + 1); } if(!cols.at(col)->maxEdge) { cols[col]->maxEdge = holdRef(columnLeftX(col) + *cols.last()->final); } return *cols.at(col)->maxEdge; } Rule const &rowTopY(int row) const { if(!rows.at(row)->minEdge) { Rule const *base = holdRef(initialY); if(row > 0) { if(rowPad) changeRef(base, *base + *rowPad * row); sumInto(base, *rows.at(row)->accumulatedLengths); } rows[row]->minEdge = base; } return *rows.at(row)->minEdge; } ui::Alignment alignment(Vector2i pos) const { CellAlignments::const_iterator found = cellAlignment.find(pos); if(found != cellAlignment.end()) { return found.value(); } return cols.at(pos.x)->cellAlign; } /** * Begins the next column or row. */ void begin() { if(current) return; current = new SequentialLayout(*baseX, *baseY, mode == ColumnFirst? ui::Right : ui::Down); if(fixedCellWidth) { current->setOverrideWidth(*fixedCellWidth); } if(fixedCellHeight) { current->setOverrideHeight(*fixedCellHeight); } } /** * Ends the current column or row, if it is full. */ void end(int cellSpan) { DENG2_ASSERT(current != 0); // Advance to next cell. if(mode == ColumnFirst) { cell.x += cellSpan; if(maxCols > 0 && cell.x >= maxCols) { cell.x = 0; cell.y++; sumInto(baseY, current->height()); if(rowPad) sumInto(baseY, *rowPad * cellSpan); // This one is finished. delete current; current = 0; } } else { cell.y += cellSpan; if(maxRows > 0 && cell.y >= maxRows) { cell.y = 0; cell.x++; sumInto(baseX, current->width()); if(colPad) sumInto(baseX, *colPad * cellSpan); // This one is finished. delete current; current = 0; } } } /** * Appends a widget or empty cell into the grid. * * @param widget Widget. * @param space Empty cell. * @param cellSpan Number of cells this widget/space will span. * @param layoutWidth Optionally, a width to use for layout calculations instead * of the actual width of the widget/space. */ void append(GuiWidget *widget, Rule const *space, int cellSpan = 1, Rule const *layoutWidth = 0) { DENG2_ASSERT(!(widget && space)); DENG2_ASSERT(widget || space); begin(); Rule const *pad = (mode == ColumnFirst? colPad : rowPad); widgets << widget; // NULLs included. if(widget) { if(pad && !current->isEmpty()) { current->append(*widget, *pad); } else { current->append(*widget); } } else { if(pad && !current->isEmpty()) current->append(*pad); current->append(*space); } Rule const &cellWidth = (layoutWidth? *layoutWidth : widget? widget->rule().width() : *space); // Update the column and row maximum width/height. if(mode == ColumnFirst) { if(cellSpan == 1) updateMaximum(cols, cell.x, cellWidth); if(widget) updateMaximum(rows, cell.y, widget->rule().height()); } else { if(cellSpan == 1) updateMaximum(rows, cell.y, widget? widget->rule().height() : *space); if(widget) updateMaximum(cols, cell.x, cellWidth); } if(widget) { // Cells in variable-width columns/rows must be positioned according to // the final column/row base widths. if(mode == ColumnFirst && !fixedCellWidth) { if(alignment(cell) & ui::AlignRight) { widget->rule() .clearInput(Rule::Left) .setInput(Rule::Right, columnRightX(cell.x + cellSpan - 1)); } else { widget->rule().setInput(Rule::Left, columnLeftX(cell.x)); } } else if(mode == RowFirst && !fixedCellHeight) { widget->rule().setInput(Rule::Top, rowTopY(cell.y)); } } end(cellSpan); needTotalUpdate = true; } void updateTotal() { if(!needTotalUpdate) return; Vector2i size = gridSize(); // Paddings must be included in the total. if(colPad) { changeRef(totalWidth, *colPad * size.x); } else { releaseRef(totalWidth); } if(rowPad) { changeRef(totalHeight, *rowPad * size.y); } else { releaseRef(totalHeight); } // Sum up the column widths. for(int i = 0; i < size.x; ++i) { DENG2_ASSERT(cols.at(i)); sumInto(totalWidth, *cols.at(i)->final); } // Sum up the row heights. for(int i = 0; i < size.y; ++i) { DENG2_ASSERT(rows.at(i)); sumInto(totalHeight, *rows.at(i)->final); } if(!totalWidth) totalWidth = new ConstantRule(0); if(!totalHeight) totalHeight = new ConstantRule(0); needTotalUpdate = false; } }; GridLayout::GridLayout(Mode mode) : d(new Instance(this, Const(0), Const(0), mode)) {} GridLayout::GridLayout(Rule const &startX, Rule const &startY, Mode mode) : d(new Instance(this, startX, startY, mode)) {} void GridLayout::clear() { d->clear(); } void GridLayout::setMode(GridLayout::Mode mode) { DENG2_ASSERT(isEmpty()); d->mode = mode; d->setup(d->maxCols, d->maxRows); } void GridLayout::setLeftTop(Rule const &left, Rule const &top) { DENG2_ASSERT(isEmpty()); changeRef(d->initialX, left); changeRef(d->initialY, top); changeRef(d->baseX, left); changeRef(d->baseY, top); } void GridLayout::setGridSize(int numCols, int numRows) { DENG2_ASSERT(numCols >= 0 && numRows >= 0); DENG2_ASSERT(numCols > 0 || numRows > 0); DENG2_ASSERT(isEmpty()); d->setup(numCols, numRows); } void GridLayout::setModeAndGridSize(GridLayout::Mode mode, int numCols, int numRows) { DENG2_ASSERT(isEmpty()); d->mode = mode; setGridSize(numCols, numRows); } void GridLayout::setColumnAlignment(int column, ui::Alignment cellAlign) { DENG2_ASSERT(column >= 0 && column < d->cols.size()); d->cols[column]->cellAlign = cellAlign; } void GridLayout::setColumnFixedWidth(int column, Rule const &fixedWidth) { DENG2_ASSERT(isEmpty()); if(d->fixedColWidths.contains(column)) { releaseRef(d->fixedColWidths[column]); } d->fixedColWidths[column] = holdRef(fixedWidth); // Set up the rules again. d->setup(d->maxCols, d->maxRows); } void GridLayout::setOverrideWidth(Rule const &width) { changeRef(d->fixedCellWidth, width); } void GridLayout::setOverrideHeight(Rule const &height) { changeRef(d->fixedCellHeight, height); } void GridLayout::setColumnPadding(Rule const &gap) { DENG2_ASSERT(isEmpty()); changeRef(d->colPad, gap); } void GridLayout::setRowPadding(Rule const &gap) { DENG2_ASSERT(isEmpty()); changeRef(d->rowPad, gap); } GridLayout &GridLayout::append(GuiWidget &widget, int cellSpan) { d->append(&widget, 0, cellSpan); return *this; } GridLayout &GridLayout::append(GuiWidget &widget, de::Rule const &layoutWidth, int cellSpan) { d->append(&widget, 0, cellSpan, &layoutWidth); return *this; } GridLayout &GridLayout::append(Rule const &empty) { d->append(0, &empty); return *this; } GridLayout &GridLayout::appendEmpty() { if(d->mode == ColumnFirst) { append(overrideWidth()); } else { append(overrideHeight()); } return *this; } WidgetList GridLayout::widgets() const { return d->widgets; } int GridLayout::size() const { return d->widgets.size(); } bool GridLayout::isEmpty() const { return !size(); } Vector2i GridLayout::maxGridSize() const { return Vector2i(d->maxCols, d->maxRows); } Vector2i GridLayout::gridSize() const { return d->gridSize(); } Vector2i GridLayout::widgetPos(GuiWidget &widget) const { Vector2i pos; foreach(Widget *w, d->widgets) { if(w == &widget) return pos; switch(d->mode) { case ColumnFirst: if(++pos.x >= d->maxCols) { pos.x = 0; ++pos.y; } break; case RowFirst: if(++pos.y >= d->maxRows) { pos.y = 0; ++pos.x; } break; } } return Vector2i(-1, -1); } GuiWidget *GridLayout::at(Vector2i const &cell) const { Vector2i pos; foreach(Widget *w, d->widgets) { if(pos == cell) { if(w) return &w->as(); return 0; } switch(d->mode) { case ColumnFirst: if(++pos.x >= d->maxCols) { pos.x = 0; ++pos.y; } break; case RowFirst: if(++pos.y >= d->maxRows) { pos.y = 0; ++pos.x; } break; } } return 0; } Rule const &GridLayout::width() const { d->updateTotal(); return *d->totalWidth; } Rule const &GridLayout::height() const { d->updateTotal(); return *d->totalHeight; } Rule const &GridLayout::columnLeft(int col) const { DENG2_ASSERT(col >= 0 && col < d->cols.size()); return d->columnLeftX(col); } Rule const &GridLayout::columnRight(int col) const { DENG2_ASSERT(col >= 0 && col < d->cols.size()); return d->columnRightX(col); } Rule const &GridLayout::columnWidth(int col) const { DENG2_ASSERT(col >= 0 && col < d->cols.size()); return *d->cols.at(col)->final; } Rule const &GridLayout::rowHeight(int row) const { DENG2_ASSERT(row >= 0 && row < d->rows.size()); return *d->rows.at(row)->final; } Rule const &GridLayout::overrideWidth() const { DENG2_ASSERT(d->fixedCellWidth != 0); return *d->fixedCellWidth; } Rule const &GridLayout::overrideHeight() const { DENG2_ASSERT(d->fixedCellHeight != 0); return *d->fixedCellHeight; } Rule const &GridLayout::columnPadding() const { if(d->colPad) return *d->colPad; return *d->zeroRule; } Rule const &GridLayout::rowPadding() const { if(d->rowPad) return *d->rowPad; return *d->zeroRule; } void GridLayout::setCellAlignment(Vector2i const &cell, ui::Alignment cellAlign) { d->cellAlignment[cell] = cellAlign; } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/dialogs/0000775000175000017500000000000012641367670022030 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libappfw/src/dialogs/messagedialog.cpp0000664000175000017500000000565412641367670025352 0ustar jaakkojaakko/** @file messagedialog.cpp Dialog for showing a message. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/MessageDialog" #include "de/SequentialLayout" #include "de/DialogContentStylist" namespace de { using namespace ui; DENG_GUI_PIMPL(MessageDialog) { LabelWidget *title; LabelWidget *message; DialogContentStylist buttonStylist; Instance(Public *i) : Base(i) { ScrollAreaWidget &area = self.area(); // Create widgets. area.add(title = new LabelWidget); area.add(message = new LabelWidget); // Configure style. title->setFont("title"); title->setTextColor("accent"); title->setSizePolicy(ui::Fixed, ui::Expand); title->setAlignment(ui::AlignLeft); title->setTextLineAlignment(ui::AlignLeft); message->setSizePolicy(ui::Fixed, ui::Expand); message->setAlignment(ui::AlignLeft); message->setTextLineAlignment(ui::AlignLeft); updateLayout(); } void updateLayout() { ScrollAreaWidget &area = self.area(); // Simple vertical layout. SequentialLayout layout(area.contentRule().left(), area.contentRule().top()); layout.setOverrideWidth(style().rules().rule("dialog.message.width")); // Put all the widgets into the layout. foreach(Widget *w, area.childWidgets()) { layout << w->as(); } area.setContentSize(layout.width(), layout.height()); } }; MessageDialog::MessageDialog(String const &name) : DialogWidget(name) , d(new Instance(this)) {} void MessageDialog::useInfoStyle() { DialogWidget::useInfoStyle(); title().setTextColor("inverted.accent"); message().setTextColor("inverted.text"); d->buttonStylist.addContainer(buttonsMenu()); d->buttonStylist.addContainer(extraButtonsMenu()); d->buttonStylist.setAdjustMargins(false); d->buttonStylist.setInfoStyle(true); } LabelWidget &MessageDialog::title() { return *d->title; } LabelWidget &MessageDialog::message() { return *d->message; } void MessageDialog::updateLayout() { d->updateLayout(); } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/dialogs/inputdialog.cpp0000664000175000017500000000330712641367670025056 0ustar jaakkojaakko/** @file dialogs/inputdialog.cpp Dialog for querying a string of text from the user. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/InputDialog" namespace de { DENG2_PIMPL_NOREF(InputDialog) { LineEditWidget *editor; }; InputDialog::InputDialog(String const &name) : MessageDialog(name), d(new Instance) { // Create the editor. area().add(d->editor = new LineEditWidget); d->editor->setSignalOnEnter(true); connect(d->editor, SIGNAL(enterPressed(QString)), this, SLOT(accept())); buttons() << new DialogButtonItem(Default | Accept) << new DialogButtonItem(Reject); updateLayout(); } LineEditWidget &InputDialog::editor() { return *d->editor; } void InputDialog::preparePanelForOpening() { MessageDialog::preparePanelForOpening(); root().setFocus(d->editor); } void InputDialog::panelClosing() { MessageDialog::panelClosing(); root().setFocus(0); } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/margins.cpp0000664000175000017500000001331112641367670022551 0ustar jaakkojaakko/** @file margins.cpp * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/ui/Margins" #include "de/Style" #include #include namespace de { namespace ui { enum Side { SideLeft, SideRight, SideTop, SideBottom, LeftRight, TopBottom, MAX_SIDES }; DENG2_PIMPL(Margins) { Rule const *inputs[4]; IndirectRule *outputs[MAX_SIDES]; Instance(Public *i, DotPath const &defaultId) : Base(i) { zap(inputs); zap(outputs); for(int i = 0; i < 4; ++i) { setInput(i, defaultId); } } ~Instance() { for(int i = 0; i < 4; ++i) { releaseRef(inputs[i]); } for(int i = 0; i < int(MAX_SIDES); ++i) { if(outputs[i]) { outputs[i]->unsetSource(); releaseRef(outputs[i]); } } } void setInput(int side, DotPath const &styleId) { setInput(side, Style::get().rules().rule(styleId)); } void setInput(int side, Rule const &rule) { DENG2_ASSERT(side >= 0 && side < 4); changeRef(inputs[side], rule); updateOutput(side); DENG2_FOR_AUDIENCE(Change, i) { i->marginsChanged(); } } void updateOutput(int side) { if(side < 4 && outputs[side] && inputs[side]) { outputs[side]->setSource(*inputs[side]); } // Update the sums. if(side == LeftRight || side == SideLeft || side == SideRight) { if(outputs[LeftRight] && inputs[SideLeft] && inputs[SideRight]) { outputs[LeftRight]->setSource(*inputs[SideLeft] + *inputs[SideRight]); } } else if(side == TopBottom || side == SideTop || side == SideBottom) { if(outputs[TopBottom] && inputs[SideTop] && inputs[SideBottom]) { outputs[TopBottom]->setSource(*inputs[SideTop] + *inputs[SideBottom]); } } } Rule const &getOutput(int side) { if(!outputs[side]) { outputs[side] = new IndirectRule; updateOutput(side); } return *outputs[side]; } DENG2_PIMPL_AUDIENCE(Change) }; DENG2_AUDIENCE_METHOD(Margins, Change) Margins::Margins(String const &defaultMargin) : d(new Instance(this, defaultMargin)) {} Margins &Margins::set(Direction dir, DotPath const &marginId) { d->setInput(dir == Left? SideLeft : dir == Right? SideRight : dir == Up? SideTop : SideBottom, marginId); return *this; } Margins &Margins::set(DotPath const &marginId) { set(Left, marginId); set(Right, marginId); set(Up, marginId); set(Down, marginId); return *this; } Margins &Margins::setLeft(DotPath const &leftMarginId) { return set(ui::Left, leftMarginId); } Margins &Margins::setRight(DotPath const &rightMarginId) { return set(ui::Right, rightMarginId); } Margins &Margins::setTop(DotPath const &topMarginId) { return set(ui::Up, topMarginId); } Margins &Margins::setBottom(DotPath const &bottomMarginId) { return set(ui::Down, bottomMarginId); } Margins &Margins::set(Direction dir, Rule const &rule) { d->setInput(dir == Left? SideLeft : dir == Right? SideRight : dir == Up? SideTop : SideBottom, rule); return *this; } Margins &Margins::set(Rule const &rule) { set(Left, rule); set(Right, rule); set(Up, rule); set(Down, rule); return *this; } Margins &Margins::setAll(Margins const &margins) { if(this == &margins) return *this; set(Left, margins.left()); set(Right, margins.right()); set(Up, margins.top()); set(Down, margins.bottom()); return *this; } Margins &Margins::setLeft(Rule const &rule) { return set(ui::Left, rule); } Margins &Margins::setRight(Rule const &rule) { return set(ui::Right, rule); } Margins &Margins::setTop(Rule const &rule) { return set(ui::Up, rule); } Margins &Margins::setBottom(Rule const &rule) { return set(ui::Down, rule); } Rule const &Margins::left() const { return d->getOutput(SideLeft); } Rule const &Margins::right() const { return d->getOutput(SideRight); } Rule const &Margins::top() const { return d->getOutput(SideTop); } Rule const &Margins::bottom() const { return d->getOutput(SideBottom); } Rule const &Margins::width() const { return d->getOutput(LeftRight); } Rule const &Margins::height() const { return d->getOutput(TopBottom); } Rule const &Margins::margin(Direction dir) const { return d->getOutput(dir == Left? SideLeft : dir == Right? SideRight : dir == Up? SideTop : SideBottom); } Vector4i Margins::toVector() const { return Vector4i(left().valuei(), top().valuei(), right().valuei(), bottom().valuei()); } } // namespace ui } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/childwidgetorganizer.cpp0000664000175000017500000002036612641367670025331 0ustar jaakkojaakko/** @file childwidgetorganizer.cpp Organizes widgets according to a UI context. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/ChildWidgetOrganizer" #include "de/LabelWidget" #include "de/ui/Item" #include #include namespace de { static DefaultWidgetFactory defaultWidgetFactory; DENG2_PIMPL(ChildWidgetOrganizer), DENG2_OBSERVES(Widget, Deletion ), DENG2_OBSERVES(ui::Data, Addition ), DENG2_OBSERVES(ui::Data, Removal ), DENG2_OBSERVES(ui::Data, OrderChange), DENG2_OBSERVES(ui::Item, Change ) { GuiWidget *container; ui::Data const *context; IWidgetFactory *factory; IFilter const *filter; typedef QMap Mapping; typedef QMutableMapIterator MutableMappingIterator; Mapping mapping; ///< Maps items to corresponding widgets. Instance(Public *i, GuiWidget *c) : Base(i), container(c), context(0), factory(&defaultWidgetFactory), filter(0) {} ~Instance() { DENG2_FOR_EACH_CONST(Mapping, i, mapping) { i.value()->audienceForDeletion() -= this; } } void set(ui::Data const *ctx) { if(context) { context->audienceForAddition() -= this; context->audienceForRemoval() -= this; context->audienceForOrderChange() -= this; clearWidgets(); context = 0; } context = ctx; if(context) { makeWidgets(); context->audienceForAddition() += this; context->audienceForRemoval() += this; context->audienceForOrderChange() += this; } } void addItemWidget(ui::Data::Pos pos, bool alwaysAppend = false) { DENG2_ASSERT_IN_MAIN_THREAD(); // widgets should only be manipulated in UI thread DENG2_ASSERT(factory != 0); if(filter) { if(!filter->isItemAccepted(self, *context, pos)) { // Skip this one. return; } } ui::Item const &item = context->at(pos); GuiWidget *w = factory->makeItemWidget(item, container); if(!w) return; // Unpresentable. // Update the widget immediately. mapping.insert(&item, w); itemChanged(item); if(alwaysAppend || pos == context->size() - 1) { // This is the last item. container->add(w); } else { container->insertBefore(w, *mapping[&context->at(pos + 1)]); } // Others may alter the widget in some way. DENG2_FOR_PUBLIC_AUDIENCE2(WidgetCreation, i) { i->widgetCreatedForItem(*w, item); } // Observe. w->audienceForDeletion() += this; // in case it's manually deleted item.audienceForChange() += this; } void makeWidgets() { DENG2_ASSERT(context != 0); DENG2_ASSERT(container != 0); for(ui::Data::Pos i = 0; i < context->size(); ++i) { addItemWidget(i, true /*always append*/); } } void deleteWidget(GuiWidget *w) { w->audienceForDeletion() -= this; GuiWidget::destroy(w); } void clearWidgets() { DENG2_FOR_EACH_CONST(Mapping, i, mapping) { i.key()->audienceForChange() -= this; deleteWidget(i.value()); } mapping.clear(); } void widgetBeingDeleted(Widget &widget) { /* * Note: this should not occur normally, as the widgets created by the * Organizer are not usually manually deleted. */ MutableMappingIterator iter(mapping); while(iter.hasNext()) { iter.next(); if(iter.value() == &widget) { iter.remove(); break; } } } void dataItemAdded(ui::Data::Pos pos, ui::Item const &) { addItemWidget(pos); } void dataItemRemoved(ui::Data::Pos, ui::Item &item) { Mapping::iterator found = mapping.find(&item); if(found != mapping.constEnd()) { found.key()->audienceForChange() -= this; deleteWidget(found.value()); mapping.erase(found); } } void dataItemOrderChanged() { // Remove all widgets and put them back in the correct order. DENG2_FOR_EACH_CONST(Mapping, i, mapping) { container->remove(*i.value()); } for(ui::Data::Pos i = 0; i < context->size(); ++i) { if(mapping.contains(&context->at(i))) { container->add(mapping[&context->at(i)]); } } } void itemChanged(ui::Item const &item) { if(!mapping.contains(&item)) { // Not represented as a child widget. return; } GuiWidget &w = *mapping[&item]; factory->updateItemWidget(w, item); // Notify. DENG2_FOR_PUBLIC_AUDIENCE2(WidgetUpdate, i) { i->widgetUpdatedForItem(w, item); } } GuiWidget *find(ui::Item const &item) const { Mapping::const_iterator found = mapping.constFind(&item); if(found == mapping.constEnd()) return 0; return found.value(); } GuiWidget *findByLabel(String const &label) const { DENG2_FOR_EACH_CONST(Mapping, i, mapping) { if(i.key()->label() == label) { return i.value(); } } return 0; } ui::Item const *findByWidget(GuiWidget const &widget) const { DENG2_FOR_EACH_CONST(Mapping, i, mapping) { if(i.value() == &widget) { return i.key(); } } return 0; } DENG2_PIMPL_AUDIENCE(WidgetCreation) DENG2_PIMPL_AUDIENCE(WidgetUpdate) }; DENG2_AUDIENCE_METHOD(ChildWidgetOrganizer, WidgetCreation) DENG2_AUDIENCE_METHOD(ChildWidgetOrganizer, WidgetUpdate) ChildWidgetOrganizer::ChildWidgetOrganizer(GuiWidget &container) : d(new Instance(this, &container)) {} void ChildWidgetOrganizer::setContext(ui::Data const &context) { d->set(&context); } void ChildWidgetOrganizer::unsetContext() { d->set(0); } ui::Data const &ChildWidgetOrganizer::context() const { DENG2_ASSERT(d->context != 0); return *d->context; } GuiWidget *ChildWidgetOrganizer::itemWidget(ui::Data::Pos pos) const { return itemWidget(context().at(pos)); } void ChildWidgetOrganizer::setWidgetFactory(IWidgetFactory &factory) { d->factory = &factory; } ChildWidgetOrganizer::IWidgetFactory &ChildWidgetOrganizer::widgetFactory() const { DENG2_ASSERT(d->factory != 0); return *d->factory; } void ChildWidgetOrganizer::setFilter(ChildWidgetOrganizer::IFilter const &filter) { d->filter = &filter; } GuiWidget *ChildWidgetOrganizer::itemWidget(ui::Item const &item) const { return d->find(item); } GuiWidget *ChildWidgetOrganizer::itemWidget(String const &label) const { return d->findByLabel(label); } ui::Item const *ChildWidgetOrganizer::findItemForWidget(GuiWidget const &widget) const { return d->findByWidget(widget); } GuiWidget *DefaultWidgetFactory::makeItemWidget(ui::Item const &, GuiWidget const *) { return new LabelWidget; } void DefaultWidgetFactory::updateItemWidget(GuiWidget &widget, ui::Item const &item) { widget.as().setText(item.label()); } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/dialogcontentstylist.cpp0000664000175000017500000000634012641367670025403 0ustar jaakkojaakko/** @file dialogcontentstylist.cpp Sets the style for widgets in a dialog. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/DialogContentStylist" #include "de/DialogWidget" #include "de/ToggleWidget" #include "de/LabelWidget" #include "de/LineEditWidget" #include "de/AuxButtonWidget" #include namespace de { DENG2_PIMPL_NOREF(DialogContentStylist) { QList containers; bool useInfoStyle; bool adjustMargins; Instance() : useInfoStyle(false) , adjustMargins(true) {} }; DialogContentStylist::DialogContentStylist() : d(new Instance) {} DialogContentStylist::DialogContentStylist(DialogWidget &dialog) : d(new Instance) { setContainer(dialog.area()); } DialogContentStylist::DialogContentStylist(GuiWidget &container) : d(new Instance) { setContainer(container); } DialogContentStylist::~DialogContentStylist() { clear(); } void DialogContentStylist::clear() { foreach(GuiWidget *w, d->containers) { w->audienceForChildAddition() -= this; } d->containers.clear(); } void DialogContentStylist::setContainer(GuiWidget &container) { clear(); addContainer(container); } void DialogContentStylist::addContainer(GuiWidget &container) { d->containers << &container; container.audienceForChildAddition() += this; } void DialogContentStylist::setInfoStyle(bool useInfoStyle) { d->useInfoStyle = useInfoStyle; } void DialogContentStylist::setAdjustMargins(bool yes) { d->adjustMargins = yes; } void DialogContentStylist::widgetChildAdded(Widget &child) { applyStyle(child.as()); } void DialogContentStylist::applyStyle(GuiWidget &w) { if(d->adjustMargins) { if(!w.is()) { w.margins().set("dialog.gap"); } } // All label-based widgets should expand on their own. if(LabelWidget *lab = w.maybeAs()) { lab->setSizePolicy(ui::Expand, ui::Expand); } // Button background override? if(ButtonWidget *but = w.maybeAs()) { if(d->useInfoStyle) { but->useInfoStyle(); } } // Toggles should have no background. if(ToggleWidget *tog = w.maybeAs()) { tog->set(GuiWidget::Background()); } if(LineEditWidget *ed = w.maybeAs()) { ed->rule().setInput(Rule::Width, d->containers.first()->style().rules().rule("editor.width")); } } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/fontlinewrapping.cpp0000664000175000017500000005060612641367670024507 0ustar jaakkojaakko/** @file fontlinewrapping.cpp Font line wrapping. * * @todo Performance|Refactor: Add a class dedicated for measuring text. Allow * measuring in increments, one character at a time, without re-measuring the * whole range. Allow seeking forward and backward with the measurer. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/FontLineWrapping" #include namespace de { using namespace shell; static QChar const NEWLINE('\n'); DENG2_PIMPL_NOREF(FontLineWrapping) { Font const *font; /** * A wrapped line of text. */ struct Line { WrappedLine line; LineInfo info; int width; ///< Total width of the line (in pixels). Line(WrappedLine const &ln = WrappedLine(Rangei()), int lineWidth = 0, int leftIndent = 0) : line(ln), width(lineWidth) { info.indent = leftIndent; } /// Tab stops are disabled if there is a tab stop < 0 anywhere on the line. bool tabsDisabled() const { for(int i = 0; i < info.segs.size(); ++i) { if(info.segs[i].tabStop < 0) return true; } return false; } }; typedef QList Lines; Lines lines; int maxWidth; String text; ///< Plain text. Font::RichFormat format; int indent; ///< Current left indentation (in pixels). QList prevIndents; int tabStop; Instance() : font(0), maxWidth(0), indent(0), tabStop(0) {} ~Instance() { clearLines(); } void clearLines() { qDeleteAll(lines); lines.clear(); } String rangeText(Rangei const &range) const { return text.substr(range.start, range.size()); } int rangeVisibleWidth(Rangei const &range) const { if(font) { return font->measure(rangeText(range), format.subRange(range)).width(); } return 0; } int rangeAdvanceWidth(Rangei const &range) const { if(font) { return font->advanceWidth(rangeText(range), format.subRange(range)); } return 0; } void updateIndentMarkWidth(Rangei const &range) { Font::RichFormatRef rich = format.subRange(range); Font::RichFormat::Iterator iter(rich); int const origIndent = indent; while(iter.hasNext()) { iter.next(); if(iter.markIndent()) { prevIndents.append(indent); indent = origIndent + rangeAdvanceWidth(Rangei(0, iter.range().start) + range.start); } if(iter.resetIndent()) { if(!prevIndents.isEmpty()) { indent = prevIndents.takeLast(); } else { indent = 0; } } } } /** * Constructs a wrapped line. Note that indent and tabStop are modified; * this is expected to be called in the right order as lines are being * processed. * * @param range Range in the content for the line. * @param width Width of the line in pixel. If -1, will be calculated. * * @return Line instance. Caller gets ownership. */ Line *makeLine(Rangei const &range, int width = -1) { if(width < 0) { // Determine the full width now. width = rangeVisibleWidth(range); } Line *line = new Line(WrappedLine(range), width, indent); // Determine segments in the line. int pos = range.start; Font::RichFormatRef rich = format.subRange(range); Font::RichFormat::Iterator iter(rich); // Divide the line into segments based on tab stops. while(iter.hasNext()) { iter.next(); if(iter.tabStop() != tabStop) { int const start = range.start + iter.range().start; if(start > pos) { line->info.segs << LineInfo::Segment(Rangei(pos, start), tabStop); pos = start; } tabStop = iter.tabStop(); } } // The final segment. line->info.segs << LineInfo::Segment(Rangei(pos, range.end), tabStop); // Determine segment widths. if(line->info.segs.size() == 1) { line->info.segs[0].width = width; } else { for(int i = 0; i < line->info.segs.size(); ++i) { line->info.segs[i].width = rangeAdvanceWidth(line->info.segs[i].range); } } // Check for possible indent for following lines. updateIndentMarkWidth(range); return line; } bool isAllSpace(Rangei const &range) const { for(int i = range.start; i < range.end; ++i) { if(!text.at(i).isSpace()) return false; } return true; } bool containsNewline(Rangei const &range) const { for(int i = range.start; i < range.end; ++i) { if(text.at(i) == NEWLINE) return true; } return false; } bool containsTabs(Rangei const &range) const { Font::RichFormatRef rich = format.subRange(range); Font::RichFormat::Iterator iter(rich); while(iter.hasNext()) { iter.next(); if(iter.tabStop() > 0) return true; } return false; } int findMaxWrap(int begin, int availableWidth) const { int width = 0; int end = begin; while(end < text.size() && text.at(end) != NEWLINE) { int const charWidth = rangeAdvanceWidth(Rangei(end, end + 1)); if(width + charWidth > availableWidth) { // Does not fit any more. break; } width += charWidth; ++end; } // Fine-tune the result to be accurate (kerning is ignored and rouding errors // affect the end result when checking width character by character). while(end > begin && rangeAdvanceWidth(Rangei(begin, end)) > availableWidth) { // Came out too long. end--; } return end; } bool isWrappable(int at) { if(at >= text.size()) return true; if(text.at(at).isSpace()) return true; if(at > 0) { QChar const prev = text.at(at - 1); if(prev == '/' || prev == '\\') return true; } return false; } Rangei untilNextNewline(int start) const { int pos = start; while(pos < text.size()) { // The newline is omitted from the range. if(text[pos] == '\n') break; ++pos; } return Rangei(start, pos); } /** * Wraps the range onto one or more lines. * * @param rangeToWrap Range in the content string. * @param maxWidth Maximum width of a line. * @param subsequentMaxWidth Maximum width of lines beyond the first one. * Note: if larger than zero, the line is considered * to contain tabbed segments. * @param initialIndent Initial value for the indent. * * @return The produced wrapped lines. Caller gets ownership. */ Lines wrapRange(Rangei const &rangeToWrap, int maxWidth, int subsequentMaxWidth = 0, int initialIndent = 0) { int const MIN_LINE_WIDTH = 150; bool const isTabbed = (subsequentMaxWidth > 0); indent = initialIndent; tabStop = 0; int begin = rangeToWrap.start; Lines wrappedLines; while(begin < rangeToWrap.end) { int mw = maxWidth; if(!wrappedLines.isEmpty() && subsequentMaxWidth > 0) mw = subsequentMaxWidth; // How much width is available, taking indentation into account? if(mw - indent < MIN_LINE_WIDTH) { if(!isTabbed) { // Regular non-tabbed line -- there is no room for this indent, // fall back to the previous one. if(prevIndents.isEmpty()) { indent = 0; } else { indent = prevIndents.last(); } } else { // We can't alter indentation with tabs, so just extend the line instead. mw = MIN_LINE_WIDTH + indent; } } int availWidth = mw - indent; // Range for the remainder of the text. Rangei const range(begin, rangeToWrap.end); // Quick check: does the complete remainder fit? if(!containsNewline(range)) { int visWidth = rangeAdvanceWidth(range); if(visWidth <= availWidth) { wrappedLines << makeLine(range, visWidth); break; } } // Newlines always cause a wrap. #if 0 int wrapPosMax; int end = findMaxWrap(begin, availWidth, wrapPosMax); #else int end = findMaxWrap(begin, availWidth); int wrapPosMax = end; #endif if(end < rangeToWrap.end && text.at(end) == NEWLINE) { // The newline will be omitted from the wrapped lines. wrappedLines << makeLine(Rangei(begin, end)); begin = end + 1; } else { if(end <= begin) break; // Rewind to find a good (whitespace) break point. while(!isWrappable(end)) { if(--end == begin) { // Ran out of non-space chars, force a break. end = wrapPosMax; break; } } DENG2_ASSERT(end > begin); // If there is only whitespace remaining on the line, // just use the max wrap -- blank lines are not pretty. if(isAllSpace(Rangei(begin, end))) { end = wrapPosMax; } while(end < rangeToWrap.end && text.at(end).isSpace()) ++end; wrappedLines << makeLine(Rangei(begin, end)); begin = end; } } return wrappedLines; } Rangei findNextTabbedRange(int startLine) const { for(int i = startLine + 1; i < lines.size(); ++i) { if(lines[i]->tabsDisabled()) return Rangei(startLine, i); } return Rangei(startLine, lines.size()); } /** * Wraps a range of lines that contains tab stops. Wrapping takes into * account the space available for each tab stop. * * @param lineRange Range of lines to wrap. * * @return End of the range, taking into account possible extra lines produced * when wrapping long lines. */ int wrapLinesWithTabs(Rangei const &lineRange) { int extraLinesProduced = 0; // Determine the actual positions of each tab stop according to segment widths. QMap stopMaxWidths; // stop => maxWidth for(int i = lineRange.start; i < lineRange.end; ++i) { Line *line = lines[i]; for(int k = 0; k < line->info.segs.size(); ++k) { LineInfo::Segment const &seg = line->info.segs[k]; if(seg.tabStop < 0) continue; int sw = seg.width; // Include overall indent into the first segment width. if(!k) sw += line->info.indent; stopMaxWidths[seg.tabStop] = de::max(stopMaxWidths[seg.tabStop], sw); } } // Now we can wrap the lines that area too long. for(int i = lineRange.start; i < lineRange.end + extraLinesProduced; ++i) { Line *line = lines[i]; int curLeft = 0; int prevRight = 0; for(int k = 0; k < line->info.segs.size(); ++k) { LineInfo::Segment const &seg = line->info.segs[k]; int const tab = seg.tabStop; int const stopWidth = (tab >= 0? stopMaxWidths[tab] : seg.width); if(curLeft + stopWidth >= maxWidth) { // Wrap the line starting from this segment. // The maximum width of the first line is reduced by the // added amount of tab space: the difference between the // left edge of the current segment and the right edge of // the previous one. The maximum widths of subsequent lines // is also adjusted, so that the available space depends on // where the current tab is located (indent is added // because wrapRange automatically subtracts it). Lines wrapped = wrapRange(line->line.range, maxWidth - (curLeft - prevRight), maxWidth - curLeft + line->info.indent, line->info.indent); extraLinesProduced += wrapped.size() - 1; // Replace the original line with these wrapped lines. delete lines.takeAt(i); foreach(Line *wl, wrapped) { lines.insert(i++, wl); } --i; break; // Proceed to next line. } // Update the coordinate of the previous segment's right edge. prevRight = curLeft + seg.width; if(!k) prevRight += line->info.indent; // Move on to the next segment's left edge. curLeft += stopWidth; } } return lineRange.end + extraLinesProduced; } }; FontLineWrapping::FontLineWrapping() : d(new Instance) {} void FontLineWrapping::setFont(Font const &font) { DENG2_GUARD(this); d->font = &font; } Font const &FontLineWrapping::font() const { DENG2_GUARD(this); DENG2_ASSERT(hasFont()); return *d->font; } bool FontLineWrapping::hasFont() const { return d->font != 0; } bool FontLineWrapping::isEmpty() const { DENG2_GUARD(this); return d->lines.isEmpty(); } void FontLineWrapping::clear() { DENG2_GUARD(this); reset(); d->text.clear(); } void FontLineWrapping::reset() { DENG2_GUARD(this); d->clearLines(); d->indent = 0; d->prevIndents.clear(); d->tabStop = 0; } void FontLineWrapping::wrapTextToWidth(String const &text, int maxWidth) { wrapTextToWidth(text, Font::RichFormat::fromPlainText(text), maxWidth); } void FontLineWrapping::wrapTextToWidth(String const &text, Font::RichFormat const &format, int maxWidth) { DENG2_GUARD(this); String newText = text; clear(); if(maxWidth <= 1 || !d->font) return; // This is the text that we will be wrapping. d->maxWidth = maxWidth; d->text = newText; d->format = format; // When tabs are used, we must first determine the maximum width of each tab stop. if(d->containsTabs(Rangei(0, text.size()))) { d->indent = 0; d->tabStop = 0; // Divide the content into lines by newlines. int pos = 0; while(pos < text.size()) { Rangei const wholeLine = d->untilNextNewline(pos); d->lines << d->makeLine(wholeLine); pos = wholeLine.end + 1; } // Process the content in distinct ranges divided by untabbed content. Rangei tabRange = d->findNextTabbedRange(0); forever { int end = d->wrapLinesWithTabs(tabRange); if(end == d->lines.size()) { // All lines processed. break; } tabRange = d->findNextTabbedRange(end); } } else { // Doesn't have tabs -- just wrap it without any extra processing. d->lines = d->wrapRange(Rangei(0, text.size()), maxWidth); } if(d->lines.isEmpty()) { // Make sure at least one blank line exists. d->lines << new Instance::Line; } // Mark the final line. d->lines.last()->line.isFinal = true; #if 0 qDebug() << "Wrapped:" << d->text; foreach(Instance::Line const *ln, d->lines) { qDebug() << ln->line.range.asText() << d->text.substr(ln->line.range) << "indent:" << ln->info.indent << "segments:" << ln->info.segs.size(); foreach(LineInfo::Segment const &s, ln->info.segs) { qDebug() << "- seg" << s.range.asText() << d->text.substr(s.range) << "tab:" << s.tabStop << "w:" << s.width; } } #endif } String const &FontLineWrapping::text() const { DENG2_GUARD(this); return d->text; } WrappedLine FontLineWrapping::line(int index) const { DENG2_GUARD(this); DENG2_ASSERT(index >= 0 && index < height()); return d->lines[index]->line; } int FontLineWrapping::width() const { DENG2_GUARD(this); int w = 0; for(int i = 0; i < d->lines.size(); ++i) { w = de::max(w, d->lines[i]->width); } return w; } int FontLineWrapping::height() const { DENG2_GUARD(this); return d->lines.size(); } int FontLineWrapping::rangeWidth(Rangei const &range) const { DENG2_GUARD(this); return d->rangeAdvanceWidth(range); } int FontLineWrapping::indexAtWidth(Rangei const &range, int width) const { DENG2_GUARD(this); int prevWidth = 0; for(int i = range.start; i < range.end; ++i) { int const rw = d->rangeAdvanceWidth(Rangei(range.start, i)); if(rw >= width) { // Which is closer, this or the previous char? if(de::abs(rw - width) <= de::abs(prevWidth - width)) { return i; } return i - 1; } prevWidth = rw; } return range.end; } int FontLineWrapping::totalHeightInPixels() const { DENG2_GUARD(this); if(!d->font) return 0; int const lines = height(); int pixels = 0; if(lines > 1) { // Full baseline-to-baseline spacing. pixels += (lines - 1) * d->font->lineSpacing().value(); } if(lines > 0) { // The last (or a single) line is just one 'font height' tall. pixels += d->font->height().value(); } return pixels; } int FontLineWrapping::maximumWidth() const { DENG2_GUARD(this); return d->maxWidth; } Vector2i FontLineWrapping::charTopLeftInPixels(int line, int charIndex) { DENG2_GUARD(this); if(line >= height()) return Vector2i(); WrappedLine const span = d->lines[line]->line; Rangei const range(span.range.start, de::min(span.range.end, span.range.start + charIndex)); Vector2i cp; cp.x = d->rangeAdvanceWidth(range); cp.y = line * d->font->lineSpacing().valuei(); return cp; } FontLineWrapping::LineInfo const &FontLineWrapping::lineInfo(int index) const { return d->lines[index]->info; } int FontLineWrapping::LineInfo::highestTabStop() const { int stop = -1; foreach(Segment const &seg, segs) { stop = de::max(stop, seg.tabStop); } return stop; } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/untrapper.cpp0000664000175000017500000000260512641367670023135 0ustar jaakkojaakko/** @file untrapper.cpp Mouse untrapping utility. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/Untrapper" namespace de { DENG2_PIMPL(Untrapper) { CanvasWindow &window; bool wasTrapped; Instance(Public *i, CanvasWindow &w) : Base(i), window(w) { wasTrapped = window.canvas().isMouseTrapped(); if(wasTrapped) { window.canvas().trapMouse(false); } } ~Instance() { if(wasTrapped) { window.canvas().trapMouse(); } } }; Untrapper::Untrapper(CanvasWindow &window) : d(new Instance(this, window)) {} } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/proceduralimage.cpp0000664000175000017500000000307412641367670024261 0ustar jaakkojaakko/** @file proceduralimage.cpp Procedural image. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/ProceduralImage" namespace de { ProceduralImage::ProceduralImage(Size const &size) : _size(size), _color(1, 1, 1, 1) {} ProceduralImage::~ProceduralImage() {} ProceduralImage::Size ProceduralImage::size() const { return _size; } ProceduralImage::Color ProceduralImage::color() const { return _color; } void ProceduralImage::setSize(Size const &size) { _size = size; } void ProceduralImage::setColor(Color const &color) { _color = color; } bool ProceduralImage::update() { // optional for derived classes return false; } void ProceduralImage::glInit() { // optional for derived classes } void ProceduralImage::glDeinit() { // optional for derived classes } } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/item.cpp0000664000175000017500000000424312641367670022053 0ustar jaakkojaakko/** @file item.cpp Context item. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/ui/Item" namespace de { namespace ui { DENG2_PIMPL_NOREF(Item) { Data *context; Semantics semantics; String label; QVariant data; Instance(Semantics sem, String const &text = "", QVariant var = QVariant()) : context(0) , semantics(sem) , label(text) , data(var) {} DENG2_PIMPL_AUDIENCE(Change) }; DENG2_AUDIENCE_METHOD(Item, Change) Item::Item(Semantics semantics) : d(new Instance(semantics)) {} Item::Item(Semantics semantics, String const &label) : d(new Instance(semantics, label)) {} Item::~Item() {} Item::Semantics Item::semantics() const { return d->semantics; } void Item::setLabel(String const &label) { d->label = label; notifyChange(); } String Item::label() const { return d->label; } void Item::setDataContext(Data &context) { d->context = &context; } bool Item::hasDataContext() const { return d->context != 0; } Data &Item::dataContext() const { DENG2_ASSERT(hasDataContext()); return *d->context; } String Item::sortKey() const { return d->label; } void Item::setData(QVariant const &v) { d->data = v; } QVariant const &Item::data() const { return d->data; } void Item::notifyChange() { DENG2_FOR_AUDIENCE2(Change, i) { i->itemChanged(*this); } } } // namespace ui } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/data.cpp0000664000175000017500000000375212641367670022032 0ustar jaakkojaakko/** @file data.cpp UI data context. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/ui/Data" #include "de/ui/Item" #include "de/LabelWidget" #include namespace de { namespace ui { dsize const Data::InvalidPos = dsize(-1); static bool itemLessThan(Item const &a, Item const &b) { return a.sortKey().compareWithoutCase(b.sortKey()) < 0; } static bool itemGreaterThan(Item const &a, Item const &b) { return a.sortKey().compareWithoutCase(b.sortKey()) > 0; } DENG2_PIMPL_NOREF(Data) { DENG2_PIMPL_AUDIENCE(Addition) DENG2_PIMPL_AUDIENCE(Removal) DENG2_PIMPL_AUDIENCE(OrderChange) }; DENG2_AUDIENCE_METHOD(Data, Addition) DENG2_AUDIENCE_METHOD(Data, Removal) DENG2_AUDIENCE_METHOD(Data, OrderChange) Data::Data() : d(new Instance) {} void Data::sort(SortMethod method) { switch(method) { case Ascending: sort(itemLessThan); break; case Descending: sort(itemGreaterThan); break; } } LoopResult Data::forAll(std::function func) const { for(DataPos pos = 0; pos < size(); ++pos) { if(auto result = func(at(pos))) return result; } return LoopContinue; } } // namespace ui } // namespace de doomsday-stable-1.15.7/doomsday/libappfw/src/windowsystem.cpp0000664000175000017500000001007612641367670023672 0ustar jaakkojaakko/** @file windowsystem.cpp Window management subsystem. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/WindowSystem" #include #include namespace de { DENG2_PIMPL(WindowSystem) { typedef QMap Windows; Windows windows; QScopedPointer' print >> out, '' print >> out, '' print >> out, '' print >> out, '

%s

' % pageTitle def print_footer(out): print >> out, '' % time.asctime() print >> out, '' print >> out, '' def print_table(out, cells, cell_printer, numCols=4, tdStyle='', separateByFirstLetter=False, letterFunc=None): colSize = (len(cells) + numCols - 1) / numCols tdElem = '' % (100/numCols, tdStyle) print >> out, '' + tdElem idx = 0 inCol = 0 letter = '' while idx < len(cells): if separateByFirstLetter: if inCol > 0 and letter != '' and letter != letterFunc(cells[idx]): print >> out, '
' letter = letterFunc(cells[idx]) cell_printer(out, cells[idx]) idx += 1 inCol += 1 if inCol == colSize: print >> out, tdElem inCol = 0 print >> out, '
' # Create the index page with all tags sorted alphabetically. out = file(os.path.join(OUT_DIR, 'index.html'), 'wt') print_header(out, 'Alphabetical Tag Index') sortedTags = sorted(byTag.keys(), cmp=lambda a, b: cmp(a.lower(), b.lower())) def alpha_index_cell(out, tag): count = len(byTag[tag]) if count > 100: style = 'font-weight: bold' elif count < 5: style = 'color: #aaa' else: style = '' print >> out, '%s (%i)
' % ( tag_filename(tag), style, encoded_text(tag), count) print_table(out, sortedTags, alpha_index_cell, separateByFirstLetter=True, letterFunc=lambda t: t[0].lower()) print_footer(out) # Create the index page with tags sorted by size. out = file(os.path.join(OUT_DIR, 'index_by_size.html'), 'wt') print_header(out, 'All Tags by Size') def tag_size_comp(a, b): c = cmp(len(byTag[b]), len(byTag[a])) if c == 0: return cmp(a.lower(), b.lower()) return c def size_index_cell(out, tag): count = len(byTag[tag]) if count > 100: style = 'font-weight: bold' elif count < 5: style = 'color: #aaa' else: style = '' print >> out, '%i %s
' % ( tag_filename(tag), style, count, encoded_text(tag)) print_table(out, sorted(byTag.keys(), cmp=tag_size_comp), size_index_cell) print_footer(out) def print_tags(out, com, tag, linkSuffix=''): first = True for other in sorted(com.tags, cmp=lambda a, b: cmp(a.lower(), b.lower())): if other != tag: if not first: print >> out, '| ' first = False print >> out, '%s' % (linkSuffix, \ tag_filename(other), encoded_text(other)) def print_commit(out, com, tag, linkSuffix=''): print >> out, trElem + '' % com.link + com.date[:10] + ' ' print >> out, '' print_tags(out, com, tag, linkSuffix) print >> out, ':' print >> out, ('' % com.link + encoded_text(com.subject)[:250] + '').encode('utf8') colors = ['#f00', '#0a0', '#00f', '#ed0', '#f80', '#0ce', '#88f', '#b0b', '#f99', '#8d8', '#222', '#44a', '#940', '#888'] def html_color(colIdx): if colIdx == None: return '#000' return colors[colIdx % len(colors)] def color_box(colIdx): return '   ' % \ html_color(colIdx) def print_date_sorted_commits(out, coms, tag, linkSuffix='', colorIdx=None): months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] curDate = '' dateSorted = sorted(coms, key=lambda c: c.date, reverse=True) print >> out, '' for com in dateSorted: # Monthly headers. if curDate == '' or curDate != com.date[:7]: print >> out, '
' % \ (html_color(colorIdx), html_color(colorIdx)) print >> out, months[int(com.date[5:7]) - 1], com.date[:4] curDate = com.date[:7] print_commit(out, com, tag, linkSuffix) print >> out, '
' # Create the tag redirecting PHP page. out = file(os.path.join(OUT_DIR, 'find_tag.php'), 'wt') print >> out, '> out, '$tags = array(', string.join(['"%s" => "%s"' % (tag, tag_filename(tag)) for tag in byTag.keys()], ', '), ');' print >> out, """ $input = stripslashes(strip_tags($_GET["tag"])); $style = $_GET["style"]; if($style == 'grouped') { $style = 'group_'; } else { $style = ''; } $destination = "index.html"; $best = -1.0; if(strlen($input) > 0 && strlen($input) < 60) { foreach($tags as $tag => $link) { $lev = (float) levenshtein($input, $tag); // case sensitive if(stripos($tag, $input) !== FALSE || stripos($input, $tag) !== FALSE) { // Found as a case insensitive substring, increase likelihood. $lev = $lev/2.0; } if(stripos($tag, $input) === 0) { // Increase likelihood further if the match is in the beginning. $lev = $lev/2.0; } if(!strcasecmp($tag, $input) == 0) { // Case insensitive direct match, increase likelihood. $lev = $lev/2.0; } if($lev == 0) { $destination = "tag_$style$link.html"; break; } if($best < 0 || $lev < $best) { $destination = "tag_$style$link.html"; $best = $lev; } } } header("Location: $destination"); """ print >> out, "?>" def print_related_tags(out, tag, style=''): rels = find_related_tags(tag) if len(rels) == 0: return print >> out, '

Related tags: ' print >> out, string.join(['%s' % (style, tag_filename(t), t) for t in rels], ', ') print >> out, '

' def percentage(part, total): return int(round(float(part)/float(total) * 100)) def print_authorship(out, tag): authors = {} total = len(byTag[tag]) for commit in byTag[tag]: if commit.author in authors: authors[commit.author] += 1 else: authors[commit.author] = 1 sortedAuthors = sorted(authors.keys(), key=lambda a: authors[a], reverse=True) print >> out, '

Authorship:', string.join(['%i%% %s' % (percentage(authors[a], total), a) for a in sortedAuthors], ', ').encode('utf8'), '

' # # Create pages for each tag. # for tag in byTag.keys(): #print 'Generating tag page:', tag trElem = '' # First a simple date-based list of commits in this tag. out = file(os.path.join(OUT_DIR, 'tag_%s.html' % tag_filename(tag)), 'wt') print_header(out, tag) print_related_tags(out, tag) print_authorship(out, tag) print >> out, '

View commits by groups

' % tag_filename(tag) print >> out, '
' print_date_sorted_commits(out, byTag[tag], tag) print >> out, '
' print_footer(out) present = {} for com in byTag[tag]: for other in com.tags: if other == tag: continue if other not in present: present[other] = [com] else: present[other].append(com) def present_by_size(a, b): c = cmp(len(present[b]), len(present[a])) if c == 0: return cmp(a.lower(), b.lower()) return c presentSorted = sorted(present.keys(), cmp=present_by_size) # Then grouped by subgroup size. out = file(os.path.join(OUT_DIR, 'tag_group_%s.html' % tag_filename(tag)), 'wt') print_header(out, tag + ' (Grouped)') print_related_tags(out, tag, 'group_') print_authorship(out, tag) print >> out, '

View commits by date

' % tag_filename(tag) if len(byTag[tag]) > 10: print >> out, '
Jump down to:' class SkipTagPrinter: def __init__(self): self.color = 0 def __call__(self, out, tag): print >> out, '' + color_box(self.color) self.color += 1 print >> out, '%s (%i)
' % (tag_filename(tag), tag, len(present[tag])) print_table(out, presentSorted, SkipTagPrinter(), numCols=5, tdStyle='line-height:150%') print >> out, '
' # First the commits without any subgroups. print >> out, '
' print_date_sorted_commits(out, filter(lambda c: len(c.tags) == 1, byTag[tag]), tag) print >> out, '
' color = 0 for tag2 in presentSorted: print >> out, '

%s%s (%i) — %s

' % ( tag_filename(tag2), color_box(color), tag_filename(tag2), tag2, len(present[tag2]), tag) print >> out, '
' % html_color(color) print_date_sorted_commits(out, filter(lambda c: tag2 in c.tags, byTag[tag]), tag, 'group_', colorIdx=color) print >> out, '
' print >> out, '

↑ Back to top

' color += 1 print_footer(out) doomsday-stable-1.15.7/doomsday/build/scripts/packres.py0000775000175000017500000001143212641367670022607 0ustar jaakkojaakko#!/usr/bin/env python2.7 # This Python script will create a set of PK3 files that contain the files # that Doomsday needs at runtime. The PK3 files are organized using the # traditional data/ and defs/ structure. import sys, os, os.path, zipfile if len(sys.argv) < 2: print "Usage: %s pk3-target-dir" % sys.argv[0] print "(run in build/scripts/)" sys.exit(0) # Check quiet flag. quietMode = False if '--quiet' in sys.argv: sys.argv.remove('--quiet') quietMode = True deng_dir = os.path.join('..', '..') target_dir = os.path.abspath(sys.argv[1]) class Pack: def __init__(self): self.files = [] # tuples def add_files(self, fileNamesArray): self.files += fileNamesArray def msg(self, text): if not quietMode: print text def create(self, name): full_name = os.path.join(target_dir, name) self.msg("Creating %s as %s..." % (os.path.normpath(name), os.path.normpath(full_name))) pk3 = zipfile.ZipFile(full_name, 'w', zipfile.ZIP_DEFLATED) for src, dest in self.files: full_src = os.path.join(deng_dir, src) # Is this a file or a folder? if os.path.isfile(full_src): # Write the file as is. self.msg("writing %s as %s" % (os.path.normpath(full_src), os.path.normpath(dest))) pk3.write(full_src, dest) elif os.path.isdir(full_src): # Write the contents of the folder recursively. def process_dir(path, dest_path): self.msg("processing %s" % os.path.normpath(path)) for file in os.listdir(path): real_file = os.path.join(path, file) if file[0] == '.': continue # Ignore these. if os.path.isfile(real_file): if not quietMode: self.msg("writing %s as %s" % (os.path.normpath(real_file), os.path.normpath(os.path.join(dest_path, file)))) pk3.write(real_file, os.path.join(dest_path, file)) elif os.path.isdir(real_file): process_dir(real_file, os.path.join(dest_path, file)) process_dir(full_src, dest) # Write it out. print "Created %s (with %i files)." % (os.path.normpath(full_name), len(pk3.namelist())) pk3.close() # First up, doomsday.pk3. # Directory contents added recursively. p = Pack() p.add_files( [ ('client/data', 'data') ] ) p.create('doomsday.pk3') # libdoom.pk3 p = Pack() p.add_files( [ ('plugins/doom/defs', 'defs/jdoom'), ('plugins/doom/data/chex.mapinfo', 'data/jdoom/chex.mapinfo'), ('plugins/doom/data/doom1-share.mapinfo', 'data/jdoom/doom1-share.mapinfo'), ('plugins/doom/data/doom1-ultimate.mapinfo', 'data/jdoom/doom1-ultimate.mapinfo'), ('plugins/doom/data/doom1.mapinfo', 'data/jdoom/doom1.mapinfo'), ('plugins/doom/data/doom2-plut.mapinfo', 'data/jdoom/doom2-plut.mapinfo'), ('plugins/doom/data/doom2-tnt.mapinfo', 'data/jdoom/doom2-tnt.mapinfo'), ('plugins/doom/data/doom2-freedm.mapinfo', 'data/jdoom/doom2-freedm.mapinfo'), ('plugins/doom/data/doom2.mapinfo', 'data/jdoom/doom2.mapinfo'), ('plugins/doom/data/hacx.mapinfo', 'data/jdoom/hacx.mapinfo'), ('plugins/doom/data/conhelp.txt', 'data/jdoom/conhelp.txt'), ('plugins/doom/data/lumps', '#.basedata') ] ) p.create('libdoom.pk3') # libheretic.pk3 p = Pack() p.add_files( [ ('plugins/heretic/defs', 'defs/jheretic'), ('plugins/heretic/data/heretic-ext.mapinfo', 'data/jheretic/heretic-ext.mapinfo'), ('plugins/heretic/data/heretic-share.mapinfo', 'data/jheretic/heretic-share.mapinfo'), ('plugins/heretic/data/heretic.mapinfo', 'data/jheretic/heretic.mapinfo'), ('plugins/heretic/data/conhelp.txt', 'data/jheretic/conhelp.txt'), ('plugins/heretic/data/lumps', '#.basedata') ] ) p.create('libheretic.pk3') # libhexen.pk3 p = Pack() p.add_files( [ ('plugins/hexen/defs', 'defs/jhexen'), ('plugins/hexen/data/hexen-dk.mapinfo', 'data/jhexen/hexen-dk.mapinfo'), ('plugins/hexen/data/hexen.mapinfo', 'data/jhexen/hexen.mapinfo'), ('plugins/hexen/data/conhelp.txt', 'data/jhexen/conhelp.txt'), ('plugins/hexen/data/lumps', '#.basedata') ] ) p.create('libhexen.pk3') # libdoom64.pk3 p = Pack() p.add_files( [ ('plugins/doom64/defs', 'defs/jdoom64'), ('plugins/doom64/data/doom64.mapinfo', 'data/jdoom64/doom64.mapinfo'), ('plugins/doom64/data/conhelp.txt', 'data/jdoom64/conhelp.txt'), ('plugins/doom64/data/lumps', '#.basedata') ] ) p.create('libdoom64.pk3') doomsday-stable-1.15.7/doomsday/build/win32/0000775000175000017500000000000012641367670020054 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/build/win32/qmake_msvc.py0000664000175000017500000001117512641367670022561 0ustar jaakkojaakko# qmake_msvc.py is a Python 2 script that generates a full Visual Studio # solution with a .vcxproj for each subproject. You must set up envconfig.bat # before running the script. The solution is always placed in a folder called # "doomsday-msvc-build" at the repository root. # # qmake_msvc.py must be called whenever the .pro/.pri files change. The .sln # and .vcxproj files are then rewritten -- manual edits will be gone! # # The solution automatically deploys all built binaries into the # "distrib\products" folder. The user is expected to 1) copy the required # dependency .dlls to products\bin, and 2) define the appropriate launch # command line and options (which are persistently then stored in the # .vcxproj.user file). import os, sys, re print "\nVISUAL STUDIO SOLUTION GENERATOR FOR DOOMSDAY\n" print "Author: \n" # Figure out the Doomsday root path. rootPath = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..', '..')) solPath = os.path.abspath(os.path.join(rootPath, '..', 'doomsday-msvc-build')) print "Doomsday root path:", rootPath print "Solution location: ", solPath if not os.path.exists(solPath): os.mkdir(solPath) # Run qmake to generate the solution and vcprojs. os.chdir(solPath) os.system(os.path.join(rootPath, 'build', 'win32', 'qmake_tp_vc.bat')) # Cleanup. print "\nDeleting spurious PK3s..." for name in ['doomsday', 'libdoom', 'libheretic', 'libhexen', 'libdoom64']: fn = "..\%s.pk3" % name if os.path.exists(fn): os.remove(fn) print "\nPatching..." def fileRegex(fn, pattern, repl, count=0, startAtPos=0): text = file(fn, 'r').read() skipped = text[:startAtPos] text = text[startAtPos:] text = re.sub(pattern, repl, text, count) file(fn, 'w').write(skipped + text) def fileRegOff(fn, pattern, startAtPos=0): text = file(fn, 'r').read() text = text[startAtPos:] found = re.search(pattern, text) return found.start() for proj in ['libcore\deng_core.vcxproj', 'tools\shell\shell-gui\Doomsday-Shell.vcxproj']: fileRegex(proj, r'=\\"([A-Za-z0-9_\-\. ]+)\\"', r'="\1"') copyScript = 'python "%s\\\\build\win32\copy_to_products.py' % rootPath + '" "' + solPath + '"' rcDirs = '"' + rootPath + '\\\\api";"' + rootPath + '\\\\liblegacy\include' + '"' def find_projs(path): found = [] for name in os.listdir(path): fn = os.path.join(path, name) if os.path.isdir(fn) and '.' not in name: found += find_projs(fn) elif name[-8:] == '.vcxproj': found.append(os.path.join(path, name)) return found allProjs = find_projs(solPath) print "Found %i projects." % len(allProjs) for fn in allProjs: pName = os.path.basename(fn) pName = pName[:pName.index('.')] # Fix target names. fileRegex(fn, '[0-2]', '') fileRegex(fn, '[0-2]', '') # Add a post-build step for Debug config. start = fileRegOff(fn, r'\n', r""" """ + copyScript + r""" Debug Copy """ + pName + r""" to products/bin \n""", 1, start) # Tell RC where to find headers. fileRegex(fn, r'\n', r""" """ + rcDirs + """\n \n""", 1, start) # Add a post-build step for Release config. start = fileRegOff(fn, r'\n', r""" """ + copyScript + r""" Release Copy """ + pName + r""" to products/bin \n""", 1, start) # Tell RC where to find headers. fileRegex(fn, r'\n', r""" """ + rcDirs + """\n \n""", 1, start) # Enable debug symbols in all configs, not just Debug. fileRegex(fn, r'false', 'true') print "\nAll done!\n" print 'Solution generated:', os.path.join(solPath, 'doomsday.sln') doomsday-stable-1.15.7/doomsday/build/win32/qmake_tp_vc.bat0000664000175000017500000000040112641367670023030 0ustar jaakkojaakko@echo off REM -- Generates a Visual Studio solution and vcprojs for the project. REM -- Note: Expected to be called by qmake_msvc.py. REM -- Set up the build environment. call ..\doomsday\build\win32\envconfig.bat qmake -tp vc -r ..\doomsday\doomsday.pro doomsday-stable-1.15.7/doomsday/build/win32/envconfig-example.bat0000664000175000017500000000123512641367670024154 0ustar jaakkojaakko@echo off :: Sets up the command line environment for building. :: This is automatically called by dorel.bat. :: Directory Config ------------------------------------------------------- set ENVCONFIG_DIR=%CD% :: Modify these paths for your system. set MSVC_DIR=c:\Program Files\Microsoft Visual Studio 12.0 set QTCREATOR_DIR=c:\Qt\Qt5.3.1\Tools\QtCreator set QT_BIN_DIR=C:\Qt\Qt5.3.1\5.3\msvc2013_opengl\bin :: Build Tools Setup ------------------------------------------------------ :: Visual C++ environment. call "%MSVC_DIR%\vc\vcvarsall.bat" :: -- Qt environment. set JOM=%QTCREATOR_DIR%\bin\jom.exe /nologo call "%QT_BIN_DIR%\qtenv2.bat" cd %ENVCONFIG_DIR% doomsday-stable-1.15.7/doomsday/build/win32/copy_to_products.py0000664000175000017500000000472512641367670024035 0ustar jaakkojaakko# Copies all updated binaries to the distrib/products folder. # Called from Visual Studio as a post-build step for all projects. import sys, os, shutil solDir = sys.argv[1] buildType = sys.argv[2] def copy_if_newer(src, dest): if os.path.exists(dest): srcTime = os.stat(src).st_mtime destTime = os.stat(dest).st_mtime if srcTime <= destTime: return try: shutil.copyfile(src, dest) print 'DEPLOY :', os.path.basename(src), '->', os.path.abspath(dest) except IOError: pass # Set up the destination. dest = os.path.abspath(os.path.join(solDir, '..', 'distrib', 'products', 'bin')) if not os.path.exists(dest): os.makedirs(dest) if not os.path.exists(dest + '\\plugins'): os.makedirs(dest + '\\plugins') if not os.path.exists(dest + '\\..\\data'): os.makedirs(dest + '\\..\\data') # Update .pk3s. copy_if_newer(os.path.join(solDir, 'doomsday.pk3'), os.path.join(dest, '..', 'data', 'doomsday.pk3')) copy_if_newer(os.path.join(solDir, 'libdoom.pk3'), os.path.join(dest, '..', 'data', 'jdoom', 'libdoom.pk3')) copy_if_newer(os.path.join(solDir, 'libheretic.pk3'), os.path.join(dest, '..', 'data', 'jheretic', 'libdoom.pk3')) copy_if_newer(os.path.join(solDir, 'libhexen.pk3'), os.path.join(dest, '..', 'data', 'jhexen', 'libhexen.pk3')) copy_if_newer(os.path.join(solDir, 'libdoom64.pk3'), os.path.join(dest, '..', 'data', 'jdoom64', 'libdoom64.pk3')) targets = ['libcore', 'liblegacy', 'libshell', 'libgui', 'libappfw', 'libdoomsday', 'client', 'server', 'plugins\*', 'tools\*', 'tools\shell\*', 'tests\*'] for target in targets: dirs = [] if '*' in target: try: dirs += [target[:-1] + d + '\\' + buildType for d in os.listdir(os.path.join(solDir, target[:-1]))] except: pass else: dirs += [target + '\\' + buildType] for dir in dirs: d = os.path.join(solDir, dir) if not os.path.exists(d): continue for fn in os.listdir(d): if fn[-4:] == '.exe': #print '- exe:', fn copy_if_newer(os.path.join(d, fn), os.path.join(dest, fn)) elif fn[-4:] == '.dll': destPath = dest if 'plugins' in dir: #print '- plugin dll:', fn destPath += '\\plugins' #else: # print '- dll:', fn copy_if_newer(os.path.join(d, fn), os.path.join(destPath, fn)) doomsday-stable-1.15.7/doomsday/build/win32/.gitignore0000664000175000017500000000005712641367670022046 0ustar jaakkojaakkovcconfig.bat envconfig.bat *.pk3 *.dll obj bin doomsday-stable-1.15.7/doomsday/build/build.pro0000664000175000017500000000664612641367670020747 0ustar jaakkojaakko# The Doomsday Engine Project # Copyright (c) 2011-2013 Jaakko Keränen # Copyright (c) 2011-2013 Daniel Swanson # This project file contains tasks that are done in the beginning of a build. TEMPLATE = subdirs # Let's print the build configuration during this qmake invocation. CONFIG += deng_verbosebuildconfig include(../macros.pri) # Always update versions.pri. runPython2InDir($$PWD/.., configure.py) include(../config.pri) # We are not building any binaries here; disable stripping. QMAKE_STRIP = true # Update the PK3 files. !deng_sdk:!deng_nopackres { runPython2InDir($$PWD/scripts/, packres.py --quiet \"$$OUT_PWD/..\") } # Create the output directories on Windows. win32 { mkpath($$DENG_WIN_PRODUCTS_DIR) mkpath($$DENG_BIN_DIR) mkpath($$DENG_LIB_DIR) mkpath($$DENG_PLUGIN_LIB_DIR) } # Install the launcher. !deng_sdk:deng_snowberry { SB_ROOT = ../../snowberry SB_DIR = $$DENG_BASE_DIR/snowberry sb.files = \ $${SB_ROOT}/cfparser.py \ $${SB_ROOT}/events.py \ $${SB_ROOT}/host.py \ $${SB_ROOT}/language.py \ $${SB_ROOT}/logger.py \ $${SB_ROOT}/paths.py \ $${SB_ROOT}/plugins.py \ $${SB_ROOT}/snowberry.py \ $${SB_ROOT}/ui.py \ $${SB_ROOT}/widgets.py \ $${SB_ROOT}/graphics \ $${SB_ROOT}/lang \ $${SB_ROOT}/profiles \ $${SB_ROOT}/sb sb.path = $$SB_DIR conf.files = \ $${SB_ROOT}/conf/snowberry.conf \ $${SB_ROOT}/conf/x-*.conf conf.path = $$SB_DIR/conf plugins.files = \ $${SB_ROOT}/plugins/about.py \ $${SB_ROOT}/plugins/help.py \ $${SB_ROOT}/plugins/launcher.py \ $${SB_ROOT}/plugins/observer.py \ $${SB_ROOT}/plugins/preferences.py \ $${SB_ROOT}/plugins/profilelist.py \ $${SB_ROOT}/plugins/tab* \ $${SB_ROOT}/plugins/wizard.py plugins.path = $$SB_DIR/plugins # Make may not have yet created the output directory at this point. system(mkdir -p \"$$OUT_PWD\") isEmpty(SCRIPT_PYTHON) { error("Variable SCRIPT_PYTHON not set (path of Python interpreter to be used in generated scripts)") } # Generate a script for starting the laucher. LAUNCH_FILE = launch-doomsday !system(sed \"s:PYTHON:$$SCRIPT_PYTHON:; s:SB_DIR:$$SB_DIR:\" \ <\"../../distrib/linux/$$LAUNCH_FILE\" \ >\"$$OUT_PWD/$$LAUNCH_FILE\" && \ chmod 755 \"$$OUT_PWD/$$LAUNCH_FILE\"): error(Can\'t build $$LAUNCH_FILE) launch.files = $$OUT_PWD/$$LAUNCH_FILE launch.path = $$DENG_BIN_DIR # Generate a .desktop file for the applications menu. DESKTOP_FILE = doomsday-engine.desktop !system(sed \"s:BIN_DIR:$$DENG_BIN_DIR:; s:SB_DIR:$$SB_DIR:\" \ <\"../../distrib/linux/$$DESKTOP_FILE\" \ >\"$$OUT_PWD/$$DESKTOP_FILE\"): error(Can\'t build $$DESKTOP_FILE) desktop.files = $$OUT_PWD/$$DESKTOP_FILE desktop.path = $$PREFIX/share/applications INSTALLS += conf plugins sb launch desktop } deng_aptunstable { # Include the Unstable repository for apt. #INSTALLS += repo #repo.files += ../../distrib/linux/doomsday-builds-unstable.list #repo.path += /etc/apt/sources.list.d } deng_aptstable { # Include the Stable repository for apt. #INSTALLS += repo #repo.files += ../../distrib/linux/doomsday-builds-stable.list #repo.path += /etc/apt/sources.list.d } OTHER_FILES += ../configure.py doomsday-stable-1.15.7/doomsday/dep_core.pri0000664000175000017500000000627412641367670020320 0ustar jaakkojaakkoinclude(dep_core_cwrapper.pri) # libcore's C++ API requires the following Qt modules. QT += core network # Optional modules: deng_qtopengl: QT += opengl deng_qtgui { QT += gui greaterThan(QT_MAJOR_VERSION, 4) { QT += widgets CONFIG += deng_qtwidgets } } win32: defineReplace(qtLibraryFile) { greaterThan(QT_MAJOR_VERSION, 4) { deng_debug: return(Qt$${QT_MAJOR_VERSION}$${1}d.dll) else: return(Qt$${QT_MAJOR_VERSION}$${1}.dll) } else { deng_debug: return(Qt$${1}$${QT_MAJOR_VERSION}d.dll) else: return(Qt$${1}$${QT_MAJOR_VERSION}.dll) } } deng_debug: qtver = d$$QT_MAJOR_VERSION else: qtver = $$QT_MAJOR_VERSION win32:!greaterThan(QT_MAJOR_VERSION, 4) { # Install the required Qt DLLs into the products dir. INSTALLS *= qtlibs qtplugins qtlibs.files += \ $$[QT_INSTALL_BINS]/$$qtLibraryFile(Core) \ $$[QT_INSTALL_BINS]/$$qtLibraryFile(Network) deng_qtgui: qtlibs.files += $$[QT_INSTALL_BINS]/$$qtLibraryFile(Gui) deng_qtwidgets: qtlibs.files += $$[QT_INSTALL_BINS]/$$qtLibraryFile(Widgets) deng_qtopengl: qtlibs.files += $$[QT_INSTALL_BINS]/$$qtLibraryFile(OpenGL) qtlibs.path = $$DENG_LIB_DIR qtplugins.path = $$DENG_LIB_DIR/imageformats greaterThan(QT_MAJOR_VERSION, 4) { qtplugins.files = \ $$[QT_INSTALL_PLUGINS]/imageformats/qjpeg.dll \ $$[QT_INSTALL_PLUGINS]/imageformats/qico.dll \ $$[QT_INSTALL_PLUGINS]/imageformats/qgif.dll # International Components for Unicode (not system-provided on Windows) INSTALLS *= qtdeps qtdeps.files += \ $$[QT_INSTALL_BINS]/icuin52.dll \ $$[QT_INSTALL_BINS]/icuuc52.dll \ $$[QT_INSTALL_BINS]/icudt52.dll qtdeps.path = $$DENG_LIB_DIR } else { qtplugins.files = $$[QT_INSTALL_PLUGINS]/imageformats/qjpeg4.dll } } macx { defineTest(linkBinaryToBundledLibcore) { fixInstallName($$1, libdeng_core.2.dylib, ..) fixInstallName($$1, QtCore.framework/Versions/$$QT_MAJOR_VERSION/QtCore, ..) fixInstallName($$1, QtNetwork.framework/Versions/$$QT_MAJOR_VERSION/QtNetwork, ..) deng_qtgui: fixInstallName($$1, QtGui.framework/Versions/$$QT_MAJOR_VERSION/QtGui, ..) deng_qtwidgets: fixInstallName($$1, QtWidgets.framework/Versions/$$QT_MAJOR_VERSION/QtWidgets, ..) deng_qtopengl: fixInstallName($$1, QtOpenGL.framework/Versions/$$QT_MAJOR_VERSION/QtOpenGL, ..) } defineTest(linkToBundledLibcore) { linkBinaryToBundledLibcore($${1}.bundle/$$1) } defineTest(linkDylibToBundledLibcore) { linkBinaryToBundledLibcore($${1}.dylib) } defineTest(xcodeDeployDengLibs) { # 1 - list of lib names (excluding core, which is always included) *-xcode { macx_libs.files = $$DESTDIR/libdeng_core.2.dylib for(i, 1): macx_libs.files += $$DESTDIR/libdeng_$${i}.dylib macx_libs.path = Contents/Frameworks QMAKE_BUNDLE_DATA += macx_libs export(QMAKE_BUNDLE_DATA) export(macx_libs.files) export(macx_libs.path) } } } DENG_PACKAGES += net.dengine.stdlib.pack doomsday-stable-1.15.7/doomsday/doc/0000775000175000017500000000000012641367670016560 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/.tm_properties0000664000175000017500000000012012641367670021446 0ustar jaakkojaakko[ source.amethyst ] wrapColumn = 80 softWrap = true tabSize = 4 softTabs = true doomsday-stable-1.15.7/doomsday/doc/libdoom/0000775000175000017500000000000012641367670020205 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/libdoom/variable/0000775000175000017500000000000012641367670021772 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/libdoom/variable/hud-face-ouchfix.ame0000664000175000017500000000007412641367670025576 0ustar jaakkojaakko@summary{ 1=Fix HUD OUCH face (enables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-monsters-stuckindoors.ame0000664000175000017500000000012012641367670027755 0ustar jaakkojaakko@summary{ 1=Monsters can get stuck in doortracks (disables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/hud-cheat-counter.ame0000664000175000017500000000010712641367670025773 0ustar jaakkojaakko@summary{ 6-bit bitfield. Show kills, items and secret counters. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/hud-frags.ame0000664000175000017500000000006012641367670024332 0ustar jaakkojaakko@summary{ 1=Show deathmatch frags in HUD. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-anybossdeath666.ame0000664000175000017500000000013512641367670026312 0ustar jaakkojaakko@summary{ 1=The death of ANY boss monster triggers a 666 special (on applicable maps). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/server-game-respawn-monsters-nightmare.ame0000664000175000017500000000010612641367670032171 0ustar jaakkojaakko@summary{ 1=Monster respawning in Nightmare difficulty enabled. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-player-wallrun-northonly.ame0000664000175000017500000000011212641367670030367 0ustar jaakkojaakko@summary{ 1=Players can only wallrun North (disables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-objects-hangoverledges.ame0000664000175000017500000000012612641367670030010 0ustar jaakkojaakko@summary{ 1=Only some objects can hang over tall ledges (enables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-autoswitch-berserk.ame0000664000175000017500000000011212641367670027567 0ustar jaakkojaakko@summary{ Change to fist automatically when picking up berserk pack } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-objects-clipping.ame0000664000175000017500000000011412641367670026615 0ustar jaakkojaakko@summary{ 1=Use EXACTLY DOOM's clipping code (disables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-order8.ame0000664000175000017500000000005512641367670026442 0ustar jaakkojaakko@summary{ Weapon change order, slot 8. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-pistol.ame0000664000175000017500000000004512641367670026550 0ustar jaakkojaakko@summary{ 1=Player has pistol. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-ammo-shells.ame0000664000175000017500000000005212641367670026166 0ustar jaakkojaakko@summary{ Current number of shells. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-ammo-missiles.ame0000664000175000017500000000005412641367670026526 0ustar jaakkojaakko@summary{ Current number of missiles. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-plasmarifle.ame0000664000175000017500000000005312641367670027534 0ustar jaakkojaakko@summary{ 1=Player has plasma rifle. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-objects-gibcrushednonbleeders.ame0000664000175000017500000000012712641367670031354 0ustar jaakkojaakko@summary{ 1=Turn any crushed object into a pile of gibs (disables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/server-game-coop-respawn-items.ame0000664000175000017500000000006012641367670030421 0ustar jaakkojaakko@summary{ 1=Respawn items in co-op games. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/server-game-bfg-freeaim.ame0000664000175000017500000000006712641367670027040 0ustar jaakkojaakko@summary{ Allow free-aim with BFG in deathmatch. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-save-auto-loadonreborn.ame0000664000175000017500000000011412641367670027747 0ustar jaakkojaakko@summary{ 1=Load the auto save slot on player reborn. (default: off). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-vilechase-usevileradius.ame0000664000175000017500000000015312641367670030211 0ustar jaakkojaakko@summary{ 1=Always use the radius of the Archvile with the VileChase action (disables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-fist.ame0000664000175000017500000000004312641367670026201 0ustar jaakkojaakko@summary{ 1=Player has fist. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/hud-keys-combine.ame0000664000175000017500000000011012641367670025611 0ustar jaakkojaakko@summary{ 1=Combine the display of keys of the same color in HUD. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-key-redskull.ame0000664000175000017500000000005312641367670026361 0ustar jaakkojaakko@summary{ 1=Player has red skullkey. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-death-lookup.ame0000664000175000017500000000004612641367670026344 0ustar jaakkojaakko@summary{ 1=Look up when killed } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/xg-dev.ame0000664000175000017500000000005312641367670023646 0ustar jaakkojaakko@summary{ 1=Print XG debug messages. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-order6.ame0000664000175000017500000000005512641367670026440 0ustar jaakkojaakko@summary{ Weapon change order, slot 6. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-corpse-time.ame0000664000175000017500000000007312641367670025614 0ustar jaakkojaakko@summary{ Corpse vanish time in seconds, 0=disabled. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-mlauncher.ame0000664000175000017500000000005712641367670027217 0ustar jaakkojaakko@summary{ 1=Player has missile launcher. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-order5.ame0000664000175000017500000000005512641367670026437 0ustar jaakkojaakko@summary{ Weapon change order, slot 5. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-ammo-bullets.ame0000664000175000017500000000005312641367670026347 0ustar jaakkojaakko@summary{ Current number of bullets. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-objects-falloff.ame0000664000175000017500000000011612641367670026423 0ustar jaakkojaakko@summary{ 1=Objects fall under their own weight (enables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/hud-keys.ame0000664000175000017500000000004412641367670024205 0ustar jaakkojaakko@summary{ 1=Show keys in HUD. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-corpse-sliding.ame0000664000175000017500000000013212641367670026303 0ustar jaakkojaakko@summary{ 1=Corpses slide down stairs and ledges (enables enhanced BOOM behaviour). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-order4.ame0000664000175000017500000000005512641367670026436 0ustar jaakkojaakko@summary{ Weapon change order, slot 4. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-chainsaw.ame0000664000175000017500000000004712641367670027035 0ustar jaakkojaakko@summary{ 1=Player has chainsaw. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-order7.ame0000664000175000017500000000005512641367670026441 0ustar jaakkojaakko@summary{ Weapon change order, slot 7. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-zclip.ame0000664000175000017500000000012412641367670024503 0ustar jaakkojaakko@summary{ 1=Allow mobjs to move under/over each other (enables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/hud-ammo.ame0000664000175000017500000000004412641367670024163 0ustar jaakkojaakko@summary{ 1=Show ammo in HUD. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/hud-health.ame0000664000175000017500000000004612641367670024501 0ustar jaakkojaakko@summary{ 1=Show health in HUD. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-key-blueskull.ame0000664000175000017500000000005412641367670026537 0ustar jaakkojaakko@summary{ 1=Player has blue skullkey. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-key-yellow.ame0000664000175000017500000000005512641367670026051 0ustar jaakkojaakko@summary{ 1=Player has yellow keycard. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-chaingun.ame0000664000175000017500000000004712641367670027034 0ustar jaakkojaakko@summary{ 1=Player has chaingun. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/view-bob-weapon-switch-lower.ame0000664000175000017500000000007412641367670030105 0ustar jaakkojaakko@summary{ HUD weapon lowered during weapon switching. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/server-game-deathmatch-killmsg.ame0000664000175000017500000000006012641367670030427 0ustar jaakkojaakko@summary{ 1=Announce frags in deathmatch. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/server-game-coop-noweapons.ame0000664000175000017500000000010212641367670027631 0ustar jaakkojaakko@summary{ 1=Disable multiplayer weapons during co-op games. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/menu-quitsound.ame0000664000175000017500000000006712641367670025456 0ustar jaakkojaakko@summary{ 1=Play a sound when quitting the game. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/server-game-coop-nodamage.ame0000664000175000017500000000007712641367670027406 0ustar jaakkojaakko@summary{ 1=Disable player-player damage in co-op games. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-bfg.ame0000664000175000017500000000004212641367670025771 0ustar jaakkojaakko@summary{ 1=Player has BFG. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/hud-cheat-counter-show-mapopen.ame0000664000175000017500000000011212641367670030402 0ustar jaakkojaakko@summary{ 1=Only show the cheat counters while the automap is open. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-key-red.ame0000664000175000017500000000005212641367670025305 0ustar jaakkojaakko@summary{ 1=Player has red keycard. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-shotgun.ame0000664000175000017500000000004612641367670026726 0ustar jaakkojaakko@summary{ 1=Player has shotgun. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-zombiescanexit.ame0000664000175000017500000000011012641367670026401 0ustar jaakkojaakko@summary{ 1=Zombie players can exit maps (disables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-eyeheight.ame0000664000175000017500000000006712641367670025726 0ustar jaakkojaakko@summary{ Player eye height. The original is 41. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/server-game-coop-nothing.ame0000664000175000017500000000010212641367670027266 0ustar jaakkojaakko@summary{ 1=Disable all multiplayer objects in co-op games. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-key-yellowskull.ame0000664000175000017500000000005612641367670027125 0ustar jaakkojaakko@summary{ 1=Player has yellow skullkey. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-raiseghosts.ame0000664000175000017500000000013012641367670025712 0ustar jaakkojaakko@summary{ 1=Archviles raise ghosts from squished corpses (disables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-stats-items.ame0000664000175000017500000000005112641367670025636 0ustar jaakkojaakko@summary{ Current number of items. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/server-game-announce-secret.ame0000664000175000017500000000007212641367670027761 0ustar jaakkojaakko@summary{ 1=Announce the discovery of secret areas. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/hud-cheat-counter-scale.ame0000664000175000017500000000005612641367670027063 0ustar jaakkojaakko@summary{ Size factor for the counters. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-maxskulls.ame0000664000175000017500000000015112641367670025405 0ustar jaakkojaakko@summary{ 1=Pain Elementals can't spawn Lost Souls if more than twenty exist (original behaviour). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-stats-secrets.ame0000664000175000017500000000006612641367670026173 0ustar jaakkojaakko@summary{ Current number of discovered secrets. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-color.ame0000664000175000017500000000007712641367670025072 0ustar jaakkojaakko@summary{ Player color: 0=green, 1=gray, 2=brown, 3=red. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-skullsinwalls.ame0000664000175000017500000000013512641367670026273 0ustar jaakkojaakko@summary{ 1=Pain Elementals can spawn Lost Souls inside walls (disables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/hud-armor.ame0000664000175000017500000000004512641367670024353 0ustar jaakkojaakko@summary{ 1=Show armor in HUD. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/hud-face.ame0000664000175000017500000000005712641367670024134 0ustar jaakkojaakko@summary{ 1=Show Doom guy's face in HUD. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-sshotgun.ame0000664000175000017500000000005412641367670027110 0ustar jaakkojaakko@summary{ 1=Player has super shotgun. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-air-movement.ame0000664000175000017500000000006612641367670026355 0ustar jaakkojaakko@summary{ Player movement speed while airborne. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-key-blue.ame0000664000175000017500000000005312641367670025463 0ustar jaakkojaakko@summary{ 1=Player has blue keycard. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/server-game-respawn.ame0000664000175000017500000000004512641367670026347 0ustar jaakkojaakko@summary{ 1=-respawn was used. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/server-game-nobfg.ame0000664000175000017500000000006312641367670025763 0ustar jaakkojaakko@summary{ 1=Disable BFG9000 in all netgames. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/game-stats-kills.ame0000664000175000017500000000005112641367670025633 0ustar jaakkojaakko@summary{ Current number of kills. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-weapon-nextmode.ame0000664000175000017500000000010512641367670027056 0ustar jaakkojaakko@summary{ 1=Use custom weapon order with Next/Previous weapon. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/hud-status-weaponslots-ownedfix.ame0000664000175000017500000000013612641367670030754 0ustar jaakkojaakko@summary{ 1=Fix original DOOM behavior when drawing the statusbar owned weapon display. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/player-ammo-cells.ame0000664000175000017500000000005112641367670025775 0ustar jaakkojaakko@summary{ Current number of cells. } doomsday-stable-1.15.7/doomsday/doc/libdoom/variable/server-game-noteamdamage.ame0000664000175000017500000000007512641367670027315 0ustar jaakkojaakko@summary{ 1=Disable team damage (player color = team). } doomsday-stable-1.15.7/doomsday/doc/libdoom/command/0000775000175000017500000000000012641367670021623 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/libdoom/command/setclass.ame0000664000175000017500000000004212641367670024124 0ustar jaakkojaakko@summary{ Set player class. } doomsday-stable-1.15.7/doomsday/doc/libdoom/command/cheat.ame0000664000175000017500000000022112641367670023366 0ustar jaakkojaakko@summary{ Issue a cheat code using the original DOOM cheats. } @description{ Params: cheat (cheat) @cbr For example, 'cheat idclev25'. } doomsday-stable-1.15.7/doomsday/doc/libdoom/command/spy.ame0000664000175000017500000000006712641367670023125 0ustar jaakkojaakko@summary{ Spy mode: cycle player views in co-op. } doomsday-stable-1.15.7/doomsday/doc/libdoom/command/give.ame0000664000175000017500000000007112641367670023237 0ustar jaakkojaakko@summary{ Gives you weapons, ammo, power-ups, etc. } doomsday-stable-1.15.7/doomsday/doc/libdoom/command/zoommax.ame0000664000175000017500000000006412641367670024001 0ustar jaakkojaakko@summary{ Zoom out to the max in the automap. } doomsday-stable-1.15.7/doomsday/doc/libdoom/command/god.ame0000664000175000017500000000004212641367670023054 0ustar jaakkojaakko@summary{ God mode (cheat). } doomsday-stable-1.15.7/doomsday/doc/author.ame0000664000175000017500000000030312641367670020542 0ustar jaakkojaakko$ Author chapter for the Unix man page. @chapter{ Author } This documentation has been written by Jaakko Keränen @mailto{jaakko.keranen@@iki.fi} and Daniel Swanson @mailto{danij@@dengine.net}. doomsday-stable-1.15.7/doomsday/doc/libhexen/0000775000175000017500000000000012641367670020356 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/libhexen/variable/0000775000175000017500000000000012641367670022143 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-key-horn.ame0000664000175000017500000000004712641367670025656 0ustar jaakkojaakko@summary{ 1=Player has horn key. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-sapphire2.ame0000664000175000017500000000006012641367670027605 0ustar jaakkojaakko@summary{ 1=Player has Sapphire Planet 2. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-mana-green.ame0000664000175000017500000000005712641367670026135 0ustar jaakkojaakko@summary{ Current ammount of green mana. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-weapon-piece2.ame0000664000175000017500000000004612641367670026557 0ustar jaakkojaakko@summary{ 1=Player has piece 2. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/game-maulator-time.ame0000664000175000017500000000010112641367670026306 0ustar jaakkojaakko@summary{ Dark Servant lifetime, in seconds (default: 25). } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-weapon-second.ame0000664000175000017500000000005412641367670026662 0ustar jaakkojaakko@summary{ 1=Player has second weapon. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-ruby.ame0000664000175000017500000000005212641367670026672 0ustar jaakkojaakko@summary{ 1=Player has Ruby Planet. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/ctl-inventory-mode.ame0000664000175000017500000000007512641367670026370 0ustar jaakkojaakko@summary{ Inventory selection mode 0=cursor, 1=scroll. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/hud-mana.ame0000664000175000017500000000012012641367670024312 0ustar jaakkojaakko@summary{ Show mana when the status bar is hidden. 1=top, 2=bottom, 0=off } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-gear3.ame0000664000175000017500000000005312641367670026713 0ustar jaakkojaakko@summary{ 1=Player has Clock Gear 3. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-gear1.ame0000664000175000017500000000005312641367670026711 0ustar jaakkojaakko@summary{ 1=Player has Clock Gear 1. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-flechette.ame0000664000175000017500000000005612641367670027660 0ustar jaakkojaakko@summary{ Current number of Flechettes. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-might.ame0000664000175000017500000000006412641367670027024 0ustar jaakkojaakko@summary{ Current number of Kraters Of Might. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-key-rusted.ame0000664000175000017500000000005112641367670026211 0ustar jaakkojaakko@summary{ 1=Player has rusted key. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-weapon-allpieces.ame0000664000175000017500000000005112641367670027345 0ustar jaakkojaakko@summary{ 1=Player has all pieces. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-weapon-fourth.ame0000664000175000017500000000005412641367670026716 0ustar jaakkojaakko@summary{ 1=Player has fourth weapon. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-gear2.ame0000664000175000017500000000005312641367670026712 0ustar jaakkojaakko@summary{ 1=Player has Clock Gear 2. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-weapon-first.ame0000664000175000017500000000005312641367670026535 0ustar jaakkojaakko@summary{ 1=Player has first weapon. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/hud-inventory-slot-max.ame0000664000175000017500000000012212641367670027177 0ustar jaakkojaakko@summary{ Maximum number of inventory slots to display in full-screen mode. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/game-icecorpse.ame0000664000175000017500000000005712641367670025514 0ustar jaakkojaakko@summary{ 1=Translucent frozen monsters. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-key-dungeon.ame0000664000175000017500000000005212641367670026343 0ustar jaakkojaakko@summary{ 1=Player has dungeon key. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-speed.ame0000664000175000017500000000006212641367670027012 0ustar jaakkojaakko@summary{ Current number of Boots of Speed. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-darkservant.ame0000664000175000017500000000007212641367670030237 0ustar jaakkojaakko@summary{ Current number of Dark Servant artifacts. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-emerald2.ame0000664000175000017500000000005712641367670027411 0ustar jaakkojaakko@summary{ 1=Player has Emerald Planet 2. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-daemoncodex.ame0000664000175000017500000000005312641367670030200 0ustar jaakkojaakko@summary{ 1=Player has Daemon Codex. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-mysticambit.ame0000664000175000017500000000007512641367670030243 0ustar jaakkojaakko@summary{ Current number of Mystic Ambit Incantations. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-defender.ame0000664000175000017500000000007112641367670027466 0ustar jaakkojaakko@summary{ Current number of Icons Of The Defender. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/ctl-inventory-use-next.ame0000664000175000017500000000011612641367670027210 0ustar jaakkojaakko@summary{ 1=Automatically select the next inventory item when unusable. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-key-fire.ame0000664000175000017500000000004712641367670025635 0ustar jaakkojaakko@summary{ 1=Player has fire key. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-key-castle.ame0000664000175000017500000000005112641367670026156 0ustar jaakkojaakko@summary{ 1=Player has castle key. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-key-steel.ame0000664000175000017500000000005012641367670026016 0ustar jaakkojaakko@summary{ 1=Player has steel key. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-key-axe.ame0000664000175000017500000000004612641367670025464 0ustar jaakkojaakko@summary{ 1=Player has axe key. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-wings.ame0000664000175000017500000000007412641367670027044 0ustar jaakkojaakko@summary{ Current number of Wings of Wrath artifacts. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-flamemask.ame0000664000175000017500000000005112641367670027650 0ustar jaakkojaakko@summary{ 1=Player has Flame Mask. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/hud-health.ame0000664000175000017500000000007312641367670024652 0ustar jaakkojaakko@summary{ Show health when the status bar is hidden. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-weapon-piece1.ame0000664000175000017500000000004612641367670026556 0ustar jaakkojaakko@summary{ 1=Player has piece 1. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-gear4.ame0000664000175000017500000000005312641367670026714 0ustar jaakkojaakko@summary{ 1=Player has Clock Gear 4. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-bracers.ame0000664000175000017500000000006612641367670027337 0ustar jaakkojaakko@summary{ Current number of Dragonskin Bracers. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-mana-blue.ame0000664000175000017500000000005512641367670025762 0ustar jaakkojaakko@summary{ Current amount of blue mana. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/msg-hub-override.ame0000664000175000017500000000006512641367670026007 0ustar jaakkojaakko@summary{ Override the transition hub message. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-repulsion.ame0000664000175000017500000000006612641367670027736 0ustar jaakkojaakko@summary{ Current number of Discs Of Repulsion. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-skull.ame0000664000175000017500000000005512641367670027046 0ustar jaakkojaakko@summary{ 1=Player has Yorick's Skull. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-weapon-piece3.ame0000664000175000017500000000004612641367670026560 0ustar jaakkojaakko@summary{ 1=Player has piece 3. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-chaosdevice.ame0000664000175000017500000000006112641367670030166 0ustar jaakkojaakko@summary{ Current number of Chaos Devices. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-key-swamp.ame0000664000175000017500000000005012641367670026031 0ustar jaakkojaakko@summary{ 1=Player has swamp key. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-eyeheight.ame0000664000175000017500000000007012641367670026071 0ustar jaakkojaakko@summary{ Player eye height (the original is 41). } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-liberoscura.ame0000664000175000017500000000005312641367670030224 0ustar jaakkojaakko@summary{ 1=Player has Liber Oscura. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-emerald1.ame0000664000175000017500000000005712641367670027410 0ustar jaakkojaakko@summary{ 1=Player has Emerald Planet 1. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/hud-inventory-timer.ame0000664000175000017500000000007112641367670026556 0ustar jaakkojaakko@summary{ Seconds before the inventory auto-hides. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-sapphire1.ame0000664000175000017500000000006012641367670027604 0ustar jaakkojaakko@summary{ 1=Player has Sapphire Planet 1. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-heart.ame0000664000175000017500000000006012641367670027013 0ustar jaakkojaakko@summary{ 1=Player has Heart Of D'Sparil. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-torch.ame0000664000175000017500000000005312641367670027031 0ustar jaakkojaakko@summary{ Current number of torches. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-holyrelic.ame0000664000175000017500000000005112641367670027702 0ustar jaakkojaakko@summary{ 1=Player has Holy Relic. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-banishment.ame0000664000175000017500000000006612641367670030046 0ustar jaakkojaakko@summary{ Current number of Banishment Devices. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-glaiveseal.ame0000664000175000017500000000005212641367670030025 0ustar jaakkojaakko@summary{ 1=Player has Glaive Seal. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/hud-unhide-pickup-invitem.ame0000664000175000017500000000011112641367670027614 0ustar jaakkojaakko@summary{ 1=Unhide the HUD when player collects an inventory item. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-mysticurn.ame0000664000175000017500000000007012641367670027746 0ustar jaakkojaakko@summary{ Current number of Mystic Urn artifacts. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-porkalator.ame0000664000175000017500000000006712641367670030075 0ustar jaakkojaakko@summary{ Current number of Porkalaor artifacts. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-sigilmagus.ame0000664000175000017500000000006112641367670030055 0ustar jaakkojaakko@summary{ 1=Player has Sigil of the Magus. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/ctl-inventory-wrap.ame0000664000175000017500000000006412641367670026413 0ustar jaakkojaakko@summary{ 1=Inventory selection wraps around. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-color.ame0000664000175000017500000000016112641367670025235 0ustar jaakkojaakko@summary{ Player color: 0=blue, 1=red, 2=yellow, 3=green, 4=jade, 5=white, @cbr 6=hazel, 7=purple, 8=auto. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-key-emerald.ame0000664000175000017500000000005212641367670026315 0ustar jaakkojaakko@summary{ 1=Player has emerald key. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-class.ame0000664000175000017500000000006312641367670025225 0ustar jaakkojaakko@summary{ Player class in multiplayer games. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-air-movement.ame0000664000175000017500000000010512641367670026520 0ustar jaakkojaakko@summary{ Player movement speed while airborne and NOT flying. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/hud-currentitem.ame0000664000175000017500000000007112641367670025744 0ustar jaakkojaakko@summary{ 1=Show current item in full-screen mode. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-key-silver.ame0000664000175000017500000000005112641367670026207 0ustar jaakkojaakko@summary{ 1=Player has silver key. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/hud-inventory-slot-showempty.ame0000664000175000017500000000010212641367670030447 0ustar jaakkojaakko@summary{ 1=Show empty inventory slots in full-screen mode. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/ctl-inventory-use-immediate.ame0000664000175000017500000000007412641367670030173 0ustar jaakkojaakko@summary{ 1=Use items immediately from the inventory. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-weapon-nextmode.ame0000664000175000017500000000010512641367670027227 0ustar jaakkojaakko@summary{ 1=Use custom weapon order with Next/Previous weapon. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-artifact-quartzflask.ame0000664000175000017500000000006112641367670030260 0ustar jaakkojaakko@summary{ Current number of Quartz Flasks. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/server-game-randclass.ame0000664000175000017500000000007212641367670027013 0ustar jaakkojaakko@summary{ 1=Respawn in a random class (deathmatch). } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-key-cave.ame0000664000175000017500000000004712641367670025626 0ustar jaakkojaakko@summary{ 1=Player has cave key. } doomsday-stable-1.15.7/doomsday/doc/libhexen/variable/player-weapon-third.ame0000664000175000017500000000005312641367670026520 0ustar jaakkojaakko@summary{ 1=Player has third weapon. } doomsday-stable-1.15.7/doomsday/doc/libhexen/command/0000775000175000017500000000000012641367670021774 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/libhexen/command/setclass.ame0000664000175000017500000000011612641367670024277 0ustar jaakkojaakko@summary{ Set player class. } @description{ Params: setclass (0-2). } doomsday-stable-1.15.7/doomsday/doc/libhexen/command/runscript.ame0000664000175000017500000000012112641367670024503 0ustar jaakkojaakko@summary{ Run an ACS script. } @description{ Params: runscript (1-99). } doomsday-stable-1.15.7/doomsday/doc/libhexen/command/scriptinfo.ame0000664000175000017500000000011512641367670024635 0ustar jaakkojaakko@summary{ Show information about all scripts or one particular script. } doomsday-stable-1.15.7/doomsday/doc/libhexen/command/cheat.ame0000664000175000017500000000021712641367670023544 0ustar jaakkojaakko@summary{ Issue a cheat code using the original Hexen cheats. } @description{ Params: cheat (cheat) @cbr For example, 'cheat satan'. } doomsday-stable-1.15.7/doomsday/doc/libhexen/command/spy.ame0000664000175000017500000000007612641367670023276 0ustar jaakkojaakko@summary{ Change the viewplayer when not in deathmatch. } doomsday-stable-1.15.7/doomsday/doc/libhexen/command/give.ame0000664000175000017500000000110312641367670023405 0ustar jaakkojaakko@summary{ Cheat command to give you various kinds of things. } @description{ @params{ give (stuff) @cbr (stuff) consists of one or more of (type:id). If no id, all is given: @deflist{ @item{a} ammo @item{h} health @item{i} items @item{k} keys @item{p} puzzle @item{r} armor @item{w} weapons } } @cbr @example{ @code{give ikw} gives items, keys and weapons. @cbr @code{give w2k1} gives weapon two and key one. } } doomsday-stable-1.15.7/doomsday/doc/libhexen/command/demomode.ame0000664000175000017500000000005712641367670024253 0ustar jaakkojaakko@summary{ Set demo external camera mode. } doomsday-stable-1.15.7/doomsday/doc/libhexen/command/stopfinale.ame0000664000175000017500000000007512641367670024626 0ustar jaakkojaakko@summary{ Stop the currently playing interlude/finale. } doomsday-stable-1.15.7/doomsday/doc/libhexen/command/pig.ame0000664000175000017500000000006412641367670023237 0ustar jaakkojaakko@summary{ Turn yourself into a pig. Go ahead. } doomsday-stable-1.15.7/doomsday/doc/libhexen/command/class.ame0000664000175000017500000000012612641367670023564 0ustar jaakkojaakko@summary{ Change player class. } @description{ Params: class (classnumber). } doomsday-stable-1.15.7/doomsday/doc/libhexen/command/god.ame0000664000175000017500000000006312641367670023230 0ustar jaakkojaakko@summary{ Toggle God mode (invulnerability). } doomsday-stable-1.15.7/doomsday/doc/engine/0000775000175000017500000000000012641367670020025 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/engine/controlpanel.ame0000664000175000017500000002705412641367670023221 0ustar jaakkojaakko@require{amestd} @begin # Control Panel help strings @part{Video} @chapter{Gamma correction} @cvar{vid-gamma} @default{1.0} @summary{ Smaller values result in a darker picture. In order for this setting to work your display adapter must support color adjustments. You should lower the gamma correction value until dark areas in maps become sufficiently dark. Note that with some monitors different resolutions require different gamma correction levels. @cbreak Use the -noramp option to disable all color adjustments. } @chapter{Display contrast} @cvar{vid-contrast} @default{1.0} @summary{ Modifies the steepness of the gamma ramp. Values greater than 1.0 result in a more colorful picture but since the colors are clamped, some are lost in both the bright and dark ends of the ramp. Values less than 1.0 fade the picture towards gray. In order for this setting to work your display adapter must support color adjustments. @cbreak Use the -noramp option to disable all color adjustments. } @chapter{Display brightness} @cvar{vid-bright} @default{0.0} @summary{ Applies an offset to the gamma ramp. Positive values make the picture lighter, negative ones darker. For the best results, use a small positive brightness value with dark gamma and slightly increased contrast. In order for this setting to work your display adapter must support color adjustments. @cbreak Use the -noramp option to disable all color adjustments. } @part{Audio} @chapter{Preferred music source} @cvar{music-source} @default{External files} @summary{ There can be a WAD lump, external file and a CD track associated with each song. This setting controls which of these resources is actually played when the song is started. If the selected resource is not available, one that is will be chosen instead. } @chapter{16-bit sound effects} @cvar{sound-16bit} @default{No} @summary{ When this setting is activated, 8-bit sounds are converted to 16-bit before playing. In order for this to be effective you must also choose a sample rate higher than 11025 Hz. } @chapter{Sound effects sample rate} @cvar{sound-rate} @default{11025 Hz} @summary{ The minimum sample rate for all sound effects. Samples with a smaller rate will be resampled to this rate before playing. To increase the quality of the resampling, activate the 16-bit sound effects option. } @chapter{Show status of channels} @cvar{sound-info} @default{No} @summary{ The status of all sound channels is drawn on screen. This is intended for debugging. The displayed information consists of sample cache size, channel flags, volume, frequency, latest start time, cursor positions, buffer flags and sample information. @cbreak O = Origin @cbr A = Attenuated @cbr E = Emitter @cbr 3 = 3D @cbr P = Playing @cbr R = Repeat @cbr L = Reload } @part{Graphics} @chapter{Field Of View angle} @cvar{rend-camera-fov} @default{90} @summary{ Adjust the camera's Field Of View angle according to your monitor configuration. If you have a large monitor and view it close by, using a larger FOV angle (around 100 degrees) will result in a more natural game view. With small monitors it's best to stick to 90 degrees or else the fish eye effect may become distracting. } @chapter{Sky sphere radius} @cvar{rend-sky-distance} @default{1600} @summary{ The sky is composed of two capped hemispheres rendered around the camera location. The sky sphere radius is the radius of these hemispheres. The primary purpose of this setting is to determine how large an impact fog has on the sky. Larger radii will result in a sky with heavier fog. } @chapter{Shadow visible distance} @cvar{rend-shadow-far} @default{1000} @summary{ Maximum distance at which shadows under objects are visible. If the object is farther than this, no shadow will be drawn. Since the shadows are extremely simple, posing little fear of performance loss, it's safe to increase this value as needed. } @part{Graphics: Lights} @chapter{Blending mode} @cvar{rend-light-blend} @default{Multiply} @summary{ The method used to draw dynamic lights. In the Multiply mode, color from dynamic lights is multiplied with the underlying surface pixels. This allows dark areas to be correctly lit up by the lights. The Add mode is faster but doesn't look as realistic. @cbreak The third mode, Process wo/rendering, is for debugging. In it, all dynamic light polygons are calculated but none are rendered. } @chapter{Dynamic light radius factor} @cvar{rend-light-radius-scale} @default{3} @summary{ The radius of all dynamic lights is multiplied by this factor. Lights are never larger than the Maximum dynamic light radius, though. } @chapter{Maximum dynamic light radius} @cvar{rend-light-radius-max} @default{256} @summary{ The maximum radius a dynamic light can have. This is just a limit: to make the lights larger, you also need to increase the Dynamic light radius factor. } @chapter{Light range compression} @cvar{rend-light-compression} @default{0} @summary{ Sector light range compression. Positive values will brighten dark areas while negative values will darken light areas of the map. } @chapter{Floor/ceiling glow on walls} @cvar{rend-glow-wall} @default{Yes} @summary{ Glowing floor and ceiling textures will cast a dynamic light on surrounding walls. } @part{Graphics: Halos} @chapter{Number of flares per halo} @cvar{rend-halo} @default{5} @summary{ Maximum number of lens flares originating from a luminous object. To disable lens flares, set this to None (zero). } @chapter{Use realistic halos} @cvar{rend-halo-realistic} @default{Yes} @summary{ When enabled, halos are rendered in a more true-to-life way. This also means that secondary lens flares are disabled. } @chapter{Occlusion fade speed} @cvar{rend-halo-occlusion} @default{48} @summary{ The rate at which a halo changes state from "visible" to "occluded" and vice versa. A small fade speed results in halos that appear and disappear softly, but are visible through walls for a short while before disappearing. } @chapter{Flare visibility limitation} @cvar{rend-halo-secondary-limit} @default{1} @summary{ Controls when secondary lens flares become visible. They are only visible when the light source is close enough to the camera. The larger the number, the farther away secondary flares are visible. } @chapter{Z magnification divisor} @cvar{rend-halo-zmag-div} @default{100} @summary{ The higher the number, the smaller lens flares become when the source is far away. Normally halos grow slightly larger as they move away from the camera. } @part{Graphics: Textures} @chapter{Texture quality} @cvar{rend-tex-quality} @default{8} @summary{ Depending on your system configuration, some textures must be resized so that their dimensions are powers of two. In these instances, this setting controls whether the higher or smaller matching power of two is chosen. At quality zero, all textures are reduced in size, to match the largest power of two smaller than the original size. At quality 8, the resizing is always done upwards. } @chapter{Smart texture filtering} @cvar{rend-tex-filter-smart} @default{No} @summary{ When enabled the hq2x texture filtering algorithm is used to enlarge all textures as opposed to linear scaling. } @chapter{Bilinear filtering} @summary{ Controls which class(es) of graphics receive bilinear filtering. Disabling bilinear filtering results in "pixelated" textures when up close. } @chapter{Anisotropic filtering} @cvar{rend-tex-filter-anisotropic} @default{Best} @summary{ When textures are drawn onto surfaces at high angles compared to the camera, bluring is introduced which reduces the overall quality of the visual. Anisotropic filtering can be used to help reduce this bluring. Sample multiplier: 0=Disabled, 1=2x, 2=4x, 3=8x, 4=16x. } @part{Graphics: Objects} @chapter{3D model visibility limit} @cvar{rend-model-distance} @default{1500} @summary{ Objects farther than this will never be rendered as 3D models. } @chapter{Align sprites to...} @cvar{rend-sprite-align} @default{Camera} @summary{ This setting controls which direction sprites face. Aligning to camera means the sprites are always upright (to make them appear more 3D) but they will turn to face the camera coordinates. Aligning to view plane resembles the way the sprites were drawn in software. The "limited" options restrict how much the sprite can tilt vertically. } @chapter{LOD level zero distance} @cvar{rend-model-lod} @default{256} @summary{ The distance where LOD level zero becomes LOD level one. Only affects 3D models that have Level Of Detail information (DMDs). } @part{Graphics: Particles} @chapter{Near diffusion factor} @cvar{rend-particle-diffuse} @default{4} @summary{ Particles may cause excessive overdraw if many are visible just in front of the camera. This setting makes large particles near the camera more transparent and ultimately invisible. The larger the diffuse factor, the smaller particles will be affected. The end result is that particles will not be visible near the camera. Increase the setting if framerate drops alarmingly when in smoke. } @chapter{Near clip distance} @cvar{rend-particle-visible-near} @default{Disabled} @summary{ Particles that are closer to the camera than this are not drawn at all. } @part{Network} @chapter{Cl-to-sv pos transmit tics} @cvar{client-pos-interval} @default{10} @summary{ The client periodically sends its coordinates to the server in order to keep it synchronized with the player position the client perceives as correct. The interval between the coordinate transmissions as a number of 35 Hz tics. Use a larger interval with low-bandwidth connections (can lead to more warping, though). } @chapter{Server login password} @cvar{server-password} @summary{ If a client wishes to make a remote console connection to the server it must specify this password. If the password is empty, clients can issue console commands on the server without specifying a password. Clients use the "login" and "logout" commands to begin and end a remote connection. } @chapter{Frame interval tics} @cvar{server-frame-interval} @default{1} @summary{ Number of 35 Hz ticks between frames sent to clients. Only for the server. Small intervals require more bandwidth but result in smoother animation and other world events. Use larger intervals (2..5) with low-bandwidth connections. } @part{Console} @chapter{Silent console variables} @cvar{con-var-silent} @default{No} @summary{ Normally when the value of a console variable is changed, its name and the new value get printed in the console. If the variables are silent, this will not happen and the value can be changed without anything being printed as a confirmation. } @chapter{Command completion with Tab} @cvar{con-completion} @default{List with values} @summary{ The Tab key is used to complete words in the console. The console knows how to complete command, variable and alias names. The completion can work in two ways. In the "List with values" mode, pressing Tab will complete the unambiguous part of the word and print a list of possible completions. Variables will be printed with their values. In the "Cycle" mode, Tab is used to cycle through all the choices. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/0000775000175000017500000000000012641367670021612 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-external-always.ame0000664000175000017500000000011612641367670027140 0ustar jaakkojaakko@summary{ 1=Always use external texture resources (overrides -pwadtex). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-cull-leafs.ame0000664000175000017500000000007012641367670026024 0ustar jaakkojaakko@summary{ 1=Disable non-visible bsp leaf culling. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-hud-offset-scale.ame0000664000175000017500000000006712641367670026360 0ustar jaakkojaakko@summary{ Scaling of player weapon (x,y) offset. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-info-rendpolys.ame0000664000175000017500000000010512641367670026170 0ustar jaakkojaakko@summary{ 1=Print rendpoly pool state after rendering a frame. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-shiny.ame0000664000175000017500000000010012641367670025143 0ustar jaakkojaakko@summary{ 1=Enable shiny textures on surfaces of the map. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-tex-showfix.ame0000664000175000017500000000013512641367670026264 0ustar jaakkojaakko@summary{ 1=Render the missing texture instead of fixing with a suitable game texture. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/sound-volume.ame0000664000175000017500000000005612641367670024734 0ustar jaakkojaakko@summary{ Sound effects volume (0-255). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bias-max.ame0000664000175000017500000000010112641367670024715 0ustar jaakkojaakko@summary{ Sector lightlevel that retains its normal color. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-halo-dim-near.ame0000664000175000017500000000006612641367670025643 0ustar jaakkojaakko@summary{ Halo dimming relative start distance. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-mobj-smooth-move.ame0000664000175000017500000000016212641367670026425 0ustar jaakkojaakko@summary{ 1=Use short-range visual offsets for models. 2=Use SRVO for sprites, too (unjags actor movement). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/refresh-rate-maximum.ame0000664000175000017500000000010112641367670026330 0ustar jaakkojaakko@summary{ Maximum limit for the frame rate (default: 200). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-generator-show-indices.ame0000664000175000017500000000006612641367670030362 0ustar jaakkojaakko@summary{ 1=Display particle generator indices. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-detail.ame0000664000175000017500000000005712641367670025266 0ustar jaakkojaakko@summary{ 1=Render with detail textures. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-halo-fade-far.ame0000664000175000017500000000007712641367670025616 0ustar jaakkojaakko@summary{ Distance at which halos are no longer visible. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-multitex.ame0000664000175000017500000000010412641367670026177 0ustar jaakkojaakko@summary{ 1=Use multitexturing when rendering dynamic lights. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/server-password.ame0000664000175000017500000000050712641367670025446 0ustar jaakkojaakko@summary{ Password for remote login. } @description{ Password that shell users must know in order to connect to the server. Applicable only to shell users; regular clients wishing to join the game do not need the password. @notice{Public servers must have a password to be accepted by the master server.} } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-camera-fov.ame0000664000175000017500000000003712641367670025244 0ustar jaakkojaakko@summary{ Field of view. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-bias-affected.ame0000664000175000017500000000010212641367670026446 0ustar jaakkojaakko@summary{ 1=Keep track which sources affect which surfaces. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/vid-gamma.ame0000664000175000017500000000007312641367670024140 0ustar jaakkojaakko@summary{ Display gamma correction factor: 1=normal. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-shadow.ame0000664000175000017500000000006012641367670024505 0ustar jaakkojaakko@summary{ 1=Render shadows under objects. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-info-frametime.ame0000664000175000017500000000005412641367670026125 0ustar jaakkojaakko@summary{ 1=Print frame time offsets. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-halo.ame0000664000175000017500000000006412641367670024147 0ustar jaakkojaakko@summary{ Number of flares to draw per light. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/blockmap-build.ame0000664000175000017500000000014712641367670025165 0ustar jaakkojaakko@summary{ Automatically generate blockmap data when necessary, 0=Never, 1=When needed, 2=Always. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/server-latencies.ame0000664000175000017500000000004712641367670025552 0ustar jaakkojaakko@summary{ Show client latencies. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-glow-wall.ame0000664000175000017500000000005012641367670025124 0ustar jaakkojaakko@summary{ 1=Render glow on walls. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/bsp-factor.ame0000664000175000017500000000011612641367670024334 0ustar jaakkojaakko@summary{ glBSP: changes the cost assigned to edge splits (default: 7). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/edit-bias-blink.ame0000664000175000017500000000004412641367670025232 0ustar jaakkojaakko@summary{ 1=Blink the cursor. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-glow-height.ame0000664000175000017500000000007012641367670025437 0ustar jaakkojaakko@summary{ Max height of wall glow (default: 100). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-detail-multitex.ame0000664000175000017500000000010512641367670027131 0ustar jaakkojaakko@summary{ 1=Use multitexturing when rendering detail textures. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-radius-max.ame0000664000175000017500000000010112641367670026373 0ustar jaakkojaakko@summary{ Maximum radius of dynamic lights (default: 128). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-particle-rate.ame0000664000175000017500000000007512641367670025762 0ustar jaakkojaakko@summary{ Particle spawn rate multiplier (default: 1). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-model-mirror-hud.ame0000664000175000017500000000005412641367670026411 0ustar jaakkojaakko@summary{ 1=Mirror HUD weapon models. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-mipmap.ame0000664000175000017500000000006212641367670025303 0ustar jaakkojaakko@summary{ The mipmapping mode for textures. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-wall-angle.ame0000664000175000017500000000007612641367670026357 0ustar jaakkojaakko@summary{ Intensity of angle-based wall lighting delta. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-radius-min-bias.ame0000664000175000017500000000011612641367670027313 0ustar jaakkojaakko@summary{ Minimum dynamic light radius to convert to BIAS light source. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/net-nosleep.ame0000664000175000017500000000006612641367670024531 0ustar jaakkojaakko@summary{ 1=Don't sleep while waiting for tics. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-sprite-mode.ame0000664000175000017500000000010112641367670025444 0ustar jaakkojaakko@summary{ 1=Draw sprites and masked walls with hard edges. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-model-aspect.ame0000664000175000017500000000007312641367670025601 0ustar jaakkojaakko@summary{ Scale for MD2 z-axis when model is loaded. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/con-transition.ame0000664000175000017500000000014412641367670025244 0ustar jaakkojaakko@summary{ Transition effect used when leaving busy mode: 0=Crossfade, 1=DOOM (smooth), 2=DOOM } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-filter-mag.ame0000664000175000017500000000010412641367670026044 0ustar jaakkojaakko@summary{ 1=Use bilinear filtering for texture magnification. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/input-mouse-frequency.ame0000664000175000017500000000012012641367670026553 0ustar jaakkojaakko@summary{ Mouse input polling frequency (events per second). 0=unlimited. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-sprite-blend.ame0000664000175000017500000000006512641367670025615 0ustar jaakkojaakko@summary{ 1=Use additive blending for sprites. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/sound-3d.ame0000664000175000017500000000005412641367670023731 0ustar jaakkojaakko@summary{ 1=Play sound effects in 3D. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-halo-realistic.ame0000664000175000017500000000006312641367670026123 0ustar jaakkojaakko@summary{ 1=Use more realistic halo effects. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bias-grid.ame0000664000175000017500000000006512641367670025066 0ustar jaakkojaakko@summary{ 1=Smooth sector lighting is enabled. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-mobj-show-vlights.ame0000664000175000017500000000007212641367670027362 0ustar jaakkojaakko@summary{ 1=Render mobj vlight vectors (for debug). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-fog-bright.ame0000664000175000017500000000010212641367670026352 0ustar jaakkojaakko@summary{ Brightness of dynamic lights when fog is enabled. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-camera-smooth.ame0000664000175000017500000000010712641367670025761 0ustar jaakkojaakko@summary{ 1=Filter camera movement between game tics (OBSOLETE). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/edit-bias-blue.ame0000664000175000017500000000007012641367670025061 0ustar jaakkojaakko@summary{ Blue component of the bias light color. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-halo-fade-near.ame0000664000175000017500000000006012641367670025763 0ustar jaakkojaakko@summary{ Distance to begin fading halos. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-halo-size.ame0000664000175000017500000000003712641367670025117 0ustar jaakkojaakko@summary{ Size of halos. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-shadow-radius-max.ame0000664000175000017500000000006212641367670026557 0ustar jaakkojaakko@summary{ Maximum radius of object shadows. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bias-grid-debug.ame0000664000175000017500000000006712641367670026154 0ustar jaakkojaakko@summary{ 1=Show the light grid (for debugging). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-sprite-alpha.ame0000664000175000017500000000007312641367670025615 0ustar jaakkojaakko@summary{ 1=Enable variable translucency on sprites. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bias.ame0000664000175000017500000000010212641367670024133 0ustar jaakkojaakko@summary{ 1=Enable the experimental shadow bias test setup. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-sky-distance.ame0000664000175000017500000000004312641367670025617 0ustar jaakkojaakko@summary{ Sky sphere radius. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/vid-contrast.ame0000664000175000017500000000005412641367670024712 0ustar jaakkojaakko@summary{ Display contrast: 1=normal. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-decor-far.ame0000664000175000017500000000011112641367670026164 0ustar jaakkojaakko@summary{ Maximum distance at which light decorations are visible. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/edit-bias-red.ame0000664000175000017500000000006712641367670024712 0ustar jaakkojaakko@summary{ Red component of the bias light color. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-filter-ui.ame0000664000175000017500000000007412641367670025723 0ustar jaakkojaakko@summary{ 1=User interface uses linear interpolation. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-particle-max.ame0000664000175000017500000000010312641367670025604 0ustar jaakkojaakko@summary{ Maximum number of particles to render. 0=no limit. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-model-distance.ame0000664000175000017500000000010112641367670026104 0ustar jaakkojaakko@summary{ Farther than this models revert back to sprites. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-nosprite.ame0000664000175000017500000000010312641367670025635 0ustar jaakkojaakko@summary{ 1=Disable drawing of all sprites and masked walls. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/con-transition-tics.ame0000664000175000017500000000010312641367670026177 0ustar jaakkojaakko@summary{ Duration of transition effect in tics. 0=Disabled. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/ui-cursor-height.ame0000664000175000017500000000004512641367670025473 0ustar jaakkojaakko@summary{ Mouse cursor height. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/input-sharp-lateprocessing.ame0000664000175000017500000000016112641367670027566 0ustar jaakkojaakko@summary{ 1=Process sharp events after tickers during sharp tics. Increases input latency by 1/35 seconds. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bloom-threshold.ame0000664000175000017500000000016512641367670026330 0ustar jaakkojaakko@summary{ Input threshold for bloom: pixels darker than this will not be affected by bloom (range: 0...1). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bias-grid-blocksize.ame0000664000175000017500000000010612641367670027045 0ustar jaakkojaakko@summary{ Size of a grid block in the light grid (default: 31). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/edit-bias-grab-distance.ame0000664000175000017500000000006412641367670026640 0ustar jaakkojaakko@summary{ Distance to the grabbed bias light. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-particle.ame0000664000175000017500000000005312641367670025025 0ustar jaakkojaakko@summary{ 1=Render particle effects. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-sprite-align.ame0000664000175000017500000000016012641367670025617 0ustar jaakkojaakko@summary{ 1=Always align sprites with the view plane. 2=Align to camera, unless slant > r_maxSpriteAngle. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/ui-panel-tips.ame0000664000175000017500000000007112641367670024763 0ustar jaakkojaakko@summary{ 1=Show help indicators in Control Panel. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-wall-angle-smooth.ame0000664000175000017500000000010112641367670027653 0ustar jaakkojaakko@summary{ 1=Enable smoothing of angle-based wall lighting. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-attenuation.ame0000664000175000017500000000012312641367670026660 0ustar jaakkojaakko@summary{ Maximum light attentuation distance, in map units (default: 1024). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-info-lums.ame0000664000175000017500000000007612641367670025140 0ustar jaakkojaakko@summary{ 1=Print lumobj count after rendering a frame. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/sound-overlap-stop.ame0000664000175000017500000000012412641367670026054 0ustar jaakkojaakko@summary{ 1=Only allow one sound per emitter object (as in traditional Doom). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-surface-show-vectors.ame0000664000175000017500000000016012641367670030066 0ustar jaakkojaakko@summary{ 3-bit bitfield. Show surface tangent-space vectors (red:tangent, green:bitangent, blue:normal). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/sound-info.ame0000664000175000017500000000006012641367670024353 0ustar jaakkojaakko@summary{ 1=Show sound debug information. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-sky-rows.ame0000664000175000017500000000005312641367670025020 0ustar jaakkojaakko@summary{ Number of sky sphere rows. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bias-grid-debug-size.ame0000664000175000017500000000010612641367670027116 0ustar jaakkojaakko@summary{ Size of a grid block in the light grid debug display. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/server-name.ame0000664000175000017500000000007412641367670024523 0ustar jaakkojaakko@summary{ The name of this computer if it's a server. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-halo-radius-min.ame0000664000175000017500000000004512641367670026214 0ustar jaakkojaakko@summary{ Minimum halo radius. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/server-player-limit.ame0000664000175000017500000000007112641367670026210 0ustar jaakkojaakko@summary{ Maximum number of players on the server. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-anim-smooth.ame0000664000175000017500000000007112641367670026253 0ustar jaakkojaakko@summary{ 1=Enable interpolated texture animation. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/ui-panel-help.ame0000664000175000017500000000006712641367670024741 0ustar jaakkojaakko@summary{ 1=Enable help window in Control Panel. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-finale-stretch.ame0000664000175000017500000000013712641367670026135 0ustar jaakkojaakko@summary{ Fixed aspect ratio finale stretch-scaling strategy 0=Smart, 1=Never, 2=Always. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-hud-fov-shift.ame0000664000175000017500000000010112641367670025677 0ustar jaakkojaakko@summary{ When FOV > 90 player weapon is shifted downward. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/input-joy-device.ame0000664000175000017500000000007212641367670025470 0ustar jaakkojaakko@summary{ ID of joystick to use (if more than one). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-bias-sight.ame0000664000175000017500000000011512641367670026027 0ustar jaakkojaakko@summary{ 1=Enable the use of line-of-sight checking with shadow bias. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-particle-diffuse.ame0000664000175000017500000000007612641367670026455 0ustar jaakkojaakko@summary{ Diffuse factor for particles near the camera. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-wireframe.ame0000664000175000017500000000014112641367670025755 0ustar jaakkojaakko@summary{ 1=Render player view in wireframe mode. 2=Also render UI and text as wireframes. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/input-mouse-filter.ame0000664000175000017500000000006412641367670026046 0ustar jaakkojaakko@summary{ Filter strength for mouse movement. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bias-grid-multisample.ame0000664000175000017500000000010612641367670027414 0ustar jaakkojaakko@summary{ Sector to grid block, conversion accuracy multiplier. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-hud-stretch.ame0000664000175000017500000000014612641367670025457 0ustar jaakkojaakko@summary{ Fixed aspect ratio player weapon stretch-scaling strategy 0=Smart, 1=Never, 2=Always. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/music-source.ame0000664000175000017500000000012012641367670024705 0ustar jaakkojaakko@summary{ Preferred music source: 0=Original MUS, 1=External files, 2=CD. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-glow.ame0000664000175000017500000000006312641367670024173 0ustar jaakkojaakko@summary{ Glow strength factor (default: 1). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bloom-intensity.ame0000664000175000017500000000006712641367670026363 0ustar jaakkojaakko@summary{ Output intensity for the bloom filter. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-num.ame0000664000175000017500000000010212641367670025121 0ustar jaakkojaakko@summary{ The maximum number of dynamic lights. 0=no limit. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/server-frame-interval.ame0000664000175000017500000000007412641367670026517 0ustar jaakkojaakko@summary{ Minimum number of tics between sent frames. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/edit-bias-hide.ame0000664000175000017500000000006012641367670025042 0ustar jaakkojaakko@summary{ 1=Hide bias light editor's HUD. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-sky-always.ame0000664000175000017500000000012412641367670026101 0ustar jaakkojaakko@summary{ 1=Always render the sky, even if there are no sky surfaces visible. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-fog-default.ame0000664000175000017500000000007312641367670025421 0ustar jaakkojaakko@summary{ Default fog mode: 0=linear, 1=exp, 2=exp2. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/sound-16bit.ame0000664000175000017500000000006312641367670024350 0ustar jaakkojaakko@summary{ 1=16-bit sound effects/resampling. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-filter-anisotropic.ame0000664000175000017500000000016212641367670027636 0ustar jaakkojaakko@summary{ Default level of anisotropic texture filtering -1=Best available, 0=Off, 1=2x, 2=4x, 3=8x, 4=16x. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/server-public.ame0000664000175000017500000000005612641367670025061 0ustar jaakkojaakko@summary{ 1=Send info to master server. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-soundorigins.ame0000664000175000017500000000014112641367670026517 0ustar jaakkojaakko@summary{ 3-bit bitfield. Show sound origins (for debug): 0x1=Sector, 0x2=Plane, 0x4=Side. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-sky-auto.ame0000664000175000017500000000010712641367670026103 0ustar jaakkojaakko@summary{ 1=Enable automatic calculation of the sky light color. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-polyobj-bbox.ame0000664000175000017500000000006112641367670026403 0ustar jaakkojaakko@summary{ 1=Render polyobj bounding boxes. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-model.ame0000664000175000017500000000006612641367670024326 0ustar jaakkojaakko@summary{ Render using 3D models when possible. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/edit-bias-green.ame0000664000175000017500000000007112641367670025233 0ustar jaakkojaakko@summary{ Green component of the bias light color. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/edit-bias-intensity.ame0000664000175000017500000000005512641367670026163 0ustar jaakkojaakko@summary{ Intensity of the bias light. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-sprite-align-angle.ame0000664000175000017500000000010012641367670026675 0ustar jaakkojaakko@summary{ Maximum angle for slanted sprites (spralign 2). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light.ame0000664000175000017500000000010612641367670024330 0ustar jaakkojaakko@summary{ 1=Render dynamic lights. 2=Process without rendering. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-vertex-show-indices.ame0000664000175000017500000000007712641367670027713 0ustar jaakkojaakko@summary{ 1=Enable drawing of vertex indices, for debug. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-compression.ame0000664000175000017500000000013312641367670026667 0ustar jaakkojaakko@summary{ Sector light range compression (brighten dark areas / darken light areas). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/sound-reverb-volume.ame0000664000175000017500000000007312641367670026216 0ustar jaakkojaakko@summary{ Reverb effects general volume (0=disable). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-particle-visible-near.ame0000664000175000017500000000007112641367670027403 0ustar jaakkojaakko@summary{ Minimum visible distance for a particle. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-filter-smart.ame0000664000175000017500000000006612641367670026435 0ustar jaakkojaakko@summary{ 1=Use hq2x-filtering on all textures. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-halo-bright.ame0000664000175000017500000000004712641367670025425 0ustar jaakkojaakko@summary{ Halo/flare brightness. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/server-info.ame0000664000175000017500000000011112641367670024526 0ustar jaakkojaakko@summary{ The description given of this computer if it's a server. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-mobj-light-auto.ame0000664000175000017500000000010312641367670026220 0ustar jaakkojaakko@summary{ 1=Enable automatically calculated lights on mobjs. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-gamma.ame0000664000175000017500000000006112641367670025101 0ustar jaakkojaakko@summary{ Texture gamma correction factor. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-blockmap.ame0000664000175000017500000000015012641367670025564 0ustar jaakkojaakko@summary{ Enable drawing of the blockmap debug display: 1=Mobjs, 2=Lines, 3=BspLeafs, 4=Polyobjs. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex.ame0000664000175000017500000000010412641367670024017 0ustar jaakkojaakko@summary{ 1=Render with textures. 2=Render with gray texture. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-ambient.ame0000664000175000017500000000004512641367670025747 0ustar jaakkojaakko@summary{ Ambient light level. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-glow-scale.ame0000664000175000017500000000007312641367670025261 0ustar jaakkojaakko@summary{ A multiplier for glow height (default: 1). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-halo-occlusion.ame0000664000175000017500000000006312641367670026142 0ustar jaakkojaakko@summary{ Rate at which occluded halos fade. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/input-toggle-sharp.ame0000664000175000017500000000013712641367670026030 0ustar jaakkojaakko@summary{ 1=Process toggle events only on sharp ticks for backwards compatible behavior. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bias-min.ame0000664000175000017500000000010512641367670024717 0ustar jaakkojaakko@summary{ Sector lightlevel that is biased completely to zero. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-fakeradio.ame0000664000175000017500000000012412641367670025146 0ustar jaakkojaakko@summary{ 1=Enable simulated radiosity lighting. 2=Process without rendering. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/con-move-speed.ame0000664000175000017500000000006212641367670025115 0ustar jaakkojaakko@summary{ Speed of console opening/closing. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-model-shiny-multitex.ame0000664000175000017500000000010012641367670027314 0ustar jaakkojaakko@summary{ 1=Enable multitexturing with shiny model skins. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-detail-strength.ame0000664000175000017500000000006712641367670027123 0ustar jaakkojaakko@summary{ Global detail texture strength factor. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/ctl-info.ame0000664000175000017500000000010312641367670024003 0ustar jaakkojaakko@summary{ 1=Show player control state debugging information. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-vertex-show-bars.ame0000664000175000017500000000010312641367670027212 0ustar jaakkojaakko@summary{ 1=Enable drawing of vertex z axis bars, for debug. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/music-volume.ame0000664000175000017500000000004612641367670024723 0ustar jaakkojaakko@summary{ Music volume (0-255). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-info-tris.ame0000664000175000017500000000010012641367670025125 0ustar jaakkojaakko@summary{ 1=Print triangle count after rendering a frame. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-sky.ame0000664000175000017500000000012712641367670025137 0ustar jaakkojaakko@summary{ Sky light color blending factor for sky sectors (off: 0, default: .2). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-model-spin-speed.ame0000664000175000017500000000006312641367670026370 0ustar jaakkojaakko@summary{ Speed of model spinning, 1=normal. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-model-precache.ame0000664000175000017500000000007412641367670026075 0ustar jaakkojaakko@summary{ 1=Precache 3D models at level setup (slow). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-detail-scale.ame0000664000175000017500000000005612641367670026352 0ustar jaakkojaakko@summary{ Global detail texture factor. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-framecount.ame0000664000175000017500000000003712641367670026143 0ustar jaakkojaakko@summary{ Frame counter. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-quality.ame0000664000175000017500000000005712641367670025514 0ustar jaakkojaakko@summary{ The quality of textures (0-8). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-model-inter.ame0000664000175000017500000000004612641367670025443 0ustar jaakkojaakko@summary{ 1=Interpolate frames. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bloom-complexity.ame0000664000175000017500000000011612641367670026525 0ustar jaakkojaakko@summary{ 0=One-pass bloom (faster). 1=Two-pass bloom (more realistic). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-map-material-precache.ame0000664000175000017500000000006712641367670027350 0ustar jaakkojaakko@summary{ 1=Precache materials during map setup. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/net-queue-show.ame0000664000175000017500000000004412641367670025162 0ustar jaakkojaakko@summary{ Monitor send queue. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-shadow-far.ame0000664000175000017500000000007412641367670025260 0ustar jaakkojaakko@summary{ Maximum distance where shadows are visible. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/sound-rate.ame0000664000175000017500000000010112641367670024347 0ustar jaakkojaakko@summary{ Sound effects sample rate (11025, 22050, 44100). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-bright.ame0000664000175000017500000000006512641367670025611 0ustar jaakkojaakko@summary{ Intensity factor for dynamic lights. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-blockmap-size.ame0000664000175000017500000000010112641367670026530 0ustar jaakkojaakko@summary{ Scale multiplier for the blockmap debug display. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-decor-bright.ame0000664000175000017500000000006112641367670026677 0ustar jaakkojaakko@summary{ Brightness of light decorations. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-halo-secondary-limit.ame0000664000175000017500000000004312641367670027245 0ustar jaakkojaakko@summary{ Minimum halo size. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/file-startup.ame0000664000175000017500000000007212641367670024714 0ustar jaakkojaakko@summary{ The list of WADs to be loaded at startup. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-freeze.ame0000664000175000017500000000006112641367670025255 0ustar jaakkojaakko@summary{ 1=Stop updating rendering lists. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bloom-dispersion.ame0000664000175000017500000000015612641367670026513 0ustar jaakkojaakko@summary{ Determines how much the bloom filter spreads out color values when blurring (1.0=normal). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-mobj-smooth-turn.ame0000664000175000017500000000010612641367670026445 0ustar jaakkojaakko@summary{ 1=Use separate visual angle for mobjs (unjag actors). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bloom.ame0000664000175000017500000000020512641367670024331 0ustar jaakkojaakko@summary{ 1=Enable the bloom filter for frame post-processing. Bloom makes bright areas of the frame glow more intensely. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-sprite-noz.ame0000664000175000017500000000006712641367670025341 0ustar jaakkojaakko@summary{ 1=Don't write sprites in the Z buffer. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/vid-bright.ame0000664000175000017500000000010012641367670024324 0ustar jaakkojaakko@summary{ Display brightness: -1=dark, 0=normal, 1=light. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-halo-zmag-div.ame0000664000175000017500000000004612641367670025663 0ustar jaakkojaakko@summary{ Halo Z magnification. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-lums.ame0000664000175000017500000000006012641367670024754 0ustar jaakkojaakko@summary{ 1=Display lumobj debug display. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-shadow-darkness.ame0000664000175000017500000000006412641367670026321 0ustar jaakkojaakko@summary{ Darkness factor for object shadows. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-model-shiny-strength.ame0000664000175000017500000000007512641367670027312 0ustar jaakkojaakko@summary{ General strength of model shininess effects. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/client-connect-timeout.ame0000664000175000017500000000011512641367670026664 0ustar jaakkojaakko@summary{ Maximum number of seconds to attempt connecting to a server. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-model-lod.ame0000664000175000017500000000011112641367670025071 0ustar jaakkojaakko@summary{ Custom level of detail factor. 0=LOD disabled, 1=normal. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-light-mod.ame0000664000175000017500000000006012641367670025660 0ustar jaakkojaakko@summary{ Show the light-level mod range. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-sector-show-indices.ame0000664000175000017500000000007712641367670027675 0ustar jaakkojaakko@summary{ 1=Enable drawing of sector indices, for debug. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-mobj-bbox.ame0000664000175000017500000000012012641367670025650 0ustar jaakkojaakko@summary{ 1=Render mobj bounding boxes (as used for collision detection). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/net-ip-port.ame0000664000175000017500000000007112641367670024452 0ustar jaakkojaakko@summary{ TCP port to use for control connections. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-radius-scale.ame0000664000175000017500000000007612641367670026710 0ustar jaakkojaakko@summary{ A multiplier for dynlight radii (default: 1). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-sprite-precache.ame0000664000175000017500000000007412641367670026303 0ustar jaakkojaakko@summary{ 1=Precache sprites during map setup (slow). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-blend.ame0000664000175000017500000000013112641367670025410 0ustar jaakkojaakko@summary{ Dynamic lights color blending mode: 0=normal, 1=additive, 2=no blending. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/music-soundfont.ame0000664000175000017500000000016512641367670025435 0ustar jaakkojaakko@summary{ Filename of the DLS/SF2 soundfont file to be used with MIDI playback (if supported by audio plugin). } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-sprite-lights.ame0000664000175000017500000000007412641367670026023 0ustar jaakkojaakko@summary{ Maximum number of light sources on sprites. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/input-joy.ame0000664000175000017500000000005112641367670024230 0ustar jaakkojaakko@summary{ 1=Enable joystick input. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/net-dev.ame0000664000175000017500000000005212641367670023635 0ustar jaakkojaakko@summary{ Network development mode. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-fakeradio-update.ame0000664000175000017500000000012512641367670027203 0ustar jaakkojaakko@summary{ 1=Enable updating of the shadow edges used with simulated radiosity. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-model-lights.ame0000664000175000017500000000007312641367670025614 0ustar jaakkojaakko@summary{ Maximum number of light sources on models. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-sky-detail.ame0000664000175000017500000000007412641367670025273 0ustar jaakkojaakko@summary{ Number of sky sphere quadrant subdivisions. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/bsp-cache.ame0000664000175000017500000000015312641367670024122 0ustar jaakkojaakko@summary{ 1=Load generated GL nodes data from the bspcache directory. 0=Always generate new GL data. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-fakeradio-darkness.ame0000664000175000017500000000007112641367670026757 0ustar jaakkojaakko@summary{ Darkness of simulated radiosity shadows. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-halo-dim-far.ame0000664000175000017500000000006412641367670025464 0ustar jaakkojaakko@summary{ Halo dimming relative end distance. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-decor-angle.ame0000664000175000017500000000010312641367670026503 0ustar jaakkojaakko@summary{ Reduce brightness if surface/view angle too steep. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/edit-bias-show-indices.ame0000664000175000017500000000006212641367670026527 0ustar jaakkojaakko@summary{ 1=Show source indices in 3D view. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-dev-sky.ame0000664000175000017500000000006512641367670024607 0ustar jaakkojaakko@summary{ 1=Render the sky as a solid surface. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/input-conflict-zerocontrol.ame0000664000175000017500000000017712641367670027617 0ustar jaakkojaakko@summary{ 1=If a control is influenced by two or more conflicting input device states, the control position gets zeroed. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/ui-cursor-width.ame0000664000175000017500000000004412641367670025341 0ustar jaakkojaakko@summary{ Mouse cursor width. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/reject-build.ame0000664000175000017500000000014512641367670024647 0ustar jaakkojaakko@summary{ Automatically generate reject data when necessary, 0=Never, 1=When needed, 2=Always. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/edit-bias-show-sources.ame0000664000175000017500000000005212641367670026573 0ustar jaakkojaakko@summary{ 1=Show all light sources. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-tex-filter-sprite.ame0000664000175000017500000000005112641367670026607 0ustar jaakkojaakko@summary{ 1=Render smooth sprites. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/net-ip-address.ame0000664000175000017500000000006612641367670025117 0ustar jaakkojaakko@summary{ TCP/IP address for searching servers. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/net-name.ame0000664000175000017500000000006012641367670023776 0ustar jaakkojaakko@summary{ Your name in multiplayer games. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-light-decor.ame0000664000175000017500000000006412641367670025425 0ustar jaakkojaakko@summary{ 1=Enable surface light decorations. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/con-show-during-setup.ame0000664000175000017500000000007312641367670026457 0ustar jaakkojaakko@summary{ 1=Show console when a map is being loaded. } doomsday-stable-1.15.7/doomsday/doc/engine/variable/rend-bias-lightspeed.ame0000664000175000017500000000011512641367670026265 0ustar jaakkojaakko@summary{ Milliseconds it takes for light changes to become effective. } doomsday-stable-1.15.7/doomsday/doc/engine/command/0000775000175000017500000000000012641367670021443 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/engine/command/togglefullscreen.ame0000664000175000017500000000007612641367670025476 0ustar jaakkojaakko@summary{ Toggle between fullscreen and windowed modes. } doomsday-stable-1.15.7/doomsday/doc/engine/command/inspectmaterial.ame0000664000175000017500000000026112641367670025312 0ustar jaakkojaakko@summary{ Print extended information about a Material to the console. } @description{ Params: inspectmaterial (uri) @cbr For example, 'inspectmaterial flats:fwater1'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/ls.ame0000664000175000017500000000030512641367670022543 0ustar jaakkojaakko@summary{ Print contents of directories. } @description{ Params: ls (dirs) ... @cbr For example, 'ls data/'. @cbr Virtual files are listed, too. @cbr Paths are relative to the base path. } doomsday-stable-1.15.7/doomsday/doc/engine/command/say.ame0000664000175000017500000000005212641367670022720 0ustar jaakkojaakko@summary{ Broadcast a chat message. } doomsday-stable-1.15.7/doomsday/doc/engine/command/saynum.ame0000664000175000017500000000007512641367670023445 0ustar jaakkojaakko@summary{ Send a chat message to the specified player. } doomsday-stable-1.15.7/doomsday/doc/engine/command/login.ame0000664000175000017500000000005212641367670023234 0ustar jaakkojaakko@summary{ Log in to server console. } doomsday-stable-1.15.7/doomsday/doc/engine/command/listlumps.ame0000664000175000017500000000012012641367670024154 0ustar jaakkojaakko@summary{ List all loaded virtual resource files from container archives. } doomsday-stable-1.15.7/doomsday/doc/engine/command/playdemo.ame0000664000175000017500000000016512641367670023743 0ustar jaakkojaakko@summary{ Play a demo. } @description{ Params: playdemo (fileName) @cbr For example, 'playdemo demo1.dmo'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/bldel.ame0000664000175000017500000000006012641367670023205 0ustar jaakkojaakko@summary{ Delete current/specified light. } doomsday-stable-1.15.7/doomsday/doc/engine/command/listvars.ame0000664000175000017500000000007512641367670024000 0ustar jaakkojaakko@summary{ List all console variables and their values. } doomsday-stable-1.15.7/doomsday/doc/engine/command/net.ame0000664000175000017500000000272712641367670022725 0ustar jaakkojaakko@summary{ Network setup and control. } $* 4.4.3 Net info Displays the current network settings along with some debug information. 4.4.14 Net search If you want to connect to a server and you have configured your service provider with the necessary commands listed above, this command lists all the servers that can be found. Unless you're using the IPX service provider, there will be a slight delay before this command gives you the server list because DirectPlay has to first make the actual connection between the computers. It's best to keep repeating this command until you see the server you want. 4.4.15 Net connect (server number) Connects to the specified server. Only works after "net search" shows the number of the server (usually zero). 4.4.16 Net servers Connects to the master server and retrieves a list of running servers. You must set the console variable "net-master-address" before you can use this command. "net-master-port" should be zero, or a port of your choice (not 10123) if you're behind a firewall. 4.4.17 Net mconnect (server number received from master) This command connects to the specified server. Only works after "net servers" has successfully ended and the list of servers has been printed in the console. 4.4.19 Net disconnect Disconnects from the server you're currently connected to. Automatically done when you return to the operating system. You will naturally have to disconnect from a server if you want to connect to another one. *$doomsday-stable-1.15.7/doomsday/doc/engine/command/toggle.ame0000664000175000017500000000022612641367670023410 0ustar jaakkojaakko@summary{ Toggle the value of a cvar between zero and nonzero. } @description{ Params: toggle (cvar) @cbr For example, 'toggle rend-light'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/lowres.ame0000664000175000017500000000006612641367670023444 0ustar jaakkojaakko@summary{ Select the poorest rendering quality. } doomsday-stable-1.15.7/doomsday/doc/engine/command/blgrab.ame0000664000175000017500000000007112641367670023356 0ustar jaakkojaakko@summary{ Grab current/specified light, or ubgrab. } doomsday-stable-1.15.7/doomsday/doc/engine/command/pausemusic.ame0000664000175000017500000000007112641367670024303 0ustar jaakkojaakko@summary{ Pause the currently playing music track. } doomsday-stable-1.15.7/doomsday/doc/engine/command/bledit.ame0000664000175000017500000000005412641367670023371 0ustar jaakkojaakko@summary{ Enter bias light edit mode. } doomsday-stable-1.15.7/doomsday/doc/engine/command/contoggle.ame0000664000175000017500000000005712641367670024112 0ustar jaakkojaakko@summary{ Open/close the console prompt. } doomsday-stable-1.15.7/doomsday/doc/engine/command/dec.ame0000664000175000017500000000005012641367670022655 0ustar jaakkojaakko@summary{ Subtract 1 from a cvar. } doomsday-stable-1.15.7/doomsday/doc/engine/command/centerwindow.ame0000664000175000017500000000011012641367670024627 0ustar jaakkojaakko@summary{ Center the window on the desktop when in windowed mode. } doomsday-stable-1.15.7/doomsday/doc/engine/command/settics.ame0000664000175000017500000000021512641367670023603 0ustar jaakkojaakko@summary{ Set number of game tics per second (default: 35). } @description{ Params: settics (tics) @cbr For example, 'settics 15'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/chatto.ame0000664000175000017500000000007512641367670023413 0ustar jaakkojaakko@summary{ Send a chat message to the specified player. } doomsday-stable-1.15.7/doomsday/doc/engine/command/bli.ame0000664000175000017500000000006212641367670022673 0ustar jaakkojaakko@summary{ Set intensity of light at cursor. } doomsday-stable-1.15.7/doomsday/doc/engine/command/exec.ame0000664000175000017500000000023412641367670023052 0ustar jaakkojaakko@summary{ Loads and executes a file containing console commands. } @description{ Params: exec (file) ... @cbr For example, 'exec "myconfig.cfg"'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/updatesettings.ame0000664000175000017500000000011412641367670025166 0ustar jaakkojaakko@summary{ Show the settings dialog for configuring automatic updates. } doomsday-stable-1.15.7/doomsday/doc/engine/command/help.ame0000664000175000017500000000006412641367670023057 0ustar jaakkojaakko@summary{ Show information about the console. } doomsday-stable-1.15.7/doomsday/doc/engine/command/inc.ame0000664000175000017500000000004112641367670022673 0ustar jaakkojaakko@summary{ Add 1 to a cvar. } doomsday-stable-1.15.7/doomsday/doc/engine/command/safebindr.ame0000664000175000017500000000011112641367670024055 0ustar jaakkojaakko@summary{ Bind a command to an unless the event is already bound. } doomsday-stable-1.15.7/doomsday/doc/engine/command/blmenu.ame0000664000175000017500000000005112641367670023405 0ustar jaakkojaakko@summary{ Show/hide the bias menu. } doomsday-stable-1.15.7/doomsday/doc/engine/command/add.ame0000664000175000017500000000005112641367670022653 0ustar jaakkojaakko@summary{ Add something to a cvar. } doomsday-stable-1.15.7/doomsday/doc/engine/command/listmaterials.ame0000664000175000017500000000006212641367670025002 0ustar jaakkojaakko@summary{ List all known surface materials. } doomsday-stable-1.15.7/doomsday/doc/engine/command/blnew.ame0000664000175000017500000000006012641367670023232 0ustar jaakkojaakko@summary{ Allocate new light and grab it. } doomsday-stable-1.15.7/doomsday/doc/engine/command/alias.ame0000664000175000017500000000010012641367670023207 0ustar jaakkojaakko@summary{ Create aliases for a (set of) console commands. } doomsday-stable-1.15.7/doomsday/doc/engine/command/conopen.ame0000664000175000017500000000005112641367670023564 0ustar jaakkojaakko@summary{ Open the console prompt. } doomsday-stable-1.15.7/doomsday/doc/engine/command/quit!.ame0000664000175000017500000000006712641367670023155 0ustar jaakkojaakko@summary{ Exit immediately and return to the OS. } doomsday-stable-1.15.7/doomsday/doc/engine/command/inspectgame.ame0000664000175000017500000000027112641367670024426 0ustar jaakkojaakko@summary{ Print detailed information about a registered game to the console. } @description{ Params: inspectgame (identityKey) @cbr For example, 'inspectgame doom1-ultimate'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/font.ame0000664000175000017500000000005612641367670023076 0ustar jaakkojaakko@summary{ Modify console font settings. } doomsday-stable-1.15.7/doomsday/doc/engine/command/listtextures.ame0000664000175000017500000000005112641367670024702 0ustar jaakkojaakko@summary{ List all known textures. } doomsday-stable-1.15.7/doomsday/doc/engine/command/inspecttexture.ame0000664000175000017500000000025612641367670025220 0ustar jaakkojaakko@summary{ Print extended information about a Texture to the console. } @description{ Params: inspecttexture (uri) @cbr For example, 'inspecttexture flats:fwater1'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/listmaps.ame0000664000175000017500000000004612641367670023763 0ustar jaakkojaakko@summary{ List all loaded maps. } doomsday-stable-1.15.7/doomsday/doc/engine/command/sub.ame0000664000175000017500000000006012641367670022714 0ustar jaakkojaakko@summary{ Subtract something from a cvar. } doomsday-stable-1.15.7/doomsday/doc/engine/command/setfullres.ame0000664000175000017500000000043312641367670024317 0ustar jaakkojaakko@summary{ Change to fullscreen mode using the specified resolution. } @description{ @usage @ident{setfullres} @help_arg{width} @help_arg{height} @seealso @list{ @item @cmd{listdisplaymodes} @item @cmd{setwinres} @item @cmd{setres} } }doomsday-stable-1.15.7/doomsday/doc/engine/command/tutorial.ame0000664000175000017500000000007712641367670023776 0ustar jaakkojaakko@summary{ Show a tutorial that introduces Doomsday's UI. } doomsday-stable-1.15.7/doomsday/doc/engine/command/inspectmap.ame0000664000175000017500000000012112641367670024264 0ustar jaakkojaakko@summary{ Print extended information about the current map to the console. } doomsday-stable-1.15.7/doomsday/doc/engine/command/togglemaximized.ame0000664000175000017500000000010112641367670025310 0ustar jaakkojaakko@summary{ Toggle between maximized and normal window modes. }doomsday-stable-1.15.7/doomsday/doc/engine/command/displaymode.ame0000664000175000017500000000037512641367670024446 0ustar jaakkojaakko@summary{ Print information about the current display mode and window state. } @description{ @seealso @list{ @item @cmd{listdisplaymodes} @item @cmd{setres} @item @cmd{setfullres} @item @cmd{setwinres} } }doomsday-stable-1.15.7/doomsday/doc/engine/command/unload.ame0000664000175000017500000000023112641367670023405 0ustar jaakkojaakko@summary{ Unload the current game or one or more data files. } @description{ Params: unload (name) ... @cbr For example, 'unload mylevel.wad'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/setwinres.ame0000664000175000017500000000041612641367670024153 0ustar jaakkojaakko@summary{ Set window size and change to windowed mode. } @description{ @usage @ident{setwinres} @help_arg{width} @help_arg{height} @seealso @list{ @item @cmd{setfullres} @item @cmd{setres} @item @cmd{listdisplaymodes} } }doomsday-stable-1.15.7/doomsday/doc/engine/command/playmusic.ame0000664000175000017500000000011512641367670024132 0ustar jaakkojaakko@summary{ Play a music track, music lump, external file or a CD track. } doomsday-stable-1.15.7/doomsday/doc/engine/command/conlocp.ame0000664000175000017500000000016712641367670023570 0ustar jaakkojaakko@summary{ Connect a local player. } @description{ Params: conlocp (playernum) @cbr For example, 'conlocp 1'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/listdisplaymodes.ame0000664000175000017500000000036112641367670025520 0ustar jaakkojaakko@summary{ List all display modes supported by the graphics hardware. } @description{ @seealso @list{ @item @cmd{displaymode} @item @cmd{setres} @item @cmd{setfullres} @item @cmd{setwinres} } } doomsday-stable-1.15.7/doomsday/doc/engine/command/setvidramp.ame0000664000175000017500000000006612641367670024307 0ustar jaakkojaakko@summary{ Update display's hardware gamma ramp. } doomsday-stable-1.15.7/doomsday/doc/engine/command/sayto.ame0000664000175000017500000000007512641367670023270 0ustar jaakkojaakko@summary{ Send a chat message to the specified player. } doomsday-stable-1.15.7/doomsday/doc/engine/command/load.ame0000664000175000017500000000031212641367670023042 0ustar jaakkojaakko@summary{ Load a complete game or one or more data files (e.g., a WAD or a lump). } @description{ @params{load (name) ...} @cbr @example{@code{load (gamename)} or @code{load mylevel.wad}} } doomsday-stable-1.15.7/doomsday/doc/engine/command/dir.ame0000664000175000017500000000030712641367670022705 0ustar jaakkojaakko@summary{ Print contents of directories. } @description{ Params: dir (dirs) ... @cbr For example, 'dir data/'. @cbr Virtual files are listed, too. @cbr Paths are relative to the base path. } doomsday-stable-1.15.7/doomsday/doc/engine/command/connect.ame0000664000175000017500000000006212641367670023556 0ustar jaakkojaakko@summary{ Connect to a server using TCP/IP. } doomsday-stable-1.15.7/doomsday/doc/engine/command/inspectsavegame.ame0000664000175000017500000000124312641367670025305 0ustar jaakkojaakko@summary{ Print detailed information about a saved game session to the console. } @description{ @usage @ident{inspectsavegame} @help_arg{path} The @help_arg{path} is an absolute path and name of the saved game session to be inspected. User savegames are located in /home/savegames/<@wikiterm{Game identity key}>. If the saved session belongs to the currently loaded game, the path component may be omitted. @examples Inspect the Hexen savegame "hex1" while the game is loaded: @pre{inspectsavegame Hex1} Inspect the Doom savegame "doomsav1" at any time: @pre{inspectsavegame /home/savegames/doom1-ultimate/doomsav1} } doomsday-stable-1.15.7/doomsday/doc/engine/command/stopmusic.ame0000664000175000017500000000006212641367670024153 0ustar jaakkojaakko@summary{ Stop any currently playing music. } doomsday-stable-1.15.7/doomsday/doc/engine/command/setres.ame0000664000175000017500000000076112641367670023440 0ustar jaakkojaakko@summary{ Change display mode resolution or window size. } @description{ @usage @ident{setres} @help_arg{width} @help_arg{height} The window retains its current mode, adjusting the display mode if in fullscreen and the window size if in a windowed mode. @examples Change resolution to 1024 x 768: @code{setres 1024 768} @seealso @list{ @item @cmd{listdisplaymodes} @item @cmd{setfullres} @item @cmd{setwinres} } } doomsday-stable-1.15.7/doomsday/doc/engine/command/quit.ame0000664000175000017500000000013112641367670023104 0ustar jaakkojaakko@summary{ If a game is loaded execute a quit request, otherwise, exit immediately. } doomsday-stable-1.15.7/doomsday/doc/engine/command/reload.ame0000664000175000017500000000006612641367670023377 0ustar jaakkojaakko@summary{ Reloads the current game (if loaded). } doomsday-stable-1.15.7/doomsday/doc/engine/command/taskbar.ame0000664000175000017500000000010412641367670023551 0ustar jaakkojaakko@summary{ Open/close the task bar and console command prompt. } doomsday-stable-1.15.7/doomsday/doc/engine/command/delbind.ame0000664000175000017500000000010312641367670023522 0ustar jaakkojaakko@summary{ Deletes all bindings to the given console command. } doomsday-stable-1.15.7/doomsday/doc/engine/command/reset.ame0000664000175000017500000000011612641367670023247 0ustar jaakkojaakko@summary{ Resets the engine (reloading all data files and definitions). } doomsday-stable-1.15.7/doomsday/doc/engine/command/if.ame0000664000175000017500000000007412641367670022526 0ustar jaakkojaakko@summary{ Execute a command if the condition is true. } doomsday-stable-1.15.7/doomsday/doc/engine/command/listbindings.ame0000664000175000017500000000005112641367670024614 0ustar jaakkojaakko@summary{ List all event bindings. } doomsday-stable-1.15.7/doomsday/doc/engine/command/blunlock.ame0000664000175000017500000000006012641367670023734 0ustar jaakkojaakko@summary{ Unlock current/specified light. } doomsday-stable-1.15.7/doomsday/doc/engine/command/activatebcontext.ame0000664000175000017500000000030312641367670025472 0ustar jaakkojaakko@summary{ Activate a binding context. } @description{ Input events can only trigger bindings in active binding contexts. @seealso @list{ @item @wikiterm{Bindings} } }doomsday-stable-1.15.7/doomsday/doc/engine/command/lastupdated.ame0000664000175000017500000000010112641367670024431 0ustar jaakkojaakko@summary{ Show when the latest check for updates was made. } doomsday-stable-1.15.7/doomsday/doc/engine/command/listfiles.ame0000664000175000017500000000006012641367670024121 0ustar jaakkojaakko@summary{ List all loaded resource files. } doomsday-stable-1.15.7/doomsday/doc/engine/command/logout.ame0000664000175000017500000000007712641367670023444 0ustar jaakkojaakko@summary{ Terminate remote connection to server console. } doomsday-stable-1.15.7/doomsday/doc/engine/command/demolump.ame0000664000175000017500000000024112641367670023746 0ustar jaakkojaakko@summary{ Write a reference lump file for a demo. } @description{ Params: demolump (demofile) (lumpfile) @cbr For example, 'demolump demo1.dmo DEMO1'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/bindcontrol.ame0000664000175000017500000000007212641367670024443 0ustar jaakkojaakko@summary{ Bind an input device to a player control. } doomsday-stable-1.15.7/doomsday/doc/engine/command/setname.ame0000664000175000017500000000016112641367670023561 0ustar jaakkojaakko@summary{ Set your name. } @description{ Params: setname (name) @cbr For example, 'setname "my name"'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/recorddemo.ame0000664000175000017500000000005012641367670024245 0ustar jaakkojaakko@summary{ Start recording a demo. } doomsday-stable-1.15.7/doomsday/doc/engine/command/apropos.ame0000664000175000017500000000007512641367670023614 0ustar jaakkojaakko@summary{ Summarize all help containing a search term. } doomsday-stable-1.15.7/doomsday/doc/engine/command/bllock.ame0000664000175000017500000000005612641367670023376 0ustar jaakkojaakko@summary{ Lock current/specified light. } doomsday-stable-1.15.7/doomsday/doc/engine/command/write.ame0000664000175000017500000000021312641367670023255 0ustar jaakkojaakko@summary{ Write bindings and aliases to a file. } @description{ Params: write (filename) @cbr For example, 'write myconfig.cfg'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/blclear.ame0000664000175000017500000000004312641367670023530 0ustar jaakkojaakko@summary{ Delete all lights. } doomsday-stable-1.15.7/doomsday/doc/engine/command/blsave.ame0000664000175000017500000000007012641367670023400 0ustar jaakkojaakko@summary{ Write the current lights to a DED file. } doomsday-stable-1.15.7/doomsday/doc/engine/command/uicolor.ame0000664000175000017500000000041412641367670023602 0ustar jaakkojaakko@summary{ Change Doomsday user interface colors. } @description{ Params: uicolor (object) (red) (green) (blue) @cbr For example, 'uicolor text 1 1 1'. @cbr Possible objects are: @cbr text, shadow, bglight, bgmed, bgdark, @cbr borhigh, bormed, borlow, help } doomsday-stable-1.15.7/doomsday/doc/engine/command/chat.ame0000664000175000017500000000005212641367670023043 0ustar jaakkojaakko@summary{ Broadcast a chat message. } doomsday-stable-1.15.7/doomsday/doc/engine/command/listaliases.ame0000664000175000017500000000007312641367670024444 0ustar jaakkojaakko@summary{ List all aliases and their expanded forms. } doomsday-stable-1.15.7/doomsday/doc/engine/command/listfonts.ame0000664000175000017500000000004612641367670024154 0ustar jaakkojaakko@summary{ List all known fonts. } doomsday-stable-1.15.7/doomsday/doc/engine/command/clear.ame0000664000175000017500000000005212641367670023212 0ustar jaakkojaakko@summary{ Clear the console buffer. } doomsday-stable-1.15.7/doomsday/doc/engine/command/playsound.ame0000664000175000017500000000004512641367670024144 0ustar jaakkojaakko@summary{ Play a sound effect. } doomsday-stable-1.15.7/doomsday/doc/engine/command/blquit.ame0000664000175000017500000000005312641367670023425 0ustar jaakkojaakko@summary{ Exit bias light edit mode. } doomsday-stable-1.15.7/doomsday/doc/engine/command/ping.ame0000664000175000017500000000010412641367670023057 0ustar jaakkojaakko@summary{ Ping the server (or a player if you're the server). } doomsday-stable-1.15.7/doomsday/doc/engine/command/listinputdevices.ame0000664000175000017500000000011412641367670025521 0ustar jaakkojaakko@summary{ List all currently active input devices and their controls. } doomsday-stable-1.15.7/doomsday/doc/engine/command/rendedit.ame0000664000175000017500000000010212641367670023716 0ustar jaakkojaakko@summary{ Open the Renderer Appearance editor in a sidebar. } doomsday-stable-1.15.7/doomsday/doc/engine/command/update.ame0000664000175000017500000000006212641367670023407 0ustar jaakkojaakko@summary{ Check for new available releases. } doomsday-stable-1.15.7/doomsday/doc/engine/command/updateandnotify.ame0000664000175000017500000000013212641367670025321 0ustar jaakkojaakko@summary{ Check for new available releases and open the update notification dialog. } doomsday-stable-1.15.7/doomsday/doc/engine/command/pausedemo.ame0000664000175000017500000000005512641367670024111 0ustar jaakkojaakko@summary{ Pause/resume demo recording. } doomsday-stable-1.15.7/doomsday/doc/engine/command/version.ame0000664000175000017500000000006312641367670023613 0ustar jaakkojaakko@summary{ Show detailed version information. } doomsday-stable-1.15.7/doomsday/doc/engine/command/blc.ame0000664000175000017500000000020612641367670022665 0ustar jaakkojaakko@summary{ Set color of light at cursor. } @description{ Params: blc (red) (green) (blue) @cbr For example, 'blc 1 0.5 0.2'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/bldup.ame0000664000175000017500000000007412641367670023236 0ustar jaakkojaakko@summary{ Duplicate current/specified light, grab it. } doomsday-stable-1.15.7/doomsday/doc/engine/command/clearbinds.ame0000664000175000017500000000005712641367670024237 0ustar jaakkojaakko@summary{ Deletes all existing bindings. } doomsday-stable-1.15.7/doomsday/doc/engine/command/fog.ame0000664000175000017500000000004512641367670022701 0ustar jaakkojaakko@summary{ Modify fog settings. } doomsday-stable-1.15.7/doomsday/doc/engine/command/conclose.ame0000664000175000017500000000005212641367670023731 0ustar jaakkojaakko@summary{ Close the console prompt. } doomsday-stable-1.15.7/doomsday/doc/engine/command/texreset.ame0000664000175000017500000000005012641367670023765 0ustar jaakkojaakko@summary{ Force a texture reload. } doomsday-stable-1.15.7/doomsday/doc/engine/command/deactivatebcontext.ame0000664000175000017500000000030412641367670026004 0ustar jaakkojaakko@summary{ Deactivate a binding context. } @description{ Bindings in deactivated binding contexts cannot be triggered. @seealso @list{ @item @wikiterm{Bindings} } }doomsday-stable-1.15.7/doomsday/doc/engine/command/listmobjtypes.ame0000664000175000017500000000005312641367670025035 0ustar jaakkojaakko@summary{ List all known mobj types. } doomsday-stable-1.15.7/doomsday/doc/engine/command/varstats.ame0000664000175000017500000000010012641367670023765 0ustar jaakkojaakko@summary{ Show detailed statistics for console variables. } doomsday-stable-1.15.7/doomsday/doc/engine/command/listcmds.ame0000664000175000017500000000005312641367670023747 0ustar jaakkojaakko@summary{ List all console commands. } doomsday-stable-1.15.7/doomsday/doc/engine/command/dump.ame0000664000175000017500000000020712641367670023073 0ustar jaakkojaakko@summary{ Dump a data lump currently loaded in memory. } @description{ Params: dump (name) @cbr For example, 'dump PLAYPAL'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/setcon.ame0000664000175000017500000000017112641367670023421 0ustar jaakkojaakko@summary{ Set console and viewplayer. } @description{ Params: setcon (playernum) @cbr For example, 'setcon 1'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/after.ame0000664000175000017500000000022512641367670023227 0ustar jaakkojaakko@summary{ Execute the specified command after a delay. } @description{ Params: after (tics) (cmd) @cbr For example, 'after 35 "echo End"'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/chatnum.ame0000664000175000017500000000007512641367670023570 0ustar jaakkojaakko@summary{ Send a chat message to the specified player. } doomsday-stable-1.15.7/doomsday/doc/engine/command/stopdemo.ame0000664000175000017500000000005512641367670023761 0ustar jaakkojaakko@summary{ Stop currently playing demo. } doomsday-stable-1.15.7/doomsday/doc/engine/command/echo.ame0000664000175000017500000000022212641367670023041 0ustar jaakkojaakko@summary{ Echo the parameters on separate lines. } @description{ @params{echo (text) ...} @cbr @example{@code{echo "hello world"}} } doomsday-stable-1.15.7/doomsday/doc/engine/command/bindevent.ame0000664000175000017500000000142212641367670024104 0ustar jaakkojaakko@summary{ Bind a console command to an event. } @description{ @usage @ident{bindevent} @help_optionalarg{context} @help_arg{spec} @help_arg{command} The event specification @help_arg{spec} is composed of an event descriptor and optionally any additional conditions for the validity of the binding. The specification may be prefixed with a context name; if omitted, the binding is created in the default "game" context. @examples @pre{bindevent key-M-down "toggle ctl-run" bindevent "mouse-right-up + key-shift" @{print "RMB released while Shift down"@} bindevent shortcut:key-f8 "toggle msg-show" bindevent key-equals-down "add view-size 1" bindevent key-equals-repeat "add view-size 1"} @seealso @list{ @item @wikiterm{Bindings} } } $ @description doomsday-stable-1.15.7/doomsday/doc/engine/command/mipmap.ame0000664000175000017500000000040112641367670023405 0ustar jaakkojaakko@summary{ Set the mipmapping mode. } @description{ Params: mipmap (0-5) @cbr 0 = GL_NEAREST @cbr 1 = GL_LINEAR @cbr 2 = GL_NEAREST_MIPMAP_NEAREST @cbr 3 = GL_LINEAR_MIPMAP_NEAREST @cbr 4 = GL_NEAREST_MIPMAP_LINEAR @cbr 5 = GL_LINEAR_MIPMAP_LINEAR } doomsday-stable-1.15.7/doomsday/doc/engine/command/listcontrols.ame0000664000175000017500000000006712641367670024671 0ustar jaakkojaakko@summary{ List the names of all player controls. } doomsday-stable-1.15.7/doomsday/doc/engine/command/kick.ame0000664000175000017500000000020412641367670023044 0ustar jaakkojaakko@summary{ Kick client out of the game (server only). } @description{ Params: kick (playernum) @cbr For example, 'kick 1'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/repeat.ame0000664000175000017500000000024012641367670023403 0ustar jaakkojaakko@summary{ Repeat a command at given intervals. } @description{ Params: repeat (count) (interval) (cmd) @cbr For example, 'repeat 10 35 "screenshot"'. } doomsday-stable-1.15.7/doomsday/doc/engine/command/listbcontexts.ame0000664000175000017500000000010212641367670025025 0ustar jaakkojaakko@summary{ List all binding contexts and their current state. }doomsday-stable-1.15.7/doomsday/doc/engine/command/safebind.ame0000664000175000017500000000011112641367670023673 0ustar jaakkojaakko@summary{ Bind a command to an unless the event is already bound. } doomsday-stable-1.15.7/doomsday/doc/engine/command/listgames.ame0000664000175000017500000000004012641367670024111 0ustar jaakkojaakko@summary{ List all games. } doomsday-stable-1.15.7/doomsday/doc/engine/command/blhue.ame0000664000175000017500000000007612641367670023231 0ustar jaakkojaakko@summary{ Show/hide the hue circle for color selection. } doomsday-stable-1.15.7/doomsday/doc/engine/command/postfx.ame0000664000175000017500000000207612641367670023457 0ustar jaakkojaakko@summary{ Set or clear the frame post-processing shader. } @description{ @usage @ident{postfx} @help_arg{console} @help_arg{shader} @help_optionalarg{time} Every player has their own frame post-processing effects. The first argument specifies which player will be affected. The frame post-processing shader is changed to "fx.post.@help_arg{shader}". If @help_arg{time} is specified, and there is no shader currently in use, the new shader is faded in in @help_arg{time} seconds. Otherwise the new shader is taken immediately into use. As a special case, if @help_arg{shader} is "none", the post-processing shader is faded out and removed. Another special case is when @help_arg{shader} is "opacity". This will set the opacity of the effect to the value of @help_arg{time}. However, hote that the shader does not necessarily implement opacity as simple alpha blending. @examples Fade in the "fx.post.monochrome" shader for player 0 in 2 seconds: @code{postfx 0 monochrome 2} } doomsday-stable-1.15.7/doomsday/doc/engine/command/flareconfig.ame0000664000175000017500000000004712641367670024407 0ustar jaakkojaakko@summary{ Configure lens flares. } doomsday-stable-1.15.7/doomsday/doc/engine/command/setbpp.ame0000664000175000017500000000021712641367670023424 0ustar jaakkojaakko@summary{ Change color depth (bits per pixel), either 16 or 32. } @description{ Params: setbpp (bits) @cbr For example, 'setbpp 32'. } doomsday-stable-1.15.7/doomsday/doc/readme/0000775000175000017500000000000012641367670020015 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/readme/plugins.ame0000664000175000017500000000123712641367670022165 0ustar jaakkojaakko@chapter{ Plugins } @section{ Dehacked patches } Most features of Dehacked are supported by Doomsday's Dehacked reader. The loader will print a message during startup if an unsupported feature is used. Let's say you have the Dehacked patch @file{file.deh} in your runtime directory. Then you can use the command line option @opt{-deh file.deh} to load it at startup. If a lump named @file{DEHACKED} is found in a WAD, it will be automatically applied when the WAD is loaded. Normally only the last @file{DEHACKED} lump is used if a lump with that name is found in multiple WADs. Use the option @opt{-alldehs} to make the engine apply all found @file{DEHACKED} lumps. doomsday-stable-1.15.7/doomsday/doc/readme/winfiles.ame0000664000175000017500000000030212641367670022314 0ustar jaakkojaakkoYour runtime files are stored in a folder called @file{Doomsday Frontend} in your @file{Documents} folder. The following noteworthy files and folders can be found inside: @include{runtimefiles}doomsday-stable-1.15.7/doomsday/doc/readme/macfiles.ame0000664000175000017500000000037412641367670022270 0ustar jaakkojaakkoYour runtime files (savegames, screenshots) are stored into a folder called @file{~/Library/Application Support/Doomsday Engine/runtime}. Additional noteworthy files inside @file{~/Library/Application Support/Doomsday Engine/}: @include{runtimefiles}doomsday-stable-1.15.7/doomsday/doc/readme/wiki_templ.ame0000664000175000017500000000106312641367670022645 0ustar jaakkojaakko$@macro{TITLE}{ Doomsday Engine } @require{amestd} @macro{toc}{@apply{@.}{omit}} @macro{plugin}{[[@glue{@arg}]]} @ifdef{UNIX}{ @macro{man_opt}{ [ @apply{[[-@=]]}{@arg} ] } @macro{man_opt2}{ [ @apply{[[-@=]]}{@arg{1}} @em{@arg{2}} ] } } @begin @toc{} @ifdef{Intro}{ @require{introduction} @require{gamemodes} @require{documentation} @include{bugs} } @ifdef{Platform}{ @require{files} @require{environment} } @ifdef{MP}{ @require{network} $ TODO: more MP docs wouldn't hurt } @ifdef{Res}{ @require{resources} @include{plugins} } doomsday-stable-1.15.7/doomsday/doc/readme/resources.ame0000664000175000017500000005270012641367670022517 0ustar jaakkojaakko@chapter{ Resource files and containers } Doomsday uses multiple kinds of resource files: @list/thin{ @item Plain data (e.g., @file{.png} image, @file{.lmp} file). @item Definition files (@file{.ded}). @item Containers (@file{.wad}, @file{.pk3}). } @link{WADs}{http://en.wikipedia.org/wiki/Doom_WAD} are the original Doom engine data container format. By default all resources such as wall textures, menu graphics and fonts, background music and sound effects are loaded from WAD files. Doomsday has a mechanism that allows replacing these resources with external resource files placed in specific directories. External resource files are easy to use. They do not require making changes to any configuration or definition files. As long as a resource file is placed in the correct directory, Doomsday will load and use it automatically. Resources are divided into a number of classes. Each class has its own subdirectory under the @file{ data// } directory. The table below lists the resource classes and gives a brief description of each. @table{25 75}{ @header{Resource Class} @tab @header{Description} @row{single} Textures @tab Textures for walls and flats (floors and ceilings) @row Flats @tab Textures just for flats (floors and ceilings) @row Patches @tab Graphics for menus, fonts and sprite frames @row LightMaps @tab Textures for dynamic lights @row Music @tab Background music @row Sfx @tab Sound effects } Another example: sound effects for Doom II would be placed in the directory @file{ data/doom2/sfx/ }. $ TODO: Is the following correct? The resource class directory can have a subdirectory for each game mode. For example, textures meant only for Final Doom: Plutonia Experiment would be placed in the directory @file{ data/jdoom/textures/doom2-plut/ }. When Doomsday looks for an external resource, it first checks the current game mode's subdirectory. If no suitable resource is found there, the class directory is searched instead. $ TODO: What is this about? Move to a move appropriate place. -jk $ $ In some cases two modes can have a resource with the same name. This is a $ problem if the same resource file can't be used by both modes. @section{ Automatic loading of resources } All WAD, PK3, ZIP and LMP files placed in the @file{ data//auto/ } directory will be automatically loaded at startup. The data files are loaded in alphabetical order, including all the subdirectories of the @file{auto} directory. All DED files placed in the @file{ defs//auto/ } directory will be automatically read at startup. The definition files are also loaded in alphabetical order, including all the subdirectories of the @file{auto} directory. Virtual files (from inside a container) in the @file{auto} directories will also be loaded. @section{ Virtual directory mapping } Virtual directory mapping allows you to make the contents of one directory appear inside another directory at runtime. For example, you could have a directory called @file{MyAuto} with a set of data files somewhere on your hard drive. You could map this directory to @file{data//auto}, which would cause @file{MyAuto} to behave as though it was an auto-loading directory. A virtual directory mapping is defined using the @opt{-vdmap} option. It takes the source and destination directories as parameters. For example: @ifdef{WIN32}{ @code{-vdmap "D:\Games\MyAuto" "C:\Doomsday\Data\jDoom\Auto"} } @else{ @code{-vdmap /home/username/myauto /usr/share/doomsday/data/jdoom/auto} } You can define an unlimited number of virtual directory mappings using multiple @opt{-vdmap} options. Note, however, that @opt{-vdmap} only affects real files. It does not affect virtual files in PK3s or anywhere else. The virtual directory mappings are tried when all other methods of finding a file fail. So, all real files and virtual files override @opt{-vdmap}. @section{ PK3 files } Doomsday supports the PK3 format. PK3 files are identical to ZIP archives, with the exception of using @file{.pk3} instead of @file{.zip} as the file extension. Encrypted archives are not allowed. If you try to load an encrypted or password protected ZIP/PK3 file, you will get an error message. Wikipedia has more information about @link{PK3s}{http://en.wikipedia.org/wiki/PK3_(file_extension)}. PK3 files are loaded using the @opt{-file} option, for example @opt{-file some.pk3}. $will try loading @file{some.pk3} first from the runtime directory $and then from the data directory. A PK3 contains a set of files organized into directories. When a PK3 is loaded, all of them become virtual files that Doomsday can access just like the regular files on your hard drive. The end result is the same as if you had unpacked the PK3 into your Doomsday base directory. (No actual unpacking is done.) For example, the PK3 could have the file @file{data/jdoom/auto/superb.wad}. PK3 files can be created with just about any ZIP utility. Make sure all the files inside the archive have the correct paths, or otherwise Doomsday may not be able to find them. @subsec{ Automatic remapping inside PK3 } Files in the root of a PK3 are subject to automatic relocation based on file name extension: PK3/ZIP/LMP/WAD are mapped to @file{data//auto/} and DED goes to @file{defs//auto/}. For example, placing @file{test.ded} into the root of a PK3 has the same end result as placing @file{test.ded} into @file{defs//auto/}. Since this automatic mapping only affects single files, it is also possible to request mapping manually by adding a special prefix character to the name of a directory in the root of a PK3. If the directory begins with @file{#}, it is mapped into @file{data//auto/}; if it begins with @file{@@}, it is mapped into @file{defs//auto/}. @samp{@file{#CoolStuff/Some.pk3} => @file{data//auto/CoolStuff/Some.pk3}} @section{ WAD files } Doomsday has a number of advanced features for handling WAD files. @subsec{ Definitions inside WAD } After all DED files have been processed, the engine will check through all the loaded WAD files for lumps named @file{DD_DEFNS}. All the lumps with that name are processed just as if they were DED files, i.e. they should contain a DED file in plain text format. The @file{DD_DEFNS} lumps are applied in the order in which they have been loaded. @subsec{ WAD as a virtual file container } Another special lump used by Doomsday is @file{DD_DIREC}. It contains a table that translates file paths to lump names. An example is shown below: @samp{ @pre{FILE001 /Md2/jDoom/Some.md2 FILE002 Another.ded} } Each line in @file{DD_DIREC} contains a lump/path pair. The paths that begin with a (back)slash are interpreted as paths that start from the Doomsday base directory (set with @opt{-basedir}) and paths that don't begin with a (back)slash are located in the runtime directory. The engine will first search the @file{DD_DIREC}@nsp{s} before opening any file for reading. Note, however, that all kinds of files are not loaded using the @file{DD_DIREC}@nsp{s}: for instance demos (which are compressed with the LZSS library) must always be loaded from real files. skyjake has created a simple utility for automatically creating a WAD file that contains the current directory and all its subdirectories plus a @file{DD_DIREC} lump that has (with a high probability) a unique lump name for each file. You can invoke the utility like this: @code{ wadtool myfiles.wad /data/jdoom/textures/ } This would create a WAD file that contains all the files from the current directory. When writing the @file{DD_DIREC} table, the prefix "/data/jdoom/textures/" would be added to each file name. @bin{wadtool} is available in the Doomsday source repository under @file{/tools/wadtool}. @section{ Lump assemblies instead of WADs } The automatic loading of data files can be utilised to load directories that contain individual data lumps. This kind of a directory is called a "lump assembly" and it can be used instead of a WAD file. Note that a lump assembly can only be loaded via the autoload mechanism (but it can be inside of a PK3 that is loaded manually). By default the contents of a lump assembly are loaded in alphabetical order. However, some kinds of data require that the lumps are in a specific order (for instance map data). You have two options if you want to enforce a specific order: @list{ @item You can use name prefixes that are used to sort the lumps but are ignored when the lump names are determined. The length of the prefix can be 1..9 characters long. You specify the length of the prefix by adding an extension to the name of the directory that contains the lumps. An example that uses a prefix with 3 characters: @samp{@file{Data/Game/Auto/DirWithPrefix.3/01_LUMPNAME.lmp}} The first three characters of the file name are ignored (@file{01_}) and @file{LUMPNAME} becomes the name of the lump. @item You can put a WAD inside the assembly. } The assembly can be built using a hierarchy of directories. For example the contents of the PK3 might be: @samp{@pre{#assembly/ data1.lmp data2.lmp powerplant.2/ a-E2M3.lmp b-THINGS.lmp xyz.lmp} } @file{#assembly} would be mapped to @file{Data//Auto/assembly/}. @chapter{ Resource types } @section{ Textures and flats } Normal wall textures and flats can be replaced with PNG, JPG, TGA (Truevision Targa), or PCX (Zsoft Paintbrush) images. The engine currently supports these image formats: @table{40 15 15 15 15}{ @header{Pixel size} @tab @header{PCX} @tab @header{PNG} @tab @header{JPG} @tab @header{TGA} @row{single} 8-bit (paletted)* @tab Yes @tab Yes @tab - @tab - @row 16-bit @tab - @tab - @tab - @tab - @row 24-bit @tab - @tab Yes @tab Yes @tab Yes** @row 32-bit (alpha channel) @tab - @tab Yes @tab - @tab Yes** } @caption{* = the palette does not have to match the palette of the game @br ** = TGAs must be type 2 (uncompressed, unmapped RGB)} @notice{32-bit images are just 24-bit images with an additional 8 bits per pixel for the alpha channel.} The recommended format for high-resolution textures is paletted PNG. It is the easiest format in which to distribute the textures due to its small size. Since the palette doesn't have to be the same as the game's, it should be enough for many textures. High-resolution textures can be of any size. The engine will render them scaled so that they fit the size of the original texture. This means the aspect ratio of the new texture doesn't have to be the same as of the original texture. Note that the engine will have to resize all textures so that their dimensions are powers of two (e.g. 32, 64, 128, 256). This means TGA/PNG textures whose width and height are already powers of two can be loaded faster. Color keying allows having transparent pixels in an image format that has no alpha channel. Color keying is applied if the file name of the image ends in "-ck", for example @file{brnbigc-ck.png}. Both cyan (0,255,255) and purple (255,0,255) are used as keys, and will be replaced with transparent pixels. Examples: @list{ @item To create a high-resolution texture for the wall texture STARTAN3 you'd place a TGA file named @file{STARTAN3.tga} or a PNG file named @file{STARTAN3.png} into the @file{Textures} directory. @item In order to replace the flat FLOOR7_2, you'd to put @file{FLOOR7_2.png} into the @file{Flats} directory. It is also possible to have flats in the @file{Textures} directly if they are prefixed @file{Flat-}. For instance, @file{Flat-FLOOR7_2.tga}. } @notice{ The file names of the high-resolution textures must match the @em{texture} names, not the names of the patches that make up the textures. For example: DOOR2_5 is a patch name, DOOR3 is the texture that uses DOOR2_5. } To disable high-resolution textures use the command line option @opt{-nohightex}. The option @opt{-texdir} can be used to change the directory from which the textures are searched. @section{ Patches } Patches are images that are commonly used in game menus and intermission screens. Like textures, patches can be replaced with TGA, PNG or PCX images. The @file{Patches} resource class directory is searched using the lump names of the original patches. For example, to replace the Doom menu title, you would place the file @file{m_doom.png} to the @file{Patches} directory. The original data lumps are required even if an external resource is found, because the original data includes information about offsets and the on-screen size of the patch. This means the image from the external resource can be of any arbitrary resolution: it will be scaled to match the original patch. $ TODO: Is this correct information? $ $ Currently external patch resources are not precached, which may cause slight $ delays when the patches are first loaded. @section{ Sprite frames } Sprite frames are patches. They can be replaced with external resources just like all other patches. The same restrictions apply, though: the dimensions of the external resource do not affect the actual size of the sprite frame. This means the external resources must really be @em{high-resolution} versions of the original images. Otherwise the size and/or aspect ratio will not be right for the resource. For example, in order to replace the Doom medikit (lump name MEDIA0), one would place the file @file{media0.png} into the @file{Patches} directory. Doom uses color-mapping to change specific colors in some sprites, e.g., the players, so that the same image can be displayed with different coloring without having multiple copies of it in memory. Doomsday will not change colors on the fly in external resources. However, color-mapped versions of sprite frames can each have their own external resources. To indicate that a resource is color-mapped, its name is formed like this: @ind{@file{(patchName)-table(classNum)(tableNum).ext}} @file{(patchName)} is the sprite frame lump name. @file{(classNum)} is the number of the color translation class. This number is always zero in Doom and Heretic. In Hexen, it's the player's class (0=Fighter, 1=Cleric, 2=Mage). @file{tableNum} is the index number of the color translation table. Doom and Heretic have 4 tables, Hexen has 8. For example: @file{playa1-table01-ck.png} would be the Doom player sprite frame A1 with color table 1. The @file{-ck} suffix makes the image color keyed (i.e. special colors indicate transparent pixels). @section { Raw screens } Some background pictures are in the raw screen format, which is used to store 320 x 200 pixel paletted images. A lump containing a raw screen image (for example Heretic's TITLEPIC) is exactly 320 x 200 = 64000 bytes long. Raw screens can be replaced with external resources in the @file{Patches} directory. @section{ Light maps } Light maps are monochrome images that can be used with dynamic lights. The dimensions of a light map must be powers of two, for example 256 x 256. If the map contains an alpha channel, the actual color values are ignored; only the alpha values are significant. If the map doesn't have an alpha channel, one is generated by averaging the color values of the image. Example: If you assign the light map "Round" to a light source, images with that file name are searched from the @file{LightMaps} directory. The accepted image formats are PCX, TGA and PNG. If @file{Round.pcx}, @file{Round.tga} or @file{Round.png} is found, it will be loaded. @section{ Detail textures } Detail textures are grayscale images that are rendered on top of normal textures when walls and planes are viewed from close by. A signed-add blending is used, which lets the detail texture either darken or brighten the underlying texture: black => darker, gray => no change, white => brighter. Detail textures can be assigned to specific wall textures and flats using Detail definitions. Detail textures can be loaded from external image resources (from the @file{Textures} directory), or PCX images and raw image data stored inside a WAD lump. When using the @opt{-file} option to load detail texture lumps, the file names of the images become lump names. If an external resource is used as the detail texture, its dimensions must be powers of two (for example 128x64 or 256x256). The image file must be in one of the supported image file formats. PCX images used as detail textures must have a color depth of 8 bits and their width and height must be powers of two. The palette should be a grayscale one. It is possible to use other colors but the result can be weird due to the way the blending of detail textures is done. If the source data is a raw image, it must be either 64x64, 128x128 or 256x256 pixels in size. Raw images contain only the pixel values, (one byte per pixel) and have only one color component per pixel (they're black and white images), which means the lump or file that contains the detail texture can either be 4096, 16384 or 65536 bytes long. Using the default scaling, the pixels of detail textures are four times smaller than the pixels of regular textures. The console variables @var{rend-tex-detail}, @var{rend-tex-detail-far}, @var{rend-tex-detail-strength} and @var{rend-tex-detail-scale} control the rendering of detail textures. @section{ 3D models as particles } 3D models can be used as particles in a particle generator. In the particle generator definition, set the particle stage's type to one of the @opt{pt_model} flags. The following would make the stage use particle model number 13: @code{Type = "pt_model13";} In the particle stage definition, remember to set a color for the stage. If the color is not specified, the default values will result in a completely transparent particle model. The model definition must have a matching ID. For example, particle model number 13 uses the following ID: @code{ID = "Particle13";} For further details see the DED Reference. @section{ Music } Doomsday can play various external music files using the @link{FMOD library}{http://www.fmod.org/}. FMOD supports many music file formats including MP3, OGG, MOD and S3M (mods are a good choice due to their good quality/size ratio). External music files can be played at any time using the @cmd{playext} console command. As an alternative to FMOD there is the SDL_mixer audio plugin. It is used automatically in case the FMOD audio plugin is not installed or fails to load for some reason. However, SDL_mixer's playback quality is not as high and it does not support 3D sound effects. $@notice{ On some systems, using FMOD with Doomsday has caused lock-ups or $other unwanted behavior. If this appears to be the case for you, you should $disable FMOD with the @opt{-nofmod} option. However, this will also make it $impossible to play external music files such as MP3s. } Like other external resources, placing a music file into the @file{Music} resource class directory is enough. The file name must match the lump name of the original music data lump. For example, to replace the music for Doom's first episode's second map, the file @file{d_e1m2.mp3} would be placed in the @file{Music} directory. It is also possible to store music files into a WAD. Again, you must name the lumps so that they match the lumps of the original songs, and are thus loaded instead of them. Any music files supported by FMOD can be loaded from a WAD. Another way to specify an external resource file is to use the @opt{Ext} key of a Music definition (in Audio.ded). An example of editing the definitions: You have a terrific song called @file{song.mp3} and you'd like to hear it instead of Doom's regular "e1m2". @list/enum{ @item The first thing to decide is whether you want to play the song from where it's currently located, or do you want to move it under the Doomsday directory. In the latter case it would be easy to distribute the song and its definition file to others, since they wouldn't have to worry about where the music file is. If you decide to move the song, create a directory under the @file{ Doomsday/Data/jDoom/ } directory called @file{Music}. Another logical choice could be @file{ Doomsday/Music/ }. Then copy the song into the created directory. @item Open @file{Audio.ded} in a text editor. In it, you will find a bunch of Music definitions, including: @code{Music @{ ID = "e1m2"; Lump = "D_E1M2"; @} } In order to make the change persist over version upgrades (each one will overwrite @file{Audio.ded}) copy the definition to @file{User.ded} in the @file{ Defs/jDoom/ } directory, or create a new DED file with any name you like in the @file{ Defs/jDoom/Auto/ } directory. Everything in the @file{Auto} directory will be read automatically. If @file{User.ded} doesn't exist, just create a new file for it. @item Now you have the new Music definition, and the only thing left is to let the engine know which file it should load when the song "e1m2" is played. Edit your definition by adding the @opt{Ext} key: @ind{@pre{Music @{ ID = "e1m2"; Lump = "D_E1M2"; Ext = "Data/jDoom/Music/song.mp3"; @}}} } CD tracks can be associated with songs in a similar fashion, but instead of using the @opt{Ext} key you should use a @opt{CD track} key: @code{CD track = 3;} @section{ Sound effects } Sound samples can be replaced with WAV files. The supported formats are 8-bit and 16-bit mono PCM with no compression. The @file{Sfx} resource class directory is searched using the lump names of the original samples. For example, to replace the rocket launcher sound in @plugin{libdoom}, the file @file{dsrlaunc.wav} would be placed in the @file{Sfx} directory. Another way to specify an external resource file is to use the Sound definition @opt{Ext} key. Doomsday will automatically detect the format of a sample if it's loaded from a WAD file, making it possible to compile a WAD out of WAV samples. doomsday-stable-1.15.7/doomsday/doc/readme/documentation.ame0000664000175000017500000000211612641367670023352 0ustar jaakkojaakko@ifdef{MAN}{ @chapter{ See also } }@else{ @chapter{ Documentation } } Additional documentation is available online in the @link{Doomsday Engine Wiki}{http://dengine.net/dew/}. Information in the wiki includes: @list{ @item @link{User's Guide: collection of documentation to help play games with Doomsday}{http://dengine.net/dew/index.php?title=Category:User%27s_Guide} @item @link{Author's Guide: articles for map and resource pack/addon authors}{http://dengine.net/dew/index.php?title=Category:Author%27s_Guide} @item @link{Programmer's Guide: technical documentation for software developers and Deng Team members}{http://dengine.net/dew/index.php?title=Category:Programmer%27s_Guide} @item @link{Definitions Reference: DED and finale script syntax}{http://dengine.net/dew/index.php?title=DED} @item @link{Command Line Options Reference}{http://dengine.net/dew/index.php?title=Command_line_options} @item @link{Version history}{http://dengine.net/dew/index.php?title=Category:Releases} @item @link{Project roadmap and features in planning}{http://tracker.skyjake.fi/projects/deng/roadmap} } doomsday-stable-1.15.7/doomsday/doc/readme/bugs.ame0000664000175000017500000000101012641367670021431 0ustar jaakkojaakko@chapter{ Known issues } Doomsday is a work in progress, so there usually is a number of known issues that will be addressed in the future. @deflist{ @item{Bugs and Features} The official place to report new bugs, submit feature requests and browse the existing reports is the @link{Deng Team's own Issue Tracker}{http://tracker.skyjake.fi/}. @item{Multiplayer Issues} There is a to-do list of known @link{multiplayer issues and needed enhancements}{http://dengine.net/multiplayer_issues} maintained by skyjake. } doomsday-stable-1.15.7/doomsday/doc/readme/runtimefiles.ame0000664000175000017500000000102612641367670023206 0ustar jaakkojaakko@deflist{ @item{@file{addons/}} Directory where to put addons (@file{.wad}, @file{.pk3}, Snowberry @file{.addon}, etc.) so that they appear in the launcher's list. @item{@file{runtime/client.id}} Unique client identifier. Used to identify clients in a multiplayer game. Delete this file to generate a new identifier, for instance if you're getting a @dquote{duplicate ID} error. @item{@file{runtime/configs/}} Saved values for console variables and control bindings. @item{@file{runtime/doomsday.out}} The Doomsday message log. }doomsday-stable-1.15.7/doomsday/doc/readme/commandline.ame0000664000175000017500000001530012641367670022766 0ustar jaakkojaakko@chapter{ Running from the command line } It is possible to launch Doomsday directly from the command line. If all data files can be found under the default directories, the engine should be able to launch itself without further aid. If not, command line options must be used to configure data file locations and other settings. To see which options are used by default, you can preview the command line in the Doomsday Frontend (e.g., by right-clicking the Play button). For historical reasons, Doomsday has never considered the command line the primary launch method. Consequently, it currently lacks convenient things like a @opt{--help} option for aiding command line usage. For more information about command line options, see the @link{Command Line Options Reference} {http://dengine.net/dew/index.php?title=Command_line_options}. $* TODO: Much of the following is outdated. Ringzero makes life simpler even with a pure command line setup. The parts that are still relevant should be revised. @section{ File paths on the command line } There are a few things you should know about the handling of relative paths. First of all, these are the directories that the engine is working with: @list{ @item Doomsday root/base directory (for example @file{ C:\Doomsday\ }). Everything relating to the Doomsday engine is under this directory. @item Working/runtime directory (for example @file{ C:\Doomsday\Run\x\ }). This is the directory where the engine spends it time when a game is running. @item Data directory (for example @file{ C:\Doomsday\Data\x\ }). WAD files and external resources are loaded from here. } The @opt{-basedir} option tells the engine where the base directory is in relation to the runtime directory (or it's the the absolute path of the base directory, e.g. @file{ C:\Doomsday\ }). The principle is that @opt{-basedir} only affects built-in default paths and relative paths in DED files. @opt{-basedir} does not affect relative paths in command line options. If a relative path is given on the command line, it is first searched in relation to the runtime directory. For example, the option @opt{-file doom.wad} will make the engine read the file @file{doom.wad}, which is located in the runtime directory. In the default configuration this is @file{ C:\Doomsday\Run\jDoom\ }. If the file is not found in the runtime directory, the data directory is searched instead. KickStart automatically uses a @opt{-basedir} of @file{..\..}, which means the root directory is the 'grandparent' of the runtime directory. The option @opt{-sbd} (@opt{-stdbasedir}) is the equivalent of @opt{-basedir ..\..}. There is one exception, though. With the @opt{-file} (and @opt{-iwad}) options a relative path can begin with a greater-than character (>) or a closing brace character (@}). When the engine loads the WAD file in question, the > or @} character is replaced with the path specified by @opt{-basedir}. For example, @opt{-file >Data\Doom.wad} would make the engine load a WAD named @file{ C:\Doomsday\Data\Doom.wad } (assuming the default Doomsday root @file{ C:\Doomsday\ }). Note that if you're executing @file{Doomsday.exe} from the command line or from a DOS batch file, you must enclose the file names that contain a > character in quotes or otherwise DOS will think you're trying to redirect output. In response files it doesn't matter if there are quotes or not. (If you really are trying to redirect output, you should use the @opt{-out} option.) The default launch method (used by KickStart 1.6) is a runtime directory oriented approach. KickStart will change the current working directory to the Game's runtime folder and execute @file{Doomsday.exe} from there, with the @opt{-basedir ..\..} option. This way the engine will use the appropriate runtime folder as the working directory, but will also know where the Doomsday root directory is by adding the base directory's @file{..\..} to default path names (like fonts and definitions files). There is an alternative approach, which could be called an executable oriented approach. @file{Doomsday.exe} is executed in the @file{Bin} directory, with the options @opt{-userdir -basedir }. @file{} can be a relative or an absolute path to the correct runtime directory. Again, @opt{-basedir} tells the engine where the root directory is, using an absolute path or in relation to @file{}. @opt{-userdir} will make the engine run in the given directory, i.e. it specifies the runtime directory. Note that @opt{-game} and @opt{-gl} work a bit differently because their arguments are directly passed on to the Win32 routine @cmd{LoadLibrary}. You should either omit the path entirely (e.g. @opt{-game jHeretic.dll}) or use a full path to the DLL (e.g. @opt{-game C:\Doomsday\Bin\jHeretic.dll}), no matter where you're executing @file{Doomsday.exe}. The default place where you should put your IWADs is @file{ Data\\ }. jDoom, jHeretic and jHexen will by default look for IWADs in @file{ Data\\ }, @file{ Data\ }, the base directory and the runtime directory, in that order. *$ @section{ Loading IWADs } IWAD files must be specified with the @opt{-iwad} option. You can either provide the path to the IWAD to use, or the path of the directory under which IWADs are located. Doomsday will look through the specified location and use all the recognized IWADs automatically. In other words, you can have the IWADs for all games stored in a single directory; Doomsday will use the appropriate one for each launched game. $ TODO: Is it allowed to use more than one -iwad option? There are alternative ways to specify IWADs: see @ref{environ}{Environment}. @section{ The @opt{-file} option } @a{fileopt} The @opt{-file} (or @opt{-f}) option is used to load WAD files and other data files from the command line. @note{Paths given to @opt{-file} are relative to the runtime directory; see the default in @ref{files}{Files}.} In addition to normal WAD files, the @opt{-file} option can be used to load any type of data files, for instance PCX images. An example: @samp{@opt{-file image.pcx}} This would load the file @file{image.pcx} from the runtime directory (or the data directory). When loading files in this manner, the engine will treat the file as if it was a WAD file with a single data lump. The lump gets its name from the base of the file name, which in the example's case would be @file{IMAGE}. Anyone can then refer to the data lump using that name, just as if it was included in a WAD file. $* TODO: Document unloading. Any file loaded with the @opt{-file} option can't be unloaded from memory at runtime using the @cmd{unload} command. This is mainly a precaution, since unloading the main WAD file of the game or any data related to it would lead to fatal errors. *$ doomsday-stable-1.15.7/doomsday/doc/readme/environment.ame0000664000175000017500000000060712641367670023050 0ustar jaakkojaakko@chapter{ Environment } @a{environ} The following environment variables are recognized by Doomsday. @deflist{ @item{DOOMWADDIR} DOOM WAD directory. Doomsday looks for WAD files in this directory. @item{DOOMWADPATH} Delimited set of DOOM WAD directories. Use @ifdef{WIN32}{semicolon (;)}@else{colon (:)} to separate directories. Supported WAD files are searched for in each directory. } doomsday-stable-1.15.7/doomsday/doc/readme/network.ame0000664000175000017500000000776712641367670022213 0ustar jaakkojaakko@chapter{ Multiplayer } Doomsday features client/server based multiplayer for up to 15 players. Multiplayer games can be joined using the in-game GUI (under the @dquote{New Game} main menu option) or using the console. @section{ Modes } The following multiplayer modes are available in @plugin{libdoom}, @plugin{libheretic} and @plugin{libhexen}: @list/thin{ @item Cooperative @item Deathmatch @item Team Deathmatch } Deathmatch and Cooperative can be set directly in the GUI or console. For Team Deathmatch set "No Team Damage" to yes and in Player Setup make sure everybody on your team is the same color. Doom offers two variants of Deathmatch: @deflist{ @item{Deathmatch 1} Ammo and powerups do not respawn. Non-dropped (i.e those from bad guys) weapons remain, but can only be picked up once per life. @item{Deathmatch 2} All non-dropped pickups, including weapons randomly respawn a short while after being picked up. Respawned weapons can be picked up more than once per life. } Deathmatch in Heretic and Hexen is limited to the "Deathmatch 1" mode. @section{ Online games } When joining games on the internet, please note the following: @list{ @item The network code in this release has some @link{limitations}{http://dengine.net/multiplayer_issues} that we will work on in future versions. @item The connection should be able to sustain 100KB/s transfer rate (1 Mbps or better). } @section{ Hosting a game } Use @ifdef{UNIX}{ @strong{doomsday-shell} or @strong{doomsday-shell-text} } @else{ @strong{Doomsday Shell}} to host a multiplayer game of your own. The @wikiterm{Shell}{Shell} allows you to start, stop, and control Doomsday multiplayer servers. Using the Shell, you can connect both to local servers and remote servers over the internet. @strong{doomsday-server} runs in a background process and has no UI of its own. You must use the Shell to view or manipulate it. Presently you must use the Shell's Console (text-mode command line interface) to configure the server. For instance, the following would set up a deathmatch without monsters in E1M5: @samp{@pre{server-game-deathmatch 1 server-game-nomonsters 1 setmap 1 5}} Note that you can set up a @file{.cfg} file where you can define the server configuration and @wikilink{automatically open the server}{Multiplayer_server}. If your server is not public (@opt{server-public}), a client can connect to it using a custom address search: @list{ @item Server on the same computer or LAN: servers on the local network should be discovered automatically and are visible immediately in the Custom Search results list. @item Server somewhere on the internet: enter the server's IP address or domain name into the search address field. } @section{ Networking details } Doomsday uses TCP network connections for multiplayer games. If you host a game and are behind a firewall or using NAT, you must make sure that other computers are able to open TCP connections to your computer. This entails opening the appropriate incoming TCP ports on your firewall and/or configuring the NAT so that the correct ports are routed to your computer. Additionally, UDP ports 13209-13224 are used for detecting servers running on the local network; if you are experiencing problems with autodetecting local servers, check that your firewall isn't blocking these UDP ports on either end. You can see information about the network subsystem status with the command: @code{net info} @subsec{Server} A server opens one TCP port for listening to incoming connections. The port number is configured with the console variable @var{net-ip-port}. By default a server uses TCP port 13209 (setting the port to zero will mean 13209 will be used). The configured port must be open for incoming TCP traffic in the firewall. @subsec{Client} Clients do not require any firewall configuration for incoming connections. A client only needs to be able to reach the server via the server's TCP port. A server running on the same computer can be connected to with the following command: @code{connect localhost} doomsday-stable-1.15.7/doomsday/doc/readme/files.ame0000664000175000017500000000025712641367670021607 0ustar jaakkojaakko@chapter{ Files } @a{files} @ifdef{MACOSX}{ @include{macfiles} } @else{ @ifdef{UNIX}{ @include{unixfiles} } @else{ @include{winfiles} } } doomsday-stable-1.15.7/doomsday/doc/readme/readme.ame0000664000175000017500000000124012641367670021733 0ustar jaakkojaakko@ifndef{MAN}{ @macro{TITLE}{ Doomsday Engine } } @else{ @macro{TITLE}{ doomsday } } @macro{ONELINER}{ Enhanced source port of Doom, Heretic and Hexen } @macro{VERSION}{ Version 1.15.7 } @macro{AUTHOR}{ Deng Team } @macro{LINK}{ http://dengine.net/ } @require{amestd} $ Readme-specific formatting macros. @macro{plugin}{@arg} @begin @include{introduction} @ifndef{MAN}{@include{documentation}} @include{gamemodes} @include{files} @include{environment} @include{network} @include{resources} @include{plugins} @include{bugs} @ifdef{MAN}{ $ See also @include{documentation} $ Man pages have an author section. @include{../author} } @include{credit} doomsday-stable-1.15.7/doomsday/doc/readme/credit.ame0000664000175000017500000000335712641367670021763 0ustar jaakkojaakko@chapter{ Acknowledgements } @a{acks} @strong{id Software} created DOOM and then released its source code. @strong{Raven Software} created Heretic and Hexen and released their source code. @strong{Jaakko Keränen} started the Doomsday Engine project and is the lead developer of the Deng Team. @strong{Daniel Swanson} is a developer and lead graphics artist in the Deng Team, author of the @link{dengine.net}{http://dengine.net} website implementation and former maintainer of the jDoom Resource Pack. @strong{Dave Gardner} is a member of the Deng Team, maintainer of high-resolution texture packs, and contributor to the @link{dengine.net}{http://dengine.net} website implementation. @strong{Vermil} regularly provides in-depth feedback and bug reports and is an expert in all things related to DOOM-based games. @strong{Kees Meijs} packaged Doomsday for Debian and hosted an Apt repository of Debian packages. @strong{Andrew Apted} wrote @link{glBSP}{http://glbsp.sourceforge.net/}. @strong{Graham Jackson} helped with the source code, fixed Doom bugs and did a lot of testing. @strong{David Jarvis} did early network testing with jDoom and jHeretic and generously contributed essential computer hardware components. @strong{William Mull} hosted the project's websites for many years. Finally, the resource pack community, particularly mentioning: @strong{Abbs} maintained the jDoom model pack and did wonderful work on the models and particle effects; @strong{Anton Rzheshevski} (aka Cheb) created player weapon 3D models and other MD2 modifications/enhancements, maintained the jDoom model pack and wrote KickStart version 2; @strong{Greg Fisk} (Slyrr) authored many excellent 3D models for jHeretic; @strong{Daniel Norton} created detail textures for jDoom. doomsday-stable-1.15.7/doomsday/doc/readme/features.ame0000664000175000017500000001036112641367670022320 0ustar jaakkojaakko@section{ Features } User interface: @list/thin{ @item Overlaid @wikiterm{task bar} for easy access to engine features @item In-game @wikiterm{command console} @item Configuration menus and @wikiterm{Renderer Appearance sidebar} editor @item @wikilink{Game selection screen}{Ringzero_GUI} for runtime @wikiterm{game} changes (e.g., from DOOM to Heretic), browsing multiplayer games, and loading saved games @item On-the-fly @wikiterm{add-on} resource loading @item Flexible input control @wikiterm{bindings} system @item Built-in @wikiterm{updater} for easy upgrades } Graphics: @list/thin{ @item OpenGL based renderer @item Dynamic @wikiterm{ambient occlusion} (corner shadowing) for world surfaces @item @wikiterm{Dynamic lights} with @wikiterm{halos} and @wikiterm{lens flares} @item Dynamic shadowing effects for world objects @item @wikiterm{Particle effects} system @item 3D models for world objects (with per-vertex lighting and multiple light sources), @wikiterm{skies}, @wikiterm{skyboxes}, and @wikiterm{particles} @item Automatical world surface (light) @wikiterm{decorations} @item @wikiterm{Detail texturing}, shine and @wikiterm{glowing} effects for world surfaces @item @wikiterm{Fogging}, @wikiterm{bloom}, and @wikiterm{vignette} effects @item Environmental mapping effects for 3D models and world surfaces @item World @wikiterm{movement smoothing} (actors, monsters, missiles, surfaces) to remove the original games' limitation of 35 FPS @item @wikiterm{Smart texture filtering} using a modified hq2x algorithm @item Stereoscopic rendering modes: anaglyph, side-by-side, cross-eye and parallel viewing @item Support for Oculus Rift } Resources: @list/thin{ @item Flexible containters: @wikiterm{WAD}, @wikiterm{ZIP}, @wikiterm{native folder}: any resource can be loaded from any container type @item @wikiterm{High-resolution textures}: @wikiterm{PNG}, @wikiterm{JPG}, @wikiterm{TGA}, @wikiterm{PCX} @item @wikiterm{3D models}: @wikiterm{MD2}, @wikiterm{DMD} with LOD support @item External @wikiterm{music files} in MP3 and other formats @item Plain text @wikiterm{definitions} that all share the same syntax @item Internal BSP builder (originally based on glBSP) } Audio: @list/thin{ @item Plugin based driver architecture @item Uses @link{FMOD Ex}{http://www.fmod.org/} for audio playback (sound effects, music, CD audio tracks) @item Supports the open source SDL_mixer for sound effects and music files @ifndef{WIN32}{@item FluidSynth for MIDI playback using @wikiterm{SF2} soundfonts} @ifdef{WIN32}{@item DirectSound3D, EAX 2.0, Windows Multimedia (for MIDI)} @ifdef{MACOSX}{@item QuickTime for music playback} @ifdef{UNIX}{@item @wikiterm{OpenAL}} @item External @wikiterm{music files} in MP3 and other formats @item 3D positional sound effects @item Environmental @wikiterm{echo and reverb} effects @item Runtime @wikiterm{sound effect resampling} to 22/44 KHz with 8/16 bits } Multiplayer: @list/thin{ @item TCP-based client/server @wikilink{networking}{Multiplayer_(Readme)} @item Automatical discovery of servers running on the local network @item Central @wikiterm{master server} for discovery of servers on the internet @item Standalone @wikiterm{server} running as a daemon/background process @item Standalone @wikilink{Doomsday Shell}{Shell} tool for server management (both local and remote) @item Supports up to @wikiterm{15 player games} @item Clients can join games in progress @item In-game chat and server management (via a shell login) } Other: @list/thin{ @item Open source: software developers should visit the @wikilink{Getting Started page}{Getting_started} @item @wikilink{Cross platform}{Supported_platforms} @item @wikiterm{Plugin} based @wikiterm{extensible architecture} @item @wikiterm{Snowberry}: GUI frontend based on game profiles; for managing custom maps, resource packs, add-ons, and starting different game configurations easily (written in Python) } @section{ Requirements } @list{ @item At least one WAD file from the original Doom, Heretic, Hexen, or other supported game @ifndef{UNIX}{ @item Minimum OS version: @ifdef{WIN32}{Windows Vista} @ifdef{MACOSX}{Mac OS X 10.6} } @ifdef{WIN32}{@item DirectX 8 (or newer)} @ifdef{MACOSX}{@item Supported CPU architectures: 64-bit/32-bit Intel} @item A display adapter capable of OpenGL 2.0 hardware acceleration } doomsday-stable-1.15.7/doomsday/doc/readme/gamemodes.ame0000664000175000017500000000266112641367670022447 0ustar jaakkojaakko@chapter{ Game modes } @a{gamemode} One game plugin, such as @plugin{libdoom}, is able to run in many different modes. Each mode emulates a specific version of the original game and typically has its own IWAD file. Below is a list of all the game modes supported by the game plugins distributed with Doomsday: @plugin{libdoom} (previously called jDoom), @plugin{libheretic} (jHeretic) and @plugin{libhexen} (jHexen). @table{19 25 56}{ @header{Plugin} @tab @header{Game Mode} @tab @header{Description} @row{single} @plugin{libdoom} @tab doom1-share @tab Shareware Doom v1.9 @row @tab doom1 @tab Registered Doom v1.9 @row @tab doom1-ultimate @tab Ultimate Doom* @row @tab doom2 @tab Doom 2 @row @tab doom2-plut @tab Final Doom: Plutonia Experiment @row @tab doom2-tnt @tab Final Doom: TNT Evilution @row @tab chex @tab Chex Quest @row @tab hacx @tab HacX @row @plugin{libheretic} @tab heretic-share @tab Shareware Heretic @row @tab heretic @tab Registered Heretic @row @tab heretic-ext @tab Heretic: Shadow of the Serpent Riders** @row @plugin{libhexen} @tab hexen @tab Hexen v1.1 @row @tab hexen-v10 @tab Hexen v1.0 @row @tab hexen-dk @tab Hexen: Death Kings of Dark Citadel @row @tab hexen-demo @tab The 4-level Hexen Demo } @caption{* = has a 4th episode @br ** = has episodes 4 and 5} To help @plugin{libhexen} recognize the IWAD of the 4-level Hexen (beta) demo, you should rename the IWAD to @file{hexendemo.wad}. doomsday-stable-1.15.7/doomsday/doc/readme/introduction.ame0000664000175000017500000000021212641367670023215 0ustar jaakkojaakko@ifdef{UNIX}{ @include{unixintro} } @else { @toc @chapter{ Introduction } @include{introbrief} @include{features} } doomsday-stable-1.15.7/doomsday/doc/readme/unixfiles.ame0000664000175000017500000000250712641367670022513 0ustar jaakkojaakko@deflist{ @item{@file{/etc/doomsday/defaults}} System-level default configuration. This is checked after @file{~/.doomsday/defaults}. For example: @samp{@pre{audio fluidsynth @{ driver: pulseaudio @}}} @item{@file{/etc/doomsday/paths}} System-level paths configuration (basedir, libdir, iwaddir). This is checked after @file{~/.doomsday/paths}. See the Doomsday Wiki for details (@dquote{Configuration}). @item{@file{/usr/share/doomsday/}} Read-only files for the launcher, engine and game plugins. @item{@file{~/.doomsday/}} Location for the user-specific files. @item{@file{~/.doomsday/defaults}} User's own defaults (see above). @item{@file{~/.doomsday/paths}} User's paths configuration (basedir, libdir, iwaddir). @item{@file{~/.doomsday/addons/}} Directory where to put addons (WAD, PK3, Snowberry @file{.addon}, etc.) so that they appear in the launcher's list. @item{@file{~/.doomsday/runtime/}} Doomsday's runtime files, e.g., message log (@file{doomsday.out}), savegames, and screenshots. @item{@file{~/.doomsday/runtime/client.id}} Unique client identifier. Used to identify clients in a multiplayer game. Delete this file to generate a new identifier, for instance if you're getting a @dquote{duplicate ID} error. @item{@file{~/.doomsday/runtime/configs/}} Saved values for console variables and control bindings. }doomsday-stable-1.15.7/doomsday/doc/readme/unixintro.ame0000664000175000017500000000315112641367670022540 0ustar jaakkojaakko$ Unix manual page: Unix-specific main portion of the readme. @chapter{Synopsis} @strong{doomsday} @man_opt2{iwad}{dir} @man_opt2{game}{mode} @man_opt{wnd} @man_opt2{wh}{w h} @man_opt{v} @man_opt2{file}{file ...} Note that the command line is not interpreted according to GNU conventions. Everything following @opt{--} is ignored. @opt{@@} can be used to specify a response file whose contents are added to the command line. @chapter{Options} @deflist/thin{ @item{@opt{-iwad}} Specifies a directory where to look for IWAD files. Searches for IWADs from all known games and automatically loads them when needed. @item{@opt{-game}} Sets the game to load after startup. See @ref{gamemode}{Game modes} for a list of available games. For example: @samp{@opt{-game doom1-ultimate}} If @opt{-game} is not specified, Doomsday will start in @dquote{ringzero} mode: a plain console with no game loaded. @item{@opt{-wnd}} Starts in windowed mode (also @opt{-window}). The default is to start in fullscreen mode. @item{@opt{-wh}} Sets the size of the Doomsday window. In fullscreen mode specifies which display resolution to use. @item{@opt{-v}} Print verbose log messages (also @opt{-verbose}). Specify more than once for extra verbosity. @item{@opt{-file}} Specify one or more resource files (WAD, LMP, PK3) to load at startup. More files can be loaded at runtime with the @cmd{load} command. } More command line options are listed in the @link{Options Reference} {http://dengine.net/dew/index.php?title=Command_line_options_reference} in the Doomsday Engine Wiki. @toc @chapter{Description} @include{introbrief} @include{features} doomsday-stable-1.15.7/doomsday/doc/readme/introbrief.ame0000664000175000017500000000334012641367670022644 0ustar jaakkojaakkoThe Doomsday Engine is a @dquote{source port} of id Software's Doom and Raven Software's Heretic and Hexen, which were popular PC FPS games in the early-to-mid 1990s. Doomsday enhances these classic games with many features including 3D graphics, fully customizable controls and client/server networking, making them more accessible to modern gamers. However, the feel of the original games has been kept intact, ensuring sublime nostalgia or an exciting introduction into the pioneering games of the genre. Doomsday and the associated ports of Doom, Heretic and Hexen have been in development since 1999; the first versions were released in late 1999 and early 2000. Several people have been involved in the project (see @ref{acks}{Acknowledgements}). @ifdef{MACOSX}{ @section{How to install} The @strong{Doomsday.pkg} installer package contains two applications: @list{ @item @strong{Doomsday Engine}: launcher frontend for starting Doomsday @item @strong{Doomsday Shell}: tool for running and monitoring your own multiplayer servers (introduced in version 1.10) } We also provide the @strong{Doomsday Engine} and @strong{Doomsday Shell} applications as individual downloads that you can drag and drop anywhere you like. You can find these in the @link{Build Repository}{http://dengine.net/builds}. However, the automatic updater will always download a full installer package. The first time you start the frontend, a setup wizard will guide you through setting up some game profiles. To uninstall, move @strong{Doomsday Engine} and @strong{Doomsday Shell} to the Trash. If you wish to also delete your frontend game profiles, savegames and other persistent data, delete the @file{~/Library/Application Support/Doomsday Engine} folder. } doomsday-stable-1.15.7/doomsday/doc/server/0000775000175000017500000000000012641367670020066 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/server/server.ame0000664000175000017500000000046612641367670022066 0ustar jaakkojaakko$ Man page for doomsday-server @macro{TITLE}{ doomsday-server } @macro{ONELINER}{ Doomsday Engine multiplayer daemon } @macro{VERSION}{ Version 1.15.7 } @macro{AUTHOR}{ Deng Team } @macro{LINK}{ http://dengine.net/ } @require{amestd} $ Extra formatting macros. @macro{plugin}{@arg} @begin @require{manpage} doomsday-stable-1.15.7/doomsday/doc/server/wiki_templ.ame0000664000175000017500000000040312641367670022713 0ustar jaakkojaakko@require{amestd} @macro{toc}{@apply{@.}{omit}} @macro{plugin}{[[@glue{@arg}]]} @ifdef{UNIX}{ @macro{man_opt}{ [ @apply{[[-@=]]}{@arg} ] } @macro{man_opt2}{ [ @apply{[[-@=]]}{@arg{1}} @em{@arg{2}} ] } } @begin @toc{} @require{manpage} doomsday-stable-1.15.7/doomsday/doc/server/manpage.ame0000664000175000017500000000367312641367670022173 0ustar jaakkojaakko@chapter{ Synopsis } @strong{doomsday-server} @man_opt{stdout} @man_opt2{port}{tcp-port} @man_opt2{iwad}{dir} @man_opt2{game}{mode} @man_opt{v} @man_opt2{file}{file ...} Note that the command line is not interpreted according to GNU conventions. Everything following @opt{--} is ignored. @opt{@@} can be used to specify a response file whose contents are added to the command line. @chapter{ Options } @deflist/thin{ @item{@opt{-stdout}} Prints all log entries to the standard output. If this option is not used, nothing is printed so that the server can be run as a background process. @item{@opt{-port}} TCP port that the server listens to for incoming connections. @item{@opt{-iwad}} Specifies a directory where to look for IWAD files. Doomsday will search for IWADs from all known games and automatically load them when needed. Note that you can also use the DOOMWADDIR environment variable or the @file{paths} configuration file to specify the location of the IWAD files. @item{@opt{-game}} Sets the game to load after startup. See doomsday(6) for a list of available games. For example: @samp{@opt{-game doom1-ultimate}} If @opt{-game} is not specified and the server is unable to choose a game automatically, the server will quit. @item{@opt{-v}} Print verbose log messages (also @opt{-verbose}). Specify more than once for extra verbosity. @item{@opt{-file}} Specify one or more resource files (WAD, LMP, PK3) to load at startup. More files can be loaded at runtime with the @cmd{load} command. } In addition to these, @bin{doomsday-server} supports many of the command line options of doomsday(6). @chapter{ Operating a server } Doomsday servers are, by default, silent daemon processes intended to be run in the background. You need to use the Doomsday Shell to monitor their status and control them. @chapter{ See Also } doomsday(6), doomsday-shell-text(6) @ifndef{WIKI}{ $ Man pages have an author section. @include{../author} } doomsday-stable-1.15.7/doomsday/doc/libheretic/0000775000175000017500000000000012641367670020672 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/libheretic/variable/0000775000175000017500000000000012641367670022457 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/libheretic/variable/server-game-episode.ame0000664000175000017500000000006512641367670027007 0ustar jaakkojaakko@summary{ Episode to use in multiplayer games. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/game-monsters-stuckindoors.ame0000664000175000017500000000012012641367670030442 0ustar jaakkojaakko@summary{ 1=Monsters can get stuck in doortracks (disables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/server-game-plane-fixmaterialscroll.ame0000664000175000017500000000016112641367670032175 0ustar jaakkojaakko@summary{ 1=Fix bug in original Heretic which would only allow scrolling materials on planes to move east. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-cheat-counter.ame0000664000175000017500000000010712641367670026460 0ustar jaakkojaakko@summary{ 6-bit bitfield. Show kills, items and secret counters. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/server-game-respawn-monsters-nightmare.ame0000664000175000017500000000010612641367670032656 0ustar jaakkojaakko@summary{ 1=Monster respawning in Nightmare difficulty enabled. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/game-player-wallrun-northonly.ame0000664000175000017500000000011212641367670031054 0ustar jaakkojaakko@summary{ 1=Players can only wallrun North (disables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/ctl-inventory-mode.ame0000664000175000017500000000007512641367670026704 0ustar jaakkojaakko@summary{ Inventory selection mode 0=cursor, 1=scroll. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/game-objects-hangoverledges.ame0000664000175000017500000000012612641367670030475 0ustar jaakkojaakko@summary{ 1=Only some objects can hang over tall ledges (enables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-artifact-crystalvial.ame0000664000175000017500000000006112641367670030562 0ustar jaakkojaakko@summary{ Current number of Crystal Vials. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/game-objects-clipping.ame0000664000175000017500000000011412641367670027302 0ustar jaakkojaakko@summary{ 1=Use EXACTLY DOOM's clipping code (disables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-order8.ame0000664000175000017500000000005512641367670027127 0ustar jaakkojaakko@summary{ Weapon change order, slot 8. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-artifact-ring.ame0000664000175000017500000000007212641367670027166 0ustar jaakkojaakko@summary{ Current number of Rings of Invincibility. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-hellstaff.ame0000664000175000017500000000005512641367670027674 0ustar jaakkojaakko@summary{ 1=Player has the Hell Staff. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-goldwand.ame0000664000175000017500000000004712641367670027524 0ustar jaakkojaakko@summary{ 1=Player has goldwand. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-ammo-crossbow.ame0000664000175000017500000000007112641367670027223 0ustar jaakkojaakko@summary{ Current amount of ammo for the crossbow. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/game-save-auto-loadonreborn.ame0000664000175000017500000000011412641367670030434 0ustar jaakkojaakko@summary{ 1=Load the auto save slot on player reborn. (default: off). } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-gauntlets.ame0000664000175000017500000000005012641367670027725 0ustar jaakkojaakko@summary{ 1=Player has gauntlets. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-staff.ame0000664000175000017500000000004412641367670027025 0ustar jaakkojaakko@summary{ 1=Player has staff. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-artifact-firebomb.ame0000664000175000017500000000007612641367670030020 0ustar jaakkojaakko@summary{ Current number of Time Bombs Of The Ancients. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-inventory-slot-max.ame0000664000175000017500000000012212641367670027513 0ustar jaakkojaakko@summary{ Maximum number of inventory slots to display in full-screen mode. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-artifact-tomeofpower.ame0000664000175000017500000000007312641367670030576 0ustar jaakkojaakko@summary{ Current number of Tome of Power artifacts. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/xg-dev.ame0000664000175000017500000000005312641367670024333 0ustar jaakkojaakko@summary{ 1=Print XG debug messages. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-ammo-phoenixrod.ame0000664000175000017500000000007412641367670027544 0ustar jaakkojaakko@summary{ Current amount of ammo for the Phoenix Rod. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-order6.ame0000664000175000017500000000005512641367670027125 0ustar jaakkojaakko@summary{ Weapon change order, slot 6. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/game-corpse-time.ame0000664000175000017500000000007312641367670026301 0ustar jaakkojaakko@summary{ Corpse vanish time in seconds, 0=disabled. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-order5.ame0000664000175000017500000000005512641367670027124 0ustar jaakkojaakko@summary{ Weapon change order, slot 5. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/game-objects-falloff.ame0000664000175000017500000000011612641367670027110 0ustar jaakkojaakko@summary{ 1=Objects fall under their own weight (enables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-keys.ame0000664000175000017500000000007312641367670024674 0ustar jaakkojaakko@summary{ 1=Show keys when the status bar is hidden. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/ctl-inventory-use-next.ame0000664000175000017500000000011612641367670027524 0ustar jaakkojaakko@summary{ 1=Automatically select the next inventory item when unusable. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/game-corpse-sliding.ame0000664000175000017500000000013212641367670026770 0ustar jaakkojaakko@summary{ 1=Corpses slide down stairs and ledges (enables enhanced BOOM behaviour). } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-order4.ame0000664000175000017500000000005512641367670027123 0ustar jaakkojaakko@summary{ Weapon change order, slot 4. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-order7.ame0000664000175000017500000000005512641367670027126 0ustar jaakkojaakko@summary{ Weapon change order, slot 7. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/game-zclip.ame0000664000175000017500000000012412641367670025170 0ustar jaakkojaakko@summary{ 1=Allow mobjs to move under/over each other (enables DOOM bug fix). } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-artifact-wings.ame0000664000175000017500000000007412641367670027360 0ustar jaakkojaakko@summary{ Current number of Wings of Wrath artifacts. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/view-ringfilter.ame0000664000175000017500000000007012641367670026255 0ustar jaakkojaakko@summary{ Ring effect filter. 1=Brownish, 2=Blue. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-ammo.ame0000664000175000017500000000007312641367670024652 0ustar jaakkojaakko@summary{ 1=Show ammo when the status bar is hidden. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-health.ame0000664000175000017500000000007512641367670025170 0ustar jaakkojaakko@summary{ 1=Show health when the status bar is hidden. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-key-yellow.ame0000664000175000017500000000005112641367670026532 0ustar jaakkojaakko@summary{ 1=Player has yellow key. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-ammo-hellstaff.ame0000664000175000017500000000007312641367670027334 0ustar jaakkojaakko@summary{ Current amount of ammo for the Hell Staff. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-key-green.ame0000664000175000017500000000005012641367670026316 0ustar jaakkojaakko@summary{ 1=Player has green key. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/view-bob-weapon-switch-lower.ame0000664000175000017500000000007412641367670030572 0ustar jaakkojaakko@summary{ HUD weapon lowered during weapon switching. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/server-game-maulotaur-fixfloorfire.ame0000664000175000017500000000020312641367670032056 0ustar jaakkojaakko@summary{ 1=Fix bug in original Heretic which would explode the Maulotaur floor fire if spawned while it's feet are clipped. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-mace.ame0000664000175000017500000000004312641367670026626 0ustar jaakkojaakko@summary{ 1=Player has mace. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-tome-sound.ame0000664000175000017500000000007612641367670026016 0ustar jaakkojaakko@summary{ Seconds for countdown sound of Tome of Power. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-artifact-chaosdevice.ame0000664000175000017500000000006112641367670030502 0ustar jaakkojaakko@summary{ Current number of Chaos Devices. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-ammo-mace.ame0000664000175000017500000000006512641367670026272 0ustar jaakkojaakko@summary{ Current amount of ammo for the mace. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-tome-timer.ame0000664000175000017500000000007112641367670026001 0ustar jaakkojaakko@summary{ Countdown seconds for the Tome of Power. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-cheat-counter-show-mapopen.ame0000664000175000017500000000011212641367670031067 0ustar jaakkojaakko@summary{ 1=Only show the cheat counters while the automap is open. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-eyeheight.ame0000664000175000017500000000006712641367670026413 0ustar jaakkojaakko@summary{ Player eye height. The original is 41. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-inventory-timer.ame0000664000175000017500000000007112641367670027072 0ustar jaakkojaakko@summary{ Seconds before the inventory auto-hides. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/game-stats-items.ame0000664000175000017500000000005112641367670026323 0ustar jaakkojaakko@summary{ Current number of items. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-phoenixrod.ame0000664000175000017500000000005612641367670030104 0ustar jaakkojaakko@summary{ 1=Player has the Phoenix Rod. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-ammo-dragonclaw.ame0000664000175000017500000000007412641367670027506 0ustar jaakkojaakko@summary{ Current amount of ammo for the Dragon Claw. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-artifact-torch.ame0000664000175000017500000000005312641367670027345 0ustar jaakkojaakko@summary{ Current number of torches. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-dragonclaw.ame0000664000175000017500000000005612641367670030046 0ustar jaakkojaakko@summary{ 1=Player has the Dragon Claw. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-ammo-goldwand.ame0000664000175000017500000000007112641367670027161 0ustar jaakkojaakko@summary{ Current amount of ammo for the goldwand. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/server-game-announce-secret.ame0000664000175000017500000000007212641367670030446 0ustar jaakkojaakko@summary{ 1=Announce the discovery of secret areas. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-unhide-pickup-invitem.ame0000664000175000017500000000011112641367670030130 0ustar jaakkojaakko@summary{ 1=Unhide the HUD when player collects an inventory item. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-artifact-mysticurn.ame0000664000175000017500000000007012641367670030262 0ustar jaakkojaakko@summary{ Current number of Mystic Urn artifacts. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-cheat-counter-scale.ame0000664000175000017500000000005612641367670027550 0ustar jaakkojaakko@summary{ Size factor for the counters. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-artifact-egg.ame0000664000175000017500000000007012641367670026767 0ustar jaakkojaakko@summary{ Current number of Morph Ovum artifacts. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/game-stats-secrets.ame0000664000175000017500000000006612641367670026660 0ustar jaakkojaakko@summary{ Current number of discovered secrets. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/ctl-inventory-wrap.ame0000664000175000017500000000006412641367670026727 0ustar jaakkojaakko@summary{ 1=Inventory selection wraps around. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-color.ame0000664000175000017500000000011312641367670025546 0ustar jaakkojaakko@summary{ Player color: 0=green, 1=yellow, 2=red, 3=blue, 4=default. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-armor.ame0000664000175000017500000000007412641367670025042 0ustar jaakkojaakko@summary{ 1=Show armor when the status bar is hidden. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-air-movement.ame0000664000175000017500000000006612641367670027042 0ustar jaakkojaakko@summary{ Player movement speed while airborne. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-key-blue.ame0000664000175000017500000000004712641367670026153 0ustar jaakkojaakko@summary{ 1=Player has blue key. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-crossbow.ame0000664000175000017500000000004712641367670027566 0ustar jaakkojaakko@summary{ 1=Player has crossbow. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-currentitem.ame0000664000175000017500000000007112641367670026260 0ustar jaakkojaakko@summary{ 1=Show current item in full-screen mode. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/server-game-respawn.ame0000664000175000017500000000004512641367670027034 0ustar jaakkojaakko@summary{ 1=-respawn was used. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/hud-inventory-slot-showempty.ame0000664000175000017500000000010212641367670030763 0ustar jaakkojaakko@summary{ 1=Show empty inventory slots in full-screen mode. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/ctl-inventory-use-immediate.ame0000664000175000017500000000007412641367670030507 0ustar jaakkojaakko@summary{ 1=Use items immediately from the inventory. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/game-stats-kills.ame0000664000175000017500000000005112641367670026320 0ustar jaakkojaakko@summary{ Current number of kills. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-weapon-nextmode.ame0000664000175000017500000000010512641367670027543 0ustar jaakkojaakko@summary{ 1=Use custom weapon order with Next/Previous weapon. } doomsday-stable-1.15.7/doomsday/doc/libheretic/variable/player-artifact-shadowsphere.ame0000664000175000017500000000007212641367670030723 0ustar jaakkojaakko@summary{ Current number of Shadowsphere artifacts. } doomsday-stable-1.15.7/doomsday/doc/libheretic/command/0000775000175000017500000000000012641367670022310 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/libheretic/command/setclass.ame0000664000175000017500000000004212641367670024611 0ustar jaakkojaakko@summary{ Set player class. } doomsday-stable-1.15.7/doomsday/doc/libheretic/command/cheat.ame0000664000175000017500000000022112641367670024053 0ustar jaakkojaakko@summary{ Issue a cheat code using the original Heretic cheats. } @description{ Params: cheat (cheat) @cbr For example, 'cheat rambo'. } doomsday-stable-1.15.7/doomsday/doc/libheretic/command/spy.ame0000664000175000017500000000007612641367670023612 0ustar jaakkojaakko@summary{ Change the viewplayer when not in deathmatch. } doomsday-stable-1.15.7/doomsday/doc/libheretic/command/give.ame0000664000175000017500000000010312641367670023720 0ustar jaakkojaakko@summary{ Cheat command to give you various kinds of things. } doomsday-stable-1.15.7/doomsday/doc/libheretic/command/stopfinale.ame0000664000175000017500000000007512641367670025142 0ustar jaakkojaakko@summary{ Stop the currently playing interlude/finale. } doomsday-stable-1.15.7/doomsday/doc/libheretic/command/chicken.ame0000664000175000017500000000007012641367670024375 0ustar jaakkojaakko@summary{ Turn yourself into a chicken. Go ahead. } doomsday-stable-1.15.7/doomsday/doc/libheretic/command/god.ame0000664000175000017500000000006312641367670023544 0ustar jaakkojaakko@summary{ Toggle God mode (invulnerability). } doomsday-stable-1.15.7/doomsday/doc/output/0000775000175000017500000000000012641367725020121 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/output/doomsday-server.60000664000175000017500000000414312641367725023335 0ustar jaakkojaakko.\" manual page generated by Amethyst (mdoc+tbl) .Dd January 1, 2016 .Os .Dt "DOOMSDAY-SERVER" 6 .Sh NAME doomsday-server \- Doomsday Engine multiplayer daemon .Pp .Sh "SYNOPSIS" .Sy doomsday-server .Op Fl stdout .Op Fl port Ar tcp-port .Op Fl iwad Ar dir .Op Fl game Ar mode .Op Fl v .Op Fl file Ar file ... .Pp Note that the command line is not interpreted according to GNU conventions. Everything following .Sy -- is ignored. .Sy @ can be used to specify a response file whose contents are added to the command line. .Pp .Sh "OPTIONS" .Bl -tag -width 8n .It Sy -stdout Prints all log entries to the standard output. If this option is not used, nothing is printed so that the server can be run as a background process. .It Sy -port TCP port that the server listens to for incoming connections. .It Sy -iwad Specifies a directory where to look for IWAD files. Doomsday will search for IWADs from all known games and automatically load them when needed. Note that you can also use the DOOMWADDIR environment variable or the paths configuration file to specify the location of the IWAD files. .It Sy -game Sets the game to load after startup. See doomsday(6) for a list of available games. For example: .Pp .Bd -ragged -offset indent -game doom1-ultimate .Ed .Pp If .Sy -game is not specified and the server is unable to choose a game automatically, the server will quit. .It Sy -v Print verbose log messages (also -verbose). Specify more than once for extra verbosity. .It Sy -file Specify one or more resource files (WAD, LMP, PK3) to load at startup. More files can be loaded at runtime with the .Sy load command. .El .Pp In addition to these, .Sy doomsday-server supports many of the command line options of doomsday(6). .Pp .Sh "OPERATING A SERVER" Doomsday servers are, by default, silent daemon processes intended to be run in the background. You need to use the Doomsday Shell to monitor their status and control them. .Pp .Sh "SEE ALSO" doomsday(6), doomsday-shell-text(6) .Pp .Sh "AUTHOR" This documentation has been written by Jaakko Ker\[:a]nen and Daniel Swanson . doomsday-stable-1.15.7/doomsday/doc/output/doomsday-shell-text.60000664000175000017500000000417012641367725024120 0ustar jaakkojaakko.\" manual page generated by Amethyst (mdoc+tbl) .Dd January 1, 2016 .Os .Dt "DOOMSDAY-SHELL-TEXT" 6 .Sh NAME doomsday-shell-text \- Utility for controlling and monitoring Doomsday servers .Pp .Sh "SYNOPSIS" .Sy doomsday-shell-text .Pp .Sh "OPTIONS" This version of .Sy doomsday-shell-text does not support any command line options. .Pp .Sh "INTRODUCTION" The Shell is a utility for controlling and monitoring Doomsday servers. It has a curses-based terminal-friendly UI that allows one to connect to local and remote Doomsday servers. One can also start new servers on the local machine. .Pp Once connected to a server, a console command line is provided for controlling the server. .Pp .Sh "USER INTERFACE" .Ss "Menu" .Pp Press .Sy F9 Ns \&, .Sy ^C Ns \&, or .Sy ^Z to open the menu. .Pp .Bl -tag -width 14n -offset indent .It Connect to... Open a connection to a server at a given IP address. .It Disconnect Disconnect the current connection. The server will not be stopped. .It Start local server Starts a new Doomsday server on the local machine. A dialog will open for configuring the game mode, server's TCP port, and other launch options. You must ensure that Doomsday will be able to locate the required IWAD files (http://dengine.net/dew/index.php?title=IWAD). After the server launches, a connection to it is opened automatically. .It Scroll to bottom Scrolls the log entry history down to the bottom of the buffer, to the latest received log entry. .It About Information about the Shell utility. .It Exit Exit the Shell. Any running servers will not be stopped. .El .Pp .Ss "Console" .Pp A console command line is provided for interacting with a connected server. These commands will get you started: .Pp .Bl -tag -width 14n -offset indent .It help Show some general level help about using the console interface. .It apropos Look up commands and variables containing a specific word. .El .Pp The .Sy PageUp and .Sy PageDown keys scroll the log entry history. .Pp .Sh "SEE ALSO" doomsday(6) .Pp .Sh "AUTHOR" This documentation has been written by Jaakko Ker\[:a]nen and Daniel Swanson . doomsday-stable-1.15.7/doomsday/doc/output/doomsday.60000664000175000017500000011516712641367725022042 0ustar jaakkojaakko.\" manual page generated by Amethyst (mdoc+tbl) .Dd January 1, 2016 .Os .Dt "DOOMSDAY" 6 .Sh NAME doomsday \- Enhanced source port of Doom, Heretic and Hexen .Pp .Sh "SYNOPSIS" .Sy doomsday .Op Fl iwad Ar dir .Op Fl game Ar mode .Op Fl wnd .Op Fl wh Ar w h .Op Fl v .Op Fl file Ar file ... .Pp Note that the command line is not interpreted according to GNU conventions. Everything following .Sy -- is ignored. .Sy @ can be used to specify a response file whose contents are added to the command line. .Pp .Sh "OPTIONS" .Bl -tag -width 8n .It Sy -iwad Specifies a directory where to look for IWAD files. Searches for IWADs from all known games and automatically loads them when needed. .It Sy -game Sets the game to load after startup. See GAME MODES for a list of available games. For example: .Pp .Bd -ragged -offset indent -game doom1-ultimate .Ed .Pp If .Sy -game is not specified, Doomsday will start in "ringzero" mode: a plain console with no game loaded. .It Sy -wnd Starts in windowed mode (also -window). The default is to start in fullscreen mode. .It Sy -wh Sets the size of the Doomsday window. In fullscreen mode specifies which display resolution to use. .It Sy -v Print verbose log messages (also -verbose). Specify more than once for extra verbosity. .It Sy -file Specify one or more resource files (WAD, LMP, PK3) to load at startup. More files can be loaded at runtime with the .Sy load command. .El .Pp More command line options are listed in the Options Reference (http://dengine.net/dew/index.php?title=Command_line_options_reference) in the Doomsday Engine Wiki. .Pp .Sh "TABLE OF CONTENTS" .Bl -enum -width 3n -offset indent .It .Tn "SYNOPSIS" .br .It .Tn "OPTIONS" .br .It .Tn "DESCRIPTION" .br .Tn Features .br .Tn Requirements .br .It .Tn "GAME MODES" .br .It .Tn "FILES" .br .It .Tn "ENVIRONMENT" .br .It .Tn "MULTIPLAYER" .br .Tn Modes .br .Tn Online Games .br .Tn Hosting a Game .br .Tn Networking Details .br .It .Tn "RESOURCE FILES AND CONTAINERS" .br .Tn Automatic Loading of Resources .br .Tn Virtual Directory Mapping .br .Tn PK3 Files .br .Tn WAD Files .br .Tn Lump Assemblies Instead of WADs .br .It .Tn "RESOURCE TYPES" .br .Tn Textures and Flats .br .Tn Patches .br .Tn Sprite Frames .br .Tn Raw Screens .br .Tn Light Maps .br .Tn Detail Textures .br .Tn 3D Models as Particles .br .Tn Music .br .Tn Sound Effects .br .It .Tn "PLUGINS" .br .Tn Dehacked Patches .br .It .Tn "KNOWN ISSUES" .br .It .Tn "SEE ALSO" .br .It .Tn "AUTHOR" .br .It .Tn "ACKNOWLEDGEMENTS" .br .El .Pp .Sh "DESCRIPTION" The Doomsday Engine is a "source port" of id Software's Doom and Raven Software's Heretic and Hexen, which were popular PC FPS games in the early-to-mid 1990s. Doomsday enhances these classic games with many features including 3D graphics, fully customizable controls and client/server networking, making them more accessible to modern gamers. However, the feel of the original games has been kept intact, ensuring sublime nostalgia or an exciting introduction into the pioneering games of the genre. .Pp Doomsday and the associated ports of Doom, Heretic and Hexen have been in development since 1999; the first versions were released in late 1999 and early 2000. Several people have been involved in the project (see ACKNOWLEDGEMENTS). .Pp .Ss "Features" .Pp User interface: .Bl -dash -compact -offset indent .It Overlaid task bar for easy access to engine features .It In-game command console .It Configuration menus and Renderer Appearance sidebar editor .It Game selection screen (http://dengine.net/dew/index.php?title=Ringzero_GUI) for runtime game changes (e.g.\&, from DOOM to Heretic), browsing multiplayer games, and loading saved games .It On-the-fly add-on resource loading .It Flexible input control bindings system .It Built-in updater for easy upgrades .El .Pp Graphics: .Bl -dash -compact -offset indent .It OpenGL based renderer .It Dynamic ambient occlusion (corner shadowing) for world surfaces .It Dynamic lights with halos and lens flares .It Dynamic shadowing effects for world objects .It Particle effects system .It 3D models for world objects (with per-vertex lighting and multiple light sources), skies, skyboxes, and particles .It Automatical world surface (light) decorations .It Detail texturing, shine and glowing effects for world surfaces .It Fogging, bloom, and vignette effects .It Environmental mapping effects for 3D models and world surfaces .It World movement smoothing (actors, monsters, missiles, surfaces) to remove the original games' limitation of 35 FPS .It Smart texture filtering using a modified hq2x algorithm .It Stereoscopic rendering modes: anaglyph, side-by-side, cross-eye and parallel viewing .It Support for Oculus Rift .El .Pp Resources: .Bl -dash -compact -offset indent .It Flexible containters: WAD, ZIP, native folder: any resource can be loaded from any container type .It High-resolution textures: PNG, JPG, TGA, PCX .It 3D models: MD2, DMD with LOD support .It External music files in MP3 and other formats .It Plain text definitions that all share the same syntax .It Internal BSP builder (originally based on glBSP) .El .Pp Audio: .Bl -dash -compact -offset indent .It Plugin based driver architecture .It Uses FMOD Ex (http://www.fmod.org/) for audio playback (sound effects, music, CD audio tracks) .It Supports the open source SDL_mixer for sound effects and music files .It FluidSynth for MIDI playback using SF2 soundfonts .It OpenAL .It External music files in MP3 and other formats .It 3D positional sound effects .It Environmental echo and reverb effects .It Runtime sound effect resampling to 22/44 KHz with 8/16 bits .El .Pp Multiplayer: .Bl -dash -compact -offset indent .It TCP-based client/server networking (http://dengine.net/dew/index.php?title=Multiplayer_(Readme)) .It Automatical discovery of servers running on the local network .It Central master server for discovery of servers on the internet .It Standalone server running as a daemon/background process .It Standalone Doomsday Shell (http://dengine.net/dew/index.php?title=Shell) tool for server management (both local and remote) .It Supports up to 15 player games .It Clients can join games in progress .It In-game chat and server management (via a shell login) .El .Pp Other: .Bl -dash -compact -offset indent .It Open source: software developers should visit the Getting Started page (http://dengine.net/dew/index.php?title=Getting_started) .It Cross platform (http://dengine.net/dew/index.php?title=Supported_platforms) .It Plugin based extensible architecture .It Snowberry: GUI frontend based on game profiles; for managing custom maps, resource packs, add-ons, and starting different game configurations easily (written in Python) .El .Pp .Ss "Requirements" .Pp .Bl -bullet -offset indent .It At least one WAD file from the original Doom, Heretic, Hexen, or other supported game .It A display adapter capable of OpenGL 2.0 hardware acceleration .El .Pp .Sh "GAME MODES" One game plugin, such as libdoom, is able to run in many different modes. Each mode emulates a specific version of the original game and typically has its own IWAD file. .Pp Below is a list of all the game modes supported by the game plugins distributed with Doomsday: libdoom (previously called jDoom), libheretic (jHeretic) and libhexen (jHexen). .Pp .TS center nospaces; lb lb lb l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l l. Plugin Game Mode Description libdoom doom1-share Shareware Doom v1.9 doom1 Registered Doom v1.9 doom1-ultimate Ultimate Doom* doom2 Doom 2 doom2-plut Final Doom: Plutonia Experiment doom2-tnt Final Doom: TNT Evilution chex Chex Quest hacx HacX libheretic heretic-share Shareware Heretic heretic Registered Heretic heretic-ext Heretic: Shadow of the Serpent Riders** libhexen hexen Hexen v1.1 hexen-v10 Hexen v1.0 hexen-dk Hexen: Death Kings of Dark Citadel hexen-demo The 4-level Hexen Demo .TE .Pp * = has a 4th episode ** = has episodes 4 and 5 .Pp To help libhexen recognize the IWAD of the 4-level Hexen (beta) demo, you should rename the IWAD to .Em hexendemo.wad Ns \&. .Pp .Sh "FILES" .Bl -tag -width 14n -offset indent .It /etc/doomsday/defaults System-level default configuration. This is checked after ~/.doomsday/defaults. For example: .Bd -ragged -offset indent .Bd -literal audio fluidsynth { driver: pulseaudio } .Ed .Ed .It /etc/doomsday/paths System-level paths configuration (basedir, libdir, iwaddir). This is checked after ~/.doomsday/paths. See the Doomsday Wiki for details ("Configuration"). .It /usr/share/doomsday/ Read-only files for the launcher, engine and game plugins. .It ~/.doomsday/ Location for the user-specific files. .It ~/.doomsday/defaults User's own defaults (see above). .It ~/.doomsday/paths User's paths configuration (basedir, libdir, iwaddir). .It ~/.doomsday/addons/ Directory where to put addons (WAD, PK3, Snowberry .addon, etc.) so that they appear in the launcher's list. .It ~/.doomsday/runtime/ Doomsday's runtime files, e.g.\&, message log (doomsday.out), savegames, and screenshots. .It ~/.doomsday/runtime/client.id Unique client identifier. Used to identify clients in a multiplayer game. Delete this file to generate a new identifier, for instance if you're getting a "duplicate ID" error. .It ~/.doomsday/runtime/configs/ Saved values for console variables and control bindings. .El .Pp .Sh "ENVIRONMENT" The following environment variables are recognized by Doomsday. .Pp .Bl -tag -width 14n -offset indent .It DOOMWADDIR DOOM WAD directory. Doomsday looks for WAD files in this directory. .It DOOMWADPATH Delimited set of DOOM WAD directories. Use colon (:) to separate directories. Supported WAD files are searched for in each directory. .El .Pp .Sh "MULTIPLAYER" Doomsday features client/server based multiplayer for up to 15 players. .Pp Multiplayer games can be joined using the in-game GUI (under the "New Game" main menu option) or using the console. .Pp .Ss "Modes" .Pp The following multiplayer modes are available in libdoom, libheretic and libhexen: .Pp .Bl -dash -compact -offset indent .It Cooperative .It Deathmatch .It Team Deathmatch .El .Pp Deathmatch and Cooperative can be set directly in the GUI or console. For Team Deathmatch set "No Team Damage" to yes and in Player Setup make sure everybody on your team is the same color. .Pp Doom offers two variants of Deathmatch: .Pp .Bl -tag -width 14n -offset indent .It Deathmatch 1 Ammo and powerups do not respawn. Non-dropped (i.e those from bad guys) weapons remain, but can only be picked up once per life. .It Deathmatch 2 All non-dropped pickups, including weapons randomly respawn a short while after being picked up. Respawned weapons can be picked up more than once per life. .El .Pp Deathmatch in Heretic and Hexen is limited to the "Deathmatch 1" mode. .Pp .Ss "Online Games" .Pp When joining games on the internet, please note the following: .Pp .Bl -bullet -offset indent .It The network code in this release has some limitations (http://dengine.net/multiplayer_issues) that we will work on in future versions. .It The connection should be able to sustain 100KB/s transfer rate (1 Mbps or better). .El .Pp .Ss "Hosting a Game" .Pp Use .Sy doomsday-shell or .Sy doomsday-shell-text to host a multiplayer game of your own. The Shell allows you to start, stop, and control Doomsday multiplayer servers. Using the Shell, you can connect both to local servers and remote servers over the internet. .Pp .Sy doomsday-server runs in a background process and has no UI of its own. You must use the Shell to view or manipulate it. Presently you must use the Shell's Console (text-mode command line interface) to configure the server. .Pp For instance, the following would set up a deathmatch without monsters in E1M5: .Pp .Bd -ragged -offset indent .Bd -literal server-game-deathmatch 1 server-game-nomonsters 1 setmap 1 5 .Ed .Ed .Pp Note that you can set up a .Em .cfg file where you can define the server configuration and automatically open the server (http://dengine.net/dew/index.php?title=Multiplayer_server). .Pp If your server is not public ( .Sy server-public Ns \&), a client can connect to it using a custom address search: .Pp .Bl -bullet -offset indent .It Server on the same computer or LAN: servers on the local network should be discovered automatically and are visible immediately in the Custom Search results list. .It Server somewhere on the internet: enter the server's IP address or domain name into the search address field. .El .Pp .Ss "Networking Details" .Pp Doomsday uses TCP network connections for multiplayer games. If you host a game and are behind a firewall or using NAT, you must make sure that other computers are able to open TCP connections to your computer. This entails opening the appropriate incoming TCP ports on your firewall and/or configuring the NAT so that the correct ports are routed to your computer. .Pp Additionally, UDP ports 13209-13224 are used for detecting servers running on the local network; if you are experiencing problems with autodetecting local servers, check that your firewall isn't blocking these UDP ports on either end. .Pp You can see information about the network subsystem status with the command: .Pp .Bd -ragged -offset indent net info .Ed .Pp .Ss "Server" .Pp A server opens one TCP port for listening to incoming connections. The port number is configured with the console variable net-ip-port. .Pp By default a server uses TCP port 13209 (setting the port to zero will mean 13209 will be used). The configured port must be open for incoming TCP traffic in the firewall. .Pp .Ss "Client" .Pp Clients do not require any firewall configuration for incoming connections. A client only needs to be able to reach the server via the server's TCP port. .Pp A server running on the same computer can be connected to with the following command: .Pp .Bd -ragged -offset indent connect localhost .Ed .Pp .Sh "RESOURCE FILES AND CONTAINERS" Doomsday uses multiple kinds of resource files: .Bl -dash -compact -offset indent .It Plain data (e.g.\&, .Em .png image, .Em .lmp file). .It Definition files ( .Em .ded Ns \&). .It Containers ( .Em .wad Ns \&, .Em .pk3 Ns \&). .El .Pp WADs (http://en.wikipedia.org/wiki/Doom_WAD) are the original Doom engine data container format. By default all resources such as wall textures, menu graphics and fonts, background music and sound effects are loaded from WAD files. Doomsday has a mechanism that allows replacing these resources with external resource files placed in specific directories. .Pp External resource files are easy to use. They do not require making changes to any configuration or definition files. As long as a resource file is placed in the correct directory, Doomsday will load and use it automatically. .Pp Resources are divided into a number of classes. Each class has its own subdirectory under the .Em data// directory. The table below lists the resource classes and gives a brief description of each. .Pp .TS center nospaces; lb lb l l l l l l l l l l l l. Resource Class Description Textures Textures for walls and flats (floors and ceilings) Flats Textures just for flats (floors and ceilings) Patches Graphics for menus, fonts and sprite frames LightMaps Textures for dynamic lights Music Background music Sfx Sound effects .TE .Pp Another example: sound effects for Doom II would be placed in the directory .Em data/doom2/sfx/ Ns \&. .Pp The resource class directory can have a subdirectory for each game mode. For example, textures meant only for Final Doom: Plutonia Experiment would be placed in the directory .Em data/jdoom/textures/doom2-plut/ Ns \&. .Pp When Doomsday looks for an external resource, it first checks the current game mode's subdirectory. If no suitable resource is found there, the class directory is searched instead. .Pp .Ss "Automatic Loading of Resources" .Pp All WAD, PK3, ZIP and LMP files placed in the .Em data//auto/ directory will be automatically loaded at startup. The data files are loaded in alphabetical order, including all the subdirectories of the .Em auto directory. .Pp All DED files placed in the .Em defs//auto/ directory will be automatically read at startup. The definition files are also loaded in alphabetical order, including all the subdirectories of the .Em auto directory. .Pp Virtual files (from inside a container) in the .Em auto directories will also be loaded. .Pp .Ss "Virtual Directory Mapping" .Pp Virtual directory mapping allows you to make the contents of one directory appear inside another directory at runtime. For example, you could have a directory called .Em MyAuto with a set of data files somewhere on your hard drive. You could map this directory to .Em data//auto Ns \&, which would cause .Em MyAuto to behave as though it was an auto-loading directory. .Pp A virtual directory mapping is defined using the .Sy -vdmap option. It takes the source and destination directories as parameters. For example: .Pp .Bd -ragged -offset indent -vdmap /home/username/myauto /usr/share/doomsday/data/jdoom/auto .Ed .Pp You can define an unlimited number of virtual directory mappings using multiple .Sy -vdmap options. .Pp Note, however, that .Sy -vdmap only affects real files. It does not affect virtual files in PK3s or anywhere else. The virtual directory mappings are tried when all other methods of finding a file fail. So, all real files and virtual files override .Sy -vdmap Ns \&. .Pp .Ss "PK3 Files" .Pp Doomsday supports the PK3 format. PK3 files are identical to ZIP archives, with the exception of using .Em .pk3 instead of .Em .zip as the file extension. Encrypted archives are not allowed. If you try to load an encrypted or password protected ZIP/PK3 file, you will get an error message. Wikipedia has more information about PK3s (http://en.wikipedia.org/wiki/PK3_(file_extension)). .Pp PK3 files are loaded using the .Sy -file option, for example .Sy -file .Sy some.pk3 Ns \&. .Pp A PK3 contains a set of files organized into directories. When a PK3 is loaded, all of them become virtual files that Doomsday can access just like the regular files on your hard drive. The end result is the same as if you had unpacked the PK3 into your Doomsday base directory. (No actual unpacking is done.) For example, the PK3 could have the file .Em data/jdoom/auto/superb.wad Ns \&. .Pp PK3 files can be created with just about any ZIP utility. Make sure all the files inside the archive have the correct paths, or otherwise Doomsday may not be able to find them. .Pp .Ss "Automatic Remapping Inside PK3" .Pp Files in the root of a PK3 are subject to automatic relocation based on file name extension: PK3/ZIP/LMP/WAD are mapped to .Em data//auto/ and DED goes to .Em defs//auto/ Ns \&. For example, placing .Em test.ded into the root of a PK3 has the same end result as placing .Em test.ded into .Em defs//auto/ Ns \&. .Pp Since this automatic mapping only affects single files, it is also possible to request mapping manually by adding a special prefix character to the name of a directory in the root of a PK3. If the directory begins with .Em # Ns \&, it is mapped into .Em data//auto/ Ns \&; if it begins with .Em @ Ns \&, it is mapped into .Em defs//auto/ Ns \&. .Pp .Bd -ragged -offset indent .Em #CoolStuff/Some.pk3 => .Em data//auto/CoolStuff/Some.pk3 .Ed .Pp .Ss "WAD Files" .Pp Doomsday has a number of advanced features for handling WAD files. .Pp .Ss "Definitions Inside WAD" .Pp After all DED files have been processed, the engine will check through all the loaded WAD files for lumps named .Em DD_DEFNS Ns \&. All the lumps with that name are processed just as if they were DED files, i.e.\& they should contain a DED file in plain text format. The .Em DD_DEFNS lumps are applied in the order in which they have been loaded. .Pp .Ss "WAD as a Virtual File Container" .Pp Another special lump used by Doomsday is .Em DD_DIREC Ns \&. It contains a table that translates file paths to lump names. An example is shown below: .Pp .Bd -ragged -offset indent .Bd -literal FILE001 /Md2/jDoom/Some.md2 FILE002 Another.ded .Ed .Ed .Pp Each line in .Em DD_DIREC contains a lump/path pair. The paths that begin with a (back)slash are interpreted as paths that start from the Doomsday base directory (set with .Sy -basedir Ns \&) and paths that don't begin with a (back)slash are located in the runtime directory. The engine will first search the .Em DD_DIREC s before opening any file for reading. Note, however, that all kinds of files are not loaded using the .Em DD_DIREC s: for instance demos (which are compressed with the LZSS library) must always be loaded from real files. .Pp skyjake has created a simple utility for automatically creating a WAD file that contains the current directory and all its subdirectories plus a .Em DD_DIREC lump that has (with a high probability) a unique lump name for each file. You can invoke the utility like this: .Pp .Bd -ragged -offset indent wadtool myfiles.wad /data/jdoom/textures/ .Ed .Pp This would create a WAD file that contains all the files from the current directory. When writing the .Em DD_DIREC table, the prefix "/data/jdoom/textures/" would be added to each file name. .Sy wadtool is available in the Doomsday source repository under .Em /tools/wadtool Ns \&. .Pp .Ss "Lump Assemblies Instead of WADs" .Pp The automatic loading of data files can be utilised to load directories that contain individual data lumps. This kind of a directory is called a "lump assembly" and it can be used instead of a WAD file. Note that a lump assembly can only be loaded via the autoload mechanism (but it can be inside of a PK3 that is loaded manually). .Pp By default the contents of a lump assembly are loaded in alphabetical order. However, some kinds of data require that the lumps are in a specific order (for instance map data). You have two options if you want to enforce a specific order: .Pp .Bl -bullet -offset indent .It You can use name prefixes that are used to sort the lumps but are ignored when the lump names are determined. The length of the prefix can be 1..9 characters long. You specify the length of the prefix by adding an extension to the name of the directory that contains the lumps. An example that uses a prefix with 3 characters: .Pp .Bd -ragged -offset indent .Em Data/Game/Auto/DirWithPrefix.3/01_LUMPNAME.lmp .Ed .Pp The first three characters of the file name are ignored ( .Em 01_ Ns \&) and .Em LUMPNAME becomes the name of the lump. .It You can put a WAD inside the assembly. .El .Pp The assembly can be built using a hierarchy of directories. For example the contents of the PK3 might be: .Pp .Bd -ragged -offset indent .Bd -literal #assembly/ data1.lmp data2.lmp powerplant.2/ a-E2M3.lmp b-THINGS.lmp xyz.lmp .Ed .Ed .Pp .Em #assembly would be mapped to .Em Data//Auto/assembly/ Ns \&. .Pp .Sh "RESOURCE TYPES" .Ss "Textures and Flats" .Pp Normal wall textures and flats can be replaced with PNG, JPG, TGA (Truevision Targa), or PCX (Zsoft Paintbrush) images. The engine currently supports these image formats: .Pp .TS center nospaces; lb lb lb lb lb l l l l l l l l l l l l l l l l l l l l. Pixel size PCX PNG JPG TGA 8-bit (paletted)* Yes Yes - - 16-bit - - - - 24-bit - Yes Yes Yes** 32-bit (alpha channel) - Yes - Yes** .TE .Pp * = the palette does not have to match the palette of the game ** = TGAs must be type 2 (uncompressed, unmapped RGB) .Pp .Bl -tag -width "NOTE! " .It Sy NOTE! 32-bit images are just 24-bit images with an additional 8 bits per pixel for the alpha channel. .El .Pp The recommended format for high-resolution textures is paletted PNG. It is the easiest format in which to distribute the textures due to its small size. Since the palette doesn't have to be the same as the game's, it should be enough for many textures. .Pp High-resolution textures can be of any size. The engine will render them scaled so that they fit the size of the original texture. This means the aspect ratio of the new texture doesn't have to be the same as of the original texture. Note that the engine will have to resize all textures so that their dimensions are powers of two (e.g.\& 32, 64, 128, 256). This means TGA/PNG textures whose width and height are already powers of two can be loaded faster. .Pp Color keying allows having transparent pixels in an image format that has no alpha channel. Color keying is applied if the file name of the image ends in "-ck", for example .Em brnbigc-ck.png Ns \&. Both cyan (0,255,255) and purple (255,0,255) are used as keys, and will be replaced with transparent pixels. .Pp Examples: .Pp .Bl -bullet -offset indent .It To create a high-resolution texture for the wall texture STARTAN3 you'd place a TGA file named .Em STARTAN3.tga or a PNG file named .Em STARTAN3.png into the .Em Textures directory. .It In order to replace the flat FLOOR7_2, you'd to put .Em FLOOR7_2.png into the .Em Flats directory. It is also possible to have flats in the .Em Textures directly if they are prefixed .Em Flat- Ns \&. For instance, .Em Flat-FLOOR7_2.tga Ns \&. .El .Pp .Bl -tag -width "NOTE! " .It Sy NOTE! The file names of the high-resolution textures must match the .Em texture names, not the names of the patches that make up the textures. For example: DOOR2_5 is a patch name, DOOR3 is the texture that uses DOOR2_5. .El .Pp To disable high-resolution textures use the command line option .Sy -nohightex Ns \&. The option .Sy -texdir can be used to change the directory from which the textures are searched. .Pp .Ss "Patches" .Pp Patches are images that are commonly used in game menus and intermission screens. Like textures, patches can be replaced with TGA, PNG or PCX images. The .Em Patches resource class directory is searched using the lump names of the original patches. For example, to replace the Doom menu title, you would place the file .Em m_doom.png to the .Em Patches directory. .Pp The original data lumps are required even if an external resource is found, because the original data includes information about offsets and the on-screen size of the patch. This means the image from the external resource can be of any arbitrary resolution: it will be scaled to match the original patch. .Pp .Ss "Sprite Frames" .Pp Sprite frames are patches. They can be replaced with external resources just like all other patches. The same restrictions apply, though: the dimensions of the external resource do not affect the actual size of the sprite frame. This means the external resources must really be .Em high-resolution versions of the original images. Otherwise the size and/or aspect ratio will not be right for the resource. .Pp For example, in order to replace the Doom medikit (lump name MEDIA0), one would place the file .Em media0.png into the .Em Patches directory. .Pp Doom uses color-mapping to change specific colors in some sprites, e.g.\&, the players, so that the same image can be displayed with different coloring without having multiple copies of it in memory. Doomsday will not change colors on the fly in external resources. However, color-mapped versions of sprite frames can each have their own external resources. To indicate that a resource is color-mapped, its name is formed like this: .Pp .Bd -ragged -offset indent .Em (patchName)-table(classNum)(tableNum).ext .Ed .Pp .Em (patchName) is the sprite frame lump name. .Em (classNum) is the number of the color translation class. This number is always zero in Doom and Heretic. In Hexen, it's the player's class (0=Fighter, 1=Cleric, 2=Mage). .Em tableNum is the index number of the color translation table. Doom and Heretic have 4 tables, Hexen has 8. For example: .Em playa1-table01-ck.png would be the Doom player sprite frame A1 with color table 1. The .Em -ck suffix makes the image color keyed (i.e.\& special colors indicate transparent pixels). .Pp .Ss "Raw Screens" .Pp Some background pictures are in the raw screen format, which is used to store 320 x 200 pixel paletted images. A lump containing a raw screen image (for example Heretic's TITLEPIC) is exactly 320 x 200 = 64000 bytes long. Raw screens can be replaced with external resources in the .Em Patches directory. .Pp .Ss "Light Maps" .Pp Light maps are monochrome images that can be used with dynamic lights. The dimensions of a light map must be powers of two, for example 256 x 256. If the map contains an alpha channel, the actual color values are ignored; only the alpha values are significant. If the map doesn't have an alpha channel, one is generated by averaging the color values of the image. .Pp Example: If you assign the light map "Round" to a light source, images with that file name are searched from the .Em LightMaps directory. The accepted image formats are PCX, TGA and PNG. If .Em Round.pcx Ns \&, .Em Round.tga or .Em Round.png is found, it will be loaded. .Pp .Ss "Detail Textures" .Pp Detail textures are grayscale images that are rendered on top of normal textures when walls and planes are viewed from close by. A signed-add blending is used, which lets the detail texture either darken or brighten the underlying texture: black => darker, gray => no change, white => brighter. .Pp Detail textures can be assigned to specific wall textures and flats using Detail definitions. .Pp Detail textures can be loaded from external image resources (from the .Em Textures directory), or PCX images and raw image data stored inside a WAD lump. When using the .Sy -file option to load detail texture lumps, the file names of the images become lump names. .Pp If an external resource is used as the detail texture, its dimensions must be powers of two (for example 128x64 or 256x256). The image file must be in one of the supported image file formats. .Pp PCX images used as detail textures must have a color depth of 8 bits and their width and height must be powers of two. The palette should be a grayscale one. It is possible to use other colors but the result can be weird due to the way the blending of detail textures is done. .Pp If the source data is a raw image, it must be either 64x64, 128x128 or 256x256 pixels in size. Raw images contain only the pixel values, (one byte per pixel) and have only one color component per pixel (they're black and white images), which means the lump or file that contains the detail texture can either be 4096, 16384 or 65536 bytes long. .Pp Using the default scaling, the pixels of detail textures are four times smaller than the pixels of regular textures. .Pp The console variables rend-tex-detail, rend-tex-detail-far, rend-tex-detail-strength and rend-tex-detail-scale control the rendering of detail textures. .Pp .Ss "3D Models as Particles" .Pp 3D models can be used as particles in a particle generator. In the particle generator definition, set the particle stage's type to one of the .Sy pt_model flags. The following would make the stage use particle model number 13: .Pp .Bd -ragged -offset indent Type = "pt_model13"; .Ed .Pp In the particle stage definition, remember to set a color for the stage. If the color is not specified, the default values will result in a completely transparent particle model. .Pp The model definition must have a matching ID. For example, particle model number 13 uses the following ID: .Pp .Bd -ragged -offset indent ID = "Particle13"; .Ed .Pp For further details see the DED Reference. .Pp .Ss "Music" .Pp Doomsday can play various external music files using the FMOD library (http://www.fmod.org/). FMOD supports many music file formats including MP3, OGG, MOD and S3M (mods are a good choice due to their good quality/size ratio). External music files can be played at any time using the .Sy playext console command. .Pp As an alternative to FMOD there is the SDL_mixer audio plugin. It is used automatically in case the FMOD audio plugin is not installed or fails to load for some reason. However, SDL_mixer's playback quality is not as high and it does not support 3D sound effects. .Pp Like other external resources, placing a music file into the .Em Music resource class directory is enough. The file name must match the lump name of the original music data lump. For example, to replace the music for Doom's first episode's second map, the file .Em d_e1m2.mp3 would be placed in the .Em Music directory. .Pp It is also possible to store music files into a WAD. Again, you must name the lumps so that they match the lumps of the original songs, and are thus loaded instead of them. Any music files supported by FMOD can be loaded from a WAD. .Pp Another way to specify an external resource file is to use the .Sy Ext key of a Music definition (in Audio.ded). An example of editing the definitions: You have a terrific song called .Em song.mp3 and you'd like to hear it instead of Doom's regular "e1m2". .Pp .Bl -enum -offset indent .It The first thing to decide is whether you want to play the song from where it's currently located, or do you want to move it under the Doomsday directory. In the latter case it would be easy to distribute the song and its definition file to others, since they wouldn't have to worry about where the music file is. If you decide to move the song, create a directory under the .Em Doomsday/Data/jDoom/ directory called .Em Music Ns \&. Another logical choice could be .Em Doomsday/Music/ Ns \&. Then copy the song into the created directory. .It Open .Em Audio.ded in a text editor. In it, you will find a bunch of Music definitions, including: .Pp .Bd -ragged -offset indent Music { ID = "e1m2"; Lump = "D_E1M2"; } .Ed .Pp In order to make the change persist over version upgrades (each one will overwrite .Em Audio.ded Ns \&) copy the definition to .Em User.ded in the .Em Defs/jDoom/ directory, or create a new DED file with any name you like in the .Em Defs/jDoom/Auto/ directory. Everything in the .Em Auto directory will be read automatically. If .Em User.ded doesn't exist, just create a new file for it. .It Now you have the new Music definition, and the only thing left is to let the engine know which file it should load when the song "e1m2" is played. Edit your definition by adding the .Sy Ext key: .Pp .Bd -ragged -offset indent .Bd -literal Music { ID = "e1m2"; Lump = "D_E1M2"; Ext = "Data/jDoom/Music/song.mp3"; } .Ed .Ed .El .Pp CD tracks can be associated with songs in a similar fashion, but instead of using the .Sy Ext key you should use a .Sy CD .Sy track key: .Pp .Bd -ragged -offset indent CD track = 3; .Ed .Pp .Ss "Sound Effects" .Pp Sound samples can be replaced with WAV files. The supported formats are 8-bit and 16-bit mono PCM with no compression. The .Em Sfx resource class directory is searched using the lump names of the original samples. For example, to replace the rocket launcher sound in libdoom, the file .Em dsrlaunc.wav would be placed in the .Em Sfx directory. .Pp Another way to specify an external resource file is to use the Sound definition .Sy Ext key. .Pp Doomsday will automatically detect the format of a sample if it's loaded from a WAD file, making it possible to compile a WAD out of WAV samples. .Pp .Sh "PLUGINS" .Ss "Dehacked Patches" .Pp Most features of Dehacked are supported by Doomsday's Dehacked reader. The loader will print a message during startup if an unsupported feature is used. .Pp Let's say you have the Dehacked patch .Em file.deh in your runtime directory. Then you can use the command line option .Sy -deh .Sy file.deh to load it at startup. .Pp If a lump named .Em DEHACKED is found in a WAD, it will be automatically applied when the WAD is loaded. Normally only the last .Em DEHACKED lump is used if a lump with that name is found in multiple WADs. Use the option .Sy -alldehs to make the engine apply all found .Em DEHACKED lumps. .Pp .Sh "KNOWN ISSUES" Doomsday is a work in progress, so there usually is a number of known issues that will be addressed in the future. .Pp .Bl -tag -width 14n -offset indent .It Bugs and Features The official place to report new bugs, submit feature requests and browse the existing reports is the Deng Team's own Issue Tracker (http://tracker.skyjake.fi/). .It Multiplayer Issues There is a to-do list of known multiplayer issues and needed enhancements (http://dengine.net/multiplayer_issues) maintained by skyjake. .El .Pp .Sh "SEE ALSO" Additional documentation is available online in the Doomsday Engine Wiki (http://dengine.net/dew/). Information in the wiki includes: .Pp .Bl -bullet -offset indent .It User's Guide: collection of documentation to help play games with Doomsday (http://dengine.net/dew/index.php?title=Category:User%27s_Guide) .It Author's Guide: articles for map and resource pack/addon authors (http://dengine.net/dew/index.php?title=Category:Author%27s_Guide) .It Programmer's Guide: technical documentation for software developers and Deng Team members (http://dengine.net/dew/index.php?title=Category:Programmer%27s_Guide) .It Definitions Reference: DED and finale script syntax (http://dengine.net/dew/index.php?title=DED) .It Command Line Options Reference (http://dengine.net/dew/index.php?title=Command_line_options) .It Version history (http://dengine.net/dew/index.php?title=Category:Releases) .It Project roadmap and features in planning (http://tracker.skyjake.fi/projects/deng/roadmap) .El .Pp .Sh "AUTHOR" This documentation has been written by Jaakko Ker\[:a]nen and Daniel Swanson . .Pp .Sh "ACKNOWLEDGEMENTS" .Sy id .Sy Software created DOOM and then released its source code. .Pp .Sy Raven .Sy Software created Heretic and Hexen and released their source code. .Pp .Sy Jaakko .Sy Ker\[:a]nen started the Doomsday Engine project and is the lead developer of the Deng Team. .Pp .Sy Daniel .Sy Swanson is a developer and lead graphics artist in the Deng Team, author of the dengine.net (http://dengine.net) website implementation and former maintainer of the jDoom Resource Pack. .Pp .Sy Dave .Sy Gardner is a member of the Deng Team, maintainer of high-resolution texture packs, and contributor to the dengine.net (http://dengine.net) website implementation. .Pp .Sy Vermil regularly provides in-depth feedback and bug reports and is an expert in all things related to DOOM-based games. .Pp .Sy Kees .Sy Meijs packaged Doomsday for Debian and hosted an Apt repository of Debian packages. .Pp .Sy Andrew .Sy Apted wrote glBSP (http://glbsp.sourceforge.net/). .Pp .Sy Graham .Sy Jackson helped with the source code, fixed Doom bugs and did a lot of testing. .Pp .Sy David .Sy Jarvis did early network testing with jDoom and jHeretic and generously contributed essential computer hardware components. .Pp .Sy William .Sy Mull hosted the project's websites for many years. .Pp Finally, the resource pack community, particularly mentioning: .Sy Abbs maintained the jDoom model pack and did wonderful work on the models and particle effects; .Sy Anton .Sy Rzheshevski (aka Cheb) created player weapon 3D models and other MD2 modifications/enhancements, maintained the jDoom model pack and wrote KickStart version 2; .Sy Greg .Sy Fisk (Slyrr) authored many excellent 3D models for jHeretic; .Sy Daniel .Sy Norton created detail textures for jDoom. doomsday-stable-1.15.7/doomsday/doc/output/.gitignore0000664000175000017500000000003312641367670022104 0ustar jaakkojaakko*.6 Read Me.rtf readme.txt doomsday-stable-1.15.7/doomsday/doc/output/Makefile0000664000175000017500000000162212641367670021561 0ustar jaakkojaakkoreadme_sources := $(wildcard ../readme/*.ame) readme_main := ../readme/readme.ame shell_sources := $(wildcard ../shell-text/*.ame) shell_main := ../shell-text/shell-text.ame sv_sources := $(wildcard ../server/*.ame) sv_main := ../server/server.ame .PHONY: wiki all: doomsday.6 doomsday-server.6 doomsday-shell-text.6 Read\ Me.rtf readme.txt wiki doomsday.6: $(readme_sources) amethyst -dMAN -dUNIX -odoomsday.6 $(readme_main) Read\ Me.rtf: $(readme_sources) amethyst -dRTF -dMACOSX -oRead\ Me.rtf $(readme_main) readme.txt: $(readme_sources) amethyst -dTXT -dWIN32 -dCR_NL -oreadme.txt $(readme_main) doomsday-server.6: $(sv_sources) amethyst -dMAN -dUNIX -odoomsday-server.6 $(sv_main) doomsday-shell-text.6: $(shell_sources) amethyst -dMAN -dUNIX -odoomsday-shell-text.6 $(shell_main) wiki: python /Users/jaakko/Dropbox/Scripts/readme_to_wiki.py clean: rm -f doomsday.6 Read\ Me.rtf readme.txt doomsday-stable-1.15.7/doomsday/doc/shell-text/0000775000175000017500000000000012641367670020651 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/shell-text/shell-text.ame0000664000175000017500000000617112641367670023433 0ustar jaakkojaakko$ Man page for doomsday-shell-text @macro{TITLE}{ doomsday-shell-text } @macro{ONELINER}{ Utility for controlling and monitoring Doomsday servers } @macro{VERSION}{ Version 1.15.7 } @macro{AUTHOR}{ Deng Team } @macro{LINK}{ http://dengine.net/ } @require{amestd} $ Extra formatting macros. @macro{plugin}{@arg} @macro{key}{@strong{@arg}} @begin @chapter{ Synopsis } @strong{doomsday-shell-text} @chapter{ Options } This version of @bin{doomsday-shell-text} does not support any command line options. $* @deflist/thin{ @item{@opt{-iwad}} Specifies a directory where to look for IWAD files. Searches for IWADs from all known games and automatically loads them when needed. @item{@opt{-game}} Sets the game to load after startup. See @ref{gamemode}{Game modes} for a list of available games. For example: @samp{@opt{-game doom1-ultimate}} If @opt{-game} is not specified, Doomsday will start in @dquote{ringzero} mode: a plain console with no game loaded. @item{@opt{-wnd}} Starts in windowed mode (also @opt{-window}). The default is to start in fullscreen mode. @item{@opt{-wh}} Sets the size of the Doomsday window. In fullscreen mode specifies which display resolution to use. @item{@opt{-v}} Print verbose log messages (also @opt{-verbose}). Specify more than once for extra verbosity. @item{@opt{-file}} Specify one or more resource files (WAD, LMP, PK3) to load at startup. More files can be loaded at runtime with the @cmd{load} command. } *$ @chapter{ Introduction } The Shell is a utility for controlling and monitoring Doomsday servers. It has a curses-based terminal-friendly UI that allows one to connect to local and remote Doomsday servers. One can also start new servers on the local machine. Once connected to a server, a console command line is provided for controlling the server. @chapter{ User Interface } @section { Menu } Press @key{F9}, @key{^C}, or @key{^Z} to open the menu. @deflist{ @item{Connect to...} Open a connection to a server at a given IP address. @item{Disconnect} Disconnect the current connection. The server will not be stopped. @item{Start local server} Starts a new Doomsday server on the local machine. A dialog will open for configuring the game mode, server's TCP port, and other launch options. You must ensure that Doomsday will be able to locate the required @wikilink{IWAD files}{IWAD}. After the server launches, a connection to it is opened automatically. @item{Scroll to bottom} Scrolls the log entry history down to the bottom of the buffer, to the latest received log entry. @item{About} Information about the Shell utility. @item{Exit} Exit the Shell. Any running servers will not be stopped. } @section { Console } A console command line is provided for interacting with a connected server. These commands will get you started: @deflist{ @item{@cmd{help}} Show some general level help about using the console interface. @item{@cmd{apropos}} Look up commands and variables containing a specific word. } The @key{PageUp} and @key{PageDown} keys scroll the log entry history. @chapter{ See Also } doomsday(6) $ Man pages have an author section. @include{../author} doomsday-stable-1.15.7/doomsday/doc/libcommon/0000775000175000017500000000000012641367670020537 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/libcommon/variable/0000775000175000017500000000000012641367670022324 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-nomonsters.ame0000664000175000017500000000003712641367670027432 0ustar jaakkojaakko@summary{ 1=No monsters. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-episode.ame0000664000175000017500000000011612641367670026651 0ustar jaakkojaakko@summary{ Identifier of the episode to be played in a multiplayer game. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/chat-macro5.ame0000664000175000017500000000003612641367670025112 0ustar jaakkojaakko@summary{ Chat macro 6. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/ctl-aim-noauto.ame0000664000175000017500000000004712641367670025642 0ustar jaakkojaakko@summary{ 1=Autoaiming disabled. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/input-joy-z.ame0000664000175000017500000000004012641367670025207 0ustar jaakkojaakko@summary{ Z axis control. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-wall-unseen-g.ame0000664000175000017500000000006712641367670026244 0ustar jaakkojaakko@summary{ Automap unseen areas, green component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/view-cross-a.ame0000664000175000017500000000006112641367670025324 0ustar jaakkojaakko@summary{ Crosshair color alpha component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-wall-ceilingchange-b.ame0000664000175000017500000000011112641367670027510 0ustar jaakkojaakko@summary{ Automap ceiling height difference lines, blue component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-hotkeys.ame0000664000175000017500000000007012641367670025435 0ustar jaakkojaakko@summary{ 1=Enable hotkey navigation in the menu. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-door-colors.ame0000664000175000017500000000005712641367670026027 0ustar jaakkojaakko@summary{ 1=Show door colors in automap. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-id.ame0000664000175000017500000000004012641367670024151 0ustar jaakkojaakko@summary{ Current map id. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-skill.ame0000664000175000017500000000004512641367670025034 0ustar jaakkojaakko@summary{ Current skill level. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-armor.ame0000664000175000017500000000004712641367670025423 0ustar jaakkojaakko@summary{ Current armor ammount. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-state.ame0000664000175000017500000000004412641367670025035 0ustar jaakkojaakko@summary{ Current game state. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-skill.ame0000664000175000017500000000006212641367670026337 0ustar jaakkojaakko@summary{ Skill level in multiplayer games. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-huddisplay.ame0000664000175000017500000000021212641367670025724 0ustar jaakkojaakko@summary{ 0=No HUD when in the automap 1=Current HUD display shown when in the automap 2=Always show Status Bar when in the automap } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-quick-ask.ame0000664000175000017500000000007712641367670025646 0ustar jaakkojaakko@summary{ 1=Ask me to confirm when quick saving/loading. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-colorb-r.ame0000664000175000017500000000005412641367670025470 0ustar jaakkojaakko@summary{ Menu color B red component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/ctl-look-joy-inverse.ame0000664000175000017500000000006012641367670027000 0ustar jaakkojaakko@summary{ 1=Inverse joystick look Y axis. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/input-joy-slider1.ame0000664000175000017500000000004612641367670026307 0ustar jaakkojaakko@summary{ First slider control. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-flash-speed.ame0000664000175000017500000000005412641367670026144 0ustar jaakkojaakko@summary{ Menu selection flash speed. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-save-confirm-loadonreborn.ame0000664000175000017500000000013112641367670030765 0ustar jaakkojaakko@summary{ 1=Ask me to confirm when loading a save on player reborn. (default: on). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-jump-power.ame0000664000175000017500000000010412641367670026402 0ustar jaakkojaakko@summary{ Jump power (for all clients if this is the server). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/view-size.ame0000664000175000017500000000005112641367670024726 0ustar jaakkojaakko@summary{ View window size (3-13). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/ctl-look-spring.ame0000664000175000017500000000004512641367670026033 0ustar jaakkojaakko@summary{ 1=Lookspring active. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-flash-r.ame0000664000175000017500000000007312641367670025306 0ustar jaakkojaakko@summary{ Menu selection flash color, red component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/chat-macro4.ame0000664000175000017500000000003612641367670025111 0ustar jaakkojaakko@summary{ Chat macro 5. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/msg-color-r.ame0000664000175000017500000000006512641367670025152 0ustar jaakkojaakko@summary{ Color of HUD messages red component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/input-joy-ry.ame0000664000175000017500000000005312641367670025374 0ustar jaakkojaakko@summary{ Y rotational axis control. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-open-timer.ame0000664000175000017500000000010212641367670025633 0ustar jaakkojaakko@summary{ Time taken to open/close the automap, in seconds. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-color-r.ame0000664000175000017500000000005612641367670025144 0ustar jaakkojaakko@summary{ HUD info color red component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/input-mouse-x-sensi.ame0000664000175000017500000000005212641367670026656 0ustar jaakkojaakko@summary{ Mouse X axis sensitivity. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-monster-meleeattack-nomaxz.ame0000664000175000017500000000007512641367670032503 0ustar jaakkojaakko@summary{ 1=Monster melee attacks are infinitely tall. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-wall-r.ame0000664000175000017500000000005612641367670024762 0ustar jaakkojaakko@summary{ Automap walls, red component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/chat-macro0.ame0000664000175000017500000000003612641367670025105 0ustar jaakkojaakko@summary{ Chat macro 1. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/input-joy-slider2.ame0000664000175000017500000000004712641367670026311 0ustar jaakkojaakko@summary{ Second slider control. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-autoswitch-notfiring.ame0000664000175000017500000000012112641367670030463 0ustar jaakkojaakko@summary{ 1=Disable automatic weapon switch if firing when picking one up. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-unpause-focusgained.ame0000664000175000017500000000010112641367670027654 0ustar jaakkojaakko@summary{ Unpause the game when game window regains focus. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/input-joy-y.ame0000664000175000017500000000004012641367670025206 0ustar jaakkojaakko@summary{ Y axis control. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-colorb-b.ame0000664000175000017500000000005512641367670025451 0ustar jaakkojaakko@summary{ Menu color B blue component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/input-joy-x.ame0000664000175000017500000000011212641367670025205 0ustar jaakkojaakko@summary{ X axis control: 0=None, 1=Move, 2=Turn, 3=Strafe, 4=Look. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/chat-macro2.ame0000664000175000017500000000003612641367670025107 0ustar jaakkojaakko@summary{ Chat macro 3. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/msg-uptime.ame0000664000175000017500000000010212641367670025070 0ustar jaakkojaakko@summary{ Number of seconds to keep HUD messages on screen. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-effect.ame0000664000175000017500000000015512641367670025207 0ustar jaakkojaakko@summary{ 3-bit bitfield. 0=Disable menu effects. 0x1=text type-in, 0x2=text shadow, 0x4=text glitter. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-opacity.ame0000664000175000017500000000005012641367670025226 0ustar jaakkojaakko@summary{ Opacity of the automap. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-background-r.ame0000664000175000017500000000007112641367670026137 0ustar jaakkojaakko@summary{ Automap background color, red component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/msg-color-g.ame0000664000175000017500000000006712641367670025141 0ustar jaakkojaakko@summary{ Color of HUD messages green component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-color-r.ame0000664000175000017500000000005212641367670025324 0ustar jaakkojaakko@summary{ Menu color red component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-flash-g.ame0000664000175000017500000000007512641367670025275 0ustar jaakkojaakko@summary{ Menu selection flash color, green component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-slam.ame0000664000175000017500000000005612641367670024707 0ustar jaakkojaakko@summary{ 1=Slam the menu when opening. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-wall-b.ame0000664000175000017500000000005712641367670024743 0ustar jaakkojaakko@summary{ Automap walls, blue component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-hub.ame0000664000175000017500000000004012641367670024333 0ustar jaakkojaakko@summary{ Current hub id. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-status-size.ame0000664000175000017500000000004112641367670026054 0ustar jaakkojaakko@summary{ Status bar size. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-monsters-floatoverblocking.ame0000664000175000017500000000015312641367670031300 0ustar jaakkojaakko@summary{ 1=Allow floating monsters to attempt floating over blocking things (avoids getting stuck). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-scale.ame0000664000175000017500000000004312641367670025036 0ustar jaakkojaakko@summary{ Scaling for menus. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-color-b.ame0000664000175000017500000000005312641367670025305 0ustar jaakkojaakko@summary{ Menu color blue component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/msg-align.ame0000664000175000017500000000011412641367670024662 0ustar jaakkojaakko@summary{ Alignment of HUD messages. 0 = left, 1 = center, 2 = right. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-autoswitch-ammo.ame0000664000175000017500000000012712641367670027423 0ustar jaakkojaakko@summary{ Change weapon automatically when picking up ammo. 1=If better 2=Always } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-move-speed.ame0000664000175000017500000000006012641367670026342 0ustar jaakkojaakko@summary{ Player movement speed modifier. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/view-cross-angle.ame0000664000175000017500000000011112641367670026166 0ustar jaakkojaakko@summary{ Rotation angle for the crosshair [0..1] (1=360 degrees). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-unhide-pickup-key.ame0000664000175000017500000000007512641367670027123 0ustar jaakkojaakko@summary{ 1=Unhide the HUD when player collects a key. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/view-cross-type.ame0000664000175000017500000000004712641367670026071 0ustar jaakkojaakko@summary{ The current crosshair. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/chat-macro3.ame0000664000175000017500000000003612641367670025110 0ustar jaakkojaakko@summary{ Chat macro 4. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/chat-macro8.ame0000664000175000017500000000003612641367670025115 0ustar jaakkojaakko@summary{ Chat macro 9. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-cheat.ame0000664000175000017500000000011312641367670026302 0ustar jaakkojaakko@summary{ 1=Allow cheating in multiplayer games (god, noclip, give). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-jump.ame0000664000175000017500000000004112641367670025250 0ustar jaakkojaakko@summary{ 1=Allow jumping. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-save-suggestname.ame0000664000175000017500000000011512641367670027225 0ustar jaakkojaakko@summary{ 1=Suggest an auto-generated name when selecting a save slot. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/msg-scale.ame0000664000175000017500000000006112641367670024660 0ustar jaakkojaakko@summary{ Scaling factor for HUD messages. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-wall-ceilingchange-r.ame0000664000175000017500000000011012641367670027527 0ustar jaakkojaakko@summary{ Automap ceiling height difference lines, red component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-customcolors.ame0000664000175000017500000000013212641367670026313 0ustar jaakkojaakko@summary{ Custom automap coloring 0=Never, 1=Auto (enabled if unchanged), 2=Always. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-door-glow.ame0000664000175000017500000000011312641367670025467 0ustar jaakkojaakko@summary{ Door glow thickness in the automap (with map-door-colors). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-icon-alpha.ame0000664000175000017500000000004612641367670025601 0ustar jaakkojaakko@summary{ HUD icon alpha value. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-glitter.ame0000664000175000017500000000005512641367670025424 0ustar jaakkojaakko@summary{ Strength of type-in glitter. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/view-bob-weapon.ame0000664000175000017500000000006112641367670026006 0ustar jaakkojaakko@summary{ Scale for player weapon bobbing. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-rotate.ame0000664000175000017500000000007112641367670025057 0ustar jaakkojaakko@summary{ 1=Automap turns with player, up=forward. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-mapcycle.ame0000664000175000017500000000004712641367670027021 0ustar jaakkojaakko@summary{ Map rotation sequence. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/view-cross-b.ame0000664000175000017500000000006012641367670025324 0ustar jaakkojaakko@summary{ Crosshair color blue component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-color-g.ame0000664000175000017500000000006012641367670025124 0ustar jaakkojaakko@summary{ HUD info color green component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-mobj-r.ame0000664000175000017500000000005612641367670024752 0ustar jaakkojaakko@summary{ Automap mobjs, red component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-wall-floorchange-g.ame0000664000175000017500000000011012641367670027223 0ustar jaakkojaakko@summary{ Automap floor height difference lines, green component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/view-bob-height.ame0000664000175000017500000000005612641367670025771 0ustar jaakkojaakko@summary{ Scale for viewheight bobbing. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-fog.ame0000664000175000017500000000015112641367670024522 0ustar jaakkojaakko@summary{ Menu fog mode: 0=off, 1=shimmer, 2=black smoke, 3=blue vertical, 4=grey smoke, 5=dimmed. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-deathmatch.ame0000664000175000017500000000007112641367670027323 0ustar jaakkojaakko@summary{ 1=Start multiplayer games as deathmatch. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/msg-count.ame0000664000175000017500000000010312641367670024716 0ustar jaakkojaakko@summary{ Number of HUD messages displayed at the same time. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-unhide-pickup-armor.ame0000664000175000017500000000010512641367670027445 0ustar jaakkojaakko@summary{ 1=Unhide the HUD when player collects an armor item. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-title-position.ame0000664000175000017500000000012112641367670026540 0ustar jaakkojaakko@summary{ Position of the map title when automap is open. 0=Top, 1=Bottom. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-name.ame0000664000175000017500000000004212641367670024477 0ustar jaakkojaakko@summary{ Current map name. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/inlude-stretch.ame0000664000175000017500000000012212641367670025735 0ustar jaakkojaakko@summary{ Intermission stretch-scaling strategy 0=Smart, 1=Never, 2=Always. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-weapon-order1.ame0000664000175000017500000000005512641367670026765 0ustar jaakkojaakko@summary{ Weapon change order, slot 1. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/chat-macro1.ame0000664000175000017500000000003612641367670025106 0ustar jaakkojaakko@summary{ Chat macro 2. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/input-mouse-y-sensi.ame0000664000175000017500000000005212641367670026657 0ustar jaakkojaakko@summary{ Mouse Y axis sensitivity. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-unhide-pickup-health.ame0000664000175000017500000000010512641367670027572 0ustar jaakkojaakko@summary{ 1=Unhide the HUD when player collects a health item. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/inlude-patch-replacement.ame0000664000175000017500000000013612641367670027662 0ustar jaakkojaakko@summary{ Intermission Patch Replacement strings. 1=Enable external, 2=Enable built-in. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-autoswitch.ame0000664000175000017500000000012612641367670026473 0ustar jaakkojaakko@summary{ Change weapon automatically when picking one up. 1=If better 2=Always } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-shadow.ame0000664000175000017500000000005312641367670025235 0ustar jaakkojaakko@summary{ Menu text shadow darkness. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-frags-all.ame0000664000175000017500000000007312641367670025436 0ustar jaakkojaakko@summary{ Debug: HUD shows all frags of all players. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-health.ame0000664000175000017500000000005012641367670025542 0ustar jaakkojaakko@summary{ Current health ammount. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/msg-blink.ame0000664000175000017500000000011112641367670024664 0ustar jaakkojaakko@summary{ HUD messages blink for this number of tics when printed. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-radiusattack-nomaxz.ame0000664000175000017500000000007212641367670031213 0ustar jaakkojaakko@summary{ 1=ALL radius attacks are infinitely tall. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/rend-dev-freeze-map.ame0000664000175000017500000000007512641367670026547 0ustar jaakkojaakko@summary{ 1=Stop updating the automap rendering lists. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-patch-replacement.ame0000664000175000017500000000012112641367670027154 0ustar jaakkojaakko@summary{ Patch Replacement strings. 1=Enable external, 2=Enable built-in. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-line-width.ame0000664000175000017500000000007712641367670025633 0ustar jaakkojaakko@summary{ Scale factor for automap lines (default: 1.1). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-wall-floorchange-b.ame0000664000175000017500000000010712641367670027224 0ustar jaakkojaakko@summary{ Automap floor height difference lines, blue component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-color-a.ame0000664000175000017500000000004612641367670025122 0ustar jaakkojaakko@summary{ HUD info alpha value. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-background-g.ame0000664000175000017500000000007312641367670026126 0ustar jaakkojaakko@summary{ Automap background color, green component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-unhide-pickup-powerup.ame0000664000175000017500000000012612641367670030031 0ustar jaakkojaakko@summary{ 1=Unhide the HUD when player collects a powerup or item of equipment. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-patch-replacement.ame0000664000175000017500000000012112641367670027340 0ustar jaakkojaakko@summary{ Patch Replacement strings. 1=Enable external, 2=Enable built-in. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-weapon-order2.ame0000664000175000017500000000005512641367670026766 0ustar jaakkojaakko@summary{ Weapon change order, slot 2. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-wall-ceilingchange-g.ame0000664000175000017500000000011212641367670027516 0ustar jaakkojaakko@summary{ Automap ceiling height difference lines, green component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-weapon-order3.ame0000664000175000017500000000005512641367670026767 0ustar jaakkojaakko@summary{ Weapon change order, slot 3. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-save-last-loadonreborn.ame0000664000175000017500000000012112641367670030272 0ustar jaakkojaakko@summary{ 1=Load the last used save slot on player reborn. (default: off). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-babykeys.ame0000664000175000017500000000007712641367670025400 0ustar jaakkojaakko@summary{ 1=Show keys in automap (easy skill mode only). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-mobj-g.ame0000664000175000017500000000006012641367670024732 0ustar jaakkojaakko@summary{ Automap mobjs, green component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-weapon-current.ame0000664000175000017500000000004412641367670027251 0ustar jaakkojaakko@summary{ Current weapon (id) } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-color-b.ame0000664000175000017500000000006012641367670025117 0ustar jaakkojaakko@summary{ HUD info color alpha component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-stretch.ame0000664000175000017500000000011212641367670025420 0ustar jaakkojaakko@summary{ Menu stretch-scaling strategy 0=Smart, 1=Never, 2=Always. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-music.ame0000664000175000017500000000005612641367670025040 0ustar jaakkojaakko@summary{ Currently playing music (id). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-line-opacity.ame0000664000175000017500000000007012641367670026155 0ustar jaakkojaakko@summary{ Opacity of automap lines (default: .7). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/view-cross-vitality.ame0000664000175000017500000000011412641367670026750 0ustar jaakkojaakko@summary{ Color the crosshair according to how near you are to death. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-title-author-noiwad.ame0000664000175000017500000000010612641367670027463 0ustar jaakkojaakko@summary{ 1=Do not show map author if it is a map from an IWAD. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/chat-macro6.ame0000664000175000017500000000003612641367670025113 0ustar jaakkojaakko@summary{ Chat macro 7. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-jump.ame0000664000175000017500000000006612641367670026200 0ustar jaakkojaakko@summary{ 1=Allow jumping in multiplayer games. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-color-g.ame0000664000175000017500000000005412641367670025313 0ustar jaakkojaakko@summary{ Menu color green component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-cursor-rotate.ame0000664000175000017500000000011012641367670026553 0ustar jaakkojaakko@summary{ 1=Menu cursor rotates on items with a range of options. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/chat-macro7.ame0000664000175000017500000000003612641367670025114 0ustar jaakkojaakko@summary{ Chat macro 8. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/ctl-look-joy.ame0000664000175000017500000000005012641367670025326 0ustar jaakkojaakko@summary{ 1=Joystick look active. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/ctl-look-speed.ame0000664000175000017500000000005612641367670025633 0ustar jaakkojaakko@summary{ The speed of looking up/down. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-weapon-order0.ame0000664000175000017500000000005512641367670026764 0ustar jaakkojaakko@summary{ Weapon change order, slot 0. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-background-b.ame0000664000175000017500000000007212641367670026120 0ustar jaakkojaakko@summary{ Automap background color, blue component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-mapcycle-noexit.ame0000664000175000017500000000007412641367670030325 0ustar jaakkojaakko@summary{ 1=Disable exit buttons during map rotation. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-music.ame0000664000175000017500000000005412641367670024702 0ustar jaakkojaakko@summary{ Music (id) for current map. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/msg-echo.ame0000664000175000017500000000006412641367670024512 0ustar jaakkojaakko@summary{ 1=Echo all messages to the console. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-scale.ame0000664000175000017500000000004612641367670024655 0ustar jaakkojaakko@summary{ Scaling for HUD info. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/input-joy-rx.ame0000664000175000017500000000005312641367670025373 0ustar jaakkojaakko@summary{ X rotational axis control. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-wall-unseen-r.ame0000664000175000017500000000006512641367670026255 0ustar jaakkojaakko@summary{ Automap unseen areas, red component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/ctl-look-joy-delta.ame0000664000175000017500000000006712641367670026425 0ustar jaakkojaakko@summary{ 1=Joystick values => look angle delta. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/ctl-look-pov.ame0000664000175000017500000000006112641367670025333 0ustar jaakkojaakko@summary{ 1=Look around using the POV hat. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-save-quick-slot.ame0000664000175000017500000000010512641367670026742 0ustar jaakkojaakko@summary{ Current "quick" save slot number. -1=None (default). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-wall-g.ame0000664000175000017500000000006012641367670024742 0ustar jaakkojaakko@summary{ Automap walls, green component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-mod-damage.ame0000664000175000017500000000010312641367670027210 0ustar jaakkojaakko@summary{ Enemy (mob) damage modifier, multiplayer (1..100). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-fastmonsters.ame0000664000175000017500000000007312641367670026447 0ustar jaakkojaakko@summary{ 1=Fast monsters in non-demo single player. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-weapon-cycle-sequential.ame0000664000175000017500000000010312641367670031032 0ustar jaakkojaakko@summary{ 1=Allow sequential weapon cycling whilst lowering. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-state-map.ame0000664000175000017500000000005312641367670025610 0ustar jaakkojaakko@summary{ 1=Currently playing a map. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/ctl-turn-speed.ame0000664000175000017500000000006112641367670025653 0ustar jaakkojaakko@summary{ The speed of turning left/right. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-pause-focuslost.ame0000664000175000017500000000007512641367670027055 0ustar jaakkojaakko@summary{ Pause the game when game window loses focus. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-unhide-pickup-weapon.ame0000664000175000017500000000010012641367670027611 0ustar jaakkojaakko@summary{ 1=Unhide the HUD when player collects a weapon. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-save-last-slot.ame0000664000175000017500000000012312641367670026571 0ustar jaakkojaakko@summary{ Last used save slot. -1=Not yet loaded/saved in this game session. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/view-cross-size.ame0000664000175000017500000000005212641367670026056 0ustar jaakkojaakko@summary{ Crosshair size: 1=Normal. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-wall-unseen-b.ame0000664000175000017500000000006612641367670026236 0ustar jaakkojaakko@summary{ Automap unseen areas, blue component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/input-joy-rz.ame0000664000175000017500000000005312641367670025375 0ustar jaakkojaakko@summary{ Z rotational axis control. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-pause-mapstart-tics.ame0000664000175000017500000000020412641367670027621 0ustar jaakkojaakko@summary{ Number of 35 Hz tics to pause the game after a map has been loaded. @cbr -1=Use the value of 'con-transition-tics'. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-pan-resetonopen.ame0000664000175000017500000000010712641367670026676 0ustar jaakkojaakko@summary{ 1=Reset automap pan location when opening the automap. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-title.ame0000664000175000017500000000007612641367670024712 0ustar jaakkojaakko@summary{ 1=Show map title and author in the beginning. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-save-confirm.ame0000664000175000017500000000007712641367670026314 0ustar jaakkojaakko@summary{ 1=Ask me to confirm when quick saving/loading. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-timer.ame0000664000175000017500000000007512641367670024710 0ustar jaakkojaakko@summary{ Number of seconds before the hud auto-hides. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-episode.ame0000664000175000017500000000004412641367670025211 0ustar jaakkojaakko@summary{ Current episode id. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-mobj-b.ame0000664000175000017500000000005712641367670024733 0ustar jaakkojaakko@summary{ Automap mobjs, blue component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/msg-color-b.ame0000664000175000017500000000006612641367670025133 0ustar jaakkojaakko@summary{ Color of HUD messages blue component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/player-camera-noclip.ame0000664000175000017500000000007412641367670027015 0ustar jaakkojaakko@summary{ 1=Camera players have no movement clipping. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-status-icon-a.ame0000664000175000017500000000007112641367670026253 0ustar jaakkojaakko@summary{ Status bar icons & counters Alpha level. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-status-alpha.ame0000664000175000017500000000005012641367670026167 0ustar jaakkojaakko@summary{ Status bar Alpha level. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-mod-health.ame0000664000175000017500000000010212641367670027236 0ustar jaakkojaakko@summary{ Enemy (mob) health modifier, multiplayer (1..20). } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/msg-show.ame0000664000175000017500000000004112641367670024547 0ustar jaakkojaakko@summary{ 1=Show messages. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-zoom-speed.ame0000664000175000017500000000007512641367670025647 0ustar jaakkojaakko@summary{ Zoom in/out speed multiplier in the automap. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/game-paused.ame0000664000175000017500000000003712641367670025200 0ustar jaakkojaakko@summary{ 1=Game paused. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-colorb-g.ame0000664000175000017500000000005612641367670025457 0ustar jaakkojaakko@summary{ Menu color B green component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-mod-gravity.ame0000664000175000017500000000011712641367670027464 0ustar jaakkojaakko@summary{ World gravity modifier, multiplayer (-1..100). -1=Map default. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/server-game-map.ame0000664000175000017500000000011212641367670025772 0ustar jaakkojaakko@summary{ Identifier of the map to be played in a multiplayer game. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/view-cross-g.ame0000664000175000017500000000006112641367670025332 0ustar jaakkojaakko@summary{ Crosshair color green component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/view-cross-r.ame0000664000175000017500000000005712641367670025352 0ustar jaakkojaakko@summary{ Crosshair color red component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-wall-floorchange-r.ame0000664000175000017500000000010612641367670027243 0ustar jaakkojaakko@summary{ Automap floor height difference lines, red component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-unhide-pickup-ammo.ame0000664000175000017500000000010412641367670027255 0ustar jaakkojaakko@summary{ 1=Unhide the HUD when player collects an ammo item. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/view-filter-strength.ame0000664000175000017500000000005112641367670027075 0ustar jaakkojaakko@summary{ Strength of view filter. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/map-pan-speed.ame0000664000175000017500000000006512641367670025440 0ustar jaakkojaakko@summary{ Pan speed multiplier in the automap. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/menu-flash-b.ame0000664000175000017500000000007412641367670025267 0ustar jaakkojaakko@summary{ Menu selection flash color, blue component. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/hud-unhide-damage.ame0000664000175000017500000000007712641367670026262 0ustar jaakkojaakko@summary{ 1=Unhide the HUD when player receives damaged. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/chat-macro9.ame0000664000175000017500000000003712641367670025117 0ustar jaakkojaakko@summary{ Chat macro 10. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/chat-beep.ame0000664000175000017500000000010512641367670024634 0ustar jaakkojaakko@summary{ 1=Play a beep sound when a new chat message arrives. } doomsday-stable-1.15.7/doomsday/doc/libcommon/variable/ctl-run.ame0000664000175000017500000000003612641367670024373 0ustar jaakkojaakko@summary{ 1=Always run. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/0000775000175000017500000000000012641367670022155 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/doc/libcommon/command/quicksave.ame0000664000175000017500000000004412641367670024632 0ustar jaakkojaakko@summary{ Quicksave the game. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/beginchat.ame0000664000175000017500000000004112641367670024560 0ustar jaakkojaakko@summary{ Begin chat mode. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/loadgame.ame0000664000175000017500000000057612641367670024422 0ustar jaakkojaakko@summary{ Load a game-save or open the load menu. } @description{ Params: loadgame (game-save-name||save-slot-num) (confirm) @cbr Keywords: last, quick @cbr Examples: @cbr Open load menu: 'loadgame' @cbr Loading named game: 'loadgame "running low on ammo"' @cbr Loading current "quick" slot, confirmed: 'loadgame quick confirm' @cbr Loading slot #0: 'loadgame 0' } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/setlock.ame0000664000175000017500000000004512641367670024304 0ustar jaakkojaakko@summary{ Set camera viewlock. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/setcolor.ame0000664000175000017500000000016312641367670024473 0ustar jaakkojaakko@summary{ Set player color. } @description{ Params: setcolor (playernum) @cbr For example, 'setcolor 4'. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/menuright.ame0000664000175000017500000000005412641367670024642 0ustar jaakkojaakko@summary{ Move the menu cursor right. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/menuleft.ame0000664000175000017500000000005312641367670024456 0ustar jaakkojaakko@summary{ Move the menu cursor left. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/menuup.ame0000664000175000017500000000005112641367670024146 0ustar jaakkojaakko@summary{ Move the menu cursor up. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/screenshot.ame0000664000175000017500000000015312641367670025015 0ustar jaakkojaakko@summary{ Take a screenshot. } @description{ The screenshot is saved into the runtime directory. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/menu.ame0000664000175000017500000000004512641367670023604 0ustar jaakkojaakko@summary{ Open/Close the menu. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/pause.ame0000664000175000017500000000010112641367670023746 0ustar jaakkojaakko@summary{ Pause the game (same as pressing the pause key). } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/movesec.ame0000664000175000017500000000005512641367670024302 0ustar jaakkojaakko@summary{ Move a sector's both planes. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/movefloor.ame0000664000175000017500000000005512641367670024651 0ustar jaakkojaakko@summary{ Move a sector's floor plane. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/chatsendmacro.ame0000664000175000017500000000004312641367670025451 0ustar jaakkojaakko@summary{ Send a chat macro. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/menuback.ame0000664000175000017500000000006212641367670024424 0ustar jaakkojaakko@summary{ Return to the previous menu page. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/makecam.ame0000664000175000017500000000016312641367670024237 0ustar jaakkojaakko@summary{ Toggle camera mode. } @description{ Params: makecam (playernum) @cbr For example, 'makecam 1'. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/menudown.ame0000664000175000017500000000005312641367670024473 0ustar jaakkojaakko@summary{ Move the menu cursor down. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/where.ame0000664000175000017500000000007312641367670023753 0ustar jaakkojaakko@summary{ Prints your map number and exact location. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/chatdelete.ame0000664000175000017500000000007112641367670024741 0ustar jaakkojaakko@summary{ Delete a character from the chat buffer. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/stopinf.ame0000664000175000017500000000007512641367670024325 0ustar jaakkojaakko@summary{ Stop the currently playing interlude/finale. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/endgame.ame0000664000175000017500000000003612641367670024240 0ustar jaakkojaakko@summary{ End the game. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/reveal.ame0000664000175000017500000000023012641367670024112 0ustar jaakkojaakko@summary{ Map cheat. } @description{ Params: reveal (0-4) @cbr Modes: @cbr 0=nothing @cbr 1=show unseen @cbr 2=full map @cbr 3=map+things } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/makelocp.ame0000664000175000017500000000016412641367670024435 0ustar jaakkojaakko@summary{ Make local player. } @description{ Params: makelocp (playernum) @cbr For example, 'makelocp 1'. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/menuselect.ame0000664000175000017500000000006512641367670025006 0ustar jaakkojaakko@summary{ Select/Accept the current menu item. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/kill.ame0000664000175000017500000000006212641367670023572 0ustar jaakkojaakko@summary{ Kill all the monsters on the map. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/startcycle.ame0000664000175000017500000000004412641367670025014 0ustar jaakkojaakko@summary{ Begin map rotation. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/coord.ame0000664000175000017500000000007412641367670023750 0ustar jaakkojaakko@summary{ Print the coordinates of the consoleplayer. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/lockmode.ame0000664000175000017500000000012612641367670024435 0ustar jaakkojaakko@summary{ Set camera viewlock mode. } @description{ Params: lockmode (0-1). } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/messagecancel.ame0000664000175000017500000000010312641367670025425 0ustar jaakkojaakko@summary{ Trigger a "CANCEL" response in the message prompt. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/endcycle.ame0000664000175000017500000000004212641367670024423 0ustar jaakkojaakko@summary{ End map rotation. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/quickload.ame0000664000175000017500000000005212641367670024612 0ustar jaakkojaakko@summary{ Load the quicksaved game. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/warp.ame0000664000175000017500000000003712641367670023612 0ustar jaakkojaakko@summary{ Warp to a map. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/messageyes.ame0000664000175000017500000000010012641367670024775 0ustar jaakkojaakko@summary{ Trigger a "YES" response in the message prompt. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/suicide.ame0000664000175000017500000000006312641367670024265 0ustar jaakkojaakko@summary{ Kill yourself. What did you think? } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/spawnmobj.ame0000664000175000017500000000004212641367670024635 0ustar jaakkojaakko@summary{ Spawn a new mobj. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/message.ame0000664000175000017500000000021612641367670024264 0ustar jaakkojaakko@summary{ Show a local game message. } @description{ @params{message (text)} @cbr @example{@code{message "this is a message"}} } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/savegame.ame0000664000175000017500000000076712641367670024443 0ustar jaakkojaakko@summary{ Create a new game-save or open the save menu. } @description{ @params{ savegame (game-save-name||save-slot-num) (new game-save-name) (confirm) @cbr Keywords: last, quick } @cbr @example{ @code{savegame} opens the Save Menu @cbr @code{savegame quick "running low on ammo"} saves to current "quick" slot @cbr @code{savegame 0 "running low on ammo"} saves to slot #0 } } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/leavemap.ame0000664000175000017500000000056512641367670024441 0ustar jaakkojaakko@summary{ Leave the current map and load the next according to the map progression of the current episode. } @description{ @usage @ident{leavemap} @help_optionalarg{exit} If @help_arg{exit} is specified, an exit by this name will be searched for in the defined map progression for the current episode, rather than the default exit; "next". } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/chatcancel.ame0000664000175000017500000000007412641367670024727 0ustar jaakkojaakko@summary{ Exit chat mode without sending the message. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/deletegamesave.ame0000664000175000017500000000050612641367670025615 0ustar jaakkojaakko@summary{ Deletes a game-save state. } @description{ Params: deletegamesave (game-save-name||save-slot-num) (confirm) @cbr Keywords: last, quick @cbr Examples: @cbr A game save by name 'deletegamesave "running low on ammo"' @cbr Last game save in the "quick" slot, confirmed: 'deletegamesave quick confirm' } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/noclip.ame0000664000175000017500000000012012641367670024116 0ustar jaakkojaakko@summary{ Toggle movement clipping on/off, to walk through walls (cheat). } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/togglegamma.ame0000664000175000017500000000005712641367670025127 0ustar jaakkojaakko@summary{ Cycle gamma correction levels. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/moveceil.ame0000664000175000017500000000005712641367670024446 0ustar jaakkojaakko@summary{ Move a sector's ceiling plane. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/chatcomplete.ame0000664000175000017500000000007212641367670025310 0ustar jaakkojaakko@summary{ Send the chat message and exit chat mode. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/helpscreen.ame0000664000175000017500000000004712641367670024772 0ustar jaakkojaakko@summary{ Show the Help screens. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/messageno.ame0000664000175000017500000000007712641367670024626 0ustar jaakkojaakko@summary{ Trigger a "NO" response in the message prompt. } doomsday-stable-1.15.7/doomsday/doc/libcommon/command/startinf.ame0000664000175000017500000000020212641367670024465 0ustar jaakkojaakko@summary{ Start an InFine script. } @description{ Params: startinf (script-id) @cbr For example, 'startinf coolscript'. } doomsday-stable-1.15.7/doomsday/config_unix_any.pri0000664000175000017500000000327312641367670021713 0ustar jaakkojaakko# The Doomsday Engine Project # Copyright (c) 2011-2013 Jaakko Keränen # Copyright (c) 2011-2013 Daniel Swanson # Common Unix build options for all Unix-compatible platforms (assumes gcc/clang). DEFINES += UNIX # Ease up on the warnings. (The old C code is a bit messy.) QMAKE_CFLAGS_WARN_ON -= -Wall QMAKE_CFLAGS_WARN_ON -= -W QMAKE_CFLAGS_WARN_ON += -Werror-implicit-function-declaration -fdiagnostics-show-option deng_debuginfo { # Inclusion of debug info was requested. QMAKE_CFLAGS += -g QMAKE_CXXFLAGS += -g } *-g++*|*-gcc* { # Allow //-comments and anonymous structs inside unions. QMAKE_CFLAGS += -std=c99 -fms-extensions } *-clang* { QMAKE_CFLAGS_WARN_ON += -Wno-tautological-compare } deng_c++11|c++11 { QMAKE_CXXFLAGS *= -std=c++11 } # Print include directories and other info. #QMAKE_CFLAGS += -Wp,-v #QMAKE_CXXFLAGS += -Wp,-v #QMAKE_LFLAGS += -v # Unix System Tools ---------------------------------------------------------- isEmpty(PKG_CONFIG) { PKG_CONFIG = pkg-config } # Python 2 to be used in generated scripts. isEmpty(SCRIPT_PYTHON) { exists(/usr/bin/python2.7): SCRIPT_PYTHON = /usr/bin/python2.7 exists(/usr/bin/python2): SCRIPT_PYTHON = /usr/bin/python2 exists(/usr/bin/python): SCRIPT_PYTHON = /usr/bin/python exists(/usr/local/bin/python): SCRIPT_PYTHON = /usr/local/bin/python } isEmpty(SCRIPT_PYTHON) { # Check the system path. SCRIPT_PYTHON = $$system(which python2.7) } isEmpty(SCRIPT_PYTHON) { # Check the system path. SCRIPT_PYTHON = $$system(which python2) } isEmpty(SCRIPT_PYTHON) { # Check the system path. SCRIPT_PYTHON = $$system(which python) } doomsday-stable-1.15.7/doomsday/dep_curses.pri0000664000175000017500000000055412641367670020667 0ustar jaakkojaakko# Build configuration for ncurses. macx { LIBS += -lcurses } else:win32 { # Curses is not used on Windows. } else { # Generic Unix. !system($$PKG_CONFIG --exists ncurses) { error(Missing dependency: ncurses) } QMAKE_CXXFLAGS += $$system($$PKG_CONFIG --cflags ncurses) LIBS += $$system($$PKG_CONFIG --libs ncurses) } doomsday-stable-1.15.7/doomsday/libgui/0000775000175000017500000000000012641367671017267 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/libgui.pro0000664000175000017500000001556712641367671021302 0ustar jaakkojaakko# The Doomsday Engine Project -- Graphics, Audio and Input Library # Copyright (c) 2013 Jaakko Keränen # # This program is distributed under the GNU Lesser General Public License # version 3 (or, at your option, any later version). Please visit # http://www.gnu.org/licenses/lgpl.html for details. include(../config.pri) TEMPLATE = lib TARGET = deng_gui VERSION = $$DENG_VERSION CONFIG += deng_qtgui deng_qtopengl deng_qt5:unix:!macx: QT += x11extras include(../dep_core.pri) include(../dep_opengl.pri) include(../dep_assimp.pri) DEFINES += __LIBGUI__ INCLUDEPATH += include win32 { # Keep the version number out of the file name. TARGET_EXT = .dll } else:macx { useFramework(Cocoa) } else:unix { LIBS += -lX11 # DisplayMode uses the Xrandr and XFree86-VideoMode extensions. !deng_nodisplaymode { # Check that the X11 extensions exist. !system($$PKG_CONFIG --exists xxf86vm) { error(Missing dependency: X11 XFree86 video mode extension library (development headers). Alternatively disable display mode functionality with: CONFIG+=deng_nodisplaymode) } !system($$PKG_CONFIG --exists xrandr) { error(Missing dependency: X11 RandR extension library (development headers). Alternatively disable display mode functionality with: CONFIG+=deng_nodisplaymode) } QMAKE_CXXFLAGS += $$system($$PKG_CONFIG xrandr xxf86vm --cflags) LIBS += $$system($$PKG_CONFIG xrandr xxf86vm --libs) } } PRECOMPILED_HEADER = src/precompiled.h HEADERS += src/precompiled.h # Public headers. publicHeaders(root, \ include/de/Atlas \ include/de/AtlasTexture \ include/de/Canvas \ include/de/CanvasWindow \ include/de/ColorBank \ include/de/DisplayMode \ include/de/Drawable \ include/de/Font \ include/de/FontBank \ include/de/GLBuffer \ include/de/GLFramebuffer \ include/de/GLInfo \ include/de/GLPixelFormat \ include/de/GLProgram \ include/de/GLShader \ include/de/GLShaderBank \ include/de/GLState \ include/de/GLTarget \ include/de/GLTexture \ include/de/GLUniform \ include/de/GuiApp \ include/de/HeightMap \ include/de/Image \ include/de/ImageBank \ include/de/KdTreeAtlasAllocator \ include/de/KeyEvent \ include/de/KeyEventSource \ include/de/MouseEvent \ include/de/MouseEventSource \ include/de/ModelBank \ include/de/ModelDrawable \ include/de/NativeFont \ include/de/PersistentCanvasWindow \ include/de/RowAtlasAllocator \ include/de/Sound \ include/de/TextureBank \ include/de/VertexBuilder \ include/de/Waveform \ include/de/WaveformBank \ ) publicHeaders(gui, \ include/de/gui/canvas.h \ include/de/gui/canvaswindow.h \ include/de/gui/displaymode.h \ include/de/gui/displaymode_native.h \ include/de/gui/guiapp.h \ include/de/gui/libgui.h \ include/de/gui/persistentcanvaswindow.h \ ) publicHeaders(audio, \ include/de/audio/sound.h \ include/de/audio/waveform.h \ include/de/audio/waveformbank.h \ ) publicHeaders(graphics, \ include/de/graphics/atlas.h \ include/de/graphics/atlastexture.h \ include/de/graphics/colorbank.h \ include/de/graphics/drawable.h \ include/de/graphics/glbuffer.h \ include/de/graphics/glentrypoints.h \ include/de/graphics/glframebuffer.h \ include/de/graphics/glinfo.h \ include/de/graphics/glpixelformat.h \ include/de/graphics/glprogram.h \ include/de/graphics/glshader.h \ include/de/graphics/glshaderbank.h \ include/de/graphics/glstate.h \ include/de/graphics/gltarget.h \ include/de/graphics/gltexture.h \ include/de/graphics/gluniform.h \ include/de/graphics/heightmap.h \ include/de/graphics/image.h \ include/de/graphics/imagebank.h \ include/de/graphics/kdtreeatlasallocator.h \ include/de/graphics/opengl.h \ include/de/graphics/modeldrawable.h \ include/de/graphics/modelbank.h \ include/de/graphics/rowatlasallocator.h \ include/de/graphics/texturebank.h \ include/de/graphics/vertexbuilder.h \ ) publicHeaders(input, \ include/de/input/ddkey.h \ include/de/input/keyevent.h \ include/de/input/keyeventsource.h \ include/de/input/mouseevent.h \ include/de/input/mouseeventsource.h \ ) publicHeaders(text, \ include/de/text/font.h \ include/de/text/fontbank.h \ include/de/text/nativefont.h \ ) # Sources and private headers. SOURCES += \ src/audio/sound.cpp \ src/audio/waveform.cpp \ src/audio/waveformbank.cpp \ src/canvas.cpp \ src/canvaswindow.cpp \ src/displaymode.cpp \ src/graphics/atlas.cpp \ src/graphics/atlastexture.cpp \ src/graphics/colorbank.cpp \ src/graphics/drawable.cpp \ src/graphics/glbuffer.cpp \ src/graphics/glentrypoints.cpp \ src/graphics/glentrypoints_x11.cpp \ src/graphics/glframebuffer.cpp \ src/graphics/glinfo.cpp \ src/graphics/glprogram.cpp \ src/graphics/glshaderbank.cpp \ src/graphics/glshader.cpp \ src/graphics/glstate.cpp \ src/graphics/gltarget_alternativebuffer.cpp \ src/graphics/gltarget.cpp \ src/graphics/gltexture.cpp \ src/graphics/gluniform.cpp \ src/graphics/heightmap.cpp \ src/graphics/imagebank.cpp \ src/graphics/image.cpp \ src/graphics/kdtreeatlasallocator.cpp \ src/graphics/modelbank.cpp \ src/graphics/modeldrawable.cpp \ src/graphics/rowatlasallocator.cpp \ src/graphics/texturebank.cpp \ src/guiapp.cpp \ src/input/keyevent.cpp \ src/input/keyeventsource.cpp \ src/input/mouseevent.cpp \ src/input/mouseeventsource.cpp \ src/persistentcanvaswindow.cpp \ src/text/fontbank.cpp \ src/text/font.cpp \ src/text/font_richformat.cpp \ src/text/nativefont.cpp \ src/text/qtnativefont.cpp \ src/text/qtnativefont.h macx:!deng_macx6_32bit_64bit: SOURCES += \ src/text/coretextnativefont_macx.h \ src/text/coretextnativefont_macx.cpp # DisplayMode !deng_nodisplaymode { win32: SOURCES += src/displaymode_windows.cpp else:macx: OBJECTIVE_SOURCES += src/displaymode_macx.mm else: SOURCES += src/displaymode_x11.cpp } else { SOURCES += src/displaymode_dummy.cpp } unix:!macx: SOURCES += src/input/imKStoUCS_x11.c scripts.files = \ modules/gui.de OTHER_FILES += \ $$scripts.files # Installation --------------------------------------------------------------- macx { xcodeFinalizeBuild($$TARGET) linkDylibToBundledLibcore(libdeng_gui) doPostLink("install_name_tool -id @rpath/libdeng_gui.1.dylib libdeng_gui.1.dylib") # Prepare Assimp for deployment. doPostLink("cp -fRp $$ASSIMP_DIR/lib/libassimp*dylib .") doPostLink("install_name_tool -id @rpath/libassimp.3.dylib libassimp.3.dylib") linkBinaryToBundledAssimp(libdeng_gui.1.dylib, ..) } buildPackage(net.dengine.stdlib.gui, $$OUT_PWD/..) deployLibrary() doomsday-stable-1.15.7/doomsday/libgui/net.dengine.stdlib.gui.pack/0000775000175000017500000000000012641367671024445 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/net.dengine.stdlib.gui.pack/Info0000664000175000017500000000017112641367671025262 0ustar jaakkojaakkotitle: Doomsday Script Standard Library: GUI version: 1.15.7 license: GPL 3+ tags: core script gui importPath doomsday-stable-1.15.7/doomsday/libgui/net.dengine.stdlib.gui.pack/modules/0000775000175000017500000000000012641367671026115 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/net.dengine.stdlib.gui.pack/modules/gui.de0000664000175000017500000001030512641367671027212 0ustar jaakkojaakko# The Doomsday Engine Project # # Copyright (c) 2013-2014 Jaakko Keränen # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation; either version 3 of the License, or (at # your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUTANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public # License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, see . #---------------------------------------------------------------------------- # Graphical user interface import Version def setDefaults(d) # Applies the default configuration settings. # - d: Record where to set the values. try import DisplayMode # The default audio and video subsystems. d.video = 'opengl' d.audio = 'fmod' # Generic user interface settings. record d.ui d.ui.showAnnotations = True # Window manager defaults. record d.window d.window.fsaa = False # Remove this (should be window-specific). # Configure the main window. record d.window.main d.window.main.showFps = False d.window.main.center = True d.window.main.fsaa = False d.window.main.vsync = True # The default window parameters depend on the original display mode. mode = DisplayMode.originalMode() # By default the fullscreen resolution is the desktop resolution. d.window.main.fullSize = [mode['width'], mode['height']] # In windowed mode mode, leave some space on the sides so that # the first switch to windowed mode does not place the window in an # inconvenient location. The reduction is done proportionally. offx = mode['width'] * 0.15 offy = mode['height'] * 0.15 d.window.main.rect = [offx, offy, mode['width'] - 2*offx, mode['height'] - 2*offy] d.window.main.colorDepth = mode['depth'] if Version.OS == 'windows' or Version.OS == 'macx' d.window.main.fullscreen = True d.window.main.maximize = False else d.window.main.fullscreen = False d.window.main.maximize = True end catch NotFoundError # DisplayMode isn't available on the server. end end def scale(value, factor) # Scales a value by 'factor'. If 'value' is a text string, the # suffixes "pt" and "px" (point, pixel) are retained in the result. # - src: Value to scale. Number or Text, may have "pt" or "px" # as suffix. # - factor: Scale factor (Number). unit = '' p = Text(value) amount = p if p[-2:] == 'pt' or p[-2:] == 'px' unit = p[-2:] amount = p[:-2] end return Text(floor(Number(amount) * factor)) + unit end def colorMix(a, b, amount) # Returns a color where vectors 'a' and 'b' are interpolated by # 'amount'. If 'amount' is 0, the result is 'a'; if 'amount' is 1, # the result is 'b'. if len(a) < 4: a += [1.0] if len(b) < 4: b += [1.0] for i in [0, 1, 2, 3] a[i] = Number(a[i]) b[i] = Number(b[i]) end inv = 1.0 - amount return [a[0] * inv + b[0] * amount, a[1] * inv + b[1] * amount, a[2] * inv + b[2] * amount, a[3] * inv + b[3] * amount] end def colorAlpha(colorVector, alpha) # Returns a new color with the alpha value changed to 'alpha'. v = colorVector if len(v) >= 4: v[3] = alpha elsif len(v) == 3: v += [alpha] return v end def dpiScaledImagePath(path) # Returns a version of 'path' where "@2x" is appended to the end # if DPI_FACTOR is 2. import DisplayMode p = Text(path) if DisplayMode.DPI_FACTOR == 2 return (p.fileNameAndPathWithoutExtension() + "@2x" + p.fileNameExtension()) end return p end doomsday-stable-1.15.7/doomsday/libgui/include/0000775000175000017500000000000012641367671020712 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/include/de/0000775000175000017500000000000012641367671021302 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/include/de/Atlas0000664000175000017500000000003412641367671022266 0ustar jaakkojaakko#include "graphics/atlas.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/ModelBank0000664000175000017500000000004012641367671023053 0ustar jaakkojaakko#include "graphics/modelbank.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/Image0000664000175000017500000000003412641367671022244 0ustar jaakkojaakko#include "graphics/image.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/0000775000175000017500000000000012641367671023102 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/gluniform.h0000664000175000017500000001067712641367671025270 0ustar jaakkojaakko/** @file gluniform.h GL uniform. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_GLUNIFORM_H #define LIBGUI_GLUNIFORM_H #include #include #include #include #include "../gui/libgui.h" #include namespace de { class GLProgram; class GLTexture; /** * Constant variable or a sampler in a shader. * * GLUniform's public interface allows the uniform value to be manipulated like * any other native variable (assignment, arithmetic, etc.). Think of GLUniform * instances as a native manifestation of shader uniform/attribute variables. * * The value of the uniform is stored locally in the GLUniform instance. When * the uniform has been bound to programs and its value changes, the programs * are notified and they mark the uniform as changed. When the program is then * later taken into use, the updated value of the changed uniforms is sent to * GL. * * @ingroup gl */ class LIBGUI_PUBLIC GLUniform { public: enum Type { Int, UInt, Float, Vec2, Vec3, Vec4, Mat3, Mat4, Sampler2D, Vec3Array, Vec4Array, Mat4Array }; /** * Notified when the value of the uniform changes. */ DENG2_DEFINE_AUDIENCE2(ValueChange, void uniformValueChanged(GLUniform &)) /** * Notified when the uniform instance is deleted. */ DENG2_DEFINE_AUDIENCE2(Deletion, void uniformDeleted(GLUniform &)) public: GLUniform(char const *nameInShader, Type uniformType, duint elements = 1); void setName(char const *nameInShader); /** * Returns the name of the uniform as it appears in shaders. */ QLatin1String name() const; /** * Returns the value type of the shader. */ Type type() const; GLUniform &operator = (dint value); GLUniform &operator = (duint value); GLUniform &operator = (dfloat value); GLUniform &operator = (ddouble value); GLUniform &operator = (Vector2f const &vec); GLUniform &operator = (Vector3f const &vec); GLUniform &operator = (Vector4f const &vec); GLUniform &operator = (Matrix3f const &vec); GLUniform &operator = (Matrix4f const &mat); GLUniform &operator = (GLTexture const &texture); GLUniform &operator = (GLTexture const *texture); GLUniform &set(duint elementIndex, Vector3f const &vec); GLUniform &set(duint elementIndex, Vector4f const &vec); GLUniform &set(duint elementIndex, Matrix4f const &mat); operator dint() const { return toInt(); } operator duint() const { return toUInt(); } operator dfloat() const { return toFloat(); } operator ddouble() const { return ddouble(toFloat()); } operator Vector2f() const { return toVector2f(); } operator Vector3f() const { return toVector3f(); } operator Vector4f() const { return toVector4f(); } operator Matrix3f const &() const { return toMatrix3f(); } operator Matrix4f const &() const { return toMatrix4f(); } operator GLTexture const *() const { return texture(); } dint toInt() const; duint toUInt() const; dfloat toFloat() const; Vector2f const &toVector2f() const; Vector3f const &toVector3f() const; Vector4f const &toVector4f() const; Matrix3f const &toMatrix3f() const; Matrix4f const &toMatrix4f() const; GLTexture const *texture() const; /** * Updates the value of the uniform in a particular GL program. * * @param program GL program instance. */ void applyInProgram(GLProgram &program) const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_GLUNIFORM_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/glpixelformat.h0000664000175000017500000000254212641367671026133 0ustar jaakkojaakko/** @file glpixelformat.h Pixel format specification for GL. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_GLPIXELFORMAT_H #define LIBGUI_GLPIXELFORMAT_H #include "../gui/libgui.h" namespace de { /** * GL image format with data type for glTex(Sub)Image. */ struct LIBGUI_PUBLIC GLPixelFormat { duint format; duint type; duint rowAlignment; GLPixelFormat(duint glFormat, duint glDataType = 0, duint glRowAlignment = 0) : format(glFormat) , type(glDataType) , rowAlignment(glRowAlignment) {} }; } // namespace de #endif // LIBGUI_GLPIXELFORMAT_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/texturebank.h0000664000175000017500000000370512641367671025614 0ustar jaakkojaakko/** @file texturebank.h Bank for images stored in a texture atlas. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_TEXTUREBANK_H #define LIBGUI_TEXTUREBANK_H #include #include "../AtlasTexture" namespace de { /** * Bank that stores images on a texture atlas for use in GL drawing. * * The data item sources in the bank must be derived from TextureBank::ImageSource. * * @ingroup gl */ class LIBGUI_PUBLIC TextureBank : public Bank { public: /** * Base classs for entries in the bank. When requested, provides the Image data * of the specified item. */ class LIBGUI_PUBLIC ImageSource : public ISource { public: ImageSource(DotPath const &id = ""); DotPath const &id() const; virtual Image load() const = 0; private: DENG2_PRIVATE(d) }; public: TextureBank(); /** * Sets the atlas where the images are to be allocated from. * * @param atlas Texture atlas. */ void setAtlas(AtlasTexture &atlas); Id const &texture(DotPath const &id); protected: IData *loadFromSource(ISource &source); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_TEXTUREBANK_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/glbuffer.h0000664000175000017500000002175112641367671025055 0ustar jaakkojaakko/** @file glbuffer.h GL vertex buffer. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_GLBUFFER_H #define LIBGUI_GLBUFFER_H #include #include #include #include #include #include "../gui/libgui.h" #include "opengl.h" #include "../VertexBuilder" namespace de { namespace internal { /// Describes an attribute array inside a GL buffer. struct AttribSpec { enum Semantic { Position, TexCoord0, TexCoord1, TexCoord2, TexCoord3, TexBounds0, TexBounds1, TexBounds2, TexBounds3, Color, Normal, Tangent, Bitangent, BoneIDs, BoneWeights, InstanceMatrix, // x4 InstanceColor, NUM_SEMANTICS }; Semantic semantic; dint size; ///< Number of components in an element. GLenum type; ///< Data type. bool normalized; ///< Whether to normalize non-floats to [0.f, 1.f]. dsize stride; ///< Number of bytes between elements. duint startOffset; ///< Offset in bytes from the start of the buffer. }; typedef std::pair AttribSpecs; } #define LIBGUI_DECLARE_VERTEX_FORMAT(NumElems) \ public: static internal::AttribSpecs formatSpec(); \ private: static internal::AttribSpec const _spec[NumElems]; \ public: #define LIBGUI_VERTEX_FORMAT_SPEC(TypeName, ExpectedSize) \ internal::AttribSpecs TypeName::formatSpec() { \ DENG2_ASSERT(sizeof(TypeName) == ExpectedSize); /* sanity check */ \ return internal::AttribSpecs(_spec, sizeof(_spec)/sizeof(_spec[0])); \ } /** * Vertex format with 2D coordinates and one set of texture coordinates. */ struct LIBGUI_PUBLIC Vertex2Tex { Vector2f pos; Vector2f texCoord; LIBGUI_DECLARE_VERTEX_FORMAT(2) }; /** * Vertex format with 2D coordinates and a color. */ struct LIBGUI_PUBLIC Vertex2Rgba { Vector2f pos; Vector4f rgba; LIBGUI_DECLARE_VERTEX_FORMAT(2) }; /** * Vertex format with 2D coordinates, one set of texture coordinates, and an * RGBA color. */ struct LIBGUI_PUBLIC Vertex2TexRgba { Vector2f pos; Vector2f texCoord; Vector4f rgba; LIBGUI_DECLARE_VERTEX_FORMAT(3) }; /** * Vertex format with just 3D coordinates. */ struct LIBGUI_PUBLIC Vertex3 { Vector3f pos; LIBGUI_DECLARE_VERTEX_FORMAT(1) }; /** * Vertex format with 3D coordinates and one set of texture coordinates. */ struct LIBGUI_PUBLIC Vertex3Tex { Vector3f pos; Vector2f texCoord; LIBGUI_DECLARE_VERTEX_FORMAT(2) }; /** * Vertex format with 3D coordinates, one set of texture coordinates, and an * RGBA color. */ struct LIBGUI_PUBLIC Vertex3TexRgba { Vector3f pos; Vector2f texCoord; Vector4f rgba; LIBGUI_DECLARE_VERTEX_FORMAT(3) }; /** * Vertex format with 3D coordinates, one set of texture coordinates with indirect * bounds, and an RGBA color. */ struct LIBGUI_PUBLIC Vertex3TexBoundsRgba { Vector3f pos; Vector2f texCoord; ///< mapped using texBounds Vector4f texBounds; ///< UV space: x, y, width, height Vector4f rgba; LIBGUI_DECLARE_VERTEX_FORMAT(4) }; /** * Vertex format with 3D coordinates, two sets of texture coordinates with indirect * bounds, and an RGBA color. */ struct LIBGUI_PUBLIC Vertex3Tex2BoundsRgba { Vector3f pos; Vector2f texCoord[2]; Vector4f texBounds; ///< UV space: x, y, width, height Vector4f rgba; LIBGUI_DECLARE_VERTEX_FORMAT(5) }; /** * Vertex format with 3D coordinates, two sets of texture coordinates, and an * RGBA color. */ struct LIBGUI_PUBLIC Vertex3Tex2Rgba { Vector3f pos; Vector2f texCoord[2]; Vector4f rgba; LIBGUI_DECLARE_VERTEX_FORMAT(4) }; /** * Vertex format with 3D coordinates, three sets of texture coordinates, and an * RGBA color. */ struct LIBGUI_PUBLIC Vertex3Tex3Rgba { Vector3f pos; Vector2f texCoord[3]; Vector4f rgba; LIBGUI_DECLARE_VERTEX_FORMAT(5) }; /** * Vertex format with 3D coordinates, normal vector, one set of texture * coordinates, and an RGBA color. */ struct LIBGUI_PUBLIC Vertex3NormalTexRgba { Vector3f pos; Vector3f normal; Vector2f texCoord; Vector4f rgba; LIBGUI_DECLARE_VERTEX_FORMAT(4) }; /** * Vertex format with 3D coordinates, normal/tangent/bitangent vectors, one set of * texture coordinates, and an RGBA color. */ struct LIBGUI_PUBLIC Vertex3NormalTangentTex { Vector3f pos; Vector3f normal; Vector3f tangent; Vector3f bitangent; Vector2f texCoord; LIBGUI_DECLARE_VERTEX_FORMAT(5) }; namespace gl { enum Usage { Static, Dynamic, Stream }; enum Primitive { Points, LineStrip, LineLoop, Lines, TriangleStrip, TriangleFan, Triangles }; } /** * GL vertex buffer. * * Supports both indexed and non-indexed drawing. The primitive type has to be * specified either when setting the vertices (for non-indexed drawing) or when * specifying the indices (for indexed drawing). * * @note Compatible with OpenGL ES 2.0. * * @todo Add a method for replacing a portion of the existing data in the buffer * (using glBufferSubData). * * @ingroup gl */ class LIBGUI_PUBLIC GLBuffer : public Asset { public: typedef duint16 Index; typedef QVector Indices; public: GLBuffer(); void clear(); void setVertices(dsize count, void const *data, dsize dataSize, gl::Usage usage); void setVertices(gl::Primitive primitive, dsize count, void const *data, dsize dataSize, gl::Usage usage); void setIndices(gl::Primitive primitive, dsize count, Index const *indices, gl::Usage usage); void setIndices(gl::Primitive primitive, Indices const &indices, gl::Usage usage); /** * Draws the buffer. * * Requires that a GLProgram is in use so that attribute locations can be determined. * * @param first First vertex to start drawing from. * @param count Number of vertices to draw. */ void draw(duint first = 0, dint count = -1) const; /** * Draws the buffer with instancing. One instance of the buffer is drawn per * each element in the provided @a instanceAttribs buffer. * * Requires that a GLProgram is in use so that attribute locations can be determined. * Also GL_ARB_instanced_arrays and GL_ARB_draw_instanced must be available. * * @param instanceAttribs Buffer containing the attributes data for each instance. * @param first First vertex in this buffer to start drawing from. * @param count Number of vertices in this buffer to draw in each instance. */ void drawInstanced(GLBuffer const &instanceAttribs, duint first = 0, dint count = -1) const; /** * Returns the number of vertices in the buffer. */ dsize count() const; protected: void setFormat(internal::AttribSpecs const &format); private: DENG2_PRIVATE(d) }; /** * Template for a vertex buffer with a specific vertex format. */ template class GLBufferT : public GLBuffer { public: typedef VertexType Type; typedef QVector Vertices; typedef typename VertexBuilder::Vertices Builder; public: GLBufferT() { setFormat(VertexType::formatSpec()); } void setVertices(VertexType const *vertices, dsize count, gl::Usage usage) { GLBuffer::setVertices(count, vertices, sizeof(VertexType) * count, usage); } void setVertices(Vertices const &vertices, gl::Usage usage) { GLBuffer::setVertices(vertices.size(), vertices.constData(), sizeof(VertexType) * vertices.size(), usage); } void setVertices(gl::Primitive primitive, VertexType const *vertices, dsize count, gl::Usage usage) { GLBuffer::setVertices(primitive, count, vertices, sizeof(VertexType) * count, usage); } void setVertices(gl::Primitive primitive, Vertices const &vertices, gl::Usage usage) { GLBuffer::setVertices(primitive, vertices.size(), vertices.constData(), sizeof(VertexType) * vertices.size(), usage); } }; } // namespace de #endif // LIBGUI_GLBUFFER_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/gltexture.h0000664000175000017500000002015712641367671025303 0ustar jaakkojaakko/** @file gltexture.h GL texture. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_GLTEXTURE_H #define LIBGUI_GLTEXTURE_H #include #include #include #include "../gui/libgui.h" #include "opengl.h" #include "../Image" #include "../GLPixelFormat" namespace de { namespace gl { enum Filter { Nearest, Linear }; enum MipFilter { MipNone, MipNearest, MipLinear }; enum Wrapping { Repeat, RepeatMirrored, ClampToEdge }; enum CubeFace { PositiveX, NegativeX, PositiveY, NegativeY, PositiveZ, NegativeZ }; } /** * GL texture object. * * Supports cube maps (6 faces/images instead of one). A GLTexture becomes a * cube map automatically when one sets image content to one of the faces. * Similarly a GLTexture reverts back to a 2D texture when setting non-cubeface * image content. * * Mipmaps can be generated automatically (see GLTexture::generateMipmap() and * GLTexture::setAutoGenMips()). By default, mipmaps are not generated * automatically. * * @ingroup gl */ class LIBGUI_PUBLIC GLTexture : public Asset { public: typedef Vector2ui Size; typedef Vector2 Wraps; public: /** * Constructs a texture without any content. GLTexture instances are not * ready until they have some content defined. */ GLTexture(); /** * Constructs a texture from an existing OpenGL texture object. Ownership * of the texture is transferred to the new GLTexture instance. * * @param existingTexture Existing texture. * @param size Size of the texture in texels. */ GLTexture(GLuint existingTexture, Size const &size); /** * Release all image content associated with the texture. */ void clear(); void setMagFilter(gl::Filter magFilter); void setMinFilter(gl::Filter minFilter, gl::MipFilter mipFilter); void setFilter(gl::Filter magFilter, gl::Filter minFilter, gl::MipFilter mipFilter) { setMagFilter(magFilter); setMinFilter(minFilter, mipFilter); } void setWrapS(gl::Wrapping mode); void setWrapT(gl::Wrapping mode); inline void setWrap(gl::Wrapping s, gl::Wrapping t) { setWrapS(s); setWrapT(t); } inline void setWrap(Wraps const &st) { setWrapS(st.x); setWrapT(st.y); } void setMaxAnisotropy(dfloat maxAnisotropy); void setMaxLevel(dfloat maxLevel); gl::Filter minFilter() const; gl::Filter magFilter() const; gl::MipFilter mipFilter() const; gl::Wrapping wrapS() const; gl::Wrapping wrapT() const; Wraps wrap() const; dfloat maxAnisotropy() const; dfloat maxLevel() const; bool isCubeMap() const; /** * Enables or disables automatic mipmap generation on the texture. By * default, automatic mipmap generation is disabled. * * @param genMips @c true to generate mipmaps whenever level 0 changes. */ void setAutoGenMips(bool genMips); bool autoGenMips() const; /** * Reserves a specific size of undefined memory for a level. * * @param size Size in texels. * @param format Pixel format that will be later used for uploading content. * Determines internal storage pixel format. * @param level Mipmap level. */ void setUndefinedImage(Size const &size, Image::Format format, int level = 0); /** * Reserves a specific size of undefined memory for a cube map level. * * @param face Face of a cube map. * @param size Size in texels. * @param format Pixel format that will be later used for uploading content. * Determines internal storage pixel format. * @param level Mipmap level. */ void setUndefinedImage(gl::CubeFace face, Size const &size, Image::Format format, int level = 0); void setUndefinedContent(Size const &size, GLPixelFormat const &glFormat, int level = 0); void setUndefinedContent(gl::CubeFace face, Size const &size, GLPixelFormat const &glFormat, int level = 0); void setDepthStencilContent(Size const &size); /** * Sets the image content of the texture at a particular level. The format * of the image determines which GL format is chosen for the texture. * * @param image Image to upload to a GL texture. * @param level Level on which to store the image. */ void setImage(Image const &image, int level = 0); /** * Sets the image content of the texture at a particular level. The format * of the image determines which GL format is chosen for the texture. * * @param face Face of a cube map. * @param image Image to upload to a GL texture. * @param level Level on which to store the image. */ void setImage(gl::CubeFace face, Image const &image, int level = 0); /** * Replaces a portion of existing content. The image should be provided in * the same format as the previous full content. * * @param image Image to copy. * @param pos Position where the image is being copied. * @param level Mipmap level. */ void setSubImage(Image const &image, Vector2i const &pos, int level = 0); void setSubImage(gl::CubeFace face, Image const &image, Vector2i const &pos, int level = 0); /** * Generate a full set of mipmap levels based on the content on level 0. * Automatic mipmap generation can be enabled with setAutoGenMips(). */ void generateMipmap(); /** * Returns the size of the texture (mipmap level 0). */ Size size() const; /** * Returns the number of mipmap levels in use by the texture. * * Use levelsForSize() to determine the number of mipmap levels for an * arbitrary texture size. */ int mipLevels() const; /** * Returns the size of a particular mipmap level. This can be called after * the level 0 image content has been defined. * * @param level Mip level. * @return Size in texels. (0,0) for an invalid level. */ Size levelSize(int level) const; GLuint glName() const; void glBindToUnit(int unit) const; /** * Applies any cached parameter changes to the GL texture object. The texture is * bound temporarily while this is done (so any previously bound texture is unbound). */ void glApplyParameters(); /** * Returns the image format that was specified when image content was put * into the texture (with setImage() or setSubImage()). */ Image::Format imageFormat() const; public: /** * Determines the maximum supported texture size. */ static Size maximumSize(); protected: /** * Derived classes can override this to perform additional tasks * immediately before the texture is bound for use. The default * implementation does not need to be called. */ virtual void aboutToUse() const; public: /** * Calculates how many mipmap levels are produced for a specific size * of mipmap level 0 content. * * @param size Size in texels at level 0. * * @return Number of mipmap levels. */ static int levelsForSize(Size const &size); static Size levelSize(Size const &size0, int level); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_GLTEXTURE_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/imagebank.h0000664000175000017500000000402112641367671025166 0ustar jaakkojaakko/** @file imagebank.h Bank containing Image instances loaded from files. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_IMAGEBANK_H #define LIBGUI_IMAGEBANK_H #include "../gui/libgui.h" #include "../Image" #include namespace de { class File; /** * Bank containing Image instances loaded from files. * * @ingroup data */ class LIBGUI_PUBLIC ImageBank : public InfoBank { public: /** * Constructs a new image bank. * * @param flags Properties for the bank. By default, the bank uses a * background thread because large images may take some * time to load. Hot storage is disabled because loading * images from their potentially encoded source data is * faster/more efficient than deserializating raw pixel data. */ ImageBank(Flags const &flags = BackgroundThread | DisableHotStorage); void add(DotPath const &path, String const &imageFilePath); void addFromInfo(File const &file); Image const &image(DotPath const &path) const; protected: ISource *newSourceFromInfo(String const &id); IData *loadFromSource(ISource &source); IData *newData(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_IMAGEBANK_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/vertexbuilder.h0000664000175000017500000003041512641367671026142 0ustar jaakkojaakko/** @file vertexbuilder.h Utility for composing triangle strips. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_VERTEXBUILDER_H #define LIBGUI_VERTEXBUILDER_H #include #include #include #include namespace de { /** * Utility for composing simple geometric constructs (using triangle strips). */ template struct VertexBuilder { struct Vertices : public QVector { Vertices() { QVector::reserve(64); } void transform(Matrix4f const &matrix) { for(int i = 0; i < QVector::size(); ++i) { (*this)[i].pos = matrix * (*this)[i].pos; } } Vertices &operator += (Vertices const &other) { concatenate(other, *this); return *this; } Vertices operator + (Vertices const &other) const { Vertices v(*this); return v += other; } Vertices &makeQuad(Rectanglef const &rect, Vector4f const &color, Vector2f const &uv) { Vertices quad; VertexType v; v.rgba = color; v.texCoord = uv; v.pos = rect.topLeft; quad << v; v.pos = rect.topRight(); quad << v; v.pos = rect.bottomLeft(); quad << v; v.pos = rect.bottomRight; quad << v; return *this += quad; } Vertices &makeQuad(Rectanglef const &rect, Rectanglef const &uv) { Vertices quad; VertexType v; v.pos = rect.topLeft; v.texCoord = uv.topLeft; quad << v; v.pos = rect.topRight(); v.texCoord = uv.topRight(); quad << v; v.pos = rect.bottomLeft(); v.texCoord = uv.bottomLeft(); quad << v; v.pos = rect.bottomRight; v.texCoord = uv.bottomRight; quad << v; return *this += quad; } Vertices &makeQuad(Rectanglef const &rect, Vector4f const &color, Rectanglef const &uv, Matrix4f const *matrix = 0) { Vertices quad; VertexType v; v.rgba = color; v.pos = rect.topLeft; v.texCoord = uv.topLeft; quad << v; v.pos = rect.topRight(); v.texCoord = uv.topRight(); quad << v; v.pos = rect.bottomLeft(); v.texCoord = uv.bottomLeft(); quad << v; v.pos = rect.bottomRight; v.texCoord = uv.bottomRight; quad << v; if(matrix) quad.transform(*matrix); return *this += quad; } /// Makes a 3D quad with indirect UV coords. The points p1...p4 are specified /// with a clockwise winding (use Vertex3Tex2BoundsRgba). Vertices &makeQuadIndirect(Vector3f const &p1, Vector3f const &p2, Vector3f const &p3, Vector3f const &p4, Vector4f const &color, Rectanglef const &uv, Vector4f const &uvBounds, Vector2f const &texSize) { Vertices quad; VertexType v; v.rgba = color; v.texBounds = uvBounds; v.texCoord[1] = texSize; v.pos = p1; v.texCoord[0] = uv.topLeft; quad << v; v.pos = p2; v.texCoord[0] = uv.topRight(); quad << v; v.pos = p4; v.texCoord[0] = uv.bottomLeft(); quad << v; v.pos = p3; v.texCoord[0] = uv.bottomRight; quad << v; return *this += quad; } Vertices &makeCubeIndirect(Vector3f const &minPoint, Vector3f const &maxPoint, Rectanglef const &uv, Vector4f const &uvBounds, Vector2f const &texSize, Vector4f const faceColors[6]) { // Back. makeQuadIndirect(minPoint, Vector3f(maxPoint.x, minPoint.y, minPoint.z), Vector3f(maxPoint.x, maxPoint.y, minPoint.z), Vector3f(minPoint.x, maxPoint.y, minPoint.z), faceColors[0], uv, uvBounds, texSize); // Front. makeQuadIndirect(Vector3f(minPoint.x, minPoint.y, maxPoint.z), Vector3f(maxPoint.x, minPoint.y, maxPoint.z), maxPoint, Vector3f(minPoint.x, maxPoint.y, maxPoint.z), faceColors[1], uv, uvBounds, texSize); // Left. makeQuadIndirect(Vector3f(minPoint.x, minPoint.y, maxPoint.z), minPoint, Vector3f(minPoint.x, maxPoint.y, minPoint.z), Vector3f(minPoint.x, maxPoint.y, maxPoint.z), faceColors[2], uv, uvBounds, texSize); // Right. makeQuadIndirect(Vector3f(maxPoint.x, minPoint.y, minPoint.z), Vector3f(maxPoint.x, minPoint.y, maxPoint.z), maxPoint, Vector3f(maxPoint.x, maxPoint.y, minPoint.z), faceColors[3], uv, uvBounds, texSize); // Floor. makeQuadIndirect(Vector3f(minPoint.x, maxPoint.y, minPoint.z), Vector3f(maxPoint.x, maxPoint.y, minPoint.z), maxPoint, Vector3f(minPoint.x, maxPoint.y, maxPoint.z), faceColors[4], uv, uvBounds, texSize); // Ceiling. makeQuadIndirect(Vector3f(minPoint.x, minPoint.y, maxPoint.z), Vector3f(maxPoint.x, minPoint.y, maxPoint.z), Vector3f(maxPoint.x, minPoint.y, minPoint.z), minPoint, faceColors[5], uv, uvBounds, texSize); return *this; } Vertices &makeRing(Vector2f const ¢er, float outerRadius, float innerRadius, int divisions, Vector4f const &color, Rectanglef const &uv, float innerTexRadius = -1) { if(innerTexRadius < 0) innerTexRadius = innerRadius / outerRadius; Vertices ring; VertexType v; v.rgba = color; for(int i = 0; i <= divisions; ++i) { float const ang = 2 * PI * (i == divisions? 0 : i) / divisions; Vector2f r(cos(ang), sin(ang)); // Outer. v.pos = center + r * outerRadius; v.texCoord = uv.middle() + r * .5f * uv.size(); ring << v; // Inner. v.pos = center + r * innerRadius; v.texCoord = uv.middle() + r * (.5f * innerTexRadius) * uv.size(); ring << v; } return *this += ring; } Vertices &makeRing(Vector2f const ¢er, float outerRadius, float innerRadius, int divisions, Vector4f const &color, Vector2f const &uv) { return makeRing(center, outerRadius, innerRadius, divisions, color, Rectanglef(uv, uv)); } Vertices &makeFlexibleFrame(Rectanglef const &rect, float cornerThickness, Vector4f const &color, Rectanglef const &uv) { Vector2f const uvOff = uv.size() / 2; Vertices verts; VertexType v; v.rgba = color; // Top left corner. v.pos = rect.topLeft; v.texCoord = uv.topLeft; verts << v; v.pos = rect.topLeft + Vector2f(0, cornerThickness); v.texCoord = uv.topLeft + Vector2f(0, uvOff.y); verts << v; v.pos = rect.topLeft + Vector2f(cornerThickness, 0); v.texCoord = uv.topLeft + Vector2f(uvOff.x, 0); verts << v; v.pos = rect.topLeft + Vector2f(cornerThickness, cornerThickness); v.texCoord = uv.topLeft + uvOff; verts << v; // Top right corner. v.pos = rect.topRight() + Vector2f(-cornerThickness, 0); v.texCoord = uv.topRight() + Vector2f(-uvOff.x, 0); verts << v; v.pos = rect.topRight() + Vector2f(-cornerThickness, cornerThickness); v.texCoord = uv.topRight() + Vector2f(-uvOff.x, uvOff.y); verts << v; v.pos = rect.topRight(); v.texCoord = uv.topRight(); verts << v; v.pos = rect.topRight() + Vector2f(0, cornerThickness); v.texCoord = uv.topRight() + Vector2f(0, uvOff.y); verts << v; // Discontinuity. verts << v; verts << v; v.pos = rect.topRight() + Vector2f(-cornerThickness, cornerThickness); v.texCoord = uv.topRight() + Vector2f(-uvOff.x, uvOff.y); verts << v; // Bottom right corner. v.pos = rect.bottomRight + Vector2f(0, -cornerThickness); v.texCoord = uv.bottomRight + Vector2f(0, -uvOff.y); verts << v; v.pos = rect.bottomRight + Vector2f(-cornerThickness, -cornerThickness); v.texCoord = uv.bottomRight + Vector2f(-uvOff.x, -uvOff.y); verts << v; v.pos = rect.bottomRight; v.texCoord = uv.bottomRight; verts << v; v.pos = rect.bottomRight + Vector2f(-cornerThickness, 0); v.texCoord = uv.bottomRight + Vector2f(-uvOff.x, 0); verts << v; // Discontinuity. verts << v; verts << v; v.pos = rect.bottomRight + Vector2f(-cornerThickness, -cornerThickness); v.texCoord = uv.bottomRight + Vector2f(-uvOff.x, -uvOff.y); verts << v; // Bottom left corner. v.pos = rect.bottomLeft() + Vector2f(cornerThickness, 0); v.texCoord = uv.bottomLeft() + Vector2f(uvOff.x, 0); verts << v; v.pos = rect.bottomLeft() + Vector2f(cornerThickness, -cornerThickness); v.texCoord = uv.bottomLeft() + Vector2f(uvOff.x, -uvOff.y); verts << v; v.pos = rect.bottomLeft(); v.texCoord = uv.bottomLeft(); verts << v; v.pos = rect.bottomLeft() + Vector2f(0, -cornerThickness); v.texCoord = uv.bottomLeft() + Vector2f(0, -uvOff.y); verts << v; // Discontinuity. verts << v; verts << v; // Closing the loop. v.pos = rect.bottomLeft() + Vector2f(cornerThickness, -cornerThickness); v.texCoord = uv.bottomLeft() + Vector2f(uvOff.x, -uvOff.y); verts << v; v.pos = rect.topLeft + Vector2f(0, cornerThickness); v.texCoord = uv.topLeft + Vector2f(0, uvOff.y); verts << v; v.pos = rect.topLeft + Vector2f(cornerThickness, cornerThickness); v.texCoord = uv.topLeft + Vector2f(uvOff.x, uvOff.y); verts << v; return *this += verts; } }; static void concatenate(Vertices const &stripSequence, Vertices &destStrip) { if(!stripSequence.size()) return; if(!destStrip.isEmpty()) { destStrip << destStrip.back(); destStrip << stripSequence.front(); } destStrip << stripSequence; } }; } // namespace de #endif // LIBGUI_VERTEXBUILDER_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/glinfo.h0000664000175000017500000000502512641367671024533 0ustar jaakkojaakko/** @file glinfo.h OpenGL information. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_GLINFO_H #define LIBGUI_GLINFO_H #include "../gui/libgui.h" #include namespace de { class LIBGUI_PUBLIC GLInfo { public: /// Extension availability bits. struct Extensions { duint32 ARB_draw_instanced : 1; duint32 ARB_instanced_arrays : 1; duint32 ARB_texture_env_combine : 1; duint32 ARB_texture_non_power_of_two : 1; duint32 EXT_blend_subtract : 1; duint32 EXT_framebuffer_blit : 1; duint32 EXT_framebuffer_multisample : 1; duint32 EXT_framebuffer_object : 1; duint32 EXT_packed_depth_stencil : 1; duint32 EXT_texture_compression_s3tc : 1; duint32 EXT_texture_filter_anisotropic : 1; // Vendor-specific extensions: duint32 ATI_texture_env_combine3 : 1; duint32 NV_framebuffer_multisample_coverage : 1; duint32 NV_texture_env_combine4 : 1; duint32 SGIS_generate_mipmap : 1; #ifdef WIN32 duint32 Windows_ARB_multisample : 1; duint32 Windows_EXT_swap_control : 1; #endif #ifdef DENG_X11 duint32 X11_EXT_swap_control : 1; #endif }; /// Implementation limits. struct Limits { int maxTexFilterAniso; int maxTexSize; ///< Texels. int maxTexUnits; }; GLInfo(); static Extensions const &extensions(); static Limits const &limits(); static bool isFramebufferMultisamplingSupported(); /** * Initializes the static instance of GLInfo. Cannot be called before there * is a current OpenGL context. Canvas will call this after initialization. */ static void glInit(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_GLINFO_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/image.h0000664000175000017500000001151312641367671024336 0ustar jaakkojaakko/** @file image.h Wrapper over QImage. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_IMAGE_H #define LIBGUI_IMAGE_H #include #include #include #include #include #include #include "../gui/libgui.h" #include "../GLPixelFormat" namespace de { /** * Thin wrapper over QImage allowing use of some custom/raw image formats. * * Some image formats do not allow drawing. As a rule, all QImage-based formats * support drawing (via QPainter). * * @todo Merge image_t and the related Image_* routines into here. */ class LIBGUI_PUBLIC Image : public ISerializable { public: /** * Supported GL-friendly formats. Note that all QImage formats cannot be * uploaded to OpenGL. */ enum Format { Unknown = -1, UseQImageFormat = 0, ///< May not be GL friendly. Luminance_8 = 1, LuminanceAlpha_88 = 2, Alpha_8 = 3, RGB_555 = 4, RGB_565 = 5, RGB_444 = 6, RGB_888 = 7, ///< 24-bit depth. RGBA_4444 = 8, RGBA_5551 = 9, RGBA_8888 = 10, RGBx_8888 = 11 ///< 32-bit depth, alpha data ignored. }; typedef Vector2ui Size; typedef Vector4ub Color; public: Image(); Image(Image const &other); Image(QImage const &image); /** * Constructs an image, taking a copy of the pixel data. * * @param size Size of the image. * @param format Data format. * @param pixels Pixel data. */ Image(Size const &size, Format format, IByteArray const &pixels); Image(Size const &size, Format format, ByteRefArray const &refPixels); Image &operator = (Image const &other); Image &operator = (QImage const &other); Format format() const; QImage::Format qtFormat() const; Size size() const; Rectanglei rect() const; duint width() const { return size().x; } duint height() const { return size().y; } /** * Number of bits per pixel. */ int depth() const; /** * Number of bytes between rows in the pixel data. */ int stride() const; /** * Total number of bytes in the pixel data. */ int byteCount() const; void const *bits() const; void *bits(); /** * Determines if the image has a zero size (no pixels). */ bool isNull() const; /** * Determines if the image format can be uploaded to OpenGL without * conversion of any kind. */ bool isGLCompatible() const; bool canConvertToQImage() const; /** * Converts the image to a QImage. Only allowed if canConvertToQImage() * returns @c true. * * @return QImage instance. */ QImage toQImage() const; GLPixelFormat glFormat() const; // Drawing/editing methods. Image subImage(Rectanglei const &subArea) const; void resize(Size const &size); void fill(Color const &color); void fill(Rectanglei const &rect, Color const &color); void draw(Image const &image, Vector2i const &topLeft); void drawPartial(Image const &image, Rectanglei const &part, Vector2i const &topLeft); // Implements ISerializable. void operator >> (Writer &to) const; void operator << (Reader &from); public: static GLPixelFormat glFormat(Format imageFormat); static GLPixelFormat glFormat(QImage::Format qtImageFormat); static Image solidColor(Color const &color, Size const &size); /** * Loads an image from a block of data. The format of the image is autodetected. * In addition to image formats supported by Qt, this can load 8-bit paletted PCX * (ZSoft Paintbrush) images. * * @param data Block of data containing image data. * @param formatHint Optionally, file name extension with dot included (".tga"). */ static Image fromData(IByteArray const &data, String const &formatHint = ""); /// @copydoc fromData() static Image fromData(Block const &data, String const &formatHint = ""); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_IMAGE_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/kdtreeatlasallocator.h0000664000175000017500000000316312641367671027462 0ustar jaakkojaakko/** @file kdtreeatlasallocator.h KD-tree based atlas allocator. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_KDTREEATLASALLOCATOR_H #define LIBGUI_KDTREEATLASALLOCATOR_H #include "../Atlas" namespace de { /** * KD-tree based atlas allocator. * * Allocations are done using 2D binary space partitioning. * * @see Atlas */ class LIBGUI_PUBLIC KdTreeAtlasAllocator : public Atlas::IAllocator { public: KdTreeAtlasAllocator(); void setMetrics(Atlas::Size const &totalSize, int margin); void clear(); Id allocate(Atlas::Size const &size, Rectanglei &rect); void release(Id const &id); bool optimize(); int count() const; Atlas::Ids ids() const; void rect(Id const &id, Rectanglei &rect) const; Allocations allocs() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_KDTREEATLASALLOCATOR_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/rowatlasallocator.h0000664000175000017500000000320112641367671027004 0ustar jaakkojaakko/** @file rowatlasallocator.h Row-based atlas allocator. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_ROWATLASALLOCATOR_H #define LIBGUI_ROWATLASALLOCATOR_H #include "../Atlas" namespace de { /** * Row-based atlas allocator. * * Suitable for content that uses relatively similar heights, for instance text * fragments/words. * * @see Atlas */ class LIBGUI_PUBLIC RowAtlasAllocator : public Atlas::IAllocator { public: RowAtlasAllocator(); void setMetrics(Atlas::Size const &totalSize, int margin); void clear(); Id allocate(Atlas::Size const &size, Rectanglei &rect); void release(Id const &id); bool optimize(); int count() const; Atlas::Ids ids() const; void rect(Id const &id, Rectanglei &rect) const; Allocations allocs() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_ROWATLASALLOCATOR_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/glstate.h0000664000175000017500000001473312641367671024726 0ustar jaakkojaakko/** @file glstate.h GL state. * * GL state management is abstracted inside this class to retain plausible * independence from OpenGL as the underlying rendering API. Also, Direct3D * uses object-based state management. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_GLSTATE_H #define LIBGUI_GLSTATE_H #include #include #include #include "../gui/libgui.h" namespace de { class GLTarget; namespace gl { enum ColorMaskFlag { WriteNone = 0, WriteRed = 0x1, WriteGreen = 0x2, WriteBlue = 0x4, WriteAlpha = 0x8, WriteAll = WriteRed | WriteGreen | WriteBlue | WriteAlpha }; Q_DECLARE_FLAGS(ColorMask, ColorMaskFlag) Q_DECLARE_OPERATORS_FOR_FLAGS(ColorMask) enum Comparison { Never, Always, Equal, NotEqual, Less, Greater, LessOrEqual, GreaterOrEqual }; enum Blend { Zero, One, SrcColor, OneMinusSrcColor, SrcAlpha, OneMinusSrcAlpha, DestColor, OneMinusDestColor, DestAlpha, OneMinusDestAlpha }; typedef std::pair BlendFunc; enum BlendOp { Add, Subtract, ReverseSubtract }; enum Cull { None, Front, Back }; } // namespace gl /** * GL state. * * All manipulation of OpenGL state must occur through this class. If OpenGL * state is changed manually, it will result in GLState not knowing about it, * potentially leading to the incorrect state being in effect later on. * * GLState instances can either be created on demand with GLState::push(), or * one can keep a GLState instance around for repeating use. The stack exists * to aid structured drawing: it is not required to use the stack for all * drawing. GLState::apply() can be called for any GLState instance to use it * as the current GL state. * * @note The default viewport is (0,0)→(0,0). The viewport has to be set * when the desired size is known (for instance when a Canvas is resized). * * @ingroup gl */ class LIBGUI_PUBLIC GLState { public: /** * Constructs a GL state with the default values for all properties. */ GLState(); GLState(GLState const &other); GLState &operator = (GLState const &other); GLState &setCull(gl::Cull mode); GLState &setDepthTest(bool enable); GLState &setDepthFunc(gl::Comparison func); GLState &setDepthWrite(bool enable); GLState &setBlend(bool enable); GLState &setBlendFunc(gl::Blend src, gl::Blend dest); GLState &setBlendFunc(gl::BlendFunc func); GLState &setBlendOp(gl::BlendOp op); GLState &setColorMask(gl::ColorMask mask); GLState &setTarget(GLTarget &target); GLState &setDefaultTarget(); GLState &setViewport(Rectangleui const &viewportRect); /** * Sets a viewport using coordinates that have been normalized within the * current render target. This is useful for operations that should be * independent of target size. * * @param normViewportRect Normalized viewport rectangle. */ GLState &setNormalizedViewport(Rectanglef const &normViewportRect); GLState &setScissor(Rectanglei const &scissorRect); GLState &setScissor(Rectangleui const &scissorRect); /** * Sets a scissor using coordinates that have been normalized within the * current viewport. * * @param normScissorRect Normalized scissor rectangle. */ GLState &setNormalizedScissor(Rectanglef const &normScissorRect); GLState &clearScissor(); gl::Cull cull() const; bool depthTest() const; gl::Comparison depthFunc() const; bool depthWrite() const; bool blend() const; gl::Blend srcBlendFunc() const; gl::Blend destBlendFunc() const; gl::BlendFunc blendFunc() const; gl::BlendOp blendOp() const; gl::ColorMask colorMask() const; GLTarget &target() const; Rectangleui viewport() const; Rectanglef normalizedViewport() const; bool scissor() const; Rectangleui scissorRect() const; /** * Updates the OpenGL state to match this GLState. Until this is called no * changes occur in the OpenGL state. Calling this more than once is * allowed; the subsequent calls do nothing. * * @todo Remove excess calls to apply() once all direct GL1 state * manipulation has been removed. */ void apply() const; public: /** * Tells GLState to consider the native OpenGL state undefined, meaning * that when the next GLState is applied, all properties need to be set * rather than just the changed ones. * * @todo Remove this once all direct OpenGL state changes have been * removed. */ static void considerNativeStateUndefined(); /** * Returns the current (i.e., topmost) state on the GL state stack. */ static GLState ¤t(); /** * Pushes a copy of the current state onto the current thread's GL state * stack. * * @return Reference to the new state. */ static GLState &push(); /** * Pops the topmost state off the current thread's stack. Returns a reference * to the state that has now become the new current state. */ static GLState &pop(); /** * Pushes a state onto the current thread's GL state stack. * * @param state State to push. Ownership taken. */ static void push(GLState *state); /** * Removes the topmost state off of the current thread's stack. * * @return State instance. Ownership given to caller. */ static GLState *take(); static dsize stackDepth(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_GLSTATE_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/opengl.h0000664000175000017500000000271612641367671024545 0ustar jaakkojaakko/** @file opengl.h Headers for OpenGL (ES) 2. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_SYSTEM_OPENGL_H #define LIBGUI_SYSTEM_OPENGL_H #ifdef glDeleteTextures # error "glDeleteTextures defined as a macro! (would be undefined by Qt)" #endif #ifdef MACOSX # include #else # include "glentrypoints.h" #endif #include // Defined in GLES2. #ifndef GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS # define GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS 0x8CD9 #endif /* * The LIBGUI_GLES2 macro is defined when the OpenGL ES 2.0 API is in use. */ //#define LIBGUI_GLES2 #ifndef GL_VERSION_2_0 # error "OpenGL 2.0 (or newer) headers not found" #endif #endif // LIBGUI_SYSTEM_OPENGL_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/glentrypoints.h0000664000175000017500000001426312641367671026202 0ustar jaakkojaakko/** @file glentrypoints.h API entry points for OpenGL (Windows/Linux). * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_GLENTRYPOINTS_H #define LIBGUI_GLENTRYPOINTS_H #include "../gui/libgui.h" #if defined(WIN32) || (defined(UNIX) && !defined(MACOSX)) # define LIBGUI_USE_GLENTRYPOINTS #endif #ifdef LIBGUI_USE_GLENTRYPOINTS #ifdef WIN32 # define WIN32_LEAN_AND_MEAN # include # ifdef min # undef min # endif # ifdef max # undef max # endif #endif #undef GL_GLEXT_PROTOTYPES #include #include #if defined(WIN32) # include # define LIBGUI_FETCH_GL_1_3 #endif #ifdef LIBGUI_FETCH_GL_1_3 LIBGUI_EXTERN_C LIBGUI_PUBLIC PFNGLACTIVETEXTUREPROC glActiveTexture; LIBGUI_EXTERN_C LIBGUI_PUBLIC PFNGLBLENDEQUATIONPROC glBlendEquation; LIBGUI_EXTERN_C LIBGUI_PUBLIC PFNGLCLIENTACTIVETEXTUREPROC glClientActiveTexture; LIBGUI_EXTERN_C LIBGUI_PUBLIC PFNGLMULTITEXCOORD2FPROC glMultiTexCoord2f; LIBGUI_EXTERN_C LIBGUI_PUBLIC PFNGLMULTITEXCOORD2FVPROC glMultiTexCoord2fv; #endif #ifdef WIN32 LIBGUI_EXTERN_C LIBGUI_PUBLIC PFNWGLGETEXTENSIONSSTRINGARBPROC wglGetExtensionsStringARB; #endif LIBGUI_EXTERN_C PFNGLATTACHSHADERPROC glAttachShader; LIBGUI_EXTERN_C PFNGLBINDATTRIBLOCATIONPROC glBindAttribLocation; LIBGUI_EXTERN_C PFNGLBINDBUFFERPROC glBindBuffer; LIBGUI_EXTERN_C PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer; LIBGUI_EXTERN_C PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer; LIBGUI_EXTERN_C PFNGLBLENDFUNCSEPARATEPROC glBlendFuncSeparate; LIBGUI_EXTERN_C PFNGLBUFFERDATAPROC glBufferData; LIBGUI_EXTERN_C PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus; LIBGUI_EXTERN_C PFNGLCOMPILESHADERPROC glCompileShader; LIBGUI_EXTERN_C PFNGLCREATEPROGRAMPROC glCreateProgram; LIBGUI_EXTERN_C PFNGLCREATESHADERPROC glCreateShader; LIBGUI_EXTERN_C PFNGLDELETEBUFFERSPROC glDeleteBuffers; LIBGUI_EXTERN_C PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers; LIBGUI_EXTERN_C PFNGLDELETEPROGRAMPROC glDeleteProgram; LIBGUI_EXTERN_C PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers; LIBGUI_EXTERN_C PFNGLDELETESHADERPROC glDeleteShader; LIBGUI_EXTERN_C PFNGLDETACHSHADERPROC glDetachShader; LIBGUI_EXTERN_C PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray; LIBGUI_EXTERN_C PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray; LIBGUI_EXTERN_C PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer; LIBGUI_EXTERN_C PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D; LIBGUI_EXTERN_C PFNGLGENBUFFERSPROC glGenBuffers; LIBGUI_EXTERN_C PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers; LIBGUI_EXTERN_C PFNGLGENERATEMIPMAPPROC glGenerateMipmap; LIBGUI_EXTERN_C PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers; LIBGUI_EXTERN_C PFNGLGETATTRIBLOCATIONPROC glGetAttribLocation; LIBGUI_EXTERN_C PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog; LIBGUI_EXTERN_C PFNGLGETPROGRAMIVPROC glGetProgramiv; LIBGUI_EXTERN_C PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog; LIBGUI_EXTERN_C PFNGLGETSHADERIVPROC glGetShaderiv; LIBGUI_EXTERN_C PFNGLGETSHADERSOURCEPROC glGetShaderSource; LIBGUI_EXTERN_C PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation; LIBGUI_EXTERN_C PFNGLISBUFFERPROC glIsBuffer; LIBGUI_EXTERN_C PFNGLISFRAMEBUFFERPROC glIsFramebuffer; LIBGUI_EXTERN_C PFNGLISPROGRAMPROC glIsProgram; LIBGUI_EXTERN_C PFNGLLINKPROGRAMPROC glLinkProgram; LIBGUI_EXTERN_C PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage; LIBGUI_EXTERN_C PFNGLSHADERSOURCEPROC glShaderSource; LIBGUI_EXTERN_C PFNGLUNIFORM1FPROC glUniform1f; LIBGUI_EXTERN_C PFNGLUNIFORM1IPROC glUniform1i; LIBGUI_EXTERN_C PFNGLUNIFORM2FPROC glUniform2f; LIBGUI_EXTERN_C PFNGLUNIFORM3FPROC glUniform3f; LIBGUI_EXTERN_C PFNGLUNIFORM3FVPROC glUniform3fv; LIBGUI_EXTERN_C PFNGLUNIFORM4FPROC glUniform4f; LIBGUI_EXTERN_C PFNGLUNIFORM4FVPROC glUniform4fv; LIBGUI_EXTERN_C PFNGLUNIFORMMATRIX3FVPROC glUniformMatrix3fv; LIBGUI_EXTERN_C PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv; LIBGUI_EXTERN_C PFNGLUSEPROGRAMPROC glUseProgram; LIBGUI_EXTERN_C PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer; // Extensions: LIBGUI_EXTERN_C PFNGLBLITFRAMEBUFFEREXTPROC glBlitFramebufferEXT; LIBGUI_EXTERN_C PFNGLRENDERBUFFERSTORAGEMULTISAMPLEEXTPROC glRenderbufferStorageMultisampleEXT; #ifdef GL_ARB_draw_instanced LIBGUI_EXTERN_C PFNGLDRAWARRAYSINSTANCEDARBPROC glDrawArraysInstancedARB; LIBGUI_EXTERN_C PFNGLDRAWELEMENTSINSTANCEDARBPROC glDrawElementsInstancedARB; #endif #ifdef GL_ARB_instanced_arrays LIBGUI_EXTERN_C PFNGLVERTEXATTRIBDIVISORARBPROC glVertexAttribDivisorARB; #endif #ifdef GL_NV_framebuffer_multisample_coverage LIBGUI_EXTERN_C PFNGLRENDERBUFFERSTORAGEMULTISAMPLECOVERAGENVPROC glRenderbufferStorageMultisampleCoverageNV; #endif void getAllOpenGLEntryPoints(); #ifdef DENG_X11 LIBGUI_PUBLIC char const *getGLXExtensionsString(); LIBGUI_PUBLIC void setXSwapInterval(int interval); void getGLXEntryPoints(); #endif #endif // LIBGUI_USE_GLENTRYPOINTS #endif // LIBGUI_GLENTRYPOINTS_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/atlas.h0000664000175000017500000001745212641367671024370 0ustar jaakkojaakko/** @file atlas.h Image-based atlas. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_ATLAS_H #define LIBGUI_ATLAS_H #include #include #include #include #include #include #include #include "../Image" namespace de { /** * Abstract image-based atlas. * * The logic that determines how and where new content is allocated is * completely handled by the IAllocator attached to the Atlas. * * @ingroup gl */ class LIBGUI_PUBLIC Atlas : public Lockable { public: typedef Image::Size Size; enum Flag { /** * A copy of the full atlas is kept in memory. */ BackingStore = 0x1, /** * When the atlas is too full, it will be defragmented in an attempt to * rearrange the content more efficiently. Useful with dynamic atlases * where lots of allocations and releases occur predictably. Requires * BackingStore. */ AllowDefragment = 0x2, /** * If using a backing store, wrap borders using the source image. This allows * filtering the contents using wrapped coordinates. Borders are by default * duplicated from neighboring pixels (for clamped filtering). Set border size * with setBorderSize(). */ WrapBordersInBackingStore = 0x4, /** * All commits are logged as XVerbose log entries. A commit occurs when the * atlas backing store contents are copied to the actual atlas storage (for * instance a GL texture). */ LogCommitsAsXVerbose = 0x8, DefaultFlags = 0 }; Q_DECLARE_FLAGS(Flags, Flag) typedef QSet Ids; /** * Interface for allocator logic. Each Atlas requires one IAllocator object to * determine where to place allocated images. */ class LIBGUI_PUBLIC IAllocator { public: typedef QMap Allocations; public: virtual ~IAllocator() {} /** * Define the metrics for the atlas. * * @param totalSize Total area available. * @param margin Number of pixels to leave between each allocated * image and also each edge of the total area. */ virtual void setMetrics(Size const &totalSize, int margin) = 0; virtual void clear() = 0; virtual Id allocate(Size const &size, Rectanglei &rect) = 0; virtual void release(Id const &id) = 0; /** * Finds an optimal layout for all of the allocations. */ virtual bool optimize() = 0; virtual int count() const = 0; virtual Ids ids() const = 0; virtual void rect(Id const &id, Rectanglei &rect) const = 0; /** * Returns all the present allocations. */ virtual Allocations allocs() const = 0; }; /** * Audience that will be notified if the existing allocations are * repositioned for some reasons (e.g., defragmentation). Normally once * allocated, content will remain at its initial place. */ DENG2_DEFINE_AUDIENCE2(Reposition, void atlasContentRepositioned(Atlas &)) /** * Audience that will be notified when an allocation fails due to the atlas * being so full that there is no room for the new image. */ DENG2_DEFINE_AUDIENCE2(OutOfSpace, void atlasOutOfSpace(Atlas &)) public: /** * Constructs a new atlas. */ Atlas(Flags const &flags = DefaultFlags, Size const &totalSize = Size()); /** * Sets the allocator for the atlas. The atlas is cleared automatically. * * @param allocator Allocator instance. Atlas gets ownership. */ void setAllocator(IAllocator *allocator); /** * Sets the size of the margin that is left between allocations. The default is * one (transparent black) pixel. * * @param marginPixels Number of pixels to retain as margins around allocations. */ void setMarginSize(dint marginPixels); /** * Sets the size of borders that are added around allocations. The size of the * allocated area is increased internally by this many units on each size. * * If WrapBordersInBackingStore is used, each border is taken from the opposite * side of the source image. By default, the borders duplicate the neighboring * pixels on each edge. * * @param borderPixels Number of pixels to add as border padding. */ void setBorderSize(dint borderPixels); /** * Empties the contents of the atlas. The size of the backing store is not * changed. */ void clear(); /** * Resizes the atlas. * * @param totalSize Total size of the atlas in pixels. */ void setTotalSize(Size const &totalSize); Size totalSize() const; /** * Attempts to allocate an image into the atlas. If defragmentation is * allowed, it may occur during the operation. * * @param image Image content to allocate. * * @return Identifier of the allocated image. If Id::None, the allocation * failed because the atlas is too full. */ Id alloc(Image const &image); /** * Releases a previously allocated image from the atlas. * * @param id Identifier of an allocated image. */ void release(Id const &id); bool contains(Id const &id) const; /** * Returns the number of images in the atlas. */ int imageCount() const; inline bool isEmpty() const { return !imageCount(); } /** * Returns the identifiers of all images in the atlas. */ Ids allImages() const; /** * Returns the position of an allocated image in the atlas. * * @param id Image identifier. * * @return Coordinates of the image on the atlas (as pixels). Always within * the Atlas::size(). */ Rectanglei imageRect(Id const &id) const; /** * Returns the normalized position of an allocated image in the atlas. * * @param id Image identifier. * * @return Normalized coordinates of the image on the atlas. Always within * [0,1]. */ Rectanglef imageRectf(Id const &id) const; /** * Returns the image content allocated earlier. Requires BackingStore. * * @param id Image identifier. * * @return Image that was provided earlier to alloc(). */ Image image(Id const &id) const; /** * Request committing the backing store to the physical atlas storage. * This does nothing if there are no changes in the atlas. */ void commit() const; protected: virtual void commitFull(Image const &fullImage) const = 0; /** * Commits a image to the actual physical atlas storage. * * @param image Image to commit. * @param topLeft Top left corner of where to place the image. */ virtual void commit(Image const &image, Vector2i const &topLeft) const = 0; private: DENG2_PRIVATE(d) }; Q_DECLARE_OPERATORS_FOR_FLAGS(Atlas::Flags) } // namespace de #endif // LIBGUI_ATLAS_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/modeldrawable.h0000664000175000017500000002372612641367671026067 0ustar jaakkojaakko/** @file modeldrawable.h Drawable specialized for 3D models. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_MODELDRAWABLE_H #define LIBGUI_MODELDRAWABLE_H #include #include #include #include #include #include namespace de { class GLBuffer; /** * Drawable that is constructed out of a 3D model. * * 3D model data is loaded using the Open Asset Import Library from multiple different * source formats. * * Lifetime. * * Texture maps. * * Animation. * * @ingroup gl */ class LIBGUI_PUBLIC ModelDrawable : public AssetGroup { public: /// An error occurred during the loading of the model data. @ingroup errors DENG2_ERROR(LoadError); DENG2_DEFINE_AUDIENCE2(AboutToGLInit, void modelAboutToGLInit(ModelDrawable &)) enum TextureMap // note: used as indices internally { Diffuse = 0, ///< Surface color and opacity. Normals = 1, /**< Normal map where RGB values are directly interpreted as vectors. Blue 255 is Z+1 meaning straight up. Color value 128 means zero. The default normal vector pointing straight away from the surface is therefore (128, 128, 255) => (0, 0, 1). */ Specular = 2, ///< Specular color (RGB) and reflection sharpness (A). Emissive = 3, /**< Additional light emitted by the surface that is not affected by external factors. */ Height = 4, /**< Height values are converted to a normal map. Lighter regions are higher than dark regions. */ Unknown }; static TextureMap textToTextureMap(String const &text); /** * Animation state for a model. There can be any number of ongoing animations, * targeting individual nodes of a model. * * @ingroup gl */ class LIBGUI_PUBLIC Animator { public: struct Animation { int animId; ///< Which animation to use. ddouble time; ///< Animation time. String node; ///< Target node. QVariant data; ///< Additional data for derived classes. }; /// Referenced node or animation was not found in the model. @ingroup errors DENG2_ERROR(InvalidError); public: Animator(); Animator(ModelDrawable const &model); virtual ~Animator() {} void setModel(ModelDrawable const &model); /** * Returns the model with which this animation is being used. */ ModelDrawable const &model() const; /** * Returns the number of ongoing animations. */ int count() const; inline bool isEmpty() const { return !count(); } Animation const &at(int index) const; Animation &at(int index); bool isRunning(String const &animName, String const &rootNode = "") const; bool isRunning(int animId, String const &rootNode = "") const; /** * Starts an animation sequence. A previous sequence running on this node will * be automatically stopped. * * @param animName Animation sequence name. * @param rootNode Animation root. * * @return Animation. */ Animation &start(String const &animName, String const &rootNode = ""); /** * Starts an animation sequence. A previous sequence running on this node will * be automatically stopped. * * @param animId Animation sequence number. * @param rootNode Animation root. * * @return Animation. */ Animation &start(int animId, String const &rootNode = ""); void stop(int index); void clear(); /** * Advances the animation state. Progresses ongoing animations and possibly * triggers new ones. * * @param elapsed Duration of elapsed time. */ virtual void advanceTime(TimeDelta const &elapsed); /** * Returns the time to be used when drawing the model. * * @param index Animation index. * * @return Time in the model's animation sequence. */ virtual ddouble currentTime(int index) const; private: DENG2_PRIVATE(d) }; /** * Interface for image loaders that provide the content for texture images when * given a path. The default loader just checks if there is an image file in the * file system at the given path. */ class LIBGUI_PUBLIC IImageLoader { public: virtual ~IImageLoader() {} /** * Loads an image. If the image can't be loaded, the loader must throw an * exception explaining the reason for the failure. * * @param path Path of the image. This is an absolute de::FS path inferred from * the location of the source model file and the material metadata * it contains. * * @return Loaded image. */ virtual Image loadImage(String const &path) = 0; }; public: ModelDrawable(); /** * Sets the object responsible for loading texture images. * * By default, ModelDrawable uses a simple loader that tries to load image files * directly from the file system. * * @param loader Image loader. */ void setImageLoader(IImageLoader &loader); void useDefaultImageLoader(); /** * Releases all the data: the loaded model and any GL resources. */ void clear(); /** * Loads a model from a file. This is a synchronous operation and may take a while, * but can be called in a background thread. * * After loading, you must call glInit() before drawing it. glInit() will be * called automatically if needed. * * @param file Model file to load. */ void load(File const &file); /** * Finds the id of an animation that has the name @a name. Note that animation * names are optional. * * @param name Animation name. * * @return Animation id, or -1 if not found. */ int animationIdForName(String const &name) const; int animationCount() const; /** * Locates a material specified in the model by its name. * * @param name Name of the material * * @return Material id. */ int materialId(String const &name) const; bool nodeExists(String const &name) const; /** * Atlas to use for any textures needed by the model. This is needed for glInit(). * * @param atlas Atlas for model textures. */ void setAtlas(AtlasTexture &atlas); /** * Removes the model's atlas. All allocations this model has made from the atlas * are freed. */ void unsetAtlas(); typedef QList Mapping; /** * Sets which textures are to be passed to the model shader via the GL buffer. * * By default, the model only has a diffuse map. The user of ModelDrawable must * specify the indices for the other texture maps depending on how the shader expects * to receive them. * * @param mapsToUse Up to four map types. The map at index zero will be specified * as the first texture bounds (@c aBounds in the shader), index * one will become the second texture bounds (@c aBounds2), etc. */ void setTextureMapping(Mapping mapsToUse); static Mapping diffuseNormalsSpecularEmission(); /** * Sets the texture map that is used if no other map is provided. * * @param textureType Type of the texture. * @param atlasId Identifier in the atlas. */ void setDefaultTexture(TextureMap textureType, Id const &atlasId); /** * Prepares a loaded model for drawing by constructing all the required GL objects. * * This method will be called automatically when needed, however you can also call it * manually at a suitable time. Only call this from the main (UI) thread. */ void glInit(); /** * Releases all the GL resources of the model. */ void glDeinit(); /** * Sets or changes one of the texture maps used by the model. This can be used to * override the maps set up automatically by glInit(). * * @param materialId Which material to modify. * @param textureMap Texture to set. * @param path Path of the texture image. */ void setTexturePath(int materialId, TextureMap textureMap, String const &path); /** * Sets the GL program used for shading the model. * * @param program GL program. */ void setProgram(GLProgram &program); void unsetProgram(); void draw(Animator const *animation = 0) const; void drawInstanced(GLBuffer const &instanceAttribs, Animator const *animation = 0) const; /** * Dimensions of the default pose, in model space. */ Vector3f dimensions() const; /** * Center of the default pose, in model space. */ Vector3f midPoint() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_MODELDRAWABLE_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/atlastexture.h0000664000175000017500000000442712641367671026007 0ustar jaakkojaakko/** @file atlastexture.h Atlas stored on a GLTexture. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_ATLASTEXTURE_H #define LIBGUI_ATLASTEXTURE_H #include "../GLTexture" #include "../Atlas" namespace de { /** * Atlas stored on a (2D) GLTexture. * * @ingroup gl */ class LIBGUI_PUBLIC AtlasTexture : public Atlas, public GLTexture { public: AtlasTexture(Atlas::Flags const &flags = DefaultFlags, Atlas::Size const &totalSize = Atlas::Size()); /** * Constructs an AtlasTexture with a RowAtlasAllocator. * * @param flags Atlas flags. * @param totalSize Total size for atlas. * * @return AtlasTexture instance. */ static AtlasTexture *newWithRowAllocator(Atlas::Flags const &flags = DefaultFlags, Atlas::Size const &totalSize = Atlas::Size()); static AtlasTexture *newWithKdTreeAllocator(Atlas::Flags const &flags = DefaultFlags, Atlas::Size const &totalSize = Atlas::Size()); void clear(); protected: /** * The atlas content is automatically committed to the GL texture when the * texture is bound for use. */ void aboutToUse() const; /** * Replaces the entire content of the GL texture. * * @param fullImage Full atlas content image. */ void commitFull(Image const &fullImage) const; void commit(Image const &image, Vector2i const &topLeft) const; }; } // namespace de #endif // LIBGUI_ATLASTEXTURE_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/drawable.h0000664000175000017500000002262212641367671025040 0ustar jaakkojaakko/** @file drawable.h Drawable object with buffers, programs and states. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_DRAWABLE_H #define LIBGUI_DRAWABLE_H #include #include #include #include #include "../GLBuffer" #include "../GLProgram" #include "../GLState" #include "../GLUniform" namespace de { /** * Drawable object with buffers, programs and states. * * This is the higher level, flexible, user-friendly combination of the lower * level GL classes. It is not mandatory to use this class for drawing; one can * always use the lower level classes directly. * * Drawable combines a set of GLBuffer instances with a set of GL programs and * GL states. There can be multiple (named) instances of buffers, programs, and * states in a Drawable. While each buffer must have a program, having a state * is optional. Each buffer can choose which of the Drawable's programs and * states is used with the buffer. It is also possible to assign external * programs and states for use with buffers. A default program (with id 0) is * always present in a Drawable. * * Example use cases: * - draw a single buffer with a program using the current GL state * - draw a single buffer with one of many alternative programs * - draw multiple buffers with the same program * - draw some buffers with a custom GL state and the rest with the * current state * - draw a mixture of multiple buffers and programs, each with a * custom state (e.g., a set of 3D sub-models representing an object * in the game) * * The buffers are drawn in the order of ascending identifiers. If no state has * been defined for a buffer, the topmost state on the GL state stack is used * for drawing that particular buffer. * * GLUniform instances can be used externally to this class to manipulate the * uniforms in the programs. The user is expected to provide the static/dynamic * vertex data for the buffers. * * Drawable is an AssetGroup: it cannot be drawn until all the contained * buffers and programs are ready. It is allowed to insert further assets into * the group if they should be present before drawing is allowed (e.g., * textures). * * @ingroup gl */ class LIBGUI_PUBLIC Drawable : public AssetGroup { DENG2_NO_COPY (Drawable) DENG2_NO_ASSIGN(Drawable) public: /// User-provided (nonzero) identifier. Buffer identifiers define the /// drawing order of the buffers. (Note that this is not a de::Id.) typedef duint Id; /// User-provided name. Buffers, programs, and states can optionally be /// also identified using textual names. typedef String Name; typedef QList Ids; public: Drawable(); /** * Clears the drawable. All buffers, programs, and states are deleted. * The default buffer and program are cleared. */ void clear(); Ids allBuffers() const; Ids allPrograms() const; Ids allStates() const; bool hasBuffer(Id id) const; /** * Finds an existing buffer. * @param id Identifier of the buffer. * @return GL buffer. */ GLBuffer &buffer(Id id = 1) const; template VBType &buffer(Id id = 1) const { DENG2_ASSERT(dynamic_cast(&buffer(id)) != 0); return static_cast(buffer(id)); } GLBuffer &buffer(Name const &bufferName) const; Id bufferId(Name const &bufferName) const; /** * Finds an exising program. * @param id Identifier of the program. * @return GL program. */ GLProgram &program(Id id = 0) const; GLProgram &program(Name const &programName) const; Id programId(Name const &programName) const; GLProgram const &programForBuffer(Id bufferId) const; GLProgram const &programForBuffer(Name const &bufferName) const; /** * Finds an existing state. * @param id Identifier of the state. * @return GL state. */ GLState &state(Id id) const; GLState &state(Name const &stateName) const; Id stateId(Name const &stateName) const; GLState const *stateForBuffer(Id bufferId) const; GLState const *stateForBuffer(Name const &bufferName) const; /** * Adds a new buffer or replaces an existing one. The buffer will use the * default program. * * @param id Identifier of the buffer. * @param buffer GL buffer. Drawable gets ownership. */ void addBuffer(Id id, GLBuffer *buffer); Id addBuffer(Name const &bufferName, GLBuffer *buffer); /** * Adds a new buffer, reserving an unused identifier for it. The chosen * identifier is larger than any of the buffer identifiers currently in * use. The buffer will use the default program. * * @param buffer GL buffer. Drawable gets ownership. * * @return Identifier chosen for the buffer. */ Id addBuffer(GLBuffer *buffer); /** * Adds a new buffer, reserving an unused identifier for it. The chosen * identifier is larger than any of the buffer identifiers currently in * use. The buffer will use a new program. * * @param buffer GL buffer. Drawable gets ownership. * @param programName Name for the program. * * @return Identifier chosen for the buffer. */ Id addBufferWithNewProgram(GLBuffer *buffer, Name const &programName = ""); void addBufferWithNewProgram(Id id, GLBuffer *buffer, Name const &programName = ""); Id addBufferWithNewProgram(Name const &bufferName, GLBuffer *buffer, Name const &programName = ""); /** * Creates a program or replaces an existing one with a blank program. * @param id Identifier of the program. Cannot be zero. * @return GL program. */ GLProgram &addProgram(Id id); Id addProgram(Name const &programName); /** * Creates a state or replaces an existing one with a default state. * @param id Identifier of the state. * @param state State to add. * @return GL state. */ GLState &addState(Id id, GLState const &state = GLState()); Id addState(Name const &stateName, GLState const &state = GLState()); void removeBuffer(Id id); void removeProgram(Id id); void removeState(Id id); void removeBuffer(Name const &bufferName); void removeProgram(Name const &programName); void removeState(Name const &stateName); /** * Sets the program to be used with a buffer. * * @param bufferId Buffer whose program is being set. * @param program GL program instance. If not owned by Drawable, * must not be destroyed while Drawable is using it. */ void setProgram(Id bufferId, GLProgram &program); void setProgram(Id bufferId, Name const &programName); void setProgram(Name const &bufferName, GLProgram &program); void setProgram(Name const &bufferName, Name const &programName); /** * Sets the program to be used with all buffers. * * @param program GL program instance. If not owned by Drawable, * must not be destroyed while Drawable is using it. */ void setProgram(GLProgram &program); void setProgram(Name const &programName); /** * Sets the state to be used with a buffer. * * @param bufferId Buffer whose state is being set. * @param state GL state instance. If not owned by Drawable, * must not be destroyed while Drawable is using it. */ void setState(Id bufferId, GLState &state); void setState(Name const &bufferName, GLState &state); void setState(Id bufferId, Name const &stateName); void setState(Name const &bufferName, Name const &stateName); /** * Sets the state to be used with all buffers. * * @param state GL state instance. If not owned by Drawable, * must not be destroyed while Drawable is using it. */ void setState(GLState &state); void setState(Name const &stateName); /** * Removes the state configured for a buffer. When the buffer is drawn, the * current GL state from the state stack is used instead of some custom * state. * * @param bufferId Buffer whose state is to be unset. */ void unsetState(Id bufferId); void unsetState(Name const &bufferName); /** * Removes the state configured for all buffers. When drawing, the current * GL state from the state stack is used instead of some custom state. */ void unsetState(); /** * Draws all the buffers using the selected program(s) and state(s). * Drawing is only allowed when all assets are ready. */ virtual void draw() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_DRAWABLE_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/modelbank.h0000664000175000017500000000412112641367671025205 0ustar jaakkojaakko/** @file modelbank.h Bank containing 3D models. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_MODELBANK_H #define LIBGUI_MODELBANK_H #include #include namespace de { /** * Bank of ModelDrawable instances. * * Loads model files using background tasks, as model files may contain large amounts of * geometry and preprocessing operations may be involved. * * @ingroup gl */ class LIBGUI_PUBLIC ModelBank : public Bank { public: /** * Interface for auxiliary data for a loaded model. @ingroup gl */ class LIBGUI_PUBLIC IUserData { public: virtual ~IUserData() {} DENG2_AS_IS_METHODS() }; typedef std::pair ModelWithData; public: ModelBank(); void add(DotPath const &id, String const &sourcePath); ModelDrawable &model(DotPath const &id); /** * Sets the user data of a loaded model. * @param id Model identifier. * @param userData User data object. Ownership taken. */ void setUserData(DotPath const &id, IUserData *userData); IUserData const *userData(DotPath const &id) const; ModelWithData modelAndData(DotPath const &id); protected: IData *loadFromSource(ISource &source); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_MODELBANK_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/glprogram.h0000664000175000017500000000700512641367671025247 0ustar jaakkojaakko/** @file glprogram.h GL shader program. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_GLPROGRAM_H #define LIBGUI_GLPROGRAM_H #include #include #include #include #include #include "../gui/libgui.h" #include "glbuffer.h" #include "opengl.h" namespace de { class GLUniform; class GLShader; /** * GL shader program consisting of a vertex and fragment shaders. * * GLProgram instances work together with GLUniform to manage the program * state. To allow a particular uniform to be used in a program, it first * has to be bound to the program. * * When binding texture uniforms, the order of the binding calls is important * as it determines which texture sampling unit each of the textures is * allocated: the first bound texture uniform gets unit #0, the second one gets * unit #1, etc. * * @ingroup gl */ class LIBGUI_PUBLIC GLProgram : public Asset { public: /// Failed to allocate a new GL program object. @ingroup errors DENG2_ERROR(AllocError); /// Failed to link the program. @ingroup errors DENG2_ERROR(LinkerError); public: GLProgram(); /** * Resets the program back to an empty state. All uniform bindings are * removed. */ void clear(); /** * Builds a program out of two shaders. GLProgram retains a reference to * both shaders. * * @param vertexShader Vertex shader. * @param fragmentShader Fragment shader. * * @return Reference to this program. */ GLProgram &build(GLShader const *vertexShader, GLShader const *fragmentShader); GLProgram &build(IByteArray const &vertexShaderSource, IByteArray const &fragmentShaderSource); void rebuildBeforeNextUse(); void rebuild(); GLProgram &operator << (GLUniform const &uniform); GLProgram &bind(GLUniform const &uniform); GLProgram &unbind(GLUniform const &uniform); /** * Takes this program into use. Only one GLProgram can be in use at a time. */ void beginUse() const; void endUse() const; /** * Returns the program currently in use. */ static GLProgram const *programInUse(); GLuint glName() const; int glUniformLocation(char const *uniformName) const; /** * Determines which attribute location is used for a particular attribute semantic. * These locations are available after the program has been successfully built * (linked). * * @param semantic Attribute semantic. * * @return Attribute location, or -1 if the semantic is not used in the program. */ int attributeLocation(internal::AttribSpec::Semantic semantic) const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_GLPROGRAM_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/colorbank.h0000664000175000017500000000407312641367671025231 0ustar jaakkojaakko/** @file colorbank.h Bank of colors. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_COLORBANK_H #define LIBGUI_COLORBANK_H #include "../gui/libgui.h" #include #include #include namespace de { /** * Bank of colors where each color is identified by a Path. */ class LIBGUI_PUBLIC ColorBank : public InfoBank { public: typedef Vector4ub Color; typedef Vector4f Colorf; public: ColorBank(); /** * Creates a number of colors based on information in an Info document. * The file is parsed first. * * @param file File with Info source containing color definitions. */ void addFromInfo(File const &file); /** * Finds a specific color. * * @param path Identifier of the color. * * @return Vector with the color values (0...255). */ Color color(DotPath const &path) const; /** * Finds a specific color. * * @param path Identifier of the color. * * @return Vector with the floating-point color values (0...1). */ Colorf colorf(DotPath const &path) const; protected: virtual ISource *newSourceFromInfo(String const &id); virtual IData *loadFromSource(ISource &source); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_COLORBANK_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/glframebuffer.h0000664000175000017500000000675712641367671026101 0ustar jaakkojaakko/** @file glframebuffer.h GL frame buffer. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_GLFRAMEBUFFER_H #define LIBGUI_GLFRAMEBUFFER_H #include #include #include "../GLTarget" #include "../GLTexture" #include "../Image" namespace de { namespace gl { enum SwapBufferMode { SwapMonoBuffer, SwapStereoLeftBuffer, SwapStereoRightBuffer, SwapStereoBuffers, SwapWithAlpha }; } class Canvas; /** * GL framebuffer that stores color, depth, and stencil values in GL textures. * Automatically sets up and updates a render target where the textures are * attached. Provides a way to swap the contents of the framebuffer to a * Canvas's back buffer. * * The framebuffer must be manually resized as appropriate. * * Supports multisampling using EXT_framebuffer_multisample and * EXT_framebuffer_blit. */ class LIBGUI_PUBLIC GLFramebuffer : public Asset { public: typedef Vector2ui Size; public: GLFramebuffer(Image::Format const & colorFormat = Image::RGB_888, Size const & initialSize = Size(), int sampleCount = 0 /*default*/); void glInit(); void glDeinit(); void setSampleCount(int sampleCount); void setColorFormat(Image::Format const &colorFormat); /** * Resizes the framebuffer's textures. If the new size is the same as the * current one, nothing happens. * * @param newSize New size for the framebuffer. */ void resize(Size const &newSize); Size size() const; GLTarget &target() const; GLTexture &colorTexture() const; GLTexture &depthStencilTexture() const; int sampleCount() const; /** * Swaps buffers. * * @param canvas Canvas where to swap. * @param swapMode Stereo swapping mode: * - gl::SwapMonoBuffer: swap is done normally into the Canvas's framebuffer * - gl::SwapStereoLeftBuffer: swap updates the back/left stereo buffer * - gl::SwapStereoRightBuffer: swap updates the back/right stereo buffer */ void swapBuffers(Canvas &canvas, gl::SwapBufferMode swapMode = gl::SwapMonoBuffer); void drawBuffer(float opacity); public: /** * Sets the default sample count for all frame buffers. * * All existing GLFramebuffer instances that have been set to use the default sample * count will be updated to use the new count (i.e., contents lost). * * @param sampleCount Sample count. * * @return @c true, iff the default value was changed. */ static bool setDefaultMultisampling(int sampleCount); static int defaultMultisampling(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_GLFRAMEBUFFER_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/glshader.h0000664000175000017500000000443312641367671025050 0ustar jaakkojaakko/** @file glshader.h GL shader. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_GLSHADER_H #define LIBGUI_GLSHADER_H #include #include #include #include #include #include #include "../gui/libgui.h" #include "opengl.h" namespace de { /** * GL shader. * * Shader instances are reference-counted so that they can be shared by many * programs. * * @ingroup gl */ class LIBGUI_PUBLIC GLShader : public Counted, public Asset { public: enum Type { Vertex, Fragment }; /// There was a failure related to OpenGL resource allocation. @ingroup errors DENG2_ERROR(AllocError); /// Shader source could not be compiled. @ingroup errors DENG2_ERROR(CompilerError); public: GLShader(); GLShader(Type shaderType, IByteArray const &source); Type type() const; GLuint glName() const; void clear(); void compile(Type shaderType, IByteArray const &source); void recompile(); /** * Prefixes a piece of shader source code to another shader source. This takes * into account that certain elements must remain at the beginning of the source * (\#version). * * @param source Main source where prefixing is done. * @param prefix Source to prefix in the beginning of @a source. * * @return The resulting combination. */ static Block prefixToSource(Block const &source, Block const &prefix); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_GLSHADER_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/heightmap.h0000664000175000017500000000267512641367671025233 0ustar jaakkojaakko/** @file heightmap.h Height map. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_HEIGHTMAP_H #define LIBGUI_HEIGHTMAP_H #include #include #include namespace de { /** * Height map. * * @ingroup gl */ class HeightMap { public: HeightMap(); void setMapSize(Vector2f const &worldSize, float heightRange); void loadGrayscale(Image const &heightImage); Image toImage() const; Image makeNormalMap() const; float heightAtPosition(Vector2f const &worldPos) const; Vector3f normalAtPosition(Vector2f const &worldPos) const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_HEIGHTMAP_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/glshaderbank.h0000664000175000017500000000371212641367671025703 0ustar jaakkojaakko/** @file glshaderbank.h Bank containing GL shaders. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_GLSHADERBANK_H #define LIBGUI_GLSHADERBANK_H #include #include "../GLShader" namespace de { class GLProgram; /** * Bank containing GL shaders. * * Shader objects are automatically shared between created programs. Programs * are built based on definitions from an Info file (i.e., which vertex and * fragment shader(s) to use for the program). * * Shaders and programs cannot be accessed until OpenGL is ready. */ class LIBGUI_PUBLIC GLShaderBank : public InfoBank { public: GLShaderBank(); void addFromInfo(File const &file); GLShader &shader(DotPath const &path, GLShader::Type type) const; /** * Builds a GL program using the defined shaders. * * @param program Program to build. * @param path Identifier of the shader. * * @return Reference to @a program. */ GLProgram &build(GLProgram &program, DotPath const &path) const; protected: ISource *newSourceFromInfo(String const &id); IData *loadFromSource(ISource &source); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_GLSHADERBANK_H doomsday-stable-1.15.7/doomsday/libgui/include/de/graphics/gltarget.h0000664000175000017500000002412312641367671025066 0ustar jaakkojaakko/** @file gltarget.h GL render target. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_GLTARGET_H #define LIBGUI_GLTARGET_H #include #include #include #include #include #include #include #include "../gui/libgui.h" #include "opengl.h" #include "../GLTexture" namespace de { /** * GL render target. * * @ingroup gl */ class LIBGUI_PUBLIC GLTarget : public Asset { public: /// Something is incorrect in the configuration of the contained /// framebuffer object. @ingroup errors DENG2_ERROR(ConfigError); enum Flag { Color = 0x1, ///< Target has a color attachment. Depth = 0x2, ///< Target has a depth attachment. Stencil = 0x4, ///< Target has a stencil attachment. Changed = 0x8, ///< Draw/clear has occurred on the target. ColorDepth = Color | Depth, ColorDepthStencil = Color | Depth | Stencil, ColorStencil = Color | Stencil, DepthStencil = Depth | Stencil, SeparateDepthAndStencil = 0x10, ///< Depth and stencil should use separate buffers. NoAttachments = 0, DefaultFlags = ColorDepth }; Q_DECLARE_FLAGS(Flags, Flag) typedef Vector2ui Size; /** * Utility for temporarily using an alternative buffer as one of a render * target's attachments. Usage: * - construct as a local variable * - init * - automatically deinited when goes out of scope */ class LIBGUI_PUBLIC AlternativeBuffer { public: /** * Prepares an alternative texture attachment. The new texture is not * taken into use yet. * * @param target The rendering target this is for. * @param texture Texture to use as alternative attachment. * @param attachment Which attachment. */ AlternativeBuffer(GLTarget &target, GLTexture &texture, Flags const &attachment); /** * Automatically deinitialize the alternative buffer, if it was taken * into use. */ ~AlternativeBuffer(); /** * Take the alternative buffer into use. The original buffer is * remembered and will be restored when the alternative buffer is no * longer in use. Nothing happens if this already has been called * previously. * * @return @c true, if initialization was done. Otherwise, @c false * (for example if already initialized). */ bool init(); /** * Restores the original attachment. * * @return @c true, if the original attachment was restored. Otherwise, * @c false (for example if already deinitialized). */ bool deinit(); GLTarget &target() const; private: DENG2_PRIVATE(d) }; public: /** * Constructs a default render target (the window framebuffer). */ GLTarget(); /** * Constructs a render target than renders onto a texture. The texture must * be initialized with the appropriate size beforehand. * * @param colorTarget Target texture for Color attachment. * @param otherAttachments Other supporting attachments (renderbuffers). */ GLTarget(GLTexture &colorTarget, Flags const &otherAttachments = NoAttachments); /** * Constructs a render target with a texture attachment and optionally * other renderbuffer attachments. * * @param attachment Where to attach the texture (color, depth, stencil). * @param texture Texture to render on. * @param otherAttachments Other supporting attachments (renderbuffers). */ GLTarget(Flags const &attachment, GLTexture &texture, Flags const &otherAttachments = NoAttachments); //GLTarget(GLTexture *color, GLTexture *depth = 0, GLTexture *stencil = 0); /** * Constructs a render target with a specific size. * * @param size Size of the render target. * @param flags Attachments to set up. */ GLTarget(Vector2ui const &size, Flags const &flags = DefaultFlags); Flags flags() const; /** * Marks the rendering target modified. This is done automatically when the * target is cleared or when GLBuffer is used to draw something on the * target. */ void markAsChanged(); /** * Reconfigures the render target back to the default OpenGL framebuffer. * All attachments will be released. */ void configure(); /** * Configure the target with one or more renderbuffers. Multisampled buffers * can be configured by giving a @a sampleCount higher than 1. * * @param size Size of the render target. * @param flags Which attachments to set up. * @param sampleCount Number of samples per pixel in each attachment. */ void configure(Vector2ui const &size, Flags const &flags = DefaultFlags, int sampleCount = 1); /** * Reconfigures the render target with two textures, one for the color * values and one for depth/stencil values. * * If @a colorTex or @a depthStencilTex is omitted, a renderbuffer will be * created in its place. * * Any previous attachments are released. * * @param colorTex Texture for color values. * @param depthStencilTex Texture for depth/stencil values. */ void configure(GLTexture *colorTex, GLTexture *depthStencilTex); /** * Changes the configuration of the render target. Any previously allocated * renderbuffers are released. * * @param attachment Where to attach @a texture. * @param texture Texture to render on. * @param otherAttachments Other supporting attachments (renderbuffers). */ void configure(Flags const &attachment, GLTexture &texture, Flags const &otherAttachments = NoAttachments); /** * Activates this render target as the one where GL drawing is being done. */ void glBind() const; /** * Deactivates the render target. */ void glRelease() const; GLuint glName() const; Size size() const; /** * Copies the contents of the render target's color attachment to an image. */ QImage toImage() const; /** * Sets the color for clearing the target (see clear()). * * @param color Color for clearing. */ void setClearColor(Vector4f const &color); /** * Clears the contents of the render target's attached buffers. * * @param attachments Which ones to clear. */ void clear(Flags const &attachments); /** * Resizes the target's attached buffers and/or textures to a new size. * Nothing happens if the provided size is the same as the current size. If * resizing occurs, the contents of all buffers/textures become undefined. * * @param size New size for buffers and/or textures. */ void resize(Size const &size); /** * Returns the texture being used for a particular attachment in this target. * * @param attachment Which attachment. * @return */ GLTexture *attachedTexture(Flags const &attachment) const; /** * Replaces a currently attached texture with another. * * @param attachment Which attachment. * @param texture New texture to use as the attachment. */ void replaceAttachment(Flags const &attachment, GLTexture &texture); /** * Sets the target that is actually bound when this GLTarget is bound. * Intended to be used with multisampled buffers. * * @param proxy Proxy target. */ void setProxy(GLTarget const *proxy); void updateFromProxy(); /** * Blits this target's contents to the @a copy target. * * @param dest Target where to copy this target's contents. * @param attachments Which attachments to blit. * @param filtering Filtering to use if source and destinations sizes * aren't the same. */ void blit(GLTarget &dest, Flags const &attachments = Color, gl::Filter filtering = gl::Nearest) const; /** * Sets the subregion inside the render target where scissor and viewport * will be scaled into. Scissor and viewport can still be defined as if the * entire window was in use; this only applies an offset and scaling to * both. * * @param rect Target window rectangle. Set a null rectangle to * use the entire window (like normally). * @param applyGLState Immediately update current OpenGL state accordingly. */ void setActiveRect(Rectangleui const &rect, bool applyGLState = false); void unsetActiveRect(bool applyGLState = false); Vector2f activeRectScale() const; Vector2f activeRectNormalizedOffset() const; Rectangleui scaleToActiveRect(Rectangleui const &rect) const; Rectangleui const &activeRect() const; bool hasActiveRect() const; /** * Returns the area of the target currently in use. This is the full area * of the target, if there is no active rectangle specified. Otherwise * some sub-area of the target is returned. */ Rectangleui rectInUse() const; private: DENG2_PRIVATE(d) }; Q_DECLARE_OPERATORS_FOR_FLAGS(GLTarget::Flags) } // namespace de #endif // LIBGUI_GLTARGET_H doomsday-stable-1.15.7/doomsday/libgui/include/de/FontBank0000664000175000017500000000003312641367671022723 0ustar jaakkojaakko#include "text/fontbank.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/audio/0000775000175000017500000000000012641367671022403 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/include/de/audio/waveformbank.h0000664000175000017500000000324312641367671025240 0ustar jaakkojaakko/** @file waveformbank.h Bank containing Waveform instances. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_WAVEFORMBANK_H #define LIBGUI_WAVEFORMBANK_H #include "../Waveform" #include namespace de { class File; /** * Bank containing Waveform instances loaded from files. * * @ingroup data */ class LIBGUI_PUBLIC WaveformBank : public InfoBank { public: /** * Constructs a new audio waveform bank. * * @param flags Bank behavior. */ WaveformBank(Flags const &flags = DisableHotStorage); void add(DotPath const &id, String const &waveformFilePath); void addFromInfo(File const &file); Waveform const &waveform(DotPath const &id) const; protected: ISource *newSourceFromInfo(String const &id); IData *loadFromSource(ISource &source); IData *newData(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_WAVEFORMBANK_H doomsday-stable-1.15.7/doomsday/libgui/include/de/audio/waveform.h0000664000175000017500000000523112641367671024403 0ustar jaakkojaakko/** @file waveform.h Audio waveform. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_WAVEFORM_H #define LIBGUI_WAVEFORM_H #include "../gui/libgui.h" #include #include namespace de { namespace audio ///< Namespace for audio related enumerations and constants. { enum Format { PCMLittleEndian, Compressed }; } /** * Audio waveform consisting of a sequence of audio samples in raw form or in some * compressed format. The sample data may be stored in memory or might be streamed from a * File. * * @ingroup audio */ class LIBGUI_PUBLIC Waveform { public: /// Failed to load audio data from source. @ingroup errors DENG2_ERROR(LoadError); /// Format of the source data is not supported. @ingroup errors DENG2_SUB_ERROR(LoadError, UnsupportedFormatError); public: Waveform(); void clear(); /** * Loads an audio waveform from a file. * * @param file File to load from. */ void load(File const &file); audio::Format format() const; /** * Provides the sample data of the audio waveform in a memory buffer. For compressed * formats, the returned data is the contents of the source file. * * @return Block of audio samples. */ Block sampleData() const; /** * Returns the File this Waveform has been loaded from. * * @return File with source data. */ File const *sourceFile() const; duint channelCount() const; /** * Bits per sample on a channel. */ duint bitsPerSample() const; dsize sampleCount() const; /** * Number of samples to play per second. */ duint sampleRate() const; /** * Playing duration of the audio waveform, assuming sample count and sample rate * are known. */ TimeDelta duration() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_WAVEFORM_H doomsday-stable-1.15.7/doomsday/libgui/include/de/audio/sound.h0000664000175000017500000001061612641367671023710 0ustar jaakkojaakko/** @file sound.h Base class for playable audio waveforms. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_SOUND_H #define LIBGUI_SOUND_H #include #include #include namespace de { /** * Abstract base class for a playing sound. Provides methods for controlling how the * sound is played, and querying the status of the sound. * * After creation, sounds are in a paused state, after which they can be configured with * volume, frequency, etc. and started in either looping or non-looping mode. * * By default, sounds use 2D stereo positioning. 3D positioning is enabled when * calling Sound::setPosition(). * * @ingroup audio */ class Sound { public: Sound(); /** * Sounds are automatically stopped when deleted. */ virtual ~Sound() {} enum PlayingMode { NotPlaying, Once, ///< Play once and then delete the sound. OnceDontDelete, ///< Play once then pause the sound. Looping ///< Keep looping the sound. }; enum Positioning { Stereo, ///< Simple 2D stereo, not 3D. Absolute, HeadRelative }; // Methods for controlling the sound: /** * Starts playing the sound. If the sound is already playing, does nothing: * to change the playing mode, one has to first stop the sound. * * The implementation must notify the Play audience. * * @param mode Playing mode: * - Sound::Once: Play once, after which the sound gets automatically * deleted. The caller is expected to observe the Sound instance * for deletion or not retain a reference to it. * - Sound::OnceDontDelete: Play once, after which the sound remains * in a paused state. The sound can then be restarted later. * - Sound::Looping: Play and keep looping indefinitely. */ virtual void play(PlayingMode mode = Once) = 0; virtual void stop() = 0; virtual void pause() = 0; virtual void resume() = 0; virtual Sound &setVolume(dfloat volume); virtual Sound &setPan(dfloat pan); virtual Sound &setFrequency(dfloat factor); virtual Sound &setPosition(Vector3f const &position, Positioning positioning = Absolute); virtual Sound &setVelocity(Vector3f const &velocity); virtual Sound &setMinDistance(dfloat minDistance); virtual Sound &setSpatialSpread(dfloat degrees); // Methods for querying the sound status: virtual PlayingMode mode() const = 0; virtual bool isPaused() const = 0; virtual bool isPlaying() const; virtual dfloat volume() const; virtual dfloat pan() const; virtual dfloat frequency() const; virtual Positioning positioning() const; virtual Vector3f position() const; virtual Vector3f velocity() const; virtual dfloat minDistance() const; virtual dfloat spatialSpread() const; DENG2_AS_IS_METHODS() /// Audience that is notified when the sound is played. DENG2_DEFINE_AUDIENCE2(Play, void soundPlayed(Sound const &)) /// Audience that is notified when the properties of the sound change. DENG2_DEFINE_AUDIENCE2(Change, void soundPropertyChanged(Sound const &)) /// Audience that is notified when the sound stops. DENG2_DEFINE_AUDIENCE2(Stop, void soundStopped(Sound &)) /// Audience that is notified when the sound instance is deleted. DENG2_DEFINE_AUDIENCE2(Deletion, void soundBeingDeleted(Sound &)) protected: /// Called after a property value has been changed. virtual void update() = 0; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_SOUND_H doomsday-stable-1.15.7/doomsday/libgui/include/de/CanvasWindow0000664000175000017500000000003612641367671023627 0ustar jaakkojaakko#include "gui/canvaswindow.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/ColorBank0000664000175000017500000000004012641367671023071 0ustar jaakkojaakko#include "graphics/colorbank.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/GLProgram0000664000175000017500000000004012641367671023051 0ustar jaakkojaakko#include "graphics/glprogram.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/PersistentCanvasWindow0000664000175000017500000000005012641367671025704 0ustar jaakkojaakko#include "gui/persistentcanvaswindow.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/KeyEventSource0000664000175000017500000000004212641367671024134 0ustar jaakkojaakko#include "input/keyeventsource.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/GLTexture0000664000175000017500000000004012641367671023102 0ustar jaakkojaakko#include "graphics/gltexture.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/GLBuffer0000664000175000017500000000003712641367671022661 0ustar jaakkojaakko#include "graphics/glbuffer.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/gui/0000775000175000017500000000000012641367671022066 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/include/de/gui/guiapp.h0000664000175000017500000000477412641367671023540 0ustar jaakkojaakko/** @file guiapp.h Application with GUI support. * * @authors Copyright © 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_GUIAPP_H #define LIBGUI_GUIAPP_H #include "libgui.h" #include #include #include /** * Macro for conveniently accessing the de::GuiApp singleton instance. */ #define DENG2_GUI_APP (static_cast(qApp)) namespace de { /** * Application with GUI support. * * The event loop is protected against uncaught exceptions. Catches the * exception and shuts down the app cleanly. * * @ingroup core */ class LIBGUI_PUBLIC GuiApp : public QApplication, public App, DENG2_OBSERVES(Loop, Iteration) { Q_OBJECT public: /** * Notified when a Canvas is recreated. */ DENG2_DEFINE_AUDIENCE2(GLContextChange, void appGLContextChanged()) public: GuiApp(int &argc, char **argv); void setMetadata(String const &orgName, String const &orgDomain, String const &appName, String const &appVersion); bool notify(QObject *receiver, QEvent *event); /** * Emits the displayModeChanged() signal. * * @todo In the future when de::GuiApp (or a sub-object owned by it) is * responsible for display modes, this should be handled internally and * not via this public interface where anybody can call it. */ void notifyDisplayModeChanged(); void notifyGLContextChanged(); int execLoop(); void stopLoop(int code); Loop &loop(); protected: NativePath appDataPath() const; // Observes Loop iteration. void loopIteration(); signals: /// Emitted when the display mode has changed. void displayModeChanged(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_GUIAPP_H doomsday-stable-1.15.7/doomsday/libgui/include/de/gui/canvas.h0000664000175000017500000001415212641367671023515 0ustar jaakkojaakko/** @file canvas.h OpenGL drawing surface (QWidget). * * @authors Copyright (c) 2012-2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_CANVAS_H #define LIBGUI_CANVAS_H #include #include #include #include #include "../KeyEventSource" #include "../MouseEventSource" #include "../GLTarget" #include "../GLFramebuffer" namespace de { class CanvasWindow; /** * Drawing canvas with an OpenGL context and window surface. Each CanvasWindow * creates one Canvas instance on which to draw. Buffer swapping must be done * manually when appropriate. * * As Canvas is derived from KeyEventSource and MouseEventSource so that it * can submit user input to interested parties. * * @ingroup gui */ class LIBGUI_PUBLIC Canvas : public QGLWidget, public KeyEventSource, public MouseEventSource { Q_OBJECT public: typedef Vector2ui Size; /** * Notified when the canvas is ready for GL operations. The OpenGL context * and drawing surface are not ready to be used before that. The * notification occurs soon after the widget first becomes visible on * screen. Note that the notification comes straight from the event loop * (timer signal) instead of during a paint event. */ DENG2_DEFINE_AUDIENCE2(GLReady, void canvasGLReady(Canvas &)) /** * Notified when the canvas's GL state needs to be initialized. This is * called immediately before drawing the contents of the canvas for the * first time (during a paint event). */ DENG2_DEFINE_AUDIENCE2(GLInit, void canvasGLInit(Canvas &)) /** * Notified when a canvas's size has changed. */ DENG2_DEFINE_AUDIENCE2(GLResize, void canvasGLResized(Canvas &)) /** * Notified when drawing of the canvas contents has been requested. */ DENG2_DEFINE_AUDIENCE2(GLDraw, void canvasGLDraw(Canvas &)) /** * Notified when the canvas gains or loses input focus. */ DENG2_DEFINE_AUDIENCE2(FocusChange, void canvasFocusChanged(Canvas &, bool hasFocus)) public: explicit Canvas(CanvasWindow *parent, QGLWidget* shared = 0); /** * Sets or changes the CanvasWindow that owns this Canvas. * * @param parent Canvas window instance. */ void setParent(CanvasWindow *parent); /** * Grabs the contents of the canvas framebuffer. * * @param outputSize If specified, the contents will be scaled to this size before * the image is returned. * * @return Framebuffer contents (no alpha channel). */ QImage grabImage(QSize const &outputSize = QSize()); /** * Grabs a portion of the contents of the canvas framebuffer. * * @param area Portion to grab. * @param outputSize If specified, the contents will be scaled to this size before * the image is returned. * * @return Framebuffer contents (no alpha channel). */ QImage grabImage(QRect const &area, QSize const &outputSize = QSize()); /** * Grabs the contents of the canvas framebuffer and creates an OpenGL * texture out of it. * * @param outputSize If specified, the contents will be scaled to this size before * the image is returned. * * @return OpenGL texture name. Caller is responsible for deleting the texture. */ GLuint grabAsTexture(QSize const &outputSize = QSize()); GLuint grabAsTexture(QRect const &area, QSize const &outputSize = QSize()); /** * Returns the size of the canvas in device pixels. */ Size size() const; /** * Returns the width of the canvas in device pixels. */ inline int width() const { return size().x; } /** * Returns the height of the canvas in device pixels. */ inline int height() const { return size().y; } /** * When the mouse is trapped, all mouse input is grabbed, the mouse cursor * is hidden, and mouse movement is submitted as deltas via EventSource. * * @param trap Enable or disable the mouse trap. */ void trapMouse(bool trap = true); /** * Determines if the mouse is presently trapped by the canvas. */ bool isMouseTrapped() const; bool isGLReady() const; /** * Replaces the current audiences of this canvas with another canvas's * audiences. * * @param other Canvas instance. */ void copyAudiencesFrom(Canvas const &other); /** * Returns a render target that renders to this canvas. * * @return GL render target. */ GLTarget &renderTarget() const; GLFramebuffer &framebuffer(); /** * Copies or swaps the back buffer to the front, making it visible. */ void swapBuffers(gl::SwapBufferMode swapMode = gl::SwapMonoBuffer); protected: void initializeGL(); void resizeGL(int w, int h); void paintGL(); // Native events. void focusInEvent(QFocusEvent *ev); void focusOutEvent(QFocusEvent *ev); void keyPressEvent(QKeyEvent *ev); void keyReleaseEvent(QKeyEvent *ev); void mousePressEvent(QMouseEvent *ev); void mouseReleaseEvent(QMouseEvent *ev); void mouseMoveEvent(QMouseEvent *ev); void wheelEvent(QWheelEvent *ev); void showEvent(QShowEvent *ev); protected slots: void notifyReady(); void updateSize(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_CANVAS_H doomsday-stable-1.15.7/doomsday/libgui/include/de/gui/persistentcanvaswindow.h0000664000175000017500000001243012641367671027063 0ustar jaakkojaakko/** @file persistentcanvaswindow.h Canvas window with persistent state. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_PERSISTENTCANVASWINDOW_H #define LIBGUI_PERSISTENTCANVASWINDOW_H #include #include #include namespace de { #undef main /** * General-purpose top-level window with persistent state. Each instance must * be identified by a unique name (e.g., "main") that is used when saving the * window's state to Config. * * Supports fullscreen display modes (using DisplayMode). */ class LIBGUI_PUBLIC PersistentCanvasWindow : public CanvasWindow { Q_OBJECT public: /// Provided window ID was not valid. @ingroup errors DENG2_ERROR(InvalidIdError); /// Absolute minimum width of a window (in fullscreen also). static int const MIN_WIDTH; /// Absolute minimum height of a window (in fullscreen also). static int const MIN_HEIGHT; /** * Window attributes. */ enum Attribute { End = 0, ///< Marks the end of an attribute list (not a valid attribute in itself) // Windowed attributes Left, Top, Width, Height, Centered, Maximized, // Fullscreen attributes Fullscreen, FullscreenWidth, FullscreenHeight, ColorDepthBits, // Other FullSceneAntialias, VerticalSync }; /** * Notified after one or more window attributes have changed. If the * changes are queued, the notification is made only after all the changes * have been applied. */ DENG2_DEFINE_AUDIENCE2(AttributeChange, void windowAttributesChanged(PersistentCanvasWindow &)) public: /** * Constructs a new window using the persistent configuration associated * with @a id. Note that the configuration is saved persistently when the * window is deleted. * * Command line options (e.g., -xpos) can be used to modify the main window * configuration directly. * * @param id Identifier of the window. */ PersistentCanvasWindow(String const &id); String id() const; /** * Returns @c true iff the window is currently centered. */ bool isCentered() const; /** * Returns the current placement of the window when it is in normal window * mode (neither fullscreen or maximized). */ Rectanglei windowRect() const; Size fullscreenSize() const; inline int fullscreenWidth() const { return fullscreenSize().x; } inline int fullscreenHeight() const { return fullscreenSize().y; } int colorDepthBits() const; void show(bool yes = true); /** * Sets or changes one or more window attributes. * * @param attribs Array of values: *
[ attribId, value, attribId, value, ..., 0 ]
* The array must be zero-terminated, as that indicates where the * array ends (see Attribute). * * @return @c true iff all attribute deltas were validated and the window * was then successfully updated accordingly. If any attribute failed to * validate, the window will remain unchanged and @a false is returned. */ bool changeAttributes(int const *attribs); /** * Saves the window's state into a persistent storage so that it can be later * on restored. Used at shutdown time to save window geometry. */ void saveToConfig(); /** * Restores the window's state from persistent storage. Used when creating * a window to determine its persistent configuration. */ void restoreFromConfig(); /** * Saves the current state in memory (not persistently). The saved state can * later be restored with a call to restoreState(). */ void saveState(); /** * Restores the attribuets of the window from previously saved state. */ void restoreState(); static PersistentCanvasWindow &main(); // Events. void moveEvent(QMoveEvent *); void resizeEvent(QResizeEvent *); protected slots: void performQueuedTasks(); /** * Forms the name of a Config variable for this window. Subclasses are * allowed to use this to store their own properties in the persistent * configuration. * * @param key Variable name. * * @return Name of a variable in Config. */ String configName(String const &key) const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_PERSISTENTCANVASWINDOW_H doomsday-stable-1.15.7/doomsday/libgui/include/de/gui/canvaswindow.h0000664000175000017500000001260612641367671024747 0ustar jaakkojaakko/** @file canvaswindow.h Top-level window with a Canvas. * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_CANVASWINDOW_H #define LIBGUI_CANVASWINDOW_H #include #include #include #include #include #include "../GLFramebuffer" #ifdef WIN32 # undef min # undef max #endif namespace de { /** * Top-level window that contains an OpenGL drawing canvas. @ingroup base * * CanvasWindow is the window frame and Canvas is the drawing surface. These * two are in separate classes as the former is a top-level window and the * latter is a regular widget inside the window. Also, the canvas may need to * be replaced with one using a different OpenGL surface (for instance when * switching on FSAA). This @em recreation of the canvas is managed here in * CanvasWindow. * * @see Canvas */ class LIBGUI_PUBLIC CanvasWindow : public QMainWindow, DENG2_OBSERVES(Canvas, GLReady), DENG2_OBSERVES(Canvas, GLDraw) { Q_OBJECT public: typedef Vector2ui Size; DENG2_AS_IS_METHODS() public: CanvasWindow(); /** * Determines if the canvas is ready for GL drawing. */ bool isReady() const; float frameRate() const; /** * Determines the current top left corner (origin) of the window. */ inline Vector2i pos() const { return Vector2i(x(), y()); } inline Size size() const { return Size(de::max(0, width()), de::max(0, height())); } /** * Determines the current width of window's Canvas in pixels. */ inline int width() const { return canvas().width(); } /** * Determines the current height of window's Canvas in pixels. */ inline int height() const { return canvas().height(); } /** * Recreates the contained Canvas with an updated GL format. The context is * shared with the old Canvas. */ void recreateCanvas(); Canvas& canvas() const; /** * Determines if a Canvas instance is owned by this window. * * @param c Canvas instance. * * @return @c true or @c false. */ bool ownsCanvas(Canvas *c) const; // Events. #ifdef WIN32 bool event(QEvent *); // Alt key kludge #endif void hideEvent(QHideEvent *); /** * Called when the Canvas is ready for OpenGL drawing (and visible). * Overriding methods must call this. * * @param canvas Canvas. */ virtual void canvasGLReady(Canvas &canvas); /** * Called from Canvas when a GL draw is requested. Overriding methods * must call this as the last operation (updates frame rate statistics). */ virtual void canvasGLDraw(Canvas &); enum GrabMode { GrabNormal, GrabHalfSized }; /** * Grab the contents of the window into an OpenGL texture. * * @param grabMode How to do the grabbing. * * @return OpenGL texture name. Caller is reponsible for deleting the texture. */ duint grabAsTexture(GrabMode grabMode = GrabNormal) const; duint grabAsTexture(de::Rectanglei const &area, GrabMode mode = GrabNormal) const; /** * Grabs the contents of the window and saves it into a native image file. * * @param path Name of the file to save. May include a file extension * that indicates which format to use (e.g, "screenshot.jpg"). * If omitted, defaults to PNG. * * @return @c true if successful, otherwise @c false. */ bool grabToFile(NativePath const &path) const; void swapBuffers(gl::SwapBufferMode swapMode = gl::SwapMonoBuffer) const; /** * Activates the window's GL context so that OpenGL API calls can be made. * The GL context is automatically active during the drawing of the window's * contents; at other times it needs to be manually activated. */ void glActivate(); /** * Dectivates the window's GL context after OpenGL API calls have been done. * The GL context is automatically deactived after the drawing of the window's * contents; at other times it needs to be manually deactivated. */ void glDone(); /** * Returns a handle to the native window instance. (Platform-specific.) */ void *nativeHandle() const; bool isRecreationInProgress() const; protected slots: void finishCanvasRecreation(); public: static bool mainExists(); static CanvasWindow &main(); static void setMain(CanvasWindow *window); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_CANVASWINDOW_H doomsday-stable-1.15.7/doomsday/libgui/include/de/gui/libgui.h0000664000175000017500000000544512641367671023522 0ustar jaakkojaakko/** @file libgui.h Common definitions for libgui. * * @authors Copyright © 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_MAIN_H #define LIBGUI_MAIN_H #include /* * The LIBGUI_PUBLIC macro is used for declaring exported symbols. It must be * applied in all exported classes and functions. DEF files are not used for * exporting symbols out of libgui. */ #if defined(_WIN32) && defined(_MSC_VER) # ifdef __LIBGUI__ // This is defined when compiling the library. # define LIBGUI_PUBLIC __declspec(dllexport) # else # define LIBGUI_PUBLIC __declspec(dllimport) # endif #else // No need to use any special declarators. # define LIBGUI_PUBLIC #endif #if defined(WIN32) || defined(MACOSX) # define LIBGUI_ACCURATE_TEXT_BOUNDS #endif // Assertion specific to GL errors. #if 1 //|| defined(DENG_X11) || defined(WIN32) # define LIBGUI_ASSERT_GL(cond) // just logged, no abort #else # define LIBGUI_ASSERT_GL(cond) DENG2_ASSERT(cond) #endif #define LIBGUI_GL_ERROR_STR(error) \ (error == GL_NO_ERROR? "GL_NO_ERROR" : \ error == GL_INVALID_ENUM? "GL_INVALID_ENUM" : \ error == GL_INVALID_VALUE? "GL_INVALID_VALUE" : \ error == GL_INVALID_OPERATION? "GL_INVALID_OPERATION" : \ error == GL_STACK_OVERFLOW? "GL_STACK_OVERFLOW" : \ error == GL_STACK_UNDERFLOW? "GL_STACK_UNDERFLOW" : \ error == GL_OUT_OF_MEMORY? "GL_OUT_OF_MEMORY" : \ error == GL_INVALID_FRAMEBUFFER_OPERATION? "GL_INVALID_FRAMEBUFFER_OPERATION" : \ "?") #ifndef NDEBUG # define LIBGUI_ASSERT_GL_OK() {GLuint _er = GL_NO_ERROR; do { \ _er = glGetError(); if(_er != GL_NO_ERROR) { \ LogBuffer_Flush(); qWarning(__FILE__":%i: OpenGL error: 0x%x (%s)", __LINE__, _er, \ LIBGUI_GL_ERROR_STR(_er)); LIBGUI_ASSERT_GL(!"OpenGL operation failed"); \ }} while(_er != GL_NO_ERROR);} #else # define LIBGUI_ASSERT_GL_OK() #endif #ifdef __cplusplus # define LIBGUI_EXTERN_C extern "C" #else # define LIBGUI_EXTERN_C extern #endif namespace de { } // namespace de #endif // LIBGUI_MAIN_H doomsday-stable-1.15.7/doomsday/libgui/include/de/gui/displaymode_native.h0000664000175000017500000000354712641367671026130 0ustar jaakkojaakko/** * @file displaymode_native.h * Changing and enumerating available display modes using native APIs. @ingroup gl * * These routines are only intended to be used privately by the DisplayMode * class. Never call these directly unless you are sure you want to access * platform specific low-level functionality. * * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDENG_DISPLAYMODE_NATIVE_H #define LIBDENG_DISPLAYMODE_NATIVE_H #include "displaymode.h" #ifdef __cplusplus extern "C" { #endif void DisplayMode_Native_Init(void); void DisplayMode_Native_Shutdown(void); int DisplayMode_Native_Count(void); void DisplayMode_Native_GetMode(int index, DisplayMode* mode); void DisplayMode_Native_GetCurrentMode(DisplayMode* mode); int DisplayMode_Native_Change(const DisplayMode* mode, int shouldCapture); void DisplayMode_Native_GetColorTransfer(DisplayColorTransfer *colors); void DisplayMode_Native_SetColorTransfer(DisplayColorTransfer const *colors); #ifdef MACOSX void DisplayMode_Native_Raise(void* handle); #endif #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_DISPLAYMODE_NATIVE_H doomsday-stable-1.15.7/doomsday/libgui/include/de/gui/displaymode.h0000664000175000017500000001077612641367671024564 0ustar jaakkojaakko/** * @file displaymode.h * Changing and enumerating available display modes. @ingroup gl * * High-level logic for enumerating, selecting, and changing display modes. See * displaymode_native.h for the platform-specific low-level routines. * * @todo This is C because it was relocated from the client. It should be * converted to a C++. * * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_DISPLAYMODE_H #define LIBGUI_DISPLAYMODE_H #include "libgui.h" #include "de/libcore.h" #ifdef __cplusplus extern "C" { #endif typedef struct displaymode_s { int width; int height; float refreshRate; // might be zero int depth; // Calculated automatically: int ratioX; int ratioY; } DisplayMode; typedef struct displaycolortransfer_s { unsigned short table[3 * 256]; // 0-255:red, 256-511:green, 512-767:blue (range: 0..ffff) } DisplayColorTransfer; /** * Initializes the DisplayMode class. Enumerates all available display modes and * saves the current display mode. Must be called at engine startup. * * @return @c true, if display modes were initialized successfully. */ LIBGUI_PUBLIC int DisplayMode_Init(void); /** * Gets the current color transfer function and saves it as the one that will be * restored at shutdown. */ LIBGUI_PUBLIC void DisplayMode_SaveOriginalColorTransfer(void); /** * Shuts down the DisplayMode class. The current display mode is restored to what * it was at initialization time. */ LIBGUI_PUBLIC void DisplayMode_Shutdown(void); /** * Returns the display mode that was in use when DisplayMode_Init() was called. */ LIBGUI_PUBLIC DisplayMode const *DisplayMode_OriginalMode(void); /** * Returns the current display mode. */ LIBGUI_PUBLIC DisplayMode const *DisplayMode_Current(void); /** * Returns the number of available display modes. */ LIBGUI_PUBLIC int DisplayMode_Count(void); /** * Returns one of the available display modes. Use DisplayMode_Count() to * determine how many modes are available. * * @param index Index of the mode, must be between 0 and DisplayMode_Count() - 1. */ LIBGUI_PUBLIC DisplayMode const *DisplayMode_ByIndex(int index); /** * Finds the closest available mode to the given criteria. * * @param width Width in pixels. * @param height Height in pixels. * @param depth Color depth (bits per color component). * @param freq Refresh rate. If zero, prefers rates closest to the mode at startup time. * * @return Mode that most closely matches the criteria. Always returns one of * the available modes; returns @c NULL only if DisplayMode_Init() has not yet * been called. */ LIBGUI_PUBLIC DisplayMode const *DisplayMode_FindClosest(int width, int height, int depth, float freq); /** * Determines if two display modes are equivalent. * * @param a DisplayMode instance. * @param b DisplayMode instance. * * @return @c true or @c false. */ LIBGUI_PUBLIC int DisplayMode_IsEqual(DisplayMode const *a, DisplayMode const *b); /** * Changes the display mode. * * @param mode Mode to change to. Must be one of the modes returned by the functions in displaymode.h. * @param shouldCapture @c true, if the mode is intended to capture the entire display. * * @return @c true, if a mode change occurred. @c false, otherwise (bad mode or * when attempting to change to the current mode). */ LIBGUI_PUBLIC int DisplayMode_Change(DisplayMode const *mode, int shouldCapture); /** * Gets the current color transfer table. * * @param colors Color transfer. */ LIBGUI_PUBLIC void DisplayMode_GetColorTransfer(DisplayColorTransfer *colors); /** * Sets the color transfer table. * * @param colors Color transfer. */ LIBGUI_PUBLIC void DisplayMode_SetColorTransfer(DisplayColorTransfer const *colors); #ifdef __cplusplus } // extern "C" #endif #endif // LIBGUI_DISPLAYMODE_H doomsday-stable-1.15.7/doomsday/libgui/include/de/Canvas0000664000175000017500000000003012641367671022431 0ustar jaakkojaakko#include "gui/canvas.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/KeyEvent0000664000175000017500000000003412641367671022754 0ustar jaakkojaakko#include "input/keyevent.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/DisplayMode0000664000175000017500000000010112641367671023427 0ustar jaakkojaakko#include "gui/displaymode.h" #include "gui/displaymode_native.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/VertexBuilder0000664000175000017500000000004412641367671024007 0ustar jaakkojaakko#include "graphics/vertexbuilder.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/Drawable0000664000175000017500000000003712641367671022746 0ustar jaakkojaakko#include "graphics/drawable.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/GLFramebuffer0000664000175000017500000000004412641367671023672 0ustar jaakkojaakko#include "graphics/glframebuffer.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/input/0000775000175000017500000000000012641367671022441 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/include/de/input/mouseevent.h0000664000175000017500000000442412641367671025010 0ustar jaakkojaakko/** @file mouseevent.h Input event from a mouse. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_MOUSEEVENT_H #define LIBGUI_MOUSEEVENT_H #include #include #include "../gui/libgui.h" namespace de { /** * Input event from a mouse. @ingroup ui */ class LIBGUI_PUBLIC MouseEvent : public Event { public: enum MotionType { Absolute = 0, Relative = 1, Wheel = 2 }; enum Button { Unknown = -1, Left = 0, Middle = 1, Right = 2, XButton1 = 3, XButton2 = 4 }; enum ButtonState { Released, ///< Released button. Pressed ///< Pressed button. }; enum WheelMotion { FineAngle, Step }; public: MouseEvent(); MouseEvent(MotionType motion, Vector2i const &pos); MouseEvent(WheelMotion wheelMotion, Vector2i const &wheel, Vector2i const &pos); MouseEvent(Button button, ButtonState state, Vector2i const &pos); MotionType motion() const; Vector2i const &pos() const { return _pos; } WheelMotion wheelMotion() const { return _wheelMotion; } Vector2i const &wheel() const { return _wheel; } Button button() const { return _button; } ButtonState state() const { return _state; } void setPos(Vector2i const &p) { _pos = p; } private: Vector2i _pos; WheelMotion _wheelMotion; Vector2i _wheel; Button _button; ButtonState _state; }; } // namespace de #endif // LIBGUI_MOUSEEVENT_H doomsday-stable-1.15.7/doomsday/libgui/include/de/input/keyevent.h0000664000175000017500000000503612641367671024450 0ustar jaakkojaakko/** @file gui/keyevent.h Input event from a keyboard. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_KEYEVENT_H #define LIBGUI_KEYEVENT_H #include "../gui/libgui.h" #include "ddkey.h" #include #include #include namespace de { /** * Input event generated by a keyboard device. @ingroup ui */ class LIBGUI_PUBLIC KeyEvent : public de::Event { public: enum State { Released, ///< Released key. Pressed, ///< Pressed key. Repeat ///< Repeat while held pressed. }; enum Modifier { Shift = 1, Control = 2, Alt = 4, Meta = 8, NoModifiers = 0 }; Q_DECLARE_FLAGS(Modifiers, Modifier) public: KeyEvent(); KeyEvent(State keyState, int qtKeyCode, int ddKeyCode, int nativeKeyCode, de::String const &keyText, Modifiers const &mods = NoModifiers); State state() const; inline int qtKey() const { return _qtKey; } inline int ddKey() const { return _ddKey; } inline int nativeCode() const { return _nativeCode; } inline de::String const &text() const { return _text; } inline Modifiers modifiers() const { return _mods; } bool isModifier() const; /** * Translates a Qt key code to a Doomsday key code (see ddkey.h). * * @param qtKey Qt key code. * @param nativeVirtualKey Native virtual key code. * @param nativeScanCode Native scan code. * * @return DDKEY code. */ static int ddKeyFromQt(int qtKey, int nativeVirtualKey, int nativeScanCode); private: int _qtKey; Modifiers _mods; int _ddKey; int _nativeCode; de::String _text; }; Q_DECLARE_OPERATORS_FOR_FLAGS(KeyEvent::Modifiers) } // namespace de #endif // LIBGUI_KEYEVENT_H doomsday-stable-1.15.7/doomsday/libgui/include/de/input/ddkey.h0000664000175000017500000000517312641367671023720 0ustar jaakkojaakko/** @file ddkey.h DDKEY codes. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_DDKEY_H #define LIBGUI_DDKEY_H /** * @defgroup keyConstants Key Constants * @ingroup gui */ /** * DDKEY codes. Most key data is regular ASCII so key constants correspond to * ASCII codes. Note that these are used when dealing with the physical state * of individual keys; text is entered separately as Unicode. * * @ingroup keyConstants */ enum { DDKEY_TAB = 9, DDKEY_RETURN = 13, DDKEY_ESCAPE = 27, DDKEY_MINUS = 0x2d, DDKEY_EQUALS = 0x3d, DDKEY_BACKSLASH = 0x5c, DDKEY_BACKSPACE = 127, DDKEY_RIGHTARROW = 0x80, DDKEY_LEFTARROW, DDKEY_UPARROW, DDKEY_DOWNARROW, DDKEY_F1, DDKEY_F2, DDKEY_F3, DDKEY_F4, DDKEY_F5, DDKEY_F6, DDKEY_F7, DDKEY_F8, DDKEY_F9, DDKEY_F10, DDKEY_F11, DDKEY_F12, DDKEY_NUMLOCK, DDKEY_CAPSLOCK, DDKEY_SCROLL, DDKEY_NUMPAD7, DDKEY_NUMPAD8, DDKEY_NUMPAD9, DDKEY_NUMPAD4, DDKEY_NUMPAD5, DDKEY_NUMPAD6, DDKEY_NUMPAD1, DDKEY_NUMPAD2, DDKEY_NUMPAD3, DDKEY_NUMPAD0, DDKEY_DECIMAL, DDKEY_PAUSE, DDKEY_RSHIFT, DDKEY_LSHIFT = DDKEY_RSHIFT, DDKEY_RCTRL, DDKEY_LCTRL = DDKEY_RCTRL, DDKEY_RALT, DDKEY_LALT = DDKEY_RALT, DDKEY_INS, DDKEY_DEL, DDKEY_PGUP, DDKEY_PGDN, DDKEY_HOME, DDKEY_END, DDKEY_SUBTRACT, ///< '-' on numeric keypad. DDKEY_ADD, ///< '+' on numeric keypad. DDKEY_PRINT, DDKEY_ENTER, ///< on the numeric keypad. DDKEY_DIVIDE, ///< '/' on numeric keypad. DDKEY_MULTIPLY, ///< '*' on the numeric keypad. DDKEY_SECTION, ///< § DDKEY_WINMENU, ///< Windows-specific context menu key. DD_HIGHEST_KEYCODE }; #endif // LIBGUI_DDKEY_H doomsday-stable-1.15.7/doomsday/libgui/include/de/input/keyeventsource.h0000664000175000017500000000254512641367671025673 0ustar jaakkojaakko/** @file keyeventsource.h Object that produces keyboard input events. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_KEYEVENTSOURCE_H #define LIBGUI_KEYEVENTSOURCE_H #include "../gui/libgui.h" #include "keyevent.h" #include #include namespace de { /** * Object that produces keyboard events. */ class LIBGUI_PUBLIC KeyEventSource { public: DENG2_DEFINE_AUDIENCE2(KeyEvent, void keyEvent(KeyEvent const &)) public: KeyEventSource(); virtual ~KeyEventSource() {} private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_KEYEVENTSOURCE_H doomsday-stable-1.15.7/doomsday/libgui/include/de/input/mouseeventsource.h0000664000175000017500000000300112641367671026217 0ustar jaakkojaakko/** @file mouseeventsource.h Object that produces mouse events. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_MOUSEEVENTSOURCE_H #define LIBGUI_MOUSEEVENTSOURCE_H #include "../gui/libgui.h" #include "mouseevent.h" #include #include namespace de { /** * Object that produces mouse events. */ class LIBGUI_PUBLIC MouseEventSource { public: enum State { Untrapped, Trapped }; DENG2_DEFINE_AUDIENCE2(MouseStateChange, void mouseStateChanged(State)) DENG2_DEFINE_AUDIENCE2(MouseEvent, void mouseEvent(MouseEvent const &)) public: MouseEventSource(); virtual ~MouseEventSource() {} private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_MOUSEEVENTSOURCE_H doomsday-stable-1.15.7/doomsday/libgui/include/de/Sound0000664000175000017500000000003212641367671022310 0ustar jaakkojaakko#include "audio/sound.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/GLTarget0000664000175000017500000000003712641367671022676 0ustar jaakkojaakko#include "graphics/gltarget.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/ImageBank0000664000175000017500000000004012641367671023035 0ustar jaakkojaakko#include "graphics/imagebank.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/KdTreeAtlasAllocator0000664000175000017500000000005312641367671025227 0ustar jaakkojaakko#include "graphics/kdtreeatlasallocator.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/MouseEventSource0000664000175000017500000000004412641367671024476 0ustar jaakkojaakko#include "input/mouseeventsource.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/GLState0000664000175000017500000000003612641367671022527 0ustar jaakkojaakko#include "graphics/glstate.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/ModelDrawable0000664000175000017500000000004512641367671023726 0ustar jaakkojaakko#include "graphics/modeldrawable.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/MouseEvent0000664000175000017500000000003612641367671023316 0ustar jaakkojaakko#include "input/mouseevent.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/GuiApp0000664000175000017500000000003012641367671022403 0ustar jaakkojaakko#include "gui/guiapp.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/GLUniform0000664000175000017500000000004012641367671023061 0ustar jaakkojaakko#include "graphics/gluniform.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/text/0000775000175000017500000000000012641367671022266 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/include/de/text/fontbank.h0000664000175000017500000000360612641367671024246 0ustar jaakkojaakko/** @file fontbank.h Font bank. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_FONTBANK_H #define LIBGUI_FONTBANK_H #include #include #include "../Font" #include "../gui/libgui.h" namespace de { /** * Bank containing fonts. */ class LIBGUI_PUBLIC FontBank : public InfoBank { public: FontBank(); /** * Creates a number of fonts based on information in an Info document. * The file is parsed first. * * @param file File with Info source containing font definitions. */ void addFromInfo(File const &file); /** * Finds a specific font. * * @param path Identifier of the font. * * @return Font instance. */ Font const &font(DotPath const &path) const; /** * Sets a factor applied to all font sizes when loading the back. * * @param sizeFactor Size factor. */ void setFontSizeFactor(float sizeFactor); protected: virtual ISource *newSourceFromInfo(String const &id); virtual IData *loadFromSource(ISource &source); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_FONTBANK_H doomsday-stable-1.15.7/doomsday/libgui/include/de/text/font.h0000664000175000017500000002607012641367671023412 0ustar jaakkojaakko/** @file font.h Font with metrics. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_FONT_H #define LIBGUI_FONT_H #include #include #include #include #include #include #include #include #include #include #include "../gui/libgui.h" namespace de { class NativeFont; /** * Font with metrics. */ class LIBGUI_PUBLIC Font { public: typedef QVector TabStops; /** * Rich formatting instructions for a string of plain text. * * The formatting instructions are composed of a sequence of ranges that * specify various modifications to the original font. It is important to * note that a RichFormat instance always needs to be set up for a specific * source text string. Also, RichFormat is out-of-band data: when operating * on a piece of rich text (using the methods of Font), the formatting is * always provided in parallel to the plain version of the text. * * Use RichFormat::fromPlainText() to set up a RichFormat for a string of * plain text. * * Use RichFormat::initFromStyledText() to set up a RichFormat for a string * of text that contains style information (as escape sequences that start * with the Esc ASCII code 0x1b). * * @par Indentation and tab stops * * The escape sequences ">" and "T" can be used to define indentation * points and tab stops for visual alignment of text. * * The indentation mark ">" determines the width at which the following * lines are left-indented. It does not affect the line the mark itself is * on. Newlines (\n) do not reset the indentation. If multiple marks are * used in the same text content, each mark is applied cumulatively on the * previous indentation. Also note that placing an indentation mark at the * beginning of a line or immediately after a new line does nothing (a * width of zero does not increase the indentation). * * Tab stops are used to divide text on a line into smaller segments that * can then be freely moved when drawn. All segments with the same tab stop * number are aligned which the same stops at other lines of the content, * providing there is enough horizontal space available. If tab stop * alignment runs out of space, width of the rightmost segments is shrunk * slightly to fit. * * The escape sequence "Ta" sets the tab stop to zero for the following * text. The default tab stop is zero. "Tb" sets the tab stop to 1, "Tc" to * 2, etc. */ class LIBGUI_PUBLIC RichFormat { public: enum ContentStyle { NormalStyle = 0, MajorStyle = 1, MinorStyle = 2, MetaStyle = 3, MajorMetaStyle = 4, MinorMetaStyle = 5, AuxMetaStyle = 6 }; enum Weight { OriginalWeight = -1, Normal = 0, Light = 1, Bold = 2 }; enum Style { OriginalStyle = -1, Regular = 0, Italic = 1, Monospace = 2 }; enum Color { OriginalColor = -1, NormalColor = 0, HighlightColor = 1, DimmedColor = 2, AccentColor = 3, DimAccentColor = 4, AltAccentColor = 5 }; /** * Interface for an object providing style information: fonts and * colors. */ class LIBGUI_PUBLIC IStyle { public: typedef Vector4ub Color; virtual ~IStyle() {} /** * Returns a color from the style's palette. * @param index Color index (see RichFormat::Color). * @return Color values (RGBA 0...255). */ virtual Color richStyleColor(int index) const = 0; virtual void richStyleFormat(int contentStyle, float &sizeFactor, Weight &fontWeight, Style &fontStyle, int &colorIndex) const = 0; /** * Returns a font to be used with a particular style. * @param fontStyle Style. * @return @c NULL to use the default font. */ virtual Font const *richStyleFont(Style fontStyle) const { DENG2_UNUSED(fontStyle); return NULL; // Use default. } }; /** * Reference to a (portion) of an existing RichFormat instance. */ class LIBGUI_PUBLIC Ref { public: Ref(Ref const &ref); Ref(Ref const &ref, Rangei const &subSpan); Ref(RichFormat const &richFormat); Ref(RichFormat const &richFormat, Rangei const &subSpan); Ref subRef(Rangei const &subSpan) const; /// Returns the original referred RichFormat instance. RichFormat const &format() const; int rangeCount() const; Rangei range(int index) const; Rangei rangeIndices() const { return _indices; } private: void updateIndices(); RichFormat const &_ref; Rangei _span; Rangei _indices; ///< Applicable indices in the referred format's ranges list. }; public: RichFormat(); RichFormat(IStyle const &style); RichFormat(RichFormat const &other); RichFormat &operator = (RichFormat const &other); void clear(); bool hasStyle() const; void setStyle(IStyle const &style); IStyle const &style() const; /** * Constructs a RichFormat that specifies no formatting instructions. * * @param plainText Plain text. * * @return RichFormat instance with a single range that uses the * default formatting. */ static RichFormat fromPlainText(String const &plainText); /** * Initializes this RichFormat instance with the styles found in the * provided styled text (using escape sequences). * * @param styledText Text with style markup. * * @return Corresponding plain text for use with the methods of Font. */ String initFromStyledText(String const &styledText); /** * Clips this RichFormat so that it covers only the specified range. * The indices are translated to be relative to the provided range. * * @param range Target range for clipping. * * @return RichFormat with only those ranges covering @a range. */ Ref subRange(Rangei const &range) const; TabStops const &tabStops() const; int tabStopXWidth(int stop) const; /** * Iterates the rich format ranges of a RichFormat. * * @note Iterator::next() must be called at least once after * constructing the instance to move the iterator onto the first range. */ struct LIBGUI_PUBLIC Iterator { Ref format; int index; public: Iterator(Ref const &ref); int size() const; bool hasNext() const; void next(); /// Determines if all the style parameters are the same as the default ones. bool isDefault() const; Rangei range() const; float sizeFactor() const; Weight weight() const; Style style() const; int colorIndex() const; IStyle::Color color() const; bool markIndent() const; bool resetIndent() const; int tabStop() const; bool isTabless() const; ///< Tabstop < 0. }; private: DENG2_PRIVATE(d) }; typedef RichFormat::Ref RichFormatRef; public: Font(); Font(Font const &other); //Font(NativeFont const &font); Font(QFont const &font); //QFont toQFont() const; /** * Determines the size of the given line of text, i.e., how large an area * is covered by the glyphs. (0,0) is at the baseline, left edge of the * line. The rectangle may extend into negative coordinates. * * @param textLine Text to measure. * * @return Rectangle covered by the text. */ Rectanglei measure(String const &textLine) const; /** * Determines the size of the given line of rich text, i.e., how large an * area is covered by the glyphs. (0,0) is at the baseline, left edge of * the line. The rectangle may extend into negative coordinates. * * @param textLine Text to measure. * @param format Rich formatting for @a textLine. * * @return Rectangle covered by the text. */ Rectanglei measure(String const &textLine, RichFormatRef const &format) const; /** * Returns the advance width of a line of text. This may not be the same as * the width of the rectangle returned by measure(). * * @param textLine Text to measure. * * @return Width of the line of text (including non-visible parts like * whitespace). */ int advanceWidth(String const &textLine) const; int advanceWidth(String const &textLine, RichFormatRef const &format) const; /** * Rasterizes a line of text onto a 32-bit RGBA image. * * @param textLine Text to rasterize. * @param foreground Text foreground color. * @param background Background color. * * @return Image containing the rasterized text. */ QImage rasterize(String const &textLine, Vector4ub const &foreground = Vector4ub(255, 255, 255, 255), Vector4ub const &background = Vector4ub(255, 255, 255, 0)) const; QImage rasterize(String const &textLine, RichFormatRef const &format, Vector4ub const &foreground = Vector4ub(255, 255, 255, 255), Vector4ub const &background = Vector4ub(255, 255, 255, 0)) const; Rule const &height() const; Rule const &ascent() const; Rule const &descent() const; Rule const &lineSpacing() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_FONT_H doomsday-stable-1.15.7/doomsday/libgui/include/de/text/nativefont.h0000664000175000017500000001045112641367671024615 0ustar jaakkojaakko/** @file nativefont.h Abstraction of a native font. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_NATIVEFONT_H #define LIBGUI_NATIVEFONT_H #include "../gui/libgui.h" #include #include #include #include #include namespace de { /** * Provides the means to access the platform's native fonts, measure the bounds of a * string of text, and draw the text onto an Image. This is an abstract base class for * concrete implementations of native fonts. * * @ingroup gl */ class LIBGUI_PUBLIC NativeFont : public Asset { public: enum Style { Regular, Italic }; enum Weight { UltraLight = 0, Light = 25, Normal = 50, Bold = 75, Black = 100 }; struct Spec { Style style; dint weight; Spec(Style s = Regular, dint w = Normal) : style(s), weight(w) {} bool operator < (Spec const &other) const { // QMap key order if(weight < other.weight) return true; if(weight > other.weight) return false; return (style < other.style); } }; typedef QMap StyleMapping; ///< Spec => native font name /** * Defines a mapping from font family name plus style/weight to an actual platform * font. * * @param family Native font family name. * @param mapping Mapping of styles to native font names. */ static void defineMapping(String const &family, StyleMapping const &mapping); public: NativeFont(String const &family = ""); NativeFont(NativeFont const &other); void setFamily(String const &family); void setSize(dfloat size); void setStyle(Style style); void setWeight(dint weight); String family() const; dfloat size() const; Style style() const; dint weight() const; /** * Determines the native font name based on style mappings. */ String nativeFontName() const; int ascent() const; int descent() const; int height() const; int lineSpacing() const; /** * Measures the extents of a line of text. * * @param text Text line. * * @return Boundaries. */ Rectanglei measure(String const &text) const; int width(String const &text) const; /** * Draws a line of text using the font into an image. * * @param text Text line. * @param foreground Foreground/text color. * @param background Background color. * * @return Image of the text, with the same dimensions as returned by measure(). */ QImage rasterize(String const &text, Vector4ub const &foreground, Vector4ub const &background) const; protected: NativeFont &operator = (NativeFont const &other); /** * Called when the font is needed to be used but it isn't marked Ready. */ virtual void commit() const = 0; virtual int nativeFontAscent() const = 0; virtual int nativeFontDescent() const = 0; virtual int nativeFontHeight() const = 0; virtual int nativeFontLineSpacing() const = 0; virtual int nativeFontWidth(String const &text) const = 0; virtual Rectanglei nativeFontMeasure(String const &text) const = 0; virtual QImage nativeFontRasterize(String const &text, Vector4ub const &foreground, Vector4ub const &background) const = 0; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_NATIVEFONT_H doomsday-stable-1.15.7/doomsday/libgui/include/de/TextureBank0000664000175000017500000000004212641367671023455 0ustar jaakkojaakko#include "graphics/texturebank.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/AtlasTexture0000664000175000017500000000004312641367671023647 0ustar jaakkojaakko#include "graphics/atlastexture.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/GLPixelFormat0000664000175000017500000000004412641367671023700 0ustar jaakkojaakko#include "graphics/glpixelformat.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/HeightMap0000664000175000017500000000004012641367671023065 0ustar jaakkojaakko#include "graphics/heightmap.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/GLShader0000664000175000017500000000003712641367671022656 0ustar jaakkojaakko#include "graphics/glshader.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/Font0000664000175000017500000000003012641367671022124 0ustar jaakkojaakko#include "text/font.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/GLInfo0000664000175000017500000000003512641367671022341 0ustar jaakkojaakko#include "graphics/glinfo.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/Waveform0000664000175000017500000000003512641367671023011 0ustar jaakkojaakko#include "audio/waveform.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/NativeFont0000664000175000017500000000003612641367671023301 0ustar jaakkojaakko#include "text/nativefont.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/WaveformBank0000664000175000017500000000004112641367671023602 0ustar jaakkojaakko#include "audio/waveformbank.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/GLShaderBank0000664000175000017500000000004312641367671023447 0ustar jaakkojaakko#include "graphics/glshaderbank.h" doomsday-stable-1.15.7/doomsday/libgui/include/de/RowAtlasAllocator0000664000175000017500000000005012641367671024615 0ustar jaakkojaakko#include "graphics/rowatlasallocator.h" doomsday-stable-1.15.7/doomsday/libgui/gui.doxy0000664000175000017500000000207312641367671020762 0ustar jaakkojaakko# Public API documentation for libgui @INCLUDE = ../doomsday.doxy PROJECT_NAME = "libgui" PROJECT_NUMBER = 1.15.7 PROJECT_BRIEF = "Graphics, Audio and Input Library" OUTPUT_DIRECTORY = ../apidoc/gui/ INPUT = include src FILE_PATTERNS = * EXCLUDE_PATTERNS = .DS_Store PREDEFINED = __cplusplus __LIBGUI__ \ "DENG2_PIMPL(ClassName)=typedef ClassName Public; struct ClassName::Instance : public de::Private" \ "DENG2_PIMPL_NOREF(C)=struct C::Instance : public de::IPrivate" \ "DENG2_PRIVATE(Var)=struct Instance; Instance *Var;" \ "DENG2_ERROR(N)=class N : public de::Error {};" \ "DENG2_SUB_ERROR(B,N)=class N : public B {};" INCLUDED_BY_GRAPH = NO COLLABORATION_GRAPH = NO REFERENCED_BY_RELATION = NO OPTIMIZE_OUTPUT_FOR_C = NO MACRO_EXPANSION = YES EXPAND_ONLY_PREDEF = NO INTERNAL_DOCS = NO doomsday-stable-1.15.7/doomsday/libgui/src/0000775000175000017500000000000012641367671020056 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/src/graphics/0000775000175000017500000000000012641367671021656 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/src/graphics/glprogram.cpp0000664000175000017500000002647712641367671024374 0ustar jaakkojaakko/** @file glprogram.cpp GL shader program. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GLProgram" #include "de/GLUniform" #include "de/GLBuffer" #include "de/GLShader" #include "de/GLTexture" #include "de/GuiApp" #include "de/graphics/opengl.h" #include #include #include #include namespace de { /// The currently used GLProgram. static GLProgram const *currentProgram = 0; using namespace internal; DENG2_PIMPL(GLProgram) , DENG2_OBSERVES(GLUniform, ValueChange) , DENG2_OBSERVES(GLUniform, Deletion) , DENG2_OBSERVES(GuiApp, GLContextChange) { typedef QSet Uniforms; typedef QList UniformList; typedef QSet Shaders; Uniforms bound; Uniforms changed; UniformList textures; bool texturesChanged; int attribLocation[AttribSpec::NUM_SEMANTICS]; ///< Where each attribute is bound. GLuint name; Shaders shaders; bool inUse; bool needRebuild; Instance(Public *i) : Base(i) , texturesChanged(false) , name(0) , inUse(false) , needRebuild(false) { //DENG2_GUI_APP->audienceForGLContextChange += this; } ~Instance() { //DENG2_GUI_APP->audienceForGLContextChange -= this; release(); } void appGLContextChanged() { /* if(name && !needRebuild) { qDebug() << "rebuild program" << name << "before use"; self.rebuildBeforeNextUse(); } */ } void alloc() { if(!name) { name = glCreateProgram(); if(!name) { throw AllocError("GLProgram::alloc", "Failed to create program"); } } } void releaseButRetainBindings() { self.setState(NotReady); detachAllShaders(); if(name) { glDeleteProgram(name); name = 0; } } void release() { unbindAll(); releaseButRetainBindings(); } void attach(GLShader const *shader) { DENG2_ASSERT(shader->isReady()); alloc(); glAttachShader(name, shader->glName()); LIBGUI_ASSERT_GL_OK(); shaders.insert(holdRef(shader)); } void detach(GLShader const *shader) { if(shader->isReady()) { glDetachShader(name, shader->glName()); } shaders.remove(shader); shader->release(); } void detachAllShaders() { foreach(GLShader const *shader, shaders) { detach(shader); } shaders.clear(); } void unbindAll() { foreach(GLUniform const *u, bound) { u->audienceForValueChange() -= this; u->audienceForDeletion() -= this; } texturesChanged = false; bound.clear(); textures.clear(); changed.clear(); } /** * Bind all known vertex attributes to the indices used by GLBuffer. The * program is automatically (re)linked after binding the vertex attributes, * if there are already shaders attached. */ void bindVertexAttribs() { alloc(); if(!shaders.isEmpty()) { link(); } static struct { AttribSpec::Semantic semantic; char const *varName; } const names[] = { { AttribSpec::Position, "aVertex" }, { AttribSpec::TexCoord0, "aUV" }, { AttribSpec::TexCoord1, "aUV2" }, { AttribSpec::TexCoord2, "aUV3" }, { AttribSpec::TexCoord3, "aUV4" }, { AttribSpec::TexBounds0, "aBounds" }, { AttribSpec::TexBounds1, "aBounds2" }, { AttribSpec::TexBounds2, "aBounds3" }, { AttribSpec::TexBounds3, "aBounds4" }, { AttribSpec::Color, "aColor" }, { AttribSpec::Normal, "aNormal" }, { AttribSpec::Tangent, "aTangent" }, { AttribSpec::Bitangent, "aBitangent" }, { AttribSpec::BoneIDs, "aBoneIDs" }, { AttribSpec::BoneWeights, "aBoneWeights" }, { AttribSpec::InstanceMatrix, "aInstanceMatrix" }, // x4 { AttribSpec::InstanceColor, "aInstanceColor" } }; // Clear the locations first. for(uint i = 0; i < AttribSpec::NUM_SEMANTICS; ++i) { attribLocation[i] = -1; // not in use } // Look up where the attributes ended up being linked. for(uint i = 0; i < sizeof(names)/sizeof(names[0]); ++i) { attribLocation[names[i].semantic] = glGetAttribLocation(name, names[i].varName); } } void link() { DENG2_ASSERT(name != 0); glLinkProgram(name); // Was linking successful? GLint ok; glGetProgramiv(name, GL_LINK_STATUS, &ok); if(!ok) { dint32 logSize; dint32 count; glGetProgramiv(name, GL_INFO_LOG_LENGTH, &logSize); Block log(logSize); glGetProgramInfoLog(name, logSize, &count, reinterpret_cast(log.data())); throw LinkerError("GLProgram::link", "Linking failed:\n" + log); } } void markAllBoundUniformsChanged() { foreach(GLUniform const *u, bound) { changed.insert(u); } } void updateUniforms() { if(changed.isEmpty()) return; // Apply the uniform values in this program. foreach(GLUniform const *u, changed) { if(u->type() != GLUniform::Sampler2D) { u->applyInProgram(self); } } if(texturesChanged) { // Update the sampler uniforms. for(int unit = 0; unit < textures.size(); ++unit) { int loc = self.glUniformLocation(textures[unit]->name().latin1()); if(loc >= 0) { glUniform1i(loc, unit); LIBGUI_ASSERT_GL_OK(); } } texturesChanged = false; } changed.clear(); } void bindTextures() { // Update the sampler uniforms. for(int unit = textures.size() - 1; unit >= 0; --unit) { GLTexture const *tex = *textures[unit]; if(tex) { tex->glBindToUnit(unit); } } } void rebuild() { //qDebug() << "Rebuilding GL program" << name; if(name) { glDeleteProgram(name); name = 0; } alloc(); foreach(GLShader const *shader, shaders) { glAttachShader(name, shader->glName()); LIBGUI_ASSERT_GL_OK(); } bindVertexAttribs(); markAllBoundUniformsChanged(); } void uniformValueChanged(GLUniform &uniform) { changed.insert(&uniform); } void uniformDeleted(GLUniform &uniform) { self.unbind(uniform); } }; GLProgram::GLProgram() : d(new Instance(this)) {} void GLProgram::clear() { d->release(); } GLProgram &GLProgram::build(GLShader const *vertexShader, GLShader const *fragmentShader) { DENG2_ASSERT(vertexShader != 0); DENG2_ASSERT(vertexShader->isReady()); DENG2_ASSERT(vertexShader->type() == GLShader::Vertex); DENG2_ASSERT(fragmentShader != 0); DENG2_ASSERT(fragmentShader->isReady()); DENG2_ASSERT(fragmentShader->type() == GLShader::Fragment); d->releaseButRetainBindings(); d->attach(vertexShader); d->attach(fragmentShader); d->bindVertexAttribs(); d->markAllBoundUniformsChanged(); setState(Ready); return *this; } GLProgram &GLProgram::build(IByteArray const &vertexShaderSource, IByteArray const &fragmentShaderSource) { return build(refless(new GLShader(GLShader::Vertex, vertexShaderSource)), refless(new GLShader(GLShader::Fragment, fragmentShaderSource))); } void GLProgram::rebuildBeforeNextUse() { d->needRebuild = true; } void GLProgram::rebuild() { d->rebuild(); } GLProgram &GLProgram::operator << (GLUniform const &uniform) { return bind(uniform); } GLProgram &GLProgram::bind(GLUniform const &uniform) { if(!d->bound.contains(&uniform)) { d->bound.insert(&uniform); d->changed.insert(&uniform); uniform.audienceForValueChange() += d; uniform.audienceForDeletion() += d; if(uniform.type() == GLUniform::Sampler2D) { d->textures << &uniform; d->texturesChanged = true; } } return *this; } GLProgram &GLProgram::unbind(GLUniform const &uniform) { if(d->bound.contains(&uniform)) { d->bound.remove(&uniform); d->changed.remove(&uniform); uniform.audienceForValueChange() -= d.get(); uniform.audienceForDeletion() -= d.get(); if(uniform.type() == GLUniform::Sampler2D) { d->textures.removeOne(&uniform); d->texturesChanged = true; } } return *this; } void GLProgram::beginUse() const { LIBGUI_ASSERT_GL_OK(); DENG2_ASSERT_IN_MAIN_THREAD(); DENG2_ASSERT(QGLContext::currentContext() != 0); DENG2_ASSERT(isReady()); DENG2_ASSERT(!d->inUse); if(d->needRebuild) { d->needRebuild = false; const_cast(this)->rebuild(); } DENG2_ASSERT(glIsProgram(d->name)); d->inUse = true; currentProgram = this; // The program is now ready for use. glUseProgram(d->name); LIBGUI_ASSERT_GL_OK(); d->updateUniforms(); d->bindTextures(); LIBGUI_ASSERT_GL_OK(); } void GLProgram::endUse() const { DENG2_ASSERT(d->inUse); d->inUse = false; currentProgram = 0; glUseProgram(0); } GLProgram const *GLProgram::programInUse() // static { return currentProgram; } GLuint GLProgram::glName() const { return d->name; } int GLProgram::glUniformLocation(char const *uniformName) const { GLint loc = glGetUniformLocation(d->name, uniformName); if(loc < 0) { LOG_AS("GLProgram"); LOGDEV_GL_WARNING("Could not find uniform '%s'") << uniformName; } return loc; } int GLProgram::attributeLocation(AttribSpec::Semantic semantic) const { DENG2_ASSERT(semantic >= 0); DENG2_ASSERT(semantic < AttribSpec::NUM_SEMANTICS); return d->attribLocation[semantic]; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/gltarget.cpp0000664000175000017500000004742312641367671024205 0ustar jaakkojaakko/** @file gltarget.cpp GL render target. * * Implementation does not use QGLFrameBufferObject because it does not allow * attaching manually created textures. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GLTarget" #include "de/GLTexture" #include "de/GLState" #include "de/GLInfo" #include "de/CanvasWindow" #include namespace de { static Vector2ui const nullSize; DENG2_PIMPL(GLTarget), DENG2_OBSERVES(Asset, Deletion) { enum AttachmentId { ColorBuffer, DepthBuffer, StencilBuffer, MAX_ATTACHMENTS }; static AttachmentId attachmentToId(GLenum atc) { switch(atc) { case GL_COLOR_ATTACHMENT0: return ColorBuffer; case GL_DEPTH_ATTACHMENT: return DepthBuffer; case GL_STENCIL_ATTACHMENT: return StencilBuffer; case GL_DEPTH_STENCIL_ATTACHMENT: return DepthBuffer; default: DENG2_ASSERT(false); break; } return ColorBuffer; // should not be reached } static GLenum flagsToGLAttachment(Flags const &flags) { DENG2_ASSERT(!flags.testFlag(ColorDepth)); DENG2_ASSERT(!flags.testFlag(ColorDepthStencil)); return flags == Color? GL_COLOR_ATTACHMENT0 : flags == Depth? GL_DEPTH_ATTACHMENT : flags == Stencil? GL_STENCIL_ATTACHMENT : GL_DEPTH_STENCIL_ATTACHMENT; } GLuint fbo; GLuint renderBufs[MAX_ATTACHMENTS]; GLTexture *bufTextures[MAX_ATTACHMENTS]; Flags flags; Flags textureAttachment; ///< Where to attach @a texture. GLTexture *texture; Vector2ui size; Vector4f clearColor; Rectangleui activeRect; ///< Initially null. int sampleCount; GLTarget const *proxy; Instance(Public *i) : Base(i) , fbo(0) , flags(DefaultFlags) , textureAttachment(NoAttachments) , texture(0) , sampleCount(0) , proxy(0) { zap(renderBufs); zap(bufTextures); } Instance(Public *i, Flags const &texAttachment, GLTexture &colorTexture, Flags const &otherAtm) : Base(i) , fbo(0) , flags(texAttachment | otherAtm) , textureAttachment(texAttachment) , texture(&colorTexture) , size(colorTexture.size()) , sampleCount(0) , proxy(0) { zap(renderBufs); zap(bufTextures); } Instance(Public *i, Vector2ui const &targetSize, Flags const &fboFlags) : Base(i) , fbo(0) , flags(fboFlags) , textureAttachment(NoAttachments) , texture(0) , size(targetSize) , sampleCount(0) , proxy(0) { zap(renderBufs); zap(bufTextures); } ~Instance() { release(); } bool isDefault() const { return !texture && size == nullSize; } GLTexture *bufferTexture(Flags const &flags) const { if(flags == Color) { return bufTextures[ColorBuffer]; } if(flags == DepthStencil || flags == Depth) { return bufTextures[DepthBuffer]; } if(flags == Stencil) { return bufTextures[StencilBuffer]; } return 0; } void allocFBO() { if(isDefault() || fbo) return; glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); LOG_GL_XVERBOSE("Creating FBO %i") << fbo; } void attachTexture(GLTexture &tex, GLenum attachment, int level = 0) { DENG2_ASSERT(tex.isReady()); LOG_GL_XVERBOSE("FBO %i: glTex %i (level %i) => attachment %i") << fbo << tex.glName() << level << attachmentToId(attachment); glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, tex.glName(), level); LIBGUI_ASSERT_GL_OK(); bufTextures[attachmentToId(attachment)] = &tex; } void attachRenderbuffer(AttachmentId id, GLenum type, GLenum attachment) { DENG2_ASSERT(size != Vector2ui(0, 0)); glGenRenderbuffers(1, &renderBufs[id]); glBindRenderbuffer(GL_RENDERBUFFER, renderBufs[id]); if(sampleCount > 1) { #ifdef GL_NV_framebuffer_multisample_coverage if(GLInfo::extensions().NV_framebuffer_multisample_coverage) { LOG_GL_VERBOSE("FBO %i: renderbuffer %ix%i is multisampled with %i CSAA samples => attachment %i") << fbo << size.x << size.y << sampleCount << attachmentToId(attachment); glRenderbufferStorageMultisampleCoverageNV(GL_RENDERBUFFER, 8, sampleCount, type, size.x, size.y); } else #endif { LOG_GL_VERBOSE("FBO %i: renderbuffer %ix%i is multisampled with %i samples => attachment %i") << fbo << size.x << size.y << sampleCount << attachmentToId(attachment); DENG2_ASSERT(GLInfo::extensions().EXT_framebuffer_multisample); glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, sampleCount, type, size.x, size.y); } } else { glRenderbufferStorage(GL_RENDERBUFFER, type, size.x, size.y); } glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, renderBufs[id]); LIBGUI_ASSERT_GL_OK(); } void alloc() { allocFBO(); if(texture) { // The texture's attachment point must be unambiguously defined. DENG2_ASSERT(textureAttachment == Color || textureAttachment == Depth || textureAttachment == Stencil || textureAttachment == DepthStencil); attachTexture(*texture, textureAttachment == Color? GL_COLOR_ATTACHMENT0 : textureAttachment == Depth? GL_DEPTH_ATTACHMENT : textureAttachment == Stencil? GL_STENCIL_ATTACHMENT : GL_DEPTH_STENCIL_ATTACHMENT); } if(size != nullSize) // A non-default target: size must be specified. { allocRenderBuffers(); } validate(); } void allocRenderBuffers() { DENG2_ASSERT(size != nullSize); // Fill in all the other requested attachments. if(flags.testFlag(Color) && !textureAttachment.testFlag(Color)) { /// @todo Note that for GLES, GL_RGBA8 is not supported (without an extension). LOG_GL_VERBOSE("FBO %i: color renderbuffer %s") << fbo << size.asText(); attachRenderbuffer(ColorBuffer, GL_RGBA8, GL_COLOR_ATTACHMENT0); } if( flags.testFlag(DepthStencil) && !flags.testFlag(SeparateDepthAndStencil) && (!texture || textureAttachment == Color)) { // We can use a combined depth/stencil buffer. LOG_GL_VERBOSE("FBO %i: depth+stencil renderbuffer %s") << fbo << size.asText(); attachRenderbuffer(DepthBuffer, GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT); } else { // Separate depth and stencil, then. if(flags.testFlag(Depth) && !textureAttachment.testFlag(Depth)) { LOG_GL_VERBOSE("FBO %i: depth renderbuffer %s") << fbo << size.asText(); attachRenderbuffer(DepthBuffer, GL_DEPTH_COMPONENT, GL_DEPTH_ATTACHMENT); } if(flags.testFlag(Stencil) && !textureAttachment.testFlag(Stencil)) { LOG_GL_VERBOSE("FBO %i: stencil renderbuffer %s") << fbo << size.asText(); attachRenderbuffer(StencilBuffer, GL_STENCIL_INDEX, GL_STENCIL_ATTACHMENT); } } glBindRenderbuffer(GL_RENDERBUFFER, 0); } void releaseRenderBuffers() { glDeleteRenderbuffers(MAX_ATTACHMENTS, renderBufs); zap(renderBufs); zap(bufTextures); } void release() { self.setState(NotReady); if(fbo) { releaseRenderBuffers(); glDeleteFramebuffers(1, &fbo); fbo = 0; } zap(bufTextures); texture = 0; size = nullSize; } void releaseAndReset() { release(); textureAttachment = NoAttachments; flags = NoAttachments; sampleCount = 0; proxy = 0; } void resizeRenderBuffers(Size const &newSize) { size = newSize; releaseRenderBuffers(); allocRenderBuffers(); } void replace(GLenum attachment, GLTexture &newTexture) { DENG2_ASSERT(self.isReady()); // must already be inited DENG2_ASSERT(bufTextures[attachmentToId(attachment)] != 0); // must have an attachment already glBindFramebuffer(GL_FRAMEBUFFER, fbo); attachTexture(newTexture, attachment); // Restore previous target. GLState::current().target().glBind(); } void validate() { if(isDefault()) { self.setState(Ready); return; } DENG2_ASSERT(fbo != 0); glBindFramebuffer(GL_FRAMEBUFFER, fbo); GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); glBindFramebuffer(GL_FRAMEBUFFER, 0); GLState::considerNativeStateUndefined(); // state was manually changed if(status != GL_FRAMEBUFFER_COMPLETE) { releaseAndReset(); throw ConfigError("GLTarget::validate", status == GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT? "Incomplete attachments" : status == GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS? "Mismatch with dimensions" : status == GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT? "No images attached" : QString("Unsupported (0x%1)").arg(status, 0, 16)); } self.setState(Ready); } void assetBeingDeleted(Asset &asset) { if(texture == &asset) { release(); } } void updateFromProxy() { if(!proxy) return; /// @todo Ensure this only occurs iff the target contents have changed. -jk #ifdef _DEBUG if(!flags.testFlag(Changed)) { //qDebug() << "GLTarget: " << fbo << "being updated from proxy without Changed flag (!)"; } #endif //if(flags.testFlag(Changed)) { proxy->blit(self, ColorDepth); flags &= ~Changed; } } }; GLTarget::GLTarget() : d(new Instance(this)) { setState(Ready); } GLTarget::GLTarget(GLTexture &colorTarget, Flags const &otherAttachments) : d(new Instance(this, Color, colorTarget, otherAttachments)) { LOG_AS("GLTarget"); d->alloc(); } GLTarget::GLTarget(Flags const &attachment, GLTexture &texture, Flags const &otherAttachments) : d(new Instance(this, attachment, texture, otherAttachments)) { LOG_AS("GLTarget"); d->alloc(); } GLTarget::GLTarget(Vector2ui const &size, Flags const &flags) : d(new Instance(this, size, flags)) { LOG_AS("GLTarget"); d->alloc(); } GLTarget::Flags GLTarget::flags() const { return d->flags; } void GLTarget::markAsChanged() { d->flags |= Changed; } void GLTarget::configure() { LOG_AS("GLTarget"); d->releaseAndReset(); setState(Ready); } void GLTarget::configure(Vector2ui const &size, Flags const &flags, int sampleCount) { LOG_AS("GLTarget"); d->releaseAndReset(); d->flags = flags; d->size = size; d->sampleCount = (sampleCount > 1? sampleCount : 0); d->allocFBO(); d->allocRenderBuffers(); d->validate(); } void GLTarget::configure(GLTexture *colorTex, GLTexture *depthStencilTex) { DENG2_ASSERT(colorTex || depthStencilTex); LOG_AS("GLTarget"); d->releaseAndReset(); d->flags = ColorDepthStencil; d->size = (colorTex? colorTex->size() : depthStencilTex->size()); d->allocFBO(); // The color attachment. if(colorTex) { DENG2_ASSERT(colorTex->isReady()); DENG2_ASSERT(d->size == colorTex->size()); d->attachTexture(*colorTex, GL_COLOR_ATTACHMENT0); } else { d->attachRenderbuffer(Instance::ColorBuffer, GL_RGBA8, GL_COLOR_ATTACHMENT0); } // The depth attachment. if(depthStencilTex) { DENG2_ASSERT(depthStencilTex->isReady()); DENG2_ASSERT(d->size == depthStencilTex->size()); d->attachTexture(*depthStencilTex, GL_DEPTH_STENCIL_ATTACHMENT); } else { d->attachRenderbuffer(Instance::DepthBuffer, GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT); } d->validate(); } void GLTarget::configure(Flags const &attachment, GLTexture &texture, Flags const &otherAttachments) { LOG_AS("GLTarget"); d->releaseAndReset(); // Set new configuration. d->texture = &texture; d->textureAttachment = attachment; d->flags = attachment | otherAttachments; d->size = texture.size(); d->alloc(); } void GLTarget::glBind() const { LIBGUI_ASSERT_GL_OK(); DENG2_ASSERT(isReady()); if(!isReady()) return; #ifdef LIBGUI_USE_GLENTRYPOINTS if(!glBindFramebuffer) return; #endif if(d->proxy) { //qDebug() << "GLTarget: binding proxy of" << d->fbo << "=>"; d->proxy->glBind(); } else { //DENG2_ASSERT(!d->fbo || glIsFramebuffer(d->fbo)); if(d->fbo && !glIsFramebuffer(d->fbo)) { qDebug() << "GLTarget: WARNING! Attempting to bind FBO" << d->fbo << "that is not a valid OpenGL FBO"; } //qDebug() << "GLTarget: binding FBO" << d->fbo; glBindFramebuffer(GLInfo::extensions().EXT_framebuffer_blit? GL_DRAW_FRAMEBUFFER_EXT : GL_FRAMEBUFFER, d->fbo); LIBGUI_ASSERT_GL_OK(); } } void GLTarget::glRelease() const { LIBGUI_ASSERT_GL_OK(); glBindFramebuffer(GL_FRAMEBUFFER, 0); // both read and write FBOs LIBGUI_ASSERT_GL_OK(); d->updateFromProxy(); } QImage GLTarget::toImage() const { if(!d->fbo) { return CanvasWindow::main().canvas().grabImage(); } else if(d->flags & Color) { // Read the contents of the color attachment. Size imgSize = size(); QImage img(QSize(imgSize.x, imgSize.y), QImage::Format_ARGB32); glBindFramebuffer(GL_FRAMEBUFFER, d->fbo); glPixelStorei(GL_PACK_ALIGNMENT, 4); glReadPixels(0, 0, imgSize.x, imgSize.y, GL_BGRA, GL_UNSIGNED_BYTE, (GLvoid *) img.constBits()); // Restore the stack's target. GLState::current().target().glBind(); return img; } return QImage(); } void GLTarget::setClearColor(Vector4f const &color) { d->clearColor = color; } void GLTarget::clear(Flags const &attachments) { DENG2_ASSERT(isReady()); markAsChanged(); GLState::current().apply(); glBind(); // Only clear what we have. Flags which = attachments & d->flags; glClearColor(d->clearColor.x, d->clearColor.y, d->clearColor.z, d->clearColor.w); glClear((which & Color? GL_COLOR_BUFFER_BIT : 0) | (which & Depth? GL_DEPTH_BUFFER_BIT : 0) | (which & Stencil? GL_STENCIL_BUFFER_BIT : 0)); GLState::current().target().glBind(); } void GLTarget::resize(Size const &size) { // The default target resizes itself automatically with the canvas. if(d->size == size || d->isDefault()) return; glBindFramebuffer(GL_FRAMEBUFFER, d->fbo); if(d->texture) { d->texture->setUndefinedImage(size, d->texture->imageFormat()); } d->resizeRenderBuffers(size); GLState::current().target().glBind(); } GLTexture *GLTarget::attachedTexture(Flags const &attachment) const { return d->bufferTexture(attachment); } void GLTarget::replaceAttachment(Flags const &attachment, GLTexture &texture) { DENG2_ASSERT(!d->isDefault()); d->replace(d->flagsToGLAttachment(attachment), texture); } void GLTarget::setProxy(GLTarget const *proxy) { d->proxy = proxy; } void GLTarget::updateFromProxy() { d->updateFromProxy(); } void GLTarget::blit(GLTarget &dest, Flags const &attachments, gl::Filter filtering) const { LIBGUI_ASSERT_GL_OK(); DENG2_ASSERT(GLInfo::extensions().EXT_framebuffer_blit); if(!GLInfo::extensions().EXT_framebuffer_blit) return; glBindFramebuffer(GL_DRAW_FRAMEBUFFER_EXT, dest.glName()); LIBGUI_ASSERT_GL_OK(); glBindFramebuffer(GL_READ_FRAMEBUFFER_EXT, d->fbo); LIBGUI_ASSERT_GL_OK(); Flags common = d->flags & dest.flags() & attachments; glBlitFramebufferEXT(0, 0, size().x, size().y, 0, 0, dest.size().x, dest.size().y, (common.testFlag(Color)? GL_COLOR_BUFFER_BIT : 0) | (common.testFlag(Depth)? GL_DEPTH_BUFFER_BIT : 0) | (common.testFlag(Stencil)? GL_STENCIL_BUFFER_BIT : 0), filtering == gl::Nearest? GL_NEAREST : GL_LINEAR); LIBGUI_ASSERT_GL_OK(); glBindFramebuffer(GL_FRAMEBUFFER, 0); LIBGUI_ASSERT_GL_OK(); dest.markAsChanged(); GLState::current().target().glBind(); } GLuint GLTarget::glName() const { return d->fbo; } GLTarget::Size GLTarget::size() const { if(d->texture) { return d->texture->size(); } else if(d->size != nullSize) { return d->size; } return CanvasWindow::main().canvas().size(); } void GLTarget::setActiveRect(Rectangleui const &rect, bool applyGLState) { d->activeRect = rect; if(applyGLState) { // Forcibly update viewport and scissor (and other GL state). GLState::considerNativeStateUndefined(); GLState::current().apply(); } } void GLTarget::unsetActiveRect(bool applyGLState) { setActiveRect(Rectangleui(), applyGLState); } Vector2f GLTarget::activeRectScale() const { if(!hasActiveRect()) { return Vector2f(1, 1); } return Vector2f(d->activeRect.size()) / size(); } Vector2f GLTarget::activeRectNormalizedOffset() const { if(!hasActiveRect()) { return Vector2f(0, 0); } return Vector2f(d->activeRect.topLeft) / size(); } Rectangleui GLTarget::scaleToActiveRect(Rectangleui const &rectInTarget) const { // If no sub rectangle is defined, do nothing. if(!hasActiveRect()) { return rectInTarget; } Vector2f const scaling = activeRectScale(); return Rectangleui(d->activeRect.left() + scaling.x * rectInTarget.left(), d->activeRect.top() + scaling.y * rectInTarget.top(), rectInTarget.width() * scaling.x, rectInTarget.height() * scaling.y); } Rectangleui const &GLTarget::activeRect() const { return d->activeRect; } bool GLTarget::hasActiveRect() const { return !d->activeRect.isNull(); } Rectangleui GLTarget::rectInUse() const { if(hasActiveRect()) { return activeRect(); } return Rectangleui::fromSize(size()); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/atlastexture.cpp0000664000175000017500000000455312641367671025116 0ustar jaakkojaakko/** @file atlastexture.h Atlas stored on a GLTexture. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/AtlasTexture" #include "de/RowAtlasAllocator" #include "de/KdTreeAtlasAllocator" namespace de { AtlasTexture::AtlasTexture(Atlas::Flags const &flags, Atlas::Size const &totalSize) : Atlas(flags, totalSize) { // Atlas textures are updated automatically when needed. setState(Ready); } AtlasTexture *AtlasTexture::newWithRowAllocator(Atlas::Flags const &flags, Atlas::Size const &totalSize) { AtlasTexture *atlas = new AtlasTexture(flags, totalSize); atlas->setAllocator(new RowAtlasAllocator); return atlas; } AtlasTexture *AtlasTexture::newWithKdTreeAllocator(Atlas::Flags const &flags, Atlas::Size const &totalSize) { AtlasTexture *atlas = new AtlasTexture(flags, totalSize); atlas->setAllocator(new KdTreeAtlasAllocator); return atlas; } void AtlasTexture::clear() { Atlas::clear(); GLTexture::clear(); setState(Ready); } void AtlasTexture::aboutToUse() const { Atlas::commit(); } void AtlasTexture::commitFull(Image const &fullImage) const { DENG2_ASSERT(fullImage.size() == totalSize()); // While the Atlas is const, the texture isn't... const_cast(this)->setImage(fullImage); } void AtlasTexture::commit(Image const &image, Vector2i const &topLeft) const { GLTexture *tex = const_cast(this); if(size() == GLTexture::Size(0, 0)) { // Hasn't been full-committed yet. tex->setUndefinedImage(totalSize(), Image::RGBA_8888); } tex->setSubImage(image, topLeft); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/glinfo.cpp0000664000175000017500000001572412641367671023651 0ustar jaakkojaakko/** @file glinfo.cpp OpenGL information. * * @authors Copyright (c) 2003-2013 Jaakko Keränen * @authors Copyright (c) 2007-2013 Daniel Swanson * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GLInfo" #include "de/graphics/opengl.h" #include #include #include #include #include namespace de { static GLInfo info; DENG2_PIMPL_NOREF(GLInfo) { bool inited; Extensions ext; Limits lim; Instance() : inited(false) { zap(ext); zap(lim); } /** * This routine is based on the method used by David Blythe and Tom * McReynolds in the book "Advanced Graphics Programming Using OpenGL" * ISBN: 1-55860-659-9. */ bool checkExtensionString(char const *name, GLubyte const *extensions) { GLubyte const *start; GLubyte *c, *terminator; // Extension names should not have spaces. c = (GLubyte *) strchr(name, ' '); if(c || *name == '\0') return false; if(!extensions) return false; // It takes a bit of care to be fool-proof about parsing the // OpenGL extensions string. Don't be fooled by sub-strings, etc. start = extensions; for(;;) { c = (GLubyte*) strstr((char const *) start, name); if(!c) break; terminator = c + strlen(name); if(c == start || *(c - 1) == ' ') { if(*terminator == ' ' || *terminator == '\0') { return true; } } start = terminator; } return false; } bool doQuery(char const *ext) { DENG2_ASSERT(ext); #ifdef WIN32 // Prefer the wgl-specific extensions. if(wglGetExtensionsStringARB != nullptr && checkExtensionString(ext, (GLubyte const *)wglGetExtensionsStringARB(wglGetCurrentDC()))) return true; #endif #ifdef DENG_X11 // Check GLX specific extensions. if(checkExtensionString(ext, (GLubyte const *) getGLXExtensionsString())) return true; #endif return checkExtensionString(ext, glGetString(GL_EXTENSIONS)); } bool query(char const *ext) { bool found = doQuery(ext); LOGDEV_GL_VERBOSE("%s: %b") << ext << found; return found; } void init() { LOG_AS("GLInfo"); if(inited) return; // Extensions. ext.ARB_draw_instanced = query("GL_ARB_draw_instanced"); ext.ARB_instanced_arrays = query("GL_ARB_instanced_arrays"); ext.ARB_texture_env_combine = query("GL_ARB_texture_env_combine") || query("GL_EXT_texture_env_combine"); ext.ARB_texture_non_power_of_two = query("GL_ARB_texture_non_power_of_two"); ext.EXT_blend_subtract = query("GL_EXT_blend_subtract"); ext.EXT_framebuffer_blit = query("GL_EXT_framebuffer_blit"); ext.EXT_framebuffer_multisample = query("GL_EXT_framebuffer_multisample"); ext.EXT_framebuffer_object = query("GL_EXT_framebuffer_object"); ext.EXT_packed_depth_stencil = query("GL_EXT_packed_depth_stencil"); ext.EXT_texture_compression_s3tc = query("GL_EXT_texture_compression_s3tc"); ext.EXT_texture_filter_anisotropic = query("GL_EXT_texture_filter_anisotropic"); ext.ATI_texture_env_combine3 = query("GL_ATI_texture_env_combine3"); ext.NV_framebuffer_multisample_coverage = query("GL_NV_framebuffer_multisample_coverage"); ext.NV_texture_env_combine4 = query("GL_NV_texture_env_combine4"); ext.SGIS_generate_mipmap = query("GL_SGIS_generate_mipmap"); #ifdef WIN32 ext.Windows_ARB_multisample = query("WGL_ARB_multisample"); ext.Windows_EXT_swap_control = query("WGL_EXT_swap_control"); #endif #ifdef DENG_X11 ext.X11_EXT_swap_control = query("GLX_EXT_swap_control"); #endif // Limits. glGetIntegerv(GL_MAX_TEXTURE_SIZE, (GLint *) &lim.maxTexSize); glGetIntegerv(GL_MAX_TEXTURE_UNITS, (GLint *) &lim.maxTexUnits); if(ext.EXT_texture_filter_anisotropic) { glGetIntegerv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, (GLint *) &lim.maxTexFilterAniso); } // Set a custom maximum size? if(CommandLine_CheckWith("-maxtex", 1)) { lim.maxTexSize = min(ceilPow2(String(CommandLine_Next()).toInt()), lim.maxTexSize); LOG_GL_NOTE("Using requested maximum texture size of %i x %i") << lim.maxTexSize << lim.maxTexSize; } // Check default OpenGL format attributes. QGLContext const *ctx = QGLContext::currentContext(); QGLFormat form = ctx->format(); LOGDEV_GL_MSG("Initial OpenGL format:"); LOGDEV_GL_MSG(" - OpenGL supported: %b") << form.hasOpenGL(); LOGDEV_GL_MSG(" - version: %i.%i") << form.majorVersion() << form.minorVersion(); LOGDEV_GL_MSG(" - profile: %s") << (form.profile() == QGLFormat::CompatibilityProfile? "Compatibility" : "Core"); LOGDEV_GL_MSG(" - samples: %b %i") << form.sampleBuffers() << form.samples(); LOGDEV_GL_MSG(" - color: %i %i %i %i") << form.redBufferSize() << form.greenBufferSize() << form.blueBufferSize() << form.alphaBufferSize(); LOGDEV_GL_MSG(" - depth: %b %i") << form.depth() << form.depthBufferSize(); LOGDEV_GL_MSG(" - stencil: %b %i") << form.stencil() << form.stencilBufferSize(); LOGDEV_GL_MSG(" - accum: %b %i") << form.accum() << form.accumBufferSize(); LOGDEV_GL_MSG(" - double buffering: %b") << form.doubleBuffer(); inited = true; } }; GLInfo::GLInfo() : d(new Instance) {} void GLInfo::glInit() { info.d->init(); } GLInfo::Extensions const &GLInfo::extensions() { DENG2_ASSERT(info.d->inited); return info.d->ext; } GLInfo::Limits const &GLInfo::limits() { DENG2_ASSERT(info.d->inited); return info.d->lim; } bool GLInfo::isFramebufferMultisamplingSupported() { return extensions().EXT_framebuffer_multisample && extensions().EXT_framebuffer_blit; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/glentrypoints.cpp0000664000175000017500000002127212641367671025307 0ustar jaakkojaakko/** @file glentrypoints.cpp API entry points for OpenGL (Windows/Linux). * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/graphics/glentrypoints.h" #include #ifdef LIBGUI_USE_GLENTRYPOINTS #ifdef DENG_X11 # include #endif #ifdef LIBGUI_FETCH_GL_1_3 PFNGLACTIVETEXTUREPROC glActiveTexture; PFNGLBLENDEQUATIONPROC glBlendEquation; PFNGLCLIENTACTIVETEXTUREPROC glClientActiveTexture; PFNGLMULTITEXCOORD2FPROC glMultiTexCoord2f; PFNGLMULTITEXCOORD2FVPROC glMultiTexCoord2fv; #endif #ifdef WIN32 PFNWGLGETEXTENSIONSSTRINGARBPROC wglGetExtensionsStringARB; #endif PFNGLATTACHSHADERPROC glAttachShader; PFNGLBINDATTRIBLOCATIONPROC glBindAttribLocation; PFNGLBINDBUFFERPROC glBindBuffer; PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer; PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer; PFNGLBLENDFUNCSEPARATEPROC glBlendFuncSeparate; PFNGLBUFFERDATAPROC glBufferData; PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus; PFNGLCOMPILESHADERPROC glCompileShader; PFNGLCREATEPROGRAMPROC glCreateProgram; PFNGLCREATESHADERPROC glCreateShader; PFNGLDELETEBUFFERSPROC glDeleteBuffers; PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers; PFNGLDELETEPROGRAMPROC glDeleteProgram; PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers; PFNGLDELETESHADERPROC glDeleteShader; PFNGLDETACHSHADERPROC glDetachShader; PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray; PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray; PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer; PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D; PFNGLGENBUFFERSPROC glGenBuffers; PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers; PFNGLGENERATEMIPMAPPROC glGenerateMipmap; PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers; PFNGLGETATTRIBLOCATIONPROC glGetAttribLocation; PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog; PFNGLGETPROGRAMIVPROC glGetProgramiv; PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog; PFNGLGETSHADERIVPROC glGetShaderiv; PFNGLGETSHADERSOURCEPROC glGetShaderSource; PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation; PFNGLISBUFFERPROC glIsBuffer; PFNGLISFRAMEBUFFERPROC glIsFramebuffer; PFNGLISPROGRAMPROC glIsProgram; PFNGLLINKPROGRAMPROC glLinkProgram; PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage; PFNGLSHADERSOURCEPROC glShaderSource; PFNGLUNIFORM1FPROC glUniform1f; PFNGLUNIFORM1IPROC glUniform1i; PFNGLUNIFORM2FPROC glUniform2f; PFNGLUNIFORM3FPROC glUniform3f; PFNGLUNIFORM3FVPROC glUniform3fv; PFNGLUNIFORM4FPROC glUniform4f; PFNGLUNIFORM4FVPROC glUniform4fv; PFNGLUNIFORMMATRIX3FVPROC glUniformMatrix3fv; PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv; PFNGLUSEPROGRAMPROC glUseProgram; PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer; // Extensions: PFNGLBLITFRAMEBUFFEREXTPROC glBlitFramebufferEXT; PFNGLRENDERBUFFERSTORAGEMULTISAMPLEEXTPROC glRenderbufferStorageMultisampleEXT; #ifdef GL_ARB_draw_instanced PFNGLDRAWARRAYSINSTANCEDARBPROC glDrawArraysInstancedARB; PFNGLDRAWELEMENTSINSTANCEDARBPROC glDrawElementsInstancedARB; #endif #ifdef GL_ARB_instanced_arrays PFNGLVERTEXATTRIBDIVISORARBPROC glVertexAttribDivisorARB; #endif #ifdef GL_NV_framebuffer_multisample_coverage PFNGLRENDERBUFFERSTORAGEMULTISAMPLECOVERAGENVPROC glRenderbufferStorageMultisampleCoverageNV; #endif static void reportMissingEntryPoint(char const *name) { throw de::Error("getAllOpenGLEntryPoints", QString("Required OpenGL function missing: %1").arg(name)); } void getAllOpenGLEntryPoints() { static bool haveProcs = false; if(haveProcs) return; #ifdef WIN32 # ifdef MSVC # define GET_PROC_EXT_ALT(varName, altName) *((void**)&varName) = wglGetProcAddress(#altName) # define GET_PROC_EXT(name) GET_PROC_EXT_ALT(name, name) # else # define GET_PROC_EXT_ALT(varName, altName) *reinterpret_cast(&varName) = wglGetProcAddress(#altName) # define GET_PROC_EXT(name) GET_PROC_EXT_ALT(name, name) # endif #else # define GET_PROC_EXT_ALT(varName, altName) *((void (**)())&varName) = glXGetProcAddress((GLubyte const *)#altName) # define GET_PROC_EXT(name) GET_PROC_EXT_ALT(name, name) #endif #define GET_PROC(name) GET_PROC_EXT(name); DENG2_ASSERT(name != 0); \ if(!name) { reportMissingEntryPoint(#name); } // must have #define GET_PROC_ALT(name, altName) \ GET_PROC_EXT_ALT(name, altName); /* try the alternative name first */ \ if(!name) { \ GET_PROC_EXT(name); DENG2_ASSERT(name != 0); \ if(!name) { reportMissingEntryPoint(#name); } /* must have */ \ } #ifdef LIBGUI_FETCH_GL_1_3 GET_PROC (glActiveTexture); GET_PROC (glBlendEquation); GET_PROC (glClientActiveTexture); GET_PROC (glMultiTexCoord2f); GET_PROC (glMultiTexCoord2fv); #endif #ifdef WIN32 GET_PROC (wglGetExtensionsStringARB); #endif GET_PROC (glAttachShader); GET_PROC (glBindAttribLocation); GET_PROC (glBindBuffer); GET_PROC_ALT(glBindFramebuffer, glBindFramebufferEXT); GET_PROC_ALT(glBindRenderbuffer, glBindRenderbufferEXT); GET_PROC_ALT(glBlendFuncSeparate, glBlendFuncSeparateEXT); GET_PROC (glBufferData); GET_PROC_ALT(glCheckFramebufferStatus, glCheckFramebufferStatusEXT); GET_PROC (glCompileShader); GET_PROC (glCreateProgram); GET_PROC (glCreateShader); GET_PROC (glDeleteBuffers); GET_PROC_ALT(glDeleteFramebuffers, glDeleteFramebuffersEXT); GET_PROC (glDeleteProgram); GET_PROC_ALT(glDeleteRenderbuffers, glDeleteRenderbuffersEXT); GET_PROC (glDeleteShader); GET_PROC (glDetachShader); GET_PROC (glDisableVertexAttribArray); GET_PROC (glEnableVertexAttribArray); GET_PROC_ALT(glFramebufferRenderbuffer, glFramebufferRenderbufferEXT); GET_PROC_ALT(glFramebufferTexture2D, glFramebufferTexture2DEXT); GET_PROC (glGenBuffers); GET_PROC_ALT(glGenFramebuffers, glGenFramebuffersEXT); GET_PROC_ALT(glGenerateMipmap, glGenerateMipmapEXT); GET_PROC_ALT(glGenRenderbuffers, glGenRenderbuffersEXT); GET_PROC (glGetAttribLocation); GET_PROC (glGetProgramInfoLog); GET_PROC (glGetProgramiv); GET_PROC (glGetShaderInfoLog); GET_PROC (glGetShaderiv); GET_PROC (glGetShaderSource); GET_PROC (glGetUniformLocation); GET_PROC (glIsBuffer); GET_PROC_ALT(glIsFramebuffer, glIsFramebufferEXT); GET_PROC (glIsProgram); GET_PROC (glLinkProgram); GET_PROC_ALT(glRenderbufferStorage, glRenderbufferStorageEXT); GET_PROC (glShaderSource); GET_PROC (glUniform1f); GET_PROC (glUniform1i); GET_PROC (glUniform2f); GET_PROC (glUniform3f); GET_PROC (glUniform3fv); GET_PROC (glUniform4f); GET_PROC (glUniform4fv); GET_PROC (glUniformMatrix3fv); GET_PROC (glUniformMatrix4fv); GET_PROC (glUseProgram); GET_PROC (glVertexAttribPointer); // Extensions: GET_PROC_EXT(glBlitFramebufferEXT); GET_PROC_EXT(glRenderbufferStorageMultisampleEXT); #ifdef GL_ARB_draw_instanced GET_PROC_EXT(glDrawArraysInstancedARB); GET_PROC_EXT(glDrawElementsInstancedARB); #endif #ifdef GL_ARB_instanced_arrays GET_PROC_EXT(glVertexAttribDivisorARB); #endif #ifdef GL_NV_framebuffer_multisample_coverage GET_PROC_EXT(glRenderbufferStorageMultisampleCoverageNV); #endif #ifdef DENG_X11 getGLXEntryPoints(); #endif haveProcs = true; } #endif // LIBGUI_USE_GLENTRYPOINTS doomsday-stable-1.15.7/doomsday/libgui/src/graphics/rowatlasallocator.cpp0000664000175000017500000004023112641367671026117 0ustar jaakkojaakko/** @file rowatlasallocator.cpp Row-based atlas allocator. * * The row allocator works according to the following principles: * * - In the beginning, there is a single row that spans the height of the entire atlas. * The row contains a single empty segment. * - If a row is completely empty, the empty space below will be split into a new empty * row when the first allocation is made on the line. The first allocation also * determines the initial height of the row. * - The height of a row may expand if there is empty space below. * - All the empty spaces are kept ordered from narrow to wide, so that when a new * allocation is needed, the smallest suitable space can be picked. * - Each row is a doubly-linked list containing the used and free regions. * - If there are two adjacent free regions on a row, they will be merged into a larger * empty space. Similarly empty rows are merged together. * * @authors Copyright (c) 2013-2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/RowAtlasAllocator" #include #include namespace de { template void linkAfter(Type *where, Type *object) { object->next = where->next; object->prev = where; if(where->next) where->next->prev = object; where->next = object; } template void unlink(Type *object) { if(object->prev) object->prev->next = object->next; if(object->next) object->next->prev = object->prev; object->next = object->prev = nullptr; } /// The allocations are only optimized if less than 70% of the area is being utilized. static float const OPTIMIZATION_USAGE_THRESHOLD = .7f; DENG2_PIMPL(RowAtlasAllocator) { struct Rows { struct Row; /** * Each row is composed of one or more used or empty slots. */ struct Slot { Slot *next = nullptr; Slot *prev = nullptr; Row *row; Id id { Id::None }; ///< Id of allocation here, or Id::None if free. int x = 0; ///< Left edge of the slot. duint width = 0; ///< Width of the slot. dsize usedArea = 0; Slot(Row *owner) : row(owner) {} bool isEmpty() const { return id.isNone(); } /** * Take an empty slot into use. The remaining empty space is split off * into a new slot. * * @param allocId Allocation identifier. * @param widthWithMargin Needed width. * * @return If an empty slot was created, it is returned. Otherwise @c nullptr. */ Slot *allocateAndSplit(Id const &allocId, duint widthWithMargin) { DENG2_ASSERT(isEmpty()); DENG2_ASSERT(width >= widthWithMargin); int const remainder = width - widthWithMargin; id = allocId; width = widthWithMargin; if(remainder > 0) { Slot *split = new Slot(row); linkAfter(this, split); split->x = x + width; split->width = remainder; return split; } return nullptr; } Slot *mergeWithNext() { DENG2_ASSERT(isEmpty()); if(!next || !next->isEmpty()) return nullptr; Slot *merged = next; unlink(merged); width += merged->width; return merged; // Caller gets ownership. } Slot *mergeWithPrevious() { DENG2_ASSERT(isEmpty()); if(!prev || !prev->isEmpty()) return nullptr; Slot *merged = prev; unlink(merged); if(row->first == merged) { row->first = this; } x -= merged->width; width += merged->width; return merged; // Caller gets ownership. } struct SortByWidth { bool operator () (Slot const *a, Slot const *b) { if(a->width == b->width) return a < b; return a->width > b->width; } }; }; struct Row { Row *next = nullptr; Row *prev = nullptr; int y = 0; ///< Top edge of the row. duint height = 0; Slot *first; ///< There's always at least one empty slot. Row() : first(new Slot(this)) {} ~Row() { // Delete all the slots. Slot *next; for(Slot *s = first; s; s = next) { next = s->next; delete s; } } bool isEmpty() const { return first->isEmpty() && !first->next; } bool isTallEnough(duint heightWithMargin) const { if(height >= heightWithMargin) return true; // The row might be able to expand. if(next && next->isEmpty()) { return (height + next->height) >= heightWithMargin; } return false; } Row *split(duint newHeight) { DENG2_ASSERT(isEmpty()); DENG2_ASSERT(newHeight <= height); duint const remainder = height - newHeight; height = newHeight; if(remainder > 0) { Row *below = new Row; linkAfter(this, below); below->y = y + height; below->height = remainder; return below; } return nullptr; } void grow(duint newHeight) { DENG2_ASSERT(newHeight > height); DENG2_ASSERT(next); DENG2_ASSERT(next->isEmpty()); duint delta = newHeight - height; height += delta; next->y += delta; next->height -= delta; } }; Row *top; ///< Always at least one row exists. std::set vacant; // not owned QHash slotsById; // not owned dsize usedArea = 0; ///< Total allocated pixels. Instance *d; Rows(Instance *inst) : d(inst) { top = new Row; /* * Set up one big row, excluding the margins. This is all the space that * we will be using; it will be chopped up and merged back together, but * space will not be added or removed. Margin is reserved on the top/left * edge; individual slots reserve it on the right, rows reserve it in the * bottom. */ top->y = d->margin; top->height = d->size.y - d->margin; top->first->x = d->margin; top->first->width = d->size.x - d->margin; addVacant(top->first); } ~Rows() { Row *next; for(Row *r = top; r; r = next) { next = r->next; delete r; } } void addVacant(Slot *slot) { DENG2_ASSERT(slot->isEmpty()); vacant.insert(slot); DENG2_ASSERT(*vacant.find(slot) == slot); } void removeVacant(Slot *slot) { DENG2_ASSERT(vacant.find(slot) != vacant.end()); vacant.erase(slot); DENG2_ASSERT(vacant.find(slot) == vacant.end()); } Slot *findBestVacancy(Atlas::Size const &size) const { Slot *best = nullptr; // Look through the vacancies starting with the widest one. Statistically // there are more narrow empty slots than wide ones. for(Slot *s : vacant) { if(s->width >= size.x + d->margin) { if(s->row->isTallEnough(size.y + d->margin)) { best = s; } } else { // Too narrow, the rest is also too narrow. break; } } return best; } /** * Allocate a slot for the specified size. The area used by the slot may be * larger than the requested size. * * @param size Dimensions of area to allocate. * @param rect Allocated rectangle is returned here. * @param id Id for the new slot. * * @return Allocated slot, or @c nullptr. */ Slot *alloc(Atlas::Size const &size, Rectanglei &rect, Id::Type id = Id::None) { Slot *slot = findBestVacancy(size); if(!slot) return nullptr; DENG2_ASSERT(slot->isEmpty()); // This slot will be taken into use. removeVacant(slot); Atlas::Size const needed = size + Atlas::Size(d->margin, d->margin); // The first allocation determines the initial row height. The remainder // is split into a new empty row (if something remains). if(slot->row->isEmpty()) { if(Row *addedRow = slot->row->split(needed.y)) { // Give this new row the correct width. addedRow->first->x = d->margin; addedRow->first->width = d->size.x - d->margin; addVacant(addedRow->first); } } // The row may expand if needed. if(slot->row->height < needed.y) { slot->row->grow(needed.y); } // Got a place, mark it down. if(Slot *addedSlot = slot->allocateAndSplit(id? Id(id) : Id(), needed.x)) { addVacant(addedSlot); } slotsById.insert(slot->id, slot); rect = Rectanglei::fromSize(Vector2i(slot->x, slot->row->y), size); slot->usedArea = size.x * size.y; usedArea += slot->usedArea; DENG2_ASSERT(usedArea <= d->size.x * d->size.y); DENG2_ASSERT(vacant.find(slot) == vacant.end()); DENG2_ASSERT(!slot->isEmpty()); return slot; } void mergeLeft(Slot *slot) { if(Slot *removed = slot->mergeWithPrevious()) { removeVacant(removed); delete removed; } } void mergeRight(Slot *slot) { if(Slot *removed = slot->mergeWithNext()) { removeVacant(removed); delete removed; } } void mergeAbove(Row *row) { DENG2_ASSERT(row->isEmpty()); if(row->prev && row->prev->isEmpty()) { Row *merged = row->prev; unlink(merged); if(top == merged) { top = row; } row->y -= merged->height; row->height += merged->height; removeVacant(merged->first); delete merged; } } void mergeBelow(Row *row) { DENG2_ASSERT(row->isEmpty()); if(row->next && row->next->isEmpty()) { Row *merged = row->next; unlink(merged); row->height += merged->height; removeVacant(merged->first); delete merged; } } void release(Id const &id) { DENG2_ASSERT(slotsById.contains(id)); // Make the slot vacant again. Slot *slot = slotsById.take(id); slot->id = Id::None; DENG2_ASSERT(slot->usedArea > 0); DENG2_ASSERT(usedArea >= slot->usedArea); usedArea -= slot->usedArea; mergeLeft(slot); mergeRight(slot); addVacant(slot); // Empty rows will merge together. if(slot->row->isEmpty()) { mergeAbove(slot->row); mergeBelow(slot->row); } } }; Atlas::Size size; int margin { 0 }; Allocations allocs; std::unique_ptr rows; Instance(Public *i) : Base(i) , rows(new Rows(this)) {} struct ContentSize { Id::Type id; Atlas::Size size; ContentSize(Id const &allocId, Vector2ui const &sz) : id(allocId), size(sz) {} bool operator < (ContentSize const &other) const { if(size.y == other.size.y) { // Secondary sorting by descending width. return size.x > other.size.x; } return size.y > other.size.y; } }; bool optimize() { // Set up a LUT based on descending allocation width. QList descending; DENG2_FOR_EACH(Allocations, i, allocs) { descending.append(ContentSize(i.key(), i.value().size())); } qSort(descending); Allocations optimal; std::unique_ptr revised(new Rows(this)); for(auto const &ct : descending) { Rectanglei optRect; if(!revised->alloc(ct.size, optRect, ct.id)) { return false; // Ugh, can't actually fit these. } optimal[ct.id] = optRect; } allocs = optimal; rows.reset(revised.release()); return true; } float usage() const { return float(rows->usedArea) / float(size.x * size.y); } }; RowAtlasAllocator::RowAtlasAllocator() : d(new Instance(this)) {} void RowAtlasAllocator::setMetrics(Atlas::Size const &totalSize, int margin) { d->size = totalSize; d->margin = margin; DENG2_ASSERT(d->allocs.isEmpty()); d->rows.reset(new Instance::Rows(d)); } void RowAtlasAllocator::clear() { d->rows.reset(new Instance::Rows(d)); d->allocs.clear(); } Id RowAtlasAllocator::allocate(Atlas::Size const &size, Rectanglei &rect) { if(auto *slot = d->rows->alloc(size, rect)) { d->allocs[slot->id] = rect; return slot->id; } // Couldn't find a suitable place. return 0; } void RowAtlasAllocator::release(Id const &id) { DENG2_ASSERT(d->allocs.contains(id)); d->rows->release(id); d->allocs.remove(id); } int RowAtlasAllocator::count() const { return d->allocs.size(); } Atlas::Ids RowAtlasAllocator::ids() const { Atlas::Ids ids; foreach(Id const &id, d->allocs.keys()) { ids.insert(id); } return ids; } void RowAtlasAllocator::rect(Id const &id, Rectanglei &rect) const { DENG2_ASSERT(d->allocs.contains(id)); rect = d->allocs[id]; } RowAtlasAllocator::Allocations RowAtlasAllocator::allocs() const { return d->allocs; } bool RowAtlasAllocator::optimize() { // Optimization is not attempted unless there is a significant portion of // unused space. if(d->usage() >= OPTIMIZATION_USAGE_THRESHOLD) return false; return d->optimize(); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/glentrypoints_x11.cpp0000664000175000017500000000310712641367671025775 0ustar jaakkojaakko/** @file glentrypoints_x11.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/graphics/glentrypoints.h" #ifdef DENG_X11 #include "de/CanvasWindow" #include #include #include PFNGLXSWAPINTERVALEXTPROC glXSwapIntervalEXT; void getGLXEntryPoints() { #define GET_PROC_EXT(name) *((void (**)())&name) = glXGetProcAddress((GLubyte const *)#name) GET_PROC_EXT(glXSwapIntervalEXT); #undef GET_PROC_EXT } char const *getGLXExtensionsString() { return glXQueryExtensionsString(QX11Info::display(), QX11Info::appScreen()); } void setXSwapInterval(int interval) { if(glXSwapIntervalEXT) { DENG2_ASSERT(de::CanvasWindow::mainExists()); glXSwapIntervalEXT(QX11Info::display(), de::CanvasWindow::main().canvas().winId(), interval); } } #endif // DENG_X11 doomsday-stable-1.15.7/doomsday/libgui/src/graphics/image.cpp0000664000175000017500000004523412641367671023454 0ustar jaakkojaakko/** @file image.h Wrapper over QImage. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/Image" #include "de/graphics/opengl.h" #include #include #include #include #include #include #include #include namespace de { #define IMAGE_ASSERT_EDITABLE(d) DENG2_ASSERT(d->format == UseQImageFormat) namespace internal { namespace pcx { static dbyte const MAGIC = 0x0a; static dbyte const RLE_ENCODING = 1; static dsize const HEADER_SIZE = 128; struct Header : public IReadable { dbyte magic; dbyte version; dbyte encoding; dbyte bitsPerPixel; duint16 xMin, yMin; duint16 xMax, yMax; duint16 hRes, vRes; dbyte colorPlanes; duint16 bytesPerLine; duint16 paletteType; void operator << (Reader &from) { from >> magic >> version >> encoding >> bitsPerPixel >> xMin >> yMin >> xMax >> yMax >> hRes >> vRes; from.seek(48); // skip EGA palette from.seek(1); // skip reserved field from >> colorPlanes >> bytesPerLine >> paletteType; } }; static bool recognize(Block const &data) { try { Header header; Reader(data) >> header; // Only paletted format supported. return (header.magic == MAGIC && header.version == 5 /* latest */ && header.encoding == RLE_ENCODING && header.bitsPerPixel == 8); } catch(Error const &) { return false; } } /** * Loads a PCX image into a QImage using an RGB888 buffer. The PCX palette is used * to map color indices to RGB values. * * @param data Source data containing a PCX image. * * @return QImage using the RGB888 (24-bit) format. */ static QImage load(Block const &data) { Header header; Reader reader(data); reader >> header; Image::Size const size(header.xMax + 1, header.yMax + 1); QImage image(size.x, size.y, QImage::Format_RGB888); DENG2_ASSERT(image.depth() == 24); dbyte const *palette = data.data() + data.size() - 768; dbyte const *pos = data.data() + HEADER_SIZE; dbyte *dst = image.bits(); for(duint y = 0; y < size.y; ++y, dst += size.x * 3) { for(duint x = 0; x < size.x; ) { dbyte value = *pos++; // RLE inflation. int runLength = 1; if((value & 0xc0) == 0xc0) { runLength = value & 0x3f; value = *pos++; } while(runLength-- > 0) { // Get the RGB triplets from the palette. std::memcpy(&dst[3 * x++], &palette[3 * value], 3); } } } return image; } } // namespace pcx namespace tga { struct Header : public IReadable { enum Flag { NoFlags = 0, ScreenOriginUpper = 0x1, InterleaveTwoWay = 0x2, InterleaveFourWay = 0x4 }; Q_DECLARE_FLAGS(Flags, Flag) enum ColorMapType { ColorMapNone = 0, ColorMap256 = 1 // not supported }; enum ImageType { RGB = 2, ///< Uncompressed RGB. RleRGB = 10 ///< Run length encoded RGB. }; Block identification; Zeroed colorMapType; Zeroed imageType; // Color map. Zeroed mapIndex; Zeroed mapCount; ///< Number of color map entries. Zeroed mapEntrySize; ///< Bits in a color map entry. // Image specification. Flags flags; Vector2 origin; Vector2 size; Zeroed depth; Zeroed attrib; void operator << (Reader &from) { duint8 identificationSize; from >> identificationSize >> colorMapType >> imageType >> mapIndex >> mapCount >> mapEntrySize >> origin.x >> origin.y >> size.x >> size.y >> depth; duint8 f; from >> f; /* Flags: 0-3 : Number of attribute bits 4 : reserved 5 : Screen origin in upper left corner 6-7 : Data storage interleave 00 - no interleave 01 - even/odd interleave 10 - four way interleave 11 - reserved */ attrib = f & 0x0f; flags = (f & 0x20? ScreenOriginUpper : NoFlags); if((f & 0xc0) == 0x40) flags |= InterleaveTwoWay; if((f & 0xc0) == 0x80) flags |= InterleaveFourWay; from.readBytes(identificationSize, identification); } }; Q_DECLARE_OPERATORS_FOR_FLAGS(Header::Flags) static bool recognize(Block const &data) { try { Header header; Reader(data) >> header; return (header.imageType == Header::RGB || header.imageType == Header::RleRGB) && header.colorMapType == Header::ColorMapNone && (header.depth == 24 || header.depth == 32) && !header.flags.testFlag(Header::ScreenOriginUpper); } catch(...) { return false; } } static QImage load(Block const &data) { Header header; Reader input(data); input >> header; int const pixelSize = header.depth / 8; QImage img(QSize(header.size.x, header.size.y), pixelSize == 4? QImage::Format_ARGB32 : QImage::Format_RGB888); dbyte *base = img.bits(); bool const isUpperOrigin = header.flags.testFlag(Header::ScreenOriginUpper); // RGB can be read line by line. if(header.imageType == Header::RGB) { for(int y = 0; y < header.size.y; y++) { int inY = (isUpperOrigin? y : (header.size.y - y - 1)); ByteRefArray line(base + (inY * header.size.x * pixelSize), header.size.x * pixelSize); input.readPresetSize(line); } } else if(header.imageType == Header::RleRGB) { img.fill(0); // RLE packets may cross over to the next line. int x = 0; int y = (isUpperOrigin? 0 : (header.size.y - 1)); int endY = header.size.y - y - 1; int stepY = (isUpperOrigin? 1 : -1); while(y != endY && x < header.size.x) { dbyte rle; input >> rle; int count; bool repeat = false; if(rle & 0x80) // Repeat? { repeat = true; count = (rle & 0x7f) + 1; } else { count = rle + 1; } Block pixel; for(int i = 0; i < count; ++i) { if(i == 0 || !repeat) { // Read the first/next byte. input.readBytes(pixelSize, pixel); } std::memcpy(base + (x + y * header.size.x) * pixelSize, pixel.constData(), pixelSize); // Advance the position. if(++x == header.size.x) { x = 0; y += stepY; } } } } return img; } } // namespace tga } // namespace internal using namespace internal; DENG2_PIMPL(Image) { Format format; Size size; QImage image; Block pixels; ByteRefArray refPixels; Instance(Public *i, QImage const &img = QImage()) : Base(i), format(UseQImageFormat), image(img) { size = Size(img.width(), img.height()); } Instance(Public *i, Instance const &other) : Base (i), format (other.format), size (other.size), image (other.image), pixels (other.pixels), refPixels(other.refPixels) {} Instance(Public *i, Size const &imgSize, Format imgFormat, IByteArray const &imgPixels) : Base(i), format(imgFormat), size(imgSize), pixels(imgPixels) {} Instance(Public *i, Size const &imgSize, Format imgFormat, ByteRefArray const &imgRefPixels) : Base(i), format(imgFormat), size(imgSize), refPixels(imgRefPixels) {} }; Image::Image() : d(new Instance(this)) {} Image::Image(Image const &other) : de::ISerializable(), d(new Instance(this, *other.d)) {} Image::Image(QImage const &image) : d(new Instance(this, image)) {} Image::Image(Size const &size, Format format, IByteArray const &pixels) : d(new Instance(this, size, format, pixels)) {} Image::Image(Size const &size, Format format, ByteRefArray const &refPixels) : d(new Instance(this, size, format, refPixels)) {} Image &Image::operator = (Image const &other) { d.reset(new Instance(this, *other.d)); return *this; } Image &Image::operator = (QImage const &other) { d.reset(new Instance(this, other)); return *this; } Image::Format Image::format() const { return d->format; } QImage::Format Image::qtFormat() const { if(d->format == UseQImageFormat) { return d->image.format(); } return QImage::Format_Invalid; } Image::Size Image::size() const { return d->size; } Rectanglei Image::rect() const { return Rectanglei(0, 0, d->size.x, d->size.y); } int Image::depth() const { switch(d->format) { case UseQImageFormat: return d->image.depth(); case Luminance_8: case Alpha_8: return 8; case LuminanceAlpha_88: case RGB_565: case RGBA_4444: case RGBA_5551: return 16; case RGB_888: return 24; case RGBA_8888: case RGBx_8888: return 32; default: return 0; } } int Image::stride() const { if(d->format == UseQImageFormat) { return d->image.bytesPerLine(); } return depth()/8 * d->size.x; } int Image::byteCount() const { if(d->format == UseQImageFormat) { return d->image.byteCount(); } if(!d->pixels.isEmpty()) { return d->pixels.size(); } return depth()/8 * d->size.x * d->size.y; } void const *Image::bits() const { if(d->format == UseQImageFormat) { return d->image.constBits(); } if(!d->pixels.isEmpty()) { return d->pixels.constData(); } return d->refPixels.readBase(); } void *Image::bits() { if(d->format == UseQImageFormat) { return d->image.bits(); } if(!d->pixels.isEmpty()) { return d->pixels.data(); } return d->refPixels.base(); } bool Image::isNull() const { return size() == Size(0, 0); } bool Image::isGLCompatible() const { if(d->format == UseQImageFormat) { // Some QImage formats are GL compatible. switch(qtFormat()) { case QImage::Format_ARGB32: // 8888 case QImage::Format_RGB32: // 8888 case QImage::Format_RGB888: // 888 case QImage::Format_RGB16: // 565 case QImage::Format_RGB555: // 555 case QImage::Format_RGB444: // 444 return true; default: return false; } } return d->format >= Luminance_8 && d->format <= RGBx_8888; } bool Image::canConvertToQImage() const { switch(d->format) { case RGB_444: case RGB_555: case RGB_565: case RGB_888: case RGBA_8888: case RGBx_8888: case UseQImageFormat: return true; default: return false; } } QImage Image::toQImage() const { if(d->format == UseQImageFormat) { return d->image; } // There may be some conversions we can do. QImage::Format form = QImage::Format_Invalid; switch(d->format) { case RGB_444: form = QImage::Format_RGB444; break; case RGB_555: form = QImage::Format_RGB555; break; case RGB_565: form = QImage::Format_RGB16; break; case RGB_888: form = QImage::Format_RGB888; break; case RGBA_8888: form = QImage::Format_ARGB32; break; case RGBx_8888: form = QImage::Format_RGB32; break; default: // Cannot be done. return QImage(); } QImage img(QSize(d->size.x, d->size.y), form); std::memcpy(const_cast(img.constBits()), bits(), byteCount()); return img; } GLPixelFormat Image::glFormat() const { if(d->format == UseQImageFormat) { return glFormat(d->image.format()); } return glFormat(d->format); } Image Image::subImage(Rectanglei const &subArea) const { IMAGE_ASSERT_EDITABLE(d); return Image(d->image.copy(subArea.topLeft.x, subArea.topLeft.y, subArea.width(), subArea.height())); } void Image::resize(Size const &size) { IMAGE_ASSERT_EDITABLE(d); DENG2_ASSERT(d->image.format() != QImage::Format_Invalid); QImage resized(QSize(size.x, size.y), d->image.format()); QPainter painter(&resized); painter.drawImage(QPoint(0, 0), d->image); d->image = resized; d->size = size; } void Image::fill(Color const &color) { IMAGE_ASSERT_EDITABLE(d); d->image.fill(QColor(color.x, color.y, color.z, color.w).rgba()); } void Image::fill(Rectanglei const &rect, Color const &color) { IMAGE_ASSERT_EDITABLE(d); QPainter painter(&d->image); painter.setCompositionMode(QPainter::CompositionMode_Source); painter.fillRect(QRect(rect.topLeft.x, rect.topLeft.y, rect.width(), rect.height()), QColor(color.x, color.y, color.z, color.w)); } void Image::draw(Image const &image, Vector2i const &topLeft) { IMAGE_ASSERT_EDITABLE(d); IMAGE_ASSERT_EDITABLE(image.d); QPainter painter(&d->image); painter.drawImage(QPoint(topLeft.x, topLeft.y), image.d->image); } void Image::drawPartial(Image const &image, Rectanglei const &part, Vector2i const &topLeft) { IMAGE_ASSERT_EDITABLE(d); IMAGE_ASSERT_EDITABLE(image.d); QPainter painter(&d->image); painter.drawImage(QPoint(topLeft.x, topLeft.y), image.d->image, QRect(part.left(), part.top(), part.width(), part.height())); } void Image::operator >> (Writer &to) const { to << duint8(d->format); if(d->format == UseQImageFormat) { Block block; QDataStream os(&block, QIODevice::WriteOnly); os.setVersion(QDataStream::Qt_4_8); os << d->image; to << block; } else { to << d->size << ByteRefArray(bits(), byteCount()); } } void Image::operator << (Reader &from) { d->pixels.clear(); d->refPixels = ByteRefArray(); from.readAs(d->format); if(d->format == UseQImageFormat) { Block block; from >> block; QDataStream is(block); is.setVersion(QDataStream::Qt_4_8); is >> d->image; d->size.x = d->image.width(); d->size.y = d->image.height(); } else { from >> d->size >> d->pixels; } } GLPixelFormat Image::glFormat(Format imageFormat) { DENG2_ASSERT(imageFormat >= Luminance_8 && imageFormat <= RGBx_8888); switch(imageFormat) { case Luminance_8: return GLPixelFormat(GL_LUMINANCE, GL_UNSIGNED_BYTE, 1); case LuminanceAlpha_88: return GLPixelFormat(GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, 2); case Alpha_8: return GLPixelFormat(GL_ALPHA, GL_UNSIGNED_BYTE, 1); case RGB_555: return GLPixelFormat(GL_RGB, GL_UNSIGNED_SHORT_5_5_5_1, 2); case RGB_565: return GLPixelFormat(GL_RGB, GL_UNSIGNED_SHORT_5_6_5, 2); case RGB_444: return GLPixelFormat(GL_RGB, GL_UNSIGNED_SHORT_4_4_4_4, 2); case RGB_888: return GLPixelFormat(GL_RGB, GL_UNSIGNED_BYTE, 1); case RGBA_4444: return GLPixelFormat(GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4, 2); case RGBA_5551: return GLPixelFormat(GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, 2); case RGBA_8888: return GLPixelFormat(GL_RGBA, GL_UNSIGNED_BYTE, 4); default: case RGBx_8888: return GLPixelFormat(GL_RGBA, GL_UNSIGNED_BYTE, 4); } } GLPixelFormat Image::glFormat(QImage::Format format) { switch(format) { case QImage::Format_Indexed8: return GLPixelFormat(GL_LUMINANCE, GL_UNSIGNED_BYTE, 1); case QImage::Format_RGB444: return GLPixelFormat(GL_RGB, GL_UNSIGNED_SHORT_4_4_4_4, 2); case QImage::Format_ARGB4444_Premultiplied: return GLPixelFormat(GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4, 2); case QImage::Format_RGB555: return GLPixelFormat(GL_RGB, GL_UNSIGNED_SHORT_5_5_5_1, 2); case QImage::Format_RGB16: return GLPixelFormat(GL_RGB, GL_UNSIGNED_SHORT_5_6_5, 2); case QImage::Format_RGB888: return GLPixelFormat(GL_RGB, GL_UNSIGNED_BYTE, 1); case QImage::Format_RGB32: /// @todo Is GL_BGR in any GL standard spec? Check for EXT_bgra. return GLPixelFormat(GL_BGR, GL_UNSIGNED_BYTE, 4); case QImage::Format_ARGB32: /// @todo Is GL_BGRA in any GL standard spec? Check for EXT_bgra. return GLPixelFormat(GL_BGRA, GL_UNSIGNED_BYTE, 4); default: break; } return GLPixelFormat(GL_RGBA, GL_UNSIGNED_BYTE, 4); } Image Image::solidColor(Color const &color, Size const &size) { QImage img(QSize(size.x, size.y), QImage::Format_ARGB32); img.fill(QColor(color.x, color.y, color.z, color.w).rgba()); return img; } Image Image::fromData(IByteArray const &data, String const &formatHint) { return fromData(Block(data), formatHint); } Image Image::fromData(Block const &data, String const &formatHint) { // Targa doesn't have a reliable "magic" identifier so we require a hint. if(!formatHint.compareWithoutCase(".tga") && tga::recognize(data)) { return tga::load(data); } // Qt does not support PCX images (too old school?). if(pcx::recognize(data)) { return pcx::load(data); } /// @todo Could check when alpha channel isn't needed and return an RGB888 /// image instead. -jk return QImage::fromData(data).convertToFormat(QImage::Format_ARGB32); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/texturebank.cpp0000664000175000017500000000402212641367671024714 0ustar jaakkojaakko/** @file texturebank.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/TextureBank" namespace de { DENG2_PIMPL_NOREF(TextureBank::ImageSource) { DotPath id; }; TextureBank::ImageSource::ImageSource(DotPath const &id) : d(new Instance) { d->id = id; } DotPath const &TextureBank::ImageSource::id() const { return d->id; } DENG2_PIMPL_NOREF(TextureBank) { struct TextureData : public IData { AtlasTexture *atlas; Id id; TextureData(Image const &image, AtlasTexture *atlasTex) : atlas(atlasTex) { id = atlas->alloc(image); /// @todo Reduce size if doesn't fit? Can be expanded when requested for use. } ~TextureData() { atlas->release(id); } }; AtlasTexture *atlas; Instance() : atlas(0) {} }; TextureBank::TextureBank() : Bank("TextureBank"), d(new Instance) {} void TextureBank::setAtlas(AtlasTexture &atlas) { d->atlas = &atlas; } Id const &TextureBank::texture(DotPath const &id) { return data(id).as().id; } Bank::IData *TextureBank::loadFromSource(ISource &source) { DENG2_ASSERT(d->atlas != 0); return new Instance::TextureData(source.as().load(), d->atlas); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/glshaderbank.cpp0000664000175000017500000001631112641367671025011 0ustar jaakkojaakko/** @file glshaderbank.cpp Bank containing GL shaders. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GLShaderBank" #include "de/GLProgram" #include "de/GLShader" #include #include #include #include #include #include namespace de { DENG2_PIMPL(GLShaderBank) { struct Source : public ISource { /// Information about a shader source. struct ShaderSource { String source; enum Type { FilePath, ShaderSourceText }; Type type; ShaderSource(String const &str = "", Type t = ShaderSourceText) : source(str), type(t) {} void convertToSourceText() { if(type == FilePath) { source = String::fromLatin1(Block(App::rootFolder().locate(source))); type = ShaderSourceText; } } void insertFromFile(String const &path) { convertToSourceText(); source += "\n"; Block combo = GLShader::prefixToSource(source.toLatin1(), Block(App::rootFolder().locate(path))); source = String::fromLatin1(combo); } }; GLShaderBank &bank; ShaderSource vertex; ShaderSource fragment; Source(GLShaderBank &b, ShaderSource const &vtx, ShaderSource const &frag) : bank(b), vertex(vtx), fragment(frag) {} Time sourceModifiedAt(ShaderSource const &src) const { if(src.type == ShaderSource::FilePath && !src.source.isEmpty()) { return App::rootFolder().locate(src.source).status().modifiedAt; } return bank.sourceModifiedAt(); } Time modifiedAt() const { Time vtxTime = sourceModifiedAt(vertex); Time fragTime = sourceModifiedAt(fragment); return de::max(vtxTime, fragTime); } GLShader *load(GLShader::Type type) const { ShaderSource const &src = (type == GLShader::Vertex? vertex : fragment); if(src.type == ShaderSource::FilePath) { return bank.d->findShader(src.source, type); } // The program will hold the only ref to this shader. return refless(new GLShader(type, src.source.toLatin1())); } }; struct Data : public IData { GLShader *vertex; GLShader *fragment; Data(GLShader *v, GLShader *f) : vertex(holdRef(v)), fragment(holdRef(f)) {} ~Data() { releaseRef(vertex); releaseRef(fragment); } }; typedef QMap Shaders; // path -> shader Shaders shaders; //String relativeToPath; Instance(Public *i) : Base(i) {} ~Instance() { clearShaders(); } void clearShaders() { // Release all of our references to the shaders. foreach(GLShader *shader, shaders.values()) { shader->release(); } shaders.clear(); } GLShader *findShader(String const &path, GLShader::Type type) { /// @todo Should check the modification time of the file to determine /// if recompiling the shader is appropriate. if(shaders.contains(path)) { return shaders[path]; } // We don't have this one yet, load and compile it now. GLShader *shader = new GLShader(type, App::rootFolder().locate(path)); shaders.insert(path, shader); return shader; } }; GLShaderBank::GLShaderBank() : InfoBank("GLShaderBank"), d(new Instance(this)) {} void GLShaderBank::addFromInfo(File const &file) { LOG_AS("GLShaderBank"); //d->relativeToPath = file.path().fileNamePath(); parse(file); addFromInfoBlocks("shader"); } GLShader &GLShaderBank::shader(DotPath const &path, GLShader::Type type) const { Instance::Data &i = data(path).as(); if(type == GLShader::Vertex) { return *i.vertex; } else { return *i.fragment; } } GLProgram &GLShaderBank::build(GLProgram &program, DotPath const &path) const { Instance::Data &i = data(path).as(); program.build(i.vertex, i.fragment); return program; } Bank::ISource *GLShaderBank::newSourceFromInfo(String const &id) { typedef Instance::Source Source; typedef Instance::Source::ShaderSource ShaderSource; Record const &def = info()[id]; ShaderSource vtx; ShaderSource frag; // Vertex shader definition. if(def.has("vertex")) { vtx = ShaderSource(def["vertex"], ShaderSource::ShaderSourceText); } else if(def.has("path.vertex")) { vtx = ShaderSource(relativeToPath(def) / def["path.vertex"], ShaderSource::FilePath); } else if(def.has("path")) { vtx = ShaderSource(relativeToPath(def) / def["path"] + ".vsh", ShaderSource::FilePath); } // Fragment shader definition. if(def.has("fragment")) { frag = ShaderSource(def["fragment"], ShaderSource::ShaderSourceText); } else if(def.has("path.fragment")) { frag = ShaderSource(relativeToPath(def) / def["path.fragment"], ShaderSource::FilePath); } else if(def.has("path")) { frag = ShaderSource(relativeToPath(def) / def["path"] + ".fsh", ShaderSource::FilePath); } // Additional shaders to append to the main source. if(def.has("include.vertex")) { DENG2_FOR_EACH_CONST(ArrayValue::Elements, i, def["include.vertex"].value().as().elements()) { vtx.insertFromFile(relativeToPath(def) / (*i)->asText()); } } if(def.has("include.fragment")) { DENG2_FOR_EACH_CONST(ArrayValue::Elements, i, def["include.fragment"].value().as().elements()) { frag.insertFromFile(relativeToPath(def) / (*i)->asText()); } } return new Source(*this, vtx, frag); } Bank::IData *GLShaderBank::loadFromSource(ISource &source) { Instance::Source &src = source.as(); return new Instance::Data(src.load(GLShader::Vertex), src.load(GLShader::Fragment)); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/modelbank.cpp0000664000175000017500000000446212641367671024324 0ustar jaakkojaakko/** @file modelbank.cpp Bank containing 3D models. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/ModelBank" #include namespace de { DENG2_PIMPL(ModelBank) { /// Source information for loading a model. struct Source : public ISource { String path; ///< Path to a model file. Source(String const &sourcePath) : path(sourcePath) {} }; /// Loaded model instance. struct Data : public IData { ModelDrawable model; std::unique_ptr userData; Data(String const &path) { model.load(App::rootFolder().locate(path)); } }; Instance(Public *i) : Base(i) {} }; ModelBank::ModelBank() : Bank("ModelBank", BackgroundThread) {} void ModelBank::add(DotPath const &id, String const &sourcePath) { return Bank::add(id, new Instance::Source(sourcePath)); } ModelDrawable &ModelBank::model(DotPath const &id) { return data(id).as().model; } void ModelBank::setUserData(DotPath const &id, IUserData *anim) { data(id).as().userData.reset(anim); } ModelBank::IUserData const *ModelBank::userData(DotPath const &id) const { return data(id).as().userData.get(); } ModelBank::ModelWithData ModelBank::modelAndData(DotPath const &id) { auto &item = data(id).as(); return ModelWithData(&item.model, item.userData.get()); } Bank::IData *ModelBank::loadFromSource(ISource &source) { return new Instance::Data(source.as().path); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/glbuffer.cpp0000664000175000017500000003401112641367671024155 0ustar jaakkojaakko/** @file glbuffer.cpp GL vertex buffer. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GLBuffer" #include "de/GLState" #include "de/GLTarget" #include "de/GLProgram" #include "de/GLInfo" namespace de { using namespace internal; using namespace gl; // Vertex Format Layout ------------------------------------------------------ AttribSpec const Vertex2Tex::_spec[2] = { { AttribSpec::Position, 2, GL_FLOAT, false, sizeof(Vertex2Tex), 0 }, { AttribSpec::TexCoord0, 2, GL_FLOAT, false, sizeof(Vertex2Tex), 2 * sizeof(float) } }; LIBGUI_VERTEX_FORMAT_SPEC(Vertex2Tex, 4 * sizeof(float)) AttribSpec const Vertex2Rgba::_spec[2] = { { AttribSpec::Position, 2, GL_FLOAT, false, sizeof(Vertex2Rgba), 0 }, { AttribSpec::Color, 4, GL_FLOAT, false, sizeof(Vertex2Rgba), 2 * sizeof(float) } }; LIBGUI_VERTEX_FORMAT_SPEC(Vertex2Rgba, 6 * sizeof(float)) AttribSpec const Vertex2TexRgba::_spec[3] = { { AttribSpec::Position, 2, GL_FLOAT, false, sizeof(Vertex2TexRgba), 0 }, { AttribSpec::TexCoord0, 2, GL_FLOAT, false, sizeof(Vertex2TexRgba), 2 * sizeof(float) }, { AttribSpec::Color, 4, GL_FLOAT, false, sizeof(Vertex2TexRgba), 4 * sizeof(float) } }; LIBGUI_VERTEX_FORMAT_SPEC(Vertex2TexRgba, 8 * sizeof(float)) AttribSpec const Vertex3::_spec[1] = { { AttribSpec::Position, 3, GL_FLOAT, false, sizeof(Vertex3), 0 }, }; LIBGUI_VERTEX_FORMAT_SPEC(Vertex3, 3 * sizeof(float)) AttribSpec const Vertex3Tex::_spec[2] = { { AttribSpec::Position, 3, GL_FLOAT, false, sizeof(Vertex3Tex), 0 }, { AttribSpec::TexCoord0, 2, GL_FLOAT, false, sizeof(Vertex3Tex), 3 * sizeof(float) } }; LIBGUI_VERTEX_FORMAT_SPEC(Vertex3Tex, 5 * sizeof(float)) AttribSpec const Vertex3TexRgba::_spec[3] = { { AttribSpec::Position, 3, GL_FLOAT, false, sizeof(Vertex3TexRgba), 0 }, { AttribSpec::TexCoord0, 2, GL_FLOAT, false, sizeof(Vertex3TexRgba), 3 * sizeof(float) }, { AttribSpec::Color, 4, GL_FLOAT, false, sizeof(Vertex3TexRgba), 5 * sizeof(float) } }; LIBGUI_VERTEX_FORMAT_SPEC(Vertex3TexRgba, 9 * sizeof(float)) AttribSpec const Vertex3TexBoundsRgba::_spec[4] = { { AttribSpec::Position, 3, GL_FLOAT, false, sizeof(Vertex3TexBoundsRgba), 0 }, { AttribSpec::TexCoord0, 2, GL_FLOAT, false, sizeof(Vertex3TexBoundsRgba), 3 * sizeof(float) }, { AttribSpec::TexBounds0, 4, GL_FLOAT, false, sizeof(Vertex3TexBoundsRgba), 5 * sizeof(float) }, { AttribSpec::Color, 4, GL_FLOAT, false, sizeof(Vertex3TexBoundsRgba), 9 * sizeof(float) } }; LIBGUI_VERTEX_FORMAT_SPEC(Vertex3TexBoundsRgba, 13 * sizeof(float)) AttribSpec const Vertex3Tex2BoundsRgba::_spec[5] = { { AttribSpec::Position, 3, GL_FLOAT, false, sizeof(Vertex3Tex2BoundsRgba), 0 }, { AttribSpec::TexCoord0, 2, GL_FLOAT, false, sizeof(Vertex3Tex2BoundsRgba), 3 * sizeof(float) }, { AttribSpec::TexCoord1, 2, GL_FLOAT, false, sizeof(Vertex3Tex2BoundsRgba), 5 * sizeof(float) }, { AttribSpec::TexBounds0, 4, GL_FLOAT, false, sizeof(Vertex3Tex2BoundsRgba), 7 * sizeof(float) }, { AttribSpec::Color, 4, GL_FLOAT, false, sizeof(Vertex3Tex2BoundsRgba), 11 * sizeof(float) } }; LIBGUI_VERTEX_FORMAT_SPEC(Vertex3Tex2BoundsRgba, 15 * sizeof(float)) AttribSpec const Vertex3Tex2Rgba::_spec[4] = { { AttribSpec::Position, 3, GL_FLOAT, false, sizeof(Vertex3Tex2Rgba), 0 }, { AttribSpec::TexCoord0, 2, GL_FLOAT, false, sizeof(Vertex3Tex2Rgba), 3 * sizeof(float) }, { AttribSpec::TexCoord1, 2, GL_FLOAT, false, sizeof(Vertex3Tex2Rgba), 5 * sizeof(float) }, { AttribSpec::Color, 4, GL_FLOAT, false, sizeof(Vertex3Tex2Rgba), 7 * sizeof(float) } }; LIBGUI_VERTEX_FORMAT_SPEC(Vertex3Tex2Rgba, 11 * sizeof(float)) AttribSpec const Vertex3Tex3Rgba::_spec[5] = { { AttribSpec::Position, 3, GL_FLOAT, false, sizeof(Vertex3Tex3Rgba), 0 }, { AttribSpec::TexCoord0, 2, GL_FLOAT, false, sizeof(Vertex3Tex3Rgba), 3 * sizeof(float) }, { AttribSpec::TexCoord1, 2, GL_FLOAT, false, sizeof(Vertex3Tex3Rgba), 5 * sizeof(float) }, { AttribSpec::TexCoord2, 2, GL_FLOAT, false, sizeof(Vertex3Tex3Rgba), 7 * sizeof(float) }, { AttribSpec::Color, 4, GL_FLOAT, false, sizeof(Vertex3Tex3Rgba), 9 * sizeof(float) } }; LIBGUI_VERTEX_FORMAT_SPEC(Vertex3Tex3Rgba, 13 * sizeof(float)) AttribSpec const Vertex3NormalTexRgba::_spec[4] = { { AttribSpec::Position, 3, GL_FLOAT, false, sizeof(Vertex3NormalTexRgba), 0 }, { AttribSpec::Normal, 3, GL_FLOAT, false, sizeof(Vertex3NormalTexRgba), 3 * sizeof(float) }, { AttribSpec::TexCoord0, 2, GL_FLOAT, false, sizeof(Vertex3NormalTexRgba), 6 * sizeof(float) }, { AttribSpec::Color, 4, GL_FLOAT, false, sizeof(Vertex3NormalTexRgba), 8 * sizeof(float) } }; LIBGUI_VERTEX_FORMAT_SPEC(Vertex3NormalTexRgba, 12 * sizeof(float)) AttribSpec const Vertex3NormalTangentTex::_spec[5] = { { AttribSpec::Position, 3, GL_FLOAT, false, sizeof(Vertex3NormalTangentTex), 0 }, { AttribSpec::Normal, 3, GL_FLOAT, false, sizeof(Vertex3NormalTangentTex), 3 * sizeof(float) }, { AttribSpec::Tangent, 3, GL_FLOAT, false, sizeof(Vertex3NormalTangentTex), 6 * sizeof(float) }, { AttribSpec::Bitangent, 3, GL_FLOAT, false, sizeof(Vertex3NormalTangentTex), 9 * sizeof(float) }, { AttribSpec::TexCoord0, 2, GL_FLOAT, false, sizeof(Vertex3NormalTangentTex), 12 * sizeof(float) } }; LIBGUI_VERTEX_FORMAT_SPEC(Vertex3NormalTangentTex, 14 * sizeof(float)) //---------------------------------------------------------------------------- DENG2_PIMPL(GLBuffer) { GLuint name; GLuint idxName; dsize count; dsize idxCount; Primitive prim; AttribSpecs specs; Instance(Public *i) : Base(i), name(0), idxName(0), count(0), idxCount(0), prim(Points) { specs.first = 0; specs.second = 0; } ~Instance() { release(); releaseIndices(); } void alloc() { if(!name) { glGenBuffers(1, &name); } } void allocIndices() { if(!idxName) { glGenBuffers(1, &idxName); } } void release() { if(name) { glDeleteBuffers(1, &name); name = 0; count = 0; } } void releaseIndices() { if(idxName) { glDeleteBuffers(1, &idxName); idxName = 0; idxCount = 0; } } static GLenum glUsage(Usage u) { switch(u) { case Static: return GL_STATIC_DRAW; case Dynamic: return GL_DYNAMIC_DRAW; case Stream: return GL_STREAM_DRAW; } DENG2_ASSERT(false); return GL_STATIC_DRAW; } static GLenum glPrimitive(Primitive p) { switch(p) { case Points: return GL_POINTS; case LineStrip: return GL_LINE_STRIP; case LineLoop: return GL_LINE_LOOP; case Lines: return GL_LINES; case TriangleStrip: return GL_TRIANGLE_STRIP; case TriangleFan: return GL_TRIANGLE_FAN; case Triangles: return GL_TRIANGLES; } DENG2_ASSERT(false); return GL_TRIANGLES; } void setAttribPointer(GLuint index, AttribSpec const &spec, int divisor, int part = 0) const { DENG2_ASSERT(!part || spec.type == GL_FLOAT); glEnableVertexAttribArray(index + part); LIBGUI_ASSERT_GL_OK(); glVertexAttribPointer(index + part, min(4, spec.size), spec.type, spec.normalized, spec.stride, (void const *) dintptr(spec.startOffset + part * 4 * sizeof(float))); LIBGUI_ASSERT_GL_OK(); #ifdef GL_ARB_instanced_arrays if(GLInfo::extensions().ARB_instanced_arrays) { glVertexAttribDivisorARB(index + part, divisor); LIBGUI_ASSERT_GL_OK(); } #endif } void enableArrays(bool enable, int divisor = 0) const { DENG2_ASSERT(GLProgram::programInUse()); DENG2_ASSERT(specs.first != 0); // must have a spec for(duint i = 0; i < specs.second; ++i) { AttribSpec const &spec = specs.first[i]; int index = GLProgram::programInUse()->attributeLocation(spec.semantic); if(index < 0) continue; // Not used. if(spec.size == 16) { // Attributes with more than 4 elements must be broken down. for(int part = 0; part < 4; ++part) { if(enable) setAttribPointer(index, spec, divisor, part); else { glDisableVertexAttribArray(index + part); LIBGUI_ASSERT_GL_OK(); } } } else { if(enable) setAttribPointer(index, spec, divisor); else { glDisableVertexAttribArray(index); LIBGUI_ASSERT_GL_OK(); } } } } }; GLBuffer::GLBuffer() : d(new Instance(this)) {} void GLBuffer::clear() { setState(NotReady); d->release(); d->releaseIndices(); } void GLBuffer::setVertices(dsize count, void const *data, dsize dataSize, Usage usage) { setVertices(Points, count, data, dataSize, usage); } void GLBuffer::setVertices(Primitive primitive, dsize count, void const *data, dsize dataSize, Usage usage) { d->prim = primitive; d->count = count; if(data) { d->alloc(); if(dataSize && count) { glBindBuffer(GL_ARRAY_BUFFER, d->name); glBufferData(GL_ARRAY_BUFFER, dataSize, data, Instance::glUsage(usage)); glBindBuffer(GL_ARRAY_BUFFER, 0); } setState(Ready); } else { d->release(); setState(NotReady); } } void GLBuffer::setIndices(Primitive primitive, dsize count, Index const *indices, Usage usage) { d->prim = primitive; d->idxCount = count; if(indices && count) { d->allocIndices(); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, d->idxName); glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(Index), indices, Instance::glUsage(usage)); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); } else { d->releaseIndices(); } } void GLBuffer::setIndices(Primitive primitive, Indices const &indices, Usage usage) { setIndices(primitive, indices.size(), indices.constData(), usage); } void GLBuffer::draw(duint first, dint count) const { if(!isReady() || !GLProgram::programInUse()) return; // Mark the current target changed. GLState::current().target().markAsChanged(); glBindBuffer(GL_ARRAY_BUFFER, d->name); d->enableArrays(true); glBindBuffer(GL_ARRAY_BUFFER, 0); if(d->idxName) { if(count < 0) count = d->idxCount; if(first + count > d->idxCount) count = d->idxCount - first; DENG2_ASSERT(count >= 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, d->idxName); glDrawElements(Instance::glPrimitive(d->prim), count, GL_UNSIGNED_SHORT, (void const *) dintptr(first * 2)); LIBGUI_ASSERT_GL_OK(); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); } else { if(count < 0) count = d->count; if(first + count > d->count) count = d->count - first; DENG2_ASSERT(count >= 0); glDrawArrays(Instance::glPrimitive(d->prim), first, count); LIBGUI_ASSERT_GL_OK(); } d->enableArrays(false); } void GLBuffer::drawInstanced(GLBuffer const &instanceAttribs, duint first, dint count) const { if(!GLInfo::extensions().ARB_draw_instanced || !GLInfo::extensions().ARB_instanced_arrays) return; if(!isReady() || !instanceAttribs.isReady() || !GLProgram::programInUse()) return; #if defined(GL_ARB_instanced_arrays) && defined(GL_ARB_draw_instanced) // Mark the current target changed. GLState::current().target().markAsChanged(); glBindBuffer(GL_ARRAY_BUFFER, d->name); d->enableArrays(true); // Set up the instance data. glBindBuffer(GL_ARRAY_BUFFER, instanceAttribs.d->name); instanceAttribs.d->enableArrays(true, 1 /* per instance */); glBindBuffer(GL_ARRAY_BUFFER, 0); if(d->idxName) { if(count < 0) count = d->idxCount; if(first + count > d->idxCount) count = d->idxCount - first; DENG2_ASSERT(count >= 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, d->idxName); glDrawElementsInstancedARB(Instance::glPrimitive(d->prim), count, GL_UNSIGNED_SHORT, (void const *) dintptr(first * 2), instanceAttribs.count()); LIBGUI_ASSERT_GL_OK(); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); } else { if(count < 0) count = d->count; if(first + count > d->count) count = d->count - first; DENG2_ASSERT(count >= 0); glDrawArraysInstancedARB(Instance::glPrimitive(d->prim), first, count, instanceAttribs.count()); LIBGUI_ASSERT_GL_OK(); } d->enableArrays(false); instanceAttribs.d->enableArrays(false); #endif } dsize GLBuffer::count() const { return d->count; } void GLBuffer::setFormat(AttribSpecs const &format) { d->specs = format; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/kdtreeatlasallocator.cpp0000664000175000017500000002007412641367671026571 0ustar jaakkojaakko/** @file kdtreeatlasallocator.cpp KD-tree based atlas allocator. * * @authors Copyright (c) 2013 Jaakko Keränen * @authors Copyright (c) 2009-2010 Daniel Swanson * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/KdTreeAtlasAllocator" #include #include namespace de { DENG2_PIMPL(KdTreeAtlasAllocator) { Atlas::Size size; int margin; Allocations allocs; struct Partition { Rectanglei area; Id alloc; ///< Id of the allocation in this partition (or Id::None). Partition() : alloc(Id::None) {} }; typedef BinaryTree Node; Node root; Instance(Public *i) : Base(i), margin(0), root(Partition()) {} void initTree(Node &rootNode) { Partition full; full.area = Rectanglei(margin, margin, size.x - margin, size.y - margin); rootNode.setUserData(full); } Node *treeInsert(Node *parent, Atlas::Size const &allocSize) { if(!parent->isLeaf()) { Node *subTree; // Try to insert into the right subtree. if((subTree = treeInsert(parent->rightPtr(), allocSize)) != 0) return subTree; // Try to insert into the left subtree. return treeInsert(parent->leftPtr(), allocSize); } /* * We have arrived at a leaf. */ Partition pnode = parent->userData(); // Is this leaf already in use? if(!pnode.alloc.isNone()) return 0; // Is this leaf big enough to hold the texture? if(pnode.area.width() < allocSize.x || pnode.area.height() < allocSize.y) return 0; // No, too small. // Is this leaf the exact size required? if(pnode.area.size() == allocSize) return parent; /* * The leaf will be split. */ // Create right and left subtrees. Partition rnode, lnode; // Are we splitting horizontally or vertically? if(pnode.area.width() - allocSize.x > pnode.area.height() - allocSize.y) { // Horizontal split. rnode.area = Rectanglei(pnode.area.left(), pnode.area.top(), allocSize.x, pnode.area.height()); lnode.area = Rectanglei(pnode.area.left() + allocSize.x, pnode.area.top(), pnode.area.width() - allocSize.x, pnode.area.height()); } else { // Vertical split. rnode.area = Rectanglei(pnode.area.left(), pnode.area.top(), pnode.area.width(), allocSize.y); lnode.area = Rectanglei(pnode.area.left(), pnode.area.top() + allocSize.y, pnode.area.width(), pnode.area.height() - allocSize.y); } parent->setRight(new Node(rnode, parent)); parent->setLeft(new Node(lnode, parent)); // Decend to the right node. return treeInsert(parent->rightPtr(), allocSize); } /** * Attempts to find a large enough free space for the requested size. * Room is left for margins. * * @param rootNode Partitioning root node to use for allocation. * @param size Size to allocate. * @param rect Found rectangle is returned here. * @param preallocId Id for the allocation, if one has already been determined. * * @return Allocated Id, if successful. */ Id allocate(Node &rootNode, Atlas::Size const &size, Rectanglei &rect, Id const &preallocId = 0) { // Margin is included only in the bottom/right edges. Atlas::Size const allocSize(size.x + margin, size.y + margin); if(Node *inserted = treeInsert(&rootNode, allocSize)) { // Got it, give it a new Id. Partition part = inserted->userData(); part.alloc = (preallocId.isNone()? Id() : preallocId); inserted->setUserData(part); // Remove the margin for the actual allocated rectangle. rect = part.area.adjusted(Vector2i(), Vector2i(-margin, -margin)); return part.alloc; } return Id::None; } struct EraseArgs { Id id; }; static int allocationEraser(Node &node, void *argsPtr) { EraseArgs *args = reinterpret_cast(argsPtr); Partition part = node.userData(); if(part.alloc == args->id) { part.alloc = Id::None; node.setUserData(part); return 1; // Can stop here. } return 0; } void releaseAlloc(Id const &id) { allocs.remove(id); EraseArgs args; args.id = id; root.traverseInOrder(allocationEraser, &args); } struct ContentSize { Id::Type id; duint64 area; ContentSize(Id const &allocId, Vector2ui const &size) : id(allocId), area(size.x * size.y) {} // Sort descending. bool operator < (ContentSize const &other) const { return area > other.area; } }; bool optimize() { // Set up a LUT based on descending allocation width. QList descending; DENG2_FOR_EACH(Allocations, i, allocs) { descending.append(ContentSize(i.key(), i.value().size())); } qSort(descending); Allocations optimal; Node optimalRoot; initTree(optimalRoot); /* * Attempt to optimize space usage by placing the largest allocations * first. */ foreach(ContentSize const &iter, descending) { Rectanglei newRect; if(allocate(optimalRoot, allocs[iter.id].size(), newRect, iter.id).isNone()) { // Could not find a place for this any more. return false; } // This'll do. optimal.insert(iter.id, newRect); } // Use the new layout. root = optimalRoot; allocs = optimal; return true; } }; KdTreeAtlasAllocator::KdTreeAtlasAllocator() : d(new Instance(this)) {} void KdTreeAtlasAllocator::setMetrics(Atlas::Size const &totalSize, int margin) { DENG2_ASSERT(d->allocs.isEmpty()); d->size = totalSize; d->margin = margin; d->initTree(d->root); } void KdTreeAtlasAllocator::clear() { d->allocs.clear(); d->root.clear(); } Id KdTreeAtlasAllocator::allocate(Atlas::Size const &size, Rectanglei &rect) { Id newId = d->allocate(d->root, size, rect); if(newId.isNone()) { // No large enough free space available. return 0; } // Map it for quick access. d->allocs[newId] = rect; return newId; } void KdTreeAtlasAllocator::release(Id const &id) { DENG2_ASSERT(d->allocs.contains(id)); d->releaseAlloc(id); } int KdTreeAtlasAllocator::count() const { return d->allocs.size(); } Atlas::Ids KdTreeAtlasAllocator::ids() const { Atlas::Ids ids; foreach(Id const &id, d->allocs.keys()) { ids.insert(id); } return ids; } void KdTreeAtlasAllocator::rect(Id const &id, Rectanglei &rect) const { DENG2_ASSERT(d->allocs.contains(id)); rect = d->allocs[id]; } KdTreeAtlasAllocator::Allocations KdTreeAtlasAllocator::allocs() const { return d->allocs; } bool KdTreeAtlasAllocator::optimize() { return d->optimize(); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/gltexture.cpp0000664000175000017500000003113412641367671024407 0ustar jaakkojaakko/** @file gltexture.cpp GL texture atlas. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GLTexture" #include "de/GLInfo" #include "de/graphics/opengl.h" namespace de { namespace internal { enum TextureFlag { AutoMips = 0x1, MipmapAvailable = 0x2, ParamsChanged = 0x4 }; Q_DECLARE_FLAGS(TextureFlags, TextureFlag) } Q_DECLARE_OPERATORS_FOR_FLAGS(internal::TextureFlags) using namespace internal; using namespace gl; DENG2_PIMPL(GLTexture) { Size size; Image::Format format; GLuint name; GLenum texTarget; Filter minFilter; Filter magFilter; MipFilter mipFilter; Wraps wrap; dfloat maxAnisotropy; dfloat maxLevel; TextureFlags flags; Instance(Public *i) : Base(i) , format(Image::Unknown) , name(0) , texTarget(GL_TEXTURE_2D) , minFilter(Linear), magFilter(Linear), mipFilter(MipNone) , wrap(Wraps(Repeat, Repeat)) , maxAnisotropy(1.0f) , maxLevel(1000.f) , flags(ParamsChanged) {} ~Instance() { release(); } void alloc() { if(!name) { glGenTextures(1, &name); } } void release() { if(name) { glDeleteTextures(1, &name); name = 0; } } void clear() { release(); size = Size(); texTarget = GL_TEXTURE_2D; flags |= ParamsChanged; self.setState(NotReady); } bool isCube() const { return texTarget == GL_TEXTURE_CUBE_MAP; } static GLenum glWrap(gl::Wrapping w) { switch(w) { case Repeat: return GL_REPEAT; case RepeatMirrored: return GL_MIRRORED_REPEAT; case ClampToEdge: return GL_CLAMP_TO_EDGE; } return GL_REPEAT; } static GLenum glMinFilter(gl::Filter min, gl::MipFilter mip) { if(mip == MipNone) { if(min == Nearest) return GL_NEAREST; if(min == Linear) return GL_LINEAR; } else if(mip == MipNearest) { if(min == Nearest) return GL_NEAREST_MIPMAP_NEAREST; if(min == Linear) return GL_LINEAR_MIPMAP_NEAREST; } else // MipLinear { if(min == Nearest) return GL_NEAREST_MIPMAP_LINEAR; if(min == Linear) return GL_LINEAR_MIPMAP_LINEAR; } return GL_NEAREST; } static GLenum glFace(gl::CubeFace face) { switch(face) { case PositiveX: return GL_TEXTURE_CUBE_MAP_POSITIVE_X; case PositiveY: return GL_TEXTURE_CUBE_MAP_POSITIVE_Y; case PositiveZ: return GL_TEXTURE_CUBE_MAP_POSITIVE_Z; case NegativeX: return GL_TEXTURE_CUBE_MAP_NEGATIVE_X; case NegativeY: return GL_TEXTURE_CUBE_MAP_NEGATIVE_Y; case NegativeZ: return GL_TEXTURE_CUBE_MAP_NEGATIVE_Z; } return GL_TEXTURE_CUBE_MAP_POSITIVE_X; } void glBind() const { DENG2_ASSERT(name != 0); glBindTexture(texTarget, name); LIBGUI_ASSERT_GL_OK(); } void glUnbind() const { glBindTexture(texTarget, 0); } /** * Update the OpenGL texture parameters. You must bind the texture before * calling. */ void glUpdateParamsOfBoundTexture() { glTexParameteri(texTarget, GL_TEXTURE_WRAP_S, glWrap(wrap.x)); glTexParameteri(texTarget, GL_TEXTURE_WRAP_T, glWrap(wrap.y)); glTexParameteri(texTarget, GL_TEXTURE_MAG_FILTER, magFilter == Nearest? GL_NEAREST : GL_LINEAR); glTexParameteri(texTarget, GL_TEXTURE_MIN_FILTER, glMinFilter(minFilter, mipFilter)); glTexParameterf(texTarget, GL_TEXTURE_MAX_LEVEL, maxLevel); if(GLInfo::extensions().EXT_texture_filter_anisotropic) { glTexParameterf(texTarget, GL_TEXTURE_MAX_ANISOTROPY_EXT, maxAnisotropy); } LIBGUI_ASSERT_GL_OK(); flags &= ~ParamsChanged; } void glImage(int level, Size const &size, GLPixelFormat const &glFormat, void const *data, CubeFace face = PositiveX) { /// @todo GLES2: Check for the BGRA extension. // Choose suitable informal format. GLenum const internalFormat = (glFormat.format == GL_BGRA? GL_RGBA : glFormat.format == GL_DEPTH_STENCIL? GL_DEPTH24_STENCIL8 : glFormat.format); /*qDebug() << "glTexImage2D:" << name << (isCube()? glFace(face) : texTarget) << level << internalFormat << size.x << size.y << 0 << glFormat.format << glFormat.type << data;*/ if(data) glPixelStorei(GL_UNPACK_ALIGNMENT, glFormat.rowAlignment); glTexImage2D(isCube()? glFace(face) : texTarget, level, internalFormat, size.x, size.y, 0, glFormat.format, glFormat.type, data); LIBGUI_ASSERT_GL_OK(); } void glSubImage(int level, Vector2i const &pos, Size const &size, GLPixelFormat const &glFormat, void const *data, CubeFace face = PositiveX) { if(data) glPixelStorei(GL_UNPACK_ALIGNMENT, glFormat.rowAlignment); glTexSubImage2D(isCube()? glFace(face) : texTarget, level, pos.x, pos.y, size.x, size.y, glFormat.format, glFormat.type, data); LIBGUI_ASSERT_GL_OK(); } }; GLTexture::GLTexture() : d(new Instance(this)) {} GLTexture::GLTexture(GLuint existingTexture, Size const &size) : d(new Instance(this)) { d->size = size; d->name = existingTexture; d->flags |= ParamsChanged; } void GLTexture::clear() { d->clear(); } void GLTexture::setMagFilter(Filter magFilter) { d->magFilter = magFilter; d->flags |= ParamsChanged; } void GLTexture::setMinFilter(Filter minFilter, MipFilter mipFilter) { d->minFilter = minFilter; d->mipFilter = mipFilter; d->flags |= ParamsChanged; } void GLTexture::setWrapS(Wrapping mode) { d->wrap.x = mode; d->flags |= ParamsChanged; } void GLTexture::setWrapT(Wrapping mode) { d->wrap.y = mode; d->flags |= ParamsChanged; } void GLTexture::setMaxAnisotropy(dfloat maxAnisotropy) { d->maxAnisotropy = maxAnisotropy; d->flags |= ParamsChanged; } void GLTexture::setMaxLevel(dfloat maxLevel) { d->maxLevel = maxLevel; d->flags |= ParamsChanged; } Filter GLTexture::minFilter() const { return d->minFilter; } Filter GLTexture::magFilter() const { return d->magFilter; } MipFilter GLTexture::mipFilter() const { return d->mipFilter; } Wrapping GLTexture::wrapS() const { return d->wrap.x; } Wrapping GLTexture::wrapT() const { return d->wrap.y; } GLTexture::Wraps GLTexture::wrap() const { return d->wrap; } dfloat GLTexture::maxAnisotropy() const { return d->maxAnisotropy; } dfloat GLTexture::maxLevel() const { return d->maxLevel; } bool GLTexture::isCubeMap() const { return d->isCube(); } void GLTexture::setAutoGenMips(bool genMips) { if(genMips) d->flags |= AutoMips; else d->flags &= ~AutoMips; } bool GLTexture::autoGenMips() const { return d->flags.testFlag(AutoMips); } void GLTexture::setUndefinedImage(GLTexture::Size const &size, Image::Format format, int level) { d->texTarget = GL_TEXTURE_2D; d->size = size; d->format = format; d->alloc(); d->glBind(); d->glImage(level, size, Image::glFormat(format), NULL); d->glUnbind(); setState(Ready); } void GLTexture::setUndefinedImage(CubeFace face, GLTexture::Size const &size, Image::Format format, int level) { d->texTarget = GL_TEXTURE_CUBE_MAP; d->size = size; d->format = format; d->alloc(); d->glBind(); d->glImage(level, size, Image::glFormat(format), NULL, face); d->glUnbind(); setState(Ready); } void GLTexture::setUndefinedContent(Size const &size, GLPixelFormat const &glFormat, int level) { d->texTarget = GL_TEXTURE_2D; d->size = size; d->format = Image::Unknown; d->alloc(); d->glBind(); d->glImage(level, size, glFormat, NULL); d->glUnbind(); setState(Ready); } void GLTexture::setUndefinedContent(CubeFace face, Size const &size, GLPixelFormat const &glFormat, int level) { d->texTarget = GL_TEXTURE_CUBE_MAP; d->size = size; d->format = Image::Unknown; d->alloc(); d->glBind(); d->glImage(level, size, glFormat, NULL, face); d->glUnbind(); setState(Ready); } void GLTexture::setDepthStencilContent(Size const &size) { setUndefinedContent(size, GLPixelFormat(GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8)); } void GLTexture::setImage(Image const &image, int level) { d->texTarget = GL_TEXTURE_2D; d->size = image.size(); d->format = image.format(); d->alloc(); d->glBind(); d->glImage(level, image.size(), image.glFormat(), image.bits()); d->glUnbind(); if(!level && d->flags.testFlag(AutoMips)) { generateMipmap(); } setState(Ready); } void GLTexture::setImage(CubeFace face, Image const &image, int level) { d->texTarget = GL_TEXTURE_CUBE_MAP; d->size = image.size(); d->format = image.format(); d->alloc(); d->glBind(); d->glImage(level, image.size(), image.glFormat(), image.bits(), face); d->glUnbind(); if(!level && d->flags.testFlag(AutoMips)) { generateMipmap(); } setState(Ready); } void GLTexture::setSubImage(Image const &image, Vector2i const &pos, int level) { d->texTarget = GL_TEXTURE_2D; d->alloc(); d->glBind(); d->glSubImage(level, pos, image.size(), image.glFormat(), image.bits()); d->glUnbind(); if(!level && d->flags.testFlag(AutoMips)) { generateMipmap(); } } void GLTexture::setSubImage(CubeFace face, Image const &image, Vector2i const &pos, int level) { d->texTarget = GL_TEXTURE_CUBE_MAP; d->alloc(); d->glBind(); d->glSubImage(level, pos, image.size(), image.glFormat(), image.bits(), face); d->glUnbind(); if(!level && d->flags.testFlag(AutoMips)) { generateMipmap(); } } void GLTexture::generateMipmap() { if(d->name) { d->glBind(); glGenerateMipmap(d->texTarget); LIBGUI_ASSERT_GL_OK(); d->glUnbind(); d->flags |= MipmapAvailable; } } GLTexture::Size GLTexture::size() const { return d->size; } int GLTexture::mipLevels() const { if(!isReady()) return 0; return d->flags.testFlag(MipmapAvailable)? levelsForSize(d->size) : 1; } GLTexture::Size GLTexture::levelSize(int level) const { if(level < 0) return Size(); return levelSize(d->size, level); } GLuint GLTexture::glName() const { return d->name; } void GLTexture::glBindToUnit(int unit) const { glActiveTexture(GL_TEXTURE0 + unit); aboutToUse(); d->glBind(); if(d->flags.testFlag(ParamsChanged)) { d->glUpdateParamsOfBoundTexture(); } } void GLTexture::glApplyParameters() { if(d->flags.testFlag(ParamsChanged)) { d->glBind(); d->glUpdateParamsOfBoundTexture(); d->glUnbind(); } } Image::Format GLTexture::imageFormat() const { return d->format; } GLTexture::Size GLTexture::maximumSize() { int v = 0; glGetIntegerv(GL_MAX_TEXTURE_SIZE, &v); LIBGUI_ASSERT_GL_OK(); return Size(v, v); } void GLTexture::aboutToUse() const { // nothing to do } int GLTexture::levelsForSize(GLTexture::Size const &size) { int mipLevels = 0; duint w = size.x; duint h = size.y; while(w > 1 || h > 1) { w = de::max(1u, w >> 1); h = de::max(1u, h >> 1); mipLevels++; } return mipLevels; } GLTexture::Size GLTexture::levelSize(GLTexture::Size const &size0, int level) { Size s = size0; for(int i = 0; i < level; ++i) { s.x = de::max(1u, s.x >> 1); s.y = de::max(1u, s.y >> 1); } return s; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/glshader.cpp0000664000175000017500000001037512641367671024161 0ustar jaakkojaakko/** @file glshader.cpp GL shader. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GLShader" #include "de/GuiApp" #include "de/graphics/opengl.h" #include #include namespace de { DENG2_PIMPL(GLShader) , DENG2_OBSERVES(GuiApp, GLContextChange) { GLuint name; Type type; Block compiledSource; Instance(Public *i) : Base(i), name(0), type(Vertex) { //DENG2_GUI_APP->audienceForGLContextChange += this; } ~Instance() { //DENG2_GUI_APP->audienceForGLContextChange -= this; release(); } void alloc() { if(!name) { name = glCreateShader(type == Vertex? GL_VERTEX_SHADER : GL_FRAGMENT_SHADER); LIBGUI_ASSERT_GL_OK(); if(!name) { throw AllocError("GLShader::alloc", "Failed to create shader"); } } } void release() { if(name) { glDeleteShader(name); name = 0; } self.setState(Asset::NotReady); } void appGLContextChanged() { /* qDebug() << "Recompiling shader" << name; self.recompile(); */ } }; GLShader::GLShader() : d(new Instance(this)) {} GLShader::GLShader(Type shaderType, IByteArray const &source) : d(new Instance(this)) { try { compile(shaderType, source); } catch(...) { // Construction was aborted. addRef(-1); throw; } } GLShader::Type GLShader::type() const { return d->type; } GLuint GLShader::glName() const { return d->name; } void GLShader::clear() { d->release(); } Block GLShader::prefixToSource(Block const &source, Block const &prefix) { Block src = source; int versionPos = src.indexOf("#version "); if(versionPos >= 0) { // Append prefix after version. int pos = src.indexOf('\n', versionPos); src.insert(pos + 1, prefix); } else { src = prefix + src; } return src; } void GLShader::compile(Type shaderType, IByteArray const &source) { #ifndef LIBGUI_GLES2 // With non-ES OpenGL, ignore the precision attributes. static QByteArray prefix("#ifndef GL_ES\n#define lowp\n#define mediump\n#define highp\n#endif\n"); #endif DENG2_ASSERT(shaderType == Vertex || shaderType == Fragment); setState(NotReady); // Keep a copy of the source for possible recompilation. d->compiledSource = source; d->type = shaderType; d->alloc(); // Prepare the shader source. This would be the time to substitute any // remaining symbols in the shader source. Block src = prefixToSource(source, prefix); char const *srcPtr = src.constData(); glShaderSource(d->name, 1, &srcPtr, 0); glCompileShader(d->name); // Check the compilation status. GLint status; glGetShaderiv(d->name, GL_COMPILE_STATUS, &status); if(!status) { dint32 logSize = 0; dint32 count = 0; glGetShaderiv(d->name, GL_INFO_LOG_LENGTH, &logSize); Block log(logSize); glGetShaderInfoLog(d->name, logSize, &count, reinterpret_cast(log.data())); throw CompilerError("GLShader::compile", "Compilation of " + String(d->type == Fragment? "fragment" : "vertex") + " shader failed:\n" + log); } setState(Ready); } void GLShader::recompile() { d->release(); compile(d->type, d->compiledSource); DENG2_ASSERT(isReady()); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/glstate.cpp0000664000175000017500000004632012641367671024032 0ustar jaakkojaakko/** @file glstate.cpp GL state. * * @todo This implementation assumes OpenGL drawing occurs only in one thread. * If multithreaded rendering is done at some point in the future, the GL state * stack must be part of the thread-local data. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GLState" #include "de/PersistentCanvasWindow" #include "de/graphics/opengl.h" #include namespace de { namespace internal { enum Property { CullMode, DepthTest, DepthFunc, DepthWrite, Blend, BlendFuncSrc, BlendFuncDest, BlendOp, ColorMask, Scissor, ScissorX, ScissorY, ScissorWidth, ScissorHeight, ViewportX, ViewportY, ViewportWidth, ViewportHeight, MAX_PROPERTIES }; static BitField::Spec const propSpecs[MAX_PROPERTIES] = { { CullMode, 2 }, { DepthTest, 1 }, { DepthFunc, 3 }, { DepthWrite, 1 }, { Blend, 1 }, { BlendFuncSrc, 4 }, { BlendFuncDest, 4 }, { BlendOp, 2 }, { ColorMask, 4 }, { Scissor, 1 }, { ScissorX, 13 }, // 13 bits == 8192 max { ScissorY, 13 }, { ScissorWidth, 13 }, { ScissorHeight, 13 }, { ViewportX, 13 }, { ViewportY, 13 }, { ViewportWidth, 13 }, { ViewportHeight, 13 } }; static BitField::Elements const glStateProperties(propSpecs, MAX_PROPERTIES); /// The GL state stack. struct GLStateStack : public QList { GLStateStack() { // Initialize with a default state. append(new GLState); } ~GLStateStack() { qDeleteAll(*this); } }; static GLStateStack stack; /// Currently applied GL state properties. static BitField currentProps; /// Observes the current target and clears the pointer if it happens to get /// deleted. class CurrentTarget : DENG2_OBSERVES(Asset, Deletion) { GLTarget *_target; void assetBeingDeleted(Asset &asset) { if(&asset == _target) { LOG_AS("GLState"); LOGDEV_GL_NOTE("Current target destroyed, clearing pointer"); _target = 0; } } public: CurrentTarget() : _target(0) {} ~CurrentTarget() { set(0); } void set(GLTarget *trg) { if(_target) { _target->audienceForDeletion() -= this; } _target = trg; if(_target) { _target->audienceForDeletion() += this; } } CurrentTarget &operator = (GLTarget *trg) { set(trg); return *this; } bool operator != (GLTarget *trg) const { return _target != trg; } GLTarget *get() const { return _target; } operator GLTarget *() const { return _target; } }; static CurrentTarget currentTarget; } using namespace internal; DENG2_PIMPL(GLState) { BitField props; GLTarget *target; Instance(Public *i) : Base(i) , props(glStateProperties) , target(0) {} Instance(Public *i, Instance const &other) : Base(i) , props(other.props) , target(other.target) {} static GLenum glComp(gl::Comparison comp) { switch(comp) { case gl::Never: return GL_NEVER; case gl::Always: return GL_ALWAYS; case gl::Equal: return GL_EQUAL; case gl::NotEqual: return GL_NOTEQUAL; case gl::Less: return GL_LESS; case gl::Greater: return GL_GREATER; case gl::LessOrEqual: return GL_LEQUAL; case gl::GreaterOrEqual: return GL_GEQUAL; } return GL_NEVER; } static GLenum glBFunc(gl::Blend f) { switch(f) { case gl::Zero: return GL_ZERO; case gl::One: return GL_ONE; case gl::SrcColor: return GL_SRC_COLOR; case gl::OneMinusSrcColor: return GL_ONE_MINUS_SRC_COLOR; case gl::SrcAlpha: return GL_SRC_ALPHA; case gl::OneMinusSrcAlpha: return GL_ONE_MINUS_SRC_ALPHA; case gl::DestColor: return GL_DST_COLOR; case gl::OneMinusDestColor: return GL_ONE_MINUS_DST_COLOR; case gl::DestAlpha: return GL_DST_ALPHA; case gl::OneMinusDestAlpha: return GL_ONE_MINUS_DST_ALPHA; } return GL_ZERO; } static gl::Blend fromGlBFunc(GLenum e) { switch(e) { case GL_ZERO: return gl::Zero; case GL_ONE: return gl::One; case GL_SRC_COLOR: return gl::SrcColor; case GL_ONE_MINUS_SRC_COLOR: return gl::OneMinusSrcColor; case GL_SRC_ALPHA: return gl::SrcAlpha; case GL_ONE_MINUS_SRC_ALPHA: return gl::OneMinusSrcAlpha; case GL_DST_COLOR: return gl::DestColor; case GL_ONE_MINUS_DST_COLOR: return gl::OneMinusDestColor; case GL_DST_ALPHA: return gl::DestAlpha; case GL_ONE_MINUS_DST_ALPHA: return gl::OneMinusDestAlpha; } return gl::Zero; } void glApply(Property prop) { switch(prop) { case CullMode: switch(self.cull()) { case gl::None: glDisable(GL_CULL_FACE); break; case gl::Front: glEnable(GL_CULL_FACE); glCullFace(GL_FRONT); break; case gl::Back: glEnable(GL_CULL_FACE); glCullFace(GL_BACK); break; } break; case DepthTest: if(self.depthTest()) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST); break; case DepthFunc: glDepthFunc(glComp(self.depthFunc())); break; case DepthWrite: if(self.depthWrite()) glDepthMask(GL_TRUE); else glDepthMask(GL_FALSE); break; case Blend: if(self.blend()) glEnable(GL_BLEND); else glDisable(GL_BLEND); break; case BlendFuncSrc: case BlendFuncDest: //glBlendFunc(glBFunc(self.srcBlendFunc()), glBFunc(self.destBlendFunc())); glBlendFuncSeparate(glBFunc(self.srcBlendFunc()), glBFunc(self.destBlendFunc()), GL_ONE, GL_ONE); break; case BlendOp: switch(self.blendOp()) { case gl::Add: glBlendEquation(GL_FUNC_ADD); break; case gl::Subtract: glBlendEquation(GL_FUNC_SUBTRACT); break; case gl::ReverseSubtract: glBlendEquation(GL_FUNC_REVERSE_SUBTRACT); break; } break; case ColorMask: { gl::ColorMask const mask = self.colorMask(); glColorMask((mask & gl::WriteRed) != 0, (mask & gl::WriteGreen) != 0, (mask & gl::WriteBlue) != 0, (mask & gl::WriteAlpha) != 0); break; } case Scissor: case ScissorX: case ScissorY: case ScissorWidth: case ScissorHeight: { if(self.scissor() || self.target().hasActiveRect()) { glEnable(GL_SCISSOR_TEST); Rectangleui origScr; if(self.scissor()) { origScr = self.scissorRect(); } else { origScr = Rectangleui::fromSize(self.target().size()); } Rectangleui const scr = self.target().scaleToActiveRect(origScr); glScissor(scr.left(), self.target().size().y - scr.bottom(), scr.width(), scr.height()); } else { glDisable(GL_SCISSOR_TEST); } break; } case ViewportX: case ViewportY: case ViewportWidth: case ViewportHeight: { Rectangleui const vp = self.target().scaleToActiveRect(self.viewport()); //qDebug() << "glViewport" << vp.asText(); glViewport(vp.left(), self.target().size().y - vp.bottom(), vp.width(), vp.height()); break; } default: break; } LIBGUI_ASSERT_GL_OK(); } void removeRedundancies(BitField::Ids &changed) { if(changed.contains(BlendFuncSrc) && changed.contains(BlendFuncDest)) { changed.remove(BlendFuncDest); } if(changed.contains(ScissorX) || changed.contains(ScissorY) || changed.contains(ScissorWidth) || changed.contains(ScissorHeight)) { changed.insert(ScissorX); changed.remove(ScissorY); changed.remove(ScissorWidth); changed.remove(ScissorHeight); } if(changed.contains(ViewportX) || changed.contains(ViewportY) || changed.contains(ViewportWidth) || changed.contains(ViewportHeight)) { changed.insert(ViewportX); changed.remove(ViewportY); changed.remove(ViewportWidth); changed.remove(ViewportHeight); } } }; GLState::GLState() : d(new Instance(this)) { setCull (gl::None); setDepthTest (false); setDepthFunc (gl::Less); setDepthWrite(true); setBlend (true); setBlendFunc (gl::One, gl::Zero); setBlendOp (gl::Add); setColorMask (gl::WriteAll); setDefaultTarget(); } GLState::GLState(GLState const &other) : d(new Instance(this, *other.d)) {} GLState &GLState::operator = (GLState const &other) { d.reset(new Instance(this, *other.d)); return *this; } GLState &GLState::setCull(gl::Cull mode) { d->props.set(CullMode, duint(mode)); return *this; } GLState &GLState::setDepthTest(bool enable) { d->props.set(DepthTest, enable); return *this; } GLState &GLState::setDepthFunc(gl::Comparison func) { d->props.set(DepthFunc, duint(func)); return *this; } GLState &GLState::setDepthWrite(bool enable) { d->props.set(DepthWrite, enable); return *this; } GLState &GLState::setBlend(bool enable) { d->props.set(Blend, enable); return *this; } GLState &GLState::setBlendFunc(gl::Blend src, gl::Blend dest) { d->props.set(BlendFuncSrc, duint(src)); d->props.set(BlendFuncDest, duint(dest)); return *this; } GLState &GLState::setBlendFunc(gl::BlendFunc func) { d->props.set(BlendFuncSrc, duint(func.first)); d->props.set(BlendFuncDest, duint(func.second)); return *this; } GLState &GLState::setBlendOp(gl::BlendOp op) { d->props.set(BlendOp, duint(op)); return *this; } GLState &GLState::setColorMask(gl::ColorMask mask) { d->props.set(ColorMask, duint(mask)); return *this; } GLState &GLState::setTarget(GLTarget &target) { d->target = ⌖ return *this; } GLState &GLState::setDefaultTarget() { d->target = 0; return *this; } GLState &GLState::setViewport(Rectangleui const &viewportRect) { d->props.set(ViewportX, viewportRect.left()); d->props.set(ViewportY, viewportRect.top()); d->props.set(ViewportWidth, viewportRect.width()); d->props.set(ViewportHeight, viewportRect.height()); return *this; } GLState &GLState::setNormalizedViewport(Rectanglef const &normViewportRect) { GLTarget::Size const size = target().size(); Rectangleui vp(Vector2ui(normViewportRect.left() * size.x, normViewportRect.top() * size.y), Vector2ui(std::ceil(normViewportRect.right() * size.x), std::ceil(normViewportRect.bottom() * size.y))); return setViewport(vp); } GLState &GLState::setScissor(Rectanglei const &scissorRect) { return setScissor(scissorRect.toRectangleui()); } GLState &GLState::setScissor(Rectangleui const &newScissorRect) { Rectangleui cumulative; if(scissor()) { cumulative = scissorRect() & newScissorRect; } else { cumulative = newScissorRect; } d->props.set(Scissor, true); d->props.set(ScissorX, cumulative.left()); d->props.set(ScissorY, cumulative.top()); d->props.set(ScissorWidth, cumulative.width()); d->props.set(ScissorHeight, cumulative.height()); return *this; } GLState &GLState::setNormalizedScissor(Rectanglef const &normScissorRect) { Rectangleui vp = viewport(); Rectanglei scis(Vector2i(normScissorRect.left() * vp.width(), normScissorRect.top() * vp.height()), Vector2i(std::ceil(normScissorRect.right() * vp.width()), std::ceil(normScissorRect.bottom() * vp.height()))); return setScissor(scis); } GLState &GLState::clearScissor() { d->props.set(Scissor, false); d->props.set(ScissorX, 0u); d->props.set(ScissorY, 0u); d->props.set(ScissorWidth, 0u); d->props.set(ScissorHeight, 0u); return *this; } gl::Cull GLState::cull() const { return d->props.valueAs(CullMode); } bool GLState::depthTest() const { return d->props.asBool(DepthTest); } gl::Comparison GLState::depthFunc() const { return d->props.valueAs(DepthFunc); } bool GLState::depthWrite() const { return d->props.asBool(DepthWrite); } bool GLState::blend() const { return d->props.asBool(Blend); } gl::Blend GLState::srcBlendFunc() const { return d->props.valueAs(BlendFuncSrc); } gl::Blend GLState::destBlendFunc() const { return d->props.valueAs(BlendFuncDest); } gl::BlendFunc GLState::blendFunc() const { return gl::BlendFunc(srcBlendFunc(), destBlendFunc()); } gl::BlendOp GLState::blendOp() const { return d->props.valueAs(BlendOp); } gl::ColorMask GLState::colorMask() const { return d->props.valueAs(ColorMask); } GLTarget &GLState::target() const { if(d->target) { return *d->target; } return CanvasWindow::main().canvas().renderTarget(); } Rectangleui GLState::viewport() const { return Rectangleui(d->props[ViewportX], d->props[ViewportY], d->props[ViewportWidth], d->props[ViewportHeight]); } Rectanglef GLState::normalizedViewport() const { GLTarget::Size const size = target().size(); Rectangleui const vp = viewport(); return Rectanglef(float(vp.left()) / float(size.x), float(vp.top()) / float(size.y), float(vp.width()) / float(size.x), float(vp.height()) / float(size.y)); } bool GLState::scissor() const { return d->props.asBool(Scissor); } Rectangleui GLState::scissorRect() const { return Rectangleui(d->props[ScissorX], d->props[ScissorY], d->props[ScissorWidth], d->props[ScissorHeight]); } void GLState::apply() const { LIBGUI_ASSERT_GL_OK(); #ifdef LIBGUI_USE_GLENTRYPOINTS if(!glBindFramebuffer) return; #endif bool forceViewportAndScissor = false; // Update the render target. GLTarget *newTarget = &target(); DENG2_ASSERT(newTarget != 0); if(currentTarget != newTarget) { GLTarget const *oldTarget = currentTarget; if(oldTarget) { oldTarget->glRelease(); } currentTarget = newTarget; currentTarget.get()->glBind(); if((oldTarget && oldTarget->hasActiveRect()) || newTarget->hasActiveRect()) { // We can't trust that the viewport or scissor can remain the same // as the active rectangle may have changed. forceViewportAndScissor = true; } } LIBGUI_ASSERT_GL_OK(); // Determine which properties have changed. BitField::Ids changed; if(currentProps.isEmpty()) { // Apply everything. changed = d->props.elements().ids(); } else { // Just apply the changed parts of the state. changed = d->props.delta(currentProps); if(forceViewportAndScissor) { changed.insert(ViewportX); changed.insert(ScissorX); } } if(!changed.isEmpty()) { d->removeRedundancies(changed); // Apply the changed properties. foreach(BitField::Id id, changed) { d->glApply(Property(id)); } currentProps = d->props; } #if 0 // Verify that the state is correct. for(int i = 0; i < d->props.elements().size(); ++i) { auto const &elem = d->props.elements().at(i); int val; switch(elem.id) { case Blend: glGetIntegerv(GL_BLEND, &val); DENG2_ASSERT(!val == !d->props.asBool(elem.id)); break; case BlendFuncSrc: glGetIntegerv(GL_BLEND_SRC_RGB, &val); DENG2_ASSERT(d->fromGlBFunc(val) == d->props.asUInt(elem.id)); break; case BlendFuncDest: glGetIntegerv(GL_BLEND_DST_RGB, &val); DENG2_ASSERT(d->fromGlBFunc(val) == d->props.asUInt(elem.id)); break; case BlendOp: glGetIntegerv(GL_BLEND_EQUATION_RGB, &val); val = (val == GL_FUNC_ADD? gl::Add : val == GL_FUNC_SUBTRACT? gl::Subtract : val == GL_FUNC_REVERSE_SUBTRACT? gl::ReverseSubtract : 0); DENG2_ASSERT(val == d->props.asUInt(elem.id)); break; } } #endif } void GLState::considerNativeStateUndefined() { currentProps.clear(); currentTarget = 0; } GLState &GLState::current() { DENG2_ASSERT(!stack.isEmpty()); return *stack.last(); } GLState &GLState::push() { // Duplicate the topmost state. push(new GLState(current())); return current(); } GLState &GLState::pop() { delete take(); return current(); } void GLState::push(GLState *state) { stack.append(state); } GLState *GLState::take() { DENG2_ASSERT(stack.size() > 1); return stack.takeLast(); } dsize GLState::stackDepth() { return stack.size(); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/heightmap.cpp0000664000175000017500000001026712641367671024336 0ustar jaakkojaakko/** @file heightmap.h Height map. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/HeightMap" namespace de { static float const NORMAL_Z = .2f; DENG2_PIMPL_NOREF(HeightMap) { QImage heightImage; QImage normalImage; Vector2f mapSize; float heightRange = 1.f; Vector2f pixelCoordf(Vector2f const &worldPos) const { Vector2f normPos = worldPos / mapSize + Vector2f(.5f, .5f); return normPos * Vector2f(heightImage.width(), heightImage.height()) - Vector2f(.5f, .5f); } Vector3f normalAtCoord(Vector2i const &pos) const { int const w = heightImage.width(); int const h = heightImage.height(); int x0 = max(pos.x - 1, 0); int y0 = max(pos.y - 1, 0); int x2 = min(pos.x + 1, w - 1); int y2 = min(pos.y + 1, h - 1); float base = qRed(heightImage.pixel(pos.x, pos.y)) / 255.f; float left = qRed(heightImage.pixel(x0, pos.y)) / 255.f; float right = qRed(heightImage.pixel(x2, pos.y)) / 255.f; float up = qRed(heightImage.pixel(pos.x, y0)) / 255.f; float down = qRed(heightImage.pixel(pos.x, y2)) / 255.f; return (Vector3f(base - right, base - down, NORMAL_Z) + Vector3f(left - base, up - base, NORMAL_Z)).normalize(); } }; HeightMap::HeightMap() : d(new Instance) {} void HeightMap::setMapSize(Vector2f const &worldSize, float heightRange) { d->mapSize = worldSize; d->heightRange = heightRange; } void HeightMap::loadGrayscale(Image const &heightImage) { d->heightImage = heightImage.toQImage(); } Image HeightMap::toImage() const { return d->heightImage; } Image HeightMap::makeNormalMap() const { QImage const &heightMap = d->heightImage; QImage img(heightMap.size(), QImage::Format_ARGB32); int const w = heightMap.width(); int const h = heightMap.height(); for(int y = 0; y < h; y++) { for(int x = 0; x < w; x++) { Vector3f norm = d->normalAtCoord(Vector2i(x, y)); img.setPixel(x, y, qRgba(clamp(0.f, (norm.x + 1) * 128.f, 255.f), clamp(0.f, (norm.y + 1) * 128.f, 255.f), clamp(0.f, (norm.z + 1) * 128.f, 255.f), 255)); } } d->normalImage = img; return img; } float HeightMap::heightAtPosition(Vector2f const &worldPos) const { QImage const &img = d->heightImage; Vector2f coord = d->pixelCoordf(worldPos); Vector2i pixelCoord = coord.toVector2i(); if(pixelCoord.x < 0 || pixelCoord.y < 0 || pixelCoord.x >= img.width() - 1 || pixelCoord.y >= img.height() - 1) return 0; float A = qRed(img.pixel(pixelCoord.x, pixelCoord.y)) / 255.f - 0.5f; float B = qRed(img.pixel(pixelCoord.x + 1, pixelCoord.y)) / 255.f - 0.5f; float C = qRed(img.pixel(pixelCoord.x + 1, pixelCoord.y + 1)) / 255.f - 0.5f; float D = qRed(img.pixel(pixelCoord.x, pixelCoord.y + 1)) / 255.f - 0.5f; // Bilinear interpolation. Vector2f i(fract(coord.x), fract(coord.y)); float value = A + i.x * (B - A) + i.y * (D - A) + i.x * i.y * (A - B - D + C); return value * -d->heightRange; } Vector3f HeightMap::normalAtPosition(Vector2f const &worldPos) const { Vector2i const pos = d->pixelCoordf(worldPos).toVector2i(); return d->normalAtCoord(pos); // * Vector3f(1, 1, NORMAL_Z)).normalize(); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/imagebank.cpp0000664000175000017500000000521012641367671024276 0ustar jaakkojaakko/** @file imagebank.h Bank containing Image instances loaded from files. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/ImageBank" #include "de/App" #include namespace de { DENG2_PIMPL_NOREF(ImageBank) { struct ImageSource : public ISource { String filePath; ImageSource(String const &path) : filePath(path) {} Time modifiedAt() const { return App::rootFolder().locate(filePath).status().modifiedAt; } Image load() const { Block imageData; App::rootFolder().locate(filePath) >> imageData; return Image::fromData(imageData); } }; struct ImageData : public IData { Image image; ImageData() {} ImageData(Image const &img) : image(img) {} ISerializable *asSerializable() { return ℑ } duint sizeInMemory() const { return image.byteCount(); } }; }; ImageBank::ImageBank(Flags const &flags) : InfoBank("ImageBank", flags), d(new Instance) {} void ImageBank::add(DotPath const &path, String const &imageFilePath) { Bank::add(path, new Instance::ImageSource(imageFilePath)); } void ImageBank::addFromInfo(File const &file) { LOG_AS("ImageBank"); parse(file); addFromInfoBlocks("image"); } Image const &ImageBank::image(DotPath const &path) const { return data(path).as().image; } Bank::ISource *ImageBank::newSourceFromInfo(String const &id) { Record const &def = info()[id]; return new Instance::ImageSource(relativeToPath(def) / def["path"]); } Bank::IData *ImageBank::loadFromSource(ISource &source) { return new Instance::ImageData(source.as().load()); } Bank::IData *ImageBank::newData() { return new Instance::ImageData(); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/atlas.cpp0000664000175000017500000003121112641367671023464 0ustar jaakkojaakko/** @file atlas.cpp Image-based atlas. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/Atlas" #include #include #include #include namespace de { DENG2_PIMPL(Atlas) { Flags flags; Size totalSize; int margin; int border; std::unique_ptr allocator; Image backing; bool needCommit; bool needFullCommit; bool mayDefrag; Rectanglei changedArea; Time fullReportedAt; // Minimum backing size is 1x1 pixels. Instance(Public *i, Flags const &flg, Size const &size) : Base(i) , flags(flg) , totalSize(size.max(Size(1, 1))) , margin(1) , border(0) , needCommit(false) , needFullCommit(true) , mayDefrag(false) { if(hasBacking()) { backing = QImage(QSize(totalSize.x, totalSize.y), QImage::Format_ARGB32); } } bool hasBacking() const { return flags.testFlag(BackingStore); } void markAsChanged(Rectanglei const &rect) { if(needCommit) { // Merge to earlier changes. changedArea |= rect; } else { needCommit = true; changedArea = rect; } } void markFullyChanged() { needCommit = true; needFullCommit = true; changedArea = backing.rect(); } bool mustCommitFull() const { /* * Simple heuristic: if more than 70% of the pixels are included in the * changed area, simply copy the whole thing rather than doing a large * extra copy. */ return (needFullCommit || changedPercentage() > .7f); } float changedPercentage() const { if(!needCommit || totalSize == Size(0, 0)) return 0.0f; duint totalPx = totalSize.x * totalSize.y; duint changedPx = changedArea.width() * changedArea.height(); return float(changedPx) / float(totalPx); } float usedPercentage() const { if(!allocator.get()) return 0; duint totalPx = totalSize.x * totalSize.y; duint usedPx = 0; foreach(Rectanglei const &alloc, allocator->allocs().values()) { usedPx += alloc.width() * alloc.height(); } return float(usedPx) / float(totalPx); } /** * Compose a new backing store with an optimal layout. */ void defragment() { DENG2_ASSERT(hasBacking()); IAllocator::Allocations const oldLayout = allocator->allocs(); if(!allocator->optimize()) { // Optimization did not work out. mayDefrag = false; return; } Image defragged(QImage(QSize(backing.size().x, backing.size().y), backing.qtFormat())); defragged.fill(Image::Color(0, 0, 0, 0)); // Copy all the images to their optimal places. IAllocator::Allocations optimal = allocator->allocs(); DENG2_FOR_EACH(IAllocator::Allocations, i, optimal) { defragged.draw(backing.subImage(oldLayout[i.key()]), i.value().topLeft); } // Defragmentation complete, use the revised backing store. backing = defragged; markFullyChanged(); mayDefrag = false; DENG2_FOR_PUBLIC_AUDIENCE2(Reposition, i) { i->atlasContentRepositioned(self); } } Image::Size sizeWithBorders(Image::Size const &size) { return size + Image::Size(2 * border, 2 * border); } Rectanglei rectWithoutBorder(Id const &id) const { Rectanglei rect; allocator->rect(id, rect); return rect.shrunk(border); } DENG2_PIMPL_AUDIENCE(Reposition) DENG2_PIMPL_AUDIENCE(OutOfSpace) }; DENG2_AUDIENCE_METHOD(Atlas, Reposition) DENG2_AUDIENCE_METHOD(Atlas, OutOfSpace) Atlas::Atlas(Flags const &flags, Size const &totalSize) : d(new Instance(this, flags, totalSize)) {} void Atlas::setAllocator(IAllocator *allocator) { DENG2_GUARD(this); clear(); d->allocator.reset(allocator); if(d->allocator.get()) { d->allocator->setMetrics(d->totalSize, d->margin); d->allocator->clear(); // using new metrics } d->markFullyChanged(); } void Atlas::setMarginSize(dint marginPixels) { d->margin = marginPixels; if(d->allocator.get()) { d->allocator->setMetrics(d->totalSize, d->margin); } } void Atlas::setBorderSize(dint borderPixels) { d->border = borderPixels; } void Atlas::clear() { DENG2_GUARD(this); if(d->allocator.get()) { d->allocator->clear(); } if(d->hasBacking()) { d->backing.fill(Image::Color(0, 0, 0, 0)); d->markFullyChanged(); } d->mayDefrag = false; } void Atlas::setTotalSize(Size const &totalSize) { DENG2_GUARD(this); d->totalSize = totalSize; if(d->allocator.get()) { d->allocator->setMetrics(totalSize, d->margin); } if(d->hasBacking()) { d->backing.resize(totalSize); d->markFullyChanged(); d->defragment(); } } Atlas::Size Atlas::totalSize() const { DENG2_GUARD(this); return d->totalSize; } Id Atlas::alloc(Image const &image) { if(image.isNull()) { LOG_AS("Atlas"); LOGDEV_GL_WARNING("Cannot allocate a zero-size image"); return Id::None; } DENG2_GUARD(this); DENG2_ASSERT(d->allocator.get()); Rectanglei rect; Id id = d->allocator->allocate(d->sizeWithBorders(image.size()), rect); if(id.isNone() && d->flags.testFlag(AllowDefragment) && d->mayDefrag) { // Allocation failed. Maybe we can defragment to get more space? d->defragment(); // Try again... id = d->allocator->allocate(d->sizeWithBorders(image.size()), rect); } if(!id.isNone()) { // Defragmenting may again be helpful. d->mayDefrag = true; Rectanglei const noBorders = rect.shrunk(d->border); Rectanglei const withMargin = rect.expanded(d->margin); if(d->hasBacking()) { // The margin is cleared to transparent black. d->backing.fill(withMargin, Image::Color(0, 0, 0, 0)); if(d->border > 0) { if(d->flags.testFlag(WrapBordersInBackingStore)) { // Wrap using the source image (left, right, top, bottom edges). d->backing.drawPartial(image, Rectanglei(0, 0, d->border, image.height()), rect.topRight() + Vector2i(-d->border, d->border)); d->backing.drawPartial(image, Rectanglei(image.width() - d->border, 0, d->border, image.height()), rect.topLeft + Vector2i(0, d->border)); d->backing.drawPartial(image, Rectanglei(0, 0, image.width(), d->border), rect.bottomLeft() + Vector2i(d->border, -d->border)); d->backing.drawPartial(image, Rectanglei(0, image.height() - d->border, image.width(), d->border), rect.topLeft + Vector2i(d->border, 0)); } } d->backing.draw(image, noBorders.topLeft); //d->backing.toQImage().save(QString("backing-%1.png").arg(uint64_t(this))); d->markAsChanged(rect); } else { // No backing, must commit immediately. if(d->border > 0) { // Expand with borders (repeat edges). QImage const srcImg = image.toQImage(); int const sw = srcImg.width(); int const sh = srcImg.height(); QImage bordered(QSize(rect.width(), rect.height()), srcImg.format()); int const w = bordered.width(); int const h = bordered.height(); QPainter painter(&bordered); painter.setCompositionMode(QPainter::CompositionMode_Source); painter.fillRect(bordered.rect(), QColor(0, 0, 0, 0)); /// @todo This really only works for a border of 1 pixels. Should /// repeat the same outmost edge pixels for every border. -jk painter.drawImage(d->border, d->border, srcImg); painter.drawImage(d->border, 0, srcImg, 0, 0, sw, 1); // top painter.drawImage(d->border, h - 1, srcImg, 0, sh - 1, sw, 1); // bottom painter.drawImage(0, d->border, srcImg, 0, 0, 1, sh); // left painter.drawImage(w - 1, d->border, srcImg, sw - 1, 0, 1, sh); // right // Corners. painter.drawImage(0, 0, srcImg, 0, 0, 1, 1); painter.drawImage(w - 1, 0, srcImg, sw - 1, 0, 1, 1); painter.drawImage(0, h - 1, srcImg, 0, sh - 1, 1, 1); painter.drawImage(w - 1, h - 1, srcImg, sw - 1, sh - 1, 1, 1); commit(bordered, rect.topLeft); } else { commit(image, noBorders.topLeft); } } } else { LOG_AS("Atlas"); if(!d->fullReportedAt.isValid() || d->fullReportedAt.since() > 1.0) { LOGDEV_GL_XVERBOSE("Full with %.1f%% usage") << d->usedPercentage() * 100; d->fullReportedAt = Time::currentHighPerformanceTime(); } DENG2_FOR_AUDIENCE2(OutOfSpace, i) { i->atlasOutOfSpace(*this); } } return id; } void Atlas::release(Id const &id) { if(id.isNone()) return; DENG2_GUARD(this); DENG2_ASSERT(d->allocator.get()); d->allocator->release(id); // Defragmenting may help us again. d->mayDefrag = true; } bool Atlas::contains(Id const &id) const { DENG2_GUARD(this); if(d->allocator.get()) { return d->allocator->ids().contains(id); } return false; } int Atlas::imageCount() const { DENG2_GUARD(this); DENG2_ASSERT(d->allocator.get()); return d->allocator->count(); } Atlas::Ids Atlas::allImages() const { DENG2_GUARD(this); DENG2_ASSERT(d->allocator.get()); return d->allocator->ids(); } Rectanglei Atlas::imageRect(Id const &id) const { DENG2_GUARD(this); DENG2_ASSERT(d->allocator.get()); return d->rectWithoutBorder(id); } Rectanglef Atlas::imageRectf(Id const &id) const { DENG2_GUARD(this); DENG2_ASSERT(d->allocator.get()); Rectanglei const rect = d->rectWithoutBorder(id); // Normalize within the atlas area. return Rectanglef(float(rect.topLeft.x) / float(d->totalSize.x), float(rect.topLeft.y) / float(d->totalSize.y), float(rect.width() ) / float(d->totalSize.x), float(rect.height() ) / float(d->totalSize.y)); } Image Atlas::image(Id const &id) const { DENG2_GUARD(this); if(d->hasBacking() && d->allocator.get() && contains(id)) { return d->backing.subImage(imageRect(id)); } return Image(); } void Atlas::commit() const { DENG2_GUARD(this); if(!d->needCommit || !d->hasBacking()) return; LOG_AS("Atlas"); if(d->mustCommitFull()) { DENG2_ASSERT(d->backing.size() == d->totalSize); if(d->flags.testFlag(LogCommitsAsXVerbose)) { LOGDEV_GL_XVERBOSE("Full commit ") << d->backing.size().asText(); } commitFull(d->backing); } else { if(d->flags.testFlag(LogCommitsAsXVerbose)) { LOGDEV_GL_XVERBOSE("Partial commit ") << d->changedArea.asText(); } // An extra copy is done to crop to the changed area. commit(d->backing.subImage(d->changedArea), d->changedArea.topLeft); } d->needCommit = false; d->needFullCommit = false; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/glframebuffer.cpp0000664000175000017500000003153012641367671025173 0ustar jaakkojaakko/** @file glframebuffer.cpp GL frame buffer. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GLFramebuffer" #include "de/GuiApp" #include #include #include #include #include namespace de { DENG2_STATIC_PROPERTY(DefaultSampleCount, int) DENG2_PIMPL(GLFramebuffer) , DENG2_OBSERVES(DefaultSampleCount, Change) , DENG2_OBSERVES(GuiApp, GLContextChange) { Image::Format colorFormat; Size size; int _samples; ///< don't touch directly (0 == default) GLTarget target; GLTexture color; GLTexture depthStencil; GLTarget multisampleTarget; Drawable bufSwap; GLUniform uMvpMatrix; GLUniform uBufTex; GLUniform uColor; typedef GLBufferT VBuf; Instance(Public *i) : Base(i) , colorFormat(Image::RGB_888) , _samples(0) , uMvpMatrix("uMvpMatrix", GLUniform::Mat4) , uBufTex ("uTex", GLUniform::Sampler2D) , uColor ("uColor", GLUniform::Vec4) { pDefaultSampleCount.audienceForChange() += this; //DENG2_GUI_APP->audienceForGLContextChange += this; } ~Instance() { pDefaultSampleCount.audienceForChange() -= this; //DENG2_GUI_APP->audienceForGLContextChange -= this; release(); } void appGLContextChanged() { /* qDebug() << "rebooting FB" << thisPublic << self.isReady() << target.glName() << target.isReady() << size.asText(); self.glDeinit(); self.glInit(); */ } int sampleCount() const { if(_samples <= 0) return pDefaultSampleCount; return _samples; } bool isMultisampled() const { if(!GLInfo::extensions().EXT_framebuffer_multisample) { // Not supported. return false; } return sampleCount() > 1; } void valueOfDefaultSampleCountChanged() { reconfigure(); } void alloc() { // Prepare the fallback blit method. VBuf *buf = new VBuf; bufSwap.addBuffer(buf); bufSwap.program().build(// Vertex shader: Block("uniform highp mat4 uMvpMatrix; " "attribute highp vec4 aVertex; " "attribute highp vec2 aUV; " "varying highp vec2 vUV; " "void main(void) {" "gl_Position = uMvpMatrix * aVertex; " "vUV = aUV; }"), // Fragment shader: Block("uniform sampler2D uTex; " "uniform highp vec4 uColor; " "varying highp vec2 vUV; " "void main(void) { " "gl_FragColor = uColor * texture2D(uTex, vUV); }")) << uMvpMatrix << uBufTex << uColor; buf->setVertices(gl::TriangleStrip, VBuf::Builder().makeQuad(Rectanglef(0, 0, 1, 1), Rectanglef(0, 1, 1, -1)), gl::Static); uMvpMatrix = Matrix4f::ortho(0, 1, 0, 1); uBufTex = color; uColor = Vector4f(1, 1, 1, 1); } void release() { bufSwap.clear(); color.clear(); depthStencil.clear(); target.configure(); multisampleTarget.configure(); } void reconfigure() { if(!self.isReady() || size == Size()) return; LOGDEV_GL_VERBOSE("Reconfiguring framebuffer: %s ms:%i") << size.asText() << sampleCount(); // Configure textures for the framebuffer. color.setUndefinedImage(size, colorFormat); color.setWrap(gl::ClampToEdge, gl::ClampToEdge); color.setFilter(gl::Nearest, gl::Linear, gl::MipNone); DENG2_ASSERT(color.isReady()); depthStencil.setDepthStencilContent(size); depthStencil.setWrap(gl::ClampToEdge, gl::ClampToEdge); depthStencil.setFilter(gl::Nearest, gl::Nearest, gl::MipNone); DENG2_ASSERT(depthStencil.isReady()); // Try a couple of different ways to set up the FBO. for(int attempt = 0; ; ++attempt) { String failMsg; try { switch(attempt) { case 0: // Most preferred: render both color and depth+stencil to textures. // Allows shaders to access contents of the entire framebuffer. failMsg = "Texture-based framebuffer failed: %s\n" "Trying again without depth/stencil texture"; target.configure(&color, &depthStencil); break; case 1: failMsg = "Color texture with unified depth/stencil renderbuffer failed: %s\n" "Trying again without stencil"; target.configure(GLTarget::Color, color, GLTarget::DepthStencil); LOG_GL_WARNING("Renderer feature unavailable: lensflare depth"); break; case 2: failMsg = "Color texture with depth renderbuffer failed: %s\n" "Trying again without texture buffers"; target.configure(GLTarget::Color, color, GLTarget::Depth); LOG_GL_WARNING("Renderer features unavailable: sky mask, lensflare depth"); break; case 3: failMsg = "Renderbuffer-based framebuffer failed: %s\n" "Trying again without stencil"; target.configure(size, GLTarget::ColorDepthStencil); LOG_GL_WARNING("Renderer features unavailable: postfx, lensflare depth"); break; case 4: // Final fallback: simple FBO with just color+depth renderbuffers. // No postfx, no access from shaders, no sky mask. target.configure(size, GLTarget::ColorDepth); LOG_GL_WARNING("Renderer features unavailable: postfx, sky mask, lensflare depth"); break; default: break; } break; // success! } catch(GLTarget::ConfigError const &er) { if(failMsg.isEmpty()) throw er; // Can't handle it. LOG_GL_NOTE(failMsg) << er.asText(); } } target.clear(GLTarget::ColorDepthStencil); if(isMultisampled()) { try { // Set up the multisampled target with suitable renderbuffers. multisampleTarget.configure(size, GLTarget::ColorDepthStencil, sampleCount()); multisampleTarget.clear(GLTarget::ColorDepthStencil); // Actual drawing occurs in the multisampled target that is then // blitted to the main target. target.setProxy(&multisampleTarget); } catch(GLTarget::ConfigError const &er) { LOG_GL_WARNING("Multisampling not supported: %s") << er.asText(); _samples = 1; goto noMultisampling; } } else { noMultisampling: multisampleTarget.configure(); } } void resize(Size const &newSize) { if(size != newSize) { size = newSize; reconfigure(); } } void drawSwap() { if(isMultisampled()) { target.updateFromProxy(); } bufSwap.draw(); } void swapBuffers(Canvas &canvas, gl::SwapBufferMode swapMode) { GLTarget defaultTarget; GLState::push() .setTarget(defaultTarget) .setViewport(Rectangleui::fromSize(size)) .apply(); if(!color.isReady()) { // If the frame buffer hasn't been configured yet, just clear the canvas. glClear(GL_COLOR_BUFFER_BIT); canvas.QGLWidget::swapBuffers(); GLState::pop().apply(); return; } switch(swapMode) { case gl::SwapMonoBuffer: if(GLInfo::extensions().EXT_framebuffer_blit) { if(isMultisampled()) { multisampleTarget.blit(defaultTarget); // resolve multisampling to system backbuffer } else { target.blit(defaultTarget); // copy to system backbuffer } } else { // Fallback: draw the back buffer texture to the main framebuffer. drawSwap(); } canvas.QGLWidget::swapBuffers(); break; case gl::SwapWithAlpha: drawSwap(); break; case gl::SwapStereoLeftBuffer: glDrawBuffer(GL_BACK_LEFT); drawSwap(); glDrawBuffer(GL_BACK); break; case gl::SwapStereoRightBuffer: glDrawBuffer(GL_BACK_RIGHT); drawSwap(); glDrawBuffer(GL_BACK); break; case gl::SwapStereoBuffers: canvas.QGLWidget::swapBuffers(); break; } GLState::pop().apply(); } }; GLFramebuffer::GLFramebuffer(Image::Format const &colorFormat, Size const &initialSize, int sampleCount) : d(new Instance(this)) { d->colorFormat = colorFormat; d->size = initialSize; d->_samples = sampleCount; } void GLFramebuffer::glInit() { if(isReady()) return; #ifdef LIBGUI_USE_GLENTRYPOINTS if(!glBindFramebuffer) return; #endif LOG_AS("GLFramebuffer"); // Check for some integral OpenGL functionality. if(!GLInfo::extensions().EXT_framebuffer_object) { LOG_GL_WARNING("Required GL_EXT_framebuffer_object is missing!"); } if(!GLInfo::extensions().EXT_packed_depth_stencil) { LOG_GL_WARNING("GL_EXT_packed_depth_stencil is missing, some features may be unavailable"); } d->alloc(); setState(Ready); d->reconfigure(); } void GLFramebuffer::glDeinit() { setState(NotReady); d->release(); } void GLFramebuffer::setSampleCount(int sampleCount) { if(!GLInfo::isFramebufferMultisamplingSupported()) { sampleCount = 1; } if(d->_samples != sampleCount) { LOG_AS("GLFramebuffer"); d->_samples = sampleCount; d->reconfigure(); } } void GLFramebuffer::setColorFormat(Image::Format const &colorFormat) { if(d->colorFormat != colorFormat) { d->colorFormat = colorFormat; d->reconfigure(); } } void GLFramebuffer::resize(Size const &newSize) { d->resize(newSize); } GLFramebuffer::Size GLFramebuffer::size() const { return d->size; } GLTarget &GLFramebuffer::target() const { return d->target; } GLTexture &GLFramebuffer::colorTexture() const { return d->color; } GLTexture &GLFramebuffer::depthStencilTexture() const { return d->depthStencil; } int GLFramebuffer::sampleCount() const { return d->sampleCount(); } void GLFramebuffer::swapBuffers(Canvas &canvas, gl::SwapBufferMode swapMode) { d->swapBuffers(canvas, swapMode); } void GLFramebuffer::drawBuffer(float opacity) { d->uColor = Vector4f(1, 1, 1, opacity); GLState::push() .setCull(gl::None) .setDepthTest(false) .setDepthWrite(false); d->drawSwap(); GLState::pop(); d->uColor = Vector4f(1, 1, 1, 1); } bool GLFramebuffer::setDefaultMultisampling(int sampleCount) { LOG_AS("GLFramebuffer"); int const newCount = max(1, sampleCount); if(pDefaultSampleCount != newCount) { pDefaultSampleCount = newCount; return true; } return false; } int GLFramebuffer::defaultMultisampling() { return pDefaultSampleCount; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/gluniform.cpp0000664000175000017500000002636312641367671024376 0ustar jaakkojaakko/** @file gluniform.cpp GL uniform. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GLUniform" #include "de/GLTexture" #include "de/GLProgram" #include "de/graphics/opengl.h" #include #include #include namespace de { DENG2_PIMPL(GLUniform) , DENG2_OBSERVES(Asset, Deletion) { Block name; Type type; union Value { dint int32; duint uint32; dfloat float32; Vector3f *vec3array; Vector4f *vector; Matrix3f *mat3; Matrix4f *mat4; GLTexture const *tex; } value; duint elemCount; Instance(Public *i, QLatin1String const &n, Type t, duint elems) : Base(i) , name(n.latin1()) , type(t) , elemCount(elems) { name.append('\0'); DENG2_ASSERT(elemCount == 1 || (elemCount > 1 && (type == Mat4Array || type == Vec4Array || type == Vec3Array))); // Allocate the value type. zap(value); switch(type) { case Vec2: case Vec3: case Vec4: value.vector = new Vector4f; break; case Vec3Array: value.vec3array = new Vector3f[elemCount]; break; case Vec4Array: value.vector = new Vector4f[elemCount]; break; case Mat3: value.mat3 = new Matrix3f; break; case Mat4: value.mat4 = new Matrix4f; break; case Mat4Array: value.mat4 = new Matrix4f[elemCount]; break; default: break; } } ~Instance() { DENG2_FOR_PUBLIC_AUDIENCE2(Deletion, i) i->uniformDeleted(self); switch(type) { case Vec2: case Vec3: case Vec4: delete value.vector; break; case Vec3Array: delete [] value.vec3array; break; case Vec4Array: delete [] value.vector; break; case Mat3: delete value.mat3; break; case Mat4: delete value.mat4; break; case Mat4Array: delete [] value.mat4; break; case Sampler2D: if(value.tex) value.tex->audienceForDeletion() -= this; break; default: break; } } /** * @return Returns @c true if the value changed. */ template bool set(Type numValue) { DENG2_ASSERT(type == Int || type == UInt || type == Float); switch(type) { case Int: if(value.int32 != dint(numValue)) { value.int32 = dint(numValue); return true; } break; case UInt: if(value.uint32 != duint(numValue)) { value.uint32 = duint(numValue); return true; } break; case Float: if(!fequal(value.float32, dfloat(numValue))) { value.float32 = dfloat(numValue); return true; } break; default: break; } return false; } void markAsChanged() { DENG2_FOR_PUBLIC_AUDIENCE2(ValueChange, i) { i->uniformValueChanged(self); } } void assetBeingDeleted(Asset &asset) { if(type == Sampler2D) { if(value.tex == &asset) { // No more texture to refer to. value.tex = 0; } } } DENG2_PIMPL_AUDIENCE(Deletion) DENG2_PIMPL_AUDIENCE(ValueChange) }; DENG2_AUDIENCE_METHOD(GLUniform, Deletion) DENG2_AUDIENCE_METHOD(GLUniform, ValueChange) GLUniform::GLUniform(char const *nameInShader, Type uniformType, duint elements) : d(new Instance(this, QLatin1String(nameInShader), uniformType, elements)) {} void GLUniform::setName(char const *nameInShader) { d->name = Block(nameInShader); d->name.append('\0'); } QLatin1String GLUniform::name() const { return QLatin1String(d->name); } GLUniform::Type GLUniform::type() const { return d->type; } GLUniform &GLUniform::operator = (dint value) { if(d->set(value)) { d->markAsChanged(); } return *this; } GLUniform &GLUniform::operator = (duint value) { if(d->set(value)) { d->markAsChanged(); } return *this; } GLUniform &GLUniform::operator = (dfloat value) { if(d->set(value)) { d->markAsChanged(); } return *this; } GLUniform &GLUniform::operator = (ddouble value) { return *this = dfloat(value); } GLUniform &GLUniform::operator = (Vector2f const &vec) { DENG2_ASSERT(d->type == Vec2); if(Vector2f(*d->value.vector) != vec) { *d->value.vector = Vector4f(vec); d->markAsChanged(); } return *this; } GLUniform &GLUniform::operator = (Vector3f const &vec) { DENG2_ASSERT(d->type == Vec3); if(Vector3f(*d->value.vector) != vec) { *d->value.vector = vec; d->markAsChanged(); } return *this; } GLUniform &GLUniform::operator = (Vector4f const &vec) { DENG2_ASSERT(d->type == Vec4); if(*d->value.vector != vec) { *d->value.vector = vec; d->markAsChanged(); } return *this; } GLUniform &GLUniform::operator = (Matrix3f const &mat) { DENG2_ASSERT(d->type == Mat3); *d->value.mat3 = mat; d->markAsChanged(); return *this; } GLUniform &GLUniform::operator = (Matrix4f const &mat) { DENG2_ASSERT(d->type == Mat4); *d->value.mat4 = mat; d->markAsChanged(); return *this; } GLUniform &GLUniform::operator = (GLTexture const &texture) { return *this = &texture; } GLUniform &GLUniform::operator = (GLTexture const *texture) { DENG2_ASSERT(d->type == Sampler2D); if(d->value.tex != texture) { // We will observe the texture this uniform refers to. if(d->value.tex) d->value.tex->audienceForDeletion() -= d; d->value.tex = texture; d->markAsChanged(); if(d->value.tex) d->value.tex->audienceForDeletion() += d; } return *this; } GLUniform &GLUniform::set(duint elementIndex, Vector3f const &vec) { DENG2_ASSERT(d->type == Vec3Array); DENG2_ASSERT(elementIndex < d->elemCount); if(d->value.vec3array[elementIndex] != vec) { d->value.vec3array[elementIndex] = vec; d->markAsChanged(); } return *this; } GLUniform &GLUniform::set(duint elementIndex, Vector4f const &vec) { DENG2_ASSERT(d->type == Vec4Array); DENG2_ASSERT(elementIndex < d->elemCount); if(d->value.vector[elementIndex] != vec) { d->value.vector[elementIndex] = vec; d->markAsChanged(); } return *this; } GLUniform &GLUniform::set(duint elementIndex, Matrix4f const &mat) { DENG2_ASSERT(d->type == Mat4Array); DENG2_ASSERT(elementIndex < d->elemCount); d->value.mat4[elementIndex] = mat; d->markAsChanged(); return *this; } dint GLUniform::toInt() const { DENG2_ASSERT(d->type == Int || d->type == UInt || d->type == Float); switch(d->type) { case Int: return d->value.int32; case UInt: return dint(d->value.uint32); case Float: return dint(d->value.float32); default: return 0; } } duint de::GLUniform::toUInt() const { DENG2_ASSERT(d->type == Int || d->type == UInt || d->type == Float); switch(d->type) { case Int: return duint(d->value.int32); case UInt: return d->value.uint32; case Float: return duint(d->value.float32); default: return 0; } } dfloat GLUniform::toFloat() const { DENG2_ASSERT(d->type == Int || d->type == UInt || d->type == Float); switch(d->type) { case Int: return dfloat(d->value.int32); case UInt: return dfloat(d->value.uint32); case Float: return d->value.float32; default: return 0; } } Vector2f const &GLUniform::toVector2f() const { DENG2_ASSERT(d->type == Vec2 || d->type == Vec3 || d->type == Vec4); return *d->value.vector; } Vector3f const &GLUniform::toVector3f() const { DENG2_ASSERT(d->type == Vec2 || d->type == Vec3 || d->type == Vec4); return *d->value.vector; } Vector4f const &GLUniform::toVector4f() const { DENG2_ASSERT(d->type == Vec2 || d->type == Vec3 || d->type == Vec4); return *d->value.vector; } Matrix3f const &GLUniform::toMatrix3f() const { DENG2_ASSERT(d->type == Mat3); return *d->value.mat3; } Matrix4f const &GLUniform::toMatrix4f() const { DENG2_ASSERT(d->type == Mat4); return *d->value.mat4; } GLTexture const *GLUniform::texture() const { DENG2_ASSERT(d->type == Sampler2D); return d->value.tex; } void GLUniform::applyInProgram(GLProgram &program) const { int loc = program.glUniformLocation(d->name.constData()); if(loc < 0) { // Uniform not in the program. LOG_AS("applyInProgram"); LOGDEV_GL_WARNING("'%s' not in the program") << d->name.constData(); return; } switch(d->type) { case Int: glUniform1i(loc, d->value.int32); LIBGUI_ASSERT_GL_OK(); break; case UInt: glUniform1i(loc, d->value.uint32); LIBGUI_ASSERT_GL_OK(); break; case Float: glUniform1f(loc, d->value.float32); LIBGUI_ASSERT_GL_OK(); break; case Vec2: glUniform2f(loc, d->value.vector->x, d->value.vector->y); LIBGUI_ASSERT_GL_OK(); break; case Vec3: glUniform3f(loc, d->value.vector->x, d->value.vector->y, d->value.vector->z); LIBGUI_ASSERT_GL_OK(); break; case Vec3Array: glUniform3fv(loc, d->elemCount, &d->value.vec3array->x); // sequentially laid out LIBGUI_ASSERT_GL_OK(); break; case Vec4: case Vec4Array: glUniform4fv(loc, d->elemCount, &d->value.vector->x); // sequentially laid out LIBGUI_ASSERT_GL_OK(); break; case Mat3: glUniformMatrix3fv(loc, 1, GL_FALSE, d->value.mat3->values()); LIBGUI_ASSERT_GL_OK(); break; case Mat4: case Mat4Array: glUniformMatrix4fv(loc, d->elemCount, GL_FALSE, d->value.mat4->values()); // sequentially laid out LIBGUI_ASSERT_GL_OK(); break; default: break; } } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/modeldrawable.cpp0000664000175000017500000011330112641367671025163 0ustar jaakkojaakko/** @file modeldrawable.cpp Drawable specialized for 3D models. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/ModelDrawable" #include "de/HeightMap" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace de { namespace internal { /// Adapter between de::File and Assimp. class ImpIOStream : public Assimp::IOStream { public: ImpIOStream(ByteArrayFile const &file) : _file(file), _pos(0) {} size_t Read(void *pvBuffer, size_t pSize, size_t pCount) { size_t const num = pSize * pCount; _file.get(_pos, reinterpret_cast(pvBuffer), num); _pos += num; return pCount; } size_t Write(void const *, size_t, size_t) { throw Error("ImpIOStream::Write", "Writing not allowed"); } aiReturn Seek(size_t pOffset, aiOrigin pOrigin) { switch(pOrigin) { case aiOrigin_SET: _pos = pOffset; break; case aiOrigin_CUR: _pos += pOffset; break; case aiOrigin_END: _pos = _file.size() - pOffset; break; default: break; } return aiReturn_SUCCESS; } size_t Tell() const { return _pos; } size_t FileSize() const { return _file.size(); } void Flush() {} private: ByteArrayFile const &_file; size_t _pos; }; /// Adapter between FS2 and Assimp. struct ImpIOSystem : public Assimp::IOSystem { ImpIOSystem() {} ~ImpIOSystem() {} char getOsSeparator() const { return '/'; } bool Exists(char const *pFile) const { return App::rootFolder().has(pFile); } Assimp::IOStream *Open(char const *pFile, char const *) { String const path = pFile; DENG2_ASSERT(!path.contains("\\")); return new ImpIOStream(App::rootFolder().locate(path)); } void Close(Assimp::IOStream *pFile) { delete pFile; } }; struct ImpLogger : public Assimp::LogStream { void write(char const *message) { LOG_RES_VERBOSE("[ai] %s") << message; } static void registerLogger() { if(registered) return; registered = true; Assimp::DefaultLogger::get()->attachStream( new ImpLogger, Assimp::Logger::Info | Assimp::Logger::Warn | Assimp::Logger::Err); } static bool registered; }; bool ImpLogger::registered = false; struct DefaultImageLoader : public ModelDrawable::IImageLoader { Image loadImage(String const &path) { File const &texFile = App::rootFolder().locate(path); qDebug() << "loading image from" << texFile.description().toLatin1(); return Image::fromData(texFile, texFile.name().fileNameExtension()); } }; static DefaultImageLoader defaultImageLoader; } // namespace internal using namespace internal; #define MAX_BONES 64 #define MAX_BONES_PER_VERTEX 4 struct ModelVertex { Vector3f pos; Vector4f boneIds; Vector4f boneWeights; Vector3f normal; Vector3f tangent; Vector3f bitangent; Vector2f texCoord; Vector4f texBounds; Vector4f texBounds2; Vector4f texBounds3; Vector4f texBounds4; LIBGUI_DECLARE_VERTEX_FORMAT(11) }; AttribSpec const ModelVertex::_spec[11] = { { AttribSpec::Position, 3, GL_FLOAT, false, sizeof(ModelVertex), 0 }, { AttribSpec::BoneIDs, 4, GL_FLOAT, false, sizeof(ModelVertex), 3 * sizeof(float) }, { AttribSpec::BoneWeights, 4, GL_FLOAT, false, sizeof(ModelVertex), 7 * sizeof(float) }, { AttribSpec::Normal, 3, GL_FLOAT, false, sizeof(ModelVertex), 11 * sizeof(float) }, { AttribSpec::Tangent, 3, GL_FLOAT, false, sizeof(ModelVertex), 14 * sizeof(float) }, { AttribSpec::Bitangent, 3, GL_FLOAT, false, sizeof(ModelVertex), 17 * sizeof(float) }, { AttribSpec::TexCoord0, 2, GL_FLOAT, false, sizeof(ModelVertex), 20 * sizeof(float) }, { AttribSpec::TexBounds0, 4, GL_FLOAT, false, sizeof(ModelVertex), 22 * sizeof(float) }, { AttribSpec::TexBounds1, 4, GL_FLOAT, false, sizeof(ModelVertex), 26 * sizeof(float) }, { AttribSpec::TexBounds2, 4, GL_FLOAT, false, sizeof(ModelVertex), 30 * sizeof(float) }, { AttribSpec::TexBounds3, 4, GL_FLOAT, false, sizeof(ModelVertex), 34 * sizeof(float) } }; LIBGUI_VERTEX_FORMAT_SPEC(ModelVertex, 38 * sizeof(float)) static Matrix4f convertMatrix(aiMatrix4x4 const &aiMat) { return Matrix4f(&aiMat.a1).transpose(); } /// Bone used for vertices that have no bones. static String const DUMMY_BONE_NAME = "__deng_dummy-bone__"; static int const MAX_TEXTURES = 4; DENG2_PIMPL(ModelDrawable) { typedef GLBufferT VBuf; typedef QHash AnimLookup; struct VertexBone { duint16 ids[MAX_BONES_PER_VERTEX]; dfloat weights[MAX_BONES_PER_VERTEX]; VertexBone() { zap(ids); zap(weights); } }; struct BoneData { Matrix4f offset; }; struct MaterialData { Id::Type texIds[MAX_TEXTURES]; QHash custom; MaterialData() { zap(texIds); } }; Asset modelAsset; String sourcePath; Assimp::Importer importer; IImageLoader *imageLoader { &defaultImageLoader }; aiScene const *scene { nullptr }; Vector3f minPoint; ///< Bounds in default pose. Vector3f maxPoint; Matrix4f globalInverse; QVector vertexBones; // indexed by vertex QHash boneNameToIndex; QHash nodeNameToPtr; QVector bones; // indexed by bone index AnimLookup animNameToIndex; TextureMap textureOrder[MAX_TEXTURES]; Id::Type defaultTexIds[MAX_TEXTURES]; QVector materials; // indexed by material index bool needMakeBuffer { false }; AtlasTexture *atlas { nullptr }; VBuf *buffer { nullptr }; GLProgram *program { nullptr }; mutable GLUniform uBoneMatrices { "uBoneMatrices", GLUniform::Mat4Array, MAX_BONES }; Instance(Public *i) : Base(i) { textureOrder[0] = Diffuse; textureOrder[1] = textureOrder[2] = textureOrder[3] = Unknown; zap(defaultTexIds); // Use FS2 for file access. importer.SetIOHandler(new ImpIOSystem); // Get most kinds of log output. ImpLogger::registerLogger(); } ~Instance() { glDeinit(); } void import(File const &file) { LOG_RES_MSG("Loading model from %s") << file.description(); scene = 0; sourcePath = file.path(); // Read the model file and apply suitable postprocessing to clean up the data. if(!importer.ReadFile(sourcePath.toLatin1(), aiProcess_CalcTangentSpace | aiProcess_GenSmoothNormals | aiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_GenUVCoords | aiProcess_FlipUVs | aiProcess_SortByPType)) { throw LoadError("ModelDrawable::import", String("Failed to load model from %s: %s") .arg(file.description()).arg(importer.GetErrorString())); } scene = importer.GetScene(); initBones(); globalInverse = convertMatrix(scene->mRootNode->mTransformation).inverse(); maxPoint = Vector3f(1.0e-9f, 1.0e-9f, 1.0e-9f); minPoint = Vector3f(1.0e9f, 1.0e9f, 1.0e9f); // Determine the total bounding box. for(duint i = 0; i < scene->mNumMeshes; ++i) { aiMesh const &mesh = *scene->mMeshes[i]; for(duint i = 0; i < mesh.mNumVertices; ++i) { addToBounds(Vector3f(&mesh.mVertices[i].x)); } } // Print some information. qDebug() << "total bones:" << boneCount(); // Animations. animNameToIndex.clear(); //qDebug() << "animations:" << scene->mNumAnimations; for(duint i = 0; i < scene->mNumAnimations; ++i) { //qDebug() << " anim #" << i << "name:" << scene->mAnimations[i]->mName.C_Str(); String const name = scene->mAnimations[i]->mName.C_Str(); if(!name.isEmpty()) { animNameToIndex.insert(name, i); } } // Create a lookup for node names. nodeNameToPtr.clear(); nodeNameToPtr.insert("", scene->mRootNode); buildNodeLookup(*scene->mRootNode); // Prepare material information. qDebug() << "materials:" << scene->mNumMaterials; for(duint i = 0; i < scene->mNumMaterials; ++i) { materials << MaterialData(); } } void buildNodeLookup(aiNode const &node) { String const name = node.mName.C_Str(); if(!name.isEmpty()) { nodeNameToPtr.insert(name, &node); } for(duint i = 0; i < node.mNumChildren; ++i) { buildNodeLookup(*node.mChildren[i]); } } /// Release all loaded model data. void clear() { glDeinit(); sourcePath.clear(); materials.clear(); importer.FreeScene(); scene = 0; } void glInit() { DENG2_ASSERT_IN_MAIN_THREAD(); // Has a scene been imported successfully? if(!scene) return; if(modelAsset.isReady()) { // Already good to go. return; } // Last minute notification in case some additional setup is needed. DENG2_FOR_PUBLIC_AUDIENCE2(AboutToGLInit, i) { i->modelAboutToGLInit(self); } // Materials. initTextures(); // Initialize all meshes in the scene into a single GL buffer. makeBuffer(); // Ready to go! modelAsset.setState(Ready); } void glDeinit() { releaseTexturesFromAtlas(); /// @todo Free all GL resources. delete buffer; buffer = 0; clearBones(); modelAsset.setState(NotReady); } void addToBounds(Vector3f const &point) { minPoint = minPoint.min(point); maxPoint = maxPoint.max(point); } bool isDefaultTexture(Id const &id) const { for(Id const &defTex : defaultTexIds) { if(id == defTex) return true; } return false; } void releaseTexture(Id const &id) { if(!id || isDefaultTexture(id)) return; // We don't own this, don't release. qDebug() << "Releasing model texture" << id.asText(); atlas->release(id); } void releaseTexturesFromAtlas() { if(!atlas) return; foreach(MaterialData const &tex, materials) { for(Id const &id : tex.texIds) releaseTexture(id); } materials.clear(); } int findMaterial(String const &name) const { if(!scene) return -1; for(duint i = 0; i < scene->mNumMaterials; ++i) { aiMaterial const &material = *scene->mMaterials[i]; aiString matName; if(material.Get(AI_MATKEY_NAME, matName) == AI_SUCCESS) { if(name == matName.C_Str()) return i; } } return -1; } static TextureMap textureMapType(aiTextureType type) { switch(type) { case aiTextureType_DIFFUSE: return Diffuse; case aiTextureType_NORMALS: return Normals; case aiTextureType_HEIGHT: return Height; case aiTextureType_SPECULAR: return Specular; case aiTextureType_EMISSIVE: return Emissive; default: DENG2_ASSERT(!"Unsupported texture type"); return Diffuse; } } static aiTextureType impTextureType(TextureMap map) { switch(map) { case Diffuse: return aiTextureType_DIFFUSE; case Normals: return aiTextureType_NORMALS; case Height: return aiTextureType_HEIGHT; case Specular: return aiTextureType_SPECULAR; case Emissive: return aiTextureType_EMISSIVE; default: break; } return aiTextureType_UNKNOWN; } void fallBackToDefaultTexture(MaterialData &mat, TextureMap map) { if(!mat.texIds[map]) mat.texIds[map] = defaultTexIds[map]; } /** * Load all the textures of the model. The textures are allocated into the * atlas provided to the model. */ void initTextures() { DENG2_ASSERT(atlas != 0); for(duint i = 0; i < scene->mNumMaterials; ++i) { qDebug() << " material #" << i; auto &mat = materials[i]; // Load all known types of textures, falling back to defaults. loadTextureImage(i, aiTextureType_DIFFUSE); fallBackToDefaultTexture(mat, Diffuse); loadTextureImage(i, aiTextureType_NORMALS); if(!mat.texIds[Normals]) { // Try a height field instead. This will be converted to a normal map. loadTextureImage(i, aiTextureType_HEIGHT); } fallBackToDefaultTexture(mat, Normals); loadTextureImage(i, aiTextureType_SPECULAR); fallBackToDefaultTexture(mat, Specular); loadTextureImage(i, aiTextureType_EMISSIVE); fallBackToDefaultTexture(mat, Emissive); } } /** * Attempts to load a texture image specified in the material. Also checks if * an overridden custom path is provided, though. * * @param materialId Material index. * @param type AssImp texture type. */ void loadTextureImage(duint materialId, aiTextureType type) { DENG2_ASSERT(imageLoader != 0); TextureMap map = textureMapType(type); aiMaterial const &material = *scene->mMaterials[materialId]; MaterialData const &data = materials[materialId]; try { // Custom override path? if(data.custom.contains(map)) { qDebug() << "loading custom path" << data.custom[map]; return setTexture(materialId, map, imageLoader->loadImage(data.custom[map])); } } catch(Error const &er) { LOG_RES_WARNING("Failed to load custom model texture (type %i): %s") << type << er.asText(); } qDebug() << " type:" << type << "count:" << material.GetTextureCount(type); // Load the texture based on the information specified in the model's materials. aiString texPath; for(duint s = 0; s < material.GetTextureCount(type); ++s) { if(material.GetTexture(type, s, &texPath) == AI_SUCCESS) { qDebug() << " texture #" << s << texPath.C_Str(); try { setTexture(materialId, map, imageLoader->loadImage(sourcePath.fileNamePath() / String(texPath.C_Str()))); break; } catch(Error const &er) { LOG_RES_WARNING("Failed to load model texture (type %i): %s") << type << er.asText(); } } } } /** * Sets a custom texture that will be loaded when the model is glInited. * * @param materialId Material id. * @param tex Texture map. * @param path Image file path. */ void setCustomTexture(duint materialId, TextureMap map, String const &path) { DENG2_ASSERT(!atlas); DENG2_ASSERT(materialId < duint(materials.size())); materials[materialId].custom.insert(map, path); } void setTexture(duint materialId, TextureMap map, Image const &content) { if(!scene) return; if(materialId >= scene->mNumMaterials) return; if(map == Unknown) return; DENG2_ASSERT(atlas != 0); MaterialData &tex = materials[materialId]; Id::Type &id = (map == Height? tex.texIds[Normals] : tex.texIds[map]); // Release a previously loaded texture. if(id) { releaseTexture(id); id = Id::None; } if(map == Height) { // Convert the height map into a normal map. HeightMap heightMap; heightMap.loadGrayscale(content); id = atlas->alloc(heightMap.makeNormalMap()); } else { id = atlas->alloc(content); } // The buffer needs to be updated with the new texture bounds. needMakeBuffer = true; } // Bone & Mesh Setup ---------------------------------------------------------------- void clearBones() { vertexBones.clear(); bones.clear(); boneNameToIndex.clear(); } int boneCount() const { return bones.size(); } int addBone(String const &name) { int idx = boneCount(); bones << BoneData(); boneNameToIndex[name] = idx; return idx; } int findBone(String const &name) const { if(boneNameToIndex.contains(name)) { return boneNameToIndex[name]; } return -1; } int addOrFindBone(String const &name) { int i = findBone(name); if(i >= 0) { return i; } return addBone(name); } void addVertexWeight(duint vertexIndex, duint16 boneIndex, dfloat weight) { VertexBone &vb = vertexBones[vertexIndex]; for(int i = 0; i < MAX_BONES_PER_VERTEX; ++i) { if(vb.weights[i] == 0.f) { // Here's a free one. vb.ids[i] = boneIndex; vb.weights[i] = weight; return; } } DENG2_ASSERT(!"Too many bone weights for a vertex"); } /** * Initializes the per-vertex bone weight information, and indexes the bones * of the mesh in a sequential order. * * @param mesh Source mesh. * @param vertexBase Index of the first vertex of the mesh. */ void initMeshBones(aiMesh const &mesh, duint vertexBase) { vertexBones.resize(vertexBase + mesh.mNumVertices); if(mesh.HasBones()) { // Mark the per-vertex bone weights. for(duint i = 0; i < mesh.mNumBones; ++i) { aiBone const &bone = *mesh.mBones[i]; duint const boneIndex = addOrFindBone(bone.mName.C_Str()); bones[boneIndex].offset = convertMatrix(bone.mOffsetMatrix); for(duint w = 0; w < bone.mNumWeights; ++w) { addVertexWeight(vertexBase + bone.mWeights[w].mVertexId, boneIndex, bone.mWeights[w].mWeight); } } } else { // No bones; make one dummy bone so we can render it the same way. duint const boneIndex = addOrFindBone(DUMMY_BONE_NAME); bones[boneIndex].offset = Matrix4f(); // All vertices fully affected by this bone. for(duint i = 0; i < mesh.mNumVertices; ++i) { addVertexWeight(vertexBase + i, boneIndex, 1.f); } } } /** * Initializes all bones in the scene. */ void initBones() { clearBones(); int base = 0; for(duint i = 0; i < scene->mNumMeshes; ++i) { aiMesh const &mesh = *scene->mMeshes[i]; qDebug() << "initializing bones for mesh:" << mesh.mName.C_Str(); qDebug() << " bones:" << mesh.mNumBones; initMeshBones(mesh, base); base += mesh.mNumVertices; } } /** * Allocates and fills in the GL buffer containing vertex information. */ void makeBuffer() { needMakeBuffer = false; VBuf::Vertices verts; VBuf::Indices indx; aiVector3D const zero(0, 0, 0); int base = 0; // All of the scene's meshes are combined into one GL buffer. for(duint m = 0; m < scene->mNumMeshes; ++m) { aiMesh const &mesh = *scene->mMeshes[m]; // Load vertices into the buffer. for(duint i = 0; i < mesh.mNumVertices; ++i) { aiVector3D const *pos = &mesh.mVertices[i]; aiVector3D const *normal = (mesh.HasNormals()? &mesh.mNormals[i] : &zero); aiVector3D const *texCoord = (mesh.HasTextureCoords(0)? &mesh.mTextureCoords[0][i] : &zero); aiVector3D const *tangent = (mesh.HasTangentsAndBitangents()? &mesh.mTangents[i] : &zero); aiVector3D const *bitang = (mesh.HasTangentsAndBitangents()? &mesh.mBitangents[i] : &zero); VBuf::Type v; v.pos = Vector3f(pos->x, pos->y, pos->z); v.normal = Vector3f(normal ->x, normal ->y, normal ->z); v.tangent = Vector3f(tangent->x, tangent->y, tangent->z); v.bitangent = Vector3f(bitang ->x, bitang ->y, bitang ->z); v.texCoord = Vector2f(texCoord->x, texCoord->y); v.texBounds = Vector4f(0, 0, 1, 1); v.texBounds2 = Vector4f(0, 0, 1, 1); v.texBounds3 = Vector4f(0, 0, 1, 1); v.texBounds4 = Vector4f(0, 0, 1, 1); /// @todo Add support for multiple UVs, not just mapping the same ones to /// different bounds. -jk if(scene->HasMaterials()) { Vector4f *texBounds[MAX_TEXTURES] { &v.texBounds, &v.texBounds2, &v.texBounds3, &v.texBounds4 }; MaterialData const &material = materials[mesh.mMaterialIndex]; for(int t = 0; t < MAX_TEXTURES; ++t) { // Apply the specified order for the textures. TextureMap map = textureOrder[t]; if(map < 0 || map >= MAX_TEXTURES) continue; if(material.texIds[map]) { *texBounds[t] = atlas->imageRectf(material.texIds[map]).xywh(); } } } for(int b = 0; b < MAX_BONES_PER_VERTEX; ++b) { v.boneIds[b] = vertexBones[base + i].ids[b]; v.boneWeights[b] = vertexBones[base + i].weights[b]; } verts << v; } // Get face indices. for(duint i = 0; i < mesh.mNumFaces; ++i) { aiFace const &face = mesh.mFaces[i]; DENG2_ASSERT(face.mNumIndices == 3); // expecting triangles indx << face.mIndices[0] + base << face.mIndices[1] + base << face.mIndices[2] + base; } base += mesh.mNumVertices; } // Get rid of an earlier buffer. delete buffer; buffer = new VBuf; buffer->setVertices(verts, gl::Static); buffer->setIndices(gl::Triangles, indx, gl::Static); } // Animation ------------------------------------------------------------------------ struct AccumData { ddouble time; aiAnimation const *anim; QVector finalTransforms; AccumData(int boneCount) : time(0) , anim(0) , finalTransforms(boneCount) {} aiNodeAnim const *findNodeAnim(aiNode const &node) const { for(duint i = 0; i < anim->mNumChannels; ++i) { aiNodeAnim const *na = anim->mChannels[i]; if(na->mNodeName == node.mName) { return na; } } return 0; } }; void accumulateAnimationTransforms(ddouble time, aiAnimation const &anim, aiNode const &rootNode) const { ddouble const ticksPerSec = anim.mTicksPerSecond? anim.mTicksPerSecond : 25.0; ddouble const timeInTicks = time * ticksPerSec; AccumData data(boneCount()); data.anim = &anim; data.time = std::fmod(timeInTicks, anim.mDuration); accumulateTransforms(rootNode, data); // Update the resulting matrices in the uniform. for(int i = 0; i < boneCount(); ++i) { uBoneMatrices.set(i, data.finalTransforms.at(i)); } } void accumulateTransforms(aiNode const &node, AccumData &data, Matrix4f const &parentTransform = Matrix4f()) const { Matrix4f nodeTransform = convertMatrix(node.mTransformation); if(aiNodeAnim const *anim = data.findNodeAnim(node)) { // Interpolate for this point in time. Matrix4f const translation = Matrix4f::translate(interpolatePosition(data.time, *anim)); Matrix4f const scaling = Matrix4f::scale(interpolateScaling(data.time, *anim)); Matrix4f const rotation = convertMatrix(aiMatrix4x4(interpolateRotation(data.time, *anim).GetMatrix())); nodeTransform = translation * rotation * scaling; } Matrix4f globalTransform = parentTransform * nodeTransform; int boneIndex = findBone(String(node.mName.C_Str())); if(boneIndex >= 0) { data.finalTransforms[boneIndex] = globalInverse * globalTransform * bones.at(boneIndex).offset; } // Descend to child nodes. for(duint i = 0; i < node.mNumChildren; ++i) { accumulateTransforms(*node.mChildren[i], data, globalTransform); } } template static duint findAnimKey(ddouble time, Type const *keys, duint count) { DENG2_ASSERT(count > 0); for(duint i = 0; i < count - 1; ++i) { if(time < keys[i + 1].mTime) { return i; } } DENG2_ASSERT(!"Failed to find animation key (invalid time?)"); return 0; } static Vector3f interpolateVectorKey(ddouble time, aiVectorKey const *keys, duint at) { Vector3f const start(&keys[at] .mValue.x); Vector3f const end (&keys[at + 1].mValue.x); return start + (end - start) * float((time - keys[at].mTime) / (keys[at + 1].mTime - keys[at].mTime)); } static aiQuaternion interpolateRotation(ddouble time, aiNodeAnim const &anim) { if(anim.mNumRotationKeys == 1) { return anim.mRotationKeys[0].mValue; } aiQuatKey const *key = anim.mRotationKeys + findAnimKey(time, anim.mRotationKeys, anim.mNumRotationKeys);; aiQuaternion interp; aiQuaternion::Interpolate(interp, key[0].mValue, key[1].mValue, (time - key[0].mTime) / (key[1].mTime - key[0].mTime)); interp.Normalize(); return interp; } static Vector3f interpolateScaling(ddouble time, aiNodeAnim const &anim) { if(anim.mNumScalingKeys == 1) { return Vector3f(&anim.mScalingKeys[0].mValue.x); } return interpolateVectorKey(time, anim.mScalingKeys, findAnimKey(time, anim.mScalingKeys, anim.mNumScalingKeys)); } static Vector3f interpolatePosition(ddouble time, aiNodeAnim const &anim) { if(anim.mNumPositionKeys == 1) { return Vector3f(&anim.mPositionKeys[0].mValue.x); } return interpolateVectorKey(time, anim.mPositionKeys, findAnimKey(time, anim.mPositionKeys, anim.mNumPositionKeys)); } // Drawing -------------------------------------------------------------------------- void updateMatricesFromAnimation(Animator const *animation) const { if(!scene->HasAnimations() || !animation) return; // Apply all current animations. for(int i = 0; i < animation->count(); ++i) { Animator::Animation const &anim = animation->at(i); // The animation has been validated earlier. DENG2_ASSERT(duint(anim.animId) < scene->mNumAnimations); DENG2_ASSERT(nodeNameToPtr.contains(anim.node)); accumulateAnimationTransforms(animation->currentTime(i), *scene->mAnimations[anim.animId], *nodeNameToPtr[anim.node]); } } void preDraw(Animator const *animation) { if(needMakeBuffer) makeBuffer(); DENG2_ASSERT(program != 0); DENG2_ASSERT(buffer != 0); // Draw the meshes in this node. updateMatricesFromAnimation(animation); GLState::current().apply(); program->bind(uBoneMatrices); program->beginUse(); } void draw(Animator const *animation) { preDraw(animation); buffer->draw(); postDraw(); } void drawInstanced(GLBuffer const &attribs, Animator const *animation) { preDraw(animation); buffer->drawInstanced(attribs); postDraw(); } void postDraw() { program->endUse(); program->unbind(uBoneMatrices); } DENG2_PIMPL_AUDIENCE(AboutToGLInit) }; DENG2_AUDIENCE_METHOD(ModelDrawable, AboutToGLInit) ModelDrawable::TextureMap ModelDrawable::textToTextureMap(String const &text) { struct { char const *text; TextureMap map; } const mappings[] { { "diffuse", Diffuse }, { "normals", Normals }, { "specular", Specular }, { "emission", Emissive }, { "height", Height } }; for(auto const &mapping : mappings) { if(!text.compareWithoutCase(mapping.text)) return mapping.map; } return Unknown; } ModelDrawable::ModelDrawable() : d(new Instance(this)) { *this += d->modelAsset; } void ModelDrawable::setImageLoader(IImageLoader &loader) { d->imageLoader = &loader; } void ModelDrawable::useDefaultImageLoader() { d->imageLoader = &defaultImageLoader; } void ModelDrawable::load(File const &file) { LOG_AS("ModelDrawable"); // Get rid of all existing data. clear(); d->import(file); } void ModelDrawable::clear() { glDeinit(); d->clear(); } int ModelDrawable::animationIdForName(String const &name) const { Instance::AnimLookup::const_iterator found = d->animNameToIndex.constFind(name); if(found != d->animNameToIndex.constEnd()) { return found.value(); } return -1; } int ModelDrawable::animationCount() const { if(!d->scene) return 0; return d->scene->mNumAnimations; } bool ModelDrawable::nodeExists(String const &name) const { return d->nodeNameToPtr.contains(name); } void ModelDrawable::setAtlas(AtlasTexture &atlas) { d->atlas = &atlas; } void ModelDrawable::unsetAtlas() { d->releaseTexturesFromAtlas(); d->atlas = 0; } ModelDrawable::Mapping ModelDrawable::diffuseNormalsSpecularEmission() // static { return Mapping() << Diffuse << Normals << Specular << Emissive; } void ModelDrawable::setTextureMapping(Mapping mapsToUse) { for(int i = 0; i < MAX_TEXTURES; ++i) { d->textureOrder[i] = (i < mapsToUse.size()? mapsToUse.at(i) : Unknown); // Height is an alias for normals. if(d->textureOrder[i] == Height) d->textureOrder[i] = Normals; } d->needMakeBuffer = true; } void ModelDrawable::setDefaultTexture(TextureMap textureType, Id const &atlasId) { DENG2_ASSERT(textureType >= 0 && textureType < MAX_TEXTURES); if(textureType < 0 || textureType >= MAX_TEXTURES) return; d->defaultTexIds[textureType] = atlasId; } void ModelDrawable::glInit() { d->glInit(); } void ModelDrawable::glDeinit() { d->glDeinit(); } int ModelDrawable::materialId(String const &name) const { return d->findMaterial(name); } void ModelDrawable::setTexturePath(int materialId, TextureMap tex, String const &path) { if(d->atlas) { // Load immediately. d->setTexture(materialId, tex, d->imageLoader->loadImage(path)); } else { // This will override what the model specifies. d->setCustomTexture(materialId, tex, path); } } void ModelDrawable::setProgram(GLProgram &program) { d->program = &program; } void ModelDrawable::unsetProgram() { d->program = 0; } void ModelDrawable::draw(Animator const *animation) const { const_cast(this)->glInit(); if(isReady() && d->program && d->atlas) { d->draw(animation); } } void ModelDrawable::drawInstanced(GLBuffer const &instanceAttribs, Animator const *animation) const { const_cast(this)->glInit(); if(isReady() && d->program && d->atlas) { d->drawInstanced(instanceAttribs, animation); } } Vector3f ModelDrawable::dimensions() const { return d->maxPoint - d->minPoint; } Vector3f ModelDrawable::midPoint() const { return (d->maxPoint + d->minPoint) / 2.f; } //--------------------------------------------------------------------------------------- DENG2_PIMPL_NOREF(ModelDrawable::Animator) { ModelDrawable const *model; typedef QList Animations; Animations anims; Instance(ModelDrawable const *mdl = 0) : model(mdl) {} Animation &add(Animation const &anim) { DENG2_ASSERT(model != 0); // Verify first. if(anim.animId < 0 || anim.animId >= model->animationCount()) { throw InvalidError("ModelDrawable::Animator::add", "Specified animation does not exist"); } if(!model->nodeExists(anim.node)) { throw InvalidError("ModelDrawable::Animator::add", "Node '" + anim.node + "' does not exist"); } anims.append(anim); return anims.last(); } void stopByNode(String const &node) { QMutableListIterator iter(anims); while(iter.hasNext()) { iter.next(); if(iter.value().node == node) { iter.remove(); } } } bool isRunning(int animId, String const &rootNode) const { foreach(Animation const &anim, anims) { if(anim.animId == animId && anim.node == rootNode) return true; } return false; } }; ModelDrawable::Animator::Animator() : d(new Instance) {} ModelDrawable::Animator::Animator(ModelDrawable const &model) : d(new Instance(&model)) {} void ModelDrawable::Animator::setModel(ModelDrawable const &model) { d->model = &model; } ModelDrawable const &ModelDrawable::Animator::model() const { DENG2_ASSERT(d->model != 0); return *d->model; } int ModelDrawable::Animator::count() const { return d->anims.size(); } ModelDrawable::Animator::Animation const &ModelDrawable::Animator::at(int index) const { return d->anims.at(index); } ModelDrawable::Animator::Animation &ModelDrawable::Animator::at(int index) { return d->anims[index]; } bool ModelDrawable::Animator::isRunning(String const &animName, String const &rootNode) const { return d->isRunning(model().animationIdForName(animName), rootNode); } bool ModelDrawable::Animator::isRunning(int animId, String const &rootNode) const { return d->isRunning(animId, rootNode); } ModelDrawable::Animator::Animation & ModelDrawable::Animator::start(String const &animName, String const &rootNode) { d->stopByNode(rootNode); Animation anim; anim.animId = model().animationIdForName(animName); anim.node = rootNode; anim.time = 0.0; return d->add(anim); } ModelDrawable::Animator::Animation & ModelDrawable::Animator::start(int animId, String const &rootNode) { d->stopByNode(rootNode); Animation anim; anim.animId = animId; anim.node = rootNode; anim.time = 0.0; return d->add(anim); } void ModelDrawable::Animator::stop(int index) { d->anims.removeAt(index); } void ModelDrawable::Animator::clear() { d->anims.clear(); } void ModelDrawable::Animator::advanceTime(TimeDelta const &) { // overridden } ddouble ModelDrawable::Animator::currentTime(int index) const { return at(index).time; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/gltarget_alternativebuffer.cpp0000664000175000017500000000502112641367671027761 0ustar jaakkojaakko/** @file gltarget_alternativebuffer.cpp Alternative buffer attachment for GLTarget. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GLTarget" #include "de/GLTexture" namespace de { DENG2_PIMPL_NOREF(GLTarget::AlternativeBuffer) { GLTarget *target; GLTexture *texture; GLTarget::Flags attachment; GLTexture *original; Instance() : target(0) , texture(0) , attachment(GLTarget::NoAttachments) , original(0) {} }; GLTarget::AlternativeBuffer::AlternativeBuffer(GLTarget &target, GLTexture &texture, Flags const &attachment) : d(new Instance) { d->target = ⌖ d->texture = &texture; d->attachment = attachment; } GLTarget::AlternativeBuffer::~AlternativeBuffer() { deinit(); } bool GLTarget::AlternativeBuffer::init() { if(d->original) { // Already done. return false; } // Remember the original attachment. d->original = d->target->attachedTexture(d->attachment); DENG2_ASSERT(d->original != 0); // Resize the alternative buffer to match current target size. if(d->texture->size() != d->target->size()) { if(d->attachment == GLTarget::DepthStencil) { d->texture->setDepthStencilContent(d->target->size()); } else { DENG2_ASSERT(!"GLTarget::AlternativeBuffer does not support resizing specified attachment type"); } } d->target->replaceAttachment(d->attachment, *d->texture); return true; } bool GLTarget::AlternativeBuffer::deinit() { if(!d->original) return false; // Not inited. d->target->replaceAttachment(d->attachment, *d->original); return true; } GLTarget &GLTarget::AlternativeBuffer::target() const { return *d->target; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/colorbank.cpp0000664000175000017500000000624712641367671024345 0ustar jaakkojaakko/** @file colorbank.cpp Bank of colors. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/ColorBank" #include #include namespace de { DENG2_PIMPL(ColorBank) { struct ColorSource : public ISource { ColorBank &bank; String id; ColorSource(ColorBank &b, String const &colorId) : bank(b), id(colorId) {} Time modifiedAt() const { return bank.sourceModifiedAt(); } Vector4d load() const { Record const &def = bank[id]; ArrayValue const *colorDef = 0; if(def.has("rgb")) { colorDef = &def.geta("rgb"); } else { colorDef = &def.geta("rgba"); } ddouble alpha = 1.0; if(colorDef->size() >= 4) { alpha = colorDef->at(3).asNumber(); } return Vector4d(colorDef->at(0).asNumber(), colorDef->at(1).asNumber(), colorDef->at(2).asNumber(), alpha); } }; struct ColorData : public IData { Vector4d color; ColorData(Vector4d const &c = Vector4d()) : color(c) {} }; Instance(Public *i) : Base(i) {} }; ColorBank::ColorBank() : InfoBank("ColorBank", DisableHotStorage), d(new Instance(this)) {} void ColorBank::addFromInfo(File const &file) { LOG_AS("ColorBank"); parse(file); addFromInfoBlocks("color"); } ColorBank::Color ColorBank::color(DotPath const &path) const { if(path.isEmpty()) return Color(); Colorf col = colorf(path); return Color(round(col.x * 255), round(col.y * 255), round(col.z * 255), round(col.w * 255)); } ColorBank::Colorf ColorBank::colorf(DotPath const &path) const { if(path.isEmpty()) return Colorf(); Vector4d clamped = data(path).as().color; clamped = clamped.max(Vector4d(0, 0, 0, 0)).min(Vector4d(1, 1, 1, 1)); return Colorf(float(clamped.x), float(clamped.y), float(clamped.z), float(clamped.w)); } Bank::ISource *ColorBank::newSourceFromInfo(String const &id) { return new Instance::ColorSource(*this, id); } Bank::IData *ColorBank::loadFromSource(ISource &source) { return new Instance::ColorData(source.as().load()); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/graphics/drawable.cpp0000664000175000017500000002744012641367671024152 0ustar jaakkojaakko/** @file drawable.cpp Drawable object with buffers, programs and states. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/Drawable" #include namespace de { DENG2_PIMPL(Drawable) { typedef QMap Buffers; typedef QMap Programs; typedef QMap States; typedef QMap Names; struct BufferConfig { GLProgram const *program; GLState const *state; BufferConfig(GLProgram const *p = 0, GLState const *s = 0) : program(p), state(s) {} }; typedef QMap BufferConfigs; Buffers buffers; Programs programs; States states; Names bufferNames; Names programNames; Names stateNames; BufferConfigs configs; GLProgram defaultProgram; Instance(Public *i) : Base(i) { self += defaultProgram; } ~Instance() { clear(); } void clear() { qDeleteAll(buffers.values()); qDeleteAll(programs.values()); qDeleteAll(states.values()); defaultProgram.clear(); buffers.clear(); programs.clear(); states.clear(); configs.clear(); bufferNames.clear(); programNames.clear(); stateNames.clear(); } Id nextBufferId() const { Id next = 1; // Keys of a QMap are sorted in ascending order. if(!buffers.isEmpty()) next = buffers.keys().back() + 1; return next; } Id nextProgramId() const { Id next = 1; // Keys of a QMap are sorted in ascending order. if(!programs.isEmpty()) next = programs.keys().back() + 1; return next; } Id nextStateId() const { Id next = 1; // Keys of a QMap are sorted in ascending order. if(!states.isEmpty()) next = states.keys().back() + 1; return next; } void replaceProgram(GLProgram const *src, GLProgram const *dest) { DENG2_FOR_EACH(BufferConfigs, i, configs) { if(i.value().program == src) { i.value().program = dest; } } } void replaceState(GLState const *src, GLState const *dest) { DENG2_FOR_EACH(BufferConfigs, i, configs) { if(i.value().state == src) { i.value().state = dest; } } } void removeName(Names &names, Id id) { QMutableMapIterator iter(names); while(iter.hasNext()) { iter.next(); if(iter.value() == id) { iter.remove(); } } } }; Drawable::Drawable() : d(new Instance(this)) {} void Drawable::clear() { d->clear(); } Drawable::Ids Drawable::allBuffers() const { return d->buffers.keys(); } Drawable::Ids Drawable::allPrograms() const { Ids ids; ids << 0 // default program is always there << d->programs.keys(); return ids; } Drawable::Ids Drawable::allStates() const { return d->states.keys(); } bool Drawable::hasBuffer(Id id) const { return d->buffers.contains(id); } GLBuffer &Drawable::buffer(Id id) const { DENG2_ASSERT(d->buffers.contains(id)); return *d->buffers[id]; } GLBuffer &Drawable::buffer(Name const &bufferName) const { return buffer(bufferId(bufferName)); } Drawable::Id Drawable::bufferId(Name const &bufferName) const { DENG2_ASSERT(d->bufferNames.contains(bufferName)); return d->bufferNames[bufferName]; } GLProgram &Drawable::program(Id id) const { if(!id) return d->defaultProgram; DENG2_ASSERT(d->programs.contains(id)); return *d->programs[id]; } GLProgram &Drawable::program(Name const &programName) const { return program(programId(programName)); } Drawable::Id Drawable::programId(Name const &programName) const { DENG2_ASSERT(d->programNames.contains(programName)); return d->programNames[programName]; } GLProgram const &Drawable::programForBuffer(Id bufferId) const { DENG2_ASSERT(d->configs.contains(bufferId)); DENG2_ASSERT(d->configs[bufferId].program != 0); return *d->configs[bufferId].program; } GLProgram const &Drawable::programForBuffer(Name const &bufferName) const { return programForBuffer(bufferId(bufferName)); } GLState const *Drawable::stateForBuffer(Id bufferId) const { return d->configs[bufferId].state; } GLState const *Drawable::stateForBuffer(Name const &bufferName) const { return stateForBuffer(bufferId(bufferName)); } GLState &Drawable::state(Id id) const { DENG2_ASSERT(d->states.contains(id)); return *d->states[id]; } GLState &Drawable::state(Name const &stateName) const { return state(stateId(stateName)); } Drawable::Id Drawable::stateId(Name const &stateName) const { DENG2_ASSERT(d->stateNames.contains(stateName)); return d->stateNames[stateName]; } void Drawable::addBuffer(Id id, GLBuffer *buffer) { removeBuffer(id); d->buffers[id] = buffer; setProgram(id, d->defaultProgram); insert(*buffer, Required); } Drawable::Id Drawable::addBuffer(Name const &bufferName, GLBuffer *buffer) { Id id = d->nextBufferId(); d->bufferNames.insert(bufferName, id); addBuffer(id, buffer); return id; } Drawable::Id Drawable::addBuffer(GLBuffer *buffer) { Id const id = d->nextBufferId(); addBuffer(id, buffer); return id; } Drawable::Id Drawable::addBufferWithNewProgram(GLBuffer *buffer, Name const &programName) { // Take ownership of the buffer. Id const bufId = d->nextBufferId(); addBuffer(bufId, buffer); // Assign a new program to the buffer. Id const progId = addProgram(programName); setProgram(bufId, program(progId)); return bufId; } void Drawable::addBufferWithNewProgram(Id id, GLBuffer *buffer, Name const &programName) { addBuffer(id, buffer); addProgram(programName); setProgram(id, programName); } Drawable::Id Drawable::addBufferWithNewProgram(Name const &bufferName, GLBuffer *buffer, Name const &programName) { Id const progId = addProgram(programName); Id const bufId = addBuffer(bufferName, buffer); setProgram(bufId, program(progId)); return bufId; } GLProgram &Drawable::addProgram(Id id) { // Program 0 is the default program. DENG2_ASSERT(id != 0); removeProgram(id); GLProgram *p = new GLProgram; d->programs[id] = p; insert(*p, Required); return *p; } Drawable::Id Drawable::addProgram(Name const &programName) { Id const id = d->nextProgramId(); addProgram(id); if(!programName.isEmpty()) { d->programNames.insert(programName, id); } return id; } GLState &Drawable::addState(Id id, GLState const &state) { removeState(id); GLState *s = new GLState(state); d->states[id] = s; return *s; } Drawable::Id Drawable::addState(Name const &stateName, GLState const &state) { Id const id = d->nextStateId(); addState(id, state); d->stateNames.insert(stateName, id); return id; } void Drawable::removeBuffer(Id id) { if(d->buffers.contains(id)) { remove(*d->buffers[id]); delete d->buffers.take(id); } d->configs.remove(id); } void Drawable::removeProgram(Id id) { if(d->programs.contains(id)) { GLProgram *prog = d->programs[id]; d->replaceProgram(prog, &d->defaultProgram); remove(*prog); delete d->programs.take(id); } } void Drawable::removeState(Id id) { if(d->programs.contains(id)) { GLState *st = d->states[id]; d->replaceState(st, 0); delete d->states.take(id); } } void Drawable::removeBuffer(Name const &bufferName) { Id const id = bufferId(bufferName); removeBuffer(id); d->removeName(d->bufferNames, id); } void Drawable::removeProgram(Name const &programName) { Id const id = programId(programName); removeProgram(id); d->removeName(d->programNames, id); } void Drawable::removeState(Name const &stateName) { Id const id = stateId(stateName); removeState(id); d->removeName(d->stateNames, id); } void Drawable::setProgram(Id bufferId, GLProgram &program) { d->configs[bufferId].program = &program; } void Drawable::setProgram(Id bufferId, Name const &programName) { setProgram(bufferId, program(programName)); } void Drawable::setProgram(Name const &bufferName, GLProgram &program) { setProgram(bufferId(bufferName), program); } void Drawable::setProgram(Name const &bufferName, Name const &programName) { setProgram(bufferId(bufferName), program(programName)); } void Drawable::setProgram(GLProgram &program) { foreach(Id id, allBuffers()) { setProgram(id, program); } } void Drawable::setProgram(Name const &programName) { setProgram(program(programName)); } void Drawable::setState(Id bufferId, GLState &state) { d->configs[bufferId].state = &state; } void Drawable::setState(Id bufferId, Name const &stateName) { setState(bufferId, state(stateName)); } void Drawable::setState(Name const &bufferName, GLState &state) { setState(bufferId(bufferName), state); } void Drawable::setState(Name const &bufferName, Name const &stateName) { setState(bufferId(bufferName), state(stateName)); } void Drawable::setState(GLState &state) { foreach(Id id, allBuffers()) { setState(id, state); } } void Drawable::setState(Name const &stateName) { setState(state(stateName)); } void Drawable::unsetState(Id bufferId) { d->configs[bufferId].state = 0; } void Drawable::unsetState(Name const &bufferName) { unsetState(bufferId(bufferName)); } void Drawable::unsetState() { foreach(Id id, allBuffers()) { unsetState(id); } } void Drawable::draw() const { // Ignore the draw request until everything is ready. if(!isReady()) return; GLProgram const *currentProgram = 0; GLState const *currentState = 0; // Make sure the GL state on the top of the stack is in effect. GLState::current().apply(); DENG2_FOR_EACH_CONST(Instance::Buffers, i, d->buffers) { Id const id = i.key(); // Switch the program if necessary. GLProgram const &bufProg = programForBuffer(id); if(currentProgram != &bufProg) { if(currentProgram) currentProgram->endUse(); currentProgram = &bufProg; currentProgram->beginUse(); } // If a state has been defined, use it. GLState const *bufState = stateForBuffer(id); if(bufState && currentState != bufState) { currentState = bufState; currentState->apply(); } else if(!bufState && currentState != 0) { // Use the current state from the stack. currentState = 0; GLState::current().apply(); } // Ready to draw. i.value()->draw(); } // Cleanup. if(currentProgram) { currentProgram->endUse(); } if(currentState) { // We messed with the state; restore to what the stack says is current. GLState::current().apply(); } } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/displaymode_dummy.cpp0000664000175000017500000000340412641367671024310 0ustar jaakkojaakko/** @file displaymode_dummy.cpp * Dummy implementation of the DisplayMode native functionality. * @ingroup gl * * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/gui/displaymode_native.h" #include void DisplayMode_Native_Init(void) { } void DisplayMode_Native_Shutdown(void) { } int DisplayMode_Native_Count(void) { return 0; } void DisplayMode_Native_GetMode(int index, DisplayMode *mode) { DENG2_UNUSED(index); DENG2_UNUSED(mode); } void DisplayMode_Native_GetCurrentMode(DisplayMode *mode) { DENG2_UNUSED(mode); } int DisplayMode_Native_Change(DisplayMode const *mode, int shouldCapture) { DENG2_UNUSED(mode); DENG2_UNUSED(shouldCapture); return true; } void DisplayMode_Native_GetColorTransfer(DisplayColorTransfer *colors) { DENG2_UNUSED(colors); } void DisplayMode_Native_SetColorTransfer(DisplayColorTransfer const *colors) { DENG2_UNUSED(colors); } #ifdef MACOSX void DisplayMode_Native_Raise(void* handle) { DENG2_UNUSED(handle); } #endif doomsday-stable-1.15.7/doomsday/libgui/src/audio/0000775000175000017500000000000012641367671021157 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/src/audio/waveform.cpp0000664000175000017500000001331712641367671023516 0ustar jaakkojaakko/** @file waveform.cpp Audio waveform. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/Waveform" #include #include namespace de { namespace internal { struct WAVChunk : public IReadable { Block id; duint32 size; WAVChunk() : id(4), size(0) {} void operator << (Reader &from) { from.readBytes(4, id) >> size; } }; struct WAVFormat : public IReadable { duint16 formatTag; duint16 channels; duint32 sampleRate; duint32 averageBytesPerSecond; duint16 blockAlign; duint16 bitsPerSample; WAVFormat() : formatTag(0) , channels(0) , sampleRate(0) , averageBytesPerSecond(0) , blockAlign(0) , bitsPerSample(0) {} void operator << (Reader &from) { from >> formatTag >> channels >> sampleRate >> averageBytesPerSecond >> blockAlign >> bitsPerSample; } }; } // namespace internal using namespace internal; DENG2_PIMPL(Waveform) , DENG2_OBSERVES(File, Deletion) { audio::Format format; Block sampleData; File const *source; duint channelCount; duint bitsPerSample; dsize sampleCount; duint sampleRate; Instance(Public *i) : Base(i) , format(audio::PCMLittleEndian) , source(0) , channelCount (0) , bitsPerSample(0) , sampleCount (0) , sampleRate (0.0) {} ~Instance() { setSource(0); } void setSource(File const *src) { if(source) source->audienceForDeletion() -= this; source = src; if(source) source->audienceForDeletion() += this; } void clear() { setSource(0); format = audio::PCMLittleEndian; sampleData.clear(); channelCount = 0; bitsPerSample = 0; sampleCount = 0; sampleRate = 0.0; } void load(File const &src) { if(!src.name().fileNameExtension().compareWithoutCase(".wav")) { // We know how to read WAV files. loadWAV(Block(src)); } else { // Let's assume it's a compressed audio format. format = audio::Compressed; } setSource(&src); } static bool recognizeWAV(IByteArray const &data) { Block magic(4); data.get(0, magic.data(), 4); if(magic != "RIFF") return false; data.get(8, magic.data(), 4); return (magic == "WAVE"); } /** * Loads a sequence of audio samples in WAV format. * * @param data Block containing WAV format data. */ void loadWAV(Block const &data) { if(!recognizeWAV(data)) { throw LoadError("Waveform::loadWAV", "WAV identifier not found"); } Reader reader(data); reader.seek(12); // skip past header WAVFormat wav; while(reader.remainingSize() >= 8) { WAVChunk chunk; reader >> chunk; if(chunk.id == "fmt ") // Format chunk. { reader >> wav; // Check limitations. if(wav.formatTag != 1 /* PCM samples */) { throw UnsupportedFormatError("Waveform::loadWAV", "Only PCM samples supported"); } channelCount = wav.channels; sampleRate = wav.sampleRate; bitsPerSample = wav.bitsPerSample; } else if(chunk.id == "data") // Sample data chunk. { sampleCount = chunk.size / wav.blockAlign; sampleData.resize(chunk.size); reader.readPresetSize(sampleData); // keep it little endian } else { // It's an unknown chunk. reader.seek(chunk.size); } } format = audio::PCMLittleEndian; } void fileBeingDeleted(File const &delFile) { if(source == &delFile) { // Could read file contents to memory if the waveform data // is still needed? source = 0; } } }; Waveform::Waveform() : d(new Instance(this)) {} void Waveform::clear() { d->clear(); } void Waveform::load(File const &file) { d->clear(); d->load(file); } audio::Format Waveform::format() const { return d->format; } Block Waveform::sampleData() const { return d->sampleData; } File const *Waveform::sourceFile() const { return d->source; } duint Waveform::channelCount() const { return d->channelCount; } duint Waveform::bitsPerSample() const { return d->bitsPerSample; } dsize Waveform::sampleCount() const { return d->sampleCount; } duint Waveform::sampleRate() const { return d->sampleRate; } TimeDelta Waveform::duration() const { return d->sampleRate * d->sampleCount; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/audio/waveformbank.cpp0000664000175000017500000000511712641367671024351 0ustar jaakkojaakko/** @file waveformbank.cpp Bank containing Waveform instances. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/WaveformBank" #include "de/App" #include namespace de { DENG2_PIMPL_NOREF(WaveformBank) { struct Source : public ISource { String filePath; Source(String const &path) : filePath(path) {} Time modifiedAt() const { return App::rootFolder().locate(filePath).status().modifiedAt; } Waveform *load() const { QScopedPointer wf(new Waveform); wf->load(App::rootFolder().locate(filePath)); return wf.take(); } }; struct Data : public IData { Waveform *waveform; Data(Waveform *wf = 0) : waveform(wf) {} duint sizeInMemory() const { if(!waveform) return 0; return waveform->sampleData().size(); } }; }; WaveformBank::WaveformBank(Flags const &flags) : InfoBank("WaveformBank", flags), d(new Instance) {} void WaveformBank::add(DotPath const &id, String const &waveformFilePath) { Bank::add(id, new Instance::Source(waveformFilePath)); } void WaveformBank::addFromInfo(File const &file) { LOG_AS("WaveformBank"); parse(file); addFromInfoBlocks("waveform"); } Waveform const &WaveformBank::waveform(DotPath const &id) const { return *data(id).as().waveform; } Bank::ISource *WaveformBank::newSourceFromInfo(String const &id) { Record const &def = info()[id]; return new Instance::Source(relativeToPath(def) / def["path"]); } Bank::IData *WaveformBank::loadFromSource(ISource &source) { return new Instance::Data(source.as().load()); } Bank::IData *WaveformBank::newData() { return new Instance::Data(); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/audio/sound.cpp0000664000175000017500000000620312641367671023014 0ustar jaakkojaakko/** @file sound.cpp Interface for playing sounds. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/Sound" namespace de { DENG2_PIMPL(Sound) { dfloat volume; dfloat pan; dfloat frequency; Vector3f position; Vector3f velocity; Positioning positioning; dfloat minDistance; dfloat spread; Instance(Public *i) : Base(i) , volume(1.f) , pan(0.f) , frequency(1.f) , positioning(Stereo) , minDistance(1.f) , spread(0) {} void update() { DENG2_FOR_PUBLIC_AUDIENCE2(Change, i) { i->soundPropertyChanged(self); } self.update(); } DENG2_PIMPL_AUDIENCE(Play) DENG2_PIMPL_AUDIENCE(Change) DENG2_PIMPL_AUDIENCE(Stop) DENG2_PIMPL_AUDIENCE(Deletion) }; DENG2_AUDIENCE_METHOD(Sound, Play) DENG2_AUDIENCE_METHOD(Sound, Change) DENG2_AUDIENCE_METHOD(Sound, Stop) DENG2_AUDIENCE_METHOD(Sound, Deletion) Sound::Sound() : d(new Instance(this)) {} Sound &Sound::setVolume(dfloat volume) { d->volume = volume; d->update(); return *this; } Sound &Sound::setPan(dfloat pan) { d->pan = pan; d->update(); return *this; } Sound &Sound::setFrequency(dfloat factor) { d->frequency = factor; d->update(); return *this; } Sound &Sound::setPosition(Vector3f const &position, Positioning positioning) { d->position = position; d->positioning = positioning; d->update(); return *this; } Sound &Sound::setVelocity(Vector3f const &velocity) { d->velocity = velocity; d->update(); return *this; } Sound &Sound::setMinDistance(dfloat minDistance) { d->minDistance = minDistance; d->update(); return *this; } Sound &Sound::setSpatialSpread(dfloat degrees) { d->spread = degrees; d->update(); return *this; } bool Sound::isPlaying() const { return mode() != NotPlaying; } dfloat Sound::volume() const { return d->volume; } dfloat Sound::pan() const { return d->pan; } dfloat Sound::frequency() const { return d->frequency; } Vector3f Sound::position() const { return d->position; } Sound::Positioning Sound::positioning() const { return d->positioning; } Vector3f Sound::velocity() const { return d->velocity; } dfloat Sound::minDistance() const { return d->minDistance; } dfloat Sound::spatialSpread() const { return d->spread; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/canvaswindow.cpp0000664000175000017500000001716012641367671023272 0ustar jaakkojaakko/** @file canvaswindow.cpp Canvas window implementation. * @ingroup base * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/CanvasWindow" #include "de/GuiApp" #include #include #include #include #include #include #include #include #include #include #include #include namespace de { static CanvasWindow *mainWindow = 0; DENG2_PIMPL(CanvasWindow) { Canvas* canvas; ///< Drawing surface for the contents of the window. Canvas* recreated; Canvas::FocusChangeAudience canvasFocusAudience; ///< Stored here during recreation. bool ready; bool mouseWasTrapped; unsigned int frameCount; float fps; Instance(Public *i) : Base(i), canvas(0), recreated(0), ready(false), mouseWasTrapped(false), frameCount(0), fps(0) {} ~Instance() { if(thisPublic == mainWindow) { mainWindow = 0; } } void updateFrameRateStatistics(void) { static Time lastFpsTime; Time const nowTime = Clock::appTime(); // Increment the (local) frame counter. frameCount++; // Count the frames every other second. TimeDelta elapsed = nowTime - lastFpsTime; if(elapsed > 2.5) { fps = frameCount / elapsed; lastFpsTime = nowTime; frameCount = 0; } } void finishCanvasRecreation() { DENG2_ASSERT_IN_MAIN_THREAD(); LOGDEV_GL_MSG("About to replace Canvas %p with %p") << de::dintptr(canvas) << de::dintptr(recreated); // Copy the audiences of the old canvas. recreated->copyAudiencesFrom(*canvas); // Switch the central widget. This will delete the old canvas automatically. self.setCentralWidget(recreated); canvas = recreated; recreated = 0; // Set up the basic GL state for the new canvas. canvas->makeCurrent(); LIBGUI_ASSERT_GL_OK(); DENG2_FOR_EACH_OBSERVER(Canvas::GLInitAudience, i, canvas->audienceForGLInit()) { i->canvasGLInit(*canvas); } DENG2_GUI_APP->notifyGLContextChanged(); #ifdef DENG_X11 canvas->update(); #else canvas->updateGL(); #endif LIBGUI_ASSERT_GL_OK(); // Reacquire the focus. canvas->setFocus(); if(mouseWasTrapped) { canvas->trapMouse(); } // Restore the old focus change audience. canvas->audienceForFocusChange() = canvasFocusAudience; LOGDEV_GL_MSG("Canvas replaced with %p") << de::dintptr(canvas); } }; CanvasWindow::CanvasWindow() : QMainWindow(0), d(new Instance(this)) { // Create the drawing canvas for this window. setCentralWidget(d->canvas = new Canvas(this)); // takes ownership d->canvas->audienceForGLReady() += this; d->canvas->audienceForGLDraw() += this; // All input goes to the canvas. d->canvas->setFocus(); } bool CanvasWindow::isReady() const { return d->ready; } float CanvasWindow::frameRate() const { return d->fps; } void CanvasWindow::recreateCanvas() { DENG2_ASSERT_IN_MAIN_THREAD(); GLState::considerNativeStateUndefined(); d->ready = false; // Steal the focus change audience temporarily so no spurious focus // notifications are sent. d->canvasFocusAudience = canvas().audienceForFocusChange(); canvas().audienceForFocusChange().clear(); // We'll re-trap the mouse after the new canvas is ready. d->mouseWasTrapped = canvas().isMouseTrapped(); canvas().trapMouse(false); canvas().setParent(0); canvas().hide(); // Create the replacement Canvas. Once it's created and visible, we'll // finish the switch-over. d->recreated = new Canvas(this, d->canvas); d->recreated->audienceForGLReady() += this; //d->recreated->setGeometry(d->canvas->geometry()); d->recreated->show(); d->recreated->update(); LIBGUI_ASSERT_GL_OK(); LOGDEV_GL_MSG("Canvas recreated, old one still exists"); qDebug() << "old Canvas" << &canvas(); qDebug() << "new Canvas" << d->recreated; } bool CanvasWindow::isRecreationInProgress() const { return d->recreated != 0; } Canvas &CanvasWindow::canvas() const { DENG2_ASSERT(d->canvas != 0); return *d->canvas; } bool CanvasWindow::ownsCanvas(Canvas *c) const { if(!c) return false; return (d->canvas == c || d->recreated == c); } #ifdef WIN32 bool CanvasWindow::event(QEvent *ev) { if(ev->type() == QEvent::ActivationChange) { //LOG_DEBUG("CanvasWindow: Forwarding QEvent::KeyRelease, Qt::Key_Alt"); QKeyEvent keyEvent = QKeyEvent(QEvent::KeyRelease, Qt::Key_Alt, Qt::NoModifier); return QApplication::sendEvent(&canvas(), &keyEvent); } return QMainWindow::event(ev); } #endif void CanvasWindow::hideEvent(QHideEvent *ev) { LOG_AS("CanvasWindow"); QMainWindow::hideEvent(ev); LOG_GL_VERBOSE("Hide event (hidden:%b)") << isHidden(); } void CanvasWindow::canvasGLReady(Canvas &canvas) { d->ready = true; if(d->recreated == &canvas) { #ifndef DENG_X11 d->finishCanvasRecreation(); #else // Need to defer the finalization. qDebug() << "defer recreation"; QTimer::singleShot(100, this, SLOT(finishCanvasRecreation())); #endif } } void CanvasWindow::canvasGLDraw(Canvas &) { d->updateFrameRateStatistics(); } duint CanvasWindow::grabAsTexture(GrabMode mode) const { return d->canvas->grabAsTexture( mode == GrabHalfSized? QSize(width()/2, height()/2) : QSize()); } duint CanvasWindow::grabAsTexture(Rectanglei const &area, GrabMode mode) const { QSize size; if(mode == GrabHalfSized) { size = QSize(area.width()/2, area.height()/2); } return d->canvas->grabAsTexture( QRect(area.left(), area.top(), area.width(), area.height()), size); } bool CanvasWindow::grabToFile(NativePath const &path) const { return d->canvas->grabImage().save(path.toString()); } void CanvasWindow::swapBuffers(gl::SwapBufferMode swapMode) const { // Force a swapbuffers right now. d->canvas->swapBuffers(swapMode); } void CanvasWindow::glActivate() { canvas().makeCurrent(); } void CanvasWindow::glDone() { canvas().doneCurrent(); } void *CanvasWindow::nativeHandle() const { return reinterpret_cast(winId()); } void CanvasWindow::finishCanvasRecreation() { d->finishCanvasRecreation(); } bool CanvasWindow::mainExists() { return mainWindow != 0; } CanvasWindow &CanvasWindow::main() { DENG2_ASSERT(mainWindow != 0); return *mainWindow; } void CanvasWindow::setMain(CanvasWindow *window) { mainWindow = window; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/persistentcanvaswindow.cpp0000664000175000017500000007061312641367671025415 0ustar jaakkojaakko/** @file persistentcanvaswindow.cpp Canvas window with persistent state. * @ingroup gui * * @todo Platform-specific behavior should be encapsulated in subclasses, e.g., * MacWindowBehavior. This would make the code easier to follow and more adaptable * to the quirks of each platform. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/PersistentCanvasWindow" #include "de/GuiApp" #include "de/DisplayMode" #include #include #include #include #include #include #include namespace de { static String const MAIN_WINDOW_ID = "main"; int const PersistentCanvasWindow::MIN_WIDTH = 320; int const PersistentCanvasWindow::MIN_HEIGHT = 240; static int const BREAK_CENTERING_THRESHOLD = 5; static QRect desktopRect() { /// @todo Multimonitor? This checks the default screen. return QApplication::desktop()->screenGeometry(); } static QRect centeredQRect(Vector2ui const &size) { Vector2ui const screenSize(desktopRect().size().width(), desktopRect().size().height()); Vector2ui const clamped = size.min(screenSize); LOGDEV_GL_XVERBOSE("centeredGeometry: Current desktop rect %i x %i") << screenSize.x << screenSize.y; return QRect(desktopRect().topLeft() + QPoint((screenSize.x - clamped.x) / 2, (screenSize.y - clamped.y) / 2), QSize(clamped.x, clamped.y)); } static Rectanglei centeredRect(Vector2ui const &size) { QRect rect = centeredQRect(size); return Rectanglei(rect.left(), rect.top(), rect.width(), rect.height()); } static void notifyAboutModeChange() { /// @todo This should be done using an observer. LOG_GL_NOTE("Display mode has changed"); DENG2_GUI_APP->notifyDisplayModeChanged(); } DENG2_PIMPL(PersistentCanvasWindow) { /** * Logical state of a window. */ struct State { enum Flag { None = 0, Fullscreen = 0x1, Centered = 0x2, Maximized = 0x4, FSAA = 0x8, VSync = 0x10 }; typedef int Flags; String winId; Rectanglei windowRect; ///< Window geometry in windowed mode. Size fullSize; ///< Dimensions in a fullscreen mode. int colorDepthBits; Flags flags; State(String const &id) : winId(id), colorDepthBits(0), flags(None) {} bool operator == (State const &other) const { return (winId == other.winId && windowRect == other.windowRect && fullSize == other.fullSize && colorDepthBits == other.colorDepthBits && flags == other.flags); } bool operator != (State const &other) const { return !(*this == other); } bool isCentered() const { return (flags & Centered) != 0; } bool isWindow() const { return !isFullscreen() && !isMaximized(); } bool isFullscreen() const { return (flags & Fullscreen) != 0; } bool isMaximized() const { return (flags & Maximized) != 0; } bool isAntialiased() const { return (flags & FSAA) != 0; } bool isVSync() const { return (flags & VSync) != 0; } void setFlag(Flags const &f, bool set = true) { if(set) { flags |= f; if(f & Maximized) LOGDEV_GL_VERBOSE("Setting State::Maximized"); } else { flags &= ~f; if(f & Centered) LOGDEV_GL_VERBOSE("Clearing State::Centered"); if(f & Maximized) LOGDEV_GL_VERBOSE("Clearing State::Maximized"); } } QString configName(String const &key) const { return QString("window.%1.%2").arg(winId).arg(key); } void saveToConfig() { Config &config = App::config(); ArrayValue *array = new ArrayValue; *array << NumberValue(windowRect.left()) << NumberValue(windowRect.top()) << NumberValue(windowRect.width()) << NumberValue(windowRect.height()); config.set(configName("rect"), array); array = new ArrayValue; *array << NumberValue(fullSize.x) << NumberValue(fullSize.y); config.set(configName("fullSize"), array); config.set(configName("center"), isCentered()); config.set(configName("maximize"), isMaximized()); config.set(configName("fullscreen"), isFullscreen()); config.set(configName("colorDepth"), colorDepthBits); // FSAA and vsync are saved as part of the Config. //config.set(configName("fsaa"), isAntialiased()); //config.set(configName("vsync"), isVSync()); } void restoreFromConfig() { Config &config = App::config(); // The default state of the window is determined by these values. ArrayValue const &rect = config.geta(configName("rect")); if(rect.size() >= 4) { windowRect = Rectanglei(rect.at(0).asNumber(), rect.at(1).asNumber(), rect.at(2).asNumber(), rect.at(3).asNumber()); } ArrayValue const &fs = config.geta(configName("fullSize")); if(fs.size() >= 2) { fullSize = Size(fs.at(0).asNumber(), fs.at(1).asNumber()); } colorDepthBits = config.geti(configName("colorDepth")); setFlag(Centered, config.getb(configName("center"))); setFlag(Maximized, config.getb(configName("maximize"))); setFlag(Fullscreen, config.getb(configName("fullscreen"))); setFlag(FSAA, config.getb(configName("fsaa"))); setFlag(VSync, config.getb(configName("vsync"))); } /** * Determines if the window will overtake the entire screen. */ bool shouldCaptureScreen() const { return isFullscreen() && !DisplayMode_IsEqual(displayMode(), DisplayMode_OriginalMode()); } /** * Determines the display mode that this state will use in * fullscreen mode. */ DisplayMode const *displayMode() const { if(isFullscreen()) { return DisplayMode_FindClosest(fullSize.x, fullSize.y, colorDepthBits, 0); } return DisplayMode_OriginalMode(); } void applyAttributes(int const *attribs) { for(int i = 0; attribs[i]; ++i) { switch(attribs[i++]) { case PersistentCanvasWindow::Left: windowRect.moveTopLeft(Vector2i(attribs[i], windowRect.topLeft.y)); break; case PersistentCanvasWindow::Top: windowRect.moveTopLeft(Vector2i(windowRect.topLeft.x, attribs[i])); break; case PersistentCanvasWindow::Width: windowRect.setWidth(de::max(attribs[i], MIN_WIDTH)); break; case PersistentCanvasWindow::Height: windowRect.setHeight(de::max(attribs[i], MIN_HEIGHT)); break; case PersistentCanvasWindow::Centered: setFlag(State::Centered, attribs[i]); break; case PersistentCanvasWindow::Maximized: setFlag(State::Maximized, attribs[i]); if(attribs[i]) setFlag(State::Fullscreen, false); break; case PersistentCanvasWindow::Fullscreen: setFlag(State::Fullscreen, attribs[i]); if(attribs[i]) setFlag(State::Maximized, false); break; case PersistentCanvasWindow::FullscreenWidth: fullSize.x = attribs[i]; break; case PersistentCanvasWindow::FullscreenHeight: fullSize.y = attribs[i]; break; case PersistentCanvasWindow::ColorDepthBits: colorDepthBits = attribs[i]; DENG2_ASSERT(colorDepthBits >= 8 && colorDepthBits <= 32); break; case PersistentCanvasWindow::FullSceneAntialias: setFlag(State::FSAA, attribs[i]); break; case PersistentCanvasWindow::VerticalSync: setFlag(State::VSync, attribs[i]); break; default: // Unknown attribute. DENG2_ASSERT(false); } } } /** * Checks all command line options that affect window geometry and * applies them to this logical state. */ void modifyAccordingToOptions() { CommandLine const &cmdLine = App::commandLine(); // We will compose a set of attributes based on the options. QVector attribs; if(cmdLine.has("-nofullscreen") || cmdLine.has("-window")) { attribs << PersistentCanvasWindow::Fullscreen << false; } if(cmdLine.has("-fullscreen") || cmdLine.has("-nowindow")) { attribs << PersistentCanvasWindow::Fullscreen << true; } if(int arg = cmdLine.check("-width", 1)) { attribs << PersistentCanvasWindow::FullscreenWidth << cmdLine.at(arg + 1).toInt(); } if(int arg = cmdLine.check("-height", 1)) { attribs << PersistentCanvasWindow::FullscreenHeight << cmdLine.at(arg + 1).toInt(); } if(int arg = cmdLine.check("-winwidth", 1)) { attribs << PersistentCanvasWindow::Width << cmdLine.at(arg + 1).toInt(); } if(int arg = cmdLine.check("-winheight", 1)) { attribs << PersistentCanvasWindow::Height << cmdLine.at(arg + 1).toInt(); } if(int arg = cmdLine.check("-winsize", 2)) { attribs << PersistentCanvasWindow::Width << cmdLine.at(arg + 1).toInt() << PersistentCanvasWindow::Height << cmdLine.at(arg + 2).toInt(); } if(int arg = cmdLine.check("-colordepth", 1)) { attribs << PersistentCanvasWindow::ColorDepthBits << de::clamp(8, cmdLine.at(arg+1).toInt(), 32); } if(int arg = cmdLine.check("-bpp", 1)) { attribs << PersistentCanvasWindow::ColorDepthBits << de::clamp(8, cmdLine.at(arg+1).toInt(), 32); } if(int arg = cmdLine.check("-xpos", 1)) { attribs << PersistentCanvasWindow::Left << cmdLine.at(arg+ 1 ).toInt() << PersistentCanvasWindow::Centered << false << PersistentCanvasWindow::Maximized << false; } if(int arg = cmdLine.check("-ypos", 1)) { attribs << PersistentCanvasWindow:: Top << cmdLine.at(arg + 1).toInt() << PersistentCanvasWindow::Centered << false << PersistentCanvasWindow::Maximized << false; } if(cmdLine.check("-center")) { attribs << PersistentCanvasWindow::Centered << true; } if(cmdLine.check("-nocenter")) { attribs << PersistentCanvasWindow::Centered << false; } if(cmdLine.check("-maximize")) { attribs << PersistentCanvasWindow::Maximized << true; } if(cmdLine.check("-nomaximize")) { attribs << PersistentCanvasWindow::Maximized << false; } if(cmdLine.check("-nofsaa")) { attribs << PersistentCanvasWindow::FullSceneAntialias << false; } if(cmdLine.check("-fsaa")) { attribs << PersistentCanvasWindow::FullSceneAntialias << true; } if(cmdLine.check("-novsync")) { attribs << PersistentCanvasWindow::VerticalSync << false; } if(cmdLine.check("-vsync")) { attribs << PersistentCanvasWindow::VerticalSync << true; } attribs << PersistentCanvasWindow::End; applyAttributes(attribs.constData()); } }; struct Task { enum Type { ShowNormal, ShowFullscreen, ShowMaximized, SetGeometry, NotifyModeChange, TrapMouse, MacRaiseOverShield }; Type type; Rectanglei rect; TimeDelta delay; ///< How long to wait before doing this. Task(Type t, TimeDelta defer = 0) : type(t), delay(defer) {} Task(Rectanglei const &r, TimeDelta defer = 0) : type(SetGeometry), rect(r), delay(defer) {} }; String id; // Logical state. State state; State savedState; // used by saveState(), restoreState() bool neverShown; typedef QList Tasks; Tasks queue; Instance(Public *i, String const &windowId) : Base(i) , id(windowId) , state(windowId) , savedState(windowId) , neverShown(true) { // Keep a global pointer to the main window. if(id == MAIN_WINDOW_ID) { DENG2_ASSERT(!mainExists()); setMain(thisPublic); } self.setMinimumSize(MIN_WIDTH, MIN_HEIGHT); } ~Instance() { self.saveToConfig(); } /** * Parse the attributes array and check the values. * @param attribs Array of attributes, terminated by Attribute::End. */ bool validateAttributes(int const *attribs) { DENG2_ASSERT(attribs); for(int i = 0; attribs[i]; ++i) { switch(attribs[i++]) { case Width: case FullscreenWidth: if(attribs[i] < MIN_WIDTH) return false; break; case Height: case FullscreenHeight: if(attribs[i] < MIN_HEIGHT) return false; break; case Fullscreen: // Can't go to fullscreen when downloading. //if(attribs[i] && Updater_IsDownloadInProgress()) // return false; break; case ColorDepthBits: if(attribs[i] < 8 || attribs[i] > 32) return false; // Illegal value. break; case Centered: case Maximized: break; default: // Unknown attribute. LOGDEV_GL_WARNING("Unknown attribute %i, aborting...") << attribs[i]; return false; } } // Seems ok. return true; } /** * Parse attributes and apply the values to the widget. * * @param attribs Zero-terminated array of attributes and values. */ void applyAttributes(int const *attribs) { LOG_AS("applyAttributes"); DENG2_ASSERT(attribs); // Update the cached state from the authoritative source: // the widget itself. state = widgetState(); // The new modified state. State mod = state; mod.applyAttributes(attribs); LOGDEV_GL_MSG("windowRect:%s fullSize:%s depth:%i flags:%x") << mod.windowRect.asText() << mod.fullSize.asText() << mod.colorDepthBits << mod.flags; // Apply them. if(mod != state) { applyToWidget(mod); } else { LOGDEV_GL_VERBOSE("New window attributes are the same as before"); } } /** * Apply a logical state to the concrete widget instance. All properties of * the widget may not be updated instantly during this method. Particularly * a display mode change will cause geometry changes to occur later. * * @param newState State to apply. */ void applyToWidget(State const &newState) { bool trapped = self.canvas().isMouseTrapped(); // If the display mode needs to change, we will have to defer the rest // of the state changes so that everything catches up after the change. TimeDelta defer = 0; DisplayMode const *newMode = newState.displayMode(); bool modeChanged = false; if(!self.isVisible()) { // Update geometry for windowed mode right away. queue << Task(newState.windowRect); } // Change display mode, if necessary. if(!DisplayMode_IsEqual(DisplayMode_Current(), newMode)) { LOG_GL_NOTE("Changing display mode to %i x %i x %i (%.1f Hz)") << newMode->width << newMode->height << newMode->depth << newMode->refreshRate; modeChanged = DisplayMode_Change(newMode, newState.shouldCaptureScreen()); state.colorDepthBits = newMode->depth; // Wait a while after the mode change to let changes settle in. #ifdef MACOSX defer = .1; #else defer = .01; #endif } if(self.isVisible()) { // Possible actions: // // Window -> Window: Geometry // Window -> Max: ShowMax // Window -> Full: ShowFull // Window -> Mode+Full: Mode, ShowFull // Max -> Window: ShowNormal, Geometry // Max -> Max: - // Max -> Full: ShowFull // Max -> Mode+Full: Mode, ShowFull // Full -> Window: ShowNormal, Geometry // Full -> Max: ShowMax // Full -> Full: - // Full -> Mode+Full: Mode, SnowNormal, ShowFull if(newState.isWindow()) { queue << Task(Task::ShowNormal, defer) << Task(newState.windowRect); } else { if(modeChanged) { queue << Task(Task::ShowNormal, defer); defer = 0.01; } if(newState.isMaximized()) { queue << Task(Task::ShowMaximized, defer); state.windowRect = newState.windowRect; } else if(newState.isFullscreen()) { queue << Task(Task::ShowFullscreen, defer); state.windowRect = newState.windowRect; } } defer = 0; } if(modeChanged) { #ifdef MACOSX if(newState.isFullscreen()) { queue << Task(Task::MacRaiseOverShield); } #endif queue << Task(Task::NotifyModeChange, .1); } if(trapped) { queue << Task(Task::TrapMouse); } state.fullSize = newState.fullSize; state.flags = newState.flags; if(self.isVisible()) { // Carry out queued operations after dropping back to the event loop. QTimer::singleShot(10, thisPublic, SLOT(performQueuedTasks())); } else { // Not visible yet so we can do anything we want. checkQueue(); } } void checkQueue() { while(!queue.isEmpty()) { Task &next = queue[0]; if(next.delay > 0.0) { QTimer::singleShot(next.delay.asMilliSeconds(), thisPublic, SLOT(performQueuedTasks())); next.delay = 0; break; } else { // Do it now. switch(next.type) { case Task::ShowNormal: LOG_GL_VERBOSE("Showing window as normal"); self.showNormal(); break; case Task::ShowMaximized: LOG_GL_VERBOSE("Showing window as maximized"); self.showMaximized(); break; case Task::ShowFullscreen: LOG_GL_VERBOSE("Showing window as fullscreen"); self.showFullScreen(); break; case Task::SetGeometry: if(state.isCentered()) { LOG_GL_VERBOSE("Centering window with size ") << next.rect.size().asText(); next.rect = centeredRect(next.rect.size()); } LOG_GL_VERBOSE("Setting window geometry to ") << next.rect.asText(); self.setGeometry(next.rect.left(), next.rect.top(), next.rect.width(), next.rect.height()); state.windowRect = next.rect; break; case Task::NotifyModeChange: LOGDEV_GL_VERBOSE("Display mode change notification"); notifyAboutModeChange(); break; case Task::MacRaiseOverShield: #ifdef MACOSX // Pull the window again over the shield after the mode change. LOGDEV_GL_VERBOSE("Raising window over shield"); DisplayMode_Native_Raise(self.nativeHandle()); #endif break; case Task::TrapMouse: self.canvas().trapMouse(); break; } queue.takeFirst(); } } // The queue is now empty; all modifications to state have been applied. DENG2_FOR_PUBLIC_AUDIENCE2(AttributeChange, i) { i->windowAttributesChanged(self); } } /** * Gets the current state of the Qt widget. */ State widgetState() const { State st(id); st.windowRect = self.windowRect(); st.fullSize = state.fullSize; st.colorDepthBits = DisplayMode_Current()->depth; st.flags = (self.isMaximized()? State::Maximized : State::None) | (self.isFullScreen()? State::Fullscreen : State::None) | (state.isCentered()? State::Centered : State::None); return st; } DENG2_PIMPL_AUDIENCE(AttributeChange) }; DENG2_AUDIENCE_METHOD(PersistentCanvasWindow, AttributeChange) PersistentCanvasWindow::PersistentCanvasWindow(String const &id) : d(new Instance(this, id)) { try { restoreFromConfig(); } catch(Error const &er) { LOG_WARNING("Failed to restore window state:\n%s") << er.asText(); } } String PersistentCanvasWindow::id() const { return d->id; } void PersistentCanvasWindow::saveToConfig() { try { d->widgetState().saveToConfig(); } catch(Error const &er) { LOG_WARNING("Failed to save window state:\n%s") << er.asText(); } } void PersistentCanvasWindow::restoreFromConfig() { // Restore the window's state. d->state.restoreFromConfig(); d->state.modifyAccordingToOptions(); d->applyToWidget(d->state); } void PersistentCanvasWindow::saveState() { d->savedState = d->widgetState(); } void PersistentCanvasWindow::restoreState() { d->applyToWidget(d->savedState); } bool PersistentCanvasWindow::isCentered() const { return d->state.isCentered(); } Rectanglei PersistentCanvasWindow::windowRect() const { if(d->neverShown) { // If the window hasn't been shown yet, it doesn't have a valid // normal geometry. Use the one defined in the logical state. return d->state.windowRect; } QRect geom = normalGeometry(); return Rectanglei(geom.left(), geom.top(), geom.width(), geom.height()); } CanvasWindow::Size PersistentCanvasWindow::fullscreenSize() const { return d->state.fullSize; } int PersistentCanvasWindow::colorDepthBits() const { return d->state.colorDepthBits; } void PersistentCanvasWindow::show(bool yes) { if(yes) { if(d->state.isFullscreen()) { #ifdef WIN32 /* * On Windows, changes to windows appear to be carried out immediately. * Without this delay, sometimes (randomly) the Qt desktop widget would * not have been updated to the correct size after a display mode change. * (Likely due to the behavior of the event loop on Windows; the desktop * widget would or would not get the resize event depending on how the * events play out during engine startup and main window setup.) */ QTimer::singleShot(100, this, SLOT(showFullScreen())); #else showFullScreen(); #endif } else if(d->state.isMaximized()) { showMaximized(); } else { showNormal(); } // Now it has been shown. d->neverShown = false; } else { hide(); } } bool PersistentCanvasWindow::changeAttributes(int const *attribs) { LOG_AS("PersistentCanvasWindow"); if(d->validateAttributes(attribs)) { d->applyAttributes(attribs); return true; } // These weren't good! return false; } void PersistentCanvasWindow::performQueuedTasks() { d->checkQueue(); } String PersistentCanvasWindow::configName(String const &key) const { return d->state.configName(key); } PersistentCanvasWindow &PersistentCanvasWindow::main() { DENG2_ASSERT(mainExists()); if(!mainExists()) { throw InvalidIdError("PersistentCanvasWindow::main", "No window found with id \"" + MAIN_WINDOW_ID + "\""); } return static_cast(CanvasWindow::main()); } void PersistentCanvasWindow::moveEvent(QMoveEvent *) { if(isCentered() && !isMaximized() && !isFullScreen()) { int len = (geometry().topLeft() - centeredQRect(size()).topLeft()).manhattanLength(); if(len > BREAK_CENTERING_THRESHOLD) { d->state.setFlag(Instance::State::Centered, false); // Notify. DENG2_FOR_AUDIENCE2(AttributeChange, i) { i->windowAttributesChanged(*this); } } else { // Recenter. setGeometry(centeredQRect(size())); } } } void PersistentCanvasWindow::resizeEvent(QResizeEvent *ev) { LOGDEV_GL_XVERBOSE("Window resized: maximized:%b old:%ix%i new:%ix%i") << isMaximized() << ev->oldSize().width() << ev->oldSize().height() << ev->size().width() << ev->size().height(); /* if(!isMaximized() && !isFullScreen()) { d->state.windowRect.setSize(Vector2i(ev->size().width(), ev->size().height())); }*/ } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/input/0000775000175000017500000000000012641367671021215 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/src/input/mouseeventsource.cpp0000664000175000017500000000226512641367671025341 0ustar jaakkojaakko/** @file mouseeventsource.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/MouseEventSource" namespace de { DENG2_PIMPL_NOREF(MouseEventSource) { DENG2_PIMPL_AUDIENCE(MouseStateChange) DENG2_PIMPL_AUDIENCE(MouseEvent) }; DENG2_AUDIENCE_METHOD(MouseEventSource, MouseStateChange) DENG2_AUDIENCE_METHOD(MouseEventSource, MouseEvent) MouseEventSource::MouseEventSource() : d(new Instance) {} } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/input/mouseevent.cpp0000664000175000017500000000374512641367671024124 0ustar jaakkojaakko/** @file mouseevent.cpp * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/MouseEvent" namespace de { MouseEvent::MouseEvent() : Event(MouseButton), _wheelMotion(FineAngle), _button(Unknown), _state(Released) {} MouseEvent::MouseEvent(MotionType motion, Vector2i const &pos) : Event(motion == Absolute? MousePosition : motion == Relative? MouseMotion : MouseWheel), _pos(pos), _wheelMotion(FineAngle), _button(Unknown), _state(Released) { if(motion == Wheel) { _pos = Vector2i(); _wheel = pos; } } MouseEvent::MouseEvent(WheelMotion wheelMotion, Vector2i const &wheel, Vector2i const &pos) : Event(MouseWheel), _pos(pos), _wheelMotion(wheelMotion), _wheel(wheel), _button(Unknown), _state(Released) {} MouseEvent::MouseEvent(Button button, ButtonState state, Vector2i const &pos) : Event(MouseButton), _pos(pos), _button(button), _state(state) {} MouseEvent::MotionType MouseEvent::motion() const { return type() == MousePosition? Absolute : type() == MouseMotion? Relative : Wheel; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/input/keyevent.cpp0000664000175000017500000003523312641367671023561 0ustar jaakkojaakko/** @file keyevent.cpp Input event from a keyboard. * @ingroup input * * Depends on Qt GUI. * * @authors Copyright (c) 2012-2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifdef WIN32 # define WIN32_LEAN_AND_MEAN # include #endif #include "de/KeyEvent" #include #include #if defined(UNIX) && !defined(MACOSX) # include # include # include # include "imKStoUCS_x11.c" # define XFREE_KEYMAPPING static int x11ScancodeToDDKey(int scancode); # ifdef KeyPress # undef KeyPress # endif # ifdef KeyRelease # undef KeyRelease # endif #endif #ifdef WIN32 static int win32Keymap[256]; /** * Initialize the Windows virtual key => DDKEY translation map. */ static void checkWin32Keymap() { if(win32Keymap[VK_BACK] == DDKEY_BACKSPACE) { // Already go it. return; } int* keymap = win32Keymap; keymap[VK_BACK] = DDKEY_BACKSPACE; // Backspace keymap[VK_TAB ] = DDKEY_TAB; //keymap[VK_CLEAR] = ; keymap[VK_RETURN] = DDKEY_RETURN; keymap[VK_SHIFT] = DDKEY_RSHIFT; keymap[VK_CONTROL] = DDKEY_RCTRL; keymap[VK_MENU] = DDKEY_RALT; keymap[VK_PAUSE] = DDKEY_PAUSE; keymap[VK_CAPITAL] = DDKEY_CAPSLOCK; //keymap[VK_KANA] = ; //keymap[VK_HANGEUL] = ; //keymap[VK_HANGUL] = ; //keymap[VK_JUNJA] = ; //keymap[VK_FINAL] = ; //keymap[VK_HANJA] = ; //keymap[VK_KANJI] = ; keymap[VK_ESCAPE] = DDKEY_ESCAPE; //keymap[VK_CONVERT] = ; //keymap[VK_NONCONVERT] = ; //keymap[VK_ACCEPT] = ; //keymap[VK_MODECHANGE] = ; keymap[VK_SPACE] = ' '; keymap[VK_OEM_PLUS] = '='; //+'; keymap[VK_OEM_COMMA] = ','; keymap[VK_OEM_MINUS] = '-'; keymap[VK_OEM_PERIOD] = '.'; keymap[VK_OEM_1] = ';'; keymap[VK_OEM_2] = '/'; keymap[VK_OEM_3] = '\''; keymap[VK_OEM_4] = '['; keymap[VK_OEM_5] = DDKEY_BACKSLASH; keymap[VK_OEM_6] = ']'; keymap[VK_OEM_7] = '#'; keymap[VK_OEM_8] = '`'; keymap[VK_OEM_102] = '`'; keymap[VK_PRIOR] = DDKEY_PGUP; keymap[VK_NEXT] = DDKEY_PGDN; keymap[VK_END] = DDKEY_END; keymap[VK_HOME] = DDKEY_HOME; keymap[VK_LEFT] = DDKEY_LEFTARROW; keymap[VK_UP] = DDKEY_UPARROW; keymap[VK_RIGHT] = DDKEY_RIGHTARROW; keymap[VK_DOWN] = DDKEY_DOWNARROW; //keymap[VK_SELECT] = ; //keymap[VK_PRINT] = ; //keymap[VK_EXECUTE] = ; //keymap[VK_SNAPSHOT] = ; keymap[VK_INSERT] = DDKEY_INS; keymap[VK_DELETE] = DDKEY_DEL; //keymap[VK_HELP] = ; //keymap[VK_LWIN] = ; //keymap[VK_RWIN] = ; //keymap[VK_APPS] = ; //keymap[VK_SLEEP] = ; keymap[VK_NUMPAD0] = DDKEY_NUMPAD0; keymap[VK_NUMPAD1] = DDKEY_NUMPAD1; keymap[VK_NUMPAD2] = DDKEY_NUMPAD2; keymap[VK_NUMPAD3] = DDKEY_NUMPAD3; keymap[VK_NUMPAD4] = DDKEY_NUMPAD4; keymap[VK_NUMPAD5] = DDKEY_NUMPAD5; keymap[VK_NUMPAD6] = DDKEY_NUMPAD6; keymap[VK_NUMPAD7] = DDKEY_NUMPAD7; keymap[VK_NUMPAD8] = DDKEY_NUMPAD8; keymap[VK_NUMPAD9] = DDKEY_NUMPAD9; keymap[VK_MULTIPLY] = DDKEY_MULTIPLY; keymap[VK_ADD] = DDKEY_ADD; //keymap[VK_SEPARATOR] = ; keymap[VK_SUBTRACT] = DDKEY_SUBTRACT; keymap[VK_DECIMAL] = DDKEY_DECIMAL; keymap[VK_DIVIDE] = DDKEY_DIVIDE; keymap[VK_F1] = DDKEY_F1; keymap[VK_F2] = DDKEY_F2; keymap[VK_F3] = DDKEY_F3; keymap[VK_F4] = DDKEY_F4; keymap[VK_F5] = DDKEY_F5; keymap[VK_F6] = DDKEY_F6; keymap[VK_F7] = DDKEY_F7; keymap[VK_F8] = DDKEY_F8; keymap[VK_F9] = DDKEY_F9; keymap[VK_F10] = DDKEY_F10; keymap[VK_F11] = DDKEY_F11; keymap[VK_F12] = DDKEY_F12; keymap[VK_SNAPSHOT] = DDKEY_PRINT; keymap[0x30] = '0'; keymap[0x31] = '1'; keymap[0x32] = '2'; keymap[0x33] = '3'; keymap[0x34] = '4'; keymap[0x35] = '5'; keymap[0x36] = '6'; keymap[0x37] = '7'; keymap[0x38] = '8'; keymap[0x39] = '9'; keymap[0x41] = 'a'; keymap[0x42] = 'b'; keymap[0x43] = 'c'; keymap[0x44] = 'd'; keymap[0x45] = 'e'; keymap[0x46] = 'f'; keymap[0x47] = 'g'; keymap[0x48] = 'h'; keymap[0x49] = 'i'; keymap[0x4A] = 'j'; keymap[0x4B] = 'k'; keymap[0x4C] = 'l'; keymap[0x4D] = 'm'; keymap[0x4E] = 'n'; keymap[0x4F] = 'o'; keymap[0x50] = 'p'; keymap[0x51] = 'q'; keymap[0x52] = 'r'; keymap[0x53] = 's'; keymap[0x54] = 't'; keymap[0x55] = 'u'; keymap[0x56] = 'v'; keymap[0x57] = 'w'; keymap[0x58] = 'x'; keymap[0x59] = 'y'; keymap[0x5A] = 'z'; } #endif // WIN32 int de::KeyEvent::ddKeyFromQt(int qtKey, int nativeVirtualKey, int nativeScanCode) { #ifdef MACOSX switch(qtKey) { case Qt::Key_Meta: return DDKEY_RCTRL; case Qt::Key_Control: return 0; // Don't map the Command key. case Qt::Key_F14: return DDKEY_PAUSE; // No pause key on the Mac. case Qt::Key_F15: return DDKEY_PRINT; default: break; } #endif #ifdef XFREE_KEYMAPPING // We'll check before the generic Qt keys to detect the numpad. int mapped = x11ScancodeToDDKey(nativeScanCode); if(mapped) return mapped; #else DENG2_UNUSED(nativeScanCode); #endif // Non-character-inserting keys. switch(qtKey) { case Qt::Key_Escape: return DDKEY_ESCAPE; case Qt::Key_Tab: return DDKEY_TAB; case Qt::Key_Backtab: return DDKEY_TAB; // Shift detected separately case Qt::Key_Backspace: return DDKEY_BACKSPACE; case Qt::Key_Space: return ' '; case Qt::Key_Pause: return DDKEY_PAUSE; case Qt::Key_Up: return DDKEY_UPARROW; case Qt::Key_Down: return DDKEY_DOWNARROW; case Qt::Key_Left: return DDKEY_LEFTARROW; case Qt::Key_Right: return DDKEY_RIGHTARROW; case Qt::Key_Control: return DDKEY_RCTRL; case Qt::Key_Shift: return DDKEY_RSHIFT; case Qt::Key_Alt: return DDKEY_RALT; case Qt::Key_AltGr: return DDKEY_LALT; case Qt::Key_Menu: return DDKEY_WINMENU; case Qt::Key_Return: return DDKEY_RETURN; case Qt::Key_F1: return DDKEY_F1; case Qt::Key_F2: return DDKEY_F2; case Qt::Key_F3: return DDKEY_F3; case Qt::Key_F4: return DDKEY_F4; case Qt::Key_F5: return DDKEY_F5; case Qt::Key_F6: return DDKEY_F6; case Qt::Key_F7: return DDKEY_F7; case Qt::Key_F8: return DDKEY_F8; case Qt::Key_F9: return DDKEY_F9; case Qt::Key_F10: return DDKEY_F10; case Qt::Key_F11: return DDKEY_F11; case Qt::Key_F12: return DDKEY_F12; case Qt::Key_NumLock: return DDKEY_NUMLOCK; case Qt::Key_ScrollLock: return DDKEY_SCROLL; case Qt::Key_Enter: return DDKEY_ENTER; case Qt::Key_Insert: return DDKEY_INS; case Qt::Key_Delete: return DDKEY_DEL; case Qt::Key_Home: return DDKEY_HOME; case Qt::Key_End: return DDKEY_END; case Qt::Key_PageUp: return DDKEY_PGUP; case Qt::Key_PageDown: return DDKEY_PGDN; case Qt::Key_SysReq: return DDKEY_PRINT; case Qt::Key_Print: return DDKEY_PRINT; case Qt::Key_CapsLock: return DDKEY_CAPSLOCK; default: break; } /// We'll have to use the native virtual keys to make a distinction, e.g., /// between the number row and the keypad. These are the real physical keys /// -- the insertion text is provided outside this mapping. #ifdef WIN32 /// @todo Would the native scancodes be more appropriate than virtual keys? /// (no influence from language settings) checkWin32Keymap(); if(win32Keymap[nativeVirtualKey] > 0) { // We know a mapping for this. return win32Keymap[nativeVirtualKey]; } #endif #ifdef MACOSX switch(nativeVirtualKey) { case 0x00: return 'a'; case 0x01: return 's'; case 0x02: return 'd'; case 0x03: return 'f'; case 0x04: return 'h'; case 0x05: return 'g'; case 0x06: return 'z'; case 0x07: return 'x'; case 0x08: return 'c'; case 0x09: return 'v'; case 0x0A: return DDKEY_SECTION; case 0x0B: return 'b'; case 0x0C: return 'q'; case 0x0D: return 'w'; case 0x0E: return 'e'; case 0x0F: return 'r'; case 0x10: return 'y'; case 0x11: return 't'; case 0x12: return '1'; case 0x13: return '2'; case 0x14: return '3'; case 0x15: return '4'; case 0x16: return '6'; case 0x17: return '5'; case 0x18: return '='; case 0x19: return '9'; case 0x1A: return '7'; case 0x1B: return '-'; case 0x1C: return '8'; case 0x1D: return '0'; case 0x1E: return ']'; case 0x1F: return 'o'; case 0x20: return 'u'; case 0x21: return '['; case 0x22: return 'i'; case 0x23: return 'p'; case 0x25: return 'l'; case 0x26: return 'j'; case 0x27: return '\''; case 0x28: return 'k'; case 0x29: return ';'; case 0x2A: return '\\'; case 0x2B: return ','; case 0x2C: return '/'; case 0x2D: return 'n'; case 0x2E: return 'm'; case 0x2F: return '.'; case 0x32: return '`'; case 82: return DDKEY_NUMPAD0; case 83: return DDKEY_NUMPAD1; case 84: return DDKEY_NUMPAD2; case 85: return DDKEY_NUMPAD3; case 86: return DDKEY_NUMPAD4; case 87: return DDKEY_NUMPAD5; case 88: return DDKEY_NUMPAD6; case 89: return DDKEY_NUMPAD7; case 91: return DDKEY_NUMPAD8; case 92: return DDKEY_NUMPAD9; case 65: return DDKEY_DECIMAL; case 69: return DDKEY_ADD; case 78: return DDKEY_SUBTRACT; case 75: return DDKEY_DIVIDE; case 0x43: return DDKEY_MULTIPLY; default: break; } #endif // Not supported! LOGDEV_INPUT_WARNING("Ignored unknown key: Qt key %i (%x), virtualKey %i, scancode %i") << qtKey << qtKey << nativeVirtualKey << nativeScanCode; return 0; } #ifdef XFREE_KEYMAPPING static int x11ScancodeToDDKey(int scancode) { int symCount; KeySym *syms = XGetKeyboardMapping(QX11Info::display(), scancode, 1, &symCount); if(!symCount) { XFree(syms); return 0; } KeySym sym = syms[0]; XFree(syms); syms = nullptr; if(sym == NoSymbol) return 0; unsigned int ucs4 = X11_KeySymToUcs4(sym); if(ucs4) { // ASCII range. if(ucs4 > ' ' && ucs4 < 128) return ucs4; //qDebug() << "ucs4:" << ucs4 << hex << ucs4 << dec; return 0; } //qDebug() << "sym:" << hex << sym << dec; switch(sym) { case XK_KP_Insert: return DDKEY_NUMPAD0; case XK_KP_End: return DDKEY_NUMPAD1; case XK_KP_Down: return DDKEY_NUMPAD2; case XK_KP_Page_Down: return DDKEY_NUMPAD3; case XK_KP_Left: return DDKEY_NUMPAD4; case XK_KP_Begin: return DDKEY_NUMPAD5; case XK_KP_Right: return DDKEY_NUMPAD6; case XK_KP_Home: return DDKEY_NUMPAD7; case XK_KP_Up: return DDKEY_NUMPAD8; case XK_KP_Page_Up: return DDKEY_NUMPAD9; case XK_KP_0: return DDKEY_NUMPAD0; case XK_KP_1: return DDKEY_NUMPAD1; case XK_KP_2: return DDKEY_NUMPAD2; case XK_KP_3: return DDKEY_NUMPAD3; case XK_KP_4: return DDKEY_NUMPAD4; case XK_KP_5: return DDKEY_NUMPAD5; case XK_KP_6: return DDKEY_NUMPAD6; case XK_KP_7: return DDKEY_NUMPAD7; case XK_KP_8: return DDKEY_NUMPAD8; case XK_KP_9: return DDKEY_NUMPAD9; case XK_KP_Equal: return '='; case XK_KP_Multiply: return DDKEY_MULTIPLY; case XK_KP_Add: return DDKEY_ADD; case XK_KP_Separator: return DDKEY_DECIMAL; case XK_KP_Delete: return DDKEY_DECIMAL; case XK_KP_Subtract: return DDKEY_SUBTRACT; case XK_KP_Decimal: return DDKEY_DECIMAL; case XK_KP_Divide: return DDKEY_DIVIDE; default: break; } //qDebug() << "=>failed to map"; return 0; } #endif namespace de { KeyEvent::KeyEvent() : Event(KeyPress), _qtKey(0), _ddKey(0), _nativeCode(0) {} KeyEvent::KeyEvent(State keyState, int qtKeyCode, int ddKeyCode, int nativeKeyCode, String const &keyText, Modifiers const &modifiers) : Event(keyState == Pressed? KeyPress : keyState == Repeat? KeyRepeat : KeyRelease), _qtKey(qtKeyCode), _mods(modifiers), _ddKey(ddKeyCode), _nativeCode(nativeKeyCode), _text(keyText) {} KeyEvent::State KeyEvent::state() const { switch(type()) { case KeyPress: return Pressed; case KeyRepeat: return Repeat; default: return Released; } } bool KeyEvent::isModifier() const { return _qtKey == Qt::Key_Shift || _qtKey == Qt::Key_Control || _qtKey == Qt::Key_Alt || _qtKey == Qt::Key_Meta; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/input/imKStoUCS_x11.c0000664000175000017500000005474512641367671023652 0ustar jaakkojaakko/* Copyright (C) 1994-2003 The XFree86 Project, Inc. All Rights Reserved. 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 fur- nished 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, FIT- NESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE XFREE86 PROJECT BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CON- NECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Except as contained in this notice, the name of the XFree86 Project shall not be used in advertising or otherwise to promote the sale, use or other deal- ings in this Software without prior written authorization from the XFree86 Project. */ /* $XFree86: xc/lib/X11/imKStoUCS.c,v 1.4 2003/04/29 11:29:18 pascal Exp $ */ #include static unsigned short const keysym_to_unicode_1a1_1ff[] = { 0x0104, 0x02d8, 0x0141, 0x0000, 0x013d, 0x015a, 0x0000, /* 0x01a0-0x01a7 */ 0x0000, 0x0160, 0x015e, 0x0164, 0x0179, 0x0000, 0x017d, 0x017b, /* 0x01a8-0x01af */ 0x0000, 0x0105, 0x02db, 0x0142, 0x0000, 0x013e, 0x015b, 0x02c7, /* 0x01b0-0x01b7 */ 0x0000, 0x0161, 0x015f, 0x0165, 0x017a, 0x02dd, 0x017e, 0x017c, /* 0x01b8-0x01bf */ 0x0154, 0x0000, 0x0000, 0x0102, 0x0000, 0x0139, 0x0106, 0x0000, /* 0x01c0-0x01c7 */ 0x010c, 0x0000, 0x0118, 0x0000, 0x011a, 0x0000, 0x0000, 0x010e, /* 0x01c8-0x01cf */ 0x0110, 0x0143, 0x0147, 0x0000, 0x0000, 0x0150, 0x0000, 0x0000, /* 0x01d0-0x01d7 */ 0x0158, 0x016e, 0x0000, 0x0170, 0x0000, 0x0000, 0x0162, 0x0000, /* 0x01d8-0x01df */ 0x0155, 0x0000, 0x0000, 0x0103, 0x0000, 0x013a, 0x0107, 0x0000, /* 0x01e0-0x01e7 */ 0x010d, 0x0000, 0x0119, 0x0000, 0x011b, 0x0000, 0x0000, 0x010f, /* 0x01e8-0x01ef */ 0x0111, 0x0144, 0x0148, 0x0000, 0x0000, 0x0151, 0x0000, 0x0000, /* 0x01f0-0x01f7 */ 0x0159, 0x016f, 0x0000, 0x0171, 0x0000, 0x0000, 0x0163, 0x02d9 /* 0x01f8-0x01ff */ }; static unsigned short const keysym_to_unicode_2a1_2fe[] = { 0x0126, 0x0000, 0x0000, 0x0000, 0x0000, 0x0124, 0x0000, /* 0x02a0-0x02a7 */ 0x0000, 0x0130, 0x0000, 0x011e, 0x0134, 0x0000, 0x0000, 0x0000, /* 0x02a8-0x02af */ 0x0000, 0x0127, 0x0000, 0x0000, 0x0000, 0x0000, 0x0125, 0x0000, /* 0x02b0-0x02b7 */ 0x0000, 0x0131, 0x0000, 0x011f, 0x0135, 0x0000, 0x0000, 0x0000, /* 0x02b8-0x02bf */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x010a, 0x0108, 0x0000, /* 0x02c0-0x02c7 */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x02c8-0x02cf */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0120, 0x0000, 0x0000, /* 0x02d0-0x02d7 */ 0x011c, 0x0000, 0x0000, 0x0000, 0x0000, 0x016c, 0x015c, 0x0000, /* 0x02d8-0x02df */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x010b, 0x0109, 0x0000, /* 0x02e0-0x02e7 */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x02e8-0x02ef */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0121, 0x0000, 0x0000, /* 0x02f0-0x02f7 */ 0x011d, 0x0000, 0x0000, 0x0000, 0x0000, 0x016d, 0x015d /* 0x02f8-0x02ff */ }; static unsigned short const keysym_to_unicode_3a2_3fe[] = { 0x0138, 0x0156, 0x0000, 0x0128, 0x013b, 0x0000, /* 0x03a0-0x03a7 */ 0x0000, 0x0000, 0x0112, 0x0122, 0x0166, 0x0000, 0x0000, 0x0000, /* 0x03a8-0x03af */ 0x0000, 0x0000, 0x0000, 0x0157, 0x0000, 0x0129, 0x013c, 0x0000, /* 0x03b0-0x03b7 */ 0x0000, 0x0000, 0x0113, 0x0123, 0x0167, 0x014a, 0x0000, 0x014b, /* 0x03b8-0x03bf */ 0x0100, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x012e, /* 0x03c0-0x03c7 */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0116, 0x0000, 0x0000, 0x012a, /* 0x03c8-0x03cf */ 0x0000, 0x0145, 0x014c, 0x0136, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x03d0-0x03d7 */ 0x0000, 0x0172, 0x0000, 0x0000, 0x0000, 0x0168, 0x016a, 0x0000, /* 0x03d8-0x03df */ 0x0101, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x012f, /* 0x03e0-0x03e7 */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0117, 0x0000, 0x0000, 0x012b, /* 0x03e8-0x03ef */ 0x0000, 0x0146, 0x014d, 0x0137, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x03f0-0x03f7 */ 0x0000, 0x0173, 0x0000, 0x0000, 0x0000, 0x0169, 0x016b /* 0x03f8-0x03ff */ }; static unsigned short const keysym_to_unicode_4a1_4df[] = { 0x3002, 0x3008, 0x3009, 0x3001, 0x30fb, 0x30f2, 0x30a1, /* 0x04a0-0x04a7 */ 0x30a3, 0x30a5, 0x30a7, 0x30a9, 0x30e3, 0x30e5, 0x30e7, 0x30c3, /* 0x04a8-0x04af */ 0x30fc, 0x30a2, 0x30a4, 0x30a6, 0x30a8, 0x30aa, 0x30ab, 0x30ad, /* 0x04b0-0x04b7 */ 0x30af, 0x30b1, 0x30b3, 0x30b5, 0x30b7, 0x30b9, 0x30bb, 0x30bd, /* 0x04b8-0x04bf */ 0x30bf, 0x30c1, 0x30c4, 0x30c6, 0x30c8, 0x30ca, 0x30cb, 0x30cc, /* 0x04c0-0x04c7 */ 0x30cd, 0x30ce, 0x30cf, 0x30d2, 0x30d5, 0x30d8, 0x30db, 0x30de, /* 0x04c8-0x04cf */ 0x30df, 0x30e0, 0x30e1, 0x30e2, 0x30e4, 0x30e6, 0x30e8, 0x30e9, /* 0x04d0-0x04d7 */ 0x30ea, 0x30eb, 0x30ec, 0x30ed, 0x30ef, 0x30f3, 0x309b, 0x309c /* 0x04d8-0x04df */ }; static unsigned short const keysym_to_unicode_590_5fe[] = { 0x06f0, 0x06f1, 0x06f2, 0x06f3, 0x06f4, 0x06f5, 0x06f6, 0x06f7, /* 0x0590-0x0597 */ 0x06f8, 0x06f9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x0598-0x059f */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x066a, 0x0670, 0x0679, /* 0x05a0-0x05a7 */ 0x067e, 0x0686, 0x0688, 0x0691, 0x060c, 0x0000, 0x06d4, 0x0000, /* 0x05ac-0x05af */ 0x0660, 0x0661, 0x0662, 0x0663, 0x0664, 0x0665, 0x0666, 0x0667, /* 0x05b0-0x05b7 */ 0x0668, 0x0669, 0x0000, 0x061b, 0x0000, 0x0000, 0x0000, 0x061f, /* 0x05b8-0x05bf */ 0x0000, 0x0621, 0x0622, 0x0623, 0x0624, 0x0625, 0x0626, 0x0627, /* 0x05c0-0x05c7 */ 0x0628, 0x0629, 0x062a, 0x062b, 0x062c, 0x062d, 0x062e, 0x062f, /* 0x05c8-0x05cf */ 0x0630, 0x0631, 0x0632, 0x0633, 0x0634, 0x0635, 0x0636, 0x0637, /* 0x05d0-0x05d7 */ 0x0638, 0x0639, 0x063a, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x05d8-0x05df */ 0x0640, 0x0641, 0x0642, 0x0643, 0x0644, 0x0645, 0x0646, 0x0647, /* 0x05e0-0x05e7 */ 0x0648, 0x0649, 0x064a, 0x064b, 0x064c, 0x064d, 0x064e, 0x064f, /* 0x05e8-0x05ef */ 0x0650, 0x0651, 0x0652, 0x0653, 0x0654, 0x0655, 0x0698, 0x06a4, /* 0x05f0-0x05f7 */ 0x06a9, 0x06af, 0x06ba, 0x06be, 0x06cc, 0x06d2, 0x06c1 /* 0x05f8-0x05fe */ }; static unsigned short const keysym_to_unicode_680_6ff[] = { 0x0492, 0x0496, 0x049a, 0x049c, 0x04a2, 0x04ae, 0x04b0, 0x04b2, /* 0x0680-0x0687 */ 0x04b6, 0x04b8, 0x04ba, 0x0000, 0x04d8, 0x04e2, 0x04e8, 0x04ee, /* 0x0688-0x068f */ 0x0493, 0x0497, 0x049b, 0x049d, 0x04a3, 0x04af, 0x04b1, 0x04b3, /* 0x0690-0x0697 */ 0x04b7, 0x04b9, 0x04bb, 0x0000, 0x04d9, 0x04e3, 0x04e9, 0x04ef, /* 0x0698-0x069f */ 0x0000, 0x0452, 0x0453, 0x0451, 0x0454, 0x0455, 0x0456, 0x0457, /* 0x06a0-0x06a7 */ 0x0458, 0x0459, 0x045a, 0x045b, 0x045c, 0x0491, 0x045e, 0x045f, /* 0x06a8-0x06af */ 0x2116, 0x0402, 0x0403, 0x0401, 0x0404, 0x0405, 0x0406, 0x0407, /* 0x06b0-0x06b7 */ 0x0408, 0x0409, 0x040a, 0x040b, 0x040c, 0x0490, 0x040e, 0x040f, /* 0x06b8-0x06bf */ 0x044e, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433, /* 0x06c0-0x06c7 */ 0x0445, 0x0438, 0x0439, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, /* 0x06c8-0x06cf */ 0x043f, 0x044f, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432, /* 0x06d0-0x06d7 */ 0x044c, 0x044b, 0x0437, 0x0448, 0x044d, 0x0449, 0x0447, 0x044a, /* 0x06d8-0x06df */ 0x042e, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413, /* 0x06e0-0x06e7 */ 0x0425, 0x0418, 0x0419, 0x041a, 0x041b, 0x041c, 0x041d, 0x041e, /* 0x06e8-0x06ef */ 0x041f, 0x042f, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412, /* 0x06f0-0x06f7 */ 0x042c, 0x042b, 0x0417, 0x0428, 0x042d, 0x0429, 0x0427, 0x042a /* 0x06f8-0x06ff */ }; static unsigned short const keysym_to_unicode_7a1_7f9[] = { 0x0386, 0x0388, 0x0389, 0x038a, 0x03aa, 0x0000, 0x038c, /* 0x07a0-0x07a7 */ 0x038e, 0x03ab, 0x0000, 0x038f, 0x0000, 0x0000, 0x0385, 0x2015, /* 0x07a8-0x07af */ 0x0000, 0x03ac, 0x03ad, 0x03ae, 0x03af, 0x03ca, 0x0390, 0x03cc, /* 0x07b0-0x07b7 */ 0x03cd, 0x03cb, 0x03b0, 0x03ce, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x07b8-0x07bf */ 0x0000, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, /* 0x07c0-0x07c7 */ 0x0398, 0x0399, 0x039a, 0x039b, 0x039c, 0x039d, 0x039e, 0x039f, /* 0x07c8-0x07cf */ 0x03a0, 0x03a1, 0x03a3, 0x0000, 0x03a4, 0x03a5, 0x03a6, 0x03a7, /* 0x07d0-0x07d7 */ 0x03a8, 0x03a9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x07d8-0x07df */ 0x0000, 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b5, 0x03b6, 0x03b7, /* 0x07e0-0x07e7 */ 0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd, 0x03be, 0x03bf, /* 0x07e8-0x07ef */ 0x03c0, 0x03c1, 0x03c3, 0x03c2, 0x03c4, 0x03c5, 0x03c6, 0x03c7, /* 0x07f0-0x07f7 */ 0x03c8, 0x03c9 /* 0x07f8-0x07ff */ }; static unsigned short const keysym_to_unicode_8a4_8fe[] = { 0x2320, 0x2321, 0x0000, 0x231c, /* 0x08a0-0x08a7 */ 0x231d, 0x231e, 0x231f, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x08a8-0x08af */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x08b0-0x08b7 */ 0x0000, 0x0000, 0x0000, 0x0000, 0x2264, 0x2260, 0x2265, 0x222b, /* 0x08b8-0x08bf */ 0x2234, 0x0000, 0x221e, 0x0000, 0x0000, 0x2207, 0x0000, 0x0000, /* 0x08c0-0x08c7 */ 0x2245, 0x2246, 0x0000, 0x0000, 0x0000, 0x0000, 0x22a2, 0x0000, /* 0x08c8-0x08cf */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x221a, 0x0000, /* 0x08d0-0x08d7 */ 0x0000, 0x0000, 0x2282, 0x2283, 0x2229, 0x222a, 0x2227, 0x2228, /* 0x08d8-0x08df */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x08e0-0x08e7 */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x08e8-0x08ef */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0192, 0x0000, /* 0x08f0-0x08f7 */ 0x0000, 0x0000, 0x0000, 0x2190, 0x2191, 0x2192, 0x2193 /* 0x08f8-0x08ff */ }; static unsigned short const keysym_to_unicode_9df_9f8[] = { 0x2422, /* 0x09d8-0x09df */ 0x2666, 0x25a6, 0x2409, 0x240c, 0x240d, 0x240a, 0x0000, 0x0000, /* 0x09e0-0x09e7 */ 0x240a, 0x240b, 0x2518, 0x2510, 0x250c, 0x2514, 0x253c, 0x2500, /* 0x09e8-0x09ef */ 0x0000, 0x0000, 0x0000, 0x0000, 0x251c, 0x2524, 0x2534, 0x252c, /* 0x09f0-0x09f7 */ 0x2502 /* 0x09f8-0x09ff */ }; static unsigned short const keysym_to_unicode_aa1_afe[] = { 0x2003, 0x2002, 0x2004, 0x2005, 0x2007, 0x2008, 0x2009, /* 0x0aa0-0x0aa7 */ 0x200a, 0x2014, 0x2013, 0x0000, 0x0000, 0x0000, 0x2026, 0x2025, /* 0x0aa8-0x0aaf */ 0x2153, 0x2154, 0x2155, 0x2156, 0x2157, 0x2158, 0x2159, 0x215a, /* 0x0ab0-0x0ab7 */ 0x2105, 0x0000, 0x0000, 0x2012, 0x2039, 0x2024, 0x203a, 0x0000, /* 0x0ab8-0x0abf */ 0x0000, 0x0000, 0x0000, 0x215b, 0x215c, 0x215d, 0x215e, 0x0000, /* 0x0ac0-0x0ac7 */ 0x0000, 0x2122, 0x2120, 0x0000, 0x25c1, 0x25b7, 0x25cb, 0x25ad, /* 0x0ac8-0x0acf */ 0x2018, 0x2019, 0x201c, 0x201d, 0x211e, 0x0000, 0x2032, 0x2033, /* 0x0ad0-0x0ad7 */ 0x0000, 0x271d, 0x0000, 0x220e, 0x25c2, 0x2023, 0x25cf, 0x25ac, /* 0x0ad8-0x0adf */ 0x25e6, 0x25ab, 0x25ae, 0x25b5, 0x25bf, 0x2606, 0x2022, 0x25aa, /* 0x0ae0-0x0ae7 */ 0x25b4, 0x25be, 0x261a, 0x261b, 0x2663, 0x2666, 0x2665, 0x0000, /* 0x0ae8-0x0aef */ 0x2720, 0x2020, 0x2021, 0x2713, 0x2612, 0x266f, 0x266d, 0x2642, /* 0x0af0-0x0af7 */ 0x2640, 0x2121, 0x2315, 0x2117, 0x2038, 0x201a, 0x201e /* 0x0af8-0x0aff */ }; /* none of the APL keysyms match the Unicode characters */ static unsigned short const keysym_to_unicode_cdf_cfa[] = { 0x2017, /* 0x0cd8-0x0cdf */ 0x05d0, 0x05d1, 0x05d2, 0x05d3, 0x05d4, 0x05d5, 0x05d6, 0x05d7, /* 0x0ce0-0x0ce7 */ 0x05d8, 0x05d9, 0x05da, 0x05db, 0x05dc, 0x05dd, 0x05de, 0x05df, /* 0x0ce8-0x0cef */ 0x05e0, 0x05e1, 0x05e2, 0x05e3, 0x05e4, 0x05e5, 0x05e6, 0x05e7, /* 0x0cf0-0x0cf7 */ 0x05e8, 0x05e9, 0x05ea /* 0x0cf8-0x0cff */ }; static unsigned short const keysym_to_unicode_da1_df9[] = { 0x0e01, 0x0e02, 0x0e03, 0x0e04, 0x0e05, 0x0e06, 0x0e07, /* 0x0da0-0x0da7 */ 0x0e08, 0x0e09, 0x0e0a, 0x0e0b, 0x0e0c, 0x0e0d, 0x0e0e, 0x0e0f, /* 0x0da8-0x0daf */ 0x0e10, 0x0e11, 0x0e12, 0x0e13, 0x0e14, 0x0e15, 0x0e16, 0x0e17, /* 0x0db0-0x0db7 */ 0x0e18, 0x0e19, 0x0e1a, 0x0e1b, 0x0e1c, 0x0e1d, 0x0e1e, 0x0e1f, /* 0x0db8-0x0dbf */ 0x0e20, 0x0e21, 0x0e22, 0x0e23, 0x0e24, 0x0e25, 0x0e26, 0x0e27, /* 0x0dc0-0x0dc7 */ 0x0e28, 0x0e29, 0x0e2a, 0x0e2b, 0x0e2c, 0x0e2d, 0x0e2e, 0x0e2f, /* 0x0dc8-0x0dcf */ 0x0e30, 0x0e31, 0x0e32, 0x0e33, 0x0e34, 0x0e35, 0x0e36, 0x0e37, /* 0x0dd0-0x0dd7 */ 0x0e38, 0x0e39, 0x0e3a, 0x0000, 0x0000, 0x0000, 0x0e3e, 0x0e3f, /* 0x0dd8-0x0ddf */ 0x0e40, 0x0e41, 0x0e42, 0x0e43, 0x0e44, 0x0e45, 0x0e46, 0x0e47, /* 0x0de0-0x0de7 */ 0x0e48, 0x0e49, 0x0e4a, 0x0e4b, 0x0e4c, 0x0e4d, 0x0000, 0x0000, /* 0x0de8-0x0def */ 0x0e50, 0x0e51, 0x0e52, 0x0e53, 0x0e54, 0x0e55, 0x0e56, 0x0e57, /* 0x0df0-0x0df7 */ 0x0e58, 0x0e59 /* 0x0df8-0x0dff */ }; static unsigned short const keysym_to_unicode_ea0_eff[] = { 0x0000, 0x1101, 0x1101, 0x11aa, 0x1102, 0x11ac, 0x11ad, 0x1103, /* 0x0ea0-0x0ea7 */ 0x1104, 0x1105, 0x11b0, 0x11b1, 0x11b2, 0x11b3, 0x11b4, 0x11b5, /* 0x0ea8-0x0eaf */ 0x11b6, 0x1106, 0x1107, 0x1108, 0x11b9, 0x1109, 0x110a, 0x110b, /* 0x0eb0-0x0eb7 */ 0x110c, 0x110d, 0x110e, 0x110f, 0x1110, 0x1111, 0x1112, 0x1161, /* 0x0eb8-0x0ebf */ 0x1162, 0x1163, 0x1164, 0x1165, 0x1166, 0x1167, 0x1168, 0x1169, /* 0x0ec0-0x0ec7 */ 0x116a, 0x116b, 0x116c, 0x116d, 0x116e, 0x116f, 0x1170, 0x1171, /* 0x0ec8-0x0ecf */ 0x1172, 0x1173, 0x1174, 0x1175, 0x11a8, 0x11a9, 0x11aa, 0x11ab, /* 0x0ed0-0x0ed7 */ 0x11ac, 0x11ad, 0x11ae, 0x11af, 0x11b0, 0x11b1, 0x11b2, 0x11b3, /* 0x0ed8-0x0edf */ 0x11b4, 0x11b5, 0x11b6, 0x11b7, 0x11b8, 0x11b9, 0x11ba, 0x11bb, /* 0x0ee0-0x0ee7 */ 0x11bc, 0x11bd, 0x11be, 0x11bf, 0x11c0, 0x11c1, 0x11c2, 0x0000, /* 0x0ee8-0x0eef */ 0x0000, 0x0000, 0x1140, 0x0000, 0x0000, 0x1159, 0x119e, 0x0000, /* 0x0ef0-0x0ef7 */ 0x11eb, 0x0000, 0x11f9, 0x0000, 0x0000, 0x0000, 0x0000, 0x20a9, /* 0x0ef8-0x0eff */ }; static unsigned short const keysym_to_unicode_12a1_12fe[] = { 0x1e02, 0x1e03, 0x0000, 0x0000, 0x0000, 0x1e0a, 0x0000, /* 0x12a0-0x12a7 */ 0x1e80, 0x0000, 0x1e82, 0x1e0b, 0x1ef2, 0x0000, 0x0000, 0x0000, /* 0x12a8-0x12af */ 0x1e1e, 0x1e1f, 0x0000, 0x0000, 0x1e40, 0x1e41, 0x0000, 0x1e56, /* 0x12b0-0x12b7 */ 0x1e81, 0x1e57, 0x1e83, 0x1e60, 0x1ef3, 0x1e84, 0x1e85, 0x1e61, /* 0x12b8-0x12bf */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x12c0-0x12c7 */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x12c8-0x12cf */ 0x0174, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x1e6a, /* 0x12d0-0x12d7 */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0176, 0x0000, /* 0x12d8-0x12df */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x12e0-0x12e7 */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x12e8-0x12ef */ 0x0175, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x1e6b, /* 0x12f0-0x12f7 */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0177 /* 0x12f0-0x12ff */ }; static unsigned short const keysym_to_unicode_13bc_13be[] = { 0x0152, 0x0153, 0x0178 /* 0x13b8-0x13bf */ }; static unsigned short const keysym_to_unicode_14a1_14ff[] = { 0x2741, 0x00a7, 0x0589, 0x0029, 0x0028, 0x00bb, 0x00ab, /* 0x14a0-0x14a7 */ 0x2014, 0x002e, 0x055d, 0x002c, 0x2013, 0x058a, 0x2026, 0x055c, /* 0x14a8-0x14af */ 0x055b, 0x055e, 0x0531, 0x0561, 0x0532, 0x0562, 0x0533, 0x0563, /* 0x14b0-0x14b7 */ 0x0534, 0x0564, 0x0535, 0x0565, 0x0536, 0x0566, 0x0537, 0x0567, /* 0x14b8-0x14bf */ 0x0538, 0x0568, 0x0539, 0x0569, 0x053a, 0x056a, 0x053b, 0x056b, /* 0x14c0-0x14c7 */ 0x053c, 0x056c, 0x053d, 0x056d, 0x053e, 0x056e, 0x053f, 0x056f, /* 0x14c8-0x14cf */ 0x0540, 0x0570, 0x0541, 0x0571, 0x0542, 0x0572, 0x0543, 0x0573, /* 0x14d0-0x14d7 */ 0x0544, 0x0574, 0x0545, 0x0575, 0x0546, 0x0576, 0x0547, 0x0577, /* 0x14d8-0x14df */ 0x0548, 0x0578, 0x0549, 0x0579, 0x054a, 0x057a, 0x054b, 0x057b, /* 0x14e0-0x14e7 */ 0x054c, 0x057c, 0x054d, 0x057d, 0x054e, 0x057e, 0x054f, 0x057f, /* 0x14e8-0x14ef */ 0x0550, 0x0580, 0x0551, 0x0581, 0x0552, 0x0582, 0x0553, 0x0583, /* 0x14f0-0x14f7 */ 0x0554, 0x0584, 0x0555, 0x0585, 0x0556, 0x0586, 0x2019, 0x0027, /* 0x14f8-0x14ff */ }; static unsigned short const keysym_to_unicode_15d0_15f6[] = { 0x10d0, 0x10d1, 0x10d2, 0x10d3, 0x10d4, 0x10d5, 0x10d6, 0x10d7, /* 0x15d0-0x15d7 */ 0x10d8, 0x10d9, 0x10da, 0x10db, 0x10dc, 0x10dd, 0x10de, 0x10df, /* 0x15d8-0x15df */ 0x10e0, 0x10e1, 0x10e2, 0x10e3, 0x10e4, 0x10e5, 0x10e6, 0x10e7, /* 0x15e0-0x15e7 */ 0x10e8, 0x10e9, 0x10ea, 0x10eb, 0x10ec, 0x10ed, 0x10ee, 0x10ef, /* 0x15e8-0x15ef */ 0x10f0, 0x10f1, 0x10f2, 0x10f3, 0x10f4, 0x10f5, 0x10f6 /* 0x15f0-0x15f7 */ }; static unsigned short const keysym_to_unicode_16a0_16f6[] = { 0x0000, 0x0000, 0xf0a2, 0x1e8a, 0x0000, 0xf0a5, 0x012c, 0xf0a7, /* 0x16a0-0x16a7 */ 0xf0a8, 0x01b5, 0x01e6, 0x0000, 0x0000, 0x0000, 0x0000, 0x019f, /* 0x16a8-0x16af */ 0x0000, 0x0000, 0xf0b2, 0x1e8b, 0x01d1, 0xf0b5, 0x012d, 0xf0b7, /* 0x16b0-0x16b7 */ 0xf0b8, 0x01b6, 0x01e7, 0x0000, 0x0000, 0x01d2, 0x0000, 0x0275, /* 0x16b8-0x16bf */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x018f, 0x0000, /* 0x16c0-0x16c7 */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x16c8-0x16cf */ 0x0000, 0x1e36, 0xf0d2, 0xf0d3, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x16d0-0x16d7 */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x16d8-0x16df */ 0x0000, 0x1e37, 0xf0e2, 0xf0e3, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x16e0-0x16e7 */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, /* 0x16e8-0x16ef */ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0259 /* 0x16f0-0x16f6 */ }; static unsigned short const keysym_to_unicode_1e9f_1eff[] = { 0x0303, 0x1ea0, 0x1ea1, 0x1ea2, 0x1ea3, 0x1ea4, 0x1ea5, 0x1ea6, 0x1ea7, /* 0x1ea0-0x1ea7 */ 0x1ea8, 0x1ea9, 0x1eaa, 0x1eab, 0x1eac, 0x1ead, 0x1eae, 0x1eaf, /* 0x1ea8-0x1eaf */ 0x1eb0, 0x1eb1, 0x1eb2, 0x1eb3, 0x1eb4, 0x1eb5, 0x1eb6, 0x1eb7, /* 0x1eb0-0x1eb7 */ 0x1eb8, 0x1eb9, 0x1eba, 0x1ebb, 0x1ebc, 0x1ebd, 0x1ebe, 0x1ebf, /* 0x1eb8-0x1ebf */ 0x1ec0, 0x1ec1, 0x1ec2, 0x1ec3, 0x1ec4, 0x1ec5, 0x1ec6, 0x1ec7, /* 0x1ec0-0x1ec7 */ 0x1ec8, 0x1ec9, 0x1eca, 0x1ecb, 0x1ecc, 0x1ecd, 0x1ece, 0x1ecf, /* 0x1ec8-0x1ecf */ 0x1ed0, 0x1ed1, 0x1ed2, 0x1ed3, 0x1ed4, 0x1ed5, 0x1ed6, 0x1ed7, /* 0x1ed0-0x1ed7 */ 0x1ed8, 0x1ed9, 0x1eda, 0x1edb, 0x1edc, 0x1edd, 0x1ede, 0x1edf, /* 0x1ed8-0x1edf */ 0x1ee0, 0x1ee1, 0x1ee2, 0x1ee3, 0x1ee4, 0x1ee5, 0x1ee6, 0x1ee7, /* 0x1ee0-0x1ee7 */ 0x1ee8, 0x1ee9, 0x1eea, 0x1eeb, 0x1eec, 0x1eed, 0x1eee, 0x1eef, /* 0x1ee8-0x1eef */ 0x1ef0, 0x1ef1, 0x0300, 0x0301, 0x1ef4, 0x1ef5, 0x1ef6, 0x1ef7, /* 0x1ef0-0x1ef7 */ 0x1ef8, 0x1ef9, 0x01a0, 0x01a1, 0x01af, 0x01b0, 0x0309, 0x0323 /* 0x1ef8-0x1eff */ }; static unsigned short const keysym_to_unicode_20a0_20ac[] = { 0x20a0, 0x20a1, 0x20a2, 0x20a3, 0x20a4, 0x20a5, 0x20a6, 0x20a7, /* 0x20a0-0x20a7 */ 0x20a8, 0x20a9, 0x20aa, 0x20ab, 0x20ac /* 0x20a8-0x20af */ }; unsigned int X11_KeySymToUcs4(KeySym keysym) { /* 'Unicode keysym' */ if ((keysym & 0xff000000) == 0x01000000) return (keysym & 0x00ffffff); if (keysym > 0 && keysym < 0x100) return keysym; else if (keysym > 0x1a0 && keysym < 0x200) return keysym_to_unicode_1a1_1ff[keysym - 0x1a1]; else if (keysym > 0x2a0 && keysym < 0x2ff) return keysym_to_unicode_2a1_2fe[keysym - 0x2a1]; else if (keysym > 0x3a1 && keysym < 0x3ff) return keysym_to_unicode_3a2_3fe[keysym - 0x3a2]; else if (keysym > 0x4a0 && keysym < 0x4e0) return keysym_to_unicode_4a1_4df[keysym - 0x4a1]; else if (keysym > 0x589 && keysym < 0x5ff) return keysym_to_unicode_590_5fe[keysym - 0x590]; else if (keysym > 0x67f && keysym < 0x700) return keysym_to_unicode_680_6ff[keysym - 0x680]; else if (keysym > 0x7a0 && keysym < 0x7fa) return keysym_to_unicode_7a1_7f9[keysym - 0x7a1]; else if (keysym > 0x8a3 && keysym < 0x8ff) return keysym_to_unicode_8a4_8fe[keysym - 0x8a4]; else if (keysym > 0x9de && keysym < 0x9f9) return keysym_to_unicode_9df_9f8[keysym - 0x9df]; else if (keysym > 0xaa0 && keysym < 0xaff) return keysym_to_unicode_aa1_afe[keysym - 0xaa1]; else if (keysym > 0xcde && keysym < 0xcfb) return keysym_to_unicode_cdf_cfa[keysym - 0xcdf]; else if (keysym > 0xda0 && keysym < 0xdfa) return keysym_to_unicode_da1_df9[keysym - 0xda1]; else if (keysym > 0xe9f && keysym < 0xf00) return keysym_to_unicode_ea0_eff[keysym - 0xea0]; else if (keysym > 0x12a0 && keysym < 0x12ff) return keysym_to_unicode_12a1_12fe[keysym - 0x12a1]; else if (keysym > 0x13bb && keysym < 0x13bf) return keysym_to_unicode_13bc_13be[keysym - 0x13bc]; else if (keysym > 0x14a0 && keysym < 0x1500) return keysym_to_unicode_14a1_14ff[keysym - 0x14a1]; else if (keysym > 0x15cf && keysym < 0x15f7) return keysym_to_unicode_15d0_15f6[keysym - 0x15d0]; else if (keysym > 0x169f && keysym < 0x16f7) return keysym_to_unicode_16a0_16f6[keysym - 0x16a0]; else if (keysym > 0x1e9e && keysym < 0x1f00) return keysym_to_unicode_1e9f_1eff[keysym - 0x1e9f]; else if (keysym > 0x209f && keysym < 0x20ad) return keysym_to_unicode_20a0_20ac[keysym - 0x20a0]; else return 0; } doomsday-stable-1.15.7/doomsday/libgui/src/input/keyeventsource.cpp0000664000175000017500000000210112641367671024766 0ustar jaakkojaakko/** @file keyeventsource.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/KeyEventSource" namespace de { DENG2_PIMPL_NOREF(KeyEventSource) { DENG2_PIMPL_AUDIENCE(KeyEvent) }; DENG2_AUDIENCE_METHOD(KeyEventSource, KeyEvent) KeyEventSource::KeyEventSource() : d(new Instance) {} } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/canvas.cpp0000664000175000017500000004201012641367671022032 0ustar jaakkojaakko/** @file canvas.cpp OpenGL drawing surface (QWidget). * * @authors Copyright (c) 2012-2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/Canvas" #include "de/CanvasWindow" #include "de/GLState" #include "de/GLTexture" #include "de/graphics/opengl.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace de { #ifdef DENG_X11 # define LIBGUI_CANVAS_USE_DEFERRED_RESIZE #endif DENG2_PIMPL(Canvas) { GLFramebuffer framebuf; CanvasWindow *parent; bool readyNotified; Size currentSize; Size pendingSize; #ifdef LIBGUI_CANVAS_USE_DEFERRED_RESIZE QTimer resizeTimer; #endif bool mouseGrabbed; #ifdef WIN32 bool altIsDown; #endif QPoint prevMousePos; QTime prevWheelAt; QPoint wheelAngleAccum; int wheelDir[2]; Instance(Public *i, CanvasWindow *parentWindow) : Base(i) , parent(parentWindow) , readyNotified(false) , mouseGrabbed(false) { wheelDir[0] = wheelDir[1] = 0; #ifdef WIN32 altIsDown = false; #endif //mouseDisabled = App::commandLine().has("-nomouse"); #ifdef LIBGUI_CANVAS_USE_DEFERRED_RESIZE resizeTimer.setSingleShot(true); QObject::connect(&resizeTimer, SIGNAL(timeout()), thisPublic, SLOT(updateSize())); #endif } ~Instance() { glDeinit(); } void grabMouse() { if(!self.isVisible()/* || mouseDisabled*/) return; if(!mouseGrabbed) { LOG_INPUT_VERBOSE("Grabbing mouse") << mouseGrabbed; mouseGrabbed = true; DENG2_FOR_PUBLIC_AUDIENCE2(MouseStateChange, i) { i->mouseStateChanged(Trapped); } } } void ungrabMouse() { if(!self.isVisible()/* || mouseDisabled*/) return; if(mouseGrabbed) { LOG_INPUT_VERBOSE("Ungrabbing mouse"); // Tell the mouse driver that the mouse is untrapped. mouseGrabbed = false; DENG2_FOR_PUBLIC_AUDIENCE2(MouseStateChange, i) { i->mouseStateChanged(Untrapped); } } } static int nativeCode(QKeyEvent const *ev) { #if defined(UNIX) && !defined(MACOSX) return ev->nativeScanCode(); #else return ev->nativeVirtualKey(); #endif } void handleKeyEvent(QKeyEvent *ev) { //LOG_AS("Canvas"); ev->accept(); //if(ev->isAutoRepeat()) return; // Ignore repeats, we do our own. /* qDebug() << "Canvas: key press" << ev->key() << QString("0x%1").arg(ev->key(), 0, 16) << "text:" << ev->text() << "native:" << ev->nativeVirtualKey() << "scancode:" << ev->nativeScanCode(); */ #ifdef WIN32 // We must track the state of the alt key ourselves as the OS grabs the up event... if(ev->key() == Qt::Key_Alt) { if(ev->type() == QEvent::KeyPress) { if(altIsDown) return; // Ignore repeat down events(!)? altIsDown = true; } else if(ev->type() == QEvent::KeyRelease) { if(!altIsDown) { LOG_DEBUG("Ignoring repeat alt up."); return; // Ignore repeat up events. } altIsDown = false; //LOG_DEBUG("Alt is up."); } } #endif DENG2_FOR_PUBLIC_AUDIENCE2(KeyEvent, i) { i->keyEvent(KeyEvent(ev->isAutoRepeat()? KeyEvent::Repeat : ev->type() == QEvent::KeyPress? KeyEvent::Pressed : KeyEvent::Released, ev->key(), KeyEvent::ddKeyFromQt(ev->key(), ev->nativeVirtualKey(), ev->nativeScanCode()), nativeCode(ev), ev->text(), (ev->modifiers().testFlag(Qt::ShiftModifier)? KeyEvent::Shift : KeyEvent::NoModifiers) | (ev->modifiers().testFlag(Qt::ControlModifier)? KeyEvent::Control : KeyEvent::NoModifiers) | (ev->modifiers().testFlag(Qt::AltModifier)? KeyEvent::Alt : KeyEvent::NoModifiers) | (ev->modifiers().testFlag(Qt::MetaModifier)? KeyEvent::Meta : KeyEvent::NoModifiers))); } } void reconfigureFramebuffer() { framebuf.setColorFormat(Image::RGB_888); framebuf.resize(currentSize); } void glInit() { DENG2_ASSERT(parent != 0); framebuf.glInit(); } void glDeinit() { framebuf.glDeinit(); } void swapBuffers(gl::SwapBufferMode mode) { if(mode == gl::SwapStereoBuffers && !self.format().stereo()) { // The canvas is not using a stereo format, must do a normal swap. mode = gl::SwapMonoBuffer; } /// @todo Double buffering is not really needed in manual FB mode. framebuf.swapBuffers(self, mode); } template Vector2i translatePosition(QtEventType const *ev) const { #ifdef DENG2_QT_5_1_OR_NEWER return Vector2i(ev->pos().x(), ev->pos().y()) * self.devicePixelRatio(); #else return Vector2i(ev->pos().x(), ev->pos().y()); #endif } DENG2_PIMPL_AUDIENCE(GLReady) DENG2_PIMPL_AUDIENCE(GLInit) DENG2_PIMPL_AUDIENCE(GLResize) DENG2_PIMPL_AUDIENCE(GLDraw) DENG2_PIMPL_AUDIENCE(FocusChange) }; DENG2_AUDIENCE_METHOD(Canvas, GLReady) DENG2_AUDIENCE_METHOD(Canvas, GLInit) DENG2_AUDIENCE_METHOD(Canvas, GLResize) DENG2_AUDIENCE_METHOD(Canvas, GLDraw) DENG2_AUDIENCE_METHOD(Canvas, FocusChange) Canvas::Canvas(CanvasWindow* parent, QGLWidget* shared) : QGLWidget(parent, shared), d(new Instance(this, parent)) { LOG_AS("Canvas"); LOGDEV_GL_VERBOSE("Swap interval: ") << format().swapInterval(); LOGDEV_GL_VERBOSE("Multisampling: %b") << (GLFramebuffer::defaultMultisampling() > 1); // We will be doing buffer swaps manually (for timing purposes). setAutoBufferSwap(false); setMouseTracking(true); setFocusPolicy(Qt::StrongFocus); } void Canvas::setParent(CanvasWindow *parent) { d->parent = parent; } QImage Canvas::grabImage(QSize const &outputSize) { return grabImage(rect(), outputSize); } QImage Canvas::grabImage(QRect const &area, QSize const &outputSize) { // We will be grabbing the visible, latest complete frame. glReadBuffer(GL_FRONT); QImage grabbed = grabFrameBuffer(); // no alpha if(area.size() != grabbed.size()) { // Just take a portion of the full image. grabbed = grabbed.copy(area); } glReadBuffer(GL_BACK); if(outputSize.isValid()) { grabbed = grabbed.scaled(outputSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } return grabbed; } GLuint Canvas::grabAsTexture(QSize const &outputSize) { return grabAsTexture(rect(), outputSize); } GLuint Canvas::grabAsTexture(QRect const &area, QSize const &outputSize) { return bindTexture(grabImage(area, outputSize), GL_TEXTURE_2D, GL_RGB, QGLContext::LinearFilteringBindOption); } Canvas::Size Canvas::size() const { return d->currentSize; } void Canvas::trapMouse(bool trap) { if(trap) { d->grabMouse(); } else { d->ungrabMouse(); } } bool Canvas::isMouseTrapped() const { return d->mouseGrabbed; } bool Canvas::isGLReady() const { return d->readyNotified; } void Canvas::copyAudiencesFrom(Canvas const &other) { d->audienceForGLReady = other.d->audienceForGLReady; d->audienceForGLInit = other.d->audienceForGLInit; d->audienceForGLResize = other.d->audienceForGLResize; d->audienceForGLDraw = other.d->audienceForGLDraw; d->audienceForFocusChange = other.d->audienceForFocusChange; audienceForKeyEvent() = other.audienceForKeyEvent(); audienceForMouseStateChange() = other.audienceForMouseStateChange(); audienceForMouseEvent() = other.audienceForMouseEvent(); } GLTarget &Canvas::renderTarget() const { return d->framebuf.target(); } GLFramebuffer &Canvas::framebuffer() { return d->framebuf; } void Canvas::swapBuffers(gl::SwapBufferMode swapMode) { d->swapBuffers(swapMode); } void Canvas::initializeGL() { LOG_AS("Canvas"); LOGDEV_GL_NOTE("Notifying GL init (during paint)"); #ifdef LIBGUI_USE_GLENTRYPOINTS getAllOpenGLEntryPoints(); #endif GLInfo::glInit(); DENG2_FOR_AUDIENCE2(GLInit, i) i->canvasGLInit(*this); } void Canvas::resizeGL(int w, int h) { d->pendingSize = Size(max(0, w), max(0, h)); // Only react if this is actually a resize. if(d->currentSize != d->pendingSize) { #ifdef LIBGUI_CANVAS_USE_DEFERRED_RESIZE LOGDEV_GL_MSG("Canvas %p triggered size to %ix%i from %s") << this << w << h << d->currentSize.asText(); d->resizeTimer.start(100); #else updateSize(); #endif } } void Canvas::updateSize() { #ifdef LIBGUI_CANVAS_USE_DEFERRED_RESIZE LOGDEV_GL_MSG("Canvas %p resizing now") << this; #endif makeCurrent(); d->currentSize = d->pendingSize; d->reconfigureFramebuffer(); DENG2_FOR_AUDIENCE2(GLResize, i) i->canvasGLResized(*this); } void Canvas::showEvent(QShowEvent* ev) { LOG_AS("Canvas"); QGLWidget::showEvent(ev); // The first time the window is shown, run the initialization callback. On // some platforms, OpenGL is not fully ready to be used before the window // actually appears on screen. if(isVisible() && !d->readyNotified) { LOGDEV_GL_XVERBOSE("Received first show event, scheduling GL ready notification"); #ifdef LIBGUI_USE_GLENTRYPOINTS makeCurrent(); getAllOpenGLEntryPoints(); #endif GLInfo::glInit(); QTimer::singleShot(1, this, SLOT(notifyReady())); } } void Canvas::notifyReady() { if(d->readyNotified) return; d->readyNotified = true; d->glInit(); d->reconfigureFramebuffer(); // Print some information. QGLFormat const fmt = format(); if(fmt.openGLVersionFlags().testFlag(QGLFormat::OpenGL_Version_3_3)) LOG_GL_NOTE("OpenGL 3.3 supported"); else if((fmt.openGLVersionFlags().testFlag(QGLFormat::OpenGL_Version_3_2))) LOG_GL_NOTE("OpenGL 3.2 supported"); else if((fmt.openGLVersionFlags().testFlag(QGLFormat::OpenGL_Version_3_1))) LOG_GL_NOTE("OpenGL 3.1 supported"); else if((fmt.openGLVersionFlags().testFlag(QGLFormat::OpenGL_Version_3_0))) LOG_GL_NOTE("OpenGL 3.0 supported"); else if((fmt.openGLVersionFlags().testFlag(QGLFormat::OpenGL_Version_2_1))) LOG_GL_NOTE("OpenGL 2.1 supported"); else if((fmt.openGLVersionFlags().testFlag(QGLFormat::OpenGL_Version_2_0))) LOG_GL_NOTE("OpenGL 2.0 supported"); else LOG_GL_WARNING("OpenGL 2.0 is not supported!"); LOGDEV_GL_XVERBOSE("Notifying GL ready"); DENG2_FOR_AUDIENCE2(GLReady, i) i->canvasGLReady(*this); // This Canvas instance might have been destroyed now. } void Canvas::paintGL() { if(!d->parent || d->parent->isRecreationInProgress()) return; #ifdef LIBGUI_USE_GLENTRYPOINTS if(!glBindFramebuffer) return; #endif DENG2_ASSERT(QGLContext::currentContext() != 0); LIBGUI_ASSERT_GL_OK(); // Make sure any changes to the state stack become effective. GLState::current().apply(); DENG2_FOR_AUDIENCE2(GLDraw, i) i->canvasGLDraw(*this); LIBGUI_ASSERT_GL_OK(); } void Canvas::focusInEvent(QFocusEvent*) { LOG_AS("Canvas"); LOG_INPUT_VERBOSE("Gained focus"); DENG2_FOR_AUDIENCE2(FocusChange, i) i->canvasFocusChanged(*this, true); } void Canvas::focusOutEvent(QFocusEvent*) { LOG_AS("Canvas"); LOG_INPUT_VERBOSE("Lost focus"); // Automatically ungrab the mouse if focus is lost. d->ungrabMouse(); DENG2_FOR_AUDIENCE2(FocusChange, i) i->canvasFocusChanged(*this, false); } void Canvas::keyPressEvent(QKeyEvent *ev) { d->handleKeyEvent(ev); } void Canvas::keyReleaseEvent(QKeyEvent *ev) { d->handleKeyEvent(ev); } static MouseEvent::Button translateButton(Qt::MouseButton btn) { if(btn == Qt::LeftButton) return MouseEvent::Left; #ifdef DENG2_QT_4_7_OR_NEWER if(btn == Qt::MiddleButton) return MouseEvent::Middle; #else if(btn == Qt::MidButton) return MouseEvent::Middle; #endif if(btn == Qt::RightButton) return MouseEvent::Right; if(btn == Qt::XButton1) return MouseEvent::XButton1; if(btn == Qt::XButton2) return MouseEvent::XButton2; return MouseEvent::Unknown; } void Canvas::mousePressEvent(QMouseEvent *ev) { ev->accept(); DENG2_FOR_AUDIENCE2(MouseEvent, i) { i->mouseEvent(MouseEvent(translateButton(ev->button()), MouseEvent::Pressed, d->translatePosition(ev))); } } void Canvas::mouseReleaseEvent(QMouseEvent* ev) { ev->accept(); DENG2_FOR_AUDIENCE2(MouseEvent, i) { i->mouseEvent(MouseEvent(translateButton(ev->button()), MouseEvent::Released, d->translatePosition(ev))); } } void Canvas::mouseMoveEvent(QMouseEvent *ev) { ev->accept(); // Absolute events are only emitted when the mouse is untrapped. if(!d->mouseGrabbed) { DENG2_FOR_AUDIENCE2(MouseEvent, i) { i->mouseEvent(MouseEvent(MouseEvent::Absolute, d->translatePosition(ev))); } } } void Canvas::wheelEvent(QWheelEvent *ev) { ev->accept(); #ifdef DENG2_QT_5_0_OR_NEWER float const devicePixels = d->parent->devicePixelRatio(); QPoint numPixels = ev->pixelDelta(); QPoint numDegrees = ev->angleDelta() / 8; d->wheelAngleAccum += numDegrees; if(!numPixels.isNull()) { DENG2_FOR_AUDIENCE2(MouseEvent, i) { if(numPixels.x()) { i->mouseEvent(MouseEvent(MouseEvent::FineAngle, Vector2i(devicePixels * numPixels.x(), 0), d->translatePosition(ev))); } if(numPixels.y()) { i->mouseEvent(MouseEvent(MouseEvent::FineAngle, Vector2i(0, devicePixels * numPixels.y()), d->translatePosition(ev))); } } } QPoint const steps = d->wheelAngleAccum / 15; if(!steps.isNull()) { DENG2_FOR_AUDIENCE2(MouseEvent, i) { if(steps.x()) { i->mouseEvent(MouseEvent(MouseEvent::Step, Vector2i(steps.x(), 0), !d->mouseGrabbed? d->translatePosition(ev) : Vector2i())); } if(steps.y()) { i->mouseEvent(MouseEvent(MouseEvent::Step, Vector2i(0, steps.y()), !d->mouseGrabbed? d->translatePosition(ev) : Vector2i())); } } d->wheelAngleAccum -= steps * 15; } #else static const int MOUSE_WHEEL_CONTINUOUS_THRESHOLD_MS = 100; bool continuousMovement = (d->prevWheelAt.elapsed() < MOUSE_WHEEL_CONTINUOUS_THRESHOLD_MS); int axis = (ev->orientation() == Qt::Horizontal? 0 : 1); int dir = (ev->delta() < 0? -1 : 1); DENG2_FOR_AUDIENCE2(MouseEvent, i) { i->mouseEvent(MouseEvent(MouseEvent::FineAngle, axis == 0? Vector2i(ev->delta(), 0) : Vector2i(0, ev->delta()), d->translatePosition(ev))); } if(!continuousMovement || d->wheelDir[axis] != dir) { d->wheelDir[axis] = dir; DENG2_FOR_AUDIENCE2(MouseEvent, i) { i->mouseEvent(MouseEvent(MouseEvent::Step, axis == 0? Vector2i(dir, 0) : axis == 1? Vector2i(0, dir) : Vector2i(), !d->mouseGrabbed? d->translatePosition(ev) : Vector2i())); } } #endif d->prevWheelAt.start(); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/text/0000775000175000017500000000000012641367671021042 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libgui/src/text/qtnativefont.cpp0000664000175000017500000000740512641367671024276 0ustar jaakkojaakko/** @file qtnativefont.cpp Qt-based native font. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "qtnativefont.h" #include #include #include namespace de { DENG2_PIMPL_NOREF(QtNativeFont) { QFont font; QScopedPointer metrics; }; } namespace de { QtNativeFont::QtNativeFont(String const &family) : NativeFont(family), d(new Instance) {} QtNativeFont::QtNativeFont(QFont const &font) : NativeFont(font.family()), d(new Instance) { d->font = font; setSize(font.pointSizeF()); setWeight(font.weight()); setStyle(font.italic()? Italic : Regular); } QtNativeFont::QtNativeFont(QtNativeFont const &other) : NativeFont(other), d(new Instance) { d->font = other.d->font; } QtNativeFont &QtNativeFont::operator = (QtNativeFont const &other) { NativeFont::operator = (other); d->font = other.d->font; return *this; } void QtNativeFont::commit() const { d->font.setFamily(family()); d->font.setPointSizeF(size()); d->font.setStyle(style() == Italic? QFont::StyleItalic : QFont::StyleNormal); d->font.setWeight(weight()); d->metrics.reset(new QFontMetrics(d->font)); } int QtNativeFont::nativeFontAscent() const { return d->metrics->ascent(); } int QtNativeFont::nativeFontDescent() const { return d->metrics->descent(); } int QtNativeFont::nativeFontHeight() const { return d->metrics->height(); } int QtNativeFont::nativeFontLineSpacing() const { return d->metrics->lineSpacing(); } Rectanglei QtNativeFont::nativeFontMeasure(String const &text) const { #ifdef LIBGUI_ACCURATE_TEXT_BOUNDS Rectanglei rect = Rectanglei::fromQRect(d->metrics->boundingRect(text)); #else Rectanglei rect(Vector2i(0, -d->metrics->ascent()), Vector2i(d->metrics->width(text), d->metrics->descent())); #endif if(rect.height() == 0) { // It seems measuring the bounds of a Tab character produces // strange results (position 100000?). rect = Rectanglei(0, 0, rect.width(), 0); } return rect; } int QtNativeFont::nativeFontWidth(String const &text) const { return d->metrics->width(text); } QImage QtNativeFont::nativeFontRasterize(String const &text, Vector4ub const &foreground, Vector4ub const &background) const { Rectanglei const bounds = measure(text); QColor const fgColor(foreground.x, foreground.y, foreground.z, foreground.w); QColor const bgColor(background.x, background.y, background.z, background.w); QImage img(QSize(bounds.width() + 1, bounds.height() + 1), QImage::Format_ARGB32); img.fill(bgColor.rgba()); QPainter painter(&img); painter.setCompositionMode(QPainter::CompositionMode_Source); painter.setFont(d->font); painter.setPen(fgColor); painter.setBrush(bgColor); painter.drawText(-bounds.left(), -bounds.top(), text); return img; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/text/font.cpp0000664000175000017500000002225712641367671022524 0ustar jaakkojaakko/** @file font.cpp Font with metrics. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/Font" #include #include #include #include #include #if defined(MACOSX) && defined(MACOS_10_7) # include "coretextnativefont_macx.h" namespace de { typedef CoreTextNativeFont PlatformFont; } #else # include "qtnativefont.h" namespace de { typedef QtNativeFont PlatformFont; } #endif namespace de { DENG2_PIMPL(Font) { PlatformFont font; ConstantRule *heightRule; ConstantRule *ascentRule; ConstantRule *descentRule; ConstantRule *lineSpacingRule; int ascent; Instance(Public *i) : Base(i), ascent(0) { createRules(); } Instance(Public *i, PlatformFont const &qfont) : Base(i), font(qfont) { #if 0 // Development aid: list all available fonts and styles. QFontDatabase db; foreach(QString fam, db.families()) { qDebug() << "FONT FAMILY:" << fam; qDebug() << "\tStyles:" << db.styles(fam); } #endif createRules(); updateMetrics(); } ~Instance() { releaseRef(heightRule); releaseRef(ascentRule); releaseRef(descentRule); releaseRef(lineSpacingRule); } void createRules() { heightRule = new ConstantRule(0); ascentRule = new ConstantRule(0); descentRule = new ConstantRule(0); lineSpacingRule = new ConstantRule(0); } void updateMetrics() { ascent = font.ascent(); if(font.weight() != NativeFont::Normal) { // Use the ascent of the normal weight for non-normal weights; // we need to align content to baseline regardless of weight. PlatformFont normalized(font); normalized.setWeight(NativeFont::Normal); ascent = normalized.ascent(); } ascentRule->set(ascent); descentRule->set(font.descent()); heightRule->set(font.height()); lineSpacingRule->set(font.lineSpacing()); } /** * Produces a font based on this one but with the attribute modifications applied * from a rich format range. * * @param rich Rich formatting. * * @return Font with applied formatting. */ PlatformFont alteredFont(RichFormat::Iterator const &rich) const { if(!rich.isDefault()) { PlatformFont mod = font; // Size change. if(!fequal(rich.sizeFactor(), 1.f)) { mod.setSize(mod.size() * rich.sizeFactor()); } // Style change (including monospace). switch(rich.style()) { case RichFormat::OriginalStyle: break; case RichFormat::Regular: mod.setFamily(font.family()); mod.setStyle(NativeFont::Regular); break; case RichFormat::Italic: mod.setFamily(font.family()); mod.setStyle(NativeFont::Italic); break; case RichFormat::Monospace: if(rich.format.format().hasStyle()) { if(Font const *altFont = rich.format.format().style().richStyleFont(rich.style())) { mod.setFamily(altFont->d->font.family()); mod.setStyle (altFont->d->font.style()); mod.setWeight(altFont->d->font.weight()); mod.setSize (altFont->d->font.size()); } } break; } // Weight change. if(rich.weight() != RichFormat::OriginalWeight) { mod.setWeight(rich.weight() == RichFormat::Normal? NativeFont::Normal : rich.weight() == RichFormat::Bold? NativeFont::Bold : NativeFont::Light); } return mod; } return font; } }; Font::Font() : d(new Instance(this)) {} Font::Font(Font const &other) : d(new Instance(this, other.d->font)) {} Font::Font(QFont const &font) : d(new Instance(this, font)) {} Rectanglei Font::measure(String const &textLine) const { return measure(textLine, RichFormat::fromPlainText(textLine)); } Rectanglei Font::measure(String const &textLine, RichFormatRef const &format) const { Rectanglei bounds; int advance = 0; RichFormat::Iterator iter(format); while(iter.hasNext()) { iter.next(); if(iter.range().isEmpty()) continue; PlatformFont const altFont = d->alteredFont(iter); String const part = textLine.substr(iter.range()); Rectanglei rect = altFont.measure(part); // Combine to the total bounds. rect.moveTopLeft(Vector2i(advance, rect.top())); bounds |= rect; advance += altFont.width(part); } return bounds; } int Font::advanceWidth(String const &textLine) const { return advanceWidth(textLine, RichFormat::fromPlainText(textLine)); } int Font::advanceWidth(String const &textLine, RichFormatRef const &format) const { int advance = 0; RichFormat::Iterator iter(format); while(iter.hasNext()) { iter.next(); if(iter.range().isEmpty()) continue; advance += d->alteredFont(iter).width(textLine.substr(iter.range())); } return advance; } QImage Font::rasterize(String const &textLine, Vector4ub const &foreground, Vector4ub const &background) const { return rasterize(textLine, RichFormat::fromPlainText(textLine), foreground, background); } QImage Font::rasterize(String const &textLine, RichFormatRef const &format, Vector4ub const &foreground, Vector4ub const &background) const { if(textLine.isEmpty()) { return QImage(); } #ifdef LIBGUI_ACCURATE_TEXT_BOUNDS Rectanglei const bounds = measure(textLine, format); #else Rectanglei const bounds(0, 0, advanceWidth(textLine, format), d->font.height()); #endif QColor bgColor(background.x, background.y, background.z, background.w); Vector4ub fg = foreground; Vector4ub bg = background; QImage img(QSize(bounds.width(), de::max(duint(d->font.height()), bounds.height())), QImage::Format_ARGB32); img.fill(bgColor.rgba()); QPainter painter(&img); painter.setCompositionMode(QPainter::CompositionMode_Source); // Composite the final image by drawing each rich range first into a separate // bitmap and then drawing those into the final image. int advance = 0; RichFormat::Iterator iter(format); while(iter.hasNext()) { iter.next(); if(iter.range().isEmpty()) continue; PlatformFont font = d->font; if(iter.isDefault()) { fg = foreground; bg = background; } else { font = d->alteredFont(iter); if(iter.colorIndex() != RichFormat::OriginalColor) { fg = iter.color(); bg = Vector4ub(fg, 0); } else { fg = foreground; bg = background; } } String const part = textLine.substr(iter.range()); #ifdef WIN32 // Kludge: No light-weight fonts available, so reduce opacity to give the // illusion of thinness. if(iter.weight() == RichFormat::Light) { if(Vector3ub(60, 60, 60) > fg) // dark fg.w *= .66f; else if(Vector3ub(230, 230, 230) < fg) // light fg.w *= .85f; else fg.w *= .925f; } #endif QImage fragment = font.rasterize(part, fg, bg); Rectanglei const bounds = font.measure(part); painter.drawImage(QPoint(advance + bounds.left(), d->ascent + bounds.top()), fragment); advance += font.width(part); } return img; } Rule const &Font::height() const { return *d->heightRule; } Rule const &Font::ascent() const { return *d->ascentRule; } Rule const &Font::descent() const { return *d->descentRule; } Rule const &Font::lineSpacing() const { return *d->lineSpacingRule; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/text/qtnativefont.h0000664000175000017500000000333712641367671023743 0ustar jaakkojaakko/** @file qtnativefont.h Qt-based native font. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_QTNATIVEFONT_H #define LIBGUI_QTNATIVEFONT_H #include "de/NativeFont" namespace de { class QtNativeFont : public NativeFont { public: QtNativeFont(String const &family = ""); QtNativeFont(QtNativeFont const &other); QtNativeFont(QFont const &font); QtNativeFont &operator = (QtNativeFont const &other); protected: void commit() const; int nativeFontAscent() const; int nativeFontDescent() const; int nativeFontHeight() const; int nativeFontLineSpacing() const; Rectanglei nativeFontMeasure(String const &text) const; int nativeFontWidth(String const &text) const; QImage nativeFontRasterize(String const &text, Vector4ub const &foreground, Vector4ub const &background) const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_QTNATIVEFONT_H doomsday-stable-1.15.7/doomsday/libgui/src/text/nativefont.cpp0000664000175000017500000001030012641367671023715 0ustar jaakkojaakko/** @file nativefont.cpp Abstraction of a native font. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/NativeFont" namespace de { typedef QMap Families; static Families families; DENG2_PIMPL(NativeFont) { String family; dfloat size; Style style; int weight; String cachedText; // Measuring is done repeatedly Rectanglei cachedMeasure; Instance(Public *i) : Base(i) , size(12.f) , style(Regular) , weight(Normal) {} void prepare() { if(!self.isReady()) { self.commit(); cachedText.clear(); self.setState(Ready); } } void markNotReady() { self.setState(NotReady); cachedText.clear(); } }; } namespace de { void NativeFont::defineMapping(String const &family, StyleMapping const &mapping) { families.insert(family, mapping); } NativeFont::NativeFont(String const &family) : d(new Instance(this)) { setFamily(family); } NativeFont::NativeFont(NativeFont const &other) : Asset(other), d(new Instance(this)) { *this = other; } NativeFont &NativeFont::operator = (NativeFont const &other) { d->family = other.d->family; d->style = other.d->style; d->size = other.d->size; d->weight = other.d->weight; d->markNotReady(); return *this; } void NativeFont::setFamily(String const &family) { d->family = family; d->markNotReady(); } void NativeFont::setSize(dfloat size) { d->size = size; d->markNotReady(); } void NativeFont::setStyle(Style style) { d->style = style; d->markNotReady(); } void NativeFont::setWeight(dint weight) { d->weight = weight; d->markNotReady(); } String NativeFont::family() const { return d->family; } dfloat NativeFont::size() const { return d->size; } NativeFont::Style NativeFont::style() const { return d->style; } dint NativeFont::weight() const { return d->weight; } String NativeFont::nativeFontName() const { // Check the defined mappings. if(families.contains(d->family)) { StyleMapping const &map = families[d->family]; Spec const spec(d->style, d->weight); if(map.contains(spec)) { return map[spec]; } } return d->family; } int NativeFont::ascent() const { d->prepare(); return nativeFontAscent(); } int NativeFont::descent() const { d->prepare(); return nativeFontDescent(); } int NativeFont::height() const { d->prepare(); return nativeFontHeight(); } int NativeFont::lineSpacing() const { d->prepare(); return nativeFontLineSpacing(); } Rectanglei NativeFont::measure(String const &text) const { d->prepare(); if(d->cachedText == text) { return d->cachedMeasure; } Rectanglei const bounds = nativeFontMeasure(text); // Remember this for later. d->cachedText = text; d->cachedMeasure = bounds; return bounds; } int NativeFont::width(String const &text) const { d->prepare(); return nativeFontWidth(text); } QImage NativeFont::rasterize(String const &text, Vector4ub const &foreground, Vector4ub const &background) const { d->prepare(); return nativeFontRasterize(text, foreground, background); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/text/coretextnativefont_macx.h0000664000175000017500000000353012641367671026157 0ustar jaakkojaakko/** @file coretextnativefont_macx.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBGUI_CORETEXTNATIVEFONT_H #define LIBGUI_CORETEXTNATIVEFONT_H #include "de/NativeFont" namespace de { /** * OS X specific native font implementation that uses Core Text. */ class CoreTextNativeFont : public NativeFont { public: CoreTextNativeFont(String const &family = ""); CoreTextNativeFont(CoreTextNativeFont const &other); CoreTextNativeFont(QFont const &font); CoreTextNativeFont &operator = (CoreTextNativeFont const &other); protected: void commit() const; int nativeFontAscent() const; int nativeFontDescent() const; int nativeFontHeight() const; int nativeFontLineSpacing() const; Rectanglei nativeFontMeasure(String const &text) const; int nativeFontWidth(String const &text) const; QImage nativeFontRasterize(String const &text, Vector4ub const &foreground, Vector4ub const &background) const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBGUI_CORETEXTNATIVEFONT_H doomsday-stable-1.15.7/doomsday/libgui/src/text/fontbank.cpp0000664000175000017500000000671412641367671023360 0ustar jaakkojaakko/** @file fontbank.cpp Font bank. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/FontBank" #include "de/Font" #include #include #include namespace de { DENG2_PIMPL(FontBank) { struct FontSource : public ISource { FontBank &bank; String id; FontSource(FontBank &b, String const &fontId) : bank(b), id(fontId) {} Time modifiedAt() const { return bank.sourceModifiedAt(); } Font *load() const { Record const &def = bank[id]; // Font family. QFont font(def["family"]); // Size. String size = def["size"]; if(size.endsWith("px")) { font.setPixelSize(size.toInt(0, 10, String::AllowSuffix) * bank.d->fontSizeFactor); } else { font.setPointSize(size.toInt(0, 10, String::AllowSuffix) * bank.d->fontSizeFactor); } // Weight. String const weight = def["weight"]; font.setWeight(weight == "light"? QFont::Light : weight == "bold"? QFont::Bold : QFont::Normal); // Style. String const style = def["style"]; font.setStyle(style == "italic"? QFont::StyleItalic : QFont::StyleNormal); // Transformation function. String const caps = def.gets("transform", "normal"); font.setCapitalization(caps == "uppercase"? QFont::AllUppercase : caps == "lowercase"? QFont::AllLowercase : QFont::MixedCase); return new Font(font); } }; struct FontData : public IData { Font *font; // owned FontData(Font *f = 0) : font(f) {} ~FontData() { delete font; } }; float fontSizeFactor; Instance(Public *i) : Base(i) , fontSizeFactor(1.f) {} }; FontBank::FontBank() : InfoBank("FontBank", DisableHotStorage), d(new Instance(this)) {} void FontBank::addFromInfo(File const &file) { LOG_AS("FontBank"); parse(file); addFromInfoBlocks("font"); } Font const &FontBank::font(DotPath const &path) const { return *data(path).as().font; } void FontBank::setFontSizeFactor(float sizeFactor) { d->fontSizeFactor = clamp(.1f, sizeFactor, 20.f); } Bank::ISource *FontBank::newSourceFromInfo(String const &id) { return new Instance::FontSource(*this, id); } Bank::IData *FontBank::loadFromSource(ISource &source) { return new Instance::FontData(source.as().load()); } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/text/coretextnativefont_macx.cpp0000664000175000017500000002402412641367671026513 0ustar jaakkojaakko/** @file coretextnativefont_macx.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "coretextnativefont_macx.h" #include #include #include #include #include namespace de { struct CoreTextFontCache : public Lockable { struct Key { String name; dfloat size; Key(String const &n = "", dfloat s = 12.f) : name(n), size(s) {} bool operator < (Key const &other) const { if(name == other.name) { return size < other.size && !fequal(size, other.size); } return name < other.name; } }; typedef QMap Fonts; Fonts fonts; CGColorSpaceRef _colorspace; ///< Shared by all fonts. CoreTextFontCache() : _colorspace(0) {} ~CoreTextFontCache() { clear(); if(_colorspace) { CGColorSpaceRelease(_colorspace); } } CGColorSpaceRef colorspace() { if(!_colorspace) { _colorspace = CGColorSpaceCreateDeviceRGB(); } return _colorspace; } void clear() { DENG2_GUARD(this); foreach(CTFontRef ref, fonts.values()) { CFRelease(ref); } } CTFontRef getFont(String const &postScriptName, dfloat pointSize) { CTFontRef font; // Only lock the cache while accessing the fonts database. If we keep the // lock while printing log output, a flush might occur, which might in turn // lead to text rendering and need for font information -- causing a hang. { DENG2_GUARD(this); Key const key(postScriptName, pointSize); if(fonts.contains(key)) { // Already got it. return fonts[key]; } // Get a reference to the font. CFStringRef name = CFStringCreateWithCharacters(nil, (UniChar *) postScriptName.data(), postScriptName.size()); font = CTFontCreateWithName(name, pointSize, nil); CFRelease(name); fonts.insert(key, font); } LOG_GL_VERBOSE("Cached native font '%s' size %.1f") << postScriptName << pointSize; return font; } #ifdef DENG2_DEBUG float fontSize(CTFontRef font) const { DENG2_FOR_EACH_CONST(Fonts, i, fonts) { if(i.value() == font) return i.key().size; } DENG2_ASSERT(!"Font not in cache"); return 0; } int fontWeight(CTFontRef font) const { DENG2_FOR_EACH_CONST(Fonts, i, fonts) { if(i.value() == font) { if(i.key().name.contains("Light")) return 25; if(i.key().name.contains("Bold")) return 75; return 50; } } DENG2_ASSERT(!"Font not in cache"); return 0; } #endif }; static CoreTextFontCache fontCache; DENG2_PIMPL(CoreTextNativeFont) { enum Transformation { NoTransform, Uppercase, Lowercase }; CTFontRef font; float ascent; float descent; float height; float lineSpacing; // Note that while fonts are used from multiple threads, native font instances // are copied once per each rich formatting range so we don't need to worry // about this cached data being used from many threads. String lineText; CTLineRef line; Transformation xform; Instance(Public *i) : Base(i) , font(0) , ascent(0) , descent(0) , height(0) , lineSpacing(0) , line(0) , xform(NoTransform) {} Instance(Public *i, Instance const &other) : Base(i) , font(other.font) , ascent(other.ascent) , descent(other.descent) , height(other.height) , lineSpacing(other.lineSpacing) , line(0) , xform(other.xform) {} ~Instance() { release(); } String applyTransformation(String const &str) const { switch(xform) { case Uppercase: return str.toUpper(); case Lowercase: return str.toLower(); default: break; } return str; } void release() { font = 0; releaseLine(); } void releaseLine() { if(line) { CFRelease(line); line = 0; } lineText.clear(); } void updateFontAndMetrics() { release(); // Get a reference to the font. font = fontCache.getFont(self.nativeFontName(), self.size()); // Get basic metrics about the font. ascent = ceil(CTFontGetAscent(font)); descent = ceil(CTFontGetDescent(font)); height = ascent + descent; lineSpacing = height + CTFontGetLeading(font); } void makeLine(String const &text, CGColorRef color = 0) { if(lineText == text) return; // Already got it. releaseLine(); lineText = text; void const *keys[] = { kCTFontAttributeName, kCTForegroundColorAttributeName }; void const *values[] = { font, color }; CFDictionaryRef attribs = CFDictionaryCreate(nil, keys, values, color? 2 : 1, nil, nil); CFStringRef textStr = CFStringCreateWithCharacters(nil, (UniChar *) text.data(), text.size()); CFAttributedStringRef as = CFAttributedStringCreate(0, textStr, attribs); line = CTLineCreateWithAttributedString(as); CFRelease(attribs); CFRelease(textStr); CFRelease(as); } }; } // namespace de namespace de { CoreTextNativeFont::CoreTextNativeFont(String const &family) : NativeFont(family), d(new Instance(this)) {} CoreTextNativeFont::CoreTextNativeFont(QFont const &font) : NativeFont(font.family()), d(new Instance(this)) { setSize (font.pointSizeF()); setWeight(font.weight()); setStyle (font.italic()? Italic : Regular); d->xform = (font.capitalization() == QFont::AllUppercase? Instance::Uppercase : font.capitalization() == QFont::AllLowercase? Instance::Lowercase : Instance::NoTransform); } CoreTextNativeFont::CoreTextNativeFont(CoreTextNativeFont const &other) : NativeFont(other), d(new Instance(this, *other.d)) { // If the other is ready, this will be too. setState(other.state()); } CoreTextNativeFont &CoreTextNativeFont::operator = (CoreTextNativeFont const &other) { NativeFont::operator = (other); d.reset(new Instance(this, *other.d)); // If the other is ready, this will be too. setState(other.state()); return *this; } void CoreTextNativeFont::commit() const { d->updateFontAndMetrics(); } int CoreTextNativeFont::nativeFontAscent() const { return roundi(d->ascent); } int CoreTextNativeFont::nativeFontDescent() const { return roundi(d->descent); } int CoreTextNativeFont::nativeFontHeight() const { return roundi(d->height); } int CoreTextNativeFont::nativeFontLineSpacing() const { return roundi(d->lineSpacing); } Rectanglei CoreTextNativeFont::nativeFontMeasure(String const &text) const { d->makeLine(d->applyTransformation(text)); //CGLineGetImageBounds(d->line, d->gc); // more accurate but slow Rectanglei rect(Vector2i(0, -d->ascent), Vector2i(roundi(CTLineGetTypographicBounds(d->line, NULL, NULL, NULL)), d->descent)); return rect; } int CoreTextNativeFont::nativeFontWidth(String const &text) const { d->makeLine(d->applyTransformation(text)); return roundi(CTLineGetTypographicBounds(d->line, NULL, NULL, NULL)); } QImage CoreTextNativeFont::nativeFontRasterize(String const &text, Vector4ub const &foreground, Vector4ub const &background) const { #if 0 DENG2_ASSERT(fequal(fontCache.fontSize(d->font), size())); DENG2_ASSERT(fontCache.fontWeight(d->font) == weight()); #endif // Text color. Vector4d const fg = foreground.zyxw().toVector4f() / 255.f; CGColorRef fgColor = CGColorCreate(fontCache.colorspace(), &fg.x); // Ensure the color is used by recreating the attributed line string. d->releaseLine(); d->makeLine(d->applyTransformation(text), fgColor); // Set up the bitmap for drawing into. Rectanglei const bounds = measure(d->lineText); QImage backbuffer(QSize(bounds.width(), bounds.height()), QImage::Format_ARGB32);//_Premultiplied); backbuffer.fill(QColor(background.x, background.y, background.z, background.w).rgba()); CGContextRef gc = CGBitmapContextCreate(backbuffer.bits(), backbuffer.width(), backbuffer.height(), 8, 4 * backbuffer.width(), fontCache.colorspace(), kCGImageAlphaPremultipliedLast); CGContextSetTextPosition(gc, 0, d->descent); CTLineDraw(d->line, gc); CGColorRelease(fgColor); CGContextRelease(gc); return backbuffer; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/text/font_richformat.cpp0000664000175000017500000003006512641367671024736 0ustar jaakkojaakko/** @file font_richformat.cpp Rich formatting instructions for text. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/Font" #include namespace de { DENG2_PIMPL_NOREF(Font::RichFormat), DENG2_OBSERVES(EscapeParser, PlainText), DENG2_OBSERVES(EscapeParser, EscapeSequence) { IStyle const *style; struct Format { float sizeFactor; Weight weight; Style style; int colorIndex; bool markIndent; bool resetIndent; int tabStop; Format() : sizeFactor(1.f), weight(OriginalWeight), style(OriginalStyle), colorIndex(-1), markIndent(false), resetIndent(false), tabStop(-1 /* untabbed */) {} }; struct FormatRange { Rangei range; Format format; FormatRange(Rangei const &r = Rangei(), Format const &frm = Format()) : range(r), format(frm) {} }; typedef QList Ranges; Ranges ranges; /// Tab stops are only applicable on the first line of a set of wrapped /// lines. Subsequent lines use the latest accessed tab stop as the indent. TabStops tabs; EscapeParser esc; QList stack; int plainPos; Instance() : style(0) {} Instance(IStyle const &style) : style(&style) {} Instance(Instance const &other) : de::IPrivate() , de::EscapeParser::IPlainTextObserver() , de::EscapeParser::IEscapeSequenceObserver() , style(other.style) , ranges(other.ranges) , tabs(other.tabs) {} void handlePlainText(Rangei const &range) { Rangei plainRange(plainPos, plainPos + range.size()); plainPos += range.size(); // Append a formatted range using the stack's current format. ranges << Instance::FormatRange(plainRange, stack.last()); // Properties that span a single range only. stack.last().markIndent = stack.last().resetIndent = false; } void handleEscapeSequence(Rangei const &range) { // Save the previous format on the stack. stack << Instance::Format(stack.last()); String const code = esc.originalText().substr(range); // Check the escape sequence. char ch = code[0].toLatin1(); switch(ch) { case '(': // Sequence of tab stops effective in the entire content. tabs.clear(); for(int i = 1; i < code.size() - 1; ++i) { tabs << (code[i].toLatin1() - 'a' + 1); } break; case '.': // pop a format off the stack stack.removeLast(); // ignore the one just added if(stack.size() > 1) { Format form = stack.takeLast(); stack.last().tabStop = form.tabStop; // Retain tab stop. stack.last().markIndent = form.markIndent; } break; case '>': stack.last().markIndent = true; handlePlainText(Rangei(0, 0)); break; case '<': stack.last().resetIndent = true; // Insert an empty range for reseting the indent. handlePlainText(Rangei(0, 0)); break; case '\t': stack.last().tabStop++; break; case 'T': stack.last().tabStop = de::max(-1, code[1].toLatin1() - 'a'); break; case 'b': stack.last().weight = Bold; break; case 'l': stack.last().weight = Light; break; case 'w': stack.last().weight = Normal; break; case 'r': stack.last().style = Regular; break; case 'i': stack.last().style = Italic; break; case 'm': stack.last().style = Monospace; break; case 's': stack.last().sizeFactor = .8f; break; case 't': stack.last().sizeFactor = .75f; break; case 'n': stack.last().sizeFactor = .6f; break; case 'A': // Normal color case 'B': // Highlight color case 'C': // Dimmed color case 'D': // Accent color case 'E': // Dim Accent color case 'F': // Alternative Accent color stack.last().colorIndex = ch - 'A'; break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': if(style) { style->richStyleFormat(ch - '0', stack.last().sizeFactor, stack.last().weight, stack.last().style, stack.last().colorIndex); } break; } } }; Font::RichFormat::RichFormat() : d(new RichFormat::Instance) {} Font::RichFormat::RichFormat(IStyle const &style) : d(new RichFormat::Instance(style)) {} Font::RichFormat::RichFormat(RichFormat const &other) : d(new RichFormat::Instance(*other.d)) {} Font::RichFormat &Font::RichFormat::operator = (RichFormat const &other) { d.reset(new RichFormat::Instance(*other.d)); return *this; } void Font::RichFormat::clear() { d->ranges.clear(); d->tabs.clear(); d->stack.clear(); d->stack << Instance::Format(); d->plainPos = 0; } void Font::RichFormat::setStyle(IStyle const &style) { d->style = &style; } bool Font::RichFormat::hasStyle() const { return d->style != 0; } Font::RichFormat::IStyle const &Font::RichFormat::style() const { return *d->style; } Font::RichFormat Font::RichFormat::fromPlainText(String const &plainText) { Instance::FormatRange all; all.range = Rangei(0, plainText.size()); RichFormat form; form.d->ranges << all; return form; } String Font::RichFormat::initFromStyledText(String const &styledText) { clear(); d->esc.audienceForEscapeSequence() += d; d->esc.audienceForPlainText() += d; d->esc.parse(styledText); #if 0 qDebug() << "Styled text:" << styledText; qDebug() << "plain:" << d->esc.plainText(); foreach(Instance::FormatRange const &r, d->ranges) { qDebug() << r.range.asText() << d->esc.plainText().substr(r.range) << "size:" << r.format.sizeFactor << "weight:" << r.format.weight << "style:" << r.format.style << "color:" << r.format.colorIndex << "tab:" << r.format.tabStop << "indent (m/r):" << r.format.markIndent << r.format.resetIndent; } qDebug() << "Tabs:" << d->tabs.size(); foreach(int i, d->tabs) { qDebug() << i; } #endif return d->esc.plainText(); } Font::RichFormatRef Font::RichFormat::subRange(Rangei const &range) const { return RichFormatRef(*this, range); } Font::TabStops const &Font::RichFormat::tabStops() const { return d->tabs; } int Font::RichFormat::tabStopXWidth(int stop) const { if(stop < 0 || d->tabs.isEmpty()) return 0; DENG2_ASSERT(stop < 50); int x = 0; for(int i = 0; i <= stop; ++i) { if(i >= d->tabs.size()) x += d->tabs.last(); else x += d->tabs[i]; } return x; } Font::RichFormat::Ref::Ref(Ref const &ref) : _ref(ref.format()), _span(ref._span), _indices(ref._indices) {} Font::RichFormat::Ref::Ref(Ref const &ref, Rangei const &subSpan) : _ref(ref.format()), _span(subSpan + ref._span.start) { updateIndices(); } Font::RichFormat::Ref::Ref(RichFormat const &richFormat) : _ref(richFormat), _indices(0, richFormat.d->ranges.size()) { if(!richFormat.d->ranges.isEmpty()) { _span = Rangei(0, richFormat.d->ranges.last().range.end); } } Font::RichFormat::Ref::Ref(RichFormat const &richFormat, Rangei const &subSpan) : _ref(richFormat), _span(subSpan) { updateIndices(); } Font::RichFormat const &Font::RichFormat::Ref::format() const { return _ref; } int Font::RichFormat::Ref::rangeCount() const { return _indices.size(); } Rangei Font::RichFormat::Ref::range(int index) const { Rangei r = _ref.d->ranges.at(_indices.start + index).range; if(index == 0) { // Clip the beginning. r.start = de::max(r.start, _span.start); } if(index == rangeCount() - 1) { // Clip the end in the last range. r.end = de::min(r.end, _span.end); } DENG2_ASSERT(r.start >= _span.start); DENG2_ASSERT(r.end <= _span.end); DENG2_ASSERT(r.start <= r.end); // Make sure it's relative to the start of the subspan. return r - _span.start; } Font::RichFormat::Ref Font::RichFormat::Ref::subRef(Rangei const &subSpan) const { return Ref(*this, subSpan); } void Font::RichFormat::Ref::updateIndices() { _indices = Rangei(0, 0); Instance::Ranges const &ranges = format().d->ranges; int i = 0; for(; i < ranges.size(); ++i) { Rangei const &r = ranges.at(i).range; if(r.end > _span.start) { _indices.start = i; _indices.end = i + 1; break; } } for(++i; i < ranges.size(); ++i, ++_indices.end) { // Empty ranges are accepted at the end of the span. Rangei const &r = ranges.at(i).range; if(( r.isEmpty() && r.start > _span.end) || (!r.isEmpty() && r.start >= _span.end)) break; } DENG2_ASSERT(_indices.start <= _indices.end); } Font::RichFormat::Iterator::Iterator(Ref const &f) : format(f), index(-1) {} int Font::RichFormat::Iterator::size() const { return format.rangeCount(); } bool Font::RichFormat::Iterator::hasNext() const { return index + 1 < size(); } void Font::RichFormat::Iterator::next() { index++; DENG2_ASSERT(index < size()); } bool Font::RichFormat::Iterator::isDefault() const { return (fequal(sizeFactor(), 1.f) && weight() == OriginalWeight && style() == OriginalStyle && colorIndex() == OriginalColor); } Rangei Font::RichFormat::Iterator::range() const { return format.range(index); } #define REF_RANGE_AT(Idx) format.format().d->ranges.at(format.rangeIndices().start + Idx) float Font::RichFormat::Iterator::sizeFactor() const { return REF_RANGE_AT(index).format.sizeFactor; } Font::RichFormat::Weight Font::RichFormat::Iterator::weight() const { return REF_RANGE_AT(index).format.weight; } Font::RichFormat::Style Font::RichFormat::Iterator::style() const { return REF_RANGE_AT(index).format.style; } int Font::RichFormat::Iterator::colorIndex() const { return REF_RANGE_AT(index).format.colorIndex; } Font::RichFormat::IStyle::Color Font::RichFormat::Iterator::color() const { if(format.format().d->style) { return format.format().d->style->richStyleColor(colorIndex()); } // Fall back to white. return Vector4ub(255, 255, 255, 255); } bool Font::RichFormat::Iterator::markIndent() const { return REF_RANGE_AT(index).format.markIndent; } bool Font::RichFormat::Iterator::resetIndent() const { return REF_RANGE_AT(index).format.resetIndent; } int Font::RichFormat::Iterator::tabStop() const { return REF_RANGE_AT(index).format.tabStop; } bool Font::RichFormat::Iterator::isTabless() const { return tabStop() < 0; } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/displaymode.cpp0000664000175000017500000002360412641367671023101 0ustar jaakkojaakko/** @file displaymode.cpp Platform-independent display mode management. * @ingroup gl * * @authors Copyright (c) 2012-2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/gui/displaymode.h" #include "de/gui/displaymode_native.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace de { static bool inited = false; static DisplayColorTransfer originalColorTransfer; static de::Binder binder; static float differenceToOriginalHz(float hz); namespace internal { struct Mode : public DisplayMode { Mode() { de::zapPtr(static_cast(this)); } Mode(const DisplayMode& dm) { memcpy(static_cast(this), &dm, sizeof(dm)); } Mode(int i) { DisplayMode_Native_GetMode(i, this); updateRatio(); } static Mode fromCurrent() { Mode m; DisplayMode_Native_GetCurrentMode(&m); m.updateRatio(); return m; } bool operator == (const Mode& other) const { return width == other.width && height == other.height && depth == other.depth && refreshRate == other.refreshRate; } bool operator != (const Mode& other) const { return !(*this == other); } bool operator < (const Mode& b) const { if(width == b.width) { if(height == b.height) { if(depth == b.depth) { // The refresh rate that more closely matches the original is preferable. return differenceToOriginalHz(refreshRate) < differenceToOriginalHz(b.refreshRate); } return depth < b.depth; } return height < b.height; } return width < b.width; } void updateRatio() { ratioX = width; ratioY = height; float fx; float fy; if(width > height) { fx = width/float(height); fy = 1.f; } else { fx = 1.f; fy = height/float(width); } // Multiply until we arrive at a close enough integer ratio. for(int mul = 2; mul < qMin(width, height); ++mul) { float rx = fx*mul; float ry = fy*mul; if(qAbs(rx - qRound(rx)) < .01f && qAbs(ry - qRound(ry)) < .01f) { // This seems good. ratioX = qRound(rx); ratioY = qRound(ry); break; } } if(ratioX == 8 && ratioY == 5) { // This is commonly referred to as 16:10. ratioX *= 2; ratioY *= 2; } } void debugPrint() const { LOG_GL_VERBOSE("size: %i x %i x %i, rate: %.1f Hz, ratio: %i:%i") << width << height << depth << refreshRate << ratioX << ratioY; } }; } // namespace internal using namespace internal; typedef std::set Modes; // note: no duplicates static Modes modes; static Mode originalMode; static bool captured; static float differenceToOriginalHz(float hz) { return qAbs(hz - originalMode.refreshRate); } static de::Value *Function_DisplayMode_OriginalMode(de::Context &, de::Function::ArgumentValues const &) { using de::NumberValue; using de::TextValue; DisplayMode const *mode = DisplayMode_OriginalMode(); de::DictionaryValue *dict = new de::DictionaryValue; dict->add(new TextValue("width"), new NumberValue(mode->width)); dict->add(new TextValue("height"), new NumberValue(mode->height)); dict->add(new TextValue("depth"), new NumberValue(mode->depth)); dict->add(new TextValue("refreshRate"), new NumberValue(mode->refreshRate)); de::ArrayValue *ratio = new de::ArrayValue; *ratio << NumberValue(mode->ratioX) << NumberValue(mode->ratioY); dict->add(new TextValue("ratio"), ratio); return dict; } } // namespace de using namespace de; int DisplayMode_Init(void) { if(inited) return true; captured = false; DisplayMode_Native_Init(); #if defined(MACOSX) || defined(UNIX) DisplayMode_SaveOriginalColorTransfer(); #endif // This is used for sorting the mode set (Hz). originalMode = Mode::fromCurrent(); for(int i = 0; i < DisplayMode_Native_Count(); ++i) { Mode mode(i); if(mode.depth < 16 || mode.width < 320 || mode.height < 240) continue; // This mode is not good. modes.insert(mode); } LOG_GL_VERBOSE("Current mode is:"); originalMode.debugPrint(); LOG_GL_VERBOSE("All available modes:"); for(Modes::iterator i = modes.begin(); i != modes.end(); ++i) { i->debugPrint(); } // Script bindings. binder.initNew() << DENG2_FUNC_NOARG(DisplayMode_OriginalMode, "originalMode"); de::App::scriptSystem().addNativeModule("DisplayMode", binder.module()); binder.module().addNumber("DPI_FACTOR", 1.0); inited = true; return true; } void DisplayMode_Shutdown(void) { if(!inited) return; binder.deinit(); LOG_GL_NOTE("Restoring original display mode due to shutdown"); // Back to the original mode. DisplayMode_Change(&originalMode, false /*release captured*/); modes.clear(); DisplayMode_Native_Shutdown(); captured = false; DisplayMode_Native_SetColorTransfer(&originalColorTransfer); inited = false; } void DisplayMode_SaveOriginalColorTransfer(void) { DisplayMode_Native_GetColorTransfer(&originalColorTransfer); } DisplayMode const *DisplayMode_OriginalMode(void) { return &originalMode; } DisplayMode const *DisplayMode_Current(void) { static Mode currentMode; // Update it with current mode. currentMode = Mode::fromCurrent(); return ¤tMode; } int DisplayMode_Count(void) { return (int) modes.size(); } DisplayMode const *DisplayMode_ByIndex(int index) { DENG2_ASSERT(index >= 0); DENG2_ASSERT(index < (int) modes.size()); int pos = 0; for(Modes::iterator i = modes.begin(); i != modes.end(); ++i, ++pos) { if(pos == index) { return &*i; } } DENG2_ASSERT(false); return 0; // unreachable } DisplayMode const *DisplayMode_FindClosest(int width, int height, int depth, float freq) { int bestScore = -1; DisplayMode const *best = 0; for(Modes::iterator i = modes.begin(); i != modes.end(); ++i) { int score = de::squared(i->width - width) + de::squared(i->height - height) + de::squared(i->depth - depth); if(freq >= 1) { score += de::squared(i->refreshRate - freq); } // Note: The first mode to hit the lowest score wins; if there are many modes // with the same score, the first one will be chosen. Particularly if the // frequency has not been specified, the sort order of the modes defines which // one is picked. if(!best || score < bestScore) { bestScore = score; best = &*i; } } return best; } int DisplayMode_IsEqual(DisplayMode const *a, DisplayMode const *b) { if(!a || !b) return true; // Cannot compare against nothing. return Mode(*a) == Mode(*b); } int DisplayMode_Change(DisplayMode const *mode, int shouldCapture) { if(Mode::fromCurrent() == *mode && !shouldCapture == !captured) { LOG_AS("DisplayMode"); LOGDEV_GL_XVERBOSE("Requested mode is the same as current, ignoring request"); // Already in this mode. return false; } captured = shouldCapture; return DisplayMode_Native_Change(mode, shouldCapture || (originalMode != *mode)); } static inline de::duint16 intensity8To16(de::duint8 b) { return (b << 8) | b; // 0xFF => 0xFFFF } void DisplayMode_GetColorTransfer(DisplayColorTransfer *colors) { DisplayColorTransfer mapped; DisplayMode_Native_GetColorTransfer(&mapped); // Factor out the original color transfer function, which may be set up // specifically by the user. for(int i = 0; i < 256; ++i) { #define LINEAR_UNMAP(i, c) ( (unsigned short) \ de::clamp(0.f, float(mapped.table[i]) / float(originalColorTransfer.table[i]) * intensity8To16(c), 65535.f) ) colors->table[i] = LINEAR_UNMAP(i, i); colors->table[i + 256] = LINEAR_UNMAP(i + 256, i); colors->table[i + 512] = LINEAR_UNMAP(i + 512, i); } } void DisplayMode_SetColorTransfer(DisplayColorTransfer const *colors) { DisplayColorTransfer mapped; // Factor in the original color transfer function, which may be set up // specifically by the user. for(int i = 0; i < 256; ++i) { #define LINEAR_MAP(i, c) ( (unsigned short) \ de::clamp(0.f, float(colors->table[i]) / float(intensity8To16(c)) * originalColorTransfer.table[i], 65535.f) ) mapped.table[i] = LINEAR_MAP(i, i); mapped.table[i + 256] = LINEAR_MAP(i + 256, i); mapped.table[i + 512] = LINEAR_MAP(i + 512, i); } DisplayMode_Native_SetColorTransfer(&mapped); } doomsday-stable-1.15.7/doomsday/libgui/src/displaymode_windows.cpp0000664000175000017500000000736312641367671024657 0ustar jaakkojaakko/** @file displaymode_windows.cpp Windows implementation of the DisplayMode native functionality. * @ingroup gl * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include #define WIN32_LEAN_AND_MEAN #include #include #include #include #include #include "de/gui/displaymode_native.h" #include "de/PersistentCanvasWindow" static std::vector devModes; static DEVMODE currentDevMode; static DisplayMode devToDisplayMode(const DEVMODE& d) { DisplayMode m; m.width = d.dmPelsWidth; m.height = d.dmPelsHeight; m.depth = d.dmBitsPerPel; m.refreshRate = d.dmDisplayFrequency; return m; } void DisplayMode_Native_Init(void) { // Let's see which modes are available. for(int i = 0; ; i++) { DEVMODE mode; de::zap(mode); mode.dmSize = sizeof(mode); if(!EnumDisplaySettings(NULL, i, &mode)) break; // That's all. devModes.push_back(mode); } // And which is the current mode? de::zap(currentDevMode); currentDevMode.dmSize = sizeof(currentDevMode); EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, ¤tDevMode); } void DisplayMode_Native_Shutdown(void) { devModes.clear(); } int DisplayMode_Native_Count(void) { return devModes.size(); } void DisplayMode_Native_GetMode(int index, DisplayMode* mode) { DENG2_ASSERT(index >= 0 && index < DisplayMode_Native_Count()); *mode = devToDisplayMode(devModes[index]); } void DisplayMode_Native_GetCurrentMode(DisplayMode* mode) { *mode = devToDisplayMode(currentDevMode); } static int findMode(const DisplayMode* mode) { for(int i = 0; i < DisplayMode_Native_Count(); ++i) { DisplayMode d = devToDisplayMode(devModes[i]); if(DisplayMode_IsEqual(&d, mode)) { return i; } } return -1; } int DisplayMode_Native_Change(const DisplayMode* mode, int shouldCapture) { DENG2_ASSERT(mode); DENG2_ASSERT(findMode(mode) >= 0); DEVMODE m = devModes[findMode(mode)]; m.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL | DM_DISPLAYFREQUENCY; if(ChangeDisplaySettings(&m, shouldCapture? CDS_FULLSCREEN : 0) != DISP_CHANGE_SUCCESSFUL) return false; currentDevMode = m; return true; } void DisplayMode_Native_SetColorTransfer(DisplayColorTransfer const *colors) { if(!de::CanvasWindow::mainExists()) return; HWND hWnd = (HWND) de::CanvasWindow::main().nativeHandle(); DENG2_ASSERT(hWnd != 0); HDC hDC = GetDC(hWnd); if(hDC) { SetDeviceGammaRamp(hDC, (void*) colors->table); ReleaseDC(hWnd, hDC); } } void DisplayMode_Native_GetColorTransfer(DisplayColorTransfer *colors) { HWND hWnd = (HWND) de::CanvasWindow::main().nativeHandle(); DENG2_ASSERT(hWnd != 0); HDC hDC = GetDC(hWnd); if(hDC) { GetDeviceGammaRamp(hDC, (void *) colors->table); ReleaseDC(hWnd, hDC); } } doomsday-stable-1.15.7/doomsday/libgui/src/guiapp.cpp0000664000175000017500000000647212641367671022060 0ustar jaakkojaakko/** @file guiapp.cpp Application with GUI support. * * @authors Copyright © 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de/GuiApp" #include "de/graphics/opengl.h" #include #ifdef DENG2_QT_5_0_OR_NEWER # include #else # include #endif namespace de { DENG2_PIMPL(GuiApp) { Loop loop; Instance(Public *i) : Base(i) { loop.audienceForIteration() += self; } DENG2_PIMPL_AUDIENCE(GLContextChange) }; DENG2_AUDIENCE_METHOD(GuiApp, GLContextChange) GuiApp::GuiApp(int &argc, char **argv) : QApplication(argc, argv), App(applicationFilePath(), arguments()), d(new Instance(this)) { // Core packages for GUI functionality. addInitPackage("net.dengine.stdlib.gui"); } void GuiApp::setMetadata(String const &orgName, String const &orgDomain, String const &appName, String const &appVersion) { setName(appName); // Qt metadata. setOrganizationName (orgName); setOrganizationDomain(orgDomain); setApplicationName (appName); setApplicationVersion(appVersion); } bool GuiApp::notify(QObject *receiver, QEvent *event) { try { return QApplication::notify(receiver, event); } catch(std::exception const &error) { handleUncaughtException(error.what()); } catch(...) { handleUncaughtException("de::GuiApp caught exception of unknown type."); } return false; } void GuiApp::notifyDisplayModeChanged() { emit displayModeChanged(); } void GuiApp::notifyGLContextChanged() { qDebug() << "notifying GL context change" << audienceForGLContextChange().size(); DENG2_FOR_AUDIENCE2(GLContextChange, i) i->appGLContextChanged(); } int GuiApp::execLoop() { LOGDEV_NOTE("Starting GuiApp event loop..."); d->loop.start(); int code = QApplication::exec(); LOGDEV_NOTE("GuiApp event loop exited with code %i") << code; return code; } void GuiApp::stopLoop(int code) { LOGDEV_MSG("Stopping GuiApp event loop"); d->loop.stop(); return QApplication::exit(code); } Loop &GuiApp::loop() { return d->loop; } void GuiApp::loopIteration() { // Update the clock time. de::App listens to this clock and will inform // subsystems in the order they've been added. Clock::get().setTime(Time::currentHighPerformanceTime()); } NativePath GuiApp::appDataPath() const { #ifdef DENG2_QT_5_0_OR_NEWER return QStandardPaths::writableLocation(QStandardPaths::DataLocation); #else return QDesktopServices::storageLocation(QDesktopServices::DataLocation); #endif } } // namespace de doomsday-stable-1.15.7/doomsday/libgui/src/precompiled.h0000664000175000017500000000340512641367671022534 0ustar jaakkojaakko/** @file precompiled.h Precompiled headers for libgui. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifdef WIN32 # define WIN32_LEAN_AND_MEAN # include # undef min # undef max #endif #ifdef __cplusplus // C++ standard library: #include #include #include #include #include #include #include #include #include #include #include #include #include // Qt: #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #endif doomsday-stable-1.15.7/doomsday/libgui/src/displaymode_x11.cpp0000664000175000017500000001752312641367671023575 0ustar jaakkojaakko/** @file displaymode_x11.cpp X11 implementation of the DisplayMode native functionality. * @ingroup gl * * Uses the XRandR extension to manipulate the display. * * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include #include #include #include #include #include #include "de/gui/displaymode_native.h" #include #include #include namespace de { typedef std::vector DisplayModes; static int displayDepth; static Rotation displayRotation; static DisplayModes availableModes; static DisplayMode currentMode; namespace internal { /** * Wrapper for the Xrandr configuration info. The config is kept in memory only * for the lifetime of an RRInfo instance. */ class RRInfo { public: /** * Queries all the available modes in the display configuration. */ RRInfo() : _numSizes(0) { _conf = XRRGetScreenInfo(QX11Info::display(), QX11Info::appRootWindow()); if(!_conf) return; // Not available. // Let's see which modes are available. _sizes = XRRConfigSizes(_conf, &_numSizes); for(int i = 0; i < _numSizes; ++i) { int numRates = 0; short* rates = XRRConfigRates(_conf, i, &numRates); for(int k = 0; k < numRates; k++) { DisplayMode mode; de::zap(mode); mode.width = _sizes[i].width; mode.height = _sizes[i].height; mode.depth = displayDepth; mode.refreshRate = rates[k]; _modes.push_back(mode); } } ::Time prevConfTime; _confTime = XRRConfigTimes(_conf, &prevConfTime); } ~RRInfo() { if(_conf) XRRFreeScreenConfigInfo(_conf); } /** * Returns the currently active mode as specified in the Xrandr config. * Also determines the display's current rotation angle. */ DisplayMode currentMode() const { DisplayMode mode; de::zap(mode); if(!_conf) return mode; SizeID currentSize = XRRConfigCurrentConfiguration(_conf, &displayRotation); // Update the current mode. mode.width = _sizes[currentSize].width; mode.height = _sizes[currentSize].height; mode.depth = displayDepth; mode.refreshRate = XRRConfigCurrentRate(_conf); return mode; } std::vector& modes() { return _modes; } static short rateFromMode(const DisplayMode* mode) { return short(qRound(mode->refreshRate)); } int find(const DisplayMode* mode) const { for(int i = 0; i < _numSizes; ++i) { int numRates = 0; short* rates = XRRConfigRates(_conf, i, &numRates); for(int k = 0; k < numRates; k++) { if(rateFromMode(mode) == rates[k] && mode->width == _sizes[i].width && mode->height == _sizes[i].height) { // This is the one. return i; } } } return -1; } bool apply(const DisplayMode* mode) { if(!_conf) return false; int sizeIdx = find(mode); assert(sizeIdx >= 0); //qDebug() << "calling XRRSetScreenConfig" << _confTime; Status result = XRRSetScreenConfigAndRate(QX11Info::display(), _conf, QX11Info::appRootWindow(), sizeIdx, displayRotation, rateFromMode(mode), _confTime); //qDebug() << "result" << result; if(result == BadValue) { qDebug() << "Failed to apply screen config and rate with Xrandr"; return false; } // Update the current mode. de::currentMode = *mode; return true; } private: XRRScreenConfiguration* _conf; XRRScreenSize* _sizes; ::Time _confTime; int _numSizes; DisplayModes _modes; }; } // namespace internal } // namespace de using namespace de; using namespace de::internal; void DisplayMode_Native_Init(void) { // We will not be changing the depth at runtime. #ifdef DENG2_QT_5_0_OR_NEWER displayDepth = qApp->screens().at(0)->depth(); #else displayDepth = QX11Info::appDepth(); #endif RRInfo info; availableModes = info.modes(); currentMode = info.currentMode(); } void DisplayMode_Native_Shutdown(void) { availableModes.clear(); } int DisplayMode_Native_Count(void) { return availableModes.size(); } void DisplayMode_Native_GetMode(int index, DisplayMode* mode) { assert(index >= 0 && index < DisplayMode_Native_Count()); *mode = availableModes[index]; } void DisplayMode_Native_GetCurrentMode(DisplayMode* mode) { *mode = currentMode; } #if 0 static int findMode(const DisplayMode* mode) { for(int i = 0; i < DisplayMode_Native_Count(); ++i) { DisplayMode d = devToDisplayMode(devModes[i]); if(DisplayMode_IsEqual(&d, mode)) { return i; } } return -1; } #endif int DisplayMode_Native_Change(DisplayMode const *mode, int shouldCapture) { DENG2_UNUSED(shouldCapture); return RRInfo().apply(mode); } void DisplayMode_Native_GetColorTransfer(DisplayColorTransfer *colors) { Display *dpy = QX11Info::display(); int screen = QX11Info::appScreen(); int event = 0, error = 0; LOG_AS("GetColorTransfer"); if(!dpy || !XF86VidModeQueryExtension(dpy, &event, &error)) { LOG_GL_WARNING("XFree86-VidModeExtension not available."); return; } LOGDEV_GL_XVERBOSE("event# %i error# %i") << event << error; // Ramp size. int rampSize = 0; XF86VidModeGetGammaRampSize(dpy, screen, &rampSize); LOGDEV_GL_VERBOSE("Gamma ramp size: %i") << rampSize; if(!rampSize) return; ushort* xRamp = new ushort[3 * rampSize]; // Get the current ramps. XF86VidModeGetGammaRamp(dpy, screen, rampSize, xRamp, xRamp + rampSize, xRamp + 2*rampSize); for(uint i = 0; i < 256; ++i) { const uint tx = qMin(uint(rampSize - 1), i * rampSize / 255); colors->table[i] = xRamp[tx]; colors->table[i + 256] = xRamp[tx + rampSize]; colors->table[i + 512] = xRamp[tx + 2*rampSize]; } delete [] xRamp; } void DisplayMode_Native_SetColorTransfer(DisplayColorTransfer const *colors) { Display* dpy = QX11Info::display(); if(!dpy) return; // Ramp size. int rampSize = 0; XF86VidModeGetGammaRampSize(dpy, QX11Info::appScreen(), &rampSize); if(!rampSize) return; ushort* xRamp = new ushort[3 * rampSize]; for(int i = 0; i < rampSize; ++i) { const uint tx = qMin(255, i * 256 / (rampSize - 1)); xRamp[i] = colors->table[tx]; xRamp[i + rampSize] = colors->table[tx + 256]; xRamp[i + 2*rampSize] = colors->table[tx + 512]; } XF86VidModeSetGammaRamp(dpy, QX11Info::appScreen(), rampSize, xRamp, xRamp + rampSize, xRamp + 2*rampSize); delete [] xRamp; } doomsday-stable-1.15.7/doomsday/libgui/src/displaymode_macx.mm0000664000175000017500000001663212641367671023743 0ustar jaakkojaakko/** * @file displaymode_macx.mm * Mac OS X implementation of the DisplayMode class. @ingroup gl * * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include #include #include #include //#include "de/libcore.h" #include "de/gui/displaymode_native.h" /// Returns -1 on error. static int intFromDict(CFDictionaryRef dict, CFStringRef key) { CFNumberRef ref = (CFNumberRef) CFDictionaryGetValue(dict, key); if(!ref) return -1; int value; if(!CFNumberGetValue(ref, kCFNumberIntType, &value)) return -1; return value; } /// Returns -1 on error. static float floatFromDict(CFDictionaryRef dict, CFStringRef key) { CFNumberRef ref = (CFNumberRef) CFDictionaryGetValue(dict, key); if(!ref) return -1; float value; if(!CFNumberGetValue(ref, kCFNumberFloatType, &value)) return -1; return value; } static DisplayMode modeFromDict(CFDictionaryRef dict) { DisplayMode m; m.width = intFromDict(dict, kCGDisplayWidth); m.height = intFromDict(dict, kCGDisplayHeight); m.refreshRate = floatFromDict(dict, kCGDisplayRefreshRate); m.depth = intFromDict(dict, kCGDisplayBitsPerPixel); return m; } static std::vector displayModes; static std::vector displayDicts; //static CFDictionaryRef currentDisplayDict; static CFDictionaryRef getCurrentDisplayDict(void) { return (CFDictionaryRef) CGDisplayCurrentMode(kCGDirectMainDisplay); } static void updateDisplayDicts(void) { displayDicts.clear(); CFArrayRef modes = CGDisplayAvailableModes(kCGDirectMainDisplay); CFIndex count = CFArrayGetCount(modes); for(CFIndex i = 0; i < count; ++i) { displayDicts.push_back((CFDictionaryRef) CFArrayGetValueAtIndex(modes, i)); } assert(displayModes.size() == displayDicts.size()); } void DisplayMode_Native_Init(void) { // Let's see which modes are available. CFArrayRef modes = CGDisplayAvailableModes(kCGDirectMainDisplay); CFIndex count = CFArrayGetCount(modes); for(CFIndex i = 0; i < count; ++i) { CFDictionaryRef dict = (CFDictionaryRef) CFArrayGetValueAtIndex(modes, i); displayModes.push_back(modeFromDict(dict)); displayDicts.push_back(dict); } //currentDisplayDict = (CFDictionaryRef) CGDisplayCurrentMode(kCGDirectMainDisplay); } static bool captureDisplays(int capture) { if(capture && !CGDisplayIsCaptured(kCGDirectMainDisplay)) { return CGCaptureAllDisplays() == kCGErrorSuccess; } else if(!capture && CGDisplayIsCaptured(kCGDirectMainDisplay)) { CGReleaseAllDisplays(); return true; } return true; } static void releaseDisplays() { captureDisplays(false); } void DisplayMode_Native_Shutdown(void) { displayModes.clear(); releaseDisplays(); } int DisplayMode_Native_Count(void) { return displayModes.size(); } void DisplayMode_Native_GetMode(int index, DisplayMode *mode) { assert(index >= 0 && index < (int)displayModes.size()); *mode = displayModes[index]; } void DisplayMode_Native_GetCurrentMode(DisplayMode *mode) { *mode = modeFromDict(getCurrentDisplayDict()); } static int findIndex(DisplayMode const *mode) { for(unsigned int i = 0; i < displayModes.size(); ++i) { if(displayModes[i].width == mode->width && displayModes[i].height == mode->height && displayModes[i].depth == mode->depth && displayModes[i].refreshRate == mode->refreshRate) { return i; } } return -1; // Invalid mode. } int DisplayMode_Native_Change(DisplayMode const *mode, int shouldCapture) { const CGDisplayFadeInterval fadeTime = .5f; updateDisplayDicts(); assert(mode); assert(findIndex(mode) >= 0); // mode must be an enumerated one // Fade all displays to black (blocks until faded). CGDisplayFadeReservationToken token; CGAcquireDisplayFadeReservation(kCGMaxDisplayReservationInterval, &token); CGDisplayFade(token, fadeTime, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0, 0, 0, true /* wait */); // Capture the displays now if haven't yet done so. bool wasPreviouslyCaptured = CGDisplayIsCaptured(kCGDirectMainDisplay); CGDisplayErr result = kCGErrorSuccess; CFDictionaryRef newModeDict = displayDicts[findIndex(mode)]; // Capture displays if instructed to do so. if(shouldCapture && !captureDisplays(true)) { result = kCGErrorFailure; } if(result == kCGErrorSuccess && getCurrentDisplayDict() != newModeDict) { // Try to change. result = CGDisplaySwitchToMode(kCGDirectMainDisplay, newModeDict); if(result != kCGErrorSuccess) { // Oh no! //CGDisplaySwitchToMode(kCGDirectMainDisplay, currentDisplayDict); if(!wasPreviouslyCaptured) releaseDisplays(); } /* else { currentDisplayDict = displayDicts[findIndex(mode)]; } */ } // Fade back to normal. CGDisplayFade(token, 2*fadeTime, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0, 0, 0, false); CGReleaseDisplayFadeReservation(token); // Release display capture if instructed to do so. if(!shouldCapture) { captureDisplays(false); } return result == kCGErrorSuccess; } void DisplayMode_Native_Raise(void *nativeHandle) { assert(nativeHandle); // Qt uses the Cocoa framework. NSView* handle = (NSView*) nativeHandle; NSWindow* wnd = [handle window]; [wnd setLevel:CGShieldingWindowLevel()]; } void DisplayMode_Native_GetColorTransfer(DisplayColorTransfer *colors) { const uint size = 256; CGGammaValue red[size]; CGGammaValue green[size]; CGGammaValue blue[size]; uint32_t count = 0; CGGetDisplayTransferByTable(kCGDirectMainDisplay, size, red, green, blue, &count); assert(count == size); for(uint i = 0; i < size; ++i) { colors->table[i] = (unsigned short) (red[i] * 0xffff); colors->table[i + size] = (unsigned short) (green[i] * 0xffff); colors->table[i + 2*size] = (unsigned short) (blue[i] * 0xffff); } } void DisplayMode_Native_SetColorTransfer(DisplayColorTransfer const *colors) { const uint size = 256; CGGammaValue red[size]; CGGammaValue green[size]; CGGammaValue blue[size]; for(uint i = 0; i < size; ++i) { red[i] = colors->table[i]/float(0xffff); green[i] = colors->table[i + size]/float(0xffff); blue[i] = colors->table[i + 2*size]/float(0xffff); } CGSetDisplayTransferByTable(kCGDirectMainDisplay, size, red, green, blue); } doomsday-stable-1.15.7/doomsday/server/0000775000175000017500000000000012641367671017322 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/server/server.pro0000664000175000017500000003326412641367671021362 0ustar jaakkojaakko# The Doomsday Engine Project # Copyright (c) 2012-2014 Jaakko Keränen # Copyright (c) 2012-2014 Daniel Swanson TEMPLATE = app TARGET = doomsday-server # Build Configuration ---------------------------------------------------------- include(../config.pri) VERSION = $$DENG_VERSION echo(Doomsday Server $${DENG_VERSION}.) CONFIG -= app_bundle # Some messy old code here: *-g++* | *-gcc* | *-clang* { QMAKE_CXXFLAGS_WARN_ON += \ -Wno-missing-field-initializers \ -Wno-unused-parameter \ -Wno-missing-braces } # External Dependencies -------------------------------------------------------- include(../dep_zlib.pri) include(../dep_lzss.pri) include(../dep_core.pri) include(../dep_legacy.pri) include(../dep_shell.pri) include(../dep_doomsday.pri) # Definitions ------------------------------------------------------------------ DEFINES += __DOOMSDAY__ __SERVER__ !isEmpty(DENG_BUILD) { !win32: echo(Build number: $$DENG_BUILD) DEFINES += DOOMSDAY_BUILD_TEXT=\\\"$$DENG_BUILD\\\" } else { !win32: echo(DENG_BUILD is not defined.) } !win32:!macx { # Generic Unix. DEFINES += __USE_BSD _GNU_SOURCE=1 } # Linking ---------------------------------------------------------------------- win32 { RC_FILE = res/windows/doomsday.rc OTHER_FILES += $$RC_FILE deng_msvc: QMAKE_LFLAGS += /NODEFAULTLIB:libcmt LIBS += -lkernel32 -lgdi32 -lole32 -luser32 -lwsock32 -lopengl32 -lglu32 } else:macx { } else { # Generic Unix. LIBS += -lX11 # TODO: Get rid of this! -jk !freebsd-*: LIBS += -ldl } # Source Files ----------------------------------------------------------------- # Prefix for source files (shared for now): SRC = ../client PRECOMPILED_HEADER = include/precompiled.h HEADERS += \ include/precompiled.h DENG_API_HEADERS = \ $$DENG_API_DIR/apis.h \ $$DENG_API_DIR/api_audiod.h \ $$DENG_API_DIR/api_audiod_mus.h \ $$DENG_API_DIR/api_audiod_sfx.h \ $$DENG_API_DIR/api_base.h \ $$DENG_API_DIR/api_busy.h \ $$DENG_API_DIR/api_console.h \ $$DENG_API_DIR/api_def.h \ $$DENG_API_DIR/api_event.h \ $$DENG_API_DIR/api_gl.h \ $$DENG_API_DIR/api_infine.h \ $$DENG_API_DIR/api_internaldata.h \ $$DENG_API_DIR/api_filesys.h \ $$DENG_API_DIR/api_fontrender.h \ $$DENG_API_DIR/api_gameexport.h \ $$DENG_API_DIR/api_material.h \ $$DENG_API_DIR/api_materialarchive.h \ $$DENG_API_DIR/api_map.h \ $$DENG_API_DIR/api_mapedit.h \ $$DENG_API_DIR/api_player.h \ $$DENG_API_DIR/api_plugin.h \ $$DENG_API_DIR/api_render.h \ $$DENG_API_DIR/api_resource.h \ $$DENG_API_DIR/api_resourceclass.h \ $$DENG_API_DIR/api_server.h \ $$DENG_API_DIR/api_sound.h \ $$DENG_API_DIR/api_svg.h \ $$DENG_API_DIR/api_thinker.h \ $$DENG_API_DIR/api_uri.h \ $$DENG_API_DIR/dd_share.h \ $$DENG_API_DIR/dd_types.h \ $$DENG_API_DIR/dd_version.h \ $$DENG_API_DIR/def_share.h \ $$DENG_API_DIR/dengproject.h \ $$DENG_API_DIR/doomsday.h \ $$DENG_API_DIR/xgclass.h # Convenience headers. DENG_HEADERS += \ $$SRC/include/EntityDatabase \ $$SRC/include/Game \ $$SRC/include/Games \ $$SRC/include/Interceptor # Private headers. DENG_HEADERS += \ include/remoteuser.h \ include/server_dummies.h \ include/shelluser.h \ include/shellusers.h \ include/serverapp.h \ include/serversystem.h \ include/server/sv_def.h \ include/server/sv_frame.h \ include/server/sv_infine.h \ include/server/sv_missile.h \ include/server/sv_pool.h \ include/server/sv_sound.h \ $$SRC/include/audio/s_cache.h \ $$SRC/include/audio/s_environ.h \ $$SRC/include/audio/s_main.h \ $$SRC/include/busymode.h \ $$SRC/include/color.h \ $$SRC/include/con_config.h \ $$SRC/include/dd_def.h \ $$SRC/include/games.h \ $$SRC/include/dd_loop.h \ $$SRC/include/dd_main.h \ $$SRC/include/dd_pinit.h \ $$SRC/include/de_audio.h \ $$SRC/include/de_base.h \ $$SRC/include/de_console.h \ $$SRC/include/de_defs.h \ $$SRC/include/de_edit.h \ $$SRC/include/de_filesys.h \ $$SRC/include/de_graphics.h \ $$SRC/include/de_infine.h \ $$SRC/include/de_misc.h \ $$SRC/include/de_network.h \ $$SRC/include/de_platform.h \ $$SRC/include/de_play.h \ $$SRC/include/de_resource.h \ $$SRC/include/de_render.h \ $$SRC/include/de_system.h \ $$SRC/include/de_ui.h \ $$SRC/include/def_main.h \ $$SRC/include/edit_map.h \ $$SRC/include/face.h \ $$SRC/include/game.h \ $$SRC/include/hedge.h \ $$SRC/include/library.h \ $$SRC/include/m_decomp64.h \ $$SRC/include/m_misc.h \ $$SRC/include/m_nodepile.h \ $$SRC/include/m_profiler.h \ $$SRC/include/mesh.h \ $$SRC/include/network/masterserver.h \ $$SRC/include/network/monitor.h \ $$SRC/include/network/net_buf.h \ $$SRC/include/network/net_event.h \ $$SRC/include/network/net_main.h \ $$SRC/include/network/net_msg.h \ $$SRC/include/partition.h \ $$SRC/include/r_util.h \ $$SRC/include/resource/animgroup.h \ $$SRC/include/resource/colorpalette.h \ $$SRC/include/resource/compositetexture.h \ $$SRC/include/resource/image.h \ $$SRC/include/resource/manifest.h \ $$SRC/include/resource/mapdef.h \ $$SRC/include/resource/material.h \ $$SRC/include/resource/materialarchive.h \ $$SRC/include/resource/materialdetaillayer.h \ $$SRC/include/resource/materialmanifest.h \ $$SRC/include/resource/materialscheme.h \ $$SRC/include/resource/materialshinelayer.h \ $$SRC/include/resource/materialtexturelayer.h \ $$SRC/include/resource/patch.h \ $$SRC/include/resource/patchname.h \ $$SRC/include/resource/rawtexture.h \ $$SRC/include/resource/resourcesystem.h \ $$SRC/include/resource/sprite.h \ $$SRC/include/resource/texture.h \ $$SRC/include/resource/texturemanifest.h \ $$SRC/include/resource/texturescheme.h \ $$SRC/include/sys_system.h \ $$SRC/include/tab_anorms.h \ $$SRC/include/ui/busyvisual.h \ $$SRC/include/ui/infine/finale.h \ $$SRC/include/ui/infine/finaleanimwidget.h \ $$SRC/include/ui/infine/finaleinterpreter.h \ $$SRC/include/ui/infine/finalepagewidget.h \ $$SRC/include/ui/infine/finaletextwidget.h \ $$SRC/include/ui/infine/finalewidget.h \ $$SRC/include/world/dmuargs.h \ $$SRC/include/world/blockmap.h \ $$SRC/include/world/bsp/convexsubspaceproxy.h \ $$SRC/include/world/bsp/edgetip.h \ $$SRC/include/world/bsp/hplane.h \ $$SRC/include/world/bsp/linesegment.h \ $$SRC/include/world/bsp/partitioner.h \ $$SRC/include/world/bsp/partitionevaluator.h \ $$SRC/include/world/bsp/superblockmap.h \ $$SRC/include/world/bspleaf.h \ $$SRC/include/world/bspnode.h \ $$SRC/include/world/convexsubspace.h \ $$SRC/include/world/entitydatabase.h \ $$SRC/include/world/entitydef.h \ $$SRC/include/world/impulseaccumulator.h \ $$SRC/include/world/interceptor.h \ $$SRC/include/world/line.h \ $$SRC/include/world/lineblockmap.h \ $$SRC/include/world/lineowner.h \ $$SRC/include/world/linesighttest.h \ $$SRC/include/world/map.h \ $$SRC/include/world/maputil.h \ $$SRC/include/world/p_object.h \ $$SRC/include/world/p_players.h \ $$SRC/include/world/p_ticker.h \ $$SRC/include/world/plane.h \ $$SRC/include/world/polyobj.h \ $$SRC/include/world/polyobjdata.h \ $$SRC/include/world/propertyvalue.h \ $$SRC/include/world/reject.h \ $$SRC/include/world/sector.h \ $$SRC/include/world/sky.h \ $$SRC/include/world/surface.h \ $$SRC/include/world/thinkers.h \ $$SRC/include/world/vertex.h \ $$SRC/include/world/worldsystem.h \ INCLUDEPATH += \ include \ $$DENG_INCLUDE_DIR \ $$DENG_API_DIR HEADERS += \ $$DENG_API_HEADERS \ $$DENG_HEADERS # Platform-specific sources. win32 { # Windows. HEADERS += \ $$DENG_WIN_INCLUDE_DIR/dd_winit.h \ $$DENG_WIN_INCLUDE_DIR/resource.h INCLUDEPATH += $$DENG_WIN_INCLUDE_DIR SOURCES += \ $$SRC/src/windows/dd_winit.cpp } else:unix { # Common Unix (including Mac OS X). HEADERS += \ $$DENG_UNIX_INCLUDE_DIR/dd_uinit.h INCLUDEPATH += $$DENG_UNIX_INCLUDE_DIR SOURCES += \ $$SRC/src/unix/dd_uinit.cpp } macx { # Mac OS X only. INCLUDEPATH += $$DENG_MAC_INCLUDE_DIR } else:unix { } # Platform-independent sources. SOURCES += \ src/main_server.cpp \ src/remoteuser.cpp \ src/server_dummies.cpp \ src/shelluser.cpp \ src/shellusers.cpp \ src/serverapp.cpp \ src/serversystem.cpp \ src/server/sv_frame.cpp \ src/server/sv_infine.cpp \ src/server/sv_main.cpp \ src/server/sv_missile.cpp \ src/server/sv_pool.cpp \ src/server/sv_sound.cpp \ $$SRC/src/api_console.cpp \ $$SRC/src/api_filesys.cpp \ $$SRC/src/api_uri.cpp \ $$SRC/src/audio/s_cache.cpp \ $$SRC/src/audio/s_main.cpp \ $$SRC/src/busymode.cpp \ $$SRC/src/color.cpp \ $$SRC/src/con_config.cpp \ $$SRC/src/games.cpp \ $$SRC/src/dd_loop.cpp \ $$SRC/src/dd_main.cpp \ $$SRC/src/dd_pinit.cpp \ $$SRC/src/dd_plugin.cpp \ $$SRC/src/def_main.cpp \ $$SRC/src/face.cpp \ $$SRC/src/game.cpp \ $$SRC/src/hedge.cpp \ $$SRC/src/library.cpp \ $$SRC/src/m_decomp64.cpp \ $$SRC/src/m_misc.cpp \ $$SRC/src/m_nodepile.cpp \ $$SRC/src/mesh.cpp \ $$SRC/src/network/masterserver.cpp \ $$SRC/src/network/monitor.cpp \ $$SRC/src/network/net_buf.cpp \ $$SRC/src/network/net_event.cpp \ $$SRC/src/network/net_main.cpp \ $$SRC/src/network/net_msg.cpp \ $$SRC/src/network/net_ping.cpp \ $$SRC/src/r_util.cpp \ $$SRC/src/resource/animgroup.cpp \ $$SRC/src/resource/api_material.cpp \ $$SRC/src/resource/api_resource.cpp \ $$SRC/src/resource/colorpalette.cpp \ $$SRC/src/resource/compositetexture.cpp \ $$SRC/src/resource/hq2x.cpp \ $$SRC/src/resource/image.cpp \ $$SRC/src/resource/manifest.cpp \ $$SRC/src/resource/mapdef.cpp \ $$SRC/src/resource/material.cpp \ $$SRC/src/resource/materialarchive.cpp \ $$SRC/src/resource/materialdetaillayer.cpp \ $$SRC/src/resource/materialmanifest.cpp \ $$SRC/src/resource/materialscheme.cpp \ $$SRC/src/resource/materialshinelayer.cpp \ $$SRC/src/resource/materialtexturelayer.cpp \ $$SRC/src/resource/patch.cpp \ $$SRC/src/resource/patchname.cpp \ $$SRC/src/resource/pcx.cpp \ $$SRC/src/resource/resourcesystem.cpp \ $$SRC/src/resource/sprite.cpp \ $$SRC/src/resource/texture.cpp \ $$SRC/src/resource/texturemanifest.cpp \ $$SRC/src/resource/texturescheme.cpp \ $$SRC/src/resource/tga.cpp \ $$SRC/src/sys_system.cpp \ $$SRC/src/tab_tables.c \ $$SRC/src/ui/infine/finale.cpp \ $$SRC/src/ui/infine/finaleanimwidget.cpp \ $$SRC/src/ui/infine/finaleinterpreter.cpp \ $$SRC/src/ui/infine/finalepagewidget.cpp \ $$SRC/src/ui/infine/finaletextwidget.cpp \ $$SRC/src/ui/infine/finalewidget.cpp \ $$SRC/src/ui/infine/infinesystem.cpp \ $$SRC/src/world/api_map.cpp \ $$SRC/src/world/api_mapedit.cpp \ $$SRC/src/world/blockmap.cpp \ $$SRC/src/world/bsp/convexsubspaceproxy.cpp \ $$SRC/src/world/bsp/hplane.cpp \ $$SRC/src/world/bsp/linesegment.cpp \ $$SRC/src/world/bsp/partitioner.cpp \ $$SRC/src/world/bsp/partitionevaluator.cpp \ $$SRC/src/world/bsp/superblockmap.cpp \ $$SRC/src/world/bspleaf.cpp \ $$SRC/src/world/bspnode.cpp \ $$SRC/src/world/convexsubspace.cpp \ $$SRC/src/world/dmuargs.cpp \ $$SRC/src/world/entitydatabase.cpp \ $$SRC/src/world/entitydef.cpp \ $$SRC/src/world/impulseaccumulator.cpp \ $$SRC/src/world/interceptor.cpp \ $$SRC/src/world/line.cpp \ $$SRC/src/world/lineblockmap.cpp \ $$SRC/src/world/linesighttest.cpp \ $$SRC/src/world/map.cpp \ $$SRC/src/world/mapelement.cpp \ $$SRC/src/world/maputil.cpp \ $$SRC/src/world/p_mobj.cpp \ $$SRC/src/world/p_players.cpp \ $$SRC/src/world/p_ticker.cpp \ $$SRC/src/world/plane.cpp \ $$SRC/src/world/polyobj.cpp \ $$SRC/src/world/polyobjdata.cpp \ $$SRC/src/world/propertyvalue.cpp \ $$SRC/src/world/reject.cpp \ $$SRC/src/world/sector.cpp \ $$SRC/src/world/sectorcluster.cpp \ $$SRC/src/world/sky.cpp \ $$SRC/src/world/surface.cpp \ $$SRC/src/world/thinkers.cpp \ $$SRC/src/world/vertex.cpp \ $$SRC/src/world/worldsystem.cpp OTHER_FILES += \ include/template.h.template \ src/template.c.template # Resources ------------------------------------------------------------------ data.files = $$OUT_PWD/../doomsday.pk3 macx { res.path = Contents/Resources res.files = \ $$SRC/res/macx/English.lproj \ $$SRC/res/macx/deng.icns data.path = $${res.path} QMAKE_BUNDLE_DATA += res data QMAKE_INFO_PLIST = ../build/mac/Info.plist xcodeFinalizeAppBuild() linkBinaryToBundledLibcore($$TARGET) linkBinaryToBundledLibshell($$TARGET) } # Installation --------------------------------------------------------------- deng_noclient { # Packages are usually deployed by the client project. deployPackages($$DENG_PACKAGES, $$OUT_PWD/..) } deployTarget() !macx { # Common (non-Mac) parts of the installation. INSTALLS += data data.path = $$DENG_DATA_DIR win32 { # Windows-specific installation. INSTALLS += license icon license.files = $$SRC/doc/LICENSE license.path = $$DENG_DOCS_DIR icon.files = $$SRC/res/windows/doomsday.ico icon.path = $$DENG_DATA_DIR/graphics } else { # Generic Unix installation. INSTALLS += readme readme.files = ../doc/output/doomsday-server.6 readme.path = $$PREFIX/share/man/man6 } } doomsday-stable-1.15.7/doomsday/server/res/0000775000175000017500000000000012641367671020113 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/server/res/windows/0000775000175000017500000000000012641367671021605 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/server/res/windows/doomsday.rc0000664000175000017500000001124412641367671023754 0ustar jaakkojaakko/**\file *\section License * License: GPL * Online License Link: http://www.gnu.org/licenses/gpl.html * *\author Copyright (c) 2008-2009 Daniel Swanson * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301 USA */ /** * doomsday.rc: Doomsday-Server.exe resource script. */ #include "../../../client/include/windows/resource.h" #define APSTUDIO_READONLY_SYMBOLS #include "windows.h" #include "../../../api/dd_version.h" #undef APSTUDIO_READONLY_SYMBOLS // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) # ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) # endif //_WIN32 # ifdef APSTUDIO_INVOKED 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""windows.h""\r\n" "#include ""../../api/dd_version.h""\0" END 3 TEXTINCLUDE BEGIN "#ifndef NO_MANIFEST\r\n" " CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST ""doomsday-server.exe.manifest""\r\n" "#endif\r\n" "\r\n" "VS_VERSION_INFO VERSIONINFO\r\n" "FILEVERSION DOOMSDAY_VERSION_NUMBER\r\n" "PRODUCTVERSION DOOMSDAY_VERSION_NUMBER\r\n" "# if defined(_DEBUG)\r\n" "FILEFLAGSMASK VS_FF_DEBUG | VS_FF_PRERELEASE\r\n" "# elif defined(DOOMSDAY_RELEASE_FULL)\r\n" "FILEFLAGSMASK VS_FF_SPECIALBUILD\r\n" "# else\r\n" "FILEFLAGSMASK 0\r\n" "# endif\r\n" "FILEOS VOS_NT_WINDOWS32\r\n" "FILETYPE VFT_APP\r\n" "FILESUBTYPE VFT2_UNKNOWN\r\n" "BEGIN\r\n" " BLOCK ""StringFileInfo""\r\n" " BEGIN\r\n" " BLOCK ""040904b0""\r\n" " BEGIN\r\n" " VALUE ""CompanyName"", ""\0""\r\n" " VALUE ""FileDescription"", ""Doomsday Engine Server\0""\r\n" " VALUE ""FileVersion"", DOOMSDAY_VERSION_TEXT ""\0""\r\n" " VALUE ""InternalName"", ""Doomsday Engine Server\0""\r\n" " VALUE ""LegalCopyright"", ""Copyright 2003-2012, Deng Team\0""\r\n" " VALUE ""OriginalFilename"", ""Doomsday-Server.exe\0""\r\n" " VALUE ""ProductName"", ""Doomsday Engine Server\0""\r\n" " VALUE ""ProductVersion"", DOOMSDAY_VERSION_TEXT ""\0""\r\n" "#if !defined(_DEBUG) && !defined(DOOMSDAY_RELEASE_FULL) && defined(DOOMSDAY_RELEASE_NAME)\r\n" " VALUE ""SpecialBuild"", DOOMSDAY_RELEASE_NAME ""\0""\r\n" "#endif\r\n" " END\r\n" " END\r\n" "\r\n" " BLOCK ""VarFileInfo""\r\n" " BEGIN\r\n" " VALUE ""Translation"", 0x409, 1200\r\n" " END\r\n" "END\r\n" END # endif /** * Icons: * Icon with lowest ID value placed first to ensure application icon * remains consistent on all systems. */ IDI_GAME_ICON ICON DISCARDABLE "doomsday.ico" #endif // English (U.S.) resources #ifndef APSTUDIO_INVOKED VS_VERSION_INFO VERSIONINFO FILEVERSION DOOMSDAY_VERSION_NUMBER PRODUCTVERSION DOOMSDAY_VERSION_NUMBER # if defined(_DEBUG) FILEFLAGSMASK VS_FF_DEBUG | VS_FF_PRERELEASE # elif !defined(DOOMSDAY_RELEASE_FULL) FILEFLAGSMASK VS_FF_SPECIALBUILD # else FILEFLAGSMASK 0 # endif FILEOS VOS_NT_WINDOWS32 FILETYPE VFT_APP FILESUBTYPE VFT2_UNKNOWN BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904E4" BEGIN VALUE "CompanyName", "\0" VALUE "FileDescription", "Doomsday Engine Server\0" VALUE "FileVersion", DOOMSDAY_VERSION_TEXT "\0" VALUE "InternalName", "Doomsday Engine Server\0" VALUE "LegalCopyright", "Copyright 2003-2012, Deng Team\0" VALUE "OriginalFilename", "Doomsday-Server.exe\0" VALUE "ProductName", "Doomsday Engine Server\0" VALUE "ProductVersion", DOOMSDAY_VERSION_TEXT "\0" #if !defined(_DEBUG) && !defined(DOOMSDAY_RELEASE_FULL) && defined(DOOMSDAY_RELEASE_NAME) VALUE "SpecialBuild", DOOMSDAY_RELEASE_NAME "\0" #endif END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1200 END END #endif doomsday-stable-1.15.7/doomsday/server/res/windows/doomsday.ico0000664000175000017500000030646012641367671024131 0ustar jaakkojaakko (F  (n00 (-trFF(  H H) 7Y._@y4rNz-M#)23O2mX~@Pz $<2 Aa9NlcL/n5R /p+e8|I`\TMEEK8{'_p@`#Z%X+b*j$`QyHnS~#Y#YMq S{6T@`']-k3xWY2v=O3p3q%V};\9X>]0vRT@BQyg9|0n&Uz 0M -I?_Fk]F%^ .J '@~skzjJDRU\z^C5|/lJp(B60+f>B=\kow|p_dcN:3r%\$=O"U}7z4w8~@Ukopffm[]\K?MYL=9~2tPw + ,E$[)f/jBJKQSVTNIJJF?B?AB4r4s.j"W 0LE?tBd&_$Z&]&Y)[)[#WOwOwKp$U{*\6T2M6SAbAc9W?_Eh6T7V$XT|?` U+Pw%]Ms#X)`'`*e4v:|8x$^PwDh"W#Y#W3q2p*d.i6u'^PwAb#W~Be5!7PwT|"V~'_+f2q8|<~0o,h#ZDe ,H 1P 2P!82Gl<^Tf&5%~yD~WUC2v)bNu5S /L4Q2?  BhL|zgubZUE8zQy ,F -I 0L*U?(HIH~k_UF<+e7W*E%>[*jd|s#a9s=t6r&h_YVLsGkHl4S +F./&VWTH<4z$c&_'^*a'\ RyEh7V,H /J +E uQ{sS5{.k*e*e+f$[OvJo>`6U.Q=/፷[UYK}SUUU??UUUU?(0` -(>>()d  d) [*3 5 75 8 841*[ Y 0%>'B 1M6T>^HlIoHnImHkAc6U /K(@1Y# $=4Q;ZEf"Nr&W~&[/k:}<7y2r.h*_$WMsDg:Z -I"9!#L  5 0LAbLqMtP~ cl$x/35=9%sd_"[ U}NsFi<\ /J 5Lc  ,E<\DhJsU,hOmqW9z+iUFlBe@a8V(B de#1M@bEk"Z?}TmiUB0mNt9Y9Y6T +D eK$ 0K?aS}6sP\^adykUD:~0j#WBe 6U .J#K  ,HCg+fAHLKNV^{dJ78~8z/mNt 4S)C 5Hn8yGD;B_b]l~yvoeZ?5}4v-j#W8Y4U Dh8{@=@>Hboktyos||{eW]ci^N=4w3t.k=] V337U2n:8|5y:@L^mlqtngisr]`fdVH:DY]UI@=7z4v(_ 0L Q 5#W~/n0p/n3s<@@Saggfi^^Z`SPMPEBIFJKI=3w7{7y2q*eJo0Q:ZT}'_*e*d6rJLFJMOPSPKPDDIOQOECFAFBLH;x7t8w+f$[Px5R .Eh%['^$\*d$[#U}$X$Z"VHpFmHoGnPw Ou&X0h-` -K$?#A +F)F.O1Q:"?)H 4T 6V)F0Kp+cR{!T{<]*M&?Nu+e'_LsR{#Y W#Z V$\2n,g$]'`Nu@c>_?aGj<]BeEiBePw+a']$W#V}#V~']"U~HlCe 2RJn$YMsAb%=M }2P!V~']PwMtQy.h*c*c,g*f7{7zBB.n,i&]JpJq)_)`']$Z.k7x3s/k+g+g>6v'_-eNt6U"Qx"S{@a 0K}9W"U}#YNu$Y&\'`*e2q5x5y=?ZR<5y2q/l&],h0n3t.o8y>4t/n+f#[9y2p V"V~Im;[BePwGk5S":YPx#YQy)b*e*e/o/p5z=J?e)h0o)d$]*e'bS}LrAb 0O>^DgEh7V"';[Nt#Y$]7x3s0pCC<==TkbWN@9;IMRuuQB4t4s6t3tK;m2V?` 2Q 0M9Y=]4R$&:ZPw'a3s?4v.rR[H0uPWgu_TUEF]ibp~lN>?>0r7tQ4f4T<\5S 2P:Y6T'%6SQz3r7{=9:ajZ.tEe`ZW[\dWzin` c"b'd"]$]$ZQ|)b7j 4S6T2Q4R 1O 1M'!4QNu0o>NFS{C?UEJRVP>izwRSEC3u4u0m$[ >a 1O)E,H6T 0L +E&  1MLr/qL_KeTDNFEMDqPr]WV?<2q)c$YIm5S+F5S 4Q&@ a -FKr3w_s^H-%bveVPTL8/q/l#XMr 1O .J4Q /K$<bU/!7Hn<qwyv{g\VUSDA'`Ad%?%> 0N -I//' 7Y3tqjS{rk`VE>8xOu6T -I*C )D#U v &?^kxM%sO0js~teUE=3t8y3u/o2p2n0j'\<] 0L'A!8 v,>h^T$9H6s*eWTPZZWWPyKsJq@b 2R+H.L6U+F(D +D'U!:(pk?B>7}=2n*d,g+e'^)`*c"W"U}KpCe:[4R ,I ,H /J&> -ie[ZFI7{.m+f)c*e+g*f)b$[OvKqFj:Z4R8U 1L##&&V8w~Y>5y0n)d)d'`*d,g$[PxNuJpGj>_6T 5VNtZJ5z/m.k*d$[#Y"WPxQy S{MrHm;[(@ )dnYD7z/o,h#YPxPwPwLrPvOvIpBc )D3LL 2gx^F3x.m)bQyJqJqNuIoImImDg -H U Vkg:3x#` X'_+e&\MtAc:Z )Cq focQ=.n+g"WFj4Q*q:50AfjpbZ>*eKq:Z%< 0+L&``FdnkcZK1u+j+dJp7T2 L8(2{ !J6aAlEn@i7[ 0L6{26-&<Z| |Z<PNG  IHDR\rf AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~@IDATxiYZw7ܗڻznf,H d ,BP8VHAa,% _a`k}79yko{ﻜ=g?) 4=0Gcz`<=1q<=0&?`<=1q<=0&?.x@Pϼ,,UJҦ(Gǟ|ަܢ<~{Q!ԀqJwΛ Z+W(OQCyrøsy,CQynJQ$ [P! J 4){)ӔHUIڼƟzb EnNw!騋N?Bz2u{);pEڥ|o(A-ʫooy [ޥ_y`^?DƓ 2"~-MkgU} wrZpt*'5&+ l4sKZk0pL Sn<#X\R=rzc\E7 -T T cw0-ahQvH v[*DH~BhPk#inɮG'/<έJ˧@*}G5ڏJ87U%C@g)~" ~##Q w V/-Rpj$K "DU *4M Ã}By&UЃSWՆPgMx|x @DZv#"nF ۠ڢuԅ( Iग$QSlD^zu;)wPw}G!7o}%uL5-wJz +!3`av*sf6MLfpL%4EA1@hsrǮ6O d.0!Y@ )>pw U G KHp{ @j@^s4gܾ1rtCsKijz.u󏎎Ti`~kN#.VUou3 waS׃"!!DAaQ{R.U @GA$[ݑ Qb_Po!Z>ml3 ]Bh n'=Um n ]ޛz?x0T_dP/+uE4T@ Z02<6 vǶeD4dVEp룫+$>I>zu*p@4LW~w&R"!6 PI˫>IԟDfirڡ {n5`{pëI0bѰ5t'jX iKw/_q~q=~?忦uc\^Fn< x u:{n*YN In_M3˔T%d=|*ٸ|P|ۈ>=iWu&T){t_n!? [A_^;:wbKw^{>b# VEDM"zs t~E{.up[@ۿF"E:CKЂQz@s]?`Sk?)FSDg^ ~-Kpt~m௦ٕij4%~,Z_vm: @i4DgWNuE6~|M5dA@k+ ᰏ`wkkn#DtT=T= tt:d!梐 @OVS {DCȱD@{>w;L]klޠȌ|m"xm `@|w7(>z;E )`κ&kŤEy S Q?8~k+CGp}%-GO>#\@!Ӱ7'zzj,:}H2ߗc(0ißҿ^z+txm  u\}~J=P5 Gg):P/p 8(ǗeP;ā.>;=w:sL8z |o!ݾwG$o.Tcj.x :p ҥg49wTEԯ n/7`O @pyE|>M~t7)~A ޑpN#h py\CF۸F;G\`׿{^فORcfSgTh1 fVa"R'h .Ru?tnY<盈7d<{s*3򹕴zBj.êO___nN_DIX F5v# o|!Q?PO;?KUˈHSг0SMtTǥ{LS?"b] {Kn?t"nlhc3X/GG%0?B5A;9_I9Z|>ιE\prTu2zw 6-lNN!֭g̿Db]w؆0~ hLNBxq<3&^C5CU|Jrx rv Ix"!Op!ܨ$4~o|4x]+ /4FH%|$u!Z-5ҕiܥ4{R]ab]뾮=/{ZOO3~Ϧ/a |Kh20 ) |\lpr'#(>0tH X&z*NPE|9@nnzԯ^/. + EuC2 ,1N}&qM2=mlHq$HpyCAl6cA2`60o.pEsW7];uLn_xשf>FC!ez汩8Ll=3tSgq#'rOtۏ|щ|D! +@tLCB%3a AldEE9$sC-L[ `Y`kuɺ 4Y\&F`fw! ?(3TfL)f§8\N_ƭ=X7G]" |R$(}|8.=#jӗ\b C@P B˪r| u.S5B^fdn~@psz<^!NOWǺ/.@> % A&0h"Rh/:/w}~ZVh * y&l#Pzf~QJ?~lWG Oww 9(h,wZ,gYwn.>fN]&gC_E|&ydP&Z1] }l'i ҋ),uI4Б5(baMӥQ.Dxj,vS*`B_;r^Pn9 ],NqҀzW@U0~,AP؅8Xgzʍ &c."?5 FJ"8h#G| P5d2?øs7xyK?K=J]#&?q54!#KkVy =+%;I_U>xv̝I:LAFz58i4]%['_US^ 9y&xCBy5&sP#s0莞Xq1 }w_pI&7u5Џ|k[ _@ LFw$,ل=?!W@j @۪ ']DI@Bn/xOfED" p C!Ewb  жV3g+ ^YLIDFޱ>y/?Aϧu %Sv@/LO+ 9D[{ |  " '5nΑ;G =~*udj{@+H—VS PӽY>zya6:W:u@ߜȔҐrKWjvwnBΏqDƾ# }Rpe;O?;"!&ғVKO E_KE|YSK,3O&u!Qj$_aI:MLfeX: >(1*u$-*^)"s͹9%7M[z: w39ӄdHUR6|I h^$n ULm޳1ǜ LLQ)lI_pr@&B=xUz0!w$% ԙJ4oG"wiK ";e{sB*H~t&ڿ jHO%iקų`Л#b'eC/gpwLS1eQocV`Ԙq €.U'O/0mW\b1ƒAMKQx.a}3O*ĉ<\NͥX/`5آԹZۀG|HQШg$ 2s^ i=Zh*DLßDS ٕLqW+ha <@j^ )ots:;"Z66~$dzS@w2\+pKii{?~~}UDI|F"m|pv٣'bʮWc7ͮ=w|:]l~ pl{}or"=@c 'F, J-Np+~+{4\5r_r$@:T A=73@e7BD%O‰IIBAWŀ8Gd۾5> ҂%"/g >*6ͨ]AjGC}N1VjYCPS`LgAzz>s~f\Ҹ>G~'~;)2(: '$崍sdUd|f!쉴xX#GP'~#b~|o7ꗜ{O1}9@άqEw 12OC~Qi#Ϣyy22i1:N-̤SKHji/G=@Ww \|JTCh"Y8>jlp >4&(ynlKci.0x=oot"ʇ/0N/\}h[&A祵t4zIe]}r ~o9C׍O ,bf"*9 Õ;wo}{B"ˉݽJzw=lNޱJCq샷 rb#:b.U1 *SwaIUw9$DGRd_k0Kc zpcO[G$$*GO> ۱␶ϓxQPWPjȡJDncZU7tJGO/!DBzϚD4gmsBDEcn~svtSiye%5/iVOj^8?z~O*ݓ`%i$"qKpZ#MݫbN]܁X[3G*č kk,ILC`Kn<ӌMz6%rp kL' gˉF] ,ګ38eôW,݁TQ"qsReЫ h0]۸ #E_;(99ɥUtf3HTON Ab#OIV+S7Cpj>ۓT$~qN3pE Y]$g)f=)c">A jVx6~ ȣE }859k]CV75s؜eZ39 :BM3$a&< "$s;?!."0F?' }$$i}cVˆh쁉YB" LHZ=FIKCJ'7% 1QIH>ܾBIH$ >RHDufX'- {L, 43rs$}X9n'v;Pt2E8V8 Xg68=]]IƳ+*R> t}% ~LJ6P) "",d$r bp c f(wu JsikD,FzACˈ֋ b+G3R Qw1r4Ÿm6ZǠ6j@|@1UV=~WyH d":r|m%<ܜk=c{(1 ucz9UlT&},~z>ͽOg:輏QK֜p.x^eר,NHr~l^d1g_Q5Ę~вˉ9Q.mpգ[HrJ#s5@ f*Ӝ'ҽׂ;+H6ßr?.a8˂2[pF3<+bU^t 2VC0EltԥǷȣ}vSp ҝuE5赜³:${J&rDE~qUH0b81DOpZ ,6p|D@=tY7ɑ: s*Ov" ߦݻ4SFXTG;`ivrY>JykP }Eg ~MP"0Tݦrx[*ƽ.0Ak t5"йYN'p m0SB@qC8<_491ò^C/EC.+L 760΄AFZНv @DY_P>=V!u B :36lW؁,h oPuiO0.x'qҰ<]AC!(E&im "Hb$qqT5#FߦolIЕsapgjki \XXH>M>|+ ~YIu!ͧ8~":VcI\\`o}z6`?IO_H7zħk vXc%-dp prxARtL^Ia Ip}'iR)'nB[n㦎1]NYC eWc`P"2v%SB gYVHEU\J\UXj"p>?zOK0#T=-Ǽ~%Wk9$ \ HWڋi{3sy#a==췤;W#jY-‰rYvrvAlȭCGp=6Gt.7}ـrVdئkRA9<3DCJ?!kL7BY/1lt^d w>3/PWdt4IJWk& /p=߁0L07@)D[]e6JwC/YT^(VO+O!r=pnOk7BqͩB-1c .9hElrT9vrmnƴRD0HH$ V @'pCD)ù &} #žǘn}lfr΃m(^clGOJ?Lҏ1ۉ!Oe)-b[J E_ w|/ʯN&ט &E(BOg{L f0;lTv nƫ7`::4FȓHfb?%\قE2oԳ?{E dd5"a7AC%~ʞw66 [ˍ#.l]r9I1=K*qMbqa J>IK$ &: Kd6L^o*n ܶTߝ9wh۹o`zWmȕ3k陌S_&r7H5_~4yOfQϥ@9-P{|.+v"O|rr&UGWX!"lܠC]⯺z`P.'BHhZܓ9f䦊r={p)+&)"F0W08C (ᆊ㞯qcJw6p Z@@ASqlxXw{]??-:<o#W秋i%d9n\jD'"juNʂW#NSN3ڠ`g#"ᣳKk[r0aj)?^+38 Lshwx 9"@O~#%{MYcec!DcLjƼ}WVM<}"C &بKD!ΛO},>]9Z b#kF28!Y3q8OCu8O| ݷ_c.<{[Ϧd O*6 ~ (!P4x6.A."a>{)G %E2g MqMh$R5lo;"V`1G120r?k喹or~޹":?E:zd&k0 ~3P7F+C%Ӭd;Ï n<$z5ݺu;Z\Hgu .3WBoSZ咰Y{M_w`LZS՟Y]@f_9B`$t1bWխ򷒉(TnP+ i m #6 j V ۦn/^l۔%jv¨}*i<7rǿ mD۶1HpR'Ւ.0& WirZI3Mx߈}Dn5X#sMmN!>asO Y|5B~w6ogwM !Ђ?Aد}t4=$:y$$<¨.-i`N@"׳5)f+xo*"Q̖JSQP[kE'$F[?_Z$ٞR ^`1~% 8&HFpO;Z3z.mVUjSZn(ge^\oA:?WLl3dӖM{χ(K3Cwh쳄>H'J'}9j+Lc?b|FVݍ_m_U\Z]É>:@C-`0/j?ԍ(Z+A#'QŲ[aXCd"O Sџ4v ~udU,zw{LPF1GՠhWB" 2:_S-BhXy8F,QWPzpnnC+eR~ Vk9@n;0S1| j44Ę̻S⦡Riv(E70`AV#2AIb<6,9{:!7)з@$O|⋈%9?/Z׉m? & vN1oH<ɛ@35i싴[pi(QCQ~\Y|I`F<`:tcC:$n-P΢APdխ8~=n2RK@~EWȓ 5J$gK&@(!(ALs pfm>B sBm-3!-%{F]k 9Gh9䇷!d-r?,YsN:f;wR?/x)  {(n"n1|)n'C^)2EFE6}*% 0D{yFNΗ0lݻ}vFVN Ym @V16" 7?I8lO˅A}/^5C}Z$$J[(n0:?Xۨ?k%$^O= zM$rzϷ?mu/?CXω#o2bdZxDRШA@IDATpW DUr~:o$:e@} Ya _wPppz%_Ik͹ka4ڙIG&pC~byjp56TTPD{b9u@f}rZA9*3SyV+P@y E"`Ӟ\+;;GoCX$l޷{E_K2PnS{g!c=59%A8>/ma!3 iЙbzu;:8mC'HHxe[o2u4 ~T 9}їP1?2as~QOݼߑ/ H"W yė6: <4h g-iD..dݼy+DzQ;hdP*)F Bgb ׶Wg{_v'\m1i{{[6>(VXK ̒HRB0_ @& DIx } GF0lf1D۔ $^Z l#ahtEydG4öJ$;DcEuGB%n[wNx0Z.[||PJdN1_89G:PB-3w̧/.䏩8=JOJ=uGɹ#q/)Оgv u.=|!R-VG`ut*9""`0=vTxu{\\;G߾ 38ur (! {b~`r[ xL&v]Cݺ,ȺrLnG}:K]ۯGyO4e0[NG9xpj/AtaQykUpq/NH<| Z?8-%vamFM'qC8 Y 1p[.Ɠ `wQbY-8^u=l|/Ot^Xgk!vU@/_Sp9ILv:N`6/N( hXq #cܪ@pɆg@p_ G~fnHst".i*A 7l`0bYG0Cv%$#?MЕ3Xw?]DƉFzlgvVk]a b]yLANn]pDAK5*BgnՀ#\ufd$yɇ"BJ 1˩}fy$TU눲pR۾J$:^҃@> GDrcMVI\}Fjy k ?Z'c_G@8kkR1Afv 6)Pzao#'_X0RK[{FTp%P.)bH4`ϊ5&H.%hcN%P^)6+YMq ;%Dau^O+K78 c?7tޟ[\vEߺ WMwg%9ζsA-wU}(n;0.3St&]u>>6qG 1&xf+Ihx>C? f☉S IAADE`Ӯ  bq*AQI^~nь=4SYJpYV1*oU)<P L,!G8 ([ E5 ,9>_jC ; teS̕Nt]Oy1A?ioc8pJŒJ Aú FA.M9|f'3 .[.G]B2X~A .Qί#8: 4gɶvJ;xv'7)t{(NdN7ҙEd ~$Es`Zy<31-|ZNCBaeyΎΟ&H$>ҁD`_PiR @>=KYǹr҂QZ%G{F9`+e9e0mvn9bv^\MB̀{Ł'g8x+1h^l:]VESJLBԥK) \`@o=_!|]YnwY"x8Y`qo B_IUvZĤĔgľ[w7]͊G <>DzL?C:hg0MDTEu"'tkzȘ$3G6~"i%0,ES!!n{(wlt_hL'ѯU# (+Li"tp;EdBjo~eDЂH8mܑTזx?\ ~| y{sm~?x=8F2՜g1wI~iq>g}92{ﱟSG+J2g>Td8QKPz2=9# kM,KsAO2"$xv~x?)۱f6g' 4%GN1LgpQ҅C,$[uz!з =%|VH`#?%"ۯ˄V侵?- ^nqSP[U;Œrj錐ߠx&K)ɕL;m7 @T9(`9`rq!/1o~f'R|fRt5쓤Bw%ܯ(~_?@ƏOb 7.#sh`439?8 !1o}jqi)A~G-T~ MnNkv#}Wbp* 7۱87:+όxq;B$CvMiއQ!:'RQ,…pqT~!msmܟG-;?&/*Aς8tp%S fۥ7WDPz/CbiS's\'P"UcXdc ‘k$ i|Y"-"=hc* Xޏm=Zg<,"OC3)Η4HQ!PLLB|k22PCY3v]¤Ԋbym@jr uiW)@T\ˁuTcaF_;m(R` ;vyNբΰӨ3äK85| &] unQ9gd}8lk7%yt6CtgCbК 0no?C16`C:<.Qj%^{Dn05'|%XjNH,Ѡ?[OO]roEm%]#9AHLVpT)Bq$% Aw,#x>6X"%prwi` UK>C bﳄ~9xLO>UֱͭQ9gXDźL\z:'@gb'_3 ~e%I'!~~@Jx)@#6U 9RT<E&@ⷔL$'R;0\@]y*1 ?m0>κo:,Aιեp9pvYmKp k[:HWy\ >WbkmY"F]xkWb!b+]__OMf^YÎTBoNW6fE^C2IpzsHF#Fp HLULM{Oo:خsZ-)84dIϜ@Aohc4a%A bZ#qn GQqLE*T<+ߓsYUL[ًzFGo՟Jhewkg90$FK^у'3 r"?'"eP1TJ%`ŅH"P޼NN/(j@pdz: LRY (@AH.Z1I{RUH{*H0΍$@q Gw֭WHEs5ԁtWlRMܫ2g9+[[鳯$N#dc@Q}Lz,]~.a oP*RTCU&`Il>ewacI߆j 3Y!-<C<' xxmԤ(o&'YWd6a ֭`mZ)$K@7 9 9H +A@$JmT_` |%ukt=r697./ ~J@y7GܾgO >W%/N%fdgn!iqo["%=xǚz6!@ZE']It/T[Db>b=Z4/8W6$Aa@kZe(i%&tjB0O>g'W_~!]}3! *慛Et*3=b3&kRd *zQO>dska*CxvI9K AβJ I Lƚy]!"W+TQ2C!)(1@\;Ji,S)m@"(F4 j1L1~ne1|^Cz @ ~vV{9#<0?=adX: RiUWX69L3<8|{`~-^E]A*̎(!Gr~r&{p"\u֡eOV̠E4@zi mr5 ^3| n짯OѧOvYi=ԙ_?7cH~;CϜZ+j q9^zzzpy&Yac n-б)\]XB2`NiyF5I`k@5(̳6vG~Msf)Qtpr[B\JXʌyU2 K<2PMIҡ%A1-", 2BYZ_csA5P6LW׮e46ۥRJێ-tq 1^]Cs\u1?ӏs~y6G>L' "F' ~zA"Wc3IC<\=4(%* Ru  -ΝS"d@We q_t`WN>=4@[pG8M.A2])@р ==<XA ODz+=7з'҇>p~=W+WoE\&kd:Fn.?}h 㩋*jA '_W}屈`@dWswl$X 3\䠯ǶݛANWJp)1()Xq+4Z9""J0X1p_ټ2ґQaOT *$({dN2,ғ!IĄ$8U rz͒ _xit@s7W#>ӥc֡J61OP~n|t2*>"K51؜CO8UuS>K^ @t[tl2۽[멺UMΛ_r P> X7ӦԽyyOq1p仑1,1Ыއb^,xk,o%|~]z U~)ڙax:tUn\.|EKD\S0<E}{D*'9vFsîyMћp'`( )8qInRB@q[0K,bQTEQbָ^ϧ:6&N[Fq齈\!1ur. 7DfcLTGeA/Gs#oSyx+8UHWwq)~9.| < :v7ytX ` =`>(a<8$Ye` |$}ZY^SM}UTgibPq`}#@F}H^r42g=!1QæD2"4>(!X8HA)6琮SaŒT"ckh2ʹ 鿷!|> ^BX糯mU)D+U `E":rCBc%zH̷ ~p.sh4KW [uҏFm}4>.{XmiGz\A%LzTZK{ {x2RНt}cZ={!}ϥwlpo$n_ L "v}]A(#[Z̧CLGPo_Cg='M% oXiP@|2P 1]DĸQ}/t[;a r*p'Ձk `V@Ftkmt[o|g_<2fy7a~ #YLt.sVm(Cô*XO/j"lv/%WRSrveøŠPp>N*m" 0$ʄ/81n\m:ﹶӛ?wcf ,)?uv 8>53L:ċ@^L3+Iz:#~=QO%y/L>q P{*!Q"%\~]ߪZzρcWe@nC86f186!c8/$@ޅܼW m{/B9[T馣A7zxh(*YnXK7WSH5ɪh46`F&Χi| B/+UaXDzPomu|ӷ1mDLOrfk*kv lӌ6?: \+ 4_5vJQ/r31&JQ9l X7E#96۠Jx"zc~sBuX$p84+BGw7Wq?x *,!ggIE,Q `b,ZrH"C` T=qŊʧm8C?CN! !JlKl#ﲇ+Xy  \9(ʞ ;3%7CޅޏPUڥJlIx(P51{!FDf<^baFTWwRCa2?]iNdl:Mqy[&j>[D"s!!PF2=CC¥g0M=%[MlBM`g凔CP6`a1bJxý*:#*e[IUe<3Vpvs5&$.;SܓܟOA 7Fn~4b+6LEc9Aۯ~eC4Z1mT@lB+8˒8/kePt ;s _eD݂EL@Jq4_:xĘeˀ෼' B7`ѳBJMu|Ql'HQGN5T@"mbs@+[ [k/8ҊRӀgCcS(5I IO^o1O#Sɱ27U_dw䤬km*RCRo-؏h?V":Q}c_]ds.ޟD&2 -jЋq6V;C7s 'N -ZVsIOJ$C+ _Gj.1:Bn}f6__brY 4!bبQD9w\oz'{UqlRo2mH#Cl~m3d)$˅3;Az鴿.Oݭ{ɪG+.6T :##@v&tN7A8dFI;a.Pi-tcVtHLrad9XZR L蚘Z~՚ kxNM3f3􅋯_%[JZJ0a[I`;Ǩ6_J})q7~`ҌKR4g"}|u8f\ G1(X2ޅ/ BQ(y^ĿbMs\4a|k(5 V C޼/I`yéyo y m&+ in.3W.MXsCZm" \po<:\%$D481?UBi7&O'63H?.\aZTƂ_FJ:48pKH,p4 ݀mawC#–$56nj˜;A/C%Rtl?\v m%ȉs%tc>)&iL~W%y~pwLET '1/;Cj{L!lA? nz~R@3M0.Rz=%_Ç~-w'FbP[=Txơ{\n)'8LO @h-Phb;_4Poě[I]f﮾π%f v֞mp>+!mn5yTy"\y7nI__8 gߟN]x:ϥt) `… xL uGLًi՗8#$NCA5(L6ù ӟ^{'f6<ʢ~w^rtկHocD䊵:ю6]=c c>kB\n\})Η7R u*$HVΤyDʹw+z.߸dZ=5+;dj-ݸ={ᒈaC(*v A0jR)d~fۋj@d9a x((_V.* s!.qPpxtd=@G͗WB+5R JqSV(RG?\B@CK$f3S=+`q>Nj9m,ug8/opKo3x La }\@D1B,*L no4y>_t{Rs 12 J>'ͶY^CPʤF]m`31CE1Iodc)Jy޼.JHl\^*H<șycim_xGڭ*bv(S˕J;;Qcv]TM;,P1R=(?u0nsqڬIi5XZ=@aDy8Z5AI\to@wrQ$X) Qbwno׼ུ@$ 6~v(fGM3 X.S_S /uyzd-LZ-@$Cr- "@dMMbā #B [HSE)ɞy՘|롽eHݿzp|x}޶NdT/RF.191/yӎ/ڳhNNB15 ЃRh]+Et3aP̔ N|C]6q?AI8iR2Ɵڿ1푣F7~c?zF?~o_@K_Uڅ,[Ėcg[gcmFv/l~&2 @_2d1ϹI4 N_{̍no mF!S /}/<^5{Y@;KBGho_;g‘-mmyҤ|m4`\,քƷm>ڶ,z-Sӊm6@+6 ͆xPրu  [2OtwS ثv5.rޫ@io*ye5x_* D;SyJ~g)M}sYj'w`k"\RL{'}V!;4-4LSKv 4#(ukfA``B4fP?BD]g),4u[7VOYS?z;zPjj>w&,L'CJO{뫏>>'[,ZwDE:+Nx_'_lujk/lJqш<@Xn6__ 0mn[vg"hh"fJ*a'bz YX`s۱=f-p{Ks[A@^g^+SѪ:8ng]@ 8<`+7̼`K`*wo Rgz1M&||rUel40Ś@l5% {0isY O}CR {bYZ@D<3 ̒K ?ğ@BFD8)(V} G b5ÄJ"F5 -LuoڨYi1gj"B-i!]GN!#,fg$$<9ϋ"OcHB0iXp+A?j:/\iܷ^}WW?;S`u_E#7#y g,Y._[YV_+-H؃R4s|lfo1aӷh1A&1ahjgXK)1 ~n8'(HF/+2o˧K>>= }سww4^65bUݍ R),Gu)4(ݦ0gqԨZE 7p xژ@1#zR)Ȫ[M1t)-Q2נmZ^,A"Ax^z;AHvݦ3(z8&2bH&Ry S4ir=Ys巇&cpt?Llr4ZSjE # p? ,-!hJ?.i)5 >0h#rܧ;yշ?^_+;7y{uY˳?]&l!1ԓՉǫ;RW7VW՞TL5 Ku _ tkͳ49gY HHK 9 %45FE_}0h6)=;wY D)!; t;Mz6`]݀ݾ[Nݗlq>.jygIjVK{St(RF3kյSzȡq!z2?!ܿlUtٺ{j3H!f m8 LI=Xm'>K3i%6^tn 'd\qv0Y}?4SbD$7\Zh|jo2z&gi)Z{c`cA:&-Gv0 Y<0b7ze,Y $f2+{H)ĽG_~2vz x쇠DKozW\WMv֎3O4ƘI6/i̒:|K????Y;JK_>%Tq`i h憑&cEB $\U3gAgBIhǢOsr'޳}_63zk|neʣe d1?tJע (@Y7\Ws-84n@X>/+r63ZdrCLB,ޗ}"p#"bGuI$1<8ny$0 > %O/yAba!3QǕr1@Ӳjg׈iio].F5jAA]'9 984ˎw>h h`ݯ7iCfYm^d"@1-UQ+f"G[ Lc tTF@&\H;]\xȤx&Y X{) Eq8ZKwtq+?mT൶l]^;2#sQ:poʂ ,:O^zbYu3LD;LَٞS]3:Z{1p>wt\X!d&wꭝD 粝}h/t-9[6QDD M,mXrbܑ hz#ϟRK[\*@G1ɩgXW룸B)5bx~S?, _ӳuD:MٰUz#l4mep \P%UP /iIVi:ufɴxAQ{KiZPA8 w@_ W@ d0&~)vc5|~,u|l,nN',Y~ю %!3w  n0(2u>_mJA2 4 +z=zOz|O@VV|}soև"}(&‚% <[gg,[r(D6묝QBRqo+M@oيCȥ{i Rܿs{[BJmkIH TjfOSO[uܴ҃q׮RbB,sz:orXT4:0vʙkQ72g:%t؅YAיS-4~7G/G'ea̋`{> 2fƌ0%w,xfDY6bkA\ nkO (25/z^:j2Ց)5Y7&0`ٔMkP )Ig~r+LTa;ƺz>Pao@{gbPVC*8`jw7I^È}#HpC׸Qt=o>Y5`_K#A=!1iD q&eVcʆif>Lϝ<:%~Jū{j+v]llLȇ%AeU,ǝ١C)HҬR"⽯\-j Cl.p(a{5yu\T2{ԉ՟VDzξu8K-w@IDAT#xjA]<V K[|b.a^12?aϞ$iJ Nl+Yf^gm۷{-B9F`{i ӯJ lrO^M *}ToUd}|٘iuittmdM0=gE)@c:1 ";X,ӖR - ݴܗL "wO?YpVjkIיINTcdcV(4fAe2hc`&(وY~c4མAoN6&0@${?mO1\[<|DL7;ԟ*bLzDŽ!{V CtUXСc'+ =$u7|sMA@AFWuth$y' ?iY~NWigNXʩbM%t#|;zu웁ީ2x&ngeknxFݍX[c`ݟ߻b 0b7}}h$5fǺE7~o!k!8', :]]*A9,q bLc+  -h 24̤ݧ׸~oWJ" Ѝ% L>Q@|vrdxR/;JSΔ_~>`nSAc֪`4Lq^skaA\6L1B 0I%f 7.p81YgO@rG# ܗO,x['(ҏV[@N9$^r-A?=L3${QK?YiΗ4-kE;"䂰<$𽎅`w"Ve %+Eqc1q;y:% 1>h<+~2mw"Ѓ2vhnz8:VWS"1;h^l=;鷯>lC;Akցs' ;@LXe'(w| '\h׿_{)c3,Vi(48ZtD:1 w*7c%'bX ? r y}$Mxtf@W FR FHuyuIY}7@#z_ 01DSݏ/vՁ5HgE=Fka%&wP4|Dc6<{5M_fFhl򑃙IЂ6DӇCK?7oGko>`_-=L5~3zXLyJGh;K| VYQ[N +ZhEK,hDӢ=k=I&Vˬ{-F ۀnLjBx@ڡzŚjXs. ohBAaqE 9,Plfs Fc =]:BC>^'e֔'kw?MzWz}q),I%kBM"1@*S8L(f$j$7s ,f}Ho\dsB@ȉsM|E`-cL1ŞYUҀ*1@62_ 0 @0lyL `&1 h"?Q]NƏM% 鲀ci|; ԩXK 5h*9Y$-&ս5by&-B^E/~1#c?IQIx57qj/mϘTPo6>Sjn\kƹ@WG,eO}ynAhd;.n@2ucw'tg%Gc t>- O ; x&3h,-)}[{ •,AI\+>y+l|hKKZ'Vwpj XP(k @Ҝ6}:(m+WBֲ /Z0Cz\f䎒[tUÄ͝0`oaa"Uxr=30,p-ٽj4vEWQm:{z`v A_ֆ[m +Ua`qpou5*EQ U.(ZkcCߑyԏf# B8\3Z@mu^ !HMӹ"| ,U10YcG& 1&|Mܵn+` ۑ#x/I3Qi‡j6ʟmu #Eei-mw du?d.ESǪˀ] V*WlvA!M6EQCpPFWk6!*CSGƕgY/38МE6R?vZ53& c+[ÏJap;Vۥ56^|@\L 9rhnޕȣ &v4\&LLĽ-~**p[Ra"ƾ/' twƮ?CM{SRǝMgh<[נD9! &:1P2[+/nل෗+S|NLJL#;øi,D;XYz?ܬē#,oϮ)*[Spsa fj`pf)rDiYwRN@&8[Frw$BC\W컧iQr#>_lKH)!V$tj/j;0-LDZa{!&Y,5˕BPg^/w|Q?鼞"@$],SǑ2;KE0Ϊrw都^R{CP@ɱ q)\xʖJ%o& @bp,M5Fe}80{q.eLuǫC_wp h_;{bh$q9f Q&Liړ HJ ZWfim ijO8(Zm;|3O]v~YELR+ɟ\j[ P`ԣ7a˄"jAaO? ?vT-֙`350 h0t:8L4f ̸ ELCPAPm64|@gUUL 褬R=W"m2󬭴u%M7^\LƖ[*|_e7"Dh87ϣ1BBtpve+ւpj>8mp1™`1EBd]ٶ|:BqD EU~81qO`ͷ0m`Y`I'"fA>Tph4< L>Muuq=wR,s|[¸g1ۣ̋s)!GO (Fs+sѾ4{i͜}gp7f)mѫY}_]?V}t K&mD'X6I˛).%J;B. ^ yȒ~uw 8|Aabc 7I'iA'Xb1K4BBBy,?hMGtĞ⋬pc=iӄEյt`1k1ݛ ʪPP$A-R~عN jC˺VLn,FZS f?wGh Fp B>ISo!%jiex#lLytí?q8W_^_wʹNlVRHxiƒuTZǓKAMz?/O}Ұee"z $—.a>aDFl0u0G:LA>mďd`݌ Nڦ{"4GaP1G/|L`kIUف4٤jRpcRhs?T~^*f:r8kP_Wclv~c1#In.@›pI7f>dƕ{.⪽A_YϯY!G>\{)P`s) =wqᔌX l)E %b!Kt7ei RO_#?fX@ףh]_uV0lRɯ-*k-6 ə|l?(9&&9uhw8hl [}S=z^&aSbg!ID2N4 .ģ r&nvWB UlZ|+>hPkk0Y yRЂZLʸ\ R$[<؃x<w\ i;xi~J2&<{V@gt|>zZQ?a XQ P2F hX,9_f_t޼sr+(F[h#zWo}~>h;6)c[Po,8a)$oohMwCGEO3V* ;mQ,1c\_uuaQ>ñi=F Qj9uu]e&!DY2WtK?~`5~|@͑՞ =p+Ŗ Aw}NˏJi3WG8T{dyi̾q-eo82@zMO:vAo"<29~uA[2iL,!fD˜i/E.KXFP`zJ;q\<$e}uw ?*4ܤ jf]F*"Tk]_lp~  Mz86çr ƍ-xL yb;-Z,?f |$25.}}c3VO?69rGe#GEN3q0X 2 T00V1Ȑ#[Ms0q 5HHH͚:MEm꯽ M'i%ouƒ^$6ϟYh*'`v[8$LX0MeӸYLO-trҨ64d<.>0>q1^[ FʜXX>~TwM3D}c.>Rk4>۷~s4-v$}nV&K[]Ɨ>W'mFCfmbzɊ+q_ b8~ ձV~_&dW<,to`neܭHYݮ1A3,GuW?pXšWB~kK4Ư?;nAi f@٘ DGxf>87-Ŝf)LyA(L_ u1 IPkޗ<h<dKZzPcD_f5R1cnGtEP @%\ ȘpѶt;6gZl;篭 |cE,XC4/g pq-k zCP1D{*+ka6!afv0f# O:'nYwDFi [bdI6,R̹*YﭠGW;c}+Iixc[$6&=z|dϞ]K@ICĺpo&[GK3>[ίd#'bZ9 Y+4$8#-$VF̄IjE$*\?[[]ĖgEa*߫feFc q`T*ƒSwZ{`d@0}gE'9~̈́r=܈#qik \W tO pވ\o =4*ޗC{O @+2:rx[k eL)}- <LbK#nGdg|2wE ʰgmK qkrp 4璦€>^z-o]U?2PEXnj-6U>Msm-}C` mf5Z >"_Y ظKS'NZm "!PX Uy]ʍJ7ުPnbRݝTImghb%X~WZqnNuûL^ ; viUC'p #ђI*: &J&7cGx0RJ7~|F~Tp?Sge)6'm<&pŧ5=1ۯ4͞svc&=]m6d+Ss7ݶ6ϥk8 iy%F+ٻrxY}-X#'ˎ,*&NY3i&2Z_#?fjN5&_!oXc5ZM2H{c|*q^h3"zNBw޿ ,vzSjwY;WC]XX G0KIgKo<Pc>&_ f?ʻLArۇ ӯdҷV=8p$`ʳ'D77/s왅B:wr?'jˈ?m\ |(nMϝ+VY*ivU{_h۽7FUJu,riL.`z%u|Ҫ[w̸lFKy[Ar>.WTʎosWsغ'&bxy\\4a 7wKKMZ|>NqLRu (&9@{WB8Tks@{\0cB@io_a0f Ws)c%61&qqo<nt{@iz46~vfMyAǎ-$\y&C],g'%J`k(XcYD3A|Uj՘cAu3`P 0F@TDT:1۰%Z-Ɯ"Ιj8/4 M2cC ad  %݇Y&`ю,bDuhQ~fQf X.}giEȀ@8_U-ڐXJ%c^iGnR~(-˟@Qϴ#-(Zg$(8kKfHX[VJ.':_(Y%GN u<|>k[|V8XLG\ʳq%p"Mw_E9v/pFs幂:0R_4RN\F[k6|K Ku j5q_/'`#QcoЍUY~|ˮIoqcs`*3)-<~R>S< :<+qCE2YA-zC-Gg BM4$ Y,:u^״k \c^ttwBL z cT L[igv5V|2FAc3Ŷ4 ZY'@;Qp֑# {gm+x61oL.(et)7VANL-@;Nz^E #P%BǺp4G_k5T#~oOg+!irf ׀o4mہ" u8ikcJqxՖz|-fovbkb}[n^kDuձqL1ƞ Xo%$LJR7g<<he|OR>lٹȏ)mcAVL?Q.;ZF%#_fk ֲ8XZTdh)Lɧg ěð CbEX{dP>0'Ǻ],GKM8!58K7X=8$' NTЍ C$<M~|3Koݼ6곃* N66ixaѺ#=8y h4ĵ1τ oL˙Od ͟/@_>[,3m8Q)<'ٮ&v>yYxm8c`ϝ:6Z.D̪T0Pv_S@Ɯl E"ǙƊPh`^?``h6&H b:>ȼ\D3L6>>S+ j,E=#z q <P`E"<ۄ H,%#ژA5S@-ھd;S@-,<ZxU~n\4^ p Dr@M~oNA2W|AF0^aϾk䵄D{px-1j=aǐ~/2:w!`X!h׾4#\[YXhmece XM,րJ?I-:hY:%Xʂէ/EVCE8l)'[KKk8_;=j;K}隶c t}bڷ(4W:̧OfϩFbB hiɒV(H,Qh̩|Lj2dǨ@½nC̉"5t {p/m+:5>s( F|%oĂh,h]6` k0nžLf /.6 smвJK9 !5(gz)BspόIKe1{H v@%PX\do  sGsBUX nH"y^95gG`4b9Dƹ[׍ŀp/Kk('AoZ˃ܑO}|@Y~-ASAcJr4B_wўϣgՠ./ u >p?3/ Ft(Gi8HWvI\73ӽy٠J!0owgROcK`NLmØ}`Puʊ@g/~3ܦr=B6gLd ;µ&wj+jh]hҡhϘҰX7" &xhiWx1gqY"n>3GS^V׶ήQw?:fuAQfV1KFLX+74lE_'h6ng<:hg_oY>6.h@@#҄LQ!ߜ~U"Y4{N>F?MyG# p01;č,tv*a.jw.,4 ѴD7QkbGYpɳ/>xu3CR.'ӶzK,@PWfLYϫI/fR{ 0ٚs0@L}dE-;M'@~E[̏O}l-H,@υBR^4N.'E=1ucDX>7haZ 1IH}-P4Sb&IX!|? P0gnd{cN{$ƸA/E7BFp0#5='irՍ?#b.&M+=9d͢mAì 6@;@D( 2 ë۝*%g\4Z81Gp0ƖbPV%Xi-,n@2orAC݁=60-z kQlJD@9Ϥη7`~p'5 mW,R@K1`GTdbOuinƋ6ڶ W@j]R[{Ք{ENk[:\i8 Lx!f#Ϭ3?:^Ðu~aY <0ғFЗSd 1" '"O`!ńЋ{LP#n?6mP`Vb+!߹(;\eK,0*n"Q\_^;:yu,m^爰}OZib3~45P}^,'6RL[EǿO>-1%}z_ėi3,=~}ܭ?|K=NEim״cR!=!Rl2 +\qzN#~uIru;eI]-*ZkYLzDu׀:LD׷-AägV1+Mgb󦵔[VL 6"t ;o5T='W'y`JބIs]ȇ(m`}~F"echN;M)'ֳд/pz/&DG>EU$ʄ[]{NsE z>OX؉i/ a?)LF(؛kFr"žhhd}0CA[w :{6p / $K0KRƯ*τz>i^ÏUz^%t}x㕓2.ym?;M/VrMX" Y@weЛw-fdz lGi:lLxC(2@"7N`+@ԉƴ1+)un/L(GFUfƘk= PR7@& uA)Aw>d bM`ŲpȯIEFx &<&V:JԀ$29G{{m_gʠs W=VDB\{t\)@ #iZVӬ'@{kt+Mڮ;sS>peP],%c #p7L QОJg%l L zh=rUg,EuƵ(G/hS~Ë꣏]XW_ p4dɳ&5Σ1D+0[si/?3tMQQ+w?] z4_>oT1"#D܁S2 yDci:ZHo`1'DG<)Ϻ ڸjEU=_\Y$gp63qS/S^U_;[II%Km{\#hfw@IpƊhaEa3wĬɒHe5LzJeO˫/4}|ъQӦk*6_k|]@IQDo#xقzm-k'J4gPhB6W0 fDPh1`Xb ~&=#siŵyr. LAgSP`ȐV? #?ha;5Š'hWOP{ΜScH7:F@Y{b\#ou3j2ۙd `(X!  301b|x"d/Oc4pnNk@y&uki2Kֿ=! 2' >tIE>2 >Thv_@ٽ3zAZQ=Ag aeQ鱀8YM՘IxPjLӇ~ƴHZIj{GzL1?IMں5(;B9۪% kHW=q?,7Y rßemto݈_d$d[4ՆGX*J}w,_8ZU.W\f{[5 xhi,~e9Ϛݗ0(? OV{tCy9}gecx2a~46P#1%H5>? Ԗ;Mi>ji?f9?bt!ij­ 1!O}cE[7@|IdObu?#([<2zΤaq)u2)XH,jIK_vJlIs{-r{M0@,,ƺ"`Ӭ|W:-L2{"1bnF(G{@/W!7NJ47@oq_*m YMiVp~pqj'5fـuݘ_Ki Y\`UJGz>aW { .,)qOnFFRglX0PJ( WRmE E?l.Z_wߛЇ-za>7{f~S_EW~a1[g>6$^; '4Ź(LM 2-壭 2,Ahd`Aouvtd9lkw2 ts=me0i8.mt<\L4kA`ۖ[b47]?{V:im9z,o޾cŠY/TVm ԣ0"_@>Y@;.ra WK!yDzMo`/k=^{ 2L,&Z^nky8%+wT 6WX?z1 r9ș984@ |ՙAqUB:f0 6G1a (9*϶ !Ǵˡ@4F{,EiSb+Shly{;)(Hï][ snw(&gF-lo'M.&cPӜ*$@'<_c^:o+7+8H $QNBpa@@Ќ?eiŹ2t^?K׶+uRq1ޫ"T]'"(!w ol ktEW.'a")v̲М,I nNA"9}/(L9µl G{2K!K@Eqj&)I9*~SVwKѹ73`]Ep X0Z \νm᳼nlnatHF8)yk'm!H4pN;ZҴ rs[:u4әfFe1c.&ᆬ;ZA9۔ܬ'-@g1A#pSgf}o1U-A(@tZ @ |1h>K .]Ch "-;ck#pu&L7m1n1*> Fחw?:Txp6xAC`E}6֞o+w H?4tBgD1BzEhL t61L)PHE1[&djF'LJs TF{>]z} Y#P~diL@I{kga*瘍Ob%pz.)]C#dO! 7NT)df]䍮w +f_A#  `"+"Z@5rV.[P3zeèM3`9-гkjxX?~ںha\ƹV$2q">{m"gfɦx7xy^ u8_Ԟ_T⛧tw,cDh5`6ϙ"i7Go;+;q ݤL>6Kp}0`HhZ4#!ȶV46S&[1f|mTY\^BZFi1}E;`)'J:H#`6(&b0 \NwJOO \/gm„،+'~WO\=28IJ-˺? h=: Зmcix@R_}PpYf@ס- (l&}ς$3nkP|`C0U:3m+LI,^/ qױ>n?X ,ۓSN;ci֧[b E๚0c=ڂg3=@$dޛN^_6^wz- :p/f:WIe&[mq~ b|A2J"0XO0,c"b!"H4E>een&b4kqi♻ 7잣Nr{0"9^"\~(F,.BSqЂJRhCzF{dVlM7^=69Z; (^}4K-I?T9lGd JsFLg/ZIL:I3?hd4Lw&Թl4JxRg.XCQZwV.^Yu~gj/}qu{>hAOwgꚮldt}}}z xg nZ[0~d%uX2])>&lp@<ɲ|{8؟ь%+@ýA@KHx輻ԩ.c9bђ a*a C-`*S}/;TyWZ l߉jF܇Rp r^ʝB*Z%Dqz9ڴc A߈yq-ڕ̖WO&=4v:#D[Dax:Mѐ?AELE iGB*j'͝zU I]x(ݻ[u&f&1-zS2ԴKKNl"F/m驘A$1n25R1Ţ$3!iTs1G|p8A:P:qd?7K?Y]9, &)8X ;ЈsD*.CI`9/y ,m(}ږ6!i&J&4[Nv6IY|\%]Yn|{@ZT ->f|D&lMѡ@ay>\ʗHY?[C4,FNϞд n)S,X}7jLJZ"!+i3@Z?J)hDۣ{7g@j*2$qe5MV' 7cBg6j|nmky+6iv6EA̲Uc:~G_ҌK 3n3&Ĥ 8ilsimw.@7Ҙ x+ ZzpH ƺ72ӗ͙瓊})Z8q#FS̀<vL!DŽ ѩr' w[d&5*/zRgv_<njGo2:b؎iUeS7լEy#޳/ .u#"=/`MCl$} 6D%ǤjDA30 f kkV+au3ujg>cSK{Dl/5Owߍ߾U{OWaU{o: zRk}?oPlVSU9O4gR{`9= ,!Lݼ:j1Vw+;vRjJQз~˯iE /!7vyXU2>>b8׎|}z ^duA2ub}pr_Pg_ˑ#GWOX@٧gXewJm2v{SY`-.t`ݍG"3}E3cTf46s_*f,#8b:.S))Š#@aֲc?`]^[r-+r_%@ }9\pj LI0Ĩ1^^gh@:Zo5Sly۸$ұ#0v|asdazG7DoI_!byb/8aF"dff[AP #;IL=a۹-HVu×i2 wNUG͌/l7i0:jy4@ppf뙴/ee^wgO[N.}sL#3` xVO,ȗνlFa w0ۙw%íXͶl9u˧ߩmΝ~M]wtlGK#0Y]>)sxw%MlJj3cZsP I~2ZiM3ڜhC[Zx'2MmQ b_/C̥.ʘҿ>lGͥ=:;/T|Iqc3be>oTD^'R==O'y6s9JDsQU1|}YfLvwky9n@cw˓Own'bVN9еضT*ߠk8%Ϟ~s@#mp(M{řsn`+@wi] ML}caS4Ak+°gnx=oC?˗z{@xU[2/,ys&'5HC!|EP썫o5 FJv2J3Y52,AsBwLB}ǎl :ur;<5=ovʠno]°\Ca;0cC@FAsX.0=+~BPʼ׌iEAQ9%6Ij9E[܈4r]*3/IP Fs!|sn"!qs94!R }=:HSяKyۦ܀%P;S1H̍D ;Ñj2hx$0iOsF|X ٳ\# 4( o.zO$ PIv&+Gc'sMY)kPޑA[ M0hח6(por7!=ݤtg-Pk'# :CDxjjlkClFM]'= 3v n?Wgb6|q-Pm>7I;ؼi ;ETzaD{1 Rw7hE㛩C?Sqm`쳇Gݾhk]xIJH֩΄lk#G^lSblĸ*w>LoETs]F"Vü'?LOqQIrv褥h_/py8ڝ^ ' 7*uY4gml+ 1$(x~*aA*$p`Xx!bҘalw!ofI'Han\(h L_Dz׳wa`o'٦3SA1N})V6r:sc$L`Wl*m l]QO{hӶwV/^vc#" ͕&) De0[/\^zl#,6j'M;愪?Nb;<…L@ыwpLg[|l;6j5ILMm1}E u.Saw_c{_fվL.͗N,#0S;XCs m:^:1^uAa)#i?8 (7B;xL^xa: yΨԱE͝q^em>]esZVآN#jVY7&BubFARJ39 7[Saf@@j9h'U X-1ݙ b4 vx.`_8:pdҀ)gI4rdUo^UG8ZZ t;frHR/a #SUϬl[}|(ŸY?d< 蛴g3yxc>iF.4&?ޏ]~_o8jI L[jx_&v\0DC8d X'?|'MױInylR/3g. t➒gH5ٞo8_{GHQ2{#0y$?OwdiVQ1uy0*S]?/$\ 32/^6Dyz£μ1@aܾJf^nM1͛9˥h,~ȱvTM ~bV@: kK}ָ.$,1{MoU2N`:ZC暰<4q $)/?,zGǣ}$e-^>{>c|{}1Ё/3 ob!-E hq`8,0i$:k@a}2&؛=a&z1+aWpV:}zkߣfw``#2DG_zs(v/ƀ#Ʌ"="F8fݗT}禓I pCHv GӨ5l :(:- $p$v]ٟZJ,u+0a bwSώ0XOU@,Ƶ~8l+Gb&yvtC[oOtHeɹUμ8XܱcYtGD; ]Kdn&Ld&O!1{Qs(ՑCfקH̐am܉G(SoV'5Mt׺y4Db=FO=۔o ^aq1ojvҝKYJ3nsG44en1uoŽaD@?So@!%N1TCRYr9PGTtޑʣSW|b2M|uH! ?嶑ޡgY%>| 1C;n>BӾ3doS-F=^wKH8$>7ӯ[!kӧ6 1\>lجK,!x@C0G;'Is)‚|i6cfY9☄t%q]UǹV*IB$a`:'I0BR*^j=" r\;vMc 8 u'.#T u֐9 ߽75 F<:H$w2<& ?Y]|iȆWʺPv)һJ|L97M"nބ#*O 7> '_XߴcϼXbqr 4нjw$;\xni$A-~@ak禆vb[V|}``16"9D)-Z8[ɇ 0`$D/gԛ>Likj4!\Vя ج@jSf#"3Gd%t&uKq3ݯm@ U5w#VDiR/cr )k*:Rsg)T5ʷojQ joo0HRcm(>:<>gIY LgQy[(i;j?s9^|e=_6ܱRgԟ_`Wiw;8{MFQ6jh bv Yv1 K'?b4tw㯽9$ f>o{"Hqi*‹9q侤9ȑAG$1Wo_z6˳;e&9픚4L 7Zy )6t U5Dxalt4Q +Ǵ@(NrV;-yWT{H v1  pBlyqgrԤK܈(bݙ˙5I;IDTtel&?%:x\!2=H ۥ*߸ڂc7> s1 #݅#'_Î+W;xԨcF$timqN#>3 `O>hO} !]gy왓O^~~#9{WyYclǀ,ZݘvbpuM}]T?4?Rkk irT QR}\ja"S茹"Dpo&ch\I0eإ S9ך?le jڿK9/{O>cCEs`s~edu)}%oԈfpiF]!Yj4^v&'\Yq69H[_U7ɓח#aSC -"I 5PL Lm=9!bH&N?9ynmF{SY@Ĥ6=tjT=bR*mp4%06{3(@Y6xaj y(f `$hcѧNRR} Y, 9 f%|o[|:Z@} hB7vr#I"QL\Qۻ:X͎l'\\L`{`&Dmp, MK8fDxT.jqhqZH9wES7=1wb6r@cjxvs{"{ iO* u|zu~nfE1*% itz.;ݤ;AҮ<exoA6W`լH~𼿱@.Np}^\Cˢ%͐㘏t6 |C & -/ݔZ}70vm.7P5fc[]b!gtG~ ^g^kր63*Qg< ȑ8P(B x(=ٿ ;&u;GFUH'@=yiQF'vgW٢zK` VN9 q=qkVi!s$IT E5H|YX|3c._ҟ(YzrRrI=9Mlr`{£$ݯ%폹0#kPyMwFr@?`fmӘ ";R݌4Кv/Sf7Szv_uj%L&dIV=%]8hQٚ3furnڳf^֦4fɴ|u|ʌwȐfeb=|U1eykcKIЮraIG{dS|m8|k͘[5jaQj4/m*ܽAGd=UP{ӆ <_pn&02i D㐬S1~`[@4YܻӳM $=B[7rmqN*TMʌXÄbb.+ӏiaLEqnih.%O:[LKOdAi3Sq Tw){rP@\PaZz+ՎH!q 78<$.Ӆ$=`(pS׺^"0CnfLbTH; #Fk0d-Yy<wa Ѻ:`&w5uR:.3?CϏWjgZɮ$? P9"%ѝ<8ή^Τf:FlfQ$p<"ߟ_!"HJ`B2  lpdc[oþGXL7Q>qhwx)LMǬV6ۓ =c 0IKcH^kw0ix1[P{_mbIQfTBW434ƄhhJ_^n =W϶(OdPy1Mgރ??Cco*"յt^珋S)HsFܗZll:F!>OŖ(,/MZ5bx9T:.6?Yd[ṟ$G,݅ӯI!p{:*pϟ,%lk#HA.W7qQc{C 8]LsVG)kFei[=O[_ ФWk5WK/ti&/YQnP2͗~{ؒI}" TþW]5r*7k˱Vvfn xmg ;I0Z7n6?uw,#I@5Qc{DS=K_` ' jX´;T`9U|EfsV11&Ę#F{15`ӟY^ WC3Az_",Ԑz@v2 52xt1!;ߊ-{3hvZ0o8)ǞEHhe U5U>eF сa*pzz$o<|Gcq& {eZR'D%ՎEz^yHr'G{I&*=6l/# B]n˻.Nm%_@:/Ors: 4 (j_ZrG{wҏi|$!aZ4e0T"*Adq60w!@7L e!in@Ć'h4XOLV_@{z\'bxR5 ;L$dty,a3_OUz|HQI?;s3/\#dL?nvCs$L~3 w45)s W3C߭ܝHrHvYd=fKIKٵ[$x8 + W=1:8~42i@kR2ZJJԙ|[G 0vv1Y={̎5z H&q=ܼ DZpN9Pk>rH{iH&V eyeތQ1#8(q چ~fA??`”EQ4,&y;8>y݅0N1%¼p_h"PlHi&.5Zjog=闞8]<ԏ*iH;_e?f)vGQ>Jxpt?KdV{b4{_H22cnVZ7QC,&ӎǎ"0X~$&Q",Bs!^;y$UVخ$yy] 'X7@S5׋~GuȎFM^mjbORoC;JqwR\p)}I&f#%u<Սfǎ7 F[o|v!V|߁@23F=l%co`&Ik@ZW~6lSZӢK^:Vuitc)r݇T<ð F!}:0ƌ#*ֶ5UOT0)U2ct-_Hu33(SU%I{rHoԄ1ʛgbrccSh/s|K}Q=/Ck';e#b-X|N?NdZCia Kn[^OUW~ѿ'HSH_s(#ؗ~S_fܽTS!!bOq4D391@d1r* (_ ¥%+3[NuU5 ږ +Em` @ȓA$*G& T}MBGՋ݌=luaʱ{7@$}*^Ue} (}@eV4X7ǣ q{>Nap`Rxiٖf*.u4XgD̵O]l6*r7'K;^dj2HʘCW6e y_}L `*>r /'#J6v H'@#8 ǚ@d̬_ .Rmh#gl$rj8t=-⑖ (x}tUCt3vY(xI@z>ml.F&1f$@`cL3vXD< <̟!W%I4z4bP4ϽH1g{vI12zfz `^GJOs.EwB0ʢQ`J՞NY~Nb⓫L -Uxn欛},L ?{js5SOν%̷ `Y'0 ˔_@@hN?cs{X{DƩ~mB1nj 5a@ & &`1B2 8s l-Yl3" da*3g #ʼn:Y < l̑+O@fxesǟ8Ė9$׳ yv>P5yB} cx û"҇)D-Z=B/? Mk aylGLY Ѻ̆$rq|4!PUYچ60, c[M1İ{xǠA]$H1PlӣS_o B_!/"׳=FLS,/]mNY2Տ}6fyOe8~sm}zr=&P1ӏoo߁~,/z^Yj>6;,sP9$s*0)9wRf^YܱӎF3Z,@ ˪K((MNJ`a []bg! ƽ$i,c(cII*wP8 IVRʻGHєܘ \nzTt{7UJ)40iL|YsoL>0 ԁ/;{}^T&fSk}VS?9P$LKsB.(nEa"XQbs:{˒-UomC64>헲bH]7LĨsF ^AcHNw?e{TW~BYn#X%V}w ^\VB83&G&Hr2޺d: e%".MJ*P-8Y n+p!9kC( P\Te!S@3_`d?{ ǁ$Tbx0$`r&0}DoHrf=h6nԼM EtlʧշwOȤO7Fb *&QhyX6 wH sihC7L_xdtZPctXy:P-~70 fXj^Hb$pID в6-_کw?WnO n"x~39?vm׏fON 2s0)j77YH:q}9t|2 q}DHKOpjHjΤ{2 C2Hਲˤ#zJ:#&']xn< + vg<uL c7/w욹Q5}:^H~ h3FIZ ,Tbpbb1ƴW.y szƦNpրbjEat/P9?f2*Gê,Z Id_̊%_T@Q?=Q`.ߩnn e=[*կr􌉓Y}`{τu8} _h A~31`a`v}Yx+7ݾ-B !*t1Ih`)TЁD:'b;${uB`τ3On7S8q @ܘ|^yƞL!s0-bsPzL#47 A3i`m@.yR"i/@McrГ,`&?$*4g.F{ w0ƃe߽ք۬ Mڌk3`] cw;= :|/N'$Ge@_~τ?c2:$^^D$<, 3c_"U:=9@7G!BËC3Rqo3p!B*?Q">v+i.f/ɀPljO#;0e{,?Bx8$ju f̩G jFc@m(0?x'S_x6.$=Jʻ=z0U2HꞮH)˨Oܻ+#1$±}%I8XlKʷ!0d>eս s.:ヲd0;dJ5Zvlۻh~]Ǟد ݭW~G1K_҂ =¢OP 1@v,81aiYl`o&qJ|"&}PQ?}tn !" rd*/ysq$$T-aL)S=f0 U0iy_Pz1/bZ Plޅ.o^o-鼀=NM.?PSM{)9(c)Xҳ[Hlo u#/&p݅0jLԷ НwwwMhnZ?q#MgH⏽١Eyޫ5>?Gh՘_vǟMZH! t|"IC9+"_9_/hQ#je(rp$#g' o#E &sv}SOsN$uIU9!ǘ82y$Y~3̟rb-A qдh"T/?Mg:E&H{`^5W$?`&x{MٽژPF#)1[9 f Bz@Zuo5k14H{\.ff׋Cz21tN<4Oh9uET2p&j`Zmj?ڿk{G dS_w?H+^(ۭ9j?)O\ki@"B 5*Z7^<|ZjCM5H ̏= hm#~ai[@vrk " .3~:}_Lgлf!1Oᨍ=o)k:m7T̄ڕԴdVY‹NvS8PQcTWٛT|w6ǁߵ0z$&OXjpٹ(yX~cP/lG3Ywt[|mAܯJwz}:̙x2S2_.VZ.-#{P{￑N~.xNO~H퍧͡jmhEcYxnp[Ӕ@O PLLfɷ27<3U{<XE«NgںM3f0Hq`E)'-.s?&7vh1"u*&&BH+e{0-')ˋ#RtS^ӫǎ^xPsG#7kgf)vTꄸGSQyө}7 0Z=aE%0$ea\:O+#wC f7_]<=tZE4IMJ2|ěRs4jSsj{3`^1cpZj0*(UچOASL #>\ ~S}>.O>lW>9Cفx3br7q>Ev$:&N-s=3bc<>2\$ q`/cePH/Ĺ;4`e39HR|=OO W*ۋi/d׫u6V@Bsd` ȍt1b,"1" 2 m1 yȭn7SxhcK(\1٥òL<"xҙI /mI7ʟ=!jLE`ǫdzE3<jЋ.I-xe3c/4X;Je"7%e${mn>#iq=Ht8I~xךJ~Sg*ߎ+>;90~q5ŠLNٷM{1?=T1]^?j^oӞEvHLz?#$aƈ@ ȤF`-bx@'Ct2:;ZZ3Lr҇4qP^~$ W_IƄn4=՜ĤPI[ïD{VWH(_?a°8F&H1ă&Z`"jy bLu0klkk6qWŋzkKpL #~/(V~K^C1zK /Y7@ެ7&@'y}N:s@r"giϿ/L+IphϬW݀Kj6Lzӂ7l*b0)m总/>쀦d Z'ddM3 Otc5Ђx֦Xt5z}[t:K>z_J":b揶W2 = &@DGG9)52h"Fvi3&Ei@8 c.ċ D8E LNm389?)wYk@lW$|̎E&8*`H$q{&kH Yp_juS?h?ϧ;""?p`&Z'bub2}NC IiBuAjdRce@Fw|4KNOn=kL̗l0 9Nǃe!|[iTrh'fyJS 2̣Sqd4$0nIG$r(gV}1)শ_f6u^ν"k/eUg|1U^ڞ}SFsq[{;tMTqGwg,&}|{AD>y|d9`ZX8 jH]5 Jd<^STe|t/fHT?sf%Xơg/m iEP>-$FN/"Ź)phC#!Ièwlt8,ߕLfQ%.q Psm4.Z[{NmҲAg.q9Pٮô@_4Ocaz5e͠Y[y|Y0j~{ 3@M`hu&Y(pS/;yd#80HB@vPӑ=s_}#s[:`vzs`R(U=dQ13 נ Nds{ B}v/֝M}wuq IP#He@v%7U!v2&3`:NV'0L2I ^nY>p1=銙l?' cIy|~W00 y#h uo#s0Hk˒K䈅@V˕/G+@D6`hޭG1 Ѩ͹asRh v<8ƭֿ<~yן;/S˒p.bxX|^2'&8 cH uεa0@&QG)fh[zq3i$h@,}wP݃xP2 #!'Ak%pϗhw(h @ p Q"E;!t5zaH, GP[gBaaF *Dnj[DvL?1kI=(E9܁=j@h<Ƿ2.pbh %|vv-՘4toL@Io~۽4"[\Hpl¯N3Y~!c &h{.u]v=G =G~|3}km퍛}x3 J >cx23ҰsA$ ;$6%gѓ>D[T5@>Ch!:SMgWZ <2EqzB[XZ\oMg=1^͢a1!ޘ6,Կ'3"]k|#;:Vo_q>m‰X o~/#[ ]}~_Ï/۶:z}> #ၘsI|RtZW?F@E&L,b s"rl> yϙ 1a0ĥ9$< CƝAQ@1 C*!ixz뽙@v.!7e6 46xw= iOlKhifo~W_NǷOXwEf:-՗teEP0Nzƨ'1IY*v}ј {L|pt0_ l;}NsID wDN`캵zUiI[E;z!yvzČMb24?Vo_, ڡDM l1/@Sk;9FAFO@tb@"m1қ@ʅ)yxؙ2HAmԒ醱` 88 Ic'S(l(@a)z+/ޘ`Fx'Rw|[^d8 Fq=3Gf͘&=s ՞:ol_-@K#`2#eQdf85 bvb}}f-vIyEWo FW.;9d"V[݃77s^ͨkz̃&bޥ_zl>tomRw/ /uO ~b? i/v뾀t`Cl-C^0 8$Б~s02m#]#Ρ#-Cas~WE@whD 4F={ %Wߝx&#0I{F9@3yg򳍯9Z7B6?ۿ7c hം} \i:,] W$v:. -XKJW!3c,s6x; @]$aHJBb:=EcD_i-?\a䩟z\Oڗc3oVgpk[Z`<(!@_?Y`0G_x`w6>IAAFM0)mo#qk`9:t~qbD&="H| i[㙳FaFč[e(u-o޹\^>~ԯ*_y kY=yMlMؙ:|@:+ J1In%mkG\d4){yz@MÐۃ'IL= \E_. )QB6x Ļ߽qJubkٷٷ#P<"[i ?梲sW[mI=$}@JrXF^jhw6&y<t`M9=MX[dMb?WOk/5_y湭_4#jy~c:|?߹G7?hߧn:sc<|}=$Io=./_}?Ѿ˟}~a [R -tB  W4q7޺hd;ma|jڳ},ws e?i]g+7 }ɼϴsߗrҝ}CNq?|X[GG?xȟ+mݾ[--jSة[[ q[j뺭x [` * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef SERVERSYSTEM_H #define SERVERSYSTEM_H #include #include #include #include #include "remoteuser.h" #include "dd_types.h" #include /** * Subsystem for tending to clients. * @ingroup server * * - Immediately after connecting to a server the socket is put into the * set of remote users (RemoteUser class). One RemoteUser instance is * responsible for each connected socket. * - Remote users may request upgrade to a Shell user, in which case ownership * of the socket is given to a ShellUser instance. * - Remote users may join the game, becoming players in the game. * - Silent remote users that hang around too long will be automatically * terminated if haven't joined the game. * * @todo This is a work in progress, as all remnants of the old network code * have not been removed/revised. */ class ServerSystem : public QObject, public de::System { Q_OBJECT public: /// An error related to identifiers (e.g., invalid ID specified). @ingroup errors DENG2_ERROR(IdError); public: ServerSystem(); /** * Start listening for incoming connections. * * @param port TCP port to listen on. */ void start(de::duint16 port); void stop(); bool isListening() const; /** * The client is removed from the game immediately. This is used when the * server needs to terminate a client's connection abnormally. */ void terminateNode(de::Id const &id); RemoteUser &user(de::Id const &id) const; /** * A network node wishes to become a real client. * @return @c true if we allow this. */ bool isUserAllowedToJoin(RemoteUser &user) const; void convertToShellUser(RemoteUser *user); /** * Prints the status of the server into the log. */ void printStatus(); void timeChanged(de::Clock const &); protected slots: void handleIncomingConnection(); void userDestroyed(); private: DENG2_PRIVATE(d) }; ServerSystem &App_ServerSystem(); void Server_Register(); // old-fashioned cvars dd_bool N_ServerOpen(void); dd_bool N_ServerClose(void); void N_PrintNetworkStatus(void); extern int nptIPPort; // cvar #endif // SERVERSYSTEM_H doomsday-stable-1.15.7/doomsday/server/include/server_dummies.h0000664000175000017500000000472512641367671024157 0ustar jaakkojaakko/** @file server_dummies.h Dummy functions for the server. * @ingroup server * * Empty dummy functions that replace certain client-only functionality on * engine-side. Ideally none of these would be needed; each one represents a * client-only function call that should not be done in common/shared code. * (There should be no shared code outside libcore/liblegacy.) * * @todo Add a @c libdeng_gui for UI/graphics code. Many of these belong there * instead of being exported out of the client executable for game plugins' * needs. * * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef SERVER_DUMMIES_H #define SERVER_DUMMIES_H #include #include #include "world/map.h" #ifndef __SERVER__ # error "Attempted to include server's header in a non-server build" #endif DENG_EXTERN_C void Con_TransitionTicker(timespan_t t); DENG_EXTERN_C void Con_SetProgress(int progress); DENG_EXTERN_C void GL_Shutdown(); DENG_EXTERN_C void R_RenderPlayerView(int num); DENG_EXTERN_C void R_SetBorderGfx(Uri const *const *paths); DENG_EXTERN_C void R_SkyParams(int layer, int param, void *data); DENG_EXTERN_C void R_InitSvgs(void); DENG_EXTERN_C void R_ShutdownSvgs(void); DENG_EXTERN_C struct font_s* R_CreateFontFromDef(ded_compositefont_t* def); DENG_EXTERN_C void Rend_CacheForMobjType(int num); DENG_EXTERN_C void Rend_ConsoleInit(); DENG_EXTERN_C void Rend_ConsoleResize(int force); DENG_EXTERN_C void Rend_ConsoleOpen(int yes); DENG_EXTERN_C void Rend_ConsoleToggleFullscreen(); DENG_EXTERN_C void Rend_ConsoleMove(int y); DENG_EXTERN_C void Rend_ConsoleCursorResetBlink(); DENG_EXTERN_C void Cl_InitPlayers(void); DENG_EXTERN_C void UI_Ticker(timespan_t t); DENG_EXTERN_C void UI_Shutdown(); #endif // SERVER_DUMMIES_H doomsday-stable-1.15.7/doomsday/server/include/remoteuser.h0000664000175000017500000000465312641367671023320 0ustar jaakkojaakko/** @file remoteuser.h User that is communicating with the server over a network socket. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef REMOTEUSER_H #define REMOTEUSER_H #include #include #include #include /** * User that is communicating with the server over a network socket. * @ingroup server */ class RemoteUser : public QObject, public de::Transmitter { Q_OBJECT public: /** * Constructs a new remote user from a connected network socket. * Each user is automatically assigned a unique identifier. * * @param socket Socket used to communicate with the user. Ownership * given to RemoteUser. */ RemoteUser(de::Socket *socket); ~RemoteUser(); de::Id id() const; /** * Returns the name of the user, if one has been provided. */ de::String name() const; /** * Returns the network address of the user. */ de::Address const address() const; /** * Determines if the user has joined the game in progress at the server. */ bool isJoined() const; /** * Determines if the remote user is actually connecting from the local host * rather than from some remote one. */ bool isFromLocalHost() const; /** * Relinquishes ownership of the user's socket. * @return Caller gets ownership of the returned socket. */ de::Socket *takeSocket(); // Implements Transmitter. void send(de::IByteArray const &data); signals: void userDestroyed(); public slots: void handleIncomingPackets(); protected slots: void socketDisconnected(); private: DENG2_PRIVATE(d) }; #endif // REMOTEUSER_H doomsday-stable-1.15.7/doomsday/server/include/shellusers.h0000664000175000017500000000326612641367671023316 0ustar jaakkojaakko/** @file shellusers.h All remote shell users. * @ingroup server * * @authors Copyright © 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef SERVER_SHELLUSERS_H #define SERVER_SHELLUSERS_H #include #include #include "shelluser.h" #include "world/worldsystem.h" /** * All remote shell users. */ class ShellUsers : public QObject , DENG2_OBSERVES(de::WorldSystem, MapChange) { Q_OBJECT public: ShellUsers(); ~ShellUsers(); /** * Adds a new remote shell user to the set of connected users. Users are * automatically removed from this collection and deleted when they are * disconnected. * * @param user User. Ownership transferred. */ void add(ShellUser *user); int count() const; /// Observes WorldSystem MapChange. void worldSystemMapChanged(); public slots: void sendPlayerInfoToAll(); protected slots: void userDisconnected(); private: DENG2_PRIVATE(d) }; #endif // SERVER_SHELLUSERS_H doomsday-stable-1.15.7/doomsday/server/include/server/0000775000175000017500000000000012641367671022253 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/server/include/server/sv_infine.h0000664000175000017500000000225212641367671024405 0ustar jaakkojaakko/** @file sv_infine.h Server-side inFine. * @ingroup server * * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDENG_SERVER_INFINE #define LIBDENG_SERVER_INFINE #include "de_infine.h" #ifdef __cplusplus extern "C" { #endif /** * @param script The script to be communicated to clients if any. */ void Sv_Finale(finaleid_t id, int flags, char const *script); #ifdef __cplusplus } // extern "C" #endif #endif /* LIBDENG_SERVER_INFINE */ doomsday-stable-1.15.7/doomsday/server/include/server/sv_missile.h0000664000175000017500000000305612641367671024605 0ustar jaakkojaakko/** @file sv_missile.h Delta Pool Missile Record. * @ingroup server * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef __DOOMSDAY_SERVER_POOL_MISSILE_H__ #define __DOOMSDAY_SERVER_POOL_MISSILE_H__ #include "sv_def.h" struct pool_s; struct mobjdelta_s; typedef struct misrecord_s { struct misrecord_s* next, *prev; thid_t id; //fixed_t momx, momy, momz; } misrecord_t; typedef struct mislink_s { misrecord_t* first, *last; } mislink_t; misrecord_t *Sv_MRFind(struct pool_s *pool, thid_t id); void Sv_MRAdd(struct pool_s *pool, struct mobjdelta_s const *delta); int Sv_MRCheck(struct pool_s *pool, struct mobjdelta_s const *mobj); void Sv_MRRemove(struct pool_s *pool, thid_t id); #endif doomsday-stable-1.15.7/doomsday/server/include/server/sv_frame.h0000664000175000017500000000226012641367671024226 0ustar jaakkojaakko/** @file sv_frame.h Frame Generation and Transmission. * @ingroup server * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef __DOOMSDAY_SERVER_FRAME_H__ #define __DOOMSDAY_SERVER_FRAME_H__ #ifdef __cplusplus extern "C" { #endif void Sv_TransmitFrame(void); size_t Sv_GetMaxFrameSize(int playerNumber); #ifdef __cplusplus } // extern "C" #endif #endif doomsday-stable-1.15.7/doomsday/server/include/server/sv_pool.h0000664000175000017500000002264112641367671024112 0ustar jaakkojaakko/** @file sv_pool.h Delta Pools. * @ingroup server * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef __DOOMSDAY_SERVER_POOL_H__ #define __DOOMSDAY_SERVER_POOL_H__ #include "dd_share.h" #include "world/plane.h" #include "world/p_object.h" #include "world/polyobj.h" #include "world/surface.h" #include "sv_missile.h" #ifdef __cplusplus extern "C" { #endif // OR'd with the type number when resending Unacked deltas. #define DT_RESENT 0x80 // Mobj Delta Control flags (not included directly in the frame). #define MDFC_NULL 0x010000 // The delta is not defined. #define MDFC_CREATE 0x020000 // Mobj didn't exist before. #define MDFC_TRANSLUCENCY 0x040000 // Mobj has translucency. #define MDFC_FADETARGET 0x080000 // Mobj is fading to/from visible/invisible #define MDFC_TYPE 0x100000 // Mobj type specified in delta. #define MDFC_ON_FLOOR 0x200000 // Mobj Z is floorZ. // The flags that are not included when a mobj is the viewpoint. #define MDF_CAMERA_EXCLUDE 0x0e00 // The flags that are not included for hidden mobjs. #define MDF_DONTDRAW_EXCLUDE 0x0ec0 // The flags that are not included when a player is the viewpoint. #define PDF_CAMERA_EXCLUDE 0x001e // The flags that are not included when a player is not the viewpoint. #define PDF_NONCAMERA_EXCLUDE 0x70de typedef enum deltastate_e { DELTA_NEW, DELTA_UNACKED } deltastate_t; /** * All delta structures begin the same way (with a delta_t). * That way they can all be linked into the same hash table. */ typedef struct delta_s { // Links to the next and previous delta in the hash. struct delta_s* next, *prev; // The ID number and type determine the entity this delta applies to. deltatype_t type; uint id; // The priority score tells how badly the delta needs to be sent to // the client. float score; // Deltas can be either New or Unacked. New deltas haven't yet been sent. deltastate_t state; // ID of the delta set. Assigned when the delta is sent to a client. // All deltas in the same frame update have the same set ID. // Clients acknowledge complete sets (and then the whole set is removed). byte set; // Resend ID of this delta. Assigned when the delta is first resent. // Zero means there is no resend ID. byte resend; // System time when the delta was sent. uint timeStamp; int flags; } delta_t; typedef mobj_t dt_mobj_t; typedef struct mobjdelta_s { delta_t delta; // The header. dt_mobj_t mo; // The data of the delta. mobjdelta_s() : mo(thinker_s::InitializeToZero) {} } mobjdelta_t; typedef struct { thid_t mobj; char forwardMove; char sideMove; int angle; int turnDelta; coord_t friction; int extraLight; int fixedColorMap; int filter; int clYaw; float clPitch; ddpsprite_t psp[2]; // Player sprites. } dt_player_t; typedef struct { delta_t delta; // The header. dt_player_t player; } playerdelta_t; typedef struct { Material* material; float rgba[4]; // Surface color tint and alpha int blendMode; } dt_surface_t; typedef struct { dt_surface_t surface; coord_t height; coord_t target; // Target height. coord_t speed; // Move speed. } dt_plane_t; typedef enum { PLN_FLOOR, PLN_CEILING } dt_planetype_t; typedef struct { float lightLevel; float rgb[3]; uint planeCount; dt_plane_t planes[2]; } dt_sector_t; typedef struct { delta_t delta; dt_sector_t sector; } sectordelta_t; typedef struct { delta_t delta; } lumpdelta_t; typedef struct { dt_surface_t top; dt_surface_t middle; dt_surface_t bottom; byte lineFlags; // note: only a byte! byte flags; // Side flags. } dt_side_t; typedef struct { delta_t delta; dt_side_t side; } sidedelta_t; typedef struct { float dest[2]; float speed; angle_t destAngle; angle_t angleSpeed; } dt_poly_t; typedef struct { delta_t delta; dt_poly_t po; } polydelta_t; typedef struct { delta_t delta; // id = Emitter identifier (mobjid/sectoridx) int sound; // Sound ID mobj_t* mobj; float volume; } sounddelta_t; /** * One hash table holds all the deltas in a pool. * (delta ID number) & (mask) is the key. */ #define POOL_HASH_SIZE 1024 #define POOL_HASH_FUNCTION_MASK 0x3ff /** * The missile record contains an entry for each missile mobj that * the client has acknowledged. Since missiles move predictably, * we do not need to sent the coordinates in every delta. The record * is checked every time a missile delta is added to a pool. */ #define POOL_MISSILE_HASH_SIZE 256 typedef struct deltalink_s { // Links to the first and last delta in the hash key. struct delta_s* first, *last; } deltalink_t; /** * When calculating priority scores, this struct is used to store * information about the owner of the pool. */ typedef struct ownerinfo_s { struct pool_s* pool; coord_t origin[3]; // Distance is the most important factor angle_t angle; // Angle can change rapidly => not very important float speed; uint ackThreshold; // Expected ack time in milliseconds } ownerinfo_t; /** * Each client has a delta pool. */ typedef struct pool_s { // True if the first frame has not yet been sent. dd_bool isFirst; // The number of the console this pool belongs to. (i.e. player number) uint owner; ownerinfo_t ownerInfo; // The set ID numbers are generated using this value. It's // incremented after each transmitted set. byte setDealer; // The resend ID numbers are generated using this value. It's incremented // for each resent delta. Zero is not used. byte resendDealer; // The delta hash table holds all kinds of deltas. deltalink_t hash[POOL_HASH_SIZE]; // The missile record is used to detect when the mobj coordinates need // not be sent. mislink_t misHash[POOL_MISSILE_HASH_SIZE]; // The priority queue (a heap). Built when the pool contents are rated. // Contains pointers to deltas in the hash. Becomes invalid when deltas // are removed from the hash! int queueSize; int allocatedSize; delta_t** queue; } pool_t; void Sv_InitPools(void); void Sv_ShutdownPools(void); void Sv_DrainPool(uint clientNumber); void Sv_InitPoolForClient(uint clientNumber); void Sv_MobjRemoved(thid_t id); void Sv_PlayerRemoved(uint clientNumber); void Sv_GenerateFrameDeltas(void); dd_bool Sv_IsFrameTarget(uint clientNumber); uint Sv_GetTimeStamp(void); pool_t* Sv_GetPool(uint clientNumber); void Sv_RatePool(pool_t* pool); delta_t* Sv_PoolQueueExtract(pool_t* pool); void Sv_AckDeltaSet(uint clientNumber, int set, byte resent); uint Sv_CountUnackedDeltas(uint clientNumber); /** * Adds a new sound delta to the selected client pools. As the starting of a * sound is in itself a 'delta-like' event, there is no need for comparing or * to have a register. * * @note Assumes no two sounds with the same ID play at the same time * from the same origin at once. * * @param soundId Sound ID (from defs). * @param emitter Mobj that is emitting the sound, or @c NULL. * @param sourceSector For sector-emitted sounds, the source sector. * @param sourcePoly For polyobj-emitted sounds, the source object. * @param sourcePlane For plane-emitted sounds, the source object. * @param sourceSurface For surface-emitted sounds, the source surface. * @param volume Volume at which to play the sound, or zero for stop-sound. * @param isRepeating Is the sound repeating? * @param clientsMask Each bit corresponds a client number; * -1 if all clients should receive the delta. */ void Sv_NewSoundDelta(int soundId, mobj_t *emitter, Sector *sourceSector, Polyobj *sourcePoly, Plane *sourcePlane, Surface *sourceSurface, float volume, dd_bool isRepeating, int clientsMask); #ifdef __cplusplus } // extern "C" #endif #endif doomsday-stable-1.15.7/doomsday/server/include/server/sv_def.h0000664000175000017500000000571012641367671023675 0ustar jaakkojaakko/** @file sv_def.h Server Definitions. * @ingroup server * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef __DOOMSDAY_SERVER_H__ #define __DOOMSDAY_SERVER_H__ #include "dd_def.h" #include "network/protocol.h" //#include "network/sys_network.h" #include struct material_s; #ifdef __cplusplus extern "C" { #endif #define SV_WELCOME_STRING "Doomsday " DOOMSDAY_VERSION_TEXT " Server (R22)" // Anything closer than this is always taken into consideration when // deltas are being generated. #define CLOSE_MOBJ_DIST 512 // Anything farther than this will never be taken into consideration. #define FAR_MOBJ_DIST 1500 extern int svMaxPlayers; extern int allowFrames; // Allow sending of frames. extern int frameInterval; // In tics. extern int netRemoteUser; // The client who is currently logged in. extern char* netPassword; // Remote login password. void Sv_Shutdown(void); void Sv_StartNetGame(void); void Sv_StopNetGame(void); dd_bool Sv_PlayerArrives(nodeid_t nodeID, char const *name); void Sv_PlayerLeaves(nodeid_t nodeID); void Sv_Handshake(int playernum, dd_bool newplayer); void Sv_GetPackets(void); /** * Sends a console message to one or more clients. * * @param to Client number to send text to. Use a negative number to * broadcast to everybody. * @param flags @ref consolePrintFlags * @param text Text to send. */ void Sv_SendText(int to, int flags, const char* text); void Sv_Ticker(timespan_t ticLength); int Sv_Latency(byte cmdTime); void Sv_Kick(int who); void Sv_GetInfo(serverinfo_t* info); size_t Sv_InfoToString(serverinfo_t* info, ddstring_t* msg); de::Record * Sv_InfoToRecord(serverinfo_t *info); int Sv_GetNumPlayers(void); int Sv_GetNumConnected(void); dd_bool Sv_CheckBandwidth(int playerNumber); dd_bool Sv_CanTrustClientPos(int plrNum); /** * Returns a unique id for material @a mat that can be passed on to clients. */ unsigned int Sv_IdForMaterial(Material *mat); #ifdef __cplusplus } // extern "C" #endif #endif doomsday-stable-1.15.7/doomsday/server/include/server/sv_sound.h0000664000175000017500000000356412641367671024274 0ustar jaakkojaakko/** @file sv_sound.h Serverside Sound Management. * @ingroup server * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_SERVER_SOUND_H #define LIBDENG_SERVER_SOUND_H #ifdef __cplusplus extern "C" { #endif /** * @defgroup svsoundFlags Flags for Sv_Sound * Used to select the target audience for a sound delta. * @ingroup flags */ ///@{ #define SVSF_TO_ALL 0x01000000 #define SVSF_EXCLUDE_ORIGIN 0x02000000 ///< Sound is not sent to origin player. #define SVSF_MASK 0x7fffffff ///@} struct mobj_s; /** * Tell clients to play a sound with full volume. */ void Sv_Sound(int soundId, struct mobj_s* origin, int toPlr); /** * Tell clients to play a sound. */ void Sv_SoundAtVolume(int soundIdAndFlags, struct mobj_s* origin, float volume, int toPlr); /** * To be called when the server wishes to instruct clients to stop a sound. */ void Sv_StopSound(int soundId, struct mobj_s* origin); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_SERVER_SOUND_H doomsday-stable-1.15.7/doomsday/server/include/precompiled.h0000664000175000017500000000413512641367671023424 0ustar jaakkojaakko/** @file precompiled.h Precompiled headers for Doomsday Server. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include #include #include #include #ifdef WIN32 # define WIN32_LEAN_AND_MEAN # include # undef min # undef max #endif #ifdef __cplusplus // C++ standard library: #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Qt: #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Doomsday SDK: #include #include #include #include #include #include #include #include #include #include #include #include #endif doomsday-stable-1.15.7/doomsday/server/include/serverapp.h0000664000175000017500000000324412641367671023130 0ustar jaakkojaakko/** @file serverapp.h The server application. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef SERVERAPP_H #define SERVERAPP_H #include #include "serversystem.h" #include "ui/infine/infinesystem.h" #include "resource/resourcesystem.h" #include "Games" #include "world/worldsystem.h" /** * The server application. */ class ServerApp : public de::TextApp { public: ServerApp(int &argc, char **argv); ~ServerApp(); /** * Sets up all the subsystems of the application. Must be called before the * event loop is started. */ void initialize(); public: static ServerApp &app(); static ServerSystem &serverSystem(); static InFineSystem &infineSystem(); static ResourceSystem &resourceSystem(); static de::Games &games(); static de::WorldSystem &worldSystem(); private: DENG2_PRIVATE(d) }; #endif // SERVERAPP_H doomsday-stable-1.15.7/doomsday/server/include/shelluser.h0000664000175000017500000000347612641367671023136 0ustar jaakkojaakko/** @file shelluser.h Remote user of a shell connection. * @ingroup server * * @authors Copyright © 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef SERVER_SHELLUSER_H #define SERVER_SHELLUSER_H #include #include /** * Remote user of a shell connection. * * Contains all the user-specific data pertinent to a remote connected shell * user. * * This is a server-side class representing a remote user connected to the * server through a shell link. * * @ingroup server */ class ShellUser : public de::shell::Link { Q_OBJECT public: /** * Constructs a new shell user from a previously opened socket. * * @param socket Open socket. User takes ownership. */ ShellUser(de::Socket *socket); /** * Send an initial data set to the shell user. This is only called once, * right after a shell user has connected. */ void sendInitialUpdate(); void sendGameState(); void sendMapOutline(); void sendPlayerInfo(); protected slots: void handleIncomingPackets(); private: DENG2_PRIVATE(d) }; #endif // SERVER_SHELLUSER_H doomsday-stable-1.15.7/doomsday/server/src/0000775000175000017500000000000012641367671020111 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/server/src/serversystem.cpp0000664000175000017500000002370212641367671023374 0ustar jaakkojaakko/** @file serversystem.cpp Subsystem for tending to clients. * * @authors Copyright © 2013-2014 Jaakko Keränen * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "serversystem.h" #include #include #include #include #include #include #include #include #include "api_console.h" #include "serverapp.h" #include "shellusers.h" #include "remoteuser.h" #include "server/sv_def.h" #include "server/sv_frame.h" #include "network/net_main.h" #include "network/net_buf.h" #include "network/net_event.h" #include "network/monitor.h" #include "network/masterserver.h" #include "dd_main.h" #include "dd_loop.h" #include "sys_system.h" #include "world/map.h" #include "world/p_players.h" using namespace de; int nptIPPort = 0; ///< Server TCP port (cvar). static de::duint16 Server_ListenPort() { return (!nptIPPort ? DEFAULT_TCP_PORT : nptIPPort); } DENG2_PIMPL(ServerSystem) { bool inited = false; /// Beacon for informing clients that a server is present. Beacon beacon = { DEFAULT_UDP_PORT }; Time lastBeaconUpdateAt; ListenSocket *serverSock = nullptr; QMap users; ShellUsers shellUsers; Instance(Public *i) : Base(i) {} ~Instance() { deinit(); } bool isStarted() const { return serverSock != nullptr; } bool init(duint16 port) { // Note: re-initialization is allowed, so we don't check for inited now. LOG_NET_NOTE("Server listening on TCP port %i") << port; deinit(); // Open a listening TCP socket. It will accept client connections. DENG2_ASSERT(!serverSock); if(!(serverSock = new ListenSocket(port))) return false; QObject::connect(serverSock, SIGNAL(incomingConnection()), thisPublic, SLOT(handleIncomingConnection())); // Update the beacon with the new port. beacon.start(port); App_WorldSystem().audienceForMapChange() += shellUsers; inited = true; return true; } void clearUsers() { // Clear the client nodes. for(RemoteUser *u : users.values()) { delete u; } DENG2_ASSERT(users.isEmpty()); } void deinit() { if(!inited) return; inited = false; if(ServerApp::appExists()) { App_WorldSystem().audienceForMapChange() -= shellUsers; } beacon.stop(); // Close the listening socket. delete serverSock; serverSock = 0; clearUsers(); } RemoteUser &findUser(Id const &id) const { DENG2_ASSERT(users.contains(id)); return *users[id]; } void updateBeacon(Clock const &clock) { if(lastBeaconUpdateAt.since() > 0.5) { lastBeaconUpdateAt = clock.time(); // Update the status message in the server's presence beacon. if(serverSock && App_WorldSystem().hasMap()) { serverinfo_t info; Sv_GetInfo(&info); std::unique_ptr rec(Sv_InfoToRecord(&info)); Block msg; de::Writer(msg).withHeader() << *rec; beacon.setMessage(msg); } } } /** * The client is removed from the game immediately. This is used when * the server needs to terminate a client's connection abnormally. */ void terminateNode(Id const &id) { if(id) { DENG2_ASSERT(users.contains(id)); delete users[id]; DENG2_ASSERT(!users.contains(id)); } } void printStatus() { if(serverSock) { LOG_NOTE("SERVER: Listening on TCP port %i") << serverSock->port(); } else { LOG_NOTE("SERVER: No server socket open"); } int first = true; for(int i = 1; i < DDMAXPLAYERS; ++i) { client_t *cl = &clients[i]; player_t *plr = &ddPlayers[i]; if(cl->nodeID) { DENG2_ASSERT(users.contains(cl->nodeID)); RemoteUser *user = users[cl->nodeID]; if(first) { LOG_MSG(_E(m) "P# Name: Nd Jo Hs Rd Gm Age:"); first = false; } LOG_MSG(_E(m) "%2i %-10s %2i %c %c %c %c %f sec") << i << cl->name << cl->nodeID << (user->isJoined()? '*' : ' ') << (cl->handshake? '*' : ' ') << (cl->ready? '*' : ' ') << (plr->shared.inGame? '*' : ' ') << (Timer_RealSeconds() - cl->enterTime); } } if(first) { LOG_MSG("No clients connected"); } if(shellUsers.count()) { LOG_MSG("%i shell user%s") << shellUsers.count() << (shellUsers.count() == 1? "" : "s"); } N_PrintBufferInfo(); LOG_MSG(_E(b) "Configuration:"); LOG_MSG(" Port for hosting games (net-ip-port): %i") << Con_GetInteger("net-ip-port"); LOG_MSG(" Shell password (server-password): \"%s\"") << netPassword; } }; ServerSystem::ServerSystem() : d(new Instance(this)) {} void ServerSystem::start(duint16 port) { d->init(port); } void ServerSystem::stop() { d->deinit(); } bool ServerSystem::isListening() const { return d->isStarted(); } void ServerSystem::terminateNode(Id const &id) { d->terminateNode(id); } RemoteUser &ServerSystem::user(Id const &id) const { if(!d->users.contains(id)) { throw IdError("ServerSystem::user", "User " + id.asText() + " does not exist"); } return *d->users[id]; } bool ServerSystem::isUserAllowedToJoin(RemoteUser &/*user*/) const { // If the server is full, attempts to connect are canceled. return (Sv_GetNumConnected() < svMaxPlayers); } void ServerSystem::convertToShellUser(RemoteUser *user) { DENG2_ASSERT(user); LOG_AS("convertToShellUser"); Socket *socket = user->takeSocket(); LOGDEV_NET_VERBOSE("Remote user %s converted to shell user") << user->id(); user->deleteLater(); d->shellUsers.add(new ShellUser(socket)); } void ServerSystem::timeChanged(Clock const &clock) { if(Sys_IsShuttingDown()) return; // Shouldn't run this while shutting down. Garbage_Recycle(); // Adjust loop rate depending on whether players are in game. int count = 0; for(int i = 1; i < DDMAXPLAYERS; ++i) { if(ddPlayers[i].shared.inGame) count++; } DENG2_TEXT_APP->loop().setRate(count? 35 : 3); Loop_RunTics(); // Update clients at regular intervals. Sv_TransmitFrame(); d->updateBeacon(clock); /// @todo There's no need to queue packets via net_buf, just handle /// them right away. Sv_GetPackets(); /// @todo Kick unjoined nodes who are silent for too long. } void ServerSystem::handleIncomingConnection() { LOG_AS("ServerSystem"); forever { Socket *sock = d->serverSock->accept(); if(!sock) break; RemoteUser *user = new RemoteUser(sock); connect(user, SIGNAL(userDestroyed()), this, SLOT(userDestroyed())); d->users.insert(user->id(), user); // Immediately handle pending messages, if there are any. user->handleIncomingPackets(); } } void ServerSystem::userDestroyed() { RemoteUser *u = static_cast(sender()); LOG_AS("ServerSystem"); LOGDEV_NET_VERBOSE("Removing user %s") << u->id(); d->users.remove(u->id()); LOG_NET_VERBOSE("%i remote users and %i shell users remain") << d->users.size() << d->shellUsers.count(); } void ServerSystem::printStatus() { d->printStatus(); } ServerSystem &App_ServerSystem() { return ServerApp::serverSystem(); } //--------------------------------------------------------------------------- void Server_Register() { C_VAR_INT("net-ip-port", &nptIPPort, CVF_NO_MAX, 0, 0); #ifdef _DEBUG C_CMD("netfreq", NULL, NetFreqs); #endif } dd_bool N_ServerOpen() { App_ServerSystem().start(Server_ListenPort()); // The game module may have something that needs doing before we actually begin. if(gx.NetServerStart) { gx.NetServerStart(true); } Sv_StartNetGame(); // The game DLL might want to do something now that the server is started. if(gx.NetServerStart) { gx.NetServerStart(false); } if(masterAware) { // Let the master server know that we are running a public server. N_MasterAnnounceServer(true); } return true; } dd_bool N_ServerClose() { if(!App_ServerSystem().isListening()) return true; if(masterAware) { // Bye-bye, master server. N_MAClear(); N_MasterAnnounceServer(false); } if(gx.NetServerStop) { gx.NetServerStop(true); } Net_StopGame(); Sv_StopNetGame(); if(gx.NetServerStop) { gx.NetServerStop(false); } App_ServerSystem().stop(); return true; } void N_PrintNetworkStatus() { App_ServerSystem().printStatus(); } doomsday-stable-1.15.7/doomsday/server/src/serverapp.cpp0000664000175000017500000001201212641367671022620 0ustar jaakkojaakko/** @file serverapp.cpp The server application. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include #include #include #include #include #include #include #include "serverapp.h" #include "dd_main.h" #include "dd_def.h" #include "dd_loop.h" #include "sys_system.h" #include "def_main.h" #if WIN32 # include "dd_winit.h" #elif UNIX # include "dd_uinit.h" #endif using namespace de; static ServerApp *serverAppSingleton = 0; static void handleAppTerminate(char const *msg) { qFatal("Application terminated due to exception:\n%s\n", msg); } DENG2_PIMPL(ServerApp) { QScopedPointer serverSystem; Games games; QScopedPointer resourceSys; WorldSystem worldSys; InFineSystem infineSys; Instance(Public *i) : Base(i) { serverAppSingleton = thisPublic; } ~Instance() { Sys_Shutdown(); DD_Shutdown(); } #ifdef UNIX void printVersionToStdOut() { printf("%s\n", String("%1 %2") .arg(DOOMSDAY_NICENAME) .arg(DOOMSDAY_VERSION_FULLTEXT) .toLatin1().constData()); } void printHelpToStdOut() { printVersionToStdOut(); printf("Usage: %s [options]\n", self.commandLine().at(0).toLatin1().constData()); printf(" -iwad (dir) Set directory containing IWAD files.\n"); printf(" -file (f) Load one or more PWAD files at startup.\n"); printf(" -game (id) Set game to load at startup.\n"); printf(" --version Print current version.\n"); printf("For more options and information, see \"man doomsday-server\".\n"); } #endif }; ServerApp::ServerApp(int &argc, char **argv) : TextApp(argc, argv), d(new Instance(this)) { novideo = true; // Override the system locale (affects number/time formatting). QLocale::setDefault(QLocale("en_US.UTF-8")); // Use the host system's proxy configuration. QNetworkProxyFactory::setUseSystemConfiguration(true); // Metadata. setMetadata("Deng Team", "dengine.net", "Doomsday Server", DOOMSDAY_VERSION_BASE); setUnixHomeFolderName(".doomsday"); setTerminateFunc(handleAppTerminate); d->serverSystem.reset(new ServerSystem); addSystem(*d->serverSystem); d->resourceSys.reset(new ResourceSystem); addSystem(*d->resourceSys); addSystem(d->worldSys); //addSystem(d->infineSys); // We must presently set the current game manually (the collection is global). setGame(d->games.nullGame()); } ServerApp::~ServerApp() { d.reset(); // Now that everything is shut down we can forget about the singleton instance. serverAppSingleton = 0; } void ServerApp::initialize() { Libdeng_Init(); #ifdef UNIX // Some common Unix command line options. if(commandLine().has("--version") || commandLine().has("-version")) { d->printVersionToStdOut(); ::exit(0); } if(commandLine().has("--help") || commandLine().has("-h") || commandLine().has("-?")) { d->printHelpToStdOut(); ::exit(0); } #endif if(!CommandLine_Exists("-stdout")) { // In server mode, stay quiet on the standard outputs. LogBuffer::get().enableStandardOutput(false); } Def_Init(); // Load the server's packages. addInitPackage("net.dengine.base"); initSubsystems(); // Initialize. #if WIN32 if(!DD_Win32_Init()) { throw Error("ServerApp::initialize", "DD_Win32_Init failed"); } #elif UNIX if(!DD_Unix_Init()) { throw Error("ServerApp::initialize", "DD_Unix_Init failed"); } #endif Plug_LoadAll(); DD_FinishInitializationAfterWindowReady(); } ServerApp &ServerApp::app() { DENG2_ASSERT(serverAppSingleton != 0); return *serverAppSingleton; } ServerSystem &ServerApp::serverSystem() { return *app().d->serverSystem; } InFineSystem &ServerApp::infineSystem() { return app().d->infineSys; } ResourceSystem &ServerApp::resourceSystem() { return *app().d->resourceSys; } Games &ServerApp::games() { return app().d->games; } WorldSystem &ServerApp::worldSystem() { return app().d->worldSys; } doomsday-stable-1.15.7/doomsday/server/src/shelluser.cpp0000664000175000017500000001406112641367671022625 0ustar jaakkojaakko/** @file shelluser.cpp Remote user of a shell connection. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2013-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "shelluser.h" #include #include #include #include #include #include #include #include "api_console.h" #include "dd_main.h" #include "games.h" #include "Game" #include "network/net_main.h" #include "world/map.h" #include "world/p_object.h" #include "world/p_players.h" using namespace de; DENG2_PIMPL(ShellUser), public LogSink { /// Log entries to be sent are collected here. shell::LogEntryPacket logEntryPacket; Instance(Public &i) : Base(i) { // We will send all log entries to a shell user. LogBuffer::get().addSink(*this); } ~Instance() { LogBuffer::get().removeSink(*this); } LogSink &operator << (LogEntry const &entry) { logEntryPacket.add(entry); return *this; } LogSink &operator << (String const &) { return *this; } /** * Sends the accumulated log entries over the link. */ void flush() { if(!logEntryPacket.isEmpty() && self.status() == shell::Link::Connected) { self << logEntryPacket; logEntryPacket.clear(); } } }; ShellUser::ShellUser(Socket *socket) : shell::Link(socket), d(new Instance(*this)) { connect(this, SIGNAL(packetsReady()), this, SLOT(handleIncomingPackets())); } void ShellUser::sendInitialUpdate() { // Console lexicon. QScopedPointer packet(protocol().newConsoleLexicon(Con_Lexicon())); *this << *packet; sendGameState(); sendMapOutline(); sendPlayerInfo(); } void ShellUser::sendGameState() { de::Game &game = App_CurrentGame(); String mode = (App_GameLoaded()? game.identityKey() : ""); /** * @todo The server is not the right place to compose a packet about * game state. Work needed: * - World class that contains the game world as a whole * - WorldFactory that produces world and map related instances * - Game plugins can extend the world with their own code (games can * provide a Factory of their own for constructing world/map instances) * * The server should just ask the World for the information for the game * state packet. */ /// @todo This information needs to come form the Game Rules. int deathmatch = Con_GetInteger("server-game-deathmatch"); String rules = (!deathmatch ? "Coop" : deathmatch == 1? "Deathmatch" : "Deathmatch II"); // Check the map's information. String mapId; String mapTitle; if(App_WorldSystem().hasMap()) { Map &map = App_WorldSystem().map(); mapId = (map.def()? map.def()->composeUri().path() : "(unknown map)"); /// @todo A cvar is not an appropriate place to ask for this -- /// should be moved to the Map class. mapTitle = Con_GetString("map-name"); } QScopedPointer packet(protocol().newGameState(mode, rules, mapId, mapTitle)); *this << *packet; } void ShellUser::sendMapOutline() { if(!App_WorldSystem().hasMap()) return; std::unique_ptr packet(new shell::MapOutlinePacket); App_WorldSystem().map().forAllLines([&packet] (Line &line) { packet->addLine(Vector2i(line.fromOrigin().x, line.fromOrigin().y), Vector2i(line.toOrigin().x, line.toOrigin().y), (line.hasFrontSector() && line.hasBackSector())? shell::MapOutlinePacket::TwoSidedLine : shell::MapOutlinePacket::OneSidedLine); return LoopContinue; }); *this << *packet; } void ShellUser::sendPlayerInfo() { if(!App_WorldSystem().hasMap()) return; QScopedPointer packet(new shell::PlayerInfoPacket); for(uint i = 1; i < DDMAXPLAYERS; ++i) { if(!ddPlayers[i].shared.inGame || !ddPlayers[i].shared.mo) continue; shell::PlayerInfoPacket::Player info; info.number = i; info.name = clients[i].name; info.position = de::Vector2i(ddPlayers[i].shared.mo->origin[VX], ddPlayers[i].shared.mo->origin[VY]); /** * @todo Player color is presently game-side data. Therefore, this * packet should be constructed by libcommon (or player color should be * moved to the engine). */ // info.color = ? packet->add(info); } *this << *packet; } void ShellUser::handleIncomingPackets() { forever { QScopedPointer packet(nextPacket()); if(packet.isNull()) break; try { switch(protocol().recognize(packet.data())) { case shell::Protocol::Command: Con_Execute(CMDS_CONSOLE, protocol().command(*packet).toUtf8().constData(), false, true); break; default: break; } } catch(Error const &er) { LOG_NET_WARNING("Error while processing packet from %s: %s") << packet->from() << er.asText(); } } } doomsday-stable-1.15.7/doomsday/server/src/main_server.cpp0000664000175000017500000000253612641367671023135 0ustar jaakkojaakko/** @file main_server.cpp Server application entrypoint. * @ingroup base * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "serverapp.h" /** * Server application entry point. */ int main(int argc, char** argv) { ServerApp serverApp(argc, argv); try { serverApp.initialize(); return serverApp.execLoop(); } catch(de::Error const &er) { qFatal("App init failed: %s", er.asText().toLatin1().constData()); return -1; } } doomsday-stable-1.15.7/doomsday/server/src/shellusers.cpp0000664000175000017500000000473112641367671023013 0ustar jaakkojaakko/** @file shellusers.cpp All remote shell users. * @ingroup server * * @authors Copyright © 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "shellusers.h" #include "dd_main.h" #include using namespace de; static int const PLAYER_INFO_INTERVAL = 2500; // ms DENG2_PIMPL_NOREF(ShellUsers) { QSet users; QTimer *infoTimer; Instance() { infoTimer = new QTimer; infoTimer->setInterval(PLAYER_INFO_INTERVAL); } ~Instance() { delete infoTimer; } }; ShellUsers::ShellUsers() : d(new Instance) { // Player information is sent periodically to all shell users. connect(d->infoTimer, SIGNAL(timeout()), this, SLOT(sendPlayerInfoToAll())); d->infoTimer->start(); } ShellUsers::~ShellUsers() { d->infoTimer->stop(); foreach(ShellUser *user, d->users) { delete user; } } void ShellUsers::add(ShellUser *user) { LOG_NET_NOTE("New shell user from %s") << user->address(); d->users.insert(user); connect(user, SIGNAL(disconnected()), this, SLOT(userDisconnected())); user->sendInitialUpdate(); } int ShellUsers::count() const { return d->users.size(); } void ShellUsers::worldSystemMapChanged() { foreach(ShellUser *user, d->users) { user->sendGameState(); user->sendMapOutline(); user->sendPlayerInfo(); } } void ShellUsers::sendPlayerInfoToAll() { foreach(ShellUser *user, d->users) { user->sendPlayerInfo(); } } void ShellUsers::userDisconnected() { DENG2_ASSERT(dynamic_cast(sender()) != 0); ShellUser *user = static_cast(sender()); d->users.remove(user); LOG_NET_NOTE("Shell user from %s has disconnected") << user->address(); user->deleteLater(); } doomsday-stable-1.15.7/doomsday/server/src/remoteuser.cpp0000664000175000017500000001762512641367671023022 0ustar jaakkojaakko/** @file remoteuser.cpp User that is communicating with the server over a network socket. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "remoteuser.h" #include "serversystem.h" #include "network/net_buf.h" #include "network/net_msg.h" #include "network/net_event.h" #include "server/sv_def.h" #include #include #include #include using namespace de; enum RemoteUserState { Disconnected, Unjoined, Joined }; DENG2_PIMPL(RemoteUser) { Id id; Socket *socket; int protocolVersion; Address address; bool isFromLocal; RemoteUserState state; String name; Instance(Public *i, Socket *sock) : Base(i), socket(sock), state(Unjoined) { DENG2_ASSERT(socket != 0); QObject::connect(socket, SIGNAL(disconnected()), thisPublic, SLOT(socketDisconnected())); QObject::connect(socket, SIGNAL(messagesReady()), thisPublic, SLOT(handleIncomingPackets())); address = socket->peerAddress(); isFromLocal = socket->isLocal(); LOG_NET_MSG("New remote user %s from socket %s (local:%b)") << id << address << isFromLocal; } ~Instance() { delete socket; } void notifyClientExit() { netevent_t netEvent; netEvent.type = NE_CLIENT_EXIT; netEvent.id = id; N_NEPost(&netEvent); } void disconnect() { if(state == Disconnected) return; LOG_NET_NOTE("Closing connection to remote user %s (from %s)") << id << address; DENG2_ASSERT(socket->isOpen()); if(state == Joined) { // Send a msg notifying of disconnection. Msg_Begin(PSV_SERVER_CLOSE); Msg_End(); Net_SendBuffer(N_IdentifyPlayer(id), 0); // This causes a network event. notifyClientExit(); } state = Disconnected; if(socket && socket->isOpen()) { socket->close(); } } /** * Validate and process the command, which has been sent by a remote agent. * If the command is invalid, the node is immediately closed. * * @return @c false to stop processing further incoming messages (for now). */ bool handleRequest(Block const &command) { LOG_AS("handleRequest"); serverinfo_t info; ddstring_t msg; int length = command.size(); // If the command is too long, it'll be considered invalid. if(length >= 256) { self.deleteLater(); return false; } // Status query? if(command == "Info?") { Sv_GetInfo(&info); Str_Init(&msg); Str_Appendf(&msg, "Info\n"); Sv_InfoToString(&info, &msg); LOGDEV_NET_VERBOSE("Info reply:\n%s") << Str_Text(&msg); self << ByteRefArray(Str_Text(&msg), Str_Length(&msg)); Str_Free(&msg); } else if(length >= 5 && command.startsWith("Shell")) { if(length == 5) { // Password is not required for connections from the local computer. if(strlen(netPassword) > 0 && !isFromLocal) { // Need to ask for a password, too. self << ByteRefArray("Psw?", 4); return true; } } else if(length > 5) { // A password was included. QByteArray supplied = command.mid(5); QByteArray pwd(netPassword, strlen(netPassword)); if(supplied != QCryptographicHash::hash(pwd, QCryptographicHash::Sha1)) { // Wrong! self.deleteLater(); return false; } } // This node will switch to shell mode: ownership of the socket is // passed to a ShellUser. App_ServerSystem().convertToShellUser(thisPublic); return false; } else if(length >= 10 && command.startsWith("Join ") && command[9] == ' ') { protocolVersion = command.mid(5, 4).toInt(0, 16); // Read the client's name and convert the network node into an actual // client. Here we also decide if the client's protocol is compatible // with ours. name = String::fromUtf8(Block(command.mid(10))); if(App_ServerSystem().isUserAllowedToJoin(self)) { state = Joined; // Successful! Send a reply. self << ByteRefArray("Enter", 5); // Inform the higher levels of this occurence. netevent_t netEvent; netEvent.type = NE_CLIENT_ENTRY; netEvent.id = id; N_NEPost(&netEvent); } else { // Couldn't join the game, so close the connection. self.deleteLater(); return false; } } else { // Too bad, scoundrel! Goodbye. LOG_NET_WARNING("Received an invalid request from %s") << id; self.deleteLater(); return false; } // Everything was OK. return true; } }; RemoteUser::RemoteUser(Socket *socket) : d(new Instance(this, socket)) {} RemoteUser::~RemoteUser() { emit userDestroyed(); d->disconnect(); } Id RemoteUser::id() const { return d->id; } String RemoteUser::name() const { return d->name; } Socket *RemoteUser::takeSocket() { Socket *sock = d->socket; d->socket = 0; d->state = Disconnected; // not signaled return sock; } void RemoteUser::send(IByteArray const &data) { if(d->state != Disconnected && d->socket->isOpen()) { d->socket->send(data); } } void RemoteUser::handleIncomingPackets() { LOG_AS("RemoteUser"); forever { QScopedPointer packet(d->socket->receive()); if(packet.isNull()) break; switch(d->state) { case Unjoined: // Let's see if it is a command we recognize. if(!d->handleRequest(*packet)) return; break; case Joined: { /// @todo The incoming packets should go through a de::Protocol and /// be handled immediately. // Post the data into the queue. netmessage_t *msg = (netmessage_t *) M_Calloc(sizeof(netmessage_t)); msg->sender = d->id; msg->data = new byte[packet->size()]; memcpy(msg->data, packet->data(), packet->size()); msg->size = packet->size(); msg->handle = msg->data; // needs delete[] // The message queue will handle the message from now on. N_PostMessage(msg); break; } default: // Ignore the message. break; } } } void RemoteUser::socketDisconnected() { d->state = Disconnected; d->notifyClientExit(); deleteLater(); } bool RemoteUser::isJoined() const { return d->state == Joined; } doomsday-stable-1.15.7/doomsday/server/src/server/0000775000175000017500000000000012641367671021417 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/server/src/server/sv_main.cpp0000664000175000017500000010756312641367671023573 0ustar jaakkojaakko/** @file sv_main.cpp Network server. * @ingroup server * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include #define DENG_NO_API_MACROS_SERVER #include "api_server.h" #include "de_base.h" #include "de_console.h" #include "de_system.h" #include "de_filesys.h" #include "de_network.h" #include "de_play.h" #include "de_misc.h" #include "de_defs.h" #include "api_materialarchive.h" #include #include #include using namespace de; // This is absolute maximum bandwidth rating. Frame size is practically // unlimited with this score. #define MAX_BANDWIDTH_RATING 100 // When the difference between clientside and serverside positions is this // much, server will update its position to match the clientside position, // which is assumed to be correct. #define WARP_LIMIT 300 void Sv_ClientCoords(int playerNum); int netRemoteUser = 0; // The client who is currently logged in. char *netPassword = (char *) ""; // Remote login password. // This is the limit when accepting new clients. int svMaxPlayers = DDMAXPLAYERS; static MaterialArchive *materialDict; /** * @defgroup pathToStringFlags Path To String Flags * @ingroup flags */ ///@{ #define PTSF_QUOTED 0x1 ///< Add double quotes around the path. #define PTSF_TRANSFORM_EXCLUDE_PATH 0x2 ///< Exclude the path; e.g., c:/doom/myaddon.wad => myaddon.wad #define PTSF_TRANSFORM_EXCLUDE_EXT 0x4 ///< Exclude the extension; e.g., c:/doom/myaddon.wad => c:/doom/myaddon ///@} #define DEFAULT_PATHTOSTRINGFLAGS (PTSF_QUOTED) /** * @param files List of files from which to compose the path string. * @param flags @ref pathToStringFlags * @param delimiter If not @c NULL, path fragments in the resultant string * will be delimited by this. * * @return New string containing a concatenated, possibly delimited set of * all file paths in the list. */ static String composeFilePathString(FS1::FileList &files, int flags = DEFAULT_PATHTOSTRINGFLAGS, String const &delimiter = ";") { String result; DENG2_FOR_EACH_CONST(FS1::FileList, i, files) { File1 &file = (*i)->file(); if(flags & PTSF_QUOTED) result.append('"'); if(flags & PTSF_TRANSFORM_EXCLUDE_PATH) { if(flags & PTSF_TRANSFORM_EXCLUDE_EXT) result.append(file.name().fileNameWithoutExtension()); else result.append(file.name()); } else { String path = file.composePath(); if(flags & PTSF_TRANSFORM_EXCLUDE_EXT) { result.append(path.fileNamePath() + '/' + path.fileNameWithoutExtension()); } else { result.append(path); } } if(flags & PTSF_QUOTED) result.append('"'); if(*i != files.last()) result.append(delimiter); } return result; } static bool findCustomFilesPredicate(File1 &file, void * /*parameters*/) { return file.hasCustom(); } /** * Compiles a list of file names, separated by @a delimiter. */ static void composePWADFileList(char *outBuf, size_t outBufSize, char const *delimiter) { if(!outBuf || 0 == outBufSize) return; memset(outBuf, 0, outBufSize); FS1::FileList foundFiles; if(!App_FileSystem().findAll(findCustomFilesPredicate, 0/*no params*/, foundFiles)) return; String str = composeFilePathString(foundFiles, PTSF_TRANSFORM_EXCLUDE_PATH, delimiter); QByteArray strUtf8 = str.toUtf8(); strncpy(outBuf, strUtf8.constData(), outBufSize); } /** * Fills the provided struct with information about the local server. */ void Sv_GetInfo(serverinfo_t *info) { DENG_ASSERT(info != 0); de::zapPtr(info); Map &map = App_WorldSystem().map(); // Let's figure out what we want to tell about ourselves. info->version = DOOMSDAY_VERSION; dd_snprintf(info->plugin, sizeof(info->plugin) - 1, "%s %s", (char *) gx.GetVariable(DD_PLUGIN_NAME), (char *) gx.GetVariable(DD_PLUGIN_VERSION_SHORT)); strncpy(info->gameIdentityKey, App_CurrentGame().identityKey().toUtf8().constData(), sizeof(info->gameIdentityKey) - 1); strncpy(info->gameConfig, (char const *) gx.GetVariable(DD_GAME_CONFIG), sizeof(info->gameConfig) - 1); strncpy(info->name, serverName, sizeof(info->name) - 1); strncpy(info->description, serverInfo, sizeof(info->description) - 1); info->numPlayers = Sv_GetNumPlayers(); // The server player is there, it's just hidden. info->maxPlayers = DDMAXPLAYERS - (isDedicated ? 1 : 0); // Don't go over the limit. if(info->maxPlayers > svMaxPlayers) info->maxPlayers = svMaxPlayers; info->canJoin = (isServer != 0 && Sv_GetNumPlayers() < svMaxPlayers); // Identifier of the current map. String mapPath = (map.def()? map.def()->composeUri().path() : "(unknown map)"); qstrncpy(info->map, mapPath.toUtf8().constData(), sizeof(info->map) - 1); // These are largely unused at the moment... Mainly intended for // the game's custom values. std::memcpy(info->data, serverData, sizeof(info->data)); // Also include the port we're using. info->port = nptIPPort; // Let's compile a list of client names. for(int i = 0; i < DDMAXPLAYERS; ++i) { if(clients[i].connected) M_LimitedStrCat(info->clientNames, clients[i].name, 15, ';', sizeof(info->clientNames)); } // Some WAD names. composePWADFileList(info->pwads, sizeof(info->pwads), ";"); // This should be a CRC number that describes all the loaded data. info->loadedFilesCRC = App_FileSystem().loadedFilesCRC();; } de::Record *Sv_InfoToRecord(serverinfo_t *info) { de::Record *rec = new de::Record; rec->addNumber ("port", info->port); rec->addText ("name", info->name); rec->addText ("info", info->description); rec->addNumber ("ver", info->version); rec->addText ("game", info->plugin); rec->addText ("mode", info->gameIdentityKey); rec->addText ("setup", info->gameConfig); rec->addText ("iwad", info->iwad); rec->addNumber ("wcrc", info->loadedFilesCRC); rec->addText ("pwads", info->pwads); rec->addText ("map", info->map); rec->addNumber ("nump", info->numPlayers); rec->addNumber ("maxp", info->maxPlayers); rec->addBoolean("open", info->canJoin); rec->addText ("plrn", info->clientNames); de::ArrayValue &data = rec->addArray("data").value(); for(uint i = 0; i < sizeof(info->data) / sizeof(info->data[0]); ++i) { data << de::NumberValue(info->data[i]); } return rec; } /** * @return Length of the string. */ size_t Sv_InfoToString(serverinfo_t* info, ddstring_t* msg) { unsigned int i; Str_Appendf(msg, "port:%i\n", info->port); Str_Appendf(msg, "name:%s\n", info->name); Str_Appendf(msg, "info:%s\n", info->description); Str_Appendf(msg, "ver:%i\n", info->version); Str_Appendf(msg, "game:%s\n", info->plugin); Str_Appendf(msg, "mode:%s\n", info->gameIdentityKey); Str_Appendf(msg, "setup:%s\n", info->gameConfig); Str_Appendf(msg, "iwad:%s\n", info->iwad); Str_Appendf(msg, "wcrc:%i\n", info->loadedFilesCRC); Str_Appendf(msg, "pwads:%s\n", info->pwads); Str_Appendf(msg, "map:%s\n", info->map); Str_Appendf(msg, "nump:%i\n", info->numPlayers); Str_Appendf(msg, "maxp:%i\n", info->maxPlayers); Str_Appendf(msg, "open:%i\n", info->canJoin); Str_Appendf(msg, "plrn:%s\n", info->clientNames); for(i = 0; i < sizeof(info->data) / sizeof(info->data[0]); ++i) { Str_Appendf(msg, "data%i:%x\n", i, info->data[i]); } return Str_Length(msg); } /** * @return gametic - cmdtime. */ int Sv_Latency(byte cmdtime) { return Net_TimeDelta(SECONDS_TO_TICKS(gameTime), cmdtime); } /** * For local players. */ /* $unifiedangles */ /* void Sv_FixLocalAngles(dd_bool clearFixAnglesFlag) { ddplayer_t *pl; int i; for(i = 0; i < DDMAXPLAYERS; ++i) { pl = players + i; if(!pl->inGame || !(pl->flags & DDPF_LOCAL)) continue; // This is not for clients. if(isDedicated && i == 0) continue; if(pl->flags & DDPF_FIXANGLES) { if(clearFixAnglesFlag) { pl->flags &= ~DDPF_FIXANGLES; } else { pl->clAngle = pl->mo->angle; pl->clLookDir = pl->lookDir; } } } } */ void Sv_HandlePlayerInfoFromClient(client_t* sender) { int console = Reader_ReadByte(msgReader); // ignored char oldName[PLAYERNAMELEN]; size_t len; LOG_AS("Sv_HandlePlayerInfoFromClient"); DENG_ASSERT(netBuffer.player == (sender - clients)); LOG_NET_VERBOSE("from=%i, console=%i") << netBuffer.player << console; console = netBuffer.player; strcpy(oldName, sender->name); len = Reader_ReadUInt16(msgReader); len = MIN_OF(PLAYERNAMELEN - 1, len); // there is a maximum size Reader_Read(msgReader, sender->name, len); sender->name[len] = 0; LOG_NET_NOTE("Player %s renamed to %s") << oldName << sender->name; // Relay to others. Net_SendPlayerInfo(console, DDSP_ALL_PLAYERS); } /** * Handles a server-specific network message. Assumes that Msg_BeginRead() * has already been called to begin reading the message. */ void Sv_HandlePacket(void) { ident_t id; int i, mask, from = netBuffer.player; player_t *plr = &ddPlayers[from]; ddplayer_t *ddpl = &plr->shared; client_t *sender = &clients[from]; int msgfrom; char *msg; char buf[17]; size_t len; LOG_AS("Sv_HandlePacket"); switch(netBuffer.msg.type) { case PCL_HELLO: case PCL_HELLO2: // Get the ID of the client. id = Reader_ReadUInt32(msgReader); LOG_NET_XVERBOSE("Hello from client %i (%08X)") << from << id; // Check for duplicate IDs. if(!ddpl->inGame && !sender->handshake) { // Console 0 is always reserved for the server itself (not a player). for(i = 1; i < DDMAXPLAYERS; ++i) { if(clients[i].connected && clients[i].id == id) { // Send a message to everybody. LOG_NET_WARNING("New client connection refused: duplicate ID (%08x)") << id; LOGDEV_NET_WARNING("ID conflict from=%i, i=%i") << from << i; N_TerminateClient(from); break; } } if(i < DDMAXPLAYERS) break; // Can't continue, refused! } // This is OK. sender->id = id; if(netBuffer.msg.type == PCL_HELLO2) { // Check the game mode (max 16 chars). Reader_Read(msgReader, buf, 16); if(strnicmp(buf, App_CurrentGame().identityKey().toUtf8().constData(), 16)) { LOG_NET_ERROR("Client's game ID is incompatible: %-.16s") << buf; N_TerminateClient(from); break; } } // The client requests a handshake. if(!ddpl->inGame && !sender->handshake) { // This'll be true until the client says it's ready. sender->handshake = true; // The player is now in the game. ddPlayers[from].shared.inGame = true; // Tell the game about this. gx.NetPlayerEvent(from, DDPE_ARRIVAL, 0); // Send the handshake packets. Sv_Handshake(from, true); // Note the time when the player entered. sender->enterTime = Timer_RealSeconds(); } else if(ddpl->inGame) { // The player is already in the game but requests a new // handshake. Perhaps it's starting to record a demo. Sv_Handshake(from, false); } break; case PKT_OK: // The client says it's ready to receive frames. sender->ready = true; LOG_NET_VERBOSE("OK (\"ready!\") from client %i (%08X)") << from << sender->id; if(sender->handshake) { // The handshake is complete. The client has acknowledged it // and sends its regards. sender->handshake = false; // Send a clock sync message. Msg_Begin(PSV_SYNC); Writer_WriteFloat(msgWriter, gameTime); Msg_End(); Net_SendBuffer(from, 0); // Send welcome string. Sv_SendText(from, SV_CONSOLE_PRINT_FLAGS, SV_WELCOME_STRING "\n"); } break; case PKT_CHAT: // The first byte contains the sender. msgfrom = Reader_ReadByte(msgReader); // Is the message for us? mask = Reader_ReadUInt32(msgReader); // Copy the message into a buffer. len = Reader_ReadUInt16(msgReader); msg = (char *) M_Malloc(len + 1); Reader_Read(msgReader, msg, len); msg[len] = 0; // Message for us? Show it locally. if(mask & 1) { Net_ShowChatMessage(msgfrom, msg); gx.NetPlayerEvent(msgfrom, DDPE_CHAT_MESSAGE, msg); } // Servers relay chat messages to all the recipients. Net_WriteChatMessage(msgfrom, mask, msg); for(i = 1; i < DDMAXPLAYERS; ++i) if(ddPlayers[i].shared.inGame && (mask & (1 << i)) && i != from) { Net_SendBuffer(i, 0); } M_Free(msg); break; case PCL_FINALE_REQUEST: { finaleid_t fid = Reader_ReadUInt32(msgReader); uint16_t params = Reader_ReadUInt16(msgReader); LOGDEV_NET_MSG("PCL_FINALE_REQUEST: fid=%i params=%i") << fid << params; if(params == 1) { // Skip. FI_ScriptRequestSkip(fid); } break; } case PKT_PLAYER_INFO: Sv_HandlePlayerInfoFromClient(sender); break; default: LOGDEV_NET_ERROR("Invalid value: netBuffer.msg.type = %i") << netBuffer.msg.type; break; } } /** * Handles a login packet. If the password is OK and no other client * is current logged in, a response is sent. */ void Sv_Login(void) { char password[300]; byte passLen = 0; if(netRemoteUser) { Sv_SendText(netBuffer.player, SV_CONSOLE_PRINT_FLAGS, "Sv_Login: A client is already logged in.\n"); return; } LOG_AS("Sv_Login"); // Check the password. passLen = Reader_ReadByte(msgReader); de::zap(password); Reader_Read(msgReader, password, passLen); if(strcmp(password, netPassword)) { Sv_SendText(netBuffer.player, SV_CONSOLE_PRINT_FLAGS, "Sv_Login: Invalid password.\n"); return; } // OK! netRemoteUser = netBuffer.player; LOG_NET_NOTE("%s (client %i) logged in" ) << clients[netRemoteUser].name << netRemoteUser; // Send a confirmation packet to the client. Msg_Begin(PKT_LOGIN); Writer_WriteByte(msgWriter, true); // Yes, you're logged in. Msg_End(); Net_SendBuffer(netRemoteUser, 0); } /** * Executes the command in the message buffer. * Usually sent by Con_Send. */ void Sv_ExecuteCommand(void) { int flags; byte cmdSource; unsigned short len; dd_bool silent; char *cmd = 0; LOG_AS("Sv_ExecuteCommand"); if(!netRemoteUser) { LOGDEV_NET_ERROR("Command received but no one's logged in!"); return; } // The command packet is very simple. len = Reader_ReadUInt16(msgReader); silent = (len & 0x8000) != 0; len &= 0x7fff; switch(netBuffer.msg.type) { /*case PKT_COMMAND: cmdSource = CMDS_UNKNOWN; // unknown command source. break;*/ case PKT_COMMAND2: // New format includes flags and command source. // Flags are currently unused but added for future expansion. flags = Reader_ReadUInt16(msgReader); DENG_UNUSED(flags); cmdSource = Reader_ReadByte(msgReader); break; default: DENG_ASSERT(!"Sv_ExecuteCommand: Not a command packet!"); return; } // Make a copy of the command. cmd = (char *) M_Malloc(len + 1); Reader_Read(msgReader, cmd, len); cmd[len] = 0; Con_Execute(cmdSource, cmd, silent, true); M_Free(cmd); } /** * Server's packet handler. */ void Sv_GetPackets(void) { int netconsole; client_t *sender; while(Net_GetPacket()) { Msg_BeginRead(); switch(netBuffer.msg.type) { case PCL_GOODBYE: // The client is leaving. N_TerminateClient(netBuffer.player); break; case PKT_COORDS: Sv_ClientCoords(netBuffer.player); break; case PCL_ACK_SHAKE: // The client has acknowledged our handshake. // Note the time (this isn't perfectly accurate, though). netconsole = netBuffer.player; if(netconsole >= 0 && netconsole < DDMAXPLAYERS) { sender = &clients[netconsole]; sender->shakePing = Timer_RealMilliseconds() - sender->shakePing; LOG_NET_MSG("Client %i ping at handshake: %i ms") << netconsole << sender->shakePing; } break; case PCL_ACK_PLAYER_FIX: { player_t* plr = &ddPlayers[netBuffer.player]; ddplayer_t* ddpl = &plr->shared; fixcounters_t* acked = &ddpl->fixAcked; acked->angles = Reader_ReadInt32(msgReader); acked->origin = Reader_ReadInt32(msgReader); acked->mom = Reader_ReadInt32(msgReader); LOGDEV_NET_XVERBOSE_DEBUGONLY("PCL_ACK_PLAYER_FIX: (%i) Angles %i (%i), pos %i (%i), mom %i (%i)", netBuffer.player << acked->angles << ddpl->fixCounter.angles << acked->origin << ddpl->fixCounter.origin << acked->mom << ddpl->fixCounter.mom); break; } case PKT_PING: Net_PingResponse(); break; case PCL_HELLO: case PCL_HELLO2: case PKT_OK: case PKT_CHAT: case PKT_PLAYER_INFO: case PCL_FINALE_REQUEST: Sv_HandlePacket(); break; case PKT_LOGIN: Sv_Login(); break; //case PKT_COMMAND: case PKT_COMMAND2: Sv_ExecuteCommand(); break; default: if(netBuffer.msg.type >= PKT_GAME_MARKER) { // A client has sent a game specific packet. gx.HandlePacket(netBuffer.player, netBuffer.msg.type, netBuffer.msg.data, netBuffer.length); } break; } Msg_EndRead(); } } /** * Assign a new console to the player. Returns true if successful. * Called by N_Update(). */ dd_bool Sv_PlayerArrives(unsigned int nodeID, char const *name) { LOG_AS("Sv_PlayerArrives"); LOG_NET_NOTE("'%s' has arrived") << name; // We need to find the new player a client entry. for(int i = 1; i < DDMAXPLAYERS; ++i) { client_t *cl = &clients[i]; if(!cl->connected) { player_t *plr = &ddPlayers[i]; ddplayer_t *ddpl = &plr->shared; // This'll do. cl->connected = true; cl->ready = false; cl->nodeID = nodeID; cl->viewConsole = i; cl->lastTransmit = -1; strncpy(cl->name, name, PLAYERNAMELEN); ddpl->fixAcked.angles = ddpl->fixAcked.origin = ddpl->fixAcked.mom = -1; // Clear the view filter. de::zap(ddpl->filterColor); ddpl->flags &= ~DDPF_VIEW_FILTER; Sv_InitPoolForClient(i); Smoother_Clear(cl->smoother); LOG_NET_MSG("'%s' assigned to console %i (node:%u)") << cl->name << i << nodeID; // In order to get in the game, the client must first // shake hands. It'll request this by sending a Hello packet. // We'll be waiting... cl->handshake = false; return true; } } return false; } /** * Remove the specified player from the game. Called by N_Update(). */ void Sv_PlayerLeaves(unsigned int nodeID) { int plrNum = N_IdentifyPlayer(nodeID); dd_bool wasInGame; player_t *plr; client_t *cl; if(plrNum == -1) return; // Bogus? LOG_AS("Sv_PlayerLeaves"); // Log off automatically. if(netRemoteUser == plrNum) netRemoteUser = 0; cl = &clients[plrNum]; plr = &ddPlayers[plrNum]; LOG_NET_NOTE("'%s' (console %i) has left, was connected for %.1f seconds") << cl->name << plrNum << (Timer_RealSeconds() - cl->enterTime); wasInGame = plr->shared.inGame; plr->shared.inGame = false; cl->connected = false; cl->ready = false; //cl->updateCount = 0; cl->handshake = false; cl->nodeID = 0; cl->bandwidthRating = BWR_DEFAULT; // Remove the player's data from the register. Sv_PlayerRemoved(plrNum); if(wasInGame) { // Inform the DLL about this. gx.NetPlayerEvent(plrNum, DDPE_EXIT, NULL); // Inform other clients about this. Msg_Begin(PSV_PLAYER_EXIT); Writer_WriteByte(msgWriter, plrNum); Msg_End(); Net_SendBuffer(NSP_BROADCAST, 0); } // This client no longer has an ID number. cl->id = 0; } /** * The player will be sent the introductory handshake packets. */ void Sv_Handshake(int plrNum, dd_bool newPlayer) { StringArray* ar; int i; uint playersInGame = 0; LOG_AS("Sv_Handshake"); LOG_NET_VERBOSE("Shaking hands with player %i (newPlayer:%b)") << plrNum << newPlayer; for(i = 0; i < DDMAXPLAYERS; ++i) if(clients[i].connected) playersInGame |= 1 << i; Msg_Begin(PSV_HANDSHAKE); Writer_WriteByte(msgWriter, SV_VERSION); Writer_WriteByte(msgWriter, plrNum); Writer_WriteUInt32(msgWriter, playersInGame); Writer_WriteFloat(msgWriter, gameTime); Msg_End(); Net_SendBuffer(plrNum, 0); // Include the list of material Ids. Msg_Begin(PSV_MATERIAL_ARCHIVE); MaterialArchive_Write(materialDict, msgWriter); Msg_End(); Net_SendBuffer(plrNum, 0); // Include the list of thing Ids. ar = Def_ListMobjTypeIDs(); Msg_Begin(PSV_MOBJ_TYPE_ID_LIST); StringArray_Write(ar, msgWriter); Msg_End(); Net_SendBuffer(plrNum, 0); StringArray_Delete(ar); // Include the list of state Ids. ar = Def_ListStateIDs(); Msg_Begin(PSV_MOBJ_STATE_ID_LIST); StringArray_Write(ar, msgWriter); Msg_End(); Net_SendBuffer(plrNum, 0); StringArray_Delete(ar); if(newPlayer) { // Note the time when the handshake was sent. clients[plrNum].shakePing = Timer_RealMilliseconds(); } // The game DLL wants to shake hands as well? gx.NetWorldEvent(DDWE_HANDSHAKE, plrNum, (void *) &newPlayer); // Propagate client information. for(i = 0; i < DDMAXPLAYERS; ++i) { if(clients[i].connected) { Net_SendPlayerInfo(i, plrNum); } // Send the new player's info to other players. if(newPlayer && i != 0 && i != plrNum && clients[i].connected) { Net_SendPlayerInfo(plrNum, i); } } if(!newPlayer) { // This is not a new player (just a re-handshake) but we'll // nevertheless re-init the client's state register. For new // players this is done in Sv_PlayerArrives. Sv_InitPoolForClient(plrNum); } ddPlayers[plrNum].shared.flags |= DDPF_FIXANGLES | DDPF_FIXORIGIN | DDPF_FIXMOM; } void Sv_StartNetGame(void) { int i; // Reset all the counters and other data. for(i = 0; i < DDMAXPLAYERS; ++i) { client_t *client = &clients[i]; player_t *plr = &ddPlayers[i]; ddplayer_t *ddpl = &plr->shared; ddpl->inGame = false; ddpl->flags &= ~DDPF_CAMERA; client->connected = false; client->ready = false; client->nodeID = 0; client->enterTime = 0; client->lastTransmit = -1; client->fov = 90; client->viewConsole = -1; de::zap(client->name); client->bandwidthRating = BWR_DEFAULT; Smoother_Clear(client->smoother); } gameTime = 0; firstNetUpdate = true; netRemoteUser = 0; // The server is always player number zero. consolePlayer = displayPlayer = 0; netGame = true; isServer = true; allowSending = true; // Prepare the material dictionary we'll be using with clients. materialDict = MaterialArchive_New(false); LOGDEV_NET_XVERBOSE("Prepared material dictionary with %i materials") << MaterialArchive_Count(materialDict); if(!isDedicated) { player_t *plr = &ddPlayers[consolePlayer]; ddplayer_t *ddpl = &plr->shared; client_t *cl = &clients[consolePlayer]; ddpl->inGame = true; cl->connected = true; cl->ready = true; cl->viewConsole = 0; strcpy(cl->name, playerName); } } void Sv_StopNetGame(void) { if(materialDict) { MaterialArchive_Delete(materialDict); materialDict = 0; } } unsigned int Sv_IdForMaterial(Material* mat) { assert(materialDict); return MaterialArchive_FindUniqueSerialId(materialDict, mat); } void Sv_SendText(int to, int con_flags, const char* text) { uint32_t len = MIN_OF(0xffff, strlen(text)); Msg_Begin(PSV_CONSOLE_TEXT); Writer_WriteUInt32(msgWriter, con_flags & ~CPF_TRANSMIT); Writer_WriteUInt16(msgWriter, len); Writer_Write(msgWriter, text, len); Msg_End(); Net_SendBuffer(to, 0); } /** * Asks a client to disconnect. Clients will immediately disconnect * after receiving the PSV_SERVER_CLOSE message. */ void Sv_Kick(int who) { if(!clients[who].connected) return; Sv_SendText(who, SV_CONSOLE_PRINT_FLAGS, "You were kicked out!\n"); Msg_Begin(PSV_SERVER_CLOSE); Msg_End(); Net_SendBuffer(who, 0); } /** * Sends player @a plrNum's position, momentum and/or angles override to all * clients. */ void Sv_SendPlayerFixes(int plrNum) { int fixes = 0; player_t *plr = &ddPlayers[plrNum]; ddplayer_t *ddpl = &plr->shared; if(!(ddpl->flags & (DDPF_FIXANGLES | DDPF_FIXORIGIN | DDPF_FIXMOM))) { // Nothing to fix. return; } LOG_AS("Sv_SendPlayerFixes"); // Start writing a player fix message. Msg_Begin(PSV_PLAYER_FIX); // Which player is being fixed? Writer_WriteByte(msgWriter, plrNum); // Indicate what is included in the message. if(ddpl->flags & DDPF_FIXANGLES) fixes |= 1; if(ddpl->flags & DDPF_FIXORIGIN) fixes |= 2; if(ddpl->flags & DDPF_FIXMOM) fixes |= 4; Writer_WriteUInt32(msgWriter, fixes); Writer_WriteUInt16(msgWriter, ddpl->mo->thinker.id); LOGDEV_NET_MSG("Fixing mobj %i of player %i") << ddpl->mo->thinker.id << plrNum; // Increment counters. if(ddpl->flags & DDPF_FIXANGLES) { Writer_WriteInt32(msgWriter, ++ddpl->fixCounter.angles); Writer_WriteUInt32(msgWriter, ddpl->mo->angle); Writer_WriteFloat(msgWriter, ddpl->lookDir); LOGDEV_NET_MSG("Sent angles (%i): angle=%x lookdir=%.2f") << ddpl->fixCounter.angles << ddpl->mo->angle << ddpl->lookDir; } if(ddpl->flags & DDPF_FIXORIGIN) { Writer_WriteInt32(msgWriter, ++ddpl->fixCounter.origin); Writer_WriteFloat(msgWriter, ddpl->mo->origin[VX]); Writer_WriteFloat(msgWriter, ddpl->mo->origin[VY]); Writer_WriteFloat(msgWriter, ddpl->mo->origin[VZ]); LOGDEV_NET_MSG("Sent position (%i): %s") << ddpl->fixCounter.origin << Vector3d(ddpl->mo->origin).asText(); } if(ddpl->flags & DDPF_FIXMOM) { Writer_WriteInt32(msgWriter, ++ddpl->fixCounter.mom); Writer_WriteFloat(msgWriter, ddpl->mo->mom[MX]); Writer_WriteFloat(msgWriter, ddpl->mo->mom[MY]); Writer_WriteFloat(msgWriter, ddpl->mo->mom[MZ]); LOGDEV_NET_MSG("Sent momentum (%i): %s") << ddpl->fixCounter.mom << Vector3d(ddpl->mo->mom).asText(); } Msg_End(); // Send the fix message to everyone. Net_SendBuffer(DDSP_ALL_PLAYERS, 0); ddpl->flags &= ~(DDPF_FIXANGLES | DDPF_FIXORIGIN | DDPF_FIXMOM); LOGDEV_NET_VERBOSE("Cleared FIX flags of player %i") << plrNum; // Clear the smoother for this client. if(clients[plrNum].smoother) { Smoother_Clear(clients[plrNum].smoother); } } void Sv_Ticker(timespan_t ticLength) { int i; if(!isDedicated) return; // Note last angles for all players. for(i = 0; i < DDMAXPLAYERS; ++i) { player_t *plr = &ddPlayers[i]; if(!plr->shared.inGame || !plr->shared.mo) continue; // Update the smoother? if(clients[i].smoother) { Smoother_Advance(clients[i].smoother, ticLength); } if(DD_IsSharpTick()) { plr->shared.lastAngle = plr->shared.mo->angle; } /* if(clients[i].bwrAdjustTime > 0) { // BWR adjust time tics away. clients[i].bwrAdjustTime--; }*/ // Increment counter, send new data. Sv_SendPlayerFixes(i); } } /** * @return The number of players in the game. */ int Sv_GetNumPlayers(void) { int i, count; // Clients can't count. if(isClient) return 1; for(i = count = 0; i < DDMAXPLAYERS; ++i) { player_t *plr = &ddPlayers[i]; if(plr->shared.inGame && plr->shared.mo) count++; } return count; } /** * @return The number of connected clients. */ int Sv_GetNumConnected(void) { int i, count = 0; // Clients can't count. if(isClient) return 1; for(i = isDedicated ? 1 : 0; i < DDMAXPLAYERS; ++i) if(clients[i].connected) count++; return count; } /** * The bandwidth rating is updated according to the status of the * player's send queue. Returns true if a new packet may be sent. * * @todo This functionality needs to be restored: servers can't simply output * an arbitrary amount of data to clients with no regard to the available * bandwidth. */ dd_bool Sv_CheckBandwidth(int /*playerNumber*/) { return true; /* client_t *client = &clients[playerNumber]; uint qSize = N_GetSendQueueSize(playerNumber); uint limit = 400; return true; // If there are too many messages in the queue, the player's bandwidth // is overrated. if(qSize > limit) { // Drop quickly to allow the send queue to clear out sooner. client->bandwidthRating -= 10; } // If the send queue is practically empty, we can use more bandwidth. // (Providing we have BWR adjust time.) if(qSize < limit / 20 && client->bwrAdjustTime > 0) { client->bandwidthRating++; // Increase BWR only once during the adjust time. client->bwrAdjustTime = 0; } // Do not go past the boundaries, though. if(client->bandwidthRating < 0) { client->bandwidthRating = 0; } if(client->bandwidthRating > MAX_BANDWIDTH_RATING) { client->bandwidthRating = MAX_BANDWIDTH_RATING; } // New messages will not be sent if there's too much already. return qSize <= 10 * limit; */ } /** * Reads a PKT_COORDS packet from the message buffer. We trust the * client's position and change ours to match it. The client better not * be cheating. */ void Sv_ClientCoords(int plrNum) { player_t *plr = &ddPlayers[plrNum]; ddplayer_t *ddpl = &plr->shared; mobj_t *mo = ddpl->mo; int clz; float clientGameTime; float clientPos[3]; angle_t clientAngle; float clientLookDir; dd_bool onFloor = false; // If mobj or player is invalid, the message is discarded. if(!mo || !ddpl->inGame || (ddpl->flags & DDPF_DEAD)) return; clientGameTime = Reader_ReadFloat(msgReader); clientPos[VX] = Reader_ReadFloat(msgReader); clientPos[VY] = Reader_ReadFloat(msgReader); clz = Reader_ReadInt32(msgReader); if(clz == DDMININT) { clientPos[VZ] = mo->floorZ; onFloor = true; } else { clientPos[VZ] = FIX2FLT(clz); } // The angles. clientAngle = ((angle_t) Reader_ReadUInt16(msgReader)) << 16; clientLookDir = P_ShortToLookDir(Reader_ReadInt16(msgReader)); // Movement intent. ddpl->forwardMove = FIX2FLT(Reader_ReadChar(msgReader) << 13); ddpl->sideMove = FIX2FLT(Reader_ReadChar(msgReader) << 13); if(ddpl->fixCounter.angles == ddpl->fixAcked.angles && !(ddpl->flags & DDPF_FIXANGLES)) { LOGDEV_NET_XVERBOSE_DEBUGONLY("Sv_ClientCoords: Setting angles for player %i: %x, %f", plrNum << clientAngle << clientLookDir); mo->angle = clientAngle; ddpl->lookDir = clientLookDir; } LOGDEV_NET_XVERBOSE_DEBUGONLY("Sv_ClientCoords: Received coords for player %i: %f, %f, %f", plrNum << clientPos[VX] << clientPos[VY] << clientPos[VZ]); // If we aren't about to forcibly change the client's position, update // with new pos if it's valid. But it must be a valid pos. if(Sv_CanTrustClientPos(plrNum)) { LOGDEV_NET_XVERBOSE_DEBUGONLY("Sv_ClientCoords: Setting coords for player %i: %f, %f, %f", plrNum << clientPos[VX] << clientPos[VY] << clientPos[VZ]); Smoother_AddPos(clients[plrNum].smoother, clientGameTime, clientPos[VX], clientPos[VY], clientPos[VZ], onFloor); } } dd_bool Sv_CanTrustClientPos(int plrNum) { player_t* plr = &ddPlayers[plrNum]; ddplayer_t* ddpl = &plr->shared; if(ddpl->fixCounter.origin == ddpl->fixAcked.origin && !(ddpl->flags & DDPF_FIXORIGIN)) { return true; } // Server's position is valid, client is not up-to-date. return false; } /** * Console command for terminating a remote console connection. */ D_CMD(Logout) { DENG2_UNUSED3(src, argc, argv); // Only servers can execute this command. if(!netRemoteUser || !isServer) return false; // Notice that the server WILL execute this command when a client // is logged in and types "logout". Sv_SendText(netRemoteUser, SV_CONSOLE_PRINT_FLAGS, "Goodbye...\n"); // Send a logout packet. Msg_Begin(PKT_LOGIN); Writer_WriteByte(msgWriter, false); // You're outta here. Msg_End(); Net_SendBuffer(netRemoteUser, 0); netRemoteUser = 0; return true; } DENG_DECLARE_API(Server) = { { DE_API_SERVER }, Sv_CanTrustClientPos }; doomsday-stable-1.15.7/doomsday/server/src/server/sv_infine.cpp0000664000175000017500000000307712641367671024112 0ustar jaakkojaakko/** @file sv_infine.cpp Server-side InFine. * @ingroup server * * @authors Copyright © 2003-2010 Jaakko Keränen * @authors Copyright © 2005-2010 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_network.h" #include "ui/infine/finale.h" void Sv_Finale(finaleid_t id, int flags, char const *script) { size_t scriptLen = 0; if(isClient) return; // How much memory do we need? if(script) { flags |= FINF_SCRIPT; scriptLen = strlen(script); } // First the flags. Msg_Begin(PSV_FINALE); Writer_WriteByte(msgWriter, flags); Writer_WriteUInt32(msgWriter, id); // serverside Id if(script) { // Then the script itself. Writer_WriteUInt32(msgWriter, scriptLen); Writer_Write(msgWriter, script, scriptLen); } Msg_End(); Net_SendBuffer(NSP_BROADCAST, 0); } doomsday-stable-1.15.7/doomsday/server/src/server/sv_pool.cpp0000664000175000017500000024414512641367671023616 0ustar jaakkojaakko/** @file sv_pool.cpp Delta Pools * @ingroup server * * Delta Pools use PU_MAP, which means all the memory allocated for them * is deallocated when the map changes. Sv_InitPools() is called in * R_SetupMap() to clear out all the old data. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include #include #include "de_base.h" #include "de_console.h" #include "de_network.h" #include "de_play.h" #include "audio/s_main.h" #include "world/thinkers.h" #include "server/sv_pool.h" #include using namespace de; #define DEFAULT_DELTA_BASE_SCORE 10000 #define REG_MOBJ_HASH_SIZE 1024 #define REG_MOBJ_HASH_FUNCTION_MASK 0x3ff // Maximum difference in plane height where the absolute height doesn't // need to be sent. #define PLANE_SKIP_LIMIT (40) typedef struct reg_mobj_s { // Links to next and prev mobj in the register hash. struct reg_mobj_s* next, *prev; // The tic when the mobj state was last sent. dt_mobj_t mo; // The state of the mobj. } reg_mobj_t; typedef struct mobjhash_s { reg_mobj_t* first, *last; } mobjhash_t; /** * One cregister_t holds the state of the entire world. */ typedef struct cregister_s { // The time the register was last updated. int gametic; // True if this register contains a read-only copy of the initial state // of the world. dd_bool isInitial; // The mobjs are stored in a hash for efficiency (ID is the key). mobjhash_t mobjs[REG_MOBJ_HASH_SIZE]; dt_player_t ddPlayers[DDMAXPLAYERS]; dt_sector_t* sectors; dt_side_t* sides; dt_poly_t* polyObjs; } cregister_t; void Sv_RegisterWorld(cregister_t* reg, dd_bool isInitial); void Sv_NewDelta(void* deltaPtr, deltatype_t type, uint id); dd_bool Sv_IsVoidDelta(const void* delta); void Sv_PoolQueueClear(pool_t* pool); void Sv_GenerateNewDeltas(cregister_t* reg, int clientNumber, dd_bool doUpdate); // The register contains the previous state of the world. cregister_t worldRegister; // The initial register is used when generating deltas for a new client. cregister_t initialRegister; // Each client has its own pool for deltas. pool_t pools[DDMAXPLAYERS]; static float deltaBaseScores[NUM_DELTA_TYPES]; // Keep this zeroed out. Used if the register doesn't have data for // the mobj being compared. static ThinkerT dummyZeroMobj; static inline WorldSystem &worldSys() { return App_WorldSystem(); } /** * Called once for each map, from R_SetupMap(). Initialize the world * register and drain all pools. */ void Sv_InitPools(void) { de::Time startedAt; uint i; // Clients don't register anything. if(isClient) return; LOG_AS("Sv_InitPools"); // Set base priority scores for all the delta types. for(i = 0; i < NUM_DELTA_TYPES; ++i) { deltaBaseScores[i] = DEFAULT_DELTA_BASE_SCORE; } // Priorities for all deltas that will be sent out by the server. // No priorities need to be declared for obsolete delta types. deltaBaseScores[DT_MOBJ] = 1000; deltaBaseScores[DT_PLAYER] = 1000; deltaBaseScores[DT_SECTOR] = 2000; deltaBaseScores[DT_SIDE] = 800; deltaBaseScores[DT_POLY] = 2000; deltaBaseScores[DT_LUMP] = 0; deltaBaseScores[DT_SOUND] = 2000; deltaBaseScores[DT_MOBJ_SOUND] = 3000; deltaBaseScores[DT_SECTOR_SOUND] = 5000; deltaBaseScores[DT_SIDE_SOUND] = 5500; deltaBaseScores[DT_POLY_SOUND] = 5000; // Since the map has changed, PU_MAP memory has been freed. // Reset all pools (set numbers are kept, though). for(i = 0; i < DDMAXPLAYERS; ++i) { pools[i].owner = i; pools[i].resendDealer = 1; de::zap(pools[i].hash); de::zap(pools[i].misHash); pools[i].queueSize = 0; pools[i].allocatedSize = 0; pools[i].queue = NULL; // This will be set to false when a frame is sent. pools[i].isFirst = true; } // Store the current state of the world into both the registers. Sv_RegisterWorld(&worldRegister, false); Sv_RegisterWorld(&initialRegister, true); // How much time did we spend? LOG_MAP_VERBOSE("World registered in %.2f seconds") << startedAt.since(); } /** * Called during server shutdown (when shutting down the engine). */ void Sv_ShutdownPools(void) { // Nothing to do. } /** * Called when a client joins the game. */ void Sv_InitPoolForClient(uint clientNumber) { // Free everything that might exist in the pool. Sv_DrainPool(clientNumber); // Generate deltas by comparing against the initial state of the world. // The initial register remains unmodified. Sv_GenerateNewDeltas(&initialRegister, clientNumber, false); // No frames have yet been sent for this client. // The first frame is processed a bit more thoroughly than the others // (e.g. *all* sides are compared, not just a portion). pools[clientNumber].isFirst = true; } /** * @return Pointer to the console's delta pool. */ pool_t* Sv_GetPool(uint consoleNumber) { return &pools[consoleNumber]; } /** * The hash function for the register mobj hash. */ uint Sv_RegisterHashFunction(thid_t id) { return (uint) id & REG_MOBJ_HASH_FUNCTION_MASK; } /** * @return Pointer to the register-mobj, if it already exists. */ reg_mobj_t* Sv_RegisterFindMobj(cregister_t* reg, thid_t id) { mobjhash_t* hash = ®->mobjs[Sv_RegisterHashFunction(id)]; reg_mobj_t* iter; // See if there already is a register-mobj for this id. for(iter = hash->first; iter; iter = iter->next) { // Is this the one? if(iter->mo.thinker.id == id) { return iter; } } return NULL; } /** * Adds a new reg_mobj_t to the register's mobj hash. */ reg_mobj_t* Sv_RegisterAddMobj(cregister_t* reg, thid_t id) { mobjhash_t* hash = ®->mobjs[Sv_RegisterHashFunction(id)]; reg_mobj_t* newRegMo; // Try to find an existing register-mobj. if((newRegMo = Sv_RegisterFindMobj(reg, id)) != NULL) { return newRegMo; } // Allocate the new register-mobj. newRegMo = (reg_mobj_t *) Z_Calloc(sizeof(reg_mobj_t), PU_MAP, 0); // Link it to the end of the hash list. if(hash->last) { hash->last->next = newRegMo; newRegMo->prev = hash->last; } hash->last = newRegMo; if(!hash->first) { hash->first = newRegMo; } return newRegMo; } /** * Removes a reg_mobj_t from the register's mobj hash. */ void Sv_RegisterRemoveMobj(cregister_t* reg, reg_mobj_t* regMo) { mobjhash_t* hash = ®->mobjs[Sv_RegisterHashFunction(regMo->mo.thinker.id)]; // Update the first and last links. if(hash->last == regMo) { hash->last = regMo->prev; } if(hash->first == regMo) { hash->first = regMo->next; } // Link out of the list. if(regMo->next) { regMo->next->prev = regMo->prev; } if(regMo->prev) { regMo->prev->next = regMo->next; } // Destroy the register-mobj. Z_Free(regMo); } /** * @return If the mobj is on the floor; @c MININT. * If the mobj is touching the ceiling; @c MAXINT. * Otherwise returns the Z coordinate. */ float Sv_GetMaxedMobjZ(const mobj_t* mo) { // No maxing for now. /* if(mo->origin[VZ] == mo->floorZ) { return DDMINFLOAT; } if(mo->origin[VZ] + mo->height == mo->ceilingZ) { return DDMAXFLOAT; } */ return mo->origin[VZ]; } /** * Store the state of the mobj into the register-mobj. * Called at register init and after each delta generation cycle. */ void Sv_RegisterMobj(dt_mobj_t *reg, mobj_t const *mo) { // (dt_mobj_t <=> mobj_t) // Just copy the data we need. reg->thinker.id = mo->thinker.id; reg->type = mo->type; reg->dPlayer = mo->dPlayer; reg->_bspLeaf = mo->_bspLeaf; reg->origin[VX] = mo->origin[VX]; reg->origin[VY] = mo->origin[VY]; reg->origin[VZ] = Sv_GetMaxedMobjZ(mo); reg->floorZ = mo->floorZ; reg->ceilingZ = mo->ceilingZ; reg->mom[MX] = mo->mom[MX]; reg->mom[MY] = mo->mom[MY]; reg->mom[MZ] = mo->mom[MZ]; reg->angle = mo->angle; reg->selector = mo->selector; reg->state = mo->state; reg->radius = mo->radius; reg->height = mo->height; reg->ddFlags = mo->ddFlags; reg->flags = mo->flags; reg->flags2 = mo->flags2; reg->flags3 = mo->flags3; reg->health = mo->health; reg->floorClip = mo->floorClip; reg->translucency = mo->translucency; reg->visTarget = mo->visTarget; } /** * Reset the data of the registered mobj to reasonable defaults. * In effect, forces a resend of the zeroed entries as deltas. */ void Sv_RegisterResetMobj(dt_mobj_t* reg) { reg->origin[VX] = DDMINFLOAT; reg->origin[VY] = DDMINFLOAT; reg->origin[VZ] = -1000000; reg->angle = 0; reg->type = -1; reg->selector = 0; reg->state = 0; reg->radius = -1; reg->height = -1; reg->ddFlags = 0; reg->flags = 0; reg->flags2 = 0; reg->flags3 = 0; reg->health = 0; reg->floorClip = 0; reg->translucency = 0; reg->visTarget = 0; } /** * Store the state of the player into the register-player. * Called at register init and after each delta generation cycle. */ void Sv_RegisterPlayer(dt_player_t* reg, uint number) { #define FMAKERGBA(r,g,b,a) ( (byte)(0xff*r) + ((byte)(0xff*g)<<8) + ((byte)(0xff*b)<<16) + ((byte)(0xff*a)<<24) ) player_t* plr = &ddPlayers[number]; ddplayer_t* ddpl = &plr->shared; //client_t* c = &clients[number]; reg->mobj = (ddpl->mo ? ddpl->mo->thinker.id : 0); reg->forwardMove = 0; reg->sideMove = 0; reg->angle = (ddpl->mo ? ddpl->mo->angle : 0); reg->turnDelta = (ddpl->mo ? ddpl->mo->angle - ddpl->lastAngle : 0); reg->friction = ddpl->mo && (gx.MobjFriction ? gx.MobjFriction(ddpl->mo) : DEFAULT_FRICTION); reg->extraLight = ddpl->extraLight; reg->fixedColorMap = ddpl->fixedColorMap; if(ddpl->flags & DDPF_VIEW_FILTER) { reg->filter = FMAKERGBA(ddpl->filterColor[CR], ddpl->filterColor[CG], ddpl->filterColor[CB], ddpl->filterColor[CA]); } else { reg->filter = 0; } reg->clYaw = (ddpl->mo ? ddpl->mo->angle : 0); reg->clPitch = ddpl->lookDir; memcpy(reg->psp, ddpl->pSprites, sizeof(ddpsprite_t) * 2); #undef FMAKERGBA } /** * Store the state of the sector into the register-sector. * Called at register init and after each delta generation. * * @param reg The sector register to be initialized. * @param number The world sector number to be registered. */ void Sv_RegisterSector(dt_sector_t *reg, int number) { DENG2_ASSERT(reg); Sector §or = worldSys().map().sector(number); reg->lightLevel = sector.lightLevel(); for(int i = 0; i < 3; ++i) { reg->rgb[i] = sector.lightColor()[i]; } // @todo $nplanes for(int i = 0; i < 2; ++i) // number of planes in sector. { Plane const &plane = sector.plane(i); // Plane properties reg->planes[i].height = plane.height(); reg->planes[i].target = plane.targetHeight(); reg->planes[i].speed = plane.speed(); // Surface properties. Surface const &surface = plane.surface(); Vector3f const &tintColor = surface.tintColor(); for(int c = 0; c < 3; ++c) { reg->planes[i].surface.rgba[c] = tintColor[c]; } reg->planes[i].surface.rgba[CA] = surface.opacity(); reg->planes[i].surface.material = surface.materialPtr(); } } /** * Store the state of the side into the register-side. * Called at register init and after each delta generation. */ void Sv_RegisterSide(dt_side_t *reg, int number) { DENG2_ASSERT(reg != 0); LineSide *side = worldSys().map().sidePtr(number); if(side->hasSections()) { reg->top.material = side->top().materialPtr(); reg->middle.material = side->middle().materialPtr(); reg->bottom.material = side->bottom().materialPtr(); for(int c = 0; c < 3; ++c) { reg->middle.rgba[c] = side->middle().tintColor()[c]; reg->bottom.rgba[c] = side->bottom().tintColor()[c]; reg->top.rgba[c] = side->top().tintColor()[c]; } // Only middle sections support blending. reg->middle.rgba[CA] = side->middle().opacity(); reg->middle.blendMode = side->middle().blendMode(); } reg->lineFlags = side->line().flags() & 0xff; reg->flags = side->flags() & 0xff; } /** * Store the state of the polyobj into the register-poly. * Called at register init and after each delta generation. */ void Sv_RegisterPoly(dt_poly_t *reg, uint number) { DENG_ASSERT(reg); Polyobj const &pob = worldSys().map().polyobj(number); reg->dest[VX] = pob.dest[VX]; reg->dest[VY] = pob.dest[VY]; reg->speed = pob.speed; reg->destAngle = pob.destAngle; reg->angleSpeed = pob.angleSpeed; } /** * @return @c true if the result is not void. */ dd_bool Sv_RegisterCompareMobj(cregister_t *reg, mobj_t const *s, mobjdelta_t *d) { int df; reg_mobj_t *regMo = 0; dt_mobj_t const *r = dummyZeroMobj; if((regMo = Sv_RegisterFindMobj(reg, s->thinker.id)) != NULL) { // Use the registered data. r = ®Mo->mo; df = 0; } else { // This didn't exist in the register, so it's a new mobj. df = MDFC_CREATE | MDF_EVERYTHING | MDFC_TYPE; } if(r->origin[VX] != s->origin[VX]) df |= MDF_ORIGIN_X; if(r->origin[VY] != s->origin[VY]) df |= MDF_ORIGIN_Y; if(r->origin[VZ] != Sv_GetMaxedMobjZ(s) || r->floorZ != s->floorZ || r->ceilingZ != s->ceilingZ) { df |= MDF_ORIGIN_Z; if(!(df & MDFC_CREATE) && s->origin[VZ] <= s->floorZ) { // It is currently on the floor. The client will place it on its // clientside floor and disregard the Z coordinate. df |= MDFC_ON_FLOOR; } } if(r->mom[MX] != s->mom[MX]) df |= MDF_MOM_X; if(r->mom[MY] != s->mom[MY]) df |= MDF_MOM_Y; if(r->mom[MZ] != s->mom[MZ]) df |= MDF_MOM_Z; if(r->angle != s->angle) df |= MDF_ANGLE; if(r->selector != s->selector) df |= MDF_SELECTOR; if(r->translucency != s->translucency) df |= MDFC_TRANSLUCENCY; if(r->visTarget != s->visTarget) df |= MDFC_FADETARGET; if(r->type != s->type) df |= MDFC_TYPE; // Mobj state sent periodically, if the sequence keeps changing. if(regMo && !Def_SameStateSequence(s->state, r->state)) { df |= MDF_STATE; if(s->state == NULL) { // No valid comparison can be generated because the mobj is gone. return false; } } if(r->radius != s->radius) df |= MDF_RADIUS; if(r->height != s->height) df |= MDF_HEIGHT; if((r->ddFlags & DDMF_PACK_MASK) != (s->ddFlags & DDMF_PACK_MASK) || r->flags != s->flags || r->flags2 != s->flags2 || r->flags3 != s->flags3) { df |= MDF_FLAGS; } if(r->health != s->health) df |= MDF_HEALTH; if(r->floorClip != s->floorClip) df |= MDF_FLOORCLIP; if(df) { // Init the delta with current data. Sv_NewDelta(d, DT_MOBJ, s->thinker.id); Sv_RegisterMobj(&d->mo, s); } d->delta.flags = df; return !Sv_IsVoidDelta(d); } /** * @return @c true, if the result is not void. */ dd_bool Sv_RegisterComparePlayer(cregister_t* reg, uint number, playerdelta_t* d) { const dt_player_t* r = ®->ddPlayers[number]; dt_player_t* s = &d->player; int df = 0; // Init the delta with current data. Sv_NewDelta(d, DT_PLAYER, number); Sv_RegisterPlayer(&d->player, number); // Determine which data is different. if(r->mobj != s->mobj) df |= PDF_MOBJ; if(r->forwardMove != s->forwardMove) df |= PDF_FORWARDMOVE; if(r->sideMove != s->sideMove) df |= PDF_SIDEMOVE; if(r->turnDelta != s->turnDelta) df |= PDF_TURNDELTA; if(r->friction != s->friction) df |= PDF_FRICTION; if(r->extraLight != s->extraLight || r->fixedColorMap != s->fixedColorMap) df |= PDF_EXTRALIGHT; if(r->filter != s->filter) df |= PDF_FILTER; d->delta.flags = df; return !Sv_IsVoidDelta(d); } /** * @return @c true if the result is not void. */ dd_bool Sv_RegisterCompareSector(cregister_t *reg, int number, sectordelta_t *d, byte doUpdate) { DENG2_ASSERT(reg && d); dt_sector_t *r = ®->sectors[number]; Sector const &s = worldSys().map().sector(number); int df = 0; // Determine which data is different. if(s.floorSurface().materialPtr() != r->planes[PLN_FLOOR].surface.material) df |= SDF_FLOOR_MATERIAL; if(s.ceilingSurface().materialPtr() != r->planes[PLN_CEILING].surface.material) df |= SDF_CEILING_MATERIAL; if(r->lightLevel != s.lightLevel()) df |= SDF_LIGHT; if(r->rgb[0] != s.lightColor().x) df |= SDF_COLOR_RED; if(r->rgb[1] != s.lightColor().y) df |= SDF_COLOR_GREEN; if(r->rgb[2] != s.lightColor().z) df |= SDF_COLOR_BLUE; if(r->planes[PLN_FLOOR].surface.rgba[0] != s.floorSurface().tintColor().x) df |= SDF_FLOOR_COLOR_RED; if(r->planes[PLN_FLOOR].surface.rgba[1] != s.floorSurface().tintColor().y) df |= SDF_FLOOR_COLOR_GREEN; if(r->planes[PLN_FLOOR].surface.rgba[2] != s.floorSurface().tintColor().z) df |= SDF_FLOOR_COLOR_BLUE; if(r->planes[PLN_CEILING].surface.rgba[0] != s.ceilingSurface().tintColor().x) df |= SDF_CEIL_COLOR_RED; if(r->planes[PLN_CEILING].surface.rgba[1] != s.ceilingSurface().tintColor().y) df |= SDF_CEIL_COLOR_GREEN; if(r->planes[PLN_CEILING].surface.rgba[2] != s.ceilingSurface().tintColor().z) df |= SDF_CEIL_COLOR_BLUE; // The cases where an immediate change to a plane's height is needed: // 1) Plane is not moving, but the heights are different. This means // the plane's height was changed unpredictably. // 2) Plane is moving, but there is a large difference in the heights. // The clientside height should be fixed. // Should we make an immediate change in floor height? if(FEQUAL(r->planes[PLN_FLOOR].speed, 0) && FEQUAL(s.floor().speed(), 0)) { if(!FEQUAL(r->planes[PLN_FLOOR].height, s.floor().height())) df |= SDF_FLOOR_HEIGHT; } else { if(fabs(r->planes[PLN_FLOOR].height - s.floor().height()) > PLANE_SKIP_LIMIT) df |= SDF_FLOOR_HEIGHT; } // How about the ceiling? if(FEQUAL(r->planes[PLN_CEILING].speed, 0) && FEQUAL(s.ceiling().speed(), 0)) { if(!FEQUAL(r->planes[PLN_CEILING].height, s.ceiling().height())) df |= SDF_CEILING_HEIGHT; } else { if(fabs(r->planes[PLN_CEILING].height - s.ceiling().height()) > PLANE_SKIP_LIMIT) df |= SDF_CEILING_HEIGHT; } // Check planes, too. if(!FEQUAL(r->planes[PLN_FLOOR].target, s.floor().targetHeight())) { // Target and speed are always sent together. df |= SDF_FLOOR_TARGET | SDF_FLOOR_SPEED; } if(!FEQUAL(r->planes[PLN_FLOOR].speed, s.floor().speed())) { // Target and speed are always sent together. df |= SDF_FLOOR_SPEED | SDF_FLOOR_TARGET; } if(!FEQUAL(r->planes[PLN_CEILING].target, s.ceiling().targetHeight())) { // Target and speed are always sent together. df |= SDF_CEILING_TARGET | SDF_CEILING_SPEED; } if(!FEQUAL(r->planes[PLN_CEILING].speed, s.ceiling().speed())) { // Target and speed are always sent together. df |= SDF_CEILING_SPEED | SDF_CEILING_TARGET; } #ifdef _DEBUG if(df & (SDF_CEILING_HEIGHT | SDF_CEILING_SPEED | SDF_CEILING_TARGET)) { LOGDEV_NET_XVERBOSE("Sector %i: ceiling state change noted (target = %f)") << number << s.ceiling().targetHeight(); } #endif // Only do a delta when something changes. if(df) { // Init the delta with current data. Sv_NewDelta(d, DT_SECTOR, number); Sv_RegisterSector(&d->sector, number); if(doUpdate) { Sv_RegisterSector(r, number); } } if(doUpdate) { // The plane heights should be tracked regardless of the // change flags. r->planes[PLN_FLOOR].height = s.floor().height(); r->planes[PLN_CEILING].height = s.ceiling().height(); } d->delta.flags = df; return !Sv_IsVoidDelta(d); } /** * @return @c true= the result is not void. */ dd_bool Sv_RegisterCompareSide(cregister_t *reg, uint number, sidedelta_t *d, byte doUpdate) { LineSide const *side = worldSys().map().sidePtr(number); dt_side_t *r = ®->sides[number]; byte lineFlags = side->line().flags() & 0xff; byte sideFlags = side->flags() & 0xff; int df = 0; if(side->hasSections()) { if(!side->top().hasFixMaterial() && r->top.material != side->top().materialPtr()) { df |= SIDF_TOP_MATERIAL; if(doUpdate) r->top.material = side->top().materialPtr(); } if(!side->middle().hasFixMaterial() && r->middle.material != side->middle().materialPtr()) { df |= SIDF_MID_MATERIAL; if(doUpdate) r->middle.material = side->middle().materialPtr(); } if(!side->bottom().hasFixMaterial() && r->bottom.material != side->bottom().materialPtr()) { df |= SIDF_BOTTOM_MATERIAL; if(doUpdate) r->bottom.material = side->bottom().materialPtr(); } if(r->top.rgba[0] != side->top().tintColor().x) { df |= SIDF_TOP_COLOR_RED; if(doUpdate) r->top.rgba[0] = side->top().tintColor().x; } if(r->top.rgba[1] != side->top().tintColor().y) { df |= SIDF_TOP_COLOR_GREEN; if(doUpdate) r->top.rgba[1] = side->top().tintColor().y; } if(r->top.rgba[2] != side->top().tintColor().z) { df |= SIDF_TOP_COLOR_BLUE; if(doUpdate) r->top.rgba[3] = side->top().tintColor().z; } if(r->middle.rgba[0] != side->middle().tintColor().x) { df |= SIDF_MID_COLOR_RED; if(doUpdate) r->middle.rgba[0] = side->middle().tintColor().x; } if(r->middle.rgba[1] != side->middle().tintColor().y) { df |= SIDF_MID_COLOR_GREEN; if(doUpdate) r->middle.rgba[1] = side->middle().tintColor().y; } if(r->middle.rgba[2] != side->middle().tintColor().z) { df |= SIDF_MID_COLOR_BLUE; if(doUpdate) r->middle.rgba[3] = side->middle().tintColor().z; } if(r->middle.rgba[3] != side->middle().opacity()) { df |= SIDF_MID_COLOR_ALPHA; if(doUpdate) r->middle.rgba[3] = side->middle().opacity(); } if(r->bottom.rgba[0] != side->bottom().tintColor().x) { df |= SIDF_BOTTOM_COLOR_RED; if(doUpdate) r->bottom.rgba[0] = side->bottom().tintColor().x; } if(r->bottom.rgba[1] != side->bottom().tintColor().y) { df |= SIDF_BOTTOM_COLOR_GREEN; if(doUpdate) r->bottom.rgba[1] = side->bottom().tintColor().y; } if(r->bottom.rgba[2] != side->bottom().tintColor().z) { df |= SIDF_BOTTOM_COLOR_BLUE; if(doUpdate) r->bottom.rgba[3] = side->bottom().tintColor().z; } if(r->middle.blendMode != side->middle().blendMode()) { df |= SIDF_MID_BLENDMODE; if(doUpdate) r->middle.blendMode = side->middle().blendMode(); } } if(r->lineFlags != lineFlags) { df |= SIDF_LINE_FLAGS; if(doUpdate) r->lineFlags = lineFlags; } if(r->flags != sideFlags) { df |= SIDF_FLAGS; if(doUpdate) r->flags = sideFlags; } // Was there any change? if(df) { // This happens quite rarely. // Init the delta with current data. Sv_NewDelta(d, DT_SIDE, number); Sv_RegisterSide(&d->side, number); } d->delta.flags = df; return !Sv_IsVoidDelta(d); } /** * @return @c true, if the result is not void. */ dd_bool Sv_RegisterComparePoly(cregister_t* reg, int number, polydelta_t *d) { const dt_poly_t* r = ®->polyObjs[number]; dt_poly_t* s = &d->po; int df = 0; // Init the delta with current data. Sv_NewDelta(d, DT_POLY, number); Sv_RegisterPoly(&d->po, number); // What is different? if(r->dest[VX] != s->dest[VX]) df |= PODF_DEST_X; if(r->dest[VY] != s->dest[VY]) df |= PODF_DEST_Y; if(r->speed != s->speed) df |= PODF_SPEED; if(r->destAngle != s->destAngle) df |= PODF_DEST_ANGLE; if(r->angleSpeed != s->angleSpeed) df |= PODF_ANGSPEED; d->delta.flags = df; return !Sv_IsVoidDelta(d); } /** * Returns @c true if the map-object can be excluded from delta processing. */ dd_bool Sv_IsMobjIgnored(mobj_t const &mob) { return (mob.ddFlags & DDMF_LOCAL) != 0; } /** * Returns @c true if the player can be excluded from delta processing. */ dd_bool Sv_IsPlayerIgnored(dint plrNum) { return !ddPlayers[plrNum].shared.inGame; } /** * Initialize the register with the current state of the world. * The arrays are allocated and the data is copied, nothing else is done. * * An initial register doesn't contain any mobjs. When new clients enter, * they know nothing about any mobjs. If the mobjs were included in the * initial register, clients wouldn't receive much info from mobjs that * haven't moved since the beginning. */ void Sv_RegisterWorld(cregister_t *reg, dd_bool isInitial) { DENG_ASSERT(reg != 0); Map &map = worldSys().map(); de::zapPtr(reg); reg->gametic = SECONDS_TO_TICKS(gameTime); // Is this the initial state? reg->isInitial = isInitial; // Init sectors. reg->sectors = (dt_sector_t *) Z_Calloc(sizeof(*reg->sectors) * map.sectorCount(), PU_MAP, 0); for(int i = 0; i < map.sectorCount(); ++i) { Sv_RegisterSector(®->sectors[i], i); } // Init sides. reg->sides = (dt_side_t *) Z_Calloc(sizeof(*reg->sides) * map.sideCount(), PU_MAP, 0); for(int i = 0; i < map.sideCount(); ++i) { Sv_RegisterSide(®->sides[i], i); } // Init polyobjs. int numPolyobjs = map.polyobjCount(); if(numPolyobjs) { reg->polyObjs = (dt_poly_t *) Z_Calloc(sizeof(*reg->polyObjs) * map.polyobjCount(), PU_MAP, 0); for(int i = 0; i < numPolyobjs; ++i) { Sv_RegisterPoly(®->polyObjs[i], i); } } else { reg->polyObjs = NULL; } } /** * Update the pool owner's info. */ void Sv_UpdateOwnerInfo(pool_t *pool) { player_t *plr = &ddPlayers[pool->owner]; ownerinfo_t *info = &pool->ownerInfo; de::zapPtr(info); // Pointer to the owner's pool. info->pool = pool; if(plr->shared.mo) { mobj_t *mo = plr->shared.mo; V3d_Copy(info->origin, mo->origin); info->angle = mo->angle; info->speed = M_ApproxDistance(mo->mom[MX], mo->mom[MY]); } // The acknowledgement threshold is a multiple of the average // ack time of the client. If an unacked delta is not acked within // the threshold, it'll be re-included in the ratings. info->ackThreshold = 0; //Net_GetAckThreshold(pool->owner); } /** * @return A timestamp that is used to track how old deltas are. */ uint Sv_GetTimeStamp() { return Timer_RealMilliseconds(); } /** * Initialize a new delta. */ void Sv_NewDelta(void *deltaPtr, deltatype_t type, uint id) { if(!deltaPtr) return; delta_t *delta = (delta_t *) deltaPtr; /// @note: This only clears the common delta_t part, not the extra data. de::zapPtr(delta); delta->id = id; delta->type = type; delta->state = DELTA_NEW; delta->timeStamp = Sv_GetTimeStamp(); } /** * @return @c true, if the delta contains no information. */ dd_bool Sv_IsVoidDelta(void const *delta) { return ((delta_t const *) delta)->flags == 0; } /** * @return @c true, if the delta is a Sound delta. */ dd_bool Sv_IsSoundDelta(void const *delta) { delta_t const *d = (delta_t const *) delta; return (d->type == DT_SOUND || d->type == DT_MOBJ_SOUND || d->type == DT_SECTOR_SOUND || d->type == DT_SIDE_SOUND || d->type == DT_POLY_SOUND); } /** * @return @c true, if the delta is a Start Sound delta. */ dd_bool Sv_IsStartSoundDelta(void const *delta) { sounddelta_t const *d = (sounddelta_t const *) delta; return Sv_IsSoundDelta(delta) && ((d->delta.flags & SNDDF_VOLUME) && d->volume > 0); } /** * @return @c true, if the delta is Stop Sound delta. */ dd_bool Sv_IsStopSoundDelta(void const *delta) { sounddelta_t const *d = (sounddelta_t const *) delta; return Sv_IsSoundDelta(delta) && ((d->delta.flags & SNDDF_VOLUME) && d->volume <= 0); } /** * @return @c true, if the delta is a Null Mobj delta. */ dd_bool Sv_IsNullMobjDelta(void const *delta) { return ((delta_t const *) delta)->type == DT_MOBJ && (((delta_t const *) delta)->flags & MDFC_NULL); } /** * @return @c true, if the delta is a Create Mobj delta. */ dd_bool Sv_IsCreateMobjDelta(void const *delta) { return ((delta_t const *) delta)->type == DT_MOBJ && (((delta_t const *) delta)->flags & MDFC_CREATE); } /** * @return @c true, if the deltas refer to the same object. */ dd_bool Sv_IsSameDelta(void const *delta1, void const *delta2) { delta_t const *a = (delta_t const *) delta1, *b = (delta_t const *) delta2; return (a->type == b->type) && (a->id == b->id); } /** * Makes a copy of the delta. */ void* Sv_CopyDelta(void* deltaPtr) { void* newDelta; delta_t* delta = (delta_t *) deltaPtr; size_t size = ( delta->type == DT_MOBJ ? sizeof(mobjdelta_t) : delta->type == DT_PLAYER ? sizeof(playerdelta_t) : delta->type == DT_SECTOR ? sizeof(sectordelta_t) : delta->type == DT_SIDE ? sizeof(sidedelta_t) : delta->type == DT_POLY ? sizeof(polydelta_t) : delta->type == DT_SOUND ? sizeof(sounddelta_t) : delta->type == DT_MOBJ_SOUND ? sizeof(sounddelta_t) : delta->type == DT_SECTOR_SOUND ? sizeof(sounddelta_t) : delta->type == DT_SIDE_SOUND ? sizeof(sounddelta_t) : delta->type == DT_POLY_SOUND ? sizeof(sounddelta_t) /* : delta->type == DT_LUMP? sizeof(lumpdelta_t) */ : 0); if(size == 0) { App_Error("Sv_CopyDelta: Unknown delta type %i.\n", delta->type); } newDelta = Z_Malloc(size, PU_MAP, 0); memcpy(newDelta, deltaPtr, size); return newDelta; } /** * Subtracts the contents of the second delta from the first delta. * Subtracting means that if a given flag is defined for both 1 and 2, * the flag for 1 is cleared (2 overrides 1). The result is that the * deltas can be applied in any order and the result is still correct. * * 1 and 2 must refer to the same entity! */ void Sv_SubtractDelta(void* deltaPtr1, const void* deltaPtr2) { delta_t* delta = (delta_t *) deltaPtr1; const delta_t* sub = (delta_t const *) deltaPtr2; #ifdef _DEBUG if(!Sv_IsSameDelta(delta, sub)) { App_Error("Sv_SubtractDelta: Not the same!\n"); } #endif if(Sv_IsNullMobjDelta(sub)) { // Null deltas kill everything. delta->flags = 0; } else { // Clear the common flags. delta->flags &= ~(delta->flags & sub->flags); } } /** * Applies the data in the source delta to the destination delta. * Both must be in the NEW state. Handles all types of deltas. */ void Sv_ApplyDeltaData(void *destDelta, void const *srcDelta) { delta_t const *src = (delta_t const *) srcDelta; delta_t *dest = (delta_t *) destDelta; int sf = src->flags; if(src->type == DT_MOBJ) { dt_mobj_t const *s = &((mobjdelta_t const *) src)->mo; dt_mobj_t *d = &((mobjdelta_t *) dest)->mo; // *Always* set the player pointer. d->dPlayer = s->dPlayer; if(sf & (MDF_ORIGIN_X | MDF_ORIGIN_Y)) d->_bspLeaf = s->_bspLeaf; if(sf & MDF_ORIGIN_X) d->origin[VX] = s->origin[VX]; if(sf & MDF_ORIGIN_Y) d->origin[VY] = s->origin[VY]; if(sf & MDF_ORIGIN_Z) d->origin[VZ] = s->origin[VZ]; if(sf & MDF_MOM_X) d->mom[MX] = s->mom[MX]; if(sf & MDF_MOM_Y) d->mom[MY] = s->mom[MY]; if(sf & MDF_MOM_Z) d->mom[MZ] = s->mom[MZ]; if(sf & MDF_ANGLE) d->angle = s->angle; if(sf & MDF_SELECTOR) d->selector = s->selector; if(sf & MDF_STATE) { d->state = s->state; d->tics = (s->state ? s->state->tics : 0); } if(sf & MDF_RADIUS) d->radius = s->radius; if(sf & MDF_HEIGHT) d->height = s->height; if(sf & MDF_FLAGS) { d->ddFlags = s->ddFlags; d->flags = s->flags; d->flags2 = s->flags2; d->flags3 = s->flags3; } if(sf & MDF_FLOORCLIP) d->floorClip = s->floorClip; if(sf & MDFC_TRANSLUCENCY) d->translucency = s->translucency; if(sf & MDFC_FADETARGET) d->visTarget = s->visTarget; } else if(src->type == DT_PLAYER) { const dt_player_t* s = &((const playerdelta_t *) src)->player; dt_player_t* d = &((playerdelta_t *) dest)->player; if(sf & PDF_MOBJ) d->mobj = s->mobj; if(sf & PDF_FORWARDMOVE) d->forwardMove = s->forwardMove; if(sf & PDF_SIDEMOVE) d->sideMove = s->sideMove; /*if(sf & PDF_ANGLE) d->angle = s->angle;*/ if(sf & PDF_TURNDELTA) d->turnDelta = s->turnDelta; if(sf & PDF_FRICTION) d->friction = s->friction; if(sf & PDF_EXTRALIGHT) { d->extraLight = s->extraLight; d->fixedColorMap = s->fixedColorMap; } if(sf & PDF_FILTER) d->filter = s->filter; if(sf & PDF_PSPRITES) { uint i; for(i = 0; i < 2; ++i) { int off = 16 + i * 8; if(sf & (PSDF_STATEPTR << off)) { d->psp[i].statePtr = s->psp[i].statePtr; d->psp[i].tics = (s->psp[i].statePtr ? s->psp[i].statePtr->tics : 0); } /*if(sf & (PSDF_LIGHT << off)) d->psp[i].light = s->psp[i].light;*/ if(sf & (PSDF_ALPHA << off)) d->psp[i].alpha = s->psp[i].alpha; if(sf & (PSDF_STATE << off)) d->psp[i].state = s->psp[i].state; if(sf & (PSDF_OFFSET << off)) { d->psp[i].offset[VX] = s->psp[i].offset[VX]; d->psp[i].offset[VY] = s->psp[i].offset[VY]; } } } } else if(src->type == DT_SECTOR) { const dt_sector_t* s = &((const sectordelta_t *) src)->sector; dt_sector_t* d = &((sectordelta_t *) dest)->sector; if(sf & SDF_FLOOR_MATERIAL) d->planes[PLN_FLOOR].surface.material = s->planes[PLN_FLOOR].surface.material; if(sf & SDF_CEILING_MATERIAL) d->planes[PLN_CEILING].surface.material = s->planes[PLN_CEILING].surface.material; if(sf & SDF_LIGHT) d->lightLevel = s->lightLevel; if(sf & SDF_FLOOR_TARGET) d->planes[PLN_FLOOR].target = s->planes[PLN_FLOOR].target; if(sf & SDF_FLOOR_SPEED) d->planes[PLN_FLOOR].speed = s->planes[PLN_FLOOR].speed; if(sf & SDF_CEILING_TARGET) d->planes[PLN_CEILING].target = s->planes[PLN_CEILING].target; if(sf & SDF_CEILING_SPEED) d->planes[PLN_CEILING].speed = s->planes[PLN_CEILING].speed; if(sf & SDF_FLOOR_HEIGHT) d->planes[PLN_FLOOR].height = s->planes[PLN_FLOOR].height; if(sf & SDF_CEILING_HEIGHT) d->planes[PLN_CEILING].height = s->planes[PLN_CEILING].height; if(sf & SDF_COLOR_RED) d->rgb[0] = s->rgb[0]; if(sf & SDF_COLOR_GREEN) d->rgb[1] = s->rgb[1]; if(sf & SDF_COLOR_BLUE) d->rgb[2] = s->rgb[2]; if(sf & SDF_FLOOR_COLOR_RED) d->planes[PLN_FLOOR].surface.rgba[0] = s->planes[PLN_FLOOR].surface.rgba[0]; if(sf & SDF_FLOOR_COLOR_GREEN) d->planes[PLN_FLOOR].surface.rgba[1] = s->planes[PLN_FLOOR].surface.rgba[1]; if(sf & SDF_FLOOR_COLOR_BLUE) d->planes[PLN_FLOOR].surface.rgba[2] = s->planes[PLN_FLOOR].surface.rgba[2]; if(sf & SDF_CEIL_COLOR_RED) d->planes[PLN_CEILING].surface.rgba[0] = s->planes[PLN_CEILING].surface.rgba[0]; if(sf & SDF_CEIL_COLOR_GREEN) d->planes[PLN_CEILING].surface.rgba[1] = s->planes[PLN_CEILING].surface.rgba[1]; if(sf & SDF_CEIL_COLOR_BLUE) d->planes[PLN_CEILING].surface.rgba[2] = s->planes[PLN_CEILING].surface.rgba[2]; } else if(src->type == DT_SIDE) { const dt_side_t* s = &((const sidedelta_t *) src)->side; dt_side_t* d = &((sidedelta_t *) dest)->side; if(sf & SIDF_TOP_MATERIAL) d->top.material = s->top.material; if(sf & SIDF_MID_MATERIAL) d->middle.material = s->middle.material; if(sf & SIDF_BOTTOM_MATERIAL) d->bottom.material = s->bottom.material; if(sf & SIDF_LINE_FLAGS) d->lineFlags = s->lineFlags; if(sf & SIDF_TOP_COLOR_RED) d->top.rgba[0] = s->top.rgba[0]; if(sf & SIDF_TOP_COLOR_GREEN) d->top.rgba[1] = s->top.rgba[1]; if(sf & SIDF_TOP_COLOR_BLUE) d->top.rgba[2] = s->top.rgba[2]; if(sf & SIDF_MID_COLOR_RED) d->middle.rgba[0] = s->middle.rgba[0]; if(sf & SIDF_MID_COLOR_GREEN) d->middle.rgba[1] = s->middle.rgba[1]; if(sf & SIDF_MID_COLOR_BLUE) d->middle.rgba[2] = s->middle.rgba[2]; if(sf & SIDF_MID_COLOR_ALPHA) d->middle.rgba[3] = s->middle.rgba[3]; if(sf & SIDF_BOTTOM_COLOR_RED) d->bottom.rgba[0] = s->bottom.rgba[0]; if(sf & SIDF_BOTTOM_COLOR_GREEN) d->bottom.rgba[1] = s->bottom.rgba[1]; if(sf & SIDF_BOTTOM_COLOR_BLUE) d->bottom.rgba[2] = s->bottom.rgba[2]; if(sf & SIDF_MID_BLENDMODE) d->middle.blendMode = s->middle.blendMode; if(sf & SIDF_FLAGS) d->flags = s->flags; } else if(src->type == DT_POLY) { const dt_poly_t* s = &((const polydelta_t *) src)->po; dt_poly_t* d = &((polydelta_t *) dest)->po; if(sf & PODF_DEST_X) d->dest[VX] = s->dest[VX]; if(sf & PODF_DEST_Y) d->dest[VY] = s->dest[VY]; if(sf & PODF_SPEED) d->speed = s->speed; if(sf & PODF_DEST_ANGLE) d->destAngle = s->destAngle; if(sf & PODF_ANGSPEED) d->angleSpeed = s->angleSpeed; } else if(Sv_IsSoundDelta(src)) { const sounddelta_t* s = (sounddelta_t const *) srcDelta; sounddelta_t* d = (sounddelta_t *) destDelta; if(sf & SNDDF_VOLUME) d->volume = s->volume; d->sound = s->sound; } else { App_Error("Sv_ApplyDeltaData: Unknown delta type %i.\n", src->type); } } /** * Merges the second delta with the first one. * The source and destination must refer to the same entity. * * @return @c false, if the result of the merge is a void delta. */ dd_bool Sv_MergeDelta(void* destDelta, const void* srcDelta) { const delta_t *src = (delta_t const *) srcDelta; delta_t *dest = (delta_t *) destDelta; #ifdef _DEBUG if(!Sv_IsSameDelta(src, dest)) { App_Error("Sv_MergeDelta: Not the same!\n"); } if(dest->state != DELTA_NEW) { App_Error("Sv_MergeDelta: Dest is not NEW.\n"); } #endif if(Sv_IsNullMobjDelta(dest)) { // Nothing can be merged with a null mobj delta. return true; } if(Sv_IsNullMobjDelta(src)) { // Null mobj deltas kill the destination. dest->flags = MDFC_NULL; return true; } if(Sv_IsCreateMobjDelta(dest) && Sv_IsNullMobjDelta(src)) { // Applying a Null mobj delta on a Create mobj delta causes // the two deltas to negate each other. Returning false // signifies that both should be removed from the pool. dest->flags = 0; return false; } if(Sv_IsStartSoundDelta(src) || Sv_IsStopSoundDelta(src)) { // Sound deltas completely override what they're being // merged with. (Only one sound per source.) Stop Sound deltas must // kill NEW sound deltas, because what is the benefit of sending // both in the same frame: first start a sound and then immediately // stop it? We don't want that. sounddelta_t *destSound = (sounddelta_t *) destDelta; const sounddelta_t* srcSound = (sounddelta_t const *) srcDelta; // Destination becomes equal to source. dest->flags = src->flags; destSound->sound = srcSound->sound; destSound->mobj = srcSound->mobj; destSound->volume = srcSound->volume; return true; } // The destination will contain all of source's data in addition // to the existing data. dest->flags |= src->flags; // The time stamp must NOT be always updated: the delta already // contains data which should've been sent some time ago. If we // update the time stamp now, the overdue data might not be sent. /* dest->timeStamp = src->timeStamp; */ Sv_ApplyDeltaData(dest, src); return true; } /** * @return The age of the delta, in milliseconds. */ uint Sv_DeltaAge(const delta_t* delta) { return Sv_GetTimeStamp() - delta->timeStamp; } /** * Approximate the distance to the given sector. Set 'mayBeGone' to true * if the mobj may have been destroyed and should not be processed. */ coord_t Sv_MobjDistance(mobj_t const *mo, ownerinfo_t const *info, dd_bool isReal) { coord_t z; if(isReal && !Mobj_Map(*mo).thinkers().isUsedMobjId(mo->thinker.id)) { // This mobj does not exist any more! return DDMAXFLOAT; } z = mo->origin[VZ]; // Registered mobjs may have a maxed out Z coordinate. if(!isReal) { if(z == DDMINFLOAT) z = mo->floorZ; if(z == DDMAXFLOAT) z = mo->ceilingZ - mo->height; } return M_ApproxDistance3(info->origin[VX] - mo->origin[VX], info->origin[VY] - mo->origin[VY], (info->origin[VZ] - z + mo->height / 2) * 1.2); } /** * Approximate the distance to the given sector. */ coord_t Sv_SectorDistance(int index, ownerinfo_t const *info) { DENG2_ASSERT(info); Sector const §or = worldSys().map().sector(index); return M_ApproxDistance3(info->origin[0] - sector.soundEmitter().origin[0], info->origin[1] - sector.soundEmitter().origin[1], (info->origin[2] - sector.soundEmitter().origin[2]) * 1.2); } coord_t Sv_SideDistance(int index, int deltaFlags, ownerinfo_t const *info) { DENG2_ASSERT(info); LineSide const *side = worldSys().map().sidePtr(index); SoundEmitter const &emitter = ( deltaFlags & SNDDF_SIDE_MIDDLE? side->middleSoundEmitter() : deltaFlags & SNDDF_SIDE_TOP ? side->topSoundEmitter() : side->bottomSoundEmitter()); return M_ApproxDistance3(info->origin[0] - emitter.origin[0], info->origin[1] - emitter.origin[1], (info->origin[2] - emitter.origin[2]) * 1.2); } /** * @return The distance to the origin of the delta's entity. */ coord_t Sv_DeltaDistance(void const *deltaPtr, ownerinfo_t const *info) { delta_t const *delta = (delta_t const *) deltaPtr; if(delta->type == DT_MOBJ) { // Use the delta's registered mobj position. For old unacked data, // it may be somewhat inaccurate. return Sv_MobjDistance(&((mobjdelta_t *) deltaPtr)->mo, info, false); } if(delta->type == DT_PLAYER) { // Use the player's actual position. mobj_t const *mo = ddPlayers[delta->id].shared.mo; if(mo) { return Sv_MobjDistance(mo, info, true); } } if(delta->type == DT_SECTOR) { return Sv_SectorDistance(delta->id, info); } if(delta->type == DT_SIDE) { LineSide *side = worldSys().map().sidePtr(delta->id); Line &line = side->line(); return M_ApproxDistance(info->origin[VX] - line.center().x, info->origin[VY] - line.center().y); } if(delta->type == DT_POLY) { Polyobj const &pob = worldSys().map().polyobj(delta->id); return M_ApproxDistance(info->origin[VX] - pob.origin[VX], info->origin[VY] - pob.origin[VY]); } if(delta->type == DT_MOBJ_SOUND) { sounddelta_t const *sound = (sounddelta_t const *) deltaPtr; return Sv_MobjDistance(sound->mobj, info, true); } if(delta->type == DT_SECTOR_SOUND) { return Sv_SectorDistance(delta->id, info); } if(delta->type == DT_SIDE_SOUND) { return Sv_SideDistance(delta->id, delta->flags, info); } if(delta->type == DT_POLY_SOUND) { Polyobj const &pob = worldSys().map().polyobj(delta->id); return M_ApproxDistance(info->origin[VX] - pob.origin[VX], info->origin[VY] - pob.origin[VY]); } // Unknown distance. return 1; } /** * The hash function for the pool delta hash. */ deltalink_t* Sv_PoolHash(pool_t* pool, int id) { return &pool->hash[(uint) id & POOL_HASH_FUNCTION_MASK]; } /** * The delta is removed from the pool's delta hash. */ void Sv_RemoveDelta(pool_t* pool, void* deltaPtr) { delta_t* delta = (delta_t *) deltaPtr; deltalink_t* hash = Sv_PoolHash(pool, delta->id); // Update first and last links. if(hash->last == delta) { hash->last = delta->prev; } if(hash->first == delta) { hash->first = delta->next; } // Link the delta out of the list. if(delta->next) { delta->next->prev = delta->prev; } if(delta->prev) { delta->prev->next = delta->next; } // Destroy it. Z_Free(delta); } /** * Draining the pool means emptying it of all contents. (Doh?) */ void Sv_DrainPool(uint clientNumber) { pool_t* pool = &pools[clientNumber]; delta_t* delta; misrecord_t* mis; void* next = NULL; int i; // Update the number of the owner. pool->owner = clientNumber; // Reset the counters. pool->setDealer = 0; pool->resendDealer = 0; Sv_PoolQueueClear(pool); // Free all deltas stored in the hash. for(i = 0; i < POOL_HASH_SIZE; ++i) { for(delta = (delta_t *) pool->hash[i].first; delta; delta = (delta_t *) next) { next = delta->next; Z_Free(delta); } } // Free all missile records in the pool. for(i = 0; i < POOL_MISSILE_HASH_SIZE; ++i) { for(mis = (misrecord_t *) pool->misHash[i].first; mis; mis = (misrecord_t *) next) { next = mis->next; Z_Free(mis); } } // Clear all the chains. de::zap(pool->hash); de::zap(pool->misHash); } /** * @return The maximum distance for the sound. If the origin * is any farther, the delta will not be sent to the * client in question. */ float Sv_GetMaxSoundDistance(const sounddelta_t* delta) { float volume = 1; // Volume shortens the maximum distance (why send it if it's not // audible?). if(delta->delta.flags & SNDDF_VOLUME) { volume = delta->volume; } if(volume <= 0) { // Silence is heard all over the world. return DDMAXFLOAT; } return volume * soundMaxDist; } /** * @return The flags that remain after exclusion. */ int Sv_ExcludeDelta(pool_t* pool, const void* deltaPtr) { const delta_t* delta = (delta_t const *) deltaPtr; player_t* plr = &ddPlayers[pool->owner]; mobj_t* poolViewer = plr->shared.mo; int flags = delta->flags; // Can we exclude information from the delta? (for this player only) if(delta->type == DT_MOBJ) { const mobjdelta_t *mobjDelta = (mobjdelta_t const *) deltaPtr; if(poolViewer && poolViewer->thinker.id == delta->id) { // This is the mobj the owner of the pool uses as a camera. flags &= ~MDF_CAMERA_EXCLUDE; // This information is sent in the PSV_PLAYER_FIX packet, // but only under specific circumstances. Most of the time // the client is responsible for updating its own pos/mom/angle. flags &= ~MDF_ORIGIN; flags &= ~MDF_MOM; flags &= ~MDF_ANGLE; } // What about missiles? We might be allowed to exclude some // information. if(mobjDelta->mo.ddFlags & DDMF_MISSILE) { if(Sv_IsNullMobjDelta(delta)) { // The missile is being removed entirely. // Remove the entry from the missile record. Sv_MRRemove(pool, delta->id); } else if(!Sv_IsCreateMobjDelta(delta)) { // This'll might exclude the coordinates. // The missile is put on record when the client acknowledges // the Create Mobj delta. flags &= ~Sv_MRCheck(pool, mobjDelta); } } } else if(delta->type == DT_PLAYER) { if(pool->owner == delta->id) { // All information does not need to be sent. flags &= ~PDF_CAMERA_EXCLUDE; /* $unifiedangles */ /* if(!(player->flags & DDPF_FIXANGLES)) { // Fixangles means that the server overrides the clientside // view angles. Normally they are always clientside, so // exclude them here. flags &= ~(PDF_CLYAW | PDF_CLPITCH); } */ } else { // This is a remote player, the owner of the pool doesn't need // to know everything about it (like psprites). flags &= ~PDF_NONCAMERA_EXCLUDE; } } else if(Sv_IsSoundDelta(delta)) { // Sounds that originate from too far away are not added to a pool. // Stop Sound deltas have an infinite max distance, though. if(Sv_DeltaDistance(delta, &pool->ownerInfo) > Sv_GetMaxSoundDistance((sounddelta_t const *) deltaPtr)) { // Don't add it. return 0; } } // These are the flags that remain. return flags; } /** * When adding a delta to the pool, it subtracts from the unacked deltas * there and is merged with matching new deltas. If a delta becomes void * after subtraction, it's removed from the pool. All the processing is * done based on the ID number of the delta (and type), so to make things * more efficient, a hash table is used (key is ID). * * Deltas are unique only in the NEW state. There may be multiple UNACKED * deltas for the same entity. * * The contents of the delta must not be modified. */ void Sv_AddDelta(pool_t* pool, void* deltaPtr) { delta_t* iter, *next = NULL, *existingNew = NULL; delta_t* delta = (delta_t *) deltaPtr; deltalink_t* hash = Sv_PoolHash(pool, delta->id); int flags, originalFlags; // Sometimes we can exclude a part of the data, if the client has no // use for it. flags = Sv_ExcludeDelta(pool, delta); if(!flags) { // No data remains... No need to add this delta. return; } // Temporarily use the excluded flags. originalFlags = delta->flags; delta->flags = flags; // While subtracting from old deltas, we'll look for a pointer to // an existing NEW delta. for(iter = hash->first; iter; iter = next) { // Iter is removed if it becomes void. next = iter->next; // Sameness is determined with type and ID. if(Sv_IsSameDelta(iter, delta)) { if(iter->state == DELTA_NEW) { // We'll merge with this instead of adding a new delta. existingNew = iter; } else if(iter->state == DELTA_UNACKED) { // The new information in the delta overrides the info in // this unacked delta. Let's subtract. This way, if the // unacked delta needs to be resent, it won't contain // obsolete data. Sv_SubtractDelta(iter, delta); // Was everything removed? if(Sv_IsVoidDelta(iter)) { Sv_RemoveDelta(pool, iter); continue; } } } } if(existingNew) { // Merge the new delta with the older NEW delta. if(!Sv_MergeDelta(existingNew, delta)) { // The deltas negated each other (Null -> Create). // The existing delta must be removed. Sv_RemoveDelta(pool, existingNew); } } else { // Add it to the end of the hash chain. We must take a copy // of the delta so it can be stored in the hash. iter = (delta_t *) Sv_CopyDelta(delta); if(hash->last) { hash->last->next = iter; iter->prev = hash->last; } hash->last = iter; if(!hash->first) { hash->first = iter; } } // This delta may yet be added to other pools. They should use the // original flags, not the ones we might've used (hackish: copying the // whole delta is not really an option, though). delta->flags = originalFlags; } /** * Add the delta to all the pools in the NULL-terminated array. */ void Sv_AddDeltaToPools(void* deltaPtr, pool_t** targets) { for(; *targets; targets++) { Sv_AddDelta(*targets, deltaPtr); } } /** * All NEW deltas for the mobj are removed from the pool as obsolete. */ void Sv_PoolMobjRemoved(pool_t* pool, thid_t id) { deltalink_t* hash = Sv_PoolHash(pool, id); delta_t* delta, *next = NULL; for(delta = hash->first; delta; delta = next) { next = delta->next; if(delta->state == DELTA_NEW && delta->type == DT_MOBJ && delta->id == id) { // This must be removed! Sv_RemoveDelta(pool, delta); } } // Also check the missile record. Sv_MRRemove(pool, id); } /** * This is called when a mobj is removed in a predictable fashion. * (Mobj state is NULL when it's destroyed. Assumption: The NULL state * is set only when animation reaches its end.) Because the register-mobj * is removed, no Null Mobj delta is generated for the mobj. */ void Sv_MobjRemoved(thid_t id) { uint i; reg_mobj_t* mo = Sv_RegisterFindMobj(&worldRegister, id); if(mo) { Sv_RegisterRemoveMobj(&worldRegister, mo); // We must remove all NEW deltas for this mobj from the pools. // One possibility: there are mobj deltas waiting in the pool, // but the mobj is removed here. Because it'll be no longer in // the register, no Null Mobj delta is generated, and thus the // client will eventually receive those mobj deltas unnecessarily. for(i = 0; i < DDMAXPLAYERS; ++i) { if(clients[i].connected) { Sv_PoolMobjRemoved(&pools[i], id); } } } } /** * When a player leaves the game, his data is removed from the register. * Otherwise he'll not get all the data if he reconnects before the map * is changed. */ void Sv_PlayerRemoved(uint playerNumber) { de::zap(worldRegister.ddPlayers[playerNumber]); } /** * @return @c true, if the pool is in the targets array. */ dd_bool Sv_IsPoolTargeted(pool_t* pool, pool_t** targets) { for(; *targets; targets++) { if(pool == *targets) return true; } return false; } /** * Fills the array with pointers to the pools of the connected clients, * if specificClient is < 0. * * @return The number of pools in the list. */ int Sv_GetTargetPools(pool_t** targets, int clientsMask) { int i, numTargets = 0; for(i = 0; i < DDMAXPLAYERS; ++i) { if(clientsMask & (1 << i) && clients[i].connected) { targets[numTargets++] = &pools[i]; } } /* if(specificClient & SVSF_ specificClient >= 0) { targets[0] = &pools[specificClient]; targets[1] = NULL; return 1; } for(i = 0; i < DDMAXPLAYERS; ++i) { // Deltas must be generated for all connected players, even // if they aren't yet ready to receive them. if(clients[i].connected) { if(specificClient == SV_TARGET_ALL_POOLS || i != -specificClient) targets[numTargets++] = &pools[i]; } } */ // A NULL pointer marks the end of target pools. targets[numTargets] = NULL; return numTargets; } /** * Null deltas are generated for mobjs that have been destroyed. * The register's mobj hash is scanned to see which mobjs no longer exist. * * When updating, the destroyed mobjs are removed from the register. */ void Sv_NewNullDeltas(cregister_t *reg, dd_bool doUpdate, pool_t **targets) { int i; mobjhash_t *hash; reg_mobj_t *obj, *next = 0; mobjdelta_t null; for(i = 0, hash = reg->mobjs; i < REG_MOBJ_HASH_SIZE; ++i, hash++) { for(obj = hash->first; obj; obj = next) { // This reg_mobj_t might be removed. next = obj->next; /// @todo Do not assume mobj is from the CURRENT map. if(!worldSys().map().thinkers().isUsedMobjId(obj->mo.thinker.id)) { // This object no longer exists! Sv_NewDelta(&null, DT_MOBJ, obj->mo.thinker.id); null.delta.flags = MDFC_NULL; // We need all the data for positioning. memcpy(&null.mo, &obj->mo, sizeof(dt_mobj_t)); Sv_AddDeltaToPools(&null, targets); if(doUpdate) { // Keep the register up to date. Sv_RegisterRemoveMobj(reg, obj); } } } } } /** * Mobj deltas are generated for all mobjs that have changed. */ void Sv_NewMobjDeltas(cregister_t *reg, dd_bool doUpdate, pool_t **targets) { worldSys().map().thinkers().forAll(reinterpret_cast(gx.MobjThinker), 0x1 /*public*/, [®, &doUpdate, &targets] (thinker_t *th) { auto &mob = *reinterpret_cast(th); // Some objects should not be processed. if(!Sv_IsMobjIgnored(mob)) { // Compare to produce a delta. mobjdelta_t delta; if(Sv_RegisterCompareMobj(reg, &mob, &delta)) { Sv_AddDeltaToPools(&delta, targets); if(doUpdate) { // This'll add a new register-mobj if it doesn't already exist. Sv_RegisterMobj(&Sv_RegisterAddMobj(reg, mob.thinker.id)->mo, &mob); } } } return LoopContinue; }); } /** * Player deltas are generated for changed player data. */ void Sv_NewPlayerDeltas(cregister_t* reg, dd_bool doUpdate, pool_t** targets) { playerdelta_t player; uint i; for(i = 0; i < DDMAXPLAYERS; ++i) { if(Sv_IsPlayerIgnored(i)) continue; // Compare to produce a delta. if(Sv_RegisterComparePlayer(reg, i, &player)) { // Did the mobj change? If so, the old mobj must be zeroed // in the register. Otherwise, the clients may not receive // all the data they need (because of viewpoint exclusion // flags). if(doUpdate && (player.delta.flags & PDF_MOBJ)) { reg_mobj_t* registered = Sv_RegisterFindMobj(reg, reg->ddPlayers[i].mobj); if(registered) { Sv_RegisterResetMobj(®istered->mo); } } Sv_AddDeltaToPools(&player, targets); } if(doUpdate) { Sv_RegisterPlayer(®->ddPlayers[i], i); } // What about forced deltas? if(Sv_IsPoolTargeted(&pools[i], targets)) { #if 0 if(ddPlayers[i].flags & DDPF_FIXANGLES) { Sv_NewDelta(&player, DT_PLAYER, i); Sv_RegisterPlayer(&player.player, i); //player.delta.flags = PDF_CLYAW | PDF_CLPITCH; /* $unifiedangles */ // Once added to the pool, the information will not get lost. Sv_AddDelta(&pools[i], &player); // Doing this once is enough. ddPlayers[i].flags &= ~DDPF_FIXANGLES; } // Generate a FIXPOS/FIXMOM mobj delta, too? if(ddPlayers[i].mo && (ddPlayers[i].flags & (DDPF_FIXORIGIN | DDPF_FIXMOM))) { const mobj_t *mo = ddPlayers[i].mo; mobjdelta_t mobj; Sv_NewDelta(&mobj, DT_MOBJ, mo->thinker.id); Sv_RegisterMobj(&mobj.mo, mo); if(ddPlayers[i].flags & DDPF_FIXORIGIN) { mobj.delta.flags |= MDF_ORIGIN; } if(ddPlayers[i].flags & DDPF_FIXMOM) { mobj.delta.flags |= MDF_MOM; } Sv_AddDelta(&pools[i], &mobj); // Doing this once is enough. ddPlayers[i].flags &= ~(DDPF_FIXORIGIN | DDPF_FIXMOM); } #endif } } } /** * Sector deltas are generated for changed sectors. */ void Sv_NewSectorDeltas(cregister_t *reg, dd_bool doUpdate, pool_t **targets) { sectordelta_t delta; for(int i = 0; i < worldSys().map().sectorCount(); ++i) { if(Sv_RegisterCompareSector(reg, i, &delta, doUpdate)) { Sv_AddDeltaToPools(&delta, targets); } } } /** * Side deltas are generated for changed sides (and line flags). * Changes in sides (textures) are so rare that all sides need not be * checked on every tic. */ void Sv_NewSideDeltas(cregister_t *reg, dd_bool doUpdate, pool_t **targets) { static uint numShifts = 2, shift = 0; /// @todo fixme: Do not assume the current map. Map &map = worldSys().map(); // When comparing against an initial register, always compare all // sides (since the comparing is only done once, not continuously). uint start, end; if(reg->isInitial) { start = 0; end = map.sideCount(); } else { // Because there are so many sides in a typical map, the number // of compared sides soon accumulates to millions. To reduce the // load, we'll check only a portion of all sides for a frame. start = shift * map.sideCount() / numShifts; end = ++shift * map.sideCount() / numShifts; shift %= numShifts; } sidedelta_t delta; for(uint i = start; i < end; ++i) { if(Sv_RegisterCompareSide(reg, i, &delta, doUpdate)) { Sv_AddDeltaToPools(&delta, targets); } } } /** * Poly deltas are generated for changed polyobjs. */ void Sv_NewPolyDeltas(cregister_t *reg, dd_bool doUpdate, pool_t **targets) { LOG_AS("Sv_NewPolyDeltas"); polydelta_t delta; /// @todo fixme: Do not assume the current map. for(int i = 0; i < worldSys().map().polyobjCount(); ++i) { if(Sv_RegisterComparePoly(reg, i, &delta)) { LOGDEV_NET_XVERBOSE_DEBUGONLY("Change in poly %i", i); Sv_AddDeltaToPools(&delta, targets); } if(doUpdate) { Sv_RegisterPoly(®->polyObjs[i], i); } } } void Sv_NewSoundDelta(int soundId, mobj_t *emitter, Sector *sourceSector, Polyobj *sourcePoly, Plane *sourcePlane, Surface *sourceSurface, float volume, dd_bool isRepeating, int clientsMask) { pool_t *targets[DDMAXPLAYERS + 1]; sounddelta_t soundDelta; int type = DT_SOUND, df = 0; int id = soundId; // Determine the target pools. Sv_GetTargetPools(targets, clientsMask); if(sourceSector) { type = DT_SECTOR_SOUND; id = sourceSector->indexInMap(); // Client assumes the sector's sound origin. } else if(sourcePoly) { type = DT_POLY_SOUND; id = sourcePoly->indexInMap(); } else if(sourcePlane) { type = DT_SECTOR_SOUND; // Clients need to know which emitter to use. if(emitter && emitter == (mobj_t *) &sourcePlane->soundEmitter()) { if(sourcePlane->isSectorFloor()) { df |= SNDDF_PLANE_FLOOR; } else if(sourcePlane->isSectorCeiling()) { df |= SNDDF_PLANE_CEILING; } } // else client assumes the sector's sound emitter. id = sourcePlane->sector().indexInMap(); } else if(sourceSurface) { DENG_ASSERT(sourceSurface->parent().type() == DMU_SIDE); DENG2_ASSERT(emitter == 0); // surface sound emitter rather than a real mobj type = DT_SIDE_SOUND; // Clients need to know which emitter to use. LineSide &side = sourceSurface->parent().as(); if(&side.middle() == sourceSurface) { df |= SNDDF_SIDE_MIDDLE; } else if(&side.bottom() == sourceSurface) { df |= SNDDF_SIDE_BOTTOM; } else if(&side.top() == sourceSurface) { df |= SNDDF_SIDE_TOP; } id = side.indexInMap(); } else if(emitter) { type = DT_MOBJ_SOUND; id = emitter->thinker.id; soundDelta.mobj = emitter; } // Init to the right type. Sv_NewDelta(&soundDelta, (deltatype_t) type, id); // Always set volume. df |= SNDDF_VOLUME; soundDelta.volume = volume; if(isRepeating) df |= SNDDF_REPEAT; LOGDEV_NET_XVERBOSE("New sound delta: type=%i id=%i flags=%x") << type << id << df; // This is used by mobj/sector sounds. soundDelta.sound = soundId; soundDelta.delta.flags = df; Sv_AddDeltaToPools(&soundDelta, targets); } /** * @return @c true, if the client should receive frames. */ dd_bool Sv_IsFrameTarget(uint plrNum) { player_t* plr = &ddPlayers[plrNum]; ddplayer_t* ddpl = &plr->shared; // Local players receive frames only when they're recording a demo. // Clients must tell us they are ready before we can begin sending. return (ddpl->inGame && !(ddpl->flags & DDPF_LOCAL) && clients[plrNum].ready) || ((ddpl->flags & DDPF_LOCAL) && clients[plrNum].recording); } /** * Compare the current state of the world with the register and add the * deltas to all the pools, or if a specific client number is given, only * to its pool (done when a new client enters the game). No deltas will be * generated for predictable changes (state changes, linear movement...). * * @param reg World state register. * @param clientNumber Client for whom to generate deltas. < 0 = all ingame * clients should get the deltas. * @param doUpdate Updating the register means that the current state * of the world is stored in the register after the * deltas have been generated. */ void Sv_GenerateNewDeltas(cregister_t* reg, int clientNumber, dd_bool doUpdate) { pool_t* targets[DDMAXPLAYERS + 1], **pool; // Determine the target pools. Sv_GetTargetPools(targets, (clientNumber < 0 ? 0xff : (1 << clientNumber))); // Update the info of the pool owners. for(pool = targets; *pool; pool++) { Sv_UpdateOwnerInfo(*pool); } // Generate null deltas (removed mobjs). Sv_NewNullDeltas(reg, doUpdate, targets); // Generate mobj deltas. Sv_NewMobjDeltas(reg, doUpdate, targets); // Generate player deltas. Sv_NewPlayerDeltas(reg, doUpdate, targets); // Generate sector deltas. Sv_NewSectorDeltas(reg, doUpdate, targets); // Generate side deltas. Sv_NewSideDeltas(reg, doUpdate, targets); // Generate poly deltas. Sv_NewPolyDeltas(reg, doUpdate, targets); if(doUpdate) { // The register has now been updated to the current time. reg->gametic = SECONDS_TO_TICKS(gameTime); } } /** * This is called once for each frame, in Sv_TransmitFrame(). */ void Sv_GenerateFrameDeltas(void) { // Generate new deltas for all clients and update the world register. Sv_GenerateNewDeltas(&worldRegister, -1, true); } /** * Clears the priority queue of the pool. */ void Sv_PoolQueueClear(pool_t* pool) { pool->queueSize = 0; } /** * Exchanges two elements in the queue. */ void Sv_PoolQueueExchange(pool_t* pool, int index1, int index2) { delta_t *temp = pool->queue[index1]; pool->queue[index1] = pool->queue[index2]; pool->queue[index2] = temp; } /** * Adds the delta to the priority queue. More memory is allocated for the * queue if necessary. */ void Sv_PoolQueueAdd(pool_t* pool, delta_t* delta) { int i, parent; // Do we need more memory? if(pool->allocatedSize == pool->queueSize) { delta_t** newQueue; // Double the memory. pool->allocatedSize *= 2; if(!pool->allocatedSize) { // At least eight. pool->allocatedSize = 8; } // Allocate the new queue. newQueue = (delta_t **) Z_Malloc(pool->allocatedSize * sizeof(delta_t *), PU_MAP, 0); // Copy the old data. if(pool->queue) { memcpy(newQueue, pool->queue, sizeof(delta_t *) * pool->queueSize); // Get rid of the old queue. Z_Free(pool->queue); } pool->queue = newQueue; } // Add the new delta to the end of the queue array. i = pool->queueSize; pool->queue[i] = delta; ++pool->queueSize; // Rise in the heap until the correct place is found. while(i > 0) { parent = HEAP_PARENT(i); // Is it good now? if(pool->queue[parent]->score >= delta->score) break; // Exchange with the parent. Sv_PoolQueueExchange(pool, parent, i); i = parent; } } /** * Extracts the delta with the highest priority from the queue. * * @return @c NULL, if there are no more deltas. */ delta_t* Sv_PoolQueueExtract(pool_t* pool) { delta_t* max; int i, left, right, big; dd_bool isDone; if(!pool->queueSize) { // There is nothing in the queue. return NULL; } // This is what we'll return. max = pool->queue[0]; // Remove the first element from the queue. pool->queue[0] = pool->queue[--pool->queueSize]; // Heapify the heap. This is O(log n). i = 0; isDone = false; while(!isDone) { left = HEAP_LEFT(i); right = HEAP_RIGHT(i); big = i; // Which child is more important? if(left < pool->queueSize && pool->queue[left]->score > pool->queue[i]->score) { big = left; } if(right < pool->queueSize && pool->queue[right]->score > pool->queue[big]->score) { big = right; } // Can we stop now? if(big != i) { // Exchange and continue. Sv_PoolQueueExchange(pool, i, big); i = big; } else { // Heapifying is complete. isDone = true; } } return max; } /** * Postponed deltas can't be sent yet. */ dd_bool Sv_IsPostponedDelta(void* deltaPtr, ownerinfo_t* info) { delta_t *delta = (delta_t *) deltaPtr; uint age = Sv_DeltaAge(delta); if(delta->state == DELTA_UNACKED) { // Is it old enough? If not, it's still possible that the ack // has not reached us yet. return age < info->ackThreshold; } else if(delta->state == DELTA_NEW) { // Normally NEW deltas are never postponed. They are sent as soon // as possible. if(Sv_IsStopSoundDelta(delta)) { // Stop Sound deltas require a bit of care. To make sure they // arrive to the client in the correct order, we won't send // a Stop Sound until we can be sure all the Start Sound deltas // have arrived. (i.e. the pool must contain no Unacked Start // Sound deltas for the same source.) delta_t* iter; for(iter = Sv_PoolHash(info->pool, delta->id)->first; iter; iter = iter->next) { if(iter->state == DELTA_UNACKED && Sv_IsSameDelta(iter, delta) && Sv_IsStartSoundDelta(iter)) { // Must postpone this Stop Sound delta until this one // has been sent. return true; } } } } // This delta is not postponed. return false; } /** * Calculate a priority score for the delta. A higher score indicates * greater importance. * * @return @c true iff the delta should be included in the * queue. */ dd_bool Sv_RateDelta(void* deltaPtr, ownerinfo_t* info) { float score, size; coord_t distance; delta_t *delta = (delta_t *) deltaPtr; int df = delta->flags; uint age = Sv_DeltaAge(delta); // The importance doubles normally in 1 second. float ageScoreDouble = 1.0f; if(Sv_IsPostponedDelta(delta, info)) { // This delta will not be considered at this time. return false; } // Calculate the distance to the delta's origin. // If no distance can be determined, it's 1.0. distance = Sv_DeltaDistance(delta, info); if(distance < 1) distance = 1; distance = distance * distance; // Power of two. // What is the base score? score = deltaBaseScores[delta->type] / distance; // It's very important to send sound deltas in time. if(Sv_IsSoundDelta(delta)) { // Score doubles very quickly. ageScoreDouble = 1; } // Deltas become more important with age (milliseconds). score *= 1 + age / (ageScoreDouble * 1000.0f); /// @todo Consider viewpoint speed and angle. // Priority bonuses based on the contents of the delta. if(delta->type == DT_MOBJ) { const mobj_t* mo = &((mobjdelta_t *) delta)->mo; // Seeing new mobjs is interesting. if(df & MDFC_CREATE) score *= 1.5f; // Position changes are important. if(df & (MDF_ORIGIN_X | MDF_ORIGIN_Y)) score *= 1.2f; // Small objects are not that important. size = MAX_OF(mo->radius, mo->height); if(size < 16) { // Not too small, though. if(size < 2) size = 2; score *= size / 16; } else if(size > 50) { // Large objects are important. score *= size / 50; } } else if(delta->type == DT_PLAYER) { // Knowing the player's mobj is quite important. if(df & PDF_MOBJ) score *= 2; } else if(delta->type == DT_SECTOR) { // Lightlevel changes are very noticeable. if(df & SDF_LIGHT) score *= 1.2f; // Plane movements are very important (can be seen from far away). if(df & (SDF_FLOOR_HEIGHT | SDF_CEILING_HEIGHT | SDF_FLOOR_SPEED | SDF_CEILING_SPEED | SDF_FLOOR_TARGET | SDF_CEILING_TARGET)) { score *= 3; } } else if(delta->type == DT_POLY) { // Changes in speed are noticeable. if(df & PODF_SPEED) score *= 1.2f; } // This is the final score. Only positive scores are accepted in // the frame (deltas with nonpositive scores as ignored). delta->score = score; return (score > 0); } /** * Calculate a priority score for each delta and build the priority queue. * The most important deltas will be included in a frame packet. * A pool is rated after new deltas have been generated. */ void Sv_RatePool(pool_t* pool) { #ifdef _DEBUG player_t* plr = &ddPlayers[pool->owner]; //client_t* client = &clients[pool->owner]; #endif delta_t* delta; int i; #ifdef _DEBUG if(!plr->shared.mo) { App_Error("Sv_RatePool: Player %i has no mobj.\n", pool->owner); } #endif // Clear the queue. Sv_PoolQueueClear(pool); // We will rate all the deltas in the pool. After each delta // has been rated, it's added to the priority queue. for(i = 0; i < POOL_HASH_SIZE; ++i) { for(delta = pool->hash[i].first; delta; delta = delta->next) { if(Sv_RateDelta(delta, &pool->ownerInfo)) { Sv_PoolQueueAdd(pool, delta); } } } } /** * Do special things that need to be done when the delta has been acked. */ void Sv_AckDelta(pool_t* pool, delta_t* delta) { if(Sv_IsCreateMobjDelta(delta)) { mobjdelta_t* mobjDelta = (mobjdelta_t *) delta; // Created missiles are put on record. if(mobjDelta->mo.ddFlags & DDMF_MISSILE) { // Once again, we're assuming the delta is always completely // filled with valid information. (There are no 'partial' deltas.) Sv_MRAdd(pool, mobjDelta); } } } /** * Acknowledged deltas are removed from the pool, never to be seen again. * Clients ack deltas to tell the server they've received them. * * @note This is obsolete: deltas no longer need to be acknowledged as * they are sent over TCP. * * @param clientNumber Client whose deltas to ack. * @param set Delta set number. * @param resent If nonzero, ignore 'set' and ack by resend ID. */ void Sv_AckDeltaSet(uint clientNumber, int set, byte resent) { int i; pool_t* pool = &pools[clientNumber]; delta_t* delta, *next = NULL; //dd_bool ackTimeRegistered = false; // Iterate through the entire hash table. for(i = 0; i < POOL_HASH_SIZE; ++i) { for(delta = pool->hash[i].first; delta; delta = next) { next = delta->next; if(delta->state == DELTA_UNACKED && ((!resent && delta->set == set) || (resent && delta->resend == resent))) { /* // Register the ack time only for the first acked delta. if(!ackTimeRegistered) { Net_SetAckTime(clientNumber, Sv_DeltaAge(delta)); ackTimeRegistered = true; } */ // There may be something that we need to do now that the // delta has been acknowledged. Sv_AckDelta(pool, delta); // This delta is now finished! Sv_RemoveDelta(pool, delta); } } } } /** * Debugging metric. */ uint Sv_CountUnackedDeltas(uint clientNumber) { uint i, count; pool_t* pool = Sv_GetPool(clientNumber); delta_t* delta; // Iterate through the entire hash table. count = 0; for(i = 0; i < POOL_HASH_SIZE; ++i) { for(delta = pool->hash[i].first; delta; delta = delta->next) { if(delta->state == DELTA_UNACKED) ++count; } } return count; } doomsday-stable-1.15.7/doomsday/server/src/server/sv_sound.cpp0000664000175000017500000000721312641367671023766 0ustar jaakkojaakko/** @file sv_sound.cpp Serverside Sound Management. * @ingroup server * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "de_console.h" #include "de_network.h" #include "de_play.h" #include "de_audio.h" #include using namespace de; static inline dd_bool isRealMobj(const mobj_t* base) { return base && base->thinker.id != 0; } /** * Find the map object to whom @a base belongs. */ static void identifySoundEmitter(mobj_t **base, Sector **sector, Polyobj **poly, Plane **plane, Surface **surface) { *sector = 0; *poly = 0; *plane = 0; *surface = 0; if(!*base || isRealMobj(*base)) return; /// @todo fixme: Do not assume the current map. App_WorldSystem().map().identifySoundEmitter(*reinterpret_cast(*base), sector, poly, plane, surface); #ifdef DENG_DEBUG if(!*sector && !*poly && !*plane && !*surface) { throw de::Error("Sv_IdentifySoundBase", "Bad sound base."); } #endif *base = 0; } void Sv_Sound(int soundId, mobj_t* origin, int toPlr) { Sv_SoundAtVolume(soundId, origin, 1, toPlr); } void Sv_SoundAtVolume(int soundIDAndFlags, mobj_t *origin, float volume, int toPlr) { if(isClient) return; int soundID = (soundIDAndFlags & ~DDSF_FLAG_MASK); if(!soundID) return; Sector *sector; Polyobj *poly; Plane *plane; Surface *surface; identifySoundEmitter(&origin, §or, &poly, &plane, &surface); int targetPlayers = 0; if(toPlr & SVSF_TO_ALL) { targetPlayers = -1; } else { targetPlayers = (1 << (toPlr & 0xf)); } if(toPlr & SVSF_EXCLUDE_ORIGIN) { // Remove the bit of the player who owns the origin mobj (if any). if(origin && origin->dPlayer) { targetPlayers &= ~(1 << P_GetDDPlayerIdx(origin->dPlayer)); } } LOGDEV_NET_XVERBOSE("Sv_SoundAtVolume: id: #%i volume: %f targets: %x") << soundID << volume << targetPlayers; Sv_NewSoundDelta(soundID, origin, sector, poly, plane, surface, volume, (soundIDAndFlags & DDSF_REPEAT) != 0, targetPlayers); } void Sv_StopSound(int soundId, mobj_t *origin) { if(isClient) return; Sector *sector; Polyobj *poly; Plane *plane; Surface *surface; identifySoundEmitter(&origin, §or, &poly, &plane, &surface); LOGDEV_NET_XVERBOSE("Sv_StopSound: id: #%i origin: %i(%p) sec: %p poly: %p plane: %p surface: %p") << soundId << (origin? origin->thinker.id : 0) << origin << sector << poly << plane << surface; Sv_NewSoundDelta(soundId, origin, sector, poly, plane, surface, 0/*silence*/, false/*non-repeating*/, -1/*all clients*/); } doomsday-stable-1.15.7/doomsday/server/src/server/sv_missile.cpp0000664000175000017500000001125112641367671024300 0ustar jaakkojaakko/** @file sv_missile.cpp Delta Pool Missile Record. * @ingroup server * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "de_console.h" #include "de_network.h" #include "de_play.h" // MACROS ------------------------------------------------------------------ // TYPES ------------------------------------------------------------------- // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- // PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- // EXTERNAL DATA DECLARATIONS ---------------------------------------------- // PUBLIC DATA DEFINITIONS ------------------------------------------------- // PRIVATE DATA DEFINITIONS ------------------------------------------------ // CODE -------------------------------------------------------------------- /** * @return Pointer to the hash chain. */ mislink_t *Sv_MRHash(pool_t *pool, thid_t id) { return &pool->misHash[(unsigned) id % POOL_MISSILE_HASH_SIZE]; } /** * @return Pointer to the missile record for the specified ID, * else if no record exits; NULL. */ misrecord_t *Sv_MRFind(pool_t *pool, thid_t id) { mislink_t *hash = Sv_MRHash(pool, id); misrecord_t *mis; for(mis = hash->first; mis; mis = mis->next) { if(mis->id == id) { // This is it. return mis; } } return NULL; } /** * Adds an entry for the mobj into the missile record. */ void Sv_MRAdd(pool_t *pool, const mobjdelta_t *delta) { thid_t id = delta->delta.id; mislink_t *hash = Sv_MRHash(pool, id); misrecord_t *mis; #ifdef _DEBUG if(!(delta->mo.ddFlags & DDMF_MISSILE)) { App_Error("Sv_MRAdd: Not a missile.\n"); } #endif // Try to find an existing entry. mis = Sv_MRFind(pool, id); // Create a new record if necessary. if(!mis) { mis = (misrecord_t *) Z_Malloc(sizeof(misrecord_t), PU_MAP, 0); mis->id = id; // Link it in. mis->next = NULL; mis->prev = hash->last; if(hash->last) hash->last->next = mis; hash->last = mis; if(!hash->first) hash->first = mis; } // Update the momentum. /* mis->momx = delta->mo.momx; mis->momy = delta->mo.momy; mis->momz = delta->mo.momz; */ } /** * Remove the missile from the record. This is called when the missile * mobj is destroyed. */ void Sv_MRRemove(pool_t *pool, thid_t id) { mislink_t *hash = Sv_MRHash(pool, id); misrecord_t *mis; for(mis = hash->first; mis; mis = mis->next) { if(mis->id == id) { // This will be removed. if(hash->first == mis) hash->first = mis->next; if(hash->last == mis) hash->last = mis->prev; if(mis->next) mis->next->prev = mis->prev; if(mis->prev) mis->prev->next = mis->next; // There will be no more records to remove. break; } } } /** * @return The flags that should be excluded from the missile delta. */ int Sv_MRCheck(pool_t *pool, const mobjdelta_t *mobj) { misrecord_t *mis; int exclude = 0; #ifdef _DEBUG if(!(mobj->mo.ddFlags & DDMF_MISSILE)) { App_Error("Sv_MRCheck: Not a missile.\n"); } #endif if((mis = Sv_MRFind(pool, mobj->delta.id)) == NULL) { // No record for this; no basis for exclusion. return 0; } // Exclude each axis separately. If no change in momentum, exclude coord. if(!(mobj->delta.flags & MDF_MOM_X)) exclude |= MDF_ORIGIN_X; if(!(mobj->delta.flags & MDF_MOM_Y)) exclude |= MDF_ORIGIN_Y; if(!(mobj->delta.flags & MDF_MOM_Z)) exclude |= MDF_ORIGIN_Z; return exclude; } doomsday-stable-1.15.7/doomsday/server/src/server/sv_frame.cpp0000664000175000017500000006553712641367671023745 0ustar jaakkojaakko/** @file sv_frame.cpp Frame Generation and Transmission * @ingroup server * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include "de_base.h" #include "de_console.h" #include "de_network.h" #include "de_system.h" #include "de_misc.h" #include "de_play.h" #include "def_main.h" // MACROS ------------------------------------------------------------------ // Hitting the maximum packet size allows checks for raising BWR. #define BWR_ADJUST_TICS (TICSPERSEC / 2) // The minimum frame size is used when bandwidth rating is zero (poorest // possible connection). #define MINIMUM_FRAME_SIZE 1800 // bytes // The first frame should contain as much information as possible. #define MAX_FIRST_FRAME_SIZE 64000 // The frame size is calculated by multiplying the bandwidth rating // (max 100) with this factor (+min). #define FRAME_SIZE_FACTOR 13 #define FIXED8_8(x) (((x)*256) >> 16) #define FIXED10_6(x) (((x)*64) >> 16) #define CLAMPED_CHAR(x) ((x)>127? 127 : (x)<-128? -128 : (x)) // If movement is faster than this, we'll adjust the place of the point. #define MOM_FAST_LIMIT (127) // TYPES ------------------------------------------------------------------- // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- // PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- void Sv_SendFrame(int playerNumber); // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- // EXTERNAL DATA DECLARATIONS ---------------------------------------------- // PUBLIC DATA DEFINITIONS ------------------------------------------------- int allowFrames = false; int frameInterval = 1; // Skip every second frame by default (17.5fps) // PRIVATE DATA DEFINITIONS ------------------------------------------------ #ifdef _DEBUG static int byteCounts[256]; static int totalFrameCount; #endif static int lastTransmitTic = 0; // CODE -------------------------------------------------------------------- /** * Send all the relevant information to each client. */ void Sv_TransmitFrame(void) { int i, cTime, numInGame, pCount; // Obviously clients don't transmit anything. if(!allowFrames || isClient || Sys_IsShuttingDown()) { return; } if(!netGame) { // When not running a netGame, only generate deltas when somebody // is recording a demo. for(i = 0; i < DDMAXPLAYERS; ++i) if(Sv_IsFrameTarget(i)) break; if(i == DDMAXPLAYERS) { // Nobody is a frame target. return; } } if(SECONDS_TO_TICKS(gameTime) == lastTransmitTic) { // We were just here! return; } lastTransmitTic = SECONDS_TO_TICKS(gameTime); LOG_AS("Sv_TransmitFrame"); // Generate new deltas for the frame. Sv_GenerateFrameDeltas(); // How many players currently in the game? numInGame = Sv_GetNumPlayers(); for(i = 0, pCount = 0; i < DDMAXPLAYERS; ++i) { if(!Sv_IsFrameTarget(i)) { // This player is not a valid target for frames. continue; } // When the interval is greater than zero, this causes the frames // to be sent at different times for each player. pCount++; cTime = SECONDS_TO_TICKS(gameTime); if(frameInterval > 0 && numInGame > 1) { cTime += (pCount * frameInterval) / numInGame; } if(cTime <= clients[i].lastTransmit + frameInterval) { // Still too early to send. continue; } clients[i].lastTransmit = cTime; if(clients[i].ready) // && clients[i].updateCount > 0) { // A frame will be sent to this client. If the client // doesn't send ticcmds, the updatecount will eventually // decrease back to zero. //clients[i].updateCount--; Sv_SendFrame(i); } else { LOG_NET_XVERBOSE("NOT sending at tic %i to plr %i (ready:%b)") << lastTransmitTic << i << clients[i].ready; } } } /** * Shutdown routine for the server. */ void Sv_Shutdown(void) { #ifdef _DEBUG if(totalFrameCount > 0) { uint i; // Byte probabilities. for(i = 0; i < 256; ++i) { LOGDEV_NET_NOTE("Byte %02x: %f") << i << (byteCounts[i] / (float) totalFrameCount); } } #endif Sv_ShutdownPools(); } /** * The delta is written to the message buffer. */ void Sv_WriteMobjDelta(const void* deltaPtr) { const mobjdelta_t* delta = reinterpret_cast(deltaPtr); const dt_mobj_t* d = &delta->mo; int df = delta->delta.flags; byte moreFlags = 0; // Do we have fast momentum? if(fabs(d->mom[MX]) >= MOM_FAST_LIMIT || fabs(d->mom[MY]) >= MOM_FAST_LIMIT || fabs(d->mom[MZ]) >= MOM_FAST_LIMIT) { df |= MDF_MORE_FLAGS; moreFlags |= MDFE_FAST_MOM; } // Any translucency? if(df & MDFC_TRANSLUCENCY) { df |= MDF_MORE_FLAGS; moreFlags |= MDFE_TRANSLUCENCY; } // A fade target? if(df & MDFC_FADETARGET) { df |= MDF_MORE_FLAGS; moreFlags |= MDFE_FADETARGET; } // On the floor? if(df & MDFC_ON_FLOOR) { df |= MDF_MORE_FLAGS; moreFlags |= MDFE_Z_FLOOR; } // Mobj type? if(df & MDFC_TYPE) { df |= MDF_MORE_FLAGS; moreFlags |= MDFE_TYPE; } // Flags. What elements are included in the delta? if(d->selector & ~DDMOBJ_SELECTOR_MASK) df |= MDF_SELSPEC; // Omit NULL state. if(!d->state) { df &= ~MDF_STATE; } /* // Floor/ceiling z? if(df & MDF_ORIGIN_Z) { if(d->pos[VZ] == DDMINFLOAT || d->pos[VZ] == DDMAXFLOAT) { df &= ~MDF_ORIGIN_Z; df |= MDF_MORE_FLAGS; moreFlags |= (d->pos[VZ] == DDMINFLOAT ? MDFE_Z_FLOOR : MDFE_Z_CEILING); } } */ DENG_ASSERT(!(df & MDFC_NULL)); // don't write NULL deltas DENG_ASSERT((df & 0xffff) != 0); // don't write empty deltas // First the mobj ID number and flags. Writer_WriteUInt16(msgWriter, delta->delta.id); Writer_WriteUInt16(msgWriter, df & 0xffff); // More flags? if(df & MDF_MORE_FLAGS) { Writer_WriteByte(msgWriter, moreFlags); } // Coordinates with three bytes. if(df & MDF_ORIGIN_X) { fixed_t vx = FLT2FIX(d->origin[VX]); Writer_WriteInt16(msgWriter, vx >> FRACBITS); Writer_WriteByte(msgWriter, vx >> 8); } if(df & MDF_ORIGIN_Y) { fixed_t vy = FLT2FIX(d->origin[VY]); Writer_WriteInt16(msgWriter, vy >> FRACBITS); Writer_WriteByte(msgWriter, vy >> 8); } if(df & MDF_ORIGIN_Z) { fixed_t vz = FLT2FIX(d->origin[VZ]); Writer_WriteInt16(msgWriter, vz >> FRACBITS); Writer_WriteByte(msgWriter, vz >> 8); Writer_WriteFloat(msgWriter, d->floorZ); Writer_WriteFloat(msgWriter, d->ceilingZ); } // Momentum using 8.8 fixed point. if(df & MDF_MOM_X) { fixed_t mx = FLT2FIX(d->mom[MX]); Writer_WriteInt16(msgWriter, moreFlags & MDFE_FAST_MOM ? FIXED10_6(mx) : FIXED8_8(mx)); } if(df & MDF_MOM_Y) { fixed_t my = FLT2FIX(d->mom[MY]); Writer_WriteInt16(msgWriter, moreFlags & MDFE_FAST_MOM ? FIXED10_6(my) : FIXED8_8(my)); } if(df & MDF_MOM_Z) { fixed_t mz = FLT2FIX(d->mom[MZ]); Writer_WriteInt16(msgWriter, moreFlags & MDFE_FAST_MOM ? FIXED10_6(mz) : FIXED8_8(mz)); } // Angles with 16-bit accuracy. if(df & MDF_ANGLE) Writer_WriteInt16(msgWriter, d->angle >> 16); if(df & MDF_SELECTOR) Writer_WritePackedUInt16(msgWriter, d->selector); if(df & MDF_SELSPEC) Writer_WriteByte(msgWriter, d->selector >> 24); if(df & MDF_STATE) { assert(d->state != 0); Writer_WritePackedUInt16(msgWriter, runtimeDefs.states.indexOf(d->state)); } if(df & MDF_FLAGS) { Writer_WriteUInt32(msgWriter, d->ddFlags & DDMF_PACK_MASK); Writer_WriteUInt32(msgWriter, d->flags); Writer_WriteUInt32(msgWriter, d->flags2); Writer_WriteUInt32(msgWriter, d->flags3); } if(df & MDF_HEALTH) Writer_WriteInt32(msgWriter, d->health); if(df & MDF_RADIUS) Writer_WriteFloat(msgWriter, d->radius); if(df & MDF_HEIGHT) Writer_WriteFloat(msgWriter, d->height); if(df & MDF_FLOORCLIP) Writer_WriteFloat(msgWriter, d->floorClip); if(df & MDFC_TRANSLUCENCY) Writer_WriteByte(msgWriter, d->translucency); if(df & MDFC_FADETARGET) Writer_WriteByte(msgWriter, (byte)(d->visTarget +1)); if(df & MDFC_TYPE) Writer_WriteInt32(msgWriter, d->type); } /** * The delta is written to the message buffer. */ void Sv_WritePlayerDelta(const void* deltaPtr) { const playerdelta_t* delta = reinterpret_cast(deltaPtr); const dt_player_t* d = &delta->player; const ddpsprite_t* psp; int df = delta->delta.flags; int psdf, i, k; // First the player number. Upper three bits contain flags. Writer_WriteByte(msgWriter, delta->delta.id | (df >> 8)); // Flags. What elements are included in the delta? Writer_WriteByte(msgWriter, df & 0xff); if(df & PDF_MOBJ) Writer_WriteUInt16(msgWriter, d->mobj); if(df & PDF_FORWARDMOVE) Writer_WriteByte(msgWriter, d->forwardMove); if(df & PDF_SIDEMOVE) Writer_WriteByte(msgWriter, d->sideMove); /*if(df & PDF_ANGLE) Writer_WriteByte(msgWriter, d->angle >> 24);*/ if(df & PDF_TURNDELTA) Writer_WriteByte(msgWriter, (d->turnDelta * 16) >> 24); if(df & PDF_FRICTION) Writer_WriteByte(msgWriter, FLT2FIX(d->friction) >> 8); if(df & PDF_EXTRALIGHT) { // Three bits is enough for fixedcolormap. i = d->fixedColorMap; if(i < 0) i = 0; if(i > 7) i = 7; // Write the five upper bytes of extraLight. Writer_WriteByte(msgWriter, i | (d->extraLight & 0xf8)); } if(df & PDF_FILTER) { Writer_WriteUInt32(msgWriter, d->filter); LOGDEV_NET_XVERBOSE_DEBUGONLY("Sv_WritePlayerDelta: Plr %i, filter %08x", delta->delta.id << d->filter); } if(df & PDF_PSPRITES) // Only set if there's something to write. { for(i = 0; i < 2; ++i) { psdf = df >> (16 + i * 8); psp = d->psp + i; // First the flags. Writer_WriteByte(msgWriter, psdf); if(psdf & PSDF_STATEPTR) { Writer_WritePackedUInt16(msgWriter, psp->statePtr? (runtimeDefs.states.indexOf(psp->statePtr) + 1) : 0); } /*if(psdf & PSDF_LIGHT) { k = psp->light * 255; if(k < 0) k = 0; if(k > 255) k = 255; Writer_WriteByte(msgWriter, k); }*/ if(psdf & PSDF_ALPHA) { k = psp->alpha * 255; if(k < 0) k = 0; if(k > 255) k = 255; Writer_WriteByte(msgWriter, k); } if(psdf & PSDF_STATE) { Writer_WriteByte(msgWriter, psp->state); } if(psdf & PSDF_OFFSET) { Writer_WriteByte(msgWriter, CLAMPED_CHAR(psp->offset[VX] / 2)); Writer_WriteByte(msgWriter, CLAMPED_CHAR(psp->offset[VY] / 2)); } } } } /** * The delta is written to the message buffer. */ void Sv_WriteSectorDelta(const void* deltaPtr) { const sectordelta_t* delta = reinterpret_cast(deltaPtr); const dt_sector_t* d = &delta->sector; int df = delta->delta.flags, spd; byte floorspd = 0, ceilspd = 0; // Is there need to use 4.4 fixed-point speeds? // (7.1 is too inaccurate for very slow movement) if(df & SDF_FLOOR_SPEED) { spd = FLT2FIX(fabs(d->planes[PLN_FLOOR].speed)); floorspd = spd >> 15; if(!floorspd) { df |= SDF_FLOOR_SPEED_44; floorspd = spd >> 12; } } if(df & SDF_CEILING_SPEED) { spd = FLT2FIX(fabs(d->planes[PLN_CEILING].speed)); ceilspd = spd >> 15; if(!ceilspd) { df |= SDF_CEILING_SPEED_44; ceilspd = spd >> 12; } } // Sector number first. Writer_WriteUInt16(msgWriter, delta->delta.id); // Flags. Writer_WritePackedUInt32(msgWriter, df); if(df & SDF_FLOOR_MATERIAL) Writer_WritePackedUInt16(msgWriter, Sv_IdForMaterial(d->planes[PLN_FLOOR].surface.material)); if(df & SDF_CEILING_MATERIAL) Writer_WritePackedUInt16(msgWriter, Sv_IdForMaterial(d->planes[PLN_CEILING].surface.material)); if(df & SDF_LIGHT) { // Must fit into a byte. int lightlevel = (int) (255.0f * d->lightLevel); lightlevel = (lightlevel < 0 ? 0 : lightlevel > 255 ? 255 : lightlevel); Writer_WriteByte(msgWriter, (byte) lightlevel); } if(df & SDF_FLOOR_HEIGHT) { Writer_WriteInt16(msgWriter, FLT2FIX(d->planes[PLN_FLOOR].height) >> 16); } if(df & SDF_CEILING_HEIGHT) { LOGDEV_NET_XVERBOSE_DEBUGONLY("Sv_WriteSectorDelta: (%i) Absolute ceiling height=%f", delta->delta.id << d->planes[PLN_CEILING].height); Writer_WriteInt16(msgWriter, FLT2FIX(d->planes[PLN_CEILING].height) >> 16); } if(df & SDF_FLOOR_TARGET) Writer_WriteInt16(msgWriter, FLT2FIX(d->planes[PLN_FLOOR].target) >> 16); if(df & SDF_FLOOR_SPEED) // 7.1/4.4 fixed-point Writer_WriteByte(msgWriter, floorspd); if(df & SDF_CEILING_TARGET) Writer_WriteInt16(msgWriter, FLT2FIX(d->planes[PLN_CEILING].target) >> 16); if(df & SDF_CEILING_SPEED) // 7.1/4.4 fixed-point Writer_WriteByte(msgWriter, ceilspd); if(df & SDF_COLOR_RED) Writer_WriteByte(msgWriter, (byte) (255 * d->rgb[0])); if(df & SDF_COLOR_GREEN) Writer_WriteByte(msgWriter, (byte) (255 * d->rgb[1])); if(df & SDF_COLOR_BLUE) Writer_WriteByte(msgWriter, (byte) (255 * d->rgb[2])); if(df & SDF_FLOOR_COLOR_RED) Writer_WriteByte(msgWriter, (byte) (255 * d->planes[PLN_FLOOR].surface.rgba[0])); if(df & SDF_FLOOR_COLOR_GREEN) Writer_WriteByte(msgWriter, (byte) (255 * d->planes[PLN_FLOOR].surface.rgba[1])); if(df & SDF_FLOOR_COLOR_BLUE) Writer_WriteByte(msgWriter, (byte) (255 * d->planes[PLN_FLOOR].surface.rgba[2])); if(df & SDF_CEIL_COLOR_RED) Writer_WriteByte(msgWriter, (byte) (255 * d->planes[PLN_CEILING].surface.rgba[0])); if(df & SDF_CEIL_COLOR_GREEN) Writer_WriteByte(msgWriter, (byte) (255 * d->planes[PLN_CEILING].surface.rgba[1])); if(df & SDF_CEIL_COLOR_BLUE) Writer_WriteByte(msgWriter, (byte) (255 * d->planes[PLN_CEILING].surface.rgba[2])); } /** * The delta is written to the message buffer. */ void Sv_WriteSideDelta(const void* deltaPtr) { const sidedelta_t* delta = (sidedelta_t const *) deltaPtr; const dt_side_t* d = &delta->side; int df = delta->delta.flags; // Side number first. Writer_WriteUInt16(msgWriter, delta->delta.id); // Flags. Writer_WritePackedUInt32(msgWriter, df); if(df & SIDF_TOP_MATERIAL) Writer_WritePackedUInt16(msgWriter, Sv_IdForMaterial(d->top.material)); if(df & SIDF_MID_MATERIAL) Writer_WritePackedUInt16(msgWriter, Sv_IdForMaterial(d->middle.material)); if(df & SIDF_BOTTOM_MATERIAL) Writer_WritePackedUInt16(msgWriter, Sv_IdForMaterial(d->bottom.material)); if(df & SIDF_LINE_FLAGS) Writer_WriteByte(msgWriter, d->lineFlags); if(df & SIDF_TOP_COLOR_RED) Writer_WriteByte(msgWriter, (byte) (255 * d->top.rgba[0])); if(df & SIDF_TOP_COLOR_GREEN) Writer_WriteByte(msgWriter, (byte) (255 * d->top.rgba[1])); if(df & SIDF_TOP_COLOR_BLUE) Writer_WriteByte(msgWriter, (byte) (255 * d->top.rgba[2])); if(df & SIDF_MID_COLOR_RED) Writer_WriteByte(msgWriter, (byte) (255 * d->middle.rgba[0])); if(df & SIDF_MID_COLOR_GREEN) Writer_WriteByte(msgWriter, (byte) (255 * d->middle.rgba[1])); if(df & SIDF_MID_COLOR_BLUE) Writer_WriteByte(msgWriter, (byte) (255 * d->middle.rgba[2])); if(df & SIDF_MID_COLOR_ALPHA) Writer_WriteByte(msgWriter, (byte) (255 * d->middle.rgba[3])); if(df & SIDF_BOTTOM_COLOR_RED) Writer_WriteByte(msgWriter, (byte) (255 * d->bottom.rgba[0])); if(df & SIDF_BOTTOM_COLOR_GREEN) Writer_WriteByte(msgWriter, (byte) (255 * d->bottom.rgba[1])); if(df & SIDF_BOTTOM_COLOR_BLUE) Writer_WriteByte(msgWriter, (byte) (255 * d->bottom.rgba[2])); if(df & SIDF_MID_BLENDMODE) Writer_WriteInt32(msgWriter, d->middle.blendMode); if(df & SIDF_FLAGS) Writer_WriteByte(msgWriter, d->flags); } /** * The delta is written to the message buffer. */ void Sv_WritePolyDelta(const void* deltaPtr) { const polydelta_t* delta = (polydelta_t const *) deltaPtr; const dt_poly_t* d = &delta->po; int df = delta->delta.flags; if(d->destAngle == (unsigned) -1) { // Send Perpetual Rotate instead of Dest Angle flag. df |= PODF_PERPETUAL_ROTATE; df &= ~PODF_DEST_ANGLE; } // Poly number first. Writer_WritePackedUInt16(msgWriter, delta->delta.id); // Flags. Writer_WriteByte(msgWriter, df & 0xff); if(df & PODF_DEST_X) Writer_WriteFloat(msgWriter, d->dest[VX]); if(df & PODF_DEST_Y) Writer_WriteFloat(msgWriter, d->dest[VY]); if(df & PODF_SPEED) Writer_WriteFloat(msgWriter, d->speed); if(df & PODF_DEST_ANGLE) Writer_WriteInt16(msgWriter, d->destAngle >> 16); if(df & PODF_ANGSPEED) Writer_WriteInt16(msgWriter, d->angleSpeed >> 16); } /** * The delta is written to the message buffer. */ void Sv_WriteSoundDelta(const void* deltaPtr) { const sounddelta_t* delta = (sounddelta_t const *) deltaPtr; int df = delta->delta.flags; // This is either the sound ID, emitter ID or sector index. Writer_WriteUInt16(msgWriter, delta->delta.id); // First the flags byte. Writer_WriteByte(msgWriter, df & 0xff); switch(delta->delta.type) { case DT_MOBJ_SOUND: case DT_SECTOR_SOUND: case DT_SIDE_SOUND: case DT_POLY_SOUND: // The sound ID. Writer_WriteUInt16(msgWriter, delta->sound); break; default: break; } // The common parts. if(df & SNDDF_VOLUME) { if(delta->volume > 1) { // Very loud indeed. Writer_WriteByte(msgWriter, 255); } else if(delta->volume <= 0) { // Silence. Writer_WriteByte(msgWriter, 0); } else { Writer_WriteByte(msgWriter, delta->volume * 127 + 0.5f); } } } /** * Write the type and possibly the set number (for Unacked deltas). */ void Sv_WriteDeltaHeader(byte type, const delta_t* delta) { #ifdef _DEBUG if(type >= NUM_DELTA_TYPES) { App_Error("Sv_WriteDeltaHeader: Invalid delta type %i.\n", type); } #endif #ifdef _DEBUG // Once sent, the deltas can be discarded and there is no need for resending. assert(delta->state != DELTA_UNACKED); #endif if(delta->state == DELTA_UNACKED) { assert(false); // Flag this as Resent. type |= DT_RESENT; } Writer_WriteByte(msgWriter, type); // Include the set number? if(type & DT_RESENT) { // The client will use this to avoid dupes. If the client has already // received the set this delta belongs to, it means the delta has // already been received. This is needed in the situation where the // ack is lost or delayed. Writer_WriteByte(msgWriter, delta->set); // Also send the unique ID of this delta. If the client has already // received a delta with this ID, the delta is discarded. This is // needed in the situation where the set is lost. Writer_WriteByte(msgWriter, delta->resend); } } /** * The delta is written to the message buffer. */ void Sv_WriteDelta(const delta_t* delta) { byte type = delta->type; #ifdef _NETDEBUG int lengthOffset; int endOffset; #endif #ifdef _NETDEBUG // Extra length field in debug builds. lengthOffset = Msg_Offset(); Msg_WriteLong(0); #endif // Null mobj deltas are special. if(type == DT_MOBJ) { if(delta->flags & MDFC_NULL) { // This'll be the entire delta. No more data is needed. Sv_WriteDeltaHeader(DT_NULL_MOBJ, delta); Writer_WriteUInt16(msgWriter, delta->id); #ifdef _NETDEBUG goto writeDeltaLength; #else return; #endif } } // First the type of the delta. Sv_WriteDeltaHeader(type, delta); switch(delta->type) { case DT_MOBJ: Sv_WriteMobjDelta(delta); break; case DT_PLAYER: Sv_WritePlayerDelta(delta); break; case DT_SECTOR: Sv_WriteSectorDelta(delta); break; case DT_SIDE: Sv_WriteSideDelta(delta); break; case DT_POLY: Sv_WritePolyDelta(delta); break; case DT_SOUND: case DT_MOBJ_SOUND: case DT_SECTOR_SOUND: case DT_SIDE_SOUND: case DT_POLY_SOUND: Sv_WriteSoundDelta(delta); break; /*case DT_LUMP: Sv_WriteLumpDelta(delta); break; */ default: App_Error("Sv_WriteDelta: Unknown delta type %i.\n", delta->type); } #ifdef _NETDEBUG writeDeltaLength: // Update the length of the delta. endOffset = Msg_Offset(); Msg_SetOffset(lengthOffset); Msg_WriteLong(endOffset - lengthOffset); Msg_SetOffset(endOffset); #endif } /** * @return An estimate for the maximum frame size appropriate * for the client. The bandwidth rating is updated * whenever a frame is sent. */ size_t Sv_GetMaxFrameSize(int playerNumber) { size_t size = MINIMUM_FRAME_SIZE + FRAME_SIZE_FACTOR * clients[playerNumber].bandwidthRating; // What about the communications medium? if(size > PROTOCOL_MAX_DATAGRAM_SIZE) size = PROTOCOL_MAX_DATAGRAM_SIZE; return size; } /** * @return A unique resend ID. Never returns zero. */ byte Sv_GetNewResendID(pool_t* pool) { byte id = pool->resendDealer; // Advance to next ID, skipping zero. while(!++pool->resendDealer) {} return id; } /** * Send a sv_frame packet to the specified player. The amount of data sent * depends on the player's bandwidth rating. */ void Sv_SendFrame(int plrNum) { pool_t* pool = Sv_GetPool(plrNum); byte oldResend; delta_t* delta; int deltaCount = 0; size_t lastStart, maxFrameSize; //, deltaCountOffset = 0; /*#if _NETDEBUG int endOffset = 0; #endif*/ // Does the send queue allow us to send this packet? // Bandwidth rating is updated during the check. if(!Sv_CheckBandwidth(plrNum)) { // We cannot send anything at this time. This will only happen if // the send queue has too many packets waiting to be sent. return; } // The priority queue of the client needs to be rebuilt before // a new frame can be sent. Sv_RatePool(pool); // This will be a new set. pool->setDealer++; // Determine the maximum size of the frame packet. maxFrameSize = Sv_GetMaxFrameSize(plrNum); // Allow more info for the first frame. if(pool->isFirst) maxFrameSize = MAX_FIRST_FRAME_SIZE; // If this is the first frame after a map change, use the special // first frame packet type. Msg_Begin(pool->isFirst ? PSV_FIRST_FRAME2 : PSV_FRAME2); // First send the gameTime of this frame. Writer_WriteFloat(msgWriter, gameTime); // Keep writing until the maximum size is reached. while((delta = Sv_PoolQueueExtract(pool)) != NULL && (lastStart = Writer_Size(msgWriter)) < maxFrameSize) { oldResend = pool->resendDealer; // Is this going to be a resent? if(delta->state == DELTA_UNACKED && !delta->resend) { // Assign a new unique ID for this delta. // This ID won't be changed after this. delta->resend = Sv_GetNewResendID(pool); } Sv_WriteDelta(delta); // Did we go over the limit? if(Writer_Size(msgWriter) > maxFrameSize) { /* // Time to see if BWR needs to be adjusted. if(clients[plrNum].bwrAdjustTime <= 0) { clients[plrNum].bwrAdjustTime = BWR_ADJUST_TICS; } */ // Cancel the last delta. Writer_SetPos(msgWriter, lastStart); // Restore the resend dealer. if(oldResend) pool->resendDealer = oldResend; break; } // Successfully written, increment counter. deltaCount++; // Update the sent delta's state. if(delta->state == DELTA_NEW) { // New deltas are assigned to this set. Unacked deltas will // remain in the set they were initially sent in. delta->set = pool->setDealer; delta->timeStamp = Sv_GetTimeStamp(); delta->state = DELTA_UNACKED; } } // Update the number of deltas included in the packet. /* #ifdef _NETDEBUG endOffset = Msg_Offset(); Msg_SetOffset(deltaCountOffset); Msg_WriteLong(deltaCount); Msg_SetOffset(endOffset); #endif */ Msg_End(); Net_SendBuffer(plrNum, 0); // Once sent, the delta set can be discarded. Sv_AckDeltaSet(plrNum, pool->setDealer, 0); // Now a frame has been sent. pool->isFirst = false; } doomsday-stable-1.15.7/doomsday/server/src/server_dummies.cpp0000664000175000017500000000556612641367671023662 0ustar jaakkojaakko/** @file server_dummies.c Dummy functions for the server. * @ingroup server * * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "server_dummies.h" #include "ui/nativeui.h" #include "Texture" void Con_TransitionTicker(timespan_t t) { DENG_UNUSED(t); } void Con_SetProgress(int progress) { DENG_UNUSED(progress); } void GL_Shutdown() {} void R_RenderPlayerView(int num) { DENG_UNUSED(num); } void R_SetBorderGfx(Uri const *const *paths) { DENG_UNUSED(paths); } void R_SkyParams(int layer, int param, void *data) { DENG_UNUSED(layer); DENG_UNUSED(param); DENG_UNUSED(data); } void R_InitSvgs(void) {} void R_ShutdownSvgs(void) {} struct font_s* R_CreateFontFromDef(ded_compositefont_t* def) { DENG_UNUSED(def); return 0; } void Rend_CacheForMobjType(int num) { DENG_UNUSED(num); } void Rend_ConsoleInit() {} void Rend_ConsoleOpen(int yes) { DENG_UNUSED(yes); } void Rend_ConsoleMove(int y) { DENG_UNUSED(y); } void Rend_ConsoleResize(int force) { DENG_UNUSED(force); } void Rend_ConsoleToggleFullscreen() {} void Rend_ConsoleCursorResetBlink() {} void Cl_InitPlayers(void) {} void UI_Ticker(timespan_t t) { DENG_UNUSED(t); } void Sys_MessageBox(messageboxtype_t type, const char* title, const char* msg, const char* detailedMsg) { DENG_UNUSED(type); DENG_UNUSED(title); DENG_UNUSED(msg); DENG_UNUSED(detailedMsg); } void Sys_MessageBox2(messageboxtype_t /*type*/, const char * /*title*/, const char * /*msg*/, const char * /*informativeMsg*/, const char * /*detailedMsg*/) { } void Sys_MessageBoxf(messageboxtype_t /*type*/, const char* /*title*/, const char* /*format*/, ...) { } int Sys_MessageBoxWithButtons(messageboxtype_t /*type*/, const char* /*title*/, const char* /*msg*/, const char* /*informativeMsg*/, const char** /*buttons*/) { return 0; } void Sys_MessageBoxWithDetailsFromFile(messageboxtype_t /*type*/, const char* /*title*/, const char* /*msg*/, const char* /*informativeMsg*/, const char* /*detailsFileName*/) { } DENG_EXTERN_C void R_ProjectSprite(struct mobj_s *mo) { DENG_UNUSED(mo); } doomsday-stable-1.15.7/doomsday/dep_shell.pri0000664000175000017500000000130512641367670020465 0ustar jaakkojaakko# Build configuration for using the libdeng_shell library. shellDir = libshell INCLUDEPATH += $$PWD/$$shellDir/include # Use the appropriate library path. !useLibDir($$OUT_PWD/../$$shellDir) { !useLibDir($$OUT_PWD/../../$$shellDir) { !useLibDir($$OUT_PWD/../../../$$shellDir) { useLibDir($$OUT_PWD/../../builddir/$$shellDir) } } } LIBS += -ldeng_shell macx { defineTest(linkBinaryToBundledLibshell) { fixInstallName($${1}, libdeng_shell.1.dylib, ..) } defineTest(linkToBundledLibshell) { linkBinaryToBundledLibshell($${1}.bundle/$$1) } defineTest(linkDylibToBundledLibshell) { linkBinaryToBundledLibshell($${1}.dylib) } } doomsday-stable-1.15.7/doomsday/dep_lzss.pri0000664000175000017500000000066512641367670020361 0ustar jaakkojaakko# Build configuration for lzss. DENG_LZSS_DIR = $$PWD/external/lzss INCLUDEPATH += $$DENG_LZSS_DIR/portable/include HEADERS += \ $$DENG_LZSS_DIR/portable/include/lzss.h win32 { LIBS += -L$$DENG_LZSS_DIR/win32 -llzss # Installed shared libs. INSTALLS += lzsslibs lzsslibs.files = $$DENG_LZSS_DIR/win32/lzss.dll lzsslibs.path = $$DENG_LIB_DIR } else { SOURCES += \ $$DENG_LZSS_DIR/unix/src/lzss.c } doomsday-stable-1.15.7/doomsday/.gitignore0000664000175000017500000000017112641367670020002 0ustar jaakkojaakkoBUILD.* versions.pri Makefile Doomsday.tmproj mybuild myrelease .kdev4 doomsday.kdev4 *.user config_user.pri apidoc-qch doomsday-stable-1.15.7/doomsday/dep_fmod.pri0000664000175000017500000000140312641367670020302 0ustar jaakkojaakko# Build configuration for FMOD Ex. isEmpty(FMOD_DIR) { error("dep_fmod: FMOD SDK path not defined, check your config_user.pri (FMOD_DIR)") } INCLUDEPATH += \"$$FMOD_DIR/api/inc\" win32 { # Windows. LIBS += -L$$FMOD_DIR/api/lib -lfmodex_vc INSTALLS += fmodlibs fmodlibs.files = $$FMOD_DIR/api/fmodex.dll fmodlibs.path = $$DENG_LIB_DIR } else:macx { # Mac OS X. LIBS += -L$$FMOD_DIR/api/lib -lfmodex # The library must be bundled as a post-build step. } else { # Generic Unix. contains(QMAKE_HOST.arch, x86_64): libname = fmodex64 else: libname = fmodex LIBS += -L$$FMOD_DIR/api/lib -l$$libname INSTALLS += fmodlibs fmodlibs.files = $$FMOD_DIR/api/lib/lib$${libname}.so fmodlibs.path = $$DENG_LIB_DIR } doomsday-stable-1.15.7/doomsday/libdoomsday/0000775000175000017500000000000012641367671020322 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/libdoomsday.pro0000664000175000017500000001246712641367671023364 0ustar jaakkojaakko# The Doomsday Engine Project -- Common Doomsday Subsystems # License: GPL 2+ # Copyright (c) 2014 Jaakko Keränen # Copyright (c) 2014 Daniel Swanson TEMPLATE = lib TARGET = deng_doomsday # Build Configuration -------------------------------------------------------- include(../config.pri) VERSION = $$DENG_VERSION # External Dependencies ------------------------------------------------------ include(../dep_core.pri) include(../dep_legacy.pri) include(../dep_shell.pri) include(../dep_zlib.pri) win32 { # Keep the version number out of the file name. TARGET_EXT = .dll } # Enable strict warnings. *-g++|*-gcc|*-clang* { #warnOpts = -Wall -Wextra -pedantic -Wno-long-long QMAKE_CFLAGS_WARN_ON *= $$warnOpts QMAKE_CXXFLAGS_WARN_ON *= $$warnOpts } *-g++|*-gcc { # We are using code that is not ISO C compliant (anonymous structs/unions) # so disable the warnings about it. QMAKE_CFLAGS_WARN_ON -= -pedantic } *-clang* { QMAKE_CFLAGS_WARN_ON *= -Wno-c11-extensions } win32-msvc* { #QMAKE_CXXFLAGS_WARN_ON ~= s/-W3/-W4/ } INCLUDEPATH += src include include/de $$DENG_API_DIR # Definitions ---------------------------------------------------------------- DEFINES += __DENG__ __DOOMSDAY__ __LIBDOOMSDAY__ !isEmpty(DENG_BUILD) { !win32: echo(Build number: $$DENG_BUILD) DEFINES += DOOMSDAY_BUILD_TEXT=\\\"$$DENG_BUILD\\\" } else { !win32: echo(DENG_BUILD is not defined.) } # Source Files --------------------------------------------------------------- # Public headers HEADERS += \ include/doomsday/audio/logical.h \ include/doomsday/console/alias.h \ include/doomsday/console/cmd.h \ include/doomsday/console/exec.h \ include/doomsday/console/knownword.h \ include/doomsday/console/var.h \ include/doomsday/defs/decoration.h \ include/doomsday/defs/ded.h \ include/doomsday/defs/dedarray.h \ include/doomsday/defs/dedfile.h \ include/doomsday/defs/definition.h \ include/doomsday/defs/dedparser.h \ include/doomsday/defs/dedregister.h \ include/doomsday/defs/dedtypes.h \ include/doomsday/defs/episode.h \ include/doomsday/defs/finale.h \ include/doomsday/defs/mapgraphnode.h \ include/doomsday/defs/mapinfo.h \ include/doomsday/defs/material.h \ include/doomsday/defs/model.h \ include/doomsday/defs/music.h \ include/doomsday/defs/sky.h \ include/doomsday/dualstring.h \ include/doomsday/filesys/file.h \ include/doomsday/filesys/filehandle.h \ include/doomsday/filesys/fileid.h \ include/doomsday/filesys/fileinfo.h \ include/doomsday/filesys/filetype.h \ include/doomsday/filesys/fs_main.h \ include/doomsday/filesys/fs_util.h \ include/doomsday/filesys/lumpcache.h \ include/doomsday/filesys/lumpindex.h \ include/doomsday/filesys/searchpath.h \ include/doomsday/filesys/sys_direc.h \ include/doomsday/filesys/wad.h \ include/doomsday/filesys/zip.h \ include/doomsday/help.h \ include/doomsday/paths.h \ include/doomsday/resource/resourceclass.h \ include/doomsday/resource/wav.h \ include/doomsday/uri.h \ include/doomsday/world/mobj.h \ include/doomsday/world/mobjthinkerdata.h \ include/doomsday/world/thinker.h \ include/doomsday/world/thinkerdata.h win32: HEADERS += \ include/doomsday/filesys/fs_windows.h # Sources and private headers SOURCES += \ src/audio/logical.cpp \ src/console/alias.cpp \ src/console/cmd.cpp \ src/console/exec.cpp \ src/console/knownword.cpp \ src/console/var.cpp \ src/defs/decoration.cpp \ src/defs/ded.cpp \ src/defs/dedfile.cpp \ src/defs/definition.cpp \ src/defs/dedparser.cpp \ src/defs/dedregister.cpp \ src/defs/episode.cpp \ src/defs/finale.cpp \ src/defs/mapgraphnode.cpp \ src/defs/mapinfo.cpp \ src/defs/material.cpp \ src/defs/model.cpp \ src/defs/music.cpp \ src/defs/sky.cpp \ src/dualstring.cpp \ src/filesys/file.cpp \ src/filesys/filehandle.cpp \ src/filesys/fileid.cpp \ src/filesys/filetype.cpp \ src/filesys/fs_main.cpp \ src/filesys/fs_scheme.cpp \ src/filesys/fs_util.cpp \ src/filesys/lumpcache.cpp \ src/filesys/lumpindex.cpp \ src/filesys/searchpath.cpp \ src/filesys/sys_direc.cpp \ src/filesys/wad.cpp \ src/filesys/zip.cpp \ src/help.cpp \ src/paths.cpp \ src/resource/resourceclass.cpp \ src/resource/wav.cpp \ src/uri.cpp \ src/world/mobjthinkerdata.cpp \ src/world/thinker.cpp \ src/world/thinkerdata.cpp win32: SOURCES += \ src/filesys/fs_windows.cpp # Resources ------------------------------------------------------------------ buildPackage(../net.dengine.base, $$OUT_PWD/..) # Installation --------------------------------------------------------------- macx { xcodeFinalizeBuild($$TARGET) linkDylibToBundledLibcore (libdeng_doomsday) linkDylibToBundledLiblegacy(libdeng_doomsday) linkDylibToBundledLibshell (libdeng_doomsday) doPostLink("install_name_tool -id @rpath/libdeng_doomsday.1.dylib libdeng_doomsday.1.dylib") # Update the library included in the main app bundle. doPostLink("mkdir -p ../client/Doomsday.app/Contents/Frameworks") doPostLink("cp -fRp libdeng_doomsday*dylib ../client/Doomsday.app/Contents/Frameworks") } deployLibrary() doomsday-stable-1.15.7/doomsday/libdoomsday/include/0000775000175000017500000000000012641367671021745 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/0000775000175000017500000000000012641367671023564 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/dualstring.h0000664000175000017500000000713412641367671026116 0ustar jaakkojaakko/** * @file dualstring.h * Utility class for strings that need both Unicode and C-string access. * @ingroup data * * @authors Copyright (c) 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDOOMSDAY_DUALSTRING_H #define LIBDOOMSDAY_DUALSTRING_H #ifdef __cplusplus #include "libdoomsday.h" #include #include /** * Maintains a secondary, read-only C-style string (Str instance) side-by-side * with a full de::String. The secondary Str is only updated on demand, when * someone needs access to the C-style string (or Str). There are no guarantees * that a previously returned pointer to the Str contents remains valid or is * up to date after making changes to the de::String side. * * The only allowed situation where the string is modified via the Str side is * when one calls DualString::toStr() and then DualString::update() after the * changes have been done via Str. * * This class should only be used to support legacy code. */ class LIBDOOMSDAY_PUBLIC DualString : public de::String { public: DualString(); DualString(const DualString& other); DualString(const String& other); virtual ~DualString(); DualString& operator = (const DualString& other); DualString& operator = (const String& str); DualString& operator = (const char* cStr); /** * Empties the contents of both the de::String string and the secondary * Str instance. Existing const char* pointers remain valid. */ void clear(); /** * Returns a read-only pointer to the string's secondary side (ASCII * encoding). @return Str instance. */ const Str* toStrAscii() const; /** * Returns a read-only pointer to the string's secondary side (UTF-8 * encoding). @return Str instance. */ const Str* toStrUtf8() const; /** * Returns a modifiable Str (UTF-8). After making changes, you have to call * update() to copy the new contents to the de::String side. */ Str* toStr(); /** * Copies the contents of the Str side, assumed to be in UTF-8 encoding, to * the de::String side. */ void update(); /** * Converts the contents of the string to ASCII and returns a read-only * pointer to the ASCII, null-terminated C style string. Ownership of the * returned string is kept by DualString. The returned pointer is only * valid while contents of the DualString remain unchanged; during this * time, the caller may hang on to the returned string pointer. * * @return String contents as ASCII. */ const char* asciiCStr(); /** * Works like asciiCStr() but converts the C style string to UTF-8 encoding. * * @return String contents as UTF-8. */ const char* utf8CStr(); private: Str* _str; }; #endif // __cplusplus #endif // LIBDOOMSDAY_DUALSTRING_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/audio/0000775000175000017500000000000012641367671024665 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/audio/logical.h0000664000175000017500000000307212641367671026452 0ustar jaakkojaakko/** @file logical.h Logical Sound Manager. * * @authors Copyright © 2003-2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301 USA */ #ifndef LIBDOOMSDAY_AUDIO_LOGICAL_H #define LIBDOOMSDAY_AUDIO_LOGICAL_H #include "../libdoomsday.h" #include "../world/mobj.h" #include #ifdef __cplusplus extern "C" { #endif LIBDOOMSDAY_PUBLIC void Sfx_InitLogical(void); LIBDOOMSDAY_PUBLIC void Sfx_PurgeLogical(void); LIBDOOMSDAY_PUBLIC void Sfx_StartLogical(int id, mobj_t *origin, dd_bool isRepeating); LIBDOOMSDAY_PUBLIC int Sfx_StopLogical(int id, mobj_t *origin); LIBDOOMSDAY_PUBLIC dd_bool Sfx_IsPlaying(int id, mobj_t *origin); LIBDOOMSDAY_PUBLIC void Sfx_Logical_SetOneSoundPerEmitter(dd_bool enabled); LIBDOOMSDAY_PUBLIC void Sfx_Logical_SetSampleLengthCallback(uint (*callback)(int)); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDOOMSDAY_AUDIO_LOGICAL_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/libdoomsday.h0000664000175000017500000000313712641367671026247 0ustar jaakkojaakko/** @file libdoomsday.h Common definitions for libdoomsday. * * @authors Copyright © 2013 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_MAIN_H #define LIBDOOMSDAY_MAIN_H #include /* * The LIBDOOMSDAY_PUBLIC macro is used for declaring exported symbols. It must be * applied in all exported classes and functions. DEF files are not used for * exporting symbols out of libdoomsday. */ #if defined(_WIN32) && defined(_MSC_VER) # ifdef __LIBDOOMSDAY__ // This is defined when compiling the library. # define LIBDOOMSDAY_PUBLIC __declspec(dllexport) # else # define LIBDOOMSDAY_PUBLIC __declspec(dllimport) # endif #else // No need to use any special declarators. # define LIBDOOMSDAY_PUBLIC #endif #ifdef __cplusplus # define LIBDOOMSDAY_EXTERN_C extern "C" #else # define LIBDOOMSDAY_EXTERN_C extern #endif #endif // LIBDOOMSDAY_MAIN_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/paths.h0000664000175000017500000000246112641367671025057 0ustar jaakkojaakko/** @file basepath.h Application base path. * * @deprecated Should use de::App instead. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_BASEPATH_H #define LIBDOOMSDAY_BASEPATH_H #include "libdoomsday.h" #include "dd_types.h" #ifdef __cplusplus extern "C" { #endif LIBDOOMSDAY_PUBLIC char const *DD_BasePath(); LIBDOOMSDAY_PUBLIC char const *DD_RuntimePath(); LIBDOOMSDAY_PUBLIC void DD_SetBasePath(char const *path); LIBDOOMSDAY_PUBLIC void DD_SetRuntimePath(char const *path); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDOOMSDAY_BASEPATH_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/0000775000175000017500000000000012641367671025242 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/zip.h0000664000175000017500000002417312641367671026224 0ustar jaakkojaakko/** @file zip.h ZIP Archive (File). * * @author Copyright © 2003-2014 Jaakko Keränen * @author Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDOOMSDAY_FILESYS_ZIP_H #define LIBDOOMSDAY_FILESYS_ZIP_H #include "../libdoomsday.h" #include "../filesys/lumpindex.h" #include "file.h" #include "fileinfo.h" #include #include namespace de { /** * ZIP archive file format. * @ingroup resource * * @note Presently only the zlib method (Deflate) of compression is supported. * * @see file.h, File1 * * @todo This is obsolete: should use ZipArchive/ArchiveFolder in libcore. */ class LIBDOOMSDAY_PUBLIC Zip : public File1, public LumpIndex { protected: struct Entry; // forward public: /// Base class for format-related errors. @ingroup errors DENG2_ERROR(FormatError); /** * File system object for a lump in the ZIP. * * The purpose of this abstraction is to redirect various File1 methods to the * containing Zip file. Such a mechanism would be unnecessary in a file system * in which proper OO design is used for the package / file abstraction. -ds */ class LumpFile : public File1 { public: LumpFile(Entry &entry, FileHandle &hndl, String path, FileInfo const &info, File1 *container); /// @return Name of this file. String const &name() const; /** * Compose an absolute URI to this file. * * @param delimiter Delimit directory using this character. * * @return The absolute URI. */ Uri composeUri(QChar delimiter = '/') const; /** * Retrieve the directory node for this file. * * @return Directory node for this file. */ PathTree::Node &directoryNode() const; /** * Read the file data into @a buffer. * * @param buffer Buffer to read into. Must be at least large enough to * contain the whole file. * @param tryCache @c true= try the lump cache first. * * @return Number of bytes read. * * @see size() or info() to determine the size of buffer needed. */ size_t read(uint8_t *buffer, bool tryCache = true); /** * Read a subsection of the file data into @a buffer. * * @param buffer Buffer to read into. Must be at least @a length bytes. * @param startOffset Offset from the beginning of the file to start reading. * @param length Number of bytes to read. * @param tryCache If @c true try the local data cache first. * * @return Number of bytes read. */ size_t read(uint8_t *buffer, size_t startOffset, size_t length, bool tryCache = true); /** * Read this lump into the local cache. * @return Pointer to the cached copy of the associated data. */ uint8_t const *cache(); /** * Remove a lock on the locally cached data. * * @return This instance. */ LumpFile &unlock(); /** * Convenient method returning the containing Zip file instance. */ Zip &zip() const; private: Entry &entry; }; public: Zip(FileHandle &hndl, String path, FileInfo const &info, File1 *container = 0); virtual ~Zip(); /** * Read the data associated with lump @a lumpIndex into @a buffer. * * @param lumpIndex Lump index associated with the data to be read. * @param buffer Buffer to read into. Must be at least large enough to * contain the whole lump. * @param tryCache @c true= try the lump cache first. * * @return Number of bytes read. * * @throws NotFoundError If @a lumpIndex is not valid. * * @see lumpSize() or lumpInfo() to determine the size of buffer needed. */ size_t readLump(int lumpIndex, uint8_t *buffer, bool tryCache = true); /** * Read a subsection of the data associated with lump @a lumpIndex into @a buffer. * * @param lumpIndex Lump index associated with the data to be read. * @param buffer Buffer to read into. Must be at least @a length bytes. * @param startOffset Offset from the beginning of the lump to start reading. * @param length Number of bytes to read. * @param tryCache @c true= try the lump cache first. * * @return Number of bytes read. * * @throws NotFoundError If @a lumpIndex is not valid. */ size_t readLump(int lumpIndex, uint8_t *buffer, size_t startOffset, size_t length, bool tryCache = true); /** * Read the data associated with lump @a lumpIndex into the cache. * * @param lumpIndex Lump index associated with the data to be cached. * * @return Pointer to the cached copy of the associated data. */ uint8_t const *cacheLump(int lumpIndex); /** * Remove a lock on a cached data lump. * * @param lumpIndex Lump index associated with the cached data to be changed. */ void unlockLump(int lumpIndex); /** * Clear any cached data for lump @a lumpIndex from the lump cache. * * @param lumpIndex Lump index associated with the cached data to be cleared. * @param retCleared If not @c NULL write @c true to this address if data was * present and subsequently cleared from the cache. */ void clearCachedLump(int lumpIndex, bool *retCleared = 0); /** * Purge the lump cache, clearing all cached data lumps. */ void clearLumpCache(); public: /** * Determines whether the specified file appears to be in a format recognised by * Zip. * * @param file Stream file handle/wrapper to the file being interpreted. * * @return @c true= this is a file that can be represented using Zip. */ static bool recognise(FileHandle &file); /** * Inflates a block of data compressed using ZipFile_Compress() (i.e., zlib * deflate algorithm). * * @param in Pointer to compressed data. * @param inSize Size of the compressed data. * @param outSize Size of the uncompressed data is written here. Must not be @c NULL. * * @return Pointer to the uncompressed data. Caller gets ownership of the * returned memory and must free it with M_Free(). * * @see compress() */ static uint8_t *uncompress(uint8_t *in, size_t inSize, size_t *outSize); /** * Inflates a compressed block of data using zlib. The caller must figure out * the uncompressed size of the data before calling this. * * zlib will expect raw deflate data, not looking for a zlib or gzip header, * not generating a check value, and not looking for any check values for * comparison at the end of the stream. * * @param in Pointer to compressed data. * @param inSize Size of the compressed data. * @param out Pointer to output buffer. * @param outSize Size of the output buffer. This must match the size of the * decompressed data. * * @return @c true if successful. */ static bool uncompressRaw(uint8_t *in, size_t inSize, uint8_t *out, size_t outSize); /** * Compresses a block of data using zlib with the default/balanced compression level. * * @param in Pointer to input data to compress. * @param inSize Size of the input data. * @param outSize Pointer where the size of the compressed data will be written. * Cannot be @c NULL. * * @return Compressed data. The caller gets ownership of this memory and must * free it with M_Free(). If an error occurs, returns @c NULL and * @a outSize is set to zero. */ static uint8_t *compress(uint8_t *in, size_t inSize, size_t *outSize); /** * Compresses a block of data using zlib. * * @param in Pointer to input data to compress. * @param inSize Size of the input data. * @param outSize Pointer where the size of the compressed data will be written. * Cannot be @c NULL. * @param level Compression level: 0=none/fastest ... 9=maximum/slowest. * * @return Compressed data. The caller gets ownership of this memory and must * free it with M_Free(). If an error occurs, returns @c NULL and * @a outSize is set to zero. */ static uint8_t *compressAtLevel(uint8_t *in, size_t inSize, size_t *outSize, int level); protected: /** * Models an entry in the internal lump tree. */ struct Entry : public PathTree::Node { dsize offset; dsize size; dsize compressedSize; QScopedPointer lumpFile; ///< File system object for the lump data. Entry(PathTree::NodeArgs const &args) : Node(args), offset(0), size(0), compressedSize(0) {} LumpFile &file() const; }; typedef PathTreeT LumpTree; /** * Provides access to the internal LumpTree, for efficient traversal. */ LumpTree const &lumpTree() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBDOOMSDAY_FILESYS_ZIP_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/lumpcache.h0000664000175000017500000000443212641367671027357 0ustar jaakkojaakko/** @file lumpcache.h Provides a data cache tailored to storing lumps (i.e., files). * * @author Copyright © 2013-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_FILESYS_LUMPCACHE_H #define LIBDENG_FILESYS_LUMPCACHE_H #include "../libdoomsday.h" #include "dd_types.h" #include /** * @ingroup fs */ class LIBDOOMSDAY_PUBLIC LumpCache { private: /** * Data item. Represents a lump of data in the cache. */ class Data { public: explicit Data(uint8_t *data = 0); ~Data(); uint8_t *data() const; uint8_t const *replaceData(uint8_t *newData); Data &clearData(bool *retCleared = 0); Data &lock(); Data &unlock(); private: uint8_t *data_; }; typedef std::vector DataCache; public: explicit LumpCache(uint size); ~LumpCache(); uint size() const; bool isValidIndex(uint idx) const; uint8_t const *data(uint lumpIdx) const; LumpCache &insert(uint lumpIdx, uint8_t *data); LumpCache &insertAndLock(uint lumpIdx, uint8_t *data); LumpCache &lock(uint lumpIdx); LumpCache &unlock(uint lumpIdx); LumpCache &remove(uint lumpIdx, bool *retRemoved = 0); LumpCache &clear(); protected: Data *cacheRecord(uint lumpIdx); Data const *cacheRecord(uint lumpIdx) const; private: uint _size; ///< Number of data lumps which can be stored in the cache. DataCache *_dataCache; ///< The cached data. }; #endif /* LIBDENG_FILESYS_LUMPCACHE_H */ doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/fs_main.h0000664000175000017500000004477312641367671027046 0ustar jaakkojaakko/** * @file fs_main.h * * Virtual file system and file (input) stream abstraction layer. * * This version supports runtime (un)loading. * * File input. Can read from real files or WAD lumps. Note that reading from * WAD lumps means that a copy is taken of the lump when the corresponding * 'file' is opened. With big files this uses considerable memory and time. * * @ingroup fs * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_FILESYS_MAIN_H #define LIBDENG_FILESYS_MAIN_H #include "file.h" #include "filehandle.h" #ifdef WIN32 # include "fs_windows.h" #endif #ifdef __cplusplus #include #include #include #include #include "fileinfo.h" #include "filetype.h" #include "searchpath.h" #include "../resource/resourceclass.h" #include "../filesys/lumpindex.h" /** * @defgroup fs File System */ /** * @defgroup resourceLocationFlags Resource Location Flags * * Flags used with de::FS1::find(). * @ingroup flags */ ///@{ #define RLF_MATCH_EXTENSION 0x1 /// If an extension is specified in the search term the found file should have it too. /// Default flags. #define RLF_DEFAULT 0 ///@} namespace de { /** * Files with a .wad extension are archived data files with multiple 'lumps', * other files are single lumps whose base filename will become the lump name. * * Internally the lump index has two parts: the Primary index (which is populated * with lumps from loaded data files) and the Auxiliary index (used to temporarily * open a file that is not considered part of the filesystem). * * Functions that don't know the absolute/logical lumpnum of file will have to * check both indexes (e.g., FS1::lumpNumForName()). * * @ingroup fs */ class LIBDOOMSDAY_PUBLIC FS1 { public: /// No files found. @ingroup errors DENG2_ERROR(NotFoundError); /// An unknown scheme was referenced. @ingroup errors DENG2_ERROR(UnknownSchemeError); /** * (Search) path groupings in descending priority. */ enum PathGroup { /// 'Override' paths have the highest priority. These are usually /// set according to user specified paths, e.g., via the command line. OverridePaths, /// 'Extra' paths are those which are determined dynamically when some /// runtime resources are loaded. The DED module utilizes these to add /// new model search paths found when parsing definition files. ExtraPaths, /// Default paths are those which are known a priori. These are usually /// determined at compile time and are implicit paths relative to the /// virtual file system. DefaultPaths, /// Fallback (i.e., last-resort) paths have the lowest priority. These /// are usually set according to user specified paths, e.g., via the /// command line. FallbackPaths }; /** * Scheme defines a file system subspace. * * @todo The symbolic name of the schme and the path mapping template * (mapPath()) should be defined externally. -ds */ class LIBDOOMSDAY_PUBLIC Scheme { public: /// Symbolic names must be at least this number of characters. static int const min_name_length = URI_MINSCHEMELENGTH; enum Flag { /// Packages may include virtual file mappings to the scheme with a /// root directory which matches the symbolic name of the scheme. /// /// @see mapPath() MappedInPackages = 0x01 }; Q_DECLARE_FLAGS(Flags, Flag) /// Groups of search paths ordered by priority. typedef QMultiMap SearchPaths; /// List of found file nodes. typedef QList FoundNodes; public: explicit Scheme(String symbolicName, Flags flags = 0); ~Scheme(); /// @return Symbolic name of this scheme (e.g., "Models"). String const &name() const; /** * Clear this scheme back to it's "empty" state (i.e., no resources). * The search path groups are unaffected. */ void clear(); /** * Rebuild this scheme by re-scanning for resources on all search paths * and re-populating the scheme's index. * * @note Any manually added resources will not be present after this. */ void rebuild(); /** * Reset this scheme, returning it to an empty state and clearing any * @ref ExtraPaths which have been registered since its construction. */ inline void reset() { clearSearchPathGroup(ExtraPaths); clear(); } /** * Manually add a resource to this scheme. Duplicates are pruned automatically. * * @param resourceNode Node which represents the resource. * * @return @c true iff this scheme did not already contain the resource. */ bool add(PathTree::Node &resourceNode); /** * Finds all resources in this scheme. * * @param name If not an empty string, only consider resources whose * name begins with this. Case insensitive. * @param found Set of resources which match the search. * * @return Number of found resources. */ int findAll(String name, FoundNodes &found); /** * Add a new search path to this scheme. Newer paths have priority over * previously added paths. * * @param path New unresolved search path to add. A copy is made. * @param group Group to add this path to. @ref PathGroup * * @return @c true if @a path was well-formed and subsequently added. */ bool addSearchPath(SearchPath const &path, PathGroup group = DefaultPaths); /** * Clear search paths in @a group from the scheme. * * @param group Search path group to be cleared. */ void clearSearchPathGroup(PathGroup group); /** * Provides access to the search paths for efficient traversals. */ SearchPaths const &allSearchPaths() const; /** * Clear all search paths in all groups in the scheme. */ void clearAllSearchPaths(); /** * Apply mapping for this scheme to the specified path. Mapping must be * enabled (with @ref MappedInPackages) otherwise this does nothing. * * For example, given the scheme name "models": * *
         *     "models/mymodel.dmd" => "$(App.DataPath)/$(GamePlugin.Name)/models/mymodel.dmd"
         * 
* * @param path The path to be mapped (applied in-place). * * @return @c true iff mapping was applied to the path. */ bool mapPath(String &path) const; #ifdef DENG_DEBUG void debugPrint() const; #endif private: struct Instance; Instance *d; }; /// File system subspace schemes. typedef QMap Schemes; /** * PathListItem represents a found path for find file search results. */ struct PathListItem { Path path; int attrib; PathListItem(Path const &_path, int _attrib = 0) : path(_path), attrib(_attrib) {} bool operator < (PathListItem const &other) const { return path < other.path; } }; /// List of found path search results. typedef QList PathList; /// List of file search results. typedef QList FileList; public: /** * Constructs a new file system. */ FS1(); /// Register the console commands, variables, etc..., of this module. static void consoleRegister(); /** * @post No more WADs will be loaded in startup mode. */ void endStartup(); /** * Find a Scheme by symbolic name. * * @param name Symbolic name of the scheme. * @return Scheme associated with @a name. */ Scheme &scheme(String name); /** * @param name Unique symbolic name of the new scheme. Must be at least * @c Scheme::min_name_length characters long. * @param flags @ref Scheme::Flag */ Scheme &createScheme(String name, Scheme::Flags flags = 0); /** * Returns @c true iff a Scheme exists with the symbolic @a name. */ bool knownScheme(String name); /** * Returns the schemes for efficient traversal. */ Schemes const &allSchemes(); /** * Reset all the schemes, returning their indexes to an empty state and clearing * any @ref ExtraPaths which have been registered since creation. */ inline void resetAllSchemes() { Schemes schemes = allSchemes(); DENG2_FOR_EACH(Schemes, i, schemes){ (*i)->reset(); } } /** * Add a new path mapping from source to destination. * @note Paths will be transformed into absolute paths if needed. */ void addPathMapping(String source, String destination); /** * Clears all virtual path mappings. */ void clearPathMappings(); /** * Add a new lump mapping so that @a lumpName becomes visible at @a destination. */ void addPathLumpMapping(String lumpName, String destination); /** * Clears all path => lump mappings. * * @return This instance. */ void clearPathLumpMappings(); /** * @return @c true if a file exists at @a path which can be opened for reading. */ bool accessFile(Uri const &path); /** * Maintains a list of identifiers already seen. * * @return @c true if the given file can be opened, or * @c false if it has already been opened. */ bool checkFileId(Uri const &path); /** * Reset known fileId records so that the next time checkFileId() is called for * a filepath, it will pass. */ void resetFileIds(); /** * @param hndl Handle to the file to be interpreted. Ownership is passed to * the interpreted file instance. * @param path Absolute VFS path by which the interpreted file will be known. * @param info Prepared info metadata for the file. * * @return The interpreted File file instance. */ File1 &interpret(FileHandle &hndl, String path, FileInfo const &info); /** * Indexes @a file (which must have been opened with this file system) into * this file system and adds it to the list of loaded files. * * @param file The file to index. Assumed to have not yet been indexed! */ void index(File1 &file); /** * Removes a file from any indexes. * * @param file File to remove from the index. */ void deindex(File1 &file); /// Clear all references to this file. void releaseFile(File1 &file); /** * Lookup a lump by name. * * @param name Name of the lump to lookup. * @return Logical lump number for the found lump; otherwise @c -1. * * @todo At this level there should be no distinction between lumps. -ds */ lumpnum_t lumpNumForName(String name); /** * Provides access to the main index of the file system. This can be * used for efficiently looking up files based on name. */ LumpIndex const &nameIndex() const; /** * Convenient method of looking up a file from the lump name index given its * unique @a lumpnum. * * @see nameIndex(), LumpIndex::toLump() */ inline File1 &lump(lumpnum_t lumpnum) const { return nameIndex()[lumpnum]; } inline int lumpCount() const { return nameIndex().size(); } /** * Opens the given file (will be translated) for reading. * * @post If @a allowDuplicate = @c false a new file ID for this will have been * added to the list of known file identifiers if this file hasn't yet been * opened. It is the responsibility of the caller to release this identifier when done. * * @param path Possibly relative or mapped path to the resource being opened. * @param mode 'b' = binary * 't' = text mode (with real files, lumps are always binary) * * 'f' = must be a real file in the local file system * @param baseOffset Offset from the start of the file in bytes to begin. * @param allowDuplicate @c false = open only if not already opened. * * @return Handle to the opened file. * * @throws NotFoundError If the requested file could not be found. */ FileHandle &openFile(String const &path, String const &mode, size_t baseOffset = 0, bool allowDuplicate = true); /** * Try to open the specified lump for reading. * * @param lump The file to be opened. * * @return Handle to the opened file. * * @todo This method is no longer necessary at this level. Opening a file which * is already present in the file system should not require calling back to a * method of the file system itself (bad OO design). */ FileHandle &openLump(File1 &lump); /** * Find a single file. * * @param search The search term. * @return Found file. */ File1 &find(Uri const &search); /** * Finds all files which meet the supplied @a predicate. * * @param predicate If not @c NULL, this predicate evaluator callback must * return @c true for a given file to be included in the * @a found FileList. * @param parameters Passed to the predicate evaluator callback. * @param found Set of files that match the result. * * @return Number of files found. */ int findAll(bool (*predicate)(File1 &file, void *parameters), void *parameters, FileList &found) const; /** * Finds all files of a specific type which meet the supplied @a predicate. * Only files that can be represented as @a Type are included in the results. * * @param predicate If not @c NULL, this predicate evaluator callback must * return @c true for a given file to be included in the * @a found FileList. * @param parameters Passed to the predicate evaluator callback. * @param found Set of files that match the result. * * @return Number of files found. */ template int findAll(bool (*predicate)(File1 &file, void *parameters), void *parameters, FileList &found) const { findAll(predicate, parameters, found); // Filter out the wrong types. QMutableListIterator i(found); while(i.hasNext()) { i.next(); if(!i.value()->file().is()) { i.remove(); } } return found.count(); } /** * Search the file system for a path to a file. * * @param search The search term. If a scheme is specified, first check * for a similarly named Scheme with which to limit the * search. If not found within the scheme then perform a * wider search of the whole file system. * @param flags @ref resourceLocationFlags * @param rclass Class of resource being searched for. If no file is found * which matches the search term and a non-null resource * class is specified try alternative names for the file * according to the list of known file extensions for each * file type associated with this class of resource. * * @return The found path. * * @throws NotFoundError If the requested file could not be found. * * @todo Fold into find() -ds */ String findPath(Uri const &search, int flags, ResourceClass &rclass); String findPath(Uri const &search, int flags); /** * Finds all paths which match the search criteria. Will search the Zip * lump index, lump => path mappings and native files in the local system. * * @param searchPattern Pattern which defines the scope of the search. * @param flags @ref searchPathFlags * @param found Set of (absolute) paths that match the result. * * @return Number of paths found. */ int findAllPaths(Path searchPattern, int flags, PathList &found); /** * Print contents of the specified directory of the virtual file system. */ void printDirectory(Path path); /** * Calculate a CRC for the loaded file list. */ uint loadedFilesCRC(); /** * Provides access to the list of all loaded files (in load order), for * efficient traversal. */ FileList const &loadedFiles() const; /** * Unload all files loaded after startup. * @return Number of files unloaded. */ int unloadAllNonStartupFiles(); private: DENG2_PRIVATE(d) }; Q_DECLARE_OPERATORS_FOR_FLAGS(FS1::Scheme::Flags) } // namespace de LIBDOOMSDAY_PUBLIC de::FS1 &App_FileSystem(); /** * Returns the application's data base path in the format expected by FS1. * @return Base path. */ LIBDOOMSDAY_PUBLIC de::String App_BasePath(); /// Initialize this module. Cannot be re-initialized, must shutdown first. LIBDOOMSDAY_PUBLIC void F_Init(); /// Shutdown this module. LIBDOOMSDAY_PUBLIC void F_Shutdown(); LIBDOOMSDAY_PUBLIC void const *F_LumpIndex(); #endif // __cplusplus #endif /* LIBDENG_FILESYS_MAIN_H */ doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/searchpath.h0000664000175000017500000000616712641367671027547 0ustar jaakkojaakko/** * @file searchpath.h * Search Path. @ingroup fs * * @author Copyright © 2010-2013 Daniel Swanson * @author Copyright © 2010-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_FILESYS_SEARCHPATH_H #define LIBDENG_FILESYS_SEARCHPATH_H #ifdef __cplusplus #include "../libdoomsday.h" #include // std::swap #include "dd_types.h" #include "../uri.h" #if WIN32 # if defined(SearchPath) # undef SearchPath # endif #endif namespace de { /** * SearchPath is the pairing of a @ref de::Uri plus a set of flags which * determine how the URI should be interpreted. * * This class is intended as a convenient way to manage these two pieces * of closely related information as a unit. * * @ingroup fs */ class LIBDOOMSDAY_PUBLIC SearchPath : public Uri { public: /// @defgroup searchPathFlags Search Path Flags /// @ingroup flags enum Flag { /// Interpreters should not decend into branches. NoDescend = 0x1 }; Q_DECLARE_FLAGS(Flags, Flag) public: /** * @param _uri Unresolved search URI (may include symbolic names or * other symbol references). * @param _flags @ref searchPathFlags */ SearchPath(Uri const& _uri, Flags _flags = 0); /** * Construct a copy from @a other. This is a "deep copy". */ SearchPath(SearchPath const& other); inline SearchPath& operator = (SearchPath other) { Uri::swap(other); std::swap(flags_, other.flags_); return *this; } /** * Swaps this SearchPath with @a other. * @param other SearchPath. */ inline void swap(SearchPath& other) { // nothrow Uri::swap(other); std::swap(flags_, other.flags_); } /// Returns the interpretation flags for the search path. Flags flags() const; /** * Change interpretation flags for the search path. * @param flags New flags. * @return This instance. */ SearchPath& setFlags(Flags flags); private: Flags flags_; }; Q_DECLARE_OPERATORS_FOR_FLAGS(SearchPath::Flags) } // namespace de namespace std { // std::swap specialization for de::SearchPath template <> inline void swap(de::SearchPath& a, de::SearchPath& b) { a.swap(b); } } #endif // __cplusplus #endif /* LIBDENG_FILESYS_SEARCHPATH_H */ doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/sys_direc.h0000664000175000017500000000663112641367671027405 0ustar jaakkojaakko/** @file sys_direc.h * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * Directory Utilities. */ #ifndef LIBDENG_DIREC_H #define LIBDENG_DIREC_H #include "../libdoomsday.h" #include "dd_types.h" #ifdef WIN32 # define DENG_DIR_SEP_CHAR '\\' # define DENG_DIR_SEP_STR "\\" # define DENG_DIR_WRONG_SEP_CHAR '/' #else # define DENG_DIR_SEP_CHAR '/' # define DENG_DIR_SEP_STR "/" # define DENG_DIR_WRONG_SEP_CHAR '\\' #endif #ifdef __cplusplus extern "C" { #endif #define FILENAME_T_MAXLEN 256 #define FILENAME_T_LASTINDEX 255 typedef char filename_t[FILENAME_T_MAXLEN]; typedef struct directory_s { #if defined(WIN32) int drive; #endif filename_t path; } directory_t; /// Construct using the specified path. LIBDOOMSDAY_PUBLIC directory_t* Dir_New(const char* path); /// Construct using the current working directory path. LIBDOOMSDAY_PUBLIC directory_t* Dir_NewFromCWD(void); /** * Construct by extracting the path from @a path. * \note if not absolute then it will be interpeted as relative to the current * working directory path. */ LIBDOOMSDAY_PUBLIC directory_t* Dir_FromText(const char* path); LIBDOOMSDAY_PUBLIC void Dir_Delete(directory_t* dir); /** * @return "Raw" version of the present path. */ LIBDOOMSDAY_PUBLIC const char* Dir_Path(directory_t* dir); /// Class-Static Members: /** * Clean up given path. Whitespace is trimmed. Path separators are converted * into their system-specific form. On Unix '~' expansion is applied. */ LIBDOOMSDAY_PUBLIC void Dir_CleanPath(char* path, size_t len); LIBDOOMSDAY_PUBLIC void Dir_CleanPathStr(ddstring_t* str); /** * @return Absolute path to the current working directory for the default drive. * Always ends with a '/'. Path must be released with M_Free() * @c NULL if we are out of memory. */ LIBDOOMSDAY_PUBLIC char* Dir_CurrentPath(void); /** * Convert a path into an absolute path. If @a path is relative it is considered * relative to the current working directory. On Unix '~' expansion is applied. * * @param path Path to be translated. * @param len Length of the buffer used for @a path (in bytes). */ LIBDOOMSDAY_PUBLIC void Dir_MakeAbsolutePath(char* path, size_t len); /** * Check that the given directory exists. If it doesn't, create it. * @return @c true if successful. */ LIBDOOMSDAY_PUBLIC dd_bool Dir_mkpath(const char* path); /** * Attempt to change the current working directory to the path defined. * @return @c true if the change was successful. */ LIBDOOMSDAY_PUBLIC dd_bool Dir_SetCurrent(const char* path); #ifdef __cplusplus } // extern "C" #endif #endif /* LIBDENG_DIREC_H */ doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/fileid.h0000664000175000017500000000652112641367671026653 0ustar jaakkojaakko/** * @file fileid.h * * Implements a file identifier in terms of a MD5 hash of its absolute path. * * @ingroup types * * @deprecated Should use FS2 instead for file access. * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_FILEID_H #define LIBDENG_FILEID_H #include "../libdoomsday.h" #include #include #include namespace de { /** * File identifier (an MD5 hash). */ class LIBDOOMSDAY_PUBLIC FileId : public LogEntry::Arg::Base { public: typedef QByteArray Md5Hash; public: explicit FileId(Md5Hash _md5); FileId(FileId const& other); FileId& operator = (FileId other); /// @return @c true= this FileId is lexically less than @a other. bool operator < (FileId const& other) const; /// @return @c true= this FileId is equal to @a other (identical hashes). bool operator == (FileId const& other) const; /// @return @c true= this FileId is not equal @a other (differing hashes). bool operator != (FileId const& other) const; friend void swap(FileId& first, FileId& second) // nothrow { #ifdef DENG2_QT_4_8_OR_NEWER first.md5_.swap(second.md5_); # ifdef DENG_DEBUG first.path_.swap(second.path_); # endif #else std::swap(first.md5_, second.md5_); # ifdef DENG_DEBUG std::swap(first.path_, second.path_); # endif #endif } /// Converts this FileId to a text string. operator String () const { return asText(); } /// Converts this FileId to a text string. String asText() const; // Implements LogEntry::Arg::Base. LogEntry::Arg::Type logEntryArgType() const { return LogEntry::Arg::StringArgument; } /// @return Md5hash for this FileId. Md5Hash const& md5() const { return md5_; } #ifdef DENG_DEBUG /// @return Path attributed to this FileId. String const& path() const { return path_; } /// Set the path attributed to this FileId. FileId& setPath(String path) { path_ = path; return *this; } #endif /** * Constructs a new FileId instance by hashing the absolute @a path. * @param path Path to be hashed. * @return Newly construced FileId. */ static FileId fromPath(String path); /** * Calculate an MD5 identifier for the absolute @a path. * @return MD5 hash of the path. */ static Md5Hash hash(String path); private: Md5Hash md5_; #ifdef DENG_DEBUG String path_; #endif }; } // namespace de #endif /* LIBDENG_FILEID_H */ doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/lumpindex.h0000664000175000017500000001770512641367671027432 0ustar jaakkojaakko/** @file lumpindex.h Index of lumps. * * @todo Move the definition of lumpnum_t into this header. * * @author Copyright © 2003-2014 Jaakko Keränen * @author Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_FILESYS_LUMPINDEX_H #define LIBDENG_FILESYS_LUMPINDEX_H #ifdef __cplusplus #include "../libdoomsday.h" #include "file.h" #include "fileinfo.h" #include #include namespace de { /** * Virtual file system component used to model an indexable collection of * lumps. A single index may include lumps originating from many different * file containers. * * @ingroup fs */ class LIBDOOMSDAY_PUBLIC LumpIndex { public: /// No file(s) found. @ingroup errors DENG2_ERROR(NotFoundError); typedef QList Lumps; typedef std::list FoundIndices; /** * Heuristic based map data (format) recognizer. * * Unfortunately id Tech 1 maps cannot be easily recognized, due to their * lack of identification signature, the mechanics of the WAD format lump * index and the existence of several subformat variations. Therefore it is * necessary to use heuristic analysis of the lump index and the lump data. */ class LIBDOOMSDAY_PUBLIC Id1MapRecognizer { public: /// Logical map format identifiers. enum Format { UnknownFormat = -1, DoomFormat, HexenFormat, Doom64Format, KnownFormatCount }; /// Logical map data type identifiers: enum DataType { UnknownData = -1, ThingData, LineDefData, SideDefData, VertexData, SegData, SubsectorData, NodeData, SectorDefData, RejectData, BlockmapData, BehaviorData, ScriptData, TintColorData, MacroData, LeafData, GLVertexData, GLSegData, GLSubsectorData, GLNodeData, GLPVSData, KnownDataCount }; typedef QMap Lumps; public: /** * Attempt to recognize an id Tech 1 format by traversing the WAD lump * index, beginning at the @a lumpIndexOffset specified. */ Id1MapRecognizer(LumpIndex const &lumpIndex, lumpnum_t lumpIndexOffset); String const &id() const; Format format() const; Lumps const &lumps() const; File1 *sourceFile() const; /** * Returns the lump index number of the last data lump inspected by the * recognizer, making it possible to collate/locate all the map data sets * using multiple recognizers. */ lumpnum_t lastLump() const; public: /** * Returns the textual name for the identified map format @a id. */ static String const &formatName(Format id); /** * Determines the type of a map data lump by @a name. */ static DataType typeForLumpName(String name); /** * Determine the size (in bytes) of an element of the specified map data * lump @a type for the current map format. * * @param mapFormat Map format identifier. * @param dataType Map lump data type. * * @return Size of an element of the specified type. */ static dsize elementSizeForDataType(Format mapFormat, DataType dataType); private: DENG2_PRIVATE(d) }; public: /** * @param pathsAreUnique Lumps in the index must have unique paths. Inserting * a lump with the same path as one which already exists * will result in the earlier lump being pruned. */ explicit LumpIndex(bool pathsAreUnique = false); virtual ~LumpIndex(); /** * Returns @c true iff the directory contains no lumps. */ inline bool isEmpty() const { return !size(); } /** * Returns the total number of lumps in the directory. */ int size() const; /// @copydoc size() inline int lumpCount() const { return size(); } // alias /** * Returns the logicial index of the last lump in the directory, or @c -1 if empty. */ int lastIndex() const; /** * Returns @c true iff @a lumpNum can be interpreted as a valid lump index. */ bool hasLump(lumpnum_t lumpNum) const; /** * Returns @c true iff the index contains one or more lumps with a matching @a path. */ bool contains(Path const &path) const; /** * Finds all indices for lumps with a matching @a path. * * @param path Path of the lump(s) to . * @param found Set of lumps that match @a path (in load order; most recent last). * * @return Number of lumps found. * * @see findFirst(), findLast() */ int findAll(Path const &path, FoundIndices &found) const; /** * Returns the index of the @em first loaded lump with a matching @a path. * If no lump is found then @c -1 is returned. * * @see findLast(), findAll() */ lumpnum_t findFirst(Path const &path) const; /** * Returns the index of the @em last loaded lump with a matching @a path. * If no lump is found then @c -1 is returned. * * @see findFirst(), findAll() */ lumpnum_t findLast(Path const &path) const; /** * Lookup a file at specific offset in the index. * * @param lumpNum Logical lumpnum associated to the file being looked up. * * @return The requested file. * * @throws NotFoundError If the requested file could not be found. * @see hasLump() */ File1 &lump(lumpnum_t lumpNum) const; /** * @copydoc lump() * @see lump() */ inline File1 &operator [] (lumpnum_t lumpNum) const { return lump(lumpNum); } /** * Provides access to list containing @em all the lumps, for efficient traversals. */ Lumps const &allLumps() const; /** * Clear the index back to its default (i.e., empty state). */ void clear(); /** * Are any lumps from @a file published in this index? * * @param file File containing the lumps to look for. * * @return @c true= One or more lumps are included. */ bool catalogues(File1 &file); /** * Append a lump to the index. * * @post Lump name hashes may be invalidated (will be rebuilt upon next search). * * @param lump Lump to be being added. */ void catalogLump(File1 &lump); /** * Prune all lumps catalogued from @a file. * * @param file File containing the lumps to prune * * @return Number of lumps pruned. */ int pruneByFile(File1 &file); /** * Prune the lump referenced by @a lumpInfo. * * @param lump Lump file to prune. * * @return @c true if found and pruned. */ bool pruneLump(File1 &lump); private: DENG2_PRIVATE(d) }; typedef LumpIndex::Id1MapRecognizer Id1MapRecognizer; } // namespace de #endif // __cplusplus #endif // LIBDENG_FILESYS_LUMPINDEX_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/filetype.h0000664000175000017500000001334112641367671027236 0ustar jaakkojaakko/** * @file filetype.h * * File Type. @ingroup fs * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_FILETYPE_H #define LIBDENG_FILETYPE_H #include "../libdoomsday.h" #ifdef __cplusplus #ifndef DENG2_C_API_ONLY #include #include #include #include #include #include "filehandle.h" #include "api_resourceclass.h" namespace de { struct FileInfo; /** * Encapsulates the properties and logics belonging to a logical * type of file file (e.g., Zip, PNG, WAV, etc...) * * @ingroup core */ class LIBDOOMSDAY_PUBLIC FileType { public: FileType(String _name, resourceclassid_t _defaultClass) : name_(_name), defaultClass_(_defaultClass) {} virtual ~FileType() {} /// Return the symbolic name of this file type. String const& name() const { return name_; } /// Return the unique identifier of the default class for this type of file. resourceclassid_t defaultClass() const { return defaultClass_; } /** * Add a new known extension to this file type. Earlier extensions * have priority. * * @param ext Extension to add (including period). * @return This instance. */ FileType& addKnownExtension(String ext) { knownFileNameExtensions_.push_back(ext); return *this; } /** * Provides access to the known file name extension list for efficient * iteration. * * @return List of known extensions. */ QStringList const& knownFileNameExtensions() const { return knownFileNameExtensions_; } /** * Does the file name in @a path match a known extension? * * @param path File name/path to test. * @return @c true if matched. */ bool fileNameIsKnown(String path) const { // We require an extension for this. String ext = path.fileNameExtension(); if(!ext.isEmpty()) { return knownFileNameExtensions_.contains(ext, Qt::CaseInsensitive); } return false; } private: /// Symbolic name for this type of file. String name_; /// Default class attributed to files of this type. resourceclassid_t defaultClass_; /// List of known extensions for this file type. QStringList knownFileNameExtensions_; }; /** * The special "null" FileType object. * * @ingroup core */ class LIBDOOMSDAY_PUBLIC NullFileType : public FileType { public: NullFileType() : FileType("FT_NONE", RC_UNKNOWN) {} }; /// @return @c true= @a ftype is a "null-filetype" object (not a real file type). inline bool isNullFileType(FileType const& ftype) { return !!dynamic_cast(&ftype); } /** * Base class for all native-file types. */ class LIBDOOMSDAY_PUBLIC NativeFileType : public FileType { public: NativeFileType(String name, resourceclassid_t rclassId) : FileType(name, rclassId) {} /** * Attempt to interpret a file file of this type. * * @param hndl Handle to the file to be interpreted. * @param path VFS path to associate with the file. * @param info File metadata info to attach to the file. * * @return The interpreted file; otherwise @c 0. */ virtual de::File1* interpret(de::FileHandle& hndl, String path, FileInfo const& info) const = 0; }; /// @return @c true= @a ftype is a NativeFileType object. inline bool isNativeFileType(FileType const& ftype) { return !!dynamic_cast(&ftype); } /// Map of symbolic file type names to file types (not owned). typedef QMap FileTypes; } // namespace de LIBDOOMSDAY_PUBLIC void DD_AddFileType(de::FileType const &ftype); /** * Lookup a FileType by symbolic name. * * @param name Symbolic name of the type. * @return FileType associated with @a name. May return a null-object. */ LIBDOOMSDAY_PUBLIC de::FileType const &DD_FileTypeByName(de::String name); /** * Attempts to determine which "type" should be attributed to a resource, solely * by examining the name (e.g., a file name/path). * * @return Type determined for this resource. May return a null-object. */ LIBDOOMSDAY_PUBLIC de::FileType const &DD_GuessFileTypeFromFileName(de::String name); /// Returns the registered file types for efficient traversal. LIBDOOMSDAY_PUBLIC de::FileTypes &DD_FileTypes(); #endif // DENG2_C_API_ONLY #endif // __cplusplus #endif /* LIBDENG_FILETYPE_H */ doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/fileinfo.h0000664000175000017500000000545712641367671027221 0ustar jaakkojaakko/** * @file fileinfo.h * * Metadata (record) for a file in the engine's virtual file system. * * @ingroup fs * * @deprecated Should use FS2 instead for file access. * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_FILESYS_FILEINFO_H #define LIBDENG_FILESYS_FILEINFO_H #ifdef __cplusplus #include "../libdoomsday.h" #include #include namespace de { class File1; /** * FileInfo record. * @ingroup fs */ struct LIBDOOMSDAY_PUBLIC FileInfo { uint lastModified; /// Unix timestamp. int lumpIdx; /// Relative index of this lump in the owning package else zero. size_t baseOffset; /// Offset from the start of the owning package. size_t size; /// Size of the uncompressed file. size_t compressedSize; /// Size of the original file compressed. FileInfo(uint _lastModified = 0, int _lumpIdx = 0, size_t _baseOffset = 0, size_t _size = 0, size_t _compressedSize = 0) : lastModified(_lastModified), lumpIdx(_lumpIdx), baseOffset(_baseOffset), size(_size), compressedSize(_compressedSize) {} FileInfo(FileInfo const& other) : lastModified(other.lastModified), lumpIdx(other.lumpIdx), baseOffset(other.baseOffset), size(other.size), compressedSize(other.compressedSize) {} ~FileInfo() {} FileInfo& operator = (FileInfo other) { swap(*this, other); return *this; } friend void swap(FileInfo& first, FileInfo& second) // nothrow { using std::swap; swap(first.lastModified, second.lastModified); swap(first.lumpIdx, second.lumpIdx); swap(first.baseOffset, second.baseOffset); swap(first.size, second.size); swap(first.compressedSize, second.compressedSize); } inline bool isCompressed() const { return size != compressedSize; } }; } // namespace de #endif // __cplusplus #endif /* LIBDENG_FILESYS_FILEINFO_H */ doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/fs_windows.h0000664000175000017500000000257412641367671027605 0ustar jaakkojaakko/** @file fs_windows.h Windows-specific file system operations. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_FS_WINDOWS_H #define LIBDOOMSDAY_FS_WINDOWS_H #include "../libdoomsday.h" #include #define fopen FS_Win32_fopen #define access FS_Win32_access #define mkdir FS_Win32_mkdir #ifdef __cplusplus extern "C" { #endif LIBDOOMSDAY_PUBLIC FILE *FS_Win32_fopen(char const *filenameUtf8, char const *mode); LIBDOOMSDAY_PUBLIC int FS_Win32_access(char const *pathUtf8, int mode); LIBDOOMSDAY_PUBLIC int FS_Win32_mkdir(char const *dirnameUtf8); #ifdef __cplusplus } //extern "C" #endif #endif // LIBDOOMSDAY_FS_WINDOWS_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/filehandle.h0000664000175000017500000000732412641367671027514 0ustar jaakkojaakko/** * @file filehandle.h * Reference/handle to a unique file in FS1. * @ingroup fs * * @deprecated Should use FS2 instead for file access. * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_FILESYS_FILEHANDLE_H #define LIBDENG_FILESYS_FILEHANDLE_H #include "../libdoomsday.h" #include namespace de { class File1; struct FileList; /// Seek methods enum SeekMethod { SeekSet = 0, SeekCur = 1, SeekEnd = 2 }; /** * Reference/handle to a unique file in the engine's virtual file system. */ class LIBDOOMSDAY_PUBLIC FileHandle { public: ~FileHandle(); /** * Close the file if open. Note that this clears any previously buffered data. */ FileHandle &close(); /// @todo Should not be visible outside the engine. FileList *list(); /// @todo Should not be visible outside the engine. FileHandle &setList(FileList *list); bool hasFile() const; File1 &file(); File1 &file() const; /// @return @c true iff this handle's internal state is valid. bool isValid() const; /// @return The length of the file, in bytes. size_t length(); /// @return Offset in bytes from the start of the file to begin read. size_t baseOffset() const; /** * @return Number of bytes read (at most @a count bytes will be read). */ size_t read(uint8_t *buffer, size_t count); /** * Read a character from the stream, advancing the read position in the process. */ unsigned char getC(); /** * @return @c true iff the stream has reached the end of the file. */ bool atEnd(); /** * @return Current position in the stream as an offset from the beginning of the file. */ size_t tell(); /** * @return The current position in the file, before the move, as an * offset from the beginning of the file. */ size_t seek(size_t offset, SeekMethod whence); /** * Rewind the stream to the start of the file. */ FileHandle &rewind(); public: /** * Create a new handle on the File @a file. * * @param file The file being opened. */ static FileHandle *fromFile(File1 &file); /** * Create a new handle on @a lump. * * @param lump The lump to be opened. * @param dontBuffer @c true= do not buffer a copy of the lump. */ static FileHandle *fromLump(File1 &lump, bool dontBuffer = false); /** * Create a new handle on the specified native file. * * @param nativeFile Native file system handle to the file being opened. * @param baseOffset Offset from the start of the file in bytes to begin. */ static FileHandle *fromNativeFile(FILE &nativeFile, size_t baseOffset); private: FileHandle(); struct Instance; Instance *d; }; } // namespace de #endif /* LIBDENG_FILESYS_FILEHANDLE_H */ doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/wad.h0000664000175000017500000001735412641367671026200 0ustar jaakkojaakko/** @file wad.h WAD Archive (File). * * @author Copyright © 2003-2014 Jaakko Keränen * @author Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDOOMSDAY_FILESYS_WAD_H #define LIBDOOMSDAY_FILESYS_WAD_H #include "../libdoomsday.h" #include "../filesys/lumpindex.h" #include "file.h" #include "fileinfo.h" #include #include namespace de { /** * WAD archive file format. * @ingroup fs * * @see file.h, File1 * * @todo This should be replaced with a FS2 based WadFolder class. */ class LIBDOOMSDAY_PUBLIC Wad : public File1, public LumpIndex { protected: struct Entry; // forward public: /// Base class for format-related errors. @ingroup errors DENG2_ERROR(FormatError); /** * File system object for a lump in the WAD. * * The purpose of this abstraction is to redirect various File1 methods to the * containing Wad file. Such a mechanism would be unnecessary in a file system * in which proper OO design is used for the package / file abstraction. -ds */ class LumpFile : public File1 { public: LumpFile(Entry &entry, FileHandle &hndl, String path, FileInfo const &info, File1 *container); /// @return Name of this file. String const &name() const; /** * Compose an absolute URI to this file. * * @param delimiter Delimit directory using this character. * * @return The absolute URI. */ Uri composeUri(QChar delimiter = '/') const; /** * Retrieve the directory node for this file. * * @return Directory node for this file. */ PathTree::Node &directoryNode() const; /** * Read the file data into @a buffer. * * @param buffer Buffer to read into. Must be at least large enough to * contain the whole file. * @param tryCache @c true= try the lump cache first. * * @return Number of bytes read. * * @see size() or info() to determine the size of buffer needed. */ size_t read(uint8_t *buffer, bool tryCache = true); /** * Read a subsection of the file data into @a buffer. * * @param buffer Buffer to read into. Must be at least @a length bytes. * @param startOffset Offset from the beginning of the file to start reading. * @param length Number of bytes to read. * @param tryCache If @c true try the local data cache first. * * @return Number of bytes read. */ size_t read(uint8_t *buffer, size_t startOffset, size_t length, bool tryCache = true); /** * Read this lump into the local cache. * * @return Pointer to the cached copy of the associated data. */ uint8_t const *cache(); /** * Remove a lock on the locally cached data. * * @return This instance. */ LumpFile &unlock(); /** * Convenient method returning the containing Wad file instance. */ Wad &wad() const; private: Entry &entry; }; public: Wad(FileHandle &hndl, String path, FileInfo const &info, File1 *container = 0); virtual ~Wad(); /** * Read the data associated with lump @a lumpIndex into @a buffer. * * @param lumpIndex Lump index associated with the data to be read. * @param buffer Buffer to read into. Must be at least large enough to * contain the whole lump. * @param tryCache @c true= try the lump cache first. * * @return Number of bytes read. * * @throws NotFoundError If @a lumpIndex is not valid. * * @see lumpSize() or lumpInfo() to determine the size of buffer needed. */ size_t readLump(int lumpIndex, uint8_t *buffer, bool tryCache = true); /** * Read a subsection of the data associated with lump @a lumpIndex into @a buffer. * * @param lumpIndex Lump index associated with the data to be read. * @param buffer Buffer to read into. Must be at least @a length bytes. * @param startOffset Offset from the beginning of the lump to start reading. * @param length Number of bytes to read. * @param tryCache @c true= try the lump cache first. * * @return Number of bytes read. * * @throws NotFoundError If @a lumpIndex is not valid. */ size_t readLump(int lumpIndex, uint8_t *buffer, size_t startOffset, size_t length, bool tryCache = true); /** * Read the data associated with lump @a lumpIndex into the cache. * * @param lumpIndex Lump index associated with the data to be cached. * * @return Pointer to the cached copy of the associated data. * * @throws NotFoundError If @a lumpIndex is not valid. */ uint8_t const *cacheLump(int lumpIndex); /** * Remove a lock on a cached data lump. * * @param lumpIndex Lump index associated with the cached data to be changed. */ void unlockLump(int lumpIndex); /** * Clear any cached data for lump @a lumpIndex from the lump cache. * * @param lumpIndex Lump index associated with the cached data to be cleared. * @param retCleared If not @c NULL write @c true to this address if data was * present and subsequently cleared from the cache. */ void clearCachedLump(int lumpIndex, bool *retCleared = 0); /** * Purge the lump cache, clearing all cached data lumps. * * @return This instance. */ void clearLumpCache(); /** * @attention Uses an extremely simple formula which does not conform to any CRC * standard. Should not be used for anything critical. */ uint calculateCRC(); public: /** * Determines whether a File looks like it could be accessed using Wad. * * @param file File to check. * * @return @c true, if the file looks like a WAD. */ static bool recognise(FileHandle &file); protected: /** * Models an entry in the internal lump tree. */ struct Entry : public PathTree::Node { dint32 offset; dint32 size; QScopedPointer lumpFile; ///< File system object for the lump data. uint crc; ///< CRC for the lump data. Entry(PathTree::NodeArgs const &args) : Node(args), offset(0), size(0), crc(0) {} LumpFile &file() const; /// Recalculates CRC of the entry. void update(); }; typedef PathTreeT LumpTree; /** * Provides access to the internal LumpTree, for efficient traversal. */ LumpTree const &lumpTree() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBDOOMSDAY_FILESYS_WAD_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/fs_util.h0000664000175000017500000000713612641367671027067 0ustar jaakkojaakko/** * @file fs_util.h * * Miscellaneous file system utility routines. * * @ingroup fs * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_FILESYS_UTIL_H #define LIBDENG_FILESYS_UTIL_H #include "../libdoomsday.h" #include "../filesys/file.h" #include "dd_types.h" #ifdef WIN32 # include "fs_windows.h" #endif #ifdef __cplusplus extern "C" { #endif LIBDOOMSDAY_PUBLIC int F_Access(char const *nativePath); LIBDOOMSDAY_PUBLIC int F_FileExists(char const *path); LIBDOOMSDAY_PUBLIC dd_bool F_MakePath(char const *path); /** * Converts directory slashes to our internal '/'. * @return @c true iff the path was modified. */ LIBDOOMSDAY_PUBLIC dd_bool F_FixSlashes(ddstring_t *dst, ddstring_t const *src); /** * Appends a slash at the end of @a path if there isn't one. * @return @c true if a slash was appended, @c false otherwise. */ LIBDOOMSDAY_PUBLIC dd_bool F_AppendMissingSlashCString(char *path, size_t maxLen); /** * Converts directory slashes to tha used by the host file system. * @return @c true iff the path was modified. */ LIBDOOMSDAY_PUBLIC dd_bool F_ToNativeSlashes(ddstring_t *dst, ddstring_t const *src); /** * @return @c true, if the given path is absolute (starts with \ or / or the * second character is a ':' (drive). */ LIBDOOMSDAY_PUBLIC dd_bool F_IsAbsolute(ddstring_t const *path); /** * Expands relative path directives like '>'. * * @note Despite appearances this function is *not* an alternative version of * M_TranslatePath accepting ddstring_t arguments. Key differences: * * ! Handles '~' on UNIX-based platforms. * ! No other transform applied to @a src path. * * @param dst Expanded path written here. * @param src Original path. * * @return @c true iff the path was expanded. */ LIBDOOMSDAY_PUBLIC dd_bool F_ExpandBasePath(ddstring_t *dst, ddstring_t const *src); LIBDOOMSDAY_PUBLIC char const *F_PrettyPath(char const *path); /** * Write the data associated with the specified lump index to @a outputPath. * * @param file File to be dumped. * @param outputPath If not @c NULL write the associated data to this path. * Can be @c NULL in which case the path and file name will * be chosen automatically. * * @return @c true iff successful. */ LIBDOOMSDAY_PUBLIC dd_bool F_DumpFile(de::File1 &file, char const *outputPath); /** * Write data into a file. * * @param data Data to write. * @param size Size of the data in bytes. * @param path Path of the file to create (existing file replaced). * * @return @c true if successful, otherwise @c false. */ LIBDOOMSDAY_PUBLIC dd_bool F_Dump(void const *data, size_t size, char const *path); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_FILESYS_UTIL_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/filesys/file.h0000664000175000017500000001570512641367671026342 0ustar jaakkojaakko/** * @file file.h * Base for all classes which represent opened files in FS1. * @ingroup fs * * @deprecated Should use FS2 instead for file access. * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_FILESYS_FILE_H #define LIBDENG_FILESYS_FILE_H #include "../libdoomsday.h" #include "filehandle.h" #include "fileinfo.h" #include "../uri.h" #include #include #include #include "dd_share.h" namespace de { /** * File1 is a core component of the filesystem intended for use as the base * for all types of (pseudo-)file resources. * * @ingroup fs */ class LIBDOOMSDAY_PUBLIC File1 { public: /// Categorization flags. enum Flag { /// Flagged as having been loaded during the game startup process. Startup = 0x1, /// Flagged as a non-original game resource. Custom = 0x2, /// All resources are by default flagged as "custom". DefaultFlags = Custom }; Q_DECLARE_FLAGS(Flags, Flag) /// This file is not contained. @ingroup errors DENG2_ERROR(NotContainedError); private: File1(); public: /** * @param hndl Handle to the file. Ownership of the handle is given to this instance. * @param _path Path to this file in the virtual file system. * @param _info Info descriptor for the file. A copy is made. * @param container Container of this file. Can be @c NULL. */ File1(FileHandle &hndl, String _path, FileInfo const &_info, File1 *container = 0); /** * Release all memory acquired for objects linked with this resource. */ virtual ~File1(); DENG2_AS_IS_METHODS() /// @return Name of this file. virtual String const &name() const; /** * Compose the a URI to this file. * * @param delimiter Delimit directory using this character. * * @return The composed URI. */ virtual de::Uri composeUri(QChar delimiter = '/') const; /** * Compose the absolute VFS path to this file. * * @param delimiter Delimit directory using this character. * * @return String containing the absolute path. * * @deprecated Prefer to use composeUri() instead. */ String composePath(QChar delimiter = '/') const { return composeUri(delimiter).compose(); } /// @return @c true iff this file is contained by another. bool isContained() const; /// @return The file instance which contains this. File1 &container() const; /// @return Load order index for this resource. uint loadOrderIndex() const; /** * @return Immutable copy of the info descriptor for this resource. */ FileInfo const &info() const; // Convenient lookup method for when only the last-modified property is needed from info(). /// @return "Last modified" timestamp of the resource. inline uint lastModified() const { return info().lastModified; } // Convenient lookup method for when only the size property is needed from info(). /// @return Size of the uncompressed resource. inline uint size() const { return info().size; } // Convenient lookup method for when only the is-compressed property is needed from info(). /// @return Size of the uncompressed resource. inline bool isCompressed() const { return info().isCompressed(); } /// @return @c true if the resource is marked "startup". bool hasStartup() const; /// Mark this resource as "startup". File1 &setStartup(bool yes); /// @return @c true if the resource is marked "custom". bool hasCustom() const; /// Mark this resource as "custom". File1 &setCustom(bool yes); FileHandle &handle(); /** * Retrieve the directory node for this file. * * @return Directory node for this file. */ virtual PathTree::Node &directoryNode() const { throw de::Error("File1::directoryNode", "No owner directory"); } /** * Read the file data into @a buffer. * * @param buffer Buffer to read into. Must be at least large enough to * contain the whole file. * @param tryCache @c true= try the lump cache first. * * @return Number of bytes read. * * @see size() or info() to determine the size of buffer needed. */ virtual size_t read(uint8_t *buffer, bool tryCache = true); /** * Read a subsection of the file data into @a buffer. * * @param buffer Buffer to read into. Must be at least @a length bytes. * @param startOffset Offset from the beginning of the file to start reading. * @param length Number of bytes to read. * @param tryCache If @c true try the local data cache first. * * @return Number of bytes read. */ virtual size_t read(uint8_t *buffer, size_t startOffset, size_t length, bool tryCache = true); /* * Caching interface: */ /** * Read this file into the local cache. * * @return Pointer to the cached copy of the associated data. */ virtual uint8_t const *cache(); /** * Remove a lock on the locally cached data. * * @return This instance. */ virtual File1 &unlock(); /** * Clear any data in the local cache. * * @param retCleared If not @c NULL write @c true to this address if data was * present and subsequently cleared from the cache. * * @return This instance. */ virtual File1 &clearCache(bool *retCleared = 0); protected: /// File stream handle. FileHandle *handle_; /// Info descriptor (file metadata). FileInfo info_; /// The container file (if any). File1 *container_; private: /// Categorization flags. Flags flags; /// Absolute path (including name) in the vfs. String path_; /// Name of this file. String name_; /// Load order depth index. uint order; }; Q_DECLARE_OPERATORS_FOR_FLAGS(File1::Flags) } // namespace de #endif /* LIBDENG_FILESYS_FILE_H */ doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/0000775000175000017500000000000012641367671024505 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/finale.h0000664000175000017500000000256012641367671026117 0ustar jaakkojaakko/** @file defs/finale.h Finale definition accessor. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEFN_FINALE_H #define LIBDOOMSDAY_DEFN_FINALE_H #include "definition.h" #include namespace defn { /** * Utility for handling finale definitions. */ class LIBDOOMSDAY_PUBLIC Finale : public Definition { public: Finale() : Definition() {} Finale(Finale const &other) : Definition(other) {} Finale(de::Record &d) : Definition(d) {} Finale(de::Record const &d) : Definition(d) {} void resetToDefaults(); }; } // namespace defn #endif // LIBDOOMSDAY_DEFN_FINALE_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/sky.h0000664000175000017500000000413212641367671025464 0ustar jaakkojaakko/** @file defs/sky.h Sky definition accessor. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEFN_SKY_H #define LIBDOOMSDAY_DEFN_SKY_H #include "definition.h" #include namespace defn { // Sky flags. #define SIF_DRAW_SPHERE 0x1 ///< Always draw the sky sphere. // Sky layer flags. #define SLF_ENABLE 0x1 ///< @c true= enable the layer. #define SLF_MASK 0x2 ///< @c true= mask the layer. #define DEFAULT_SKY_HEIGHT ( .666667f ) #define DEFAULT_SKY_HORIZON_OFFSET ( -0.105f ) #define DEFAULT_SKY_SPHERE_XOFFSET ( 0 ) #define DEFAULT_SKY_SPHERE_FADEOUT_LIMIT ( .3f ) /** * Utility for handling sky definitions. */ class LIBDOOMSDAY_PUBLIC Sky : public Definition { public: Sky() : Definition() {} Sky(Sky const &other) : Definition(other) {} Sky(de::Record &d) : Definition(d) {} Sky(de::Record const &d) : Definition(d) {} void resetToDefaults(); de::Record &addLayer(); int layerCount() const; bool hasLayer(int index) const; de::Record &layer(int index); de::Record const &layer(int index) const; de::Record &addModel(); int modelCount() const; bool hasModel(int index) const; de::Record &model(int index); de::Record const &model(int index) const; }; } // namespace defn #endif // LIBDOOMSDAY_DEFN_SKY_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/ded.h0000664000175000017500000001401012641367671025406 0ustar jaakkojaakko/** @file ded.h Definition namespace. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEFINITION_DATABASE_H #define LIBDOOMSDAY_DEFINITION_DATABASE_H #include #include #include #include #include #include "../uri.h" #include "dedtypes.h" #include "dedregister.h" // Version 6 does not require semicolons. #define DED_VERSION 6 /** * The ded_t structure encapsulates all the data one definition file can contain. This is * only an internal representation of the data. An ASCII version is written and read by * DED_Write() and DED_Read() routines. * * It is VERY important not to sort the data arrays in any way: the index numbers are * important. The Game DLL must be recompiled with the new constants if the order of the * array items changes. */ struct LIBDOOMSDAY_PUBLIC ded_s { de::Record names; ///< Namespace where definition values are stored. int version; // DED version number. ded_flags_t modelFlags; // Default values for models. float modelScale; float modelOffset; // Flag values (for all types of data). DEDRegister flags; // Episodes. DEDRegister episodes; // Map object information. DEDArray mobjs; // States. DEDArray states; // Sprites. DEDArray sprites; // Lights. DEDArray lights; // Materials. DEDRegister materials; // Models. DEDRegister models; // Skies. DEDRegister skies; // Sounds. DEDArray sounds; // Music. DEDRegister musics; // Map information. DEDRegister mapInfos; // Text. DEDArray text; // Aural environments for textures. DEDArray textureEnv; // Free-from string values. DEDArray values; // Detail texture assignments. DEDArray details; // Particle generators. DEDArray ptcGens; // Finales. DEDRegister finales; // Decorations. DEDRegister decorations; // Reflections. DEDArray reflections; // Animation/Precache groups for textures. DEDArray groups; // XG line types. DEDArray lineTypes; // XG sector types. DEDArray sectorTypes; // Composite fonts. DEDArray compositeFonts; public: /** * Constructor initializes everything to zero. */ ded_s(); void clear(); int addFlag(de::String const &id, int value); int addEpisode(); int addDecoration(); int addFinale(); int addMapInfo(); int addMaterial(); int addModel(); int addMusic(); int addSky(); //ded_flag_t *getFlag(char const *flag) const; int evalFlags2(char const *ptr) const; int getMobjNum(char const *id) const; int getMobjNumForName(char const *name) const; char const *getMobjName(int num) const; int getStateNum(de::String const &id) const; int getStateNum(char const *id) const; int getEpisodeNum(de::String const &id) const; int getMapInfoNum(de::Uri const &uri) const; int getMaterialNum(de::Uri const &uri) const; int getModelNum(char const *id) const; int getMusicNum(char const *id) const; int getSkyNum(char const *id) const; int getSoundNum(char const *id) const; /** * Looks up a sound using @a name key. * @param name Sound name. * @return If the name is not found, returns the NULL sound index (zero). */ int getSoundNumForName(char const *name) const; int getTextNum(char const *id) const; ded_value_t *getValueById(char const *id) const; ded_value_t *getValueByUri(de::Uri const &uri) const; ded_compositefont_t *findCompositeFontDef(de::Uri const &uri) const; ded_compositefont_t *getCompositeFont(char const *uriCString) const; protected: void release(); DENG2_NO_ASSIGN(ded_s) DENG2_NO_COPY (ded_s) }; typedef ded_s ded_t; #ifdef __cplusplus extern "C" { #endif // Routines for managing DED files: int DED_AddMobj(ded_t* ded, char const* idStr); int DED_AddState(ded_t* ded, char const* id); int DED_AddSprite(ded_t* ded, char const* name); int DED_AddLight(ded_t* ded, char const* stateID); int DED_AddSound(ded_t* ded, char const* id); int DED_AddText(ded_t* ded, char const* id); int DED_AddTextureEnv(ded_t* ded, char const* id); int DED_AddValue(ded_t *ded, char const* id); int DED_AddDetail(ded_t* ded, char const* lumpname); int DED_AddPtcGen(ded_t* ded, char const* state); int DED_AddPtcGenStage(ded_ptcgen_t* gen); int DED_AddReflection(ded_t* ded); int DED_AddGroup(ded_t* ded); int DED_AddGroupMember(ded_group_t* grp); int DED_AddSectorType(ded_t* ded, int id); int DED_AddLineType(ded_t* ded, int id); int DED_AddCompositeFont(ded_t* ded, char const* uri); int DED_AddCompositeFontMapCharacter(ded_compositefont_t* font); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDOOMSDAY_DEFINITION_DATABASE_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/dedfile.h0000664000175000017500000000366212641367671026261 0ustar jaakkojaakko/** @file dedfile.h Definition files. * @ingroup defs * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDOOMSDAY_DEFS_DED_H #define LIBDOOMSDAY_DEFS_DED_H #include "../libdoomsday.h" #include "ded.h" #include LIBDOOMSDAY_PUBLIC void Def_ReadProcessDED(ded_t *defs, de::String path); /** * Reads definitions from the given lump. */ LIBDOOMSDAY_PUBLIC int DED_ReadLump(ded_t *ded, lumpnum_t lumpNum); /** * Reads definitions from the given buffer. * The definition is being loaded from @a _sourcefile (DED or WAD). * * @param buffer The data to be read, must be null-terminated. * @param sourceFile Just FYI. * @param sourceIsCustom @c true= source is a user supplied add-on. */ LIBDOOMSDAY_PUBLIC int DED_ReadData(ded_t *ded, char const *buffer, de::String sourceFile, bool sourceIsCustom); /** * @return @c true, if the file was successfully loaded. */ int DED_Read(ded_t *ded, de::String path); void DED_SetError(de::String const &message); LIBDOOMSDAY_PUBLIC char const *DED_Error(); #endif // LIBDOOMSDAY_DEFS_DED_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/definition.h0000664000175000017500000000431712641367671027013 0ustar jaakkojaakko/** @file definition.h Base class for definition record accessors. * * @authors Copyright © 2014 Jaakko Keränen * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEFN_DEFINITION_H #define LIBDOOMSDAY_DEFN_DEFINITION_H #include "../libdoomsday.h" #include namespace defn { /** * Base class for definition record accessors. * @ingroup data */ class LIBDOOMSDAY_PUBLIC Definition : public de::RecordAccessor { public: Definition() : RecordAccessor(0) {} Definition(Definition const &other) : RecordAccessor(other) {} Definition(de::Record &d) : RecordAccessor(d) {} Definition(de::Record const &d) : RecordAccessor(d) {} de::Record &def(); de::Record const &def() const; /** * Determines if this definition accessor points to a record. */ operator bool() const; /** * Returns the ordinal of the definition. Ordinals are automatically set by * DEDRegister as definitions are created. */ int order() const; /** * Inserts the default members into the definition. All definitions are required to * implement this, as it is automatically called for all newly created definitions. * * All definitions share some common members, so derived classes are required to * call this before inserting their own members. */ virtual void resetToDefaults(); }; } // namespace defn #endif // LIBDOOMSDAY_DEFN_DEFINITION_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/music.h0000664000175000017500000000254512641367671026004 0ustar jaakkojaakko/** @file defs/music.h Music definition accessor. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEFN_MUSIC_H #define LIBDOOMSDAY_DEFN_MUSIC_H #include "definition.h" #include namespace defn { /** * Utility for handling music definitions. */ class LIBDOOMSDAY_PUBLIC Music : public Definition { public: Music() : Definition() {} Music(Music const &other) : Definition(other) {} Music(de::Record &d) : Definition(d) {} Music(de::Record const &d) : Definition(d) {} void resetToDefaults(); }; } // namespace defn #endif // LIBDOOMSDAY_DEFN_MUSIC_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/episode.h0000664000175000017500000000333612641367671026313 0ustar jaakkojaakko/** @file defs/episode.h Episode definition accessor. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEFN_EPISODE_H #define LIBDOOMSDAY_DEFN_EPISODE_H #include "definition.h" #include namespace defn { /** * Utility for handling episode definitions. */ class LIBDOOMSDAY_PUBLIC Episode : public Definition { public: Episode() : Definition() {} Episode(Episode const &other) : Definition(other) {} Episode(de::Record &d) : Definition(d) {} Episode(de::Record const &d) : Definition(d) {} void resetToDefaults(); de::Record &addHub(); int hubCount() const; bool hasHub(int index) const; de::Record &hub(int index); de::Record const &hub(int index) const; de::Record *tryFindHubByMapId(de::String const &mapId); de::Record *tryFindMapGraphNode(de::String const &mapId); de::Record *tryFindMapGraphNodeByWarpNumber(int warpNumber); }; } // namespace defn #endif // LIBDOOMSDAY_DEFN_EPISODE_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/mapgraphnode.h0000664000175000017500000000320412641367671027322 0ustar jaakkojaakko/** @file defs/mapgraphnode.h MapGraphNode definition accessor. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEFN_MAPGRAPHNODE_H #define LIBDOOMSDAY_DEFN_MAPGRAPHNODE_H #include "definition.h" #include namespace defn { /** * Utility for handling "map-connectivity graph, node" definitions. */ class LIBDOOMSDAY_PUBLIC MapGraphNode : public Definition { public: MapGraphNode() : Definition() {} MapGraphNode(MapGraphNode const &other) : Definition(other) {} MapGraphNode(de::Record &d) : Definition(d) {} MapGraphNode(de::Record const &d) : Definition(d) {} void resetToDefaults(); de::Record &addExit(); int exitCount() const; bool hasExit(int index) const; de::Record &exit(int index); de::Record const &exit(int index) const; }; } // namespace defn #endif // LIBDOOMSDAY_DEFN_MAPGRAPHNODE_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/dedarray.h0000664000175000017500000001421212641367671026451 0ustar jaakkojaakko/** @file defs/dedarray.h Definition struct (POD) array. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEFINITION_ARRAY_H #define LIBDOOMSDAY_DEFINITION_ARRAY_H #include "../libdoomsday.h" #include #include #include struct LIBDOOMSDAY_PUBLIC ded_count_s { int num; int max; ded_count_s() : num(0), max(0) {} }; typedef struct ded_count_s ded_count_t; /** * Array of DED definitions. * * The array uses a memory management convention suitable for POD elements that are * copied using @c memcpy: element constructors and destructors are never called, so * ownership of data is managed manually using the elements' release() and reallocate() * methods. * * Any memory allocated by the elements is @em not released automatically. Also, the * array itself is also not freed in the destructor; clear() must be called before the * array goes out of scope. * * @a PODType must be a POD class. All elements are initialized to zero. * - The class must have a release() method that frees all memory owned by the element. * - The class must have a reallocate() method if the entire array is assigned to * another or if elements are copied. The method must duplicate all memory owned by * the element; a plain @c memcpy has already been done by DEDArray. * */ template struct LIBDOOMSDAY_PUBLIC DEDArray { PODType *elements; ded_count_t count; DENG2_NO_COPY (DEDArray) public: DEDArray() : elements(0) {} ~DEDArray() { // This should have been cleared by now. DENG_ASSERT(elements == 0); } /// @note Previous elements are not released -- they must be cleared manually. DEDArray &operator = (DEDArray const &other) { elements = other.elements; count = other.count; reallocate(); return *this; } void releaseAll() { for(int i = 0; i < count.num; ++i) { elements[i].release(); } } /** * Duplicates the array and all the elements. Previous elements are not released. The * assumption is that the array has been memcpy'd so it currently doesn't have * ownership of the elements. */ void reallocate() { PODType *copied = (PODType *) M_Malloc(sizeof(PODType) * count.max); memcpy(copied, elements, sizeof(PODType) * count.num); elements = copied; for(int i = 0; i < count.num; ++i) { elements[i].reallocate(); } } bool isEmpty() const { return !size(); } int size() const { return count.num; } PODType const &at(int index) const { DENG2_ASSERT(index >= 0); DENG2_ASSERT(index < size()); return elements[index]; } PODType &operator [] (int index) const { DENG2_ASSERT(index >= 0); DENG2_ASSERT(index < size()); return elements[index]; } PODType const &first() const { return at(0); } PODType const &last() const { return at(size() - 1); } /** * Appends new, zeroed elements to the end of the array. * * @param addedCount Number of elements to add. * * @return Pointer to the first new element. */ PODType *append(int addedCount = 1) { DENG2_ASSERT(addedCount >= 0); int const first = count.num; count.num += addedCount; if(count.num > count.max) { count.max *= 2; // Double the size of the array. if(count.num > count.max) count.max = count.num; elements = reinterpret_cast(M_Realloc(elements, sizeof(PODType) * count.max)); } PODType *np = elements + first; memset(np, 0, sizeof(PODType) * addedCount); // Clear the new entries. return np; } void removeAt(int index) { DENG2_ASSERT(index >= 0); DENG2_ASSERT(index < size()); if(index < 0 || index >= size()) return; elements[index].release(); memmove(elements + index, elements + (index + 1), sizeof(PODType) * (count.num - index - 1)); if(--count.num < count.max / 2) { count.max /= 2; elements = (PODType *) M_Realloc(elements, sizeof(PODType) * count.max); } } void copyTo(int destIndex, int srcIndex) { DENG2_ASSERT(destIndex >= 0 && destIndex < size()); DENG2_ASSERT(srcIndex >= 0 && srcIndex < size()); // Free all existing allocations. elements[destIndex].release(); // Do a plain copy and then duplicate allocations. memcpy(&elements[destIndex], &elements[srcIndex], sizeof(PODType)); elements[destIndex].reallocate(); } void copyTo(PODType *dest, PODType const *src) { copyTo(indexOf(dest), indexOf(src)); } void copyTo(PODType *dest, int srcIndex) { copyTo(indexOf(dest), srcIndex); } int indexOf(PODType const *element) const { if(size() > 0 && element >= &first() && element <= &last()) return element - elements; return -1; } void clear() { releaseAll(); if(elements) M_Free(elements); elements = 0; count.num = count.max = 0; } }; #endif // LIBDOOMSDAY_DEFINITION_ARRAY_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/dedparser.h0000664000175000017500000000272112641367671026631 0ustar jaakkojaakko/** @file dedparser.h DED v1 parser. * @ingroup defs * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDOOMSDAY_DED_V1_PARSER_H #define LIBDOOMSDAY_DED_V1_PARSER_H #include #include "../libdoomsday.h" #include "ded.h" LIBDOOMSDAY_PUBLIC void DED_SetXGClassLinks(struct xgclass_s *links); /** * Parser of DED v1 definitions. * @ingroup data */ class LIBDOOMSDAY_PUBLIC DEDParser { public: DEDParser(ded_t *ded); int parse(char const *buffer, de::String sourceFile, bool sourceIsCustom); private: DENG2_PRIVATE(d) }; #endif // LIBDOOMSDAY_DED_V1_PARSER_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/dedtypes.h0000664000175000017500000003623412641367671026507 0ustar jaakkojaakko/** @file dedtypes.h Definition types and structures (DED v1). * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEFINITION_TYPES_H #define LIBDOOMSDAY_DEFINITION_TYPES_H #include #include #include #include #include "../uri.h" #include "def_share.h" #include "api_gl.h" #include "dedarray.h" #define DED_DUP_URI(u) u = (u? new de::Uri(*u) : 0) #define DED_SPRITEID_LEN 4 #define DED_STRINGID_LEN 31 #define DED_FUNC_LEN 255 #define DED_MAX_MATERIAL_LAYERS 1 ///< Maximum number of material layers (map renderer limitations). #define DED_MAX_MATERIAL_DECORATIONS 16 ///< Maximum number of material decorations (arbitrary). #define DED_PTCGEN_ANY_MOBJ_TYPE -2 ///< Particle generator applies to ANY mobj type. typedef char ded_stringid_t[DED_STRINGID_LEN + 1]; typedef ded_stringid_t ded_string_t; typedef ded_stringid_t ded_mobjid_t; typedef ded_stringid_t ded_stateid_t; typedef ded_stringid_t ded_soundid_t; typedef ded_stringid_t ded_funcid_t; typedef char ded_func_t[DED_FUNC_LEN + 1]; typedef int ded_flags_t; typedef char* ded_anystring_t; typedef struct LIBDOOMSDAY_PUBLIC ded_uri_s { de::Uri *uri; void release() { delete uri; } void reallocate() { DED_DUP_URI(uri); } } ded_uri_t; #ifdef _MSC_VER // MSVC needs some hand-holding. template struct LIBDOOMSDAY_PUBLIC DEDArray; #endif // Embedded sound information. typedef struct ded_embsound_s { ded_string_t name; int id; // Figured out at runtime. float volume; } ded_embsound_t; typedef struct LIBDOOMSDAY_PUBLIC ded_ptcstage_s { ded_flags_t type; int tics; float variance; // Stage variance (time). float color[4]; // RGBA float radius; float radiusVariance; ded_flags_t flags; float bounce; float resistance; // Air resistance. float gravity; float vectorForce[3]; float spin[2]; // Yaw and pitch. float spinResistance[2]; // Yaw and pitch. int model; ded_string_t frameName; // For model particles. ded_string_t endFrameName; short frame, endFrame; ded_embsound_t sound, hitSound; void release() {} void reallocate() {} /** * Takes care of consistent variance. * Currently only used visually, collisions use the constant radius. * The variance can be negative (results will be larger). */ float particleRadius(int ptcIDX) const; } ded_ptcstage_t; typedef struct { char id[DED_SPRITEID_LEN + 1]; void release() {} } ded_sprid_t; typedef struct { char str[DED_STRINGID_LEN + 1]; } ded_str_t; typedef struct { ded_stringid_t id; int value; void release() {} } ded_flag_t; typedef struct LIBDOOMSDAY_PUBLIC ded_mobj_s { ded_mobjid_t id; int doomEdNum; ded_string_t name; ded_stateid_t states[STATENAMES_COUNT]; ded_soundid_t seeSound; ded_soundid_t attackSound; ded_soundid_t painSound; ded_soundid_t deathSound; ded_soundid_t activeSound; int reactionTime; int painChance; int spawnHealth; float speed; float radius; float height; int mass; int damage; ded_flags_t flags[NUM_MOBJ_FLAGS]; int misc[NUM_MOBJ_MISC]; void release() {} void reallocate() {} } ded_mobj_t; typedef struct LIBDOOMSDAY_PUBLIC ded_state_s { ded_stateid_t id; // ID of this state. ded_sprid_t sprite; ded_flags_t flags; int frame; int tics; ded_funcid_t action; ded_stateid_t nextState; int misc[NUM_STATE_MISC]; ded_anystring_t execute; // Console command. void release() { M_Free(execute); } void reallocate() { if(execute) execute = M_StrDup(execute); } } ded_state_t; typedef struct LIBDOOMSDAY_PUBLIC ded_light_s { ded_stateid_t state; char uniqueMapID[64]; float offset[3]; /* Origin offset in world coords Zero means automatic */ float size; // Zero: automatic float color[3]; // Red Green Blue (0,1) float lightLevel[2]; // Min/max lightlevel for bias ded_flags_t flags; de::Uri* up, *down, *sides; de::Uri* flare; float haloRadius; // Halo radius (zero = no halo). void release() { delete up; delete down; delete sides; delete flare; } void reallocate() { DED_DUP_URI(up); DED_DUP_URI(down); DED_DUP_URI(sides); DED_DUP_URI(flare); } } ded_light_t; typedef struct LIBDOOMSDAY_PUBLIC ded_sound_s { ded_soundid_t id; // ID of this sound, refered to by others. ded_string_t name; // A tag name for the sound. ded_string_t lumpName; // Actual lump name of the sound ("DS" not included). de::Uri* ext; // External sound file (WAV). ded_soundid_t link; // Link to another sound. int linkPitch; int linkVolume; int priority; // Priority classification. int channels; // Max number of channels to occupy. int group; // Exclusion group. ded_flags_t flags; // Flags (like chg_pitch). void release() { delete ext; } void reallocate() { DED_DUP_URI(ext); } } ded_sound_t; struct ded_text_t { ded_stringid_t id; char *text; void release() { M_Free(text); } void setText(char const *newTextToCopy) { release(); text = M_StrDup(newTextToCopy); } }; typedef struct { ded_stringid_t id; DEDArray materials; void release() { materials.clear(); } } ded_tenviron_t; typedef struct { char *id; char *text; void release() { M_Free(id); M_Free(text); } } ded_value_t; typedef struct LIBDOOMSDAY_PUBLIC ded_linetype_s { int id; char comment[64]; ded_flags_t flags[3]; ded_flags_t lineClass; ded_flags_t actType; int actCount; float actTime; int actTag; int aparm[9]; ded_stringid_t aparm9; float tickerStart; float tickerEnd; int tickerInterval; ded_soundid_t actSound; ded_soundid_t deactSound; int evChain; int actChain; int deactChain; int actLineType; int deactLineType; ded_flags_t wallSection; de::Uri* actMaterial; de::Uri* deactMaterial; char actMsg[128]; char deactMsg[128]; float materialMoveAngle; float materialMoveSpeed; int iparm[20]; char iparmStr[20][64]; float fparm[20]; char sparm[5][128]; void release() { delete actMaterial; delete deactMaterial; } void reallocate() { DED_DUP_URI(actMaterial); DED_DUP_URI(deactMaterial); } } ded_linetype_t; typedef struct LIBDOOMSDAY_PUBLIC ded_sectortype_s { int id; char comment[64]; ded_flags_t flags; int actTag; int chain[5]; ded_flags_t chainFlags[5]; float start[5]; float end[5]; float interval[5][2]; int count[5]; ded_soundid_t ambientSound; float soundInterval[2]; // min,max float materialMoveAngle[2]; // floor, ceil float materialMoveSpeed[2]; // floor, ceil float windAngle; float windSpeed; float verticalWind; float gravity; float friction; ded_func_t lightFunc; int lightInterval[2]; ded_func_t colFunc[3]; // RGB int colInterval[3][2]; ded_func_t floorFunc; float floorMul, floorOff; int floorInterval[2]; ded_func_t ceilFunc; float ceilMul, ceilOff; int ceilInterval[2]; void release() {} void reallocate() {} } ded_sectortype_t; typedef struct LIBDOOMSDAY_PUBLIC ded_detail_stage_s { int tics; float variance; de::Uri * texture; // The file/lump with the detail texture. float scale; float strength; float maxDistance; void release() { delete texture; } void reallocate() { DED_DUP_URI(texture); } } ded_detail_stage_t; // Flags for detail texture definitions. #define DTLF_NO_IWAD 0x1 // Don't use if from IWAD. #define DTLF_PWAD 0x2 // Can use if from PWAD. #define DTLF_EXTERNAL 0x4 // Can use if from external resource. typedef struct LIBDOOMSDAY_PUBLIC ded_detailtexture_s { de::Uri *material1; de::Uri *material2; ded_flags_t flags; // There is only one stage. ded_detail_stage_t stage; void release() { delete material1; delete material2; stage.release(); } void reallocate() { DED_DUP_URI(material1); DED_DUP_URI(material2); stage.reallocate(); } } ded_detailtexture_t; typedef struct LIBDOOMSDAY_PUBLIC ded_ptcgen_s { struct ded_ptcgen_s* stateNext; // List of generators for a state. ded_stateid_t state; // Triggered by this state (if mobj-gen). de::Uri* material; ded_mobjid_t type; // Triggered by this type of mobjs. ded_mobjid_t type2; // Also triggered by this type. int typeNum; int type2Num; ded_mobjid_t damage; // Triggered by mobj damage of this type. int damageNum; de::Uri* map; // Triggered by this map. ded_flags_t flags; float speed; // Particle spawn velocity. float speedVariance; // Spawn speed variance (0-1). float vector[3]; // Particle launch vector. float vectorVariance; // Launch vector variance (0-1). 1=totally random. float initVectorVariance; // Initial launch vector variance (0-1). float center[3]; // Offset to the mobj (relat. to source). int subModel; // Model source: origin submodel #. float spawnRadius; float spawnRadiusMin; // Spawn uncertainty box. float maxDist; // Max visibility for particles. int spawnAge; // How long until spawning stops? int maxAge; // How long until generator dies? int particles; // Maximum number of particles. float spawnRate; // Particles spawned per tic. float spawnRateVariance; int preSim; // Tics to pre-simulate when spawned. int altStart; float altStartVariance; // Probability for alt start. float force; // Radial strength of the sphere force. float forceRadius; // Radius of the sphere force. float forceAxis[3]; /* Rotation axis of the sphere force (+ speed). */ float forceOrigin[3]; // Offset for the force sphere. DEDArray stages; void release() { delete material; delete map; stages.clear(); } void reallocate() { DED_DUP_URI(map); DED_DUP_URI(material); stages.reallocate(); } } ded_ptcgen_t; typedef struct LIBDOOMSDAY_PUBLIC ded_shine_stage_s { int tics; float variance; de::Uri *texture; de::Uri *maskTexture; blendmode_t blendMode; // Blend mode flags (bm_*). float shininess; float minColor[3]; float maskWidth; float maskHeight; void release() { delete texture; delete maskTexture; } void reallocate() { DED_DUP_URI(texture); DED_DUP_URI(maskTexture); } } ded_shine_stage_t; // Flags for reflection definitions. #define REFF_NO_IWAD 0x1 // Don't use if from IWAD. #define REFF_PWAD 0x2 // Can use if from PWAD. #define REFF_EXTERNAL 0x4 // Can use if from external resource. typedef struct LIBDOOMSDAY_PUBLIC ded_reflection_s { de::Uri *material; ded_flags_t flags; // There is only one stage. ded_shine_stage_t stage; void release() { delete material; stage.release(); } void reallocate() { DED_DUP_URI(material); stage.reallocate(); } } ded_reflection_t; typedef struct LIBDOOMSDAY_PUBLIC ded_group_member_s { de::Uri* material; int tics; int randomTics; void release() { delete material; } void reallocate() { DED_DUP_URI(material); } } ded_group_member_t; typedef struct LIBDOOMSDAY_PUBLIC ded_group_s { ded_flags_t flags; DEDArray members; void release() { members.clear(); } ded_group_member_t *tryFindFirstMemberWithMaterial(de::Uri const &materialUri) { if(!materialUri.isEmpty()) { for(int i = 0; i < members.size(); ++i) { if(members[i].material && *members[i].material == materialUri) { return &members[i]; } // Only animate if the first frame in the group? if(flags & AGF_FIRST_ONLY) break; } } return nullptr; // Not found. } } ded_group_t; typedef struct LIBDOOMSDAY_PUBLIC ded_compositefont_mappedcharacter_s { unsigned char ch; de::Uri* path; void release() { delete path; } void reallocate() { DED_DUP_URI(path); } } ded_compositefont_mappedcharacter_t; typedef struct LIBDOOMSDAY_PUBLIC ded_compositefont_s { de::Uri* uri; DEDArray charMap; void release() { delete uri; charMap.clear(); } } ded_compositefont_t; #endif // LIBDOOMSDAY_DEFINITION_TYPES_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/dedregister.h0000664000175000017500000001035212641367671027160 0ustar jaakkojaakko/** @file dedregister.h Register of definitions. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEDREGISTER_H #define LIBDOOMSDAY_DEDREGISTER_H #include "../libdoomsday.h" #include #include /** * General purpose register of DED definitions. * * The important characteristics of definitions are: * - preserving the order in which the definitions were parsed * - definitions are looked up by ID, name, and/or other members in addition to the * ordinal number (lookup is text-based) * * DEDRegister is not specific to any one kind of definition, but instead maintains an * array of definitions and a set of lookup dictionaries referencing subrecords in the * ordered array. * * This implementation assumes that definitions are only added, not removed (unless * all of them are removed at once). */ class LIBDOOMSDAY_PUBLIC DEDRegister { public: /// The specified index or key value was not found from the register. @ingroup errors DENG2_ERROR(NotFoundError); /// Attempted to use a key for looking up that hasn't been previously registered. /// @ingroup errors DENG2_ERROR(UndefinedKeyError); enum LookupFlag { CaseSensitive = 0x1, ///< Looking up is done case sensitively. OnlyFirst = 0x2, ///< Only the first defined value is kept in lookup (otherwise last). AllowCopy = 0x4, ///< When copying from an existing definition, include this lookup key. DefaultLookup = 0 ///< Latest in order, case insensitive, omitted from copies. }; Q_DECLARE_FLAGS(LookupFlags, LookupFlag) public: DEDRegister(de::Record &names); /** * Adds a member variable that is needed for looking up definitions. Once added, * the key can be used in has(), tryFind() and find(). * * @param keyName Name of the key variable. * @param flags Indexing behavior for the lookup. */ void addLookupKey(de::String const &variableName, LookupFlags const &flags = DefaultLookup); /** * Clears the existing definitions. The existing lookup keys are not removed. */ void clear(); /** * Adds a new definition as the last one. * * @return Reference to the added definition. Same as operator[](size()-1). */ de::Record &append(); /** * Adds a new definition as the last one, and copies contents into it from an * existing definition at @a index. * * Double-underscore members will not be copied to the new definition, and neither * will lookup keys flagged as non-copyable. * * @param index Index to copy from. * * @return Reference to the added definition. */ de::Record &appendCopy(int index); de::Record ©(int fromIndex, de::Record &to); int size() const; bool has(de::String const &key, de::String const &value) const; de::Record & operator [] (int index); de::Record const & operator [] (int index) const; de::Record * tryFind(de::String const &key, de::String const &value); de::Record const * tryFind(de::String const &key, de::String value) const; de::Record & find(de::String const &key, de::String const &value); de::Record const & find(de::String const &key, de::String const &value) const; /** * Provides immutable access to the register's dictionary, for efficient traversal. */ de::DictionaryValue const &lookup(de::String const &key) const; private: DENG2_PRIVATE(d) }; #endif // LIBDOOMSDAY_DEDREGISTER_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/material.h0000664000175000017500000000622212641367671026456 0ustar jaakkojaakko/** @file material.h Material definition accessor. * * @authors Copyright © 2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEFN_MATERIAL_H #define LIBDOOMSDAY_DEFN_MATERIAL_H #include "definition.h" #include namespace defn { /** * Utility for handling material-decoration definitions. */ class LIBDOOMSDAY_PUBLIC MaterialDecoration : public Definition { public: MaterialDecoration() : Definition() {} MaterialDecoration(MaterialDecoration const &other) : Definition(other) {} MaterialDecoration(de::Record &d) : Definition(d) {} MaterialDecoration(de::Record const &d) : Definition(d) {} void resetToDefaults(); int stageCount() const; bool hasStage(int index) const; de::Record &stage(int index); de::Record const &stage(int index) const; de::Record &addStage(); }; /** * Utility for handling material-layer definitions. */ class LIBDOOMSDAY_PUBLIC MaterialLayer : public Definition { public: MaterialLayer() : Definition() {} MaterialLayer(MaterialLayer const &other) : Definition(other) {} MaterialLayer(de::Record &d) : Definition(d) {} MaterialLayer(de::Record const &d) : Definition(d) {} void resetToDefaults(); int stageCount() const; bool hasStage(int index) const; de::Record &stage(int index); de::Record const &stage(int index) const; de::Record &addStage(); }; /** * Utility for handling material definitions. */ class LIBDOOMSDAY_PUBLIC Material : public Definition { public: Material() : Definition() {} Material(Material const &other) : Definition(other) {} Material(de::Record &d) : Definition(d) {} Material(de::Record const &d) : Definition(d) {} void resetToDefaults(); public: // Layers: ----------------------------------------------------- int layerCount() const; bool hasLayer(int index) const; de::Record &layer(int index); de::Record const &layer(int index) const; de::Record &addLayer(); public: // Decorations: ------------------------------------------------ int decorationCount() const; bool hasDecoration(int index) const; de::Record &decoration(int index); de::Record const &decoration(int index) const; de::Record &addDecoration(); }; } // namespace defn #endif // LIBDOOMSDAY_DEFN_MATERIAL_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/mapinfo.h0000664000175000017500000000324012641367671026306 0ustar jaakkojaakko/** @file defs/mapinfo.h MapInfo definition accessor. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEFN_MAPINFO_H #define LIBDOOMSDAY_DEFN_MAPINFO_H #include "definition.h" #include /// @todo These values should be tweaked a bit. #define DEFAULT_FOG_START 0 #define DEFAULT_FOG_END 2100 #define DEFAULT_FOG_DENSITY 0.0001f #define DEFAULT_FOG_COLOR_RED 138.0f/255 #define DEFAULT_FOG_COLOR_GREEN 138.0f/255 #define DEFAULT_FOG_COLOR_BLUE 138.0f/255 namespace defn { /** * Utility for handling mapinfo definitions. */ class LIBDOOMSDAY_PUBLIC MapInfo : public Definition { public: MapInfo() : Definition() {} MapInfo(MapInfo const &other) : Definition(other) {} MapInfo(de::Record &d) : Definition(d) {} MapInfo(de::Record const &d) : Definition(d) {} void resetToDefaults(); }; } // namespace defn #endif // LIBDOOMSDAY_DEFN_MAPINFO_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/decoration.h0000664000175000017500000000350412641367671027007 0ustar jaakkojaakko/** @file decoration.h Decoration definition accessor. * * @authors Copyright © 2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEFN_DECORATION_H #define LIBDOOMSDAY_DEFN_DECORATION_H #include "definition.h" #include // Flags for decoration definitions. #define DCRF_NO_IWAD 0x1 ///< Don't use if from IWAD. #define DCRF_PWAD 0x2 ///< Can use if from PWAD. //#define DCRF_EXTERNAL 0x4 ///< Can use if from external resource. namespace defn { /** * Utility for handling oldschool decoration definitions. */ class LIBDOOMSDAY_PUBLIC Decoration : public Definition { public: Decoration() : Definition() {} Decoration(Decoration const &other) : Definition(other) {} Decoration(de::Record &d) : Definition(d) {} Decoration(de::Record const &d) : Definition(d) {} void resetToDefaults(); int lightCount() const; bool hasLight(int index) const; de::Record &light(int index); de::Record const &light(int index) const; de::Record &addLight(); }; } // namespace defn #endif // LIBDOOMSDAY_DEFN_DECORATION_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/defs/model.h0000664000175000017500000000310512641367671025755 0ustar jaakkojaakko/** @file defs/model.h Model definition accessor. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_DEFN_MODEL_H #define LIBDOOMSDAY_DEFN_MODEL_H #include "definition.h" #include namespace defn { /** * Utility for handling model definitions. */ class LIBDOOMSDAY_PUBLIC Model : public Definition { public: Model() : Definition() {} Model(Model const &other) : Definition(other) {} Model(de::Record &d) : Definition(d) {} Model(de::Record const &d) : Definition(d) {} void resetToDefaults(); de::Record &addSub(); int subCount() const; bool hasSub(int index) const; de::Record &sub(int index); de::Record const &sub(int index) const; void cleanupAfterParsing(de::Record const &prev); }; } // namespace defn #endif // LIBDOOMSDAY_DEFN_MODEL_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/resource/0000775000175000017500000000000012641367671025413 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/resource/resourceclass.h0000664000175000017500000000603512641367671030445 0ustar jaakkojaakko/** * @file resourceclass.h * * Resource Class. @ingroup resource * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDOOMSDAY_RESOURCECLASS_H #define LIBDOOMSDAY_RESOURCECLASS_H #include "api_resourceclass.h" #ifdef __cplusplus #include "../filesys/filetype.h" #include #include /** * ResourceClass encapsulates the properties and logics belonging to a logical * class of resource (e.g., Graphic, Model, Sound, etc...) * * @ingroup base */ class LIBDOOMSDAY_PUBLIC ResourceClass { public: typedef QList FileTypes; public: ResourceClass(de::String name, de::String defaultScheme); /// Return the symbolic name of this resource class. de::String name() const; /// Return the symbolic name of the default filesystem subspace scheme /// for this class of resource. de::String defaultScheme() const; /// Return the number of file types for this class of resource. int fileTypeCount() const; /** * Add a new file type to the resource class. Earlier types have priority. * * @param ftype File type to add. ResourceClass takes ownership. * * @return This instance. */ ResourceClass& addFileType(de::FileType *ftype); /** * Provides access to the file type list for efficient iteration. * * @return List of file types for this class of resource. */ FileTypes const& fileTypes() const; bool isNull() const; public: static ResourceClass &classForId(resourceclassid_t id); /// @todo This is unnecessary once the resource subsystem is part of libdoomsday. -jk static void setResourceClassCallback(ResourceClass &(*callback)(resourceclassid_t)); private: DENG2_PRIVATE(d) }; /** * The special "null" ResourceClass object. * * @ingroup core */ class LIBDOOMSDAY_PUBLIC NullResourceClass : public ResourceClass { public: NullResourceClass() : ResourceClass("RC_NULL", "") {} }; /// @return @c true= @a rclass is a "null-resourceclass" object (not a real class). inline bool isNullResourceClass(ResourceClass const& rclass) { return rclass.isNull(); } #endif // __cplusplus #endif /* LIBDOOMSDAY_RESOURCECLASS_H */ doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/resource/wav.h0000664000175000017500000000521712641367671026366 0ustar jaakkojaakko/** @file wav.h WAV loader (obsolete). @ingroup audio * * @deprecated There is a better/newer WAV loader (de::Waveform). * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDOOMSDAY_RESOURCE_WAV_H #define LIBDOOMSDAY_RESOURCE_WAV_H #include "../libdoomsday.h" #include /** * Verifies that the data in the buffer @a data looks like WAV. * @return @c true, if the "RIFF" and "WAVE" strings are found. */ LIBDOOMSDAY_PUBLIC int WAV_CheckFormat(const char* data); /** * Loads a WAV sample from a memory buffer. All parameters must be passed, no * NULLs are allowed. * * @note The WAV file must contain a mono sound: only one channel. * * @param data Source data. * @param datalength Length of the source data. * @param bits Bits per sample is written here. * @param rate Sample rate is written here. * @param samples Number of samples is written here. * * @return Buffer that contains the wave data. The caller must free the sample * data using Z_Free() when it's no longer needed. */ LIBDOOMSDAY_PUBLIC void* WAV_MemoryLoad(const byte* data, size_t datalength, int* bits, int* rate, int* samples); /** * Loads a WAV sample from a file. All parameters must be passed, no NULLs are * allowed. * * @note The WAV file must contain a mono sound: only one channel. * * @param filename File path of the WAV file. * @param bits Bits per sample is written here. * @param rate Sample rate is written here. * @param samples Number of samples is written here. * * @return Buffer that contains the wave data. The caller must free the sample * data using Z_Free() when it's no longer needed. */ LIBDOOMSDAY_PUBLIC void* WAV_Load(const char* filename, int* bits, int* rate, int* samples); #endif // LIBDOOMSDAY_RESOURCE_WAV_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/uri.h0000664000175000017500000002577312641367671024552 0ustar jaakkojaakko/** @file uri.hh Universal Resource Identifier. * @ingroup base * * @author Copyright © 2010-2013 Daniel Swanson * @author Copyright © 2010-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDOOMSDAY_URI_H #define LIBDOOMSDAY_URI_H #ifdef __cplusplus #include "libdoomsday.h" #include "api_resourceclass.h" /// Schemes must be at least this many characters. #define DENG2_URI_MIN_SCHEME_LENGTH 2 #include #include #include #include struct ddstring_s; // libdeng Str namespace de { /** * Assists working with URIs and maps them to engine-managed resources. * @ingroup base * * Uri is derived from Path. It augments Path with schemes and path symbolics. * * Universal resource identifiers (URIs) are a way to identify specific * entities in a hierarchy. */ class LIBDOOMSDAY_PUBLIC Uri : public LogEntry::Arg::Base { public: /// Base class for resolve-related errors. @ingroup errors DENG2_ERROR(ResolveError); /// An unknown symbol was encountered in the embedded expression. @ingroup errors DENG2_SUB_ERROR(ResolveError, UnknownSymbolError); /// An unresolveable symbol was encountered in the embedded expression. @ingroup errors DENG2_SUB_ERROR(ResolveError, ResolveSymbolError); /** * Flags determining the composition of textual representation: */ enum ComposeAsTextFlag { OmitScheme = 0x1, ///< Exclude the scheme. OmitPath = 0x2, ///< Exclude the path. DecodePath = 0x4, ///< Decode percent-endcoded characters in the path. DefaultComposeAsTextFlags = 0 }; Q_DECLARE_FLAGS(ComposeAsTextFlags, ComposeAsTextFlag) typedef String (*ResolverFunc)(String const &symbol); public: /** * Construct an empty Uri instance. */ Uri(); /** * Construct a Uri instance from a text string. * * @param percentEncoded String to be parsed. Assumed to be in * percent-encoded representation. * * @param defaultResClass Default scheme. Determines the scheme for the Uri * if one is not specified in @a percentEncoded. * @c RC_UNKNOWN: resource locator guesses an * appropriate scheme for this type of file. * * @param sep Character used to separate path segments * in @a path. */ Uri(String const &percentEncoded, resourceclassid_t defaultResClass = RC_UNKNOWN, QChar sep = '/'); /** * Construct a Uri from a textual scheme and a path. * * @param scheme Scheme for the Uri. * @param path Path for the Uri. */ Uri(String const &scheme, Path const &path); /** * Construct a Uri instance from a path. Note that Path instances can * never contain a scheme as a prefix, so @a resClass is mandatory. * * @param resClass Scheme for the URI. @c RC_UNKNOWN: resource locator * guesses an appropriate scheme for this. * * @param path Path of the URI. */ Uri(resourceclassid_t resClass, Path const &path); /** * Construct a Uri instance from a path without a scheme. * * @param path Path of the URI. */ Uri(Path const &path); /** * Construct a Uri instance from a UTF-8 C-style text string, using RC_UNKNOWN * as the default resource class. * * @param nullTerminatedCStr String to be parsed. Assumed to be in * percent-encoded representation. */ Uri(char const *nullTerminatedCStr); /** * Construct a Uri instance by duplicating @a other. */ Uri(Uri const &other); inline Uri &operator = (Uri other) { d.swap(other.d); return *this; } /** * Swaps this Uri with @a other. * @param other Uri. */ inline void swap(Uri &other) { d.swap(other.d); } bool operator == (Uri const &other) const; bool operator != (Uri const &other) const { return !(*this == other); } /** * Constructs a Uri instance from a NativePath that refers to a file in * the native file system. All path directives such as '~' are * expanded. The resultant Uri will have an empty (zero-length) scheme * (because file paths do not include one). * * @param path Native path to a file in the native file system. * @param defaultResourceClass Default resource class. */ static Uri fromNativePath(NativePath const &path, resourceclassid_t defaultResourceClass = RC_NULL); /** * Constructs a Uri instance from a NativePath that refers to a native * directory. All path directives such as '~' are expanded. The * resultant Uri will have an empty (zero-length) scheme (because file * paths do not include one). * * @param nativeDirPath Native path to a directory in the native * file system. * @param defaultResourceClass Default resource class. */ static Uri fromNativeDirPath(NativePath const &nativeDirPath, resourceclassid_t defaultResourceClass = RC_NULL); /** * Construct a Uri instance from a user supplied, variable-length list of UTF-8 * C-style text string arguments. * * @param argv The arguments to be interpreted. All of which are assumed to * be in a @em non-percent-encoded representation. * * Supported forms (where <> denote keyword component names): * - [0: ":"] * - [0: ""] (if @a knownScheme set) * - [0: ""] * - [0: "", 1: ""] * * @param argc The number of elements in @a argv. * * @param knownScheme Callback function used to identify known scheme names when * attempting to resolve ambiguous cases (only the one argument * is provided. */ static Uri fromUserInput(char **argv, int argc, bool (*knownScheme) (String name) = 0); /** * Convert this URI to a text string. * * @see asText() */ operator String () const { return asText(); } /** * Determines if the URI's path is empty. */ bool isEmpty() const; /** * Clear the URI returning it to an empty state. */ Uri &clear(); /** * Attempt to resolve this URI. Substitutes known symbolics in the possibly * templated path. Resulting path is a well-formed, filesys compatible path * (perhaps base-relative). * * @return The resolved path. */ String resolved() const; String const &resolvedRef() const; /** * @return Scheme of the URI. */ String const &scheme() const; /** * @return Path of the URI. */ Path const &path() const; /** * @return Scheme of the URI as plain text (UTF-8 encoding). */ char const *schemeCStr() const; /** * @return Path of the URI as plain text (UTF-8 encoding). */ char const *pathCStr() const; /** * @return Scheme of the URI as plain text (UTF-8 encoding). */ struct ddstring_s const *schemeStr() const; /** * @return Path of the URI as plain text (UTF-8 encoding). */ struct ddstring_s const *pathStr() const; /** * Change the scheme of the URI to @a newScheme. */ Uri &setScheme(String newScheme); /** * Change the path of the URI to @a newPath. * * @param newPath New path for the URI. */ Uri &setPath(Path const &newPath); /** * Change the path of the URI to @a newPath. * * @param newPath New path for the URI. * @param sep Character used to separate path segments in @a path. */ Uri &setPath(String newPath, QChar sep = '/'); Uri &setPath(char const *newPathUtf8, char sep = '/'); /** * Update this URI by parsing new values from the specified arguments. * * @param newUri URI to be parsed. Assumed to be in percent-encoded representation. * * @param defaultResourceClass If no scheme is defined in @a newUri and * this is not @c RC_NULL, ask the resource locator whether it knows * of an appropriate default scheme for this class of resource. * * @param sep Character used to separate path segments in @a path. */ Uri &setUri(String newUri, resourceclassid_t defaultResourceClass = RC_UNKNOWN, QChar sep = '/'); /** * Compose from this URI a plain-text representation. Any symbolic identifiers * will be left unchanged in the resultant string (not resolved). * * @param compositionFlags Flags determining how the textual representation * is composited. * @param sep Character to use to replace path segment separators. * * @return Plain-text String representation. */ String compose(ComposeAsTextFlags compositionFlags = DefaultComposeAsTextFlags, QChar sep = '/') const; /** * Transform the URI into a human-friendly representation. Percent-encoded * symbols are decoded. * * @return Human-friendly representation of the URI. * * Same as @code compose(DefaultComposeAsTextFlags | DecodePath); @endcode */ String asText() const; // Implements LogEntry::Arg::Base. LogEntry::Arg::Type logEntryArgType() const { return LogEntry::Arg::StringArgument; } // Implements ISerializable. void operator >> (Writer &to) const; void operator << (Reader &from); public: /** * Sets the function that is used for resolving symbols in Uris. * * @param resolver Resolver callback function. */ static void setResolverFunc(ResolverFunc resolver); private: DENG2_PRIVATE(d) }; Q_DECLARE_OPERATORS_FOR_FLAGS(Uri::ComposeAsTextFlags) } // namespace de namespace std { // std::swap specialization for de::Uri template <> inline void swap(de::Uri &a, de::Uri &b) { a.swap(b); } } #endif // __cplusplus #endif // LIBDOOMSDAY_URI_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/console/0000775000175000017500000000000012641367671025226 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/console/alias.h0000664000175000017500000000277312641367671026501 0ustar jaakkojaakko/** @file alias.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_CONSOLE_ALIAS_H #define LIBDOOMSDAY_CONSOLE_ALIAS_H #include "../libdoomsday.h" #include typedef struct calias_s { /// Name of this alias. char *name; /// Aliased command string. char *command; } calias_t; void Con_InitAliases(); void Con_ClearAliases(); void Con_AddKnownWordsForAliases(); LIBDOOMSDAY_PUBLIC calias_t *Con_AddAlias(char const *name, char const *command); /** * @return @c 0 if the specified alias can't be found. */ LIBDOOMSDAY_PUBLIC calias_t *Con_FindAlias(char const *name); LIBDOOMSDAY_PUBLIC void Con_DeleteAlias(calias_t *cal); LIBDOOMSDAY_PUBLIC de::String Con_AliasAsStyledText(calias_t *alias); #endif // LIBDOOMSDAY_CONSOLE_ALIAS_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/console/knownword.h0000664000175000017500000000754312641367671027440 0ustar jaakkojaakko/** @file knownword.h * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_CONSOLE_KNOWNWORD_H #define LIBDOOMSDAY_CONSOLE_KNOWNWORD_H #include "../libdoomsday.h" #include "dd_share.h" #include "dd_types.h" #include #include typedef enum { WT_ANY = -1, KNOWNWORDTYPE_FIRST = 0, WT_CCMD = KNOWNWORDTYPE_FIRST, WT_CVAR, WT_CALIAS, WT_GAME, KNOWNWORDTYPE_COUNT } knownwordtype_t; #define VALID_KNOWNWORDTYPE(t) ((t) >= KNOWNWORDTYPE_FIRST && (t) < KNOWNWORDTYPE_COUNT) typedef struct knownword_s { knownwordtype_t type; void *data; } knownword_t; void Con_DataRegister(); void Con_UpdateKnownWords(); void Con_ClearKnownWords(); /** * Sets a callback that is called whenever the set of known words needs updating. * The callback should add all the known words using Con_AddKnownWord. * * @param callback Known word addition callback. */ LIBDOOMSDAY_PUBLIC void Con_SetApplicationKnownWordCallback(void (*callback)()); LIBDOOMSDAY_PUBLIC void Con_AddKnownWord(knownwordtype_t type, void *ptr); /** * Iterate over words in the known-word dictionary, making a callback for each. * Iteration ends when all selected words have been visited or a callback returns * non-zero. * * @param pattern If not @c NULL or an empty string, only process those * words which match this pattern. * @param type If a valid word type, only process words of this type. * @param callback Callback to make for each processed word. * @param parameters Passed to the callback. * * @return @c 0 iff iteration completed wholly. */ LIBDOOMSDAY_PUBLIC int Con_IterateKnownWords(char const *pattern, knownwordtype_t type, int (*callback)(knownword_t const *word, void *parameters), void *parameters); enum KnownWordMatchMode { KnownWordExactMatch, // case insensitive KnownWordStartsWith // case insensitive }; LIBDOOMSDAY_PUBLIC int Con_IterateKnownWords(KnownWordMatchMode matchMode, char const *pattern, knownwordtype_t type, int (*callback)(knownword_t const *word, void *parameters), void *parameters); /** * Collect an array of knownWords which match the given word (at least * partially). * * \note: The array must be freed with free() * * @param word The word to be matched. * @param type If a valid word type, only collect words of this type. * @param count If not @c NULL the matched word count is written back here. * * @return A NULL-terminated array of pointers to all the known words which * match the search criteria. */ LIBDOOMSDAY_PUBLIC knownword_t const **Con_CollectKnownWordsMatchingWord(char const *word, knownwordtype_t type, uint *count); LIBDOOMSDAY_PUBLIC AutoStr *Con_KnownWordToString(knownword_t const *word); LIBDOOMSDAY_PUBLIC de::String Con_AnnotatedConsoleTerms(QStringList terms); /** * Collects all the known words of the console into a Lexicon. */ LIBDOOMSDAY_PUBLIC de::shell::Lexicon Con_Lexicon(); #endif // LIBDOOMSDAY_CONSOLE_KNOWNWORD_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/console/var.h0000664000175000017500000000630212641367671026170 0ustar jaakkojaakko/** @file var.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_CONSOLE_VAR_H #define LIBDOOMSDAY_CONSOLE_VAR_H #include "../libdoomsday.h" #include "dd_share.h" #include #include "../uri.h" typedef struct cvar_s { /// @ref consoleVariableFlags int flags; /// Type of this variable. cvartype_t type; /// Pointer to this variable's node in the directory. void *directoryNode; /// Pointer to the user data. void *ptr; /// Minimum and maximum values (for ints and floats). float min, max; /// On-change notification callback. void (*notifyChanged)(); } cvar_t; void Con_InitVariableDirectory(); void Con_DeinitVariableDirectory(); void Con_ClearVariables(); void Con_AddKnownWordsForVariables(); LIBDOOMSDAY_PUBLIC void Con_AddVariable(cvartemplate_t const *tpl); LIBDOOMSDAY_PUBLIC void Con_AddVariableList(cvartemplate_t const *tplList); LIBDOOMSDAY_PUBLIC cvar_t *Con_FindVariable(char const *path); LIBDOOMSDAY_PUBLIC ddstring_t const *CVar_TypeName(cvartype_t type); /// @return @ref consoleVariableFlags LIBDOOMSDAY_PUBLIC int CVar_Flags(cvar_t const *var); /// @return Type of the variable. LIBDOOMSDAY_PUBLIC cvartype_t CVar_Type(cvar_t const *var); /// @return Symbolic name/path-to the variable. LIBDOOMSDAY_PUBLIC AutoStr *CVar_ComposePath(cvar_t const *var); LIBDOOMSDAY_PUBLIC int CVar_Integer(cvar_t const *var); LIBDOOMSDAY_PUBLIC float CVar_Float(cvar_t const *var); LIBDOOMSDAY_PUBLIC byte CVar_Byte(cvar_t const *var); LIBDOOMSDAY_PUBLIC char const *CVar_String(cvar_t const *var); LIBDOOMSDAY_PUBLIC de::Uri const &CVar_Uri(cvar_t const *var); LIBDOOMSDAY_PUBLIC void CVar_SetUri(cvar_t *var, de::Uri const &uri); LIBDOOMSDAY_PUBLIC void CVar_SetUri2(cvar_t *var, de::Uri const &uri, int svFlags); LIBDOOMSDAY_PUBLIC void CVar_SetString(cvar_t* var, char const* text); LIBDOOMSDAY_PUBLIC void CVar_SetString2(cvar_t *var, char const *text, int svFlags); LIBDOOMSDAY_PUBLIC void CVar_SetInteger(cvar_t* var, int value); LIBDOOMSDAY_PUBLIC void CVar_SetInteger2(cvar_t* var, int value, int svFlags); LIBDOOMSDAY_PUBLIC void CVar_SetFloat(cvar_t* var, float value); LIBDOOMSDAY_PUBLIC void CVar_SetFloat2(cvar_t* var, float value, int svFlags); LIBDOOMSDAY_PUBLIC void Con_PrintCVar(cvar_t *cvar, char const *prefix); LIBDOOMSDAY_PUBLIC void CVar_PrintReadOnlyWarning(cvar_t const *var); LIBDOOMSDAY_PUBLIC de::String Con_VarAsStyledText(cvar_t *var, char const *prefix); #endif // LIBDOOMSDAY_CONSOLE_VAR_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/console/exec.h0000664000175000017500000000537212641367671026332 0ustar jaakkojaakko/** @file exec.h Console executive module. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_CONSOLE_EXEC_H #define LIBDOOMSDAY_CONSOLE_EXEC_H #include "../libdoomsday.h" #include "dd_share.h" #include "dd_types.h" #include #include #include // known words #include #define CMDLINE_SIZE 256 #define OBSOLETE CVF_NO_ARCHIVE|CVF_HIDE // Macros for accessing the console variable values through the shared data ptr. #define CV_INT(var) (*(int *) var->ptr) #define CV_BYTE(var) (*(byte *) var->ptr) #define CV_FLOAT(var) (*(float *) var->ptr) #define CV_CHARPTR(var) (*(char **) var->ptr) #define CV_URIPTR(var) (*(de::Uri **) var->ptr) void Con_DataRegister(); #ifdef __cplusplus extern "C" { #endif LIBDOOMSDAY_PUBLIC void Con_Register(); LIBDOOMSDAY_PUBLIC dd_bool Con_Init(); LIBDOOMSDAY_PUBLIC void Con_InitDatabases(); LIBDOOMSDAY_PUBLIC void Con_ClearDatabases(); LIBDOOMSDAY_PUBLIC void Con_Shutdown(); void Con_ShutdownDatabases(); LIBDOOMSDAY_PUBLIC void Con_Ticker(timespan_t time); /** * Attempt to execute a console command. * * @param src The source of the command (@ref commandSource) * @param command The command to be executed. * @param silent Non-zero indicates not to log execution of the command. * @param netCmd If @c true, command was sent over the net. * * @return Non-zero if successful else @c 0. */ LIBDOOMSDAY_PUBLIC int Con_Execute(byte src, char const *command, int silent, dd_bool netCmd); LIBDOOMSDAY_PUBLIC int Con_Executef(byte src, int silent, char const *command, ...) PRINTF_F(3,4); LIBDOOMSDAY_PUBLIC dd_bool Con_Parse(de::Path const &fileName, dd_bool silently); #ifdef __cplusplus } // extern "C" #endif #ifdef __cplusplus LIBDOOMSDAY_PUBLIC de::String Con_GameAsStyledText(de::game::Game const *game); #endif // __cplusplus #endif // LIBDOOMSDAY_CONSOLE_EXEC_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/console/cmd.h0000664000175000017500000001005512641367671026143 0ustar jaakkojaakko/** @file command.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_CONSOLE_CMD_H #define LIBDOOMSDAY_CONSOLE_CMD_H #include "../libdoomsday.h" #include "dd_share.h" #include #define DENG_MAX_ARGS 256 typedef struct { char cmdLine[2048]; int argc; char *argv[DENG_MAX_ARGS]; } cmdargs_t; typedef struct ccmd_s { /// Next command in the global list. struct ccmd_s *next; /// Next and previous overloaded versions of this command (if any). struct ccmd_s *nextOverload, *prevOverload; /// Execute function. int (*execFunc) (byte src, int argc, char **argv); /// Name of the command. char const *name; /// @ref consoleCommandFlags int flags; /// Minimum and maximum number of arguments. Used with commands /// that utilize an engine-validated argument list. int minArgs, maxArgs; /// List of argument types for this command. cvartype_t args[DENG_MAX_ARGS]; } ccmd_t; void Con_InitCommands(); void Con_ClearCommands(void); void Con_AddKnownWordsForCommands(); LIBDOOMSDAY_PUBLIC void Con_AddCommand(ccmdtemplate_t const *cmd); LIBDOOMSDAY_PUBLIC void Con_AddCommandList(ccmdtemplate_t const *cmdList); /** * Search the console database for a named command. If one or more overloaded * variants exist then return the variant registered most recently. * * @param name Name of the command to search for. * @return Found command else @c 0 */ LIBDOOMSDAY_PUBLIC ccmd_t *Con_FindCommand(char const *name); /** * Search the console database for a command. If one or more overloaded variants * exist use the argument list to select the required variant. * * @param args * @return Found command else @c 0 */ LIBDOOMSDAY_PUBLIC ccmd_t *Con_FindCommandMatchArgs(cmdargs_t *args); /** * @return @c true iff @a name matches a known command or alias name. */ LIBDOOMSDAY_PUBLIC dd_bool Con_IsValidCommand(char const *name); LIBDOOMSDAY_PUBLIC de::String Con_CmdAsStyledText(ccmd_t *cmd); LIBDOOMSDAY_PUBLIC void Con_PrintCommandUsage(ccmd_t const *ccmd, bool allOverloads = true); /** * Returns a rich formatted, textual representation of the specified console * command's argument list, suitable for logging. * * @param ccmd The console command to format usage info for. */ LIBDOOMSDAY_PUBLIC de::String Con_CmdUsageAsStyledText(ccmd_t const *ccmd); /** * Defines a console command that behaves like a console variable but accesses * the data of a de::Config variable. * * The purpose of this mechanism is to provide a backwards compatible way to * access config variables. * * @note In the future, when the console uses Doomsday Script for executing commands, * this kind of mapping should be much easier since one can just create a reference to * the real variable and access it pretty much normally. * * @param consoleName Name of the console command ("cvar"). * @param opts Type template when setting the value (using the * ccmdtemplate_t argument template format). * @param configVariable Name of the de::Config variable. */ LIBDOOMSDAY_PUBLIC void Con_AddMappedConfigVariable(char const *consoleName, char const *opts, de::String const &configVariable); #endif // LIBDOOMSDAY_CONSOLE_CMD_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/help.h0000664000175000017500000000475112641367671024674 0ustar jaakkojaakko/** @file help.h Runtime help text strings. * @ingroup base * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_HELP_H #define LIBDOOMSDAY_HELP_H #include "libdoomsday.h" #include typedef void const *HelpId; // Help string types. enum { HST_DESCRIPTION, HST_CONSOLE_VARIABLE, HST_DEFAULT_VALUE, HST_INFO, NUM_HELPSTRING_TYPES }; LIBDOOMSDAY_PUBLIC void DH_Register(void); /** * Initializes the help string database. After which, attempts to read the * engine's own help string file. */ LIBDOOMSDAY_PUBLIC void DD_InitHelp(void); /** * Shuts down the help string database. Frees all storage and destroys * database itself. */ LIBDOOMSDAY_PUBLIC void DD_ShutdownHelp(void); LIBDOOMSDAY_PUBLIC void Help_ReadStrings(de::File const &file); /** * Finds a node matching the ID. Use DH_GetString to read strings from it. * * @param id Help node ID to be searched for. * * @return Pointer to help data, if matched; otherwise @c NULL. */ LIBDOOMSDAY_PUBLIC HelpId DH_Find(char const *id); /** * Return a string from within the helpnode. Strings are stored internally * and indexed by their type (e.g. HST_DESCRIPTION). * * @param found The helpnode to return the string from. * @param type The string type (index) to look for within the node. * * @return Pointer to the found string; otherwise @c NULL. Note, may also * return @c NULL, if passed an invalid helpnode ptr OR the help string * database has not beeen initialized yet. The returned string is actually from * an AutoStr; it will only be valid until the next garbage recycling. */ LIBDOOMSDAY_PUBLIC char const *DH_GetString(HelpId found, int type); #endif /* LIBDOOMSDAY_HELP_H */ doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/world/0000775000175000017500000000000012641367671024713 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/world/thinkerdata.h0000664000175000017500000000406012641367671027362 0ustar jaakkojaakko/** @file thinkerdata.h Base class for thinker private data. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDOOMSDAY_THINKERDATA_H #define LIBDOOMSDAY_THINKERDATA_H #include "thinker.h" #include #include /** * Base class for thinker private data. * * Contains internal functionality common to all thinkers regardless of their type. */ class LIBDOOMSDAY_PUBLIC ThinkerData : public Thinker::IData { public: DENG2_DEFINE_AUDIENCE2(Deletion, void thinkerBeingDeleted(thinker_s &)) public: ThinkerData(); ThinkerData(ThinkerData const &other); void setThinker(thinker_s *thinker); void think(); IData *duplicate() const; thinker_s &thinker(); thinker_s const &thinker() const; de::Record &info(); de::Record const &info() const; private: DENG2_PRIVATE(d) #ifdef DENG2_DEBUG public: struct DebugCounter { de::Id id; static duint32 total; DebugCounter() { total++; } ~DebugCounter() { total--; } }; DebugCounter _debugCounter; struct DebugValidator { DebugValidator() { DENG2_ASSERT(DebugCounter::total == 0); } ~DebugValidator() { DENG2_ASSERT(DebugCounter::total == 0); } }; #endif }; #endif // LIBDOOMSDAY_THINKERDATA_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/world/mobj.h0000664000175000017500000000254112641367671026015 0ustar jaakkojaakko/** @file mobj.h Base for world map objects. * * @authors Copyright © 2014 Jaakko Keränen * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDOOMSDAY_MOBJ_H #define LIBDOOMSDAY_MOBJ_H #include "dd_share.h" // This macro can be used to calculate a mobj-specific 'random' number. #define MOBJ_TO_ID(mo) ( (long)(mo)->thinker.id * 48 + ((unsigned long)(mo)/1000) ) // We'll use the base mobj template directly as our mobj. typedef struct mobj_s { DD_BASE_MOBJ_ELEMENTS() #ifdef __cplusplus mobj_s(thinker_s::InitBehavior b) : thinker(b) {} #endif } mobj_t; #endif // LIBDOOMSDAY_MOBJ_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/world/thinker.h0000664000175000017500000002433112641367671026533 0ustar jaakkojaakko/** @file thinker.h Base for all thinkers. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDOOMSDAY_THINKER_H #define LIBDOOMSDAY_THINKER_H #include "../libdoomsday.h" #include struct thinker_s; /// Function pointer to a function to handle an actor's thinking. /// The argument is a pointer to the object doing the thinking. typedef void (*thinkfunc_t) (void *); // Thinker flags: #define THINKF_STD_MALLOC 0x1 // allocated using M_Malloc rather than the zone #define THINKF_DISABLED 0x2 // thinker is disabled (in stasis) /** * Base for all thinker objects. */ typedef struct thinker_s { struct thinker_s *prev, *next; thinkfunc_t function; uint32_t _flags; thid_t id; ///< Only used for mobjs (zero is not an ID). void *d; ///< Private data (owned). #ifdef __cplusplus enum InitBehavior { InitializeToZero }; thinker_s(InitBehavior) { de::zap(*this); } DENG2_NO_ASSIGN(thinker_s) DENG2_NO_COPY(thinker_s) #endif } thinker_t; #define THINKER_DATA(thinker, T) (reinterpret_cast((thinker).d)->as()) #define THINKER_DATA_MAYBE(thinker, T) (reinterpret_cast((thinker).d)->maybeAs()) #ifdef __cplusplus extern "C" { #endif LIBDOOMSDAY_PUBLIC dd_bool Thinker_InStasis(thinker_t const *thinker); /** * Change the 'in stasis' state of a thinker (stop it from thinking). * * @param thinker The thinker to change. * @param on @c true, put into stasis. */ LIBDOOMSDAY_PUBLIC void Thinker_SetStasis(thinker_t *thinker, dd_bool on); /** * Generic thinker function that does nothing. This can be used if the private * data does all the thinking. */ LIBDOOMSDAY_PUBLIC void Thinker_NoOperation(void *thinker); #ifdef __cplusplus } // extern "C" #endif #ifdef __cplusplus #include /** * C++ wrapper for a POD thinker. * * Copying or assigning the thinker via this class ensures that the entire allocated * thinker size is copied, and a duplicate of the private data instance is made. * * Thinker::~Thinker() will delete the entire thinker, including its private data. * One can use Thinker::take() to acquire ownership of the POD thinker to prevent it * from being destroyed. * * Ultimately, thinkers should become a C++ class hierarchy, with the private data being * a normal de::IPrivate. */ class LIBDOOMSDAY_PUBLIC Thinker { public: /** * Transparently accesses a member of the internal POD thinker struct via a member * that behaves like a regular member variable. Allows old code that deals with * thinker_s to work on a Thinker instance. */ template class LIBDOOMSDAY_PUBLIC MemberDelegate { public: MemberDelegate(Thinker &thinker, int offset) : _thinker(thinker), _offset(offset) {} inline T &ref() { return *reinterpret_cast((char *)&_thinker.base() + _offset); } inline T const &ref() const { return *reinterpret_cast((char const *)&_thinker.base() + _offset); } inline T &operator -> () { return ref(); } // pointer type T expected inline T const &operator -> () const { return ref(); } // pointer type T expected inline operator T & () { return ref(); } inline operator T const & () const { return ref(); } inline T &operator = (T const &other) { ref() = other; return ref(); } private: Thinker &_thinker; int _offset; }; /** * Base class for the private data of a thinker. */ class LIBDOOMSDAY_PUBLIC IData { public: virtual ~IData() {} virtual void setThinker(thinker_s *thinker) = 0; virtual void think() = 0; virtual IData *duplicate() const = 0; DENG2_AS_IS_METHODS() }; enum AllocMethod { AllocateStandard, AllocateMemoryZone }; public: /** * Allocates a thinker using standard malloc. * * @param sizeInBytes Size of the thinker. At least sizeof(thinker_s). * @param data Optional private instance data. */ Thinker(de::dsize sizeInBytes = 0, IData *data = 0); Thinker(AllocMethod alloc, de::dsize sizeInBytes = 0, IData *data = 0); Thinker(Thinker const &other); /** * Constructs a copy of a POD thinker. A duplicate of the private data is made * if one is present in @a podThinker. * * @param podThinker Existing thinker. * @param sizeInBytes Size of the thinker struct. * @param alloc Allocation method. */ Thinker(thinker_s const &podThinker, de::dsize sizeInBytes, AllocMethod alloc = AllocateStandard); /** * Takes ownership of a previously allocated POD thinker. * * @param podThinkerToTake Thinker. * @param sizeInBytes Size of the thinker. */ Thinker(thinker_s *podThinkerToTake, de::dsize sizeInBytes); Thinker &operator = (Thinker const &other); operator thinker_s * () { return &base(); } operator thinker_s const * () { return &base(); } struct thinker_s &base(); struct thinker_s const &base() const; void enable(bool yes = true); inline void disable(bool yes = true) { enable(!yes); } /** * Clear everything to zero. The private data is destroyed, so that it will be * recreated if needed. */ void zap(); bool isDisabled() const; bool hasData() const; /** * Determines the size of the thinker in bytes. */ de::dsize sizeInBytes() const; /** * Gives ownership of the contained POD thinker to the caller. The caller also * gets ownership of the private data owned by the thinker (a C++ instance). * You should use destroy() to delete the returned memory. * * After the operation, this Thinker becomes invalid. * * @return POD thinker. The size of this struct is actually sizeInBytes(), not * sizeof(thinker_s). */ struct thinker_s *take(); /** * Transfers the contents of the Thinker into the designated existing POD thinker. * Any memory owned by @a dest is released before the copy. The destination is * assumed to have the same size as this Thinker. * * After the operation, this Thinker becomes invalid. * * @param dest Destination thinker. */ void putInto(struct thinker_s &dest); IData &data(); IData const &data() const; /** * Sets the private data for the thinker. * * @param data Private data object. Ownership taken. */ void setData(IData *data); public: /** * Destroys a POD thinker that has been acquired using take(). All the memory owned * by the thinker is released. * * @param thinkerBase POD thinker base. */ static void destroy(thinker_s *thinkerBase); static void release(thinker_s &thinkerBase); static void zap(thinker_s &thinkerBase, de::dsize sizeInBytes); private: DENG2_PRIVATE(d) public: // Value accessors (POD thinker_s compatibility for old code; TODO: remove in the future): MemberDelegate prev; MemberDelegate next; MemberDelegate function; MemberDelegate id; }; #ifdef _MSC_VER // MSVC needs some hand-holding. template class LIBDOOMSDAY_PUBLIC Thinker::MemberDelegate; template class LIBDOOMSDAY_PUBLIC Thinker::MemberDelegate; template class LIBDOOMSDAY_PUBLIC Thinker::MemberDelegate; #endif /** * Template that acts like a smart pointer to a specific type of thinker. * * Like the base class Thinker, the thinker instance is created in the constructor * and destroyed in the destructor of ThinkerT. */ template class ThinkerT : public Thinker { public: ThinkerT(AllocMethod alloc = AllocateStandard) : Thinker(alloc, sizeof(Type)) {} ThinkerT(de::dsize sizeInBytes, AllocMethod alloc = AllocateStandard) : Thinker(alloc, sizeInBytes) {} ThinkerT(Type const &thinker, de::dsize sizeInBytes = sizeof(Type), AllocMethod alloc = AllocateStandard) : Thinker(reinterpret_cast(thinker), sizeInBytes, alloc) {} ThinkerT(Type *thinkerToTake, de::dsize sizeInBytes = sizeof(Type)) : Thinker(reinterpret_cast(thinkerToTake), sizeInBytes) {} Type &base() { return *reinterpret_cast(&Thinker::base()); } Type const &base() const { return *reinterpret_cast(&Thinker::base()); } Type *operator -> () { return &base(); } Type const *operator -> () const { return &base(); } operator Type * () { return &base(); } operator Type const * () const { return &base(); } operator void * () { return reinterpret_cast(&base()); } operator void const * () const { return reinterpret_cast(&base()); } operator Type & () { return base(); } operator Type const & () const { return base(); } Type *take() { return reinterpret_cast(Thinker::take()); } void putInto(Type &dest) { Thinker::putInto(reinterpret_cast(dest)); } static void destroy(Type *thinker) { Thinker::destroy(reinterpret_cast(thinker)); } static void zap(Type &thinker, de::dsize sizeInBytes = sizeof(Type)) { Thinker::zap(reinterpret_cast(thinker), sizeInBytes); } static void release(Type &thinker) { Thinker::release(reinterpret_cast(thinker)); } }; #endif // __cplusplus #endif // LIBDOOMSDAY_THINKER_H doomsday-stable-1.15.7/doomsday/libdoomsday/include/doomsday/world/mobjthinkerdata.h0000664000175000017500000000332712641367671030237 0ustar jaakkojaakko/** @file mobjthinkerdata.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDOOMSDAY_MOBJTHINKERDATA_H #define LIBDOOMSDAY_MOBJTHINKERDATA_H #include "thinkerdata.h" #include "mobj.h" /** * Private mobj data common to both client and server. * * @todo Game-side IData should be here; eventually the games don't need to add any * custom members to mobj_s, just to their own private data instance. -jk */ class LIBDOOMSDAY_PUBLIC MobjThinkerData : public ThinkerData { public: MobjThinkerData(); MobjThinkerData(MobjThinkerData const &other); IData *duplicate() const; mobj_t *mobj(); mobj_t const *mobj() const; /** * Called whenever the current state of the mobj has changed. * * @param previousState Previous state of the object. */ virtual void stateChanged(state_t const *previousState); private: DENG2_PRIVATE(d) }; #endif // LIBDOOMSDAY_MOBJTHINKERDATA_H doomsday-stable-1.15.7/doomsday/libdoomsday/src/0000775000175000017500000000000012641367671021111 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/src/audio/0000775000175000017500000000000012641367671022212 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/src/audio/logical.cpp0000664000175000017500000001570612641367671024341 0ustar jaakkojaakko/**\file logical.cpp * * @authors Copyright © 2003-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * @par The Logical Sound Manager * * The Logical Sound Manager. Tracks all currently playing sounds * in the world, regardless of whether Sfx is available or if the * sounds are actually audible to anyone. * * Uses PU_MAP, so this has to be inited for every map. * (Done via S_MapChange()). * * @todo This should be part of an audio system base class that can be used * both by the client and the server. -jk */ #include "doomsday/audio/logical.h" #include #include // The logical sounds hash table uses sound IDs as keys. #define LOGIC_HASH_SIZE 64 #define PURGE_INTERVAL 2000 // 2 seconds typedef struct logicsound_s { struct logicsound_s *next, *prev; int id; mobj_t *origin; uint endTime; byte isRepeating; } logicsound_t; typedef struct logichash_s { logicsound_t *first, *last; } logichash_t; static logichash_t logicHash[LOGIC_HASH_SIZE]; static dd_bool logicalOneSoundPerEmitter; static uint (*logicalSoundLengthCallback)(int); void Sfx_Logical_SetOneSoundPerEmitter(dd_bool enabled) { logicalOneSoundPerEmitter = enabled; } void Sfx_Logical_SetSampleLengthCallback(uint (*callback)(int)) { logicalSoundLengthCallback = callback; } /* * Initialize the Logical Sound Manager for a new map. */ void Sfx_InitLogical(void) { // Memory in the hash table is PU_MAP, so this is acceptable. memset(logicHash, 0, sizeof(logicHash)); } /* * Return a pointer the logical sounds hash. */ logichash_t *Sfx_LogicHash(int id) { return &logicHash[(uint) id % LOGIC_HASH_SIZE]; } /* * Create and link a new logical sound hash table node. */ logicsound_t *Sfx_CreateLogical(int id) { logichash_t *hash = Sfx_LogicHash(id); logicsound_t *node = reinterpret_cast(Z_Calloc(sizeof(logicsound_t), PU_MAP, 0)); if(hash->last) { hash->last->next = node; node->prev = hash->last; } hash->last = node; if(!hash->first) hash->first = node; node->id = id; return node; } /* * Unlink and destroy a logical sound hash table node. */ void Sfx_DestroyLogical(logicsound_t * node) { logichash_t *hash = Sfx_LogicHash(node->id); if(hash->first == node) hash->first = node->next; if(hash->last == node) hash->last = node->prev; if(node->next) node->next->prev = node->prev; if(node->prev) node->prev->next = node->next; #ifdef _DEBUG memset(node, 0xDD, sizeof(*node)); #endif Z_Free(node); } /* * The sound is entered into the list of playing sounds. Called when a * 'world class' sound is started, regardless of whether it's actually * started on the local system. */ void Sfx_StartLogical(int id, mobj_t *origin, dd_bool isRepeating) { DENG_ASSERT(logicalSoundLengthCallback != 0); logicsound_t *node; uint length = (isRepeating ? 1 : logicalSoundLengthCallback(id)); if(!length) { // This is not a valid sound. return; } if(origin && logicalOneSoundPerEmitter) { // Stop all previous sounds from this origin (only one per origin). Sfx_StopLogical(0, origin); } id &= ~DDSF_FLAG_MASK; node = Sfx_CreateLogical(id); node->origin = origin; node->isRepeating = isRepeating; node->endTime = Timer_RealMilliseconds() + length; } /* * The sound is removed from the list of playing sounds. Called whenever * a sound is stopped, regardless of whether it was actually playing on * the local system. Returns the number of sounds stopped. * * id=0, origin=0: stop everything */ int Sfx_StopLogical(int id, mobj_t *origin) { logicsound_t *it, *next; int stopCount = 0; int i; if(id) { for(it = Sfx_LogicHash(id)->first; it; it = next) { next = it->next; if(it->id == id && it->origin == origin) { Sfx_DestroyLogical(it); stopCount++; } } } else { // Browse through the entire hash. for(i = 0; i < LOGIC_HASH_SIZE; i++) { for(it = logicHash[i].first; it; it = next) { next = it->next; if(!origin || it->origin == origin) { Sfx_DestroyLogical(it); stopCount++; } } } } return stopCount; } /* * Remove stopped logical sounds from the hash. */ void Sfx_PurgeLogical(void) { static uint lastTime = 0; uint i, nowTime = Timer_RealMilliseconds(); logicsound_t *it, *next; if(nowTime - lastTime < PURGE_INTERVAL) { // It's too early. return; } lastTime = nowTime; // Check all sounds in the hash. for(i = 0; i < LOGIC_HASH_SIZE; i++) { for(it = logicHash[i].first; it; it = next) { next = it->next; if(!it->isRepeating && it->endTime < nowTime) { // This has stopped. Sfx_DestroyLogical(it); } } } } /* * Returns true if the sound is currently playing somewhere in the world. * It doesn't matter if it's audible or not. * * id=0: true if any sounds are playing using the specified origin */ dd_bool Sfx_IsPlaying(int id, mobj_t *origin) { uint nowTime = Timer_RealMilliseconds(); logicsound_t *it; int i; if(id) { for(it = Sfx_LogicHash(id)->first; it; it = it->next) { if(it->id == id && it->origin == origin && (it->endTime > nowTime || it->isRepeating)) { // This one is still playing. return true; } } } else if(origin) { // Check if the origin is playing any sound. for(i = 0; i < LOGIC_HASH_SIZE; i++) { for(it = logicHash[i].first; it; it = it->next) { if(it->origin == origin && (it->endTime > nowTime || it->isRepeating)) { // This one is playing. return true; } } } } // The sound was not found. return false; } doomsday-stable-1.15.7/doomsday/libdoomsday/src/paths.cpp0000664000175000017500000000327612641367671022744 0ustar jaakkojaakko/** @file basepath.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/paths.h" #include "doomsday/filesys/sys_direc.h" #include "doomsday/filesys/fs_util.h" #include using namespace de; static std::string ddBasePath; // Doomsday root directory is at...? static std::string ddRuntimePath; char const *DD_BasePath() { return ddBasePath.c_str(); } void DD_SetBasePath(char const *path) { /// @todo Unfortunately Dir/fs_util assumes fixed-size strings, so we /// can't take advantage of std::string. -jk filename_t temp; strncpy(temp, path, FILENAME_T_MAXLEN); Dir_CleanPath(temp, FILENAME_T_MAXLEN); Dir_MakeAbsolutePath(temp, FILENAME_T_MAXLEN); // Ensure it ends with a directory separator. F_AppendMissingSlashCString(temp, FILENAME_T_MAXLEN); ddBasePath = temp; } char const *DD_RuntimePath() { return ddRuntimePath.c_str(); } void DD_SetRuntimePath(char const *path) { ddRuntimePath = path; } doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/0000775000175000017500000000000012641367671022567 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/fileid.cpp0000664000175000017500000000503112641367671024526 0ustar jaakkojaakko/** @file fileid.cpp * * Implements a file identifier in terms of a MD5 hash of its absolute path. * * @ingroup types * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include #include "doomsday/filesys/fs_main.h" #include "doomsday/filesys/fileid.h" using namespace de; FileId::FileId(Md5Hash _md5) : md5_(_md5.left(16)) #ifdef DENG_DEBUG , path_("unknown-path") #endif {} FileId::FileId(FileId const &other) : LogEntry::Arg::Base() , md5_(other.md5()) #ifdef DENG_DEBUG , path_(other.path()) #endif {} FileId &FileId::operator = (FileId other) { swap(*this, other); return *this; } bool FileId::operator < (FileId const &other) const { return md5_ < other.md5_; } bool FileId::operator == (FileId const &other) const { return md5_ == other.md5_; } bool FileId::operator != (FileId const &other) const { return md5_ != other.md5_; } String FileId::asText() const { String txt; txt.reserve(32); for(int i = 0; i < 16; ++i) { txt += String("%1").arg(String::number((byte)md5_.at(i), 16), 2, '0'); } return txt; } FileId FileId::fromPath(String path) { FileId fileId = FileId(hash(path)); #ifdef DENG_DEBUG fileId.setPath(path); #endif return fileId; } FileId::Md5Hash FileId::hash(String path) { // Ensure we've a normalized path. if(QDir::isRelativePath(path)) { path = App_BasePath() / path; } #if defined(WIN32) || defined(MACOSX) // This is a case insensitive operation. path = path.toUpper(); #endif return QCryptographicHash::hash(path.toUtf8(), QCryptographicHash::Md5); } doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/wad.cpp0000664000175000017500000002655712641367671024065 0ustar jaakkojaakko/** @file wad.cpp WAD Archive (file). * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * @authors Copyright © 1993-1996 id Software, Inc. * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/filesys/wad.h" #include "doomsday/filesys/lumpcache.h" #include "doomsday/paths.h" #include #include #include #include #include // memcpy namespace de { namespace internal { struct FileHeader { DENG2_ERROR(ReadError); Block identification; // 4 bytes dint32 lumpRecordsCount; dint32 lumpRecordsOffset; void operator << (FileHandle &from) { uint8_t buf[12]; dsize readBytes = from.read(buf, 12); if(readBytes != 12) throw ReadError("FileHeader::operator << (FileHandle &)", "Source file is truncated"); identification = Block(buf, 4); lumpRecordsCount = littleEndianByteOrder.toNative(*(dint32 *)(buf + 4)); lumpRecordsOffset = littleEndianByteOrder.toNative(*(dint32 *)(buf + 8)); } }; struct IndexEntry { DENG2_ERROR(ReadError); dint32 offset; dint32 size; Block name; // 8 bytes void operator << (FileHandle &from) { uint8_t buf[16]; dsize readBytes = from.read(buf, 16); if(readBytes != 16) throw ReadError("IndexEntry::operator << (FileHandle &)", "Source file is truncated"); name = Block(buf + 8, 8); offset = littleEndianByteOrder.toNative(*(dint32 *)(buf)); size = littleEndianByteOrder.toNative(*(dint32 *)(buf + 4)); } /// Perform all translations and encodings to the actual lump name. String nameNormalized() const { String normName; // Determine the actual length of the name. int nameLen = 0; while(nameLen < 8 && name[nameLen]) { nameLen++; } for(int i = 0; i < nameLen; ++i) { /// The Hexen demo on Mac uses the 0x80 on some lumps, maybe has significance? /// @todo Ensure that this doesn't break other IWADs. The 0x80-0xff /// range isn't normally used in lump names, right?? char ch = name[i] & 0x7f; normName += ch; } // WAD format allows characters not normally permitted in native paths. // To achieve uniformity we apply a percent encoding to the "raw" names. if(!normName.isEmpty()) { normName = QString(normName.toLatin1().toPercentEncoding()); } else { /// Zero-length names are not considered valid - replace with *something*. /// @todo fixme: Handle this more elegantly... normName = "________"; } // All lumps are ordained with an extension if they don't have one. if(normName.fileNameExtension().isEmpty()) { normName += !normName.compareWithoutCase("DEHACKED")? ".deh" : ".lmp"; } return normName; } }; static QString invalidIndexMessage(int invalidIdx, int lastValidIdx) { QString msg = QString("Invalid lump index %1 ").arg(invalidIdx); if(lastValidIdx < 0) msg += "(file is empty)"; else msg += QString("(valid range: [0..%2])").arg(lastValidIdx); return msg; } } // namespace internal using namespace internal; Wad::LumpFile::LumpFile(Entry &entry, FileHandle &hndl, String path, FileInfo const &info, File1 *container) : File1(hndl, path, info, container) , entry(entry) {} String const &Wad::LumpFile::name() const { return directoryNode().name(); } Uri Wad::LumpFile::composeUri(QChar delimiter) const { return directoryNode().path(delimiter); } PathTree::Node &Wad::LumpFile::directoryNode() const { return entry; } size_t Wad::LumpFile::read(uint8_t *buffer, bool tryCache) { return wad().readLump(info_.lumpIdx, buffer, tryCache); } size_t Wad::LumpFile::read(uint8_t *buffer, size_t startOffset, size_t length, bool tryCache) { return wad().readLump(info_.lumpIdx, buffer, startOffset, length, tryCache); } uint8_t const *Wad::LumpFile::cache() { return wad().cacheLump(info_.lumpIdx); } Wad::LumpFile &Wad::LumpFile::unlock() { wad().unlockLump(info_.lumpIdx); return *this; } Wad &Wad::LumpFile::wad() const { return container().as(); } DENG2_PIMPL_NOREF(Wad) { LumpTree entries; ///< Directory structure and entry records for all lumps. QScopedPointer dataCache; ///< Data payload cache. Instance() : entries(PathTree::MultiLeaf) {} }; Wad::Wad(FileHandle &hndl, String path, FileInfo const &info, File1 *container) : File1(hndl, path, info, container) , LumpIndex() , d(new Instance) { LOG_AS("Wad"); // Seek to the start of the header. handle_->seek(0, SeekSet); FileHeader hdr; hdr << *handle_; // Read the lump entries: if(hdr.lumpRecordsCount <= 0) return; // Seek to the start of the lump index. handle_->seek(hdr.lumpRecordsOffset, SeekSet); for(int i = 0; i < hdr.lumpRecordsCount; ++i) { IndexEntry lump; lump << *handle_; // Determine the name for this lump in the VFS. String absPath = String(DD_BasePath()) / lump.nameNormalized(); // Make an index entry for this lump. Entry &entry = d->entries.insert(absPath); entry.offset = lump.offset; entry.size = lump.size; FileHandle *dummy = 0; /// @todo Fixme! LumpFile *lumpFile = new LumpFile(entry, *dummy, entry.path(), FileInfo(lastModified(), // Inherited from the container (note recursion). i, entry.offset, entry.size, entry.size), this); entry.lumpFile.reset(lumpFile); // takes ownership catalogLump(*lumpFile); } } Wad::~Wad() {} void Wad::clearCachedLump(int lumpIndex, bool *retCleared) { LOG_AS("Wad::clearCachedLump"); if(retCleared) *retCleared = false; if(hasLump(lumpIndex)) { if(!d->dataCache.isNull()) { d->dataCache->remove(lumpIndex, retCleared); } } else { LOGDEV_RES_WARNING(invalidIndexMessage(lumpIndex, lastIndex())); } } void Wad::clearLumpCache() { LOG_AS("Wad::clearLumpCache"); if(!d->dataCache.isNull()) { d->dataCache->clear(); } } uint8_t const *Wad::cacheLump(int lumpIndex) { LOG_AS("Wad::cacheLump"); LumpFile const &lumpFile = static_cast(lump(lumpIndex)); LOGDEV_RES_XVERBOSE("\"%s:%s\" (%u bytes%s)") << NativePath(composePath()).pretty() << NativePath(lumpFile.composePath()).pretty() << (unsigned long) lumpFile.info().size << (lumpFile.info().isCompressed()? ", compressed" : ""); // Time to create the cache? if(d->dataCache.isNull()) { d->dataCache.reset(new LumpCache(LumpIndex::size())); } uint8_t const *data = d->dataCache->data(lumpIndex); if(data) return data; uint8_t *region = (uint8_t *) Z_Malloc(lumpFile.info().size, PU_APPSTATIC, 0); if(!region) throw Error("Wad::cacheLump", QString("Failed on allocation of %1 bytes for cache copy of lump #%2").arg(lumpFile.info().size).arg(lumpIndex)); readLump(lumpIndex, region, false); d->dataCache->insert(lumpIndex, region); return region; } void Wad::unlockLump(int lumpIndex) { LOG_AS("Wad::unlockLump"); LOGDEV_RES_XVERBOSE("\"%s:%s\"") << NativePath(composePath()).pretty() << NativePath(lump(lumpIndex).composePath()).pretty(); if(hasLump(lumpIndex)) { if(!d->dataCache.isNull()) { d->dataCache->unlock(lumpIndex); } } else { LOGDEV_RES_WARNING(invalidIndexMessage(lumpIndex, lastIndex())); } } size_t Wad::readLump(int lumpIndex, uint8_t *buffer, bool tryCache) { LOG_AS("Wad::readLump"); return readLump(lumpIndex, buffer, 0, lump(lumpIndex).size(), tryCache); } size_t Wad::readLump(int lumpIndex, uint8_t *buffer, size_t startOffset, size_t length, bool tryCache) { LOG_AS("Wad::readLump"); LumpFile const &lumpFile = static_cast(lump(lumpIndex)); LOGDEV_RES_XVERBOSE("\"%s:%s\" (%u bytes%s) [%u +%u]") << NativePath(composePath()).pretty() << NativePath(lumpFile.composePath()).pretty() << (unsigned long) lumpFile.size() << (lumpFile.isCompressed()? ", compressed" : "") << startOffset << length; // Try to avoid a file system read by checking for a cached copy. if(tryCache) { uint8_t const *data = (!d->dataCache.isNull() ? d->dataCache->data(lumpIndex) : 0); LOGDEV_RES_XVERBOSE("Cache %s on #%i") << (data? "hit" : "miss") << lumpIndex; if(data) { size_t readBytes = de::min(size_t(lumpFile.size()), length); std::memcpy(buffer, data + startOffset, readBytes); return readBytes; } } handle_->seek(lumpFile.info().baseOffset + startOffset, SeekSet); size_t readBytes = handle_->read(buffer, length); /// @todo Do not check the read length here. if(readBytes < length) throw Error("Wad::readLumpSection", QString("Only read %1 of %2 bytes of lump #%3").arg(readBytes).arg(length).arg(lumpIndex)); return readBytes; } uint Wad::calculateCRC() { uint crc = 0; foreach(File1 *file, allLumps()) { Entry &entry = static_cast(file->as().directoryNode()); entry.update(); crc += entry.crc; } return crc; } bool Wad::recognise(FileHandle &file) { // Seek to the start of the header. size_t initPos = file.tell(); file.seek(0, SeekSet); // Attempt to read the header. bool readOk = false; FileHeader hdr; try { hdr << file; readOk = true; } catch(FileHeader::ReadError const &) {} // Ignore // Return the stream to its original position. file.seek(initPos, SeekSet); if(!readOk) return false; return (hdr.identification == "IWAD" || hdr.identification == "PWAD"); } Wad::LumpTree const &Wad::lumpTree() const { return d->entries; } Wad::LumpFile &Wad::Entry::file() const { DENG2_ASSERT(!lumpFile.isNull()); return *lumpFile; } void Wad::Entry::update() { crc = uint(file().size()); String const lumpName = Node::name(); int const nameLen = lumpName.length(); for(int i = 0; i < nameLen; ++i) { crc += lumpName.at(i).unicode(); } } } // namespace de doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/fs_main.cpp0000664000175000017500000011073312641367671024714 0ustar jaakkojaakko/** @file fs_main.cpp * @ingroup fs * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * @authors Copyright © 1993-1996 by id Software, Inc. * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/filesys/fs_main.h" #include "doomsday/filesys/fs_util.h" #include "doomsday/console/exec.h" #include "doomsday/console/cmd.h" #include "doomsday/filesys/file.h" #include "doomsday/filesys/fileid.h" #include "doomsday/filesys/fileinfo.h" #include "doomsday/filesys/lumpindex.h" #include "doomsday/filesys/wad.h" #include "doomsday/filesys/zip.h" #include #include #include #include #include #include #include #include #include #include using namespace de; extern uint F_GetLastModified(char const *path); static FS1 *fileSystem; typedef QList FileIds; /** * Virtual (file) path => Lump name mapping. * * @todo We can't presently use a Map or Hash for these. Although the paths are * unique, several of the existing algorithms which match using patterns * assume they are sorted in a quasi load ordering. */ typedef QPair LumpMapping; typedef QList LumpMappings; /** * Virtual file-directory mapping. * Maps one (absolute) path in the virtual file system to another. * * @todo We can't presently use a Map or Hash for these. Although the paths are * unique, several of the existing algorithms which match using patterns * assume they are sorted in a quasi load ordering. */ typedef QPair PathMapping; typedef QList PathMappings; /** * @note Performance is O(n). * @return @c iterator pointing to list->end() if not found. */ static FS1::FileList::iterator findListFile(FS1::FileList &list, File1 &file) { if(list.empty()) return list.end(); // Perform the search. FS1::FileList::iterator i; for(i = list.begin(); i != list.end(); ++i) { if(&file == &(*i)->file()) { break; // This is the node we are looking for. } } return i; } /** * @note Performance is O(n). * @return @c iterator pointing to list->end() if not found. */ static FS1::FileList::iterator findListFileByPath(FS1::FileList &list, String path) { if(list.empty()) return list.end(); if(path.isEmpty()) return list.end(); // Perform the search. FS1::FileList::iterator i; for(i = list.begin(); i != list.end(); ++i) { File1 &file = (*i)->file(); if(!file.composePath().compare(path, Qt::CaseInsensitive)) { break; // This is the node we are looking for. } } return i; } static bool applyPathMapping(ddstring_t *path, PathMapping const &vdm); /** * Performs a case-insensitive pattern match. The pattern can contain * wildcards. * * @param filePath Path to match. * @param pattern Pattern with * and ? as wildcards. * * @return @c true, if @a filePath matches the pattern. */ static bool matchFileName(String const &string, String const &pattern) { static QChar const ASTERISK('*'); static QChar const QUESTION_MARK('?'); QChar const *in = string.constData(), *st = pattern.constData(); while(!in->isNull()) { if(*st == ASTERISK) { st++; continue; } if(*st != QUESTION_MARK && st->toLower() != in->toLower()) { // A mismatch. Hmm. Go back to a previous '*'. while(st >= pattern && *st != ASTERISK) { st--; } if(st < pattern) return false; // No match! // The asterisk lets us continue. } // This character of the pattern is OK. st++; in++; } // Match is good if the end of the pattern was reached. // Skip remaining asterisks. while(*st == ASTERISK) { st++; } return st->isNull(); } DENG2_PIMPL(FS1) { bool loadingForStartup; ///< @c true= Flag newly opened files as "startup". FileList openFiles; ///< List of currently opened files. FileList loadedFiles; ///< List of all loaded files present in the system. uint loadedFilesCRC; FileIds fileIds; ///< Database of unique identifiers for all loaded/opened files. LumpIndex primaryIndex; ///< Primary index of all files in the system. LumpIndex zipFileIndex; ///< Type-specific index for ZipFiles. LumpMappings lumpMappings; ///< Virtual (file) path => Lump name mapping. PathMappings pathMappings; ///< Virtual file-directory mapping. Schemes schemes; ///< File subsets. Instance(Public *i) : Base(i) , loadingForStartup(true) , loadedFilesCRC (0) , zipFileIndex (true/*paths are unique*/) {} ~Instance() { clearLoadedFiles(); clearOpenFiles(); clearIndexes(); fileIds.clear(); // Should be NOP, if bookkeeping is correct. pathMappings.clear(); lumpMappings.clear(); clearAllSchemes(); } void clearAllSchemes() { DENG2_FOR_EACH(Schemes, i, schemes) { delete *i; } schemes.clear(); } /// @return @c true if the FileId associated with @a path was released. bool releaseFileId(String path) { if(!path.isEmpty()) { FileId fileId = FileId::fromPath(path); FileIds::iterator place = qLowerBound(fileIds.begin(), fileIds.end(), fileId); if(place != fileIds.end() && *place == fileId) { LOGDEV_RES_XVERBOSE_DEBUGONLY("Released FileId %s - \"%s\"", *place << fileId.path()); fileIds.erase(place); return true; } } return false; } void clearLoadedFiles(LumpIndex *index = 0) { loadedFilesCRC = 0; // Unload in reverse load order. for(int i = loadedFiles.size() - 1; i >= 0; i--) { File1 &file = loadedFiles[i]->file(); if(!index || index->catalogues(file)) { self.deindex(file); delete &file; } } } void clearOpenFiles() { while(!openFiles.isEmpty()){ delete openFiles.takeLast(); } } void clearIndexes() { primaryIndex.clear(); zipFileIndex.clear(); } String findPath(de::Uri const &search) { // Within a subspace scheme? try { Scheme &scheme = self.scheme(search.scheme()); LOG_RES_XVERBOSE("Using scheme '%s'...") << scheme.name(); // Ensure the scheme's index is up to date. scheme.rebuild(); // The in-scheme name is the file name sans extension. String name = search.path().lastSegment().toString().fileNameWithoutExtension(); // Perform the search. Scheme::FoundNodes foundNodes; if(scheme.findAll(name, foundNodes)) { // At least one node name was matched (perhaps partially). DENG2_FOR_EACH_CONST(Scheme::FoundNodes, i, foundNodes) { PathTree::Node &node = **i; if(!node.comparePath(search.path(), PathTree::NoBranch)) { // This is the file we are looking for. return node.path(); } } } /// @todo Should return not-found here but some searches are still dependent /// on falling back to a wider search. -ds } catch(UnknownSchemeError const &) {} // Ignore this error. // Try a wider search of the whole virtual file system. QScopedPointer file(openFile(search.path(), "rb", 0, true /* allow duplicates */)); if(!file.isNull()) { return file->composePath(); } return ""; // Not found. } File1 *findLump(String path, String const & /*mode*/) { if(path.isEmpty()) return 0; // We must have an absolute path - prepend the base path if necessary. if(QDir::isRelativePath(path)) { path = App_BasePath() / path; } // First check the Zip lump index. lumpnum_t lumpNum = zipFileIndex.findLast(path); if(lumpNum >= 0) { return &zipFileIndex[lumpNum]; } // Nope. Any applicable dir/WAD redirects? if(!lumpMappings.empty()) { DENG2_FOR_EACH_CONST(LumpMappings, i, lumpMappings) { LumpMapping const &mapping = *i; if(mapping.first.compare(path)) continue; lumpnum_t lumpNum = self.lumpNumForName(mapping.second); if(lumpNum < 0) continue; return &self.lump(lumpNum); } } return 0; } FILE *findAndOpenNativeFile(String path, String const &mymode, String &foundPath) { DENG2_ASSERT(!path.isEmpty()); // We must have an absolute path - prepend the CWD if necessary. path = NativePath::workPath().withSeparators('/') / path; // Translate mymode to the C-lib's fopen() mode specifiers. char mode[8] = ""; if(mymode.contains('r')) strcat(mode, "r"); else if(mymode.contains('w')) strcat(mode, "w"); if(mymode.contains('b')) strcat(mode, "b"); else if(mymode.contains('t')) strcat(mode, "t"); // First try a real native file at this absolute path. NativePath nativePath = NativePath(path); FILE *nativeFile = fopen(nativePath.toUtf8().constData(), mode); if(nativeFile) { foundPath = nativePath.expand().withSeparators('/'); return nativeFile; } // Nope. Any applicable virtual directory mappings? if(!pathMappings.empty()) { QByteArray pathUtf8 = path.toUtf8(); AutoStr *mapped = AutoStr_NewStd(); DENG2_FOR_EACH_CONST(PathMappings, i, pathMappings) { Str_Set(mapped, pathUtf8.constData()); if(!applyPathMapping(mapped, *i)) continue; // The mapping was successful. nativePath = NativePath(Str_Text(mapped)); nativeFile = fopen(nativePath.toUtf8().constData(), mode); if(nativeFile) { foundPath = nativePath.expand().withSeparators('/'); return nativeFile; } } } return 0; } File1 *openFile(String path, String const& mode, size_t baseOffset, bool allowDuplicate) { if(path.isEmpty()) return 0; LOG_AS("FS1::openFile"); // We must have an absolute path. path = App_BasePath() / path; LOG_RES_XVERBOSE("Trying \"%s\"...") << NativePath(path).pretty(); bool const reqNativeFile = mode.contains('f'); FileHandle *hndl = 0; FileInfo info; // The temporary info descriptor. // First check for lumps? if(!reqNativeFile) { if(File1 *found = findLump(path, mode)) { // Do not read files twice. if(!allowDuplicate && !self.checkFileId(found->composeUri())) return 0; // Get a handle to the lump we intend to open. /// @todo The way this buffering works is nonsensical it should not be done here /// but should instead be deferred until the content of the lump is read. hndl = FileHandle::fromLump(*found); // Prepare a temporary info descriptor. info = found->info(); } } // Not found? - try a native file. if(!hndl) { String foundPath; if(FILE *found = findAndOpenNativeFile(path, mode, foundPath)) { // Do not read files twice. if(!allowDuplicate && !self.checkFileId(de::Uri(foundPath, RC_NULL))) { fclose(found); return 0; } // Acquire a handle on the file we intend to open. hndl = FileHandle::fromNativeFile(*found, baseOffset); // Prepare the temporary info descriptor. info = FileInfo(F_GetLastModified(foundPath.toUtf8().constData())); } } // Nothing? if(!hndl) return 0; // Search path is used here rather than found path as the latter may have // been mapped to another location. We want the file to be attributed with // the path it is to be known by throughout the virtual file system. File1 &file = self.interpret(*hndl, path, info); if(loadingForStartup) { file.setStartup(true); } return &file; } }; FS1::FS1() : d(new Instance(this)) {} FS1::Scheme &FS1::createScheme(String name, Scheme::Flags flags) { DENG2_ASSERT(name.length() >= Scheme::min_name_length); // Ensure this is a unique name. if(knownScheme(name)) return scheme(name); // Create a new scheme. Scheme *newScheme = new Scheme(name, flags); d->schemes.insert(name.toLower(), newScheme); return *newScheme; } void FS1::index(File1 &file) { #ifdef DENG_DEBUG // Ensure this hasn't yet been indexed. FileList::const_iterator found = findListFile(d->loadedFiles, file); if(found != d->loadedFiles.end()) throw Error("FS1::index", "File \"" + NativePath(file.composePath()).pretty() + "\" has already been indexed"); #endif // Publish lumps to one or more indexes? if(Zip *zip = file.maybeAs()) { if(!zip->isEmpty()) { // Insert the lumps into their rightful places in the index. for(int i = 0; i < zip->lumpCount(); ++i) { File1 &lump = zip->lump(i); d->primaryIndex.catalogLump(lump); // Zip files go into a special ZipFile index as well. d->zipFileIndex.catalogLump(lump); } } } else if(Wad *wad = file.maybeAs()) { if(!wad->isEmpty()) { // Insert the lumps into their rightful places in the index. for(int i = 0; i < wad->lumpCount(); ++i) { d->primaryIndex.catalogLump(wad->lump(i)); } } } // Add a handle to the loaded files list. FileHandle *hndl = FileHandle::fromFile(file); d->loadedFiles.push_back(hndl); hndl->setList(reinterpret_cast(&d->loadedFiles)); d->loadedFilesCRC = 0; } void FS1::deindex(File1 &file) { FileList::iterator found = findListFile(d->loadedFiles, file); if(found == d->loadedFiles.end()) return; // Most peculiar.. d->releaseFileId(file.composePath()); d->zipFileIndex.pruneByFile(file); d->primaryIndex.pruneByFile(file); d->loadedFiles.erase(found); d->loadedFilesCRC = 0; delete *found; } File1 &FS1::find(de::Uri const &search) { LOG_AS("FS1::find"); if(!search.isEmpty()) { try { String searchPath = search.resolved(); // Convert to an absolute path. if(!QDir::isAbsolutePath(searchPath)) { searchPath = App_BasePath() / searchPath; } FileList::iterator found = findListFileByPath(d->loadedFiles, searchPath); if(found != d->loadedFiles.end()) { DENG_ASSERT((*found)->hasFile()); return (*found)->file(); } } catch(de::Uri::ResolveError const &er) { // Log but otherwise ignore unresolved paths. LOGDEV_RES_VERBOSE(er.asText()); } } /// @throw NotFoundError No files found matching the search term. throw NotFoundError("FS1::find", "No files found matching '" + search.compose() + "'"); } String FS1::findPath(de::Uri const &search, int flags, ResourceClass &rclass) { LOG_AS("FS1::findPath"); if(!search.isEmpty()) { try { String searchPath = search.resolved(); // If an extension was specified, first look for files of the same type. String ext = searchPath.fileNameExtension(); if(!ext.isEmpty() && ext.compare(".*")) { String found = d->findPath(de::Uri(search.scheme(), searchPath)); if(!found.isEmpty()) return found; // If we are looking for a particular file type, get out of here. if(flags & RLF_MATCH_EXTENSION) return ""; } if(isNullResourceClass(rclass) || !rclass.fileTypeCount()) return ""; /* * Try each expected file type name extension for this resource class. */ String searchPathWithoutFileNameExtension = searchPath.fileNamePath() / searchPath.fileNameWithoutExtension(); DENG2_FOR_EACH_CONST(ResourceClass::FileTypes, typeIt, rclass.fileTypes()) { DENG2_FOR_EACH_CONST(QStringList, i, (*typeIt)->knownFileNameExtensions()) { String const &ext = *i; String found = d->findPath(de::Uri(search.scheme(), searchPathWithoutFileNameExtension + ext)); if(!found.isEmpty()) return found; } }; } catch(de::Uri::ResolveError const &er) { // Log but otherwise ignore unresolved paths. LOGDEV_RES_VERBOSE(er.asText()); } } /// @throw NotFoundError No files found matching the search term. throw NotFoundError("FS1::findPath", "No paths found matching '" + search.compose() + "'"); } String FS1::findPath(de::Uri const &search, int flags) { return findPath(search, flags, ResourceClass::classForId(RC_NULL)); } #ifdef DENG_DEBUG static void printFileIds(FileIds const &fileIds) { uint idx = 0; DENG2_FOR_EACH_CONST(FileIds, i, fileIds) { LOGDEV_RES_MSG(" %u - %s : \"%s\"") << idx << *i << i->path(); ++idx; } } #endif #ifdef DENG_DEBUG static void printFileList(FS1::FileList &list) { uint idx = 0; DENG2_FOR_EACH_CONST(FS1::FileList, i, list) { FileHandle &hndl = **i; File1 &file = hndl.file(); FileId fileId = FileId::fromPath(file.composePath()); LOGDEV_RES_VERBOSE(" %c%d: %s - \"%s\" (handle: %p)") << (file.hasStartup()? '*' : ' ') << idx << fileId << fileId.path() << &hndl; ++idx; } } #endif int FS1::unloadAllNonStartupFiles() { #ifdef DENG_DEBUG // List all open files with their identifiers. if(LogBuffer::get().isEnabled(LogEntry::Generic | LogEntry::Verbose)) { LOGDEV_RES_VERBOSE("Open files at reset:"); printFileList(d->openFiles); LOGDEV_RES_VERBOSE("End\n"); } #endif // Perform non-startup file unloading (in reverse load order). int numUnloadedFiles = 0; for(int i = d->loadedFiles.size() - 1; i >= 0; i--) { File1 &file = d->loadedFiles[i]->file(); if(file.hasStartup()) continue; deindex(file); delete &file; numUnloadedFiles += 1; } #ifdef DENG_DEBUG // Sanity check: look for orphaned identifiers. if(!d->fileIds.empty()) { LOGDEV_RES_MSG("Orphan FileIds:"); printFileIds(d->fileIds); } #endif return numUnloadedFiles; } bool FS1::checkFileId(de::Uri const &path) { if(!accessFile(path)) return false; // Calculate the identifier. FileId fileId = FileId::fromPath(path.compose()); FileIds::iterator place = qLowerBound(d->fileIds.begin(), d->fileIds.end(), fileId); if(place != d->fileIds.end() && *place == fileId) return false; LOGDEV_RES_XVERBOSE_DEBUGONLY("checkFileId \"%s\" => %s", fileId.path() << fileId); /* path() is debug-only */ d->fileIds.insert(place, fileId); return true; } void FS1::resetFileIds() { d->fileIds.clear(); } void FS1::endStartup() { d->loadingForStartup = false; } LumpIndex const &FS1::nameIndex() const { return d->primaryIndex; } lumpnum_t FS1::lumpNumForName(String name) { LOG_AS("FS1::lumpNumForName"); if(name.isEmpty()) return -1; // Append a .lmp extension if none is specified. if(name.fileNameExtension().isEmpty()) { name += ".lmp"; } // Perform the search. return d->primaryIndex.findLast(Path(name)); } void FS1::releaseFile(File1 &file) { for(int i = d->openFiles.size() - 1; i >= 0; i--) { FileHandle &hndl = *(d->openFiles[i]); if(&hndl.file() == &file) { d->openFiles.removeAt(i); } } } /// @return @c NULL= Not found. static Wad *findFirstWadFile(FS1::FileList &list, bool custom) { if(list.empty()) return 0; DENG2_FOR_EACH(FS1::FileList, i, list) { File1 &file = (*i)->file(); if(custom != file.hasCustom()) continue; if(Wad *wad = file.maybeAs()) { return wad; } } return 0; } uint FS1::loadedFilesCRC() { if(!d->loadedFilesCRC) { /** * We define the CRC as that of the lump directory of the first loaded IWAD. * @todo Really kludgy... */ // CRC not calculated yet, let's do it now. Wad *iwad = findFirstWadFile(d->loadedFiles, false/*not-custom*/); if(!iwad) return 0; d->loadedFilesCRC = iwad->calculateCRC(); } return d->loadedFilesCRC; } FS1::FileList const &FS1::loadedFiles() const { return d->loadedFiles; } int FS1::findAll(bool (*predicate)(File1 &file, void *parameters), void *parameters, FS1::FileList &found) const { int numFound = 0; DENG2_FOR_EACH_CONST(FS1::FileList, i, d->loadedFiles) { // Interested in this file? if(predicate && !predicate((*i)->file(), parameters)) continue; // Nope. found.push_back(*i); numFound += 1; } return numFound; } int FS1::findAllPaths(Path searchPattern, int flags, FS1::PathList &found) { int const numFoundSoFar = found.count(); // We must have an absolute path - prepend the base path if necessary. if(!QDir::isAbsolutePath(searchPattern)) { searchPattern = App_BasePath() / searchPattern; } /* * Check the Zip directory. */ DENG2_FOR_EACH_CONST(LumpIndex::Lumps, i, d->zipFileIndex.allLumps()) { File1 const &lump = **i; PathTree::Node const &node = lump.directoryNode(); String filePath; bool patternMatched; if(!(flags & SearchPath::NoDescend)) { filePath = lump.composePath(); patternMatched = matchFileName(filePath, searchPattern); } else { patternMatched = !node.comparePath(searchPattern, PathTree::MatchFull); } if(!patternMatched) continue; // Not yet composed the path? if(filePath.isEmpty()) { filePath = lump.composePath(); } found.push_back(PathListItem(filePath, !node.isLeaf()? A_SUBDIR : 0)); } /* * Check the dir/WAD direcs. */ if(!d->lumpMappings.empty()) { DENG2_FOR_EACH_CONST(LumpMappings, i, d->lumpMappings) { if(!matchFileName(i->first, searchPattern)) continue; found.push_back(PathListItem(i->first, 0 /*only filepaths (i.e., leaves) can be mapped to lumps*/)); } /// @todo Shouldn't these be sorted? -ds } /* * Check native paths. */ String searchDirectory = searchPattern.toString().fileNamePath(); if(!searchDirectory.isEmpty()) { QByteArray searchDirectoryUtf8 = searchDirectory.toUtf8(); PathList nativeFilePaths; AutoStr *wildPath = AutoStr_NewStd(); Str_Reserve(wildPath, searchDirectory.length() + 2 + 16); // Conservative estimate. for(int i = -1; i < (int)d->pathMappings.count(); ++i) { Str_Clear(wildPath); Str_Appendf(wildPath, "%s/", searchDirectoryUtf8.constData()); if(i > -1) { // Possible mapping? if(!applyPathMapping(wildPath, d->pathMappings[i])) continue; } Str_AppendChar(wildPath, '*'); FindData fd; if(!FindFile_FindFirst(&fd, Str_Text(wildPath))) { // First path found. do { // Ignore relative directory symbolics. if(Str_Compare(&fd.name, ".") && Str_Compare(&fd.name, "..")) { String foundPath = searchDirectory / NativePath(Str_Text(&fd.name)).withSeparators('/'); if(!matchFileName(foundPath, searchPattern)) continue; nativeFilePaths.push_back(PathListItem(foundPath, fd.attrib)); } } while(!FindFile_FindNext(&fd)); } FindFile_Finish(&fd); } // Sort the native file paths. qSort(nativeFilePaths.begin(), nativeFilePaths.end()); // Add the native file paths to the found results. found.append(nativeFilePaths); } return found.count() - numFoundSoFar; } File1 &FS1::interpret(FileHandle &hndl, String filePath, FileInfo const &info) { DENG2_ASSERT(!filePath.isEmpty()); File1 *interpretedFile = 0; // Firstly try the interpreter for the guessed resource types. FileType const &ftypeGuess = DD_GuessFileTypeFromFileName(filePath); if(NativeFileType const* fileType = dynamic_cast(&ftypeGuess)) { interpretedFile = fileType->interpret(hndl, filePath, info); } // If not yet interpreted - try each recognisable format in order. if(!interpretedFile) { FileTypes const &fileTypes = DD_FileTypes(); DENG2_FOR_EACH_CONST(FileTypes, i, fileTypes) { if(NativeFileType const *fileType = dynamic_cast(*i)) { // Already tried this? if(fileType == &ftypeGuess) continue; interpretedFile = fileType->interpret(hndl, filePath, info); if(interpretedFile) break; } } } // Still not interpreted? if(!interpretedFile) { // Use a generic file. File1 *container = (hndl.hasFile() && hndl.file().isContained())? &hndl.file().container() : 0; interpretedFile = new File1(hndl, filePath, info, container); } DENG2_ASSERT(interpretedFile); return *interpretedFile; } FileHandle &FS1::openFile(String const &path, String const &mode, size_t baseOffset, bool allowDuplicate) { #ifdef DENG_DEBUG for(int i = 0; i < mode.length(); ++i) { if(mode[i] != 'r' && mode[i] != 't' && mode[i] != 'b' && mode[i] != 'f') throw Error("FS1::openFile", "Unknown argument in mode string '" + mode + "'"); } #endif File1 *file = d->openFile(path, mode, baseOffset, allowDuplicate); if(!file) throw NotFoundError("FS1::openFile", "No files found matching '" + path + "'"); // Add a handle to the opened files list. FileHandle &hndl = *FileHandle::fromFile(*file); d->openFiles.push_back(&hndl); hndl.setList(reinterpret_cast(&d->openFiles)); return hndl; } FileHandle &FS1::openLump(File1 &lump) { // Add a handle to the opened files list. FileHandle &hndl = *FileHandle::fromLump(lump); d->openFiles.push_back(&hndl); hndl.setList(reinterpret_cast(&d->openFiles)); return hndl; } bool FS1::accessFile(de::Uri const &search) { try { QScopedPointer file(d->openFile(search.resolved(), "rb", 0, true /* allow duplicates */)); return !file.isNull(); } catch(de::Uri::ResolveError const &er) { // Log but otherwise ignore unresolved paths. LOGDEV_RES_VERBOSE(er.asText()); } return false; } void FS1::addPathLumpMapping(String lumpName, String destination) { if(lumpName.isEmpty() || destination.isEmpty()) return; // We require an absolute path - prepend the CWD if necessary. if(QDir::isRelativePath(destination)) { String workPath = DENG2_APP->currentWorkPath().withSeparators('/'); destination = workPath / destination; } // Have already mapped this path? LumpMappings::iterator found = d->lumpMappings.begin(); for(; found != d->lumpMappings.end(); ++found) { LumpMapping const &ldm = *found; if(!ldm.first.compare(destination, Qt::CaseInsensitive)) break; } LumpMapping *ldm; if(found == d->lumpMappings.end()) { // No. Acquire another mapping. d->lumpMappings.push_back(LumpMapping(destination, lumpName)); ldm = &d->lumpMappings.back(); } else { // Remap to another lump. ldm = &*found; ldm->second = lumpName; } LOG_RES_MSG("Path \"%s\" now mapped to lump \"%s\"") << NativePath(ldm->first).pretty() << ldm->second; } void FS1::clearPathLumpMappings() { d->lumpMappings.clear(); } /// @return @c true iff the mapping matched the path. static bool applyPathMapping(ddstring_t *path, PathMapping const &pm) { if(!path) return false; QByteArray destUtf8 = pm.first.toUtf8(); AutoStr *dest = AutoStr_FromTextStd(destUtf8.constData()); if(qstrnicmp(Str_Text(path), Str_Text(dest), Str_Length(dest))) return false; // Replace the beginning with the source path. QByteArray sourceUtf8 = pm.second.toUtf8(); AutoStr *temp = AutoStr_FromTextStd(sourceUtf8.constData()); Str_PartAppend(temp, Str_Text(path), pm.first.length(), Str_Length(path) - pm.first.length()); Str_Copy(path, temp); return true; } void FS1::addPathMapping(String source, String destination) { if(source.isEmpty() || destination.isEmpty()) return; // Have already mapped this source path? PathMappings::iterator found = d->pathMappings.begin(); for(; found != d->pathMappings.end(); ++found) { PathMapping const &pm = *found; if(!pm.second.compare(source, Qt::CaseInsensitive)) break; } PathMapping* pm; if(found == d->pathMappings.end()) { // No. Acquire another mapping. d->pathMappings.push_back(PathMapping(destination, source)); pm = &d->pathMappings.back(); } else { // Remap to another destination. pm = &*found; pm->first = destination; } LOG_RES_MSG("Path \"%s\" now mapped to \"%s\"") << NativePath(pm->second).pretty() << NativePath(pm->first).pretty(); } void FS1::clearPathMappings() { d->pathMappings.clear(); } void FS1::printDirectory(Path path) { LOG_RES_MSG(_E(b) "Directory: %s") << NativePath(path).pretty(); // We are interested in *everything*. path = path / "*"; PathList found; if(findAllPaths(path, 0, found)) { qSort(found.begin(), found.end()); DENG2_FOR_EACH_CONST(PathList, i, found) { LOG_RES_MSG(" %s") << NativePath(i->path).pretty(); } } } bool FS1::knownScheme(String name) { if(!name.isEmpty()) { Schemes::iterator found = d->schemes.find(name.toLower()); if(found != d->schemes.end()) return true; } return false; } FS1::Scheme &FS1::scheme(String name) { if(!name.isEmpty()) { Schemes::iterator found = d->schemes.find(name.toLower()); if(found != d->schemes.end()) return **found; } /// @throw UnknownSchemeError An unknown scheme was referenced. throw UnknownSchemeError("FS1::scheme", "No scheme found matching '" + name + "'"); } FS1::Schemes const &FS1::allSchemes() { return d->schemes; } /// Print contents of directories as Doomsday sees them. D_CMD(Dir) { DENG2_UNUSED(src); if(argc > 1) { for(int i = 1; i < argc; ++i) { String path = NativePath(argv[i]).expand().withSeparators('/'); App_FileSystem().printDirectory(path); } } else { App_FileSystem().printDirectory(String("/")); } return true; } /// Dump a copy of a virtual file to the runtime directory. D_CMD(DumpLump) { DENG2_UNUSED2(src, argc); if(fileSystem) { lumpnum_t lumpNum = App_FileSystem().lumpNumForName(argv[1]); if(lumpNum >= 0) { return F_DumpFile(App_FileSystem().lump(lumpNum), 0); } LOG_RES_ERROR("No such lump"); return false; } return false; } /// List virtual files inside containers. D_CMD(ListLumps) { DENG2_UNUSED3(src, argc, argv); if(!fileSystem) return false; LumpIndex const &lumpIndex = App_FileSystem().nameIndex(); int const numRecords = lumpIndex.size(); int const numIndexDigits = de::max(3, M_NumDigits(numRecords)); LOG_RES_MSG("LumpIndex %p (%i records):") << &lumpIndex << numRecords; int idx = 0; DENG2_FOR_EACH_CONST(LumpIndex::Lumps, i, lumpIndex.allLumps()) { File1 const &lump = **i; String containerPath = NativePath(lump.container().composePath()).pretty(); String lumpPath = NativePath(lump.composePath()).pretty(); LOG_RES_MSG(String("%1 - \"%2:%3\" (size: %4 bytes%5)") .arg(idx++, numIndexDigits, 10, QChar('0')) .arg(containerPath) .arg(lumpPath) .arg(lump.info().size) .arg(lump.info().isCompressed()? " compressed" : "")); } LOG_RES_MSG("---End of lumps---"); return true; } /// List presently loaded files in original load order. D_CMD(ListFiles) { DENG2_UNUSED3(src, argc, argv); LOG_RES_MSG(_E(b) "Loaded Files " _E(l) "(in load order)" _E(w) ":"); int totalFiles = 0; int totalPackages = 0; if(fileSystem) { FS1::FileList const &allLoadedFiles = App_FileSystem().loadedFiles(); DENG2_FOR_EACH_CONST(FS1::FileList, i, allLoadedFiles) { File1 &file = (*i)->file(); uint crc = 0; int fileCount = 1; if(de::Zip *zip = file.maybeAs()) { fileCount = zip->lumpCount(); } else if(de::Wad *wad = file.maybeAs()) { fileCount = wad->lumpCount(); crc = (!file.hasCustom()? wad->calculateCRC() : 0); } LOG_RES_MSG(" %s " _E(2)_E(>) "(%i %s%s)%s") << NativePath(file.composePath()).pretty() << fileCount << (fileCount != 1 ? "files" : "file") << (file.hasStartup()? ", startup" : "") << (crc? QString(" [%1]").arg(crc, 0, 16) : ""); totalFiles += fileCount; ++totalPackages; } } LOG_RES_MSG(_E(b)"Total: " _E(.) "%i files in %i packages") << totalFiles << totalPackages; return true; } void FS1::consoleRegister() { C_CMD("dir", "", Dir); C_CMD("ls", "", Dir); // Alias C_CMD("dir", "s*", Dir); C_CMD("ls", "s*", Dir); // Alias C_CMD("dump", "s", DumpLump); C_CMD("listfiles", "", ListFiles); C_CMD("listlumps", "", ListLumps); } FS1 &App_FileSystem() { if(!fileSystem) throw Error("App_FileSystem", "File system not yet initialized"); return *fileSystem; } String App_BasePath() { return App::app().nativeBasePath().withSeparators('/'); } void F_Init() { DENG2_ASSERT(!fileSystem); fileSystem = new FS1(); } void F_Shutdown() { if(!fileSystem) return; delete fileSystem; fileSystem = 0; } void const *F_LumpIndex() { return &App_FileSystem().nameIndex(); } doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/lumpindex.cpp0000664000175000017500000005031612641367671025305 0ustar jaakkojaakko/** @file lumpindex.cpp Index of lumps. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * @authors Copyright © 1999-2006 by Colin Phipps, Florian Schulze, Neil Stevens, Andrey Budko (PrBoom 2.2.6) * @authors Copyright © 1999-2001 by Jess Haas, Nicolas Kalkhof (PrBoom 2.2.6) * @authors Copyright © 1999 Chi Hoang, Lee Killough, Jim Flynn, Rand Phares, Ty Halderman (PrBoom 2.2.6) * @authors Copyright © 1993-1996 by id Software, Inc. * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/filesys/lumpindex.h" #include #include #include namespace de { namespace internal { struct LumpSortInfo { File1 const *lump; String path; int origIndex; }; static int lumpSorter(void const *a, void const *b) { LumpSortInfo const *infoA = (LumpSortInfo const *)a; LumpSortInfo const *infoB = (LumpSortInfo const *)b; if(int delta = infoA->path.compare(infoB->path, Qt::CaseInsensitive)) return delta; // Still matched; try the file load order indexes. if(int delta = (infoA->lump->container().loadOrderIndex() - infoB->lump->container().loadOrderIndex())) return delta; // Still matched (i.e., present in the same package); use the original indexes. return (infoB->origIndex - infoA->origIndex); } } // namespace internal using namespace internal; DENG2_PIMPL_NOREF(LumpIndex::Id1MapRecognizer) { lumpnum_t lastLump; Lumps lumps; String id; Format format; Instance() : lastLump(-1), format(UnknownFormat) {} }; LumpIndex::Id1MapRecognizer::Id1MapRecognizer(LumpIndex const &lumpIndex, lumpnum_t lumpIndexOffset) : d(new Instance) { LOG_AS("LumpIndex::Id1MapRecognizer"); LOG_RES_XVERBOSE("Locating data lumps..."); // Keep checking lumps to see if its a map data lump. dint const numLumps = lumpIndex.size(); String sourceFile; for(d->lastLump = de::max(lumpIndexOffset, 0); d->lastLump < numLumps; ++d->lastLump) { // Lump name determines whether this lump is a candidate. File1 &lump = lumpIndex[d->lastLump]; DataType dataType = typeForLumpName(lump.name()); if(d->lumps.isEmpty()) { // No sequence has yet begun. Continue the scan? if(dataType == UnknownData) continue; // Missing a header? if(d->lastLump == 0) return; // The id of the map is the name of the lump which precedes the first // recognized data lump (which should be the header). Note that some // ports include MAPINFO-like data in the header. d->id = lumpIndex[d->lastLump - 1].name().fileNameAndPathWithoutExtension(); sourceFile = lump.container().composePath(); } else { // The first unrecognized lump ends the sequence. if(dataType == UnknownData) break; // A lump from another source file also ends the sequence. if(sourceFile.compareWithoutCase(lump.container().composePath())) break; } // A recognized map data lump; record it in the collection (replacing any // existing record of the same type). d->lumps.insert(dataType, &lump); } if(d->lumps.isEmpty()) return; // At this point we know we've found something that could be map data. // Some data lumps are specific to a particular map format and thus their // presence unambiguously identifies the format. if(d->lumps.contains(BehaviorData)) { d->format = HexenFormat; } else if(d->lumps.contains(MacroData) || d->lumps.contains(TintColorData) || d->lumps.contains(LeafData)) { d->format = Doom64Format; } else { d->format = DoomFormat; } // Determine whether each data lump is of the expected size. duint numVertexes = 0, numThings = 0, numLines = 0, numSides = 0, numSectors = 0, numLights = 0; DENG2_FOR_EACH_CONST(Lumps, i, d->lumps) { DataType const dataType = i.key(); File1 const &lump = *i.value(); // Determine the number of map data objects of each data type. duint *elemCountAddr = 0; dsize const elemSize = elementSizeForDataType(d->format, dataType); switch(dataType) { default: break; case VertexData: elemCountAddr = &numVertexes; break; case ThingData: elemCountAddr = &numThings; break; case LineDefData: elemCountAddr = &numLines; break; case SideDefData: elemCountAddr = &numSides; break; case SectorDefData: elemCountAddr = &numSectors; break; case TintColorData: elemCountAddr = &numLights; break; } if(elemCountAddr) { if(lump.size() % elemSize != 0) { // What *is* this?? d->format = UnknownFormat; d->id.clear(); return; } *elemCountAddr += lump.size() / elemSize; } } // A valid map contains at least one of each of these element types. /// @todo Support loading "empty" maps. if(!numVertexes || !numLines || !numSides || !numSectors) { d->format = UnknownFormat; d->id.clear(); return; } //LOG_RES_VERBOSE("Recognized %s format map") << Id1Map::formatName(d->format); } String const &LumpIndex::Id1MapRecognizer::id() const { return d->id; } LumpIndex::Id1MapRecognizer::Format LumpIndex::Id1MapRecognizer::format() const { return d->format; } LumpIndex::Id1MapRecognizer::Lumps const &LumpIndex::Id1MapRecognizer::lumps() const { return d->lumps; } File1 *LumpIndex::Id1MapRecognizer::sourceFile() const { if(d->lumps.isEmpty()) return 0; return &lumps().find(VertexData).value()->container(); } lumpnum_t LumpIndex::Id1MapRecognizer::lastLump() const { return d->lastLump; } String const &LumpIndex::Id1MapRecognizer::formatName(Format id) // static { static String const names[1 + KnownFormatCount] = { /* MF_UNKNOWN */ "Unknown", /* MF_DOOM */ "id Tech 1 (Doom)", /* MF_HEXEN */ "id Tech 1 (Hexen)", /* MF_DOOM64 */ "id Tech 1 (Doom64)" }; if(id >= DoomFormat && id < KnownFormatCount) { return names[1 + id]; } return names[0]; } /// @todo Optimize: Replace linear search... LumpIndex::Id1MapRecognizer::DataType LumpIndex::Id1MapRecognizer::typeForLumpName(String name) // static { static const struct LumpTypeInfo { String name; DataType type; } lumpTypeInfo[] = { { "THINGS", ThingData }, { "LINEDEFS", LineDefData }, { "SIDEDEFS", SideDefData }, { "VERTEXES", VertexData }, { "SEGS", SegData }, { "SSECTORS", SubsectorData }, { "NODES", NodeData }, { "SECTORS", SectorDefData }, { "REJECT", RejectData }, { "BLOCKMAP", BlockmapData }, { "BEHAVIOR", BehaviorData }, { "SCRIPTS", ScriptData }, { "LIGHTS", TintColorData }, { "MACROS", MacroData }, { "LEAFS", LeafData }, { "GL_VERT", GLVertexData }, { "GL_SEGS", GLSegData }, { "GL_SSECT", GLSubsectorData }, { "GL_NODES", GLNodeData }, { "GL_PVS", GLPVSData }, { "", UnknownData } }; // Ignore the file extension if present. name = name.fileNameWithoutExtension(); if(!name.isEmpty()) { for(dint i = 0; !lumpTypeInfo[i].name.isEmpty(); ++i) { LumpTypeInfo const &info = lumpTypeInfo[i]; if(!info.name.compareWithoutCase(name) && info.name.length() == name.length()) { return info.type; } } } return UnknownData; } dsize LumpIndex::Id1MapRecognizer::elementSizeForDataType(Format mapFormat, DataType dataType) // static { dsize const SIZEOF_64VERTEX = (4 * 2); dsize const SIZEOF_VERTEX = (2 * 2); dsize const SIZEOF_SIDEDEF = (2 * 3 + 8 * 3); dsize const SIZEOF_64SIDEDEF = (2 * 6); dsize const SIZEOF_LINEDEF = (2 * 7); dsize const SIZEOF_64LINEDEF = (2 * 6 + 1 * 4); dsize const SIZEOF_XLINEDEF = (2 * 5 + 1 * 6); dsize const SIZEOF_SECTOR = (2 * 5 + 8 * 2); dsize const SIZEOF_64SECTOR = (2 * 12); dsize const SIZEOF_THING = (2 * 5); dsize const SIZEOF_64THING = (2 * 7); dsize const SIZEOF_XTHING = (2 * 7 + 1 * 6); dsize const SIZEOF_LIGHT = (1 * 6); switch(dataType) { default: return 0; case VertexData: return (mapFormat == Doom64Format? SIZEOF_64VERTEX : SIZEOF_VERTEX); case LineDefData: return (mapFormat == Doom64Format? SIZEOF_64LINEDEF : mapFormat == HexenFormat ? SIZEOF_XLINEDEF : SIZEOF_LINEDEF); case SideDefData: return (mapFormat == Doom64Format? SIZEOF_64SIDEDEF : SIZEOF_SIDEDEF); case SectorDefData: return (mapFormat == Doom64Format? SIZEOF_64SECTOR : SIZEOF_SECTOR); case ThingData: return (mapFormat == Doom64Format? SIZEOF_64THING : mapFormat == HexenFormat ? SIZEOF_XTHING : SIZEOF_THING); case TintColorData: return SIZEOF_LIGHT; } } DENG2_PIMPL(LumpIndex) { bool pathsAreUnique; Lumps lumps; bool needPruneDuplicateLumps; /// Stores indexes into records forming a chain of PathTree::Node fragment /// hashes. For ultra-fast lookup by path. struct PathHashRecord { lumpnum_t head, nextInLoadOrder; }; typedef QVector PathHash; QScopedPointer lumpsByPath; Instance(Public *i) : Base(i) , pathsAreUnique (false) , needPruneDuplicateLumps(false) {} ~Instance() { self.clear(); } void buildLumpsByPathIfNeeded() { if(!lumpsByPath.isNull()) return; int const numElements = lumps.size(); lumpsByPath.reset(new PathHash(numElements)); // Clear the chains. DENG2_FOR_EACH(PathHash, i, *lumpsByPath) { i->head = -1; } // Prepend nodes to each chain, in first-to-last load order, so that // the last lump with a given name appears first in the chain. for(int i = 0; i < numElements; ++i) { File1 const &lump = *(lumps[i]); PathTree::Node const &node = lump.directoryNode(); ushort k = node.hash() % (unsigned)numElements; (*lumpsByPath)[i].nextInLoadOrder = (*lumpsByPath)[k].head; (*lumpsByPath)[k].head = i; } LOG_RES_XVERBOSE("Rebuilt hashMap for LumpIndex %p") << &self; } /** * @param pruneFlags Passed by reference to avoid deep copy on value-write. * @param file Flag only those lumps contained by this file. * * @return Number of lumps newly flagged during this op. */ int flagContainedLumps(QBitArray &pruneFlags, File1 &file) { DENG2_ASSERT(pruneFlags.size() == lumps.size()); int const numRecords = lumps.size(); int numFlagged = 0; for(int i = 0; i < numRecords; ++i) { File1 *lump = lumps[i]; if(pruneFlags.testBit(i)) continue; if(!lump->isContained() || &lump->container() != &file) continue; pruneFlags.setBit(i, true); numFlagged += 1; } return numFlagged; } /** * @param pruneFlags Passed by reference to avoid deep copy on value-write. * @return Number of lumps newly flagged during this op. */ int flagDuplicateLumps(QBitArray &pruneFlags) { DENG2_ASSERT(pruneFlags.size() == lumps.size()); // Any work to do? if(!pathsAreUnique) return 0; if(!needPruneDuplicateLumps) return 0; int const numRecords = lumps.size(); if(numRecords <= 1) return 0; // Sort in descending load order for pruning. LumpSortInfo *sortInfos = new LumpSortInfo[numRecords]; for(int i = 0; i < numRecords; ++i) { LumpSortInfo &sortInfo = sortInfos[i]; File1 const *lump = lumps[i]; sortInfo.lump = lump; sortInfo.path = lump->composePath(); sortInfo.origIndex = i; } qsort(sortInfos, numRecords, sizeof(*sortInfos), lumpSorter); // Flag the lumps we'll be pruning. int numFlagged = 0; for(int i = 1; i < numRecords; ++i) { if(pruneFlags.testBit(i)) continue; if(sortInfos[i - 1].path.compare(sortInfos[i].path, Qt::CaseInsensitive)) continue; pruneFlags.setBit(sortInfos[i].origIndex, true); numFlagged += 1; } // We're done with the sort info. delete[] sortInfos; return numFlagged; } /// @return Number of pruned lumps. int pruneFlaggedLumps(QBitArray flaggedLumps) { DENG2_ASSERT(flaggedLumps.size() == lumps.size()); // Have we lumps to prune? int const numFlaggedForPrune = flaggedLumps.count(true); if(numFlaggedForPrune) { // We'll need to rebuild the hash after this. lumpsByPath.reset(); int numRecords = lumps.size(); if(numRecords == numFlaggedForPrune) { lumps.clear(); } else { // Do this one lump at a time, respecting the possibly-sorted order. for(int i = 0, newIdx = 0; i < numRecords; ++i) { if(!flaggedLumps.testBit(i)) { ++newIdx; continue; } // Move the info for the lump to be pruned to the end. lumps.move(newIdx, lumps.size() - 1); } // Erase the pruned lumps from the end of the list. int firstPruned = lumps.size() - numFlaggedForPrune; lumps.erase(lumps.begin() + firstPruned, lumps.end()); } } return numFlaggedForPrune; } void pruneDuplicatesIfNeeded() { if(!needPruneDuplicateLumps) return; needPruneDuplicateLumps = false; int const numRecords = lumps.size(); if(numRecords <= 1) return; QBitArray pruneFlags(numRecords); flagDuplicateLumps(pruneFlags); pruneFlaggedLumps(pruneFlags); } }; LumpIndex::LumpIndex(bool pathsAreUnique) : d(new Instance(this)) { d->pathsAreUnique = pathsAreUnique; } LumpIndex::~LumpIndex() {} bool LumpIndex::hasLump(lumpnum_t lumpNum) const { d->pruneDuplicatesIfNeeded(); return (lumpNum >= 0 && lumpNum < d->lumps.size()); } static String invalidIndexMessage(int invalidIdx, int lastValidIdx) { String msg = String("Invalid lump index %1").arg(invalidIdx); if(lastValidIdx < 0) msg += " (file is empty)"; else msg += String(", valid range: [0..%2)").arg(lastValidIdx); return msg; } File1 &LumpIndex::lump(lumpnum_t lumpNum) const { if(!hasLump(lumpNum)) throw NotFoundError("LumpIndex::lump", invalidIndexMessage(lumpNum, size() - 1)); return *d->lumps[lumpNum]; } LumpIndex::Lumps const &LumpIndex::allLumps() const { d->pruneDuplicatesIfNeeded(); return d->lumps; } int LumpIndex::size() const { d->pruneDuplicatesIfNeeded(); return d->lumps.size(); } int LumpIndex::lastIndex() const { return d->lumps.size() - 1; } int LumpIndex::pruneByFile(File1 &file) { if(d->lumps.empty()) return 0; int const numRecords = d->lumps.size(); QBitArray pruneFlags(numRecords); // We may need to prune path-duplicate lumps. We'll fold those into this // op as pruning may result in reallocations. d->flagDuplicateLumps(pruneFlags); // Flag the lumps we'll be pruning. int numFlaggedForFile = d->flagContainedLumps(pruneFlags, file); // Perform the prune. d->pruneFlaggedLumps(pruneFlags); d->needPruneDuplicateLumps = false; return numFlaggedForFile; } bool LumpIndex::pruneLump(File1 &lump) { if(d->lumps.empty()) return 0; d->pruneDuplicatesIfNeeded(); // Prune this lump. if(!d->lumps.removeOne(&lump)) return false; // We'll need to rebuild the path hash chains. d->lumpsByPath.reset(); return true; } void LumpIndex::catalogLump(File1 &lump) { d->lumps.push_back(&lump); d->lumpsByPath.reset(); // We'll need to rebuild the path hash chains. if(d->pathsAreUnique) { // We may need to prune duplicate paths. d->needPruneDuplicateLumps = true; } } void LumpIndex::clear() { d->lumps.clear(); d->lumpsByPath.reset(); d->needPruneDuplicateLumps = false; } bool LumpIndex::catalogues(File1 &file) { d->pruneDuplicatesIfNeeded(); DENG2_FOR_EACH(Lumps, i, d->lumps) { File1 const &lump = **i; if(&lump.container() == &file) return true; } return false; } bool LumpIndex::contains(Path const &path) const { return findFirst(path) >= 0; } int LumpIndex::findAll(Path const &path, FoundIndices &found) const { LOG_AS("LumpIndex::findAll"); found.clear(); if(path.isEmpty() || d->lumps.empty()) return 0; d->pruneDuplicatesIfNeeded(); d->buildLumpsByPathIfNeeded(); // Perform the search. DENG2_ASSERT(!d->lumpsByPath.isNull()); ushort hash = path.lastSegment().hash() % d->lumpsByPath->size(); for(int idx = (*d->lumpsByPath)[hash].head; idx != -1; idx = (*d->lumpsByPath)[idx].nextInLoadOrder) { File1 const &lump = *d->lumps[idx]; PathTree::Node const &node = lump.directoryNode(); if(!node.comparePath(path, 0)) { found.push_front(idx); } } return int(found.size()); } lumpnum_t LumpIndex::findLast(Path const &path) const { if(path.isEmpty() || d->lumps.empty()) return -1; d->pruneDuplicatesIfNeeded(); d->buildLumpsByPathIfNeeded(); // Perform the search. DENG2_ASSERT(!d->lumpsByPath.isNull()); ushort hash = path.lastSegment().hash() % d->lumpsByPath->size(); for(int idx = (*d->lumpsByPath)[hash].head; idx != -1; idx = (*d->lumpsByPath)[idx].nextInLoadOrder) { File1 const &lump = *d->lumps[idx]; PathTree::Node const &node = lump.directoryNode(); if(!node.comparePath(path, 0)) { return idx; // This is the lump we are looking for. } } return -1; // Not found. } lumpnum_t LumpIndex::findFirst(Path const &path) const { if(path.isEmpty() || d->lumps.empty()) return -1; d->pruneDuplicatesIfNeeded(); d->buildLumpsByPathIfNeeded(); lumpnum_t earliest = -1; // Not found. // Perform the search. DENG2_ASSERT(!d->lumpsByPath.isNull()); ushort hash = path.lastSegment().hash() % d->lumpsByPath->size(); for(int idx = (*d->lumpsByPath)[hash].head; idx != -1; idx = (*d->lumpsByPath)[idx].nextInLoadOrder) { File1 const &lump = *d->lumps[idx]; PathTree::Node const &node = lump.directoryNode(); if(!node.comparePath(path, 0)) { earliest = idx; // This is now the first lump loaded. } } return earliest; } } // namespace de doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/filetype.cpp0000664000175000017500000000325512641367671025121 0ustar jaakkojaakko/** @file * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/filesys/filetype.h" using namespace de; static NullFileType nullFileType; /// A symbolic name => file type map. static FileTypes fileTypeMap; void DD_AddFileType(FileType const &ftype) { fileTypeMap.insert(ftype.name().toLower(), &ftype); } FileType const &DD_FileTypeByName(String name) { if(!name.isEmpty()) { FileTypes::const_iterator found = fileTypeMap.constFind(name.toLower()); if(found != fileTypeMap.constEnd()) return **found; } return nullFileType; // Not found. } FileType const &DD_GuessFileTypeFromFileName(String path) { if(!path.isEmpty()) { DENG2_FOR_EACH_CONST(FileTypes, i, fileTypeMap) { FileType const &ftype = **i; if(ftype.fileNameIsKnown(path)) return ftype; } } return nullFileType; } FileTypes &DD_FileTypes() { return fileTypeMap; } doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/file.cpp0000664000175000017500000000651212641367671024216 0ustar jaakkojaakko/** @file file.cpp * * Abstract base for all classes which represent loaded files. * @ingroup fs * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/filesys/file.h" #include "doomsday/filesys/fs_main.h" #include namespace de { File1::File1(FileHandle &hndl, String _path, FileInfo const &_info, File1 *_container) : handle_(&hndl) , info_(_info) , container_(_container) , flags(DefaultFlags) , path_(_path) , name_(_path.fileName()) { // Used to favor newer files when duplicates are pruned. /// @todo Does not belong at this level. Load order should be determined /// at file system level. -ds static uint fileCounter = 0; order = fileCounter++; } File1::~File1() { App_FileSystem().releaseFile(*this); if(handle_) delete handle_; } FileInfo const &File1::info() const { return info_; } bool File1::isContained() const { return !!container_; } File1 &File1::container() const { if(!container_) throw NotContainedError("File1::container", "File \"" + NativePath(composePath()).pretty() + " is not contained"); return *container_; } FileHandle &File1::handle() { return *handle_; } Uri File1::composeUri(QChar delimiter) const { return Uri(path_, RC_NULL, delimiter); } uint File1::loadOrderIndex() const { return order; } bool File1::hasStartup() const { return flags.testFlag(Startup); } File1 &File1::setStartup(bool yes) { if(yes) flags |= Startup; else flags &= ~Startup; return *this; } bool File1::hasCustom() const { return flags.testFlag(Custom); } File1 &File1::setCustom(bool yes) { if(yes) flags |= Custom; else flags &= ~Custom; return *this; } String const &File1::name() const { return name_; } size_t File1::read(uint8_t* /*buffer*/, bool /*tryCache*/) { /// @todo writeme throw Error("File1::read", "Not yet implemented"); } size_t File1::read(uint8_t* /*buffer*/, size_t /*startOffset*/, size_t /*length*/, bool /*tryCache*/) { /// @todo writeme throw Error("File1::read", "Not yet implemented"); } uint8_t const *File1::cache() { /// @todo writeme throw Error("File1::cache", "Not yet implemented"); } File1& File1::unlock() { /// @todo writeme throw Error("File1::unlock", "Not yet implemented"); } File1 &File1::clearCache(bool* /*retCleared*/) { /// @todo writeme throw Error("File1::clearCache", "Not yet implemented"); } } // namespace de doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/fs_scheme.cpp0000664000175000017500000003551112641367671025234 0ustar jaakkojaakko/** @file fs_scheme.cpp File System Subspace Scheme. * @ingroup fs * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include #include #include #include #include "doomsday/filesys/fs_main.h" #include "doomsday/filesys/searchpath.h" namespace de { /** * Reference to a file in the virtual file system. */ class FileRef { public: FileRef(PathTree::Node &_directoryNode) : directoryNode_(&_directoryNode) {} PathTree::Node &directoryNode() const { return *directoryNode_; } FileRef &setDirectoryNode(PathTree::Node &newDirectoryNode) { directoryNode_ = &newDirectoryNode; return *this; } #if _DEBUG String const &name() const { return name_; } FileRef &setName(String const &newName) { name_ = newName; return *this; } #endif private: /// Directory node for this resource in the owning PathTree PathTree::Node *directoryNode_; #if _DEBUG /// Symbolic name of this resource. String name_; #endif }; /** * Name search hash. */ struct NameHash { public: /// Type used to represent hash keys. typedef ushort hash_type; /// Number of buckets in the hash table. static hash_type const hash_range = 512; struct Node { Node *next; FileRef fileRef; Node(PathTree::Node &resourceNode) : next(0), fileRef(resourceNode) {} }; struct Bucket { Node *first; Node *last; }; public: Bucket buckets[hash_range]; public: NameHash() { std::memset(buckets, 0, sizeof(buckets)); } ~NameHash() { clear(); } void clear() { for(hash_type hashKey = 0; hashKey < hash_range; ++hashKey) { while(buckets[hashKey].first) { NameHash::Node *nextNode = buckets[hashKey].first->next; delete buckets[hashKey].first; buckets[hashKey].first = nextNode; } buckets[hashKey].last = 0; } } Node *findDirectoryNode(hash_type hashKey, PathTree::Node const &directoryNode) { Node *node = buckets[hashKey].first; while(node && &node->fileRef.directoryNode() != &directoryNode) { node = node->next; } return node; } static hash_type hashName(String const &str) { hash_type hashKey = 0; int op = 0; for(int i = 0; i < str.length(); ++i) { ushort unicode = str.at(i).toLower().unicode(); switch(op) { case 0: hashKey ^= unicode; ++op; break; case 1: hashKey *= unicode; ++op; break; case 2: hashKey -= unicode; op=0; break; } } return hashKey % hash_range; } }; struct FS1::Scheme::Instance { FS1::Scheme &self; /// Symbolic name. String name; /// Flags which govern behavior. FS1::Scheme::Flags flags; /// Associated path directory. /// @todo It should not be necessary for a unique directory per scheme. UserDataPathTree directory; /// As the directory is relative, this special node serves as the root. UserDataNode *rootNode; /// Name hash table. NameHash nameHash; /// Set to @c true when the name hash is obsolete/out-of-date and should be rebuilt. byte nameHashIsDirty; /// Sets of search paths to look for files to be included. /// Each set is in order of greatest-importance, right to left. FS1::Scheme::SearchPaths searchPaths; Instance(FS1::Scheme &d, String _name, FS1::Scheme::Flags _flags) : self(d), name(_name), flags(_flags), directory(), rootNode(0), nameHash(), nameHashIsDirty(true) {} /** * Add files to this scheme by resolving search path, searching the * file system and populating our internal directory with the results. * Duplicates are automatically pruned. * * @param searchPath The path to resolve and search. */ void addFromSearchPath(SearchPath const &searchPath) { try { // Add new nodes on this path and/or re-process previously seen nodes. addDirectoryPathAndMaybeDescendBranch(true/*do descend*/, searchPath.resolved(), true/*is-directory*/, searchPath.flags()); } catch(de::Uri::ResolveError const &er) { LOGDEV_RES_VERBOSE(er.asText()); } } /** * Add files to this scheme by resolving each search path in @a group. * * @param group Group of paths to search. */ void addFromSearchPaths(FS1::PathGroup group) { for(FS1::Scheme::SearchPaths::const_iterator i = searchPaths.find(group); i != searchPaths.end() && i.key() == group; ++i) { addFromSearchPath(*i); } } UserDataNode *addDirectoryPath(String path) { if(path.isEmpty()) return 0; // Try to make it a relative path. if(QDir::isAbsolutePath(path)) { String const basePath = App_BasePath(); if(path.beginsWith(basePath)) { path = path.mid(basePath.length() + 1); } } // If this is equal to the base path, return that node. if(path.isEmpty()) { // Time to construct the relative base node? if(!rootNode) { rootNode = &directory.insert(Path("./")); } return rootNode; } return &directory.insert(path); } void addDirectoryChildren(PathTree::Node &node, int flags) { if(node.isLeaf()) return; // Compose the search pattern. We're interested in *everything*. Path searchPattern = node.path() / "*"; // Process this search. FS1::PathList found; App_FileSystem().findAllPaths(searchPattern, flags, found); DENG2_FOR_EACH_CONST(FS1::PathList, i, found) { addDirectoryPathAndMaybeDescendBranch(!(flags & SearchPath::NoDescend), i->path, !!(i->attrib & A_SUBDIR), flags); } } /** * @param descendBranch @c true = descend this branch (if it is a branch). * @param filePath Possibly-relative path to an element in the virtual file system. * param isFolder @c true = @a filePath is a folder in the virtual file system. * @param flags @ref searchPathFlags */ void addDirectoryPathAndMaybeDescendBranch(bool descendBranch, String filePath, bool /*isFolder*/, int flags) { // Add this path to the directory. UserDataNode *node = addDirectoryPath(filePath); if(!node) return; if(!node->isLeaf()) { // Descend into this subdirectory? if(descendBranch) { // Already processed? if(node->userValue()) { // Process it again? DENG2_FOR_EACH_CONST(PathTree::Nodes, i, directory.leafNodes()) { PathTree::Node &sibling = **i; if(&sibling.parent() == node) { self.add(sibling); } } } else { addDirectoryChildren(*node, flags); // This node is now considered processed. node->setUserValue(true); } } } // Node is a leaf. else { self.add(*node); // This node is now considered processed (if it wasn't already). node->setUserValue(true); } } }; FS1::Scheme::Scheme(String symbolicName, Flags flags) { d = new Instance(*this, symbolicName, flags); } FS1::Scheme::~Scheme() { delete d; } String const &FS1::Scheme::name() const { return d->name; } void FS1::Scheme::clear() { d->nameHash.clear(); d->nameHashIsDirty = true; d->directory.clear(); d->rootNode = 0; } void FS1::Scheme::rebuild() { // Is a rebuild not necessary? if(!d->nameHashIsDirty) return; LOG_AS("Scheme::rebuild"); LOGDEV_RES_MSG("Rebuilding '%s'...") << d->name; Time begunAt; // (Re)populate the directory and add found files. clear(); d->addFromSearchPaths(FS1::OverridePaths); d->addFromSearchPaths(FS1::ExtraPaths); d->addFromSearchPaths(FS1::DefaultPaths); d->addFromSearchPaths(FS1::FallbackPaths); d->nameHashIsDirty = false; LOGDEV_RES_VERBOSE("Completed in %.2f seconds") << begunAt.since(); /*#if _DEBUG PathTree::debugPrint(d->directory); debugPrint(); #endif*/ } static inline String composeSchemeName(String const &filePath) { return filePath.fileNameWithoutExtension(); } bool FS1::Scheme::add(PathTree::Node &resourceNode) { // We are only interested in leafs (i.e., files and not folders). if(!resourceNode.isLeaf()) return false; String name = composeSchemeName(resourceNode.name()); NameHash::hash_type hashKey = NameHash::hashName(name); // Is this a new file? bool isNewNode = false; NameHash::Node *hashNode = d->nameHash.findDirectoryNode(hashKey, resourceNode); if(!hashNode) { isNewNode = true; // Create a new hash node for this. hashNode = new NameHash::Node(resourceNode); #if _DEBUG // Take a copy of the name to aid in tracing bugs, etc... hashNode->fileRef.setName(name); #endif // Link it to the list for this bucket. NameHash::Bucket &bucket = d->nameHash.buckets[hashKey]; if(bucket.last) bucket.last->next = hashNode; bucket.last = hashNode; if(!bucket.first) bucket.first = hashNode; // We will need to rebuild this scheme (if we aren't already doing so, // in the case of auto-populated schemes built from FileDirectorys). d->nameHashIsDirty = true; } // (Re)configure this record. FileRef *fileRef = &hashNode->fileRef; fileRef->setDirectoryNode(resourceNode); return isNewNode; } static String const &nameForPathGroup(FS1::PathGroup group) { static String const names[] = { "Override", "Extra", "Default", "Fallback" }; DENG_ASSERT(int(group) >= FS1::OverridePaths && int(group) <= FS1::FallbackPaths); return names[int(group)]; } bool FS1::Scheme::addSearchPath(SearchPath const &search, FS1::PathGroup group) { LOG_AS("Scheme::addSearchPath"); // Ensure this is a well formed path. if(search.isEmpty() || !search.path().toString().compareWithoutCase("/") || !search.path().toString().endsWith("/")) return false; // The addition of a new search path means the scheme is now dirty. d->nameHashIsDirty = true; // Have we seen this path already (we don't want duplicates)? DENG2_FOR_EACH(SearchPaths, i, d->searchPaths) { // Compare using the unresolved textual representations. if(!i->asText().compareWithoutCase(search.asText())) { i->setFlags(search.flags()); return true; } } // Prepend to the path list - newer paths have priority. d->searchPaths.insert(group, search); LOGDEV_RES_MSG("\"%s\" added to scheme '%s' (group:%s)") << search << name() << nameForPathGroup(group); return true; } void FS1::Scheme::clearSearchPathGroup(FS1::PathGroup group) { d->searchPaths.remove(group); } void FS1::Scheme::clearAllSearchPaths() { d->searchPaths.clear(); } FS1::Scheme::SearchPaths const &FS1::Scheme::allSearchPaths() const { return d->searchPaths; } int FS1::Scheme::findAll(String name, FoundNodes &found) { int numFoundSoFar = found.count(); NameHash::hash_type fromKey, toKey; if(!name.isEmpty()) { fromKey = NameHash::hashName(name); toKey = fromKey; } else { fromKey = 0; toKey = NameHash::hash_range - 1; } for(NameHash::hash_type key = fromKey; key < toKey + 1; ++key) { NameHash::Bucket &bucket = d->nameHash.buckets[key]; for(NameHash::Node *hashNode = bucket.first; hashNode; hashNode = hashNode->next) { FileRef &fileRef = hashNode->fileRef; PathTree::Node &node = fileRef.directoryNode(); if(!name.isEmpty() && !node.name().beginsWith(name, Qt::CaseInsensitive)) continue; found.push_back(&node); } } return found.count() - numFoundSoFar; } bool FS1::Scheme::mapPath(String &path) const { if(path.isEmpty()) return false; // Are virtual path mappings in effect for this scheme? if(!(d->flags & MappedInPackages)) return false; // Does this path qualify for mapping? if(path.length() <= name().length()) return false; if(path.at(name().length()) != '/') return false; if(!path.beginsWith(name(), Qt::CaseInsensitive)) return false; // Yes. path = String("$(App.DataPath)/$(GamePlugin.Name)") / path; return true; } #ifdef DENG_DEBUG void FS1::Scheme::debugPrint() const { LOG_AS("Scheme::debugPrint"); LOGDEV_RES_MSG("%p:") << this; uint schemeIdx = 0; for(NameHash::hash_type key = 0; key < NameHash::hash_range; ++key) { NameHash::Bucket &bucket = d->nameHash.buckets[key]; for(NameHash::Node *node = bucket.first; node; node = node->next) { FileRef const &fileRef = node->fileRef; LOGDEV_RES_MSG(" %u - %u:\"%s\" => %s") << schemeIdx << key << fileRef.name() << NativePath(fileRef.directoryNode().path()).pretty(); ++schemeIdx; } } LOGDEV_RES_MSG(" %u %s in scheme.") << schemeIdx << (schemeIdx == 1? "file" : "files"); } #endif } // namespace de doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/fs_util.cpp0000664000175000017500000003053512641367671024746 0ustar jaakkojaakko/** @file fs_util.cpp * * Miscellaneous file system utility routines. * * @ingroup fs * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #define DENG_NO_API_MACROS_FILESYS #ifdef WIN32 # include # include #endif #ifdef UNIX # include # include # include # include #endif #include #include #include #include "doomsday/filesys/fs_main.h" #include "doomsday/filesys/fs_util.h" #include "doomsday/filesys/sys_direc.h" #include "doomsday/filesys/lumpindex.h" #include "doomsday/paths.h" #include #include #include using namespace de; int F_Access(char const *nativePath) { de::Uri path = de::Uri::fromNativePath(nativePath); return App_FileSystem().accessFile(path)? 1 : 0; } int F_FileExists(char const *path) { int result = -1; if(path && path[0]) { ddstring_t buf; // Normalize the path into one we can process. Str_Init(&buf); Str_Set(&buf, path); Str_Strip(&buf); F_ExpandBasePath(&buf, &buf); F_ToNativeSlashes(&buf, &buf); result = !access(Str_Text(&buf), 4); // Read permission? Str_Free(&buf); } return result; } uint F_GetLastModified(char const *path) { #ifdef UNIX struct stat s; stat(path, &s); return s.st_mtime; #endif #ifdef WIN32 struct _stat s; _stat(path, &s); return s.st_mtime; #endif } dd_bool F_MakePath(char const *path) { #if !defined(WIN32) && !defined(UNIX) # error F_MakePath has no implementation for this platform. #endif ddstring_t full, buf; char* ptr, *endptr; dd_bool result; // Convert all backslashes to normal slashes. Str_Init(&full); Str_Set(&full, path); Str_Strip(&full); F_ToNativeSlashes(&full, &full); // Does this path already exist? if(0 == access(Str_Text(&full), 0)) { Str_Free(&full); return true; } // Check and create the path in segments. ptr = Str_Text(&full); Str_Init(&buf); do { endptr = strchr(ptr, DENG_DIR_SEP_CHAR); if(!endptr) Str_Append(&buf, ptr); else Str_PartAppend(&buf, ptr, 0, endptr - ptr); if(0 != access(Str_Text(&buf), 0)) { // Path doesn't exist, create it. #ifdef WIN32 mkdir(Str_Text(&buf)); #elif UNIX mkdir(Str_Text(&buf), 0775); #endif } Str_AppendChar(&buf, DENG_DIR_SEP_CHAR); ptr = endptr + 1; } while(NULL != endptr); result = (0 == access(Str_Text(&full), 0)); Str_Free(&buf); Str_Free(&full); return result; } dd_bool F_FixSlashes(ddstring_t* dstStr, const ddstring_t* srcStr) { dd_bool result = false; assert(dstStr && srcStr); if(!Str_IsEmpty(srcStr)) { char* dst = Str_Text(dstStr); const char* src = Str_Text(srcStr); size_t i; if(dstStr != srcStr) { Str_Clear(dstStr); Str_Reserve(dstStr, Str_Length(srcStr)); } for(i = 0; src[i]; ++i) { if(src[i] != '\\') { if(dstStr != srcStr) Str_AppendChar(dstStr, src[i]); continue; } if(dstStr != srcStr) Str_AppendChar(dstStr, '/'); else dst[i] = '/'; result = true; } } return result; } /** * Appends a slash at the end of @a pathStr if there isn't one. * @return @c true if a slash was appended, @c false otherwise. */ #ifdef UNIX static bool F_AppendMissingSlash(ddstring_t *pathStr) { if(Str_RAt(pathStr, 0) != '/') { Str_AppendChar(pathStr, '/'); return true; } return false; } #endif dd_bool F_AppendMissingSlashCString(char* path, size_t maxLen) { if(path[strlen(path) - 1] != '/') { M_StrCat(path, "/", maxLen); return true; } return false; } dd_bool F_ToNativeSlashes(ddstring_t* dstStr, const ddstring_t* srcStr) { dd_bool result = false; assert(dstStr && srcStr); if(!Str_IsEmpty(srcStr)) { char* dst = Str_Text(dstStr); const char* src = Str_Text(srcStr); size_t i; if(dstStr != srcStr) { Str_Clear(dstStr); Str_Reserve(dstStr, Str_Length(srcStr)); } for(i = 0; src[i]; ++i) { if(src[i] != DENG_DIR_WRONG_SEP_CHAR) { if(dstStr != srcStr) Str_AppendChar(dstStr, src[i]); continue; } if(dstStr != srcStr) Str_AppendChar(dstStr, DENG_DIR_SEP_CHAR); else dst[i] = DENG_DIR_SEP_CHAR; result = true; } } return result; } /** * @return @c true iff the path can be made into a relative path. */ static bool F_IsRelativeToBase(char const *path, char const *base) { DENG_ASSERT(path != 0 && base != 0); return !qstrnicmp(path, base, strlen(base)); } /** * Attempt to remove the base path if found at the beginning of the path. * * @param dst Potential base-relative path written here. * @param src Possibly absolute path. * * @return @c true iff the base path was found and removed. */ static dd_bool F_RemoveBasePath(ddstring_t *dst, ddstring_t const *absPath) { DENG_ASSERT(dst != 0 && absPath != 0); ddstring_t basePath; Str_InitStatic(&basePath, DD_BasePath()); if(F_IsRelativeToBase(Str_Text(absPath), Str_Text(&basePath))) { dd_bool mustCopy = (dst == absPath); if(mustCopy) { ddstring_t buf; Str_Init(&buf); Str_PartAppend(&buf, Str_Text(absPath), Str_Length(&basePath), Str_Length(absPath) - Str_Length(&basePath)); Str_Set(dst, Str_Text(&buf)); Str_Free(&buf); return true; } Str_Clear(dst); Str_PartAppend(dst, Str_Text(absPath), Str_Length(&basePath), Str_Length(absPath) - Str_Length(&basePath)); return true; } // Do we need to copy anyway? if(dst != absPath) { Str_Set(dst, Str_Text(absPath)); } // This doesn't appear to be the base path. return false; } dd_bool F_IsAbsolute(const ddstring_t* str) { if(!str) return false; /// @todo Should not handle both separators - refactor callers. if(Str_At(str, 0) == DENG_DIR_SEP_CHAR || Str_At(str, 0) == DENG_DIR_WRONG_SEP_CHAR || Str_At(str, 1) == ':') return true; #ifdef UNIX if(Str_At(str, 0) == '~') return true; #endif return false; } dd_bool F_ExpandBasePath(ddstring_t* dst, const ddstring_t* src) { assert(dst && src); if(Str_At(src, 0) == '>' || Str_At(src, 0) == '}') { dd_bool mustCopy = (dst == src); if(mustCopy) { ddstring_t buf; Str_Init(&buf); Str_Set(&buf, DD_BasePath()); Str_PartAppend(&buf, Str_Text(src), 1, Str_Length(src)-1); Str_Set(dst, Str_Text(&buf)); Str_Free(&buf); return true; } Str_Set(dst, DD_BasePath()); Str_PartAppend(dst, Str_Text(src), 1, Str_Length(src)-1); return true; } #ifdef UNIX else if(Str_At(src, 0) == '~') { if(Str_At(src, 1) == '/' && getenv("HOME")) { // Replace it with the HOME environment variable. ddstring_t buf; ddstring_t homeStr; Str_Init(&buf); Str_Init(&homeStr); Str_Set(&homeStr, getenv("HOME")); F_FixSlashes(&buf, &homeStr); F_AppendMissingSlash(&buf); // Append the rest of the original path. Str_PartAppend(&buf, Str_Text(src), 2, Str_Length(src)-2); Str_Set(dst, Str_Text(&buf)); Str_Free(&buf); Str_Free(&homeStr); return true; } // Parse the user's home directory (from passwd). { dd_bool result = false; ddstring_t userName; const char* p = Str_Text(src)+2; Str_Init(&userName); if((p = Str_CopyDelim2(&userName, p, '/', CDF_OMIT_DELIMITER))) { ddstring_t buf; struct passwd* pw; Str_Init(&buf); if((pw = getpwnam(Str_Text(&userName))) != NULL) { ddstring_t pwStr; Str_Init(&pwStr); Str_Set(&pwStr, pw->pw_dir); F_FixSlashes(&buf, &pwStr); F_AppendMissingSlash(&buf); result = true; Str_Free(&pwStr); } Str_Append(&buf, Str_Text(src) + 1); Str_Set(dst, Str_Text(&buf)); Str_Free(&buf); } Str_Free(&userName); if(result) return result; } } #endif // Do we need to copy anyway? if(dst != src) Str_Set(dst, Str_Text(src)); // No expansion done. return false; } /// @return @c true if @a path begins with a known directive. static dd_bool pathHasDirective(const char* path) { if(NULL != path && path[0]) { #ifdef UNIX if('~' == path[0]) return true; #endif if('}' == path[0] || '>' == path[0]) return true; } return false; } const char* F_PrettyPath(const char* path) { #define NUM_BUFS 8 static ddstring_t buffers[NUM_BUFS]; // @todo: never free'd! static uint index = 0; ddstring_t* buf = NULL; int len; if(!path || 0 == (len = (int)strlen(path))) return path; // Hide relative directives like '}' if(len > 1 && pathHasDirective(path)) { buf = &buffers[index++ % NUM_BUFS]; Str_Clear(buf); Str_PartAppend(buf, path, 1, len-1); path = Str_Text(buf); } // If within our the base directory cut out the base path. if(F_IsRelativeToBase(path, DD_BasePath())) { if(!buf) { buf = &buffers[index++ % NUM_BUFS]; Str_Set(buf, path); } F_RemoveBasePath(buf, buf); path = Str_Text(buf); } // Swap directory separators with their system-specific version. if(strchr(path, DENG_DIR_WRONG_SEP_CHAR)) { int i; if(!buf) { buf = &buffers[index++ % NUM_BUFS]; Str_Set(buf, path); } for(i = 0; i < len; ++i) { if(buf->str[i] == DENG_DIR_WRONG_SEP_CHAR) buf->str[i] = DENG_DIR_SEP_CHAR; } path = Str_Text(buf); } return path; #undef NUM_BUFS } dd_bool F_Dump(void const *data, size_t size, char const *path) { DENG2_ASSERT(data != 0 && path != 0); if(!size) return false; AutoStr *nativePath = AutoStr_NewStd(); Str_Set(nativePath, path); F_ToNativeSlashes(nativePath, nativePath); FILE *outFile = fopen(Str_Text(nativePath), "wb"); if(!outFile) { LOG_RES_WARNING("Failed to open \"%s\" for writing: %s") << F_PrettyPath(Str_Text(nativePath)) << strerror(errno); return false; } fwrite(data, 1, size, outFile); fclose(outFile); return true; } dd_bool F_DumpFile(File1 &file, char const *outputPath) { String dumpPath = ((!outputPath || !outputPath[0])? file.name() : String(outputPath)); QByteArray dumpPathUtf8 = dumpPath.toUtf8(); bool dumpedOk = F_Dump(file.cache(), file.info().size, dumpPathUtf8.constData()); if(dumpedOk) { LOG_RES_VERBOSE("%s dumped to \"%s\"") << file.name() << NativePath(dumpPath).pretty(); } file.unlock(); return dumpedOk; } doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/zip.cpp0000664000175000017500000006453212641367671024107 0ustar jaakkojaakko/** @file zip.cpp ZIP Archive (file). * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/filesys/zip.h" #include "doomsday/filesys/lumpcache.h" #include "doomsday/filesys/fs_main.h" #include #include #include #include #include #include #include #include #include #include // memcpy namespace de { namespace internal { #define SIG_LOCAL_FILE_HEADER 0x04034b50 #define SIG_CENTRAL_FILE_HEADER 0x02014b50 #define SIG_END_OF_CENTRAL_DIR 0x06054b50 // Maximum tolerated size of the comment. /// @todo Define this at Zip-class level. #define MAXIMUM_COMMENT_SIZE 2048 // This is the length of the central directory end record (without the // comment, but with the signature). #define CENTRAL_END_SIZE 22 // File header flags. #define ZFH_ENCRYPTED 0x1 #define ZFH_COMPRESSION_OPTS 0x6 #define ZFH_DESCRIPTOR 0x8 #define ZFH_COMPRESS_PATCHED 0x20 ///< Not supported. // Compression methods. /// @todo Define these at Zip level. enum { ZFC_NO_COMPRESSION = 0, ///< Supported format. ZFC_SHRUNK, ZFC_REDUCED_1, ZFC_REDUCED_2, ZFC_REDUCED_3, ZFC_REDUCED_4, ZFC_IMPLODED, ZFC_DEFLATED = 8, ///< The only supported compression (via zlib). ZFC_DEFLATED_64, ZFC_PKWARE_DCL_IMPLODED }; /// The following structures are used to read data directly from ZIP files. #pragma pack(1) typedef struct localfileheader_s { uint32_t signature; uint16_t requiredVersion; uint16_t flags; uint16_t compression; uint16_t lastModTime; uint16_t lastModDate; uint32_t crc32; uint32_t compressedSize; uint32_t size; uint16_t fileNameSize; uint16_t extraFieldSize; } localfileheader_t; typedef struct descriptor_s { uint32_t crc32; uint32_t compressedSize; uint32_t size; } descriptor_t; typedef struct centralfileheader_s { uint32_t signature; uint16_t version; uint16_t requiredVersion; uint16_t flags; uint16_t compression; uint16_t lastModTime; uint16_t lastModDate; uint32_t crc32; uint32_t compressedSize; uint32_t size; uint16_t fileNameSize; uint16_t extraFieldSize; uint16_t commentSize; uint16_t diskStart; uint16_t internalAttrib; uint32_t externalAttrib; uint32_t relOffset; /* * file name (variable size) * extra field (variable size) * file comment (variable size) */ } centralfileheader_t; /// @todo Do not position the stream here. static bool readArchiveHeader(FileHandle &file, localfileheader_t &hdr) { size_t readBytes, initPos = file.tell(); // Seek to the start of the header. file.seek(0, SeekSet); readBytes = file.read((uint8_t *)&hdr, sizeof(localfileheader_t)); // Return the stream to its original position. file.seek(initPos, SeekSet); if(!(readBytes < sizeof(localfileheader_t))) { hdr.signature = littleEndianByteOrder.toNative(hdr.signature); hdr.requiredVersion = littleEndianByteOrder.toNative(hdr.requiredVersion); hdr.flags = littleEndianByteOrder.toNative(hdr.flags); hdr.compression = littleEndianByteOrder.toNative(hdr.compression); hdr.lastModTime = littleEndianByteOrder.toNative(hdr.lastModTime); hdr.lastModDate = littleEndianByteOrder.toNative(hdr.lastModDate); hdr.crc32 = littleEndianByteOrder.toNative(hdr.crc32); hdr.compressedSize = littleEndianByteOrder.toNative(hdr.compressedSize); hdr.size = littleEndianByteOrder.toNative(hdr.size); hdr.fileNameSize = littleEndianByteOrder.toNative(hdr.fileNameSize); hdr.extraFieldSize = littleEndianByteOrder.toNative(hdr.extraFieldSize); return true; } return false; } typedef struct centralend_s { uint16_t disk; uint16_t centralStartDisk; uint16_t diskEntryCount; uint16_t totalEntryCount; uint32_t size; uint32_t offset; uint16_t commentSize; } centralend_t; #pragma pack() static bool readCentralEnd(FileHandle &file, centralend_t &end) { size_t readBytes = file.read((uint8_t *)&end, sizeof(centralend_t)); if(!(readBytes < sizeof(centralend_t))) { end.disk = littleEndianByteOrder.toNative(end.disk); end.centralStartDisk= littleEndianByteOrder.toNative(end.centralStartDisk); end.diskEntryCount = littleEndianByteOrder.toNative(end.diskEntryCount); end.totalEntryCount = littleEndianByteOrder.toNative(end.totalEntryCount); end.size = littleEndianByteOrder.toNative(end.size); end.offset = littleEndianByteOrder.toNative(end.offset); end.commentSize = littleEndianByteOrder.toNative(end.commentSize); return true; } return false; } static String invalidIndexMessage(int invalidIdx, int lastValidIdx) { String msg = String("Invalid lump index %1").arg(invalidIdx); if(lastValidIdx < 0) msg += " (file is empty)"; else msg += String(", valid range: [0..%2)").arg(lastValidIdx); return msg; } /** * The path inside the zip might be mapped to another virtual location. * * @return @c true= iff @a path was mapped to another location. * * @todo This is clearly implemented in the wrong place. Path mapping * should be done at a higher level. */ static bool applyGamePathMappings(String &path) { // Manually mapped to Defs? if(path.beginsWith('@')) { path.remove(0, 1); if(path.at(0) == '/') path.remove(0, 1); path = String("$(App.DefsPath)/$(GamePlugin.Name)/auto") / path; return true; } // Manually mapped to Data? if(path.beginsWith('#')) { path.remove(0, 1); if(path.at(0) == '/') path.remove(0, 1); // Is there a prefix to be omitted in the name? if(int slash = path.lastIndexOf('/')) { // The slash must not be too early in the string. if(slash >= 2) { // Good old negative indices. if(path.at(slash - 2) == '.' && path.at(slash - 1) >= '1' && path.at(slash - 1) <= '9') path.remove(slash - 2, 2); } } path = String("$(App.DataPath)/$(GamePlugin.Name)/auto") / path; return true; } // Implicitly mapped to another location? if(!path.contains('/')) { // No directory separators; i.e., a root file. FileType const &ftype = DD_GuessFileTypeFromFileName(path.fileName()); switch(ftype.defaultClass()) { case RC_PACKAGE: // Mapped to the Data directory. path = String("$(App.DataPath)/$(GamePlugin.Name)/auto") / path; return true; case RC_DEFINITION: // Mapped to the Defs directory? path = String("$(App.DefsPath)/$(GamePlugin.Name)/auto") / path; return true; default: return false; } } // Key-named directories in the root might be mapped to another location. FS1::Schemes const &schemes = App_FileSystem().allSchemes(); DENG2_FOR_EACH_CONST(FS1::Schemes, i, schemes) { if((*i)->mapPath(path)) { return true; } } return false; } } // namespace internal using namespace internal; Zip::LumpFile::LumpFile(Entry &entry, FileHandle &hndl, String path, FileInfo const &info, File1 *container) : File1(hndl, path, info, container) , entry(entry) {} String const &Zip::LumpFile::name() const { return directoryNode().name(); } Uri Zip::LumpFile::composeUri(QChar delimiter) const { return directoryNode().path(delimiter); } PathTree::Node &Zip::LumpFile::directoryNode() const { return entry; } size_t Zip::LumpFile::read(uint8_t *buffer, bool tryCache) { return zip().readLump(info_.lumpIdx, buffer, tryCache); } size_t Zip::LumpFile::read(uint8_t *buffer, size_t startOffset, size_t length, bool tryCache) { return zip().readLump(info_.lumpIdx, buffer, startOffset, length, tryCache); } uint8_t const *Zip::LumpFile::cache() { return zip().cacheLump(info_.lumpIdx); } Zip::LumpFile &Zip::LumpFile::unlock() { zip().unlockLump(info_.lumpIdx); return *this; } Zip &Zip::LumpFile::zip() const { return container().as(); } DENG2_PIMPL(Zip) { LumpTree entries; ///< Directory structure and entry records for all lumps. QScopedPointer dataCache; ///< Data payload cache. Instance(Public *i) : Base(i) {} /** * @param lump Lump/file to be buffered. * @param buffer Must be large enough to hold the entire uncompressed data lump. */ size_t bufferLump(LumpFile const &lump, uint8_t *buffer) { DENG2_ASSERT(buffer); LOG_AS("Zip"); FileInfo const &lumpInfo = lump.info(); self.handle_->seek(lumpInfo.baseOffset, SeekSet); if(lumpInfo.isCompressed()) { bool result; uint8_t *compressedData = (uint8_t *) M_Malloc(lumpInfo.compressedSize); if(!compressedData) throw Error("Zip::bufferLump", QString("Failed on allocation of %1 bytes for decompression buffer").arg(lumpInfo.compressedSize)); // Read the compressed data into a temporary buffer for decompression. self.handle_->read(compressedData, lumpInfo.compressedSize); // Uncompress into the buffer provided by the caller. result = uncompressRaw(compressedData, lumpInfo.compressedSize, buffer, lumpInfo.size); M_Free(compressedData); if(!result) return 0; // Inflate failed. } else { // Read the uncompressed data directly to the buffer provided by the caller. self.handle_->read(buffer, lumpInfo.size); } return lumpInfo.size; } }; Zip::Zip(FileHandle &hndl, String path, FileInfo const &info, File1 *container) : File1(hndl, path, info, container) , LumpIndex(true/*paths are unique*/) , d(new Instance(this)) { // Scan the end of the file for the central directory end record. /// @note: This gets awfully slow if the comment is long. bool foundCentralDirectory = false; // Start from the earliest location where the signature might be. { int pos = CENTRAL_END_SIZE; // Offset from the end. uint32_t signature; while(!foundCentralDirectory && pos < MAXIMUM_COMMENT_SIZE) { handle_->seek(-pos, SeekEnd); // Is this the signature? handle_->read((uint8_t *)&signature, sizeof(signature)); if(littleEndianByteOrder.toNative(signature) == SIG_END_OF_CENTRAL_DIR) { foundCentralDirectory = true; // Yes, this is it. } else { // Move backwards. pos++; } } } if(!foundCentralDirectory) throw FormatError("Zip", "Central directory in \"" + NativePath(composePath()).pretty() + "\" not found"); // Read the central directory end record. centralend_t summary; readCentralEnd(*handle_, summary); // Does the summary say something we don't like? if(summary.diskEntryCount != summary.totalEntryCount) throw FormatError("Zip", "Multipart zip file \"" + NativePath(composePath()).pretty() + "\" not supported"); // We'll load the file directory using one continous read into a temporary // local buffer before we process it into our runtime representation. // Read the entire central directory into memory. void *centralDirectory = M_Malloc(summary.size); if(!centralDirectory) throw FormatError("Zip", String("Failed on allocation of %1 bytes for temporary copy of the central centralDirectory").arg(summary.size)); handle_->seek(summary.offset, SeekSet); handle_->read((uint8_t *)centralDirectory, summary.size); /** * Pass 1: Validate support and count the number of lump records we need. * Pass 2: Read all zip entries and populate the lump directory. */ char *pos; int entryCount = 0; for(int pass = 0; pass < 2; ++pass) { if(pass == 1) { if(entryCount == 0) break; } // Position the read cursor at the start of the buffered central centralDirectory. pos = (char *)centralDirectory; // Read all the entries. uint lumpIdx = 0; for(int index = 0; index < summary.totalEntryCount; ++index, pos += sizeof(centralfileheader_t)) { centralfileheader_t const *header = (centralfileheader_t *) pos; char const *nameStart = pos + sizeof(centralfileheader_t); localfileheader_t localHeader; // Advance the cursor past the variable sized fields. pos += USHORT(header->fileNameSize) + USHORT(header->extraFieldSize) + USHORT(header->commentSize); String filePath = NativePath(nameStart, USHORT(header->fileNameSize)).withSeparators('/'); // Skip directories (we don't presently model these). if(ULONG(header->size) == 0 && filePath.last() == '/') continue; // Do we support the format of this lump? if(USHORT(header->compression) != ZFC_NO_COMPRESSION && USHORT(header->compression) != ZFC_DEFLATED) { if(pass != 0) continue; LOG_RES_WARNING("Zip %s:'%s' uses an unsupported compression algorithm") << NativePath(composePath()).pretty() << NativePath(filePath).pretty(); } if(USHORT(header->flags) & ZFH_ENCRYPTED) { if(pass != 0) continue; LOG_RES_WARNING("Zip %s:'%s' is encrypted; encryption is not supported") << NativePath(composePath()).pretty() << NativePath(filePath).pretty(); } if(pass == 0) { // Another record will be needed. ++entryCount; continue; } // Read the local file header, which contains the extra field size (Info-ZIP!). handle_->seek(ULONG(header->relOffset), SeekSet); handle_->read((uint8_t *)&localHeader, sizeof(localHeader)); size_t baseOffset = ULONG(header->relOffset) + sizeof(localfileheader_t) + USHORT(header->fileNameSize) + USHORT(localHeader.extraFieldSize); size_t compressedSize; if(USHORT(header->compression) == ZFC_DEFLATED) { // Compressed using the deflate algorithm. compressedSize = ULONG(header->compressedSize); } else // No compression. { compressedSize = ULONG(header->size); } if(!App::game().isNull()) { // In some cases the path to the file is mapped to some // other location in the virtual file system. String filePathCopy = filePath; if(applyGamePathMappings(filePathCopy)) { try { // Resolve all symbolic references in the path. filePath = Uri(filePathCopy, RC_NULL).resolved(); } catch(de::Uri::ResolveError const& er) { LOG_RES_WARNING(er.asText()); } } } // Make it absolute. filePath = App_BasePath() / filePath; Entry &entry = d->entries.insert(Path(filePath)); entry.offset = baseOffset; entry.size = ULONG(header->size); entry.compressedSize = compressedSize; FileHandle *dummy = 0; /// @todo Fixme! LumpFile *lumpFile = new LumpFile(entry, *dummy, entry.path(), FileInfo(lastModified(), // Inherited from the file (note recursion). lumpIdx, entry.offset, entry.size, entry.compressedSize), this); entry.lumpFile.reset(lumpFile); // takes ownership catalogLump(*lumpFile); lumpIdx++; } } // The file central directory is no longer needed. M_Free(centralDirectory); } Zip::~Zip() {} void Zip::clearCachedLump(int lumpIndex, bool *retCleared) { LOG_AS("Zip::clearCachedLump"); if(retCleared) *retCleared = false; if(hasLump(lumpIndex)) { if(!d->dataCache.isNull()) { d->dataCache->remove(lumpIndex, retCleared); } } else { LOGDEV_RES_WARNING(invalidIndexMessage(lumpIndex, lastIndex())); } } void Zip::clearLumpCache() { LOG_AS("Zip::clearLumpCache"); if(!d->dataCache.isNull()) { d->dataCache->clear(); } } uint8_t const *Zip::cacheLump(int lumpIndex) { LOG_AS("Zip::cacheLump"); LumpFile &lumpFile = static_cast(lump(lumpIndex)); LOGDEV_RES_XVERBOSE("\"%s:%s\" (%u bytes%s)") << NativePath(composePath()).pretty() << NativePath(lumpFile.composePath()).pretty() << (unsigned long) lumpFile.info().size << (lumpFile.info().isCompressed()? ", compressed" : ""); // Time to create the cache? if(d->dataCache.isNull()) { d->dataCache.reset(new LumpCache(lumpCount())); } uint8_t const *data = d->dataCache->data(lumpIndex); if(data) return data; uint8_t *region = (uint8_t *) Z_Malloc(lumpFile.info().size, PU_APPSTATIC, 0); if(!region) throw Error("Zip::cacheLump", QString("Failed on allocation of %1 bytes for cache copy of lump #%2").arg(lumpFile.info().size).arg(lumpIndex)); readLump(lumpIndex, region, false); d->dataCache->insert(lumpIndex, region); return region; } void Zip::unlockLump(int lumpIndex) { LOG_AS("Zip::unlockLump"); LOGDEV_RES_XVERBOSE("\"%s:%s\"") << NativePath(composePath()).pretty() << NativePath(lump(lumpIndex).composePath()).pretty(); if(hasLump(lumpIndex)) { if(!d->dataCache.isNull()) { d->dataCache->unlock(lumpIndex); } } else { LOGDEV_RES_WARNING(invalidIndexMessage(lumpIndex, lastIndex())); } } size_t Zip::readLump(int lumpIndex, uint8_t *buffer, bool tryCache) { LOG_AS("Zip::readLump"); return readLump(lumpIndex, buffer, 0, lump(lumpIndex).size(), tryCache); } size_t Zip::readLump(int lumpIndex, uint8_t *buffer, size_t startOffset, size_t length, bool tryCache) { LOG_AS("Zip::readLump"); LumpFile const &lumpFile = static_cast(lump(lumpIndex)); LOGDEV_RES_XVERBOSE("\"%s:%s\" (%u bytes%s) [%u +%u]") << NativePath(composePath()).pretty() << NativePath(lumpFile.composePath()).pretty() << (unsigned long) lumpFile.size() << (lumpFile.isCompressed()? ", compressed" : "") << startOffset << length; // Try to avoid a file system read by checking for a cached copy. if(tryCache) { uint8_t const *data = (!d->dataCache.isNull() ? d->dataCache->data(lumpIndex) : 0); LOGDEV_RES_XVERBOSE("Cache %s on #%i") << (data? "hit" : "miss") << lumpIndex; if(data) { size_t readBytes = de::min(size_t(lumpFile.size()), length); std::memcpy(buffer, data + startOffset, readBytes); return readBytes; } } size_t readBytes = 0; if(!startOffset && length == lumpFile.size()) { // Read it straight to the caller's data buffer. readBytes = d->bufferLump(lumpFile, buffer); } else { // Allocate a temporary buffer and read the whole lump into it(!). uint8_t *readBuf = (uint8_t *) M_Malloc(lumpFile.size()); if(!readBuf) throw Error("Zip::readLump", QString("Failed on allocation of %1 bytes for work buffer").arg(lumpFile.size())); if(d->bufferLump(lumpFile, readBuf)) { readBytes = de::min(size_t(lumpFile.size()), length); std::memcpy(buffer, readBuf + startOffset, readBytes); } M_Free(readBuf); } /// @todo Do not check the read length here. if(readBytes < de::min(size_t(lumpFile.size()), length)) throw Error("Zip::readLump", QString("Only read %1 of %2 bytes of lump #%3").arg(readBytes).arg(length).arg(lumpIndex)); return readBytes; } bool Zip::recognise(FileHandle &file) { localfileheader_t hdr; if(!readArchiveHeader(file, hdr)) return false; return hdr.signature == SIG_LOCAL_FILE_HEADER; } uint8_t *Zip::compress(uint8_t *in, size_t inSize, size_t *outSize) { return compressAtLevel(in, inSize, outSize, Z_DEFAULT_COMPRESSION); } uint8_t *Zip::compressAtLevel(uint8_t *in, size_t inSize, size_t *outSize, int level) { #define CHUNK_SIZE 32768 LOG_AS("Zip::compressAtLevel"); z_stream stream; uint8_t chunk[CHUNK_SIZE]; size_t allocSize = CHUNK_SIZE; uint8_t *output = (uint8_t *) M_Malloc(allocSize); // some initial space int result; int have; DENG2_ASSERT(outSize); *outSize = 0; std::memset(&stream, 0, sizeof(stream)); stream.next_in = (Bytef*) in; stream.avail_in = (uInt) inSize; stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.opaque = Z_NULL; if(level < Z_NO_COMPRESSION) { level = Z_NO_COMPRESSION; } if(level > Z_BEST_COMPRESSION) { level = Z_BEST_COMPRESSION; } result = deflateInit(&stream, level); if(result != Z_OK) { M_Free(output); return 0; } // Compress until all the data has been exhausted. do { stream.next_out = chunk; stream.avail_out = CHUNK_SIZE; result = deflate(&stream, Z_FINISH); if(result == Z_STREAM_ERROR) { M_Free(output); *outSize = 0; return 0; } have = CHUNK_SIZE - stream.avail_out; if(have) { // Need more memory? if(*outSize + have > allocSize) { // Need more memory. allocSize *= 2; output = (uint8_t *) M_Realloc(output, allocSize); } // Append. std::memcpy(output + *outSize, chunk, have); *outSize += have; } } while(!stream.avail_out); // output chunk full, more data may follow DENG2_ASSERT(result == Z_STREAM_END); DENG2_ASSERT(stream.total_out == *outSize); deflateEnd(&stream); return output; #undef CHUNK_SIZE } uint8_t *Zip::uncompress(uint8_t *in, size_t inSize, size_t *outSize) { #define INF_CHUNK_SIZE 4096 // Uncompress in 4KB chunks. LOG_AS("Zip::uncompress"); z_stream stream; uint8_t chunk[INF_CHUNK_SIZE]; size_t allocSize = INF_CHUNK_SIZE; uint8_t *output = (uint8_t *) M_Malloc(allocSize); // some initial space int result; int have; DENG2_ASSERT(outSize); *outSize = 0; std::memset(&stream, 0, sizeof(stream)); stream.next_in = (Bytef *) in; stream.avail_in = (uInt) inSize; result = inflateInit(&stream); if(result != Z_OK) { M_Free(output); return 0; } // Uncompress until all the input data has been exhausted. do { stream.next_out = chunk; stream.avail_out = INF_CHUNK_SIZE; result = inflate(&stream, Z_FINISH); if(result == Z_STREAM_ERROR) { M_Free(output); *outSize = 0; return 0; } have = INF_CHUNK_SIZE - stream.avail_out; if(have) { // Need more memory? if(*outSize + have > allocSize) { // Need more memory. allocSize *= 2; output = (uint8_t *) M_Realloc(output, allocSize); } // Append. std::memcpy(output + *outSize, chunk, have); *outSize += have; } } while(!stream.avail_out); // output chunk full, more data may follow // We should now be at the end. DENG2_ASSERT(result == Z_STREAM_END); inflateEnd(&stream); return output; #undef INF_CHUNK_SIZE } bool Zip::uncompressRaw(uint8_t *in, size_t inSize, uint8_t *out, size_t outSize) { LOG_AS("Zip::uncompressRaw"); z_stream stream; int result; std::memset(&stream, 0, sizeof(stream)); stream.next_in = (Bytef *) in; stream.avail_in = (uInt) inSize; stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.next_out = (Bytef *) out; stream.avail_out = (uInt) outSize; if(inflateInit2(&stream, -MAX_WBITS) != Z_OK) return false; // Do the inflation in one call. result = inflate(&stream, Z_FINISH); if(stream.total_out != outSize) { inflateEnd(&stream); LOG_RES_WARNING("Failure due to %s (result code: %i)") << (result == Z_DATA_ERROR ? "corrupt data" : "zlib error") << result; return false; } // We're done. inflateEnd(&stream); return true; } Zip::LumpTree const &Zip::lumpTree() const { return d->entries; } Zip::LumpFile &Zip::Entry::file() const { DENG2_ASSERT(!lumpFile.isNull()); return *lumpFile; } } // namespace de doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/sys_direc.cpp0000664000175000017500000002474212641367671025270 0ustar jaakkojaakko/** @file sys_direc.cpp Native file system directories. * @ingroup system * * @todo Rewrite using libcore's NativePath (and Qt). * @deprecated Should use FS2 for all file access. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include #include #if defined(WIN32) # include # include # define strdup _strdup #endif #if defined(UNIX) # include # include # include # include # include # include #endif #include #include #include #include #include "doomsday/filesys/sys_direc.h" #include "doomsday/filesys/fs_util.h" #include "doomsday/paths.h" static void setPathFromPathDir(directory_t* dir, const char* path); static void prependBasePath(char* newPath, const char* path, size_t maxLen); static void resolveAppRelativeDirectives(char* translated, const char* path, size_t maxLen); #if defined(UNIX) static void resolveHomeRelativeDirectives(char* path, size_t maxLen); #endif static void resolvePathRelativeDirectives(char* path); static void Dir_SetPath(directory_t* dir, const char *path); /** * Extract just the file name including any extension from @a path. */ static void Dir_FileName(char* name, const char* path, size_t len); /** * Convert directory separators in @a path to their system-specifc form. */ static void Dir_ToNativeSeparators(char* path, size_t len); /** * Convert directory separators in @a path to our internal '/' form. */ static void Dir_FixSeparators(char* path, size_t len); /// @return @c true if @a path is absolute. static int Dir_IsAbsolutePath(const char* path); directory_t* Dir_New(const char* path) { directory_t* dir = (directory_t*) M_Calloc(sizeof *dir); Dir_SetPath(dir, path); return dir; } directory_t* Dir_NewFromCWD(void) { directory_t* dir = (directory_t*) M_Calloc(sizeof *dir); size_t lastIndex; char* cwd; cwd = Dir_CurrentPath(); lastIndex = strlen(cwd); lastIndex = MIN_OF(lastIndex, FILENAME_T_LASTINDEX); #if defined(WIN32) dir->drive = _getdrive(); #endif memcpy(dir->path, cwd, lastIndex); dir->path[lastIndex] = '\0'; free(cwd); return dir; } directory_t* Dir_FromText(const char* path) { directory_t* dir; if(!path || !path[0]) return Dir_NewFromCWD(); dir = (directory_t*) M_Calloc(sizeof *dir); setPathFromPathDir(dir, path); return dir; } void Dir_Delete(directory_t* dir) { DENG_ASSERT(NULL != dir); M_Free(dir); } const char* Dir_Path(directory_t* dir) { DENG_ASSERT(NULL != dir); return dir->path; } static void Dir_SetPath(directory_t* dir, const char* path) { filename_t fileName; DENG_ASSERT(dir); setPathFromPathDir(dir, path); Dir_FileName(fileName, path, FILENAME_T_MAXLEN); M_StrCat(dir->path, fileName, FILENAME_T_MAXLEN); // Ensure we've a well-formed path. Dir_CleanPath(dir->path, FILENAME_T_MAXLEN); } static void setPathFromPathDir(directory_t* dir, const char* path) { filename_t temp, transPath; DENG_ASSERT(dir && path && path[0]); resolveAppRelativeDirectives(transPath, path, FILENAME_T_MAXLEN); #ifdef UNIX resolveHomeRelativeDirectives(transPath, FILENAME_T_MAXLEN); #endif Dir_ToNativeSeparators(transPath, FILENAME_T_MAXLEN); _fullpath(temp, transPath, FILENAME_T_MAXLEN); _splitpath(temp, dir->path, transPath, 0, 0); M_StrCat(dir->path, transPath, FILENAME_T_MAXLEN); #if defined(WIN32) dir->drive = toupper(dir->path[0]) - 'A' + 1; #endif Dir_FixSeparators(dir->path, FILENAME_T_MAXLEN); } /// Class-Static Members: static void prependBasePath(char* newPath, const char* path, size_t maxLen) { DENG_ASSERT(newPath && path); // Cannot prepend to absolute paths. if(!Dir_IsAbsolutePath(path)) { filename_t buf; dd_snprintf(buf, maxLen, "%s%s", DD_BasePath(), path); memcpy(newPath, buf, maxLen); return; } strncpy(newPath, path, maxLen); } static void resolveAppRelativeDirectives(char* translated, const char* path, size_t maxLen) { filename_t buf; DENG_ASSERT(translated && path); if(path[0] == '>' || path[0] == '}') { path++; if(!Dir_IsAbsolutePath(path)) prependBasePath(buf, path, maxLen); else strncpy(buf, path, maxLen); strncpy(translated, buf, maxLen); } else if(translated != path) { strncpy(translated, path, maxLen); } } #if defined(UNIX) static void resolveHomeRelativeDirectives(char* path, size_t maxLen) { filename_t buf; DENG_ASSERT(path); if(!path[0] || 0 == maxLen || path[0] != '~') return; memset(buf, 0, sizeof(buf)); if(path[1] == '/') { // Replace it with the HOME environment variable. strncpy(buf, getenv("HOME"), FILENAME_T_MAXLEN); if(DENG_LAST_CHAR(buf) != '/') M_StrCat(buf, "/", FILENAME_T_MAXLEN); // Append the rest of the original path. M_StrCat(buf, path + 2, FILENAME_T_MAXLEN); } else { char userName[4096], *end = NULL; struct passwd *pw; end = strchr(path + 1, '/'); strncpy(userName, path, end - path - 1); userName[end - path - 1] = 0; pw = getpwnam(userName); if(pw) { strncpy(buf, pw->pw_dir, FILENAME_T_MAXLEN); if(DENG_LAST_CHAR(buf) != '/') M_StrCat(buf, "/", FILENAME_T_MAXLEN); } M_StrCat(buf, path + 1, FILENAME_T_MAXLEN); } // Replace the original. strncpy(path, buf, maxLen - 1); } #endif static void resolvePathRelativeDirectives(char* path) { DENG_ASSERT(NULL != path); char* ch = path; char* end = path + strlen(path); char* prev = path; // Assume an absolute path. for(; *ch; ch++) { if(ch[0] == '/' && ch[1] == '.') { if(ch[2] == '/') { memmove(ch, ch + 2, end - ch - 1); ch--; } else if(ch[2] == '.' && ch[3] == '/') { memmove(prev, ch + 3, end - ch - 2); // Must restart from the beginning. // This is a tad inefficient, though. ch = path - 1; continue; } } if(*ch == '/') prev = ch; } } void Dir_CleanPath(char* path, size_t len) { if(!path || 0 == len) return; M_Strip(path, len); #if defined(UNIX) resolveHomeRelativeDirectives(path, len); #endif Dir_FixSeparators(path, len); resolvePathRelativeDirectives(path); } void Dir_CleanPathStr(ddstring_t* str) { size_t len = Str_Length(str); char* path = strdup(Str_Text(str)); Dir_CleanPath(path, len); Str_Set(str, path); free(path); } char* Dir_CurrentPath(void) { de::String path = de::App::currentWorkPath(); // FS1 generally assumes that paths end with a separator. if(!path.endsWith(de::NativePath::separator())) { path += de::NativePath::separator(); } return strdup(path.toLatin1()); } static void Dir_FileName(char* name, const char* path, size_t len) { char ext[100]; /// @todo Use dynamic string. if(!path || !name || 0 == len) return; _splitpath(path, 0, 0, name, ext); M_StrCat(name, ext, len); } static int Dir_IsAbsolutePath(const char* path) { if(!path || !path[0]) return 0; if(path[0] == '/' || path[1] == ':') return true; #if defined(UNIX) if(path[0] == '~') return true; #endif return false; } dd_bool Dir_mkpath(const char* path) { #if !defined(WIN32) && !defined(UNIX) # error Dir_mkpath has no implementation for this platform. #endif filename_t full, buf; char* ptr, *endptr; if(!path || !path[0]) return false; // Convert all backslashes to normal slashes. strncpy(full, path, FILENAME_T_MAXLEN); Dir_ToNativeSeparators(full, FILENAME_T_MAXLEN); // Does this path already exist? if(0 == access(full, 0)) return true; // Check and create the path in segments. ptr = full; memset(buf, 0, sizeof(buf)); do { endptr = strchr(ptr, DENG_DIR_SEP_CHAR); if(!endptr) M_StrCat(buf, ptr, FILENAME_T_MAXLEN); else M_StrnCat(buf, ptr, endptr - ptr, FILENAME_T_MAXLEN); if(buf[0] && access(buf, 0)) { // Path doesn't exist, create it. #if defined(WIN32) mkdir(buf); #elif defined(UNIX) mkdir(buf, 0775); #endif } M_StrCat(buf, DENG_DIR_SEP_STR, FILENAME_T_MAXLEN); ptr = endptr + 1; } while(endptr); return (0 == access(full, 0)); } void Dir_MakeAbsolutePath(char* path, size_t len) { filename_t buf; if(!path || !path[0] || 0 == len) return; #if defined(UNIX) resolveHomeRelativeDirectives(path, len); #endif _fullpath(buf, path, FILENAME_T_MAXLEN); strncpy(path, buf, len); Dir_FixSeparators(path, len); } static void Dir_ToNativeSeparators(char* path, size_t len) { size_t i; if(!path || !path[0] || 0 == len) return; for(i = 0; i < len && path[i]; ++i) { if(path[i] == DENG_DIR_WRONG_SEP_CHAR) path[i] = DENG_DIR_SEP_CHAR; } } static void Dir_FixSeparators(char* path, size_t len) { size_t i; if(!path || !path[0] || 0 == len) return; for(i = 0; i < len && path[i]; ++i) { if(path[i] == '\\') path[i] = '/'; } } dd_bool Dir_SetCurrent(const char* path) { LOG_AS("Dir"); bool success = false; if(path && path[0]) { success = de::NativePath::setWorkPath(path); } LOG_RES_VERBOSE("Changing current directory to \"%s\" %s") << path << (success? "succeeded" : "failed"); return success; } doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/lumpcache.cpp0000664000175000017500000001126512641367671025241 0ustar jaakkojaakko/** @file lumpcache.cpp Provides a data cache tailored to storing lumps (i.e., files). * * @author Copyright © 2013-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/filesys/lumpcache.h" #include #include #include #include using namespace de; LumpCache::Data::Data(uint8_t *data) : data_(data) {} LumpCache::Data::~Data() { clearData(); } uint8_t *LumpCache::Data::data() const { if(data_ && Z_GetTag(data_) == PU_PURGELEVEL) { // Reaquire the data. Z_ChangeTag2(data_, PU_APPSTATIC); Z_ChangeUser(data_, (void*)&data_); } return data_; } uint8_t const *LumpCache::Data::replaceData(uint8_t *newData) { clearData(); data_ = newData; if(data_) { Z_ChangeUser(data_, &data_); } return newData; } LumpCache::Data &LumpCache::Data::clearData(bool *retCleared) { bool hasData = !!data_; if(hasData) { /// @todo Implement a proper thread-safe locking mechanism. // Elevate the cached data to purge level so it will be explicitly // free'd by the Zone the next time the rover passes it. if(Z_GetTag(data_) != PU_PURGELEVEL) { Z_ChangeTag2(data_, PU_PURGELEVEL); } // Mark the data as unowned. Z_ChangeUser(data_, (void *) 0x2); } if(retCleared) *retCleared = hasData; return *this; } LumpCache::Data &LumpCache::Data::lock() { /// @todo Implement a proper thread-safe locking mechanism. return *this; } LumpCache::Data &LumpCache::Data::unlock() { /// @todo Implement a proper thread-safe locking mechanism. if(data_) { Z_ChangeTag2(data_, PU_PURGELEVEL); } return *this; } LumpCache::LumpCache(uint size) : _size(size), _dataCache(0) {} LumpCache::~LumpCache() { if(_dataCache) delete _dataCache; } uint LumpCache::size() const { return _size; } bool LumpCache::isValidIndex(uint idx) const { return idx < _size; } uint8_t const *LumpCache::data(uint lumpIdx) const { LOG_AS("LumpCache::data"); Data const* record = cacheRecord(lumpIdx); return record? record->data() : 0; } LumpCache &LumpCache::insert(uint lumpIdx, uint8_t *data) { LOG_AS("LumpCache::insert"); if(!isValidIndex(lumpIdx)) throw Error("LumpCache::insert", QString("Invalid index %1").arg(lumpIdx)); // Time to allocate the data cache? if(!_dataCache) { _dataCache = new DataCache(_size); } Data *record = cacheRecord(lumpIdx); record->replaceData(data); return *this; } LumpCache &LumpCache::insertAndLock(uint lumpIdx, uint8_t *data) { return insert(lumpIdx, data).lock(lumpIdx); } LumpCache &LumpCache::lock(uint lumpIdx) { LOG_AS("LumpCache::lock"); if(!isValidIndex(lumpIdx)) throw Error("LumpCache::lock", QString("Invalid index %1").arg(lumpIdx)); Data* record = cacheRecord(lumpIdx); record->lock(); return *this; } LumpCache &LumpCache::unlock(uint lumpIdx) { LOG_AS("LumpCache::unlock"); if(!isValidIndex(lumpIdx)) throw Error("LumpCache::unlock", QString("Invalid index %1").arg(lumpIdx)); Data* record = cacheRecord(lumpIdx); record->unlock(); return *this; } LumpCache &LumpCache::remove(uint lumpIdx, bool *retRemoved) { Data *record = cacheRecord(lumpIdx); if(record) { record->clearData(retRemoved); } else if(retRemoved) { *retRemoved = false; } return *this; } LumpCache &LumpCache::clear() { if(_dataCache) { DENG2_FOR_EACH(DataCache, i, *_dataCache) { i->clearData(); } } return *this; } LumpCache::Data *LumpCache::cacheRecord(uint lumpIdx) { if(!isValidIndex(lumpIdx)) return 0; return _dataCache? &(*_dataCache)[lumpIdx] : 0; } LumpCache::Data const *LumpCache::cacheRecord(uint lumpIdx) const { if(!isValidIndex(lumpIdx)) return 0; return _dataCache? &(*_dataCache)[lumpIdx] : 0; } doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/searchpath.cpp0000664000175000017500000000261112641367671025415 0ustar jaakkojaakko/** @file searchpath.cpp SearchPath * * @authors Copyright © 2010-2013 Daniel Swanson * @authors Copyright © 2010-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/filesys/searchpath.h" namespace de { SearchPath::SearchPath(de::Uri const &_uri, SearchPath::Flags _flags) : Uri(_uri), flags_(_flags) {} SearchPath::SearchPath(SearchPath const &other) : Uri(other), flags_(other.flags_) {} SearchPath::Flags SearchPath::flags() const { return flags_; } SearchPath &SearchPath::setFlags(Flags newFlags) { flags_ = newFlags; return *this; } } // namespace de doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/fs_windows.cpp0000664000175000017500000000255712641367671025466 0ustar jaakkojaakko/** @file fs_windows.cpp Windows-specific file system operations. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/filesys/fs_windows.h" #include #include using namespace de; FILE *FS_Win32_fopen(char const *filenameUtf8, char const *mode) { return _wfopen(String(filenameUtf8).toStdWString().c_str(), String(mode).toStdWString().c_str()); } int FS_Win32_access(char const *pathUtf8, int mode) { return _waccess(String(pathUtf8).toStdWString().c_str(), mode); } int FS_Win32_mkdir(char const *dirnameUtf8) { return _wmkdir(String(dirnameUtf8).toStdWString().c_str()); } doomsday-stable-1.15.7/doomsday/libdoomsday/src/filesys/filehandle.cpp0000664000175000017500000001623312641367671025373 0ustar jaakkojaakko/** @file filehandle.cpp * * Reference/handle to a unique file in the engine's virtual file system. * * @ingroup fs * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/filesys/file.h" #include #include #include #include #include #include namespace de { struct FileHandle::Instance { /// The referenced file (if any). File1 *file; /// Either the FileList which owns this or the next FileHandle in the used object pool. void *list; struct dfile_flags_s { uint open:1; ///< Presently open. uint eof:1; ///< Reader has reached the end of the stream. uint reference:1; ///< This handle is a reference to another dfile instance. } flags; /// Offset from start of owning package. size_t baseOffset; FILE *hndl; size_t size; uint8_t *data; uint8_t *pos; Instance() : file(0), list(0), baseOffset(0), hndl(0), size(0), data(0), pos(0) { flags.eof = false; flags.open = false; flags.reference = false; } }; static void errorIfNotValid(FileHandle const &file, char const * /*callerName*/) { DENG2_ASSERT(file.isValid()); if(!file.isValid()) exit(1); } FileHandle::FileHandle() { d = new Instance(); } FileHandle::~FileHandle() { close(); // Free any cached data. if(d->data) { M_Free(d->data); d->data = 0; } delete d; } FileHandle &FileHandle::close() { if(!d->flags.open) return *this; if(d->hndl) { fclose(d->hndl); d->hndl = 0; } // Free any cached data. if(d->data) { M_Free(d->data); d->data = 0; } d->pos = 0; d->flags.open = false; return *this; } bool FileHandle::isValid() const { return true; } FileList *FileHandle::list() { errorIfNotValid(*this, "FileHandle::list"); return (FileList *)d->list; } FileHandle &FileHandle::setList(FileList *list) { d->list = list; return *this; } bool FileHandle::hasFile() const { errorIfNotValid(*this, "FileHandle::file"); return !!d->file; } File1 &FileHandle::file() { errorIfNotValid(*this, "FileHandle::file"); return *d->file; } File1 &FileHandle::file() const { errorIfNotValid(*this, "FileHandle::file const"); return *d->file; } size_t FileHandle::baseOffset() const { if(d->flags.reference) { return d->file->handle().baseOffset(); } return d->baseOffset; } size_t FileHandle::length() { errorIfNotValid(*this, "FileHandle::Length"); if(d->flags.reference) { return d->file->handle().length(); } else { size_t const currentPosition = seek(0, SeekEnd); size_t const length = tell(); seek(currentPosition, SeekSet); return length; } } size_t FileHandle::read(uint8_t *buffer, size_t count) { errorIfNotValid(*this, "FileHandle::read"); if(d->flags.reference) { return d->file->handle().read(buffer, count); } else { if(d->hndl) { // Normal file. count = fread(buffer, 1, count, d->hndl); if(feof(d->hndl)) { d->flags.eof = true; } return count; } // Is there enough room in the file? size_t bytesleft = d->size - (d->pos - d->data); if(count > bytesleft) { count = bytesleft; d->flags.eof = true; } if(count) { memcpy(buffer, d->pos, count); d->pos += count; } return count; } } bool FileHandle::atEnd() { errorIfNotValid(*this, "FileHandle::atEnd"); if(d->flags.reference) { return d->file->handle().atEnd(); } return (d->flags.eof != 0); } unsigned char FileHandle::getC() { errorIfNotValid(*this, "FileHandle::getC"); unsigned char ch = 0; read((uint8_t *)&ch, 1); return ch; } size_t FileHandle::tell() { errorIfNotValid(*this, "FileHandle::tell"); if(d->flags.reference) { return d->file->handle().tell(); } else { if(d->hndl) { return (size_t) ftell(d->hndl); } return d->pos - d->data; } } size_t FileHandle::seek(size_t offset, SeekMethod whence) { if(d->flags.reference) { return d->file->handle().seek(offset, whence); } else { size_t oldpos = tell(); d->flags.eof = false; if(d->hndl) { int fwhence = whence == SeekSet? SEEK_SET : whence == SeekCur? SEEK_CUR : SEEK_END; fseek(d->hndl, (long) (d->baseOffset + offset), fwhence); } else { if(whence == SeekSet) d->pos = d->data + offset; else if(whence == SeekEnd) d->pos = d->data + (d->size + offset); else if(whence == SeekCur) d->pos += offset; } return oldpos; } } FileHandle &FileHandle::rewind() { seek(0, SeekSet); return *this; } FileHandle *FileHandle::fromFile(File1 &file) // static { FileHandle *hndl = new FileHandle(); hndl->d->file = &file; hndl->d->flags.open = true; hndl->d->flags.reference = true; return hndl; } FileHandle *FileHandle::fromNativeFile(FILE &file, size_t baseOffset) // static { FileHandle *hndl = new FileHandle(); hndl->d->flags.open = true; hndl->d->hndl = &file; hndl->d->baseOffset = baseOffset; return hndl; } FileHandle *FileHandle::fromLump(File1 &lump, bool dontBuffer) // static { LOG_AS("FileHandle::fromLump"); FileHandle *hndl = new FileHandle(); // Init and load in the lump data. hndl->d->file = &lump; hndl->d->flags.open = true; if(!dontBuffer) { hndl->d->size = lump.size(); hndl->d->pos = hndl->d->data = (uint8_t *) M_Malloc(hndl->d->size); LOGDEV_RES_XVERBOSE_DEBUGONLY("[%p] Buffering \"%s:%s\"...", dintptr(hndl) << NativePath(lump.container().composePath()).pretty() << NativePath(lump.composePath()).pretty()); lump.read((uint8_t *)hndl->d->data, 0, lump.size()); } return hndl; } } // namespace de doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/0000775000175000017500000000000012641367671022032 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/finale.cpp0000664000175000017500000000236312641367671024000 0ustar jaakkojaakko/** @file finale.cpp Finale definition accessor. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/defs/finale.h" #include "doomsday/defs/ded.h" #include #include using namespace de; namespace defn { void Finale::resetToDefaults() { Definition::resetToDefaults(); // Add all expected fields with their default values. def().addText("id", ""); def().addText("before", ""); def().addText("after", ""); def().addText("script", ""); } } // namespace defn doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/decoration.cpp0000664000175000017500000000363612641367671024675 0ustar jaakkojaakko/** @file decoration.cpp Decoration definition accessor. * * @authors Copyright © 2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/defs/decoration.h" #include #include #include "doomsday/defs/ded.h" #include "doomsday/defs/material.h" using namespace de; namespace defn { void Decoration::resetToDefaults() { Definition::resetToDefaults(); // Add all expected fields with their default values. def().addText ("texture", ""); // URI. Unknown. def().addNumber("flags", 0); def().addArray ("light", new ArrayValue); } Record &Decoration::addLight() { auto *decor = new Record; MaterialDecoration(*decor).resetToDefaults(); def()["light"].value().add(new RecordValue(decor, RecordValue::OwnsRecord)); return *decor; } int Decoration::lightCount() const { return int(geta("light").size()); } bool Decoration::hasLight(int index) const { return index >= 0 && index < lightCount(); } Record &Decoration::light(int index) { return *def().geta("light")[index].as().record(); } Record const &Decoration::light(int index) const { return *geta("light")[index].as().record(); } } // namespace defn doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/definition.cpp0000664000175000017500000000266412641367671024676 0ustar jaakkojaakko/** @file definition.cpp Base class for definition record accessors. * * @authors Copyright © 2014 Jaakko Keränen * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/defs/definition.h" #include using namespace de; namespace defn { Record &Definition::def() { return const_cast(accessedRecord()); } Record const &Definition::def() const { return accessedRecord(); } int Definition::order() const { if(!accessedRecordPtr()) return -1; return geti("__order__"); } Definition::operator bool() const { return accessedRecordPtr() != 0; } void Definition::resetToDefaults() { def().addBoolean("custom", false); } } // namespace defn doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/sky.cpp0000664000175000017500000000647412641367671023357 0ustar jaakkojaakko/** @file defs/sky.cpp Sky definition accessor. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/defs/sky.h" #include "doomsday/defs/ded.h" #include #include using namespace de; namespace defn { void Sky::resetToDefaults() { Definition::resetToDefaults(); // Add all expected fields with their default values. def().addText ("id", ""); def().addNumber("flags", 0); def().addNumber("height", DEFAULT_SKY_HEIGHT); def().addNumber("horizonOffset", DEFAULT_SKY_HORIZON_OFFSET); def().addArray ("color", new ArrayValue(Vector3f())); def().addArray ("layer", new ArrayValue); def().addArray ("model", new ArrayValue); // Skies have two layers by default. addLayer(); addLayer(); } Record &Sky::addLayer() { Record *layer = new Record; layer->addBoolean("custom", false); layer->addNumber("flags", 0); layer->addText ("material", ""); layer->addNumber("offset", DEFAULT_SKY_SPHERE_XOFFSET); layer->addNumber("offsetSpeed", 0); layer->addNumber("colorLimit", DEFAULT_SKY_SPHERE_FADEOUT_LIMIT); def()["layer"].value() .add(new RecordValue(layer, RecordValue::OwnsRecord)); return *layer; } int Sky::layerCount() const { return int(geta("layer").size()); } bool Sky::hasLayer(int index) const { return index >= 0 && index < layerCount(); } Record &Sky::layer(int index) { return *def().geta("layer")[index].as().record(); } Record const &Sky::layer(int index) const { return *geta("layer")[index].as().record(); } Record &Sky::addModel() { Record *model = new Record; model->addBoolean("custom", false); model->addText ("id", ""); model->addNumber("layer", -1); model->addNumber("frameInterval", 1); model->addNumber("yaw", 0); model->addNumber("yawSpeed", 0); model->addArray ("originOffset", new ArrayValue(Vector3f())); model->addArray ("rotate", new ArrayValue(Vector2f())); model->addText ("execute", ""); model->addArray ("color", new ArrayValue(Vector4f(1, 1, 1, 1))); def()["model"].value() .add(new RecordValue(model, RecordValue::OwnsRecord)); return *model; } int Sky::modelCount() const { return int(geta("model").size()); } bool Sky::hasModel(int index) const { return index >= 0 && index < modelCount(); } Record &Sky::model(int index) { return *def().geta("model")[index].as().record(); } Record const &Sky::model(int index) const { return *geta("model")[index].as().record(); } } // namespace defn doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/mapinfo.cpp0000664000175000017500000000375712641367671024203 0ustar jaakkojaakko/** @file defs/mapinfo.cpp MapInfo definition accessor. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/defs/mapinfo.h" #include "doomsday/defs/sky.h" #include "doomsday/defs/ded.h" #include #include using namespace de; namespace defn { void MapInfo::resetToDefaults() { Definition::resetToDefaults(); // Add all expected fields with their default values. def().addText ("id", ""); def().addText ("title", "Untitled"); def().addText ("titleImage", ""); def().addText ("author", "Unknown"); def().addNumber("flags", 0); def().addText ("music", ""); def().addNumber("parTime", -1); // unknown def().addArray ("fogColor", new ArrayValue(Vector3f(DEFAULT_FOG_COLOR_RED, DEFAULT_FOG_COLOR_GREEN, DEFAULT_FOG_COLOR_BLUE))); def().addNumber("fogStart", DEFAULT_FOG_START); def().addNumber("fogEnd", DEFAULT_FOG_END); def().addNumber("fogDensity", DEFAULT_FOG_DENSITY); def().addText ("fadeTable", ""); def().addNumber("ambient", 0); def().addNumber("gravity", 1); def().addText ("skyId", ""); def().addText ("execute", ""); QScopedPointer sky(new Record); Sky(*sky).resetToDefaults(); def().add ("sky", sky.take()); } } // namespace defn doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/music.cpp0000664000175000017500000000237512641367671023665 0ustar jaakkojaakko/** @file defs/music.cpp Music definition accessor. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/defs/music.h" #include "doomsday/defs/ded.h" #include #include using namespace de; namespace defn { void Music::resetToDefaults() { Definition::resetToDefaults(); // Add all expected fields with their default values. def().addText ("id", ""); def().addText ("lumpName", ""); def().addText ("path", ""); def().addNumber("cdTrack", 0); } } // namespace defn doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/material.cpp0000664000175000017500000001364512641367671024345 0ustar jaakkojaakko/** @file material.cpp Material definition accessor. * * @authors Copyright © 2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/defs/material.h" #include "doomsday/defs/ded.h" #include #include using namespace de; namespace defn { void MaterialDecoration::resetToDefaults() { Definition::resetToDefaults(); // Add all expected fields with their default values. def().addArray("patternOffset", new ArrayValue(Vector2i())); def().addArray("patternSkip", new ArrayValue(Vector2i())); def().addArray("stage", new ArrayValue); } Record &MaterialDecoration::addStage() { auto *stage = new Record; stage->addNumber("tics", 0); stage->addNumber("variance", 0); // Time. stage->addArray ("origin", new ArrayValue(Vector2f())); // Surface-relative offset. stage->addNumber("elevation", 1); // Distance from the surface. stage->addArray ("color", new ArrayValue(Vector3f())); // Light color. (0,0,0) means not visible during this stage. stage->addNumber("radius", 1); // Dynamic light radius (-1 = no light). stage->addArray ("lightLevels", new ArrayValue(Vector2f())); // Fade by sector lightlevel. stage->addText ("lightmapUp", ""); // Uri. None. stage->addText ("lightmapDown", ""); // Uri. None. stage->addText ("lightmapSide", ""); // Uri. None. stage->addNumber("haloRadius", 0); // Halo radius (zero = no halo). stage->addText ("haloTexture", ""); // Uri. None. stage->addNumber("haloTextureIndex", 0); // Overrides haloTexture def()["stage"].value() .add(new RecordValue(stage, RecordValue::OwnsRecord)); return *stage; } int MaterialDecoration::stageCount() const { return int(geta("stage").size()); } bool MaterialDecoration::hasStage(int index) const { return index >= 0 && index < stageCount(); } Record &MaterialDecoration::stage(int index) { return *def().geta("stage")[index].as().record(); } Record const &MaterialDecoration::stage(int index) const { return *geta("stage")[index].as().record(); } // ------------------------------------------------------------------------------------ void MaterialLayer::resetToDefaults() { Definition::resetToDefaults(); // Add all expected fields with their default values. def().addArray("stage", new ArrayValue); } Record &MaterialLayer::addStage() { auto *stage = new Record; stage->addText ("texture", ""); // Uri. None. stage->addNumber("tics", 0); stage->addNumber("variance", 0); // Time. stage->addNumber("glowStrength", 0); stage->addNumber("glowStrengthVariance", 0); stage->addArray ("texOrigin", new ArrayValue(Vector2f())); def()["stage"].value() .add(new RecordValue(stage, RecordValue::OwnsRecord)); return *stage; } int MaterialLayer::stageCount() const { return int(geta("stage").size()); } bool MaterialLayer::hasStage(int index) const { return index >= 0 && index < stageCount(); } Record &MaterialLayer::stage(int index) { return *def().geta("stage")[index].as().record(); } Record const &MaterialLayer::stage(int index) const { return *geta("stage")[index].as().record(); } // ------------------------------------------------------------------------------------ void Material::resetToDefaults() { Definition::resetToDefaults(); // Add all expected fields with their default values. def().addText ("id", ""); // URI. Unknown. def().addBoolean("autoGenerated", false); def().addNumber ("flags", 0); def().addArray ("dimensions", new ArrayValue(Vector2i())); def().addArray ("decoration", new ArrayValue); def().addArray ("layer", new ArrayValue); } Record &Material::addDecoration() { auto *decor = new Record; MaterialDecoration(*decor).resetToDefaults(); def()["decoration"].value().add(new RecordValue(decor, RecordValue::OwnsRecord)); return *decor; } int Material::decorationCount() const { return int(geta("decoration").size()); } bool Material::hasDecoration(int index) const { return index >= 0 && index < decorationCount(); } Record &Material::decoration(int index) { return *def().geta("decoration")[index].as().record(); } Record const &Material::decoration(int index) const { return *geta("decoration")[index].as().record(); } Record &Material::addLayer() { auto *layer = new Record; MaterialLayer(*layer).resetToDefaults(); def()["layer"].value().add(new RecordValue(layer, RecordValue::OwnsRecord)); return *layer; } int Material::layerCount() const { return int(geta("layer").size()); } bool Material::hasLayer(int index) const { return index >= 0 && index < layerCount(); } Record &Material::layer(int index) { return *def().geta("layer")[index].as().record(); } Record const &Material::layer(int index) const { return *geta("layer")[index].as().record(); } } // namespace defn doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/episode.cpp0000664000175000017500000001076412641367671024176 0ustar jaakkojaakko/** @file episode.cpp Episode definition accessor. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/defs/episode.h" #include "doomsday/defs/ded.h" #include #include using namespace de; namespace defn { void Episode::resetToDefaults() { Definition::resetToDefaults(); // Add all expected fields with their default values. def().addText("id", ""); def().addText("startMap", "Maps:"); // URI. Unknown. def().addText("title", "Untitled"); def().addText("menuHelpInfo", ""); // None. def().addText("menuImage", ""); // URI. None. def().addText("menuShortcut", ""); // Key name. None. def().addArray("hub", new ArrayValue); def().addArray("map", new ArrayValue); } Record &Episode::addHub() { Record *hub = new Record; hub->addBoolean("custom", false); hub->addText ("id", ""); hub->addArray("map", new ArrayValue); def()["hub"].value() .add(new RecordValue(hub, RecordValue::OwnsRecord)); return *hub; } int Episode::hubCount() const { return int(geta("hub").size()); } bool Episode::hasHub(int index) const { return index >= 0 && index < hubCount(); } Record &Episode::hub(int index) { return *def().geta("hub")[index].as().record(); } Record const &Episode::hub(int index) const { return *geta("hub")[index].as().record(); } Record *Episode::tryFindHubByMapId(String const &mapId) { de::Uri const mapUri(mapId, RC_NULL); if(!mapUri.path().isEmpty()) { for(int i = 0; i < hubCount(); ++i) { Record &hubRec = hub(i); foreach(Value *mapIt, hubRec.geta("map").elements()) { Record &mgNodeDef = mapIt->as().dereference(); if(mapUri == de::Uri(mgNodeDef.gets("id"), RC_NULL)) { return &hubRec; } } } } return 0; // Not found. } Record *Episode::tryFindMapGraphNode(String const &mapId) { de::Uri const mapUri(mapId, RC_NULL); if(!mapUri.path().isEmpty()) { // First, try the hub maps. for(int i = 0; i < hubCount(); ++i) { Record const &hubRec = hub(i); foreach(Value *mapIt, hubRec.geta("map").elements()) { Record &mgNodeDef = mapIt->as().dereference(); if(mapUri == de::Uri(mgNodeDef.gets("id"), RC_NULL)) { return &mgNodeDef; } } } // Try the non-hub maps. foreach(Value *mapIt, geta("map").elements()) { Record &mgNodeDef = mapIt->as().dereference(); if(mapUri == de::Uri(mgNodeDef.gets("id"), RC_NULL)) { return &mgNodeDef; } } } return 0; // Not found. } de::Record *Episode::tryFindMapGraphNodeByWarpNumber(int warpNumber) { if(warpNumber > 0) { // First, try the hub maps. for(int i = 0; i < hubCount(); ++i) { Record const &hubRec = hub(i); foreach(Value *mapIt, hubRec.geta("map").elements()) { Record &mgNodeDef = mapIt->as().dereference(); if(mgNodeDef.geti("warpNumber") == warpNumber) { return &mgNodeDef; } } } // Try the non-hub maps. foreach(Value *mapIt, geta("map").elements()) { Record &mgNodeDef = mapIt->as().dereference(); if(mgNodeDef.geti("warpNumber") == warpNumber) { return &mgNodeDef; } } } return 0; // Not found. } } // namespace defn doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/ded.cpp0000664000175000017500000003375512641367671023307 0ustar jaakkojaakko/** @file ded.cpp Doomsday Engine Definition database. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/defs/ded.h" #include #include #include #include #include #include #include #include #include "doomsday/defs/decoration.h" #include "doomsday/defs/episode.h" #include "doomsday/defs/finale.h" #include "doomsday/defs/mapinfo.h" #include "doomsday/defs/material.h" #include "doomsday/defs/model.h" #include "doomsday/defs/music.h" #include "doomsday/defs/sky.h" using namespace de; float ded_ptcstage_t::particleRadius(int ptcIDX) const { if(radiusVariance) { static float const rnd[16] = { .875f, .125f, .3125f, .75f, .5f, .375f, .5625f, .0625f, 1, .6875f, .625f, .4375f, .8125f, .1875f, .9375f, .25f }; return (rnd[ptcIDX & 0xf] * radiusVariance + (1 - radiusVariance)) * radius; } return radius; } ded_s::ded_s() : flags (names.addRecord("flags")) , episodes (names.addRecord("episodes")) , materials (names.addRecord("materials")) , models (names.addRecord("models")) , skies (names.addRecord("skies")) , musics (names.addRecord("musics")) , mapInfos (names.addRecord("mapInfos")) , finales (names.addRecord("finales")) , decorations(names.addRecord("decorations")) { decorations.addLookupKey("texture"); episodes.addLookupKey("id"); finales.addLookupKey("id"); finales.addLookupKey("before"); finales.addLookupKey("after"); flags.addLookupKey("id"); mapInfos.addLookupKey("id"); materials.addLookupKey("id"); models.addLookupKey("id", DEDRegister::OnlyFirst); models.addLookupKey("state"); musics.addLookupKey("id", DEDRegister::OnlyFirst); skies.addLookupKey("id"); clear(); } void ded_s::clear() { release(); version = DED_VERSION; modelFlags = 0; modelScale = 0; modelOffset = 0; } int ded_s::addFlag(String const &id, int value) { Record &def = flags.append(); def.addText("id", id); def.addNumber("value", value); return def.geti("__order__"); } int ded_s::addEpisode() { Record &def = episodes.append(); defn::Episode(def).resetToDefaults(); return def.geti("__order__"); } int ded_s::addDecoration() { Record &def = decorations.append(); defn::Decoration(def).resetToDefaults(); return def.geti("__order__"); } int ded_s::addFinale() { Record &def = finales.append(); defn::Finale(def).resetToDefaults(); return def.geti("__order__"); } int ded_s::addMapInfo() { Record &def = mapInfos.append(); defn::MapInfo(def).resetToDefaults(); return def.geti("__order__"); } int ded_s::addMaterial() { Record &def = materials.append(); defn::Material(def).resetToDefaults(); return def.geti("__order__"); } int ded_s::addModel() { Record &def = models.append(); defn::Model(def).resetToDefaults(); return def.geti("__order__"); } int ded_s::addMusic() { Record &def = musics.append(); defn::Music(def).resetToDefaults(); return def.geti("__order__"); } int ded_s::addSky() { Record &def = skies.append(); defn::Sky(def).resetToDefaults(); return def.geti("__order__"); } void ded_s::release() { flags.clear(); episodes.clear(); mobjs.clear(); states.clear(); sprites.clear(); lights.clear(); models.clear(); sounds.clear(); musics.clear(); mapInfos.clear(); skies.clear(); details.clear(); materials.clear(); text.clear(); textureEnv.clear(); compositeFonts.clear(); values.clear(); decorations.clear(); reflections.clear(); groups.clear(); sectorTypes.clear(); lineTypes.clear(); ptcGens.clear(); finales.clear(); } int DED_AddMobj(ded_t* ded, char const* idstr) { ded_mobj_t *mo = ded->mobjs.append(); strcpy(mo->id, idstr); return ded->mobjs.indexOf(mo); } int DED_AddState(ded_t* ded, char const* id) { ded_state_t *st = ded->states.append(); strcpy(st->id, id); return ded->states.indexOf(st); } int DED_AddSprite(ded_t* ded, char const* name) { ded_sprid_t *sp = ded->sprites.append(); strcpy(sp->id, name); return ded->sprites.indexOf(sp); } int DED_AddLight(ded_t* ded, char const* stateid) { ded_light_t* light = ded->lights.append(); strcpy(light->state, stateid); return ded->lights.indexOf(light); } int DED_AddSound(ded_t* ded, char const* id) { ded_sound_t* snd = ded->sounds.append(); strcpy(snd->id, id); return ded->sounds.indexOf(snd); } int DED_AddText(ded_t* ded, char const* id) { ded_text_t* txt = ded->text.append(); strcpy(txt->id, id); return ded->text.indexOf(txt); } int DED_AddTextureEnv(ded_t* ded, char const* id) { ded_tenviron_t* env = ded->textureEnv.append(); strcpy(env->id, id); return ded->textureEnv.indexOf(env); } int DED_AddCompositeFont(ded_t* ded, char const* uri) { ded_compositefont_t* cfont = ded->compositeFonts.append(); if(uri) cfont->uri = new de::Uri(uri, RC_NULL); return ded->compositeFonts.indexOf(cfont); } int DED_AddValue(ded_t* ded, char const* id) { ded_value_t* val = ded->values.append(); if(id) { val->id = (char *) M_Malloc(strlen(id) + 1); strcpy(val->id, id); } return ded->values.indexOf(val); } int DED_AddDetail(ded_t *ded, char const *lumpname) { ded_detailtexture_t *dtl = ded->details.append(); // Default usage is allowed with custom textures and external replacements. dtl->flags = DTLF_PWAD|DTLF_EXTERNAL; if(lumpname && lumpname[0]) { dtl->stage.texture = new de::Uri(lumpname, RC_NULL); } dtl->stage.scale = 1; dtl->stage.strength = 1; return ded->details.indexOf(dtl); } int DED_AddPtcGen(ded_t* ded, char const* state) { ded_ptcgen_t* gen = ded->ptcGens.append(); strcpy(gen->state, state); // Default choice (use either submodel zero or one). gen->subModel = -1; return ded->ptcGens.indexOf(gen); } int DED_AddPtcGenStage(ded_ptcgen_t* gen) { ded_ptcstage_t* stage = gen->stages.append(); stage->model = -1; stage->sound.volume = 1; stage->hitSound.volume = 1; return gen->stages.indexOf(stage); } int DED_AddReflection(ded_t *ded) { ded_reflection_t *ref = ded->reflections.append(); // Default usage is allowed with custom textures and external replacements. ref->flags = REFF_PWAD|REFF_EXTERNAL; // Init to defaults. ref->stage.shininess = 1.0f; ref->stage.blendMode = BM_ADD; ref->stage.maskWidth = 1.0f; ref->stage.maskHeight = 1.0f; return ded->reflections.indexOf(ref); } int DED_AddGroup(ded_t* ded) { ded_group_t* group = ded->groups.append(); return ded->groups.indexOf(group); } int DED_AddGroupMember(ded_group_t* grp) { ded_group_member_t* memb = grp->members.append(); return grp->members.indexOf(memb); } int DED_AddSectorType(ded_t* ded, int id) { ded_sectortype_t* sec = ded->sectorTypes.append(); sec->id = id; return ded->sectorTypes.indexOf(sec); } int DED_AddLineType(ded_t* ded, int id) { ded_linetype_t* li = ded->lineTypes.append(); li->id = id; //li->actCount = -1; return ded->lineTypes.indexOf(li); } int ded_s::getMobjNum(char const *id) const { int i; if(!id || !id[0]) return -1; for(i = 0; i < mobjs.size(); ++i) if(!qstricmp(mobjs[i].id, id)) return i; return -1; } int ded_s::getMobjNumForName(const char *name) const { int i; if(!name || !name[0]) return -1; for(i = mobjs.size() - 1; i >= 0; --i) if(!qstricmp(mobjs[i].name, name)) return i; return -1; } char const *ded_s::getMobjName(int num) const { if(num < 0) return "(<0)"; if(num >= mobjs.size()) return "(>mobjtypes)"; return mobjs[num].id; } int ded_s::getStateNum(String const &id) const { return getStateNum(id.toLatin1().constData()); } int ded_s::getStateNum(char const *id) const { int idx = -1; if(id && id[0] && states.size()) { int i = 0; do { if(!qstricmp(states[i].id, id)) idx = i; } while(idx == -1 && ++i < states.size()); } return idx; } int ded_s::evalFlags2(char const *ptr) const { LOG_AS("Def_EvalFlags"); int value = 0; while(*ptr) { ptr = M_SkipWhite(const_cast(ptr)); int flagNameLength = M_FindWhite(const_cast(ptr)) - ptr; String flagName(ptr, flagNameLength); ptr += flagNameLength; if(Record const *flag = flags.tryFind("id", flagName.toLower())) { value |= flag->geti("value"); } else { LOG_RES_WARNING("Flag '%s' is not defined (or used out of context)") << flagName; } } return value; } int ded_s::getEpisodeNum(String const &id) const { if(Record const *def = episodes.tryFind("id", id)) { return def->geti("__order__"); } return -1; } int ded_s::getMapInfoNum(de::Uri const &uri) const { if(Record const *def = mapInfos.tryFind("id", uri.compose())) { return def->geti("__order__"); } return -1; // Not found. } int ded_s::getMaterialNum(de::Uri const &uri) const { if(uri.isEmpty()) return -1; // Not found. if(uri.scheme().isEmpty()) { // Caller doesn't care which scheme - use a priority search order. de::Uri temp(uri); temp.setScheme("Sprites"); int idx = getMaterialNum(temp); if(idx >= 0) return idx; temp.setScheme("Textures"); idx = getMaterialNum(temp); if(idx >= 0) return idx; temp.setScheme("Flats"); idx = getMaterialNum(temp); /*if(idx >= 0)*/ return idx; } if(Record const *def = materials.tryFind("id", uri.compose())) { return def->geti("__order__"); } return -1; // Not found. } int ded_s::getModelNum(const char *id) const { if(Record const *def = models.tryFind("id", id)) { return def->geti("__order__"); } return -1; /* int idx = -1; if(id && id[0] && !models.empty()) { int i = 0; do { if(!qstricmp(models[i].id, id)) idx = i; } while(idx == -1 && ++i < (int)models.size()); } return idx;*/ } int ded_s::getSkyNum(char const *id) const { if(Record const *def = skies.tryFind("id", id)) { return def->geti("__order__"); } return -1; /*if(!id || !id[0]) return -1; for(int i = skies.size() - 1; i >= 0; i--) { if(!qstricmp(skies[i].id, id)) return i; } return -1;*/ } int ded_s::getSoundNum(const char *id) const { int idx = -1; if(id && id[0] && sounds.size()) { int i = 0; do { if(!qstricmp(sounds[i].id, id)) idx = i; } while(idx == -1 && ++i < sounds.size()); } return idx; } int ded_s::getSoundNumForName(const char *name) const { if(!name || !name[0]) return -1; for(int i = 0; i < sounds.size(); ++i) if(!qstricmp(sounds[i].name, name)) return i; return 0; } int ded_s::getMusicNum(const char* id) const { if(Record const *def = musics.tryFind("id", id)) { return def->geti("__order__"); } return -1; /*int idx = -1; if(id && id[0] && musics.size()) { int i = 0; do { if(!qstricmp(musics[i].id, id)) idx = i; } while(idx == -1 && ++i < musics.size()); } return idx;*/ } ded_value_t* ded_s::getValueById(char const* id) const { if(!id || !id[0]) return NULL; // Read backwards to allow patching. for(int i = values.size() - 1; i >= 0; i--) { if(!qstricmp(values[i].id, id)) return &values[i]; } return 0; } ded_value_t* ded_s::getValueByUri(de::Uri const &uri) const { if(uri.scheme().compareWithoutCase("Values")) return 0; return getValueById(uri.pathCStr()); } ded_compositefont_t* ded_s::findCompositeFontDef(de::Uri const& uri) const { for(int i = compositeFonts.size() - 1; i >= 0; i--) { ded_compositefont_t* def = &compositeFonts[i]; if(!def->uri || uri != *def->uri) continue; return def; } return 0; } ded_compositefont_t* ded_s::getCompositeFont(char const* uriCString) const { ded_compositefont_t* def = NULL; if(uriCString && uriCString[0]) { de::Uri uri(uriCString, RC_NULL); if(uri.scheme().isEmpty()) { // Caller doesn't care which scheme - use a priority search order. de::Uri temp(uri); temp.setScheme("Game"); def = findCompositeFontDef(temp); if(!def) { temp.setScheme("System"); def = findCompositeFontDef(temp); } } if(!def) { def = findCompositeFontDef(uri); } } return def; } int ded_s::getTextNum(char const *id) const { if(id && id[0]) { // Search in reverse insertion order to allow patching. for(int i = text.size() - 1; i >= 0; i--) { if(!qstricmp(text[i].id, id)) return i; } } return -1; // Not found. } doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/mapgraphnode.cpp0000664000175000017500000000366512641367671025215 0ustar jaakkojaakko/** @file mapgraphnode.cpp MapGraphNode definition accessor. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/defs/mapgraphnode.h" #include "doomsday/defs/ded.h" #include #include using namespace de; namespace defn { void MapGraphNode::resetToDefaults() { Definition::resetToDefaults(); // Add all expected fields with their default values. def().addText ("id", ""); def().addNumber("warpNumber", 0); def().addArray ("exit", new ArrayValue); } Record &MapGraphNode::addExit() { Record *exit = new Record; exit->addBoolean("custom", false); exit->addText("id", ""); exit->addText("targetMap", ""); def()["exit"].value() .add(new RecordValue(exit, RecordValue::OwnsRecord)); return *exit; } int MapGraphNode::exitCount() const { return int(geta("exit").size()); } bool MapGraphNode::hasExit(int index) const { return index >= 0 && index < exitCount(); } Record &MapGraphNode::exit(int index) { return *def().geta("exit")[index].as().record(); } Record const &MapGraphNode::exit(int index) const { return *geta("exit")[index].as().record(); } } // namespace defn doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/dedparser.cpp0000664000175000017500000033671112641367671024522 0ustar jaakkojaakko/** @file dedparser.cpp Doomsday Engine Definition File Reader. * * A GHASTLY MESS!!! This should be rewritten. * * You have been warned! * * At the moment the idea is that a lot of macros are used to read a more * or less fixed structure of definitions, and if an error occurs then * "goto out_of_here;". It leads to a lot more code than an elegant parser * would require. * * The current implementation of the reader is a "structural" approach: * the definition file is parsed based on the structure implied by * the read tokens. A true parser would have syntax definitions for * a bunch of tokens, and the same parsing rules would be applied for * everything. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2010-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #define DENG_NO_API_MACROS_URI #include "doomsday/defs/dedparser.h" #include #include #include #include #include #include #include #include #include #include #include #include "doomsday/defs/decoration.h" #include "doomsday/defs/ded.h" #include "doomsday/defs/dedfile.h" #include "doomsday/defs/episode.h" #include "doomsday/defs/finale.h" #include "doomsday/defs/mapgraphnode.h" #include "doomsday/defs/mapinfo.h" #include "doomsday/defs/material.h" #include "doomsday/defs/model.h" #include "doomsday/defs/music.h" #include "doomsday/defs/sky.h" #include "doomsday/filesys/fs_main.h" #include "doomsday/filesys/fs_util.h" #include "doomsday/filesys/sys_direc.h" #include "doomsday/uri.h" #include "xgclass.h" #ifdef WIN32 # define stricmp _stricmp #endif using namespace de; #define MAX_RECUR_DEPTH 30 #define MAX_TOKEN_LEN 128 // Some macros. #define STOPCHAR(x) (isspace(x) || x == ';' || x == '#' || x == '{' \ || x == '}' || x == '=' || x == '"' || x == '*' \ || x == '|') #define ISTOKEN(X) (!stricmp(token, X)) #define READSTR(X) if(!ReadString(X, sizeof(X))) { \ setError("Syntax error in string value."); \ retVal = false; goto ded_end_read; } #define READURI(X, SHM) if(!ReadUri(X, SHM)) { \ setError("Syntax error parsing resource path."); \ retVal = false; goto ded_end_read; } #define MISSING_SC_ERROR setError("Missing semicolon."); \ retVal = false; goto ded_end_read; #define CHECKSC if(source->version <= 5) { ReadToken(); if(!ISTOKEN(";")) { MISSING_SC_ERROR; } } #define FINDBEGIN while(!ISTOKEN("{") && !source->atEnd) ReadToken(); #define FINDEND while(!ISTOKEN("}") && !source->atEnd) ReadToken(); #define ISLABEL(X) (!stricmp(label, X)) #define READLABEL if(!ReadLabel(label)) { retVal = false; goto ded_end_read; } \ if(ISLABEL("}")) break; #define READLABEL_NOBREAK if(!ReadLabel(label)) { retVal = false; goto ded_end_read; } #define FAILURE retVal = false; goto ded_end_read; #define READBYTE(X) if(!ReadByte(&X)) { FAILURE } #define READINT(X) if(!ReadInt(&X, false)) { FAILURE } #define READUINT(X) if(!ReadInt(&X, true)) { FAILURE } #define READFLT(X) if(!ReadFloat(&X)) { FAILURE } #define READNBYTEVEC(X,N) if(!ReadNByteVector(X, N)) { FAILURE } #define READFLAGS(X,P) if(!ReadFlags(&X, P)) { FAILURE } #define READBLENDMODE(X) if(!ReadBlendmode(&X)) { FAILURE } #define READSTRING(S,I) if(!ReadString(S, sizeof(S))) { I = strtol(token,0,0); } #define READVECTOR(X,N) if(!ReadVector(X, N)) { FAILURE } #define RV_BYTE(lab, X) if(ISLABEL(lab)) { READBYTE(X); } else #define RV_INT(lab, X) if(ISLABEL(lab)) { READINT(X); } else #define RV_UINT(lab, X) if(ISLABEL(lab)) { READUINT(X); } else #define RV_FLT(lab, X) if(ISLABEL(lab)) { READFLT(X); } else #define RV_VEC(lab, X, N) if(ISLABEL(lab)) { int b; FINDBEGIN; \ for(b=0; b struct Dummy : public T { Dummy() { zap(*this); } ~Dummy() { T::release(); } void clear() { T::release(); zap(*this); } }; void DED_SetXGClassLinks(struct xgclass_s *links) { xgClassLinks = links; } DENG2_PIMPL(DEDParser) { ded_t *ded; struct dedsource_s { char const *buffer; char const *pos; dd_bool atEnd; int lineNumber; String fileName; int version; ///< v6 does not require semicolons. bool custom; ///< @c true= source is a user supplied add-on. }; typedef dedsource_s dedsource_t; dedsource_t sourceStack[MAX_RECUR_DEPTH]; dedsource_t *source; // Points to the current source. char token[MAX_TOKEN_LEN+1]; char unreadToken[MAX_TOKEN_LEN+1]; Instance(Public *i) : Base(i), ded(0), source(0) { zap(token); zap(unreadToken); } void DED_InitReader(char const *buffer, String fileName, bool sourceIsCustom) { if(source && source - sourceStack >= MAX_RECUR_DEPTH) { App_FatalError("DED_InitReader: Include recursion is too deep.\n"); } if(!source) { // This'll be the first source. source = sourceStack; } else { // Take the next entry in the stack. source++; } source->pos = source->buffer = buffer; source->atEnd = false; source->lineNumber = 1; source->fileName = fileName; source->version = DED_VERSION; source->custom = sourceIsCustom; } void DED_CloseReader() { if(source == sourceStack) { source = 0; } else { source--; } } String readPosAsText() { return "\"" + (source? source->fileName : "[buffered-data]") + "\"" " on line #" + String::number(source? source->lineNumber : 0); } void setError(String const &message) { DED_SetError("In " + readPosAsText() + "\n " + message); } /** * Reads a single character from the input file. Increments the line * number counter if necessary. */ int FGetC(void) { int ch = (unsigned char) *source->pos; if(ch) source->pos++; else source->atEnd = true; if(ch == '\n') source->lineNumber++; if(ch == '\r') return FGetC(); return ch; } /** * Undoes an FGetC. */ int FUngetC(int ch) { if(source->atEnd) return 0; if(ch == '\n') source->lineNumber--; if(source->pos > source->buffer) source->pos--; return ch; } /** * Reads stuff until a newline is found. */ void SkipComment(void) { int ch = FGetC(); dd_bool seq = false; if(ch == '\n') return; // Comment ends right away. if(ch != '>') // Single-line comment? { while(FGetC() != '\n' && !source->atEnd) {} } else // Multiline comment? { while(!source->atEnd) { ch = FGetC(); if(seq) { if(ch == '#') break; seq = false; } if(ch == '<') seq = true; } } } int ReadToken(void) { int ch; char* out = token; // Has a token been unread? if(unreadToken[0]) { strcpy(token, unreadToken); strcpy(unreadToken, ""); return true; } ch = FGetC(); if(source->atEnd) return false; // Skip whitespace and comments in the beginning. while((ch == '#' || isspace(ch))) { if(ch == '#') SkipComment(); ch = FGetC(); if(source->atEnd) return false; } // Always store the first character. *out++ = ch; if(STOPCHAR(ch)) { // Stop here. *out = 0; return true; } while(!STOPCHAR(ch) && !source->atEnd) { // Store the character in the buffer. ch = FGetC(); *out++ = ch; } *(out - 1) = 0; // End token. // Put the last read character back in the stream. FUngetC(ch); return true; } void UnreadToken(const char* token) { // The next time ReadToken() is called, this is returned. strcpy(unreadToken, token); } /** * Current pos in the file is at the first ". * Does not expand escape sequences, only checks for \". * * @return @c true if successful. */ int ReadString(String &dest, bool inside = false, bool doubleq = false) { if(!inside) { ReadToken(); if(!ISTOKEN("\"")) return false; } bool esc = false, newl = false; // Start reading the characters. int ch = FGetC(); while(esc || ch != '"') // The string-end-character. { if(source->atEnd) return false; // If a newline is found, skip all whitespace that follows. if(newl) { if(isspace(ch)) { ch = FGetC(); continue; } else { // End the skip. newl = false; } } // An escape character? if(!esc && ch == '\\') { esc = true; } else { // In case it's something other than \" or \\, just insert // the whole sequence as-is. if(esc && ch != '"' && ch != '\\') dest += '\\'; esc = false; } if(ch == '\n') newl = true; // Store the character in the buffer. if(!esc && !newl) { dest += char(ch); if(doubleq && ch == '"') dest += '"'; } // Read the next character, please. ch = FGetC(); } return true; } int ReadString(char *dest, int maxLen) { DENG_ASSERT(dest != 0); String buffer; if(!ReadString(buffer)) return false; qstrncpy(dest, buffer.toUtf8().constData(), maxLen); return true; } int ReadString(Variable &var, int) { String buffer; if(!ReadString(buffer)) return false; var.set(TextValue(buffer)); return true; } /** * Read a string of (pretty much) any length. */ int ReadAnyString(char **dest) { String buffer; if(!ReadString(buffer)) return false; // Get rid of the old string. if(*dest) M_Free(*dest); // Make a copy. QByteArray bufferUtf8 = buffer.toUtf8(); *dest = (char *) M_Malloc(bufferUtf8.length() + 1); qstrcpy(*dest, bufferUtf8.constData()); return true; } int ReadUri(de::Uri **dest, char const *defaultScheme) { String buffer; if(!ReadString(buffer)) return false; // URIs are expected to use forward slashes. buffer = Path::normalizeString(buffer); if(!*dest) *dest = new de::Uri(buffer, RC_NULL); else (*dest)->setUri(buffer, RC_NULL); if(defaultScheme && defaultScheme[0] && (*dest)->scheme().isEmpty()) (*dest)->setScheme(defaultScheme); return true; } int ReadUri(Variable &var, char const *defaultScheme) { de::Uri *uri = 0; if(!ReadUri(&uri, defaultScheme)) return false; var.set(TextValue(uri->compose())); delete uri; return true; } int ReadNByteVector(Variable &var, int count) { FINDBEGIN; for(int i = 0; i < count; ++i) { ReadToken(); if(ISTOKEN("}")) return true; var.value().setElement(i, strtoul(token, 0, 0)); } FINDEND; return true; } /** * @return @c true, if successful. */ int ReadByte(unsigned char* dest) { ReadToken(); if(ISTOKEN(";")) { setError("Missing integer value"); return false; } *dest = strtoul(token, 0, 0); return true; } /** * @return @c true, if successful. */ int ReadInt(int* dest, int unsign) { ReadToken(); if(ISTOKEN(";")) { setError("Missing integer value"); return false; } *dest = unsign? (int)strtoul(token, 0, 0) : strtol(token, 0, 0); return true; } int ReadInt(Variable *var, int unsign) { int value = 0; if(ReadInt(&value, unsign)) { var->set(NumberValue(value)); return true; } return false; } /** * @return @c true, if successful. */ int ReadFloat(float* dest) { ReadToken(); if(ISTOKEN(";")) { setError("Missing float value"); return false; } *dest = (float) strtod(token, 0); return true; } int ReadFloat(Variable *var) { float value = 0; if(ReadFloat(&value)) { var->set(NumberValue(value)); return true; } return false; } int ReadVector(Variable &var, int componentCount) { FINDBEGIN; for(int b = 0; b < componentCount; ++b) { float value = 0; if(!ReadFloat(&value)) return false; var.value().setElement(b, value); } ReadToken(); return true; } int ReadFlags(int *dest, char const *prefix) { DENG2_ASSERT(dest); // By default, no flags are set. *dest = 0; ReadToken(); if(ISTOKEN(";")) { setError("Missing flags value"); return false; } if(ISTOKEN("0")) { // No flags defined. return true; } UnreadToken(token); String flag; if(ISTOKEN("\"")) { // The old format. if(!ReadString(flag)) return false; flag.strip(); if(!flag.isEmpty()) { *dest = ded->evalFlags2(flag.toUtf8().constData()); } return true; } forever { // Read the flag. ReadToken(); if(prefix) { flag = String(prefix) + String(token); } else { flag = String(token); } flag.strip(); if(!flag.isEmpty()) { *dest |= ded->evalFlags2(flag.toUtf8().constData()); } if(!ReadToken()) break; if(!ISTOKEN("|")) // | is required for multiple flags. { UnreadToken(token); break; } } return true; } int ReadFlags(Variable *dest, char const *prefix) { int value = 0; if(ReadFlags(&value, prefix)) { dest->set(NumberValue(value, NumberValue::Hex)); return true; } return false; } int ReadBlendmode(blendmode_t *dest) { LOG_AS("ReadBlendmode"); String flag; blendmode_t bm; ReadToken(); UnreadToken(token); if(ISTOKEN("\"")) { // The old format. if(!ReadString(flag)) return false; bm = blendmode_t(ded->evalFlags2(flag.toUtf8().constData())); } else { // Read the blendmode. ReadToken(); flag = String("bm_") + String(token); bm = blendmode_t(ded->evalFlags2(flag.toUtf8().constData())); } if(bm != BM_NORMAL) { *dest = bm; } else { LOG_RES_WARNING("Unknown BlendMode '%s' in \"%s\" on line #%i") << flag << (source ? source->fileName : "?") << (source ? source->lineNumber : 0); } return true; } int ReadBlendmode(Variable *var) { blendmode_t mode = BM_NORMAL; if(!ReadBlendmode(&mode)) return false; var->set(NumberValue(int(mode))); return true; } /** * @return @c true, if successful. */ int ReadLabel(char* label) { strcpy(label, ""); for(;;) { ReadToken(); if(source->atEnd) { setError("Unexpected end of file"); return false; } if(ISTOKEN("}")) // End block. { strcpy(label, token); return true; } if(ISTOKEN(";")) { if(source->version <= 5) { setError("Label without value"); return false; } continue; // Semicolons are optional in v6. } if(ISTOKEN("=") || ISTOKEN("{")) break; if(label[0]) strcat(label, " "); strcat(label, token); } return true; } /** * @param cond Condition token. Can be a command line option * or a game mode. * @return @c true if the condition passes. */ dd_bool DED_CheckCondition(char const *cond, dd_bool expected) { dd_bool value = false; if(cond[0] == '-') { // A command line option. value = (CommandLine_Check(token) != 0); } else if(isalnum(cond[0]) && !App::game().isNull()) { // A game mode. value = !String(cond).compareWithoutCase(App::game().id()); } return value == expected; } int readData(char const *buffer, String sourceFile, bool sourceIsCustom) { char dummy[128], label[128], tmp[256]; int dummyInt, idx, retVal = true; int prevEpisodeDefIdx = -1; // For "Copy". int prevMobjDefIdx = -1; // For "Copy". int prevStateDefIdx = -1; // For "Copy" int prevLightDefIdx = -1; // For "Copy". int prevMaterialDefIdx = -1; // For "Copy". int prevModelDefIdx = -1; // For "Copy". int prevMapInfoDefIdx = -1; // For "Copy". int prevMusicDefIdx = -1; // For "Copy". int prevSkyDefIdx = -1; // For "Copy". int prevDetailDefIdx = -1; // For "Copy". int prevGenDefIdx = -1; // For "Copy". int prevDecorDefIdx = -1; // For "Copy". int prevRefDefIdx = -1; // For "Copy". int prevLineTypeDefIdx = -1; // For "Copy". int prevSectorTypeDefIdx = -1; // For "Copy". int depth; char *rootStr = 0, *ptr; int bCopyNext = 0; // Get the next entry from the source stack. DED_InitReader(buffer, sourceFile, sourceIsCustom); // For including other files -- we must know where we are. String sourceFileDir = sourceFile.fileNamePath(); if(sourceFileDir.isEmpty()) { sourceFileDir = NativePath::workPath(); } while(ReadToken()) { if(ISTOKEN("Copy") || ISTOKEN("*")) { bCopyNext = true; continue; // Read the next token. } if(ISTOKEN(";")) { // Unnecessary semicolon? Just skip it. continue; } if(ISTOKEN("SkipIf")) { dd_bool expected = true; ReadToken(); if(ISTOKEN("Not")) { expected = false; ReadToken(); } if(DED_CheckCondition(token, expected)) { // Ah, we're done. Get out of here. goto ded_end_read; } CHECKSC; } if(ISTOKEN("Include")) { // A new include. READSTR(tmp); CHECKSC; DED_Include(tmp, sourceFileDir); strcpy(label, ""); } if(ISTOKEN("IncludeIf")) // An optional include. { dd_bool expected = true; ReadToken(); if(ISTOKEN("Not")) { expected = false; ReadToken(); } if(DED_CheckCondition(token, expected)) { READSTR(tmp); CHECKSC; DED_Include(tmp, sourceFileDir); strcpy(label, ""); } else { // Arg not found; just skip it. READSTR(tmp); CHECKSC; } } if(ISTOKEN("ModelPath")) { READSTR(label); CHECKSC; de::Uri newSearchPath = de::Uri::fromNativeDirPath(NativePath(label)); FS1::Scheme& scheme = App_FileSystem().scheme(ResourceClass::classForId(RC_MODEL).defaultScheme()); scheme.addSearchPath(reinterpret_cast(newSearchPath), FS1::ExtraPaths); } if(ISTOKEN("Header")) { FINDBEGIN; for(;;) { READLABEL; if(ISLABEL("Version")) { READINT(ded->version); source->version = ded->version; } else RV_STR("Thing prefix", dummy) RV_STR("State prefix", dummy) RV_STR("Sprite prefix", dummy) RV_STR("Sfx prefix", dummy) RV_STR("Mus prefix", dummy) RV_STR("Text prefix", dummy) RV_STR("Model path", dummy) RV_FLAGS("Common model flags", ded->modelFlags, "df_") RV_FLT("Default model scale", ded->modelScale) RV_FLT("Default model offset", ded->modelOffset) RV_END CHECKSC; } } if(ISTOKEN("Flag")) { ded_stringid_t id; int value = 0; char dummyStr[2]; de::zap(id); FINDBEGIN; forever { READLABEL; RV_STR("ID", id) RV_UINT("Value", value) RV_STR("Info", dummyStr) // ignored RV_END CHECKSC; } if(qstrlen(id)) { ded->addFlag(id, value); Record &flag = ded->flags.find("id", id); DENG2_ASSERT(flag.geti("value") == value); // Sanity check. if(source->custom) flag.set("custom", true); } } if(ISTOKEN("Episode")) { bool bModify = false; Record dummyEpsd; Record *epsd = 0; ReadToken(); if(!ISTOKEN("Mods")) { // New episodes are appended to the end of the list. idx = ded->addEpisode(); epsd = &ded->episodes[idx]; } else if(!bCopyNext) { ded_stringid_t otherEpisodeId; READSTR(otherEpisodeId); ReadToken(); idx = ded->getEpisodeNum(otherEpisodeId); if(idx >= 0) { epsd = &ded->episodes[idx]; bModify = true; } else { LOG_RES_WARNING("Ignoring unknown Episode \"%s\" in %s on line #%i") << otherEpisodeId << (source? source->fileName : "?") << (source? source->lineNumber : 0); // We'll read into a dummy definition. defn::Episode(dummyEpsd).resetToDefaults(); epsd = &dummyEpsd; } } else { setError("Cannot both Copy(Previous) and Modify"); retVal = false; goto ded_end_read; } DENG2_ASSERT(epsd != 0); if(prevEpisodeDefIdx >= 0 && bCopyNext) { ded->episodes.copy(prevEpisodeDefIdx, *epsd); } if(source->custom) epsd->set("custom", true); defn::Episode mainDef(*epsd); int hub = 0; int notHubMap = 0; FINDBEGIN; forever { READLABEL; // ID cannot be changed when modifying if(!bModify && ISLABEL("ID")) { READSTR((*epsd)["id"]); } else RV_URI("Start Map", (*epsd)["startMap"], "Maps") RV_STR("Title", (*epsd)["title"]) RV_STR("Menu Help Info", (*epsd)["menuHelpInfo"]) RV_URI("Menu Image", (*epsd)["menuImage"], "Patches") RV_STR("Menu Shortcut", (*epsd)["menuShortcut"]) if(ISLABEL("Hub")) { Record *hubRec; // Add another hub. if(hub >= mainDef.hubCount()) { mainDef.addHub(); } DENG_ASSERT(hub < mainDef.hubCount()); hubRec = &mainDef.hub(hub); if(source->custom) hubRec->set("custom", true); int map = 0; FINDBEGIN; forever { READLABEL; RV_STR("ID", (*hubRec)["id"]) if(ISLABEL("Map")) { // Add another map. if(map >= int(hubRec->geta("map").size())) { QScopedPointer map(new Record); defn::MapGraphNode(*map).resetToDefaults(); (*hubRec)["map"].value() .add(new RecordValue(map.take(), RecordValue::OwnsRecord)); } DENG_ASSERT(map < int(hubRec->geta("map").size())); Record &mapRec = *hubRec->geta("map")[map].as().record(); if(source->custom) mapRec.set("custom", true); int exit = 0; FINDBEGIN; forever { READLABEL; RV_URI("ID", mapRec["id"], "Maps") RV_INT("Warp Number", mapRec["warpNumber"]) if(ISLABEL("Exit")) { defn::MapGraphNode mgNodeDef(mapRec); // Add another exit. if(exit >= mgNodeDef.exitCount()) { mgNodeDef.addExit(); } DENG_ASSERT(exit < mgNodeDef.exitCount()); Record &exitRec = mgNodeDef.exit(exit); if(source->custom) exitRec.set("custom", true); FINDBEGIN; forever { READLABEL; RV_STR("ID", exitRec["id"]) RV_URI("Target Map", exitRec["targetMap"], "Maps") RV_END CHECKSC; } exit++; } else RV_END CHECKSC; } map++; } else RV_END CHECKSC; } hub++; } else if(ISLABEL("Map")) { // Add another none-hub map. if(notHubMap >= int(epsd->geta("map").size())) { QScopedPointer map(new Record); defn::MapGraphNode(*map).resetToDefaults(); (*epsd)["map"].value() .add(new RecordValue(map.take(), RecordValue::OwnsRecord)); } DENG_ASSERT(notHubMap < int(epsd->geta("map").size())); Record &mapRec = *epsd->geta("map")[notHubMap].as().record(); if(source->custom) mapRec.set("custom", true); int exit = 0; FINDBEGIN; forever { READLABEL; RV_URI("ID", mapRec["id"], "Maps") RV_INT("Warp Number", mapRec["warpNumber"]) if(ISLABEL("Exit")) { defn::MapGraphNode mgNodeDef(mapRec); // Add another exit. if(exit >= mgNodeDef.exitCount()) { mgNodeDef.addExit(); } DENG_ASSERT(exit < mgNodeDef.exitCount()); Record &exitRec = mgNodeDef.exit(exit); if(source->custom) exitRec.set("custom", true); FINDBEGIN; forever { READLABEL; RV_STR("ID", exitRec["id"]) RV_URI("Target Map", exitRec["targetMap"], "Maps") RV_END CHECKSC; } exit++; } else RV_END CHECKSC; } notHubMap++; } else RV_END CHECKSC; } // If we did not read into a dummy update the previous index. if(idx > 0) { prevEpisodeDefIdx = idx; } } if(ISTOKEN("Mobj") || ISTOKEN("Thing")) { dd_bool bModify = false; ded_mobj_t* mo; Dummy dummyMo; ReadToken(); if(!ISTOKEN("Mods")) { // A new mobj type. idx = DED_AddMobj(ded, ""); mo = &ded->mobjs[idx]; } else if(!bCopyNext) { ded_mobjid_t otherMobjId; READSTR(otherMobjId); ReadToken(); idx = ded->getMobjNum(otherMobjId); if(idx < 0) { LOG_RES_WARNING("Ignoring unknown Mobj \"%s\" in %s on line #%i") << otherMobjId << (source? source->fileName : "?") << (source? source->lineNumber : 0); // We'll read into a dummy definition. dummyMo.clear(); mo = &dummyMo; } else { mo = &ded->mobjs[idx]; bModify = true; } } else { setError("Cannot both Copy(Previous) and Modify"); retVal = false; goto ded_end_read; } if(prevMobjDefIdx >= 0 && bCopyNext) { // Should we copy the previous definition? ded->mobjs.copyTo(mo, prevMobjDefIdx); } FINDBEGIN; for(;;) { READLABEL; // ID cannot be changed when modifying if(!bModify && ISLABEL("ID")) { READSTR(mo->id); } else RV_INT("DoomEd number", mo->doomEdNum) RV_STR("Name", mo->name) RV_STR("Spawn state", mo->states[SN_SPAWN]) RV_STR("See state", mo->states[SN_SEE]) RV_STR("Pain state", mo->states[SN_PAIN]) RV_STR("Melee state", mo->states[SN_MELEE]) RV_STR("Missile state", mo->states[SN_MISSILE]) RV_STR("Crash state", mo->states[SN_CRASH]) RV_STR("Death state", mo->states[SN_DEATH]) RV_STR("Xdeath state", mo->states[SN_XDEATH]) RV_STR("Raise state", mo->states[SN_RAISE]) RV_STR("See sound", mo->seeSound) RV_STR("Attack sound", mo->attackSound) RV_STR("Pain sound", mo->painSound) RV_STR("Death sound", mo->deathSound) RV_STR("Active sound", mo->activeSound) RV_INT("Reaction time", mo->reactionTime) RV_INT("Pain chance", mo->painChance) RV_INT("Spawn health", mo->spawnHealth) RV_FLT("Speed", mo->speed) RV_FLT("Radius", mo->radius) RV_FLT("Height", mo->height) RV_INT("Mass", mo->mass) RV_INT("Damage", mo->damage) RV_FLAGS("Flags", mo->flags[0], "mf_") RV_FLAGS("Flags2", mo->flags[1], "mf2_") RV_FLAGS("Flags3", mo->flags[2], "mf3_") RV_INT("Misc1", mo->misc[0]) RV_INT("Misc2", mo->misc[1]) RV_INT("Misc3", mo->misc[2]) RV_INT("Misc4", mo->misc[3]) RV_END CHECKSC; } // If we did not read into a dummy update the previous index. if(idx > 0) { prevMobjDefIdx = idx; } } if(ISTOKEN("State")) { dd_bool bModify = false; ded_state_t* st; Dummy dummyState; ReadToken(); if(!ISTOKEN("Mods")) { // A new state. idx = DED_AddState(ded, ""); st = &ded->states[idx]; } else if(!bCopyNext) { ded_stateid_t otherStateId; READSTR(otherStateId); ReadToken(); idx = ded->getStateNum(otherStateId); if(idx < 0) { LOG_RES_WARNING("Ignoring unknown State \"%s\" in %s on line #%i") << otherStateId << (source? source->fileName : "?") << (source? source->lineNumber : 0); // We'll read into a dummy definition. dummyState.clear(); st = &dummyState; } else { st = &ded->states[idx]; bModify = true; } } else { setError("Cannot both Copy(Previous) and Modify"); retVal = false; goto ded_end_read; } if(prevStateDefIdx >= 0 && bCopyNext) { // Should we copy the previous definition? ded->states.copyTo(st, prevStateDefIdx); } FINDBEGIN; for(;;) { READLABEL; // ID cannot be changed when modifying if(!bModify && ISLABEL("ID")) { READSTR(st->id); } else if(ISLABEL("Frame")) { // Legacy state frame flags: #define FF_FULLBRIGHT 0x8000 #define FF_FRAMEMASK 0x7fff READINT(st->frame); if(st->frame & FF_FULLBRIGHT) { st->frame &= FF_FRAMEMASK; st->flags |= STF_FULLBRIGHT; } #undef FF_FRAMEMASK #undef FF_FULLBRIGHT } else RV_FLAGS("Flags", st->flags, "statef_") RV_STR("Sprite", st->sprite.id) RV_INT("Tics", st->tics) RV_STR("Action", st->action) RV_STR("Next state", st->nextState) RV_INT("Misc1", st->misc[0]) RV_INT("Misc2", st->misc[1]) RV_INT("Misc3", st->misc[2]) RV_ANYSTR("Execute", st->execute) RV_END CHECKSC; } // If we did not read into a dummy update the previous index. if(idx > 0) { prevStateDefIdx = idx; } } if(ISTOKEN("Sprite")) { // A new sprite. idx = DED_AddSprite(ded, ""); FINDBEGIN; for(;;) { READLABEL; RV_STR("ID", ded->sprites[idx].id) RV_END CHECKSC; } } if(ISTOKEN("Light")) { ded_light_t* lig; // A new light. idx = DED_AddLight(ded, ""); lig = &ded->lights[idx]; // Should we copy the previous definition? if(prevLightDefIdx >= 0 && bCopyNext) { ded->lights.copyTo(lig, prevLightDefIdx); } FINDBEGIN; for(;;) { READLABEL; RV_STR("State", lig->state) RV_STR("Map", lig->uniqueMapID) RV_FLT("X Offset", lig->offset[VX]) RV_FLT("Y Offset", lig->offset[VY]) RV_VEC("Origin", lig->offset, 3) RV_FLT("Size", lig->size) RV_FLT("Intensity", lig->size) RV_FLT("Red", lig->color[0]) RV_FLT("Green", lig->color[1]) RV_FLT("Blue", lig->color[2]) RV_VEC("Color", lig->color, 3) if(ISLABEL("Sector levels")) { int b; FINDBEGIN; for(b = 0; b < 2; ++b) { READFLT(lig->lightLevel[b]) lig->lightLevel[b] /= 255.0f; if(lig->lightLevel[b] < 0) lig->lightLevel[b] = 0; else if(lig->lightLevel[b] > 1) lig->lightLevel[b] = 1; } ReadToken(); } else RV_FLAGS("Flags", lig->flags, "lgf_") RV_URI("Top map", &lig->up, "LightMaps") RV_URI("Bottom map", &lig->down, "LightMaps") RV_URI("Side map", &lig->sides, "LightMaps") RV_URI("Flare map", &lig->flare, "LightMaps") RV_FLT("Halo radius", lig->haloRadius) RV_END CHECKSC; } prevLightDefIdx = idx; } if(ISTOKEN("Material")) { Record dummyMat; Record *mat = nullptr; bool bModify = false; ReadToken(); if(!ISTOKEN("Mods")) { // New materials are appended to the end of the list. idx = ded->addMaterial(); mat = &ded->materials[idx]; } else if(!bCopyNext) { de::Uri *otherMat = nullptr; READURI(&otherMat, nullptr); ReadToken(); idx = ded->getMaterialNum(*otherMat); if(idx >= 0) { mat = &ded->materials[idx]; bModify = true; } else { LOG_RES_WARNING("Ignoring unknown Material \"%s\" in %s on line #%i") << otherMat->asText() << (source? source->fileName : "?") << (source? source->lineNumber : 0); // We'll read into a dummy definition. defn::Material(dummyMat).resetToDefaults(); mat = &dummyMat; } delete otherMat; } else { setError("Cannot both Copy(Previous) and Modify"); retVal = false; goto ded_end_read; } DENG2_ASSERT(mat); // Should we copy the previous definition? if(prevMaterialDefIdx >= 0 && bCopyNext) { ded->materials.copy(prevMaterialDefIdx, *mat); } defn::Material mainDef(*mat); int decor = 0; int layer = 0; FINDBEGIN; forever { READLABEL; // ID cannot be changed when modifying if(!bModify && ISLABEL("ID")) { READURI((*mat)["id"], nullptr); } else RV_FLAGS("Flags", (*mat)["flags"], "matf_") if(ISLABEL("Width")) { int width; READINT(width); (*mat)["dimensions"].value().setElement(0, width); } else if(ISLABEL("Height")) { int height; READINT(height); (*mat)["dimensions"].value().setElement(1, height); } else if(ISLABEL("Layer")) { if(layer >= DED_MAX_MATERIAL_LAYERS) { setError("Too many Material.Layers"); retVal = false; goto ded_end_read; } // Add another layer. if(layer >= mainDef.layerCount()) mainDef.addLayer(); defn::MaterialLayer layerDef(mainDef.layer(layer)); int stage = 0; FINDBEGIN; forever { READLABEL; if(ISLABEL("Stage")) { // Need to allocate a new stage? if(stage >= layerDef.stageCount()) { layerDef.addStage(); if(mainDef.getb("autoGenerated") && stage > 0) { // When adding a new stage to an autogenerated material // initialize by copying values from the previous stage. layerDef.stage(stage).copyMembersFrom(layerDef.stage(stage - 1)); } } Record &st = layerDef.stage(stage); FINDBEGIN; forever { READLABEL; RV_URI("Texture", st["texture"], 0) // Default to "any" scheme. RV_INT("Tics", st["tics"]) RV_FLT("Rnd", st["variance"]) RV_VEC_VAR("Offset", st["texOrigin"], 2) RV_FLT("Glow Rnd", st["glowStrengthVariance"]) RV_FLT("Glow", st["glowStrength"]) RV_END CHECKSC; } stage++; } else RV_END CHECKSC; } layer++; } else if(ISLABEL("Light")) { if(decor >= DED_MAX_MATERIAL_DECORATIONS) { setError("Too many Material.Lights"); retVal = false; goto ded_end_read; } // Add another decoration. if(decor >= mainDef.decorationCount()) mainDef.addDecoration(); defn::MaterialDecoration decorDef(mainDef.decoration(decor)); int stage = 0; FINDBEGIN; forever { READLABEL; RV_VEC_VAR("Pattern offset", decorDef.def()["patternOffset"], 2) RV_VEC_VAR("Pattern skip", decorDef.def()["patternSkip"], 2) if(ISLABEL("Stage")) { // Need to allocate a new stage? if(stage >= decorDef.stageCount()) { decorDef.addStage(); } Record &st = decorDef.stage(stage); FINDBEGIN; forever { READLABEL; RV_INT("Tics", st["tics"]) RV_FLT("Rnd", st["variance"]) RV_VEC_VAR("Offset", st["origin"], 2) RV_FLT("Distance", st["elevation"]) RV_VEC_VAR("Color", st["color"], 3) RV_FLT("Radius", st["radius"]) RV_FLT("Halo radius", st["haloRadius"]) if(ISLABEL("Levels")) { FINDBEGIN; Vector2f levels; for(int b = 0; b < 2; ++b) { float val; READFLT(val) levels[b] = de::clamp(0.f, val / 255.0f, 1.f); } ReadToken(); st["lightLevels"] = new ArrayValue(levels); } else RV_INT("Flare texture", st["haloTextureIndex"]) RV_URI("Flare map", st["haloTexture"], "LightMaps") RV_URI("Top map", st["lightmapUp"], "LightMaps") RV_URI("Bottom map", st["lightmapDown"], "LightMaps") RV_URI("Side map", st["lightmapSide"], "LightMaps") RV_END CHECKSC; } stage++; } else RV_END CHECKSC; } decor++; } else RV_END CHECKSC; } // If we did not read into a dummy update the previous index. if(idx > 0) { prevMaterialDefIdx = idx; } } if(ISTOKEN("Model")) { Record *prevModel = 0; int sub = 0; // New models are appended to the end of the list. idx = ded->addModel(); Record &mdl = ded->models[idx]; if(prevModelDefIdx >= 0) { prevModel = &ded->models[prevModelDefIdx]; if(bCopyNext) ded->models.copy(prevModelDefIdx, mdl); } if(source->custom) mdl.set("custom", true); FINDBEGIN; forever { READLABEL; RV_STR("ID", mdl["id"]) RV_STR("State", mdl["state"]) RV_INT("Off", mdl["off"]) RV_STR("Sprite", mdl["sprite"]) RV_INT("Sprite frame", mdl["spriteFrame"]) RV_FLAGS("Group", mdl["group"], "mg_") RV_INT("Selector", mdl["selector"]) RV_FLAGS("Flags", mdl["flags"], "df_") RV_FLT("Inter", mdl["interMark"]) RV_INT("Skin tics", mdl["skinTics"]) RV_FLT("Resize", mdl["resize"]) if(ISLABEL("Scale")) { float scale; READFLT(scale); mdl["scale"] = new ArrayValue(Vector3f(scale, scale, scale)); } else RV_VEC_VAR("Scale XYZ", mdl["scale"], 3) if(ISLABEL("Offset")) { float offy; READFLT(offy); mdl["offset"].value().setElement(1, offy); } else RV_VEC_VAR("Offset XYZ", mdl["offset"], 3) RV_VEC_VAR("Interpolate", mdl["interRange"], 2) RV_FLT("Shadow radius", mdl["shadowRadius"]) if(ISLABEL("Md2") || ISLABEL("Sub")) { defn::Model mainDef(mdl); // Add another submodel. if(sub >= mainDef.subCount()) { mainDef.addSub(); } DENG_ASSERT(sub < mainDef.subCount()); Record &subDef = mainDef.sub(sub); FINDBEGIN; for(;;) { READLABEL; RV_URI("File", subDef["filename"], "Models") RV_STR("Frame", subDef["frame"]) RV_INT("Frame range", subDef["frameRange"]) RV_BLENDMODE("Blending mode", subDef["blendMode"]) RV_INT("Skin", subDef["skin"]) RV_URI("Skin file", subDef["skinFilename"], "Models") RV_INT("Skin range", subDef["skinRange"]) RV_VEC_VAR("Offset XYZ", subDef["offset"], 3) RV_FLAGS("Flags", subDef["flags"], "df_") RV_FLT("Transparent", subDef["alpha"]) RV_FLT("Parm", subDef["parm"]) RV_INT("Selskin mask", subDef["selSkinMask"]) RV_INT("Selskin shift", subDef["selSkinShift"]) RV_NBVEC("Selskins", subDef["selSkins"], 8) RV_URI("Shiny skin", subDef["shinySkin"], "Models") RV_FLT("Shiny", subDef["shiny"]) RV_VEC_VAR("Shiny color", subDef["shinyColor"], 3) RV_FLT("Shiny reaction", subDef["shinyReact"]) RV_END CHECKSC; } sub++; } else RV_END CHECKSC; } if(prevModel) { defn::Model(mdl).cleanupAfterParsing(*prevModel); } prevModelDefIdx = idx; } if(ISTOKEN("Sound")) { // A new sound. ded_sound_t* snd; idx = DED_AddSound(ded, ""); snd = &ded->sounds[idx]; FINDBEGIN; for(;;) { READLABEL; RV_STR("ID", snd->id) RV_STR("Lump", snd->lumpName) RV_STR("Name", snd->name) RV_STR("Link", snd->link) RV_INT("Link pitch", snd->linkPitch) RV_INT("Link volume", snd->linkVolume) RV_INT("Priority", snd->priority) RV_INT("Max channels", snd->channels) RV_INT("Group", snd->group) RV_FLAGS("Flags", snd->flags, "sf_") RV_URI("Ext", &snd->ext, "Sfx") RV_URI("File", &snd->ext, "Sfx") RV_URI("File name", &snd->ext, "Sfx") RV_END CHECKSC; } } if(ISTOKEN("Music")) { bool bModify = false; Record dummyMusic; Record *music = 0; ReadToken(); if(!ISTOKEN("Mods")) { // New musics are appended to the end of the list. idx = ded->addMusic(); music = &ded->musics[idx]; } else if(!bCopyNext) { ded_stringid_t otherMusicId; READSTR(otherMusicId); ReadToken(); idx = ded->getMusicNum(otherMusicId); if(idx >= 0) { music = &ded->musics[idx]; bModify = true; } else { // Don't print a warning about the translated MAPINFO definitions // (see issue #2083). if(!source || source->fileName != "[TranslatedMapInfos]") { LOG_RES_WARNING("Ignoring unknown Music \"%s\" in %s on line #%i") << otherMusicId << (source? source->fileName : "?") << (source? source->lineNumber : 0); } // We'll read into a dummy definition. defn::Music(dummyMusic).resetToDefaults(); music = &dummyMusic; } } else { setError("Cannot both Copy(Previous) and Modify"); retVal = false; goto ded_end_read; } DENG2_ASSERT(music != 0); if(prevMusicDefIdx >= 0 && bCopyNext) { ded->musics.copy(prevMusicDefIdx, *music); } if(source->custom) music->set("custom", true); FINDBEGIN; forever { READLABEL; // ID cannot be changed when modifying if(!bModify && ISLABEL("ID")) { READSTR((*music)["id"]); } else RV_STR("Name", (*music)["title"]) RV_STR("Lump", (*music)["lumpName"]) RV_URI("File name", (*music)["path"], "Music") RV_URI("File", (*music)["path"], "Music") RV_URI("Ext", (*music)["path"], "Music") // Both work. RV_INT("CD track", (*music)["cdTrack"]) RV_END CHECKSC; } // If we did not read into a dummy update the previous index. if(idx > 0) { prevMusicDefIdx = idx; } } if(ISTOKEN("Sky")) { int model = 0; // New skies are appended to the end of the list. idx = ded->addSky(); Record &sky = ded->skies[idx]; if(prevSkyDefIdx >= 0 && bCopyNext) { //Record *prevSky = &ded->skies[prevSkyDefIdx]; ded->skies.copy(prevSkyDefIdx, sky); } if(source->custom) sky.set("custom", true); FINDBEGIN; forever { READLABEL; RV_STR("ID", sky["id"]) RV_FLAGS("Flags", sky["flags"], "sif_") RV_FLT("Height", sky["height"]) RV_FLT("Horizon offset", sky["horizonOffset"]) RV_VEC_VAR("Light color", sky["color"], 3) if(ISLABEL("Layer 1") || ISLABEL("Layer 2")) { defn::Sky mainDef(sky); Record &layerDef = mainDef.layer(atoi(label+6) - 1); if(source->custom) layerDef.set("custom", true); FINDBEGIN; forever { READLABEL; RV_URI("Material", layerDef["material"], 0) RV_URI("Texture", layerDef["material"], "Textures" ) RV_FLAGS("Flags", layerDef["flags"], "slf_") RV_FLT("Offset", layerDef["offset"]) RV_FLT("Offset speed", layerDef["offsetSpeed"]) RV_FLT("Color limit", layerDef["colorLimit"]) RV_END CHECKSC; } } else if(ISLABEL("Model")) { defn::Sky mainDef(sky); if(model == 32/*MAX_SKY_MODELS*/) { // Too many! setError("Too many Sky models"); retVal = false; goto ded_end_read; } // Add another model. if(model >= mainDef.modelCount()) { mainDef.addModel(); } DENG_ASSERT(model < mainDef.modelCount()); Record &mdlDef = mainDef.model(model); if(source->custom) mdlDef.set("custom", true); FINDBEGIN; forever { READLABEL; RV_STR("ID", mdlDef["id"]) RV_INT("Layer", mdlDef["layer"]) RV_FLT("Frame interval", mdlDef["frameInterval"]) RV_FLT("Yaw", mdlDef["yaw"]) RV_FLT("Yaw speed", mdlDef["yawSpeed"]) RV_VEC_VAR("Rotate", mdlDef["rotate"], 2) RV_VEC_VAR("Offset factor", mdlDef["originOffset"], 3) RV_VEC_VAR("Color", mdlDef["color"], 4) RV_STR("Execute", mdlDef["execute"]) RV_END CHECKSC; } model++; } else RV_END CHECKSC; } prevSkyDefIdx = idx; } if(ISTOKEN("Map")) // Info { ReadToken(); if(!ISTOKEN("Info")) { setError("Unknown token 'Map" + String(token) + "'"); retVal = false; goto ded_end_read; } bool bModify = false; Record dummyMi; Record *mi = nullptr; int model = 0; ReadToken(); if(!ISTOKEN("Mods")) { // New mapinfos are appended to the end of the list. idx = ded->addMapInfo(); mi = &ded->mapInfos[idx]; } else if(!bCopyNext) { de::Uri *otherMap = nullptr; READURI(&otherMap, "Maps"); ReadToken(); idx = ded->getMapInfoNum(*otherMap); if(idx >= 0) { mi = &ded->mapInfos[idx]; bModify = true; } else { LOG_RES_WARNING("Ignoring unknown Map \"%s\" in %s on line #%i") << otherMap->asText() << (source? source->fileName : "?") << (source? source->lineNumber : 0); } delete otherMap; if(mi && ISTOKEN("if")) { bool negate = false; bool testCustom = false; forever { ReadToken(); if(ISTOKEN("{")) { break; } if(!testCustom) { if(ISTOKEN("not")) { negate = !negate; } else if(ISTOKEN("custom")) { testCustom = true; } else RV_END } else RV_END } if(testCustom) { if(mi->getb("custom") != !negate) { mi = nullptr; // skip } } else { setError("Expected condition expression to follow 'if'"); retVal = false; goto ded_end_read; } } if(!mi) { // We'll read into a dummy definition. defn::MapInfo(dummyMi).resetToDefaults(); mi = &dummyMi; } } else { setError("Cannot both Copy(Previous) and Modify"); retVal = false; goto ded_end_read; } DENG2_ASSERT(mi != 0); if(prevMapInfoDefIdx >= 0 && bCopyNext) { // Record *prevMapInfo = &ded->mapInfos[prevMapInfoDefIdx]; ded->mapInfos.copy(prevMapInfoDefIdx, *mi); } if(source->custom) mi->set("custom", true); Record &sky = mi->subrecord("sky"); if(source->custom) sky.set("custom", true); FINDBEGIN; forever { READLABEL; // ID cannot be changed when modifying if(!bModify && ISLABEL("ID")) { READURI((*mi)["id"], "Maps"); } else RV_STR("Title", (*mi)["title"]) RV_STR("Name", (*mi)["title"]) // Alias RV_URI("Title image", (*mi)["titleImage"], "Patches") RV_STR("Author", (*mi)["author"]) RV_FLAGS("Flags", (*mi)["flags"], "mif_") RV_STR("Music", (*mi)["music"]) RV_FLT("Par time", (*mi)["parTime"]) if(ISLABEL("Fog color R")) { float red; READFLT(red); (*mi)["fogColor"].value().setElement(0, red); } else if(ISLABEL("Fog color G")) { float green; READFLT(green); (*mi)["fogColor"].value().setElement(1, green); } else if(ISLABEL("Fog color B")) { float blue; READFLT(blue); (*mi)["fogColor"].value().setElement(2, blue); } else RV_FLT("Fog start", (*mi)["fogStart"]) RV_FLT("Fog end", (*mi)["fogEnd"]) RV_FLT("Fog density", (*mi)["fogDensity"]) RV_STR("Fade Table", (*mi)["fadeTable"]) RV_FLT("Ambient light", (*mi)["ambient"]) RV_FLT("Gravity", (*mi)["gravity"]) RV_STR("Execute", (*mi)["execute"]) RV_STR("Sky", (*mi)["skyId"]) RV_FLT("Sky height", sky["height"]) RV_FLT("Horizon offset", sky["horizonOffset"]) RV_VEC_VAR("Sky light color", sky["color"], 3) if(ISLABEL("Sky Layer 1") || ISLABEL("Sky Layer 2")) { defn::Sky skyDef(sky); Record &layerDef = skyDef.layer(atoi(label+10) - 1); if(source->custom) layerDef.set("custom", true); FINDBEGIN; forever { READLABEL; RV_URI("Material", layerDef["material"], 0) RV_URI("Texture", layerDef["material"], "Textures" ) RV_FLAGS("Flags", layerDef["flags"], "slf_") RV_FLT("Offset", layerDef["offset"]) RV_FLT("Offset speed", layerDef["offsetSpeed"]) RV_FLT("Color limit", layerDef["colorLimit"]) RV_END CHECKSC; } } else if(ISLABEL("Sky Model")) { defn::Sky skyDef(sky); if(model == 32/*MAX_SKY_MODELS*/) { // Too many! setError("Too many Sky models"); retVal = false; goto ded_end_read; } // Add another model. if(model >= skyDef.modelCount()) { skyDef.addModel(); } DENG_ASSERT(model < skyDef.modelCount()); Record &mdlDef = skyDef.model(model); if(source->custom) mdlDef.set("custom", true); FINDBEGIN; forever { READLABEL; RV_STR("ID", mdlDef["id"]) RV_INT("Layer", mdlDef["layer"]) RV_FLT("Frame interval", mdlDef["frameInterval"]) RV_FLT("Yaw", mdlDef["yaw"]) RV_FLT("Yaw speed", mdlDef["yawSpeed"]) RV_VEC_VAR("Rotate", mdlDef["rotate"], 2) RV_VEC_VAR("Offset factor", mdlDef["originOffset"], 3) RV_VEC_VAR("Color", mdlDef["color"], 4) RV_STR("Execute", mdlDef["execute"]) RV_END CHECKSC; } model++; } else RV_END CHECKSC; } // If we did not read into a dummy update the previous index. if(idx > 0) { prevMapInfoDefIdx = idx; } } if(ISTOKEN("Text")) { // A new text. idx = DED_AddText(ded, ""); ded_text_t *txt = &ded->text[idx]; FINDBEGIN; for(;;) { READLABEL; RV_STR("ID", txt->id) if(ISLABEL("Text")) { String buffer; if(ReadString(buffer)) { QByteArray bufferUtf8 = buffer.toUtf8(); txt->text = (char *) M_Realloc(txt->text, bufferUtf8.length() + 1); qstrcpy(txt->text, bufferUtf8.constData()); } else { setError("Syntax error in Text value"); retVal = false; goto ded_end_read; } } else RV_END CHECKSC; } } if(ISTOKEN("Texture")) { ReadToken(); if(ISTOKEN("Environment")) { // A new environment. ded_tenviron_t* tenv; idx = DED_AddTextureEnv(ded, ""); tenv = &ded->textureEnv[idx]; FINDBEGIN; for(;;) { ded_uri_t *mn; READLABEL; RV_STR("ID", tenv->id) if(ISLABEL("Material") || ISLABEL("Texture") || ISLABEL("Flat")) { // A new material path. ddstring_t schemeName; Str_Init(&schemeName); Str_Set(&schemeName, ISLABEL("Material")? "" : ISLABEL("Texture")? "Textures" : "Flats"); mn = tenv->materials.append(); FINDBEGIN; for(;;) { READLABEL; RV_URI("ID", &mn->uri, Str_Text(&schemeName)) RV_END CHECKSC; } Str_Free(&schemeName); } else RV_END CHECKSC; } } } if(ISTOKEN("Composite")) { ReadToken(); if(ISTOKEN("BitmapFont")) { ded_compositefont_t* cfont; idx = DED_AddCompositeFont(ded, NULL); cfont = &ded->compositeFonts[idx]; FINDBEGIN; for(;;) { READLABEL; if(ISLABEL("ID")) { READURI(&cfont->uri, "Game") CHECKSC; } else if(M_IsStringValidInt(label)) { ded_compositefont_mappedcharacter_t* mc = 0; int ascii = atoi(label); if(ascii < 0 || ascii > 255) { setError("Invalid ascii code"); retVal = false; goto ded_end_read; } for(int i = 0; i < cfont->charMap.size(); ++i) { if(cfont->charMap[i].ch == (unsigned char) ascii) mc = &cfont->charMap[i]; } if(mc == 0) { mc = cfont->charMap.append(); mc->ch = ascii; } FINDBEGIN; for(;;) { READLABEL; RV_URI("Texture", &mc->path, "Patches") RV_END CHECKSC; } } else RV_END } } } if(ISTOKEN("Values")) // String Values { depth = 0; rootStr = (char*) M_Calloc(1); // A null string. FINDBEGIN; for(;;) { // Get the next label but don't stop on }. READLABEL_NOBREAK; if(strchr(label, '|')) { setError("Value labels can not include '|' characters (ASCII 124)"); retVal = false; goto ded_end_read; } if(ISTOKEN("=")) { // Define a new string. String buffer; if(ReadString(buffer)) { // Get a new value entry. idx = DED_AddValue(ded, 0); ded_value_t *val = &ded->values[idx]; QByteArray bufferUtf8 = buffer.toUtf8(); val->text = (char *) M_Malloc(bufferUtf8.length() + 1); qstrcpy(val->text, bufferUtf8.constData()); // Compose the identifier. val->id = (char *) M_Malloc(strlen(rootStr) + strlen(label) + 1); strcpy(val->id, rootStr); strcat(val->id, label); } else { setError("Syntax error in Value string"); retVal = false; goto ded_end_read; } } else if(ISTOKEN("{")) { // Begin a new group. rootStr = (char*) M_Realloc(rootStr, strlen(rootStr) + strlen(label) + 2); strcat(rootStr, label); strcat(rootStr, "|"); // The separator. // Increase group level. depth++; continue; } else if(ISTOKEN("}")) { size_t len; // End group. if(!depth) break; // End root depth. // Decrease level and modify rootStr. depth--; len = strlen(rootStr); rootStr[len-1] = 0; // Remove last |. ptr = strrchr(rootStr, '|'); if(ptr) { ptr[1] = 0; rootStr = (char*) M_Realloc(rootStr, strlen(rootStr) + 1); } else { // Back to level zero. rootStr = (char*) M_Realloc(rootStr, 1); *rootStr = 0; } } else { // Only the above characters are allowed. setError("Illegal token"); retVal = false; goto ded_end_read; } CHECKSC; } M_Free(rootStr); rootStr = 0; } if(ISTOKEN("Detail")) // Detail Texture { idx = DED_AddDetail(ded, ""); ded_detailtexture_t *dtl = &ded->details[idx]; // Should we copy the previous definition? if(prevDetailDefIdx >= 0 && bCopyNext) { ded->details.copyTo(dtl, prevDetailDefIdx); } FINDBEGIN; for(;;) { READLABEL; RV_FLAGS("Flags", dtl->flags, "dtf_") if(ISLABEL("Texture")) { READURI(&dtl->material1, "Textures") } else if(ISLABEL("Wall")) // Alias { READURI(&dtl->material1, "Textures") } else if(ISLABEL("Flat")) { READURI(&dtl->material2, "Flats") } else if(ISLABEL("Lump")) { READURI(&dtl->stage.texture, "Lumps") } else if(ISLABEL("File")) { READURI(&dtl->stage.texture, 0) } else RV_FLT("Scale", dtl->stage.scale) RV_FLT("Strength", dtl->stage.strength) RV_FLT("Distance", dtl->stage.maxDistance) RV_END CHECKSC; } prevDetailDefIdx = idx; } if(ISTOKEN("Reflection")) // Surface reflection { ded_reflection_t* ref = 0; idx = DED_AddReflection(ded); ref = &ded->reflections[idx]; // Should we copy the previous definition? if(prevRefDefIdx >= 0 && bCopyNext) { ded->reflections.copyTo(ref, prevRefDefIdx); } FINDBEGIN; for(;;) { READLABEL; RV_FLAGS("Flags", ref->flags, "rff_") RV_FLT("Shininess", ref->stage.shininess) RV_VEC("Min color", ref->stage.minColor, 3) RV_BLENDMODE("Blending mode", ref->stage.blendMode) RV_URI("Shiny map", &ref->stage.texture, "LightMaps") RV_URI("Mask map", &ref->stage.maskTexture, "LightMaps") RV_FLT("Mask width", ref->stage.maskWidth) RV_FLT("Mask height", ref->stage.maskHeight) if(ISLABEL("Material")) { READURI(&ref->material, 0) } else if(ISLABEL("Texture")) { READURI(&ref->material, "Textures") } else if(ISLABEL("Flat")) { READURI(&ref->material, "Flats") } else RV_END CHECKSC; } prevRefDefIdx = idx; } if(ISTOKEN("Generator")) // Particle Generator { ded_ptcgen_t* gen; int sub = 0; idx = DED_AddPtcGen(ded, ""); gen = &ded->ptcGens[idx]; // Should we copy the previous definition? if(prevGenDefIdx >= 0 && bCopyNext) { ded->ptcGens.copyTo(gen, prevGenDefIdx); } FINDBEGIN; for(;;) { READLABEL; RV_STR("State", gen->state) if(ISLABEL("Material")) { READURI(&gen->material, 0) } else if(ISLABEL("Flat")) { READURI(&gen->material, "Flats") } else if(ISLABEL("Texture")) { READURI(&gen->material, "Textures") } else RV_STR("Mobj", gen->type) RV_STR("Alt mobj", gen->type2) RV_STR("Damage mobj", gen->damage) RV_URI("Map", &gen->map, "Maps") RV_FLAGS("Flags", gen->flags, "gnf_") RV_FLT("Speed", gen->speed) RV_FLT("Speed Rnd", gen->speedVariance) RV_VEC("Vector", gen->vector, 3) RV_FLT("Vector Rnd", gen->vectorVariance) RV_FLT("Init vector Rnd", gen->initVectorVariance) RV_VEC("Center", gen->center, 3) RV_INT("Submodel", gen->subModel) RV_FLT("Spawn radius", gen->spawnRadius) RV_FLT("Min spawn radius", gen->spawnRadiusMin) RV_FLT("Distance", gen->maxDist) RV_INT("Spawn age", gen->spawnAge) RV_INT("Max age", gen->maxAge) RV_INT("Particles", gen->particles) RV_FLT("Spawn rate", gen->spawnRate) RV_FLT("Spawn Rnd", gen->spawnRateVariance) RV_INT("Presim", gen->preSim) RV_INT("Alt start", gen->altStart) RV_FLT("Alt Rnd", gen->altStartVariance) RV_VEC("Force axis", gen->forceAxis, 3) RV_FLT("Force radius", gen->forceRadius) RV_FLT("Force", gen->force) RV_VEC("Force origin", gen->forceOrigin, 3) if(ISLABEL("Stage")) { ded_ptcstage_t *st = NULL; if(sub >= gen->stages.size()) { // Allocate new stage. sub = DED_AddPtcGenStage(gen); } st = &gen->stages[sub]; FINDBEGIN; for(;;) { READLABEL; RV_FLAGS("Type", st->type, "pt_") RV_INT("Tics", st->tics) RV_FLT("Rnd", st->variance) RV_VEC("Color", st->color, 4) RV_FLT("Radius", st->radius) RV_FLT("Radius rnd", st->radiusVariance) RV_FLAGS("Flags", st->flags, "ptf_") RV_FLT("Bounce", st->bounce) RV_FLT("Gravity", st->gravity) RV_FLT("Resistance", st->resistance) RV_STR("Frame", st->frameName) RV_STR("End frame", st->endFrameName) RV_VEC("Spin", st->spin, 2) RV_VEC("Spin resistance", st->spinResistance, 2) RV_STR("Sound", st->sound.name) RV_FLT("Volume", st->sound.volume) RV_STR("Hit sound", st->hitSound.name) RV_FLT("Hit volume", st->hitSound.volume) RV_VEC("Force", st->vectorForce, 3) RV_END CHECKSC; } sub++; } else RV_END CHECKSC; } prevGenDefIdx = idx; } if(ISTOKEN("Finale") || ISTOKEN("InFine")) { // New finales are appended to the end of the list. idx = ded->addFinale(); Record &fin = ded->finales[idx]; if(source->custom) fin.set("custom", true); FINDBEGIN; forever { READLABEL; RV_STR("ID", fin["id"]) RV_URI("Before", fin["before"], "Maps") RV_URI("After", fin["after"], "Maps") RV_INT("Game", dummyInt) if(ISLABEL("Script")) { String buffer; buffer.reserve(1600); FINDBEGIN; ReadToken(); while(!ISTOKEN("}") && !source->atEnd) { if(!buffer.isEmpty()) buffer += ' '; buffer += String(token); if(ISTOKEN("\"")) { ReadString(buffer, true, true); buffer += '"'; } ReadToken(); } buffer.squeeze(); fin.set("script", buffer); } else RV_END CHECKSC; } } // An oldschool (light) decoration definition? if(ISTOKEN("Decoration")) { idx = ded->addDecoration(); Record &decor = ded->decorations[idx]; // Should we copy the previous definition? if(prevDecorDefIdx >= 0 && bCopyNext) { ded->decorations.copy(prevDecorDefIdx, decor); } defn::Decoration mainDef(decor); int light = 0; FINDBEGIN; forever { READLABEL; RV_FLAGS("Flags", decor["flags"], "dcf_") if(ISLABEL("Material")) { READURI(decor["texture"], 0) } else if(ISLABEL("Texture")) { READURI(decor["texture"], "Textures") } else if(ISLABEL("Flat")) { READURI(decor["texture"], "Flats") } else if(ISLABEL("Light")) { if(light == DED_MAX_MATERIAL_DECORATIONS) { setError("Too many Decoration.Lights"); retVal = false; goto ded_end_read; } // Add another light. if(light >= mainDef.lightCount()) mainDef.addLight(); defn::MaterialDecoration lightDef(mainDef.light(light)); // One implicit stage. Record &st = (lightDef.stageCount()? lightDef.stage(0) : lightDef.addStage()); FINDBEGIN; forever { READLABEL; RV_VEC_VAR("Offset", st["origin"], 2) RV_FLT("Distance", st["elevation"]) RV_VEC_VAR("Color", st["color"], 3) RV_FLT("Radius", st["radius"]) RV_FLT("Halo radius", st["haloRadius"]) RV_VEC_VAR("Pattern offset", lightDef.def()["patternOffset"], 2) RV_VEC_VAR("Pattern skip", lightDef.def()["patternSkip"], 2) if(ISLABEL("Levels")) { FINDBEGIN; Vector2f levels; for(int b = 0; b < 2; ++b) { float val; READFLT(val) levels[b] = de::clamp(0.f, val / 255.0f, 1.f); } ReadToken(); st["lightLevels"] = new ArrayValue(levels); } else RV_INT("Flare texture", st["haloTextureIndex"]) RV_URI("Flare map", st["haloTexture"], "LightMaps") RV_URI("Top map", st["lightmapUp"], "LightMaps") RV_URI("Bottom map", st["lightmapDown"], "LightMaps") RV_URI("Side map", st["lightmapSide"], "LightMaps") RV_END CHECKSC; } light++; } else RV_END CHECKSC; } prevDecorDefIdx = idx; } if(ISTOKEN("Group")) { idx = DED_AddGroup(ded); ded_group_t *grp = &ded->groups[idx]; int sub = 0; FINDBEGIN; forever { READLABEL; if(ISLABEL("Texture") || ISLABEL("Flat")) { bool const haveTexture = ISLABEL("Texture"); // Need to allocate new stage? if(sub >= grp->members.size()) { sub = DED_AddGroupMember(grp); } ded_group_member_t *memb = &grp->members[sub]; FINDBEGIN; forever { READLABEL; RV_URI("ID", &memb->material, (haveTexture? "Textures" : "Flats")) if(ISLABEL("Tics")) { READINT(memb->tics); if(memb->tics < 0) { LOG_RES_WARNING("Invalid Group.%s.Tics: %i (< min: 0) in \"%s\" on line #%i" "\nWill ignore this Group if used for Material animation") << (haveTexture ? "Texture" : "Flat") << memb->tics << (source ? source->fileName : "?") << (source ? source->lineNumber : 0); } } else RV_INT("Random", memb->randomTics) RV_END CHECKSC; } ++sub; } else RV_FLAGS("Flags", grp->flags, "tgf_") RV_END CHECKSC; } } /*if(ISTOKEN("XGClass")) // XG Class { // A new XG Class definition idx = DED_AddXGClass(ded); xgc = ded->xgClasses + idx; sub = 0; FINDBEGIN; for(;;) { READLABEL; RV_STR("ID", xgc->id) RV_STR("Name", xgc->name) if(ISLABEL("Property")) { ded_xgclass_property_t *xgcp = NULL; if(sub >= xgc->properties_count.num) { // Allocate new property sub = DED_AddXGClassProperty(xgc); } xgcp = &xgc->properties[sub]; FINDBEGIN; for(;;) { READLABEL; RV_FLAGS("ID", xgcp->id, "xgcp_") RV_FLAGS("Flags", xgcp->flags, "xgcpf_") RV_STR("Name", xgcp->name) RV_STR("Flag Group", xgcp->flagprefix) RV_END CHECKSC; } sub++; } else RV_END CHECKSC; } }*/ if(ISTOKEN("Line")) // Line Type { ded_linetype_t* l; // A new line type. idx = DED_AddLineType(ded, 0); l = &ded->lineTypes[idx]; // Should we copy the previous definition? if(prevLineTypeDefIdx >= 0 && bCopyNext) { ded->lineTypes.copyTo(l, prevLineTypeDefIdx); } FINDBEGIN; for(;;) { READLABEL; RV_INT("ID", l->id) RV_STR("Comment", l->comment) RV_FLAGS("Flags", l->flags[0], "ltf_") RV_FLAGS("Flags2", l->flags[1], "ltf2_") RV_FLAGS("Flags3", l->flags[2], "ltf3_") RV_FLAGS("Class", l->lineClass, "ltc_") RV_FLAGS("Type", l->actType, "lat_") RV_INT("Count", l->actCount) RV_FLT("Time", l->actTime) RV_INT("Act tag", l->actTag) RV_INT("Ap0", l->aparm[0]) RV_INT("Ap1", l->aparm[1]) RV_INT("Ap2", l->aparm[2]) RV_INT("Ap3", l->aparm[3]) RV_FLAGS("Ap4", l->aparm[4], "lref_") RV_INT("Ap5", l->aparm[5]) RV_FLAGS("Ap6", l->aparm[6], "lref_") RV_INT("Ap7", l->aparm[7]) RV_INT("Ap8", l->aparm[8]) RV_STR("Ap9", l->aparm9) RV_INT("Health above", l->aparm[0]) RV_INT("Health below", l->aparm[1]) RV_INT("Power above", l->aparm[2]) RV_INT("Power below", l->aparm[3]) RV_FLAGS("Line act lref", l->aparm[4], "lref_") RV_INT("Line act lrefd", l->aparm[5]) RV_FLAGS("Line inact lref", l->aparm[6], "lref_") RV_INT("Line inact lrefd", l->aparm[7]) RV_INT("Color", l->aparm[8]) RV_STR("Thing type", l->aparm9) RV_FLT("Ticker start time", l->tickerStart) RV_FLT("Ticker end time", l->tickerEnd) RV_INT("Ticker tics", l->tickerInterval) RV_STR("Act sound", l->actSound) RV_STR("Deact sound", l->deactSound) RV_INT("Event chain", l->evChain) RV_INT("Act chain", l->actChain) RV_INT("Deact chain", l->deactChain) RV_FLAGS("Wall section", l->wallSection, "lws_") if(ISLABEL("Act material")) { READURI(&l->actMaterial, 0) } else if(ISLABEL("Act texture")) // Alias { READURI(&l->actMaterial, "Textures") } else if(ISLABEL("Deact material")) { READURI(&l->deactMaterial, 0) } else if(ISLABEL("Deact texture")) // Alias { READURI(&l->deactMaterial, "Textures") } else RV_INT("Act type", l->actLineType) RV_INT("Deact type", l->deactLineType) RV_STR("Act message", l->actMsg) RV_STR("Deact message", l->deactMsg) RV_FLT("Texmove angle", l->materialMoveAngle) RV_FLT("Materialmove angle", l->materialMoveAngle) // Alias RV_FLT("Texmove speed", l->materialMoveSpeed) RV_FLT("Materialmove speed", l->materialMoveSpeed) // Alias RV_STR_INT("Ip0", l->iparmStr[0], l->iparm[0]) RV_STR_INT("Ip1", l->iparmStr[1], l->iparm[1]) RV_STR_INT("Ip2", l->iparmStr[2], l->iparm[2]) RV_STR_INT("Ip3", l->iparmStr[3], l->iparm[3]) RV_STR_INT("Ip4", l->iparmStr[4], l->iparm[4]) RV_STR_INT("Ip5", l->iparmStr[5], l->iparm[5]) RV_STR_INT("Ip6", l->iparmStr[6], l->iparm[6]) RV_STR_INT("Ip7", l->iparmStr[7], l->iparm[7]) RV_STR_INT("Ip8", l->iparmStr[8], l->iparm[8]) RV_STR_INT("Ip9", l->iparmStr[9], l->iparm[9]) RV_STR_INT("Ip10", l->iparmStr[10], l->iparm[10]) RV_STR_INT("Ip11", l->iparmStr[11], l->iparm[11]) RV_STR_INT("Ip12", l->iparmStr[12], l->iparm[12]) RV_STR_INT("Ip13", l->iparmStr[13], l->iparm[13]) RV_STR_INT("Ip14", l->iparmStr[14], l->iparm[14]) RV_STR_INT("Ip15", l->iparmStr[15], l->iparm[15]) RV_STR_INT("Ip16", l->iparmStr[16], l->iparm[16]) RV_STR_INT("Ip17", l->iparmStr[17], l->iparm[17]) RV_STR_INT("Ip18", l->iparmStr[18], l->iparm[18]) RV_STR_INT("Ip19", l->iparmStr[19], l->iparm[19]) RV_FLT("Fp0", l->fparm[0]) RV_FLT("Fp1", l->fparm[1]) RV_FLT("Fp2", l->fparm[2]) RV_FLT("Fp3", l->fparm[3]) RV_FLT("Fp4", l->fparm[4]) RV_FLT("Fp5", l->fparm[5]) RV_FLT("Fp6", l->fparm[6]) RV_FLT("Fp7", l->fparm[7]) RV_FLT("Fp8", l->fparm[8]) RV_FLT("Fp9", l->fparm[9]) RV_FLT("Fp10", l->fparm[10]) RV_FLT("Fp11", l->fparm[11]) RV_FLT("Fp12", l->fparm[12]) RV_FLT("Fp13", l->fparm[13]) RV_FLT("Fp14", l->fparm[14]) RV_FLT("Fp15", l->fparm[15]) RV_FLT("Fp16", l->fparm[16]) RV_FLT("Fp17", l->fparm[17]) RV_FLT("Fp18", l->fparm[18]) RV_FLT("Fp19", l->fparm[19]) RV_STR("Sp0", l->sparm[0]) RV_STR("Sp1", l->sparm[1]) RV_STR("Sp2", l->sparm[2]) RV_STR("Sp3", l->sparm[3]) RV_STR("Sp4", l->sparm[4]) if(l->lineClass) { // IpX Alt names can only be used if the class is defined first! // they also support the DED v6 flags format. int i; for(i = 0; i < 20; ++i) { xgclassparm_t const& iParm = xgClassLinks[l->lineClass].iparm[i]; if(!iParm.name || !iParm.name[0]) continue; if(!ISLABEL(iParm.name)) continue; if(iParm.flagPrefix && iParm.flagPrefix[0]) { READFLAGS(l->iparm[i], iParm.flagPrefix) } else { READSTRING(l->iparmStr[i], l->iparm[i]) } break; } // Not a known label? if(i == 20) RV_END } else RV_END CHECKSC; } prevLineTypeDefIdx = idx; } if(ISTOKEN("Sector")) // Sector Type { ded_sectortype_t* sec; // A new sector type. idx = DED_AddSectorType(ded, 0); sec = &ded->sectorTypes[idx]; if(prevSectorTypeDefIdx >= 0 && bCopyNext) { // Should we copy the previous definition? ded->sectorTypes.copyTo(sec, prevSectorTypeDefIdx); } FINDBEGIN; for(;;) { READLABEL; RV_INT("ID", sec->id) RV_STR("Comment", sec->comment) RV_FLAGS("Flags", sec->flags, "stf_") RV_INT("Act tag", sec->actTag) RV_INT("Floor chain", sec->chain[0]) RV_INT("Ceiling chain", sec->chain[1]) RV_INT("Inside chain", sec->chain[2]) RV_INT("Ticker chain", sec->chain[3]) RV_FLAGS("Floor chain flags", sec->chainFlags[0], "scef_") RV_FLAGS("Ceiling chain flags", sec->chainFlags[1], "scef_") RV_FLAGS("Inside chain flags", sec->chainFlags[2], "scef_") RV_FLAGS("Ticker chain flags", sec->chainFlags[3], "scef_") RV_FLT("Floor chain start time", sec->start[0]) RV_FLT("Ceiling chain start time", sec->start[1]) RV_FLT("Inside chain start time", sec->start[2]) RV_FLT("Ticker chain start time", sec->start[3]) RV_FLT("Floor chain end time", sec->end[0]) RV_FLT("Ceiling chain end time", sec->end[1]) RV_FLT("Inside chain end time", sec->end[2]) RV_FLT("Ticker chain end time", sec->end[3]) RV_FLT("Floor chain min interval", sec->interval[0][0]) RV_FLT("Ceiling chain min interval", sec->interval[1][0]) RV_FLT("Inside chain min interval", sec->interval[2][0]) RV_FLT("Ticker chain min interval", sec->interval[3][0]) RV_FLT("Floor chain max interval", sec->interval[0][1]) RV_FLT("Ceiling chain max interval", sec->interval[1][1]) RV_FLT("Inside chain max interval", sec->interval[2][1]) RV_FLT("Ticker chain max interval", sec->interval[3][1]) RV_INT("Floor chain count", sec->count[0]) RV_INT("Ceiling chain count", sec->count[1]) RV_INT("Inside chain count", sec->count[2]) RV_INT("Ticker chain count", sec->count[3]) RV_STR("Ambient sound", sec->ambientSound) RV_FLT("Ambient min interval", sec->soundInterval[0]) RV_FLT("Ambient max interval", sec->soundInterval[1]) RV_FLT("Floor texmove angle", sec->materialMoveAngle[0]) RV_FLT("Floor materialmove angle", sec->materialMoveAngle[0]) // Alias RV_FLT("Ceiling texmove angle", sec->materialMoveAngle[1]) RV_FLT("Ceiling materialmove angle", sec->materialMoveAngle[1]) // Alias RV_FLT("Floor texmove speed", sec->materialMoveSpeed[0]) RV_FLT("Floor materialmove speed", sec->materialMoveSpeed[0]) // Alias RV_FLT("Ceiling texmove speed", sec->materialMoveSpeed[1]) RV_FLT("Ceiling materialmove speed", sec->materialMoveSpeed[1]) // Alias RV_FLT("Wind angle", sec->windAngle) RV_FLT("Wind speed", sec->windSpeed) RV_FLT("Vertical wind", sec->verticalWind) RV_FLT("Gravity", sec->gravity) RV_FLT("Friction", sec->friction) RV_STR("Light fn", sec->lightFunc) RV_INT("Light fn min tics", sec->lightInterval[0]) RV_INT("Light fn max tics", sec->lightInterval[1]) RV_STR("Red fn", sec->colFunc[0]) RV_STR("Green fn", sec->colFunc[1]) RV_STR("Blue fn", sec->colFunc[2]) RV_INT("Red fn min tics", sec->colInterval[0][0]) RV_INT("Red fn max tics", sec->colInterval[0][1]) RV_INT("Green fn min tics", sec->colInterval[1][0]) RV_INT("Green fn max tics", sec->colInterval[1][1]) RV_INT("Blue fn min tics", sec->colInterval[2][0]) RV_INT("Blue fn max tics", sec->colInterval[2][1]) RV_STR("Floor fn", sec->floorFunc) RV_FLT("Floor fn scale", sec->floorMul) RV_FLT("Floor fn offset", sec->floorOff) RV_INT("Floor fn min tics", sec->floorInterval[0]) RV_INT("Floor fn max tics", sec->floorInterval[1]) RV_STR("Ceiling fn", sec->ceilFunc) RV_FLT("Ceiling fn scale", sec->ceilMul) RV_FLT("Ceiling fn offset", sec->ceilOff) RV_INT("Ceiling fn min tics", sec->ceilInterval[0]) RV_INT("Ceiling fn max tics", sec->ceilInterval[1]) RV_END CHECKSC; } prevSectorTypeDefIdx = idx; } bCopyNext = false; } ded_end_read: M_Free(rootStr); // Free the source stack entry we were using. DED_CloseReader(); return retVal; } void DED_Include(char const *fileName, String parentDirectory) { ddstring_t tmp; Str_InitStd(&tmp); Str_Set(&tmp, fileName); F_FixSlashes(&tmp, &tmp); F_ExpandBasePath(&tmp, &tmp); if(!F_IsAbsolute(&tmp)) { Str_PrependChar(&tmp, '/'); Str_Prepend(&tmp, parentDirectory.toUtf8().constData()); } Def_ReadProcessDED(ded, String(Str_Text(&tmp))); Str_Free(&tmp); // Reset state for continuing. strncpy(token, "", MAX_TOKEN_LEN); } }; DEDParser::DEDParser(ded_t *ded) : d(new Instance(this)) { d->ded = ded; } int DEDParser::parse(char const *buffer, String sourceFile, bool sourceIsCustom) { return d->readData(buffer, sourceFile, sourceIsCustom); } doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/model.cpp0000664000175000017500000000734112641367671023643 0ustar jaakkojaakko/** @file defs/model.cpp Model definition accessor. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/defs/model.h" #include "doomsday/defs/ded.h" #include #include using namespace de; namespace defn { void Model::resetToDefaults() { Definition::resetToDefaults(); // Add all expected fields with their default values. def().addText ("id", ""); def().addText ("state", ""); def().addNumber("off", 0); def().addText ("sprite", ""); def().addNumber("spriteFrame", 0); def().addNumber("group", 0); def().addNumber("selector", 0); def().addNumber("flags", 0); def().addNumber("interMark", 0); def().addArray ("interRange", new ArrayValue(Vector2i(0, 1))); def().addNumber("skinTics", 0); def().addArray ("scale", new ArrayValue(Vector3i(1, 1, 1))); def().addNumber("resize", 0); def().addArray ("offset", new ArrayValue(Vector3f())); def().addNumber("shadowRadius", 0); def().addArray ("sub", new ArrayValue); } Record &Model::addSub() { Record *sub = new Record; sub->addBoolean("custom", false); sub->addText ("filename", ""); sub->addText ("skinFilename", ""); sub->addText ("frame", ""); sub->addNumber("frameRange", 0); sub->addNumber("flags", 0); sub->addNumber("skin", 0); sub->addNumber("skinRange", 0); sub->addArray ("offset", new ArrayValue(Vector3f())); sub->addNumber("alpha", 0); sub->addNumber("parm", 0); sub->addNumber("selSkinMask", 0); sub->addNumber("selSkinShift", 0); ArrayValue *skins = new ArrayValue; for(int i = 0; i < 8; ++i) *skins << NumberValue(0); sub->addArray ("selSkins", skins); sub->addText ("shinySkin", ""); sub->addNumber("shiny", 0); sub->addArray ("shinyColor", new ArrayValue(Vector3f(1, 1, 1))); sub->addNumber("shinyReact", 1); sub->addNumber("blendMode", BM_NORMAL); def()["sub"].value() .add(new RecordValue(sub, RecordValue::OwnsRecord)); return *sub; } int Model::subCount() const { return int(geta("sub").size()); } bool Model::hasSub(int index) const { return index >= 0 && index < subCount(); } Record &Model::sub(int index) { return *def().geta("sub")[index].as().record(); } Record const &Model::sub(int index) const { return *geta("sub")[index].as().record(); } void Model::cleanupAfterParsing(Record const &prev) { if(gets("state") == "-") { def().set("state", prev.gets("state")); } if(gets("sprite") == "-") { def().set("sprite", prev.gets("sprite")); } for(int i = 0; i < subCount(); ++i) { Record &subDef = sub(i); if(subDef.gets("filename") == "-") subDef.set("filename", ""); if(subDef.gets("skinFilename") == "-") subDef.set("skinFilename", ""); if(subDef.gets("shinySkin") == "-") subDef.set("shinySkin", ""); if(subDef.gets("frame") == "-") subDef.set("frame", ""); } } } // namespace defn doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/dedfile.cpp0000664000175000017500000001114112641367671024130 0ustar jaakkojaakko/** @file dedfile.cpp * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/defs/dedfile.h" #include #include #include "doomsday/defs/dedparser.h" #include "doomsday/filesys/fs_main.h" #include "doomsday/filesys/fs_util.h" using namespace de; static char dedReadError[512]; void DED_SetError(String const &message) { String msg = "Error: " + message + "."; strncpy(dedReadError, msg.toUtf8().constData(), sizeof(dedReadError)); } void Def_ReadProcessDED(ded_t *defs, String sourcePath) { LOG_AS("Def_ReadProcessDED"); if(sourcePath.isEmpty()) return; // Try FS2 first. try { Block text; App::rootFolder().locate(sourcePath) >> text; if(!DED_ReadData(defs, text, sourcePath, true/*consider it custom; there is no way to check...*/)) { App_FatalError("Def_ReadProcessDED: %s\n", dedReadError); } return; // Done! } catch(...) { // Try FS1 as fallback. } de::Uri const uri(sourcePath, RC_NULL); if(!App_FileSystem().accessFile(uri)) { LOG_RES_WARNING("\"%s\" not found!") << NativePath(uri.asText()).pretty(); return; } // We use the File Ids to prevent loading the same files multiple times. if(!App_FileSystem().checkFileId(uri)) { // Already handled. LOG_RES_XVERBOSE("\"%s\" has already been read") << NativePath(uri.asText()).pretty(); return; } if(!DED_Read(defs, sourcePath)) { App_FatalError("Def_ReadProcessDED: %s\n", dedReadError); } } int DED_ReadLump(ded_t *ded, lumpnum_t lumpNum) { try { File1 &lump = App_FileSystem().lump(lumpNum); if(lump.size() > 0) { uint8_t const *data = lump.cache(); String sourcePath = lump.container().composePath(); bool custom = (lump.isContained()? lump.container().hasCustom() : lump.hasCustom()); DED_ReadData(ded, (char const *)data, sourcePath, custom); lump.unlock(); } return true; } catch(LumpIndex::NotFoundError const&) {} // Ignore error. DED_SetError("Bad lump number"); return false; } int DED_Read(ded_t *ded, String path) { // Attempt to open a definition file on this path. try { // Relative paths are relative to the native working directory. String fullPath = (NativePath::workPath() / NativePath(path).expand()).withSeparators('/'); QScopedPointer hndl(&App_FileSystem().openFile(fullPath, "rb")); // We will buffer a local copy of the file. How large a buffer do we need? hndl->seek(0, SeekEnd); size_t bufferedDefSize = hndl->tell(); hndl->rewind(); char *bufferedDef = (char *) M_Calloc(bufferedDefSize + 1); File1 &file = hndl->file(); /// @todo Custom status for contained files is not inherited from the container? bool const isCustom = (file.isContained()? file.container().hasCustom() : file.hasCustom()); // Copy the file into the local buffer and parse definitions. hndl->read((uint8_t *)bufferedDef, bufferedDefSize); int result = DED_ReadData(ded, bufferedDef, path, isCustom); App_FileSystem().releaseFile(file); // Done. Release temporary storage and return the result. M_Free(bufferedDef); return result; } catch(FS1::NotFoundError const &) {} // Ignore. DED_SetError("File could not be opened for reading"); return false; } int DED_ReadData(ded_t *ded, char const *buffer, String sourceFile, bool sourceIsCustom) { return DEDParser(ded).parse(buffer, sourceFile, sourceIsCustom); } char const *DED_Error() { return dedReadError; } doomsday-stable-1.15.7/doomsday/libdoomsday/src/defs/dedregister.cpp0000664000175000017500000002473312641367671025050 0ustar jaakkojaakko/** @file dedregister.h Register of definitions. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/defs/dedregister.h" #include #include #include #include #include #include using namespace de; DENG2_PIMPL(DEDRegister) , DENG2_OBSERVES(Record, Deletion) , DENG2_OBSERVES(Record, Addition) , DENG2_OBSERVES(Record, Removal) , DENG2_OBSERVES(Variable, ChangeFrom) { Record *names; struct Key { LookupFlags flags; Key(LookupFlags const &f = DefaultLookup) : flags(f) {} }; typedef QMap Keys; Keys keys; QMap parents; Instance(Public *i, Record &rec) : Base(i), names(&rec) { names->audienceForDeletion() += this; // The definitions will be stored here in the original order. names->addArray("order"); } ~Instance() { if(names) names->audienceForDeletion() -= this; } void recordBeingDeleted(Record &DENG2_DEBUG_ONLY(record)) { DENG2_ASSERT(names == &record); names = 0; } void clear() { // As a side-effect, the lookups will be cleared, too, as the members of // each definition record are deleted. (*names)["order"].value().clear(); #ifdef DENG2_DEBUG DENG2_ASSERT(parents.isEmpty()); foreach(String const &key, keys.keys()) { DENG2_ASSERT(lookup(key).size() == 0); } #endif } void addKey(String const &name, LookupFlags const &flags) { keys.insert(name, Key(flags)); names->addDictionary(name + "Lookup"); } ArrayValue &order() { return (*names)["order"].value(); } ArrayValue const &order() const { return (*names)["order"].value(); } DictionaryValue &lookup(String const &keyName) { return (*names)[keyName + "Lookup"].value(); } DictionaryValue const &lookup(String const &keyName) const { return (*names)[keyName + "Lookup"].value(); } template Type lookupOperation(String const &key, String value, std::function operation) const { auto foundKey = keys.constFind(key); if(foundKey == keys.constEnd()) return 0; if(!foundKey.value().flags.testFlag(CaseSensitive)) { // Case insensitive lookup is done in lower case. value = value.lower(); } return operation(lookup(key), value); } Record const *tryFind(String const &key, String const &value) const { return lookupOperation(key, value, [] (DictionaryValue const &lut, String v) -> Record const * { TextValue const val(v); auto i = lut.elements().find(DictionaryValue::ValueRef(&val)); if(i == lut.elements().end()) return 0; // Value not in dictionary. return i->second->as().record(); }); } bool has(String const &key, String const &value) const { return lookupOperation(key, value, [] (DictionaryValue const &lut, String v) { return lut.contains(TextValue(v)); }); } Record &append() { Record *sub = new Record; // Let each subrecord know their ordinal. sub->set("__order__", int(order().size())).setReadOnly(); // Observe what goes into this record. //sub->audienceForDeletion() += this; sub->audienceForAddition() += this; sub->audienceForRemoval() += this; order().add(new RecordValue(sub, RecordValue::OwnsRecord)); return *sub; } bool isEmptyKeyValue(Value const &value) const { return value.is() && value.asText().isEmpty(); } bool isValidKeyValue(Value const &value) const { // Empty strings are not indexable. if(isEmptyKeyValue(value)) return false; return true; } /// Returns @c true if the value was added. bool addToLookup(String const &key, Value const &value, Record &def) { if(!isValidKeyValue(value)) return false; String valText = value.asText(); DENG2_ASSERT(!valText.isEmpty()); DENG2_ASSERT(keys.contains(key)); if(!keys[key].flags.testFlag(CaseSensitive)) { valText = valText.lower(); } DictionaryValue &dict = lookup(key); if(keys[key].flags.testFlag(OnlyFirst)) { // Only index the first one that is found. if(dict.contains(TextValue(valText))) return false; } // Index definition using its current value. dict.add(new TextValue(valText), new RecordValue(&def)); return true; } bool removeFromLookup(String const &key, Value const &value, Record &def) { //qDebug() << "removeFromLookup" << key << value.asText() << &def; if(!isValidKeyValue(value)) return false; String valText = value.asText(); DENG2_ASSERT(!valText.isEmpty()); DENG2_ASSERT(keys.contains(key)); if(!keys[key].flags.testFlag(CaseSensitive)) { valText = valText.lower(); } DictionaryValue &dict = lookup(key); // Remove from the index. if(dict.contains(TextValue(valText))) { Value const &indexed = dict.element(TextValue(valText)); //qDebug() << " -" << indexed.as().record() << &def; Record const *indexedDef = indexed.as().record(); if(!indexedDef || indexedDef == &def) { // This is the definition that was indexed using the key value. // Let's remove it. dict.remove(TextValue(valText)); /// @todo Should now index any other definitions with this key value; /// needs to add a lookup of which other definitions have this value. return true; } } // There was some other definition indexed using this key. return false; } void recordMemberAdded(Record &def, Variable &key) { // Keys must be observed so that they are indexed in the lookup table. if(keys.contains(key.name())) { // Index definition using its current value. // Observe empty keys so we'll get the key's value when it's set. if(addToLookup(key.name(), key.value(), def) || isEmptyKeyValue(key.value())) { parents.insert(&key, &def); key.audienceForChangeFrom() += this; } } } void recordMemberRemoved(Record &def, Variable &key) { if(keys.contains(key.name())) { key.audienceForChangeFrom() -= this; parents.remove(&key); removeFromLookup(key.name(), key.value(), def); } } void variableValueChangedFrom(Variable &key, Value const &oldValue, Value const &newValue) { //qDebug() << "changed" << key.name() << "from" << oldValue.asText() << "to" // << newValue.asText(); DENG2_ASSERT(parents.contains(&key)); // The value of a key has changed, so it needs to be reindexed. removeFromLookup(key.name(), oldValue, *parents[&key]); addToLookup(key.name(), newValue, *parents[&key]); } }; DEDRegister::DEDRegister(Record &names) : d(new Instance(this, names)) {} void DEDRegister::addLookupKey(String const &variableName, LookupFlags const &flags) { d->addKey(variableName, flags); } void DEDRegister::clear() { d->clear(); } Record &DEDRegister::append() { return d->append(); } Record &DEDRegister::appendCopy(int index) { return copy(index, append()); } Record &DEDRegister::copy(int fromIndex, Record &to) { QStringList omitted; omitted << "__.*"; // double-underscore // By default lookup keys are not copied, as they are used as identifiers and // therefore duplicates should not occur. DENG2_FOR_EACH_CONST(Instance::Keys, i, d->keys) { if(i.value().flags.testFlag(AllowCopy)) continue; omitted << i.key(); } return to.assign((*this)[fromIndex], QRegExp(omitted.join("|"))); } int DEDRegister::size() const { return d->order().size(); } bool DEDRegister::has(String const &key, String const &value) const { return d->has(key, value); } Record &DEDRegister::operator [] (int index) { return *d->order().at(index).as().record(); } Record const &DEDRegister::operator [] (int index) const { return *d->order().at(index).as().record(); } Record *DEDRegister::tryFind(String const &key, String const &value) { return const_cast(d->tryFind(key, value)); } Record const *DEDRegister::tryFind(String const &key, String value) const { return d->tryFind(key, value); } Record &DEDRegister::find(String const &key, String const &value) { return const_cast(const_cast(this)->find(key, value)); } Record const &DEDRegister::find(String const &key, String const &value) const { if(!d->keys.contains(key)) { throw UndefinedKeyError("DEDRegister::find", "Key '" + key + "' not defined"); } Record const *rec = tryFind(key, value); if(!rec) { throw NotFoundError("DEDRegister::find", key + " '" + value + "' not found"); } return *rec; } DictionaryValue const &DEDRegister::lookup(String const &key) const { if(!d->keys.contains(key)) { throw UndefinedKeyError("DEDRegister::lookup", "Key '" + key + "' not defined"); } return d->lookup(key); } doomsday-stable-1.15.7/doomsday/libdoomsday/src/uri.cpp0000664000175000017500000003565012641367671022425 0ustar jaakkojaakko/** @file uri.cpp Universal Resource Identifier. * @ingroup base * * @authors Copyright © 2010-2013 Daniel Swanson * @authors Copyright © 2010-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/uri.h" #include #include #include #include #include #include #include #include "doomsday/filesys/fs_main.h" #include "doomsday/dualstring.h" #include #include namespace de { static Uri::ResolverFunc resolverFunc; /** * Extracts the scheme from a string. * * @param stringWithScheme The scheme is removed from the string. * * @return Scheme, or empty string if no valid scheme was present. */ static String extractScheme(String &stringWithScheme) { String scheme; int pos = stringWithScheme.indexOf(':'); if(pos > URI_MINSCHEMELENGTH) // could be Windows-style driver letter "c:" { scheme = stringWithScheme.left(pos); stringWithScheme.remove(0, pos + 1); } return scheme; } DENG2_PIMPL_NOREF(Uri) { Path path; ///< Path of the Uri. DualString strPath; // Redundant; for legacy access, remove this! DualString scheme; ///< Scheme of the Uri. /// Cached copy of the resolved path. Path resolvedPath; /** * The cached path only applies when this game is loaded. * * @note Add any other conditions here that result in different results for * resolveUri(). */ void *resolvedForGame; Instance() : resolvedForGame(0) {} Instance(Instance const &other) : de::IPrivate(), path (other.path), strPath (other.strPath), scheme (other.scheme), resolvedPath (other.resolvedPath), resolvedForGame(other.resolvedForGame) {} void clearCachedResolved() { resolvedPath.clear(); resolvedForGame = 0; } void parseRawUri(String rawUri, QChar sep, resourceclassid_t defaultResourceClass) { LOG_AS("Uri::parseRawUri"); clearCachedResolved(); scheme = extractScheme(rawUri); // scheme removed if(sep != '/') rawUri.replace(sep, '/'); // force slashes as separator path = rawUri; strPath = path.toString(); // for legacy code if(!scheme.isEmpty()) { if(defaultResourceClass == RC_NULL || App_FileSystem().knownScheme(scheme)) { // Scheme is accepted as is. return; } LOG_RES_WARNING("Unknown scheme \"%s\" for path \"%s\", using default scheme instead") << scheme << strPath; } // Attempt to guess the scheme by interpreting the path? if(defaultResourceClass == RC_UNKNOWN) { defaultResourceClass = DD_GuessFileTypeFromFileName(strPath).defaultClass(); } if(VALID_RESOURCECLASSID(defaultResourceClass)) { FS1::Scheme &fsScheme = App_FileSystem().scheme(ResourceClass::classForId(defaultResourceClass).defaultScheme()); scheme = fsScheme.name(); } } String resolveSymbol(QStringRef const &symbol) const { if(!resolverFunc) { return symbol.toString(); } return resolverFunc(symbol.toString()); } inline String parseExpression(QStringRef const &expression) const { // Presently the expression consists of a single symbol. return resolveSymbol(expression); } String resolve() const { LOG_AS("Uri::resolve"); String result; // Keep scanning the path for embedded expressions. QStringRef expression; int expEnd = 0, expBegin; while((expBegin = strPath.indexOf('$', expEnd)) >= 0) { // Is the next char the start-of-expression character? if(strPath.at(expBegin + 1) == '(') { // Copy everything up to the '$'. result += strPath.mid(expEnd, expBegin - expEnd); // Skip over the '$'. ++expBegin; // Find the end-of-expression character. expEnd = strPath.indexOf(')', expBegin); if(expEnd < 0) { LOG_RES_WARNING("Ignoring expression \"" + strPath + "\": " "missing a closing ')'"); expEnd = strPath.length(); } // Skip over the '('. ++expBegin; // The range of the expression substring is now known. expression = strPath.midRef(expBegin, expEnd - expBegin); result += parseExpression(expression); } else { // No - copy the '$' and continue. result += '$'; } ++expEnd; } // Copy anything remaining. result += strPath.mid(expEnd); return result; } private: Instance &operator = (Instance const &); // no assignment }; Uri::Uri() : d(new Instance) {} Uri::Uri(String const &percentEncoded, resourceclassid_t defaultResourceClass, QChar sep) : d(new Instance) { if(!percentEncoded.isEmpty()) { setUri(percentEncoded, defaultResourceClass, sep); } } Uri::Uri(String const &scheme, Path const &path) : d(new Instance) { setScheme(scheme); setPath(path); } Uri::Uri(resourceclassid_t resClass, Path const &path) : d(new Instance) { setUri(path.toString(), resClass, path.separator()); } Uri::Uri(Path const &path) : d(new Instance) { setPath(path); } Uri::Uri(char const *nullTerminatedCStr) : d(new Instance) { setUri(nullTerminatedCStr); } Uri Uri::fromUserInput(char **argv, int argc, bool (*knownScheme) (String name)) { Uri output; if(argv) { // [0: :] or [0: ] or [0: ]. switch(argc) { case 1: { // Try to extract the scheme and encode the rest of the path. String rawUri(argv[0]); int pos = rawUri.indexOf(':'); if(pos >= 0) { output.setScheme(rawUri.left(pos)); rawUri.remove(0, pos + 1); output.setPath(Path::normalize(QString(QByteArray(rawUri.toUtf8()).toPercentEncoding()))); } // Just a scheme name? else if(knownScheme && knownScheme(rawUri)) { output.setScheme(rawUri); } else { // Just a path. output.setPath(Path::normalize(QString(QByteArray(rawUri.toUtf8()).toPercentEncoding()))); } break; } // [0: , 1: ] case 2: // Assign the scheme and encode the path. output.setScheme(argv[0]); output.setPath(Path::normalize(QString(QByteArray(argv[1]).toPercentEncoding()))); break; default: break; } } return output; } Uri::Uri(Uri const &other) : LogEntry::Arg::Base(), d(new Instance(*other.d)) {} Uri Uri::fromNativePath(NativePath const &path, resourceclassid_t defaultResourceClass) { return Uri(path.expand().withSeparators('/'), defaultResourceClass); } Uri Uri::fromNativeDirPath(NativePath const &nativeDirPath, resourceclassid_t defaultResourceClass) { // Uri follows the convention of having a slash at the end for directories. return Uri(nativeDirPath.expand().withSeparators('/') + '/', defaultResourceClass); } bool Uri::isEmpty() const { return d->path.isEmpty(); } bool Uri::operator == (Uri const &other) const { if(this == &other) return true; // First, lets check if the scheme differs. if(d->scheme.compareWithoutCase(other.d->scheme)) return false; // We can skip resolving if the paths are identical. if(d->path == other.d->path) return true; // We must be able to resolve both paths to compare. try { // Do not match partial paths. if(resolvedRef().length() != other.resolvedRef().length()) return false; return resolvedRef().compareWithoutCase(other.resolvedRef()) == 0; } catch(ResolveError const &) { // Ignore the error. } return false; } Uri &Uri::clear() { d->path.clear(); d->strPath.clear(); d->scheme.clear(); d->clearCachedResolved(); return *this; } String const &Uri::scheme() const { return d->scheme; } Path const &Uri::path() const { return d->path; } char const *Uri::schemeCStr() const { return d->scheme.utf8CStr(); } char const *Uri::pathCStr() const { return d->strPath.utf8CStr(); } ddstring_s const *Uri::schemeStr() const { return d->scheme.toStr(); } ddstring_s const *Uri::pathStr() const { return d->strPath.toStr(); } String Uri::resolved() const { return resolvedRef(); } String const &Uri::resolvedRef() const { void *currentGame = (void *) (!App::appExists() || App::game().isNull()? 0 : &App::game()); #ifndef LIBDENG_DISABLE_URI_RESOLVE_CACHING if(d->resolvedForGame && d->resolvedForGame == currentGame) { // We can just return the previously prepared resolved URI. return d->resolvedPath.toStringRef(); } #endif d->clearCachedResolved(); // Keep a copy of this, we'll likely need it many, many times. d->resolvedPath = d->resolve(); DENG2_ASSERT(d->resolvedPath.separator() == QChar('/')); d->resolvedForGame = currentGame; return d->resolvedPath.toStringRef(); } Uri &Uri::setScheme(String newScheme) { d->scheme = newScheme; d->clearCachedResolved(); return *this; } Uri &Uri::setPath(Path const &newPath) { // Force to slashes. d->path = newPath.withSeparators('/'); d->strPath = d->path.toStringRef(); // legacy support d->clearCachedResolved(); return *this; } Uri &Uri::setPath(String newPath, QChar sep) { return setPath(Path(newPath.trimmed(), sep)); } Uri &Uri::setPath(char const *newPathUtf8, char sep) { return setPath(Path(QString::fromUtf8(newPathUtf8).trimmed(), sep)); } Uri &Uri::setUri(String rawUri, resourceclassid_t defaultResourceClass, QChar sep) { LOG_AS("Uri::setUri"); d->parseRawUri(rawUri.trimmed(), sep, defaultResourceClass); return *this; } String Uri::compose(ComposeAsTextFlags compositionFlags, QChar sep) const { String text; if(!(compositionFlags & OmitScheme)) { if(!d->scheme.isEmpty()) { text += d->scheme + ":"; } } if(!(compositionFlags & OmitPath)) { QString path = d->path.withSeparators(sep); if(compositionFlags & DecodePath) { path = QByteArray::fromPercentEncoding(path.toUtf8()); } text += path; } return text; } String Uri::asText() const { return compose(DefaultComposeAsTextFlags | DecodePath); } void Uri::operator >> (Writer &to) const { to << d->scheme << d->path; } void Uri::operator << (Reader &from) { clear(); from >> d->scheme >> d->path; d->strPath = d->path; } void Uri::setResolverFunc(ResolverFunc resolver) { resolverFunc = resolver; } #ifdef _DEBUG LIBDENG_DEFINE_UNITTEST(Uri) { try { // Test emptiness. { Uri u; DENG_ASSERT(u.isEmpty()); DENG_ASSERT(u.path().segmentCount() == 1); } // Test a zero-length path. { Uri u("", RC_NULL); DENG_ASSERT(u.isEmpty()); DENG_ASSERT(u.path().segmentCount() == 1); } // Equality and copying. { Uri a("some/thing", RC_NULL); Uri b("/other/thing", RC_NULL); DENG_ASSERT(a != b); Uri c = a; DENG_ASSERT(c == a); DENG_ASSERT(c.path().reverseSegment(1).toString() == "some"); b = a; DENG_ASSERT(b == a); //qDebug() << b.reverseSegment(1); DENG_ASSERT(b.path().reverseSegment(1).toString() == "some"); } // Swapping. { Uri a("a/b/c", RC_NULL); Uri b("d/e", RC_NULL); DENG_ASSERT(a.path().segmentCount() == 3); DENG_ASSERT(a.path().reverseSegment(1).toString() == "b"); std::swap(a, b); DENG_ASSERT(a.path().segmentCount() == 2); DENG_ASSERT(a.path().reverseSegment(1).toString() == "d"); DENG_ASSERT(b.path().segmentCount() == 3); DENG_ASSERT(b.path().reverseSegment(1).toString() == "b"); } // Test a Windows style path with a drive plus file path. { Uri u("c:/something.ext", RC_NULL); DENG_ASSERT(u.path().segmentCount() == 2); DENG_ASSERT(u.path().reverseSegment(0).length() == 13); DENG_ASSERT(u.path().reverseSegment(0).toString() == "something.ext"); DENG_ASSERT(u.path().reverseSegment(1).length() == 2); DENG_ASSERT(u.path().reverseSegment(1).toString() == "c:"); } // Test a Unix style path with a zero-length root node name. { Uri u("/something.ext", RC_NULL); DENG_ASSERT(u.path().segmentCount() == 2); DENG_ASSERT(u.path().reverseSegment(0).length() == 13); DENG_ASSERT(u.path().reverseSegment(0).toString() == "something.ext"); DENG_ASSERT(u.path().reverseSegment(1).length() == 0); DENG_ASSERT(u.path().reverseSegment(1).toString() == ""); } // Test a relative directory. { Uri u("some/dir/structure/", RC_NULL); DENG_ASSERT(u.path().segmentCount() == 3); DENG_ASSERT(u.path().reverseSegment(0).length() == 9); DENG_ASSERT(u.path().reverseSegment(0).toString() == "structure"); DENG_ASSERT(u.path().reverseSegment(1).length() == 3); DENG_ASSERT(u.path().reverseSegment(1).toString() == "dir"); DENG_ASSERT(u.path().reverseSegment(2).length() == 4); DENG_ASSERT(u.path().reverseSegment(2).toString() == "some"); } } catch(Error const &er) { qWarning() << er.asText(); return false; } return true; } LIBDENG_RUN_UNITTEST(Uri) #endif // _DEBUG } // namespace de doomsday-stable-1.15.7/doomsday/libdoomsday/src/resource/0000775000175000017500000000000012641367671022740 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/src/resource/wav.cpp0000664000175000017500000001736712641367671024257 0ustar jaakkojaakko/** @file wav.cpp WAV loader. @ingroup audio * * @todo This is obsolete code! Use de::Waveform instead. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/resource/wav.h" #include "doomsday/filesys/fs_main.h" #include "doomsday/filesys/fs_util.h" #include #include #include #include #include "dd_share.h" #ifdef __GNUC__ /* * Something in here is confusing GCC: when compiled with -O2, WAV_MemoryLoad() * reads corrupt data. It is likely that the optimizer gets the manipulation of * the 'data' pointer wrong. */ # if GCC_VERSION >= 40400 void* WAV_MemoryLoad(const byte* data, size_t datalength, int* bits, int* rate, int* samples) __attribute__(( optimize(0) )); # endif #endif #define WAVE_FORMAT_PCM 1 #pragma pack(1) typedef struct riff_hdr_s { char id[4]; ///< Identifier string = "RIFF" uint32_t len; ///< Remaining length after this header } riff_hdr_t; typedef struct chunk_hdr_s { ///< CHUNK 8-byte header char id[4]; ///< Identifier, e.g. "fmt " or "data" uint32_t len; ///< Remaining chunk length after header } chunk_hdr_t; ///< data bytes follow chunk header typedef struct wav_format_s { uint16_t wFormatTag; ///< Format category uint16_t wChannels; ///< Number of channels uint32_t dwSamplesPerSec; ///< Sampling rate uint32_t dwAvgBytesPerSec; ///< For buffer estimation uint16_t wBlockAlign; ///< Data block size uint16_t wBitsPerSample; ///< Sample size } wav_format_t; #pragma pack() #define WReadAndAdvance(pByte, pDest, length) \ { memcpy(pDest, pByte, length); pByte += length; } int WAV_CheckFormat(const char* data) { return !strncmp(data, "RIFF", 4) && !strncmp(data + 8, "WAVE", 4); } void* WAV_MemoryLoad(const byte* data, size_t datalength, int* bits, int* rate, int* samples) { const byte* end = data + datalength; byte* sampledata = NULL; chunk_hdr_t riff_chunk; wav_format_t wave_format; LOG_AS("WAV_MemoryLoad"); if(!WAV_CheckFormat((const char*)data)) { LOG_RES_WARNING("Not WAV format data"); return NULL; } // Read the RIFF header. data += sizeof(riff_hdr_t); data += 4; // "WAVE" already verified above #ifdef _DEBUG assert(sizeof(wave_format) == 16); assert(sizeof(riff_chunk) == 8); #endif // Start readin' the chunks, baby! while(data < end) { // Read next chunk header. WReadAndAdvance(data, &riff_chunk, sizeof(riff_chunk)); // Correct endianness. riff_chunk.len = ULONG(riff_chunk.len); // What have we got here? if(!strncmp(riff_chunk.id, "fmt ", 4)) { // Read format chunk. WReadAndAdvance(data, &wave_format, sizeof(wave_format)); // Correct endianness. wave_format.wFormatTag = USHORT(wave_format.wFormatTag ); wave_format.wChannels = USHORT(wave_format.wChannels ); wave_format.dwSamplesPerSec = ULONG (wave_format.dwSamplesPerSec ); wave_format.dwAvgBytesPerSec = ULONG (wave_format.dwAvgBytesPerSec); wave_format.wBlockAlign = USHORT(wave_format.wBlockAlign ); wave_format.wBitsPerSample = USHORT(wave_format.wBitsPerSample ); assert(wave_format.wFormatTag == WAVE_FORMAT_PCM); // linear PCM // Check that it's a format we know how to read. if(wave_format.wFormatTag != WAVE_FORMAT_PCM) { LOG_RES_WARNING("Unsupported format (%i)") << wave_format.wFormatTag; return NULL; } if(wave_format.wChannels != 1) { LOG_RES_WARNING("Too many channels (only mono supported)"); return NULL; } // Read the extra format information. //WReadAndAdvance(&data, &wave_format2, sizeof(*wave_format2)); /*if(wave_format->wBitsPerSample == 0) { // We'll have to guess... *bits = 8*wave_format->dwAvgBytesPerSec/ wave_format->dwSamplesPerSec; } else { */ if(wave_format.wBitsPerSample != 8 && wave_format.wBitsPerSample != 16) { LOG_RES_WARNING("Must have 8 or 16 bits per sample"); return NULL; } // Now we know some information about the sample. *bits = wave_format.wBitsPerSample; *rate = wave_format.dwSamplesPerSec; } else if(!strncmp(riff_chunk.id, "data", 4)) { if(!wave_format.wFormatTag) { LOG_RES_WARNING("Malformed WAV data"); return NULL; } // Read data chunk. *samples = riff_chunk.len / wave_format.wBlockAlign; // Allocate the sample buffer. sampledata = (byte *) Z_Malloc(riff_chunk.len, PU_APPSTATIC, 0); memcpy(sampledata, data, riff_chunk.len); #ifdef __BIG_ENDIAN__ // Correct endianness. /*if(wave_format->wBitsPerSample == 16) { ushort* sample = sampledata; for(; sample < ((short*)sampledata) + *samples; ++sample) *sample = USHORT(*sample); }*/ #endif // We're satisfied with this! Let's get out of here. break; } else { // Unknown chunk, just skip it. data += riff_chunk.len; } } return sampledata; } void *WAV_Load(char const *filename, int *bits, int *rate, int *samples) { try { // Relative paths are relative to the native working directory. de::String path = (de::NativePath::workPath() / de::NativePath(filename).expand()).withSeparators('/'); QScopedPointer hndl(&App_FileSystem().openFile(path, "rb")); // Read in the whole thing. size_t size = hndl->length(); LOG_AS("WAV_Load"); LOGDEV_RES_XVERBOSE("Loading from \"%s\" (size %i, fpos %i)") << de::NativePath(hndl->file().composePath()).pretty() << size << hndl->tell(); uint8_t *data = (uint8_t *) M_Malloc(size); hndl->read(data, size); App_FileSystem().releaseFile(hndl->file()); // Parse the RIFF data. void *sampledata = WAV_MemoryLoad((byte const *) data, size, bits, rate, samples); if(!sampledata) { LOG_RES_WARNING("Failed to load \"%s\"") << filename; } M_Free(data); return sampledata; } catch(de::FS1::NotFoundError const &) {} // Ignore. return 0; } doomsday-stable-1.15.7/doomsday/libdoomsday/src/resource/resourceclass.cpp0000664000175000017500000000430712641367671026325 0ustar jaakkojaakko/** @file resourceclass.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/resource/resourceclass.h" using namespace de; static ResourceClass &(*classGetter)(resourceclassid_t) = 0; DENG2_PIMPL_NOREF(ResourceClass) { /// Symbolic name for this class. String name; /// Symbolic name of the default filesystem subspace scheme. String defaultScheme; /// Recognized file types (in order of importance, left to right; owned). FileTypes fileTypes; ~Instance() { qDeleteAll(fileTypes); } }; ResourceClass::ResourceClass(String name, String defaultScheme) : d(new Instance) { d->name = name; d->defaultScheme = defaultScheme; } String ResourceClass::name() const { return d->name; } String ResourceClass::defaultScheme() const { return d->defaultScheme; } int ResourceClass::fileTypeCount() const { return d->fileTypes.size(); } ResourceClass &ResourceClass::addFileType(FileType *ftype) { d->fileTypes.append(ftype); return *this; } ResourceClass::FileTypes const &ResourceClass::fileTypes() const { return d->fileTypes; } bool ResourceClass::isNull() const { static String const nullName = "RC_NULL"; return d->name == nullName; } ResourceClass &ResourceClass::classForId(resourceclassid_t id) { DENG_ASSERT(classGetter != 0); return classGetter(id); } void ResourceClass::setResourceClassCallback(ResourceClass &(*callback)(resourceclassid_t)) { classGetter = callback; } doomsday-stable-1.15.7/doomsday/libdoomsday/src/console/0000775000175000017500000000000012641367671022553 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/src/console/cmd.cpp0000664000175000017500000003235012641367671024025 0ustar jaakkojaakko/** @file * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/console/cmd.h" #include "doomsday/console/alias.h" #include "doomsday/console/knownword.h" #include "doomsday/help.h" #include #include #include #include #include #include using namespace de; static ccmd_t *ccmdListHead; /// @todo Replace with a data structure that allows for deletion of elements. static blockset_t *ccmdBlockSet; /// Running total of the number of uniquely-named commands. static uint numUniqueNamedCCmds; static QMap mappedConfigVariables; void Con_InitCommands() { ccmdListHead = 0; ccmdBlockSet = 0; numUniqueNamedCCmds = 0; } void Con_ClearCommands(void) { if(ccmdBlockSet) { BlockSet_Delete(ccmdBlockSet); } ccmdBlockSet = 0; ccmdListHead = 0; numUniqueNamedCCmds = 0; mappedConfigVariables.clear(); } void Con_AddKnownWordsForCommands() { /// @note ccmd list is NOT yet sorted. for(ccmd_t* ccmd = ccmdListHead; ccmd; ccmd = ccmd->next) { // Skip overloaded variants. if(ccmd->prevOverload) continue; Con_AddKnownWord(WT_CCMD, ccmd); } } void Con_AddCommand(ccmdtemplate_t const* ccmd) { int minArgs, maxArgs; cvartype_t args[DENG_MAX_ARGS]; ccmd_t* newCCmd, *overloaded = 0; if(!ccmd) return; DENG_ASSERT(ccmd->name != 0); // Decode the usage string if present. if(ccmd->argTemplate != 0) { size_t l, len; cvartype_t type = CVT_NULL; dd_bool unlimitedArgs; char c; len = strlen(ccmd->argTemplate); minArgs = 0; unlimitedArgs = false; for(l = 0; l < len; ++l) { c = ccmd->argTemplate[l]; switch(c) { // Supported type symbols: case 'b': type = CVT_BYTE; break; case 'i': type = CVT_INT; break; case 'f': type = CVT_FLOAT; break; case 's': type = CVT_CHARPTR; break; // Special symbols: case '*': // Variable arg list. if(l != len-1) App_FatalError("Con_AddCommand: CCmd '%s': '*' character " "not last in argument template: \"%s\".", ccmd->name, ccmd->argTemplate); unlimitedArgs = true; type = CVT_NULL; // Not a real argument. break; // Erroneous symbol: default: App_FatalError("Con_AddCommand: CCmd '%s': Invalid character " "'%c' in argument template: \"%s\".", ccmd->name, c, ccmd->argTemplate); } if(type != CVT_NULL) { if(minArgs >= DENG_MAX_ARGS) App_FatalError("Con_AddCommand: CCmd '%s': Too many arguments. " "Limit is %i.", ccmd->name, DENG_MAX_ARGS); args[minArgs++] = type; } } // Set the min/max parameter counts for this ccmd. if(unlimitedArgs) { maxArgs = -1; if(minArgs == 0) minArgs = -1; } else { maxArgs = minArgs; } } else // It's usage is NOT validated by Doomsday. { minArgs = maxArgs = -1; } // Now check that the ccmd to be registered is unique. // We allow multiple ccmds with the same name if we can determine by // their paramater lists that they are unique (overloading). { ccmd_t* other; if((other = Con_FindCommand(ccmd->name)) != 0) { dd_bool unique = true; // The ccmd being registered is NOT a deng validated ccmd // and there is already an existing ccmd by this name? if(minArgs == -1 && maxArgs == -1) unique = false; if(unique) { // Check each variant. ccmd_t* variant = other; do { // An existing ccmd with no validation? if(variant->minArgs == -1 && variant->maxArgs == -1) unique = false; // An existing ccmd with a lower minimum and no maximum? else if(variant->minArgs < minArgs && variant->maxArgs == -1) unique = false; // An existing ccmd with a larger min and this ccmd has no max? else if(variant->minArgs > minArgs && maxArgs == -1) unique = false; // An existing ccmd with the same minimum number of args? else if(variant->minArgs == minArgs) { // \todo Implement support for paramater type checking. unique = false; } // Sanity check. if(!unique && variant->execFunc == ccmd->execFunc) App_FatalError("Con_AddCommand: A CCmd by the name '%s' is already registered and the callback funcs are " "the same, is this really what you wanted?", ccmd->name); } while((variant = variant->nextOverload) != 0); } if(!unique) App_FatalError("Con_AddCommand: A CCmd by the name '%s' is already registered. Their parameter lists would be ambiguant.", ccmd->name); overloaded = other; }} if(!ccmdBlockSet) ccmdBlockSet = BlockSet_New(sizeof(ccmd_t), 32); newCCmd = (ccmd_t*) BlockSet_Allocate(ccmdBlockSet); // Make a static copy of the name in the zone (this allows the source // data to change in case of dynamic registrations). char* nameCopy = (char*) Z_Malloc(strlen(ccmd->name) + 1, PU_APPSTATIC, NULL); if(!nameCopy) App_FatalError("Con_AddCommand: Failed on allocation of %lu bytes for command name.", (unsigned long) (strlen(ccmd->name) + 1)); strcpy(nameCopy, ccmd->name); newCCmd->name = nameCopy; newCCmd->execFunc = ccmd->execFunc; newCCmd->flags = ccmd->flags; newCCmd->nextOverload = newCCmd->prevOverload = 0; newCCmd->minArgs = minArgs; newCCmd->maxArgs = maxArgs; memcpy(newCCmd->args, &args, sizeof(newCCmd->args)); // Link it to the head of the global list of ccmds. newCCmd->next = ccmdListHead; ccmdListHead = newCCmd; if(!overloaded) { ++numUniqueNamedCCmds; Con_UpdateKnownWords(); return; } // Link it to the head of the overload list. newCCmd->nextOverload = overloaded; overloaded->prevOverload = newCCmd; } void Con_AddCommandList(ccmdtemplate_t const* cmdList) { if(!cmdList) return; for(; cmdList->name; ++cmdList) { Con_AddCommand(cmdList); } } ccmd_t *Con_FindCommand(char const *name) { /// @todo Use a faster than O(n) linear search. if(name && name[0]) { for(ccmd_t *ccmd = ccmdListHead; ccmd; ccmd = ccmd->next) { if(qstricmp(name, ccmd->name)) continue; // Locate the head of the overload list. while(ccmd->prevOverload) { ccmd = ccmd->prevOverload; } return ccmd; } } return 0; } /** * Outputs the usage information for the given ccmd to the console. * * @param ccmd The ccmd to print the usage info for. * @param allOverloads @c true= print usage info for all overloaded variants. * Otherwise only the info for @a ccmd. */ void Con_PrintCommandUsage(ccmd_t const *ccmd, bool allOverloads) { if(!ccmd) return; if(allOverloads) { // Locate the head of the overload list. while(ccmd->prevOverload) { ccmd = ccmd->prevOverload; } } LOG_SCR_NOTE(_E(b) "Usage:" _E(.) "\n " _E(>) + Con_CmdUsageAsStyledText(ccmd)); if(allOverloads) { while((ccmd = ccmd->nextOverload)) { LOG_SCR_MSG(" " _E(>) + Con_CmdUsageAsStyledText(ccmd)); } } } ccmd_t *Con_FindCommandMatchArgs(cmdargs_t *args) { if(!args) return 0; if(ccmd_t *ccmd = Con_FindCommand(args->argv[0])) { // Check each variant. ccmd_t *variant = ccmd; do { dd_bool invalidArgs = false; // Are we validating the arguments? // Note that strings are considered always valid. if(!(variant->minArgs == -1 && variant->maxArgs == -1)) { // Do we have the right number of arguments? if(args->argc-1 < variant->minArgs) { invalidArgs = true; } else if(variant->maxArgs != -1 && args->argc-1 > variant->maxArgs) { invalidArgs = true; } else { // Presently we only validate upto the minimum number of args. /// @todo Validate non-required args. for(int i = 0; i < variant->minArgs && !invalidArgs; ++i) { switch(variant->args[i]) { case CVT_BYTE: invalidArgs = !M_IsStringValidByte(args->argv[i+1]); break; case CVT_INT: invalidArgs = !M_IsStringValidInt(args->argv[i+1]); break; case CVT_FLOAT: invalidArgs = !M_IsStringValidFloat(args->argv[i+1]); break; default: break; } } } } if(!invalidArgs) { return variant; // This is the one! } } while((variant = variant->nextOverload) != 0); // Perhaps the user needs some help. Con_PrintCommandUsage(ccmd); } // No command found, or none with matching arguments. return 0; } dd_bool Con_IsValidCommand(char const* name) { if(!name || !name[0]) return false; // Try the console commands first. if(Con_FindCommand(name) != 0) return true; // Try the aliases (aliai?) then. return (Con_FindAlias(name) != 0); } String Con_CmdUsageAsStyledText(ccmd_t const *ccmd) { DENG2_ASSERT(ccmd != 0); if(ccmd->minArgs == -1 && ccmd->maxArgs == -1) return String(); // Print the expected form for this ccmd. String argText; for(int i = 0; i < ccmd->minArgs; ++i) { switch(ccmd->args[i]) { case CVT_BYTE: argText += " (byte)"; break; case CVT_INT: argText += " (int)"; break; case CVT_FLOAT: argText += " (float)"; break; case CVT_CHARPTR: argText += " (string)"; break; default: break; } } if(ccmd->maxArgs == -1) { argText += " ..."; } return _E(b) + String(ccmd->name) + _E(.) _E(l) + argText + _E(.); } String Con_CmdAsStyledText(ccmd_t *cmd) { char const *str; if((str = DH_GetString(DH_Find(cmd->name), HST_DESCRIPTION))) { return String(_E(b) "%1 " _E(.) _E(>) _E(2) "%2" _E(.) _E(<)).arg(cmd->name).arg(str); } else { return String(_E(b) "%1" _E(.)).arg(cmd->name); } } D_CMD(MappedConfigVariable) { DENG_UNUSED(src); // Look up the variable. auto const found = mappedConfigVariables.constFind(argv[0]); DENG2_ASSERT(found != mappedConfigVariables.constEnd()); // mapping must be defined Variable &var = App::config().names()[found.value()]; if(argc == 1) { // No argumnets, just print the current value. LOG_SCR_MSG(_E(b) "%s" _E(.) " = " _E(>) "%s " _E(l)_E(C) "[Config.%s]") << argv[0] << var.value().asText() << found.value(); } else if(argc > 1) { // Retain the current type of the Config variable (numeric or text). if(var.value().maybeAs()) { var.set(new TextValue(argv[1])); } else { var.set(new NumberValue(String(argv[1]).toDouble())); } } return true; } void Con_AddMappedConfigVariable(char const *consoleName, char const *opts, String const &configVariable) { DENG2_ASSERT(!mappedConfigVariables.contains(consoleName)); // redefining not handled mappedConfigVariables.insert(consoleName, configVariable); C_CMD(consoleName, "", MappedConfigVariable); C_CMD(consoleName, opts, MappedConfigVariable); } doomsday-stable-1.15.7/doomsday/libdoomsday/src/console/con_data.cpp0000664000175000017500000000000012641367671025015 0ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/src/console/knownword.cpp0000664000175000017500000002756612641367671025327 0ustar jaakkojaakko/** @file knownword.cpp * @ingroup console * * @todo Rewrite this whole thing with sensible C++ data structures and de::String! -jk * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/console/knownword.h" #include "doomsday/console/exec.h" #include "doomsday/console/var.h" #include "doomsday/console/alias.h" #include "doomsday/console/cmd.h" #include "doomsday/help.h" #include #include #include #include #include using namespace de; /// The list of known words (for completion). /// @todo Replace with a persistent self-balancing BST (Treap?)? static QList knownWords; static dd_bool knownWordsNeedUpdate; static void (*appWordsCallback)(); void Con_ClearKnownWords(void) { knownWords.clear(); knownWordsNeedUpdate = false; } /// Returns @c true, if a < b. static bool compareKnownWordByName(knownword_t const &a, knownword_t const &b) { knownword_t const *wA = &a; knownword_t const *wB = &b; AutoStr *textA = 0, *textB = 0; switch(wA->type) { case WT_CALIAS: textA = AutoStr_FromTextStd(((calias_t *)wA->data)->name); break; case WT_CCMD: textA = AutoStr_FromTextStd(((ccmd_t *)wA->data)->name); break; case WT_CVAR: textA = CVar_ComposePath((cvar_t *)wA->data); break; case WT_GAME: textA = AutoStr_FromTextStd(reinterpret_cast(wA->data)->id().toUtf8().constData()); break; default: App_FatalError("compareKnownWordByName: Invalid type %i for word A.", wA->type); exit(1); // Unreachable } switch(wB->type) { case WT_CALIAS: textB = AutoStr_FromTextStd(((calias_t *)wB->data)->name); break; case WT_CCMD: textB = AutoStr_FromTextStd(((ccmd_t *)wB->data)->name); break; case WT_CVAR: textB = CVar_ComposePath((cvar_t *)wB->data); break; case WT_GAME: textB = AutoStr_FromTextStd(reinterpret_cast(wB->data)->id().toUtf8().constData()); break; default: App_FatalError("compareKnownWordByName: Invalid type %i for word B.", wB->type); exit(1); // Unreachable } return Str_CompareIgnoreCase(textA, Str_Text(textB)) < 0; } /** * @return New AutoStr with the text of the known word. Caller gets ownership. */ static AutoStr *textForKnownWord(knownword_t const *word) { AutoStr *text = 0; switch(word->type) { case WT_CALIAS: text = AutoStr_FromTextStd(((calias_t *)word->data)->name); break; case WT_CCMD: text = AutoStr_FromTextStd(((ccmd_t *)word->data)->name); break; case WT_CVAR: text = CVar_ComposePath((cvar_t *)word->data); break; case WT_GAME: text = AutoStr_FromTextStd(reinterpret_cast(word->data)->id().toUtf8().constData()); break; default: App_FatalError("textForKnownWord: Invalid type %i for word.", word->type); exit(1); // Unreachable } return text; } #if 0 static dd_bool removeFromKnownWords(knownwordtype_t type, void* data) { DENG_ASSERT(VALID_KNOWNWORDTYPE(type) && data != 0); for(int i = 0; i < knownWords.size(); ++i) { knownword_t const &word = knownWords.at(i); if(word.type != type || word.data != data) continue; knownWords.removeAt(i); return true; } return false; } #endif void Con_AddKnownWord(knownwordtype_t type, void *ptr) { knownword_t word; word.type = type; word.data = ptr; knownWords << word; } void Con_UpdateKnownWords() { knownWordsNeedUpdate = true; } /** * Collate all known words and sort them alphabetically. * Commands, variables (except those hidden) and aliases are known words. */ static void updateKnownWords(void) { if(!knownWordsNeedUpdate) return; /* // Count the number of visible console variables. countvariableparams_t countCVarParams; countCVarParams.count = 0; countCVarParams.type = cvartype_t(-1); countCVarParams.hidden = false; countCVarParams.ignoreHidden = true; if(cvarDirectory) { cvarDirectory->traverse(PathTree::NoBranch, NULL, CVarDirectory::no_hash, countVariable, &countCVarParams); }*/ // Build the known words table. /*numKnownWords = numUniqueNamedCCmds + countCVarParams.count + numCAliases + App_Games().count(); size_t len = sizeof(knownword_t) * numKnownWords; knownWords = (knownword_t*) M_Realloc(knownWords, len); memset(knownWords, 0, len); */ // uint knownWordIdx = 0; knownWords.clear(); // Add commands? Con_AddKnownWordsForCommands(); // Add variables? Con_AddKnownWordsForVariables(); // Add aliases? Con_AddKnownWordsForAliases(); if(appWordsCallback) { appWordsCallback(); } // Sort it so we get nice alphabetical word completions. qSort(knownWords.begin(), knownWords.end(), compareKnownWordByName); knownWordsNeedUpdate = false; } AutoStr *Con_KnownWordToString(knownword_t const *word) { return textForKnownWord(word); } int Con_IterateKnownWords(char const *pattern, knownwordtype_t type, int (*callback)(knownword_t const *word, void *parameters), void *parameters) { return Con_IterateKnownWords(KnownWordStartsWith, pattern, type, callback, parameters); } int Con_IterateKnownWords(KnownWordMatchMode matchMode, char const* pattern, knownwordtype_t type, int (*callback)(knownword_t const* word, void* parameters), void* parameters) { DENG_ASSERT(callback); knownwordtype_t matchType = (VALID_KNOWNWORDTYPE(type)? type : WT_ANY); size_t patternLength = (pattern? strlen(pattern) : 0); int result = 0; updateKnownWords(); for(int i = 0; i < knownWords.size(); ++i) { knownword_t const *word = &knownWords.at(i); if(matchType != WT_ANY && word->type != type) continue; if(patternLength) { AutoStr* textString = textForKnownWord(word); if(matchMode == KnownWordStartsWith) { if(qstrnicmp(Str_Text(textString), pattern, patternLength)) continue; // Didn't match. } else if(matchMode == KnownWordExactMatch) { if(strcasecmp(Str_Text(textString), pattern)) continue; // Didn't match. } } result = callback(word, parameters); if(result) break; } return result; } static int countMatchedWordWorker(knownword_t const* /*word*/, void* parameters) { DENG_ASSERT(parameters); uint* count = (uint*) parameters; ++(*count); return 0; // Continue iteration. } typedef struct { knownword_t const** matches; /// Matched word array. uint index; /// Current position in the collection. } collectmatchedwordworker_paramaters_t; static int collectMatchedWordWorker(knownword_t const* word, void* parameters) { DENG_ASSERT(word && parameters); collectmatchedwordworker_paramaters_t* p = (collectmatchedwordworker_paramaters_t*) parameters; p->matches[p->index++] = word; return 0; // Continue iteration. } knownword_t const** Con_CollectKnownWordsMatchingWord(char const* word, knownwordtype_t type, uint* count) { uint localCount = 0; Con_IterateKnownWords(word, type, countMatchedWordWorker, &localCount); if(count) *count = localCount; if(localCount != 0) { // Collect the pointers. collectmatchedwordworker_paramaters_t p; p.matches = (knownword_t const**) M_Malloc(sizeof(*p.matches) * (localCount + 1)); p.index = 0; Con_IterateKnownWords(word, type, collectMatchedWordWorker, &p); p.matches[localCount] = 0; // Terminate. return p.matches; } return 0; // No matches. } static int aproposPrinter(knownword_t const *word, void *matching) { AutoStr *text = textForKnownWord(word); // See if 'matching' is anywhere in the known word. if(strcasestr(Str_Text(text), (const char*)matching)) { char const* wType[KNOWNWORDTYPE_COUNT] = { "cmd ", "var ", "alias ", "game " }; String str; QTextStream os(&str); os << _E(l) << wType[word->type] << _E(0) << _E(b) << Str_Text(text) << " " << _E(2) << _E(>); // Look for a short description. String tmp; if(word->type == WT_CCMD || word->type == WT_CVAR) { char const *desc = DH_GetString(DH_Find(Str_Text(text)), HST_DESCRIPTION); if(desc) { tmp = desc; } } else if(word->type == WT_GAME) { tmp = reinterpret_cast(word->data)->title(); } os << tmp; LOG_SCR_MSG("%s") << str; } return 0; } static void printApropos(char const *matching) { /// @todo Extend the search to cover the contents of all help strings (dd_help.c). Con_IterateKnownWords(0, WT_ANY, aproposPrinter, (void *)matching); } D_CMD(HelpApropos) { DENG2_UNUSED2(argc, src); printApropos(argv[1]); return true; } struct AnnotationWork { QSet terms; de::String result; }; static int annotateMatchedWordCallback(knownword_t const *word, void *parameters) { AnnotationWork *work = reinterpret_cast(parameters); AutoStr *name = Con_KnownWordToString(word); de::String found; if(!work->terms.contains(Str_Text(name))) return false; // keep going switch(word->type) { case WT_CVAR: if(!(((cvar_t *)word->data)->flags & CVF_HIDE)) { found = Con_VarAsStyledText((cvar_t *) word->data, ""); } break; case WT_CCMD: if(!((ccmd_t *)word->data)->prevOverload) { found = Con_CmdAsStyledText((ccmd_t *) word->data); } break; case WT_CALIAS: found = Con_AliasAsStyledText((calias_t *) word->data); break; case WT_GAME: found = Con_GameAsStyledText(reinterpret_cast(word->data)); break; default: break; } if(!found.isEmpty()) { if(!work->result.isEmpty()) work->result.append("\n"); work->result.append(found); } return false; // don't stop } de::String Con_AnnotatedConsoleTerms(QStringList terms) { AnnotationWork work; foreach(QString term, terms) { work.terms.insert(term); } Con_IterateKnownWords(NULL, WT_ANY, annotateMatchedWordCallback, &work); return work.result; } static int addToTerms(knownword_t const *word, void *parameters) { shell::Lexicon *lexi = reinterpret_cast(parameters); lexi->addTerm(Str_Text(Con_KnownWordToString(word))); return 0; } shell::Lexicon Con_Lexicon() { shell::Lexicon lexi; Con_IterateKnownWords(0, WT_ANY, addToTerms, &lexi); lexi.setAdditionalWordChars("-_."); return lexi; } void Con_SetApplicationKnownWordCallback(void (*callback)()) { appWordsCallback = callback; } doomsday-stable-1.15.7/doomsday/libdoomsday/src/console/alias.cpp0000664000175000017500000001024312641367671024350 0ustar jaakkojaakko/** @file * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/console/alias.h" #include "doomsday/console/knownword.h" #include using namespace de; /// @todo Replace with a BST. static uint numCAliases; static calias_t **caliases; void Con_InitAliases() { numCAliases = 0; caliases = 0; } void Con_ClearAliases() { if(caliases) { // Free the alias data. calias_t** cal = caliases; for(uint i = 0; i < numCAliases; ++i, ++cal) { M_Free((*cal)->name); M_Free((*cal)->command); M_Free(*cal); } M_Free(caliases); } caliases = 0; numCAliases = 0; } calias_t *Con_FindAlias(char const *name) { uint bottomIdx, topIdx, pivot; calias_t* cal; dd_bool isDone; int result; if(numCAliases == 0) return 0; if(!name || !name[0]) return 0; bottomIdx = 0; topIdx = numCAliases-1; cal = NULL; isDone = false; while(bottomIdx <= topIdx && !isDone) { pivot = bottomIdx + (topIdx - bottomIdx)/2; result = qstricmp(caliases[pivot]->name, name); if(result == 0) { // Found. cal = caliases[pivot]; isDone = true; } else { if(result > 0) { if(pivot == 0) { // Not present. isDone = true; } else topIdx = pivot - 1; } else bottomIdx = pivot + 1; } } return cal; } calias_t* Con_AddAlias(char const* name, char const* command) { if(!name || !name[0] || !command || !command[0]) return 0; caliases = (calias_t**) M_Realloc(caliases, sizeof(*caliases) * ++numCAliases); // Find the insertion point. uint idx; for(idx = 0; idx < numCAliases-1; ++idx) { if(qstricmp(caliases[idx]->name, name) > 0) break; } // Make room for the new alias. if(idx != numCAliases-1) memmove(caliases + idx + 1, caliases + idx, sizeof(*caliases) * (numCAliases - 1 - idx)); // Add the new variable, making a static copy of the name in the zone (this allows // the source data to change in case of dynamic registrations). calias_t* newAlias = caliases[idx] = (calias_t*) M_Malloc(sizeof(*newAlias)); newAlias->name = (char*) M_Malloc(strlen(name) + 1); strcpy(newAlias->name, name); newAlias->command = (char*) M_Malloc(strlen(command) + 1); strcpy(newAlias->command, command); Con_UpdateKnownWords(); return newAlias; } void Con_DeleteAlias(calias_t* cal) { DENG_ASSERT(cal); uint idx; for(idx = 0; idx < numCAliases; ++idx) { if(caliases[idx] == cal) break; } if(idx == numCAliases) return; Con_UpdateKnownWords(); M_Free(cal->name); M_Free(cal->command); M_Free(cal); if(idx < numCAliases - 1) { memmove(caliases + idx, caliases + idx + 1, sizeof(*caliases) * (numCAliases - idx - 1)); } --numCAliases; } String Con_AliasAsStyledText(calias_t *alias) { QString str; QTextStream os(&str); os << _E(b) << alias->name << _E(.) " == " _E(>) << alias->command << _E(<); return str; } void Con_AddKnownWordsForAliases() { calias_t** cal = caliases; for(uint i = 0; i < numCAliases; ++i, ++cal) { Con_AddKnownWord(WT_CALIAS, *cal); } } doomsday-stable-1.15.7/doomsday/libdoomsday/src/console/var.cpp0000664000175000017500000004230512641367671024053 0ustar jaakkojaakko/** @file * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #define DENG_NO_API_MACROS_URI #include "doomsday/console/var.h" #include "doomsday/console/exec.h" #include "doomsday/console/knownword.h" #include "doomsday/uri.h" #include #include using namespace de; // Substrings in CVar names are delimited by this character. #define CVARDIRECTORY_DELIMITER '-' typedef UserDataPathTree CVarDirectory; /// Console variable directory. static CVarDirectory *cvarDirectory; static ddstring_s *emptyStr; static de::Uri *emptyUri; void Con_InitVariableDirectory() { cvarDirectory = new CVarDirectory; emptyStr = Str_NewStd(); emptyUri = new de::Uri; } void Con_DeinitVariableDirectory() { delete cvarDirectory; cvarDirectory = 0; Str_Delete(emptyStr); emptyStr = 0; delete emptyUri; emptyUri = 0; } static int markVariableUserDataFreed(CVarDirectory::Node &node, void *context) { DENG_ASSERT(context); cvar_t *var = reinterpret_cast(node.userPointer()); void **ptr = (void **) context; if(var) switch(CVar_Type(var)) { case CVT_CHARPTR: if(*ptr == CV_CHARPTR(var)) var->flags &= ~CVF_CAN_FREE; break; case CVT_URIPTR: if(*ptr == CV_URIPTR(var)) var->flags &= ~CVF_CAN_FREE; break; default: break; } return 0; // Continue iteration. } static int clearVariable(CVarDirectory::Node& node, void * /*context*/) { cvar_t *var = reinterpret_cast(node.userPointer()); if(var) { // Detach our user data from this node. node.setUserPointer(0); if(CVar_Flags(var) & CVF_CAN_FREE) { void** ptr = NULL; switch(CVar_Type(var)) { case CVT_CHARPTR: if(!CV_CHARPTR(var)) break; ptr = (void**)var->ptr; /// @note Multiple vars could be using the same pointer (so only free once). cvarDirectory->traverse(PathTree::NoBranch, NULL, CVarDirectory::no_hash, markVariableUserDataFreed, ptr); M_Free(*ptr); *ptr = Str_Text(emptyStr); break; case CVT_URIPTR: if(!CV_URIPTR(var)) break; ptr = (void**)var->ptr; /// @note Multiple vars could be using the same pointer (so only free once). cvarDirectory->traverse(PathTree::NoBranch, NULL, CVarDirectory::no_hash, markVariableUserDataFreed, ptr); delete reinterpret_cast(*ptr); *ptr = emptyUri; break; default: LOGDEV_SCR_WARNING("Attempt to free user data for non-pointer type variable %s [%p]") << Str_Text(CVar_ComposePath(var)) << var; break; } } M_Free(var); } return 0; // Continue iteration. } void Con_ClearVariables() { /// If _DEBUG we'll traverse all nodes and verify our clear logic. #if _DEBUG PathTree::ComparisonFlags flags; #else PathTree::ComparisonFlags flags = PathTree::NoBranch; #endif if(!cvarDirectory) return; cvarDirectory->traverse(flags, NULL, CVarDirectory::no_hash, clearVariable); cvarDirectory->clear(); } /// Construct a new variable from the specified template and add it to the database. static cvar_t* addVariable(cvartemplate_t const& tpl) { Path path(tpl.path, CVARDIRECTORY_DELIMITER); CVarDirectory::Node* node = &cvarDirectory->insert(path); cvar_t* newVar; DENG_ASSERT(!node->userPointer()); if(node->userPointer()) { throw Error("Con_AddVariable", "A variable with path '" + String(tpl.path) + "' is already known!"); } newVar = (cvar_t*) M_Malloc(sizeof *newVar); newVar->flags = tpl.flags; newVar->type = tpl.type; newVar->ptr = tpl.ptr; newVar->min = tpl.min; newVar->max = tpl.max; newVar->notifyChanged = tpl.notifyChanged; newVar->directoryNode = node; node->setUserPointer(newVar); Con_UpdateKnownWords(); return newVar; } String CVar_TypeAsText(cvar_t const *var) { // Human-readable type name. DENG_ASSERT(var); switch(var->type) { case CVT_BYTE: return "byte"; case CVT_CHARPTR: return "text"; case CVT_FLOAT: return "float"; case CVT_INT: return "integer"; case CVT_NULL: return "null"; case CVT_URIPTR: return "uri"; default: DENG_ASSERT(!"Con_VarTypeAsText: Unknown variable type"); break; } return ""; } template void printTypeWarning(cvar_t const *var, String const &attemptedType, ValueType value) { AutoStr* path = CVar_ComposePath(var); LOG_SCR_WARNING("Variable %s (of type '%s') is incompatible with %s ") << Str_Text(path) << CVar_TypeAsText(var) << attemptedType << value; } void CVar_PrintReadOnlyWarning(cvar_t const *var) { AutoStr* path = CVar_ComposePath(var); LOG_SCR_WARNING("%s (%s cvar) is read-only; it cannot be changed (even with force)") << CVar_TypeAsText(var) << Str_Text(path); } ddstring_t const* CVar_TypeName(cvartype_t type) { static de::Str const names[CVARTYPE_COUNT] = { "invalid", "CVT_BYTE", "CVT_INT", "CVT_FLOAT", "CVT_CHARPTR", "CVT_URIPTR" }; return names[(VALID_CVARTYPE(type)? type : 0)]; } cvartype_t CVar_Type(cvar_t const* var) { DENG_ASSERT(var); return var->type; } int CVar_Flags(const cvar_t* var) { DENG_ASSERT(var); return var->flags; } AutoStr *CVar_ComposePath(cvar_t const *var) { DENG_ASSERT(var != 0); CVarDirectory::Node &node = *reinterpret_cast(var->directoryNode); QByteArray path = node.path(CVARDIRECTORY_DELIMITER).toUtf8(); return AutoStr_FromTextStd(path.constData()); } void CVar_SetUri2(cvar_t *var, de::Uri const &uri, int svFlags) { DENG_ASSERT(var); de::Uri *newUri; bool changed = false; if((var->flags & CVF_READ_ONLY) && !(svFlags & SVF_WRITE_OVERRIDE)) { CVar_PrintReadOnlyWarning(var); return; } if(var->type != CVT_URIPTR) { App_FatalError("CVar::SetUri: Not of type %s.", Str_Text(CVar_TypeName(CVT_URIPTR))); return; // Unreachable. } /* if(!CV_URIPTR(var) && !uri) { return; } */ // Compose the new uri. newUri = new de::Uri(uri); if(!CV_URIPTR(var) || *CV_URIPTR(var) != *newUri) { changed = true; } // Free the old uri, if one exists. if((var->flags & CVF_CAN_FREE) && CV_URIPTR(var)) { delete CV_URIPTR(var); } var->flags |= CVF_CAN_FREE; CV_URIPTR(var) = newUri; // Make the change notification callback if(var->notifyChanged && changed) { var->notifyChanged(); } } void CVar_SetUri(cvar_t *var, de::Uri const &uri) { CVar_SetUri2(var, uri, 0); } void CVar_SetString2(cvar_t *var, char const *text, int svFlags) { DENG_ASSERT(var != 0); bool changed = false; size_t oldLen, newLen; if((var->flags & CVF_READ_ONLY) && !(svFlags & SVF_WRITE_OVERRIDE)) { CVar_PrintReadOnlyWarning(var); return; } if(var->type != CVT_CHARPTR) { printTypeWarning(var, "text", text); return; } oldLen = (!CV_CHARPTR(var)? 0 : strlen(CV_CHARPTR(var))); newLen = (!text ? 0 : strlen(text)); if(oldLen == 0 && newLen == 0) return; if(oldLen != newLen || qstricmp(text, CV_CHARPTR(var))) changed = true; // Free the old string, if one exists. if((var->flags & CVF_CAN_FREE) && CV_CHARPTR(var)) free(CV_CHARPTR(var)); // Allocate a new string. var->flags |= CVF_CAN_FREE; CV_CHARPTR(var) = (char*) M_Malloc(newLen + 1); qstrcpy(CV_CHARPTR(var), text); // Make the change notification callback if(var->notifyChanged != NULL && changed) var->notifyChanged(); } void CVar_SetString(cvar_t* var, char const* text) { CVar_SetString2(var, text, 0); } void CVar_SetInteger2(cvar_t* var, int value, int svFlags) { DENG_ASSERT(var); bool changed = false; if((var->flags & CVF_READ_ONLY) && !(svFlags & SVF_WRITE_OVERRIDE)) { CVar_PrintReadOnlyWarning(var); return; } switch(var->type) { case CVT_INT: if(CV_INT(var) != value) changed = true; CV_INT(var) = value; break; case CVT_BYTE: if(CV_BYTE(var) != (byte) value) changed = true; CV_BYTE(var) = (byte) value; break; case CVT_FLOAT: if(CV_FLOAT(var) != (float) value) changed = true; CV_FLOAT(var) = (float) value; break; default: printTypeWarning(var, "integer", value); return; } // Make a change notification callback? if(var->notifyChanged != 0 && changed) var->notifyChanged(); } void CVar_SetInteger(cvar_t* var, int value) { CVar_SetInteger2(var, value, 0); } void CVar_SetFloat2(cvar_t* var, float value, int svFlags) { DENG_ASSERT(var); bool changed = false; LOG_AS("CVar_SetFloat2"); if((var->flags & CVF_READ_ONLY) && !(svFlags & SVF_WRITE_OVERRIDE)) { CVar_PrintReadOnlyWarning(var); return; } switch(var->type) { case CVT_INT: if(CV_INT(var) != (int) value) changed = true; CV_INT(var) = (int) value; break; case CVT_BYTE: if(CV_BYTE(var) != (byte) value) changed = true; CV_BYTE(var) = (byte) value; break; case CVT_FLOAT: if(CV_FLOAT(var) != value) changed = true; CV_FLOAT(var) = value; break; default: printTypeWarning(var, "float", value); return; } // Make a change notification callback? if(var->notifyChanged != 0 && changed) var->notifyChanged(); } void CVar_SetFloat(cvar_t* var, float value) { CVar_SetFloat2(var, value, 0); } static void printConversionWarning(cvar_t const *var) { AutoStr* path = CVar_ComposePath(var); LOGDEV_SCR_WARNING("Incompatible variable %s [%p type:%s]") << Str_Text(path) << var << Str_Text(CVar_TypeName(CVar_Type(var))); } int CVar_Integer(cvar_t const* var) { DENG_ASSERT(var); switch(var->type) { case CVT_BYTE: return CV_BYTE(var); case CVT_INT: return CV_INT(var); case CVT_FLOAT: return CV_FLOAT(var); case CVT_CHARPTR: return strtol(CV_CHARPTR(var), 0, 0); default: { LOG_AS("CVar_Integer"); printConversionWarning(var); return 0; } } } float CVar_Float(cvar_t const* var) { DENG_ASSERT(var); switch(var->type) { case CVT_BYTE: return CV_BYTE(var); case CVT_INT: return CV_INT(var); case CVT_FLOAT: return CV_FLOAT(var); case CVT_CHARPTR: return strtod(CV_CHARPTR(var), 0); default: { LOG_AS("CVar_Float"); printConversionWarning(var); return 0; } } } byte CVar_Byte(cvar_t const* var) { DENG_ASSERT(var); switch(var->type) { case CVT_BYTE: return CV_BYTE(var); case CVT_INT: return CV_INT(var); case CVT_FLOAT: return CV_FLOAT(var); case CVT_CHARPTR: return strtol(CV_CHARPTR(var), 0, 0); default: { LOG_AS("CVar_Byte"); printConversionWarning(var); return 0; } } } char const* CVar_String(cvar_t const* var) { DENG_ASSERT(var); /// @todo Why not implement in-place value to string conversion? switch(var->type) { case CVT_CHARPTR: return CV_CHARPTR(var); default: { LOG_AS("CVar_String"); printConversionWarning(var); return Str_Text(emptyStr); } } } de::Uri const &CVar_Uri(cvar_t const *var) { if(!var) return *emptyUri; /// @todo Why not implement in-place string to uri conversion? switch(var->type) { case CVT_URIPTR: return *CV_URIPTR(var); default: { LOG_AS("CVar_Uri"); printConversionWarning(var); return *emptyUri; } } } void Con_AddVariable(cvartemplate_t const *tpl) { LOG_AS("Con_AddVariable"); if(!tpl) return; if(CVT_NULL == tpl->type) { LOGDEV_SCR_WARNING("Ignored attempt to register variable '%s' as type %s") << tpl->path << Str_Text(CVar_TypeName(CVT_NULL)); return; } addVariable(*tpl); } void Con_AddVariableList(cvartemplate_t const *tplList) { if(!tplList) return; for(; tplList->path; ++tplList) { if(Con_FindVariable(tplList->path)) { App_FatalError("Console variable with the name '%s' is already registered", tplList->path); } addVariable(*tplList); } } cvar_t *Con_FindVariable(char const *path) { if(!path || !path[0]) return 0; try { CVarDirectory::Node const &node = cvarDirectory->find(Path(path, CVARDIRECTORY_DELIMITER), PathTree::NoBranch | PathTree::MatchFull); return (cvar_t*) node.userPointer(); } catch(CVarDirectory::NotFoundError const&) {} // Ignore this error. return 0; } String Con_VarAsStyledText(cvar_t *var, char const *prefix) { if(!var) return ""; char equals = '='; if((var->flags & CVF_PROTECTED) || (var->flags & CVF_READ_ONLY)) equals = ':'; String str; QTextStream os(&str); if(prefix) os << prefix; AutoStr* path = CVar_ComposePath(var); os << _E(b) << Str_Text(path) << _E(.) << " " << equals << " " << _E(>); switch(var->type) { case CVT_BYTE: os << CV_BYTE(var); break; case CVT_INT: os << CV_INT(var); break; case CVT_FLOAT: os << CV_FLOAT(var); break; case CVT_CHARPTR: os << "\"" << CV_CHARPTR(var) << "\""; break; case CVT_URIPTR: { os << "\"" << (CV_URIPTR(var)? CV_URIPTR(var)->asText() : "") << "\""; break; } default: DENG_ASSERT(false); break; } os << _E(<); return str; } void Con_PrintCVar(cvar_t* var, char const *prefix) { LOG_SCR_MSG("%s") << Con_VarAsStyledText(var, prefix); } static int addVariableToKnownWords(CVarDirectory::Node& node, void* /*parameters*/) { //DENG_ASSERT(parameters); cvar_t* var = reinterpret_cast( node.userPointer() ); //uint* index = (uint*) parameters; if(var && !(var->flags & CVF_HIDE)) { Con_AddKnownWord(WT_CVAR, var); } return 0; // Continue iteration. } void Con_AddKnownWordsForVariables() { if(!cvarDirectory) return; cvarDirectory->traverse(PathTree::NoBranch, NULL, CVarDirectory::no_hash, addVariableToKnownWords); } #ifdef DENG_DEBUG typedef struct { uint count; cvartype_t type; dd_bool hidden; dd_bool ignoreHidden; } countvariableparams_t; static int countVariable(CVarDirectory::Node& node, void* parameters) { DENG_ASSERT(parameters); countvariableparams_t* p = (countvariableparams_t*) parameters; cvar_t* var = reinterpret_cast( node.userPointer() ); if(!(p->ignoreHidden && (var->flags & CVF_HIDE))) { if(!VALID_CVARTYPE(p->type) && !p->hidden) { if(!p->ignoreHidden || !(var->flags & CVF_HIDE)) ++(p->count); } else if((p->hidden && (var->flags & CVF_HIDE)) || (VALID_CVARTYPE(p->type) && p->type == CVar_Type(var))) { ++(p->count); } } return 0; // Continue iteration. } D_CMD(PrintVarStats) { DENG2_UNUSED3(src, argc, argv); uint numCVars = 0, numCVarsHidden = 0; LOG_SCR_MSG(_E(b) "Console Variable Statistics:"); if(cvarDirectory) { countvariableparams_t p; p.hidden = false; p.ignoreHidden = false; for(uint i = uint(CVT_BYTE); i < uint(CVARTYPE_COUNT); ++i) { p.count = 0; p.type = cvartype_t(i); cvarDirectory->traverse(PathTree::NoBranch, NULL, CVarDirectory::no_hash, countVariable, &p); LOGDEV_SCR_MSG("%12s: %i") << Str_Text(CVar_TypeName(p.type)) << p.count; } p.count = 0; p.type = cvartype_t(-1); p.hidden = true; cvarDirectory->traverse(PathTree::NoBranch, NULL, CVarDirectory::no_hash, countVariable, &p); numCVars = cvarDirectory->size(); numCVarsHidden = p.count; } LOG_SCR_MSG(" Total: %i\n Hidden: %i") << numCVars << numCVarsHidden; if(cvarDirectory) { cvarDirectory->debugPrintHashDistribution(); cvarDirectory->debugPrint(CVARDIRECTORY_DELIMITER); } return true; } #endif doomsday-stable-1.15.7/doomsday/libdoomsday/src/console/exec.cpp0000664000175000017500000011026012641367671024203 0ustar jaakkojaakko/** @file console.cpp Console subsystem. * * @todo The Console subsystem should be rewritten to be a de::System and it * should use Doomsday Script as the underlying engine; everything should be * mapped to Doomsday Script processes, functions, variables, etc., making the * Console a mere convenience layer. -jk * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/console/exec.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "doomsday/console/knownword.h" #include "doomsday/console/cmd.h" #include "doomsday/console/var.h" #include "doomsday/console/alias.h" #include "doomsday/filesys/fs_main.h" #include "doomsday/filesys/fs_util.h" #include "doomsday/uri.h" #include "dd_share.h" using namespace de; #define SC_EMPTY_QUOTE -1 // Length of the print buffer. Used in conPrintf and in Console_Message. // If console messages are longer than this, an error will occur. // Needed because we can't sizeof a malloc'd block. #define PRBUFF_SIZE 0x2000 // Operators for the "if" command. enum { IF_EQUAL, IF_NOT_EQUAL, IF_GREATER, IF_LESS, IF_GEQUAL, IF_LEQUAL }; #define CMDTYPESTR(src) \ (src == CMDS_DDAY? "a direct call" \ : src == CMDS_GAME? "a game library call" \ : src == CMDS_CONSOLE? "the console" \ : src == CMDS_BIND? "a binding" \ : src == CMDS_CONFIG? "a cfg file" \ : src == CMDS_PROFILE? "a player profile" \ : src == CMDS_CMDLINE? "the command line" \ : src == CMDS_SCRIPT? "an action command" : "???") typedef struct execbuff_s { dd_bool used; // Is this in use? timespan_t when; // System time when to execute the command. byte source; // Where the command came from // (console input, a cfg file etc..) dd_bool isNetCmd; // Command was sent over the net to us. char subCmd[1024]; // A single command w/args. } execbuff_t; D_CMD(AddSub); D_CMD(IncDec); D_CMD(Alias); D_CMD(Echo); D_CMD(Help); D_CMD(If); D_CMD(Parse); D_CMD(Quit); D_CMD(Repeat); D_CMD(Toggle); D_CMD(Wait); D_CMD(InspectMobj); D_CMD(DebugCrash); D_CMD(DebugError); static int executeSubCmd(const char *subCmd, byte src, dd_bool isNetCmd); static void Con_SplitIntoSubCommands(char const *command, timespan_t markerOffset, byte src, dd_bool isNetCmd); static void Con_ClearExecBuffer(void); byte ConsoleSilent = false; static dd_bool ConsoleInited; // Has Con_Init() been called? static execbuff_t * exBuff; static int exBuffSize; static execbuff_t * curExec; void Con_Register(void) { C_CMD("add", NULL, AddSub); C_CMD("after", "is", Wait); C_CMD("alias", NULL, Alias); C_CMD("dec", NULL, IncDec); C_CMD("echo", "s*", Echo); C_CMD("print", "s*", Echo); C_CMD("exec", "s*", Parse); C_CMD("if", NULL, If); C_CMD("inc", NULL, IncDec); C_CMD("repeat", "ifs", Repeat); C_CMD("sub", NULL, AddSub); C_CMD("toggle", "s", Toggle); #ifdef _DEBUG C_CMD("crash", NULL, DebugCrash); #endif Con_DataRegister(); } static void PrepareCmdArgs(cmdargs_t *cargs, const char *lpCmdLine) { #define IS_ESC_CHAR(x) ((x) == '"' || (x) == '\\' || (x) == '{' || (x) == '}') size_t i, len = strlen(lpCmdLine); // Prepare the data. memset(cargs, 0, sizeof(cmdargs_t)); strcpy(cargs->cmdLine, lpCmdLine); // Prepare. for(i = 0; i < len; ++i) { // Whitespaces are separators. if(DENG_ISSPACE(cargs->cmdLine[i])) cargs->cmdLine[i] = 0; if(cargs->cmdLine[i] == '\\' && IS_ESC_CHAR(cargs->cmdLine[i + 1])) { // Escape sequence. memmove(cargs->cmdLine + i, cargs->cmdLine + i + 1, sizeof(cargs->cmdLine) - i - 1); len--; continue; } if(cargs->cmdLine[i] == '"') { // Find the end. size_t start = i; cargs->cmdLine[i] = 0; for(++i; i < len && cargs->cmdLine[i] != '"'; ++i) { if(cargs->cmdLine[i] == '\\' && IS_ESC_CHAR(cargs->cmdLine[i + 1])) // Escape sequence? { memmove(cargs->cmdLine + i, cargs->cmdLine + i + 1, sizeof(cargs->cmdLine) - i - 1); len--; continue; } } // Quote not terminated? if(i == len) break; // An empty set of quotes? if(i == start + 1) cargs->cmdLine[i] = SC_EMPTY_QUOTE; else cargs->cmdLine[i] = 0; } if(cargs->cmdLine[i] == '{') { // Find matching end, braces are another notation for quotes. int level = 0; size_t start = i; cargs->cmdLine[i] = 0; for(++i; i < len; ++i) { if(cargs->cmdLine[i] == '\\' && IS_ESC_CHAR(cargs->cmdLine[i + 1])) // Escape sequence? { memmove(cargs->cmdLine + i, cargs->cmdLine + i + 1, sizeof(cargs->cmdLine) - i - 1); len--; i++; continue; } if(cargs->cmdLine[i] == '}') { if(!level) break; level--; } if(cargs->cmdLine[i] == '{') level++; } // Quote not terminated? if(i == len) break; // An empty set of braces? if(i == start + 1) cargs->cmdLine[i] = SC_EMPTY_QUOTE; else cargs->cmdLine[i] = 0; } } // Scan through the cmdLine and get the beginning of each token. cargs->argc = 0; for(i = 0; i < len; ++i) { if(!cargs->cmdLine[i]) continue; // Is this an empty quote? if(cargs->cmdLine[i] == char(SC_EMPTY_QUOTE)) cargs->cmdLine[i] = 0; // Just an empty string. cargs->argv[cargs->argc++] = cargs->cmdLine + i; i += strlen(cargs->cmdLine + i); } #undef IS_ESC_CHAR } dd_bool Con_Init(void) { if(ConsoleInited) return true; LOG_SCR_VERBOSE("Initializing the console..."); exBuff = NULL; exBuffSize = 0; ConsoleInited = true; return true; } void Con_Shutdown(void) { if(!ConsoleInited) return; LOG_SCR_VERBOSE("Shutting down the console..."); Con_ClearExecBuffer(); Con_ShutdownDatabases(); ConsoleInited = false; } #if 0 // should use libshell! #ifdef __CLIENT__ /** * Send a console command to the server. * This shouldn't be called unless we're logged in with the right password. */ static void Con_Send(const char *command, byte src, int silent) { ushort len = (ushort) strlen(command); LOG_AS("Con_Send"); if(len >= 0x8000) { LOGDEV_NET_ERROR("Command is too long, length=%i") << len; return; } Msg_Begin(PKT_COMMAND2); // Mark high bit for silent commands. Writer_WriteUInt16(msgWriter, len | (silent ? 0x8000 : 0)); Writer_WriteUInt16(msgWriter, 0); // flags. Unused at present. Writer_WriteByte(msgWriter, src); Writer_Write(msgWriter, command, len); Msg_End(); Net_SendBuffer(0, 0); } #endif // __CLIENT__ #endif static void Con_QueueCmd(const char *singleCmd, timespan_t atSecond, byte source, dd_bool isNetCmd) { execbuff_t *ptr = NULL; int i; // Look for an empty spot. for(i = 0; i < exBuffSize; ++i) if(!exBuff[i].used) { ptr = exBuff + i; break; } if(ptr == NULL) { // No empty places, allocate a new one. exBuff = (execbuff_t *) M_Realloc(exBuff, sizeof(execbuff_t) * ++exBuffSize); ptr = exBuff + exBuffSize - 1; } ptr->used = true; strcpy(ptr->subCmd, singleCmd); ptr->when = atSecond; ptr->source = source; ptr->isNetCmd = isNetCmd; } static void Con_ClearExecBuffer(void) { M_Free(exBuff); exBuff = NULL; exBuffSize = 0; } /** * The execbuffer is used to schedule commands for later. * * @return @c false, if an executed command fails. */ static dd_bool Con_CheckExecBuffer(void) { #define BUFFSIZE 1024 /// @todo Rewrite all of this; use de::String. -jk // dd_bool allDone; dd_bool ret = true; int i; //, count = 0; char storage[BUFFSIZE]; storage[255] = 0; // do // We'll keep checking until all is done. { //allDone = true; TimeDelta const now = TimeDelta::sinceStartOfProcess(); // Execute the commands whose time has come. for(i = 0; i < exBuffSize; ++i) { execbuff_t *ptr = exBuff + i; if(!ptr->used || ptr->when > now) continue; // We'll now execute this command. curExec = ptr; ptr->used = false; strncpy(storage, ptr->subCmd, BUFFSIZE-1); if(!executeSubCmd(storage, ptr->source, ptr->isNetCmd)) ret = false; //allDone = false; } // if(count++ > 100) break; // Don't hang here. /* { DENG_ASSERT(!"Execution buffer overflow"); LOG_SCR_ERROR("Console execution buffer overflow! Everything canceled!"); Con_ClearExecBuffer(); break; }*/ } //while(!allDone); return ret; #undef BUFFSIZE } void Con_Ticker(timespan_t /*time*/) { Con_CheckExecBuffer(); } /** * expCommand gets reallocated in the expansion process. * This could be bit more clever. */ static void expandWithArguments(char **expCommand, cmdargs_t *args) { int p; char *text = *expCommand; size_t i, off, size = strlen(text) + 1; for(i = 0; text[i]; ++i) { if(text[i] == '%' && (text[i + 1] >= '1' && text[i + 1] <= '9')) { char *substr; int aidx = text[i + 1] - '1' + 1; // Expand! (or delete) if(aidx > args->argc - 1) substr = (char *) ""; else substr = args->argv[aidx]; // First get rid of the %n. memmove(text + i, text + i + 2, size - i - 2); // Reallocate. off = strlen(substr); text = *expCommand = (char *) M_Realloc(*expCommand, size += off - 2); if(off) { // Make room for the insert. memmove(text + i + off, text + i, size - i - off); memcpy(text + i, substr, off); } i += off - 1; } else if(text[i] == '%' && text[i + 1] == '0') { // First get rid of the %0. memmove(text + i, text + i + 2, size - i - 2); text = *expCommand = (char *) M_Realloc(*expCommand, size -= 2); for(p = args->argc - 1; p > 0; p--) { off = strlen(args->argv[p]) + 1; text = *expCommand = (char *) M_Realloc(*expCommand, size += off); memmove(text + i + off, text + i, size - i - off); text[i] = ' '; memcpy(text + i + 1, args->argv[p], off - 1); } } } } /** * The command is executed forthwith!! */ static int executeSubCmd(const char *subCmd, byte src, dd_bool isNetCmd) { cmdargs_t args; ccmd_t *ccmd; cvar_t *cvar; calias_t *cal; PrepareCmdArgs(&args, subCmd); if(!args.argc) return true; /* if(args.argc == 1) // Possibly a control command? { if(P_ControlExecute(args.argv[0])) { // It was a control command. No further processing is // necessary. return true; } } */ #if 0 #ifdef __CLIENT__ // If logged in, send command to server at this point. if(!isServer && netLoggedIn) { // We have logged in on the server. Send the command there. Con_Send(subCmd, src, ConsoleSilent); return true; } #endif #endif // Try to find a matching console command. ccmd = Con_FindCommandMatchArgs(&args); if(ccmd != NULL) { // Found a match. Are we allowed to execute? dd_bool canExecute = true; // Trying to issue a command requiring a loaded game? // dj: This should be considered a short-term solution. Ideally we want some namespacing mechanics. if((ccmd->flags & CMDF_NO_NULLGAME) && App::game().isNull()) { LOG_SCR_ERROR("Execution of command '%s' is only allowed when a game is loaded") << ccmd->name; return true; } /// @todo Access control needs revising. -jk #if 0 // A dedicated server, trying to execute a ccmd not available to us? if(isDedicated && (ccmd->flags & CMDF_NO_DEDICATED)) { LOG_SCR_ERROR("Execution of command '%s' not possible in dedicated mode") << ccmd->name; return true; } // Net commands sent to servers have extra protection. if(isServer && isNetCmd) { // Is the command permitted for use by clients? if(ccmd->flags & CMDF_CLIENT) { LOG_NET_ERROR("Execution of command '%s' blocked (client attempted invocation);" "this command is not permitted for use by clients") << ccmd->name; return true; } // Are ANY commands from this (remote) src permitted for use // by our clients? // NOTE: // This is an interim measure to protect against abuse of the // most vulnerable invocation methods. // Once all console commands are updated with the correct usage // flags we can then remove these restrictions or make them // optional for servers. // // The next step will then be allowing select console commands // to be executed by non-logged in clients. switch(src) { case CMDS_UNKNOWN: case CMDS_CONFIG: case CMDS_PROFILE: case CMDS_CMDLINE: case CMDS_SCRIPT: LOG_NET_ERROR("Execution of command '%s' blocked (client attempted invocation via %s); " "this method is not permitted by clients") << ccmd->name << CMDTYPESTR(src); return true; default: break; } } #endif // Is the src permitted for this command? switch(src) { case CMDS_UNKNOWN: canExecute = false; break; case CMDS_DDAY: if(ccmd->flags & CMDF_DDAY) canExecute = false; break; case CMDS_GAME: if(ccmd->flags & CMDF_GAME) canExecute = false; break; case CMDS_CONSOLE: if(ccmd->flags & CMDF_CONSOLE) canExecute = false; break; case CMDS_BIND: if(ccmd->flags & CMDF_BIND) canExecute = false; break; case CMDS_CONFIG: if(ccmd->flags & CMDF_CONFIG) canExecute = false; break; case CMDS_PROFILE: if(ccmd->flags & CMDF_PROFILE) canExecute = false; break; case CMDS_CMDLINE: if(ccmd->flags & CMDF_CMDLINE) canExecute = false; break; case CMDS_SCRIPT: if(ccmd->flags & CMDF_DED) canExecute = false; break; default: return true; } if(!canExecute) { LOG_SCR_ERROR("'%s' cannot be executed via %s") << ccmd->name << CMDTYPESTR(src); return true; } if(canExecute) { /** * Execute the command! * \note Console command execution may invoke a full update of the * console databases; thus the @c ccmd pointer may be invalid after * this call. */ int result; if((result = ccmd->execFunc(src, args.argc, args.argv)) == false) { LOG_SCR_ERROR("'%s' failed") << args.argv[0]; } return result; } return true; } // Then try the cvars? cvar = Con_FindVariable(args.argv[0]); if(cvar != NULL) { dd_bool outOfRange = false, setting = false, hasCallback; /** * \note Change notification callback execution may invoke * a full update of the console databases; thus the @c cvar * pointer may be invalid once a callback executes. */ hasCallback = (cvar->notifyChanged != 0); if(args.argc == 2 || (args.argc == 3 && !qstricmp(args.argv[1], "force"))) { char* argptr = args.argv[args.argc - 1]; dd_bool forced = args.argc == 3; setting = true; if(cvar->flags & CVF_READ_ONLY) { CVar_PrintReadOnlyWarning(cvar); } else if((cvar->flags & CVF_PROTECTED) && !forced) { AutoStr* name = CVar_ComposePath(cvar); LOG_SCR_NOTE("%s is protected; you shouldn't change its value -- " "use the command: " _E(b) "'%s force %s'" _E(.) " to modify it anyway") << Str_Text(name) << Str_Text(name) << argptr; } else { switch(cvar->type) { case CVT_BYTE: { byte val = (byte) strtol(argptr, NULL, 0); if(!forced && ((!(cvar->flags & CVF_NO_MIN) && val < cvar->min) || (!(cvar->flags & CVF_NO_MAX) && val > cvar->max))) outOfRange = true; else CVar_SetInteger(cvar, val); break; } case CVT_INT: { int val = strtol(argptr, NULL, 0); if(!forced && ((!(cvar->flags & CVF_NO_MIN) && val < cvar->min) || (!(cvar->flags & CVF_NO_MAX) && val > cvar->max))) outOfRange = true; else CVar_SetInteger(cvar, val); break; } case CVT_FLOAT: { float val = strtod(argptr, NULL); if(!forced && ((!(cvar->flags & CVF_NO_MIN) && val < cvar->min) || (!(cvar->flags & CVF_NO_MAX) && val > cvar->max))) outOfRange = true; else CVar_SetFloat(cvar, val); break; } case CVT_CHARPTR: CVar_SetString(cvar, argptr); break; case CVT_URIPTR: /// @todo Sanitize and validate against known schemas. CVar_SetUri(cvar, de::Uri(argptr, RC_NULL)); break; default: break; } } } if(outOfRange) { AutoStr* name = CVar_ComposePath(cvar); if(!(cvar->flags & (CVF_NO_MIN | CVF_NO_MAX))) { char temp[20]; strcpy(temp, M_TrimmedFloat(cvar->min)); LOG_SCR_ERROR("%s <= %s <= %s") << temp << Str_Text(name) << M_TrimmedFloat(cvar->max); } else if(cvar->flags & CVF_NO_MAX) { LOG_SCR_ERROR("%s >= %s") << Str_Text(name) << M_TrimmedFloat(cvar->min); } else { LOG_SCR_ERROR("%s <= %s") << Str_Text(name) << M_TrimmedFloat(cvar->max); } } else if(!setting) // Show the value. { if(setting && hasCallback) { // Lookup the cvar again - our pointer may have been invalidated. cvar = Con_FindVariable(args.argv[0]); } if(cvar) { // It still exists. Con_PrintCVar(cvar, ""); } } return true; } // How about an alias then? cal = Con_FindAlias(args.argv[0]); if(cal != NULL) { char* expCommand; // Expand the command with arguments. expCommand = (char *) M_Malloc(strlen(cal->command) + 1); strcpy(expCommand, cal->command); expandWithArguments(&expCommand, &args); // Do it, man! Con_SplitIntoSubCommands(expCommand, 0, src, isNetCmd); M_Free(expCommand); return true; } // What *is* that? if(Con_FindCommand(args.argv[0])) { LOG_SCR_WARNING("%s: command arguments invalid") << args.argv[0]; Con_Executef(CMDS_DDAY, false, "help %s", args.argv[0]); } else { LOG_SCR_MSG("%s: unknown identifier") << args.argv[0]; } return false; } /** * Splits the command into subcommands and queues them into the * execution buffer. */ static void Con_SplitIntoSubCommands(const char *command, timespan_t markerOffset, byte src, dd_bool isNetCmd) { #define BUFFSIZE 2048 char subCmd[BUFFSIZE]; int inQuotes = false, escape = false; size_t gPos = 0, scPos = 0, len; // Is there a command to execute? if(!command || command[0] == 0) return; // Jump over initial semicolons. len = strlen(command); while(gPos < len && command[gPos] == ';' && command[gPos] != 0) gPos++; subCmd[0] = subCmd[BUFFSIZE-1] = 0; // The command may actually contain many commands, separated // with semicolons. This isn't a very clear algorithm... for(; command[gPos];) { escape = false; if(inQuotes && command[gPos] == '\\') // Escape sequence? { subCmd[scPos++] = command[gPos++]; escape = true; } if(command[gPos] == '"' && !escape) inQuotes = !inQuotes; // Collect characters. subCmd[scPos++] = command[gPos++]; if(subCmd[0] == ' ') scPos = 0; // No spaces in the beginning. if((command[gPos] == ';' && !inQuotes) || command[gPos] == 0) { while(gPos < len && command[gPos] == ';' && command[gPos] != 0) gPos++; // The subcommand ends. subCmd[scPos] = 0; } else { continue; } // Queue it. Con_QueueCmd(subCmd, TimeDelta::sinceStartOfProcess() + markerOffset, src, isNetCmd); scPos = 0; } #undef BUFFSIZE } int Con_Execute(byte src, const char *command, int silent, dd_bool netCmd) { int ret; if(silent) ConsoleSilent = true; Con_SplitIntoSubCommands(command, 0, src, netCmd); ret = Con_CheckExecBuffer(); if(silent) ConsoleSilent = false; return ret; } int Con_Executef(byte src, int silent, const char *command, ...) { va_list argptr; char buffer[4096]; va_start(argptr, command); dd_vsnprintf(buffer, sizeof(buffer), command, argptr); va_end(argptr); return Con_Execute(src, buffer, silent, false); } dd_bool Con_Parse(Path const &fileName, dd_bool silently) { // Relative paths are relative to the native working directory. NativePath fn = NativePath::workPath() / NativePath(fileName).expand(); if(!QFile::exists(fn)) return false; QFile file(fn); if(!file.open(QIODevice::ReadOnly | QIODevice::Text)) return false; // This file is filled with console commands. QTextStream in(&file); int currentLine = 1; while(!in.atEnd()) { // Each line is a command. String const line = String(in.readLine()).leftStrip(); if(!line.isEmpty() && line.first() != '#') { // Execute the commands silently. if(!Con_Execute(CMDS_CONFIG, line.toUtf8().constData(), silently, false)) { if(!silently) { LOG_SCR_WARNING("%s(%i): error executing command \"%s\"") << fn.pretty() << currentLine << line; } } } currentLine += 1; } return true; } /** * Create an alias. */ static void makeAlias(char *aName, char *command) { calias_t *cal = Con_FindAlias(aName); dd_bool remove = false; // Will we remove this alias? if(command == NULL) remove = true; else if(command[0] == 0) remove = true; if(cal && remove) // This alias will be removed. { Con_DeleteAlias(cal); return; // We're done. } // Does the alias already exist? if(cal) { cal->command = (char *) M_Realloc(cal->command, strlen(command) + 1); strcpy(cal->command, command); return; } // We need to create a new alias. Con_AddAlias(aName, command); } D_CMD(Alias) { DENG2_UNUSED(src); if(argc != 3 && argc != 2) { LOG_SCR_NOTE("Usage: %s (alias) (cmd)") << argv[0]; LOG_SCR_MSG("Example: alias bigfont \"font size 3\""); LOG_SCR_MSG("Use %%1-%%9 to pass the alias arguments to the command."); return true; } makeAlias(argv[1], argc == 3 ? argv[2] : NULL); if(argc != 3) { LOG_SCR_MSG("Alias '%s' deleted") << argv[1]; } return true; } D_CMD(Parse) { DENG2_UNUSED(src); int i; for(i = 1; i < argc; ++i) { LOG_SCR_MSG("Parsing \"%s\"") << argv[i]; Con_Parse(argv[i], false /*not silent*/); } return true; } D_CMD(Wait) { DENG2_UNUSED2(src, argc); timespan_t offset; offset = strtod(argv[1], NULL) / 35; // Offset in seconds. if(offset < 0) offset = 0; Con_SplitIntoSubCommands(argv[2], offset, CMDS_CONSOLE, false); return true; } D_CMD(Repeat) { DENG2_UNUSED2(src, argc); int count; timespan_t interval, offset; count = atoi(argv[1]); interval = strtod(argv[2], NULL) / 35; // In seconds. offset = 0; while(count-- > 0) { offset += interval; Con_SplitIntoSubCommands(argv[3], offset, CMDS_CONSOLE, false); } return true; } D_CMD(Echo) { DENG2_UNUSED(src); int i; for(i = 1; i < argc; ++i) { LOG_MSG("%s") << argv[i]; } return true; } static dd_bool cvarAddSub(const char* name, float delta, dd_bool force) { cvar_t* cvar = Con_FindVariable(name); float val; if(!cvar) { if(name && name[0]) LOG_SCR_ERROR("%s is not a known cvar") << name; return false; } if(cvar->flags & CVF_READ_ONLY) { CVar_PrintReadOnlyWarning(cvar); return false; } val = CVar_Float(cvar) + delta; if(!force) { if(!(cvar->flags & CVF_NO_MAX) && val > cvar->max) val = cvar->max; if(!(cvar->flags & CVF_NO_MIN) && val < cvar->min) val = cvar->min; } CVar_SetFloat(cvar, val); return true; } /** * Rather messy, wouldn't you say? */ D_CMD(AddSub) { DENG2_UNUSED(src); dd_bool force = false; float delta = 0; if(argc <= 2) { LOG_SCR_NOTE("Usage: %s (cvar) (val) (force)") << argv[0]; LOG_SCR_MSG("Use force to make cvars go off limits."); return true; } if(argc >= 4) { force = !qstricmp(argv[3], "force"); } delta = strtod(argv[2], NULL); if(!qstricmp(argv[0], "sub")) delta = -delta; return cvarAddSub(argv[1], delta, force); } /** * Rather messy, wouldn't you say? */ D_CMD(IncDec) { DENG2_UNUSED(src); dd_bool force = false; cvar_t* cvar; float val; if(argc == 1) { LOG_SCR_NOTE("Usage: %s (cvar) (force)") << argv[0]; LOG_SCR_MSG("Use force to make cvars go off limits."); return true; } if(argc >= 3) { force = !qstricmp(argv[2], "force"); } cvar = Con_FindVariable(argv[1]); if(!cvar) return false; if(cvar->flags & CVF_READ_ONLY) { LOG_SCR_ERROR("%s (cvar) is read-only, it cannot be changed (even with force)") << argv[1]; return false; } val = CVar_Float(cvar); val += !qstricmp(argv[0], "inc")? 1 : -1; if(!force) { if(!(cvar->flags & CVF_NO_MAX) && val > cvar->max) val = cvar->max; if(!(cvar->flags & CVF_NO_MIN) && val < cvar->min) val = cvar->min; } CVar_SetFloat(cvar, val); return true; } /** * Toggle the value of a variable between zero and nonzero. */ D_CMD(Toggle) { DENG2_UNUSED2(src, argc); cvar_t *cvar = Con_FindVariable(argv[1]); if(!cvar) return false; CVar_SetInteger(cvar, CVar_Integer(cvar)? 0 : 1); return true; } /** * Execute a command if the condition passes. */ D_CMD(If) { struct { const char *opstr; uint op; } operators[] = { {"not", IF_NOT_EQUAL}, {"=", IF_EQUAL}, {">", IF_GREATER}, {"<", IF_LESS}, {">=", IF_GEQUAL}, {"<=", IF_LEQUAL}, {NULL, 0} }; uint i, oper; cvar_t *var; dd_bool isTrue = false; if(argc != 5 && argc != 6) { LOG_SCR_NOTE("Usage: %s (cvar) (operator) (value) (cmd) (else-cmd)") << argv[0]; LOG_SCR_MSG("Operator must be one of: not, =, >, <, >=, <="); LOG_SCR_MSG("The (else-cmd) can be omitted."); return true; } var = Con_FindVariable(argv[1]); if(!var) return false; // Which operator? for(i = 0; operators[i].opstr; ++i) if(!qstricmp(operators[i].opstr, argv[2])) { oper = operators[i].op; break; } if(!operators[i].opstr) return false; // Bad operator. // Value comparison depends on the type of the variable. switch(var->type) { case CVT_BYTE: case CVT_INT: { int value = (var->type == CVT_INT ? CV_INT(var) : CV_BYTE(var)); int test = strtol(argv[3], 0, 0); isTrue = (oper == IF_EQUAL ? value == test : oper == IF_NOT_EQUAL ? value != test : oper == IF_GREATER ? value > test : oper == IF_LESS ? value < test : oper == IF_GEQUAL ? value >= test : value <= test); break; } case CVT_FLOAT: { float value = CV_FLOAT(var); float test = strtod(argv[3], 0); isTrue = (oper == IF_EQUAL ? value == test : oper == IF_NOT_EQUAL ? value != test : oper == IF_GREATER ? value > test : oper == IF_LESS ? value < test : oper == IF_GEQUAL ? value >= test : value <= test); break; } case CVT_CHARPTR: { int comp = qstricmp(CV_CHARPTR(var), argv[3]); isTrue = (oper == IF_EQUAL ? comp == 0 : oper == IF_NOT_EQUAL ? comp != 0 : oper == IF_GREATER ? comp > 0 : oper == IF_LESS ? comp < 0 : oper == IF_GEQUAL ? comp >= 0 : comp <= 0); } break; default: DENG_ASSERT(!"CCmdIf: Invalid cvar type"); return false; } // Should the command be executed? if(isTrue) { Con_Execute(src, argv[4], ConsoleSilent, false); } else if(argc == 6) { Con_Execute(src, argv[5], ConsoleSilent, false); } return true; } D_CMD(DebugCrash) { DENG2_UNUSED3(src, argv, argc); int* ptr = (int*) 0x123; // Goodbye cruel world. *ptr = 0; return true; } D_CMD(HelpWhat); D_CMD(HelpApropos); D_CMD(ListAliases); D_CMD(ListCmds); D_CMD(ListVars); #if _DEBUG D_CMD(PrintVarStats); #endif static bool inited; D_CMD(ListCmds); D_CMD(HelpApropos); void Con_DataRegister() { C_CMD("apropos", "s", HelpApropos); C_CMD("listaliases", NULL, ListAliases); C_CMD("listcmds", NULL, ListCmds); C_CMD("listvars", NULL, ListVars); #ifdef DENG_DEBUG C_CMD("varstats", NULL, PrintVarStats); #endif } void Con_InitDatabases(void) { if(inited) return; Con_InitVariableDirectory(); Con_InitCommands(); Con_InitAliases(); Con_ClearKnownWords(); inited = true; } void Con_ClearDatabases(void) { if(!inited) return; Con_ClearKnownWords(); Con_ClearAliases(); Con_ClearCommands(); Con_ClearVariables(); } void Con_ShutdownDatabases(void) { if(!inited) return; Con_ClearDatabases(); Con_DeinitVariableDirectory(); inited = false; } String Con_GameAsStyledText(de::game::Game const *game) { DENG2_ASSERT(game != 0); return String(_E(1)) + game->id() + _E(.); } static int printKnownWordWorker(knownword_t const *word, void *parameters) { DENG_ASSERT(word); uint *numPrinted = (uint *) parameters; switch(word->type) { case WT_CCMD: { ccmd_t *ccmd = (ccmd_t *) word->data; if(ccmd->prevOverload) { return 0; // Skip overloaded variants. } LOG_SCR_MSG("%s") << Con_CmdAsStyledText(ccmd); break; } case WT_CVAR: { cvar_t *cvar = (cvar_t *) word->data; if(cvar->flags & CVF_HIDE) { return 0; // Skip hidden variables. } Con_PrintCVar(cvar, ""); break; } case WT_CALIAS: LOG_SCR_MSG("%s") << Con_AliasAsStyledText((calias_t *) word->data); break; case WT_GAME: LOG_SCR_MSG("%s") << Con_GameAsStyledText((game::Game const *) word->data); break; default: DENG_ASSERT(false); break; } if(numPrinted) ++(*numPrinted); return 0; // Continue iteration. } D_CMD(ListVars) { DENG_UNUSED(src); uint numPrinted = 0; LOG_SCR_MSG(_E(b) "Console variables:"); Con_IterateKnownWords(argc > 1? argv[1] : 0, WT_CVAR, printKnownWordWorker, &numPrinted); LOG_SCR_MSG("Found %i console variables") << numPrinted; return true; } D_CMD(ListCmds) { DENG_UNUSED(src); LOG_SCR_MSG(_E(b) "Console commands:"); uint numPrinted = 0; Con_IterateKnownWords(argc > 1? argv[1] : 0, WT_CCMD, printKnownWordWorker, &numPrinted); LOG_SCR_MSG("Found %i console commands") << numPrinted; return true; } D_CMD(ListAliases) { DENG_UNUSED(src); LOG_SCR_MSG(_E(b) "Aliases:"); uint numPrinted = 0; Con_IterateKnownWords(argc > 1? argv[1] : 0, WT_CALIAS, printKnownWordWorker, &numPrinted); LOG_SCR_MSG("Found %i aliases") << numPrinted; return true; } doomsday-stable-1.15.7/doomsday/libdoomsday/src/dualstring.cpp0000664000175000017500000000456612641367671024004 0ustar jaakkojaakko/** @file dualstring.cpp * * Utility class for strings that need both Unicode and C-string access. * @ingroup data * * @authors Copyright (c) 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/dualstring.h" using namespace de; DualString::DualString() { _str = Str_NewStd(); } DualString::DualString(const DualString& other) : String(other) { _str = Str_NewStd(); Str_Copy(_str, other._str); } DualString::DualString(const String& other) : String(other) { _str = Str_NewStd(); } void DualString::clear() { String::clear(); Str_Truncate(_str, 0); // existing pointer remains valid } DualString::~DualString() { Str_Delete(_str); } DualString& DualString::operator = (const DualString& other) { static_cast(*this) = other; Str_Copy(_str, other._str); return *this; } DualString& DualString::operator = (const char* cStr) { static_cast(*this) = cStr; return *this; } DualString& DualString::operator = (const String& str) { static_cast(*this) = str; return *this; } const ::Str* DualString::toStrAscii() const { Str_Set(_str, toLatin1().constData()); return _str; } const ::Str* DualString::toStrUtf8() const { Str_Set(_str, toUtf8().constData()); return _str; } ::Str* DualString::toStr() { Str_Set(_str, toUtf8().constData()); return _str; } void DualString::update() { static_cast(*this) = QString::fromUtf8(Str_Text(_str)); } const char* DualString::asciiCStr() { return Str_Text(toStrAscii()); } const char* DualString::utf8CStr() { return Str_Text(toStrUtf8()); } doomsday-stable-1.15.7/doomsday/libdoomsday/src/help.cpp0000664000175000017500000001235312641367671022551 0ustar jaakkojaakko/** @file help.cpp Runtime help text strings. * * @ingroup base * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "doomsday/help.h" #include "doomsday/console/cmd.h" #include #include #include #include #include using namespace de; typedef QMap StringsByType; // HST_* type => string typedef QMap HelpStrings; // id => typed strings static HelpStrings helps; /** * Parses the given file looking for help strings. The contents of the file are * expected to use UTF-8 encoding. * * @param file File containing help strings. */ void Help_ReadStrings(File const &file) { LOG_RES_VERBOSE("Reading help strings from ") << file.description(); de::Reader reader(file); StringsByType *node = 0; while(!reader.atEnd()) { String line = reader.readLine().trimmed(); // Comments and empty lines are ignored. if(line.isEmpty() || line.startsWith("#")) continue; // A new node? if(line.startsWith("[")) { int end = line.indexOf(']'); String id = line.mid(1, end > 0? end - 1 : -1).trimmed().toLower(); node = &helps.insert(id, StringsByType()).value(); LOG_TRACE_DEBUGONLY("Help node '%s'", id); } else if(node && line.contains('=')) // It must be a key? { int type = HST_DESCRIPTION; if(line.startsWith("cv", Qt::CaseInsensitive)) { type = HST_CONSOLE_VARIABLE; } else if(line.startsWith("def", Qt::CaseInsensitive)) { type = HST_DEFAULT_VALUE; } else if(line.startsWith("inf", Qt::CaseInsensitive)) { type = HST_INFO; } // Strip the beginning. line = line.mid(line.indexOf('=') + 1).trimmed(); // The full text is collected here. QString text; // The value may be split over multiple lines. while(!line.isEmpty()) { // Process the current line. bool escape = false; foreach(QChar ch, line) { if(ch == QChar('\\')) { escape = true; } else if(escape) { if (ch == QChar('n') ) text = text % "\n"; else if(ch == QChar('b') ) text = text % "\b"; else if(ch == QChar('\\')) text = text % "\\"; escape = false; } else { text = text % ch; } } // This part has been processed. line.clear(); if(escape) { // Line ended with a backslash; read the next line. line = reader.readLine().trimmed(); } } node->insert(type, text); LOG_TRACE_DEBUGONLY("Help string (type %i): \"%s\"", type << text); } } } HelpId DH_Find(char const *id) { // The identifiers are case insensitive. HelpStrings::const_iterator found = helps.constFind(String(id).lower()); if(found != helps.constEnd()) { return &found.value(); } return 0; } char const *DH_GetString(HelpId found, int type) { if(!found) return 0; if(type < 0 || type > NUM_HELPSTRING_TYPES) return 0; StringsByType const *hs = reinterpret_cast(found); StringsByType::const_iterator i = hs->constFind(type); if(i != hs->constEnd()) { return Str_Text(AutoStr_FromTextStd(i.value().toUtf8().constData())); } return 0; } void DD_InitHelp() { LOG_AS("DD_InitHelp"); try { Help_ReadStrings(App::packageLoader().package("net.dengine.base") .root().locate("helpstrings.txt")); } catch(Error const &er) { LOG_RES_WARNING("") << er.asText(); } } void DD_ShutdownHelp() { helps.clear(); } D_CMD(LoadHelp) { DENG2_UNUSED3(src, argc, argv); DD_ShutdownHelp(); DD_InitHelp(); return true; } void DH_Register() { C_CMD("loadhelp", "", LoadHelp); } doomsday-stable-1.15.7/doomsday/libdoomsday/src/world/0000775000175000017500000000000012641367671022240 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/libdoomsday/src/world/thinkerdata.cpp0000664000175000017500000000442412641367671025246 0ustar jaakkojaakko/** @file thinkerdata.cpp Base class for thinker private data. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/world/thinkerdata.h" using namespace de; DENG2_PIMPL(ThinkerData) { thinker_s *think; Record info; Instance(Public *i) : Base(i), think(0) {} Instance(Public *i, Instance const &other) : Base(i) , think(other.think) , info(other.info) {} ~Instance() { DENG2_FOR_PUBLIC_AUDIENCE2(Deletion, i) { i->thinkerBeingDeleted(*think); } } DENG2_PIMPL_AUDIENCE(Deletion) }; DENG2_AUDIENCE_METHOD(ThinkerData, Deletion) ThinkerData::ThinkerData() : d(new Instance(this)) {} ThinkerData::ThinkerData(ThinkerData const &other) : d(new Instance(this, *other.d)) {} void ThinkerData::setThinker(thinker_s *thinker) { d->think = thinker; } void ThinkerData::think() { /// @todo If there is a think function in the Record, call it now. -jk } Thinker::IData *ThinkerData::duplicate() const { return new ThinkerData(*this); } thinker_s &ThinkerData::thinker() { DENG2_ASSERT(d->think != 0); return *d->think; } thinker_s const &ThinkerData::thinker() const { DENG2_ASSERT(d->think != 0); return *d->think; } Record &ThinkerData::info() { return d->info; } Record const &ThinkerData::info() const { return d->info; } #ifdef DENG2_DEBUG duint32 ThinkerData::DebugCounter::total = 0; ThinkerData::DebugValidator ensureAllPrivateDataIsReleased; #endif doomsday-stable-1.15.7/doomsday/libdoomsday/src/world/mobjthinkerdata.cpp0000664000175000017500000000300112641367671026104 0ustar jaakkojaakko/** @file mobjthinkerdata.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/world/mobjthinkerdata.h" using namespace de; DENG2_PIMPL_NOREF(MobjThinkerData) {}; MobjThinkerData::MobjThinkerData() : d(new Instance) {} MobjThinkerData::MobjThinkerData(MobjThinkerData const &other) : ThinkerData(other) , d(new Instance) {} Thinker::IData *MobjThinkerData::duplicate() const { return new MobjThinkerData(*this); } mobj_t *MobjThinkerData::mobj() { return reinterpret_cast(&thinker()); } mobj_t const *MobjThinkerData::mobj() const { return reinterpret_cast(&thinker()); } void MobjThinkerData::stateChanged(state_t const *) { // overridden } doomsday-stable-1.15.7/doomsday/libdoomsday/src/world/thinker.cpp0000664000175000017500000001554712641367671024424 0ustar jaakkojaakko/** @file thinker.cpp Base for all thinkers. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "doomsday/world/thinker.h" #include #include #include using namespace de; DENG2_PIMPL_NOREF(Thinker) { dsize size; thinker_s *base; // owned IData *data; // owned, optional Instance(AllocMethod alloc, dsize sizeInBytes, IData *data_) : size(max(sizeInBytes, sizeof(thinker_s))) , base(0) , data(data_) { if(alloc == AllocateStandard) { base = reinterpret_cast(M_Calloc(size)); base->_flags = THINKF_STD_MALLOC; } else // using memory zone { base = reinterpret_cast(Z_Calloc(size, PU_MAP, NULL)); } if(data) data->setThinker(base); } Instance(Instance const &other) : size(other.size) , base(reinterpret_cast(other.base->_flags & THINKF_STD_MALLOC? M_MemDup(other.base, size) : Z_MemDup(other.base, size))) , data(other.data? other.data->duplicate() : 0) { base->d = data; if(data) data->setThinker(base); } Instance(thinker_s *podThinkerToTake, dsize sizeInBytes) : size(sizeInBytes) , base(podThinkerToTake) , data(reinterpret_cast(podThinkerToTake->d)) // also take ownership of the private data {} ~Instance() { release(); } void release() { if(base) { if(isStandardAllocated()) { M_Free(base); } else { Z_Free(base); } } // Get rid of the private data, too. delete data; } bool isStandardAllocated() const { return base && (base->_flags & THINKF_STD_MALLOC); } void relinquish() { base = 0; data = 0; size = 0; } static void clearBaseToZero(thinker_s *base, dsize size) { bool const stdAlloc = CPP_BOOL(base->_flags & THINKF_STD_MALLOC); memset(base, 0, size); if(stdAlloc) base->_flags |= THINKF_STD_MALLOC; } }; #define STRUCT_MEMBER_ACCESSORS() \ prev (*this, offsetof(thinker_s, prev )) \ , next (*this, offsetof(thinker_s, next )) \ , function(*this, offsetof(thinker_s, function)) \ , id (*this, offsetof(thinker_s, id )) Thinker::Thinker(dsize sizeInBytes, IData *data) : d(new Instance(AllocateStandard, sizeInBytes, data)) , STRUCT_MEMBER_ACCESSORS() { // Default to no public thinker callback. function = Thinker_NoOperation; } Thinker::Thinker(AllocMethod alloc, dsize sizeInBytes, Thinker::IData *data) : d(new Instance(alloc, sizeInBytes, data)) , STRUCT_MEMBER_ACCESSORS() { // Default to no public thinker callback. function = Thinker_NoOperation; } Thinker::Thinker(Thinker const &other) : d(new Instance(*other.d)) , STRUCT_MEMBER_ACCESSORS() {} Thinker::Thinker(thinker_s const &podThinker, dsize sizeInBytes, AllocMethod alloc) : d(new Instance(alloc, sizeInBytes, 0)) , STRUCT_MEMBER_ACCESSORS() { DENG2_ASSERT(d->size == sizeInBytes); memcpy(d->base, &podThinker, sizeInBytes); // Retain the original allocation flag, though. d->base->_flags &= ~THINKF_STD_MALLOC; if(alloc == AllocateStandard) d->base->_flags |= THINKF_STD_MALLOC; if(podThinker.d) { setData(reinterpret_cast(podThinker.d)->duplicate()); } } Thinker::Thinker(thinker_s *podThinkerToTake, de::dsize sizeInBytes) : d(new Instance(podThinkerToTake, sizeInBytes)) , STRUCT_MEMBER_ACCESSORS() {} Thinker &Thinker::operator = (Thinker const &other) { d.reset(new Instance(*other.d)); return *this; } void Thinker::enable(bool yes) { applyFlagOperation(d->base->_flags, duint32(THINKF_DISABLED), yes? SetFlags : UnsetFlags); } void Thinker::zap() { delete d->data; d->data = 0; Instance::clearBaseToZero(d->base, d->size); } bool Thinker::isDisabled() const { return (d->base->_flags & THINKF_DISABLED) != 0; } thinker_s &Thinker::base() { return *d->base; } thinker_s const &Thinker::base() const { return *d->base; } bool Thinker::hasData() const { return d->data != 0; } Thinker::IData &Thinker::data() { DENG2_ASSERT(hasData()); return *d->data; } Thinker::IData const &Thinker::data() const { DENG2_ASSERT(hasData()); return *d->data; } dsize Thinker::sizeInBytes() const { return d->size; } thinker_s *Thinker::take() { DENG2_ASSERT(d->base->d == d->data); thinker_s *th = d->base; d->relinquish(); return th; } void Thinker::putInto(thinker_s &dest) { delete reinterpret_cast(dest.d); memcpy(&dest, d->base, d->size); // Not valid any more. d->relinquish(); } void Thinker::destroy(thinker_s *thinkerBase) { DENG2_ASSERT(thinkerBase != 0); release(*thinkerBase); if(thinkerBase->_flags & THINKF_STD_MALLOC) { M_Free(thinkerBase); } else { Z_Free(thinkerBase); } } void Thinker::release(thinker_s &thinkerBase) { delete reinterpret_cast(thinkerBase.d); thinkerBase.d = 0; } void Thinker::zap(thinker_s &thinkerBase, dsize sizeInBytes) { delete reinterpret_cast(thinkerBase.d); Instance::clearBaseToZero(&thinkerBase, sizeInBytes); } void Thinker::setData(Thinker::IData *data) { if(d->data) delete d->data; d->data = data; d->base->d = data; if(data) { data->setThinker(*this); } } dd_bool Thinker_InStasis(thinker_s const *thinker) { if(!thinker) return false; return (thinker->_flags & THINKF_DISABLED) != 0; } void Thinker_SetStasis(thinker_t *thinker, dd_bool on) { if(thinker) { applyFlagOperation(thinker->_flags, duint32(THINKF_DISABLED), on? SetFlags : UnsetFlags); } } void Thinker_NoOperation(void *) { // do nothing } doomsday-stable-1.15.7/doomsday/dep_directx.pri0000664000175000017500000000041712641367670021023 0ustar jaakkojaakko# Build configuration for DirectX. win32 { isEmpty(DIRECTX_DIR) { error("dep_directx: DirectX SDK path not defined, check your config_user.pri") } INCLUDEPATH += $$DIRECTX_DIR/Include LIBS += -L$$DIRECTX_DIR/Lib/x86 -ldinput8 -ldsound -ldxguid } doomsday-stable-1.15.7/doomsday/net.dengine.base.pack/0000775000175000017500000000000012641367671022040 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/net.dengine.base.pack/helpstrings.txt0000664000175000017500000011143212641367671025145 0ustar jaakkojaakko# Doomsday Help # Generated by conhelp.py out of engine # # CONSOLE COMMANDS: engine # [activatebcontext] desc = Activate a binding context. inf = Input events can only trigger bindings in active binding contexts.\nSEE ALSO:\n- "Bindings" in wiki\n [add] desc = Add something to a cvar. [after] desc = Execute the specified command after a delay. inf = Params: after (tics) (cmd)\nFor example, 'after 35 "echo End"'. [alias] desc = Create aliases for a (set of) console commands. [apropos] desc = Summarize all help containing a search term. [bindcontrol] desc = Bind an input device to a player control. [bindevent] desc = Bind a console command to an event. inf = USAGE:\nbindevent [(context)] (spec) (command)\nThe event specification (spec) is composed of an event descriptor and optionally any additional conditions for the validity of the binding. The specification may be prefixed with a context name; if omitted, the binding is created in the default "game" context.\nEXAMPLES:\nbindevent key-M-down "toggle ctl-run"\nbindevent "mouse-right-up + key-shift" {print "RMB released while Shift down"}\nbindevent shortcut:key-f8 "toggle msg-show"\nbindevent key-equals-down "add view-size 1"\nbindevent key-equals-repeat "add view-size 1"\nSEE ALSO:\n- "Bindings" in wiki\n\n [blc] desc = Set color of light at cursor. inf = Params: blc (red) (green) (blue)\nFor example, 'blc 1 0.5 0.2'. [blclear] desc = Delete all lights. [bldel] desc = Delete current/specified light. [bldup] desc = Duplicate current/specified light, grab it. [bledit] desc = Enter bias light edit mode. [blgrab] desc = Grab current/specified light, or ubgrab. [blhue] desc = Show/hide the hue circle for color selection. [bli] desc = Set intensity of light at cursor. [bllock] desc = Lock current/specified light. [blmenu] desc = Show/hide the bias menu. [blnew] desc = Allocate new light and grab it. [blquit] desc = Exit bias light edit mode. [blsave] desc = Write the current lights to a DED file. [blunlock] desc = Unlock current/specified light. [centerwindow] desc = Center the window on the desktop when in windowed mode. [chat] desc = Broadcast a chat message. [chatnum] desc = Send a chat message to the specified player. [chatto] desc = Send a chat message to the specified player. [clear] desc = Clear the console buffer. [clearbinds] desc = Deletes all existing bindings. [conclose] desc = Close the console prompt. [conlocp] desc = Connect a local player. inf = Params: conlocp (playernum)\nFor example, 'conlocp 1'. [connect] desc = Connect to a server using TCP/IP. [conopen] desc = Open the console prompt. [contoggle] desc = Open/close the console prompt. [deactivatebcontext] desc = Deactivate a binding context. inf = Bindings in deactivated binding contexts cannot be triggered.\nSEE ALSO:\n- "Bindings" in wiki\n [dec] desc = Subtract 1 from a cvar. [delbind] desc = Deletes all bindings to the given console command. [demolump] desc = Write a reference lump file for a demo. inf = Params: demolump (demofile) (lumpfile)\nFor example, 'demolump demo1.dmo DEMO1'. [dir] desc = Print contents of directories. inf = Params: dir (dirs) ...\nFor example, 'dir data/'.\nVirtual files are listed, too.\nPaths are relative to the base path. [displaymode] desc = Print information about the current display mode and window state. inf = SEE ALSO:\n- 'listdisplaymodes'\n- 'setres'\n- 'setfullres'\n- 'setwinres'\n [dump] desc = Dump a data lump currently loaded in memory. inf = Params: dump (name)\nFor example, 'dump PLAYPAL'. [echo] desc = Echo the parameters on separate lines. inf = Params: echo (text) ...\nFor example, 'echo "hello world"' [exec] desc = Loads and executes a file containing console commands. inf = Params: exec (file) ...\nFor example, 'exec "myconfig.cfg"'. [flareconfig] desc = Configure lens flares. [fog] desc = Modify fog settings. [font] desc = Modify console font settings. [help] desc = Show information about the console. [if] desc = Execute a command if the condition is true. [inc] desc = Add 1 to a cvar. [inspectgame] desc = Print detailed information about a registered game to the console. inf = Params: inspectgame (identityKey)\nFor example, 'inspectgame doom1-ultimate'. [inspectmap] desc = Print extended information about the current map to the console. [inspectmaterial] desc = Print extended information about a Material to the console. inf = Params: inspectmaterial (uri)\nFor example, 'inspectmaterial flats:fwater1'. [inspectsavegame] desc = Print detailed information about a saved game session to the console. inf = USAGE:\ninspectsavegame (path)\nThe (path) is an absolute path and name of the saved game session to be inspected. User savegames are located in /home/savegames/< "Game identity key" in wiki >. If the saved session belongs to the currently loaded game, the path component may be omitted.\nEXAMPLES:\nInspect the Hexen savegame "hex1" while the game is loaded:inspectsavegame Hex1\nInspect the Doom savegame "doomsav1" at any time:inspectsavegame /home/savegames/doom1-ultimate/doomsav1 [inspecttexture] desc = Print extended information about a Texture to the console. inf = Params: inspecttexture (uri)\nFor example, 'inspecttexture flats:fwater1'. [kick] desc = Kick client out of the game (server only). inf = Params: kick (playernum)\nFor example, 'kick 1'. [lastupdated] desc = Show when the latest check for updates was made. [listaliases] desc = List all aliases and their expanded forms. [listbcontexts] desc = List all binding contexts and their current state. [listbindings] desc = List all event bindings. [listcmds] desc = List all console commands. [listcontrols] desc = List the names of all player controls. [listdisplaymodes] desc = List all display modes supported by the graphics hardware. inf = SEE ALSO:\n- 'displaymode'\n- 'setres'\n- 'setfullres'\n- 'setwinres'\n [listfiles] desc = List all loaded resource files. [listfonts] desc = List all known fonts. [listgames] desc = List all games. [listinputdevices] desc = List all currently active input devices and their controls. [listlumps] desc = List all loaded virtual resource files from container archives. [listmaps] desc = List all loaded maps. [listmaterials] desc = List all known surface materials. [listmobjtypes] desc = List all known mobj types. [listtextures] desc = List all known textures. [listvars] desc = List all console variables and their values. [load] desc = Load a complete game or one or more data files (e.g., a WAD or a lump). inf = Params: load (name) ...\nFor example, 'load (gamename)' or 'load mylevel.wad' [login] desc = Log in to server console. [logout] desc = Terminate remote connection to server console. [lowres] desc = Select the poorest rendering quality. [ls] desc = Print contents of directories. inf = Params: ls (dirs) ...\nFor example, 'ls data/'.\nVirtual files are listed, too.\nPaths are relative to the base path. [mipmap] desc = Set the mipmapping mode. inf = Params: mipmap (0-5)\n0 = GL_NEAREST\n1 = GL_LINEAR\n2 = GL_NEAREST_MIPMAP_NEAREST\n3 = GL_LINEAR_MIPMAP_NEAREST\n4 = GL_NEAREST_MIPMAP_LINEAR\n5 = GL_LINEAR_MIPMAP_LINEAR [net] desc = Network setup and control. [pausedemo] desc = Pause/resume demo recording. [pausemusic] desc = Pause the currently playing music track. [ping] desc = Ping the server (or a player if you're the server). [playdemo] desc = Play a demo. inf = Params: playdemo (fileName)\nFor example, 'playdemo demo1.dmo'. [playmusic] desc = Play a music track, music lump, external file or a CD track. [playsound] desc = Play a sound effect. [postfx] desc = Set or clear the frame post-processing shader. inf = USAGE:\npostfx (console) (shader) [(time)]\nEvery player has their own frame post-processing effects. The first argument specifies which player will be affected.\nThe frame post-processing shader is changed to "fx.post. (shader) ". If (time) is specified, and there is no shader currently in use, the new shader is faded in in (time) seconds. Otherwise the new shader is taken immediately into use.\nAs a special case, if (shader) is "none", the post-processing shader is faded out and removed.\nAnother special case is when (shader) is "opacity". This will set the opacity of the effect to the value of (time). However, hote that the shader does not necessarily implement opacity as simple alpha blending.\nEXAMPLES:\nFade in the "fx.post.monochrome" shader for player 0 in 2 seconds: 'postfx 0 monochrome 2' [quit!] desc = Exit immediately and return to the OS. [quit] desc = If a game is loaded execute a quit request, otherwise, exit immediately. [recorddemo] desc = Start recording a demo. [reload] desc = Reloads the current game (if loaded). [rendedit] desc = Open the Renderer Appearance editor in a sidebar. [repeat] desc = Repeat a command at given intervals. inf = Params: repeat (count) (interval) (cmd)\nFor example, 'repeat 10 35 "screenshot"'. [reset] desc = Resets the engine (reloading all data files and definitions). [safebind] desc = Bind a command to an unless the event is already bound. [safebindr] desc = Bind a command to an unless the event is already bound. [say] desc = Broadcast a chat message. [saynum] desc = Send a chat message to the specified player. [sayto] desc = Send a chat message to the specified player. [setbpp] desc = Change color depth (bits per pixel), either 16 or 32. inf = Params: setbpp (bits)\nFor example, 'setbpp 32'. [setcon] desc = Set console and viewplayer. inf = Params: setcon (playernum)\nFor example, 'setcon 1'. [setfullres] desc = Change to fullscreen mode using the specified resolution. inf = USAGE:\nsetfullres (width) (height)\nSEE ALSO:\n- 'listdisplaymodes'\n- 'setwinres'\n- 'setres'\n [setname] desc = Set your name. inf = Params: setname (name)\nFor example, 'setname "my name"'. [setres] desc = Change display mode resolution or window size. inf = USAGE:\nsetres (width) (height)\nThe window retains its current mode, adjusting the display mode if in fullscreen and the window size if in a windowed mode.\nEXAMPLES:\nChange resolution to 1024 x 768: 'setres 1024 768'\nSEE ALSO:\n- 'listdisplaymodes'\n- 'setfullres'\n- 'setwinres'\n [settics] desc = Set number of game tics per second (default: 35). inf = Params: settics (tics)\nFor example, 'settics 15'. [setvidramp] desc = Update display's hardware gamma ramp. [setwinres] desc = Set window size and change to windowed mode. inf = USAGE:\nsetwinres (width) (height)\nSEE ALSO:\n- 'setfullres'\n- 'setres'\n- 'listdisplaymodes'\n [stopdemo] desc = Stop currently playing demo. [stopmusic] desc = Stop any currently playing music. [sub] desc = Subtract something from a cvar. [taskbar] desc = Open/close the task bar and console command prompt. [texreset] desc = Force a texture reload. [toggle] desc = Toggle the value of a cvar between zero and nonzero. inf = Params: toggle (cvar)\nFor example, 'toggle rend-light'. [togglefullscreen] desc = Toggle between fullscreen and windowed modes. [togglemaximized] desc = Toggle between maximized and normal window modes. [tutorial] desc = Show a tutorial that introduces Doomsday's UI. [uicolor] desc = Change Doomsday user interface colors. inf = Params: uicolor (object) (red) (green) (blue)\nFor example, 'uicolor text 1 1 1'.\nPossible objects are:\ntext, shadow, bglight, bgmed, bgdark,\nborhigh, bormed, borlow, help [unload] desc = Unload the current game or one or more data files. inf = Params: unload (name) ...\nFor example, 'unload mylevel.wad'. [update] desc = Check for new available releases. [updateandnotify] desc = Check for new available releases and open the update notification dialog. [updatesettings] desc = Show the settings dialog for configuring automatic updates. [varstats] desc = Show detailed statistics for console variables. [version] desc = Show detailed version information. [write] desc = Write bindings and aliases to a file. inf = Params: write (filename)\nFor example, 'write myconfig.cfg'. # # CONSOLE VARIABLES: engine # [blockmap-build] desc = Automatically generate blockmap data when necessary, 0=Never, 1=When needed, 2=Always. [bsp-cache] desc = 1=Load generated GL nodes data from the bspcache directory. 0=Always generate new GL data. [bsp-factor] desc = glBSP: changes the cost assigned to edge splits (default: 7). [client-connect-timeout] desc = Maximum number of seconds to attempt connecting to a server. [con-move-speed] desc = Speed of console opening/closing. [con-show-during-setup] desc = 1=Show console when a map is being loaded. [con-transition-tics] desc = Duration of transition effect in tics. 0=Disabled. [con-transition] desc = Transition effect used when leaving busy mode: 0=Crossfade, 1=DOOM (smooth), 2=DOOM [ctl-info] desc = 1=Show player control state debugging information. [edit-bias-blink] desc = 1=Blink the cursor. [edit-bias-blue] desc = Blue component of the bias light color. [edit-bias-grab-distance] desc = Distance to the grabbed bias light. [edit-bias-green] desc = Green component of the bias light color. [edit-bias-hide] desc = 1=Hide bias light editor's HUD. [edit-bias-intensity] desc = Intensity of the bias light. [edit-bias-red] desc = Red component of the bias light color. [edit-bias-show-indices] desc = 1=Show source indices in 3D view. [edit-bias-show-sources] desc = 1=Show all light sources. [file-startup] desc = The list of WADs to be loaded at startup. [input-conflict-zerocontrol] desc = 1=If a control is influenced by two or more conflicting input device states, the control position gets zeroed. [input-joy-device] desc = ID of joystick to use (if more than one). [input-joy] desc = 1=Enable joystick input. [input-mouse-filter] desc = Filter strength for mouse movement. [input-mouse-frequency] desc = Mouse input polling frequency (events per second). 0=unlimited. [input-sharp-lateprocessing] desc = 1=Process sharp events after tickers during sharp tics. Increases input latency by 1/35 seconds. [input-toggle-sharp] desc = 1=Process toggle events only on sharp ticks for backwards compatible behavior. [music-soundfont] desc = Filename of the DLS/SF2 soundfont file to be used with MIDI playback (if supported by audio plugin). [music-source] desc = Preferred music source: 0=Original MUS, 1=External files, 2=CD. [music-volume] desc = Music volume (0-255). [net-dev] desc = Network development mode. [net-ip-address] desc = TCP/IP address for searching servers. [net-ip-port] desc = TCP port to use for control connections. [net-name] desc = Your name in multiplayer games. [net-nosleep] desc = 1=Don't sleep while waiting for tics. [net-queue-show] desc = Monitor send queue. [refresh-rate-maximum] desc = Maximum limit for the frame rate (default: 200). [reject-build] desc = Automatically generate reject data when necessary, 0=Never, 1=When needed, 2=Always. [rend-bias-grid-blocksize] desc = Size of a grid block in the light grid (default: 31). [rend-bias-grid-debug-size] desc = Size of a grid block in the light grid debug display. [rend-bias-grid-debug] desc = 1=Show the light grid (for debugging). [rend-bias-grid-multisample] desc = Sector to grid block, conversion accuracy multiplier. [rend-bias-grid] desc = 1=Smooth sector lighting is enabled. [rend-bias-lightspeed] desc = Milliseconds it takes for light changes to become effective. [rend-bias-max] desc = Sector lightlevel that retains its normal color. [rend-bias-min] desc = Sector lightlevel that is biased completely to zero. [rend-bias] desc = 1=Enable the experimental shadow bias test setup. [rend-bloom-complexity] desc = 0=One-pass bloom (faster). 1=Two-pass bloom (more realistic). [rend-bloom-dispersion] desc = Determines how much the bloom filter spreads out color values when blurring (1.0=normal). [rend-bloom-intensity] desc = Output intensity for the bloom filter. [rend-bloom-threshold] desc = Input threshold for bloom: pixels darker than this will not be affected by bloom (range: 0...1). [rend-bloom] desc = 1=Enable the bloom filter for frame post-processing. Bloom makes bright areas of the frame glow more intensely. [rend-camera-fov] desc = Field of view. [rend-camera-smooth] desc = 1=Filter camera movement between game tics (OBSOLETE). [rend-dev-bias-affected] desc = 1=Keep track which sources affect which surfaces. [rend-dev-bias-sight] desc = 1=Enable the use of line-of-sight checking with shadow bias. [rend-dev-blockmap-size] desc = Scale multiplier for the blockmap debug display. [rend-dev-blockmap] desc = Enable drawing of the blockmap debug display: 1=Mobjs, 2=Lines, 3=BspLeafs, 4=Polyobjs. [rend-dev-cull-leafs] desc = 1=Disable non-visible bsp leaf culling. [rend-dev-fakeradio-update] desc = 1=Enable updating of the shadow edges used with simulated radiosity. [rend-dev-framecount] desc = Frame counter. [rend-dev-freeze] desc = 1=Stop updating rendering lists. [rend-dev-generator-show-indices] desc = 1=Display particle generator indices. [rend-dev-light-mod] desc = Show the light-level mod range. [rend-dev-lums] desc = 1=Display lumobj debug display. [rend-dev-mobj-bbox] desc = 1=Render mobj bounding boxes (as used for collision detection). [rend-dev-mobj-show-vlights] desc = 1=Render mobj vlight vectors (for debug). [rend-dev-nosprite] desc = 1=Disable drawing of all sprites and masked walls. [rend-dev-polyobj-bbox] desc = 1=Render polyobj bounding boxes. [rend-dev-sector-show-indices] desc = 1=Enable drawing of sector indices, for debug. [rend-dev-sky-always] desc = 1=Always render the sky, even if there are no sky surfaces visible. [rend-dev-sky] desc = 1=Render the sky as a solid surface. [rend-dev-soundorigins] desc = 3-bit bitfield. Show sound origins (for debug): 0x1=Sector, 0x2=Plane, 0x4=Side. [rend-dev-surface-show-vectors] desc = 3-bit bitfield. Show surface tangent-space vectors (red:tangent, green:bitangent, blue:normal). [rend-dev-tex-showfix] desc = 1=Render the missing texture instead of fixing with a suitable game texture. [rend-dev-vertex-show-bars] desc = 1=Enable drawing of vertex z axis bars, for debug. [rend-dev-vertex-show-indices] desc = 1=Enable drawing of vertex indices, for debug. [rend-dev-wireframe] desc = 1=Render player view in wireframe mode. 2=Also render UI and text as wireframes. [rend-fakeradio-darkness] desc = Darkness of simulated radiosity shadows. [rend-fakeradio] desc = 1=Enable simulated radiosity lighting. 2=Process without rendering. [rend-finale-stretch] desc = Fixed aspect ratio finale stretch-scaling strategy 0=Smart, 1=Never, 2=Always. [rend-fog-default] desc = Default fog mode: 0=linear, 1=exp, 2=exp2. [rend-glow-height] desc = Max height of wall glow (default: 100). [rend-glow-scale] desc = A multiplier for glow height (default: 1). [rend-glow-wall] desc = 1=Render glow on walls. [rend-glow] desc = Glow strength factor (default: 1). [rend-halo-bright] desc = Halo/flare brightness. [rend-halo-dim-far] desc = Halo dimming relative end distance. [rend-halo-dim-near] desc = Halo dimming relative start distance. [rend-halo-fade-far] desc = Distance at which halos are no longer visible. [rend-halo-fade-near] desc = Distance to begin fading halos. [rend-halo-occlusion] desc = Rate at which occluded halos fade. [rend-halo-radius-min] desc = Minimum halo radius. [rend-halo-realistic] desc = 1=Use more realistic halo effects. [rend-halo-secondary-limit] desc = Minimum halo size. [rend-halo-size] desc = Size of halos. [rend-halo-zmag-div] desc = Halo Z magnification. [rend-halo] desc = Number of flares to draw per light. [rend-hud-fov-shift] desc = When FOV > 90 player weapon is shifted downward. [rend-hud-offset-scale] desc = Scaling of player weapon (x,y) offset. [rend-hud-stretch] desc = Fixed aspect ratio player weapon stretch-scaling strategy 0=Smart, 1=Never, 2=Always. [rend-info-frametime] desc = 1=Print frame time offsets. [rend-info-lums] desc = 1=Print lumobj count after rendering a frame. [rend-info-rendpolys] desc = 1=Print rendpoly pool state after rendering a frame. [rend-info-tris] desc = 1=Print triangle count after rendering a frame. [rend-light-ambient] desc = Ambient light level. [rend-light-attenuation] desc = Maximum light attentuation distance, in map units (default: 1024). [rend-light-blend] desc = Dynamic lights color blending mode: 0=normal, 1=additive, 2=no blending. [rend-light-bright] desc = Intensity factor for dynamic lights. [rend-light-compression] desc = Sector light range compression (brighten dark areas / darken light areas). [rend-light-decor-angle] desc = Reduce brightness if surface/view angle too steep. [rend-light-decor-bright] desc = Brightness of light decorations. [rend-light-decor-far] desc = Maximum distance at which light decorations are visible. [rend-light-decor] desc = 1=Enable surface light decorations. [rend-light-fog-bright] desc = Brightness of dynamic lights when fog is enabled. [rend-light-multitex] desc = 1=Use multitexturing when rendering dynamic lights. [rend-light-num] desc = The maximum number of dynamic lights. 0=no limit. [rend-light-radius-max] desc = Maximum radius of dynamic lights (default: 128). [rend-light-radius-min-bias] desc = Minimum dynamic light radius to convert to BIAS light source. [rend-light-radius-scale] desc = A multiplier for dynlight radii (default: 1). [rend-light-sky-auto] desc = 1=Enable automatic calculation of the sky light color. [rend-light-sky] desc = Sky light color blending factor for sky sectors (off: 0, default: .2). [rend-light-wall-angle-smooth] desc = 1=Enable smoothing of angle-based wall lighting. [rend-light-wall-angle] desc = Intensity of angle-based wall lighting delta. [rend-light] desc = 1=Render dynamic lights. 2=Process without rendering. [rend-map-material-precache] desc = 1=Precache materials during map setup. [rend-mobj-light-auto] desc = 1=Enable automatically calculated lights on mobjs. [rend-mobj-smooth-move] desc = 1=Use short-range visual offsets for models. 2=Use SRVO for sprites, too (unjags actor movement). [rend-mobj-smooth-turn] desc = 1=Use separate visual angle for mobjs (unjag actors). [rend-model-aspect] desc = Scale for MD2 z-axis when model is loaded. [rend-model-distance] desc = Farther than this models revert back to sprites. [rend-model-inter] desc = 1=Interpolate frames. [rend-model-lights] desc = Maximum number of light sources on models. [rend-model-lod] desc = Custom level of detail factor. 0=LOD disabled, 1=normal. [rend-model-mirror-hud] desc = 1=Mirror HUD weapon models. [rend-model-precache] desc = 1=Precache 3D models at level setup (slow). [rend-model-shiny-multitex] desc = 1=Enable multitexturing with shiny model skins. [rend-model-shiny-strength] desc = General strength of model shininess effects. [rend-model-spin-speed] desc = Speed of model spinning, 1=normal. [rend-model] desc = Render using 3D models when possible. [rend-particle-diffuse] desc = Diffuse factor for particles near the camera. [rend-particle-max] desc = Maximum number of particles to render. 0=no limit. [rend-particle-rate] desc = Particle spawn rate multiplier (default: 1). [rend-particle-visible-near] desc = Minimum visible distance for a particle. [rend-particle] desc = 1=Render particle effects. [rend-shadow-darkness] desc = Darkness factor for object shadows. [rend-shadow-far] desc = Maximum distance where shadows are visible. [rend-shadow-radius-max] desc = Maximum radius of object shadows. [rend-shadow] desc = 1=Render shadows under objects. [rend-sky-detail] desc = Number of sky sphere quadrant subdivisions. [rend-sky-distance] desc = Sky sphere radius. [rend-sky-rows] desc = Number of sky sphere rows. [rend-sprite-align-angle] desc = Maximum angle for slanted sprites (spralign 2). [rend-sprite-align] desc = 1=Always align sprites with the view plane. 2=Align to camera, unless slant > r_maxSpriteAngle. [rend-sprite-alpha] desc = 1=Enable variable translucency on sprites. [rend-sprite-blend] desc = 1=Use additive blending for sprites. [rend-sprite-lights] desc = Maximum number of light sources on sprites. [rend-sprite-mode] desc = 1=Draw sprites and masked walls with hard edges. [rend-sprite-noz] desc = 1=Don't write sprites in the Z buffer. [rend-sprite-precache] desc = 1=Precache sprites during map setup (slow). [rend-tex-anim-smooth] desc = 1=Enable interpolated texture animation. [rend-tex-detail-multitex] desc = 1=Use multitexturing when rendering detail textures. [rend-tex-detail-scale] desc = Global detail texture factor. [rend-tex-detail-strength] desc = Global detail texture strength factor. [rend-tex-detail] desc = 1=Render with detail textures. [rend-tex-external-always] desc = 1=Always use external texture resources (overrides -pwadtex). [rend-tex-filter-anisotropic] desc = Default level of anisotropic texture filtering -1=Best available, 0=Off, 1=2x, 2=4x, 3=8x, 4=16x. [rend-tex-filter-mag] desc = 1=Use bilinear filtering for texture magnification. [rend-tex-filter-smart] desc = 1=Use hq2x-filtering on all textures. [rend-tex-filter-sprite] desc = 1=Render smooth sprites. [rend-tex-filter-ui] desc = 1=User interface uses linear interpolation. [rend-tex-gamma] desc = Texture gamma correction factor. [rend-tex-mipmap] desc = The mipmapping mode for textures. [rend-tex-quality] desc = The quality of textures (0-8). [rend-tex-shiny] desc = 1=Enable shiny textures on surfaces of the map. [rend-tex] desc = 1=Render with textures. 2=Render with gray texture. [server-frame-interval] desc = Minimum number of tics between sent frames. [server-info] desc = The description given of this computer if it's a server. [server-latencies] desc = Show client latencies. [server-name] desc = The name of this computer if it's a server. [server-password] desc = Password for remote login. inf = Password that shell users must know in order to connect to the server. Applicable only to shell users; regular clients wishing to join the game do not need the password.\nNOTE! Public servers must have a password to be accepted by the master server. [server-player-limit] desc = Maximum number of players on the server. [server-public] desc = 1=Send info to master server. [sound-16bit] desc = 1=16-bit sound effects/resampling. [sound-3d] desc = 1=Play sound effects in 3D. [sound-info] desc = 1=Show sound debug information. [sound-overlap-stop] desc = 1=Only allow one sound per emitter object (as in traditional Doom). [sound-rate] desc = Sound effects sample rate (11025, 22050, 44100). [sound-reverb-volume] desc = Reverb effects general volume (0=disable). [sound-volume] desc = Sound effects volume (0-255). [ui-cursor-height] desc = Mouse cursor height. [ui-cursor-width] desc = Mouse cursor width. [ui-panel-help] desc = 1=Enable help window in Control Panel. [ui-panel-tips] desc = 1=Show help indicators in Control Panel. [vid-bright] desc = Display brightness: -1=dark, 0=normal, 1=light. [vid-contrast] desc = Display contrast: 1=normal. [vid-gamma] desc = Display gamma correction factor: 1=normal. # Control Panel help strings # # Video # [Gamma correction] cvar = vid-gamma def = 1.0 desc = Smaller values result in a darker picture. In order for this setting to work your display adapter must support color adjustments. You should lower the gamma correction value until dark areas in maps become sufficiently dark. Note that with some monitors different resolutions require different gamma correction levels.\bUse the -noramp option to disable all color adjustments. [Display contrast] cvar = vid-contrast def = 1.0 desc = Modifies the steepness of the gamma ramp. Values greater than 1.0 result in a more colorful picture but since the colors are clamped, some are lost in both the bright and dark ends of the ramp. Values less than 1.0 fade the picture towards gray. In order for this setting to work your display adapter must support color adjustments.\bUse the -noramp option to disable all color adjustments. [Display brightness] cvar = vid-bright def = 0.0 desc = Applies an offset to the gamma ramp. Positive values make the picture lighter, negative ones darker. For the best results, use a small positive brightness value with dark gamma and slightly increased contrast. In order for this setting to work your display adapter must support color adjustments.\bUse the -noramp option to disable all color adjustments. # # Audio # [Preferred music source] cvar = music-source def = External files desc = There can be a WAD lump, external file and a CD track associated with each song. This setting controls which of these resources is actually played when the song is started. If the selected resource is not available, one that is will be chosen instead. [16-bit sound effects] cvar = sound-16bit def = No desc = When this setting is activated, 8-bit sounds are converted to 16-bit before playing. In order for this to be effective you must also choose a sample rate higher than 11025 Hz. [Sound effects sample rate] cvar = sound-rate def = 11025 Hz desc = The minimum sample rate for all sound effects. Samples with a smaller rate will be resampled to this rate before playing. To increase the quality of the resampling, activate the 16-bit sound effects option. [Show status of channels] cvar = sound-info def = No desc = The status of all sound channels is drawn on screen. This is intended for debugging. The displayed information consists of sample cache size, channel flags, volume, frequency, latest start time, cursor positions, buffer flags and sample information.\bO = Origin\nA = Attenuated\nE = Emitter\n3 = 3D\nP = Playing\nR = Repeat\nL = Reload # # Graphics # [Field Of View angle] cvar = rend-camera-fov def = 90 desc = Adjust the camera's Field Of View angle according to your monitor configuration. If you have a large monitor and view it close by, using a larger FOV angle (around 100 degrees) will result in a more natural game view. With small monitors it's best to stick to 90 degrees or else the fish eye effect may become distracting. [Sky sphere radius] cvar = rend-sky-distance def = 1600 desc = The sky is composed of two capped hemispheres rendered around the camera location. The sky sphere radius is the radius of these hemispheres. The primary purpose of this setting is to determine how large an impact fog has on the sky. Larger radii will result in a sky with heavier fog. [Shadow visible distance] cvar = rend-shadow-far def = 1000 desc = Maximum distance at which shadows under objects are visible. If the object is farther than this, no shadow will be drawn. Since the shadows are extremely simple, posing little fear of performance loss, it's safe to increase this value as needed. # # Graphics: Lights # [Blending mode] cvar = rend-light-blend def = Multiply desc = The method used to draw dynamic lights. In the Multiply mode, color from dynamic lights is multiplied with the underlying surface pixels. This allows dark areas to be correctly lit up by the lights. The Add mode is faster but doesn't look as realistic.\bThe third mode, Process wo/rendering, is for debugging. In it, all dynamic light polygons are calculated but none are rendered. [Dynamic light radius factor] cvar = rend-light-radius-scale def = 3 desc = The radius of all dynamic lights is multiplied by this factor. Lights are never larger than the Maximum dynamic light radius, though. [Maximum dynamic light radius] cvar = rend-light-radius-max def = 256 desc = The maximum radius a dynamic light can have. This is just a limit: to make the lights larger, you also need to increase the Dynamic light radius factor. [Light range compression] cvar = rend-light-compression def = 0 desc = Sector light range compression. Positive values will brighten dark areas while negative values will darken light areas of the map. [Floor/ceiling glow on walls] cvar = rend-glow-wall def = Yes desc = Glowing floor and ceiling textures will cast a dynamic light on surrounding walls. # # Graphics: Halos # [Number of flares per halo] cvar = rend-halo def = 5 desc = Maximum number of lens flares originating from a luminous object. To disable lens flares, set this to None (zero). [Use realistic halos] cvar = rend-halo-realistic def = Yes desc = When enabled, halos are rendered in a more true-to-life way. This also means that secondary lens flares are disabled. [Occlusion fade speed] cvar = rend-halo-occlusion def = 48 desc = The rate at which a halo changes state from "visible" to "occluded" and vice versa. A small fade speed results in halos that appear and disappear softly, but are visible through walls for a short while before disappearing. [Flare visibility limitation] cvar = rend-halo-secondary-limit def = 1 desc = Controls when secondary lens flares become visible. They are only visible when the light source is close enough to the camera. The larger the number, the farther away secondary flares are visible. [Z magnification divisor] cvar = rend-halo-zmag-div def = 100 desc = The higher the number, the smaller lens flares become when the source is far away. Normally halos grow slightly larger as they move away from the camera. # # Graphics: Textures # [Texture quality] cvar = rend-tex-quality def = 8 desc = Depending on your system configuration, some textures must be resized so that their dimensions are powers of two. In these instances, this setting controls whether the higher or smaller matching power of two is chosen. At quality zero, all textures are reduced in size, to match the largest power of two smaller than the original size. At quality 8, the resizing is always done upwards. [Smart texture filtering] cvar = rend-tex-filter-smart def = No desc = When enabled the hq2x texture filtering algorithm is used to enlarge all textures as opposed to linear scaling. [Bilinear filtering] desc = Controls which class(es) of graphics receive bilinear filtering. Disabling bilinear filtering results in "pixelated" textures when up close. [Anisotropic filtering] cvar = rend-tex-filter-anisotropic def = Best desc = When textures are drawn onto surfaces at high angles compared to the camera, bluring is introduced which reduces the overall quality of the visual. Anisotropic filtering can be used to help reduce this bluring. Sample multiplier: 0=Disabled, 1=2x, 2=4x, 3=8x, 4=16x. # # Graphics: Objects # [3D model visibility limit] cvar = rend-model-distance def = 1500 desc = Objects farther than this will never be rendered as 3D models. [Align sprites to...] cvar = rend-sprite-align def = Camera desc = This setting controls which direction sprites face. Aligning to camera means the sprites are always upright (to make them appear more 3D) but they will turn to face the camera coordinates. Aligning to view plane resembles the way the sprites were drawn in software. The "limited" options restrict how much the sprite can tilt vertically. [LOD level zero distance] cvar = rend-model-lod def = 256 desc = The distance where LOD level zero becomes LOD level one. Only affects 3D models that have Level Of Detail information (DMDs). # # Graphics: Particles # [Near diffusion factor] cvar = rend-particle-diffuse def = 4 desc = Particles may cause excessive overdraw if many are visible just in front of the camera. This setting makes large particles near the camera more transparent and ultimately invisible. The larger the diffuse factor, the smaller particles will be affected. The end result is that particles will not be visible near the camera. Increase the setting if framerate drops alarmingly when in smoke. [Near clip distance] cvar = rend-particle-visible-near def = Disabled desc = Particles that are closer to the camera than this are not drawn at all. # # Network # [Cl-to-sv pos transmit tics] cvar = client-pos-interval def = 10 desc = The client periodically sends its coordinates to the server in order to keep it synchronized with the player position the client perceives as correct. The interval between the coordinate transmissions as a number of 35 Hz tics. Use a larger interval with low-bandwidth connections (can lead to more warping, though). [Server login password] cvar = server-password desc = If a client wishes to make a remote console connection to the server it must specify this password. If the password is empty, clients can issue console commands on the server without specifying a password. Clients use the "login" and "logout" commands to begin and end a remote connection. [Frame interval tics] cvar = server-frame-interval def = 1 desc = Number of 35 Hz ticks between frames sent to clients. Only for the server. Small intervals require more bandwidth but result in smoother animation and other world events. Use larger intervals (2..5) with low-bandwidth connections. # # Console # [Silent console variables] cvar = con-var-silent def = No desc = Normally when the value of a console variable is changed, its name and the new value get printed in the console. If the variables are silent, this will not happen and the value can be changed without anything being printed as a confirmation. [Command completion with Tab] cvar = con-completion def = List with values desc = The Tab key is used to complete words in the console. The console knows how to complete command, variable and alias names. The completion can work in two ways. In the "List with values" mode, pressing Tab will complete the unambiguous part of the word and print a list of possible completions. Variables will be printed with their values. In the "Cycle" mode, Tab is used to cycle through all the choices. doomsday-stable-1.15.7/doomsday/net.dengine.base.pack/defs/0000775000175000017500000000000012641367671022761 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/net.dengine.base.pack/defs/flags.ded0000664000175000017500000004565112641367671024546 0ustar jaakkojaakko# Doomsday: Flags # Texture Groups Flag { ID = "tgf_smooth"; Value = 0x1; } Flag { ID = "tgf_first_only"; Value = 0x2; } Flag { ID = "tgf_precache"; Value = 0x4000; } # State flags Flag { ID = "statef_fullbright"; Value = 0x01; } Flag { ID = "statef_noautolight"; Value = 0x02; } # Sounds Flag { ID = "sf_shift"; Value = 0x1; } Flag { ID = "sf_shift2"; Value = 0x2; } Flag { ID = "sf_exclude"; Value = 0x4; } Flag { ID = "sf_nodist"; Value = 0x8; } Flag { ID = "sf_repeat"; Value = 0x10; } Flag { ID = "sf_dontstop"; Value = 0x20; } # Decorations Flag { # Decoration will not be used with IWAD resources. ID = "dcf_noiwad"; Value = 0x1; } Flag { # Allow using decoration with PWAD resources. ID = "dcf_pwad"; Value = 0x2; } Flag { # Allow using decoration with external resources. ID = "dcf_ext"; Value = 0x4; } # Reflections Flag { # Reflection will not be used with IWAD resources. ID = "rff_noiwad"; Value = 0x1; } Flag { # Allow using reflection with PWAD resources. ID = "rff_pwad"; Value = 0x2; } Flag { # Allow using reflection with external resources. ID = "rff_ext"; Value = 0x4; } # Details Flag { # DetailTex will not be used with IWAD resources. ID = "dtf_noiwad"; Value = 0x1; } Flag { # Allow using detailtex with PWAD resources. ID = "dtf_pwad"; Value = 0x2; } Flag { # Allow using detailtex with external resources. ID = "dtf_ext"; Value = 0x4; } # Materials # Flag { ID = "matf_unused1"; Value = 0x1; } Flag { ID = "matf_dontdraw"; Value = 0x2; } # Material should never be drawn. Flag { ID = "matf_skymask"; Value = 0x4; } # Sky-mask surfaces using this material. # Lights Flag { ID = "lgf_nohalo"; Value = 0x1; } Flag { ID = "lgf_dontturnhalo"; Value = 0x2; } # Map Info Flag { ID = "mif_fog"; Value = 0x1; } Flag { ID = "mif_sphere"; Value = 0x2; } # Draw the sky sphere even when models are in use. Flag { ID = "mif_nointermission"; Value = 0x4; } Flag { ID = "mif_lightning"; Value = 0x8; } # Sky Info Flag { ID = "sif_sphere"; Value = 0x1; } # Draw the sky sphere even when models are in use. # Sky Layers Flag { ID = "slf_enable"; Value = 0x1; } Flag { ID = "slf_mask"; Value = 0x2; } # Blending Modes Flag { ID = "bm_normal"; Value = 0; } Flag { ID = "bm_add"; Value = 1; } Flag { ID = "bm_dark"; Value = 2; } Flag { ID = "bm_sub"; Value = 3; } # src_alpha multiplied with dest_color Flag { ID = "bm_sub2"; Value = 7; } # src_alpha multiplied with src_color Flag { ID = "bm_revsub"; Value = 4; } Flag { ID = "bm_mul"; Value = 5; } Flag { ID = "bm_invmul"; Value = 6; } Flag { ID = "bm_inv"; Value = 7; } Flag { ID = "sdf_fade_top_mid"; Value = 0x1; } Flag { ID = "sdf_fade_mid_top"; Value = 0x2; } Flag { ID = "sdf_fade_mid_bottom"; Value = 0x4; } Flag { ID = "sdf_fade_bottom_mid"; Value = 0x8; } Flag { ID = "sdf_middle_stretch"; Value = 0x10; } # Models Flag { ID = "df_fullbright"; Value = 0x1; } Flag { ID = "df_shadow1"; Value = 0x2; } Flag { ID = "df_shadow2"; Value = 0x4; } Flag { ID = "df_brightshadow"; Value = 0x8; } Flag { ID = "df_movpitch"; Value = 0x10; } Flag { ID = "df_spin"; Value = 0x20; } Flag { ID = "df_skintrans"; Value = 0x40; } Flag { ID = "df_autoscale"; Value = 0x80; } Flag { ID = "df_movyaw"; Value = 0x100; } Flag { ID = "df_nointerpol"; Value = 0x200; } Flag { ID = "df_brightshadow2"; Value = 0x400; } Flag { ID = "df_alignyaw"; Value = 0x800; } Flag { ID = "df_alignpitch"; Value = 0x1000; } Flag { ID = "df_darkshadow"; Value = 0x2000; } Flag { ID = "df_idskin"; Value = 0x4000; } Flag { ID = "df_noz"; Value = 0x8000; } Flag { ID = "df_nomaxz"; Value = 0x10000; } Flag { ID = "df_selskin"; Value = 0x20000; } Flag { ID = "df_ptoffsub1"; Value = 0x40000; } Flag { ID = "df_noptc"; Value = 0x80000; } Flag { ID = "df_specular"; Value = 0x100000; } Flag { ID = "df_litshiny"; Value = 0x200000; } Flag { ID = "df_idframe"; Value = 0x400000; } Flag { ID = "df_idangle"; Value = 0x800000; } Flag { ID = "df_dim"; Value = 0x1000000; } Flag { ID = "df_subtract"; Value = 0x2000000; } Flag { ID = "df_revsubtract"; Value = 0x4000000; } Flag { ID = "df_twosided"; Value = 0x8000000; } Flag { ID = "df_notexcomp"; Value = 0x10000000; } Flag { ID = "df_worldtime"; Value = 0x20000000; } # Model grouping Flag { ID = "mg_hud"; Value = 0x2; } Flag { ID = "mg_player"; Value = 0x4; } Flag { ID = "mg_monster"; Value = 0x8; } Flag { ID = "mg_weapon"; Value = 0x10; } Flag { ID = "mg_ammo"; Value = 0x20; } Flag { ID = "mg_bonus"; Value = 0x40; } Flag { ID = "mg_light"; Value = 0x80; } Flag { ID = "mg_decor"; Value = 0x100; } Flag { ID = "mg_misc"; Value = 0x200; } # Generators Flag { ID = "gnf_static"; Value = 1; } Flag { ID = "gnf_srcvel"; Value = 2; } Flag { ID = "gnf_spawn"; Value = 4; } Flag { ID = "gnf_srcdir"; Value = 8; } Flag { ID = "gnf_blend"; Value = 0x10; } Flag { ID = "gnf_floor"; Value = 0x20; } Flag { ID = "gnf_ceiling"; Value = 0x40; } Flag { ID = "gnf_space"; Value = 0x80; } Flag { ID = "gnf_density"; Value = 0x100; } Flag { ID = "gnf_modelonly"; Value = 0x200; } Flag { ID = "gnf_scalerate"; Value = 0x400; } Flag { ID = "gnf_group"; Value = 0x800; } Flag { ID = "gnf_blendadd"; Value = 0x10; } Flag { ID = "gnf_blendsub"; Value = 0x1000; } Flag { ID = "gnf_blendrsub"; Value = 0x2000; } Flag { ID = "gnf_blendmul"; Value = 0x4000; } Flag { ID = "gnf_blendimul"; Value = 0x8000; } Flag { ID = "gnf_extra"; Value = 0x10000; } # Particle types # (note: these are an enum, not flags) Flag { ID = "pt_point"; Value = 1; } Flag { ID = "pt_line"; Value = 2; } Flag { ID = "pt_tex00"; Value = 100; } Flag { ID = "pt_tex01"; Value = 101; } Flag { ID = "pt_tex02"; Value = 102; } Flag { ID = "pt_tex03"; Value = 103; } Flag { ID = "pt_tex04"; Value = 104; } Flag { ID = "pt_tex05"; Value = 105; } Flag { ID = "pt_tex06"; Value = 106; } Flag { ID = "pt_tex07"; Value = 107; } Flag { ID = "pt_tex08"; Value = 108; } Flag { ID = "pt_tex09"; Value = 109; } Flag { ID = "pt_tex10"; Value = 110; } Flag { ID = "pt_tex11"; Value = 111; } Flag { ID = "pt_tex12"; Value = 112; } Flag { ID = "pt_tex13"; Value = 113; } Flag { ID = "pt_tex14"; Value = 114; } Flag { ID = "pt_tex15"; Value = 115; } Flag { ID = "pt_tex16"; Value = 116; } Flag { ID = "pt_tex17"; Value = 117; } Flag { ID = "pt_tex18"; Value = 118; } Flag { ID = "pt_tex19"; Value = 119; } Flag { ID = "pt_tex20"; Value = 120; } Flag { ID = "pt_tex21"; Value = 121; } Flag { ID = "pt_tex22"; Value = 122; } Flag { ID = "pt_tex23"; Value = 123; } Flag { ID = "pt_tex24"; Value = 124; } Flag { ID = "pt_tex25"; Value = 125; } Flag { ID = "pt_tex26"; Value = 126; } Flag { ID = "pt_tex27"; Value = 127; } Flag { ID = "pt_tex28"; Value = 128; } Flag { ID = "pt_tex29"; Value = 129; } Flag { ID = "pt_tex30"; Value = 130; } Flag { ID = "pt_tex31"; Value = 131; } Flag { ID = "pt_tex32"; Value = 132; } Flag { ID = "pt_tex33"; Value = 133; } Flag { ID = "pt_tex34"; Value = 134; } Flag { ID = "pt_tex35"; Value = 135; } Flag { ID = "pt_tex36"; Value = 136; } Flag { ID = "pt_tex37"; Value = 137; } Flag { ID = "pt_tex38"; Value = 138; } Flag { ID = "pt_tex39"; Value = 139; } Flag { ID = "pt_tex40"; Value = 140; } Flag { ID = "pt_tex41"; Value = 141; } Flag { ID = "pt_tex42"; Value = 142; } Flag { ID = "pt_tex43"; Value = 143; } Flag { ID = "pt_tex44"; Value = 144; } Flag { ID = "pt_tex45"; Value = 145; } Flag { ID = "pt_tex46"; Value = 146; } Flag { ID = "pt_tex47"; Value = 147; } Flag { ID = "pt_tex48"; Value = 148; } Flag { ID = "pt_tex49"; Value = 149; } Flag { ID = "pt_tex50"; Value = 150; } Flag { ID = "pt_tex51"; Value = 151; } Flag { ID = "pt_tex52"; Value = 152; } Flag { ID = "pt_tex53"; Value = 153; } Flag { ID = "pt_tex54"; Value = 154; } Flag { ID = "pt_tex55"; Value = 155; } Flag { ID = "pt_tex56"; Value = 156; } Flag { ID = "pt_tex57"; Value = 157; } Flag { ID = "pt_tex58"; Value = 158; } Flag { ID = "pt_tex59"; Value = 159; } Flag { ID = "pt_tex60"; Value = 160; } Flag { ID = "pt_tex61"; Value = 161; } Flag { ID = "pt_tex62"; Value = 162; } Flag { ID = "pt_tex63"; Value = 163; } Flag { ID = "pt_tex64"; Value = 164; } Flag { ID = "pt_tex65"; Value = 165; } Flag { ID = "pt_tex66"; Value = 166; } Flag { ID = "pt_tex67"; Value = 167; } Flag { ID = "pt_tex68"; Value = 168; } Flag { ID = "pt_tex69"; Value = 169; } Flag { ID = "pt_tex70"; Value = 170; } Flag { ID = "pt_tex71"; Value = 171; } Flag { ID = "pt_tex72"; Value = 172; } Flag { ID = "pt_tex73"; Value = 173; } Flag { ID = "pt_tex74"; Value = 174; } Flag { ID = "pt_tex75"; Value = 175; } Flag { ID = "pt_tex76"; Value = 176; } Flag { ID = "pt_tex77"; Value = 177; } Flag { ID = "pt_tex78"; Value = 178; } Flag { ID = "pt_tex79"; Value = 179; } Flag { ID = "pt_tex80"; Value = 180; } Flag { ID = "pt_tex81"; Value = 181; } Flag { ID = "pt_tex82"; Value = 182; } Flag { ID = "pt_tex83"; Value = 183; } Flag { ID = "pt_tex84"; Value = 184; } Flag { ID = "pt_tex85"; Value = 185; } Flag { ID = "pt_tex86"; Value = 186; } Flag { ID = "pt_tex87"; Value = 187; } Flag { ID = "pt_tex88"; Value = 188; } Flag { ID = "pt_tex89"; Value = 189; } Flag { ID = "pt_tex90"; Value = 190; } Flag { ID = "pt_tex91"; Value = 191; } Flag { ID = "pt_tex92"; Value = 192; } Flag { ID = "pt_tex93"; Value = 193; } Flag { ID = "pt_tex94"; Value = 194; } Flag { ID = "pt_tex95"; Value = 195; } Flag { ID = "pt_tex96"; Value = 196; } Flag { ID = "pt_tex97"; Value = 197; } Flag { ID = "pt_tex98"; Value = 198; } Flag { ID = "pt_tex99"; Value = 199; } Flag { ID = "pt_tex100"; Value = 200; } Flag { ID = "pt_tex101"; Value = 201; } Flag { ID = "pt_tex102"; Value = 202; } Flag { ID = "pt_tex103"; Value = 203; } Flag { ID = "pt_tex104"; Value = 204; } Flag { ID = "pt_tex105"; Value = 205; } Flag { ID = "pt_tex106"; Value = 206; } Flag { ID = "pt_tex107"; Value = 207; } Flag { ID = "pt_tex108"; Value = 208; } Flag { ID = "pt_tex109"; Value = 209; } Flag { ID = "pt_tex110"; Value = 210; } Flag { ID = "pt_tex111"; Value = 211; } Flag { ID = "pt_tex112"; Value = 212; } Flag { ID = "pt_tex113"; Value = 213; } Flag { ID = "pt_tex114"; Value = 214; } Flag { ID = "pt_tex115"; Value = 215; } Flag { ID = "pt_tex116"; Value = 216; } Flag { ID = "pt_tex117"; Value = 217; } Flag { ID = "pt_tex118"; Value = 218; } Flag { ID = "pt_tex119"; Value = 219; } Flag { ID = "pt_tex120"; Value = 220; } Flag { ID = "pt_tex121"; Value = 221; } Flag { ID = "pt_tex122"; Value = 222; } Flag { ID = "pt_tex123"; Value = 223; } Flag { ID = "pt_tex124"; Value = 224; } Flag { ID = "pt_tex125"; Value = 225; } Flag { ID = "pt_tex126"; Value = 226; } Flag { ID = "pt_tex127"; Value = 227; } Flag { ID = "pt_tex128"; Value = 228; } Flag { ID = "pt_tex129"; Value = 229; } Flag { ID = "pt_tex130"; Value = 230; } Flag { ID = "pt_tex131"; Value = 231; } Flag { ID = "pt_tex132"; Value = 232; } Flag { ID = "pt_tex133"; Value = 233; } Flag { ID = "pt_tex134"; Value = 234; } Flag { ID = "pt_tex135"; Value = 235; } Flag { ID = "pt_tex136"; Value = 236; } Flag { ID = "pt_tex137"; Value = 237; } Flag { ID = "pt_tex138"; Value = 238; } Flag { ID = "pt_tex139"; Value = 239; } Flag { ID = "pt_tex140"; Value = 240; } Flag { ID = "pt_tex141"; Value = 241; } Flag { ID = "pt_tex142"; Value = 242; } Flag { ID = "pt_tex143"; Value = 243; } Flag { ID = "pt_tex144"; Value = 244; } Flag { ID = "pt_tex145"; Value = 245; } Flag { ID = "pt_tex146"; Value = 246; } Flag { ID = "pt_tex147"; Value = 247; } Flag { ID = "pt_tex148"; Value = 248; } Flag { ID = "pt_tex149"; Value = 249; } Flag { ID = "pt_tex150"; Value = 250; } Flag { ID = "pt_tex151"; Value = 251; } Flag { ID = "pt_tex152"; Value = 252; } Flag { ID = "pt_tex153"; Value = 253; } Flag { ID = "pt_tex154"; Value = 254; } Flag { ID = "pt_tex155"; Value = 255; } Flag { ID = "pt_tex156"; Value = 256; } Flag { ID = "pt_tex157"; Value = 257; } Flag { ID = "pt_tex158"; Value = 258; } Flag { ID = "pt_tex159"; Value = 259; } Flag { ID = "pt_tex160"; Value = 260; } Flag { ID = "pt_tex161"; Value = 261; } Flag { ID = "pt_tex162"; Value = 262; } Flag { ID = "pt_tex163"; Value = 263; } Flag { ID = "pt_tex164"; Value = 264; } Flag { ID = "pt_tex165"; Value = 265; } Flag { ID = "pt_tex166"; Value = 266; } Flag { ID = "pt_tex167"; Value = 267; } Flag { ID = "pt_tex168"; Value = 268; } Flag { ID = "pt_tex169"; Value = 269; } Flag { ID = "pt_tex170"; Value = 270; } Flag { ID = "pt_tex171"; Value = 271; } Flag { ID = "pt_tex172"; Value = 272; } Flag { ID = "pt_tex173"; Value = 273; } Flag { ID = "pt_tex174"; Value = 274; } Flag { ID = "pt_tex175"; Value = 275; } Flag { ID = "pt_tex176"; Value = 276; } Flag { ID = "pt_tex177"; Value = 277; } Flag { ID = "pt_tex178"; Value = 278; } Flag { ID = "pt_tex179"; Value = 279; } Flag { ID = "pt_tex180"; Value = 280; } Flag { ID = "pt_tex181"; Value = 281; } Flag { ID = "pt_tex182"; Value = 282; } Flag { ID = "pt_tex183"; Value = 283; } Flag { ID = "pt_tex184"; Value = 284; } Flag { ID = "pt_tex185"; Value = 285; } Flag { ID = "pt_tex186"; Value = 286; } Flag { ID = "pt_tex187"; Value = 287; } Flag { ID = "pt_tex188"; Value = 288; } Flag { ID = "pt_tex189"; Value = 289; } Flag { ID = "pt_tex190"; Value = 290; } Flag { ID = "pt_tex191"; Value = 291; } Flag { ID = "pt_tex192"; Value = 292; } Flag { ID = "pt_tex193"; Value = 293; } Flag { ID = "pt_tex194"; Value = 294; } Flag { ID = "pt_tex195"; Value = 295; } Flag { ID = "pt_tex196"; Value = 296; } Flag { ID = "pt_tex197"; Value = 297; } Flag { ID = "pt_tex198"; Value = 298; } Flag { ID = "pt_tex199"; Value = 299; } Flag { ID = "pt_tex200"; Value = 300; } Flag { ID = "pt_tex201"; Value = 301; } Flag { ID = "pt_tex202"; Value = 302; } Flag { ID = "pt_tex203"; Value = 303; } Flag { ID = "pt_tex204"; Value = 304; } Flag { ID = "pt_tex205"; Value = 305; } Flag { ID = "pt_tex206"; Value = 306; } Flag { ID = "pt_tex207"; Value = 307; } Flag { ID = "pt_tex208"; Value = 308; } Flag { ID = "pt_tex209"; Value = 309; } Flag { ID = "pt_tex210"; Value = 310; } Flag { ID = "pt_tex211"; Value = 311; } Flag { ID = "pt_tex212"; Value = 312; } Flag { ID = "pt_tex213"; Value = 313; } Flag { ID = "pt_tex214"; Value = 314; } Flag { ID = "pt_tex215"; Value = 315; } Flag { ID = "pt_tex216"; Value = 316; } Flag { ID = "pt_tex217"; Value = 317; } Flag { ID = "pt_tex218"; Value = 318; } Flag { ID = "pt_tex219"; Value = 319; } Flag { ID = "pt_tex220"; Value = 320; } Flag { ID = "pt_tex221"; Value = 321; } Flag { ID = "pt_tex222"; Value = 322; } Flag { ID = "pt_tex223"; Value = 323; } Flag { ID = "pt_tex224"; Value = 324; } Flag { ID = "pt_tex225"; Value = 325; } Flag { ID = "pt_tex226"; Value = 326; } Flag { ID = "pt_tex227"; Value = 327; } Flag { ID = "pt_tex228"; Value = 328; } Flag { ID = "pt_tex229"; Value = 329; } Flag { ID = "pt_tex230"; Value = 330; } Flag { ID = "pt_tex231"; Value = 331; } Flag { ID = "pt_tex232"; Value = 332; } Flag { ID = "pt_tex233"; Value = 333; } Flag { ID = "pt_tex234"; Value = 334; } Flag { ID = "pt_tex235"; Value = 335; } Flag { ID = "pt_tex236"; Value = 336; } Flag { ID = "pt_tex237"; Value = 337; } Flag { ID = "pt_tex238"; Value = 338; } Flag { ID = "pt_tex239"; Value = 339; } Flag { ID = "pt_tex240"; Value = 340; } Flag { ID = "pt_tex241"; Value = 341; } Flag { ID = "pt_tex242"; Value = 342; } Flag { ID = "pt_tex243"; Value = 343; } Flag { ID = "pt_tex244"; Value = 344; } Flag { ID = "pt_tex245"; Value = 345; } Flag { ID = "pt_tex246"; Value = 346; } Flag { ID = "pt_tex247"; Value = 347; } Flag { ID = "pt_tex248"; Value = 348; } Flag { ID = "pt_tex249"; Value = 349; } Flag { ID = "pt_tex250"; Value = 350; } Flag { ID = "pt_tex251"; Value = 351; } Flag { ID = "pt_tex252"; Value = 352; } Flag { ID = "pt_tex253"; Value = 353; } Flag { ID = "pt_tex254"; Value = 354; } Flag { ID = "pt_tex255"; Value = 355; } Flag { ID = "pt_tex256"; Value = 356; } Flag { ID = "pt_tex257"; Value = 357; } Flag { ID = "pt_tex258"; Value = 358; } Flag { ID = "pt_tex259"; Value = 359; } Flag { ID = "pt_tex260"; Value = 360; } Flag { ID = "pt_tex261"; Value = 361; } Flag { ID = "pt_tex262"; Value = 362; } Flag { ID = "pt_tex263"; Value = 363; } Flag { ID = "pt_tex264"; Value = 364; } Flag { ID = "pt_tex265"; Value = 365; } Flag { ID = "pt_tex266"; Value = 366; } Flag { ID = "pt_tex267"; Value = 367; } Flag { ID = "pt_tex268"; Value = 368; } Flag { ID = "pt_tex269"; Value = 369; } Flag { ID = "pt_tex270"; Value = 370; } Flag { ID = "pt_tex271"; Value = 371; } Flag { ID = "pt_tex272"; Value = 372; } Flag { ID = "pt_tex273"; Value = 373; } Flag { ID = "pt_tex274"; Value = 374; } Flag { ID = "pt_tex275"; Value = 375; } Flag { ID = "pt_tex276"; Value = 376; } Flag { ID = "pt_tex277"; Value = 377; } Flag { ID = "pt_tex278"; Value = 378; } Flag { ID = "pt_tex279"; Value = 379; } Flag { ID = "pt_tex280"; Value = 380; } Flag { ID = "pt_tex281"; Value = 381; } Flag { ID = "pt_tex282"; Value = 382; } Flag { ID = "pt_tex283"; Value = 383; } Flag { ID = "pt_tex284"; Value = 384; } Flag { ID = "pt_tex285"; Value = 385; } Flag { ID = "pt_tex286"; Value = 386; } Flag { ID = "pt_tex287"; Value = 387; } Flag { ID = "pt_tex288"; Value = 388; } Flag { ID = "pt_tex289"; Value = 389; } Flag { ID = "pt_tex290"; Value = 390; } Flag { ID = "pt_tex291"; Value = 391; } Flag { ID = "pt_tex292"; Value = 392; } Flag { ID = "pt_tex293"; Value = 393; } Flag { ID = "pt_tex294"; Value = 394; } Flag { ID = "pt_tex295"; Value = 395; } Flag { ID = "pt_tex296"; Value = 396; } Flag { ID = "pt_tex297"; Value = 397; } Flag { ID = "pt_tex298"; Value = 398; } Flag { ID = "pt_tex299"; Value = 399; } Flag { ID = "pt_model00"; Value = 1000; } Flag { ID = "pt_model01"; Value = 1001; } Flag { ID = "pt_model02"; Value = 1002; } Flag { ID = "pt_model03"; Value = 1003; } Flag { ID = "pt_model04"; Value = 1004; } Flag { ID = "pt_model05"; Value = 1005; } Flag { ID = "pt_model06"; Value = 1006; } Flag { ID = "pt_model07"; Value = 1007; } Flag { ID = "pt_model08"; Value = 1008; } Flag { ID = "pt_model09"; Value = 1009; } Flag { ID = "pt_model10"; Value = 1010; } Flag { ID = "pt_model11"; Value = 1011; } Flag { ID = "pt_model12"; Value = 1012; } Flag { ID = "pt_model13"; Value = 1013; } Flag { ID = "pt_model14"; Value = 1014; } Flag { ID = "pt_model15"; Value = 1015; } Flag { ID = "pt_model16"; Value = 1016; } Flag { ID = "pt_model17"; Value = 1017; } Flag { ID = "pt_model18"; Value = 1018; } Flag { ID = "pt_model19"; Value = 1019; } Flag { ID = "pt_model20"; Value = 1020; } Flag { ID = "pt_model21"; Value = 1021; } Flag { ID = "pt_model22"; Value = 1022; } Flag { ID = "pt_model23"; Value = 1023; } Flag { ID = "pt_model24"; Value = 1024; } Flag { ID = "pt_model25"; Value = 1025; } Flag { ID = "pt_model26"; Value = 1026; } Flag { ID = "pt_model27"; Value = 1027; } Flag { ID = "pt_model28"; Value = 1028; } Flag { ID = "pt_model29"; Value = 1029; } Flag { ID = "pt_model30"; Value = 1030; } Flag { ID = "pt_model31"; Value = 1031; } # Particle flags (for Generator Stages) Flag { ID = "ptf_stagetouch"; Value = 0x1; } Flag { ID = "ptf_stagevtouch"; Value = 0x20; } Flag { ID = "ptf_stagehtouch"; Value = 0x40; } Flag { ID = "ptf_dietouch"; Value = 0x2; } Flag { ID = "ptf_bright"; Value = 0x4; } Flag { ID = "ptf_shading"; Value = 0x8; } Flag { ID = "ptf_hflat"; Value = 0x10; } Flag { ID = "ptf_vflat"; Value = 0x80; } Flag { ID = "ptf_flat"; Value = 0x90; } Flag { ID = "ptf_force"; Value = 0x100; } Flag { ID = "ptf_zeroyaw"; Value = 0x200; } Flag { ID = "ptf_zeropitch"; Value = 0x400; } Flag { ID = "ptf_rndyaw"; Value = 0x800; } Flag { ID = "ptf_rndpitch"; Value = 0x1000; } doomsday-stable-1.15.7/doomsday/net.dengine.base.pack/defs/materials.ded0000664000175000017500000000315012641367671025417 0ustar jaakkojaakko# # Doomsday Engine 1.9 # Default Materials. # # # The "Unknown" material. # Used to render surfaces which do not specify a material. # Material { Id = "system:unknown"; Width = 64; Height = 64; Layer { Stage { Texture = "system:unknown"; Tics = 16; Glow = 0; } Stage { Texture = "system:unknown"; Tics = 24; Glow = 1; } } } # # The "Missing" material. # Used to render surfaces which specify a material but which cannot be found. # Material { Id = "system:missing"; Width = 64; Height = 64; Layer { Stage { Texture = "system:missing"; Tics = 16; Glow = 0; } Stage { Texture = "system:missing"; Tics = 24; Glow = 1; } } } # # Used when drawing bounding boxes, selection areas and more. # Material { Id = "system:bbox"; Width = 64; Height = 64; Layer { Stage { Texture = "system:bbox"; Glow = 1; } } } # # Basic mid-gray material used to render surfaces for various lighting debug displays. # Material { Id = "system:gray"; Width = 64; Height = 64; Layer { Stage { Texture = "system:gray"; } } } # # UI Box-corner material. # Material { Id = "system:ui/boxcorner"; Width = 64; Height = 64; Layer { Stage { Texture = "system:ui/boxcorner"; } } } # # UI Box-fill material. # Material { Id = "system:ui/boxfill"; Width = 64; Height = 64; Layer { Stage { Texture = "system:ui/boxfill"; } } } # # UI Box-shade material. # Material { Id = "system:ui/boxshade"; Width = 128; Height = 128; Layer { Stage { Texture = "system:ui/boxshade"; } } } doomsday-stable-1.15.7/doomsday/net.dengine.base.pack/defs/doomsday.ded0000664000175000017500000000017212641367671025256 0ustar jaakkojaakko# Doomsday Engine 1.9 # Definitions shared by all games. Include "flags.ded"; Include "xg.ded"; Include "materials.ded"; doomsday-stable-1.15.7/doomsday/net.dengine.base.pack/defs/xg.ded0000664000175000017500000003004412641367671024056 0ustar jaakkojaakko# Doomsday: XG Lines and Sectors # XG Line Classes Flag { ID = "ltc_none"; } Flag { ID = "ltc_chain_sequence"; Value = 0x1; } Flag { ID = "ltc_plane_move"; Value = 0x2; } Flag { ID = "ltc_build_stairs"; Value = 0x3; } Flag { ID = "ltc_damage"; Value = 0x4; } Flag { ID = "ltc_power"; Value = 0x5; } Flag { ID = "ltc_line_type"; Value = 0x6; } Flag { ID = "ltc_sector_type"; Value = 0x7; } Flag { ID = "ltc_sector_light"; Value = 0x8; } Flag { ID = "ltc_activate"; Value = 0x9; } Flag { ID = "ltc_key"; Value = 0xA; } Flag { ID = "ltc_music"; Value = 0xB; } Flag { ID = "ltc_line_count"; Value = 0xC; } Flag { ID = "ltc_end_level"; Value = 0xD; } Flag { ID = "ltc_leave_map"; Value = 0xD; } # Alias for end_level Flag { ID = "ltc_disable_if_active"; Value = 0xE; } Flag { ID = "ltc_enable_if_active"; Value = 0xF; } Flag { ID = "ltc_explode"; Value = 0x10; } Flag { ID = "ltc_plane_texture"; Value = 0x11; } Flag { ID = "ltc_plane_material"; Value = 0x11; } # Alias for plane_texture Flag { ID = "ltc_wall_texture"; Value = 0x12; } Flag { ID = "ltc_wall_material"; Value = 0x12; } # Alias for wall_texture Flag { ID = "ltc_command"; Value = 0x13; } Flag { ID = "ltc_sound"; Value = 0x14; } Flag { ID = "ltc_mimic_sector"; Value = 0x15; } Flag { ID = "ltc_teleport"; Value = 0x16; } Flag { ID = "ltc_line_teleport"; Value = 0x17; } Flag { ID = "ltf_active"; Value = 0x1; } Flag { ID = "ltf_player_use_a"; Value = 0x2; } Flag { ID = "ltf_other_use_a"; Value = 0x4; } Flag { ID = "ltf_player_shoot_a"; Value = 0x8; } Flag { ID = "ltf_other_shoot_a"; Value = 0x10; } Flag { ID = "ltf_any_cross_a"; Value = 0x20; } Flag { ID = "ltf_monster_cross_a"; Value = 0x40; } Flag { ID = "ltf_player_cross_a"; Value = 0x80; } Flag { ID = "ltf_missile_cross_a"; Value = 0x100; } Flag { ID = "ltf_player_hit_a"; Value = 0x200; } Flag { ID = "ltf_other_hit_a"; Value = 0x400; } Flag { ID = "ltf_monster_hit_a"; Value = 0x800; } Flag { ID = "ltf_missile_hit_a"; Value = 0x1000; } Flag { ID = "ltf_any_hit_a"; Value = 0x2000; } Flag { ID = "ltf_player_use_d"; Value = 0x4000; } Flag { ID = "ltf_other_use_d"; Value = 0x8000; } Flag { ID = "ltf_player_shoot_d"; Value = 0x10000; } Flag { ID = "ltf_other_shoot_d"; Value = 0x20000; } Flag { ID = "ltf_any_cross_d"; Value = 0x40000; } Flag { ID = "ltf_monster_cross_d"; Value = 0x80000; } Flag { ID = "ltf_player_cross_d"; Value = 0x100000; } Flag { ID = "ltf_missile_cross_d"; Value = 0x200000; } Flag { ID = "ltf_player_hit_d"; Value = 0x400000; } Flag { ID = "ltf_other_hit_d"; Value = 0x800000; } Flag { ID = "ltf_monster_hit_d"; Value = 0x1000000; } Flag { ID = "ltf_missile_hit_d"; Value = 0x2000000; } Flag { ID = "ltf_any_hit_d"; Value = 0x4000000; } Flag { ID = "ltf_player_use"; Value = 0x4002; } Flag { ID = "ltf_other_use"; Value = 0x8004; } Flag { ID = "ltf_player_shoot"; Value = 0x10008; } Flag { ID = "ltf_other_shoot"; Value = 0x20010; } Flag { ID = "ltf_any_cross"; Value = 0x40020; } Flag { ID = "ltf_monster_cross"; Value = 0x80040; } Flag { ID = "ltf_player_cross"; Value = 0x100080; } Flag { ID = "ltf_missile_cross"; Value = 0x200100; } Flag { ID = "ltf_player_hit"; Value = 0x400200; } Flag { ID = "ltf_other_hit"; Value = 0x800400; } Flag { ID = "ltf_monster_hit"; Value = 0x1000800; } Flag { ID = "ltf_missile_hit"; Value = 0x2001000; } Flag { ID = "ltf_any_hit"; Value = 0x4002000; } Flag { ID = "ltf_ticker_a"; Value = 0x8000000; } Flag { ID = "ltf_ticker_d"; Value = 0x10000000; } Flag { ID = "ltf_ticker"; Value = 0x18000000; } Flag { ID = "ltf_mobj_gone"; Value = 0x20000000; } Flag { ID = "ltf_no_other_use_secret"; Value = 0x40000000; } Flag { ID = "ltf_activator_type"; Value = 0x80000000; } Flag { ID = "ltf2_when_act"; Value = 0x1; } Flag { ID = "ltf2_when_deact"; Value = 0x2; } Flag { ID = "ltf2_when_last"; Value = 0x10; } Flag { ID = "ltf2_while_act"; Value = 0x4; } Flag { ID = "ltf2_while_inact"; Value = 0x8; } Flag { ID = "ltf2_key1"; Value = 0x20; } Flag { ID = "ltf2_key2"; Value = 0x40; } Flag { ID = "ltf2_key3"; Value = 0x80; } Flag { ID = "ltf2_key4"; Value = 0x100; } Flag { ID = "ltf2_key5"; Value = 0x200; } Flag { ID = "ltf2_key6"; Value = 0x400; } Flag { ID = "ltf2_line_act"; Value = 0x800; } Flag { ID = "ltf2_line_inact"; Value = 0x1000; } Flag { ID = "ltf2_color"; Value = 0x2000; } Flag { ID = "ltf2_health_above"; Value = 0x4000; } Flag { ID = "ltf2_health_below"; Value = 0x8000; } Flag { ID = "ltf2_power_above"; Value = 0x10000; } Flag { ID = "ltf2_power_below"; Value = 0x20000; } Flag { ID = "ltf2_singleplayer"; Value = 0x40000; } Flag { ID = "ltf2_cooperative"; Value = 0x80000; } Flag { ID = "ltf2_deathmatch"; Value = 0x100000; } Flag { ID = "ltf2_any_mode"; Value = 0x1C0000; } Flag { ID = "ltf2_easy"; Value = 0x200000; } Flag { ID = "ltf2_med"; Value = 0x400000; } Flag { ID = "ltf2_hard"; Value = 0x800000; } Flag { ID = "ltf2_any_skill"; Value = 0xE00000; } Flag { ID = "ltf2_any"; Value = 0xFC0000; } Flag { ID = "ltf2_multiple"; Value = 0x1000000; } Flag { ID = "ltf2_2sided"; Value = 0x2000000; } Flag { ID = "ltf2_global_a_msg"; Value = 0x4000000; } Flag { ID = "ltf2_global_d_msg"; Value = 0x8000000; } Flag { ID = "ltf2_global_msg"; Value = 0xC000000; } Flag { ID = "ltf2_group_act"; Value = 0x10000000; } Flag { ID = "ltf2_group_deact"; Value = 0x20000000; } Flag { ID = "ltf2_override_any"; Value = 0x40000000; } Flag { ID = "lat_timed_off"; } Flag { ID = "lat_timed_on"; Value = 0x1; } Flag { ID = "lat_flip"; Value = 0x2; } Flag { ID = "lat_flip_timed_off"; Value = 0x3; } Flag { ID = "lat_flip_timed_on"; Value = 0x4; } Flag { ID = "lref_none"; } Flag { ID = "lref_self"; Value = 0x1; } Flag { ID = "lref_tagged"; Value = 0x2; } Flag { ID = "lref_line_tagged"; Value = 0x3; } Flag { ID = "lref_act_tagged"; Value = 0x4; } Flag { ID = "lref_index"; Value = 0x5; } Flag { ID = "lref_all"; Value = 0x6; } Flag { ID = "lpref_none"; } Flag { ID = "lpref_my_floor"; Value = 0x1; } Flag { ID = "lpref_tagged_floors"; Value = 0x2; } Flag { ID = "lpref_line_tagged_floors"; Value = 0x3; } Flag { ID = "lpref_act_tagged_floors"; Value = 0x4; } Flag { ID = "lpref_index_floor"; Value = 0x5; } Flag { ID = "lpref_all_floors"; Value = 0x6; } Flag { ID = "lpref_my_ceiling"; Value = 0x7; } Flag { ID = "lpref_tagged_ceilings"; Value = 0x8; } Flag { ID = "lpref_line_tagged_ceilings"; Value = 0x9; } Flag { ID = "lpref_act_tagged_ceilings"; Value = 0xA; } Flag { ID = "lpref_index_ceiling"; Value = 0xB; } Flag { ID = "lpref_all_ceilings"; Value = 0xC; } Flag { ID = "lpref_special"; Value = 0xD; } Flag { ID = "lpref_back_floor"; Value = 0xE; } Flag { ID = "lpref_back_ceiling"; Value = 0xF; } Flag { ID = "lpref_thing_exist_floors"; Value = 0x10; } Flag { ID = "lpref_thing_exist_ceilings"; Value = 0x11; } Flag { ID = "lpref_thing_noexist_floors"; Value = 0x12; } Flag { ID = "lpref_thing_noexist_ceilings"; Value = 0x13; } Flag { ID = "lsref_none"; } Flag { ID = "lsref_my"; Value = 0x1; } Flag { ID = "lsref_tagged"; Value = 0x2; } Flag { ID = "lsref_line_tagged"; Value = 0x3; } Flag { ID = "lsref_act_tagged"; Value = 0x4; } Flag { ID = "lsref_index"; Value = 0x5; } Flag { ID = "lsref_all"; Value = 0x6; } Flag { ID = "lsref_back"; Value = 0x7; } Flag { ID = "lsref_thing_exist"; Value = 0x8; } Flag { ID = "lsref_thing_noexist"; Value = 0x9; } Flag { ID = "spref_none"; } Flag { ID = "spref_my_floor"; Value = 0x1; } Flag { ID = "spref_my_ceiling"; Value = 0x2; } Flag { ID = "spref_original_floor"; Value = 0x3; } Flag { ID = "spref_original_ceiling"; Value = 0x4; } Flag { ID = "spref_current_floor"; Value = 0x5; } Flag { ID = "spref_current_ceiling"; Value = 0x6; } Flag { ID = "spref_highest_floor"; Value = 0x7; } Flag { ID = "spref_highest_ceiling"; Value = 0x8; } Flag { ID = "spref_lowest_floor"; Value = 0x9; } Flag { ID = "spref_lowest_ceiling"; Value = 0xA; } Flag { ID = "spref_next_highest_floor"; Value = 0xB; } Flag { ID = "spref_next_highest_ceiling"; Value = 0xC; } Flag { ID = "spref_next_lowest_floor"; Value = 0xD; } Flag { ID = "spref_next_lowest_ceiling"; Value = 0xE; } Flag { ID = "spref_min_bottom_texture"; Value = 0xF; } Flag { ID = "spref_min_bottom_material"; Value = 0xF; } # Alias for min_bottom_texture Flag { ID = "spref_min_mid_texture"; Value = 0x10; } Flag { ID = "spref_min_mid_material"; Value = 0x10; } # Alias for min_mid_texture Flag { ID = "spref_min_top_texture"; Value = 0x11; } Flag { ID = "spref_min_top_material"; Value = 0x11; } # Alias for min_top_texture Flag { ID = "spref_max_bottom_texture"; Value = 0x12; } Flag { ID = "spref_max_bottom_material"; Value = 0x12; } # Alias for max_bottom_texture Flag { ID = "spref_max_mid_texture"; Value = 0x13; } Flag { ID = "spref_max_mid_material"; Value = 0x13; } # Alias for max_mid_texture Flag { ID = "spref_max_top_texture"; Value = 0x14; } Flag { ID = "spref_max_top_material"; Value = 0x14; } # Alias for max_top_texture Flag { ID = "spref_sector_tagged_floor"; Value = 0x15; } Flag { ID = "spref_line_tagged_floor"; Value = 0x16; } Flag { ID = "spref_tagged_floor"; Value = 0x17; } Flag { ID = "spref_act_tagged_floor"; Value = 0x18; } Flag { ID = "spref_index_floor"; Value = 0x19; } Flag { ID = "spref_sector_tagged_ceiling"; Value = 0x20; } Flag { ID = "spref_line_tagged_ceiling"; Value = 0x21; } Flag { ID = "spref_tagged_ceiling"; Value = 0x22; } Flag { ID = "spref_act_tagged_ceiling"; Value = 0x23; } Flag { ID = "spref_index_ceiling"; Value = 0x24; } Flag { ID = "spref_back_floor"; Value = 0x25; } Flag { ID = "spref_back_ceiling"; Value = 0x26; } Flag { ID = "spref_special"; Value = 0x27; } Flag { ID = "spref_line_act_tagged_floor"; Value = 0x28; } Flag { ID = "spref_line_act_tagged_ceiling"; Value = 0x29; } Flag { ID = "lightref_none"; } Flag { ID = "lightref_my"; Value = 0x1; } Flag { ID = "lightref_original"; Value = 0x2; } Flag { ID = "lightref_current"; Value = 0x3; } Flag { ID = "lightref_highest"; Value = 0x4; } Flag { ID = "lightref_lowest"; Value = 0x5; } Flag { ID = "lightref_next_highest"; Value = 0x6; } Flag { ID = "lightref_next_lowest"; Value = 0x7; } Flag { ID = "lightref_back"; Value = 0x8; } Flag { ID = "ldref_none"; } Flag { ID = "ldref_id"; Value = 0x1; } Flag { ID = "ldref_special"; Value = 0x2; } Flag { ID = "ldref_tag"; Value = 0x3; } Flag { ID = "ldref_acttag"; Value = 0x4; } Flag { ID = "ldref_count"; Value = 0x5; } Flag { ID = "ldref_angle"; Value = 0x6; } Flag { ID = "ldref_length"; Value = 0x7; } Flag { ID = "ldref_top_offsetx"; Value = 0x8; } Flag { ID = "ldref_top_offsety"; Value = 0x9; } Flag { ID = "ldref_middle_offsetx"; Value = 0x10; } Flag { ID = "ldref_middle_offsety"; Value = 0x11; } Flag { ID = "ldref_bottom_offsetx"; Value = 0x12; } Flag { ID = "ldref_bottom_offsety"; Value = 0x13; } Flag { ID = "chsf_done_d"; Value = 0x1; } Flag { ID = "chsf_loop"; Value = 0x2; } Flag { ID = "pmf_crush"; Value = 0x1; } Flag { ID = "pmf_abort_a"; Value = 0x2; } Flag { ID = "pmf_abort_d"; Value = 0x4; } Flag { ID = "pmf_done_a"; Value = 0x8; } Flag { ID = "pmf_done_d"; Value = 0x10; } Flag { ID = "pmf_follow"; Value = 0x20; } Flag { ID = "pmf_setorig"; Value = 0x40; } Flag { ID = "pmf_1snd"; Value = 0x100; } Flag { ID = "lws_none"; } Flag { ID = "lws_mid"; Value = 0x1; } Flag { ID = "lws_upper"; Value = 0x2; } Flag { ID = "lws_lower"; Value = 0x3; } # XG Sectors Flag { ID = "stf_gravity"; Value = 0x1; } Flag { ID = "stf_friction"; Value = 0x2; } Flag { ID = "stf_crush"; Value = 0x4; } Flag { ID = "stf_tagtexmove"; Value = 0x80; } Flag { ID = "stf_windplayer"; Value = 0x8; } Flag { ID = "stf_windother"; Value = 0x10; } Flag { ID = "stf_windmonster"; Value = 0x20; } Flag { ID = "stf_windmissile"; Value = 0x40; } Flag { ID = "stf_windany"; Value = 0x18; } Flag { ID = "stf_tagwind"; Value = 0x100; } Flag { ID = "stf_floorwind"; Value = 0x200; } Flag { ID = "stf_ceilingwind"; Value = 0x400; } Flag { ID = "scef_player_a"; Value = 0x1; } Flag { ID = "scef_other_a"; Value = 0x2; } Flag { ID = "scef_monster_a"; Value = 0x4; } Flag { ID = "scef_missile_a"; Value = 0x8; } Flag { ID = "scef_any_a"; Value = 0x10; } Flag { ID = "scef_ticker_a"; Value = 0x20; } Flag { ID = "scef_player_d"; Value = 0x40; } Flag { ID = "scef_other_d"; Value = 0x80; } Flag { ID = "scef_monster_d"; Value = 0x100; } Flag { ID = "scef_missile_d"; Value = 0x200; } Flag { ID = "scef_any_d"; Value = 0x400; } Flag { ID = "scef_ticker_d"; Value = 0x800; } doomsday-stable-1.15.7/doomsday/net.dengine.base.pack/Info0000664000175000017500000000016212641367671022655 0ustar jaakkojaakko# Shared resources for all Doomsday applications title: Doomsday Base version: 1.15.7 license: GPL 3+ tags: core doomsday-stable-1.15.7/doomsday/doomsday.doxy0000664000175000017500000002340212641367670020540 0ustar jaakkojaakko# Doxyfile 1.7.5.1 # This is a shared configuration file for all API documentation. #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- DOXYFILE_ENCODING = UTF-8 #PROJECT_NAME = #PROJECT_NUMBER = #PROJECT_BRIEF = #PROJECT_LOGO = #OUTPUT_DIRECTORY = CREATE_SUBDIRS = NO OUTPUT_LANGUAGE = English BRIEF_MEMBER_DESC = YES REPEAT_BRIEF = YES ABBREVIATE_BRIEF = ALWAYS_DETAILED_SEC = NO INLINE_INHERITED_MEMB = NO FULL_PATH_NAMES = YES STRIP_FROM_PATH = /Users/jaakko/Projects/deng/doomsday STRIP_FROM_INC_PATH = SHORT_NAMES = NO JAVADOC_AUTOBRIEF = YES QT_AUTOBRIEF = NO MULTILINE_CPP_IS_BRIEF = NO INHERIT_DOCS = YES SEPARATE_MEMBER_PAGES = NO TAB_SIZE = 4 ALIASES = OPTIMIZE_OUTPUT_FOR_C = YES OPTIMIZE_OUTPUT_JAVA = NO OPTIMIZE_FOR_FORTRAN = NO OPTIMIZE_OUTPUT_VHDL = NO EXTENSION_MAPPING = BUILTIN_STL_SUPPORT = YES CPP_CLI_SUPPORT = NO SIP_SUPPORT = NO IDL_PROPERTY_SUPPORT = YES DISTRIBUTE_GROUP_DOC = YES SUBGROUPING = YES INLINE_GROUPED_CLASSES = NO INLINE_SIMPLE_STRUCTS = NO TYPEDEF_HIDES_STRUCT = NO #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- EXTRACT_ALL = YES EXTRACT_PRIVATE = YES EXTRACT_STATIC = YES EXTRACT_LOCAL_CLASSES = YES EXTRACT_LOCAL_METHODS = YES EXTRACT_ANON_NSPACES = NO HIDE_UNDOC_MEMBERS = NO HIDE_UNDOC_CLASSES = NO HIDE_FRIEND_COMPOUNDS = NO HIDE_IN_BODY_DOCS = NO INTERNAL_DOCS = YES CASE_SENSE_NAMES = NO HIDE_SCOPE_NAMES = NO SHOW_INCLUDE_FILES = YES FORCE_LOCAL_INCLUDES = NO INLINE_INFO = YES SORT_MEMBER_DOCS = YES SORT_BRIEF_DOCS = YES SORT_MEMBERS_CTORS_1ST = YES SORT_GROUP_NAMES = YES SORT_BY_SCOPE_NAME = NO STRICT_PROTO_MATCHING = NO GENERATE_TODOLIST = YES GENERATE_TESTLIST = YES GENERATE_BUGLIST = YES GENERATE_DEPRECATEDLIST= YES ENABLED_SECTIONS = MAX_INITIALIZER_LINES = 30 SHOW_USED_FILES = YES SHOW_FILES = YES SHOW_NAMESPACES = NO FILE_VERSION_FILTER = LAYOUT_FILE = CITE_BIB_FILES = #--------------------------------------------------------------------------- # configuration options related to warning and progress messages #--------------------------------------------------------------------------- QUIET = NO WARNINGS = YES WARN_IF_UNDOCUMENTED = NO WARN_IF_DOC_ERROR = YES WARN_NO_PARAMDOC = NO WARN_FORMAT = "$file:$line: $text" WARN_LOGFILE = #--------------------------------------------------------------------------- # configuration options related to the input files #--------------------------------------------------------------------------- #INPUT = INPUT_ENCODING = UTF-8 FILE_PATTERNS = RECURSIVE = YES EXCLUDE = EXCLUDE_SYMLINKS = NO EXCLUDE_PATTERNS = EXCLUDE_SYMBOLS = EXAMPLE_PATH = EXAMPLE_PATTERNS = EXAMPLE_RECURSIVE = NO IMAGE_PATH = INPUT_FILTER = FILTER_PATTERNS = FILTER_SOURCE_FILES = NO FILTER_SOURCE_PATTERNS = #--------------------------------------------------------------------------- # configuration options related to source browsing #--------------------------------------------------------------------------- SOURCE_BROWSER = YES INLINE_SOURCES = NO STRIP_CODE_COMMENTS = YES REFERENCED_BY_RELATION = YES REFERENCES_RELATION = NO REFERENCES_LINK_SOURCE = YES USE_HTAGS = NO VERBATIM_HEADERS = YES #--------------------------------------------------------------------------- # configuration options related to the alphabetical class index #--------------------------------------------------------------------------- ALPHABETICAL_INDEX = YES COLS_IN_ALPHA_INDEX = 4 IGNORE_PREFIX = #--------------------------------------------------------------------------- # configuration options related to the HTML output #--------------------------------------------------------------------------- GENERATE_HTML = YES HTML_OUTPUT = . HTML_FILE_EXTENSION = .html HTML_HEADER = HTML_FOOTER = HTML_STYLESHEET = doxygen.css HTML_EXTRA_FILES = HTML_COLORSTYLE_HUE = 220 HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 HTML_TIMESTAMP = YES HTML_DYNAMIC_SECTIONS = YES GENERATE_DOCSET = NO DOCSET_FEEDNAME = "Doomsday Engine Manual" DOCSET_BUNDLE_ID = net.sourceforge.deng DOCSET_PUBLISHER_ID = net.sourceforge.users.skyjake DOCSET_PUBLISHER_NAME = skyjake GENERATE_HTMLHELP = NO CHM_FILE = HHC_LOCATION = GENERATE_CHI = NO CHM_INDEX_ENCODING = BINARY_TOC = NO TOC_EXPAND = NO GENERATE_QHP = NO QCH_FILE = QHP_NAMESPACE = org.doxygen.Project QHP_VIRTUAL_FOLDER = doc QHP_CUST_FILTER_NAME = QHP_CUST_FILTER_ATTRS = QHP_SECT_FILTER_ATTRS = QHG_LOCATION = GENERATE_ECLIPSEHELP = NO ECLIPSE_DOC_ID = org.doxygen.Project DISABLE_INDEX = NO ENUM_VALUES_PER_LINE = 4 GENERATE_TREEVIEW = NO TREEVIEW_WIDTH = 250 EXT_LINKS_IN_WINDOW = NO FORMULA_FONTSIZE = 10 FORMULA_TRANSPARENT = YES USE_MATHJAX = NO MATHJAX_RELPATH = http://www.mathjax.org/mathjax MATHJAX_EXTENSIONS = SEARCHENGINE = YES SERVER_BASED_SEARCH = NO #--------------------------------------------------------------------------- # configuration options related to the LaTeX output #--------------------------------------------------------------------------- GENERATE_LATEX = NO LATEX_OUTPUT = latex LATEX_CMD_NAME = latex MAKEINDEX_CMD_NAME = makeindex COMPACT_LATEX = NO PAPER_TYPE = a4 EXTRA_PACKAGES = LATEX_HEADER = LATEX_FOOTER = PDF_HYPERLINKS = YES USE_PDFLATEX = YES LATEX_BATCHMODE = NO LATEX_HIDE_INDICES = NO LATEX_SOURCE_CODE = NO LATEX_BIB_STYLE = plain #--------------------------------------------------------------------------- # configuration options related to the RTF output #--------------------------------------------------------------------------- GENERATE_RTF = NO RTF_OUTPUT = rtf COMPACT_RTF = NO RTF_HYPERLINKS = NO RTF_STYLESHEET_FILE = RTF_EXTENSIONS_FILE = #--------------------------------------------------------------------------- # configuration options related to the man page output #--------------------------------------------------------------------------- GENERATE_MAN = NO MAN_OUTPUT = man MAN_EXTENSION = .3 MAN_LINKS = NO #--------------------------------------------------------------------------- # configuration options related to the XML output #--------------------------------------------------------------------------- GENERATE_XML = NO XML_OUTPUT = xml XML_PROGRAMLISTING = YES #--------------------------------------------------------------------------- # configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- GENERATE_AUTOGEN_DEF = NO #--------------------------------------------------------------------------- # configuration options related to the Perl module output #--------------------------------------------------------------------------- GENERATE_PERLMOD = NO PERLMOD_LATEX = NO PERLMOD_PRETTY = YES PERLMOD_MAKEVAR_PREFIX = #--------------------------------------------------------------------------- # Configuration options related to the preprocessor #--------------------------------------------------------------------------- ENABLE_PREPROCESSING = YES MACRO_EXPANSION = NO EXPAND_ONLY_PREDEF = NO SEARCH_INCLUDES = YES INCLUDE_PATH = INCLUDE_FILE_PATTERNS = PREDEFINED = EXPAND_AS_DEFINED = SKIP_FUNCTION_MACROS = YES #--------------------------------------------------------------------------- # Configuration::additions related to external references #--------------------------------------------------------------------------- TAGFILES = GENERATE_TAGFILE = ALLEXTERNALS = NO EXTERNAL_GROUPS = YES PERL_PATH = /usr/bin/perl #--------------------------------------------------------------------------- # Configuration options related to the dot tool #--------------------------------------------------------------------------- CLASS_DIAGRAMS = YES MSCGEN_PATH = HIDE_UNDOC_RELATIONS = YES HAVE_DOT = YES DOT_NUM_THREADS = 0 DOT_FONTNAME = Helvetica.ttf DOT_FONTSIZE = 10 DOT_FONTPATH = /Users/jaakko/Fonts/Helvetica/ CLASS_GRAPH = YES COLLABORATION_GRAPH = YES GROUP_GRAPHS = YES UML_LOOK = NO TEMPLATE_RELATIONS = NO INCLUDE_GRAPH = YES INCLUDED_BY_GRAPH = NO CALL_GRAPH = NO CALLER_GRAPH = NO GRAPHICAL_HIERARCHY = YES DIRECTORY_GRAPH = YES DOT_IMAGE_FORMAT = png INTERACTIVE_SVG = NO DOT_PATH = DOTFILE_DIRS = MSCFILE_DIRS = DOT_GRAPH_MAX_NODES = 50 MAX_DOT_GRAPH_DEPTH = 0 DOT_TRANSPARENT = YES DOT_MULTI_TARGETS = NO GENERATE_LEGEND = YES DOT_CLEANUP = YES doomsday-stable-1.15.7/doomsday/client/0000775000175000017500000000000012641367670017271 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/client-mac.doxy0000664000175000017500000000135012641367670022211 0ustar jaakkojaakko# API documentation for the Doomsday Client @INCLUDE = ../doomsday.doxy PROJECT_NAME = "Doomsday Client" PROJECT_NUMBER = 1.15 PROJECT_BRIEF = "Internal documentation (Mac OS X)" OUTPUT_DIRECTORY = ../apidoc/mac/ INPUT = ../api include src EXCLUDE = include/windows src/windows MACRO_EXPANSION = YES PREDEFINED = __DOOMSDAY__ __CLIENT__ UNIX MACOSX __cplusplus "C_DECL=" "GL_CALL=" \ "DENG2_PIMPL(ClassName)=typedef ClassName Public; struct ClassName::Instance : public de::Private" \ "DENG2_PIMPL_NOREF(C)=struct C::Instance : public de::IPrivate" \ "DENG2_PRIVATE(Var)=struct Instance; Instance *Var;" doomsday-stable-1.15.7/doomsday/client/api.doxy0000664000175000017500000000133212641367670020746 0ustar jaakkojaakko# Public API documentation for Doomsday and liblegacy @INCLUDE = ../doomsday.doxy PROJECT_NAME = "Doomsday and liblegacy" PROJECT_NUMBER = 1.15 PROJECT_BRIEF = "Public API" OUTPUT_DIRECTORY = ../apidoc/api/ INPUT = ../api ../liblegacy/include PREDEFINED = __DOOMSDAY__ __DENG__ \ "DENG_API_TYPEDEF(Name)=typedef struct de_api_##Name##_s" \ "DENG_API_T(Name)=de_api_##Name##_t" \ "DENG_DECLARE_API(Name)=DENG_API_T(Name) _api_##Name" INCLUDED_BY_GRAPH = YES REFERENCED_BY_RELATION = NO OPTIMIZE_OUTPUT_FOR_C = NO MACRO_EXPANSION = YES EXPAND_ONLY_PREDEF = YES doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/0000775000175000017500000000000012641367670023661 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/0000775000175000017500000000000012641367670026404 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/profiles/0000775000175000017500000000000012641367670030227 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/profiles/renderer/0000775000175000017500000000000012641367670032035 5ustar jaakkojaakko././@LongLink0000644000000000000000000000015400000000000011603 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/profiles/renderer/defaults.deidoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/profiles/renderer/defau0000664000175000017500000000016712641367670033050 0ustar jaakkojaakkoprofile { name: Defaults (built-in) # The values for all settings are taken from the built-in defaults. } ././@LongLink0000644000000000000000000000015500000000000011604 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/profiles/renderer/amplified.deidoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/profiles/renderer/ampli0000664000175000017500000001020212641367670033055 0ustar jaakkojaakkoprofile { name: Amplified (built-in) setting "rend-bloom" { value: 1 } setting "rend-bloom-intensity" { value: 0.8 } setting "rend-bloom-threshold" { value: 0.3 } setting "rend-fakeradio" { value: 1 } setting "rend-fakeradio-darkness" { value: 1.2 } setting "rend-glow" { value: 0.8 } setting "rend-glow-height" { value: 200 } setting "rend-glow-scale" { value: 3 } setting "rend-glow-wall" { value: 1 } setting "rend-halo" { value: 5 } setting "rend-halo-bright" { value: 48 } setting "rend-halo-dim-far" { value: 100 } setting "rend-halo-dim-near" { value: 10 } setting "rend-halo-occlusion" { value: 48 } setting "rend-halo-radius-min" { value: 25 } setting "rend-halo-realistic" { value: 0 } setting "rend-halo-secondary-limit" { value: 1 } setting "rend-halo-size" { value: 80 } setting "rend-halo-zmag-div" { value: 50 } setting "rend-light" { value: 1 } setting "rend-light-ambient" { value: 0 } setting "rend-light-attenuation" { value: 924 } setting "rend-light-blend" { value: 0 } setting "rend-light-bright" { value: 0.375 } setting "rend-light-compression" { value: -0.1 } setting "rend-light-decor" { value: 1 } setting "rend-light-fog-bright" { value: 0.295833 } setting "rend-light-num" { value: 0 } setting "rend-light-radius-max" { value: 256 } setting "rend-light-radius-scale" { value: 4.8 } setting "rend-light-sky" { value: 0.30425 } setting "rend-light-sky-auto" { value: 1 } setting "rend-light-wall-angle" { value: 1.2 } setting "rend-light-wall-angle-smooth" { value: 1 } setting "rend-mobj-smooth-move" { value: 2 } setting "rend-mobj-smooth-turn" { value: 1 } setting "rend-model" { value: 1 } setting "rend-model-distance" { value: 1500 } setting "rend-model-inter" { value: 1 } setting "rend-model-lights" { value: 4 } setting "rend-model-lod" { value: 256 } setting "rend-particle" { value: 1 } setting "rend-particle-diffuse" { value: 4 } setting "rend-particle-max" { value: 0 } setting "rend-particle-rate" { value: 1 } setting "rend-particle-visible-near" { value: 0 } setting "rend-shadow" { value: 1 } setting "rend-shadow-darkness" { value: 1.2 } setting "rend-shadow-far" { value: 1000 } setting "rend-shadow-radius-max" { value: 80 } setting "rend-sky-distance" { value: 1600 } setting "rend-sprite-align" { value: 0 } setting "rend-sprite-blend" { value: 1 } setting "rend-sprite-lights" { value: 4 } setting "rend-sprite-mode" { value: 0 } setting "rend-sprite-noz" { value: 0 } setting "rend-tex-anim-smooth" { value: 1 } setting "rend-tex-detail" { value: 1 } setting "rend-tex-detail-scale" { value: 4 } setting "rend-tex-detail-strength" { value: 0.5 } setting "rend-tex-filter-anisotropic" { value: -1 } setting "rend-tex-filter-mag" { value: 1 } setting "rend-tex-filter-smart" { value: 1 } setting "rend-tex-filter-sprite" { value: 1 } setting "rend-tex-filter-ui" { value: 1 } setting "rend-tex-mipmap" { value: 5 } setting "rend-tex-quality" { value: 8 } setting "rend-tex-shiny" { value: 1 } setting "rend-vignette" { value: 1 } setting "rend-vignette-darkness" { value: 1 } setting "rend-vignette-width" { value: 1 } } ././@LongLink0000644000000000000000000000015300000000000011602 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/profiles/renderer/vanilla.deidoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/profiles/renderer/vanil0000664000175000017500000000776312641367670033106 0ustar jaakkojaakkoprofile { name: Vanilla (built-in) setting "rend-bloom" { value: 0 } setting "rend-fakeradio" { value: 0 } setting "rend-fakeradio-darkness" { value: 1.2 } setting "rend-glow" { value: 0 } setting "rend-glow-height" { value: 100 } setting "rend-glow-scale" { value: 3 } setting "rend-glow-wall" { value: 1 } setting "rend-halo" { value: 0 } setting "rend-halo-bright" { value: 35 } setting "rend-halo-dim-far" { value: 100 } setting "rend-halo-dim-near" { value: 10 } setting "rend-halo-occlusion" { value: 48 } setting "rend-halo-radius-min" { value: 20 } setting "rend-halo-realistic" { value: 1 } setting "rend-halo-secondary-limit" { value: 1 } setting "rend-halo-size" { value: 80 } setting "rend-halo-zmag-div" { value: 100 } setting "rend-light" { value: 0 } setting "rend-light-ambient" { value: 0 } setting "rend-light-attenuation" { value: 1024 } setting "rend-light-blend" { value: 0 } setting "rend-light-bright" { value: 0.46875 } setting "rend-light-compression" { value: 0 } setting "rend-light-decor" { value: 0 } setting "rend-light-fog-bright" { value: 0.15 } setting "rend-light-num" { value: 0 } setting "rend-light-radius-max" { value: 289 } setting "rend-light-radius-scale" { value: 6.50625 } setting "rend-light-sky" { value: 0.29375 } setting "rend-light-sky-auto" { value: 0 } setting "rend-light-wall-angle" { value: 1.2 } setting "rend-light-wall-angle-smooth" { value: 0 } setting "rend-mobj-smooth-move" { value: 0 } setting "rend-mobj-smooth-turn" { value: 0 } setting "rend-model" { value: 0 } setting "rend-model-distance" { value: 1500 } setting "rend-model-inter" { value: 1 } setting "rend-model-lights" { value: 4 } setting "rend-model-lod" { value: 256 } setting "rend-particle" { value: 0 } setting "rend-particle-diffuse" { value: 4 } setting "rend-particle-max" { value: 0 } setting "rend-particle-rate" { value: 1 } setting "rend-particle-visible-near" { value: 0 } setting "rend-shadow" { value: 0 } setting "rend-shadow-darkness" { value: 1.2 } setting "rend-shadow-far" { value: 1000 } setting "rend-shadow-radius-max" { value: 80 } setting "rend-sky-distance" { value: 1600 } setting "rend-sprite-align" { value: 1 } setting "rend-sprite-blend" { value: 0 } setting "rend-sprite-lights" { value: 4 } setting "rend-sprite-mode" { value: 1 } setting "rend-sprite-noz" { value: 1 } setting "rend-tex-anim-smooth" { value: 0 } setting "rend-tex-detail" { value: 0 } setting "rend-tex-detail-scale" { value: 4 } setting "rend-tex-detail-strength" { value: 0.5 } setting "rend-tex-filter-anisotropic" { value: 0 } setting "rend-tex-filter-mag" { value: 0 } setting "rend-tex-filter-smart" { value: 0 } setting "rend-tex-filter-sprite" { value: 0 } setting "rend-tex-filter-ui" { value: 0 } setting "rend-tex-mipmap" { value: 0 } setting "rend-tex-quality" { value: 8 } setting "rend-tex-shiny" { value: 0 } setting "rend-vignette" { value: 0 } setting "rend-vignette-darkness" { value: 1 } setting "rend-vignette-width" { value: 1 } } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/shaders/0000775000175000017500000000000012641367670030035 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/shaders/generic.dei0000664000175000017500000000627712641367670032150 0ustar jaakkojaakkogroup generic { # Simple shader with untextured vertices. There is an additional constant # color applied to all vertices. Uses a combined model-view-projection # matrix. shader color_ucolor { vertex = " uniform highp mat4 uMvpMatrix; uniform highp vec4 uColor; attribute highp vec4 aVertex; attribute highp vec4 aColor; varying highp vec4 vColor; void main(void) { gl_Position = uMvpMatrix * aVertex; vColor = uColor * aColor; }" fragment = " varying highp vec4 vColor; void main(void) { gl_FragColor = vColor; }" } shader texture { vertex = " uniform highp mat4 uMvpMatrix; attribute highp vec4 aVertex; attribute highp vec2 aUV; varying highp vec2 vUV; void main(void) { gl_Position = uMvpMatrix * aVertex; vUV = aUV; }" fragment = " uniform sampler2D uTex; varying highp vec2 vUV; void main(void) { gl_FragColor = texture2D(uTex, vUV); }" } group textured { # Simple shader with one texture plus a color per vertex. Uses a # combined model-view-projection matrix. shader color { vertex = " uniform highp mat4 uMvpMatrix; attribute highp vec4 aVertex; attribute highp vec2 aUV; attribute highp vec4 aColor; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { gl_Position = uMvpMatrix * aVertex; vUV = aUV; vColor = aColor; }" fragment = " uniform sampler2D uTex; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { gl_FragColor = vColor * texture2D(uTex, vUV); }" } # Simple shader with one texture plus a color per vertex. There is # an additional constant color applied to all vertices. Uses a # combined model-view-projection matrix. shader color_ucolor { vertex = " uniform highp mat4 uMvpMatrix; uniform highp vec4 uColor; attribute highp vec4 aVertex; attribute highp vec2 aUV; attribute highp vec4 aColor; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { gl_Position = uMvpMatrix * aVertex; vUV = aUV; vColor = aColor * uColor; }" fragment = " uniform sampler2D uTex; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { gl_FragColor = vColor * texture2D(uTex, vUV); }" } } } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/shaders/debug.dei0000664000175000017500000000164412641367670031613 0ustar jaakkojaakkogroup debug { group textured { # Visualize alpha information. shader alpha { vertex = " uniform highp mat4 uMvpMatrix; attribute highp vec4 aVertex; attribute highp vec2 aUV; attribute highp vec4 aColor; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { gl_Position = uMvpMatrix * aVertex; vUV = aUV; vColor = aColor; }" fragment = " uniform sampler2D uTex; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { highp vec4 col = vColor * texture2D(uTex, vUV); gl_FragColor = vec4(col.a, col.a, col.a, 1.0); }" } } } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/shaders/fx/0000775000175000017500000000000012641367670030452 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/shaders/fx/bloom.dei0000664000175000017500000001016412641367670032247 0ustar jaakkojaakko# Bloom is a specialized additive blur that only applies to bright pixels # (given a configurable threshold for brightness). group fx.bloom { common = " uniform highp mat4 uMvpMatrix; uniform highp vec4 uColor; uniform highp vec4 uWindow; attribute highp vec4 aVertex; attribute highp vec2 aUV; varying highp vec2 vUV; void main(void) { gl_Position = uMvpMatrix * aVertex; vUV = uWindow.xy + aUV * uWindow.zw; }" shader horizontal { vertex $= fx.bloom.common fragment = " uniform sampler2D uTex; uniform highp float uIntensity; uniform highp float uThreshold; uniform highp vec2 uBlurStep; varying highp vec2 vUV; void main(void) { highp vec4 sum = vec4(0.0); sum += texture2D(uTex, vec2(vUV.s - 7.0 * uBlurStep.s, vUV.t)) * 0.0249; sum += texture2D(uTex, vec2(vUV.s - 6.0 * uBlurStep.s, vUV.t)) * 0.0367; sum += texture2D(uTex, vec2(vUV.s - 5.0 * uBlurStep.s, vUV.t)) * 0.0498; sum += texture2D(uTex, vec2(vUV.s - 4.0 * uBlurStep.s, vUV.t)) * 0.0660; sum += texture2D(uTex, vec2(vUV.s - 3.0 * uBlurStep.s, vUV.t)) * 0.0803; sum += texture2D(uTex, vec2(vUV.s - 2.0 * uBlurStep.s, vUV.t)) * 0.0915; sum += texture2D(uTex, vec2(vUV.s - uBlurStep.s, vUV.t)) * 0.0996; sum += texture2D(uTex, vUV) * 0.1027; sum += texture2D(uTex, vec2(vUV.s + uBlurStep.s, vUV.t)) * 0.0996; sum += texture2D(uTex, vec2(vUV.s + 2.0 * uBlurStep.s, vUV.t)) * 0.0915; sum += texture2D(uTex, vec2(vUV.s + 3.0 * uBlurStep.s, vUV.t)) * 0.0803; sum += texture2D(uTex, vec2(vUV.s + 4.0 * uBlurStep.s, vUV.t)) * 0.0660; sum += texture2D(uTex, vec2(vUV.s + 5.0 * uBlurStep.s, vUV.t)) * 0.0498; sum += texture2D(uTex, vec2(vUV.s + 6.0 * uBlurStep.s, vUV.t)) * 0.0367; sum += texture2D(uTex, vec2(vUV.s + 7.0 * uBlurStep.s, vUV.t)) * 0.0249; // Apply a threshold that gets rid of dark, non-luminous pixels. highp float intens = max(sum.r, max(sum.g, sum.b)); intens = (intens - uThreshold) * uIntensity; gl_FragColor = vec4(vec3(intens) * normalize(sum.rgb + 0.2), 1.0); }" } shader vertical { vertex $= fx.bloom.common fragment = " uniform sampler2D uTex; uniform highp vec2 uBlurStep; varying highp vec2 vUV; void main(void) { highp vec4 sum = vec4(0.0); sum += texture2D(uTex, vec2(vUV.s, vUV.t - 7.0 * uBlurStep.t)) * 0.0249; sum += texture2D(uTex, vec2(vUV.s, vUV.t - 6.0 * uBlurStep.t)) * 0.0367; sum += texture2D(uTex, vec2(vUV.s, vUV.t - 5.0 * uBlurStep.t)) * 0.0498; sum += texture2D(uTex, vec2(vUV.s, vUV.t - 4.0 * uBlurStep.t)) * 0.0660; sum += texture2D(uTex, vec2(vUV.s, vUV.t - 3.0 * uBlurStep.t)) * 0.0803; sum += texture2D(uTex, vec2(vUV.s, vUV.t - 2.0 * uBlurStep.t)) * 0.0915; sum += texture2D(uTex, vec2(vUV.s, vUV.t - uBlurStep.t )) * 0.0996; sum += texture2D(uTex, vUV) * 0.1027; sum += texture2D(uTex, vec2(vUV.s, vUV.t + uBlurStep.t )) * 0.0996; sum += texture2D(uTex, vec2(vUV.s, vUV.t + 2.0 * uBlurStep.t)) * 0.0915; sum += texture2D(uTex, vec2(vUV.s, vUV.t + 3.0 * uBlurStep.t)) * 0.0803; sum += texture2D(uTex, vec2(vUV.s, vUV.t + 4.0 * uBlurStep.t)) * 0.0660; sum += texture2D(uTex, vec2(vUV.s, vUV.t + 5.0 * uBlurStep.t)) * 0.0498; sum += texture2D(uTex, vec2(vUV.s, vUV.t + 6.0 * uBlurStep.t)) * 0.0367; sum += texture2D(uTex, vec2(vUV.s, vUV.t + 7.0 * uBlurStep.t)) * 0.0249; gl_FragColor = sum; }" } } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/shaders/fx/post.dei0000664000175000017500000000443612641367670032131 0ustar jaakkojaakkogroup fx { # Post-processing shaders need to have uFadeInOut (0..1) for # fading the effect in/out. group post { shader monochrome { vertex = " uniform highp mat4 uMvpMatrix; attribute highp vec4 aVertex; attribute highp vec2 aUV; varying highp vec2 vUV; void main(void) { gl_Position = uMvpMatrix * aVertex; vUV = aUV; }" fragment = " uniform sampler2D uTex; uniform highp float uFadeInOut; varying highp vec2 vUV; void main(void) { highp vec4 original = texture2D(uTex, vUV); highp float intens = (0.2125 * original.r) + (0.7154 * original.g) + (0.0721 * original.b); gl_FragColor = vec4(vec3(intens), 1.0); if(uFadeInOut < 1.0) { gl_FragColor = mix(original, gl_FragColor, uFadeInOut); } }" } shader monochrome.inverted { vertex = " uniform highp mat4 uMvpMatrix; attribute highp vec4 aVertex; attribute highp vec2 aUV; varying highp vec2 vUV; void main(void) { gl_Position = uMvpMatrix * aVertex; vUV = aUV; }" fragment = " uniform sampler2D uTex; uniform highp float uFadeInOut; varying highp vec2 vUV; void main(void) { highp vec4 original = texture2D(uTex, vUV); highp float intens = (0.2125 * original.r) + (0.7154 * original.g) + (0.0721 * original.b); gl_FragColor = vec4(vec3(1.0 - intens), 1.0); if(uFadeInOut < 1.0) { gl_FragColor = mix(original, gl_FragColor, uFadeInOut); } }" } } } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/shaders/fx/blur.dei0000664000175000017500000000635412641367670032111 0ustar jaakkojaakkogroup fx { # Blurring is done in two passes: horizontal and vertical. This is more # efficient than doing the equivalent amount of blurring in a single pass. group blur { # Both passes use the same vertex shader. common = " uniform highp mat4 uMvpMatrix; uniform highp vec4 uColor; uniform highp vec4 uWindow; attribute highp vec4 aVertex; attribute highp vec2 aUV; attribute highp vec4 aColor; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { gl_Position = uMvpMatrix * aVertex; vUV = uWindow.xy + aUV * uWindow.zw; vColor = aColor * uColor; }" shader horizontal { vertex $= fx.blur.common fragment = " uniform sampler2D uTex; uniform highp vec2 uBlurStep; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { highp vec4 sum = vec4(0.0); sum += texture2D(uTex, vec2(vUV.s - 4.0 * uBlurStep.s, vUV.t)) * 0.05; sum += texture2D(uTex, vec2(vUV.s - 3.0 * uBlurStep.s, vUV.t)) * 0.09; sum += texture2D(uTex, vec2(vUV.s - 2.0 * uBlurStep.s, vUV.t)) * 0.123; sum += texture2D(uTex, vec2(vUV.s - uBlurStep.s, vUV.t)) * 0.154; sum += texture2D(uTex, vUV) * 0.165; sum += texture2D(uTex, vec2(vUV.s + uBlurStep.s, vUV.t)) * 0.154; sum += texture2D(uTex, vec2(vUV.s + 2.0 * uBlurStep.s, vUV.t)) * 0.123; sum += texture2D(uTex, vec2(vUV.s + 3.0 * uBlurStep.s, vUV.t)) * 0.09; sum += texture2D(uTex, vec2(vUV.s + 4.0 * uBlurStep.s, vUV.t)) * 0.05; gl_FragColor = sum; gl_FragColor.a = 1.0; }" } shader vertical { vertex $= fx.blur.common fragment = " uniform sampler2D uTex; uniform highp vec2 uBlurStep; varying highp vec2 vUV; varying highp vec4 vColor; void main(void) { highp vec4 sum = vec4(0.0); sum += texture2D(uTex, vec2(vUV.s, vUV.t - 4.0 * uBlurStep.t)) * 0.05; sum += texture2D(uTex, vec2(vUV.s, vUV.t - 3.0 * uBlurStep.t)) * 0.09; sum += texture2D(uTex, vec2(vUV.s, vUV.t - 2.0 * uBlurStep.t)) * 0.123; sum += texture2D(uTex, vec2(vUV.s, vUV.t - uBlurStep.t )) * 0.154; sum += texture2D(uTex, vUV) * 0.165; sum += texture2D(uTex, vec2(vUV.s, vUV.t + uBlurStep.t )) * 0.154; sum += texture2D(uTex, vec2(vUV.s, vUV.t + 2.0 * uBlurStep.t)) * 0.123; sum += texture2D(uTex, vec2(vUV.s, vUV.t + 3.0 * uBlurStep.t)) * 0.09; sum += texture2D(uTex, vec2(vUV.s, vUV.t + 4.0 * uBlurStep.t)) * 0.05; gl_FragColor = sum * vColor; }" } } } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/shaders/model.dei0000664000175000017500000001350012641367670031617 0ustar jaakkojaakkogroup model { # Shader for skeletal animation and generic per-pixel lighting: # diffuse color, normal map, emission map, specular intensity. shader skeletal.normal_specular_emission { # Mapping when used with ModelDrawable. textureMapping vertex = " uniform highp mat4 uMvpMatrix; uniform highp mat4 uBoneMatrices[64]; uniform highp vec3 uLightDirs[4]; uniform highp vec3 uEyeDir; attribute highp vec4 aVertex; attribute highp vec3 aNormal; attribute highp vec3 aTangent; attribute highp vec3 aBitangent; attribute highp vec2 aUV; attribute highp vec4 aBounds; // diffuse map attribute highp vec4 aBounds2; // normal map attribute highp vec4 aBounds3; // specular map attribute highp vec4 aBounds4; // emission map attribute highp vec4 aBoneIDs; attribute highp vec4 aBoneWeights; varying highp vec2 vUV; varying highp vec2 vNormalUV; varying highp vec2 vSpecularUV; varying highp vec2 vEmissionUV; varying highp vec3 vLightDirs[4]; // tangent space varying highp vec3 vEyeDir; // tangent space highp vec3 transformVector(highp vec3 dir, highp mat4 matrix) { return (matrix * vec4(dir, 0.0)).xyz; } void main(void) { // Bone transformation. highp mat4 bone = uBoneMatrices[int(aBoneIDs.x + 0.5)] * aBoneWeights.x + uBoneMatrices[int(aBoneIDs.y + 0.5)] * aBoneWeights.y + uBoneMatrices[int(aBoneIDs.z + 0.5)] * aBoneWeights.z + uBoneMatrices[int(aBoneIDs.w + 0.5)] * aBoneWeights.w; // Vertex position. highp vec4 modelPos = bone * aVertex; gl_Position = uMvpMatrix * modelPos; // Tangent space. highp vec3 normal = transformVector(aNormal, bone); highp vec3 tangent = transformVector(aTangent, bone); highp vec3 bitangent = transformVector(aBitangent, bone); highp mat3 tangentSpace = mat3(tangent, bitangent, normal); // Light direction in tangent space. vLightDirs[0] = uLightDirs[0] * tangentSpace; vLightDirs[1] = uLightDirs[1] * tangentSpace; vLightDirs[2] = uLightDirs[2] * tangentSpace; vLightDirs[3] = uLightDirs[3] * tangentSpace; // Eye direction in tangent space. vEyeDir = uEyeDir * tangentSpace; vUV = aBounds.xy + aUV * aBounds.zw; vNormalUV = aBounds2.xy + aUV * aBounds2.zw; vSpecularUV = aBounds3.xy + aUV * aBounds3.zw; vEmissionUV = aBounds4.xy + aUV * aBounds4.zw; }" fragment = " uniform sampler2D uTex; uniform highp vec4 uAmbientLight; uniform highp vec4 uLightIntensities[4]; varying highp vec2 vUV; varying highp vec2 vNormalUV; varying highp vec2 vSpecularUV; varying highp vec2 vEmissionUV; varying highp vec3 vLightDirs[4]; // tangent space varying highp vec3 vEyeDir; // tangent space highp vec3 normalVector() { return (texture2D(uTex, vNormalUV).xyz * 2.0) - 1.0; } highp vec4 diffuseLight(int index, highp vec3 normal) { if(uLightIntensities[index].a <= 0.001) { return vec4(0.0); // too dim } highp float d = dot(normal, normalize(vLightDirs[index])); return max(d * uLightIntensities[index], vec4(0.0)); } highp vec4 specularLight(int index, highp vec3 normal) { if(uLightIntensities[index].a <= 0.001) { return vec4(0.0); // too dim } highp vec3 reflected = reflect(-vLightDirs[index], normal); // Check the specular texture for parameters. highp vec4 specular = texture2D(uTex, vSpecularUV); highp float shininess = max(1.0, specular.a * 7.0); highp float d = dot(vEyeDir, reflected) * shininess - max(0.0, shininess - 1.0); return max(d * uLightIntensities[index], vec4(0.0)) * vec4(specular.rgb * 2.0, 1.0); } void main(void) { highp vec3 normal = normalVector(); highp vec4 diffuse = texture2D(uTex, vUV); gl_FragColor = diffuse * (uAmbientLight + diffuseLight(0, normal) + diffuseLight(1, normal) + diffuseLight(2, normal) + diffuseLight(3, normal)); highp vec4 specular = specularLight(0, normal) + specularLight(1, normal) + specularLight(2, normal) + specularLight(3, normal); highp vec4 emission = texture2D(uTex, vEmissionUV); gl_FragColor.rgb += specular.rgb + (emission.rgb * emission.a); gl_FragColor.a = diffuse.a + specular.a + emission.a; }" } } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/shaders/vr.dei0000664000175000017500000000067312641367670031155 0ustar jaakkojaakkogroup vr { group oculusrift { shader barrel { vertex = " #version 120 attribute highp vec4 aVertex; attribute highp vec2 aUV; varying highp vec2 vTexCoord; void main() { gl_Position = aVertex; vTexCoord = aUV; }" path.fragment = "oculusrift-barrel.fsh" } } } ././@LongLink0000644000000000000000000000015300000000000011602 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/shaders/oculusrift-barrel.fshdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/shaders/oculusrift-barr0000664000175000017500000000431012641367670033101 0ustar jaakkojaakko#version 120 uniform sampler2D texture; uniform float distortionScale = 1.714; uniform vec2 screenSize = vec2(0.14976, 0.0936); uniform float lensSeparationDistance = 0.0635; uniform vec4 chromAbParam = vec4(0.996, -0.004, 1.014, 0.0); uniform vec4 hmdWarpParam = vec4(1.0, 0.220, 0.240, 0.000); varying highp vec2 vTexCoord; const float aspectRatio = 1.0; const vec2 inputCenter = vec2(0.5, 0.5); // We render center at center of unwarped image const vec2 scaleIn = vec2(2.0, 2.0/aspectRatio); void main() { vec2 scale = vec2(0.5/distortionScale, 0.5*aspectRatio/distortionScale); vec2 lensCenter = vec2(1.0 - lensSeparationDistance/screenSize.x, 0.5); // left eye // vec2 tcIn = vTexCoord; vec2 uv = vec2(tcIn.x*2, tcIn.y); // unwarped image coordinates (left eye) if (tcIn.x > 0.5) // right eye uv.x = 2 - 2*tcIn.x; vec2 theta = (uv - lensCenter) * scaleIn; float rSq = theta.x * theta.x + theta.y * theta.y; vec2 rvector = theta * ( hmdWarpParam.x + hmdWarpParam.y * rSq + hmdWarpParam.z * rSq * rSq + hmdWarpParam.w * rSq * rSq * rSq); // Chromatic aberration correction vec2 thetaBlue = rvector * (chromAbParam.z + chromAbParam.w * rSq); vec2 tcBlue = inputCenter + scale * thetaBlue; // Blue is farthest out if ( (abs(tcBlue.x - 0.5) > 0.5) || (abs(tcBlue.y - 0.5) > 0.5) ) { gl_FragColor = vec4(0, 0, 0, 1); return; } vec2 thetaRed = rvector * (chromAbParam.x + chromAbParam.y * rSq); vec2 tcRed = inputCenter + scale * thetaRed; vec2 tcGreen = inputCenter + scale * rvector; // green tcRed.x *= 0.5; // because output only goes to 0-0.5 (left eye) tcGreen.x *= 0.5; // because output only goes to 0-0.5 (left eye) tcBlue.x *= 0.5; // because output only goes to 0-0.5 (left eye) if (tcIn.x > 0.5) { // right eye 0.5-1.0 tcRed.x = 1 - tcRed.x; tcGreen.x = 1 - tcGreen.x; tcBlue.x = 1 - tcBlue.x; } float red = texture2D(texture, tcRed).r; vec2 green = texture2D(texture, tcGreen).ga; float blue = texture2D(texture, tcBlue).b; gl_FragColor = vec4(red, green.x, blue, green.y); } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/shaders.dei0000664000175000017500000000116512641367670030523 0ustar jaakkojaakko# Doomsday's Core Set of Shaders # # In each "shader" block, there can be: # - path: path to both the .vsh and .fsh files (omit extension: # "shaders/test" => shaders/test.vsh, shaders/test.fsh) # - path.vertex: path to the vertex shader file # - path.fragment: path to the fragment shader file # - vertex: source of the vertex shader # - fragment: source of the fragment shader @include @include @include @include @include @include @include doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/0000775000175000017500000000000012641367670031457 5ustar jaakkojaakko././@LongLink0000644000000000000000000000014700000000000011605 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/graphics/doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/graphic0000775000175000017500000000000012641367670033015 5ustar jaakkojaakko././@LongLink0000644000000000000000000000015700000000000011606 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/graphics/halo.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/graphic0000664000175000017500000061120012641367670033017 0ustar jaakkojaakkoPNG  IHDRDH AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  $iTXtXML:com.adobe.xmp 1 5 72 1 72 500 1 500 2013-11-23T15:11:47 Pixelmator 3.0 R0z@IDATxi%&oeɞnioґ̑iU[,wEx]fq́htrʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+rʁ+r/_kW8zZ~_;K_9OBxk#9gˁX`!1Bgehk6\krpg`W5dl%NI^Wm8p _%S@RI‡C23S2AI?;'5ʁ d浨9S$1CM}^eH,"34K׹:;E&PO {ks m-c/!@q#/|w_,s;p]*҉ 5$ub@錄F^9'r "d}?0P^"ZG^ȔR;c:UL#;Z>doC\w6wkT,dCK:k R+"__W!M,#@:ِY:ܔ` xS%]v Y.,S`j_bH ^d%t[44cdvS!gk'0#7A=F+>wy Y.G|gl.J2{G^0Fv~Okw مse2%u:W]0 T]V-i6|__ohʁ?΁~YdCaG j4(w kc$@77J%_wX,#vuC.}ҕvRBMk%@H2u-^WΫr>NFX>@jQ 7ȡ|.c֛Ed(_@P:[wIn0x:vE+r.|eB0`GC@?(A^+l2!erh@PNjbTW,CneYN۩|f厬-697A?H&ne+K,Wp!_/Qc$tjlLDdw! 2(Eʥˊ4pvQ2v|r)Ý Bt[;evjK!][1_l~ӠW~paY?@'m|dʁKq54 5ĈlXC;P6 ud#c; Lv]o㗼wKun[%4 ܑ<VlŸ픫ۯ@0%t3Y.j*u $}IRc7Se( ICd 55s _ۇ_\bKk'F!1v.ۮ5dQ. n4q۹,⛾];;nTN0då ݀) Dօ̢:_\MEIt"˘kkuW qGxhF®&@daؘ(1ED%\ ; _i5Z:z ªWi]P"C $SUݏsL۩hb;CۭrN _H4|e-]fE)su`;{'H} }MН׿>z"`"K#,baW*%(+ه[1(M1_ᒬ!LL5C[C ]]%ˀz>@pIwFY͊!Ch8Ĝx*kHEqIkT.IhRNW1M&*r.K9_]qsd  @h,.xH:^vzxBWcp2jvr:-åq"U.'緒@.Kn]esw4aMW*oW|ը|d]ИnC.FodWٍP]7Q6AW&NԷqu_ÐLL7 *kl0i[i @EXQMS>Uzy!Pu0q{\OǯM؅%>NSU4;H}6+r9׾}ZX|4@Hۡ:12ҁT4M֬ŸUxsICᗥ]е׿z㹟jPo&u!i'd0 }gA3\,0b1]iE%qMCb i&k4Rӡ|OdK?d #k[r)9ho|HcL6m駓Sc5b ಷf_-&XtXtifoĝ0:^z s "/wc[j淓 P jt7]W]Ψ@|K2mǣKM %_i >yE %ה + (H%OB}+h9.4З"py=8|m_ߑXʟBNFv:+;J4b]qY?7eʎ @xu8w]zs=G2n{@kAB Һ(4"}¯W]Ms# (|-ldz\!t ZfV*&@BZyƠ4^XkIԡȆo M=Y5 WL^lޑr Nj' c$u~R=$ J~o?;_j{]?%. oOS "]ShV--JЧt u ܥ:wO Rr8 [r`j_14U)/^YVhrd^YdA"j& WiyR HE U1sKqд @po%qE)FGb2y] .RѸdtJ~-r5,TB" 5,+>5#Cו2Wb٧8 IybH2]Ep6&5zdyFT{/9Z%&}xIk OxPW-]3H14LfkLG*V.@Ns ]zsl4 {aTZ24Z) 4P^@)I( 'PŦ hU0*ڥh4 Ȕ:>m+V+i`(6'lҗcIčj^FhC8&jG>wqۏQ`QPPkJf*egŀz"4^D Ugl {t/#'ѣj&# džW`x/v*.);q@^CeICfv,i ~:$54G b|- 64\ & &v]W 3+qP%`Du[ `:!6LGh1}XhXѩ ꝣ,|hxdj i{~4 b*ңυxZ+Q@'aqڬ=" "T% pIzRH :P$r]BC@g,_\o9\P`˘/g0Go9Hv.A^ԅ4DKN= C } Ju)4zp <"kp "h[Ҭ2d rIW Aݳp5Umn1} p7CD}_D;F)Ϗ .ϢL1Y2h]JlϚԹJ,@fgbh-qe>(}z  "de=׾ ߳h^> Tf:_猤vjjl^;= ;@GRDz4&tK֜+M xL4|a0x=}R?(S*cbx.,c\T<֩T{ziɥdN3}K";F7[=qr]?-W)o|7JK@N׈.i`AFva+@2;TAhV;peaYʼW*M]rI=)zu2HI0Zz^qyu ׈t2ySRHo`#W= U"k\-sI*–@(2mREjzS~w@py{ qqֲ=͇H͐"U0JX  q! ,"\ 4,@kH|*`LIFj*JZr'8hL!vH{tI&UDi %*Y@ 5O @cU<Ѥldz2y8obQj(pӮ@.P%_RoS{ѓgT-2ɠ}cٮ`2WiT%{Yy#BOyd݇poHWw vDGx1Ҋ;E5zp`Z=7DvLa;`F5JX/x! J\m,+dBvxDp߁uULjwh-+j "dj ~i0-`J5+4(ynSl9 ~ܫD0NrzP[R"U!KB܋ |1Um?FlK*=yw/p?Fv}7*J߫^Ħ .u[i B$ /Pr+uWLΫ Iȿ#twn7 N#5ȦB 71[MYQ0`ThuRC<7q<}ϣ]eF9^J#ϢvΫSFL Z BRm3肍2bRJ Tj0Tbڥ$7Ղ*}:w);dм rU) /\AvBTgYDc8?4j3N1@օ3%yJŋ͓j bI֥+BltSZUuHu"Dx(d|IdI˩ʰc+|,sR' dM oj>Ț#;|A8p0B\_1zA5w/⛸KVB4`w%C%J!O. TD+?Gv,t9E F oFElׇsa,`wT(P,U\Ež { w@\%x,I=jwx +wh"2[])< YDܔ(ãlXmI;jmMok=dڀ `Wpz$$8Qb-+P.^S;1\Sփf|7A3[FXS)VvL=mz]Lߞ5? N2\{=;B@LץY@p*  `J0LN@BlR%@dvdb>FN uP {ʁ bVHAaIQ\!lYS%g&fi9%'½% ٵ6S8P2aٗ*Wʋ9__t望V@zt\ny__z?y =l4B`0C|rR3F]]\c@/WB𽱩pPzZuv8覹ynO ?4r]% w3" 8K3Rhi{]Vj']n`d =aΗD|hl.  *Чl>6L5=:j=oIYsrd!{-\19̩UbwvoU=o~j6&)qm?? yX]ajTEOw瀗~>0`'MRj5;YQ=Ev!"rۗ|%4INCF XG3C.^ǿ;Gr7{7L@GQ(D~zg@MH$;U ^EI%%CmE S {U1p:ȯr)08~Z 3t!}1npM }<;OԢ-MZJ 4K6/:_,JCR4Gd0ﲚI6:=uU}'̾ѶGJ_|NR^A]hz2vSVRq"~:ut{^ߋz x=`zlTI=:I5ˁ.R-2*{n ߁:_ddIUt(M~ ј-,cL DQꫦ$Cs U`Q*<}R䙧%tGgLTUWj&K ʒc}:#g"yA|E8jhxȼAJ5sNǑ5tD)pzb}>Z.R/,}4hdu :=a=f/maYa[vJZ%V.-}bW@_/#X{h̪T4ݺ%MU\:NJTT[a#;PW+n 2T܃SU0—M$TÈJTXk0On)[H/G|i%ѪYLi*srD?!G#_J|H FބQZo9"yjyd_D'^^f8syRkIzH$hnҊ4=jt=)f}7~(P:U8*B7$N!{-FNjmwjT"=q+&: ߌWp~H.P?^@~I|8j '{06 l99wCb7<od_, CryiW3[7|48gӡ,( #tp9ۋP M$acR(4>{R9We/9Lq̩qe({2L>4;Y~Ve-|D{4Sy2řH$qOPf,=6X7&ا1a#RCl%Uᝤ_YMt2Qp$,Ey)0|{N>J Uio8ՍQQyu6uJcsہ TX Uy]~zWw}NV_bzRbTG=:l@ uN=kЏN**XIsPt<`UD)v<&SΥ))snwh"OfDY9"frE90.YXbYU'dhN&45@jVAgrMpKpxNi792RNs lZ*LtndyqE[%i˰vH;BaفzE\/\{eà[($}wc|,^p}Նq.αDRH- |!r7ݟF?F8Ὗn9KsI "!*1# 3Y΋y8ow'**o| bnmzZ)Ol6i p'7$* bA\昴$zkVyѾe)+/UܗB# z8p oTeGE?w/%ކ# N*8@!\B_+cCFmI/H60DoJJf9$&9*}6]wz1P )6nd_Pʁ`wY=(ѺL?i4'PXJ5 /NiDMHY33OOɩl?R?,6K10'jD»pmJդzZ~?_g}=a܎ϻ0^Z}=ɜQ~̻t,gfjM+|=u, K֨U"9 jyϷD_YM5 4Z򚘤8u8p!"ZqnޅIݵ;Hp=ANd_M~G _Wp 0;+]r}N,<'=Z$j4o䳽Rʊ]8fɜAԗ.@40XOdwѯZ>G9)5|r{9̢w>uj v<}L7(IؔR*e-HWπ;]uF%u0J%LZi1&Q)-I$w8~&_OOXwD>՚8=pg),qT,YI6=Ox38vԒ//|E9#k7X\%f6[~v v9,C7pଵG+L,aR!o t4~Gvx aw80`ǯowe5u?ny/pVK(b"n)r]T/bb.:lfҚw3!_F#~!=̃S~q;C{{3?ϿyNc=v n>7z d2ϣ~2i;^ᡎ ͱ`T3qpFG)( 5㖒O1" Ợfwk *KMv:gVS }.N?G 8=FG[TbA|fy/n|~2^9?>\WɜD.ד HEK8`O&3鞾>kf f>f=m>(|)B(kyESI8LδjvVwROQgٜonNT4_ B+F=Rrřޜqͺt4liA3Jw(8R#-nҹGEɬ:=DGP3AϮ8#O'0kv%ΟgHl+Rsr-ct;&$e'5|?"OVzv-zviM GLk6_:YJ'-͌17ѿ~*~i Ie㥇؞r);pJ'39K(QCOMn-STZJhkhpWxW8$ǖEex ixww񮦡QRߐ0pj)ٸb}Harl@4z, XmP%fY~Hc3AwB)Lp)bi8y׏A xqBY0ܩ"!kK ac/M f܌߿$ha#ɝH t&,7w;ϛo Ѽկy<]mSJD-2*,{@תkY՗ ^(=Jzzj"fVS '4ϘQN Z!jPN=65CoNbE6֓p>,#ލ}fbYCUb;wxK/v˒Ed/G+9}rq]O~wYc~$WL+*<q.5"(ʪZ^IЌoE=p+LE/x.oɏQTkً61܀"^d\W怷s0W~MڮmuwIn;L!nſNNxbF(Rhw@Sc^2k`+A$Kn#; >Nwc3wFf:9 ~>w?d܏?' 0e-hs{ZoEJ43Q[|\0{`n?y$ 4!\:˪9ns^H-n NlCujL e^?fṔudpw!)RݖR?U|(R?>z:mG]emYg%1eiZ.-yzѣ0~=0/狛vL! o{7bꀇ6<&XaV*?O<}6&c2{., {TB^Σ+4]1CZ&$`A6 F`x+ԡMUTk }+ _G繺wv"^==l~: E'Ǭ tqeὠNoЭH~?eJA1C;ClWQkӱ:Dô,p8x{}Rn}uϺ)x}7IPif ga6]ތnNfsx(>EiG.I* e;͌=uQZ nkdIV6 i5cڣ!e~*_r ͠ds3 Jra @T/v.GUF6qxX]y;홠9`!aNsS/Dtxڷd9f9,_};(4f?|fL96'*&[ɻfxL0˴-=\uz8 (;Z9։>2ŧjb~n?_kO8KE"/iwL2> E:~[Vx?^F]"i֣6AGWk6-[* q4h6j>}t&هJterO|&>n{l"uKb,/q|o/> ͖"?Ǚ!%狅탄 ;Y$YXfM5g/V4޳Jd(`z4ڽ<TZ'6N:Ҷ ;S`{0-"͆CƏ\].2ai-0<O|:[gpoE l*ԫ9侜آ?eb+ϋع~bKL[66; }`7m~;Mf 3F`3jXgF`饅D8H䑏3ąXGgTbSo @@Ӂr)!>SE9`]\1H;#go(;q_nV ^;aGwG~.P21Lѿ:j]$ЮpnJc6|)sb!=Vq XGx/ftz~2YMya{~X||e6oޝGR lj7kw;clf$k ٜJDe'؎98Lx/p'>C8f*o,gWyEɼ=/2L %Sj{B~P˼Jw }(Xf0?)2-kp7 h:҈VO #Ziwk_KYFBhhFAcCx mŗAr=?,hJ' lJl6AcL _oFv2k@@IDATy-fU(ԡ4[Si^F: 8bЇu'!,S4mtUA8z_I)Vekws /Cv:ECY_qaZ20 Y.KN%+E3ctٶ;!FlWj9/%=}ISd[n9]QE7:c^D b$yCO@3]:ۻf{>>Ǝ?nOV[l>]ލf̶ۓms=Z j98%2q$w}tImDg7K=q8IWC.L$ ߡy(w9֔p`8e u6;u0Kyb;}5@^i8W\HlD?jv~9X'/,2k)cK~rN3Y9kF;f&v| ݻ5} Qc:islwt5џD v⑳%vj L{ԗxta:d oJ1E;U=>o2i[f(-<+2n6.Ra {M}]rvw^X@clz4 _!B uB~wc ;.z#'Aħof'G2z!ݺa9ޣQk۪olA:(LJ؃;~frG~;Q[_%GB~(IY2Yvҋd}_VPX:0#);]c:rǂҩ(8 A儖y2Vbat y=ՉXY+@7fmJx&%.AI5UXtƋjR9ۅ">{|zx޼DC|" V~68_=T?.m^ZnVtt|>NaXfҚG-Clq:;R0 ܣ 7w߲Ԝ}`ؗe*MXI .Pr^_?skQIuh3.J+\>7lmEɚ 87IW2R3l͙fňxֱb$La&,.,p:m ^W+yUշӽ_gw^#bu513\_p ?2zc@kO8 "uӁ}|"1=u*j.X(AR# qpa~9=58?B}3@gb3[&ӛLabLV`z2l7xݙnXFLџϧoȜJ.o7f9_-6 d >G>,̎s 6ɯ03C3@<"قϚ'r@ŧɏ b:IB? 2s^@6ӱb}{0&n>(ݛɹY1gKՑ%dGd|3җ] ;Skྱ0rb$=/V`ci3 3c6f_,O(kv8g9qz÷+ t)zDfvm Ro7&&ۺEX|J)t)Ǥ{NbЩ, GlߙfΛT:,s":TU?t_J&ҷ/FM9ѹ5..d;wp{ \r Й.]v?]JxH]C%X~ɝ@jg >p04S/}س_Y.;orW3$ Zδ[:hog##:dyޟ~2&31XݞV7OO!(X9Y޿,o_7ϟ>n~ޥި2r&HGt'nhEvD.8aƄ!:(>r\UF4'$G:t.M ӾRG94Sy\&  g_NYκe{7jKmV@lwgV8\n6/x)ד:-evX͎̐ZL#lsf}^;{|ztY0ZόeoO,zc!Fw;2j;GcolN LͰ!?u oqxI肀NWȮC5W=FB1k$^Q"/ /Ro;dq z̥!WK- ¯[l|㔳-ŀ.rN_MM]fOKI|;nw6vwxl߿^ԆȔ ڄ~i7Nd2cNtHͲik7w6ƑǗz"rq;}wOgN9fPyew<[ 6pƑ W;,Pf4N2 aFwE !8TcW6" >5Tښ }}|q=SWgq+|A/Ep.;`"f(bF:.OOkz>l[Zdq~`!9`REuL9D!&XtBfYvvj4]~BȮ/@Qae'GVNj$NG14dB0kE|\w)a\ӽ\M!Ry53Α ٍʖ"(>66ٍFǛx33o/oJ4[?S~TFhf !n'{9hLO燗~o'p[{|>) ڶh:3tO 6BkU~la~:!}A#:vڎgib<[~&dF_=dE"@ B$D܌s ΄XDrwRRL0,ܩŠ`bȬj2J/M$a ;ݛՓe?w),pR/ӕ/#gVS6?~>Ss^Ύȯ̚E'CC@_ܭv?6"T{|$ n~s7]MV`丛OFaȤ7="WϑPbd)ЮiT ~;^jOb w7l07֠ÇŘr; æ1лj$M ~-_5"(ݓ5tӽ} sIْ"TbPP5zc D5+R%G\z٭ p[~7f|Mǿ8}O{n(g vs>o'GVGXQG^5 j/`ov&ݻ/g?LBݏz\Zچz 3uJh v~]/my9ߣɲkS%y{z~>?V]Mal-g'ro!!;ș)5<!C٣?aB9zsOgJO>2'sӎ I1,qp837غ֧vtuS*Zm[)/Ҷo$hM[HHbdW SF/Gax?PdO %ܱp]W7h\x9>Gw ͍_f1om7!yw~ZK}*uZhK\+:_JDM[ wRuRJ.y^p?\ݓzUw2ݟշ| @aRHn#M|gڡ 8Z.);?G7/#6t/$_?@8^Qάg'<hM 6̎>`YbyKx?Z}:ۜ/ϧOG>Π$@pn:}|dP?(=d=VtA/~9-*t}Yv ;6/M%qGl9C!u'ke~3Z*'p$w5$j0$S,|D9Yqw&Pc)};Xx}b-t-j;?snȀ5:/8eB2?ig bju>:'8 PV_f9Ƿ&JRg(};#N e$_+Shm(-G}Uob$fpu5pw,zIw:AR"ueW5TsN&B qzpcȑRN00NJpr;xOTߎ~~m_7On4xo'v^A'!)~kLLxx}9mv|y?Z=n񔻎xe[}6~Gcv{;|bwuNwJ0rfzhl0]EO =٤jZhW ܃aOeKak$H Yb_{fJ2"Rw Jmo.~5Js~ 97W! ܟvm+jP5@ؠf:Z*pOOn ݤݵaXzz5EJ_WE^p:ʀɆj-YCm=MDyx{Ụᕃ1oXOW~ s {-r5;%&?#_*/<Pѯٷ8(j0>:?{b ˘t8do xՉUx慉йGФo(3e9ƺ~u #,rr嗞r^DoitSOXb)#Wg?甃 k\w&(BƻYaa4~ȮpJyl'#׃Q(_j)ثoQ׿Ư C>/n{YWgJ]ZDݚ ܃T~,CՑ֏AÔ2EHuF l ~t?F?twev qf>N;. cVƉ_n[mXTk)O>>o7wpbyY/޿<>&<=?$}~<,vᛎ^p!aJϖR܃9C1Bl鋢^#y&HWT.NF4 %.@g' td1Qii 'n- 6Z<ftVvJ([掦扑Ulvg?_1҇}Vs)vީRT'4}֓ؓ|kpO;Z-Xf%YfNSfLtYU:l|g@(dB\3Kzzf&C%w 觳qQ~= A(&g|. 66ݕQ$Y:UG@#oUjE}6~uWo,p\@T0q 47(_{X!{ ],K&7XY:,[F($GxI)4jW7c0|2}5;~v}:YL/Oѱ8H*^۬xs~ʇl@~<=lĎͻ/@||||<:,6իf2@a>+'Rѕ[9坕{[?Um0W9vpYsa5c1#TjX s )@9& s-T:x%r* vtXeL!Htb"\~r 5u  ǣC}_Bœe,((=23=ܬ]v ֣m7 Д`GlpK0X7+^ư%&4ѹ\`;&%4kpzr~[lϟG4?Ϝ'sà;춴R֯vWZ&)dZh8w'i z^}`w>}>ڂurxhy~lNuX^=g7rΓO )jV:cNȝݳ6wc' y45hN~pfԒ/o(> &2k45ڪO7ٍ)'^Cì+EL<z u1WӋ"%Wy|絯Gv's+>ᓓ_lޓDP$?_GAuV» bFȧ Gy.DKÁ}㖟{yv_~[:=~aUms>Ys퇔?~j" F~ӻ/nVߎ7?=~nw? wZ~7}0V_ ルܾ޼m^>|C3 cjOl ,E;@&<Lqnia,Y&i C;`^fIPc2,~:Em`UI[f{v;_9A_~d$cK3B?zsyoOifݬfz؜79 e}98]7ŻwX:iA!6yޓ-'o7?vv-3 yml*|`vW3bFD/{;,T?[~n{ON7rdͰ+,Z s4M6gaODl7XԔQM TDI,Q"?t漕q1\b\ 3\vK2w^ #}p],_X x>,zUAP PốкEecNUm9?w @x/跑.93]9Lqy||c~06"?_p\ nF~mE޽u2zw8{}-F:?~ܮ>ad~]>8}nw|y?﷫ zپ<?nWv(s+NY>>QV)9%k0?lp;Q5ըx*m >Q43|ktɎyɂ!:cS<'V/J-w7 s\Y |nAIknP=<5.SirFxISFKiw7ݶ;}ye?[`t c`%.^H^#`Tc"7j1Z7@zTn@j6pw_7 \wohdo1ωzp#>z|I#u 4$חqܷMg~dz'BjPVr|=Bgwl77w~x|to?yHd w 2nU=O23Օ$~GDzȮDd{x8v9vLM5+{4^^3A|J5L9 R&Øn^gzWY muD-q4 4xo5P j98~$DA5LfabE/EٍE{ W;^iܵ ]1n"f^^]D}Ƿj5Wtmǣ{!pwԌ@Yzg{ cjUYtɛש%QQiuE0[,n8pYcGlҏxJ]yxEo2Sj?lZ1ɨ3 [Ѻ-Û'Hm-jI&(J -xF2';QyVجd-wuu#ӺpSGit*G9g޺}a+D@ߏqd vf]^)T%I /d]]#%cK'2AKx7'p|c `IhԮcj-A_^]mY^.x$]f`lEt}d9^gk&f{4ǧ#t|4pKiR*>t;mX( >y[Gp/X=;y[Oڲz]|/d'údA$jG nkft\}B3% [N,h.6nyxrɧ_MY$@;Ld^O6l}b*hmyz;.>]2>}.6O1E쟜_<ެ,wlr^RT'g#J9߾$G;"Q}L #!G>:>H/P ţ;#&vl(NG]a#ο^Lzs{(͔0@6pO)SaGpmu'pO?w|i7kdd~ؔ/ ьZhJ\~_帀@9]/7T=M2;=9amGKa7&j^Vɜrhl+p 06ϨcT,nqEQa"0-[;_v_m~Rr̊XvPɃTlBApiLO/7+ÓSzZ6e$fӝKj&yϴE9[F4A&Nhkh-cNS淘 fzO.NJҗ%näMvsXG̡9LE0D΀X((d6pJBy,{FjDOm,;; wMƈ.H^HJ`{B)Oן,Gq˄XZKlC;=ӳs`ɷGȾ~^|8'#>b݌>I-Cprso@6kcnfy:>_/V _.suo$VJW.ѝd+$*P05Ѩi&NTt3Մ*W5)2CeWȡ[Ӯ[{|Ȏu?'kDv 8m*bE{М E %VDXUlq\ 9S,[HHy߉ > VSk/Ƈ(&|7_fޕ}v 3^Nnwӏs寃/W߾},Nuǧnx5Ԫ(A?"zp{09ﳽYoȮ,2+>5W8od:cTtLR٨V84 n=xQIFȏى@=rz> L ʥ2ԹU$>w;oKz,?' K]%h]lU/Z59Cj.l1d 㺚N؈U%Sc'˟ceev)KDx'|Ɛ1#j^n&pМ"@l9؟'^US{=߹l^ۓ삻;vEF6fvh6,l v)r._OVO#"S jwx1=0|YLӋobb@!\=CݘeSNLBG`7 ?M!=_Avj̞T2 Fwǐvzk *.rouc?\{Պ 5r](C[{|G-WɥHàwCWN(KU`b{1zt fD*G-i3^^NKd۬5K?(<<}mu˩y1hn4Z^˅?>< >Rҡax~xNsfÁe:#)Z2vͩk);d9x|>l86nNL҈N+ќ(^f{`QLKܯ-kW {hUH$})R@ 8L\҈OV<|Vw/fj̲ZXjR^jv;_ NWa}z&}?x3u>a_lj>:XVo_2*BdnS -\{߹}Y3:c g1yc^Μw\(y^>N#b;%NErr°VYۑۏX:Q#Fd]B1OE~Dvyv ]Ǝ}?JF$&mAstH!>&Q>pMX֤(%#^pCbH7D'%5Zbt H֩"GKkJ쳣exb!;kbjn}4]l7sH.|oH2@f5^lM+]ǽқG(i܏7/5VQ-VY3VV\.@zY6u~' 0ASf q2MXp,Z&jt%ˇO$pbS*gh=1tz%Kfgs`WaCD(֞Ɗf>kjJM- K'I3hpg]]pI-2p9nP&6PdFW ^͇i{V z ]ݔcެqk(;iOF6s, uA@}/5xq1;]ͦ˘2 ~ބdǰڛ t"ʖ]Bsm;94;|EH o;p'5nr_L}t쭦-s} ?pu< }S]! ?*PclP*:G)Pp%˫wbz|'HNQ) />p.d~ RaC޽1*䒞7/"N-~Lja^Y<;=NWSn;NOjso'-Szw?A'd΂b6at{?X<t~;/W1IփBp,#P!3[7 q(!L''{YG^W+-)kզaShUz7ɝ "$ X/˱XHqWp2*21]<.-gJrg4`] p2qUEːUM5Eީт-f=&1|kf,-WIh8x}~qo]\qxX(*^w(qH7Ϩ)iMu˓9HĜd?v ͂x&,"'?p1|> öOAZV@vm.*8'XOUiQYԍzbz-@t`E"RfʾBgd lwL!8l*3;|'?is/\nH S4O򠤫kc{dMjD N,*Cvi{֗fKf _>r[;vx ߟO^"ws_-DY{DqHx]| | O/vp5Ҵ!!bapw7}!S3`8<| RRyceA$wy ùh0XVHXq]qbds0snbsD [l1DOORo8I)<,q,t {c I?^%<83-<62~t#bo~kj,4-;ˍ", " 3*Y7c=*-b ?0i~#܆gjq~YoL Μ$+Cg|"F 0`!ޞ7P\Z'8<|?NS_& [B2 oȞvPX~lQ>@IDAT2 R}hk:OpWNǽ6P^xoyE qNJic:^ EHyƕXf^n@-g{]R,wƄ(>N?@W6|Oq|=dRfɸ\61|ipgjFGO~f9E*-rEI.Z9BxS-z #5HJ K1USjwްߏnٜCj?I5T#OѪ={ikQ]NQD@`qH=jg{}L97Bt}6eע4A>HmF< ېj΋IY9;fJ~yiuOr=9__xOg_ƚ≷fCpBuME<'`U'o~4/uFcP0X>=2 Wo2rm njN 鹍)Mc00f\iFV:4뀩{6^(r)E)LU"H%~bLWzƦ#[8Yf(6| y&g74f/o&Ø۲Kqxv3313qGe@A˹P~pf2}֝oh fùjJg8tXZ }Drv't.p瞵s9Šӛ XAjSDJ,p Z؈c_Bym)t@ F(MWp#@srۊY uT@7$wP(=s.1`x|T1IMF1)s!MKL:OVC3RLz"ɋ`qaҕ"`ֻ1Q̤J8MXSlw-7[jWNRNl?Ό Mfib7ل+}RLI&udyqIJƨ`E`?|)pliʻ޾Kye:AP'9 5:*=ڲU_~ʹʬ >??_kAqu:x;_F>6Lw`77ĻEFѱ5__:/ YɐF* e2xvaf Dp3r{̄Q0Uh5y,\6;_2=yٞl2ƔV4cLy _otח:|;7Lf sGINj[%*sm zX@,K@qi4 ܭ]G:o)VExfGxBfi1[6Gr6gZ^rhkpzX)h+ZX*պӸI ,B$PLB31 $xPzOw8jڃxkk ͨth@CL"![eArgLoF-^owXb;6Ji}R_d~}6d_xPTO$д˓egW4ق{EvHۨ$PA o7ϲM4=~-kzRtJtgyTz75Ni81r)RF%8/endeW;q))Ԯ*Z]uz!?2.ByytzP۱wLҦ ;\"K 2"@ '/>5bf } B\WNXBEmYcouC%6s2.;X4O5X.pDθfq.th҈Ge`'M]{ /*:g֠r% } Nj[Z>T,~z;Zdj 3 f,Yzz0;PLf"SF!5m>f;vY dBUDTJ8\,+qrV[FC\ˁp,KIܰgH^4c- Bx'!Q!c~)j82S}˔f :ďu I{fmf*+Fegdelr%mofh:PI_%|7Z˞1 5Z~2:[ pr0>p ֱ9#) NʳL]&EP.v3]Zn߮6\7[_ ְ[cjFPjL%yZ;oWX >TXЅ[x@^jE!bfT jϣ2]=Z.#Vn"-G OYn~y6ZG#S&r39q|ӳ٧/׏COΆiE3+اPS~r3X~Eas{xn3#=l-C p17x!K(Oa!.&u:Yq7|Eaɗ>}~͋K"PSJآ81ؿNVfJ, Nx_Edq8:a7.iԑ|^L?=@Dԍ!$P1jDn-TT"Aq*AbrKBIq, #BDʼne̳K}>O BOCEIOBS7NjUuC4QT`t"޷w31~K–/20iVF4vF$˖NԒnއK~[t++L":;[vf(xvkX~J}6<" _SQmsˇa3ߞX 'l;p'{ FJ(DESvӲEg<h8&7+Zt*]MKQ+җ$j]o쐟M^2; y#I50Iv_CXU5UԹV [K]N`Tc|'EMZp1ud=VXߠT&Ul5Ѭ @oR(7f!pT'vnBO' 87}8蝿cq;^]ϮKS<}Jz2^Dx*CiP:ʶ8ާC8>F-}%{~k Q=hXW0Ux7䞖]R67V`N]GZgʹ5ftu55,Fw@rjwq\tʐhjJ%GvtKޫ&wfD*<^À%}219~SPho N' 6:^JG8o{$O|7_n24l¶UVvg,MXL+ OWH~,%۟e 큃o& 79Y*{V>6haoI~o|ɇ<>W#FׇrϯF% E;D2 x%"A0[dRÔ[w W&Bjl}8;D'?CϼɐriXWϔtWpv)=X(+1~|0im1jGLl'wNWF9|O0gǞgϓIhɄ\`[$x;3;FݳQh3Hř5G\]h_5f8K|u0ѷo )ҍO& tQ9p?a,2kPd_ӴR5朁"\/&>~vq$Pw@,!"%1f4+yJoVƴ_-NEfB}ayB0{Sv3U%pܗzǠO|*O~Ma.>OFy3~wA?WLNwN~|[Kɳ(T PO# WERrNt>2fq\Fq4~ -ObCe$A0:|iƮw R.wv7~b _a}d)e10ߗ3]u!c%c|I9+H#)|HkHVFIw5g!Am8+-qiBC$&h!(Q  c@98@"<#G,LDb!`YKwIgУ7URS(~N@-U|'pĩڊfnwR*; CN^RpyUIKV h7=,+ {=+w'dm}Pt xtiÃxjCgB&n ^nX Mkliޱ*{ ,1&#l/GgLEci~:{>σ$V[Y 8po}l[M^')O>~ C&kW|`?3% &A4pL])0TЯ+ O)C`eair,E)E7 T-Qf] @_{@<'ݟs9?%ѰXQ[]|+BH_ %oo4aTǠeG1[97jan2It~clDzt>bgfY)Ev b*-Ƨ&,NCYg X`6!c+UuHQ/3E<IpVFgPsd¥ Ï[hbƪ!bj4ܵh⍴חE2I8\cfaVB䷥I)q m5bzBY@y5yYMt6 Uj"qSN~?PۑCx7, &߽:;xDP^C\1)/"Wy >E|H^~;JAى-e0"{㙨^ٱdO}j1j|}}x>'Hnn FX޽z_oK^dؘ'WO7<{qgF2#S=i}w`ŗ_?\lWw߯0x֤j^ķc*^˰ أe_$ϩ4% ƚ_\颮e\w)exryX|.c EpFSiߓS| {*nR6dXn^ɄBvvOӒ\XZ\X}<17:/dK$ը̏Yme"xUObب o´Jrgl"1$PL),X9߃f_^+♵w¸wl(j[3}i͑YH 6.@HHv y@8sΓj3pI̮]. IvהF`nŝ$RR'dj ًT`ifp^#< ŧQoK''٥ ĻV P|̾ 7=$7GhZ2<Ӵa9>1]W۽S(0F&f6߯F#Z[y1D&c#_8p{w~wjg V,?zyawu=a]$22 \˓J3n Oh#s yXtPeCZ`op$1CP?cOǬS'G(,5z!'9êe`zq!V(晴[gfH@:j B/= aZ¶ϔ¯xf :pl8[S+ {"5p|,:.XvG֝ɯ4GFj^8,ps6$ML3牙-;տ=\sB D,E¼)XjAG0I%檻$eU)(` ,YGv瞪N+q%)~{]ʭC{h֣HV<%[Zcw7UR&"cwR >c}I۱fkeWknDvLO)uΞ)59@5}cO0y>4qQE#Bo}qK+Ue M "2f-el5WnoWכ3^Ro};/dafFS,_3J\ uIet6vɠ9dϰU@ٍ 7tSveA&X~V޽VNJT`;fw{;X?Y<ڠN",&Ta=&,2U,oO∀]$x`HrY4uĦ ˆ5B*EHهɇ/#뛝% 0-f=cB/n6ji\^~_W ֫__XvR2{VudBF$.dU͠XN ph?,A#SJH{RI0>==ln)S9Mˋ/~=rvv1'̺?ˀA}d^}: {gV{&.yY(g_Qd^3c(S\uS( 坏ZJ!{ %j۽MmsǷ?<'rގywc3դva׆ulo^,eni a#='$ vF0} ִͲ;Qu%J/F"6 UfLb&%u^;1kkY)L t dLy|fAx9Dݫ$ EVW |>iě;p#l{.p5z5 @rZq wׯۡ<:qp#D "=e6e(.Co*!z@;}⒟gW _"#yt w/#SrRn^GxsȮPSd*#FwR\&$٥*%P%djtzULYs[ҍK0+#\L`ad) +|¹bN'K&B*+}? ANjr\}>3;ׇ۫G4==edpՍg{1NJ/z*Hct;`i_YëPP.]S3`3*k:wې̮\9d׻*]mE&=̍JHhio[s%3U~N\i{jXenzr Lk/) 5X0lQ $vrt:z|!«YD1|g9R1Eu |ix}!t;SPwP7&G3`$YڃD 5#G9l#LۈWB0O]0U׀)cl53S&Ii4`Z8&`I %BFN 2>B:Ip Ik#BO.uL?=Tn^!C@nusҏVDWLխ:tl))W&x|i|Q̎~ E9-I$}1uZ S:NJoP^/m ?W4d^^TZQ!1dDX{+wۻ燍zJ~?N?,y|vw쏴$ _!;,Q-qf0'Uq1o`<Ɂ"hpݐ]!8Aޢ"j٩KaHX5%+Zեt99Erew8v?ֹճO']]hsb"Nv%gxyw ٨Ӈ!Lc$ P1Vdplk"zHfP 3p$ӏB|:ej*ӘdCWeDr{v|Z0ڝ$oLµ U[G~w{i%[W!}]B.IN~r@e /wɓ+$j:Fn70 8xV*5 {]m,yTp\UG#w5r=B4O/}rEn^OiD1|m#T^hZ.NVgˏ|:rON=oT$B.YĮ4G;7UujzƫH8p? 3 7)oA>G>Y[xBpw\i¤gR q_h(8o!`-Zkw= 4n#‡Q9XgK{^8C $X۲ y?\C3*9>ӭd|al%b`4eN8+.T.u~tcw[ge@t[9j+dα%wB%I%zĚ3'șV*ﹸ 2HXJ ~̖^3}e(53&8:|'O;{YEo M=yNVZk|@¢iؾV% ó/ #m믥Ci1sEOȮVF#_qݯ7;CkQ~/6ߪec!fH`.6b%)m7W6eQ[WM\{ԕC~ w;wbpa_ FMvw%:'۳c8a})>Bvuώm77[NUF M<8Ր@XB Bc@cw=I i>1f 6{jGl̓QO5ڽ4s@㳌 3ZL RP1Pn)l;|S٫‡II!eTsw4yĝcBط Kq.ڣ6Ǯ)N:i;C<5._5mUz4뀬]ڝ#(o]u]R8Fj=pYӫ,b R9f$p7o!z tO!;$fp%KЏb)d ;kZ'nvw[HffyZ~y/xIvodb {fpݷrZ͖g(E '[TUUI}{fꊊd6 y1x24!we#VT?]9Ma ]v'Mv0 H~5xG.INmwhhLj^ދ!w8"#Ýj1j~sF r㑅Y$H׬Bһ4$J߽MRWN:nGN%8ڄr?/B_NY->ه (ꩤ$jGl-K$l #_Cߪ1nM| \l^1 iOHяGJæ&QL9{\Syaq$dtu˗˓չQ%o7Ww7`S ;g`ԺASsq1yrvzB>.Ƙ+ ;8n_׻7{Fؙ )Wt&X35|i_A \ܶ䲖J–嫜Ҙde*k zu!?6ػ+X6]1qj䕆[7`JQ?)B&N~VK)vb;ݘXfgL{b. ?"qbKz ^rh>=!x$Rc  2`ؽD~E=)ې\y[gi0>wLaT@h&W#pg24\j*#w "|+X4;"G~Aakk(5DS"N}.;{}to>Wd撘rɉE'nPMgR^d%b +&ՏeTM[`hśYuOȞV IzѼGew.u9.ǬN!,r4W%]OMudCd&DUuJFGgw9@᮶*Cށ M]Ow3.Վyw9_IoBoU/W;}|w5Ү. tu|GjKẸ.4ŭz5*mJ<12[EDJmu|;U~T_'iAX'c;bHX&Wp;?}X%~1Gdp0DWjRH߅:Y;Syșs:ZCIV(v3n+R#iSJZMl“\i?e]}o)?9/C>Kouz!N2:v8,Y!3.ŢC!mL0tøDUK)|M8:uԞY'n!|%sK[ٛ4Hb|q/l0!9/+_ ;w`|소tq>էYyB3g^b~03CvmȯNG}6>0rp4S'X߆0eS$W)$ai*{|''B!Y[EՕ]c$coj;Goyqr^:()8b` ?fj2G*6ỉ̗F}̵K*#p:S\V'!t] ﱚ8Z&bwr}{:bÔ l3ư݋ݮzrA4p@jghըS ߤ#Yio5-#k a~KQLv!A Xo:r?˹xsy| qr|>g{?.[aX]t,gD:Y:W=.jd\Q/2FZn9RxIr#Afswx&3NGg:HZW7W[K[oM}>lH0N%wdb9?_DcnAmdxAlc坙)BoS2{ Lg/`Hqxq2{~%+PQr )dV{!)9tM5 )cat>#1lGَ{]Uw}QG'h0=-vF˰0"( cYT>6&Y1FRY$D?yI%?wN߀c0[K;j/w6?[anƣ!sH{Bvt`Kܫ]VӪ!^JO@%w:wKxOwCp.?;]1?s~혃}lq\w34ŭG~'sS t.i=&[@bf94+QMOwf$ĺ)kG7jgtn~ɞ 2 J"Mr5i}(P&<8|"4w~Tyd5Jz'YnJ7#!3k(HWUs5$ށ;X4\QW}UZg [A)͍i>F;ЉŎt`'I-AwyԕCG{7O/!}wn?yfQ_NyGLv.? &tdowO&p2LaI+E~]P4&Q㲵Y!w' ̵B@}j2ut!I˸/k,Դ(> 6-B>lv6"3~쏬lH鳓xS! <8]Avn,_jq m,Ɍ&Q4oӪ^o|x5&ߨ2J#̒ IwnQH{+k>юU?KD?JыwC?+HĪ,F VʃrCf]R5eց<*ztէtwQoǺjf?*fB*7din=;>Sd6Qa$pg qHw6jf!j>=7G>N[ʎ^zɬgx]yg*#[Тe+9#r1ʁ<ڌeSwv<}onZr>|<>|r}`z}4:~s7!o$0Bφ nn½Ce3*CIĦ-}2k":6Ūab +qU%y9͌kASFgG- ;W/7_ִh au40*ֳ$:|~~LZ+*}pۨE.MFc8{-.18r;d)V/&mCib{J$P{HD&Q]~M&:+qB:G( <2^$}zxN:PR89s/ Co,{;ȼgp/#kf3<;bӁu_2 o9В'{Q4D)%| 3/-sF~pw=;f10Sl*i:3#ExkC0%>k ^ɝSTZU*@!%i]I [G{c)99x|h7V6\vo2~yny ZܣղA&@PްIb*.H`#LA#d,G wˏ(o(zB1S0!p֕2'kQwoǢd(w'|/ǧmzPTmcIQ wyi4Iѐ0dRS_w;ǦiG-wa?C̲uP>*3evewdLM(1\0լ y͉-̌Z3ۉZqGs-YYkS!KaNxk"B)?|;kCu/+uF?{?昪-FHl]K: 7Sj'٢k FxID Ԍ$^ N"I밹o٩gG(ػ&ˑ]g_#rL>Lv2c?`EgFTVVL0==N' p ?x͸uIȭw|rAsoZJ)9X&G-n:ܝEDѺYƂ~_jwAmxBj!{[Fj2g91zwF:,7BzknF!^6W<¿F8lM!W5qڲy{l {]#V1v0. KT$(kNs"lФ'i D#dM¥tFȞ ̂xKpػXG /w\pP`} ͋p_hO=g_Vcv )>f1 i@VɃڒwjIbt(b]B_㻼9lࣱCw>E:ou]3*| C\?Ճ|zkzqQ S {W6QP2eչM* 4{sA.^9ePe7LR2;]|ZMZ:(hm2fx%.$M!{ldiYl߽nݏEp39߾tK~g<8r]ΌC໋zß7SK!oO6GYwt*{b3[463~9'H`3Jz.2^AF;=O<sC?[L,egp: u@T=W.uNˁ>i7uYx KQH%<*<{Nv|]2#/EfӮ8ѰdbټbzL|>R\]N| qXˌ4C><%&:&49񨧖ZGxg0Wu> qAZj *uj}+mn JqMԼNun5C|>?ş&st@ 5vJ4 M-ԗ&")%%cV t^׿ (˱sٽ-?61G9y#l|aƽ@3߸TuܤU,Y|>z TՕ]s4iz#˜ {q\ Anr̹Î1 o;pQH˖5 KvQYB6HN ّ_UXC5;-RXU?ӨhkRN(MdJQWt v߆ _ 8R f4gSycv\a2}:h5:^,=s k)(qG!=/LL8 #DvH~rĤ׸e/'!8P'R VgfЊ!3 f6 9]cr pwSZs43dqwmTr}]y}CA@n1F\*[ ȮBc&ub€{wӘ1zMrMDyLԝMfD߳W_΢8#E] &%931w/=O1d>P7GD2&,+x9,9 _]@!0rN VoA@ޭ_9|l)<P E2мa+{ xa,G޴vN?xDWKܝ dzM<N2TbTˏ0=4G.@'|Y%5##2{}NJcR 3ʝ@a4r*u^(fyQoFL 'C*B<_>ڻCu@Q쏏|<}d#"9'V|Ĝ[r6l~ՃT1 '0J0OZh#%'ׄݠњtV@u\a1 \ =Qpf`yFl V RPTv3y 4}r߁_=SO۽ĦyC lmx{% 8ָZ?m ~LJ򾵧L$P%?Eh)bv_m?0 {Lxdlw;2ME_9~=1R9M,xcun YGcBbū$X/j H?vݺ`pozspI:([wPCuDV%(f6p7W )YV.}!ȮRLͤ FxbD?zND;:&`prgnͶgH{W_U L<'Dn << +t?X&KYN8X62C#zc"A 2TzZj\̷U;v:!C~t輡[ۊBym&~M>yxaMkVMЂx ;5r2D`1w*g\߮4LU 9r2BRf֣v'4Ɏk&3%N^ejnwؚw}6p C-v]@/х:9pW׊_E|+`͊8/#!xsXTn'G:U3ْcB]KppGEBpd)<;, RtyvyIF xK(3ӧX{$zt^͛!L kSP)W}ڼnVwSmԖ kev>ݹfӭmi1;/c6q{b1/[f ̚ . ;%ZH}xt33',g`Wc~bӻw+'WA8fZOr>r>}qHh,k+H-1 &)ђ3BPG C9#uWܞF7^߳Щ(>##_7roC7 _v{@w+C}sϊ]PҪKXjYM|sݷТRa gFau{/ɬhb#P#:WT3Y,~nֳ"k׈x`yf7/93W8?6N,> 32)q,8)ژx*[x(hO\:f޺f_>Sǃ;Bu!5eWıWp+QN(F|@^2!ތj<)h<iiX"Tq9;F˘ur(*';VV%f_UwϟI׺=)4PzfZeY]9nVoٿyMEA ny~AGgi18Lg2ٌ&`~Ffnei@BvQ,jaڈRеȭj@[ۿ_?}d ٽ0|-߿[k*BwrZRy* ;k|}%ljj@ٝE>i[Yd)a,[9w>&*^šue| Mz1ʻ?ʇn{3;<PPܟEIBKiSޗN$%#(/5 ;_OK~[=UGKSR<1{H*[]Dg'``T&̲ܹ9qᅠ&n?')q0^Ɩ2fx> 3 Y g|<٭G t^{ϣׅ顋7|aL:l*CZ㿱{ a\ׅ>*r8Vx_] Cզ[5PI&' wbU쀞rnGk_]eH>Nl(,qb/L^edE˵|ۍWgղ\hfne)z̅dm 弁Ud#l"3xN n9?ddOg/#*Ҷ:ʈd%вa'XnKRy7uRg?6"o~M hw_ިV"5^o{{z^ѐ7"ZJxrKDxȚdB,}ѿ߭ëЦQQĄn,O1x8Y7g2KS8lw;9ugoO<; r.;mC$OLds˗!=^jO2»1ܚ{QS!SʝBWItasm)/A{khT_)V*313\KzF&.yF*dv/-8 CI8u,g ٧-;9 C Z^oخw?q;=K|sV6inl Zǧc >ٮ& c=Jծ ~,S,!٣ IJ=Jt&WDCa)|opX r@.8F}n^L7LӬ0uiGǸ0өֹ40N쵼-E}*>*=cQƢ<荫ZN!AT481ifP/kZt`|T y, a@F\#*{آQݩywvDz=3Z8n<c!IqBg}5C}t^!S}7 t9dw6Q5}$H-kO@`fBpkYd`$ [4.dbpu\5ǑMz O?/\ ooNd|> Y GO>2\X}sG[ƉdrT [a9!>K|[xBvTR;`p* pjpwnSIdu|~_-m 7-b Jӥ]:=45{:~^I}¥2̝H4>1fPgg,9 ;z;q^8rlDFxbmC_<g sIbK԰$mL{rL֠w!'C즜9wg(ٹnǻ@ M!\-݀|wm<%հ|>r]U*jmFQ*;^:b㑕֝X=& Cx~N)b%GfFlJjx{$ǓzF$Z(~Gf?RszJŞٱwk2yyJ}8>=f5SWz g`Nf/)}|NcZ]vw&Cg=cdzCv꩑ac8 O篯=_ jd b {ёU hӴ <@"Wj] ԬQg(.v^%0&{Yn>11FRz$}̝rc+@3fyd3SfJɞ\'04 O;*B(߁{ٰc4@ߵε*5af +VPC]Px>Mbe}GC}zH&n3{i/d@[j1<_uȔ. (oGs-nY[ 4sUb6wpk}Ӭlp,d'şbt^}Z^sEnML7tzIu}Wp` neh0T0-)AyyJbR 4\=8蔲Ŵ 6 ->2/MAbaR3})MDxo|sK1$7qXE AlҌb=Q83 ߸qZndrB&cno9=V4mC1mj®U_nTCnսeZk_>=*(;P2dg˘Ho2:;4Ε|82Bߞ8r',ߧlEx~rIL|#)uf31j5S;VT~"~wN\*re'QfGj3p=p͚ӗ㏟?~ մ7&af&!nM/|%DhԿ/W-"]ZMntw5_d^~Dy j_PlQkH3@| ҏxYKcg IEeWwccrpKtU%j07,㱝.51i>>ް_u  ߽z&I=L={1}rסd[_/{ٚvԽע@e0uy?7C `8cFambҡcQh >ؔI% CfBYEB<݌]g?S˰,-,OZ^_̈́flf58m̾Ηb^ҰN޷ٵ) fX {e=LG <$ΓI $?+Ctz#g*SpvM&bW\߾ʿJ 3]ylS}UeByk_س?}G0Xׯ<q0`.xwWԂl'q@K3[GxLg(?)!kRDxPˤ==A9 {9XMA,*,Ϡip/K:)4SzX ] TeI7I :hS'R4v(/w5tI*޼]A'1t4擺6dvVߎ":YN~vo$p ,[6ntϘ161 |6[ȾnMە̘>2qK-~]'sT;v=:ϟ_<^{ʚhc>ux"AGf4Udz! 3# Dq5?V7/ceʒrR3 >~g}b7@&]]V38/p|<a>=Y򄷚U$ rg@=KR *&17(x 5}0`㉻%0n|5\3=k%W[׿W=kCC  ;6@ {0;%jJ:H2o(w7byjFB9:Xtv~vubhsP?,K3ni[a<Z n-}RDxxm^$DK̠S|d(*;@c.Xᆖ1-i||sa&ӈ~8?0%Lz$Hw3H~ϔi QltzLͣ$ͤB;WVu>th?0~qf>[w ^e&cBSQ)K8; 4!tO&`mmIՕUbAո6[nX/d.OS?W\|X]_/D?xsF[y@ 5&~_7%;xkiXOF$+6XZtD&_SR.eP!M`}ǕG#P׺z5;0]:ѭ30u~^.Fr/,rvn.Бdii`rkˀ= b\;-#z-1>CEPp7n Gvl_HƤyr36;E;(zla jͦ1b;zu;Q@סu ؘr)cMI`#zM Xg$/q,#O_ya1"ji̳؀&Zu3rxZ'XaCI5mNɿݦNLr_ ٻ{Œ⁙+6ʗg7ʂ1}`NEL](-w,k||ǹ{Y ժlD3g_ă_'go@:%67v-j. :s4nqCH "5HjD30 3ɼ=mʁˏ(06?G"@IDAT]< ]oGnU%o aWJr %m$n%bd,c#/Wz3F\őNp=M|{ 8f4z4^ ;1D_7N9VRЛ3%=܎\e{3ގVM? khiL$*Hjpc`nfBߜQEn2prBv*޲!5[M>g0TE{E2=C)>ߗAgR@YWfV,&o<&r"c/޿ l]-??rE@e m' ؋ C. ## Gd3\.h)dOªmfHFɈY@Vj$-UvrS~Gp̯7 \6#Uzj٩!Ɍh 30@V$0̓#/kvqo\!;a;72[GL<'0p.eECżGE(>|q e<X#,,? A%{3gHe}ifr aCZ+Ynp!ްۅTm(sNkzH Kb tXIַ*hq= KDŽ*"s %j]uGy@c~* beػ 7$w'VO "WW+jjmnb&lތ.t?fguG3|5pp8B z̔[w?O.(*{ L;u`xMܢs;Gpw C^y>Wt-nAv-VJEC+}Z"[PTSkLğWֶ{4F9_g0f+19,r=͵?&3&g묗Ѷ?\+ sM-MP?53NJb1qNF$jtpL#HTBZߗ^w@~$&jW}WoiPpm%Fs$I(Mb1"3Lds{Xwme$|TXP$ca-s7P|rő*I|hY!./Ot=?|0' v$L3ZvrYqR@mي{ߕ ^z"37id.Gi7duKi E\-jf.}MHor$BDH~cݚc0ZOyyu\o>?(P-Jѝqwi>N\P? ؍z|R99'"|,%ggfMabq}aIs4q vǞ,x1*ƙ=o젉zh,\2nȎf/}r$pLa=wh}?3Kxn c̓%A/ `R2o 4n.Ӈ|{ZnrutGyDE.tYO.hyyN`Qp)v2ԼV(yZ}:+"|q7ǹsvazREHyx҅l -OO= lCviKZvr#)>$S`*&4&ڨ08! +|BcgÔḙG15ٌ7 ǿbR ?Yʗd0L޷f,O2)&m}geU,$hz|LỤX#VHh(A.kZƢJhc.S >>ƣqD !zd6M]p^P]]_:gH$*q6/7De6g{.J0ߜ,{ǝIRħݙAdՉ̎eRG'#Ohf,he C]ٳ GfǖD8=?.}XGopK S55t}vUpT1U.UbKLpatk}e$EO˜߭ij4(Bvp\1ONdfΦcK@8viz 1>n7?G ^≸0zŒ1}M]iX2Fɔfnšn$dI=Qi*:?gM5_ww4PA:\9קmѴf6Wޕ9E5(>HD'0В$GRKnݯf9is{yK=⺠S}q?L^$ hM+2aLc6T|˓na{PGeuMoGO˻Ū?"`4Og-ANe5r*J5i/iWW ήz#,HwFOsf`u-|2!DZ{k؝2_],JKXfU, zsoskk#i}I<&>]L&Q ¬JTS9*~s4qn;WIWfP.tOc{c#y}0Yy[73SF_X.)a}9$3+iMzC4_*~ǿ19^p/]M`!aA|7OiБXgKx/ra̴8]ğ p@Y] i|_맛GD}Evv[uC: OTetG=[-U@/ {8S=Hc3FWOp P#QS&/[_ɇ]mXod/{fA~:ا1C<6t)eEC`7ʦ~4֯]5&2xxs`}Oɺ%~9/]x5m>3kسw'>M|̮WӏهJoX<ʽN`ڈ'J~L_V3dhtBH"8ߗS*U6uZA81ШxKU7 {=7#:Wk[6ˇB6*@jaw{뵼(|1~O{ ^|x_;Z˔( U[dlF?#aG$u 4=3`>e] +z! .R6zbn^* !loة#)T|_:jкdϲ&qx5&4-m ݗ،'YDU]H K2q_g Fԛ#ZJ^_?_|z7~q;5w]>?&UVȐ+jqE#؂ S=cy?9V:eM@*=d8 RW$CVwؚhԅ(M^؟u Ƌ'5. ELݥ]>%S敓i+38! CIN 4{4G/mF L6Fw{Ɱ Bɝ:LK^!cNeRY:jPx@Rٷr IngFӗ_<Ұ|'g;wgztU>?#$.W n,q.Ϗ#+kM{n=f羅ɱD`y\x1ǟE0TժM5f3ES;WR!mSS {d^ujXc"[7~ծ6ђكUWLE >;ZLQd<ǻcV\qd|EA▨^Y/=q F:/dvf9lJ ?͚J{\ov|Ynb2sMYFF/1R{Zύg|g\2 ym0;q^WVx@(\߆mc֕J!FrZZAh3"4paoLR)΁eJ5f&JX/+p}=^_\Ayg@FI0bo-޽_i݌!j||{}:˯iQ=B6kokӚdxZkotl 8u ihNEle\P'3cU`}bNއ؊ rYDJ?gϘ\]]_8 qOwWCNwo?SYM<3xvoljE棌 o9=܏nGgNFqE }OsF)Ra$<2ttu|osoS#; @Beg1@sW!Gdଆ$7w7Wta9jSNCe+ZlfO3⟸٩X{xػs\4%O[z:?CGԭ8)JawhX 4pgM WPT07L1@ //Q(dk։i&aו}[FNLcmFvfkf ?;ZsbaTU8V3瑁˅zݬW:՘HY%䂤uKliIGwwVWh,xe7}\ͯWssdW>::>ۻޏۧNX#Ls<- 1le@"VӴ,5yuzF 8!_ N 5xV8T1uӢ5XcųeZ'{˚XgQ$) 9=>0R$xq(ߙx#1íuV,`A-GQwe 5r0-EuMڞ{jمq\_eSEnCU?Q5I)cޕK8jغOG4ZiHF5cƑS`3WfX7-Ge$29*{~}^'^v~^^Mg`AG)ǻ=,1G]/ΐ}3918lN?ߏnNt&</JƲW|'"!Ebu5(gm(T}$,&y `ރu+XOy ANf bLEK/._U6ǽ99 jˁk&1vzڌ#3s|~9}>ݞx7?ޞ 70G׸&pCboى-XӸ}u)Gگ_mm[t/6Z,P~{&T"KHxxrCybB^ڪʙXH'ѦM4"V'Qx̧(j'2TVƓ563yxszXL d5}nT*"M=Zt޽UK;i:s7 3)Q]Ň5[Y36xuUG}ҍٻX74[Nlfg͖WFO|<23?&`}daջ{3  2 8ؓO_7Cܑq!|dw\@5^t4~ MLG VC1دF`6MǺ?ok~WhHկףz/6q3zpqMXl.I)W@01QihFqI"EHߞy R<0#-m];=\otsX/cgޟMI&$q/Fu%GLI , _nlׅϞ]_,aq3*rwyr>.-4|7'_ojiy9>LiT7NC}w?=wrѼCy}jwuo?JP YyD{o`8嫝~&k6!UV7$NSF" ii(^me#9x^*C!z(x]]piܦ/rfsa])0Y` nh s -rixe33\MO߿}4^_o'G>f y2NEbz~76ۋj&Nvoaxs L`V6Df#Uٖ8=і/(gZ!zpYB vT-۪5q;<%xK`z]!u_n?-ڙ6waY0&yx+~b365/ђ)^T4UJ(P(`oh k)mUX\,i;Y]|KFBǻ( >l""ޣY:F{n"K3C ߏA0YYw 5 ^׼(Zr6tŐxTO$Xnhhb_*nX) H]櫱~/W^/?MN䲏>;/fvz7n't27r^=>baw#=>&==v p'j&p)ɽ ك^YMpB2=>h[E\cC KMMyK @o[6>W# eDJ;x4_;(ȭ7 LpH;z3!Zp Bcg@$xץǰw`}/ IȨLjG#kg<~r8s̄Bp)1B1ޣMka!`NTVq v3M ħ?:f$熤FK|f/ VJ^oǜ*8SV8VF Pc<ޏFW|Әt{iLAv_ڜL)W5gSRN0ry8?o7O7dve:b$փ|?fǻ$!r/z%kg-{>R2JC6Ɋ/TQwj-o bue/emwxtq&6Q6ғQl[f֓J؄<~<׏Q jH:ZJM&^؉E*~'om(f9pț$aW<6& ?40r*ehn0~eS{OttxI`Ⱦ^,8yD><=\H7;~xdp9Qǻ[z"z40$@C`=b5$Sz|"MJ5v.Mjv߉to݆|rv;& $8O+Ͷ9N6|MǛ%h0x!| '1k5XLCс}O lcqYb?m|gW:[zrhQb2'QxMĺf+./hpG{.40aw :,cxDwf˯ ߴi TˮyBdu-{7@u>bʹظx2:O <  aKhv0|m.p6CwBf/D^lfNX/e(p@lp5 F؏G>w0|<ެ[^ȋB& y"gFD5=G%:Zr`&]tB)*& 4Zno}h(T^ X/f_/)e_QyCR[@=.MԢhnOPEf8N\#1㣉Ν+𗮌H4j1"M^짨eѫe{Zb Q: hiT5 #(_>pA(A; ByHCp뼙3 X jrd+CF6ej __ɐQfbc`)Ř*4X^acv|MYEXA6>2lܝ!{Jd&a*Je>dyD8qV-"{YгݾOm(GV>rBQ+%//IM_W;o흻_5h1P5g+GǦw?tY|1Qiv5]Ncre*?͉&.XFB{m vbMEst:K$(Op^yD0N#QwE5{Չ1#"R d(a{CYm{Gdٵ9Pc?Znm[.Td0Z0x`݇, >!Y$xBYroS]{vOS?Z~sz$2E=N`Nr<ÑH5r7+ L`Ό2( ,ۇ6h,AEMk%ͻf鞍: NTwr. hvM5o᩺н9@ϻkdoaLZxRyr#$*d)[I|Fy,Vs@KBFYqqa)>OgX$D31O./|ֲ6{G$J?o!4%ifxĿ'*_#uR|FoIf¯XH\ cdn|-\ f|->ʂKTh3#423WW|ٯ\&~!ww'.͙Ó gtd^jX/Hh`Hd5Z%u~K0L{|dyK?SzjD3[ &s_to!Wc[MoFP~U?ܽnw.6xX/cװ뇠vm(JQs>\wG؈>ҽ'ㄕY;>8" |v2 yOP93SXcUY{?ݴ.W»^oCJ8׈D-t/ Qw@c?yg][q&=[ h@1I`7~?\RڋSl㉏h#W"</cy#:F*[`^g˭C ,'HpG!6YSFQ.y< 6 }z6\EXsA<שkRdCˡoz\pFnY$*F6oH6_:/ :ߵ=C@uJ͌2;5MbQ1WDuѪ'jܩSSwG󭏧,K/wwJ]_ͺ>K"^^& > iBu"_;_rpEs:\Gf*I1!CHfR  KrO5",%;#\lj(l_o慾zGW=AiW((oQ== LTB]y(w pMl*X ;z|_KD+h- à7UHӪ9yG_׿(m)М9>ƟWcs54˘.gGY>zI QFws/ډ+ۇr? zD;Ʒ u 4TlqY(>A7vz%`l -kVǎL2L/[flZZ<[/్lnVC#CoȮh<˥&P]#!_W~۞ [/=3m w3|-cIpUzxyPfR]!iq%?Oz܏Xe88y<8z̊zE;+%)/i(PsO*X]屢2^.i߻j\@mmohlÖ]Uvj\R/(7""D2Z^LȈTCx{<Ưe'!]"N/ݜwS`vӏOV b^j.c ~ $)e9X717eÕho"mtn {MxMJEx9hXr8d$w> _&p ݽU z4ۋǘt:rJ- m“CQ`%G/OQ՚Uq_J2;V0AdoעTX '5טO1yݲvzS{>v50\0qyu/~yH!R_IrS[o!ۿ{W cEZ7%>oYe ֣a )%7L=wG5%̤DR@Μ²y`}$L`/Asõ&Dr3֢sA 22uAWS.{9K)8%^{4\; ݷ֮7h7K2ogwq֛[D]z8VJDu9˩4=DyIyi^3ڍ˟u5i_7Ӕhw_4TT]o0 q&J(˒R(Db!{B0Rt!' 9 i.{i/Zaĺ>X8P_-M̖ >Gb*e6"uhlcv^&:H.5e#r g]3Q2zLnr=dȐ":\Ye[JN}n_$7]l:wf7u޻}x@ {B.P+;W} H^ASWѱ>ʣ!CSxNpA,KU6pJlOϣ[1w~~uKzy!4ZG Ri?~b6PL՛`:מg-T*xBɝRm4ap{\+!mFOuÍ3mCjd7bfχ{$ {`^pvm*X/<Y|"{aBgiH|Ğ-fSt΋s\1{.8 \:L_ g9,$8FILL+Zhp<#/ޤow:/Lmd}Px.FO|XiȳYta^6+kBy/w. [~:*`qq+ h&tn_Q{,Sv^0Y/{+>én\͆T5z() qlv wao\`K=ۣ we@Z'5gur؃.X|9Y'9`HJxY}|o7%6w>bPʷ,3)Ҩ 0Z׳zz*4Ŏ{N@aN EE(#%Xz}D#"ۀ]!9j* 4}RVp;\owWp绁?ϐJf;!as CQ#32]>lyVA'1,oϓ3'zYV( ԋlj )9yL(ٲ s1]̻aTkcl[3,y WcCaܮ-W|IoC(]0ʖJ{pD/w߯njO~:8"4UzܛKRwy;`k+X.j);Wo׷\]W e7*o/I&y}9 G.?+/lɜ2ڮVs8C咫+i=;Ov`єc}Lrey泦{w0l{n.K_CSCfGcJ O'vttOzކ^s!HN4(to7 |g CO2{p[ӆytekml#:duqQf\Nmֳ%s^q:c>#rN((L7 천 O!e3̊061gOڣF{svWU:3п 5l=@0ڇu7 p}ww{0YȖF3Ι؄)<5~ُK*\6z}is5Mz-/}LODRVZD_it?㩯7l|a2q뙙4i : {8r&loݓ?qLے{?U$|dC xBp]?^ceDRbPgTx}TfT|X:$ Bg#e!Oqu/{o;Xo)%Ytw(DRF>? Ӊ$|LDW"@tYj;L<}~&M=qz#a}:뺤ue)M< Ҳo;q QqRݑ-vEFϨE0 VU۸c/>؍G:IlgJ`osME-a|}|-` _./Op1 %㍊,8L\EAs@*::8<03<2].!:C>](.Rj'b@cZtKa m!> |-ïo(Ko< -`G|OT-䀗sw4 X FϯC-sۡZgP޺&J)n9%ק__ȧO&v{t3G/?NN볍 ,^>q+yԉMmY~)_$Aa3TWzL@IDAT[D7eH*fgL>d: E~;h骪)hGýiZlBỐKrMPdo=$r}|?\ħÇ_&{5|7k&퓝^.*s3;U8 : *;.+Y@RB'@/w`z Bt5eD I&ve݇}da?\vv[n/6ެ-3:_/fz pzxϧOd?O0)@ͅ2n՛J2Szr1^n}QYpYVq|}4B*!Ɛ5ES6&5LeM׸av1G?Dx'm{K¦L9 @|t]}|t~qqJ%{6xɬi=9S};-ͳav.𚶩[25}QD.FH'hahzFq%kh|j!VLl*]l# 0ےp^N SK){'nn~0NNd*nѦ gJTϹ!#bs]7%j+۷2{]w{5KѸ%q.63MݬSb3XZba*GuFf3#X1JI{P«}),4x}2O\ aoG.VW~"7y<_ݟ7ekwphs $PnޛuR[afdvtT7Z :_}K]wߓy:63x$7Exm na3S7Z91=3W~E뛬J:27@K=@_j(џJ+s k=z>. x{ӂRYUZCdQQ1sTeR-=nF.nM@^[v7N\k %p]nѶir.xzG,H*^!g̥Vzakӧ&al}۱._}5ۡ`[+=c~:eߚM쎗ݑw ShK M^P5Fw!^ߡy[3nO%Z8ʁzVw,O_^uWUA9U dN{EmM{zBPNŚ&qxquy^_V/W'cbG`85ּIWEt069= =䁏pZLt60=T'OPYX/o382577~@Ҥ=d!jF0_zTڠG8hHgje0&.#JgGn'ݿ-bcq̾b_[cCy{mI˧qU[l<|2@<ٷqoqZkYUKU!/Fu Ϲǎ*VO zEKpV+;K"H,e\pW8 SՂ}w<md>/R) R_ח5X_^?]?:3qfnvVGvԁ<`:3`MDuvt!nB!^ 4` ^ ᅮ<:RGw l%QXCjo\.eKk5VV !Cd)S  >/7+'ՊxSo/#="κAyMn >c%fOo4^͢}@<|uͫ /8V5nC@cӲfj|lDJ#t_fTYu z?  JM׸'_~ю'ĝJ y!鱂/r!!6ŖiMeHaKƌ#)?Qۋ0ҠC.ݬ(@F>b]#zr9AM|   0cOC`j{ax G_ ju,w9ݰ_C:(>Sp ѱKÃb 3i~ 1_O&-qfjs6.vP5gٕłߎvL]}o>+ -O7qM.G&76B ް1z%&t22"vއ߷wiG|GxK{ҽaYs kHvX$ ؼeܥ8[3W ts$2LZ?SZs0Ϡ0c&m%GD'j̃NJgpo#ŷ@;Y L$_V6h2^!݄a/yծ)<ر&oۗ}{QzmpW%x (E~e 5=W"w䞝 7L/0|IVy!FdS,"x{Za֮ۏϷo糥 +5/Ӗ+>^>v9#ܛ%s8]uB,ow!;w(5m "9 Xw^[x̱K_dQCMIRnDO{ޫM. wXMzӫ<%,kla'A|ά%-vD7_ ;5,#꟯/ʐ:Uue0 OTDr]7wwЎNA4\(]QHe=SCiTSA>,e~{[mBx :[/Ymiߗ\fk1moWS.{D;%k6Oo\/i||d:叆isvBrR4oMƸ!8Zj4M*ZR:T촏Nϖ q9]ǀ~u܇2 }x[NlLY  T]w{B WSyU^{ہ=c fr54}am!&Z3g!'w+~|{^&>v8 ';s):\˿>e|4TKD6j?^=<ׄ8Q%G9s$|҆ 6tk6wY3>.ͺnxi5oo۞ĀZ* SK`AYQP>1eVfL\9Y.XXhҲS `S0qgW8nӧsD`7>]j*?:[6ﯯZ-KlKb ED[ą҇خMg ׷coiŷATC4&o+y&Hk*֐M7a؛uXo˚8e"6Br1}5kHl5_V3W_w_NOh3&³/NJO>4T{zFu_ 40ͫLVIʗ,g 9u&wԿō-;%Ty`_gGZ/:Q=BI;.DD4ImL :l˵nކI.سeG\nqO8{;aѺ6ӖeK~#_cS.-tޯ?|z8BL{XY=Lg@}*ԪH`qZq`K˱pGW_`C @X@)uݾ`=y}C-%P#4׀Λ mˇ83~_\i3ǐOO;1{?[o Y![Jf?ٮ'ѦSNpj 0iv1Px !8bɬ Wёul&6@@%.h_'7qۅ7#4FkX[22'k¶]mDp^.v+Ok0\~j BT7m _6ۊ?~-j.ϝ/'{d/VOt| 6}LF 2QÌiz% 7{ ߧcr{&5xȁ^{y˟GYbsec,[w Y$qLZ> !7WO{NnWJ_,`5G,Ήѝt 3T߲2{>ɠ@ )ZV @VPˢ3Ef9vmV! { xGU2Kt{o~p`4}i#viЈwŇs*liZ] !3`g/aics_+l^f/9@obN]uy8$]h@?`MO=!V>4=ĸ?칚ȼ?ZtWgĬ$ϯ2qn%d)v$H =~XSƗ^vnp]+{,LWEPU UJrv;͔g Np.bǣN:Oe˫LQ49e+ C&54keeĻyH|6k ro9=pHPڼmC=`NnB ,۱JТC,}WBL3SL ͭtK#C_]?:֭kJWйg]5YT~qݕ![3,_5MDlmJlg7S[?@amW˝ANol7€e6%Cx6'TzP`Smyȩ[8.@19Ҍ.{]ǧtV'JnAm2rC8wܿχ$F(L]R6%+! YȘafov뭅NBWG/vj,W# _vMӖ^(n/f^ ~uh-ؕ:LTt9pގouÀ3TՐ: 2=+:,e=~AŃF$wswZ9v+tiZ iy}EMFT8 w`n>3FxF~kY\<ۋ}OT/ z0Ա+9rdJI~5wAa;a8=-R? @9ҞDFٺ2ЯB^zsbp^ث``T7qt#z֩a7揶Fo;1z6JuLݵ74ǿ%N{R[@=:hj6I#zt7y:+mJ{&/|'6x;lֹ"^l'XY7S(X~+ЂAcNT)eY'lw,QnMǁu97pK{WE<vBfg$H^(j L1a=eB}˘[W `eo?_ڕD#;`@Yg7jM)d-\uxc}Vx?1@6\@Uu{mlkHZJ&E9u)<1|HKMyezm>N,&[2>(z}PV0P5Ћ rr`l-^ !{=(9ѩ:X1ʆ$%^ L|JFf"*kÙ*U6PbNT^{PTmU/Wg*d: {|O-ZGUhnaIÞbg߅? úxR`2_Mۢ0+Tm Y(φ"Ҹ)6fV6ޣsuZRlJ%:9x˗v`8~~Z4'GZxq20};}]x /Vr!)YVIPNfkG йI;j SSpSw2| wB(VR:t(v?o[P^{d{36VC͘|$魬Hz[pM՞<3QsjlT(c!S% &*-KnUGGí"zg4Ns@7':4xˁT]|tua],4#?sӻ78|j;s ѻh9VlolZ1۬Bs);_)a~}:<#ߎ|%Ѫ8Xz~΂U~f,n K6b̴CKkˍa,\v"5 %Yv-ȣX ăWsg#@_(=ts+bvͳ˒O1^|eN>)^oʐ^>k2ʓسЍ0v~do};R'\W*RuK>4D[T{a(?ԙ;q|a]>bB.:!XutмgQ$܃E%w7{"'@dZdoMp 0|\vZNg0mgPq n>|(e3,k2٧MV^Ɓ8I/N}  A>aZf wsn}| SB"wa(Sw ]8}p6/߀B$V1&/6X Vu/MC1[tLB~7R3Q:j(GCp&F1=\e/Axq>YJ85}H1=Ftq>lƈ;ᖻCq=N=!ۗFit9bxgkڀ{a*Xg0Rymf`Kb}}e܂ﵓj6S@kKepY$X; C3٥&Ԁp=_G#yP:!/5?k6` Mϊ㠽So^XBFs&w:}tϠ~ߎ  ~tzs]Ƿq2 "=K-] LrP|͚"pG"7PAN7o{1C :-bzg_=+5L??!ܖgy/2u0 #81-<S(]͇ B)(d`".uȎ;p^2z஭G1P {2ɾ?cI+nFDIxn]?:FxYTG䠐}ژ M~~+v-L.muXɆfR=EtZ>kru=Js3 oqNrgAS`iΝ~KwR7Lcl™XS:(a;LЃI,eC3gfpU7\u? ~p7 "\Nd#mHв xg:^׋eLP2؅Ѡ}LP%PQҳ7*syGz5~qjֺqz2ЇT)Y{e%Dt c4Jt wzFF[.=0Px ĕ˝*Ej[p\w/f2S@/A x&g`^2m{#E}Мmnu>O=,CTT3*@1P>lL gpu=2xj_vOj6)&#p%2 GgO_RD5I.^'S2P{VH >(/-_53VWlة84ՙZnY8΁~]HL=Ȅ[k<* FU֐dqǥ;SwcY=v64Fb]Lh[qƊ渽7g>ۭ9ج(p`S<|:``/2Q 1JT8x;)xdzS⪒Zup:Rz:淃̗҅ Pb7gʤm!,dzTKO h>@vWp`npfV|u-'v#j7|%m1#Y/?c}]ۥ[>f)p ;8%On>z‰AИL{+L fl&k ܙB>Z6 2DX.;RD77fqGTG,'$빌aDkvj3 2M QP_dv7q_b8 7/ͬ7}44BeopGӾY}ZA:F/<1z!8כٕ6̕{S>*K e /k shu~8]25TN|$H-}_H4?^7ẃ{t' %<H@RZ)y;sok1^.㮷|Gl#PGK#Vt㓭89 :obHc k7*[9c^rtVShNّ5-x`:=p\k 13 ):KCQ&A'D԰Vr@:d 6fcנmLIu#f=ؼʰ7ECyݯTb۰^5LZ."~*9p~'(2ŕaBJy@cpOw9xsХAZ_)KÂ_OSkZ:&: `X/|I|^_ylNGu١~EÊv6v|So[+N5_MȭP^IZ(,9:hnmHhQqOl24=r"(?I=f]؇ǏimNV{@R̉Ve`xc_d)˔O{C]5WV8?;0f4b2:l*jh2 #*Z-"Qq H=>]băqqOąX8#rthX0ҭC#goh7-#(j\dY;R"e3Fs鶤n;1 K _Ex/w|a9O'1Bh `E~vC2\C 00!`Ĺ}nA pŊ;R2m8&N jX ^2ji.֌Qgb4Mf\Bv.pYja:Fua]L~o7@Ve*"z? so0E%n4; N8t9[Gl}/9]MG VVzއF͋whВjI ]T.X Y )OL\d0s~f׽t8QſL>>p0OmkUd1Φ&mL0rק;.bZge1k@0.Y}c0m~xD7&BћH|!uMOh``8e{V;HbV#F)>-VFEx]4f?{Ra`Րaj8c9lI#݊&@"Bk(u%ۭs RoL fJa[^@ƫ*`(|EDxĨCHJ̔76X3m矑*\> G4S Z~Bs1нdޣifZٻ#Kj@֘&d]|Ɓ4Lw/({jK5b|g.&sW0ԫ65)l8onǼ~3ӖcOǁ~7Lu_^ ֳt 1BRbѼ}FN&j_>u(d0-XIwEulzYM Xݕu 124Y>_HnA N"Uk.F<6}|p]ໟSx4<| >RQ|=@ wq6V"\\n\8ŁG<3OBLj駵fUO_pXm tf{^g*,c{UM1c_n,@YC[|Yx!+T 6OVo  mz`}JS2r9BFBg@BZ ;)sիg)×=352CY3gBWydm)&\}r'߻RFUSˮX8sۮ ܿ~Aߩg)2S@]Wil`G~3 % #Fdg)o%ӿ}gէ~mfqJlklF2U+h=;Z~FȂ jףgLwD&jfkj37WXh{pLv|'󝊆4 &T9,Nڻ]m/, =b=63\ՌmT yAv;-[I!ڤ^YOAquC؇#hʘP#=b|xx)ߐFGM<5ws3踯#~33 {wQjWeHˑh/8~1=SSܷa~B4=)Û$] 9'~z\ӳY6p/d?\rwypߞVKi95!>`eBidoOb%_ʊ!59ЬxpM+hSxxBv *c#,fڄf GR|>]>{\3BCx&Vo7Gٛw(d}gzd1b9LW4uB=*5YpW\6=[vT8~LP~u}{X{gO%Am?ȋ(=#g a'B$z̒ 븥ݮ \r0|&d&^pow8A{;Z.B _=>{1[^7njVꦻ?g$6'*S#q:@}!{| |G%L4dٺV×lp6&^e!cҪ#J%zVB KU>YHz1b9rAGGܡ4}J ݫU˷ d KTُip|d6CdUߋU>LW/)l㎿ ;d27t~"bxu qy fsy!zs0s[.ݩ݃F^^ȭl&Boau) 5wch&П^q6NCw}=>Ŧ@Ϥx zgG.bYec^RYqz$yR>Y uE6w;tKO^2k1Jػ,O[1o7{FƵcyڥgu;4Y=`z&Q~s xd@ [Xf[~L߹0`f60=qCsϲ f5q EFv#S_ogbB`uNKFwɯ+d!~ݟ{Nͤ g%7@cKrtnk4р* XmQQ.yBND(MWl0 ѱBhu56rm-VY%_ Q+v ©tW $u-9pkNbI!O:v%;Ees>HkhFː=$`5Cʛ,5tЌ =`ݧ:xΊFzfA gǐҘ6!0\ՀdK-XG"x5:wvRܗdl%F7Kw|txӧJQ )CL ݰٴW'M{LJr]s̭9Kn0<*^)r'@諩B bO0Qq+w-I`7ԇF 96F=KJtsed@˱pgx}^uIqhـx1] ~ǷT{ϔO|Nخ42yf^o)ˑWNF*8,9c?bp/ߐԝ.,`KUٹVă@k@IDATS-[T7=x:ҳG^.eH>pwL 1q8 zƦx{YֵFќ>:tbctt{y"[ -i*|`sq5}Mb{?cBB_;@߅?4R=jtw8 q.'˫Lt<WeȶL&d߱mi=Lya{.;h3 cE|fY/Nc${+q/SYzVLݯ'`=VXd 𝝲uYZFjBbСYt6pohAt9يĝXz3*,_`}|GχXB2TG^@{a;^c 7|.=+L t)0"Â?TCX"˱p7| ⓞn~5F2A|g*Sf$S揠:֐ !wTY.VY2o[%KvUe>/_ص|)eI5 p>|ɖ@,[CqbNO/`40#@  fMOFJQX4k)NYp53| (up fwfxfLfCd1,2 jg$'hƔRһ3iWȩvp}eh)t ~d7˯4~AĦ8<|\! ;"7uS&f3Lhao<ٯU/L-LdM]DN'naKvf3a5{9Z!Mi[b%8))Q>DŽs f ![?.4|`V3Nς{?` wiGKDuvY߾|YI;]=*3"B8/,u׻`;lַt;L@KcY@3Lv4G,+bʳX6&+=\^ܯڨSk[ٹ ~&>HL}1!;ӣ5CVn%8(g2GIq z Y8tDxm МɠtLBy ڝX6L6Ym/{˸zwcS@q2Yp~G7Q̐7C쇎K3C9Pj^S~ڶ܂?c|ώX|ǫO;X?oomq}ę ]9;EMO}TxZ(ֽ[tk)ٛs\?K|1Fcǁ_UC3 ,2t*+օԗ2ft@g8*Kqdif늨8@9Φ ȑ~_iXك(iT5䄮8Sb? ?_J 3 YlIܣٴ{*5J:-u \CF,tZIlD,;;Z l*O ϤgĥsLdz4#W"׬bE+6j\/ ̗ͥ*>4:| p@qPJ Mk)Gd⾩rv##ͫb ف4bY~bwh@\xdrSD>IibNtğQ~} \ N,%pXZM56!:kɡyjJ0}|,epzm{~,h+q*zuS{iIZDXIgw.k{ >!{+mW CMdX8sp`@PtgnL3O(z.Ĕ9Pы_"gKbEWFu;cN Я$LW"Bkw{ڝtN<)o,ETS9c䟯TX(  ֧!1ʚ {6lɔ SSu>Ď½㇨D ě.woG&8<{dW k⃶e='"K݇aInj?3XD ~:@>5A>64!h:WH/ ;]wIlz=A=K; Zt:VZb :Sg{hÅg%'P ݉ll@w6X>hk=&c*GV2Duj&mA%l#1L̆WmñmMd=(#gbV dޓYj 1eG~K`/#9e}A 'Xލ#VͳWf-ThR{+D]_<+B7AyM6`z'u!@v=! UPNミΜj$wR`Pxh]ЏCDϮ6wp, (!Y WATdl"7&7cAɮC,K 2g@e{Sd^Z6, 1z-cn̔XC]{`eSسΝM#njVkEen+Dҟա0R҆(Ef"IҀ>\IQ -!д0 %Ov2m G0=}S3,dzd* yjB=  o*ǡ`lGa+qtC tp+Ww:ZN6m9=F,Tm:[+bU(Qˌ60gHu]?zʁthZEA().4k]=qU5PkZ6<侸ҌijA$zf;I@yj?3nKkFrge1 ' K<'G-ʛU/Zdiw?fX >0`=:9#h>R acF2b8x+$-񜲳.uMiTr8. C }\  p.un#"!1eFctuOT)h- GK s5JP:@]X|P]N}g?qr]DPW3@(Dv~fj&ilMLYSrj}ɟ $D !uUx6ѿ1u)Kp@zX;.Oaq I.+R[^EAKۃ  Iv u`2vژ6 y㹊7l2@]Ħ~AvC9ȜZ '&2baA> }ݑwɹ mxU> %e@=+`eLM3NkApϝ=Ȑ;y ܇{Ψbjk FS\J'aiN h'nwKKJƚ~.gIr1Riuo~"1wEH<tEf77 ˱5sjϑAfISa*q pw}B&}I7ipNu)='q!eX1Y:Tmߜ7ۀ)b;7|@mo;@͠/즣!$b#'#ѩo 6FiV. ]7fJ~t 4W7)p:bKEFaW!{{ӭK~\fP~Mkj o7޻et7qG 5US! C'<&__I| >HZNp?J}7LR:tz;y+qnDݸFrYAyz>O[u֧u[sN܏pvٗ#,ɒ,4TbGlh<6%xa,ROl獒ng\eH71B=7|E4F# }V /it'8(Mhtԏ(.N[{o,"@xzt4KB*{+gX<΅L:kեtOƁ׻^_JLctqO-7:^(dY Ѝ.bi9`&xQ(.+/VMqs=n۾&x&k׬TVb>{< :4y0!6J6zxq̕x] KpOVBt5f}ւRjRP/յkZ&6suLvjژX%6q~h|ܑaz(O)}k}fUn!J4!?U-9୾&^>~ᥠ>t}I:iĉ0 =:hт>>`&W\A3:#oD)}̠R3Pob佯7ȏV |Z-8m_{B׀f =CNWEW. KI}Z9=/ O86Ӭӯ؎`eLq KwG..l(|ZC%.Y3R6@;Tazvbջ,x5`HpF{[~]hEl8Mi]{+SxDs<4P0)N}~SMw}Dd5۠\h8޷k78:2Xmi>S#cO~U1^~u47ꀐަi f?Bć7'}lh^~.LM9>3v}9JוAC$azdC#fGFD/5.%M׾w@M|mt8/th^nC$Fv2H :F\JI5"Ձ] ~Dx@FQeHYf:}GLZv?؄s{xx>!a|ke*D"/I2H~ =NHWMXB /ْ:HC˙˭!]} փCfHB'vUrћlvG<38`&\wSoqJOԄLk"Y {|JV}b.>s~>mTr Ƈ{ǧvHLf}9&(J3@{~)je>*Nax́ݧ}t\rTnI)8 OÁ< D,tdfh5SvX+:/{8lW̽]V9Fö1 Yc䴤|JѦ>AXSD Sd/}haK4?{&WkhCV ;шЧa8KTҝ2>(k~\&eD?Mnpx(OCNC%M[ ;cqw=P m=G[ncrٵ/n->V@vأA%I/P0UJl$D`U8^Y&;!X8r`)$xHШgUKiD"09wny}o~ݯRYN~gjXVHֲZ`OS&JS_Ԕpgird Dyy{: ٝgY RԤҤLj`:xeҘ(Eʦ'sm/RNg&YtY%ak2ep;|{U O!&(odtEB}!RLE/K~+:y ~h nVNiXO7!=!"6UnsyR#&VR}9]3زMt[ﶇ-p6}~E?l+lq{Pʠn&ycEI:+GA;QP.5f LAqڴC-p;Œg=@wNo2]K ݸ)R9aą:t"kkm~,%3Ց1:ڰWy.˽kGm@gDdhʽ{G5҆Z "Qєr(8>]pxPe 7bW;,Q?Bܡ^zHN#9SJNrPNFp}>fh-U!6oGp;"=;Ol79 "Еw VIy4^R{u3ZNMuS@H0~}ğw K<~_bp꣱TO L9 R6-_Kr5Sۤ|hVx+4LƑ Wܢc1<+ yk^>OMkƢ|P(Uwt\Z["yWZǘ쑒.Mtd׃ )$`<ޘ.! :Z`[4|w2jܘb\f`WtܑNo L55uV-}jB25drtTy"%Wt_,(w(_T5c]yW4א H`8i`!4!Ә^q {3Ap~t,spa=۹uc=Fo ٖ<5cQU{WGhRyKdjȩ;8jryY?[IKp'@W#E*pOѵ(XOlLDZ0Nzпx ("a)C|덯qIkčZXˊ%pO,:# h/P0 cQ~3BMLiv4ЃU2$ܻ(DG魁*c$vz[Lw%k4  1.HW#(ƑۇWsи73GNP8^)ڭ>~L55l0ݿ<)mȡ b-D%c* krx-90gtQǕ|:,|F`I1>cSKa}SK΢H:f|֘fb4 uGqWc(Dpo|,2g8P'Y<%O\fGgw=>JSHCFbL.qFA5x,N( ׎-Qv{ۆ-hNR{Ư#>2"E-@fmekv3#1)U1_ُ`RƶEO=ˁHҥF${~>ȕ2cepS4r}jK~M;zvpoFy@,?\XW@T)Zk6poOV>-n S.Cń-_%LY)ЦS:1=zg{dOxٞP-sl>c~>j lbՕͶ^ uZM Il-W )XK2:ŋM/Uiw~u mFyH+}0(=F$ĿyB3FFDM=M9li)jXf$κkуle5.I$*0@ +D񘕫Sֱ@;Š]@6}Uo?OU&=!R[y@l|i~L"3GckeD;4'GA[lWg@C+FuO˄G>ơ-󨻋&nyu*vq&l/BfpyM8>>=^N/gp:* m[WsKp'〾5"Nw;?Il F:`/^p~?!$ U1/Spbba֥BEg1^g[37叀~ut!S9gd ͻaJV^U ྥ;]>˛izۡ -vtH5({Gx {A::~Ͻ^8sq_(nD k;mf6whDiAݴ#p>L:`k=Zv2 YhaDy|d3lPL&2g# >3wLA6[m) kvH#4BC<J BN(/CJp}I 4j F.[ DIIӈ;򬋤Z5]mk#ϸc=34kTQ͙ B 8?ވ=J()4j#p/Tx /C^Yi^ Si`6aqMkCHܴ:=LԠi&~F\LwIJ"5Ӊæ~[wNQI׿W |9;;vSo;(H#G~cl:٘Enxf ^y/.Z R}p.vQE&ZGv3ēC%* 9EM"a#CDxĨk@;erBYuSN ? r~OS(O4&@'SuvS9,. b`] ADn<#L@DZ0=)͹3> 9=%b{oF@'AUWMs±BG:%ж ~%eeq !8դ7[8P0l4@4pWpLw}bhhjStE׭Ykt71e`uY[jS0+ {~b[ӭ{T4QdNUDCcn/c=SA& 9v3Ķ4a_l90?&ɗ;N[j|}.RP: 43e^K3Ki>B4o&xkׯXr5hus_`7_͞΃khfzep[:8/2[R`$erӁKr/ UtmhcgNE rzu@ZoۺpI]S]Jgt!HVc /gҜ40$y;iٹă2<-ΘDs9.o dqkJS<5qρj4.MS$= $h8H4lH!M<{v5tLj6oH߳y_$=uYg0HiqW!mLH%$ D,pwYaY& wû20;4ݩ#z57L)!t!F ѿ'HsN23dpW#i:K#'iZGqC[CF:  kPkCz4qR1AعڝnZ܏ + jSdv@{e;:i)@YI('<q"\W r>Umvg! KkG,K?܎.ˊXF:#Cc~5Ո1@5v?b9?H ڍ|o n gN6C~CX5.85FOmF[ڶiN%ScIn7ߨ%NML=3(EAh4~C^238)84#,Cew1~CaN_~'|w']x']+/׵Y~(E0a] rY99^h[fiu&P1jй5gKsa(dG,h*:=nZR8L-;XK6sAvԩt}&mݓ*Wc"Y٩ҹR;wN9$@E6$"rAvP^v &ɢl-_Rk:5cqAʮ^K['܅УX7e"gnXmj[UFT,l0Pd&h~4xC6=$B= -[2sp:x2^!k1U&&M ;aOm)?^/H]h ̱.!}:]h.W,ѵ!y)MɟL BoPB=gxR^/7*V\b rt;Rɗ 6*rVˡ%*b%˩ӡ SCc{])Sٛtun'LNf/- @vX#_ Jq_ˮhdM-嶀\hn}Q׳>ӡpw'kly&:n&=l>!:u8Jvk8"L6+lZxM C @ya pEd$)4ë2v|n\hy sa`pbmX+: ;32xeoqL`uL.bd;?1Is.^QtRLd.omx{dwXy"IXM>R)47!³rQu}n)'| Av&CG9c KF ]_,XcߠKEBlG`iݑ8Cp8ccۑ8F~`.Cx3O2ﱵ谍whݾRP YQ+%15}e&@%_h"'^1|1XEJ. O郉U0I1  W({H74HTx JYˈLl 7=^ ҧ/ [{X.ٯA"| ҶA,kc2$˜2P,da ÀIL QZQ@?<9C'%i\f]W |79E{ar2'~/:7H"Jʛ1 +bT3my;:͖w d<0G`Wͱe&;I |C~~݅n!WaW[u{t1LkV !;;_@IDAT.s^բsNRVmU8&61 ټa]Ȱv78C/g!m)X=J16!5)w*T7w,Y4n1C,>L; GQ>K/{w'7)N=驝O3aesoʼ,vtVHU@t .K'#fN6Xh4ݪ`" }KPupʔͦ{*B:%/nӓJp;2>,j Z%Tףܩp ؗ"OYz뫻mosis'7C)w(rUxtIg& kqdG6@K)RQĩ$ \09xaN.aMB:rW} &OBvbvsQr31 'WpR2/]+ Р9h6Yh>I?g#e0 ]9f)5.aMmd̸U=ɋ?2r#|$H/{%a/>W hxQJlU>7]QFz)^+lMJO{6R?E@>__| IK7p=8d?c"@gɅo9Y}9 9sA/쳤f(]iުнx# % |6f7 lL=hػSCe6L xp" cBڗoB?:R kH Dp@<~8`7p| &H{p u]3 *DUTV3IGxFeHۦɄ~k5B~^(`B)&gXτih`~Rܺ̚*RR&ϲaC8h_!&i` HvK'KjЙOc 1vGBxx7mxּȭaJw|фwJ w-q ~ @oW{ %_R7bpq10"ԃg]5/UhWaeQQ;I甊(6YRfZr-[c{RE:9^&YDϢ=̬/$wٲKf}T֡D؉ig ۦCmǧa6&#Xe"edV,@~1įC3AAJ!6ݪwY M>qp 7\&κaڄГ* \+ ZdɳNcgHEڧtנ]*S*`^j'`.E< jtd+&[_nݍׂm+i嫔3&;ɆP>ݮrhġN)}k7Ww_"պG)9)"I4vvpȧnMlb pnGw]4bet$Ç|nȾ(@t>%XgIf6r,U嗀YC=IξFuy=?//<Sdi#E%4 lĴɾC)Lh̊FZ8H\'UI9]+UpbO8zL'ԔOc "<4l [vs+LVB _MR$s͟!Y+oJnЀ;[c63w?t3G7+.ҼZAFudryrJ)h?/.&F >j) 5.IK {h{@|%G,pd;`[ N,q+0%xth= K7_)\aDhbqb>d;!f u F&K <6lyO[Sj"3C ˯$8~J C/Ћk 5%܇Hzi( bXjUC[fcRKOYZ,dg ;{2Y-@\}QͲ 3( LB/ 6OO7z|cרoo5k=l2c1/5-*ç?}t=:7@Y)1W |'5)y}?HF$HL'v9e/n{F8b 7HmKQ#'r 2u8+>#~x t&Z~_Wwi 9ȔztМ7 1:9ĺSbt~ PDAW ӗT'HGO=%TC@a!_PIzCigl*~|>|/[2"ƥ;>Oy , l,.O$bVc-ןW"3S^G]/q{v n&BXi-3#<8>T 1DZgϖXc!tC0g?}Q/ | W,XpbdROZ69xeJnhh19#]jnh#~δ Mjj e y2 $3cx3w+ MJa0٬_cߵqĈT<8Փn.p ?gʃ=a4&m{O\ 7˃`E1e57gCv6V {X b1tA8%w̓hE2 ;/2pFڃ_ǩF23`1C)4z"UnXچ\oxn0C{ECƒc5RS~#L>%<~@6Lkt~Vt86Daϸk0:Tl bt $ME6<8-bi(P^vLfL7`yFGu* ҙ#߳ Xjw[}z'Ŕ0ʼBZ%ocj;LW# !` m0|fȾDWF@|"H|e vˍ21*6?BXTQhgV/nxX?/|>6ǶꚊNZz1WBkݪs*Y&8*`9jL_ bx O??94xy"Ä?!8@6ei崁N<~8g׿W |[ ow2a.7hm,xN!Nml^Ȁ]m[cw2{9ZLz$͌df(ٙ5d7 {3=K $9&aOH7Zщ bpjC 'ȮCY3ܟ2ްyn W^>p.) Le! ߖ0_s"V~3=1XW E,H‰ހٞjrY^<m]w5Oؿq8O|1c79/cL Y![}F}=l>ǰO)RL_h%U Cd:`}.X̉g\J!'b[5RRl Xr/E^48A&5FVSLbxHxѦЋt<#{,2lЁ_u|'f`>)H-1s)yͨn [(}= n]2|My=1^(M^S.I;kxf" 4 #KfFsX,alOa>P5 a:īCrb1sgTA+ @Nj (8] Jץm>t7z#69X LwAOp󨿖 u#o'+!H4Eh@}ӶqՋHOXͤ ; r^҂yS3Nb d~6IM7ݰۧ~yP=hqI_n7N Nbu+ozwф4^NbܠnsyqXJ zz`m? CtXG6`S?^>O#6r+Q==}a)zw 528t= 32J3LGox:uT8!!(DoN+^SΖ.9Ҕi֤π.dg=U"jʛί] $m;0ZzxQ)M

B~)W5's) U{Y0 <̣xL=m8\ 뗖F߫NHeU̠\R0LC7{Oگ0y?E8G'Tހ[ҔWsپJk u:W \WSAVڧߧ ~RwNHD&.Xֈ&Fpib&A ƍ7sT|@:$ ۠zFghAF6H#Dޣ9҂O]h*"3)%q:;gF0hQXdSUoED۾' CׁA'sQ.]*s/ SY nD*Z{D7"op: O ?(ģ'cc;9of[A)2 ϚTŊ y@+s./=Q*>)r7_rԶ( d|%$gQ|{2{ ߗZ*v2$[DK >g6W$MhN#)@e''=EgCH O&9G"a#~4U.#%sW=fV~vQn} i d'}^!M0'1:yr)Pԥ,y((5Hs6wD7tz< ETE'|7 fNR(BRiGt]IY𙈑$ [#*+}.Я?#n |od{K GOB|"Z^i%@7!:KyG1)g"@2m6zE0` " wR_?(~D([ӈ?_'/b׿i/?wst hޡ[cM+j<6kDlk"zRaIcD*DQ1`rs$Q<҅bx"#yj)DҁF;"y> pՁ0 8VAMPrV`An)|a@ E}//J/HS7?T@E':ß* ~֑FCr!S F4%5(8 Lh>e1pU~V)Ku9B]o_Ry@{  "c)h;X!hD06ٌS@9nd(p% CwJ `C=>O89_Gqv*dfQ)RD1ȩA(ߪ,qk! 69ܿR}0pA"cN>"pdp*ҟh|*23IH:2z&& yXGk &nf`FAPDž7yZj@*#;"gC$#&4|^Ў#E=3"ޚv}*؏dˉRPsO;'f 0OJz6~pIDAT"v[ QƎ|qyP6 Ȳf9t6"W NlrدOP:00Ĺ~PH4<M 4)G 0M|Ww:p 8ER07߄&SO`OC'p>׃ ? 05DEs-j*"=KA4ex':j̟"0r/f}(DoPuԐ_c? DBI1hgo:p$z1H~rR +)W"V3}t/D.GLAHU6l 94~#ҁj@ħ[|k@֏0:xس_/aoP ݠ@S]ȧ7>U}ؘ4r ޟ/R' q^x˨Č>BTX[jE~th mUڼu|I_3G/XA^jMWVjܟO! ɞ-ۤ@T[ 9XԇT:%H Q7sEP&/.ƇhLQQ_1O$ĩ'X鯥vNl4f6q?2"k.~hOИG~o;7^uUy$@R_}>E{0S!D f(& = DxO p,) bKW4d(c^ ߺ_Q'/w9?(Y-bm }b/HWFVS'>o(Exh=9L.N$giP9n5EbH#jzQV.%@S~]_}}@|i8 ҅]9E{k@(.6`PaF)2( \@1=Ks|V<Bz*B^`- VmM"4Jn2y22.98Ru~-Q @ J?L 8 ) M<@yO3LC حm2x4r/] "=~JA` :m8~aT5QE?@OM(J:p<kZO=}*۴4B绲@3"@ru-xeF.#"}?b H~)jS݌aA )օJ*ar+.=%R nF |_d #|oEf!8mA%%ZSbY&iBQzL@?<PS> m(B|Xk)e@ߠRjF$,R龧p5?')H!س & ZP1܈R x"\ȧ%uuٯH6/,VHz3^#.-'PqO%>WE)`RY|J!h`K0_`B*hr 3F1|"%Հ>8&dI(:4f4<^o 6Y*""95k l]} > s` C<1De"H~0Hf)xR)5o DͿf3[]b &"TOc~08\sOCrit])gV> $yo ޮ H, :@_ZM9$d8%9@ kZ %amPRZZ`%`E%hǃ!u),V>[,q~o;qW R@^_wy~!\ j՛"HSлkngߒRxgf ['M'; _9I D0gT~Vӌ+TQ uꑌCfdNj f \B*os >jXM69e`7,*s G`vU%xf F0~0(j\j$NPWYo$H<ֹ~^ Q0#LD\x-Sz-y<X?(\ۼki3Gh n #PjE*_EZ$B`![oPoUa?(LV\/"W &h^-8v{wyrr h Q<;J`$эd>E5# Ho_dp|np<7 1}Ux 5.DDof{ Pϟ[2 -QZ-vӁ#^Y)᧵%"`dFܢ D^gTԂ\3s F{i5gZ X%.MLm?i`X"u5%Y+G̷Ÿ%ﹼ~͌PTߡ/)h,i5 -@R/IXs|OKZg-<ƳO:F*s,K#K2׫2ײ(f_F{vtnz1#?CD>e>'k@SHJq2y2."ІƤOG? ޣ 2{FdQߪ Έmy]5b/]c)ǍoQHylU^%-[y 0'Ŏ<>W"&X[:%y)txH:4R3:Qie]Qh@M.&Y$2%zFS,ϊ#ϔ~+0j F¨R|OIADі-ZxےLr|s:5GL@2"[G `4EW"Xr!L/[!d3Ku=_EW@0y7 x^_ .w̵Y9ljXE:c~o%>Q Ѳb++7$ 3=S෼&YֺxYkeFYSM,ˆk7uiւkA0YJ*zDӁlO -6ߕ@-]&f%t;I(#FuwU "Y=jntbH6$3H;6˽L[>rf."LAJ"ߏx35L 2҂Tmd@6brPwF6H Y$N*By SqTGg)oAXEo/ߝfFl=DF#weu0-H`K̐+Izȱ+S a3W%h2H{ K{ 3o'd# hF@lfHHߕ\@'Va@?GӀ? O!U)_4pDt/3 6 "qc3s@63Ay&Br>s!kr#eTnLʄE mfWH@3 2p-VXIQI_n٩Aׂȝ\WI^wPT`/SVy.C7m"!sMy}$QٿJlox"Bk`ĐY`&D:{#moNN3MhI& CC_&lod@JM m`X>߬m!w3@ אAx}~@/Rm&m=%m\Tn>=ـ `oU»mp m䰷/ `o{&momFcIENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/mouse.png0000664000175000017500000001134212641367670032742 0ustar jaakkojaakkoPNG  IHDR @{u :iCCPPhotoshop ICC profilexwTTϽwz0)C 7Da`(34!EDA"""` `QQy3Vt彗g}k=g}ֺtX 4Jc `23B=ÀH>nL"7w+7tI؂dPĩق }F1(1E";cX| v[="ޚ%qQ-["LqEVaf"+IĦ"&BD)+Rn|nbң2ޜT@`d0l[zZ ?KF\[fFf_nM{H? }_z=YQmv|c34 )[W%I Ȱ316rX7(ݝ ⺱SӅ|zfšyq_0sxpєqyv\7GSa؟8"Q>j1>s@7|8ՉŹ,߳e%9-$H*P*@#`l=p0VHiA>@ vjP @h'@8 .:n``a!2D UH 2!y@PAB&*: :]B=h~L2 p"΃ p\ u6<?g! DCJiA^&2L#PEGQި(j5jU:jGnFQ3Oh2Z mC#щlt݈nC_BF`0FcDa1k0Vy f 3bXl `{ǰCq[3yq<\ww7Zx;| ŗ]8~ M!8Ʉ*B !HT'\b8 q$C'bHBvay=+2Mv&G&Ec[ [bDDĐ I* Zc0&8(&iYH~Ho(%46h0װu wKDŽ7EGGDDōFG7FϮX{xULQ̝:+sV^]*uՙXXf8t\DѸ@f=s6'~_ ˍ̮`Oq8圉D]SINII\7n5ewrm\J`ᔅԈ4\Z\) /ד>aQ1n3|?~c&2S@L uYY5YoóOHrrsNy};_-cZuuk/\?kÑ)*0-(/x)bSWr±^$E[nEmnfmOk%%%JY׾1ꛅ ˬir]+wZiYYGgʿs{?T'U߮qiݧo۾C*זԾ?=xΫ^P֡ 2mjTl,ixwxHȑ&JG˚faԱc7sŨZr}wN>8(mP{nLGRHgT)S]]m?x3g]8wn| ƺc\x'ߥ+=/_u=wvWO]c\n}Ϫ'l:o\:xviMoܺ~{;˾;y/Ylx~XHQc?:b=rf}Icda)iDӤ)ϩV<|~W_}oοDΌ\«ï-_w>~f~#zGPQc'O6gAMA|Q cHRMz%u0`:o'IDATxb diip]#v0220R 0 oCb0CTT@G:"^ &P<???deeG8 ///իWRhG#Ξ= rl#ri´ .^Ƞ? & 3};$ @,߾} /_-/JGxt r  ]\\ Rr #be-X8AC&L`8rb`2!8 9@>%a 9Xq0qg@}@a[@  x]+zA jA511a*/@A@U>,A =,\p:9ݻw`yP<1'1(/ 17 bj;u <޸q!)) 8('*CXXfA ='ԁ,Py &\J 9re{ܻ;vxj.((w ĿaDD3Cp: 8A@`|3P!ЛSN7@!J Pi3@GH9x-׆ ϝ;ǐ` PvXs :77D_ ~I ?POۃ2,7G .jjj  r@+T/Ȝ@?(-/%(2@! :Ç @HF`Gbz# T/>B}/Xl9(˂r 2JI]!J  @a*5dcfoC18wipaz p;._#bAq^-]5sLˁ 1B@1B}*Ʉ4A t!ǏAӧOAP-ub@ bHؠ|*^/]^an ,@3 J?@BTs0@Ov8, J9V:Plƭ>|08;TNy ӑ "4D`EDfPd9ȎkJ'X !&./!J P@Ҁ,l%K744r$+<"-1 FDm\faP 3@l<q1 r[hhj@?h} )P:b 2C (/ P%\Pŗ X J_>Yu(i@ ,X jdDx@PN`5  RB5@ Z*: Hr4`mP40B-bZ+Q0"5@ P ~#QA(99 0!!DE@1Yij 0%>p#,@d9!(j"P5@ h@pЀ; 4w@ h@pЀ; &eDIENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/fold.png0000664000175000017500000000726412641367670032546 0ustar jaakkojaakkoPNG  IHDR@@iq AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  iTXtXML:com.adobe.xmp 5 1 2 O8IDATxINA9 w"(.< 5py6pk{ G&MwSUM飻.d" " " " " " " " " " "@=@h6C9P%ǰC"K;Jr-sd"&ewvx5E\QF. OF bwZ *2ެ`w__]$Eow(璘{ =&zU1Gռj!VXoWLq(d?Z87~ mg_%'7;|@z3&55͠lZ>q6Gt=P_̀AwS=h3iQ=zKⰃฝ{7. >-4e#gӌw.ؙ"{s[y`K̸J;TFVPɵ=du* _`l&s7 +!{u34:&Q]ރ`Rhqh BSHEWD@D@D@D@D@D@D@D@D@DKtB)`OIENDB`././@LongLink0000644000000000000000000000015400000000000011603 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/game-libhexen.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/game-libhe0000664000175000017500000001752012641367670033025 0ustar jaakkojaakkoPNG  IHDR22?gAMA|Q AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  iTXtXML:com.adobe.xmp 1 5 72 1 72 50 1 50 2013-07-16T20:07:66 Pixelmator 2.2 IDAThřyl\'cq !%mhZ@@JZ!(T((!ЋU BPh8DAHB.QLH rcwyz^'RՑfgo~6d2%e>Xb(Sdž-[O'&4.K&M J6& ?qX̷!!((g,e ӍahoǢ #}cB$7N 0688XPYYOX,+0 АbWh P ⯪Fl %GQ ! NcH$AמB$ۜ)0^)@y@C?*c\X J20%%%"` a-{L2 `xܞ1ົSBiMk_֦ 񐒽ٳ~ɲ;zh0vX^kxay!SX+;gNa3mUϚW id]r71x n޵B+?'3++li@ `.--ilZX)]"Z. ЪܐR}c^&bZr9ɨxJ^ )`<(3Vu?o;ݵ;v\?bICVHd1@>Д"p-'խ__ǡ(m5ͳzYQ_opw\}6 @Ҏe~ަn )>WN?~u*eIj* d)ctgyhǬY6úER kyx9}kD0b@燲L^t.soWחr)Mh#ۆ0O4fĎ$-ȩmw=y&_[S괞L&P}2x %ENL 1E@GA}HέIESZ bY᤯`+k"qٳ8b~HkeCFH@ e9+|+>nڹ$w2wO_ crܶX͜3/o*Iw{.qT1Oܱ@ r slbC2d^|G_[94nNνrE~`cRS2Ws0}ΜB{Um[m5(fͭm jݪɢa`߾}v ›Pf!fv֒ܢOD=C@9g.d9b7 ixKiG+Z+"wdYHQ"14mG6eIW44,&7gSW5% !I(,KXȬL0\0vg '=ⷒق]"0Is"B={K=3jSeneϠ{_9v3RB Usy ,,,~Jpv~3G\QFĈriUM2ȐFX;~/m^8fBѾ:Q(z1|F=%do!fc@p+ӬO:CtьC08æz@}-[[)h(PQ]S.;!@w 1Uaόd"QڰXڮ8щ-WB rA_Wz]o4/}eyb@c4{Q7w׿u/`PiXaX!.M@YC8=hK[CޭtmW{˂30DNz 럾ˊ^xalmfhR 6=pGX ?]˘@[]=YD(3XLeBr+}X[hMokA5 Z@Lo9,5M!-׿%D)U<"Ysw1 (Pá 6ב@@݇t}7kȥ-8U/%3]MlI[=wMIwwY4_x]:8v f,0k.0$Qѳ/˗HfNl]2Pe3]u֭vS8m :ڃ%,rg/Jݺ->y~C͆c 8+< Y%ZޭDsN'BFK.(>d bD (8жm53X{2=R?]00>.t/7}ٜIoUG;q3n[4&\;>R xS/t0F[)?s9- w^#P==m{Xtu _Q|õф)Q*Jn}w~JV^^ %"򣲈mP|WD1W*6n֬{'C'լ>teܖB~;QAt'|V'YpuU ->m|z3FVg=!.y-Ya3ılR&ЩoΚuyeUjoyZqR/z?? j#{oXrʛ靤{ U\O6 :ݮ=3u <_ Ү4G:hnE+`^f떘5c]۝wwn|v8#1ՅкU#Υn'.Ysל3o_ڏ$/I$PT WL̐Iܒst\"p=ϠY>Ȝ9 wսbm[ $V{F]exos ̢xhWu!AF(d% {I4z/bBJoߤD gs+*8%̠V`{㞶n>e@h1k =\qc_wm _1>UТlÐ NL^)VgZ(]AZ&YP+gk!Y ,Qܔ)StQ9#r뮙] WQ;Պ ,%<y+T);t-^pa@ ' %_+o]_\0&[1e$?- 9V!mohHXlo.Vx-|!2'O^h%NmTp=Kx7ʢ%ߘ ?YvC{h~n6m IpudQZ^p#1Dk%\0Q߃R?_p^a,uHDfч Z_ϟoQ5W?" }Nkd6X`T|xy t;o(w M lBD-m<ц`rzk?Ε *C'ep?dIN| IENDB`././@LongLink0000644000000000000000000000015300000000000011602 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/game-libdoom.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/game-libdo0000664000175000017500000001514112641367670033030 0ustar jaakkojaakkoPNG  IHDR22?gAMA|Q AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  iTXtXML:com.adobe.xmp 1 5 72 1 72 50 1 50 2013-07-16T20:07:25 Pixelmator 2.2 v IDAThXk]U}͝C-R!D* hh4D5фML@%1c QB$!HF(i;3>]=5Օ^{w>sCf}Rؗt %Ioe[vR6fsu7 m<}R?@܂So򡮂].;}I3`O?@Tp/8_(JrsMkɗcư%t%uqyQ3} Ʒ+V']s- ]Rq@CEPpJ2}] /"OE݀QR :I+: pY fwЗc %ϱ_xl.>~=bC2G(ǐ5'k.tr4gD,1nuBu8/8[_ J@4 JB4<\6QCֈp'_\jxՊ "/I?mBn-8O@(6@ p!0 &Jmɠ!.rF}U JdZ0&'Z/#:9QlϱOCqĵ dY76R?@TH@fRS6m܋d*FSFpW19u;b {v/Q\xj$ &. @\c#|rޘos[èдrS# sPKEg}HTjaԎZ 1Zqf~$^](v q ouY; t+mZU*z3nȍc,?l[aĆFQX%X5gqp D'ٳ^QNy&+?xٛ}_ġ4^xzޫHǣ)=h折;懲:5)Q[v  GTyr=ɣ6̟/c[n&x3dI5~_gr,5\ N_&0['nFц}D5Z0O`1zy{Y<=?~zwcgϣ:maH;jH$cM~^zn5_{`wű8hyq,_탯B9AЩ_ 풵l9 G#K!a8>ڷTc5~c5ܾc']Vgә[W迉| L$\:ƅRW%T|fƵTQhtPW:^9y?*|#>I9N-pWj!BN`2j7øӘHc:_d>X2,N~]q- [ ܺhtfg$iwS:Bv4{I\'ů5,mv(ZjX8Y'^:p]fј ­2s  ,\n3揚XNjO䰸RtS*$:O}pK ֖4E*Y =Cvr\Y<%:ƍ;zӫ(fޱt_ Ѿ::n\O$]RŨE؍ɵ]WtxyephMM.v[Gl3:(<\[";֝O<,"gD"^}`kZ mMݰm2 1I۱\<g2u;D'MpIYh; tI@(,^RFah 1Ӧ/\1nΜ-t3D\PDWpQ_B;IQݹ7;%`aFcW[kmڻR0nWd*Z~Ƹӯ_͵IDIYJIdA`83tOs=7rhLWwAsꝞ#~[\;]6I?njNqX_ L]rǮ8L[_ Jحh~W# .w1sz/`+n%_HK(IENDB`././@LongLink0000644000000000000000000000014700000000000011605 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/renderer.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/renderer.p0000664000175000017500000000242512641367670033075 0ustar jaakkojaakkoPNG  IHDR@@iqbKGD pHYs  7ttIME )OiIDATxmTUum 4-T3 BbP !bJ# z">2P EfB!EZ*zUͶ@Btmv]ׇ9{fνO;syr1%QBI3$3ǀy ?3G7hOWCpe?Z!}6 c; LJ+g#^yO@{ZH F$0:eˁw=nMӞf"V,?t! [--@_ ٞv(C*8G(:0o~w:j(`ǁ1<{R]DQjdpt`r`c6f`k 9@\Up|Sm`X_ypEQ!I008&@ )Ψͬo<`f/dC.(@I#$"d$u%U.U ?ZwEHm,v=#Ťyc^7 `_$m.ټbh /ސtXRRYYQ5*wT3My{@UBRkl]Hکb LTfI;CMVMIZ/j$B|I$p}L 䯑C3KIH҃Fcqג\Ҕ$R8NJ:(KqQ@RKZ$Hݑ6$%@T PL ( X*p`P'+`0IK?F$-3@l&IIzNQ5wey*499,Cn0)RZjf٭.f{\Q +iu\s*;Up)pI+uk(ߙ+Uj/i_Nfmf-t$(W%6yfcfwXBn xIvYphɯ:;zVP'"ݽ xsЪ]`XP-8LWjFzF cI8X |$$)rwfC%MDܫ.-IENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/fonts.dei0000664000175000017500000000666312641367670031132 0ustar jaakkojaakko# Fonts for the default UI style # # - family: font family name # - size can be "pt" or "px", defaults to "pt" # - weight: normal, bold, light # - style: normal, italic # - transform: normal, uppercase, lowercase script { import Version, gui # Load the Open Sans font contained in this pack. def loadOpenSans() import App fontDir = __file__.fileNamePath() / "fonts" # Sadly, light fonts seem to not work in Qt presently. for style in ['Regular', 'Bold', 'Italic', 'BoldItalic'] #'Light', 'LightItalic' App.loadFont(fontDir / ("OpenSans-%s.ttf" % style)) end end } group { condition: Version.OS == 'windows' script { loadOpenSans() } font default { family: Open Sans size: 12pt weight: normal style: normal } font monospace inherits default { family: Courier New size: 10pt } } group { condition: Version.OS == 'macx' script { # Define mappings for native font styles and weights. import App, DisplayMode App.addFontMapping("Helvetica Neue", { ['regular', 25]: 'HelveticaNeue-Light', ['regular', 50]: 'HelveticaNeue', ['regular', 75]: 'HelveticaNeue-Bold', ['italic', 25]: 'HelveticaNeue-LightItalic', ['italic', 50]: 'HelveticaNeue-Italic', ['italic', 75]: 'HelveticaNeue-BoldItalic' }) App.addFontMapping("Menlo", { ['regular', 25]: 'Menlo-Regular', ['regular', 50]: 'Menlo-Regular', ['regular', 75]: 'Menlo-Bold', ['italic', 25]: 'Menlo-Italic', ['italic', 50]: 'Menlo-Italic', ['italic', 75]: 'Menlo-BoldItalic' }) } font default { family: Helvetica Neue size $: gui.scale('16pt', DisplayMode.DPI_FACTOR) weight: normal style: normal } font monospace inherits default { family: Menlo size $: gui.scale('13pt', DisplayMode.DPI_FACTOR) } } group { condition: Version.OS == 'unix' font default { family: Liberation Sans size: 13pt weight: normal style: normal } font monospace inherits default { family: FreeMono size: 12pt } } font title inherits default { size $: gui.scale(self.size, 1.75) weight: light } font heading inherits title { size $: gui.scale(default.size, 1.2) } font small inherits default { size $: gui.scale(self.size, 0.75) } group editor { font plaintext inherits default {} font hint inherits default { style: italic weight: light } } group separator { font empty inherits default { size $: gui.scale(self.size, 0.5) } font label inherits default { condition: Version.OS == 'macx' size $: gui.scale(self.size, 0.7) transform: uppercase } font label inherits small { condition: Version.OS != 'macx' weight: bold } font annotation inherits small {} } group choice { font selected inherits default { weight: bold } } group tab { font label inherits title { size $: gui.scale(self.size, 0.75) } font selected inherits tab.label {} } group slider { font label inherits small {} font value inherits slider.label { weight: bold } } group log { font normal inherits default {} } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/Info0000664000175000017500000000012012641367670030112 0ustar jaakkojaakkotitle: Doomsday Default UI Style version: 1.15.7 license: GPL 3+ tags: ui style doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/colors.dei0000664000175000017500000000512612641367670031273 0ustar jaakkojaakko# Colors for the default UI style script { import gui, Version } # # THE BASIC COLOR PALETTE # color text { rgb <1.0, 1.0, 1.0> } color background { rgb <0.0, 0.0, 0.0, 0.75> } color accent { rgb <1.0, 0.8, 0.4> } color altaccent { rgb <0.63, 0.75, 0.67> } color glow { rgb <1.0, 1.0, 1.0, 0.14> } group inverted { color text { rgb <0.0, 0.0, 0.0> } color background { rgb <1.0, 1.0, 1.0, 0.75> } color accent { rgb <0.5, 0.4, 0.2> } color glow { rgb <0.0, 0.0, 0.0, 0.15> } } # # COLORS FOR SPECIFIC WIDGETS # group label { color highlight { rgb <1.0, 1.0, 1.0> } color dimmed { rgb <0.72, 0.72, 0.68> } color accent { rgb $= accent.rgb } color dimaccent { rgb <0.85, 0.68, 0.34> } color altaccent { rgb $= altaccent.rgb } } group popup { group info { color background { rgb <1.0, 1.0, 1.0> } color glow { rgb $= inverted.glow.rgb } } } group choice { color popup { rgb $= gui.colorAlpha(background.rgb, 1.0) } } group tab { color selected { rgb $= text.rgb } color inverted.selected { rgb $= inverted.text.rgb } } group progress { group light { color wheel { rgb <1.0, 1.0, 1.0, 0.25> } color shadow { rgb <0.0, 0.0, 0.0, 0.45> } } group dark { color wheel { rgb <0.0, 0.0, 0.0, 0.25> } color shadow { rgb <1.0, 1.0, 1.0, 0.54> } } } group dialog { color background { rgb $= gui.colorAlpha(background.rgb, 0.9) } color default { rgb $= altaccent.rgb } } group editor { color cursor { rgb $= gui.colorAlpha(accent.rgb, 0.7) } color hint { rgb $= altaccent.rgb } #group completion { # color background { rgb <1.0, 1.0, 1.0> } # color glow { rgb $= inverted.glow.rgb } #} } group log { color normal { rgb <0.85, 0.85, 0.8> } color highlight { rgb <1.0, 1.0, 1.0> } color dimmed { condition: Version.OS != 'windows' rgb <0.72, 0.72, 0.68> } color dimmed { # Apply some extra black because we don't have a Light style # font on Windows. condition: Version.OS == 'windows' rgb <0.65, 0.65, 0.62> } color accent { rgb $= accent.rgb } color altaccent { rgb $= altaccent.rgb } color dimaccent { rgb <0.85, 0.68, 0.34> } } group document { color normal { rgb $= gui.colorMix(inverted.text.rgb, [1, 1, 1], 0.2) } color highlight { rgb $= inverted.text.rgb } color dimmed { rgb <0.5, 0.5, 0.5> } color accent { rgb $= inverted.accent.rgb } color dimaccent { rgb <0.85, 0.68, 0.34> } } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/images.dei0000664000175000017500000000360012641367670031232 0ustar jaakkojaakko# Images for the default UI style script { import gui, DisplayMode } group window { image background { path = "graphics/background.jpg" } image borderglow { path = "graphics/borderglow.png" } image icon { path = "graphics/window.png" } image cursor { path = "graphics/mouse.png" } } group logo { image px128 { path = "graphics/deng-logo-128.png" } image px256 { path = "graphics/deng-logo-256.png" } # With HiDPI displays use the 2x versions of the logo. group { condition: DisplayMode.DPI_FACTOR == 2 image px128 { path = "graphics/deng-logo-256.png" } image px256 { path = "graphics/deng-logo-512.png" } } group game { image libdoom { path = "graphics/game-libdoom.png" } image libheretic { path = "graphics/game-libheretic.png" } image libhexen { path = "graphics/game-libhexen.png" } } } # Generic: image fold { path = "graphics/fold.png" } image gauge { path = "graphics/gauge.png" } image gear { path = "graphics/gear.png" } group close { image ring { path = "graphics/close.png" } image ringless { path = "graphics/close-ringless.png" } } image alert { path = "graphics/alert.png" } # Subsystems: image log { path = "graphics/log.png" } image display { path = "graphics/display.png" } image network { path = "graphics/network.png" } image renderer { path = "graphics/renderer.png" } image vr { path = "graphics/vr.png" } image input { path = "graphics/input.png" } image audio { path = "graphics/audio.png" } image updater { path = "graphics/updater.png" } # Widgets: image toggle.onoff { path $= gui.dpiScaledImagePath("graphics/toggle-onoff.png") } group progress { image wheel { path = "graphics/progress-wheel.png" } image gear { path = "graphics/progress-gear.png" } image mini { path = "graphics/progress-mini.png" } } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/testmodel.pack/0000775000175000017500000000000012641367670026576 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/testmodel.pack/bodyheights.png0000664000175000017500000076412512641367670031634 0ustar jaakkojaakkoPNG  IHDR{C AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  $iTXtXML:com.adobe.xmp 1 5 72 1 72 512 1 512 2014-08-10T22:08:40 Pixelmator 3.2 >`@IDATxk5q^u{^.JZjڴI$I_FP B EB4hCRiQE˃RK)9ۙyfvf]{u_kϙy؏~#=IޏGmo{^򗿘׿ ˿!Û ,Ľ'x{xy{vawp;wX 8_뮻 ޻n߁_|߽o5ܿn5wd'#pg~3* ~_>AxCys4? V*>\#v$sBp\NJ˹cF +6awYgNwMhX~>9׾_W> }cկ+[Z2+A . lY ӀO2Fx 2`xJ,h0CR #H/ $!]t c]owGf>0zi&G5k7k,X*>cyKqaG!/,h'?IO~My4!Ǖ%P,&9,v,OW^V_D[ڕqW^s=c_T3A{_EI]ɧx! brHSW.(«L2+K߹2IX\ ]H|Vh P˻Ǯ }Eš }`+O*1lplY][6*Ci)6""p rDm47Pl#i(DrvtYۂTB'g;ҌRXT8-(;fplG#u_z.(*(vva(0ۍ;Dž $2BJb[{%/ym6:5L5JP"Ȓ1;nF)&*d,=!Ģt0&=$ce\X=ci6 ĘBPgi'qNLjWC:n '.G<^{-k6m3OxtaPꀮҶ$o'˞Y+XFvl[e9zoQG$8+z%@qWSޗ l;^[>")=~_|;̹\/7ow󝹞l9_WjJs,%c@&p[q|KJ'Gk}TP6Kz4uتwb\Z|FFEB>i7CJ"jL9u2^r(I6XX0^B-#,q G촤Ѯ ΅׃0n+{;CWYƠ10%ZQ<7"Q v[Լe[p3+k[4\tʯOs5=jqB27Fb8N8-+ S`Ggzw*k$}0 *b~SJ~Ъݔ.08} ,ltY*.{^0*:2K,fNNLZ SO48< SpK})xw YD@> VY*]9YW,D̎ J8(3|istItwl#>FK9nH'x -cnv U+zU.5)vW(U^ A~Àc" 5|kcp\[\;NM=eErA,cK|4n5fs‹@AG,ݔq0@x_LDa9ry#~!XB}b=Aꡎ`GȲ( SȪuY@l>VW~,"ԉF,lP~ْEݖH40&?xի^F2Z,}xAl=k܄2=o- 0( A3f:Qh.[rAGȷ4ˬ`ZfP\ 06WL%@u[H+udInFڊR<}"-! (I0XKx+إ"D$ /5I ns7sqMNGڅ^GBS\b39+("y%G;b=5xtt| ,J~ KRa\; 0:XK B2@Pff}S(0?(cɡ,$|+_Y$zn;s[#h5U#,vusE/d0>h"Fp hiF /}K)կ~uѫyJh'P3Ij=ZcW$Ͳ`\,\[4| (pR)*4݀X!`*3&ىFL|L! }2C]Ϥ0ڠ=BpFZ"f d0Xe''sl1/zĆ>¾l'ng e$$7`Y0h AK x?QHYM>ts[E՟QruGPzLٰZ !7J\:½!}%bPFIXP}ct믟tU Aӑm-Ra.ߊ+43F3zKU=x82im)7TH͵Q]5#=p[E2[xB&JxFa-@9 FUJQ.vUL3$X6ـ+gFڂC q2KDI$p><9r'گRF9 `8۪L,(( $<-Y- ȄQ0nAԴ`H6ˬ8l(C1;(h**xA RkS3^$Xo>ĩ4;dQg?:`xj)Z*( Ƣ=KOAHH0`hyIL k_>/6/'z)8+R |YBG$∪Co&S[7 Y%  0d]};ۊ8%@ q*d3츰̑ ⬂DYmŊ~` āc=|E}y !%8.yn?24e0aGx/.|Jt䴏Ֆ,oV<^MUG;4f!$MHb( \Bpaqԝ7:o C;`ÈP7lJ e_^h"5PA  5$"B fϣvpߓX 41>u4* mY 0P9!x#^L( v! lbjq0 KTktKFCalވ&E9 22'DKy0FѰ$RI@8N9S{.nWlžCb7g@Uj ^2+q0JJI9j]YK/VԘ={sg-*O~fo:/q*^domĒ)0 )\ 6Hxr^Fp1`x(d%`TX"c/FL5 d.< &C]xc'd2]`ᤈjcOQ };`h"\ۂ43}J]\JXY7b8*U![=feZ}gY J:CU}f!Q@QTvqYY_ʔitG63R#S&-^uSzQcn8_"b]:tGCHj:)rla<؀/YABNvq{ᗏ2:, V3MvUY,5tyCTTy `;/; EHQwU@|8:Kfk12aQusU0xrfd,fpE-hd5P\s={md-5Ѡuykg3D;2bXu| iFﳈmV`Kbf{AAB.mA:gܬ!;RyWK$K͌H& 0["}z= >}hk zȔdx4b7^2- 0t'H '@ĸ`#z0%eZ`r ~XFBE,!-:FՌn7eFXc]TVCL s2*K=JC-d2WU9'd`4 2aJZgG`iq;Cݖ/P@jv)t|Z&GNׄ)JO]e&.ƬMЯ1fHfs9^eM%YTڍ&J+rÀit -=ƤR(U ͮA-IQ3|$1Ldٮ4aKrg-34nilC&",'{dc)Ь#ȥ8Ũdz3.kǫvQ*Ȗ!RFb@e3RG0t2JPZBĕih4/>@w0`G] Q?Tha˻k%gkǑGlH'>~Zjf `ˁK 3OP+ܯ;[ڲċ 2;n1fMC&Y.b#qĂƬ/?XL^d'(T IR*fc*cҩmQg5nfzY,R v?FBWx]ُ̃eBDIx55Cf$LN*x_!0Oٿկ&a @5hrT28b1 b3 Y@QW&y|[i*5껋^ҪoD8ҲHp+([NEOXYz]%.H0ْur7eȞ;A`Y+wx 7MtYJZ:DFɖX%hH ]l0Z w0X0:iOUᾑ_E< %}-is1, ƋY@c7\͜, 5eRAxXh> ٦z풦E;l>A&d\YK*4=a0"*YXix,-WoB5n6Ŝb&詷 r 4 p{ʊVUwT-$)Ur^YkvLmR:EQbGFT*Xa(Mؒ3JWl Zma>lHy睧q9= ]wu=0}ғ$[ΝXrֳ,!_t_$sz 'oa5QZO}SsnZBvއ%0rg'k-Jf[ȤېlFi $ƈ qՒ520޶k!d bi$y GCf&:h &b= Ȋ|yFCMW(YI_850­0\N/'E o}aihUy9 RfxLUVayw(A}G|@fH򈶄 ]nIN:$9yC.? sdBX# "@Qz=^2x ސLYac)tv;}C#, {xl`|ZX姎`!S<=DO-hyd F$lAa4[b7P ɉ Y(JINcB ida&bbiQ }K0ِ,!"s7JaO$z # n KHL2,Xvl˪> ]lUA7`Y,rɦbFVv*>"C\VɞRrlY3/-UH3@Հ+ISZ$R9v5'{,H1D!;p RUi_6ۯ1žErV'/RE׎!#]@f-LTZ.J1bK*hBqGaf`wtՏ|厝W ,@cXVj(!q2 R@~"Hj\SǎkbR$ =pa!%%DKA3CZ24' &ic q&S v' (T,,xim8mQ@$d/tpRXdBr8j. &5+5> H;'^)&R%YPX hu72cST+5J@^TBw^T][ 0d:} ` ^!^#,1 f2XLD 9jˆ;@  RpJIkh6:;x-Y49tE0kLTGyg%s;ɖk#Y~H<,\.uE 7#hȷece@B':H/`m$b]10+a 2#Ғ1ћӂ$ 0`T⃘(49(̃!V`F0^+;#178+ jItI`:sa8K@8>[=[Ǭ9{m)DµK άq0/ۤا~d\<'0rTB9v6XW2bPdfy=NH<8h'noWv{mЇ̉}!FM%i 1_whOFz.׸iijH.ٮg6Z؉6̐x<-o:i%{I`Gd3oJ C`HI]rr-Q@4p!{6d`4Aba)"N&7KdxYxۅ HPq7vc t = ZU([0fK0R _!,puJ}t`[(Zdr P^=5~˅G v7eЇWsW`L1N2&.IxIBi雚&͍E_W_TfJ=+u90|,> T?XBBiP~/Fl= >Q`s"KT1*ZƐ#΅4 J|ǁ 90L#F P*2l7xI0(bie hٌ2XoYD9M S}`DÏsCf rT %˖˻ N,[" ^4FQ\(\C5X#v`x͑L-O}C"s(??weĎ(&]Z D t1 .I{95Ѡ % ;dd&cm5kLa&gx^T6,I-v>K}JȌ:1YbpQÊЮhY%_p( [< Qv 9NU,cr[c## 3 0cȅrb1AC澦M;bfGXmP@ &` 2)+ 6’ KikxQ./jNf}Vm̃+Όԗ~ œ"iםDtĐΉH-Ew'N)yszҮK 1w3<}1K1v,,TqXQ!R[Jb,f39TRd+JY#61D|ͫτIV+G>:UV*(M^AH;!QZ4[KCT7l &Xنg7Biإ;ɝ-x@ˮ=9xHH3z.3 azue99tyd~(g)ZlE 20Ҳ11nᩥGDҰ`A$&g続5cHge1Pʤ ())_6Q}gaq!T[$k(@KĎؐ0%/Dᅤ={, ,f6xԘ;96:č8iuG'eVlcO%7ŏ=`w^a‹,;GDu"vt'4T@ ;˜TG(qx4Nup A.ץ؊PKH(#+J[ed!0pQ(IJudrh_0X(%if@vai``O8x~;}2 F>قiN5%M!K, 7GI `qSE^qAi2h@#0O$-دhAtnw'<  *o y+U" @`Z<+^z)cab0Ɣw!u%9j)Qj ^5쳎BbA w̄BHW*CaCfgF =.rH@L20>OI5HJ =,7; I') ȝ/闛N\ M0 d3ii>}f6 _TQTreS2@-P)*TZ+5%qT77ۥFǻp "6 ~WƮd.8os=}Eu/v&;ޣe_5)K׵񒣍 ït>KBc jB01.@)d$ (E> mɜ|ɩdLEZj59b!SiI$ͼ$ H` V0R##`C&U2 23I)ɪ` I+eIK~z-S<+ԥE;_xaw 1A$B@GL3k [,H0.< *'kDf\< STgޯp0əDur$DK/d+,x^NcYƨ@I䠇!ALɦpfHf`/x:JHCajejsxA`6ygK֚Α?NDs#v˝(bJeW FDR1eցh=pD.Ly2G\(>cl`I0}N ߥ)q HsnuDI=HDa둡083{_`c2IϟX`D@ ˍ$#L2fb1&-KdJB2'!Y1IZBtdgSmCd?&4\ 0R%2 8D X F\е x.f J2F_Dҭ 0.[h圭C\͑U(qR,,˯zA<+ytRGs HUW v,⹊~T8 `|t@Xix*drݠ7K3G|.sk/yls#Z qiEɅ)|KȢ1Ð6FOB9 L/b!zdMp[Z9NrIG ȋ駟W@ؾ߫<$Zt1R`X W]u p_N[D@OExIذ5 vm[ .<$@a$$͇ fA  EU^y] l 'OH`bR3 ;4[ 0$MuV2BtH'EGC%҄12 ivFgtgԜCoI )qԳͬԟ$}0j𕾄*D,bk cAO,z[0 `R4jvJnvT՘rˇd6 kfuj͵N@M9̕GsN-hkP:KlŅFvM'ߌo0h#%.b U!$R ̶Xcg\/eG7"5I hF '*$l. V3fp%/8oM2۪& {HO&J\'<ۚɁXj屘LUD0 2ЙPHs 2h&5. 3,eJr*PL($ g\[ m1Jx;H &[2oW'1r]1] iR4մ;2eX6G/(5;ek Tn`è%=g̬\ZuF[2Ϡe F* Kfs S-8^vA)3X"8ҮM8H B|d=%X -~N'77tsn3vf &dJb1J1Rdi6jIK%v\fsS(FHii` @ʣ>rz˒˞I8 x ډ@C W! ;$9(lNX2sAJ`L#WF4نWp^ ⃅vqn>S02H6L&W2E@#+ @ L ī+hR>Įj=^a접sޯQJSgI+_`4b2{ 8%wЛUge 0]K[ELq#CZlB>UuvՈyC Xqo\~W]*Fu7/$h) G;)7KOEWu(a+ Ҳ]DTrcXr,!XE> {Rˀ7n`)X2\[uc4F5XybSB'2`kЕIvxȸx˒ 8-]I@F2(.@'h0Ҡ.`edvm'DAk#&X=g.|PҦVOޘK4Z $(eivgh+Zw>4U:`j^g[fx4"(OT X`Wį6JvlѷV8[(jJJ1GŷFHON-N4 ĒIrT^z3؍ ! -] ;~\ذZw7'PW2sLZ8GEr:* /DdP8PP7 o3p%eKK .xK"2fsg>Fҽ|ѣy𸮹giKZ*"D% >tX\~9.KXD5{`Ȝ5$$ ]d*0rDdpT.!ze\M=؉_y _<_u(d*A+ CvH5iidRFv#4.z8xn*1|42)&Lhdg.yʨ&E,\(@w 7vCOTm". `EYX 4ǹۮ%M-5?_EC( HjQ-GB"0CFC10@1DmDB^b 3vK*QH4sxfê`%<u@qm0@B,zQB)  h$ҕ@w=|ӟ.uL* `αsy}t30-%lI5 iTD-`f*nr(DX fzdG>y$Kta$wBm"౛%=L(zȔ1If9.3^TBa d1a3e)h^Nw ?q$ z +ل #MQڅqJ1*1j.GU\F#7>,Ë,N$~ʻ4`T4U:iY} HVwdq7G헔q CbK`R+0lA)22+lJ_hI$0cꫯc 6W-u5hVkp#S(\!\h0· @IDATDs"0DBH @Q2bNҋޖY"^KRfr3 H"vØ!28n %vdZ@Y!5b-32 1GHH;͐|!DN> &xSA ip0-:e D[<- \.&(uhaHpXaLѓv|Of^2Z&3VJg=5YJv/ %smaY drŅ99[R N*gAGZ;jQ18nFQ  A0H \8x4T e_l#PE؊ح}|V^+U /SOhvfFy\E1 wJ͆hZ5XtI(\-RrS_@|!7&('$i$Y0RK4Cdbфd#&oCVQY[RjZQQBLl4&` dvYR00| t U> 3 -L8$;ۂ,4S Cʆeb!h$RL22 娉+2-?~c_}((2wZ5X 7<5Fgse1G_:.UHXA*2ǫJ^ZV/Ȭʵ:8Rm(TQ5%2\ʆdOVҮGMjL  =# @3VgfŭՐ@o^fm<=bcZȄG?PTMטp͎orjiRj68)380ldv=%`. Z- ^0Qˣ-I6`DI im H{52XB]vE;ѓ@%93 途,H[X0f0btـÒ boF`6]|` `f'^|gRR(cB,%/RTOJ-  ވ-!nasLQiBCF11e'b 8а09曅{`>YwzrIkAIX37bURDXއV B,{/|00ᗚXMA }w8?яnmw\ٔ8NI8~fDiаl1sy= V!(:QU<UBg[=lexwSjK?R'N STTu)/[1Y4hMM(!b0H .X2YHvENdu%%92in'^ aY*$Na]S].d'Qd:%n: ЧS'Kp*o ! Cp ņX2qtL8vmE L5IZ CoiDK,":Vel,]APѠ.1/c5}I3HQH q8@i,z8p\̬(6[`^dhfmˊ $q[\B/ ׎(53-oy;y!rDx}w^Au6+aO{ӤC0>u#ԡ6KIxb#`g)8Τ% Gy$5M1.+D1BqUjHF@Œ;PpYgoMs:y] jBlp\Y.,l/<hg햽<1-v%BARAin KsfJ-O~?NO Q0"^$G"jUR3l lѠIB @Czĸ e0mh ^40dJ4@*Q b121KBOT\" ﶸOvHa-^~Q'-Nmɩ=GIZq9f5`72X9A7T*/ 9tЀ͕ G3*1f O2-l{*`dR #֊:H%)' SɁ'ٜDLYIpqaIv /'X*AhkfN!l5oLDl(f`l2;ɤq)+q9ݝ|`f_S)8vJYߧ/}ig_P{^xG,;nrJ=em^UeJa<F-=@=#S-%FHJPYE]l2 6s tU2~b!-ٖ+,{ y2$(PF}%iS-\3B SrF,uzQ&n]22iaGUXA&>v+5;cX j a3w8Rt%oJC~#|z$ipղ9a+,X ~$>TAj+70 DjZ+e @m`ܗB֊qKW/G`XU{Fd?Ea w 9L!B`C"jP〔h%b<܁IsJ/bݮCΐHcfP"b6sc6Lb )\Șa&3L&_T?$pn , Su[}KI ^KB b-h o@ы-xyLL fvx!LN1 ,)64J0b7q7sC@ 8| גoNHQ `[\Xz{:z3Ѕ`{BamG"wj根һY`t/QGuo/NsWa ]er`aFJ}EAF-} */9a͌@)P%!6aQh+ؕJ.f>;C~lf_kCC S"s /,saT0z w7 s xה /4X,Y%; o+H aG g!z"#4@AH c)ӜOaŒ,:\X`jҞpaU`"I"nΘы/` _",$MC͵2^Ó8w,#6$ zhQԅaRƃiG)&f @u&rY\rDuUN])vy: 2!؁81}3Ϋ6NOf9$R@5\w&u'pwb"b;‚1E opZ^ G\dǗnY4y]$b}-zH0bwD~(屴( +r~Dkh {£Id!o*\sKqHr5P\p-q~ZPg/<݊ L9XLu BzꩂSɹ ML"X~.{ 1ZR$9 ?fGѠfN7Ǝ6 (߀I}Bw!VQԙgvv͏L׀oW(Z%ޘYVZ0'v7;bKSXY;B!.0 W/E-EdKJ&uXxSB(P%L̸`FglgIAFiQDI0%$KDx 4LY5X3vy`P5'1J~bF+?D!6pKf)K3|>bq|l%5nGe qHrl< `IW!7j3qDCKg`A*# RfF$}%'KeCBI0v}&̺I'ȼ)BoGxƣ1Fe8~s!0~*4U.tz/8Օ;GPr!# ޣ,>F c!DB!OF[P0 -f TOFXSQ)/x=7 vn`ìDINVamZ.̳1|ӹ#ȓZPABaJ[T%T H;.s9nJ>Pe=2yQB(2+ȾB/42a9"@`<&0`Y42#37e_jM.Hrl >Jdi1`fifK3Ic-<Y6wwBU0R dH7gWФTlqE aC@ - _BL fA\ "Ki:`{mYpBXxG ~wS)eiNqR d I՟-G˷|;FYZMq6 Aʾ?q+.)b7+f!7:@E|||KH0h4_}XDB% H68fToRcV$Nc 0${-85)!R0NwS&X1 r)Kѳ:Q;NdoM{p,yxB{"P5\K%YWH,r&1U0Tv#hYFj8@\LxX̼U\ C!c`؂$ɱ4Ѓ4,-!mxtFl/7!L1Qln4B0D%Y#G:Af,$4&.2MV5;Ef&pZt= +^\R{APO;4,~{;7MSN9mg-9\㎶vۙ^9*sw?-]U"DDhh[Qb!ādwtH T4jl۶Ѧ ADA~ sηjɽUI쬽oks HH& w/Ҽ K:?Ւ$\:[NK".ő2PP @f$,m]ճ"%=<Si+QUQZ Bn/0"YW_700w?~1!:WZ5C"i4C,r?QQ3[Fa!Nm>ctpt`484CߨYì[g3m{ma3|]Ϝq(>:|S:)$KIr"dCpDq\TI!KDXIYoz]jISp4Y"PVӏ]1I/ʙ^DChRvȘBpud-43v@̅d:_d;֑s9-NSWZg2_.ox0/t~0mIȬHAՖ!r,^2O푾9~=ڋ;o`9:Bp-ňo̙Z\%%u6fKPz{u箾_hQ ,Fѩu;M} Rf):?D8>ޮdZV0aC61SMU"WrHzP( 40 H%Jrx?ο8@qf 8 w) Y#gp 秌* tdIP* @c2=%UgA/҅sd?]rRRd40jM.C34oC!ng mFy 0X,3N@瞒i2=dKK]h!Xb´0b˲g9 8#H,C&R e07p?"C͜O7wf_owl󼎯avKs"a\8P  ixÇyeyuJ (wJBCCE!o )Kb'.vUaɪzwdGh-gTF%&Y&J'5K|`#>"O(a]`g_9Cn[ANү9B׈ ɧ#UݡrR{3R$yQ鴅T셎2 &0fnDsaѤWO'R|0dҐj8;"b44V,ipv01[2Ǚ*p|^ ؉;Gx3FPh|4E!+B Ӊ)BCnVMVapx;8` cL4g"& yXh-'<'S,hG@tmL@L4>s%vrqU]el캱0?=aWF(2XmG}3\~*جЬk^4U>0gLD DٔDס Y4t%2m[K.0%ܣ͌YkXKD@4 "Zy&>ܗ+#8/ϚwZPRU-Ѓk֎CShNCS0=ib!LH6 @n9(y53tl `V=Hs$G~v>Wft{/몈cIB,lqҏ{(:҆*).`@d#Ɣ\ 刬gl4$bVKQBvRSu| @4B\mӡPKp+; *?ڪ%U98cK-A#(zK/`^7 <<,@FJb2La0ۢ!"bB`N,.K>#KLM'CfR<:͖3PzhG$[)dE:./um*Xspx I:`8 ,MKT ]mRvU']{[h8HS3;Mrr}>1ӰQ ZȑOeO#YJ KsD%͑p/n}xhy("88hQKS)'a4S#& R~HcL^! ]f\u8Jr"8L cJ92 ˑՂ ƥ"NfaXSiI~[e [c .9d'" Z]|y> n|CBRhb?>=*fYH]1ʥD߮-WUh**~9G9 Lu"hg]C(xbL >=p)1|`8OK$& >ԊfeʵvKU 9 _ѶLChc93Ǎn'τGdV4J.u[:-R '8f]Cmx|ɣp=k>=ڇ#>BW<=) M֏9"KTmḡ(cILBb{04>3LQ4FofTVo[2-E"ɕFpXsKn5&C/pZ`E Mݒ`u;zAq{V?f@3__hЃcW[vIq[S\-)~ד=ATY^3%a9ru0U* o#ϣ-pe61&en6喐 ,&.>ũPɧ-dF<7OZO<6H S1j)"9%('z긫CecbEQ"XN¨4&.]C;7ЖSE~ hdVHv=t/]S82ۑ[[&ouxU2dC->w| ,i T3p#-&CtfK`!-D>nCf'`'drF 0itDE'uISFvÌԡk\%!oҜ 4-=I)aI->ccΖBK ϙ:,ė7wH]"qH4>-H,4%+8DyKU~'J; 4W}ΡN&<é` sI1DLL`1\>;l.[*PʐksЩqA~ @+rAbWqNDZNgK)4 %Kcz~#h+frd"OW,{LE0 Ѐ(Ƕ~L&(Q?lT-hZ'ZPXO1h9w0 9,U*'F4Y9{7otҢlDV0ږ|S"iD3F2Tt(Y/'`\ oIm&Fa sy;D)eK8oB˷I=5D2cwWri`e-lזH3GSJLJDG[zΆ,R|'9.?H!D=Ø pOIX0[T̤ v 8-|VB6e2/evH۽aE$@1-'[ 1 AXoEb6Іw:{عfAU)&T rT%%F[sC䀥ҡP,6akHP 6:ŻC3c"bb/iȜ] =! t( TTjȩue\B0- KL]R+)B,aqo:ZϳIS > ]~;|dνt: r/Zve6U e[fWa͔EC 1K8 fWy1cs[9#* 82N/(\ױZj۹+Njh% Z0`0RG)LF0#ôTކ]Et.=NUЂ'absGЖ8LmfHn x0L~O$OS<8DY꽠Hjg23撲8ry6lGJѣЏ\4Dⵚ'^f4 2a@-p aIsEbZ*fBZyg~_F{^9rI3⡥g7 GBpt +4N_  D2 #vF; td΂NQfv)'N-Q`'nKOapvz"pux Et X!@-(b]5)d)!N'+2/%P+i ZF6==ZJ_x**%Lۥ6"Uv~!ϣv0<:oU"]q_oG&=8;ST2"p3,}u)3&뎏"JA -aDS q> vm$0sDK"IlB"KĒZӹ0\B. ]:Kc.{-4ۍ[h9|"{0wز\KNL"K{C3fB |p` X̘.!Jȉʳet"U9Zyf>DOɌbu:j75Õ(`*#@cUva"n#$M2!Ƚ5{:&Ӆo]R84~VcهV >pyt+M~~}CRΗZ8:wdqW* |z ?i4[bLYd7qVjIġZ4/ӄ-k%n[[;bFc5Rg֟A4ŌCp#] Ư!xBt%DIxS^*i/ͤdI(H}Ʈ l[-"hvDfm[f|tE)<9$+FӸc=*d$&Y d[PM:>J͔K4 _hou.'-#zThu5\NEح’ȲAO3W&nL0eGyO0EaWϒNJ|~ūo*e.D,vUB;b94i%!6®ԑ[ʛ){K&+)3;[]Lɔ4jdҏ b6nj!Ӷ]ue 3^ Q4C0ݴta dLR*QHf0T` s "0 8gEj75Rn`we`0v ?nNKYF1 njP8)yFgF i>AHe%!( bNd]`%ԝ!0>shEpӀ4Rkڐ(R @PN q ->I_\י,m#&ք /':gX`oxxǾ)HxIX('4V*G%KfO:r٤`ylw\:DmdU`^U'm,hU-âF$ZD8вv  CJiu 6aZb0Al0nYRhE9&[-TŞA # K4ct M a lYx@UYUYi [#eWh#"TZE|߹Γ  43'hL-`bW- ?+dp|!AÈ:Rwj]o2+Pߨ`r^O# O^TK٧Mz=B$x.LHQOh4뾾)8A IQ -ٱ,pX<]y啠9,pe7 c`4p<+8B0F[DG+~BDLS2Lc$& Z1qq b:liĿ7r jUUGfI3hi1K)4"`ɲhitf*-%듉"XH04Ydt^mä  FYT 4@8D `ZS-0[0bD3`#KeK 􃵤*RF9o҃t 'Ut7Zlьϐ%YItǔ3ĵeŒrRɒXG-z>. ʕ+ Y" L2>X0=,~"g|DV5jNNB,r &dU-1˶A?qi?@aiLgpDEF/ *TsBpNz4rU K.翨u@1Bq|'A$BI4ZFEGp_=Vh v!;eJ˲sa vuIHѽo=U%gUIp'r?y5uL^pI`k@ ve )!rD, Ѷ(D0], A& 5˧BizxTzrUe*ڒ)fu H!7gD̆,RR0:h6[vxOqO]=_^o5 Ӧ-W, vt򲩒\VR[L pX4b3tѩ_V$"ds1yd{p K.!*qG ifzpa m8a/476aw2R݋]Wu>092`F{۽zgo߼0|sip5.$qԮ$gf2"ѹ0M -KO "]F,ӐgT%!YJ w#CZRtQqf021"4YY16b8®`tᅐf  nK~<Ho7re]ri&D*2REՒ(4l{HpC8~ OG9,ait 0Ba2-WCn g}ַR:n&%Z<ϼCPso)ۜdNQrW㛬fW$h:96ݳl};״T_\ճD+$L[C/$b(9',p:RQ'`h0t-G4w% ÖS}7/f{Kt9$^کSL),K.va R0yLO1ez/0`P`(RgI0`fyg(Y'XJ%ؕ0<&´qKW|Po Fz,ٮS?x_.UO[wWTZBLE[_ AHC MyJ)"AOFAAʃfMoy('?g{˨)-%F\89tF hd(o4h˒e$Uft܆fY4;$h)9dqȲαNM-?vsnsq@YJuwj FA4 l1睐O[#RِCQR14"qFG,3.#NKz [ܐT͊-K39hO`!nкB9}]h"cx*V-)ĉjS ePaY9F- CfHtz  "<چɴOˌPgq,2ѳ' NR`{n0fjȆms-jsB叒#>RU?2D^5_X;hLp^! ӰӰERyh:i3oJ3@AŠWI{zފԳʏ=|OW^}#7h-?3pL9Vմ9> ,63>o{͐'Cd}4QIaKrL`K  J[cRh?oےCa YJbZ"ɂK(NM,K%gȆ1(?,3=N4>Q"UM 3Jp0|)Nf(|8"] Gi-)YcLQ9Q0ׇ}S2S|& mnPL%VH>]9 8v6[j;`0oKrmO>v a'M\jm90D0%5$GDqʕ+Ԟy9"zx"3~G?ύj;'CfE)"(X[Dc3 :pxLR-AH\T9*}=PJјʉ~xzTcc cr×9>L_/wK(w?۪ Թo̷ķqxh!""8 eָQ3.i`7<;Wa47^mrOWJ\,dIPE"Dr@ Jh3͐;\UcvpN,ƧraFw.r ǀw^Dͽ*}3KM5~" a/^PDP-0e/z]`JCg_/o=0#CUH 2Ca+C fG!'N[mS _g?;>OPȐ  Yx<{Jn7|H|>|Sd{҉ Z Byzd? . 4x*[NR%B-}'~_`I8':}TδŚV+  ]Du[ N㡒+F{à@L2A$OXd,tL" .?sYZg* S\tQb*=[ι,ɭ9|E.{.!]Ha7hCq*fu7! -WL3}i,yd-cJAgmq[o14*BOhVDw,L: eJM ,w=YЃ\KyB-  Iiۜ?fʨ2-~<Ý1Hc2J fOkv4W-3`ΜOʐ/3a/(;^4=e GMH{eam' ܷhb/Ύ0zAOqH% 0<Лl! ` kywK%9q>`r&?Vvi˥f{։_U `"Fa\e8[ k/ҟ rB /:Gp Q"c–ftd60r#1]O⯎/5KYi'M$1la [mwĥYk{/vv#d?IoʢiO5q"˼@_^K{wŊ;YU7/]H##7Hb'JuyIy7->d N56 L38)h|vNssJ( WwpGQNӖʒ7lWIS7+y;;xUhPM IeУ,= Rٸ R 77"s('4RMcaWQa rLr e-ddI!f4[+Vr{[=b3*fL,8Fhhj7H[ⶸb)9v)0[WOE}y>P"ʅN?`ګN>p{\:JEYzX9*t:i$ lܙ"0{?@3&Bd5 |:`%ay@ 2grCMr>v!X{Z8G?Ζ8%P*9co{vi0t Ե)j߹d8-xq4^lg3mNa8 $MW iFSk3oT :9)E%&81A2 A i LUgt UvbE|qZiD!VNF!%1^Rc0,/.0N %I)8Cq.qn'LQ_tD#C: lgG3x$=8 mK23TjIZ|-I F?SknYgNg3̾}av L;t"|NpU-<ZHx~ڥYhCKM4IUz)0K9soND>:}I $3Ԟap~?1#z4ZHam|xJ:5U]"eҖCBQ2|$͐044)hq44!8% <4ClGƹp$DXW9l:A*Cv*BiG^;r`Р˳;HLIndWL`0na9t(71&`3~gj708!؋HV0>IRː1K80xi0GC #l9[B޿(XUn D5#BV[忓I)D'biYm1$54|n;WN^XGoV jF!=#Q0`G,WiEt?ߤFT?: ,ܣA$ϊzj|)99.FDHLRNgNf:EMTXrNi׀vFI-mco~ŋ?h@rJx{eӧ1@<嬿Q!8A_[@K3JZ~ru|"t17sI5,S[5\s')$:$;¤$î7>sfpɒڎ ǖAZv-(mpV Q(j"( thyY,@]3L)*Jw8R-](iLN9XZzJK)ǜTnq^Q33H 2T Pp+Wԟ{a@4$EҖ 1`I3kBK̥,mӖt?;0آ5V0L?T{ιإa.I\|m1roΙ(RZ*.}uL!mn8<0)JЮ620A r[: `q]i+EVB%BƇ䳟\vGn)Lȹ+'[tl9zC399tpwV%jY$(?RWI+!I#iDuI%o(,;xمt9c+2|v\uMr@>!|"Ky֡8701zKwy$`#+LqO3"nV`V>x0m syF{QU4r۶z`,wuۑ %~:ڱ߯4C4)8iWnyb0A!\ CW@#̲pRf%$® RuJW\vh7pI]' )_hjN2HUÖ%\i3-gd -Fsp$خ`@%I,/K|42\kT[;.LA'BÎ28N+h<,F3GuN%t$m4I Ϝ^:sGַ|]n0ĨZ\g@=ʂ]QPʆ;ߓ)Į@"ḡoBi o)p0;[(9 L .xѶ /6R'X,_xYDO?3e]/ڙlf˒{H'Sf &wg.g 3ZKh.`9<$ܝB*7C(ܘQԈC\*c@\ Of$AKHW{<țdVů=- xOY83|!U439ӷeo)km6r@M$sU1Mu[l0iX@vY IkŃ0Dęy_0&8U-vS%H EЕ, e L mh>ZIi)˗/ge|;-AVwb?1o}J4"CPʮ8/(s^6A$%d)T謐 C x kuFGzɤ\0(7\]OQ<$9>xF+5aS#ۮi#eÃ$>Ӗ,ɖ#ui):$AJ"L1ikmWTqGtRa3LqH oJ!'E-38fxi7lg G΢Yl,b.a&4J:Gl1Jgc_[&ꢩ}f,gw$ ]&L[\o+wA붣Ǎfqrrtaж菏 >0; L]/:: |欛ͧ9 pyjмu:v ?3jW[gn2Eh*Xg:>>|g>ŤZ'=0z8OU0vE/(hKB64B =!rkP^}Zf_ܷ6ߤHRznA.e#Rz󧻗eˉ#D;|.E@94 |"t%Hf,:A0CStu(c+"|6QXJU7˃ˏu4] ٣ Ҭ,U곸*M< AAf`L08gpӖx"@KAEQ]B=>ٍo Ǡ]$H"Ga;D!WZ Nt3C8"B"[@BnaCg>kM.Bb[]IC9: Smg 0Va9G3"U~mD+p_'@B<]I+u|aݡ;\O굃+t-{>I sz>gg$"K0BXاxLJ,?'@7MBhJZX L~ăИH!"bW&3*Kp(pKmqbCAө .U=ׁq7+ UIOְL'%2 Ֆ[Bqh0[nJ'(0_Ą7ƄKj @145Y0WXvt[ԩ**y[-5-x3$0Sdl* d! ,ˆ 9(B[)!Q*[1jh+:"В]Tk@͖8 : M+HIoX_3&ֹ;j5Jτͬ+Jo"|SkOo˟x '?g}V h0ŗ3<]eK4x̔>k78%=K?}ѯr@s_# ]QN:k< $Guyo̞#-XB|,Pf̔H)}$PHҋI`Z[fhKM[."4SxJ#ĒKVłYsXHa F]!v#iBnHŮ"Wplv|3 w.áeMܖJ4,{RBp08fǚ'B.wveȣ3,0%ìu.I1AMÕ*` 6sB5V#y{T83ND-.WHF&Ȓ``֒)A׾t֦ ؓfKY M 8G>BUu0Ψw8⣉_#w(i/5}`~\%~TI W(7oeT4m--ʡ@(z  5M! n,+}%L`4%cHNmDa7+9WI凐NSjR㒇AYf>/=풥݊m\-mc2d H]UŁg]; T"6 )Bӵt*/,uzQmDg\c|QI᫭EL`e68 oC͒uƶz#QO'uR"20Y8JXR>ϙg`T>!8\M?BQK$?O^P{g-[ׯ_˗=7Yp}/Rʬ`\lR ~4 MG/IUu+7 ~ _dD롆O9W8zz;2ַ*. JEMR87Ls8 L;k1x!?9zYҠrLN|q/Fozӛ|_c\A {A5vT)t83 @y5`D04Z3vU&UNPG&q'y'77G)Fe6~—4KG!Ap+Z)RU]hI-4 Cp`t fz3bRCMvOPTr"!Ƙ{Chb>RM}CC bpUDžԿrFVͳVRQ۲9ȒqdsŇ&`fsa6i˭M?^-p攳QgK)c*Mɭ]'%Cz^Kdw,@Jf57QN|)JOl$DDaWO1GtRgj~衇|R8X׮]\d z &<0.AxCHS RB7W Qx@e]hz**3-ʅ {gG9 zʭ9B1s܋Ĭ#R'v4 '%p?4q17 -gJ`"Mϫ|""g3Xϐ*0^eƇNy&8%Vј0ݝX1;^=R-ij.S:(Ø&K&)3&: ?]>laьZ"^̧+G' B)9_0J, ?iv]hѓ䏙,<&рOز#Dҹj zK)dLD#̾a·{D)(%,uOᘲP]գzu&HV JeJ=@/&*nuy¯k[߮G xy`>C(hK Zlj΄ Dnܸh8 .' =dO|{ $HRL&faVOOڨXy3ɄWrsvd!0B8- a0ʤ].a"%{@oHQ DIDA  "@Q$*I|H=y;p>k}F]UwTc1Z{WUg6N$RFW/f-΃m0[d".;m9ʁnLĪlĶ PI*GC 6Vprƌd S2gMC3"z9`o~k^3,ųՑawc)j$^ݕ)| ._U-6,._t$\Ԟ(n bgZ(?m >2ǩ O^A2rdH8#F`4Y* m.3RDyKJ_?/|oB{j63WbHXw:A9w ̍ٝORZ"[`8 l̖4RMKBB2Dm:KYnXҩ+DfK/[}湹3lTeX[Jo͚ˇ ⺱5;3$2UhT` iE+%9%:C nfotF)avX%]ZrwAl${n.l d]!t<]O9[d4$njQ0lsl+%nanv$JrlqT@wjǔXF̱Ri``FxX"b7$۸B Os&}mgU-|gpxta0 bD(U[Y n^"-h[% q#"8ZK3$&%($E0 G9;\p2 0(q2pѪc[{L;ĝvSU$DhrQ[bҀC9YL,qK:}#dGy`-}p;"la"JOSE3[`gk~xv[*%Y)EboWT?@E!B¤#21.8|3 7WX#kN 00(~qa oFg yeɖ9UeM+(G|I D3aIvp#YD*dKQeC4&C4~gsEOY] ǔm y"WBsj:n.&x1Z]×?% D`̼uyziOJap_l6g.w^i`|0C\/!DPj5+;+Y}pmdB1'G-y%p`[B2R'6AJF1mYiŠa Y*[R GvK(vqps.>W\X:5~=։!])h nRѰ@6'"?ʡXM$^@-D-:k{UT^pť#kLQ}`D#cp)qKx̿-82i ̐zhF~Bvs%3zI-~ ZR?a(pXuP_0!pykfu/5ʤ(zfٰV*-I9$%bص,BKǰoEg}z8%n@HoOO= N><$CYKZI2^9![vØ-݉4(Qw[n|V.|K_b!ūnm4&f-n|Zήf !q;̈́`&´MJn <mz]JgV5W&خ,%P{'\9,"EfRlP#-+8r(Pk ]L0ͮ-l@Ќ ~+pcb"8ikl"CE *9~'0p: .'dL[{L i4\z$A]vv b:5xڲE-s4Z4K-|20A-z)ˎNWDB=L8K9e>y'.JGe]~xKiaQP4`"d@JX7qzTx'`!)E M+Z"" &C덜,dB\՘'|p*?Zza\t".wB fA$״#;X==$:TI伙E:8bDph0?f\ᜋG)!zIQ.#c&! ©C!li"A cn,~8(#l[ny ޒzN87P@IDAT {.:"4A-_(g_F 햝"4!q3<=;*v938z`plԚq(A8<`J_y" i9 E费%~AF_'Yw9&LntLC{LeCeȮgCK40J'"\.²rNvq\7|: ,_GYp sX: CHT/P4{1MsxK'aI͒jja$ 8)H#[)*)|Jrî%Yљ#+xjG9ږ90-K~=!Az0Z0#7gf>9"Wz4 @*ZH|s{,+[C ;/QBl7|}bfH,"┫Юqm^Ƨ\'\da)0VE1 .=>Lɺq BdޮPyRD3*lx FO$ъ<XRKCque_Klamrq:9Oe 6 fi$ ,1fɉ2L҂T+JARXׯE(qjE8DRv)̲GqsҮLmn4s(,:u4XdҥK=*Jg!=ЏqϞ1:g{ ۡq ߣήK|#& Ղ)-ֳ(`b/:%'m $n"cHH6] aWƲUpb.|̙˘Q+ 0͔@%ˢ&lW97Rk QGo&Xqm$uhcU:T2D89UgRp+/$/&YxeR2 '^x@?|`.UЀ0S4zmKެwE{^ׁ~ :%i>9_";N{vm.{9͠SV qIK;1!lY::\jѿx' Yuƫcu+\G|BD73ST%Aef7V!`|mݘk{RAbCmWliJ!&:qI2x0Ȧ [h=ʄDJL;N}t2jpiSu[9'"%vєO󥖖E}sKʌ!KnԖw:!q!sRH!<JII"<7%s mWMg9 vhf/ {A' LΙmvmF]#naDJڕ&H:'n[N˸IvRIX‡8bLKpW1}J`v{j:Im9g|H|CӀ){'¥s!<uf$m` Hd-O-Sh=7xg<<Dn)W?Oإ8ϮiT`Ø S%p5+)SO%D 2q.Lz"m>S[C? As"yrf}_>Lt` ^ɀΔCoV  WZ-4|}4؂1j[anBeXQ;Sz[nw759Tԩk١ba*՞sR Sz,>7(-/8, r tٮ$ʉ88J4leKa-8|`!^ uD i%:%?gˎ %d:[0dZavWJǮK g4O3[>CDI%6/oJܓs n|q!Y*(XXiiT`?Z,9 Vhv\X$%v%-F4|d"4b|0 [rf -3SoC?G/ wug)bF)4j-a{q'BQ_W`'%R,cM(9`2ЏK'"~]# oE)@k90]a٩G,t7'˰-EIᙱ/u8qoה/Y 5 G<9cٛ#Z3YKb̐JٷkS XrNb{=l*G>h1Ky=g8 j?/1G Sd77B )R`'AP/ K #nKSL 78C'v08&>⼅|Wwʟ\2%Eeу`3tco^Gxq$ L[Ge[40w.L => K҈ŋ c JsQ"Npv2<%:-vJ}GUqF -qhF~nOtZhRfCZU1"0RYƥ%Bu"nnp]eG9Ө3K_͍0>ÁPʉ^ 9Apv!!6kDH5 ㋫F޼!i ΏF quIV`W(,2M64 _ސ"_q…ۻ:~sLp{_\\Rrp䭻gREVS،Om"OX?ͥT6WSkQS"q-?vRMozިcKQL+[ ]`̾0LVIo]3NLO9mX:v-Ҏg0Y|HL-0uJ$ek0hJ, B*`bN^)5^M@+%b:f0]'oE2$TCnK' m )Җane K$qFyRS2&ҧnL.bA=<0is>O$07[?^|vq8O!<#ӯ-%>pƏp'>6Cۛ/4!S Ӱd{r $ !#jjO8FV84ϓF9 -FA O7Dt6 8TaKNgp:f3S S*,i0)TzJ =*_3%04eJМa%o`a.jg 36VT(,k_>_%$!nW*ᓾ2H4UJX|}́(|pݭֺ]~g=$>]'d}S`adzQu<(arHy8~m緕,]TAI+mSm.I"IY.4UK\ (Ab]E3f Y~*E;ɞ3-[0!8FfK`0D̲DCx)s%A N٢WҶҩddOVGz!s Yտ4eVPŕ۫`%ͯj!-Q0uOTh6RJ 3J'*,&YzS!@^j`C!$Y IYW/[3 0b4^xgbw~'+[B"&UKt4X( ZY [0#|w%5-fhЮ&sE-6q *"hn >00O90'+2liDh|9З3$YRH]0 0YwysIЭ|8J8uv32d q#d=zM]q y0i&6KZSuX4>*F9X7Rcy%p4F{=;EWz|/]E<- GSE3Ԯ\ݮ+0,ZȖM oHݐ7jc @K)7rEϮ]zp:*>[fܻע[da`g6_Ws*j vt|2;?4柃ё_z8~%'e_F1$CaӃO[n]nlxȥ)ULX_φdn\.(֝IA?A;-%0([fNIɼE,Rp&^8ҐwWx x'䰡ӬieN?t+Cr1}/߿tTLvYzO+-Z/Ba{ =_0pQ[#1$W6p#70M%`pU)ѷFŮpJ`[vtUMvAG+@94c}{uW%42$X$rKe~BSe3%&8.&z5L $h7z~G+=/^oj$4B{ uA, <,R|c>A}4#Bo#Hз#l(-7![z(7;Tѣ2:eήf&N-@@Se hxjrLWJ)uFٰE{C+_G?Q~EoY +RCEP3jBl郿//oD:KM% ݜ1YWED`0]&1{$ON~ ;Q5OBw1=S8 ai@3w.8h à:s`8_~h 97z|< (sk?rnRn-r |sa<`rK:K T;0f5[8\vcN6ۡ0%(g!N(q Bo|4>Kyੂ''.D)"y(d"APP5{YjY8 e. TDdL! ,< >d0&hRe/0<1lo9ˡ#k5Qe^!XԇʁGcc'ZzH@=x:C:?rE!}M8::[).DA3AuKY"ۻ9q4A.`JcH]9J i`Ζᘠ+T/2--L~1[}1i38b.W5Z̔[E|QlGZ~3.d~2eFgH+FD"F/Յu%硓C.iwrpLvm9FNZ22& xقđ+(dHnVN:!f}z F)bN" _h$80GosӃhA ߔZؖ}phfڬjuL8g0;󒏆p4lC`Hm!8Yt<`0Iixl6,w;{CЅ <%i1TbuZZڲY()|KU#hWa?C8@xA?Y'jƴ IyEt᳈o'"Rh|=)iIII:g6m0CD`g4TpEFd!q,5tfI;'Bmosi=g~Q/LֈY\c ]Υ9͇hy`z#T) rb ,ҾtPn07)THm@`Z僥p;"Lp-Ä]Dإ  Oq:bI0S E&3]!.#QdGJ;z\J$B [&!ّh .tUL;`j& ,f \f fK[;ޔ"k}yV-LM~8o+[(x @4$&BS>1` 4Xv_o7>P.u"%D O<LC0UQPLq̬K/OijhD9_y4=O\2A"jnj =yu<{ry"z15/[Qؾ|#dͦrF9}_J&W~G Aa%Qucdn0sɬdhBڑu4}RdazI>8آ3^i@By`3t{TZaZ?G>ivnݧUk#o Iy Yf2!Sx>wBܣAHp̖a:+1@`Kt|08vۢ,m8gvpX8O 2@ji+s%E jm+0qD0ѩ2"HUt|V!W(܈듌⧳3"(v(Nx`0 40{|C|`sg ]9lXJ/]T, hؚ}vVk-J6R6ն`ZFD($?>9勱?CGr"ݿHg2q]wO MrkIlG(Kʪ:4XW ]bTSד-[|7;5X 99چo=+o@(pQ9dQ#[~җLؕ:Ϝq"^:7,>% vNav]RfŊ ,9 %1+kg0ODR.HN91RqT!3ږr0ieQ8Ң.RA*>@!)=GNOY">chVS:N!1-g0m'ӆ< d?}(-yBZ_ۃXh-4 ʛ <:g4Ot,cf|(9ih I1!G-X.L"‘HH[4ѫCm}HhACˏv_jj|h=_ѣޢ\@9v0̾,*E|ϛ;/D]$Vu|[5QɳfUZӒCJOԚfuqSbLyoz)")a+n] Lq 8sf`*|=]̮-xR*h0!cr043>Dױh| f=Lt9'nswǜR137|Dx[z( F1#f3S'?,:*+|LZ4r6KCo?Sjl%JPJv?5 |"0.8Yw"% Ц^ /uR{)ADP f:rzZ}5~ Kkڸ13$+Ȣ] K^cK$O2U$EBNPq"J\ɀVڥ%48'z #P:wߑsa3ݝ%o вP 6?;y9iI$i+^1`@cRB*i즍i4(yy'ob>c?OYZ2̨%qF-%an3 FP%8<>xk><)]˹}?m44///R `O0 'xClikʀXCig[׼JfKRDhC B3ٮrSۑ5պ =gp-?֠׋b:`pxhI<(D8W==f"μzC~~%OSli``&!o;moۘg' ]ِdfEC Kh`` n!YEAB0Jsқ$@:u{[v9̝? "Wf.#|K渍YD⥖T( ح-C҈#Kxp08ZW0.? qLQ`,ш!FhsefV" qIg'F?il]0:N5򖷀C$U B*TmQjL4N +k2i 4,@:] Jq7tZ1 ]y@ga>0UO'Kx10|4A6l#TchWsC" y;YEF* 1[UX!*L a@PcƏcκFJ.F^%)3&0yhs܀OČOPoaz] ¶[7j$Ag_.zGDHy0D^o[^O>=q0M9U W.\Ru݇[^:zO Եu j DeZ|j/m].|L[r'!ŠӾ͖u.E_7WOBޖ&&98qA?iC^5d*֯Z-ȉ?O+*Qg0&rJ`#Jb)BB0|i`[82 ]9Q][b7v [TE ݙ_J`@4 ϵrhBWjW˚n1y(X g͢&+-gHQKO~8&ME&q8o)HeLS{-12b:yLQ3ߤA |aИ۱KAeAY"hICT-#cN!=o|Q!/rf夠J Gf| :?6v*i_X;Lkюa԰f.YRB@#s['QB82 %+-Vh`)EIxl5)KaD [n8, |3%fzx@LA'2U͘#$33v+TWDA10KHIےx MpHQxN;s[xz#\^13}IOJ,^yBN=-HH^[hm86v_ gADgkqC947+h֑U;i5ۭ;<4xTD]N!g]0JsxHH0 UY!( B`I&ڕ%odphח~U=Epf/bG. r oElAhd LrR"Mhv D \_yfҰ3p,S{ CnbFL4WmD`l``%|`R4s 6"AK:fݵ"L= =8_jjoml> o*9;wĻOӤv'.ui׮8=SH.ph3GDЖ`R}yM'AEvܰUPC1E7?3s&qr%"؂j |8f#:HCH-=UOqcV%FmѬ_HZW"gi~v :E1K'T?kux,Y䤨T!`s`J\} mMgH`8ڭU,+<'V(̈[8$ZHVa^8/LҀE5!yV>õ|wdWΕTCET\u[܆Hc28)|idVs!tWFh]"۩<Z֊Z^@F3 zЖ%,ќ8, `kPKU}  U`ͅ c,^iMgiKGEcRI#u]`o _ZF-YnnBa4k_ٶ >,kKME9{d}م4;~AgCH6Ҙ={ୖ{B iᐰbɐOٶK(?h>kӹ?O+WbBȵB1NAEtjv%1!",|-9 &h^s47 a1JvӒ94me!r&*hp7qJ,dIR S 9FԺܻɢ2#RD tOPWbQ_'ȓ^`r`%d|_ϴ >U0LmLW͖Lv %)-4\i0lR⼄$SRRzTXiqL7KR1ó 04dZ\d ZJ-8g@#q<5 OrW'ƝSB()B[ UA N‰]=-iC|-Ft&nKE"ݜa|%Ni3E3g+o^ (}:a(9-j)A`r=C`!Eg/؂q*g|md*کc" |c8i"D IPO!K4AN(!aVPL 4L֮z2*L|KȞ"Rv9Vҩ0%dlN9U @r-`R+D(7 Vt8 NJF@g7$-ctʜ5 3>mŕQxH`&Rt!W`1&dT٥o O.A{" P$Gֲhr`Hsދ6?RcvPYQ!33c#^CI27p.t'v :A$&$ JyYP;4u͉ED#F*8xr.Ϣ Bc 83d 6 }1gіˑf"803rgړؐ4Ua 7;g`diLHX5<>P)Lnb%G,ve O>~2hJ^ss?fdZ~33}Q10V4@h78[M*U@UQhPO-Xu* 6R;;;LKXIr~9#px" K>aSk ,jl+@<`TZ-aXMά8vyϨހӒfh('?0)4@'h-ÇxYDPh>}-)|#Qb:)030>1DcLQ`% ,E}{m+|3m/J`򌀡nj:bI0[CkDNysQfvakJ=[vkJVʲ+ċ2*bh ^ {0=`J17|š3T!p .O`fc2O-DSb+ =4Ov9I[cmk/2#O e0`:pɕ"Y-&!.v(?]%\E*g | iqƨ#F8Ӣ@Tױ\DK[0A-3΢- !ծ N_hʲ+w=UrKրT&:ojh4<ƍ\ FWv @eiih㺷{ttdIA#?agog>{A-u8Rd!p9&Kr2IH6_5-<@Kx2-r^ԧ6#Sh DfSS3Z&EK _QP. ZP8oI0ٴ -5-߹.7-F%9or *`Ctvٍv x%8"(,hZl)/-Y!:DSboeug_`R{k} @&sô+H3 c [ⵧu]QA(CL8RS)oipƜ-V8k1 {y8?ƎJx 1tJB:6D# JD (T0 6AAQI$zIG D`ַjUuv$'̘cs9ڻRneH4mY".L/dP)RY:0B؂av'mV[>]=] ז+ "BH4G ±[6+0̱.Dt5Jƌ& aW_+&X7<|CF @WqNNB0`aۮ#?]3\bd'-aY }#b?!x< 1 mEp~i-8#1@Uc`)1t4L0,Sc"b;|K[svz衁qOysu\iY#rY ⩢Ҭ>B̌f3v;ʊ. Ga 4zFU"d,̼*qHIiDcǯڶbӏ %=vF# P8LXFc "% W37h}-?]SD^ Rqx) c.$qH t f$`MYzݖ&%f3۞Z='RIBU"> '1oNaKP5iݘ7.qh>pp@1!kmO3fv5 S翠љx|OG08,RH&& F4SX%F*Pl`Re Y3 ӮA_U> wto.4(ci2M]z,d_rHOv F+~=2'hW*ٶ1%v,BeRr3P̳L+ 4G8H]ru8+!"`"ĴD3B 4 z ( NCCLqqm7dpXjE#gbX0=,)K0 GpNii!2DP3C3 )lI'Ud!ir O{K,e4]U8w@ 6A&07qh G ϭܳ۷4^Ur j =/.z $cZRy5*e-I@3jfА%E̺W-Ю+B3["(နXD|S;H8`d͖E Qdҡj펉la1Q},!oƜ1(:ʶdAa[H]X)QK>=8Z5G~g - ̩OJ2(%示;]HzsZzJ4H#̙2K'48nՈE$!dNb4B6`'b).8?W5QUY _? Eq 8 `L-ƒ)oG'L>#FMD,8IѓhK[(pC2 ;kmYxtan`œG7S#(Lf! =0 LulQʶ -0Ijx]OwL[s\-OJ'4_֬:4mՀEm&|##$JHȒ#f h[,s, fׅ%L ƒ܈e'xƗ~4i|Ԇp<;#Z\y)FWY[[l'<wwI/?{7rNVA> LK6څ˓V1sѹ8O3h7;~ 3ؘ Σ2cb"Ձ/:-`=30s``4ĴD7#k)58GB见xpQØpXDc9IsoG1h %^L E@p|-i8rx$[-`~uٕpuA6>򑏜B*C' Fmqc"|w;zg?fAnc@ђfiW1ݝd0Lg\ K[[Α^ދص5s@ηMǡMJXƊ]]C#ic=-#RbI$B@ ̖|N (MQiBs!X4*-З 3-yF f[Fâ*\4-?0iit45R[33 (7j$l9`P d\~0M\L|)#g]C Ĭ2-!x7.3uhvp2`E^,EL0;Sŧ]I`+=QE][Rg T٢VI[fo.u-A6ʍ^q o^,}) CoI%OҘ}Uܸ?9"P_qj8ӸpcZy[s}6@_yy`(uER&Sv{r^a9% X5Ӡ`pQ9ēU{" )f"Z+wV@!WBVhs¡QHNs`ɺAmYjtJ,Egvp,o yB[nqwXIx3]θ8b!X$"*d Ls|cl7zٲYƁDJW!`P")to]ʸJ,+ q)-yB4F2[p fR6 $%b;ckŇl;$i¢*`_6J_*lPUt|S"B G,N(o6jdap"lQL-0()|2C`fI? ۛ/f xp.gw7 ZYws][z_ [l3L$t_vE{0[P*3F \vve̷R"U[OC{}TL!x"OV[*Q79 Lތr=Jٮ!~ ^8RdWN觜!^) bʭ@WJ_副>V]٘ws=_qDg2&'ӈkyx"$Ip@ oV6N@%(|g0=c*th]zhS<&H) |J6͘diN8U 8!I vmQˊ]fL[P,l ND*o!9z,'D .Jcq1|K:P,DEֵ%y퐈nR E5 &eyiK'1Rs&,^ @6YK#;D#҃&bS}FkהE7'B]uYV؝>?eHj?1-Z dA^#E;8!&9fםUTq kv)f,Iu.f)Dv-7Se m%i3a، (XDzXGOˢ-cF`.vql-mRT 8g&˞C1߶=!qf1Jv=eFY"0%7Ua"R` ߌ`MKִ]#YuNɐAU8-̫]~ d1.9?G[[#N?Μ'eiIR2 a![QҏA\+*O>;^upx.L93[io43=N' %!.BJ>:*- =-qҠc d jafz뭞\P)[^_p lDx>um?P4<@|p!{3Ɖmm8E@ϓk_>yq}K.r(v1y1(-}ȓao偔Q Gt| ~A7PDg*0@UՅ| _@3@q_Ǭuk%T04 ̐23$Sv|?J)\ ̡G<(1+$) & "&ekw@ʎ4A"O pKjhC BМ8)Ń5hŁOC`VtF3/d!j p4pJ#HV!>~igspD[`|т ⺿Rcl]*q}!ҩTsLҖ%W.<=-Y$m\`u0 U^YoYp[~K>/>&v:ml`N׃>! +VWwˤai^[ݣ;A/(F !4bh|@aF39f̮9vLM8cҮ9 v?_!mq6¤$ߚ 00UЁ͖QI'Aْ `| OLaݮ qh:e)%*qPӶ(uC[b~~0կnKZobjy-(#Ҩ|_P42;SK)W0΂>;8z9sĤAiЕ.0=4]NJIYP]9}t`VxOO|c 38+ aI]Yr?U wĉT' h|43D1cWc* IAm0o3ܫN /טɗ_@"H;%@b 1-6YZmܙٞm ճ!0LKR3,0ẢW[@ @˸ѳ.{VY•+WNxtu@djX˹H2+*K,K2N%9M!: 0xW2vmA[;Y["dh:b&r"Lc2[S,B77ڂ8\25ә,)DL#̆@doyRi* oCb'G kՔ0;BYD41CL^捌<%DIgDyP`DH''ѓW8|ĕ%鴜B^֬aZ⒄;7Jz YaA#e5,og3KQq3 0oCCaTw֯eL]f>W%~)l1[8| 7ɛqr߳_R%5E|:<}!O8WdE3ʏ]K~7V Ӆ~vOFR`ً I ^፞z84~d g oik87aQ=^EH#d'"cHt#涥!|˭'?-Cή8Xih$ҰT|8,1RUӄw} ܶeN 8 K%gLfK 'i *4d»h&44 Yݫl`3 :;+` ,X91ȝ`$QFxz1 ^!Ab<%҉'CI DGe4 \a>`H!-Qꉖ{GNa:xly)){tj.% q A8kr밶wo5Q6`; о]c6F]%_I?2__:_z#.RNus*a=?υ @rrX*)97䤴PI%L-#c .Zu{ڱ>v8 AH3DVCa1_׿zS(uI>qݡ}saLȈ¡D/o o`Yg0 $:AO0d-`Lϊݶ90hl$gHΑNzu0f|RfxD YJT0eJ,nXhv 6h]oi3:K`12ŷ3$'PfiG#hE>)5@aǡ*EõHqQF0rmI.Y:o1K75s1f"C2&z>D8Rd&.e\'+*P09'=V#xb{oiD%tI8^ 1T8C nPc78DTtQl3A Y|0-Xa+rKzDʌK)_ qVSfJCJv1-=0`(R0LAcbKlSmv(H<< 2l)Uy\̘VitlH0ehm]eU`ra8-DTc7ZTiI6Sb$)a2ڬPQYR8|w֒,qV Ma4ԃ݀ceX6e1Үc|miwD`hSg㋤-0c0+Rf!fElG:rI%kUɏ )ы |w[HmMy5k&"}H5~H7^?<#@H=!قt~OPKL!M-G,SG #8 .-[8Y A$Tl<'nŗ-|rʈ_WѸ朑Ohʪ/`FRo jU!(ZLZ7ɧM & a)s9fv-T`$)ҏ>h5QPnTE+0ʵJN~X<lP$ta.]в/nLR9Ð*[#؅D#]lzEXf#'K?H-@tKB"!8+D(C1T܀Vŗ0rE\5xb\~ڛ_7Ǽ^ķҦ|"If=W0/DaOH(u[ݟX r p: #S)9*X/[F u`dBqf r=kTCgz TKȿZV@yw8]a $uYπ}{iޟ9%  b哇A4͒l{##zpWDJad.s ?cK U޲a,h㪙~ǝ,Bܧ@'v4#tweVu1A9A.>8n[L?*RlХ h _R^*y+'H(X-e|xzѹSR鷴O jdy&TvQvEw7RCI#`RPGoi6,I5s!^#$-$@#1`Z/yK5h&Dj^:!>: 7Fԑ4T/'IUe jIKZE8^TKĢS&~5iD.ۖ23sQsAo <%= W$UYٺdew `H2g!.h8 r뀜U(4Igl`1Gh34@IDATˆ@8EIKs8]$W00 ungK N d pR6RK$?@'F![ⴤǮA:\b"x ^Vjdtϸ0. SpK׼|1L Ô]| ؠA`f1Ǭ#O =cLqD2̥3m#+jrZИ7e`Nz}'N6-ӏy)iwԋ"4kvz.Iy]supX{;F9/bF/^9H2|;PIe9m/>6{3ePՠ՛!qyId([Vʡ@OZaz $yz oPf|g^fᡬjf8YI؎it?g-ᖪa^e̕vⲁ$ta1Ce}Hۉ(e?[E!~i8zSِ| 1wNJ}e~ Y*3 x7aL$IVC K> & [Uޚ.PF-6NLM~ex423MЈX"8̱ I6PhmoƤQtc%BYLNAڌ5 Ov ɖCݛÌ3()!ʢk99yP7ʸtZL;o WaHtc` $O^\ 8&LI”O!jOq)E S좻Љ鹅07XaZ 9}> :M\GRITRDlF]M-́:vewE m^h$>pew2 )s,Vܓ1' GK%@ F܅ "FY+R!I9!T]/vP0h|h_;[`mk&m-qh˫Exʁh:!uEnDۥʀ15K =9ct!<7p` AT/' c` _TFlSmZf8AT&0<6$VR<=9ZhdcLH9OUs8X=82XeP#®v&EgE!Q[5r체oBo{W/ڊ??vmzs7g{=Qŀ9QO&?:X-R&aC˪< UV 9t!1f_VLJhy4>Nmgα?OTp^\"t}$ͲnFNg@/d2fIJeI 4ӣ$U瞿/! \grPѵ -H49YGD9w Sv=;5hxs[+-:[Se M')>1̔ Z}!e79LAڝgTQ!:$Y;Vasà~3189X$DkE$R+F´3VmKH6mz`CY@ !~7JC?*':&klƷ I`-vEtV8$0^< !_^gq:| 뮻N5k!&;Ot~Xn=:ffYdF'RnuuoiЬ]9⴩NiA%EaTâJ9R ?>g?Oo \~-d e)Q$[vȰyL9?a}f¼6Z\y.(AOCH@ >IX(qU,{zGLm=8mTfiƁDaLylȺH "HyҘ ȢxjI]2 $@cn2iRvppE$1#f<3IpИ0vl FD(\H ][oVxcې ^!8RAX(L[ W *MB i+`i񖷼ԭ |>І<~_8)h&&swC {/e P~\hߟp1]'~1;;Bva"uen㑐r km8K2Om[ {ۯ)Ii8eqczh9M>;k'(j#,AxN0N6䰛m{, R9@'?ie.([4{v20`Lr[8gEwBP W2-9>IO|^^/V%U6FvuT4B!Ւ/,ט<xn%|aW R` +7_p>B֦!/jo讐ڡK&R %3<>eit֚%Z}:qC8CrOq~X>?o~<Ի%z_mKֶU][ [RݬsgˉDN;2^&Gjt [sԨ' L345>A3[d Bv-fL)9H0tdqosA3ͥt\u^~߽G\ 00ab!` ,Sy0 snR@ ݒ80 &qД+w[Kd̉%/ J-*^AeeQeK"d AT!8  _4ߔwo8>|5H~C6ʿGZ|!#KUWfiLѲtZ҆'gܖ7;/e>qv]ǧoEXnRV]mԦ52 z=~0`罖!%-r-6[I=g C1FֻEg7]>9\ۖ9P,AC=Ph GN@,ofG@]V&Ԫ1[.Y4C,Juաם]dݫ|?0&!$mRhIDħק<= Qp% !'7av̸^` KCH±Հ3h|iE+q|˜&+4oDWšY+"!Q`SVr9SX-gўYbJ Ftw?O:=o~}S$RǤ]{Cb6TYZ\s[bW0R[=ӕTli91O5PY:*OB;k˜gʺZU4H?fvDѪ S-@Z5*A~ oEEC4B*63q#<@4'ɚi#e 'A[Rl$..LtB +[&8Yx#p|իW}G`f1}p`FK~"D!cҀQvBvv#"x PRo!*|*Փ%$E.!|@@SU+3 "h}1 y{衇]zכ&_]-^-U9I1|WRw^J'z5$jisd48 >~C醽?cx"*g5B%>S` ,SŎC!ɢe޲CTɹ]lvהr<}^an^YȡrN=2(wӭW'>ϙ.,}J!.8tdEm"d(,tFJ*1GD7$—s[^A*iQNA 7J7T^ҩa",/k>+v)/-0xH/-{["N:,`hi<ٰ'fE,!_5JNۅIKAt)`xO!BWa,?!v^:lq K%F$xW3FmI|Ym3Mω#oOz2淿m< ɞuGȉ"ozHIY=q̐gW<'|}Ih´ J&KIMUJuٔ2.%v,әc PV| C̺ nK8  S*+ Y !W-1NahfISg!͐!9vh{Os$8#U6_&CcJL%#m `ew_9h}Ϲݟ\SaZV85qK5;C*l boجͲTC7+άQD(lɨ"7f1ݽuAH09n[h #?v ܖIإ.s]m+9`#ڵ:cП-[2a @ϓ!J(!A [8 >EM*`i=xꐂWqԭAa8_h=K] sy':qxA8W8]7\l嘥0,Se/ ILLBSr=-wH)f2r0=az4CӒZ1]ǩ08_[ߤ c"?'P* Lw/ɛ"T!^Ǽt/?Ͻ>_xDw߉eSzPe6U=\4S`'Y] 5L$BV}!͞8.͏>聂'iS$~`iכ$*s!93sjQ.A]#~„M&&dKj*!zD S%e-0̌'/LΚ!.|{ʟ`d4Z"\B'_8$ 9gYLR,Hucv R '}W4f)#Ϥ;ÿSeԸMWo"7|$|gxŖ[lE%U .mB4Hr-.XCybޟ;cr!}_&Ul|C:IO o꘺V3]TKŃG'fXQK #\%Z)Z"̤ܯlxp'+n펠/9J]vIsh&<5]fʳ6|%,I<1v 9U=|3q&,EI;nʫI"fI'> AP q=:$? ͣR!a%8 uL+2LCd*>m+4b?4` i'#f`jWwpҳ%/y8,:>ZhfW,C?u,O. ?gT\ ?wS\7!v{r1`kWgJV /X. eH:$uLˌ!t҃p)^gdKJy} ",nNN!j)ˁf|8Y:}.LzB ^!Qb 13U#$9|*@q>MSD ؠ Y Iyj0j H|_CB_ iHr{ ?hS Kg\jѬ)m(GaL|+||S RUUQH/g/B{W.8 l aA]̒ReV`y]G63 B㰥%@3+wą-l lLfْ95h0m2cQ45|vBP-s*]"G7SNV,G=JǮSN$Nu.iF6Vw85S8 .'h3R,.xEә[ yz+fT&UDyի^UP~aEFOg~u].Ov1GZ  gqފŏr3c< S1M\[b^ ̗P>m=-56Y᩟ʣ"W30K;nQ nKf|Z{oc}`M 8ߞWiac ^ 8$=:uVۆMIaاH/d)s1^ >s_[QL5&Bslٖ\Q"9d#L[<s"% %_]Ux.y%ݭ+eH `ShlUE}'qPR *Uڃd,1:Gy@d0λQN4e7?]vWB, &h .^|@̜OO>a bm!Z} Éod f$>pC4GZM-a ш؏'>F"'QjG<33B-BsړGr->ND,~f#* _{l9>T_yc)n49| #Le!:0e0JJ.u4K#RZژ?ѩ)V%hCPp O08 مħ mVƽZ^4g\7wۤkս+jo|5hbu[pq4D?3H38r:CxLK| (}7bt|k_K C[#[hBNi8>[,Ak"j Scl EØEEo (ĵ6|23G8nJIR83Т^OQJS8grn)GvKi*J?իWI')+O"9''l (7MBՙcd]*ƍ!ɲ'>P#t.)_Zn_ yw5W9f-r;S>ϳED>JW!+PAeTW=oDfw=hxu"c/nF-L@ V 1pXP84sF]Ix0s K|385q[0"023|y;XH3 I5C瀐eWٲ*P:q ݳ14MAG" *"(i-,s(8?j>_dܸѾ{ڃ묽j>;ws|{lhβ*T.{L;49έ} wXVqpJ5hԉYYf.f^>+;~gjiwيr'*} 4*TDWwVh%)%KiI"h+2um`W9q{R!>j?rM8D_J^ug%j>mo ᾷ՟$tyo ]qbXM纶z d̶9`[+`n#)b\%Tp"^oGS+M`(,ȸ*]n&Â% >y)A2ʾļu9ө@kB$E5*-c.]IʌA1;G5;<$3eN*- *ƭ/˦O2GveH$8Vq2Kn,0,-`[1Ft~;9}b~S4:V$7[bAh <(hK%U }ѭ`\8z:W>DەwQk8;QiC.,2j_*#+Dww.lm#|D/$"-ˌȪc31֔Co8 we*qt"m@e#̘$*nA`v>$=DuؒA^k #px)%hRتu"gDƈc;Ai<'exQAII*Nxe&qX5q @'ݿF}BR$ uOW~#94ȟ( vמ;+& L]hgMWZq}`A7r!"& ܕ@ŹlSH?TaGHK~@r\1oe_jX5tc;:/)X/ƜMXpb:VY ן 8͚P3:זt gdEtT榀*{  }DqA866T$P1NQ$fH pA뀦.#!lGLY{,RX0;U,ɛsfpfyg68*E<8eq#hVKGKdĜ<dP^d?ekL#r1f֞{3M s$5V]f@ؓGhүa8RLȊmUiYRBv^{׉b/^Zwwjeα cխ;=quGJoǴdIi_XC'T8eWԩ-`п{Zv׼h+Frvz،h@kppMK-V IQ1rR}6(Du;Q֚A blÙM|#D0U tGv@]As29eYǢɳOZhɣ̐=U c6Y!qI> 3#at^Η)& cՒqr'm9m~|cǝ|bK!\sv{ ůhAk$`0ˣ%(4I~]!FkG4>Bsb pvOqj+pb|eQjk)#\}%j% ;oRGҋ6I^&9b7 ng\囹_c;q y?b/ZsC6$PP,p')3CvGQjcKG!+Gen?ƫZIKVq3-X/7éBEӪ^QNȸɼB3N"?0y'*{0,FmgCZ x l]=܌Ojl^'BhG<"0BQPND@e0@ZbGmZ88Ҧ]ɮ>OaI%p({9Ew;T V.Zĩp/ّwdyɵ@c |iTD[2,{|wwaS><=Q~m<2'dH@obZO=$p?˕Lbtm>pK$ b11$YS嗀 ^ ɧm9+J6Gs/#fDpm"LlȘ+JL$pqQNpBykpD(2f((HfK$&o&yLZBdpbdjڈP%F\9y'ђ2م$o[!ak"n pxD3@Q t~Ah8D!@Ǘ@tp$)LzX-5?FE9ap'ViKJnJw=I!lx hePE᱊֋r <1#١ bIA&-$ѬC[ZF 2`*1#PYobġlt!&b|\`G=iZg9M.ǸfR GppX^]a8w@Zv?qץNyrq/p`U* gih߯֏vj׻Ye/C`k:ǣ=J,"/zLD6o7k'lJk_&k~[*.LK, 0b "hdxg\cyD.pmXߋ#Ⱦ=h z{Nb~kәMȫ%_pX4s<]ǖ `H3k)m r-@'"DPyO{v1SKUKN0/9pQRRBhKjs%9 _ L;D^D G'nH$H}L,?B"6aH"XZ#&%Sa-Sm" %p,dhKzի8Šmyy$u1l*kwJ)&z$,Z8uz7 fU~f[Ed< 0fVq%,pmWn֭B[I\{~r EH D8YmS"ͬ~:N@GO(y@LU~DTh|1 'ӪBSbg/ cN-9(2죧[r8U5QA+T8!u.Q2`Ac7;8B@3 wȵ]*T+!ev::d]}m"]dOXaY`0]Y!I{xqg>@'?S ,4gX# ݭJ?>CS#slɗ%}o{ۄ 2Q?h#IDnFdGZusc؍M]* A!on~dAzM*hs]`|Zo_GMCu/>P,Uk*kU^ʼn`-!cIchxYcߪ%#<ǡbGw"BUe /-4Qh"ڰL2,1<-Ź-W|ժ^Qt8rcB3*tP96GNlnjQ>qp84kAIjh#BHIe/هEN#Žӯy8X,b_"ø ea8$W5vH4:V..eaǝAe0BUs*'% EXe{c^0Qlx >a ?ס"F!̪`/_ҺE%JW_G1?IOU5-#TL:TUV_L`]ߦ|.gͪdS^k*;~G-aižOʩnOQZ=?>򑏨B2_yN"1s&~ hȣ.gSYq4$EbI~X6sǔU#%"y}$"u*-9 Wp"C *ƽTYh3a%YK-;K8 $[kNxbfp?N2wv'YJ|.dFQ]nfY#4_̙nϹ\~NTV􀮖 c76kd*?9@+e Fzl+!bt_Z{386B=_ dؽw P>Y!,;hp8&ʋzD[<#'0yxFI`^bNዅ†SԂe+JضBBdH_Tap6t-5PT,L)5~] S! 6k`RL0`f6pϖqyZ.EKL"M#W8wVnW\[@J5nQ5t5͢ ]yUYK~wkO6ZhZw>fK6) iaGoHtq%0'>/| 9 J>-G0鞗RE(;c)id #I_X+v"w,?|j).8 xfՕ:쀳|*yǟQlx 0k`a kVy7L/;s݉l6 E2> hی,is:O8;~ ɠ /$* wpu^ћ܄/{AW@IDATձO"1F"tpp 39q -5Tv< /;f:Ŕ0TK´^_ ,xD!FetvW[f;Y uaʶ=-c^u£_`1 ?-K¯mh_PSAD<"s6|itO5=&<0O]7=|=*${eM58҇P~|U0$j,y?cz@ntz(Z_9aS;n(JX@:(LZV+1>}03bc3N1e0N%`XlA" P AsHc5$> f@kbaL6}&y3I⨬%Vz/ӫ+_BSeZFNkF<' f+`sg[`\ő5BBpV~5邏I ﳋ$(K+%hI\nHBvLˬp3k%vRnQ[լ>$c\DzrvTv-7n;D-.% ?|%/41fWkPn8TUεH.Y}(#mbQI7ǁz;4IJcߊ %̂#8h.-]Hh%7B-!D(fK˖xȚT;ᑰFҲ0|ަt!~̕",ط ޫ52GeO+\l6z1H]]p˲㱃^ .?8D|-`;60-F1lP$t ᠉-وo) ?NOWL_ѡ/o>$l+rJʧ10=Eb rA'dq*I$*f_68Bxa$ :YFu _We>%C]s{{G]MQ4Xp872.iQ|;Xp"  =c.1+[r"@GIeQu 2/9pZ.1`STCSvc8" t=2"uh^p&-5 v i# i2`Ԏ0 Y*@Q.6 ?NUQiT8e$!7Sw$K%q Tc6KҕpRLԐ@%@$еIAF}҉L A,>bNY&qޤp)!V6~GfHg'.U2kBL}J5;ϻa}L 6xT8sTÉIGK>D( Ԋ<69۪pe}R-dgܶ/] :Ū :4C$_-vʣrWI0LdЁKa%rf0?ω`Tw:|iø=FR{,.$@wČ/{@ځj9ΞDH)baĔ3b3RAMv{ &_F{{Ii ́EYc_WACB-yi]R~ne9=/ m{!x$$ >rDܡ/!?\Wz{ݰ;Td_P`ܝeQ2dʹ;2܊Q{8뜶!1ŵrtvJBlZuj3K"meDᴊG^r 僊)S! *ldz=.}Ui).S(l7x^@EH{/¸Pi#xx9,W{D\t@|ݩNd |$,9*ﴴ, KNsr6+uwRTQhV-?NWE1B)E)Xa+Ya9?0:m*}\8̺ec'eÒm&8HpHw ~ (.{$Hxg#aޥ$l9h L&E܉ $q 4l1#\ sA vGfM7=~߾0;^>Op>ʞ4jq4 1NLCzwKi{إ"ҵk6UGo`uآxf'TM0i-LZh*>#zȘuP?r$hǵ$Z}*B/uJ /Hzg͑}bZQ{3΂G2?wrVdCیyӬmFrI:j4:hLUO˿ĖΥuJGji JثX]qjRh,I%B{1RLQ;F`Zu<93\EQNJ1چ+1ώd#cx0ޥx]&N4LYcm{UR .GLZ#, Z _P.I%qKa9eqP9GfT"kʇk1K68#&[2V7[h 1BԱ2CbsfQ/u.g#;R*G*Sf_ȶ[j2ui)kAc<#d cΕN)ȠUА7VGb&1yTe-`fx$ܽs<$ fgZ@ J ,QJ$@Z萑"*VY__,h{e/DoCk͙]#<92 hlQSuTvf|P$f\6ɒĩ‹-1l\fw(^oM"ZYk}pH P0_ L[".lE: K w pqkC 1 ^X\A[d#wl:!6[Rs2 t89!sѮSvڀb5;d;x qimo$:^GلDY PA"-I'0…=)2˱s.)Y{pZk ʴ"\Om$G5%0]a%CQ&޾EM@uVK̓13hV+(1., Y7V;fk!Q8h}BIbUF!쾯+*Pd 67G*ٝ}`ϬGQ8ف',Fy@a;UPmo{izK4U~ ;4RMo2 1L6gUm0]8{U8=я%xIWf+8bjPǴt; lP3Lv0 ؇TѦ,Oy$J)$m6 G'v{O^bo0Zgf/`RC If!.9'w2Eƹ#-w#oK]Hʳ&GPmpAlmF;DX ݱrs=7wrQ$^CheYuBYͭ-e^ul{l;볃IK_F^<Յ5Fg=E Nӟ3-pȨJ tEºHévrIc=2 &;l0'&M.䉱@0E&ϣ`Nۏh9$ ?v%U'o~Q.~g,6pˋ, WBbhL^|[x~|q**{*d]HW'L5F/袕5 WCH{U`F~e008֋:O=d5XES!&cT!aa;8&OǦe 9Iѻ gS?4:(L} wc2N.!| p/ zˡ\,ZSLQB28,˚Gu]NX@`s 6ax(% 9, }9"# ˃fCW5K\(=ICbSKWY1)8H_M9OpF@Ftg.Hp~(;n}("a-1KV]V]=FHTzHޅ Nd$>_{ɒ(8M!I˖H$#of|^h6?MYY/GF@_=aȩ/6Cפֿ))WXs3+\ SpܴOd,QaU0[: {EDkAw":tX*I91]F6ph'8uÙ׍ "z5Ĉ_^CfX0;aAii\bq`\` ;XT,{6` ӓN ^Qq _SeBvٴW36A0$ӧ.ӓrYG/f:ט2!0QEQfMt !t]\I&u ۾'qXԎ^]#\rzj~Ё8iL MX-$Vǚe#/n*-b8m}D̀ }r[%YhNKox@Yb"?D@BRha 6s Z3%W,a}K`o u2Zж$cOocO$F1H! K%*ԟ}ٽ_Y9 l :/^/_` Bat`Pd~5ۜ~fę[Bd,k Q |2 3k3T;*ȓCH/m6B(iT}NLl|&-^l02f}P,d/1bzt3Cb2 O,`l8Sͣb!:H!OFPcIHT落S*V,;;Td X2g@\ y?$Ci؟f(.b=ډpdʕ#* $ [uK2TtKHU5"&yGt*.=o.`R_3 !H," \ Q'LqFGp[4fu $5ڧy$]3 !&P- \< M(Ky@ȗCH<"bջo}[x1!k>iJFjhzN1AuFUL0ci:ۖPZ֔:N%׾c_/rm  :a lbXoiG=)B>˚0BfmA9, xX$[)z!y!,F;j= A.dp\Z!ӕa˶BEj64䌔ME'4Ah0ٺˆdYzX%1,*IZuя~F]%Sl X Y|I7BI UnWbGbz$CYK9L%MKsd E݈YB:)d*4ѷ:6>liXc F8f(ٗOfJVLQ!!>$cS~@I@}Wǿ{t$r g?g՛mE1u1 ZŨѦAQT ZJzbl8}ã?ՋMnbq"`c; AOHzE]U!*ALԖd ;d K>Å}[#iy#,>xە5yY *t,AH) q%$EŤhgR,ڗ9f_%O$Dl0^{dh3PшYK%H`V' sUPHӳJ}!R9y2[<@hzͰy|Upʃ*TI{c7/w6L|ȵ0߹yy"f t8 r@5,Rt*3jieXz@$,qD$TLaFe.D CEı Id)#fA,q1lv*!TPD/)+rɲ*;r5)5𐧮k$:KNBR#a*jYxdDX\G_6kPJ/ `GIv*A[U~6z*ZJ~.\\kM,o:[B_?inРDlG옦%9M+hP0p8;"'HfJU0"@.jqD0*Ieu.aDX#&9FFվO`Y2c Ag; v5rQ]Cg?YF86G'E UelVzEUYx{2N`\D*&]UV݅CR#njp{I5(Q%I&ɣcS%($9EƆh!ffnR><2b.9fSV ńP=TbTR  6 `ZD0 1 |*B[kP*|ʠ|(usB6v ,0T:x-wjp&C }ΎA$Lv@pUYFs{HA!,60؇"N ΐpm/aBoF#u9aǏU9V x慡93(j!#NY@{:j*W%pk"<$RLԙeUcQZ& #VGbFI,-VKgy_sw^ k]]὎ZE\}zPSbdy%i-<1B%"-wN})#6]u4[6X?S |rGȀV3a*y#KYjMxC_j×N < UӲ*UkbΕ9 ],;e:L|6qdR`\;Y` Swukx8SdpK0`s:Q\0XjT5Ò/.Ul:x*Q8ix4IS v6EH1bH0%xqM a ?w3+[L6D<0U2h4:-$8L)$ujQ |Tv03)E4J,J;wt&$ԝd04 N|>mZ% 'Gnӵ4gܑMH!FE2B%LY%/pM[N)M1J)mƣþ#o6x{+b4uz #lt e)C E,ɵY!GJBZvZ6s֤ˠYW1ˉ_-*_9 hn|K%Zy&~үeLC҂#N#%e2ns˶Z-f()y"IGl'$ $+fyDzbY pPt"(&wv3HeV+0ٝQIrTS%2)dUiJ'ҭKhIY=P2%QLvzp%}bHݵ` KvNՑ)A:r .EJvhYxƈ 2#ji Y0f` K8&桂f ߣ$YdpGIc`8wBX*TzEQe7Q%GȯHỴH7 V$ NujQtEHIVb'$ QzͲZ #>2i2&L0aP2ʼ[e.Hھ_-bF]e1ee\׉Wrj/;x{`@%]`\Q\PԹ({GD57HȈ3 P0kz2Ew3Eb8x` ȌC+-d>UPEXvy$# _0s5؁崓bMSG07路7h,H,ldk`dYaSS"D0am{V mNr|3E3uj1;kY3sIvsF4i3cJc};!NpÑ3ˈ=cXRAHF2o/K *cz<'>ڌiSa t;ze$b Zb9%, Z(rM\ m/~/۠ MEfq$q|ߩW{Y^T1 S(XmĔ͚UĒE] 5 30ĄVbC9^+%HBQ؜֜mI;?eYh%vNLE``H3۟  ',aL< Hfr壺!~3'}zȉyd[4uHoz^s 3vʘ`Y, HRdN%db:1QsJ&1 I%6(tXvʰ$ V՚t) ,|8/iYRASbdt&:[9 )!Rg0ějK2>1l'IeQP! {*PDԙhZ).awvlmȅ)|GP #I)U] /!x%p2wH Glh1CΉLLԯƘ ^0̓sW-t5{=ˉ0LDmaTTM+`] ɑsC S#$c[p avpZ+^0KՀXyHEqQ!`0!UyR,Sj bW} ]}_##jFUQP}բ*kc7h!rAA)eLuϠ$]NEITZ' Hs$/ L+d=:T g/޵2 wAQt?c[A#~53.s7>uLrNNm #SiT$"B*oΦʘ.F]Z7$*ԥW{'EZ6%kQu'~THa&""iɌ=J`q0ewlQ.'t|0mRɢ4qNB*2j/Y+sFh qw#XYh}YJZƸ FBr*۬5m r^L'@>"Д8MWӄ +יbmpUwBԖkzUX@< zW:p*qvXS10$|Q08p J%:3ⴰ[,b5ȳQUd|S!r'=^RAA!Rd3qJٱGߖ,8ri+F6=8 D,SX'I '#a,(8U2|'S>):b$@lRרaC!8mjܦ{T1!z$ %I(mb*BIF]~2Ҷg.:|Q}BңアGpdX5 J#G^M<¦ 5U٩$ w;*ADwv1h‡z <$Ys3EGV$2!G&|Ȳh_)r#P_X9S8B,l'~UTX|8CD% SKL] G"5{zm)е78N _Y %%bI+qPIł'щ%LWaWaT{<QT?p[0 s8E/Z9$y\1 j&<._hs|\QTNb Ӯm?ЌT^*f٬ ֬:{h.) rbPDMR!GiRt,e}A9UMLJS$G zUޠ9F/7(bc`zĔ8 DF~j*˘y@ Ub(F?iu+ {8 lwȞ.Z$ݒIE6.KT?RƧn:k.rm &Ö6Yvo$E4T ^ލO/ZÉB7SY[rȈ=ݭc S+GQ!e,p̄=bC#a,U)jL ]ZL*d>GC;ȏL lzU.\uVK*GrE&)$, 1kF{l^1嫸 jN+G2-$zT.#2&ȧŽ$u@4wNdc3,׿zxM7vրҵdBtΡG (Abe9#8!yK8nHY&sgBvlI`YmI1Cbyx$ W>>H +FuFo{|UE w_BOk)cIۭ !(˿2#4k !`ztGUf|"5KTSЇz *uIU0[^xT38]MZ 0pU)}þ,pPXۈeZ/(0SoGTl:[{h5J|Y :&"~YXH 0H$wYE82["V!2<)'DMzJ#xƹI9f@N\sE  >Tflj_g pY|K"s #\dle[eÈK%U;=V#!$CmOɑ|U $W^<+: Zdm5{[W+#!TN§~(~4*z(5{|keQhgx3˫{"#RȈ!1p%3h6e/;%~!Z8Wrf_s/n`&;tq_c3&#fŀ3<-z=kE*> ZSwn/ v|Rc SZzuH;]xʻ-CK48K@ز#9qL(ZbJ5KM@ S\Sf$C`4vNwt 3@ҞUR, OBä 8 `udI]8"щL;]2U/[ ޅYr R ]% Kơ+rѽ2`{f9*G35 (5߉;!ebdI8NGT#X֫,9W[&/2#8|Q[wfx}â1NrK3];r V+Vݎ7=alR ^Ӏރ|BkggLJ0}c2wa%apѲjiw8%f0h7R\S`xP8",dIAuKMEHks$@-{tݐ(b6yD%jkЈ,i$ GB ̬AF"A $$dl4SbAHJ!$06eʼn)eMOZ0!QkvtC*)0d(z٭e[8Αj6LG TQˀţY` n$Ki³DRh>j̣+9$FM>qQX, : m+%V־%Wp~[tfԓt04ѪrdD`zGmQy*ےb|A7Ħ3#.]U$>_WyPD\ta~[CSkhaVf x.^xy!k7 /E$4bzXLZ{hPe@& *b:W9qd \{Tغ~nlsTKE })sG@a=RѾCee֊tُPB SU7EM-G#҈Ú. ,vGCR UTX[3R\OFR$㖒2$9 Uc2Tx/fʡ*B ;wL|FʞMœ#iԣ `X[,NXpXc-ňUÁGz15.pz8 L܎êd :͊vfj:bV"RgX8?ޭTdMˠ B晢XW::T#ӿ,9 ?~7 S<(k8b/8[ue4oU{ofQSWG]bgww׷C ;z0a1ǒN>&C G0YwnSK ,5C %>1@;#@ntp| %/iGoK#k@j=/(i+8LviI͘L!-jHATҨǑvNGH /B"LE>-**d4 85}3#W MF#1-Cq$CbZr>j|iT D#keXǙAh r^Sa 1 ` ڳz4BGS< ZJ ~l_vw(֖Kt~8x0uL,ĔY|5k%ϬY/fxU-w~UM_җ|6`@-*S:VaàKo' K6- Qo>@˾Q7HMh9ѾMHK&;{r‚q' ?0N劤%9A8b淕ea#4޹lgz%Gs-*iQt +kFVI/'őxF(0Q| 1{D~l+bHQ`aN)ª=;wQ@w%*#*d;/afV.KE Bo% }ը<3 ʀM1e0¦**B$ivj<×*M!,p0٤HRrH8V1[2:q@a,Ƿ,RN[ Ky-98䕅i E^ȰO Q)RNދ ӏ|x̢6о_u1 ~jΕ"殣e S;rB8lz_qTƿSK^` q%Ξ@|HWz!o#Kv00(U7;ƒ'j0tQP<_7>w !Fzqj4<;vfͣڈN@~0\ATtٳ㘁>̦$](oG.<0ukH;v9-MZ*G)#W-_" ÅWuH2 $-`$!x)^.>kW'DJՃYZ%1CYi~DAqbu2eH4+$Eʀ⩺@%F`{ xjmqB :2 nC֔OhQ9VU!U&>ho&,ڇPUJQV$谊#BqHQ*sMͳit$)/X6x:C/ YU}]r2InYW~$ 'ι ętn{=n)B`}2[SU@TY"8K4. \q־庥ԾO$iL#Pޝ0%{ij"P 8S?#Ќ0en/3.>y ~55`MuxoxWlHEl{в1$#;Iw8*E/q1<#AԙR[tI3wb@ѣ6T0h)LB6ùӀ%ᢣHBLk1qNArҕɇJ"š~iEY4[E0+ qcN@8,` 60($M/_1Lk[/fi\(gmX<],NTXlb tAN+} +KTȨNLQ|L)So9uÉRaްu9qvyA}bT3$`j=_SFPļO%_嗿^2<:&!'WF G*$v-1.Vе-4_ეY|k_~x_` wGΪƑPHa_H!G\'{-]LqGH _tP QZ"/*wj.MdZBVZ@ʏ:nAEd,fJa9] g)~c]BHع 0Hv85cG)2J))"lRa 2(sʂ$2#i5tagԪrbw2(ELAb_uGiA֬Rw3_ri\ hv5SĘ5fϦC5G5a8B BfpQ عr<*e%0 mf0Kw%^|Uv # `cn1z? pƘ%$b7ХAz ]<Kg@h@fY"f55~0LYeD댦e#t:~"ΑCӂ'5۽x#/Ã<NL%-I]fFd#1*Yc#Ns?(K%3/R$n)o'_ԥȯ,Sܰe;Pׯ~jRDՃVm)f++29+p!ZGFX,CG|><''Y2t%]t \ ֡—j) ,.ABu %.q]f߾2bf@+p^buǛr C-K -r0,,KfABJ؏RݟIڂ::{W`5kI0ioF~+_fǵ K^&;_.2h1F͛f4E*E* K^!.W0(@Q1*@8tʘŷdX+dZ< [?N`\sM_ƌ6;ᚘ 90NWn aIHnfA o[aF -GC.^pq1jCą/#;/꽫ʼzvpCxZd2JӪ h.{-|$-VfU J7jLe#?~iePY/wrE#N`Xc! Td$x2B/^pR,sJ Tސ1;1k籕'p2]4ײ&eꃢW[BP, (F8I5_$~=TI9ʎG~3 c `pvvvw|#QB 9]`΂٣qbY^}Y !XC?h븦-I ֠{CK02hOr(/R9gSb+ZsuGF\>)!6w8>c4K&ƻp< t0W!ā'&`$[Eӑ\t!U W Td#餲AҷCfQ[aZTi2LMsf8~P{ucQf`LnEuUgL׌I %RbԽPI9Rcv%fAu OS @6Z8Ѭe #nk J@$iFˍä兌QENDpA( 0E.Ř0G6LT"цإHtŪ2`Y t UvD-<2bkm>q*dhIIJug֠uoyk->&j 'P% ZGsa kNOmF̱#p9L-̒p@ET'%*o9GKS.ͨScUuM59e 01LF\gZcUXv`us#ňHS2F,2uL v/^h1lb.LC^63'uY#.WAu^JŅ@,F$o }v.e"nΣ%дX0\T#'>QVScz)3t͐xǰ-&8 @0e!!V|-,ĩ̄N$A+S8ږ˚UdžJԛK_R'eBwhiXSy?ɸVbhH_h7sG_IH ͩG@ +5[ஈ 4m[[e|#233hՒ,vfD5)ZgAf0/],6>@^OQ;˦ydGbmGk 4{Dqc$3evOx2!ψ@ʭȂG -v_ aǣ ʕv&JYūd^f)hoJYm[;4ςp[V!LP5Q,;.){[߹("2HRA$ 7U Lɗ}lŗ"IVGRaJz`wAL]`.1!$ dD E,D~Da lX"?;N,C sW3%q$kV–f#kAEV ';d("TT 'D!P,= Ze9F9?E*aoW ؜U#^ Y,lM,FLs(pZE0 `|oɣZ! (G?m믿>;1XJr*Ve٦)pX3.[_ڒYA&'693R(L*P+#dr^"&§KRyu:Ľ $U3 e|5]?ULhMr ªG ?./>|m|K<:'pH ?I $x,`6Ti DJbJ[VȎa6[BC0ø0IW 85C:@_@??p{ZRŐ59U]2)S L1Ӧ~Pmd)@Ւt _=qZR'E2VVq1LXm(&ճwg2u)嗀_1ֿT, u^-%y&V1YԎ G $HJ~g5P| [# Ĥq, h :/aLZ`:OH.RYb$ N ZEʗ8'f)zi`]a}5ǒ! <#lY@XCYth)[ˑYe˳<*&QmJrx4:V*nFY ਗ#3#]l<KHxTdc(J.eGA#7x#a=N|m5>VeC/^2kebP ͎ ߌ QhҒ%ᭊ9¬9BcI`_b,dѕaaj癩:|Yr* pGϟ>%Ν{tGM0,@b=ƿϯd,O@|qvʼpx%$Ne$ (8ՌP^(xyђffPq^0ȸ ^K ]`bMzCAbjGH8-U#EE"(QиXx5q}dY i;ɳBr$q6KND tI%eb 0E8J޺RCbD{OeYp3e>,b)뭼p,5k 8HJ a.vt{lSN `Ն zv@T ?Yk_fFTՎ]\(\+D~Y2HYu҈Q4뉼Xͦ١evww%ߌ3G`'yW8l:N&&w|3gANfݗ G .| Q$ȯ,1rջf8Xptf#9 $Km !eV'> k=2F)9崳3V} `F,@YRteLD))Yބua%`2 ! Z ik6||A v$1LƇЃ泹SL0T,E' d%oWj;|F=* /Z"MlcŒH%MNOw$|{mI:<:"YGDabQ5NCM+X؄BT8u ig5c/ZCYľ26g Tk_Z8FdI="0aʒGJmwؤ~&2 jLfNxÖITψ˛SF5bfFKJM~)z$UUgkYv_J`1$ NfXV%#0B㷈K\Bp {Igsi -QGsĻU` 5v*%pT-c`2nE̖}dՓ`NݡŒܜSţ~/\ޡԨJBK*U'U3FpfWs>Aa*kV}-\mrrw+2˿Aݒ‚'`kO:w@߷O &0,3e$25P( y2IYnj5tP {a q=7t%S#ԭ(9B@b[{Mr"ƆpZ1M@U2{-r,;$&B$kIbǗa\*d@\ieIUg -]6B[Y*8ȏm2WKdxqlE]T IM GU#"-^X&ܝr-F(,a`J #8, n8U0Xy(:;w,K/,;Hȳ )H9!d«b&;y$̅?ܼ|v!+֧C  ΒG1Y1ecøt7Kꌖvae%o#B6Ƕ90U%H׼@0@$C)x}ckC`Jnjy=?FjJGQjԥ3uQY%FȠ!O=*Z2ZDگbN T )-Ch hIj[/t}k,s-L ђMU$`>[J1KA_=Qkepl4zDDjPeR<$+cUXU6EVyAHcxZ|_@Rg QZp[6Ba0B]DBJVjG0nS|iC ~3L79' nPGFMW5oU,,@b])Rasnˆ0e#JȪBXB҂V|*ʻMƚ_#7ƻcXX;'Yfe줄qߒ]@b0NRټe(}>z<t8&F`Ppbʰ= l*U 3V Ç\͊[,+;7/ָPNIe8Z~JG+28q( 3TJ/s>)ObCC<y4IGdW*(,daӬ8JKAYD YG63>};RaB%V6$R2*a '&RִEFT-ɭYBHMhG08>w *8.MN21# Aւ ӵ!MKw8hL6Ưd!e)1]IHޠk1:bNW!n&2 ѱBY%cXT"MV%^*%k+D~m(UnMZ퀡i郕ih)t5)ӵN^kl?٪6άVɀIO&N-^dW\[%;\UVvKi*#ʬjF[R H]Td Y{h1t%Pa I]W NYkJgV|LecN`:tEA0вO҉z_[Z2o~%!a| 8P9>7e_["FF뷊).^E*bľ _,]ofrHTXhashB.lLYdl,G~Kg|oE! J!" #^U`ȧ$ ^hl[^CqEHؠ(ѽ\[cVil_sKA%TWE+ TZ`4S28]l@2"^5$تB]6yg`=%]ȋ"( defCe6:#N&9~ݟ`dN!u0浱i-_B@"-ZygLBT𵀟ctqon m2)i,)WŲg+ۘ0qh'S|Hg]-X h&yPIjŮĽ8r/ ъp 7p^z_&TfߒB!Wg֝Ƕ\#H^G5 9¯1 g[DY6o|IBfQoKP:{nz21$(2K @-/ο.[*0ȏ*Pq @,]#Rn ![DD *7u_ȐOn_y m,V)%Ÿ%j,J•æGH22Π[dv"\p@e֎@m1; r)Z/LK iziQPc@``G8^k5񵚪$RARBQJU\I ;ePhH&G{l˘u2"г |ki-e qPeVexՒ>9'QF$|GG02e^׋?&?{5&a;> yr>WGh.<]t=)g—48՟*qI!K^ h*B*rkMU*6@@VJ/y&hL;z!Ne# VT[ⲳ (=#8TT';fQćqP=A"-|ǎsg&2yXT)y 2 ^_C[~þ> LHpfk4a$.-{4XYFGT+IȋZʪk 8Z+oR$Te!5/RJ>h)0g0\:&-2^wph&$""|O^<,Ix єhC 6E|þXW14hIY`_5a'.`#6ŢhN),G*Yl5G,W\pp) {Bg)7eMTdp Σ4N10f5wvv< ?#ƣSg=eq ځN [˧mfkJ\S GT*( L\ \aU[A5LQAKrod,W)(S'\$f Pd$hN7|{bi"0=&&|Ǫ;N*2:-8T:',FN *).H@'cAb;dž#HH :;fl3׼@wãyL{ux, 8a.ffJ\`6B8CaZdON$D%.q(cI*6}<A;p4\fKpd_,(ma20X#hޅc f6yКvv>s^2)ZG\!KIgS <\G=bdlVAwT%qUhLeVK@ ͨ{'P-q&f2c'B%ώ<&:*Ͳ/;Ef.) $Ef: ~W`;>Qqsήy@j#I_+ * Qu%jA--B KD!|p a%ٱ"%` r > œb>Z 3z E;\*-FFEEdG\)" RD<9c1%בFe^$Y(ˋvEKۡ4;|J !?ԕ#oRbt(*sBB Zofr}'wF>19wKyyW%=ε^˻tbOfqͣ =J$ 1 Pq쩃)|׃܈"Iv0fy8睊%QH*_)Z*\%1D1)uߺYJ0^HPXL!@,îy!-CKzBa#qa`0p1L?ge)=6.WK_\`ˈa*nU VFSD;.$ Jmk}ݙӊGk5u.G22NxD|Y;-9$muLJؑjӣBHu 0"$d\qPh{Y+}F6 CW3&FSTs YÑZccS(It}<"/f|5lVIէLt;έ*@C>l)^B`3A$WrQLUH^+JZ I6 !tN2ADg 7t-Xޭ{NAT+HB -EVYT*K% U D*lr%0I[@0bR԰)aiK8$&dH&Vm uqNgsXAU*@zid꼣 c` ,]B,y0+?ь6AaI2~Fe3]2fT 3>P&3Aoar^>&u'pÂz@ *yDxdk=Za_$A#㘋/$A¬TEɷMQ$҂$6WbeӐ&FIFصıc Ǡ!8Vk(N\ PQe\llI3",/Dfye?yHnk$am&Ur T hg[t@E"BQ82)S xGf .T_0[U[Y?Qr b2 'D{p@,)A3eP0n2d@2cY`84/) JN.?>JZsF^%CnyȐp̚y!/h3E0^h1z EAOt}_߄_8OAlpq@hII`˞IK 1 -Xvǣ /9 VWiS 9,kyZ$bMDe)hN C%t &\8A"!潇ko~|Hw&Xv"1*yFe0Lc/q8/`Kr(FMO8dFDbNIhKa5FQIRg\#CCd|x\Cy%$mTPeKT\h79"fFŊxYecA.d.pJ_KfAdT“YH,hՀp I}|x yAOX}hP҄1*]6ޖh V" 4,|@#H XT-,X$vS(e0̒$fnX%&$ ǰ5 XzT^qp@f4'wY| vA]TX+A=GuPqbYDv<0 By13UggnmQWUK PQR̂ G~T6cuW ǒkՑ8ʹUC> *a6HOw85q{P:] 靝)w-WE6fY&F#v @%hGݴXR%a4.F|ADN\5.3\SP|,(EE!"nQe̛ۘe:g''9ֵGv9b"(+T[XqMa%#;QE y髭էtd)XVQUeˌjc[&iB%) [&ԭ)D^=m/?h{@i*Q"3E  q@,/ Qu^)0r|o, \逫 (U@lr+?xpGC,Á}Y"@Qa2*rA29x&*PM)Z)zU,հ)wJz"[A$eת՟۾+klkD]% Z1ַ@,c=n>U{1Y%{>ʘ|Ϊ ViE[a@ˏ[f-4ȹmoՋn d$dg{!Eh{^R?Q_ z`I{&  8^!o-=M+.SѬ4 uC]=I|g aWU2J}}2׬,eLymq82#(26,Α `n-n87NCEP|!nX,@\-BϬهX򊘢)3zuresh ?` BI#&.xZ>ƀ=(/j xZM\8AUD@j{/2 bd G8 ǔ$A,Yqxh{ EIJMdDUU=w B1r$!b! K 2cFDOe $ ˈ tJ0,)lY*J-HR?K2R&'  FUK {FT֭4ۉV pnp65M*;u 6ߒya5S{AjDfcS2Ss'W=85 AR9ԪKp*|W_h>|33G*OCB4&kL@Cp0dCp?u 5;} 4 Z)fpyG^{5գGtʖa\VKt]>JMiȔ{4pDPB bUY,3 f2|)cLp؛IX3&Sr2%VY A8SC&P"m`Ҝ3vPD4@YU)PL-4+Pi(n!C%  p ЃݭVD ViP% KcEt%k.f}mq"{l.ho.h'uPpsl`@npvwzQ4ɖ]P p'З_~TZ*zXxR?pWz$! k$Le=8[)}뭷ږͺ:#(|qq]|9qD=wY{B:̦%Zk,b̬ Ak)EN@ráOu|;JD򌗈Q)L@!4': ֢mEpgQ1y'wF"8!5g֨Q+"kr/ԝϑ L]߬{AaRnc^h~>ujWS[8H$K`Q#=>1gS(/R(z $R@I)dC/;F 6 Z()&GLlxQivOJA0  !1aK]o8d'_%Eq 5o:M<%eX\hmEr+آm=O61Y?V )j ;c.AY5|tsDm ēW|1Oqc*  (Jzѵ:@̨! >GCea)c/G`\BVwTvC_vy+g` G %Kuk}lE>p ѣԕՇSC%npsp2*8nؕJbD@Ug Jb,/8J XzB&f()Ud6mX!8vZwSF/hJ =؀(nQƭjJYf uSйgC;PĒlG>Eex>d$K} P2 zYQ<-:r5>Hy-Ҭűw@ ?܅BDt0bA&kcCJ6e߲SsKJwraT+)#:h [f%L|mQF<32@xq'Fì?zٙԘ/Ƙ0p5% K+ݾ}ۃSĂrVۉM \ HZ ,TJ_fb#WDUeh ZP-a!sr Ccz2 AhunI-+WR%e1lc,r CL53ZUA!3/YFMd` ׎C!-l\aU"j)`ܥ Am \ĵL:Arkd1&%zFXk]+:ԑi>ى޳ g%E[1E1ől'eMa.Xbqՠ1EU7oݺ|$K쟛U+HFJU`Dlp4Ȃ2AрlP*A$(:ܷ zLR<3Ôx$/+?w}K1k a`[1 X)#`-: =/8ZvY2p@^vd!Xj !{Ohi<ǐ)_:b(%4\(F! `eY fil Ay?2*Jp>C%0ʮc 4}D !,+}K݅,A. Bsc1-ARfXlp@-5Cܰ5K1b}0Z  La┐F2JҬ)><4 !hXєTǐMA0 *y|p\XHD40k͗z!{VS 4(R2"ӡZ}ޔS_h G\':O8^E==XSa%`)KH[@ǁYmGo[=$['& [2{]%2* hG^RUƔ~iC6JP*i@Q'xOƚm(w7]6/8b$6|5T"dUwJ^C @lYB:m&Ungl*~{p1- HʄK3Gd‰ˠHʂkث!_Մ NΧj)8 HEsxεφDRI9噉N{=]-,u DŽ!IP q=~ΙRɔwL (,,d\,1f=;)il> d |`[a2&H,42-MhE(y!Li, ǫp*tF ]xfNjp|"1R0.AKխ7 =9̭qy|9, @nìnMw\Z$ D^- J U_:x$LYrWBφQhhV$+(39{GF٬EBmJ4B Gh0##[8Kz6Wt0oX  C{+駟VRD*,4jG]A_qh<r+Ş*%dljV yIfŷ2RCY3i$<^ 分u' 4JWcl6w"FvDZFAo݁sAhCcý"2ɹV#*Z9J[DF)t T{*Gp̂r%Kh=|2P59_ ;ðVafuWe3ӱ/G+BaP? $c=8dNO!8VB[,eaFÝXYl(>Fl46h129$H"C@=cWJ7U,@+ : =,!w&+\d+d锔EgLVqzs0A?+Yԁ3ҭ6pPT,f #m0fU*@;6|;Ft"`@FZe}ʲQ86$!9;1DL)P" ,%n=h#'ڼ h -A\AP X^cV ,ɨ|ApH aThL|9cc\ጨ-`F/cG _A&e$@w%S֦2x(LMunh u6s˿e) ę ~cY'=Ӓ !%j_ uHDtm5]-+Eh wf|`sڌMpuhJi̵[2or1E_OGzJk MF4;J_:q髏lp%|ʂ*W4p;;\!ؠyÞ/>_'+k<1Q# Gډ \,pWe+AcE&aqU |j! F&TGQ%3%1CQFfƓ>lT%UFf-dߐ(  "W?G6|1`2e׀^LlK [VJK@Ԟa)i6vZ.}E@Xt@,c-2)[ʫ.!$tFXin2St^aTvE/qyɑ`ʁm!@pZF6n*.`5@,-d[eg?QtTdt,S@:G:΋Cҭ) M`fa@E .pOrQO;3FPQCAl!(i8H02Gaxe,! OqiذodYr]pWFUAN` [Lr2HA5e S|,TB'x& mvK&bowAL|+Yk֓%pz!C }M,tt3V;|ASzW6OM3ו)8#+zTJKbp9zb12#QIS:A6 p܅0[qM*څWS8, dyd\1׎Y3iŲcW5ֈU\KF5+./_k\9 n]EDѩ/_9>SZ?L j*@VvJP Ec5w)S%\hRd?JfAQp/YBQXWVSAhCAf rWZ1kE>x)z89Vvb%Qh( nUKLJ̪6RqK JEdZB\CkTzV:4SJ:VH T}|C Tk 8~!D"Uz>Bs׭|,X0݊Qy h:.ݕqHv䂪j+4:v$I@]-7@`D)4pT$XI}<VI'B˜"BkvXM2IϸmRHkw 0r|y3SW\)1 `)R2 ʠa̲ӟFqEX8g_鐷c+?ń1|lfD8Ȋ'T^!5*eu8F:|Gj)z}0%9Oq5C=d - c%"%EȔ OG 6 %gy*YA+sŕ %I ~&䥒믿'XDrd#eV`̄p7`\JBUBAe3RSA +#K-`,_LqT Sܫ Ti:Sٸ2Xn$q2#֚֊],Ơ(̊_|exkB7%P}7X Fm ̇ Sz*(&X׀/V(qG+"XJz ed;HJD A9zgTrMKFR\V4H$+5{M@6WkP8.sr2(>)Ȫg49DY J A%0ӽ DZ{(e9 s idn9ˍY d |d'.@ѹ/Lxh\8be˦[, >=CX.HZNQgpt,GupՔVBhAo8tXU S+ P4H\,!U^|Ɉi)_Νm'b {j^a~'W]F*6C2 [LX!$—(df6FWLK#)p{= G.//-:/PjmTc."@bl,hhںe ;B6c/π4yBDB%/2Maa'(4N7B*Cpu(腀/4Xh DMY =q9i,JUkY^@IDAT6ʢ[J/IY>XB, 9d}*+ޱE -VnY@& .IM*1[0:>CD4,%G<ɠ;⣇*!æ6pksk!J5Ocκ y<ɑ"Yn ;Y$Рŋ$m**(O\\U~f5KP82ZY#{=ED ` G-4+Qr[Ș)І3|M $uhtUlfN`cuN"'3ٸm(#b"EMle32,aƊ:-3+%(M.b9#^ YHP|d  !܊[9 zn ʈwRGca`Д B }-Di}{/b23!⫟{X@1Ɛ L1=L NE&G4 @#ׄ_=`pR805eff ES+.2Đ*UUe VKJ2/MJ%.5!1hXRb * ܝt&aИŁ/>p IDs(M; tS*@,(=TOrh%W­i H*Y= jo *z8R=/b g!Kبh4l{Qp#G Kl ddR?'cմcnѭ Zv4^ =(0 3,+ AVYMi!C Xe7{e`ƀuV˔ZX!@*:/8Ҷ JUBA2˚A&>Ѡ C1=Kj<(L +Sz`VJ3Q`y0^±հ;rt"m3Yo =Рㆠb$bԣ!lH7zía˅)4\( 2;Q劶\pFOtyceO>y?_D*qŷkNfGsmc@ !+@qo)ӔRAP: =)mʋ'fY 0S^"`*NFCsAP;z-(yU/?0Gۗ?b m0qEBnpQX m ^$R '#FTuVCs]:D*_2^\ l '#Ì^8((Yŭ!G^+2VҰװ(dqÒ;p&+gmˆ/=/n rq[ 1<:LU3+x- K}O4pޣXb D4\ԍW=dҡ!҄gCD)MP0]r|` ͦW7)[zw9I4)6FEߵVm'V 3Ȉ8vFȈ ,ràYY:Sa&s099*ddoQM2rXQDEAɅA YfEBP>=MʀFF+; ҁ T`/04 L1[g!ո5ȪAİ :jb;ǒ<W҂^%z-j8Yz7h <~76gp-pz饗\/hȘp%}&4e Rv3EGK,I͏z,ӭC\0n5uEOB^'ˎ=fKFpc+"Y6GP2pr(⊅ (A@vLϒLYBc,Ɖ_!4d`JA鱥$@qiV04"2w([)\) X\mɷRs9}䈼pNU800e!D4f>dp؇GVj-=* =F,(B(nT)aLfC @:S3#[{O|CgPR27HJ8P9 QuIMs*Ue%+b&21+7Ef!F>nG4k@`P] VQ rF$D0dYZK$ KJ]8L_E',+e,UR 4x5!4E)4Gڮd%SU(/%zOC(wQ.jvܭ|?̗,56 l?vfHEC`4n@~v NUdyCP+0+m~~36銕#jdBjEʢ> eOuyY=p.Fpei( td0uh Z\AΑ=zȀ;c]Xhdɑ@*ZG&B`\ ˲`D j.6"p@.% |+՞BVFN4"ׁX‡{N[-QU'(z5 ĀR% afrA{QCEfU ͪ!}C4Ms)UAԐpE$]k 豂&)/)!x0P+Xqsuf kpGAѻŕƕpiw=p6fsVCV>"@A!fܮ^ /T (bA]h_P'{rU 5{8,Bpdp0[H``((w^\d30%diY%Y\t!LSnФC4f)VY0  @ ).{;dBe]sru: ,7M`?5W;J\K=ȂZ= ƆNIh6,qƍW_}84ʂQօդ"g7|=s>ڏ.X"#}T%#L>"MI[RuP%"ĽchG i 3,F%/+| 8zL UdLnfe־ TRjp,ٞ@-rt\326*2.2XyQJD,S8aB [;HdAIHD.UO"6p(%ߜ 2%\!J%Tj JMА;@C1 M.z  nhTp lhE {Q,nX&sJꀆ@,q\T[&ӭJqoFK%B.#Ԯ@*?sd>du 0u @(@fzHq'Zj=r| o6ːQseH@͸^B=xEށ<V2=ied &%"zҲD@dϮ2QjMIuUC=L8JSLXJN >f#'+^/zZ{*g/~åoE`v RdwkHZYJ}W,+/(©0Š\LƓXp:k~7?tᾭLXDIS0[ WRz%{]jV_tWֺn%JrxhQE@ J[ -KŁV+5!|272 LEOh &Tur+5) L7| V+VM7luh]T]g #*3wo cdJ!dbM\mUlO )~68*; mHƐhɴg[055[vvpD,Jd :h+btȒ1 *CYUi<,ɤ (9"g抌dU4Xz*DZ̀.^mSԔR=1haza4K66824Y/ ?lcU\WY}RidX\xb6[l.FI@FFqvD/V +W 1 LOXBZA%`e)qz=o %(.fˋ,"ǖ`J nM]N04 A}(yQbƗW_}#يl̔Nh9‰9zOP])M .(u&9)AJ-(v#d-/ Tg<;;U'KܛTSIz IC^HrtdXBπLmB]m(ZeɌCDD7KoKlŪP1%tInJJr$ŋs70pu Ҝd!Dd@H_EEi0|kuڧ@y9TC%}8/ 2zUƟ HǙcVUE,\ tjZY6Jh^kh(TG*9>4Zex52P4HLJ7bh;G4x[yUu5eZp\y5j#7kJc xJ^PQMǞF@&x@\:9Tc`@ rh7{L&K!f,aM@  r!#>K.U *nAE@ȬQ!JniP1 (nuNOa*4)Y0cQ"qׂ31yQ, c{Rc<t\8!05@+^'d ^I#HЮ~`j`^k 'HAIE0EI,81 3XT;*I)ʸɑA``1f:%q-(48i8ȒdR#@NmUh $bD 29U@5JJP[qBB$Gdt)48W(W5^ @n_)#ɀUd(/);!bU1 [q-"F?q@XJSB>/[}n)y(\u8`ʲezO&AEc {Z Mѳte^aE M,x1{! Ұ$e;D%pXzKD4f])MĵbEe!'_9I % @V +얁A)![hd\c +,0h{[zK,hNL @Q\ʋ>)vE&xxRP e`ܔ.n^[_+.e61TyP*!P(5fܑ!G$K%% &c1Cfj"*x+% X8$0pk2f[/(]1VPL*&+_ngh0NW W^Dy%Ho$ch6o(W2z0kO mѨŪ洓tbKjx)fV_,u2`AVvrU2@!tc^RzW6Uu"7{fhTqTAל"q^f"o (-|iKEFЮ6a gɽ+|i 15EC}dBǭ>wK=xN0XPSܺ CL 4$[UccH5,RPl&\4| 1 iƮ"X!vn@hTP Њˀh<ܲ$V%{z+f$:ǞjՕoacöq 14C"W[֌;3?Gg/y e>6ֱc;'GY&|G)WU;hںujH0=dEhB zI!mmJ.l?f8zɵGR8k5T- _ V pn%Xܭ_NOӫ4刌,Rӫ$;ݪ9  M7T=| _{{׉//LkuVpv!kajG,|g?2PdG %~,XV4S~ “064Dgoilq+(J+ |e@:`q-[ YJ28e2>ﺆyLtlXjC\ᄆc֠4˗/#(c`aƲWM譽8AL!aSVDBB^8;l/Td0 cJ b1&Ul2@SȒ)6nDU \$|1Ѳ{ *e4N:6yRT J dXTOvbj% J*YA˅, Œ=|w]jM>nn}T"IGmWJ`yPi2$ g$Hv+#fֲ1˒jWH_ F#))] d! &GWBVPC.ҳ'7[hf/SA00J:qd\A@ G1(^a0lX2`&@8AD2cȱ>y!fe@c|=ahjS֩ ØAOqUV.,3ER,6AlZ 8`; 6p*[*㋏̞a)sa[fyp1"\<4 zdgVl$5 čє@\4sy"V.`Bq!kK8'2F2b""#G10FY+xކlWN k`AsSw::]*@Yt+PG#{EkE7+SIEwK ;[ddDc# (23"g(Q 32fh%( 8yKmͬrKVU+#j>e]ȃ!kFՇ Q (+8NƊ^rtk* ӟL{cK܊pT1Q:5$(dMCSC4Hʭx#RanrDƭ٘uU= R`B#SsŅ*E 0U+/"$v ,2R6nY64:0FC8vhXj=rB ŵX]B;҈ml#=FJVc.̭ jwŪ+pCE L2P5LtnM sڢBdQ1%) _U4+ž8G.bN#kddW{;mE#h>!V_{` ape&@ڮlDE3Dx9YzsoMU5wf!DC{ LAlA X@ 3`1e5Z_) BȤĊ` P఑]P Vj MyG |DQ{(^Z@<J'$PW !"d!0D赑;^"b)[ Lu0 rKh\} 64v:%QDFBsjHQa`".¹v|adkp$Lt#iEޗݙ(#/NZyP'ӛN KhdVL#40Mq9͛7}KfY!w#Nc6H*3ЂF Bs)+K~X8v%Vt!+%AHz̺Z. =dlrȺr0EFd[0hnݒ(hw?ͣЪ&V-b+; mSh*& ^Yf(N%I5AOmP[ %Bဘ-PAEĶԤ G%}ཕ oV[SRPl Sn "2? K:v}SX*#{H MOJ"u5`O!\гCmCV| U*%u_L@)) FWF,qIX6²!$ rdlͺU=6ɦĂimT4`gF&(}Ab2*./J}Zh 4Y ʽ 0."`c0>pΒa*zd&3b`J,x7:\}>N:^b>(3IP( ]\!9xt1Vs3/%LA=e:O.YY+c-YhpdL94Ƒ^)$RtA|]!e 0P1ns|2vX8%ܶ"ޙ];vBU(!Y*y$(ZTH+:r0ĕE=Ş)JEIsj\P5ҋ>kh1[kPT f#g鳁l*RB ,BHEA0zj~a4A\r%Gȫ%w5U*SllWf`ejh`ġu2-CtW %қV 12± X4f_c[6ǜ[Q 68$і[J} 6iؔҭ-+84+rKIh;p ^"hP5L@| Wi͏.!d\,x*(` CS'fX(bW9*_nE>R1[8x0lߢǒ" ي Y 6 R+f %д,rWvzh!/Ȗ1αȮ+_̄!|t =~z{ȥ0 N@ V+R@I8 J8@Pmճ)+X)e|\iGE4mBARv@ ,@+-cƠ,-%3qWWGX!8(Yq Jݺv|Hm`4# \4M)@DA,ۭz=,04=V]A8b6r4֒lDi]T 9Ru+(A 2c_mTC}0s5&VY}CΝ&Tb0jBYY fzm6Nt@"3A cF5auLjs|gYiK+V'PXcX8QbF Li*\ȌlW&/D bs.cGR82?E9)[w)p]D3V_S@:@q|de ,_fMvS%8[4(EE"(m“3ogØ#=4 d YFn3@Һ lYx @Vx=Ba>sJTa,hwpsL>8 Ot$BБ\PG\ǨY{Dž3vPe|xb d)]a8nE,A>}cbpL͆IC.5̤`nhVWNSI Qd j &`u/\G  e8RTz}J%]*a$yKEaS73%☗n˛ȒĶ7ۖLډ;iqGdD[aXYӴ@, Ȼ"WK\,yaR4hLкA3W"|cnp]-b<;wp0f[x88\Gp 6K UZ@ @c`Ѱ1 pɆ a؉N:J6a)l?,%b!A$";| !fNTB`EXRC@)f|}Y|,3 tcbb*"Us:@`o%|^FC`5MU:" z3Lm.6h˟_UD׎0 Ϳ<=I0k *+(*HKLVHp5P):q( V4$[V@b(VM W Zi8,{.ΫaѕdfțVhLuf@osW4Lҝ hh(={1fE$H:ҹtz^^IM4ޢ]Ł5LƎ QT5TFAo9UϢm~Pd8"hW (yl`Jq0>X"/W65j6X{vq]JPP@ D +"0\s!"Kt  %KY34s%S2 ַ,3S" >ի3fjYͣȆsLYjW ,[SW 59V,%J-:Ƚ,X!QF)C5D18!C g7%Ok2 L79uۭ@;R]v`Ov(E胵)v `RS$MXr A鲛-坆b˗s2+= !--ަD.X$B FleW'+BO{ԸV}̔ WŁB .8[UJcP 6 @m8Lt ^l\eue(H^TLB\ЈMM apz:~b`.0ѐKeqhJ94{Wwt w5ekEvnV@vH90[$I</xՙ_ R` <2ފRPMX,"*` JDl HYRsIq'Nf;5aV4$h96Ny憘lBYX;i fT׫CI35LR6eT rlb° [ !%[a& !*AS6*Y=-0hJ^R*m^X^T0rʔd9"dX. SR AZ^T8#.`Sr %NWoYBc.鋉sP!ʜy lVёtiMB:ZFɯ S5 3AUMe!Mx+1wgq"q,%"i:?,rq#P:{ZQ!=$\Ok`VؼzCǂAp 9dM6%h+;Myb1W [JsqB$³(5sZ92t Xb639M5,` Pcȶ3٭gYe !Y$E 4b%¶61 CufJդ駽;fkN!i^yY&E:^`g%)<85*{ycK/̅y+Kx Bf?`|ϗYJ=B˴2 wV6cbr$J]l9J,fNMal{ r~7ţ)V4`52 _fE\Tv0&*oӛͅwAzHPSdYSz)_zgr,+|vTVO Jm}mJhut9YQ_ *r*"jO*w4ĉ Bl(Tf2*AG A^8Dz\]40O*`4kwAPzߋ`ɣ5F!쐻J #lr-V"V1 >=5ҷ 66{L;57Uo:p)"Ê#4Ua $6ư|! $Pj7& sz 2F҈,4e@OYGDo֚k;yUTlI+8+dVnC֛<<|SQ٢> &xPG?oHF*QQٍl8n6^zH&7! u@0+g\Cf$!MD/A`&xvM ɬ-h;! +`)k>d;PR(x) 36H`hl`@` 92 H{=+xf"p(+0z! 9FPdIS!Cpz[B\ %/M#1VJϰ*&۹/ɔzm@lQG2ic^k%nM_}Uɯ ;&%&k]0vP50ꫯ\W!WS<#LqCS4,iZrPҤӛ64i4!Mњ"_G׆KISۈhß9C(~(UHa5w4f§$3Pc4efS`c8w=#^'.@okIroN)_WBg!*}Ylُ+53_$2{RT6/ [n fO; "/UL!10MQ*^['][7'XOT0R˅C0ҤT+2Cr١eĖ-U f+{qQ U0Bغ=Eh Rkf]9b(HP%Y1TFNFd5ڽ5n!gS04!uǙFE 0ې|l3Jo @ j,R0Ɛ`#UXeyae,=~-6Ne\T)$GNyL?*\q:CwTqMJlZHH/3L$lkhdʥ/;H!8Mܶ 2+Htd `z0L9 Ϝஞ;@IDAT)rJp+0[#lSO iWsJ)MHb؀]iRdl0"f5Ґ 1^#-(O'4%MIw8||ŏ\<ٔ^1 &eQd.JmD,1@V2y+%.,pj{=$DTd ={p*sTshTʚw^Lm_txbcs~oI}p;2)1wJm>oH*׬*ͪ9;SN = &0G!+'e\ZJ#NtY(s(] 2/iwSy͝ &_)R+!7tt1Sλb˅0!@?<0ˆG9%wz !{ft{Tv 6MTvY[]$z=*!?G38x<񝸓>z^㢬9E w (`J&SM6]0}\`s*ZrH{00'GϔShp)l($ʚ;J44lt BzTF4Ku9FNN#wDb(AȮ, KSgQ L#éJJMIF7IӥĻ Bsq [&7)zΔF*!0&J<<ȹsi1Ɂ-dGݬ/xlrms!5REy[7k(^YyܣE/Z9bD ?s #镱l CZ?Sm5$tlu@h)둛9sy(T.Gfx脢՛5j gT #eދ6_4v32٪׋^ f˙B^Rj%Hm h)-|,3i*p @J3v5aSفZC% vEY %K`.-vW{O_x5< jxM`6*VN*ҁ=I.YhOsdJ)ahJ.zN 4MM`S!Z&Y"VCy"*CSsxѢx; aNk rX\QYTJJ Sv98&ræxdkn=nlŅHpT.8tfZWvER5a[V; UQ}Cq'pf{Ĩ`KrOtQV&M| DC$uo򎙭 نd;} G`!4' !\wQL# *nT)N;d["f#Q`:~; p" 0$;s^0` bW) S5 Ler̆ ajRUM=`xCX`3{Ưᙗ!wulhPdf}l?{'Z-xJ{';&$ @r0y! On@zo#{-C=ءh9w 4l z{ð@F`@Ҵ&-(f'8DaB14MKO/Q dL(<ʮ/Rk0MI*~ی̋PaEsD8+qNUʲ`r8dKLJ>o-/3(big" giJR+X) 4;`yԅmhi4g^jǗw jf[ U|>!Gj2 s飲f4i0`3Ӵ]:K9ѫ\Bk+ӧ3y#! *_Ot3Gsu4Vx!X͎͐ek*}~~ַ8kU&|zS >>}.Pg^ď0~;hJy1Ħ7U'dbcK])lYΦZyS\sHa]okLחfϧ?iO0w#W2\hy,&"1Ex/JpjݹD0}A&$)cY5C& e"K6<foy_ E$r+Bv'6nY* gCl1w~ڋ86s&=]djȩMЬLm>P[3@xTXJM/0? ]HzCvpEOAȤaf 9 |lrWhyQ:T'0Yߐ>a ]KDN`%"/4c +4LvE$60!Vqކwü֦wyeo-*[ڬ[[5^Xou>:i.ƣwmc> ar%!avonЏEo=JmQ|Q2cÞ3͓@[l2AlGb}mL ?+ʖ|8O_MǔHosUf:V֝bHƶQah+"aJlMjtmP 5j4Ǐ2Y'%_rdW]MSfx RgRc̘7{#,R34)GvH, % $ˢ5ߺp6EpesgJv0hF9M}y/{&p=bwӋZv~? `>wܜ_Yn&H;ߠQ=)Ow(_[S*%Y5 wrT2!@i H8 3 [5($bRo(*}N&)zLbv$FOy/0 @-f5054.J67Tv5뢣t yu1zYҊ"7kYMY/?.'r蝲hrYS$kH {tJc1 `|!XX4q„9FKE Z$HT*EM,U8zC}`` wCI<[2_zSl ib&˝ w_.3Gm\!y4E `Ĝ8_0tǐySzm,\Ӑi̭K$ @)445d*YTV9} =!*Lc6y4.9gdSmi"ܰ,bӟ1$X._grKYM!Svy<\0Tɞ./8sE-0CZ+Cε `pOvS q$24&߻:HAЕ|p1 ԐQr̿/GR֮T.) Fϝ+lhkJRxR 0ÊI{,u&j92 ~AB𶷽M̕bw/ӽzM־Q@Kv.h0'*J@0|5cλ"@nv CAX_Ar@"0}%RuA`zfEC  5ǔzLw`=oWe`z d6Z0_m:A`3mа۟_M9eC2@(Slf2Gd~Ǖ0(5$5-+2Fc V3LHfa, ޚv p=pxR &3xQElrMR!)>̓ןC,_(NU4z<9͹g;Jr#J1lyg2s-Jp_#`\ [ Bdq,H#[H$NDdRFkn]\F=6 Ȃk&,|ǘф䣆;3wPs!p!$l]843%V˔_ +ђC ^+VnaI\fί[[|u]b^>wp [ӈa@ Z)}C)?񏪄eNƏ\dˆ6N>`n߻bC讔!~ "AO/CN ҏ6 Z14dEH!ÐQҐKY^#wx" O#x&\Kg<fSȆ9gQOn !f!p$EN?g*CSV[fF=sۃkBd-ƯpC&U^)6 _zhWT40S9JA#΋8]X1x[b⊷r:MεKS_<~w\ ]#ObpƬ/LT/z0wb?w^%Bp'kc#H\?ㅻ?|2%ڣu첒+WFIľtx?~eU3ȹ?@k(;'J 6Qɽ[C"~1OeD<̤I!tCݬHkY#N9k5Usk$)0̄ tn=@dK@23T`"Vr[faVL;0/ژZ lV)s녚 #?dH*A ˋԐTNA.``YC.YGq0,N'Mjȝ∹p #pmKs-#&eą1%ېl 0j`}%$,~wQ)2Gn1 Z׮ ؔTl1"~}Iv" ݳŠ+#zHS@;P"DBؘ3QU%Hw-Bz`1KAx'V.d> wߘSYrT@;69曫fyh Ob>mAIV.@PPՐ*kR#xGR](ZDE)Or$Aސ!d'o¶:iC`Vhmf6|\S* Óߨqhodu_-7?</;ਰϹ17T2~tdMg{6iغ7C`K-ApdB 0Ea~N)`d^0!rz-s-$,,贺 D3)|SJs}e컈hP+eң%b( 7=]Y`تK/orܼ1!E/!GzW޵NBػod(eVLt!G(R6Ö'A3l0W|0kRe%h5m KnV/6?pKV!6DwiĔZl<-)>FEb1,\$ S]*P=Rm6m&_Sy&FAƿYP6KQMA.6cß1+QZJ}Ca/'k6S2f2t$i3o5kmI-*߱a+DPm4O/fۜh"_`U`7j=Wx2WO+2 ^0VȄBʥ8%Ŝ{eli!N2/`\oyRe|4"al};_X {}O+v'*B ZkEB[w!J;ފdLI_rjS6f'$.}_kezP+f"1LPb{{0GN}\H/X, ͒eA&-G79m-kPk蹨^ rǻh3lx[<iE ^`.JLPx FNz24FMm!QR"RW)+P0iPͼT2̝&G0P)0EB*!1h0MϚ a*B!4T($h4v'(5ȁUUpmnaxĶ}7m߬̚Um)݌eEOk-]J _/V֔`$-SOA";4Ql"ɋHN" '= #t(N@)+vviL :ٶҥ $h߬vrh'Fn8QBoȪ?ז_t>L̊Wyacoۻf/ s󅍒"\]])2/48T$$pBE(i,ȑ *.)SoϜW%Sa:{25esͣj0rT@ykJ!۬J =ްEcV4Rl &*}& r)qE6'ULJxŶYCSZ`"Yfcx;axҰ6i+2)Z_ƿ~yٝЭkVP m{6 i,6GPMe)a|i!r4iتy(eBn b#4ʿa›iyd$ 1=KM:`U'^Ս8VLYFxVyaPjzE0 Ńl( [Wv){ftW3z }無)$Jؼ4r~^< "1Ae=xG<P @/rL2$چ4٬d2=r<<9Ͷ4aP 0ȽTPUr0R54f`ޓxZĶap Zр)[VK_KQRUمi,*Oђ5J zUج\m ܅,/qx_DunEeV[Ml]ZIJ0eP 2LRX:#RYE:.D^val[,A)H5uS q:t:E]f%\l$Jm(5,C `5F؆1TgGW& fVvtEbz Z+G.ɯL;Hb 0`$M*!$rO*gd)Kj+SJ!QUqJ.e5%* @$ES7 ÝLfud[&rĀk.x{< s9W^y-zx0aٯ\£W6m@;X)mv;$̳5 $| zHkfb8P 5٩|lB)nW嫘L^ VFH {pH3W1̜zܐN18*~ND;A^R) _ nqmO\Thl0'-BVJ Zk֐9fMsi(u/Ȇ#T,l,Q&<$:Д@zvJ BcÒxlV)E](c-HxEm2N2sz0F dJ1,O)kcQ>T GƄ[hi3®B 8mY%bƪWxVNpU爭()B;!~H 1"J ^0ؔ"Mi+!* d=/04ZCzBD F#k,iJ [~[aÊ\A*LT k.MQrd!aaUŽ̋KC [yZq.@!g0l0V~'H:hE33y__p-6蓅* Yچ4dhxҨ,!YEe`4JєE`2j1Y J!Yg9ƋLQid< sܔ=++)y4PXk @ɵݵt(M ,f$\H܁yᎹf L0g" ,0-M!Q*y/jLzS2^_ ۓ^)1`ju?)s ٜQWXmC%0wrObin*,eʷJU#bX!-, !$rc]jZ+>V]iO:2{v&^v$lƄ^ gޛ0(5;R_\kK5^ LvHa+N1+|Čl f't`63-*rjG}( ;LZh^0`>b?4Ȕ%K 0ZܑkM`-[ωbE&X& 0NM0Hu@V%& CqbK:0_./^Ɣjqb|ul]=X'CxCCnxH [jwZp21_EU8dbR&4󫯕2=6 l LMJpj`i6^Z{-l6J*](SS/bzs\`@oV0*,x~_O' wH nѰYVKzެ"|FMO ;P6Lr> *yfT NY17Kei̦7L0*ȟ^gkcHejh˙Pڬ6*˔L |}}øMVlx8<\0EBҬsji=޹SsBZxx hI5QO=P=/aWv4HІs RHC/Z杠לa UG1 \Bb& bډ;CS! 3= zh 5V~v G40NS D|{Fj ܉P!!tB`NЗuVMOVIGaEiG4fv8my?pWgB M[>歲PyN)(Ç}pyKīZrŦDlVs kA)3$(Jc!%G4vT<-wΔ47َFUUτKO?mV1kfθC<-TEsu&K8=w$؊S&* Mu7M7KdTN?3h[ ӺlHMTMXfh}ڄrKNo_Q0Q)0HMsbl В[ VIãqNZ ;` oʊ )ǏX^΃$n\tr8RUVW2g~b.0b(=FvG&xJr#wÊ_&x&I<Μ iZCDjb; ̢5K>R6/W^s!b ;߭ߔ c8Z?gR䆐 "2$-nL ²5Eyk3MiJzHz)I'$4``S_dͬ~Rk티-A{#ᨋ fƃ՘l'XnťnRFV gv5ְZ2RT2<@Jِƍ v,ِGTun ,aiTd7}fnWC*3%~!ǜ7?InM?ϛO)'ӓU-^xY2ɔ׿-H2s6o-hΤ Fϝw>,k3Lv4)CVEДsdӋG+#Pn.Nw\C`"ʬ3_ Ήtg<5CjJxcD꥙*"_nUYBDzq/0/$4\ C/;vP[eiǬؔ ^ 1s6:^0;02U4.Xy0HJ#5t<+ "oďS<_9rq W7cb2Ɯ",Z8MF5Lґ$EGP\p<[NEV| *OE!)}BF+NTs/;o \w[-xm0QdJV\ */`Lxc!fV(UL>|#uS=!~1;*za[}`VP:\h?V"r @04ۂZ0Q=} JRx0H^<6C+h(_^z$)j囆 iLCr_\Is' 9a4w4 c֔0շ,5C䭫)%c‘f6YWB2TYZ)zTeoL_7|;][ΜǦ[לO~~'?׾_W ׹`/p*G_ukn)}p[jD<6d5Ѥ/ڂw_H2z~Uזebfי0p" 3wbOCR$Kᅕ3.BbH)ҢR*B17EAp_S$sJ kB39=*Ҁ[F^f1qQ (6ůVU@&B[T^ưhF2$G>$b`\B'+ }7S%- f| V;c+-02CJю4Ci&n1g ی-#eȦƜia͞M&3! `#L<@}.5Eno޺3mqmz.5tT nJ=SEk K<~Ty1?~CH>14f ƌ͑œhJvZS̑=L0UZHF_ڗ( 1-3qڿ_GXa2tN|x]ֶG>b-}`|VV{: ^lyccomH/$ua-y " -\J\ y 5Żk!Ý|}0ME T;Y/bPz7B:3 )ciX Ҭ\KֽE  'G/q);0=W0r2|P=QqDCT[ $\8uLyE5B˄A+PL1ZƦz)UmK `d<*ʂ aDİYxh52fYa#q(/~$vȻtl <2ujbۺa.e@ޟR[]LK6۩Lc(쁙Lz U|SK%e60G.qr|߳[ .U5k21fA6UdaXOn k}OP_eȨγd:1ްVn0/az 0,, et o>hĥd.;Uȅ^7P0VMV`A&+Gӫ7kUP-oju\nxyojo c ܫ"_j0ȥYfJ:6)26w!- 0CT9g>O} ;_Tx_m$ ^zɚh$~Sٿj%YS%<59,Å[w gl*2] f%Z0B!Ɛ/{MvVJDjߋ?KY$Na(67`˗ ! 1X$\6`F\,S0T 孯Yt'c_H.ͮ2 ZtYoG<R0f pqπ CdC;Ӭ/<+bKXkr,0XVNFZʁ9E(<-+0Mil,&[B; ?~*_sm T䐭: s ۑȕL:%.m8=ńt4r`*z!Ng?;o\/)Kٱb+/V)LA`vbĆ0E`*#-կ~CǾꆍ'G7dޯVO'V\oݦt"kN?B) 1%; RؚJ-<`HX"DmC]$*/^VBjJo}% =C-A]|(\S++#A"r"IidE#Z`!Bգھ;N`.2"J YBl}Ju&J9<^$TQ[Sv'Ҹ79 [ 0PM!ե)}%@˵ PQγ0]>c66l*;ʫcn{X2YknÊx1|,Aڐܱrxo=lJsU͜bz \ڦya Fcx<%DT^]>O|p,ډzY5E<}*6Z3j†/e ٪; #`9BbTyHSZ0K܅,8=a) .lQ"qB;#0~..Vъ_Tx+%H125%"} 4+:;T< G66(Bu+ʆN%~e!,n"$lFl|Ѩ%6]/la+5.p2 `¯R}]G34GgR;iԁ̱Y\H`Wpuje\uDRks*ږ4~RkJ)2Xlllz 7C]gyR˹)Tϐl d՘9 %I+{0;\VIc0wi2 &2v lI G@IDATtx7wY Gε&1{Kiőpv  OBrV{/m5P`| -ə^`4G„[K~;4GMد I:A.+Wٱѳu_TJ 4TEF2DYZ`PkC).CC]p\իh A`l«j<׮?ۚ^Gw_"I[[M̢U*Nf%e(b.HVY(5c?Y/B01iy} *vܥIw8U[iʋw'C̈́*f`dMń] ExKM06r TLRRQA\3+pSv @.0zWi7G`dEZHJJBGR' !GP 2BH0ŐwSL#*JEN04ܔVVYEn8FoȯG,k?lC(;I}?^wI_valʐFm Q#s>_yw`sYfem/_1$PkQLČ*z؄![ɷ? ť"17 }yA M=0N`uŵq%BO u 0 ArT37*g  dr`yr }+x_ӤS" g攡H 0zu e[ ˓Y 0? y;'r9ZeRF>ٖL @0L#$Shd$ sZG˶pV\2&[c2Oh"#XQ2YNڐoFP*=hIul@bflYJ,n/#RJd rFM)0XQ.}kƐ>h-NI}j˔[I\dTZ'͖rw z6 B0iU9mƵl\xئBuwUa< 8rLC\0ČD hH& )yD.nv*O)5P-P:;7 `qgQ(#kW[pv?+vT[&v# "hPD_!Su (hDMzT|O׻9Tuxص[:; pZxW\:(shanfaϪ%x{J|R&Ef&5sͱ!my$51:u&ʛ":Vc u@JDx"ۭ L`pR锾>DMz@RMa@UiV] L|:NVR@V4ohepK>4ӕ%'[xbnu]xaEi*($Paom/A`%qiJՄTR&tJ]1eKp[ x7x):On}.4f(~V>'J&Je:I,pA(4.{lj,8:HR` a=v y(8(nĽ߉&co2!IG:Z7Ms(0Yu*+H[F! 3*XbÛ2`W^~m z+]$`tf9%ϸE7'Hx\8ڵ&M:)^&• *P9%Tt+{i.}#!<;a-ѷ䰣T86Tq9B{J!`t Nj`#.#:)Yg"} iU:6T`+(!Dф<ԡ@).prB`[% p8 Np"CWKJ"&4}L20ZFv^ޓDns:cB9lkz@h"ԟ!'n)cB8MJǡ"`L V!Dڀy9+[rz"b`xV*.4%ʙPߪXr !y'01-l E[qɅCWAmaW o.TUU/pC$(b硴S@ñ0֭0 ;PWg 'f5G+,c[ɚ ^K|5.'N=rrAI< VuppypsiN_n)g+zǷJFGD;C=˙4 sB{ ʜ+nB# $R Zڃ'#~(tЇh"Y 4Kqj!5r\9tpՑVk4h&9!C)==neJ[ra5sj*\TAhNhLYfuҡ|(֜gGI-).5~PAnP0!o'EwkUhW B@((V JA^&SGbVs<0/|#< _/"!cm/dNiN* Mp-`'"2O"> Kyo&"N "L$gd*TVr$snrdf >Q!\~P4Gh&kA'IhdB+xHĄ9H$ʊx`b‰@<A;^ijqƊc܃߄[ ijGUy\Ր`ح&Z!0Mި 6RYIPFbrl=TT1XV9**k,EruA e1@ _PMW樑?F@(xP($j ?(|k '}~-WGqjtH{ lл@NX-jvjc; W39/ _ ؜n J^iՀMyS)>* *q o 0t,$Bu4S@Jgbs>z耊r=ބc QlI \r pvCdǰ m vZ,!JhSK!RVY o!5N6'R׮ }G,l1 rR/#(KGPE1Le"Ic9s~AGm)S6'n!M+Ur-}QNAVJ<$Hfɱm.:[Kcs A 'R0`,}&"'"Ű*b-39ܲReDNJh"sBO?e&:䘤̿Aɝa*? .4CBW9I~B Fgiul=fӗwP aM#R,4(j#1(!6J3~ZW1!FU8D_IsD*,K#~Z$?EY>uJVcr9r"G]"nd jIJiӱ$ ЧTG*lVAWUAWiB.AWd+Dwj*B(58D<ɳDVQ K\ h'&)"p,.'=CnA'~d[G䍎VLtjP㪊5igVndZ†L -ni˨N!5N@-[Mh׼ '\8\'o$e"D)Px5!`Ctp s+4@j* +{04&q(s{G Pyf%.7þ)TD卂(ܿp"K-z۪DinBsnٚB"LPaUY2']} eaf誅O* 4漁j)RɄ*@g)dK$2,.M |ʈUIX|N:4 Nfr1Ġ9#aX3\t&W6oeޗ6KMQP&+ N Bk5 WͼHj! xXbb+%] W0XHcD,x32'nICBHE\W(?mPVC liU;Jt:x󖚜Ur 5{- eA M 4uH^^Ã@ @I'b1 /}v=SJӖC-{h]~΃aU)҄ Vd鋫2U ˝C~j#J2!E@­0pBDŽ9h{/q$5@Lg~D&sQL }Im [g4Z&,4\,A\h;N@ŕBl@%}& ~͜4Ž_VIz`]Ƀ6)2Ě:r z xbX J g@O~ f Ռ2d *̦˗ ?N}fC"ΐ8YC#,sT:̝>2H"HyȅNy&"C{2CJ74jRљS֚%׸8sN+\ <ܴжtӵ\=veR`RqYل"2HA 42V Z HC+$:8oHZV8zVّ6 0`Ϳv7uxi9$8%xTDsn)C|jsi!T)6M8 b+QDGIE"tT8gW&T-&*Jm 91VOr ҕ@r }M̀+EyDPԙWQ]^`T-!V5-)ˍܓ`<;- !m~׭:&/R*%s/Jw'[n?ν`_|1v&<xVCkei86 lpbnDŒҢ3oUʨ,e趬ACD4˕K~6;DteCZ9yni<l Q]`n0њ!M!P{Ï""t:[o!U[~0U7hb T@>[H9!o0-t '5(H& $B,ʔ&\vtD[i>jk‰g(ϼ^jƕ0W;H#o:x1?!=FCy%jQKq%4%JAB+Mv+ iv1 jȡ`^NZ$7-!*A x888p}/Xl?+Vqׯ3$\$ZE l$tK-n-QvܦiOvA!$?%|2WV䖎>/2tKF; WD`4-)5C TB(=Q9[H$TY˂+!Li=x(-_aڂ7>8} s&\ zaxqKΣ8Vx$OJ 8& 4ݻW^.oq)Q U,pXEWKb-!ĪQ .rDӟ:\Ųi*Yr $t hɫ]sz֡3HpX[Eͥ`2T(6>ɹȽk#Q;z-[+Pce r>IYE*eNDn&,DEz3吹"ֆPXrjp v,~K4!WӱD 3++N5wKHT!\WM2@RքRp_&ʎ[0fk%8сiR-kI "2Ir떟:}D"Cr@udśD\0\d(iP'\tq^iG~Tdwŕ>Ҙ (W1Etlqbbê8Thf`":r!5Q$n84?W G}TWp.x\ONN|Sۤ'tEo_ѷA£P@ޠqHH5uQP`T{a’_i@C5ƿMD\D)آ&K.(y㖄C=o@h Ԅ`*\tB`&aee@rpy<:Iڰ;Ny)@Pü8%LYV ;( y{SX(XMYjDlsB5_#fe.A~Е-؝# =Dt;S^{P>Q)n s*B˂+VP BQjw{#C&`&J[d}RD i>>с%=# TSR"S;(`-+E{}%)+*آ\:S)Th MOGhOD{a-B#&sHLTJA:9{Hc`OCJMb~9P8qiȯ$V~:&c0N88퀋w䔵Iq+C#{MIs¥U`PB0jv$v4eĐ>zUt`$rFQ)NP77C BlbYebHPj8l z(!}>{xq <;=i2)3QDWAfm̃2Œ| ?$0آ4a ΎD-zSbēO +kq bՕy-["#pbg"V}QĜ7CDe5܄a07閎 oK[m*}ѥ*Ƕ!AWd*X $!皓:7$ G3&:H\A<,-1@s&-1P腌gi2z@۠Α3؜*. ['VW>Fߩ$NFW_i!*e]@$p 8W 0 'zBw>$>5\LRֺ$HܠJB#p oh r &$ެVq*/אAZ9M6r[6DBSxf! pWS+EGIBc[T܉Z<)YŲw:9}Բbȟ9%$o\ꛒB ǑstБ+aл*[ɡ 1q+mse`ʼnwUTf:$%V+Pr၉(J۷s6RIܲw)5ipٷѻ>1vX9[9οB) }'`nʪ(g /J=+K\#Yq.@=-zRfss{D>!>$YY%4ˡ[~<L `TC`Χ!n+dTnjJ9!C>]|-y8!:94Rι%g3[6@j$zBCYa"qbrAΕ*Ζ7s ݒm@er1~x-{ 툇~qoeM믿>/^xrrÄO B?h[oi!?)# A ZbIb~Vb0Z&@eGk0ӯ99$ M˗P|%ƃsY9@ !Rk x`*?6 UqF 45*iZB/95WN4 !_[?F%9r%Hv,D +2lYuh:) $rX!W7|͛lЉPET%&ĭUiү0 <n+@2iI,Yp R* $&F&1#gHx5˱ʹҜ̈́8x'UHrchNb"Z">;;q]p!¸*SRޒHD.VKD4vՉ2dՃ!NL;!*-}^ȠNM(Eުx< Q]xSO@7rjk+qK=<-t[Ohs 5}ڄ[ٓr- ;?`jQܺ}eaWC&g䮢&yRKx/jK7`tdyƕ)shKnW0UCKƠ6KP\1_x,[(⓭ J Xe w`PD &9dc acSH`"4\ !5!Y&<@(O?E';lI"s"JtCNSsj>JPp" {{{#xZ`[.L8qqK$K~RCoOyC^VM r"l4@pV>ceŀjg4Ċ:YwE=` căvCIA" A; 9[BȪ.MrebL ]]z +>!GTNͤ?D2ŭUs $O#Ѹ|Rp!L-WS6#@`ǀ[z~[PNZŀ\:& QD3TzDٮN|RV\|=(¾!:1,:d":s>j0ط0L[i sC-8VN% P^42xDM'ulE1Ǖ-"R1ɜ0#Q xKb$psl љ(9?᪵Ē@"4!!t2CB`b HDn+7x aH.iBh|(/9iH64/]t||̕ƋyUSKp+w*>=-7?`ֺ~lΉ#:V, P!Aө'e% )AOVI$b+QさۭÿARPoN5)IDm~pŕ֨`薜2Ns=X %;b&$shiul=n@ w|сO\I`@\I苤Ҕ֤*m5iۥfڛHש6<-[,0$)5o(fnBޖ6]b-ņRSA¾UN0+A;BNVtPR6Ѡ}W0xˠ tbx@K'aΊI j|;,LRXAF<GH*DU lq x%}:No": #GYdrQ%ųpn- cW๒H+9`037 ryKE"Rs?4eBMiSԺJ & >{rhNeJmщtYy0;ӽ'&r 2!LNN}飪wytT]xwKz@d lz/żU@%(WNCG<} ׹~D#y pR &Y8M"5&< :4NSWȝ*a39/_ )W\=q+`ceա 9޵9s?M6R9n 'AC^$VLePJqgBUzv`%!ddEgSBgg˞޸)ko~sʕkk#<5x`9 g.(HN/#25t<] k5 $Rˏ%uʩw%!tXMtn5"9H`ɾu?%E+ҁĮ-o1 $e {n)hΝi_-E[j~QU,Q>—6 5 12ELf:Aoy!P&3Bp|%D+\a RK6^*}VEΡ,ܚ؍3tX+JKMl*:BIdă2iVJc;Ӑ[nV0dD$m4!9CUf (9v2*APviфNO>9含!0lo9v+FA K׻ $jQQ|T^r&}ʐ{״\)z`|,кޟBp?'<;oBӤ }2 (%ڌetD8xS| 16:IU*bQCV\xx@[VƪC+&4*hKXWNS+Iv +>D\]jbcMJmCnW:hA8 ؂b t̒˖8`u{[Žv䙦w yE֍7vݲoOBE`" (W,yC^3k s%Tc] )ըǏLH' n%DD< !?&!z*rϿhz7CYgV|VP+y& rd.6gEBHdG ZCA5H0t(sfKIpF+sEl4w $ !蔾[T[԰|R$qHJ!`_с*D愰 `S MX\1̳&Ög$"et'Ʌsn[,y&B -M4ɵx҈'sxDEHѡrKa^h(t JMB%(X2MS*#K`DNU@Ժ HzB>܊"4M_W j;Mq墬`љ }ȅ\)S@$U^R'_IxC2kz@r$[Ħ sUD&]j-%lb+s>@є~#;xWjG)sk8\7ζz H/) QC-sͪF.lƒn`{NRoNָ St)[4QO*jHA@L C^''t-L"sMŕ!XrnJ%2Tl}'۷%9-ޯBa Yӟ+O:(ұHx]ڠBJiApkrE8b%Q*m Y ˜m4]AeP(+PPď6[N l\i"iÃ(bӐ'[ؔ9UPIkvcEvԏʀQ9-Ö;3E NV]ru[o+ VMV - "IXX@V03e2t`%؊0C5{~hd%xdIh+n6l$iR#L"'t| l)iNͫ <L#J׽ t$%OQ" Bͭ*nE2XK(l@KV63K|Ve9 ! DΜ QTuV!T} GǎS $0f^?[0:jk@aul=Q (/(-`Êa\w5F M&0E7yU&ŤPڷ(7,U˜XJv85Ӆ`BS?1& #wiYu#UUƄ D欴aBͶIbe=~ xr) ?c-b3! eJ$%jI.W0r&7aB ʝgRN&o 1- 4[tN6M/SCItVn [BK,8E/eK CnH#Jhl-!s4n%+ W5 lV]䳇nML\\^@IDAT9;EnK$9 (xzs\25dP!C:ܵy:LWM?Ng=f '\oƻ<~E jQ>R56jo }ttFg)*S4"A6?e!wm w)x>j"ve:6lM愂D|C-$!G:A: ?+A pj BWG1th2$W4 YJd!c+ }&.֝]ʋbBV0O_$p񨰅Hh/'(r'L T>+ }wEHH _sءP[Cd>0iӄhZ } S+*f%tr:6[x9&'__t 98 RHCGwpEU +`[C&sA`mA.y )i1Bqkg AO*]uj/[K8!ԙKܮ 0˪M -nQ-:qD 4Pc~0PjB@,[C/"_n(ԭ%1$&Y~",I{@sR`4[9t\:996F5!O^MS[ kONyG*: >0e0V;ǫ oe d!MLȾ2!ʋs)\,{fͩ ru a-uL03WM&,?BNFYnxP#x#}Dd_lY O2BcsP\ޠ}'ph|>5+䠢sK&J.XB&6c 6r)t` /(N" `4R+-Xa0BIKl_|EBbkީ]MM ħ^19ă qkI҄slA UD9O];v˺&IǾWB`"q]g % $Y3 ny`I-l AAR6G#0t/@%m&)NJ@XQA!,"u:[qbNYD! K M!U(|})hhBB\8d(.'C tjWjJơܭwґfU\I\& :X$5-3O\gtDB5'&9xC]aN7CK>5*X"yLrwN؃ ڌ©/hgU:;MoJLx (^}Œ,>M$e&0U0 Iĵ('OrZJPQ]A' R-AS䖼 'T597Qw+7[8Y5p\S+R"ԜST(X<3UTZeK;C{qG.ҡ z q*` lcMRRWյ"UN /MxxЊAĆ( \-%UqBΜڋվG"DTTdc3AP7)*AmJSD$/B\F|`2?tK-- 6H|8@oGܡ-\i9Vقm% ' ͛3$H0c 1ʶ 9;$H IxBíd.z* D':ê펀$[u@k!VKۘ< kxP+Jm&,f\e+Eg(4eACB$SjD8†RG =}BV}AUO<3sYHm?3gXF:Go3s.ޭ( wtO9WMts$^{5s< $B(@:|#Ar 'lF T;X!~ \|<::Kʖ4 {z~ŋ;Ɂhф 4}2gq f=$W:&~`PӨ^j8KRG7Z ʬHj#/4٫ECv!~d&[9;VbH G[ ^R-_,1 T@JJg ă%Ҵae #uD NoKNRnMCۙ"@▦! RSئU }Vsr>%͒` 5utj\yrGj6PQa"uS`+(4lUJn^Z 2 *HbZsʑb#H*E 1v9BXT`[S\ J ~mIA"~ :Jtw,ZݺnGю}Y.H6Wqёɿ]ɖaC] < |ÃdnQOAEG xr(w)$2W $*kHAXʭ*Z Qʪ4lRˡlL$H'6T?JH- xa_2K `ГĠ#Y14V@A;ݨO>|is=De/WbA!dH;S]jƸwZ$e'>LcOWWVFl^tOۏO4J0ܺ/.D]gI+l`SQEZ 6!o&<[VP"D=5?38e}dX9964l5%ƒ=jKК6YfLs+0PUj%r@JaԜw&Z1ʉMRlG mfkǜ'r`^ Vv\A[-ǭ+<>11B.QBx␄rkRO?or ԫV9BQYDr`|[`a''hC wd 9 :F$Ճ#H;?$f[Rk0MVIE,T,*04 ν4{͍MRd]\YH*aNP AD͸#򫯾be+7h-/3CC70NzN(0V/daf fx7PP-zE' !onNw-jjغbReՅ2~؊"~ 7FV@:TBl&R&} mùZkOGZ@:dY1qU П1%p(e" M'Cmԝ\,'SgXry˜4'얓#U0 B<2LvD,Kb%@j(\# `ʃW^3!CPY + +}T<{5WW xPVI~ڃr!6dvJ=" T,Ym)Q"@2 Dt,Z[OtLu Bv1mKq|H0]DŽ[ !3ƫ嘞Ecƀ8.$@h'k׭`ɼ2KJY#\ӖK t`%@ _0#r 5V8k"sH,Qv˭Li\&ȱJY]7 {L(^`ij5`HSc hh*[UZI>{:Yxdk-|ʶg5B&  Ͱ|í:$zOjJY Uis1bO jwM[-0ɐ&<<4 DUsWE.w:A> a+r!+&j*}N *r!5neN&yT֠jlŅ\䪠[W4iأg8lS :̐D $Lw 5lC'Cx4 xV:4 t & -CYΏUArN=s{zXe9}yo|~;'`8ޒGȫ4u5A2nuB\Zן rO`rV4߰c~<0AeVt}rtɑc1i)5ƳI_s g2 ﰄL<9K`[2;8~А )+ k@U)3`{x86E1~p[ݩЭLHGkY9r = z`U &U_,&"^^LD Q!A"&ﶓZS96ITȥO (*aS\uS`0]Pj!l8;jXP{yHxViwH!sd1(*q棟"l50IIp 0e͠r+)6L9Ǐ*xDq65PQ 1'PI|:a.)1A2}PN!5uT$ yg[K[x H/\H <99񁝡p,H!ol} $e8n@XD3n!o?zcbbn}e tì|.b 2 g Y,Aqz$r=@dJ$¼IwW^]Ḛp%J$p_|j$@Ĕ5]RPdI"6#bxP[ I"cڀ\\a(:wŷLOZdfO 1_<&D/A9Qqf O V.c|$*@ghJd Ɩ40t)K$uW ~,0yrh>xC?x擑W@.g}id\+$q K=KXu%o;fi(k~N8`5 AXRKV DQYi paFԪGZ$@sVP̓XV״Yo\7Bwٷ@H@KHFFDDcG@C(ME2EZceA⒲, SX- ;_6e"l063/!]. Q|ӡl*NlU8p􅅉EF8 2@6UQ4¸j,A. \5ƅ(#mH `_&T0W FZ2;EJ ' Aυ;|ڢaU"5!z|are 접O/L\fw;wX;?G~D >Sc4Np2X?b]̈́j/H}%nS) |B#%) -E9B6UlU.Cm%BPl7H LR*4?AIYhtS*g_sbY:vT b 5+Aw`+eg7*eƗ8@x6-qE ā=o޼yA).&lȝH75c*{c6t X̍a4(O.ӼfGH@]wLU//.?`Nywx$(~rⰇ,|0 e(A=0r={+fM墣ry;Jb>ii E:& &*! ᔵI8j3\&StE%A.e(0""K I9eHD04?D=8:ȯ)XE%]af1r株~H艹!Fla['kr˖1il@)b GV,SfԥU4\4 PbQpMĉF =L_,[W8u˪.DLo[Qs`H}rEe3J>LJ={`lW#/],}kǙMP"&2RiZ"5#?Hk%i_IVPjG܌gQHcV{hX$ 5m0%4[V6ubyo'(|J8!+- JkcZgx7HpW ё!O B[VFR8* ny@P>4keh~RҋD"t@*T@FeɅ5'V 1IԮCåkWI!~6.@[q3@$ŲDg/;W; 2ƙzm$3bIzB`]o+:B#= 1:+hxQ :֌G"eoqKGQ||;|Q\ad>#0tM(ݻw}(puwJKBԫ,Cxf8L*{ U$e6(Ą>" Z+CQ&1H/nѐ8*3l7Q w/ETY4|.3P}JmEtIQ&Hv%.*[π2_⑏`Lr3/dI@0CT;}f9<@CZ%VYرԑ$&(\D26d*mu%1l1 AIυ(fl aFF `?T?4K)8D8U cܻw*GP.G g1¶"VJEXBRv>;_2s7)k u$eߟ <(||JH=#4,C 7F&}czUnL74̻hb+W &^?`^q 8q-!dTAkU ǙJҒP! i(7w4R;UYҍh@7bAfL"Ug`LIJ*3V14UJw~pd4wo1b_.I2t,K{+E>Akxx"J!W;2o%F}4HУ*pAi%.)F(3dMh/y\Q8 Q0(}4):ּf\u hW#(bʰG8 ?p.ǝ: %w*Qz=Zu 9M2o*a/V!v;K:sDR0b7V|UǖVD.)nDZ(I Cҗ .z1iBqyytD Cg@]X$(c2*L.LȱU5!# ^vyQa&]=:Ǎ|m1y/9:̿E=/hQ=PJG?W NGD ~'9.傞D]R&_|dX*z60I[-' }'":R dl"EJ0{ и`Wua=KJJiQUj"ia2:[ꝡ,!2_]Lu0 $^umBFRڶO93r -oc3k o󩼤/ G \|5b,Ԍtac:Vq%(\Xl|P|]=7c 1q]1Ì=XB(<_T$YBrESä.s,];s:B>sNui&ӧOm 1͓MK PΪI8kJzQf*A'-A >@0LO ֧%4Q J7izdcLa jjkhv/g|/iIayrJGȾLqS&cA1CfY}gk.M7n4!p@.p`PI5+#K,]R=p@2"8G%Qh ȼqBjˑb!s ^KsB^~tu8XTAW5 A@| FË25;zϦy%YHa a%,H$bo4qL׺*1ɗ%Yt] v+6wT@qa@ /53gR3~5̝Dj#DK<Ɗ^1+JwfU9׼pjOވj?n" !; g{=slP jG)I7*: [W9/IN@="ܸ' :"*˅@GWD؆wу)4Վ`Wnqer ~Of@,CRF` *N+jM`aRGշ&ThK&B%e(I])Y:.~v7E9wؿN"vZ_7 P}zofEb# lpq0dMӜVSzYl*|%@8pFCPusK< ,9}+KY&5Š>ee=r 2j뱚MCf3*Qc^ 6y] ?X1I@#I1̽mNMwͧ1!CߤTHH#$GpvQ^JBDN[grG^@4Pik&k f5 @t $,0wc6\r6]%MYf!w7Pbde6 B/2bl{X7AYL@GU@[l#R &F @mǔ 3i!LX '2.//JE&ؙ`u5_~E"|a"/Sӌ' UzȠh'4/pBjmN 5eɗ#;(N&;Ɉ*c=@9Q,G6GjxYҊjpǔ9ȍ ~l"y4/, ،0iJRxBg (lh2/eX_KY\ ȋ,8hHrȝ(D؋B s~qUR<Zȳ/DcsH(bvȀ,<P¡ pdI0Wt6ًh92Y)MAPUQ ּI-M f#D̈]Vp҉ Uܼ N-kB"dGhDKD(an`Ƙ= Oz!Zvl}{Af>wrAƐUM-#@c_x~9-i#!S~2,7Л`hжjȘEmd*k8Bo_,d #@gud*K6bQA&М5P7Z2 X⪂IADa&}!@b5kWem /ercY\-NEb-EZ<$X.6\4h{@E8͟.sR2EO;DAco*3!mƶD4`) a ٛw)vOl6>gׯh8j*Q)Qci)̈́pHCv"0|<T4ݍmZ-N@Ygc@IDAT:4^UMRc|}Iu.̎*hhun(HWTBU8LH F>ٛ 8cxPq`ёtqq]y,D7!`[%qmoAƉfID3(rZ1m k4{%E:%)3l> S#8 jtl9{X8. DC%̣kij1sw!x2/FĊLš`D:D{w*LY(9(`UQb]DH]`"iv8NR6P.3pXRK@(THsVe†,bñc5W@ 3D4ॾA13$KMߌ.Mfc_Ƽdg,hthiCưM&0 cɒE!$mbn0wfטڠ0c fh,2#˜1p4Z{1ݤ7%("5G(1xI.`2^z$A +S%B:32kiƅ \TBC}f"rDz `͛`{Hzϧ2?R@̏ cUt&J"O7*7(143*"Nuԫh^MY%5t  È񣌼l ];l`[U^?C6ZD.fYULے7^+X%i1(!AA7c6B;J]'i-fADզqI* KpjDǘ_[TrxဧGACl4@zΪ(fM. GVǐn`o(pNȈ/e'Pma 9Wi2v!̋eE+A"0=8ګת%@@lrBBjhRiD[cnh O֖‘Ge14KdD SȓΎi@`BC13ܨ Ni~x:MIK\2-T2iV7";4A)΅;٨˥&4sЄ@/%RI99Ɵ0Q,hۣq^?2Cc~Ipd@+G^I>B?i.k?#L^l"t#Qt 7_J+݋<"@Bt9.p0#YԱV;(Isf>u e 6aB&.LK -'XLr=%&q6:3{q pL c!pۀ|8IC;cDJt`b%Ttu5)e5٘#{K$hDIdd;!x9FMB*mg"UNDU3~f(d%]FD'Bu}oWLoib`% 5t'~CCad]7JʊX6h#qisyDW Q `FR2KR&b\.$WmG1vadHtg0/,T* Y{ya j10c_0P6^W~LwKjH]8$ 'jJu H͌25zP[ܝ\e H AҝfhUhBO ܒ&k'8nJ, Y!R~ߊ/K="N,E%o "SؤFԥV"iF.8#6m|8'#R sCg'!t% *F s= "ZR2 w4A]Im@M* Vb%;2r7όUBɑKClE46L @vEUlA l{93L[.f֌ebőSH¯DhC$6&ܹIcI[jY c$pcmF0VRS*q uX Jo-XrTvpd&L#t9(i8F \6iok"m'fxWewf-DK%/&4\u wRNO~WQ8* m V6|)`|\FA+,)A0llIF\`@RU%4Pwi3M +E.>V0f(%10vIO<3;vq9īA1h TNT*|"&|^ns8PʌKY=KV7d^^~hugeS \B3y PY&/xCUIAgIaZd* - 2~= sȔq"rӬƔP>4ARPl5B{n5:3T!#$uTz*Kҗ8q/ehp؀"_0R<$ԐaW Yw,~`F v;BcƋ~4t5&02>,]-d]zplb|(7|Gg≛Oų8Gۃ/>11o*h!:3&xKGO@8WG%]Jpt/yh`V,ťp @C2·1TUKFUbR%zO<\8emLKŊe~J]{_f/2SQA{@FLAMVxld^$x d"Ϳ@TR iI"ꇏLpU鑁,0o &Ai,k_Wi3#}0@\tpʰQu3K\ZASHL\$rQ>sUL dKhF0nSSoK%%k.ȍ#vmuO[H EA/ Vjl hYX= mb aۼ >OWL4^i`qz { 4x$)pՔ˼eD%飔c#c(%q@2`! K tGbyIpg\_GAg }³Ԅq65V{ J01ףݵBAn@fXs%34; C|'%֧Q%tBMeKjٻdyP&Ֆ]Ȕ5ݚ̊H(I{.|m"8|ɀ>%E f*"{cyg"g4 5dp@2Jp۶W>!fNv$\L v2o`D\^$#hR;55 gE͈VnKh7.܉+Yo."_&azQr2c`@(xLCAfv3v36<$e]að=ݖw"p^jlt2%S^-'edp7:p dHdIi [8^#,?`F^}F\g##fdtj'. \ ՋV%"eʭ y8$&1gX+6DdǘU Vqȥ,[(^м`ȑ&ܝ`(Aی1Yoe*/dyK$$w ܓ@_Vlʊ@8ŕ#/GyIp,>K B#IVMd,p^%5N. f|]\LzژWo VThLK9,Gp%#*sI3 ")bL.'/4Wua=L&LLEB%/]PxybT*תH 6QjIsW9iM9zXޑFѠҮ!^* wѯ[!c#l;V%>d22)G@b0 eodq0Qۀ1C殄3;:2"#r#pyyPlfVD>wB"BIfE4G)cBCC-D > n|eXvbB%%BG,=f<>Na G8q! C] t=e/AVu7G8f ZW@z=/l˪}z SL.8!i RppD@@..!vG jgЋ1|Ɛ-(qf'K3p׽Lb+;Na*(N4 ʑhe#| !m(KP,:%()e摞| .ҡ*;<&pȸB*㾉Q"Ue$ysPwoۭM]!f#KD3.3 퇀%ZY(Y.U ,k R‚ ̬~eXTM:@Kd@LIYE{qIO1Ԧr.wr]7?T?H'&~0RoZq݀L1cRBT Viy?I `.`,MhJ = Z _31'\3l+ c̘ `BR))F@55y. <0qkNz#eHwbeVeكJj'\G}i!HSL,cwWU:;Ca j` g-R\`v2SlbpipIQM2cZ%_gږ_G@:5C55B᫼u9cGRʚG. 4{CgGڄJIgՖff+uV]J fEKIf%;],~Q/3BΗ)hdMHi5w]n` )v|y8D, duyVhfJAK.c՗pt AF)pLYhn .Ȩ!x$5&lTi`19_'5&44I6J6c=*4|IJ$q yH?aOݨIN_T:#_f DD}Ocy@LZJd0]H%OL M,Afb@84/KPJ&W5?#AU ǟMbݻwV=Q逸#>UG\#hS^3L,";!t0KCc3rQKRV^.3J(b@'LoYQ\PhZ2/ж`E6;\Cc\3/ѩ4HE `1Pp:vGT6p !(A̧ pKdQ6w.6A[]mY(c _ge.Ewa/F}\"EgB@4TtC 8_hz"d23Aረ\ccsZ \P2rO`Г8xGUG,9zOw ~Ōb#xjc7\@d^w?bcR8/|-1fg"lJ"(&&tԮhe;DH$e.H2pV]dc:c҇ &%Q32@XrB#d`M-tݸX4)$Kh7 (3.2)5P j]*BU)b( vs4cw `zJ*_3%hսIX,* [K$ё)Jl"K 3چ,)O6I|Ii'㳪 vcN"K8q@GvX8zT9h,] 2̵,4AiDY< ɾl}]:fRHl^ct*1bRQIDU VBXUs$ gA !v*rd7/6"A4> 5V$wC(: c_^~ 0ѣtC^G&f "Ct0gBzǢSҮ@AxTH\xt~"S Dc#.]' daGjpae3{I#>RO`F "p1"Ey1q#@pPFC48&(eBPhRh{P_$Ȣw#3yYKel %S@dnl[A=Ȓ`J3}=|)}Q| PG̀."/ c3~QM ],^*3ѐ y6蹟r,DA^8 p*v⢃Id0Lv40bfw}=BBp:J"|Ju#h+;qeXp8ȅG2Ğ#&we@`h!mZY2@ T,EƲ5N)HER&mCNN(qU%.e0]-13Cw^A P2,u'j;RCLTN0ΪI KL\~ݧWYR`g/PxKq_%ba3%(4Kс3𲧞\~Ҕ@A|c<}rG?*4wPN%ٱIb_h6, v;ߎTd$:1GCXHv ʌ,lnb *AfΆk&T2vZ2I6fCcg&gĘYkU7X x!{jw`ޤf񈯱%.vO?5KH'8&'/;ѣ/GPtVe%fXh=EO&7_+w*AIMYE7选1!8ȋ\.mUwRj3=D8~M"I. 1&@7Ogz]PhQWT885(ŃLiKg)?f]u b@]h/|p ̵H'sX( & $mJKԴSw23#\ nH7=d wU_pؘ ^;6z: b GPfSb { %cۊͶ]ِ#u?X:Ą/mƐ22WTčx!y3BFAON@Ĥ6/cV.HVYIfPGMY/.{LPf]Eh~"G42(EO-i9PHWp1oD.`Wřv6)kdb !]f` NEulD*~RuUlZ 2_%qI!4ArI$ Cg/!GE|$.2UR,])׼@.lI2|]b1$(~|V~3Xi2ذϘG aLH~D+6C9(Yѩ?rX:Iv ,QT1"Zʗ^L>Ed#SCl;IbCUri^mR0 v1ĔB;#~g6\^JY(NRKT2e|0CX Ny WA^B'qMhBYsO`,Sf耉2MZr1S#T]P;9@GWJB˂*Uc qG>1RiEǘEJYʴ=p#/wHh8ܑ6&q3o#(q?E !yߐQ,iZ7RRVA|"O84ʕJ"j;Gq&U2t3@2l @fPa3/>{)FYVx@ftP,u $-ٽ¦FOAQyHD.D t4&m p2LoJX.R+[5β/_u0)lN˞A'LڄBb [%#`yAךJ w?hnWG-8qKD2,}W/f{ z~=耏%U #rb̢Ζ)w5F/DH! #?-֓ /:J,ݥ|-~BU[HPbA ae85CQ*/R:$4"ӄeeEg`!`۩ X t,@c9Dxb|PT1NjQ%^JS{Iʋi%/dBh:=uG턃nd42K0ׄ ޖ%ͨ>+" !YI-C4=rOp$TYg?EʼG#A|^WGaVh,b\Kn)&g _ǗMUlq3{H[Z-IQ'gX<8A$-.͋`\%;ŵlEn*GLR, m234Nl`fh’mE&dQt&FҒ?i#_'9 AFČ!߄]11l Mg]KfW&`ke`@J_W!q:ȼYB+zIQV7 hwa%k@\R[E>{/9Xʫ#UhVJ{MJ(DitGK[ z]ML+*K@ dK$#f(m™+|tlT2{Є8wA;,/cD4#c8 *_鐚{=:aJYԥDJ3X\q%&IzD;B@\xBk7 …j?^^11L^3c.eyf>HY 4TA)()خgi 幸dOU3pQ;Kk2kup1~^eDHekN"KfVD8x BhQ= G*33vh/mI5˝9g3sѣc ~{QO?2Q. NB&61dEE1~rѼxΌO?vgXAy+Fn,k4l{,0 $(3(.wJS.ftGB-N{6v#&Y*{$U%K@ p074482+SYSCtط|`%Y.@GqBp`qchRfe;_%lK13ʘtQ8}^1.yA&_C/L6!z|;@`Xɚ{MV;ą(3.rS i0O)MH Ƃ`Ko_[{c/IF2EI{Q.Ԗ˪p.|)ѸY)3b*+4yI\rHs6ylAT$ dRP}4k,#Rȗ,xg o1M(. 04(ܙ՝}hX0 `ȋh tg,Neu-nRnKVpam?i"&^(uy4OQDUDCz[:˥Qtjz@q sڤD+~#Mf SuKbqnDxQ(Hxy^L .li/2,s2G \;-0sJa J,3)SeVB5F@qV$  m2t쭖#g,Iӿ!`?H6>@_: q4`G\D `0c[5v%5e*tL*~LMHL4̝ pA I̋GTpoBA,v]1 5gڂ>Rs&Ț22uhYScŘz]);NR.3nUu$(jfeM4-0>\e OQ*T_ɚVhzPJ2F_ddR7|81jgƣMG iU6Bczz\~b)`Y$DulCW veU3'3XŠZ|M"`3&%SD@]cn,5*6K^mrjn] Dюe</v kKICdyjf~ q QB%~ޤm&3;@ C ΎR5攈fE|CƊŠkU Vl/ o2466J0hSYcK,sƳꐒIcwyt@1i`R &."wh4{JcH$DZ34ۋq,U ']M(SM,6\-:^L wbH+%do(jʀ ƘOr,p ؛CwI^,3 $$/"jPa$Eh&c,Y]%==yU^m1F8d&FȎ4tOF.T^sv*(E51Lwɲ،9ƪ ^Θ˪8FԷɵL ]h1 mXSA0 ʷ`2+5b3(-u)R.Nh(Ex+U@f@;]ሤr?sw$Ga>$A-b2p1ArJIweԩ˟IplmWK#F?MxcKh7݆\b !ZAV=b[&%h4s!(6e:CNM,,|R\ Vת#`2y Ja@a"AI%DN)n NkX3&TR#}:s Sa"9_G/CбKZo+&!7rR`(i[pl z9r"V;w.YrGͣK8*YyĻDs- PԐi…^~G!+ *,d:3D*"gA$= 0whu5Jb a \\I2)Es@][Ga`wϵj*5. -Ji$|A4dʅ7lJR`^%q ̑ލ6Bw眼w-1#e  (̺ l,55H, C-R@ u ;̵$%uůGKA92k5٥ܗtMPh`^(@Fsg.L[ ?LjS3NB@/w@܃$&Q;bb+EihrŢO42',U0hbh(@(G` %[zT88M߂͵LbB; r٠*iт>dGÇ; &NBJdh@_@9EeW8Bܼl4jGt_ߌ$NXKCrJ -m\R|5`TP8jx^pP BeI>3H3[ ͜>+k r+?kR$Ţ4 P2cnQȅVܢv!48.jt Sdۑ]h\S_eݽ@%9tVAp )J{"wiEBV{?,d\xĖDS-#D(qH ["2p4]:$!qȃGMiky !I#%}8ߜ X:HcQv#}B4":ȜL[._a3& ~0{D e֚P@2C29nȣav58ݕQpz=/9eHlA Q}*,ɱFF%%qEzZ[^D{Q]*8k$PtAAքLHBdZ*(/<aǂ$>٣I>~kQ/>a5,q ,2l80$ PxR,!ܚ@UC %$G } yɔZ3'psZJG k?52[OKt"֮)M[rdQH(u&fG r*ص~@jL @Q ia8Ό5iZWT]=c} |N5HJ8XJH P=ܰ" CT2Ti;$:؂/,v@IDAT-0!G8tʏdO=眡]&1QlEgbbBbGr *M=W( n] dywYpף G`ȻoHe滅o'3qG2q0Ch!9tq\%#+ii+T ,,cAPLXqa0N 9&"dj]7.zr]>B?!H CI0#E-7%_RRf(N  ?$~`B29+bK~P$3LS ^!>+eTBG(qI] R bj $q<>:H9MB gɛ0 9sj50p)x,H:^T@HuY] {ڔz"CF@ȰuyBV GЅ X t%W]x $=V`:J# &nr 1'9a]42 0"ʭԂP֎^[Im\"cX1|cP;u\`&|#IU~ 8XAð'8FcAɰCN9ah lUhɌ+C-jRg-k£+S]a.Qp ԐY@]i>n8{ل:&jP I(%6 [ PB 5&Q@GLXG"f(crhy&|К| ՌFg7ukxh&}!SGBM&F`KKw}!WeklX Za}GvsV̈́u C @|/G1;UrMC.rzzKH"!gg-GÛ(8] uopaOLgrw$҇5d 8L!IA&#^tɀ9-楂# sS>ygEΜrPBv)mЙMOJDzR1h/\PЄle72j7ya\Yp\Yr N!["Vi D[+úT/ j1lPAOb )h%w y#5y0 AE9Iq BC V4%ӣ1r$m8$ bʁoz7O>ITŅ*"u GA㯿 [ <ueGcGZmc<sBQj*+Rלw].Pu1A&G5!IM 孋R, ʉ{tTX(6I^"  f ,.o!ЁVE[\_s08`Bnڮ.v[/@rֿ6 AbзpGL1'T}:JLof^[A97}*XH93D!E20/r"!a(ET; _d^: VB&`m =ґ̝)E)3fuH4"EЉӫue~v/piv+sR_a$pv (1PSlK.yERUp)BC\%֧&P}&myiFQ~ CUtG\x L[k~ajXڒêU! BRз f D8eT2tךFv fi A}ʁ5sn튚[*sy\"kDQ[\ Z!vѐ.Q $sm)(>"vG:ˊ294NeH#QÈ*_L#@h$Q"NMKĕ9Cjm0DD9rMRLJ9>#3FqsBJF-S*!AϮ)<-} zy@4Z UF¹dp gB[pg"^^pE YU@ #EeC%+'Zd[*e&@W6M׹k M:\5GLNxBP+8̗6("ؽr(:&,W01P*^d'VhBBM@BNFƚm2$G1iB-@AGEkB8 ‘jk ʬeý)Bu..r.K&䢛5Rh% ):Ä +/2pUwT8G# VŅI AFAk A.hR>HHh D9`?! >YylBN s(-Z@@dj~vs @{@)(`G:RDQꛫ<"KJ/>|//Wx׸GPڷÏ]Α ͐)#׮% %P>[QhBcQ}Qksk\cx/?HJ)ÙHY*(L#eB`B2ZS&Ef6t5~`-Bh DŽq&'T&B⮖P4,1pՂ<⃪ȯWIRP^%U.\wqQ]"}BÓ@>@5wzĸQzɒA̅I̓Cj$sHr+'8 3 j!d]a\fzׂImsP KH1K! aEYɍcCfZ (kTM* 9B /OBY̚&kVmxtT|Dӣ;5][H7k˧- RAhز'T3~aBw~F}fv ϰajGAby^\TUǽ;>j`XwL4zlM .LRG]J>1P6e#4@2l!&d^$J\NP]u^Eh7álזw_w:Y (ے~)x! i2B`B W5VqG0&G#&l-b]ɑGAu>#vhBQXE-eҥ$@F"r8P,dίɡk~%҄/<IMnLr'J,j~9WʞQA: sN\kՐ*c Ŝ M$D}A^E>)HTH-P$ HGc5%N.z@fxTw& ۅҫVݴ{Òo@,V!!BH(hwc_of\@3+koIH G:,-$4uTBieݯ*]a(3bF@1pQBB RD!JuT8 Ȫ_<2,4ȔZܪJ;ZjylTB@5-XQвhxJ8h ȭ j(li /94bZ@z%`-G]4RO5h(80ʡ@$k+!Yn‘dSHce^:蠤.af.oeIV4E0ȯ)Ƕ i0=33} ! 9}tMK)ek~:dF=`A"4eX/sjZӁ !{ʮ`!T%} W"6ї@5(&rf C@ s~21ſΉ,RN>>m,*t޸s{ly1[rG!8x].iQ}ba0W>"3m _:,+I% +[Hz4 l4 f )W/X颱 V:^" Wި5&MD_ ǝ&W>%)Q[m7 Vl ,ek̹6 BV)XH{/rFY[0z$ԍҫ]h"Y]2,6a @.@ͶH+Ll F.5[.qQS. sBh|/3uc7+*3/4\ Zsxrlťw#,'}:@ fl˒YND/lEGvˌEu(ߚ!@)H_);5!, S #$kon|SVDa D(pA2G u̩+ *&YQ/09ɡ$lѳ #528]" Rj H{`3 +;V8;X`STU\ᤛPNMx wm-Eytɘ'ҨqHG\0E*0AbLeZ45}d_7ȳK1H~9v/o =Ξ8-]@ŸB[4ە>QI-E Cy!OԣQć m-xKsGBGN!+$SՌ\y&%D9"ɒ;Z[rH'2 r &qh'1O#bz˚@O!9x#̅[IU0$%\*-@,(cEn`Quw;97iN䈚f$Vv$]yg(XPYrCec_/ZFCB$5 V2PE(7MHKڂ2@QYRw~my g J$@[Z j VB̢@2MXSfU kBQA$%&A`h-XP֮0<RК ? k*^ ȆBہ2E;ϑ`=@:AKonB5L-VD^]%o%-8hWbF_B$rk6ʭ۔H,G 0v/(,9^sϫBByTBISDxGN=ّVǒ \hJbrONh6bvun ]sÿ[Zu݃%pz\T UW{MX8+Z`o&Q]<)>c9B hs%(pphܧ-ga7)t)ҿq'ˆ #>e85Bv&I7j+BC1( Z,)&dȑmVܰ8Pu25k32 ڹeE]&%H+K-(SCVBPSr.EoM]*K#M>jNkC. wڲW&Cn1!7''pzL_j;V4$e,u蓋aZ7H±FL $֗QMGʄN@k3b##4leO[t&v+8z 9 λ8ZNsM"&Y8K9hƹR:^" OM< 3d̶QJ~ŸP&eΝp1(3'Dr>iG\dɣbk˭!X.][R/Hp|WX@(|M7? Hq{Aň ^Me#q(X[Paiǖ" k21$QTrĀW.P:~w-?)UP[!-b)F,3|%w{pʖĜoW [TI$HE<YtO 3{t;@Z&g :>w%\sVki!dnnfu=dz34[h1:HH C aNI#> QYxTt͠ Xr34!LCY"AȔmAL[\С] sKVXL2[!hV6C1/{b䋤B@OIA.~e(W$§lm9q ^ufȭ$ =rmtbNV> 1 Ul1VZ ǂDh֢J(-8#R|eK'H5e."Y-cc'H X"KɡiÆo41&Gj{PFP?df"s/*4ėA";fl-@& Hp9=e^܃.bw `C,Qzcg ^ \YÑjnB ]V1JrB#9)Mp& }ƊƖ9<ڽqrs 8OR,Xh[s73JAgHɮHu=R 05W_ք|光$.,ٮvQ2ua1Qn% CPDƽPr%B-I&nlB ˯Jv[gC b\DSТ3>Pqi0'!:V*%aA ʢ?+w:U$!ˌDMBH,>|;Ucv& >Yc"W)-PtBё PB&(B\3Б49+Eنɝ ȏtydkk^޽H 8DfB, WJӐd?ԓy򋶲ƗoC4zޖ[Z aAYW0m jmD\ -pA5w0쑚geQ'a΄#dCEW9M@cDU\@XuL[Cp9uu$sK%7ˉa!^p &źTZrƝ|" 5N] vt1?z8KIO4 V [&\r z[ L nsТw9[Pn7A)ŦI𞹖"e ^^Cز`H۸8*Xfa"8tkk#/7t ׂ_IkVJdE>g˃`e>s$BjJ=ek-Z&?!7`r#(S4,"|GG"b!X9@Uk2,ӂ;8!s:ۉW[JQ* 8V}IU`&[Cv/,٫(Tu$fBՖ1 .+5fIk}Fxu*?/<4Zz#!)ã55Y=' ɛ>t Gt`%?՛<})h5 $"5ߔ@U[SB!%#TQX]-o fBNhMFWƕ]:ʕ@4!WMb.dtG/3r5p~}29BKD,&zm  8$)H"}JP7ʃqMJAi (>Qք",dis7|IW>L@Dn|pyQ:{WFA sA _ܕ>VR>[ŠPku+q6ŵnNrJ 3j21M9%s^, t2%MZ p&29v/oC!-T3@;>1qI QDȖo XC9(-uɀDO~9({)?~</LhᄫdRP oW'F9"yF&2+Uc  3sZ`N΅tYCM,q$E bB#0e- nR!?ˆ2Ms6NquחV!(|82&u,WG:pJHo}6[ޭO T)cr("'PՅPf(#^ jf>d dC,]A$dh ©鑡DX#jrB[ O.E9}[ysƝl\%sw} [K#Mǂ̼Б. `oMeh-+资T{&L҂sbKY>RBXXE2IdFfKbIC.G,2A4pλZE{qgUJ1XB :Bp}<-lE.p|mT*"I^R6 wU\` Hl.kȲiMyPWpb wpztz4Iʪ"C W*!&e 8 PR M:(Rٶ%p |145 / ؕ@y$XiU .pf JyfF'nO制8)0kk)"gSѣ%BD yjt0L-GQ쎀XpgW{fb!2U,C83n1&^Rj@My!? @ZG{j23\K !YʓKnD8hjZfELaGsUb.>R!KlqmL5 8N&2kKy&u!]t2R‘]]G }L(AqT9"Q6am-/68!)W[pP#$; t)(oƍ-!w E˫hֺ Bxr3nrN>Zͼ(gU4;'BӚ;pڅLrDs(0CP%f[ 9u X;xRhN:nɄ2)CB;@FP7(M:f̹`=jG5ىSb ŋX35lyIeX 74a2&!f SZxgg'%5&:I}9eϽ&qC_B"|$!>f{ QVB.SQZ\v (׽R<(U͜!rEcRLEdQ>-`^#$Oh@PA?-D(+֨݇ӶH8`B5t/`jn͡KBS -eyּr %υr2%X*'a >(!0SkW@ʩI٪\SN.A F8|5e#du mN>=$v5ńw<) m*'2>5hPP2p 2Ulp6095":X(^v7ܤɣ()!׉04!k=җ^}5* ӡ&'&4Z Xv-0'.[[% T^6q@RnI, #]p%3xZӄ`jBW'X9_?G~1eM99\DŽwL$t-Ȑ{R `!\0[P D, h& 2jJx~p!H7KK KX5F͉0*{MCQhBHBAI~PJkKM}t%XQm vHZyXxQ!y E eV$: rGS r%>>sLRDay% £j&Ȅ[0 #:aŬf|u'6Z"]Fk]r$ 5W?r%өptrH> #er vsA[% -阭>lj@&fXB%(!k厖>KYh4qX|C]DH Ь[܋cj+/ˋJrMrP"Z3&Ş jt%ehBȭ-^PC-yq+l^$`eyxH7$.>-)G=vSJJ 4PP eE. ˆ-C \):?k҈\,@9$ ff3/uPQ%YPn lw,zdЕ Ö`cH5#TKGBU G[0o ӄvÿ0,^kX %5adHeRfkL!LqQހيX\vpXW`~ʽ/`rPD3IԐ(I^PdbK!,"3E! € Gq"_?`.k7:9oqmq#>(xлΆ(aEv~ {+޽K:X=#Th0¤Q,j Wa3|9䝕gb ~@9.(2Ё`nc>}?dNGtX)B M,K E@+v;t_5P`OJ -H PL$t iBgo[- M($73UoIER>?RJhuV3Ck&p̺iHP(\ȻDEj͝3歜wb%E1ĢHJ yzZS XMڌ@.a2F9ѐJ*7} YГmE 7%.E(ye&V)D)5L(s!| rBzoPz36 ߯[:\{4.S{,u^kZ0{O=Zyō]dn>< qA@ĺ: U q*pC =A 5uUl -˄]#4k Mk dz\Wz 2oM +8\X(y ȃ2KG)45%5a~9JB7(XQ+af^ŵ+l -&ꃅT~ +V0AIHhJBV-RV$@ʜ Il%wrvƟDhfޭ dxN; 3}[&8tG$| I&7n yB#D2„L2y%=E{5۸Q/H8jw p*&JwFkirxdRjD[y1!HȄ@(4k kΑ^LČE~> T< N\G"NFp-#W$l KЙnp8՚&HlEW$1/r lْn~,Gt8kYir-G}_^9$Q/ ICEYD!,&lM2}yנD5WEr#ejb5B :QR}j$hZ,"#@![4j $a)tH%0$m@93t`0P0Tύ\#{ Gk ꑚGXM"DP$ע_-CJ"FVujB:4%|EcdVzM}:Hurg @{2'3̪@ـO$[hmVDpƳXrgb k8E>9 ۅwaNG7l!cWo4$^L?22`-( -+PN$k&[{`M +ޙ".Id [B.ҽP&d[*HVk&@f]9`!tFvi-I4HЂfl-Bn#'EQ"p<ҧX+tliPYYej݄U2/QG0^N2 .T#i֕2v@ e̩]rġ:5^~̄9 ?c;uf8iW* h Vv-\5 ZU;,q1DQ3{R8K{J:WRb,K/Pyv#2t"i2ӱ[j3FEp'Ąc^<4 c?)C&m3@n}p FhG{  4HV="0nAQ(:N)@j |!OBZ> s4!vJ]]A\/=}BE @64 2yP4S|((.kB8MDX lj( }ҷ9Fd7FrW[xz4BeeW?xW+yt^vM\cB HMe#|Qt 5FYJ&!wѓآ9@S_PUߟPJ0+8\|uB9*Q! )I,HwӁ|`pp䆕Ǣ_,HGMu)y!\ac-3C0ApH -3r@ʁ G 5z"Jbq0\(8Qf `j _;I93@_8f\{ˋXRM\-hfH|oHHOif[D)Mj̆]i'0Ibזaݎ [Q BpC0 4f G8$fJmBLsdEmfbՂ_ gIue%j[3'̵܁MfhN)!I^+M}K5/xwv)H@m)7"2HyI]hy=Sق*|.ϕquL4^5K/Hv ;;$@Xf0d#hy&3 F"liZy|=nKbwDa܄Hm} @@(2 +2) e撐m6}8F::@ȇ$B&n4[tH2 DHH~,.!@l)3Fbݞv8vFvc5b9A@ =J+@&kx IjU'>cf#`zdNZ/s=1MkB 5 yd!eh˚ 5䚐̈&$ W3C@y9+1fI\{7=UA0}lu (9*¼3K9pAkoAKiү=2@ JJSoiM#:i=$%VNX!d3)PvJy%3) R_P@ Ta;4Z^2`KE<# T)8,x5!U=Zи>f^>01 gC˔u!X9 G5 }ٗffIbavKGPԀ ܰ sD9+ytZbzBsxY!][ 1['n| L*XA3&W o9G*GB7l, l-L\JyhCN. 0v/ڗYQ:u@ptp Ǻhz8<{%3 ğGG8@5 #GkIT ih+@s̐<2hAŠ&\ eaeALZpN]Ȭ3@Xg@3GI2+Nـ8P~pEdL <"{Ԍ0 57HWb1$/WtMB1Jv19Cj@CFB!e͜G\lHXA(": :bB*5f=5HWt:>G:$RN lM5.#a^[1D/ ȲgU5%ieAwn(kPFVAN&ߓ=Rvh)0'VO8T@Em; a%|BZVm8V4Jt-H(幢SY()ɟZbo Zk9ܽrlPWt!hٲd$ȇY1A¥IHleTN=K-Jz\fSKL-s25u 9P ۴0OX3TIw%w hGH#[L r$l7{Jl<|o3УJRZQ6ڍ?xvNPh 4H;]ƄII\>3@HaBm嗉a+-W8.\oլSehجKYbN'H9=Jœtb5]'}Df[I|&5ig؋ &h$-׫e?Ȭ $D\na_}4x}9!!YfLNE(3IIVPiˠf ^^' 9eB7k.y lnk iLGyE6)XX2O3-BP]+@]Vf:Vq ̕)ڂ/֠V^=qʅc2J$hG5(PJ=fҌG&f 95r\$[8]VGZE#V &8 ~eݐ!"`55aֹ3S`hIhmura2~Lͮb6(gHpFH|l kC8$ ]kx| s(!>wwdzXW[ԉ3[lWJlµO/f8Grp<^dü{ZxZ԰Alla.IU{w@tզK"@)yd6d޽$5Q^ 1` X,ߝQ(R|HJU{ her^>WZΐI[۳c*τ[B7 l? x6Y.)|$INHP0 Po"Chi?BQ 5EJ}E# Aeޣ@DΦ\Z#4wt)o7# LHεI0BY-D)Q ˊf&02)Mdk"$2OZ 3ɪ-xS 99p\ $OoL!Wb5! 0XkKke oNpmmJ&58iZCfd.-GPR (N%ZƒF{`栓32DěPiJۣUJ`9Tr瀀kV&g`3ad%gJ$0; 02$xf(I$'a!c8"ZV$ID\ ă(FZrn8w@HϤI埜,Ob +B"j9zc^eY2qYEVr9@@tcxlhAubCbW{:ᙾngn{XHP4 GBPWᵛS{š0 kvEM<"F, C$@$$eC3!E|b_14)&'MR[\&A;Fy- bQG=)r%`.BB*g@2MCO R+n9x1B!L(Yд[!vr 9ɢb72L;@K>3 FGrF&7776%A#"FnAr[| x2rԴV47opԄaIѩRX]|x aZ/C&?ê3Cܔ3E#TD% ͐Ik$DM$E0).o E$ ecDD4c8[COJxεcJC+|Z|ɓXJ%^o, E6`BP L y j䊪txL'-ב|R&.cRevMIW IkrW6f- E1 QY͎j9 I]NIRW\LIK7()$158kӄ5 9|@ĐeA@iPBP)ܐ ^-4u}OyQZŝBoE<27-9i{,P"]tܒ,Aj aB/ %3u?Xrߔ]VQTe_,$Q[x/2ԺČI,~!(CU"G[8_fcm}HW}Qqa_G plictPn 9cx!XY, EPe㝮߈OBNW)æs{OnHo՘/ =G2G`?5UxgM@}?a0{0:}/ W!}9IcIENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/testmodel.pack/marine.md20000664000175000017500000066421412641367670030472 0ustar jaakkojaakkoIDP2,LZ DD4 $;hmarine_green.pcxmarine_indigo.pcxmarine_brown.pcxmarine_red.pcx   OR@]u u  (2 d  U@O0!+g #! {u"X$@^ #*7(#(**1 &,';"A7:>) 2tDM>4u,*=/?:/; 0Y*C&1/&*d/C2K^^gJu|g|*{uA/?QTwonuMZLl@u@BFGOKOOk^iii{h;`l^6[feji~`zO!^Z^s^z`}cR`1|`#`^?, "+ ,"^%G^`{sneF;;DGnsboeF;Fn? !   "! #                 "!"#  $  %#  &""#'  (& ) *  "+  ,-./ 0  0  )0 1 #1) #"23.  ,$%+4,+4'3-% $,5&2"'.3/* 3    #! " !6!"6!"  ## D&$+'*'%(1.$ /+* D,&&21,'+*'-%',1$&($) )$.  /,+ /4, 9,41;5*-015.  .5=  (%C!'(3"()7 #/*8 $9&,08*29:7;123<-"&(73#"2&95;6'-<0&(A:)0) =56 *-'3">94+@4/, ?2A.-A=?0 /=A. 0 .A) 0 7)># 1):> )1@372"#?B2.38@/$, >:9+7@74,47>44+;B63':A27-<80<@85,$3@<"2&B?63.'=6? */B;23%,C (CD!6C,D (D$6GEI89:EFI9;:FHI<=>GIJ?>@IHK>=<LJIA@>IKL><ANEGB98NMEBC9MFEC;9MHFCD;GON8EBJOG@F?JPO@GFQPJHG@JRQ@IHJLR@AILKRA<INUSBJKSMNKCBSTMKLCTVMLMCUNOJBEMVHCMDOZUENJWX\OPQXY\PRQY]\RSQPZOGTFH^K=U<]_\SVQ^uRUWIRK^I<UVTaMLXbXWYZ[fY(\]^_`aVeiMbc(YX^]ZkgddefdhkfgdViHMcDeWlb[hf]Y\i]fmn\jkfn]\kiieolmnelomonngkkedHp^=pUHip=lpioplnplW[oOqq_nrVs{l[toqW\sOQu_]nviktPQwGHQutHWwRuQIWH^puUpW[WsqOu_v`Vxy_qvVrxvqrxrzrqkzr{`|\y|QwXb}ZYdx~febWbY[y(^gxdefgnmekjPtZGwTpoupnWol{nots\|uQ|{[~tq~[q[squ`\_yQVaVXMeVbMeb*ebbe*Ybw)X}Z)wy}f(\^xjmfj\jmj}mj}m}jzuoWno{nt`vyxv}xvz}xzvxtuwW~{taTXLaXyw}yZNa`jf\||||`ysusus|u||`|y{tsu~SKTSLK**jjtwtwuWuWxx~nkqs{rrkz{hdgf~xvrxzz}kh{hrhhzrzrxgegmej:98:;9>=<@>?<=>>@AA<>89B9CB9;C;DCB8?F@FG@@GHHI@IA@I<AKJBBCKCLKCMLBJDMCJEQPOQRPQSRFTG<U=QVSIWUU<IJ_`XLM[ZY'^]\cbM'Z]^feddgfDcMh[b]i\kj\ik\nmlnomdekUp=pl=pnlqOosVrtonuQOkivHGwwWHHWIWpUuOqyxVxrVzrx{rzQ|yK_YZ}f~[Yb'^fejkeTwGWnptqo|QuqtquqVQyMXMbb+b+bY&Z}&}'^\\jjjjnWtnxyxxxWwtLXX}\||yuu|uy|tuKL++}wWW~r{s{z~fgzx{ejeNJETTwtZwTZTSK_U`_J_a_aN``_a                           #"%#"$$#!  !  ! ""##$!!  %#%%SUKJ_ZUN`J_JKNEJ`NJKwa__ ##&Z&'^Z'^Xc(Z^(cy^)cXZyc)*wb}Y*w}+Y}+$"";J==N>c(XV*stand01@ߒUpƣv{{Z͝p}wнsάjޣmܹ" ^"bٯެb2d-ݯܯI8򳂟αӷ=쯁 ާhڑNyїGXz8 x &· no̐=$94 >fC;"&<_Op!(/2%1>V4A"Z7R>(Hd1g a͌eӈ'\ hу}Xzw-d(ό&ه3q6|q Ė;8Ce1;5bu3ssn6Gid:?gB ^9vNs`g2&&=ky2zCZK:EanAxwBr"I#xIuTN}{ZhRT$O[?KR 0yeE(mYI}K]M(d#!SR{L\ZE^F2a$4d)"bb|ia˒q?og'imJEhA"dhpdh{VqNpq?qAqzTrq_qH`IQ:dWP3[0dN"<:^+%&M|tqL/ rАnɍĦ5K1Iv P,H' gD>'z,4K*؎V4 -,ѐA3Ϛ2x=pĢ !w"΄6ƚ,nwgp{hƌt9 ;zSpsqQF{zOK=>~ m{#sbiˎpqmsQhw:s~|krpsc+7{]ɣMӀ q\L؇RۉJ͌Piq?f7.{?2V^qUŌHqᛛRqWޠ$ޝqCq5qֆXVB+stand02@ߑUpŢv{{Z͝p}wϼsͬjޢm۹" ^"bدެb2d-ݯܯI8򳂟αҷ=믁 ާhڑNyЗG]z8 x '· noː>#94 ?eC;"&<`Op(/2%1>V4A"Z7R>(Hd1g a͌e҈&\ hЃ|Xyw-d(ό&ه3p6|p Ö;8Ce1;5bu3ssn6Fid:?gB ^9vNs`g2&&=ky2zCZK:EaoAxwBr"H#xIuTN}zZhRS$O[?KR 0yeE(mZH}K]M(c#!SQ{L\YE^F2a$4c("ba|ia˒pFog'imJEhA"dgpdh{VpOpp?qApzUqp_pH`IQ:dWP3Z0dN"<:]+%&M{tqL/rϐnȍĥ5L1Iv P,G' gD>'z,4L*؎V4 -,Ґ?3ϙ2w:XV/+stand03@ߑUpĢv{{Z̝p}vμs̬jݢmڹ" ^"bׯެp2d-ܮܮK8򲂟ͱҶ=믁'ާkِNyϖGXh8 x '· noː?#94 ?DD;"&<`Oq(/2%1>W4?"Z6S>(Ie1g aoe҈&\ g~Ѓ|Xxu,d'ό&ه~2o5Вp!Ö;8Ce1;5bu2rsn6Fid:?gA ^9vN 4pnN/B6F6;7[>s`f2&&<kz1zCYK:EaoAxwAr"H#xHuUM}yY~hQS$OZ?KR 0yeE(mZH}K\M(c#!TQ{L\YE^F2`$4c("ca|h`ɒp?ng'hlJEgA"dgpdg{Vp~OopCpApzUpp_pH_IQ:dWP3Z0cN"<:]+%&M{tqL/qΐnȍĤ5L1Iu P,G'hD>'z,4L*؍V4 -,ҏ?3ϙ2w} mz#sc~hɎoqmrQhv:w~{krq~rc+7z]ɢM p[K؇QۉIΌOip4e7.z3/U^pUËHpᛙQrWߠ'ߝpCp5pJ=!=*O>sXV!+stand04@ޑUpâv{|Z̝p}vͼsˬjݢmڹ" f"bׯ߬b2d-ۮۮI8򲂟ͱѶ=믁 ާkِNyϖG]h8 x '· noː>#94 ?eD;"&<`Op!(/2%1>W4?"Z6S>(Ie1g aoe҈&\ h~Ѓ|Xxu,d'Ό&؇~2o5|p!Ö:8Ce1ԋ5bu2rsn6Fid:?gAۆ^9vN 4pnN/B6F6;7[=s`f2&&<kz1zCYK:Eao?xwAr"H#xHuUM}yY~hQS$OZ?KQ 0ydE(lZH}K\M(c#!TQ{L\YE]F2`$4c("ca|h`ɒp?ng'hlJEgA"dgpdg{Vp~OopCpApzUpp_pH_IQ:dWO3Z0cN"<:]+%&M{tqL/qΐnȍĤ5L1Iv P,G'hD>'z,4L*؍V4 -,ҏ?3Ϙ2w| mz#sc~hɎoqmrQhv:s~{krq~r~c+7z]ɢM p[K؇QۉIΌOip4e7.z3/U^pUÊHpᛙQrVߠ'ߝpCp5pvXV+stand05@ޒUpâv{|Z̝p}wͼsˬjݣmڹ" ^"bׯ߬b2d-ۯۯI8򳂟ͱѷ=믁 ާhّNyϗGXz8 x &͇ noː>#94 >eC;"&<_Op(/2%1>V4?"Z6R>(Hd1g aoe҈&\ hЃ|Xyw-e'Ό%؇2p5|p Ö:8Be0ԋ5bu2ssn6Fid:?gA ^8vN;IxU7 4pnN/B6F6;7[=s`f2&&<ky1zCYK:Dao?xwAr"H#xHTM}zYhQS$N[?KQ 0ydE(lZH}K\M(c#!SQL\YE]F2`$4b("ba|i`˒p?ng'ilJEgA"dgpdg{VpOop?pApzUqp_pH_IQ:dWO3Z0cN"<:]+%&M|tqL/rϐnȍĤ.L1Iv P,G' gD>'z,4L*؍V4 -,ҏ?3Ϙ2ww`UtH[Ga9iLnXoQ\V錹*Sȃ.„ &'%ۇ*ΎG͉/pUӍa5׎I?|O2@PHC[38J==zN>cWV*stand06@ޒUpâvt|Z̝p}vͽsˬjݣmڹ" ^"bׯ߬b2d-ۯۯI8򳂟ͱѷ=믁 ިkّNyϗG]z8 x &͇ noː=#94 =eC;"&<^Op!(/2%1>!U4?"Y6R>(Gd1g aoe҈&\ hЃ}Xyw-d(Ό%؇2p5|p Ö:8Be0Ӌ5bu2ssn6Gid:?iA ^8vN;IxU7 4pnN/B6F6;7[=s`f2&&<ky1zCYK:Dan?xwAr"H#xHTM}{YhQS$N[?KQ 0ydE(lYH}K\M(c#!SQ{L[YE]F2`$4b)"ba|i`˒p?ng'ilJEgA"dgpdg{VpNop?pApzUrp_pH_IQ:dWO3Z0cN"<:]+%&M{tqL/ rАnɍĤ5K1Iv P,G' gD>'z,4K*؎V4 -,ѐA3Ϙ2w=pĠ !w"΄8ƚ,nwgp{hƋt9 ;ySprqQFzzOK=>| m{#sb~iˎoqmsQhw:s~{krp~r~c+7z]ɢM~ pZJ؇PۉI͌Oip?e7.z32U^pUŊHpᛛQrVޠ$ޝpCp5pw`⚀U쑑H[Ga9iLnXoQ\V錺*NɃ.♀썑Ä &'%܇*ώGΉ/pUӍa5׏I?|O2@PIC[38~==1@>8VO=run1܋UqÜtt}Zʗp{v͸t˧~jܜmٶ" ^"i֫ݦp2d-۪۪K8񭂢̭г=멁 ݡk׊NzΐG]z8 w %ɇ noŐ:xP#90U8N>,Da gaȌe·%^i˃] r.E):#Շ,,|A9He1;5b,gs+8i.3g> i1|ux2Ayx,5h*XV1AycJHRD1yoE,BB/o3p}? `85)^.);guZ'$-'k-zAK!=5&q|:x;s095y8-vcEmRvJt,E9IU4GC3[4( ])"`C}uTB4U2 ZLTX}YEZF/R",FV1"n[|vWǒu?h\'b9m]=x``ral_mvWmq?m{AmqT|g~hi8R M>,^DBKK0'U-;N($Ӿ&M|wrmƍ(/ q͐lďǫ.ƤL2&m'ТG,̝<& uuB7'|0=|L/K4 ,ن-A)n1i !"ycIu{wycn؆p:ł ;rKpfsZ|DtqkWTy=>r mq"zlyqvǒiimlIhpw`UtGX[Ga8iLnXoP\VƖ*×SՃ.N΅ &'ǔ%*ېGي/pU[<4CBUG=FX}3|;k==ÒR>|j,;>run2Td򨍙 fŒfݥޞgzvnryۢz$͇ߜ]$xʴg-çgշ;٨i0䰉ۣKoޱ߲޷?寁,Τw'MqI [zxx# ІĊ nu&̎0P*9A 0e5;~**{h>XmBQiObIa){lNmBd,+%&չMz"sgÜfL/ wfѐaʎ.L1IfuUJ&MD&T1I>='lź3>Lz(xCCP -,^6>6.EMP8i'!rÓ"AE7(enIo][CU ;bJbusR.N}! kJB=C5 m*'zX]ˎwkiR|Ph,~AzWvxZRc\+b77~](;o xk~Yه_܉Wό]i4u7.3/c^ǀUHᛐ_reߚ,ߝ؀C܀9р<Ā< qn퐲YoYXm8bMwvptdtU࠻kUaGiZng8_\e댲*SÔ.vtN &'%ך)ɡGǜ/рUKCk:@VDB\kG=H>NL7rLS@run3T Ðߡۗg ˃՘z,ȅב]$βs-⹞sϬ<ڢg. 㧇ҘAɼp٦᫉C᥄+ƛx&NH [zz!}#+Ԇl nx2ώ*P.:M'e2;2+9UP=!HIL:W sg%n#b,׉6\~zu1Ԁv1q*u=b8Ҋ=܆|SrZӐn;ė?oIcA֋EaIvhLTmPKiW uF}rRDrNBm{DS,rm2ZcYh^9TGpeH9]K\TsŁC,Rz}Izr&.Z#mt[xy]r^r]XxYm|rukny0h X,̅bZ{vE}"`e^xleyfy$}&pw͎yFr$S'9~g}{of}`DfxTowIN6e5q{ HP\w-*I-ڵM œӺK. ­4Aw(u3@ 8Iǘ,O6QJHBM?qR Б")GX)K}à¯(\ fc}(a SC6_mAaaylYV*Yme_?CLi5Sks}cieJhSUjmpwp{;w41Yk KtQ I\1x[؇h܆S͌`ixFnE9F;f^UHbrhޚQ̄Cτ5DŽ<< 㛅쐲[o[Xo8eOwsᚉf둖WߠnWaHi\nioa\g録*N.ᙉN &'%˞*¦G/DŽU?CY0RgQ4kh<}/EJs=P =e6C>nn|yn,;)>run4Uc _ؼdߚܖgssm"hƃכz%Ʌٔ]$sέg-gд;ڠi0 㩉ԞAǽnڬૉ߱F,ȟw&OrI[ztv""̆l no)Ǝ({P,:E'{d-;}-+9O"JXgdX0E[99F8b :zK{C7=mGB{=?)-iI9zWVÎ_i;gvBjq^rei'q)O- _C~%+6/a xo~xo~]ևc؁[̌bi4z7032h^UHvߛcr{iݠ$|ݝ҅Cօ9˅<< |gd됫]o]Yq8f}Qwlukht~YޠoYaKi^nl6c\i茫*S.}lk~N &'%џ*§G/ʅUŨD4 Up3ClZtjYrun5UhУi‘uZգprmڽkحujk f"iݺb ɩp-ᵉL8yմٺ=$ߴhOwܙI]z8xx 'цĊnu%AQ%9> ?eE}'%<`Or!(52$ >V4G"]5U>(Jf g aόe ԇ+\hs!Ҁq!mw5d1Ќ0نs=bAВc)ŖF8Me;֋?bt?vsgJLa_PEaL\EvBQLxONAgt2)V8w&kDYe=}bdECO] ?ppe7B?c6BU[Is`~:'+[k|;zJu=?_ilKxrL!c#wfQZld~^\o$^_?Up 0vyM(YR}>qQ)%!Q[{HfcEhF8$4},"^l\lɒ{4}n'dQ ;~E"bvvctTzKz{?{A{~Ub{~Z{Q~I\Xdbn3x5N'ZC{*%&ƹMvogcL/ scΐ`ȏů.ŨL I|'Զ[$ͲSmY"R qS[L.\4,-,ҠCJ6+%MņGas4!y"4]7&lrtBM0srl^mD~ܗ:,:\pZ.sMY}{AhKH=>2mDz^\Ɏ{xm̀[iWynztX#\!L FH1(Xs*6p v{~e~Vڇ[܁TьZi{4q7032_^{UH~{⛐[ra࠭${C{5{<{<{ {{o{k{퐴VoUXg8^Jwtlr_tRXeRaDiWnco[\a댴*QƎ.trN &'%ے*̚Gʔ/{Um]=L>'tR\5?run6ܓUyǡr[˟psv˻yͫvyݣlԾ!Є^"bճ߫b2d ۲ܰHʌ򲂚˶ͽ.믄 ާkדNЙG&Yh6x +ˇ noʐJ$97 KfN;"%5kOz'10$1Ba4B"d4\=(Ro1Zc8eϋ&^ iςw4E2:+օ/ׄ-|tO9SD<;>e~2qzi5Fg_9DgEt=Jh2z*XAO)DFZ=y=q&GVj G^kPP׀}It%Q[ LQCV0AqcB& kYtI}[ZW)aS$nQ{g\_F\?aD'4`O!w[|zT{~3ka'hiJSeTtfowgznn{hlv?n>nwT{b~shd3B,XW)O WbfC>Iv]AI#&}MtnrQiÓ,-un˒k/( &u R, J'~{EA'|,4L/׊[5,֌D3ٔ2Gu?pҝ ""ׁ"8Җ,y{k|y~rćw:wVppqiFxzhe~=>z mx%stwtpǒmpmpRht:szf}m|c+7x^מM|}g~WـS׊RىX:SZ~3u4/) X`sUHsTqZޠ$ޝsCs5s4WunZVattack1@śSԝ&ӧ)Ǜ-*ԧ*`|<_d=Ĭ;tot ˢ1x;`};L1lZ;C9~Ս>¼HΘ&yJB Y!1hF<03+.ȉ y&P]M4mFE1\ [k N.}V#5%9'E(8.L+ ziJK+~3 7.~D+Ck(% 7k4q4(ʂ*=X )v#C~KFƒK+aP*l<9Љ.Y%l^s.Ɋz2bT9ΈZ8Z=HptD3DwB:bBI Z3zU=+c)gi3GMT]e*>c x9V{H^8.5F-ĴCL!V41 |s򲞮:? =w,ԾS.ԽH) H ?*ˮS0 8F=mI>Þ TVattack2@ƞT,ϫ5  00l:g[4ۨ)lžˆij=`t6~A gZ-DF̊FŌA+u_&RIzbc%H&+Cȉ3 ;x,eFM*%hDN͉S;qCJntN!EwN(~Az6{(>iF?;sfoh[ ;x4DFA'7ZhB N/<F|vCitHbU *Qz,OFRoxad3tPC=^r7,_a(YkGYxNUɌQpX}cps`PWmoKd8irO7,ZV>]U.<ʽF ͯA)I'7< ":F Cw*ҬT.ӭJ)~'I$мB*O1 8|E:mTo 25ڇ;Eش<IL{7q %Ǐ%ўbԾ.җ$ҨI!lMbgزgpn oMgt!Ewma{k6fdϠ z!cę"YϾpcrzIr9_|fZ?oҙXG˙h rZPZ]ՁRugK͈qSsbo[nχo8Ԇ~R\p`VYߕZkۖSyݛSvhNdDb8k:tz i]OLqNmoOX\n^mK[tLFOdߚ~^lKqd:hrOlGnF[Pl3O5vr X\pM I+.5K5h`h`zDEh0\t RntG8em=>c=٢H>鎳Vattack3@ӂ@ʰ*ϋ;,ͦO 1ƹ1ZŇXQG9ӭQhPr~1`$*wCD8o7ܡFڹ.^f$J~RuLz"u&(o‰a} gx)#CI~O(Sh?zF| wVz|^w@KqSJC4WmCHG5coqu ;RAFK'SjA Y/W_~Xm]p_ *],aJͳhnwb?lA=l5,ta(plGqxNm֌epm~ct`RhɒtKmUiyi78dVIw_0 8 ƶe.#>(8 Gϋ7 7w*S.G) 'I$?*׫J2 :7:Qo 258E<IŪ)Ύ5q %ԏ%ֳb)ݝ$ީ(!laozon ςLg!Fwy||8zdѵ z!sі"_crHr:_gZ?oԮXGͯhylXIPӁI]uςe~w{ˊr}ϊӉχքp|\TSQڝPءIP~RmG`NE䌅xqXOF۝B`}mbzYoM~=tBP;CWh^xqUlz_jpnvo[r}l5Vq=G> &ybVattack4@·Uנ(ї5'ҙN ۹/ҫ0T;NWG9ϨjƉß s%^t%~1 zIE.vFҭ>ǚ=_D2'}RH'&zfc+S&K8x j-Ú%z"[7Ɉe6iBN̉MEqFPltP'BwO.:u=p4yEOAE<6v2g0F~:lFi_9kv>bUQ T=z_.'\AphINIbkT'>2l w?k{Mg@9_=;/?g883z$bobd ;{<>F7'9[o>!H/>GzxKm{QbM *L,N<[o}h`3wY>=^z5,b^&\gG[uNW͉Zpa{cup`EUexK]CilY7"SV5fM.<7 űD)!K(7; {:F Fu*ݳS.޴H){'I$A*ըV2 :E:vTo 259Fּ<I+́6g %ь%Ԩb)ؘ$|ا(!gMWaޮrqn uMgv!Ewocxt6qd̨ z"uaɔ"Q׸wcqIr:\cPZ?oСXGʣh v eNCMnӁE\Y͈dcs}oouhnЅ|ˌԆrb\~_UK۝L{ءDٚ}ViG[NtEu8}:{sUJ>ܝ:aama_YrrK~9nt;P4>v۠kv^\qUNh_djXnyV[vcl.Slmattack5@U(č5&Q-ӹ..M;~IPF5 j e$fxÒ#0yE7(s?>L_CY T)Dh JDDD6#x"zUt2G8#m5R v"Z{& W:('J<52H';8<#[< iBZ%K C> 5M<4{>(' <[*%k,&Z!3ǃ*IX3u LŐTNC8i>6l(Fρev dc9Ɗk=bAEΉJCg2MluC0Aw?88r9aՄtz0SA2D5wfPh/,:lNbQ;kp&=b>S J>zT>'\&qi0Q<]u\c(>m x'`{9pA$c'w{*t#i*$r3aoMe ;oP??'Ev?"-/%|rUpnXd5 *5,3'k6lsa)fh?+I5$J_&BiG:v5Šhdzl|f_rU19QxKHFiX[7;Vg50<7-B> ݬ@H41 lc}O9? ð+v%T/J*ur{ I B*|ƭT1 9Fw]V͊gbhv6wmo;8Ӆwd\_JAݕIpڡ>{ۚv&^NN~D~9:~;u PB2ߙ-e^ne]XxrJu7^v2P,;gݠosbYZ[Ihe`lan[\}en{)zO0YS";Q;SN{ yIz,y/.H.UֈcEE`1+^s(MonV7|m=z=SJ>Dob Sattack6@{YGɒ=Mxt#۳+Ʀ)G|5AG31tt 1g^|#4 jEF q>JIY@R T Ah:4>2#&āxzGqM:m=B t Zx"H:J1006'B&:;Y* pGK*; C, %N*Fv+! 5 [1k1Z*!Ȃ-8X!u;~E =I'aG%l14ρ#VgWk)NJr-bJ4ΉR3g7?muD%BwA.9rASՄh|1JB395wX`0,z:s?bW*krH R.zU4'c&fi0FDO\\(>b x/R{9f@%Y.k{1gx(~m1*u%`6M[ ;pH>?'!FpA"-/%+}yGpuKd5 *5,9.^6pka)gb?+I}5$NaGlAx;\d_}fesU7?QoKG9iWP7;V]505ӾF-LL ޭUG31 rh}N9? +v,ҽT/ӼI*{x| I A*~ĮO1 9FZrOattack7@ȈTМҡ)t#,Ӧ*T|5NW31tt ȥ1v*^}1D lS-C->ĭJŖIg@ER7h; 550'%Ɖxz GfM8 mB?1kZo !H5J-0*5'D#6 S& pIH*~6>* |!K'Cm& )4k3Z/ ɂ,7X!v;~D>’K%aL$l73Ё(T fWo(Ɋv+bO3ωW0g<>ntD(CwA0:wFRք&f~1MC3;6wXb0,}:xG X+zT5(c&ei0EJL\^)>b x6N{9gA%Y5g{7dx{/~o50x$^6L[ ;pK>?'*FsB"-/-2Cp|Gd4 *4,A3[6tha=gdA+H6$Sc'NnIyCXd[~emtU<GPoKG8iWO7;V]500F-´CL!V41 wn񱞪:? ?w,ֺS.ֹH*~ I A*ȭS1 9F<T6 2.;FѲ<I(ǁ8g %͏%Тd)tј$qѨ$"{lNUϲwrn yOgbߋ"FwZكN|v7oEƛ z"uTEsfsKr;t`shPZ?oʓXGėmt aKDWU։JhiHϊtUsi8_ˌыx8ՆY\|UVLVauKmޚSgMXݝvDv9z:~; `R>:uOprOWd[eGLv>;pJWࠁdpKZk8iwQnVoP\Yn)O0h]"FH\N I,/.H.xU{aCEa)-^vJd{D7mmY=@=!M>o+OVattack8@ëTй)ؙ**r|Ǹ5pv=̪;.t+nuŅǴ1ˊL`}N^2mmMCKԟ>΍H&ԊJ:R/h?7..$*ljx yK[M3m?B1^[k J/~~V-.%5'A&0M#-~bFI+w3"9* yG'Ce" 1k/q,%ʂ(9X%v?~GCÒG'aI'l47Ё%XhZl,͊v-bL8ЉW1g>A [)zS5*c&_i1>LK[]*>\ x;I{:bA%T8c{8cs}3{r21y#^6MU ;oL??'0DtC",%27?pAd5 *4,D2[8wdb=feBLF6$Ve'RpNzITdUeryvU:KQgKH3iXI7;}VV5z05ٽF-ʲA(!H41 wn}7fm>='@>/ocX7 pain101@V$`RϠq[v!!R鬗ʾx Wս|]֪%rե[ܸ 뙪s:Ψgf˳糣@bûx뻲ȷ˷ Ȳů+ܨv(I؟Ku YFhk%9J1Sl/&"2}5wEzIOK'ƈ/QqAI\& U(.\;'9<΄f'-pAY!.i6!\?0""Q^q1+Έ;*p#4΃BAa&(s;*<]5DEn7FՉL~\ƒ]HSXۅbSn1V؄MFmB7ptD'AwA/8w"l2Z4EC485w6QXX1.m;gXo]zk8b9E X2yO4%\$^h-CFNsVU&>[ x>D{5^J#S9Wt3[rl{w9lr%v{u \dFU ;kC;?'5rFcA!+}/8{=~wMn}LGo5$Xnd'VupGRvNI`Lesfw_DIdKA9iOL76wHW1t0f-q.Ysٰ?c'OcKYMl0c N;<ܫTNo2~5W uô3z$ѨX/ӪM*A} M!͵E%gҢU4";51zW8 2.=Fֱ<Ip(8g #$ӡdѺ*h XH"|m:Nlt sPird"Gw`Wzy<\8Π h"rN&dfqwKq;ayhPZ=6қ]G˚mH8,"ˎ0v$~ҐFo-v@`:YՋY2^;P 7rcRfVC SX`O*?I3HWwoJ~<%  S |y)s^јTΙ Y"tqnz>ij=ea)zC1SA0 6'#V :K(P=N/62Y1`?F XyDsdFF[))^qGd~88[nM>%=w%E>_hpbٖpain102@ԍV%hP—ri$#[Ȩxߺ2cӾ~˨X޹*vԣӰ xwȡrv֮ └@mx隣骨l׬=֋{ї+I’KcY/X k;20NY,&!1,x2zKOF l4MZ 4HR U).L7'=6ŅWs%-pDT%/[1 [65!<L 2%l8$b%1Ƀ9=\ (s>AX;H:p=<͈%H]ȐbO[L҇gIp:N҄NBi<9ktD%=wA,5w,g*]{3D?463w+SWW/.l9kRpY$}km6b8G U2zP2"\#`,EDOrWT$>\ x7H{4_ J"T3]{0_m{x2olŎ$||s![6EW ;lB9?',wFc=!*//4yuLnwBd/{ *2v,: s:qZ^=cW;LGo3$Sr_'O{iGJsNB_oR~eknq_ >IgKA:iON75zHX/w0y23k{׭Cq,[iHb{5t2^IFYN}14g|3v%ϱW/ѳL*Pvz L"˾E%iӢV4":41W8 2.=Fּ<Gr(̆7g #}$өd*d ZßG"wg)<ts zOir]"EwX}Nuy9c6Ω h"rJ*lcg}Jq:u\tcPZ?6Ҥ]GˤmXH8*'̎;oՄ-{~PeӉAozl:dϋh2k<_2NmceffC)Q)ѡ"R\+I&:Opy6e}WA7Q2qk=k]8{ؖR ֛X.7ig(`z*vj[elUa\ErjT0`JJ1/= )S&AHB]+[*K2q1s>\1n{B{cFF\'&^rFdw>7bl9=L=K>jiW`e|͵L܋I&ТJKRAk<1>+%,ā#xzJiN=m=F mZw &G?{V,)53'C,A\#2pHL*1H+ S*Fs! 14k5g,)Ȃ09[%w>~C>K-iH/l1<ρ"YeUh8ˊq9bI?ωT8g:08w=[&a}0JA1:4w"UP]/*v9sGbY%}hm :b;F V/zQ6&c$bi.DGNrX['>^ w4K{7cA#V2b{3br}.vn+*t%\6IX ;kI+Ex4$P{a'KlFu?\6W}ehwrU0AMiKD9iSN78~HZ2{00?-ѭA(w"{Hy41} c[l󭞘Iƫxq {Oi\χ"EwVKxw7k6ʢ z"uM8pdqJq:^qdPZ?oΜ]GǞmt bQ=9ѐNaՉ?qaUχ``h~w8wnˌ19{1obcBA=ؙDqס:|Wr+[&LNxEz:;mbB7/ٙ)Y^m^\Yer;y/au/P&6iX\tYYZPKhUbl]nyX\odr.vVr0NO9Q7QOitLs,m/5K0z_~aEE]++^r HdwB7hlh'=S߫=UM>mOVpain104@ŭTѼ)ݘ**s|Ƕ5qw=Ϊ;.t+ouŅȸ1ˍM`}N_2mnMCLա>ьH&Պ@:R/h<6--(&ȉy z"GZM5 mC=1^[i H0}~V-.%4'C"80 M#2}bHF+v3"9( xG%Ce" 5k2q1 ʂ)7X!v:~D=ŒJ$aM#l91Љ+R"dVq&Ɋx*bQ1ψX0Z:AotB-Dw?5;wIOք&h/QD1?7wWd0*:zb?E X,zT8*c(ci2CJM[`+>` x5O{;fA&X4g{7dw{.q81w'_6NX ;nOA?')DwC"-%,2Dp|Gd6 *5,A5Z6tic=egB+F6$Re'MpIzCXd[fmu`>HRlKI6iYM7<VZ6~.5ؽ?2˱A(!H4< |q:F ?w,ӸS.ӸH) H ?*ӭO0 8F<~S6 2.ޑ;FѮ<I(À8g %ː%Οp)uϙ$sΪ&"zmQV̳vqn yNgbݍ"Fw[ׅN}u7nEƘ z"uTGtfsKr:tbqiPZ?oʐXGÓm x dNG|ZSւMfmFΊwQhf8]oЋu8ՅU\wUVOX^ܡNkޚSl[ݝqDq9u:z;} cTA=wLptLX`]bIKvB}PG=J>Uk pain201@ØU'š5 ۩N /ٺ0];~[PR5ѶmӉv0_t(~|4 hL"F4Ƀ?Ž>թ+nB:7wTf%8sz 1e(W KvVv{T8G g1+r_(niIpzM`]phey_8[јiKaOiof7'WH9uQ0;7 C)K'8مqp"֋7݆4~.лS5ϾI3l$I%A3۷I0'87:{Op 2<̖;d;I°)8r %ќ/ʯi3#zޱ+${G^smm MrHu|"vtoqp͵ s"fכ"I߼kL9mvQZ?oѭXGƪhk `R=07h̀0~GUɂ/uTigm`amt׋nsnoۆ4bTOUUdbDH.ԙ2Oh{w2RW-VvdsomwJN^U p.UI[$lKMinyFv|8^s^>nhRDZVPF`vZp͇Ccm/.\|$Nm~XorhbH>!j=_L>*NZVXpain202@”U'ƣ5 ٢Q /۲/Z;XPR5αmΆ w-_t(~5 hL#F0ɂ?ü>ա+lB:|TI'z cc1K(C 3}:vgU$G$&h2Q V[Yk'$.uJ)0$U,/5")A6 ni;W.i9 /Bd"79EZ-x4~#(|X )z!,[8"UYBt[h c75a52k#HÀo{P*j],a7?łH2k xDlOk@;`?9~P0Ki-53z!_oac ;|D>F7'?_|> G/GRzvDm|JpM *M,P5Rng`?z`>=`2,k_(giIgxM[ϊRpZew_:WdwK\?ikW7#RH5eM0<7 ƺC)K'7 tp"ҋ7 4{*ҭS5үI3q$H$оA)ײH0'8}7:kQd 2<҄;d֯;I+8r %ɗ%͝iҽ3ם#y֬( MuJ]ݱenm wMq|!Huv"nl8fd͟ s"cə"IԺzhrK9gqZ?oї]GƖhq eVB5?^}6w~OLy=^Ҡqbng[mzՋupΌu HP]bS`tnBGM4՛4Ns\5^֠&Zu2\xP1zI>XEGxs8xVch\ o%PGV kIaty?5o:xLߌ|aCpc:fE`f\nwDei#-\zLp~Gofm>T=SM>8>Rpain203@T'Ǯ.ٜQ */d|<ab3ͭ;tnɆ :`v9I m[6C<ˌ?Kښ(vB< T0*z SL08&8%}'x[VG)m7J1Y[_ Y'' vJ*.#C(5-&D, si>O)n4 14k :/D]&$( k'"q$0ă$GX4tOX T<.a=.j+Aʁ dwlZ,ʈe-i?=ˉM7ZFGmtK,CwJ3:u7]6t|>SC>?7vh*g0<8l=i^'~zr)b x?X{Ge@4Y?.'8SvA =/=F|yAm}CpD *D,J4Wo{ca3p`?LU4,`b']mG\wNRRoUex}v_:R\oKT9ibP7JH,]D0G@wGOpain204@ʤTһ)ܘ%*r|ʷ5ou3Ϫ1.t*nuŅǽ ʐL`xM^2mmLCJ؛>ʍK&؄@CY0kB52*&(+ȉ x"L^M3 mCA1aZm I-V,.%2&B$- L# bHE+y2"7({C%Ch"'[4k1q0%˂)8X%vA~GCĒI%aL'l77Ё)W"gZo,͊x-bO7ЉZ1gBH{?dA*W;b{ëj6=pain301@ƐN!ĥ)䭓 //ԐNJ}ʱ9҉Ő䏎=Ļȴ:QtIbΌ϶;ΔjrSwfr/{N'{3g('~ B3I ? j+zw;) ]9? aY$:^0yXq"k.B!6k{EsMA3,A$Td6S0 }+v^Z6)FA/+N(An8 *p.$.WKgD XJk71P3u9Á)-+|Z^e iU jE%ˁ9=-6p$^lll,];puur eDqPl/c&i1pZPX.> w1I, !?< À9?ً=݀*T3K3ᆎ$I$C.0'6?8ĢRd 23¶?6;I,:s %֠/m.Э  M~hfnp OqtHugUFC s"_ZnrN9srl|Z?oʹXGhs } }|eh`G~v&kg3Ӂ%ms%PLE>dg9T6k<T*UAKreaC؛_RNU)i,tfp n nx:qNisfpmsn#Qh`)~J9^dRXSE۝{iӠZrl zgWhD7UI8Nz%zz|z&!U=Y59:8:>'8!U\oĬGe\a4^pVNd}v7h=} =$J>«j+y=pain302@ƋS ͠)"󣕵 //ːȇ}ƻ9́NJ勏=ճ®;Dt;dǦʋĭ;͛fTx^m1i{bA`|ֵ>I'ԡJA}S 2 g .=2= 7%xz23 ^);iR+;_}Xql(D 3l}0vO63);'O63Q( |cT9)~==)"K#Am-n$XDi=[Bj21W, u/!ƀ!4"(|U\]mKLj:40L%Cn#cblf+\>`ouDOCwAX;wY0Љ(2vD3c8w'N1-:;cbGhqB=b>g c8sTZ*c&i1eVO])> x;gv;A'|=q{Cb{/rFm<|zH_6M| ;qp??'.HC",%07ȊSb~be5 *5,IAY6vc=iBLJ5,Te'MqLӅMɛhe}yfmwSLOQKHYiXp7;V~505?2͸K,G ?< z9?;={*S.I)$I$A*ַM1 9?:ڒR6 2.֫;E<I­,8s %ݘ%ʾp){ܣ#{ϳ u]_ļ‰pn NgnHwcSFDż z"u[RhrLr:~gsqZ?oʵXGh rZU\~k5j\GԀ}/lq2џXdKd|9c7<-`DUcKb|ZPؘT_۝y)'z1e%o+n.4mXbiWSl"hm/RmZ>[n7[=pain303@ǯTœ)噙 %*x|ʶ5w{3Ҫ;2t-ouŅ ʓS`xQa1mpRAP|֣>ՋH&Ս@BT0h851-,É x x'A^M5iG51`[n!D/j~V+.%2'F8. M! bK=+{2"8%}E!Ci" %9 k5 q5ł+2X!v#2~=6’MaRl=(̉.I%\Ou!n}'bU*ˈ^,ZAEltI/?wF78wKGӄ+k}7TA8C5wRi/299bb,}hv#:bAO _+zX<&\(oi2MQI`d'>k x;Q{LN4$Wa'RlNzJYd`zfqrU?LSvKIAiYW7=He705ٽ?2αA(G4< {q~:F ?w*۹R.۹G) H!?*֭O1 :F<~R6 2.9FѸ<I(Ʉ6g %̐%Чb)xЙ!uͩ&"lQXȲvpn zMgiFwa܅S~w7qEƣ z"uXHweqJr;t`xgPZ?oʜXGßh w dNHt\KӂN^Ԁp?̈nHsc8YomkDRInSX>KtAQ8rDWWxKg>[^-hl=k:p<]Al.SyE@wGO=pain304@ʤTһ)ܘ%*r|ʷ5ou3Ϫ1.t*ouŅƽ ʐL`xM^2mmLCJכ>ʍKIׄ@CT0kB52*&(+ȉ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'4k1q0%ʂ)8X%vA~GCĒI%aL'l77Ё)W"gZn,͊x-bO7ЉZ1gB7wXb0/~9{_ x>H{?eA+W;b{;bu{6zr54|#^6RX ;rM??#'3~HuC"2/6;?pAd: *9,G6Z8ydb=ieB+K6$Ye'UqQzKTdTeuxvU=NVkKM6i\M7 BH!Z:}.0ڽ?-ͱA(!H41 yp~:F ?w,׳R.׳G) H!?*ԬO0 9F AȝWAjump14@ӑV ܦ.ʚ --{ŽЦ{ؼ<ǁӧW.ޭձ turoդȇױ ˷卓UwƯ p䠥>׉{ɛ=I'߸A7|2 TƐ,&!N%T 6ɉ$e"P728e%ShgM2);hY/L3O?P<4% I-,qp;`Ii167$u-DZxHASwBnք 8XU;HOvZ%bG7yNfiaAomt4]p>L k=ds\\x}C`'D.V*s{%o.{yn3# }.hV}:MZ 9tVY3'OsQ 5,* ǀcvcYc:x*?t2,)+Ecr)mhV=SxI,ChG5oO)~N#ҒvffBLzU80IoI=H\NXD 8} M^Bp3Ӌ,Ҷ%ղ{,~ i'y$˺v,OnsCl ␏:IJ80Ř+ޛlCJ42Gt̨(LJlgE%\%PcR+[ޠ!_ذV!D{sVLHһW8DŽkvBuA"vVx</{ChBBz]v<ˤ<s~su}t]qqY/ɴ,>Þ+erE6W9Im s_HCSoԂH}bflqmgE{z6:7Ӆp\UVKSwrIޚ}OfWND9:; ^P<9rhpohXxXzDgv=8Gp}xmeZfXhsklmoh\pn)S0f\"EG[N I,/.H.UŘtEh4Hcr?l[9hzhr=F#=%8>'F.1Ajump24@ӄV ܝ.ʎ --}軏Ѧ{ض<ԧW.ޤթ;tro՚‡ר ˷升Uw򊖁Ƨ p堛>؉{ɏ=I'߸A:|6T,)!N)U #6É$e"SQ:2;e%VhkMN5) kzY/L#3MCP<7% L-$tp>`Im197!w-G7#D+0*z{&I+|J`P9&V(b[;M* :uE\4e&T^L&2W%*xr^}\T^8I*=N-()/Eal=pUU>U]C(D\H4_O%rQوq_axfHmU=/ŞM;ICaS%F]9KO- ~,r%h, bu'c a,OQ"ZCkŖٞ:0ˋ+[F'5 GsϠ(ʔWg!%W%Ȭ5d6*Y!_ک'4!rcORJԷ2ޥuk;}"_4x7t*o|Be@z?u;ϝAvsksZt{bXhaRϝ >bWå'm weMHXmςM{ӁeenvkqE|zd87͋n]UJPܕXuءN~ښOl\ND9:; cUCߕ>wfpsfWv]xJevCޚ>MnܠvqcZlVhwhlkof\nn)S5ka"KޝL`N I,/.H.Uf7dOim;uV.ez>=bU>2U!@jump34@ԒV ݧ.˛ %%Îө}۾<ԫ)ᯘֲ zuxo֧ɇײ Ϲ䒒Uw򏟁ȱ p礤>ٍ|˛=I'޼JG.|B#b9<'O8c1F̄%3Á.s`QG=ƊLo%bzt -WND2;wY%>6$(WCDo$;"v2 zGTЊDMkB`҆8Tq0Yσ.n#bt"hʐVXiW^ɊEgӉ90!tÍt]nvdaYeψYkZ5t{?4r}=3l{Tyׄ 7Mk4@Ekzy?gLd>Q^[(OoHErO>ԂQ9ٖeuf^~`LDɧ7=*A +g3&*d }2.U-VJ0-R?F IC–9ʤb6 0͛3ůJE9Gӫ(Hs%v%*p*+k!o߲) mu^Zھ'~ů ]hUT)xOBDDz3uPҧPtsYrKtqmdsP>ɮe W m oZUdyւZفqrl~xqEo:7ԇz]_J\dݡ[ߚRugND9:; naPLtp|tWhWsvQLY{᠊zqZvchulxot\zn)S5ul"XYkN I,/3H.Uo˩QER_gh3k4m6 >=qh$>ƫjJ=jump44@tT̎.ᇙú %/ⴎ՛~}ͮ<Ǿ֜v~)ҜƟ;lvuk}oŐȽǝ өrUx􂀁 m薈y>y|޻}L{I'߭wJQX'+|N/['9H)OCY<>2/:k"hQS8Wg/is}:TMP/~oY#y>C;GZR v&}*S3|?$WYCccdA;d_6?'H5$+#w$mix]`~`, %0*?MebU=_r3>J1%GLG=TO6iN1yvUlq_UcU\GǟDK;riJ72V,<շ? ϥ@+HF: }7>:ȡ>t,^%R+ kn'Q!K,מO<"IAx̏Ԩk: 2Ҁ(ѳ?C5Iҙ(>g %e%c+i#rڧ$!uXOpaѸƳ _hKxMxGo;hx@qV z)uPѝ\hsVsHv}UXnURZ?ЮdXGm цԆkermjiyρ{gnkqe}d8E:m]`Vlؙstӡk|֚H&vRD9:; |p_ܕ]fpfXuwvfev`P\hmؠucZVhglioe\mn)S5{"fڝgzN I,/.H.UzڬKENJ\dLOnay4q}z1r==i6>īj=jump54@|SΓ)厙%*﷎є}Ҳ<ї~위)آȥ;`~t_dƘuȢ1Ѧ|zTxy1m菍}>w|ںLI'۪~JFW'{Bj"W:=,M7P04%(.f"`NH0Mc-a}sv.UME*w nR({=8 v)}0 V|2{;&^G9kRaE ;iY8?'G|8!-- {'rrychc0 *3,>@dhX=co6LJ2%ISGA\G;nN6܄fttf\hUMDǛIK@giPy76V0 <޹?2ժ@+HýF< v9C;ʨ>s,X/L+qr'L!E,ܤO7"CCÌ1_8 2͈+ܰ;F5Iҝ(Ş:g %p%b+l!qަ'"w]N_[س{ձ ƘVhQ~HxKu@nB}@ z%uPњQesOsBvWWoZR̥Z?oXGm p[VenςZ}sdllsEzo:7΅o]UV]ܕewء\ښOvhND9:; obPߕMgp~gXxizWfvQޚMZoܠx|dZwUhhllog\on)S5vn"XݝYlN I,/.H.UϡnFEVA"^k=Lniz7|~zF=8=4M>E@wGO=jump64@ʤTһ)ܘ%*r|ʷ5ou3Ϫ1.t*ouŅƽ ʐL`xM^2mmLCJכ>ʍKIׄ@CT0kB52*&(+ȉ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'4k1q0%ʂ)8X%vA~GCĒI%aL'l77Ё)W"gZn,͊x-bO7ЉZ1gB7wXb0/~9{_ x>H{?eA+W;b{;bu{6zr54|#^6RX ;rM??#'3~HuC"2/6;?pAd: *9,G6Z8ydb=ieB+K6$Ye'UqQzKTdTeuxvU=NVkKM6i\M7 BH!Z:}.0ڽ?-ͱA(!H41 yp~:F ?w,׳R.׳G) H!?*ԬO0 9FE@wGO=flip01@ʤTһ)ܘ%*r|ʷ5ou3Ϫ1.t*ouŅƽ ʐL`xM^2mmLCJכ>ʍKIׄ@CT0kB52*&(+ȉ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'4k1q0%ʂ)8X%vA~GCĒI%aL'l77Ё)W"gZn,͊x-bO7ЉZ1gB7wXb0/~9{_ x>H{?eA+W;b{;bu{6zr54|#^6RX ;rM??#'3~HuC"2/6;?pAd: *9,G6Z8ydb=ieB+K6$Ye'UqQzKTdTeuxvU=NVkKM6i\M7 BH!Z:}.0ڽ?-ͱA(!H41 yp~:F ?w,׳R.׳G) H!?*ԬO0 9Fǫj` u x7k{5{A"oq:x{Bq{/rH;pG^6Fn ;ei??&,>C"(%/4`bwce0 */,C>Ŏx6mb=^BLA6$Oe'LpIzCŎuEufivUHHJ~KAQiPd76Vp0004ҶC("J41 rh}󲞪9? ɲ?w,R.G+z H ?+ѮT0 9C<ԐS6۷ 2.Ρ:F<Iʦ(8g %֐%b)oٛ!nլ!"unWU̷qn֠ Ng`"FwYM~FE z#uSJgsKr;tzbmk͓Z?oXGk y hXW}~o_Ԉ`lׁZˊ`q7u6<7;c^_J]dmr[wޚ)whݝ|D|9:; oaPM\p~\WnipWZvQMzZcߠn}XZxIh]l`o[\cn)S5wn"XYmN I,/.H.~Ǔ_ԙBEXK/^lBKdw`7mI>o=*3H>ūjUj)wWӇ++{F-k:wK]2&:icXYhaT?b5g eXhIc,c!i*f_iuO+>~ x9{}2B!zqD}Pu{1uc{LlY`6Bx ;axA?&/<̛T6μ 2Θ)ũ:F<Iҩ 9s %ߓ%b)oߡ!pز! spk\snʧ Pg_HxYNCB z$uVŤYksMr=twdlmZڹ?աoXGm {otw؀bl~iւdocqFE540f^UHuߔ|ortxݚ,ݝ}D}9:; yhe_p_Xprp^vie{rfߠp[ZMh`lco^\fn)S0"pQqN I,/ę.H.UŘaˢCET[1^gTLdur7m>%y=QS>ǫj)jflip04@eުO,߬SȪ\w'ܲ{#Ői*ƴsxĻz8Ƀɐ"φ.)ε?٭K4I:APzdTATQ=&v:^/I5wV%cyh#hC9]wux ~bW&^(lnfQnCXFI-o]$o)5qngF>$w$i40} u-^y}%sn32:'&ix*-lgHq+xw9bGi<>^>d;~JÉmVgQ~WE`_JV]?smv>aDw;g;wU‡1~-C/o8w`[0'7n^^]{heX;b7h r[hLf*c"i+gnirR)>~ xA~}4@"{qN}^r{6p{t\r]ZdDy ;d|=?&5?A"'%7;}wc}wf. */'SfFp^=]>L?3'Tb'RmPvKЊf{_pr`sVGK>]iMp74Vz/.0ֹ4%>&;zٸN̳4ŤI1ը(r,N.C+ { D ;+ӫ@- 8K<ԛO6Ի 2э*̩6F<Iҟ 6s %%b=wޙ!{թ! {i~i¶mnϧ KgfDx`TzAB z"u_ÝkdsIr:t~_rgƛZ߸?ڡoXGm rɁbfƇhob[FF.?0e`ǀUƠHϛmrv͚,Q|D{9~:Ă: }ҕ{^p^Wo猗p逄]v~КzzّeϠoZZLh_nbo]\en)Sǘ5"НN I,/ڗ.ТI͝.}U͙[Ӣ>eW_/^lXHd|u}7lU>u=)N>ƫjƐ6flip05@e߫O,߯Sȫ\w'ܵ{#ɐg*ƶsvĽz8̃ː"х5=г>۬H4C9CLzcT?VM?&t:\0G6uV%[{e#hB9Vyup `X&Z)lcgQnCTHI}-oY%i)5goqE>#x$e50~1q-^r%sp056)~&it*,leIo*xv8aGh;>]=d:}IƉlUfPWEa_IV]~ x<}2B!{sK}\rt{3rzrZn]]6By ;a|>>&2=B"&*48ŀxcxyf- *-'PeŐCl`=[AL=4'Pd'NoLyGҍ_~_ku`sTEK<]iKp72Vz-.0 ֻ?%>&;͉wټN̵4ŦI;ի(u,P.E+ {}'F <+Ӯ@. 9K<͜Q6ͻ 2ҏ*Ū8F<IӢ 7s %%b+sߝ!w֭ vl~g»pnɧ NgbFx[P|A@ z#u\áifsJr;tyaniZٹ?ԡoXGk цṕaneȈgoaZFF.?0d]UßHқlruК,SzDz9}:; |Օy]p\Xm쌔o[v}ӚyxܑdҠnYZKh^lao\\cn)Sė5"ӝN I,/ז.̡Hɜ.|Uƛ^̣AeT^0^hXJdww7l>I=>aR>ūj|`ͼflip06@eߪO,ݬSɪ\zڲt#Ɛi*Ƶswżz8ʃɐ"φ5)Ҵ>ܬK4C9DLzdT?VM?&u9]0G7wU%\ze#hA9Vwup ~aX&Z)ldfQmCUHI~,pY%i)5hnqE>#v$e50}1q-^r}%rp056*%iu*+neIp*xw8bGi;>]=d:~H‰mThPWE`_IV]=tn{~ x<}3A"zqK}]rt{3p|q[n][dBx ;b|=?&2=A"&*48~xcxxf- *.'QfCm_=[?L>3'Pb'NmLwGҊ_|_ks`tTFK=]iKo73Vz-.0ֹ3%L';ɄyظN˲4ģI;ը(s,O.D+ |{ E ;+ӫ@- 8K<ΜP6ϻ 2ҍ*ƪ7F<IҠ 7s %%b=tޚ!yժ! vihnnʦ LhbEx\QzA@ z"u]ÞkesIr:tz`ng›Zڸ?աdXGk ͆oɁandćgo`ZCF.?0c]~UŞHΛkru̚,GRzDy9|:Á: ~ѕ{\p\Xm猖n耄[v~ϚzxؑcΠm퇨XZJh]l`o[\cn)SƖ5"ϝN I,/ٔ.ϠI̛.{Uǚ\͢?eU^/^iXIdxv~7lFq>'*=T>īj03Xflip07@EߪV,ܫSɪ\zٰt#Îi*ƴs߰wǼzӐ8ȃȒ"Ά0)Ե?ݭK4G9CNzeT?VO>&v9]/H6xU%`yf#hA9[vut ~aW&[)kfQnCVGI,p[%m)5nngE>$w$f5/} r-^v}%sn148(%iv*+lfHq*xx7cFk;>]=d9HnTiPWE`_IU]>smv=`Cw:f;wS/},~C.n7wa[/'7n^\]{hdW;b6g o[hKe*c!i*flirQ(>} x=}3@"zqL}^rt{4o}qɁ\p\Z6Cx ;c{=?&3>?"'%59|xczxf. *.'RgCn]=\>L>3'Ra'PlMvHщ_{_mq`uUFK=\iLo73Vy.00ָ3%L'Džǁz׶Nʰ4ĢL;Ԧ(r,N.C+ }z'D ;+Ԫ@- 7K<ћO6Ѻ 2ы*ɩ6F<IҞ 6s %%b+uޘ!zԨ! xhiln̦ KgdDx]RyA@ z"u^œldsHr:t|_pfĚZݸ?ؠoXGk ˆoǁ`dÇfo`[CF.?0c`~UƞV̛krtʚ,QzDy9|:ŀ: ϕ|\p\Wl䌗n怅[v͚{x֑c̠m釩XZJh]l`o[\cn)SȖ."QNš I,/۔.РH͛.{Uʚ[Ϣ>eV].^kWHdzv|7l>=S>ëjVXflip08@EV,ެSɪ\w'۲t#Őg*ƴsvŻzӐ8ɃȐ"φ5)ҵ>ܭK4L:AQzdSATR=&v9].H5wU%fzg"hB9bx{ aW&](thQrCWF&,o\$p).wpgE>%z$h5/ t-^|#wn31<%%iw)+lgHq*xw8bFi;>]=d:~I‰mThP~VE`_IU]?smv>`Dw:f;wT0~-~C.n8w`[0'7n^^\{hdW;b6g qZhKe*c!i*emirQ)>} x?~}3@"zqM}^r{5p{s\q\ZdCx ;c{=?&5>A"'%7;}wc|wf. *.'SfFo^=]>L?3'Sa'QmOvJЊB{_or`tVGK>\iLn74Vy..0ֹ3%>&ɅɁzطN˲4ĤL;էLr,N.C+ z D ;+ӫ@- 8K<қO6Һ 2э*ʩ6F<Iҟ 6s %%b+vޙ!zթ! ziimnͦ LheDx_TyAB z"u_kdsIr:t}_qfŚZ޸?ؠoXGk ͆pɁaneňgoa[FF.?0d`UşHΛlru͚,RzDz9}:ā: ~ѕ{]p\Wm猖o逄[v~КzxؑdΠn퇨YZKh^lao\\cn)SǗ5"ϝN I,/ږ.ϡH͜.|U˙\ѡ>eV]/^kWHd{u}7lj>^=qH>ūj&Xflip09@e߭O,Nɬ[۱w"''⼗{-g*Ǻrv z d҃ΐ!Շ4=ɳ>حIFVP99Uz^Q>NV9&q8Ƈ[+G/qT#qMg hC5n r ^T^%rS?WC&~,o]!t)0zgF>( h3- t-_ # n7)C~$iu(+ldEm)xr8ʉ]Ec;ɀ<^=d;NJxJωfUaPV6__IŒU]?rtv<`Gw9e?wUχ1,~G-m;w\Y2&:m^]ZhbW>b5g oYhJe-c!i)ekhrO+>} x?}}2B!zqL~}[r{6vtuXo[_dAw ;a{A>&6=E"&%7;Ävc{ue- *-'QaːFmc=[CL=6'Rg'QsO}JБf_nx`mTEK<[iKn72Vy-0 5 ?,K&;|tN5ůL1ֲ(y,R.F+ ~ H >+ѴU0 ;K<ϚS6ͻ 2ї)Ʃ9F<IӨ 9s %%b=tߡ!vײ!"xoxdrnȦ OgeHx^SCB z$u]ådisLr=t|dpmZظ?ӠoXGk }rցb̊f҉gobZFF.?0e]UHܛnrwښ,Q|D|9:; wߕt^p^Xoq~]vxPtz摀eܠoZZLh_lbo^\en)S5"~QN I,/ј.ǣHğ.~UȘ`͡BeT]1^hWKdzs7l>u=FKJ>ƫjɄXflip10@ݤ^ޤMȵ*쳞ʢWʤu',輏ڊ|/ށ)ȳr̾ avpߋwϾѬpc̃~#ʊ~Fs͎婴?AԡI锶DP R$,I kDJ)CI1&W3ʉE"2$yUP#lMUmnAR&xU9΄DD‘H;|o4\{5nn8͇[LӄMZGQɐIoQfCɊvL\wvSԆ-+tF-b:wDW1&:`cZOhbK>b6` fOhJ[,c!zi*^^brP}+>w x6'Pf'NrL|Gʐ~e|fkw`XNFK=TiLf73Vs..24ѻC("J4 ulN5> гLx,R.G+{ H >+ͱT0 :><ϓS6ѷ 2Ș.ǣ9F<IΨ'8s %ے%b)pܞ!q֯! vod[ȼrn̢ OgbGx[PFD z#uWUhsLr%ˉ/8X*$v,=E ?ȐQ)aY+lF6Љ5S+c!V{3Ɋ8b\9·`9Zg x:X{7nA$a:m{=h~{2r<6u3^6Ia ;iZ??'/BC"*%26Op}Qd2 *2,E8g6rsb=brBLC5$Se'PpLzGcddfov`AJMsKDAiTV79Vb3.0?-ͳB("V41 tk|󲞪*ϭS0 9F<ރR6 2.ח9FƱ<I¦(7g %͐%Ƥb)tҙ!qѪ'"zmQU̳}pn Ngeލ"Fw]مQ}7vE z"uVGzfsJr;tbrk׉Z?oXGm v cPL~~bZւTiuQΊyZht7i8;9Յ^\~UJS[gܡRrޚ&o_ݝxDx9|:; fXFCzUpwUXh`kMTvFBvP^ࠅhtQZoAhzWlZoU\^n)S5od"NOcN I,/.H.zUր^ލBE\9/^q/Jd}N7umF=7=,^M>G@wEOXflip12@ʤTһ)ܘ%*r|ʷ5ou3Ϫ1.t*ouŅƽ ʐL`xM^2mmLCJכ>ʍKIׄ@CT0kB52*&(+ȉ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'[4k1q0%˂)8X%vA~GCĒI%aL'l77Ё)W"gZn,͊x-bO7ЉZ1gB7wXc0.~9{_ x>H{?eA+W;b{;bv{6zr54{$^6RX ;rN??#'3~HuC"2%6;?pAd: *9,G6Z8yeb=ifB+J6$Yf'UqQzKTdTeuxvU=NVkKM6i\M7 BH!Z:}.0ڽ?-ͱA(!H41 yp~:F ?w,׳R.׳G)ր H!?*ԬO0 9F1ūck=salute01@ŒUצ.yP #%pДt׻0Ɓ֗Y휥.ثҰ ZXͣƅհ ŭy@wvpy3sܼ=K'ਗ਼A=R2h 931+#&ʼnx xD^M5mA; `[rD2zV$3(1'B"3 R 2bHB*,"=& K#Cn "5m3Z/ Ȃ)4Xv5~< 6K'bM(l41ω!K~YÎMq2͊z5bN6ωZ3aC5quQ"EsM)OՄ&T8>E{=18wO Q20h=|Dbg'm!AeD: j_,kb3+c,Ui3:KGrfW-LSw9CvB[A,Mq3Y{3Ye{s-hq&'vw)b6SL ;{HC=!'%pJbE"1{%&}*IpJd:r *;s#$9&d8ycd+p\D+Mo:$SueK|pA{9^6]Dmxv`+=V]KK/i]C7ArHP,wجO1 9?5uO6ݧ 2.ш7F<׶I~(w4g %$b)h!c!om9Dro׏ nLgXɎEwO†C~z4qE z!uI3lhsrJr9tucfl̓Z?pXGmv bO:6vԐPV։?cրgNчhXhp9~f8;~9~օx\\{UVDq)0f"N6G7on*==FQ>b&xkk=salute02@ܩKw-GҽrRϩXSiwB–в2ayݺUݶ d˱Xֽ`nӭ[v̇x=tg阾uǹvݞ6Ψv2럮ӵ/( dz)8}P {?h##7!1‰+xz&+^wMmQ) \s|~*/K nyeNu=9%'V:Mf0laZ1*#|!W2 e?x wo !P pN \Eň8$]. w&ŀ"{b!ed nF̉&+̀5’,3͋8E_+·m,eV%sssIhl?gE7Ԅ)3uM(GvX!:s4.25v6HBEE(n)FEV( jv(m//f89bB&U6q{L3("6zD-xQFK<5Z4.w V5+8Ǻ1=l yLs'J,C,NPF"<,UưNo0"^9=.~JF 2*4C5Ics2i %i!e+H"BMk"]Hp#\zk8| nJiR4pEgz,#y3qF} zr w0~}dkhlJsoz8w\xgN{qqZŌ?woL{XGl^ E2&mϐ.S|VCֈ>HՃrFЋdHii9_9x1u5k vMcg@|H+n=Mr/Tޚf%I5ޝfCf9g<~h;ri Cm0nppeDobDXzUBP-;w_T/DߠpT`?Z[2ieEnNoH\No*lS~|.H8! 9Nu i&h,o/.G/fU|W9Ń:C+4B+S7I5qS==eX>K?hjsalute03@+AȍFO`$Ԁ3=ǠĹ`` IkLETg҄Amk`VԊf}r1ceΖYnˡur7ϠϒڄU %k24 {Oh#>G Az0z4!Zukpi# Vprxn:)g ona i>M"'t:hoy.}f\u,%#u!v.x ?y#mdt"usoo cc lLYJs9Ń() v!DČ_̈4̃ %{6:/x?/:v =;}A|:G?`?- 1hTJ(1Z#7}KG2S$A{ LN7OF\G %2S"%N{ Gh5p]k'LZM !W98Vr6V|^^d4`Cl_|UU|[:Lb'4 OfGH;']D.G N-4M(#А i/^ 3ŏ “1+U g(]viL'dD'5,6FT=$4ú).6+*I? -$8?h0GRru5bY $U"oEb,/(4& {kѐ u(c4 sJb-yr.{Gt<}z} qz)r?c} kD}!s iczdkiblKg6t8s?low8kyy\=6mz]bMgnO 0rX|-BԀd;׈F;ւDҋk>ag5]9q2q5_ E^^@xH^9Aq&Gޠe$A~)yߝ^C]9]<];w^ ;^%^__l:oh:YL=D'0wP F)7XwJe7aa)il;nGo?\En*kSp.>z)o y+Nxr h&d'n%u*|Gw/^UR?>C>>B?OFM3t7NJ==PX>K?;e`=salute04@0n4לIA"ޒ9Z.Rӧ@\Erת.~n@u|{?^hBIyq񑔲X݈ic{UP}|oYtA ǰ9 {Vh) "H$M Gz4z;$dutpu$ _pxnC-q o{a#k=W&':ro.e\/%'u!.y ?z#md t'u~pz ^nlU"YRsA Ń./ v#D Čï;̃%'{#::CF*·.D]&usU*KiN/Ci^1Մ3,v1Dw=,;h.-3+uL4NE(~dGGC ,pwE9FU2n3g^`8,;- xd%zeH3M4mG2xH2!AvC"BG| ?OF_?1 1k^K(8c*<}TG8\$G{SV7VFfM %5\"+U{ Nt5xgk'LeM a8=_s<_}#hgp4kCwi|U]|"d?Ib*4WrMHD)gJ.8F)d%%ڎ>!c;W+ א ˍ(<(S s,`vdL'^E'62,EN=$a&%-4+,H? -$9?u0G[p6bc !`"v6h,5/8,"}wѐ +aF {Jb3r%#Ft?z~)~?n kE!s%uo~pii\xLg.7sBwpw=uz\ì=Ɠ6x]mMsnTk3j"{~ a|4HԀpA׈OAւJҋvEis5g9y;}.flK^h@V%hAGq,MߠqZIR/ߝgCg9gp^S?dXM=salute05@0n4לIA!ޒ9Z.RӦ@\Erש.~n@u|{?^hBHyq񑔲X݈ic{UR}{oYtA ư8 {Uh( #H%M Fz3z;$dutpu# ^ pxnC-q o{a#k=W&'~ro.e\.%'u!.y ?z-md t&u~py ^nlU"YQsA Ń.. v"D Čï;̃%'{"::BF)·.e]&usU*KiN.Di^0Չ3,v0Dw=,;i.-2+uL4ME(~dGGC ,pxE9FV2o3g^`8,<, zc%zfH3N4mG2xG2!AuC!BF| ?NF_?0 1k^K(9c)<}TG8[$F{RV7VFgM %6\"*U{ Mt5wgk'LeN a8=_s<_}#hgo4iCwi|U]|!d?Ib)4VsMHE)XhJ.8F)d%%ڎ=!c;W* א ˍ(М<(S s,]v"\L'VE'51$EG=$a& , 3+,HC -$:?u0G[h7pc !`"s6f,5.6,!~vѐ )`F sJb3r&Et<z~)~?n kB!s%unzpiiTwLg(6s>vqw;t{\ë=Ɠ6x]mMsnTk3j"z~ `|4HԀp?ׇN?ւIҋvDir5g9y;|.flJ^gUV$hAGq,MߠqZHR/ߝgCg9gU&'~:ro.e\/%'u!.y ?z#md t%u}py cm lT"YPs? Ń-- u#D Čḧ9̀$'{! ::CF*·.D\&usV*KiO/Ci]1Չ2,v1Dw=,;h,-3+uL4NF(~dGGC ,pxE9FU3n[r4g_`8,<- zb%zfH3N4mE3xF3!AvC CG|AOG_?1 1l^J(8d(<}TG8\$G{SV7WFgN %6\")U{ Nt5whk'LeM a8<_r;_|!hhp4kCvi|U]| dAIb*4WsNHD*XhJ.6E'c%#ڎK?jsalute07@y"Ot,aMK{3J~'QKSց> \Jrڈ(:ABz|₫E\^AdazofK]Q0y {Jh!: B <z,z0"Tocpb# Pi|qn6*` of\ d>H#'l:aop.t`\m-%$n!n.r }?ws#fdt ukof c\ lG YEs5&& vx"D{ X0&{7:>Fq(Ç,eQ$osO'GiI+>iO.ɉ**u-?w9)7h%*/)uF2IF'xdACCo *pl>5FF0[[`1gVX4,0+ zS$zWD3C1i:0x:0<v >=wB|<IA[?. 1aUF(-\ 8}MC.U$B{ MP7QFUH %/U "P{ Ik5h_d'G\I Y65Xk2Xu`y`f4b~CdauUV|]<I}b(4 Q_HH7(XVE..Ё: $vX/!Î:Tsleqfdi[oGg2w5s;nhw4mr{\=6d}][M`nG *uZ|*Dɀ]<ˇA<ʂ|EƋcAii5_9f2|s5V wF^`Ux{Haԛ5Cq$HҠ^#;&|ӝ`C_9`<|`T Ŀkwsalute08@a;mO@0ѐG]1P__݄Cw`Uy܉J:fJyቭpZ\cpzyoi_ug* {7h - -‚(zz!%KwGpI% Ish|n%+E o_eNq=3"'O:GY.hkaQ.%u"z!P/~ \?d~ to LpI \A ň2!Y-s#ƃvZ D\Ō?͈"$̀,Ò$x3ϋ9DV(·b+eE%usP"JiK&Bg;1Մ .u*,Fv;';h.2/uH:{EEs&p6GFO# k(md74D24n=!K2gXR6(0 z<'zFD>33i,5x,5AvEAyJ|DM8d4\2 J 3~]*4$E$/W/P24ǽn/z+K\+\v sM,oD'22MFb='ZĽGA/28x=*KF -o%7?h0GKrv6i\ $N"nEd,0+C6*w v9h7 |tKb;{r CHtHz{w3o?c kM"sroiekieoKgFx9w?rmr5rv{\=6,i|]bMenE -{`А#IԀJ>ׇ3AփbDҋSCipf9m]9^2kq5Q bG^pa@f|He.Fr!LޠS$7%ߝ`C_9z`oQ>XeO2I 5wW L"=\MO:aK-iT?nIooC\lHY*WSht.6'u(N_v U&S,Y%w.tIpz/|`UV4>Cr6:B6RFJ3q8=^=~S>+VRh`=salute09@&xNzS`H_sD\rPvH i͹Yφ^:Ͻ\[ҁцm$`gghՁnz\ "$-P/h% $*"%Éyz.J{8m<+ Jv`16 n`kN{=)&'A:7M0eqaD2*l#"?2r KC\ ~o : m8 \3j)&Y" wǀ%{J!eL n4 ͉/̀9Ò0e2΋n7EH,·S,c?'twQJhL"Ag5:Մ8t4-Hv=%;s775t%NAlDda&pz)FEA* jY(m^01f*<p1(B8qZO3+9z3/x!=_i5cs&x~{|`7u]DShyUsʐ$uLJKG"bT4Fd 6XO76R)V`2Ji=N;T׻'Ed0] F/+EӸq1y=W c=iu'M,D,ADgE"y<,`ʴG].O7v=.M7 -r*7?{0GS'z6iq $W!~dy+="9\"Q=r!T~l8 wLiUr/ZF^z)!|y3|pFw kc!w*liigsuKg\9wL~hAr\=6I|]vMwnM :+"rА%W|CEֈ1LցXFчOJhii9d`:`1fu5W \N^hi@cJ%s1Pr'WޠT-=.ޝyfC|f9qgwcX'HXUKBZG3iPGnuNofH\dNnY*WSd~.<1"1N] V&V,Y/{.oHk/sgUY9>Cl,6f}+S7rH5}qo>c{=EWP>'hdk=salute10@H-dSЪXbzX۽͹r a|çTʸ-vұX [:ͱ[p΄j춬Lmsꬺxznӵ%⨪r߱,' խ(-R'h 52'*"ĉx x>H,m561H}Y?*atO} 3!0'6 + C2c{a;=*e("3$ i>!CWy  .m,Z(ǂ"2Xw/~ 7 1>&c@&l,-ΉD~QÎ F\2͊d5b@3ΉI1a62rwA"Gs>)=s2IՄM,;F{2/9wJK3{$b>eDdU&mi%Be66 jO+kQ3-c$Oi)673kHJ0e.q y.dyк>i$VgI]v5o \ FAоW嵞{,kүH{/j8{?5P7 2}.8F<Ia(~6g %e$d+O"Jw"rRo*2o{n xMik@qFvx:1}x4{p7 zy"u7%sohszKsq:u[ePn|Z?ofXGmZ I:,*mԐ@Q։2]րTLчQThjn9dd8i;j{9b ]W\ks@iJ.6^r-hޚ[,F9ޝyoD|n9rq:fu;_x ?2"QOpOOXa_:a)Mv#xPl,Vߠ[_LKZG"*Q*=Nc ^I_,a/}.sHo.spU~\8AD\,2fm&O6kG4|on?>=~PN>sYVksalute11@UΫ/Q%%x뼍чv1~։W蔦.Ӭǵ MrQ§ʆȵ Ǡo@vn~mt=e{ص=ߚL'ܢA5 R*) zKD,4&!2Ɖ x !xSQM,m:E1R[^ Q)nV%3#='9*(E'2mbAJ)m/"3/n=+D\" -k+q)+ɂ%AX,vF~NIÒA,aD-l0<ρ#[l`a1͊i2bF<ωO6Z9Apt>0Ew<8;r;Y(e.SD0D7w_h0+:mBbW&hf&>b:H S.zP=+c(gi.HEOWb,=f v6M{:iA'^4f{4e|/|s.-n(_6IW ;ePA?$(,AvD"0/.3uEpuFd7 .6 ,>/^olgc=^gC+D8$Of'KqG{BXdXef{vU5DMiKA9iQL7<Hb7~514 ѵC,}I4 icv;F ?w,S.H)pwՀ I!A*׬V1 :Fogs{Kr;wcik˄Z?ߋdXGmu eUB:L]׉?p]Pш`[zzn8seΌӅ}|8zֆn\\~z_VBGlܡ>xޚrN]ONsDr8w:z}:s SG63aVm^VXsfnKn8Zv50};cߠmg\RZVChaWlVpvS[v[nx)vS5]V">AUMy vIx+x/5H.vUt_΃CDZ10^l$KdtC7hmF=7=M>G@wGOV)taunt01@ʤTһ)ܘ%*r|ʷ5ou3Ϫ1.t*ouŅƽ ʐL`xM^2mmLCJכ>ʍJIׄ@CT0kB52*&(+ȉ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'4k1q0%˂)8X%vAGCŐI%aL'l77Ё)W"gZn,͊x-bO7ЉZ1gB=ptF-DwC6_ x>H{?eA+Xq;b{;bv{6{s54{$^6RX ;qOA?#'3~GwD"1%6;?pAd: *8,G6Z8xec=hgC+I6$Xf'UqQ{KTdTeuxvU=NVkKM6i\M7 BH!Z:}.0ھ?-ͱA(!H41 yp~:F ?w,մR.մG)ր H ?*ԬO0 8FWioq)taunt02@ǼV'3 癝 *.˷9Òႎ4ݪ]ChŠŇƼ;ÚpU¾{b~fh|\Dt֭?ٍK(ԝA5 P)2kK?0119ȉ"%#y*UZ?mRK1X v &P: wtS!?,;&Q3:Z&/||aXO++ G12W.Ds G&mC'ZA3ʉ2CX(/v'FKFƒ\8bb;lGDЉ.Z~"eŎZŽJ͊NdbKЇqIaeBsw'Nz,FsO`ք7[|u>K{|0Cw[VI>{gbH\dEl*M6}BlwCh43c`Qbg=]ZrW4LJKzIUvsXA^G!qBh{BhEY(}$_Ekk/ϐ1Di)?q:UАV,taunt03@͌I*׺:$Ə ܻ<ܬ<̫{ۯGٌn檕ll}n̠ĉv͛sBrϺz{w“~wbv㴐9ݘ)*宇?;&z"WO(=&:DȄ)23x7]4|H)k`P1)zQl4Y&B)n8gzkC7F(]9>!Q%4>nqiR)h v$S45o {;d3DV~%spp3*tW0mR1ZP:ʈ>LX57w3K$Q#L|mCcuFnVLЉ8]~*gŎ"_ÎY͌^esUЇUawSosLHhQBs[gքB`z^EvR;z*ak`1vp|:kdSnXJ6T Pm[2e`oM[lcrz0+wX zWYxr>\aKmvKmgfvOfKvk2ϐ5`t_l9h 1xK)d/{Ew!03~q6qE} $ H/9l,J'8 xeenMKzB7DuU1HwINdd3L{O~U[z*.?-ճK(}"V)0 dWl󲞷0= Ҵ+s$I,=(yzC"7'wحO,:?;.HF 2̜*0C5G̦'ƨ2i $֏!e(qј"kШmrn=Hİwg8ϰ بGinr\ӏEkSˇGà4—F k"wS6dgFs;vbvlƣ\=6_]GnmVEH~or׉Wyրpчvh9:19 {cBJJߛ\{rNޠ#iUݝCĖ9<< dQ72됇spqXbLkw;7Nsࠓna|_iun|ou\}댑*S.iX?>YN &,/.H/UU96DY8fZX8s5Ęo=G=8`M>{?,taunt04@ȊK*ض:%ǎ ׺<ج:ū֭}Iʋn誜jilПÂtqBtӹw{ruit்7ؖ3.઄F;(z$VO+=&=DȄ+22x:]6|L)keP1+zUl7Y&E)n;gkB:E(a9B!U%4BoqoR)n v$X45u |;k3DZ~%ypp3*t\0mV1ZU:ʈBLX87w6K%Q%K|rCc{Fn]KЉ>],gǐ$^ĎYɌ^e|T·VaoVosPHhTBsefք6cz}aEvU^iCpvHo_gvGgAyk9Ґ2Xucm9i 1{K)]"Ep!#'~sduE % ?/Č7l,J'8 pe[nM?z57DuU5AyIOde3C|OwW[{*5F-ֲK("V=0 m[p0= ӳ+s$J,=(qC"7'zڭS,9?†.H7 2Κ*0C.GΦ'ê3i $ُ!e(kә"hѩyehnEFųph8Ͳ ֪GifrQ֎DyHφ;~¢4F k"wL7dgFs;vbmlĥ\=6W]Gn y `MQ~{pֈaxׁnΊth98<9 y^_JTߛfzrWޘ*v`ݝϒCԓ9Ė<< p[A;뎓qopXmUiwE?WrࠡlZ^isnzos\{댠*S.vdJIeN &,/ѭ.H/ŒUU96D\8D^Y8w5o==fL> \ޓV,taunt05@ŇK*ܵ8%ȍ ֺ<ث:nͪ|Gn뫜jltpӞ‚ô{qfzӹy|rwiuެ7֒?ߜ3ڦD;+z&VN-=&BCɄ-22x>]:}P)klO1.zZl:XI)n?hkB=E(g9F!Z%4FoqvQ)u v$]35| |;q3D`~%pp2*tb0m[0Z\9ʈFKX<6w:I'P'J|yCcEneJЉE\0fɐ%\ĎXČ^eS͇WacYpsRJhVCsqdֆ'fzucFvW=z.`Zd1v`:meZn^M7W Um_3ecoP[cfr~1+lZ z=`xu>`i8r}CpXhv>h2{kC~/Pven9k 1}L)WEi!~vdxE ${ 51Ďɍ7m,K'9 deOnM1z&EDuvU<6zIPdf3:}OsX[|*5C-ֲK(V=0 x`v5Ѿ=1Ҳ+t$J,>(eC"7'ܭS,z:IJ>ć.I7 2И*1C.GΦ'4i $ۏ!e(cԛ"eҫt_]nODĶii8Ͳ ӫIi`rC؏Ey9ч,~?F k#wB:egGsUFJi,taunt06@K.ᴜ8%ǎ ջ<٬:n̦~Hl묜lokuԟ‚ɶtf~Ӹ}|r|hw"ੌ6Ӓ?ޜ3٢D:,z'UN.<&DCɄ.22x?\;}R(koO1/{]l;XK)nAhkB?E(k8H!]$4HoqyQ)x w$`35 |;t3Db%qp2*te0m]0Z_9ʈGKX>6w`i5r}BpViv(aC"8'ݭS,w:Dz>Ĉ.J7 2И*1C5GΦ'4i $}ܐ!e(aԜ"eѬr]YoSEĸgk8β ҬIi_r>ُEy4ч'?F k#w?;fgHszL,taunt07@>/뱨:%Gս ;ش;۝Ð훊K}l筝ĸvv{Τăβžևe}Ѹ}~rŋrh͘8Ξ3ۦ.7;)z$VO+=&>DɄ+22x;]7~M)kfP1,|Vl7YF)n<imB;E(b9C!V%4CqqpR)p x$Y45v };l3D[%zrp3*t\0mW1ZU:ˈCLX87w6K&Q%LǒtCb|Fn[Kщ;]~,fŎ%_ĎY΋^d{UЇUazTqsNHhSBs`gׄEb~|`FvT;z,aba0vg~9kdPmZIET Pm\2e`oM[sbr{1+tW z^Xxs>]iPmvQmbfvJfLxo5ϐ9Zt^i9g 1yJ)`5{Er!58q6qE} $ M2ˋ9l,J(8 {hirMQ|G͆7ĄDxU4ɐNwINdc3Fz~O{U[z*04-ֵ>+"J)2 i\|0Ͼ= Ҷ+t$I,=(݂C"7'٭V,9Ȱ?.FF ˝*߼0C5G˧'§3i $Ր!e(yЙ"sϩ|htrAMñre6Ѱ զGiirdђFzZʊMР4ЗF k"wY:egGs;vfypȣZ=6Z]Gm t[JM~vq׉\y׀p҇uh9:1< zcBHOa|rQޠ/r[ݝ˒Cϓ9<< kU:5됎rpqXgNkv>9QsࠜnZ_munzot\|댜*S.r`DD`N &,/ͮ.H/US77DZ8f[V8p5Ҙo=C=7M>֌,taunt08@>%䲶;$G۸R ڽ 헝ŇvÐKyQ䆄MeȫPȀȰ̘Exιws|sڄgm덏<Э3ܳ/9;%z!W O'=&8DɄ'23x5^2}E)k\P1'{Nl2Y>)n6hukB5F(Y9Yihdv^ghavRbesx*ƎE`oVg7c 1tJ+fZr|Hv~!Y~Zkf6y $|! e7:n,K(9 nxqgс9zD}U/fsIId_3OwzOQ[v*25Ժ>+|A&.2wZY湜{N2.м*w$J+?(cӉE"9'ծJ-:3.EF 2Ƨ*ִ2C5GǬ'4i $͖ e(ɚ}˨&~oz5Wvd6ͬ ͞HiprǚIzzmԚ5㌎7 h#wq?igJsh߳,taunt09@>%洯;$GؼQ1۹ 朔yÐI}Q׍|m̧Pƃʳɜ섇EyϷ}x|sԈmm:Ҧ3ޭ/7;&z"WO(=&9DȄ(23x6]3|G)k_P1(ztPl3Y&A)n7gxkB6F([9=!P%4=oqgR)f v$R45m |;c3DT~%qpp3*tT/mP0ZM;˂=LX48v1M#R#MƒkBbrGɊQMщ3^~)fŽ#`[Շ^dlW҉TbNrsIHhNAsPiY][FvO;z*al^1vqz:ioImUGEQ LmX1e]6J[y_sx0+wU zsRxo>Ya`gvYihbvPc^sr,Ɏ?apXh9d 1tI+eNuFw!NOmoi6z $} ]4:l,I(8 i|tf~]ς7}DyU/^tIKda3Mx{O~R[w*24շ>+~"J.2~ ]XyN2)ѹ+u$I,=(lքC"7'׬V,:3繅.EF 2Ǣ*޸0C.Gȩ'£2i $ϒ!e(˙"y̧mu7Rwd6έ ֡FinryʖFzoÍbҜ5ۏ7 k"whʎ=Аbu؄K|Հsֆwx׈ ցԄل}֗ϔԘ=ߛNr?ޠ|w]IܝC9<< WD+&xupttXTȝ,taunt10@>/㶧:%–Q1ܳ;ء{衉Hفl鬝twxУăȵ׌}Dxж{~rŎum͜~署9՞3.7;'z"VO)=&:CȄ)22x7]4|H)kaP1)zQl4XB)n8gziB7E(]9>!R%4>nqiR)h u$T45p {LX57x3L$Q#L|mBbuFNJULЉ7]~*fĎ#_ÎYϋ]drUЇTb}QosKGhP?sXgքJ_|]EvQ;z*ah_1vp{:idOnWHES OmZ0e_6L[qary/+rV z`VxqK[aRkvPlcdvJeRum0͐8\r\i9f 1vI)_9yDs!:<oooE| $ O0‹9k,I'8 ~fnoMUzKDž7DvU0ƎPvIMdb3Fy}OxT[y*04״>+"J32 bWv0ľ) ѵ+s$H,<(vۀB"6'ګV,93ؼ.EF 2ʝ*߻.C5Gʦ'æ1i $ӏ!eIyΘ"qΧ|gxp;L®se8ʯ إFihrfϐDk]ȈPʞ5̓F k"wZ8dgEs;vc|n\=ܭ6Z]GniTDFҐlr׉Tzրp҇vh<: ; zcDKGߛX|rIޠ׆gSܝC9<; aM4/됃spqX^Glw73Itࠏ|nZw`iun{ot\|댏*S.gW<<WN &,/.H.UR74BX6fXV8o5Ζo=r=#N>r V,taunt11@>%赯;$GھQ ܹ 枔}ÐK怛N»֐|MrΧPƃ̵ʟ뇇E~ѷ΁}|rԌrm:Ӧ3߭.7 !A$ y'\R%?GAJʄ,87=c,I.kdU1!~tH l5\&A-n,ll lB6HI_><%K'43rskV)] w,Q75d }x8S)V*S|nFbwLȊWRщ:b~/kÎ)e`ԋbdr\҉YbTrsOHhTBsWn]baFvU;z-epc1vu~:noOm[HEV Qm^1eboP[}ds|0+}Z zvWxt>_icmv]nngvVhbxr3ɎEfu^h9i 1yI+lQzF|!QSqoo6~ $ `:Î:l,J'8 k~tMi~`ц7тDyU5ŽayIPde3S|OX[{*24׷>+"J.2 c]~N2)ҹ+u$I,=(r؄C"7'٬V,9ñ3缅.EF 2ɢ*ߺ0C5Gʩ'ĥ3i $ђ!e(͙"|Ψsu=Wí|d6Ѱ פGitr|̔Gzrƍeԟ5ܓ7 k"wlBfgGs;vhsɣZ=6e]Gm~fSEʎEѐhx؉RՀwՆ||j քԄ؄WϔՖEߛTrGޚycPݝC9<; ]K2.}ypzwXZDrw51GzࠉwtZrfi~{noz\댊*S.cT::TN &,/.H.UR76B\7f[U7m5ޔo8=?=:M>V,taunt12@>%涶$GݼR ܾ Ň}ÐKQ卄Mo̫PȀ˵ϞEѹ{|s܋qm씏:ӭ.޳/9$ F!$y,aV BGEP̆/>ƒ=Bh!"H3kdZ0t< 6`=1nqY oB3KI_E9)C)4&wshY=Oz'L:3V<^:DB*Uup;1{Zbiqovgqtlv`ln{x6ƎOmx`h7m 1|K+tc{|H!bctp6 $! oC:Šn,L(9 oyypԈ8ڂD}U:p{ITdi3\O[[~*25׺>+AG.2~cb繜N2.Ӽ*w$K+A(m։E"9'׮J-93&.FF 2ɧ*ٹ3C5Gˬ'5i $З e(̛Ω{{A`Ĭd6ӱ ϤIi|rʛIzĒuנ5䒎7 h#wyJ”igJsHXGtaunt13@ÙL%⵪;%Q1ص;㤎}ÐI酒孝jӚuxw˥ƁȱȞtDzҸυt|rԎwiܟ9բ3ݩ/4 $D##y)^U$BGBLʄ-;:?e) H0keV1tE l7^&?/n)lf mB6JI_A;'I(4/srkV=Z x,P85` ~aiaov\pqivZk`yq5ʎEiw`i9l 1{J)oN|F!NPsoqE $ ^:Ï9m,J(8 i|sf}]ψ7̈́DyU7Î_zIRdg3V}OY[}*04#նKL"J)2 e^yN0)Ӹ,u$I,>(uڃC"7'׭V,9³3㽆.FF 2ˡ*0C5G˨'ǧ3i $Ӓ!e(ϙ"{Ϩvt?Wĭe6ӱ ٦GiwrxΓFzoȌbҡ5ؖ7 k"wiCfgGs;vgqˤZ=6i]GmhUGˎHѐly؉UՀwՇ}j քӄ؄Yϔ՘HXrJޠzfSݝC9<; `N51됀zp|xW]Gsw84J{ࠌzuZugi|no{\댌*S.fW=Q=WN &,/.H.UR76B^8f^V8p5ڗo=F=NM>АV,taunt14@͌I*׺:$Ə ܻ<ܬ<̫{ۯGٌn檕ll}n̠ĉv͛sBrϺz{w“~wbv㴐9ݘ)*宇?;&z"WO(=&:DȄ)23x7]4|H)k`P1)zQl4Y&B)n8gzkC7F(]9>!Q%4>nqiR)h v$S45o {;d3DV~%spp3*tW0mR1ZP:ʈ>LX57w3K$Q#L|mCcuFnVLЉ8]~*gŎ"_ÎY͌^esUЇUawSosLHhQBs[gքB`z^EvR;z*ak`1vp|:kdSnXJ6T Pm[2e`oM[lcrz0+wX zWYxr>\aKmvKmgfvOfKvk2ϐ5`t_l9h 1xK)d/{Ew!03~q6qE} $ H/9l,J'8 xeenMKzB7DuU1HwINdd3L{O~U[z*.?-ճK(}"V)0 dWl󲞷0= Ҵ+s$I,=(yzC"7'wحO,:?;.HF 2̜*0C5G̦'ƨ2i $֏!e(qј"kШmrn=Hİwg8ϰ بGinr\ӏEkSˇGà4—F k"wS6dgFs;vbvlƣ\=6_]GnmVEH~or׉Wyրpчvh9:19 {cBJJߛ\{rNޠ#iUݝCĖ9<< dQ72됇spqXbLkw;7Nsࠓna|_iun|ou\}댑*S.iX?>YN &,/.H/UU96DY8fZX8s5ĘoӚ==9K>v80lࣾtaunt15@ˈH,5 횞 00Ǹ:yʧNڊ8㩛s\zs˟ĉxĉw@tȻ{w}thĉm6zض3L+׬>%P2,zPG+71=lj"+*x+XJ}A%mTN1D zg (T;%n]kQtC/B&R6;V&.brg[Q+|&}$K40 Z2Dg#yn&J+mF,ZD6ɂ4HX*3w(HMH|_>beBƊIHЉ.[~ eĎ\ŽS̋WdePχtPaoJqs:Lh?EsOdք:[xNJvCCz ^dU>}tpHddOnEN6O{JmK2fi\ etI [^^rl3LSTzJWx~iJmU!ZAkvAkOa(x*fHng+ΐ-Kp*Sr?~cn։Mwր~iЇ|sh9: 9 xc@HCߛUxrGޠ}'_MݝC9<< [I0,~ooznXZ}Few41HnࠉxiZt[i~qnzor\z댅*S.`O87PN &,%.H/UY99DF7fI^8o0o=vL=H> #v,taunt16@ʾV'Ĝ.痝 //}ƍ˷<ʑP意5ۨ;WAişćž śnUva~hh}^Fpٯ=ۍK(ٟA9R#2 kE:1.-2lj y&O\;mLF1\qu "K6{tS#3*7&L-7W$2|aRK+,"C- R+Dr   A m= Z:-ɂ.=X$*v"A~FBĒU2bZ5lA>ω*VbĎVC̊Gb[EωhCa]>nw(Fs.>sJ\Մ2XxiAFvo3kg-ΐ.6p&;k:wYJĮߔo ܔKir[ҊEzT˂Hz47 z"wP8dgƖHqWdrW,taunt17@ɯTǜ)u%%|캎|ҵ0|솗)ѧƩ1?vAouÅǨ ʝ\`x`r2n}`CY|ب>ӉH&ڒ@HT4k:-3(((Ɖ y GcM6 mC?1gZsC0V6.%.&D$80 N$ bHE+|8"8&%E%Cm'!*[7m4q1#ɂ)4X!v:~?;ÒK&aO(l85ω&RaRr0ˊ|3bQ8Ή\3aL3lubAs_!9rDT-WyN8B{P*6wQ3M4|Ge=~Cbl'm=dP>ja+zp+&^6YnB<OJruQ(L&Tx=GvL]J7L9_{9_%d{ r7kh//'v #b8]S<>>?''+y`^?"<}%.4{GpHdCy*Hr!$B/b8bb)V?+al7$_v_UhKvD\6[{eszqU5G_fKY3bhJ4IyH)PEt/0?-ѮA(O41 oet󲞧<4 ?u%߻R/߻F+y{ I!>,}թO2"sdg€Iq~UZ։FkkOϊr\hq9g8х: a^TSFߕRduGoݚxS_OܝC9<; ZK63sWppVWmWiDPv85qFZߠ~lnRZiAitYn`oY\b*|S.`T"==SN {&{,/.H/U`:>D *^MoF9nnF=7=E>I@wDOV)wave01@ʤTһ)ܓ%*r|ʴ5ou3ϧ1.t*ouÅƽ ʐL`xM^2mmLCJכ>ʉKIׄ@CT0kB52*&(+Ɖ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'[4k1q0%ɂ)8X%vA~GC’I%aL'l77ρ)W"gZn,ˊx-bO7ΉZ1ZF7ktP>wM'6wEV/_z>B??23wX&Y/9q9{_ x>I{Ac J,U;b{;br{8uk54 \6RX ;{?:?#'1Sf=!4/5;|?pAd: *=|,G6Z8~_^=tXj=}hL> {u8Twave02@ߘV%}천Wˠq!#uݺӴw2ƀ׸Yǫ%ѫ۶ 톢q8ΨqɆ²즠Az羚Ƶ޶)ᚢvݣ+IϠK:R21zTJ58&'?Ɓ ,}+x%]RM3#k=P R q^ 'X2%h|V!3.C(<62K*2hiES)l- ;6 mE2D^#{"'2*k1*q.8ʁ.IX :v!Q~XTD7aD8l5Hρ,f&vh_8NJdt x3c{*wAm3x6u-r82\<[d8m ;Ta=?',5B!"//3iSpgWb' *',>6}co]w_=Ox?L64,Gc'FoDy?ycdqh~f]t`>D<|K4NiAa7,Jo'.13 ԳA%yI|7 met:F ?w*S.H)so~'J$A*تO3 =F<~T6 23:F<Iw(8g %uʏ%b)fϙ$eͩ&"lkMQͳzrn {OgYދ"HwSكI||7|xE z%uNE~{fsKr=tp`dgPZƬ?’o}YGmv i [ICPaՂGt\T͊h_hvp6pgo~ϋz{8{ԅtc\_|VIޕQhrIsܚoS]QN~D~9:y;r UK=:gZpfZXumRlFVv>P;uH_ޠomdVZ`Fig[lao{\\ycns*rS{0[T"DQDTNu qIq,s/.I.U{aBDGD-^X8HpfU6|vm*>=EDP>Yt`wave03@HԴYa}|Ĺg }UȲ\Ӻ\:ӵaτňIhzj:ǂ%ڸq &<R<8z&Z&O>;&/H)6}$5x.`SM;,kDS P^ 4Z&=,cwS}4;F(C<=&R)0bgLT)n$$G82m N5E`#xn$/.%84k83q5Aʁ8MX)Bt*V~([#YJ>gI>jr x6o{&xDps7~;x 0yD:QP[d3l ;FtBC&2+E *47fZic`b$ *!(C?{cdUa>CDL,5'Dh'EsG|Cugdmof\w`IH6zK/Ri:b7(Hn$. /̺>#z۷&|"9ͅrlyضŌF Cw*P.E)ynҁ'G$=+ήG/ w:F9}Od 257E<Iw(7s %u‘%b=hĜ$i&~ wgpWWvznn ~Lsq[ԏ!zGyvUЇLʀ~{Ewze z{#uQNsgrJrwm=GQ>ëjV)wave04@H񸞸ζUcŎa}Jƹcf;bԈ(mhjにՋyZ '9"Q$F9%x+a,VMB?I1P,?}(>v3f!O=3kHW KZ :^&A1j\tSy FAJ+ECÊ?*S)v8k 18"gVaGVzA^Ba*_ LVz1i4aul!^Hk70>s x8x{%zDtz:>} 2{J? NZ\b3n ;D~DC&5(F *7:h`idfb$ * (GF~edUb>AFL)5'Ek'HuGK}Gwlentf^y`PM6{K0Vi;e7(Vo$52=պ'9ͅysҷČDхCv*N3C=rЂ'E$;)̲G- t8D9Md 256E<I|(6s %z%b=nž$o(z sir_^r}kn Ksn`Ғ!wGuvZЊ"Q˂~Ey}e zv"tVUrgr|Jus;ff_pZ?oiXGh xlYS[_ԂUoׄdTlu[g|idwco͋r6҇^^w_VWݙ_bڟWkP{Vk_NtFs9v:y;yz `WMޙJrWnsWY|f]eSTvNwPJmV[XvfqTZlGiqXl]oX\^n}*}O0ea"SQRaM||({,{/.I/uU}Yo=e8c9\G\KbceE{}m>=Q@Q>jnVVwave05@LJcP\̑>^fla~˾z/rw{jܵu]#, %v|M*[L tb|b{/G'Ny8t *%|:*()Kj:oSY |E,f=2^'C9:<6 wrAp3O%,5Q~҄)G=v)Q 18"_mZEozElBb-z Epw3{2am#x?;.> x2{'Ds36 .w=7Pl^d7 ;ICC&0ǖ-E *3э7߃cyiab& *#(C˟=xzdWb?FDL.5(Eh(FsGI|NFsbmc]x`HJؠ:K4oi?7+H&5 4* ӋӉwt"ȌFԅ´Fw*P.E)zt'G$=)ݽK0 {:D9Od 2š58E<I|Ш+7s %{%b=o%o, |lqX[zmn Lsva!}G|vZ"PEye z~#tTޠPtgrJrz;he`nZ?orXGh{ qdPFLz҂FVljbossdlxpϊxo~ԇnn\~_ULܝNڡHۚvTe&YN|E}8:tn ULCޝ>_lm`kXmzM~>utAP:D|ݠf|]gqV\h^oj~entc[qnlx.ySz<__ KNL^Ou yL{+w05K0_[o?e;r7\JiMba6zm>=L>t[A %&wave06@nF*+Ao` ԭ MEY͓BD<ĭ` UwȋX^ޖ⋝ɚQؗ+卤v=$Sc 5/Q; P|QT} lK#{}|-%! 4v(!,L]y'"gSJ tE'x>2lW.G:!qrFx4C)z*/_<< 6Xb31/>zjzh(hy&hr)w+W">gZ?iy1~Ā/1$^Zkk^\DqĂSr[IptLhGvKn>uC<BEAx:v,N.1B9snabmzqRBbVz jYqw[f-Z?hJxRg+C2 xGLB8rHGP-GoGIŰ2x]cob ;wwBF6'DɐYB F/IӈQ~vi~bM *J+[ϚNнwpc?tB=[5,fc(cmGeyNabc}u`WdۛeK]oil7!RH5M0݇81!7.׈  L87م4w*R.G)'I$?)D2 ;E:Ro 259E<Iͩ*Ȣ6s %%b)%*!mMhx ˒on əMs!Gv{o}6d z#r"e"erJr;ahZ?dмXGȽhu]NR΃M~_qycsr~p{ymӇn؀pm[z^BZܝQژMڙ֖}pOlen㌋y쌁{c[QݝN`sk_qWtzlR~309A=LV)wave07@GEÄ5]Ԇ3A[T &.Sل"D"΄TUpQrQɔnǕ|ʓ|"Α/~ ǂO=͋a8u<,ɌUxQ \}#Gs~K& |1%>tGfx>;6r##+Ld!!cSB rd$>1o_)L9nuCz4;)v++e<5 1[b-3|.6wyp)mx'mr*{+W&"ly103(YYkf[aCsSq[Rzmt[TDvZY;uACySxBQe8v-NB1U:oiagek};Aidu ZlwiV)\PiYsTvv(>D wL[@ILJQ?1OhFKɬEOdop ;bAFI'INjh?$V/NуW{}piwi\ *Z,^ϖPָspb3{A=l5,o_(hhGivNfyp{cq`UgܗsKlhix|74aVG\/ ۊ m9%e?ي  ӼKҌ7܆5v*S.H)|'I$A*f2 :6:So 258E<Į*͚5q %%ջb)%%!hMdw ȋpn ̎Mg!Ew~ry6dо z!t"dcrIr:]dPZ?oԸXGͺh|nZKN}I̐ZsyXw֡}}psxmՈnـbn[zv^`X۞K٘IڙtyQoOdbfn~twsaZRܞPUxzTvWh{jN8IQ??ܚdQvܡFqwV|ypYkd_ӟfmy9V:v~,_NexH >3h9=pjF>@ΌViwave08@QDŐ5cȋ)JfQ ;;:Ӊ._&†STntxYpƓč%t6 "MEl795oHyQR}!mK&{~ |/%&6wG _x>};+t/*L^/gSQ vE#|>3hY/I<+ qrDx4G)z,,bC wK{[@IKIP?0NfEJǫEIdoo ;^?FI'GËi>$V/M΃Uz~lisb\ *[,]͔Nշspa3w?=m5,n^(hgGguNdwp{c~q`SeږsKifixz74aHF\/مn b9%̺f>׌  Hы7ۆ5u*S.H){'J$A*`3 ;6:To 258E<Iϧ%Ζ5q %%ֹb)%%!gMcuɈqn ΊMg!Ew|qx6dм z"r"bcrIr:\cPZ?dӵXGͷh{mYKO}I͐\qyZw֡~~nuymԈnـdo[|y^_W۞K٘IڙԖxNmMhbinwyuaZPݞNXwzVuWl{jO9HQ??ݚfSurIowY{ytZkg_ӟimj4V:u{,]NcwH >359A9o`݆aAetC,\9Onq8m;=>=ZF>ǚ̆V& wave09@i_̰)x:+Vz <ư<J̇CT2dzQ„vdeŏ%v<n-Y8ψFџ3IE2|Rv>x M#^I$ue|i}+#5*|G#Ny?s;/yA&( OFnVgF#oL5[J7B0CwsEp3X+,.X2U 7OeG+%^l] J+Uy)Vr+e)xWo{">ZZC[y3pā--"^PjkRaFfĂTb[OlktZHAwYO9uB=xPmAO\6v&=0P:saafVk2=b\p [\zhP&\HmPoRts&>: vH{VJDGFP8(IgAD<Dboh ;\>?A'Be>!P/GƃOzfilbU *U,XƓHϷrn`3v>=g4,k^&dgGbuN^tp|zc|p`N`ՔmKcbirw7,[H=U2;o2e3$BL8 GЋ7؆ŵ4u*S.H)z'J$A*ϲU2 ;E:To 258F<IѦ,Д5q %%չb)$%!gM_oˈqn ЉMg}!Ewvkx6dκ z"umݖ"\crIr;t\cPZ?dѳXG̶hلzkVIPσI~_mj_trnxynԁ̊؁ln[^ӚTܝMژHۙՖuhMpeqጇ|~y`WKޝI^rk\qWrzlP~:Eޙ)59KYyV_wave10@ЊUӤ(ә5'ΝN ۽.Ю0^;[RP9Ϭ! kˆš x3_{-~7 zO&7:zFԬ>Ş=fB/RM$"znb+G&&TB} C}'tL M+2kB]1JZ#n&(5ewV4%U(D[)#} 5'0:k.:q.JÁ*\XN{!folEEaHFj6WɁ,y(gBlrEaLSɂXOZLWktW9?wUB7uEp8yJ]?JN4v#|4u0H9xTifBk*:bS` ]IzeF$\=~mF^Qioi%>.z vEl{N{J:pCC-Ch<=16_o_s ;TH@wGOV)wave11@ʤTһ)ܓ%*r|ʴ5ou3ϧ1.t*ouÅƽ ʐL`xM^2mmLCJכ>ʉKIׄ@CT0kB52*&(+Ɖ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'[4k1q0%ɂ)8X%vA~GC’I%aL'l77ρ)W"gZn,ˊx-bO7ΉZ1ZF7ktP=wM'6wEV/_z>C??23wX&Y/9r9{_ x>I{Ac J,U;b{;br{8uk54 \oRX ;|?:?#'1Sf=!4/5;|?pAd: *=|,G6Z8~_^=tXI@wDOV)point01@ʤTһ)ܓ%*r|ʴ5ou3ϧ1.t*ouÅƽ ʐL`xM^2mmLCJכ>ʉKIׄ@CT0kB52*&(+Ɖ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'[4k1q0%ɂ)8X%vA~GC’I%aL'l77ρ)W"gZn,ˊx-bO7ΉZ1ZF7ktP>wM'6wEV/_z>B??23wX&Y/9q9{_ x>I{Ac J,U;b{;br{8uk54 \6RX ;{?:?#'1Sf=!4/5;|?pAd: *=|,G6Z8~_^=tX=fH>xPifpoint02@̻U˭*𰞣ӞP$%{踔֋u2ܖT蘬/ȬŹ \XUorŷ ɜ~>xpvzÊ߄y.sֵ+֚I&ܦL6 R '. kKA.2&-5lj" y)UNM4mCI1Oq^ %R0dxS'3)<&B.0H'2faGN+i0"90 mD,D[#|!7 k4 q5.ɂ-BX%/v&H~OKŒG0aK0l;>ω0])pag3nl8aO>͈R?Z6Smt:;Aw8D9wHYՄ&z~,^B-M6w#`r0(9mKbQ;~h^0:b1U P>zFG&\ss&SG[Lm&>p x3b{.uUg4w{8r{-n;4e8\6+ЪV1 ;F<‰S6 238F<IyŤ(6g %t؎%b+fٙ$f֩'"lhOQҳrn NgV"FxPGzFE z#uM›FesJr=WH>r~VSpoint03@֪H鯞޾$NѪYZ$$z㸛׷[ {ʐʰV߽ ƵqTos Ʋj,xqʂĂ҈ۨz 鷭Xֶ,&γ'1P/0z&QH/84;ȉ+''y1ZF: mGN1D }U -W6#ToSp!}?/C&F36G)0WwaJS+_.!=42c G0DT##m n!">&m:'q<3ʂ4H.4v/L'T'OƒJ7aN6lCBω9_2s(ee9ngCcRBˈQJZ1hqv1SEw/Yz x0w{(@ur4{9|{)rD8WO^63t ;Oq>?'(2B"%*-ǁdfb^oe# *$'<=zqEVa=J?L24'Ad'>o&:Іz ieM:? ư?zw,R.F+ta~'H =+ϯJ0 s;C}9S6 2Ɣ38D<IpΥ(8s %g%b+^!aݭ$x"r^lTQֹqrn OglNxGvxI?}xDpD zx#uJ͡JpgsKrv=tabXiPZ?oh]Gmy m`QNZ]ӈRnׁdSln[gxuEtk6:{7}҅w^\}UVSߕYgrRrݚvIf[NxDx9{:;y `VJHnVplVX{h\kOUvJGvQ^ߠvhkRZfBhoXl[o}V\}^n{)xS5e_"OQP^N} xIy,{/.H.zU`AeDP.^THJd^m7pmr6>y=2F>jٍ)point04@HN$NгYƺ\ ~ܸѺ]|̒O ŻZܶT7h l#x[Ԃyى۽hݹY$GM!0P22z,RJ1:9<ɉ0)%)y6[C~>"mKO1A {rR 2X:$nMkQkv?3D&I5:H).QsgMS+\+!B50` K1DR!#g {n%$C(m?)qB4ʈ8I35v5L,U,OȐN9aQ8lHBψ>^7r-dg;ngG\VBʊRN[0sst/]Gw,b>wSXՆ%#~G#k;w0e2:e[cFZhKR>b'd JXs7b,cibFm;*>} x0}&Byq5{< {)uJ<SY_d0w ;Jz?>&(/D"%*-΃aoc[xc! *"'=CyuERb=EBL/5'=f' ư>vx,R.F+v^'H =+ͳAy0 m;Cy9S6 2{˓39D<IoЧ,9s %d%b=^#bݰ$q"lZnYTս"ksn PheLrHoxG>uClf{ zr$uJϥNkiszLro>t]dTlZ?ob~XG{k{ qeVS^[jWlׁfSnqXqyvEukd:}7;z\]zUVXߕ]dۡWoݚyIi`NuDt9x:};| d[OMrTppTW~e`gTRvOMsV\ߠyfnPZk?hrUlXoS\[n}){S5ic"TUcN {I|,~/.H.wUaAe?Y0^OSKd[v7mm9>I =x\E>Φp\Vpoint05@ݴHM$޵O̳Y\ ظ̻]~߽̒MZ׶V7h Ȼl#x[Ԃkډܹh عY$GMݾ!2P15z.P"I49<;ʉ2)')y9YE~A!mMM1B zT 4V<$OkQlu?5C&L4<J(.RrgOQ+]*~ D40a M0BS!#h zn%&F'mB)qD3ʈ:H64v8J/S/MȒP9aS7mJ?ψC[:p0bi:phG\X?ʊSO[/vuv._Hw+eAwVUՆ%"H#m | x/}%Bys6|=~ {(vM>R\`d/v ;I|A>&(.E"%),υ`rcZ{c! *"'>FyueQc=DCL.6';į>tz,S.G+y ]'H >+ʳAx0 k<Cw9T6 2z̓39E<IoΧ,:s~ %c%b=^#cڱ$p iYp]V"htn QhdKpInxF=sBk_z zp%uKͧQhlsxMrn>t\eSnZ?d`|XGyk~ thZWzaWjZgׁhQntUqztEwhd8~}EЋ}X]vUV[ߕ``ۡZkݚ|LmcQqDp9t:y:{ g^SPtPpsPWbddWOvS{PPoYXޠ|bqMZn[2^NVMdZy7kmvl:>(=C>Trl0point06@ڴHM$۵OɳY\ Ըɻ]̒۽M ZԷV7h ȸl#x[Ղkډܶh չY$߳Gھ!3P 16z0O#H58=:ʉ4()(y:XF}B!mNL1C zrU 6U=#PiQlu?7B&M3=K'.SrgQP+^)~!E30b N0DT #i yo $'G'mD(qF2ʈψEYɊSO[.wvv-`Iw+fAwXSՆ$!H"n | x/}%_yq6|>~ {(wO?Q^a6/v ;H}B?&'.E"%),φ_t\Y}^! *!'?GyuePd=DCL.6';ï>s{*S.G+{ \'}I >+dzAv0 i<Cv\2^MWNdY{7ilw:>'=qB>rtpoint07@شH޶M$ٵNdzY\ ҹǻ]̒ٽM ZҷV7h ȶl#x[Ղyۉܴh ҹY$ݳM׾!3P!06z0O$G58>9ʉ4()(y;WF}C!mOK1D zrU 6T>#PiQmt?7A&M2>L'.SqgQO+^)}!F30b N/DU #i yn!$(H'mD(qG1ʈ | x.}%Uyq5|>} {'xP? Q^c6/v ;H}C?&&-E"%(+Ї]u^X~^! *!'>HxveOe=CDL-6';i&9t8׀5ݔh___O{`SD1K,Yi5l7%Vw!014%~L'ҋфpkM7>;¯>r{*T.H){ ['|I ?+ƳAv0 h<~Au’\3^MXOdX|7hkĊ:>=x B>:npֈ֣point08@״HݶN$صNƳY\ ѹƻ]͒ؽM ZѷV7h ȶl#x[Ղjۉܳh ҹY$ܳGߴM׾!3P 06z0N#G57=9ʉ4')(y:VF}B mOK1D yU 6T=#PhQmt?7A&M2=L'0SqgQO+^)}!F22b N/DU #i yn!$(G&mD'qF1ʈ | x,}%Uyq5|=} {&xP?P^c6/v ;H|C?&%.E"%&*Ї\u^W~^! *!'>HxveOe=CDL.7':i&9t7׀4ݖh`^_N{`SC1K,Yi6l7%Vw!01?%}߷='ҋс~pkM6>;>q|,U.I+{ Z'|J ?+ųAv1 h<~At’\3^MWOdW|7gkF8> =܋B>«j2)point09@شH߶N$ٵOdzY\ ӸȻ]}̒ڽM ZҶV7h ȷl#x[Ղkډܴh ӹY$޳GMؾ!2P05z.O"G47;9ʉ2('(y9WE}A!mMK1C yrT 4T<#OiQlu?5A&K2<J'.SqgOO+^)}!D3/a M/DT #i yo$&F'mB(qD2ʈ:F52v8G.Q/KȐP8ZS6mJ=ψCXՄ:m/_Ŏh:pfH\X=ɊQO[,wvv-^Iw+dAwVRԉ!"~H#l | x+}%Uyq3|<} {%vN=Q]c6/v ;I{B?&$.E"%%)φ\t^W~^! *"'<FwueOe=DDL.6':h&8s63ݔg_^_Mz`QB2K,Yi6l7%Vx!014%|='ҋф}nhM6>;~¯>q{,U.I+z Y'}J A+ƳAw1 i<}As’5W6 2w̓+;F<Imͨ,;s~ %_%b=[#a׳$p iVp^U"hwn ShdGnJmxC:p@fUz zo%uI̪QemsvOrl?tYfPoZ?oa}XGyk t iZXvaTjZcׁhOnuRqyrEvfd8~{Eϋ~U]rUJ\ޙa]r[fܚ|=n cQmDl9p:t;w h_SQuMpsMX^d`XLvSwQkZUޠ|^rIZn9hvOlQoM\Tn)S5ng"XQYfN I,/.H.oUeEe>Z2^NVOdV{7fhQb/>=#HF>gt%Cpoint10@ԫHM۾$NΫYZ$$|޸ص[ }ːȱV߻ õqTosǯjᄉ,xqʂł҈ۦz 鵮Xӷ$&̴$4P-4z)NF3778ɉ.%#%y4WI=mIK1H }X 0U9!lWoSs!~?2A&H19J(0ZwaLP+b-!A32e J.DW"#p n &A$m=%q?0ʂ6F01v2H*Q)KǒM6aP4mF=ψ<[5o+ag7pfC\T=ʊQK[.ort0XFw._=wRTՆ"$zF%g:w.b3 ;eX\EXhNN>b)g HUs9`+\i eDi>*> x,~{'Uzr2|:|{&sH:TU`62z ;Mw??'$1C"%&*̓`lcZvc# *$';AyrESc=HBL16'=f':q8|4ۑmfb_PwUKA¦5K/Zi9o7'H{#0< ̾?%|>':І}mfM8>;ů>vy*S.H+w]'I ?+̯J}0 o;Cx9U6 2{ʓ3:F<Inϧ,:s %c%b=\#aܰ$v pZnYRӽ"oun QhkJtHsxE<tBl_ zv$uIΥMkks}Mrr>t]dTlZ?dfXGk| qeUR|]WjVgׁfOnqUqysEvgd:}}7ы{X]wUJWߕ]`ۡVlݚy(i_NrDq9u:z;}| dZNLrPppPW~b`dSOvN|PKoUXޠybnLZk;hrQlUoP\Xn~)|O5ic"STbN |I},~/.H.tUcCECV/^ROLdZs7lm>ϱ=58J>|dV!Ipoint11@̾Uή%𱞣֞P$%{織׎u2~ܙT目/ɭǹ _XXrǸ ɞ>xrv|Ê߆|.uֹ+ٛI&ݩL;R$3hG>31&//lj%x+PSM8mEE1T[b (O4kzS'3,:'D)4K&2lbIJ+m0"<-pG*C_"' 9k7q7)ʂ0>X'*v)C~ KFŒJ,aM+l=8ω2X+l!]h.nm4aP8͈S;Z5Unt9CBw7L:wKTք&{+fC,V7w']z1&:mHaP:~h\9:b1a jPy x3a{.Jr4v{9p{,p=5c<\69c6a}a=U}?L;5$Hc'EoBy<ƍ|fesn~f]tUCD>K8TiEi7.Hu)014 ͶC,K9 og|:? ?v,R.G+wl} H >+ЫV0 :CH@wFOϾpoint12@ʤTһ)ܓ%*r|ʴ5ou3ϧ1.t*ouÅƽ ʐL`xM^2mmLCJכ>ʉKIׄ@CT0kB52*&(+Ɖ x"L^M2 mCA1aZm I-V,.%2&B$- L# bHE+y2"7(zC%Dh"'[4k1q0%ɂ)8X%vA~GC’I%aL'l77ρ)W"gZn,ˊx-bO7ΉZ1ZF7ktP>wM'6wEV/_z>B??23wX&Y/9q9{_ x>I{Ac J,U;b{;br{8uk54 \6RX ;{?:?#'1Sf=!4/5;|?pAd: *=|,G6Z8~_^=tXI=&n>y$֋ crstnd01@biRϋ{IdFʱIzf}ׯ.ܙ?֊4ƺ"B}ӄ8© Bt3vFt}}h|Ιh:N%rR~l:x/s_*}SsBxn@τtKHo`E |Q7eHG} .rtI,/;}O5^jXy5-/zu<42G*_G*nTd64$tHNgo)[8S5D!|hSuAQF'4e.O; ?^\AD$\+eVtIdk?ewNö6}e*xH{?a\/m MU7%/ALPy^B##`pDMZAgoP=DhU`O+N| 9K]WW|u]zx]Lq9Mo*?cΒs~cr|cE`LZE[bK.^K_\GTiZSfw"t^b^zT`mהORD0BrD?p=Ol@*rJo8ܝ:ٗbګ=X>\fV crstnd02@biRϋ{IdFʱIzf}ׯ.ܙ?֊4ƺ"B}ӄ8© Bt3vFt}}h|͙h:N%rR~l:x/s_*}SsBxn@τtKHo`E {Q7eHG} .rtI,/;}O5^jXy5-/zu<42G*_G*nTd54$tHNgo)[8S5D!|hSuAQF'4e.O;?^\AD$\+eVtIdk?ewNö6}bh=>P|֖ crstnd03@biRϋ{IdFʱIzf}ׯ.ܙ?Պ4ƺ"B}ӄ8© Bs3vFt}|h}͙h:N%rY~l:x/s_*}SsBxn@τtKHo`E |Q7eHG} .rtI,/<}N5^jXy9-/zu<42G*_G*mTd64$tHNho)[8T3D!|hSvAQG'4e.O;?^c?D$\,eVtIdk?ewNö6}<= >ix֠ crstnd04@biRϋ{IdFʲIzf}ׯ.ܙ?Պ4ƺ"B}ӄ8© Bs3vFt||h}͚h:N%rY~l:x/s_*}SsBxn@τtKHo`E |Q6fHG} .stI,/<}O5^jXz5-0zu<41G*_G*mTd64$uHNho)\8T3D!{hSvAQG'4e.O< ?_c?D$\,fWtIek?fwNȯ6};x΀H4:OhqXgkVr̈YՄV=o_hrlXbhk_z?o/Sf/P\ltΉ:`4dX-[V53{HJuK-zxX^D3x2NY{4;Er5nw*WZMD?e*wHz=a\.m MU7$/AKPy^B##^pDMWAgoP=EgR`O+N{ 9K\WV|u[yw]Lq9Lo+?cΑs~cq{cE]KZEYbL.]K_[GTiZSfw"s^a^zS`mדPRD0BrD?p=Ok@*rJo8ܞ:ٗbګ=>j9r֢ crstnd05@biRϋzIdFʲIzf}ׯ.ܙ?Պ4Ż"B}ӄ8© Bs3vFt||h}͚h:N%qY}l:x/s_*|SsBxm@τtKHo`F |Q6fHG}Ά.stI,/<|O4_jXz9-0yu<41G*`F*mTd64$uHNhn)\8T3D!{hSvAQG'4e.O<?_\?E$\,fWtIek?fwNpˉ6~;ỳH4:OhqXgkVŝYՄV=o_hrlXbhk_z?o/Sf/P\ltΉ:_5dX-[V53{HJuK-zxX^D2x3NYz5:Er5nw*WZND>e+wHy>a\.m MU7$/AKPx^B##^pDMWAgoQ=EgS`O+N{ 9K\WV}t]zw]Lq9Lo+?c͑r~cq{cE]KZEYbL.]K_[GTiYSfv"s^a^zS`mדPRD0BrD?p=Ok@*rJo8ܞ:ٗbګ8=>&OSe֡ crstnd06@biRϋzIdFʲIzf}ׯ.ܙ?Ջ4ŻB}ӄ8 Bs3vFt||h}͚h:N%qY}l:y/s_*|SsBxm@τsKHn`F |R6fHG~ .suI,/<|O4_jXz5-0yu<41G*`F*mTd64$uHNhn)\7T3D!{hSvAQG'4f.O<?_c?E$\,fWtIek?fwNpˉ6~;ỳH4:OhqXgkVŝYV=o^hrlXbhk_zAn0Sf0O\luΉ;_5dW.[V54{HJuK-zxX^D2w3NYz5:Er5nw*WZND>e+wHy>`\.m NU7$/AKPx^C##^pDMXAgoQ?EgS`O+N{ 9K\WU}t[zv]Lq9Lo*?c͑r}cq{cE^KZEYbL.]K_[FTiYSfv"s^a^zS`nגPRD0BrE?p>Ok@*rJo8ܞ:ٗbڬ=V=>t`'ZV crstnd07@biRόzIdFʲIzf}ׯ.ܙ?Ջ4Ż"B}ӄ8 Bs3vFt||h}͚h:N%qY}l:y0s_*|SsBxm@τsKHn`F }R6fHG~ .suI,/<|O4_jXz5-0yu<51G*`F*lTd64$uHNho)\7T3D"{hSvAQG'4f.O< ?_\?E$\,fWtIek?fwNpˉ6~;ỳH4:OhqXgkVŝYԄV=o^hskXbhk_zAn0Sf0O\luΉ;_5cW.[V54{HKtK-zxX^E2wj3NYz5:Er5nw*WZNC>e+wHy>`\.m NU7$/AJPx^C##^pDMXAgoQ?EfS_O+N{ 9K[WU}t[zv]Mq9Lo*?c͐r}cq{cE^KZEZaK.]J_\FTiYSfv"r^`^zR`nגPRD0BrE?p>Ok@*rJo8ܞ:ٗbڬļ={X>lQV crstnd08@biRόzIdFʲIzf}ׯ.ۙ?Ջ4Ż"B}ӄ8 Cs3uFt||h}͚h:N%qY}l:y0s_*|SsBxm@τsKHn`F }R6fHG~ .suI,/<|O4_jXz9,0yu<51G*`F*lTd74$uHNho)\7T3D"{hSwAQG'4f.O< >_\?E$\,fWtIek?fwNpˇ6~;ỳH4:OiqXhkVs˂YԄU=o^hskXbhk_zAn0Rf0O\luΉ;_6cW.[V54{HKtK-zxX_E2wj3NYy6:Dr5nw*WZNC>e+wHy>`\.m NU7$/AJPx_C##_pDMXAfoQ?EfS_O+N{ 9K[WU}s]zv]Mq9Mo*?c͐r}cq{bE^KZEZaK.^J_\FTiYSfu"r^`^zR`nגPRD1BrE?p>Ok@*rJo8ܞ:ٗbڬ==d>;rYNV crstnd09@ciRόzIdFʲIzf|ׯ.ۙ?Ջ4Ż"B|ӄ8 Cs3uFt||h}͚h:N%qY}l:y0s_*|SsBxm@τsKHn`F }R6fHG~ͅ.suI,/=|N4_jX{9,0yu<51G*`F+lTd74$vHNio)\7U3D"{hSwAQH'4f.O< >_\?E$\,fWtIek?fwNpˇ6~;ỳH4:OiqXhkVs˂YԄU=o^hskXbhk_zAn1Rf1O\luΉ;_6cW/[V54zHKtJ-zxX_E2w3NYy6:Dr5nw*WZNC>e+wHy>`\.m NU7$/BJPx_C##_pDMYAfoQ?EfT_O+N{ 9K[WU}s]zv]Mq9Mo*?c͐r}cr{bE^KZEZaK.^J_\FTiYSfu"r^`^zR`nגPRD1BrE?p>Ok@*rJo8ܞ8ٗbڬT=\>&olP֢ crstnd10@biRόzIdFʲIzf}ׯ.ۙ?Ջ4Ż"B}ӄ8 Cs3uFt||h}͚h:N%qY}l:y0s_*|SsBxm@τsKHn`F }R6fHG~ .suI,/<|N4_jX{9,0yu<51G*`F+lTd74$vHNio)\7U3D"{hSwAQH'4f.O< >_\?E$\,fWtIek?fwNpˇ6~;ỳH4:OiqXhkVs˂YԄU=o^hskXbhk_zAn1Rf1O\luΉ;_6cW/ZV54zHKtJ-zxX_F2wj3NYy6:Dr5nw+WZOC>e+wHy>`\/m NU7$/BJPx_C##_pEMYAfoQ?EfT_O+N{ 9K[WU}s[zv]Mq9Mo*?c͐r}cr{bE_KZEZaK.^J_\FTiYSfu"r^`^zR`nגPRD1BrE?p>Ok@*rKo8ܞ:ٗbڬ=L>=hUcrstnd11@biRόzIdFʲIzf}ׯ.ۙ?Ջ4Ż"B}ӄ8 Cs3uFt||h}͚h:N%qY}l:y0s_*|SsBxm@τsKHn`F }R6fHG~ .suI,/<|O4_jXz5,0yu<51G*`F*lTd64$uHNho)\7T3D"{hSwAQG'4f.O< >_\?E$\,fWtIek?fwNpˇ6~;ỳH4:OiqXhkVs˂YԄU=o^hskXbhk_zAn1Rf1O\luΉ;_6cW/ZV55zHKtJ-zxX_F2wj3NYy6:Dr5nw+WZOC>e+wHy?`\/m NU7$/BJPx^C##_pEMYAfoQ=EfT_O+O{ 9K[WU}t[zv]Mq9Mo*?c͐r}cr{bE_KZE[aK.^J_\FTiYSfv"r^`^zR`nגPRD1BrE?p>Pk@+rKo8ܞ:ٗbڬs=7>s_Zcrstnd12@biRόzIdFʲIzf}ׯ.ۙ?Ջ4Ż"B}ӄ8 Bs3vFt||h}͚h:N%qY}l:y0s_*|SsBxm@τsKHn`F }R6fHG~ .suI,/<|O4_jXz5,0yu<51G*`F*lTd64$uHNhn)\7T3D"{hSvAQG'4f.O< >_\?E$\,fWtIek?fwNpˉ6~;ỳH4:OhqXgkVŝYԄU=o^hrkXahk_zAn1Rf1O\luΉ:_6cW/ZV55zHLtK-zxX^F2wj3NYz7:Dr5nw+WZOC>e+wHy?`\/m NU8$0BJPx^C##_pEMYAfoQ=EfT_O+O{ 9K[WU}t[zv]Mq9Mo*?c͐r}cr{cE_KZE[aK.^J_\FTiYSfv"r^`^zR`nגPRD1BrE?p>Pk@+rKo8ܞ:ٗbڬC=( >QU^a3crstnd13@biR΋zIdFɲIzf}֯.ڙ?ԋ4ĻB}҄8 Bs3vFt||h}̚h:N%qY}l:y/sf*|SsBxm@΄sKHn`F |R6fHF~ .suH,/<|O4_jXz5,0yu<51G*`E*lTd64$uHNhn)\7T3C"{hSvAQG'4f.O< >_\?E$\,fWtHek?fwNpˉ6~;ỳG4:OhqXgkVŝYԄV=n^hqkXahk_z@n1Rf2O\kuΉ:_7cW/ZV55zHKtK-yxX^E2w4NYz7:Dr6nw+WZPC>e,wGy?`^/l MU8$0AJPw^B##_pDMZAgoQ=DfU_O+O{ 9J[WU{t[yv]Mq9No*?c͐q}cq{cE_KZE[aK.^J_\FTiYSfv"r^`^yS`lגQRD1BrD?p>Pk@+rJo8۞:ؗbجx=>Ih#ͽcrstnd14@biRϋzIdFʲIzf}ׯ.ۙ?Ջ4Ż"B}ӄ8 Bs3vFt||h}͚h:N%qY}l:y/s_*|SsBxm@τsKHn`F |Q6fHG~ .suI,/<|O4_jXz5,0yu<41G*`F*mTd64$uHNhn)\7T3C!{hSvAQG'4f.O<>_\?E$\,fWtIek?fwNpˈ6~;ỳH4:OhqXgkVŝYՄV=o^hrlXahk_zAn2Rf2N\luΉ:_7cW0ZV55zHLtK-zxX^G2wj4NYz7:Dr6nw+WZPC>e,wHyA`^0l MU8$0BJPx^D##_pEMZAgoR?EgU_O=O{ 9K\WU|t[zv]Nq9No*?c͑r}cr|cE`KZE[aK.^K_\FTiYSfv"s^a^zS`mגQRD2BrF?p?Pk@+rKo8ܞ:ٗb٬=&>;fq0crstnd15@biRϋzIdFʲIzf}ׯ.ۙ?Պ4Ż"B}ӄ8 Bs3vFt||h}͚h:N%qY}l:x/s_*|SsBxm@τtKHn`F |Q6fHG}Ά.stI,/<|O4_jXz9,0yu<41G*`F*mTd64$uHNhn)\7T3D!{hSvAQG'4f.O<>_\?E$\,fWtIek?fwNpˉ6~;ỳH4:OhqXgkVŝYՄV=o_hrlXahk_zAn2Rf2N\ltΉ:_7cW0ZV55zHLtK-zxX^G2wj4NYz8:Dr6nw,WZPC>e,wHyA`^0l MU8$0BKPx^D##_pFMZAgoR=EgU_O+O{ 9K\WU|t]zw]Nq9No*?c͑r~cr|cE`KZE[bK.^K_\GThYSfv"s^a^zS`mגQRD2BrF?p?Qk@,rLo8ܞ:ٗb٫4.=>V+f{0̳crstnd16@biR΋zIdFɲIzf}֯.ڙ?Ԋ4ĻB}҄8 Bs3vFt||h}̚h:N%qY}l:x/sf*|SsBxm@΄tKHo`F |Q6fHF}Ά.stH,/<|O4_jXz5,0yu<41G*_E*mTd64$uHNhn)\7T3C!{hSvAQG'4e.O<>_\?D$\,fWtHek?fwNoˈ6};x΀G4:OhqXgkVr̂YՄV=m_hqlXahk_z@o2Rf2N\ktΉ:_7cW0ZV55zHKtK-yxX^F2w4NXz8:Dr6nw,WZQD>e,wGz@`^0l MU8$0AKPw^C##_pEMZAgoR=CgU`O+P{ 9J\WV{t[yw]Nq9No*?bΑq~cq|c7`KZE\bK.^K_\GThYSfv"s^a^yS^lדQRD2BrE?p?Qk@,rKo8۞:ؗbثL=g>/0crstnd17@biRϋ{IdFʱIzf}ׯ.ۙ?Պ4źB}ӄ8 Bs3vFt||h}͚h:N%rY~l:x/s_*}SsBxn@τtKHo`E |Q6eHG} .rtI,/<}O4^jXz5,0zu<41G*_F*mTd64$uHNho)\;7T3D!{hSvAQG'4e.O< ?_c?D$\,fVtIdk?fwNȯ6};x΀H4:OhqXgkVr̈YՄV=n_hrmXahk^zAo2Rf2N\ltΉ:`7cW0ZV55zHLtK-zxX^H2wj4NX{8:Dr6nw,WZQD>e,wHzA`^0l MT8$0BKPx^D##_pFM[AgoR=DgU`O+P{ 9K\WV|u[zw]Nq9No*?bΑr~cr|cE`KZE\bL.^K_\GThZSfw"s^a^zS`mדQRD2BrF?p?Qk@,rLo8ܞ:ٗb٫k={w>#0Խcrstnd18@biRϋ{IdFʱIzf}ׯ.ۙ?Պ4źB}ӄ8 Bs3vFt||h|͙h:N%rY~l:x/s_*}SsBxn@τtKHo`E |Q6eHG} .rtI,/<}N4^jXy9-/zu<41G*_F*mTd64$tHNho)[8T3D!|hSvAQG'4e.O;?^c?D$\,eVtIdk?ewNö6};x΀H4Ž:OhqXgkVr͂ZքV=n_hrmXbgk^zAo2Rf2N\lsΉ:`7cW0ZV55zHLuK-zxX^H2wj4OX{8:Dr6nw+WZQD>e,wHzAa^0m MT8$0BKPy^C##`pEM[AgoR=DhU`O+O| 9K]WV|u]zw]Nq9No*?bΒs~cr|cE`LZE\bK.^K_\GThZSfw"t^b^zT`mדQRD1BwF?p?Pk@+rKo8ܞ:ٗb٫J=m>y$c crstnd19@biRϋ{IdFʱIzf}ׯ.ܙ?֊4ƺ"B}ӄ8© Bt3vFt}}h|Ιh:N%rR~l:x/s_*}SsBxn@τtKHo`E |Q7eHG} .rtI,/;}O5^jXy5-/zu<42G*_G*nTd64$tHNgo)[8S5D!|hSuAQF'4e.O; ?^\AD$\+eVtIdk?ewNö6}e*xH{?a\/m MU7%/ALPy^B##`pDMZAgoP=DhU`O+N| 9K]WW|u]zx]Lq9Mo*?cΒs~cr|cE`LZE[bK.^K_\GTiZSfw"t^b^zT`mהORD0BrD?p=Ol@*rJo8ܝ:ٗbګ{95;crwalk1@dfRwIeFܴIΐwhz첦.?錄4ؽCz熥8ӫ Cp4sFǞvyyh~k:O&nYƞzm}:z0pf*yTpBzi@pKIk`J V<kHM҆2xyP,2BN9cj`9/5~uC86G.dJ-oTn84(zHUnn-a=Y5H$~h[{CYJ'9k.W?EccGH$d.l[tOkkFlwUuψ;B~ҀO9ŎAWnr`mk^wЂc_DvchzoWkmkf|:q!WgT_wxЉ6c&hX_Y:!J:xI/zX^53uP]u>GuovX`4E;cyF}b[p#P[% 9Mh'!%Zk$NJ?bo1>BlDaK>1|5H`UXx[yw\-r91p'?gϔckx_EOJUeHbD?UI_UGThZSex"t^b^{T]rٔ0SDCr$Ap'0m@t*p::bﭳ<ӚgCè*ײ`潓Kڔpޘ⭽ 2ε#đGԌbɗ@ܗʵ?g*P.B)НŅ_(F%>*êR<ǰA_xXo۱1t>&B%ھKˎ*ى*q*{/Ʋ YL%ٟ/"#HHص!#ɑzϢzNzm$ϛAvauYr_w_x$tސYrԀErƖ>|DYDRÜFؙiXIxʍśɈЈՆքrkщmh`X|iubŃm}iɆldỲYʙSוeˠlЙ̚QQ|Cw7v9r:rx|ܕ萷Vp\W`o]Rwvؙh⑍\Ϡ^W[HiSnbd`]^oĞ)N҅5ܝ۞̆ &,Ǚ/.ԝGՏ.Vn.e 0RwGK{p7qxf >+=>sٔv[0crwalk2@`nR~IbFڮI΋~d謭.햞?爋4ַA䁬8Ҧ Aw2yFǘrhzޗf:M$uYƘi:v/w_)QwBuq@߁wKFr`$L,XG"-mHW˅?x)zZ,?CyOEejh9<6wuMWBRܭFќmXH}Ljj͈jԄqz}΁mh^X|iubĀm{iǂkbY|YŔSוbˠiЙ̚PRyCt9s9o:ouxܕ萶TpZW^o[Pwtיf⑏ZϠ[U[FiQn_d^]\o*Ń5ܝ۞Ȃ &,Ô/ڌ.ΘGϋ.{Vac/>ov^Yn7px%>>C>nҘb$3crwalk3@]vR܄I^F֨IɆ`䦵.鐧?⃔4Ұ>}8Π >0F’nhvڐŅc:J#}Ye:r-f'NBqy@}KDz`O} Z9oHJ 0y{L,1EqS6fj\9-8qu~ÀJ6<Sqq\pkVxYYAuhh}uYcoziWVqI[hJW^lvłOaNkYGbW7MIc{M-~Yu_={NUitRDGrPrwF]hkMCeF{\{[fcJq!^`R%JWLPyh\##wy`SsLl8m=YhnhR+i"9`]nWs]y]hu9hu,3uБxcf7zV^EukO.vM_sIT[N}x"u^c\U]זlXDLIr`GpZlp@Evfs8ċ䋺p1͏gퟰF峻/֦_宦>ЊŊҌƇ֠ DZ #صGхdˑCыn̫3`)95%>Ðч^(0+"K¡R)$θ5B8y?pɯ ̺?`ɻKÜ/΋$w*{/ Hޔ-ϫ2$&EOͽ#$ړlWCPөFǙmXK}yyy}сxpzx{Ƀ~nh\X{iub}hzh~y`YyYN}ו`ˠfϙ˚PRvCq9p9m:mrvwܕu}萫SpXW\oYOwxqיwd⑂XϠZT[EiPn]d\]Zo*N~5"}ܝ۞ &,/҈.ƓILj.yVXcT7SshXs9qzS>&> >X&Hjecrwalk4@[zRԁI]FϥI„_ܢ.?ڀ4ʭ<z8Ɲ =/Flhtҍʅa:I"Yc:pƒ,_'MBo}@zKB~`Qx [7p|HG .z|I,/EkS5hjX9+9mu9D2G*hF*f]c;}9$|HNqn)e8^5?+wyS}ARLtI4o.NI?hcAOw'X7o`tInkAowMv6<G4:PsYqkRxUU=rig|wY_pzhW[rP]kQY_evTaUmZOdX4TIi}N-YxeB}VVloZFHrXswN^hrPDeN|`||chcRr!``Y%R\MPrec$#{|gUyPn8t=]islT+q"9d^rXr]z]pv9pw-3vАrch7Y`E{nQ.{N_xJT\Syu^d\U]זsYDTKrgIpasq@Nwnt8܎ɋڈpۛ;ƌg㛶Fޯ/Ѣ_ߪ>LJɊɉʇ͜ԅ#ӲIʄdƐCȈnǨ3a)<5(>Ӈ_(2*%LR+$и7B8{Dpͯ1ʸ4`ͻK2ы&w*|/ Hۚ-̯2$&FO#ږo¢?ztp!צ6}d\|Dyfx%|٘TwՋ8wˠ8?WDRשF˙mXHxyyyՄwπspzrzǃzng[X{hub|hyh|z_YxRQ}|֕`ʠ}eϙ˚PRuCp7o9l:lqtq}ܔp{萣RpXW[oX|Nwrpיqc|XϠYSZEiPn]d[]Yo)N}5w۝yڞ~ &,/ʇ.G.wV\c[9Uso[t9sz=u ><>QSF crwalk5@XvR}IYF矡I[.?|4⧯:v8ޗ :-FҊghpƅ]:F!~YЊ_:l*_%JBkz@vK?{`E vO> bHPw 4moS,4:S;[jdt50/uC78G/\M#v`q17)oHXdn.Y?R5I!k^pA\B&;a.Z<H[cIC'f+bTtS`kIbwYh͈=wEqрR|:ƎCZfceh`ijf{لdFygg~wRldhkMlĉ΅٣ г#묣G݅eӓAnצ?s)D3/>ׄΊw(;,+LјR-$AƗ@:HpƵ1Ҽ?ޙ^Kü0ȗ.w*/ H׬-ļ0$&\O #ڙxڍGw}#γ? u@Vx(|ұcu̘BuBSPZPѯFĠmXHy{k̂kkۉu׉ezmςggRXzcrcyixawhVYnTτNrיVˠ\ϙ˙PRkCe7d9a:afisܕq萼IpOWRoOFweיY⑑OϠPJ[P^=m>>^\>D'[-eVtHdk>ewMö5};xπG3Ž9NhqWgkUr͂XքU<l`hpmX`gk]z?o1Rf1O\ksΉ9`6cW/ZV44{IJuK-xyX]F2wj1OW{6:Dr3nw)XXND>e*xF{>a\.m LS6%.?LPw]A##^pCMXAgoO=ChS`O+M| 9I]UWzu]wx]Kq9Lo*?aΒq~cp|cE^LZEYbK.\K_ZGTgZSdwt^b^xT`kהNRD/BrC?p<Nl@)sIo8؞:՗b֫e<=o>a^ؐb crattak1@^m`夂+k:Ɯ,اxOqwŷ֧0ࠐ<ǥIΈD}26ЀkɌ{_%X~l>$]|B Ldr҉4_>FHf?y]KBLTGNNs!L=JPpWW|#`R_3e"_8k=Σ+p; Ҵ#ӛ'p@Bi*ڥf-ܢ_%ą[(ͫU׭YHGE_xx 1x>M4<Kō+߀>a*t*ߩc%.,ٚ+$!GH ܮ!nehzc$ŠIƱwuXhSMwfrfҬxͧ'p݉!^soWqˆAG]CTFmԤWIٮhȇφtքohjp{҉p_hVŸomfbzmpmZXzRQyڕ\ϡxdԛ˙QQvBq7p9m9ntyoo쐜KpQWXoUxFvpqܛoaxRԠVL[9iInXdU]To)N5 "tߕwܞ1&,/3G.yTY7XDiZ&}rp9kw>~=m[>qa crattak2@bYLs7է(wɻ254ٴPJ76p&y† h~*X$';x>cy8Ve|{_}CI\=bW{L|x{hhI&wt($;6aV$aj<1:%ar:6w G aTgf];<zyI5t[4Yy0Isk>?PLu&*m/FD3feBHq Q0_q Oiu+qy'ow+%|$9qq;ry102!S]k^c\>uyMx[LZB+KD*BAJOHCBC:>#Kf:]Z< fp\ay~PZyVD WutrN)XkOOPc%,eOwPh^X_OeMnGUSS;RHG͗_!bJueCcQop=PZc>?*it0ms>_k;UsTOkn~p[aN`UexpIDT4re1p` og@Oqkl<8e1]ɳ?ݾ%ҤJޭL6ȋ !&oḊ4l/^2S/d,ܳQ#N/ǡIBȴCD mc 15;F9Kϖ%1[ /z/^ )%-"NHfo |wnRgl!͙Bw`|Vood xж"q{"`TqwG[ʖ;EYESFiշXHs͉|oZMMyJUqkrm[v{jnuny{yɂ|r]SQPӝZrʠRzPuPbNWSC9:;y\R~E֕BqapqgY}o[kOZwGPDwܑQgWwloa\kNiq^npom^lnx*xO0`YJ֝JYמ{ w(v,x/.I0JqpEEM1qW mf9hsW>X=J>z>Әb crattak3@c}WKt7آ&wζ034ݰP8J7E;z$j;h)X#&c7We}x_zAI\=pWN!iuH/{ x0$94lV)ejD<6$nr;7$G"cQqfa6<|ISH{I-o%JE6heCHy'T0a Pyu3uy.sw4)%|,!#?tqDvy:::'[`kee\IyySzL`:-R:,ILńJVAEH9U[>2orZf{K]yDC Zuyr>/Fit_g=VtTOonp[aN`aky^GDB2rT0pN ]f@ɮ=D~;iY 13/C5KҒ%)[ *v/ٻ`+%-!HHu y {riKsh!В<u]~UondǾ xɴ v{"l"QrrAqʏ8{?YASסFiɷWHw̉~gYXÃU`uks[yznyyɂy]RQ^וhxϠ_ҚΚqNdQC99< pcSܕQ搂hpnWyons_`wUؚS|_mӠv~h\|Uignxot]to*N0sgX۝Xfڞ &,/.G.Unh7D?7rVnf8gs!k> =/>b crattak4@c[Ƿ}Gډ4۝|vܰ-͟+˽)ܫVέ9KM4̞1)"ʇt ў2|0Z:G/lW /e;dDсy_ϟvKRc'3wXp!EyN$_zG*u l)$H3uV,ajB0H"wsK7"G']fyftC5!tI=y'X >X/\jE=bO&1k%ZI[.GK&=I%-hAB]?2msZa}<^y'? Tt}r(3)ftKP@$-fsKs0TT"bGwF.!#AFQVIџ9 -]Cu8=OPdD?HdJB?+Bq9NYYRxvhz}mi@k 9@`"?\yVڹzpmXSET#EeQE;3Zs?U[?VkSNhn"~pZa{N`eezCDD%/r8-p2 Cc@m>g<Ƚ:ȷEξ1\׬?%ٜJ(d !&nDƇe_*I/>* Z,A-/=G>gQ3b crattak5@d~[ºzGӇ4ܝzxذ-ʝ)Ƚ)۬U̩9MJFȜ<"l͜2z.[2=0kQ 'e8cDɃs_ȢqKQc,.wXf$?yxN'ZzG&o d$!N1vV,]j?2O!wsR4 G(Xnw_{E5"oI;w(S BU/dlD=fP3f%`H>dEUG r8I;|r,sj)nw+({!?rs?uj3ā54W^k`d[DyLy;d$.W#,NIʄ8[)GL!]?2kqZ]}7_y"= Pt}u$4$dtIL<#-fqGs+RTaCxC)"a@k9f<ɹ:ȴeɼ1\Ԫ?%ٜJ(d !&oDŋD`.J/?* Z,B-=/ʘO:ĩ9~D{;eOo߶ 14%C*Kя)܂"[ *s/̸ TL}%% #CHy w ymԤeDse$̍7u[sSdmd xup|"l~Nrn:Ŋ4{=>hb crattak6@exXxI˂F۞yyү/ś)ĺ=٭TȢ9NEF˜< lǘ2v+]*3os=qj2Ł32V[k^a[BuJv7e!.X+OH˄4[&FL%לV(d !oDËD`.L/A* Z,C#>%ʙO;ç:~D|;dQߴ 14&C.KϏ*܀#[ *s/˶ TL~%/ #DHy v yoԢcFhe!ˋ8u~[pSdmd xtm}"l~Orl;qÈ5{RțFܗiXHvɉΆiYVǂǗ\oko[zlytpk~yłv]Pș`ڕnpСcvԚΚrNeQC49rEqv}g7fzr>n=>-5b crattak7@V[ͪ{G{4͛{hί-о+ѯ)ΪIԠ5=A4ҏ0ʊtū א2o#Z-Ƈ9/DŽlJ "e/WDtw_ՑtKEc('zXb"8ytN%S}G+g _*!J'xV-TjD0JzsN+"G'Qh|fx55"fI>l'L AL/^jG=dB&3]%\<=ZeS<l+D5u1hj-dw2xĄ*y ~|(CgsEkj:~Ɓ<:!\Vkc][InOr;f&-Y%*PP̄8[+EM#:L%*dBAZB2nk[\:cy$7 Qq|u'7&_uCN? 0flFt.MT [D{E+z" C=Ak9IYURwr~gwoi>b 9>Z#?YY˾rbkYVES!JeOB>3WqBUWAVgTNdo"zb[axN`ic݀B<D"(r6%p/ B[@d<`<ΰ:ΪEǾ1\ɫ?ٴ%˚Jۤ(p !ܠnBËea*N/D* Z,E#A%V<ĥ;~f};cT 14)C.Kː*}&[*s/β Y+%%!DH { zq֞bIhe$̉:u[sSdnd xuq"r}Qri>qņ7{>Y>R˗FޒiXIøwȉ qa]wy\΄bhkf[ul~opkxjÂnYRQgڕugϡknԚΚyNlQC49u=>Ka crattak8@ZuYϚ{InF{d/ج=՜3Ʀ?ב9:3FҀ:l̤ zZD&3e/V?>`cIC'd-Z Jt?kj8iwCxɉ00|:)-KlqQmkK{ʉMJ0f_hmiXWojYz?i-F_-CU`~Ή:]2YR+PP,0tEGlF-uu[^B!my-HV|u0.8p8 Ig@$oDl:֨:աdѶ1Z׺?ͯ%UʝKdĝŵ յ!ΚGdBDf*V/L*ʅ\(߭K#I/TBūA~B u` 1~?1C4Kʑ/ޅ-[*v/ұ`L%ܝ/"#FHޯ!!zۡsShf$̒Bu[tUMzerex#tvXrxHqŏ>{CYBRЛFmWIȷzĈ̇щrӄmvymωpmmfXzmtpk|mjmYRQwٕgΠynҚΚQ|QCƅ49~<}~kޛi鐢]dbWk6a|Rwnwښnh{]Ҡg]\LiZnn6i]ho*N.oޕp~ܞ &$/)G/V}r:E2HsO;{{l7kw4m>_=m>?"b crattak9@biRϋ{IdFʱIzf}ׯ.ܙ?֊4ƺ"B}ӄ8© Bt3vFt}}h|Ιh:N%rR~l:x/s_*}SsBxn@τtKHo`E |Q7eHG} .rtI,.9~S5^jXy5*-u<62G*_BtYc.7$tHNgo)[8T3?kSuAQB&4e.Q; ?^\AC'[(eVtIdk?ewNö6}e*xH{?a\.m MU7%.ALPy^B##_pDMZAgoP=DhU`O+N| 9K]WW|u]zx]Lq9Mo*?cΒs~cr|cE`LZE[bK.^K_\GTiZSfw"t^b^zT`mהORD0BrD?p=Ol@*rJo8ܝ:ٗbګχ=>0crpain1@S`=Y8+Od <:tCʊv>Q(oqY `xքh}Rk#`wɍmuv{yvv.r bd/EdeDwF:^yURypv taL |{-0gZKz-;-YX-ap(AF^fJSu=$l*v15Dgz24Clt,"26d'e6on%=Na [xW›*q*y$')αΤC~zP~]0yEXRiK7bM3V5L[OLSKAP"ǎOdD_aI-Z\g]tjT4QrVAqUWwM>N~i+?EMbUu^JcPUSmK˄Us"QSHPAM]z"#l_a3m*f8l=SbgLO+gi!5XXbRig~tqg[9f[+3exM"oi{ob7q6ZEnOL.l{J_hETqWSor"yi^Z~Q`Qo|"k8DR&ra$p\kU@M\fY8ڇp Z7/B?m؉~ #ƺLmE}ق8m/ٞg-ڜc%q_,ΧXר_ UNFdqv<9R4#<Kơ2?\/w/%c0//ޤ2/KH\s$)lbh g!ŌH#r\yUMpqpڽwӱ,x!_"[rpTZǎ>GYDTFiٹW I߼!gmԉh؄`QC?>Ek8VchZholfhmЈ6TD|^Tsf[IEOLJ^~ofDiJ;m6mv8x}:}<LS aL`-0ُ6O0{<<WHO'CO1~3ǝ*є.,ȑ0Q/ڒYzm_yBF{y\ s9hKm.pCHayegspV|lG`kSfcKpj?s`x[7WMsb6m9kzf,>\=t>MqX[Ucrpain2@TT=W8LOa199 xAyŠy>)6l_izzІaMp#`tm~t}},y ei-EefBtC<^s`Rz|v lL!||(-+b_Hz5;'U]+\v!(;C__JN:L+l*z|<1Blz7?Bdt+$07^ *c4hq%=Irh"x%""03z.11#IwhT|]9yHXOiNCbP=U?ƁHZPUROJM%RlAalJ]]c`(vj\APr_,Bq^`wVJNqox{KYwHiPHfvtvox|Jcenl ~.hIo%zKdky|F|7qslUptdBlbn^``Ä[7_OgcG|n5lzс>l=>zcrpain3@YxT>X7INa0‰4}7?x ?,Ekuzxņ_J%Ygr}+f,IDk|@wC?~`eslx,|I5ņ!;%*ViO'qzI:%Ji0P,I!v;8d`SB8!KpLJ #IpCl4n*e/]*ц`(ٰV#W-RHŭHB‡ v14HF9K/=Z*y/c#)ݎ%Ѧ2$!KHԷ#!|dzg!ʗLȵw]{VM}eueԯxϫ'}ގ{ar΀WqǔEJYGTFmթXHڰhɈ҉քufavxádpsr`˜za|ua|myby|[WgT{VoٞdvΛbӝQGNZcQpVnXn]zxrhfqUso^QOmhg~Tnݑa۝YZvәQl[ɠbQrrPzIh~RXNh4Oe8%tMz"iL3{E=>`# crpain4@biRϋzIdFʱIyf|ׯ.ܙ?֊4ƺ"B|ӄ8© Bs3uFt||h|Ιh:N%qR}l:x/r_*|SrBxm@τsKHn`D |Q7eHG} .rtI,.9}S5^jXy5,,}u<62G*_E!q`d44$tHNgn)[8T3CkSuAQE&4e.P>?^\?D']-eVtIdk?ewNoˈ6}e*xHz?a\/m MU7%.AKPy^B##`pDMZAgoP=DhU`OLN| 9K]WW|t]zw]Lq9Mo*?cΒs~cr|cE`LZE[bJ.^J_\FTiZSfv"s^b^zT`mהORD0BrD?p=Ol@*rJo8ܝ:ٗbګ1>횎.=death301@Q^ٚ"?cʓЎaCUԵaڥ;uރirϼVā~W`Zѯ2m]z=àba ۪ ٛ:wimpȫ 9莌!ugmbݖlvlO&tc#^)āqjulK3v)gCxz{a4(<a_A0z8 !;',kirJ &k4$ t/?) w g8q w|su(Ɗ{(db+ʉn4f^ hJ+}Q+[H&u=6w<̇X7ρ><|;@A3RۅY8gDԉJ6>kz.;m.3hKKӀkI{u5>,4kU9Yd28oI?c9Go?.De0 In6^Q4p[(ZPgNGA)mGz[=RU-qoPwhO\A5&-uhUg+\KZ:?(OX4gA 0T1(LZX{Q9HK!X_Uiuk9k4FI$XJ"]k7c1lW([2~W3"gY{lddynPRy~5z?zq@(oXw`KGt.Ev@);DJN=( PH%Xb,HdIFͺ4FO7ðo*_ A,/7n *Sv,u"S!G :nwMCSV>J*-R3 #%1K+Mu׶3e"y"B eZM(`{b!?)v< Ti,Ik]X`_Yd8b8-TK$ %!UN2KM+>$%` i=BrqFd06K7_FA4i.: =\=g1>մsP7sAf2_:*Sk adtˇ6aʂ}'ee hNvb\Ur18t.ЇU&΁=-{>2P FۅN8e5ԄIY9?0>F] eI=(>?7YK{HAp=5$XS}W`ra9a4V;"':2_a8Yz1e`'Q<M<"\X_aNesnPTsx5t?tq@(fYpH? O]N3H=+AC>E"uh:"I[dϽ,JaIFѹ4BO5űr*bC-Ž/4o *Uw,w"U!K";mySL"NSRS*-M=Y&%2Oe%IBw4cS+{",@s6fiEAAxd"|AkEskWLz|;pW{Wq.3`(pGX߶#`ެ(H׹\ǙIE3Ь>˖N4ӟlų*]A,.2h *Qp,V)":e],$IS}1,*q-=N$N4O p^1vu"` TYazWMY,A2lC">[,I9]fI2Tm hepS_SR.q38iwiWy0y5c^n0X:XyL8 +#?cG5~-/i4RtOG΂>I}fLЉHJbt{1po;] l1P ROEekFZ=&tT0PZ(W^N#<(1Jkkl*4U85death304@hQ{胓 c͟|~vZjִx""avhi#zbWq!{qޚ2O[\?Ɋi͹ ܄YMlfnΘs Ǩ9ue Xgrs|hQLi*qVBN j{aV /f5]҉ N!G+)}=2>iƚ.ax3%]Ř{"679 Ň8 ƒ#Eiu ucq,ƌ%ˉtŃd%{g+}XAՅJ8,΄6o'Uz#mkl7~2cu*#l*_29- W86r%D1$X6n]S%]3&=i6MV#g3{z*Ss=w4]fM=}|+kb&dzpx`ɔ.Ҋv46&iq"i|Zy8c\(n>KX)y!#uQA ,1&0;\n_WYi]RuGтgI}LӉoJi{1o; 1w xOElF=QuTZPZSW`t#d(ZJka~eCiV5kEO{SId`V`-wAO 9PMUAN[KDq+e$P =hiF'?O^2,4_q.Ԍo7e >=W>2UFdeath305@nQ臓 p͢҂zZwֶ""nvmi#fWu!qޞ2T[a?Ɏiͼ ܈^MllnΛw ǫ9yk ]gvwnQ=i*qVBN jaV /f5]ԉ N!G+)}=2>iȚ.ax3%]ǘ{"679 LJ8 ă#Ei ucq,:%̇ǃq%{t+XAׅJ8,Є6o'Yz"mhy7̀6mu+ k*_-62 W90nn'D)$]6n]H$`&x=i<MGg3zp, Sog=w4)9OCIPD.J{A'i?$Su_ea9a{4CB{; amW~1bB&MI"!]FcTu\Ptu{5vsCv_@`fr$H":Y,3 5E9J?G߽#޴({ǢIxiдtˠNkӨź*udeh *,a/$& pv8)"|Sg3y:*n%KN$ O-O \*V`OMK+6/a[tL+;?]^=.2yUpwgΔ.׉y40mdigy V`1$F_s3ًr74$>l=B(>OOPHQdпdeath306@k{!ЩՇpqٶ !ô}Wr%ǻWV*r.5.@˄gϯ0/kdḎmBsуoI8ɖETp?+Dwh z]^I {Z58rxQN n oh%<0o=_')鎩|5,2&B|*B"g5t_y8,^tw 4wfICE )fi x]r/E'%){F9ND.7e'Phii9Ȇ0mx!,jm/Yx-|43ZD3Ud5_$ [6wp$]= ^!x=gA ;i/uk[$S^ g;y;qY)lG3C^NǀHtU- F*>$!OOGE10EF*V1$ N||[rcsFb]C8+c2"^|U{킚3h9W}I$_IbWrer~s?wV@uTTe~o1 # K6 .01#>' G%IڻɝOһְͦȪLȷ/ í߻.ⴣLҺ2ͼ&[1$, ƪu{>t0 ^<kBङLX,J νz$vO&O $s[$)"yJTH'C6 1>eB6.AY^74lz9H(i}sݗt=Ջ\>)aU(hx7\%Zq1kLrWz9>!#y02\Ѡ܈˄k~TɀJƇKɉO:Na3t5ݝ/䊶3ן-SfnBAyYTg[]ۙ,˛B_n7u1y Ђ Ȅ䎮{R͔QI\ld|FwhY`ҘK[mHi;mXXMb]ե/ޝUҗ2T˝¦Vʜ ۣIշ,ͩ#/Kߗ-mBAS)Y=C`V>ӍUD \>X=;=]LEdeath307@ys$μљ|tuּ-!$ʼnV}ݎ(˸:U:Kw|>; Ɓqˢ3pLT\8&Ȯp3kc2i|_Dh%JG~d$['h^TqA]Dw(^Xgzbael.tC2 syH^E.2k>܌r?l03/goF働LFb|']}o9F?YWKJE_o0YZ j w4^,E!8)ć+Á{IxDOm]378\'Xhn%#ag&"g?<(ye(?ja+i5SClU8NYo`4IcrDU\4cb\4Y( ^&;gUM'-i(~m;1N>a6l8k;:m. O:|RԆF9E LJ SmZ+5G;aLF92.+`:xZ!)]ۣ5מ9ԍ7m5XG==Pr9B9>death308@|ٚ T'Šq}ǚ.ׇ$ۈ$ʳMYIϩ79 UxMHuBm?lsͺgCCH6q~%Sǟmi|=<\>k=]>y'X>O'ݛHIZ6u5^ })SP8cbAZ5[ Z."(U$p9aNI)c*q)*\x=X`<:I "I>&%m8El$K8h%M_3CBFm2C&JGSDDBOQF^?W`b'[grlx&AXF^lE 8#7 `<+$JB)Vq%s\2\qV6_g2dF΂U8jO3og/}A7jHQH:-V]_OT 20a&\ZMK3Y8K^,R}.hJeL0nim.[SM.CSZ-l.m+:m>6H:<ω)C {L-V F%V_eJe2V[ -X$=NOCYNQRWU&4]uPօLȅn0Shu'_lk Jf|[Wbc h=RbVb]<_߆W9/0F%QR \]1xQCGpImDž+Қ IJ/ȽʱYNд_Kط=ک0Ѣ ĶǨEOHի`Kx$d_'\^'ݝx?YUn EɒIL[HdVF,o&S,O#LIi"M=[!w-$3=Y4W'#/ǃ !x0},~*U,V`=[ls W.lswJNwQQC[7bzp m ywfq]\sL}zwxz} 1 M/!It/]ړ^?ޏ}FDaCM>?NBDlLB5_IdZo9Vd4UяFHz;T֑OIMP`%W*PsWQrXNKu("LQOL^XoVY QTM&PkN TPRHxQ]pW==MH>ījs=punch01ΣC锝$}޳Ζ[Žh}־{Ἲg*͹CĬse߆Ƥh2˸}%lsѰ|ҷ{ո8Ѽvaܰ,ږAGӵ,DQqz0>X+H((//|<wJw/MLTjCI p}OE&_3> g/'I-ˆ^~t*lVL-Ad*~.ڍc+<.)s\b?jFi4,̓O8c/#r$<Ē5AY&8U4iO3Έ9AӁ0`~)pƎ(Oo2pn>\Q9ЇPEZ*bsu6HGw5O>wNT׆&kF)X:wFST{1";lScFOh`=Dd8[ FMzIQ-c#yi,Y>euPw-=v w!u{4|B!oq'{0y{{qF/bIfoFo ;_hE?'8E"'%Ȃhcb`me0 *. ,2:nE]g)WF+<8$>f'8q2}-בywelfSxUJ9¤IKANiOc76Hq0..>-Ա@'OF< {nx8C;űAz,V.K* d I C*ӭN0 8C9X8ڽ 2ǘ3?7<IxϨ(;g %m%p)a!eݰ# M`o]Qվ"unե RgJGwD7DyD z#uEΣKyksNr;edXm̘Z?d]Gk nXSz`SԈVe؄lHn~PgpEdd8{EыS]sUVZߕb\rYgݚHteNnDm9q:v;y m_NKKp|KX]f_TIvNyJlWSߠ^zGZu5hLlPoJ\Sn)S0tl"UVkN I,/.H.pUdãIETF3cc?Ro`k7ymC>~@=VD>«ja~=punch02ӰIüʫ-ϸGTɓ]##̹䌽YɎIXyKPpq|z2ueXx^wՂsrcφTئ%Ǿ'Ļ'-teR!_ xC]Z5Kp(+O*yKfz ml {VV(W%|4tF b7+KJ̈́V"e3yWa/uD&]8h9\;;q2m4~d[miCÂH9h>R̀RMa;Hr6eÎE\]9]TMhOOЉEcED>pb9mcEqQQшSZr9w)xNv&|G}Tiׄ<"JB}RgT58"fXZLhz=XCg%d Nfw.h3gkbN5,C } xC e~wFI ?wXMM[^p-w ;D?B&DЖ1C *GڍLfigdua * (TɢU|bbX]CDA>.2(Mc(QoGY{Wurcn^ev``[գ/K*Wi3l7#Vy0;F/͹=,ϊ"۵&nCыCs)G49> ,?,3=A+'v:D8pFp 24-cy9I+|3ww %ݐ*yZ~>y%{װ,p$cwmo mڽ hwim Gw^q#{Dgvk aeycs zl#{cڢdrfu{Fts=hC>j'=punch03ֱKЯ̷-ֳMTɛ[$ӹ#й䉵] ʐ뙸KlUHnq͇z}2vaXǃ_vψuub |Y۪%Ŭ(έ&㟹%teN"] xL&yG_{ik{RT(T"|4u< ^7,GD̄Sc/zjT[/u=&Z5g2Y6:q-m+a[ka>9ʂD3h9K̀NJa5Br0^Î>YY2XPGiLIω?\=}<7l`8maD[NLЈOVq4u{+aKv)eCvPeׄ2$I!p?}MdT5#9!cWZIczCCBi'c Jaw0W/ghaI{9w)C| x9"{_wu=B 6rRFPOap0v ;Hk?D(9ʓ5B *<ԋCdha`ta! *#(LĢNzdbX_?H?>32(JbIKmGQzNssck^_u`YSң3K-Wi7k7%Vw"0 <F/״L'͌Ά{"oCЋCu)L3>= y'D$7=J. y<D8wKp 240e9IĦ+4w %}/a>s%uد,w!lrlMi eٽ"n}ml Iseg!~EovaX~}Dve{ zs#t\آ^sfrGux=maeiZ?ob~]GysӅ~ saY[\ԂXr؄aMmvOqzfdv^pʊndЇQ]pTS`c\ڡ]fݚQshNhDd7g:i:lpzeWVxFptJWSokXZHvWxUh\SޠRsEZp3hzDmJpI[Ln)N5v o"]_mឋ I,/3I.mTwZn7c7L4ZIENp`r6wk)>]=DC>?k=punch04ٷAMа%ްʛXǩq $պ㖳[/줴A rͶufbЊsзƦ/y{ȟ||Œَu]ۻ*͛KܙG䭳,xDF\z0RO6Hn((,C|4't?w/_>Gy>Uio}JP(P4{5 [5,A8ʉOc+jOS/y8&W0k*V0:t(s#_[la7-y<)i0=̀GC\+6r$QÒ3QY%ML=iF>Ή6P1q.*a`5paBZIDЈIOq+qrt/LGw-Q?vI]ׄ%qF$\;vC_R3%;aV\B[zO5Cb/a BYz:L+Z~h%_>rtDo)>z x({)z@r.4{'nG6VHeo9t ;S^BF'#8B #/'ȉ.׀`fa[rc( *(,;A{idYc?PxB=95,Cb&>mG?zN;qvcffUu`NCˣ<K5TiBh7-Vu(014 ȴB,H7ͅ{roCЋCx*T.H) m'J$?+̫O2 =C:Uo 249E<Ixʦ(8s %q%b=g$kۯ$ hlM_Yؽ"tn PgzV!HwPF}|Dse z$uNԢQvhrLr>ia_hPZ?pvXGh zmXRXXԂRm؄`KlsPq{ldvadʌt6Ї~R]p`VX]^ۡViݚOocNhDf8k:p:s h]NLuIpqKWXnb^QLvN{JnTVߠYpGZl5hvJlLpI[On)O5qi"UWgM I+/3H.lTcBeD?0\S9QpZo6smlW==hjD>«js=punch05ΡC钝$}ޱΓ[Žh }ֽ}Ἰg*ͷCĪseމƢh2˷}%lsѮ|ҷ{ն8Ѽvaܮ,ړAGӴ,DQqz0>X+H((//|<wJw/MLTjCI p}OE&_3> g/'I-ʆ^~t*lVL-Ad*~.ٍc+<.)s\b?jFi4,̃O8c/#r$<Ò5AY&8U4iO3͈9Aҁ0`~)pŎ(Oo2pn>\Q9χPEZ,`pueuTr*>v w!uv4{A"n'{0y{{mF/eGf8Fo ;daC?'>B"(/hcb`me0 *0,2:nE`e)]zC+B7$Ac&9mG2z-׏ywelfSuUJ9¢IKANiOc76Vq00.>-ԯ@(OF< {nx8C;ůCy,W/L* d K C*ӬN1 :C9ɞ[8ڽ 2ǖ3ǭ?F<IxϦ(X*=Ǝ>>40=^|Tf392eR twODgH%}0VIW@mig.6D+-l#pL8(^A# , z/hJZ-I>xS#x+WS$A q^-yp+hM+}[AM|?ۉl5v].u,ZF3?<_O>LCI|BWK l)$?U=eQB|CB#`**g+ WT~rA}>g]d(V^D'=fA!QcmPfyIxD{n@~X@gf{UwQ܄Ue|2S I.%a7:Fr$cO=&Z#(u#u&ptĺGd`a[ɍ(-ti`Ӑ[Ɨ*&2zwy M,?( ]xF"8'ki/e=G0KF {$0Cr*|n4if !h!yfq&\a_SZswn^{Ȏ\n8 ;Ji[{sOrHfkKFKnJq mk$zVxha||nigIgq?xff]pc+7Pu]iMqp ~\ۄJ؉J̌FӊW7YbL~>,L%f`TNf㖘LgMޡ ᛳK3.0 tl`薀_WES[t4T4kQ堊Dv<ߟo;Oc?dZ8wFf^n7'ԙ,}t|㕫  $,M%TY97BW3*_o-EE|??oo.>=2@>/?rMv|_{GہfDvt)t7ZF4R<_]>LA[|AiJ!%$>h=yVD|MD%u*+{) UemN|>hkd+WmD(=y>!QsmPvxIDrBa@hszUs]܄Tw4fI.8a8MF&x OT 'o(%(x}̹GnghɻbƓ+/{ncѐaÔ)I2&}y$M,A( aDŽE"9'qr/l=I0L7 ,2C{.s6io !n!e{(`dgZ^rvy^ǎbo8 ;KicsRwHnkMGKw@y mr$zWfq|vigJsv?xif`pc+7W~]sMyp eڄO؉IΌHԈR7Va=|?*=/b^VOl◔NgPޡ A350 uo}g}f|S6QXn9T5zV䘆Iv?ߟi;Mc_w=D>ER]^|뚕 uϸҚ\٬iywxb$Є>ıiF醞өm ܾx#Ɍi޵vv5se$IM _Zl T1 }kk~:Ȯ/3d?ω+M"džVKx#1$Ȅ?J:Bdɜ8XӐ<-Lf a b·ʌ́'EmxvfZ/9,τp.̀g5{?FHc,͋6aHGrvHCDsCL<$Pl OwH~CuBn@hzUlrQ;K3Ra?eF +Or+L%LնHuwܸNq=0uil׾5L1ǴIy,Q*D+ kۃG"<(|ij/zDYR,_oFHE{V4n[>T=nG>Ge@n0ڽpunch09C똝 ڵЙZ̫i}i,CƯhE牴ͨh2ֽ}$Ȋgڴ{{޼9wc,KO, Bkh O*&d}zzz_.*HLjm:d'҃4<>Y,x&/+Ռ}G!Pў,>ڎ029Dㄮ\bt͈|oǵ+eh hV{m$^^2o}.φf-фWF~OQÒ\)76C\}/υw=a>\ruDTEs?]Tc\DhkR=e6o jfChQf+c#a)n_VwU,L w:bv6J%Cn}Pbx{0wdmLwsO_6C ;gyB='-?G"'%/1aexee/ *0"'ITΐxFmf)^E+A:$Nk'KvE~>ÓxDyfgzUaLFK1ʳ>z,U*I+ r I A,α1;><͙W6 2Ș)Ƨ=F<Gʩ';g ${ޔ$d+m٢!rѳ"npo_uѯ RirXIzRH؁CD h%uU\lsOs>tufhn˟a=ܭ6]Mm rtiKӈzXׁHoPgzFmE9Fqjr=punch10ϣC锝$}޳Ζ[žh|־{ὺg*ιCĬs!D߉Ǥh2˸}%lgҰ{Ҹ{ո8ѽvcݰ,ږAOԵ,DRrz1=X+H((/.|=wK w/LLUjDH p}PE&`} 3? g/'J,ˆ^u*lVL-Ad*~-ڍd*<.)t\b?jGi5+̓O7c0"r$;Ē5?Y&7V3iO3Έ:?Ӂ0_~*oƎ(Np1pn=\Q8ЇPEZ*aru7HGw5O>wOS׆&kF*X:wFST{1";mS\GNha=Dd9[ FLhJQ-c#yi,Y>duPw-=u w!s{4|A!oq'{0x{{qF0bIfoFo ;_hE?'8E"'%ǂhcbamc0 *. ,2:nE^g)WF+<8$>f&8q2}-֑zwemfSxUJ8IKANiPc76Hp0.0>-Ա@(OF< znw8C;űAz,V.K* d I C*ӭN0 8Cj>:=crpunch1n޳ }k֣YֺsWҶ2oOݝ Yڮ wTmכY5۲Ԟhׇ._Ǐѣsƚ}ͤLՔrř ~-"t%]^&=R 1VQK$L5|)})\ ~J5(GQ ir4EI7#C2->-LB=Dž5![&# ÊUH3.G-!r'N/6v!&Մ+ As:9ʈ64h6Dτ6?[@}!TNQS>iRBÊBLщ;c/p\iBpgOXWDŊLS[ao* f|,]{\TЇ}`(/X$%W,R?I4ELm\]7xQ wlJ)?Z{r; Eq0<h;':huR =C9v{|31f4 },tN$OlLW^s1#V#RI Y5;`!g9%f'hK9O(8N /]Wgju\\yx^?Q%9B+),Llea\c?b,ZFP>K*E`K@8qGT'ZQ!w|wt_l}bfNT_f3]AKS id.7 _GPH#6?N/ý=ŧ'İ5-  VF< DU8Hm$߯_%U%;h`'ɱRնQ#GG~@tm:׶2|L;>3I'ڒ6a%]{#^(a#m $}JO\#ѥ݅^mr?i"Its;^-XvBqw@k'rAb`g΄RgFvH]vFRǜZ?ߡd|]Gh~ɇчt~kkՆnRlh^ЇrLpU[hd_eupldxn]]wYQreכO˟|QК̚}Pp~Rz>x3t5nbLod7]^\8,˝~/|ocpܙbxoۜ}  %ō*M/zV؏~FFDPgR'_xq5qqz@/>@W=>.crpunch2յ$ù ݽ!دħTعᷤYʼn¦I宼 ̩SjUoY}Τg/񘰟q锺xgࣰr WĽ پŶ-&W_ 0}-vhO*AK4a#W~X~:u%?V+AyGY~.xUZbRkWSRpDk=m|9b{Z^Ԋ}R_B[|.x ]IlR#UyY36%ul)<7z%'HgRh :81;?DOv|Gf L$u&|3 h%fd^~:r"?^2J;>;e7(lj-\R( f/|tO]H`#g* ^+=0Ph^EzmF=JZC/^I+1[J,]V'tQ#ZTQ|`>n`b 4Ȳ5WK/2i9B7x'fHK#d0ǃČ|ð? ܺ. fƅ q DzH7wƦKg{,Z%I+:Ny'O!C,ڧ(q>nJrV_Μ_82kԎ(0Al)|Ibҧ,4gg%M%r]qIP#[ֻ$_#QS`Os UX$|VhLu6xJ^ z3}+ubJXVbkf)w<Ѱ^ejsNqsI}[XWN\RvZ?oHeXkGgk8nyni݋cw܇a`maiԈ_`ixXrl^iycesfoYXpRSgݚhdudnܛ@yWpWifhEm7r9u;xacTiwUǠ`o|fdX}_|`rc_ߘaxRqxFhUnTpRXp=3z"fߕmtԞ0G()?O3mRt8D+Ug:;I4Wze >s=dK>.Rcrpunch3ޥ,ǹ ΜSؿڶ㻔W{ŕG믩 դSjTpȡYϭq.󘚠r얤a⦟r;Pˬ2%ɤ/)XS("s]O)7K*aS~O|0u$DV%o߄4"xQXbNhXLSn>k~8j|4_{SaӊuL\A7x+eHJ&c0Ĉ8ƀ˫> *emą} n !e{H7vƣKfy%^%P,:Kt'R!I,&vArJ~qJZ͘f82iӊ(6Cu3I_ң'7gq%H%{^z(K#Vص'f#YS[On N^#[hSr1}Ke z-v$naJTVlkn)w5ҩWfigRgxI}]UYNWRZ?dOoXuGqknvncۆ\zڇ\al[mӈ[`iy[qifgxcerfn^XxRSaۛfcu`k؛ÚrPgPuBtFv9x9y;}pYZVpxWŠeoxdbSxYwZm`[ژdyTqzGiWn]dX[^o+3 p"]ߕblמ1(*=G)vRy?D,Sg99F5Tz| >nC=>|3crpunch4(۪ ݎRѳąŸx˄H󵘅۟PͬoSiڙWjήҼw3󝇘áw윒}b寐t;x؜0%#ё.;]8'dDO5&K!YC| 8|&l$X J'0CEv |%;f> |&[#\VPR+e"?T;B?;H,d7w(w4JP(([/l dQ~}]G].]%)Q+)$BofJnh?I9YC8NK*2vSU)RVfQՂ`{`So_<``W*FMK='iK77o3\HA.Z/9ÇҧK2%Do‹ s eJ8~Kpt%^%R,;Pl'R!M$CI~yJ^i: 2sȄ+9C3Idɝ(7Z%K#^(Mܘ#Xѭw#i\TOmLn"\mbr1xKvz-m#ei}@YUk'w3ɟSqegRgH}gPYVQRZ?d]XGmˊxԊaنZw؆[\lYhӉ\Xp[[qyenoepy}enaY~TQ_xڛmZϟc^՚ʚmWaPA}4|5y.J|5Y{z}=S>>>f:crpunch5Kݡ2α׆WԻ 軁ɱ1kH󯖅ϛWӦoRhّXԩϽw4Zȝt噒|»e⫏wӒ0,#ɋ.S^'7U2VHK O7|*}$` pJ/)>T vid.HI1$C~3-7.L9?ȅ/"Q'# ŊLK3q0>.!d(E06h"w&ք& 9s3;̈06h0GЄ0B[C}WQTIAiIEŊ:P҉4f*v`]Ep\R[MHNJDV[dr{% d|&Z{RX҇e!1W%T{'UPBF,JK`_]2{H slB*j8^r5 Cq*>m4(3nuI"=C;v|-3f6{'zQ#RtPPTx+Z"JJ;O7;U"d7!k(o?=N'1Q/c\\oy\R~{^8S%2F*)'C}qeV_d?V.XFEBJ*;eNU1yLV#_Q{n}x__gfEY`Z-SDKJ!iY/7 c>RH88P/93;ʣI2Ѵ!C8;  ˺EU8Jo$ƹ^%ȸS,;\e'R!N$ѣDG~@iܗi:2Lԫ:>3Iq(6Z%T#^(Vɐ#`#{lNOsQ"Ë\mtr8p"Iz4d(]yzBe{@k&w8WagQgFvzKYeJRZ˼?ƩooXGhˊӇf ^p׆aUl]b҉dPpX[~mdzce|pqdnaY}TQdi؛zR͟nTњ͚oPcR>~3z5sTaB<Cskin1(䒔.騎c05ΧvVЖsTTF"B˃O~s$ ?||,[Ƕh+Sb_BzϦp/,EbmŌnhޠ7u8F~nl+NnwM#'F+-`s.#40U1S ɂSwE zA)Nm% ?{u }pZ7 dN j'jKy/,ƌp?a m[2/-z#3y<?"/lb&'tpczψm']ZsC΃6/u#e!όw؉@+ր%.˒#$Ƒ<:EE3ׅ5bm/us}/Liv3DirC-6u76Dw`/=h=7*3-wR5UE6oPIF7 4pK9F>,\w@sk7,m0 ke0zV)z;p>CxADr=>w):N{c~Y-LSd?O 2kK(A=){-ڬdFа Kb<q+FtHz ؜3ԗ? mQ!s# ÍkipLg88wEpw;zȞc=7]xMn cC1ӐD`؀TۇcWہWӌVaˀ4s9/Ǎ5-]^р@H>aXqH_ࠚ'hIC5<ˀ< jJ쐨QoPXh8l\KBw)l&^tLLXeLa:iRnaoX\_ꌨ*Qɘ.lK)&LN &'%*ըGҡ/UԭR??CN?CRRFf3֙78e=q>mBX>$r0uskin2&Ǎ,{`.ő3S;SQρQD@ŸLR m<ɍ~ YT1P+v@΋ >^fTFD3_1KPJ!ɐ%|C(!ʗ+8ʎ]+-Nō28R*TPVPHC;?WÞKPE Y'<|~ XET9;Jàeu7@S// F]͂RKH1?)B7Kɋ$3ʎ%-ʐ{R ~L҈fUoGiGPЀ8Vː7Lv)QǒPoW dW҄6\|+_)S`:dE\ԇ]ej[vsy[Lir]Dibbׄ,^u5]Ew][=mLI(]-wi6jE]phIF[ ]pf:F_WZtagr7,e[ kb[zj)w_p=bx?cq?=?<%?X=(?h=3?=%? =1?>&?=4?=> <><>h=>=>=>=>`<>! <>" <>x=> => =>=>=>=?= ?=> =?X=(?=%? =?<F?!=B?`<@?"h=3?`<7?<%?<>;J>(="> h=>=`?=B?X=M? 8=Y?#h=w?=u?>l?=_?=O?= =>=>=1?h=3?=??=4?;z? ;z? ;{?;z?;z?=> = ?X=?=?>?>>h=3?=B?=(="> =B>>>=>=.> =B>(="> >>=?=>=>=> >>=>=?=%? >&?=?=??=u?h=w?>?>l?=B?&<R?$<F?Dh=3?,=??>=R?:=>(<>' <>% <>C`<>D=>$=>)=>7h=>3<>'x=>.=>$=>1X=?5=?==>A=>)=>$<%?'X=(?+h=3?,`<7?%;{?';z?-;z?*;z?0;z?8;z?<h=w?;H=?5<^?1=`?2=u?B>?6H=?5=?*=?8=%?/>&?@=1?4=4?7>??>=`?2=l??=u?Bh=>3=B><(=">-<>'(=">-=B><=.>0=>)=>A=>: >>>=>7=?=X=?5>?6>>?=>Ah=>3=>7>>@=B><=?<>&?@=?8=u?B>l??>?6`<@?C`<7?%h=3?,<F?D ==I8=X=G==E==N">*>Mn> >S>^>T>Z>>z>a>>j>z>VB>>e*>>i==I==E<>F">*>M*>F>Hj>z>V*>>i#?*>F?^>H7?">I#?*>K"?>L?2>R?=J ?>Q?=P>=t?(=Z>=?=>=>=3?=G7?">I?=J.?H=O"?>L?=J7?">I 8=X=G==O==N>=Un> >Sj>=>>>=>=.?H=O?=J?=P?(=Zj>z>V">*>M>^>T>z>a==O&>=U>>h=j>=n>X=>=?2?X???\??W>?s?>[>>>>~>>>>{>>?>o>>u>>p?J>^?^>H#?*>K???\?2?X?F?`>??|>?s?J>^>>u?2>R#?*>K2>1?X*>?WR>>bB>>e>>*>>>>>>>>>>>>>Z>>z>>^>j>z>">*>*>F><> v>B?f>>;?Y~>1?X~>1?c>(?)>4?y> ?w>?>>*>> >>s?d>s?kF>l?gJ>g?n>g?mv>B?f>I?j>9?>K?>~?h>s?k>>s?d>>~?B>>e*>?W*>>l>>B?]>>;?Yv>B?fJ>g?n*>J?_ ?>i?>e?>o?>l>>{?>[>>~>>p?^>H ?>i?>o?>l??W?>[?f?q ?J?_ ?f?n?n?k?~?r?b?v ?J?_ ?>Q>>u>=t>n>>F>>>>n>>>>= ?>?=?=.?H=3?=?2>R>>u ?>Q>F?` ?J?_?b?v>W?>K?>??|> ?w2>1?XR>>b>>*~>s?x>>~?>>s?dF>l?g>l?>p?>~?~>~?>>~?>4?y>9?~>1?c>>B>>e>>>>>>> ?w>(?)2>1?Xv>B?f~>9?>~?>~?>p?>~?z>g?m>I?j>Z?>h?}>l?F>l?g>p?>l?>h?}>~?z>i?}>W??b?v>~?z>~?>~?z?b?v?~?r>?>9?>4?y&>X=>>h=>K?>9?>??|> ?>?s>>>>>>>>> ?>?>?????>F? ?J??b??f??~??n? ?~??}?>>>?s>>>>>Z>n> >S>>>>>>n> >>^>>Z>>>>>> ?> ?>>>>> ?>9?> ?>I?j>K?>Z?>=t>F>>=>>>=>=>=>=>>>n>>>u>>>>>>>> ?~??~?r?n?k?}?h>?8=>? >P?2>R?v?X=rf?=R?r?2>z{? >v?X=rb?.> ====8=X====h=>===&><>= ==<>==">*>==n> >>=j>=>>h=n>X=&><#?*>7?">?^>#?*>?J>?2>>> ?>3?=?=7?">"?>#?*>?2>?= ?>?2>"?>">*>>^>n> >????2???>??=??? ?J??D?R>>*>?2>1?> ?>>+B>>*>?v>B?~>>;?>>B?J>g?>g?>I?>9?~>z>*>>B>>>>>z> >>;?~>1?~>1?>(?&>4?> ?>?>>+>>>>F>l?>s?>>s?~>s?>l?>g?J>g?>s?>s?>~?>>s?>>~?~>s?~>~?>~?*>F>*>>j>z>*>>*>?B>>?>?> ?>>>>>>>>>?>?>>>?^>?J>>>>> ?>?^> ???>?>>>>>>>>>>>>?>> ?f? ?J??f??n?J>g?>>B?*>J?>????>>>j>=n> >>>>=n>X=~>9?>4?~>1?>>?>>>>>>>B>>>>+2>1?>(?&> ? ~>s?>p?>l?>h?>g?>Z?>I?>K?>9?>~?>~?>p?~>s?>p?>~?>~?>h??b?>F?>W?>i?>~?>~??~?>>>>>>>4?>9?>?>K?>??>9?> ?> ?>>>K?>W?>F?>??>F>>n>>=>=>>>? >>?8=P?2>R?<b?.>f?=v?X=v?X={? >r?2>b?.>?(=?=>=>=>=>=>>>>>>>=>=>=>=>=>=?=>=u?>c??f??r??~?>r?>^?> f?>c??a??m??f??K?$? ^?n>K?n>K?>5?n>5?>"?>!#?>?> #?? X?)?X?@? J?7?J?S?:?@?J?k?;?k?J?v?;?v?J??;??X?k? X?@? i?j? h?t?X?v? J?v?J?k?X?@? J?k?J?S?X?@? J?)?X?)?J?7?:?)?f??m??l? ?X?v? h?t?X??J??J?v?z?X?i?j? X?@? p?9?h?t?i?j? z?X?~?`?i?y?X??r?>^?n>^?> K?>K?>5?>-?>$#?>4??"2??#K??K??%f??K?$?K?>K??c??K?>4??"K??K??f??c??f?>^?> K?>c??p?9?X?@? X?)?:?)?J?7?:?@?$?8?!*?k?:?@?;?k?*?t??X? :?@?*?t?;?k?;?v?;??(?x??`??X? 2??#(? ?+??6??K?$?K??%?h??`?(?x?#??2??##?>$?8?!:?@??X? ~?`?~?h?i?y?>=? < ?`<?=+??6??(? ?>>h=&><>=>=?= ?`<? <l? ?a??m??K?>-?>$4??"doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/testmodel.pack/guard1_body.png0000664000175000017500000135121212641367670031511 0ustar jaakkojaakkoPNG  IHDR{C AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  $iTXtXML:com.adobe.xmp 1 5 72 1 72 512 1 512 2014-08-13T19:08:41 Pixelmator 3.2 X6@IDATxYmYz3ɓӽuvWuXԃd H%YHv#26Ba;L:<D@v` ? l#l˓$CU!sN['3Oޛw[Uͮ['^{ 7aw;֟iדCxVxJk~‹KR P 9_P ސ{F5qB$WϨOҟ8=n|CcM 26i)EMNdŶn_Z/@+%ſ./k{L˭u.\6.}Ue2z~F3+n yߣBޣ܈GSl#G-G<|c2C6fw587` '#tsLyv[v}vCHg9|Y׈2k8%aNv{WFa`=V+<<9]-׋`0vz٩Ozzyz0wރ<|W2=T{8\kSHkmkI+}ï _l7;읜<|x|C<[g |UB2,ҦS-ۼťŧ\mZVa}CF޺3tǻ;xl F8Nϗ*_̦^_^.z æNOf4XnyEMjx+% ۙo7fATµ{8Ns?`2snZ`{~t9$6B**yO^}' 2=F-J*h x沆۵m{LmYs5m YY%i~t>S?{>,|o2('zѫ`Gs8?~xɱ>q|,1k׾ik/' =S^4Og뒧;1s{&j>QNºkݯ3@d{PVTx(OU inَ^8~$_nH4czNn ۊJtkΑ~(%U5%^O7D@Ϳ^F1A s՝KHԐmF&,m_eywIٷcx,ݽ?- we: 0*@1wagB\ksw4NOϘlK9 ;rz?;W7f34Igg㱼}2ݩ/Yg,KH%=KMm_|#WvIl饽pOKEI!A]Am u闋C6*X8)J_U/ )tJxC_2=DٍˉKtP7.=ߣ< op{E00W E@ ܐuI 1fO">DA*4i~[=GѨ9xGu@0Z/y_ww:尔{l,Wں?7;fƽSlA7`aK8.9xsMȥ'ȳ+#2~qiul'[*X#dž%ԧ|Sϭ1ft)oUY b6wJm9 "E(mdc@:Ŏت1x Nc5?g$ZWp|~ ⨑xg.̘ZY7* ݸ~|^W ~p=?.R[7]h&gެ#"CMbQe0:b[m|z1\~>/̗Nj8pۡCe6>-^$2l v:YtBpPEGu](:A۪ е )R(3uϼii"%Nn((*l+99MKЉ_Rᙂu=R֮ZBSP-fNB\ jޭ}s:{D W I=BL,6 thoۆdjy${1p8)BD{Y_b.@5UʛLr%"jDZ/:]a ?U v 3*fF1hMΠu$8E'$غRL]vA^MFVb]OgYiQk.eD2K? BXb03bAuoXCc㳞Yt&\앸p~[^jwq]%=JƋ } =Z\\ }Hlvh:=C0FoK2 kĹY}0n,X5+JEtÝQ8P`8\0X1CbQYpݕg ߲ x%ڽTlAHo1:("I@(;<7$l;Ñg~b4SN,in'/>z=@T|c_(!YIz7g)UYi@b5 ֆ0NcrU XI 3eTlt_TrQ2Xؖh</kTs2c C,_Rd Rz"s:rp6cǒMq#泠)Sl&LfAяl=C}ÝJTE~UH+INfMjGVvG|;O Q% >ɗo]O"2+3%A@d\OlbW;0^5]?/-{ݳ`o4:de~ooEC~/L>~F@&o2Olb.!R8l{WV )rYf2ZUtwcNPhQJ $0HVwLU=Pmɖ\+zK0ƙɈv %3 "s \,$(@ 蛂MF6S@vveG'/ Gʞ=xXs%=p\\}"MQܟPuCCBHtyAon&v|n>S<50L]s UZ~Q%K޴HuTRaQ+/> `@@g`gݱ{?kp}/j3IA#s#:> +bNA yKC"[SYm[7+}o݆;;zni"]HRӷ^e\-#QDhgRK))`Rd NTG[HC%YvDsuZ׾%ż Lu oWރbaZTʻY>hdjD`6QF0f”~>lbl&X ^X&9 O[+ݝHg}&Uyp 렚KC5ZkQd(hŸ15LBacdp~ՠMa);==1|0޿ӳ7z&#WuFfe'ޠ.FdF F.C-ͨTs 0K)rQ؆gluXf_V {U7>)OO)-Cye߯έ~0!!xo܍6,nڂ 䝄c:[Y !Y/{#dAz0#a"EIڰ3/fqqhיl==. S}1 fSOugbT[BB'96Q(A D1.98ѬVW_=Co[/tXo<`MO|3g54E& KFVbt`EC,9}/c_y R;GʼnfU <ܸ$`cD`VPY\Z=J'pj4aQ0ŦDt+T6z,Z T lCKUKLk;uXކ7]Ss<3{% ٌ^|;_귲ɿHN=My-{~Pff 9vZ=@|x|:8zpBFr`Va" _QmЉvH H7Euc@8awWo=<8ki5;a~vr~>wl{=L Za8b8i#-5_Yg|Z^ǘoujQw\pw]PY#@|>ڛȈ]2rm'k;l!1[R >CẂf"iEoяbn;հvS]€撍j4VKAt;h<ǯF[E;];! T(Ŀ-YmzY)DPַ߀juV QF-'MRut]$cpv'OP|RvwxT%bJ,XXy>&&9Yh>BLx{w!a.a=xSDD;;Qen?`O`_iw?k?Ve(06lQVʳL rʄEg" \'@XYQ1Maxݫk문dk P{g>kEI ʐD >ξ{TX^+#VV&eZ Z- *lɦ!em3?1ÞcY?L jFH6%lwhw _?zYM/}5~z2\Ia?[t+h/DªMEՋdt˂(DB Rck@YX_E `Zgz^H95яAl1&T6Atb'[@B$d1uwvvXs+yT5^dHlH`{Oʚ΋DJ&FmZC|zTdu-*O2JRJQW; eh&y;P:micD3T !2Kcmi sʼn^}BqϾK٩fBFE`Q `J|3* L7A zF2iY&ZBQ# |02JϺ2f'e|z%xvO};a%Hgw zk/ٷYI]vdJ̅=r '^C?{U !A"Q"n C3r#MbkaH(nGZt.83ra2) @Kh"k:VWj&)eSy$$ ?x/|78|疽4J:}rb EpmKTdA,+5Kɲac4ӵ]aZkp-{* x}u`>XW֨ϮR;p` ۇp/iV̘ƒ>Gx ")+XH IP$ưg4!TBh>رE}c^%;Rb1>ߧ7Yvg YN@ۦl2bWk=,P#yF5([EL[?OT3/dU\XDVf pG~*+bVdU$PaZϪ,s(cu TY@Y4'M`9UYcFǐ bkT蔨[(ib91iKM(.9l9&sLEʆ 2v"TF=Ky I=ytѕl>M|O|^Spc 50P|Zc@M逌 |ϛ Xplht4XjlsEo ԛu,lلqoM gcNZҌ‹Q_@~K328mE!05s=dC6*R A GJ+9#D^$8?MWz_YYϮ%;1ߜ ;q9̜hs)*Lb}TãCDӔ7Yg LY5L([FJ--u@pHyE21vhFӀum!pMNehx 2AT `ܚ|^0Q)= E h&oU5K;xP?}#ܜKN ooV 71UQ`0!ˡ7MuBB;9 ,ghXQi>&] ޣ [}(*Cb V4'+RV[h{11Y fDwj"M>rc2ԦH zNPܩA`z̪F";Y|Pibְ_ v`Fʟ/#L1by:udٵ:, zT}&Gmbq28%ɪHbAoP"(/Q؉qCp;XK?Kmސ|}_^u-C+}1&bS7:077"8&r1ri+yqFaNh ؒp!ez8Z[A)}rAGKIտȨce$̙]fo;ߘl6,j54TU("'ț-$۽z*on.yE~IF)M55rBWrrW950cc$"+K&gAqXw0 ֬mb$ш`ϱG3$\la*tKd̟|N+:e`>m|#)N"sCߩ0*$rOG]u2ɑ:!,I?#m+@| e/Lˋ޲E bcj=8~8ήS WG~>éֲm#c4#%WXS%lfgKxCY 8IZNDs4\Zy0;FҘe|c$L%ռ[ -QD=>>?Q]aWc|ccvspx)!Ew Fh%a٬ch;O>O>JHb' /?06Z * 0E0'L3B:QM)!g \ NC b;]  Z` "q1˰tDc\$!A$_@{&ba&tq[h,~mfZd ;槧g(u#IJaf&ҌݗopZ߳7+aݘ'hFByMYɣIaNC.e#m'~~zBdyNF \g<]HK[|j3=%Q"JfjLX 9h%'+cOvGT KCH L* Ҹ [WeP]dP0iq}65L$nҒ FPlh i" nKF.ᳰgCކ"kT~ժA]v[27ƀL2+d#$W\h +ˣj77ira-uM/ѾXpLٹ2FِTjHt /* D,q*Ϲl|Ǧuv|^^yADw[I0lTJ;YnT-I2%zƳOpUR1J+HZ URRW+ތx⃥O0h/S̩=TDס19,"IF]zdM&PR"U+!3v}u312:1E,*!-5}vM})cC~Ռ\;?/|]M`.8!ImkzSE^ nsF(Pn0>Qܿ=<|&A,&Uʭu:-0"ApY3lpEt d]ǎq_1pۧBuX_w"tH ;A౨Jɉ?)t +n%.MP~qs ,O0(pqQo9BjeAc{J *򍷇}.(LpޡrItf(*- +1"nK2\iK gKg>߻5 l!S_???z`S[lap(AB[Flp"6 #r.3'D&\)n'? &J]/:0 IuY! ?I6{-{ 8_7O{hS"b\#Ƣ(^j"f8aYJ ^8X4Gbbt*Px!/AXJ` z>ܞ Qc)Qo5U?uIwo 3ˊ=i ZD{j9݃Igzo\M G>?3?[0=yXQn5 V$O)zZVR1C**wgPݓ.[d2%t'ܢPUXg3Fnht,\a Ip4|7ooF:}ls,׉xARK#> Jh@wga.YA$e5TM 93]$ SPP0KTE#&b*u+/S7)Da/(4I؉ ~ Q !H1>X:r?ݏca a+E ,$Tm7 ݙC EՕhzd J^_BֵܾuO1\t ^= 1J,]-=b]<K321P0RN0j U._LiX+#`_=Y*%N#aP$": > @|BBOv=Y*nc*mļ Kucu!X`4Y0ϨCM1D֜eƼ<`*sAbVBo[K IuSb}GY^ F6w9*_.NONXu'>~@-[ j<Žp)},lK+d3k1=|X!3w~W>xf=Fؿ 01fʷd23nԷjX,!xk, `o!U:b%6Etd(r0OGrM7@8%ܽ}?3o߿Y M 1EH)ocvq]71rI["K0G#K"<[Y#gKfWQQ*!hFU/#O[e0Y:8/ЙRIROs-jzCBmKRMw"QBotR3"(eEm;!q0u {J]E"fʔT[F tPz=OV5yy5RKWZI{}\bCtr'G򘋶yGI~45o'y KєV6K7#:O>(JTg8+x4 Wl[G{b3XlD}HYBB6JZi^r! ~ bfj 6KcbvˈS2#'B K|0v@A_^:5K0 lmsЮ `˘jJiqr> bud@O}_{oׯ֣Ro{ H5an'1 G&툣ڝAĐփIԳ713 |YDծ, ȌFR30[tPNgsܿAbvF  )͸y)bM(Y̌_BΤQaRQHlRLX{bVqPzvez{te7P97?"fd=UiT0 >:7eUs\m)1(rϼu<*HeUdIV`! 3\HD l1i#D]}#}69f'0(ũ6E|V( 5fdC 5{3#r ZQcO#b#0%8/Ae9OЅeiTEALSXL>g6~ag9Xo~ōdΥ{Q<^],s,,:p{֤_4%(hi2VNLpe@F;z^ *&n@ ĉE`޺ޙ H{%L> ]q :͘ojr4U,*^蒂\W#뒹U}Iw }. jٚ4lfDǸȼd5 7SyZTAzՀWAiH=Žb^UD "Oq,gqzɭ9,n:GMW Լu/HD-diŨkdDƺ c%BEs3H*dj-q~FCwlF ™'d;BpI>,,0Π ÓU&.-жO<˨;|yfN,'pzeuF䟓"xONC5.׶#65g{V9-2w>qhS'OJ{6T[PӾͥqg໨u"}G{^zzdmgs[䓽2Q B9D%#8k)4v "Y_"Ԏ9s0nGX+A3fd˒EUj? PÑfkTzQpS~alf~o`۪;N>M$+TlF)9"+eJ!c#J*q`;9 )?!Hĺ̦P /ǎB\,o dysbnjD(T*V4 H#W*\^?U_'D>q2gE'fWV( SQB|#RL)T+? m) ?l_(K4Hݺs[WDǢf ez<Ÿ)7@IDATi!dvMD`QJ"h\`?YJGnyL E),iYH!T؅Ea79qZ5>;!>}8; 18̮ 2-%E. c-|_'2`OhR `` %KRǐV rl:S=R`2_qFV&N*>kGF/0h#Ȅ V]u+膌&N,u n{ uB} I7865\Qf J9j4\dq{+!DRMh-O4kM%nT5hEoĬnEJ>6 U[XkSmsy'FІ >Yp*)nq/ˉlza  8.q,XFN[7lK@Mm8RoRY)KfFІ}!pw$&Z/$K:(*QiY5ǝo?fTV+]Ab E(0HGk'>P IWŃϭxDR[YO*;029![9ƣ;BInqY3ͳ3rh'RmC8xw?!=P3BB$ی(&k=ZUYAc;$Zy } wdk#Ӭ7\x Qm4r. Zn ~~vϭ8z+#2d%RLO4` /[Z"164N,)I |: fl'ROD6ˆMʳD1Z nM$+M5w7 lތKڎ')+)p$[yt^DZBi:,=<)$drm1(qfur0[;& |&ň'3SC*rЫ>lPH jTZ\։Е' vR4} ߟdxS »ԦKaSʇlcu> x2&sH;?cX-ʟ}ZS@ 1׹4(rG7<,pm#2>w>[Bqk?Sv낅\v4\Ke-: _#nXO۴}yd%}/+4#>'a"CAf~Y*(oop($'$g2g\"hƓx[S9 ݾKM~˟'?W%1J  3 tI/rӉW0ڞa"I,aЉe>E& *J&fz2=Q~& _Wkk(NԂ,Fx|iUτ T{9t}6]:md2 | [_ġoXg]jn]ֺcţ{`7)MTUٲ,%2 ikD$$+.DJCFCCC(@MΑU61<~f!Y;JIAfF,!LI#ߪGMvF`K HȈ3d$gJf?G${H(\up˜>n >7C~1޷`jEtnv 7}8QG+}5tE7XoS:/t[g>ʚ'YgwX7|޽{`i3m^3X')|Y>i?^QR8H"Ȕ @,  ,ʬ )!T`k;Cиd0Ę1ә{fV!| 7!Q۸`E'(_!@ZȵN߲F ,.ƖBYEQ/eMNn!rWm=ˢNl+&RZ `uBO9љm jlNsSZHmca \K a?z2KZZUN{4>DHQi⼡4A >3P@abdΆj Ms0tߚ") ,=!5* xk P| ,IȄ5JоZ?ţh.Рc (dDfVy7[->M)]ǠaS!t> 4ޜE&Zذ+Yg%(o9ތ+K@"`˄ʓVU؅#I! eghV*c!::.V$75O62;Qvω9'i]c/˟@f f*SCN* Dd<:3o_ʒڭcy+"'gځFr%`"yϩ81ᛅRnH[H [IwL`֡$ Qnz ,C9{m(jH0ʸۊ2vȫļ.?`4Wz{Mt2lœ'tY*y%*l(QSl)3 ݌9P3O1E%4&=hp홳/\N T>1Hʭ˱rjXWv2!sv0⎁у ZKW/ V5s ţۼI;~Gˆ,/軷.0{xzйOG~"P, ,IY8b!VݻgjƉDfCЪ)\.Bp_o_sf/퐐?a f>0n|C8ꍭ|`mW8=5v'_~RtJ nQc$O }Iܕ}Jw1.'Hi]3LyI QLlTͶ:dnhCD%wyEՖۓCx'3վ۵T3K(b_ҟ7W27CfA60:l h3[͆J"7}8y~8wNUn7g<*C mT dS5vR{MZdfI, I/#q9X`P;5Qq/VQQ3^i-eI?VjQ[ j`B ]0i"d챯V _@:F ʐy0-CNyRLzt|jal4q.vmTBDJ ^jKm)ERY¤K6tXPwLS{tz $ tʶƥ!u +t fo=<Z H":~gŐ3u !iTfpۦ1z5+}ՠ_,ZJU!Z?G(R5@eO^vKBDT4(:?vλSk.bA:;$Iu gRhs/[ثa8w9 {U~t8hW꺳x1q9Z: ٶT" |-FL_1^$Oh#[5 [kb-U1< pgJnaӝ$љp LT Kw6BYwĵfp-L:=XZV29"sl0[};@ nAQ. 1Q䥣[KGe܃{qۨODSHZ,`قCuAfļv[%, \,"Zv@a ''> sX ":] ]֔$CZK} 6/[ߢ˧nR@\b:DB ܈Am ƶ3_NO- p&LQLՒR2F|o#յ=a9#*i)W9`Q D.R/20JZRZ֭߭J}գ*,܆ ^s`d7rfoN}t4cI#LqGs1̑gG1[Oݺcpz8ƅQWͻ֗`A6.Ps3Ҝ+!v>ͺGlNL8I~}{$3"4ˆѓ׎ҿun/lIԢ=pnj&֝/ _u*di&^i[dOH$_Z  `b'k^,oEk(M 8IY1 @Gtcor:JT*:sqPÎG^.2*cސ}:?'-v.‚,N÷r+BC<(ݠ'*{q 2ݲAsa 4=)ǁPs:vEYbGvD_}EZ,Ń7Zo p_Ny1`~mnbW)O(K$2 Ek+U͙}O[ݭCg䅩$'C|"vTDqsc3&__fH=,Vgb$AdY )0XCٳ9{#F;68ZrSF:ۇߴ ~H2>PoȈZGt +u6VlXAӻѣP"2ވ=rdʲ/.ll`C2]'  7%Ҳ{PmwيCxY4[+gfV+[Q<$pU%Du:F^5#YFiu$qfXlXJ:oge!>ذWCUoR;nt@~0PilmJ)H|Ed,6+܆(VxLX .2\Vez>.TՌ388%VwxJ6(6t_~nLz'+W  X~'+9.-Y<ŻN`Ԓm9|.lw63]o%LsǸxf91$^,N+ bն"0Kl]ʉ9&h;V .+Jj-J;vz팧! IY]~_wEʸ<D ZB5$qLOwTy z@};]֋B|DO`;6CM̆Q_g>N+c0'㱯p$GBXYpd2G##$|$hv(9cL )ܽ "'.7l=G M߀!<Ogk?Gہ2jPR*R5<)#@L%1,xuO)j1BXx:gh_"a| >u耡vo$(Y2j3gfr,|6`9p30DEl֡z!~7[ӓSzEc'[seL42)(8sa,H@( JȇL#!*|'@U. $),R0.gN|_ҁ[kB8sSxSy9z3صֈ"J\@LPod0Z*5`լ`[ [A^#~,(GK1ı#ݘa^A&?H!Z0swv=Ȱ ݱ8tT[e.(h|{J8D'ظ3ʴ>:;sv`S*:SlKY>Ҿ!n03Ķ_?E}ݓbq- vP {#U, #N$&گv?ᝂ eV%R{7=uhG7}ή#3[t!}xfNѷJ >gć;RƙpnP?@"#tcf|{@DCf%cdLd5&9F p?Y`~x?e-h!j6dO.^`R1?S5D&r^CEhI&dpܻ !-R꒱mV ̑fi8#abX${a\\5frCC %/GlBJ6QKuQYyuOh{:)ya '3[bשi5\G$UW[R#e,1{9[y@Fէ*@2IS{SGƔj vbJ8=Ο#"'ՋV[Ƞ-B^fEH71+7eKg AºÇg]>~"h_vx5AmjDcM;hX̭'{qWL NE~Zu~Yu~22U\*$mT׆}D64 #3j{ Mx/.yyS=lC:xV;M^xޤo+yGDVEgXGGwB^`4˾vk#e[5y^'߼w<%ǕU.aUAi¬u6" ,W=W nP9.`F&qp#S S?g^Y_S䎱E|Lq4m;F"[6`ٜ.̩NL 3}b"ʍN}W%K5dˢ+=Oks8K\x>3%Pk ^"Bލ8:!8Bֶw+2qH> pgY!Kfb̜3Ef,]%jϼ)/#qUo#!<Ej_PYba~w ӍXFھ7tft+J1KH1a{DnC3|f<gT+/$c1`XF." ՗zMe&q yT _[jݙ8$hJHeeյ3vkĦ->Wje,*IS!I H7j+QFF!rzkPj afe/1%)Kb^($ IYȐ#)M_$XܶF lP&?TD١6ʩl  =x_cb4pn2YYZ-Dr"yjy8 #C  h"O _'Lb2  oxsc|)zԱy;Rn3Jj\bPg\=69wlHN&62k %Yۗ vU%U^dx,## ?m]j&2$ M^pn[ײ|'Fq; $)5E'dYRlFܻo ፐm܀9Ex 8Ūf>Oky =F{t-o 3!o~%1j\ծQMO\}+MW˽zɾ r BQx '_CW/gugKV[U:1Ŗ́$& p4nRyIfe[s0S4<%pI*@mIHq{Kz҂^MsG`yOj0< wd[uwu=&2QEQ[13#Dau&zbnIb-Xr?s(T"%</_ws~=75.aY)JiXL[0*9hӦC@AIwk*ܩ4]_^8CjZ9d6DfoY-$'xjXٱE5D&yn(:DF%"} TZ7o28Kg^^g~/F֓%1Jya0:FT XRSL6S250L5o%6r(og ٘eC6!N  u8d@qǣ,3kd r>zƒı$ii&>r|ˉH(/ ʰ"F\ٓcdpeH{n >RGF28@@L"q>GLj%L"2̴ƤT@9&n4-۹r"[-7Oz4|ʧ&}Um @nPՔkO4Jyg&֥XGFτ4̉Y6Z.\ÏCbW_bjpp9eJ'c12F<T֞YxmA*Io#BW΄/ VӾ*$qSZՐ(1U^FMWkد=p:x/Wv f`'dcoI 7p%+gg/PED{ֲp.*lzIWd|Oi;vID"Vwh]_n梋(OOo cJ^8fgǻ'gJ/ ߪѷOo#-CSHĩ,Hp8xB]Ir Z"[JVFdҊ4 s>1I;xQ//|b y{Āq[oÍRyMAz1owkhWO0bi/bˋ.50)0hWI 6ӎEboJ &)Uo2LVdE=iiHO:9yTf{"JEtd ĿIj9 yÜV&L>~rij,:K/0D Qty4@dL4[M!c$D9iu?  L,6ضgbBnm@Wo!K ۀhoh|qr>uHj!9p@Fɱ ;b7M"T6gk)&$m I4j1\'#w_mkLɦ@呐'H'g 0?XM{oc6+9t׬SF7rlDA|c$5:La2YT Ns0l?Aٟ"$=@ PZM*l, ULnGaA %LSN͆͘2 h8٨*͵v/?XЛ"ɿu]E$_8 vD gt@(h)<+waD[x^&|C^V1bZ)()Ux3?&⏧~UI{QyVAYaqlb>LqUTWV&:IhxAy~ kKD\pwb؁U7)tG&m: F#gtYŔk3 :0-\[+ӣ_ u1ԖRFF\S_[;#4yLYϮEjwPh@K{q0NH3/$U_Y[g_>pg[ϊb~򷛡l|1?η2h7탷SYmaG]1nov|cЍLC섺'1G¡C pc٦[QU|Zθ w)zlͭ-CmwVlv J`@Էۧ TH$6ַ%\d÷@!*&jQp%/O;/d<CazlϿtJDvvh 5oNTTs~,.#FP<%X|EZ̅D8RJ=ŷ4K+ |[%3\ +McY&MdJX[̛bCsE>/O-U9 ؃Z0m3.rú cϟ==?;*8UFeV l>bi%KYTl#>}Ah&NȡU{V˽թl< j*L1Rb@`1#\fq?ɓFDH ֯8K[|ix5YecWgjֻIN؆[L=O/1 p(2~<؎ 峹x7Jqɧd4U^? :;ԉk')$HXPWCIs"{nφ/A,?䐴Xp]mW}xq54 Nw{HnWo}sB5e@k>|/!hޚ G%)̀|<=bcrձ(Pn; mǓsnUCl9qc-& [[8HҮhb1C6b!̘CnSDmko'|q@ug_u0dG.tǣC*.h% ky@+W~WWWۅ#"1=_Hjivټ޼^MV;Ō=˨D0DW7 BMSA~rD7 k'hl+AVV3Bo~ڌi:<9D0pw$]2 4H,YzRZN`ť-܇;6ẑQ@"*Und[SɞJ5QKV^ SN^Œ!Qp,6 KƄ)3a9JDӏ0Y[\SL'i[l?^W%3Y $JvX$DjsyKWWMM,F< .qdaI#O`DW ~e@IDAT,;;ALh> sGRCK>)kSmlT~Oqxv~{}'Oe9ʁ3NE@o?-P G,tJ,F#v۶˄PxbVKdf>XZQn_ƽnQfAƲY*1$LW],}iju|x!HxKMU1dHpuC*c3eAܹ2,l"|,u6yQy7I08=~{;m_KҀUcb ʻ&Ҟj|KdĎA%Z̏ęo/Ƶp5/9PU'eMh'moEPn9WIH W` LS!.W^`@ҍlK-&br }@gh-M+[m506~!7 6S~΁?¡`  2w-aZ>6A5 <3B":]#Z;=j|{?x[{kHfl=b:Z4U+<T Z\\=:?t?9>^}gχȸazkB&X=gwyr|%;w g.S@6u8`Tq:ֆqc'yʒW~s>Ӝ6 K >:w憌,K ?1% O/~nҴq~, O`Q܇>$Ў$s\> (P UOI$>%ߒԳHW,ۄe{vzK4F[\X[(F56*k :p {H +$w_XB C˄SA_7 JSP5fZ)!ˉcZ湰5`oé.`Gȋ%Ͻ}{۹Vw9to6G_uF)_jW8?ey@ޜEQ:BȓiѼW4ь0!*  ̉[,mekbiVt8ii5$Ul`ٺߧLҼ"|i֧lֿَuf6QAHHWCKWXGab֭EJzBT=`<pDVlK2*z:%Į𮘭{g/^0G%*~!SYLU,fݷ ;w2OG@wy>/JNsӆӾqkoC6!L;Zpٳo qppF1w#`N-AեZKqSsByBL  EopAh)1+jwH1-f=ɕ_:%Yx[6a96  *$Wq|u"wόt!Kq gWlAOЅ 2-'tef'|Аb=m 7[58s=Dk&JJv}m#CN{9[B 3nu[BѨA(_'MӉ옒d*_1,[^Osv=w{>}f.T u5M8I/|Q"z9bXF≱k67?gas;{g'Fw;l1*3,&N⪸`jM|,?L_;=&'.,bH=v '!n,2z[J!JF6p'8[dcEfad8Wx&Ngb2u3;l EV _O 2\ 0˯`=22/j|r[w՛n G=L|H?o}~mLNipn/gbCȰ|%'=_s구GiŻmB-:D+ј/SG_b׹.,xjty }A%hെƧPtϓvA 卋 )ee^ϻ:3pBI)QW/[@1/8pbhE|bM AG[y"RUWbZI|O8)+rOJT?)_ciG d^@S̛tI1r,+BK%1[Qܕp>՛;<{3=d !]3- ^w:`mvLQf6o-P&Qmq_u߭ [5d znSRQg./8T[>0 d9');Y6ZX#yOZ˻/[u/n3u)e9=RvM<=Ю"ˋ#u3C 쟜mV`KF|hx1i{{{`+B9_6}K Kw+o#BUQ#vJ5{`so9["}v jiM=`Ą{EcQPAA|(J;RQ#(o'КU (U4pϮ, H>@?pGCC7ǔ䙍i^pQC^ l> }17@=?Ѧ)5.Ss_7s s-UڔQ^=adS{ K?[a]pJm>QC7\Ȑ[:0 ˶IVVdt/'~?W׍AoUZU+S<C}2"10'f%N# *^odhKG- kvbDMw~AOG_sGH,^ D:P ^]L{|ePCAPjujc"(+\Lm9!FR NG\I!DD#c R\Q i :H2#?_VW<fKjkQm2AHao czt|#ɅN\fC']>/fQ[0q NoǗC.o BB =vlS;$E` Ri N$BļlZߛct.mRv)^~_ r/IkVY+иݼ2Wɼhv)d+AxYqN ,'d 'Y!3icP,x%dv$4d"C<"hcMH _@|qnooggBP-\ *:GA[>n%0n9Ӎb>ۥ #Pbpwag. iɉ2G14F!/k*34(oU.SOJ ۆI@rfPAִ3rv=oVҦW߈LЈ8=.|>͓@]WzMtݮe!;[qA(bCٓQWNt_h:M-KcbrhB}tyCK\,fr^m\ktckh`>"Zl0@]3D+ˤǡ5?ڞPz}b-[56!<0LЏFt֊Dp_647$[[1_[> k>߹ ;t2bm\mJ'`\z%*{4P5k< NoxwH֫}ܛxt:vvr:F񚨇6IyacH&Ƴ U/Π2KDe #ߐ"6]\R QDX˺7~Rd=l81d6ȶ%&ľ ll ~ l*K\^kTjٸ@#@^vlbc1TF PrQ74DKVOmpFm6~&` x)nxr!E-+{N#FϵwD+pA_u\b|~5+r~!U7 +*n_Ջ&*pSn=JY%>2MMW)WpW^B>]9U!j r{M +U?).W"1s *B%Ghj8ݹAJ)< &m6KƢXyEJ&FQ+$oRb$tUCߌN56W@̭)γ@<.+%qަ4mɏcWPJ;>GIkm|F"k)t:N#VRv$MM8ovᮭOf@Hivས0OG4>ݨSEI4W1.ϫ٥ʚ&f]qxr@Z1(=I [m ~ ⻺^ihV/W-}D"00;N-2?O_qvIhcdx̾(X/kO7J@D|Y?Z;CYTI+z2;ehgwzy3<c.懽S:c7Y[b]w,-`$Zw-{}Y=3p})UnR܇ ?KV蹝U[1Ȓ BeS< (,=#-n&ݏ 3Nۮy{QkL%%uµIhw4KصMĨrp| !^|cO+pxYy!d o& [ICd0FYc%׋)+p0sDRl<\t6ZL#W_t{k;oCm{ʷ5Undɡ?f)4R 2K0i{!wInϓ}ҁķz$^T#F_\~ ]bLgY$##-66TMg}kՈInZ6W,< IUhE߂g5a@) ("aM< `&D.y$G/d ;7Q0nl1D zl8^?:!0{ntxo٧[[}ʼn+Kw@HnAp`F dw=^Lu\K~ t /2C ^*Ub,/0xRYި}g)"VJ-jYXG,iJ9:b͛yr^swL%e օTec**e!r U`KYLf\ sp"L~ "[jp6f.g]GwȞ.^c4䈗g]eKST.}U^LFO3Wg~8U:力1^HTFgZ[0#J\!WR*GڽgcLoKd)T9M<֦ͩ9 gKaI)/|| #ӳ ^Ӌ}rTO\d5U,Z}uMDTg `Ul PT@ʵ33]/¨#|]]EZFT6?XuwO]6Mf=L:D k޲hDQ5J3]ƢbԫjHnk @as|ttC$D'kݛ`Tb+j[~i$ u>Ɇ# )ivS öҒժiide{FD+pc<9Uy૤Nf@LbtTRFx+ ճz1ֆ_ GTVʗi5'¬`tK\J?$5pZh7Wۨeg.?GSծ#!LśId^,V@NHDe[u `7> JbgD @Z Q&ã -zc`\ۊ=99ӓ8{\_[Sp|SI&yx< qU$+E(DzjGH97h嚐xAC LAi牥\o\-&zJ=8<{~lM-{Ҿ,NZ2;pfgX'ui vw]'ͻl/fyH"-Ab.91_4 \\" %o2Da첷'yfš "I|"aTЭk#NqxPo&2+ " 'avg!,ANj]{b"`/d,_& GT%dvKN_4l0.d"~-TYAQ>3*<'MίGEUXt jў5>T3a-FzO)G (u!Q׍E2OM; XґptyY<;4U;(W, :bm^0] l+z}-͖jVp??:2`z/~<VFi dsFP YT,|v$-'I&ucв[H/ jBMX@Tb4ًO4z)cFd,:ۤւ(<3w"1*R\Ȩ$Y1%t0+q#~cklw3o ޥ!9,\85xX>~]{̷1 JYCIJ.z/Ά\ٰT *;(Ct9"QVV3 TՊuĭ3 Tu6ca}`кhwKFpY ;//m{"4Rc -J.+po$y5 N fYtFŔqoZMOwxL[RZx6%h(+=MWmA99Ar}M0#3cO/f(}1a~t>ʳ[b]l󙸂ӧ[x|Oo8;U>^_}EXzH`Y9j1muWh3\91_B5I$غfrR?*m͜1Pܚ},,5ZZfmGE_(7] @>2_xTJC@OpFUc}f"?HJ9An\}rE!iPY, mG/,Rw>R*eQ~+m\R(+zeV".[1|/giY7P=zE%v!HkNEG=U@!$Jt@4-8m,n >yr6# b_-cA8 µ >nC0Θƌ˪j.[Ϝ,}xpΥA7z jҚ<V Xb֕yۧB9T˰ZQtJ:6պP= ,Prabm6G]oeV9m["0 5aZdmf$e9 j@NϗW/Y^ Iο|vxݻ!s'֡u=ij?,~VQ}fcJѵ$ KÔ6 FǓ0T\ n'q? iX%< ʽC-1E()Uc1(3A/RK ,* C:> )tT-ݵ%@s§cSB{ %LG:kQĕ_977w27!bdU%bCUGB2:9&7%"`5Xb;4Jغuudr \, 36(b;2)gkj #?K>kӔ-RhL e쓴 ] ߱Ui+: JȜs`IyevmJO14T`AsSl`Ndd,RJ-Tz%5EӑopPc/$ C|8\_n=:[I}v+0.'C"N*G̎\UZ-ϜW`Lc 1zx0+c>dwh0:}@t4YIDQ3WSVc+c8ۇ'^]a (?K4vLNV7X+4_V Pe4ϚrŌ,LK8&z%+[s}nDw&/t}κ7~g3,sv۶h?e2K@d/lpl'W@Fj L˄ޓ>4lͿo\CݗLS"%x%I,G'Ň;6{" DL]pXҬQa#MQOT J{Y-H($<{I'$ p-@ h/Cd:+t2YmG:x=qyt#?xS8=K2ez a5OYI,ptWmtkT/Tt+ Y,`bX:E۞ox_3dʂuO'OQmM7b+{s͐"gel9@Fм:Dc1}KIS>ru'SU:)8^^ P.6d^G 3cch;{G} N^wvWA췎'3FQ8E"~`⛁U4"jj9j?ft|xmiV KTXˈ ۇ>e eE1g!sĄ&yxpc@Tf]3~xڿ!ܽuWnt_̓J39<0'"D< 8?GEX:(qr M B2#4E4Ӥ+lbYRm2\EGGcޚ2[xB7QzrR^ ^h!zI, U]Z|ExO(/.l*w[\+qq ?P`2o ^p l:$>?sU;*L&fAANh?Z5>@?$S6ѹnL\LR>=CuIcJ8SbS8X3!l!1͕@#""3Og zo=.qCAg#xcۡikXaox*XB95?0Ts<s"l}`_fgx{6(2$?|ڳzǑm@zwTDͤ,: w4ee$ą{"&!UՊi"SCKyi&.zr9RP@U^Z%=L 3*|xJE㷢)VOT-ԪN?Wvܻ7kD> ^[Dw?ǒv,d::T^}lK*z.B 欽O3s^_S64G`MVR(^֫6)yLJzQdF\Ĺɰ2 NCJXr LPCLޖ_mjQd[F.04$01|E 8Yzaw_<%[ʱ ]1DDH GZpk1˱DFM YHț/~{' %\u; Z!iM [b>b~UT,hjƼj8<>j9eވ3MͫuͯV(wyφg!}FX kw#uS2y[7=]QMQbbC:a>d1nAtVΎ, 1fhyFTvѽǟ}A~6҄|8 탍S+zZ |;==r8\,K+,]q,2t@L./r7頝MVv֦'ft?q92@W!>K,WZFA+|'$ N洢aѠ ғ-QH?`@$ NC=)~6__" >N?apМJ8df- ˉ1l1 ?tTVHz>&M䁚haFNs2j{s|ԨNr ~=%/N<`HPnssնʵNF<&b7Sgk8+9!Eb, Z \=y2>zkM '`ϜLg7#K+7bI3G6Wq]g8HÎ,B|Lgc~ <-iuuoD~馆>`rރ` 3iB\4 #i}C>rO*QwXs7RoiE݉=r2 4NA-uWM{⧚0q"sdIgi]myZ^}[^Lt')b P  ]†!`z.)DͨJ\zhCSwMY7Gh?>RM{P :hLH)R#t>iFp'ae ɺl d4yxtu:񠮬zɦ18|H|ʟh*fq箵৿HG ;tجH~$^L  o?lQjf~@gx:LՐCOF ,D"BPϢ#ǯ›8X%+Gu`҉y[ Qac͍RWP%cpwd`?lj_ˬ3R`_AXsA9 ]rp}!Bӡ UEEJ}弗`_~y䋳}ět:\<ֵ;VqMDċ-AԘr&`4 ͊'Q*OЅiDX=> C{-bvLϐe ,p}G/@IDAT7Qt$#'R4eF %इl"S!d0 gCQYcT$ΓG#5B \#l4qnSV#JNH#_]:=kycpҨ Ⱦ?554^~Y+?K(QZ |0lUH/aMsK_5isb^֡-PdA&0UA8#}  +6I-ԣؒhx,7wgG%PPPz] BcƙyǪxMcYD$"fFobG 4:#آBVƙ #`i5BmZY&#V[:aK/ȇ ib G9*{?Vj4㝓'×S~<<ˇ3q8;awg1fNLRa^ꩬJjG:b<uۃͿ.fd -h};NʱW+·?_B,IRfv||,voO|wӨ=(m:G+0KaV,s"JA{>%˟dDnn숌{^]lpq,5 v@Uch(}`pvQZnAa WAr.Kx7H+p0s=}ӹp#'+_~ (1"YL\vpk;s :k2רQA8^8<.Iͯ.'%JaiNsua\=΍dp岠#0im -뽇)WF5eu=@jjd jIlž/elV Z'/ķd픮xw:|6|Y]~fÅ1/;8;m [:*˚?~v rHCJȪ/*l[xo1ˇNDe 1z·ht$`ՆaQ?~?G^#uy/Ov{[ IH7BUPVW+m$#ZAWX.,N;Ymw/tF -|W0z6S0I}!B~,}ݷxF(iyd2z~g?<=y=]s+~~}?KHLw{|N~~#phJX[ޒ/ ;>MlIN:I'ONÙm7cdE"&~fHn 9c^oפ~8*Ti"H8\0eDOut6G|.9Tԝt+j$fjIl%ݨ[0ZM0L~{eH- a]HH$&\rbx>\7o{]w_]niWB@._'3ݠ0ې rxӎ:o4R]PazQ=^5Yq mӷƨ[~BP fӥjD,rոe9w܄uC K|Bd[A:?3bjY1H& +u6bP%T_4 kXJ37"8#ZVQrS/4{9T~M?R$-SB'N4+Yem!{5cdE)+:HLC6Clpd^~ӯO WwW~|5s7{a6ݭlM.E<~p}8xd4 H}||t>zU[_׷[ ?>[9Fb߅nYbed@=/{h ۰gd@ 2S@B|6$q]JRP D. R<ŹKhqFyuӓs E48%Q<vV@ӱt>nҩ?/'y:Yܖ0hW6Χ_ WwKus7]f>~btE|gWͧMPI|l _oFDNzO??}v2Ƕ;Rɞlpo.iױlv>^ퟞ< (uwInȥ뽝CͣJ a@_IrY ֧ Pfq d4 nq}{K`8d M`׃lCM|> sZc>\(#^3?ہsEQp a Xz^62gODPd(y}L(퉇nڅ'O(3޷~>Jșhu@#^"&ؘqYGERȋ>&hI?/pN@<1a0+#c%;(cCOw%L3.[ POᄙNcLSu}цLhyEw+t29ǻk?U(84Ar}J6TiToW]"ب\b9읅ӜBA | GU}`<)WVNO& H8ymFF ^P1 KL?*7J Eib+BE X:~fO\]WVRyHX4)VTM#:8z?~?~x{T˃imMV[T.$Y>tJu~1|r2dk_va%dӧϾ;o>{e[}[ʈ1D瘍љܜn+c6mr:T̓'Oz|zgÝ_>VBMyCVJaRxozvd8 F68)AO:)Qr- G@rץ !VHl ,d.ؾR,ӠfI!(8D\ZCBya- RNAZrb kja*i*ߌvJ\O@ȧ~X(C]"VGkd` 2a,.]JFȜI,L\rMmPc%TePYvF0*dnS%?& e e 3R,j3`-pS }(h6t[֔",BJmC qB_&eٛU7g1AJp2>wz SЬ(֬"q''@fˎl%5*E+CBbI9fxPPo^7U"fYSFIm󸞳mipFţO'`ٹ7FWo30HIr/fB摃VzG{͹|~~,,AR83Fe`U};j;aZ  XAD:`2gD%:1CUTGlV'> όtd\)u^N\̦='O{/+@h- ff;lWdo&S>踡siPz C=TNWĒfXp"V :/R$F\KpF b@sEW?_{RRm4ZtL1iV*LVzKx}e)W Ep R*RvUX3EWh+Rx@ :yܷo6FJ51c s}cq0 ѷ1f8a1E=v>-3hB88P9H-etNHDxV xfA[qc/i>aO0b)S^!S€:g\ajAujI{=ׁ rZMC3V/tnda@p믏߿ioo-ʓ77у$XyjQ:Tɑ-V=Y V" d=4"Őe39C<+b{7R׋g2M55fB_K1>癐) 5"4Ȟ3oW,}NX6%wǓ@۰YP/?Sf{swg͒&}ث^u>Lώ mɦ;+_XWGp8B _xˢ, I$H pL/g:}A ƢvNOw7g'z|skΖY"kNJl.~˫ٱjdv rDlxC݄.ݐ;- K(K@\s/Jhnj=&ƇC̅s]>> ^ ?8|'tS|*ʁ6 v qR4-DFQz4?s.kRi*rњlf(l'9B9wtVa0h"vr$Hnh1p &%`j k{⮉`MK-5 T2'S3 q&L{2k[_|haA0!D kjc 4 )Hi /KiCKoE >t1/%B؞[tH2h"FH L J Vp&H9_U'khyBCS8[\@"ת~jx"h)EEiı*?yH\\#Y)!䴍z휖>X8%["-"bGΝ60EVEO-r=| |0jƄAm^p^0YEj ĢɓIaDVU*S~ef,Ru&PAh>.uny4?Hqʎ>zĹ.[q}7G?4#RiӤ[?{)ꥭ  d9lw}k0:89;_ZA=v {2lÃg㘍( иĞ:]/@jEa,؞ŕ~Kn±ݝOf2KLfˋKfb_Pk+TCc!F;{ ;g':YB[P[8`~#Hf'V(Oy`_,8IEZu+KjWH,s}:^\y@ba% 0r]9S3]*@Ap|"h"iN SDP ,*e|AYnENa$̰ Tb2m`"㦓`$=V(JݓfBq{fĨ"/bCI`)4M+ZEl/6wTsWc}ˑ?;DjP8A[ 2̵/$PiqAiw'O8N=7m&[hZ-:eaS &KH̗iкSͥ/Rf!vٗX&>?0Ǎ{b- hHU}+)]ex-ZI@_?<Ц%[]唴:϶nO‘nvv~--Z&X~c, 8*Ћ$ K&3\l\(kgQxSB1?Ҙ^6~&<!+НC2V\5_8bGaM+9R uЍrZL0dtL z4+.Do;~L}< ƲIP@#q}y4F[GcKe~0nnl3(Gv6&:3]&aθ_tVx"uŬFi,ҍ}:$~K?f9|n~cmymy aW9PV{Lẵej`ot2ӽ=ׯYy;_2}^t %a`p8o|-$ed͈‡إŕvNhc) 7F!5jGQ5QM9&P-CӆMq3}: G)w9Ӿ>8?zIgQ^!24#:\ Vw3[, &RFbUBm{blAh=}Yfd)ϔA!6~M0fB#E0iO8 %w!jɓgQ"q mjOj8J}5aY6i Z,j9썙[,PmP$HSe~pR~# {kc?Agҕ)^h(f(AM?P2H@2TI}D(ͯ~PULl/27ytM0NmJ-OdЯ7v=I-!Yak1s\jUll`p" 4K2*@e7P'Vv__Y }XDa&gBdD[] 'Gcu=O(V-t>7:*K&gMh8zU5`%33C`.?BYr 6; "c`8(/])YKnDh9+vT !>]CCk|νݡ5걝5lp?n?ښYBk" Dӛ0^Ec,ٵDFz9dU*pD7Rr] m4C's-9(a*Sr:"1/)B{CL_;J\A`A)؀a{ޤ<ۉ ^N4OZJOx5wZڗUB=-(0(h`V^eg=D+[ >2!4>+[^ r܊ @"&Mca`{հCVIHӉW[`amJs3tΰ0/(ac"g$2i(و?fQ1COVxaoҲH5.x])j(hImk`UG}O2JՃ~i!_iɩ{aH lNkժld2+ۻFfD&vFpP捿(kIm3FX6#{awv$tR"%H.c!}1yK@doD,.cjTU(qeU,Q9{+&Im3;9Xix ; C` Ã=e/,DXX[σ(⩹'tMtDdiRIT%s6(1`R^u506T*PD\\N,˷W_};䢵"oɓv@͕ z$1f^#L~$)>w wQiXH$sƾд_xn,Ј[!~tT{ꯓɦBY O(&HDlف8wN@s$$ޒ'UщP1WR\^1$*Y4iĎW}ZQpt9)H"6㌑lfg5h6Qò/` _upgщU^-E"QzQ AmC,=rm/981ӥ'[W64A\1ұ!4V Ŋ9Z`H3zp=1_}*Rܹ(gY&QJqRg^'di#\24 ꭦe,>~|^61LuÛ7]Xucb}<E//f5;k+gN雌|q9.jo&wOSD p,I & ye#q3E]Be/뾫VFbXV7 B_`Loo3~4_cfP 11hɚI! )B:L!9>p_ZT^ܘ &/iGRD%%^-֘y;4j#yR`  ^^ gb9Iڅ.BBr TJm09$zcHa҃`ɫārdh-t|*qu}E?cY%AoNob֕ː< i;$Dh(g;D NcEaQg jAI2!É|'yK+'4:^RJ*e'±l}g&Y+ݰotYd0ފٿ(Y,\Yl 1dSLλb<"t"Dڷ8`Sq~{lH p h*5f-6ƗC_4ana(qGV~ &'~IQskYr(^|0٥l0CG^ޢ D9:`)P17}НBt}o߿lai|̽; JnwgFZY_^Gj1B#.i\tB<˴}ZҩҘ@ihbrJ )++*!`IlŨ=ՑĴ>`ؖTqؑ'H##[@"gSIf!buYU`M$ igM{ /Rc$>›8c^֡F^PJl@EFK' *?[,2kW OwuokLi .<̞tA1ĴGXGI u"lU . ^y^RuL2Hl4MD~@ Tz( ^f#&C QOqcZ^(%˖ 4/J'Pjy̠Jwʑw{ӯ[8&gښ 1jTw7x4,?s>eGԣ\J/|gu{9<7ڗܶ9&*fvOV6Nbb>D.Y}t$esԌbft}뵛OޑZyY|r }~4ƺ1W(96>-/vmOwpV+}&5,d-d{ƭG ~XaX%s G nBWe^*ccx0.NtA$\I9%>(-XJ1&M%|d4P8p T!*/9-> suPB*V|h( `-=󓫿-wza,eD>OPswFtA[vaO;x{QP8y09~{pHOr8sT}58+4㮄ɓO`}r)K"E( "ΐkJɩ\q_3M*]aZ'Uܿ.+j7S>$Z MFzb\y%F3P(<.OCzD1U{m$oOryU@}tM"p W8sM3Ϭ_uX7W`,%Fhѯ2dd^]@ /?я\iߒ|(zږ T8y_ܼ9> (,c/9@Gʐf,=`랩k^d_#P 9'`/D/YSݐѱ1T˲ySC8=2x9 OG~ۯ<,_D-.9{xcu}MNg/G!#z~w<ʁѕW?NK Sb{&GGᒆɩzPU!zxUo5\ӹPuTl-)&AA*P!Ll¢l$12cEXUc@z:!;z< gRCb"ʲ (抟kh 80PBԧV͟S&@4hϏHol<ʉNz޻oƛˏ=xѽ/6$y#jA,zB=up2:[kUo,ΆgubUDK1 RG>9QHٱx/Μg^iMjg~O?k8S$VWtHN\M++fn++By~.qLex!#)XO4Aj43zԯR&>*dADmmX|-/>b0Qj@F-]dp@hGeSf| (\Q**Eb xi̏,@H .PX$f7Cˀb1&wȊhLPg8A0Yrqāu  {AZc&b91+5IZ4aP/|c$ɐ9YE..>SRښ 9~L램?@_i@N "1$۰t-%& v7Os=q50h(q,6֕vFyuf 7Jpv92CLEt^i7;w `Ex$;$t$)mfGz7dv >~C|ޔ.YOj\w !3]#|Z\BYWo^[]Y6&x#;D|;Ea$FQo%YJvʆ]9wV, *2?WoKkilZ[IuP%ODdHS 5B с%|"YƂ+FBHUA-@F2|r!`r;OևZ׈dOsuc^/ [RCSeLٺ` ¼UxC_MT2Tb|7i^^b 2 N xJ]>@-5E[HD^cAlBqGx 5! =큺Y\L(eu;! m_ZOtgx>xkf"rX D@+P÷e$/>Ն[# Lh#&rp&;ivhVed4F7r>ݗ3$ջ?ѧ,4uڙֹ5ؼZ\tńgKׯ_/ G8U 8 kz!KVX>,'1w`@ָwH2%M"G 2жR@-U.x cz@<J䬖h[Q:~~pgoJ$m]]! (, w ͠>sEi/CW{ޫ77['It ftVzztk JKc7nz|dCӔ[Htnݻo-P$'ɣTB(ʄ*sʏ-LmːV\`ĿE~)i^ʮW5ybȖfbĹ$@H!52@(eŏ~Y@n#h(){$gF0 4ILIҰv݌ʹ?p4-yGͤֈv@IDAT(O !S ¶О 2ďe*.0(xeW ]Jo,[<2?]ྂ@W dGcLfPfNwsʗw|!`޷:Z&}490DG>P< ?"-"f`"|YvWD~l !mgEfq5ITG׮,L†(`UqԛSdאb'B]H5eCAPJp6 {ﷇ\ybcW m+~]$]1`C sE >5Q,(/fØ%jR~.,eMSVV)KiZ"J\hPE1_"{h1f@%yr(@0G>4 p50!tZ4j3Ƞ0E|Ѫ)˿|kTYжT0F$o4;M)U TM4K2O(s9 )~/ tj,Pc&s_dE AOUPA3p/2T*a\f:1@PFͨVh#>(lGϻJ|g_fGIV|E^ :x(r{ڻy}lP!bfyP@kpXPA*L}WotDA-B$o* <-dD1,5p##ݴщI&Jts}kusd.4`mZ}i->x‚a:]\}%sϿ/wQ i^K~ܶ~%rfd`Eez^2c.i8GPy} @ H2Ai|NܲRtxa>B#<';Z!D״]$b9[7)Y[KTD\{:PTNW6޿{ţu^{[q~d?7LIqm-.v6v__:V橭,cE2kA_ []3Ic8S2P ַn}WDb%t W:,AwhDJ'$,vBɩXc|1\Ȗ&U eV^921˨BɞE~B'ASY[!tm:P*tԷr\kEƀsAķro/C:گTG2 fL[U!/#T<:"Ȗ9c"Cq)`\hYEg#rxTD6^Ɣ-bxSPLRDMA#OkLP# {2JN)BuOڣ=k3mbg3Y X_gmna VA:G)#TdxI0`wm Ui7-v-TBu5L!U-PzXC+3y6TsL{"??FL{`"X*jd+I(%Q'%`&lɘAO׹|2̜g[k6`C9jVjd&};ok;#@јݺ42K(qJddkB $5}u:Zl8g}@E|E\pF6ݠ.l 6=uF99VW|xgc ^)"P{Vťƴ\oPXz02-_{څFu_.|5A8qLFp@S #VMayH:ZVK43Ь&TZ jLˬ4,))%V"/'<issskk!Ӝ <r PSS2@h`Jp-՜Ne[ +ULK>ЪS/zht[2~m|Zߜ ANEm~hq9@Dc[Ίm>Mb969i<[`4P6 '(섆$*' 89.%4`{a_ySWW۷nۿBgxG82$b]i2V oI D(1]'jiaYkx"LT_rFA.lX`Roė͢ݺ?xI'S!U  4Q"bqLn9 dC2}X4SO_p󥫶Eo7nvn,.O?{@S.'_y7ر%j5>{オTဋٗol"vJC` ;_>x8lM>Ja߿| oE$+0^㑖݇)n;&{hL`T27 O~~`tEƌJM&+6KYY/}3l{`Ȼtq#!03eP މ1Qq%T.B{z*MO|X5VCb9"; RS߄b[8FQ^ԲR6WJBtr"!gNgؘe'er6xn7Du*ap_^q@Wi|trmLƪ( '=G/o@|WfxE*tmid{§֙"ii⮚ &׬EDSR8>;[YLvU=>.{<&K,1M&C$;5ɖ #A搯R%o@+"x+5*T~eCɩ!(D)3lJE_z6nz>8-gBKx,f8(lxf}ekŏ8cR~ Wf?F _w_G?f;['{TPͪawvNfVA\X]-\EY1=?k@b>/H؛}xsu}!?x;;o ,>`o#ey2 Cbײ5  >6ԋ:snU`Ut*~R)nJseiOB_ERl)fȟXo*C3,H_ɟϞa|~trk󊸂D D}> bKx3B\@^!8#PDX@љJo2`SUO[ ]'߆myĂ_H=됣]( '؃:p#SvؒM]H!Hd\ǘJQ%&&N;++|+c;gFc܃w>se*Uv C^pw!e9!봲R.֘!Cw;涵z~457}Ҧb.Jv#d0)L=O|GHI}"s'I^9ɍ+WA<Ց }h`?z֦'U؁>4zgtူϯ_y>o|o]g%&zeA_l8A\r@)?CN+n1%NF'ʒVYhu˦E ;=g-=xxI(Vۏ, p ~٭j%ǜF] 6D6iLDxy ˽lVcg'l&29C[6pSp\(x$%m@ a?c z-K:EO>VPՀt1}pwS>k uKYfP?Q_ IȳOqŔ5@uY$ yl$ِ@x xP U^DK G@UFY;# MVM0x% m Y AjI @_/5)|t׿F %F׬u.,`Dx6\z,}fI T"IU.lo @7 'g?\y;/Vl}ϣݍ6m;*?sS@ +͝;wBVVvvԶқze͠0M̞,;2L56@Xw Ã}{+q>Y6CN;FU|mhOBt$12'|1GFTyIr/g(@p?906 st:}ôxRuDT7K0AqҪ1\j! |"'9ceO;iXR 4},SOI"#0d)gx)`X'DDQ9)%> O>J"1M."ȸRk_SᄡSFUxo Ƽb  rz?>@ ~,H,6 Idg[X;;;}Koˬ޻7_qdsn>sdט};{nw~?=,<'rXluS̱m]El$8vֲq9w.`jkϾ<6cc] bz` TO>1ҡ!yce2L 63vJހڬ"@r,jJFbcM~}0[;X}F1_Ja#: `Y̷lQ$QcERZr4-C5XSJ[nQ)vصygtgFGL4M%=,fVI xJu*:F|WL<Bkkϕ_Zǟό@A§"a%[yΒNl)Wyƪ= C$'*Mf$gxI TP# ˏ!R5M#S'vr+ jj:5ԪkB3ɺS{Z9UK<9Ψu_T4 y%[e 6gDpURX }c6jf\V5ofi}w܆JjCj6 { HOGO_2{"9braM cZRO~+/ 49=2ML8kǒ#ޏkgpg__[(b^F%m|]qƔ9 ɀX]E"(1>r'tGZasƋ{:cT5Pl CES (n:4uxmB] 5ZF Wjۅ_^d")3h0:k/AٔN:N)yb8@*Q糋L;l1ŠBٖ4Lq A]g|b)ǘ9xҞ_j(-˾9\ а`t[H,a٧Dݨ:-l{dUU%cK-KAEP3☯chTZ f,ڞ^Lg|Q3 ( jB4Y CCiVGZL.Cb3*/1kH(>[+ ^٬*Tf_(s\kehKdan\y#p)hn%F ׆;,G%j惷y- X1E0>ߺr,ƒ,v ohL(  䦇roX =/!7rreO; orw()YL^0_o DӁ23R! I>,MɍXS7t9wB)G'CĄ_E 'ע,hJV,)Q1-(A[[ 40˂G;=u[/bz5@%bv]i$y%N:pЍd7j1KKՐqeinj)LWՓ|[}.Hn<,$U"+P84<--f'y{ot_/]FnLd/ynԘRYQgcpvU2%j[sR". Cⲏ)vz`$/`57+l*{bҢRjfGh8m~XrV֕ F>aW!pZ;°f5c*sO Un[8sW^FEeAsYB\d-!c&Oh5O5^ +ɖQy:Nl{>ioC+ _YeeB TZ*D5RP'E-[Ӥ:W}:[JP= ٠2MmF,,ʌ/į. 6](:/,}8 +f0bet Bf,Ȓ`k/rn7#*-<Lh%gVD˖YǶ*>,QZHI}7EƴP"=¸gR5i/wZ!¦S{jtр"G< Ħ\7ۗ",|@5 ([SkI0{ꊏcVNJ?n`@+얮=yCvY[)|}dp8[G.-#tam(YG\7),p 6~PwMqLj$@7R*YAyša "02u-G8R bu ̾1H6e@e6F2)Wl_TrjwJnn!@&oC1 J`nr5kG y/Ќ\n9R%CY O}aG1 B^U140 ה1!}6Y6TT^˙N1d*5ت̉`f2IΒJz,w}gGSM*zdZB`$y\=By./s% jTx/zE0B̩p)]TRS|rJ2Qj=F>Q3KE"rY rQ>aW\FRϽ~@jA>X_5Xp mE)F9 ^G%.* NOܒ $&&;DeDͫTS+\ZZT{JԳ{rWo}Ir6méχvkoua͡ bsQ֣%OƧ?\Lt%8-9>%=v n8q%HS9dkkԬl83Eʏz e %d\gY!N93;eM6J%E, 3pr_sلzkN[ 7LYri7%ViApNG<7 )p1 m%jT eV$tJ4SN<F} [rxoW `5Odp !O-ӟbw/OO䏓 snΆ, 7R 6bTа΢mv2K+!AA0Ȕ"VL91߹L4ySđ7VNPg#vXWf`Ƙ㥖aBY(`h5'~?5T;I۾Yhſg'%T.{nd Tz\GVqTn\ue`uMYe^`FP24jgkٰlIG.X#~WI7NMQ T 5K c/3%=;O\/ @#Əșa~g5?,6, Il՛vsK$bYIXCL'ar4V] 1a_"tJ`-Y x3_$sN%4U!P~a{l cUNLX>%Gdٴ #w`xgwje>M|δN!@3G%(;s5k%vdkPFхPC+3Gc!$5I%}l$Q/ {ّ?ƷG d88S(Ĝ 5=ͭt?vEۙ*|R Hh ݩnЎ8Wcs qN&YJܱ7^y5k [uqrqN/o{iKU=* tD+eaǠ维9{/c"Z7;4NQ)%%e%Tr"]UIu}k[ "UސM͘6Qm /A,8!v3MuF_7Hf,QA>NUB$ct6AYDQx J% .֖ J>CA,y/UPRLa]Ŋoɟ 5Cu]P#&Cb*R %`-Vu9Bť%o~ ,L*qXN'3L/?ezKPr>hXkBOr0;C-흀 fhY,(GJc4H03ORtN F%gAsp (la ~ܣgÃA{C18>`A -nE9YwŸl'$DVfb<0qf3Nw޸x!FLQoSsU{*{ s)~!+z" tu c2KrDJɩJ$S0]uUà9SVZA:*A/QL}]+BPAKPj$!w3ޭ6]XD=YL0%{׀UaNRN=4M|-dĖ}~ct!p_NG8c%hN)Ne}MX_T g㴥AoaBM2g )VxMTx̡b6S9Y_> t]E^Ӕ`@\6H0"EC,Swo?]j>@S۫T݇{NTuv,#f`%vz&Cʆ-2s gLh&r|(sEaFZH?j_U|xI<6 cfCVC=,o>A3~|hw<7ԘDؒF?JB#D䗊L?L)6UX]<&\ BH,2bحh+EH뀢DG 4qӵ3!ˈ@W:.*1Ҕ%ʞ3)F(D/ߠ-Xkq0{N5f]Zm)}1>,`8y럽.`v淮^zֽzڤW7zoz?/A941:Wƶm ̾^NpńjOw%=Xi[(ZRA?f }~tߩ0Oţ[YZf QϭS Nksiz++ ^}'X]vf1OgÑ9,8bvi:.އ{)vt^ƍ ZZ^츙i`؇6b2uD3wx80.vL_siE#aWV9ạ4g~5NI8 Dd(tCn!N(bڗoWU+-'dŌ P-%djtp.-Bifڄ*OpT374QA_D99kjdI 'BhBһ=ڧw*50<7*::aITr<(̔i%AlSjatDwFa@Ni6M4LQIV~Զ\^][<4gHGqypf]O[Vyr7muȅ.Kj+j_tk}6ZUlo8W8qjDBϥrBFs֓ 3HeRDe.ӂPrsrOb)hԛz{c(){} _CTPdbl LHxpGF̌,9+ݳ3pV2icD$cgU//A&A Z惩+HBdZ L*Ͳ: <]YLJ(Tî:`zqx|r;鱷v 5cBeex>)$.25º.)%[CR*Bwx>T qXAaU\4 bV1~1[3)yt%@8MjK h](OU#gdյeUom^g3󙶉K]2vSvʲfkmj綐JQ *lwJl[-gQI߯cI2+?VeH威qoM 𫌺c7Amض>ց;vx9謵}ڄo2X{P( nM7d&8"%#D;O6oOsNu=_͆rZ2VO=`@WX9. ur3ҍk2w["ۇ8Ȳ#;؝PNz\E ;S;=8Sն\ Eg[ٹB(gJG&`pa)Ɏi,#V]pЃ#r6֙'믬IgR>MǴjt|)c\`:v=U: e bŠi%JÅ4$ K@ uQ %~E5n@H:@#-+iM0TC9ߦF+ N`wp \e{(*/t~o7쀢侁n@WIFEa^Hz-Dtxc%!u|k$(̑Qbb<-YuםNQqa!r'!2fKӋf& u*6tЙ]M dKFXh0C%`.d AT"i)"R@qҪ[FNg&j?fknr9ϼ%e~LjE3|T4^2WǧƩ3 ܱjH#.vgh:gH'HtmVBbA%eȑVRl}5inB}0 ccYK ֛h}ލn9mp¯ErӥS.7QfSReks ǢAocOOx:?\KU mlm4Ŏz[IW*{?!0p!X8=mE$`83#_FeA4kh T̳֓x4&i`C>YVqd=-;i}rf#?폹-ʸ:=P VEt* w% 0^\e1k,G(;1Zy?ߋN[%V>wtJIyF":E }0xTK" _i˯ L@&ӻ/T%N[r] Y4}zR"im[J8@qp T_Gp }|}%?xS}$E$FEzi59Px; :_BJPo;oڞ ȁD lhx8ѣvOFxÂȔ. OR %X*@'~Οl߾=ޡ7| pyEI)U.'vUJ! MRSl'dYCh")BHQ$@ }o}} <>=kX'%hrڈ.D ɹ`@-ţe$uU<pZhBRI.="#Y RHa/h)${^bB@8(P\/h,ʺo׎|c>F(KA==(7-#rTH\O1X1V`L uR|8\/|aoך=itfN Rn90Vf\˂-lyױR|X~FpNCQbD1Mp) YDZQTh5tqX &5޵}@+}hJc<;D ;Sc6[oK-i2!'ꉮo*]Hr0O$ځCʊVYnqɩo'n' Xqmgt~pк"ch. Jg5% Kє:pR&cL<^> W3u#T QnDs.wiJ]F(Ȧi} }"G4):4>}k?^zʂ'*l Cy,;X6O"ʥ4Cκ n| E ,ef4ৌgr&X㫟F҃K9 -b՘*!5UHsФa.%hgR]9R#ڐC{z{=7{kiC5Y ]5E*rկW?auLUVe)|ا6Wi4g>fhQnEۊbV[rb Yҋ5 G~vF/j؉1p$NP)FSgEsٚOKw@S"U`bßw7*TXIILt~kZjldGǚ$ 葙`DF =.sĽGY(q3hLNVNQT'VEʟZsJүX:&o,u<3CyIS>._ml>h Hf^>3:JbP5~F'%+.ާ^jpM+cAMz2^Rһ+-av%ok|=3{냕}7_76Ev\T Pk=h0r [a RҖb> iS/ $wW{AShǓ!3([pSfH ̤Ge|>s b$Bא҆|K2q$&)%<ޫ=RD>k@K?Zbcײ@s1#ïXc.)Wowayڢ^Z{d?yu@Պ ɽv_o]8寒j;dǵ `D^cJq26.'  KWS3-N`% aa`wnVٴ+f8XW&i1]h hvڳ6Jݱ]S7l?w|&){”/0e4>seō_%(8'b(d$$>s2sotA4ѻ0!4¯Bb&XFMQlEQBj Z홉-7H)P|VTP yȿJ,Ϲꥎ֏7X?s;)z߻oܸaJI2gVj,+;>C sSOő|]K{ӦC*l*A";|vv-% ȟ0*Ҩxbxng!WWƊOŀF^d"sjИ<)redL2{Kg)Exi'qЌ7!2 F]RK,-,yP(qȟ\+os_ǿiO'}!3I$E`Q J'w~֊u;{_s A<_agbH9F[cTGkqI/9.1<\Ys)`"t^(/Y` K@%ZMƞfMơBpzB/"q4/v+OUd\K[{$ aµ=M)X'\.K;5?F4[bwpSV,KvsՅ#6@rHJjJ.9P[=cBlJs[%lXCcN1zBL œ1UH(.)IҒkSu6I;^*2viMxSnkZ|9֫t,iks~`%LDO~'!vi}Z0`?-3gqIgD͡т;|SDND^X񏿰_~766'Nh਎;Zd2c`kP90Cit+E@w9fH[ȀI\HE' L@@74Cek(,+_8F#X"4%αSi2R ?4@-J"'˄I >?β>],Q_RѨ~|6%AG]ޤYw=Kx]>n[Ki}'l.h3"%z.KLNׄ*5$˭~QbB\Lxԁb Ӷ^0>G Iz`c8Oęg{g CTYџXLLԊ6kx@{V8&no0AR} jp*k<)2L}E)wB̧>s8W}i2rWQ8Z ؅_/qiMAx^x/[ Dtr2F3)?^g#h_[1NGRqW?F2,,lڊP)T=ONj5؁{-fp%rΏs`( kЎAFQY!"A&252WΘ#0ME(4an%oP`&1lԠvpvTq`V!Z~avWW{qf( Yi2w(z%'sՕv?)`t>,e LkT^i nDVn)Sf/ݣr%4F-syqO=q"C Z(y+~-.&YSg?\=rP=u˥:uχl:I#u&%?JNHސo. .~ ֆzJ ֟j/Y8G)1|I%_PdBbYR]mePu,Vrf7r"+c5L'Wi t)J.{;ԠRR!Ee4,G,+bB K9G>TbwWpAYJnKʕy{m W-ޡ{*HNMaeH 02r6$k~+:w!UFk^1%g~W>M55𙔥0-DuN:ӯ|cY3"O UCDV/i oSi?9]Xؙ 0"V2i@11Seb 8 8 s"[xK7w-uUWܖbep-D]7 /qzod8JNN!\J%N75ms̠~twؼA<2,{$K}9ak% wV y+\^[F0H~?ʟ=կuG$`Ӵ(O=~?q+΁0ecKX9\,svE7-D#&OC }de`Ii"b-S%ջ4Ʌo{i7e Gb^ {cгAs)@gAB/{g2e%"e)kbF?76nYYM{z>`}'?OdEKƔݝŰYF֢;CMCL`u`ǓChfY^qOGOVUI- =SK&gx _ 42d p(Yb@Wɫj [t"5X,35d=~#-,:=`ȡsȍz؛߮# K~m[Mq;*keamdy#t7v^xds`.)g4zЍ) M w69[b9]f>հ$Rh`-!_|D 5{,nr( A9>y.{yPۭs˼:?.JBr7RM"Z۴6ѴS~flͫS"M2VS~1QI.Ҩ I躨,*!i P ml] H>UxVv*֥ :wrC bҨ^)k] R}N-K+Ki=Kٮ-woc|tEѪ)HQl[I`0IO"g.Rž կ}$ ZQt}{ɗϟ.;(> {BG< Ye 0ĐAZC G{ofiI6<IC'<+>hg MAƶ4ʜc =RIvÎ)h?c2 g@{N6{ݭ": J:2V2=jB+@R +vIiݡRdck]FDkG%Kjl?Xnv^޹w w%@ޮ`;ra[;6y\|*2{Lb{/Ƣpagں;ɼKv c*kO OzI 0\]$2<(݈Vl`zN=GK+Wk>z& [-6ʨV77ㄭgjZY8u}]ج4 BӸo|C c2a"L.i"|٫u]l"+ڐi;L{V2~O bhҏ,7wvu̓o|4U,0I;ҍ'5z/8Y6h!|V`"]Qn2?jaf LT ;eYpt-=G2:R[.{˝>dڠ VL* Y0ƧVӓ¯'uYajRo$Eq?7G7wó{DƘYt[<g4şY!]d<]ս"=J5P"RJOpZ{2lf.Kxu? =gU V jqZ%oNea5!;^E˜Da!B 4' qIYK19-6G_,dyĹc/m 㼝UZGOx00'Fmm0x/7>rrR݁b[5c6 ,Y[nZaŷ}{q9x\Lq6+Bd=. jLB3 >;}/0r+bqwo|p28Ǎҫ[y?+JY6qƝ.P nHAj fM6!vс !>#JHeM'|9+El&!fU&e #Pec !&+d"j ֤,9;,˛Y5"PCOCasF33|~cPH4L)`mskk{{Qv`[^ΣfOs0(sKok#<ϤP,V[#{< yj+)ځ04s9 H=a*%O/ zXr34OmܧWs=b 㽾)b¿3ȁa+\Է07bEf F>aZ !q?J $<`dy(S=ϓH=?aY Ftf;D# t|@qf:#yI6Lh) {D?`hk`i!ChG/*CܡZu1dϚ6Qb͕H<9A:hE& ƴ75j@>eߵ  'fiJ^S3qO̺WpJo:B]lJIK. Ba_ q޵6[L zs\LŘ5s5X"W,ZZ1kfSyz@ /5%(%$Su*/bEP@V`vfD֘6BS1}@ 0W*^dxW 䪾ųG}mbK.<@( @US/O=gO}Ux G̑."R"%)kC m6!YŏO?G(Z*K$^K}Ul)y c>߅ߵ_OvMhk)`}s9dv;e'.P6ce0˼'9]8ydq(hL9}hd =Lhk}HY7]H\yW ѼQى8ˢ$p$&' }cNF'VsFp0z`ג@oݚFx-*K/SڡF{+]h2Be*ET8b8 x4`b5e2?vcso@l iV_5A!)^wpCO@.oZdRDD0? S2Q4-3͉GGx7շhrM NAW +Oa v`&bTbط2 u:aiߊd(f(Hh!z(08xgJsҞ(%oI^F~ivȱCl]6Cu7TE7Ԫ?._2BXP*۩^56<*HEҔUHQ" t6flGA~  :5ߓÉ6DQqIM( ZfNF%T45h|( '%;jky[Rͫ.ԻL٬^ 1Y*~AlUv~';oVT^f: ij<ԿRbI\%d3΋\g@7Ax%ay1Y0eϱGX]b?@JkEA f{ga/!DtX{xIA+7ָs 3sX7 *4(BD-E?PG鏛o]gGƈiR2HxJp:!Qs*sAAaC;KԄYjJ:tՐ☶omg)ZF*&\JaBɳ7I#a2 -eK e+[L˒$,聺OMӦj$)T!I¤\J."}=dN.Ps3]V`UlU岴TYl3.Y` Qr)> U. W t #Mr )K&Q8|d}h!fx; ll}ti`J޻ZwZKl`efW1 e`E jV4@FɅ}]~?&'Ͽr8}!f)~]Awo|&TWXo v5 zrx A'[M G20nڋ@FI#o + f܁cޅ&t$|dGËzq#{F#Q'yBy:ŵ60`D 8l0_7Q/ SͅK5S>oSr.lXM$ 5vg j!W#adDsq,:TU jeq\HNǩ`[G%3{]1ysUkQ5:tTU Q# :htLuk_*B^m ;i)* k%q{Z0 4%('_Y8{p= K,x H js%F'Ĉ.&fljoL ؒ_8^!l{3gwd./`4`E)3AS %D̮=a{a#3(śVjn#VGB&-} 5o>T藂C#{~wٯ^<X] MvD3Ba0*MОbN!㊆74s [q٤>9o SL!!Ax?ž,!!8:O|u* e)oz">˿*X5 zO(,[ݲBtjYIc`o]S"iEH J!5KڊF.&PG~2ʱ AyuC7P+OF~}Ye}v-#[qҳD,_~66Z\kJwσ%ppsÒ\vӳO=+%𳽧0Ǿ (~-M+dE+b;/q',6Jho_XUy=Ts%҃Kc0E(U]^u%uO%Tݥ yu((A`r'w87wdXAVV꤉] vKg&d, 1>e{~)|ީ$K./EϿ Ym@IM?c4o^^K.$suܿHJN7ajXPjIIP0eVAuw89SO|[ϟkg uoohdgՠy9 b\ٿrg4 HTkx:̷eZXlud9q-4 JYk (ӤKDnO1l_oiP3녒ˮ€// F  QpmBk zQ9l3#L,JRpRXrβo~5Z6^G"8ȸKr-\D%ᭊs%'ư \% 7-,'A~o 2`7tձƼ*]5<`hA_rT- gŇ:Bg]-+%Ks# ObX(iZX9?S'nolJ[^ )yױ5߾_ɦ޳i6Y[hwQ˥Kd//_:&7:JwrqE; :8әqxlnV^o[;ÐϋhF >N[l/Ɛ)7L@. &`.ђ &Dj:;Rf!آ xIM-~'&xr(6xg&Ki/^xd'] O ƫf'zByIRD K7aSԆŸv霖a5Y&iExPu`hqwڕ+]OB?{yg>36 ½Ms”5^E"d捋r9Nw!əcaUvteJ#[& FmX|`^+9ICK*3яWJ@ǺNIŜn"ed: s?6jy9^C;j%#KTlKwqz!٠ddvi~B QG33b8JX̓j$X-Hlr+pѐ7^keF bDj*p@a\>B"b# o;7i&z}񑝡q6Lf6 &,nf9Þ8b'yDӣx>e|ZJô&]οZv^ht42cIu=*5 L1"1@{h?1r <ӛfאaVC5D#.pD<}M.é%T d /NXD"8!42]rZ JeX\< :Γ֏T5 7$9">;>ȮTvq`8av'p5aSvp0}iͤvcj}Yfa{@L @+41t7_fKP!Б)9Rz !YVSX TuE**'1غqy! 7?yw?͊XSp]`"b7D`xV^g[}b?+(`y0"YmD0ټ}^!IK0'ѥ|?** k.\'|{=I~;݅KY, 7֥|a3>)#,"iidZ7=QZb[2 J>֥9ڟysvdYDDJrDܠ\qIaږ3.bIA#?pJNT44UƲe! %z rz_x3-ɖhQB$/Vb#TfNdb_)ߓlm 0k-A{?\;pϮEl|e_G=@ lTsbYб&& 3nՔaC^.q;Yܶڵkv5+u&vz'C!}+_2˘եOy̆(j`]^._)nLdy0zƧDl_ǨcI/\JeP$~zB+,p"F"?)ZpA+[$rP/kQid.- &ƴ罔}RupM;njHxS1P.C8Fuyy0OO 9+:3Tq\ʥپjdڬSބEMTYo~WZ/4M ~QH,vƧ{{z߮/Ssץe+իk\f+MۨuFԀ X]#i@ U3E,uJ@/o2"A@j@IDAT4~FQ!2>:/Oc]3 _$OTK= D@2cJv$D*3ۻ:ٍM}M}>(h#?;^Y3Ej'EdܢK.láӳE$֒K+^Xiť89kPƊ^s&Hy|kNnر%KVbC?>-f 6oh*bxQv hpuC!LF_1/mH H=K 5#Zu#(j&v%ԧ8ƈ90z@.e]ȍMGnq &c@+b秲E;?'- IB -u]>2aIG}F0s]Ň\VQiIg$op[{c3YBFd[llj5 1^3\F*LOslzSv [LXӝ9Bᤤ>c.[b^ΐ#2A}С#s;<G{Ko- K2Eۈh:|a[7)Aѻ~@yD&(.iG׸G{e*+a61Ԑ@XME| /hz8o0񽳄]}g?O+#o;Bbߩ-7LfKfVWB"OJG/b xHӪZļc,v\w^w]>|`H]xTϋ&%yKJ9>k) 鱱hbYai]@ڃ".#H{{I@_PNj^EL7&c`3:u u=gPQ"uʌ`rBcJelm1رԯ!c5L5BZPQ6PS%g(*#2kvHBO%:b[i{J憊7k o{ϊDR,:6X2لuŚJ>>)$v̄BڥbX$i˂Jf)o[5<!-=Vytņn|TW鰻j}ccv(OƑeaA9hf aTК4)!;mEZD\:\ޢ4 ctCt%~20k3/ڄ51ݸwWG^؅!1CĦ:'|%eߺA  mM@UZu KںX@7qv+'奃nĦ0fo7^̌P(?{??a@q5þϰ-bâ S@݀WE%*Sq7q[K1 sLA| ꣟KI‰\Rϛp$r _ R;R;’-ѩPqՂЉKY o[TKTBJAZd|H! / 0%ľGE^[sh0D Htlm/p#RX fg9b UKR2S\޳+Rb⢌.y]δ,/Tv-̉=3 5hJ8lK `uhzApƠeKՆ48)!{mܟ²gGCxN5!x1R\V t/Rk " CwҔ8 `r}8WɟS 67 lXAR֌'pT;f7_Ag$+1s >-O{eW'$e%h"Yef3dirG92tW)H¦mu8rmjL?rU3|F9ˠB |]c8ӥF3VLH&**\ydlbʓ&!%Ic 6`*{Mb21%k02^hŏ BK쳽? 9g'!J ^[مI19p{k\olR EDtR *j1$6᫗X\T3J~{} {=giaʆ=M~lY^4"^% f{73RjD^\J@get.#"\զυr;;ab4n]HiBLxl!dnb aZ;ZpN!yZ;xYMJZWTXwO ZL"#<n1BX@X2i])O9 "%PZWZT  % A}=kaW&49|5jt]EvBfҲ#YJf{F"{@Ć[͂Ҫk[[`!!`y4m;(e00ؚ9p9927{aȅt~J_ֺuOإOAs phꣾ𶰲["_hg/ăVYL4Qt z,i_)9,6E Md|aE=)'g%+$ʳWA[xPٸ2:[طbgm.@!wBwٝZ70[iKPH7V0:!,-kp{z*v6bӯN؉An;]sf>Y^ pƷ)_I2&X\(nfb @)^8d/3<3crؘ$xF7l+ ܢda~PRͺf_rD;O;/ۊ1,壨A4*eWԕ+WR?Ik6"7.|x?X+R$-1kn 0A"2Zm@Պr镬۲ rBѾbᐦn͐˥xのC" ͠@TXlqkfk6Fy^ Fr?pҺMc#puA:;2 Pcq0sJq 0t 29&F!<fĦ>Gl uH$̏cM T$m*8HLYVcKpFoRx\ =DɌBd-V l bqѶa|ZfiQ18Xh82=ʺX"f߰"Tqsj5$3G:ZԼs" w07\U׀_O~sl/ef^v~]0XF1fԥFi fa"hYp4l` xkyf $ߖSL aNۙJ;5YČ}xN C̈́1gv9z9{sl 0^ޤQXlyĉWiHg.h+ljH.F O&7|JiG(!s$XGczJI3P^%Y`%Y$)\MEXPԸ2++c_w2 ,R][ۯ[˯T228UMCd.6x)8h,Tט5I= ?=%ڐH^O27h[l c Iyo E&UWzIm5h4G`S뜯"TI 65i,O :.(m(fE%Ը$|X 8ᴊZ] JSxҋeˀM #Ϊљˆ" ʺ@<ѺP You(xCg{Rc.G4$nʧ ha9*V(wv` 4I(5^'@xںJc^{+ 3W8d0E/>;eCk1f*bқO>Tx͐⫋gMpץ銀c=R ƞBGa:BsF'>3B..F!SP*;PC3|J7-ksBim`ҥ" at6n.YT񡃆-h ^Y}( 'Jcccrܫ!G9eXe;:B0̹gN eTbܾBHviv \n5Nc}-Pڅ֗8*Wǣײ6rO> *Bf}kKSǿczlSGqP4A@_)A#ŧЙ!{#vJ&pS4)"O>6$Ilc0<٧Y{_/~~ҒR$~ʯ=QUK1$f$*[N4MH.eGw77o/.j 0ī}Gظ޷\ v%HL_혣)%b@HܧLp3i< H; rz+]*= 6p6*ЁGzp'K<4. QdfD0ordӽ9V9dKXK yƺ\C% K,`͂#P1I*7{q+ !uC/e%W.L5CHCiف=N[µM`uYc1LD3Xn}qs;TW&|c׳Q$BqI(Wz& M,C&!%D{Rlqg(1{i}w&p(e7@k˼G)"-J:֘ҜnYR7 ㉵ԅ=Q|2ˀ`Gp:Y&1]nŵAvA|$EVwvv?o}E97-Q}8ٖ_T!ڜě,s#pg&ΪKOOc߳DlȳkzD C-m|#Pn2GT#a|ۣp\~]EN]֝:姿oZ_+ZFc*ԟK}PАVc1R+QK5B$+1 2HaϔTT@)&Sn8XUb Fay 4Zjb<>8D+KR'}xbki^G?~Ju_u#qL62UyZBXbA9Bb~FޥXu 0M%ik%iC8%KX꫾˛W "_cl%4Oz!FsD;͚(܏^(  =o'.q'V@5<b1_ y^k3@+M\C%D1tݤ6ި)Xڈ89xWh%=| ūR"<74+QK n5G( dHRB]j>ˆ6%k3>KLZ~!P%"NCxIN1/?av =+unx\>;t/!l9%3 T^?\;\I.F־uY#2-Rc@roCq7\|ΟeǬ*_OxPhFT3d C.'Ϝ%b/fG%`w8fxlO23q' nsqwf[!3Y\(`5y@)!άkL l4!A2eۚx^>O[Ͽq{7?>KA`݀k˫qqF w&pӵ9(;2M#SfXiY! x|W˿lx]:+v3™ІsCg d&]  bchScSI :8;^lAIYY 2hR BxXsYO: BgQl =J8"mz7vgԲd('\7 27<,L@zʈp$ Z_zEa x3Bg/2W06TMxi@6G=a e6w9w[檔UğB_¦D7{=/&GQOoB&,ƶ( 6K|v85+d0n"4tuWQケ7%^%~[VE "U"'/N(šy/kw5&m+y\G#4}%c_'O^+dݗ~?OkIe7ʈ'jr٨ZJ''ߦ0d/{[fiAdF-r*,ɥ t?15FI\O qmP0-*ElK l5=J5ԭbwXHa6{HVSBX&t« w c͊li󲕉8-rDTZ%KNCE*zN:Oe&rQ9*& "mRM5=hOM{ľCDt)ƪ g"ȌY/ _jF\ tg[j Õ俈,*YJDnX \[o~+&_R|i%=0Q&r>!/N~oB͎ȧO+ׅ ѠLA V5Ɓe!uS!5 A8j0 |1Ut34/|43 E 4~|,Ή庵u(nfE!0ޠP)'X(sl8;›K2O3ӝٝI<@0ĉ3 詿ԥUq r%;8D8&VQ\L@ؠax /Nomow# m#<#4N7kO|Gnػd58$ хLRiHEiEh8iHs$l")d,ƝW,KqWFt?x6#k~iG`{9]^_]¢=U+~q8vw`.dGǐTM3\ 1=ũRB`붮g;/g:-/Vb?U^c>9*Xh쵓DJ29!O"CEFP;|{4a26EguN#fvְH`!j^ƫҚ,^Ъ̌t.9P0s@]Y]W1,-&y1kwT5& gX\5d j'gB,,CQ2hCh?Y?$bR>䍯qk*JI^P0ؠEəYzE4EN_'Mq9L̸XZ;iI~9}miQd jG<a߁̗ !\3/GhI<`ya?fpGtЁ\ F]uPX&VXi=;5B$eQ4Lw6X4ٲ ttsUa hJ3XHX"&e`\"}[U"8drT})8yMD-/]eBC?[\-jpoǿ}zڕA #60SYnӠ {0 6@Q f^LvU`p_u+B Uj4xٔxhy t ^bK,mX"݈l[f|j[Y&!.=,+~X!:Q b w6"/mpޘ21|2;P farR$|<* %sk[hwrrsgDqGު| bjH&7FeFXO&2= c蜘9hveq k_܋;؉AkqP.^|Wn߹'tǢ POf+ʆ[#wMC0?/, _G%'I;iϯ7HcZJѿ?Ͽ2Ȼ]^;/|Smpڦԛ&3V֟hrX4G((fuv{E/<(Np fNE6 ,A0ntsQ)R3.*SurHO),gNQx{jAon"%ݵUjR3qi~*XCG8FqaJB˳\[.6Ųu\{Ŧ6:Vhʖ{YSB_E貐_aKXjwMQe)B*E#Y(5$b_&sp$J*RڥXp^o|P8xt`h0:l^n?G ٟA$* Ж|سO+8=d&h9TS6YKxw?[72^3P͉UN,n $hhЦ.PHLȶwo[w0ύbN4X1픐%؂` |l-0dXPzD21Խw NY~ʫ .6N~KO_G>˿\pc@QHa=:VWIS4}&[^ gG{âd)#Z¹fTGa]^Rl&onfQtwgfY}ϝ9)ǪnZ $#mL=aaY ғvveˁA8lf4`!݌=TWUWgUfLw>ʮ*=k3vڽQ,I 80iWߍy3RTt2dZ*S-Y! QMV<f ֩6x\Kg5jɕW2~hdrI~HZ!,S'JI^_'mQ4 2[vY.Vpn9N΁B#bjb9W4α ZFRx M4Bn80 ^1h ÊH8K|w3OQtF}N,&~:{+D]D 2e @GZaKQ9i*> V *K:&J#2+>4^?JRlWiw\n4G<6z((曹Az|I1ZP'ΗDힵldf|D ?6 @!?k>h:mE=*̙- UtDH3ںylʼn$%:Q2$ﲭÖ{whmB<,H82OaAI*8;LMݱ^1Fvev=b7AYJ?U*B蛞%4]&hksMSn{hzdeA$[ {wGEYgQ ".N̼x I CnO4ȝW˚p"ؒ` llI/yNeqxk'p]\,kIR9>ʾEVK8  5K N]e&P|~F<˓D5ެ@2} Ds]I1("n$ %S`Vji^kG'8s$AgyK(mHf$@YY?܍0UuŢ(YVX Quj"A,|BH/XZ2!**8k^V/i8m!ڊBY ז.xdXDrzRC/Z2//{ TbaʷT Fnps*@?Y7nd%e{,idiqQmTUA̶Cj"pTxA/,DyhZ8?Q;bk狸tki`:X:xC\\B^ĐVέLaMWV7AO9=OaW,ZгGT[bW}0=ؿ}[\ Tښjޅ*/fosX?3j\OreuD/}q{<=;ցX EQߊsUTB?x[W@]>WJ!MFxHdB-e&[gjxuɥ^u@A,QHc!A9xS+eq{Jà\4@0&?@Y 7i_$`)K3.~P uq+"6fwIW9B'Г <6!$B\B ,UyHl&O3KsNͩ$m[p| W46)]0-WLÇb*/[[>=J GگSX0qKp,N˻83ʂ*d![[ !VcNF1M0C0Ô#9*x1C҈Ml9Ę#w9D;d}mna#HH"8(rB _=ۋ+bpgm{n؇ޢ'3&6h Ȥ}`(0QJ#uP;Gv'[ۛse<(5x߾ov6r 8;6VG>^S'jTGן~o}[]ovTf{SqՕG)bv d;|O}w>I̩X/E |![8}pn㼏OM͍Pù,.¦mnȢRc+~`tL~c eZfa"d`*.| v DRrAsK*pm0E\pϡrnhTqӤj>d u*G-n\ 5IP4˻g謟Au%{EU-=Dz}H=q - %KjL6-egA24U*`EmG-js1 mO3@ץ)) 6R*Jd?tih Y{4-~θxF.ئЮ5,BUމg"rӵ w74 8MNThbG2Ms""%X|8:."?szItU`^ë MÖ =˭E8{4ut:XB/:"olm:&A4Z3IŋMOEJtQĻ{dOI.<=\|O-7?oLԳ[͎L ciENyY~~ _yJ&%؀S\$p,Z!)kg?l3 hb끞}AΪVj0Yj% 7k"PH MbS ~"2g\gS\W LOx}P{U [ќ@[UΠ #1%,{. }_d^]ޮ/J/!2 C\\)tLrQZrqL?O)M]B3zkIL@ׯ+Ū?kq=lA͗%>,~D\WLELsѼMm"iԾLDQ,Pu$)|zzF3:+}g0ӯ˚=eY]/ !tgP)/uwELS\ jI>cY[KʯBFZ+}JDJC Y>uO/> ۟FjDjUCu%Fz5*R7DI]#\3o|*4p8y[ 5uAn4%7l'jnهpK;#E!9ϟǻ?IfN4HŃQڀT}dn64X.ڲIB{&COd!$d95^RMD FzeEǵ>4w"R2],7Ň+Rx κϼT5#p!&bL "1kVd%6/.a`j8`l6)?B}z4{ȶ͡^<~I4T ѫYAzEI&R>)I Rl^RjSW x_ .'}rk5 CKN)˺GjU΁:1 rJVe >*_ٖYᾀ8 Ì۽3& @t^=hpCmT XmeA=][DzrԃC |)r"eB [dv!;P X"* 5+Ǭl)w~]k5OclV<6ՃYTfR(s>5H [HkCƘ:Q^1L@ʧ( ,-EU\ 2JŬ $oN^Dm6侼Ashޅ04}K6Z +WRAl50 N!n6ՠ^]es$طp6*FOujս6  KU1 ǦDAcr f`d@#WO:s„tA P®& `hr.pL`xKBz9 =j2|WQ"JI&b,_R*sU ZX) qg@޹j%nZha݌Bm:5ŹXxC,='vhim=C˶;]4axܹ\] Qj&E!Yr&q>1d::ٌ~oUdS qÚ V{Ev*?lGxh<qsk3ܥӣӣٚ8Z*&YFvpnOZ H8w,n׸_f{Tg,#m?wӆTHf9)bv[h~$DK+wo|凿X?=e 0fBJyKֱHP$Nse|fMy)W U "X:W 5f68-$%2 `r [#no6 ̧s`S@ .jϑ\ْ?hVטdaNǂ ,VrwDOqgB|S^RB@z[2CF5I4l)3d $_ & Iob)!~)lɘG&DͲY)]&iVc7\S4px([#1l]9=&Bhmִr-SxWlu%v@H, a/!O#쨚_fVeg\S9V؆% vkܣFl?`\ l:1"Y`}A֪r^@ QO(u={Ub/RtסTg*- 䍳s~2uš=鶶 O땩$y vJQ5 1x#1?Fzhv9Yzz[fJ2ZYڝ80)fG N&nY E{bv͋Sz)oS5~$5뙃8ZY cT奝lx0S&x 㐏՗Ƣ mvPbú -K̥( a?..;77@k*$WU$%d0ė7B!-v@"Y"pV,< kAH9fSԠ12cEgG/0ˤ֬H5ݑ43; x135aE P?2x@"JgPa6m@!ܣQ+2cpyh&9-43q/u.5Sj N t)-ow-SX&Pt(*6YkG}6A姫{[Qe&3L"d)5`}45ku!֡@/Q]9q&L.ó UHH6qƘdt֋;9k{{WvXR8AY%̞qy(A0zF&#lPh( ebt_R{jB@`U G-Zr#!6p kJbx ْ)T9pf\B0ãx&&„eCnȓE` Vie|l/HNK!(Ȃa#N*zڬB5u b-MXC] m|I1=(׳\=a.{>h?œqo$^# m\GkMѠzӞ oL`";yDؐ9#('-KSjC/++HM4"xx n8MrܷBZ 2#G{k gSё<[LU/HwV0-^j+3BbWJ鋡j7~*=sʣUlFejSUaF U(kP\v7#,"ERTP|@h2*VAC%QToK275UϙAR6B/2f:ɊO- Qt^E:G9ȹT@YHddˇ(XsrDS (󊠬, dKMI9HyPa9 $؈2ja}eigc=)e9튫u90ynYV0HVZwyv[HĆtdʈmAiV^ #$`G r d-\1Á,X؅&PNqC2O)" _Fٴ-f1ˀ;ڃz#|A`Z_1}cwqVr555a_7STf)rPii^ <"S+ - 3kuB1v_K2IkWI?XIpΌP5 ~M`)m!z9 卄*,}#E.K.zFH/m2.r5=HjdkK,^[k%;]fIl+JC&zȢi#q(TUꯔJ%X:k0t2c1ͮ0݈nLB`b[ L GP Ӗѫ/v[ E|Gh:!\-Z4M CS^%)KAM-[MC[4)uizOŪIAhQЅ ]O +9nfȋi$F!Aɐܖ0x!:AHI~3;`;;ogՕfw]ۘzD.=QȒ0[X}a$=Ey$zMJ~spo(41y0-]'lB8g*=ύ gLƲsD&lgaG@Щd`=K+N0L4S;>zb ౱zC?XI", |5Xޔ4iy5.ٱ=,# (شf?1R%˼S)do'']{Сtˆz ]@vlȉB@Ab2AwO\%3%FGD⍆;'ˎꂽXd{W(c%#'CHޣrT!=fv00./76 bB kV7 ceW^ԆURNSKI$Jk IsQqyN'U+ ;A0:0RZP\)U4fE*J練Y.Ij, # 5+CxȨ'¤Q:H@}R=dtj1pf*ιpP8m#_!1{qƶ#kQR,$|,ҴW c"B2qL N|\JٗNA@XD<&S yRM:(% ->TV%:_8wcL78!ki];0L&0Sձi} \lr ՖѴ2G>c|m,GMlQ.;lԛXP&9Fa (@#os[MuT%̳eOsS)eYp?U}DiEu V@`oFCl@l1Mpf7 66PYx;<_{Ҁ\m6y/G |7|/yl֔y- iV41ic|:]EQ3PfY K0]y/7S KL8,GsphTSU!voIbBMdZ"%dW[UMԘ](IeVI!1SVlݑQrX5Qa$V=Hа&Av@m_IZ" PʝFxfZIzWxf ڍ4ވ^?xESZpN`@^oC-7q@`yǏ+Lp먠oI.8bWT aɂ-іdQK͸ha,kiU9,RM^5|.[_asQ Gv]Diқj!1x$"~D™Bc}T<#cG.)u.P{Sº*o B5ih߆ ^DilZ2B AE;^/^{&B΄Rֽr`p$T@Bw 'iCkQIk-k[}gl~ RPEOLyeJPy sNP0$fJҏTnY_::4d1cTgoa=9b, <cpNX6k-E)g {c($rc:d^P54)Oo ޠDykbTcՓAD\Z W6^ )cvKhhS_v؈ Lno"@8-h+8b}= R+# 5)IPhgXܵOG8QjON,Xy!-"h\Xm=zRմI0PNW{;+,N}ī|0]o[_^x@!z+O*B\j!Di:<Kƒ!)d,V8:=AJ@|D<Aw(*o$[MuIٲm=A$2BP$\*XHT-uoI_]*7tocUIc.֭1LOEYW h ]U9W-/7 qv_N-c([w1Id5cķ+NoUEr|{O(Ap*#`!ֹcΪ2~O,_hXEb'㣩JN96se&Z[ }l=|x7XOY`螋zffʩ7akUY F)pcD\a 6>획d0j-1Q常dkWP.R"K[j5S pQq U~?l2O% I$4[Pxoۭprd?#$quAks"Rt(z>4dNXE uDH`[[@ˑ;?<s>AH?/hk zcZy\%)k-fd]( dc-H(OPa[@T$gB3v+6sw&f 1Ԛ40і˫γҼt׆H$^ ģ8( V2_ƈtchrB8V\i?APR*_-$R;$k _KY4IjzD> wS ۅkx*uC3}ΫsܧX`hX9X#Cb(S49iBi$&K" E dr1LӸs7IY[Ҥz6og%2p>,*9ԥtࣖHHbZ@V`2YDІR29/[ל|4G?[#} ]N.[W1'ZGAP. "1 (׺v/EI~[- `iˋ~#UGc"D6.-l z( x4:_ZuƚW Ƈ j>Y$6Gh~$uifX@!*+2BfwMoNN5r8z9FqqK }Y&Pq=IL::Ermuгys`FvqJR q)bG͓W8mɼ_RI^8Ƅf[01d|I(?" 7x> ,Xl8Ơ3f DH x %FSib>{2ʩvâ(LTNG+g!2ykECI(mpB9-$)9[C5rAe;+K0"%PD*W0RQj<.r}~Y*$7EeMcOhG_ՊrAn% 7b=Tf)3&P4I jo~4")U$Z5FGLv40&*\HξApQb'd5t&"+4AT(5S'#fcxjIS_G4q8)3$񕔞"I+zSS ~)A[ʼԇϮ`O0ͼxR"+IvTҽsnF6ş1 =1e 31BIRqqRT%^hRd;>x3z 9@o|xmu>fAlBŠ1VW`o08:8ֽY)92L ;޶WJ:G`[<ord>*Y);"~~ǮB -%q3A}mX?_$+ Sa*j%O俐QD-h4R:#Av]Q%ܖ`Pwg>Ѳ{O^gpP&4.^ ~.ճҪȯrVfI u)f՗c8|%ڊૼ.\AI%G<"|f<f_r<yE3R ʣnmӵZ֯pbflzo[<x.D6d. Y @ WA %[I7 3t$` ' $*wS. #xhRk16-}A:&PTѰK[%4*XK 7f(%Y,- X#9eu/3<:ep4tl}Z&2yya<Mwc~ߥbHN֝}U:ʮv Ek qF7\i:) V" D~} <Ӳ$V )Ejd2_|Bjt#98~\s\Cj O[XQfYTouu#Ip@蘳a:DЖt]Aq-PrTN]fuAFT^ìEAjO4&e ծzWKV cH4J6E3>YD&aQ*'\a'jtGM'&J@ {oq5$=_J߫U{ՀM܀'U+OP%S1X]dz/Or L*;9y7@Jڃy8~3}k-nl:ASD4޲2<,MiXt.]sRޑX9Td$\alb4*pH :rJY/]ujgnQ0@_l{cCb C*$ GL f8*OD#n׾H(uo۲v $͝MXB ]+K}NGobZRƛs *Wi8AxoR6Qt<[v+5/SrOH>D{ЪJ^{~EKKvTOq!wyF0bB9xOyNuMt >4eO}5J(o2p37pnM^+uEI 3XQ|Ch1J}=0Uƒ %ؗY4/hD8e>)YiTݶhBޓ:R59}  SNH H|v4"rG@diG@c&ro?>}| dԖ,[L[X_73{blp֜y4v(z$J I~\jLe<2'fP.GWHݨ _D!4a[#fFNQp!7ʆ۠RF>9 |]ިYNb^Gr}{T_Jօ U1h;9_YxtӸ7c8pD|xrt:dc]LE&r>Jǜvt':(R'7 4l-X_EVu0-egtskuO6DP=TщH)xب8vxx9xݓB 9:;9qց; v>{߾}x*p?N ƣ|u ='?&t48 /[IZ Y›]vĴ@&@& ugrzb? chW/[$_Uŏ2!lϦ5k:ۖWuGR5c HK+Y枈gVbYEa]&q2t@(eF/aWVuɝ6FBnn:ig "!"k1X9頌Mnkk!kuʛlCwِIQbt-֝\ Uqx@`Ձ: DV2 Tu./c*RYӊ"?bIf(e%( 嗚bSQZZ)}c"ŕ*{'T Ж<^>:C9P4CB+ldfID{0 NH@h@ЂL K+DX#9G'~A˘5 ^Jٷ,ңtj{ӷy3[3? B 6qĤ~Y/+}kf>1_[E/ ;/l}g}lz-."~go=!9|A!ef! '}Vh-"(hBfLbElr_kع%蘁ysaQF:[|nv 6o?읐>K^Aq~]+v;$r/w߹PK. FM `s+%2:kZ^"Q8$0+smLP†srBUTo{f^Ǭc_THT:'90"jy4Z;m$\-.!JM"oPC7kvL%P`h3߳Sb zIcMtQvX9KG| W~Wp٭ łw{#n87f6mJeM9̎XΒѡ9^h@hdU2™'C,'b\"E‚x쮓G޺>؜WSdv=.Hi0WCf";wo.[C{Fv*`xt>&%IZ`fQ$gE4zO_3O?NdַZבEESs5@/$s-laqi崶xva-DO2%hmM5Q+r.tq G^.z Zx4%ʹ9IrcY |D%/DZ hV,:g\Vq %Hi-j ^/4+l :?cQQ) Osd]Z qp7kYƫW&'~2W]K +TZGV ZO.g ֜<3 *lJ0LcYT^3W bL2{l%)8REiqTz^`m# X  =ًE 4kxN7X43sdZkR'>0 [1$K9ZC6LO2EC(8x EAӏY q'Qe4n%$`?R #cWZ0~?eWّ[22j'%sMR[Xg/|#pˡ6q6lg_Ce,-RӀ6ԧ=^R%?%|J(YF MO~=λ,|_}Nwu:ݹ}pt2;VjlvR$lAba`.v( 7OS"$&E )kg mI 2""7N*!19???aES._Ɏ? %xdҐ|woYEd⛥uu-f)Q/óf5dcw;lʊ%GhP&Apu!oޭ=~r'MsIffK0#ѵ^Ƴ^&(E*R(9ksHȀin֡˜hn  IYQ5Bڠ^"ZqܡX޷)ʛ|Lйnfh\IZBQt OzW%Gr# J &1EE84?_dPC@}ZqYM6CWV ?E0:APtj .0(ي~';&o/{{ q6/ Sm`|'6)Ϊrխu(Z 1Lp'Q?faѻ?_LO2i"؀0ɺFOg >;;ze w^=6踄WWqMWeMWWڃ{>OY /DĬ, Fh=nG :m DPE^džCZ,1& $ܮr&wNQ&d[IhAk?bށD;7ϟX%^SI^@!0IK]@s~$fxFp(l;x¢WNCي.OOS0l.(LEA0Yۻ!4 OѰZ( R$>fF|i2%-i`+uW՘SB^//Ycf$ܩ?WEyY}!.'ÑnXAq^,oAyGx-6$tZQhr)4H(H;J:'ƘkFE$V<! g*Jh4=b:_[8Nn3lŝspw(وd@Hq98`Ӆ-- 3 [_SѠ%Yh4T%,-hiu^!DOߐŏ~NjQ `SE6:.-BY#g1kxrdm%N?w_u]optFf}<k"JM8ll?:Yw D H 7i(Şk?Y-5½xT,-N˻?{94./П~_%Na\G]*-R/*B.?NMCHRܿjJ3E0.Y~o@<!iԏ)Yx=Fqv_*^DW]PFX} +-BN^DKYyzv`oT!Ie+ HKW@2e)6hޠv2JXFL{&eE5_4<}YU+QHi3^X$s'hzrSf"l[DrϺoKN ;0=$ίb $t E+=)HeM!-uHH\lxE@QB> "Z{6ք0띜EZ[(6e># {WLwh*6KEӝB,aow '~B(ꇣd k;.2v)7m.s` e .fm^x&bk"pvYI ;6jrWi>^c9c_G뛅W:ۻO~n~37e7e!b#1+j' :(vd{c$8`6lZ2VbbTws_yc˓޺x ^ta{Ͽ{ρNso?O[&Xc1- sܿ(eF.B\!CU܆YEC%.=0*Md٭lӞD aX>gT2ɹcΒu(ve&Ŏb[rM5Dage;aZ_g6RKڠlf9/'Z1`D\ [w1Eolf00[ĉ[3fԃ2{:iYσ% 5Iiֱ⸦:ZH)7QI 2|٫ǿL$"HO->mn%%n1@v@]']'F21Q ]_2Wd:Χ^xg+߾5}tr}?9}^z*Fv!V^0 _yymuq^ޛWø$(VX26~:# 2]Cys5AbwerԾz[rc2F;EšDL3.5 HSHMVIQ\I(~Oj%kg-) 1$r~[')M erf[Fn# L@AsQ5g:WGjUfq58+ߏd3gDO7.eBK2RHRT]Rl ҸkFvuI(Y_(=6D$(@.,Ms!g@Ycl{ v bT# RHaVō~kc#ȫgzcIˉ͡:<2A/VǧXAO< Dn. UJYv-1cfd/ Kow7V/ػp~h<f8zhwgs8&fle{?+،*0\=k@5}C2r,47KU3S8tĄ02-gs,Y VF'.Bbl*1!)@^h<qBA aӸHx'j}7z$8M܎xᡖ c|Sph|u4vn0 DUr:q3rV:p%"='84om1'Â@GsB)0ohz(WM13!5R|xf2 qrv|$-hiZ8ZTϴ]'&vtg>4֌.Ө22hi!>f Q!,Kvr\8fkOF6STvlh7R%5a"xreš I:O@ PJє&9;ZPQ |fE$Di.L9Uu~K˨eB+K;Œh\NC}e,N`zS&-~c*B \N+[ fDOFH/fs/}7:_ww9:1}qa).$N7lu63Հ9}f? PsAie ޠA^|BHHpޅQQl'r@ 6JDqR VG>N'4-U7I^Z.A .3m(OΈCxP.tUw@Oʔ|™ÀQ,O_~Q Uuiy 0}\M*$+ρԐEmWZA_%קt4=z_?}ĕ13(+ *1%@bӟIj{+DTTk%_mXcD,%5ȩcxg}qgQNZrBʍM9̴ h2 h~dY(݈y L&ҠaŞǻqQIҿ#P@G9kzcs2QR:]b拳E 'BǎtZg}[1jOLIa==Cy;^^Lˠ$xdoQ}aP̲E4, f1uc||_3g |{9 L,:7/i6 09>8Y8Y~^qL%(m^OѻxseNKCvҌX+_W2*"!H>ǰQO鬵":2:(xB59cy<#~G|I} iu>DD!C0o'@SZ\dMF6eg=9 )~1d?X ,F/4g"x,-4G :OFR|$g^'8JYLEnbՕae1qi&>,-#=iPD_ 42;pmn@K^ `k !ڌ="EY3*GkXiʲ$iK=cdD(_Ir >+bW{C.\bEPw]/|E09Dd5`zOxd_MNa6kgCť,ۯ%6hꍵsNjkv4{"Ur _8| +P/m;~w.;=?֟s/vS/KVu@0Yߌ&0V.NxeO3'f{\gKF 1b btX'=V?\Y*ۏ&L}(:aOd}`!7D[ם5ӝNsk!G/P] !ha@]ʊ@./!@LI|m#e,J/「r^VtjmJ5V'tx\FBZfƑ@o崪sYS{pR!(ڐf oZ۔PW.lԥFK-ND$6~T[ow9#hz[b (Yմ3g3F@9&FbYٖ;̞oO"3'&kx%k?O:wE@]n;PNAR1dV 2;We<Q@t-Fd.YWۃ Gc. g&96i$ZȻ'͟ku[_F8?o̧7Ugwx>Oy"ɟ !,3 oϦd"i$F*ߣF6ɂsZ13ŋ~| o.Y vK{$ }}giԚ..-4ӬvSk`?*N?f]&vM^* O8)PTX@n̺&p4't=ih辌/ۖ.>f6.…@qYRwM0OYNjD"r񫧵lq#qCZvԈp}Urۡ?!4ouOB^+1a2 3e{ıq6 P̲ª9+É́]28bdC3A=+:GW J[} 8X٩D|CE Mm@(+lNh%am܎-5f.ͻ݇ dzgko{&WCxs/ k ZOpcE묲 29(OpΠs`6 :ыN:qsbbWUU7>D$$) NJ-lD \D % \"M#@(r8&19zYUߴs{]Yڥ[_! >Gc9Ov07PK˥+i',ŷSӪ'e†Pl6q;$$#F$%e_?,1!Kj#Ol7SWpU\tȕO0Ceo+ZrGz}vxÇ#ԙԪ \Pt%mn#pb\bGN;5[v-{.indd@qq|ԇ5F̮_.+v29wzAL~A>xV@]([)"/1Þ0tF([ZU]ʄ;%?V{%HV#adV- w^.ml@(w9\W"ʱL{$Pc6.IKa*nkYe6sA^B]P }/[vBj2&"jtd Y$*&TmRoz+\-]66I)L+=44Wro6qs>Qӧ?pӱr}4Jzֵw?ݿO&WgpRØC,%QM`S1X*6 0$I HQ6L*Gd0% } xXV,,6=@]-f2(3aj-6pg(Lo5|V ,ٔ]Y7e T&1` -!GU 06w (}g6s5ODNf4HXvF{E'>U tɑӑ< b3BBV>84޺b˛WCN=dƺ)ݴ+jALIRj' WpZGt-p1+:9.2.$Tp$rڣyx2mjԜ[Db{0+?/~VK5#ʯq87(Y&/WiNTWKC2X9dĘPnwx+l$P(:7DrS V"ѭJ_Vo!7Q.\-q.LW!1pfXy1Җ.충;IFfx^w-EZz{ ޓbD?YbUng/=06FW)L#U>|SL""CMOeQgo</Ս=*l"{`lPNK0$!,Ҍ%ebeR}J !v-A iRy5/x~"usW.|ΰnb@zYPQTA~sdyT<+Gfr3e]" mMEpq]c~O[X2-聿dO75- 8u7[%xykO9N/X; UfÑ 45Q +'] G|Aн,+۠~E?ďA/0+fݾM^-a"'v]ڠS_ՆC<;]  $"Sxt=\L-.X6qx N"g-tDAJSLEbC)t,-*LS6Dq^/Umy_zX ;{ 9UQZe*Vܱ<^dgB*lLM= :Q+Hd9n $NP 7^ք/Iް<1X7QJRcmV*8`X>j1Rh#LG5w݉z*FZ3Ƞ)Ll /Uk΅F'KW˟؜7#eW{Rvv?w_|N&Gv ۩{rDP8OZ͝_u e8qS:jy( ̈\:>9/QvyŞ+uDP(5-/ԯ`^1q}҆OPdI9Ofۗwy _#IP%f|%lq>o۽齾dHӺbv}O/ ,7—Xin$TCjpS}Fl'G'_o<@^\ />_h7xhf=zVIZ#Vx >[W*I|0X2/1GQ|r\aoohr# oƝG?:oj3[YT%P v+C GM}YsicH)!a@sD"<\ R̀ H?.Y6lJÉH!vҺjU#C"SuD :hOjU#Z{F8QΰAߧjp%d$iB_zwto-~8c&t`#oCD_z 7=$S(W?lO_p6 pF9m:ϩ7w{z̡cY.F5斖M`n'r4$Jdc&З[“SJmr`{0]x_mS{ͭU)Bà[ $4}E13sCL26;d a'6]kx2A\B95RLqr+&Q&HX\=dzڥpm "aOZLyM{{>05Sݿg[^'h}Gcol֨B BdD9n MY7HÍ)DD8SI5}ec AhVǵ|:KF@QB9Cln~cYvsge2}kFhArnp1HNx\o#e]j8buP# VT+U㫪pmYId!Bv2K% TiNsl@8oy8kMv?uR˙<h~#ћ5E镪4sv! "0,p;} 脏ŘrjىCPbMf) [5WDmm7Y!CȐ8dL<ꣃ?F+@^n5|=&:ϕ×#[̥3 A'$ڕIHl@cS*j.[Zev[J啳mf5f]|fH=X:=܅7(D P899WAJ\aٞ }+2{!`qڵLpKÍL!i _B.CSw4C(rOFsf62ׄl6x `pptp0vpjJͅß W0 DGŧlV£GU8FB 0У(Kz+JEJ'AS}3 %U3DTi8ÊҨ/U^焷7*# e DRQk'Opr7՘f4C] $K>Kg6BE NO@cKC*#mޞ%`N&<mV g>S> 2hV.z^ ~i( 2@Sh1)@eRsՆv300Y.k3DO4`Z{V: ʕq@,ɸ5#"c>s+F~Jʱi&[Ж Lw3cȚҺdݿ|HW[xR Ti+cO.w7$#6mM|wn)S!eAb\-!1f׾f4ڨHFRڨ܋7}zU^oʣL赹r4p,oĬ9S 9*ED$ V6=x1Y6*B= bkKVsi0PΠPgis_7G0^oR"pݫ(G^>>DC@DI~(^{`DzMNeh@={)"BG6s7? ȞB9$54lyK=351&N *h),#oʔarD b>0?u*KQrJOVzWBFMXF$l.Ubvٚ4tz'wJ y7rufq^`2>sDYGxHe2?sx*`oӫf- H3戤kXE{BA6b8q;&P$IobB[P^3RAPLƙ닭Q2!"V; "Yd×St&ݙ5ӜnIl*zFu#W!p+5!kDzeUCk7JbX^7sZӚl}}rr}ZD'5Y{3`A\~\0&S4;o~'t3O9]+@oʯy!G&v)t)vYZɟRO+פ*p n.Qݑi1>'qkZ  piMNWNc,4[-yny +OT!vDaņ<L!vTHg|p} t$ WZp_ݚEttJ5+^#E.'),J`Mw87v=)tF=ȱVC5 /"'er+*1CP=ݹȺ+GP1]ly9g!K& N w2Ioćqzse,OuEy6OjCp=u?sIS_[. o>l4cW:]c}<>VfA[g%W=4ƤHoc#fK6%p1g%7!^l8I8=L{(t2`c5$ && udo}p}>_Z$f iG$_(&L5^)]/$d#0GO']oI/)o+΍˦ps{ f%@?E8&6:a&7Bil9gbH0inmL䀝ZR5T+pȝ:M%bz:߹y1OΝ6yCz! ~48->_gt>k[.%85T0:-/Q]bP]!%^(TX;`Ԥjw#f A7k+vDhw3 _JD sŃ[awM\ 2?ҟVqyzRgrUT^}3˦7ͷ@IDATfЧ&r4 y fCM[K#@ d$sR.h'͆ fDIƙgͩ}`.ō#f8U5zW+wD[{] {{y/Ⱦ=Z7GA.G'bY-3"5GJyH OMYUS,ޘqeAy_eV!ULD\^#WoLN[˛Gn0Ӝ] ܓ=wHM}֑ vq` Nݝ"ucȫ\+Ínř$Di ,[0 k@pTS,/ֿ0eec(L%-0Pd:,#l0CZJ@4nBl&ҟa6^FNá畯ZJ_47)8vwg'9h{{9i*qTz7Z>I4}ɖnեWD=sMS(!E^R+U]]9: k`\6 eF} 3D;ڟbz5!%VCH, D52_U}A30ѫGِqx%txH n(a֓hbPY(8}@B YW2ki8EBR?jW{o7EM=9:>;*p)>וx|DAua]U%3S%nWi ha[01awbpx^b)윃|@ [Ƣ+j Ơ76~ݰȗf㉪ؘʅV(K|؜cv% 9^#RFM*FZ#+6{[?5j{ܼ~dn85ʦ2P%l nqZ3eh8a: j6u#BZW!V4JJDDOtmKvem7üy9/S3,6tcvOS.HD溈%!VE ~F.%:L= KcSt) DZzVÝP<Yb* !~%kĥ7M34>@87Z[-Ĺ8틵%D.xNs@s~逸QI>TD(ȴ(#F^rhQa3BFuUlk)+c"W*op&rn.lş~SI>׊Z1jEț>mCa W=VF=皹!H~-B?5ƬgѱW/}|C\ 8PzW=?۬U"u4:~ﱿõ7߸/@& Q/(Qi$=jB43% <g7CBjksX[2߰yW|=u3ƍ푆j)Ol8bnױ{K9vvLz~5]G){Dquga9 ;z4mn !ӕA0"Gz 2{&o5Reu v謡etX.D'?oyŻ[(U/a>?ndogww{{7&_Źfvz bea qJN0(N}+JXv9 "By8U Vs. J=yMB^X;'}Y/$zԾ񰵙nvѰaG[C#'S$Ci;+]nt͈I$4LŒ蛠,KeuZ^mz-he6xws@E)_Ӎy@%krcs)[\'7v_Ȭ5XγD~ecpf]PԔ|'bGZ7xu. RrmD!\bdCV: .~N0>8Jmu'§[=/nS2`o_+ܪ&%\G{~!=r0\}筇*c'f71Q/vh![XaA"*k%{ts1%cf ѵw!J݈f䳿>Azpjx:o 0l/үگ"/ŧExF _U$N[ 6QId,SoyI T0prmx55=O7r va`7],de=I,y!cë{54!J0 eB$ٲwdˑ60nGs/Y\|DC8a7q `;X A@14,{'| ӎyi^ML%rgzNFuT5-( ?.%bb&.[ca`1%Zi1I0-0奓1r)t$b._@؀*y{ gD[?` ȘP3"lQь{rv I2__Ĝ>dט=.K*>5_$UTx+ZV׿ۀ.[x7z5wÞ-w*< !g Sv2u6ڠp{df ú=6\סcisdW 0r*`=x˸zA{0asK[sgLG/l-SSkݦEE` ܞ6O@EUt{ -/`Up|ç/?*NimYbAtZLznFC!8Hj`~f .Q>'E+W~4_yqU%9?=?$9 ׄERuM0žјAHl l#L 9F^ X%,1xk8V D5ZbB`X W l2= laWC5H>ƬB^1tM&ER.6%"^`C黯׺=%Rc2lpկBb֯4A0"<4DU5 -;o4E sO?5sKfxS"e5u&"&+tKP3А`&Wrtb^VwEȴ'+\^}]?r lnF1ÌP;Jw& UPj)a% l ۪҃ҠWϞ>}S,יvѵxMqˊBymZm=$onoQZ9̠Vdi[ʪp|s%!dKPa pP%v+p|(/s$SOٟg6\1O{w~{R_bk|odN.{,p@/gWŝsjXhcḤbC.(!eJ[fˤ=MFO9Ů$Omz߷;LffU66rivru/˔|`˞T?zATT7 =] Ƕb7!C|j89 ~ɭ-gʴ'}./?;[ =MÁah48zQ?=aY-PXI7Z8b|'tz G|ocj1Mn h0v"8Yb0eqū7͛+Og:Hq2(?8QLew{g?yV$c}Ex PDD5opY+YL.Es0k4JGCC( DYWoλ71E?W/ 4 jkdڳ*;,Ic}ě2q<Ȗ6'}BVm&`[[>) }pylnAD[LY̦6G:mZ8&kfˣG{㉌&Ż`vbhq:3pw IR>s"7ÇȘJP~+VjlthB"!e4KJ2zU+5_Ew&r5#rMuy/O%\:Dt1tR/_VADhszGSQs6IX\PT*Yu@=oegdՑ@xkfU[N!mG9/dZRviqdЭ/5x eGS\-;&?eI~o~=ZY :le>V/}877v$X+Uظ rdٹxU_f;녗x~gF[&Qvm[ OŅLW/?:|4y%@r! o>Y>bhOг ¬X\8yq  5@Y4ofPjmh8k2x˞dwRË?,\G! w.dFdBL/0-R{܈ڪޤ&Uz :`]TKmm-*1H%޷\캁bF$kzPz2],{ |fzZVmg#qN3L'V0hS{0./k)^ƈAg"˪m&C=nc2Vb,E@oafҳr<\r !GfqCᒐ0b.; ]arA0Ui\8p$W@γ^vxXiMrap*#YY.գTXK&ـj>kZ\s؃—K{kr8#+嘾Р%6"^nc`5->qEڰ4^È,#9]Ms ':,x=ȇͫ][~j Ԇ'"6c tÍK)H{ yda- 09'!'T#Ca!2"aW; DE(\P-!q~?&QW[9(>ᤓg7kP4NSnU 0AjIlWX08#:=gYGr촿>ۦc} W>}!Mk5( Xh7/-9DCrL@<9B, A3^T#hBeC<1"+@دj}Q.(v8?L|~ؽ81k*/41& /nZhp1@M«@Fjh.IρN#togB7?b{[{)I9,WS;OHn[͝|Iz/ 78vn`>:'h4}sa Qݥ;EƉ(T@`pw|TcN#ǡ-8X0b/ =T'ja3תi1Ā89fs~SI+Z)Rlb[JbҲ{Y (W\`!&IEb?x+m5t%s05 D άy>SF[I(x2 2˯:Ǔ?)P6x'#5 m6Q|\,qՋDOIuK[ 6J]$8Oit"3̞1T]K2;<r09+L{ (N ~C.),l[WO)7}+. dH&\9ż1}49o^AޝӃg{ifrLgLH{wDFCV4Ƶ\B+y۲,ߝw F 8Ȧ HjBikwf1+' gd s@U J< :=.?=>CN׭6=~!J"V>lY[AjoB׿ 1.cC)hևܙ8A=9e2XZ93g3*/N<\D% =dr._xgZBW+Vvȸ2`^jOL q٦0ԵoplA7 ,X5$p)$&:3'%Q763Q'SW{%z) VۺBp JIFȸ[..<ˎuN|tkRMTԽ`2 UW&in57=W$cտ@5--Jxsv&{Y ~: SA-R \I#Bn`k _ѻ-%όLhX]? W-'K|É<G1\nn><&j(@&ƒa?A`PZ}`QJ+;yG*,C.냧` }=4ཱྀl5=XH1MM.)TXbu0{iyvp+ll VuauGed!<@N[?S0h_˱~&UBDٔ^4ή[X1 EryMs2VU_q0m(>>0pU8"rt1O־ cu3x`%TҀ6QQ%LC-c@'O?}tEVl?8v0O8Q@gllRU9 chJ1 %`vyx<$SǶ$N$`jP g D@E=oĀjpwը!*~Z,3bxYPI\`g'7ޤoAXo֒6M 6tнpBX' CטK%T}B!Ł1@svJ_v3X؄Dr0W"BxuF7A2  )2ґLu86duĕx5dM2 uO)0AC% rAb!Wz++NK> c 5/J\ICh'r˺U[͹plM³ 9>U[3!QiZ]ˍ.rPO*`I뛁L8z}pBTl*x* \!_ B.yN)@P^54d,f(1FphP!pUAļ Ç,%{y'tpW>Arx1m:i(Ss&Abb4hϮQ1t~x_~qr kS}{ XQxa DS=8;ri%s">:0KA`M7imXSrXN]OrU$i7'Ն!4ISviLЂo[Q,h5Rm4*O^+IAt-F'@I*|J(H,Gg@O̊Z3ã1RcרqHeFՊa-l2C.<]J/wf\H&ݾ2|uc-|,Kn2g$}0?U(\ {?>+/!yEbxa EP8k1<@$^̦(D.noAsǬynxQ4{~Z"j_ů*K @,a5mX[(ד;ӢmҞg o?߿_]p=Dlk@/{`8vȾ"<B{\N XP1* nOsdO{s9*9OTد<ŁR2; oBLzX"l sYjvJ1Y-Hx^-N Ch U")P,Fی3+z0j=, ? *$Yy rp('P\ů3HMEE9,E%h;*wqn %Qu|TEf,JxYj/5ģR:V͚¼ryvz7L^p0Jl5謡[h 1FG;!bT+CKC֨iT~n֚a*C(YP'J^:TiQ gj7O% 1Z2"IWZ Yim,|Y=`hP( *LiTӾ) ϡʸ<7nzeCV_*$KWG)_R_KʻR3LC@\$q2)=EsW0 y1w>R]kzƷ-X#baEfEegN.>K.{WmJ|=ԏ}띁lJ''lvp8"r鑔bF(T0aM(1?I<ɗԽԙk#:}ѲlgO&G,yj}t. 9# 'h̬|! @'yd+QO1ó&s`Xά {s82mī>y-3Y~V|scyDj8'"s| Ux]*W7DpwUkvDLHZ$d,¢C=AXqGgÁ/jY漴fB 2ށIKk(M]$#8`MwE8 @2 ڑeE C +ڐʒ.ݲA9P(^ݍ"WVV~9N@rj(f4=>’"Pr 뀛aA#v?gg!.;|}Qm fu{u`I}in5ccTZw}IU a(&_# x[˂YWA@d[IG5A&G>k]5AbLncsr8L?TȌOj/rRDi&{(aH?U*j^ٳgSm;CD!oF.3 kgsJrfJ6(GojN"ޜQ2vӟzXVmI-` BKm{ '0eQC'4Ե_ꁍ w{C'؏'["kNνQwS.'`*A@cs7)Wis:А-Z5`/ZL++ûד(4ni׺][1Є a*;A+[Eo!I24l2Yڇj&3L_+`62O=pssM֍#dsŲ`y* |{7 WAӨKtޯW& $lȁHmC o]lJCwwah;:%+v6QvOfjy@qm$FOsV N%S!x(1R$Ƚ:D`7Iά 1um3ˮ?~`Rwu ` Oiy&6~ B{ցpe}Bz7^I""!ԙDh#V T,&$އ5Kbd^~[WRs!j Ⱦt'{em5c$Ӡ4"W5HlW6=-Y,Y'}Ǔ1C=XL"ʸQ ,l x%[|Q2–c2$ a4gGfYFVۑ(\`Εlbe,DC$B^񉔷Z& s:_WqqI¦ŰmQBfU{o I+^ߤNR” H22R-:Qqww! #e݀:LyE10jrEvq:#61O*cNнJDS桙=`mv-,NCeaUffPR|ܰL'O"w ug [7~?z4]y1on+9j!)#7 b \^n#OB !Mj\\+pld3o;F-TI*t'c!"Ѩݜ': l/.xqTk0/p}[u͐[FY9~+Z+R~Rb(͆ɧO(UOX)xAn8cQ8.n| daBo`QR#txAZ[}Wo;RZZKE%Ҹ&2Q2ɜy- °wͺ@;4n {lƈ ZqXnn>:"{3 JԣA"WH/r|jZ+ Qmu;+(C[vj `b/11ýCT ը/GI>I9|zb߽AǯD >z)K?܀ A)aDsӫ3.,oM)|1*>MM9yW'h[:5#L_ QFƶ0Eϥ|QkQ4 bh~s ?3.x*>*iǓ172{;}$ʄBh:G c\?f+tXi'(T yo_@&l18%_m1̈2hBӐ22 ?=MN|)͋ \ 0[C$ FVis'1ȿ:۳re'Fo>PW_SĚb#MkYH ӶdiudQgKrM g6iht(* 8:z~[w`?|Š|ywb*mqes iMd>zb-ߺ" 9TEz{(/knA@kcFQ3w[6َ0,@/B}TWf^rAd?UEs7f-A@tmT\=KT$~m/WZ=l T%yrBK"2yz,] 2gLϧ4i㜄8#^KMgɩ>6 ->}+e!iyQTWlu\"jEmuPz0y{U_0밞`Uv]nr|>1d|]W~M9n!BUpx! e[1A&ʹVRyPPm!twt#&t>P!~t7QMhT{b:fAM4-*k @dMwUq njIC;;VZ5T. kH=5g Sٞ]t,@ތh!BXz\ވT갼W WF`y EfRTo6(%yl0"r}e կZj( ݺg ]$щxaOΧGR}cE. jxX R5~E&KQdHd"ht*2GG~W橪_pnd,5PA`.4L ؍=QVx7v`R&Q8%gßíWܑ5s)+$Qq٣7!w jaۏ?x;U|񧟂|aQ!'} a( ؖ <ՐuF4_ 986L%[;)7Q[}o]qpg>vs))B]Ņ{X-֫.U{ ;_ V W(:7:|vW@qH/"<2T!>()Bv F30.50-J҉T} &*J'x4w)-@IDATJρQdoUO<+|נFyҋ(q)sU hE!š׉\o!C;.fQ(E5Bq``t9JI+>9*PVٹ*+v?4f¢oa0&ne2Vlc`ٜȘ>[2&?}6Ͳv"jBYAG+*+V\Cp4}ԃ~r`QI+A'Ӟ/~}Sy7A6O!/ =,o~G[ʼ0x`%8Wpm֍~da• B`!֗%Ʈ YB☂uu-iCz_{NyԪ&~^ ]s ŅIK 'ؑŗ~OYb-&4"!]CcUreR> _{ņ]fx, R|ָ#r+t)2ZǜUzmՆx-SokѾjprFc;jP̷H EOT!γxDH[?lj3ۡ9]ÇU]f҂qsiDB3EPX<1Ք{\^Er򌱠Q<Bд0VU=~Lb@zIK94cYrS]Tf'1bNKoJ&ܱB N ̥Ble ֐2REٔsn3\bjkn^6x/nJ:"X~F{ܳxČ J>8ǔ >$'3 ҇HVku3kW ML"#"t/ٸWٌgm!kdxFS堎/$w8:;Ғq~9NҔwkl)SXm,ЊhE &gUւ+Qٹ83Wp6 jq}`uVb@':7뜇(Nh%V)Gk'I9-2)t\Y־-)Zԝ0DQ{%cSTl.>1`01>|x4;NTcw㧎;ֶ:ZiBc\LV>*6] _fznRHطWwo7j{e-@s5ϓ8=K+b͍M?0W d>dB_t$Cafu ܺ3j[}ss. nuA ovrе %u8LZ+d RK/p{N>Ws{皭%:zSWn̜{p~e}G.>B7doem_'g×LqT\ UOBR|q5 Ptp RE  NW Du<G 4IKzRUPֶ bª6LK{_lus__q~4$;EqY\?zgx +`u{]s פν fqjНleS6Q(=sY.,Q!vS.ox˷7M~Sc]=; o CXބ~'`=yʝ͇⢇zpK<R|P `` `Vp̳xӥ6sQ+N͋_9ЋqًYkY8hȼDU"HI~ંLtuzB4tYxNJjļ>aI$$y?,${uUtOpHS- Hb Al@aa؆-')J@H)6)lgzzs̼yYյus-[_~_|'N-N8a?A,xʕJTIlU16qW~:hjn;GotVW.yM^%n 83&lk@5 %f wܢYF-3 E~߀*UY$KH.]h:{bU܀{:ɻJG#䂔v&ՠsO~K_|A?>]?~tM8C&N Cb͒3RJ!LPZ$`=#] A,FeSfjh$x)v _U{ȝ;1?6;þr̳VPpr_,O6)Fˡ .+x+Y#G@0c }cNԵSlW# xVu*EU8ҳ&(T7|YQ b3Y'Z8]@2ˁv>+h'> l g:l 97Y&*#aXHGNMSJO^@p/&j{t{ɘ,Ŵ'djg>"W20q]I)+J<"L<^(8O /;83 <5 GIm?N ~p!8e *'d# 7E4P6'9LDrk[Wf֨D $1b#CD[;U& 9}@Z[I| RJO/0oEЂS 5t#: <56hķL{{b/**œ#LjǷ B.cSN>mA͕Y$r ʨ  2veR޸"3@weib4<(c7$pQ d ( !in5rw}^(Dd^9ߍ~OF̅c[W$@#a  58b,\49vXa>vtewhÈlQ{/' 0Ӝ̟_6~i2KaOH¨93爠$) 'i7C/ IMGҦipd.h.etC6f )rK۷Ř#38-̡m7?"i%?͍@Z9FŘU@^$ í<ؖi3tR2HjjE' UKTў _=T Äc-VֵUl:V,PIb<|YK<ȩx 7)T-RuRRnڳlO-?jPiqK@ehޑ H֕~VC~[ߓtL`.*]Sfxji,.$7dPAqPER/b/D̶HIۗ<1Ui2 6ypʔOn`[{SUj*F,E^*bl_\^Z@"o;@&-X@Σ!Gh+Vt+ NM{0jSf; P5Sy蓊Mg`4z%"X*ǖR٨' 4YSu2r^{hzf?,p:؇aufFIafJJ3K -/='P o{drM+a€?qzc7N+|sIPk\IeueEĈBQWhzb\FSbCԞs2qtG_T,bm~y~޷K0;hgc|塷 KzL,SV-(AF!s#ͩ?˖i[WY+W\4$*DvHniDU6+/~-IBb;EtRRftz 4"asgN4}j$J8v$uX|yJ´HHOd!pW41:e* \^7#+۸d@^eM?1%,Bh (솋iDmC3>Z !Vh{ o$^NX!nm1l|k;Dƾ7RpOp$"m3 0suށ٩[)=eeq_6JlT[!\[2ᡡ?:wYL_S4[ZJ!,UkR$ԓ?F&-x"DCx  \!އe\Ipjd8UG0ქs -%M<~yz6Eж{q>aERQfӒ*'xM(6"r SrcHl':Afe+cˋ+8Uc&+I@lHX3FTBC}Jl%7;Ns$Ddk_Bx}}32?9EVLn(=9c kc\ ѹ͢#RZzCnk޺Pe>kTfW(5{{0岊,$,.nnD*E&=W^X]㺏st)knx=)c@zPj;CFp>giLNX8ۖ'| yfU1DX2AǗmYRgi" Y$LDL12V6ckn&|c ɐz5d5 FRDi"`YdS{c[K4 ph*8UxЊ#9^=%2 ;,Ew# Ց,&1R:UIp@% jY˺!q;@Ƈ *ifh>lRY$v(-.`5kY}L `k;Tm7ſ}OV s}{ (Pi+2a*c1"ʽ> Hjcxik%V}bcRjfBei2)9 *2?=j?Q)Da`b@)+K>բ&4c>':c* Q^_@̜q*vb4}gc.?-"}GTɀ/zoHBpti*Mz,P '\s}Fpe\=kmly2<|-;7 Bgeh-u&x>]|s`ytcczZ3g'9j^[[?7Burt>eҩXswXhql ;tDnJ0] 'xZY~AX9T5u{3 b`$n 8:j#oeCE%H+b=ȭ+ϟj=ܴ߳筰h :'F+h:y/-q  Ksb@Pic6xw46%3۾X(XRLYap[6ng6!:Uo|B%0!C?X\Q$dv4wbNd\,oQ7l}GOCQU^QH{$#_r#ArH;( n$ P*0 0b.#XDY)SQ`S9;gRFg@+|Gl@'h7i"el'4=<DyGG_Z\Roa|loɼ;ԬB_h@@wf[LFWC[~RS+sR3(b lu6n{) D';Tb,c̒Suٱꈒr]Q }D]sՉ*%z3HMf nV ZX7TlȬwdjM]!!иjC㲘'}.B-PҪ'ɬ~4 $9m5 4|$琱q @ij>&2'+H1NT RA jfZ,Ib 2A٤PTȔa,9UŢ+@pgP'O%OE[{tD:%ٓe:k!Y<"kךO(>zOThe"nt̥~i٘l{avE*HL⴬4P#e=Oh ?=MEr(B M(ɠz-щogW9 2$005fQj.=C E(Ū5qd8)N S;[1\UuKQ#u4M?)|u. i,D +j3 F(?ˉ̦#J$B8k L.q=qg1XXFRr OLBpYSiEtnw uF#Ќ~5dZg6NYOȭ˫x<tG4th@SϙZܿ.+q2c WΑM'tiY5sׄ첊ɓ#>53Ĭࣄ d(\9.l)]sS!vv' 61THH3䭏 f࣮/B i:涮3xq@9~p$`ܡU(l뱷uetB+^}>WUFS$`mI{NdL/t/5r3(.фt$5'sdU2W]Cho7CC<'`!͎v:&{'nvdmZHMc=X!OԼ#ˡKQN\j3:!VQb(D%`$j, 8ge@CU"|SؒEehMd;r2+ jQ7U[֊4xâ7;0F?&QP z)X(Fv=Hc Ԝh7pgN7VX/yE(h`4 Sg}.c.*c4@Owvfmx4\@X/$1XgjF]'dP[T0:8B4 9QŹyyuGjdՍmF^&fۿMjrkx~Ѧq <* Ka/4xa gg:U`x Y7li݄7NԈF3Q-#>{!`:ps<$}]`w`2xImu:`a8{ EVg[E1#%b1=URʣ=j6FL_RL?i] =YL-{lGokfe$%;X0n0 S1Q=&u&ZgMJ춴]Cx(0}fueyGf=gWMZ@MOH7ևIRxJYHGUd">.28j`2(ĂC@ajG$I{e ҽ P|!;򿎿{_w]"t~gzK/&J1E() wMˍ5EA,% 14z54Cvܡlg )OKG ٹ, "H#_j)(4X?|V S*׺l'UyQ) .@-P!.B48AdfN "F43Fz_l=vs ճ?qDY I2R -4@zSe6z7<C Mui,GդDNcN ^79IcP"!Cege HߙYɎz,%kZA@zQ^M$*\f*ľ}C && <-IGOTm~C^'c{* ksiyc5SC@o"á| c 9hN(42j`27hF&[ILƗ.?[r4 0y||Bǜ jJd4*VN')Ҵz'FE$.9y[B qSC0߆B~:!o+8FVXCoomȠġai7ZP׉lsÃ*6Pс(y;$#waJ?1Hǀ Ɉ 9jįbV|9џ%.!f}ZI;RQ)lwr>0ށŅn<:+ǯČkxۦڷn;OsW 5&՜uXÂiX'')]χOԞI+!F:gi{爾Ӎ" eUy5lGFssi姵ҥ?ASKKK/pMVIjGgEud _)>;g4SMDZI֖M&:H&ђtˢ'q{COWyrKb8zHp=lc:Kv\#+kq l3+iǦ)K T} D_\@>4f::hypLEM'tDxS p(LoU8ߚdjX9MeTFĭ{hHS[ʀV7)-[蜐<Ȑe5= pQy.aZv4V )Ԭ`IkGD/3f`#TG#.йC)zm@j#Bk{3s֖f!x2+P=-TSCuUΚ;k*[V#S{ߖS&N"{<T\J!m.&l޲Ѯ+e3J$5&+a'<`!Ku5D$ 9QzgX\ϮL+zY Xj 0v{Aiα#Ꙓm(+COI$Ʒ_ ^e<6 %8-W\|z]tbhoP;N׶zbM 0Y= zaz fV\(`hE* 4Bw+ZpoBcsgk[P!M,Yy_rH6.t!3fUctdRQB&RfW1ΤC>RG*z Lqg~T[ c?~8S&?0*cJQoY[Vd@:(^;K XRSHj]lxHR #o=l $DP'u> m^r^*(53]9|MfUC@ V&DTqM4n G4-{b4oݼi*X% ՃnM/_Q6RiɭOFCn6v*Ϫj7O峔1ޚ43H>^d ԀR*n]xygl1RxϴxpzHDhBj;H2VCtHBD:ٍ7R,v@iR.^L[u8ϒk)m&Hk kVCL2(lŠnݺuuM'ٔ95KA̠ٓȖv٪q+Ԥzap|#,Z9デL<ܟ:I NUvP e,%6B1-gsfv{e\IQ-5e,$ 29[Z^6>zKE"ʯ!@8Z̈́[wLZc՜y9K_d0PES& ()9 L7ݾ}[0t^[ }o y_+OzYJ./b܍cxWm4vNt^_F2[ {T*:.%fv Q=K P,l@IDAT!l5-Xķ.C+M2 !dMqq]sMI8EC0'6p -i]?㸱,{YАC[~5(P;N"+_g$L A%L{`Pg [GһqJbY8Ʋ'c=A;I8g1Y\\ {yoD1@Mh]i9^*{B2/t&.8W{;|c]D/Õb H20폿zW[i)9K_Z^Z7yokkJ0EEr$.)T_5Bll'I|z&a8-gPeE  xd}Ш+cq^iO>J>%ʇѸ8rNn `$V5;QA|!$]M̷@ҺTN" ",T-u1cO\$D cFlE9VAfD6(d 6QKYHV+F30*oqЩ@M,5hE\C %v5p,za0Nxx&GAa5创8Ze'~ETۛOmO.5(k}kɩ ^oY /+Q?kje'8mgH1C>L3_g+wZ:0⬧eoϴOf{"+:/;l4sKaNz 2OػM}X @e+оjO< *yVilG^Ͽq+]e%8x!`hrthR. c13FU 9]l!dQwgmd|eX( +U^xTc r[ޞd1ԾU.̃bFdkrws\k i}c>4u0/ؕ$hIl67 s~,hD/@~o߹ޔm1Gf| "K tw;xh "\I%Od yvwO0/ˆT,F,q#ͨqAo VpE$p$[Oq!t?4:yepUKqET"%C2,6z`21Fy88l3!C>mh_d'X(.#B^k+!j^ tnbֲ,T l\t:15`CON)OՌ}dp&K@cm Fۛߝ. TIB9Hm H)8BӀ4hbun~o]ͼ}zW5 לM'P Eok6845ؐ.(Iyg}-,3tX.0 h4][X]fVUև׃d3`uLD8PY\(ppCk>@lFd !G9+-Op 1aϼfsՂU^T33{F_b%|D\"Hn%钒*)Q4ND3XD* oϠMĪXRiӢA 0mjF0E:ԇ&JIChXFTnl8]č_yBv <߷5 1̆R[Ijϛa#Ӱٕb ]`RU%J}:)4DyPqV5\!$~SxsB([-qJF da-H m\9a %o @2:Z"??z| ~&6s:90-@>tsSýrDL_zꨭc]YA1f h%LܻsruuY2A 9{{7>!ImrV8y![6Or/IGhJb5"&HVLm c@x{i\Vb C6F1C\y dm|?R{X4&?RiF Ď[? :%Dh]>0:gO© ^Փ`C|LNNi]ިme- p GH .2qr+4teE$_p: @lL-f>]>M`lvKc"tx(cƐ$ JlG퉪 ^+|*ްhބ|(v-)~U%^sְ2vSFךRQUCiFh -R*kdAEr:rMIuFz!&J*[3eJ,(T.afl˰x,}' Ᶎ38ahcd؈X$8<.erP:2S` `Aƚ,hYɮ#5J-,I9<@!k4$FJ 9I?2H)jQ;cHq~܂jTziL F*-rJoW6F(p&m |f6[=ְۍ 6gEg? IpCB{$~O-n0m@ ' $V.Z<|n [z[UY6-zv":~̷'Nޞ+%oX`ce x'2['쨐}*`SD,B賃B ֊!BЄFm#_QIgױ7;_XZtW߿~B\v횄?ZDHxEhBVݏnޔW,4/Db64t3<҄/3>э?|W+_,E A$T,2'ܴ+m:6QE遽Ek{IOʱAe-bϚOk]+) K3mjuʷ>Tql+hu*kɟ IP xgrD߁cURwƎWgK8*j Fu*5-I])YP\D]*xvk.("@7F4{:j٣|eP=1UߓI'&@PsEyTy #U95OYC߻qq׬/"⫘>K+In}omBD%>dk,^> m|[/tk̀_oC A%+_*q.r䥗^G5CǗUᝏ9P>n>h_CB)7ync@%I+{FU-,kqxy{oz_ou_ ˅T#O"bϟxSjjwAWs7gJ+UI=0C.0N1=E! K[58n6WYaqFB%޿2:x)EN(ԁISKw?BXF]$E JO2єW$ }G |R6^o~󛪍࿴b!Kkh\a $$a~w6%'pǩ7|B-M&.aQ!D45j,@ض3Lbwy?KWVnݾK[pCRFx .>vS0gR11 reeP`01fCsJ|  &q_HP/):(J(U;5){  )NPiͺq OcX2,Moa-5u_j Sekw7BEyht 7Ld6:^N-vZsptLܴBxp`VOc1 %!DvJ"/luto$Ci4tg#q ؁E+[PW٪&WQMM7qƄ`g?gkB r`d.Q46`f@#Y)"oںD[K7A ?k]]تlt T鹙A\ʓr%uZTsW@.tT[`xH"`Nh1L'}F=|nphèdP̽RL , |螴rg4\"i|_?΅: %OQO MuC8I"ŧ_HLIuX< 0D~`9,3B*3.SS9g啊.X\JjQKs2llF8qRa*Loӑ7oFJ0IKCl61[KLrC@oOUSzo-@df0UGd) ap38v…/HfgflUm :5[(`̽%7 d2;à(k ̰RJK|ganvW>͇W/,oDž )iK]sH̪f.?t4((Gf'k9O\Ge4@~,1%9X-%3|&'ȽV2uYxTmYvmR(РMKAN²"ZٙH8 KUAL\L=J6=Iڐv=,q/~`£339#9j&()͊9菉%}nN l7 rsei!#βeЕ՚{j#? ;aMƒO Z4fMri>IΌ"ch|jMnE7 m>3Ef?qؔqxLHnkEԱ9ɡ Zh¾uѻ3H U٫V꬞nxM߸-`..ޓfdnzm'g所t2V q`*Rj;'g ,n\\rae\8.N1H3d:Cӓ"~\=?ͩȜ*60_!3T`Apqo>ʢT9yd}(QҼ>M߾*49CZJlE#_g%>&#Ԉ@ZX !x|ס2Gd./ZINLmuO_ڵX$u>?+8%Fnmcىr"lÜ iZ'L (~x}) YXi%¦̌Bن(^s`ORomSRc풸xyt&P.nƆd񥹹MN.)WX5%yXV,E6cC߈AhOAapz @.=|2dO}{Vg<1kճ\хٕr!ZPqR&rBDwj`u=Iꑳ7Q|+]?748;эwy/< ;hH}4 ,f6v_}4;]Djk֦3խ$m?HDi]ӽCE|Px `C߾y]|Z!y>C0Ŭoؾk'V\ք?|ׯWx+;d IS򄂄z J[rTu,q b[_e'B3si(X|OЇQkIDl@id"y)KE( j23Ԅ 8 b$$:0̦eǨώOg~C1@Pg T&3+3f No!RlCBj nH CY8? GLg2-FNmog/ k3wcr ѢȎ6'C bcxJAW όjɹHN>B֯X5Q Mص#Y)}#L 6'di g&- B5-G'06'%:aԶw#%}adeS!IJo厣*O\NJ߁B֛2}ԙ8)S^_UtT\<J՗Z9qqlyDr\UC %eB;ÎٛV9xWʥˊ[ .zKah^iiSwa/Cl^Ld0=3K|e,_rrfP{ld lm|LjqǕb GIwx p _0#t%稒[P(Y=ܛ\Zxł*| Y'y2`k oLq3l l+p`s!a0ƚ&m)c_Mβ!4dYu(bg6EM0gxL*T/ 2 ۱/mO夢Zu'YwHƁH\VW8lFPT Ċs+1jͨ2Gi: %׳BMƱ rXd`БWXZZz@RԮ]~|^xa-XJ֭8}N=h(Fu9[-Ld&Z0PP{mY֮yI[Ayp6 m_;=ᔜ_)FJ.x2.lefN)z"vFruaw7VYSSWa~M٬#>"vp_ӑ58՟r=u0?A2> |,sZM=-i'{V }wo%s љ" !|ݣ): P6LR0 Q{%I =+xEuߵd‹׮pwKGa#ɥ|{Ľ{;XqBOLM3HɁy%&T|;1>=AXbҪŌnxNXsʥ?_T6[}cRrŽ;I׫Vy҄P(R+߯91)cёVK_cJ2K7J &b G*:CU@Si+tؕ f~EkRc3:a~dgҺaYA"ɧ$OGEn\@rHV`=):v'3SjxI*%3~^{W)MfI!e`PH웊SpH5z)8dY$~:w K l@0+qCPl`b&B\bIQ3Y3DD뉷A,fف4{֚͗ެww/N \8Kt \xW[ꪤ f مr(%&֍TsfPcJ\bʕ[j&4q&ߌfiV&YYBOԛ .%ۧcQ<'o=QO/^?]ק}jG+_CKh)T\@*qzrq ɰֲ*T4 C 0tL?!+/}I%:ĜȉY[ʄ7ߥRWhomlc"f3e͓ǧg@5K/tm˫/{wj(lO^S JRDUPG^]Y ;_oVKWu3ۊMDVdsgGR;$wag9a/TDԚ4| F2N8?/$6Ic^JMLofH~ MeT !3f<IgO-H>2a<0-^фβ'ݻ+"s=!am+ q&)VV.\bQa#NE$+6,xm N%(FCn!VI> 67@ o5@Ɣ ;@\*V(En27yeM5D'R(2ЦJВcÁU$I 2.]Yd_`GqU#3~?vMR L7ƞFv:9}ƟUJzהְYc>#Wx~fs'3>f`&6愣ݐW|8H0Fe6!M@ '2]Jtݳ)*ZJ%S޽oD?u{Al 8tS[W}>n>%/l>NIR,h;0#6:.E7\}ů]Xu@U|A+Kq5{e@fjFɏڞ8SXNL潖?byfܾw_5;ܺu'IP@x3qDADS_=a5s& F ؃ۦ3-,腸$x嗱ٍ7f0\xqmm#ɳָ&K{|F d)`چbrMi#B)κRyHz%وBenñE~Ȝ?S=*̈Kx+⼪Tv'b,.jRDyn`>TFô8FF?y?v[wődl iz`ⲙITP4@,%"QJ<VHHcI<$V\ "N l^^A^1z"rm{8m2,6d#ϚJ֠}+(&Ǭf"[}dt_SAGVF@S'6HW"s\͡'t ^ )aU xxR$R!ٛ\ &'Ȥ4 bcS#Y DGfZt9 |+jWa ֞V5|̒>;ʐ<\P!ɒ *e`U#É֕!'u6IAfyѢ_ϴ{([ɏ:XjO~؏o}̱W^pk ` R^\]"~Dw0 \Q$o_9x,.a:4VIz]f)tFҘwT(3)ThzKs:$,Y$JjeI1P+K7;UǵoolN.ჽ*"DTh`8/53X:k_@hR&)e\0(%\Sy 7~dѡ/$m dJKq(nL9bճ']8u5iO^gƨ;Sٝ|T'_ks'G2ҋk8o'S@! :c"Fw\.^:?;u*hD6lrV4"Y+bt193-9Oh wn]?lL5 V)' g-~gGV}8<7DN &P߷B!6 *%: <r?EH r9p;";~w u%weۀQӗʭw?D WfgHL( /_{KW&G^Jc7i4T6 K7m>??s(r$N$HstNA, Pӓv ٯl ވ]m* È@Gk7^Kn"MIQǰd_ #Rw$;A>xxi/ԏOb·䁾sj% yD8_ WUJ#/̫PP9_pt~#TʞP&zKkl[+QIQ2tث0(L&AsR{=:u@ﭭ/γ0Oxa" 6!gk/X2!zhCvhzaZhLG1e:pb!5>HL]? ^xݾmZ7"Aokֶ%vO}9r]0W؍G_K7ʜד3ݟ+s&24gv9\/swkyv 9nDFFLݶvٮrQ -TPo xGLAU4]A6Q1 +*a u8uCwFb5Xiqk_ ,6fWi~l<+hC7,. %DI?Zɻ>W5J5CW NJi^ ߴ>d J),r^ O{7b&EZaZrRUTm}u^"mU)ϳ4W7 :FTt1&~C8>hH.Ic]ڻ䌌,dvB3A`g.yo,k_1S:Zn~6nȚ)*>SYlR2cF0 n|W#3_j`@ yj,?Կ:#6Mu9˨<#'d}XXu~Ӿ >u~^>[}ba|x˝ÏI!Uz sI[O/4@o1YXTkqdaœ!6i)v|L~:J) Q<}f12tö۷omlmB8=֢X3 áVmѷ+hr$&K.s"?^tCFn8ΣǯVkFA13-g0R0sx`uщ|yA\G6Wm Og3x2>ɏŲfx}eOZ$T^ pzڡ3t-⼍w=% * L:џW+S)qXUjaO3 > '7O|OGPo2P e=L讈0G! &v ^ur'Z-#${3 ┸Z1l2[SE uٺY|?b+E%99 MiL+i_"++Im"͚[d8z&AhN^97E[;ܧ`%_۩) 4r#.h.ġ&R#SZGj:lZu0X;]*f|VA0Y#ȘG9@Fiʅ#W@tD>&<Af;9)BP NZ_*ȚI p4?njbCUMCz~/x!^J ?WOe )8 Q Aۣ}$,[GzrBϻ&x }RQ׽ߡV8 ZңЖ5[7߶{G+Մ "(>q`D,m?3v@rR5&*.hz@[x%JdP3vՍ?js2vq}(SN@IDAT7>zhB=T⦬7J2ȾJD^d-T71j6,2OTȘc}];X_v蹰k͙pI5=ǟ2bS1┅<ttB2# R5x+G^f-UI G精[<ؠWKC۠ :a|ô( gUa2#PWs,œpIᔿqyɌ&?O ?AOXZIxxP/ȭzD n?>̿)bs^Ky0FLmeqWC;~\鐈-$oyG#2؞[j󈁃9T"31HH*# X)(t:76қp.l*dS5NBiUJHCGC,qh]vZ wZ@vu!"Ux#|6:?I\-͝AA}xN0od8yBd;$j2٬O%;QPbk f`&DYqquм2bPe?}[&ٜl%5gmwP̚_$GrZ'5إMXtIWK1\#Ji,ٵBW1vz¿qOj϶1~F%K]s{z|n6'!MghibU9./ozKV@<ٱ253 ^u_ G'}*Cfh1S}MW@8;24HXNP2˄Wx*7}>Dj5s?N-ʶ~!Mܿ m^3z 2cd~VdL~!RP RY P"1>"My_F1a$P.fU?^Y]F01EuTdN8!QP, ٦UeEF_ ȖG<2dFA[8:Y{ |y}]} Xz!!GUdUD<ӡ,72L#0K7 c6E.b++N ] JGϸ\Aaq*9X,Wq)6>\&򿁬Ži*8oO8@<M$ xstyi+KZx%b7c#9oDI Vl@e]` " ')2%'QCV۔`B,0D?Y0 in8(ϖ-rLAD Y! {LCI8C 5R%iceUgX+A^%S *bh*>,ͿE\tXOA>t,IwHT6lHe,t0?2IxUլUP=έ鉳nǺdxa&揼,ʼGQPEgݫ6#_Zϯ?lȂbT5ڡov~>3<Ԣj7':XLx" 4m {D?7`-C;2dG&0 v][Hq[)bL@2ԼhV*GgjZ~j>[8xr5\՘ `PsfFʯ^aXSeNc<]S(ZaB!]{%yiQQYUmQaRsI9mj8iB ,v@'~o,ؕ7&Tv؞*to=-Uu>E?YWu[!H>bK^0MDN;#006/T%#,d. 0o^~ ~YۈG2L4|[s$ة@cFp"!NF\$z$nREVI&tC7iCF)MxQ ~OO=/*cſ?O~!]{g6V&z]6EUѧS>M޸o+A!z{CC1I*ӯO_ߋul.i1?O]~X?%e Sqrjti7??u4R?dҵ[L*ә@0ugt j^&tKS !絜xdٞ"puҴ}#L:m fC:onnszE(fY#P] 2%T,$HCySEцڐslIȒP&(ţ*`Dp$La\ d,l*YJfARUlЉC zsKnGx <@doe]󹈢29†L[/g6)ٴ_by;gH a#:')qbXV`_aI@ZO4m{T߼q݇ff𢿰8C4¥E'gԇ!`{ 5_`L¯sڻ J! TQUvW1@<'{,zlqvhWmoFf?avfҍe45rT h4Vw{&s6Lv%.%4 8holS䵹DsW:֔Xt~XW,%6 >:v;c`MwC)($T/B:irT*B;98JyvkDÿP4Rn?3}s^.5"ŏ`]:%lY1 yB_P6U"yDFc^Q`؍m ]'﫶I2~{V,>{tO7DML\NOt]ӡ@p*Ktlx6!=VFV=#nn0;ڽN0 *M*,0 y+eʷ(֣a΃ޑ Ưe.qebI"F:X{;fBmtޛ91u.b‘ꉸ"SM$Ol2iB3<\L]4.i`ٗ^Dܮŵ ryPzԜ˞f`N&!-]C v/[%JG\ڊDX D#{S5F . 8tBTIV:@xl|Я!d/Ⱥc[@8Hh<`⫹Rzʱ1RAs:BvNy!8ظ[?=Ja?>f*%'BXNr+k.cjm fu$WU{3K4ץ>.SG +άH`z$3 PF.Ypg mjT1m,Ɲ-{c 'SA1a:9"F#5+:9MF9p[_> !ʇ2?Y7lps'',ز(\S@pdv)({BA~;g) z20jPF$Yv'57ԉe+; 4p%9==>sƷCޢ͝ՙ5 %SHNd"/N] ,{Ga3yBiHdb hdF$Pޏ@|n^!5'Y3kz (9A*"_pjT2^'u_q b3jğ\OĎA99z9g^/(Oo3@w4ާ\v̎qexE~f^ /=Y]\N Wz*r/9xs-tzӳpK( !Bj20L􃄅aט|0Xg;шb.==~uɁU@*#kf'ӟf qBa~l2D!PDKȏGW4\gg'Woส))q^L9̺^<֣~C>ޞTzI.n~ͣlҟ+3:sJ P6)COgeS* հr//a Gs8>7  aQ~9L c9%;Xn:?]q=.,QM nY}m7?E3_юs.cGC9&^_o=ZtgX&:x"v6<6ڸ|]WpaaX*]'"1_'fPvM9-06ױeR!iCm1S&1I>kՒ3)OV?+%ܸPg$K8?I_@p(߹\Dab&]+ IINWRT͎m- p ˛?I̎eyе*W+~|{wǯqz5;MzFJqKbm~t4q{h.SF@'tm98uܬD!S;;9YZ1!F!R^w"R`Yz,ٻZ28լ]%` B ,.%E aPxl%k[XWěiEʪ(4h[AIzD> k'q00|G)ݙs݅)@q9>+wQ˭G|u>k3@,vO#&Oݫ'%^~ңř\5L1W\ADEA}&W쫯g<\o??B0" 5*Bb}0&9{3Y* ?n\)_3< f5lϾCO̚[Lq'gIX87;>yM3.h銇A +9)<ȗ/o<8{gP۷ͭ,7qRq^}y>o?3O;$K?sji zW&N08qj1 jG(-Q^`"6G'I$cCY.%p:.:Ë6dT1E ǝ镛' > > nZ(2kB4*ӧWխfN(kX~njƻݧb}̧ S/ePu9v6v66?|{GVj/L[ϸ"я.e!]fw0|XXQpIW(7zV%պKD)KiІpEI4w';;i+n| l$"͇MMN/wVNN?K/Z~Ryov~`YWymۺ԰psitv6vVS-D5R ;0}Ǚ~!}vpK_^`O4&Xe'Gvd)xqe% POpE-zSI|*uX[[ih ں/2+Lg̈́]y1hsx\0 Y2{#6԰IW""KqpIp *ω! ЩE] 3Әw`Vrᰦe#I7|k`<jN˾ /RF C%67vIZ{;8ZU; `L1I"0-zT((V,4$Ȁ(LGlgltk|GQW2¢2XXnݝ$B?m'^^G/NMo&x3I[Ž0 /7hw~f⮙ 7켻J^s/49~go;"bܧ_!olJ=fnnzCqr Q'Lң(tޙ[\ 䌋DxMyvw?{{嶺Z IrLFA$~V\=z,j~t;;Hz= x2~f~.9;'Qu4A2Ugyv6wùĴο{Z*?;|NZgkcob#:7X۝[7;qDdP\wOt0/.p"ԭ9$yeyC]|;C\);sUαֱ^ >5AqtKYh-X7VPeT-*x6TiDCZK߽ʼnK8Kp @,>6D^ٗ坛_wZ!3վ9L}lvŕgf㨇ޖZk2@iLf;?aT$c@d 5t\<7n!KP"nRՠ!/GXT%(֤<yrPy}q;( Lzhewؾ6ioHTTJ((_nb t'Lc^8&}-tpZ5 ɤ3h0ACIX^x#SìΞBI>\Hh#'p9 U5Y&?`ܞ$,'s3 @MIit>jdspM5<ߛ!ߋJMdt(wo,Ƣ N0DCbF@> I8Sz.$w +MGH+֋Na#cS <Gx"(*&K=YݱX[roN uBhr":P0셽ۿcߝ>ލ[~ucO]Ʒ.ݿ->(h噹O>=Y[]oK.O/~dC}̞W SCv?'_y;oz}>_V]4UYouZXPW+YI]Z⇅6:0%)ÏF x/ {B_t-)GhCLРQm]A\ 6pHGdtsRM~,a8+e@H[ Js 1 uUZC^3zLtƧIly%Fc^zέG$̠frLH 44Eq #R"Y3"C 1CDi4BKX@g<ىP]S ~:ɀPZХ^ǿ.Ls5m#ފ " ЅM(xVQ*X5rTH>?O1(1JlX\S~_7gfdD:C N<*v&qf{`h7 f?Z67a8Ya]4&l^Zmv4OIXb2DCj_C{ѧ GM-35 EV{ :Sx^DxJ..ې?P.m\6j$L?$մ*a'wYӨ"uc`brC#*%x:)?Ɉ.=G࢝um=><L\OZ xkBdHZrlriq)Vt?ۋ{|yrwxS~Xe?Y߆houؐ>?Я;|k}1{0x *=^׸-(OX`_\%|T4 Qm Ӵ)C{FY!rHNVYݚފ4H)ĪLq/a7{1o|kթW>Yjx_/_ԫ_ 6 oٌWb_%نQM[6KThZ\lgc:F- BƧj)\ f+7 (~< -Gw{x<#-˲ 2u ѣK<ns6wOé_# &f1Ǎܓ8=8Է@(``_~H|i;~:K2e+־,S-/g3 B+ k<.;.;++zf!M%5!\2/"샽J,8&iK̂9pL'QZr:{C~nM.`8F, ;q;]WaQƤh6C0ϭ\$g'.`dDҜ,Œ&D:scN(@$ G oݿִPsJ>g>aBѸuNCK M2p_2{{Ϋ_~Zoƭ+C?ox֋Fonsx3̅4/i'E"\e]mG NdYM/j=KgF9l:$uq ̤j'Sɣ#:c,D:IQ8IZbR@e1>t9)׾?C'6%Yb# .8,.,@}ϥ2`'2_ ' ̳A?l'󽱋wo{bF,̗pO;Y}ks{lGGw>knml8l2D\v2\AQόXmfi;t%;!V(ӝt=b/kF`(7y.8bw'< {uy|y+--]\ls1bE1H* 2L3Wj(x>r'GcF؋Z&ޔ,(v $1D8 -@N~0`t7.xTND̝nHDij4f@'Lob{[ C͵goo?c;;8GNq82v1^NNH54؈|n?K1ydMmO}iaQ=sk$0_Gc.@? g@Jy66(_grfg'fw2 H6#uN'7= v)b |t)NT|W6Q{kCjyHكRK4zME`Z1J%''*"B >!9S AZ js w,dd]fWdW U,iJPHŢ>?l<|g0: hc}cj޿0ZL\GkЏ&:$2EƏ_ǏJ`Mzg Gzg6eP~&p(lIG҂!;z!Ragz:yp6&W2ev29bX7䜅yAh!@1E\*OGw/bɁ=]tCz^џPPY:ULzjgo03BOpO:}H6Q-cSOJþ|?a??Y3swGIoHZ3~E4LaIWȡ),DvuAM+_꿳,s9CeX Ni[I2ekDRKU 8 ;)9y^oӜ~]:w#^?@d\xI2sAvrˬs1YI+S,϶IO˖>WS`⛓lymQI_B¯VMsdfAmƒIOS>RILEX~ xf3ӯ~e#@&8kԧvnpY*VvjNOLY=:@dҟZTu IFyyS#{RI7Sc|b.)|zr/?Q8݃[՟<..8Ҝ^/H]PUBSXG!2Cq)z40q"BZJsc;P+Tӯ*ۖ|f˩,x1HY8!۴u0FpG7M1[ '>3rںEJї-\7E,Pb[L` %K5] 7XK"JVal%:~S4>΄e( ܀D1Y>8ĔdHW@'PVXy7nG^c}"dRݮ\, ZT']t1rq&)pP8͈3-6dsd%|.iW~7YƨOȓZC*zjB0e/a 6>R?&=}\8…s_w>cc2NP2=0Z+FeP޷xGXﲿfĵأRg:K[]@]< aiiY:WB ei t/ L*{>fkJ&r I3NHD?'4 QxK 9_QI"7GrzfKAFWѰDPdQ.!7PP$ K5ř\zp(qsko18[?z]iVO6?{vqe:48sBI}%IBdhq>ȥܗ)M$YxumC^-p>켾onnfe8v@ j{Eav[Bb]SHb:ЪO8]x0HPSO~%WِnosHA$ y!.!> .m50s(Swā1kH)n~`g֢L.-/ oI(`eaY"'pֶev syA6*o61G{c3p=*;ϯuck'<eZ'H4 0zFo vc}s&6@fjT(ڦxV74'b@UZVF.6{@x+э,JUY0rխ{uthZQN,VyysAF){<%.ǒ]T5~ӟNptA8 W>s(b(МϴsJU̮eo<??duW@QEWqHu,jfWX9ު2Ƌ&QEªOjב!*莜9c:m4!岾4tM4eC/dTī*MG ]COT[" F 'k w=ƼZУtXhÉb+p} .,eyPvk.ЇjS2sΈ@{^WyKӖ -GD`˝b9OfnK38|gĬ+Ĝ65Ofa UPj{OA݀#W,F;=xc8Sԗc٥>5hoݙM_5#JFGX&IAygC&&!Ix710I`+h@D=^]ePb&P#4te؝mR;W/,"*6WL9HťyZѐCXs#JFzM|I(Ve_cn,~B`TDZBg+0 H"3rTO΋`]6 NT[U$"EH_ $B_b)KلF7FY`~N2jJ[g$LhaY\Jq>Yhb<@p8WM.D:O~KXDҤs-q=^WHzR"̦ %1ﹹ֨H!SCS#E:; 5Ƣ#%csHGak9tboT#d([:PcYq~YNF괱BYS`'U}h J0)I4 tOs|QѬzKOI3W(& L617&#Xq{>8nA4u(inr:ֶ 9[#f^z1IW B*D(>>(R9Y6ׇ&@9:/Ե@UxqrLdZ K)S8QJȗ|\#3|ӟXYC!XY#; r{-y^ +a1EoĨd)$3v:rb^''.?ys$g۰1/C =& vX$ Qo/,AqBwZUT$ AEsx.a;8EPO{_͉ dQMw,Z?v\Q}86huö )kp7Ќ,B.'Ot P_456_bRFm\#7k"s? `址nZⶡ1s^HcUbK6Tәx=٨q(/XSsh#`\+Or}+~W7(1fffm4h犇 9)h;f5JUbsTH Oǵ0Fc.`uj6_~f.v.G77J[7sCn.LڲM7(&d(v`x#{?aS@IDATS$㛶 VmllA69}ut0 2d! T[͸Q|c͍HFeI+ C@ym,`onm 2pPU+28fեMq҉?}] ӆJa!&d5Ql-h-Wpeѷ=17<ʻD1r}U@lZkLI7k+c)Y\k} g$!>ǭѱA'nK` 2NM(t,6Օ `׏T'V+8ZOjE=ȗc90ݸWp%_M{|Aq8CMMeWq,rYGծ` ^yXp1 / >>\)y#,x7]@UIYTt|=<6KaMPRCܽeOxS/b'rn0=Q> n<\׍6=qocDwVEbG0hkˠe0G绿Jt$~Azh0R 1C8,4rD?WI!k%絨 ꗝݝ-V 8:0RMIԮ ܽ= ĈFIten4P9:;v [NQ!B<\?we2w"u`/I2-JuN7j/ flqhBbxo}R2{9"mmb-GYl{xCua Xlnqt2,9jdP[P2bvĬir`4Z{uo79"7h&$ KJ ix1ȭ0pe Txe?M9 ("rhT $Ծí:9e YTե&Ғ֐5: TH9 %7zT՘J|HOQxhdbyO`diLF̢hWor|d\B=>ꬲde^kCt$;ѾL ]2GОUE=;ysEtgiqv:J.;мO枓Q%R=29qAJQ'yĶtF!0<\P"18I=ީ,bV?[_L'Wj s KZYl KK)^\̯6~2 hH{BҀCEu7!x;8x5<8Bx1 .aՂ8{2(eGT4hجNJ2'CJSډvGHRO+J,GP uDp=P>8N5A6yty Ws)Fr !Zѡ]sᡴeUo-V&,Kc P.w{ႊ*G<84K,v|zAC3̝Q[d.oi- ) 66͑k`JjΎ\h`+Q*~e,lE 5ӀQTH-2"ڛNUV)(&$0lρiM0uxj6;:0JTi#+(|.Z4rFS vmf+ ԊlX_~WGBqa2gg75)7#VVBCi=!A gWYGK٪6AZ̈wDqaUhVSk2$+ nAN±T AͭȈ2Ee"[f%I3:* Uh*zhq,ҾOo=8 = =`D1304YDɲ-='VKׯ_kZt"r2v7OZ>_yXQu$.bw5:g~IT~̮\`eBu6|BB=ܻE D}p#zFELK?uŘ| yt BB6Qy-Iy`K͏Ee bvu7 8JF f^j"kȀ"Åejӻypr)cGr%Ɯa;r%$-4Dx4Zk[_% ɑ^PTKurB,c| D,&;jPlHt#†G󧮟f,0gdwLe?C[Da꿛_O q~$X\}(g n}.j$$Gm"#)_u֕ԂwĆ jKMj)'z4CQ~! 6]ӠHӃi})Y@tg SEsJ{j-BF2^eH[\~ u N.pG0ƴrB)`Ѩ &soX|)p*@S3/ sG򭶕"4wEǬ9|c{_<]h# ׃tx^-N3tj1GoSIT_ EuN0#V/CxCǾ52?#6AqF}P0i#;;}zgGHlcΥG<~>UUonA-eY킄M]@ě \H>.uh^62p9O-.ckpsS$ucB6øPCFa oXGG,+@L74TXxENC72#&*]ZEWۥYI@K.' T䊂@LvPG.qoDڌcx [ɗr@3_̍ErFwGغܴ>c7yo3yrv bk[ſ,2e%SiThtjf Y$J2Y~ 9\d0}Bߜv\fJj\ EESz@b}`dZ*e,# Fo<pY.mQ*¡Mu۷w#u/a@쥇fOqbjW%ְTCyhӍ+9Nyӟ#ݸ &NMI:xRku(DQ]OOyܳ@Rou{1ckzɳ@o&cX)/~Î1iW ;2@DhAaiAhtjh$=z( j!ӴPsxn n0} N䍎?86UЧ=A ڔ4RF&)c\Q\p⌔U8v=8S>uŏ,<ñ7gE$ޮHJ{ ~[N9-EC頠1pDyxxm,nLU[ 8i)Xgz4Ls7YJz3.!Ihr}(M'JvFg,Э J6 Y / ,IIKXгR+ Ll _l[b0J|mO 119VFAj1Bpƙi|/,%I|y[i^!@5ڡ馡 w'v_hrgoOa,l_A(&l Ͻ U;goߺc\Q#A:Y6cw]{NS8'Arɟ[X]]rc tG>`@mFQI VL !OEZz %!bVWH{BU\SExQy09y >se\  QE6')~S'gA‰h܀O/+TV1J}1֤Zf o^C%ݠ'3V3PF½b>}|w6Gꃃ, j)bLভLzbK$ ~ؕ{\PTT5(L4_JbNA {u f`Wfa # bD= f)^䯷gbg.8,fJ:t!'dtP(8BN ]1cWXw'-0p[;k |c 4f`f +)w)GSy%:W؁WWl\DG >y!ѺSi{t JzJmh nCfy !&4r)FRs*Wkj@"Ĥ'aC𡠲wɧ~xTOZ M.Onb6Zm" a94&rWۘ CŒ$)/ɻ䰻^690^ٝ=Ѕc=tԦJ _=%22UJ P5\@}ˤ k:>8̯,2)rߺbNFrfN$AbH%HRh8rLcd0d4%%1z8X1Rt-!f9&jF)T7zkX!Ax~䜤q6h` ԐU>u#bG4@l~fI@P3X-U?]Ody$>c¦c`GfN*2H}_ga(.h0 05kjlCd'rnL(7LVNƨɝa:`4;t9M-T c 9%]Sju$ʩ% M/\kYƺU䂄P|F7!^H;ǒ:Cq@p9ao`%PCR&:+G! ^`Эr lei'@zܘ$M1wU̲J:i6O=p`Ol3s,SZO Z$9f,WmƧל526Ȑ=.YYmO`C yQ rIVϜ4p.a#Ƙ0&Q8#3lo{x}?s:^FƫsA~ۻ^=YKЎ\[X. 1\Nmܼax9{ lx7l匊ä$1d9WL rAðKVAm ܖFt JGGx]]c^xSd nJCz*#J)ʜwY'(u@2,iikh*v6r0脐sڄ=)esLZkUm:&f!dǰ[CLS67"ũNjtcs/-㰞,3#eP{J tT&g6s1}=w Rq70{ fUNj:S {BnM@pBGdO %JͲ1g(;?Q`[ӊ|>\p91Qh~W&C0ft[ɋ* +4f5訂,ZwMKbkҚo5S]Q}#F1tz¦=/ otՠT]HK+EX.'HՖULgR zS sI)Pͩ-$I*{.ԥlܫΠ4C@2\C#b9Ɵ2vtt4wvE|14}ͽJUH|Kh D/uMJ ,I54dk$wDTM2Y'׏Z[{[xX Te xQKI\hKaԙEʧY|IGV2XPF"mJIaoW-&V2I[_x!qA\4!{֘$*n}Pۄ -\_;ighNxKk,k(I1%ޯ]/ K+z'dpCs.mi=) fJ”KoQq0 V[ >= c0бcQ0oQQ`A'@[ұ8Re}t*աkz98 s5Zj1#,{4-PTœkV/(z9oFz6t-_C',Sd:$$K1(蛇S֊9P!I"0.21e :鼟DLYbbhx.Wȧ,eYc`:1Li4 yu{8SI?쁈r>T@*\60>HG~EpwvGIͯp4|qcєTM<%n6f51`"𭭍M1*e@{bhL)+[_3Z+t@]h)NE\_,`TȈ2_Q h q+ c"{(~9\J%=Xt2լ.#6p6;6I &eM2 0jf3jb~BHF=Z;.b$qȢ ݲM l3xVS ZG;zt<.Fƀ=|Ha#(H T\iMSUƛ_3` bhN'$([|9zb%jC8g!4R𐭂gŠc֜"yA =N~_J m2V]8bE:¦&5'G( '+gG~ N+wQ'\4ފqdivDZz]w@Y>b0[{τ_B[-Ȥ$pLur^Y?gAl [;8rN~{oR%I~;sNr>%j,#:M;G!js?YDt C@uK :T+<˩WM&c8{FP0ZT,,ƀx+8-Im{K"ڞe$ (S3:_.# Dd?TW3,P.R*XXq4-P x F4h.S㸥޾a*:8Nծ=׮]UUm 55#X+Y`C⣃`@7eb4`HwibP\, yF?'{< $!._]u=_ H$Ba`C(}뼄 Zz`1n:')wS@l2նNN<]j0wH19K5 [2 IFG;N@Z(u70rCZjXf7BH֥z_k%#b9'JVp\R&R(25A^fop 75ƕ*gcx3y0RNID04;#*ăCfjC& RAv1`a^ SN4K8fl!^pNjWG H5f*lrbսw?~A ۸Q_^, MҾt[A(?9THB7奚jqZ53\~Ā?_$q, O~ EN8hH?Y$?"΅OJj4y=K~BxH5OIGg@,N56"2 gж#6& ~=vr;[1$"1*/$~0,T18Y`חR%!PI+fTK1FtFb зM>+Z~ nSY'a m.esrt̚ekꅇLYDëm?W.e520YyoC~bQHn[t~~;d?=& Իf'J)FA7ȘTV |չ0b;̆h\'-ū)3цTJڌGK?3_75ǭY6P&iI0d k*XW\7hHIȇ{:ſ{~`ODY 0?r7acֶ# ކMt(}4܂۫fZ]#Tȍsк~e v074s_<p1QV4N*plFe$IxxDYol Z/Mgt4Eד8r-gk@~PX͍Uoq rPJ%&l?eE_9HZV*Ye_j O!Or@巷w>؊?2U%u~{[.!<\P*Egi־3mۭ<}fm@B,{[NѲŖ_8| ,aݰj†-OA%kH K+}hP kx4zqUyϽJqrRa*zB^t4mh4s*LntJ_ hVj8}'EaUWR>[lO? d)*u7-fm=*3ZFbe s0X{e#@nbd" pQJZW8@nQm#>U?W*iGARMP _bc/ Ea'nzP=Pβznph%]޺x唊AfxCa/q:+,fX noX6YЙs׿?sxh~@\Z2f՘U6?&Q01& ]mlP^@–AY&.Ì:V&}퟿yrC睔\}auj#0nq }b\x-@)%?5P%R4(?~w4 V.Q=:Eh4 )t)bY gC8xiEjbg?5 ^ : _9.XZ6Lq5D$YͺP6zI49XD/ RjN}(H˯TyzȡtT^z6l#>d/u_ TNy^pi=umo;3}6=={ӹw%B}?+=ϟ<uJv7E9Pg 0i)edBh.J MҺl>P/+F@gZj<0NlvHAh#PC`M #+P ܱ[=ـPhj"m$-2Il?e حђH\-tTXXAǧ@b>91MƤ`s&ӕgn?nzkϽ-lt&.M3(,=SCx 8h#XW[^Na %\d$YgMI3Ax7_m?5 7i#qBUhUD)pKh / kfa ]tVo2Yzfx[M;Y0RSX y v] Mſ-9!BaDp KLT"W BrB "ڒɊ'=IJȤFM`F\`DU)v-wҸ>M[-<8*Cޔ1B2Bpe"ư$&~e=n#RpDבDܮLݽ3T%Z,ZS8AvY#diOw pϤ]"[$l2dNP)" N!5re 63E$/DzL|E3!V/0\THٳ=o;Y"?T3aB ހZ†jC4L&Z Ɂ2r`X AէQ%*!{D*-}/S 4s\I6 C+ E<҅>$viH!u/"(Fyi؋r}(%L(mMFuA dՌuh(b4?$Ͽf@!/ ަ$><::NI .%?`h jzgLZ)^;3]35 g nឰ(?.Ъ eo ̔*h.WJcقX3>*4`Y1.Fe63?YR".<=*/iΟRG~4}쑩@4jn x@Ap99Ջ+3bSjUtqDA=>MsM 0Z?D|E`ulVL- fCt4{EZɣ]\Ŏvę]y iѪZі/ܳCV֓':oe5`R6T>Mba7y.=ܕ[ٵe2л0`D)3hGjVu7еXʋ *:]1¹'\otp/nTyDc{cHշͧ"&UgGCRa~#" Xa*.A$aH]+obac3Dn-IY53'vnX@/^1 eb.ꡂjga%<# XeDu7+.j]IlɊz5AQw,l٬?GA$^ϻ9`9AoAW:vSs^T`\ L5S+H¤;7JxלýO_|%h =m L;(?Nh0Q~|_ϯ^e&l'jqRAzDG,#gmsTb%y<46l 0>=VL q]m$Ǭ =Pg;ݮdY 7_d4!z``~u:ކgCвŽE䩱:%RF73`[q$E$]eSaft|1 s-J'm>}JZ_=d8nh[LMQao;թF I _<{4N@wqrHTY w:n $ 7fZ+ϐ} ~ȷ3=N’ U'pkObzH=XX}ҝG&hc{~x^]F3l1]J঱H3hѳmFv#!>]D+nȷtj8t>>Mo{%*=;T68t"}i-u[Vj`|V{(譀Wƙ~V/Q` "РErQ9U.17Y#Ҟ]eIE耄u ~h-Ծ/y?-. #Xve3ob}b3((d"*l|O:Mx‘LڴT9Wo~_Nme)iZ!?"UQ]}>8 ?~gë2N> RbF$hfbocCH]/^dޗW0‡؞~;p8hf2V~X -@SJ&~XL?0LIf:~Dٞ)`h=5j+<9V/7`"WY[Ri[o#8aaUG%#txpҀ bzW(bܡ2뉛b8|ZTu䧏'|hk2! [ȴW5hjG[2X҃aInfuHZ3⻜ngJy57{'%TI(?50Fa؞N l.8/1 +^6m= I+!ݨy5̸nbДqn'|ogU]':OXw(? F*'ZT̚25<`Rp7;u_1ރY&!b OQf&⚚Xny[QY {~[RY0oH={[1OYFvpւy)0BO1>7+ β{,b 򿔑pqt 1Out SIWK*h5E{aq&I'yɕ0}i5Q(iOp*._x"^-2, W`1tw'!~$ce 3!u3D) -"wa؎: 08Pl5n0~اue[/?{NԍC:;1]CfJ@ 9@IDAT_>3+11'-g&9GG7<;x&rdow)cΆG+(Ej*XNP7[ELşiOJ㐙iDk(Yr V * C% iĕ4Z9Ig|0SoJE4{omjӬHLĄr;H &(9K/߱9-hPb1^8;=3Zl}Qs btRtoB Eyv̧Ť!hevpo~~`is Oc{x&t춴/pä% 4݌f/KkP6n bMXoC a}xs(:QJihapa{KM GYl4 B fF7XbfG}o`/nDUa_o9 PbL tXQVψ)0[m PK/5,MxU0g- KdE>Tέ-Y-n8VFJgܶ7'?3wWX8MwHl[i<*)f>WoкpS?Ōt;[o>}8 j-#,A :? Xψ SjQ~XFadeMaz9é&;" k=z5՗d11VP6Fϒ7y $J\{dLd~x+]ݢGzŇ 8Lk0jo:88 Fs6vLG@Eg+W/ԁhDcS 098n,xl a8xDǃsw~; 05Q/ѬSiō֙֎qȱVM\?}RΨ22:I,k_7

qE>L4 })aDxJyoͶD2U% AHh@ x` WZoNB4 :I%|ɫw^.7e2y뜃d;zRՀۡ:0f<.ti- 𯰇V]uuLjg\a_.&唅LE~ 0 S+6`X"34ӛªN{>08j=iMƳt{40V4GmxIVJ ْP+)H;Ϟ{ E?;52Xi`ꨟ#P?~a[Ɯlx@ސ~8V#BOxWI49a+ޮ‚0܂p("e5Z%B6ߟ&G(w%^i7Q {9F>">#6)@Rx]_" q\(^rgo~>2ضUB"SN%oӡ1#/Z ڪ!.CC;s|b;?4o/!HvelMb1*{"X_{e c7" @l pqtxMԖ5̴7'6A-q2׷w{)o?aހsMĿxgm}<W6 il .a)jm1RWη`:mRsuAnCm9[wN!Sd}|Oo=d:`Mx. ~nH L-C {Eo1 :T hnH{ۂ {y'874j *捞!-njʒ' XLY/-.۪I|U`ãcP_&yN@ %ۘ=GKPo6Eɼ'ˈǘ0Qp~yمA'* w PאHDt^X,Q|RaȘpZ1A8/DV3tY]H:֒( n#Z`lfp_17eT1F^k$h7I!yN5g':mIe ՟f3^0c1:$s?8f CevX WGU4 ,IUgt%pcj!A !H>2L@O]'3cɗG_X^[CJxk5}(G|b Wl`裦H. ӍAb3bp@LK &40`*J2>=wE9ChAZ[pIVG8)=T<=Emlz3D* NH6Є‡ou\NP 4ܰ 0{eA=~D҂{Z[i(K'4^Y~x2pvg:\ EFHbtS0ڍFk6F(wB]؛>Өo c!OH]ŚW:dK?x اZ9tZD yj% #ШazK>s¤0vIlM7fG%K%-o6 դ$ż't\Mvl'nX0HqsdX4elB`9#/PT+y25 lJ% YIM oХLo N: z_"N"A+ /A_fsv-T$}UtGӟR:NgNBN5_QJʨSLyo QV(8 IaJWS^_DZl!#i c̩!ɉu/#5L:YiMD ̈́{1S5Y5@%6"P\r5RUɌ Qn]3 Ԁf)Vnnz~-#3(|U 6;VfgocP*!HjTLs]`~EXC+/\2c^v}^vգiF5*s[tYmnV<iHCՊ$$Kn ;p+'݆ |VjH%t ˨rs;z/mydʤQ7,GpIQش"#JL,KUt @RЦ65m%QЗ3v8r=,VfD|hȿ&@T׌MŀH+;C=&i}u^SBqKV1$~WR@T0ICFA-D]Ҡly;"3O%)$ Ŏ>3&THy@wPNѶ.QF6+,vyqq.G>:Оtm^=۹g#BGͦWAyu\l?yҏv>y,%2 c{6ݏ9*Ou;OX+T2<SahPw`jl=#Z$cpM<(bU;[ .c~;`ڂ<&;&a5&/rʍ$>=b )؏. 6.8DY < ]<_W[+)WFuj!s:`Ned`#K:зͦ$51-ץ n iB0:n{EZJaa 1 ;>99>>F F 4&z#py ?iJDPmKȠf8D[QxI7;p@=wTWi͎cox\5/eh.ƨQq"&ލ? Ry:,5Mpuo"D@gμvtuF=xU+Z.sQ򦔵 &ʨ\D d[DԦ쩰j73I:vyH -0iۄ 7|t 6M`yG_бc=`  o`4qi1 ,i !ꌄ@y -1oU5`nnKAfTf#;B־NOyB)!` z2 \K)0]Efھ}{vmZM6knb 33 (8k,ysM ⊶)nԥBp8]Q~![|\ghb8Ky>F*J(=Z"῀=ηPa7 VbJPORƅF 7sUL+@!`.d`>MפfJ3YfkOK>FCa Sj|41D$l:9✪J\bEnua`(qK\mS$E>R& ilnxr1ֆsi]$(4H-K!;1șj]ݻItCE K\,dܾB&Jf2*#C5T@cR=G,*>-A 6 LTVGC] ڬ.Gʇ&ceE%n=9]r¹Ҝt_SAZJն_qZL'š ә|45Ex|N͌~ K70#>]![=ZG',,]C 9z*O_DPC}X[@"c`7?%$Vwsu_P0O\Hք~ea EهS :5Z(CO| e_ W L4O܏t@|sWnMg]c>PSGQM;_§^5)l$i `9yJoE4XR>&"q"Zl(Z9A~ߝ[o0| _ g"ׇǔBk m76 i혈;ls"JZ0dz&"6 hX9 wnb9CZT̰ )8ތv`Z_ k0KO^ش]k~1[_?~K{ { GXU _W(u̜x -rw>.鬈rdԜ-U-  A xiw*V_Âwl&/n7@A *ŷ11N0(5Mz~hH+MX bvwbl$E <3톁jXe蚌JX۵3V5)?P=Wrd.Dۀ\n͑;ZT/?!&h-gY=@8H ˲7$4ž qMɞJLSMxAN,^̇j;֍Bn[D;$b :k/xaȦc.N+>g+ A=d.C|N#0IrgESsr0[GP]B_(.nr[.h\#|j]! **FT8YCWP!p ן챠%A10 TcV lC;!d$mO]0/}We>P3ĭ.h-m#(A6.俁 ^'<CEe}(dge1<2#ˉ10"q GB:{DO#bXʥc)yX6 =bFSٱ(apqTR$"ug" Ms5lJxBiÄ<FPQ.AK;S>_l54w`Cuqǃt0] M:qׄI}9 nHW~o7flW/߻ͽsAv9w,Ǘ]×U_=TRsʸ 1H (EU9 ^m-|ķ2 uHOsva.ha^C)GU(F8MuRRmZW kΚ2cA62R ;Nٹ}ypXo;YEW-;)>bx'UĚ̪~(ZyEI/ &ϾX 6fP|B-KzwQ4` jC7%L9h@¿ܘ9:*8N|o(Uqpd0l-F"k=l3i¢'[߽"\)[j݆;$Z0N>Rb}a 1TfZ50]GRf`ZCxׁc0:b z9t20t7 ⮣Kִ.ϚMI 9,FyZ,dEr#j2ʬ{rh<ss$`Kl1_vpxxkxC$vCAH w  i`m5Ƽd9鑞Ϳ ^aN6h?|fv #Ֆ*S*@~u$jwς53T%`*MR:ruAP L>WX- a|$N=^4P6$=5 vк&<]3Z4s*Z/VP+796rg81$L}^2A#5dzkOxpI=O' sBӬ] lj5jI 'n%=U'2:)ʍa}Mb ~1u; F3˝f /[Ǔ-2Ô9:~ DowBG>xëZzBpfQ-l>Z.i&K;a Gvd8 W[1IfĈmZOpO-㻲a_jTib.l╓]y,CtqT4&n C d]X'^\{JK4=;A3j;5jhW=bhz砦,6KP1Iy%OqUy7WgDJ:juZ؅ & ^5 &z͔kɒ?W !mK,=3ӐO?Ż Usl8oRj,0PnkHNKO߂.| :ߒ2 GEYvb.!b|alcy:F [梙8Z.(|\;NXIr2\*O<#OT+3,3SXky魛жV\uXJP{-4'Q Őy{: )#kI+0U_H1vgXJȂݰ3!wJf2Zs4$'fЫ;P7Ҷ{'SSۋY>bl( fy՗u)Ȥ/4VaN#Qo8vyP~3cim+1 x7VYUK(Ɨx{i ?-Z%dӿE}2.P N߸exQ |ʽD9-zqr rylUeAq[xv5@ Aau qU$4l628PtSl`;_ bҞMigqJ ID+oɖPXF;Q3u֑ 50gb i%K~@0n@ ?nhEH k *9<&jc|:X㼩돶hqXʡ(*!oc%4@hj%dY8=,s:Vui|nW⼯cQl&Y~ӫ8<.4W-TQTGFe߈:>9-j7Ⴖm7^ h7T+X\8hi8FKV̛D+ ^ Ʃ\q~vŜ(ɄJA5οOiLIL)I ^% Km.XWa ha!qK-&!ߤdUQbCWj -գ,P`9OUܖM-dڸAT,BGXJ0:BXqhX$E@.n[:OeQ6SE{fNyi)CSB~@g` 向 }WjEh6)`9_A=3p蠁,t`[}E::77G[P W]p^ʼZeaBx:_Hd<~ӟ?'0 Ycw"Rbb)":~ w;3ׯoXŻ+ F&ϼzi"i76o}ç4* 3L>rV Q``V C ҠӇ%U0`<oF>x9F\1ZVH)JP ThVE+ԓQ!y\!|S|ipf< RUŚzЍgNmު_?  *;hwwc&MH_̒ s2jp~NdZN~+0xCi{[g?=N_;v1#8CZ˓ۋ n>zuA8 /?씤?kΓx/CV.?::>k?͓#L'/Ii?|cJ I O14W!ʧ g|7xU1> M`l !T(<K2M29G%`7˾Kۼ3x5gOK<_db%[ 6&0hMح Lg!cB3xw~kԭ.Bqٴ>gD^WmZA*G$,Q[ц "i[!aNtYmL{rչGG'~ jN Ňr &|̕ fx8A~ɒh+0QUr\RˌT@E_AKB=dM|b2fI^FiP W60+-3Q$#yYPO|*^w_W P8h/MG}*cŮ!hXiD@nXl"\Zv_nI8}rrO<|yНppQO/傻8 _,S<;Cf];XLTgєYȣݞMr_'֠8]T*k0Q-& Z  ]J[l"DXm8ЎQee׍"f_>Ŋ/,mS4|s[i7S[Q< 1K_x7l]̬F=|5V)|t eK1G,rW_!|<.$D&Vm:s㽇W+x+p#6YeIW6&DAKR{뀯 %VܜYKq4X|IJ{^B4]\ѣW8/_ϯ#Kֽ .Z`]6ѢM}trO^~Ltl̏uf}[qXt]|785ĥpY! Ԛl'+usxbH0zdipz5,ik: P.m:ht[R)QjbbRL/E}2K<=>8=!=ha ΏSFeԥ6ԤNDjׯްʘ&S8u*3yp", ic: eNsՉvVL:q;ǭd/-LƈjvѸ8G: xrơTK*dZ˵S{hB9`w*ZT>p$"w/y}I矂tC-]hƒtAƿ"f Q6t9moukt_ByY +NA נUmj[ 5zmo*YU;>of_DZ4q}w_D\ӹJbiܥ{BdoG5i 'R4.=}`DKX=q6eԔSg&voo𨬾ꟽ8xi񇽝? OO-.`?՛w7쥍#!'N:9ʮ^xfl , 8߾q/.:6*y6Lmg͂&ڼz%CG~ UhMgc9jBG9mN!3WIXo9: ψn #'DF;oɱ= Jol{gO wNNl?8~#["m3sdB'1O8Z?+jwxr|xt+^=A#",-FA*a.._Acy 0*+;+6m122c U3FE%~ok p5dp}zN_Ą>q̤+ T 0U%1tXv!>R0o]O|CZ>>Lf8QILa3o3A.ЭqAJ#fUArlgZ+(X͡["Gis\YK4_i/? 0pl+1>(!oUV?oE (qCΌio€ŬH~#i~ a%McCfB]*@uՑm=qUРׂօ%%gj ,)‡ OڣՋ>CnFܽ3k ð j,*7o' 0t.Ӯr=_i)Q(ӧ{1c6̷wvzG&b|{o6ޚ;uIͱ>:9>r/ ΉOӊl|d::a#.x?h [;l~<9x FƊv50/z[f_]Mr\΍}e+>'7!R\n Dr<4 x#g [GHͦDeqnD_B X/z/C1(Ic7nP}?QjiV)|| IyɩqOS#ޮX)-Y zϲ)p./N)G㬷^1 bk`ߜYn9-mc„MIzsZc@&Gk1u8Kٹ~tNmf(rzvk!w뛩lɲt1n\o~;S""5r0a5Ɋ=ٔ 8c>t*z7piJA xzsnz!8nһ]0PH,jԾ\3?:Y&Ž]GwmyN[ag#`Eյ*oZBBV^ # z`׆KqrjM]eݩÆ} dSFrpq$]6bs]i32=Z˻9[c H9y oϯ>y` l_Ro} '{1{q_|q .v=ܙ~Y}ڈ?92Q6~c|-ʒN|(Ž{OKvfj!k/?\*(;t&s\B[Kv^.l6 ( ]x36<+ɿIh V'p8-N}W,m}DrtA1Ǯ#y>ٛ \jSAVwd+'Y9H]l[MPՊ?b$O{ 8`3< haoˬ,.vo7Cy^e&JIbXѥUy[63<i.'uJUְ>-߽b IS/ uħ-9 ̠_ ߼Ŧ%" ͻ@J8QuA8T)2*u}Z&,gfX} "ĭ)wCdx '"Acm%(Vcyds?|q至/_{G뗟gBp<owmt1O~H.@J8V@IDATϻ[;'e 0O/ ղUvJqcttkvİvR,rI+_ "Yt e>6EQqӕ&b$@_\7) +H_OL76>2aæX> :UOƱ'~[즶Z۸z,"rQgI cr<Vt!QoV!]yFz@?~4l"%'K]VcL*6()Q& EL m*~ᄈab_\?19u =`_*}<>n4l V&#:Go@č.89sx;Pzv' @7KmHR}f}HK1ŲءJxJ=hܷ\- *? Mh2Amd~nV9}"Ix&i=M‘"]x޷Ї $=)۩.f=</ PdIt4+T7b>9,QڈS˼ EC2h}W6}Ã?x.? Th^n FLnx%ZhmInos ̭֖ja?._M7'oLWNwxYf'g#hXEquThHШY"кnµo9OlXl=LYmT,a8; mbL=D}07XB!f]mP Z5BoV <ݚ1XT%"]bjğNS/u﹚ |аɦʂ ,TڢƷIt}mE|r}1It'x5r阦gIq6cX>7#b%Mp\wAG.U (Mܳv[*ޜ ,diȐ+b` HYt.%>Tgxz~5|w~ٓvƧ8:fSDu\7<=͵:TԩA8.4k8$_J8pVFU'z-D˳eCȬ5(_!va2&Ms̄@:yH uCrPlY /-F7Ni*FPal]W~I.H<fT?m75vT% n xb\3…jtte¹|zWKA@A_^[>|mI./(Zõ/V2<e'&>8G60tjmȚnN1LbGțf̳ypCe__nGXs-RcH?Q S9,bQR}LӶZ]LuHcfcI4&m&_Fq4؇b[m}=sh!|8wH}b1K<۵VNEx&rt͏ǂo6*YHa30Gw.Σ:hC;o=_3C!D7T}Aq J$>1CtMɗV;X1L:4j7E`xL"'A ZPjxi?tUkqCW+׺ŭhaxCRL)QfؠGG;~_JoE:YM. DVhNҹy IX _FПo1r4'~ڽ9}hCAoW]$y88o_EDZU&*[Ѐwݤڪ}'Kfr:?k3S B`ɅvMGԗ_/ޯK`ϑ":)78YgXqvrU0 v%Y Az#G`NÁf7܉ ̸ Z_8T6H'!G(-fqJmឲwDƅ=Lt-&>j#P([[:`H/-tebv]J(u|ױkޝ]go6ݞΎ읏{w={ve+-:+|fh:_;]G(v Z ?ڻb2pQ%L[N $ @ Usňxlzε] -'M= /ŽlPCŷ^Yp+^Q 2UJ(hWdM*ܻ|%b.⠾:ŷkAjSn.UNi=;Շ>\ݥuDLW[!{HBYYgmc!+a&p厾zr 5O,dNϊ,f۩4>QR*J:Z#YޟQ=JgQ9bc4pX f]C矇EXcJGSЂ}וa9 jp?*pdZџ h|Uw=a* ǥ>LA'խE}\L} rF/AՖ\=OVp$L!\*L+V8:8mMAMJa>/Jj0jhفS x[q ,/`lpo35+[Žv7%.NN͋O_RtzY,c;w̪*tIMӒ1X`lc-+ Xaƒ-h!R !q#q}3KOǏw?~!qX\ni;UBTqa!x["[c a'MЯeAN/jjbaL*"P1of\^{^b UDafS;hvH~گG-NLwgL_EE蘻%)RF{-J!dmH(9˺E(bou{;^TJV lSqP=8@q4xU޴oC@0YͯV?-Yk~V/75dQlk߭K48tt9vc2̘Kx&92dsX\RH;J9v\V!30hJ-Pf"D.Ng\gWōϊd͂`nB'RS,W&wTo_dlbmck<(dq9M].ź`A\;*itKrhS6 VQ\]nh/.J+]!zqIG0M cӃa/Lœ `㱶9G U8E;Wczy5 9'bDY(g1+D喟` Y%=W~Ỷ#7XPO |V(]*[!$J:>CZ ࠒqH]xp! F+R6|0-*+map6WUUXΣ~{rE؇/[-rE[`Ͼs΢\@É'bf$I%1o҄5r_ S_K{4--ˎ6kSE!dnӨl|aFޑU`՘؂Mua / q昳\#à kLlubex(XʻT :E/6vJޔ깮ƛU˕G(Y_cI)\ }R@U^_^W@MO/ (Jwil4zW WGz 5U))& SSxHtYfLe^nM0OU¼@mJ0Sm+űB,+cIް'QT])6X"l +Pm%n}%f}<l2&G5oH8$ھֹh)wow'10Xg`w.("Z1bHVol%ҤIio@40?UA^gVޘ &h-{,EըK #Ȅ7[5iՊwaZ؂d(9en%/}M+{ʮ[iUW,)%m4$ī~z oݿ M]L`RG7%() q5/ɗUxcZ>\RB-ИwD *G䗴g+$VZiU~ o7% !q C$XOaՊ/ר *". "lu+ K*O߳jˑxyIr&ƙ$uz3&J34g/*'ÏÑMߒQ,w  F11҈ Glp1E=eΧd7JpMse[NRQYdY\EЦXH0/ 6L*"{NE+OU]DaXK5yzDg 7@V%#;- w{ob,eLLʢ((+_F1{=ょdΰڂL svcNNE8ڍGމ4x`ǵu:KxlfB`+)VCÆ/L@*eOzЕ2+}m7CTa1 B[k.Łs<}QJF}"ru$z;!&;Aͭ^Cjb^zyL Go9ρaVnnNϦ53#mjӖ54A^)qˬ &,V\w M ҬwҘ@\&tI fnrv5'~:INQg=rP`סk^xiWE.c~=ASICh&ߤn WTci&a\<p)[ک4A~[K{fQ_TKwu$L18r7jkpĔ9[00Y)n/X퍠}$6D}vÒlr+,Lo.czblgd`bFSĢ\Ϲ$`8?"oƿ ,奆=}*<%O&,ԁ.誙 SE|^i֚aSv5nd ~DkmRLKL"_Ie/<ګ^a|oe8MY '3J aiZ]ŵ,L* ϠMMZK$}Jej+ʇV]qт./a݆Ӕޠ s)Bn[4J ʞaiKJ"ϰP@ZגI [FP֜ƪRtX/TMZWׂXnr}YPs⧺ a};h,U=_ 0U)M#凋FJğF`E+g)OWxS;n^QlJA>qcQ?ci%h5H"F=ӍͥrVc1zþ4Yo:20hcڊ^;;-rIu_s*^ᚗt]@@MrIj'dW-i>$4bҽs|mB^_v*@FNgWSKoq$Bl<;yQ'l1{&,GWRNwR/^[J+vr>}N˵x I8"]lp cNH<+3Y_ͺ)+;=-&Rt޼(,]` ՕAW=2%5D5mffUɔ; E>5="[PTBX_w;EQLf(33&0^ꨕ(ZBW~vHGLkG7ݢ =T !jJ3$"E7Қ@T)Y4DHZ8--d\4S %ɆegYJInj]M=S .u)}-.*QtFXxItXY156)RߘbIs/isL|u5G)ږQTR(>vjyRTjy6;z/ D8  H:^6SOˤ*IIbDL$@⊭nZӼOSM'IC^`qy+xA.v-E)>ITzBzq,"V2 04))<fj|1\ X#U LS6]TLa7ժw0n/u ſlbCW[R00t=쟓bnm-kKJ pN՛BDl':eb w%+֒]Q5}x>2Ϊw{pze= OgQ{7/cA3ݭm'y1y5(<;Ll7?v~eIڥ{yi Jp:Hp⣪#O}rkEs$g/>§Ģ (V*6eh,!ͅ{ 6萳X.,]*:nNHF@D̓Qvt*wsN?L4WĒmaJv%Ud=Cҵ +r/e|]%^qf0 -MXUq۪"Y3iRA2X?5RIM W6BOVި!5՜Kjx6!k0-%)]{ɞc {-KmlKbj_ݶ7iTMn<Օ )_Zǜ)JPyG[]vO]m*T9* i̇8 Ϧ(/INF!t@VʺQN)OyUz[zoQF WtmZ@ը"G؈_ UyTiqodH|<҄, uy\I+hNPys3{z+9Җ̝nƣ]c]>q\n =ȫyUSRvW0SX(j]J{,݆kV8loMO~<-i\ŨٸH;ⵕSҲűF.m[Q BÅ^K-0HJIv>EI06:uH5r^5]YcR]z̓eG~]+!kk`;4ܫ*(&唎5,TCz"GzbU./cD eHK 8 NژդiD)3RP*d W$dtn 8Է`mH--|RM2~@*6_XSYk>gIZd E_( ;oip_ ՘ɷ!$CCUk]I3/+2ZzA>a֌0o4ʫӎ-3I(jpd):FC.^o=}C퓥i H5J_Pqhj8`\m䍒7ۼvb~z`&Deu_TMor1tZTtCjDz_*WRb߫JҬDl~3D#@K/(([TRJAPө=Тi4tIζΤ\vUCCQU{ ʦ% ˘ŵ REddݒUܱ8Hș}綪1HՄECQ:{̹f0Ӱ -SK EbuHvaD;PptB tvRI^'/r5[.˸!l 'hD!}`ѭAܣv4!`Gќ0d1XCƕ@EALnMٻG& ul;DbBE~])>w+ ݐgps2+_v/0ehd@mZSWUCGmno#ٺ0uJkcS^D/$8Sb l1m^#*G:%}Y} /da)Ҧ3G Nid)ͷIM@1 }% ׯKb/)=]\|$\5;=uﷲfUK]#c<5>Nx#9(N_މq|-kR3gg}Mk.i_Z3wvNT53| '%aUWÑ7+ŜIi5 ~(eDKersV~SF\> Ua9.)>!Ԕ ݽߟ(9!Nxp@lN}yo{1t@딫7;&|{)W4LոH%٥ F$$$\n@pay\?߳:Yd|Rf2XtQwh2tH4yЇlך`JZbMQمH#ħw&ĭʔG'l$T k7V.t6bO+Cp21"W"R'0 T` @.VѶ Wnc;>bEm_>/~!>ܙcȱ9˓;Vl#t܆8 P.&aZY} `KPѬm4L@2'vvш0Hvhwm JIAKd6zQCvcbٝ(̫K 0\tS.kA qs6]ڱbu[6c91s"q#nɹئ9b'=b/秆qqQ/7TzAqֆ :/s VAW#SȽ.0U蹉a?7%"i IWq\kHŊr^dx+%a©ʗ+d:P!{㑼b*5U<cOBiCѫ#qŬ%{%nʦ{[Yq|2Uʬ: kwFV `NLMw<^cn0rJmzz+՜go !1:p&&ЄxT&WV;!\?_«pT/$[k]I]^4Ehpzx A0(EQp 5]uQO^b,d^m,5f87VwNtRz Ij6>_Tf14:F,UdՄ xkIݙe"ʦ}]$؆(Y/Ex 98j][4ʅ{-3nz'ꘓFsáM,nϖ^["!3Qםfne[d>JjR3pI.uvxrz~*MnP&(MI zqiY΄}z‚p0/`ua"tja4Y >D- a_8G}}Br9YZpD[@IDAT_p 9u_l NG{TnkWQ-( ؔ7Aj)#B5<"*4q;,95G{+y֡QJsB'=N65">& QJl(yy.MƂ5(qHT G\$PįUhiW>ifQ9XT6 ˗ +2 a؟!& ҊlcS20ZVkcm)11G-){[w*╦0LF Pr*Ze35-8.&X:J#pomW7Vi눖@t]Җt~l~<~jV2 eڗK A>TUT_H\K"x320("~[13(Ƌ8'=+}u0MXA@c0xi<ӣY,t>3խKY^ v <Ø>;^^΀ !D`Zsdo3':ANT|mVc+QO.F2!9WaA{`1p';ZIq !G)iJqq)lW^l+I?jlګe>Iu#cج^ Q2Kv/V4.]aO۝ Bs^~U+m@%YE|TPB+BI?h%9o(deiKvʶX@Ha+̂r)"89M7{K7r4\MOm&W3|Y 5C$H~KoZfdk:'fUi`٣7ѹO{X Xk~h4y(]tNh;CSkKႿBpR̒ oG:m> 6!V]@2lumu2ddDq& )6),,607+܁H="7ЦHf 8>8.!O3{njX0N9n( ?Z <[Uʲ=NiҖEP+0hmz` .)0"Ctdu4"N]`DZ֠X9MI/o>T=%u:rW"6'KisJM#S<Ѿn+-'EUz@`vTf`9Qy/[.h%E8`etC$RV|K3U|cѷ$ &DQ(k`eUb/89&#ߍ݃woF-r9E.M)gQ6*€̋E\(䧋Єuc \ ѝBIe7!0M΋x,ֺjb W5i+ J' ?Jm'e=H;<0 ]>V[_cpt~o>ݽg7&+2Śq#RtOΒQjNYfDSvHjЦ+KX\j8mp~sUʵ%XǙ 5tҴ꠿_e6]_ Ϩ.*$668 ¼`M/I E0zEayjC"4>5ТK, ՙ,} Tn^t6zÔuFm@H0Fլ~ӯ?Y6,5(eVNSnSZA h1t\ BR9Q6M/)N?ΔFI5h*v~ LXG^/wFAWU:󪠢"@)m/I&EkQeTjaY{#'\H8&\LݪB,AGB$Ksd3w$klL^òjium,6z Fŗjx][P;3 #ٟ29| &6w$.Fol-Ef0v"ut!%(` %O972hB2 _ReĕaI|H޻" ,[ormIR ,56)cubOSЧ8flx=UwNMX:6#7P=b8)䷷=%|<?iH0Wv.#n/CANFNoLyuJJI2*lI#ҰE2)ӡe۬0W8Xe:_  %N#ID6Qkks!^#Tja8tQ,A<0 IJd/4oKЪ;G<1űyw}D'}!z?$|20$LMł$|)5>@H.G֨MkحK>&(mJK7*,<\LMs̊V/xC2VVxEyo<=d,%`uy`I@ * fUi'[@U%M c,= w~UIK3gdhlPTdj[n'НVC4mP-OC.]1LDzF, ZmlcPc?_s3 f<Rjz F#VgovIp\UbjVz9Onb!YQ]mP2ط /;YA2ԭ5xjԓAS!h"t׀\.*7k[UX|y%l!G-DMj)cbV,C֖Vd*^biichlMi{BY5ޘmO*،M&~i'T< \< {ol_1:v_\hQcO-. ! ^V+F" qosEzdعWoLk>pN/ӵUZlt)e6n0AY?dwy>Tc@b|t[:MTPxՈZF_\@ B{֘F\lyqN^ =w1B&܅thGkCHJD`Cۥ`_0z0g\[EX exb q:3EF5<TJcLͥko;a6[tS7:bD|^e *0n}km/'si!~¨RT٣k "_-?p5541\ $dSjҸlKQ`+#MV]2 `'. t*;hkDD C7+L: d*-ӭ6`J { 9шf']><irA)jLdiGѶv@꭭>hs^-/sւ"@Ui3lvNO_e\)Ҷ彖b8w 8C,RBϛg ._8#oeww`Ik o䷻l^k\m< wU G ḓ͌-q͞ CNX+ x<|!3]IXokKٳ~%r|GjMo&w%`i1VJɅE~H\,-b!ZG-[kKVԌ[^g;Aj[-ƅBK69"t p2$k9tμӣ@d~.ԧHcקt֖@b(UQ-n݊3&) *B.dc*0 ]jrIeĄxJzz܁4ٝA\nʜ?7?x{j 3Qz1!-29ܣ33eŘ*eһ Pl_I7n`C+ob@$ª^uQ:|JjϽu}L?c^=ޔŚk_#0 0Esvy Mf7,16܄`CxV-PW!D 3sZ8)'|?qZ*r&#w)AH qIj|#$jKi)b .:Wb-?RXgϓYt7H۶PonBY:CzaJgDUK;?;<?}~05csroys?Xo|ggneߵF:O)?9ųe"4 ֖W6żcF^lNi?QkZ;{rq@%ڗV0)af ^o~x7nt'kNy/8mz,15Fbc̠MJWSi]=H"V>WL9[7;J$uhG^@S){ hoa4J/! ס!h18qX31ċci6uFČC0<(m63\pK"¸?e~q>Id:Dw5:9::6R3qq΍^xzhW-TNޘph䮸3 ;+1: xLi˓sƚj[Y~f rXQY#ie2"A;8QoҐLq=%: 7m빉RfQR"z擃 Ml%x7#J+T*sx(5F!3N iVXðAa!W?r4QXV 𔯙O8RZ\jk2)k1%_¾*'}( | W,P(M6sXϢ(v0[Kxlkݨo al9=\![-`vʂxwI0ןGab0b";;CTgU8{y>}I"W %q?L%BV -ݿ \L6x9xg&kQ>tx㤭Jdͣ_?&mJc>?QG[t~}:YzrraqJm\CA7ngf1Y O+C3lft sQz;xQ0#$6/" MMd[n\#`Azv: 4o,MlwwArP8%EAEizk[;n6AueeP~ʹ׊fQmq}j H^`X".ҊHL w0P1S}=؅ܼ חU.BB- 4eN\A gQdJGyA9_8 JI^۲F A+kzT@k<RS-2E 4m@AMmc^x}LH~4=l!bR0*Ɲ\9Š>+Oy̠.X#UJYQy6 6j_eHf䐗`+pJ hLJ0 U=Dla@4R$`U<&pxB2 8Bkd3(S SO*m( hWa2f̪ŘC)S| /lb%cmW5'U}E60R`Vߕ *?)6`_Y/X+uet|rD5߻^ڨq~n}{K7wFG?./=;yx ѓ8!;2@ZcMG\w'ghMv|TTlBìdH.g 愽Ӕ()9aL" /qģzemHM~2;>??2:EmTPZ] xs/,acOB fs;DܔrEgz'rVԓ@%@"3,֛ܙ/t `v`B( 2ͷ]bX(e\; FKLB;VYHu{$`N3===IK͡~d*C:ĭ2]u]qY[W.?C'G`.nI!T|c0@Ü8NNw΋T^؇fJၰG(U!)br-Gc82>HvΈtf),gYھe c w.m=W<(^> q?9.gt=?EGX1m =,峭/0FK.δ![^lξˍ zƋ7NY#Y@J^ɢ?oa{uA%`?ysƘբ$}} ga.8!FϚ~&4xS,*;T2< <;Aa SfrT!1_M>n,9 H"i 9M;1.BpU^\48Aʀ@˜UmIXƛ腛#k4['+S` h ):`@[= Xsƍ.9Q-_a;b c}#V_;1s`P<`qDhbww_$bт\Q" Dd"sǘ slHҠ'OWh)b阥:6YpO -TB7eiYlhLNlηS8rZR#'{(E"?8"ڏv_7`wNwkOBi0PFawSp3 i)eP3hSoHQ[j3^fAQ~&N`,Jc-+l|}g ERd-fOZuYH?RV.hI e,©8~B4XŜ 2V`h*>_bQ/Z1K`K+^\PqLTEP*=2FI.;03ᖀ}eݮebQ/Yk}I.'_3",H\8,Vclp0 ކg#4=Fz7w6{tnX -}gcQcR5ÜഗZ)8C}(٪^1b 2CĨ9ܶZ dk` mV^M^4١m|I fL8Cy]k'0VV "] 7Μ̾. XB]\\^2\Lvo#N"0"VwewCfNIb 45 4< nf1wY4Q |\*OMǿ?;Xt;'/w_}j !U<=E9}b/GM7?AYz3K_a5HATEIj,qVHBHдVs;zS܋[H2l)]L) y{qw~_åٟupt`HQ%MbcӉJNcK}a?_obfffkTg`2>ۥ&eeYK:^]%%vfkav}pnmww# 2p14S#6։n<2x46tO*4lBs|^  /2̈́,aes •OYskX4f1Ȍ& A\aZQAlyP:ZÙO!*e195Ba͊ ԩP1bAJ7Eixa(|S|-)}bx "e5…@5FNLY!lIUhdC7 iGN)e g7ee9`u !j.\ ˝ү2HɷEWZLknJd2ܳ;Sl+L)4W5bwDXKhblCO+ZW`x~ HyůIJH28}J7T;Ez bjFܱH,>7/o,ͭ?=na %(<KߟH1nn?\- gZ'0[#ݞ.- B6 LJ ż6!cUS3DfiFgK [dG)YO B6DUHoYBk{˗;?k.Vrmc`Xls^;1MG 3樷q`ċ1ٗ=ؼ#8X5h7<;_⊥b<%b$a!*ymPx739:~MZ d*WwwᐻBW]_4ԃ]+X鱃)f<$B1[~1|>}ӍE; @rq\0[M.rb]25fZ'̮ HF=> jJȴqS=2%F-LSjgg(h3Ė]Bv K>&#6ؐ&}zooW>qreN ^bcpд9K\xمl 9 Nޘz2͖)zۡuk@ ĭZb{P涯__f [\sl5p!+o':N6]~r d=rn'g_E  yo8wVYB$+_&u!oPd&C .'d.D3?;|q:c!LX0M-M.d[2ޭ/bcR Eo?s&ïLl2|f_FFS`O0M"\'CUIh+ٮ_!8DSqdozDn.`iQrEg롻:s)bypw*GNx5ABmDdFw"nE 6k7bYV}R_fÌ㘎רƬ ZX$=2 uo|:/gW''G'q֬_o*~1CHf0af%~ꥐ<Ŕowi阁`(.+DiCK^W+HwNwjYk{edM%dwYrG' ɨō oEJ3&4UQֺfi,aF-7f;aH{d=A)vsB"PF~MBc PI~%>eq\!ʜ$l}oјT1=\{`pK\I07rɱeSHxZIg{ۊV/_ĕh/xMҹ*, jqbG Aly|P7y VzHXuUN ]} ƼH"~w`ȢbbREPm5%׊>3 ̴nŶUkDFJ'䐲wpTMјև) vb'IIT圌^;O`2S2(s6߱D57fR&)TGc6:˻Q?0BTP1WPih2E=Jw;>`CU>fG&o|#T²+pw {mg i}]PH'Ŗ;;A(qZniݜ\XrU0sNҢ&NWV( 'w99ylt5/0#ra'I?5}?Ղ_SlM )C3c<'`])R&g|oK.i{m_wLU;|k/;ߠ# 6ŘG/{[IDdyuyvq-¥Dd ::z?lw?y%$uݫ7Z%`k<ܦy wLps*}Nx0dY>lE5'#nİnfo&9˫`Bǧs+z;,{q+&ĝէ8a-ѺcVb31 m<&ČM0dv }-9 <://ω~ 񄤔]:"[Q@7B!"[D˓ݣ^+fA ntMr`8{??SS}!խ=Rơ+YB=U3 \ۘLݞ=fz-|Ж| 5~BiDFʦqeȽ|א`L D{aCN üYoZ7xxow[NrV`FdɫY6ıWU p$BSX]ngv,B1iȺ>u 1¶=p6z]3n脼ӳ318m]|98E s|!܉Beq&xެ1$Mk q.m1D x;6?9 :ܢS|6x7?ǃ%!\u*Kӟf`&yL1XЂCiT 1Dz|`Ѫ zDwmkkrr[ L--EdWF_S ATdm y*vCƩS 2DcX}ӅHAPa6 `#YLE%;I m3vg1i~#%2c,U]]i L l,.0ܘ .qGB2BB򀰐,# dʹA=x{&S]u曚8~Ov[t: 4 BIP ;4D[$bю:1D3B3SX]$;1r$rTpF8P"J$.kSn{L_t $M&TflgO>"!!m$I1H! <lxx11dyXO!~NNѱh'j iNYeDSNpd!d%βnXZAAw3=a J:/ ڐŊ*wGs.s?,I^,&!WfIn7u [+M Oz\ׄD:m9LPtbؒb ŝ-~ a,#`tNNKh}7dQw~vvI CXetǽX^nl;S߂K >͂L$*8hxf*M\Lp}y 'M³ U|,J,Mֻ$pq[@}$./!(m9LMG^O;\]'.,u*eW=hZ3^B߀ZhqE$Rlu]$XXy#44yEodC &YEVxo.$6Plۻ :!s15$!LRE$Ԑ-I,F~15q6z*s3G,! eo߼ߣR hW##1j)O,R+Qh Qq*a`s%tQ?#T[!l0[.\3'GpB\LUcZyA) m@VR%=vlWxL t:XexU斝{״,P0Y^Pv82 @5`?%hV (fb"HQRDTkrQzb 8 )Ɗsrg&;j /NO^A0Ϣ^ @IDATC>k⫉(fu{U8w?\4t*"f#㔦پzl1b~bӉ& [l#JHhw=><Ȳq"ۢGSs&x@P!3UEnw,V1*IuԳGWq6sCx LBm>nP>OvS\B]"ܜ~?|2'Yv8^p+f% z {~&86 h^d^(dSe?! yϠaMc&ZGhx G㱟`Jld,H[}g-nIYdH\ F^ =mQYEl}.z Ş>Œ  dѵh&Pn/ Bac2P4 /GLEJŠ^IdJSE_%,l0nnlݦbSٺRazAKDu_.s4<E蛦9(aSa SUI~ "Dn1,INJaO'eC48gTHvo4S_JU?cGBvU暥 J5׹+rbZ ߠhtfDe]fBUB߯(l b{2$J(Np>G>~K 3it3aN,?- ;x)ʮ~V[~ǿYvO]թeg-҇/ÕVwHߊM8fo"SɀRJ>fj=ꄕ,S,v\+d7F돓 See=K,Ǹno,hGioj)үr{I+[ %FTuC֘Rb0 bx fṼVWodBv\w+4/4$ֿwI7Vu$D7MZM&s[_[D`cN)ȥe z$ 'Xz}_Ҟ\?JW"M76i9&X-ڗr3I2@/ZyH1.즆--7VZ' T|O+YMWt”47 L&7kE@~ 2K@8!6z.`Y]p5%LJ)%*t1`Z+?+:_L'ּU/[ jd&o?LWD[ M2Y yC \X<=>HJ\eo-1?7~d%梮=a ߀EWI'4(y^{AeMGfsV7e.\5&JO4Y=LZS X&*Y:W!hWNc89ap2L]F|s3rWCF1 1̻[n33GSLZj(|ҎvA\(TAIhm| 1rPQɉ8:N{,ftYUzWdX|q@_O@UTOf@֒ l`n )4lw3BTL[IO= +ߓ ծP N$'Fky*U˼^}7R5&alKvm`R('lO#XO&lŴ GP9zuyuCnK!4՞ϐȫYI4,s{ ) !"CURlXt X,Lh^Z1 늪-;Z}I0y6 ʘ0%)[b}Waog_PΙtEؽųg_=ì *h?י<"c/E,ߎv8`QDfsgESz٩ˇѱ;PnqThs Bk7Ugy闔9h]\ нςә+mmՌe?K?_[k a5&@ٜ~kWݱ_Hf7Y憘_AHEO-V8x(Nj]I  ta`֘o̼ 5!z@ݳur Ę usaK$ Ks;cʬ/OnOJ?AĘ,3+ &KF ʛ6A+1OEˑU#7K %d$.b1+[٘`UL8}t>'Eu{t?$<"^]Q_=SEK e"~i^PFÒr8IaCONAQ4Σ$=[93gE[h&v8v&c0!llKOq*o2:HQ΀}uerӋ@m&JԦfo'~5Ȏ;]b$HjyH3"{k1윰MŽ8|b{',σk<3 pa]JkMX2-K,/vdB˗2Fy[P`!j00On8pt$^om`aINY`y, R(Q!8dzbu&@Delj0bKG|SeKg Gimaʉo0#X.~ 8fZá\cǓ1T8ʀwW'jBQaD(n<+Dj;|S1@nֻo KGd$SdE%:aGHaCP+Dm[)|(78Lr;ڑT<ށbPn}n*̘IYZ$ V|J /_;TrC:e(V:ǭ*jyLFo~߰-_}qߍ'+kcrY)W\_f>] =8|b ͐E! AO)㑞!$w2w!ngO6&Œr@)}+<=5XbA,9HlbKE0މ 4ő=Y6ˍM5f=3"*ʸg\MOJd^*a "]oT+? 9/kqQM.~;FnSj6CEr:|[:T1e;a\0f_+7 A `v٘PItp= EJ(( -٢c$ b <[M1ԏXGx8 2L[U!s<8Q:I\`'0 815%x ,%~WBiA  G!^A|AԮf,DbÈ혩J{˳.áFBxǜݬ4LODb(!>R&|1P 5ՃU Ԡ?h$Pp [#Ȃb>/BL;˫l$V(}??폽fkݝlD3%KZlt/OE}7`%.=KIHrIL{eHu8#iME!ecBf@O@FWSڲ00!~-aR 1"xϋnHdMv)!.ey"+FՍtmN}}v U?JoWB彽#{4M$Da2pdc*k>6q;haY-)Rt*,P\ 8fKiH +od]Zp3V9<2"G^G=RҼ@'lYN o&0m)At9% I@+`oly[lm LɚPгg̢(~'c0\1n52rZ޵P3 %7"^<KћjwFٻV۫|"ƙ'6⃻c"Oi@ ~$e&b9fC̈E2&P4np#;4NJ@)UdLa||~L!DL-DH#NzV)F{rqgL _Myɣ,Sb#StnKkȎGﰍUXLߵ~4[vD%DelӒ([/#hT|ˋ\`,6) r-*m``YY5 م,U |) D Jw?>B %r7f6;b{>'b6EɅ b+@2R|-D$HvL3':v(? ,ۼXyN">eO2TVLGҘCY1j#dKn - gԌ*|b&_Gg ;Sf¶iyv`1o/,2OD=X_bzwU j؜VI&fF zMB؇O8%4"h1E60ڇ6NNt8o3ډ~B ӔAR"LuQVe4㾉i;jН wTm vfr, +H;eD(Y7[y=:8|{[!M ;LJG **#yf[e}.?}\c`zlu}rr9t' AHdVj]3uƠH*vP\沿MQP`UYӘ ]m{'F- srGGQ\$-nGMwۛj4IXi'n]ktC(?|vq\p)_K3+J DX٬^k$IȒK(i2k{J9Fxn%&jҐ[#֛XyqSr%j-ԝ )?6_'g|`&W%= E%DC$k|yկ_v(.WZneܚ[N_oo^IP.H8A6NbK D*p̞[…d,"mZ ƘhD4HL"$P#)AX@Be6);[W{"-EBy~xGAro^YΐiLu[&35bBj03'em~L"ĻN3f fˁj&4W9u3 <\HyGcD"$K&rqq@ aaUs/9Wrstm/b`1I%!n h 8ZxsvmʮlI r8,j!. 9`!5"WNM6aԲj=<)OFw} 8 lW 5ku,C$'%( jVےQU:OZ] RH]l Вn,ۯя7qnF<`ڭw_;3,zs<uA4Pq/|B7p> ݦ)̨jXg%rh:Klq8J4Ǵê+/@{,fE⹺c?]tǷo>j:FdiARP{x$)_ 431 ǒ t#ﯟe8AGM hNDGbX^ðÕ}Ęlv@s*;Y؛smU7([5U|?/}X3m|o1as|p)Fp Y7v7D8qc.H)Ӝ|JE'Q3½][NtP2Sx z D%u* I(N@!$$7n'P<(;NMRT:ض^(`Vl^*o'߳7rJikEHY ͠'2v}Z{Q!NI4:Ex2M!5f1\>u S$=nH (*cr&0*C?/0a7Zr<ԏbHBJXI͍F@2ãD6RizL+G=ؼtT a^%qKAt cF*6ຑb ʸbbX;EX{rʍ> 2>%GLUݤdRΧ'kzx;4[jXK#GR{B*̓m(HaO*4(D664w?b>8FRcVOᇝteg ̯*u`? +52K +j>CpĄ޷H'Xk$Iu8څO>kHr8bv8wj>?|l OZ'MsuEgU:woCsnVZmӵmɾqHl!AT2X?_/!IHj//: Pm#oɴ h@,IDY)}YDH(7R9Wz/+ŕ)MY/L!?FwO ~iUM}S $~)h.ׄ9 8T3% @$:X6i 4Ĩ)-vw?\zAYHJ;0\!1C~"2鰔 :cJzO.0!'mՁ"V. o ʴN| V /jJu|&":\_`6F n;qfbwӺ8 o0e,0ajEPAZ͓2,Gq^P@&| ȨZZ,r,UX쵪Q)xW;|cq6i@/SZxjcاr:٩o!}2+Uε4%kB#j)R# ֛l)291Ғ2+5,?M,./Y:)s:(+zK_~Ab(]_ڮq6ILZVN{~>F\wYP>}'%9 xy;+{##jYT C(4ah/f̣.L#).Ī֭(Z,2x>ryjlVa V?}&bvDZr#Dd RfQleO`FFa`' :f?u}L`#K Ox훬!rv^3:joŇ~j,^m4ŧ}v2Μ f̿{?-n(OMKS+-AC" J\/a~`FW;~v!P\Sƭ#u|c9#k CH/#ݥIG1p3^i".k"I-+,wO.Wrg`aDSf3 t7V6+/ˋ(Q cQGO_abw2- 0COF'Z$FK!$'H,阝kY"3up^Z|;z"1[씶iN`Y{'0\7">+z8!<7bR{SY* Vz@^?yi. 3W7ĉKqNrgu<|y$"yѤ}LYĪbh-|Ӏب_|l=~߼u*h7N=P<75X\Lb)I"@!Bbpj[xO!vPږO$Kx1?7IN$B^Gib& v?+w{O?!g0lMǣ@ E$'G|IY}f9b7ʢBqqH]m.MdD[nIZHV>#5䄁2$+LE% LFıI{S^T4E /g -ekA m8CU~jܰשfNƆH8#H UJ^̓GÝ=&.n %>G)Z3E>8d^\ '>*$*z&nO j$\^E>C-"E?#ņ9702iu5ర2e>abK В|e ? OGLƗ,0OIަ>" 5yq}Ϩ#R[L&ĩH;wɞ/Xՠd=z@=l|`'YoB$?~v/xX^q!wVȠ!k`KVaVƷ*#,%79c'X9ը?}yB,,+89;@@)R('!UQP`h| `CL#b|BրNlI R[lړ)ǯݵ7ɸXmv썷$f@$-i0sНPPή c&UTS'TݣǩpA?lf"3W L^F{y(+०b6+kCϙWW+YYnium5g팔h\:OƔiಱ[Y_nv@cr/_1JQׂ'SzMMLMRh|?[hy+|evej+ PL,|jx%;=Fq btz[8v_ ބ)vR~#~a[Sn]z|ZiCKslٍ83:lFT4u| Efb$9JRCBt]PL\_zh"@LT#ڜ:=S0*Ȑ}"DHa'48 ʻx55,duRǟ~uQ5dϝ} m5䣏{/L,,d(;><|ɫW% 9V6Jdq;/RBQL#4)t4|͡j-8lSBЉ<:8FjԀH+Be,)ާYQ+Ez9'J5aqE_V%W!+ #D%!{M.kꤸ=R>VDߌf1d0.oDlSśU%~[Zݜ?^x84n^ A#WfEkb >|4C* l?yP6aMG6TZ t tb9@Unऩ.H~)j&q39;Qđ Dkz:8<<RP=M k2sF'!wVbuIJ$M*eO}$wrY)覨!2=-t>?C|1c gHe''dNjh-+ !jaWRRnYrzb3F b0''݊6bE(ݩVuFBz;J6_:aCM'yb{p3w@"Ry@SU8"L"z/u 9r2$&FX^L[B(pP6zW Z7ON;9![Y9Fi$>d7ßC*[66 =O/GgkrlL*]U|/򊠾MNhI΄) wՆq9OpE59>93vwwYB?>=$յ;6f뜛>ڗ.RHJX>hFΘ=}B:P3VBBsw]&vXj ˯s&oI@IDAT*$hsSe!8Q AO̧o8v  X(}٠( DSbmda8.umKV鴫z+jF8K13CaZ %qLĝ USH|WVbHm9LKܫUNU°5ePh)}yHmJ,!wh2:el=*>ȣ$n{[kU}!K{Q3̛j!QˏyaeMѻF<^8+pZƖ^FD E+x 8 hXWc^'_ЎyR٬hM JBΰ/qۺ"4R6t)WNU hϔ`~F6 f/;̖O uΥ$NSTJMeτ&>&ZDxtyz: k&aNnᵸ3nLՕuGkdmU{#:QnItLFw46SEٳ_م嚶ps[qƭT^rcӃexT2kVI&k"-q8?Ard/Pm#4%gč8󛑊vxNpMq!f. TԦq0pxć&:i@´J7"2|Ŧ, ȏh<~{E+V$# Ӭ&,w>})i}2/^` NhA<Mۻn4̄mz h3G\\T4/$b`kذb*ab]"7.#vB<QTuuSc"=WJzbbi!'X yh ]jRtP$*ڈ.M%Wkpoޠ ORgF޾ @;&R * SHd2Zh\sێ #Fb;=z<6Ȇ{,'"Hh3ۛwPڊ"4Dli㺴"w\ 7j*^Hk6"43&H" ߃#4+tOIURL}̙f0BuIE>6|؍6;1.*&!ZBs]mn96%z)e]EI"T 2%l|w}V4,PQNٛ ;bO{NHɇ<SK A۲+ϖhB6 iaQѬy޶4Rn_Z Ha>c)Q#mxT2 ?k&+)aWK0כsė b/b> 7 VrV3ZRdl`pMbbR84%Hȋi lDkz,Ù|tzAicsٯk=el#St&c.Vkj Y=o$кLV08AFij;W=NHb6 &3e;@O|PvU, 5y T RD)D9qFLf.bEiM`vKGj%BO񾕎$"ԗߴ(YLN Y7Srr K>\s&;%m$<)D̳*_M#GSs۵$"]{NAlA( s'<4rjPt>®RnӷX4/2ЯE?lzI$ 0 TYƯ} BΞN#tL_3gг)ٙӜvOf%;W"TOC\(ΐO[ ̓`{/kF=$Ob1‡W B~ M8p[UST2/^wˍFZ;xЧ1Lk"m])>2a8OϟPb8]>D*&J܁tFwʇEQ5^DsjbDYDnTSuy"VΘKVT EQ*7 k˫E3p..zb$6q( 'sڴ;\P@̝L2c#Gʥ3H\.-1㯐gjfdrxCb(ceqbk~zq?VD <}7ٻa ceKP b ּ=9W^-^)ng<5޽ RUWTYtIuhϜk wg.%"M@ ( usDAIi'$W`Ǘءj #nD/_y[ E60Dgb^!vlXsz?wl$xD`!W6:8xoѮm{hyrlL18 ̱з-~+22I{ TNl<ء$:gzRYdFГ1aShFODonfc9+{XF$H*҉D!z Ls)u@viU}sɩ&>I&I( X_*m MS :71-q|5Jaj[B%}9RUOiQ̅ NX,9>oӘhBBhBe&0W3L 20KRч9\L^э` ;>!񅖴'βR'C ^:N"CGs.gBѪXy2:esMRko*̌Xx3gu(z{ev=ZC #PL qb-NAlliO:U]nl0g`MOe-rHx%%V{LF lK@t0 u'/z[٫JW:q1#L"&Y5O`y7į}AοB߄z6HzB^hl-$>r=Q5a:C3oEۜ.1Py;[ Y-ErAwvoUOU W7zJ{vBNQ 6 e@'ѹ001%([M +; G 5+lѰdbCR__FGqv0^^&gF5B{Cpݹ [ Gy+CF=7"(DӾ@V#9*F+4Īpb&&LL0M9Vh U9+7(b]Ǣ,e!WyFeoNǓ;;jdB(*+XD.cJ$.L7zK"XJAшM\> ?QF8GHSmG51B1J22"6e{ySL,T(vC^_M߮Tqevvx8hj*ylahy"xG) ݬ29~@91I6n Ő ,_QJ0YYZ*e@0n/02dͅM|RśJHmO{~B8fqd+:τGLkϟ'zs/3AWBeTsh%l &xk}"Yݳ;Vb!9,uSAr:>Yϟ=`a#$nyҌ$',U (6'@9Uvu㿴o ʹޮs @nКA) JKMxp*d!pd,VaL \XO4Ê%S)0TbY5b&P`M搇L-_:;\ U"o.+ !8,ƑvR iF YW11ad-1A+ҏYK^m`z/o,=ٌ6N5~/޾Օ1teoE&_r;1)ia)A67AzAt- '`I*lZϋPJkVt,n')[{Cod/?8%a.8$M*1d@,0Y'Y'@LCWS]s9&aWI"L5D a`h~&\tі8enaq Wx&C:LhOjG茔'E]vUg]BՎ:LF( '9vO"hG%PWHz(f_c%'vvG_$KļW&l"OfIyMq\IPig|4~,҉- Τj9m"1S&02|c8bviUc[G{ŘRJ3x ƮڰH@ ( WRL cupgihL5r%b0}Ci\>lsXKOF3R-s{gPUhnT"ʃ/^*.v[fU d'5}w;q裏dAiC~*H銣i2" ˗?y'w͇>Op gRTճD-;NZaϞ[ iByО,L!9#yc4xV6MY&XPcp#-P*A $9:I۝#)JlU^ FF'++̨@/=WAQFPT Q`xr^2 21*#[}_|S }2[2  ڵ$fBwgk[SEF_R⦥OONI#`1($Ǽ ¯>æxAQv@\1[jYyV!c*`mڤ>GP1lWSkpfڂ` zBטo鲀ӜY )e e`3x{],{q܇ȃ!5^:?Ć('C@%+_CA3 NA U ʎ49[XΦ~~թRc (!01WdZe% (+BD~K*OF:BM?]͆?iK'0Z5޴C]NQ„Iĉ + /N,?I='"XS3DaJ-L[C1M6ᬘfXj̈/s@CF(nyTX<S2OF@F@@Htan {YU0'X}/⥤&Ye mIcckSmUKLJ45QjГ%q?~,t(1*ʻ\C<wrr_9mʇ! dhtKv)4̵ /?ӭ |=@HQdb Lk.ʘK*?o2IjhD&` h(npurf5JRĉ S=.~|A7paN_FmLFX-D2ҹ JZtS'2? gVh'WuHtU0@B1DzLĘи`"IcKHlmv#(OSOG:1 q{*e/]Dp0y'''{;Ȁ&>-8Q j:q**-ÈL3d!> 2葫(?&`+H첓hʸ8HYdL~ÄAx |@G'zO"1 77ÛwԵ–e"=`߁WȪ[ #+GTޫd>`Ňh%ɐ1 .w :x ^/pߜ><{0CN BjӐN/Z8':JE4TփS#z~2l/ wy7GD$M(l-9,pnNO0%Gȋ cW[J)!=()ND yDAxpEag _D)tƢNscO )^@z'+GhU' ȒPz xL!o8A<~) NXz줹7c*[A?PJXlo? h( \gqyx)QŎj3s.G% k7Q~QK<Ҕ GS*RP# >nw] 'p <%Hy ) W3\V|j+$4-.0 VGژu^gi6N\fL"-?TH>̊"0=[ov$ch5H('|Ғ$EHj|t?] f|cьlu]!(+N(F!;=uFL&L dչ' Y0"}WtxtD\ pAK:/ a<k?` $/Qe FPH E8uk|n>Ӕ/ ;?$A %pA"p b7Ð^*@URɂowQL5FjVTDm?2' 3'3%* tN?EZyQB3ȂPĝCtp<3EA^+hӃ*b-ry5ҪMh7RsdVuy}`l&1h-6(sipbmvYSmD[%ÂUɂ*fVl45i?& hΉď Hx@0ըhhPfpp$j[`ɃwPaXs-5{Sr^HBHpX W9?H7&MeF&%M9 %e,/v;^Q|}!΋^JpkdlpT6EFfa#6P4 6$jN%# h3F ALZ5IH2ASFCr0@C_RBug8I=+݌bA?D XDJ-kBμ:Hm+qTBUqZinS7#X 8'uyoa.FInc|C+!LU7򄷴bKyA|,02Q >+^A7"_X=ҬD)&kTPI6,D$d/BQ2Rn h6RNU! HuUaL׿XpV9eяYQA.D_]Js;Zp9ϒ5#LaDZbhh.sX%B0u/ >¡T2;r"1 - evF2VG'プ?%ޒ?]5%N@)+B)aFkTA $*CBh\96;b}e&WJ7z5W7|$@Τ06j$pE4" sxT! ,r1^[ hU$@6(/`@U @@c3 oì]r>J6~Ou^H\z3GM1Tfo\kUX)e>3 cb(+<)G%ot$3U\Eo1 ,"!XbzemNh-^qeCP0 t6-73LW}_"~)d3@+>VLj(\A9/v\*F3l EHgM+_}eP`@8:MIY9) ÉR%L)Bo~>d?|lqa]$?&c;.G' ~ŴE{԰~ErV?_ƴ^rbf_06D+92ҽ։)dp^ D.&#pF2S- {&O d7279ew{;0pկ}6 Bۓ_gjolbw}>:g%Mo.3I!E[le c58<84A F6_r DqfpssEo.hPAR}+<2!YP m0>6͙Hd:,'gN0V ֪3$>E1IvNpCSP0߱0??LJU:;'KϖM6m9rLx芤E~a(,z!uhLAV\4,Jc'f6Ud+97\ ;!. ojHxs Q 9Pڼ+d!c&[cN0c`&OшY S1+At74a?>zSA>" Ʌ .x,4x(3y1{2acfJ2TȂ;KG N]qo}oFp 88u~ŋf3Y2r}ylMpp\!wlfu~hr4фʱl\ ۪bGQp+:4 /OoFugoGGTG8á|pr7gEko_nYZA7C:㤵LDxhL_K14۟[[N/5[:EZ4!8A0Nj7W"O0ܝ{LI7Җt>8el it,*XV %>p18vJ+>اW$0 ?njMt&J~OׄCI!Ba'/ϲGp%g)*B.@ ؊A(=9"9h$ f' ?_ՉchS5 W_nvVVa~uJ =ZGkbo_7/?j)nAZ,,,L*vs}5B^[RFn%U!+ }vPo/UMת3NɭͽL=9T;; +E3;x0 G4*&Hfj(ӎөY SEveh酊R7ݏ1Փbp{j瀍sR$GYVV# XcsCh02!hH8dmœfK(M*͔xM--,?0YٸU8ͤ־ʝj_fo]؏ +Bn.0 Dƅ(.9k$nibچfiYQMn}fb!uhl1H]WGi@ TИng՝Y]dK@6oU: ieܖR;b}''~_fiq]ZKo'A\SslX0ɪ3㱳+N\G.UkhM;2 "VD::&-\6h7ek0RcȖ ..OBL=sa@[.2u(o؍5oF¢w-8Gtl>|G(54Jv$o@=ڶ0%fUl(#@LUJ ҸM(roafzw24Q&'J=BgW'\%'M˥?Z8(5; b!B\ίWwISl’frNԣ#߽\[Y"rGwJMI/=NmojdiMϞQ}ѸwF ,a4R{85%fm+뚑A"xʪ+9SMy! aN.c-}嗎D6{{o2|3_Vh)xŸɟ_5(~v{8' {#`.(}2cQΏ..@ٲ 4Ajc*RЗ355U5PtdZ5&~9?'xbIҟF^Ov52Fpcx{ /,\gFqI.@f[{rCQY wb5 u ޢ]:aiKω38K'a:2$7+8$fU>DHОll /xEeqzS&rN4Wnyd1AEtBT}hWL<)Wl0ҤrQ;1/z\ITe= S׶J 'i=&&8=6X#q龅",}|0pks+Ԅn #˱)P*^HNE'`aPBj$f K#JoHJZwSDY[%"Ǡ&@yTtljvxjBB)6s6iQ{!Xԝ[3 PP4IyBciq9G)&m:Yc\!ZX0S8JQEQP-A<ï&uV_%.ZmRL*25Bf=悵ፖ@+*BieMbc8s8ְ&ysf$T2įn ? ed8 kXiLꔇ.g;/0:~LJ*uΡ+GxJLV*7DS{\|/^$t `;9myO\oKqO/_=x7tݝ}/$mts(cd攗.姟d}uGZžYd*0s?W& c+װo32ұfMNwK2"Pwtr  f J)u6siHaBG/SrG*΃ԒC>x KPPlY~ 5ppr1Bo-KqKyeKK_r M}U b Aeh,-t%TUH=noQ!j0Ê\7=@rPZn & 0z1@+SsFnrhC"PUNK ~;/Gw ɓc%[kKe@nGVk}iJ-zUL4;)ϣX{A(q}r{xYA2A`D`h]E2Y ^ ڷ-̧˖LW}-Fίk~krjfVLwчoȇ=7~>̷ch0 dy^Benpa]sz8gZL!jG*᠄Tj& )1Ջ2o)ԝA('q!僱yǁ>qs9oTλ=tՎ05;;K+5VSLsBY nR{?/}׿|ێ87{fi'N`$jnmlFopPLWld>g ثoEVF`iV'9 p,Ȩ@"QGxFygϦ{JZBC*(tΌV$Nfcm d`54WkGsˌ'9%HYܨ+uz1WAYa峓Sxʃ#0r_vQ;2*Ò vb;R@Ɣv  *xO :w0 r﵏)~۾cqå?p::GR2oOTΖ][rN!$-Yy\qs(Y2lfPhe9/ ;'!4qf),W;1TVp^16W,혎Iyl~8AeqnR;mD*Qr{^v̗dռfi0BjP0༚8" -!;Rˆ*Y6ϧ6dD;9YhOhYWFJOV }fH4™ۚ| ,`7OL;j&b <ߵwm ʴ7ɯAFsvv: Obo ǎ̂}t_αvІ3ؤ F2,<#!}>5*jU#qK<*?$z/@])*<'\wcҸ ٜ.#$BJ:=A3'P)p=:BBDY%CQ้4wѤ)6*Ap\")F|C }ϐ#B>EZ"KHY=q*&Fi.-4Kܬ,p_/~ !&0V_,! qɁjAx 3سǘ$}ϓu+dZӎ}]vYݙjysjE E"0䭂u](21aTNE@IDATnKBKM (';Y};o2kFZccFvq}t>1CnXUvBt77M3Uτ˔ZG6K.`صPL %M#Z M›aTnksƢӋIP|nhqᷤOuԎ14 EvKIFfҡ8KˋbZfGzzT}̙Vl%xI㍣8fq&-#ZR8 9tQяԨY^]]|4I3Aӓ "@bD_ϟol4#"BH;Sq#ȳ/5{]s) :SJAq{;4kR'lO2%Q`C#"L,\i!Z?MGCx#;#.b(/5Ԣ8suGC`0cR^@%Ki_s)tYn%c$9*ege-@>.=s nDˆD/0Hlή*tG.Yq)Σ]^!d9ѹ Aw R螻olaR#,a/( JSϬ3.ԣޕr7W/Jy`=SH" +4Vc {eYQJCYPRbWpr- GZISC\X۵Uޡ߽Wױ(J+\ZQ Em~♣*Cp.z}ݫo_9Flu!5M׭AahdȂ=۟i:*l8 DJt0旖hTFGX ( yɌ#zBE82 E(B{b}.#, <<G$/|mmM_ltcdEJzf)ɞ5[M  isrrlӗK=3XA3x"4f,+bkCճ }{roQ2ixj+A3q!h^kͰ1~uh#b ېΨUceSO X]uзO)أՖ`Xȏk}zy6+V"Y|"B S"= 482Ev!D4 i'FƮ ߿}w[ĽDy&# H\RĂG cV}YryC>Y#*vL= 8CWzIA̲ 5f z=,)H R2ėaYɘ0*npvxIb:ts+ M'E|]0 JIQT%/\oP܉ [`,sc55bfMl%.ˈG[ 1KE[v1 W'2P7;3G;teߞ>y4>zv+׿ q+͌~7/g_n|٪iOH3lDN8Ʋ/ƧN.Գ9z}yx26kە97 6alp$nE#5FfDѢ)v!4 ]Qp)LBqp+[ Sfܹ89g7RТ8HCOgɜ*QZخo48όe̗Y#ݛicUW5_B`pWFP8qv]ٮ%gs-17Eyь5I[|ʖP18!mO0ہAr%g=ڳ0mW+T"D;<a|tFqȏc| WfarW=nlDn˞={`Ϟ8+ , tyi[E Syej0KfQx)35GpVu_﾿t;d=63x͹K\+t^0(ehf* Kbm,T%5Kf:М`sMdNŦѕ4415dE2U]1hB$0B6ΪA@(4\l'"Yۤ.^z?w~ *yD BdM׿ ̾ksTD/n,|h9Tm7yآȼORR߿s{_0r~M[p_rITYpU4WB R SI`hySSVn:бHLYBo!P&A 1 B|@-APlP9,q]>C#.AW,<:3'D&v3_vYWÂ\A _(AѾs߅ bkwuaU= c\'ˌݢvJBq-,$ *},~/†^-5!Mq8yOb)R 1n(W7lVwMPn ? Pm#CɍQG~7*ފ2ud,1p$Xi8]ͮ.lMf;hj1D:ES{#{24ɒ`IVY`J`ȯ!PF卸"=GTA9t>ERuIo0g-3NU16OapimFĆw{vS\ryy=clOcjTaK/WBu ͤѩ~!s;2 ׯ#2u'3GY &m=֋qi#M7_n"e"3d&HC-ŭNlG a&jr:á%,mZL*S FtJ'Q`u03bj6]؂U%݄Oz|z &-viћѳ_WK 5efSWg>W*cqr|fR8Ssmg777WVo1&~1 $!\`O q{R,]'p*é TH1Mjp^uڦ^? `'kz!&D _rmg.pCmMh5@Gp"64nvDlm![Mzuz VM'ϱdxx /-ތx`wS JG!d&eMlE(v*,\"V=N4&DSXL-(K[D ݓB(M@P91ʭLA1 zKYI=k,xrm-05Q|.`lh4 IeagoGpQ1& 2fA>#EڃRo:&$|󕭝~%gFۡ^֯78<1.eSQGU:C1@6IyAHrHrL9 j+RNffI} cO@\%ҠّӖz߻KO |f9=0cTb5ױ''a%=R.媿P`O+ b/U  "k@]6Qb,e}E'!GȻ`|a<7a$c9C#RĆzL s Wu-L퀓峡ہ5~!VWW>{ch|DnZ6Y#N*Ȁ2.M ~D*jXN] EA `?xA fN>Lz܃tؠ>)H_1r<UE@_t DްJk&0 !cD06A΃0Q"+V !T+,5|4؇a܂خe6ayf4'2x=yWa)Qa͍ߟ5FvK\8v KʈRT@i1cR8o$)Lf&bP39cK$mm%RaY~PqhqE&R(/eR@82)מ$uܪT/?٣oyf:D8:w}c V Ϙ@3 )joN@vVNtL6eYΆT A0Y 1"qH'=i6,Ey:)E2g8/z$=Jm3@"vt22RGr4HG=)8jEN"pE#̮dNB)h{8ptD#s HڈXҒNAjr{0)rRW]p@HڤD!3!۰v~M= 5_-dOt͟CZkJiԠj%|ePP;B&D: }d7~'cV{,]gZ_ؐ]{<''m2vTθi) `w&'&2BP.` nV!Ƨ`"U_5SnJVm| Ԡإ52Rh)șcv` Ÿjw-17!2 MiLMWb NK>?$yeִaz]\y򛯾*ݠ (L]C t-8W#gHff+(v*EdƂcZ̻-Hݤ4k.xSF+htl|qz"1nB !4~d'jԚ-*ZVucֶ'A^N) ͤG d}|P<5cciqʁʌ<~Mcdc!U8:[|ڇ=A2Asi}b3iB1KkI jkCLhWu4.|}3EQBKVz\nKKIi*fD 7Q,=gۆoMO0WS:,7T҇x0ל"zQR<*KtdG7BWGNhp", V-;mʎqL* YBeCori+PB~oǫ/e?NfPJi\nV8 s 3 $rl:lk ѪD1w'T?S M6b\9vM ٮp>[Ḭ9RI@%^&BʶQ ΄Pbf=yD 늾1*FIb/?Gh3L"Ner J-{H%Gkœl2i\24/,w>e}h[dR7Bd6wy3d[8.8%֟dJͶgݸ) rqqj5'a莤jN3;ZM|g< v>3wU{8X6\̔SbA458A|682q dĊDPj2^45e|@AMV8ܓ:ҵ(89scLo H/u$7]F!1E<3* )wS:5+F쫼 |M ABB,Tq'xݭY{7YغsLJ5r{3eWٽf 'uHPl:f̐ ;*  yRTGeyUl:Dh%ۃk$Bp0 '\L?KwCo-0dk 6i8&#-?@8XӶ集G_K߷)rʿ^Wgc =cmmerr|`#{'O>}j Dqڴ ;0 An~yV89jga~T~0qh)Q=Mh,;U ~4Wp[d܄wL=ygc6Ž5? o,Q!_upK#]ű1npqs\""EޙIŎ ,"CTBơ)b \!|pH'C7.;egKwj-xlO҇.6/hԎr(TTʵu㏖ c'(xYm?Շ\>K.8ne:6vQ|Dy('8MʞH(+bh4k) 6zgY{:N+ Ԯ\\3Ψ!V ߯4uLsMgV,m?.ֺIٽc{?TAJ g4/y;։97L G7f;f-Ef::yBY(a&' ')^F[3a+܊T5h4]ƾ^y⟢#L.r?AaE }q3FrVӦ+LF,M`i7ޢMdi Q$p9K7MՔͲPǹdfN. .# \ڧ^W'AE(Q? <șiܲ$4 )+\NXhԅ'5ؔMЕ zihHjʁ;4Ben4'0'6gIu -E 5`C)3tsm=WG{7o#ѭ'y$bY-c^M/N~︝dDM?.-cE_wNtARg58(t#NPV:5HH]MQ>֖hXN6"p2iӨWCwvWg09 M" 펤D?}ɏ6h*%s20 3n)J;emh '3Rc՞D.]÷9F]ܙ!j 6A4_Ct|z'|A@0$']>f?ȋy\4`Qy${3vcV*ol\,0OL/4ZtFהHHyi6#ufP6X*w/.XZX19Pz&Ѣ2˶C첁!dgzqS>݋\ S'“UB& Aro/ktc׻(A#s 3tfZw[gp~"|Q[60WuS %2Y4DACP1X=PY+Vx.[a^GmK#J.t41Eg<jP)IʋeGMؽ55)Ex2)_-Z-K%D/ЯِZx~hM3eM,xT,t]O@]k^dћ+5(VQHbЎZah~sxx:8YMյAH[4a`FSks~_FQ J1=)?y 8M9%;} ONIo` %}Hs nw~;q%] iS)-ov\6KX|4lw0M'\J(8# Ei"9XyF\1]*Np@뙦X ɳ&}ux5⊘0L ІNѶ jJ  Va݁NoyNsdNNpFݣvwT!-׿^;{;޼|iqzOOWL ^􎟎tH6rVG^R:!*Fv V@h;X怜s- |to"HFdzݦٓ?s0*`)bn@bqkd?y Kowa~3T\ tyyJD;sҡG{b@9V0,P=~C4@Yc 8FGrȨC'Hu@@fL("篙B {}_nQS~Z>R_Y>=NoD}pۻVF[T*wFJmr0B1*Ox50/p3gj39Z~TehK[! s YPhd ;<8ɐ7*>/H@K|x`HVfC. Ĝ%rWΉ3rU澯R-9aa+AZrgљuQf^S嫙ɱ0h`yW _ "B]$0͗Aejِ:R?M,O9&lN!  ir&EԜ@1ka"|DM9S_گ+8M%Gl"2Jp[t$+3)5~KG \\nw 2OJ<\ѽ$!w-3Ér[" 4GdY@N) rJb ,#ST"랧ŀX> Uw C<տ&rc| $EϿL0q1F"bCOW/`؊]4/42% J\J%n|,€2 . y\ gF3pŒcT60S:IeDONdր="G`0^>k0}$\ku=PrDp|'YP;%;j^80 vͅa  >XNQ8ʾ"2OQHd={T4E'j֋;| ]:IG; $ƍ+[[ԲwKx7;QޖLhh$`~c, הˢ*ߝi%ix#{$!Dr*~cQ$8 G TxEiGɧE Aә }}#ZM앤dX2a,l7xnGlOgvV,Ue^~+N>z͒ё`P NW_ !&* v@kYߥ?* F8,q`,;3աޕ[EsZU)5$g4ٓDTA SX:v}zUk?!F(6Ps3IJͪ9-{+$&#ڭkM8zGޥ%L>'bHg02øP "pdPیYmcVb_E72;ȹ}8;rUQ1rѮMXq{ Tnv$q YqxL-( "rtplOu}ȣ:'#L$"N(%&9}(s !$krJvTXߏ?JlB6w7J8l_j>q;ᅆ(XyYC3i *#R:Jv6mIk;;z%x/H&:l@}{e{]}(cOAո>bM)h6|B8*zR8KdCpZzkD,m*IDd< `MRBUo y^)8 1cܑX鋭ɝl"HmW\)ZwI,"12%nwl8BèEDŅzq MÇL Ks&QD*x`{$64dܺ& ؠJqo=9R|׼'%ANAfm`'M\;ɩ.ZdxpbYL$}`ShM0On#5-C)@(3;E8Fɡ$b0EttЦ%'0gSk`,"'2¤o*OSlMJ22dI]qV[<`_K0/DhԖ;iĪk H/^wTץe쪒/s" bkJfID`]wH}C61Z_l_; *q7h=Iyҕ͑$B 'CF5k=*Iڱ/QG[͛Tlm0fVhޛT(zb.K+Kg2i|\lM7 a0 Y̔\.jznז8. 6Gd}NCPg&ME7zb +IW8qGW{@\td|BIws1A&gT˾ef*^ǡ`,򑴯Fض/6+Ml4~hN'quy(cJ[ZkCإ~}.-JR$ꑑ:[Kv?k|yLe?z@pP i&ZufzDQe5fa^y<΀0CvǓ.qU؅ /_\ڸ"٣,Qݩ5fig>Js-[F۹0b~ix+?jj|"^ -*-l6 FwYHȆ"r[Q<Gѿ'ny_Oog4bnz9RFVbizMas !(bU089W4rzc?bfIEN\% 0Mlu2l ;9GfVh_y9L0(83N6~n{\I 8 euOMQ~- (WWf0SK7!m` gS.[ &=a7nj~a-[앰 pѱߟc-hUbdqacwf,:CiHX!%-ܻ% F5@5.zOaX%Dt==fƄR.,@{=XfVCDC#{`ssxƿ2~Ug](C x&,]nnxFG;m9dCe 1En#y=,).c"@ ֎6Y~PDYuIж^]}ɺ m1jZ睄]FNfNJޞдJR b (4W_.0h~}"zKɑe Ҍ 0he ]*EdҚklB;pLv4l([|A ShPwqlw)8ELn3rh1ONsqHyh"0h?-$D(IZ|kפxiB{,2H$j `@m85B7 l29]!Q]r5Ŋה!ؗ!>c-s\e\ZaWѫd s}]\PFI\U9T?HF{Α0PdAqCC&Ơp`dv\h rupcvw~_)+Ko{ou!s4=.fNY/r 9(kGcӷg$>6<jyS7䦩Ԏfx݂pQrE Ӌx 剢F~;Ί8DX"tf]Z11C X}݁$.7V{pĀ0t@*zҒ&QT5b bgt4ZJSIaTbֻ'%Pkҙ&cTSEFF'A֠2d2ozd5e^6'vƢ9䆿/HЋIkZp$Bf uz>TH>H}F փ'q1c זW0{卵|s\l`InE`fОÄ:l \IќXvϪ]jV3-wFB}={5‹^Rehiz!~y^)+lM8)qA(0$0z9R $"˲6@YDIZ"rGwv_\C*AQ@)Jnæ]qPl}l0}h hP9_ָmS6UOu[LAK_2y>sgXM[ъ89V$$c]Oƫ#Tx@q~ryuȹP3" q9`nIov3UQ'BP\gb{#4fx$J)7OAn%=JH_I 8e7fIާXDwk>0Bm"f_|h*@$^JFfqľtJ̐T0D!Vgtr@ 77Jnk:'A;9hX¡,Brz9B𓂀݆ Wۧt͠JFfO﫚nRrdĵDl1)_Mѽ+!x-@˒{+:#,Y9a>([c#&# 1`8%)v3Rp0,lލUaBK0AF=u*:T(+3<5걉uX]Ph$z^{v~j݇#0>T,0$d֊2SHz04 am>9CO"4_CGhx2}P͵(7dWkxkAkh) rܕmnGdR xL-'% #̄fD _PNm;b{?ee+ܱf'bN'k!I1!i+.%8%>AyY٫@`Bx9얚#Z,qD_Ni*aHC")CZ8L7.'bU4 s/ 8k(C@x$NcIbt%Enj2ǒa w`.ƣهDxfUsmgߜ/H*d4`JT>7h?l+Z ^c~0JꇙC~d:DݽϿגV ?Vx( ؚxr|ؾܟ&$ Ph/Սdlf!eA–<#s? V))y5[#ACS(KsPg6 :)D$wPHp|BkSkThz^ e-;yN#73j b0\jH928W uL=A2F SE~ A_@^w[ ly(@=@AVVF7 )vSVQbaȇDZ<#^>x36Ў 먼b-ܦ5ŝ9=9 ȗbm( AK*ښѬjjd:u3}L_I #2ѱr 鸷å>Ue]o_o_WH$W3uyʓ`"{H}kij/^_Q4/ 1G~P>oAC0uj5@lUhNR3zdq/d-` lI[J?ҜDnF6&2̈́ R#*4LI/ۻ2Mj'f H#ܺ1#8:z,eͽe=}}&'n{5 ?n{iZp 6pO BƱ;+ [0cyDžYR$M0h!¤ "l B^i /@%ȫ8re|mk8(OXVZ7vhܠƹ?0q9E}/R1#MB,̣!Ц$cO@p.=*_Zz89DΰJrS$)V7sM&QGH,tE:aG韻>Mm}cxrXޢ'Yd9H΅.ōI/Ccv\1(x@~b Zhd !`V**&Y0px(q;Q&D1JR#2583Lgq&;ōjKt{_" "J:ijnH8!Ȋy9:ĕsQ a;w%/ `k/D0;5Q854P Ntçb1/4=C~Xx=]Y1wHoϔ0xz  >Q̺ x^ 6ÝASA|#Y4~Ԟ"FL&/`s778: ZTN(; E`^me,)/JJ=IN\!'orڢJfP+W+k =aʄrPfum;@!ONQQkj p(/ntpu [ak\&xUεH;ʥj%"W|N[@6*!Z>0vI!k/=Ȭofd yF7.{k OZu^6ek v$+TlO+JTOFg@ɮj`m w 5wax )_jQV487[‡r=ѽ$)ca<~sы05aӹ5ܔT93MYB͈ Od,uH䝽Zw]0YA ( OqK__^W9Lr١ ) `Mtf2'2\ȇ>R7YHQl4Uɪ?Ɔj?2Y!qN Ut3qs(jרE`BZF)=l.ߐ/ވbyq5RggaiH}Rd% TzLz/`M *ΐ<`DK{a-j18;¤Ns"Ϟ]|W E9WFSd/ Dyn {Y[(Va4+LUZ0)nX|[=FKCB|yKcs&E1u%Y:K\z˔KQl! b@dXƶ%gmo9U60 dN\CPsVO? ,6 Q`h.mK4f#`㢽d",)1D EkL܀e:u8mѽhfhA_+o}-ɹ5O'twrtt<j#{x$𿋴`ALٌMLX"p۠͠w><Ο <Y-5H543w5x)VMZ\s&/y |3ZM #>ȀysO20q@O[,R(5La s/Ǩ <YQbKya \;uYDN˳DyUwnT+!qnnVnۛzuȳ`H0<%iÍ4PO'D_~UI3ݚ B >~bG+θ+=ҴP* PxrkSL?+Te5MɽOhyoN"#(bmH'^q!j: Pi:ZAndכVFZeC,P!]`Ug_I(+),<R/RNJb:Ԯcm3m[(). Q`V :߃VW%^/l2P¡z|H8ɺjЗ/upx]%Wcx $&O}\lЀC2ৰ8;$ӰH u$ؑqAIzͧ;Wsڴ)& JMֳ6#%jSVM6BCQ)le2 90yse;&rZE 3"> .ODHe|ٟWoO.XFQ*?kX999Ha<͸Xb}l5Pqmx aa-lIIGYY:4C=P+Pe`ԉP4oSKiwlue#c7OYc2($e##P-ݐ}}|8_z%P?Isǜ +bV߼n[.qt<ΚxHaSi/u 2,G6[cd(;p ?}> ( l!Dg肆⧗Ϟmq;L:TW Pe`[ш7o^'? M610#Z+,[ s Ch_,ٛӧOSO6m|c7XwP#o?H /p6f0v3S2 ?iy EBS׿8 5>{RAWBLrd=}`j1p9I5mNQV0`*{:xHuW*\W1z | + \r~ [1|pomfV@iJ[@ `"D\g'Ҝs=iJ*SguBB"|ʛ0tCӘfp>@/N'Ê؞cDiٓC~XHb. /{2p"q} vRiO|^'Ǡ 39H++ "KG'Xѱk0g=5cV}f; wf$)p,(H5e+W$jyZR lʟy$DOCAs9>61ā_ecY aX@O{W9,Ƽ%  k? 6fc8`;t7+0-G rmhba]DӬ6*aD v,mns'Љ 2jT=]|yTPIDm#z<7fsmz Xde61A ÃSACL\4%i;q޺ΧĔuCzV*.s^o̓8p+y~~oi2sӑ[yrrt^2~ 2Cȝb"TGݧJQ&I' kP4BN/VY;_H(6F03P1'ֲh<M*$$Ϡʭq9`VЊe :5L1>lYαSL[yPBǔ͂ )$r^Pn¼~l[޻}+֕^fEvDL5*CMt`˭_ !@d^r($n꤁.,y˂ ~^>o}6i"J(. G2oɷ QC. Xv^) ZEя;q!zhTPIMP*l1nS6n%P34x#Ԟ shx-/LO`ſX>_ I[ió: .;A[FCЪ.yÙrgT^}h`K"M1&>?8jKbugYξ/rb(u땝aw' ~/Gb>VX@ˀJCNXn;mҺj1fӐHHF۬OIGpl=9 &MuECayqcsP0ԌmLBO OC_ɖ)3'D4Sp FPחMȒHX:B dnr*1Ō(Ӧ-q,8O-0,r;|o6LЖF˩b 8y:^a$&0ypc 6㲽Ł@@Ў?B\;6 x|ؖh 2 ə{ $Dg( `?``>&1.'gq"lN{zlG'CNI*KMBb+F%AgMK1Ś6U:^ޒZe+R jW9tQEypskx:N@K&&#ꝓy"Bκ4Bd'n*sJGZNڒR2D]Wxa=c `~4zX8i 2",,j΂)pn Ԩ=h䦹ւj]TY9ďziF>!]^5a@~Ke'<םvOIHz)(SqZĶ ;vC+MJKۍB*1 d$bGJ2^2ƷU|!L2ˇ TPՈ^ad3SLk4ewk\ D?j4Z=xY&qSޢCsrNnCPA8w^ ;ƽAqN&0vGזŜ\`]w#&߽;5)&`G jS$zkPW0-HgiO%Ml !jj!ɒM6)$ ӉUMHt+w#ƼOr$ [%}6֛1 jt~nErj)ny@f(]hL9ݎbu^PBJ5}rREh8DC0#2.#ڲzL ^JR}F",V;cUe^P?烹rtLD?1>qB`0Tc6W.. s-hr&0˹4&nfde㡯t-fژnזS\,ehXlcTotΝjQI?,hx`_MfzVY!^Z'i0伸90=#^:x ̳db\U?ʊ4 q K 0kՖG: * 併BW=`&a a)DqȆleܼehdר=t!3-Qq\A0,s=~|w@oɰXo?>_[P ~kc[FMK{vdjݼuU,8FO$jGKU1%$#.uNYאtZ>яE,]Q V\@EhN@Oh$3 b4 + Q DNWof 7XLT$J‰!f8Cvҳ}FpMP[aX4^@,MiZ ICM2D8)-26{ױKHfu-p ۆ'} bp^-v2/F˱G#7Yd-ni0?ךv㚴paRA0D޷Y|Lc0W_LPGn#B/hȕHuc:]s_`s(kwbw@DFӣmZ (Rߦh٪Vhi <*kxqߗ9 l\pX %/3C +nEl Hk x|9X@_/.= uGix ʹ #(Z8 mKͿzy U.7t>y&V9ۣqpS[[|X!7_]j̫_Ye[6pʱf(hG8gٴ{pVGlmPPvGUiq.װ-AKsҩ(ِf~- fmG&'B bБ{GGN$ʻMFgXH~_1ڏ> X9?J0fӐ9Tǻ/O0LL,"?ִꁆOP$\o[(1޺k,2snөqʨ 45![ӷp5=tC8=Nk(v:e{(Ooܔ ڮ Ğ5LI%U/(qag֚1N^Wˇ%g+Scb*gdj I* HPf y-/<GC`T Vt\.x_zzFOO߼<{|x j JcN7270xΧ`bJwn|.2v+MC:`rY.֨zFhѴ\ 1y2C[w Gh'>W <`^lögДnFc[;x'Ҡ>ǟZ N<pA#R  _#Hqʪ*SUsfd_u:-W;F k(mx1l<2Kx?oP]Ϙ`sRؤz柅_ Sޚ' nPcD) A@ʼn7++ /ā\ ;-Fo1%>Q摽j [Шæ !ȥv |4\ԲFT30#E0CdAJmm8FUoZ #{8Eݜ?,P>+x"h^E 5,_6<z8@Q&iblvք-ؘg+)M {3":.N 8"c Dݴi"!ͅ>X^[ <>4KiBXC1њ6i=tLٷVøw60SW0\飁 !D};D-1^|xדP|FSZDզ䓉*GS]މ)װqL5tĖࡒ!nnv)>_GYn,}:~z 'ԔHK &kFb ~Z)wXo$a`jSUۢݖ )R΢ Rp*G'Xyw]> 7uHk!|]oqV&`& lY)y0?[ЊXĶӔ e`ͫS vcd`=throԄ+L B<#rX`ha43RQU>Lywqs:i6J+DġA TfN(gGhQVV杘-Җ?.od ?󋃝ן &yxifc0i-IK{o Dy/]>]92s7l4񺅐e? d!̻g\`Л+( "LTV~p.wl3M WtIs69p ^Dм {U#h\Mv9 \+3Woޖm/Wj6U4ܠb77A5c,օ%Ga*^6:`G.LPѮ 7 *J#5].TMyhв'ֿ5 `*ﯟͥ ^@(@IDAT2.PD YDGf l*׮BCcK!|لY $rz\̻jH y| v{;x+WgDjެLj XTț8VJ[7ƔQLzH"hsuTr(wlʍ  )zi%J2i 2{>::JD WErRrϧdX.-Ɨj08hH7G|C'Z j 5z\‹?XYVïMl_+CL]xOw P"ߌo;tv0!lk&_}O895TZW۪( V4GKS e86J*VX\ܛj=˦ Bs"͉~g*j^(Ҿ܃f)hl`>&M&[$YS{SkxYyEȭjsHZvʹ3Z,rz0p NPJ3Ns6PF~ڝ fw:NKaUw-e9 e ET*"[C۷33GLܗ2ASV7a._3+D~Hv : *YZ\Jtìbcȑ6D.s5|WoZT KÏ5&D b4V`u:T t@!.~,7 %PQ5HPsee>݋Tyl\dzZ'r>k BJssd&AMxlΩpUÆNX9X1\JC-!]ESnH,P U!d lo/6;Bΰ@`zqQ{A`R  KV(O$lQD Qv!L6F7(gr6B^1v81?a^U&EVڌrX$T ? ŔW^\_eICO_0AQ+qa;(GBSgkz~D?֛ ۛ}yk*Wq&,һ77yմ1Rė&> JI`I*](p5C^@{v 44{BLXF-ɣCs !k{:*'gR "CnˁdJY>@jpG퇷5KnJ%V+z Ffo`sO^<ϩ-,nG[\[+ ;ŷQ+Upeia{G8 ~QÉ >W/s21b& "/pq:8IKj!C;my :XBa}QFbvDHr) Тd429-Eڡp,@e$S! yG[LmD^C"(" Γ\>Ol5~Y7f1mtlkDoj JwhnU_=VuC~L{(I1 &4&E1&S򌥿 v+WP! xCdžB7fnkSBebZlHy㘔tu9LCPk V I`7c'hN~MюJ)K)6"c@*|QFSH81%1IZ3hSJzM+3rwcZKD^LlJƃJhy&KN)֥ ˿PMO@SaG͇?\hK#R[ Ν<9>h]N 4Lpj__5g&(Ár`eaQ(s(!RPi-٬ƽmR)caO4]PZz̵ހcBPZl4bH 21Y&a}0H/DD~mW`=<`a礃UYcg-nbW>q7#yxW1@xfz%%8tUGLב|RLYtdvȮ$iDvR=i5/F_9\U([npN +wF:ٖ|l=|JLHOax.&:dwc7[A !  kWl c&j_ZOc3Bhm& L:?eʛpA'P1ā {|5HdTkԦV ܍psJDX]c DPdҍޔa|owhr'oL~o:?,:e5n[[lc/,!Ad6;2'\9 xg!7rjhyzv3~*Rz'쏷K˗b  ~,nq1[]cLmjmd{j?xrxrL0`uKoYW2.m&~gtZ2Tage˒x%UqLBKRPYB<ٷ;#QEΤBFs'#J; C0 1e S*I 2r,z#OjAn`;t}ܰ6h>s"֕=.V!٩d5zƗ3`D'PlRLz+Y+Ɯ5#_K/ჰE|t/fDsÚR.Hp~bSYDmd :7lJ@Sp (wZ/ㄩl1W@D 5 U A!35'6F<(;9* '\v~_>t1&Zg)x°3 M$Ac6(+;Mv}sfZsF[Ȑ'8&ŅҪ96HG.i=˰lS[ BZٵ;/1'K J$J`3/ ODq\]1whd{S﷡Y} i,`$Po1-N+ݧT]8)s G F"cJ1A` .`NSFKk'Ouqp7o|DƁ`|_HluK%iaaWTҽHAigT~Z.qWNzx3_unIn V! r.ȗwqS\i5-Z =;{,0r*5Q9@rA(m$Vɛ39/67NERU\,if%:)RBc"6 `3.MJ#81N5W^nZ@Jm.qmZ|@ňzצ_ #F:.q,EMD:3b7x}ӏ[pQs,̓ri KPWʸ؈!l_ x0c*P00P}$B3cΰQ'3DV-{rT 1mfU0޾҉_B 11ۅa?]hmB!^lMWa+(5^  A &HHEa?wBy04H~m9`{uP9i&DEhk+@ep59!J?p\Rcwu^(-d ԝ;gtXBT#'Q[G ]@PzT&˷0zF@*7Ibu.jk6s/`1fVZ5dXz _|N/UImy294eoFqE!N8o'}:`k]h~>Bnn8& MdʹeOE2;1 ZdZXL ^ش7Lk e3Aq+GضiËk_o `Hx{fz8xItX ̗gOw_m^H:BWFq/ѓWgϯ@㢃z =MW N@u7&,moK=1Y*9Nx夠p0/ ON srtװY Ɠm9iutrwW$z_H7(c(GMKM3޾s~q#4k1F?oȽE{ʴ *~q=DF"lExOX%cy5cIR op:c$'SQ0%":2zsgo>aRE$acmw|NOq1j-Z-2;{<68$>,IfeK"}if_́B0$ L֐.\ 7N;<UicAI4\l̇ #rB!bdޟCfNZSiO wP4k`F Š$uE=)0kL:r^Œ-t@#B1)H"dl19EfG0ON"JR-_K&_5QD eWWOԲ +#"=4Nz8#̙>d%-S,σ\5+~iw~r|8^=q(A8*9 `67 MsrzeibRdsυt 61 %PW*7Un )C<^7"l2_^͸!9 PNJ nYb^866}!JWUfbzvyGТ'LF9B̋WJ*<?qZXL@9IFƄx821խLf~~#NҲcdLV3R=> ltl[d<}9,kiٖaSeY8d?QA@볘r]s&aNVZ$;fZl?iPt(1lVm !3M4*OF{Es$pYr+?D||"A G?2,@VT5RUi#bnM|foRÂa_z!LHP*jx/?vh2?C68q*/2xgϑ$5~hӔ<ڷFW?"\WZÌBYWp;,[  9GHce6_hУ LU06 sPQԤEĔY82HZa8j2hLaLۦA.__I'\ɺTFDߖk[.>! YVI1Y]+H {/mg֐ZDZD874E6I 2>-ARTz:N|&I`%%nuO0o/YHTeUEQqxJ` [:ͭ[=$ ުSoˎxHarg0Ыӏ9,UMEEthJALZmOe?%=d[m+ikɣB.yL9sVS+Q&"~BݖHsaaOQW\{f1 =A%Ɏ*Y8AH1=-Y|@6M&@tUɨܦ`U^rf6-רZeXW|/"nxngFk IP0(3TPz C6:TLi<;} Xle =:LuSdx`/V.47dO \@q ~y_A(fA^&8Cn1ڏi x" A!o_;!ꫩvٕ|,%oTʫAmfh*Z}V6d}eg Dy-20oLJ*OlsMF1gLnb xiXp7iTƖ\b>P%W\VH&ꣲl}hlcR=<&~dx2TNHnhf38p^H8afM_f(QH/tg]MVΑLΏ߶KlI'30$\tsä&!T8h~*R$ zXTѦ704"S yqDpܸBM=WJw7WoBo8݂ j]͍=ǍMeʒ#|\ ;R Y6 JBFZ|wQw\2w٭*x~'EOk8lp7pbGRhno-Z:uJ)o dClڡnT&u\6`{arj4&'BIu^#Q'-(ȱ kSx`] DPBk=b=QX骶v$ !Kמ2kGR8qclviԫ}VoaDﮰ ;D "V׭6U`OeGT ȫYA=>9 T%l K*zǐ \eAVZFn=gC"FZUNENR[5ITv@5lEr! !yOf-n" Ȩ"PDgml\ޞ{NE'| 8.6xEO4j /'zQUk4у,8'&ѹ<#+ZDA|:590,mI,u[&A#_smoId 4L`ٮV)v8N`kZWx3is8` ?y%gcZmT㚗 xzMFu E 9(껇vǥQ㢮1tJ:wFD܀z*#hM.;(E&׬DOMmu-cr'-+;J|&+5LY2lb5MY|\ [ȑ5SF8_ίY` X 'o\N7a" z@L~֊ύb^>9 G4W֜_$ZzЭOu0Ӥ>1K ]tLZW~Ѡ77j&oa~hnmun&Lv6r(W!=TFE_󼻲 ޓ )o`f(- fN\KQl 0 v`&!ߺN})$;G"t[8L5)ˇ{!u3Z'(<&? ʩ sy+rh<ߚQ6?X?uн]QarBzr̈aRϣ3cߎNo`R++RPJ( ' 6pI4c@q_`hB.L]I>pjv[UbQ lsc"K' 3`sI2A!Eo' {xS\Qy>'ðAZSY&nRgCUHr_S^MMVԦרDR_.{,u b#Umr Ľ ͈c% Ԟ Swhxrꚱ+kc ?T hIv6H.?8AZcTjٱ^@WޘNn1/@HRbtC!LH_;eiW& `Yk=L6PxX]7f:Y`/_P; *MBHo__G'&9E􌎛tK.5E` CG꿟hЮYFի7Lcc: sʯD&Al#_A[eF%/g46Ycƃ$\T`KR]C< o YuAif4 K3!D~%oJ^@j0X@/0aHQ~h˄ xG r/M6u5uu'nqG4pRץEslSfUy2RU,KFj}FF5 ķ['BHLmQL34JA?e|1 Mw.n.sꅚ~#K=6$| MS!K/CY qd[ۓ lD0X}40wRf eQ ޫG'!UdG` KL40Å^Ma]a %D%w(|0@TejE%0q #-T omqe-Ѱ  tP6/Ȃ) oK (>gSt `m.:3a|sK!݈`GCjbl1eVj3'[|UOkpSP%kD}y<H֊f'5XCjrJ\ 5*u//gkVz'oS6Tj0b%T)hZҠu~|gyW+Mk,G oC>`ˎ< S\"w 4ч@C˕!#޸[f>g2p0ǗS [Dè&r!;ZtR5fVm~faO\}Q#rzqb%\F X !`N Z-RKr_-{( 8FW.x`+U~& |Q_qi0&\5ՆHee}$Npc1RPW:n5bT˷htnl5;`|O%}iL V:nCNN=<$M|K%QPH§O$|9p;ezw:BY$ +qCg<@41"Uމ/^xfk2]@ZIVvPvFàA CbŨB22xÚԙ 20|U_jXf,&s> 8GVzhF1+p[Q\ 7J49V:4NYR ?j" uä/a GB5(myM m-ל8ioyqV΁P^)|ݼH&ֈVڥo}+7"zϺx1G5;$+ӻ92+tyfoLO5:1H$.B:WۧZCR{}mߒ^g*@a B G u2A_mŤ!Zp#N khv?7˫9ȉ#.b$7\`& HWv]NZw;꞉;$E)_6$vk@FA#7/+b |Py[rNhDex& MYB\NyԑwnO*q{ ,`My.zC(LrtN߂e!,.L1;%j࢓16nB(Ձj);?y<]z9F]|[n4ib* }u@Texf m7_O x!74gl+'l!ȖLS:.m\ vgZԜ^Lfdy?^s\[ *l+JDTIŬ}C{!{=V(HTh)38`3>E;gt U+c Ҏm1/Hj徃l+|YH$?HڙSFшY{%Z 홷SITY`s[ ]pI z&]dln=lw%bX8^&`lxz= MS<qr%^UmEBLO#hwDD;Gw)o߱p!JՀA80= ]v|pEp8~fLh,Yq CPwOj/1XNj`f3O%~&'5ҊTE{H+,=iAr]/fQ=.Hd' ^H}XsdY<' N!k9xF OWx+0T`v6֣XI*um4|I,fgB"2hPOE)5[[̜Si/tB_7"_<7p +w,iW@js{m_Ɯ.`};e9D*FY͞@jξ"A+j𡩧'Təf[k wi("_Cj'X^$uԁ%LRfkNݽP8L}*w#J[o,RqP=v cYK*mMl&>׀î=OI}RЀs> Xl>5qp\I a x(XM UTzFP" h H]~aYܻM&Em^^Xr%O *g#.Muob M ')^ZBջ5M\fFc0<|gЌc4D$ko|7Gֺ eq Fd0Q 8pCq{EP!4Xn6-1̊`/8ٿp00tFJak _ =Y~>;gMwghBAt{J@'#J3d%6,/ծl1 NEU<|;b9L~%d>֝]JKG $37spG|g`z )+)U稨sgOne%ńĬ)RU `fPE0}u;"SA[Fs1AaX7 =A Rϝ_g+\\_HRLy鳆#?°*AKi ǂv8c ]6-'@#>0GxE]pn=b󶆰.$A#?oC էCDX$}= F=5#eHiHʨ}WJW : *Sꬆ6Y^~JA?7֩L7:s69S5m/:(,L:ip2@IDATq!MoW=hG"U1*[_TBL!GDy rA"dJ/L1ll?=hPE\_Ua2#4~qt 7W5EcZT(uw U#4>x4qM@P 1}}2VP9T (@>:ņ}(c< vvrׇf=?Vsщ(Ick(^tl:H}+M1l 4I+?+Pjž&Ԡ ) ΣP%ɮ#Gxqhfh{EvG8<{wKoVH{NQ̓D"$\N85k.49l5JXAܪ&2fq9B4xIݽꬾQ>e(Gѩ ٬X\1,81/sҮ@_S\l^q7Hi*<Z$S],4C(1ٟ`3\tYw\̛?(Ht3k2[NXh7蜰K"q# 4pSXG' $}y Wmzh~c UIZa v+=7 c%n7nÔ'HE&jiLKhӃen`tXtss6+|S[v7V_SM*#Q)L@7ּO"{>LG6g C(‰׹eM7nB z}龓lQ<<5i}lMǹ=f|q3л#wNDxN[N@F#nGG(3k MļrҔh^d?xf43#hEvڴu4OR0_4~63Dz73du_=jO=R)UsQFL)5Y㓬>=+XRXQs ԗ^os7>F =M Ji5lSŬ ҈n˕~L a/@aB`VU`m#ߏf#0<dọ*CbZb-@ڝf}fX{i)Ξhd|1ts"R^hZ?UyPT-;L 5B3F ;,blV z2q(~E/z楿׏M1)K^zg?T70+$=&k5'{r6>("#BR"ʛw;JܼH/>(j|~94!{p}biSUp ~ܒ}1VIF`:x*_zڶB'mkU V.|:Ad$9bhUeZpf-̮em W@jSҙcN3&w} DB2yR]R#vE֒EJ*~g tDbwRw*Lѳ?zv᳻cg^ qOXR1gn_D ?hSp zg۽ȑYӟ()x$9}1oKqT6>m[3S$J֘ыK` 4ff1 7uyA 04J^ծslk/5gQU֜棃$G~21HS4COݼ|jʷk <k]yBOkUDOM@9C9T*9NSFqyI9Śj0 6Wڋ1-c\-wݰogm[vS1xKcg:Ta`Uݼ]ѪGq F ~Mb6uafjWf`ˠ 7=F֢i2:5nE{ `>x'?P {hQY PyуkLC0э֥{I)pdXӖZumZCYD` 4>3}N?x",8ۆz$KW4|lS=Ao+D( 0O^t5n DC>Zzإ%)"?8붛{gH>$ETt=]3^;cq 8gM?A" § gFD S *xN;ֺuϵ݋ZǏg7d?{a/7=hGq|ɰ¯XۥRqcj3UxaPAGmR:u؆/2m1WXF¾ .pxngdӆGq5y!č9Jt:YxgRfzCqL m͔gBΘL >t4qa4M[Fη8ad|uZo7D=DY; 8Etj_W?B>OX0rLcFo\ ; r-7I  'tͼ|7Tafu_?ًO%NO}W󛇧[^/2ER"ó&jgRXhN~z.'SlYa§)K'#C:ëI]bG@Y&|t$Vm|nݖ _qrv,XqbWg]*6Ht9XcaQK䔍ꥉ/"ts?%{$["yҶm4̏+Jܬ_):6g4S 8d3K}fgs,<4,jwp`Txx̵>xmrѿx;cj1,~}+&x5{V/S h{dW΀<2tn:xX+蛴㑀eofu:r1Gޡ)31Rr.go4Œ֛]!5+Fe}P&I CwcZFR a[!v)n+.BP֞9˼z-OM8'3Ĕ?&5@FޣI[}@m;.ToоT}V/8?c3"~sAp[EܤS^SS[7aC:=1f3t7V!PZ$==OPc4]r4xxpY^PMhCO>{VžY10iMs'HH$6pO<f ]dL5s)y8j?eu|qj2mTTsyUј D҇[xsde# -fټ%<$uό;E5!ٍ's'b1"\%"Bw;;oIRC$(ܦ^~֩ƭ"9NPqśܠ-:GGo ߎ8=AV 00W5JmLsK@E^$Y@i򩑲½mW!A XZMAlL꧖Dc 6>(0[HL2OD$ό\8N v Xg@ 1~%Đcs-=0dE~Z:;\Y7޶q{R767mTquck,W7P5rwWTxnvOXT Pu7FvND+ӭ6/ݨU=eTvPz*ϰ2r *jں.ff'F`CntC?[aBt| l8aHVgglאָi?f 0F)?o`|a "]%4+>eK\F.c٭1mrњox~u*0OMlqW^xBI+hJd.Kl_"G-[JFkX+}5" c ?$um9ZAjz-Wj:-t&s7W= h5J@]jX'5߲-n݂TԵ&q N%FL7Q ̏GyU$=F_q`tC[Tj'W- Ӭ#p6npjs\`T%Y>}W9ȧLNG4O, Q7Sn$ 2fT:8aȯ#DVzGԵ ڌtWE0 gANޛV+xZ`CvMZq^c:XIN]=LK-sJP_w!9k.2[\ $ o ye  `<%( X]< N7^R2*ሏȹRs{\'lWYoFq['?۫NwU3Ḵʪ /,SU@u3R'N@s#kKL3IӄvXjs[b{_gļ|-;L6wXR%vهW1kW<m4kۖel^M+}^ɖQ1dӗ>׸Ԑq z 0·-8:8+$9R},OqU) N?SMˮ>9SjM_9CRVT!3G U *IR mK|ݫF.B!ԗZU/rhY?3jܺh4} @ ȁ[E 5v+ɾ3. '+m$zCdC{w}+$$ц" %R,$ $k}esЬ>zf NJZʊcΚ, Nd@z1fb1b wdfP{[gZ1Ҋ$?jrNFKĐ RQ^b `kg X́78vx#䶧4—l[Vi) sn ]'Z)LvL-S^GC2A$Sbh'̭M>L5\MYJ||$6S@ fșP lU;6dV6ƴc)-),֜2Fl_۩ЏI5Z|!*x lKA{B>.ȟ>H̅Z[IC3kviMmD[$6˒ ڋqU!RXXAӚ 2̫~bpK!#lՃ姲zԃ-;w6R-8rQ -;A CWǙmlfj|6"c32̮Ֆ}N@d9r{_j,ٲx'_N.N>З70|yR]{fP)|IV7n:]a^TN-ZSH)4CM`PS̵n,qޚj'x*ywX3zaKTV!0BL45l=ݞ@?Yb+]?8s^5u><< #KU3nڇ?[(wOpGc# gs=^ZdcFs"bnr3dvL_`hMꇻW|sW[yj>z{йg P`Q pZi("24%?͛1+lO4*;mW.ϵVyX eBBbT e#UK9e&͞㼨p&ZhXk |f xN wy${XwV{zOŨRt3)N, T5qW#z(b#~ SqdZ/SB2U㕧n>>mT:tÐo=S0 qjC5‰D *qNE1`T46V+w:f w#֮zꕈ;w-Sl{gj ?<#<}}?J~5Piynq[f3K+#:3yX4;yXEZ ] 4:R1fIF$Mc!muug Wc]xD"Jp0{~Oi\ KgvڱSvU>חGFHOK>~b#0Hdž+YO{8~a-vD542%;q}lMj\4JpQWOZNޛӜ'ZsO=q]#u@& `:>H;0dlB=n32ID4HM )dޠȤ@ JY}6y V?w 9%6tejPPX>1hH'R>.{avX@hi*@=]bh?#ZNW *. v0"3-~G|dK5]e ÁtNcd-^',v̆^-![e尟Uh\aLa*+H7z衚m ݽ9ϗs6F$%`Ƀ!ʕwp"qVHHӡCJ(qy .:RdĕopT}K]Q;? a bqoNl?Z+jTCFf"3!eN!g~JpnYʞv?y}$u9GŶ:OکyHq[ UvmB.VGC'w}+7FA-Tniv77Mj3ּ`dDVڸaDs ݴV1èmD d)¸,&S[gLL0U ~A:8&#U* @nTS?ߘ8 0n`5 \P=ӥ-DTK!ʼn TO"AV=ᇙA"כ"OBU\>1%bHUb,9ӮV;ZJpScxA^~.32¯4'Tfs^oշwu|^A([,&$.'秬$gDb{|m-!yUD@ dۜJ2EӴŔlc&@Hri!o@I톁Ƥu@G  Em8;gF(:ӳc*!P{FUMn~~<:;ֻ9&}yf OA;ŰC8ؓcl^ yihۛ H05LVdefV͟9ēMJL*=+`f`wa$\\Tvg nrq݄+T-zGzDM`` JM`rf| ii̺-(0`@h3Df 1!EF4: Z Bxge!XbխHI猫 8@HJ)kg";oH@>*6z9%"w"jըre+i?z-#C@F޸ mm3-7 I%9:R6u~2L7]aNd02j# ˓o Ζ&M]ktaYWK7M!G>w8m6[lQu=ی˽RڰMDnSڑk%}M>߸n*2/Ȭ`V`Ų!μTQ%mҚf&9]OH137 Clgb紱fl@?fhE䄥oDEDܧwo4yoýY_ij9cbEOdTn߲% &KNx+Q;RVi~!DC+Q/:n>A= np(,,9n>1H&@ˆRvs Rc,hQ%N _fuizŮeSS@5tVمEA +Xy `k !6 6LS=S%mU$ޣDLC*idLh^Pe2}alJ oZq +"4RHBR%߽HEZ"P]''/Y8Lı)2`o+K9^Pz| /n(s,;_!MGJJq; Ѥ9A|UŦ!Xq{SzQ)|k Џk,*̡w+E5m 8.&KF:HU߻cv/)[SJvHQdjj]OL  ti~lx|OcjZYdke*n5TM[;}ȋuόMR󮪨*PZ\o m! xՌMLY]9:3g:T܀ub(13 h)W0 xشGqx8|qm,s%6ܩն٥Kl*7%d@B ZSV~ykn-BTN^r?n4 T>A  sƵp K+zt?#MM2c8^?əpd/ysd\mScB1nɮ.BW"yhFOCsԏH'%M5~Mso33zNҦ"qtE,$^pUf'ni_' qI?>CR 慇x&~9uf%$ql|$L:Mmo~8SJ^FF0!R@L8GW[O|͇]9{7(" &_{̲[!0=7Q2??U"d]({K ?V:4 ϧyϗzo70ۙtJ+gRZGzFSAy1?Tѡ ZLDKS({.zs:,L1UqLyߺ $GJL|'Z`K Uh_c;52d hlhirH0̦l5x9 sf)C{[-+ 弹Y![2 O]>93qIvSSlU׭Hϵ(JGZQ5B1M 6)EKm`]r[0ms1G|G}/ g'/Ǐۖ\ ܸݼ,crkokԙ`(]_prKs@VCFϯ8_ᙐ 7 ܖy]QWf8>46}&|+nVaSİSwtދ=PjmQChk'9;Ԕ|b.l*=v91e+=ͻ6~"9R秧ԁr0UiM۲f3\w8k 6jH@7gnsE$Lzi092ߍS52c2=3DSji/prp*̐E9gVSVȽ}I6*X-3ifp7rYv(ǎ͜uDZxRr'㼐{::ӹXwq_A4^ ifpA O lX`̎_CXiݤ.7ѳ]Erdm>"$/YZ|)1ae?e*8(r9I{uJ f$*xouw} ).E:̎MPi%hqC]aIa=;>v-ʳ )-R0ljbwz"*?ɯI!x۠zl9HQx@s/DU{]gb҂DJnְhB:rQvdIa">?tgI.s*"k^.;DE?1={]޺~E.ÍYRa!Xo0ʘQI)(ȢW0n[2>T;i| '#0!h .7إ Ti%e"IOnv%Նn3gaަ N8ա-f4 Dʾl\^'oڽ凉`8*׺I۲qA{"a:?Fހ&oeۋ(:} bN6dh{WHstۖD%`4߲5j 9 ~&DG҅yG ]7gO> n߿S6 ,NRPP&nh -Ţp>MI"|F_۳#3iu8̩9!sax&׭#P}t߽Ug59Dk=.M|phYH[`W,y]u/3 S"SK7B9\|B¼?Mr qJc.|OO޽WA>oe$]E  T, 7+ /'uC`9ӵiC /\@cCR3tJL,h1^pq k w2/Eo#;%H>L+V9!XL™D*η{vf3ԅZmPjg@܉ES^Z 0 qRrh=%{ɼęEW³4(zrޥ~{[/t!xCڜ'3zaez]ȷ?Eel|>+\oJgbZa4摛~ FA{B+̴' ~-_~D"A.ea/ CE/x!N(rˁ#oza}sE]:f| 0Zm*91 gvGlosvo]ł=c P5`ΞxWfvh Ś3}?Lzkwa֊?/_QlҞIzG4smKԍf.t f 9gVØ$ղ<ɤI3.s#{1jn?E,FEz}8t2UE"˒/[0pyi`Ol_d76}AZ^/D<;,yq\~ϸ#IDATDFdAW ?1ÿOD7y2yv; xڷe$$NO&gg`XGF{ǃvPsePUSk.uE8?ŧ|: ~/,|˳0_aA\ ?hXN@4Zqo鬍Yaqs8k{)CK-":0ȫߣ1e:m@~Q"teOW2hð6aoZʬW ڏ1I8A @wBstm[. ͟95)_ IcaF8=8.KᱷE޹{w up <0, 0, 1> group material.@0 { heightMap = "bodyheights.png" #emissiveMap = "bodyemission.png" } #group material.@5 { # heightMap = "bodyheights.png" # emissiveMap = "bodyemission.png" #} group animation { state POSS_STND { sequence @0 { timeline <0, 5, 10> } } } } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/testmodel.pack/iron_grill.png0000664000175000017500000017772612641367670031470 0ustar jaakkojaakkoPNG  IHDR\rfbKGDIDATxڴYmI߇bX9y3rꮦmɰj؆ aɖ, X HE˂G%IdfA$,(XՕUyNg^c ~uNVuu+DߊX"wcYն:$E`;0`|hDKdiLxcAIT r$U?Qx&0LDB q%%:sXcvAy+@@ 06s` h CaL&ж-Y+˫`)0>}z淿,w)`Ǔx qe 1O?j1"R$B_wt]飷v %W,rr{go@||FUUjΧԛkƧzSd2MӐ)'nqҕKv=Ikn?H "wDJ1)Gxsxŗ;ٙIBJ E0Gs8߿)%z9 hŎl:d4Z;:>|?}u(Yl`4,F|紝Apl6"e4/+lDbzvwpQVl;5Qà(8a߿ߏOR<kIWCOg05O׵/SP5itcd>b\c<(X׸#wli )%eU4-APu#,5EɃH)ɦ%(H>Z)uMB'k,=~ ۛYJ$q(`C/JHwȇ^ypi>d\bI]7dYF4k^,wzPnnV֚Śx1W/!`8,9oG9R\.I7l6F7:xBM 8V+NO^ G!*Q.@m۲8c ÷XNu˒QgGᛓ<}zoyOv<~:;akS?4MK86 Cч8%eP-hkCry&zzS)ׯm{p{~O&"C=\087t:E)Ig(HlY>=B j$9y&]1Qa 9@I}9seX%RI>($eU)Zx "t۶M#?mt3@pƀ1ZHEJU$ d‹O@> ,$<ZRl_ MRK8$$|;oKCE8(IP"`|Ph}1u]c!p RmI3Ʉjb]unEkQ 577|z!D@&B)нiۣ Tn=m!Mס$I֊jBHU%1!ޔ/͎1n7uc?Innu!BkIHxEݻΡFm.`BU< $XcAG8"ߕW4mGg yќAR%aQ@؀Zk_|kBpjhSʪBKu'rDQD۶t&xttr>l`0U#l@B01GW>C)p~xxz4cWt]d8"2(WpΓ)E~wքzpt8e{#@>,Ҵ>dΐQBܺ?!Dk mg;CGGǴmdL'<:}д-!N|'G14=F$rs`8[kY\|N2㚚, %9.@)E䫣܀$Ih64_0:yȃՊ 4ưzcTųy<8;9a8R.^Qz}y- 2 n_a:^o0G"de)JIR 8N%/գ"*pu%d}<ã5 C꺡n:/Z޿ {6(EY{r8hMHt8kC$:Nk(SD: |!d_5pqufQ1$q4 FGƬ`+͂( tZm(@ ,MHOF'[k٘bvѐ4M;,myANdYx<0!ł$Ymz{RR"Bд-IP y!΅$A8H1[= C!%`2&Ibk"-GssƣI`8D IBu+b@۶ln^Nb[)E4MC1G|M6ɳ,vQDQ\\^,#Iz$Ge#^~[}mkIF|7 zh4@G5U)%Zɐd:NS"Ͳ4<͆Pt)%ZjbIc}׆so> uJ2+m#'Kc\{qڶ%RH !X %mR] чRJ<ӰfpzurŽ4 1n``FUUMM&6=5ruI4 ({1 o.t>N@eF[K@@Zs dŹ@ݼ I q h1s}FkSG4hhU:^_ccr.Q{?|CJs\~qG;Z7xB$7;%>.D*u?H+c]zk,Q-8Z zGL/~Z1($1A\6 ;rS(M^9yMP.XסSQt|m[>[.039ڦtѪ077KXg)&gPEB)Ou໿AB'`44k!$I'0POXV=IQ5-ѐIg suM>zyĒ}<J*W gk ev-Z#k-y۽C$W4r}Da3=g]%v1 WXov(ѐkVm_vΑf).fC[R%א8rMqs/#24= 4 ;3,W&m<3!>kuG'L\ X[7#c4 k QOu MgΓf MmWb00 د4e;Th|za4-:B3'gxprXR gih9 fgy,+V8R(qEQPGo,iruޣttm6[\<^# J?g6,!b;Nzk5s.붻v6wGA\=AR|QBe{).Py(`svn4i20ƱBݬ39s,D1 Ҏ{|jłfs֚:F 㱆c8(BRLI/h֗< )-%/^$1Oβ/><|+)%P Oۙ8XBawRda Z}{'Of÷j_ҚmvpOaU(=qҏGHE&X Ug\/kNH8X{,|@G%"noJ\myA=miCϘ?xrIB3P#RCKX~]1sEF\]/J1 Cs4\׷:R_xIdYJ ?{ߓ%"_i" %Ļp?zUUu #:0î,n\__r2f8X<>yx$8 zObng6uk`FTL..ʇߤ,Kʲ$׎,97`d6e<TבqPE( #\H9>"G)qK6 +)th> GrAaݒ)Mpݾ$RΏ k:GY6,W[>x+ 鄓dzGuq]n#IT>e{+rUwXkJw}UcE lT#!q(pֆ216 C<ػ\uTu{\r!2ŧ3fg67!, v=J $ I3R)UUveZ3 yu~Et# Lcc_V#Ak :}5ٌfbt-GJi!Ze9Yܺ/dP%R)6h)ɳg4К6 }PeH)RHX1;A q_!"Pә[6Onmn~=o[ⅶ5xaMlhH_ ~?q=yu]vWEg3Ϳo/_,j "c:2N&*EY8v! ˓h ?[iSÿgmKV(-{APLߔĝDx۶N/|g_o1ﺎ,^2ʺe1NJ4eݳoa Kw?<ﺎ*R'>YʫWHW~?G[?]m8xNODZ1(EF%)J>s?RgOiQn Oi M⬥3 )ә;gOmyxFW&NdyApw[/?ٟ_v;5HI/N+n;m_泩o~ğy4d"ձDZxo++X_NOK?OR4M>U6X Z1tW'k__|_>I)fzi65EQZ 2'?Sܟ+'EQl s"g0X8g,%MSʪd0ʲt_OgoO$"⃯<>˳4 0!Bl+ݎ4;?=?/?֟K'Il8gTux8`bZ3vun0#Ҋك?)/ogs$д-К.㈔dۢg=~~7_zgOnepkEAWI$1;5h=z?OA& - ?#G}ofi"23.DgUx[9`)1֣1aacuӒHAcʪk"-{K\n=Zܡ BVXGkq޻@"%yaږ #MbʠnGoSR>a<ra E -%^xz}yã1u`{b-q:%*Nf)WWAFMY) mI ,(AH&g=qyo/<>9Eݶ0A`^;rxcM~GSiێildiBX$qqVе ^x}Pu$f+XQq 0dYB,c٫ }"Me!`"$w8PZK'IDk7x$I(%"~)Mhf)5`@Խ*_yr/{SaLoI::cRKJa;0.T"Yo42EGk>zccv£A< ^#͆7}لrH ö2!@8(;lWnxH4C[RonR$yJ @}T-iSW%qĒX'Qb4q} დR`]ՒE)v}/*QZIW pdI4Xow$fAgBMȾqA:4JRQH$*zH;,ϐ*i@g|{WBߩ? *J% %/giZK'7< xӡUf8]`e~2fكNvh  t`+^ѶZ 0!+RV5U?ݡX()Jtt]ưI"Io@pXc+bD_vJv|(DRP ǽQ ^Q;B i/*)dP+ޖR]mx0H]#!tX5: xO;(Y(q#^ƻ!sV_tyJ$Cث)$Zi<C8n"ڪ , '4KZ/Ah+$Ixy]AӠ6.x^JeIG]G8*HmIk;yH!zwX\R)T})4= (:Akdp"}6, Iq;i3B\n(f2lg !cLۄ -߃<gqAE-ٖ+pzC$a2c ޻1Ѹ/ؠW}_'~tg^d~_a4NQBiNZ4E 0a:C(S]!aS%pIjo.d1NFy$RZ g?lK5QDDu"Lߔ0BTbB' J$Pe8g$5Ea\2i)eAq$-W:?rcMGDK΢ē a OnS$c/1^G}k~Dv4"bڦ >m:4ky ͣa徤(rRThURALJA? 1Z΀ #(-k,JTh0ƐgIP6 e9YÚ'M~OQ%Nh 5~K 9lvOPnw 'ӠC ,P`I\@c퐄*KkCkGE+& AޫEXZZG)nn֡>G2ïw8ĕ[l@9!I5!M4i x",:(˒,A$aQܱM@<2tρiZZ2 % 9 Y.7CE]xQHt(FqL&J-c>W mgX6 UbSX sF:R!{, 8Z,QpyuDYibᝣ g)sb6 q n$=PkADmע,(^)-uP]eh늢(bk,iP:&u@1R3l8(B'1hqmJt}㖿0h5diFr4m8i6N@ XQVU*hkdf\xTyے4ٯW)xP:"b5aY@4O@qIC }iG+EewmʵaGxOQܿnPLFY$8VDx 9J |]xEJ }TYk(CLZ4`AH9'-6ڒCJ)x"=LX@J=:R/&8(]зnvxI4|zǽ ס֢iP>Ѱ`2!#4Y^Qvz{b@`E Ͼ *7t,.^ݑVk; Tؗ/Z7O)H$$A&4eoR %?O(Qd:躖۶J)5sFdӛϗѠ`<F %-JGخ(8_4|zFDq4 ݝms?Pӓ ‡\N/Q1M̧CHQ HIy :P ͹^uݾd: MX[-?}臆'e40ΐF=e10Y;KtC8v#ĻNY"M:gEE|TqƆN?y5Lx1W/؎isc,ep:D:C+#Kcmh!“gYl}lW3dbDG1I{&F q2IRcuٳ,ZA@KƞM.S34m+8"XR& NCPiZ*p, ,It$"dm!=뻚'tiHUx5hg]|@94C˞JKBÌqPt}Mk[vij\ B0?,Uӱݕ9}M1fP1:J}֦Vo2Hqx~^K]g>w(?c s$ku֐$)kp>03DZt>c:n8ڦ%Kv 6Lzb-2, fg rs(#UҶ-3LU>tˠȩEnk-yt$IBPw8QWZI[ϲo we xnZCu.7Mtމt6[= !V/!%VBg{j lzW<@R 5ɉ}YryD4+nT- &ܖ5M+!tLFC$X+>Π,XYZ[ݶ$K6qiZ 2J@]׳n|>gV2,q8dmvǫu/DN mzVFdXnۏ!æzq)I,AFTn|6#ՁBA]Wܬ k/uT%=QɲFki~; mFLc"XwGb܇5օ,-$ =ށt`lpMK+x;{}`g[$@4i3PJsqyrCa zvॕU $UhjWb<E6 } ?~u' Xӄ??K_&7~?>l4IRQ lc8.p]ѵ-?_/ ꇟ?T dz:1]2B45-K/ڇϞ= ?q$ܰOUo>_G_??^^^R$փilKX1ÔO}_}?ܛ(7UCrv2+ts\aُ/G\_O?ar2ZZRwpK&ILSU<~?oO7 5tmߨێD<(,yp6O՗^_|dZā|Y(i;d@Z9:}[c?2GКލ$ֆipY\A3zRh18$dzuO7>??#'GZ]͚T\|>zK|R |pOP5ZH"%"XSK>|9q o aNN4dP 2Ⓩ?棏}y,d6歧HmscY/r%_?Y|D׃ 9$sVԭBsZd7p8 .2xzV`{ge}'Itc#(,!AXQ,m-s{)^ ZuqFgH%BDw̿(i5-,aZ Ǒ$yJD|oľ1 N l:tRmBCJFEAw ?N8kY7Xp{^|~jjn֡f=]|( ћ}UIf8bjGq{ Dqd2f8FdgEf#YL(:~]J!0XKt JA53=im]"uh89KT T^~׵ NT :Lue.Z^!QSmJT: Px5tco:`zmLK2,p@>KFVDlK,1(`\y1펗1>W=64Ĵ]`om(Ձd\1u`[ 4̾)%q |HFD> w*ZӢH,-;6 k rY(%{@>?|}odq"ˆu(=,Mf0HAB(^q@8T7`"%"Xa=p$o@9j"b:7. !54]FڶU'@r4c:dȶ*I>4H |W\\ E[ۡGiQHB[TdiBݶxkSA#%4'7=MՄ=}8x &F*o!x:[6i%3KV(M3,eX2JӃٿEEf$$UFλzC4̾%8(,Ix0%&tmV;N?bBi0bs9dє; *N(oB_)`4 $G8t2&Ofd<&.ɲ\tBE"T:0/o-"M٘W;Kڮ#Ib'pXu@b (u?,n~'MmnHAXExO㸧H@ zp[#uF^FXDXi(&Ug42iprv>Riv $%U٨81:;$b0}HSU h( Gǃ %Cה$4{u$ ` $^?mhvG)EJwHʄj=b0CȹY2uoPM(cNO'_=F [dFd4MG' DeB `X8KՀa"jGLSFqBN(P!NR,CKlv Q>!&)a|6e*,%MwM Q=<bסf:1* 4`X$IpMg##?o`g+$0TuhzbZ!&IF'p:qih0+{J8&Q(C)16I(;jb QFU T4mHGí'v2ᜣm Mљ F*V*8pQć{0̯V1(O&,AG=S%0 kyݷfީ +8 qҊQQ_=߮<|l㠃|)dm [m΅(Rp͞hNg:J-,y}.F|Imi[$Jt껇;UWUJbHגDI2Y`rʰ{{]F#vG2tgXk6 DWn0ޑD[m P`k;|wv!IOc'-~ﭥ*V4Mn%~wfÏ_iJ&;h6خcri=KYʃZkdRU;LA*]~uu[U5)Q񵯽{0?ʵHLwH34bќ3l[:]I (2,ԻkC+e<;c0HJoSDZ3:܌=B)$BߌsY|j㝧 ٶw]U!Ah{ezyw0otcPdf2mܯA)>~yw77^xA4yk{T>"ʧ̦{[OVUUaxNuxȾ=o~v5~x骬Kg,>`m[W/y9r~cU5Tu8{;5˗Tu~x8OxZVA\OŃ9=^uFE(Ṻx}+OWM݀}Uq*Hc̝< xッ^>ڇ/8:Pmf3lDJuh;(=}[hxɣdEQC5RqP_Yqןo: $y$ vw1?֚Pn$)v:=LSHn?a [Re`MDZ3> 9J+ځ !EW!)YѨ]~F4wj=5>pH}F8,;y=i ReWZ1㻇zvB)Is%rY^v)89kgfrAf !K^Yo6!K=zDuܼm?|7HPss AoEA90t]P)>QRxHrqtSE3@H0M"4OR',cJS"y8-(f98b PsJG쫆C?z۪ uc[3uui_l6hͯyoa:8vGUo[Ɣk2{U][ yVpY9RJ>8Z,qL&`H۶ԡ(/u-AA:\\ʫWZS/O B)]`Hbд^2jHy*@cvBvxPł`{X^QJ̀ƖKnG\^_vu# *08Pg:z>89;Q/Iɨ[Q1E)~['DvGes}NdI0ڏOu'8BsԾub"I :kE/^.mL>vi|#: Db`4pPϟ?'cpA$71kD%Ō;98 N9Rp4n:dJAeP.#Nq*M`=NcOR0l&aL1|ۿ)|3Xkw~=D2yq)U({uBi"x8'e:>BUk5OlnOHy}yŢtTU^4^N' ~`7g-Hi7+a^Cէ%B K>8vOԌFlbnm;4 o= ګ =<˃I"P Χ{t6i>|Ç]uGo8%N q8H$fquLw @$: rOVD\4Bќ4M999!˲ 4l6; HC栣*~W!Z HgPd["B麻!Guxh;͂Rh>*YktǼ"t!9Z\'Z#.v"B@G8~4*!%IKbs޶Ri XoBaz)oR.Y,ȯa]/PHID ]P%uǸCUe#.5ł];F#t>gޡh4d:+_C RBH5C/=R f Of1eU`|>Ȳg3u>@ۗF#u4Oyֻ#\A!쵢Șf^VyUݗ7;`0t؆ V+ZQ{a0ݖ4Ϩ8>XV\^߰8ɲ,P QI)Ct`T>丽UR$_9UUX,c9cLo730 f75n.yI~W<4MKwl[>49$2gOf5DZ_5A#R֫%eUifyOU4M_iXmvTuC>9#re`r"fC6MSn;:p%bv>nw^R$: tIg-8k[XDUYloH8}6Z ~ 1*89aB`<syu+NNtmG ]~Ys?6D%)Bq(:GŤ9擇 zqCc >Y_do4$#=Wlv;$E shF*v7ΒgRh,+m;t,aѴ:R I1!GPyMCvLS5qoi?PJl AUe񚲬ꚲ ݀&;3 /:bpd~e:Qd6FXovC7\H٫dJUGq1ɐ,O9qSPbt6w(7e憮Bw3nIr=b3Tͯ~fsR)S_~@RJTu.̕םCA@)fsr}Wo ׯ͆?65_ZF:b3Ͷ$RF ]_?F?cp~ubM2{6]=1F 'g}k>%/ (o7O$4mC4F{Ojg?H:Ѭ bM1N:P٠tPJdH:y1Q1#рt|#ه֚xt*fLSVՂr_$ 'iK)bb}}3W &Yk2eb:"HF'y~cA}EQo~ $ۯ} ӵ \| MC-c-Cݚ5U]ZoFOb/R2 ᰠk)AQ;NqcYw׿s2ιvu?>Mb!v{M]\^\^cx}i!cQqMSҶt͎՚'MN^a!$ 4%;&x2*y(6~vnt2g/06䅤he[M)%@ NfE Ͻs i\z$IB۴ C AAvT=SIh4<Q@8y4MYWt֐eys0F{j}`]`= ϑfd<*^CfÜxns6"l9)`<ln{OfjCӶq>=Ϟ`677 ..13$#(q8DfC$tv%>`yQpBfu%H!c@XGӶ/\aV ,q4`0( O ~Xp2"d4 .Xw>cy8,%J:U0 <˒`HibFkI۴}cX AT1 !89sm_e8#Mׇ_2XBG>׮4|EG`W-#&,3Xchێ(̦C.P1UufnSv;4 E\1_TJ)R E1`W&C,!>QZdqS3_Z riq  frntFQ&5iGBu]ӏI:g>2>{gt2Oh ( ë#Q.ZӺp!$ńfIĔKn^ Ӷ-O8ո$&2Fq_7GEHI&p뺦.kH8SJqy^YYgy qՋdTM'=gPd%]\\^VFsA$"+s:=}!McG9h'9WQ*&T6Ɣ|RJ_|%o ʉ_ܾR5mbORvstE, &!iK(btdtJs}*D۶8{Ou#iq՚Q8TۮAOѣш XQbP!g-ə \>:O+K4ɡ?پXَ 9F6nU)N:G*@0'"\_EzJh7Y|{G]m : Rƺ'ӆtM`KO_td:R7(BWo|bZWo󜻻;ڶ#1߽sݩ9>x(E@k-eQR'G( Np\_>nGyIbn;Y5imNq旯L&gAt}n;_r1 onE9BZVhۖ|Λ/ϘNo}w#_4 U2x|aq1xzֶ-]x#aG$nH,<\!Z{us@I݉N4 M#9mr1CZ8b9_jM;N##\@I/a !R @g& Cڶd=;JaR[?~OovαϸX|cɧ m披$Q>@XZ uD`}3ɲ4Mi?MlA8]A@0YVp]ull6#2R7`J6 3l0ٔtq3,KS,E! jb c#S!\@ktE:i:`7h\nZ>ofp"M" ,8;Hm0>xAd!bEI$,@#*ߓOQD֬kaExg3e1$Nd<#8KDQv5*Mh1R' c,{a9jtmłO1ZQw8:{RXΜcIr]G:'( r&:n7[ƌG m; a6MCMKݶLWd()GNWkb>9Mۡ%@ e5=z&OqzxG6дqLu$]9_:X^uD(pUU>> 7I8I(ڣq@x]s\0$ xP)5V }W?k~@K|>3 {)⼸Z3b CڦcX`AiɘYdB8Y"G\/חx=Y9G۶tp̙ϧa0#)k1QzzYt]i/? Ǐ 5>gzP֔%GEfYxkEH۶p8RM3I(&Ϗ;mH/T ;w#Bϸ7{g&S:Ҟ6e5 I;ݡϽ8bW $(Eh!QR$ky8 ư?Dq 8IǢ#zVDRJ4/˱Ovmqifk ǏX3LږnA F!!m| ݚR(\.;UUrq:qF!Ŕ,k d8kфŒ|{G0`ㆱ_|9X>ݡxA8E<:m(d2-o(ċKb;/5777l!v=Qߣ矴mK(rA5Yq ; ~ՊP+ gnY7?='@v C4ܣi 2% CK%O'WD ;$s8Lӄm닅7X瀡/NٱXIØ(Q1>=P9Qy0b|2Iy.b>|K&1*]b ru`!hٿ;X&]}?~z!^m{` 1FuP%ł( I(Jxx!,G)ZkdiLe/pɄwv{[#ktAQUvtl~9ʻw6?I1?zu1NU|j RJe:%JBk(0`2I GDZs<hۏh-Yf$qD^M˥dv+v(" $꒼lHfkay!ߎd@]wq@0(+ѐjl`>˼nLZRa$hZCoZYo&<ߚǻ_}A(*Q`zG^U~pbmX.(%̲OK~#riR"`2I};9Yia6!ٌ,KN2-=Hi( *H7HA!(o-/pQؐ^Xa(DJv iFU<,]ol@gm^|A`k"_Rʒ(?aߏfyu`6ˊ0',kKʞo^!CW9C&/+|~>T'-~ <fm[ji<#zIcw8޼$ ~|Px5UJF}nd(H8Qݑ(B& #eUoIvXh5dtZ!( )eӣr1!v^\O/<%8 8>nd Z+md)EY㜣kVk8pOYl㘦HcAdA#NR︼?PU%B*v ֚d?Ngq$Ak*|vIDUDQ'?pV 4>n&O'Q1Ƒ$IAsHklă.$mz(Ο#u>|bp"/s@Xk9<|ѾOZI{sj휣(ck,  !]=='K}[:6#zP%m@OXkIp6jQzsD:M5藿g G7{jNPC(hB LoM3x|)8d{t]|6cr /| mzv("+.+yQ2MH/Y-@) c' ç{_ Q`A$|t ?|,5I8 3N9eYѶ-Mrܹqь1gΏ^LHM>SۚQr@Zf9\^WCDh}W/o8:8J˚$# Tt# W=' PJoJ)' CVd>/J) b?6 |FYn€ƭfb39m>.Y_,((1qR5Q<]x#i2᲍$Sg-JKz.!(t%1Xc< Ԛ` cY59;bAjjZ$ 9NOS5iHi2Obtb fWM/st)p nHG9Es1MSa{@ j>{鐲i& ZI#˛8ލKfdiJ"}Ky?"/85jI۵TUCdiJH| ";JzM&1I4mKۍ[ Vwu Ne$ω6Ơ$Qȷ?F@q8H4 dՅzNWޓޕi :hڞ4Y_,xx܂sX|>k[, NgzcH%F@eC'{,KX/f' `84x^aIDATMD5Zz!' 5iѶ=ih;{(EZK2u 떶w4MY v=1\ %4A)b6n:'I"ʲyϓ*/HXfY?#~/FFR%2YFF*IEP}g/Z qeYQ7ZI./disۖ0JS^faڶpз-w;g~A4Yu/v-R f~=uWdi|$N2./\^mKgzd/!QEQ`{ YÑ$i[(+kX-|{~gOԴ3nP9'ң!*-b|I4PYVƱf+@o2)q1N9,Y<\WG '&Y)@Hc6FsǭO?~u]$)ir"774Hc#G@1s@&^TkV >5h)kgݡB)EYԻ$~Lw=@1r8Lx"xr`@i?&zoL8%EYѴ YP ͱgH7BuUu=Q(%)c Mo (5)%S~";~ 5`1]?0 9t6 4k&IzIzE4>WOkzC >ngk}-?~DaHo;`>hێ !{@&!4AIEu򂢪J2IB,3C` aqd%,ij/ p8aTb7b-{ Q@P<fSk~IQ-#?n-4Ik8ң#i75UuiZS]4D)a-Ip8^߰%%#[k_}ICH,$!?#!K-w8 81mi;! p >%78,uely`%8$_Bl|Juh\Y̧0o>tU5ԕ7 Mဿf#Iv@Ȓ*hɷ̧ac>I)v%au=i_ў" \ w^zVKb>*f˗{Dsś.c^ 70#eY %15+TmKw?͞i`Ō8IhD2 P%5i3?|0$n`'sMŚpa֑$ ,AjXkjXxysޡtxnZڶb~5AIIhcGqf֒4qu98Gx'TbE\QR{}ߏ⍧:Wګk:;l I 9#]-Iuc-NR)hPpt(0<;kmTA PJ?=x֑A㉺Ipsx9-1}l>g{}j9݇;Cwt"h@}XH@*iM S|֍^rJ)4Sy'm'W0J8Qׯn>w-UUᬡ,+$4˨]4,10b¶xS2*d=sE/oh8d abVU2Ľpɒ8NXg}wGV)m]?BA:1@kt4'qkGC dCg:F ֞ m{z`:b@)1=AF*A+?0_,U;=wo$q 8k:D]O'nI8.-ęCࡑeӡͫQtمTMtb\2|7 by{vI;tdiLEhe@(xhc갡j1L')Ji vsbqp6hXzá(X-b`yC%4mᘳ?`1޷G$!mk3`zTތs\)G# u\ hz 858GIÄG@mjr`+h@Yo rZٙ9Tu㖲-Ŋ<f1GnϞHSi{>~dH%ͧhۆz*\){&t4ow>=w ]eΫ$Qdǹp&˚QVblGYE=?m59ƅ (Yq*JdYAJI#$%Qrpc @k~?l~!Qp4y$H-ͯ:E!f."b%1dyOH֊,shf1U1Ձ?ݯNjM<ڶ-}w"(` 8 :c,xo-Z*lUk&H) C ǜY߰O;uYh7=Mh'ƞ/_Z}3k?-w%a_!Rz2G+b:4(K(فn^^dn~,MpRY$19TO/}ci"M^sqQ>s?Rz%RJZ!׿L{;|1o׿7U?H5F UM|*Кn҄"Rgy}9uuk"A(q;AHOEuoZzW9jR@]w&{~t$MB2flt]$x\\Eެ& 86_jᄀiStw"JjPhD/xx74 uq{fr1*nV9jHOk FNAF$)+OXw :ڶ9L&ZnV $>%~,_/Tkk N黎w;}ѲZ1 c/4}}*+o:ӖHBB;D ݖ| %cΫ=Xʲ&o!h4M#"TW_ם["#`Jp*s`[߰hwuO6_1q*y RW:Tuy:`kcRS MVa{da%CoDQ@a'STMK]ׄAxT9҄jw=e3_3hՕĮŗazhMwJ3'Ԓ6RBS?ps@!eۛeGjob-SMM ח!Ԛ,x}OoNj/y/e}PyE+#|8Kkn_B@G軎3e{]ABm7H[qkfm;S%iBtU {ߥQQNiJz Ԛi|yN8#"NqO_}1jq+lwE.=JLׁazlJЧQ*ʺF AmM}7T}wI|A*s(Pv3"Aj>JzO2ns:@IG$^RL#ɿŒ]s +'׃H1c8MgvLdCт/jR/HMYo󾄡V^uX|ׇp ZڮCꀮizz5ceQ0_8nm(Pw>n(P)X'^'||jIky!vYeMk܃@kՠezQUZFI9âbʿ5//25(+^^Ɛ%!Aa):\hOcz RӀw?|ǡ(O&͔͡"jpJMFYtʦo;k I,M<%Ih:!隊Y(b::4!Њh $t82N}GCB4w7}5eU!B`% CnK"~#o0]]a(0C BtZM#IZںa6`LPv=hOts=Qsr^%jNyl6j(PCs<G1ioSp}`rtFӴCa|! B%ACL~c\]_c{ǩ1ET6P0 $fW(ӗjAg%릡*:6x8H"g%: ܆b2෿Ip(,Ö/_09oj!g*AIA5Yl}=+N'T?B߷0J?S^_D|O,EUi1}طP[)(K ^ġ}ݖ4#ɔ~#oWNaE RAxl;埠} 7KC"X.(76i$"\],GO_-q"T\]^rȰÀrZJzuT$MyX̽b/=鄛/_-)SQ6 TZiT}RT%N" AWDaGSLfsڦ*uXy{?WsNyyF z\WBE(qC KH?Q }k/f4xiWyJaLő(~Q}.U9Mksg0T($&1_1_;~j!exq L3LӐʖ85Rj AIS 8&M$|-_^7;9C jMTu0]ׂ $ }nTa-dKOWj`w5b'!8UPT d9y8$#sTudpx"2#7W,]yA:?:?S7 5 V"" fmlNy:p* t$N3hrɜ#B)MRp xpNM磘IS6C2t~u,x \N'jJy9x!x}򂺷 c󷪔B0DcHy[ȦS01,' p:䷿G^锎*[z~A(%{{dAHciZ_PàÐ0P|3>9o~F+ߠ[Og8ivIJ]5UA40$P =¶m:YŜOk_̹Xtiڞ,`TFz'~!`eՐ&!mgŒ, m9GQ[Eu=ij<HYl阪,Ё^{3h%x_3c3IcƷqˆ;xua>˨!x,Aț\r,0 P~q5*i5t]dBX1/qED)SAtCP/^?Ӵ5ugˆq*d[߲mAPB@98wXI[GTm:wd z<軞v (zo=-b("?PsIt@~7O>@Tu;NFa=+LHxQ7{RN'(zg_i? 2 pu>oYKƘB!JztRm &η(*@b}}Hi$8QV @Y6@+%IJ)(= /]H||1JecM2Jp ϋP8JzEcLGO !7Mg([Ktar>##tp<z#8v/&Ic%MObo[2ֱnlvByHc]` <;YBTeC*݁8\,LajNEjElDj`!QS'PoB<1"QR {?C߶/{aT7-MښS !=艼azg{zcCu5?io]IL]w<0fЎ;MsqbBRI= rn4f%8ۡ3Д9tfAZO`Pz!ř(~xJ9Ŋ}]cS%uC1LhnՊ@M1ƲlHIb {fG}v>R5psshE"Ԋ$XZ?kzL7q== m37" tNر}GkNk~. C7YY!q t]O=z,֘╒ƕV>s=M RJxvukX$& 7t6|6f=W?"um{{Get:pBy҃:K7TSNhD9Y˷_*ȪEN$8cÍvKg^cIo'lIǬkDH D(1Ň\__EO(wrx#, NRF"}$SNfߴL҈IҌk{?xIiii;^`% rU%b{3}?O(BM(8".i$#ԒTpx{gymn$gFc h҄'g0ϯ6 Σ״S. 4e0OGmJDb ;g#@Q5>4PGyF \@Y A RI)O)^g4M h)Ǣ!]mJRUѥ$Chޏ`H1o <ÿb5{߰ %0cCsW/iu8q3D,mtxb\ې2?/*wh qCXdʲd1y2Ik t8XkY-fp82y0XEY-b@=٪):8{vzX@0Y݁ -ԇiJÆ>0Qw- df CTM=rENj}Q$0Nsw?<`*Rb8n8A <X;X;,fؤa<>Fסt@>m *V@kW0x:aq"@KVmi!Dx~Gv]KuH(K͐& eU_۾E _1o:[$MYH@ԝ`΋XR%:qP* HZzJ4 % ZK _m 6!m+p:>Fv@G%A?@\I4 Pk[T5EMZ0,ڦ7 ( yd)zOӋ3~Ji;Ph@G"J#$m7^b+17f tQH8|76kXvonl4KB1XCY֗t}TeUqyq/H7뿣*Kp0mHc{:ΦxBHg(v+%c f?E$JYJq=jzM β @!jI4g^|U?UNVP@9SPYo$PC(4͂ -W TOf /%>>6s{cHטj9-_T؃dBow;TC}?#OknP~ddykN ڊx0՞7_R7._~|Jk'@G's=aؓ[\^udKrGQVDaу< h= Ok ze\VKZL1u]?*›7o|%) /|x@!m?_vB~1黕|vIq<<<<}Þd⋨ٜS䅧1Wճ矻-??2eٌ̦}y p4:Օ!̧(Bk|tp|Kg=S҅$t &o=-gׯ_%ګ"5#Qz/t=߿}///6d]M,8P Ab|DMvVSĽ1Ap~dNDTCUYuѝȮj<{cǏ? Z޾ar =r :ߑ^P5qQYEk- |mw899o9V%uz|)=iN2^=hQXcI|vYҜY^j4_hv u]Rxp M_ih-6cp&Z =R t ]=<g)%|+\tn]~cHO_8shk_p(o;!Ik\珔+&zHG!Mg=m{L˜ PuCz0( 2|+din;4ftg"ᜣ,K `XPUW>?~`ey1GAN)$k46S c  ds4{x[f3eh`y!\B KSL%֒PKHR4MGO}i: 9%|nN`=1zAgzrG::<`a΂c}{G^Ⱦ8Gm[9׈Bp)%9P8 GUI1ApAnۖ񅗑 y?m;gk,:Os \ <53- mFxx/jGYXq *u3G8Ц=]Ax8 9Rd*I1pY# ?<<BP7Db~gڧߊ#"6]rM9ŵoڏ4|s4Ep7n.ĂOl>zxRS|E7zhW ÀݻAtax|@aLg3)d;(1(i^"RToι@Pg/"kGIAɊ0 E3ED"}% CmHAM~w:ܓS"G GI\{P7Q$ -N̞rXciOcDvPԉŸ$A;`H_'fgwi_=H\W>U $lp$$"P hΘQj OƯ}C8J0g7cp |T8Niz8@lqhj)UIU5L'(ZKjRÓGVUq|qB~OQgک -n8c,U4wz =Bu%;oJ0MɏGsݏAÖ?q=uӉXe$zg/{|R0='E8- @H $#ٶ-X BqZ{B 0c@Om&r<4,K:7Nfl75/{f)>zsHoHӄhzi}"KcʪBK5;sUg S姂xvk>jd2p܀]q?~QJ1!^raHHu,3$!Bkf1Y OFvK>Y4MS= (~Iv(M|Hpx.v{Y̧<ߏ:AKS%Ӌs@)%W%nx#mmcwޓN,o>78cd`:i+?# ?a8U'7qd]8ŅSBt:Cw{g?ㇻѾ7#Go(_ky<!XEH?{!(()$mA)= )$H))?w`u"Vd9u]Ы쏸UDGo=n RG4đ#cɜluf%ZsOӴ|ڷ,t;URJ~6v%BuzG p8Qjcm`Z?ep 5|¸;})v՞tJ REƻ7hI8w1M}~8L2躎mۖ'8t\i4 jd& Yy|~2 EeUE$С{1ϟ*)pf3o#Ipb6hvMw| D@ZI_d:./UE${H~ʟ}~ՄZL] ǓYh7rwLA}f!YG)a-{|m[u躎X|dupzAd CJI&_So|'P"Ic/N|`;{V矑8|J6pVr{4 y%X]NR iQ@nB^\?cD}ZzNro,]۟sd%|V[+1`|q(Mco< 1;G珟:o.㹷2\sDn`}g&߼>VMuМJWX6kY\扞ܣm[.^~C'_ c?λZͮGt]՚oGcv_S%Ri6o#6`/^fz<}= sJ#-Гl+?>Jɳ{'b_}fk2֯iA G۶?08&Ӊ[Kt];Zq^G?[\hC)^=1u]K<mӆtt"BŵFo8r=EYxӦe>Ҷ5QpsgkO1@ RC@ 횶-yמ{cH=U }m7!ohw|te.wZ| ˫TFeW4c%j{?`Æ4tcI &~5G|Ge{n60: hsP֯h4MIW/=!c:G֫%D=)ḮbM OxRC.)"v̐·)dHӔ@Yf Jy,q8a"ߏŠTJ!ޓfqC5upoFx8sYz:mr"Kbbc )w=Qp:8NL҈bia6p2۪-s;` ۻ{OkB*6~Xcɫ}0I*1w9H 7xXuYv.2N ޯopR@WgG AɊi^ }+p8iQRp faPV5BicKյwVH7xZ7oISoy|`~I<[ I(di4c?N_~uwQR :I%`,FA!'Cq[Pnu݆a axrhcqԽ {& Ʉ0oz{tmrH7TU#c>x̅f2.qvGU7|qE:zv#Zk&iBf>Ҩ*Ty Y4ay(-Ч/?R%/vŎwkq΍9gQRd-6hQ0p#1ojʇ5􆲬x@AHQ3-x6MXPÁ'@Xk$Iηq]GC kxN O,@{6`v:z? za?xct!{7oHé ^b U/GB;LXk$/*N h8f6(F)B !8X.Lח`-op2BnOxZv9nZ;ڈ~35N20D)Iz1us?E!7P ~u1"躮}/|6]?J,T#cX7$jמɌ1oyůS%m@۶]GpkLp,&c<ޛʂnKQgۻzcF tОQR!47Y|8__'5"+MOq),!B6;R,fSE1{r=SY&;KQH0g:؀qq1ξ 煇 m˿#A^J \ u!͘!/KN 4ٜn?|eٛ !5W@]j-ɋjIuOɊ [ooo9m_ʲ,KRmGQozdTšwEȲ?ٟZ`+H9Nq·,#\RzoW?y$c`>.Z3]^^j5Ջ7S/ʚ0G(bpw ^l4UIvQk(^Ԑ1ݽ71KRT{c)],[zP M^8c@7 `{vTuvpʽoHqFkis`8?{ibhAN㐑w=lIfi%GQ# Áx4Uܳi;0T|k,Yr4U0甗h%υf&_iҕ8]<.&QnC-ZGߛsa:=;b'wR)~CUw{nvn,e1OR&,wO<ë@ c|~c u!%?hIA79Gn. VH8EoiG2:OhpK޽sꂢ,x 8c``isoXQt!FRlI㈢AJuꯘ-mi6c,qOmqG]U8`:ͼho~ (c,,f6lK(Q1ՐZ"o j?Pc8C%Ǥ[[m:TT`CHV|l$eyb}3$U۶|q80x|X@cK)#b ;4qwx*8`1\Wda)qr|xGYV4]l6eݎV]Kư\ɒ Z%Ǽ QR0;X%I%дm{7;_2n1HE`c Bz737AyMOΜ1,,k5(!d Cl2!$ͨ& K>*c)M]S UiڎI$12УRJʺ"8z#b6aqL۶lw/Ҷ-u#cIa4;1@с"]r߰Z.hSj:0x JHQR H]oXΧTe1?SA7%4֎~CiAIkRr=GIԛĎpJ)9AT1jILfxHLIv=aHnJG!AGG;RJʼZ;RmR Bk!$y֙wz,!#~#i: Il>X-8k}մq `eGdB]X-֯hOm//fw1b4_@-Y!(m`jgKo((Z0: \Q%ϼƐ)~3v@Ki[v,.!L G#M&Ւhz<ZKsI÷{ew8qS֡sn{xd^i5ԾW*2h9N\ 淳IbAE0^*UA@gH Aj>~SYUғ=i ) p^:Ow@fN9oV1}u$ -/H6X{1M!a2_,}K;C9fL)O Ask$qH[XΧR\/8w%|Z 6w^zYpûkDW EG Mٴ,ۻ&YJS7=:ItdYJ+a#1dE8Biՠ&t&>9faOzͅH$A3PHadLoIo9Rti[3B 9Ō.BIUUXk "u9ٛWJAQ7 AS+C$QCE`Qr||ЇDq֐ыC48V/ּ=ua=H88m[0ڞөZɔ.p:|sm+֞iDIfq }1Ek)CDB Aմ"P»4-uӠ(J"rO,B`9ޮY*魣…8ۖ$KqBŲ\؝*,#cTQCcȫ?cIc^)M1f(x8 4m Ut;9MY a아HÈ('Jk(b\.mŷ߿41'"ꦦ,rDeSPH%nL')|BjO Z }+?8@jޡjAߵTUKg iEB((fvc:Daaۃ(8#&8CU59U#FHIG` !AyaApXIe M3ڶR#Gubm|J54דRZءߒO8 3__E?h;\8~t`I,=nWR7>dBJbuLJG^_.hQ7 Ma$qbMOke>IZ ٌ掺9uP0jݠ ~zt=#}bLT|ưy1 LN4Ø$!B&G3<'=ķ T?MyuCêjзiP'.8 +5aжBH$$IӉt 5Nd Y6eE0P\q}kz%,(!Jd 6RS3%"3~'"A`lnX2}X{;DբEi˖U21LU&x4Zl|&-abw3##7+w|uig 6OD$J)S^DX3]kn6t\v9kr؎L&X,Y/Ě[M47 >mgB̌ۯn6 )\Ŵ&eiY1|"dqi$ںmYJy-7o0Nta ޾ǟ>R;v~$94N>q{{K^ƠUD ·v{hE4NCjcb8NQ?{Vmq0E$ǀa9(BR8kJ};:;x<[B6P1I~pƸq$5id mR5=_cIo25X*sZ^yf腆sdxT59->~m*z〫5Ϻ%@WƱ447˞37-3?|xۯn03~fy{feqչ G9 1iY1Q͚<~O]7(mH][7]OBlvZ}"#'Hb^3KӴ/EמG?g軖vK/J4c*'w]%D;GRM'$zuU3dmu0 ˿;隆-<,xzBU??K@YvOvR4m[J9NlA~w`Zrf!rZfN?wlyBhiJ2>+f0tEflX >Dhbds`y//\#hٓlY4gN)Θ'2i1zMj_~e?#٩Sy/m]Sr {AR~chjno|EشY&%R؇aDk7?@zj^?⪚軎y8vR_j1V}9kڮc=U7_qϛ{ljH(\b=IQD[d@H`(k L6$:o߿o{)dYR{[K'/Bҍ/k9+Yw2Hl1g_ `2 2Z@GK3 IB%;vHII}-wT%:5MCe (3@R[7-?}x Z)qM'NJm߼}oߡM0OL4r85ͧ*`c,i"3LrQ߮4KQ,aY.?b9'\ՐDIW42ϏOK|Hԕ]ZQنOOLӟ=<1ѯ>Br*MDy1Z<յ>}C0T8ж5W?a")MV ӄAx-=t.{ A筳#UUԎ{|h]M2K)BnjjkZCfE2u45. Dm_'m#e4$T뜳v[К-Hvunx O4 U|_=7z!gL6![Մ!dENpl$m0&'K?Wxx m"R!y}|arps9''fPYҚ+QEG9=*q'땈h[3nڐ]{S"e>ƙ#U]SWaJ aX "GL4S2b*10Gφ9SJi[6=uemTCPn3ω4h0I#ؿ38ڮE4yi)ኜEVݧe 9!zI8)M[;ǙdynXzU?r#ʺdlj"kؚ~բ/RndX0g<{U> /;C ~`l/n<*Gb ';jg*1#n,E~$ UNa,cr]܎t!$@@\RliҚE W|>j 1gc7buR+eO6bw %l]GrUbQ9K,Du9IL&TkP)wm9E c,7Fd~3~`86)Y/"r*5rgYzZS2 Αl4}<H酛;v//cJkv-Yi8XTuG]Ap{2xspig6)ƉǫmbevB!U*52U[A2Œ?R/ V8kh^|( y/kI3Tuz:Iyb=j=)&U,5!i,5W"|NUoN%777V4"jqDVRU54nh^kQʀV81` ew NK8Vk,&0&qk[Ms֠,C]7hlkτ k cHyJ3Q!zOSVn?/iD8i>b L͂OJ>6M >?Kg, @7Kd~]['@4M%}gFiafa=?o 4rss~$%v//XWIVy+/Ia8YKvr4Rw r1ȒQY{b0lUt#+i˔ q@K0yO 2Gp<%J)nk]p<}=?.$23n=?ެzN#DI)˅J@V]ےRMUY&͚iʳmqv&Z@+)5m%81x|R%kb1sgz,Xk8w誡[#!k*'3+3_GV,>%fGByuֲZXUr<k qb"e2b_~5V)b#n8 TiyȍM@Z5x¯2iێ҅>iq{n,i*%emmijj$DVBjZ!8uZ&)J)}V)IHyق6BE}PW4T9-u JKX)U(<iZ==QUNR^)*ƎARm|vFvݺ端gr8 EAVb3jGvk֡sd?qr2':k-!c+ K9h M)RbB@۴ʈʁy~<ϲc BQt8 Bm5 5"k)d֫4maB :" c:kb)MV )T(b+{`fg)fy!r;޽{wx 7m 9Is Z}RvM}+%Q !TK 1"D[ؚ8vejbZֆeKH4OrRX)g1 Grr*sG'ŢJ_$vAL Qz8uUZb2by9 D-JU2]6~G֋XWQ7D}_8ʑIu}MexN )Ub]cP:QMJiˤ.}i~}x]SY\uLs"#DMX,W*bLxQdGtvig%{hkslɌmm,(s1ERdV_`ܲu`i^Yն8$ZfzJLR (кjA2RJ1MoR>@Ng|5H&˘r)쐛EC"bHB)X6sB]1( mJ0<(3WEbfڦxY (mX-{rٱ?}G*m۠ )F)< gjaf033n lȢ!ꊻ%u] uURiZT ? Ajg&M$Dijq<4N2njںF,%Kvqo$vUi.R81-:pvZeˣ.#gΗ~:EgmSP' z)RW_|F^ga!&YV7E#][Of#u]4 u]<\f¹8.Ps\N)HE{x?ç-"U E\C]7mC]U4όޣ +͇J:r.L6] ;?>c: 9mlb~~oNYXjzc9o&q*~8?~@V;N3y0 |zް#MYw\tʗerдڳ<+ .djgAJ+m1bEH{7*c[evl`˟7o睏q[n1ǧ-9vEIˋ<cdY28m6|,宼Tv.f* i*M_eU5,,ZTUDa*'^\0 z.,F+8ҷ5SNx|[yi2C!kq_˾c'ٹqi*-c#?rъwoQE^oP%nM%mX]Y\c ?~|]H]92J}EFTAus`Ե5{4!ɲ)gKx\r)M%cղc<) pz}_is2F3VCx˘Y#w%/,=OJ aXY#A0&n q .k{4]Wrtmu”q Q✳0+bçŝkB)%/xSHM]qFѬNK C:;|Vq21T5WF*͢8Ɇuβj*BHRSu"EѦ9qhu ۇ[|ᑶY/cBO/|>U1aj"Z5:,f סկoOc#WO9N.!H tME&&6 :Kc-_FP+JNty!]?Y$ L)ѵ 8kp2y_ʈDwuNI{S$)Q"9gq>~O8k͝t.pG_^t>)S @J墣-]pjgfs ?EƄ`᳞i94B(}rPN^[ _1F- "TZ]a**5ƤC_h9>0#sQb ^|Xar^ V{euv+#]'`Iϱ+if˸zY~ef8ͤj{qUu*t^9c1ŴO黽&Nxג }~6Ĝ.*/tn*%UUe)meQ+E b˖)JG :K՟s1}5[w @EEܠŷ _dq%7TSj`cdO L`Dp%Llw"g%충g"w8?YG`YX"`]0Dܹ's&A ێR8ɑp-Qi֙30YTz^'星#UNbSf/31ۈ.4V2B'(^v_<3cW9ՂʹqN^D1/R03/)IxO 4SNB"O3/RJQ\c JgC^:嵺0TF~1"x. A4 7cq g~,*#pi3U'ROI˘2Q=/ >if+&c|zPf&HrwSȰ`[WwwxwqZ<}OZ##QotnqnDbf^D4;Al/,YDԛ2Q32e+;0xlXEs)w/(Q9Ij@,g!"_{Y "b0z}iܘHΣ/1'bfW:81\Ad+rMܖFDǂ`v@2`}8h֔Ehoaр%v2K),B8Rq{s}ܶ1e#t83Ǝ$"dw>t~ذNDr[W'Վ#gZ91XG)pU#>l0T}6wWqbW9 "}0^X% pP__#g8XC ]'dM8xAQՋk`]~qWf$%nJue}/Gki!R?x˗ǻn#\׹&{wq^dD̎^jD?+9Ӯ`V[ض tDDjE,D=a8cw5$!"aM)̍{-K&pf hg&aNʄPEӆȲQ۲؃{ou]a^0S/9ueD˒a8W8c㾔\oQϯ/^k jfvL1 @kg1Fea,v '&ܝL*DA8diLf.D \msFW&{pal8~vD8J]ѴSk0$ dL,bav0գ呙3sXf󞝀kU ?wy;މ5t\)] Lr[#?=DF9LdRU7:v+ȸ1a"qaWG7 g9S\ii.00(glYC{b6"һۺˈ逓bHl$JfDhELɡ9gw[;u]S '21su6baq$"ۍч$"GT1ԉa?s~]aa٬jDDF0x ݵwgR3sg"s=ڙ5,-"Dp0FϷ "A6Hx]9{Gjfoے(;H-϶xWV  Rcj'Ӻ'H"-R1` rJs&,|۲,l 0+AFDa)^}?PUuKJeq0N}ىʶnQ! +6'ɻ],a.N8_><>,E,e.7ѰwS/>PLܧ'B m0|e fynWRvƤdhhQfƒ/\I4}x+?2 Xw@+"3s919s$ DDtuXX83L麓>jd3LgdgVn/gcf)`vx;ex(fޟ_1ԕI\.{GxI"M#~ƅk9_N '8s}omwJ|/vEv&];mLf&^W"..UwKq?~m[\[]nP8$1ϒCná1u+^J-|j`M,K7W9™ pBl?^wueVU'2S3;RGP)K ZpPN'zFDDy]0"lKISѝ䭵,V/\k,^J&"3'g}__ﭵ2Af"c[?sl>@n wcP;v7u\JDnp3gI n”K?> s$N:eQGXE#D gw뽷>އF(=P#a_P;jSs!fUwL9V6IaOy^24 j9^sLSJ,n U*iA bf@c\XYH'ߘO|N,3 @|:2sweVn1@NFL a8Wf1Skb剅f|ǜRFhd39vwkmp9Z{"053%bcWS7\(6̔㨭njZrJB{=}"r(ȅIp+pWΑKZ[$=_+'II#5Ý5UVJvG,o)MM aƺ$RJ)Dk1TuDzVu:!yjd9kY05waafbN)BET˲Pl f'0;oo۶1}M{?H}Gz*4>'tFoKQ )8Ή{J)'Jg1D(9eYN3aFBP6'8n7as[Rbs#Z$YធR0aV5CMSsnJP ?޴w7KYgEMjLijk)\֔CYJ 4籭%"~{}=jKz98 'q8fr̽>r:jp=Q"NvU 4t4/ό 7tG4̘ .aάD\RN~Dg1qDKLn10\'/8(3j(%7‰}>$-"%H%pyqa+z;elD< Id^zgf1Y8P2׹+R~f_Klq:4Y5&R<9vU`5ny UɈTdn4| OSfs_WY\B_.3|)E݆rS'W%yYmj뺂h6m`dRL+,,Ľ>2lg"dn5,]ʥ^£8gt`ɋ Kۏ ⼔n`w?uỶÍ}°0q]z65ޏU`,몪c0/|K$om]J)˲8xV֪N%mD`6*,c޳Yۏu]iYD,0"6kT-Rp:+'fܻ:]k̴5aQܙY-||7Ϊ.98v_h4W`G; @{^OQ`,D2+DDw3+L6bآBDOYjvw;EH9uD|Qgq>lDF >F`W}Ȅs^6Qy{1ɰPff87za⚵0k#ovMД3ཝ r]W|>Ɂq7Kaf*"}4'ޠK:K.E\zS:hs$ PL0 esp #Hf۲8Z%XU5#v J7#'f`R u{m,mL NJ~-G98˺j.^0W"UhX3`r5YݏJ~n@ϧ,kl ."d^E'Lܝ@n6`F7 i8Nʐ04e[՘CQi^RJ8zjM 0nܺq w?~uRE5^n7%wo[4icLn[`F)"u?{RH ?#JXu6='lQZMp6yE%@Ɖ2쓏0fLSt"E;ZfvmDԻQś E*n}7q{mOWKi\#NYh&")a|(` PI]cpegJ4GQ< _D׫5 3tR(Ny }RwcC$JRzW@#6PLј¬D#_< F n5>ra>G!"Z 0we8:a)"־Qɦ#-teE뺚Ydձ";k CG<[s##1yry4KWcq"191FڲʒKq"Ug& :`cݞG#E8QcR\2Hbfm1,D( "bɢ3vn{E 2AKGHD^nKY^/0MtU3;gbVSFěx +NoJDj|B,ƉOFĉ[#"7rwIo<&3^`j]ḊpfU\O1x" vj]dt )%~$I|7m؊qPm\'NV"}v'p0=&9/L40I̅!dq|[?^`]Ԕ "or' +40"IDDDLgAϧt<}a "u;%V"YU#ضŽp7s{tpC-=!bF=.[~@Xyٶqvc@zՇ#F~h-PJj=܈hR*f|-o @m?kXW;R*c&W' K)dD\D$.q-b>4βJ|J~"]GJ8ɷ~;O'0mIu!Jee>ONj`d3ޓ'K.Z[smJTFZ D N)JeY,6zuꊪ|[Jke?6l1/)a=K&Ffs)9uYeY,%3.{G!~ƀr!9ev80pOٕv 1zpe<(̌D@pS3 d1x}yII$'=O"1j!Rcȹ f!zŜRZJ:q>RgJ4W03sH&|ンOzGu^IX$"ffQ>mZU$)X$h'l-<^xN,s>tBBQY%xz(/֟ܝX_0‹\7WJL< >dP [1Zo6<9Yo-`z)ABo? KJ) R f>50=1R2%eSy܆jY}>?|3JYmR)a]A>Sg!6j͒@#0 oI1Qe]T ?x>zNGVEDSB)'!:0%818#1F | ,- >}RF7Uo:tn;0lۺۺm &[S{k-oG;eB0tO^_P<-]UoG5@hkѺ80U]ED8T]E@b~GT̢*Iyo g_Œ q"u9 ˱[q,/p"!eI~G"# 8f!nIuY8}[EBfn}'"`Dž݃83/h[Owda̾<Ю# uľ4?+}b<'W|b/O3r.wqㄏ,.U`5,G#;$w|2hXUgafZ&hpv 4\~Lj5| Y&,I3ٕH# |Ϫf13<$%o|]bVrD~]TQm[jQ`D$>KN}? O5 (my)wͭ{".۪ l=n",k*hպ3q.G} 8$z]/uYwۍ|!tc = E>"R(̍k㚿򾢧Xs-Rr}^">D3s5]HJi>Y|LY_v߃`_1mO•D,G؍.JwȈoW  ü0Z^k"`Ε>#sr~g"&o$%D_qm; N@ܣjx'EMw ~I-P~D]7͆Uy`6_=f:0bOFZRDi-+=鍮Ʒ*2˅9%_.#, l6IVw"*i 6 ׮{u-"rnZ׭D-gbؤ᪾dX1-N( 0T0m?=a6UeJ`R]Gl1ۂye < fokv0s}?nǿqH 17r')NJn݁fF1 QyI n:RO_jSf%ڈH܁( {m]hE痻̳ t;/ܭj4=15zzSW%ԇ>#)'z>8'AZ/%3pOlaY)щh-ş_nwfxec&c:N Ѥ3s@4яʏ}OjL%%,\:1#? QD \`*9.yP8f }!2Laj"gt3g-k C5Tefu$"GZw\g13gEDdQn]BD" ZS*B#V`iͬ,_^D@b90"23f'@+eEq۶J$NbJ|ө}+>0 :.Y҇ j -M+JǽGG1ٶ0v_i^th3o>#ɹu͒ &fYxQP79['ળ{ΆP65-bS8mcGU{[ו ʜW m a"icp hc(ɒ6zT FiY03E>G^gUcf `Nӆm[ė JH"u]?~U7}}1e!9grJ)cR<dwckCRNXotFl8Vk}Sx Nz~"FR~leQu]~<:Lkm=!r~Iw;,ə蕏K¬}1D?-}5䄜 %Of23Ga<@&"R $b^vfvƯ_'vRHϣs3-6̣rŭ1ffn=,>&J~VMҡ#2`47] U'>l4tNp $fWRx؏Rn_uŇc7U.ɧ֓w 3RN.vy8+vy6pݵw1xƋ8gr4)@EI۸+m2NX]l">* )8o)Jh>¶!<Q$).'d 70ѺwFZN*nduG1z]<|¬c&l@݈Նw}Ͽ),yuQ7"z۟F3lqNյc2\8 FYe]>IJ֎qus᷶;`MvZ1)<%FD(˒rvO"e97|(Uӡm>m})`&G 8}*ăZ;_i; 5l(" ǂŀPd$"UyIX+Szo;pfVIhj%/7Щl3ӄ9Tp&0CuoIM%@9m%`,ҝ$D)>T fad/QSgY "_6<}Ui#pGURǺ-z<"ͼ7-f1s= Np aVl)eę,n┸ꖈ(ZšOyLN&BJ}sG73n>΅=" 8t=KҺX0hw wR$ L3"&Ruw;F[+tZz'"&z{:ܩ>|1z8b u]8TFHR˗ovK)e1BݨoC$9W\߸˄Eg"': XS՜RbO/GIBBi6Em!hQJֿX+0cmsmI$C ~?uijA0hc'9KJ%6f.H)їe ucDډ2H$x~qe y&Ǖ3L觡Q\r^kk#d&XjSJΤc(%3~|Z`>L9K qG'Ŵ{YuUuy9<3z~bf)ѸGaffS8LEєv Bگ,Qs\z*9ATdSǴq~"bxh}AP4? TPϟ>)PI S\LK%@%_FJDxF[rCi2?wyzX 9.1{tB\Cظ8r|zmWOH+nEբ}DZdBL{=ܝ)ޑ(ce0"C#}]Mj\M8N c%*]^Bh=ݔ1^.~¼yZkݵ?BxGA\Tj??ýH/|RJK>2趖 9ނ%㹃lYs4Z o|>u}axV-ԛ̞ɂI{~򅄛8Ti7zZ'G朤DW` pq,֗q*!%q)\a۶{JGm 6Nnhm6},kf0T8ؑw e"OjfCU>qw/KaBp) B'%J wdf,XUw\)s}݊iDF4aVѰLS`nBΧT8ȂVHD }+R?#+ɭE  jFnf7,Sk/nJ| ;!SʉAp^໇ 5AS|6o:f> ӥGl ݇C`Q(i Ql68rbTPHTkRJc%/t"@uJDT{K^qQjMd :E{cɁ$"6;HSvC)G;ę9$%qؖmyrށfs/l^u'Y Zb)IإRr]6겼rJ6R߄ӱLu)!cenNH퇷/~er%xtqp^r ,ɳZs9\ zShRGOBPmꗏiPItcf#ȗ|XcY o~njޞQG. %aGᜓ m$>dcT)Iklܥs㧒e}4ZtBaA<جH˃GށoոRBlL 31s/p?%$b\ t Ğp&">va#տ>ɬR_+Btwu[z ta3a!LSs)0Mzŭ5μO :3!^?A:"b=+RzygwiٱIdoo=+WήJfL;bu +*%0pDZrݧh7U{$MGWgS:\{ '[5leYۋ &v?u÷~d/?|;W$f3!"f.cxJvncT2#-eߨ-p3YiYsyYwbdZs?#cgmvHP[.JI?Rw}r)8臷uۜhF$ +u#s @]K) f _߮`vdI$pw?77l'r NhN8Na,\vPpT̗ADH,<>+PdrBoyf'ۺc&DC*M"|.%*&S'g}Wթ/P1 Bt9ȘryB^,!u̼?+0&k|2F"aޞ08Ht ԓ ` j'<BdDbU cjtjDma?R!rÝW:}@+"3rkk-\@.' Cr*"$w(Qը]IK3 #ganfe`kw&οsmnW{|@kG&(D,TnkT,}߳ٺ.!3sIj徑N93', ̪REH4(Q R%g_պ/?ᆵG70K8;(ؓ RFqB Р@ D)eY>0h!Wpݖq[o˲1HC 2῏i۶,˲Џ#bRJq${n7NYjL8Ut@j'`JĮff68Zn _R66'ivؾeYF~z%aL?9$沮$,Pӗ θc(+ ;<<Ƿ~1̘)}r?>v@w뮨3Wʭ̢̳Y,J)]1DrbÁjd f~{{œ;Sң23{o|e$9F)~S`NI8VYId;dӻi~^9Gf6;jQ,'\!7|>;[klХ5 zҩypK\B]Y*z$WDW3^/< N~v^g@8NHnh5-|̨.D1inRd ZoBUD#r k!N*"˲؉ֶ')6Fo]b>c,IXrfP4OJc#k)[y{{o[ Ė?S朘y)ͯ'/PJF=1 ;qמJftMKJgK.-3޷*51J)֚,$m1s% Nm"(A&Sm)R5D"cDDj ';<b.E`dM~ϯ%[',P/eY:<1 : za!*"uq챭_ux u7Oߤ>ķ.ˈل>>Zj?g&p{3+'5bX*WQ[N Q73Hʺ%vMk.G˺Y.Ar :zdF?}l[k= s&f=) Cvj,=cwǯVlL.ͥL|mw~eF (Xτ☥?ǹ,˾u(PA˒Bl|S3/:OU]ohݷ 1mqH4y 2g8RJh$^nf!d5Bfg^씫j=`r:|Zd0%:Bj6Rh8vKI~kL/mpYe]@8s۵.g.z;ƩEؙ)ApQQSJBv9lbD qdCv&N'{BrZ$qzm_?Wsú5ܷeY@Z -{1 }}V0sNP~17e|VJv'" Yo׿? )_78<Ú8utvD@o:kky)GQ6;&VJgHN6eZ)-%Dt֡Z5dp0/F @O"W"& bBK nqTq[c|̼d!6ae]<%1 Zxcfk&"N`{2W,K6Ӝj`C <8\O>{ }\=s,w? Itca"*Ih-?>b %N 9KN,`W,kyYֵQ1\x c(,̶m ᡗmcf=1tbcP $V syw|@C[-Hd^ I;2 [J?n |:M]NY ;RJ;'HeM숒Yt={gEohz]p%89)4|iO)6=Dl<3>.p ` )+QYD6:yɏДXChC.<'IDs{]M|Q,n ۹?Lfj۶Y#8~F3*>5]3sSHc*cśuǰ@S*,~vP`g:C/]_ۿXr~hݗKp*E^{?ݖk.BdDZ,K*qHNpB"mJ ,֚,pfHN>}Q┉t48dsaCobf,גR|?Dl)"0Cmɓ4NzN0pMmmYeo 1NJnLR^EXf7R:٨99| 7fhU} Sut@E{Zjnh S 1zK͒pP~Dprm,IRR30^ʒeZIc]aZr ٘C,ooo_~)1c3X䖳x*YT)ۖKCd*|֖sv`֜X͗/g{s>RJ, w3OjjK^8L"ޠcsu:gfE9{Y@0KmHeK)Y [0Tԛr{ ,H GL6Z r3DH9KJ6Ѹ6G4!Nn0XLRF\P* af0S 9N"f, wp4Nwm]ة,Utڎ) q?C؜G fqB:F5ƈKEܙwaYR^Y b<àO$9횓;-誺mAdnD^r¥qԜ ]Rr^އ%|Änv5Ȓǯ}͗Qh#%Qr -n'Q7KJ\rMB҇H8Ra9z yNL)3PE/Ϸ0{k^mO_?֣ކY=FKJ1^RVzjf lb7RRs)9f[~ׂ+D1D׵CU{nZdS%86zgo,Yzp,?&S/r*+J(ՒtvŖ±:zJiB@eLH23ILn.Z4Q($朒Z[%Qfnj?A @5GThc Qo r_*9MDbJQbu]5,"IRc!>gjN ɍjRJ Ib8$l\G7u'ಝGOs =(\Gm4wjɽ3˹i*\.axޔRNG{xj N)x(TN۶-f"\JDcc!7;+ ۺ^qM9^IC}ԕ]l";ª朣 m1E3Yv9pO RJ+\ZͦqvW\29Ks^źs/S 3\8YmQq"q9'ʅÉF(sIPh?_>ɷw_O^^?}Gi'4k%dz{{k>z㥸A~c 5'RտۿO~jD1]޾0z+9~oo벴[y)'RsΉ%|ݶׯ_I⬳1)Lc(/Ot "">SMRcA ] !"݁Im8` 9 cWIAn𜒃X{]EhcO Qo[.9ceYPrZsa$i&u)̬ _J)K޶7F׉ L~_{mfv/Vp\С'Uݶ$968qZͼcs~O":1g];ٺnoD?}+,L~Tg)zh>G\ź XJN{=Z{m}FɄ`SJ  j%$r 3H9BøA"wD5'eKv<F@b0ѷ5KRq?ל~|}K{. ahF`Hk-b1:୎1Tͱ4isrsfb"9뺦a"$1C 3-e9;4% xV:uiPɋ0D!C23S.)%a!I@ $A(rokܦ;Sv}m[؟}tExs8^^^9P%fUf2|sSӉi'0Lw췿=G?v핁ϟ^>}_ܶ~琩WL,Ąm]3 ۨMBǷw?D޺05zTa"ϾKy`M-L؏h`":|63H@9L]F\Zt1t Zr>-޾%" n."9r7%g0Aec^9-Zj tFi✗ynUXBb)aOؑ$f˚Km+nۺ.RZe nL\5 $|>,̀ǩ,0[Nˡ"x>O,-oQQ= hLa~"O0p0?wr<#ף́,K9"Č(q2vhNb ŜS.(6bI)zʒC 9k볥,%9R0qt]\&~ot{D>HYG{D3Ib!&F)EU ?_ 6}/F՚R"f I$ØS)e6>x?3~J~}}g?ůʐ 2#k{;~'"{}[ P;2Gf̢<.16Gښ}Z?_2s8RfɹO 3>krJmMJYEW}Ji!,˶?ESFD[)1l[W0۶Z~[m[S},{䜗NL$,3e/s؁c[of8Dn%Ȳc;!bԁٟ{=x{_Nf~>1F.p8rHPc"{8}r H[f9$ܠj8,r&Dw_E xdf9!I>%S"ZpԪ:hsըXxLӲ,!ZM^'SThrX+ iGp6x\ruYEmυߘNf=DLDOc|V"zP%YpԨnHWnVJ!d'Zż|h`:YX%s:3 .L'_[kmtzm9ۈ Y",) e]nRĪTrϵv0HHo% jN{aYrS=->x+Ky)ZDKdEHIÏoxpNl$Ej=@$l56#mrkYR}73x{BՈc$1߁Z}4%f> 3"v1\U:u5ᇯB?Sj>Seso1PGB ДSJ̡;͢{ZFx%&9DJ澮RHےuicnK$TJJ JBI0|]go.k.9k̾R|zy]lpVǛ%y}hCk1"6zf[_rf7.ktzt4a Mǘ0m6 8dv/+|ȓ;tS'vՑ%M?FTԩrN 3bR,IzW3#EbCufmv\$OG 8B\"g%""FTJΑ9͎brC`Y!D9l  nkY$`>N1~2j2Op钱cf4*RR5 ufiý"8ꉣYnLBTr*9 / yTO+z\E/#Pݏ.W؟%ZxF6:th˒H?կ$>N__2ƽp D n_s%HR"vf']'*փSJy)/?}v_mYmےu]&_Я_~˟;KSTpw'9ta2L g߃ ǩkK0 1sWRB-#cYW6|YD~2>5  ͊h-=P~DGG+Myh+@@.Y7'p"ItLʉ=xqͯ'gLrNi"HN#3XϾIWo/|Gݛ ל0K Ru3֖ĤO @>59Q*C2$ N!;$ 6FωڕXS0Ne[%_2۳^c^MZ,%9L:\9Ƕ-X6Zf(dI 9)zeaRrDYsbS*vUB`T5:C*?NW}:sL Ik("5CN:̬|}4Ib9~=ğu\[HFJD\b L|_^^DDS2T{܏넩p3'!i2Z!jꘪW+` f`a}OiS41nb&rJ&>KQkoYRHʼnj1֔E)$RLnRDvqN} g2P~kp7bzDŻ{>&͔޾朜 1 j}ԗ5HQJC[k|%FOzav$V 4TSJs4)L [TgdmHq$l6Zk!i$!NTB矏ZuvٜEDj'ѹڥhU 䀜4'nm(1OOZR."W Lfs{ƌ7sOݍ 5IɣeOIш$I2#$%fI"):}"ŤVsѓTQJy}}]퍀R\-1*hJ:RFcx}}]~[fnaS)N0w>zs8zCڒѺ̞i) ǒ8=^}~2,rV?ܜH}?#Pޏ w}\ŨFcmJQs64g!yH^SjF>Jp`Ùmra3}oϙf,<|>q^n7wܶ7'<Ƕϟem7RGBN^SQe%=8%\ԧ5.m{*3>mLhe:W.Rg㡔M͒`? Uf=OH<A$@>yDԆ } w]B,r(Z 8+ ;4,~5FC+4{(37JbFS*9V/x{ |Z˚UvGu~{D.F8?uΧjY#^DT[7u+"|HR s4><{J<# bRiA2gŭ^>bOj8@D’U V5r"lnHڛ?+L9x>lݜe[ۺۭؗ I̙,rN3Ez)ɰa6r.$"g ac-.9ZdH hkזsΒTwmts:ϟ'ӷ>}}РJ' i [ܶukEz{yLrbJS;@9BZQQOsc^;qZוKE@'DשAq%>_CXSJscU: <0GDS*}Y)E;|kWգ؈,&‡ Sn yHF@}ێ9)P<o149v4:*HcRP3,vuT{_;uNQk`pNwdQnro|ْ ֊1.˒__U-`Y94Ï= |psJ?g2ҊWk5 ŝ8="쳼jt 3򰐣% H48`F`B89ǿ9fMi9&"_$(sb/!;TF3#?S/}3RjqU"z<޶m@r }>vSwZ0s|8zk%|/?_ݿ֧b֔Gc-%.{j2cnw0%ӄBA5EJ)Gm1 qLH䛙ffy~ؖU%7P>)q1}W7z}]J`+X8ɒ7fq_‡S&Aw/Շ:TBybgGu4`u&HJC;۟|{۶mBn4眄] njªׯc0%pf t2 3sޖe-KN6x<1?; "jmt)+w~zud6zEX֭as`*\ۍthZk̴,+IJ9%IBwHZc?=L1.C (bPRJηmej^TmJQר""m+3Pb[m]Ѧ8*SytX唢+A[rYR[q?'d9!l|r6Y1SdC44p&tZЛ 8خ8zZkYJJc},|mA yaC$v5|)K)Hbojo@%y=T `[,Go@rVwش3.E\juِF`mYTe":8:H/Y{#u]jJpt s u7#Zl!L\#xuL9 S [hu۶m7D%gw_<ùsCw0$h7W7ЫKBUI69eR*)3 1WFDf/ej{4G`ctLH%x8?>>< ,rswWG[fK?p5 Xh尖̬1{0h3@,m^C z6~u]\!dfo}q[8WZb 腱ةC'BSg&~̏7@^6(obOSp(p4I_?<=Nڈ }M<5$8nil=BD)Ȧi J7Mݾ^2[}s0ó;#>W7оQfb@mf s@RDFz"ZHEBO.kzVNL3{:ZqgcZ$;_H)fTG > ";\ , rW֪U<7Ŝ[~G&>oV+z$Bl@v) ;0` MD^_@`Fmsݟw M"O(% )#+} PF'cS@彬kY3uSeYkݳI}c]׈DYuu[W} ~ ~"@2@ݟe]OIqY$!8c9#R" udpkbh`2?]\@8=۶!Bwc۶`:, txb9sc+l_su?ƛWoT@q^xOg'\{-I1r6RSTP$)6١{nˉ ha/[ N$~}^dD)SCTK8o/8x<ͽVSh}LٝG`frb?vVG*95ڰeI$щs18! "⎁d L]!h:agY1&,dnqJ=?UmYl/wկ'IKﵵ^1~~J_^n 49a`<"r49gP;sYy#J<p;ׅcAGRk L\(j@_0(g2Ko{?eP bBC[TKcnZh]W6SpaJvۂu;8?G]z`Q@eІI1k#*lfm^ q%`"9@ĺ۶ \뚋T~U"h=pY |>oooחy!Ea(r^~>|D {vB> ߁08-A Yr?qGkF%3Bbp No^;+r0R~{4Fl"?)W#bWң^7ޢL3Is8unxG g4T8ZX&5t9 O({~nF'Ke_?s%D'ǯWE2m)G0ccץ1BAԾ]k"wohkgQ.$4d1Es<㄰0k6_5*T=e۶:TB{/i!jAF"8}t:!:33:_k+)n7'8꺮L7p3P0Aj0WEvU7"Mu[pQ8ԭn> ͹'yG*HvJp˾}KѽQ@(~k<bY^璉Ñq58|Hl]W4n > 9HmEb8$ m,+aI̖"S}Yd>iM^hΓ ^k]BKa;'kplbtRY6bb@6Lpz8sWg й=n7P{e3S$3tH)?t/NpXԟp"ww"z<,b_9aD dY1b$Q53c3A8ffd؅4\q*Y&N D5R t7MD}#B]1d.,SAb_q"$O'?*Qo[ ^fp0i!'Bwhcf4Zw*% sݺǀd'шxֺHrw3̘ay䐃) !yY)L|ԣ/: /X{0San3Zqa$UEyFT(\SJueMLi)D~qY$bөGYyѳiqd$(5"?WDHF<&W@ĜKbZf77 : R9To3FNUSJ~i7lpOIDx])K.=jĜKvuDѣBh6uB{k5  (91GE)qwdfV=[oU:Ԣrwb{|<8Hj+N0?w}gf@.%DB0; }-[MHk|s)QX(ҝb3:"R7s( 6&#t ?aIz\eY˒ChQӉ s.^hYc">t 噺eL8%)ig9 JN9&WG~8iCu.Re'-|isG, Dm?|õb]R|Я/?KfnKtQ"r6"#&N+:pRIA+| ,>\ a.L'933J>i$y<3\$Iz@DhIkw~'vZS$Hjc)%F3Ix ZpAZc??w@Fy.94=57׮^?^v V#1SΌ1~;a>;IXD%O,ϣ] ؙ!54f4-#n%W+Qxy234w: @ 2eZJ'hK@ւ ee⮶+q$)q]PYOK)3Kx!!"N ZxSsZiXZ+%;"ܐؼDY]糔 8 ."rWDХ%U#r'0CE_q0Ju|_p*C#ўy}Ih\rZՆ8u]ќI D>F Ylץ\6H 〪xj:IZnV@<ѨigәFMZ5@>M;1T5Bj6NjJ)0}HzdU2mŴY""PtҐErDr#A(zkAj1 LǾ?km񷉨f}MBnc(E 0=rA:[k\݇$VFT|\ NEDZժe=̙OutbAf 9y&B䡳")7c5"h2wmAOy)}}뒅 鮦s"4Ԁΐ]u\yL/aTKq?cZ[4dm[b)\9p@w F80Ъ8Z]JA+f+2 eHRrf6I .Y)SJ"]X!); $0M0`!DD7#wf&"Gk-L+)AFsnXU$ ueψ<@>1ZJUݑ㋙"ak_^_$' -`tuįPVB,Iڬ鴑Qݏ#ymD"1SNB3}Ym]=6]seYh#pZ{D%>;B7lAxD$u^}kmux>(PTtGd`]=1 XF ;S“~u1 o0v6qFki]{i6˺!gCSUpq j6FJDSGfݾpN%epȉ:sX.Ǻv+|E8[JB`^^^L{& ADpZ$ናUP"W=.aw()%VuҤOW2$ȫY wG1c,sK_ߨ(蔑O@px>u܅£d_FpDxF2#!/ke-qT#)somO"3f 'KDlcDSu?oK~~׿:DDEKӚMxM[X),wI)9 N$* Лs`-NmDW!X#x3 ikl۱^ |-!hcq_~<y)^Һ(ж."N  k3D3{1+?MLaYȧ8i.q8?T5F3,0)'Ab'I['=7`f`?9huD`Ŭ,n>T}fڶ  DĮc?eY/h|2I|Y#A8C#,3EDvdtFg)P~"11RH){2taSJ)EMIIRnL/iWseR,n1H*9R=ɿ]BDN4z1 !88"')+!8X /V+NأMu`vmm!!BVB>U7w:s 7tzu!bwcxo`Ý ͬ3Q2S3ď, 9`Ƹp:C@=I+БR2SaJ%1KJi)em΃С:‰ʟ#nm")Bdbes`c3Ƕ&6U1^#?#N%,k<"?{]4 ͡AyEZkr)] 1܃P6A󛻮@ڻMamʹFqֈ%#p}zޙDy j0a\Uܮ BNw;ܘ91L2 #qAxO {)ߘ?>>~RUBE2kK))eG07^qf`?Q8L=`I%Ԋ {z1=Tdp&Z-f&,unf۶2RI%fD瓈1u@8ԐHk`gV[b 6փ{׮$~0 's0؟twL,Q0HGqGv] <{m,Nt#'m]RJf fERfF`rJ{׮.%9?;J) 0|0R==%Aد#"0H}I?L>{hU Zo} difx(0#_joۺ֠C݌1` w`!f\5 zD#3އkmM;I33@:cI1du S/ٶeY<Ƙ(gƧc0cZ}:AK!07b)Ctsju麮F#|3s.91U!lpEmO$Z@'&A`f$a f)IJ%q@,Œ(!mKy^Aߪΰ/}?RsXH"$S& HNP(>Y_Jf&EDmnQyFL^=t^s M5G tbF6Dh`wl?!zU lTNL{Iueit#H:/߿ V伬 3[$@<&Ӛ'%aI0YM؉E?9y9K>T t^խזJnwb-ROM.1F\{Z8aw!v 0v a˲8Bz} GNH"b>B_$˫"FMt~PB0_![ R)Ø=3z=D"k,x:nR'C#wI )99 ~-axJI  Nx&sa W>w<+F%%Fp֚ #&M`bx<+X5&YC׌|h[`I4J 3ހnljA|`1k_ F$˙{_d݊ܦ:hߘz毶%*yـh}.K۲i{Fg*ZۺO X3 KI~[7i v5E.\n6~~GȹQk)tg YHae}ձjlܘ#NIݜ$}M͛!b7u2i8N~d1D$S`+!(p᫸41 囏$#]<3F]ȳ`Zk`Y1" Dbk 16pY$IdHDž\;W7DfH[ZF"p$j;BY LUD$X~̊qBg q 9=CmD?ǿgABHvGVL# 81kp,<| RYOb]7:Z_p#(DT֎eY ]d:,! [p f E:)]yɞ;ieY"#"C@pqC Xn[t9pM\Ҳ{3€aD,):ly $m?j3i3!bN)9W3Gk3g>Ƶ>1TixeY0Fa*xlAC8>yQ`(~evA35==4<}|9; 8ާ}Jg- 1>T5,"DAi&oZ[U(EV-.s?[Tm[%,@UȾxK)۲l]L wnB{A1u=07ݽڧdJ)%(D`Ykt }iDuu\H?1GA @gNC 10Oɝ0@T1Ԧ;_Nh!MxmUM՟Rr, !%xc"0.$ߴ63'i D)(SGk=Vr)Q)%_b[5BLgPfWZjއ140wsh{>~zV[?juh;P܁! EF405p7B"Ēg, H8THTn[0ܼ>kcLf&ϐd&,0q8>ں&g.ED(PB쮢b"(" {NWKjabZւqiO_6c]VỈhݶ?ZJ✳ObȞ305ȧ`m#:A=,o| H"7'w3q)6 Yox&+i4>qc=DV0d_Pr^W!!󹮫D%miF,pl !" Uy<.TGǾHP|2p9LA {tL hZG'D~,D(Ay,q@DLQ.{/eE6:"kUl3rD"nۊHLSP}p\'/ p$8;<Af8%2m!HhH$#.p5۶MϔȽl1/u0!"{xM@7pJ2tB' GȒ 05M֘g,;-RDn`nF|]^4OG>}H1&Ϛ K0JI [s, N>$BL7 \ '03A&'+kr.Mמ_H-Y♳h1mf~)U=4)k}?(BCQk F)pژ_<#/8=J8mဥ  8e>LO5P~ǿ?}<:>zbfq޴ι9ō8:E:EZ.=ZDb`'=Hz>;*0ッz:MD#QJIІ:nϒۧZww7j(Yd6RJa5©Ӕp%'ڪ s21De)Ђ"V#D'BZNbY:%sUG'k:^'r:4cn4&j q!btC@clxcYtp˲|9iHHxȵ‘r 7[^m‚Ist>7[ơ%*9n36Ȉkjkd#xfN9, rLVZGcT -k޶ܕ\,{Aۉ#.o>x :нsagreB4e1%g0Eq=yY=Ir} hջ7x$]{C$[/8zt$azUM<弾ZsYU52E0 G+d1 : 22ӺnG]r9fa2 GW_.K$z Dc HxG)MN|_Wuo<\-"sպP o`SDN6nl>Q0yܷJ>ZU?`δiQD2=]gcW% U10mqJ[o~:` b}9vHNmZDumR h x<%gf8){⪸~Q™%89JApt۶~,Kd9+r~06C///,"S~("]q{@MºO1=n,lމI;f9f̎13̞ϧ,414H,%eBtЅLIeh:bK߷ "bT1F>j`c mh@h8-kLXS!0cD~/׹F@UK ERptqæ_Rg Axi7ݗ`wBx0caSVn:\NoRg˅SsDx}zERJVc h|~9̔"JDl(zOox<[k^J:8%x)`tm}|^{s~;re37#b83ˤnAv8ih|>MW_~p1/ErAwSS=ޯ{2Έh;__޷m=b?w_K2 Ei*B0YU<^1Y -^@ /*48ܡB} 2|tM\[lJX$DHp{y3pUn h[k+"FY?G)gu;#fv_7w`)ކ!b ZAĸ,mӾz侽}z GP.cFȏY萗}C13câUs=\(H6ZkXK0[U4UZ0af=4KN%Kݻ pb9AyǗiF8BCPu 0Îߑ)焈}p0b50ڵpd $H,Ecޝym5@\5R2)vId"<1uFDn+q 8ʒ=9$dfV3T'/3s19 qs&#bYGYFL$d/xEŎ0fh&r*vBYk-%&e1su]!xuxL-VwSIJ_Uyk=܇x"9 cۈȇvSD'ahd`:?珣e]I$kGb"Ͻ ]8v&vGFDv(aDa'"S; )kP [N;u:YvA6Mj͌(B83y)EZ; PND__.nfIA벎1al SxD Yڏ֧6pTLd* 3RҒgkGE>O@ @ZT~ER2rvw'J{ED=utUBp2}ͱc8Tf6LGjuoܟB˲{F?v|1Q=sIOT Jh f0G2Ms6h-#Gm 2 ?3|{{Ǔ''| <[k)q h^Jb$鰏fCErw U!im]WC(Q-`t F3_?\bV~~fJ Wc#8ZfIu^Դu$ {߲lfX;)8GEe"&]!b"ehz<Ip.E\-g9L"`wUʼ%ՙSuo^^[f KBD_EO8^u.)#bv ̀etL1V$Hj1p:G+{Y6p"14TuC$YR1e"$BO9}&.^'f8Ƙau<<Û8OA 1$`_Ͷ;cZy :89P7kjkݏ!d|:࢛q7,F*_R;Pm)dv49YNL"Ѳ1Fv8˶;EVo zZ3)V(ʺN0A*N 7{;q@샲;c2]`۶k(쵧0q=/ $Qfmwu()bwDC _Pփh @ؘYO9汪i˲8a?D rBjuoi))1 uT ;:M.?r̯A 7~{y)GL u-#j/G|v^zDpͯ~ 38MX"O8?"՞gbapV}<8LVPN"Z;1Tʭ1BwK#pcjD#Ab81%'aNH .eHԡaǦ<)- DSE==#GtwCz|5f~OBLC73rp9r T}?h8#غ$ڱ_~Y2b"-K!`D@5іҖe[䗭Kmo"%bbJD)toe’ I"$7 z0 QLt$̄9$BȌ”u-I})Eh΄Q2a8#nk߶,YoǾwT1A[P3UI T0UĒ$$@U3BwT>]|D 3WiEs)q\{, uDYfr0 {ȓ8r"%gf굚ZNqo]3C90QN)u)%ϟϧN˹ganhD @Ё9xIX `n9/n8" |n*INRCu)mnےSX|||:\G;Zqa7L3C[ܭ0gp-ۺݷuyo/.uyݖ%uEO8θB{4uyJSHzDD<MRɄn:z#BafeYr$YD P0~ݿ{QP5V{7MIXD/ER ,bS뱮3 ǭKDLeLvYK b@ w_K ^b<-D'b uq0nu 97(I5WXchHJ A͌a6Ơ2v$Qq#cQaHR[eNfZ#55>4:F4PQr[(cIe",283/}BLq `B.þUo/͟34щlV:֥ 0d"&5ܢS|iTϕeq8?7sĢ [qՆBLj8eYg^LLLtf !}M>:3Z+"IƩ ?!ǁ1m8n6,K Q͒TH4G|r(\1z׋쪽w0]=}uV!o ʲ(}o79¾A{Q7ol ;:FJ8wF!ef5VO{(ERI" .uZRN\1"}٤ 3ݵ%PRb{cRJ<8|YKL.&6.'s&1.,QԠj{<`ZU{lHDDrI7c1N?x"8 V0q${ tӀ#oOm6 #CJh/|F!J씾:#Ciz"ONFt{/_~/sʜdyODjL7sfjOIx<S1I O4 !p *@u!!z=lD jL<0$,]PSnQ,{L_l\Jl:vuĚ|:C)WgQdE$KΦz۶R1Z 1}ۘ}ؠ1a$}`I!$&pyJ̀$1}tGNzah #7GyR/[Q$J l8sZ ¡#nAf1"=I$PƪjmGGk>~n71L;tiww#aF"яfðE|rۘ Mu[>ݷO^˶$KYb3FER:Qc4_E8\!,(sNPFhu$3 @L}ny 9K┘$uR-yɲaT78jk~6"!R` qiϧ m ΔEisMDk$ADL̵!:L6ƈ-s#ڙBP) .q-Ky㴍 H.)Fjm-l!S-/%G&D8|.o!\]af`,!*ze9#|aD]!# Fo~F'&0Q,0s@d#} #:6Zk[I0gBK"NStbMZ/)D% 'hD4/ ؼ&7 *_s`67Ss; 117ZQDB'-ܝpO=p2IgѾiab 飂0גߵ֟~R",LX㨵0FNvS/BhOa>N;#&I)z 9%a7 *:ӹ f)6vDZ =91?`'C.tR&4dRxxY>tDubᅮLLL@>\ ǾD$cZkF-"/%sz]OmuI !p3ZHTuN ?5oI#>wDLI- 1qs:WmE B`N49pZͯ|0"v1YJY(0n%my%1ު^[k*l˒D\\z/P5|<0䴔b94|mw?=̍BNϼ̸rLRj8>yn#Qg?"K !ƨvkVN21=jǿ fI N<3RM \c`Is_E8'N (D%š$# M9xӘЏZS?Q=fTa1,*pgy׮8t%ۺIJW໙>>]X2#z8@Ȳ 6dë} 3P 8"CAD"$lT-C喫sZ%'"aFxzb"50}Q?lDԡuYu'!#&5Xk=ZJRc4!t!ff>Fޚ0# 9 !;64hDFj!:XuS%n 9˶C3RpuK@hߥa,P`C='!Ssuj|>|0m߷{L2v>ݿ{}eyY\xO2Ǝf!$p'j c)̥.""0GsE#uQlHg6#ob"OCՆy&gL3kARIpfN, Koe͂C{k1k;fC{n罅uB5$rٰծ#R̔LڜWB=ЛzdQq.s[W lRkr"0{YѶ,ۺg`Apoc>T;1m*9TQFm5řE(,.y?t.4.ˆH?}y7{=D1JNi1Ԝ)/8f'0B)!LicU$E$g#lĆ=缬o/Oq*,CGı;x.bx<:z)Elɿ/or!"fs,%,6[km sRk\a9Ń[wcGU5@@UM*I)ft0!jPUj0S`f:,KH][3 '$IѤ? ,yWk ٭;:T܆Z[a 9R c1sE /LJ!] ,i{mV6pet[}[RJIr$sr{є)'_O6dNZV&ɈnAMDD:!6UJfCRW @$$6xniNݞFb\}@pp'pR 4 5ɂ*h\M12_=_#pĵB$rcu oFJIko]JN)3AIu505Unz`bI)!NZ( R"'7`6psx &H[)2:gdhf$Z)nӧ rћ<O3D$]=!gx<|;QʙZ~Hz]4?Y+oyݽ1ɤSgvG}H.2Q퇘}`F1rwD48B 01:܉B3x+⻫GiXm6%!3v"wHdDwwK m,˶nX$?>NGr[hD)8IH`B@`\{7@MLf7{25b>@Pďt}zŚ>uuqBx8Qav<~|~1>?$mYs9%S4Fw7 ?FY[US}Op {1y' acmfCo(n)2v )L+ 1pv!DjE$ g!d^Lfx&\ל{hu-۲z۶Q1ڶr̭1TiiPk Vis( 2hj1I1$U^--`\a>"Zt z 3 0foDz)%b:m{hzޖU$!tv%@؎z]$ Z33ݖâBwR.KLjBV#TU~F:Gj.B`Ca&L8㳙IJ,)ga#0K.?8tt/}swFc>n_uYy>{旭ϟ^-۶&%eafccHU@6AЂFco ͇RO~E<Μn@+Tec s4-LÈY Dt }SeB0%b&dtf̄}Z{`@%F:|]I#ֺ+B1P6!>9{]V$N1x~# Sh', zuf.Tz.)!O?; Ib'ŐH}Lל- p5p&&fa0;!  WDB :` }h%G G' q\RJ~FD<6gx~?/?#q@-me154I`fW <*(aOz!$p(~ӫ%gD 13G4hsaP"R7tFeFVHN7Aw q8|x4ASj$]S" OOgx[WC i-M˒&j!9?>Qv n [J7vH}uĥxxǰ1۶7f̻E矗e Dd]}u0[w/ZraL&}gj}ruq쬭&"k՘ezn.5ZP-M!I<؈1'0U?'*fqm~*vp>i~`L^c`sc Ӱ P$ @!62rt%d%mhL˦0&q1TUf۶9z$%espz ;6}55 6 8B.͌qtK)lNO?nmYr*13Is{pˋSK|5`4/"MXImӃ2)?Ehx  J4~}'&>iiY GNEWϣɲ گVt)2A>]!Y׹J=G3Gӎc;3H8Xnkya:  .kC9t<`_[C,?fw3c&ԣe](SלK22l=}<.whqruSC[~]v4p䜃Ϙ0r~&SUejI3#p C>̥z9 1| c<8"b>mR8lѢXpb5j8?܎&@K~Yw/o}+kq(Wwv."IrZox9 y\9t_4*\bQeI.vBe_X%CD5"O]K$}P!^:t뽻)|a&f"w9 $$H%ɺޘG5B E~*f G3nR9ct$}?1ƾZ.~N~9$9R1"s٦h%\'x{U$9圈IDL`1" so-VLq9k#!V{?ӟ4U-a&03.eRzWDDD{d;IRox}?) ~ 1 8fxڒ||M)cb8b8(a ٹya w@WUS9]4Z["VIĒq baj5wIISj sއѺm^ln^C$wuɟVۧVrL0cdE9X/K$F7hS@c ,fP̝@HFgr 9:0?)8 :3DG\mLE@,h0{ߡq7$Z-CZGG&f`1bE v'D 3C[<0 #$KY$'&h[VZl",KZs΄I8W(HA9[3#bIRJf"7t,8B-V ,ZDhjKSwDgP\Shg7s.byf)4yJi>CVdFOP0:DĒuz,b:2s9,I$^Jm[8}?j 0BLrZk;xhA8]ua 4/c)>MY$:\SۏAȾޯ.8x4gS Fg\"~H"A.ќQsˆ(z+&Ib,'h&"8R$!H&`Q2$:3E1" ʙ<|bTTї/K* 8ؙ|=u9. ޏ}_^L!>:BaPN5""V:ag\xO`]qeHHI{)8"IJD$)E"3dmGGSv<8HL,|ڻJ$ S6%K f2 ɘ0*N) %@RN1Ů&D90견D!IC1>;P|Oiw!~H~ v[,ctt=V8} xh?okp^rZKYsƔ$#IcepOf&0 "Gg-2OQ]LE/SC;!ȕ$)rs@@qÉ`!m[?!/Q C5[.JlnO-#"|P 4"JL@HL(SX|B)y)9$L9pY$ Yuq}?ZaLT SnRJm-tD,9Dj; !"rN BK4`\OuX`tGc 8k\y$&a3jHlƉWI9\Z13k7>Dk0W31IXoJ-B191F '&@x 7KQ1$ʒ"Մ2/Zޟ;JO hRfKa$`XJ澇RΒRb$|Ψ :Ch$ ],4,эƜLDcB VlG+8_h%vFfÇ9>Ô(^n׮x督zH-Zt4xT׽RL7 }߶OxKDvDS걩,o WӧOѧ8pt%G=8 f.Bke?sF SnYR3GĢlx{" הʦd-Z:!hxM4F^/&#UsA@g'0C&P͔AD8q]I)P$Ԗֻyn~:@n+.t ȱ̹C3˶l~tw!Yι4O_LLy˗/, PEtWө#/`@&BZڃc=x9g Įw%I~I̡͠M8De)c4<5YVD..ר}c^]|jA*ܜU=޿|OO}Bq\|=p׊q}";W@D47?aH3f:TK.¬nhܭcoKL#:j~ZK*i>#j 6fX#@{)"!EEDUP}< P˻9Q[ '1֕}%f ZP<2O`[aL$\JNI˨MNL0o,R9L1qRP @Z$ GyZId;ϦfkUݺ dw/24i $ܥnUeFYyf%)%L,HҷJ7wR)DhN,N{+5m74)5UDg @ I9UXjL\3M 6L(RtCou ϊ2Y@kZ",:i4D9ew7Mq~_v]'2 dB2wlgĈBf9Y{$%!z^smS|5̟3ecpRT5yZIy̭rLR}bk5Vk)EbLlƣr$!HMD7"G\*1t"Jyo-%W|FJmYdRaFmu%"&8aqw<`CLY`,iGJ>l9vU=r^ZAa"75,'e7o׵0\r̔):<:-,3ֆ\8G _If;DSCfYxL+q^'=uDDA3ΤIu&`X=#<_Es[Ns+oL}x 1|Gf>3s{̓nT$zQZ-(K[=KդǾlӒ^# Y>sc+Ci1 ;BLP)%Qy('y}:4o71YYDc xҀ6kUb|3˴q$jvc"F"8W OqZ) D:=tcav D DT"3ޣk+n"Q߯ D0Qe9̙LY*#3*Nk<ɿw b+U\ͩ>WD$z^tҷ3S 0c, dK?Պө˜|8 `SX5\(3N)ٸɞ$2;%L0G & .>ܶ 3xƄ( m35o A#>~pvDa1{\}ۈ3r޶{wsO;;xR}x 97M 2 `Yl)blXɩ/',m:2׶e8;KDo.5G7p c,{heH~LjZEں>nLLsDk,mRb]@JAtY7hY$uA Ubu#֚G=#6y>#@u8$9s+# Z S o#cB0}U*:B0DzX01%agyQ==&W QJ>W D!ȏjs&z6FH8"L-]Q 7 tYɑ;NLB3 tuˆHji&;.еD pkMog`BfZ rXosiUuB/KHjKjZd{2|?}` H|I2=F# 9J60e0;PZ5ˑl. mےvb2_R\-A2GX9"U/6A;DD "Xwtl`ܶo~mR+  =]Nh#~JNcŸ,K⩴!wHaʿ7q +އ[ -N @qǶH)6tUVO1a2?#OXQp;y\#?B¥{n"5CP`=J B"0#3*"#g 艃C}9HF Bڇ6vD) KkRm&d3}m'GY`dW˳"s$ 3#ytYWfB"Ү:X___^m/i)Lpf SSF}\-=ֹR305ġJټ:aaAP w7e&9k8Du~90 &5RoE€HOs^p΀dr6"NK[Yt+˲Pm1quBHQ[?ɚZ+Qlhn"鈦Gy;ok,vU5zY2gCmfZLy,?؈(.YWDn#R2'KZ&(ۙ(xD aaEDRJC uZŇ/ɚ}QK JXjnyOݮ.IᱫC˒|,J:`C} ͉mtZ c0縯Ě nVjED7"s/!\l}v4i_ۢ6z\~8뮽_9-R1uDc]`d~3&׶>&)q2ȔgՌSrDܳ֗rK 9SʫHý:VDN,R;4i}ćTt<LBʺV DsSJt8A숀4d2yMWELEZY!ܬ0"b50k) jjԌ8tj=+u]dWkpa2#HRtL) A9[.Ro\.K)R0Mw_ZS;WYI&eƉR=,,Cwuu}l{P3l{10,"(Q G$Dt8². ̄TJCv*w45*c 2b5kj0UX%"E! }LZƁ@} XpsjT@LNezǸ?A,/#"ocT|wacLdɄ*eY 1knJ-Ӓj۾!"K&[>IO۶MO=B}m{hkۦcx8 U^J}< {B鼦uypc\۾}:ƾN7,k) ĈLN$ u{$gu HZbǷ1 чۇ {۽އh(Ccj1cjTx灇k-?\2zvݑ"h 8&csa<6"B)0elNX;j ;4'@CX03zLmY,b湎")RxJE9~`s]W us$anS1|cY˹1wxyUC߽_~T 5f$(B fPdZ8L60ݶ=]#orX߷}:FMsIW2S?y/{8C+2)FZ0pC88U>-ߡp'Cי # x0M jcX = '^p)*DtGjfN9%p`^ r S?nًեf->T8ZQN}>#OD&&Jbq[K!ZkuUG̈A)µ*(LX<ۋSE٪N&0 qm\knϯ?}kϧӛsmwUp plF@7wLsrHDh9%F*"ILRXnjNNJ u|\Y>pȔH hR %4N\gH}:La<$IWn3Q^^wY\;48:cT8MGu1 `/8/㜺NdD.sdqcB JMd[Gxd`}߅X=hejFCgorm=k ICB=Xq^{@88'Vs ͡H1-1O=z0bH̡$)v# st|"ԄOU4V!0%]- j+֛,Lyу?ض "mm{ﺍ1{EA<< Y3H!™1_XCրifu8iȹ n xLcj&,/rVP6UlNG-Ui}=Ѷozu}0!BsD,t`US( ̘(eCh5{eXt9Y0f\%!^ČyBHNb*,U᮵VN{[' {"P 4 .Dp9#iEd~pBBmpy:q91!U'%`] 4q:ch] Y͹bRkMy_Jf 9's̠T2ޮ*[)U !b?̵| 5*ErMjLZC=}l-"(<0w5o[ @>;kjW6c'[s'ptjIk^B{eJWI8- (i%2ɿ, RR-:;º.㡩}pR,V$?_l~>-WD x:Ey]o6m_ϧ<9٢d(R؋b1/C@ ffE<==}?_ODͷJ)>tݐ!AGȒY0B mbo "RapO* t< P`0f@Ĝgh b!9b[CkSJ˂FU׶$Q`.HJLņ#l<\J7_/ysWԉ`>rx\2&dZԯ+TqL+ oKCCr}(v.H3s&C?:z{WE*Z>}N}2<ǣp<|$<<Ƙ%Ej: "p51[c5}6li|}$&6e*3)1k>S^jiHv;f_&"$2th##tiKz݆BDL8O[Dfyo][Юj]coC1kf:}IЄe#9@E@RZ@(^gwo?#K1H1RrSMLL9b//Q*|DԒ0RJ3S)bi+TqG@k]N鲮'*85م B+#h&112;͓$$@kB.bJ A2=>v,8w\^Km"Hy,Jfy I}ۆjBR3:8LB%~3nKj␊L[T!b;)9EdbsdCu,^T f(G_ݛsU2V* 뱼q,-Y:C=jxH\x}C#zNއcdv:q,'0>8p0u9tRjR,Eb{\kHm8$uQSGx﬒ g#yj1̷}â3n/ ̝2Ϡ<$ ј캆Taa$b'F&*DcĎvIDAT:IIO}~:8GkmEZK(5K#XjEUc%Cܲ7q d"sTyd~xdln\%`3#!;8pCcPB8lJ@MD` ':x#92GaȆh Z`nfS%%2v3O_LH: L'BIt(~55yhKۼc c~|v%CR"R19̄{nJf&M78Fg.A8^K'EsZ(f<MIҴgyz@mv"!KU xx}i>NLL"0~v0@kZ""_BpNIaDܷC}nϟ_lȂZc{o~KS&Ҥ4em=L#:"ydz>X|ڇ1Re2|=T"4{PcCn@ƈh:I 0KS E;"BM!XJ%'nL3"d#Bm} 5RnD[PUO!ZEtGOl}sɑd*)z'RC>? Lvjm.-m jCĶei-82,(gbWA+܄e?Z\.3 0ڤ~= oz:Hx\yӗ0yD&R4݁|oR䑊p M""Dž24Qt;F_GA2/INN,.`L5㘀_*ϙ',ۜ/&|17'HhjĒu#'^8,}iMϭmk"Բ:z~"vքFKI%A Cݭo}޿ V@dd@(R0U}׷_WBVӲ,@SLj\U܆n1bfǾ1g$% ޷އNLѮ@dчMn$(e#9dHvc03sw"a$ m9UfNA>A)R8t }yDhcws7wP>4'۽+}]5eȭ}u0,foSTv`}Hfi) T3@¥|Xt:/ cپϳ/;|M˧Ϸ}P#HXJ-$RYH13,EPͥT$ߗ JcV "pCDA]ptwNA0PK7?n@ؖEDR)̅e3 V*W9!@"CH=|## @*F)c&\y6r˙|NP"Aу AO4>c '"dwB㻇Zks"2dY+}Uܭ`Cg$T1[Lα̱ ^?}=_/~{)06*"J$7`c޻;ԥj1N<#IPU4*r~^"r^/o.9Vk%B`fZ9}o>u-нՆm_ZTT"V̸y@׽+C3D>bX[Ȯ!1?B܄KT̓!<[ "kxkŬ bVX*WwJ9גfhBߐ `@*r@QRn 1z"n",TQ!$̪1 q!.5) 3sF11W~wEħZ0ҦO,"BPIxۉ7ݿ]o7m˲ªj+% T`>P8 B$.cjDf "ڬ',633dL:"39RTA^ YFy$(OW~ߞ.D|{ AvF qr13~D;:?qǑ9;gFc#Ofy0"0BMb a48{sh>S kG#2ӑs>?m]%w{T{)˨ i|f}ϿݺΈX|Ӳ+8[́vj2c.ܝ9) Cp$4azv@8])D9LZءf":mz1ֺԄʹ[>uֳGn?ӳY 1= QaS1TanXDHHaCZ! PH} ۤkRwpRآضr1X}"}̩TAZF}l}}\{׭@$2',UDME4za)D6H)__op>©5GH)Eo[gigDw} 9oљ|¦!V,Zĥǽo{߶?ͯ~oRxᦜAT5[?7'q=Kpf_ |=Ǥ>3fϔKZ[Ꞹ֞Q6]|Ҧ,ݹ\ CxC9DTw0rx ܝy=+ T0ZKk !D~=tɷ?Ma\' v;d#7wP r6vYCe [a%ClF FD{ln7SyCUwmK)Ӳ|c<h y" >9VRij@X_!!v5 0;IʥɊ:FۈX@*o.OoTն^kϧImbý|>/k!O v%'Br7E<.}b G~g"G95?EdY|>N, vm82nH k^RL׹KL#S"0uˀu*%S=EF-G~);3#<,S?S]}730P; " (& W`dC CIgxEV-"aBj XRJcZ[َU d#uB2 0 T[?<_~_겊dX|!FJ}y=1(y9km~G@m$DL|&i'wOOo}c0-2czty\Z"ȍ\JV A,깎zTJٟ!"$f#_7":eLa:1~KE.5)xgbZJwU]^^^^މRp/0ieZ c C@2_?ߩ5^^3e[MAӧoucϟ? fN@,˓ O;d8^-Yѧ*oVHȅ{>_ gR׾eYS.c$he ?6FPf>^̆B)Psqִ:RlIgϐ*7P^q ;G@ɒd \C@+5`}8s>Yp^ +iT{y(Dh B_p99؀8ťUw_jCĮ;Z%Zz_N_."gN?A uN'U}:[%GRHA\x޼yP+֮۶%n/wk/W!1mLc~,HFB$F$,E" 19SH(.\ﯯXwom"tY1#b)\ry9 47&Ðrj,K%"""2q)2p~}߅òT=M6H 3  H<%:-Ծ'ws۟N7oΕet:U)K+c )Vfum]D ę19 Ff)m#7 F W/Ϸ{+Xh1՚O("Vw׮/1##b) 0#`gU >o߾E߿%~F ^>_PmYBsͰ秋Z~VvwΠ$!3-I\p! 0d j5U HH}0D0ƨm=-jo}Y0RQjNmV3uA̚'PIUwx>c8A};Gi%iΪQ~zm+5C<`.(1zSIzd~yE'@͊f+=C1G]6#"#mkm 0܃{$R"<-VH)|>sTI[i|>?=%r\."",miUD$\ Z I=,ZMBE,ƸnZFp7sՈe͞?y--:mLPu5}AN"I؄Yxֺ̘V3۶˫o?~XJOϟ?C5{ԺAĵJ!r.lTic)s7A*U.SY>@~Rt*­K[j:TDde][`\YV!Yӈe o@2xDP ҕ+4j&KBD1%iv{x+mi"L[]MW|D0/0I}18&dLF[g0' 2,NO?t- [ĬǶm鿙SSNgP(""4Y1.Ƣނ|_e=#')Jvߙy]00rJmH0}S p>KX$ 1y"/[8!Cs֝9Bc@grl1뭵2zN„Q1pfIa U-`Jɸe 0gz oD~} 7BS8!a,edb2w@34f- HGE^}6{Y2T~BHvFlABIKᶥ;= )EX 0k" O X?}:rp9)ql'K}GĤLg*L$PJ|." a«ץW$ޫ &Kq9+//~?eҊ&)0nDjU3Ə|R"`APӮ}IJn~ 7|xzyvu@n 5Rj[Zk)N_r9=Hˆ #"P<7OҪRK[X@vyK[E@aV옆<ݷmxf0q̇oNK98 1G|T\Icv6l6/c>"DxmRʲZt?0If|-ڗC,"fL,?R!d~B Tm 0Vb΃FD벬˪2"A3Ƌƨ1??^~x΀ $"r0$.D}ab|DR[|?34]DPLjH8\8Sې~L $-e84ѡH5R<@oN€`G9U[.\p)Re"$arPZ1e#@ap[emnF{&ED p<ȴ5]ss7Jm%0ѐd=P9`eDL"A6 JDUb?1͢2U= /SW=[Y&_&Ч4Ոޒc,]-DKR $ Jic}_C0nn3 (C_^f^UM_>F7ok-?wO_:"pkal2>z] .IDFnnYDZsY2*R=?Ua"jfgcZHfDmiLҪH1wɉl"nw/ziZBoz<̜qZ6wK;D 0bLb {$̀v7})Ek)i BɈubdu]ZkdↇǑF@~lJ i]")'>wZ8Db>9A+@@U]ץLZ~,mUP>==REY( u 1K=ܶ>|H^,"ӑmۀ0I1rX噗it)2NGKlR723!t|\L "A>I1Hg1E9\};%ZIZ[Ϝ|0KO!0Sm7ݽ=\ӫ Dط&\r9G10"ˆ3 NjC$ "fHJ4,&d0@MlfL^kvg D}kYj"961=XKCn2M7+DR$iAd@LskM]秵VJn02}}s\K,Ē8ƨ&"gpu@DB!=..$mY z߶~~w.E /t9?]9\/B@aIʩo<=9y1)H׵1A!BF,[Bqjs-^HRJp^\ 3 0RМ4R0"eY;l{?_oiKԕ{xR-zfw"o. BytlF@*7i]0)1Q+ gri]Ha)"ei@i5;511 i[eϯF"yC |}ۥf<5)ZHIYW9xs.Uڜ29=^`L9E$H*޵|4,yG BZ&T IE1tʱAD[ q$)|d<%sZ'#Ѷ1yYϩ8$L2CfDYF$sMT!,R~kC2M-[0N^v!d0%0go %ot3AKfm03>zȐ4̏]yY g4'D^۶Z[k^[(w)%F8J Tm߻.=~~z2Us}yݶ7ӇNҖk!#Q9!$F<-˻秥m:TU' Zwo Z0r^Z>*I@)yF\PaRI;gV$e#L6drvI6p@VTc%q^wVDYqCdaC =b]o=ZR ٩VkxN $Xv:Nr>OtuRE ܇)7Ĩ7]Ow'pei|*߂2 l{d "qL6u]%笚+~{N򝭭 j6 `j?淿{G&T&$E0 䜧s+ 9pȀ3ʚ wi$ɣiDcsZmvbfxB+p, RsI$2{{wFjua!}hd%hĜ?eSCdYqrC0-p\Xhi{ wZ&0q.'K:=7sj! 83)1L"uix#st:uՈBDU=_\W P%OOlW@DLS7Ő<> TWIKDm7OaSSat^(gZ8-WE$5"cmk 42.| SF\1 cΌp5ua>?9 ѯ"QZ "Z1fI,HGaaֵ0UvJqt^;CuDRSUX8k3"s$3Z(21Idc:RCDLP zy]^<0a.r߶q ,JA|:B̔p$p}`{^u Ā~}yqi6po.m+t" 0d1|13֊ &aCW7~H @03⌨,wVw hRJ)vU#bY֢η#η/) @)q8nf˲PC޿+9z_׵*KpnTLߥ">eĐc4!C3G f6ƣ0g` n P[5M9/WĶ \<蜎sZYl,Zi hATD1fP˃ Wdf<;MZM3QDRL#N<ķq>I1w)Gnזp<8* -"!{ƖMM'XdByZ[ /˒Z=y]1}wa5+6smP8@$1RgC,­wcl[Os|^ oח0u 7}pi ?wVNȌu8~YnБ| 3,:}dZӺ$m?|ޖǏ璻p{jmEsbvjݻs"}10yTaXj9_zRxZOk%ðGT6̌HN64S TRD,~RSͮ7Pjg">Kei,`aZ$/5wRYʤ1 lͬf)a֑}~߶|ͮZKepס5r"/OkB_y1Fgb7U7IB8EUIp:vɧkqLz" &8<<L5U}m:I8]LIRMq@ֵԢ0|3\KKkthw}-"d^kd1`֖y8Q:PJItX<ډft#1F˨1ˈShZ B*}j`/*pfə!ayPB=Ǽ"p%nG沙ZF!YBptFRxvgLDjRә*h[)n{FUD8m€@ZF?T_"e.Ag*bcv׾mZ>^=($jVKSZ,r9bz͇wo,k[j#0BH.>Fw !-iBBO.۶oe]"%3I֞O"@\NԶ`97sqH;p\q!Kp clBn*"Ǹ^M өό2սU!2banuѡnABHzY/>wn0MK|>4Gun 1Vj%lP8g>#l; ] B0A;*%GsJQJ!Sk"C- Za D&f(LmzsS "eOU1F-U}>[U+GoR5?}v}w9Y@V8~7Gm$ga`$d)Hd("g\"2K -<2 (cthdp^) rvv0DGq3톈b}'a5379&_z"TBs4Me؁;g1C:&{ƒ#F_^XoX_v scmۻZ //Wiu!r卒_R)zk勏!Ea^f<8 KF-Cz:S⮆\JvoV[ށcϟG׿zZEfEN[#"^D1ME [-iD̏4$>==}ò־l`0 ]^ EZr:{|nb}Ee R0saY׵a"TB ʤ$'=<ե 4u#bi(.x8x tRym#31WZV1, #Lw@:tW!Bo/z dߖ"Rt6fBL!J$0"<c ;!w.}r T3_A#aU{>/?)D:"bRjtjglZ0 uWucKJDc%v}Lﻻ2-4p$U{S7o{&R2F8`oy:]>~GmRnˋ7ID́lNx(?SY4k{+X t6tDr" YTT9:swdlZ+E՛"$IugJF;1۶f\ǏK|^|)"o46rn2:Ac ZED*cNņ#րpÇz(sc" =D"E!B%Qf!"B 3X/.umm)c Kzߺmyirl&έԑTa|den7ZkG˧J=? 2)eV4ÆvHBR&Md;z˥1}?)Bi"DNa5o~]&L yxPB!Táy9-l/2C֞e.^o[m\$"z|>#FP2v}}5w2>݇ 1pS?_7B6jVr]oO/~i_]﷜ڛYNH{oI )-k#*6)h#T@CD!us=fͤa$ x}w_i>~tKkVI[_­GXUv+yf tͩдPPDy8=]a&A2f;n,~t'"a}sܟĞy=Z~/Z޷TL鎁 = -"iNF! z]w"*MzZknyDhxcJ%$}pt N{XNԜQx2>iEDaJ;kiȘ"VkM*a?|f˲[+>{#C"/eqw>-ַ\McevyY&~ 5L czZ27T 簏&Bp0 Ee9/)7 Lc /Ba<%ܙT0%,y[jj!1 "Y'N3&HJZ}i'A"ȞR 90f@tvsˉg>jBTEC,YPWOT33w7a_EDc1 #b̷C~ gTVy7|t[cSf<-q]>(VGt Zfy:`Va? HD~_o_ZEsA7G¼TYja&F@)RZDĥVfcXkI'ȮR8S 1"!"eb,3wJ@j.EJ0hfCf|}G1]]mJvt:o߾9O~]GKK2s!ts]VbƙjLhLZOGn׭wնT5f^'K"EG } 55>` 3R2B vn^?]NR%eMi+,FohR:p@|vg)awEj+IDExofI?+z qDO3P P`V[.49,*j"]SC}DXx]-߭4)׶sRgﻻgLVj9A1G(cȟ;+zQRJ3#˾X.8w"S80ݹ)0i͔(fζ#w!~ڶz"/늄+HϑmÌ(;N33m=BRΔ^^^,{~uR4aD1:ԯ[Vc~(TUkإI=" DҀaeYH,{zn0˜RRaR$ 'DK~8."85≈ :FO) ڒjiG=g"$0{dԸ"xqx%Հ-/miΧseZ !|]&eYOǎTm|("ufdUr;?mnLmީpY3R RܷnˁGDa U$#r>ZӶµk9'n>YQJ}-Zf~lq0k8~_TՅݛ77Qc]ڲ6?ݻw$2&&D8JKD?8i:AF_{3'4,PsUdjL>bf!dH/; o>~󭚧"~Vt׈Xqdτt=( |z'Ҽ ( <. 0XgaT'SXXHT"L1tKnTatb7Xp5!>].9-eDp+r, p!pm\#o[U7JkCD OW|ݙU{nhxm DBfVQRUrA齋秲O?}땤ZH;"ֆYcBks:Ғ""}.%T{:44@gf$d|y\eY(\ !X8<8--[D E>.vzY>}wO0 uz:z, $vBZH!p v<2S \J18R@l9w _ۊR [0Ej-ZhF` A[~\ot\ў\r:HwًH@6Qsҭ"E<AsJ L9'3=͒?rSq_yv8eTݦy[)<}ϣ$9Ɇ~|Ԥ+`{Wt;TI(@  xg6"x%`ϧSkkYzٺ,}mߧ[X\__޷۷5 UkNs9 !i3V,̳Me2ISZK|Bݘ|AbO7oΧS)mg @eYNSK)ʥ⯯6hTq]kZjmmi9a!`9L«! H"(STR0 xDjan nshF0c5}۾}ޮmwuȈ}Dnﻹof*̭յxTmw"LQP5i\T]g<s\IDvp5߇$%)D%m:!_RJZK۱4<|S2}Hp̽rm۲H%}"bijO>>(,L"i+HRq&?mt1PĄ07__ßSzek}XfdT-ґa GV5Fts@$l,˂n}חϟ#BV"EZkeӚqbݯxt?nL#YS]PGψnb V3(0tOx5K秊k"2夎@nQ1匈|Z P#>>ͻ\ خ7AXZ_o 3ǵ4+aa1Ó@(a:"T|m}9}Km ³# r~o۽@8n}>t,""&=݄,KkK,sgM$Y&v7fQ53_"2rB4D P<0%LUˢ=AIIfw9;R+b"1PȄ,H+I-3":f:})fN#!Pwǜ\57 t@D0}jG6:"J)$D  ,\)uuu),^Ed)d '`]"t`t)w'@<Ԓa8"U%3E`f +'Z@ė[Y* ݏmeZ ?=]iYfd] py}CDH"RV6G9u2S"wZ1"ƈ.+MXXR~UJ|_m>̾?\W*f$HO^+sai>?WJ ?H} " ,a?}|PUXc@]T]I?h!ZDTYc`ӟ."Q"#&b`HT NPD8819mx >5?cN`.T#:-Bj\jqeJ.b~121x~^hhcYI֚^rI2R#$m]WˆN\k;_|y8߾}>qe@u]Uy07YC|Gv˺Zy0`D`Fr#)גCY*pFADq fL\ey}Wv׷K꒙Im"n69ah}wx],oX0KE@\¹P*En;>9<_~eqw,(`*"fD2#Aq-#?~~O@ a,<*w5%B(%m߳Ul(HPI`@۶Eef!"e۶e"Gd~_ZJq\?|xmK?\p!#8ʩ:?ϟ>#:Tt u&`u]0 ~sIzpWD2E^y J׹x:fZi<)c,+`$)οJGd6o蹦6";È0#b;r9 d@u0D 0e$(4RRib; I֭0 m?m;zĶ^J):3wuF3 Rqoy\9^.hu{<5#*۶NnicTԡ6T} Zx5 ԫfRYltWHX~%Uj,SZ)[CnǝmEm]|[8vKWFx =z!B/?D('K6k2 e\t0"f$qsr?A$yÁeP !HTa1MZh ̕@1UÄ`k]{~Y Fۭz٘i{wTJZTIXzưHBl;b2dJRs-6bڙY4o! aV<]ڲSGNm$DLlA@ eYw3#bߏRJppW; GZ8\m0kmAH#Z+!S hXqP>ڱ,?o4޴{õ+":F)j}omYl? B)u*Sz8il˶~^ߑ5DTSqowfRJF GGġQ!"}\KWcmKB=|H9Y_ERJxM#ǣY34 {!0?a<^|NB$8[H+XkT煉qOwsMM*]5e[1:knH!5beYn:ܠ.˒ `*"?Go޵1\@;~߶\d:?7w0U>e)yxDHQY/JHz^)jqd6L23"RUuR.KrZkIB뺈,&ץgqqNa̻uO\ݝ3ClD$=,Hā@˶գxanX`Vͬ sB,%5=-3e/_2u[}Q>uMG$QNOXYha e^*,ݜbT (hYч,\^/Z#.O#=mlE~,5f,R ጃKvܷm@&Ru_м.=cPXX'̓@H-E#siHKl}s/ n}۾ ^J1B\P~6z @7EĶӇ׿5O/k.˺Us y*2!Rx&0bDmz¥fYy, Ml"J]7W 2 Ff+eY__~332 0#:qȈfv""Y/?rq}#P<պ a8v˿Sy^9sN bfN'f20)+ZxSEp$Fٙ+F:y~I="CԘ%'@AG<Ӳ3'0R0<= "s+s>Js>c"H3b"!K*S3!)A{hGo$SjnD`.f:luj,e@ hPv qӴMV/Rjڍ1{SJ)cmF@"0Y!`Ss 3 HE$}hB=X=< G(eD" ps7!n붬ۺl벜KH-"EkywYrXJ)R="cv1 C8 e沬].Dp zxĄ~#scw DX0YH"y.h]TO V akK-" ą˲lp`B&AYr;{n 烏hN"v"pol#ކz|}ڻy>aC }6,K]___Q"A%9HT {]Z+t3-`-%GՈ`0":voH"µV譫j]{oj"8A NHvzʜœ$pc&@"3>}뺬 +:RGH!^?+9 @DD5 )Ie,eإlpukɀ=RoZ'?x)NZy1t #!|*GГԱAIMNЌDoR,-b~YJgUg؅@Z#pϔ5 8pY֡6l[=LA`;?}<9?XNm2؀ih}K]n@,6ip)ض C5\-]$ bkT~缡rA Dn㾔,Cޗ*ݥedpgYt>amv4&)L34" ޻dYN1"jLq :e#e 2pXXc&@08eY{w)-#L'f7#8guYH$Dd:^rי,uasbcCXX8vҖtX{eB𜎚f\(+5I @cp-~b6gf)K;d\ 0P+"^qRPanj}lח~{ϗ+Q( \K]гG/T !H MJ9z{th}/lāǾgB>u"V2#P'Ȁ>?&mYJgԝ<1 w ".VR0(Bi,_)b\+ SDv5f ?rA@֒[F`6wȋg8`~  gʜB}7W-ƉsJIƧ<;qz|aޜ6hS7̕Ls0= K@̼,*!2:'"fsȈRR-R Cf観@H~j4^J~)E0!Dpl0 SU.zY˺nDR*rXJusF;=hR E2җ_n뽏vuEo{̼^Q35MUuq U c]j1squ&6oVrYS>2)@UDZ/R+ 0 7GT 8ZC `c'TO4y{>3H= .*a/ha)w'N]Rꐜ`Dʉ Y¬cRg&H0F 0]]Ku%@ĦF" "3A].Rk wʈ\MRNU5"HWoLxw͏[OMY1}Qxx)eI<">wmn(o YGb=  " ]}w9C4s1J؇LQa<#`".0׈$zeLL;}J$}grQ)LU=>̷u!ws#́!q{G?3*~&5ˬRe[~?x? SFKF]!@!2iDHL̮VKqJ!Qa1hυ#@n1z:o0}FRZ:Fk@DzȂRJfPq~RZX2a)03]p~.(!=cO( BP%m, C-K]-"JNEHdp: 'fD-#ioM5|뀎^`jJDsu#s&"Pޗu:T,uw7sh{0.R̙ՙ8Ȳ,LKa լAg<G꙾pHceTc!jUڄ䭊_5 juA Qu[D25g!ϔlF̰mwn!f93g3%R %DrTb&"LR fPͱz.HC&t !G@;iy(kT&0! g>D3w09 tnH) S quD? Kk0!~u_~gbRS5]J$ģc+EaAa,n K@Ζ3JS ̃͢utˑ%Bi Ғi93d]M G#Z&Ò1KA^j#@LI9 B#D}VJ!b U@Yω0'1% Dϡx"`des, i}!Ey[sU./?ﭑȺ,w2wĚ:p7_nק'9c.CvhRKe"%A1zu6DdDGgGIqFu-R/2 B}($8R%0/ Sގ{#ܼGOHE >5p5ˣg!!AL3ZY{;8χ"DqFR+Ez Q^ "E0R|Q#tP1]qV  0$:?m0ގ֚YS S]ꏅC!)yj!=/99Pz3Щ(1eP,z|)H">fc{{%>[ХVdaz*7,lo.>B́w.2fbΠLJ01|&$gNc!QNeZX pO}fb@ތ%?۾Hln"`}3lleexASX&磏EYxCMSX}@&\."D =N>NmߗR."RDl]9FֵmzlZ;C͏c'u]_^^۾rEUߏ}t82V .eYJ 4RM 0J)"eZ4slb.Bh3!eEpqmߏcOj|jer G.6T@|޵kk]Uւz}Z5,\9"2H 3&#)RB ddHwHӘB@|1Tu42L{0@03 # 1jfN磙mS{PRDHsɒ $oJ^q[;Ft޻X1j۶nzٮ?0",?7Ցij޻z "l3<<"zIJHa37!hp虿f >'$jч"sPBsRf)7fW pM,(HB8ZO;N8GKv?ѣ-za$=z5R0ֺ)+P S005&h~.BzQ:Pjmo9UIy4e|$IGv𡝙{J^"tܢy})Tt؝h?W{qDL>DxF.Yz$B?1jZ7?~S-"B>F&ou]~ۏ~f.U $ uYkZ KN}=ǣ) ba K"Pu)4P4)ݰ>4R(FsSC`d.ӵ[Wchٮ?"zXDꚗ73/0z'TՒ32WbH@Kf֏cS )[3"Xia i<6pf8 g'xK`!8e&__.[Jq|y^^_Yߊ 5R,њc[1Hr d-` *"G]WũN̄D\J=+:|ȱ 4#ijNv֧ &C,#IL)+iZ~gS~ qB[e$'ɒ+,ltGD3s]JJLmL[>#s9HAp_nF̭e"r^k>F?*"E$Mg@(`㺮,}76'P]1 4< sCu 4P{REL8qlBCvN0P[䬔R3&?s򍇓tr\"<3ڇ*/uZAsH̀s~2^_9kߎISEP%ȥ3#1-h}Nc].XlO0l>~D˺mz}z6^_mkؖEU__ߗe-\~Fx0q)xyy.~y~V  ;39c`)D"HEnǜ+B’mfBmnC͔:պ̡% -&gx+d0֎Zuٞ>DRXJbm Ua4wӑ9Ą{Z pa6fT)2-<,t ?jI$BH @tb ^&Nùv;Ԅ0MGiary IMz0q\M5U;M}oMR1_>|&{Cv矟?|].}{5˜8j@KoRJFzm61`91x@apK #` $"$CNA 2D 3 8x&CT\j%wRriژvOOU-`pR}ṰUKv3O6I+<[@Ȁ487dt}N;¬x&Fx ӱ3 rKYJ͟]{)}8u<-*_.׍S!qz>]r-R",0 "F9ӱ,T`ܖglʯr4%84=aيJu]R8\e:HXd]SCUUq., pYק?l0V6̖f>#=%u)9&Z*E`]R$c4i?-rt <ϥL#"n+GzR. x}t*uϼbL15':<#M)G͹,{Lm]ײ.:`]/V8~зoÊT{~"@$ zDgXr)i,%]DO%X"l,f8@ޅ~N'7&n> HWD;ZVFl k<7ddȘ8H08ES@4Tn˶fGBDzhK&u d"(L m[lW8O|]s'"1? f 2g LKa᭏O(1#;@:\n&l\uLkM>sQ8=sRa9hRc1֊4w!´Y0 1͛!] sWl3LP5@7S ! #ez?r+ntZDH;Z;:JRR_$"}&~sSQGk:4jmP;cxG#pR><_&5 ` !C!p#7>@>2TKͼH 4<1YHK$, Clfސk}iٶZ("P8$r!@S5cBf =UD @PjUWwSMO4)4#Zו"3q(_S(ܔ IJ#RbNKb xL#d"0+CZ+~U}!Qe뺿\ a6R #.m*OnKx@WU۽օ ~\*{@k]%p&'Ya@/Kg#̝Ob;@ϗ=|^K 𔙧]k.#(%=hG 3QYN:FaJlVo[("B,\ Bҡ@rٮK1I2Ū~̭rN-¡s)ɡv4kE("j)e]@9 3ͼBZ@<|YX"],K&K>fnwR20H~?/pGeJq72ᔥN5։1ջs#<qr\әUI, nβ?^DReLt{zdl:cbd{n[{]Hׯzd'Eey~~[_ں""%gmyo_[kӕ#|hVky\{/㔅kp7H1X-wՀ֤e#RJsKOrLpnkF?hC'Ze]?~f5 w9l  <ˤ܁098[XjR9l)}C9-E~sɔ3 >3Q`ݓoDyn83=OD#{w =g c @\я-Jķ۷d*,m%nf˺~q"5If&Rr8F\~Vk%m:Fmۦ0 eL7"DR8̌\}% L P)Ĝ]1 0]u5߭$&/L+#fvqN`x.|bʢ,F63en4gt&jݔÝNFlfw)Ds3x,kYEݘ?Ҕi,BFCӢ2OtbB=Bd\0ߜ

_W0:Ƕlgixd(CSLn'r0󿣷ޛ#7fQ*BD:l.O)c"Huh@6?4zОyX8:kf5\F_]=?#%77ec9onR >FwߟMF#K` z!f`rSL+Dq HRNq̅DffQ^\q h,ENCP33 CpYԆ# ccnt )L|J,JN3EeFs}?f%nƠ !O7 &"Tίˢ;z\ jΗZpτ@i_D?<*3%"윸yVj8LY9NpǾ'3O}\$&ti>S< w\"@n >H z8G򰋀Cxg~.遐; ^IDATcq_),YFĶm<1KQiJ tv>,i׺"bSݏFT ֊ȗ/_}o///R})…xh<@Us*,?||ӇtMe.뚩j%ift\]fp5!*.Kg&4bݒZ;SHHYdukk-Y#Tͬ޻Dr}z#z)q@4>HJ B5c )' j 35,eZ7hL^Dz)YHk{y3 'F"f3.=2:1цn.A<m]z_[%EeY>.GW5\.mz{R%\agЎN3a0KO&I>%z4P!ꌇ̝0Lі{n| pFs嚺kgVZx~#ÙN*;p)BzpJ5SsY9iRxXx8^YN RNG;^UU۾ۦo[^f_R#`]8ܭ.EuO?EDJ${P,d 9&2-N,Id~Qah6~9)!+%DnfRK^@CÝ1zsZUP ]OJHBnBLJa!Ҟa7Kf < "a1q1:!7HRMzR*F<|cB;2D."DĹQ*$Tj9zC]sn붦OeuiGK N"Y]W5{{_J9,9\흋d4Tm:Gڽ30 G!z1̲ߴtIiTghH.i<X'7(鶐"%nx~嗼Y#"%iWBԥʈJ`6N 23 )LL6z ]ha&AHk #nl 22-_"wS?[s6^cB ʜh}xjoOݮuYm.2BDzY[M qVvlAs{zl>ADdzeNPLs1FPr*1RwU͋!ٵ?\~bv3Mwoƿ\k]͔w~r>zap7 @8hn7/G SRڎ#þ2x}LDC!==?f~{1w&Xkn&s#Afh&Pyv!0YlpS}t ͷ0e ) ﯯ!"x||)r۶{U]î|lGP$).b}x &=ԇU.Uo?0tdB~yR]St1$""cOODM 1>ޤ,)"aXJ@gZ{{Lpb⬜No6eGymxJhS\1`ƈ0  Cu:';K]x 8!6UIO9*afT"(oDL$""q1.͆^. xi]Oi'"SHi ?;/h-uDl;"r)BBa; $pFjPxY3u ¿^=uryfSk@9pz X'+bY. [4WIDrkA"ȥ$ Б e9RgLۥQ*mV($x1lP#q-APn(q,b~}/o߾vnXDZ ~x\_|EຬL@]>+"q\Ӟ *K]mE0 k6RRsOD$M[@ \_b6h~$-a!GL% 8Iw>Rq0aCjGG9욞ۺBW' яXJD,Q A$~n0#bo!("6ضMCHLy#ZH֮tUUy|}GSVZ֭:k%nRȤ<<}at 'zt .e@~lQg<]C%lwm~ٮ&4+88xe~DY )cP]0.8&DnkəݶK뽖zy^^}{~yZj}{lZ}]Ja$~}p-zo/{e>}xO@uY OF$ㆈNq2Lc:,YM6q\.؏=.j.[WgY,hN玐t>"2&m¤dobt~gs'&?d8ŸS^0Sy]~̆TuԥWjpY˷o//rPs叿 e] s3 Os.32œ^YO!iaH7qza'gpJu]>|\\ !Z?﷾C ,rbpdy(W@sM/xt6T|L6u{ooc pPbdTr[///-Tc eY-RoߞtK-ˢ:7}v-uhǁ,oHN5<|# :n3~%)րܗ>H4qf`y8U*e{rX&cGJ: XT$Rٌ% I&FKvtjl܊p= ӌ[^m"dLFٺrNAxbܽ vJ*GZO,;15͔"$BœE@E{|Q~MS1ES+pʿ|@ZjM^J sMA@4|4u%na3f*'hA}.h:X.UJ=(wGvMpom2`Ysj:zknwuss6qwe:t Uuqh8iR-(,}RXU3O6C`3I-?ؑ媔Ufr53O;APOI)h9|~%{J&Am6ƺ˺ ~" /_zeڇnI*&8Y^{7)ˏ~I)o:ObY4s"(*&Xl8w|0QS(:uay۾hmVGhˆ2ME8X9DSŔw#+ ~=$EZ=z?;RR܇*nC)gA:c cD0!kGuYR zC.M)SH)+yiJ(ɉJ#0 D Ys>Um/,D{MUf~G".irε'p!X Ti$ DT 7 ե'"Yhu}$~q쭵}0I35'"( ;=룷fc@BsC )~߿~i0OP!&gh@Ln&<+n$lf,gQ @ખ}:"e`ǩ*1~׷ޕxėߖe~_Zd!˿0-jCش_ T&͕Gyo_a[R "㇏ {.{EC`Ij:Ծ bՒm930 3<<4ҼiĜLCeGSh'DEXfq,ER3fRzH)$Ē} ϥ.οxM`c烘m~ >zt>ߛxl.N{WTNt|N̗ٔ VR~zlV뺮zn9łY%Wr T7No]fϼ>kHn᳜⹡# .xS8O 8Z/nku]WbQ˿mv~w{oM~i=ER{kY%pR elI)DiLdC5*f͔49y&Нoeڡ{^VK߯#9d 1e~6zkhم0晣bsUȥ@<`a}vc_APJ!R/tR+yfMW+ǫ&Qff"'x]36"0,v,~u-~orYE=]DtYn 3קs1 O_C\vZ=bvlixa>S@#,;5,!ytĜٶ |T+ur0|#nWf~?fd3#?Y1)b\zk˲L!`-,ts˲ RJnDRؚZk>N[`:XbCOYYJ)Rrn#-nEFϴe"S 'L3ҟ(p#s2 XLSD8C:2m!/EWDD#$=Ƒ1M;\Y-#rI3(&qmD ET53cS ZfV`*L,6鲵v`Pz^__q],Qk%b?mN[>}ˇ?P\]ze]^EDx:tfdd /82ԂD̅3qj38svUՆ֏ct35$Ñ2m]FoqǾ}o~Gvw uGkGDvjXM0̏>Pݿ~{}rߏ֎r~$*;f#DP3q4=dHAGԕ-H2#Kal\uVMx ѿlx{.]Ƕae~[ @XvBH:z;n)pKaP֍J=Te|e[E8?c⡣uBBbtPQ)ȯ'3eW-EPG=!Cg̻( tdfd:A*7 20[J }쓈i"DֵyrFsFNI<\/ic^L|oƑgu0 `۶e"K"8!K'e[t7<] #eS""˶S0Xkyy~"~>d .zn/iuYT/_~۶].zvr+%Ch9$%";e-kŇ\*m%i3|TAzbL=7i 73Uf v~[jSkj`hC[CY fm1{@P%_d M1~)RR:B91-Nt"ls%~|fBg:7@RSL2_ZraZ+3ZLE$ !\Kރ)|&fCGʞ.ʈBď?#Dm?|,u2bdzю_~k}r>]یԏ>gcȲu|ze[.Y-w3%)I$pRh;ҬIW#+Mt\Չ([򙉇xh]}W;><|$ z}Gk:Lvvw<,_c}uM9!RkA"K9 Qx~:S(Ef(V3& AXjj YjuQ(JLUs8:lnu۾TkuO紮 )"^__z͍Ǐy26Zjٶ s"Ho]cyُ_t8ϋW<0!$.OYdyLR誚4"zRF-5{-"4̙{WBXl &DT$/B]|L3!p*05iyDrhni}u] |ՌZkϟ""[*bTf檖P 2)|`.kCGw$i! Z \fCfE{5EOC&DѲ͇(?>yQ{YX.2ueRny!RueA&lR9<LjeWB(® ٚyG؏-K]ƌѻyoMOO :>}󕈎֎)v~܇^?~De]۾uYR.׍X$Vȶm9NsqRW$V7 PB,˺! A >F]0Tt:`eYuI/H{c{߇ͼ;tdFFmh{Cu1CSWᮎQ|{/ߺLJ1msʥۅRzuaF^HBhn̅&8=))3z&%T$B j->Z?˷o?N氙U).kv$R.F,__՚GÒIaH,k)ӧx{}?)` En. 3Lj%'[W {4uyh1!@p0=81=kT?,KDj]XztՒޯsn<ƐRXON]✒~@01yCc*OY|0"#i1G^m?e ¼n!p'A("Gkj:!0J>. E_ Nm?KlyM!D"PӡwvxknUSb D_SDl^+nm]WI_(F`.8SqU\b l۶IS6&ʑ;R{@oCj*i>?x{{˷. qYӇz!{{DUKmoR8\1D'P)_PZkIDyҍZk株Lh cPk-pk{{ݬc`CW]8LSD_}j>#ҐDDlmmg}^;eݶИKu9,4f7H2 $ LjpZ VYm{ݯOOozno-ܲ<==Ocvf݋HׯfDdWsxn1F|>$g돽}|T#BA>9%p9вsE?GF)A6eA G@HgL ) L1RK{;02C[~`~e\!E{ 怈D]6-,׫ADO!8 "e?w3q"1=LD"\""쓽0~*Tϰ"ƹȆ˹ӟ.Aa?Α#e7"f'hD9 2q”ịu+L{_/[J J=mE@F!K @ba)Rաmk."BvYץ<_ KDv|jJ%&ض-5`vKvi,ew]|b=w].M"DZJaP"f&?tb=P3/uQu5'$hFXzC$RkB!H$2̆GwH?  i<b0`m.Zsei{Nb  Z J-H}җuy 2 U[OIMO[9WA~}}{z0ݶK-;BY˅ҍ,ws}~y*o<܇rEHRe][ S;LAy" ~\!"b) LЌX<8UA'l\i cy9?DRcfpF&); T* E}ȤOkRkFe~{$iXS}4ӕ o*JofYzSW) :x;ɾH3̊=vs4>9 0qf:CJ2Eb /J!2Nt&pc1H?uY<b'aS`vF,n9OG*A$035µԼWc֍qReFe]zaZJuw@|lyazzCSk\??}/R_-ӆ#]./oߘXdҥttpUݶI \s"O735nD\PUSbԡa>7{M iBk*RkE>F&l f$gp pDBPL.R?uo~Y8v81cd%5ļLHuL%O܂4/Y뎈bmt Jvա<8F)ߨt^z5CRJߏ1Foo߮קe[gٱO=,B}g[]qm%Jt#"32>֥TyLzOw9KI͜O9Tp=M4jb]siɓ8ϱԼR "Lf݄%MM^d2(|Ӻ;1'Հ|*YDJ#J&2#!uiuYLV{J "0 dG!"?i{7M{3e޾7fQ*DRb^1ٜ_~Q[8@V+;s\ t\2hAd$nFOi&@Z(I3ۤwbMU41lY#SD04 <<<#J;F&t<\MvY v1nw{R.ZR<_~K)+! a]ݏu\e]}}o~{.^6fZYa^ RJEZbw*8 wbz'S@<Cp˂$DݢRGTQu{M )?BtlED(Â1 F{` KY DTJ-ERDl[;²"u0 0 5RKF͸R "RP$&_̐$Ժ.i$@p3?n:Y 3pgfunfv{z~[o"~<,~12ܗa+QJÏ}4Sgf(86񏯯2Gb{ǽ姏w8#"a).GNP038dͮyxO}zo[?R>1c3(M%aD[Zi֔'2&)yx̹{~b0{敀xa楈n*E 2z D1ƾ$\jbȂ&` M0eIUd\%()>0Loa|?S_ɹqo5 wyS=m;'뻳N"c /P\Fɩ*[H!B;ө=,$ cc3,k9bDkM?]KV$eLnD4.Bx&ɜ[?M@ܡiˑ$eRU31dVVzݧ^}Ϻ}2cpLQC:pw`**?ބգH\03c ?Fo"c~߾~og}Öp_/ޭG"z?_rIeYׯ^H孭$&“X}C08>w_1;@.pT`}G@șowrYQkuNҴpG?>G|}Sלݝ oZ!/f!2E$$ã:Dۣ5ܵ8;P Qh?ZoI^pև1gZA$"YGq hUL253{QۻڂjMǾ?{9gq?.K>v%Lx{uY9j9m?tt/_MEd%\>FrN,Ǿ ELxkc ,z!h㓻D mNN,e?\B6c$Dn‚~d@E 2ka̢>?N}8R|O`C9 :!~hH`I@>F@Mh2pwDHDZ3912㳯 ֐3(dA\.Ls9(3J ZʂL" |]!If>D$ q' j?Fo#1\޷oo֮Cmc߻jWU@sx{_׽֣vNr>Do 3~]G)DNr*  \B̭a8'$5}?o''2Ǿ1 ph>`'LgG TVLe.0*. ~ױ\9z1"Fk _~ew"Jdh}jG,^JQ3_,% ߿|_Wyn}Y$Eej=p% s(c^2;0aSb #jsrgz)d]fS8+`gnzL^t#$"I%ԂhGϸss RLa=l^kaFNO2NRш0LIuLMqsbGԣ͋ƓS'6f@ l٥Ha lSx(i)eͧGO6`,q؈Ɂw9 54 #31FL`f$D^6 RGv11."3{9rJ>" rYlaFlDlPrz> z^s9%I9 4j;Rm-N9H9 Cn!;!yNZ]-$|9Ϭo/Akm)O{km'6OƱ/LeD ŌyJ=lڝAobK϶RrJi]u]Wur{{d^P 1 uh 멍D Ǜ?%>8炶H1sn;ӀEb#RsRdy pw i!&"v0˗o"3I63D/.et5{ooomWnV42!3\Ha_?Puu}ZkM]u,%!V*-DBa) VZ*9 jƜM-@J pZDǏ}3eS.?䔆v˗_>}seI f:Fm\{nEERʹ9jCXRΥ,Ĵm[Ǹ?ވԮG|\.#%q=XᏣ֣41̘U3/D=r鏡n4bpzkITZ___-&X31a0֮%hD{~'"0 ~]'Xj#RTÇm6ܬu0F1kkp>{NE )'FZfLѠ6ٯDXhѓxX@>" )ebQʕ$=;6~z ,KYR6[tdz!A"&Fg,$L鋸zYN˲D7nL;d&iSJ=Q US3zɋ㈤د茣?E`c^@ Ńnfr0G) tU^$O'wngtQJ ^+euGu` XsTA՟ #g,1rZweNYҺm?jէ[;uUZ+gZm6_ƀETˇW_ֺ,9_I| sɵVp צ] 7w_VGkա>cK)f$0f}[z`{Lȵׯ 'dח$WA`9g'ER#fÒAQ$RhGII,* %N0ȴ_J nݺG6Flfu1V²&rBRUI=cT99{*Yc;l݌q-чq˲$zl/mrݬ#Z!?J)pҦr^5rC†ČDSܑbO̦! я7p.[P' j=1g%gضux)% {CHW;G TVUt9akY"^eF=F|CzQH~sڵ^YK)mt-歵8Kpn"qzonQ=#(-qQB#11M8jMiV8$ba>ڑX(@D+trC; 0e%c8%Pm sNY[WuYbg*gMy,F0FMU.D&YAEv$uP59p1"{?mnR};R«,~kGm^%똰Ox<w-nJHRxT ²3'=f4@5WCĦ%'&6ZkkΉ@!5j#vJv߶1bމLL)˺ }|YݖoyuI _o˺N`yMfok!J@r:.˲=Ľ1Ԇ,#ЁDc1A^[wuYS" Hλ嗥Ĵ-{<ƨ ƑD$2ӔfIA1SB\5~g/ח+3\=#2=t^7B)T-fs6̴rvʄ ) !9]P_%q@u\5 (-jS8|!sUaz<{Ql2 8=thG:.뚘A{Nj@D[kRԴP] &fs۶M$? XF:8qV<H!oBӥ5a-ruG3;'Ĉݛ/8W1(a>ΞO08ޘR'\]u6LCk3byhQJ V=)gdl(< {< ڻ$j:ƺ,9 !֣պOey}>_>ڿ'\/k ?rP%7jVJoo߾WKmmDMmIOu""Ƕx}$ hzgUen1( uǾ+B^>17׏kk~PJ>u傎$9!Z҇Z`? #,9h*k) "%2@a^,Q߷YL͌e &ƦMDk)I(#r Gt0L9QގQ+޺1TmtֵZmc].+!2;|ZcC("{J=/$v0Fvu]uY|om,) Och$fBoc47aZY{IIfǾGpcۅK}3)$*naF$\[#fScpTͰxtD0znk=f)fG19Ű7|)ĩ>#sb!L,E"+# s\ptG9RLqO y#0F 6X/J@oMX U0f \eS# 'l|w<@$ Ck"5D, BLc;ʄ?GA!ѣ<&1J<`pWتLڶprr@ Eyr& ky1wX +"N;:%gw>(l#D~n#L%緷>ڇwx{{csűպ\8~a[9u]u*|Rqr}ZO?K21Q[jn9,IQ:"P~ߺ,03>Dڀh^Ӳ|uDQ%'a,Y\Rb^o0L$=s91'jΒz^. n$iYWs3Ru/ܦ<oP]֋ )㱻;y.K77udV0}j~IÚ/_QjEї07v{y?Z#\לh.LY$^֗u-)}~6у\-'aD!ZsĴ$)9ˌ3/'RĪ\nנ~Y!!giqW3G4u}2"T$Fe-ō$ 810xN~DYchz>9)!`k5J:jvEX!Rjq Sd)ln(`EJLv-*2m|6ZC/u6$~Q P凜hH)ÜL3 Q,Q#ehb3Fz$ P=KK F\^)PU]%hE"J. 34ngS=̄m:Oն/ˊ!b8zYv0?}WYd۶~{?y8RrJkbYkQk̜RaY"rВvmMJrF 1 y8V''1؆m{ݎ{6{0 PmCtG(/K)KN KN^ۧ "BLDbK˺kakÇ-C9$z#G G7uX4=o^>|r<-DPG7njC `tED&{(͐H2wGba1u]W]ݠzϒ $[^oسyK)RJ%IdZ Nak8ZC c v 8o:8MEϓ41#huFDVL=N*9XUnގ+hƖ5~3O<:ױOr)P Dx[k>,1qoW"e!0לqJ> <6Fb {!-|SGwo'(֚ qH1>HE  LHLOWp$0j #F3#(lٵ֒!89Vz~ h.9A;p;"#՝ʲmu TANĬb?"ǶLnmWm^WYEԣ÷ߗ%YmVrv_z^.K)3cyd39E ^DL5?“xlm,뭵|L'3\bTxu]ʒRb3yN@9f9PJ\.G0hռqJq^n뺾>lXm&d>m]^r+yɒ˒AKJIT{oHK|Y .9Y ve%e]וײv$T gy{6'&", б$뺔T)P$/j$X'ݰѱښ:9~?F!7"?\m5ʲ흉?~`,7x5jm3/|qt[2Z/9=Կ=6,e EY3,E$@=BLnVr.",'9X IPV9x ;! 228 FA{xB{bx;5d(u33DtG %U" f}ڷz,,\5,|H$N9DX HN6 U#/fOՋ}-zY'}9C!~7 uJ C'C@~ԍf,kL}0܏# ѧkRF8E?kfqjS0Y"Fq)%8/"_UIxP joGzg7534Z%e]׏D_󜠤߿?~8ZO=05HRRJ߾}yj_TU'?v^x|{8JQp",!s} oehC0()_^^DhΗa!,B91y- &6DSJqRbޚxJs@ц庘iAI"  $tɩ MגKI(V,uy *tŁL,9aH)Ӊ:G"}۶"+px{׮L[){򊈽3KYbOhhꈨ*"I) =q//_|/뺽R0v o8 OE 6?QՔRq#{$BOB">>OgR" lJ.ߣ φ34rИ7="6d.%c8ƄBw.K.a0W0 @rBŒמНqKJE<0snq\ЁЌ6uӈO2L7oM%@rDO8"0zڙۘ!x<L%$ D$N ~,p%HRL!"M>L:9RQ#d ptC8,94 ft #8E`˄ZM"H-$Hחm)u^m?2T_oWG`s}}}}\rJ"Ckk%$xyz{Ͽ2TmGĽ߾ .k Lv~Ce9]e ۸\6p1t}?^SKɈ?.DkkG_^[nqS: /eoQ߾Z]UȌ 予0Dq;Ǿo{o}?:F|v^DZ,0 #d5ఘD&ɹ=v|0f@DdjjAo0|R3jZ=w V#8’=zKHJ<˓`D!œdRbfY"´{Jb 9 Qgy}}mEZ;'UM)ZmK2#T?O?^^LJ""jy4$|(j:,icۺ;L3 rh@s}̛sPtSՃZ?DVJ,B3#B\kmG R_֚J&d4<c5UCgߓ\Lfwpъ9dREY֢콷$Q9:3SuI/Zc Zdz=?Z[kex<{\v_]׿~򽎈CSkZNM<v?F:#Z- 90U~}lQ[U:FuUֺn1z5Hd@@gt1KٶRN؇ ,ΔeYhR,E4hnGGj" 䪄6T5%!DfݺuYrZUQ1mk.r[K,,9kov5jGY%τ}LDR! jkPr;%qձ1`"Y #[5N4۶s")C4=X (:; ƈ)" I8@ тZ7Ӝ3Q% !3s"aF1ݜGm¸0H7A4 ó#b)̄u'b"D^J 2㲮8ԣ#6L"N  !j,n[]tp³ă@?磏(fg iL."vn'q/DD|V@߃K>"Eֈ9g N-<3 gԣ${L"a!@c_)%Y8hncc~|}c21~2h岎1t~DUs۶RJZGz?~p{fr\KfkBL),)%X5摮uԚ2 у3Q!@;$8ps0Gj! B"h tu$pfZk;#‚"!FRd't\ȝZ7;[=jct0O J^@./˧ ai/Uv)~5^Xms @Dc?ucI!}>,ƿ>TA(%SZ[k>x\뺮Lm[ݏIs%-f:l$3[֋#Ƕݘ c!IQH/%hMKOOU p"z@F\Q$qS]{k)0Eco-Kfȩ)OU9a(L)|K +`p6 P$!H.$8gOTsCD,%rLM5iN4,QRO4g?Ζ0uwf8)KD%Ĉ)iY!=Y=;~)aNrȹo1xrJ{=a]e˥Ȫz Zޓh_㨵rcIf<4g$Y-]o۱Ij_aMLjg x\C=!P0ggd>h 50C0p`r!Fwp8g0`u+8?a'!0",I8fx[u%{ r5YbZI HzuDUU /_TSιhlֺ+1zS : 1fփ9 ٍ$4gJh႘Lpf7S;P1RxG*goK^# u ma-#K.ZkLCDY{\"kD3%KzD:cL}F`@t b^E{C&3sjd`rB8ٛfv+Ddez'w=V,%2kJ)v>Rᬍu0 ad9,5?ӸL2O@Wu|NVӈ4k ~ΣP3Ys#N[PR177" #DClH>Հ`k]o7A'"ʒtZRz-窺rYjf`M-oe`$qow_KVaU$r[S/zcF'hMgj@N"1H2x 3KeYD(homӯ1* 1ZkQf!~y; ˚Rnq*Z81R)Mp @ Tu$?ZejfЈ2coqAǷo~!BuP^.ER??~Hn+'8~>FmC[ƅ%Ni&tcdt%@w,p |0.[ ΜJ X"ݬ2 $:Qb0 Z[.kK#/s{XZBr ݅ XzyCfiΊ}#qjyeEwOݙ#=?(ueY"Cc"2#G)eYǾ+8ӫhf@7 A^ 3E7`IK sj%pM,)P}tQ~Hep%Um:b'cfw~kΙȆ۝HzD]ttс 3JF%cJBF?s24}|p2ĉda#IU t:X#Z0$Tbq襨ROi!Ds F6=ףN6GS>sڏq5ϳ uD}sg93yG_~1ƺ?c]xUQ^0PF %+N_΅խnrYO::@F 1/@hQMgOȻ)ޠ䂂\Zoa1y;ѺzS* 3Ӓ,r} hf:yqDJ )'HwF U>Z B;(0k]\\j ܀a)׸Bt7jfr ɲ۔FٹDۋ[dGb:7^ΧFDd 5Bnf*%56ۚ@n,8Sl-~}{̳7ӰN]]qԖsZOru;]ގJY#%X@Dp6(hI.ڎ @GNhUz]},sY}gG=RD4)/^m3P NH_lo@DF?lk~]Rw3Ki_%Č.i'4Hꀵ~\F %K1@B5 p&ȌnȀ~EDy9kޏԎa#DaBJ0N]|Q2M>H9#m]z[{wE"vÄ)tǣz9 .g>֣:1^,3@m[t@R ؗeY( Nuuq|re*v/$}~\0%!KmO>14bNcCEz][{շCDR˒lK*#;*YD!@dوʩHSoH~8Ja[$@HAZ bfҎ8(xDDGOtyo ">ԑӈ@rep% 18wQ(,1{`DSwJpq-~ &9s;>?}։`Dp,O_ϒ6v@",`j0(?9+Lj?(片Δ!tٚ8+{8Oo SJnyQ/Bp h!ڏBF#::j;ָcZRjj_]$9E]j_L"}jKYD$IKJ)>q- fmhvg1 .OieUL)'P%2 RD񴽇}H f”0H3S,"՟a }ێ6ƾ]̺3b!,)-K)Yq(ٿ@C`~}!AuEU7 #Q˲DJ նm]E޷=qT!~^.﵍c?:t6"zhtQ[<6&Hオ,_˲Znbgos{{D~:jmD9RHDcqtxkx M 2 0mԝGP[/ #cK)rG0xrRO bte,s :Zx ۺ#Y9'4քX=:Usiu?EDjGSswNѳ)TS}^!)S 㳪fϮsb?z?qBG 4 ׮(jE@."LV'm\p 3tD ")fЎ8Ķ3%2%~X.VGmCr8:8U~~*',ޝS"b\jyޑ&D%*tD|hݶOAu]?RZaNHvԜ믿?_я~\u%jߪ#!\8?c )qQ{}/}Wc0B:81FIɑ^#(&%jK){ɡ'&$V) Ku64G`΂-`QT~Bs:P\B$EBYzWΜR 4pSB &2[S[N.Z+'FSlFG'vh(!k> _߾ob]M>ڴv ~&0a;3}zqAc;K!œ~<IDAT~c|һ&KmXP[eY>~}}Bs&0Y"rYG%XFOiX Czcf6THٓ:,Yع{7%1Z)e:~KE2"v0!8ufqwG58!# $ ik1O՜nǑRT= w@^kIXע[jD4р~\FfUBsJB)8~칬' gA[l.C? 3pPx{'Sjf1t} ,ci)U3 |9WIvs> " y8{_1{2ؾu6zDJ~*xʖkc^$r,Lqlor^۷į dfv_E$6/_喎scć,ϟGsD\ŽzVGǏ/f^sArEU=N&"֍D҄* 'D %XNE1 ¥"̉EĵbS {3ގRJtKNHbC]3[ZvT-8AOL쮆殨6:mhΒddE#zE^:z$),3In:GZ㮪fQ{FՇc?uDFx> F"Dڻ-lYϯRr)JeYZkM~,pz{LG7IiT tRz3/B"6Ѫ'eYR*OBL$]x7ըxLRMzh͡q؁9 31Y"=u0bJzYBa4 W1ϪZ;JY|D#6,$r:P%͘)Frw15͒PuD":- tXKmSJJ)1#X}/)dB2b1_ Pp$, "tjL1ztF$0Qh@9ݻ1Q+H+dDN71Sõ;x21G! \! i.ĵB9jg3!?!wUz[,/HqL9TѪ6F_/r)EX?H0_v l.!ږr:\~a[3Bo#k班ݚjNk=bH벤,IX{IWcڢeYkf=эkY afdz,)u 1SI{qp7I 2& ^$v{}]eab"N9r.%PQmo8DvUD$aOD36,, @ʲ\޵Bf~s[W156qT`6htU݂m'$N&Gt5 y=wS{]Ǐq|Ϸm[~yyc,63˲¨PSrYIZ룤e-΄At(!U R HfD D:X(!%D*3k>8mщFn=S8c_Y$ &bB6fњgւF) K Bfh55誣藜l B'8B (9IKN) ? pYXBo)3?cdD37T: Ĥu"R)O*#i&l\UC4GAٮyfi<_Ohpj>_5OM !A'C}Ff&Xfӈ:"h;bM误Du´ՖKCcI\h{<ܵrF㇏D,c4f~ܽ}n//Ƕx[$˯Vhx<>~0`b˲nۖ fx˺[! Ĉr$chA)ID`k/zYrbYHJ,\0SJ"Dr攢lny 㘛 'Dd^t*A$6C{6z}ߘ)ʄHa#BAHJ1P">Z;N)岘}eQx?VmcLxvt< P O3)@,Q0&9KJi߶r,04oooe~-/˪{335UZcH ip`9Dy> â͍p033N* C~x Q3wW &!Ue%g1DrOk-HD|H?]nI^IJXrn=c{ ݐ80RVοsTvCĩ!R7u5:YO+^A<)ؙ8QyVs=MgSO j=F{C) %3G??2&"Rvn7zEpUgzXUR9zD&~{۶]ɼs^?Q?|Gr!X㇏^[\ڻ#mǾ![NÇfO*$vf @ךC)dr[K),Kb"X2$YD⠆,\qX|ch::)[Ƕajf]Unc[;vlGݷ][OY>|xvԸ9݇a[ ]]sZGwr <"sFlfۺ۾caߎxv558Z5 =a':£.„8Q1SJ!Y>~2F?z,e}<h?,Q?in;,EQkzYRˌFk}8L\"3=sN)}?rcú*D;gG;jѓrFR?8& D`&mMՔ mX3>@0aD$DBAǘt-sUl{P2TUѵ{BS#1 nnB7m_&5۞Sc>X6Ov[9ȏÎk=PbnR2Wb)n_nfG>ضcUb~FQݷ, 6fvSf4$}t7;m{%lwwqvJP$F%QBY"pKeFh6\uaǾlt6w=>Zmvhq譎zk`~.?}jR:h $TcI%x't>. hutGC"IiWz&vb8\ O! 8 KsI8|fV򢦗uAB!ߒp}~YrAD@Bt^kke =p"4!0ch'H8m""cA4fC?+k@Ŝjpb#f݆\%,~̩5`fBC?$)(h 9G™, %V[& 5I$1=JAwrAow~3;#;ւdj`VL3~!J)_~c; ΠnhQ-8FןvG$fTZS͡0f#cc~+骊$ޞj0 !2ul܆;0a}~!Di8U$`6HUڵ]/kd[^{KJ/KӧOHJڷ_*8o˺r~؏HjHum]%mvo}^||SWh.G6b)FI4‘ݽ:i朄Qgd,Ъ5j>cA\{&1 BeROuX4 Wܵ!`sʪĈy) l {c{@,[[k9Va +#yN) %H[5V$I  $L$#,fVS̜Pdy=g%'9V%>kޟJx;p Yy<=oI)>'O}Oy8׳Fw(~Y"(^<.j?Hl@ݡYhJ)|)6^lM tn~}-K,/y5ffrڢ4w w%5]~Q7?mD]uݶ .K4%5r{hFZJGYKWns*_NTfCwSV[zYV,/{=H[4݅(k'ʒu+B|T}ErkxR4ʄj%"1ƈp`Cia!"I#"~3m]Rc,N 'qobS9:Z>)RB-KyIF܇G>>PWhkL`'M@Ͷ}O,ԥ_}y-b/ZWu~ߞeV1^R.5]3K9)ZŘ`#S!;R' TGP9""2i0H{,E')=I9AkyJ4}y19({IRJI wȓ t ڮ5T^.Eo A+NFơAs+(e:~ؾ`GeHc@1L-Ą03PabbU>o_?f&\?^3"NDO\a 5sD:?#iZ͐d L<%@>lw01Skm$?Ax192qƑƙU&½ֈض},F{*)cSck&eY_~Q&ֺ.Hz<8Tz,5޶M^bfm~t]_,߾|Ñz]1Iyl80?86Pb~ov5].<A[#c9'p'a˲$*OjO,?,RV%)$<ӯ`:EqR^[6Al$(wh <-KvQ 9=]D>|F^?>.`5ևA˪G^EFъ.S|[T0˒9I I@hq"hƒ.}^RyYںH//F"I}eA𷷷R޷c?vѕ_?| m;FdPH|<(7咄u$F`ƜQgu"K^_<[gf Q[8mkk-:³Tg1j?jo>1yuTg !(GSHGca$NKVt$i1t,9G7C{އꐔSֵ5hT@Y*`/9/Rk"C&a N$RJIMH.///em}_Wd90 ĔѠ aDQa"I$DW_rb$Ȁn6ƐĈd:ܛ%'$Dbf*"B.k9$G'hK!tH t&-F3Ps v؞81 ng؜14Ͼ'⤸9cWus#n 1tȜIB}&-9r}S,D94nCs*01^ޮV1la*)'";v|譾 Qj߾}{}Xk񃩕R}Wwbz{nK=W08^ntY/"9>8b߾}3h}tz8Q,?R,rUO>e1\oID *.);zh&.„G%8N x~XDbHx#$uգ>~cVkxL͆đ$(Gچ?`Z_^_~Vk\.CZo>8n/{(4zXGTǪXjaHo@l#8!I q!1Ǐ/_ۿ|ێ>tR$-~ eNk>("@aŁp48n`7탐': ygBF>%1M<3@JdDg)+<{>zS\rb 7! G Fa!1P08#"PPEὬ%Xњ$&"~Ԥs:u":Ά*_9xhvb BH)A[a~6|5yΘs?ٍٛ#IhO=OQKўW ;8^͎VDS.BgFjA#8rS#`]X@Fs34u#Or4Q|$)*9TVu:sOWUVVD($tw3fNFVLI9}0>|0v{mǔNZULQE~|VwIL|>ZSn^[d)䶽*EK=ai0x8v+F֧O1׷"sJøo_EǏ)!)yJ) }r+=! C/> jZZ+KotήtiֺRkiMժ)+D5һReI9#D>4m*Nǥ&( yeͽwC�b12/X:-yM4aBgf@{-z`MؐAIr V][K)p4aGH1覫0d8t7CD݃滴@d)jg Oh-%~<Η˥˧OҐo[=zq8HmRfqt>Ҫy y~qa_jDl?7Gjx RNdpGVs3Db86`h@"}ȉcî4;0C `E61Hc̑)l]* Z! 0Lċ9gꭢ|p?BS@B5-<̀@aQ\jjzSDɵ{Ty5xfv(h!4 b Ć1{9 Y) sB E HU"CCD6akmiNӍD J T ޱt-N#^Wqb;цߡpC< ܉XG_nI~"1\PUE`Bv\{ 6?NSQkM9;[wK'FpŬG>)q~U)Kbw3!ˏ{oߍ80o{&ny(M[ku؍l~U>?aYıM/L)祕חRp:\9HoinoKR[?NR[w Lv\|`΁SJ<C1Cډm8!k1m nDIԠ65b&R{MVR Rj2އKw!VqZ9e5+wO<,[9#XzWեV!{GZQ&ni[tS-uCu7n78nnz `d9փ-眇X[։YNRJqqB1bD8ڡDFB50!bY}~䋨+T֛S 4 *b`%~oEMaM ~"3nf)Fqc 1FiSK)i7[gˁXX56*XK9M2/AʴZ*(=cvOu|dY`nԱɵ^XՈh$BD8#BVGhtEX^F٥n܌1[;{^ߝinpx XoW9"jbŻ,z$2[H jő"s 4 faCa ՜H]ZJ&WqL)ڗRjq9NC(dm6VLJkKUwpKYJU ~oש>cN[Eeyig)r~8}7y)p b9@VB!%!r&>v WL.j}*}e15]0{ Z N9R!2_fL@"b@1*DZRyja~k8˗/{yyvKYQq&wp+\Ϥ[._7֢q]M)ZsL*-N9@Ud)eO}[o,XJ,eQ(]ޮ4 )gt. NK]$g'|PūYNBK٠[L)=} `""qX[t2M]R`Oh&""x仡)sbF?FRWZo9ETk651UwfF8Bti4:_;ZkW)A[)E$~JUUVi3l7_^ }dPN%uVw7up_ ;q_`p%XMZ=rbbzg=Ç?~vG!c22ozb2[D7yW0fzV]clZ8SiS@2ecK+]RՖmz:~m!Zm`& EUDNSkeYPkl_RN QBDrQJIHwpH‡O߿G!PJDs-OOOf?1j9E0u)yz;V-ΪFfckbb̥u1@ḼD\5!V%6>|]84dJ@Zu1Xjmvmz MO>٪(qЩ,g="e1矾EU)m#  PM1Gf]Xٷf7S(tm$7@AJ)QwT!Q]Z}zW RJG3bFSm@[+BN-6*Ÿ%ڜq@D=n6?K)2"yZV[!+4mX ZjYH+Wywu0k[?²̌8vɸ'oK3Q2[bN92/RҦ qBї 5 ?% m ײ>qÖa ]9GDUYw** UQW#{w7Aj92/)y&7OP=o`u0 n3|2cqq@*Y1(_YZ@K)nU'mDbs%`l4,X#pB$Q@U`)0s8-k+)0xJ+cΧezoo"퇱Z LK)!Ů >8@R),p(v}}K8wiWM~1^ )8 )7Pzin_oWr<vp؍څ)E(pl<S\@|׮c@~a0! fN%L9"8SѝAS !0{2b 5R5!ڥԊX#K}܍O4x>v4o_)NeRayx^KiyD)pkIH;kn`8.Km] TZ3&!{5 $ ,:lHflHѽ:pi}-u㨢JУV{oMOffjewV`w">P Έd!(ʊ D:6GIJHėcJڅUBLNL1!6kL MLe01LD)"bj 8c32i QԴ E]ecѪE/?==s<$Cj.a.n> _4pR7C ۢe\N_;\X#Gf+=w$$X݃w>} Nl]m&bEsk?D`}oA|M] nS("0x+ V)H:&RvDd@~̻q֚Z^ǡ{v Ft^RvvYJCy8}J!㧇iJmRNb`3z4{Ov8Մx$,ŌL`Fd=^HX׮q xn491zCL#iя)3p8b  -Ƹ(v{ QHx۴1 lY<+1ӴQx4a|D4 ~J?^_}وqm}U/z԰H\k a֚IW'´\5ֻv"6ddں40st PJb~_+R6a:"7f:1 ~+n۱zf5`?bL(+=Y̞hoJ1`s .q܇e9'ۏn7g"qp:a!$|FM*spS9W) FF9Zjw)' aDO+@}s!VU-] f"^̘ K÷/6ZRn9o\O_zKC>ooo6]d44Eq3F!YSJQQm֌)HTZ1 &5LKD]e%+ܝ7|[kL]1RƜ_ bT ^J̘?Q}~kg?okL C(^iT4[7ȫL|z̫`"ik)K CRDQ9\WQ!ӧaJ#ZCq <R}Lx<ݘCJZjDnz)y^"׷V/˲0Q9,ːq.NarOr/Щ(o̓|i) ~]U#;XDB1ګy {޻vcp8M:"”ÐR]?4 ! ŹҚ~0?GG\]n)e1@DcV*uϭ^m70R8nب M-gyWU{kw2oPm`5=#+} 0c a/_K)yOӲUCJ}p#V~yB.! \Lxnw -@V|y]A9pgL/)tY}URJq?GsLDPk^P$rc1!Jv^ĖR ?N$L$[[j$_itrH$`btWlgTJ$&͇{ݭ?x huj[kG7P-=|Y4n ZUfx'>ۀ#:xbݡu돩*Huٽ$hy`@fJHjXs`M"iQpXE nJ,1a 6_fK@˥~]ugDn9J] ^.5F&Ɯإsi qe[։B)9|{ywoa2nZZt}|\vݸey|x`?^\(Miȃ,x4qj&֘bCLzSĐۤ`DqU楄[_u9?RN[caqHqrp>tpSkf LK)Ds0itYր81x#SH>r1e)K-j)e4tS,E糩VݕanZTMɥr?qLB iƀ1|\fm;- LDLz!S(A)p C.ݦ[)v-J@ {ﵩt1f.QeB0mS0$G)AhH{S]tͦ0wzm{oSkUD(%* AAi;tG}5.j)%jpʎ+(Z#sP@QCbCښr|4%yv[9fXelL^ ӽ,‡?e{ f"u_PtBWP:3}|5֧;F |ZM9>[wF/8ö%|h ͇mw^;ɷqm݉@FDp_Q+b7&qbv\=tl^fb.rN!#S`90RHN <'8s%lJK߿wҪM86w;J)?8M8ΙH̜bލn7{9^^^<)0x CDcp~xTUS@8L[_2w1F楋4pMԽޜrGDr [ŏ+))x:Df"0B[*q4w&8cD(5NTADn v]`$bN!~>lI ! @%0{H/?,C*n\ZtH 6o(NڰA)e_1fU@ȑ<׭f /s# uުnV[SuP [fѳBi7js- HꮚΩOKxАԔ}&Xߊjy~~&/RN69"E㶵_ sW[+Hv~Z|͂!{1ݰx{jkbDŽ5w/*UY7?őq@H6g'/ ̀,.+S05[m5.K5FcivqːͷsJouYfҰnɌ qLKYJQ?^Zont} iw<^oǧ'$Rph9CyZo߿z?Nn?ͷq7NBHeqΡr=641DS"]o˂b< PKs̹8ܭgFr0fR#0 "՗2+m-ZO ]s|||}|u9jǡp4֚! CFe&vnmï6-8~|:]ݐSiae[yTpH)C>R`ݦ8wӏ> R`79T=`SΙ|ыk4512zCm' bo;L޻V Y UF BpK1Xх}y ]zԆĞ8}!Ep9+"'Ҩjin z75CaHM{k< #-1 ZK-*1ōwg|BBZZ0Khղ|ɫba%s`Th`:,LN_vn柼tw{_GhP.6D~|ixVbLn 5Dk$0u=5DTg\QiFL,̡uqs nUTUB!b+eOO1Hzzx<뭋G8 3yYeQ"˲B|>] 9bL)Sn#KTvq9 ,GMUlVާeqcv۫YvZxvZ/g?9\.f6 ~wJ`)%0Mokܦ9;ayY Ƽ;mK߾}o])QMC&Dt0u8z[i2)N)/yX*]Btnކ!LNv&3!Xrjʲ*qfLjxV&?pQD0q^H`^fk&Tbo 26DN_{򭳱EEjSe]TZYw!&6Q)C{"1s)EB)&AoME̳ ~wU C@̗B2/c*R1]Kmm.I4n|UvQ4ʙRThzQ 5|$n\D(*ze }-߇aϵ6K~х!rve8j+O?\JwֻwV77îLSo?MrZqKY?`D)nK7RJWEn қ4&D0j`kuM0 4&㑘Eo1%9avJm/oDJ16qQ1+Qvۭjf1[] 9-Ƅ`ġz9&0SE[zD.ͯp `U*Lf׸Yb.N.>4QuvN]sFRdPDD9Ĉ+޻ikC={6־gtVJBݛq9›i-MmZ$%jE+[ 9UHh`b_jOv9HKD`8cYw)ޜE HVC ^Bi}9*ne)LLh.X-5W tUm9:3#"*!lޡ}?UV.5p?<^[]G&ىpwvo;n`OK!p1lj>"xUjN,Bm'Lul۟ฆ""f^B40@!}pAtiu-]S>a5ɘY{&dۅɔZ.C Vk .pL!R[SL|8R1d a):8s_t\n1ӗU;Aֈt:y]/9i}u΁9NӄLK)9 х8v߿~j)j6-S`եRD1Qn범RZ'q^/BuAP0m\% f8^.7d"X{GDgBWN0ͥqs2y~Wea.SyY6]c?~23V ǽRHT]%k@1E7u`UָD`kccN!e)+Q3rlAU9%`H8cm03(jobHVbN>ú C]"B2.jm6rϲ-?FN8rnDZv]ơ K[ށB9 뱲^ܭ[vCιA$hdV$ާӐ^{4f^j3O ̵VU 1 m/DTSc܉g RY'AK+(Y"jV/;q+L \ksss>Rzu(8r_I>ݝR6y*}tJ ~K 7t뎉)a;wK;5jx غ`~k1Fb2fj Ff>:k-*?pn sΥpvm@-$IPb*x25"vCZkM,;w˗?M$`)嗗%çq߻~vv 1EW9cmx:.ץ,10r)*9*@m)PNp8q/L: C@!eB@"Ew_A aVtMzח/KDt:?xrg Nߦ)R*z,Kø^oC8t<,0dRZəV05.U3xgD@VC"9Km"P︁q,U~IZ"4W%H""/Kq`;қb~-˂LX5?''C<c`@LVHLԵꮫ?]Š޺sэFt 9i3Bya,) %g9v\{S1V}q?|b} y"DWD m҆Cu}:@.qtITD5hR {n?BB!e^ v;DT|:ib???o[k~9hf<ۿ-˲RJ3bL!<ϭ q?u׷WZmre1|ݣ}{wS,jCKq+wO^J񋩫Ɯpi ,ei]I?=d"ƛ;Od~0^.*DD`*X'վWb1s@9!o=D@bfCmUCUTtM6ƀъNmz-;j,ād= }%쀙OVF$Fo bbЭ *>n8lX Vӷdb]~`z=lߍa[;|qfjqwPwlk=䜹ٟ+! "gJ,4kK?#|c@X}"䘶9j9Uٵ6 9 #Iw\[8d#RһRnri.+-Sj("}!U$yKK9 UϏO1 ~߾_~>9?r<>}ׯ_1'R<2i{Dyv)N|=a<[)xeY_~n|6ODDӼrJt^ "#"p` aѐinpc@TZFv^)ITC$^9kNcz5] 8.()! MMOf]_pb*L{뽋)$UumiÜȌ|oAcLt70ZĄnd2R;2&bVѬ5[YDFQaq1ibB !)j.?yaN{ܚkѝU}|mC؀pnBOGxXya'{ sw 41V5/eG!ݷ>n0`9]ɯZ[u:Ø #v# 9_hrm)EDƜUk"^i嘘?=>D|:rJVpn9?=眉VJiߗRv!x>v"] Oz\i61|!'U/59|z|@z^k9= *5ѥԔ3vy[Y{?Now8Ci(eY8D5<π~G tXjxZV0 \Db0.ڤ-Z ̓k햇{nx)OLDo-VsmZD"֤|gחO>ɧϷ4XYܿ/ipkq} >ԇ&|0嶇fsVv2y* u$h0CrC"^JnQptPڲ,9@)"n<O33YoR~.b SNO?X߫[sFd4Joכ>ϗr䔞]Os6s:]_8nw8R~xy{}q,[SZjLiFD,]/1'|^0:nsi~.Dt>Uu. ===Ř#"LZry{{ !Or]}.ږe&b[JA0-sozvڦ{Y/_~jeV>eY^_~p>!~_iYVK aަۍS !הS)SJooxey}}}z8$GB-C1p_. OJYJ-E՚ZڦiƱRR+5l؍9]v8n$ s)"b^?^^CXU/MKY{RßS{!ڐ"] >òCN~!ihcHyR3cIff0lD.߻բ!zL3š"!jJ)}Бuѹ; rֿH]Vph^tϯ5!hX}OLġDOf$Q0  E}<)LnmbyUށk)GK%Mٶ}03$u5T5uFUsD$` OnCk]aBmHY@O)!3Rpͼ{Tda )``ƈezϟ n~/oeUG~/廨X;廜OLw-Wwv, =~8>%c=a@_@n#KTJvj9PJyx|a1Qe82/ 9OX[\n|>ߏәS+ "&bs->ʏ?\Hϟi6ꡄK0D`xʰ9Dacz.kRavHZ%eYCcle圿~ja7 .u/0O7f~y{9?N̼ZE$Ðy_\zU$?NZ5TJ9&ߛUz&&"]rҐB!FXmݧm&֍.H2!2#$He/ f&*nIĪDƑUE^Vך3 V3gaʺd7R 18G^ONoRDp>L 0f`k)n5[.bH̞y;5QՔRIL5]C@b6]w]ܔ QoqҐ3!䮯:M!;VsGBg^1>BOphM$4oWj=#w]|}CEC=LwF8œ\B#3{ω5k$]/75|ՑuMDN&f }{^2`@LnÂf``BJiH͈ô)n]VgAaL)R겔8JY]ќbRaqQD8ӧ'bnt~8^w^b(4cL~-ŨO?_oSzϟw&xݮ׷":noFD)pܮ'0uZ  ,EMח&ۏ{3ā40[kkmw0˲sm;taҦi΋LӼ>==Z[kTD1ya)KfQDH5B`ObN~<e1o+BU~Uu4K:xnͅ+vhR qwsm3?~V!eqNm!܏eY8&@܂]! ( rvYw5ugL^ޞ?y^zkv CJM^j&U.Đb!ӧ?u]5%0ׯ_/{Y 6^~}Joݮsr.A@TuǷn,p\q;8N!׷x< lA.RJ/Ӳ,Ȕb u%AxÐcB5ZkǏ)R${h߲UNӤD4T[Ӽ !]ox3-uwV $DNyn8!D1Z'In^X\N55B5bC Q3tZOq#6&v oV]4sj"7333O-Hb(7 rI<9.U_SgeqX|ZjNzC 1Č@6HN"b0Kk#,Z픠íOO6@7mMa,1$،dI;ԶOj7 t?v}n%mxc ):ƝGDN ?PwٵzEvWoz*[rx!JnQ4SY#k))5ʙ!@c1" BfWW"B1>ƽ ʼ8Dd^JeƜ`NSǠbSt<<)PDӴ,nY"&OOHO?=[oVG=#!^_^p?^U+X۷oo?ΏӼLo_{0ր16O>yPjE&0M9}}ݍ4OϟӯZ[ 1.ۤfCz),{k!"uMTUb] t'ux<@ò,ڻ&UU}{{!nckp8&vhHxؿ]=M$ȁLAlf NؠV`ȂR h Ž6)L\[C q&[*Xog` /.FCDw RP "aJi땐Lq {Bk Q.n)VJsܿ+2xJpA*`ug>iYhI lfX[4"puߗFH7I)2S]DW<@! ψzg*,}}IJ_jP#׸>;ë łK*mf~ ^^< 0Yw'1DûOܳ,+.e@0e"3%D&϶. ?1Ey],NC1]bJ҈v.6eh_7D ́ ԚҐsJ93RRND.,F"bղ3;`+vpےV.2MŴ eqj(:~^}=/#^ѮZ?dx jIw\ޟWܻﺿ1mٮsvb۟C!dy{l/GED.0"jj[JC zWi0)Ҋgz!CV 9^/fmooo qK)p}kU7G;N˲Hn\*q^SpTx#b$];GoZ<뀻:-!&nkzeY2a4mh+'](ٶwWxCZ߉uYLdf]Ƹ_khD#|wj[½];/lHJk55єJnw#=^w} \Z1rqS <PvL"+"ssή]{mz?q"| C@lfK9*4ޒTDEe؞hWd+ `"DLȲYk0U٬nm(@xyxlcv@$p0 Lj [Jgbo ="Ci;cmYf zӐ9qDfS^Y\MKY<9f`E jfVc0%ߡ6>zr}?돟t>?`ކGD;kL׿%,a#qM5+,C* Z) N |GV t!f{2hRG@HɑH`{mۍn :B(l.6.zR?j66~.k'|J֣3amǮGw;EZMOD$F"]zBH[=}6 뭛)6l(zPrdGm ,G޺"F43Ю)F"F5߽B`3C&*}MÐy.ei=>Ĝq7eYB8~?_y7LZk6xxx8?JMD>??Zk|>3w` Uwn{?N?Rz;N16;"Lҕe)):p88矿|\86_CCjmK!b4.jb60ܦ~8 x<-r^.f "]Z7d|zxzx8s昮}J^rx|zo}8D^~z:j //VO.Z*ؘnr -$ֻ857jx:0tZj3y:=bFV̋2O" Pm=D3g@d֪W>YU~`7"Vp':O-Vi Eg̻ӛ"3s;7e[* Uϻ W( }v*[J/W9Jj.|siHOo7;"4!5bf)H,5=J)FZ$c2T[\nLAZDϟ.ۯfvX{{[r:h!!m ˗/|K)9D4E0t3Q'DPreܮ1 G.ki[6 ;Rt8k-n\zO%cme^o@1)rHcF0ۼTlR(].oȁe)^^_Ot\.9z3k-HЪaԔs Zalvinz  ZMqPAzO7|M>Zu#Rk.}CͤIfq sb8R]$.R{MCD)FD)9cڥãC?W+2cN ]g2Z2 kTjbvJf]]fј)n XANDNvK.1D5,hITJ!&U&ihxu 3iꞌNdZW%3s@fTvވC""RR/!=g.jR&bPC[mZ2OSJ)ĔbByaH)/BN"<lA"7ieO.wc]`wY$<}+}y?|qpއؤwxGy%7"3^oaݠ2Pػ8[*sx>>L*"1%&cL]d%~ ,f)EK6ys !n\ED!VZBj$; Cƥvs)^f61efR;k~ jN-]{q?:EXw6=C1ZJ9`1E&}0:P`?t2>==}R{!wqad-D"h+-Moqc78sD!d&&THbs\]jhmX<cJYa^)ڼ2@ULlenj5TVřu,**(IWi7Y#"U'9)@#=Z)N3׈圃x p D"z`)"HiA1Zc;[p)Y} _T zt.%nZi^q?JTUPI)6QC X#9q1HK{h0dϟX,0 pKq|!xhzV+Uktö1Y]qUlz꺡8KÝ@<>|aҍAQq_aD"܆g^Qlm;GrT@Zoм7k,̇>mHG4dw^kU:R11q ;c*V":)ijq[<_j<___4M_|!Z+2zYD=rY0l]{q[]z~xly Do720"TaʼIESQﯷz81wi[kv:>nR44/]9qzͦt>_?Ի\Ӳ|^{asKS0bB)6C=d&9g?DdߧRcy7D{98RUyTC1[R6sm@t1a!;W+.Ӳ֗,f9g 8FfYIR1!bn$Rhj k}uگҚ]ZQZ-)h[)FH1"VMWr "n"DDR!7E^RPU՘Wu}syKOBRشD5ߙ? `&ۿ|A$ar u7!ySl͌ـ ܜWmg!m*/S8i`@O(~5nWy$D ev3Mgh827E{!ltFjk~Ҋ 2IAW/"ۧUKB$$qgt0'0p%6l]ܴ5mrWaj!X^S-1B?x<ǜ)r!???!Q1=eo߾Ki.|~6nڻO{BH),>߮Ӵ̵qEo1&^p{omM_~,sDB >mpP^V0niJm`qHbىwM%8OsŕJ?C׷r:{OG S wRD.mv6,i΃ C?~xx8Q4-]zkӯ}]b]t<䜧iVx<Ӕ ǯp[ ݐ7[s$94eSU3+dUwUϼ;uUU5xpb&9a$y]U{,fV[3s*9B׻ 70s|޵@d3NU$)0$-& 9w%eH" C I"jdqe,Pd$O%3ںvE&'Pm1jr0j:H {)cį\=}hFUkS֭7LBL2Ma@ss0!E G rdDrue23/߃!`J<>f>/cj?AdF|\UH/ i`}#M֏N%;\ȑ6)!؏/[?~g#$|0G o|Ik1\x]m|z0s&Im_.uyC=Ě׷'fjn/{ϩz|>Mtj4~0O￁rI9y*%m4͈kJǟZTAMclzgd[Lt9ֺz$OfgɗoJ,e%8Yq2^[`*Y$ȩ>qY(Mq8`N HURW"!ġ&27]UP݄$.qʘeG)Y"[5ҒELG< Y,h$'d N" Ppx ǵDdH#h)]B3Ў;{%;!$ yԠڇ_U阁3sPZQRҮF[GD$"t,NrX]6MC'ѾmS4851Tcl0cfD W𮻹;921P|jL_aD,=4Ӝ2BX59 5l\,N]lN ibݗ-Oˇ׷/\.oϗsry~zz{}]?ϗm۞jx}jOfVJ3+Ӵ[N%I@!ʴЧ\|us_GqSΧi[׷VSy}{i_VtS6UpܶM$1rMW?GUyr}NӲn)zZO󶭭2M.o">^W3{׶U^׵?hHMrss0IlcK~RےSD|Bnz-"Q?rNiz4ͷn")gsΗۺ#qr)IF59 |k@LE8 PM0;}M6003$*|>l]5DtG'INxTla-ȳr DJt lh R%L9s8RJ,,9]V0(>{kNTQȵn n/<]^^^Rg{sݷϟ?ǗZ7ò,-?$|6S"ϷmwTڷu۶ӹ9>~yyi!QX QvS痗d]݆!%AZkk5m[ ׵Vrt/?Eee  sk{)4Mr\DJRڶ~K9k7f]Umöל~Ħri/"DeYΗ, ""akun@2qӴ%IT# b][q/Yz_צUsJ)GjHx-ăǨZB aj7f~0]Hҏ1tYqݏ`zL{2%@*:7x.62q-҉^psS#,6 Jh9329^e;)ںnOt2ӧe[)m[U˗τoۺ[)4͙Lrt9"Ï>}bNMo"뫹uj'圄z}eY[|zɓ>R[뽷_>~\JN!z>iVݍ u5s8eYPݘÇZCˆM)j; !V>|/kkm&f"y*붤$f:{}]q[wf,B1)/OoooOg"ߗR|v]y>V{euj`-%eI˗91NSkjH\yf$SaiS Tuo`w $Ai$^b28}~,^fZN8Q?\ '1NsCwIoC!lŨ<(u}sxl#T{|7j2Z"L&&F;%GeR P(xhcE`SW X6ĭ޻$a&5ڟ?|`NL|_y.mkœN秧Zw"]si{yyٶ~$ڧ?? =.{S9LFudQA$75i3zB]CDlJ)5^( wq+ \U$t**<=w3mhh.FxAC)"HdkᯔM])Zb8.֛;()0b1voi""&Ԃ P#{tW7 JXJ9@kJ:ٺA0q}?@H"pAz8[L]Hqnt Ǧڶ%YΉ sY=1ֈ}xbLG Axgߗ%fS9g5%,2sk5TR)NӼ,Hݮ|ݞ/v#O_/Og"콟.1Z{o??ķ<`Y/׻HZוN3?==GU~B$4R4%FAA y A!F#ݬ|Pm䄌!7:80O9%ܲ!jSp,9% K;7 ^0[0j{ԇ ӾWUMBD߂E02#YoLW771G7`[kZ/ Վ"T )&a$)NPRMeV퉸 nIon+*'&la 94MHdQcǺþ[aF7f^wd]%v)G75~0{߶MK4S  9X) ѮEO%'vݝ"gBP^Jb>'?Qd3= |L$|p`vn0v~LaPz'htHD R$L{0>G !c<"!ږM젓𣏝w%AIDB$pM ;lZ[+Syi$TR:ϟ?}<]~ǨzTUZ ^tZdf?~CrDL)RQ][\h-?PY8%'̜ry,(P"MZ{kmݶ$0ԙf!J6?jC 74Trcx>1sJ2g8SAۏ}'`w3K9!#jdhC'=%.{yER01,!zHvcv**IBȨ{ f*L!]r=(wF<'BhY$jEtú5FJ9'@ڷ R $br јL) ;T*80Sb.4%Dza5%=}]{IUrV˔~} G,}D+fi⾓} {J%4EA#S6zP 0pPOIb* D=n."(Β oGGqo{) %@w$>X\3)uVkN1jŰ{7"R@#jƻHR52 vSsG7H"즦ڵp^^C# M xqk  ސ L>_"BLV<}<mk(THdo!$BFn)/>cDzGb:CNhNDK#fs3:Cnψ:twp JzJP;Fc!L8'-p Q s"0It:"#vu)l 8q#xL|4 #4C]1B`x=Vܬҋ"F6.QձH̩-.Kޛ^OvnϧeYz;!|z\m%//?ߚi~G5$m&q7s[B+]Df,k<.Ogf]߮π(9<_N)ڴ);~zΗyO-_|ixY˹oDҶmZ޼drbi{'̉l"-ۺ]R#c'DŒ̇,,8 (UA$VEXCn<عP y,jnܴiyo5w!Gk1{K3K<h;Šİ,,HT:nuM)9:2 xⳈHk{2"9 ZPGm4b1bqԍF" C8 Dm02LBLhqZkp|1c88fn[ruAI.K*EׁeY܃3;.?Dq^|>~>]$IЁ km$&jM,D>9D^렿n73g&W#}ߞk_M9[J2SkYy>m#of&.OD,~}M)}~)g R>\^}9sk6buk3')%N"}irwI{om]HDhy4aGl;Q j6z&I[1{Q)&z>b@cƄ08`3G($`^EѐsގsNIPYbk{3@ɋ$p[kiyzÁ9rέX$8)"zBĐ2 `$zGDL"mHJ!V@G@s[`oW0y?}S$ _c z$j]^} 3GI(B̸om_&)n}cIM8Z9gt[e#H,@8ſ4͢3{7C 8昊4g{)&.L1t֬j)[%B;[QhOb7 GL{rDG{!314du]sji@LFDbSv+]=l2_>bOOo|ޖ"ӗ"2'SK9ERannjfiDmP"<=_Tϧi~z=" Z}z^ߖ2kԚU>̽n[<ݩF\FW۶sN.˾f|?u Bw+(}kn y*%8rt:Eavso]kנ1PRX6cDa떧,^M$bXaX\}G,Y AgfB| &c${C jL fZ%I8b挸~;8[%GR<2>8h w< X1%Ff)bƈY\bwvpFc1@U5uffJ%{9$fy(]0wC n=Š[eJ3/t`ˆQ5R[0|>xdw"9#P@T]p$g jX~GD{Gy&W ijQqhDnO(U$ }Fvm)>G[uM)7g DL7;:oWFy &L8$t_K‛mYsy~SnPS53D{sV~}d-;u#P===a<JΡC>).u]r"֯׷yk/u^+ iuӹ!_.gfk `M ijuO)m#`S)lҷiWoMӼ#bz緷t:Noi3!t}{> rct>/rݐ?ʩI}k̉߯i>]\}Yޮ .~/eyDoo73;_.4kIzo/˽wE$=䡵uI n (^׽ĜHm{N[< *qII% }*%"YkWppW3p>FA8qscmfy*P,eIEޣ.t r@c>LdͬCzDLHLD¼of&82{(w@'ZQ B&#^ tEG kF%'"kedm. 8茈w!:Me]Wouݪ:1m]our.ey5i]__8dB(|>Y(=mOZC&t[9t~Ww/sq)˶Z2Rεԯ~y}ysWsG)t:aB(9+ m޻֮ 3n>K7G=KL޶Kwr):t$ڡЍeYb>  "0D ǿ.v]M[8QIrZsx~{u]SIZWHR;;R"qHj7(Pf圈ػŒ۰b(=u Li-A+e|bŨY8r#0~aCyNBDb*"]#N_iʽuYNSoܗ]rܼkkm۶|:ݮS֚nvl[붯I:RI P}Ywݶ-je۫D^rN)Vjd/˗/ ٴc̯Ǘu5{ϟ?s:{,JL벪smoUSۧ۶9,[w75}xHڎy>! pHl!0GzKƀ[O\+pSs0fDo{-%FXL;ɱY<>!#|ty*yZu[7,UU)Pt%nEaf9Im js muۦ~_DmYmgwJJ#YDΧy]3;NZ|YX(+>k+Ll/˝Η3^/OO)Vv-rOo[R^ġI]Jv]_&e*i'FHq"0HÍ[8{7faIbaLc1ND)*cF80߳mYZEd]C byԨBM[msoML1.6XPd8,Bn?HH }Ji:͒׷׽X#0'Dx޺6aɥXQqp1'I[<',iԻƒ(j_η|o??}jr IR.붇Tcۛ!?MӔ׽׽qHPJ9RuZU䒄ub{ge]&Ȯۊ޴w֔%zCYJnt#8$,˔|Z|:4ie.{W2z_oo8Z.Ֆs^-OE$#[e~۽V44Me1O{x¨Z`DNscx 9}W9"E=j׈wT[)w?ݵ[k){krGfĬ(P[HR)Ԝ^4稨bje`*) f='a Zy9z&Ihaj}u7O圁0h@svRћ2RʙuRVV s$!VkoJyjpT˲HQȇԺ44ͪ-*~`c0X"#668s.O@FiPο'52j7R&=^:BQs.~ľn8 7 H#8BDǥFH0RN#"6U3K'Mv[F*x`nTLc EUq8E<^1zjc󔏴gPNKIFaE"˗ ̷D4]o7C6 T5O\_~}ۖjκ)i1|9 [l5f9/6M4M뺶4M)uUY32Ѳ?O>|_~2z8Dr{UDjr+oiVm '9Gn{9 ?m끐[kPsN]=tʅjo>\Mrf"s@!;|d:Qj-I)͈0`h\7}bp44=Їt!>4h觑޻f),IUI□)4&52F07tpp$`ܷ=JTmnm[ǬrRZ@G`w)3C!TTbcCblJ]E)N'̓d$ wej#0e!H Z nu-%$1D/~ #qƯc:8"hSSD7SDq!  6- #8'aX ^ İ>!֞RX MS׽Lr[\Ҷ<__jRDʴ6 oyuW@bIyKΉh܈Q " "pĵv!N)!Hrдo zo1d؝Zo]y]W1Sα 3w f"2rJ@wQoLFg#=Ԑ! by;vT8t1 $F/vBg"k!ZwjMє8 !Z pMc\3x82Z1tGldZr0 v%ǡDɧ2Gﻛ)Cz4#tCfP1hV1}ۣ$]uKI @ >YA7g1s'aK$lf aYB Œ!,šm/%7U290{ff>@wqW$`)t@y:,V[c#TfPBr%#(yDpD>s$,"̙9!й}+Sqvﭯ=Ʀf6凞ާO931YSk<##sz\m=Op{Χut>n뾇#|>T}|9uV4Mz|v?im\Zק(=^r"e+$ןЍ~cIq?i$>ʂ"J{ϹPPu$zo0zTS{)9l#}x04ͰF" a*%{n,,0ut{o:^A`tweW K 霧",D*m\"c:{8[CINJLSk꺹)!LSxͽi%mG!)S"wV 5`&2tif&& ;" pFƼE랄i_;TM"oTm0$?|1}G]U{'u% xP}; DHvHjL;3fcͬnA SH.[U[Ȓ%'5M _߮뺻3!nj`&waL݈"?u۶}/j9kڶmo$RKNӺLS)D)e_7y>II?>wz[;#ү|bɭ{W7V[zZ7'bNRrIQxN[ksaf0G@f!☀92GJIZk5G`4MBBʜkG '? v@#We 05]{ZERC3NUmR$ᇁz{1}ݸзpGlX$v5ښ:b<R;hL5D;^QHN!X)n"VmܻNSS4ш[SUcݷ#*(!:?RBan]SlSL1%J99O{/Y%QT: EA'Ю"z1܉SX_iYpf^C2䨩u^6KJ[Cb/)\)}%4 |C@x{+HLDऻBǰqfDk` ?0LWuH~O?Rk]%|y:Oe᧧~yHKnPvr]DbSM?|x^kkf {ivJng B`3" e 8¸H![2)h5 h! C]E$AO)0 2sP[ǿHK%ssH׷9'@誵R@DzooTyYsEt rmE4mxob;1 h33q3$QR 3b?Ss*) ˹ 㺭DUkm)ei-n[}{" IȾ}ߧӌhimNkOsYu&zz~kT{$zmz~#[k_-G3#IeՉ9Rn QRJ"9,"%9#qجGGu{[zW=h,v`ըLUm"6N,A ؙs\3.3ރ爀#@oȚ譆OUHk-nѮ.6atx0jx$!@H9SԔ% xծ󴨆$?M.Ș"z0)Diy"ˬ *,x&ҖőChjI13{ԛjXd$D`fi5p ( cCLޕ]X rajB͜sS(Rq3,;/n`@8B!$c<@Ud#bf"mҁ&#B0l0Ra5AFnwI3cNy*i|IJRseݖmkX:B%~L$v5jkdžC58:[xN8)h7R;oWU|HܣZ7U?_<"w)aˎ`L Ѹ`8pU5W`It>/y>͗ˇ2͙âKέҮ$W۶52rR~%eu#~!8yY|ZʹweЀg_ˇgǟuIRڵri?~㏗)(u}="m49Q'_nx01p2jm{ mG%:<1"!*;|cD/ϣfdM@x!KCP.ܢ!?9 5X;-lH(C;PpO&*DD "0E1wt$FB2[Fx&rppėR`*+" ,Rw7wC0Dt?IENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/testmodel.pack/bodyemission.png0000664000175000017500000001721212641367670032013 0ustar jaakkojaakkoPNG  IHDR\rf AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  $iTXtXML:com.adobe.xmp 1 5 72 1 72 256 1 256 2014-08-13T19:08:29 Pixelmator 3.2 +IDATx{e˞햅HAA@0ܪK#F ! kQ@@b V)t93{-]ӳ̙yϞw޹3@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@` ]E6z&T+̹ZlmzɼW-[s28m[ W,zg~{VA~&+zPR4UCCIﺆ2ezCgfG { д%:.۝Sb[ZupaqjqIa}А:V,"!IME{t `0SjUe=V:Vra~xmnmu-3nz,GY6qJl3 H;{0tOKWRPYooznSgZ':9|mUgzP"zG` `$Ӟj_ C?'Ym̀,ɾâPPUߨ  Bx`gC XW$ sSҚ4o5PuG| 3 A;^ը_;^_oqc^RPNUZ< .ko%vk_$P!ԥZz~4fvA+Dz- H;{ S:d3]B|;Gyk09 ~@=Z(wxiu 3!@2YNOI/? P?5hۇsu=ist-@!+D.>BMM.w$MOxH$mMv̜T* PGcs.sj+H8SPÔZ)TͅRi[;keŬWj 47 ]żiW[s̱UgCAN)—j qVz0XxX=a{Pz@K4uV|/KQ.fκk<.**dzgW'芡|?=h"uvZ;OӝtL6qsuFI^\5mx6ˊTlP=7(Tb菒hud2) ð3Bfp_RW:ɧƆRˣ_rUw̚? bwohu mHېj8?u쮫^ s+j^+9,ώTL ]#ɼOqo  l+/hXuu-?a@~;K-SX~|:%J(4D:wgW? k"֞](~/I_O؃ }*_2s}pP*<|*Mix欨 ähLpbGGd pj #\?]߯[{o*t&ZH}vpNkV\b*mUomP`@`vjJӧw>T=,&Ā¢=EhL@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@6 r79IENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/testmodel.pack/round_grill.png0000664000175000017500000021421112641367670031625 0ustar jaakkojaakkoPNG  IHDR\rfbKGDIDATxyԯY xOTSR )AD@E$k787fjݺlD%6򂈊(! :s7Þ?}N P*ԵVV;(^ߪ!f?]@ z!}⾄>??ݸzEn~/셸Bfk//3ׯ_7|G޸<=6i^H[+//S]ۙcدZU}?$/$2Bf>~Bŵ5Ħn?nW{! o0/U~TJ?8>D釁~x7E'y! | ?|>u}?G+T<AF)k?]aq^HB5?}'O0O3D [n0)%ۈ,1^HB~/׋٤ki:Jk5(XZC09H%I1! m]Ϝ~ x"]1<%?]Yco(@SDJA =uUզhEBDb7O\;lgM^1^1╯;9:| 5 Z3RNBRh4#c'!x*7om>w{{Z]|+fp|kk{㚟WBxvjv-D %J&Ę" wH)v's^Zt2{XI`|CbDUy޿>,,>6mO2 D1EbJ >\_[+M|K+i)wh[' kB՟>}?׺O!G!Rj)WgD{ %%hAiPea^>?t}bރ ˛8hOob$H;bL~VE|H)!aU{nsib'v} )BDIR"bobZ7cx!<󨦓KfWq!)Օ* R1&hPJIo !\L& r-m J+YrI, ,N*L}&U¼\A?ȉU5%%X^-kR #g/$gJ?$Ѥm׏>!m )rRBH J,~\sxX^6kט4%)`>i1,,$G|J)B|!* Xk 7R7GK9/$guQ*ѡks )7Ri*RdH["0R7 TSYX_H;FiMJ;vB78tC!îwRd49j-"mדX$<+)_|3 Zk )@]IB &%ADwH) RJϽ뾱la͆f%nsJT̙{9 .@#ezH[D*Jt}cZQTEa? ڟwBxQŗuTDKąf&]@JI!"2%å{MӶ-}3=}?PZ9f2V7[H HpO+%"e!4BH9@Jc]EALj\Fk]/L~x=򉲴2BO@*L$- Rb"Db#\o.?܋?~_V95CI*V_iҭJ ;q E=C) !ORrlO%GuWe%m{[Pm!(;Zn6ϭڟBxOP%aB& #F>V$t5GfT.y̩1!uDM5gT>Xp*PE=|CS_8x/ScH!R BD+ke16]Oy,"A? ?q'IZx#M*' 81]GR@)5kj*{S}b~c a}D}m6'׈)bd{k¼!B x}a˔RZ*@*omH2U [0'~{{ex~ʕ)xmK4 >B A ID$Ę>o5?عM)0lNn`%#(X^a{kc8m1Z{\ʾ1(%- %b Dc̉  Mw=QL͛7]? ?1v/$g׻'''Z1 1FʺԳml=hvP*"<>R{D:snXrjr ,7-kkHcBpv1Sw&zM:'̘rE"I9 KG4h[ۀ4ʕњ?!ŕ'x{=%1Fj#:N0Q=gt]¾DJ{aZkkZYsrIjvF=!Ϻh8S+T HNJIf[OgԓnrrZrr%Y}xBb[6O~pjLY 4UF $R*(2d4juQ]^*h2n"[6r+z=!(H)^T%]ק)Ui1AJH) hloSI]ONp!MC;D1C)ۜ&Ikk/ u4[Te1e6HG.zB߃~BsziJ\P:&w*!@ DS!ևc1_ZbX|AiMS#bMPR$ %X%%熘~n1x+ߦ; w홪;@"nNQd6TrxPF剁ҍVsCw-XYR"/FITC<; )=A*)>W)y)uGhe H1VRfgX|h^}k%<& I!DRK!4$6$6Bps|XE9LSR=K/CH. Z&!|J ./8-^bk~jZ}(6F*p )zУ%O!x&Uqa\= \.ͰcLפ Q3C²>5a{ X\tCe,IN$"̲SPo0e._~kh=cWU3/JmwRU3qUu3+{eYKcͿwpQBR>"'HI@ IRRъ?8[n٥ć/_/^4 >SA"eZRDHRӀV $5ο"ƻVv')OtX|O!)"H["RlHiB@p4tiS}@MY .P6.pΏM]]I)^QXP;!@Ber-Hh V/_)aZi|6V IHB >9щ{>J$PZk#RB Akt #.k+V}([;?wtxG DO7,R$Rp4m&9꓀wim֚Efkn jnj ӦjJR4bTW S!!.oEi_*}sQ6!G x$|XdTCm~/L⹘IZlSB)MZ)@faΘwR.L+zg.@*|=QW8\18v BP{Wʯ q ) jӒH V,V+FOf&9lR$P6'ʡ K] G79r½'a4_\ABvYiÒXs#"%& by&\LMJQu}DH !`<|Zp D C(W ԎOD$e@]7آ1Bjf/.%szdpuegLI]R:ݯTuUCvEV"+H`qvK枺,k%/OOR T $(S $WR:I)P㡴Y/z)\3bАDu1ĬV#$HHLCzRB( 1 k(ο(/::<2xED7FsEE]گ[ɍ]`g v2+it] k[tc|5u J˜@]przpP+.m}qa )Hzn )CĻ0xO뻘RTCWgp&DkmgpC/xD+IRe*gI$R>tqZ{CruӄjEc JMVzܥs;mBi; kD c!ZȺ_]b-T@N"EBhSptxϋ~͹7b"FFJg %au98u{.&MD:璒D1!rG#1%b9!>?{y?\h V4e)ېW@ VQmw6@ (wFw.m(%I1"Ĩ]TUC4a\>"jk’Ά5gMA<Ś0GFTܸ~3wB+iͻseaJRP gA@b>fͽ 6 㓳Zݼ} /n\GL (Aa5B3t B4AHڮ#lk^=$nM k zD7$$VY)Jc1"FKJkp8BTYVtK=i]~S=ݦh4JJbLYޫ8:[ެB2nA/x]UدԓՓymYM V|[F'}uѠTI;wL3<twsIl󞢙%LJHSMYb>m^5y~0O*fmB'Ĉ[RY/&T!U_z֠fRHZ4U5YS.%ZKeʧx)AHp.~6u7rչ_ܚ<\+RHeQXå,ʂԠkiHCl>]g9x׻~HSG!"~I[' 0hɥٸEoAHTfuW{$qĔ$Beq6 Ƚeݰ5kX30d"k~ U9nX.T2;ViUbGiLaL1FBH%0 ﻋ_ٚc>m^ZV0FSZ T 4 ͜}6UjOwS=?szz;Bw颞)u?# 1xno `Uc|hge]*̷ws?ZI"D7Vڒa櫥/YciHpz…|֬nzbAr`LB(D`ʜ0n_mUioY gV~zR(M+gggߑx$x_v?S;)iP$S5YW TrJ6C@ IUz5sׯ_.a3_ly- wC^c5MeH/ DBlPWEa)f4N%@ ҚĦIF(^ZM$|V.~Ua7hg>z~!L>C2tkvM Đ=V%~M }{w]\U{~ciƞ %e3AJ@t3H)2%W$$"%MU~V˹M7n|-lΡ&{F*EU4U-Ӧ#׀=R$V 9;m$XKLҝ4}/# k2ZSe–\~n֨?<7V=/$EUŔSTXch/a$SC<;L}?mMym&c^7t絔3+L R$&0F#E؊w#eDH)`95{l߻|w|RZEtR&! 0h4"R2>_xK.}k< !A#AH;L)Eic,)% xup4Z1ޚ.Pe,Jd™|F 0e IըTR:Y<_Vܿ;9[/\ǴV$עm!n CO׭/= 羬n?H+ڵن;H%Z M9H#mc@ItRݑkV"MGYXTM?xn\b$Q} Ą@cF)6BP{B7£^7m߽F]M3,;$|8QJeȹ ʪ?R!}kW7{šz<_+cBxbX(ꡪ:mVY7F(n1"'@QMrfPjͦ>@9D3|<=SBd _®Ɛ2 g1f<*t!x… /w#blzOUSXeZup-R dn_!|IJqEFDh91dpqא.x?{?v\jO>x^x87ijR2#D б#)uZ klٰ\w]y6r;xα>1n?+1N:s*gl]p知oI !$]}yL䕫eo 봯}kMUb!zLQtmգO^!Z7hJ(5ȊRb m|OU+R)|+sۣk7 GU@&[ID)s0c9)_ KuY7?b;ʜzُyՔO]NNN\UW1nV#5 J)#:,o+bXSWB闁wjQ!ċدj !A-4y6KN !ʼ}b,3h7波F]nRus1!D1|t:t`]׍-Ji!o4V: zA#H[s^}S>y=,7kfJ͒ 70$XZZ`Qi JJY1fwʢB87Pج @̉*ۤ-[OЇ8R/))5~hQɻl8*")$D ctW^c]}a|%%fH HiRNΥ#AL@dlP?t!`͈~Hh G, >C@5Gǧ߾X,|Ӧ|UUӇ1`F& w賸l;w;88[/O]xtɤyq"1*"R>侁xv) \$BQ%1D8%ݦC %"RJLŤ&%D!eQR;[s.cw{Fe ³Y/YTDzœG(KH>Rd^***&MlV3mH&Ces!D" ̧H! h&Ia'xiۍIp> !opC"{RHmDӓ%so|&[9W*]G@YZ6(G4vZ[&қ|e?ܹ@~uZRDUPjp~: @ߵbg v%# +NNNMv҉BBqtxu;Gk,1IJh{zfZҮD? SUfg3&&wC * &3+(ѻY;:rbÛN=i^Mq] 3Z$aB(BYZ~-VwX=/pmҥKwL)JT9EŞw(%G!$$Ȥ*ђ;_loUSh! =JW+&eɬ]Ѷ=1Hjf+CF&R s,NHm?:ow{ʤ #EFO&FB xn`Z|O(f,+t /^!RD).*>ԟ&;woޚ9 93h5B|))ep'~6]yiVH)d۶[,˗}+ЮJM!n2D=7)ěRe%s~woIp'2tV 擒Y;ΖK )BK %%*&H >f |7C%nX=@J$قrւɌ#Ng9)]߿%p|QdkRco{l֚kq}JN @]zzzzh7n=xa0I~ *Ca7(Vk>F|$IBD|S|v#—K)vYn:>Tc!]QȁzՓ w>JmFK*Ca 5D7i}dS{MtUdAUЬkqqyyh XRRJ -Xwcfc%d:kTuu.ߙRzѳB;.\{z\1y)T9%(FӶ(ع?_M|n$xHŤ.Njb)8ZXc0d_bdf.rMn"PuݼhUއe=cmG'uN$XJwwX[_k=9u; gznc",4b>wzS u1_UL&hz>&;~XrecydCփGYB( DJe7nlXk 1nlƥ  GZBn(-$p3ktFYZf'gvH&eܸ \K?kXk)H2# 8[R(024&)PM|`"B !,$`RNʒfK0", f95,W@J] !w2gMۭY3AHdkFJҚsAbfLJxmϋ_ZM]|3ǘwO$JN'ם;G.\ -/3xTRRZ+yQI)yqL1~bZ{?(1nk_naIcV)51=MM4 NT<fU7X=RAWEEe-Z lFjZl0m̥)':7PHF;r swyXG?Q =uj6P|*_nU!28wn<ڸ 6=FfM|:c>Q5[{9qjDJfBQVږi-@Wu';~l>ug>}16I)ƌ6,!DR@kEL=>vߩJ󇶧wmϛoN&;EUa?!^'2}6 |sK_w4ꕮLK%Ž"{I!=|1sS⒌yt^W;C |K;Bx >ev4R@aRR4i˲Ѯb:wC%npL!$ʌZP蚺*>|@HA~Re1n1w1"Fg+"1DϔaRh2w14^ ؂3!P5H(E58lFcB$ XA]@ jB%իK#|%EQܾ0Zc(ӬE))K< 6g*GbޥKt .Cdzxwv>d^kH)ѭ61[ !DRʛ0ݚSyB YgRsXuj:XƞE `:ebpΡEdSk ${! 1* ']{o~sq %Z˷ԥIS~jYVjHA %|2j{g3~)=CH)gA@a=KU՟ܶJeu[9}䦙 UԤQ.fIk0ARY]ohb7Q'^rFM=#eY)+Q'T]|jCpv|B+̏\F>F799:ƅImmrK)&א=vzs}?P L7ҋ^nC]Kss}ж*Pʃlѓ)3ZP윓nHBM-՟M>xI}طkFe])xt IDZSK )%5F̶vI)_S|hRJ\ܗm۶1._xK^O@^Hh[#V֖~*s%$ (7_e}z;xw1Ls[DȐߘHhἧsB`l{srtF 9[,II0k*V MjJX,*#~V$79eQ}\G8rG׮RMG 2[ݺAi$LuIm%/TQrF7 6lo;>>~Nm~ʾmo>iy]R 9f0 )TCk96>5$ -"͛Gg0vVS1 ΝwN(%жukdB Q1_T Qh2BO֚ )yȚs"Xt/wϽ%QgnY\ܩyoʱW{Ё([>@U7p2o~%>kٹ # gPrˇx;񺐝lpvF<%h ?J:a>=\Hs5vKNV,tÆQV"G=zɯo 7ϧ^žRvE>xT%PE'+d/jS"7ߏ?~gpp>1`KIv)x--^XI3AwuMF %t:yMSr?|8>t?fNr{g-)|jYKlE 򄫇k#n-IICZK7NF?jh aHPTXk6!GKT0 4R VZe;cKN 랭YCL ThbUO7ÅEAVHNP`<;Qt{Jȉ%&X/7HBl{LBulrӳq \d(*WS[c˗͛υ_/UJ~AS߰=LM^@ar 2OIM3c_rvzoZU|J!x!nUQhCk5oJxa܀ ?1-eF4Cc%]ԳQ1Fg]T)IzmpȂ?uvzSB BT%"+$GmNB<ݬUMid   Q'RʉQjk/޴gmY3>E+ŋ_G*;p+#w})`{PZñRV?d VplljD*IY_<6NXfOl?65M~ٙOt B CO~˕=f :%%~<1 N8>+2''sb)$< a>DN]NJOmueq{;˺h)6mJwa#R%E2?*DcĔEM$ewIOob,qb|k)@J.QT@C xȲLO¯aT>ς[0fӵkjtH|cQ/B:uMB=D?|!1J 6Æm#qJ$gZ1M^.^J)>[k MS}T%$ IUk Z Qe+ңWAWHBi0資!"{\Mc*٢X<{6{cM{v6 Bpi@*{"Iz|H̶pCKZ| }G4) hDWH]CfLQՓ𑒪/O+{/*'Mcʎ,;A] brrr*%LKczRS sOf@O(5*[~ȵzD Ltyv.]m !M8C׳\s\Dr~6uU'qv?YC$5\ a'{;TUE?d(rWD6q2R%cR$mlgU?h)%d>|e"Jkȼƙ} 3j*ѶeEҖ;pa>~*)T" !%V lQQͶ2rqΡGLpJdGJ(stݚ"!ޑb$L>ÄnM [gU޼)0O-1._֚L&M]efV޼4iW9[0ZRW) O#4X@?I1sO{/^}bVg֝߮T!QJc۴ÅG^ @&-1\N,iDjZgqO\YlmM1dӏ{cԈV*;)%l=~qOӈFllֹ58O"VR WI]5k(@]g3&sK]|U!B"782I)r>i̋8'vϿn޹$eSdli&HSYFߓ#j)#mѦ@ E_4[H)OzxC^nG߻SI-̶4yP2s*3*Flz6sDLDU0 eR!ޚ0bJg`-M3[)%UYsG7oR躖̶f͌UdR7Xh [g0F+1*J"bd~}U]dj5rbKJQ 8[!$x^DkR°yPǷ1fLX(M{O0۞3Lr2zʷi0jõFEw{gdg{66AqĔD|̿)!.j9|ṯؚe5ݷ¾mG|Ž;%E= 1x֋S)P qh[!# #N Yp6 kBm֛<-*j8pCOoHNC]zTrպ0<8;}dq!v.]LGܚBD؊0ϐҹ@޹, (] SO)"EQf2(n,D݋4x˛1 >+1gZ"*dB*5B/:"z*QBD){<)if͔J&(> Ĥh 1'3 BqE)[Xo>B0 p*qE/fsvbOa5'ў[]|FnXV` ͈D5Ξ *'fiYg7ݟ<4)MSSŸ3%ƈP*7#0QՎ@kK |*4Z TUź3Qk'93ݰFq>ug|/= ";"vĻ ) #\DSSLY%!dL$4%%R+if9WI{!{SzK!wp';r'mwlo+u߭|a kDp(SEvj$DV d>cIBl]R=Rۮ RI]UƨϙVŷe}Cy滛f2+ "hBTi=u9 hDJQ_js-EYOlI)},bTۓuno/־, QU%ݤeBHp'\f\BQOf߷$)_t9GǍ5O nZflc>_]v>=[eA0m۾;#{7}ߝ*j[rq1f0|/M.+Ȃ(_Gh3Gw埴ң@$ƀsYT3//]UAXO{:}fm@4Lk!VT6VUjCuYkR*LE!3;f!fXH& E{6  IR7\FkMt퀱fʐࢴ=6C0xBs!+*4%8GN0\clgی"nU")j6l iOSy4LEOSw[Z%Nh]y![IVhELwH3&Q*] jm.m<ëV򕳦;5ݻ۳YVq2csC+BJ2\>*GuQ!ߏ fG 5hBԶlãst揯כ8]b~<{n˂d36˛uvzFLg[nڶ̅w`=!5JJB0!F HB x>Д,Kk޼5mަx4!+ʲsd1YGIw,O)>Ԇj0* /]G qUa3LXg0PYB9 `nЋ -!df2'V%J٦ nKq&a C ]N9~@8;;CDi5z!1%JiaHe}׽'e-_ݛOl|*˒$ 0W!b8/&(SA GHEZ-BH$eJ_3L9wX1uQ:'/_'m{R0XWk㓓}H=Z5S-G+ЈP(k(6EN=X*H*SJU+bh[f[Qn˳C׭W~{o3wŕ'c%_hloͿpuvv^O/ƖJ&\r,@" ewamzЭ.u QcH11 ~twNc eU*|}vSV":g *kJ!bqp?DxWV#9ɵ|.A)HBe1Q)2N|R\9UUsঀ%u|ah(ÖdAin@,Xbk; 1q+*Ѻ,K {w 3Oj-+ms2ٴ=Ud/Sqr|L9vwE\\$U%ٷ M9_BzHuH* fmgw"C%|sپ\EM}ukԛgM٤:!rmHY|V)摭Jdwk,,  ?thUDkuL{N߽lH)opgB\qȼU=Mtj_j(]Ab^erB׮q.SGԏ*3mI/(Y=ʘI%|PwVJd zxAzc227\J잻Ht!PXMMߞKƑ(jЦڎFB)%lZXٚW򕴫S ՄUCbh-6KHboC̨4!7F&:t=ꕧ軎)5cp:;GcJ }eWPLS7M~bL+k,'AV$q.cZߚ6oSѷ|d789Fi*%;fL5^Eb gtl2eYSaXOA8C?j ){*w?+ᗼm7=첔p>;-pϐrF,]t.?&ͨ>["5Xʺ‡H$ێRZw4T<_L1Ƭ`5Bj]?dDb!:5EێVKBIRuӓ޹ONߐaRiLRFဃBbãٴyl&{] ~Z{ rq!d~5!?ʪ92з"yP%iߵC"> U8g s:\)!d̕H1sOZ+Ru'i1dţ~Kp*G1rR4UYR1ٽSM薧L*7%]#Eɦγ'qt*gk&mo!cʏQ#v@\IjƍCJxalF׶F`h;jG6~S,nP\Gb]e=u-ٙP2"gclKJ&e3 jnaPZS3|bvԓu3e\ʍ=yëGwV3Zp7!|d8NWo#A0 kGgQi%捗RŘ!%!unb @QH$B[/#vnj$)zx3i)]igk{S@ 5#"ت1%[/N&6 Cx[k m籤Pꄵc,6w'gU>=&uքsZ FĠd&[0dA$Q6;^- ù310 ,!gAIRjύW(hO"Ia(q}gXnڶ09֠f>iFe=irgB⽻}. 4[{_|Wwsi<GF_~Bjrm&\)q>Sg#*/W=Vgen xxǞ< zŦo) /l|dZ3$hcFw[!`\zt{#7D?V Brc)129%<"˳Q|4R);GzHz3j;VI0zBY)59Q?DHÛ7؝5g3"ױbSZC<>d +O>6rGCbVS*',ʊXozO5*Mѣ(ң5)+* b̍[vmF+0Lsڶ{{~W{b!_Lǀ.M^~stS|@e|:!nE6kH :n&\Ȋ[[%[9Ea3^ ]z捷i O0iu{#*-6{@b60 z,!RYEQڴǻkne-Tlex^v8<8Հ:e!1Q5=R$Ca9Z ]ghztN<_/Fn5vˇ>A7oP|}>S?)I35 688[Esxz>r?}Z&>Aś> MF!uy|YEga>F Iȃ7ڵk; sb#j24g @R0>xWxgъ'έHDQje3L)h~ }|L zm VgK:ػp:جd,Bv1FgL'{i B){qrtc|Đ]0E>{L7 1E%9Ύ dҰCHdB={0|ZWwm2b!_&M9V'cl5$7>W0יوDͽHF"134 lBUU| I݇bA}4e,UhѢD6)=TdqXԕ+Wןx9ep't۶O߾\~Fζv^% Z E.0PơQ"sۋbmLJf;8OYoWg\|rUi"kl@MGa{k 5łÓS?7:; nu_ϷY6hBfg&ǧBCw}$2~"xOʊCױٷf\H?Ls3QXCԬDYb`s~ߵ3$+d]ff趤0RLM:ClmT$Hst_*ϩ-Y1LT% g'y##a(1hAbx9Gruo?Z~L@9? !G7_WnO|mwWJo!P 4s_S=6c,Q]t%։y Dz\r, ߃㞱QbjXTvϾ8xD̍]H)!H{Ir3o *{=V)c+ʱlJLlF+ɆjEbgUšD=JΈdۏ ,H1꾡vՆ>x8ӓ#6P4 1i8z`P;(\Ci h~&˲jx1=D-j\Y{~A`O`RgZ}GU lunZ0(ZnzYaYPcV4u42fgRoVk%3pה8bxcqW/;իqe#DOӔtmKiuS,7bu48a9%5!P2nqե cK3ncҠ}0WZ>_DZlQ(#˪,p%M:e1?W[nnnia:7)7z97Wu%e)2˺/o@T7 KcEY:6TU)pPJb&u/@ H:( 0zbuL跌(Jnm?B<'y z"fVuZ6 .!҆)*6DvoԉIk\)9c-CגILc=1qt|BJgw\owTEd*R18_*WWXk,J%DL44GISZa.Kb5G|+Epɹhҙ:•(ūW/w?$_hקPJE6JiRuŪ0L@]h-UB(ZAc -@"AI ffTn2wOCwVrVG UY1Nȓgo45/^ Mƪ#WW73a4QK*hmV%}r8t00 #v0f xNɬW > 4$7l41?/ss}ђNN8>9ǁ >k-lRuI?8ܝ"1":~LYf5j3㔹z4j1I)'.?Sc'w˜3G+)M*)^ݰe{}EȊY,$ɳP=FB1o$1FɠS$F鴻Cb|mwt]5 S>EږE-LĊ:L ib"bteU/+XoVL>̩ ]{ HiK5Q*فzxxq)RovII.9;Qq9yI9ρ}gfbQjA>m9^L23$BO|> \pjz@ >%b:n̉0p{XVQ%vý͐^5E0ܳ$Daُ(c5 mKۍ^Kbq9ibCP/|U닺FiE,^P%.2$t%9Ogߎ3_~>` VkPkkbSNPՒᰥRWV,Ə](RsSBQAҺ{BP~%0C9r 9E) ~&k$ж1^ sEQΪڈy MnM 3lo %fFNiJ;Rd?ͥphA0q]܁c, Ja/5i\ah=зk\#TEIߏXvhW+bV\^ݠU:]?1o<ss}G0nXW,ghW0((0kmus~I/oR{UWn*m8+臁™{*FκZRm_ #b?x H:aytJ&vrv68KkC1 Fw?1J|vB8t#ʂF#Jb*~"@7z&er->x>|aҏNOb@Pj&$Q(A U(g+g`+@D9 +H$FbYlc$Ģݘ]7PU PEM:aoyC>4 TPT v60 ̡rE '>קn Ygu2]Kaisf<>f-y j2>tUs|"6{8X+H1@UrȂ)S4o>9D9a>C,2"D4Ǒqg1N|t~1hDܺYbcN ?NCz`ݭKfqdlhMU20ӬK="ML?u6([pчܾtZht|wLn#Ff!'[Uw>>m@i6=Bό ,*S:&/cv;mfvp a =}KTyϞQTkÖz2Bnr.܃6~`w(nh7]'寱 3\+е,KlQܒ~*K&)  3gtm8EKs 8%Ix8r-*b 4h~Ys8@k>"4 b${zVfa8phݯ^PXggHKYiT=u9KH3E (>mM93Z[yi,h v3Z2_')3nV6V*9?9f#O`[Կ͆I0kcmŢ$A ըyPnX0Q-l|`}${;XWXc[B)c9WȶuSv#Dib8 m+~m^2Α_T9EY\ZƖ%vl\qãZpVf3N6ѿ2,4itJTkrm̒NUW՛JOz~#Opj]K◟fxHZKuJ=kc4T:A J aݎ_,OY`&'Ҝc™,:z9=?`-cc Q!UYkipǧ9=^#H0ML~mOH<}iHI?%)01Zd7kvo)EuQZě AptDy?cLc4Z_(WS,7y^=Gep!#ϰ*z69RIQR[53 3Z\h/b (ܲ,0a"O}>ePZ%$ހF^D<뵝+zG;p_}ќsz`„XqzC7h*88Y/+!r6>~iJKXWOtc"M$OU7@Fg9OruI>-iǑf[&/f0yBENQYsna?y٨s !R( -@>e,V9m1I|ҋydvR u)D)W)|_f&z)2R)1R53F1Gu{̯qTwO~t70?֫:Ph6ӄi{77<1~K=C۲ZZn(8 #YkՆ1ɬV+{-XC? |g%^%/$Պ8B>44s)3cacdĸ2U$Dl8I,*RhW`]C/_kd( 4,R77b)LLukEH빯>Eߛ 8ZT8?o/ RZck,UhxB_ûC/׹z۬ߡCgR*(rv)GuY>J9R2y(mYqbL81n_Ɣޏ1RlMBA%`bl&/l'yu[# 3'8g&w{tL}K__"# SqcңǏ>pFf0nkfc^'^\6g L] gJJq^DCqĐ*J 99=g&N7|/Hq$D0),sSIJ#A][0 F8Z&,fnf_;A8ȝNV0DM`F,96!9h[xw)` F+1aUڋO6O>9MӔ4zmFml|1?P]Y~M'fz# +i\+airָMX몢jyj})QÚScLav)CeG912vzv_NݾeZV[M]%vi~7󂚟! .ԧdsh| g6p}}-˦S?bcp]2TYi{T2oRmbHCsx\-+W{ʲ8q:ni '@5˫WLDzʙ vD邜FQk645/^_`ꓼ2Y{w#?K{w"I?GM-Y氧]5 #ckٲj,6'JEQ+eYE9+k,"MUWx&jsa+oxwon\|;_)(O$LrKb%j)X$M(6,ϟ+nw-MS9&>BDŽ`@90rM p KV2~p,◢,}-IDAT4$(ŷVH]$GɩsN)[JrD>Ͱ4yN6KCohe!)j,̧T21Ytľ el v[8#kWV8nwh9tE@NmRU%1ɜ(0yv-fE7I<6w Qҩ1g 0xϼq,|#*niHʒ~ uSSWW7k@q8!\Bt{Qwtmb ~* &a*QUJ+EuرHP9|XB%[)' 'W<W/AY~ |+fu4M)Zcʆ)&m3LO ʲm[st~NJ1uxugY%X8! cƁQE]Cz?I8]p3/IJ8V~KvÖeSvV!ҧ?yʌ!w4gLJv?ےܶ8ެQҟŜجWWv{ڃD-- /K~I9${{%`2eY2L#GlV :BG/Įm]3M`iO@(,Y'.RHՆ00rOXg鮟#+9Jk\ ۗ( Zn~o0dGKf·|#&LNi 2AWXW3Ukqa~eT`FUH~ F&K|s(4ܱꅈ[aI1Ǟ9U0;7|ʙZ1:ͥX@ULft|5ᄌ=N4!+8Imʙ~pNmV EGP4MmG"ÁqSOzho^ҵ[~#h- IY.Đ }зTYƑբzd(X-V+6=OٶU, ]GÌE!ǑiMh%~ΰ\->`5L>R%u]g?Le*DUc(EFY.9Sau\?^0 Lf!&.N8 [^^fRd /?[jTq8)u@kfE KɑYRB(mX-.NBHRpM9Ƿרr3 '.hFm&xD&Oе-J֩a{7okȾ*jt*ł}H=}oyy5yNMw=;9W?oߣ%-Q,OEJ}uAvbPo9Z/%;C $E o`LE)1Ϩb)iow^ub\iԌSC;bY#MSZ4tpivNJhJɣmjǟ?ȫgn+Bn85ueݵ,h0c N?H{*0Eb_unDQ89;/xed1Uuq5d 4·`w8#GgLLwGG ==9f{՜) ~b;QO)V>3DU;4ܰ?n$*-DZw#Fy*\tL]U𣕞^2:I9ϕa#$꽪K 'y3d/ ɻ0礚2ruBc3x=|TЗB ΊwD+%82F'Lm"zqALcd`:\c+q-N=rMf}=o)t AմdigISOѬِŚNE Uv8ptH K?|v!*v__7/GZ+rUSahwr)#NF6HZI&ID$֝sy aa@%k7ıCkI-˂{ kg[bAvb4lvgU`߬>o9/w[rq;@`}7fh(TZ#Qʴr^DŌjiN#fAL(=5x4%(++?<8p1L$(Ѫ'1a't9pQ%˺&La-/DzrLɍI&n@Y Dw^n^bS P҉habTX#3˦f;0k, Kڰ=NO?q{^ m>i.pge:`}]ˡ1\4X+jQ>0NRAF?R,9LķTGR4kb{I-EnE90% ը4f\nJwI[7۷9=^˿յhfc')sfwp,agXkXt@ ͌U JA}4urɞ$Ls $2k.\!W/=8F4КcR3UW$X;}Wx 3tCj<;84׋a臉j;LKpƲ5e(g0yb8;|ai; -1HC*Z಴l8llYxۉI*REQHUXr,h:DqHssshS'~DEUQ"OÀ}۳YoSr<9^s?R)+Zp1OM3eIQ8ZbʢDIR)ķ(vl57x/GaZ-:&btD+%"17MA ʕ SF¢c+r\\M2 4MM6 3XDE:[-=e#/]?/|/p7EcqDk WPv=°/ V)Yt"-!$fE,dƁx&' [b  ~gT,):rz C+ZmbL9mF|UUPه/}x~xWl63B9 'JkgoeӜ}iM]W#eQWgGҜ^Ym?>:"EY( sh97 tnɓh(f̰FxuiiCYTS΢.899( 8Vgxӓ#'H Z;mb @4>hnpvfGHa4弡$~Y5+T =m'ahM+#f0[c銖t%c(e񵂋 Ͻjrpr`4MB,Ι&yquG~#NEA+%9 DS\VMccX=#] )"gBg}MFS.8>>N_n^_UmҰ\xիc@B-#9kńrkf]Pg-cTtSd'lZQT+%:knVcOL< xj(z{(${4e BJ=0J1n_8G˜u0 \*޾(i<(YVYBʳŊʍ̾a|+3vO]Z&)Zc(*g޶u8EaŒܶie9hC7 65Fb&e5 [LQrv~F#SZG9s{sEa5S`Nnx'&\p"gbGK l"˦VQжY[,CiyŔA*%Y %!%q3~JY2rʤ)џSe_x\\v, dd nfloe7g4~BN Т'45:O4'SP쯞^4zE:ˢFV;g!PmɾGxiUI9wu ۮ3|_/3n.+UYI^f)$]Sj98PŒ!'5 #9bFuPL`qlSk6TN4EY`䩣)^-AƒE1GVG9Y;xQ,ɓ%O-e!.=GH/ %ͷ,ܽiEe EszsV^  UI7lNPZu=9%F)ˊjQT&IіW׷r%qr.  }O6tvvJe8۬qՂp}@6 ^FMހ%UU2#7|u82  mKG糵f,ص=eJ1ZkN |QOY*$>. qn ,sYI4d+8Z1 g hâ% ;ˡ _Z)m/,U]?ȍ9WuG<뾵U$!+%z 'rr:yTvvvR9+M2m1nAdO;F~wSS,Qs\0|bÞ iNտ򿽼x7H{_J_f땾|pr&KK?HG6M(S-M]-Zb&)&?٩3%MTf-vߒZq92k~\E@E8:MO}xs>Gq?6~hʂatrQA34U)߿h%ԋ衪< v\b/PiS3uAr}N GSfq֧iV-ĉ=Z($mXTd\)DLcTpF2!{i;Qakv0k>Hes||˪f/_8IaXU9z@KsRzt')1f߿T ʢXlIzr0.qBٷ2i.K:W nR8I5FIL$7ԝ@$[,Ha4'@&in~2L.vLUTi*YcRf8Z4sЊfє?Ё$ <ʍ.,>$ı7E Bz+jMR)(QBt Jib}XCst?E>&q_߻?GY/a)%HϞEg9fqttD:#. ŚZK`3*y%Xpt^Jzyy1QϱwiY2몾mKhqPJhCZ)%% v1M, ВD\H)$[\^ʊ(ee7ۧ*PtLYqg Y;6݁F7GtDQ5WnhJ4h Yqv~N?IS!fUIӔbcQɱ %1vV?ʳKzCsc. Ѩ9Nli6 I@IU(|mHA̟{SL$6ddop9OE9v=ȸ2%Swfd3m- AM׍)WS*YR P%<{Y2<G~o폑x;٬?hm>9 ;)5nQ/paGL"!;Y^1#8MOi9^J)B1s倫?Blu0iG1*<}H1Qxs3-q r5H$eq;a;zb^ѣ i,ΉymG1Cs8qY2q|$ՙ*?"xcTͪaGeE]S U5j<O<5N8gX4 WrA֖aG[JS3 a1BZ>Y^rBYG&_f?:15ZScSCzdz~R 2ZW>DumqΩeSJUI*J1Gq$,9W#p?f 请߭~ݷ|w"譏R.9NzŲƦox^{)r[^ہ f.eY:,S⸑s/ y٣n؀5W_3% H9;-x -cҶ= hGX'Y}~)(BmE+볬y cϨr( ͣ10CI81\~4f_Ϝgn,+Z=6F?5Z?\4{czZnc(|H3icB .fd\|}?Ÿ7mJ)W?§"~ucgOW&BRGZz@),7?f&@ɼ!(X6!"{Yz8=#ʪ0iqsaX4Ka`;SB#μ9tYc^  /tAyve08K?֫867WH-18,枀z}ȑr;9cBY=Ω7~2EtiTBqp'A^yͿ?iX;Đ G񳳬NϹ؜PLoofpۛ[Jf~.uiQJsmQI4veEeE4EI̭hCL0IF@l!rِr`a @㩪[bbjgpYgIł-ygi~]އqs(0l~I9ѻ}>Jw!IP)1'tN)Ozu.|T1\~觍 ^NL\,0zVUO)cҐD໧Fia1ɍzf#OOg+k0Ļ:1%ӪOIt僌֖ i2}W,KZxD?rDi8˲2۱Y-=u]X*1 qbVbFb-~EJHQi}=(p|@I"H(ԣ@5p}sËSXmhB\]p^`u#(} E1L5\ow<<;h6W7{-]m1d%:{keԔb.*K3( WLӈ;"_4'ٍ)p[겚^$jq8\ ;`)G)5+$,x1Ioi^_:F`іYrcDB,YR lN'<9xjL($ݗ0.hL,aglZ>h)Q 3nGN+iz%(uT%%q%)fAI|!3GˆaI)sz^,:h#ȣc4%}7v/!u-gEI(ѢuJ UXcHJH95$L4#12eM :&^>GMA>/%O~J,G>KedRߜƧTmmL髊-by'49GDjETlI@c?ʯc1/\.SG9 B+qCL?4e9+5a(ePtGbvrTE#jq9'B1EUJF0Q,,Q6e,hEh1pU7ι*N$@]c`'Bkx|{5dB?TR$є$3q"䙩Y \Y?tTvKTfwa{}IQ!ߛ}F" ~t?Uק-4jwvZGRj.cבkd>B!hc}XVL;K2r3OF친 4?+L~ڕҠ, ΏlV  <)F }OJCN$41kuɃ5x(~s1FX1D1gU˶8m !N՚Y3ak9x8 ~y7$XrӞg1sýja~VKꊊvavٌ8_F_ 9.78gqgb,arSεm{ 9=vYnEž)f!TY46Ȫp?T-Y% qf9fG3S@93)M^Pzm{IUqo434WHJ^['jDʢdy򐾗ra]A^ MҖ*Pɸ EHz>( :a>+'Ɲ,9g4Rzcf]N"|y{{sΟp`0,(stZ1)3ah8GeA#iQ-h (MBƈ_[ğ;SNhYU)Їpj. r`S/פAn~o(?PS {=9U(cTfIђ;@VgK(vц=1z@?軖#d95 \peF/Y" #]EÃ'TN}1f؂V.t3FڊQs$ƀhm\&^,7hߡq +CU?Gi{L!sݣxp-USrQQEiJdcN )b?(kM8F1(Jq_ Y$ > ݨF@%QWaFs QX%5n@i~ry}GnX3ڛWi8Ă)+,8ྂgYʊuәC;PVRKqjYH^쮙2q0@U F?̚opc; #)%7 (KRr´Fb(AUYHl`HIngRs,99aTUA]̙{ʪ)2 p^ .g~8H^)IBK6zR(-‘Swl-4 d+7?#ǀ&7G'Ç~gQcOOz~#OpZggVkNy>v?!24<ՌSݍFb30IV ?tC͖kۖ8g]<|[onw9p K|ֲ(-Զx~y ,pUCHIZa\8 M+vaiogv^>0"8Rj* OO(lLIYtic}rA7 ];VWa0Zv0ֻ2 !^VnL)xu|ѶA 'O[Ⱦb[~Ç~7a_O\.գG~o!iY؉ջcQ)OSfTEC 9fy!Ԭ5OI⭗'vZ\ސ o<{^ >9ZkF IJY0Mo !DAZ-"} (+rb=ۛ[GG("m'Ǜ.!ќP}scsb!f";Lov"0R$Hn$%=0WeWӟbJmI0.:=ON7ڊViy?Q9${'~קbPJONN7z?z{5[8Hci~F/vOkEc٬i5UiqXcH9 ow\]^ zJX_\cb#БmL$Oq3Mж;4(KA0N$IJ&fҴ}+9m!VUjf\qv|B&q}}+,OfM!1 LJ@8J1'=a v/|3|F4tqN,[w=> cGǎ>ąj?:bu6wۿn#Kz]7i˟gf:VFe9aHibQ;ˎnR`Ts}lcl5;O^+|cs_vVxgh>Κy%d٥DdWiսp׶(t*WT/)d;0Nl6kœlASWkNON$y9 _EI4 <|:)P=eB;G\<‡a83No^5CdǙŘqFZi ݏ+(mxo?ZЬrt23-ƻf=Κ}oc_\ е7(O{02ZacsWrF6G':Sܹa \m;*躁iEptNp|a@ 9Co;n;nonMN*龧bg-KQVF)c*X{ʨGORSX&Qcx)虽`%3 #{aPZLcbZ`cNQnGDI$F}E&Zԉ㇖R89=o<{!|1yR?U=z[/<*8w=ragƻxEjT_=? ]jYM]qqajj)x~@P!Kw2'գǼMυ (%6]/(dsɷ_y°8#'BQsU{2Gm'AlĮ;cZS0se _lą'a 3{:' WROʏW/(TfY x9vfL 2PXRxn1͋5s%̮/{vH[/jy)93F-A]?ϻ?BX´y}Z[􏎎;Ѫbv% cﮜ\ow;5N-*eC7I krdqv-/oPaQrͬ;R͝`q~p>&-YAwbG2-W[I7sDY°Z6DyM"mqq$^]QΞ) =˥ !Sp熁!h//)KK9C$ i3ȑ.cArCZv'oz4s~\$ӄRe‘Ќ4GyR%'tqӌ*̬ qF9YH+-:% md1 DSgz{[a]y j/Oea6ۿοE ߡgƹ|E0ȼ`ϔD? ~LLIrin eUSiv}~{k;P <k^Sn~>)&FOUJ0*qi-=[ohwW\]rur!Q=CSWLDUQh P=}&*Xkxpqzբ#8٬5X''ǨbvJLGrS 2rv_W1RĢFh5E0K4Zq8rx{C{!X3 ykM],Wk Uza,LfD q 6m1&ˆcSyKgA"ڛ}NN雟)ŷ_>bJ'~4JoZ}9=z^|lLrQ6s"‡ HIeX658J7s e jv‰|L;ϊƢ^bx-B臉=8>i 5猶c~dI?A*6kgIJٔL5㓳,ܷ4UwUwL_^~m5Zů.u}?~Յ|t=%(c@H8I¬3?uG }EZRXW2~LFsԆEUpn N;v[|Dfv@}v$[ޡT὏w'qM cKW7!Mȳ]"l5&!`PYGV\$pO.|V6)7}@};dlv[TTuCLլ8Ӽ9eĬC~K%GY:|DɩE`gN} zѠDOHViѓcDim^lNΞZ>::;NNNKBH)ʙ67,pzrVtʋŮ(ON`+8_?L\rjplG/^rC`rM$lvvA w}_)-7g:Z}[ouKZ8o~oVkJ/cfO>''ǿfX~;Z勜M~"EO4RĹBp,6 JJeTӢuIsBD@qg0LAYsk-p0ƌaInH?d8Lxgoԝ;^@QH羟wIJwg樱//-74GGJi tv{Y q٬%I>Jܔa?xݰX%mq( ̶(1EΑY0>FbC-F9AY*7\ "435sV ΨHJrRJi}Ǣi}jQ)__O>ڔ `33_7|0FS>~o}睷0cXZ"&b81hYu& "JV-1Ba_V7˿(ZK3(D+Y)xҘ)gƻ H:xNιxT(gxfNsu"fqjƱի]K=Ke| Cp՜Fk@kE\v()q!:КǹC/,ˣS}=cQFݻ͕,,鮡V|!V u%Z}2u%8X#p򑪖!u+JrY^0L)V-uD'81D?J/B&$_z?LWG>o1֥WolԲiH14H6i@iKΉt8cd%58u|raT,KN~7Xk-(h}Rΰyh`6c|t|g߃ߤ߶L=EA2J!h(8=s1Li@9qD[ )Fڮgh}O>~׼}bF2byTWDQ̒sssauzO-EQEw3Ik^9'l8akçNq@Up咾?U#Ն\բA+DIZϖ/!ij ]{'eD\U[?ẋ_}W&$|w'[P5!DGIM"ξpEWkRCK{8سrYP-$X6KBkKQŋ/Tp0rwq ޟ8=MQBH!GqiD,ahIr(Kj9i=ؓR’LуYm :u,捋^,(k2O'hWekyoor\b*fS([P8 !'Z4*g !sY~+Tڡ~ #qN䑆ZYb<Znwvw'Sۊgw(`(2 ǽ<͛ pL@E4W(Kū+N[0]t|;mZ f]ί^n$?oz/nV,NPDSUGRr&֫qp͚ϱJ]u)ޘ"y6J/G ɲUr;ׯt.Kd`s8YDt3D)1g4X6,ʹ,jzn)C]Um>~fCn? }F8e(\"F8(䦏c 2M-.*B&+Z 9lA>V)xakr<:>9AfEWr-y_KeS{CieJʩPVJ:4sjBSIMan^Yi4\l77 S?ٕ.˿_kQB1g1)gŔ꒛}Yl8>=i~(ď)Ȃg-snwYed5؜Y/}n6@<O<:[77[54Q6ݢ(P3ر9}Ki`բ*2"~4Yӯ=-D?Lp{M9I\edxjml?="0eRB2(8|M@؜E~ 4OCs(h}+^?ǡ}_393P EoA[PDlQrmqIY 3Yi+9ZB[B΄&Iڜ+-+[̻ߨ }ͺ+6< Z[TSܐC:I{tԑb&{n+o[BlQRkrD%嬛c!jOՇG_u]ژ*d,˜p7vdyr'O ݎD#?QhZ㿵V->CRgwu d3nH1sr嬑2s ,]+:c%_9Aܸ)5 CUJ=Z9rlNnfG]}}?1Oqo$Q''˱cso&N"CRXzf;yCUUw_loӟ۵ݷ'l ؚ֚4?HEb )EuJlzjo Bvlځu0>W4zoZ[^[4GN0v`)kN2٧eEHs@REFN)ZfAƃnw?܏?)cYuU~p3V9Lɼxr}njL4L,4=\=uUc,61,D9t[={_a'$3ga`? lzIuԙ*w7Dzvm")GsS)`n/^z;"?W pz{x_єŷ1F++om*Q$*)OOQ 0 z/8䧸L+/<~_,_ho^e]ߡu&Y99A2PN z}2C ƾH At9tχn~oOɮ4M?Y-APy|c5Uf{˾-V&Ʋ\J=ce>oaHRopk-1IUZXlח[ #M㥸BK(`9t#g1De_Kn}V4U! D,a> (ei`|+ 9lj̈А4pu=rhg'u۶%ge9|B _'rJ>!(VZiW8\Q) S2JPBVSX(59ErBRhq"Z0(Ĝ+b}vеU ^/ur tHSD+fvI{g%NJf1.]鏦$>?V˔YCN*0 k5޵#v&㽢 5cu繾⻞FJnd*<} Cֆ1&AXCPw0M\^oI$'Gb1'Hf3#G^ߴ7+%L9Spkz}G%rDg9SrlIK?8>^BhO]lpY.տus&~+x׏c,,ZRJ)9g-A{VN6v$Uc4  9?= e)^s9/T^dxrzzێۏ ]C$(G ӄ2=@g4ZfIZg:Io1jRT K !v0ə'u|.Ÿc̷M~v>}7rWܙsU4UΊ":.n|;zET䳟PQ6Lqsꪔ!-]únƞ+QjG ʢILZ6Z=td4m?]M{(.G{\ (~,E:@=Emw0Q%2!DHH݀ყv/Ul'K7M춻?53!ݡS0DH8^V0: g+21XpEE榧5z?Sy~~߾\/JCqE#2\?AN EZ!LJVRML>)4}wyO_ 1\WL9bzƓ'}G&sW?zN׼ݢxkĘ,v@!b{NFjKN/g} h|ĔED7{ 5!9fѣ#1D|jG7XRrA]VĔi7n9q8iCa\IRJʊ9 B#ZQq8tûW?qki W'ysvv>{!)˛c7c ł)FmKgq5͠ ãga:4BX2_qf?fG]|LBtCQLH7N,ꊺxu}KSá=< tgyw[[A HbI a1vW0$"1.W@Pʉ-mac!,4HH2mBuz}wy;#fVMUTys!xXҀ娪^}{WkCEUkE9"F|"BC1ǛT" M{vuY8ԩӧ_{UL2*Kƅʡ"{0"!6xA ʒM& 6g(/ n`HJ߁bJqP4mru(C8p:V!ө >Jt51.͙G/iɯ\?jcn[UyT)w28:_'E) _炪Pyik۴(4eYbF|v{KD\[nГ=>[5VME7f) ! Y`꣆2!'t`TdY'TREiߛw}q<ٗ.s+R_x:'Ԛ`ZxgE܅ !x\wfoŪ'u!F$˗/=ѠD ,/NEo[r-(H2*"=U6#&UIQ,ùZ!N_𛞾70@ҋ fLG/z^$g<(:Y2)>WMn>D 4N1k<TŦRjxE!"WIgzG8*"(0.= ֦3&+gN5l67Uxz uSۯ6,۶vE>vb=j8B)A)=ŏ`TƼ(S[}_}2d_(/URDFјKqM!."QF zo1S;7_kϟ?`sp{ql iTUq)6h>}kN U#LshY%JoQV]x^b:RGJŃb.ZyHGYxdTl [t!3eqi{)#/0Q!}#L ֤Jġ΁ÐeGOU}tL&'BH"x9i{t\2=7V=wd6Jga $t.|҃{{{wNtnwݾ{Ų2/!3td"l2I󃫿T7;n~1?Er<ϫj*R 1F!|CG%ЭzoQއnpwm˗.=eMȏR4ӱ{Pssn9h (DE׬JyFYcp("K]c!&1 \`\t"Uf:Z"RƑ$QAgȱ>*فXNػy\ r`bL7#yU2UXKƣRWS*csx1@+шhL*bD@{XRb0GU&!.C5yKmlzqYݼ!~:ֹ c:tPK7n?'y ~`]Vsk,A_S-!ύНY6Mg_0ػ>˗.>k0ӶUHqSnpJ"B߰jZ̑Zb2 ]!A8ʪyPeų%Ц"E,4qDiZRjQ JɘjTYD!?yZ|uV Ɠ EJk@2ͨ{z:# p*Y.\):>C8{HVaJ%q Μ3˺^^񽽱or-eBzn7oU]9p86?B:ޘ_^,X-qy9 R*byLWMUho쏅 8cՙ3g_ש$p 1Tf4:RibHV˺Ԑ"$S1(u&ER)&D I^h1a -QzJI<#.D T,ưw j#>r5&Q2ԏE25-1BYٽ~B?u^J];RmB`s}vl9M& N%e5lh]88d.m]ϔV)vؙƣh UҞ}l}1jXZ{μo|H/i9i޷p˽}iZ̓ſkϝ;K/>Ő {=2Uŀw!%&HSk e姲<BLO5'MS1R|Y_B_U;먻Q?lÕ``Qě32Q{c? aN4sġb/0c܋ 6UCִKcSdRR%h޲a!Y@NgiwȔz@8$!Kܒt^^qW;痫|ho[|;$SDjce vΝ;{->VDK^ Ag`XMo؛^wlBQ*Ї()R\ 1"TD $k-6%7Ŝi>fM2َG2LtC3dsY[31gBWD֑Zyߣx*Ye5ֲjVQwc{33 g3l!RR Lnww]{[, !S!ϲd呃[zPqW[kSktGSw 3·}ypk>=iX,y'dwŗB`a`E}7UJC>X$;lb˲_,Y, }A9.K**^V<%s,Sx1.$nQüu<˨Y$Sq{yec0k1HR?"u&k׮G"zFeQ=5Rց(>s~=K5Yu۶ˮ랊qQ/Fr-q/, EfbmWB,,xVy ÅtP:R,:"1݈p7nP ~),پ$]gR:Ւ+}BP}W=ja1M?d|(q,5c΢iq184Vd}ի3m>2QUtK1ZKݦC۳AG8g~ﺊ ܙӯ9yuGܴ L#Y^ uE۬{\S7-WwuY뮎F˄SH3Ѵ2NhES@%oY\3DxR0x\6>ᷲ,c쓩"ou&7^%,ͅHFJIVEb |aY55Ǯgcs>u<G\Y#n_vwjVy\?cHd)eb"pwV1x } @9?1c Uw pqh5S(] Tr`ozw}?}uLR!C!`9ϥ+W#[,~W&y><*, ×TB +s@2,YKc̊I>cmmt#1D1'c»眫sΉY!mb(XTEʽg9Rg*jDYdY1٢l\fXJ=b\P9dz*r4"*$ >!LHTM ȡL ڤSTMSSVU >ƍ=zogۜeeYeY:TCSH|Gd}7,w=I6LZ$p޾)"biWl?8\书* BV5;_w^G)xk]o>e㺵=[NiL O,0|"sI! ֦Th!˒Ye:H@(̦c櫎͡&B8-R]O5]_0F-0)2PMny1bTUTb{Dnh1q!rvL'O1}wwsyndJ>PBjd>oQ Te'鄠r1}GsYu]׹U3<g_7^vYRRҴ=J(L|4K$#"Œ45Ԋ-$;Ir2->bo<.OcUw|@,dE\-ް(|Ĺ5^39ړԚ<ή{E5|1{[ Y LۢT1$΃(D}`ͭ/# I<^Ƌ}f^[_gΞj1 ;&pX|,ۆCBWXeESmEK.~_߬ң k)&TF\Kg bsQPOY`~A54ni!ru:Z= 4m܌ygz6Oƍrh},2@Y0*yTl aږvW/jV§P[NzStm~WB lrq6}LǏ:ښXߙzQha,:\wHa+plKY1˲\iUH!呱&R})F 4Ƣ^9XXg;GoZv{9yښkNHqn~j`uֺL;`ٴ'{m%SUT{{8quNrP:kdf4Re@HDta,e5zjht>ZkfɗK ЃMZڞ&%uۥx`EƼpX>|mmB֕JY/i#wy' `݄?/A{>Xַ)ʂlǜs׼st}{i!е-W&dYA+OP˺Hģe\Bfpop#k;+y&mAV;i.|V+HZJ0}67_$u_}؟p})zig⅋߸vڣC߼q mB' @@e6vv39#;w5U&V +uBB=U/W1r9w9wts¬( (~5%(88֚i^[uˢdyFqw Y8ӲǿN%h_^qkY'eQE8ĈΓwB9@V:p3ιŋ߼{Ye-F$YO#@g_>|ٳg_=ιe::@6Z~s:ˏ=IG%mng$c*u+jʸ1Nt1&cL:廞~^y`9sۥJO)Gx*JBDDwޏRQQ5:+궳 !|`|œ"6g?|?ʄRt={q/p_찜n7G'"P5TĻ׷t RoC)Ml٨F/| Lk1멸lk/|U~c͆(4lǺz[dQ@(BD$E|O<ٳ=tr,Y,b])8ּj0a(BJBIRf1'x\kwϞ~ʳ4] @۶c#m<#A0ƶKux!YlB!uwsH~)(Hn@tCj!5Uݿ'r,rfIki6+vG.Oit?,%J#S)S3VYlƄp}0=q,V?cWYIUh }^ΐA8DSEVf6F3*Z1^;}zoXǤrX}v*bFRT-4֧vÚ(sL*0u#w6!x4fkmFYχp%reݑ0, <]o$ˉVmr,W-^]"&y+8QC xMΜ~k;;ϣUz_7YqޱmBC>c5owD- r(OZ$r(b\ u*Jop}PݙFm;4qށቈm4yjVk{oB{£5Z͙ۯݹI 3H@F)I X21^}aUw(%ӎ>&7Ʋ6M_;eݽ!zP#sxY[YZGg,8}z[I=(wXn Q"AT:Ʀ1X, PdwFk=ǢqkG;I(L0ZDk"Qdbayw)/o͗;~#OYaawi/HD2,)R|z csq̦KVgHRV]Ӗ:ϓ˲.GZ-HO^jD"/_+9q?u`֚7BE"UI hX}G4 Ax]AVRHpozBw.l!w 1t,ܠJi}7XsV(|zXJ{5/_E߮DŽd-D&F|DnR5 ,M݈eIG<ԍ?LʊD'ӗZK׷mMSj6}V7w߯B"@ W=J*<'}rSRr Le">ݸ8$!2C@*Ş"Q2ljq`IJDV"TbT鯵7w}xI_VV}[%ڴ,2+ghDxJ1UU!=>q܎t'M,b]- H g{Nʢȿף BQD! D@AAD$:ݒ=Z'IJ,pΙi턯ݸ%g2^h%V?!&IDAT:ˈHyT+ G:"!|?3ֽhNB"S>U4AkAeGH,3?N&@ R#Nuܸ>E8sq|`Ѽ=G9D#L455rpA@Q_Wl}o<1FcTe)R!"e)xb@Fʈ !-R1aE7],V9bԆ B @hd*9##SrNxM|UZA:xJlV' 2;Ox2B"HڻJwP Oq4 9mCHDBkb ךYO? W˷NcHu:4 4Ub *Ջ}V9u|LxD*F094Y#Ʋn?.;T sWy GaZ?=Y|x YV6zuEQum[xo^'sZ\t囔@q`:kQaEJ :wǸBL'@SRPC| K:׹BPyQB 9{d lO^fϞb\},c}G;?U ! J!HbI!4#(4BpX<BrU_B btB 1v"|=iR`2llcU)"ѱn ĹLiF "ٚLI6zTթ6c5=$c1yh ؖj;lYON@Mױ΢lu׿|'?ߵcKwMVR7^uҗ&MU]êihwTum)qf1!Z\rcLQm.x)dN4T'P`gw|G L#0*ޢuyQ!H.J3^4O頧pWk<*l6{V )RA^TGoxΗv8&?)E ht R*MTF dy1߶Eۙ_BBo|@ʈcEj C7\;j{5S{ k/stӶy)@TdbF\H-mg~ eϲ3-tKӭP4|{ڮ5ՊiX]1|8lBbO>$L'rTS9k0Ƒy6.E`M'iQ1.dE]'rt\ib\l4_1| zLc\WJΤ^O5ZG3b\;S  y!Ov,tmts3pX/:il&M\-qV<1]rhBUVS46K!! yӵuʲ]o~'czg*ӌ4ʭŌhh_q2Uh yO9[{sBi$ᨢ0͒fOj:<ϒ'荍eǁcia|u,?b@J`-uoxZ Hl3"Xr-Ʉ'Ę&. k{Z"w[>8"6dZ^dTb?3>u<BI"υ[:oMF Ddc0E`<8}Wyu)hd8$`;}W~%1:+w+"iJ6+d3k15?cUJ^J"d^2F88اݿoaBEQ 8~{vNZOoE`t A('׽q(; kzLxOP6 vNXX4MO]l5 Bt ;h;vr,rr c-Z8T^7[s,SCRV $5":20]=>7ݬ.9.oO`9~iFGo B $Z7βLƔEm,uQQpaXkJɓ56;!mdbK љ&KpdN}cVѨz*ѽ1j\4U/7iߧ^dΐ%:*20M;— A4޻ws̬Q] xMjr rQ%#3Tɓ{boR$8X,wj?| :'SUXg/2. !2.ZFǨ3[1.ċ,MDc>iHZ$bYdu,>\Z6=!H]X1"2Lcc,7dYFUUh)D1r77#ɱxj >5D^3`ilBNmUUUx6]GEJuR6q_;7ת*?R)1"3wC'(2 TDE lN W_9?uox,JיY!BL|^ #*'J lZJXgcۚkk$kidB@" i(Ĭ߻iyƍ78Rʒi7okNg|`kke_Z"tA. zyLZ׃h! e{h{O;<]sMS,K/ h)CȔ`4'RBG.SIHIbD| y*\> shbzcWxL`<65}o~oeUj1Vb<* }&p:Fx'`!δe^c30+,KF.P۔AOw#1Iܶ#g1@g>$UY (6W^}X*n`R>s^}| '5ާ-]rUW?}<`i@G1&>}?GB\Z=;ɪ5BMlX[w7N̳m1F}īe8N&i{ CzʶY ŇxQD]n~w]ٝMy%)ҙYk~-"O^vΣG}KlH`05'<'媬n}}U/"DxUq?vc{T׽7ƾC۟}ߗJ%E bU]ޯ,vC1i (RJ=@7H%A}BRݒ 8ă#כ}oӶ~woWmnm~ڤ:_V]RC'=ȧ-S1ҹ_VMYW[7޳\-$k'\B B()Pz$<#"CZ#dʁ7ݷ?{m6ۧ_S'?uox o]Vʕg}n{XBu䀔R !uk-R%sP hJJ&fB4xkFbhMB0bO3/ZE§=UugL'w΋ޘ!~Ktܩ(D)lu2*AvdZ#U!H!n9{ 7~׏sTOw'{Oq8?#׸RPSS_^hd ъ_d[9M3m;\DQ~?{Ԣ' g-wFwD?mqއFe%a8wc}X]]=Y[ kV^r|/%JQd XY [g/3|Zɮ[ 1okW_5K!$EZVf#x>qP}{׬W{ F @ʣ`s~h\}[9 ]o~^˳*H(8} eQwC.V^?3FgBߛwm](Bey֩-7,{?tox)4mK]◾(»,1^{g{g^[3V?KJ!LIENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/testmodel.pack/boblampclean.md5anim0000664000175000017500000106321512641367670032501 0ustar jaakkojaakkoMD5Version 10 commandline "" numFrames 140 numJoints 33 frameRate 24 numAnimatedComponents 198 hierarchy { "origin" -1 63 0 // "sheath" 0 63 6 // origin "sword" 1 63 12 // sheath "pubis" 0 63 18 // origin "pelvis" 3 63 24 // pubis "spine" 4 63 30 // pelvis "neck" 5 63 36 // spine "head" 6 63 42 // neck "upperarm.L" 5 63 48 // spine "forearm.L" 8 63 54 // upperarm.L "wrist.L" 9 63 60 // forearm.L "thumb.L" 10 63 66 // wrist.L "thm_end.L" 11 63 72 // thumb.L "fingers.L" 10 63 78 // wrist.L "fingerstip.L" 13 63 84 // fingers.L "lamp" 13 63 90 // fingers.L "upperarm.R" 5 63 96 // spine "forearm.R" 16 63 102 // upperarm.R "wrist.R" 17 63 108 // forearm.R "thumb.R" 18 63 114 // wrist.R "thm_end.R" 19 63 120 // thumb.R "fingers.R" 18 63 126 // wrist.R "fingerstip.R" 21 63 132 // fingers.R "thigh.R" 3 63 138 // pubis "shin.R" 23 63 144 // thigh.R "ankle.R" 24 63 150 // shin.R "toe.R" 25 63 156 // ankle.R "tiptoe.R" 26 63 162 // toe.R "thigh.L" 3 63 168 // pubis "shin.L" 28 63 174 // thigh.L "ankle.L" 29 63 180 // shin.L "toe.L" 30 63 186 // ankle.L "tiptoe.L" 31 63 192 // toe.L } bounds { ( -16.340680 -13.251548 -0.285210 ) ( 16.321431 70.077741 66.474757 ) ( -16.343818 -13.249535 -0.286453 ) ( 16.317874 70.094876 66.479983 ) ( -16.341916 -13.246621 -0.287754 ) ( 16.313486 70.125240 66.489477 ) ( -16.335548 -13.242885 -0.289112 ) ( 16.308388 70.167721 66.502786 ) ( -16.325233 -13.238372 -0.290508 ) ( 16.302700 70.221201 66.519418 ) ( -16.311446 -13.233151 -0.291947 ) ( 16.296514 70.284557 66.538854 ) ( -16.294616 -13.227257 -0.293404 ) ( 16.289921 70.356682 66.560578 ) ( -16.275123 -13.220754 -0.294892 ) ( 16.282997 70.436486 66.584101 ) ( -16.253326 -13.213671 -0.296387 ) ( 16.275798 70.522844 66.608911 ) ( -16.229554 -13.206061 -0.297894 ) ( 16.268393 70.614664 66.634588 ) ( -16.204108 -13.197965 -0.299401 ) ( 16.260831 70.710822 66.660700 ) ( -16.177280 -13.189433 -0.300903 ) ( 16.253164 70.810154 66.686854 ) ( -16.149325 -13.180512 -0.302399 ) ( 16.245430 70.911514 66.712770 ) ( -16.120496 -13.171244 -0.303875 ) ( 16.237680 71.013641 66.738138 ) ( -16.091024 -13.161699 -0.305344 ) ( 16.229938 71.115223 66.762743 ) ( -16.061136 -13.151920 -0.306786 ) ( 16.222246 71.214820 66.786413 ) ( -16.031040 -13.141967 -0.308201 ) ( 16.214620 71.310834 66.809072 ) ( -16.000953 -13.131911 -0.309593 ) ( 16.207094 71.401460 66.830688 ) ( -15.971074 -13.121815 -0.310959 ) ( 16.199672 71.484528 66.851315 ) ( -15.941604 -13.111735 -0.312279 ) ( 16.192358 71.557343 66.871004 ) ( -15.910506 -13.101773 -0.313580 ) ( 16.185167 71.612428 66.890121 ) ( -15.876421 -13.091981 -0.314839 ) ( 16.178068 71.726124 66.908698 ) ( -15.840468 -13.082445 -0.316055 ) ( 16.171008 71.989818 66.926322 ) ( -15.803612 -13.073249 -0.317234 ) ( 16.163954 72.244008 66.942701 ) ( -15.766702 -13.064477 -0.318367 ) ( 16.156899 72.477756 66.957631 ) ( -15.730523 -13.056225 -0.319466 ) ( 16.149855 72.683741 66.970949 ) ( -15.695778 -13.048571 -0.320511 ) ( 16.142858 72.857924 66.982636 ) ( -15.663166 -13.041627 -0.321526 ) ( 16.135961 72.998815 66.992779 ) ( -15.633358 -13.035465 -0.322483 ) ( 16.129246 73.106118 67.001581 ) ( -15.607028 -13.030195 -0.323395 ) ( 16.122793 73.188480 67.009149 ) ( -15.584880 -13.025908 -0.324261 ) ( 16.116718 73.256762 67.015443 ) ( -15.567669 -13.022705 -0.325077 ) ( 16.111143 73.312780 67.020578 ) ( -15.556215 -13.020675 -0.325836 ) ( 16.106187 73.357428 67.024512 ) ( -15.551442 -13.019914 -0.326542 ) ( 16.101991 73.391220 67.027283 ) ( -15.554410 -13.020508 -0.327183 ) ( 16.098694 73.414279 67.028852 ) ( -15.566357 -13.022561 -0.327770 ) ( 16.096430 73.426524 67.029171 ) ( -15.588756 -13.026144 -0.328285 ) ( 16.095324 73.427703 67.028184 ) ( -15.623368 -13.031337 -0.328728 ) ( 16.095505 73.417409 67.025785 ) ( -15.672326 -13.038212 -0.329090 ) ( 16.097050 73.395137 67.021856 ) ( -15.701522 -13.046825 -0.329357 ) ( 16.098220 73.360254 67.016230 ) ( -15.852245 -13.235599 -0.329537 ) ( 16.098293 73.312027 67.008715 ) ( -16.188511 -14.198901 -0.329601 ) ( 16.098629 73.249611 66.999040 ) ( -16.809161 -15.166696 -0.329548 ) ( 16.100361 73.172065 66.986914 ) ( -17.784731 -16.109243 -0.329346 ) ( 16.104523 73.078328 66.971960 ) ( -19.219563 -17.049730 -0.328987 ) ( 16.112074 72.967180 66.953721 ) ( -21.123245 -18.029491 -0.328449 ) ( 16.123989 72.837313 66.931672 ) ( -23.121393 -18.974523 -0.327713 ) ( 16.141300 72.687227 66.905165 ) ( -24.821818 -19.646140 -0.326737 ) ( 16.165131 72.515307 66.873474 ) ( -25.965707 -19.995502 -0.325497 ) ( 16.196787 72.319664 66.835680 ) ( -26.553075 -20.069814 -0.323940 ) ( 16.237830 72.098309 66.790767 ) ( -26.794724 -19.969871 -0.322033 ) ( 16.290230 71.848929 66.737494 ) ( -27.095587 -19.755434 -0.319153 ) ( 16.358585 71.514856 66.661644 ) ( -27.853889 -19.506769 -0.369999 ) ( 16.437701 71.063014 66.551461 ) ( -28.830154 -19.219202 -0.434204 ) ( 16.516771 70.520985 66.408710 ) ( -29.580326 -18.910144 -0.506447 ) ( 16.587428 69.971013 66.235662 ) ( -29.918144 -18.587730 -0.576355 ) ( 16.644058 69.434544 66.036010 ) ( -29.991529 -18.267112 -0.640469 ) ( 16.684107 68.873486 65.815444 ) ( -29.857283 -17.966620 -0.696018 ) ( 16.840717 68.304445 65.582013 ) ( -29.908004 -17.709158 -0.740734 ) ( 17.011300 67.745599 65.346336 ) ( -29.967597 -17.523010 -0.772602 ) ( 17.153157 67.217370 65.122013 ) ( -30.030994 -17.442892 -0.789601 ) ( 17.261487 66.743276 64.926081 ) ( -30.055995 -17.510727 -0.789200 ) ( 17.330412 66.351749 64.779940 ) ( -29.965432 -17.701704 -0.777535 ) ( 17.370527 66.022896 64.675422 ) ( -29.826560 -17.954569 -0.763714 ) ( 17.397281 65.713368 64.587231 ) ( -29.642978 -18.262686 -0.748764 ) ( 17.411103 65.421871 64.515123 ) ( -29.564958 -18.619791 -0.733761 ) ( 17.412823 65.147264 64.458761 ) ( -29.448977 -19.019868 -0.719769 ) ( 17.403280 64.915457 64.417739 ) ( -29.452071 -19.457281 -0.707943 ) ( 17.383122 64.823041 64.391651 ) ( -29.353580 -19.926702 -0.699562 ) ( 17.352624 64.757473 64.380016 ) ( -29.008358 -20.420604 -0.693497 ) ( 17.315301 64.717684 64.382296 ) ( -28.508599 -20.930702 -0.687641 ) ( 17.274663 64.807675 64.397888 ) ( -27.894280 -21.450933 -0.681950 ) ( 17.231008 64.902090 64.426117 ) ( -27.194858 -21.975504 -0.676399 ) ( 17.185029 65.001566 64.466276 ) ( -26.434729 -22.498784 -0.670921 ) ( 17.137645 65.106534 64.517536 ) ( -25.726063 -23.015488 -0.665482 ) ( 17.089864 65.217191 64.579048 ) ( -25.097222 -23.520572 -0.660011 ) ( 17.042670 65.333542 64.649868 ) ( -24.445982 -24.009427 -0.654453 ) ( 16.999408 65.455327 64.728966 ) ( -23.772702 -24.477818 -0.648766 ) ( 16.969141 65.582078 64.815311 ) ( -23.399312 -24.922005 -0.642876 ) ( 16.947238 65.713100 64.907775 ) ( -23.139479 -25.338771 -0.636742 ) ( 16.935617 65.847533 65.005255 ) ( -22.857728 -25.725458 -0.630303 ) ( 16.936532 65.984193 65.106554 ) ( -22.555840 -26.080020 -0.623520 ) ( 16.952512 66.121816 65.210562 ) ( -22.235920 -26.401066 -0.616337 ) ( 16.986200 66.258889 65.316133 ) ( -21.900456 -26.687829 -0.608723 ) ( 17.043637 66.393761 65.422158 ) ( -21.552203 -26.940178 -0.600628 ) ( 17.125161 66.696350 65.527611 ) ( -21.194222 -27.158643 -0.592030 ) ( 17.225499 67.042598 65.631499 ) ( -20.829785 -27.378385 -0.582897 ) ( 17.340150 67.382305 65.732927 ) ( -20.462365 -27.566763 -0.573212 ) ( 17.465174 67.713076 65.831108 ) ( -20.095599 -27.724339 -0.562969 ) ( 17.597047 68.032648 65.925326 ) ( -19.733253 -27.853517 -0.552143 ) ( 17.732527 68.340507 66.014986 ) ( -19.379168 -27.957119 -0.540756 ) ( 17.868590 68.795418 66.099606 ) ( -19.037251 -28.038224 -0.528790 ) ( 18.002324 69.226804 66.178813 ) ( -18.711473 -28.100176 -0.516281 ) ( 18.130863 69.632250 66.252322 ) ( -18.405813 -28.146416 -0.503239 ) ( 18.251317 70.009550 66.319919 ) ( -18.124299 -28.180368 -0.489693 ) ( 18.360690 70.356725 66.381512 ) ( -17.870989 -28.205377 -0.475689 ) ( 18.455793 70.671923 66.437030 ) ( -17.649976 -28.224562 -0.461270 ) ( 18.533132 70.953429 66.486483 ) ( -17.465411 -28.240591 -0.446480 ) ( 18.588773 71.199494 66.529808 ) ( -17.321544 -28.257466 -0.433235 ) ( 18.618156 71.408381 66.567030 ) ( -17.260242 -28.274943 -0.420014 ) ( 18.615845 71.578193 66.598048 ) ( -17.254539 -28.290145 -0.406601 ) ( 18.572209 71.717005 66.626396 ) ( -17.301722 -28.300070 -0.393070 ) ( 18.489044 71.835148 66.654863 ) ( -17.392238 -28.303922 -0.379513 ) ( 18.375280 71.934103 66.682377 ) ( -17.517895 -28.300191 -0.366028 ) ( 18.238075 72.015430 66.708093 ) ( -17.671465 -28.286887 -0.352715 ) ( 18.083488 72.080711 66.731348 ) ( -17.846417 -28.261761 -0.339703 ) ( 17.916901 72.131419 66.751680 ) ( -18.036747 -28.222462 -0.327107 ) ( 17.743375 72.168881 66.768780 ) ( -18.236829 -28.171225 -0.319660 ) ( 17.567900 72.194175 66.782470 ) ( -18.441291 -28.108764 -0.320549 ) ( 17.395656 72.208081 66.792688 ) ( -18.644905 -28.034074 -0.321240 ) ( 17.232280 72.210948 66.799440 ) ( -18.842459 -27.965784 -0.321724 ) ( 17.084235 72.202603 66.802797 ) ( -19.028578 -27.873317 -0.322006 ) ( 16.959304 72.181991 66.802669 ) ( -19.199691 -27.754619 -0.322086 ) ( 16.852844 72.149644 66.799984 ) ( -19.354932 -27.602527 -0.321972 ) ( 16.753618 72.107584 66.795945 ) ( -19.492517 -27.407548 -0.321697 ) ( 16.660964 72.056030 66.790652 ) ( -19.610511 -27.160641 -0.321263 ) ( 16.574299 71.995223 66.784258 ) ( -19.706873 -26.853611 -0.320694 ) ( 16.493093 71.925359 66.776834 ) ( -19.818227 -26.479313 -0.320000 ) ( 16.416863 71.846616 66.768427 ) ( -20.080488 -26.031929 -0.319183 ) ( 16.345178 71.759307 66.759138 ) ( -20.197225 -25.567815 -0.318272 ) ( 16.277639 71.663696 66.748977 ) ( -20.380001 -25.045906 -0.317257 ) ( 16.213866 71.560204 66.738029 ) ( -20.485673 -24.431503 -0.316155 ) ( 16.153518 71.449300 66.726332 ) ( -20.525100 -23.724174 -0.314964 ) ( 16.096265 71.331546 66.713934 ) ( -20.515985 -22.925953 -0.313698 ) ( 16.041801 71.207614 66.700883 ) ( -20.480254 -22.041296 -0.312363 ) ( 15.989822 71.078281 66.687255 ) ( -20.403621 -21.076976 -0.310955 ) ( 15.940046 70.944411 66.673107 ) ( -20.264187 -20.041879 -0.309490 ) ( 15.892193 70.806975 66.658506 ) ( -20.088580 -18.969906 -0.307968 ) ( 15.845988 70.721234 66.643553 ) ( -19.874942 -17.909063 -0.306393 ) ( 15.801160 70.670595 66.628318 ) ( -19.556926 -16.812055 -0.304772 ) ( 15.757440 70.616783 66.612916 ) ( -19.124449 -15.692378 -0.303105 ) ( 15.714557 70.560491 66.597447 ) ( -18.610263 -14.564094 -0.301403 ) ( 15.672228 70.502547 66.582060 ) ( -18.049492 -13.441409 -0.299667 ) ( 15.630176 70.443816 66.566868 ) ( -17.472337 -13.254941 -0.297905 ) ( 15.746903 70.385299 66.552057 ) ( -17.193474 -13.255202 -0.296120 ) ( 15.863894 70.328048 66.537781 ) ( -16.966579 -13.254949 -0.294308 ) ( 15.967922 70.273151 66.524200 ) ( -16.764480 -13.254365 -0.292488 ) ( 16.060696 70.221824 66.511545 ) ( -16.593932 -13.253609 -0.290665 ) ( 16.141918 70.175319 66.500025 ) ( -16.461806 -13.252817 -0.288838 ) ( 16.211822 70.134921 66.489882 ) ( -16.375059 -13.252103 -0.287014 ) ( 16.271204 70.101947 66.481366 ) } baseframe { ( -0.000000 0.016430 -0.006044 ) ( 0.707107 0.000242 0.707107 ) ( 31.228901 6.251943 9.236629 ) ( -0.022398 0.133633 0.852233 ) ( 0.003848 -11.026810 0.100900 ) ( 0.001203 -0.000819 0.001677 ) ( 26.002844 -2.030195 0.014076 ) ( 0.000000 -0.000000 -0.750740 ) ( 0.000000 4.130581 0.000000 ) ( -0.008569 -0.043541 0.136531 ) ( 0.000000 7.977538 -0.000000 ) ( 0.001867 -0.044560 -0.030738 ) ( -0.000001 13.487594 -0.000000 ) ( 0.030480 0.081639 0.353221 ) ( 0.000000 4.943967 0.000000 ) ( 0.000004 -0.000009 0.403507 ) ( -1.626972 11.080726 -7.581193 ) ( -0.972402 0.054889 0.001102 ) ( 0.000000 12.910863 -0.000000 ) ( -0.299251 0.030900 0.085823 ) ( -0.031254 11.193578 0.010376 ) ( 0.060311 0.000230 0.004107 ) ( 0.054595 0.018457 -0.015135 ) ( -0.176653 0.062347 0.335345 ) ( 0.000000 3.968525 0.000000 ) ( -0.039798 -0.055173 -0.355365 ) ( 0.000000 5.348644 -0.000000 ) ( -0.333183 -0.042087 -0.017112 ) ( 0.000000 2.438549 0.000000 ) ( -0.530821 -0.101863 0.096338 ) ( -0.655277 1.265096 -0.625148 ) ( -0.192019 -0.907991 -0.305115 ) ( 0.975809 10.864569 7.905795 ) ( 0.942812 0.268265 0.080217 ) ( 0.000000 13.379274 -0.000000 ) ( 0.179574 -0.160291 0.616037 ) ( 0.028222 10.991364 0.020885 ) ( -0.009560 0.110497 0.242448 ) ( 0.000004 -0.125166 -0.066536 ) ( 0.332332 -0.124874 0.331347 ) ( -0.000000 4.224101 0.000000 ) ( -0.226414 0.016245 -0.268104 ) ( -0.000000 5.160579 -0.000000 ) ( 0.363836 0.004691 0.037130 ) ( 0.000000 2.493865 -0.000000 ) ( 0.619859 -0.137444 -0.141287 ) ( -3.265696 3.709081 5.241207 ) ( -0.026046 0.012720 -0.997340 ) ( 0.000000 13.074334 0.000000 ) ( 0.020356 0.084646 0.038299 ) ( -0.035381 14.148866 0.000001 ) ( 0.000204 -0.010442 -0.504513 ) ( 0.000000 4.097309 -0.000000 ) ( 0.000147 0.001216 -0.253475 ) ( -0.000000 3.425298 -0.000000 ) ( -0.004317 0.002866 -0.069741 ) ( -3.261971 3.695171 -5.223284 ) ( -0.023142 -0.020279 -0.997340 ) ( -0.000000 13.074365 0.000000 ) ( -0.026086 -0.005701 0.039174 ) ( -0.035380 14.148879 0.012529 ) ( -0.000089 0.008070 -0.504150 ) ( -0.000000 4.097290 0.000000 ) ( 0.000000 0.000002 -0.256032 ) ( -0.000000 3.425305 -0.000000 ) ( 0.005016 -0.002226 -0.069715 ) } frame 0 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.228901 6.251943 9.236629 0.022398 -0.133633 -0.852233 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008569 0.043541 -0.136531 0.000000 7.977538 -0.000000 -0.001867 0.044560 0.030738 -0.000001 13.487594 -0.000000 -0.030480 -0.081639 -0.353221 0.000000 4.943967 0.000000 -0.000004 0.000009 -0.403507 -1.626972 11.080726 -7.581193 0.972402 -0.054889 -0.001102 0.000000 12.910863 -0.000000 0.299251 -0.030900 -0.085823 -0.031254 11.193578 0.010376 -0.060311 -0.000230 -0.004107 0.054595 0.018457 -0.015135 0.176653 -0.062347 -0.335345 0.000000 3.968525 0.000000 0.039798 0.055173 0.355365 0.000000 5.348644 -0.000000 0.333183 0.042087 0.017112 0.000000 2.438549 0.000000 0.530821 0.101863 -0.096338 -0.655277 1.265096 -0.625148 -0.192019 -0.907991 -0.305115 0.975809 10.864569 7.905795 -0.942812 -0.268265 -0.080217 0.000000 13.379274 -0.000000 -0.179574 0.160291 -0.616037 0.028222 10.991364 0.020885 0.009560 -0.110497 -0.242448 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.265696 3.709081 5.241207 0.026046 -0.012720 0.997340 0.000000 13.074334 0.000000 -0.020356 -0.084646 -0.038299 -0.035381 14.148866 0.000001 -0.000204 0.010442 0.504513 0.000000 4.097309 -0.000000 -0.000147 -0.001216 0.253475 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.223284 0.023142 0.020279 0.997340 -0.000000 13.074365 0.000000 0.026086 0.005701 -0.039174 -0.035380 14.148879 0.012529 0.000089 -0.008070 0.504150 -0.000000 4.097290 0.000000 -0.000000 -0.000002 0.256032 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 1 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.222026 6.236773 9.264546 0.024988 -0.134426 -0.851489 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008541 0.043391 -0.136426 0.000000 7.977538 -0.000000 -0.001779 0.044549 0.030951 -0.000001 13.487594 -0.000000 -0.030764 -0.082259 -0.353265 0.000000 4.943967 0.000000 0.000361 0.000378 -0.403283 -1.626874 11.081237 -7.581213 0.972424 -0.054681 -0.001101 0.000000 12.910863 -0.000000 0.296768 -0.029812 -0.089659 -0.031254 11.193578 0.010376 -0.060908 -0.000267 -0.004304 0.054595 0.018457 -0.015135 0.177324 -0.062355 -0.334922 0.000000 3.968525 0.000000 0.039691 0.055327 0.355585 0.000000 5.348644 -0.000000 0.334266 0.042215 0.017963 0.000000 2.438549 0.000000 0.530197 0.102228 -0.096753 -0.655277 1.265096 -0.625148 -0.193708 -0.908580 -0.302376 0.978884 10.880526 7.905150 -0.943543 -0.265304 -0.080549 0.000000 13.379274 -0.000000 -0.180379 0.161234 -0.612912 0.028222 10.991364 0.020885 0.008257 -0.110219 -0.243008 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.261482 3.709407 5.247099 0.026051 -0.012855 0.997330 0.000000 13.074334 0.000000 -0.020504 -0.084629 -0.037977 -0.035381 14.148866 0.000001 -0.000213 0.010438 0.504591 0.000000 4.097309 -0.000000 -0.000146 -0.001213 0.253449 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.218260 0.023873 0.020160 0.997333 -0.000000 13.074365 0.000000 0.025905 0.004986 -0.039393 -0.035380 14.148879 0.012529 0.000088 -0.008070 0.504136 -0.000000 4.097290 0.000000 -0.000000 -0.000001 0.256038 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 2 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.215008 6.221424 9.293080 0.027615 -0.135230 -0.850725 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008462 0.042959 -0.136125 0.000000 7.977538 -0.000000 -0.001672 0.044535 0.031175 -0.000001 13.487594 -0.000000 -0.031527 -0.083926 -0.353388 0.000000 4.943967 0.000000 0.001402 0.001432 -0.402644 -1.626597 11.082675 -7.581272 0.972484 -0.054098 -0.001097 0.000000 12.910863 -0.000000 0.294122 -0.028704 -0.093535 -0.031254 11.193578 0.010376 -0.061256 -0.000289 -0.004419 0.054595 0.018457 -0.015135 0.177714 -0.062359 -0.334675 0.000000 3.968525 0.000000 0.039628 0.055417 0.355713 0.000000 5.348644 -0.000000 0.334895 0.042290 0.018458 0.000000 2.438549 0.000000 0.529833 0.102440 -0.096994 -0.655277 1.265097 -0.625148 -0.195311 -0.909263 -0.299383 0.981969 10.896532 7.904502 -0.944289 -0.262231 -0.080920 0.000000 13.379274 -0.000000 -0.181182 0.162258 -0.609644 0.028222 10.991364 0.020885 0.006879 -0.109889 -0.243673 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.256169 3.709961 5.254688 0.026058 -0.013027 0.997316 0.000000 13.074334 0.000000 -0.020696 -0.084608 -0.037496 -0.035381 14.148866 0.000001 -0.000222 0.010433 0.504669 0.000000 4.097309 -0.000000 -0.000144 -0.001206 0.253373 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.211812 0.024811 0.020008 0.997321 -0.000000 13.074365 0.000000 0.025677 0.004070 -0.039613 -0.035380 14.148879 0.012529 0.000087 -0.008070 0.504122 -0.000000 4.097290 0.000000 0.000000 -0.000001 0.256055 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 3 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.207887 6.205945 9.322063 0.030270 -0.136042 -0.849945 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008336 0.042270 -0.135643 0.000000 7.977538 -0.000000 -0.001548 0.044520 0.031410 -0.000001 13.487594 -0.000000 -0.032655 -0.086386 -0.353578 0.000000 4.943967 0.000000 0.003044 0.003094 -0.401634 -1.626164 11.084918 -7.581362 0.972577 -0.053196 -0.001087 0.000000 12.910863 -0.000000 0.291352 -0.027580 -0.097441 -0.031254 11.193578 0.010376 -0.061388 -0.000297 -0.004462 0.054595 0.018457 -0.015135 0.177863 -0.062361 -0.334582 0.000000 3.968525 0.000000 0.039605 0.055451 0.355762 0.000000 5.348644 -0.000000 0.335134 0.042318 0.018646 0.000000 2.438549 0.000000 0.529695 0.102520 -0.097086 -0.655277 1.265097 -0.625148 -0.196850 -0.910011 -0.296204 0.985061 10.912575 7.903854 -0.945044 -0.259076 -0.081320 0.000000 13.379274 -0.000000 -0.181983 0.163342 -0.606268 0.028222 10.991364 0.020885 0.005446 -0.109519 -0.244415 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.249868 3.710721 5.263806 0.026066 -0.013232 0.997296 0.000000 13.074334 0.000000 -0.020929 -0.084584 -0.036872 -0.035381 14.148866 0.000001 -0.000231 0.010429 0.504748 0.000000 4.097309 -0.000000 -0.000140 -0.001193 0.253251 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.204082 0.025934 0.019826 0.997304 -0.000000 13.074365 0.000000 0.025405 0.002971 -0.039835 -0.035380 14.148879 0.012529 0.000086 -0.008070 0.504108 -0.000000 4.097290 0.000000 0.000001 0.000001 0.256082 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 4 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.200696 6.190378 9.351347 0.032944 -0.136860 -0.849151 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008168 0.041350 -0.134998 0.000000 7.977538 -0.000000 -0.001409 0.044504 0.031654 -0.000001 13.487594 -0.000000 -0.034045 -0.089413 -0.353825 0.000000 4.943967 0.000000 0.005220 0.005297 -0.400292 -1.625597 11.087862 -7.581481 0.972697 -0.052022 -0.001070 0.000000 12.910863 -0.000000 0.288496 -0.026444 -0.101367 -0.031254 11.193578 0.010376 -0.061336 -0.000294 -0.004445 0.054595 0.018457 -0.015135 0.177804 -0.062360 -0.334619 0.000000 3.968525 0.000000 0.039614 0.055438 0.355743 0.000000 5.348644 -0.000000 0.335039 0.042307 0.018571 0.000000 2.438549 0.000000 0.529749 0.102488 -0.097050 -0.655277 1.265097 -0.625148 -0.198344 -0.910796 -0.292900 0.988158 10.928642 7.903204 -0.945801 -0.255865 -0.081738 0.000000 13.379274 -0.000000 -0.182781 0.164463 -0.602815 0.028222 10.991364 0.020885 0.003978 -0.109123 -0.245208 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.242681 3.711664 5.274293 0.026076 -0.013468 0.997273 0.000000 13.074334 0.000000 -0.021197 -0.084556 -0.036120 -0.035381 14.148866 0.000001 -0.000240 0.010425 0.504827 0.000000 4.097309 -0.000000 -0.000134 -0.001177 0.253089 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.195203 0.027224 0.019618 0.997282 -0.000000 13.074365 0.000000 0.025095 0.001710 -0.040057 -0.035380 14.148879 0.012529 0.000085 -0.008071 0.504094 -0.000000 4.097290 0.000000 0.000002 0.000002 0.256119 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 5 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.193465 6.174762 9.380800 0.035627 -0.137680 -0.848347 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.007961 0.040219 -0.134204 0.000000 7.977538 -0.000000 -0.001256 0.044486 0.031906 -0.000001 13.487594 -0.000000 -0.035605 -0.092806 -0.354120 0.000000 4.943967 0.000000 0.007869 0.007980 -0.398650 -1.624912 11.091417 -7.581625 0.972840 -0.050617 -0.001044 0.000000 12.910863 -0.000000 0.285584 -0.025301 -0.105308 -0.031254 11.193578 0.010376 -0.061127 -0.000281 -0.004376 0.054595 0.018457 -0.015135 0.177570 -0.062358 -0.334767 0.000000 3.968525 0.000000 0.039651 0.055384 0.355666 0.000000 5.348644 -0.000000 0.334661 0.042262 0.018274 0.000000 2.438549 0.000000 0.529968 0.102361 -0.096905 -0.655277 1.265097 -0.625148 -0.199811 -0.911596 -0.289527 0.991257 10.944722 7.902553 -0.946554 -0.252620 -0.082167 0.000000 13.379274 -0.000000 -0.183576 0.165605 -0.599314 0.028222 10.991364 0.020885 0.002489 -0.108712 -0.246029 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.234705 3.712772 5.285999 0.026087 -0.013730 0.997245 0.000000 13.074334 0.000000 -0.021497 -0.084525 -0.035254 -0.035381 14.148866 0.000001 -0.000249 0.010420 0.504906 0.000000 4.097309 -0.000000 -0.000128 -0.001157 0.252890 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.185301 0.028661 0.019386 0.997255 -0.000000 13.074365 0.000000 0.024751 0.000304 -0.040281 -0.035380 14.148879 0.012529 0.000084 -0.008071 0.504080 -0.000000 4.097290 0.000000 0.000003 0.000004 0.256165 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 6 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.186224 6.159133 9.410300 0.038313 -0.138499 -0.847533 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.007720 0.038901 -0.133277 0.000000 7.977538 -0.000000 -0.001092 0.044466 0.032166 -0.000001 13.487594 -0.000000 -0.037251 -0.096380 -0.354456 0.000000 4.943967 0.000000 0.010938 0.011086 -0.396741 -1.624124 11.095505 -7.581790 0.973000 -0.049017 -0.001007 0.000000 12.910863 -0.000000 0.282649 -0.024153 -0.109255 -0.031254 11.193578 0.010376 -0.060787 -0.000260 -0.004264 0.054595 0.018457 -0.015135 0.177188 -0.062353 -0.335007 0.000000 3.968525 0.000000 0.039713 0.055296 0.355540 0.000000 5.348644 -0.000000 0.334044 0.042189 0.017789 0.000000 2.438549 0.000000 0.530324 0.102154 -0.096669 -0.655277 1.265097 -0.625148 -0.201267 -0.912390 -0.286135 0.994357 10.960806 7.901903 -0.947297 -0.249364 -0.082597 0.000000 13.379274 -0.000000 -0.184367 0.166750 -0.595790 0.028222 10.991364 0.020885 0.000995 -0.108296 -0.246856 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.226033 3.714024 5.298781 0.026099 -0.014016 0.997214 0.000000 13.074334 0.000000 -0.021825 -0.084492 -0.034288 -0.035381 14.148866 0.000001 -0.000259 0.010416 0.504986 0.000000 4.097309 -0.000000 -0.000121 -0.001134 0.252658 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.174498 0.030230 0.019133 0.997222 -0.000000 13.074365 0.000000 0.024376 -0.001230 -0.040506 -0.035380 14.148879 0.012529 0.000083 -0.008071 0.504065 -0.000000 4.097290 0.000000 0.000004 0.000007 0.256220 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 7 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.178999 6.143524 9.439730 0.040993 -0.139315 -0.846713 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.007448 0.037416 -0.132231 0.000000 7.977538 -0.000000 -0.000918 0.044446 0.032431 -0.000001 13.487594 -0.000000 -0.038907 -0.099964 -0.354827 0.000000 4.943967 0.000000 0.014375 0.014566 -0.394591 -1.623247 11.100056 -7.581974 0.973174 -0.047257 -0.000958 0.000000 12.910863 -0.000000 0.279718 -0.023005 -0.113203 -0.031254 11.193578 0.010376 -0.060339 -0.000232 -0.004116 0.054595 0.018457 -0.015135 0.176685 -0.062348 -0.335324 0.000000 3.968525 0.000000 0.039793 0.055181 0.355375 0.000000 5.348644 -0.000000 0.333233 0.042093 0.017152 0.000000 2.438549 0.000000 0.530792 0.101881 -0.096358 -0.655277 1.265097 -0.625148 -0.202727 -0.913158 -0.282773 0.997455 10.976885 7.901252 -0.948027 -0.246118 -0.083022 0.000000 13.379274 -0.000000 -0.185155 0.167883 -0.592269 0.028222 10.991364 0.020885 -0.000490 -0.107887 -0.247671 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.216753 3.715403 5.312502 0.026112 -0.014322 0.997180 0.000000 13.074334 0.000000 -0.022177 -0.084456 -0.033233 -0.035381 14.148866 0.000001 -0.000268 0.010411 0.505067 0.000000 4.097309 -0.000000 -0.000112 -0.001107 0.252396 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.162906 0.031911 0.018862 0.997184 -0.000000 13.074365 0.000000 0.023975 -0.002875 -0.040731 -0.035380 14.148879 0.012529 0.000082 -0.008071 0.504051 -0.000000 4.097290 0.000000 0.000005 0.000010 0.256283 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 8 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.171816 6.127967 9.468980 0.043662 -0.140127 -0.845889 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.007150 0.035782 -0.131077 0.000000 7.977538 -0.000000 -0.000734 0.044425 0.032702 -0.000001 13.487594 -0.000000 -0.040497 -0.103394 -0.355231 0.000000 4.943967 0.000000 0.018133 0.018370 -0.392226 -1.622293 11.105005 -7.582175 0.973359 -0.045366 -0.000894 0.000000 12.910863 -0.000000 0.276821 -0.021860 -0.117145 -0.031254 11.193578 0.010376 -0.059806 -0.000199 -0.003940 0.054595 0.018457 -0.015135 0.176086 -0.062341 -0.335702 0.000000 3.968525 0.000000 0.039889 0.055043 0.355179 0.000000 5.348644 -0.000000 0.332266 0.041978 0.016392 0.000000 2.438549 0.000000 0.531349 0.101555 -0.095987 -0.655277 1.265097 -0.625148 -0.204207 -0.913883 -0.279485 1.000551 10.992949 7.900603 -0.948740 -0.242900 -0.083435 0.000000 13.379274 -0.000000 -0.185939 0.168988 -0.588776 0.028222 10.991364 0.020885 -0.001952 -0.107493 -0.248454 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.206950 3.716890 5.327032 0.026126 -0.014647 0.997143 0.000000 13.074334 0.000000 -0.022550 -0.084419 -0.032103 -0.035381 14.148866 0.000001 -0.000278 0.010407 0.505147 0.000000 4.097309 -0.000000 -0.000103 -0.001078 0.252109 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.150635 0.033691 0.018575 0.997139 -0.000000 13.074365 0.000000 0.023552 -0.004616 -0.040957 -0.035380 14.148879 0.012529 0.000082 -0.008072 0.504036 -0.000000 4.097290 0.000000 0.000007 0.000013 0.256354 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 9 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.164699 6.112494 9.497941 0.046311 -0.140930 -0.845063 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.006827 0.034020 -0.129830 0.000000 7.977538 -0.000000 -0.000544 0.044403 0.032978 -0.000001 13.487594 -0.000000 -0.041949 -0.106510 -0.355664 0.000000 4.943967 0.000000 0.022168 0.022455 -0.389672 -1.621274 11.110295 -7.582389 0.973552 -0.043372 -0.000815 0.000000 12.910863 -0.000000 0.273984 -0.020720 -0.121075 -0.031254 11.193578 0.010376 -0.059207 -0.000162 -0.003742 0.054595 0.018457 -0.015135 0.175413 -0.062333 -0.336126 0.000000 3.968525 0.000000 0.039996 0.054888 0.354958 0.000000 5.348644 -0.000000 0.331181 0.041849 0.015541 0.000000 2.438549 0.000000 0.531975 0.101190 -0.095570 -0.655277 1.265097 -0.625148 -0.205721 -0.914548 -0.276318 1.003643 11.008990 7.899954 -0.949432 -0.239731 -0.083828 0.000000 13.379274 -0.000000 -0.186720 0.170050 -0.585334 0.028222 10.991364 0.020885 -0.003378 -0.107124 -0.249186 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.196703 3.718469 5.342248 0.026141 -0.014986 0.997104 0.000000 13.074334 0.000000 -0.022942 -0.084379 -0.030909 -0.035381 14.148866 0.000001 -0.000287 0.010402 0.505228 0.000000 4.097309 -0.000000 -0.000093 -0.001047 0.251799 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.137790 0.035553 0.018275 0.997089 -0.000000 13.074365 0.000000 0.023109 -0.006438 -0.041183 -0.035380 14.148879 0.012529 0.000081 -0.008072 0.504022 -0.000000 4.097290 0.000000 0.000009 0.000016 0.256431 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 10 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.157675 6.097137 9.526503 0.048935 -0.141724 -0.844237 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.006485 0.032146 -0.128500 0.000000 7.977538 -0.000000 -0.000346 0.044380 0.033257 -0.000001 13.487594 -0.000000 -0.043192 -0.109156 -0.356126 0.000000 4.943967 0.000000 0.026438 0.026778 -0.386951 -1.620199 11.115870 -7.582614 0.973749 -0.041302 -0.000717 0.000000 12.910863 -0.000000 0.271236 -0.019590 -0.124986 -0.031254 11.193578 0.010376 -0.058562 -0.000122 -0.003529 0.054595 0.018457 -0.015135 0.174688 -0.062324 -0.336582 0.000000 3.968525 0.000000 0.040112 0.054721 0.354720 0.000000 5.348644 -0.000000 0.330012 0.041711 0.014624 0.000000 2.438549 0.000000 0.532648 0.100797 -0.095122 -0.655277 1.265097 -0.625148 -0.207284 -0.915136 -0.273316 1.006728 11.024999 7.899306 -0.950099 -0.236630 -0.084194 0.000000 13.379274 -0.000000 -0.187497 0.171056 -0.581969 0.028222 10.991364 0.020885 -0.004755 -0.106790 -0.249849 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.186091 3.720124 5.358028 0.026156 -0.015337 0.997062 0.000000 13.074334 0.000000 -0.023347 -0.084338 -0.029663 -0.035381 14.148866 0.000001 -0.000297 0.010398 0.505309 0.000000 4.097309 -0.000000 -0.000082 -0.001014 0.251469 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.124470 0.037483 0.017964 0.997033 -0.000000 13.074365 0.000000 0.022650 -0.008326 -0.041409 -0.035380 14.148879 0.012529 0.000080 -0.008072 0.504007 -0.000000 4.097290 0.000000 0.000011 0.000020 0.256516 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 11 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.150769 6.081928 9.554554 0.051526 -0.142506 -0.843415 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.006126 0.030179 -0.127100 0.000000 7.977538 -0.000000 -0.000144 0.044356 0.033539 -0.000001 13.487594 -0.000000 -0.044153 -0.111170 -0.356616 0.000000 4.943967 0.000000 0.030903 0.031298 -0.384087 -1.619080 11.121678 -7.582849 0.973948 -0.039182 -0.000599 0.000000 12.910863 -0.000000 0.268606 -0.018473 -0.128873 -0.031254 11.193578 0.010376 -0.057890 -0.000080 -0.003307 0.054595 0.018457 -0.015135 0.173933 -0.062315 -0.337057 0.000000 3.968525 0.000000 0.040233 0.054548 0.354472 0.000000 5.348644 -0.000000 0.328794 0.041566 0.013670 0.000000 2.438549 0.000000 0.533349 0.100387 -0.094655 -0.655277 1.265097 -0.625148 -0.208911 -0.915632 -0.270526 1.009806 11.040966 7.898661 -0.950738 -0.233617 -0.084526 0.000000 13.379274 -0.000000 -0.188271 0.171989 -0.578707 0.028222 10.991364 0.020885 -0.006069 -0.106499 -0.250423 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.175188 3.721840 5.374259 0.026172 -0.015698 0.997019 0.000000 13.074334 0.000000 -0.023765 -0.084296 -0.028375 -0.035381 14.148866 0.000001 -0.000306 0.010393 0.505390 0.000000 4.097309 -0.000000 -0.000071 -0.000979 0.251124 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.110774 0.039467 0.017644 0.996970 -0.000000 13.074365 0.000000 0.022178 -0.010267 -0.041636 -0.035380 14.148879 0.012529 0.000079 -0.008072 0.503992 -0.000000 4.097290 0.000000 0.000013 0.000024 0.256606 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 12 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.144008 6.066902 9.581977 0.054077 -0.143274 -0.842598 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.005753 0.028135 -0.125642 0.000000 7.977538 -0.000000 0.000061 0.044333 0.033824 -0.000001 13.487594 -0.000000 -0.044757 -0.112388 -0.357135 0.000000 4.943967 0.000000 0.035523 0.035975 -0.381102 -1.617925 11.127669 -7.583091 0.974146 -0.037037 -0.000459 0.000000 12.910863 -0.000000 0.266124 -0.017372 -0.132729 -0.031254 11.193578 0.010376 -0.057208 -0.000038 -0.003082 0.054595 0.018457 -0.015135 0.173167 -0.062306 -0.337538 0.000000 3.968525 0.000000 0.040356 0.054371 0.354220 0.000000 5.348644 -0.000000 0.327560 0.041420 0.012703 0.000000 2.438549 0.000000 0.534058 0.099971 -0.094181 -0.655277 1.265097 -0.625148 -0.210619 -0.916019 -0.267996 1.012873 11.056883 7.898017 -0.951346 -0.230714 -0.084817 0.000000 13.379274 -0.000000 -0.189043 0.172835 -0.575574 0.028222 10.991364 0.020885 -0.007306 -0.106264 -0.250890 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.164067 3.723601 5.390826 0.026189 -0.016067 0.996974 0.000000 13.074334 0.000000 -0.024191 -0.084252 -0.027055 -0.035381 14.148866 0.000001 -0.000316 0.010389 0.505472 0.000000 4.097309 -0.000000 -0.000059 -0.000943 0.250765 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.096794 0.041491 0.017318 0.996903 -0.000000 13.074365 0.000000 0.021697 -0.012247 -0.041863 -0.035380 14.148879 0.012529 0.000078 -0.008073 0.503978 -0.000000 4.097290 0.000000 0.000015 0.000028 0.256703 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 13 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.137421 6.052094 9.608648 0.056580 -0.144024 -0.841791 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.005369 0.026030 -0.124135 0.000000 7.977538 -0.000000 0.000270 0.044309 0.034110 -0.000001 13.487594 -0.000000 -0.044925 -0.112633 -0.357683 0.000000 4.943967 0.000000 0.040261 0.040772 -0.378018 -1.616745 11.133792 -7.583339 0.974343 -0.034893 -0.000294 0.000000 12.910863 -0.000000 0.263822 -0.016292 -0.136548 -0.031254 11.193578 0.010376 -0.056534 0.000004 -0.002860 0.054595 0.018457 -0.015135 0.172410 -0.062297 -0.338013 0.000000 3.968525 0.000000 0.040477 0.054197 0.353972 0.000000 5.348644 -0.000000 0.326341 0.041275 0.011748 0.000000 2.438549 0.000000 0.534759 0.099560 -0.093713 -0.655277 1.265097 -0.625148 -0.212425 -0.916278 -0.265776 1.015929 11.072739 7.897376 -0.951920 -0.227943 -0.085058 0.000000 13.379274 -0.000000 -0.189812 0.173577 -0.572598 0.028222 10.991364 0.020885 -0.008451 -0.106093 -0.251227 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.152797 3.725393 5.407622 0.026205 -0.016441 0.996928 0.000000 13.074334 0.000000 -0.024622 -0.084208 -0.025714 -0.035381 14.148866 0.000001 -0.000325 0.010384 0.505553 0.000000 4.097309 -0.000000 -0.000047 -0.000906 0.250395 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.082623 0.043542 0.016987 0.996830 -0.000000 13.074365 0.000000 0.021210 -0.014254 -0.042089 -0.035380 14.148879 0.012529 0.000077 -0.008073 0.503963 -0.000000 4.097290 0.000000 0.000018 0.000033 0.256805 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 14 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.131038 6.037544 9.634434 0.059026 -0.144755 -0.840995 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.004977 0.023881 -0.122591 0.000000 7.977538 -0.000000 0.000479 0.044285 0.034397 -0.000001 13.487594 -0.000000 -0.044570 -0.111716 -0.358259 0.000000 4.943967 0.000000 0.045080 0.045650 -0.374859 -1.615549 11.139997 -7.583590 0.974535 -0.032775 -0.000103 0.000000 12.910863 -0.000000 0.261735 -0.015235 -0.140321 -0.031254 11.193578 0.010376 -0.055886 0.000044 -0.002646 0.054595 0.018457 -0.015135 0.171682 -0.062289 -0.338470 0.000000 3.968525 0.000000 0.040593 0.054030 0.353733 0.000000 5.348644 -0.000000 0.325168 0.041136 0.010831 0.000000 2.438549 0.000000 0.535432 0.099165 -0.093263 -0.655277 1.265097 -0.625148 -0.214347 -0.916391 -0.263921 1.018971 11.088524 7.896737 -0.952456 -0.225328 -0.085242 0.000000 13.379274 -0.000000 -0.190579 0.174198 -0.569812 0.028222 10.991364 0.020885 -0.009488 -0.106000 -0.251413 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.141449 3.727201 5.424541 0.026222 -0.016817 0.996881 0.000000 13.074334 0.000000 -0.025057 -0.084164 -0.024362 -0.035381 14.148866 0.000001 -0.000335 0.010380 0.505634 0.000000 4.097309 -0.000000 -0.000035 -0.000868 0.250018 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.068349 0.045607 0.016654 0.996752 -0.000000 13.074365 0.000000 0.020720 -0.016274 -0.042316 -0.035380 14.148879 0.012529 0.000076 -0.008073 0.503948 -0.000000 4.097290 0.000000 0.000020 0.000037 0.256912 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 15 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.124895 6.023293 9.659188 0.061408 -0.145463 -0.840215 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.004580 0.021702 -0.121020 0.000000 7.977538 -0.000000 0.000689 0.044260 0.034684 -0.000001 13.487594 -0.000000 -0.043599 -0.109427 -0.358862 0.000000 4.943967 0.000000 0.049944 0.050574 -0.371647 -1.614348 11.146232 -7.583842 0.974721 -0.030711 0.000120 0.000000 12.910863 -0.000000 0.259902 -0.014207 -0.144042 -0.031254 11.193578 0.010376 -0.055281 0.000081 -0.002446 0.054595 0.018457 -0.015135 0.171002 -0.062281 -0.338896 0.000000 3.968525 0.000000 0.040702 0.053873 0.353509 0.000000 5.348644 -0.000000 0.324073 0.041007 0.009975 0.000000 2.438549 0.000000 0.536061 0.098795 -0.092842 -0.655277 1.265097 -0.625148 -0.216405 -0.916336 -0.262492 1.021997 11.104226 7.896102 -0.952950 -0.222895 -0.085358 0.000000 13.379274 -0.000000 -0.191345 0.174677 -0.567249 0.028222 10.991364 0.020885 -0.010398 -0.105996 -0.251422 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.130087 3.729012 5.441480 0.026238 -0.017194 0.996833 0.000000 13.074334 0.000000 -0.025492 -0.084119 -0.023009 -0.035381 14.148866 0.000001 -0.000344 0.010375 0.505716 0.000000 4.097309 -0.000000 -0.000023 -0.000829 0.249636 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.054058 0.047673 0.016320 0.996670 -0.000000 13.074365 0.000000 0.020229 -0.018296 -0.042542 -0.035380 14.148879 0.012529 0.000075 -0.008073 0.503933 -0.000000 4.097290 0.000000 0.000023 0.000042 0.257023 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 16 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.119028 6.009390 9.682744 0.063714 -0.146145 -0.839455 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.004181 0.019508 -0.119432 0.000000 7.977538 -0.000000 0.000898 0.044236 0.034971 -0.000001 13.487594 -0.000000 -0.041903 -0.105530 -0.359485 0.000000 4.943967 0.000000 0.054815 0.055505 -0.368406 -1.613151 11.152442 -7.584093 0.974901 -0.028730 0.000376 0.000000 12.910863 -0.000000 0.258365 -0.013213 -0.147701 -0.031254 11.193578 0.010376 -0.054736 0.000115 -0.002266 0.054595 0.018457 -0.015135 0.170390 -0.062273 -0.339280 0.000000 3.968525 0.000000 0.040800 0.053732 0.353308 0.000000 5.348644 -0.000000 0.323088 0.040890 0.009205 0.000000 2.438549 0.000000 0.536626 0.098463 -0.092464 -0.655277 1.265097 -0.625148 -0.218623 -0.916089 -0.261555 1.025005 11.119831 7.895471 -0.953400 -0.220674 -0.085397 0.000000 13.379274 -0.000000 -0.192111 0.174995 -0.564948 0.028222 10.991364 0.020885 -0.011161 -0.106096 -0.251227 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.118779 3.730811 5.458336 0.026255 -0.017569 0.996786 0.000000 13.074334 0.000000 -0.025925 -0.084075 -0.021663 -0.035381 14.148866 0.000001 -0.000354 0.010371 0.505797 0.000000 4.097309 -0.000000 -0.000010 -0.000791 0.249252 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.039836 0.049728 0.015989 0.996584 -0.000000 13.074365 0.000000 0.019740 -0.020308 -0.042768 -0.035380 14.148879 0.012529 0.000074 -0.008074 0.503918 -0.000000 4.097290 0.000000 0.000025 0.000047 0.257139 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 17 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.113481 5.995889 9.704917 0.065933 -0.146798 -0.838718 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.003781 0.017315 -0.117837 0.000000 7.977538 -0.000000 0.001105 0.044212 0.035256 -0.000001 13.487594 -0.000000 -0.039361 -0.099752 -0.360120 0.000000 4.943967 0.000000 0.059657 0.060407 -0.365160 -1.611971 11.158567 -7.584341 0.975071 -0.026863 0.000669 0.000000 12.910863 -0.000000 0.257174 -0.012259 -0.151288 -0.031254 11.193578 0.010376 -0.054269 0.000144 -0.002112 0.054595 0.018457 -0.015135 0.169865 -0.062267 -0.339609 0.000000 3.968525 0.000000 0.040884 0.053611 0.353135 0.000000 5.348644 -0.000000 0.322243 0.040789 0.008546 0.000000 2.438549 0.000000 0.537110 0.098178 -0.092139 -0.655277 1.265097 -0.625148 -0.221025 -0.915621 -0.261186 1.027991 11.135325 7.894844 -0.953799 -0.218698 -0.085346 0.000000 13.379274 -0.000000 -0.192876 0.175125 -0.562951 0.028222 10.991364 0.020885 -0.011756 -0.106316 -0.250795 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.107589 3.732587 5.475010 0.026271 -0.017940 0.996738 0.000000 13.074334 0.000000 -0.026353 -0.084030 -0.020335 -0.035381 14.148866 0.000001 -0.000363 0.010366 0.505878 0.000000 4.097309 -0.000000 0.000002 -0.000752 0.248869 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.025767 0.051760 0.015660 0.996494 -0.000000 13.074365 0.000000 0.019257 -0.022297 -0.042993 -0.035380 14.148879 0.012529 0.000073 -0.008074 0.503904 -0.000000 4.097290 0.000000 0.000028 0.000053 0.257259 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 18 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.108304 5.982852 9.725492 0.068052 -0.147416 -0.838012 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.003385 0.015136 -0.116246 0.000000 7.977538 -0.000000 0.001309 0.044189 0.035539 -0.000001 13.487594 -0.000000 -0.035829 -0.091775 -0.360750 0.000000 4.943967 0.000000 0.064433 0.065243 -0.361935 -1.610819 11.164543 -7.584582 0.975231 -0.025145 0.001006 0.000000 12.910863 -0.000000 0.256383 -0.011350 -0.154792 -0.031254 11.193578 0.010376 -0.053898 0.000167 -0.001989 0.054595 0.018457 -0.015135 0.169448 -0.062262 -0.339870 0.000000 3.968525 0.000000 0.040950 0.053515 0.352998 0.000000 5.348644 -0.000000 0.321573 0.040710 0.008022 0.000000 2.438549 0.000000 0.537495 0.097952 -0.091881 -0.655277 1.265097 -0.625148 -0.223642 -0.914898 -0.261473 1.030952 11.150689 7.894223 -0.954143 -0.217007 -0.085191 0.000000 13.379274 -0.000000 -0.193643 0.175040 -0.561307 0.028222 10.991364 0.020885 -0.012156 -0.106675 -0.250091 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.096580 3.734324 5.491404 0.026287 -0.018304 0.996691 0.000000 13.074334 0.000000 -0.026774 -0.083986 -0.019034 -0.035381 14.148866 0.000001 -0.000373 0.010362 0.505960 0.000000 4.097309 -0.000000 0.000014 -0.000714 0.248488 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.011934 0.053757 0.015338 0.996402 -0.000000 13.074365 0.000000 0.018781 -0.024252 -0.043218 -0.035380 14.148879 0.012529 0.000072 -0.008074 0.503889 -0.000000 4.097290 0.000000 0.000031 0.000058 0.257383 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 19 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.103555 5.970353 9.744214 0.070056 -0.147996 -0.837341 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.002995 0.012987 -0.114666 0.000000 7.977538 -0.000000 0.001509 0.044166 0.035820 -0.000001 13.487594 -0.000000 -0.031136 -0.081222 -0.361347 0.000000 4.943967 0.000000 0.069107 0.069974 -0.358758 -1.609710 11.170296 -7.584815 0.975380 -0.023616 0.001390 0.000000 12.910863 -0.000000 0.256059 -0.010494 -0.158200 -0.031254 11.193578 0.010376 -0.053641 0.000183 -0.001904 0.054595 0.018457 -0.015135 0.169160 -0.062258 -0.340050 0.000000 3.968525 0.000000 0.040997 0.053449 0.352903 0.000000 5.348644 -0.000000 0.321109 0.040655 0.007660 0.000000 2.438549 0.000000 0.537760 0.097795 -0.091703 -0.655277 1.265097 -0.625148 -0.226506 -0.913876 -0.262518 1.033884 11.165905 7.893607 -0.954425 -0.215646 -0.084917 0.000000 13.379274 -0.000000 -0.194412 0.174707 -0.560075 0.028222 10.991364 0.020885 -0.012330 -0.107194 -0.249071 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.085815 3.736011 5.507420 0.026302 -0.018661 0.996645 0.000000 13.074334 0.000000 -0.027184 -0.083943 -0.017768 -0.035381 14.148866 0.000001 -0.000382 0.010357 0.506041 0.000000 4.097309 -0.000000 0.000026 -0.000676 0.248113 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.998417 0.055708 0.015022 0.996309 -0.000000 13.074365 0.000000 0.018316 -0.026161 -0.043442 -0.035380 14.148879 0.012529 0.000071 -0.008074 0.503874 -0.000000 4.097290 0.000000 0.000034 0.000064 0.257510 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 20 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.098596 5.957820 9.761416 0.071980 -0.148538 -0.836689 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.002612 0.010880 -0.113109 0.000000 7.977538 -0.000000 0.001703 0.044143 0.036098 -0.000001 13.487594 -0.000000 -0.024225 -0.065733 -0.361896 0.000000 4.943967 0.000000 0.073639 0.074562 -0.355655 -1.608724 11.175412 -7.585022 0.975533 -0.022124 0.001682 0.000000 12.910863 -0.000000 0.256305 -0.009800 -0.161100 -0.031254 11.193578 0.010376 -0.053519 0.000191 -0.001864 0.054595 0.018457 -0.015135 0.169022 -0.062256 -0.340136 0.000000 3.968525 0.000000 0.041019 0.053417 0.352858 0.000000 5.348644 -0.000000 0.320887 0.040629 0.007487 0.000000 2.438549 0.000000 0.537887 0.097720 -0.091618 -0.655277 1.265097 -0.625148 -0.229588 -0.912431 -0.264748 1.036812 11.181094 7.892993 -0.954643 -0.214633 -0.084491 0.000000 13.379274 -0.000000 -0.195246 0.174104 -0.559301 0.028222 10.991364 0.020885 -0.012300 -0.108042 -0.247657 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.075358 3.737634 5.522961 0.026317 -0.019007 0.996600 0.000000 13.074334 0.000000 -0.027582 -0.083902 -0.016546 -0.035381 14.148866 0.000001 -0.000391 0.010353 0.506122 0.000000 4.097309 -0.000000 0.000038 -0.000639 0.247746 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.985298 0.057600 0.014716 0.996214 -0.000000 13.074365 0.000000 0.017865 -0.028013 -0.043666 -0.035380 14.148879 0.012529 0.000070 -0.008075 0.503860 -0.000000 4.097290 0.000000 0.000037 0.000070 0.257640 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 21 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.092965 5.944811 9.777611 0.073865 -0.149050 -0.836038 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.002241 0.008831 -0.111584 0.000000 7.977538 -0.000000 0.001891 0.044121 0.036372 -0.000001 13.487594 -0.000000 -0.014862 -0.044781 -0.362307 0.000000 4.943967 0.000000 0.077989 0.078966 -0.352657 -1.607893 11.179723 -7.585196 0.975704 -0.020524 0.001790 0.000000 12.910863 -0.000000 0.257107 -0.009324 -0.163254 -0.031254 11.193578 0.010376 -0.053551 0.000189 -0.001874 0.054595 0.018457 -0.015135 0.169058 -0.062257 -0.340114 0.000000 3.968525 0.000000 0.041013 0.053426 0.352870 0.000000 5.348644 -0.000000 0.320945 0.040635 0.007532 0.000000 2.438549 0.000000 0.537854 0.097740 -0.091640 -0.655277 1.265097 -0.625148 -0.232822 -0.910527 -0.268293 1.039757 11.196375 7.892375 -0.954807 -0.213925 -0.083914 0.000000 13.379274 -0.000000 -0.196185 0.173260 -0.558945 0.028222 10.991364 0.020885 -0.012118 -0.109292 -0.245854 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.065270 3.739181 5.537932 0.026332 -0.019340 0.996557 0.000000 13.074334 0.000000 -0.027966 -0.083861 -0.015378 -0.035381 14.148866 0.000001 -0.000401 0.010348 0.506202 0.000000 4.097309 -0.000000 0.000050 -0.000604 0.247390 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.972658 0.059422 0.014422 0.996120 -0.000000 13.074365 0.000000 0.017430 -0.029798 -0.043889 -0.035380 14.148879 0.012529 0.000070 -0.008075 0.503845 -0.000000 4.097290 0.000000 0.000040 0.000075 0.257773 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 22 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.086961 5.931582 9.792780 0.075704 -0.149534 -0.835395 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.001883 0.006852 -0.110100 0.000000 7.977538 -0.000000 0.002072 0.044100 0.036641 -0.000001 13.487594 -0.000000 -0.003904 -0.020274 -0.362458 0.000000 4.943967 0.000000 0.082117 0.083145 -0.349795 -1.607166 11.183493 -7.585349 0.975884 -0.018873 0.001782 0.000000 12.910863 -0.000000 0.258374 -0.009006 -0.164887 -0.031254 11.193578 0.010376 -0.053759 0.000176 -0.001943 0.054595 0.018457 -0.015135 0.169292 -0.062260 -0.339967 0.000000 3.968525 0.000000 0.040975 0.053480 0.352947 0.000000 5.348644 -0.000000 0.321322 0.040680 0.007826 0.000000 2.438549 0.000000 0.537638 0.097867 -0.091785 -0.655277 1.265097 -0.625148 -0.236202 -0.908238 -0.272862 1.042715 11.211722 7.891754 -0.954922 -0.213486 -0.083212 0.000000 13.379274 -0.000000 -0.197195 0.172208 -0.558947 0.028222 10.991364 0.020885 -0.011790 -0.110844 -0.243728 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.055613 3.740638 5.552236 0.026345 -0.019659 0.996516 0.000000 13.074334 0.000000 -0.028331 -0.083822 -0.014273 -0.035381 14.148866 0.000001 -0.000410 0.010344 0.506283 0.000000 4.097309 -0.000000 0.000061 -0.000569 0.247047 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.960577 0.061163 0.014140 0.996028 -0.000000 13.074365 0.000000 0.017014 -0.031502 -0.044112 -0.035380 14.148879 0.012529 0.000069 -0.008075 0.503830 -0.000000 4.097290 0.000000 0.000043 0.000081 0.257909 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 23 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.080849 5.918362 9.806893 0.077486 -0.149992 -0.834765 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.001540 0.004959 -0.108666 0.000000 7.977538 -0.000000 0.002245 0.044080 0.036905 -0.000001 13.487594 -0.000000 0.007936 0.006204 -0.362275 0.000000 4.943967 0.000000 0.085978 0.087054 -0.347101 -1.606499 11.186954 -7.585489 0.976069 -0.017220 0.001723 0.000000 12.910863 -0.000000 0.260027 -0.008793 -0.166194 -0.031254 11.193578 0.010376 -0.054167 0.000150 -0.002078 0.054595 0.018457 -0.015135 0.169750 -0.062265 -0.339681 0.000000 3.968525 0.000000 0.040902 0.053585 0.353097 0.000000 5.348644 -0.000000 0.322060 0.040768 0.008402 0.000000 2.438549 0.000000 0.537216 0.098116 -0.092068 -0.655277 1.265097 -0.625148 -0.239719 -0.905627 -0.278201 1.045680 11.227109 7.891132 -0.954995 -0.213283 -0.082408 0.000000 13.379274 -0.000000 -0.198246 0.170973 -0.559258 0.028222 10.991364 0.020885 -0.011323 -0.112609 -0.241337 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.046449 3.741992 5.565778 0.026358 -0.019961 0.996477 0.000000 13.074334 0.000000 -0.028677 -0.083785 -0.013239 -0.035381 14.148866 0.000001 -0.000420 0.010339 0.506363 0.000000 4.097309 -0.000000 0.000071 -0.000536 0.246720 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.949136 0.062811 0.013873 0.995937 -0.000000 13.074365 0.000000 0.016619 -0.033116 -0.044333 -0.035380 14.148879 0.012529 0.000068 -0.008075 0.503816 -0.000000 4.097290 0.000000 0.000047 0.000087 0.258047 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 24 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.074874 5.905360 9.819918 0.079203 -0.150426 -0.834155 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.001217 0.003163 -0.107292 0.000000 7.977538 -0.000000 0.002408 0.044061 0.037164 -0.000001 13.487594 -0.000000 0.020016 0.033220 -0.361741 0.000000 4.943967 0.000000 0.089527 0.090646 -0.344612 -1.605852 11.190315 -7.585625 0.976252 -0.015610 0.001668 0.000000 12.910863 -0.000000 0.261995 -0.008636 -0.167354 -0.031254 11.193578 0.010376 -0.054800 0.000111 -0.002287 0.054595 0.018457 -0.015135 0.170461 -0.062274 -0.339235 0.000000 3.968525 0.000000 0.040788 0.053749 0.353331 0.000000 5.348644 -0.000000 0.323204 0.040903 0.009296 0.000000 2.438549 0.000000 0.536560 0.098502 -0.092508 -0.655277 1.265097 -0.625148 -0.243370 -0.902751 -0.284082 1.048649 11.242512 7.890509 -0.955030 -0.213290 -0.081523 0.000000 13.379274 -0.000000 -0.199311 0.169578 -0.559835 0.028222 10.991364 0.020885 -0.010719 -0.114505 -0.238732 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.037842 3.743230 5.578460 0.026370 -0.020245 0.996441 0.000000 13.074334 0.000000 -0.029000 -0.083750 -0.012285 -0.035381 14.148866 0.000001 -0.000429 0.010335 0.506443 0.000000 4.097309 -0.000000 0.000081 -0.000505 0.246411 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.938416 0.064355 0.013622 0.995851 -0.000000 13.074365 0.000000 0.016248 -0.034627 -0.044554 -0.035380 14.148879 0.012529 0.000067 -0.008075 0.503802 -0.000000 4.097290 0.000000 0.000050 0.000094 0.258186 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 25 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.069264 5.892774 9.831813 0.080848 -0.150837 -0.833568 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000914 0.001480 -0.105988 0.000000 7.977538 -0.000000 0.002560 0.044042 0.037416 -0.000001 13.487594 -0.000000 0.031712 0.059385 -0.360902 0.000000 4.943967 0.000000 0.092713 0.093872 -0.342367 -1.605185 11.193772 -7.585764 0.976429 -0.014084 0.001669 0.000000 12.910863 -0.000000 0.264216 -0.008490 -0.168533 -0.031254 11.193578 0.010376 -0.055685 0.000056 -0.002579 0.054595 0.018457 -0.015135 0.171456 -0.062286 -0.338612 0.000000 3.968525 0.000000 0.040629 0.053978 0.353658 0.000000 5.348644 -0.000000 0.324805 0.041093 0.010547 0.000000 2.438549 0.000000 0.535641 0.099042 -0.093123 -0.655277 1.265097 -0.625148 -0.247154 -0.899666 -0.290300 1.051616 11.257909 7.889886 -0.955031 -0.213484 -0.080575 0.000000 13.379274 -0.000000 -0.200365 0.168043 -0.560640 0.028222 10.991364 0.020885 -0.009982 -0.116461 -0.235957 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.029853 3.744341 5.590185 0.026381 -0.020507 0.996409 0.000000 13.074334 0.000000 -0.029299 -0.083718 -0.011421 -0.035381 14.148866 0.000001 -0.000438 0.010330 0.506522 0.000000 4.097309 -0.000000 0.000091 -0.000476 0.246122 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.928498 0.065783 0.013391 0.995769 -0.000000 13.074365 0.000000 0.015905 -0.036025 -0.044774 -0.035380 14.148879 0.012529 0.000066 -0.008076 0.503787 -0.000000 4.097290 0.000000 0.000053 0.000100 0.258327 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 26 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.064236 5.880794 9.842532 0.082410 -0.151227 -0.833011 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000635 -0.000076 -0.104762 0.000000 7.977538 -0.000000 0.002701 0.044026 0.037661 -0.000001 13.487594 -0.000000 0.042386 0.083267 -0.359864 0.000000 4.943967 0.000000 0.095482 0.096675 -0.340406 -1.604465 11.197511 -7.585916 0.976595 -0.012683 0.001779 0.000000 12.910863 -0.000000 0.266632 -0.008314 -0.169887 -0.031254 11.193578 0.010376 -0.056853 -0.000016 -0.002965 0.054595 0.018457 -0.015135 0.172768 -0.062302 -0.337788 0.000000 3.968525 0.000000 0.040419 0.054280 0.354089 0.000000 5.348644 -0.000000 0.326919 0.041344 0.012201 0.000000 2.438549 0.000000 0.534427 0.099754 -0.093935 -0.655277 1.265097 -0.625148 -0.251071 -0.896425 -0.296666 1.054578 11.273278 7.889264 -0.955001 -0.213844 -0.079581 0.000000 13.379274 -0.000000 -0.201384 0.166386 -0.561638 0.028222 10.991364 0.020885 -0.009112 -0.118407 -0.233054 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.022547 3.745309 5.600856 0.026391 -0.020746 0.996380 0.000000 13.074334 0.000000 -0.029570 -0.083688 -0.010655 -0.035381 14.148866 0.000001 -0.000448 0.010326 0.506602 0.000000 4.097309 -0.000000 0.000099 -0.000449 0.245857 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.919465 0.067083 0.013179 0.995693 -0.000000 13.074365 0.000000 0.015591 -0.037298 -0.044994 -0.035380 14.148879 0.012529 0.000065 -0.008076 0.503773 -0.000000 4.097290 0.000000 0.000056 0.000106 0.258470 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 27 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.060006 5.869606 9.852020 0.083882 -0.151597 -0.832490 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000381 -0.001493 -0.103625 0.000000 7.977538 -0.000000 0.002829 0.044010 0.037898 -0.000001 13.487594 -0.000000 0.051327 0.103280 -0.358803 0.000000 4.943967 0.000000 0.097775 0.098997 -0.338777 -1.603655 11.201711 -7.586085 0.976748 -0.011446 0.002044 0.000000 12.910863 -0.000000 0.269191 -0.008067 -0.171567 -0.031254 11.193578 0.010376 -0.058338 -0.000108 -0.003455 0.054595 0.018457 -0.015135 0.174436 -0.062321 -0.336740 0.000000 3.968525 0.000000 0.040153 0.054663 0.354637 0.000000 5.348644 -0.000000 0.329607 0.041663 0.014306 0.000000 2.438549 0.000000 0.532882 0.100660 -0.094966 -0.655277 1.265097 -0.625148 -0.255124 -0.893083 -0.303001 1.057531 11.288600 7.888645 -0.954943 -0.214352 -0.078557 0.000000 13.379274 -0.000000 -0.202346 0.164623 -0.562798 0.028222 10.991364 0.020885 -0.008110 -0.120278 -0.230062 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.015987 3.746122 5.610374 0.026399 -0.020960 0.996355 0.000000 13.074334 0.000000 -0.029811 -0.083661 -0.009997 -0.035381 14.148866 0.000001 -0.000457 0.010322 0.506680 0.000000 4.097309 -0.000000 0.000107 -0.000425 0.245618 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.911400 0.068243 0.012991 0.995625 -0.000000 13.074365 0.000000 0.015309 -0.038435 -0.045212 -0.035380 14.148879 0.012529 0.000064 -0.008076 0.503759 -0.000000 4.097290 0.000000 0.000060 0.000113 0.258613 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 28 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.056787 5.859398 9.860214 0.085253 -0.151947 -0.832009 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000157 -0.002755 -0.102586 0.000000 7.977538 -0.000000 0.002944 0.043996 0.038127 -0.000001 13.487594 -0.000000 0.057682 0.117509 -0.357955 0.000000 4.943967 0.000000 0.099526 0.100769 -0.337530 -1.602723 11.206551 -7.586281 0.976884 -0.010412 0.002515 0.000000 12.910863 -0.000000 0.271840 -0.007708 -0.173724 -0.031254 11.193578 0.010376 -0.060176 -0.000222 -0.004062 0.054595 0.018457 -0.015135 0.176501 -0.062345 -0.335440 0.000000 3.968525 0.000000 0.039822 0.055138 0.355315 0.000000 5.348644 -0.000000 0.332937 0.042057 0.016919 0.000000 2.438549 0.000000 0.530963 0.101781 -0.096244 -0.655277 1.265097 -0.625148 -0.259319 -0.889697 -0.309134 1.060471 11.303855 7.888028 -0.954859 -0.214992 -0.077518 0.000000 13.379274 -0.000000 -0.203226 0.162768 -0.564091 0.028222 10.991364 0.020885 -0.006975 -0.122009 -0.227019 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.010239 3.746767 5.618636 0.026407 -0.021146 0.996334 0.000000 13.074334 0.000000 -0.030019 -0.083637 -0.009456 -0.035381 14.148866 0.000001 -0.000466 0.010317 0.506759 0.000000 4.097309 -0.000000 0.000114 -0.000404 0.245407 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.904387 0.069252 0.012826 0.995565 -0.000000 13.074365 0.000000 0.015063 -0.039423 -0.045429 -0.035380 14.148879 0.012529 0.000063 -0.008076 0.503745 -0.000000 4.097290 0.000000 0.000063 0.000119 0.258758 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 29 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.054796 5.850361 9.867043 0.086515 -0.152278 -0.831575 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000036 -0.003848 -0.101654 0.000000 7.977538 -0.000000 0.003043 0.043983 0.038347 -0.000001 13.487594 -0.000000 0.061885 0.126927 -0.357365 0.000000 4.943967 0.000000 0.100901 0.102162 -0.336547 -1.601632 11.212210 -7.586510 0.976998 -0.009620 0.003240 0.000000 12.910863 -0.000000 0.274531 -0.007197 -0.176509 -0.031254 11.193578 0.010376 -0.062411 -0.000360 -0.004800 0.054595 0.018457 -0.015135 0.179012 -0.062374 -0.333856 0.000000 3.968525 0.000000 0.039421 0.055716 0.356139 0.000000 5.348644 -0.000000 0.336989 0.042538 0.020105 0.000000 2.438549 0.000000 0.528622 0.103144 -0.097797 -0.655277 1.265097 -0.625148 -0.263665 -0.886327 -0.314898 1.063395 11.319024 7.887414 -0.954752 -0.215747 -0.076478 0.000000 13.379274 -0.000000 -0.204003 0.160835 -0.565490 0.028222 10.991364 0.020885 -0.005704 -0.123538 -0.223960 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.005369 3.747229 5.625542 0.026413 -0.021303 0.996319 0.000000 13.074334 0.000000 -0.030192 -0.083617 -0.009042 -0.035381 14.148866 0.000001 -0.000475 0.010313 0.506837 0.000000 4.097309 -0.000000 0.000120 -0.000386 0.245227 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.898512 0.070098 0.012688 0.995515 -0.000000 13.074365 0.000000 0.014854 -0.040251 -0.045646 -0.035380 14.148879 0.012529 0.000062 -0.008077 0.503731 -0.000000 4.097290 0.000000 0.000067 0.000125 0.258902 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 30 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.054262 5.842696 9.872425 0.087656 -0.152591 -0.831194 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000194 -0.004757 -0.100841 0.000000 7.977538 -0.000000 0.003127 0.043973 0.038558 -0.000001 13.487594 -0.000000 0.065240 0.134450 -0.356878 0.000000 4.943967 0.000000 0.102123 0.103398 -0.335673 -1.600348 11.218874 -7.586779 0.977087 -0.009113 0.004272 0.000000 12.910863 -0.000000 0.277214 -0.006492 -0.180078 -0.031254 11.193578 0.010376 -0.065091 -0.000527 -0.005686 0.054595 0.018457 -0.015135 0.182023 -0.062408 -0.331951 0.000000 3.968525 0.000000 0.038940 0.056407 0.357125 0.000000 5.348644 -0.000000 0.341853 0.043114 0.023939 0.000000 2.438549 0.000000 0.525806 0.104779 -0.099661 -0.655277 1.265097 -0.625148 -0.268173 -0.883036 -0.320124 1.066298 11.334088 7.886805 -0.954624 -0.216604 -0.075452 0.000000 13.379274 -0.000000 -0.204653 0.158838 -0.566969 0.028222 10.991364 0.020885 -0.004294 -0.124799 -0.220921 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.001445 3.747495 5.630986 0.026417 -0.021428 0.996309 0.000000 13.074334 0.000000 -0.030328 -0.083601 -0.008764 -0.035381 14.148866 0.000001 -0.000484 0.010309 0.506914 0.000000 4.097309 -0.000000 0.000124 -0.000371 0.245081 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.893863 0.070767 0.012578 0.995477 -0.000000 13.074365 0.000000 0.014687 -0.040906 -0.045861 -0.035380 14.148879 0.012529 0.000062 -0.008077 0.503718 -0.000000 4.097290 0.000000 0.000070 0.000132 0.259047 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 31 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.055422 5.836614 9.876265 0.088665 -0.152886 -0.830874 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000316 -0.005468 -0.100156 0.000000 7.977538 -0.000000 0.003193 0.043964 0.038757 -0.000001 13.487594 -0.000000 0.067933 0.140492 -0.356480 0.000000 4.943967 0.000000 0.103195 0.104484 -0.334904 -1.598832 11.226737 -7.587097 0.977146 -0.008935 0.005664 0.000000 12.910863 -0.000000 0.279838 -0.005549 -0.184594 -0.031254 11.193578 0.010376 -0.068273 -0.000724 -0.006737 0.054595 0.018457 -0.015135 0.185596 -0.062447 -0.329683 0.000000 3.968525 0.000000 0.038368 0.057228 0.358295 0.000000 5.348644 -0.000000 0.347632 0.043799 0.028508 0.000000 2.438549 0.000000 0.522448 0.106720 -0.101873 -0.655277 1.265097 -0.625148 -0.272858 -0.879892 -0.324644 1.069177 11.349028 7.886201 -0.954477 -0.217550 -0.074455 0.000000 13.379274 -0.000000 -0.205151 0.156789 -0.568503 0.028222 10.991364 0.020885 -0.002742 -0.125726 -0.217938 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -2.998536 3.747550 5.634861 0.026419 -0.021518 0.996304 0.000000 13.074334 0.000000 -0.030422 -0.083588 -0.008632 -0.035381 14.148866 0.000001 -0.000493 0.010304 0.506991 0.000000 4.097309 -0.000000 0.000128 -0.000361 0.244972 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.890531 0.071248 0.012498 0.995451 -0.000000 13.074365 0.000000 0.014564 -0.041377 -0.046075 -0.035380 14.148879 0.012529 0.000061 -0.008077 0.503704 -0.000000 4.097290 0.000000 0.000073 0.000138 0.259192 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 32 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.058535 5.832345 9.878455 0.089529 -0.153163 -0.830620 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000398 -0.005964 -0.099609 0.000000 7.977538 -0.000000 0.003241 0.043957 0.038946 -0.000001 13.487594 -0.000000 0.070094 0.145342 -0.356161 0.000000 4.943967 0.000000 0.104125 0.105426 -0.334237 -1.597046 11.236007 -7.587472 0.977168 -0.009135 0.007477 0.000000 12.910863 -0.000000 0.282351 -0.004322 -0.190233 -0.031254 11.193578 0.010376 -0.072022 -0.000956 -0.007976 0.054595 0.018457 -0.015135 0.189806 -0.062492 -0.327000 0.000000 3.968525 0.000000 0.037695 0.058195 0.359672 0.000000 5.348644 -0.000000 0.354447 0.044607 0.033915 0.000000 2.438549 0.000000 0.518471 0.109006 -0.104480 -0.655277 1.265097 -0.625148 -0.277739 -0.876966 -0.328277 1.072029 11.363825 7.885602 -0.954313 -0.218570 -0.073503 0.000000 13.379274 -0.000000 -0.205470 0.154702 -0.570066 0.028222 10.991364 0.020885 -0.001041 -0.126247 -0.215049 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -2.996712 3.747380 5.637057 0.026420 -0.021572 0.996306 0.000000 13.074334 0.000000 -0.030473 -0.083580 -0.008656 -0.035381 14.148866 0.000001 -0.000502 0.010300 0.507067 0.000000 4.097309 -0.000000 0.000130 -0.000353 0.244902 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.888606 0.071526 0.012451 0.995439 -0.000000 13.074365 0.000000 0.014488 -0.041649 -0.046289 -0.035380 14.148879 0.012529 0.000060 -0.008077 0.503691 -0.000000 4.097290 0.000000 0.000077 0.000144 0.259337 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 33 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.063882 5.830137 9.878872 0.090232 -0.153423 -0.830442 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000437 -0.006231 -0.099212 0.000000 7.977538 -0.000000 0.003269 0.043952 0.039122 -0.000001 13.487594 -0.000000 0.071814 0.149206 -0.355911 0.000000 4.943967 0.000000 0.104918 0.106228 -0.333668 -1.594945 11.246910 -7.587913 0.977145 -0.009766 0.009775 0.000000 12.910863 -0.000000 0.284693 -0.002759 -0.197184 -0.031254 11.193578 0.010376 -0.076417 -0.001229 -0.009429 0.054595 0.018457 -0.015135 0.194739 -0.062543 -0.323842 0.000000 3.968525 0.000000 0.036907 0.059327 0.361283 0.000000 5.348644 -0.000000 0.362441 0.045553 0.040283 0.000000 2.438549 0.000000 0.513783 0.111686 -0.107536 -0.655277 1.265097 -0.625148 -0.282836 -0.874334 -0.330832 1.074850 11.378461 7.885010 -0.954134 -0.219653 -0.072611 0.000000 13.379274 -0.000000 -0.205583 0.152588 -0.571632 0.028222 10.991364 0.020885 0.000815 -0.126287 -0.212293 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -2.996048 3.746968 5.637463 0.026419 -0.021586 0.996314 0.000000 13.074334 0.000000 -0.030478 -0.083577 -0.008848 -0.035381 14.148866 0.000001 -0.000511 0.010296 0.507143 0.000000 4.097309 -0.000000 0.000131 -0.000351 0.244875 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.888185 0.071589 0.012439 0.995441 -0.000000 13.074365 0.000000 0.014462 -0.041710 -0.046501 -0.035380 14.148879 0.012529 0.000059 -0.008078 0.503678 -0.000000 4.097290 0.000000 0.000080 0.000151 0.259480 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 34 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.071774 5.830271 9.877369 0.090759 -0.153664 -0.830348 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000431 -0.006252 -0.098975 0.000000 7.977538 -0.000000 0.003276 0.043949 0.039286 -0.000001 13.487594 -0.000000 0.073163 0.152239 -0.355723 0.000000 4.943967 0.000000 0.105577 0.106895 -0.333193 -1.592481 11.259694 -7.588430 0.977066 -0.010888 0.012633 0.000000 12.910863 -0.000000 0.286799 -0.000804 -0.205655 -0.031254 11.193578 0.010376 -0.081549 -0.001548 -0.011126 0.054595 0.018457 -0.015135 0.200499 -0.062599 -0.320136 0.000000 3.968525 0.000000 0.035986 0.060648 0.363161 0.000000 5.348644 -0.000000 0.371782 0.046659 0.047763 0.000000 2.438549 0.000000 0.508270 0.114814 -0.111105 -0.655277 1.265097 -0.625148 -0.288175 -0.872079 -0.332099 1.077636 11.392917 7.884425 -0.953944 -0.220785 -0.071797 0.000000 13.379274 -0.000000 -0.205458 0.150461 -0.573177 0.028222 10.991364 0.020885 0.002835 -0.125761 -0.209711 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -2.996618 3.746301 5.635960 0.026416 -0.021559 0.996329 0.000000 13.074334 0.000000 -0.030433 -0.083579 -0.009217 -0.035381 14.148866 0.000001 -0.000520 0.010292 0.507218 0.000000 4.097309 -0.000000 0.000130 -0.000352 0.244892 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.889363 0.071422 0.012464 0.995460 -0.000000 13.074365 0.000000 0.014489 -0.041547 -0.046712 -0.035380 14.148879 0.012529 0.000058 -0.008078 0.503665 -0.000000 4.097290 0.000000 0.000083 0.000157 0.259623 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 35 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.082563 5.833060 9.873776 0.091091 -0.153888 -0.830349 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000376 -0.006009 -0.098909 0.000000 7.977538 -0.000000 0.003261 0.043949 0.039436 -0.000001 13.487594 -0.000000 0.074194 0.154561 -0.355589 0.000000 4.943967 0.000000 0.106109 0.107434 -0.332810 -1.589600 11.274638 -7.589035 0.976918 -0.012571 0.016133 0.000000 12.910863 -0.000000 0.288593 0.001607 -0.215880 -0.031254 11.193578 0.010376 -0.087534 -0.001919 -0.013105 0.054595 0.018457 -0.015135 0.207212 -0.062661 -0.315789 0.000000 3.968525 0.000000 0.034913 0.062187 0.365348 0.000000 5.348644 -0.000000 0.382676 0.047949 0.056537 0.000000 2.438549 0.000000 0.501791 0.118461 -0.115266 -0.655277 1.265097 -0.625148 -0.293784 -0.870287 -0.331839 1.080383 11.407173 7.883849 -0.953743 -0.221954 -0.071080 0.000000 13.379274 -0.000000 -0.205060 0.148334 -0.574671 0.028222 10.991364 0.020885 0.005031 -0.124575 -0.207348 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -2.998500 3.745360 5.632430 0.026411 -0.021487 0.996352 0.000000 13.074334 0.000000 -0.030336 -0.083586 -0.009775 -0.035381 14.148866 0.000001 -0.000528 0.010288 0.507292 0.000000 4.097309 -0.000000 0.000128 -0.000359 0.244959 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.892243 0.071011 0.012528 0.995495 -0.000000 13.074365 0.000000 0.014573 -0.041144 -0.046922 -0.035380 14.148879 0.012529 0.000057 -0.008078 0.503652 -0.000000 4.097290 0.000000 0.000087 0.000163 0.259765 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 36 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.096652 5.838863 9.867895 0.091205 -0.154093 -0.830457 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000268 -0.005486 -0.099027 0.000000 7.977538 -0.000000 0.003222 0.043951 0.039572 -0.000001 13.487594 -0.000000 0.074950 0.156267 -0.355503 0.000000 4.943967 0.000000 0.106517 0.107847 -0.332516 -1.586243 11.292061 -7.589739 0.976680 -0.014895 0.020373 0.000000 12.910863 -0.000000 0.289987 0.004546 -0.228120 -0.031254 11.193578 0.010376 -0.094511 -0.002353 -0.015414 0.054595 0.018457 -0.015135 0.215035 -0.062729 -0.310687 0.000000 3.968525 0.000000 0.033662 0.063980 0.367891 0.000000 5.348644 -0.000000 0.395370 0.049450 0.066833 0.000000 2.438549 0.000000 0.494170 0.122710 -0.120117 -0.655277 1.265097 -0.625148 -0.299695 -0.869048 -0.329779 1.083088 11.421209 7.883281 -0.953535 -0.223145 -0.070481 0.000000 13.379274 -0.000000 -0.204348 0.146224 -0.576084 0.028222 10.991364 0.020885 0.007416 -0.122623 -0.205255 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.001775 3.744131 5.626747 0.026403 -0.021368 0.996382 0.000000 13.074334 0.000000 -0.030183 -0.083599 -0.010535 -0.035381 14.148866 0.000001 -0.000537 0.010284 0.507365 0.000000 4.097309 -0.000000 0.000124 -0.000371 0.245076 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.896928 0.070341 0.012633 0.995547 -0.000000 13.074365 0.000000 0.014718 -0.040488 -0.047130 -0.035380 14.148879 0.012529 0.000057 -0.008078 0.503640 -0.000000 4.097290 0.000000 0.000090 0.000170 0.259905 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 37 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.114507 5.848101 9.859486 0.091075 -0.154279 -0.830684 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 -0.000106 -0.004664 -0.099340 0.000000 7.977538 -0.000000 0.003158 0.043956 0.039693 -0.000001 13.487594 -0.000000 0.075465 0.157433 -0.355462 0.000000 4.943967 0.000000 0.106807 0.108141 -0.332307 -1.582336 11.312332 -7.590559 0.976326 -0.017955 0.025467 0.000000 12.910863 -0.000000 0.290871 0.008098 -0.242674 -0.031254 11.193578 0.010376 -0.102661 -0.002859 -0.018112 0.054595 0.018457 -0.015135 0.224164 -0.062801 -0.304684 0.000000 3.968525 0.000000 0.032201 0.066073 0.370855 0.000000 5.348644 -0.000000 0.410172 0.051200 0.078939 0.000000 2.438549 0.000000 0.485177 0.127667 -0.125780 -0.655277 1.265097 -0.625148 -0.305944 -0.868459 -0.325594 1.085747 11.435004 7.882723 -0.953323 -0.224344 -0.070025 0.000000 13.379274 -0.000000 -0.203278 0.144146 -0.577383 0.028222 10.991364 0.020885 0.010006 -0.119781 -0.203489 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.006526 3.742594 5.618783 0.026393 -0.021199 0.996421 0.000000 13.074334 0.000000 -0.029971 -0.083617 -0.011506 -0.035381 14.148866 0.000001 -0.000545 0.010280 0.507438 0.000000 4.097309 -0.000000 0.000119 -0.000388 0.245249 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.903527 0.069396 0.012783 0.995618 -0.000000 13.074365 0.000000 0.014927 -0.039562 -0.047337 -0.035380 14.148879 0.012529 0.000056 -0.008078 0.503627 -0.000000 4.097290 0.000000 0.000093 0.000176 0.260043 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 38 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.136681 5.861269 9.848263 0.090670 -0.154446 -0.831047 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000116 -0.003524 -0.099861 0.000000 7.977538 -0.000000 0.003067 0.043963 0.039797 -0.000001 13.487594 -0.000000 0.075767 0.158122 -0.355459 0.000000 4.943967 0.000000 0.106983 0.108318 -0.332180 -1.577797 11.335884 -7.591511 0.975819 -0.021865 0.031553 0.000000 12.910863 -0.000000 0.291112 0.012359 -0.259885 -0.031254 11.193578 0.010376 -0.112216 -0.003453 -0.021276 0.054595 0.018457 -0.015135 0.234855 -0.062877 -0.297584 0.000000 3.968525 0.000000 0.030488 0.068524 0.374320 0.000000 5.348644 -0.000000 0.427468 0.053244 0.093227 0.000000 2.438549 0.000000 0.474511 0.133472 -0.132414 -0.655277 1.265097 -0.625148 -0.312569 -0.868618 -0.318892 1.088355 11.448537 7.882176 -0.953109 -0.225534 -0.069740 0.000000 13.379274 -0.000000 -0.201793 0.142118 -0.578529 0.028222 10.991364 0.020885 0.012820 -0.115902 -0.202116 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.012840 3.740733 5.608402 0.026381 -0.020978 0.996468 0.000000 13.074334 0.000000 -0.029696 -0.083642 -0.012704 -0.035381 14.148866 0.000001 -0.000554 0.010276 0.507510 0.000000 4.097309 -0.000000 0.000111 -0.000411 0.245479 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.912153 0.068160 0.012980 0.995706 -0.000000 13.074365 0.000000 0.015203 -0.038351 -0.047543 -0.035380 14.148879 0.012529 0.000055 -0.008079 0.503615 -0.000000 4.097290 0.000000 0.000096 0.000182 0.260179 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 39 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.167990 5.878447 9.833464 0.089966 -0.154567 -0.831518 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000401 -0.002046 -0.100604 0.000000 7.977538 -0.000000 0.002949 0.043974 0.039884 -0.000001 13.487594 -0.000000 0.075878 0.158384 -0.355492 0.000000 4.943967 0.000000 0.107048 0.108384 -0.332133 -1.572204 11.364903 -7.592685 0.975393 -0.026517 0.039808 0.000000 12.910863 -0.000000 0.290119 0.018210 -0.283104 -0.031254 11.193578 0.010376 -0.125587 -0.004286 -0.025707 0.054595 0.018457 -0.015135 0.249790 -0.062965 -0.287539 0.000000 3.968525 0.000000 0.028089 0.071950 0.379155 0.000000 5.348644 -0.000000 0.451515 0.056082 0.113357 0.000000 2.438549 0.000000 0.459366 0.141576 -0.141683 -0.655277 1.265097 -0.625148 -0.321589 -0.869201 -0.308366 1.090909 11.461786 7.881640 -0.952919 -0.226669 -0.069689 0.000000 13.379274 -0.000000 -0.199425 0.140006 -0.579550 0.028222 10.991364 0.020885 0.015760 -0.110386 -0.201230 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.020808 3.738528 5.595465 0.026366 -0.020700 0.996524 0.000000 13.074334 0.000000 -0.029355 -0.083673 -0.014140 -0.035381 14.148866 0.000001 -0.000562 0.010272 0.507581 0.000000 4.097309 -0.000000 0.000102 -0.000441 0.245772 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.922923 0.066615 0.013226 0.995813 -0.000000 13.074365 0.000000 0.015551 -0.036838 -0.047747 -0.035380 14.148879 0.012529 0.000054 -0.008079 0.503603 -0.000000 4.097290 0.000000 0.000100 0.000188 0.260312 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 40 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.209943 5.899361 9.814881 0.088969 -0.154631 -0.832080 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.000752 -0.000209 -0.101582 0.000000 7.977538 -0.000000 0.002801 0.043988 0.039954 -0.000001 13.487594 -0.000000 0.075819 0.158265 -0.355556 0.000000 4.943967 0.000000 0.107006 0.108342 -0.332163 -1.565452 11.399938 -7.594102 0.974838 -0.032454 0.050509 0.000000 12.910863 -0.000000 0.287705 0.025776 -0.312722 -0.031254 11.193578 0.010376 -0.143369 -0.005393 -0.031604 0.054595 0.018457 -0.015135 0.269599 -0.063051 -0.273987 0.000000 3.968525 0.000000 0.024896 0.076505 0.385561 0.000000 5.348644 -0.000000 0.483078 0.059802 0.140281 0.000000 2.438549 0.000000 0.438838 0.152313 -0.153976 -0.655277 1.265097 -0.625148 -0.332700 -0.870429 -0.293106 1.093402 11.474725 7.881117 -0.952757 -0.227761 -0.069871 0.000000 13.379274 -0.000000 -0.196027 0.137730 -0.580503 0.028222 10.991364 0.020885 0.018786 -0.103102 -0.200799 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.030525 3.735960 5.579825 0.026348 -0.020363 0.996589 0.000000 13.074334 0.000000 -0.028943 -0.083711 -0.015828 -0.035381 14.148866 0.000001 -0.000570 0.010268 0.507651 0.000000 4.097309 -0.000000 0.000090 -0.000477 0.246130 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.935961 0.064744 0.013524 0.995938 -0.000000 13.074365 0.000000 0.015975 -0.035005 -0.047949 -0.035380 14.148879 0.012529 0.000054 -0.008079 0.503592 -0.000000 4.097290 0.000000 0.000103 0.000193 0.260443 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 41 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.258863 5.924092 9.792896 0.087681 -0.154655 -0.832759 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.001173 0.002010 -0.102809 0.000000 7.977538 -0.000000 0.002621 0.044005 0.040004 -0.000001 13.487594 -0.000000 0.075606 0.157800 -0.355648 0.000000 4.943967 0.000000 0.106862 0.108196 -0.332267 -1.557858 11.439340 -7.595695 0.973522 -0.040711 0.062793 0.000000 12.910863 -0.000000 0.284224 0.034273 -0.345624 -0.031254 11.193578 0.010376 -0.163741 -0.006664 -0.038368 0.054595 0.018457 -0.015135 0.292197 -0.063104 -0.258199 0.000000 3.968525 0.000000 0.021229 0.081723 0.392873 0.000000 5.348644 -0.000000 0.518405 0.063958 0.171157 0.000000 2.438549 0.000000 0.414801 0.164538 -0.167992 -0.655277 1.265097 -0.625148 -0.343097 -0.873136 -0.272919 1.095832 11.487331 7.880607 -0.952597 -0.228873 -0.070231 0.000000 13.379274 -0.000000 -0.191960 0.135376 -0.581390 0.028222 10.991364 0.020885 0.022002 -0.094505 -0.200725 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.042090 3.733008 5.561328 0.026326 -0.019963 0.996663 0.000000 13.074334 0.000000 -0.028457 -0.083756 -0.017784 -0.035381 14.148866 0.000001 -0.000578 0.010264 0.507720 0.000000 4.097309 -0.000000 0.000077 -0.000520 0.246557 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.951395 0.062527 0.013878 0.996080 -0.000000 13.074365 0.000000 0.016480 -0.032834 -0.048149 -0.035380 14.148879 0.012529 0.000053 -0.008079 0.503581 -0.000000 4.097290 0.000000 0.000106 0.000199 0.260570 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 42 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.311669 5.952790 9.767776 0.086097 -0.154651 -0.833578 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.001670 0.004632 -0.104302 0.000000 7.977538 -0.000000 0.002408 0.044026 0.040034 -0.000001 13.487594 -0.000000 0.075253 0.157021 -0.355765 0.000000 4.943967 0.000000 0.106619 0.107950 -0.332442 -1.549684 11.481751 -7.597410 0.970775 -0.052412 0.076016 0.000000 12.910863 -0.000000 0.280013 0.043073 -0.379359 -0.031254 11.193578 0.010376 -0.185294 -0.008010 -0.045531 0.054595 0.018457 -0.015135 0.315971 -0.063108 -0.241201 0.000000 3.968525 0.000000 0.017336 0.087251 0.400586 0.000000 5.348644 -0.000000 0.554485 0.068192 0.203607 0.000000 2.438549 0.000000 0.388795 0.177364 -0.182719 -0.655277 1.265097 -0.625148 -0.350219 -0.878190 -0.247200 1.098192 11.499576 7.880112 -0.952416 -0.230062 -0.070722 0.000000 13.379274 -0.000000 -0.187527 0.133019 -0.582210 0.028222 10.991364 0.020885 0.025505 -0.084964 -0.200928 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.055609 3.729651 5.539813 0.026302 -0.019497 0.996746 0.000000 13.074334 0.000000 -0.027892 -0.083808 -0.020022 -0.035381 14.148866 0.000001 -0.000586 0.010260 0.507788 0.000000 4.097309 -0.000000 0.000060 -0.000570 0.247058 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.969361 0.059944 0.014291 0.996237 -0.000000 13.074365 0.000000 0.017069 -0.030304 -0.048347 -0.035380 14.148879 0.012529 0.000052 -0.008079 0.503570 -0.000000 4.097290 0.000000 0.000108 0.000204 0.260694 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 43 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.365640 5.985668 9.739703 0.084208 -0.154632 -0.834565 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.002246 0.007684 -0.106075 0.000000 7.977538 -0.000000 0.002160 0.044050 0.040043 -0.000001 13.487594 -0.000000 0.074773 0.155957 -0.355905 0.000000 4.943967 0.000000 0.106280 0.107607 -0.332687 -1.541157 11.526000 -7.599200 0.965641 -0.069000 0.089585 0.000000 12.910863 -0.000000 0.275482 0.051585 -0.411689 -0.031254 11.193578 0.010376 -0.206723 -0.009350 -0.052662 0.054595 0.018457 -0.015135 0.339449 -0.063058 -0.224011 0.000000 3.968525 0.000000 0.013446 0.092761 0.408241 0.000000 5.348644 -0.000000 0.588706 0.072198 0.235375 0.000000 2.438549 0.000000 0.362386 0.189983 -0.197232 -0.655277 1.265097 -0.625148 -0.351130 -0.886609 -0.214787 1.100476 11.511431 7.879632 -0.952195 -0.231378 -0.071306 0.000000 13.379274 -0.000000 -0.182997 0.130729 -0.582965 0.028222 10.991364 0.020885 0.029387 -0.074801 -0.201344 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.071190 3.725865 5.515109 0.026274 -0.018962 0.996839 0.000000 13.074334 0.000000 -0.027244 -0.083868 -0.022559 -0.035381 14.148866 0.000001 -0.000594 0.010256 0.507855 0.000000 4.097309 -0.000000 0.000042 -0.000628 0.247637 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -4.990002 0.056974 0.014766 0.996409 -0.000000 13.074365 0.000000 0.017748 -0.027396 -0.048543 -0.035380 14.148879 0.012529 0.000052 -0.008079 0.503559 -0.000000 4.097290 0.000000 0.000111 0.000210 0.260814 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 44 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.418241 6.023010 9.708788 0.081998 -0.154608 -0.835743 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.002906 0.011190 -0.108146 0.000000 7.977538 -0.000000 0.001875 0.044079 0.040029 -0.000001 13.487594 -0.000000 0.074175 0.154631 -0.356065 0.000000 4.943967 0.000000 0.105850 0.107171 -0.332997 -1.532480 11.571020 -7.601020 0.956419 -0.092567 0.102781 0.000000 12.910863 -0.000000 0.271201 0.059157 -0.440225 -0.031254 11.193578 0.010376 -0.226573 -0.010593 -0.059275 0.054595 0.018457 -0.015135 0.361032 -0.062962 -0.207843 0.000000 3.968525 0.000000 0.009821 0.097884 0.415328 0.000000 5.348644 -0.000000 0.618667 0.075696 0.264122 0.000000 2.438549 0.000000 0.337464 0.201534 -0.210538 -0.655277 1.265097 -0.625148 -0.341676 -0.899617 -0.173613 1.102680 11.522865 7.879170 -0.951913 -0.232874 -0.071949 0.000000 13.379274 -0.000000 -0.178622 0.128572 -0.583661 0.028222 10.991364 0.020885 0.033742 -0.064308 -0.201918 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.088951 3.721626 5.487036 0.026243 -0.018352 0.996942 0.000000 13.074334 0.000000 -0.026508 -0.083935 -0.025411 -0.035381 14.148866 0.000001 -0.000602 0.010253 0.507921 0.000000 4.097309 -0.000000 0.000020 -0.000695 0.248298 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.013468 0.053595 0.015306 0.996592 -0.000000 13.074365 0.000000 0.018522 -0.024087 -0.048736 -0.035380 14.148879 0.012529 0.000051 -0.008080 0.503549 -0.000000 4.097290 0.000000 0.000114 0.000215 0.260929 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 45 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.466983 6.065179 9.675083 0.079447 -0.154587 -0.837142 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.003656 0.015180 -0.110532 0.000000 7.977538 -0.000000 0.001551 0.044111 0.039992 -0.000001 13.487594 -0.000000 0.073470 0.153064 -0.356243 0.000000 4.943967 0.000000 0.105330 0.106645 -0.333371 -1.523851 11.615792 -7.602831 0.939509 -0.126957 0.115658 0.000000 12.910863 -0.000000 0.267376 0.065698 -0.464721 -0.031254 11.193578 0.010376 -0.244881 -0.011741 -0.065382 0.054595 0.018457 -0.015135 0.380780 -0.062831 -0.192730 0.000000 3.968525 0.000000 0.006455 0.102630 0.421869 0.000000 5.348644 -0.000000 0.644633 0.078718 0.289874 0.000000 2.438549 0.000000 0.314113 0.212055 -0.222674 -0.655277 1.265097 -0.625148 -0.316564 -0.918298 -0.117462 1.104796 11.533845 7.878725 -0.951548 -0.234603 -0.072618 0.000000 13.379274 -0.000000 -0.174650 0.126617 -0.584303 0.028222 10.991364 0.020885 0.038671 -0.053763 -0.202602 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.109014 3.716908 5.455405 0.026207 -0.017664 0.997054 0.000000 13.074334 0.000000 -0.025679 -0.084011 -0.028597 -0.035381 14.148866 0.000001 -0.000609 0.010249 0.507986 0.000000 4.097309 -0.000000 -0.000004 -0.000770 0.249047 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.039920 0.049781 0.015915 0.996784 -0.000000 13.074365 0.000000 0.019396 -0.020354 -0.048926 -0.035380 14.148879 0.012529 0.000050 -0.008080 0.503539 -0.000000 4.097290 0.000000 0.000117 0.000220 0.261040 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 46 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.509303 6.112637 9.638585 0.076524 -0.154580 -0.838792 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.004501 0.019682 -0.113252 0.000000 7.977538 -0.000000 0.001185 0.044148 0.039929 -0.000001 13.487594 -0.000000 0.072667 0.151276 -0.356437 0.000000 4.943967 0.000000 0.104724 0.106032 -0.333807 -1.515468 11.659292 -7.604590 0.913537 -0.169412 0.128193 0.000000 12.910863 -0.000000 0.263733 0.071641 -0.486837 -0.031254 11.193578 0.010376 -0.262503 -0.012848 -0.071266 0.054595 0.018457 -0.015135 0.399628 -0.062666 -0.178012 0.000000 3.968525 0.000000 0.003192 0.107221 0.428172 0.000000 5.348644 -0.000000 0.667986 0.081427 0.313831 0.000000 2.438549 0.000000 0.291333 0.222045 -0.234215 -0.655277 1.265097 -0.625148 -0.278107 -0.937731 -0.048392 1.106817 11.544333 7.878301 -0.951078 -0.236624 -0.073281 0.000000 13.379274 -0.000000 -0.171339 0.124936 -0.584899 0.028222 10.991364 0.020885 0.044289 -0.043446 -0.203353 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.131509 3.711686 5.420014 0.026168 -0.016894 0.997176 0.000000 13.074334 0.000000 -0.024751 -0.084095 -0.032136 -0.035381 14.148866 0.000001 -0.000617 0.010246 0.508049 0.000000 4.097309 -0.000000 -0.000031 -0.000855 0.249887 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.069526 0.045509 0.016598 0.996981 -0.000000 13.074365 0.000000 0.020377 -0.016172 -0.049113 -0.035380 14.148879 0.012529 0.000050 -0.008080 0.503529 -0.000000 4.097290 0.000000 0.000119 0.000224 0.261145 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 47 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.542426 6.165965 9.599230 0.073194 -0.154597 -0.840731 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.005446 0.024727 -0.116326 0.000000 7.977538 -0.000000 0.000776 0.044190 0.039840 -0.000001 13.487594 -0.000000 0.071771 0.149282 -0.356644 0.000000 4.943967 0.000000 0.104036 0.105335 -0.334301 -1.507538 11.700439 -7.606254 0.879976 -0.214050 0.139448 0.000000 12.910863 -0.000000 0.260324 0.076962 -0.506517 -0.031254 11.193578 0.010376 -0.278945 -0.013881 -0.076761 0.054595 0.018457 -0.015135 0.417061 -0.062478 -0.164132 0.000000 3.968525 0.000000 0.000124 0.111530 0.434066 0.000000 5.348644 -0.000000 0.688244 0.083770 0.335350 0.000000 2.438549 0.000000 0.269830 0.231236 -0.244849 -0.655277 1.265097 -0.625148 -0.231468 -0.952489 0.024997 1.108736 11.554289 7.877899 -0.950474 -0.239002 -0.073906 0.000000 13.379274 -0.000000 -0.168966 0.123610 -0.585458 0.028222 10.991364 0.020885 0.050726 -0.033648 -0.204128 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.156577 3.705929 5.380647 0.026124 -0.016037 0.997307 0.000000 13.074334 0.000000 -0.023718 -0.084187 -0.036048 -0.035381 14.148866 0.000001 -0.000624 0.010242 0.508111 0.000000 4.097309 -0.000000 -0.000061 -0.000949 0.250825 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.102466 0.040750 0.017358 0.997177 -0.000000 13.074365 0.000000 0.021469 -0.011515 -0.049296 -0.035380 14.148879 0.012529 0.000049 -0.008080 0.503520 -0.000000 4.097290 0.000000 0.000121 0.000229 0.261245 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 48 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.563205 6.225907 9.556896 0.069407 -0.154648 -0.843001 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.006498 0.030348 -0.119774 0.000000 7.977538 -0.000000 0.000321 0.044236 0.039723 -0.000001 13.487594 -0.000000 0.070791 0.147098 -0.356864 0.000000 4.943967 0.000000 0.103268 0.104558 -0.334852 -1.500292 11.738032 -7.607774 0.842186 -0.255953 0.148774 0.000000 12.910863 -0.000000 0.257225 0.081607 -0.523597 -0.031254 11.193578 0.010376 -0.293714 -0.014811 -0.081702 0.054595 0.018457 -0.015135 0.432584 -0.062281 -0.151551 0.000000 3.968525 0.000000 -0.002654 0.115424 0.439375 0.000000 5.348644 -0.000000 0.705138 0.085717 0.353926 0.000000 2.438549 0.000000 0.250326 0.239377 -0.254280 -0.655277 1.265097 -0.625148 -0.182250 -0.960296 0.094252 1.110544 11.563669 7.877519 -0.949706 -0.241814 -0.074456 0.000000 13.379274 -0.000000 -0.167849 0.122732 -0.585989 0.028222 10.991364 0.020885 0.058142 -0.024693 -0.204886 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.184365 3.699607 5.337076 0.026076 -0.015087 0.997447 0.000000 13.074334 0.000000 -0.022575 -0.084288 -0.040355 -0.035381 14.148866 0.000001 -0.000631 0.010239 0.508171 0.000000 4.097309 -0.000000 -0.000095 -0.001054 0.251866 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.138933 0.035476 0.018200 0.997367 -0.000000 13.074365 0.000000 0.022680 -0.006354 -0.049475 -0.035380 14.148879 0.012529 0.000049 -0.008080 0.503511 -0.000000 4.097290 0.000000 0.000124 0.000233 0.261338 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 49 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.567913 6.293425 9.511378 0.065100 -0.154745 -0.845654 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.007663 0.036582 -0.123618 0.000000 7.977538 -0.000000 -0.000183 0.044288 0.039576 -0.000001 13.487594 -0.000000 0.069732 0.144738 -0.357094 0.000000 4.943967 0.000000 0.102424 0.103703 -0.335458 -1.494001 11.770674 -7.609095 0.805492 -0.290975 0.155874 0.000000 12.910863 -0.000000 0.254549 0.085478 -0.537761 -0.031254 11.193578 0.010376 -0.306211 -0.015598 -0.085886 0.054595 0.018457 -0.015135 0.445612 -0.062092 -0.140823 0.000000 3.968525 0.000000 -0.005022 0.118739 0.443880 0.000000 5.348644 -0.000000 0.718454 0.087247 0.369053 0.000000 2.438549 0.000000 0.233694 0.246174 -0.262164 -0.655277 1.265097 -0.625148 -0.136560 -0.962053 0.151183 1.112231 11.572424 7.877165 -0.948733 -0.245156 -0.074892 0.000000 13.379274 -0.000000 -0.168362 0.122415 -0.586500 0.028222 10.991364 0.020885 0.066734 -0.016952 -0.205581 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.215031 3.692687 5.289056 0.026022 -0.014039 0.997595 0.000000 13.074334 0.000000 -0.021314 -0.084398 -0.045080 -0.035381 14.148866 0.000001 -0.000638 0.010236 0.508230 0.000000 4.097309 -0.000000 -0.000132 -0.001170 0.253017 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.179133 0.029655 0.019129 0.997542 -0.000000 13.074365 0.000000 0.024017 -0.000660 -0.049649 -0.035380 14.148879 0.012529 0.000048 -0.008080 0.503503 -0.000000 4.097290 0.000000 0.000126 0.000237 0.261425 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 50 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.551920 6.369793 9.462373 0.060188 -0.154903 -0.848756 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008949 0.043464 -0.127882 0.000000 7.977538 -0.000000 -0.000740 0.044345 0.039397 -0.000001 13.487594 -0.000000 0.068600 0.142214 -0.357332 0.000000 4.943967 0.000000 0.101505 0.102773 -0.336115 -1.488994 11.796655 -7.610145 0.777072 -0.315298 0.160740 0.000000 12.910863 -0.000000 0.252457 0.088406 -0.548427 -0.031254 11.193578 0.010376 -0.315579 -0.016189 -0.089025 0.054595 0.018457 -0.015135 0.455310 -0.061938 -0.132735 0.000000 3.968525 0.000000 -0.006809 0.121236 0.447266 0.000000 5.348644 -0.000000 0.727842 0.088322 0.380022 0.000000 2.438549 0.000000 0.221155 0.251212 -0.268014 -0.655277 1.265097 -0.625148 -0.100948 -0.961181 0.187515 1.113787 11.580497 7.876839 -0.947503 -0.249147 -0.075162 0.000000 13.379274 -0.000000 -0.170970 0.122799 -0.586991 0.028222 10.991364 0.020885 0.076758 -0.010882 -0.206161 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.248746 3.685133 5.236325 0.025964 -0.012888 0.997751 0.000000 13.074334 0.000000 -0.019929 -0.084516 -0.050249 -0.035381 14.148866 0.000001 -0.000645 0.010232 0.508287 0.000000 4.097309 -0.000000 -0.000173 -0.001297 0.254284 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.223284 0.023255 0.020149 0.997694 -0.000000 13.074365 0.000000 0.025487 0.005601 -0.049817 -0.035380 14.148879 0.012529 0.000048 -0.008081 0.503495 -0.000000 4.097290 0.000000 0.000127 0.000240 0.261505 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 51 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.509196 6.460926 9.406602 0.054276 -0.155148 -0.852555 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.010605 0.052696 -0.133529 0.000000 7.977538 -0.000000 -0.001377 0.044411 0.039173 -0.000001 13.487594 -0.000000 0.067400 0.139536 -0.357578 0.000000 4.943967 0.000000 0.100515 0.101771 -0.336823 -1.484869 11.818057 -7.611011 0.757873 -0.330637 0.164122 0.000000 12.910863 -0.000000 0.250858 0.090584 -0.556334 -0.031254 11.193578 0.010376 -0.322292 -0.016612 -0.091275 0.054595 0.018457 -0.015135 0.462223 -0.061821 -0.126915 0.000000 3.968525 0.000000 -0.008096 0.123033 0.449698 0.000000 5.348644 -0.000000 0.734257 0.089055 0.387683 0.000000 2.438549 0.000000 0.212134 0.254791 -0.272173 -0.655277 1.265097 -0.625148 -0.080834 -0.962638 0.192379 1.115260 11.588137 7.876530 -0.945888 -0.254198 -0.075311 0.000000 13.379274 -0.000000 -0.175969 0.123848 -0.587653 0.028222 10.991364 0.020885 0.089181 -0.005934 -0.206478 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.292712 3.675438 5.167735 0.025888 -0.011389 0.997942 0.000000 13.074334 0.000000 -0.018126 -0.084667 -0.056910 -0.035381 14.148866 0.000001 -0.000651 0.010229 0.508342 0.000000 4.097309 -0.000000 -0.000223 -0.001452 0.255823 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.280737 0.014916 0.021478 0.997827 -0.000000 13.074365 0.000000 0.027403 0.013755 -0.049974 -0.035380 14.148879 0.012529 0.000047 -0.008081 0.503487 -0.000000 4.097290 0.000000 0.000129 0.000244 0.261579 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 52 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.442645 6.567477 9.343272 0.047313 -0.155466 -0.857018 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.012765 0.065110 -0.141039 0.000000 7.977538 -0.000000 -0.002117 0.044487 0.038892 -0.000001 13.487594 -0.000000 0.066136 0.136717 -0.357830 0.000000 4.943967 0.000000 0.099457 0.100699 -0.337579 -1.480957 11.838356 -7.611832 0.741976 -0.342706 0.166922 0.000000 12.910863 -0.000000 0.249449 0.092468 -0.563154 -0.031254 11.193578 0.010376 -0.327958 -0.016970 -0.093175 0.054595 0.018457 -0.015135 0.468034 -0.061718 -0.121987 0.000000 3.968525 0.000000 -0.009186 0.124555 0.451755 0.000000 5.348644 -0.000000 0.739471 0.089650 0.394017 0.000000 2.438549 0.000000 0.204498 0.257792 -0.275661 -0.655278 1.265097 -0.625148 -0.070526 -0.966476 0.177076 1.116703 11.595628 7.876227 -0.943880 -0.260316 -0.075408 0.000000 13.379274 -0.000000 -0.182960 0.125401 -0.588528 0.028222 10.991364 0.020885 0.104065 -0.001442 -0.206524 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.350648 3.662822 5.077531 0.025788 -0.009415 0.998175 0.000000 13.074334 0.000000 -0.015755 -0.084861 -0.065605 -0.035381 14.148866 0.000001 -0.000657 0.010226 0.508394 0.000000 4.097309 -0.000000 -0.000285 -0.001645 0.257735 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.356316 0.003932 0.023226 0.997892 -0.000000 13.074365 0.000000 0.029928 0.024493 -0.050113 -0.035380 14.148879 0.012529 0.000047 -0.008081 0.503480 -0.000000 4.097290 0.000000 0.000131 0.000247 0.261652 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 53 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.358734 6.684172 9.275178 0.039646 -0.155818 -0.861863 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.015288 0.079645 -0.149831 0.000000 7.977538 -0.000000 -0.002952 0.044574 0.038561 -0.000001 13.487594 -0.000000 0.064813 0.133764 -0.358086 0.000000 4.943967 0.000000 0.098333 0.099561 -0.338380 -1.477250 11.857592 -7.612610 0.728470 -0.352548 0.169328 0.000000 12.910863 -0.000000 0.248179 0.094139 -0.569190 -0.031254 11.193578 0.010376 -0.332881 -0.017281 -0.094827 0.054595 0.018457 -0.015135 0.473064 -0.061625 -0.117695 0.000000 3.968525 0.000000 -0.010137 0.125881 0.453545 0.000000 5.348644 -0.000000 0.743851 0.090149 0.399422 0.000000 2.438549 0.000000 0.197847 0.260383 -0.278676 -0.655278 1.265097 -0.625148 -0.063544 -0.970146 0.158452 1.118117 11.602965 7.875930 -0.941575 -0.267115 -0.075427 0.000000 13.379274 -0.000000 -0.191555 0.127446 -0.589415 0.028222 10.991364 0.020885 0.120522 0.002262 -0.206433 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.418363 3.648169 4.972205 0.025671 -0.007109 0.998424 0.000000 13.074334 0.000000 -0.012984 -0.085080 -0.075720 -0.035381 14.148866 0.000001 -0.000663 0.010224 0.508443 0.000000 4.097309 -0.000000 -0.000357 -0.001868 0.259947 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.444580 -0.008911 0.025266 0.997810 -0.000000 13.074365 0.000000 0.032876 0.037043 -0.050232 -0.035380 14.148879 0.012529 0.000046 -0.008081 0.503473 -0.000000 4.097290 0.000000 0.000133 0.000250 0.261724 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 54 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.262686 6.806654 9.204642 0.031563 -0.156174 -0.866866 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.018058 0.095420 -0.159423 0.000000 7.977538 -0.000000 -0.003872 0.044669 0.038182 -0.000001 13.487594 -0.000000 0.063435 0.130686 -0.358346 0.000000 4.943967 0.000000 0.097145 0.098359 -0.339225 -1.473740 11.875803 -7.613346 0.716808 -0.360760 0.171445 0.000000 12.910863 -0.000000 0.247017 0.095645 -0.574620 -0.031254 11.193578 0.010376 -0.337237 -0.017556 -0.096288 0.054595 0.018457 -0.015135 0.477499 -0.061540 -0.113888 0.000000 3.968525 0.000000 -0.010981 0.127057 0.455130 0.000000 5.348644 -0.000000 0.747610 0.090576 0.404127 0.000000 2.438549 0.000000 0.191952 0.262663 -0.281329 -0.655278 1.265097 -0.625148 -0.054397 -0.971765 0.152036 1.119501 11.610145 7.875639 -0.939054 -0.274275 -0.075346 0.000000 13.379274 -0.000000 -0.201448 0.129983 -0.590139 0.028222 10.991364 0.020885 0.137814 0.004871 -0.206323 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.492440 3.632199 4.857051 0.025542 -0.004587 0.998667 0.000000 13.074334 0.000000 -0.009951 -0.085308 -0.086752 -0.035381 14.148866 0.000001 -0.000668 0.010221 0.508489 0.000000 4.097309 -0.000000 -0.000436 -0.002116 0.262399 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.541088 -0.022966 0.027495 0.997527 -0.000000 13.074365 0.000000 0.036098 0.050772 -0.050331 -0.035380 14.148879 0.012529 0.000046 -0.008081 0.503466 -0.000000 4.097290 0.000000 0.000134 0.000253 0.261793 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 55 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.158970 6.931093 9.133719 0.023318 -0.156511 -0.871842 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.020973 0.111655 -0.169401 0.000000 7.977538 -0.000000 -0.004869 0.044773 0.037760 -0.000001 13.487594 -0.000000 0.062004 0.127493 -0.358609 0.000000 4.943967 0.000000 0.095897 0.097096 -0.340112 -1.470421 11.893026 -7.614042 0.706629 -0.367724 0.173339 0.000000 12.910863 -0.000000 0.245945 0.097018 -0.579556 -0.031254 11.193578 0.010376 -0.341138 -0.017802 -0.097598 0.054595 0.018457 -0.015135 0.481460 -0.061462 -0.110472 0.000000 3.968525 0.000000 -0.011739 0.128113 0.456553 0.000000 5.348644 -0.000000 0.750886 0.090948 0.408279 0.000000 2.438549 0.000000 0.186662 0.264695 -0.283696 -0.655278 1.265097 -0.625148 -0.040719 -0.970393 0.165585 1.120853 11.617162 7.875356 -0.936393 -0.281516 -0.075142 0.000000 13.379274 -0.000000 -0.212390 0.133026 -0.590540 0.028222 10.991364 0.020885 0.155290 0.006077 -0.206314 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.569913 3.615535 4.736658 0.025407 -0.001949 0.998889 0.000000 13.074334 0.000000 -0.006778 -0.085536 -0.098262 -0.035381 14.148866 0.000001 -0.000674 0.010219 0.508532 0.000000 4.097309 -0.000000 -0.000522 -0.002382 0.265035 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.641992 -0.037664 0.029821 0.997013 -0.000000 13.074365 0.000000 0.039461 0.065124 -0.050408 -0.035380 14.148879 0.012529 0.000045 -0.008081 0.503459 -0.000000 4.097290 0.000000 0.000136 0.000256 0.261861 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 56 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.051635 7.053909 9.064347 0.015153 -0.156808 -0.876637 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.023944 0.127629 -0.179390 0.000000 7.977538 -0.000000 -0.005937 0.044884 0.037299 -0.000001 13.487594 -0.000000 0.060525 0.124191 -0.358872 0.000000 4.943967 0.000000 0.094590 0.095773 -0.341038 -1.467286 11.909292 -7.614700 0.697679 -0.373696 0.175055 0.000000 12.910863 -0.000000 0.244949 0.098277 -0.584080 -0.031254 11.193578 0.010376 -0.344663 -0.018025 -0.098781 0.054595 0.018457 -0.015135 0.485029 -0.061389 -0.107381 0.000000 3.968525 0.000000 -0.012426 0.129069 0.457840 0.000000 5.348644 -0.000000 0.753772 0.091275 0.411980 0.000000 2.438549 0.000000 0.181876 0.266523 -0.285825 -0.655278 1.265097 -0.625148 -0.025158 -0.966783 0.189419 1.122174 11.624013 7.875078 -0.933673 -0.288577 -0.074793 0.000000 13.379274 -0.000000 -0.224173 0.136599 -0.590463 0.028222 10.991364 0.020885 0.172346 0.005558 -0.206530 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.648061 3.598751 4.615248 0.025270 0.000711 0.999081 0.000000 13.074334 0.000000 -0.003578 -0.085752 -0.109845 -0.035381 14.148866 0.000001 -0.000678 0.010216 0.508574 0.000000 4.097309 -0.000000 -0.000612 -0.002662 0.267807 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.743753 -0.052481 0.032160 0.996271 -0.000000 13.074365 0.000000 0.042846 0.079585 -0.050465 -0.035380 14.148879 0.012529 0.000045 -0.008081 0.503453 -0.000000 4.097290 0.000000 0.000137 0.000259 0.261927 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 57 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.944574 7.171548 8.998464 0.007305 -0.157049 -0.881113 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.026884 0.142638 -0.189037 0.000000 7.977538 -0.000000 -0.007069 0.045001 0.036803 -0.000001 13.487594 -0.000000 0.059001 0.120788 -0.359136 0.000000 4.943967 0.000000 0.093228 0.094393 -0.342003 -1.464330 11.924633 -7.615321 0.689769 -0.378860 0.176624 0.000000 12.910863 -0.000000 0.244021 0.099440 -0.588246 -0.031254 11.193578 0.010376 -0.347867 -0.018228 -0.099857 0.054595 0.018457 -0.015135 0.488265 -0.061322 -0.104566 0.000000 3.968525 0.000000 -0.013052 0.129940 0.459011 0.000000 5.348644 -0.000000 0.756334 0.091565 0.415303 0.000000 2.438549 0.000000 0.177519 0.268178 -0.287753 -0.655278 1.265097 -0.625148 -0.008473 -0.961237 0.218461 1.123461 11.630693 7.874808 -0.930977 -0.295197 -0.074272 0.000000 13.379274 -0.000000 -0.236609 0.140738 -0.589749 0.028222 10.991364 0.020885 0.188385 0.002954 -0.207107 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.724222 3.582411 4.496943 0.025136 0.003303 0.999237 0.000000 13.074334 0.000000 -0.000459 -0.085952 -0.121107 -0.035381 14.148866 0.000001 -0.000683 0.010214 0.508613 0.000000 4.097309 -0.000000 -0.000705 -0.002952 0.270671 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.842913 -0.066905 0.034432 0.995332 -0.000000 13.074365 0.000000 0.046134 0.093656 -0.050503 -0.035380 14.148879 0.012529 0.000045 -0.008081 0.503446 -0.000000 4.097290 0.000000 0.000139 0.000262 0.261991 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 58 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.841754 7.280267 8.938128 0.000024 -0.157219 -0.885147 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.029708 0.155959 -0.197990 0.000000 7.977538 -0.000000 -0.008256 0.045125 0.036274 -0.000001 13.487594 -0.000000 0.057435 0.117290 -0.359400 0.000000 4.943967 0.000000 0.091812 0.092960 -0.343003 -1.461546 11.939078 -7.615905 0.682758 -0.383350 0.178070 0.000000 12.910863 -0.000000 0.243152 0.100516 -0.592100 -0.031254 11.193578 0.010376 -0.350793 -0.018413 -0.100840 0.054595 0.018457 -0.015135 0.491213 -0.061260 -0.101993 0.000000 3.968525 0.000000 -0.013624 0.130736 0.460082 0.000000 5.348644 -0.000000 0.758624 0.091823 0.418303 0.000000 2.438549 0.000000 0.173537 0.269684 -0.289507 -0.655278 1.265097 -0.625148 0.008696 -0.954273 0.248651 1.124715 11.637198 7.874545 -0.928402 -0.301105 -0.073550 0.000000 13.379274 -0.000000 -0.249527 0.145495 -0.588230 0.028222 10.991364 0.020885 0.202787 -0.002153 -0.208199 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.795640 3.567095 4.386014 0.025009 0.005734 0.999355 0.000000 13.074334 0.000000 0.002464 -0.086128 -0.131645 -0.035381 14.148866 0.000001 -0.000687 0.010212 0.508650 0.000000 4.097309 -0.000000 -0.000799 -0.003246 0.273584 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.935893 -0.080407 0.036554 0.994258 -0.000000 13.074365 0.000000 0.049207 0.106823 -0.050527 -0.035380 14.148879 0.012529 0.000044 -0.008081 0.503440 -0.000000 4.097290 0.000000 0.000140 0.000265 0.262053 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 59 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.747474 7.375889 8.885652 -0.006414 -0.157305 -0.888614 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.032327 0.166807 -0.205879 0.000000 7.977538 -0.000000 -0.009495 0.045253 0.035717 -0.000001 13.487594 -0.000000 0.055830 0.113703 -0.359661 0.000000 4.943967 0.000000 0.090345 0.091475 -0.344037 -1.458929 11.952654 -7.616454 0.676532 -0.387270 0.179410 0.000000 12.910863 -0.000000 0.242338 0.101516 -0.595673 -0.031254 11.193578 0.010376 -0.353473 -0.018582 -0.101740 0.054595 0.018457 -0.015135 0.493907 -0.061202 -0.099633 0.000000 3.968525 0.000000 -0.014150 0.131467 0.461064 0.000000 5.348644 -0.000000 0.760679 0.092055 0.421020 0.000000 2.438549 0.000000 0.169886 0.271057 -0.291109 -0.655278 1.265097 -0.625148 0.025741 -0.946824 0.276296 1.125934 11.643523 7.874289 -0.926057 -0.305998 -0.072592 0.000000 13.379274 -0.000000 -0.262759 0.150942 -0.585715 0.028222 10.991364 0.020885 0.214862 -0.010270 -0.209983 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.859286 3.553446 4.287156 0.024896 0.007900 0.999437 0.000000 13.074334 0.000000 0.005067 -0.086276 -0.141016 -0.035381 14.148866 0.000001 -0.000691 0.010210 0.508684 0.000000 4.097309 -0.000000 -0.000894 -0.003542 0.276505 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.018755 -0.092415 0.038438 0.993145 -0.000000 13.074365 0.000000 0.051935 0.118530 -0.050540 -0.035380 14.148879 0.012529 0.000044 -0.008082 0.503434 -0.000000 4.097290 0.000000 0.000142 0.000267 0.262113 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 60 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.666676 7.453489 8.843780 -0.011687 -0.157292 -0.891377 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.034640 0.174275 -0.212286 0.000000 7.977538 -0.000000 -0.010777 0.045387 0.035135 -0.000001 13.487594 -0.000000 0.054187 0.110035 -0.359921 0.000000 4.943967 0.000000 0.088829 0.089940 -0.345103 -1.456475 11.965386 -7.616969 0.671001 -0.390701 0.180655 0.000000 12.910863 -0.000000 0.241574 0.102446 -0.598993 -0.031254 11.193578 0.010376 -0.355931 -0.018738 -0.102566 0.054595 0.018457 -0.015135 0.496374 -0.061148 -0.097465 0.000000 3.968525 0.000000 -0.014633 0.132139 0.461965 0.000000 5.348644 -0.000000 0.762529 0.092264 0.423490 0.000000 2.438549 0.000000 0.166533 0.272314 -0.292574 -0.655278 1.265097 -0.625148 0.042052 -0.940334 0.297600 1.127118 11.649666 7.874041 -0.924071 -0.309518 -0.071352 0.000000 13.379274 -0.000000 -0.276133 0.157175 -0.581978 0.028222 10.991364 0.020885 0.223786 -0.022043 -0.212671 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.911632 3.542209 4.205840 0.024802 0.009680 0.999489 0.000000 13.074334 0.000000 0.007206 -0.086392 -0.148714 -0.035381 14.148866 0.000001 -0.000695 0.010208 0.508717 0.000000 4.097309 -0.000000 -0.000989 -0.003835 0.279396 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.086912 -0.102272 0.039981 0.992120 -0.000000 13.074365 0.000000 0.054170 0.128136 -0.050550 -0.035380 14.148879 0.012529 0.000043 -0.008082 0.503428 -0.000000 4.097290 0.000000 0.000143 0.000270 0.262171 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 61 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.605399 7.506923 8.815950 -0.015395 -0.157161 -0.893267 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.036528 0.177229 -0.216691 0.000000 7.977538 -0.000000 -0.012097 0.045524 0.034530 -0.000001 13.487594 -0.000000 0.052511 0.106290 -0.360177 0.000000 4.943967 0.000000 0.087266 0.088358 -0.346199 -1.454179 11.977299 -7.617451 0.666093 -0.393704 0.181818 0.000000 12.910863 -0.000000 0.240857 0.103313 -0.602081 -0.031254 11.193578 0.010376 -0.358190 -0.018881 -0.103325 0.054595 0.018457 -0.015135 0.498637 -0.061097 -0.095471 0.000000 3.968525 0.000000 -0.015078 0.132757 0.462795 0.000000 5.348644 -0.000000 0.764200 0.092452 0.425738 0.000000 2.438549 0.000000 0.163450 0.273464 -0.293916 -0.655278 1.265097 -0.625148 0.056946 -0.936836 0.308002 1.128265 11.655620 7.873800 -0.922604 -0.311219 -0.069771 0.000000 13.379274 -0.000000 -0.289461 0.164329 -0.576730 0.028222 10.991364 0.020885 0.228507 -0.038331 -0.216523 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.948317 3.534308 4.148822 0.024736 0.010928 0.999517 0.000000 13.074334 0.000000 0.008705 -0.086469 -0.154113 -0.035381 14.148866 0.000001 -0.000699 0.010207 0.508747 0.000000 4.097309 -0.000000 -0.001081 -0.004121 0.282217 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.134699 -0.109169 0.041060 0.991343 -0.000000 13.074365 0.000000 0.055733 0.134856 -0.050563 -0.035380 14.148879 0.012529 0.000043 -0.008082 0.503422 -0.000000 4.097290 0.000000 0.000144 0.000272 0.262228 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 62 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.569181 7.527489 8.807201 -0.017673 -0.156893 -0.894423 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.038107 0.176561 -0.219558 0.000000 7.977538 -0.000000 -0.013450 0.045664 0.033905 -0.000001 13.487594 -0.000000 0.050803 0.102474 -0.360429 0.000000 4.943967 0.000000 0.085660 0.086732 -0.347324 -1.452037 11.988414 -7.617900 0.661746 -0.396332 0.182906 0.000000 12.910863 -0.000000 0.240184 0.104120 -0.604955 -0.031254 11.193578 0.010376 -0.360265 -0.019012 -0.104022 0.054595 0.018457 -0.015135 0.500712 -0.061050 -0.093638 0.000000 3.968525 0.000000 -0.015487 0.133326 0.463557 0.000000 5.348644 -0.000000 0.765710 0.092622 0.427786 0.000000 2.438549 0.000000 0.160615 0.274519 -0.295146 -0.655278 1.265097 -0.625148 0.069758 -0.939434 0.299672 1.129376 11.661382 7.873567 -0.921792 -0.310416 -0.067691 0.000000 13.379274 -0.000000 -0.303628 0.173458 -0.568864 0.028222 10.991364 0.020885 0.227012 -0.062692 -0.222504 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.973441 3.528864 4.109737 0.024691 0.011784 0.999532 0.000000 13.074334 0.000000 0.009732 -0.086520 -0.157822 -0.035381 14.148866 0.000001 -0.000702 0.010205 0.508776 0.000000 4.097309 -0.000000 -0.001169 -0.004396 0.284926 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.167452 -0.113889 0.041797 0.990783 -0.000000 13.074365 0.000000 0.056800 0.139455 -0.050584 -0.035380 14.148879 0.012529 0.000043 -0.008082 0.503417 -0.000000 4.097290 0.000000 0.000146 0.000275 0.262282 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 63 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.553429 7.518814 8.815646 -0.019068 -0.156505 -0.895166 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.039596 0.174193 -0.221808 0.000000 7.977538 -0.000000 -0.014829 0.045807 0.033264 -0.000001 13.487594 -0.000000 0.049066 0.098593 -0.360677 0.000000 4.943967 0.000000 0.084011 0.085063 -0.348475 -1.450045 11.998754 -7.618318 0.657910 -0.398627 0.183924 0.000000 12.910863 -0.000000 0.239552 0.104872 -0.607629 -0.031254 11.193578 0.010376 -0.362171 -0.019133 -0.104663 0.054595 0.018457 -0.015135 0.502614 -0.061007 -0.091953 0.000000 3.968525 0.000000 -0.015863 0.133849 0.464258 0.000000 5.348644 -0.000000 0.767076 0.092776 0.429652 0.000000 2.438549 0.000000 0.158010 0.275485 -0.296273 -0.655277 1.265097 -0.625148 0.080900 -0.946269 0.277198 1.130449 11.666948 7.873342 -0.921526 -0.307357 -0.065121 0.000000 13.379274 -0.000000 -0.318808 0.184495 -0.558432 0.028222 10.991364 0.020885 0.219730 -0.094899 -0.230350 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.994768 3.524222 4.076533 0.024652 0.012510 0.999542 0.000000 13.074334 0.000000 0.010605 -0.086562 -0.160978 -0.035381 14.148866 0.000001 -0.000705 0.010204 0.508802 0.000000 4.097309 -0.000000 -0.001252 -0.004656 0.287481 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.195273 -0.117894 0.042422 0.990290 -0.000000 13.074365 0.000000 0.057705 0.143356 -0.050608 -0.035380 14.148879 0.012529 0.000042 -0.008082 0.503412 -0.000000 4.097290 0.000000 0.000147 0.000277 0.262334 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 64 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.552258 7.489040 8.836735 -0.019697 -0.156024 -0.895551 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.041000 0.170258 -0.223486 0.000000 7.977538 -0.000000 -0.016231 0.045953 0.032609 -0.000001 13.487594 -0.000000 0.047302 0.094650 -0.360919 0.000000 4.943967 0.000000 0.082323 0.083354 -0.349651 -1.448197 12.008337 -7.618706 0.654540 -0.400622 0.184880 0.000000 12.910863 -0.000000 0.238961 0.105571 -0.610115 -0.031254 11.193578 0.010376 -0.363919 -0.019243 -0.105250 0.054595 0.018457 -0.015135 0.504356 -0.060967 -0.090406 0.000000 3.968525 0.000000 -0.016209 0.134329 0.464901 0.000000 5.348644 -0.000000 0.768311 0.092915 0.431351 0.000000 2.438549 0.000000 0.155620 0.276368 -0.297305 -0.655277 1.265097 -0.625148 0.090665 -0.954065 0.248825 1.131482 11.672313 7.873125 -0.921674 -0.302677 -0.062189 0.000000 13.379274 -0.000000 -0.333973 0.196243 -0.546559 0.028222 10.991364 0.020885 0.208477 -0.131242 -0.238821 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.012522 3.520335 4.048869 0.024620 0.013114 0.999549 0.000000 13.074334 0.000000 0.011331 -0.086596 -0.163615 -0.035381 14.148866 0.000001 -0.000708 0.010202 0.508826 0.000000 4.097309 -0.000000 -0.001329 -0.004895 0.289837 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.218450 -0.121227 0.042942 0.989867 -0.000000 13.074365 0.000000 0.058457 0.146602 -0.050637 -0.035380 14.148879 0.012529 0.000042 -0.008082 0.503407 -0.000000 4.097290 0.000000 0.000148 0.000279 0.262384 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 65 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.561410 7.443999 8.867258 -0.019662 -0.155472 -0.895621 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.042323 0.164879 -0.224630 0.000000 7.977538 -0.000000 -0.017648 0.046100 0.031943 -0.000001 13.487594 -0.000000 0.045513 0.090652 -0.361155 0.000000 4.943967 0.000000 0.080597 0.081607 -0.350850 -1.446493 12.017183 -7.619063 0.651602 -0.402347 0.185776 0.000000 12.910863 -0.000000 0.238408 0.106222 -0.612423 -0.031254 11.193578 0.010376 -0.365519 -0.019344 -0.105788 0.054595 0.018457 -0.015135 0.505949 -0.060929 -0.088990 0.000000 3.968525 0.000000 -0.016526 0.134769 0.465490 0.000000 5.348644 -0.000000 0.769428 0.093040 0.432896 0.000000 2.438549 0.000000 0.153430 0.277175 -0.298247 -0.655277 1.265097 -0.625148 0.099163 -0.961989 0.215894 1.132477 11.677472 7.872916 -0.922157 -0.296832 -0.058984 0.000000 13.379274 -0.000000 -0.348277 0.207733 -0.534223 0.028222 10.991364 0.020885 0.194749 -0.168612 -0.246934 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.026908 3.517160 4.026424 0.024593 0.013605 0.999553 0.000000 13.074334 0.000000 0.011921 -0.086623 -0.165763 -0.035381 14.148866 0.000001 -0.000710 0.010201 0.508848 0.000000 4.097309 -0.000000 -0.001398 -0.005110 0.291943 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.237250 -0.123928 0.043363 0.989516 -0.000000 13.074365 0.000000 0.059066 0.149233 -0.050669 -0.035380 14.148879 0.012529 0.000042 -0.008082 0.503402 -0.000000 4.097290 0.000000 0.000149 0.000281 0.262432 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 66 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.577430 7.388359 8.904709 -0.019052 -0.154867 -0.895409 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.043569 0.158165 -0.225275 0.000000 7.977538 -0.000000 -0.019077 0.046248 0.031268 -0.000001 13.487594 -0.000000 0.043702 0.086603 -0.361385 0.000000 4.943967 0.000000 0.078836 0.079824 -0.352071 -1.444927 12.025309 -7.619392 0.649061 -0.403826 0.186616 0.000000 12.910863 -0.000000 0.237892 0.106825 -0.614563 -0.031254 11.193578 0.010376 -0.366979 -0.019437 -0.106279 0.054595 0.018457 -0.015135 0.507401 -0.060895 -0.087696 0.000000 3.968525 0.000000 -0.016815 0.135171 0.466028 0.000000 5.348644 -0.000000 0.770434 0.093153 0.434297 0.000000 2.438549 0.000000 0.151431 0.277910 -0.299105 -0.655277 1.265096 -0.625148 0.106586 -0.969326 0.179877 1.133430 11.682420 7.872716 -0.922933 -0.290198 -0.055578 0.000000 13.379274 -0.000000 -0.360927 0.218066 -0.522409 0.028222 10.991364 0.020885 0.179993 -0.204102 -0.253852 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.038120 3.514657 4.008899 0.024573 0.013987 0.999556 0.000000 13.074334 0.000000 0.012382 -0.086644 -0.167451 -0.035381 14.148866 0.000001 -0.000713 0.010200 0.508868 0.000000 4.097309 -0.000000 -0.001457 -0.005293 0.293744 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.251925 -0.126035 0.043691 0.989237 -0.000000 13.074365 0.000000 0.059540 0.151285 -0.050705 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503397 -0.000000 4.097290 0.000000 0.000150 0.000283 0.262479 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 67 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.597129 7.326347 8.946893 -0.017945 -0.154227 -0.894944 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.044742 0.150215 -0.225449 0.000000 7.977538 -0.000000 -0.020512 0.046397 0.030587 -0.000001 13.487594 -0.000000 0.041869 0.082508 -0.361608 0.000000 4.943967 0.000000 0.077042 0.078007 -0.353311 -1.443496 12.032731 -7.619692 0.646892 -0.405078 0.187403 0.000000 12.910863 -0.000000 0.237412 0.107384 -0.616541 -0.031254 11.193578 0.010376 -0.368306 -0.019521 -0.106725 0.054595 0.018457 -0.015135 0.508719 -0.060864 -0.086519 0.000000 3.968525 0.000000 -0.017079 0.135536 0.466518 0.000000 5.348644 -0.000000 0.771340 0.093254 0.435564 0.000000 2.438549 0.000000 0.149613 0.278577 -0.299884 -0.655276 1.265096 -0.625148 0.112938 -0.974772 0.147134 1.134343 11.687154 7.872525 -0.924001 -0.283129 -0.052038 0.000000 13.379274 -0.000000 -0.371066 0.226283 -0.512233 0.028222 10.991364 0.020885 0.165769 -0.234713 -0.258795 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.046339 3.512789 3.996013 0.024557 0.014268 0.999557 0.000000 13.074334 0.000000 0.012720 -0.086658 -0.168704 -0.035381 14.148866 0.000001 -0.000715 0.010199 0.508885 0.000000 4.097309 -0.000000 -0.001504 -0.005439 0.295175 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.262710 -0.127582 0.043932 0.989030 -0.000000 13.074365 0.000000 0.059888 0.152792 -0.050744 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503393 -0.000000 4.097290 0.000000 0.000151 0.000285 0.262522 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 68 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.617072 7.262413 8.991596 -0.016414 -0.153568 -0.894249 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.045841 0.141122 -0.225177 0.000000 7.977538 -0.000000 -0.021949 0.046545 0.029903 -0.000001 13.487594 -0.000000 0.040018 0.078370 -0.361824 0.000000 4.943967 0.000000 0.075217 0.076160 -0.354570 -1.442198 12.039467 -7.619965 0.645069 -0.406123 0.188139 0.000000 12.910863 -0.000000 0.236967 0.107899 -0.618364 -0.031254 11.193578 0.010376 -0.369507 -0.019597 -0.107129 0.054595 0.018457 -0.015135 0.509910 -0.060835 -0.085454 0.000000 3.968525 0.000000 -0.017317 0.135868 0.466961 0.000000 5.348644 -0.000000 0.772150 0.093345 0.436704 0.000000 2.438549 0.000000 0.147968 0.279179 -0.300587 -0.655276 1.265095 -0.625147 0.118121 -0.977743 0.124889 1.135213 11.691668 7.872342 -0.925398 -0.276007 -0.048429 0.000000 13.379274 -0.000000 -0.377606 0.231186 -0.505108 0.028222 10.991364 0.020885 0.153940 -0.256939 -0.260900 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.051737 3.511519 3.987503 0.024547 0.014453 0.999558 0.000000 13.074334 0.000000 0.012945 -0.086667 -0.169549 -0.035381 14.148866 0.000001 -0.000717 0.010198 0.508900 0.000000 4.097309 -0.000000 -0.001536 -0.005539 0.296161 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.269827 -0.128602 0.044090 0.988893 -0.000000 13.074365 0.000000 0.060116 0.153785 -0.050787 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503389 -0.000000 4.097290 0.000000 0.000152 0.000287 0.262564 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 69 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.637346 7.195519 9.040102 -0.014523 -0.152905 -0.893343 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.046870 0.130975 -0.224482 0.000000 7.977538 -0.000000 -0.023382 0.046694 0.029218 -0.000001 13.487594 -0.000000 0.038151 0.074194 -0.362031 0.000000 4.943967 0.000000 0.073363 0.074283 -0.355844 -1.441030 12.045529 -7.620210 0.643573 -0.406974 0.188825 0.000000 12.910863 -0.000000 0.236557 0.108372 -0.620036 -0.031254 11.193578 0.010376 -0.370586 -0.019665 -0.107492 0.054595 0.018457 -0.015135 0.510980 -0.060809 -0.084496 0.000000 3.968525 0.000000 -0.017532 0.136165 0.467359 0.000000 5.348644 -0.000000 0.772872 0.093426 0.437724 0.000000 2.438549 0.000000 0.146489 0.279720 -0.301218 -0.655276 1.265095 -0.625147 0.122307 -0.978614 0.116045 1.136040 11.695958 7.872169 -0.927266 -0.268678 -0.044528 0.000000 13.379274 -0.000000 -0.379659 0.231727 -0.501796 0.028222 10.991364 0.020885 0.145139 -0.268476 -0.259897 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.054474 3.510815 3.983118 0.024542 0.014548 0.999559 0.000000 13.074334 0.000000 0.013061 -0.086672 -0.170009 -0.035381 14.148866 0.000001 -0.000718 0.010198 0.508913 0.000000 4.097309 -0.000000 -0.001557 -0.005604 0.296798 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.273485 -0.129126 0.044172 0.988822 -0.000000 13.074365 0.000000 0.060232 0.154296 -0.050831 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503385 -0.000000 4.097290 0.000000 0.000153 0.000289 0.262604 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 70 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.660430 7.122568 9.093852 -0.012332 -0.152252 -0.892245 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.047828 0.119858 -0.223386 0.000000 7.977538 -0.000000 -0.024807 0.046841 0.028534 -0.000001 13.487594 -0.000000 0.036268 0.069986 -0.362230 0.000000 4.943967 0.000000 0.071482 0.072379 -0.357134 -1.439988 12.050933 -7.620428 0.642384 -0.407645 0.189464 0.000000 12.910863 -0.000000 0.236181 0.108804 -0.621564 -0.031254 11.193578 0.010376 -0.371548 -0.019726 -0.107816 0.054595 0.018457 -0.015135 0.511933 -0.060786 -0.083641 0.000000 3.968525 0.000000 -0.017724 0.136431 0.467715 0.000000 5.348644 -0.000000 0.773511 0.093497 0.438629 0.000000 2.438549 0.000000 0.145169 0.280201 -0.301781 -0.655276 1.265095 -0.625147 0.125873 -0.978486 0.114870 1.136822 11.700018 7.872004 -0.929551 -0.260873 -0.040303 0.000000 13.379274 -0.000000 -0.378318 0.229046 -0.501187 0.028222 10.991364 0.020885 0.138012 -0.272521 -0.256733 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.054704 3.510643 3.982624 0.024541 0.014558 0.999559 0.000000 13.074334 0.000000 0.013075 -0.086671 -0.170105 -0.035381 14.148866 0.000001 -0.000720 0.010197 0.508926 0.000000 4.097309 -0.000000 -0.001572 -0.005649 0.297245 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.273882 -0.129183 0.044180 0.988816 -0.000000 13.074365 0.000000 0.060242 0.154351 -0.050878 -0.035380 14.148879 0.012529 0.000040 -0.008082 0.503381 -0.000000 4.097290 0.000000 0.000154 0.000291 0.262641 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 71 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.686085 7.045030 9.151158 -0.009901 -0.151623 -0.890971 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.048717 0.107853 -0.221907 0.000000 7.977538 -0.000000 -0.026218 0.046987 0.027854 -0.000001 13.487594 -0.000000 0.034372 0.065747 -0.362420 0.000000 4.943967 0.000000 0.069577 0.070451 -0.358436 -1.439071 12.055691 -7.620621 0.641485 -0.408146 0.190055 0.000000 12.910863 -0.000000 0.235838 0.109196 -0.622950 -0.031254 11.193578 0.010376 -0.372398 -0.019780 -0.108102 0.054595 0.018457 -0.015135 0.512774 -0.060765 -0.082887 0.000000 3.968525 0.000000 -0.017893 0.136666 0.468028 0.000000 5.348644 -0.000000 0.774070 0.093560 0.439426 0.000000 2.438549 0.000000 0.144004 0.280625 -0.302276 -0.655276 1.265095 -0.625147 0.128867 -0.977684 0.119176 1.137559 11.703843 7.871850 -0.932090 -0.252818 -0.036054 0.000000 13.379274 -0.000000 -0.374720 0.224462 -0.502512 0.028222 10.991364 0.020885 0.132006 -0.272017 -0.251902 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.052571 3.510975 3.985795 0.024544 0.014487 0.999559 0.000000 13.074334 0.000000 0.012993 -0.086666 -0.169858 -0.035381 14.148866 0.000001 -0.000721 0.010196 0.508939 0.000000 4.097309 -0.000000 -0.001580 -0.005677 0.297517 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.271205 -0.128798 0.044120 0.988871 -0.000000 13.074365 0.000000 0.060153 0.153976 -0.050927 -0.035380 14.148879 0.012529 0.000040 -0.008082 0.503377 -0.000000 4.097290 0.000000 0.000155 0.000292 0.262676 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 72 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.714251 6.963917 9.210765 -0.007282 -0.151033 -0.889537 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.049537 0.095041 -0.220065 0.000000 7.977538 -0.000000 -0.027612 0.047131 0.027181 -0.000001 13.487594 -0.000000 0.032466 0.061484 -0.362601 0.000000 4.943967 0.000000 0.067650 0.068500 -0.359750 -1.438276 12.059815 -7.620788 0.640864 -0.408486 0.190600 0.000000 12.910863 -0.000000 0.235528 0.109550 -0.624197 -0.031254 11.193578 0.010376 -0.373137 -0.019827 -0.108350 0.054595 0.018457 -0.015135 0.513505 -0.060747 -0.082230 0.000000 3.968525 0.000000 -0.018040 0.136871 0.468302 0.000000 5.348644 -0.000000 0.774555 0.093614 0.440117 0.000000 2.438549 0.000000 0.142990 0.280994 -0.302707 -0.655276 1.265095 -0.625147 0.131281 -0.976366 0.127386 1.138250 11.707429 7.871705 -0.934770 -0.244695 -0.032039 0.000000 13.379274 -0.000000 -0.369701 0.218992 -0.505215 0.028222 10.991364 0.020885 0.126700 -0.269199 -0.245704 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.048213 3.511781 3.992419 0.024551 0.014341 0.999559 0.000000 13.074334 0.000000 0.012821 -0.086657 -0.169289 -0.035381 14.148866 0.000001 -0.000723 0.010195 0.508951 0.000000 4.097309 -0.000000 -0.001584 -0.005688 0.297625 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.265634 -0.127999 0.043994 0.988981 -0.000000 13.074365 0.000000 0.059969 0.153197 -0.050977 -0.035380 14.148879 0.012529 0.000040 -0.008083 0.503374 -0.000000 4.097290 0.000000 0.000156 0.000294 0.262708 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 73 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.745020 6.879979 9.271611 -0.004531 -0.150496 -0.887957 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.050287 0.081502 -0.217879 0.000000 7.977538 -0.000000 -0.028984 0.047273 0.026516 -0.000001 13.487594 -0.000000 0.030550 0.057200 -0.362772 0.000000 4.943967 0.000000 0.065703 0.066528 -0.361074 -1.437601 12.063319 -7.620929 0.640505 -0.408675 0.191099 0.000000 12.910863 -0.000000 0.235250 0.109865 -0.625309 -0.031254 11.193578 0.010376 -0.373770 -0.019867 -0.108563 0.054595 0.018457 -0.015135 0.514130 -0.060732 -0.081668 0.000000 3.968525 0.000000 -0.018167 0.137046 0.468535 0.000000 5.348644 -0.000000 0.774966 0.093660 0.440706 0.000000 2.438549 0.000000 0.142122 0.281310 -0.303076 -0.655276 1.265095 -0.625147 0.133074 -0.974634 0.138231 1.138894 11.710769 7.871569 -0.937501 -0.236681 -0.028523 0.000000 13.379274 -0.000000 -0.363975 0.213546 -0.508866 0.028222 10.991364 0.020885 0.121739 -0.266057 -0.238346 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.041761 3.513035 4.002294 0.024562 0.014123 0.999558 0.000000 13.074334 0.000000 0.012563 -0.086644 -0.168414 -0.035381 14.148866 0.000001 -0.000724 0.010195 0.508963 0.000000 4.097309 -0.000000 -0.001583 -0.005684 0.297581 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.257338 -0.126808 0.043808 0.989144 -0.000000 13.074365 0.000000 0.059697 0.152038 -0.051029 -0.035380 14.148879 0.012529 0.000040 -0.008083 0.503371 -0.000000 4.097290 0.000000 0.000156 0.000295 0.262738 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 74 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.778651 6.793829 9.332661 -0.001699 -0.150028 -0.886248 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.050967 0.067317 -0.215366 0.000000 7.977538 -0.000000 -0.030328 0.047411 0.025862 -0.000001 13.487594 -0.000000 0.028626 0.052898 -0.362933 0.000000 4.943967 0.000000 0.063738 0.064539 -0.362406 -1.437043 12.066212 -7.621046 0.640399 -0.408719 0.191552 0.000000 12.910863 -0.000000 0.235006 0.110142 -0.626288 -0.031254 11.193578 0.010376 -0.374298 -0.019900 -0.108741 0.054595 0.018457 -0.015135 0.514652 -0.060719 -0.081199 0.000000 3.968525 0.000000 -0.018272 0.137192 0.468731 0.000000 5.348644 -0.000000 0.775308 0.093698 0.441197 0.000000 2.438549 0.000000 0.141397 0.281573 -0.303383 -0.655276 1.265095 -0.625147 0.134165 -0.972602 0.150553 1.139490 11.713859 7.871445 -0.940208 -0.228971 -0.025809 0.000000 13.379274 -0.000000 -0.358248 0.209074 -0.513102 0.028222 10.991364 0.020885 0.116779 -0.264664 -0.229980 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.033341 3.514709 4.015224 0.024577 0.013839 0.999557 0.000000 13.074334 0.000000 0.012226 -0.086627 -0.167252 -0.035381 14.148866 0.000001 -0.000725 0.010194 0.508975 0.000000 4.097309 -0.000000 -0.001576 -0.005665 0.297395 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.246481 -0.125248 0.043565 0.989355 -0.000000 13.074365 0.000000 0.059342 0.150519 -0.051081 -0.035380 14.148879 0.012529 0.000040 -0.008083 0.503368 -0.000000 4.097290 0.000000 0.000157 0.000296 0.262766 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 75 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.815634 6.706026 9.392735 0.001159 -0.149644 -0.884426 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.051578 0.052566 -0.212547 0.000000 7.977538 -0.000000 -0.031641 0.047547 0.025222 -0.000001 13.487594 -0.000000 0.026697 0.048582 -0.363084 0.000000 4.943967 0.000000 0.061757 0.062534 -0.363745 -1.436601 12.068505 -7.621139 0.640534 -0.408624 0.191959 0.000000 12.910863 -0.000000 0.234793 0.110382 -0.627134 -0.031254 11.193578 0.010376 -0.374723 -0.019927 -0.108884 0.054595 0.018457 -0.015135 0.515072 -0.060709 -0.080820 0.000000 3.968525 0.000000 -0.018357 0.137309 0.468888 0.000000 5.348644 -0.000000 0.775583 0.093729 0.441592 0.000000 2.438549 0.000000 0.140814 0.281784 -0.303631 -0.655276 1.265095 -0.625147 0.134423 -0.970444 0.163127 1.140036 11.716693 7.871330 -0.942816 -0.221811 -0.024306 0.000000 13.379274 -0.000000 -0.353340 0.206736 -0.517578 0.028222 10.991364 0.020885 0.111431 -0.267537 -0.220727 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.023073 3.516780 4.031025 0.024595 0.013492 0.999555 0.000000 13.074334 0.000000 0.011813 -0.086606 -0.165820 -0.035381 14.148866 0.000001 -0.000727 0.010193 0.508986 0.000000 4.097309 -0.000000 -0.001566 -0.005632 0.297076 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.233217 -0.123343 0.043267 0.989608 -0.000000 13.074365 0.000000 0.058909 0.148664 -0.051133 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503366 -0.000000 4.097290 0.000000 0.000158 0.000297 0.262792 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 76 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.857216 6.616469 9.452676 0.003989 -0.149362 -0.882507 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.052119 0.037333 -0.209442 0.000000 7.977538 -0.000000 -0.032917 0.047678 0.024598 -0.000001 13.487594 -0.000000 0.024763 0.044258 -0.363225 0.000000 4.943967 0.000000 0.059763 0.060515 -0.365088 -1.436273 12.070208 -7.621208 0.640902 -0.408396 0.192320 0.000000 12.910863 -0.000000 0.234613 0.110585 -0.627849 -0.031254 11.193578 0.010376 -0.375048 -0.019948 -0.108993 0.054595 0.018457 -0.015135 0.515392 -0.060701 -0.080532 0.000000 3.968525 0.000000 -0.018422 0.137399 0.469008 0.000000 5.348644 -0.000000 0.775791 0.093752 0.441892 0.000000 2.438549 0.000000 0.140368 0.281946 -0.303819 -0.655276 1.265095 -0.625147 0.133509 -0.967994 0.176949 1.140531 11.719265 7.871226 -0.945399 -0.215434 -0.024397 0.000000 13.379274 -0.000000 -0.349012 0.207079 -0.522914 0.028222 10.991364 0.020885 0.105615 -0.275855 -0.209965 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -4.011073 3.519224 4.049517 0.024616 0.013086 0.999553 0.000000 13.074334 0.000000 0.011329 -0.086582 -0.164134 -0.035381 14.148866 0.000001 -0.000728 0.010193 0.508996 0.000000 4.097309 -0.000000 -0.001552 -0.005587 0.296634 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.217698 -0.121112 0.042919 0.989899 -0.000000 13.074365 0.000000 0.058402 0.146491 -0.051185 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503363 -0.000000 4.097290 0.000000 0.000158 0.000298 0.262814 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 77 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.902850 6.524810 9.513315 0.006734 -0.149201 -0.880511 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.052591 0.021699 -0.206070 0.000000 7.977538 -0.000000 -0.034151 0.047805 0.023992 -0.000001 13.487594 -0.000000 0.022827 0.039927 -0.363355 0.000000 4.943967 0.000000 0.057757 0.058484 -0.366436 -1.436057 12.071330 -7.621253 0.641494 -0.408039 0.192635 0.000000 12.910863 -0.000000 0.234465 0.110752 -0.628435 -0.031254 11.193578 0.010376 -0.375272 -0.019962 -0.109069 0.054595 0.018457 -0.015135 0.515614 -0.060695 -0.080332 0.000000 3.968525 0.000000 -0.018467 0.137462 0.469091 0.000000 5.348644 -0.000000 0.775936 0.093768 0.442100 0.000000 2.438549 0.000000 0.140060 0.282058 -0.303950 -0.655276 1.265095 -0.625147 0.131547 -0.965011 0.192839 1.140975 11.721569 7.871133 -0.948014 -0.209628 -0.025722 0.000000 13.379274 -0.000000 -0.344647 0.209135 -0.529301 0.028222 10.991364 0.020885 0.099619 -0.287523 -0.197653 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.997451 3.522016 4.070529 0.024640 0.012625 0.999549 0.000000 13.074334 0.000000 0.010779 -0.086555 -0.162209 -0.035381 14.148866 0.000001 -0.000729 0.010192 0.509006 0.000000 4.097309 -0.000000 -0.001533 -0.005531 0.296078 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.200067 -0.118576 0.042522 0.990224 -0.000000 13.074365 0.000000 0.057826 0.144021 -0.051236 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503361 -0.000000 4.097290 0.000000 0.000159 0.000299 0.262835 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 78 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 30.951043 6.431133 9.573434 0.009334 -0.149180 -0.878457 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.052993 0.005750 -0.202454 0.000000 7.977538 -0.000000 -0.035339 0.047928 0.023407 -0.000001 13.487594 -0.000000 0.020891 0.035595 -0.363474 0.000000 4.943967 0.000000 0.055743 0.056445 -0.367785 -1.435951 12.071880 -7.621275 0.642303 -0.407556 0.192902 0.000000 12.910863 -0.000000 0.234349 0.110881 -0.628891 -0.031254 11.193578 0.010376 -0.375399 -0.019970 -0.109111 0.054595 0.018457 -0.015135 0.515739 -0.060692 -0.080220 0.000000 3.968525 0.000000 -0.018492 0.137497 0.469138 0.000000 5.348644 -0.000000 0.776016 0.093777 0.442217 0.000000 2.438549 0.000000 0.139886 0.282120 -0.304023 -0.655276 1.265095 -0.625147 0.128930 -0.961646 0.209452 1.141367 11.723599 7.871051 -0.950568 -0.204121 -0.027842 0.000000 13.379274 -0.000000 -0.340615 0.212294 -0.535929 0.028222 10.991364 0.020885 0.093503 -0.301197 -0.184557 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.982317 3.525135 4.093894 0.024667 0.012112 0.999543 0.000000 13.074334 0.000000 0.010167 -0.086524 -0.160060 -0.035381 14.148866 0.000001 -0.000730 0.010192 0.509014 0.000000 4.097309 -0.000000 -0.001512 -0.005463 0.295414 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.180464 -0.115754 0.042082 0.990578 -0.000000 13.074365 0.000000 0.057185 0.141272 -0.051287 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503360 -0.000000 4.097290 0.000000 0.000159 0.000300 0.262852 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 79 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.000652 6.335391 9.631949 0.011725 -0.149325 -0.876370 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053326 -0.010430 -0.198618 0.000000 7.977538 -0.000000 -0.036475 0.048045 0.022846 -0.000001 13.487594 -0.000000 0.018955 0.031265 -0.363582 0.000000 4.943967 0.000000 0.053721 0.054398 -0.369135 -1.435954 12.071866 -7.621275 0.643322 -0.406952 0.193122 0.000000 12.910863 -0.000000 0.234266 0.110974 -0.629217 -0.031254 11.193578 0.010376 -0.375427 -0.019972 -0.109121 0.054595 0.018457 -0.015135 0.515767 -0.060691 -0.080194 0.000000 3.968525 0.000000 -0.018497 0.137504 0.469149 0.000000 5.348644 -0.000000 0.776035 0.093779 0.442243 0.000000 2.438549 0.000000 0.139848 0.282135 -0.304040 -0.655276 1.265095 -0.625147 0.125973 -0.958098 0.225656 1.141704 11.725349 7.870980 -0.952986 -0.198700 -0.030418 0.000000 13.379274 -0.000000 -0.337264 0.216102 -0.542126 0.028222 10.991364 0.020885 0.087324 -0.315846 -0.171316 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.965772 3.528560 4.119452 0.024696 0.011552 0.999535 0.000000 13.074334 0.000000 0.009498 -0.086489 -0.157702 -0.035381 14.148866 0.000001 -0.000731 0.010191 0.509022 0.000000 4.097309 -0.000000 -0.001487 -0.005385 0.294652 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.159024 -0.112666 0.041599 0.990955 -0.000000 13.074365 0.000000 0.056483 0.138264 -0.051335 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503358 -0.000000 4.097290 0.000000 0.000159 0.000301 0.262867 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 80 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.050668 6.237421 9.687731 0.013836 -0.149660 -0.874273 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053591 -0.026753 -0.194584 0.000000 7.977538 -0.000000 -0.037556 0.048156 0.022311 -0.000001 13.487594 -0.000000 0.017023 0.026941 -0.363679 0.000000 4.943967 0.000000 0.051694 0.052347 -0.370484 -1.436064 12.071296 -7.621252 0.644545 -0.406228 0.193293 0.000000 12.910863 -0.000000 0.234216 0.111030 -0.629414 -0.031254 11.193578 0.010376 -0.375358 -0.019968 -0.109097 0.054595 0.018457 -0.015135 0.515699 -0.060693 -0.080256 0.000000 3.968525 0.000000 -0.018484 0.137485 0.469123 0.000000 5.348644 -0.000000 0.775991 0.093775 0.442179 0.000000 2.438549 0.000000 0.139942 0.282100 -0.304000 -0.655276 1.265095 -0.625147 0.122974 -0.954641 0.240356 1.141986 11.726811 7.870921 -0.955204 -0.193165 -0.033157 0.000000 13.379274 -0.000000 -0.334978 0.220173 -0.547251 0.028222 10.991364 0.020885 0.081137 -0.330579 -0.158543 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.947920 3.532270 4.147046 0.024727 0.010946 0.999525 0.000000 13.074334 0.000000 0.008774 -0.086452 -0.155148 -0.035381 14.148866 0.000001 -0.000732 0.010191 0.509029 0.000000 4.097309 -0.000000 -0.001459 -0.005298 0.293797 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.135877 -0.109329 0.041077 0.991351 -0.000000 13.074365 0.000000 0.055725 0.135012 -0.051382 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503357 -0.000000 4.097290 0.000000 0.000160 0.000301 0.262880 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 81 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.100060 6.136908 9.739425 0.015588 -0.150216 -0.872197 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053788 -0.043134 -0.190380 0.000000 7.977538 -0.000000 -0.038574 0.048260 0.021805 -0.000001 13.487594 -0.000000 0.015095 0.022626 -0.363765 0.000000 4.943967 0.000000 0.049666 0.050293 -0.371831 -1.436280 12.070176 -7.621207 0.645967 -0.405387 0.193414 0.000000 12.910863 -0.000000 0.234198 0.111049 -0.629480 -0.031254 11.193578 0.010376 -0.375192 -0.019957 -0.109042 0.054595 0.018457 -0.015135 0.515535 -0.060697 -0.080403 0.000000 3.968525 0.000000 -0.018450 0.137439 0.469062 0.000000 5.348644 -0.000000 0.775884 0.093763 0.442026 0.000000 2.438549 0.000000 0.140170 0.282018 -0.303903 -0.655276 1.265095 -0.625147 0.120257 -0.951650 0.252318 1.142211 11.727981 7.870873 -0.957152 -0.187305 -0.035762 0.000000 13.379274 -0.000000 -0.334235 0.224132 -0.550579 0.028222 10.991364 0.020885 0.075007 -0.344520 -0.146916 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.928857 3.536243 4.176525 0.024761 0.010300 0.999513 0.000000 13.074334 0.000000 0.008001 -0.086411 -0.152413 -0.035381 14.148866 0.000001 -0.000732 0.010191 0.509035 0.000000 4.097309 -0.000000 -0.001428 -0.005203 0.292858 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.111151 -0.105761 0.040518 0.991761 -0.000000 13.074365 0.000000 0.054915 0.131536 -0.051426 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503356 -0.000000 4.097290 0.000000 0.000160 0.000302 0.262889 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 82 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.147598 6.033324 9.785197 0.016893 -0.151028 -0.870173 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053919 -0.059486 -0.186031 0.000000 7.977538 -0.000000 -0.039526 0.048358 0.021329 -0.000001 13.487594 -0.000000 0.013173 0.018325 -0.363839 0.000000 4.943967 0.000000 0.047637 0.048239 -0.373173 -1.436600 12.068515 -7.621139 0.647583 -0.404430 0.193485 0.000000 12.910863 -0.000000 0.234214 0.111030 -0.629414 -0.031254 11.193578 0.010376 -0.374929 -0.019940 -0.108953 0.054595 0.018457 -0.015135 0.515275 -0.060704 -0.080637 0.000000 3.968525 0.000000 -0.018398 0.137366 0.468964 0.000000 5.348644 -0.000000 0.775715 0.093744 0.441783 0.000000 2.438549 0.000000 0.140531 0.281887 -0.303750 -0.655276 1.265095 -0.625147 0.118218 -0.949648 0.259951 1.142379 11.728850 7.870838 -0.958739 -0.180856 -0.037881 0.000000 13.379274 -0.000000 -0.335699 0.227539 -0.551158 0.028222 10.991364 0.020885 0.069013 -0.356661 -0.137294 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.908681 3.540461 4.207741 0.024797 0.009615 0.999498 0.000000 13.074334 0.000000 0.007181 -0.086368 -0.149509 -0.035381 14.148866 0.000001 -0.000733 0.010190 0.509039 0.000000 4.097309 -0.000000 -0.001395 -0.005099 0.291840 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.084970 -0.101980 0.039927 0.992182 -0.000000 13.074365 0.000000 0.054055 0.127852 -0.051468 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503355 -0.000000 4.097290 0.000000 0.000160 0.000302 0.262896 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 83 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.193674 5.925032 9.824945 0.017769 -0.152243 -0.868110 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053985 -0.075722 -0.181566 0.000000 7.977538 -0.000000 -0.040406 0.048449 0.020888 -0.000001 13.487594 -0.000000 0.011259 0.014041 -0.363901 0.000000 4.943967 0.000000 0.045610 0.046187 -0.374510 -1.437023 12.066317 -7.621050 0.649390 -0.403359 0.193502 0.000000 12.910863 -0.000000 0.234263 0.110974 -0.629216 -0.031254 11.193578 0.010376 -0.374569 -0.019918 -0.108832 0.054595 0.018457 -0.015135 0.514920 -0.060712 -0.080958 0.000000 3.968525 0.000000 -0.018326 0.137267 0.468831 0.000000 5.348644 -0.000000 0.775483 0.093718 0.441449 0.000000 2.438549 0.000000 0.141025 0.281708 -0.303541 -0.655276 1.265095 -0.625147 0.117021 -0.948655 0.263540 1.142487 11.729412 7.870815 -0.959982 -0.173642 -0.039519 0.000000 13.379274 -0.000000 -0.339686 0.230561 -0.548853 0.028222 10.991364 0.020885 0.063095 -0.367542 -0.129593 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.887483 3.544904 4.240549 0.024834 0.008896 0.999480 0.000000 13.074334 0.000000 0.006320 -0.086321 -0.146450 -0.035381 14.148866 0.000001 -0.000733 0.010190 0.509043 0.000000 4.097309 -0.000000 -0.001359 -0.004988 0.290750 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.057455 -0.098003 0.039304 0.992607 -0.000000 13.074365 0.000000 0.053151 0.123976 -0.051506 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503355 -0.000000 4.097290 0.000000 0.000160 0.000302 0.262900 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 84 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.239633 5.811946 9.860784 0.018337 -0.153904 -0.865914 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053988 -0.091757 -0.177013 0.000000 7.977538 -0.000000 -0.041209 0.048531 0.020484 -0.000001 13.487594 -0.000000 0.009354 0.009778 -0.363952 0.000000 4.943967 0.000000 0.043587 0.044139 -0.375840 -1.437549 12.063589 -7.620940 0.651382 -0.402175 0.193466 0.000000 12.910863 -0.000000 0.234346 0.110879 -0.628882 -0.031254 11.193578 0.010376 -0.374111 -0.019889 -0.108678 0.054595 0.018457 -0.015135 0.514467 -0.060724 -0.081365 0.000000 3.968525 0.000000 -0.018235 0.137140 0.468662 0.000000 5.348644 -0.000000 0.775187 0.093685 0.441023 0.000000 2.438549 0.000000 0.141654 0.281480 -0.303275 -0.655276 1.265095 -0.625147 0.116385 -0.948201 0.265000 1.142535 11.729658 7.870805 -0.960996 -0.165836 -0.040991 0.000000 13.379274 -0.000000 -0.345583 0.233677 -0.544645 0.028222 10.991364 0.020885 0.057147 -0.378408 -0.122871 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.865358 3.549554 4.274807 0.024874 0.008144 0.999459 0.000000 13.074334 0.000000 0.005420 -0.086272 -0.143249 -0.035381 14.148866 0.000001 -0.000734 0.010190 0.509045 0.000000 4.097309 -0.000000 -0.001321 -0.004871 0.289595 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -6.028726 -0.093846 0.038652 0.993035 -0.000000 13.074365 0.000000 0.052206 0.119926 -0.051541 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503355 -0.000000 4.097290 0.000000 0.000160 0.000302 0.262901 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 85 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.285373 5.695444 9.893481 0.018646 -0.155910 -0.863611 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053930 -0.107504 -0.172403 0.000000 7.977538 -0.000000 -0.041927 0.048605 0.020120 -0.000001 13.487594 -0.000000 0.007461 0.005539 -0.363992 0.000000 4.943967 0.000000 0.041571 0.042098 -0.377161 -1.438176 12.060335 -7.620809 0.653557 -0.400878 0.193375 0.000000 12.910863 -0.000000 0.234464 0.110746 -0.628412 -0.031254 11.193578 0.010376 -0.373555 -0.019853 -0.108491 0.054595 0.018457 -0.015135 0.513918 -0.060737 -0.081859 0.000000 3.968525 0.000000 -0.018124 0.136986 0.468456 0.000000 5.348644 -0.000000 0.774826 0.093645 0.440506 0.000000 2.438549 0.000000 0.142417 0.281203 -0.302951 -0.655276 1.265096 -0.625147 0.116168 -0.948222 0.264651 1.142520 11.729583 7.870809 -0.961795 -0.157593 -0.042323 0.000000 13.379274 -0.000000 -0.352941 0.236812 -0.538881 0.028222 10.991364 0.020885 0.051192 -0.389105 -0.116968 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.842394 3.554390 4.310375 0.024914 0.007364 0.999434 0.000000 13.074334 0.000000 0.004485 -0.086219 -0.139917 -0.035381 14.148866 0.000001 -0.000734 0.010190 0.509046 0.000000 4.097309 -0.000000 -0.001282 -0.004747 0.288380 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.998900 -0.089528 0.037975 0.993460 -0.000000 13.074365 0.000000 0.051224 0.115716 -0.051571 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503355 -0.000000 4.097290 0.000000 0.000160 0.000302 0.262898 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 86 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.330801 5.576724 9.923691 0.018738 -0.158172 -0.861224 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053814 -0.122880 -0.167765 0.000000 7.977538 -0.000000 -0.042557 0.048670 0.019798 -0.000001 13.487594 -0.000000 0.005581 0.001330 -0.364020 0.000000 4.943967 0.000000 0.039564 0.040067 -0.378473 -1.438903 12.056563 -7.620656 0.655912 -0.399468 0.193226 0.000000 12.910863 -0.000000 0.234616 0.110573 -0.627802 -0.031254 11.193578 0.010376 -0.372899 -0.019812 -0.108270 0.054595 0.018457 -0.015135 0.513270 -0.060753 -0.082442 0.000000 3.968525 0.000000 -0.017993 0.136805 0.468214 0.000000 5.348644 -0.000000 0.774399 0.093597 0.439894 0.000000 2.438549 0.000000 0.143317 0.280876 -0.302569 -0.655276 1.265096 -0.625147 0.116246 -0.948652 0.262757 1.142442 11.729176 7.870825 -0.962391 -0.149048 -0.043536 0.000000 13.379274 -0.000000 -0.361372 0.239894 -0.531858 0.028222 10.991364 0.020885 0.045251 -0.399500 -0.111751 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.818682 3.559396 4.347116 0.024956 0.006559 0.999405 0.000000 13.074334 0.000000 0.003518 -0.086164 -0.136469 -0.035381 14.148866 0.000001 -0.000734 0.010190 0.509045 0.000000 4.097309 -0.000000 -0.001240 -0.004618 0.287111 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.968093 -0.085063 0.037274 0.993879 -0.000000 13.074365 0.000000 0.050209 0.111364 -0.051597 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503355 -0.000000 4.097290 0.000000 0.000160 0.000302 0.262893 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 87 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.375827 5.456857 9.951987 0.018649 -0.160615 -0.858773 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053643 -0.137802 -0.163132 0.000000 7.977538 -0.000000 -0.043091 0.048725 0.019523 -0.000001 13.487594 -0.000000 0.003716 -0.002847 -0.364036 0.000000 4.943967 0.000000 0.037569 0.038047 -0.379773 -1.439730 12.052274 -7.620483 0.658444 -0.397945 0.193017 0.000000 12.910863 -0.000000 0.234804 0.110359 -0.627050 -0.031254 11.193578 0.010376 -0.372143 -0.019764 -0.108016 0.054595 0.018457 -0.015135 0.512522 -0.060772 -0.083113 0.000000 3.968525 0.000000 -0.017842 0.136596 0.467934 0.000000 5.348644 -0.000000 0.773903 0.093541 0.439187 0.000000 2.438549 0.000000 0.144353 0.280498 -0.302128 -0.655276 1.265096 -0.625148 0.116506 -0.949431 0.259541 1.142298 11.728431 7.870855 -0.962799 -0.140325 -0.044646 0.000000 13.379274 -0.000000 -0.370531 0.242860 -0.523840 0.028222 10.991364 0.020885 0.039345 -0.409469 -0.107104 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.794309 3.564553 4.384893 0.024999 0.005731 0.999372 0.000000 13.074334 0.000000 0.002524 -0.086106 -0.132915 -0.035381 14.148866 0.000001 -0.000733 0.010190 0.509043 0.000000 4.097309 -0.000000 -0.001197 -0.004484 0.285793 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.936418 -0.080470 0.036553 0.994288 -0.000000 13.074365 0.000000 0.049163 0.106885 -0.051619 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503356 -0.000000 4.097290 0.000000 0.000160 0.000301 0.262885 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 88 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.420365 5.336832 9.978883 0.018411 -0.163166 -0.856278 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053419 -0.152188 -0.158535 0.000000 7.977538 -0.000000 -0.043522 0.048769 0.019296 -0.000001 13.487594 -0.000000 0.001868 -0.006987 -0.364040 0.000000 4.943967 0.000000 0.035587 0.036040 -0.381060 -1.440655 12.047474 -7.620288 0.661151 -0.396309 0.192747 0.000000 12.910863 -0.000000 0.235027 0.110105 -0.626153 -0.031254 11.193578 0.010376 -0.371286 -0.019710 -0.107728 0.054595 0.018457 -0.015135 0.511673 -0.060792 -0.083875 0.000000 3.968525 0.000000 -0.017671 0.136359 0.467618 0.000000 5.348644 -0.000000 0.773337 0.093478 0.438383 0.000000 2.438549 0.000000 0.145529 0.280070 -0.301627 -0.655276 1.265096 -0.625148 0.116846 -0.950499 0.255200 1.142087 11.727339 7.870899 -0.963033 -0.131537 -0.045670 0.000000 13.379274 -0.000000 -0.380100 0.245650 -0.515074 0.028222 10.991364 0.020885 0.033492 -0.418893 -0.102929 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.769363 3.569843 4.423573 0.025043 0.004883 0.999336 0.000000 13.074334 0.000000 0.001506 -0.086046 -0.129269 -0.035381 14.148866 0.000001 -0.000733 0.010190 0.509040 0.000000 4.097309 -0.000000 -0.001153 -0.004346 0.284433 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.903988 -0.075763 0.035814 0.994685 -0.000000 13.074365 0.000000 0.048091 0.102296 -0.051635 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503357 -0.000000 4.097290 0.000000 0.000159 0.000301 0.262873 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 89 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.464328 5.217582 10.004853 0.018050 -0.165761 -0.853757 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.053146 -0.165958 -0.154006 0.000000 7.977538 -0.000000 -0.043845 0.048802 0.019122 -0.000001 13.487594 -0.000000 0.000038 -0.011087 -0.364034 0.000000 4.943967 0.000000 0.033622 0.034051 -0.382332 -1.441678 12.042167 -7.620074 0.664031 -0.394558 0.192413 0.000000 12.910863 -0.000000 0.235288 0.109808 -0.625106 -0.031254 11.193578 0.010376 -0.370325 -0.019649 -0.107405 0.054595 0.018457 -0.015135 0.510722 -0.060815 -0.084727 0.000000 3.968525 0.000000 -0.017480 0.136093 0.467263 0.000000 5.348644 -0.000000 0.772698 0.093407 0.437478 0.000000 2.438549 0.000000 0.146846 0.279589 -0.301066 -0.655276 1.265096 -0.625148 0.117168 -0.951804 0.249906 1.141808 11.725890 7.870958 -0.963113 -0.122791 -0.046623 0.000000 13.379274 -0.000000 -0.389786 0.248208 -0.505804 0.028222 10.991364 0.020885 0.027711 -0.427662 -0.099136 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.743931 3.575249 4.463020 0.025088 0.004018 0.999295 0.000000 13.074334 0.000000 0.000467 -0.085983 -0.125543 -0.035381 14.148866 0.000001 -0.000732 0.010191 0.509034 0.000000 4.097309 -0.000000 -0.001107 -0.004204 0.283035 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.870917 -0.070960 0.035058 0.995066 -0.000000 13.074365 0.000000 0.046997 0.097612 -0.051645 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503359 -0.000000 4.097290 0.000000 0.000159 0.000300 0.262859 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 90 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.507627 5.100014 10.030344 0.017594 -0.168339 -0.851230 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.052826 -0.179031 -0.149580 0.000000 7.977538 -0.000000 -0.044053 0.048824 0.019005 -0.000001 13.487594 -0.000000 -0.001772 -0.015141 -0.364015 0.000000 4.943967 0.000000 0.031675 0.032080 -0.383589 -1.442798 12.036356 -7.619839 0.667083 -0.392692 0.192012 0.000000 12.910863 -0.000000 0.235586 0.109468 -0.623905 -0.031254 11.193578 0.010376 -0.369260 -0.019581 -0.107046 0.054595 0.018457 -0.015135 0.509665 -0.060841 -0.085673 0.000000 3.968525 0.000000 -0.017268 0.135799 0.466870 0.000000 5.348644 -0.000000 0.771984 0.093327 0.436469 0.000000 2.438549 0.000000 0.148307 0.279055 -0.300442 -0.655276 1.265096 -0.625148 0.117377 -0.953296 0.243818 1.141459 11.724076 7.871031 -0.963061 -0.114195 -0.047518 0.000000 13.379274 -0.000000 -0.399310 0.250483 -0.496271 0.028222 10.991364 0.020885 0.022021 -0.435661 -0.095643 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.718099 3.580752 4.503100 0.025134 0.003139 0.999250 0.000000 13.074334 0.000000 -0.000589 -0.085918 -0.121749 -0.035381 14.148866 0.000001 -0.000731 0.010191 0.509027 0.000000 4.097309 -0.000000 -0.001061 -0.004059 0.281606 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.837316 -0.066077 0.034290 0.995429 -0.000000 13.074365 0.000000 0.045884 0.092850 -0.051650 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503361 -0.000000 4.097290 0.000000 0.000159 0.000299 0.262841 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 91 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.550168 4.985030 10.055787 0.017068 -0.170837 -0.848716 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.052464 -0.191330 -0.145289 0.000000 7.977538 -0.000000 -0.044137 0.048833 0.018947 -0.000001 13.487594 -0.000000 -0.003559 -0.019147 -0.363986 0.000000 4.943967 0.000000 0.029750 0.030131 -0.384828 -1.444014 12.030043 -7.619584 0.670304 -0.390709 0.191542 0.000000 12.910863 -0.000000 0.235922 0.109083 -0.622547 -0.031254 11.193578 0.010376 -0.368087 -0.019507 -0.106652 0.054595 0.018457 -0.015135 0.508502 -0.060869 -0.086713 0.000000 3.968525 0.000000 -0.017035 0.135476 0.466437 0.000000 5.348644 -0.000000 0.771191 0.093238 0.435355 0.000000 2.438549 0.000000 0.149914 0.278467 -0.299755 -0.655276 1.265096 -0.625148 0.117379 -0.954931 0.237084 1.141037 11.721887 7.871120 -0.962906 -0.105851 -0.048368 0.000000 13.379274 -0.000000 -0.408400 0.252425 -0.486725 0.028222 10.991364 0.020885 0.016438 -0.442778 -0.092373 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.691954 3.586334 4.543682 0.025180 0.002250 0.999201 0.000000 13.074334 0.000000 -0.001658 -0.085851 -0.117900 -0.035381 14.148866 0.000001 -0.000730 0.010192 0.509018 0.000000 4.097309 -0.000000 -0.001013 -0.003912 0.280150 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.803297 -0.061131 0.033511 0.995771 -0.000000 13.074365 0.000000 0.044757 0.088025 -0.051648 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503363 -0.000000 4.097290 0.000000 0.000158 0.000298 0.262819 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 92 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.591850 4.873552 10.081612 0.016494 -0.173195 -0.846236 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.052063 -0.202775 -0.141167 0.000000 7.977538 -0.000000 -0.044090 0.048828 0.018953 -0.000001 13.487594 -0.000000 -0.005322 -0.023099 -0.363945 0.000000 4.943967 0.000000 0.027848 0.028205 -0.386049 -1.445327 12.023232 -7.619308 0.673695 -0.388608 0.190999 0.000000 12.910863 -0.000000 0.236296 0.108653 -0.621024 -0.031254 11.193578 0.010376 -0.366805 -0.019426 -0.106221 0.054595 0.018457 -0.015135 0.507228 -0.060899 -0.087850 0.000000 3.968525 0.000000 -0.016781 0.135123 0.465964 0.000000 5.348644 -0.000000 0.770315 0.093140 0.434131 0.000000 2.438549 0.000000 0.151669 0.277823 -0.299003 -0.655276 1.265096 -0.625148 0.117080 -0.956668 0.229844 1.140541 11.719314 7.871224 -0.962680 -0.097868 -0.049185 0.000000 13.379274 -0.000000 -0.416788 0.253987 -0.477426 0.028222 10.991364 0.020885 0.010982 -0.448890 -0.089255 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.665583 3.591979 4.584630 0.025226 0.001352 0.999147 0.000000 13.074334 0.000000 -0.002737 -0.085782 -0.114007 -0.035381 14.148866 0.000001 -0.000729 0.010192 0.509008 0.000000 4.097309 -0.000000 -0.000965 -0.003762 0.278673 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.768973 -0.056138 0.032725 0.996091 -0.000000 13.074365 0.000000 0.043618 0.083154 -0.051640 -0.035380 14.148879 0.012529 0.000039 -0.008083 0.503365 -0.000000 4.097290 0.000000 0.000158 0.000297 0.262794 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 93 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.632564 4.766545 10.108254 0.015898 -0.175350 -0.843813 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.051627 -0.213284 -0.137249 0.000000 7.977538 -0.000000 -0.043905 0.048809 0.019026 -0.000001 13.487594 -0.000000 -0.007060 -0.026994 -0.363894 0.000000 4.943967 0.000000 0.025973 0.026307 -0.387249 -1.446735 12.015924 -7.619013 0.677253 -0.386386 0.190381 0.000000 12.910863 -0.000000 0.236711 0.108174 -0.619333 -0.031254 11.193578 0.010376 -0.365411 -0.019338 -0.105752 0.054595 0.018457 -0.015135 0.505842 -0.060932 -0.089085 0.000000 3.968525 0.000000 -0.016505 0.134739 0.465451 0.000000 5.348644 -0.000000 0.769353 0.093032 0.432792 0.000000 2.438549 0.000000 0.153577 0.277121 -0.298183 -0.655276 1.265096 -0.625148 0.116380 -0.958470 0.222239 1.139969 11.716344 7.871344 -0.962423 -0.090356 -0.049983 0.000000 13.379274 -0.000000 -0.424205 0.255123 -0.468651 0.028222 10.991364 0.020885 0.005672 -0.453866 -0.086213 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.639073 3.597668 4.625809 0.025273 0.000450 0.999090 0.000000 13.074334 0.000000 -0.003823 -0.085712 -0.110085 -0.035381 14.148866 0.000001 -0.000728 0.010193 0.508995 0.000000 4.097309 -0.000000 -0.000916 -0.003611 0.277180 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.734458 -0.051115 0.031933 0.996387 -0.000000 13.074365 0.000000 0.042472 0.078252 -0.051625 -0.035380 14.148879 0.012529 0.000040 -0.008083 0.503368 -0.000000 4.097290 0.000000 0.000157 0.000296 0.262765 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 94 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.672187 4.665037 10.136167 0.015301 -0.177236 -0.841470 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.051158 -0.222777 -0.133571 0.000000 7.977538 -0.000000 -0.043571 0.048775 0.019172 -0.000001 13.487594 -0.000000 -0.008769 -0.030827 -0.363831 0.000000 4.943967 0.000000 0.024126 0.024437 -0.388427 -1.448239 12.008121 -7.618697 0.680978 -0.384040 0.189683 0.000000 12.910863 -0.000000 0.237167 0.107647 -0.617467 -0.031254 11.193578 0.010376 -0.363903 -0.019242 -0.105245 0.054595 0.018457 -0.015135 0.504340 -0.060967 -0.090421 0.000000 3.968525 0.000000 -0.016206 0.134324 0.464895 0.000000 5.348644 -0.000000 0.768300 0.092913 0.431335 0.000000 2.438549 0.000000 0.155642 0.276360 -0.297295 -0.655277 1.265096 -0.625148 0.115173 -0.960306 0.214407 1.139318 11.712968 7.871481 -0.962178 -0.083432 -0.050776 0.000000 13.379274 -0.000000 -0.430367 0.255783 -0.460697 0.028222 10.991364 0.020885 0.000530 -0.457560 -0.083176 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.612513 3.603383 4.667084 0.025319 -0.000454 0.999028 0.000000 13.074334 0.000000 -0.004911 -0.085639 -0.106145 -0.035381 14.148866 0.000001 -0.000726 0.010194 0.508980 0.000000 4.097309 -0.000000 -0.000867 -0.003458 0.275676 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.699864 -0.046079 0.031138 0.996657 -0.000000 13.074365 0.000000 0.041322 0.073338 -0.051602 -0.035380 14.148879 0.012529 0.000040 -0.008083 0.503372 -0.000000 4.097290 0.000000 0.000156 0.000295 0.262733 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 95 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.710581 4.570158 10.165834 0.014730 -0.178784 -0.839234 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.050662 -0.231167 -0.130168 0.000000 7.977538 -0.000000 -0.043081 0.048725 0.019395 -0.000001 13.487594 -0.000000 -0.010447 -0.034593 -0.363758 0.000000 4.943967 0.000000 0.022311 0.022600 -0.389581 -1.449838 11.999825 -7.618361 0.684871 -0.381569 0.188903 0.000000 12.910863 -0.000000 0.237664 0.107068 -0.615417 -0.031254 11.193578 0.010376 -0.362276 -0.019139 -0.104698 0.054595 0.018457 -0.015135 0.502719 -0.061004 -0.091860 0.000000 3.968525 0.000000 -0.015884 0.133878 0.464297 0.000000 5.348644 -0.000000 0.767151 0.092784 0.429755 0.000000 2.438549 0.000000 0.157866 0.275538 -0.296335 -0.655277 1.265096 -0.625148 0.113344 -0.962146 0.206494 1.138587 11.709175 7.871634 -0.961995 -0.077224 -0.051576 0.000000 13.379274 -0.000000 -0.434971 0.255914 -0.453886 0.028222 10.991364 0.020885 -0.004421 -0.459804 -0.080066 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.585992 3.609106 4.708318 0.025366 -0.001358 0.998963 0.000000 13.074334 0.000000 -0.005998 -0.085566 -0.102200 -0.035381 14.148866 0.000001 -0.000724 0.010195 0.508963 0.000000 4.097309 -0.000000 -0.000818 -0.003306 0.274168 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.665307 -0.041047 0.030343 0.996901 -0.000000 13.074365 0.000000 0.040173 0.068426 -0.051572 -0.035380 14.148879 0.012529 0.000040 -0.008082 0.503375 -0.000000 4.097290 0.000000 0.000155 0.000293 0.262696 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 96 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.747588 4.483164 10.197786 0.014211 -0.179913 -0.837135 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.050140 -0.238364 -0.127079 0.000000 7.977538 -0.000000 -0.042424 0.048658 0.019699 -0.000001 13.487594 -0.000000 -0.012094 -0.038288 -0.363674 0.000000 4.943967 0.000000 0.020530 0.020797 -0.390711 -1.451532 11.991037 -7.618006 0.688929 -0.378969 0.188036 0.000000 12.910863 -0.000000 0.238205 0.106436 -0.613178 -0.031254 11.193578 0.010376 -0.360528 -0.019029 -0.104111 0.054595 0.018457 -0.015135 0.500974 -0.061044 -0.093405 0.000000 3.968525 0.000000 -0.015539 0.133398 0.463654 0.000000 5.348644 -0.000000 0.765900 0.092644 0.428044 0.000000 2.438549 0.000000 0.160255 0.274652 -0.295302 -0.655277 1.265096 -0.625148 0.110765 -0.963961 0.198650 1.137773 11.704952 7.871805 -0.961928 -0.071872 -0.052400 0.000000 13.379274 -0.000000 -0.437679 0.255456 -0.448573 0.028222 10.991364 0.020885 -0.009156 -0.460402 -0.076800 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.559600 3.614818 4.749370 0.025412 -0.002257 0.998894 0.000000 13.074334 0.000000 -0.007081 -0.085491 -0.098264 -0.035381 14.148866 0.000001 -0.000722 0.010196 0.508944 0.000000 4.097309 -0.000000 -0.000769 -0.003153 0.272660 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.630905 -0.036036 0.029552 0.997118 -0.000000 13.074365 0.000000 0.039028 0.063535 -0.051534 -0.035380 14.148879 0.012529 0.000040 -0.008082 0.503379 -0.000000 4.097290 0.000000 0.000154 0.000291 0.262656 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 97 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.783023 4.405492 10.232615 0.013772 -0.180537 -0.835207 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.049596 -0.244270 -0.124344 0.000000 7.977538 -0.000000 -0.041590 0.048573 0.020090 -0.000001 13.487594 -0.000000 -0.013706 -0.041906 -0.363580 0.000000 4.943967 0.000000 0.018787 0.019032 -0.391814 -1.453320 11.981756 -7.617631 0.693154 -0.376236 0.187078 0.000000 12.910863 -0.000000 0.238790 0.105749 -0.610740 -0.031254 11.193578 0.010376 -0.358655 -0.018910 -0.103481 0.054595 0.018457 -0.015135 0.499102 -0.061087 -0.095060 0.000000 3.968525 0.000000 -0.015169 0.132885 0.462965 0.000000 5.348644 -0.000000 0.764540 0.092491 0.426199 0.000000 2.438549 0.000000 0.162814 0.273701 -0.294192 -0.655277 1.265096 -0.625148 0.107288 -0.965725 0.191044 1.136874 11.700286 7.871993 -0.962038 -0.067536 -0.053264 0.000000 13.379274 -0.000000 -0.438102 0.254333 -0.445155 0.028222 10.991364 0.020885 -0.013642 -0.459119 -0.073285 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.533428 3.620501 4.790100 0.025458 -0.003149 0.998822 0.000000 13.074334 0.000000 -0.008155 -0.085416 -0.094349 -0.035381 14.148866 0.000001 -0.000719 0.010197 0.508922 0.000000 4.097309 -0.000000 -0.000721 -0.003001 0.271158 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.596776 -0.031065 0.028766 0.997307 -0.000000 13.074365 0.000000 0.037892 0.058682 -0.051488 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503384 -0.000000 4.097290 0.000000 0.000153 0.000289 0.262612 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 98 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.816665 4.338816 10.271002 0.013445 -0.180552 -0.833487 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.049034 -0.248777 -0.122003 0.000000 7.977538 -0.000000 -0.040568 0.048468 0.020574 -0.000001 13.487594 -0.000000 -0.015281 -0.045443 -0.363476 0.000000 4.943967 0.000000 0.017083 0.017307 -0.392888 -1.455203 11.971984 -7.617236 0.697546 -0.373367 0.186024 0.000000 12.910863 -0.000000 0.239420 0.105004 -0.608094 -0.031254 11.193578 0.010376 -0.356653 -0.018783 -0.102808 0.054595 0.018457 -0.015135 0.497097 -0.061132 -0.096828 0.000000 3.968525 0.000000 -0.014775 0.132336 0.462230 0.000000 5.348644 -0.000000 0.763066 0.092325 0.424210 0.000000 2.438549 0.000000 0.165548 0.272682 -0.293003 -0.655277 1.265096 -0.625148 0.102740 -0.967410 0.183858 1.135887 11.695166 7.872201 -0.962386 -0.064401 -0.054185 0.000000 13.379274 -0.000000 -0.435772 0.252447 -0.444080 0.028222 10.991364 0.020885 -0.017844 -0.455665 -0.069418 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.507572 3.626137 4.830362 0.025504 -0.004030 0.998746 0.000000 13.074334 0.000000 -0.009217 -0.085341 -0.090469 -0.035381 14.148866 0.000001 -0.000716 0.010198 0.508897 0.000000 4.097309 -0.000000 -0.000672 -0.002850 0.269667 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.563042 -0.026151 0.027988 0.997469 -0.000000 13.074365 0.000000 0.036769 0.053884 -0.051433 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503389 -0.000000 4.097290 0.000000 0.000152 0.000287 0.262564 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 99 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.848249 4.285129 10.313747 0.013270 -0.179838 -0.832020 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.048456 -0.251768 -0.120103 0.000000 7.977538 -0.000000 -0.039345 0.048343 0.021156 -0.000001 13.487594 -0.000000 -0.016816 -0.048894 -0.363362 0.000000 4.943967 0.000000 0.015422 0.015625 -0.393933 -1.457182 11.961720 -7.616821 0.702105 -0.370358 0.184869 0.000000 12.910863 -0.000000 0.240097 0.104199 -0.605230 -0.031254 11.193578 0.010376 -0.354516 -0.018648 -0.102090 0.054595 0.018457 -0.015135 0.494955 -0.061179 -0.098713 0.000000 3.968525 0.000000 -0.014355 0.131752 0.461446 0.000000 5.348644 -0.000000 0.761468 0.092144 0.422071 0.000000 2.438549 0.000000 0.168463 0.271591 -0.291731 -0.655277 1.265096 -0.625148 0.096914 -0.968983 0.177308 1.134810 11.689577 7.872427 -0.963038 -0.062688 -0.055186 0.000000 13.379274 -0.000000 -0.430111 0.249670 -0.445862 0.028222 10.991364 0.020885 -0.021715 -0.449680 -0.065074 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.482127 3.631704 4.870009 0.025549 -0.004898 0.998668 0.000000 13.074334 0.000000 -0.010262 -0.085265 -0.086639 -0.035381 14.148866 0.000001 -0.000713 0.010200 0.508870 0.000000 4.097309 -0.000000 -0.000624 -0.002701 0.268194 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.529827 -0.021314 0.027222 0.997604 -0.000000 13.074365 0.000000 0.035663 0.049159 -0.051369 -0.035380 14.148879 0.012529 0.000041 -0.008082 0.503394 -0.000000 4.097290 0.000000 0.000151 0.000285 0.262511 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 100 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.877960 4.247571 10.364341 0.013249 -0.178106 -0.830862 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.047829 -0.253266 -0.118577 0.000000 7.977538 -0.000000 -0.037562 0.048160 0.022015 -0.000001 13.487594 -0.000000 -0.018310 -0.052252 -0.363239 0.000000 4.943967 0.000000 0.013807 0.013990 -0.394947 -1.459255 11.950964 -7.616386 0.706830 -0.367204 0.183608 0.000000 12.910863 -0.000000 0.240822 0.103330 -0.602137 -0.031254 11.193578 0.010376 -0.352240 -0.018504 -0.101326 0.054595 0.018457 -0.015135 0.492669 -0.061229 -0.100718 0.000000 3.968525 0.000000 -0.013908 0.131131 0.460612 0.000000 5.348644 -0.000000 0.759739 0.091949 0.419774 0.000000 2.438549 0.000000 0.171565 0.270426 -0.290373 -0.655277 1.265096 -0.625148 0.089698 -0.970486 0.171088 1.133640 11.683505 7.872672 -0.964113 -0.062825 -0.056336 0.000000 13.379274 -0.000000 -0.419626 0.245461 -0.451496 0.028222 10.991364 0.020885 -0.025257 -0.439857 -0.060075 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.457193 3.637184 4.908888 0.025592 -0.005748 0.998588 0.000000 13.074334 0.000000 -0.011288 -0.085190 -0.082872 -0.035381 14.148866 0.000001 -0.000710 0.010202 0.508841 0.000000 4.097309 -0.000000 -0.000577 -0.002555 0.266744 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.497259 -0.016571 0.026471 0.997713 -0.000000 13.074365 0.000000 0.034578 0.044527 -0.051297 -0.035380 14.148879 0.012529 0.000042 -0.008082 0.503400 -0.000000 4.097290 0.000000 0.000150 0.000282 0.262454 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 101 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.906067 4.225570 10.423315 0.013341 -0.175361 -0.829993 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.047124 -0.253462 -0.117313 0.000000 7.977538 -0.000000 -0.035020 0.047898 0.023248 -0.000001 13.487594 -0.000000 -0.019760 -0.055512 -0.363107 0.000000 4.943967 0.000000 0.012240 0.012405 -0.395927 -1.461423 11.939713 -7.615931 0.711724 -0.363900 0.182234 0.000000 12.910863 -0.000000 0.241597 0.102394 -0.598801 -0.031254 11.193578 0.010376 -0.349820 -0.018351 -0.100513 0.054595 0.018457 -0.015135 0.490233 -0.061281 -0.102849 0.000000 3.968525 0.000000 -0.013433 0.130471 0.459726 0.000000 5.348644 -0.000000 0.757867 0.091738 0.417308 0.000000 2.438549 0.000000 0.174862 0.269183 -0.288924 -0.655277 1.265096 -0.625148 0.081218 -0.971957 0.164793 1.132373 11.676934 7.872938 -0.965575 -0.064778 -0.057634 0.000000 13.379274 -0.000000 -0.404131 0.239638 -0.460804 0.028222 10.991364 0.020885 -0.028511 -0.425880 -0.054479 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.432870 3.642555 4.946842 0.025635 -0.006578 0.998507 0.000000 13.074334 0.000000 -0.012289 -0.085115 -0.079183 -0.035381 14.148866 0.000001 -0.000706 0.010203 0.508808 0.000000 4.097309 -0.000000 -0.000531 -0.002411 0.265324 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.465469 -0.011942 0.025737 0.997797 -0.000000 13.074365 0.000000 0.033520 0.040005 -0.051215 -0.035380 14.148879 0.012529 0.000042 -0.008082 0.503406 -0.000000 4.097290 0.000000 0.000148 0.000280 0.262393 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 102 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.932386 4.216469 10.487675 0.013520 -0.171853 -0.829359 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.046348 -0.252459 -0.116290 0.000000 7.977538 -0.000000 -0.031914 0.047579 0.024757 -0.000001 13.487594 -0.000000 -0.021163 -0.058669 -0.362965 0.000000 4.943967 0.000000 0.010726 0.010871 -0.396873 -1.463687 11.927967 -7.615456 0.716787 -0.360440 0.180742 0.000000 12.910863 -0.000000 0.242424 0.101388 -0.595210 -0.031254 11.193578 0.010376 -0.347248 -0.018189 -0.099649 0.054595 0.018457 -0.015135 0.487640 -0.061335 -0.105111 0.000000 3.968525 0.000000 -0.012930 0.129771 0.458785 0.000000 5.348644 -0.000000 0.755844 0.091509 0.414664 0.000000 2.438549 0.000000 0.178362 0.267859 -0.287381 -0.655277 1.265096 -0.625148 0.071560 -0.973348 0.158505 1.131008 11.669849 7.873225 -0.967268 -0.068178 -0.059015 0.000000 13.379274 -0.000000 -0.384682 0.232509 -0.472760 0.028222 10.991364 0.020885 -0.031467 -0.408631 -0.048471 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.409264 3.647796 4.983709 0.025677 -0.007384 0.998424 0.000000 13.074334 0.000000 -0.013261 -0.085042 -0.075588 -0.035381 14.148866 0.000001 -0.000702 0.010205 0.508772 0.000000 4.097309 -0.000000 -0.000486 -0.002271 0.263939 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.434593 -0.007447 0.025025 0.997857 -0.000000 13.074365 0.000000 0.032492 0.035614 -0.051123 -0.035380 14.148879 0.012529 0.000042 -0.008082 0.503412 -0.000000 4.097290 0.000000 0.000147 0.000277 0.262327 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 103 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.956721 4.218201 10.554905 0.013769 -0.167785 -0.828915 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.045506 -0.250355 -0.115491 0.000000 7.977538 -0.000000 -0.028400 0.047216 0.026461 -0.000001 13.487594 -0.000000 -0.022515 -0.061715 -0.362815 0.000000 4.943967 0.000000 0.009267 0.009394 -0.397782 -1.466047 11.915723 -7.614960 0.722019 -0.356819 0.179124 0.000000 12.910863 -0.000000 0.243304 0.100307 -0.591348 -0.031254 11.193578 0.010376 -0.344519 -0.018016 -0.098732 0.054595 0.018457 -0.015135 0.484883 -0.061392 -0.107508 0.000000 3.968525 0.000000 -0.012398 0.129030 0.457787 0.000000 5.348644 -0.000000 0.753655 0.091261 0.411830 0.000000 2.438549 0.000000 0.182072 0.266449 -0.285738 -0.655277 1.265096 -0.625148 0.060773 -0.974616 0.152294 1.129540 11.662234 7.873533 -0.969060 -0.072733 -0.060420 0.000000 13.379274 -0.000000 -0.362176 0.224339 -0.486500 0.028222 10.991364 0.020885 -0.034109 -0.388844 -0.042214 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.386484 3.652886 5.019323 0.025717 -0.008163 0.998341 0.000000 13.074334 0.000000 -0.014200 -0.084970 -0.072101 -0.035381 14.148866 0.000001 -0.000697 0.010208 0.508733 0.000000 4.097309 -0.000000 -0.000443 -0.002136 0.262596 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.404772 -0.003108 0.024336 0.997895 -0.000000 13.074365 0.000000 0.031499 0.031374 -0.051021 -0.035380 14.148879 0.012529 0.000043 -0.008082 0.503419 -0.000000 4.097290 0.000000 0.000145 0.000274 0.262256 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 104 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.978853 4.229102 10.622778 0.014070 -0.163333 -0.828628 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.044604 -0.247234 -0.114897 0.000000 7.977538 -0.000000 -0.024614 0.046825 0.028291 -0.000001 13.487594 -0.000000 -0.023815 -0.064645 -0.362656 0.000000 4.943967 0.000000 0.007866 0.007976 -0.398653 -1.468503 11.902980 -7.614445 0.727422 -0.353029 0.177373 0.000000 12.910863 -0.000000 0.244238 0.099148 -0.587199 -0.031254 11.193578 0.010376 -0.341624 -0.017833 -0.097761 0.054595 0.018457 -0.015135 0.481953 -0.061452 -0.110047 0.000000 3.968525 0.000000 -0.011834 0.128244 0.456730 0.000000 5.348644 -0.000000 0.751288 0.090993 0.408792 0.000000 2.438549 0.000000 0.186003 0.264948 -0.283989 -0.655277 1.265097 -0.625148 0.048883 -0.975719 0.146232 1.127967 11.654070 7.873863 -0.970842 -0.078200 -0.061796 0.000000 13.379274 -0.000000 -0.337446 0.215380 -0.501270 0.028222 10.991364 0.020885 -0.036415 -0.367178 -0.035861 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.364643 3.657801 5.053508 0.025755 -0.008909 0.998258 0.000000 13.074334 0.000000 -0.015101 -0.084900 -0.068740 -0.035381 14.148866 0.000001 -0.000692 0.010210 0.508691 0.000000 4.097309 -0.000000 -0.000401 -0.002005 0.261303 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.376153 0.001055 0.023675 0.997913 -0.000000 13.074365 0.000000 0.030547 0.027305 -0.050909 -0.035380 14.148879 0.012529 0.000043 -0.008082 0.503427 -0.000000 4.097290 0.000000 0.000143 0.000270 0.262180 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 105 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.998533 4.247787 10.689212 0.014410 -0.158653 -0.828472 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.043648 -0.243174 -0.114492 0.000000 7.977538 -0.000000 -0.020679 0.046418 0.030184 -0.000001 13.487594 -0.000000 -0.025059 -0.067450 -0.362489 0.000000 4.943967 0.000000 0.006527 0.006620 -0.399483 -1.471056 11.889733 -7.613909 0.732996 -0.349064 0.175481 0.000000 12.910863 -0.000000 0.245230 0.097906 -0.582743 -0.031254 11.193578 0.010376 -0.338556 -0.017639 -0.096731 0.054595 0.018457 -0.015135 0.478840 -0.061513 -0.112734 0.000000 3.968525 0.000000 -0.011237 0.127413 0.455611 0.000000 5.348644 -0.000000 0.748728 0.090703 0.405537 0.000000 2.438549 0.000000 0.190164 0.263351 -0.282130 -0.655277 1.265097 -0.625148 0.035894 -0.976613 0.140388 1.126284 11.645339 7.874216 -0.972523 -0.084366 -0.063095 0.000000 13.379274 -0.000000 -0.311311 0.205892 -0.516414 0.028222 10.991364 0.020885 -0.038359 -0.344262 -0.029559 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.343860 3.662517 5.086082 0.025792 -0.009620 0.998175 0.000000 13.074334 0.000000 -0.015960 -0.084833 -0.065521 -0.035381 14.148866 0.000001 -0.000687 0.010212 0.508645 0.000000 4.097309 -0.000000 -0.000360 -0.001880 0.260066 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.348888 0.005020 0.023046 0.997913 -0.000000 13.074365 0.000000 0.029641 0.023430 -0.050786 -0.035380 14.148879 0.012529 0.000044 -0.008082 0.503435 -0.000000 4.097290 0.000000 0.000141 0.000267 0.262100 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 106 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.015471 4.273069 10.752158 0.014776 -0.153893 -0.828424 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.042641 -0.238247 -0.114261 0.000000 7.977538 -0.000000 -0.016711 0.046006 0.032083 -0.000001 13.487594 -0.000000 -0.026243 -0.070124 -0.362315 0.000000 4.943967 0.000000 0.005253 0.005331 -0.400271 -1.473706 11.875978 -7.613353 0.738745 -0.344915 0.173438 0.000000 12.910863 -0.000000 0.246281 0.096575 -0.577961 -0.031254 11.193578 0.010376 -0.335305 -0.017434 -0.095640 0.054595 0.018457 -0.015135 0.475534 -0.061578 -0.115578 0.000000 3.968525 0.000000 -0.010606 0.126535 0.454427 0.000000 5.348644 -0.000000 0.745956 0.090388 0.402049 0.000000 2.438549 0.000000 0.194568 0.261653 -0.280154 -0.655277 1.265097 -0.625148 0.021793 -0.977254 0.134840 1.124488 11.636022 7.874593 -0.974031 -0.091041 -0.064268 0.000000 13.379274 -0.000000 -0.284610 0.196156 -0.531351 0.028222 10.991364 0.020885 -0.039909 -0.320726 -0.023461 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.324257 3.667010 5.116856 0.025827 -0.010291 0.998095 0.000000 13.074334 0.000000 -0.016772 -0.084769 -0.062461 -0.035381 14.148866 0.000001 -0.000681 0.010215 0.508596 0.000000 4.097309 -0.000000 -0.000322 -0.001762 0.258893 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.323136 0.008762 0.022451 0.997898 -0.000000 13.074365 0.000000 0.028786 0.019772 -0.050653 -0.035380 14.148879 0.012529 0.000044 -0.008081 0.503444 -0.000000 4.097290 0.000000 0.000139 0.000263 0.262013 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 107 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.029323 4.303892 10.809499 0.015156 -0.149202 -0.828470 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.041588 -0.232521 -0.114189 0.000000 7.977538 -0.000000 -0.012825 0.045603 0.033926 -0.000001 13.487594 -0.000000 -0.027364 -0.072659 -0.362133 0.000000 4.943967 0.000000 0.004048 0.004111 -0.401015 -1.476456 11.861712 -7.612776 0.744668 -0.340574 0.171236 0.000000 12.910863 -0.000000 0.247394 0.095149 -0.572829 -0.031254 11.193578 0.010376 -0.331861 -0.017216 -0.094484 0.054595 0.018457 -0.015135 0.472023 -0.061644 -0.118585 0.000000 3.968525 0.000000 -0.009940 0.125606 0.453174 0.000000 5.348644 -0.000000 0.742955 0.090047 0.398310 0.000000 2.438549 0.000000 0.199226 0.259847 -0.278052 -0.655277 1.265097 -0.625148 0.006545 -0.977593 0.129672 1.122575 11.626097 7.874994 -0.975312 -0.098044 -0.065267 0.000000 13.379274 -0.000000 -0.258217 0.186480 -0.545579 0.028222 10.991364 0.020885 -0.041033 -0.297216 -0.017724 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.305966 3.671253 5.145629 0.025859 -0.010918 0.998017 0.000000 13.074334 0.000000 -0.017531 -0.084708 -0.059579 -0.035381 14.148866 0.000001 -0.000675 0.010218 0.508542 0.000000 4.097309 -0.000000 -0.000287 -0.001651 0.257792 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.299066 0.012259 0.021896 0.997870 -0.000000 13.074365 0.000000 0.027987 0.016353 -0.050508 -0.035380 14.148879 0.012529 0.000045 -0.008081 0.503453 -0.000000 4.097290 0.000000 0.000137 0.000259 0.261922 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 108 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.039669 4.339282 10.858936 0.015537 -0.144732 -0.828595 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.040493 -0.226057 -0.114264 0.000000 7.977538 -0.000000 -0.009141 0.045219 0.035653 -0.000001 13.487594 -0.000000 -0.028418 -0.075046 -0.361944 0.000000 4.943967 0.000000 0.002916 0.002965 -0.401713 -1.479305 11.846930 -7.612178 0.750769 -0.336031 0.168862 0.000000 12.910863 -0.000000 0.248571 0.093621 -0.567322 -0.031254 11.193578 0.010376 -0.328214 -0.016986 -0.093261 0.054595 0.018457 -0.015135 0.468295 -0.061713 -0.121765 0.000000 3.968525 0.000000 -0.009236 0.124624 0.451848 0.000000 5.348644 -0.000000 0.739702 0.089676 0.394300 0.000000 2.438549 0.000000 0.204153 0.257927 -0.275818 -0.655277 1.265097 -0.625148 -0.009905 -0.977573 0.124982 1.120541 11.615542 7.875421 -0.976328 -0.105200 -0.066039 0.000000 13.379274 -0.000000 -0.233055 0.177199 -0.558654 0.028222 10.991364 0.020885 -0.041692 -0.274423 -0.012517 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.289122 3.675219 5.172192 0.025890 -0.011496 0.997943 0.000000 13.074334 0.000000 -0.018233 -0.084652 -0.056895 -0.035381 14.148866 0.000001 -0.000668 0.010221 0.508484 0.000000 4.097309 -0.000000 -0.000254 -0.001548 0.256771 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.276854 0.015484 0.021383 0.997833 -0.000000 13.074365 0.000000 0.027252 0.013200 -0.050351 -0.035380 14.148879 0.012529 0.000046 -0.008081 0.503463 -0.000000 4.097290 0.000000 0.000135 0.000254 0.261825 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 109 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.045991 4.378297 10.897860 0.015906 -0.140653 -0.828793 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.039361 -0.218916 -0.114471 0.000000 7.977538 -0.000000 -0.005791 0.044871 0.037194 -0.000001 13.487594 -0.000000 -0.029401 -0.077275 -0.361748 0.000000 4.943967 0.000000 0.001861 0.001897 -0.402362 -1.482254 11.831626 -7.611559 0.757048 -0.331276 0.166306 0.000000 12.910863 -0.000000 0.249814 0.091985 -0.561410 -0.031254 11.193578 0.010376 -0.324349 -0.016742 -0.091965 0.054595 0.018457 -0.015135 0.464336 -0.061784 -0.125127 0.000000 3.968525 0.000000 -0.008491 0.123585 0.450445 0.000000 5.348644 -0.000000 0.736172 0.089274 0.389997 0.000000 2.438549 0.000000 0.209363 0.255883 -0.273442 -0.655277 1.265097 -0.625148 -0.027634 -0.977129 0.120885 1.118381 11.604335 7.875874 -0.977050 -0.112327 -0.066524 0.000000 13.379274 -0.000000 -0.210119 0.168689 -0.570185 0.028222 10.991364 0.020885 -0.041840 -0.253098 -0.008027 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.273869 3.678879 5.196323 0.025917 -0.012021 0.997873 0.000000 13.074334 0.000000 -0.018870 -0.084601 -0.054429 -0.035381 14.148866 0.000001 -0.000661 0.010225 0.508422 0.000000 4.097309 -0.000000 -0.000224 -0.001454 0.255839 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.256685 0.018411 0.020918 0.997789 -0.000000 13.074365 0.000000 0.026586 0.010338 -0.050183 -0.035380 14.148879 0.012529 0.000046 -0.008081 0.503473 -0.000000 4.097290 0.000000 0.000133 0.000250 0.261722 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 110 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.047629 4.419982 10.923168 0.016249 -0.137158 -0.829056 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.038194 -0.211153 -0.114799 0.000000 7.977538 -0.000000 -0.002929 0.044573 0.038472 -0.000001 13.487594 -0.000000 -0.030309 -0.079338 -0.361545 0.000000 4.943967 0.000000 0.000886 0.000910 -0.402961 -1.485306 11.815794 -7.610919 0.763508 -0.326296 0.163554 0.000000 12.910863 -0.000000 0.251126 0.090233 -0.555062 -0.031254 11.193578 0.010376 -0.320254 -0.016484 -0.090592 0.054595 0.018457 -0.015135 0.460128 -0.061857 -0.128684 0.000000 3.968525 0.000000 -0.007705 0.122487 0.448960 0.000000 5.348644 -0.000000 0.732338 0.088836 0.385375 0.000000 2.438549 0.000000 0.214875 0.253708 -0.270913 -0.655277 1.265097 -0.625148 -0.046752 -0.976185 0.117524 1.116091 11.592450 7.876355 -0.977463 -0.119228 -0.066652 0.000000 13.379274 -0.000000 -0.190512 0.161372 -0.579795 0.028222 10.991364 0.020885 -0.041422 -0.234087 -0.004468 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.260361 3.682202 5.217785 0.025942 -0.012487 0.997808 0.000000 13.074334 0.000000 -0.019438 -0.084555 -0.052202 -0.035381 14.148866 0.000001 -0.000653 0.010229 0.508356 0.000000 4.097309 -0.000000 -0.000197 -0.001370 0.255006 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.238759 0.021012 0.020505 0.997741 -0.000000 13.074365 0.000000 0.025995 0.007795 -0.050002 -0.035380 14.148879 0.012529 0.000047 -0.008081 0.503484 -0.000000 4.097290 0.000000 0.000130 0.000245 0.261613 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 111 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.043724 4.463304 10.931006 0.016548 -0.134483 -0.829381 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.036996 -0.202825 -0.115237 0.000000 7.977538 -0.000000 -0.000741 0.044345 0.039390 -0.000001 13.487594 -0.000000 -0.031136 -0.081223 -0.361336 0.000000 4.943967 0.000000 -0.000004 0.000009 -0.403507 -1.488460 11.799426 -7.610257 0.770152 -0.321080 0.160591 0.000000 12.910863 -0.000000 0.252511 0.088354 -0.548240 -0.031254 11.193578 0.010376 -0.315912 -0.016210 -0.089136 0.054595 0.018457 -0.015135 0.455654 -0.061933 -0.132446 0.000000 3.968525 0.000000 -0.006873 0.121325 0.447387 0.000000 5.348644 -0.000000 0.728167 0.088360 0.380407 0.000000 2.438549 0.000000 0.220708 0.251391 -0.268221 -0.655277 1.265097 -0.625148 -0.067409 -0.974642 0.115081 1.113665 11.579861 7.876864 -0.977553 -0.125677 -0.066337 0.000000 13.379274 -0.000000 -0.175518 0.155747 -0.587075 0.028222 10.991364 0.020885 -0.040363 -0.218390 -0.002099 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.248761 3.685155 5.236329 0.025964 -0.012888 0.997750 0.000000 13.074334 0.000000 -0.019929 -0.084516 -0.050238 -0.035381 14.148866 0.000001 -0.000644 0.010233 0.508284 0.000000 4.097309 -0.000000 -0.000173 -0.001297 0.254282 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.223285 0.023255 0.020149 0.997694 -0.000000 13.074365 0.000000 0.025488 0.005601 -0.049807 -0.035380 14.148879 0.012529 0.000048 -0.008081 0.503496 -0.000000 4.097290 0.000000 0.000127 0.000240 0.261497 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 112 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.035101 4.508420 10.924330 0.016813 -0.132473 -0.829768 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.035772 -0.193984 -0.115773 0.000000 7.977538 -0.000000 0.000901 0.044175 0.040013 -0.000001 13.487594 -0.000000 -0.031856 -0.082875 -0.361108 0.000000 4.943967 0.000000 -0.000783 -0.000780 -0.403984 -1.491958 11.781278 -7.609523 0.777460 -0.315224 0.157177 0.000000 12.910863 -0.000000 0.254023 0.086265 -0.540637 -0.031254 11.193578 0.010376 -0.311136 -0.015909 -0.087536 0.054595 0.018457 -0.015135 0.450718 -0.062013 -0.136575 0.000000 3.968525 0.000000 -0.005960 0.120051 0.445659 0.000000 5.348644 -0.000000 0.723453 0.087820 0.374860 0.000000 2.438549 0.000000 0.227109 0.248829 -0.265246 -0.655277 1.265097 -0.625148 -0.091714 -0.972036 0.114684 1.110998 11.566025 7.877424 -0.977381 -0.131837 -0.065516 0.000000 13.379274 -0.000000 -0.164273 0.151515 -0.592593 0.028222 10.991364 0.020885 -0.038049 -0.207634 -0.001436 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.239017 3.687765 5.252054 0.025982 -0.013227 0.997699 0.000000 13.074334 0.000000 -0.020347 -0.084483 -0.048520 -0.035381 14.148866 0.000001 -0.000635 0.010237 0.508206 0.000000 4.097309 -0.000000 -0.000153 -0.001235 0.253666 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.210181 0.025153 0.019848 0.997648 -0.000000 13.074365 0.000000 0.025061 0.003744 -0.049594 -0.035380 14.148879 0.012529 0.000048 -0.008080 0.503508 -0.000000 4.097290 0.000000 0.000124 0.000234 0.261367 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 113 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.023496 4.556073 10.909729 0.017069 -0.130767 -0.830218 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.034525 -0.184683 -0.116397 0.000000 7.977538 -0.000000 0.002285 0.044032 0.040496 -0.000001 13.487594 -0.000000 -0.032456 -0.084266 -0.360852 0.000000 4.943967 0.000000 -0.001438 -0.001443 -0.404385 -1.495987 11.760370 -7.608678 0.785786 -0.308394 0.153126 0.000000 12.910863 -0.000000 0.255701 0.083902 -0.532014 -0.031254 11.193578 0.010376 -0.305785 -0.015571 -0.085743 0.054595 0.018457 -0.015135 0.445169 -0.062099 -0.141190 0.000000 3.968525 0.000000 -0.004941 0.118626 0.443726 0.000000 5.348644 -0.000000 0.718013 0.087197 0.368546 0.000000 2.438549 0.000000 0.234263 0.245944 -0.261897 -0.655277 1.265097 -0.625148 -0.119626 -0.968120 0.116181 1.108012 11.550530 7.878051 -0.977038 -0.138032 -0.064257 0.000000 13.379274 -0.000000 -0.154789 0.147949 -0.597318 0.028222 10.991364 0.020885 -0.034469 -0.201074 -0.002422 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.230854 3.690102 5.265396 0.025999 -0.013513 0.997652 0.000000 13.074334 0.000000 -0.020703 -0.084456 -0.047002 -0.035381 14.148866 0.000001 -0.000625 0.010242 0.508119 0.000000 4.097309 -0.000000 -0.000136 -0.001182 0.253142 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.199085 0.026760 0.019594 0.997604 -0.000000 13.074365 0.000000 0.024702 0.002172 -0.049357 -0.035380 14.148879 0.012529 0.000049 -0.008080 0.503523 -0.000000 4.097290 0.000000 0.000121 0.000227 0.261214 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 114 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 32.009186 4.606026 10.887940 0.017316 -0.129336 -0.830726 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.033259 -0.174974 -0.117098 0.000000 7.977538 -0.000000 0.003435 0.043914 0.040850 -0.000001 13.487594 -0.000000 -0.032946 -0.085420 -0.360572 0.000000 4.943967 0.000000 -0.001980 -0.001991 -0.404716 -1.500479 11.737062 -7.607735 0.794947 -0.300671 0.148494 0.000000 12.910863 -0.000000 0.257518 0.081283 -0.522426 -0.031254 11.193578 0.010376 -0.299904 -0.015201 -0.083774 0.054595 0.018457 -0.015135 0.439049 -0.062190 -0.146246 0.000000 3.968525 0.000000 -0.003825 0.117064 0.441605 0.000000 5.348644 -0.000000 0.711845 0.086489 0.361488 0.000000 2.438549 0.000000 0.242102 0.242754 -0.258196 -0.655277 1.265097 -0.625148 -0.148752 -0.963157 0.118247 1.104736 11.533532 7.878738 -0.976543 -0.144250 -0.062635 0.000000 13.379274 -0.000000 -0.146900 0.144992 -0.601360 0.028222 10.991364 0.020885 -0.030224 -0.196931 -0.004695 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.224125 3.692191 5.276578 0.026013 -0.013752 0.997611 0.000000 13.074334 0.000000 -0.021003 -0.084434 -0.045666 -0.035381 14.148866 0.000001 -0.000614 0.010247 0.508026 0.000000 4.097309 -0.000000 -0.000122 -0.001138 0.252702 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.189808 0.028102 0.019383 0.997564 -0.000000 13.074365 0.000000 0.024406 0.000859 -0.049098 -0.035380 14.148879 0.012529 0.000050 -0.008080 0.503538 -0.000000 4.097290 0.000000 0.000117 0.000220 0.261042 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 115 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.992418 4.658068 10.859621 0.017555 -0.128155 -0.831286 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.031978 -0.164909 -0.117867 0.000000 7.977538 -0.000000 0.004374 0.043818 0.041088 -0.000001 13.487594 -0.000000 -0.033339 -0.086360 -0.360272 0.000000 4.943967 0.000000 -0.002419 -0.002436 -0.404984 -1.505371 11.711678 -7.606709 0.804772 -0.292131 0.143333 0.000000 12.910863 -0.000000 0.259454 0.078423 -0.511921 -0.031254 11.193578 0.010376 -0.293536 -0.014800 -0.081642 0.054595 0.018457 -0.015135 0.432397 -0.062283 -0.151703 0.000000 3.968525 0.000000 -0.002620 0.115377 0.439311 0.000000 5.348644 -0.000000 0.704940 0.085695 0.353706 0.000000 2.438549 0.000000 0.250563 0.239279 -0.254166 -0.655277 1.265097 -0.625148 -0.177075 -0.957588 0.119788 1.101196 11.515167 7.879481 -0.975910 -0.150476 -0.060719 0.000000 13.379274 -0.000000 -0.140455 0.142589 -0.604809 0.028222 10.991364 0.020885 -0.025784 -0.193836 -0.008044 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.218700 3.694055 5.285797 0.026025 -0.013947 0.997574 0.000000 13.074334 0.000000 -0.021252 -0.084416 -0.044494 -0.035381 14.148866 0.000001 -0.000603 0.010252 0.507926 0.000000 4.097309 -0.000000 -0.000110 -0.001101 0.252337 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.182185 0.029204 0.019210 0.997527 -0.000000 13.074365 0.000000 0.024166 -0.000219 -0.048821 -0.035380 14.148879 0.012529 0.000051 -0.008080 0.503555 -0.000000 4.097290 0.000000 0.000112 0.000212 0.260853 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 116 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.973416 4.712011 10.825363 0.017787 -0.127202 -0.831895 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.030685 -0.154539 -0.118694 0.000000 7.977538 -0.000000 0.005119 0.043743 0.041220 -0.000001 13.487594 -0.000000 -0.033642 -0.087106 -0.359954 0.000000 4.943967 0.000000 -0.002766 -0.002788 -0.405196 -1.510607 11.684514 -7.605610 0.815098 -0.282847 0.137693 0.000000 12.910863 -0.000000 0.261487 0.075337 -0.500544 -0.031254 11.193578 0.010376 -0.286718 -0.014371 -0.079361 0.054595 0.018457 -0.015135 0.425247 -0.062378 -0.157524 0.000000 3.968525 0.000000 -0.001335 0.113577 0.436858 0.000000 5.348644 -0.000000 0.697289 0.084814 0.345218 0.000000 2.438549 0.000000 0.259587 0.235535 -0.249827 -0.655277 1.265097 -0.625148 -0.202566 -0.952106 0.119703 1.097417 11.495559 7.880274 -0.975151 -0.156698 -0.058570 0.000000 13.379274 -0.000000 -0.135317 0.140689 -0.607740 0.028222 10.991364 0.020885 -0.021606 -0.190509 -0.012349 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.214460 3.695714 5.293230 0.026035 -0.014102 0.997541 0.000000 13.074334 0.000000 -0.021455 -0.084403 -0.043473 -0.035381 14.148866 0.000001 -0.000590 0.010258 0.507820 0.000000 4.097309 -0.000000 -0.000101 -0.001071 0.252041 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.176066 0.030088 0.019072 0.997494 -0.000000 13.074365 0.000000 0.023978 -0.001084 -0.048526 -0.035380 14.148879 0.012529 0.000052 -0.008079 0.503573 -0.000000 4.097290 0.000000 0.000107 0.000203 0.260650 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 117 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.952380 4.767683 10.785702 0.018012 -0.126456 -0.832547 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.029386 -0.143916 -0.119570 0.000000 7.977538 -0.000000 0.005690 0.043686 0.041256 -0.000001 13.487594 -0.000000 -0.033864 -0.087677 -0.359622 0.000000 4.943967 0.000000 -0.003029 -0.003054 -0.405357 -1.516132 11.655843 -7.604451 0.825774 -0.272888 0.131624 0.000000 12.910863 -0.000000 0.263595 0.072038 -0.488336 -0.031254 11.193578 0.010376 -0.279484 -0.013915 -0.076941 0.054595 0.018457 -0.015135 0.417630 -0.062472 -0.163675 0.000000 3.968525 0.000000 0.000023 0.111672 0.434260 0.000000 5.348644 -0.000000 0.688881 0.083844 0.336041 0.000000 2.438549 0.000000 0.269121 0.231536 -0.245195 -0.655277 1.265097 -0.625148 -0.222760 -0.947762 0.116628 1.093420 11.474820 7.881113 -0.974275 -0.162906 -0.056245 0.000000 13.379274 -0.000000 -0.131364 0.139249 -0.610215 0.028222 10.991364 0.020885 -0.018230 -0.185470 -0.017559 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.211299 3.697186 5.299039 0.026044 -0.014221 0.997512 0.000000 13.074334 0.000000 -0.021615 -0.084393 -0.042588 -0.035381 14.148866 0.000001 -0.000577 0.010265 0.507709 0.000000 4.097309 -0.000000 -0.000093 -0.001048 0.251807 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.171314 0.030773 0.018965 0.997466 -0.000000 13.074365 0.000000 0.023836 -0.001754 -0.048215 -0.035380 14.148879 0.012529 0.000054 -0.008079 0.503592 -0.000000 4.097290 0.000000 0.000102 0.000193 0.260434 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 118 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.929495 4.824928 10.741127 0.018231 -0.125898 -0.833239 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.028084 -0.133092 -0.120487 0.000000 7.977538 -0.000000 0.006100 0.043646 0.041204 -0.000001 13.487594 -0.000000 -0.034014 -0.088088 -0.359278 0.000000 4.943967 0.000000 -0.003217 -0.003244 -0.405471 -1.521900 11.625916 -7.603240 0.836656 -0.262327 0.125174 0.000000 12.910863 -0.000000 0.265761 0.068538 -0.475334 -0.031254 11.193578 0.010376 -0.271867 -0.013436 -0.074395 0.054595 0.018457 -0.015135 0.409576 -0.062563 -0.170124 0.000000 3.968525 0.000000 0.001448 0.109672 0.431527 0.000000 5.348644 -0.000000 0.679707 0.082784 0.326190 0.000000 2.438549 0.000000 0.279115 0.227296 -0.240288 -0.655277 1.265097 -0.625148 -0.235726 -0.945540 0.110096 1.089225 11.453054 7.881993 -0.973289 -0.169087 -0.053795 0.000000 13.379274 -0.000000 -0.128486 0.138226 -0.612289 0.028222 10.991364 0.020885 -0.015654 -0.178326 -0.023440 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.209119 3.698489 5.303371 0.026051 -0.014307 0.997487 0.000000 13.074334 0.000000 -0.021738 -0.084388 -0.041827 -0.035381 14.148866 0.000001 -0.000564 0.010271 0.507593 0.000000 4.097309 -0.000000 -0.000087 -0.001030 0.251630 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.167807 0.031278 0.018888 0.997441 -0.000000 13.074365 0.000000 0.023737 -0.002248 -0.047889 -0.035380 14.148879 0.012529 0.000055 -0.008079 0.503612 -0.000000 4.097290 0.000000 0.000097 0.000183 0.260209 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 119 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.904930 4.883601 10.692087 0.018444 -0.125512 -0.833968 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.026784 -0.122119 -0.121436 0.000000 7.977538 -0.000000 0.006366 0.043621 0.041073 -0.000001 13.487594 -0.000000 -0.034098 -0.088356 -0.358925 0.000000 4.943967 0.000000 -0.003335 -0.003364 -0.405543 -1.527864 11.594972 -7.601989 0.847608 -0.251240 0.118393 0.000000 12.910863 -0.000000 0.267964 0.064850 -0.461578 -0.031254 11.193578 0.010376 -0.263896 -0.012935 -0.071731 0.054595 0.018457 -0.015135 0.401111 -0.062652 -0.176841 0.000000 3.968525 0.000000 0.002933 0.107585 0.428671 0.000000 5.348644 -0.000000 0.669759 0.081633 0.315687 0.000000 2.438549 0.000000 0.289521 0.222829 -0.235122 -0.655277 1.265097 -0.625148 -0.243664 -0.944913 0.101155 1.084850 11.430353 7.882911 -0.972202 -0.175230 -0.051270 0.000000 13.379274 -0.000000 -0.126581 0.137583 -0.614006 0.028222 10.991364 0.020885 -0.013373 -0.170117 -0.029729 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.207829 3.699638 5.306361 0.026056 -0.014364 0.997466 0.000000 13.074334 0.000000 -0.021826 -0.084385 -0.041178 -0.035381 14.148866 0.000001 -0.000549 0.010278 0.507472 0.000000 4.097309 -0.000000 -0.000083 -0.001018 0.251505 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.165430 0.031618 0.018837 0.997421 -0.000000 13.074365 0.000000 0.023677 -0.002582 -0.047550 -0.035380 14.148879 0.012529 0.000056 -0.008078 0.503633 -0.000000 4.097290 0.000000 0.000092 0.000173 0.259974 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 120 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.878842 4.943569 10.639000 0.018653 -0.125280 -0.834729 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.025489 -0.111049 -0.122409 0.000000 7.977538 -0.000000 0.006501 0.043610 0.040870 -0.000001 13.487594 -0.000000 -0.034123 -0.088494 -0.358565 0.000000 4.943967 0.000000 -0.003392 -0.003422 -0.405578 -1.533980 11.563234 -7.600706 0.858506 -0.239703 0.111333 0.000000 12.910863 -0.000000 0.270188 0.060987 -0.447106 -0.031254 11.193578 0.010376 -0.255599 -0.012414 -0.068960 0.054595 0.018457 -0.015135 0.392263 -0.062735 -0.183798 0.000000 3.968525 0.000000 0.004474 0.105419 0.425701 0.000000 5.348644 -0.000000 0.659033 0.080390 0.304550 0.000000 2.438549 0.000000 0.300292 0.218148 -0.229711 -0.655277 1.265097 -0.625148 -0.248787 -0.945160 0.090045 1.080312 11.406805 7.883864 -0.971020 -0.181324 -0.048716 0.000000 13.379274 -0.000000 -0.125558 0.137284 -0.615406 0.028222 10.991364 0.020885 -0.011346 -0.161088 -0.036397 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.207347 3.700647 5.308134 0.026061 -0.014394 0.997447 0.000000 13.074334 0.000000 -0.021882 -0.084385 -0.040632 -0.035381 14.148866 0.000001 -0.000535 0.010285 0.507348 0.000000 4.097309 -0.000000 -0.000081 -0.001010 0.251425 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.164078 0.031810 0.018810 0.997404 -0.000000 13.074365 0.000000 0.023651 -0.002770 -0.047200 -0.035380 14.148879 0.012529 0.000058 -0.008078 0.503654 -0.000000 4.097290 0.000000 0.000086 0.000162 0.259734 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 121 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.851377 5.004709 10.582254 0.018857 -0.125189 -0.835518 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.024204 -0.099935 -0.123398 0.000000 7.977538 -0.000000 0.006516 0.043612 0.040602 -0.000001 13.487594 -0.000000 -0.034095 -0.088516 -0.358201 0.000000 4.943967 0.000000 -0.003394 -0.003423 -0.405579 -1.540209 11.530917 -7.599399 0.869233 -0.227797 0.104047 0.000000 12.910863 -0.000000 0.272414 0.056960 -0.431958 -0.031254 11.193578 0.010376 -0.247003 -0.011875 -0.066090 0.054595 0.018457 -0.015135 0.383058 -0.062813 -0.190967 0.000000 3.968525 0.000000 0.006064 0.103181 0.422627 0.000000 5.348644 -0.000000 0.647531 0.079055 0.292804 0.000000 2.438549 0.000000 0.311386 0.213265 -0.224071 -0.655277 1.265097 -0.625148 -0.252795 -0.945653 0.076792 1.075626 11.382491 7.884847 -0.969751 -0.187358 -0.046179 0.000000 13.379274 -0.000000 -0.125331 0.137295 -0.616521 0.028222 10.991364 0.020885 -0.009535 -0.151453 -0.043419 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.207596 3.701530 5.308808 0.026064 -0.014399 0.997431 0.000000 13.074334 0.000000 -0.021910 -0.084388 -0.040178 -0.035381 14.148866 0.000001 -0.000520 0.010292 0.507219 0.000000 4.097309 -0.000000 -0.000080 -0.001006 0.251388 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.163653 0.031868 0.018805 0.997390 -0.000000 13.074365 0.000000 0.023657 -0.002827 -0.046838 -0.035380 14.148879 0.012529 0.000059 -0.008078 0.503676 -0.000000 4.097290 0.000000 0.000080 0.000151 0.259488 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 122 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.822673 5.066902 10.522214 0.019057 -0.125224 -0.836334 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.022935 -0.088831 -0.124396 0.000000 7.977538 -0.000000 0.006425 0.043625 0.040275 -0.000001 13.487594 -0.000000 -0.034020 -0.088435 -0.357834 0.000000 4.943967 0.000000 -0.003346 -0.003375 -0.405550 -1.546509 11.498225 -7.598077 0.879686 -0.215607 0.096588 0.000000 12.910863 -0.000000 0.274625 0.052784 -0.416174 -0.031254 11.193578 0.010376 -0.238134 -0.011318 -0.063131 0.054595 0.018457 -0.015135 0.373521 -0.062884 -0.198322 0.000000 3.968525 0.000000 0.007698 0.100878 0.419458 0.000000 5.348644 -0.000000 0.635258 0.077628 0.280478 0.000000 2.438549 0.000000 0.322758 0.208193 -0.218217 -0.655277 1.265097 -0.625148 -0.257293 -0.945746 0.061238 1.070808 11.357488 7.885858 -0.968401 -0.193321 -0.043705 0.000000 13.379274 -0.000000 -0.125820 0.137587 -0.617383 0.028222 10.991364 0.020885 -0.007912 -0.141406 -0.050776 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.208502 3.702300 5.308491 0.026066 -0.014383 0.997418 0.000000 13.074334 0.000000 -0.021912 -0.084394 -0.039807 -0.035381 14.148866 0.000001 -0.000504 0.010299 0.507088 0.000000 4.097309 -0.000000 -0.000080 -0.001006 0.251388 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.164062 0.031805 0.018819 0.997380 -0.000000 13.074365 0.000000 0.023692 -0.002766 -0.046467 -0.035380 14.148879 0.012529 0.000060 -0.008077 0.503699 -0.000000 4.097290 0.000000 0.000074 0.000140 0.259239 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 123 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.792858 5.130040 10.459224 0.019253 -0.125371 -0.837172 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.021685 -0.077791 -0.125395 0.000000 7.977538 -0.000000 0.006238 0.043648 0.039895 -0.000001 13.487594 -0.000000 -0.033904 -0.088261 -0.357466 0.000000 4.943967 0.000000 -0.003255 -0.003282 -0.405494 -1.552844 11.465359 -7.596747 0.889772 -0.203221 0.089013 0.000000 12.910863 -0.000000 0.276807 0.048470 -0.399800 -0.031254 11.193578 0.010376 -0.229016 -0.010746 -0.060090 0.054595 0.018457 -0.015135 0.363676 -0.062947 -0.205837 0.000000 3.968525 0.000000 0.009373 0.098516 0.416201 0.000000 5.348644 -0.000000 0.622228 0.076111 0.267604 0.000000 2.438549 0.000000 0.334367 0.202946 -0.212165 -0.655277 1.265097 -0.625148 -0.264153 -0.944634 0.042993 1.065870 11.331868 7.886895 -0.966979 -0.199202 -0.041337 0.000000 13.379274 -0.000000 -0.126951 0.138130 -0.618017 0.028222 10.991364 0.020885 -0.006450 -0.131128 -0.058452 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.209997 3.702967 5.307287 0.026068 -0.014347 0.997407 0.000000 13.074334 0.000000 -0.021891 -0.084402 -0.039511 -0.035381 14.148866 0.000001 -0.000489 0.010307 0.506953 0.000000 4.097309 -0.000000 -0.000081 -0.001009 0.251421 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.165217 0.031634 0.018850 0.997372 -0.000000 13.074365 0.000000 0.023752 -0.002598 -0.046087 -0.035380 14.148879 0.012529 0.000062 -0.008077 0.503723 -0.000000 4.097290 0.000000 0.000069 0.000129 0.258988 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 124 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.762058 5.194015 10.393614 0.019447 -0.125619 -0.838030 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.020460 -0.066869 -0.126389 0.000000 7.977538 -0.000000 0.005966 0.043680 0.039469 -0.000001 13.487594 -0.000000 -0.033751 -0.088007 -0.357100 0.000000 4.943967 0.000000 -0.003125 -0.003151 -0.405415 -1.559174 11.432514 -7.595419 0.899411 -0.190729 0.081379 0.000000 12.910863 -0.000000 0.278943 0.044034 -0.382880 -0.031254 11.193578 0.010376 -0.219675 -0.010161 -0.056977 0.054595 0.018457 -0.015135 0.353551 -0.063000 -0.213487 0.000000 3.968525 0.000000 0.011083 0.096101 0.412866 0.000000 5.348644 -0.000000 0.608460 0.074505 0.254220 0.000000 2.438549 0.000000 0.346172 0.197537 -0.205931 -0.655277 1.265097 -0.625148 -0.275115 -0.941437 0.020971 1.060826 11.305700 7.887953 -0.965493 -0.204988 -0.039122 0.000000 13.379274 -0.000000 -0.128652 0.138896 -0.618447 0.028222 10.991364 0.020885 -0.005128 -0.120788 -0.066434 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.212014 3.703544 5.305296 0.026069 -0.014293 0.997398 0.000000 13.074334 0.000000 -0.021850 -0.084411 -0.039280 -0.035381 14.148866 0.000001 -0.000473 0.010314 0.506816 0.000000 4.097309 -0.000000 -0.000083 -0.001015 0.251483 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.167036 0.031366 0.018897 0.997367 -0.000000 13.074365 0.000000 0.023834 -0.002337 -0.045700 -0.035380 14.148879 0.012529 0.000063 -0.008076 0.503747 -0.000000 4.097290 0.000000 0.000063 0.000118 0.258737 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 125 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.730391 5.258728 10.325701 0.019637 -0.125954 -0.838904 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.019264 -0.056121 -0.127371 0.000000 7.977538 -0.000000 0.005620 0.043720 0.039002 -0.000001 13.487594 -0.000000 -0.033567 -0.087683 -0.356738 0.000000 4.943967 0.000000 -0.002962 -0.002986 -0.405315 -1.565462 11.399883 -7.594100 0.908536 -0.178225 0.073744 0.000000 12.910863 -0.000000 0.281020 0.039488 -0.365464 -0.031254 11.193578 0.010376 -0.210135 -0.009564 -0.053798 0.054595 0.018457 -0.015135 0.343171 -0.063044 -0.221249 0.000000 3.968525 0.000000 0.012825 0.093640 0.409459 0.000000 5.348644 -0.000000 0.593982 0.072815 0.240369 0.000000 2.438549 0.000000 0.358132 0.191979 -0.199529 -0.655277 1.265097 -0.625148 -0.288457 -0.936413 -0.004299 1.055690 11.279049 7.889031 -0.963953 -0.210669 -0.037105 0.000000 13.379274 -0.000000 -0.130857 0.139858 -0.618697 0.028222 10.991364 0.020885 -0.003925 -0.110554 -0.074713 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.214491 3.704042 5.302613 0.026069 -0.014224 0.997391 0.000000 13.074334 0.000000 -0.021791 -0.084423 -0.039108 -0.035381 14.148866 0.000001 -0.000456 0.010322 0.506677 0.000000 4.097309 -0.000000 -0.000085 -0.001024 0.251571 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.169438 0.031014 0.018957 0.997364 -0.000000 13.074365 0.000000 0.023937 -0.001993 -0.045305 -0.035380 14.148879 0.012529 0.000065 -0.008076 0.503771 -0.000000 4.097290 0.000000 0.000057 0.000107 0.258488 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 126 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.697972 5.324081 10.255789 0.019825 -0.126366 -0.839792 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.018102 -0.045601 -0.128334 0.000000 7.977538 -0.000000 0.005208 0.043767 0.038499 -0.000001 13.487594 -0.000000 -0.033357 -0.087299 -0.356380 0.000000 4.943967 0.000000 -0.002771 -0.002792 -0.405199 -1.571673 11.367659 -7.592796 0.917094 -0.165802 0.066167 0.000000 12.910863 -0.000000 0.283025 0.034847 -0.347603 -0.031254 11.193578 0.010376 -0.200419 -0.008956 -0.050564 0.054595 0.018457 -0.015135 0.332561 -0.063078 -0.229097 0.000000 3.968525 0.000000 0.014593 0.091138 0.405990 0.000000 5.348644 -0.000000 0.578831 0.071043 0.226099 0.000000 2.438549 0.000000 0.370209 0.186286 -0.192978 -0.655277 1.265097 -0.625148 -0.301936 -0.930149 -0.031432 1.050473 11.251981 7.890126 -0.962370 -0.216231 -0.035336 0.000000 13.379274 -0.000000 -0.133498 0.140990 -0.618788 0.028222 10.991364 0.020885 -0.002822 -0.100593 -0.083280 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.217368 3.704471 5.299329 0.026068 -0.014142 0.997386 0.000000 13.074334 0.000000 -0.021716 -0.084436 -0.038986 -0.035381 14.148866 0.000001 -0.000440 0.010330 0.506535 0.000000 4.097309 -0.000000 -0.000089 -0.001035 0.251682 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.172346 0.030588 0.019030 0.997362 -0.000000 13.074365 0.000000 0.024057 -0.001577 -0.044905 -0.035380 14.148879 0.012529 0.000066 -0.008076 0.503796 -0.000000 4.097290 0.000000 0.000051 0.000096 0.258241 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 127 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.664913 5.389977 10.184178 0.020011 -0.126843 -0.840691 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.016980 -0.035366 -0.129273 0.000000 7.977538 -0.000000 0.004741 0.043820 0.037966 -0.000001 13.487594 -0.000000 -0.033126 -0.086867 -0.356030 0.000000 4.943967 0.000000 -0.002557 -0.002575 -0.405068 -1.577768 11.336034 -7.591518 0.925044 -0.153558 0.058706 0.000000 12.910863 -0.000000 0.284945 0.030127 -0.329353 -0.031254 11.193578 0.010376 -0.190553 -0.008339 -0.047280 0.054595 0.018457 -0.015135 0.321749 -0.063101 -0.237008 0.000000 3.968525 0.000000 0.016383 0.088602 0.402466 0.000000 5.348644 -0.000000 0.563052 0.069196 0.211462 0.000000 2.438549 0.000000 0.382362 0.180474 -0.186294 -0.655277 1.265097 -0.625148 -0.313639 -0.923352 -0.059305 1.045188 11.224557 7.891235 -0.960754 -0.221662 -0.033863 0.000000 13.379274 -0.000000 -0.136513 0.142268 -0.618740 0.028222 10.991364 0.020885 -0.001802 -0.091075 -0.092132 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.220583 3.704841 5.295534 0.026067 -0.014048 0.997382 0.000000 13.074334 0.000000 -0.021628 -0.084450 -0.038907 -0.035381 14.148866 0.000001 -0.000423 0.010338 0.506392 0.000000 4.097309 -0.000000 -0.000093 -0.001048 0.251810 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.175684 0.030100 0.019112 0.997361 -0.000000 13.074365 0.000000 0.024192 -0.001100 -0.044499 -0.035380 14.148879 0.012529 0.000068 -0.008075 0.503821 -0.000000 4.097290 0.000000 0.000045 0.000085 0.257998 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 128 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.631324 5.456324 10.111159 0.020195 -0.127373 -0.841598 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.015902 -0.025474 -0.130182 0.000000 7.977538 -0.000000 0.004229 0.043877 0.037408 -0.000001 13.487594 -0.000000 -0.032879 -0.086396 -0.355690 0.000000 4.943967 0.000000 -0.002324 -0.002340 -0.404926 -1.583709 11.305205 -7.590271 0.932359 -0.141587 0.051420 0.000000 12.910863 -0.000000 0.286771 0.025344 -0.310770 -0.031254 11.193578 0.010376 -0.180561 -0.007714 -0.043957 0.054595 0.018457 -0.015135 0.310763 -0.063112 -0.244959 0.000000 3.968525 0.000000 0.018193 0.086036 0.398893 0.000000 5.348644 -0.000000 0.546696 0.067279 0.196515 0.000000 2.438549 0.000000 0.394555 0.174558 -0.179495 -0.655277 1.265097 -0.625148 -0.321593 -0.916996 -0.086843 1.039846 11.196838 7.892356 -0.959119 -0.226947 -0.032741 0.000000 13.379274 -0.000000 -0.139837 0.143666 -0.618573 0.028222 10.991364 0.020885 -0.000848 -0.082174 -0.101268 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.224079 3.705163 5.291316 0.026065 -0.013945 0.997379 0.000000 13.074334 0.000000 -0.021529 -0.084465 -0.038864 -0.035381 14.148866 0.000001 -0.000406 0.010346 0.506247 0.000000 4.097309 -0.000000 -0.000098 -0.001063 0.251953 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.179378 0.029561 0.019202 0.997362 -0.000000 13.074365 0.000000 0.024339 -0.000572 -0.044090 -0.035380 14.148879 0.012529 0.000070 -0.008075 0.503846 -0.000000 4.097290 0.000000 0.000040 0.000075 0.257762 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 129 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.597316 5.523028 10.037025 0.020377 -0.127946 -0.842511 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.014874 -0.015981 -0.131054 0.000000 7.977538 -0.000000 0.003680 0.043939 0.036830 -0.000001 13.487594 -0.000000 -0.032619 -0.085896 -0.355361 0.000000 4.943967 0.000000 -0.002078 -0.002091 -0.404776 -1.589459 11.275372 -7.589064 0.939024 -0.129986 0.044367 0.000000 12.910863 -0.000000 0.288491 0.020514 -0.291915 -0.031254 11.193578 0.010376 -0.170466 -0.007084 -0.040602 0.054595 0.018457 -0.015135 0.299630 -0.063111 -0.252927 0.000000 3.968525 0.000000 0.020016 0.083447 0.395281 0.000000 5.348644 -0.000000 0.529824 0.065299 0.181320 0.000000 2.438549 0.000000 0.406748 0.168552 -0.172600 -0.655277 1.265097 -0.625148 -0.323323 -0.912485 -0.112840 1.034459 11.168886 7.893487 -0.957478 -0.232071 -0.032027 0.000000 13.379274 -0.000000 -0.143410 0.145159 -0.618309 0.028222 10.991364 0.020885 0.000056 -0.074078 -0.110689 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.227798 3.705446 5.286762 0.026063 -0.013835 0.997376 0.000000 13.074334 0.000000 -0.021422 -0.084481 -0.038848 -0.035381 14.148866 0.000001 -0.000389 0.010354 0.506102 0.000000 4.097309 -0.000000 -0.000103 -0.001078 0.252108 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.183356 0.028980 0.019300 0.997363 -0.000000 13.074365 0.000000 0.024495 -0.000004 -0.043676 -0.035380 14.148879 0.012529 0.000071 -0.008074 0.503872 -0.000000 4.097290 0.000000 0.000035 0.000065 0.257532 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 130 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.562997 5.589996 9.962065 0.020559 -0.128549 -0.843428 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.013901 -0.006948 -0.131883 0.000000 7.977538 -0.000000 0.003104 0.044003 0.036237 -0.000001 13.487594 -0.000000 -0.032353 -0.085377 -0.355045 0.000000 4.943967 0.000000 -0.001824 -0.001834 -0.404621 -1.594977 11.246740 -7.587906 0.945033 -0.118850 0.037604 0.000000 12.910863 -0.000000 0.290099 0.015653 -0.272852 -0.031254 11.193578 0.010376 -0.160294 -0.006449 -0.037223 0.054595 0.018457 -0.015135 0.288381 -0.063098 -0.260890 0.000000 3.968525 0.000000 0.021851 0.080840 0.391637 0.000000 5.348644 -0.000000 0.512505 0.063264 0.165941 0.000000 2.438549 0.000000 0.418906 0.162475 -0.165626 -0.655277 1.265097 -0.625148 -0.315971 -0.911223 -0.136955 1.029039 11.140761 7.894624 -0.955847 -0.237019 -0.031784 0.000000 13.379274 -0.000000 -0.147166 0.146722 -0.617969 0.028222 10.991364 0.020885 0.000928 -0.066988 -0.120402 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.231682 3.705701 5.281960 0.026061 -0.013719 0.997375 0.000000 13.074334 0.000000 -0.021308 -0.084497 -0.038854 -0.035381 14.148866 0.000001 -0.000372 0.010362 0.505955 0.000000 4.097309 -0.000000 -0.000108 -0.001095 0.252270 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.187542 0.028369 0.019402 0.997364 -0.000000 13.074365 0.000000 0.024659 0.000593 -0.043261 -0.035380 14.148879 0.012529 0.000073 -0.008074 0.503897 -0.000000 4.097290 0.000000 0.000029 0.000055 0.257312 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 131 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.528478 5.657134 9.886572 0.020739 -0.129173 -0.844346 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.012989 0.001566 -0.132665 0.000000 7.977538 -0.000000 0.002512 0.044069 0.035635 -0.000001 13.487594 -0.000000 -0.032084 -0.084851 -0.354746 0.000000 4.943967 0.000000 -0.001567 -0.001573 -0.404463 -1.600222 11.219523 -7.586806 0.950393 -0.108275 0.031188 0.000000 12.910863 -0.000000 0.291587 0.010777 -0.253644 -0.031254 11.193578 0.010376 -0.150071 -0.005811 -0.033828 0.054595 0.018457 -0.015135 0.277044 -0.063073 -0.268825 0.000000 3.968525 0.000000 0.023691 0.078221 0.387968 0.000000 5.348644 -0.000000 0.494814 0.061184 0.150445 0.000000 2.438549 0.000000 0.430991 0.156343 -0.158595 -0.655277 1.265097 -0.625148 -0.301914 -0.912093 -0.159880 1.023597 11.112525 7.895766 -0.954242 -0.241771 -0.032082 0.000000 13.379274 -0.000000 -0.151043 0.148331 -0.617574 0.028222 10.991364 0.020885 0.001786 -0.061126 -0.130417 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.235672 3.705937 5.276999 0.026059 -0.013599 0.997374 0.000000 13.074334 0.000000 -0.021190 -0.084515 -0.038873 -0.035381 14.148866 0.000001 -0.000355 0.010370 0.505808 0.000000 4.097309 -0.000000 -0.000113 -0.001111 0.252436 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.191863 0.027738 0.019507 0.997366 -0.000000 13.074365 0.000000 0.024827 0.001210 -0.042844 -0.035380 14.148879 0.012529 0.000074 -0.008073 0.503923 -0.000000 4.097290 0.000000 0.000025 0.000046 0.257103 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 132 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.493870 5.724348 9.810845 0.020920 -0.129805 -0.845262 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.012144 0.009497 -0.133394 0.000000 7.977538 -0.000000 0.001911 0.044135 0.035028 -0.000001 13.487594 -0.000000 -0.031819 -0.084326 -0.354464 0.000000 4.943967 0.000000 -0.001311 -0.001314 -0.404307 -1.605152 11.193945 -7.585771 0.955116 -0.098355 0.025177 0.000000 12.910863 -0.000000 0.292950 0.005905 -0.234359 -0.031254 11.193578 0.010376 -0.139821 -0.005172 -0.030427 0.054595 0.018457 -0.015135 0.265652 -0.063036 -0.276709 0.000000 3.968525 0.000000 0.025534 0.075596 0.384284 0.000000 5.348644 -0.000000 0.476829 0.059066 0.134902 0.000000 2.438549 0.000000 0.442969 0.150174 -0.151527 -0.655277 1.265097 -0.625148 -0.284488 -0.913731 -0.181557 1.018145 11.084239 7.896910 -0.952679 -0.246307 -0.032995 0.000000 13.379274 -0.000000 -0.154976 0.149960 -0.617147 0.028222 10.991364 0.020885 0.002648 -0.056741 -0.140747 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.239709 3.706165 5.271966 0.026056 -0.013478 0.997373 0.000000 13.074334 0.000000 -0.021070 -0.084532 -0.038898 -0.035381 14.148866 0.000001 -0.000338 0.010378 0.505661 0.000000 4.097309 -0.000000 -0.000119 -0.001128 0.252603 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.196243 0.027098 0.019613 0.997367 -0.000000 13.074365 0.000000 0.024997 0.001835 -0.042426 -0.035380 14.148879 0.012529 0.000076 -0.008073 0.503949 -0.000000 4.097290 0.000000 0.000020 0.000037 0.256906 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 133 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.459290 5.791538 9.735190 0.021100 -0.130433 -0.846174 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.011371 0.016783 -0.134063 0.000000 7.977538 -0.000000 0.001313 0.044202 0.034423 -0.000001 13.487594 -0.000000 -0.031562 -0.083815 -0.354204 0.000000 4.943967 0.000000 -0.001061 -0.001061 -0.404154 -1.609720 11.170243 -7.584813 0.959222 -0.089187 0.019626 0.000000 12.910863 -0.000000 0.294185 0.001053 -0.215066 -0.031254 11.193578 0.010376 -0.129572 -0.004534 -0.027028 0.054595 0.018457 -0.015135 0.254236 -0.062987 -0.284521 0.000000 3.968525 0.000000 0.027374 0.072971 0.380592 0.000000 5.348644 -0.000000 0.458638 0.056922 0.119381 0.000000 2.438549 0.000000 0.454803 0.143987 -0.144442 -0.655277 1.265097 -0.625148 -0.266404 -0.915186 -0.201811 1.012697 11.055969 7.898054 -0.951178 -0.250604 -0.034613 0.000000 13.379274 -0.000000 -0.158896 0.151583 -0.616715 0.028222 10.991364 0.020885 0.003534 -0.054124 -0.151414 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.243731 3.706396 5.266956 0.026054 -0.013358 0.997372 0.000000 13.074334 0.000000 -0.020950 -0.084549 -0.038921 -0.035381 14.148866 0.000001 -0.000320 0.010387 0.505513 0.000000 4.097309 -0.000000 -0.000124 -0.001145 0.252768 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.200605 0.026461 0.019719 0.997367 -0.000000 13.074365 0.000000 0.025167 0.002457 -0.042008 -0.035380 14.148879 0.012529 0.000078 -0.008073 0.503975 -0.000000 4.097290 0.000000 0.000016 0.000029 0.256723 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 134 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.424855 5.858604 9.659923 0.021281 -0.131047 -0.847080 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.010677 0.023355 -0.134667 0.000000 7.977538 -0.000000 0.000727 0.044267 0.033824 -0.000001 13.487594 -0.000000 -0.031318 -0.083328 -0.353967 0.000000 4.943967 0.000000 -0.000824 -0.000821 -0.404009 -1.613878 11.148668 -7.583940 0.962734 -0.080868 0.014593 0.000000 12.910863 -0.000000 0.295290 -0.003762 -0.195835 -0.031254 11.193578 0.010376 -0.119351 -0.003897 -0.023640 0.054595 0.018457 -0.015135 0.242829 -0.062926 -0.292239 0.000000 3.968525 0.000000 0.029208 0.070353 0.376902 0.000000 5.348644 -0.000000 0.440328 0.054762 0.103953 0.000000 2.438549 0.000000 0.466461 0.137800 -0.137363 -0.655277 1.265097 -0.625148 -0.250618 -0.915681 -0.220342 1.007264 11.027780 7.899194 -0.949756 -0.254633 -0.037033 0.000000 13.379274 -0.000000 -0.162734 0.153173 -0.616302 0.028222 10.991364 0.020885 0.004469 -0.053614 -0.162443 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.247676 3.706640 5.262062 0.026051 -0.013240 0.997371 0.000000 13.074334 0.000000 -0.020834 -0.084566 -0.038934 -0.035381 14.148866 0.000001 -0.000303 0.010395 0.505366 0.000000 4.097309 -0.000000 -0.000129 -0.001160 0.252925 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.204869 0.025839 0.019823 0.997367 -0.000000 13.074365 0.000000 0.025333 0.003066 -0.041591 -0.035380 14.148879 0.012529 0.000079 -0.008072 0.504001 -0.000000 4.097290 0.000000 0.000012 0.000022 0.256557 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 135 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.390692 5.925440 9.585377 0.021462 -0.131632 -0.847977 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.010068 0.029144 -0.135199 0.000000 7.977538 -0.000000 0.000165 0.044329 0.033239 -0.000001 13.487594 -0.000000 -0.031093 -0.082877 -0.353756 0.000000 4.943967 0.000000 -0.000605 -0.000599 -0.403875 -1.617575 11.129489 -7.583165 0.965676 -0.073498 0.010137 0.000000 12.910863 -0.000000 0.296263 -0.008523 -0.176737 -0.031254 11.193578 0.010376 -0.109187 -0.003265 -0.020273 0.054595 0.018457 -0.015135 0.231467 -0.062854 -0.299841 0.000000 3.968525 0.000000 0.031031 0.067747 0.373223 0.000000 5.348644 -0.000000 0.421991 0.052597 0.088687 0.000000 2.438549 0.000000 0.477907 0.131633 -0.130312 -0.655277 1.265097 -0.625148 -0.238410 -0.915167 -0.236804 1.001860 10.999742 7.900328 -0.948490 -0.258005 -0.041300 0.000000 13.379274 -0.000000 -0.166417 0.154701 -0.615938 0.028222 10.991364 0.020885 0.005400 -0.057169 -0.174400 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.251477 3.706909 5.257384 0.026049 -0.013126 0.997369 0.000000 13.074334 0.000000 -0.020723 -0.084582 -0.038929 -0.035381 14.148866 0.000001 -0.000286 0.010403 0.505220 0.000000 4.097309 -0.000000 -0.000134 -0.001175 0.253071 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.208950 0.025242 0.019923 0.997366 -0.000000 13.074365 0.000000 0.025493 0.003649 -0.041177 -0.035380 14.148879 0.012529 0.000081 -0.008072 0.504026 -0.000000 4.097290 0.000000 0.000008 0.000015 0.256409 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 136 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.356932 5.991933 9.511900 0.021645 -0.132176 -0.848862 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.009552 0.034074 -0.135654 0.000000 7.977538 -0.000000 -0.000362 0.044388 0.032672 -0.000001 13.487594 -0.000000 -0.030893 -0.082475 -0.353575 0.000000 4.943967 0.000000 -0.000409 -0.000402 -0.403755 -1.620753 11.112996 -7.582498 0.968072 -0.067181 0.006321 0.000000 12.910863 -0.000000 0.297106 -0.013211 -0.157845 -0.031254 11.193578 0.010376 -0.099109 -0.002639 -0.016936 0.054595 0.018457 -0.015135 0.220186 -0.062771 -0.307306 0.000000 3.968525 0.000000 0.032838 0.065161 0.369564 0.000000 5.348644 -0.000000 0.403720 0.050438 0.073650 0.000000 2.438549 0.000000 0.489109 0.125508 -0.123312 -0.655277 1.265097 -0.625148 -0.227953 -0.914249 -0.251685 0.996500 10.971927 7.901453 -0.947366 -0.260611 -0.047661 0.000000 13.379274 -0.000000 -0.169867 0.156137 -0.615654 0.028222 10.991364 0.020885 0.006276 -0.064997 -0.187430 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.255065 3.707215 5.253029 0.026048 -0.013020 0.997366 0.000000 13.074334 0.000000 -0.020621 -0.084598 -0.038897 -0.035381 14.148866 0.000001 -0.000269 0.010411 0.505075 0.000000 4.097309 -0.000000 -0.000138 -0.001188 0.253202 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.212761 0.024685 0.020016 0.997364 -0.000000 13.074365 0.000000 0.025644 0.004193 -0.040765 -0.035380 14.148879 0.012529 0.000083 -0.008071 0.504052 -0.000000 4.097290 0.000000 0.000005 0.000010 0.256282 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 137 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.323714 6.057964 9.439868 0.021829 -0.132664 -0.849733 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.009136 0.038068 -0.136022 0.000000 7.977538 -0.000000 -0.000843 0.044442 0.032131 -0.000001 13.487594 -0.000000 -0.030725 -0.082135 -0.353427 0.000000 4.943967 0.000000 -0.000244 -0.000234 -0.403654 -1.623353 11.099506 -7.581952 0.969939 -0.062032 0.003211 0.000000 12.910863 -0.000000 0.297822 -0.017810 -0.139232 -0.031254 11.193578 0.010376 -0.089148 -0.002020 -0.013640 0.054595 0.018457 -0.015135 0.209023 -0.062678 -0.314611 0.000000 3.968525 0.000000 0.034623 0.062603 0.365937 0.000000 5.348644 -0.000000 0.385609 0.048296 0.058911 0.000000 2.438549 0.000000 0.500034 0.119445 -0.116389 -0.655277 1.265097 -0.625148 -0.218577 -0.913037 -0.265561 0.991198 10.944415 7.902566 -0.946292 -0.262732 -0.055316 0.000000 13.379274 -0.000000 -0.173001 0.157450 -0.615483 0.028222 10.991364 0.020885 0.007116 -0.075447 -0.201079 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.258364 3.707570 5.249107 0.026046 -0.012924 0.997362 0.000000 13.074334 0.000000 -0.020530 -0.084612 -0.038828 -0.035381 14.148866 0.000001 -0.000252 0.010419 0.504931 0.000000 4.097309 -0.000000 -0.000142 -0.001200 0.253313 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.216206 0.024181 0.020101 0.997360 -0.000000 13.074365 0.000000 0.025782 0.004686 -0.040358 -0.035380 14.148879 0.012529 0.000084 -0.008071 0.504077 -0.000000 4.097290 0.000000 0.000003 0.000005 0.256177 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 138 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.291191 6.123405 9.369680 0.022016 -0.133081 -0.850588 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008827 0.041040 -0.136298 0.000000 7.977538 -0.000000 -0.001263 0.044490 0.031623 -0.000001 13.487594 -0.000000 -0.030594 -0.081871 -0.353316 0.000000 4.943967 0.000000 -0.000117 -0.000105 -0.403576 -1.625307 11.089368 -7.581542 0.971289 -0.058172 0.000880 0.000000 12.910863 -0.000000 0.298414 -0.022303 -0.120973 -0.031254 11.193578 0.010376 -0.079338 -0.001411 -0.010395 0.054595 0.018457 -0.015135 0.198018 -0.062575 -0.321735 0.000000 3.968525 0.000000 0.036383 0.060079 0.362353 0.000000 5.348644 -0.000000 0.367754 0.046183 0.044534 0.000000 2.438549 0.000000 0.510650 0.113467 -0.109567 -0.655277 1.265097 -0.625148 -0.209776 -0.911585 -0.278848 0.985970 10.917289 7.903663 -0.945204 -0.264581 -0.063665 0.000000 13.379274 -0.000000 -0.175730 0.158606 -0.615463 0.028222 10.991364 0.020885 0.007935 -0.087278 -0.215003 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.261295 3.707989 5.245740 0.026046 -0.012840 0.997356 0.000000 13.074334 0.000000 -0.020453 -0.084625 -0.038713 -0.035381 14.148866 0.000001 -0.000236 0.010427 0.504790 0.000000 4.097309 -0.000000 -0.000145 -0.001208 0.253399 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.219183 0.023745 0.020175 0.997355 -0.000000 13.074365 0.000000 0.025904 0.005112 -0.039956 -0.035380 14.148879 0.012529 0.000086 -0.008071 0.504102 -0.000000 4.097290 0.000000 0.000001 0.000001 0.256099 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } frame 139 { -0.000000 0.016430 -0.006044 -0.707107 -0.000242 -0.707107 31.259527 6.188116 9.301774 0.022206 -0.133410 -0.851422 0.003848 -11.026810 0.100900 -0.001203 0.000819 -0.001677 26.002844 -2.030195 0.014076 -0.000000 0.000000 0.750740 0.000000 4.130581 0.000000 0.008635 0.042898 -0.136471 0.000000 7.977538 -0.000000 -0.001610 0.044529 0.031156 -0.000001 13.487594 -0.000000 -0.030510 -0.081700 -0.353246 0.000000 4.943967 0.000000 -0.000034 -0.000021 -0.403525 -1.626541 11.082965 -7.581283 0.972117 -0.055739 -0.000589 0.000000 12.910863 -0.000000 0.298888 -0.026672 -0.103144 -0.031254 11.193578 0.010376 -0.069713 -0.000813 -0.007213 0.054595 0.018457 -0.015135 0.187213 -0.062465 -0.328654 0.000000 3.968525 0.000000 0.038110 0.057600 0.358824 0.000000 5.348644 -0.000000 0.350247 0.044109 0.030580 0.000000 2.438549 0.000000 0.520923 0.107598 -0.102874 -0.655277 1.265097 -0.625148 -0.201093 -0.909909 -0.291906 0.980834 10.890640 7.904741 -0.944056 -0.266352 -0.072166 0.000000 13.379274 -0.000000 -0.177956 0.159566 -0.615633 0.028222 10.991364 0.020885 0.008744 -0.099371 -0.228898 0.000004 -0.125166 -0.066536 -0.332332 0.124874 -0.331347 -0.000000 4.224101 0.000000 0.226414 -0.016245 0.268104 -0.000000 5.160579 -0.000000 -0.363836 -0.004691 -0.037130 0.000000 2.493865 -0.000000 -0.619859 0.137444 0.141287 -3.263771 3.708487 5.243059 0.026046 -0.012771 0.997349 0.000000 13.074334 0.000000 -0.020394 -0.084636 -0.038541 -0.035381 14.148866 0.000001 -0.000220 0.010434 0.504650 0.000000 4.097309 -0.000000 -0.000146 -0.001214 0.253455 -0.000000 3.425298 -0.000000 0.004317 -0.002866 0.069741 -3.261971 3.695171 -5.221583 0.023393 0.020235 0.997348 -0.000000 13.074365 0.000000 0.026007 0.005456 -0.039561 -0.035380 14.148879 0.012529 0.000087 -0.008070 0.504126 -0.000000 4.097290 0.000000 -0.000000 -0.000001 0.256050 -0.000000 3.425305 -0.000000 -0.005016 0.002226 0.069715 } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/testmodel.pack/Marine_green.pcx0000664000175000017500000017500012641367670031710 0ustar jaakkojaakko ._y~:__x..x xl«s .NVV.´..OOs_x.´x x.C=;´ xs _sҫ_x.x . _lNV=.x_l.xOlx.x_..OĴ.. =b;٦CV;Ĵ´ s«55_ ._5O5s . .xô.N.b =ٴ =;.´..´.x llO5x. _sOs_. _x.5Osx.;V.٦b bbAD=...ô. x_OOl._s5Ol._x.xOlxx x_x . ..V==;»V=b C.bD=.N´;..xs5ll5Oӫll5sx._sOOl_x._ sOO__l«l_x..V=b=;.V .= ;.VC...;;. sl5OOl_ _OOs_ . x_. sӭ5s__l5lls_ .V==.٦‰D=.=* C..VCvV.VCVٴsӭӃOO5Oll. __lO_. _.xOll55Ol55O5l_x.NV=b;.;= =. ´»VCV.N.VV;.lӭlO55 _O_x. _ x_lҭls_lOOO55 .»٦=V.vb .C=..C=.VVٴ._lO5ӃOOl__sOOOx.xs _sOӭ__lOlҫ5l5x.V==Vôb b.N...V=V;¦=V;´._5ll5ܭ5Ӄ5_ x_sOӤ_s ._Ol_«lOOӭ5lOҫ5l .;VŦV..CV.ô´;V¦VV¦b=V;.;.;.O«l­OOa_a_.N.x_ls_x._5Ols_5lӭlO5l5O.;VvC¦C.´´.V;.V==´.VV;xܭOO†OҫӭOl4x. OOҫ_x´.xO5_«5O­ҫsô..;V»..Ŵô.;.VCN;V=..5­OOOܫ :x._alӭO_x´._l5_l5«OƒOl..;.N;...N..NV;¦b=. Ӥk†ӭOOsxN A´._ayOOlsxôslOܫlOlOll5s..N´....N..»ٻVvb=ٴ´xslӭӃ­Oҫlx.DD=.Nxayllx ...5Ols_x_slO¤5lOO ........»N.ٻæb=;;.l5Oӭ¤5l5_ (b ADb N x:yOOl_ .._5O5_x_5OOOOll_;٦=.´´.V .;V».CV.Ĵ 5OOҫ5s_ vV V x aylOls_x ´´´´.x_5O55 _ s5OOO҃O .VVb NVVVVV;VV=VN.N.ٻ..;.xOOs_a_ NN.V=D*axxl5OܭOҫlx.´.. x x x x.x x x. _5Ol5. _OOOOĭO5 VC .V=æ=V.VVV *b..VV» O­OOOx_x.CA:.l5_xx..s__5OӭO5ls_x» *DAAC.s5OOӭOO5.ٻ;٦Db´;C=¦=b;.V¦= D vNô.´= =V;5OOl_4n*s(..=Dn:a_. slO­O5lsxxssss5ܭƒҫ_x  ڦx_OOO5sl ..VV==.;æ.V=bDô;´Ĵ.vAD=_OOsanD ».´ADMsx. x5O5sl«l5l5«llOOӭ_ .bNb _sl5lOҫl5_;´.V=¦=b=.¦V.= D.V.´...DAb=V´_sOOOOҫ5laJAC bV ´.D«x l5OslOOOOOlxbDV.CxalOӭOOOOlx..٦=¦=.;¦V.; DvV.´.N.b=sOӭOl b;*DAbV; (;Vb%x5OOl_ _sOOOkíĭOOlss_nD.N(__lOӃOOlsx.Ĵ.ô;Cæ=VVC== .´=b=.´.N»;V bC;xslll5Ols_.=VvbD _4CvlOOOls_l5llOOOOO5llssb»x_xslOOOOӭO5l ..Cæ==VCV=*V.v bVN.;b ´xl5ū55l5O5x.N.bAa:Yy5OOl_s_.lll«l5ll _MA;An_aOOOOlxVV..Ŧ=æ=Ŧb.CV;.;.VC==. 5ҫOӏ/Ol5_xa .xAA~:MO­ҫx x_ ._llsys_xD.´xbں_s5l5OOls ;V==V;.;VvvƦæ=....;..V¦=b. 5l­/OOs__ax.ôCDnM5­ŭO_x..x _ x_sss__ssxsl5zDvx.bD* sllOOOOOls.;b=¦V;..;.ٻVCVVCVV.VVVb .OӤҫl_xssax.´A%lӤOlx .ô x x x. x._x_sl5l~* v.xVbDAba_slOO5lsx.;V=b b=;ٻ...V»V´N٦VC¦bA*=.l5OӤOOs_sa_N.(N A=ƫOӭӤ¤O5l_x x x_  .x x___5lls:y=Jx_AADx_x x5OOOҫ55l5llsx;= =Cٻ;...ٻ.NCVٴVV= A=._lӭO†OOO5sss_N.N_ V%O†ƒOOl5__sl5lllya_aA.x.xl5OOO55 .;V=V».N....»V;.VV=DڵA ._lO¤O55«5s_s:a_.x_ _b_†ӤƤO5ll_5ë5llM~5MlAb´.x_x_ssl5OOOOҫ5lx.٦CV¦V;N.ŴĴ.´´N.V¦ A^>A ssl5OOOҫ_a ´x x x†ņƒOsl5OlOl5OM%._ssӭӭlsx.»VC¦=¦V;..´ôN.;...V¦=bs5ls5OO5l5ls___ .. xslĭ¤äÆäƒӭO5Oë5O܃5ܫ-ډA x.x_ss_5í¤†«l55x..¦=b=C»V..;...N.٦= ^bVsO«ll5lls_x.x._slOӭӆŭ†Ƥ­lOOOOӭl5OҺڦbx . __­OĆOO5_xV¦=V=b=;...N.´.;.´.V=¦=D=v.¤Ol55«_s_x..xlOOƤ†­†ӭOӃOOO5ȎaAbx. sO«OӭäOO5s_l.=C٦= V.ٻ.»V.´;V¦=CVN.s܃Ćҫ5ll55s_x_s_ .. _lOOܭƆ­OOӃ­ӃOlO­OOOOha4:WR .xs_sl5­5s_O.»¦=٦b A V.Vٻ..vV..V=ٻ»..s†O_l5l5 _x.´. lOƒ­kkíOOOӭ¤«Oílys4a_ .x__sslOlsl..¦=¦٦= .VV.ô.V.´.¦=¦VV;..lܭ­Ol_sl5_x. .._ܤÆkӃOO««O¤­OOl5lx´ __sl5l55lllO´٦CV;Cb V.;Vٻ.;.´..´C=.ô. .5ҫs__ll. .xsl†ӭ¤­ܭ­Ol5lӭ­­OO5l5.. _sl555l5ƒl_.».»= Abٴ.VV;....æ=V;. 5s_.lx. x . _5O­܃ƒӃӃllOO5O«55.x_xlë5lOӭäO_ ´.»..= =...;;´.x_s_ »x .x_x ._lOOӃOӃ5ӭ­Ӄ­OOO5555 .x__x.x_l«5lk¤O´.´..»C=C´...;´´.. _s_.__._x .x_lӃ­Ӄҫ5l55llOĭƒOO«OOҫҫ5x. .l_x_O¤O5~_xô.Ĵ.Ĵ´;´a8a#R8R#as . x.x__x._OOĤíOO5555lOӭOO5 .5___­5sx..´.;.ŴR۪H6rSrSms_ ..___x_5OӭӭӃO5lOOӭӃOO5OOOҫ .lls_sOҫls׻.´.´...ô;:IH6900ÞMs_.´.__l5ӃӭƒӤӭҫ5lOOOܭOOOOOx. l_x__ssls_ .;ŴǴǴ.ɴ.ɴ´´..;´´(IH6\9S6fa_s sxxl5lOӭOӃܭӤíӃO5OlOOO­ƒOOOOO. __x.. _s_´CADADAD n n*n*nWf****Dbn ADADAKDN#II6r۪۾r`S:(_a_x.x_x_l5¤OOlO5ƒOOOl_ .xx._s x».´A°ő?Db¦=b b*«___svB)`m77IR(Y#JnfJf"7f"fJ f7˜)SB7K7%"7%"7I7%#(f"%7"zm &v88IIª"._yya_x xVCCbC K*J۾60wwB8f b b bæV= b=bs5ls___ mSm77K7IfRN4(( #R##RaRJR#R#R*#4(»(RfI77r`).#"H۪f2288Iª8f.xV=bb=b b=DK=AI60+XooEXpb AA>^ b¦=bb=b*s~55s_ ___s_x%SB7&7"fa(Rf.(#nf"ڟ%7%7AWn%&7z"7%7"7f4vf(Rf"77p` v(pS6882R(.*b=b=A^' b =٦AJIHEEEXbbDDADbv=b D* =:Ols_x _ v&S&7"##H`R(=D>^'j^''DZj''^YBS7R4Rf"Km"7`&v(vS9`H2IH(.D*bÌ' =AIH+X+u©rpJb *^D b=b DA =b=:y«5ls_ _.4mSm7&mB&7fvf`0SR¼j11±jjjj1Ìjj^J\9f##"pm&7S9mRvCS9S¾HI2I6r9SfV b=b=ü^^^b =bv% =b^%2I60+uX+0ž\H*A =V¦b‰Ab b¦ b_:~lOҫOҫs_.NfS&mdm7"4(4f9&R^''j1³³1ññ1Œ1j-@&#*7mS)`SK7H9`vS6þBHR26r0u0S(.bV¦=^‰µD bCb bbH6rr–r6S`SI: AbD> =Vb DA b=VVb=V x__s~lO5s_ASrmdm&7nRv((#9*'j1󓳌1jıj1Œ1j^f7GR(vKS`m&9`&VvSr6rS6Hª88ĪIHr0u+u7; b b^>Db b= =b >A#IHp¾IpH" D>AbDA b D D b=y_O«l vS`m7%4(4(#p9pA''j1Ó±j1“1j790R(Y(v"S9)7pB)9m=vH6`r6R8IH6r90@+X+0rv AAV=A'A¼ =bA>ADAD 'D%27H7z۟IrS=D^'ּ>^ ^''^>b:a_l5.&S`m7f(Rf#NRm`f%jñ1“{1±1³11`@r##"f#JrKS9`*v(I6r9`rH8:8IH6\0u+Xu+`;bADAVbAv b>j1Œ^'j'-A‰-'1>b>jjj^ * AjjjDb bn .fm)9S&7b((#2p7R(#S0*7'jj±1{111“ó1-J0`R(7RY7`Sm`\.(6r9\96I8#8IHS90@+X+u09 bv=b^‰b=Aej'j։A^€j^'AA 1j^AbA'^>¼^'^b D '^ü'D µ‰n:(pS)`7f4 #f&f90&*%jj1{{1ŒijŒ1Rm9f(BSIR(N*`m&`9mC(N8I`9\96I28#8IHr9\0+X+u9S´===vbA˦v=bd=F>^^>> b=¦A' =b ^> >>  bA^-bD>^Ab bADD‰A VvmS`&f(#fpS)"9BR%j11{Ô{1³³1jD&\"RB9`I#7`m)9S#N.8Ir9\9r8#8IHr90u+@0\S4´=VVæA‰ A* ڦvAF>ډ^>>AbV'^¼=¦٦ b¼D bb¦=b= AA V¦=b=b=V.JS)dSB*C I9`#"9\7#A'j±1{Ĕ11Čų1j#I9#`S7WR)m9..NRIS`908RI6r0+9B#´x D Db==ډAAD ACDj^A^AµAb=b=։ b=bA b DADAb= bD bD DAD =b b b bvm`Smfv(Rn7S#p9 %#A'j±1Ŕ11ȳ1^ Vf9\p47])7f#YISm`\#.N#IHS9\@0H88IH6r90+u@9S&4 D*b=VAAډډAdAV -ĉDA^A=* ֗j b=b‰D=bDAA =VA‰DA= DA.Jr`m`(7)\9747`#A'j11³“Ô{111Œj Jr97Y]9Y(fmBS@Kٴ.4"H`\096۪88H6r`90+u\9fC.b bVA AAԉ= ¦=bD DAD b=bD=VC=b—։=ĦAԼ>%bVbA>A=v=bbٴNI`&`R4f%9\`%v`f4j1Ì{11111>C*r\#4p939m%JfS`S@X.N:690SHR86r9@u0\r7(R4bvADAAֵVAAADb DVCb—^ D b=DA^Ab=bV ڵA¼> b D DCvS9``7#fB\@`f(#79SJRj1Ì“{{111jñ1³>D=9@Yf`3@7*YJS``u).#۾r90\rHI8R۾6r909`#.4x_:4CA%DA V=DAAAڵAڵ‰>D¦=b>A V= bD‰v C=b A첵 V*D DAA R`@9`S9S"43SfY#p9fj1Čē1ıj1Œó1ּbJS9R2)u`"4(4H9`0X9N(#IHS9r8IH6r9\ɞ9Sf#N(#_slya:b =b=bCbDA‰ڵډAAA¦b=b * bnbC=DԼ‰Db*=vAD¼>=b Ab˦.f9\9`9m(N#"3f(RSmJ*j1ij1±j1Œó1^JS&R(JG7R4(fS\`@X\7ٙ´(S90\`I28R86r909`7fR(slO5y v¦=צAAAڵADADV4#((v4RbD* b* ¯= "%b=bb bb nJn*:4C ´C9\\`BfNR'JYf%fRb1Œ³1jj1Č1^=R"fY(e]7((9 \u@&(.#RIr9\`6Ia8RHS6r9SH#´5a~5-*vD‰Aډ%A>ACNaaRyf:*4v*n=44%-ݎ˜4:n%n"n:Mzx.vS9\\7fvN(pS9mf(#R b>րjŒ³ÌŒ1±jj1Ō1j'^=(#4v(f)G9B:(pS]@XuSR.(#RHS\96I28R69`S7I(í5܎yb b b *D%bCx_sl55Ž55ܫl5«5O5lOs #S9\`7fYNB)].vfC='j1Ȍ1jj1Č1ּvCR4.v)]u\f (7S3X X`fV(R#Rr9\S28R8۾6r`SHf.l¤OӃyW a=vCvV¦v4= ba=4(a«O«ӭܭӭOO .JS9\@\`pf#J&`]`&#m#v1Ō11±jjj1³Œ1j¼DC#I&JVv7u0fYNISuZwu\7.(R#IHr9S288I6rHIיO¤OӃO~n%J:a4R:*4v (v(NxslOOņӆkOOOOOӤs .`\\9)JC(f&)39RC4(^'j1Ì111j'1ij1'ff(4B939fv("rZXu\7(Rf:f69`688IH66HI:.lOܭOOO55l:4:a4xOOOO†ӃĆOOҫOks7939SHJ(fS9\m#(v(4'j1Œ1±1j1ó1^^A=(#Yv(4B9@9Sf4("` wu`7N#fI69628#88I6HfNlOӭOOOOӭҫsssOOӤk†Ć¤¤ÆOOӆkx49\9`)7J(4fKmS&fv.fR(Jj11±jjj''1jRNfS99"4NRSuwu9S7Y.7"f96882IH664ҫӭOӭӭOOO5O5lÆĆ††OӃƒӭk R)9`7:(#f7JfR(R7pR#%^j11jj'j1ó1'¼A Y7R(#R*fSr4N#R0wu9`7N#J"f6`rH8IþHY. 5OӭOO_5ҫ5O¤̆¤†܃­kk´vJ`S7N*RJ7f#(RpS#*'j11±'^ü^Œ1'ּ Y"`mR7:fR#N#HXu`H*v;ٻ.f"I6r6۪R2۾6þH"4.ҫOӭO܃OӭO5s5l5OܭĆʆOƐk5sx´#fn`r"R( #fS9"R#fSJv%^1'>AD%ډ&KA>'1Œ1'4R974fS0SJY (NR`9fn"7.N7p7f2r6H82H¾66S7(OӃӃO­ӭҫl5OӭƆ¤ĆO†ĤkÆs Ny)&f*J7IJ2RN79@\"RB9#(J'j1j4RJfW*-^j1j''^#f`Sf#S3wNN#"&7f7`)SN.R%H۾r6H2H66S7 5OOӭ­ӭ¤†k­O«ҫlƆkĆ­O¤Ɇ†ӫ´R"m7J:NR"7S9@`&*7r`f#%'jñ^YY#J"7&7JRJj''R`B#9uXGS7R.N#"fJfSm.NJ7I66ªIH6rSJOOĆkO«O†ņOkƤ†äs (#"mJ*R#.(m`@\S%S9SJ#*>jv pK3`)`f'''ֲ#79S"#Ju@\`7JC.J7)`&fR7m6HI۾r6H۪I2Iľ6Hv.lOÆkkkǐk5ss«Oӭ¤­ÃO܃¤†äƒӃsx#fm.N47pm\3]9&J7`7R*%''^%RWp)\GQt]3P9"'' 4#S`7JR7`]PuS7f(.Jm`9`7J#(.*mSII6H۪IHH۾S7vOO܃†kkkȐkOl_s5ƒӭíӃņĭ܃ܭӫxI&SvN."7m) ]#J&9`*#AR#fHr9\C#7\`74#7)@Χ]9`H%fRN%r9S׻N(N#v&SrI"6IIHzIH4 lO†ÐkkĐk¤­l_x_s_l5l­íӭ¤††ƒ55ӭ5.׻N.4"&7.(RfI7`3\Sv7S`((#"9`S`9uXu@9uZEHRbR 4"r`fB9u}e9`SJ#()9`:R.N (v*7mS`96IH6۪`rI۾Hf(.OO߭ӤÐkkl_x_slӃƒӃ¤¤†­OllO NN.N(4Rz(Rf"7&S ]3]`BJ#7r`"v((RHS`r`S`\Xu0+ 4RJ7S7RR`9PG9`m:Rf"R(N.N(.Yf7mr\S۾Hr9mIHf( ҫOOӤĐkkOsx _sss5ƒ5.(N.N4#NRf7Hm)]G3\)7#(#79`#fp-)`9\@Xww@9S4C(4J7`r7R(Rr93u@`Sp"R4 RNN(..CR-9m7I"fp`)77m`SOOOӭkkÆl_x __s_5ӃO5OܭO5´.(»NNaf"pm)\]]\S"#C4m7R(R"p7K7r`@wPuXZEZu0`SB%(Rpm%#(#f`]XP@09SIRNNN(()-A-&)-%-É- J..lOOOk†kO5_x. x_slOOOO5slO´.a4(N(.N#I7mS`\3@9SpR(R"2pmS`)0@wEX@`SRYfR(7S\uX@\9pfRN™(#>¼^>ü^^Av OO«OܭÆO5s_x_x x_ss5«ҫҫO5llsOOls_.V4R#(4(N.(R"p7)9@]03`&fR ת"BmS`\`)`9`9r9PZ@`& RfYv2)`u`S"R(N(#4##.bDDAA‰‰ډ-b;lOO5OÆO5s __x__sllllssO_.44R#N4I79\]Xwu9mfv(#f7m)`)m)`9`BS9uwX\S7f(fH`\@Xu@)7J#(.(R4 4=b b b bٴOOOO5OܭOO__x_x x_sss5OOl_.(aRN""7)\u^ ..lOOOO5l5O­Os_x_x.xss_slOl N((#(N"7&m`9P@\9#fS-BmS9\9`SSB0@`SR(Rp9u!^>Db=bD¼>bV. OOl5Ol_s__ x ._lsҫO«OҫO܃Ӄlx.´ .N(( (J%R(#RfI9\)SmfRf7SC»CNCYC(Cv=Br9S"RfS¹`\ur7I7R#RIf((N.b A¼>D=VC‰DA^'։=܃55O5ƒO5_s_sx.x__x._ss55OO­...(f7fNRaf7`&Af7B#RJ"pSmfvVN.(Cû(JS\9)IRJm%f%mS`IfJR#׻(m(ô..= A> =V= bD>ր'b; Ol5OOҫOO5s_ x _ . _s_«OOOOӃOl_.(۟#.(4RfSm%R(R7%R7)S7f*4v#4=4(4*f\`SYf7*C#SfJ#S4( (V= A¼b.Vb D^'=V l5lOOӃO5lsx_x. xssl5OOOOӭl.7J .N(R:%mf(vf#f&SmB7&&7&d\`SIJRfK4Jm"fR#.fY( .VbDÉ.´;V^'=V OlO5Oӭ5lë«55ls_x.x ..xslss5OOOƒ5 7f..#fSf#Yf#R"BmmmS` `)SmS&m9\9SJ#&JRmfR׻fm74N ׻٦ Db;.=^։V.lӭ5lO5ӃOO5ls_x.x.x_sss5OOOOӭӃO .(R7JN.#"`7fRIJ8JI7SmS`9`9`SSm\09S7f4f7ImJ4(´NfBmf(.٦b=.´.;.VbVlӭOsO5lӃOOO_x.x_._ssslO«O­ӭO5_ (#p%(N(n9S7%"&*(Rf`S)`)`9@@ɞ\9`S\u9S7RS9mSJ(N.N"#(N;V;V´..VVV= AC. lOlҫlO­ӃO__x. xs_ss5O5ӃlsxN7J((NRS\9SmS&R#Jf`rmB` 3\ `3`9\SIY#\9`&R(.(N"7f#N..VV;.;¦.Vb AD>A 5OOܭӭ­ls. _ ´ x_ssss5OO«OOӭӭl_.RJ4R4("S`p4f`S7*v=v=*C=-&S0@rf49\9)%(#4("..V;CvV;V;.b AÉA>V; 5ҫlO5lO¤¤¤lx _s_ .xsOlOӃӭ5x.4N4fJ#N(R`\9"fIB`7=.N.(.N=&S0`H47 ]9`7R(N4"fR(4(´;;V=.V٦ *A>>> .x55llOӭƆ­Os__ .x_sslOO5OӭO.´.N#(fJ#"m&Jf9`Bfv4#Y(YY4#"m)\@9pRJ)m%R(NHp7"R(4R(.;V=V;CVb*D>^>bV.x5ӃO5l5l5ҫk†5_sx´xs_ssOOOҫOOOlsx´.N(p7mf.N(#J(#fB`&7777pHBS\`pYRJ#׻.m&7fR(.;.;=;V٦ bAډD blOӭOl55ӭkkO5.´. ssssl5OO5OOO .(R"m( .R8#9SB)`9`9`SrSm9\S7((N(f(N.Vb;VƦ= b= bA lҫlllƒk¤s.._s_ssss5OlOO5´.N(7f R# $NR8"m9`H9 `9\9\`9`9S&9`BJR(Y#R#(K4((N.V bVb b=bAb D‰DxO5ll5«Ok¤Ol5_s_xssssO5l5OO«5ls .(N(fRfpfN#`Sm&"%f"%fnf"7)`m`Sp8J(fI"R(7JN(.;bA¦A>A bD>A >Dv 5ܫll5OӃ†Oll__Ĵ.sssslOl«O_.NN#"(#R")7NR"S9&v(C(V(v-mB`SJ#Y7za(f#NN$N.. ¼b=Ab >DbA‰% 5OO55Oӭ†l_s_ . x_s5OOl«Ol5llsx (N(4Rf&BvN"97J(v((f7`SIf2#4m&"4R#Nb>D= >^bADbbDAbC lOҫO5sl««l__.´x_5_slOë5llx..(((RK&R#797%fR#RfnfJ##RJ7SI4(*&dz(a׻.(..v‰Db=A v vbv. «O5lOӃOl__..5l_l«5l5«5s ((׻NN#p(2Im9r7&7&7Bm&7%7SmfR(&%RN..(b D == DbV=.٦.xl5OlӃӭOO5_x. __ë5s_O5l5O´  N a-n(pr9mr9`9`mp7&K7mIfR(f7IR(N. ..CvCVN(vC.Ĵ.´.«Oҫ5l†OOlx.´x_s_sl5_Ol55s_..((NN#R(aam9`r&m`939`p"I7&IfR:*N(($.(Y#R#4fJfR#C 5l5OOl܃†Ol _x .___s5O5__«lӃӃx.( 4(N((Na#7S9`S`9097%"p7fR4N(N(4((7HBHS9S`\97N llOO5Oҫ x__ ô.x_sOO5s_l5ƒx.(NN(Na"7p7"I7m7f%f"I"%#.((4(.4`r6\0`XPu`J. l5Oӭ5sO¤Ӄ ._s__ .´x__slsl55­O5l5..N(#NN.4RfJ*R#JfRf#׻..N( (NRIH6r0\6r@9HR´ 5llOlOӭܫl_ _s__.´x__sO5s«lOӤ5.N#NNN.(R*R4RJ*JR#R*#4(.NN( ((..R"HSr9\9r69\`H27RsllOÆ55l._sss_.xsӭssl5lOOx.(#(..N ((Y#J*#((NN.N$N#(N.("6r\9SrI`r" 5lll†lOҫ_ 5s_xô.x_ssOs_5lOOO´N#a#(N.NN.(N#R(»N.N. #R#(..JH6r9\9rS6HS9r6JN.«5l5Æl5OOܫsx_5Ol_.slܫs5OӤܫ.N(#N4#fJ# $N((RR(»´CHSH۾6r9rHIIr99` 5«ܭ¤ÆlsllsxlOsx´x _5O5l5lOO5s_ N(#a#R#$N(IfI7Bmp"fN(##RN.v`rH۪Hr9SIr9\09m 5OOlO†ss5«__sO5x´x5OO5lOӭOҫl.N(4#RR#aN(N(aRf"7&m7na#$( #R4..rSBI۾rSI"ɚu@u\J OҫOOk†l_lOx.._sssslOӃOll5l†Ox.N(#R#8(N(*JR4(»N$(#R4$ 7S66f6\0u+B lOOOĭӫssl5lOӭOls .xss5«5ll5O†ҫ_ RR((NNaN(fR#(NaS6IIr@u+u©9fx5Oҫ5O__5O­†Osx´._s5OO5l¤Ol_x´.(4a2:R#N4(N(( #R:#´#S6r"ª690@+u+u4x555«llOܫsOOĆO_x´.xs_slOӆ†O5Oӭ†O5ls..N(#a#R(###(#R#R*R(.R66I"Hr@u+uX`R 5l5~5ӃO5lӭņO_.´xsl܃¤äÆ5sx´N((4#:RR#R:R:RR#4#Y((f66II6r\0u+XuXrYx5l55܃O5OӃ†kĆ­O5~yx.´ xs_ss5ӭ¤†k5sx .N4R#R:JfJfRJ:RRa $.RH6/6IªHSž@u+X+wX`vN55ls«l5OOOOOƒ†kk†l~x.x_s_ss5¤OOkÆO5ls_ N RfJ:JJ*JR#($™C66۪Br90u+XqX\J_l5ҫ5lOOҫOӭdžܫ_´_ss5k†ҫl .(4R2JffJfRR(™.#۾r\0u+uXuXX@(_sl5OOO5l5ll5ҫll5ӭĆa_..x_ssslO«OܭÆҫ_$(4:RJJJ*R4 $NNf¾H¾IIS9\0uX+X+`#´sOlasy~ays܃­__. slssOҫl†¤s__x.N( 4#4YN´N(7rSI۶۪6@@0uX+X+w9R.s5Oҫya:_a__a~5ƒOl_axNslҫ5s_slOlӭ¤ܫs_..N(NNN(N.C4S\9IHI8S`90@+u+Xw9R.sOO5s"f"%"n:4a:~ޏs_a_a ´.xsO5sO†O«l.NCv.´.N(N.#f9u9HI۶IIBr90u+qXw9R´ ҏ6`9`)9 rr)Sdp"yƿzlla_ax´x5OOs5O5lOl55sx.´.´.N4J7 (#(N.Ĵf`u`6۾I2I`90@0@uX9 slҎ609r90]30@30@0]\S"f:__axsOOҫOO5lӃҫ55ls.´™´V;N ٻ(RISP?GKJ#f9fJ#=vCN.N´fJ`0@0\rSH"êIS9\@0u0quSY´5lH@0H¾mm)`)9]@rH"zn~yx(x..x҃ëOOl5ҫ5O5s N(R#R#R4Jf"%77&r\XZ u3\9\w +0uPG3S&%fJfJRv.J9\ɞ`SIIIpr9\00@+uv.sz6S\0r۪f7&-&m&mSmS90\rm:a(xa_N..xsO¤O55OOҫ.N#"f"H)]90\\0uXwu@XwZwZE,i`m-7K7&7N:#7Sr909`rIHIpB6r9žu0Rayf696Sr`r`9@09\]0@0u9SB&S9H"Ra4a_xN .´.sOŤO5sOӃӭӃO«_.NvR"If""fnH`\ɚ]\9\030@\@XZ oiH-B&7Bm%v.#YIHSrr6IHpIpH6r990u0`SI:fH6p69@\SH9`rS9]3@9\P`)m7I6H"Rax.N.Ns5ӤkÆҫls_sOӃӃOl.(#"7"fJf"r9\\9\9@0@\9\u0uPXX\z"7K&BKmS7R´v(#RªHII۾SpIH6r90@307%BRIHISrfRJp7&m&m&m`uX9"fRR׻..*sܤņ†~ss5OOOl(RfIf"IJfRfr9\@09\ɚ0@\0XP9f%-m&m)dm(8#8I"I۾rHIH¾6r90##JpIpmBBHS9)`)9&\u\HY $ô(sӆk†äO~ays_y5O5O_N(RfI*J"ru\\3\9\0\ɞ@XZ7J%7&Bm&7 N#I28Ir9SpI۾6r9YV##RJf`0@9+wZwX^' ٴ(^mB%*vN..(#J%7%7Sm&%=ٴ*%mSB%%JR(N.N..NVRIBm^(a.Nm&J8"&mSmS9`9`RJmdSmmSS`9`)SmfRIS9`mm&7"7S)^&f(N##R(4>^''‰AbVV*-Ÿ%J#(.(R#v4(.´. R4J%7K%b;4*%7K7f*R4.NCv4:#4(.vf%Ÿ&-*N(4.R)7Rf&m)\9`7fmSmSmù)S`99`99SJRpS9`m-7f%pr\`m(.44R4((V=bA^'>Db=* #v(vJ`)mA=..N»(v#R*W=..=*4(N.NvԷGi7C(vY=.#((4NR7r)BRJ7&mS`9`R"mSm`\39\9\9SmS%R%dS&7"9\`7RN##(#; A>A7mm> ´Vvٻ.(#7m9\]3^4 ..Vv=vV..V#=Y...)G<t 7f#(.CvC.N(a84"rS7J8f7&m`)S7*#S&7dmB&)`9\99\9`9S7%8J7&m&m&7f7\9`%vN$#R#N. 7S99rm&-D=...;.(:WK7S\@] 9&º=Nô.»VCV..CVC.ôCR%-SG!i}7 ..;.NaN(4.(I&KffI7p7&7%`)m`9`9\9`9`&J"7I7ff7`]9%(N #J ׻V= A7)0@0@\r&>A..4R A)3G9u9S&A%*Y.N..V..V..N.N.("7SuPi! <3&%A4.´(4#N44N 7m7Ifnf"f2` `S)S`]@]\]\]9`KJŸ•I`)Kn(N(RR (N;=A90@u0@0S¼^¼=..NvCR"m'j93u9m7%7v.»;..C.CJ7%`@Pi}PmJ*#v(.N(R(xR8#"mSSmBmSS9]'`)]uPu=.´(v=R4#R*f1\)mK"fn*R4(...´N*R"JfuG<`JR*RY= ׻. 4R(N4aR(#f&mS` r`9\š]\ ` mS93ǥ\]\\\9r`9`)f=CN ((R#(#NV= %9@\09\r^>^.»(=4R"7 Gj S&ffJ*vN..vf%J7nJ%u' nJ*(V* .((#׻N4(N»v*Jf"I7Hmm7p%B7&m&&p7K7&7&7êI7%JCN$(N#4(4 (;=* % 09099`)^A^ ..(4RJ7Km 3'^&7"%f*J%b(.V A-ԼCR"%f%7-]G'j<}G7f%f#V=CN(#(a((N.N((4#R444Rf"fJR4#4#v4#R#Y(v((.((N(##(($ b`9–099)'^>AD¼DV.NYRJJ=R* W=bD%J*J*J* =b=V..٦= -v4fAPG}P)&7fv V((#(((N$(N.»(((4RJ*J#R((((YY##RR(#(N. -`r09ɢ%>A񵉵NNv4= CCVv4=*#RJ* b=.´.b Dn4(*nmK PGiP K%fvVb(#R#(($N(N(NN(C#RJR#(($N(aR#R4R4(R(4aRN;= ډH9 rmz'>D b b.NN»YR=#4#R(Vv# *#b* *V= b *vWf& iG!i\m&K7JVb(N´™$#((N $N($N($(#RR (((#4#RR#(4(N.b -%nz67A>Abb;´N.(#R*f7H&K7m&R.R*b *b;٦* nYR*f7&K7&)i!iG S&77vV=CN(a4R4( #RNN  # $#R*:#4 ( #R#R#fR4((a#4(N.;b >->>'bb.N.N(#Jf7`9]\3\S7J4 (vbR**=.V=b% Cv7K7m3 , ,i!oP)BI%7"JYC..N4R4:4 ( ((#RY 4RJ#R##4a#( ( 4( #RR(R#4N.V= A>A^^>'> v=A .N.N.(4#J%`uZ !XwX9JRN(**bC= RV4f%7"Ÿ7)t, `S%JfJ*vN´..(R:R(4aJ*8#((4R#RR:J8*R#($4#4#4#R#a4N=bA‰A>^‰>^¦Dv»N4YR*Jf79wT,wXS7fJR4(NCvC=C.v=v(CRf%%J7&\XTϋuS&7JR´N. (4#R((4R##RN(R:fR##RJRfRa( ((((#a(N=A>D>'= DbN(#R*m@EQwXPu9`7fn2fRvN.٦CvC.´(#7fJfwZQt 7nR4(.N.(4:R#N4##4R$(#JR*R#R#4(4aR(N.;=A>‰D^^>>bVb =NN#f&9uwZwP00`m7f&B7f=C.V.V.R7%f%SwZuPu3XwZEX9#.NN# (N(44R#RJRJ%"fRa4a44#a(N((#4(.V=D>A‰'A>^=bV.(Rfm9]3@ɞ9`S9@9`p7& Vٻ.;»;. 7m&Km @Pw `r)`) \]Gu &fR#Y.(4R#(((((R#R#( ((((Y4#4N;VD^>A'D^'D> b=û#*Wf7B \9`mBHp777S\@`SmK^A C.VC.ڼ&S`\@u\`&77m-m)\¥`IJRN..(4RR:R# (( $ R#RR#R( ( RJRN..;VD^>A^DAډA‰ b=¦((4#J7S)9-"J*R4*Im`\ɚ9`m>*CV bV ^&S9u\9`m*RRf&\])J#.N4#RJfJR:#4#8#Rf"fJR###4#RfJR#4(..D^DA‰D ÉAD=V((#*mS)%#(vY(YRf7`9`mBډKڵ:.%bC mS`9S&7fv(CvC4%mS)7%f4.N(4a#:fJR#R#R#Rf7I"fR8R4R#R#a##RJfJJ#4(N..D>Ab A‰A=CC(#f%B7%"*#(RYR#Y(vRJI&mSm7"%b..b%(YJ&&-I(4#R#b(4f%7&K"fR(N´N(#:R:#R*RaJ*RJ*fI7I":R:RR:#RR#(NV bDD A‰Db;v#JRJnJR4(*fJn#YJff" #R=; %(R*"f%*JR#bJfJ7J(v4RfJR׻.´N44R#a#a#aR:RRR*fJIfJRRaRR#aR#4#a#R:#(N.= D=ډ Abb;(C4(Y4((Cv%K%RJJR#vR=#4Y(#R*bv.*R(4##RRf%f*%-%#Y(vYv(.(#(#48a#Ra*Jf"ffJ:R#44#((NbD=>b=A=-D.((YY(YvY#J%JR*J#J*Y(#Y( v4v#R (Yv#=J*#RJf%f%J4(#v(44(N.N#a#4Ra#RJfI"fJ*Ra#Ra44#(N.=D > =٦ CNv#RvC*YRJ#(4R#YCf4C#.((((.(#*4C#Jf= 4*J%*J(.YR(##Y(N(a( a#R*f%JJR#R4 ((.;=Db>Db =bb.»CY#RvC*(R*#(4#Y(Yf4(#Rٻ(((((.(#J4(YJ(4Jf%R*(R=(#R(.´N(4((4#R:JfJJR4R#R4 ((NCˉ>A=AAb=b=CC=V.NV(4v(.Cv..V(ٻ(.4#(4Y.(.N(N(. v4((4N(vRbC(C(´(CN.(v#(N((RffJf"fI"f##4(($N´ оO%biD$cBb)&0ohbetHU3aE-y]M_d3FT,% D4ʦWѥ}O-ݳشxiu|ws<{]<<?<7i\mL/WdPjT4l&kPNK2Ǒ\>임{R#M׮Ŝw7, ֫-EL"k\3ﴜԽ@T#B Ã:xTD\οdL\tD|d_^^s9]T<ɜTĮ᳄~lA4O?H$s.Nw2 4أ*ҥWX,u}<([e_h"27!"Ghve/տ f"bPie04M VfxUI"*"D9"C QUQ6M@u8Sۗꓪ}|HC1iT X?hOַzGZJ"mt]ri̷*ob1F;8 扨<]iafa QmJm;+d70/iB[ZWWg/0+nQ1׾o&ŷ2۽]eq3͛7*`IYhEDLrF*^))*ANS29Um}B`4Ik{E!(L85|/[`)u4;/k|e)X\cfR횦y~:O$" "PʝQyq1#d}- b'rSo(-$"4!svq97&-daIJ?$.1Clpߐ[u`.7dʣa\Rr\=vn{Bbp}T B;6FFF^D^CRȱ}dc4C u=hkR\ᕬtI|;}ihtf]HJL/6jTjx*S$o_3ߘ.fɈk}m2c|yfrl[_et!ڧ0CsX:6Tuc-h/8⏢Z!kbXVɷMq;8Ʌfkh$Ue,[Y_K2KXU6~}r:]DĻkk ٿDٙ]腶w}b7ljUbil&[T9www爟(aZi*(n0ODٻ,HI0mx>UåXkqM-h\o:L嗪&u#όP !h9g0s<: FT 'G!ND4No{;$^.8nw-I0&tC!h=xF۶KQ'[ CfW;?*UYJRWȺŽ"绮;0K{c0i̥2MSA%*E~0%J%UxƮfGN'{iuZ4ٽr(P#.b2/EdpHA`\O{gWT͋/V]MJ 0ȗ #*R!N.dZ2BLD)`dt8LTc߰o}S1ޛidW_}`Jlq}3sd׻zmے_L/yo\.'"v{G}Cӹv"Y?k|]CBfXȾ_Φ9_~ U5"֏lIC`GiM GJ&%ư[7k6UV{o)cp Sǎ[ 9yIKZLMs0 !Hp6C諯J`1b ΀)*yF R>frLsC-2n7ݻp:E5JTqMv᪘:of(Qr?aJ0^Uɩ@P; b# QH>,PGJ@FD\SFYn  &Jjf"M (Q]Q vQQsAX!  ;RqvΫF䚞4ND zAm'偟#%xDṗáۻ}۶mۖ.Ux>D5FK/2qUD8p1,EfL IB-5jsޯ@K4+ s@LRXٙ"Z,q-LQUC`5RLv< SW!HKPQIJSf+ a/?yPQQ11s0UE2c2tyV@DJ&b; L#18EĜ+9#haf#A͊Dgv b *aǔBU{eW"(@ *1a'HjǦmonnOݮ/l`p{wW_4H:ȕO~ -oIg`f&M4b΅(OO ǧx$F#wɆ>i2dR}`~lDDVٵNj/H!ܪ7XIH-P:B2h* {Mf̫ fE>*"br@ȗ α[(H*͑HαDQ4wB% _iT윭~ @ETi#}f*==>BQ F< `Ab#9)Š|$Q foH4V~ILH8 Y:B JlxJbk R8%8X2PFc1$T)ԋ`$*e‚x8mTbU1LbyOsnb 2Av{s,Rv})yoxi~fvLoi`I)5ͱm4Ϫb%0~QVJ `Gsn݌̦s Ob Z1D)D̔,g60hPKI  3Ɍ֎j I$8QJP2r*PXYvDݬ\*C3|>=9rgtN8 8*$nu '䁿1½LD[Nq,wnキJi xxxhv~.a0{B=Vf(9윹BqmXЊIZXâTɸqI>+U 2̂is qP 8 %HJN%s%*)P@>K1Ó ]HDbι A!ahs]׀Qvb_|Eʼnཿ?8pLBeT9{Uel)Ɓqyea&G ͨԂ21e=\>;)җeTL*g M|l58&g*)!] ,%OvSKȜ%&5!!jfƍt8ބ۶)v}xef{[fԯy`#Sbc}Yg_v{f}fft]ٛ7p/_O4cP a 1Hd^eɵ }V5<(5i='&p Vp`YfTRHr.)4a$Q~Ky 3 xa)`t$`6Pf>#I0U B "_`p"9bUeZ3qMav. >lI  ;Z>쳦۶e<<9e(1¿%(F[eCJL!S̚~s+-L^IPX ltwS",ܓN$FL!T4mc,mHۚɎIh1Ɯc&y9GH9NΉ]n`op8bVKyo \\B |%wm۶7i iƲ/矽q>Ox hDaOMPDRTD׌mRV\HI ! %D'W+rUpaѮߝ.0~'"$H-\L0g훦mێCi84Mf9NTHሣI U$P6|(Ѫ1Uܴ4%voiB 5#? z$g7`kw~?o, Wr|:_5D("c HU|3ޞ͛*''jK6+ELUievx\-&D2ylJ}?MrquLe`y1xcwz. qɫrJiYl^2V8 C}sOS}ikpwݻϮV}+v|`K h;O`s<8g͟,S*Qd X%Fhjw](@)>e3妺02Ȳ 1k 犕]Bs M1ڶ=/Q4[:N#;jolbE1 (xD9?)B1KP w,*ٓI?43bshCKTUf<]9ε*\z@[WeuD$~_p:mBկw.!Lr&UAUSI~hiuĤbɹb &ves\!' "˂C9%T7:#OBLcLmqHn.h#[ye}FQ"jGED,cv졘(S3| !`j yL;rblNKbvΑJ$Bxӵ}Ȏ%үI77Nf R-_ԿR{"/IΪjŇ_|Evp?Os&L1BIDf3(QFgU Ί ̴rMNQazClaBDBh|Bu}׶8'y8la3e6]S$A\+Gʍ( 0e-kBli)D%F"ԚeDh4Uڻ#^oi p8G/ҪĴp yv4A$%Q$a* "!bI$Y+K x!% PA-Zn) d4[Qon=>{H52'M 3'*bPi"R*GTV`xKspㅲsR )@7pOmғLQnnoumn?nooƱd^Ec?׌{0#v/޽{ww:o=30??p8Pd ~~C ?ۑlYaXvֈŨ`(J$e%kf/0oy|^8@szs_{<[XF")l hI\VXڣ®!h5^C'e>[!%'r*%e@1DJ&"mwrbn}o޼ڶUX*K|_%2Uˇ;1?~~F3绻;O.c D *Q%XbsT@p<⛑5Ccv lD{)UbRꗟ3}_/=}_E^\Fu9qA3l$0]8JRy=EuX,Bb( T !M !w,\d {|~r6ߠȋۿ/$SWy{O[,|>=4躶{͍?5 Ih`jQg|D$Լh4K,A ghOfb,,WvŞ cXFFә _孒#%|Q@ ι!`^q1xXVHHV%JUyRrlv:IkdCu|ݷߚ2%a{zz}:giK?!ٍLϝ3"~!o/m}8o.ItGcgdwPQ T R)ZfN.a^QgfFIt 2wR6fEsyNo}, R5Β;\oUC2p""Ds,&9w(S9m!e8{A@I994^x{T4Mz(8rfN}q2 Xʦ2KBFB(h ײw4 MjϤXC$icq3;7WZ˖n#Jz,Q/tpptznƼ^D.KWٻI`R J|9a״}1QuV}t:=?1 1?@)7g< "vH]#Q-O"g`'!a2UclsJ20lw; %pJ~ %0AQ7*c\Fm"*,]* W[I]*&KR鶐uV+~D(c4_ȹ BU$޽9O] ;E;[$}Lg z"nd>߳sMr79ό0Nx9I&YS|LT  4 >|8UљlL%ah 8g$4M4" tyWe-;CyYOzn@Yۍqy9qs FMjeڅ>%#zqJLyyߚm4m߷z:=4MV1 e5r3B)wB@SLv@ o(~6-*uL^Ṷ#%sIjt9iu]6(Y/YģD% U`Y;,ǎzSlUMBʕldSVx1iO/{o|18h[%BBQ8{,*~Ӈs}_kqLr_%VFU811`Yaヴ]ҸTP t_8?sx1hоFs>r&w#jL:r.Zf2{"7J`WأM:S5!%q|<`KRIPpb^U*|P{}'M=M=|DFKdy/|Q;A?Ƕk1B</YSCLBTkLFBABdHf瀬|CMӌc*(w~Bȁ%NYҩm3Cj=Mה^ |+?N'Q8+eStScacCHLM {EO`ֽ%!aJĄ%Z-$ t>F수>L8bJOin |E7?>HFݡo޼yzz:nafrnƿ\NiHU5"U%jݬTIR4\Pl2V]p@<+}1LSW:cŹ\(^5(W, UK*CUrLcUnRU!L:C jCx-bq" v\:=+Sh5T%{S *P:ۻ~Զ]i|Au^D.E$?Xت՟KOb\.]oڶDz`4 r"8N3D d5s DUϛ! (+Ѩb.dZ*aa@jGȌ}w1ZL 5Z7?G1kAA qWPrSߢjHd7|T&9(I,Lm1\gX--7Bf ̟27Hd8˳aaBr8cp"Ab((F}MӶrs"onny/e8D"uZ#rr,85gǣJU,LOIqE{OBD]>wV8LRQ%_> ._7" YRK3LcZ]:]*p8P%1{Cv1׊\7[)nb=-!yUM(Ӝx2+.眙V]׷ jm4~CېJ(߇?7Mîqαw 1Ǩ&Qb Ug?D51ҤPrݍ* v.m^,p&9u]&"2#v;{|if6˧]o4H69~? bb)[,sLXյZۙr*1jJ'"8ݡq(CjW,w|>T}^/Pɥ9_2-F]EExj)oB61Tg԰ݛ7o.Nd/| /BPUs4E:9uL`U Io#%N5/2>vݮHB70 p.'k0M ,[a $:XoFHR3K$sy3N8߼֞}*Laۘ)aFS5qaΒrm3EV,ŴѳA)eI,g^~9ƩwN`R][( AQQD/޿߶kޏez ̶|noo smG۶A8bN"~^Kk)1@ޙc̾L tudKK4Gf"f{"4´`j G.$LQY8D$ZBJ$j*+cIy#7d IP6JF̢6,1Є5XYs"֧y5y~YZ0/<( au~߶2LӘD9>o߾m™MZp$.ݦu;l-\fxt!dCO3Skl pRy/e_b5]ToUlgB:SvB:KM0z_u.̩"|b_M(8P"ΩFYN'<>wH_ /]Z[ʼS@؊x2rmM"D WҺEZ=@4y?t]oO4 ޺~||\M25̦`Ҷi1hW WoƁD.8L 0]REeEQ#edUS7r*Uqcz4ɉU{+Z7.O)E2+tZ AsK2IgvLuKX,˸bT,CWZCITNqYC@ kF!J~a$.|RQJ3&(eIeJJ2rwx\D5L{o!v`2P oeIc%,(yUU)eTY5M&4湽9{9%" ]t_ ԈPIy|KS9'܁ձ(=x #a;u:4Ng͝m̯+tHWwKJRkM éDuv Y&PM1s]'ZD0\5~hIoHR4k|o4Jy<]TQĜ L.4Wel(/#dU(s$-6R 1V85751:N8l`Afjj.."xD3?W t)G Qաl:2;9k'+p-vh.?[TTBɱлƣDb}(ͰanooGŐڂ0yb rf( plޮW G" oG3/!bP Fޞj3Mg !4P@EVLX0@\mjEg/ U4 f耊PLtf9WKWL*TqOyց?ay*:3R&b9n;}I%&ͬr877߿#C0B?83J"O*Ab q8v t !)~ "\BȚh2ZfeG`fwCJ)\#0UAYM '$"ʶ*o-8dZ{FhJFZmJaTV ԡ<6oϤT']$IDK; < E258iDcL&FMW>k 6#"l]Ҩx>vr <&5h9U1{q^̟Bʴ53 N/xzCN`c)ĉJ9 Udja#'\_UkNR߸EDݡt/1KIJc)kUKe8wu 0M.j\>f"y(%kgz3 Ʊu$a8cFvde\RrKcT&C~*Js=?xdBtlr6KKL0;uc@dEn&b{ci+^6~HQdn3**/HI "S1TQ%;?CTCo[LYd&kXI-d.%If>O)*`Cot\. 3sHUA*c|@ט8uxʓči4@V$SXSPT*:\j o(Bf@,u,EauM#0SOsj3o_e0lz;[;ܑ%b#XRlBRćϝPj{8^UDBN#Z@L#*ih΢# Ԑ|9BS`oKj2#u}"׶L+oizj|Ed 4R*fѸZ8%T { ~UqOrt9RœTs]2`Rd T&J\>{|.=܅ͯjwJs c,uvK/&@9¾6U}R H44MS6/,yֵVry &[~%DEa,b }aA{wZL+s"/+T9 f_Z2MACd@XгX?%pN]STH檷P_~4孋s u|!M*77]kЪJME ))üDgQ=SϑUTF bHm{Q4sl]$3OEs4U(u ;J:9Syf@f3ۊQ"l ZB&3̟ʟvyxPAZ-{c,+|ݕG,`]OJ[cp-<&Ѩi|zQH4n^H gɪbtj5JbfeTs˥Zkgs.T!ۋNdQUgɔ)lCٸsefy}Uԧ;Uk4lM+X YjrL) UU9EZ89%h5( Sq1tK1^Z4{9Nfh`4NJjQjs;,fI)[-J]y7I`# `3]~̪3KZ?ԹߪAT+a1XgVVxMM^[nV2~Z_N+v4&C=ƘZAϚ.׍14 mٗgߔ֧'VƱ2o]EsD41jIeE bX׈#O:+Vfuk[[~/1FRM\JWH-?98RΐSe3猆eZFh%;@B*hN<3=~,mF3M;U_EI4i΁i!s&֮&wqK`B +&K$mch~ Z Q{Tػ m-iՀ ZerBYtxTd,U=X^ڸ_IV.M|>l^.-ZPU[VV-DZzWE,jIMH%kP'|nӡ sT\r^K(JE*W2i1 |1zbO n2ٯ|٠ D ?`DS,IYw"uQQ"L*0>eMR#*G$vhe%"6 Cj͠$Цؼ\Po KOo&3s?u>! $]$*o:tg)UAL9`kvUyȄu߮зog3R3xWT>-~AoJƆݏ*d]K-Dգ6I~zm=x %%xo%WgMxAr2"qc.%W]ZKpآ9(6 VXiI[Ϣ9.z5UnjhR>l0S8`WZnDa6YhBjtmRrSr6/NTSZEnjTݘBn' !5*V !|8@y(rнW!eyUӓ.\1hNHιc}o۶/|/ݕL+S[]ߊ|.w2=ESh|q_aV. X53'λ)8/Fs~iꌭR Ec׌("8NhDr\;NX@\FU^lsdNX(m[34(-;i401Ƥ^DbR!SuS8q 4~ߗBc$mrz9}KGv!pŦmb71gu-[ȶ¦QF ʝ` $G%49QSح+yk"2Q%/ض~ UpUAuXX1kW[ GI R8XOǏq){5R6uл +_2jT8 3+u1 w΍`c솧im]-U(Ixy.K8E^܇펬ddy^Mzʡ9+i2ӳC1).B9yqǂ%+\aD(~ǾtL 3:V&KIŐwتw+bC 皔X9r(.!2;  oSۉ|yejsh4WA1dF9Bv3__NfH &fm)E!E g8%R+ 1;bsgLQvEi׌9%,Ei9zygCqx8 Bh8_jˑ~G7-İ2J聈HL*EsLH(Ez~?'Aj<% 4!i:[d~KpEԢrs$ ^R5xBh7Ms{WخUj"f!{X^<>=ۻۇۦ!@mzKKĶڔWإN5+(![1 bmhﺦi ijk"u=Iu<"K43CPA5,bi4} Jѡw~/oIrgMA<eDĹ 47dASjzS$v+Qeno*yLy^ z.^#ߡ;IO J }pZ|J!څG!f޲hu578.3<]c !cC+E,1B|zʬ5~+6xqYDX3*`YW^c4jVsw}npM۶cߐ8?.!Wwk^!WL,R9)IWZrV*xZr[@bwݡCwy|:};EaYp\};$TȶQsD5iU y33Op0^JC|o"r m4L."2ȉi( x\EAԗVbknl# }crOoi ST4L k0JRaX=M(z[^b+̩?6辅tPBE-rͳ"Zu&Qpi a rqu()$֌SM՚Ĵܲ@;Ve윶}=?=QG ݷ߆)f`5fRr|.4Nb3@մͯ3]Ե/^(EUUE$b;LOu޲*8Fph%yavq?&QR!3(^Ya~XqW-('9heN֧|ݗl>pm|&Z"K|Hp5Cf9"$*A5:Aoo80 kJ9t\Y=R/-1E|RIoeY sEfyBY?Et/e/'G*s:tʔ\v<) m~Rk}&egUTevHs*e _c (R,:CD&`\c?$n6khhJn)5kZGZYm oy&RY)_V \ĕ@MW%0]U%%Ezmh uZyHX!u_dC^@aqKw(i:Ю~%JPt:*q+@s֡5hd"\ Ohffl͝ OZ$z݉}*8pjVU33RTbuV %](vW>V[reRP2y b@>W!ȭ, 0`ʬ@ kb}+!Hd2-ɎZʆmY.a:|JvA'n-,-/Wg~}^ᄕ~m*ڰ^}Vll~ï<[o]y!%!XHdQ#TdyߛX<*x w3>ެqUC^bQݮKrk54[^묈i,J2#p:c+bWI9t=TUsL QZ CBYмhq%iK:+tUM>b%ō ?T4Χ":k?;x?DYWD$QIr em9?F^ҍPY Z^ϕ k[R+LhHwBZ}|}+KߗRhW>k ^ʤq*މttt}?N5cH g~LՀիBAcA g; s9 MG$i\Z")Hd2g@^S@sքs/ zHp[\=[MXK5 !}u#O=-uLp%dn뜔!:o*R*|ӒF,6L཰kݙs2O E9;5]$irsi'IvqMci vΰ *G5|P>vFhEլ>[Y$B# ֤Gָz<(.S^1d""-`愱cג{]*L!ͷ+6 gM% uΉJn;TݤWeXt<`\6Fid•DQp\ Bu(1m5K\ڔހ2X ̜I #|EMm ڀ#}Zf! -EY8ĽRKhb($-E"* 7}{lӋq^-&PM7,SyKf$걯96d,Dj}ߙ1yo[j&F&1Ι-ܹ̍&cRBSּm sS4`j Wqʿ^yY+}XK rPg"rK>̽MKҋ;_f;-\y W}Z[M|D- "W1({G<]1NV8nNry,JEY|,"PveNܛ9V1{q0Mh7enǛ}6M{ezBnôQ5ŏ^ SÂFEu;uk 򽛺~_%dcڗkYmv>^R%f~8@F| h2^ڈ |JRSD;Ni4"(#vl`Y :F;컦."e²L}-򅘘SW+է2a[ ]8TOzd&Oeʕ^0꽷C6-_%3P֚K^b (udh[pχlCcL%Q=WB)͟=`4|Hp LJP2 z{DnL:拈uo\un˄]s |3TvbhO!"4w'SI)P!aSasJSGk{X+mQeX K)Bk&[_e՟5}/$5-r5rMfJ_ ^eQGW)D)7_WS r{[3 "6HezE824O'+*V㽏o\bo.7Io49u296d4Mo߾N֦%/|!Zkͭ(P}#εδ:ukZ/_OX^VKQ}eBonZtNJFgү8JI4J\^_qҠ9`H!ex|\(Pyb|RyC,XA{u_s#e-PNsig ACk+[{'\g͒8 Ldjp ,i LjO)Smգ 7AZ̼?X* F@[CPZEj%')HijW-V4sd*41A8OC8]mr%X2VWd̵*Ì7RVl:opT{~]RJٺ%\_FyXY"ϧۑ_ޠWA%wem0iBs1yr|%RG4,U})iK7ڶ=9\譛()%IĻ^K@q Vv|XDj:uftZ-\}mUQG?)vJ_|Y6W=:"kN!LR-f*Υu#A{a.A @Irϊ0/k8wa-ڋ堛-nJ__lPԲz)&vk29N@nx+P " nҕ괊XryC$jnN @XfdgV|jyq7p^vgf{Eb&MĿ|eK{%Bpd4o]La"ڿ0=?)`U.k.^rqI\sXtTQZ'F966.@A~,Nz: _i?$rҜMUiyذH|z"s6L6>u2)E .A--E_}ڮ68OAL*2hr GRvV|{TKn%iZ%IV-[ffBT`n9L5*&rr\g2_r KSa5o*X^^Jr}*u5XAq!ѷJC$",c*-±(C+5}Wl+vIJwpМ_cf(rY='cI7tE\"6ZM 5iԒ5}Ҟ ;#Eֶѯj&{2u|9[f+W:dN&M$7KܙV^%cH75Lbi4*C5YYkհIHl '>rz"v6=vŽ8iʼ+o&ɷTSUB֤t +>|LA_xE'\!4l>z#9 ϳe,ګұj6qb^s{R2^[WlW9ODdNnazuFm޵su !Ę՘=3D?&s" @u5ղU*Xɕ7faۧ<Ǭj(Pėz7Oj:'!͆YQ$ )$߹b| 0Om+QREQfGܰ/ T]POԟ9% 8(XȋL?S ؆6n?]uAYpo6 jC|zR~LJt#u5\LKv]yQ[fiODi>{0ꈗ LO#8f&+ҡC~ZpsnbPGXlU}T.z-WI跗8mɫ>w6[Q+P霣9~Re"Y%Z>CEˮ3itMf 0{ Maw'PŬO>eEgKYd ȋRyTQ%屪5蓪2) T!j|3^Cs-Ņ`TB1DdU\++zQeW"I8Bs7-Ơ+ciHm[Y3 1ӣn ?˛xJ:js>^Нoa3@@]|^#mH˱ x* JQh}5{-_ޞn|?y2rvn_ ܁>P ;[-s5t6?N af.ߒƧg3gH %fVP4\$P?1>3'H'_$2;,HE6hX(_enK*ṫ\sŁK^׽W|$P3jT&);Y),l@S踶Nf6Fh ]%a`c۶m[[ύ?nX/~ЭI4UR24pf":zk,h}Kڛm!K|tˢۗV7A[%x_|Y5m'ϕݡЀ;+_;ZG 'lNWLuI4+[RÉJFU-.=j DS%]XJDݽ(({%3d(-Ij]')s7M9l:H$KW$D\cѨk[*i얼kgLL%J:+XpPmC=8 g5UY&:*?(YO .=(,jEE`ӵ I  Mg=S>˺4 VA)ܚo]=W mKGOV ^vm[nΑyRy>v\͹t:Y  ~3{1yJ ڶIΦz%MCsv,iD5I!1sJ3"+ȩyO\~̨mӫOiRM7YD+]Wf^4? /#hUem4n^2py4N8ʕ:d/@DJSC=\=lpy~M/A1uiƾecIWȬk'ax躮[FS3t3Uv|g-skQu`uhKKXZ\/vş/6"m\)=.*{fok2I,._}(]>no$THIz|2Xa&@*r䈛)J gaN9A4WeSem)&3 $+2(],gZ$ĝ$(`+%QJ'$R! ˚"62skA3Bw ̈́n] kd5a26wL#- bS$hVD2&m`Gf6e5 .(Y3IOEUK9-|vX"$F0si@vr:qk0F94Y+Z'dК;K 9dZn|65"5U,%) I"k0B,]j5Gnǯ˷]n(˾+jLu+RzCW<w3~Ιsbi6xWU?huZ )c՝;L9x&NCmߩj㻑/a\GbGD*ƄXg.es˜FQx>Op "hs,7EH,E^jwn  $I5ͮtxyv펁EF.IlgB% UU”%v 42Nӄ:޶lHD$RI#Ԁ[t]=_Ʃ!=3\g֫p4t:w޾ɷ@ DAҹh f'O+-&9~>Oa|E)Jk>UU1~eZS U*Zez ҚJs]vtvw nGk'̡:P^K<%|X H躮SLFYM U81Q6FHP9W3=j+iOOOI|'Uh9Fs؆TEo, D- ׬r!Wu/_nLevßvG$T"O[{CZBe䫪Gr8t7'QUeW=_],$]\ 3L'+ݡZ Q8g.ZE|9*+ ^y `.DoQkcD(hiּiiDD==FGin@{UviT4Wonr9"$B)(1N|^7gbb߲NCysΚjZ#hoF^Z2mhGӵ]yvqeooa:}{5b'1$2IA$"I|vUwW!XPJHjmTR˼FϚv]{uaFv/LqBHSU I}ۼy8_=>CJbHvL4#Z+c;y$ٯ@ptgiTis0E}L+c}um۶m>psscaȢz!1.j$Q Q\u`߫ L VsID Wy"*tl/7?U Mko秧 1041tYcqGJ]88ZeU'eZ_ ~{kvݮv1jABE$ܗ@9%RDU (/N5`.M9 tH%(oxձhfK|QY]&I0?j1)VJL Z䬠\ 0|ΞPkW,n\ ~{PmwK !X~*;y"uJ|Wʌ!qit HXH)77d22dipuU#JlD,~I؜/CT֮Bn%uCsf s%$ՙm3m>jv]df^tnﻔ˜h=8*͎0Â_/)i|Ǩ]Qbqڄ+zL"}(M>3AzztQxLa1Lag0bմs0X:sA4$ "ƹApVHM \nISHMlHXBiVB EJU2] lWhe+XRS û2LdyV}'"X\>J$rq!ڴ6EK|9"(ሉ13%Ďu i68 CA:WRubۦ5T|9y֜u4SBӔ$v h)&JiaU9q4obF<$Pxȉ?%0[_ĕԕOU$ ^Aq4gI'QQ3ER̓R:gAfb;bWH3lIb\<dH]o$~G@uAbAb/Kɭ Pkݶmuq.r(LFYM4Km;LѦ RzbīXM T(*"*`0E*Pf()1{kcs']W2,)%fA&uZv/E,ɾ)O@)KGix ؽU0[کAX뚅hJPEj^=\ Gir !@buhm@Vg@<;FN51u] +[}5{ug%gsD+6 "f_SW$Nq+=fJpP"Blw[ƑiB_뭆WICX1yo$VD秧'081e{T\8=T*t QWiZ6Sxʣ工1lO/DcZ}"" YzN|}%Mf_zM/̝]ݿl R5:AeCƒuGDؙFD<))snCۛcAU??΂[a1R%kZ$1:׈w_+J0{CE]_woBULp\x(RA%V{Z Y%~ZV%YhbnYd&4DZ))9L/,*/69طyT5k?e]ʻ1%SUcjfAEXE^2ATސ:K%".9̻ݮn&F dH rsfEKk.Ixs.sh_ݮ?s1f1BEizjbaPhkjW'fU)W1 uʗ_?:j\hF]7kKMˍwk3d?cI}[=jIi4@<|K%(02d(X(ΗA}+94~&ۛS5Jc $ԊfB9I# Xҕm}$9:E!QK 50gk\j@)}$ qTB`RlJe-`ȕVuUڰ(rBvҤw9"xݾooq*^baa& +m[yc}IY 5-=dpo߾co~s8ܐk{g'7 Y6[k=H[ovHuXj{KhWwXu3M"Gksh"Xrk,|b2 \i5}߽͛{g^vgaJqbm@,4@tevMu}ߧTU 6??=)?X0a$&f4>hf=;VhMq 1]U2fɼ1F̎bgoI3"bt>|*I.&;{CvL)lUh9jrN UԺ2ElndgWXYm |Mw3@P_&4MZ"ggP}QJO9IA@!nc3,n׏΍HJQ"ɱaJfu)^ I0dtϭruHR&y5LȉJ6ur5ƢRN`[9sbl ir_ܾ[0(C,P̥LJ$i(EXϹ^n|ssÔ4m!ܕ3qԏZ9wc)ɰQj0JNa1*ATR_.vTL!(MzQuD S̀UavqwXGC0ˡOIcF`fϾck: mla$vQgt⥌B[H@6)6*Q@AnN 0@P%!ayRj';ik\ n_} 9Ǡob V; w9亨0KHߌD777.Ub ϣ1F(5MDDꛫbN3|Jp9_9ĉS!Sܸ̚3S|piOH$SRJϢ%է/+P^?-ʟ_y0U{[_7fFUu(4C.v 9:/*?m+ r~;9Uev!&Uw]Fk?>X򙑿k:DDnRxd>` 1!.0\ڮ0w~۝ hzz2^sIq2%(M$Z,/fM(4AˢtP+Aҥϲ#W\ol1L 2N$?GTfDx4s3q4̩\8믿>˥o;$\X•[ÏOH `wjf8rL1(J{jg%wnmq-VBD}7M# LѼ7!A婔B #Uz b/-n<)L暤~B*@ Ylcyn? KDa/gg`Ib 7IKFmϾ꫻[":1a,ѿWF]hՏ9>5{xxPJILʧQQ߸9>\4M~p8>mێ(I&sz RWU2,p*jQRK:֢*q.V@7Ƕ8_"u$b?-d Wv/r2ӳ45Hqq)b#sT}.J̚թ8.dqoA{[ڮ}=]Ev΢rsIuOL~ &1 (9XRqf%-ϐnj*%$@ZSM֋iKΊ&kl͕EW e ARu^/Or3UMA1skGK)FQb+X~kV+; ֓c&jnƘ]g.4ϧ)_E> 0H~I6K9H%vm{:_VQZ6_0 E a~ 2 a8$p6|hTA1D)(%6 EƇBo&W nuGog[HAAw+gfnwǻ;taW%2//VUPٿ۞aPMn55g.p+G2{\0/JpLo߽="hE)p{%SNSPN ̋u]mXgT)mc9gWR< 7ݥ9Y KµRS.X Y=p̉ETRPWtRO掰rW7}ή$E(14E(l]J d8Eg-A@ d-[ШߺETl3{%Uཿ߱(VSPqqv S]wJO 0wSч$_۰AAUߜN{ r2Px<M Ȁ;j&D Y鸩xV+0C$E, U4g&;lVsݨ\+RFSѺpB%Ԡ*@^y[ +~ 5ioOa*z{} c՗ϧӮ'ưf΢!??ӨeUg%/"L|ssST*0]Qu(x~~fWtj PVEͭs޻ "DY5bK@LߺIj/3'8v:5iGyݗ72GׄJczFL5'- 86"b ̝@U2^k9GEJYԋ}od `{Gm0#T]C !)D~a ,Կ2~VKɍ;> @v2(jՖ%L: Ӹ:Y9Tb4a!1;w o_mURrT%府aYv]x>=Op[bzDk4Vߵ7m<ܫ߸î9޼yxxzzLZ1$Hk_ zT)fS2P0Sz,J (1з7xs5ov ӛ7o~TXIJpbm );R*Po|y:O1|\I,%gv|L_rATa2yU\,󐘈T n9Hd%AHQ22HAh_t}nBqiSΐ{׀д-P95mv~O8]RLq/zm#IH$EY{9NwWOleY2#Cd %ru\`S$xܸ!^}y~FD`T{26?3FAEU)?TA=]xp*7t  f^$mۚn à*!4øzuu16Rru#Ȧ!yRCL2FcIIb}S̐Y? NSGl*o)i_qd`TtTMЉzgAȈWnwo}?\$b.Lin7HhTVH|äin\,w3o |y p@2J"%UńՒ ^{AeML`%F>B|J 8 #V+lDC퍈"Ad>)Fh x^q?vKFDIZZ#A݂7C^a$*^( %9 F PNhmovu$PbkH* -1#!c۶3,˦b-ˆRi>.}ҏm׉0cUHB?DD .*!w>Rzݏ?h ۔z.f.g8E?H#7J@<#QP|r/ ؎ ɯnG t% RX RSYU&4TIhg(Ռ6pWWW:T9P*<Ǔw:-By/Fl~G\gTHvq~>vЩv-~u*[,PcDǨ BJETX(JIJA%(& Tj$LPM|,a4 \K"H*JpuuoiEmb*ÅĹ9)bZZ.|yvsg8D~bJy@RGVj U L9$"( 4d/R;$f?L18\kIq\.~\8Ɣڶۛ< Hl-!RRXEDt4D̄| Tj|\OHjG@KRn*14McfK蒈 Tժ^fc9< j&RPUx?k$ |3hVH  Q & ed7MgSS գZ͕1bi*ȴdJJqulV$:wtYtPeS sêB]"hp4%B⠖ 3KVyTCKu] Hf`2j<9m @25(Sh}/") u]u5  Dm.7C߽{4rB)IJ_fS3N^׿F^q07#*ST #2Kg_` (v[U],Jy̢|}#_ 9dUyw]gfuӶ*à(c:iL;U\*N;Lg]K`nh9uP̆8Ǫ7~ :#hM[}oOXή?|/v0/Z Z$ǐET?@^$f umG6ۢin\.Aڦ\غD AJpՂWl3JUE/R ""1w$ b=Tإ *z}T\3M (Q,꽩FD 80Dȥ҅W)Єiv w5Bo?+( pU; Dn RQ%>%fƘmvbщ}Jč{Db" ,2Yz;NFzgBAəkhzU _?緯_CnuJfc/803[V{BD #1u*eOވ.!ժjy\,gv{U `"2#o~F'@~+(. n_=*5gATb[m6QX sxU].WmZ0Z$1cmZͣab; S򢎈Ȉ}N!pRBJ*2+!⋒waQЖ뷗|~FvSѬKbEE:*@ 4` my?CӴm۵m\.30 &@Y~pw P͙*QMif孎@}KDcVplb XwӴmۂ8F`REDBha5b{"b)XR'n; ~oGf| :32 6r,A"RȈ6 ^ygn: >_V s#bSJ?e) s~|`O#Aw eʈ[D@UZmy)KH]>\_\2DUaEu]kܰf5Mc8 3 YU(g3`ވ-\(!2_כMuL87oTBMJެ, rJs (eʄ]ӒKr27zivٵ]VkB~*!EnOVju0l5ýGpyTV I eWRE Z@pB{Nх?0DmMoWqR;INk@j|AGM(Rq8qqLc1 픋r-+WDybȚZ'hŌd]s4"o~ݐ53jt-rXٺ)b##0PԅT%EZ[SHP+b]VVH&DyL2K嗓U?mgv T3&PuC1 m_Z@J DDY@aeP]{S9!Ky!8 ̐a̚pDU ! V|6H8/#~e\)dɞoӂ #! GRvщsZ q ~73 %DXOdYI?|@B`Y ^=hj^1R.;yclbvxyycu4'E\ի5QH)M10 DM.GSM:(1zc]l;%Nnj2riZJ3NrTN Ci9E -/VIUaT~c>ֹ smJJdss~x iz :#('uzHZ}VJFS +JT>^J(Ww&P(nRҮJxB{UU"5Y8!lVd+~_b)EDl6I A B%!zu eBDRQmr@ꉂ>4p{^B$~lCDQ< ;)}B [ (QeOܘU*8u(6~L:n?Of5*wYGCmۚ%0#CRw4I1Q2`ΞE L>Tt]cJ.nooMIUӹ)e5y4snvYs71`ԹGÙk)p/Ps`8pZ07D!jfLOD4F aJɓŠ> l/ûu+h1z(|pXR1a1{ D h_Ni4֊@A`%" v)W^nS)2(ȋ<̙Rt#v\.SλEBh)S-$ J`h,v)t5F'"t?2gxPgК]@P.?/ew1~ob"Th % BC@=>i~ԙ8(DU Wcꫯ(9V~fX{0m۴ %Ք̷10 )18HL5X,0#q hI+db|""q߽~T%m-{XHkMBm% 14Yj;3+\tb1 'HPsmjEgeP='}(͌iŰUTADu'MdAX-8vqrQFZβ"V0J"f>U4qqSJ~vkt,sy|BDnooS6EP:@Y|2}E?Kœ[|꜈bDL +Y`4R(Y8r zE?~EQ/KW(>nnnnT͛7H$ְ}:-`Ӑ1Ŷ4imm[d}T~$&3Mv$U\#(o&4]m[+qmnnn̩Hc&ԱqYEwssc"~@ǜ9ſ<_- "lU59Xx Bz[N{yK(~_\\x@Ea<#|ى !L*})qEbTRFsLF1ЄiiWb@ ޏxs{= &iƜڼ@Fe)Rl6.>Qf,X<7o.l49P=j'7V>Q ;C/[O \ ˛pXP_obsE="r4mmJJDWW!&o: K(+r~8ð}ۖe[ᤙK T`xJd#} ~xx p8$CHc [^@D3+)GQ ۅjN~6Er3XUqZY...J k8b9cyVf D %eڹm(Sf#"8+I\CSٮ)}W߇~1]WK`+%Ss;0Ki3T4Ϭ8 o|v'P\Xf/~>~ "bN,(݅T}߮©u r[(n*.R~n&k_Ów"+Jzܡ \b9 u+*( ݺ(!&߿^UI^jpߋZh7TQTfPM֤~37'&`gp1VUK,CL7!Kl~3 ^dD%u#=Ԟ}S~~O;2LDG5Uv6y`j5"S܎Po"wQ9rO?-s}ui SPP'93UasHPE+LQKMjZZ{vk*jVx*%fࣆ6NB~5`szUBHT ?.*'*@}'o'uor66MrB2` ]K-vf3SJDnȏoP3ۜT&kz6~aXȟ,~ _r 5s J p68=>EDԐEUR.UdI!=@ٟf]LBQn4ZOnƽ}*ޚyۑV,ZS$G/(zϞNxQ%/:DA$ JDu-Vn?9Gu~x=h-&L+&mv]kqXm!?U^Wr]9Vo6~,ϙg8>^<_UvAV%!+iO e&17ZJK bX#RI0u,y >n#۴ }1b?WS}}QZ_֎Gj>^z8OS]וdΐsMjbs5_Ceܸzf7_jYvP͢Z6ٳKh\}-Z{ONqX(7gT}ߗdu!E~:f8 bg}TUf)h]~9i&P6ۯ>v|Db@x1 ?uff dUSTooI;}B(^xN4᤮ڶ৚hgz2i,||#g7> 9@`yDheJJ9Ϧz˓OI#U~' [55:wjqF[_>ߨPWó9q-#cbò}+$;%zK_>GbfJxl:֩cmhO{ԯOrFh78sFL(Odꭦ=}QQ kJDD_u|yd~}s۲k<8tr|*EM["̯:jC p9lv7fuNJ(1[N;Ǿx-gNxHTVGH,UWqZ+˘Ĕ֞e^_m[xBb-+bZ$%`@F!FpHMoug%)%C[" ?52~p߷Ml9w{pUG6*pL] ֋Zxp>S2^( PT+ȭL@VňMo-D1"ZCVR$Ei |?#fKO8B5flz}}}so/l JD45Ke5 FDi9G"BE䀔`2i#s: sB5ū8'9lj P@R_V) \?k18PrَW l!*)*]"ET6 hn^IAzt؋2NvJz㴏f c2NaQJ!<6fUq/bB^oY9V a;%^/1/cndWxSꆍuE[_H"vE(* Dҡ#l۲lS_6e~iY.Esjz޺LA~ GDGp#k7ȳ! l琍4f7WMe l,YN5}H:HӒrμX$ Sb>) ;it· y}߃}Tc^e<8_En0NP8 Dh$sұ:Gn7LJ0淞җi ه}̉?ErܗvQaIENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/modules/0000775000175000017500000000000012641367670025331 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/modules/appconfig.de0000664000175000017500000000555312641367670027621 0ustar jaakkojaakko# The Doomsday Engine Project -- Doomsday Client # # Copyright (c) 2013 Jaakko Keränen # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # 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 # along with this program; if not, see . #---------------------------------------------------------------------------- # CONFIGURATION FOR THE DOOMSDAY CLIENT # # This module contains configuration related scripts for the client # application. It is automatically imported by Config and used as needed. # # TODO: make sure the server doesn't run this import gui import Version import Updater import Log def setDefaults(d) # Applies the client's defaults. # - d: Record where to set the values. gui.setDefaults(d) # Additional Log defaults. d.log.filterBySubsystem = False d.log.showMetadata = False record d.alert() generic = Log.WARNING resource = Log.WARNING map = Log.WARNING script = Log.WARNING gl = Log.WARNING audio = Log.WARNING input = Log.WARNING network = Log.WARNING # Alert notification dismissed after 3 minutes by default. autoHide = 3 * 60 end # Input defaults. record d.input record d.input.mouse d.input.mouse.syncSensitivity = True # Defaults for the automatic updater. record d.updater() frequency = Updater.WEEKLY if Version.STABLE channel = Updater.CHANNEL_STABLE else channel = Updater.CHANNEL_UNSTABLE end lastChecked = Time("") onlyManually = (Version.OS == 'unix') autoDownload = False delete = True downloadPath = "${DEFAULT}" deleteAtStartup = "" end # Console defaults. record d.console() snap = True script = False end # Master server settings. record d.masterServer d.masterServer.apiUrl = "www.dengine.net/master.php" # Resource settings. record d.resource d.resource.iwadFolder = '' # Renderer settings. record d.render d.render.pixelDensity = 1.0 # Overall pixel density. # LensFx settings. record d.render.fx record d.render.fx.resize d.render.fx.resize.factor = 1.0 # VR/3D settings. record d.vr record d.vr.oculusRift d.vr.oculusRift.pixelDensity = 1.0 end doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/modules/Updater.de0000664000175000017500000000216112641367670027247 0ustar jaakkojaakko# The Doomsday Engine Project -- Doomsday Client # # Copyright (c) 2014 Jaakko Keränen # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # 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 # along with this program; if not, see . #---------------------------------------------------------------------------- # UPDATER # # This module provides access to the built-in automatic updater of the # Doomsday Client. # Update frequency. const DAILY = 0 const BIWEEKLY = 1 const WEEKLY = 2 const MONTHLY = 3 const AT_STARTUP = 4 # Update release channel. const CHANNEL_STABLE = 0 const CHANNEL_UNSTABLE = 1 doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/modules/bootstrap.de0000664000175000017500000000477612641367670027676 0ustar jaakkojaakko# DOOMSDAY CLIENT BOOTSTRAP # # This script is executed after the client application has been fully # initialized, immediately after subsystems are all ready. No game is # loaded at this stage, although all plugins have been enumerated. # The window is not yet visible -- no GL operations can be performed. # Config has already been loaded from persistent storage. import Version, App, Config, Input, SavedSession def bindDefaultConsoleTildeKey() Input.bindEvent("global:key-tilde-down+key-shift-up", "taskbar") Input.bindEvent("console:key-tilde-down+key-shift-up", "taskbar") end def bindWindowsMenuKey() Input.bindEvent("global:key-winmenu-down", "taskbar") end def runLegacySavegameConversion(newGame) if newGame == 'null-game': return # Schedule conversion of all legacy savegames located. # TODO: Improve this logic so that conversion tasks are done only when necessary. SavedSession.convertAll(newGame) end def runPluginLoadHooks(newGame) if newGame == 'null-game': return try gameConf = Config.plugin[App.gamePlugin()] for func in gameConf.onNextLoad: func() # These are single-shot hooks. gameConf.onNextLoad = [] catch: pass end def isUpgradedToBuild(number) return Version.OLD_VERSION[3] < number and Version.BUILD >= number end def addOnLoadHookForAllGames(function) for plug in subrecords(Config.plugin).values() plug.onNextLoad += [function] end end def upgradeMaintenance() if not 'OLD_VERSION' in Version: return # Ensure that the default Tilde binding is present when upgrading # from an older version. if isUpgradedToBuild(920) # Register a single-shot hook that ensures the binding is present # after a game has been loaded the next time. This must be done # per-plugin as the bindings are stored separately for each. # Config is persistent so these will be remembered even after # shutting down the app. addOnLoadHookForAllGames(bindDefaultConsoleTildeKey) end # Add the default Menu key bindings. if isUpgradedToBuild(1479) addOnLoadHookForAllGames(bindWindowsMenuKey) end end # # BOOT SEQUENCE # # During launch we will perform any necessary maintenance tasks. upgradeMaintenance() # Whenever a game is added, we'll schedule legacy savegame conversion tasks. App.audienceForGameAddition += [runLegacySavegameConversion] # Whenever a game is loaded, we'll run pending hooks. App.audienceForGameChange += [runPluginLoadHooks] doomsday-stable-1.15.7/doomsday/client/scripts/0000775000175000017500000000000012641367670020760 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/scripts/mac/0000775000175000017500000000000012641367670021520 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/scripts/mac/sv-start.sh0000775000175000017500000000071112641367670023641 0ustar jaakkojaakko#!/bin/sh RUNDIR=`pwd`/server-runtime mkdir -p $RUNDIR if [ "$1" == "doom" ] then commonstart.sh jDoom Doom -server -dedicated -userdir $RUNDIR fi if [ "$1" == "doom2" ] then commonstart.sh jDoom Doom2 -server -dedicated -userdir $RUNDIR fi if [ "$1" == "heretic" ] then commonstart.sh jHeretic Heretic -server -dedicated -userdir $RUNDIR fi if [ "$1" == "hexen" ] then commonstart.sh jHexen Hexen -server -dedicated -userdir $RUNDIR fi doomsday-stable-1.15.7/doomsday/client/scripts/mac/cl-start.sh0000775000175000017500000000071112641367670023607 0ustar jaakkojaakko#!/bin/sh RUNDIR=`pwd`/client-runtime mkdir -p $RUNDIR if [ "$1" == "doom" ] then commonstart.sh jDoom Doom -userdir $RUNDIR -connect localhost fi if [ "$1" == "doom2" ] then commonstart.sh jDoom Doom2 -userdir $RUNDIR -connect localhost fi if [ "$1" == "heretic" ] then commonstart.sh jHeretic Heretic -userdir $RUNDIR -connect localhost fi if [ "$1" == "hexen" ] then commonstart.sh jHexen Hexen -userdir $RUNDIR -connect localhost fi doomsday-stable-1.15.7/doomsday/client/scripts/mac/commonstart.sh0000775000175000017500000000075012641367670024427 0ustar jaakkojaakko#!/bin/sh IWADDIR="/Users/jaakko/Dropbox/IWADs" CFGDIR="/Users/jaakko" # Use Guard Malloc for debugging. export DYLD_INSERT_LIBRARIES="/usr/lib/libgmalloc.dylib" DEBUGGER="gdb --args" GAME=$1 IWAD=$2 ROPTS="-file ../../doomsday.pk3 ../../${GAME}.pk3" $DEBUGGER Doomsday.app/Contents/MacOS/Doomsday -appdir . -game ${GAME}.bundle -iwad $IWADDIR/${IWAD}.wad -basedir Doomsday.app/Contents/Resources -wh 800 600 -wnd -nomouse -nomusic $ROPTS -cparse $CFGDIR/util.cfg $3 $4 $5 $6 $7 $8 $9 doomsday-stable-1.15.7/doomsday/client/data/0000775000175000017500000000000012641367670020202 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/data/graphics/0000775000175000017500000000000012641367670022002 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/data/graphics/dlight.pcx0000664000175000017500000001047512641367670024000 0ustar jaakkojaakko ??,,@                   #"#"!   "#$%&)(&%$#"   !#%)*-/12/.-*)(%!   !#&(+.12456765420-,($#    "%(+/257:;<>?@?>=<;:742.+(%"    #(*/369<>@ADFGFEDA@><863/*)$   !%(,15:=?DEGJLNOQONLJGED?=:51,($!   !&).37BFJNSUWY[]^\ZYWUQPJE@>84.)%!   $).4;?DIMQWX[_defeba^]ZUQMID?94.)$  #(-3:?DIOSX[_aeiklmnmlmifb^[ZRNJD?:4.)#  !(,28?DJOUY^afjmrtuvyvtrokfb^YWNID?72,'!   $*16>CJOU\_dinrvz}~~zyrnjd`ZUOJD>71*$"  #&.4;@HOUY`ekqu|~ˆ~uskf`ZUOIA<5.(#  %+2:?EMUY`fksw}‘”}zrke`YSOE?:2+%   "(.5=DJQX^ekrw~~wrke^WSJC<5.(!  %+18?FMU]aiqw~~wria[WME>71,%    '-5;BJPX^elu}±ume`WSIC<5-("   )06>DMU[cjq{º~ria\TLD>60)   $*1:?GPW^emy~~yle^WPF?91*$   %,3;@KPX`irzż|ri`YPIA:3,%  !&.4FNV]glvŹvmd]UNF>7.(!  "(17?FQW`dlyźvoe]WNF?70(!   !)17@FOV`envźvle]VNE?60(" ")/8>FOV]elyùvle]VNE>6.'"  "(/6=ENU[dlu~~tkd\TMD=5.'!   '.5=DLSZ`is}ú}tja[RKC<5-%    &,4;AJQYairzżzrjc[QI@;3,$   %*2;?GRX^dmy~~vme_WPG@91*#  #)07>EMT[ajr{º{rmb]UME>60)"   (-571*$   !)-38?DJPUX]afklrtuvyvxwsnjea^YUOJD?83,("  #).4:@DJOSX[^beikmnmlkifa^[XSNJD?;4-(#   %*/4:?DIMQUX[^aedfefea`_[XUQMID?:4/)$    &*/48>AFJQTVY[\^]^\[YWUQPJFA>84/*%  !&*.37@ADEFEDA@?<863.+("   #&),0257:=<=>?>=>;:752/+(%"  "#&),.12476765421.,)%#!  "#&*+-./1/0.-+*)%"!    #$%&')'&%$!   !"#"#"!                         !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~doomsday-stable-1.15.7/doomsday/client/data/graphics/radiocc.pcx0000664000175000017500000001032212641367670024120 0ustar jaakkojaakko ??,, @¼÷º»ºª¬®°®ž‘Ô—»ÄÉ|{z|}~~ƺ¹§}{wtrqopqrutuvtvx{~¾~{wuqojhgfghjilnoruy{÷¹®xrolifba_^]\]^_a_bcbcacefilosx³¯~wnheb_\][XWUVUXYWX[\^_bdiov{î±®wsjd\[WVSQPNMKMNONPQSTXZ^dlnvzî¬|olc\WTQOMJIJIHGEFGHIJIKMPRSX^dflox¬«xqfaYTONIHECBCBA@?>=>?ABCBDHIJOSZ\`dmtª©{pj`\ROIHDB@?=;89898:;<;<:;:7534568;?@CFHLNQZalv¥unb]TPID?;:97454320./01343659;<@BEIKU[fmy{nh]VOIB?:8765421/,+)(*+-/1024698:?CDMT]eq{š}sjd[SJE?;87621/.+*)(&$&()+,.0./1347:>@GKU]ktulc]SKB=75230.+*)'&$"!!#$&')*+-.1479@BLVckxzqj`WLE>731-,+('&#"  "$%'*)+*-0249?HQ]dpwxrjcZQFA83,*('&'$#!!"$&'&(*-27?HS]irz~yrld\TLC;5/)(&#! !"!#$&(-08?LXciw|}xrkc\UNF=6/,(#!  !$'*09CP\bnv~{yxple^VPGB70*&" !$+2?HSZfmwz~utpid\VNIA;3-$"      '/9BNT^eosyomjb]TPHB<7/)"  $*4=IOW`hnqjgc\VNJA<61,&   "&/8BIS[bhkda\UPHC=84.'"  #,4;CLT]bf\XSMGB=53/)"  '.6:3/,(%!  $(-2:@GNRHC?94/*&$    #).5;BILDA;50+)%"  !%+16>EIA<70+&"   &,3;@F=93-)$!  #+08@D;6/*%   &-5;B82-'#  #*2:?4/)#  '17;1,&  %,4:,(!  !*28)$  (/4%   $*1!   &,  "(  &  "               !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~doomsday-stable-1.15.7/doomsday/client/data/graphics/unknown.png0000664000175000017500000026431512641367670024222 0ustar jaakkojaakkoPNG  IHDR?1 :iCCPPhotoshop ICC profilexwTTϽwz0)C 7Da`(34!EDA"""` `QQy3Vt彗g}k=g}ֺtX 4Jc `23B=ÀH>nL"7w+7tI؂dPĩق }F1(1E";cX| v[="ޚ%qQ-["LqEVaf"+IĦ"&BD)+Rn|nbң2ޜT@`d0l[zZ ?KF\[fFf_nM{H? }_z=YQmv|c34 )[W%I Ȱ316rX7(ݝ ⺱SӅ|zfšyq_0sxpєqyv\7GSa؟8"Q>j1>s@7|8ՉŹ,߳e%9-$H*P*@#`l=p0VHiA>@ vjP @h'@8 .:n``a!2D UH 2!y@PAB&*: :]B=h~L2 p"΃ p\ u6<?g! DCJiA^&2L#PEGQި(j5jU:jGnFQ3Oh2Z mC#щlt݈nC_BF`0FcDa1k0Vy f 3bXl `{ǰCq[3yq<\ww7Zx;| ŗ]8~ M!8Ʉ*B !HT'\b8 q$C'bHBvay=+2Mv&G&Ec[ [bDDĐ I* Zc0&8(&iYH~Ho(%46h0װu wKDŽ7EGGDDōFG7FϮX{xULQ̝:+sV^]*uՙXXf8t\DѸ@f=s6'~_ ˍ̮`Oq8圉D]SINII\7n5ewrm\J`ᔅԈ4\Z\) /ד>aQ1n3|?~c&2S@L uYY5YoóOHrrsNy};_-cZuuk/\?kÑ)*0-(/x)bSWr±^$E[nEmnfmOk%%%JY׾1ꛅ ˬir]+wZiYYGgʿs{?T'U߮qiݧo۾C*זԾ?=xΫ^P֡ 2mjTl,ixwxHȑ&JG˚faԱc7sŨZr}wN>8(mP{nLGRHgT)S]]m?x3g]8wn| ƺc\x'ߥ+=/_u=wvWO]c\n}Ϫ'l:o\:xviMoܺ~{;˾;y/Ylx~XHQc?:b=rf}Icda)iDӤ)ϩV<|~W_}oοDΌ\«ï-_w>~f~#zGPQc'O6gAMA|Q cHRMz%u0`:o^IDATxsNXR%67֖zt <8nۛ=Ïn<Oa] vM)Xɥ-\["jm:cA DLxy#"&}^D:@!4Eu9sJR2c h@✟c Fn_a1,@ ۖSR6;xǸbeXʍSG.R f\x;#D^L#ጃ\d5s4%H'ѣxϛrNf{b`V!P-$E !Xbi՗oMjcm= M>1ҙחg,MrLLMN` DU)Jp!,z,69 6Ъi_,"3^=<>=J6We h DvH<ސ;_b(H/7 [pLe=/DÄR~u!MC1ZE<.-FU\"ucbljmhCC ҮGG\CSs*.PHRx_)N`k?M!_b5/Ob[ We^%INr3X~q*]Nx޸P )ހgc_OD1󧊧 Ԏ8 d( APqD;B15  ϣqi_MsE U'ZU7ӝr|xJلGh+?y!LfH>91l ٍMT3NOw0.q $8ض=x~K~Kb\#Qq p9a3'5%֛֍Z KA8zP"ϧ0GX2ʄ1l9QG>ǝ$*AHK)n=]Q)>Dէ);d֦9vZ>`t))ʲ  (/ڍXo-`TlC_r叨,<q-e0 8IJD%3R7afCwmŲ"9'P7Rcֆ C|h]?Yg7QAA 4u%&)L9_NZeV?I9uAgu)MF]Fj{bb؎ e+4DM^֦ViD/g{ ^1Y? jƑu fztGg>3'vHWeB(GLR控Y*zc >{F2j%Dhgw6YeP՟4@ A2tb8{ѵ%kiAhZ<RRuf^̾Q# X/Q􅺡=ʬF\ޥmlR {;^vڤp'Kk:b1@Қ6~%`lx&$*LOB:@%1:l'ԐeyRZ Y1~0&v>#6kb+=pL%V]!M[H3RCΫ@ޏQUes{}/.>|{i%0nIObD]Gf?G*N8v`uR||s[Bh )( dExf 8} Fs?7e-T-KtuV~@9yL95cqD?rc364DN+& SuzL!V$Mx7a"ı=,ūEM_4ytXԜ)Zwgfv\<5UjGdluAZnPF ͟<$9'k]S.Q&AM,;JT0l .峟k!l2 ݘ+?)kff ;O@9ާrWc iN @& C1-t]jTӤ&MLwAզ9c;szzJyswߴ;Mf|K!uFGQ_,Ӫv2;ϕ1}AeX<SH@l: NP!IjثtȂU-"K15Ӥ@¢b!T:ot &~,[8ƅ`_}gY*uz(X|2'>pV;XS~Z.JzJyאLe#̶ n9}!t^bI&cX\ 'Sfӛ0 THTU(5Mtgz?f?>MQ' 2Xϒ79^%'M4MHuxլ3B.2[ăMG:75x5Ch=*4!j :_M$6 qJ-)YKU\ܰpx'FV- /rNzji穒*Ҏ_9C5f6ByZ(qO' qVdpqI)B41v  u)Bˠٝ |yzI9Ibsa;U֧)U1o(2s|ܖ' EhmGJz[nӹ`̒:8aK W_'T6'^Q/~p3_)pcyrm(&.k Kt=Dުdq6пw-yf`t^!AY% ^Lӵ:FVsGX ږ<IDhlo)9Gq9%sAtΥ`.3J3U~-;QWL\tn/,tR9'8|iBQ fɣfONP] ^%k ܯf&^  \pEc*S- aeQT4v!l=tyeζ"S!!嵸['f!e1&'Ͼx1YcF`5$@hgƗ׷-eڷ.z(ӱ(Wa>$gfo&+ƛls\AdoHVSe3F_7D[K/Tƃ}ٓnyaHpwF)LzػBk>CRnx4ՍFbsE˲?տU +!kln`.k]($^ho>Ql-s[Qr@c@RUe 8ƣ?Ĵ IіZG[Ţ%5;CNmq|*l LJ6b|#mՙSw ( @8Эč;XuGx>'D jȱ33vf* r#=@ 3ұNqv ўOhOQ]Wr `chqLϰkt'-g>&03Sf{j &I %bN>y^S.;@ݠwTkl+0t.x0(~c؟BxMs#1 &S0@C(gPT`j , Z3\\wCwd 0WE@!4:]BmD+N6 E*嚓gHw&eY/*}Q Rb'aNIsi :Z{(萂H枳Y*;;ۗ7悢9_뀶 IRi%6_ONѣ V:ڝS3}8poHd눷ӕ6f)gz8nI8 g͛|ɻg?({gͿKل`!6E-W2]nY\+Z3i;W߮oQ/Gsd0ihV- ??ÉLMB̾:RXqP`乸ܐ6-d5:2K7$NRIb労".-Q@DvU?4h5t58O,zdrPFd"`/C`@nwAD@;|` yj_{K9"_@ѫv4Y+Kb"5(*S%*jsU̠>5:5vOu ݯ;?LONڊK."VŠݯ%XE>FqvCyh1On2CqqYu9hr; \(k=I*)t9/*1Uw9FcV%?5hl}NYMvWԡe^,5T+r$V0Z[K1R 6@tCH9?Jҙy9 v=c@y3ׯEkPS<Uҽ4IWJӱ@]!Yo 9/* 2,=i|53Rǂ3;/xKP4NNr#zcx1꿧*b3[zmD` 2a蔍vnrօ"y牽ߖVöz#Gw9J犥.SY|ݵ!~٣ΎoןSaKI iA<%U`s]BC઒(lNpG}{ V2[aSo<9VTb]\d;o@v yӤGi"Ho}pk{;?> U DouS,C /yRg S/ b?%fXD$"n09zI۾|,n V٘au%˗>;;7%Y*k G]QL'}Z}6rWcfPC8`6̺Eƒ?Ջ+baYH ch}|̾TET!áNL FP>-;3ĉP=P6"ysWAqZB G+QlA^o7ZAal&Qb hY$&@K @KwmkmbѤR=DU[5B cfO|tyX?߄OQ@\F:b.oB5v9$tx>JT< RAqq10+#%mՎg?QpP]-L;Z᱔]vU igUMF":vtgM⍳xu䝺 УB\X ytihv iŜ6̿w> "i~sIrPN_os=${DI6QQ 'Ķ }Im2R{aӅ.r:ﭱ !(@]"R˾vrez^E}nLKgv;j w60r 1 ?V Dwn*F!yǼ-WI,ds eEu]Nxwv~_[w:/RD>蚰h R/w$ۤk H8p^2?Lz#pgǙgjBY늌U U7o'*p)DTF$֩Oc2{UQJt6Ąz/`u٤$Jas'AcRh =WTE2u@PK)r|nsrlr QA|65w2ⓨj}nd#yfOIs3 զ.K/MO}g6FM 6U[luֆSg)Ʒ˄2 >h+Ci+ϣ?w1ˎ{ i^Rbr)zZ]N}StT^dy/ 0HT{4 61B7;Z$QSt̒%*J|`Ws,q1$l;i(Iq= A A 4ћ'?ĉ4~3h 7[G? dZZXgZ98 aNA?X:vZY7լmPNHvNi0}xNׯcR*`wEvggNwY 8KζW *NP@B xK [C]?iח5ꊄ"E>K~b5RN*y)T}[DT-muiq /dܡ 6"'ǟR\ і OmWz@j˅@d%9)9u cmT@J2.g{pYϣ{@ ,]ּkI 9TJw Hj\MGaeHzT@n_r`SJTT\>pq6;@ncgO^}7DDq PZ;;|Na(uXi_C j SF8!h@|Oh7H#n J3Į' 4~nW fn74QuͳC`a/Ϣ" 'ϯ{i ͣ$a1b%ٮ+NLhx$(2fiUkvꇶK@aX9D}Fv3M hZ &4>hH**:;.U , +#dZF$`) ӧ#UH 7_{`Jeu8FnXZ'gݾ֩A6 vvĬ<S+0!bS98kg $qvqڼ\͟lj*P-3 5" (lzIbScHC(Q.SMI _Ƒ[ά ^aXnUw`q3n'˓n#6a2sKO*W(,tHPs15,ڼu9^m}Fn,c8x@Ź& AԲ|.UֹP$HSUo[?Tu|"[~SFLJ $O&2gPj1ese${tat!m>1;ý͍HbCb RQ'j>Ɗo<ΚڮuVq7?lu.H:ٮ8};k,%enq1VUnF-)Uu<D%>q߃EӊQ1=\j~!QKh̾D+̜Na߼Js%fyH0+Ll4J]EYib 9Le3*@qKh =`lv F4q!fhnU}=0{1mq%2tL@q}Y;>RWbSq)3YCGβp2$>*&oԷu҂싸A] ^Z׋|r=u,pVomUg ~ qqVeWj==5&>-ߝ}湳_&XA eGJ)R"K1y( ݝyCL˶% $^%B"PjDe!5vm?r7͉E|:ue">ӱx'wa gYIRƯZ!!wf}NYǖX8Ƌ+wbzKhYf\0tKEPwg,I0Y:PZtPE]do[b `|W{:ٖY{y}GO2-`;:q<ȧvZN7&rX&ibޞ\o˾?JTGe`KNϪ*::,PF|z_$y!)mpYR(LIŜVqG,Gtz Am0`lt<FԷ/mYNCsWAsMjAw5i{lGqeExgcKIҶ&F"pNyLMLp޲Xx _hђ@ o;)v61+SRZ|qjÙ^f_{C͚q6( 0UA.y_R HMH}9 |C*mYsAHb;D`IJp TGk+m hTgnT`^+ˤ'_R 1:W2I0\%1,7'Q`ua 'EH3$DRB{9bao9]ow;ݎ-h8\*+yw vsR2ٸߞ,- G}*DF-wW<5S"TfH|S !Q4W(WX•-8n4^20G a?쪛?D$a0T-O醷 QңW4g~N @YFpPj s$NbaCurXMĢFmo,Qq-.X#9~縇)5𽞏]< +AT #mei=:|qz<-㮟 ]-C ʰ"1( b.p!]HksΞ0r3ڢY&qɋmgm[ ="N7Bw~ڮ z˳K H*l':aeu'.snĆ\Qw$0O48ޡMT\+58)ۈ<2=Q$/PHS&7I?TV9?- gwNvH8k8] ٥T{)ݎjgBN./fpyJъ,G$(.BJ3-@ it9xtj5v_ B㺐2JŮˁ__Νa*@w1  )}R!8wOc4cH,dR:; @'6Bt+2݌u=~?v[ c? \̵-NU쌆zoE+vj1;=o)X!h4KNCJ㒃u9^\LPvO΅ 7/Dnbt!9>d|ۂ\i/m~J@M022Dl!)6N#=1Hتyka1d\.AiAD6.aT~<u A ,5*EgK\Bvv<)_?"J/ "|Ű @ScUH53^)ƼܳvU;=iFyO9>5)2$5?WZ7wܯ.oeQ[b6ȩVOSju{˨;5+֬fpQF`+7y7hKDIAюR&;U?AkewL|oW:h*lSM#MKS#ii|mp$̌Nf}1e-^7I י,8:Q؃1ha¹`mǛzWa۲wL"I;%!9SP"F+#S0XO.<H_~oN2.JWUz|2%`a%( ݬ̕,ul&u]KQBbR(rj{^ώKoٻЅ,Y$tEFEc_An8&ij}/;ʖ [!w?&2 _nai!:RAEٜLrr:FK5i\Q8}&ĭJa )ۗ(5VC M ?ol'IkM@SA,Jeiִ|$|*fXdA4z۪/J 5r̚1vvR'& im5-trfZ5u #Y&]jk0`8Q4Ʌz!=9 +}齶#*[\׌lL-RfkAI 0t`Bbb?D4YecA QB{\oreRT%b8} u@0pK&*cȫ{͔TLd8`&=@ 8- U 9.^y ; 07(^ќM*AW}Nd@PNu_W63zޣ)n!olLU>|gz-N:-bt٨8oq#/Έ0 #܄mk&<@]0!iZ> pgc"gy8)F^aEQs}S\Mi:L@ C7Ga8(8:fH֓ 5^ɒ6:V n8 vѱ31sY2P&bou7K!*?,M@XkàVUꍎ`QԶBe59mRA"&~_\mW0l Ai?0:g.牏2 4|11{#+S@Ea~YXReLH8yD*W%gPUi& Vp`PFw@n7eGWsȜqN]_l\y@U/FI)iܡz.ӹvQ~T1y΅r nA<:sb J[PROe\=upK3}ow6<ݲvn L28LwM!7\PX֔cj#Q~$ =zk%_P4"CZ L/#8qelK 0?)4@!I :9b>$it ?%I}rcct灄Ͷ[nHy\uÕٻXЩQQfjRZl3C7h䈰%" .|ԭ"d@K=ЖߟUgW OϯF11_j ǗGB=_ekuU-lV ^<|<"bii|(Xb̬@%U (s3zlQqO \ψ%Ux5R`n,Ԋ8z`t4V%= HLs;|mރ$ q=JbKՌ2 QS ͌ҵBƉR/%#8!]wέC>m!U_꺎gBʼn^\ow6]Rn#wL0)g</ BճJvMEDžk?rZ@^?ؼojq^rR^W6[<6fjCӣ+.#Te C̎䃘4;鄇g220~S!Ѱ= kv6+S|1l `ٔOi-򺄧4Ċk|-Aoる H?Vc mww oMhʆ) sڙ.Xl0), ռ>3ެhɻk Su;U*Ns%oix)jD#G%?|%?TW>x:PPAp$^!R̷zbߙ$Ҷe =Y96x;5?|8,݄%-).㬢j0 I(l]b?: LWT91=xan[ŗv!F*Pw4)G p1ob2E VZXd궤,Jxk:Ac4'~h3޽ w0B~{J9' ycx~J{ Az c0H;cדf*Rݔ0סQ Rtsaa(M/:MThjY]`g! MΤ.eLpQU~$Ssy(QRg)ge GBZy .8&0-.h oTI-*ǃ&'nPx VdJ5ǚr DH98уTErSa<-fCk=@ 4l[L\0$9O`E1^$G\ҙ W1|NŮb3#XD!4Kitq؟@'~]x.d޾jBI~Bs^c!wt6ztn;0 6n!^_7*]IDZ7 ,7e@FV^G7X1ϐ8w=*'?hd!@mT>hVs0ŧ@3g({Ie*zz.X 7zN*긻DNdgwMW,ގZÖKЬ3OU&487RD5lr*`n*]x2!Uap`!$GDQڤxKeٞdO=1EtQJ"mZElЕ+_ ÏIǪ5'Yj)*Dvhӗ\mIK=Mǧז6TIN:Sb|#`X׃t0]|aro5۔x*SJpmGA,]6-KFa-+LzQi|zB_`^hy#oiezA'sO7+bˬ^5--T ‰}&7|`G=A_ΥA Ht1 .BWmgiB$mڢ/[yvrR{gg)V#\g+mtγ<›v 3<$*ql%z46~цe:*v OGù"(+P "7iDԖڗ0s8*PNH֭h$ߟ%oĶmML: zl:{*ȳ1+!B@MScQ K);LX9jڴD &v/_B)bx? ?!$N,jwYN X0Ŕ5X;,U~0u+ @$( ˪^ѓ.#̌uŃLw:nw?T.x=˔( 8 ӆFg2)^(X%A掤)uvi?yF#k.W%<)xMcS3 G mʲv̟s,ə/Q7o716 0LBH>Tׄ?GhbI~i^lQѩ$ТScS;?Ogjzo6w}8!T•qO0[YwUPQD_М:=K@ޱ+trlθIjM=lG,(T@ef ylFC_Ih,j;Ag'vVCؾ7o?mbg!B_OG'{ Ȗ&HchमFH~򵡥21ir@nj3^.bOTt7zro@C9a.x ,;I>Ύ֐OtOgTG82|w $dxT,-L#82r+!%P`c+̡W_yٺ:k)17=pl/_gwᘠw/doY?>ȲFF`cRp720@Ϙ Ca~)fZ;k@j?s!: y ;MmC #6&N@n| pu&_lp 5_լ\# n!"|kįAM)AH  Ju>0~| :"9!"Hj׳ S0LQ^^sSCz^~a`'Y l-Cvde/` `B} R!_'Kw.] PA[5||p?B$CpH  f4U('=nKVn@&ܷ=,ɂ(PW~ IIU MJ-`(!n!u!i))H@gaYI܄+W-X\TC/`lO*Mt!ꁱ&$& `̊og+ nn4g@G⃞ cni @LI"j0F ꐥf{m@oYb@V,#e.u=1Yal[׳!Bؚ`Hq/Mџd+AYY " 3> r h HuzCW@{"omoޅ4 CVGq*d2H CH`NCXPEx=H 0xxOJANO8u"(2-N?Xv0C߸WhPU#7]d`nlkr7H= ; nǃΔoe> '2H<r:g<dH] J!'C<0t ylHHT8>|k1 !~I^ )\ϊ%*f]* ݰ 9?bHSrR,Ѕ;@IyRWlq͜7$+43*5'8;j쥟?o5@;8@?|9MCB d;@Gp4 z?:?ز.`>}W;[j*AR:gs@76rdM =4 r&fNHƀX ^"("9" 87=tA=%RUp쌻mzرL `Ɉ B3҃zy)D@k!}Ѓa~s^e ʘ0)Ics+#@h~OK)`Qa% nֳn_맔8$AFED(P1;E pvN`dD s9N&\ j M ?Nx d\ҕx D$䀮D@obYntxdmCmMiaCc h2[gkI>bXIr23+ȱ9 U `f@w@[CQUoᆤW30@7n- .*XZJ2:V#AZ5tOFJOk'{Kh ;po=y TCtӧʆ}Ck9CCfKy]7dN T;`pa{P4!xFd%!a7<ߣeipPP$3d$l Y1l?-$ޘ Si@F@OGCÈ tuڻO`M/7k7XcK+%ft!G6YrKh'/H  m2AБ L!r=2 !Y =aں! >^ڊ|K0EAlOBk] *Օ'6>` ߼ x~N˲.7CYpy3i>P/x2>"t&tRG ` |WA x)%yIH @:D[^ aCB`@A6Gg̿!`]pc_p;<:&^!xfvF0CgAn.Q|;1AHA M r 6t339p Tc>wGЙ*d7y@ R :aT: -LݫgXPjoNWo'Xazy@;{*9!!SŐidOo*S{`?+K3 :._/L u8 gQ@u[Ll- [u.p 3 lAtIK;Y0Ip~8u"eaflBЁlP X}h:`Hcg/^ON z r83/𚨯}aб߿ 3\Ʌ.zOv{t=+<>@7|A֜A&5 = k<_>AY&qrDd?b+ @Z6#! HCD'H ә ~ <4]xw \~>f0uٰ/ ɐ.fcdAC2UMlyU`[IA{ J9@K!y#/V!#@Çuv@ZQ\ [ ߼m?C a, 58 9zo1P6L|EH1 o599XBf녁hAdO)0†kPbgjx8TdV#^ๅ'Up`aY#A]>:`E 3hI(S!ѱFH}/+t.(C%*I褢uA(f3{z]ܙ> +%z 뇺jp#[!$ lOVb6'{>Mjbqv'QviU V却IlϏEbxDi(lzӽv\^s ̚/3M,^ti#å)/lS4~\?Uw8:dȒF,$:dx'B0YZmWrckEK&.&G49VB?}3KfrK?4^rkZQoټ1k9'D]*4p@unAb6hLHlR‡X-1P&VFp 򈻞9geb{YvsɽsfѪ$4lΎ܄̎mf(A5%UZhgzwXr{ C+wMYꐄާc7n[IYY-v ON/Z!~0/J R~!{%Yz׌țjuذιyw|)Mi6̥fO-XB>WVx(^^4cJ7}Yt8̺wQ(3'[Cp9fލ&%CyPT eUxsOB3R_gR[8D N[uê>oú˿pu5; Ax)4PLLL8LN>'&p"<\izkl .-Wz- Ս #Wa(lvċ4j'c5#(>wNei 0K^lwlg$y'_7yHHHimD3"K!DDsYb2\ y "'⡐pu AP@.pH<hH((ˑPtpHswaWe[z=?Pnm%ڞXuj|T#r##ou^.7E1?[#̨W>*y\ܟNiP $je>^)2̳zn=HatZFA?~oBr%>=r *!8X C?zαfZ~:!'^!|[/W.O2q ,Iawg{kgaeYOy%_ǚjJL>@aơ5ye/陒s] iFyXtiOHEƙXndfoDpD9 ? WAp; 2 ntBo,Ԉ!dA'xOlUd-_f<0;wسD<7#^CM_X]+7R-;qD(Zv k I!'AOBFvڽ}!9zNB6s>d Yu =6ox؂e2]@vu [[+yl@PU;A3aFEM2R*`)C!R8@6B1G(Q x!?F:̐)=+d.d #P=@1AN\8d.V&!kΠd`}z!*hO@(C:NSBj7e.` ˜j@s-[ L`[9,j&d mw] ۤi ʒ,@*0}̄>w˓Hm Rr Q_"$pp[9h,#- zCr}vShl tYK A`?"&:%-.Aw >V HJf`٬4p|v =h5 I 0 R "BR(7n&I?0+l @ZęrLHS5䢙0q^$XƁlC ^Фh=H ̈́CFٜ`bH݆\::\C`ZǼEo"Mwd@,QzeK}Q~xJQD eA0j^Gc?YI:۩NiU!8 ^˲:>dg !pT!=Z84٘ǧG"% yܪ:nSMx4#e[LCAYPu: AO@= 1>-065Vv4TE Zƹ͎Džc7 ws3|Ss3Z{~$ؘ8 EB-Z(^S_'t rJ/Abb]}o ߚ/>^e](| $8OS!O7כ0M|v:;hVA]HzpVQ^Hwլ|,z N&(-A%)nk'MqL'CV)sFĀTKϋB"jxQS⇂!NP9I4O,*ݛ5Yr_]U'G eF\ GOv)v@L߿~bAX@΀frX?#~n|m0e 2 z =lfT'؂uUeFvUȬ8zhVig?CRB7و^ 6 juUr,+xOH|å@e@76wLIrt4^߇uX5AȐmM%+7U#F`1ӓ XB$}|p Qv5=4>vnb ug߈ʄC`U灔J@!2ÎD 4S/x.hmɝ{WRW(,*vM%sAS62jFƺYpvkr 0~k )S#P~2UYPhWoݬ%&؈$ 23чi;wL`ӻ. =l.\ #D &6FBbyjm9厅te>}%W4;\D`ol0DF=8hnrptv=l9>?> wBrYJ5-枞؅@m8Nf*޶@!YB:xȔH -d^Ood NwFT,: feȿb,-`*?! ㆐\`A q.hd֨eϞܰeDXLЬu=PH\5{޽{gV d od\*]_ L's,3m/pqps9p GvBN=r_=Kds  {' ' P 0[›7piv|Ήps3'6@~œߜQ ,k?||'pد_<NAgwށty; A+:f)A\-)ykZ? #I4VW/Ғ dŹU3wZ$B{ڿQ x6z =#Ru/$ϝ^LNɑw&Fjrw&R"~hNޛᯗġcoMhiLA @4~::*%WE5YIxtaXR'@kkz— tUiΕ|3Ia$TW@NWeN:)}ps`@4P |YOȺh LUP $+#>woʚ&uۑT)mM;d +++Z H\ ڐ é#v=l?ݯ^Urd/Љ7AO: %G0+"Ț"Xr? =P6lNV̰d^0-"*!"zD^5sxH?RD\2c`&QSQhv3?Xy:s ٢+VI1pRVB&?Sxqw,\lZ 2 IK>IeI5wAν,o腬lLHI iI :"梿BjBnڹ ѦQ7_io}XXAV„v9ݙp`\̽w޻soL`+5Mr( okRkn>!;} z̻9C#;o-b")}<ӳdzO>MRNYcȖ 1bn Euщ.2`( ?H%F3_En/ou@oHniV}.cRѩ OW[&80kMQ} yϗ)1ŀ'iM a.3ب&H0Y"Zd9 r1RuMHAoE޸ 7\_]{x鹪" ڈ dx@LwBX|o>VZjS}45XQ|m(vtpw&1^˴"m0H$q4VzWt:0AT Atޱ #9 p`HSabۨfe2&7IINq;y L&fCG̎Svj(4:r/{(M`U£"GˆPG2h N Sjҙ[VXGrsX1H۳lJURu\}t@eza/ElZLއ9Kv ﷫Z =],rd pu?vqXSdzϗmf5P\KYRp^G(-L5dLzT򏼡-u{8O`e ?-c:a kA+\LFݨཝ]{~I:0fS̛ˁF4gxv"{o L әrjg%Bt?9Ww~Bs1uoМkel >uɃX129~=f%w5<wuTLc" xUFg-^aqBQI 7& ;9w$'ĮyW%amB Gc4-`ϳA $h4{n8M;iSD4~>,$6%?!+m;2~raI`M 0UI :3.[- dXEH[o3&"|BO42V!~'aԴ#jy}uc*zg֭W_CF;#+D紾 sn@ͮ>Jª'wNU3ȅ!0Z4ö^dPtoTbfD<9rSDGo m/zI۳:{a7 f@*M}0?}9}vcFp {^^T;4y\^3=\,pאduk8&DMC Emoy >B[ W֩("buguWT4#wF!4ܔK{WZQ?lpRKy bJ˜ +e~@gabޢ[sӇ^ !eba¥+>*:ȃ![D2GejrѶǯ,U=l| LG ;iS_ w׉ v5m],B鱜n uv?έ:V5B'Og0 &W_Dv c3Ri:?ǎm}ufFn")m~WA8Kx4[{C [BVSuɽKaG.ɈFx41\g;%IqdUX1̎b+t3bt$A=@j s|TǬq["W=o0[Pm)5S+l5/5.%cO2B~VNbKӋ"dTjlYO3lr&P\+i-6j(C]wQH}rS!G}}=)vќm Pry2Env&o_:vCYmVf&k0MsmU9 {}2t}| >u [C(w\S$1s3u[}CayarIg8=,hȔۼo䥹|PqEZRW"Y]]4aQ#We+*RS`1YmB>dW8ͻnW7rڝY ;e?|BʯRЯhG{H0,OȾ̐23JdaJ''4,Kgg-Eՠ"^$}xE8E$R̔YaJ~0u.90IK,q!h )<'eGQwCת61dmyG VޯN, ?f+ORlJIU?oݾǔ$3aQQ:]zD+ |AaQ 3N H,#甁%g{%9^6[[i(CPPn!3*u4\ʔJ+Jev`;5htuQikT ` al|,)t:}r=$0. &2tЊo*2q&|(D_(czGRe](2pɂ.~lf*`}Zuxz;H"LZX {( o!я<=su5nRRm*ف%ᐣ!ǀF \ra P $X'RZyN˺dƟ|rcLqHLQ'"T\H|=ok4DxLH!ϣkĖcI,; YXn`Ve ߚ\=ˠLIֱ!/=z( ~% -Ca|X]Y"1pSV E ɗQfԳΗqZ3pF=oߧG%m`PLQ; 88J؋+16[ uhAG̘JTĥfԪt@1pdc\e'ٳ&uR/Og0 C?$nIjّ((Qi&tH(Z5Spv_#c9\72=a>ڏO\NgRg6b?_(Ξ2pV>fDO_bg4*)z`JA\!&I:«KKI/n(ţ%3D[GcCOV鈙 ;t{oԺn_$>q ňq َ]He`0';Z~ӹ$NT,ijˆn$ dn)U3_|8.O[8[Xd&Α ,}W6` V A< O$PhITR۝MJ KmIET`sQCOp2 Ơs~Η^rוOo #Ks21P|h rlf"(ww?GK,Xt)[,W Qkh0AoQj0aG_6 cFXԎ<:Fw9td67.7q [4#yŚYA 3a&_-F=}?`@yg?:ca)ꄄX SXYX@m+McvCP+.j7jjvzWdܮoy{ܯ!&܈/SAa]ra O敘`g0Մ=58=~]Ӻuqå׎  z z=^`-w τ斺ϓAU- i߇Wҋ*bdҫ[Brt145ER 6I=kRJC$dVt\ݓ "IԼIie b^.zȐN$dsϲ,qqؿ3mah -Ku %KnC Y>x:/ζC`"ǀ0ogh[ Ĺs~H2Na0kyH8)K9m#sӎ SIǖK"(Va4]Oί.v#ԓ5Ly   #*y.Qȇ"oqDl.mfyю/NqHح"8K_ 4 +,B%UiW0:1ipgokܗ4~@8'wEgw;mB0-gk?<ݾ3AaR:1_iҦ4~It߳Xc3?m28Z5L?M8 Sܙܰ 'ibKxe,9>)cM3R#nerN˞—dkIq#M ~T5t![)u:02$;gbCj|o%`[fںBG{y2o-xaЮ$mBztju Q!V hP,J;TŹxsSL+ЧAƚIRqJ3NZ1FQD?e TtSq@!h\T܅ֈǐH.:DZ_i/~7@"|䕥"?\ Bg./HPQF0DU 5B8¢ <[$>X@v\!V$"y8Hh4dqj̮cfP&CX94TE/x|DC%&kǎ*&Mװ@o Iu%4oܼ.lcI''h;NƖ jӊng(OYjgVKhݑw&!`.!-H&@ 8RHN|5gw@؏ PS]*8@Qc*rZƒ̕yjW 0+O綃 Qڪ!b&1(9}P^fv6Sb@E+a/sv~ii4oz.O3~Y8(}WB "$jq-EOca+/Zb5&)=t^]O#jS< Bɑ~R2*^+ƉJΆ;W]oSRuۡDJb# {.dYRM#Uӧ: F5-0Re"^yょ,Mo֥&1<rstچL_%a$B9 zԦl96d E>g0sYn6>r# ?4j^y!)f=HrtBEnQۂ %h%wHȢí #}Fצ֙UD("q0I]0:کh! # O8jcK?UvdE0/x4 \r*uĂaOaͯIx|Ͽ%'{Ĭ֊Wo3hfhfLީzthWxD Vv?br"#JGs$Θ;a@&ov3 y0d+T߹Kp& H6*ex@&wwJ4*0`Z q]IHգL",Cl(nO,f:Ҥ:;Fe\H!^? uc$[Mjz<[\;=W()D'Sƨ $qI B bj>+S粄 CQKy#L]8X=7սPhGb87q)B 1"`ABzm{H|v{[eecfG07q/)GA!ղ[ZϗnXQ Y4<&@if'GdɭG _4CKc3v@:Jfo ‡qw6$:Y`%'7f9voZ:.BA3֟A,a E-}S*GT&e8|BT QB\-I@ur}ҼE4WDdEv`3AF$h! !#"k]|4wakI@(`P98\up7=~P*1Gkxx? NBci^>yz$p?{eNYymzM"(k&ŒO^l@bjL0=Q 4G$h,)Pa%t}{D *mID3mJJYo]π`7Y붐ͩ@]>PnؖjJ9KP,J:Y):K /ŵ{i#hΡЋ q[)(a\'r @ԙ 0*H`PϏ_LwTjhljOgeB SaBGrCS">CFuʯ&+'w5sؗބkB@QuC $\ѣv397i c -(c };8/լR֗E/[rɜ".K4N inKvWba@ԉ3B"~"-dT"Lɵa+5\*&oaQ6i%beH93ջDZC3߉:R02Aap8q!&A hץLՊhH +7"[Xp1]њ?) 9~A2ZLl0|\ne{=wFd<Ũ1S9=(eMΐPܙ<]0aȩGn8-2_撚`Lچ+4ydsN=㯖ELCe=KqÆJ͵ * >B5xr/i/%78sy or$T4҈=',Y Un,B8y[m>tP-6Ȭo?`k-C0u:0- /3=L (^GQ⺎t#ȅy5b2=JllUkVpFBGC+ yFS8֠N>+f bb(E䂌ka_<2")U8Dk+ʷRjwx+")Qȫ4 JIh)2ZjYV^`C[,PTocg\l(\2 ^`߮HByHC]Eɣ QP,7yn웜ɎzX>zU ɞb5l7  QERR4 ~l/|dq\ Yr$B Թ @twDD|/(Vj2'ߤXm2/P@[DUkOӌY/ r[@ԟÙ&(+ȯ! : wwB[tJfm)'j,eOSz_}ĝLC c?\n{FJ>zz:=pN9ݰo sA(*^bPcb4bwNK|,N/%IJB Y:PQݙB/_@iH&)OTM4{P 3_]D$,>NåYgӰdFȈN ƹ %M1t8=E)%[15ʽv LDК]fd9k1 2Aޠ1sD~y`y_K}#N[.ώM+fbkR;x*~n3zUOxyp *-xlUOщ$llِ~-9Mh39!D+eYA$Ix NvOқ[f3"P`3A*M/ ܙx0n;Ld,qѰc[4Kף-Ud6#N(-BIRqϴ:{{c:2rףa^֣6d(sS7MikS(kZIs6v>gp9ZIE{fm))rv{AcOC18)G+qrF~7\+xUg}!uՠehk[-\X5/|z&'oMך[f{nm?G2 VGЖԾ粜1Ң5P«9r$L@ DY6AYymŃFEng:n,O [Svo=Q%/c=rrLng AѾڲO *S^n+ ;7RHpOE(8P~Κ|ʡh8[%Ɛ+1֘Lƒ;j"D!+hU<*zʓ95j{EVvE|fejR1qk3b1v:|y>ڑ&s^WHz m w< x. K]6D@{B=!j&He٢80u;0 f &CB" ! VGSiSqG-q S;)r{_qHPnׇcMʪMjXa*(izx5ve%x mNm%јy\ǩ׹g0: Dq_aHMx(g҆nIjP0ϠТZmQe#g9Uy l۟.Yc 9R6I!|Fp pU 9fHM q(M3^] "9>U@3$'(Wc܄A׻,>~ƿ.D,k~>Y痏n|/~;dz}u!]d?;wQkċTh՚|TM.l3L23Zob2E#CT#\rC`LbI)#LH{cL1KusK ~?CܰhX&.q,xZu(1;X8KgiLJ-gI]>8Lo# ŖV}BB=Ɇ!)>5Dtt(̲~ NSt R U5ug αrWtEWs"!^c_}͂I =DAҾ|I jva=ZlUGc_<`jzh Iɾ` V jZK/"x*/Aڤ7 Dؐdgwfg,p*V*S%dNG/Mm$uc W*]]0ԝbNׇOϙ:Dh}W#ydzn{#^lwh:+Jf0jQu@$эe ?,+ EqLleltKْLuQS?!"~"s i9?+U-OC(S{[($)*2}2$s^t׏{DwA;)D3aQ.Ɠ`ʆ$oRDZpӱIُu6 1n}IRR 9^[]i>iP",i43z~LF^K;Y %y 1 85%/ݻbŅ!!Orw^=2ܹ֋E@/4 jQCXGy.ƠQ3`(H7"ώm5 >Y>c>hP޾[pK`SzQ=c1gY,쵋..Ӿ/ha5q>xYA W| ݈d>iZߟ/'٩Oǽ⯫bΡ!I:om|+~s`g5܌j`]F9%=ARÂkUS9Lw (""p$5@3Sca~#We6l&>I,p$Mb&[˅xȢ3Tpq, 5!u1'XX`ȵZ'eg NqX,ܠMgFEs.CM^R^u܄&BV]J͏ ULl'8'} X :"08zƹg4 vyInn}ia.!PbV<]Ø#Qt 6CD;ulmj2r wB-_皂0]bQDRx,d&3Ҳ 1⇮rb62bX;)J*n!¥GbۑlX&cL-zb9jk>1)A7k}{3> ς1J<0 a3<4/T;y<l[ҋ[IPpiK] }`ށ^Ph8Hk+v;d30jBZIr~rbK-d^2Ɲ|3:׹p nwq*}d8mAbpxcAbkiՔ|Ḧ(\§tMeM[]Kud0: KVbO+:k+"<7x⥏&(;IX%AӠaq`2D >yMgCfYM䔴oPK}ȧx8F} (@E!7W&$=$L;ڂ φ*8`TUr~q͞Դ ^ gif.*kll_?7zidAl18uEw}p+ ]˕n?H=6|ܹPqh٦R9T @ D. }*XI%6M&3 :͇, =K'u9]bkI0>5ğ?4Z c0]x Bw;lx!2m X2JJ*8aGYt݆_E݌JG5C*3z%<{O-IGUܡ8 `_hIk0 ZM Js궽HSM3{H Anyٽ$&\ve@KӢ.;tDHޡ+bvNFi^jթL;:Ö0wJ\\cZ˶1 ŗK".tZ~\^Ͽu>-QWJPN]?SF fn;CUPDq޼zSkZ b|XK|"LjAC8n Ko`C2c`Fa0 -yrG.!3(_]`Zs>3nΆƗcӛH_,Sqn!j8>?֫칒)`Fi23|| v#:kܛVjqU_!qP$]Gd1; ƁY5.K*cgw7 U L+Ӊ!OyCթuN͢L *mۗ7g{NJ_b.@d빇}gL]e #.{]B:>u { XMBË厎ا^n}>9-~WB;>"W=*@R۾~rE/v`!lj)^⮧y5c^gϯَOqvK581a|0ז/&~hӘ W߭kIS\w66͵y)sĽ]L_0/Dl ܮz ųu^1͛͌Z^6DZ19tcb+n 24߄Vb.j#*(?s&_KaӞ4MSѠj$Ň1H:A Ʋ |?۶q-M0Bܝݢ ~LMVcT %86[I%4< dwDu9=yXߪ""ymHRMs Gdܳ~{Vkt:UĔ3,c@c8D/]f|1R^QOPF"|ܼ,S&gب,"hnT'xXp}A<ԥ33FPnc7r0MчP7?6qLv=&Z}]!݄=A5AK;tE܆/Qg0 ઴ x, ?_%UvQB; H=, Ov_@ӏ>0vh̉Ę2r2cH'W~^.dL.,S{!@LiVAѸzŧ"u6'V3v0a}۰9g&WtmF!џ0̺fmCÍz2=JJ.*9vdZS_HԻM;,& T1ulSйe̖McnwUu|CNg { tp%:Cp\R z-Bx0Ol3x}*\/ 3AhҴBBbga(Ρڎ{ϗ͠>ǛSH%V/h ^`Hf_jd#5\e.>Pkq?!I<7w9C 0 /l+r57 c%מ~s'^c@(p;}^k %\v4*)Ӎ SW&ۦ̀|/O5xtU恙jsmN:8Irq6{:{0BkNwYZ}tm+`b.h˿!)Bꠂ%k!z#,-1d9)מK<@ CYpPnseXKIQ! \_*WVrqwN+b<2yhɜ':l2 ܣ2`G&z* nn:3Pr]Du?'+XRhRej6` "<θ\:ˌkHdwmXafEwS~ճzKؙK=C]$x5Gc:}̀v'G2@u+? K2e"zf˥Vlu.%pF4(Έ8Pфf+%Lw,]։(_GUXEhzBK3D9zLlYX|OP8Z ra 6 &&{ЪƱP"uI x "њ@1!Є5k B٣?XfƀiE9Ya[ \fk fpg{ӻRMZA T0ǖS~dh i*):$-L8"[k\ݥJry&7"7-JFyAP}||bVxL {ʋi\w1kzb̎ Q$`dd+z*vWy׺r O A nyQxj%'9A4Lfz,c ^HQ51[yW<.ײ ˲pLj'~I‰ZZilܞahBTD4t,bM]R}h;44#֔"Q5a4YjcEtzs 0^6B !r'@U*Fw#ժxP\B><&RKHwzs)d^lZ5cs|]b_Vj`N nh鬦gO6@,B-|J4N%~a|sD0 De9 BB-ϳýj/3c<_))*q͈8Cr`RCμ0ި5BqҾD ]z,FD]Fn2xn&X lk/&"I754J22X.0DS}E~8b2^:[6j@@fO,a SQ@G t蔉_2XJ8mZ sr]Ԉ2:"7bÚ[=``5ҕ-E0olqȓnfѱbkb Pm>o*o5HGm kݹ 3],;%-E&/EU/Z4PzƓPh1*M%PE&duq @~rg=ztiI_zcY4<+fG Ppj̊li+Hqu1FpIU2ly߱~6] Ȣ`ܑa āRPQPq'>kC"E&YVz,g b4)3B(nj=|X=$#fVSvLiCpng1ץ̹z:_~t%900-BTW]{\AuǞ(_gkw "3ٿ9(ncB렱#OṊoKļBs!q ŀ a1$W*۰5pJ0ne;=\hxBN"ȵ4\H' P9k'Uܣ- 0u՘$9Cd2s'X#9s~ÉD-tIFJ^l#bK⚿$ S6,Lu eaa3Z&%Aul:Z }XLxL2^IS0ѦV﫥B" 8e 4}#w<`lvaL@? lT\4muq#gP^@.Wb*exanQ?┖Pf"&@ ]t( nRIV95)&-c?[@dmVŲv|)V3xVmT62 .`}-:_3<:uSDW֛5 +:Y*8Ty'RYS].qdMa=7KL}DHYK]#j>K[|b.hTm:)t1ȹʟtĿ3]o7~m,@)+k?)F">w19Swq  @ z:jAԖ̛, "&L& C7 ?(,uI*?#4kMeLtp")InM pŷuK$'!N9oˣ܂y`nr0$Q.[,SxǚhzIePkCN瞱/~ϚS-N|{$SDY&L=QS"@#Tws~[0Lp:RIA@blU",HX bZe?̦"6G8N Rp LEE uJvt n쾼mu$yH:73mbI ChmaUG}kbhƤoHd0lǎ(Q2͋D[-N:pCiJts.М~'j{t9Urź` &p] )Kk8>KeLDFu٦xL<0U#5ĵeHH()Lp(&.cdf+sGBa~& pJ..NBVO6mI+kWui]oޓ4| ʑޯLuv"-cu UF(#? ^KVq&?,7WȍpC8:_C19ɾtiUvF4ŦjJ^WFL/Jk4ঁDsiTԡsr-sJ ~ RBL|w2<2Zzi_Jo%&,ymNh0' jUz[[֞4QIz^[̋熰jS|u&;@ MV ! wEА̳UIg=nE.@R줖IcI $rZ@`Q$܎.$ҼciI}v4#>Tkvѓ!Vb?NP2QJhY,N45J|b%[3) ;'7+'1w&ۧznD2f K +%*Av4^g}?{ݷ-b$}[X%>}[mgO['-3ME4nVR+X[ rt$t {rnE^Hz@ѓO4#4M$ߒN:a+M\޸ gƲ6?τQڮ,«Yv!:3'[ptOSQ)vVk*Z.ό"!qbxwK,)\ȿQr;D 6$qB_F(w߇]zg ,rtZ+ݹ?9>mx\O4Q/s.aamfW)lHߞΥEM櫑w9n>) n:wwpjjg8_dJ#-rZhW8r=t5ڑŒ8V_: `H1a vux샙NyJk 6_imJYpJ+GdUmDB*azBrvY*,3G1!mtK}=Dw*UUΞ.Hdײ2ȇPPnizL?:[-ŧd8+`5s^JĢ0}HTTc $9J] !SsAb(.QcLTBEE1ᶲ^ A#2 N kwFzs?sC *Ԛ<'8 HQ&/v 4kÁ).4Osf2^إNJN,쉁Kmn+T ?0PY.H+0s1r.8E' 8YYu;:Bֵk ٭* @݊Rk_Pu3_b{'lB2C (( qkLڑGUʙ̥)bpBr͎!@W$9XBXU쾬"|9[u[qg+YIW 萍f`(YzQ TduԨ%ץr q,#꟔kf*7Db{'tihArG] y0 E@KR?#gXFi<䌥nw uͶ`+܎ 5xH(d>j)iWmhTS?x:N} (DwGsNUЎ{ A2179YNg\˼(dnhI z* 1}tM ^dQO~%e73"!5 ܲ(M5Cb'NX8j ݖKM QghR$LvtjRZ 0h/ E:EmiClc;%(-΍e{E>ׯFT'/H׼|h^Ze6AV)`I;T~?$ .Ʋ!' TLPQ 3m/Sg0\(F3Y,M98&@2Cc:G}{Xq)eCKbB HRDXn*`AU?R+}sjkӳyY )1{u WcWq9Q$s'~/J3xqm_kPnL6eE+}@ g-j9 uax'1$CĠō}wvD wQN5z`2qp_ jbO n3־񃚉X;qQ{L@ D@9Ay*1{"Z +/I3*X#{3qrE]c6Vfնa[&E5:RID~Qqہ/ȠCખdv*lE9|E#A:Zͷ` o'i(ڗkԠ)U, Aç5?:a [EO 65sL,6]% GPp 1J(K! ^G(Tzf;ݙk#"b\L}6[=&hѪEka]?uhyiy݆$ 쑿|?T޼۹ ԩ.&jA9V; AR]rA LT+W>RtҹG3N3:9ҏ͹Q_)6#RV1i/XY]Ͼ@}wJ,]d 6-0kL+XDβ064ܡ9;[v*#c9د٭, !3[a5RC -QԦ{f}!,wZ5+7ĬhF#ʒud ,PL1@$R3='} { )<\ C?wMvjw'n BW]pG4|N&1ͨ*lDZ;J$բ|fzK;aBocbT y .󫮆1#Qpϑ^& 6U:~5'xv4)Kn8j㎢~*\|HKy.V(kf[nb\>LaSIf`QߒQp4ZDK0 DJ?G@bX"g5Uhv|?FgSAIrL_쐔8@'0^sp(A-֯'uN݀)X9ep$_V‰__1͈^'76 ݘ u$cNS:W>C>FNwأIpolVDmWdbOR"h a4Zn Y nNw BE 8 +N#W_<ġe:U!XN#8BRymVh>HoQmV䖬DtO7aؐ&Ҁ ^ mbߝkNSjy+ ?73ҏSUe]$8_~9VCx;y-. ʌ22|imB;PD;u2\FzHNV^M%ӌ3bjO( yfay  [ ;y ߩ5dM{0.`)-k̨ٕwn[0Ѽsos>IV-qkLNlڔ&` :|ym\jIGX2^3pf,1: 10|b2 ح  ba,eoѷL0 C!1!i؅')q@4 u adұ +I*K謔-xU&f)}BD{}7kb_BT_bp8_,v]hH.тf^6%ȴmA D7\Zd!b_n5Bcg\$&䦻v-4Z~S?E{i[_^Zgw=Pg,?2/-->iHݣ)ڈHEQi3cwy^49X^aNYt.K 1rQadkq 79 k6eMr.v3To12;*1J;.g^e.O#~Ȃ1bP"JhX|t-lpưa%#S qj o?_-k<|R u :Na߿VWtmaf[OㄅU*\(,7ΕBW Wq9blDqѽ|3xb٤'eMb-MqTY|yKzmR98_A6 SAsw݆x:a(6 v֚;gnK}6gx3U>L21' h!Ux0U618U+P'c}vWĹ1}/w+)V48PM[)m^b~=In?j}{sқi^#_?2f;yj 1LϖtaO6B>C!7". J`XUw\5yc7/*UiG`N( 9Gvw3Ork{ ;q5'^S1CIRIY9wowx%s23ahίbGb/Qg0 H 6$&P .َo\omFd~ c=f$RNx=/ 5_Cj"[N#땓lkY:Hʪ#96 K.bgglHD%͟%b֜M!}2gHϚj_!#U *p8ħ&@6`0zJ1"J^< Tqp*ڰސi/0#TyU;Br؊oUAHVqxLY9~ S%6 !H|Ŵj&'U粃0K T8PML1 DL]\'KQ*buR@99Lۄ6m}JXڨlVzDL,c85JZaDyµ&SJ-nGM/m&GY偵DS'!W #swԞ@] &__:-4kE;߷v=#p&-n|\LPѶ(\GYcWF!4 ]h Vv&(͹tQv7tT&ed1/ߋ2S*+}lJBBA ./$KΠUV](^ و`D54& "?]px{KN3\AVlT|'TЁ(c5/V\z]O][nJz)aPV鮳k童.KBX<}YJ4| f79ZѲYclVM%ß]8"p(Yc=lpD "`E]a ɾHvt;ǁ{v θ5lP`אJI Cft3s^T+po;(3)DK ~7Ծl,ZCM?F{$dG]˫E[8#1.\ :{b;bO4Q 'BHdEό0$dYBF0#=V4]t  4-)l{tńz+6<ǁrԂrP]908:4l7KKi'jʗr15'cgeۇ7baM&0c,޳X'ĒЦ! ˾`9Eb/%{$ @3N1syrg$ A)D|4e' I'Xq4]:]movKJYtH')]7}C1XHYKM{*Hv}i߾| \rbZ: 6`Zj?g8@%Z&'vA SK3ݖn1;t5urYw!h>6^~'O K6wIt/BMmC:kIP枒i-1+S8dmŝp讽4W"!."9s?fٙ-mƝ4%w[j87mei* ppojwn,驪T[ ,K火}v9=c6GxB.6I@m.9iIS1 46]c;L;P4e3PK(eǖ2tYU `uaڊ !!20ń H0W]ۤ;jzC  s8+콛l=NJOhwVH1l(bgnHY7 zs ȡ͉ /B6,!0P"¥eUT/ GK\1R!NI 15WfcO3GHgajY ;Q@R)k9i_3[dXlwmx[ڤFlCkz:zD2!<ar=l1N'(^!} uW~ 4mlR u7 Sڜ-KQ&ugaXnưz}^mP[P+9..Gǥ;9;jrx z,.p9vXܼ گ?v(vaddfP"u @CzQ@.} oںReO aiF^R[R:HI\8z7|l1jzgLі옂2[5e5J*U+z>Tz,yQx C[ *i^eR98bQ JIhσ0!O[1[qТ;I9) ZE # /*\u@0_-+Э z} R]:i"5Ρ99F$l轀I6\$wV4hu8x[Da);DM/b| va* M;?u8O*ze?O(F|)Tj,Znl ʓsvc۬U~ͭi75l/ y'Y-[Of-ЋpʀQŌlgԓ1 9r& bIAH-`5,O $bpOxN/l~k} V ˊ5kV_zBuJ-cZxb#ATR-%7(ALs9n?  WqD"׋*5Zg &Xgȩ:atNbigڒ:L)a0 Xu_;v}dV 3GW̸sjAnWkQ ő/-`W W![#g>fئ:Z#@ʽ p(\V9=?ܐk q Ț#cꮃ-0Dч7 `D `LOv9e":lr{c隈Yzd!nv b&ǒI u;X#q'NA``va(1Q!^h!c~mK$4]siziPXYA,,R ʮ:_%ܫ晖() heO]'F7 \a"kLK ɠ7/ s(:E#)iKo@!sX'&EMc=(Hd$@Z(eC0ZxG9 ם>lK0>қ:r3ZW5d.ޤl/(#z2-ff!)uDdX;>̈́k\v6/ @( @&-ZHBeESRIrJtl<<(*ڲy?7^ĚX36zyZ3: zǪQEFI,P_:WcG9߯,)#^Iou2p՟/ُP 7׵Nz8+ݞ3UxPj)hw0nU>s@CC0u69 1Z;:,*?!~_]wO߫6ѬqMUykY8Mmn@,ճ-z2a(D3Lo\!l]Z|FUCV{`hU墺%ay'F]>o5iblZZsZKMH*#9,fˁ\eqd>%@DBqEϧI+}T;UB \&-vHK1&"ً5$ FMNN'0B}6j3}-=xUsp9X}GXWݩ!}_daeSA,'āD e㙡~W؇V~i"x"SP[֧t"3GPAENvĒ;V>ĉ^!Li$}~|i\'D+_N#6Wsْ-z)sR\t`p t؏{rcZ*G̈́8 %3aDnIy%mvSx8R@&T~v_nl(( _dӦ0bX b(xF!Wps*t6m ޾FG99q)[ EI@- 3ev#l]f !Z,͉Av #H2 *<o 8y~p1CBcllo I=|62ti߿zr7H k urvD/kTnY wƭ`ק;|!𷯟]AkqK{oBIݵ>;'^p rBX-WWv8恎(( bhƀ n޾#/yRE'!2Oȕ C;n|<8m/O">8`nVH{ =P&:?vM |(q&o_> RTAw1A-\l8JE0t!7d 3t h<9D(@ @a^}z @ QHv'|wg'd|d!]܁_v΂|f&-3/TMFFsS=o f,e^n3c}zH?'L`n Aغ HfKUID_r| ca:8.2PWU9-^F d߸y$X^Z .dp%'5}anr֝p@'}@z̐@cuuL?l7}%H XgcY9jq: +l:9$x9. 2:v&)$=CJ6( Pu* A^.Gb|FF Fw iP8TQ{/m^~Ke^` %N>cT\ - O[F:ass'ЗKz5wsdtK ʐW2Nϟ-PǑsgtt:Ot+ a晤ipI@b23vԟb'hXc> q$рA +Ox'p@z` uTе~aȀxC87u iBWr9dXC$"JP 9H'x/_p= d03q@20" `wj)UB Q`b*lv%=J`Kzpbk g xjF 5e`k~Ema~so5/hvv֐ %b`z^#}HZg&O!Q^ U%b`|9nhPWQY/)m98]yХ[g!;ːJIu԰+_@i%-I!Py wN!07dBdI@11–1C!+ AA;3 iBNA"b41Ud -/0[Љ͜.(e#;:Շ|%*F*`߶ ̀359<RBɀTZ-πg; 9!V2ACӝ On܂g2nLM!0-F{ Kȹ䐀k 9/б `s 5Sg.F-X@3$-4EKE8ruoB?k7nCd50h:d2>!)!"#- TL;v43ـYWmk=p 4+h- dKRORg hi(Ծ&^^P /&&Hj֬H qabF"{ j&\|9d#MaG +۾~!H1^ JY:&,$D=NI!%vY{wq32`(صwFC&Za>th/_|VٝOF pY)p, h0s V:z|~2>:FpJT L +hYt 8 Ed74d .9ҐְS@/ql``T\% 3A@/rIӇ76+rhÖ=P[ %EdZ= +ؠhpV]EqVH/h~xI D08>7l/X}xdX,>*"s:gA` Yh1yǮ^ATd'$>|t4MCapKl Qpƭ'v [<])@|:<RAZ,h/0 Xh ./cyz\d0t5;mH2~1x3آT#@!E 7?oƅ]k>i^6tM w"'2Ț[H r%ِ6 O k5gg_a2w"ejٴX)Zݻ/^ dE, }q!aAT<0emind hgO "'`F,6> 2`9jz>$H) l,A37Ub`)?Ppu, At9wBh',[Y(h#")"¡QSh%)< 18β-̼}& ).ɮy2)D~Z5$CR$h͸؜RGĔJj";ufxE86`MLi[_\?=y)y*q%IY3XφA[]_[={7Y~ǧqǕX+4`KW8XG>K;$)$m|NPP`x|Ֆ%IއVn]]= C"Ht]>(em !NL%^v@*1qQ 6:s)6=Y$ {ck8svPU @ٳ4aNR XX !`c-RHF +DBE"X؈<۽xwf^5 ~'}!޴NiWEAP^Ҝ=A-VX3BQCN vK€"TbJoؔz'!oCVnߚ B둺 ^OϬx4ndQV\벵H\{,^݂2~'ŋ?NB>:ặáIA{n D䃋Z]eZΚ-5sKI.GGP-f&sN1P"ME #9D#Ed|j;7 Zf^T]=KQ]x)("H'""(`ae  QĠ&~`YHE̻K"%>owסjoT͍D2Y( ?Qk%F(+|r0Cm,N'gb1{KW(#&fSWSݩmoTufvևc'&@X6xxj*DF}78l`0EQ^HA:6lNoVzLZ#FM?u, QpMq^X5`!`~Pڐ4ZRY VZ;IA$E$]rmv߾I˜".nH*,߲R60z<{ˡDH2&烊0M%G \.oSZa٦z(W}(rfY+Ŝ⭶Svdd)#9@Xz$}b}UR"q*cu+-Q% 75 h!2+E|i7qX O lkD?]]8 ɌЙt3Cȅ1GM+3ʍP[:)*UIE%w^=i)Ț@TX<&>Y9=!y82{9{xjt!8RA1YX5,P|".L'C/ه4F! 2ƌZp=: v=Vc8@PD )Y KY?~? "(-%:R \h}Ϣ ZvjHE\xYD.`sG d)xr-a.4"ƒ F͢pGPT= Qt݇/yX<MК*||бVx!zrx^03LhBJ3躒+n0 < v@2󏩥s24v$C MkRHb WBN^[|,!;6!!|dt_<0u, A>2*E $ib¿!*(HiHTJBZI/} ^y1ޛg.)BC[8(61[`0E)O=OcbV08`@&ཿaӴp4q2|ppiLPƗ&%tW7QGGY[7WiqVDu*{)OrK'{]4vA6oq=8p$'P4͘*@ -*L-S4pqz ' ~-d;ӄԬ_!Q3̕kSP%i6 v# Úp!H[zs #a)!k -u)t q3ŽLA7C؆E^ou?lc^KŪ .L/CP6{)!T]nqE2 VcNy%MlGarB$#,dA t<_֚ޗ c<pfPL#9!CFK)uUMΎ{A m._d`zfKܸu篿ywh r\۳^A%є CU3Aβ ِ > d/{H d 80A9] @:T7,`ތۣ~chn;d%?؁J%[wKÙ`&zwȚ癠Gm2/&òԀ

^j 4Ԕ⢂ 2sM $' JHxTOP;D?d/8+P-&P4v 4ypu, A#,"0r,K!b?aqΤXY6̇圙7.ݙyofj-T.rmoTE2b2L'B .8l߯݀4YWĦ)#mP'%S;LSK)2F(i_^Ȱ_.(@E_?gh7nXRav-䠵p=YS5"mq->TرϙK?| K8)W"Po*cEü) ԟG:vcV`JlmoH V 妨~02bК3I]: &a%  g/E ,Upu= & QD`O qBAcD +ZnAH)tVqw?7x.rU*EhhC[d@,q7% {@SǢy ÕM;eA`dܤ$mjU K;gr$iΦo#a <=`D*H7RA2ZNNl,[SЪ;ُS˗| 5_^sJ׉})(Lu+lڥE]Qʔ!z5bLu>pVYlގroZr9*帊}ƘaGαH߀fZ\x6[;@Z kSCdHn: Ï/i=;'/Ob1I+E8*Ǚ;Gf~*qqFI^;:~7[7)C'D ߇"ԙd;~dܛ8q1 z|Y 1;vy [ZXXZvZNYRAhT ("Ȟ\N"p,ewfZNg0[`0 zV_,h+Z,2X} ++{;Cꮜ`f]L\O-d@DW]>INKysˋl(ՏjcʬZcvH 77֝qYZMd|Y#/S.jmeIH=<҈>V{O"3yL4ޭA1d{4RҘ(kK-R<#[!2eW׽igRc17ӮQFsDXx.8iehPxyC|uz0}+1MW>î3]b*gM 'Dv6V` MRXiaRX  9,EHμSkyy?*|r+2V-d$}]A]ZZE"u(MŠ' ~ awCyuf8:~ Jo#),p :aA>&|.VSSځ\_np(nl ҁ*^tv{< ͸?P^^Ct~!dIfDmy~2ņUgdxG^}mBʼnM }8s澜/F,{(ؖX8Ux6qU?Ҏj0%?jHńaMt: BbJJA$^Pi!K׬ 7\Y]י7o>6|$*M;'2\L%崧)`j7 BJAc` yh r:_?8kT@Uh dPhhUA%=`B:^ fMrgOd69bOq( #Z{t*OaeʨoS63F~[JZ:1}m2m1đ, Af' ׇRּB . sS`]J2/r'nn/^jASߦ`4:>JO=x&4YrجA=xfaӞ @ @_O;x+qۘ_ x9éw!fL:uS k+C̾=?_xdC̚,}0]9K21,^׈t+|O4OAo[Z8f30ᒭ m[tk /LzdlvIw!;Ĝ[K$ My 4#jtݵϝc96B9u0{9 );#UfQIѪDt_BwOiJQ1W%aq) '`Ò J)q7,@&$vϛ&x:k/2!#Z.'ӝ,U3f;@Ѳ-2u C8z@.Iݨ.̢D"i VVщ}IÖ;W2q2b4I校 7ೣ660`fN~Fݰst,rj$\ЖcK(,йv⊷<:Ufi-$,'Lڷ, nJvFl$IwL6m'hɴ6Kt4;R~Fӵ[#ꮮ/& .`la BAozЁgP~I JKi6ϑtɥ#%Yr]tBJw69n~>yYiR|LD p6J2X(mȯsrCuhK4a8a{M@F} Efhrzߙh2WauTIKy:_Y_v⥕OlzQKh ֪qE%JDdE ^ ю$E =P$P7K?wL M߯o8紈H@(l5{2Vlzl\OM1 \gԀ#J%seQ21,bh} va(ڤ8btݨq4I\7tϙ*~>yZ$6do'maKFpz܋Yt+Z,>@u-F9mPɥNz-2\=Y|ٜ Cay+=Hi@|/x= E5YD"_! Kd 0MWqj<(oa(|^p?*{Y{ w$"g hcV JVNZ!SB + 6> IXWc^=M> oFw'0Ln0u& 0L8G6KRҰ?`Wk7)(iK=3Vָ3mwjAc19[dv(#Įs9Y2B)t: xɚd-0X/k̅-md=O_eIн嵓\'w;Qm0^0 ~9Aknނ{<6qHzwf yi<]༖gidUyNשr ,}G( >)%8cHh_joMԌ5.8% K ^Yc۷N8mSNK]%ċP?:!Z},վ^ >II*e-KtvQ.jjn8Ne >q:_>+vGY(&ѯ&NZ }8szyR<& 6R~u:0Mt FVӘ+ٮ\7=%gΎ.p/Ύ|ph8mg-LKjkΆI[Azjՠ巼G۹pN^u۩j9ɕl9,X-"g]'hw`} +L0I|잆N)}U5ONVTt|4RlzU9+}bԵ[2tםdTY_KHLV~-!DV/I2|9l]`f] t:HXzF4WZ gz.I4+X+tdcc- JS5NٜW %s$V O&mRG]PImAJP?EP wIdQ_Wa6\`jrAp!Oq(3eEF1}ya PBL,Ă ! }600:6Iٹכmy|GSS&γ<[1-_Gweps!y]ez$]jeJgTnԆ ۈ3cA.qeEѰ 7 ) &=XD?7JCy mmM[+ h<[hhºuEwvU>N~ƒ*C31{v魸X#X| P>k&XNSFZX%L.BS^c<'0m8Xcc^ ݹ9T$7~'/m z(FHD Xe}9lT;;o޼+H!cg3)iY6$%ϲvu{N̐1UI2w(y%K"^4:TQ҄mTP8# +OnZw`AhUI\ =Qŀ/dKڲp9Umd&D s_hjvkMaL6+E7"jΝ&R+ l4ĮJ>/2jpQST]zVA;ˮl͎Z@ jqsAhĂ?" DŦ`ϳSP@ 3cY^6"灉+Ӗgtt.ςy Zġy6̳)OlyV?&p:RMP$;7*#| ާB|KΖ N{c tvH !Ev+mD0;>15iT&eٶi0 Km7'-d\٣[òc@Σ 7^8_,F՗eQ^iP d;.0RVssǗB=3:>YQ 3A(M[!@ UBE! 9yQ-_,ºʔ8Ngu;"h1ή8fgưIdB_LqvhtK82,aOΚ7-Fe;GiI}I"#9L3owB  .YTȀ5ϋAs! #]2h(%UZ>m۴Y۲_-SƅHac wK |OG(_?~$-ĭ:a`x\Gpq\8I![LϗG(ak4`| /ՠ#K}qIV?mYQШ^dvVhAr6}q2nσ?' /Cԥ#_y9DDTvm qtRUÙHV H]*voZ)nNj$| m*-n)TSN5i\pspϲU[/bDyF!(ڟPKꑊ&<6ѵK]r/EFN`xl[?{ $?LC`ž#ΕJ[*<9wڃ~/#5̐x;,af$o06`0$?vU; @ٴ @V('x4ou57Y.b+t gYobk{]d,_ԥ̼Zϟd0 ^ygD&`!nihN +8#WE ͟(pmq6֢~p>H)AA#/>e]]KOX3X6yݙ1 sS8#}n4_μdx6d-͑l:+<jf;, _^M]lA}IpA&/(+rL)LGyc!ɲAb2IMzlABl}9c(1`EUlrKqBBݳ99^?0u+0ZpA7ne kit&IyX!0|'u3E@>mjSz_17`gYG]ye6(6osPbrKx̎1ukⓈJ}WSgtTdct>e-[_xJ.Mf*ԦpD5D(0nRĝoBԺTuM0ٝ%ǖW LHQr1-TS˟K;x:5Qxi Ż6VC{Pb`91.r >lOkh;6jR/(U|;b5D0 &ԅ*mRIܝ/vWAI*2%g(9 ķ05f4v$a`&:"=@qKƮQJ3L{ZW YE!x~=eǽ_OJh+j"|8Y`@E -ZޝG#GݣC\K6سk \l_sWŷ/A?Hݴlf@.EӈW.@dJ2 Ue۞f; v+G H;J[>`x#9*TL2"l%65Ǹxc^e(?:aw?%$.6:N؎#K;vC 7d`_QveۇEFH[H-g PJF!4.*}sHJεH&0yܞ}c\eD)q\{<'Z- *A! jU56n ˒t^Zu&Y3C&- $B5KS&?1]!l_eBin$c|NZt,&_ɷ:di L2Vˏݲ@;?=ײ "Q}Gګ;fڠy3\Apl )ʆt6]#ԦirwI2 n4_q(W[uxSML*Lx!(Q$IRs=ݲ΢?>A\N65;oluZxr@qH¾N,EJvWƚmǔyݤK;6`_4hܨt,ZD ! fnNkq\p9bIR^%C\5+qWn.MߺFJ, 2CX:MsoiBqp n`׾AlЅiTiG, k'уgd,;~`\ra ;aRycTm$3|*?XHMU  |+aYiEgbE 'e\jEpޮ//@Iw{f#fK[{E>au tVR Auq[ot<f B ຄcԷTKיp/g j*մ vW9f8CS"XlsŒoT# rte K`)o]FkkЗ3T؅TU\Yu:3'Sg0 6p VHؖ%9?_.jD{ߜW"f 47joC`{Dg"'!Iřnn *؎u4#[V]NZ&Hvô=W6rY k2{C7BĐ8,$3}ݑDKL͛2iEQˆ;)!(?,T|ccK|Kf^t0>dJDzw@\j &[-^ƭEq|33ms% Dе ת11g!2<9S~8f6e؄tTejǞA[Y9V^@ (rÁ3JBb(HKm$ھKDƍ`_q[7߱~֞}G;Oiچ%I s& bk};} AI6l(Jdr,J]I iٹn*n:n/E +'=bMq_y C;\$Ѹ&ߡ hp7C|.B2P;]['VqzV} , ˿ #vx(7#C$yEb ,dfYZ*UD]Cah2ӅNA 9ayZe13͋iwnh:U,1&V6'>-PI#^L |"z`K1b qHm޻ c3$s/Ya *-Q,— )a@*|\agDrVw &97cqF#McrM9O!=MWS)1{怷)~,`-S{*h3 PBP-ԫSD}ԣ*v:UnZn!2gE\b9[+S]haPWṧ I]ʅ>MY[)eAKM&?6a9ý\}Iβ;њplm`zaqe^4\؎7#GR]ݴ6ɥHR1G]AS'<63˜(J!NrqvdzbN@Z^>G$'sApҖ!!66C Ržc;5i<5 #y/>%--eF6iTRhBU a&$Sj'Fi%$>|UWhmD6MȮtۿSBAn #!Aug%T,blO?d1KDAPD,(Z$W'3YvdGp @M/FP շߩ"Zzp)U]G a=o6e_Us^`{:aGxGDBЍƟS8s@ںql'rնqrd7OfuG̊q zePZ xLv#U}DDʓR]?òDcŁ4CF3ޏ.Wk'RH_L@ qRWF < dHsNQfS1u+ۣn|u:|b'28XV/rnٵYQ,@^ ˽/E?:ZrpMߎQϘF:N4AKl21Ċl_\@D=.+WgԂ0 2E?PPD}=6^/ɡƢ<*hiVXX6!x^1Rڶ+dY#iT!`7T,GVw(IY-Y `~JC)fWĢ:VUx"+2ZFqhq8REB(.v)T uDٷ*\L=ېODk]˄-Ϡ᤿,*suK}D溍LFtB;+p(6)Z"`EH.m>SJJ#9鬨Fz6C#l2P3_[֪ X2vj~t: 1 !zttԱp? w$cٽ+ ɥ;-HVZkYȉ5 z5sXS{D4W\lnq Jir'K>2tQa8)Iʊzt}f3rzڻt(N(>4\7%>}kv"+f8meļ|i<_y4%s/8OmWd`Ʊ]@29V'9-;j J,|VR#q@Կ#лQ9_Tib5o+-R|Y5ڃ2$ #mh$k0u6+1nk(x_QP{ح|ś -L&yE{r*hK;sҷrA2#\胿WH}G M19pMe e;ϙE6+pNi,D"Ȓ ^YI$?a( y _8Q[+fVs5`,`NThLx e?Lf']懱 `}h & c}-pp8INbfHCu¾E6APzU]Y}cVI\0&$K&9Z6w 9솿y i3e> vb  vv5YL`Vd0+#{O`&_% ,=hgvy.im^R+<錧0HaFc[{,M*w Pƣh o &Ah>ǒIxIJddmB1ރ$oX>k좃XȌNlu}F?p爷%7Äĵ/gGЩivH37Y-d4 Ve Todv˟,VQ"ZِHh q1H 3?n-.}c^JFS!! H33Aah(b)^DBP"Kʕjaq8v$ΘgJC]ҸƵyWĆ E`! Qw]t_+$M 9Nyq6oa\YaםtOL 3 b/ h1˜ X"_SDѐQ 4-r; 8QGpfLU?:ae .\$. $i_R Ck؎\o5~Q#b@mL&R> .g<^ljzs읱^_"z dxAϾ%P3yT~Z |59A| yk" !eFRSs<\t!X>MOR:yNPlF0C3ͨIIzu+5/LEpj_LJ >qXq_') 3pDdb 5^͡@uCYI\'56޺ U]< P2ȏEˢ'SΛ@ a#wSIagwBegC&.OO؂@3rP޸ :y,;jɺ<6>̻T;Xa *b^56ʓϾmä2wHAWYtQ,sX]NRvCJZ?ܴx::۶ǧR?TiC*5| U^!ӷ~׾%̾[pg-2B^Ct|Vk&.7׎Z6uH)_^$ 贓z4n_ ϓdy~t b59̒ nSRxxHUc Z< S )e67ھxIniX.D 0@T[m4uQW-WW~g 3r˱V-)m._fF>)k4@VeRjy9LI"sT7g%5jՅQA^ wfK&`@д\iB($ҧ,Õ4%;T ;݄.ĉ"Z::b):if8Z%Q޶إJe&IHOlԙ5uMr?s _݅%n2__b4Іz Tk|#>^>#J<2~ |'Qg0aȨ3uƋS|Rfٵ]E#[J\lb㓯+(:bBĚފYe'^pObMohbz+nJ ?1+ mT&*+INoie][-;.G'XL09''>oxqiH3an x]7eI7vXзhZ:=lgg<Z~ d9%:|;JUz[LH\ |F \z<ݨtwg?\aUR]yY׏,;LK @ /E 5;ߤҦl2CifTLuӼNfa"mˑ=x<_ԛ4/y!b{TAȍt$oMըCDTJ`Ӈ6iUj53{R2ExE8t [oO:"w͉XpĦKĖ߯>':z> GTx~7uW]sĎn.orʫؖ/3Qq ʳRv0uf;1 jb|'h{Oy#0eV-֑c/p8>y(RۙqJ\f#lti' 6! VUșE`9XfVd4QF1E1SDjhцTjZt*\\?/[.΃tѻCH&uZS:0YڋufʥD D|@)SEY|􁫪 Bll~T@QVx,`>\^ɢ.5/@fgtD()}b7Pk)SOƓ ]H*/muI=֖DS.OeAIpcB@NiJ4Plu cq>sikl֮NP~ٓCϗZT`Zz<1= `%sI-ОliW$wOKrɀb-H+UIԱ#KrZ!uE9q<ƒv`Rb}Y.1QtgF7bz.;A/35N1Jןn' 56"i`u#QQ% CyߵUږ9 JS8^f mT7r @ @t"I<)ƣ xkSϥtfI2q_rmrT,Ph3G[-釫q`s|&{>n@u^np^X0@_fQ 葊0Jd&af7f5GLO HhDz1< novR5&>Z{ԇxګQ&x=0g8d\PSXWXhAzaDg2 /lW".dbAHSP]ÀKD60n5hL2{ٯ@X $) "횔eRIENDB`doomsday-stable-1.15.7/doomsday/client/data/graphics/bigflare.pcx0000664000175000017500000002574012641367670024301 0ustar jaakkojaakko HH   !  $&.58;952.*%#!"   &+48BGLRSUY^bdgjknptvxz‹|{yrsqmleca^ZTQOG9 &4BCFJPTUX_bghjnpqw{©¸„|zxtsgca]YTQJ< %5@BDHKRTY[acegjkoswzŠ®®}{wvqsoifcb`ZTQOI;"%1;:?EJKQSUW\_bcedfkjloq•Ÿ”‹‰{wxsqmlngfe`^]WSQKJC5*549?CGIOQRTYWX\^`bfgfg{xwx|þ}|}onkghfb`^\XWTQLHDB91%234:@DHGIQRSTYX\^`bcbqpmqsw|{}Š|{xwsrswehgedb_]\ZWSOJHFB:4("().3:;BDGHGHMQRSRSUYZgnehnlrqtwz|´ù|zwqrpljhfoj]`[YUXTRQMQLJHC?:34.$ %&.3:>BEGEHJLJKS`|i`ekjprux|ž˻zwtpqlhgfecb^g|aTRTSQRLILIHIDB?:82.*& "%&.489;BCEGEJKJIKQTqj\_`egosx|¿ï|wrolihfecb`]ZYhrUKQKHIJFIEBC<:;953)&! &.2595;:9B:CFCIJIHKknZY`chlow|}ztronhgb[XYSRSg|fJHEGDBD>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~doomsday-stable-1.15.7/doomsday/client/data/graphics/zeroth.pcx0000664000175000017500000000346612641367670024042 0ustar jaakkojaakko HH      "%&$" &*02631.*$ #+26:?ACB@=:4/("%.53& !0;HXlܼugZJ=3& !02& -8DTezȰqcVG<1# *6@P_q̷{k_RE:.!%2@>=:50*$  &,/2321-*#  %&%        !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~doomsday-stable-1.15.7/doomsday/client/data/graphics/vignette.png0000664000175000017500000000041712641367670024337 0ustar jaakkojaakkoPNG  IHDR+7RbKGD pHYs  tIME%O3k"tEXtCommentCreated with GIMP on a MacwCnIDATh10M?tɜ.Df4K/P8ũ0fHqU$6( Id"$[l&-w/F!>OMIENDB`doomsday-stable-1.15.7/doomsday/client/data/graphics/missing.png0000664000175000017500000026532612641367670024177 0ustar jaakkojaakkoPNG  IHDR?1 :iCCPPhotoshop ICC profilexwTTϽwz0)C 7Da`(34!EDA"""` `QQy3Vt彗g}k=g}ֺtX 4Jc `23B=ÀH>nL"7w+7tI؂dPĩق }F1(1E";cX| v[="ޚ%qQ-["LqEVaf"+IĦ"&BD)+Rn|nbң2ޜT@`d0l[zZ ?KF\[fFf_nM{H? }_z=YQmv|c34 )[W%I Ȱ316rX7(ݝ ⺱SӅ|zfšyq_0sxpєqyv\7GSa؟8"Q>j1>s@7|8ՉŹ,߳e%9-$H*P*@#`l=p0VHiA>@ vjP @h'@8 .:n``a!2D UH 2!y@PAB&*: :]B=h~L2 p"΃ p\ u6<?g! DCJiA^&2L#PEGQި(j5jU:jGnFQ3Oh2Z mC#щlt݈nC_BF`0FcDa1k0Vy f 3bXl `{ǰCq[3yq<\ww7Zx;| ŗ]8~ M!8Ʉ*B !HT'\b8 q$C'bHBvay=+2Mv&G&Ec[ [bDDĐ I* Zc0&8(&iYH~Ho(%46h0װu wKDŽ7EGGDDōFG7FϮX{xULQ̝:+sV^]*uՙXXf8t\DѸ@f=s6'~_ ˍ̮`Oq8圉D]SINII\7n5ewrm\J`ᔅԈ4\Z\) /ד>aQ1n3|?~c&2S@L uYY5YoóOHrrsNy};_-cZuuk/\?kÑ)*0-(/x)bSWr±^$E[nEmnfmOk%%%JY׾1ꛅ ˬir]+wZiYYGgʿs{?T'U߮qiݧo۾C*זԾ?=xΫ^P֡ 2mjTl,ixwxHȑ&JG˚faԱc7sŨZr}wN>8(mP{nLGRHgT)S]]m?x3g]8wn| ƺc\x'ߥ+=/_u=wvWO]c\n}Ϫ'l:o\:xviMoܺ~{;˾;y/Ylx~XHQc?:b=rf}Icda)iDӤ)ϩV<|~W_}oοDΌ\«ï-_w>~f~#zGPQc'O6gAMA|Q cHRMz%u0`:o`IDATxsNXR%67֖zt <8nۛ=Ïn<Oa] vM)Xɥ-\["jm:cA DLxy#"&}^D:@!4Eu9sJR2c h@✟c Fn_a1,@ ۖSR6;xǸbeXʍSG.R f\x;#D^L#ጃ\d5s4%H'ѣxϛrNf{b`V!P-$E !Xbi՗oMjcm= M>1ҙחg,MrLLMN` DU)Jp!,z,69 6Ъi_,"3^=<>=J6We h DvH<ސ;_b(H/7 [pLe=/DÄR~u!MC1ZE<.-FU\"ucbljmhCC ҮGG\CSs*.PHRx_)N`k?M!_b5/Ob[ We^%INr3X~q*]Nx޸P )ހgc_OD1󧊧 Ԏ8 d( APqD;B15  ϣqi_MsE U'ZU7ӝr|xJلGh+?y!LfH>91l ٍMT3NOw0.q $8ض=x~K~Kb\#Qq p9a3'5%֛֍Z KA8zP"ϧ0GX2ʄ1l9QG>ǝ$*AHK)n=]Q)>Dէ);d֦9vZ>`t))ʲ  (/ڍXo-`TlC_r叨,<q-e0 8IJD%3R7afCwmŲ"9'P7Rcֆ C|h]?Yg7QAA 4u%&)L9_NZeV?I9uAgu)MF]Fj{bb؎ e+4DM^֦ViD/g{ ^1Y? jƑu fztGg>3'vHWeB(GLR控Y*zc >{F2j%Dhgw6YeP՟4@ A2tb8{ѵ%kiAhZ<RRuf^̾Q# X/Q􅺡=ʬF\ޥmlR {;^vڤp'Kk:b1@Қ6~%`lx&$*LOB:@%1:l'ԐeyRZ Y1~0&v>#6kb+=pL%V]!M[H3RCΫ@ޏQUes{}/.>|{i%0nIObD]Gf?G*N8v`uR||s[Bh )( dExf 8} Fs?7e-T-KtuV~@9yL95cqD?rc364DN+& SuzL!V$Mx7a"ı=,ūEM_4ytXԜ)Zwgfv\<5UjGdluAZnPF ͟<$9'k]S.Q&AM,;JT0l .峟k!l2 ݘ+?)kff ;O@9ާrWc iN @& C1-t]jTӤ&MLwAզ9c;szzJyswߴ;Mf|K!uFGQ_,Ӫv2;ϕ1}AeX<SH@l: NP!IjثtȂU-"K15Ӥ@¢b!T:ot &~,[8ƅ`_}gY*uz(X|2'>pV;XS~Z.JzJyאLe#̶ n9}!t^bI&cX\ 'Sfӛ0 THTU(5Mtgz?f?>MQ' 2Xϒ79^%'M4MHuxլ3B.2[ăMG:75x5Ch=*4!j :_M$6 qJ-)YKU\ܰpx'FV- /rNzji穒*Ҏ_9C5f6ByZ(qO' qVdpqI)B41v  u)Bˠٝ |yzI9Ibsa;U֧)U1o(2s|ܖ' EhmGJz[nӹ`̒:8aK W_'T6'^Q/~p3_)pcyrm(&.k Kt=Dުdq6пw-yf`t^!AY% ^Lӵ:FVsGX ږ<IDhlo)9Gq9%sAtΥ`.3J3U~-;QWL\tn/,tR9'8|iBQ fɣfONP] ^%k ܯf&^  \pEc*S- aeQT4v!l=tyeζ"S!!嵸['f!e1&'Ͼx1YcF`5$@hgƗ׷-eڷ.z(ӱ(Wa>$gfo&+ƛls\AdoHVSe3F_7D[K/Tƃ}ٓnyaHpwF)LzػBk>CRnx4ՍFbsE˲?տU +!kln`.k]($^ho>Ql-s[Qr@c@RUe 8ƣ?Ĵ IіZG[Ţ%5;CNmq|*l LJ6b|#mՙSw ( @8Эč;XuGx>'D jȱ33vf* r#=@ 3ұNqv ўOhOQ]Wr `chqLϰkt'-g>&03Sf{j &I %bN>y^S.;@ݠwTkl+0t.x0(~c؟BxMs#1 &S0@C(gPT`j , Z3\\wCwd 0WE@!4:]BmD+N6 E*嚓gHw&eY/*}Q Rb'aNIsi :Z{(萂H枳Y*;;ۗ7悢9_뀶 IRi%6_ONѣ V:ڝS3}8poHd눷ӕ6f)gz8nI8 g͛|ɻg?({gͿKل`!6E-W2]nY\+Z3i;W߮oQ/Gsd0ihV- ??ÉLMB̾:RXqP`乸ܐ6-d5:2K7$NRIb労".-Q@DvU?4h5t58O,zdrPFd"`/C`@nwAD@;|` yj_{K9"_@ѫv4Y+Kb"5(*S%*jsU̠>5:5vOu ݯ;?LONڊK."VŠݯ%XE>FqvCyh1On2CqqYu9hr; \(k=I*)t9/*1Uw9FcV%?5hl}NYMvWԡe^,5T+r$V0Z[K1R 6@tCH9?Jҙy9 v=c@y3ׯEkPS<Uҽ4IWJӱ@]!Yo 9/* 2,=i|53Rǂ3;/xKP4NNr#zcx1꿧*b3[zmD` 2a蔍vnrօ"y牽ߖVöz#Gw9J犥.SY|ݵ!~٣ΎoןSaKI iA<%U`s]BC઒(lNpG}{ V2[aSo<9VTb]\d;o@v yӤGi"Ho}pk{;?> U DouS,C /yRg S/ b?%fXD$"n09zI۾|,n V٘au%˗>;;7%Y*k G]QL'}Z}6rWcfPC8`6̺Eƒ?Ջ+baYH ch}|̾TET!áNL FP>-;3ĉP=P6"ysWAqZB G+QlA^o7ZAal&Qb hY$&@K @KwmkmbѤR=DU[5B cfO|tyX?߄OQ@\F:b.oB5v9$tx>JT< RAqq10+#%mՎg?QpP]-L;Z᱔]vU igUMF":vtgM⍳xu䝺 УB\X ytihv iŜ6̿w> "i~sIrPN_os=${DI6QQ 'Ķ }Im2R{aӅ.r:ﭱ !(@]"R˾vrez^E}nLKgv;j w60r 1 ?V Dwn*F!yǼ-WI,ds eEu]Nxwv~_[w:/RD>蚰h R/w$ۤk H8p^2?Lz#pgǙgjBY늌U U7o'*p)DTF$֩Oc2{UQJt6Ąz/`u٤$Jas'AcRh =WTE2u@PK)r|nsrlr QA|65w2ⓨj}nd#yfOIs3 զ.K/MO}g6FM 6U[luֆSg)Ʒ˄2 >h+Ci+ϣ?w1ˎ{ i^Rbr)zZ]N}StT^dy/ 0HT{4 61B7;Z$QSt̒%*J|`Ws,q1$l;i(Iq= A A 4ћ'?ĉ4~3h 7[G? dZZXgZ98 aNA?X:vZY7լmPNHvNi0}xNׯcR*`wEvggNwY 8KζW *NP@B xK [C]?iח5ꊄ"E>K~b5RN*y)T}[DT-muiq /dܡ 6"'ǟR\ і OmWz@j˅@d%9)9u cmT@J2.g{pYϣ{@ ,]ּkI 9TJw Hj\MGaeHzT@n_r`SJTT\>pq6;@ncgO^}7DDq PZ;;|Na(uXi_C j SF8!h@|Oh7H#n J3Į' 4~nW fn74QuͳC`a/Ϣ" 'ϯ{i ͣ$a1b%ٮ+NLhx$(2fiUkvꇶK@aX9D}Fv3M hZ &4>hH**:;.U , +#dZF$`) ӧ#UH 7_{`Jeu8FnXZ'gݾ֩A6 vvĬ<S+0!bS98kg $qvqڼ\͟lj*P-3 5" (lzIbScHC(Q.SMI _Ƒ[ά ^aXnUw`q3n'˓n#6a2sKO*W(,tHPs15,ڼu9^m}Fn,c8x@Ź& AԲ|.UֹP$HSUo[?Tu|"[~SFLJ $O&2gPj1ese${tat!m>1;ý͍HbCb RQ'j>Ɗo<ΚڮuVq7?lu.H:ٮ8};k,%enq1VUnF-)Uu<D%>q߃EӊQ1=\j~!QKh̾D+̜Na߼Js%fyH0+Ll4J]EYib 9Le3*@qKh =`lv F4q!fhnU}=0{1mq%2tL@q}Y;>RWbSq)3YCGβp2$>*&oԷu҂싸A] ^Z׋|r=u,pVomUg ~ qqVeWj==5&>-ߝ}湳_&XA eGJ)R"K1y( ݝyCL˶% $^%B"PjDe!5vm?r7͉E|:ue">ӱx'wa gYIRƯZ!!wf}NYǖX8Ƌ+wbzKhYf\0tKEPwg,I0Y:PZtPE]do[b `|W{:ٖY{y}GO2-`;:q<ȧvZN7&rX&ibޞ\o˾?JTGe`KNϪ*::,PF|z_$y!)mpYR(LIŜVqG,Gtz Am0`lt<FԷ/mYNCsWAsMjAw5i{lGqeExgcKIҶ&F"pNyLMLp޲Xx _hђ@ o;)v61+SRZ|qjÙ^f_{C͚q6( 0UA.y_R HMH}9 |C*mYsAHb;D`IJp TGk+m hTgnT`^+ˤ'_R 1:W2I0\%1,7'Q`ua 'EH3$DRB{9bao9]ow;ݎ-h8\*+yw vsR2ٸߞ,- G}*DF-wW<5S"TfH|S !Q4W(WX•-8n4^20G a?쪛?D$a0T-O醷 QңW4g~N @YFpPj s$NbaCurXMĢFmo,Qq-.X#9~縇)5𽞏]< +AT #mei=:|qz<-㮟 ]-C ʰ"1( b.p!]HksΞ0r3ڢY&qɋmgm[ ="N7Bw~ڮ z˳K H*l':aeu'.snĆ\Qw$0O48ޡMT\+58)ۈ<2=Q$/PHS&7I?TV9?- gwNvH8k8] ٥T{)ݎjgBN./fpyJъ,G$(.BJ3-@ it9xtj5v_ B㺐2JŮˁ__Νa*@w1  )}R!8wOc4cH,dR:; @'6Bt+2݌u=~?v[ c? \̵-NU쌆zoE+vj1;=o)X!h4KNCJ㒃u9^\LPvO΅ 7/Dnbt!9>d|ۂ\i/m~J@M022Dl!)6N#=1Hتyka1d\.AiAD6.aT~<u A ,5*EgK\Bvv<)_?"J/ "|Ű @ScUH53^)ƼܳvU;=iFyO9>5)2$5?WZ7wܯ.oeQ[b6ȩVOSju{˨;5+֬fpQF`+7y7hKDIAюR&;U?AkewL|oW:h*lSM#MKS#ii|mp$̌Nf}1e-^7I י,8:Q؃1ha¹`mǛzWa۲wL"I;%!9SP"F+#S0XO.<H_~oN2.JWUz|2%`a%( ݬ̕,ul&u]KQBbR(rj{^ώKoٻЅ,Y$tEFEc_An8&ij}/;ʖ [!w?&2 _nai!:RAEٜLrr:FK5i\Q8}&ĭJa )ۗ(5VC M ?ol'IkM@SA,Jeiִ|$|*fXdA4z۪/J 5r̚1vvR'& im5-trfZ5u #Y&]jk0`8Q4Ʌz!=9 +}齶#*[\׌lL-RfkAI 0t`Bbb?D4YecA QB{\oreRT%b8} u@0pK&*cȫ{͔TLd8`&=@ 8- U 9.^y ; 07(^ќM*AW}Nd@PNu_W63zޣ)n!olLU>|gz-N:-bt٨8oq#/Έ0 #܄mk&<@]0!iZ> pgc"gy8)F^aEQs}S\Mi:L@ C7Ga8(8:fH֓ 5^ɒ6:V n8 vѱ31sY2P&bou7K!*?,M@XkàVUꍎ`QԶBe59mRA"&~_\mW0l Ai?0:g.牏2 4|11{#+S@Ea~YXReLH8yD*W%gPUi& Vp`PFw@n7eGWsȜqN]_l\y@U/FI)iܡz.ӹvQ~T1y΅r nA<:sb J[PROe\=upK3}ow6<ݲvn L28LwM!7\PX֔cj#Q~$ =zk%_P4"CZ L/#8qelK 0?)4@!I :9b>$it ?%I}rcct灄Ͷ[nHy\uÕٻXЩQQfjRZl3C7h䈰%" .|ԭ"d@K=ЖߟUgW OϯF11_j ǗGB=_ekuU-lV ^<|<"bii|(Xb̬@%U (s3zlQqO \ψ%Ux5R`n,Ԋ8z`t4V%= HLs;|mރ$ q=JbKՌ2 QS ͌ҵBƉR/%#8!]wέC>m!U_꺎gBʼn^\ow6]Rn#wL0)g</ BճJvMEDžk?rZ@^?ؼojq^rR^W6[<6fjCӣ+.#Te C̎䃘4;鄇g220~S!Ѱ= kv6+S|1l `ٔOi-򺄧4Ċk|-Aoる H?Vc mww oMhʆ) sڙ.Xl0), ռ>3ެhɻk Su;U*Ns%oix)jD#G%?|%?TW>x:PPAp$^!R̷zbߙ$Ҷe =Y96x;5?|8,݄%-).㬢j0 I(l]b?: LWT91=xan[ŗv!F*Pw4)G p1ob2E VZXd궤,Jxk:Ac4'~h3޽ w0B~{J9' ycx~J{ Az c0H;cדf*Rݔ0סQ Rtsaa(M/:MThjY]`g! MΤ.eLpQU~$Ssy(QRg)ge GBZy .8&0-.h oTI-*ǃ&'nPx VdJ5ǚr DH98уTErSa<-fCk=@ 4l[L\0$9O`E1^$G\ҙ W1|NŮb3#XD!4Kitq؟@'~]x.d޾jBI~Bs^c!wt6ztn;0 6n!^_7*]IDZ7 ,7e@FV^G7X1ϐ8w=*'?hd!@mT>hVs0ŧ@3g({Ie*zz.X 7zN*긻DNdgwMW,ގZÖKЬ3OU&487RD5lr*`n*]x2!Uap`!$GDQڤxKeٞdO=1EtQJ"mZElЕ+_ ÏIǪ5'Yj)*Dvhӗ\mIK=Mǧז6TIN:Sb|#`X׃t0]|aro5۔x*SJpmGA,]6-KFa-+LzQi|zB_`^hy#oiezA'sO7+bˬ^5--T ‰}&7|`G=A_ΥA Ht1 .BWmgiB$mڢ/[yvrR{gg)V#\g+mtγ<›v 3<$*ql%z46~цe:*v OGù"(+P "7iDԖڗ0s8*PNH֭h$ߟ%oĶmML: zl:{*ȳ1+!B@MScQ K);LX9jڴD &v/_B)bx? ?!$N,jwYN X0Ŕ5X;,U~pu> A_"- 7UJ^&|K73PN)~Հ~ERx|W$C5YFŝ?b$*Ď-)*R.ElvA(1HPjj|[Vo%=v4nqX!(XŠLZ/{cf{vCWK(b9*.TGL,q\: *; #9i<\#Oj@,i|dVFf}f́ϣҭWNcXSkY45'&.*/Aj'T MtqgS=t $AcnG7B 2~ i ;IVm}'h,>>ZB27f= Iu@n{ݴu/d|xE@CrS詣 '+7#AKCXZ_~{Ű;]"*yީ6ncZp;]:A `;e$E]l,5Ԕ̹~s:S`|@PSCԀܺ` ~[$?5a!:CS ^3d9VaE;*vfBύ6'F9Yi{k'{+L?} >y%Hf6 :|<a ?F xo /b+Ѕf]Ag4 9̘FԔbT@/Y _ iCЮh RZCRaAπt [ #:&)+'N:2ڔ.zv!8LC{ͺ- & n 3}*.!' `-t p8#jHQ򂯧Knz>`>(**L, =Npy]P d !Μ|}l) G9|35E쬓Jg_={ R#:sSvMްe79`< C^| :Q2Z ;zȝ@ݺ<Iǐ: |f|S0 UTb 6Hx2 du#uH6 ܇[YBB:decC8٬۸?:`\CBSdd=dN R@r<%<y+8ghN]3#!z;7i&t~6Qjo O?h^?뮏?AY~7K:13^ي̴:h>Z; ৈǬ~@ղ0SOOjuNաU[Z\P~N[R_/bc`:6ׇb.tц)g@pn:hlڋ.@(e߾$"!t 6gzo1y mCmMiaCc@ !=Y1 rr$A gQRO_2,aZõC63 d3U" 2W 1a 5h#jѲuo!!zC?}4uR-nvq@cmi) x!:oъKnv1 ԙZm=θ!M 6氋0@T$Ȳ,{!6:!|^rHya3C.o%Gw6A$ƆAe'3.LKT )s)2D\? ="F]E=޽~paO Ug0DacP A\$ WI+PapDf߷J\Y=wfg07O,M#EJinnY|dCiRᖑ=*KgW`F}2v(>&欤dqHi'U5&&/I_wE)e[LfS9$w{#O2 ±M\J1T8;[>quTVהKMpXIB-Gχ'awJ} OtH'Y=Aћeeh~jX#*O. g~@:BňW VF'~1r88>Bsn5}s+&A ~ :G]\u{$#g7r=Yțtc~/L;! - G_* JeՓ]E$nT Pdofrc%$U>gSxWi n\ VB*-4o`o,R0h#$j ft[.ݹw93RY|Lh(@aM! ׆$3ѩBebtRE\rR5Փ|ȇH k 6QL|6,ғD >vTd5Z=,-iAܡ9pr>9 }=@ W~ عr?wן!?x|67@F 4 C*u>[xrl$h5m#\,g3Y'OCLTΐՠ a=L<YλHVD M/P/판.?PovX3 t/|q(hg &eB2 l α U/`'<%iPH+x"+xz|%%Op1C2_@yVo@ _z z6Rк]{3[&NYxϾXAdH#2^o0CAF޾(qrOd\ /bH?a$)HXb}+5_UYp9 iӖ6\֝𥝿a3`.R PuBȿ#Pn#2a=rp9#v*r X{'o2k^a Lm{ X Lh 8E YP iC 5xЛBA~C>m6!'!eP"{͍[w5Ԕ$ded#"PV]UEFZ94shHclWvda 6 0ZCT{z/h79R!:кWck)3؛ǘ\l~^JIY؟??>&A71'x5LL\ḱԙ \[}ɆU0pcqtmx"?,!J w H; <=ȃT{>)&3+(A>{ 1͌ c] +)tGАxx:|u/N @S_|an`]Y díKF9d59nXB xf=9yE: eB*OptCXY9`@+7@@ N'e4Щ:b2=ߴ墳CN#=9V8` I{},8>PE?pg))ۃKM^V4t >t<1 !KtTMLHd<rPژA H4FͿTJA w RVVP x)^[TϾEE7Ѓh1d.{jiZ3I$},΅ >U2K.|:GiM_3@5eEn# &1bMxđEEޢRX$h~ZkRuȋkuuK2&)LW{v텥j+TGw $nBa;f gVǧhgum2: Te˥yi<ږ[]y=I]Tںכ~a*MTmms#i %cLc1e_g|#k[-k3GOF^q‘?:݄_ȉ2G`@OˡaH?-AZ,(6;[%F.^~(lP.C:O`~Z|1oT#̅"];ښamGA)v展&y~]d5"hxsd*2q4 WFtJA3U2^;\Yޥx͵pu: DAf)Xh,hH2@lxJ›X%,=6nvwܙ9gΤ/ Ď!>S^=HPU& ?,xŊL0h`H¦ p0G:?ݩc I-S{v,< h7bM+s=ZD\(siont&5.MX7ʼxTf&u*WvT*vւbIZUسj ?Djꖸsie1ϸ]֛ױ3)^*bx:yy^SղȊŽ(>w Pbj`a5. K[1<-)ӕJr Pu* A\ј">Nce%&y IX)"ڈ&`!7͑wsVgwP>@&FU &Lf";/Y:4LhTM !$8Izz.Lڎ_c5AuLƼ./F[Ӑ SdX;Z=I5-/c 0:c WQB!@4BA$"V *)_,LBDͮwߑyMFIbaM9!rl4|G"oXa:lo"Qn,N3* `|a#"/6tM;xnP!1Cj'V7 #+ג5O`sDzuXತb+F[^RUKa Z0c}u렰ZƏM#Q ]ݒW$H ăA 4tj`wQ t _Y|=' iN@q)lEsijQ4 pJFùA"%8,(!CAErO(jkE.Oeƙ8B .MNp/Aa\j>JABnbkQ4u:$srte@'֎wnķ`L'W }f5Xl)p5!;L2k_ 읓HhdgbIQM|Jfg7LV:ΧIJF[\D,sz|͸z|T@jyܒ)BTǰgI s`zMƾ]oT c TWmE$L>V?pGeHKM9Z˕y>dn3Nf NNLpЯʟ!9q"my1 1iDsr+kk#RjOp\e$ AX a{,Tu|tx0e~K«kuɬ9G㻨 ]saѹmF MY)IMp8ڈZU9c˽+i_:kk)lkd$^>v̸ Z(%zlw i^ܮ%?I̕Qw"j)%+趸r?ޥ%s 险; 4;C(MH\ښA:^` U0R+NH3EԎJ?HN,f\嬶 |Xǩx)( A219Uw[ǘwT:G,I]۬r}KyY_O1V\[<P7Oq^-jVCi6K UOK#"Q ?TNe͡GN/)1S_ ԈPl3Ah i(ho μ5F+{rM4 1|(D,Qt7nyH8DJ14$y Np] j" % C@Ä #qnp$Ix@slfqIDu/߯$ 9̿rֆs?!tM׎;SޡQj/e=6VOv56mPMe+_x pNC} 8C3E7b1cFa-T͞(I"`ngѺ G]bawS  cZ ~l0޸cq,E,)"η&*^ʿM*PVa j;,)e`Xjz̑RqGL?B*[i!q!BviIx"qC] v7p4L$jc<zw#8]£f:~H t;;S&&Yѐ/OcQ7gKf0\yW PΪӾ;FP3.彲թ] +37X5/j2yx)_ڹ!uz'3%W.u,,CDlv:bϯ<;0DD8%?),6e)†SRdP qb \$;D~~'/5һ2oXk^]-ǧ(>S~БhTS-m[U`gPUW0T?<+P"%/=}/tΜsz|?>^j%[pk^t-Vm0~mʁ[n4O8FlNŞf2XyfB͂^2֛.Cγ^@<oQNR Y׿DA 0 Y+2Qţ'1nm%y//W?9Mo]Ng;/d2"r+0&Ԙ\0)mlY) 䄡TuXhб;h@=Taw`x2զ}/ %>`} 5+F Iڏ6f w LrjlIϤ">b6v1 UaEC}"f?ɤËFm{6 u^Xɋۘ8>Q7DX'$n1 k_0;`و'{#"DXD*a:A[A G JK'HRHL0 D00 $D{vTMKU-[}M7[ll!VtVӣD6r٩6<#Or 62CUa H78E,B鱝Lz>ͧw\#t.90!P\_  *|+ڋ/N_N;N&${\ [-Lf6$[҃QP<:/WDn a3\!J\U(]Z1:0H}Y Amȹё' vu6(c ڮ젯ԓ:?}NuFI 5ʹO?{iط$,PHh&זK'_ݽ59)|B6arGO6$mMi\=;0.NX4r H8KERjy=KVfL' od0xH\/]4#mh $wwB8>g2Ss8ϫI)]F¾ӧWڠ{ya#Vg|ADMF[TYv/(wG@sMi.0k`'`|KQ}hܩ'x0ޏ? YQƛ lvahn! K3Ҧ4Mۙ; cw -Ͻc%"3h4,44a#S9K7~;S~yjˉh^uYBr0ҿmv"Fj(gd0١xJ~G_=`:y/xWݝK@HL.I yಥOsI~Չ`Pɞ/dir֤f%dtTf-L2RG)u>9(}UjMqɑ, >*$Vk~ixʞ5ܸVz& x,6x+ػѵ|A0M GLHL'$(s6 U|>*ĒjwqW_I;u,b'VgA2qq̑@ .w+ev`nMSuJht١t%qB-%gTNًSd,B 5v.ua9Oƿfq 8MZ!IѠj"Cv@\&Ntf /eqR&`F}yT %iD!?0HF45^TVFmk sV/TqXg{( 42 C!},TmXu؁%dmb& ~"x:aJ#`Ŗ JK/Dcי=|!L$tXi|!{^1Ԙ 5/\h=P~Ln2Njԇ0s\*V*c#Y&I];bCnPZkJk[V #V_U7ȰbY ԥ|nRZ&0[ $E5@)c-!6cmLF1jl}k["pQ~ϗ+NJwmgrc[VN*+cgH~VrEYJAY>7!c:eNn&Ib.:Фh҉|0!+A)~(TbdX/S҂@ a*ŋW|ܭ| " ]MLR Q%}2U0!&Y|n4lnw,UP+uTSkd`H@RH!>o K5g~9I@>)k xOCT:H Pr hTd=qsSMK=67[j0a_$>;a+5qgERߢ8YXK7FMjՒSx JBlCM#AhA,4y6 @1pmW`ƀ͖ VYR%⡎Ǫ&ݨ7l%K_[Ǧ<0 DFOcBbD9c6J:ΎRO=3csQCo֎jӆ'ze?lo}K3uz (XHIr,,MߞZ(fc뢗|b\ ywSL X!FmD rWhf!Aٜb9DNBe#pR < Lu2|pfJIqQpU7;ĸc!CYID˯a \칥ZK :$5h#!qŢY"e0؈9kWղaX"(F==E`V zX2 0u:0bBB,<(, , `;W0$in5/ p3%6Px!*Mdsvޯq7Fs;Y8ڥ& 9o,82Օ۩{2jC B; 8=,^Qݺuyå׎ cHo,GG/Vm )$[O:d~v OGйH;0u|I/ڻQG~7Zs{'M)@REQd>YG I=kRǡTlDoe{nŤ(֬#b3"I͡[9b^.zH_?}N$dsϪ,qqkAMڪTⱰ?ĈH M|)k%8}\o-xͬf/GBv"<j)Q &Yoѫ v>">15(yc3mo " WOȍ0+.i`=(F!KExD$!2FO tT5 g2<;i&[2lv#Ʒf=rbVi/A )u∴邥AT mM 39]Eڄ P4c?8'kpgPkeuwAgKՆ1Yϴ0?ʸ;5O׏Va(^5ebgsʖA,!ovhGd̋J#u}Zx8MLir)H` :8hq]au+&r\|x=@"‡y6ŭZXHY@kqLYߒL ⅜0.ܴyZtx_~qŸag9 U"XBߤp,<Ӆ~BS85 K5w8>r %ANG  ~;kǛ?:]^( _<i>&;xX"N<^@׎zr R#%5qn"e8Hɚvv ]9-Eh5ri6Aؓ^+#\zK p5Ӯǩ9xs6M:m!:$Xb C3=+#iދ.֮o3qG#,NKv]ݽ@Ev$n\BF>ȜjʑV]ykJY%۾ZR*цM6MZגLuI{֮gNWH!W'[PX6;|IO dbQjQǦÌxϊEu`&'H„[8z`Lk- UKT="$c!" @d͒>@ݢJ%mEj1i0)ɢW8Lrc7ZEfT@6ZeCU j3XBbhKqx*yI=l6sPCy}-Eج֊Vr+ጹ̕N̈&HVZN}խC#jS!@CP,Mn5Vz%i *v4GF["?]T].qA,gԪ ؅R9J TRҒA !*SK6\hcš?p++P`@{NRI-O9-.-X ºۀ#y<М]tc$3L0)WY8)(3[}pؤKvfm~`ub:CAq*P%778㹺"Q(ܱCXt)hqu_K:/wݣ](=)Ta/6΃Z`uEx2#Q>f tJ$ݠq)0VFfR'L^j}A^"8զ0I1$i+Ye VQQhZ,jlOڱF"J Tqmf/#M5@_kK*=ZCΚqh{4 8I6I]q?} PSF' %3FQ%1KR t9/m?:EA#MY`v3m!ԧd 1&C`A)aڧ#§~trMyEð 0Ud] "p4"*D.4aIϻh&_rVpZA섢0?G1̪S^㥌@֔m۠5P U5 ;O f%nPsl6=(){i_IXZɉꡨo ^(J_ҁSςWNP.a`d᣸f\GU]?KSgK n5IQg괥 @ՙ& QX}n&sLEV2WtfW }kYB l;!w]E 5.^ aŴeuY=5.?fY9c;v o`UI2+Pp9*ʁɃG9 Ld MAznB[' T}1sL!57wJ2<M`CE0NUh~pEw,rV)]Q5 uFxq^E֌T>Y*In"jNYBp}^}  4fMw q^<͔Th?!7v,A8xȨOyN9ig2 =m{mxj6u'1=vz<_ khA>y?z9@ >%|[D[?nBGAdxBZSJCd2na'utM|-O8)N36>ldU^ᒓrt8I'R]#B&>~"-dAWBVgK5ЉZ.bdR輅MPZ\KMdK(3drCؽRuO'Ic.eN 㬐!"65YzRr|ode h4J}O;c/}PaU]L:1 ٝ/ ֥9s&+1ٲ̻R'L 2 [s= 5/ljT;Q\J6ݙ|0Z2 @`""JnpX '9k<$ɩRTl@>X5ܘ^=/ū]*hi qXbJS \--m`|C'L0D#*Bt X[)Vl֛6Og]" 2/GLUɣRWŧ%^ ȅ*M$K8ְjwSv6^9qnbzkQ% 1끩6[~O (.ʘJ _c UB$:^cZ9ʫaհI,RuYCRSLVs_ q BK d\ "*)194zщ+{RORF֨vP(6[\FmDĐJ`To_ ٗ®0UQTLے"JZ, xzOY ٦H7E.ǂ|.m DzΟÙGRVȯ!INu980>,ڤ}^--?E+Ů/c¯`S)ڔ?fs SeXpȫA?ngLtk6Hd>0u= 0 .ċCbiBC?1zNΎ&ym2H0LrJ_LyCR[&5SR$B5دX }o7kSI/+ҌvnieATNA 錃:MIg:M%dN/R.oyPqt|CC^7F. x9;]s~=oEn%6|x9 qO2싧q9KꕙR] N2F<'"?lqm@z0GÙ1 eQկ67 gKe +h'.oԴ%k21!*[g #4 l}D7BccaD l}?f vٱŭWQ [JW )82`*tHkLGMg9٫%#C] ގtNRqS7Mi;h+] ԎÌ|Slcuj6iSZIE{mRSrv{7$R<5[L0☆}tѬ+"~9^V6>NLTC\AkQ_,[F[jpP/3I>;A.zEeR6_3zYLMRV˛C e1Ң}g;XK|,`vb `LAg[xAv"/<SvзY%/cݥrmK̴yqOp7W4A=c5,McW} tKdܴKY [(=[!U YB9*Ѷ8[1J!gz+@֘Lƒ+j"D!7n+k]/WТDB{snB> &x$hQ;Yɚ-cB-,!NJE;rEqФC<4yʼQT]/ mw{< |UAwHvlPx0C =eS[5\2lQ:ބa':*L$X;?vwT9q#%RʋA8)j{)rȆ ji/ zʪ0ֲߊއ[㴟9Yo/=#9{- (;&613:lOӌž0C~6^[ ]!)U6jv&ueU}L?VawrRzptfMRH¼?x<܅C9H]H0_(^t[묠+ p,Y'A ˻N@v/!R&gIV\|4~vqzvϏP _ea(U7 WBQkr&W|`fdfĐ 2&G ۇ+s<6*v.ɦ}Z&=)!K*R{Rܮ'W)"΅P @܉hXf.+x+p}"L*8Һ|q+.cOi-tJF=Ɇ%)^>Dt0t-|f%zФi%LCu-rWt 椘z>v3j+X*a F~'c o dó8 ׾6x"{Ԩ! @do>0u:@ %mQ+U,ms* U,p*FJ'ɒ[/v LsξVكHJ[$"ǕUL)q3'p)<2l2G v3P2уBupC$2(  t Wh "X0%9 ~"BV;«8;?+Յ̬!)RJf͹Ly~7$|ޯ`ꂨ7wRfpâ\F'rÆI*a&~cJcW Vb $rJ?B!uvy&л^#vh?S%NV-c5A^_; -J{qJ#GGz'nI[>Rb1!rZ2OCO{G;z(iZ/?L dU2c;FzHNl|}J1|~PR{K`ぎS{I=c1g/,| zCb?ӾW/&4k e\P_÷s#j͂|?I+N=|l=r$:Y|kL\q9M{i&G9<ךTPs י APDD 1^HPmk$'۝=f2@:GRnAh[ Ym·M:-|*$ Wqo%YKIL Eg(c(]?.ldİ6|e SUdCX13K C{/Ʒ#@]UM2w7( ǘj%rF+Wg}0neS\5Vo>lgg4<}&cxPa*gxp;i._juvxؤ34Γx ,# 9>VQp.%-ֶ8h'0¶;Hҗ-$k ]:wl'åϼ6;1 й΅xfp3${ֲ5.\j KfAPc B<,gζ3Yg)& <}]4Mv\`6 kF"cOo+d;k+,<%IvA}˕n_〕=&|PܻRĺ|АMbysOo:]/ "U5'Kl.Lf( uY}W{Nr֒`}j?1 h _ұa @e_{⾈7jG7ɪڏsA]93:cd%6vS t} !~n3G dcוMTjmDL^mmu:@ J@ǂ@I]R5?lGH/tmrx 4k/< 0 jɜH)H*j-=NCB{T.f/2 _ẘ"oV;;9{r&:ƵxT\RnVEph99oru΍J|+:S27a`unǛ>kLbiq|rEs `=ꦴ*l+&pK et D E@ad93Ja36P ƜTLm G& AȦϯ=|UajW?x:a &M" w.\&Eh37~i3j狍 ί sS 9:Nn,j,dt NEs*T {sŸ_yR@J&i䙟2 ĔF[1Io۬W|(bYgSpb5acAwI soqMf> Sa+yͬk\LY>4H .(ӣ[ҚcJƛ5Dt|ùi`H]0[l94fpvGAXCV|ܰ=T,~l#_f Aµ0<á@+oN4?fzɺ=5tލ A !+ԣ4KC?aHU1U)+woU)]X.\j( fba2RBwlsq:6dtAz׉u3½3*g=mF+ 8{['Шezvpxiy'q1iHEĊ8Y=Rӝ*9UԆ́5H}$KQHGW-4|5•>#Y>F(5@WYz1WB չ AISq BaX oqxWg~H]>_$! 6b/RPς&?ʜᩍ2^ʌkn -VԒ.ỈmigGgaZK7(n+#lYp}u'l9׿3N6T'[G%bR;eM9/|pNkVSYp7T8~F ~PJ笡PR3Z9%Uܰ=>p(=>ɖn;3ߧB@1_:ca&M; $$vF*hrʎؾw|/ s9TkJ` Ɗd%}H0x9"+QuI o?Z%js{3 ڠ¶,k*Ws?Ɗ\r97g|r)*<ZY赖@_M0Ye.L#y<ݘ =Uzem byd`[sGQEYn6'Tӏ$o3#kB]= sL.N_1ȅr+0$6ԈT_:%A[yiypdU%M#!_r/3V!pDa L{`]Bol_t+} Ҙ|GV̵a.u喰l(ȍo{ܰ[r J ypitL_\:a*914D :hRO'"J>g ]:Kcc[-K%d8Ţ]Ɏ">V5pyYa?rS²!ǧƀUj3-vaۄE ֆryA$?|z`oFI[/Ϯ_~Gw\v!Vyۚ2+*X7"L`yOCCx ӹ-!0Q/(6'Yibp9re.wW/$Z:8N(.fk0iyn1&*s=j(S@vtOkG35+׵O_ZGMycxkҚyO^qCFxbdre-G1 ` bۘ.G#FƲ}:6Ԝ-߅I9L+s;K;^8^*nQA_0u.90ͯ$,XsA"yv 5z?S97Q#zBA7xҞLrNR#zUȎA(Eڅse#DDz2R,9#u;/Uf ?KK "ˣ%cHDvنmVtw..^]gD⌈MhRBtǢ?إiuTUV!'+4#Lڡ9Ep(<oO am8oBmwj{ 5(^J^w7 Y+ b Mؿ^."T=%NaVj V_<}I0>!eƐj wN(Q7+4@Psl97HvP V",CRdY,]$w1a2}3+{qzӢd{Jyad/ 7'x-6o'@ ᯼n=׿!(`EF:I&ۿI{bww몛!' rJQ a -<ƝV2JId_7h.2b^<U5wbq- /}.^ݰ@, [pr*g$H+{U1%F(I.0&IEDc_Agx"/fU u.|WCHh<mM U^F3Йa8n*ZN9SYe?hh."~2Y8T!bt>QJ (c oht|7HEU#?6̱/oEtF{v(oKjz4a"Œ̧t+@#T8 @ԙ @4K+QC/q*$<;ܫ:23B'ьCJ$>TrciPz0sD؀- "|zsiP2zP U~?YBndU**}㠇Ix5CW_|f0api[̞3F#)jy(dx"%VR S+^:" Է W$@QN^04`䑊>A#?fU=&A"!^2sb r0:hw4l$UJBC$5tdDj nNAp vLPw6L hڎ|5mܦL"kiA8b[BJdď†k5-y K߃ H fYuA EPGT(LhՎ:"3;Z)GV,\)!9i9? `(_<,$+!}*8KЅabNQyj&sF:oҋuK=f"o(b4s]CI$)XC4ѧ/J_臓$ENl9țIZ9k-lW#&ʎHhF8. h`rb:UiD>KN%$ئ2y:'E(#|p#V9@%mC F [#]ؒXsfP5eMS i< f>_*HD~ QUdB6j_ZK D!gȟz(޳L~!MƛJ9Eãb+||.*?pGfi϶d)]c4gT%cɖwgl؀,ꋟ_ aI!-wZޥyGB F0`ti,Bsg3q?>L @ D+GSDmͼҫ Bkd2`? DiS,D=| C;s:"IɊ~廯3jڐVT%ZsM'UPz]?.&FlLdK69oY v pB)ӼHUY`⤛>s NPp_9TJ7i |GJ>](\ ٰ_l"ǣݔ2аa::Ԕ8/9Q7(9qO-Km0,A3VV%,@~/S璃0 |T(`φ 'bUv~DŽ>_zrDiGMǟ 8Z[كq?h0湁X5"ۮO;IvɊ,|'*f1s^<΍l7eȌ. #q$6nΌ1 0 N#79 mTVS1Ifk'3$'xoB~~tř^K`J&^u-0Ս1q(0MSm\C\+\$" k:jJiΪ?:w$gGa$ddC,ؖd~YW e=xI(GYz:Tgj!b2_[ aԈ>@{k5ah]lrs{ܘ17!8I˽OWVZhgTJASl:jaT9uuk$}dF#> nHt=;KE:/>7Pni۠/,Dw'#cըVbz(*sb>ݠVeIsޟ јn*ż(xnF?+Qg0 ta H8P <;Qt3.FZd $Nj49@r)_ ̟NR*M":t ag@3S-Mv(h=|b,vcn\%Rb}@rm:oz}j,4ڷI4u!279&ēCqE"'&]4cLƖ24헊eSI[(_:a7e5wv Hܡ?NNat-nNiFV1*Z@@zǬl9/& &f86pe3z;t[ðsb}rm0 C8ngB(}O.jV 4߰RZdHhx>hsW} /F޷E1["=ӷfpuB ߢ?CTߺKO톎=n%l 1; OKGZ,MWq ߰'w\`V4Za4=4I3H3$`O- @Aax_:|vh,k8NL%̱ʒ/ujZ:3~Pk7Ϫ/,Mt15j."lgV B,Q(">,y΂e8{/C`3J(er}إZ{V"GAEHН;iӆ>dII,n<fz՞F(IQT\Zt1hy0B̑6}sw7ý vV/y~Ey9"H)v%#hKY?qxX,Zc5sAbhƀaW_0>i;ϩzl֦ěE00Qr4@f[6M$.'= g2~FTCq|7[Z D6 -+3|Xp($+R|* QiIc*p &X:7z#J{n;nVMJQS 6ẅ́/Y~~=T^2tb*eԉ=ʪtJxqA,̎ 'AL9fA/U1^h` "9_: %1N%DYdzZn+ۙ饨4" mB2vg7>7BY٘s|zkb2hNI8=(ROÿ=g&S]DPtɞX1h?vBco*Osu ,:a_K2#RM8$ G*S/;h{B#+nFl5٥쿬SgsYARBAt;_8' *C{tƵ̋J_醶^GawKEG0_^|3#R-d [h{8$vnY$5k:Q0 өFаm(k)>ٴ0ȫ^y&Hr$oK&&3U#MfcN}{t')]V@P@OnDZ[pYzޭ\Ԗ8v)!q:SO܈]WsjDux+ʇe{Zfta" &=PqCCi,+r ۊKɰ80u*A΅"(;l;Iw҄͞sni  39vx޷7z%hgj2P*o>X_H?.F*ah ΀*K4oQE?\>N!RjWh`M16=`BzӿW~iMpe=J?p E?w" Q?Sډv V idSV+]Br"+\OwrO2D Z8wG lGȀz'ԉn^J{^*iP>&m63i`k? i7Y]1 ԙ @!()CgA -^㱧( 8^4"N99/W%:.2;O`jF(j,3. B]wҹW;idvXP\t$K2IA6Qtbo6䷖V`GT7S2:|b]7dIw ,:V$x/1@lKB%O2c잤՘']^_HjT$5$&9lvkE[]Bޡ΢my{3ӟ c6 2¦VWeDѓ N5^) Q1/fuK0m(|_#2:" 6zP!fwN ܪ$3z} D]`r0΁[U @N|Tfe4bxX>4X' ?OgAܤ-2(*_QyA?&ew\-LTI7Ǟy&h6ܳXϺ(ʱR[ì$ њQCnneVmo2 ZT!O*Ub :jIOf9mׯR]Z7?P4x1u|  ;/Z{Jۡ}yF R8|Z#3[AI+UDA`kS3KAh|\PxEө>Q|Bgjӝ))6."6Ug󇁾cF@_6 S7w8m:AYNQSMͻ{o@Abm^mАP$%%Ożr9#eL.{03tN8 c/)Hڜb;"eU[lՅX t7.'2kEPi+p>)~δ|Li,˟ cCꚳE`2<ʘ:@RkQ?:a ZC(>REmg&WKrY#H.˼|Jf4,Q^詯KK-)u:o?sҷ!Hɺߐj0sjv~R0!twD>|]iӌʁ&JI"^-gIt&ZF9fKI.&Mũp2j3e*lN!h`]WsO؉1~oG4@ f6(ᇍi ۹GyD!2oP_if&6Ev]-=sS'VM*~"_$'Lřsxxy{Wp c7bl 9!϶6lcX"Whb!-<#9 .u,di0eT>W~msKAhP{o-Ajsgy1. |wi0cXD.c~P"Zs]lS?_E|}2WiJyLxIkE %^ݴe̯/דz#Y.܁=w*5#k&S7~dlIfds!#<B|# xYd6Ń^Ey]S7vbΩRv #}dw7tq ƸWɸix}R5J3`]l?+Ӛxv[W2M!:/F~$z,u:@ nZTXyaCbbuH 9йܟ&!~.aT_@v1-3zl+I2*5oqoRkR=D^&eD/8r^9F寃: c"~vFΆM[1^Zi!fG/sdI>Re 0 C|*kdi F S*aȣh@gٚr 1 V}^x#=/J.ZC,?!,' (a[^VE k؏ǔ+.=Ubg@]Lv[yJj~Pu.;1 ܾPKE)K@Ĥ?h)ޥu9¡=Q,_.%OQߚ3 > ,۱M()ns֧$݌խkGL΂8S补F'\KLa2ըTp~"hl`zUXK?5pr36K;O:[l'ǟt8{!rUfe,Tjx2Q zgjiF^f7rZzL0 DӦ Ā+Y ʀD8~wOKIዟ8nI$sbnGH`XZ׎ڥO7Wם̭>wT;:2p,k˾"#Zlo&FcE=ӎ+%;5Gw|"&SǠn57+Tk{GW_GzO^0UIږ"ޥ9WDXJ."'8b0ܜ)+5/Lr1*Rm8Ry%1=wG lu ӲLMhVdn}o9'ail҂ Vͧ m…xğ5xer) Mx߅fZ!`egޜ;:aIKJMJEm}CGe[fIb(1'Ͷd*!™Kr: ]euٜ޵ϏР(|֮O~i^Cca"B,ӕOd:\:aouʦLB2Vmͥ{HI?EIƭTzG'he:6xZ$,E$0Est 4P*hC Rdu/gO 4ݒ5OmEH&hqŌ3clx5 %E»#E lٕ0'O\ƻt|<_/x{ V!Ѿ6[:+( sJW)։dm?czÏ-5QoP_>Uъs)5G"[UPk{9,yiWh́Nsۜg{Qækzi v L1iFkoA0=3WըQMտGa1BLO4lN}sLMƢ.OZY)q̘IB m`#fs$H;'2zkF,;?ӏ@jo5h>LdټKhQ&KZ0S ջĈRvl.CUOX+#YLHC[.~q ܵM8;_&9D!pX?爳޻96+zoۃ-ĸfzgxQ抂H-v掔/9x'xp@Y.7ǐڜ`,l#Y %|P(\KYVAp#E4 ZxXzj6dZ\uس̮҇4*f502֔|{g0*2$'HP p]AXa8XMbP#.h9)]ޮ۩G, 1&ףte,h“W%vnТ6isҲXJ7\/o2_;n('/ |`Y,ٺ2%E82d\_$Q$CE9CSRlum~#Ɠ"K͔rZj~VVQщ u uRP d4|\gLgڢp ,#,/t5WGXY!oDtO?"[.-2ܢP6ӧP~姻@6(Pw0߲Ԟe20Qw9%f %oELrt\/ǑbXvn8Y:G>xT#lc *^S[3Q)ث+W };4ugP!Q<ԜAap4H\xDBZ|v9ikqhzXD&NIP PH2)PHL\Mxn̅x:y=86ҕb'9*W 𥃯h#]%vp @MN&)W&tŔ5{GJn y`֯w!X)pdm9Q̎).S5~_Q[l]%X99rp!)!NA‰P2k 21bUZ&ߜ#5zTJFX`< -jUԙ0¥;,^W ݢ-B ϠW0ڷ k"HJE?!_cHIadM1 `cOrgEV7 aKNh2!W`apشSW  q(aѡWtbϗK.Fφ <>g1Z܊v?\ Pwղk/ w /\vH xF*H=)NQz Ӯ #gNq+; fX+@(f/yp`X1lGm+TwN^Q2Ѩ'6^ZJ%){٪[r X04f8 pG$B{*R3X0u|&+N ¸iy3aa`K7i4!ƘF}v>-ɔBZOeZJ3`GGf`0tŌ1Wveّ bP+ v/yҿE9zim5 40ܫ {q%zoӓa Iɸf@?zo 029:R@} ~Kj/VLĽPDndShX&S&7朮X8LKNF_hP,&+Lm"p, KPe9ɨ~bԚ=>;*OnA&18&DOڵ==QZM5-^o^! NnMp:\U½jiRV(XI{zl4p&2}qԐ x0[Y*fɥ.|84mKҝ~ LjgML,fM|8L]_fE wXkmja=ySdd991I#j)}6 @VeTqY=˃cpݮPq)V>ĉKv%ufmwdqAj)4q U%.K5]Ŧ/ DM֍׊kB^\_lPTz<=Mj9Jk5[\Hᒑ31H|riR$>ɡDLA+R6 wmqtnMpȰ qazFQZ8#^#7#ayePLOHM2"'lkf"Zh9+zQHO4MVzLeaè)~ȟT0 DiRТĉ9 TV4%ϛ-G.Js>΃-\zO,U>j7yJq7:àwUh2ks5:ytz)Βk<2v\'Y2{pcz]P蠟S9c[1-S e1vuL,1яEdU L&z :2+avjqk–!PN{#lF~f=Yay`TO߶ צRzQ>k6v+{v?{]cI;<* $>=?-Sg0C +âR[*Eu*4_?i.$\^:-NžTnt,@ X=zޢ'"@4#t9"F5j4X5a' &V/Z.K}QX&wb5+^8phfX&&ɬu>%yԄ12oϥQv'MnPtLd/InQ|QGSu!obc"\C` ;TpQ-iPmhhY. DLz{D3GP!N 9 ~']}sπU#qWmScnfn=ctUد&cc( #y _fX2ڮz >O'i*9e %6BoGPIz1>fFAT Lr'SQVE!*u$g8xX1nsOEb2l*h(1$̑(rRD_m!p ,݈9^)!U$x4 8|:A3^\NrkP[rڌImv]{۸RB:BU"\ W pq : مqkNz"yy+NX2g,'#ldgT̀ nܼ{[<1>-96PCM ] YϜܼ㇏>~v/x]8ɠ\xܜ MҦГ .Pt֟‚.ޮv*h.w;|Թ^ àh9K 4ܿ}-M #r:zt….XCJ@Hw *L. ><!@hl~)F!7 dGHk|xw [<|B@Ƨ!N&ؙH;؎YXHU"8/Ϧm 3@/Cnn 9Tk)jCsa Aq 9Y]Ryy ue 6{{ GCC}Pˋ5e 2sѬ { ߐ2`k)Z1 ff;hU"'&ܴ7X?v d㻗"h<`!Mv*FȀd xmsAN Vg[Z|sH3 <]o'H -?c,T{[_Xa!9 qI\|oFؙ mJ4v4X Q b՜v,i 'xMt \@[8ne]: |6@͐ml,#,mG pҊۏߐC-!)߯Bh\Yh?6i +.gl>}]v̇@ŁDFZg<5[ Sxgâ|͹~_p#;X)I w;? E,\e3pǜx.`oQ`. v@HKq=@>I zg/ *LD=n+:nBAK'!wT~dW,ld Rt7!:mH ,M,8[S?`ϟׇqNMzX@J 0T-u] x_Kӊ ?| 3aC s3$3~7o,mz6$ 75OE@r`Xy6ȎO_1ˀpp J0Cš3Vnf\CiL5/a7dB WWӒ@Eʂ̊pAEiBTmۢeFD@i jMh{{Hdp7{;4fX!Rce8E;zD&6: ҄iKK_Ӳ?,>ChI*rZ 6,Ú0[e*]g8nlqgho]yMnnMJV]*+O) drkPi"v0 ]#6Bzςr$tTjQ([VDS! l^HL`mV Oɱ bv\(?7kPdsgAxnKǧo'x"p88:mW군(q(Uc"Ǒnvt~[(>bqDY)@޳U8@@SAo\rJ

E@/pDHJJ?O!|rSH 7`]S޼~ Za>ts 7&@Ôh!Qi1d崐$^Ж.Q@ŪqdH )!q& E>T?L +hY`V0eM=H $]tDE^F&xe 7sؽ=zϜJD0sX&j+5]:Eɬ$Pj\(ȈYY ЎA`"43"Wv9v5CF}OW;Mt.\6qvK#Fe_WZ(CMoшGɔ×e-q4V yy=jV悓$#C{|(Byx,7=y$0 DB'Tu"- ^tW[UagF#̑WF!'ҭs{<$^,N.+2 TF`9ÂUƜGLp 9$k1F`UvM/P46?%BU[[B;u-D\ϙ3.&0;3D&,MJZSiMЩ"?VdWdDžQ9iE*d1 s郔$iP,cJ?/PYNLCkӓtegQWM ٌ GF+*΂r2^`i PY,9@YUп.d_z[<W1o/[k{7w4gVݟ C / F>2>:n7k+-~OCc[+`ml<<84Rc+_8dG?~{wK$L/G\A=3PKu0r+cp^OϮZ 'nƐb"IT,ݎN&1UJ>FؤN!_>ù%ҙS2 ?JT MhwXHJIh6m]򪥵7$ZvxP:1 O_a( il4/ ?..>OE:$.nQꮃ`EmwmI|''̼>ސ|w:,tc}ԜAЭå ,@bU@k4o2'U# A(`zvQ,[pAHgZcYsYɀJh"r9¢mqg{v>ig/Kp|f9:e9jgM @^N6" zL&炔NL\ Y>EZ{޻bWoxܴTK:KwަEx!lPDZT`o#U!]QyP ް~={8H&;4i40C Т2؞-+Bn: H{D ),Ҕt.(+,@f ?лf@ Xxbڱ{nf"kom:mRhgU@;= 1l ~,?X 4l׵2OlO2Ƽ?7E70AƩ *t<8Н߽5@M>Q$:?-Hl~<+= I + He, !gA*0x.SHE ^Z X Kd`Fh/ XÂ7\qIi!U2d42= q?d/ _Bf4Jd Yh'㭻Ք$dexۚ$+- o>yTkms )`$w󀾾YX|~Fr%j`j&/׊b}oز[m|u 0y 0*#'-XAf@_7_ʒ,9?whC_zw!ˀQ/CKsBo*K+T Gn l_> It+2-d !590ԡk.1b nE h2.y%?kv!#@A? HFb G t.uo\BNb PK B3aw!CR?#ti-,`!g{j~`r-;h7ZC3CtȘhE8ND@瀆Rꪊ<ܜXcHKAV=}'Hka>avdR {ؠngݽ4bBt dEU^`|Lswu4K?`\P4 PjBNc 8'h2 \+΢$ Κ*c1;~< 7qP7ww^6] 6ӈ.ݥPhZ{̇aNZm]n3CCґ.`x,n.OYhsuA :ԷYZѬzay}:tv(*e\6@&[NAx^Zۭ a7y<rd:iz *u|rȯWg \5S]0:*X9f``B`\\m4xc>л+lv(\lZC IܱaܸmL1+c ?=QY6ni眞3i0WnU@c+JyCPqe7*f-j:7QEP:$C$ RfUT{bg L4{C몜o'#/{SL4d,< x}/DZh}ppnѽm"§C-KgХ^}y@f&ROyǧgv>{]{߬??VK;~?CuYkĐF5]Ood$eܗwCNVٮdݬ]B8/Ћq#Nˆ Mv|Ypej`|V8bmS%Eī x^RR|)A xUѧ,Ҭf794n2?ߔM&?5Ž!+}T0&N ?MBdǯ 9y"eA?$)wZ5&FtGZ-Z5J" `q&ߵVZ0ӹ}>`p83ӞχAwNnj{'ֱ0*YOxVӄ֦Xao-ΨF3~3sN,x[(E[Z͉M@[`?#z|nw̏|+h>X`)&{3`{Wp C  Ǜxpr3Xs`( Ht xA Hp>7@ea(!G(((_q)BEP*>PT"AkRJuѤqMM!yg2s$^,y\;F'䷢rCv= M@S%~Cyu W?* kɆdHГl2 7T bx ,1&, <0Hh@)#sC.˥ۺmnƊsYWzu_bER]Vɻ<ͭe: C]h)g=w [LK(jߛиϯڥ_oI*W}w(6Ώ\Х:Ϟ뿱z0;91@|xRզ`pa,3RHr !I)Hvhc=8P3$A +XaWCOK RxCj=hJ¬Z ?!w fA=ˏ^AnA_?\J a@ : nM 6C0?? \ Ak cP@z@}'Nl90 炄 P*Q:B2 AS,,%3!e AA].gH$h&膧?tlrg@>x^V^!yܸyw!t#l С-Ug+3AsAR-tƊzعeQ\2A80C`u|!|BCeki B@U>" h"hv`#_]Ge}yvvD3 k2$E\ӍNWd`\;fFPaD 5x~ErES oGم?c ~JJ w(-8S;jI0H#(O]Z/{vqufo@).ގ5.|ӿ₉Xs;({@ I*_ ֶJ-Prs}fnI G[ki?<>f .pi"MXJ@*0TI)[ w5*IwXAoˆ1\\]BW^1 & A(hiDQDt$_H gvVn1۝ٝiv">v4J1!4{FgQCP@Z9O Np٘"3uA쎪.)~ȠEY@FmFSKH=l(xެjndH/0ǣ[vc돂ۮ控\^il>LQ![FCA$ךX8(m>{ n붴q\{yP M =;$>hҐ_X1&ؑ{h'p,`B+>lgٷPuX0 u:L܁_HheQƎb˖,݉41Jš*jzʼX81+{@VBϼ Z "܄RA9:o˹EвVި;&r r :C&@fIJAm<*"ݞjaY޻Hǩ@,X'91IfS+.#> IUJ>+i]{[74mZ?!+['g濫ˇyY$(ȯb|p)" 51w 0 0!vi9=WU@bޟ7 \W]L"+un:zqsH!hYd|H/dK!E+BŸ UҔjED,:qNSxxX_ QqAiu~*a̝y\ؔrنRl4R\u*wp\{[U 6Pvɮq,S/yTRE8'J1BNTBL5hn\J| , M h0%&2'~SFg^n!fzxGMײh}UXj1.weNd9L0P|7БM06XP8۽ϑpc([Nh{Gd ,gdX»xaEFzaG&2?I)=Z4ibaf o{hI&ΰ,@ N<]^_+!dG$R.^+¬񚻵Q&z!- ' VE)U F%eC1zh+p$: =*e 'Gg QfBN@ԾhF"0u 5 /;pTGkr_ L)@}s(撈nۗt`_ˣH"*n ra't;0%! O2Ƭvh Lӱs}|ەd ' 6&nǒ}=(¿Be¾PMOs7sJRR1#A,(֘7%SA@`a 5 8DQ#ď_z$_tRf1∂-D)oVcU0Fp|MG8(J 7AƦbK5IN'fI"jm醋䏊;Ww團+ӹV9c$Fr3؂t(iC^T. M7s'S.?yV[4AxsAb(DXw{:O0Li(9O/ٷ"U0`5Ҥ2i5=H:Je2LrLƆ"?I*uy!>&}H`Dg4T*P&KjlF\3@(ˮ 7m+p\g< Q6qm;qCы|vKX'\E|Iq9-H3z,Ak>N8nhI*cYnޢsz[okՌwCajfYſl?b)Hl`%, *Gk5-y TTv. xo0u:0mJ* -jB}ϮةHq糯<|K`,5ᇨғ |ҿQDzA C;oĆovb?'uQ g6RC R¶8¬o :ߧZ]$>r##@t,#@/DF*&+m \us%p[cټ0""$Ga2ٯǻNZz8nۮe' &7pRx(1=@C)GT.*ϮI ґyW7i>B?;.#@aDK L`Ld `~Zsaņ.o`l[a AQDXksOSYa/vؘT~,f^NUjY g]9YtHGXDy:[)Z<=N'&(Z$w-|/_ͭςR)QNB!tm/i_65|i{`s3cT!Y5^>ڐu$KֲH$I _rq/'5Xzj0r XƏ#⼲M>/[g Q=x(`;[1RNAlg.q6J.7Κ9 gaEkgC~ִxonhP~{e23{Llj50bgZ.^8K'5*悒DLD󇉣.-j9{@M8 kb3x= Gؠ!zKyxYLxW?i^L iqb & ADPDQ4iDcHc_v5)@t>Ԍ NmW-M}xJJbwaqkb,0fe $\͘Y >IWùN ^ShukӯQM=6D@뽖Dsvjm[Q*N&jN%'/ɘ:tUSOhKsrɏ2ò1.6j jƒ̔5& rcaTk ͫM*fyGAaҫ_g7s?%59U:%"3nvǯW s 'd87z_iii\Vb A)"_s#ZdN"]ťyM2jw8r"wS8;;JkNM9 ]^hoݞ?2lŭ fSR!Q0 91SFJkg2W]Z7`Au[i(Ք/H ^iѧKڲp9 2F5mdƌ<ֽb3خywo׻V?j\w2Љ|\1Tx=Pe Ie*gQ(LlK'j L>SÌqN=p#Yg0 Ѝ#"m+_ @6٠'ё0ihﮧ: >Y0 jd*]g-OuV/8/0 -p8ܜ7*#I iB- IhF2˲b"2cicc=nE6Qo;waGL lxQ9l:SC)я?5ˎ4uW@bԗ~ݔ7.?,Ġ57"{&A*pR5\5Gkrv  pڪ <;dy=pu;1D/B~CI] j*siF|t;oltp!Up췎MLkW2HD]16]Dr{♎rR'W@}hҥHCMACDEtRn<}XyoiAmű%qe8#ʒ)q6oW+(@XJD8kemϖšLsm|{F J-&*Fc2{}hobv>.O7$a2Kl`uU?-\DeeH* X oou3uM7'ٹ5nJ1bi^@7,_eMej +<J$PHY[؄bЦof3ԭAרW59oӾ⵽vE1ٳN4Y}i·+Yg0nACQQ뮝"⡴d'L BLWE]D㙮lghi-gFx`6=FqK36 u]:D;;J*GڈtnY;D3A" јxo&tY鼝 _n۝N釒"gD8_*Y٣CYbnz4Pv,O;xxg,Rdxi{Ln0mE8 @V|+gRܹ?17{T7’xlP5Y: +"yVJ {M@QgstiCEco 3uTfZ[9SĴGQ$p#%nTFFy@,ޯMѱa QP&+R+=1ܬ'aWՆh]nP&@o# ̧_؞wJI=;+TAY.4G9?ͺKB.8RQ'O玃0 Q'!3Prs B6y34ݝn:K**a`#SqT;Nm,Y^χlm6;'GFf:nE@U=~K+u4-- @]Xx+MdncjnWPEẌL ߜ鱉+[xbX6iCႦ 0f݊.<+om 8;%B:FGך[3`i5N "A0 &Wt)\Kәi;NdmMP:~%9B,_>x6BӳpEƹ*_K uyCI K+v*vѴ c$x[<%@xA!L.sJ `5U@e{|3 :&Ncˀ+ LHQr->x/4vڒK^1?9D2jAMT^Ҷem(MN2Y 1gf4b91.rӣr\ >lO#g|aymYTG *Aa5wǿD0SZ? Ԑ #R&czOݍC$v3l^ʻ"mU ejS P<-@lQ.SAA-&fKr+5>*8Hq5Dd> HbVlG# NQViϿetRO$Q/#V6 {^|zog3Wbp;d&M!1 -:R ѤT3-:(O} ~8XTPhT{@"z93[lAl9n5g^ #xMʀH|01x5=s</k>'g\;v<}RHT! QٕaH=&]Z ,2Cշ&m!&?1җ~叹ZG-8,&?ŷxt,mZu& ]\(8ADѧH]T'LÔWPuFI0Gutd$b f6 )7?ޖ|_ܙ[oT5rùL6aZ}dо{%?=ΎSDjljKlÝف'8l쯇 tHqr P_va@RwS Q'fzh#Dr]=D 9o'`qĮKg;欔*+ u9R1f1w-H\ף>A-gaQ\eȷqF Fh~${d-p }e@]T k}+|0j `Q3:$a&mO44Zٓtڦil˒OtsR8|`а݀P `YiE1" 'eP+pH~Yx)|O?Y1;Ч}|Í}v 3 \Jk $R Auq?YNLr0گeG;*~ZZ>^@E Nj* 6㕾W`g+@_˘>4(%"-9.+1r'rte =㔷{Ѷ5(K!TIu:֍/Sg@CQ'rV B<_ajQ|vǏ  "qmo`1v}< YidЯPMD%QwQrVC Rӄ)n4m9MFk~Pn6"`13!23d#2irJ3&(N9q^ /}Kʈ1N56dh/ 2[ZAQ4ﰈf>?WFꄊO'a-Ei%W֋Iy) 0 M_ISpf.D5GG[ @bT*"GVOnwIMcU% d(BoǸ)_+g(M*5whBpF'uN)3?I\t ]+>PGCseBJ$ΊNRcDrƒJu?bu qS4Bs8:,?q;D#YHqNF3^;}qE· r}rt { չ Q4M| s#(R453Nwn=w| >Df&%}܃bx'Lh?؂:Gz:ZJ4zQL VQPi|a["\׊>ɒ:12(Vc!{|E^Ǒ;AuR r|i/jDŖJ1Pg>_L` a@ fYݘњp^ `x]matߌ=w"Iw:ieB%Z"I5y9M+XV!_ 7E`0TjzG$3Ah!,ơ7ԊJ{8I3|00G/9xOb!ފȖ ,C03ƕ-k 9$NRn9^"dVa] }@Tv~m"ѱ{M/QW^"bfA:)ŭE7[[X`DdG1ٯw"l&2?cd}!dz-*yc&ߡHN'rqR啑(dE;~~GLr]݊roPD7ٍBը\7\RdABFԪʱt5TyR[zX,v%kBO_x:ae[X-+WVE`ϳs)%N̸Η K4i.=X#ȗCgu{1忣`ɘmS p&aQ4ބ@R@m9l|!t iO&bŪ_$fJ4qr6(<}T-LTJCvJ❌ `VMޖZE!&!BnIm%ću֝Ptԋ=U(AUkt\=ZnI%ktk(J4d|J/gn,1"]G a 󍭀Xm_U`\V  RDAЅ6dEd^A}: 'iv7el2`V܏mizw^#"ajZL]&)zXP;#WR㮈[aYdcābhqmW*frBasl Ջ[vo(F a tpBpaF^ktM1)kN)?Gi %h-L ǘ-r"YApuYE|Eh5o҃.Ivv~Dn Pj1[a޳`s_υ?1kSe1`m=WTy4!- z09;#3 ܽbCGٻGɇN*B17J;2Mv`oMZSŬRC X-C]Ī+3/BaAa9 1Wx=u,IU8zېOTka,dw81=nG=zaQYg/uko*r1\f&Jra$O*MW=ڞ:jo̷Cŏfx-D_]Ƭ 92d83A(Q68DK=o6Mx>3/UHr-߇/%Zier9;3 Mr: k[!0BW73/f'y {S"|;OYO 2qmW!\/v+1-AيJ)M:Q+8JƼg䉈1>Կ!qܨkdH&&:?L?r_>(JFSMDt_fA @@_E|ě t:~HeQ.oҔ* @V>JȌLp?~^tYcs MeiO e:%*tȉ+E%aL"I<&C o3&aBXIkª8YȜ@hLtXLU?Rw9Z[ro\E[K2Kkf8exd^'hEM^BuXbxf[78^A%lv>צw=FA@tIǷ5vY:n^*]77UܴL[/pun;1݅H xamlw#+B;^"Y9t||3!\`鍝rPrOk &"薒P fN]h3 E3{<$rUB7Bk`fv-1W8K&%*9k[1I 7aV>^>nbSkGfG^b}F?HoOKn =kZׯv iivH3O&[iP?_dv˟,VQ"ZِƺX.+ʎ)a&%ޥoWI4<:b5 lvb Q4r_(&x$Nӎ[$1]ȑWwkd xF'VA3./^|3l/.΍4w@n S 5)!G-Ge?a6@5b%Z ̚*?LpFW=DkFVJ0]{g1\CXް!3ޖ`k:6  %}V&aJM=̵r҅hb4=eK9Ap )Vx6&&Ae$ּ0ÙW|1}*.aQ`r9~ߦ8[3#"c i7z56e 'e[s% xfJ/tVu 3@al_ ?=.ҞOǟL;o1}w R JߡN& (BvOtYu>;Q.<=a :p8-@y.%(^vh3R hc%<{yHp+O:K+&T識UQvq$f&ש"Y()pkJѱRzFe"౒Yօ:5ʭYc"yt5 srr6m𼍷j'6>'yS..;НnўOc1e }s3zjk+{Ƴ'(@]Q˷YkT%`]5̄j!Ǔۍ*iKq|5<V5(յБWynqBC Թ @Z+@w^ ]MJ-m&3<!ћ+kJ@T7͛]dƁ,,߶cEAI#:-^]gM_Hg@`Mbvdl\:pKD6}hZV3'%S^3݄7NJV9";gTwU+ 0 qTț\!9FEq]QKxO.|ל(Nl ؽHlz+P-`l{qDOwcQw:'@)(mYR<3ϱ9< ew/Sg CQa &|2.qt7B Pmؒm9G[G*E,~IGjn{nh;FAv{"`X>hUQYT ֟efEF(j8-|ΉQtq3E[mH5?|ܮA7 ?)ͅCo2aOt&MTo7Hs_m^̳^a'UCL`eG}IHQe؊sfCe E0]|Սd 1FJA~(ӀIvN(rL* P.vn qdHH9U`PU9t} Ddjsp#V6~:1j*S ĎMjN,B߮I;Zvi+ 0SIENDB`doomsday-stable-1.15.7/doomsday/client/data/graphics/wallglow.pcx0000664000175000017500000000200112641367670024337 0ustar jaakkojaakko ?HH ;:9876543210/.-,+*)('&%$#"!  <   ###&&&))),,,///333666:::===BBBFFFJJJNNNSSSXXX]]]aaaggglllqqqwww}}}doomsday-stable-1.15.7/doomsday/client/data/graphics/gray.png0000664000175000017500000001045312641367670023455 0ustar jaakkojaakkoPNG  IHDR  :iCCPPhotoshop ICC profilexwTTϽwz0)C 7Da`(34!EDA"""` `QQy3Vt彗g}k=g}ֺtX 4Jc `23B=ÀH>nL"7w+7tI؂dPĩق }F1(1E";cX| v[="ޚ%qQ-["LqEVaf"+IĦ"&BD)+Rn|nbң2ޜT@`d0l[zZ ?KF\[fFf_nM{H? }_z=YQmv|c34 )[W%I Ȱ316rX7(ݝ ⺱SӅ|zfšyq_0sxpєqyv\7GSa؟8"Q>j1>s@7|8ՉŹ,߳e%9-$H*P*@#`l=p0VHiA>@ vjP @h'@8 .:n``a!2D UH 2!y@PAB&*: :]B=h~L2 p"΃ p\ u6<?g! DCJiA^&2L#PEGQި(j5jU:jGnFQ3Oh2Z mC#щlt݈nC_BF`0FcDa1k0Vy f 3bXl `{ǰCq[3yq<\ww7Zx;| ŗ]8~ M!8Ʉ*B !HT'\b8 q$C'bHBvay=+2Mv&G&Ec[ [bDDĐ I* Zc0&8(&iYH~Ho(%46h0װu wKDŽ7EGGDDōFG7FϮX{xULQ̝:+sV^]*uՙXXf8t\DѸ@f=s6'~_ ˍ̮`Oq8圉D]SINII\7n5ewrm\J`ᔅԈ4\Z\) /ד>aQ1n3|?~c&2S@L uYY5YoóOHrrsNy};_-cZuuk/\?kÑ)*0-(/x)bSWr±^$E[nEmnfmOk%%%JY׾1ꛅ ˬir]+wZiYYGgʿs{?T'U߮qiݧo۾C*זԾ?=xΫ^P֡ 2mjTl,ixwxHȑ&JG˚faԱc7sŨZr}wN>8(mP{nLGRHgT)S]]m?x3g]8wn| ƺc\x'ߥ+=/_u=wvWO]c\n}Ϫ'l:o\:xviMoܺ~{;˾;y/Ylx~XHQc?:b=rf}Icda)iDӤ)ϩV<|~W_}oοDΌ\«ï-_w>~f~#zGPQc'O6gAMA|Q cHRMz%u0`:opIDATx˜.(骒"F <2FHȀ38K1K !%3.z3’*@1AXpJ($( ̌T 48VyH7P e`+F78d h96F[&V;o:!VNLd`dLpfcRL?8$!?(ׁ\`KIENDB`doomsday-stable-1.15.7/doomsday/client/data/graphics/radiooe.pcx0000664000175000017500000000606512641367670024147 0ustar jaakkojaakko ??,, @Ŀ}zwtqmifb^[XUSPNLJHEB?<:63.)#}zwuqnjfc_[XURPLJHFDA><963/+& ½~{yvtpmjfb^[WTQNKHFDB@=;851.+'#½~|ywtqnkhda]YVROLIFCA?=;8631.*'$~{yvtqnkhea^ZWSPMJGC@?<:7520-+(%!Ŀ~{xvspnkheb^[WTPLIFC@>;9641/,*'$!~|yvspmjgda^ZWSPLIEB?<:7531.,*(&" |yvspmjgda^ZWSPLIEB>;8631/-+)'%"  }zwtpmjfc`]ZWSPLIEB>;8520-+)'%#! ±~{xuqnkgc`\YVSPMIFB?;852/-*(&$"  ª|yurolhda]YVROLIFC?<952/-*(%#! £}zwsplieb^[WSPLIFC@=:630-*(%#! œ|xuqnjgc_\XUQNJGDA>;851.+(&#! ~zvsolhda]YVROLHEB?<9630-*'$" |yuqmjfb^[WTPMJGC@>;852/,)&#  ‰}zwsplhda]YUROKHEB?<:741.+(%" ~|zwtqnjgc_[XTPMJGDA>;9630-*'$! ~}|{ywtrolieb^ZVSOKHEC@=:752/,)&#! yxwvtqoljgd`]YUQNJGDA><9641.+(%#  tsrqoljgeb_[XTPMIFC@=:8520-*'%" onmljgeb`]ZVSOLHEA>;9641/,)'$! jihgeb`][XURNKGD@=:7520-+(&#  edcb`][XVSPMJFC?<9631.,)'$" `_^][XVSQNKHEB>;852/-*(%#! [ZYXVTQOLIFC@=:741.+)'$" VUTSQOMJGEB?<9630-*(%#! QPOMKIFCA>;852/,)'$" MLKIGEB@=:741.+(&#! IHGECA?<9741.+(%"  EDCA?=;9631.+(%" A@?=<:8630.+(%" =<;:97530-+(%" 9876420-+(&#  65431/-+(&#! 3210.,*(&$! 0/.-,*(&$" -,+*(&$"  +*)(&$"  ('&$"! &%$"! $#"! "!                  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~doomsday-stable-1.15.7/doomsday/client/data/graphics/bbox.pcx0000664000175000017500000000735112641367670023456 0ustar jaakkojaakko ??HH @)9A?:2)!  %,6=CJ;0{|ule^WQJF@;73/,)&%(*-148=CHMTZbiqx{%wlcYPHB<60*&# $')/5;@IP[fs,.̞lcXNE>95/,($" !#'+07>DN[i/:ߙcJB;5.*($!   $),4:B[J?XB;4.*'$"  !$(-4;MC=N:5-)&#!  !$(-2Co>8xE4/*%#    $',;c70l>.*'$    $&4X.'d9+'#!  !#-L& [5($!   *DS2$"  &=K+!   #7|E(  0|u>%   +tl9"  &l e4!  #d ^,  !] W)  W Q&   PK#  JF   E@  @;   :7   63  2/  /,  ,)  )&  &&  &%  %%  %(  (*  *-  -1  14   48  8=   =C  BH   GN"  MT%  S Z(  Y b.   a i4  "gq9"  &px=&  *xE)  0K+!   #8T0$"  %?"]5'$!   *G#)g;+'$   !%0P+3pB0,'#   !%(7\4:{J82+'#!  !$).?i:@S>80,'$!   $)/7Hx@G`D=7/+'$!  !%)/7>WG.͝l`SI@;6/,'$!   #',28@JWg.+}od\QIB;5.*&" "#(-3:AHS^k{+{տ|tld]WPJE@:62/,)&%(*-148=CGMSYagpx{/:G@92)!  $,5=CJ;0   !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~doomsday-stable-1.15.7/doomsday/client/data/graphics/flare.pcx0000664000175000017500000001116612641367670023614 0ustar jaakkojaakko ??HH @ 'O|ϓQ0  QΥd( 2ɕCAļ½԰R .·ƳΗB\­«Ĭ¯q"|Ÿ Ӕ7 9•—ҨM DŽÍм[ DžĄл[ 8Ľ~{z{}~лQ #Ļ{xvsqopqsuxz}Э>|ƽ~zupnkjhijijmptx|Қ" ]ztpkjgecacefimtz~|/}vplieb_\ZXWXVXZ\]aehmnv|J ú|unjfb\YVSRQOPRVZ\afhmrzҦAƾ{unid^YUROMKJHGIGJLOQSY]dhlszf|tmgd]WSOKIECA@ABDGJNSV\agmqzѼ!2Ž~umhb\VQLHDA<:9:9:>?CGKPUZagmr|\ vnjd\WPJFA<:865656789BGKPV\cjnwѿCƽvnhd]YSNIEB?=:;:;?AFIMQW\bfmt}wù~voje_XTPMJGFCBCBDFJMOTY^cgmt|0R}vokfa\VSQNLJKJKMNQSWZ_fimuzԀ ż~wqlhc`]XVTRSQRTUW[^cgkpu|ӹ+Bysnigdb^\Z[Y]`dgjmrw~` r|wqnkigecdcdfhkmrt{Ԗȿ{xsomlklklosv{ӯ77}zwxtsrtuvz|ѽQMƿ~~f [ƿ†‡n [»Đp Qƿ›Ӿe >½â­ԯQ#}Ŀ­­Օ7 Jµµչ`f½¿¿Ȁ+ !\վw1 9̟M &Yˌh1    !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~doomsday-stable-1.15.7/doomsday/client/data/graphics/boxshade.png0000664000175000017500000002321312641367670024306 0ustar jaakkojaakkoPNG  IHDR>a :iCCPPhotoshop ICC profilexwTTϽwz0)C 7Da`(34!EDA"""` `QQy3Vt彗g}k=g}ֺtX 4Jc `23B=ÀH>nL"7w+7tI؂dPĩق }F1(1E";cX| v[="ޚ%qQ-["LqEVaf"+IĦ"&BD)+Rn|nbң2ޜT@`d0l[zZ ?KF\[fFf_nM{H? }_z=YQmv|c34 )[W%I Ȱ316rX7(ݝ ⺱SӅ|zfšyq_0sxpєqyv\7GSa؟8"Q>j1>s@7|8ՉŹ,߳e%9-$H*P*@#`l=p0VHiA>@ vjP @h'@8 .:n``a!2D UH 2!y@PAB&*: :]B=h~L2 p"΃ p\ u6<?g! DCJiA^&2L#PEGQި(j5jU:jGnFQ3Oh2Z mC#щlt݈nC_BF`0FcDa1k0Vy f 3bXl `{ǰCq[3yq<\ww7Zx;| ŗ]8~ M!8Ʉ*B !HT'\b8 q$C'bHBvay=+2Mv&G&Ec[ [bDDĐ I* Zc0&8(&iYH~Ho(%46h0װu wKDŽ7EGGDDōFG7FϮX{xULQ̝:+sV^]*uՙXXf8t\DѸ@f=s6'~_ ˍ̮`Oq8圉D]SINII\7n5ewrm\J`ᔅԈ4\Z\) /ד>aQ1n3|?~c&2S@L uYY5YoóOHrrsNy};_-cZuuk/\?kÑ)*0-(/x)bSWr±^$E[nEmnfmOk%%%JY׾1ꛅ ˬir]+wZiYYGgʿs{?T'U߮qiݧo۾C*זԾ?=xΫ^P֡ 2mjTl,ixwxHȑ&JG˚faԱc7sŨZr}wN>8(mP{nLGRHgT)S]]m?x3g]8wn| ƺc\x'ߥ+=/_u=wvWO]c\n}Ϫ'l:o\:xviMoܺ~{;˾;y/Ylx~XHQc?:b=rf}Icda)iDӤ)ϩV<|~W_}oοDΌ\«ï-_w>~f~#zGPQc'O6gAMA|Q cHRMz%u0`:oIDATxb?(ԀJ`hbᣀzZ\xb*2( o@) "lzC\ jMMQbbPMr >ԴuJ x@ @n[IPJFNZ#F-65r4T=#Rdbb`Tm4mj%BF.~`NrSZOLDP5PR [fܒ[5#lBf㣷@!гV!^Ʉ 5JPg @!jaSmČ"ȦaahzbOb$ bلq!G2rcMD@i$#я^GHuzD)M@4GN`@ DB)#\"GĀ %t5HfWmp>@#PQELO8\O(Q`KĶK4,dh0ԫĘEn" VzD/HD L !D@O#&qE:D zb@ ^!# 0XHjb Q~ xt{Հ-u&)jyY-9 9I-pr9zRNr//Lz" h0Ք' d6O(U06>2@L|r#]=.|\w* a`BqE>.>k" 4@B7'G?1§/cb1z>m9A7@Q+8CW}HH>as#zQnFghF46RdžX @Hr8)jq%t1|E$%L}H-E8_x|[ui8@Ѫ İ6q܆r 5# `[·#J*8 JE?O$5 2zbπ۱ݝȆ<6݂#% 0 JO'%KN=amhF(MG`s@'5F4>|0G0Ā-!`˽ȹ}#Dj \Jr+%9@(Z\N#6caJt bℐEhPN#X;a Wc>|>Ɩ U 00F4,#mBCp6@bǥYǦ[Dh\Ͱ?]J I~rT zc+qLhd6."ч>o h@b<.Ԃ1lėr?+a'd|d>Hl6?ADn 69jRb6u눍|lx|Wg`@~d`@.pE02gz\9Y6bKb2M_"V?/." 7`4_$#M E>A&f$;1Zh|X8W@Q{.\Kn5@ll@6b#'O,F7 /``^ 7m—Hll9AD>1\bKbf܏(p%f4=|\~ b[AN 4 jCV-q%:Bj]r20ʑ͇̀ 8 j `ӏ!d&pq%@d>9F$9F9=~\%X` /ɓRf"_NU|r/2۱Jxltf•ŸEFWKh .7[J-ұu5A'z#Je jⳌ+REt1.t$ 5# ǀZcngc ( $TY_ ȍ-F܇-pE2337} ' 2@;H**qE".yd9|ӹu"=q%flE>!6܏`f^BJ\Qƥ_'&Rus'!" j#G&1-ґ6ت|BOxbr09[ BK@(#G&4:f( ˀi7_q%l I) p2>sI @JgDb ̂dZPf%kE1;Ӈ/g_d#E8 2UFoFa JQ+qAjÖ ыzB =k["`q#`[~d7 JGa⸊Rlz pA66+a|ll@"=cs+ff$B~GD`bظJHJtq|U>\O/,G" O"!"f(bPqV+ ݽb 8\-G<,Bq0v=lvb` @|>2=–hdl?+׳0N U?$6@t; VDr".5|Bb{B [øZJ\u>=#/܎Rad9\fpv\WnU"P# p5% qސĚB''cS/! +Ǖ-Ygb̭CA4lPϽX@sIn"=ٸ|؊yl W+7+p r"fD8WqOL5Bl㲋آ["qE6b3bXh |00]D7`|0>1CI`t%(Bf`r9jVc+αE/~dfakK"e:AuMTDP5K W񎯎GpK+ׂaʀ G@xd=t|I p\ό[5Վ8G/)uKd!7a6LDN@ߎ="pu"j|E<98Ā^ XB؀r_w; UjD>g`r8rS kaX\%R3_ jU>5V\hǖ`l)HFjYaF[ ; }$>jWDk32`F rx`+`l䳁E>p Ą.@!u ̀GpcPy葍-W+ڱhl, Wra6űU BqYcqUcK }l*Rar@zV10`{0 \zD".+0r#"Ԩ#&*pc@R+Qa B\ h0,G F$bP=''uq?2P\Ula<r"1 _b`FѸo1 >\>\8p^`Da䰩#TGaK(<'6`\%~7z)CLCLE 3أbq+'Ӈ $T P D+p#[JY^\P@ &B>܄-qr<5|bJ _M/0y8@@L D/*&|~HEU}|B>>?ă-Q@3H`K}R=6>ĕ|\|Hõ؃~lUDD('#L @&_MtEPA#kE>)9W"' |>܈jl ȧ [.ĕ+ EL'{܃˽+g!5." TzH\6P3%Ụ_?2 <* hyH"\@ >Ѓv\-Up@H\a3@Qre &_G0l`\F\L32`OJB%|ታd rN!T#O(#YB [ъ+#%#܆f18 rzr2.~l _Nŗq"\ld oŧW#O [TjE>6s>3_aOv@Qx\4؊{\45#r7L3Q01 ]-WbGV/`SC(8ň#@ϭ"6l܏.=Џ n >1\M[[N~lu3\N(l$F‰M*߾l9:t6198@N"[nÖ˱E>zDC!G>\2'h&AEl3!)R!2H)ЋH\E&/9"[}O@O| pȹN!d=X@1px|?Dq">>1Wd㪾--11y"[K?Al+S< ^H0j \ `_q/#6*% X&gb'R 8\;JlX9lpeV#G>RW"BΝJ|y49\#:WLfb1Y[ %!Ѱ0,&"$%Q˓Q\Õ pCcF3q ď/!" @N8+l9y\45z^B ibs5>A#  J~d B%:(&10lcׇf&>w da Jzp@lŗ#Ff/&U`\a˝"=O܉D^ǧ>X3H1pdyE0م~M;B`s/YHr?,rAH {@rN .q" W=l"]=OF8:zeb..7 G&fNd*Ea o D$ɀH@Q lė P=2܊L E:zn%PذDaCDŽdzY,ŠrX1 #'X)^`k "lrr-#yt5|v dt7 *,БőXE^Ɍd:n8۱ s+.y}w>,w DvJ3XaoNPa8ڥLMM0I]dܥ9>c5։E<ӓ3v5OwO dqB$=m|0t5a$UxX dl\b:MLi"PpaG4DCO@z>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~doomsday-stable-1.15.7/doomsday/client/data/graphics/brflare.pcx0000664000175000017500000001036012641367670024133 0ustar jaakkojaakko ??HH @ "!#"%#"!!#%&'&('&%"!%&'(*)()*)*'&%"!"%&(')*+*+.-+*+)*'&%!!%')*+,-./0/.-*+)*'&#! "&'*)+-./4243454323/.+)'&! !&'(+./325686530/-*()'&# !%&*-/45468:<;:8634/.,*('&%! !&')*+./23689>L[Ÿ`QC;87434.-)'%" &')*./3578:Ft}ljk{{O?98543/.,*)&%!"%')*-.358:J|re\WPNMORX`oS?98630.,*)&%!%'*-/3468>ik\QLIJILJIHMTbwM:864.+('&"%&)*-.357:Gn\OJKMONPQPONMHKReW>864.*)'&!  "&')+.3469PeRHJMOPRSTVTURQPNJMXb?8640.+*'%!!%&)+.3469Q_MJMOQTVXZ[\ZXUSPNMJQud@852/-*)&%"&)*./368O\JKOPSVX[]_abca`^\YWUROMJOrd>842.-)(& !&')+/358A\JNQSY[_acdghihfeb`\[WTPMHOtX<854.+)'% %&(+.348>aIJNRVY]`dgimoqprqpokhfc_ZXRPMIRM964/-)'&!!&()-/369]iNKNRVZ_beiorvwyzyxwuqngda\XTPMHWr@854/+('%#&*.348?xUJMRTY_cfmrvy}Ã|wvokfa]XROLIhT:63/-*(%!%')+/379`cKLPSZ^chnrw}Œ{wrlfa[WRNJQuA84.*)&" &*+-/38?uSJMRX]bgnsy}wrjd`ZUPLHcS:63.+)(%!&)*.468NfJLPTZ`emty¨}wqic^WRNJRh=84/-*'%"%')+/469wXJNRX\chpw|ungaYTPLH952.+)&!%'*./48>wPJOTY`enu|Ľxpic\WSMHdR853.)&!&')-/48CgLQV[ahowĻ|ume_ZTOJS^;63.+)'# !&(*.258KaIMQX\dkrzĺwogaZSOMOd=830+)(% !&*).348c[IMRY_elv}ypibZVPLMz@830-*'%!&)*.458gXHNRX`enuƵ|sjc\VQNIA84/-)'%!&)+.47:jYIOTY`fovȸ|ric\WQMIF84-('%"!&(*/268hWHOTX`fovǹ}sjc[WQMJC83-)(%!!&)*.358gYINTY_emw~ĵzric[VQNKA84/-)'% &)*.368T^IMRX]dmv|˼zqibZTQLMm?83/-)'% "&)*.358JcJMQW[diqz¶voga[TOKPb=83/+*'" &'(-/48AiMLPT\ahow{smd_XTMIV[873.,*'# %')-.26;RJOSY_emsz¿woic]WQMHsP842.*&##')+.469`[IMQW\bhou}±zsle`[SPMyC953.+*&!#&*+.48FlNLOUY_dkqw~{upgb\XSMIW^;72/-)'%"%(-/47;WJNQV[`flsx~˜{wpid^ZSPLKyN964.,)'# %'(+.358LgNKOSX]bfmpw{~yupjd`ZWROITc>843-*&#"&)*./48=~ZHMPTY\bfipuwz}€|yvrnid`\WSOJNO963/-)'% !&'*,.259IvQHMQTY\adhloqwvwvpnkfc_[VSOLId_=853.)&# #&*+-/37:_hMIMRTXZ_cehjmpomifda_ZWROMJYrG853/,)(&" %&(+.468=vcNJLPSXY\_bdegfeca_\XTROLITP974/-*)& #&)*-/258A}dMIMORTVY[\_a`_^\ZWUSQNMIWU:852.+*'%!!%')+.468CnQJLNPRSVXYZYXWTQONIK`T<8520-()&"  !&')*./469AhZLHMNOPRSRSTRPONJIPvwR:8540-*)'%!"&(+.3568=ZgTLHJMNONMLJN^hK:853/-*)'&!%&)*+./358:JosYPJIJIHIMRiyXA9853/-+)(&"%'*(+./258>Oqf[WSRSZd|yZI:863/-+)'&""%')*,.3589=KYvzaSC98753.-+)(&# !%&*)-./3589>FNTY\][YSLB;986542/+)'&! !%&')+./045689:;:98563.-*()'!!&')()+/234565676530.-*'&%" !%&'*)*+-./.34232320.-+*)(&%! !%&)*,.-./0/.-+()'&%! !#%&')()+*+,*,*+*)'%!  "%&'*)()*)*()('&"! !"%&'&%#!  "!#"#"!     !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~doomsday-stable-1.15.7/doomsday/client/data/graphics/boxcorner.png0000664000175000017500000001563212641367670024520 0ustar jaakkojaakkoPNG  IHDR@@iq :iCCPPhotoshop ICC profilexwTTϽwz0)C 7Da`(34!EDA"""` `QQy3Vt彗g}k=g}ֺtX 4Jc `23B=ÀH>nL"7w+7tI؂dPĩق }F1(1E";cX| v[="ޚ%qQ-["LqEVaf"+IĦ"&BD)+Rn|nbң2ޜT@`d0l[zZ ?KF\[fFf_nM{H? }_z=YQmv|c34 )[W%I Ȱ316rX7(ݝ ⺱SӅ|zfšyq_0sxpєqyv\7GSa؟8"Q>j1>s@7|8ՉŹ,߳e%9-$H*P*@#`l=p0VHiA>@ vjP @h'@8 .:n``a!2D UH 2!y@PAB&*: :]B=h~L2 p"΃ p\ u6<?g! DCJiA^&2L#PEGQި(j5jU:jGnFQ3Oh2Z mC#щlt݈nC_BF`0FcDa1k0Vy f 3bXl `{ǰCq[3yq<\ww7Zx;| ŗ]8~ M!8Ʉ*B !HT'\b8 q$C'bHBvay=+2Mv&G&Ec[ [bDDĐ I* Zc0&8(&iYH~Ho(%46h0װu wKDŽ7EGGDDōFG7FϮX{xULQ̝:+sV^]*uՙXXf8t\DѸ@f=s6'~_ ˍ̮`Oq8圉D]SINII\7n5ewrm\J`ᔅԈ4\Z\) /ד>aQ1n3|?~c&2S@L uYY5YoóOHrrsNy};_-cZuuk/\?kÑ)*0-(/x)bSWr±^$E[nEmnfmOk%%%JY׾1ꛅ ˬir]+wZiYYGgʿs{?T'U߮qiݧo۾C*זԾ?=xΫ^P֡ 2mjTl,ixwxHȑ&JG˚faԱc7sŨZr}wN>8(mP{nLGRHgT)S]]m?x3g]8wn| ƺc\x'ߥ+=/_u=wvWO]c\n}Ϫ'l:o\:xviMoܺ~{;˾;y/Ylx~XHQc?:b=rf}Icda)iDӤ)ϩV<|~W_}oοDΌ\«ï-_w>~f~#zGPQc'O6gAMA|Q cHRMz%u0`:oIDATxb?H4h@#>h@ Z܀UR/f)@ r#@1RzZHeq AVPP`URR%$$(//O@@߿ ~bz ۽{Xn޼p,b tsj @+1@<^cgpssohh}@wƏ?2{СC'Od޻w/Ç`FRH@zD&XFFCff&.L )_@RXnzÇ,'Nݿ?fx 9@u(r<E R ~\\0%|_= 74 *5C%X XyΝ;9g={ (̿ę@?#Dv=`:LPdkH<yP GP5 O#(UqCO.7ydwJ c 9 ^ Ub3444QƒȂ7 ؾ W pB@ avd`AɉdO/I @z~;'ND((Ukrh a??{ Fw_yyy3''[$MF I H  *c6mC^^L NG4ߡg H (@Uq%TwP*ի).AAHN,?58$APJ/D VX(@!~נ◐ tk j`'Dz3Գ ,T/ ~>?/㯾.^^އHN^ t=1~ 4d с[<`P2~(r ?zAYT?(0@*233{ׯ_|?PC <3ng'?"&` <#3(| ]P,f@dHf zߡj?B`[-㟀@۷oY(0[GuH /  Ag e YI&!{+Al@ y`1 P,~b@-,KTVVm>/_-oB" @>?"=5aU"K@ ǿ~EPF0,P@񛍍#0; _@ g@z uuu<,3@r78S A#R@!<($$$c߿_e7y@0=*V@R@+ͅ2 >VJvXyTj4X (إ~l_`*^SL9T@XRW޾}4 V(yϠ$(HuRNCDd0iK _fV+%@ra3 pł0x{AcҠDV [AP5*:X=o|a f?`6fN|Pn+@CY N0@A*};P>((;A@oʚБ"Po 4O`9FH+Ɉ ObV7(@YT σ< +i @ ؠ J1`C鏚k!!!h8b @8E---Ay yPl<J >(σ< 8( 0 CPA*Us416;AY?abv` 0@ Z{:(X2 9#~1 BzP uqpqq}ĭ p,IP>.A-[Ȓ V-Úlׯ)+>+ yԎU aPzg{϶B 8eL? \q + e%-,y6j3 GBqka`  PT0A- ?dOXu@ `LY ?Aa\1N-TK)x™H`T[{P:+LLq) lCB4FJȣ)m|ROQvf+N@@<L D  `a@ 4&P #**'p+߿3hȖex\2a! ! 2@plx py'0]r4i 8?u-a@ σfx%m`9ƃU`zٳge~2ء 2dZ}~)))`RlfL:7oٲe?>@nORaM444}&c.l"@/z'00XoݺSиl` @c2 8Y^SSNa`V mf@AQjAgΜ18q0EVf 8prr-,,KG#M,J*P`GSPPPƍ!?FFlf* V&L O~6 z//5kihhg9r7fbeZ0Եk`Bp@ZIu@Xo>}Gv` 0:a@5j666Q-"0b@n σ(y$V`q@8dҪ* ]LMM=cA{e @=jX@7 S‘#G I2 - \&"""U[` L\ v2 БfT\ J|AU |||De0 %3C%]wsf6܁1/qAEXՁ `h#D5X&(H|!P2`&&&R[ @wg>@\#?mpss??MGfdЬ ۠M P%CCC^y.^MʒDp,(nd&}`򋟟2h:ԱT0 A#G#K $xPUuP>JWSS } <dSQk A|`6P@gu<\@bP?h=P>;Hyl&(̐# , *CBBdUŔP4 1~ AI=`8 4 ``^#,cyP uP^b@`^E58v Vh~ a < w5?R hغ!ب-8"o PaM,,,e޼y~9.oZ[C܅T A񧨨y`U۷?Ae(P{%کl L<.-nL"7w+7tI؂dPĩق }F1(1E";cX| v[="ޚ%qQ-["LqEVaf"+IĦ"&BD)+Rn|nbң2ޜT@`d0l[zZ ?KF\[fFf_nM{H? }_z=YQmv|c34 )[W%I Ȱ316rX7(ݝ ⺱SӅ|zfšyq_0sxpєqyv\7GSa؟8"Q>j1>s@7|8ՉŹ,߳e%9-$H*P*@#`l=p0VHiA>@ vjP @h'@8 .:n``a!2D UH 2!y@PAB&*: :]B=h~L2 p"΃ p\ u6<?g! DCJiA^&2L#PEGQި(j5jU:jGnFQ3Oh2Z mC#щlt݈nC_BF`0FcDa1k0Vy f 3bXl `{ǰCq[3yq<\ww7Zx;| ŗ]8~ M!8Ʉ*B !HT'\b8 q$C'bHBvay=+2Mv&G&Ec[ [bDDĐ I* Zc0&8(&iYH~Ho(%46h0װu wKDŽ7EGGDDōFG7FϮX{xULQ̝:+sV^]*uՙXXf8t\DѸ@f=s6'~_ ˍ̮`Oq8圉D]SINII\7n5ewrm\J`ᔅԈ4\Z\) /ד>aQ1n3|?~c&2S@L uYY5YoóOHrrsNy};_-cZuuk/\?kÑ)*0-(/x)bSWr±^$E[nEmnfmOk%%%JY׾1ꛅ ˬir]+wZiYYGgʿs{?T'U߮qiݧo۾C*זԾ?=xΫ^P֡ 2mjTl,ixwxHȑ&JG˚faԱc7sŨZr}wN>8(mP{nLGRHgT)S]]m?x3g]8wn| ƺc\x'ߥ+=/_u=wvWO]c\n}Ϫ'l:o\:xviMoܺ~{;˾;y/Ylx~XHQc?:b=rf}Icda)iDӤ)ϩV<|~W_}oοDΌ\«ï-_w>~f~#zGPQc'O6gAMA|Q cHRMz%u0`:o_IDATxb?H4h@#>h@2@cG>+P K@w i, bU :Kx$P@K8 9i0^ ğe(@1 Kڃ@A%w35R/b ʣ$(L Di"   @@|5yx;k@@]7b' >BF"'@U"R5j  ~B&"5@PL*ZԈ2a @az n$)P@y #U @fP (z@)"61 σ I"j!2h)1)*Ρ?ل2 JT-jS@R4Pm PD $@ W*:d>I IPC 3H" P%@` : p)K F\/7> @xK m8dNL-K< H#I#4 B' @`2P`>I"f@ @ -vP $2DLSA f1ј b'F@ck g "3jMEt D6aPMp3Xđ* RA`ASDOZCpq@ZB"g@:L&C- d]!P#apP_Z)@.Sb%A1O@Q:&iHrh$h=E7DA@l F))^< @^, /fS{XT0@T[5@Z.<:3/,(%!ƿ¥|xtnkfc`]YWSPLJHGEA?;972+)$#þ~zwsqlid`\YVSOLIFB@>=9853.-,'$! Ž{yvsnlgb\[VTPMIEA?<;:631.,&$! ſ~zxurnkfd^YSPNKGC?<87652.,+'$"! ÿ}zvspmhe_\XRNJHDA?9721/.-('$# ž¢~{vqnkfd^YTQLJD@>=850.-*'$! ~yvplec^[URMJFC?;630.,+)%! ~zuokgaZXRPLHEA>:62/,*(%! ƒ|xwrmhc[XQMHDC@<751.*(&$# }{vrlkec^XSNJIFB<;940.+*$# zurmida]ZTRLIFB>972/,*(%"! }vsmjd`[VROLHFDC@;810-*('#! |xrqje^[UQLIFCA@=:62-,)'%! }ztpmhc`XTOIDB?>;74/,)&$" ~xrnkgc[YTPHD><:8731+(#"! }zrmif_\WTNHB>986531.*'$" ~ytnhc_ZWSNIC<;7510,+(&$  wrmhc\XTPKG@<8751,('$#$ ~tmhd_XTMJF@;852/.+'"! vmfd`\UQHD>:741/+(&  sgcaZWQLFA;821/-(#! ka][VQLG@<73.+*)$ ud[YVQKFB<920,)&#! k_XSPJD@<85/-)%! f[SNLE?;730+($  _VMJE?:42/+(%  [PIFA<71.*'%" WJD@;62-+'$  PD><63.+'$# NC942.*&#  G>51.+'$! D9/-*&%  ?2-)'$" ;/)'$! 7-&$" 7)"!  2&  /$ (  "            !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~doomsday-stable-1.15.7/doomsday/client/data/fonts/0000775000175000017500000000000012641367670021333 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/data/fonts/normallight24.dfn0000664000175000017500000104401612641367670024520 0ustar jaakkojaakkof## '! '" '#'$3'%D'&Y''l '(v ') '*'+', '- '. '/ '0'1'2''3''4 ''50''6@''7P''8`''9p'':' ';' '<''=''>''?' '@''AN'BN'C&N'D;N'EQN'FaN'GpN'HN'IN 'JN 'KN'LN'MN'NN'Ou'Pu'Q)u'RAu'SSu'Tcu'Uuu'Vu'Wu'Xu'Yu'Zu'[ '\ '] '^#'_7'`G 'aS'bb'cs'd'e'f 'g'h'i 'j 'k'l 'm'n'o''p9'qJ'r[ 'sg 'tt 'u'v'w'x'y'z'{ '| '} '~ ' '0'B'T'b'z''            :=! $@A=JF#3* %1  .HK9! 'J] ]X=.=/191  %A:.E==E.  ,# !6.3Ta _U366 =G(((GKFV! #FFFL >:_333riiisjT/ . gV:]]]]v<_s}s]9333F>  5;;;`..;;;`6  3<P;;;H=:. Nw$D]]]fiiifF <= Vc333^g!(((]uuu\'>|ZNrRRR[.^˒]]]zߣD 2oV=nf8 333_X* kg##g k*>]]]g333ye# 5 //]]]cRRR[%>mm>TV]i 333en3j c U] =}iii߭uuu֞C8|iiiybMtߧ(((U (mo/ 333YRRRxiiixU 333eYXv6  6 sXO333333]+Ay,Kߥd((((h(((}\[(((e(\__k333a֟i Y֜v.+tߨ\[ gi6 Jߦ{(8 };;;[9[RRRi<.nFFF|Q2_}FFFY333a˒M 8]]]kHJ]]]l8  .g {>Uݴz: cNKuuuuGD]]]tߦH\`]i333Xb#Hmty(((x+(((aߩqVxh*$(My(9 }(((jcs~J2o {8M]]]y;;;[333W}> _n!#o_iiiCߧ˗﾿x(Yݴ{< .FFFhiiii3cnia\`XhHurG33373339333\֡|bza 9}iiixbNx ;;;|\(uuub`G|WHh+/RRRiFFRRRi/RRR>˓֨˃(]ݴ|AH֚i;;;[H D;;;[\`M֛d! 465B? 4ˇ߭˚ˉ 4Uo `˒]]]{֟m(`iii ˍA! 4.Kߦp''sߦK *b((({@'/1Cާf91/'cT!333_;;;j/,;;;g;;;_$\` @y` 9vXT@.(((hN  , d yF  V333˒\% ,% `cb`N(((333a+(((6]dfluuu{ifd]6*333im8(;;;c;;;`%!333^RRRb,\` :u]Yߧm#$n֜U2kߦh6 %GZdp֞uuu{jdX:6 x߮6%N譛]$h˒NM˒h=RRRh333w^!ih Di!.qiii]% 333]2\` :u] (tu2< vuuuwR  $N֤]]]e<'Tq˖rU  / k´;;;}ORsv+;;;Z~= $333c;;;i..;;;i;;;`%\`,zFFFF5FFF1 y k%;;;\D F;;;\\`<;;;A*  . vFFF(((m333f333(((333]333A%  }[ `;;;qmy39z26w;;;w9 Y 2;;;Z|96u;;;[Vݴz<  #DZ``Y@ Y_agjc\`!HN* ky38Y'  (;;;>D2NzrNRNoMJwx(.}{56wFFF|FT >aJ GaMݴv5 :=!:=!!jD KRRRuAC]]]tK\` =HdU'333d]]]e/;;;B֙ ~s9 >iiijX!2riiiv\@b{]]]`(+jUH_333{x1 c\[c 5ߝ[$ =H =G9o333i**333fVV(((h*\_O\Zv<1h_ G֧߬['ek' ]FFFtV]w˖`Qp oT($UcO\MbAr~~r@UX >H;;;?FFFL! .ߞH'[uuulu\%2j1  .hf++k333lO>b֘%6luuun: 9iiinp8Vg >HCߡ@ JjjH =@ <>! #0% 3 8!6[ k< = F, +NY [YJ'  $H^ a a`M, >M8ecbhN}Z<>! :96U _ _U668   Fvh%     @yiiin<  :]]]nA @@      #uuuM<fn''ng (hC$+ 5FFFh֙OO֙RRRg6 Fxx$$xyF>iiiLIIiiiL>                #D\bbX:=Yab`N,'3# A]giidR/ %A@!.QdiiiiiifT/6UaaV81Rad^N,$K^ZD!*F[] Y[ [YH+' _֙tY#E;;;e> 5 d;;;E >\ 1m >V`JuuuikOARRRf333f<L\  %M(((iߤ333fK$ Dޛ|Vv޴{h6  .z;;;^V]]]G 2v@ ]]]F߭(((a@}ˎD 8h賎zߨ]]]d5 #   R}֙C 2iߥiiif333_333]RRRdˏߤg. >]]]ffNVo64RRRI[MKi]]]zY%i;;;e!\cURC% +q_# %CRUUUYx V ^ [[ (((a! XFFFzcMbFFFz_+KdhFFF<RRR;[bF'iii8333E`_ 3 j֚x\G:5:Rwˑh%31: Q!]FFFYXߥ;;;c!\k! bY# X֜FFFo2.r]]]tFFRRRv{3$;;;byTRf, 8Y(((nߥv $%%%%%%%%$lˏnT2 %ui(i]]]n`MVdfYFAXiii|O j_JRRR|;;;\Jx ;;;^!\i H֛\ !o_2j z,+|}:+q]]]k5*u}9  %DguuulFFF^<2RRRKRRRWRRR[RRR[RRR[RRR[RRR[RRR[RRR[RRR[RRRWRRRJ 1=iii`RRRjc>!tcT;;;sUNlߥ|ˌ333j.c_/q[=]]]i}333u;;;Z!\X:$,333ij(  Azuuun9 ,miiisHG]]]tt2'RRRa]]]wH .> $ B;;;B'$ B;;;B' 1QlmO,rr2V n֚tjJ+5;;;uN/;;;j]QRRRivK qQ#O֞J 5 opH333_;;;Y!X߫ߧ(((eJ!RQ.! ii ]cc(((`  ]D+:k= DHDH.]FFFpiiikgC%.H X [ [ [ [ [ [ [ [ XH/*Hinߦ(((tX+ bFFFk.KwT333ouuuuj[}ߪb]] 9iiinm8 ?, .v H>333g;;;e1  Dߥ_! k߮FFFi]]]]RRR[[< 9uuunzA J|֠˗OFߤ]]]s o]]]n8GLGL RRRQ֟ z\8   <c;;;vߩ;;;P :oT\`k|R 3GCu`cg(((A߬/d(((n{333;;;g/ Ebiq;;;zߥG2twO_m!G(((RRRO [ˏiiii+*iiiLW/ *iiiLW/ $|p<   Cy߰ˆ$Yu(_RRRi[]]]kq!kFFFm/2333h^AuuupA %UT/ߡT>(((Y DRRRn sx}> 2FFFmːG#d(((f(C]]]]\iii_;;;niHrːnT3  2=>>>>>>>>=19\333mߦlF ;;;[uuunA];;;eg{D io#3uuue^ 1tg 2rFFFb%'gˎːˏˏ˓֘RRRAD]]]t;;;c%RRRRd8/N [Un' @ˏz[,+VuuuxF $2Y;;;ߦJ:bRRRj]]]jfA$FFF<~}~~~~~~~~}}3339'Fhuuum;;;l]8$uuu\m$\iiicv333q+!wc!Gz]$k;;;n5  :333y|5*<CDDHb(((333V# *333hp+U}( RFFFZ+(((nOV{%$|Y 1x g' 2Tlˏ nK$ aY 'Onˏ lN. 333U Y]333n;;;i% @vߨR,iOX֛ːN.;;;l|6 +333`;;;`+ 6333rFFFb' Jߦv# <333z;;;^ Mߦ333n,\pr] (ktA  'Fhn(((H.N`cccccccc_N,333IiiilgA$K;;;E]]]]n;;;x5 'xuHMuuuo]]]h3Jut.   Nvuuue+ 333Y;;;Y!%! %f333[5riiisGY;;;Z%mYQ 98R +kY:=!:=!:];;;l˂!  !w֙(((mX6 Am[%Xːߩg.,`˕333f w[A]]]svQHF9 1KM@@\333~] 333Y;;;Y!4֑ k[GKpQ a(((}YHbRRRzX C]]]m56oTRs9 *mj% =G =H .HH32 GD*O@ D֘FFF{>,RRRa˓ːːˏ֕RRRA wߥuuuupߧp: 333T;;;T;;;A|xj* :k˓uuuv]]]d3 TcY{\En, O\Uc   ;;;VQ((((fߧx֢}FFFI(  2b;;;HFFFhC333>(((@ 5bߥi1 CRRRg;;;g=333@c8 %]ˏb'FFF@֘d+  >HVg H@ Kߤ}333ߦ֙FFFdrFFFnߦq3338 GdmkgfdfbO/ 'J] _a_Q1 !/ 2Rad^\F$ 2T _ a _Q/ #71>Y ` `[A  %@= <>!N}Z $A=$g˔}jT:/>Yk|i5          @@   .jiiitydUQ\m smH(hC 'Viiij֚֚ f>8\FFFdˎ￿]]]aaH( +AQVUN>,                    @A! %McgfbO2$C]bbbcYA' %Mcgfdff`M2  %Mcgffff_F  %McgfffbO.#A\bbbd\D( /! 5, /! !6+ /! >H2 /!  $@= (HF# #<666QT(((Hiiik[( @ lː޿333cD(((Hiiijb6(((H7 (((HZA kː333eG(((?333>Ca((((?333> C['(((?333>#\֘˂ (((?333>(((HߤX+oK(((G{N <= .333u;;;s/333\˓f##Yu˒wߡ6 333\˔ːː˒]]]kM333\˔ːːːːˎˆ3 333\˔ːːːˏ֕RRR@%\z˒w9 333S;;;T\z9 333S;;;T333Xz8333S;;;T!_֜X333S;;;T333`~K!gg333_sDTVRT 333]333dRdߦF [iiisnQCGVk;;;n8  333]333dJFGObyߧˏO 333]333dJGGGC1 333]333dJGGF9 _uuuspQCFVk;;;o:  333Y;;;Y!b~>  333Y;;;Y! 333_>  333Y;;;Y! [ˑv.  333Y;;;Y! 333e]]]m=R˒k 333fFFFn<\_(p q* 333Y;;;_,> bD }A /C: 333Y;;;_, ,X]]]tn:  333Y;;;_,  333Y;;;_, F ~C /D< 333Y;;;Y!b~>  333Y;;;Y! 333_>  333Y;;;Z' Vz2  333Y;;;Y! 333` r5 Coˢg 333c r6 \`J֛;;;;;;ߦM 333Y;;;Y!xi%m 8  333Y;;;Y!H]]]vd 333Y;;;Y! 333Y;;;Y!'l9  333Y;;;Y!b~>  333Y;;;Y! 333_>  333Y;;;^<T|x3  333Y;;;Y! 333\(((iiis.9333o c 333](((w1 \`# nvv p$ 333Y;;;[$ /c CߥzG 333Y;;;Y!]l/ 333Y;;;[# 333Y;;;_,  DߦH 333Y;;;[$d~>  333Y;;;Y! 333_>  333Y;;;npw u5  333Y;;;Y! 333Y;;;lt˒qR z]]]pv|a 333Y;;;nv˒r*\` CuuunCCuuunF 333\333oC,CxJ [|$ 333Y;;;Y!3;;;sD  333[333n>!  333]333bFCC<*\~% 333\333oA$!!!!!!8}>  333Y;;;Y! 333_>  333](((t5 333Y;;;Y! 333Y;;;^Ci˘|TOwa 333Y;;;^Cgd!\`kj! in  333_333q;;;es8 fk 333Y;;;Y!lU 333_(((q333Y333X333X333X333R > 333a¿ˌ`(fm 333_;;;n;;;[;;;X;;;X;;;X;;;X;;;X;;;Y;;;g]]]>  333Y;;;Y! 333_>  333c ~: 333Y;;;Y! 333Y;;;Z*.t]'@wa 333Y;;;Z*,qˑV\` >u](([~x@  333aFFFQhi 333Y;;;Y!n] 333a, 333c/hgC\fcK# 333a>  333Y;;;Y! 333_>  333fr# 333Y;;;Y! 333Y;;;Y#3wr( 333^;;;}fdcY=fj 3J 333^uuu} v[YZZZZZl(((>  333Y;;;Y! 333_>  333c;;;vA 333Y;;;Y! 333Y;;;Y!<]]]c o2 :wa 333Y;;;Y! 5siiil@#_` 8iiiquuur9  333[333l< /T Y`y! 333Y;;;Y!1333uD  333[333l< 333[;;;e5[|$ ,guc 333[;;;i:/u>  333Y;;;Y! 333_>  333](((FFFRRRiA 333Y;;;Y! 333Y;;;Y!,9'  :wa 333Y;;;Y!:;;;m333oKj`_RRR(((l333Y333X333X333Y(((lRRRb 333Y;;;Z#XFFFb$ JuC 333Y;;;Y!Yp1 333Y;;;Z# 333Y;;;Y! DߦzF(>nd 333Y;;;Z#c~>  333Y;;;Y! 333_>  333Y;;;i`FFFl]]]jD 333Y;;;Y! 333Y;;;Y!  :wa 333Y;;;Y!Auuum`2FFFob+ +_ߧRRRn5 333Y;;;Y!Fiiiwl** q3  333Y;;;Y!CFFFwg 333Y;;;Y! 333Y;;;Y!'l6Dyc 333Y;;;Y!b~>  333Y;;;Y! 333_>  333Y;;;[3>;;;luuukH 333Y;;;Y! 333Y;;;Y! :wa 333Y;;;Y!M|`V(((q., s[ 333Y;;;^+ (jߨ;;;b#K֛}> .A< 333Y;;;_+ 'Q;;;wq<  333Y;;;^+  333Y;;;Y!F|@ Qzd 333Y;;;Y!b~>  333Y;;;Y! 333_>  333Y;;;Z$:(((noM 333Y;;;_+  333Y;;;Y! :wa 333Y;;;Y!Y֜`+(((pVU333o, 333]333\CDRu~W!f]]]rjK=ARj333q<  333]333]A>AJ\xˑ֚R 333]333\A>>><. 333Y;;;Y!\iiirmM:9F`a 333Y;;;Y!b~>  333Y;;;Y! 333_>  333Y;;;Y!8 quN 333]333]A>>>=2  333Y;;;Y! :va 333Y;;;Y!$ib CFFFl11;;;mD 333[ߪ˒FFFc1  (cozߧ: 333[赴~uuulQ333[ߪ~~~|{ 1 333T;;;T#YumkK333S;;;T\{: 333T;;;T 333_~> 333T;;;T6rr3333[赴~~~}};;;;333T;;;T6s\333T;;;T +x[ @QQ? (((Hާyd3Fk֛֚333gJ(((HΎqf9(((H3335333>333@@ jlY%(((?333>Cg*333>333@!333`}9333>333@5 n8 (((Ha333>333@'`C333>333@ 2uC  @@  @@  %MbdccbU< 'F] ` ` ae_G* %McfccddbR8  %Mbdcccc]G# !/! #A\ ` _aabQ5 /! 5. !/! *333ih/ !/!  *F@ %Mcfcccc`N. !/! +5 !/!  *G@          Jv;;;b$      , oZ @ˌ@ 5ߞ]]][H(%                #A\d a acV:  %Mcgff`M/$D]dbbcX<  %Mcgfd]G* $F]dd`H'  F_fffgiifffd[>8= !6+ =:/8#  <8 #/ >=6K<+HD!AH++HA,Nbfffffddfgg\>=lrd3(((HRRRiT$@ lːvf6(((Hߦ333hH /kߦ(((V% 9/ <ߡG C[' AG 6`333@D֕A#TOO< $ߝˌT 3m8 8k3  2l8 R/ Tq֜wwFFFjC333\˔ː]#YuߧwwRRRjG333\˔ˑ֝֘K( k˓9 2ˆˎːː˓߬˙˒ːːˌg*Uc333Xz8Hl'`VOj Fː]]]l3.FFFh֘>  \{M 1y|9 2ku.  ,tk2;;;<֔ˏːːːːːː˒f,U֚ytRACXz˒nC 333]333dJM`= [~uUCDXz֜xG 333]333dMTmFFF{iiic,M֠XTfFFF^w. 1CGG[VGF>+]i 333_> 2RRRnːG1FFFnːF :ss< nVO(((n* 3suuumC*rߧ\Tˑj$$i֜V 8DGGGGGNm˓R@u(((wH! 'T]]]r;;;l3  333Y;;;_,A[C(((yJ# (U]]]sRRRj5  333Y;;;_,M]]]y֛G d5$# #ti _k 333_> fkR o( o_8uuurr'# n]:FFFlFFFl<$fj%#f֜Y X֜g# (fߨg!# l(((z=Juuuu_ 333Y;;;Y! {e$l333y>Kuuuub 333Y;;;Y!*Tn| j__k 333_> @{p: $oXN333o,[D @wp: Cuuum s<+\֜t+ +tw[[wt, Hwt+ @ː˒Odiiif+ 333Y;;;Z# .b @ː֜Rduuug, 333Y;;;\'A333KdAj__k 333_> # n_ C]]]n5.;;;nߦM*(((nhcnM| yw1  3uRRRFFFu3  Mߦ333p.ig`m J֛RRRvviiin6/FFFnߦKY֜ߛt5 9;;;n;;;n9 3tFFFn<iq lR 333_}s [d]]]kg'ir lT 333`]]]oiiicuuunߨߦQ'mRRRnc9j__k 333_> ,333oN 6iiimC =ws< lk@pYO333o,%n;;;AGxxG+twGlpn[ 333ag2 lpp\ 333a}+ ,bqmOj_\i 333_~= ] o$Yo$!p_ :pDU q,' n_Y;;;2!vx!!f֜Xiq kR 333_(((q333[333X333Y_J' iq lU 333^]]]yh y*9](((rߧˎDj_Ti!333`{6 :p|G6(((nRN333pAbp% 3]]]nːRMwq:  5 }VbcTˑj$[(5;;;vA  333[333n>!Y( 8FFFuA  333[;;;g83mߨ֛O .[]]]y333\j_ F}m,333j;;;e*ktc֛FFFn1/;;;oߧx(((}Ui}l  .vsG\`Cov.  A֚M`uuug, 333Y;;;[# @ːMfiiif+ 333Y;;;Z! *r;;;q6 [uuug.j_8}FFFn2G;;;^ Gː]]]~cc֡]]]n3 D]]]333ߦM(nmT u]]]k>\`8333o s5 %l }<Giiit` 333Y;;;Y!# n }<Mv` 333Y;;;Y! 6333uk!  >;;;}3j_(]]]c\%t]'o跂s= >xi'o333o,!cn*Uz֜sF 333Y;;;Y!$o333p1 <ˍvVMdRRR{]j` 5]]]gumJ>Oy˓h%3RRRs֝JOv'5]]]tx< Jwz3 M|u/ \_]֥\D>>>>>>8%#Yuˑno֜RRRiF333T;;;T%_tzߩiiijG333S;;;T :]]]nM =ߧzl: dYCj}uuuo333h5 g(((t+/FFFnVhp .iiia r6Y֛YUX6w˟~~~~~~}yY(@jːzg6333>333@G(((l￿zD (((?333>QD!UsRRRgCH@ =jh2 :pOO]]]W. =ˈD 5FFFd9#_˂  =@ 9/ $D] `b^b[>  !/! *Jf}qKA6 /! <>!<Ybe _R1!@<(H_^b^\A 28  2*12AJ/!AK368 GcifdcccccccbY>   .[iiip֜uuupu1            8dmZ, 8Tb;;;YFFFL6           1UficK$ >@# Hbig]A #>< @F*35*31MB 35+3+335*3%_333< :M =@ $bV 6(((b/ := %YAd֚d% :wa :wa1]]]gRRRh/\`    ;F5.M~9\iii}V(:c Fߥ}`. ''Hd1 1peQ;;;kFJ|> %r}QX=8qm \d1.b;;;b$ \v#Ciiirk/]p$`u'Dzch n [ ^|]]]Qc~> fߨ\@(((~FFFX]+.333lo2 :wa :wa[]'  :wa :waQd\`/[]]]eߤ> \c%l}9 dM bT `˕i%Nuuut((([\fh}< :wa :waYf% :wa :wa5uuuj;;;g*\`.(((c> \`$k}9;;;YC ;;;\֛M :va333^ ` X Y [ _ \E b~> MRRR}nf T\`c~> :wa :waYiiif%  :wa :wa hD \`Siiiti]> \`@]]]sk/fT f˕] ,sߨ]]]_2 \`b~> :wa :wa[ `*  :wa :waV_\`;;;Y{_+5|J\j 5(((b [3  X= NzdY9  32b~> _uuu~_6\`b~> :wa :wa[p2 :wa :wa :p(((i'\`;;;Y]]]d@_]]]~r+ [O=RN @N=FG/ 8mXARh  V˙xU3\_b~> :va :wa\j>cː r6 :va :wa#hv9 \`Ruuuw;;;sߦFFF=Kxuuun˒k% c}iiioe!Tߥ֜uuureYuuur9 \{:  333a}n`C! UX[z86sZ :waUY!Tq333](6sZ :waM9 \`   3]]]^֖<+RRRZvb+  (buuuQ  RuuulK#XqoT Ch* :ˏa( =@ AZ''`C :wa =@FFFFf2'`C :wa#A>\`=RUUUUUUUUR=1R]]YX]Z@+M_aa _Y< >[ _ `_Q.8V ` `afcK#:X _baX:5.'hϧ~sO683++5 =xc681J@+5 :wd`` ==          F(((mH@U֡ [   K}c   b#6 Q [ [ V B% @ T [ ^ [G# D ]]]]\dduuu_ _Q+(uuuIM*   %,.(  1NG#3*9]]]cDAU%#+'333`F[z8*FFFN: Cߥuuuc%b~> KXT}*b~> $/1$ ,1(!.2( !,/,! *22' %12*  $*  */*  'ty/             Xߨkb~> # C VdFFF]rnRRR`X<CYkouuub[, !B(((Qb333]lnuuubZ1 (Q333_kiiiek333_Q( !D((([ liii`nniiia\3.YRRR`nniii` l g333F'#A(((GM[]]]\iRRRM*2Zcmuuu`T3!;;;I333333333] >#A(((?$$;;;73$;;;= B!3FFF9( $;;;;>.;;;8/ >;;;;$333=;;;KD +N;;;H 1#333= B( =FFF<&  0;;;H333V333X333X333\333a333a;;;O 1[ib~> Fuuuui*D333d/ .ll/  DRRRg> 3nNFC1RRR^Y# .,FJG{. @N 5aRRRD @G,RRR[]]][.H@ .֘Y  .ok 9]Fߡ(((?r]_ib~> Y~ ~ߩOY֛ y}V#h vo vh$Y rlwߧRRRg2 'j wn q˔dYFFFw(((u: Q;;;v nソv$Kb=[]]|< @֘(((l+]X Aߣ l$QR%lߤA !cX(2p~G2iiifyA'lG/ H V Z g ߮Qfeb~> ][/Av:9{ a]c/.b;;;`# D֚i3!3jߦF ]6 /_yXKj3 1ne] zF3/a{<29+!nr(]`c~> (nߥK1FFFju> +(((mA# q p!D(((m+ .ruːY`n#J֙(((n, 'Qu*(}[b~> ]}'Y֞8 Ap;;;[]+.333lk1]v(  *y^\f d;;;_  `u( Cyc\RRRy@YX2]`\`b~> Q l$Ql!Xf%Ax߰s>'gX 1wߦ_! 6RRRmJ3 nR(o333p6 !`ːA b~> \f Awk(333d;;;Z!\fh}:bNO d\` 8FFFq]]]b*;;;Z|G :wa\q <ߦ;;;jR' \`\`b~> /;;;lG3lߥJ6iiik]]]lKidNuuumiiil6 <r(\~mː333l.[vF C{'b~> \` ;;;Z|D D|;;;Y\`+333iq, a333r5 :va\bGRRRhf%\`\cc~> \udː(((n+f333|(((}g.uuu`2FFFkUFv\Jtb~> \` :va 333Y;;;Y!\`b~> g˓XVg\` >]]]q;;;c%;;;YsC  8iiil}Y C֡pK o(((A R;;;q<X333p/ 6333oo(/FFFgRRRm1b~> \` :wa 333Y;;;Y!\`b~> X : 9 V\n .u [ ]r* Mxc\` $F_\k M.DFFF{> fߎiiil6%mT%Y o$Fo333 q61333yX+u(((y> >Nb~> \_ :va 333Y;;;Y!\_b~> 8m R@R m8 ]Y>K{֞ߥHHqA5K֡g[_ ';;;BYMX[XU<! 6rqKX> >xgO]]]r39tO>]]]j vDK]]]k t1  x333w/duuuOA=2pcb~> UX6sZ333S;;;TUX[z8RߥiiiqߥR]˒uuuozf#%huuupRRRi|kTXFFF<x֘A F֡x333<^uuux˒~8!pA /FFFpkr;;;r.(333` q6Gq\( yY 8~~}};;;:cib~> =@ '`C(((?333> =@ AZ' RuuukuuukR ]c*  .g֙i :> 9ˍV%c(((@ .f(((]'C֖ ] T֖A FT2FFFb9Nz֏!Gˏ;;;m1 :Y_kb~> 68+5 /! 683+8V _d _V8\oa _Y@  #CZ ]^ee56<Yb _Y> *O _ ]K%  (K] `]THA,: >% #=8:>#@J/9G3$mV Hdjgccc_N,_kb~>        \m(  Kza         F;;;l1 \pb~> \` :va#mYQ:c~> []9u] =FFFk2 9y333_(]}= HJ.sJ @T^7 K|/'FFFB]]]E(3335uuuE( +TE% (Xiii[Y+ *J8    #      #@Ydfd]F(!FM1 8C/5U_S66Va^O,A k֚(((jM$ C]]]c:8X_K1%/3M֖ˁ Jiiii֐(Ck333`. (]z֜˒ߦm1  D;;;_( 8qFFFn}333q>=iiikSJ~ߧpb$.iiib}K%g]]]txf_cr(((} p/  5 /#buuu~ߦF  KG$ho2  6iiii(((}\=% G `f| >V[Ygni[Vtg! Ay,*Y<v u(((^+ 5 k8c y> N. 6<! 8iiilyTit֙nOgːG  6JK6Uݴz: m_.D@@Uqy`2'M]]]\mN!6uuul|J @֙;;;|A Y |Mg|֝_>nc .Te`F.5`sᅴs]'Yݴ{< j_  #G(((Xg333_nskh6 (Q333`oRRRro333eR( Vp#/ ~}8!333_|TNz֞yVgx˃,=FFFru3.(((f֜X]ݴ|Aj`<Y]]\D' *!D333d/ .ll/ 3t֝] 8v:,uuuc333g:_1 AuuuL9!oG S uuue*'/1Cާf91/'efGzˏnVAJiˆMY֛ y}V#h vn vh$H]]]x6YFFFi=3sg1e֞\iV;;;Z˙HVRRR~8(((6]dfluuu{ifd]6Y}($pvyuuurvp#]c/.b;;;`# D֚i3!3jߦF 5k(]]]`]]]yFFFi95id/g˕V i[333`uuu}t,>]]]o< ih @~`! RsdKH_333mnF]+.333lk1]v(  *y^/i߫Ч;;;<5|FK]]]lj*1rg5bm/FFFB @ kߧO333X 333bp|3.K\_mf_\K.%|D  ' ,J^((([ [Y9\fh}:bNO dT333uuuy`5 5|{5GiiipM%;;;bRRRpHTFFFyJ(= q95 z~< K(((`! (j޴O  rJ \`c~> ;;;Z|D D|;;;YJo( (]]]a֜]!#d֞`[pH(((iRRRty ]]]cC[ߦRRRa(#Xuߥ֙;;;dYbst>Yݴ{= /FFFnRRRf/\`b~> g˓XVg\`XyU/[c Dːz]R]]]fˍ\T;;;vV3C='< []]]]]]]^Z<Xݴz: N@\`b~> X : 9 V k˓]# ,mrY#iiiitfYifuk g``(((y(((j.  !!  Vx= cp\_b~> 8m R@R m8 2iiil]]]mdYJDH> 6 o˙֙C  2333h˒`URU_vuj> 2=>Jpˍuuuh_D>=1icUX[z8RߥiiiqߥR CߦːuːA5 ty%8o|333jj(((lsFFFiAFFF<~}~~}}3339j_ =@ AZ' RuuukuuukR  <֝֝A2y֢d/dvˏi5aYj_683+8V _d _V8CYYRR[cd a ]J#5d =[ _]]]Y;;;^]]]Z333]_C! .N`cccccccc_N,q]    $Quuuy333[ #$#   :T!uuuKzR9]z(((\*;;;^z: +֓FFF|T 9^Wuuub5 +Yiii[X(  *U]]]]euuu_W6 #$+' doomsday-stable-1.15.7/doomsday/client/data/fonts/normalbold12.dfn0000664000175000017500000024201612641367670024325 0ustar jaakkojaakkoPf !" # $ %( &4 '@(E)L*S +\ ,f-l.t/y0 1 2 3 4 5 6 7 8 9 :;< = > ? @A B+ C6 DA EM FW G` Hl IyJK L M N O P Q R S T U( V ( W(X'( Y3( Z>( [I(\P(]X(^_( _i(`q(aw( b( c(d( e( f(g( h( i(j(k( l(m(n( o( p< q < r<s<t$<u,< v6< w?< xK< yT< z]< {f<|m<}s<~z< < < < < < < <  DC:OGFN: 08% 3A]]]PK!:SD$ 66RVA (KCEB @A' 6C BGWJ$5N: /NWG8TWG (H=CYYYCCH% (KYYYV='KWO1%KWK( 333PD68,ˎXuuu\ B333\߂l8 H]8fFFF= JboC FFFf/8h2e;;;U* ? @Q:FFFdk*183ːFFFd8FFFd2y9AC;;;eco.+s֛>.oz81<x XFJ: Tvfˣˑ5!~<$|Y(((i333n+V RRRDˎRRR;<@$u֞1 U;;;dFM;;;IG=F#]]]kKYC jFFFWxu# (((Vf]]]do#>1>1!G g˕FFFGDXXXXF yV2˘6'*z߰qtiiiY '֛uuuDjFR/ \,iFFFB99 ^d$|(((:HN;;;9֕UMˣR iKY dTy! ,LYk 8߰A]]]^֚1]]]^֚1Pr G;;;;FFF<RRRbR >iiiRDCiiiR AFFF_߯߯FFFa;;;TRRRV O [!';;;;;;e C 0iiiT333Jjh !]]]vR DFFF 3(((@(((@CXXA.{'=MHN2FFFN,Mc߰OOuuui'!sRRRbi( oFFFn3(((J'@'>(((E nH8}}8`߬A  (((e˟֧(((f(;;;h֚.>{uuud# R0 (O 333a;;;W1iii(333B333B#CC$iX>MHNd+,V3ˣ u<֘333K:FFFA+` RRRnw%333[3X˒uuug'Y˓iiii'333EmD 4jiii{iiiyiiiyiiizuuug 4 k߬C++ (RRRYFnHt;;;333z.֝N RRR[333[(((LuuuTUj MCCQ֙g(AC Qߤuuu_!<]]]p%} :HNVJ "FFFNuuu333V:/RRRD;;;niii;;;Y 3ߩF `.3śAicX]]]f%\˓uuuj+ R{D;;;8FFF9r N֣@;;;?8`֩֩Nd߰(((~#{Y333n W  <@+(((LCYYC*F;;;lF UFFFcFMF<;;;C IA Wy֥x~ JRRR^(} c (֩6 Iuuuj$*D+(((L %M333h֞FFFDDYYYYEFFFRCBuuu^Yˎ,1\ߡiCˎ/>֛ -333^333o2˕G ? @iiinFFFWe=*(((` RRRek*18^9=iiif$!l:(((Liiif$+lA i֚2 JߧYRRRAiiii,e=iiinFFFW3<FC 3#8. ,{RRRo@ 3= . M M/ 1OXTNTYC=Gs]]]jJ$F=<AGWJ$5N:'KYYYU:<UXH 'G:<UXH $JWN, 'A13QWT8AD!$F=J/dl!:˒8aa_.:ݴC 1515361( ES< (MWWR65RR5CYYYU<'KWWU<CYYYR9CYYYYCCYYYYC'KYWVACM+ CT8CM+ :R=CM+ DYCCM+ CU:FM+ CU:8TC'KWWO/ CYYYT9'KWWO/ CYYYU<'KWV@8TYYYYYYC:1333T?U|]88A(((`Kz=AFFFa.ACACNzRRRQAxA5Ax69Aw6i/AxAUFFFdxA_3AFx˔[A_Fx˔[Ab+s;;;P6C;;;E˕(((hJ#~;;;Tj˚RRRFFF333ci gXDF;;;Q Xt(XCXCHX*[KX(HKXRRRiX(X֟_s(X_XY>X XC>X XF 333R<F FtR J333NO 賿H6ߩߩ6XOuuucxf333p;;;>X}(((|UX.X,uuudFFFypxFFFd]]]GXyZNX(HMXu,X(X(XY]]]_uuu{iiiy*X֩Y]]]_uuu{iiiy*X߰Uuuu_uuumuuuL(((DFFFY;;;]]];;;^;;;GH n333F8/uuui(((YeˡhX֥N!zFFF|. 'X+!;;;zkX@XC!z;;;{CRRRRnuuum]]]V#XNX(HNX˞RX(X(XY#;;;}/' FXѿG#;;;}/' FXѿLL]]]wJ UdDm;;;GJ֣V 9֞iiiriiiFFFX 1˕uuuiii֞3XFFF_#{333%X*(((y X<XC!z333{TAXNX(HNXuX.X֩(XY$333|'GX]]]e#{333|'GX=%qˏ+HYD| T hMCΎ333uR˗ { R a c XRRRˎ!fFFFva jˉ3337Xyy;;;`X}.XtUAuuuc333333QXxYNX(HNX֥bX֥x Y(((G Xq(XYiiibRRRw;;;v˓,XyWHuuubRRRw;;;v֝.X [ .j;;;@HY;;;C֞333iO' zRRRR3ߤ֟FFFː1+;;;d;;;b,Xiii`HFFFM X=X333EX(FDX(YKX(MNX}J XFFFH X=3%`(X=nYCY X*D^XV{';;;H1GY<3333RH]]]ciii֢16RO8A5M} >AMA333EAoM}Ђe#AoA5AoMAo/fgA333FAoAoAohAHx˔ZAoHy;;;_Ao'T5 <T3AFCMiii333((( OCX@@XCCYYYYM+ 'JVXT9CYYYXJ* CYYYYFCK( 'JVXUA CK( CT8CK( C<CK( @XK( CYYYYFCK( CK( CK( =VC%HVXO/ CK( (f.CK( CYC9TXR58TCjߩ֞:K]]]a9c gJFFFdˑ賧z[/ 'hx333T$.NC,@D:#  #%DK'CM+ CYCCYCCYCCG1OC8TV=:UU:CYCDYC'KYYYYYK'CYYC?B:UYK( DD!˅~]]]V %G=FCFB%G=>:>:%G=FC(((DiAx6N K96KT]]]a2֚@!`]$/h.g/i`AA,333a2o(((`_hKc9333EC;;;eAc9B2B2c9333EC333['X((uuuq<]]]o˕/+]]]nFM^Ad˝f333j;;;lxNX@;;;nJ/ߤ(C=/]]]j333B1MUD##|Q<>TM, $F[(((Y>TJ* YQ<JJD/#|yKH%Z߬DZ߬D#|T$DA 333[Y>D@NG((GN1>D@OJ$>TO5 333[(X( ^ f a ]]]o lk(qz, 0;;;P333q kX=֟Fb]]]l XYi[ H(G=|J%}JFFFY6iiiaYiii_ߧ28Adߧ.%}J333YW333YW%}uuu֝9 333[Y!z;;;[!zHRRR]H333WDw( ch fˡu$#FFFw|5mt8˔< XY.*H((((@]Z:333PRRRiiiY%}333;;;X+֜Y@1ߨ333Y/ߨC ]]]v!rˣ333l%};;;Y 333[Y 333[Y%}f 333[Y%};;;iiiy %};;;Y1(((FFFY Ruuu{(((}w3֞ߩ6=]]]uX333hp,KN!]]]m((( d N / XY ]eH(C@>@iiid~%};;;;;;Y+֜ [ ?2ߨFFFY.֧֝>Y;;;^ FFF_G%}[333]Y 333[Y 333[Y%}z 333[Y%}Y333]c](%}Y333]Y/֝(((FFFY /ˑU g i;;;lN6֞(lFFF]]]iGKe XY#uuus֞1H(+֩<#| JFFF\:hY]]]]Ĵ:X;;;[ `ߎ2#|T333[Y 333[Y 333[Y#|;;;c 333[Y#|T333[YY'#|T333[Yiii_ J (usrrsuGVVXV@%JXVVG!HWR5 ,NWYYC!HXXK'CF1֩RRR %F:DCFC:U%F:$JT8FC%F:DCCK'%F:DC$JXU>P:3$QVUUVQ2ѴgG:2ߤ֠֠ߣ3*x֞֠ߦiiiN J賂g2+y֙U*66* !26/ %=D8 !%  $$  %'DL- 7- BR98qr{s /333Rˇ$ /NC H WU:%KWV@+ˊ֖.#FFF9# T C C=_;;;CDˋ% D˒Z!iiif@Dn333]$GG@MNQ=$FTRO:<98G82NQ5%FFFj;;;wG#>2><:C% A<8@!39@>6J9#D@5F.A>.FJHKA ~sX*C;;;T3n˙uuu˛U up.֔GFFFH>;;;FFF֣]]]^ (!GGDDuuuW333_2o=uuuWߪ5 Dߧ6]]]e333B\ߥ6(((B<3ާvFRRR`31ނg\ZW@'u3*֚J333]:*֗uuuM /= X(|;;;_FVR8(*uuuY踴߂m$']]]`]$ |1[߰=]]]_FFFL$uuuW;;;cRRRi]]]W;;;!z IfV!z:r֩A.333B#zO333XV,˓֝/+˒(((H333^q% q8,K \;;;dX(Diiiy/Eߩ]]]~FFF>!yHRRR^H^R h߫]<ΎuuukFFFY HFFF;;;D4ˌˌ 5%}FFF;;;Y2ߨ;;;Y%}˛iiiiFFFH`тA333J%}\(((eY f fhu$%RRRxJX(((l>l,˓ YX(/˕uuu_FFFC(((B%};;;Y1(((FFFY u@w6FFFF}g(((f;;;\ FFFF{uuuvC$jm$%}333;;;X1ߨ;;;Y%}r ViiiRRRQ W]]][, !zY3֞˕1<[ 333V $u=1]]]f6bX((iii|53=S VG%}Y333]Y/֝(((FFFY q֩Zp]]]M/֘֞]]]} 333W333Eb[ g(((T={߬}>%}JiiicY#|Mu߰iii[JuuudYf d333m(((.j,f(((lFFFl;;;GxFFFX X(<˗G #|T333[Yiii_ JuuufFFFH O: Tuuu֠˙RRRD  <<(((B333E%};;;e5Yc6iiiVD%muuu\HC2֛ː/AWNU +8d=/;;;GG;;;qX(Q333lc=(((DC.m(((bl;;;H$|(((!RRR\˘g333C;;;E%}WF +Mb;;;Y%F: HXT:%JXH!=UXVXC1F. =A=@@U=#5RC!uuut(((lCYYYYF/O X(333s;;;X%F:DC$JXU> (HTVXD3ߥ߫nTnˌ֗]]]aKFYYYYF!}D ;;;RPA=nˌ U%@L? @AB<C<D$<E7<FG<GW<Hj<I~< J< K<L<M<N<O<PZQZR%ZS7ZTGZUYZVmZWZXZYZZZ[Z \Z]Z ^x_x `x a'xb6xcFxdTxedxfsx g~xhxix jx kxlx mxnxopq r0 s= tJ uWvgwvxyz{ | } ~/BS   !,!        DWP,Cbi]U]jcF3FFFI333E,#>FFFO(((E$ A jhC +Rb cO( *NP78[ hidK% 1VfX3 FVG$$GVF!1 NM3 FYL* .T ficH$ 2VdO' G(((c( >C (iii\VKˌC 5f{Л|j>,k k1/n֑#Kol+%`f($bߦFGߦc%,^j,#AA#Q]]]G:333mn. 'f(((K333YG V[ R|֞Q2RRRf9 Ukmb :yV3y8_ߨH J` *Jm;;;(((i J, G G3;;;vU1;;;li!6z;;;b(((iVTX3b߰bWR(((buuu y3 [;;;_2z5K{*, |J T(((333]]]Q!;;;^;;;_dFFFu5]˓D :~;;;h!333gV @ ˒C $cp*(((cFFFRRRRRRK333`(((iiiRRRuDc;;;`$;;;iFFFi'1;;;suuuxADz333r/%uv*8333w;;;w9 $MbN$#(((mF J i2v]]]uuu9EEFclnkW6D˔K8v;;;xn;;;h! :~;;;i!Os! HˣJ,n֝N+Qtiii߮;;;d81{ ,/xFFFt.2quuuj*!\˞֕.GG Au+%wu$8y;;;vmFFFg! :~;;;i! C333x%ls''Tt;;;^FRRR^( Ri!5s;;;l$'333rq/;;;K;;;s]]]T#% I e f I%#H\M( R~6!G\K% N֞yA 5;;;߫;;;d :~;;;i!Q֟3333'or'C;;;z ;;;FFFd #k߰FFFߦ> (((dRRR A2z;;;m%*;;;sp. +N333F:9333F M1<333{333{<Wh( >l*RV. yj(]]]h֥e :~;;;i!333XN`߰c6[333\Q˒߰ߧC ;;;^iii@+iiik]]]s16x;;;l$  ;;;_;;;_.zNC`npkV5'f|/\RRRv8 _ߧH :~;;;h!333[NM˔|}˔T MJ/]]]f;;;xjsq2Te (((fJOd G G.|f *s}1,RRRi\ 2;;;ln$6z;;;bJ]]]b1 =֗MSiii[(( `|Y  6|J+TߥT*h֜ާ*Vq sO#AA# fb Y(((V2 f+:333jm/ 'f(((K G^T2!C(((O >#* E(((I1 CpRRR]]]~q>$333B333L> D_333aaG  #F`jlh_Ybkm_=1FFFquuuy@D{(((q+ T(((H  'OaQ* CZP, ,QgjcH% 2VdO'    #EG#   O֝y'+|GYl%  $#iD H`bxd,  +kC Fߦd',C9  $HVF#$GVG!            !C` jh_@ %Kejh]>>]`F 'OipppiM#@V\> 'OipppppiO% :\ hj fU18Y hieN*  !K df]< 8Vhjl kdK*%NipiN%(e˒[ 333P[ RˏC 333JK$]~ B333J333EQvRRRi=Ouuuo m83553O~F5fq(((nJ __NߦF.yHFw\333c` _ߨx333]Y >RRRg.Gː333l.   ,MsoCCosM.  cuuu`%CRRRl֛\  _ a*s`9;;;uc 333iKO˓{Q;;;JA ^O#;;;cV FYH#  FYH# !>d333wߨ\#KgnnnnnngK#\ߨ;;;uf@#au2Am(((֣֛MjkJˑ;;;d5VFFF] 1 yc 333iX' 6]]]pY# *Nht#(((`RRR]3333`P UP U*Ou|KFEK| tQ+9|.1;;;l֤(((֞;;;uuuiiiFFFj. 9iiivuuuv: $333:A9i˚;;;b5 liii`'rc 333iuuuz uKc[   K;;;˔Ha߰ Q8|uuu333g%dz,%dz,.FFF^RRRrY$HH$XRRRpFFF_.ib[ߪ(((Ncdb֣` @]]]T];;;k333cr>(iiigHYw'[ A /w߯]]]333e'mp,'mp,Niii`8 /333X333w333~333~333~333~333~333~333w333X/ 6]iiiNd z1 %FFFf֣d3FFFwRRRu59}> A` 9z5 ;;;P;;;^!3zт ^,(((wQiJ _fZ[Z[ORRR\3 .^^. 2YFFFP333e:6y֡` h\uuu]]]] 1y. ,Ai FFFi'Q333K (Xyx15uRRR(((hR(((w,(k]]]˞ [ /lMFKFN1]]]`ߨ;;;sV$GG$U;;;q֞iiia2 + ˜x# Fuuu U< ~˗;;;_/333w;;;w1 ,xV(  9ߟ(((q֘1HFFFM(((B333uuuz3.w;;;_(wX1pRRRRRR`2uq(W][m*.UwKEEKwV/ FRRRu5 J(((~FFFRViiiߦF UX'p;;;JVFFFk(';;;J]]]b]]]i]]]qRRR]]]y< 333Wiiif( cY D֜;;;w1+u֥FFF(((^%m˙iiio<'f|/,tN%CiFFFt\$KelllllleK$\FFFtiC%XR @|((( ֤ i'* v}x|(((w+M;;;[RY $'8xm333OY@zRRRi2 F]`H.rG*s}1,{f /OtrDDrtO/ (((WU.k߮uuuuM AߦtC8AqߪD T(((K 1iii_֜h*CA 5]]]b֜g(Hiiil333j< 1c]]]`1 ,jߧwX #;;;Pː]]]lF Y(((V db 5665 Fk6 iiiiiiiRRRW( @333c.,333`@ ,RinppppiO' .OfllbD# C``C.QfnlbD! 3VhlfN+1MK/ $GcjmiY: $F\S3 'OaQ* T(((H   C]V6Fˑiii ]]]iiiv(((uuutiiiy333ě333<!JfiR,  +RigJ!      Yl%   i333FFF֘> bxd,  ,mːY,C9  %Q(((nˑ֜ nG 'D_k kj kk_F'                           A`mpppngR2%Gb jj kj[=A`mppnk`K/A`mppppnbCA`mppppkX3(Hc jllldK*A`iV21VgX5A`iV2 'OgcFA`iV2+Rjm\:A`iV2A`j\:1UicFA`j\:#MiiM#%Gb jlifO/ =iiikJ@n֝޿K =ߨ]]]n_. =@ =f(D oߨߨ]]]]/ =a'%`l* =a'(((KC =a26u% =a' =xJ9FFFjC =zOKK>l֝RRRnU$ V= Jv1Vs3 VRV}3NSV85z: V8333c\V_{ `V8ViiiqA6|\V{QggFqf$]\ 9o: ] p']A ]f( :s`]HF> ]>  333ic](((w6 ]> ](((~`c]zO1tn 5]]]mQ_a]ߪ;;;z y(((y}._RRRM_Y$_rC^}M_|ONy> _>  333id_]]]s>_> _d_wcn\֡iii~RRRh*_߰ U$333lc2!*@H 3_T+K333f_]]]uuu}uuuxj K_333;;;;;;vFFFb D$333mRRRX@Ri|y>_˕˕> _>  333id_O_> _d_n#333jRRRbAU˓C _(((X.s(  _@ {;;;c_~,_}+/ty.DH_> _>  333id_._> _d_n,o{% \R_RRRi..s{#  _> y m_}+_}+1yx2\;;;[_> _>  333id_333@_A _d_n,oy$ [Q_֧߮K$;;;jRRRU(%:G3_R(F;;;__߰ {hG_(((333o333f;;;U ;%FFFj;;;_q;;;g _iiiuuuruuuqiii> _>  333id_|5 _\( _((((((]]] d_((( n#333jFFF[9M> _;;;X \wzw}._;;;\_xU%_\*a iii;;;i!_j86d> _>  333id_RRR}1 _ 333o333h333c(((K'_;;;v_]]]grg333{d_;;;u[(((yn]FFF;;;i']uuuK 9o9 ]iiim6 ]H]@  :r;;;e ]D@> ]> !333jd]t']N];;;j22A66333mc];;;j1<(((vn 6iiilOVFFFd,Jv/VvHV^V8MzFFFOV85z: V8 2333zcVTc֝KVaV;;;c! 333c\V;;;c!9(((vgGrf# =(((j8@ m֝ާxI =ߨ n@ =K =a'A k˔֝RRR]3  =a'%`l* =a'$g_ =a.*buuuI =K =333J JC =333J9(((pK=k˓RRRnU$ A`mppppndK*'GblmnjX9A`mppppmbH'A`mpppppiM#A`iV2%F`lmnk`G(A`iV21VgX5A`iV2JNA`iV2 !FcmiQ* A`mpppppiM#A`gO' %NgcFA`gO' .TjiM#$D`kolfO/                dq5        d_G޴d*D\V<                 A`mpppndN,%Gb jlifO/A`mpppnfO.:\ hj jbF$ 2VkppppppppkV2DcgO' A`iV2 HfjV2 HfjX3!JfjU1 'OdY5$MijR+ <]mm`@ <\mm_>Fdk\9 +RjjU/ 'Oipppppppm`ACbnpndF D[P/CbnpncF =FFFiA>l֝RRRnU$  =FFFiAQv֜ ]''fa' A(((K =a' >iiib1 Qp  @]]]`. _h6 YiiiK(֚V/N* 5xF/ o]333J. >@ 5333f. =C Vo9 Fqf$Vl8 >J5x~6Y;;;bV8 @֛[,(((u333a  AߦVAf!3RRRr^ a|]]]|c!/iiif;;;zXtX333^;;;d(XJ/RRRl`F\][ 5]]]mQ]]_V,w._;;;h!]> (v(((y:TߨX* wwCrRRRwG\˔G 5 w333u8 R˔z1 ;;;MJ_ߧ_/ \iiiv: ._c_333c\֡iii~FFFi*_ ;;;b bA  ;iiiZiiii]]]uuuu]]]viiiiiiiZ=_;;;i!_> Rߩp ;;;w1Xuuu}w':;;;siiirA!jiiit@ .RRRORRRdRRRnFFFuuuf_r@'. ym'@qd_ 333a!333iRRRbAU˓D _ ;;;``V%!(>@*!_;;;i!_> ,(((w]2FFFvQA]]]uO .{˔R '@߭{,_GN֞|CCd_W+m{% \V_[ @ˏf'rr_;;;l$`= YRRRu5c(((w,g+ 9;;;vk!HyRRRt<_> %uu% :~d_RRRf2 ,ry$ [X_A !\xHnn];;;9n62FFFvc :uuuvY'v9 N3  5(((y˕T_> C|֝N :~d_˕uuuka3#333lFFF[9M˔F _ˣJ'Uxuuu_nnVvAO FFFo*biiiv9 mRRRt3[֞y. +(((ym'uA$ _> m v, :~d_|M<, [FFFiiii,_ q$G dnn Aߧ߫FFFiii(((b 8]]]vj Az`dNr* 333jc[(((333q333i333c(((K'_> :]]]vY :~d]H  6iiilU]g˔G` ]nn$jMix= !tn+K˔uuuv: Cuuusg! 333icAvN_> `FFFu5 :~dV8Gr߭5VFJ{ _Sˏ@ gg 5333nj$ % uk+FFFc \֚C333c\# ca_> 1(((yT :~d =a'>r333U$ =a+#`R/]]]^oOKK3i]]]oX' _p: $a_Fߤp: 3]]]h=2u]]]?(((KC (K_FKߤ]]]FCdA`iV2*[uuus3338A`iV2 %NijT,+MdlnhX6#MiiM# >\jllgR2%NdY8 *OaN%A[V8FcnkV2 +RjngN'  'OgcF<]mppppppppiM#_p<$AYL* #:nc  :m|:         _ћzZ.  +uuuUyc >gRRRrwQYJF\ 1Nb U< AA >C  DcnpndG FcnpndG  C\\D    Db_A 3T 'OcO( Db`C.T fcL# 'OcO( 9PJ$9PJ$ 'OcO( #MdX5OߤJ,iii_RRRn2(((K;;;J @@  2333hK(((K;;;J/w M/w M(((K;;;JKl*2;;;u v,>;;;oA   333c;;;l1  #d[   aY  333c;;;e+  @(((] @(((]333c;;;c   g:      _V6ar2  =Vfi]A!  333i(((i\C# 'F]gcQ12Obpc'F]f`J*>|F,CRYYYUA  333i333d_fV5 Fuuu~u Fuuu~u 333i;;;j.*FVU>n>  ATNDJ\bR8+:Tc\C!  ATNDJ]fU2 6RRRv333w/ /* ,]˒e( 333i֜ h/ >333g֝ςd2Miiikd@333g֜;;;g>,333f]]]2$U]]]j֜B 333ilDRuuug*Ruuug* 333i;;;rTl4 n> D]]]iiiww֛c%D]]]iiii@f֩\333IT 333ij% sk8 K/TFFFM 333iuuui2c6c6 333i333ˌ8 n> ;;;\M;;;\]]]f.2uuum ;;;FFFm.;;;P333;;;` 333i֦JdF (((d߯d(((`(((֩]Ju($iiicN 333iQm= m=  333iRRRߨ]n>  333hh 333hM 8fj5  C}]]];;;h  333iFFFc/x߭rcG!1xd/sRRRFFF^*vG*mw:  333i`n> n>  333iv(n>  333iFFF(((h 333i_!D``H*+Lc_D M;;;;;;l$ 333i;;;V߯j6|(((]3* 8|333]d6|FFFVGz!#]]]^h( 333i;;;[dn> n>  333iiii`n>  333i;;;Xyi֞;;;g  333i;;;Xyc$]]]\ˢuuu3338  333i]]]g/w֤teF!2yd/s;;;RRRRRRRRR{Y <oQ֟V 333i;;;n9gdn> n>  333i333z:n>  333i;;;m8cnR};;;h! 333i;;;m8cd2~ ഴ֡(((T 333iOfF#;;;dc(((a333iii֢333R :~n1 ߰;;;t3  333i;;;i3bcn> n>  333i333֥z3 n>  333i;;;i3bmN~;;;h! 333i;;;i3bc,uˈ*333c(((m+=tRMY@yFFFM6zg/333z֧;;;nH333a;;;b/[[g9 o> 333c;;;b g9 333a;;;b/[cFy;;;b333a;;;b/[[V߬]]]S!(((K333i8F]]]jˌ: !\> G]]]kގi:'fKV|8  J333J#A@ Kf( }> (((K;;;QDs'Kf( J333J#AF1\(((K J333J#A@ <SWWWWWS<#HdigdijhR, 'OipppngQ,1Tglk]<@_iijkm`A1TgllhU52VdM#5sT%NcO'C``C#MdX39]]]z9 'OcO**Oik]<#MdX3%NcO'C``C1UdO' %NcO'C``C,,      G߰FFFX Ul,  <SWWWWWS< <֙ߥ@ []Vː[ >֘333i2 >_ hjjj`A? WT/      #DUH<AA*  (Nea I! GY J$$ KceN' !`ߨ uuuO#DRRR>'' gF CLK f% @ߥFFF\          (fߥY                    KT[h\H R [%D[fcT9  AVXV]d]D# 3Qb`YXXN3 ATK>G[Y< #D]f_G' *md>!DTC <QH+=TR: @RG(<QN52H@#'GUD# 9RXK1*FUN58OT>=QK1*HXYYYYXM2dߤ> _nFc (kiii [@(((i֜ϛxc. D˙֜i2Quuum˙6 D(((ˌ9 +c֜ߦFFFX+#g?  F E A΂X$ <ߣJ,V΂Q 9HFr]A(((\FFF@.uuuifs'/U/Q˄%!uuuT֐%nFFFT_n \RRRk !>QN:#G߬]]]iiimA>s n(;;;\ k+JߦT;;;\M PG 9N333^;;;^1a~6 C(((zix;;;X A֟W+c333FFFW#3xuRRRze(,w]]]Y$x~%_n+333sw( `ː|s_[in1 ;;;[]]]f. dM 333hO#;;;cb 333h> (((aRRRF 5ˌ߯˕֔: 333h;;;g6o= /;;;s߬U/;;;uuuuuuuNOuuup>!pg!uuuT˓M@k_n(((e>KO 333hM1~d 333i]]]g2yd 333i߭uuuY 333Wˢ(((9 bX3 333i;;;j:t> ]֩;;;w1b;;;u. gVGx= ,[m#.]]]e__ncFFFg.R֣L 333i_8|333_m 333i;;;V߯j8|333\d 333iQ=$ J֛ːG A8 333i;;;wQ> 3FFFw] :uuuvbAD#tmF;;; =KQ_nTK 3˃ocfwߦ _+ 333i;;;Xyc/se 333iFFFd1yd 333iFFF> >ˡ [:~ y I* 333hiiiuuu> bFFFv3muuuv: Vk#Gy> *wFFFV, G֡T_nU֡H!%>TVD$  333i;;;m8cd(((a߯N 333i˟K!(((f֦d 333i;;;m%333I RRR[3FFFJ(((c> 8]]]v_ DnCuuut˔T%|n_΂X$(ja_nd߫i(   333i;;;i3bc@y q* 333il(Jߦd333c;;;c;;;QY#FFF]FFFXX: fFFFu3# q˔k}D *;;;d(((j1j֥z> 5rz2 5n_n g3 333a;;;b/[[G]]]k֝ j1  333i(((j5  Yd(((K333J Cߣ i.=kH.(((jl* 5iiieUD֘`2Kߤ_!2 vkq6 !yn 9l(u}#_n*333qs J333J#A@ 1TgnlbF#  333i333mgO+>]izd 'OcO' AblneO+6YilfH!.U fjd_diX53VbH!>YN(A[N( FciR/!8YjdG >z|@ GdnppppkX5n;;;N_nYFFFl%NcO'C``C  333i;;;s5 'nc      jo  e9 _n Cf333c;;;cY\/uuuj{@ OQ[h[M(((K;;;J AC 1`+ gF CLKh( 'OcO( Db`F@`gN% ,Thb I! GY J$$ KcfQ*  <X]F!    'HH( Dˍ(((5 !DabP( 2Xflh_> #D_cfdT3333J;;;J%ns5  +j֜ c!9RRRhM9 hː]]]lV% 333d;;;c >uuuxT!jˏ= FFFXk*@iiif߬k(  5333w;;;v8,j;;;O! J֝< .o, 5RRRg (((](% (AVV=#KjRRR]]]jK#@(((l֞Лyd. 2{RRR}J 5{(((߬WbFFF(((˙s: =FFFTe]]]^u֖֙QFE>s n(:r˚: ARRRQ1o 趎Y 5qz8 HH dM(u1[(((q2 A tj֗uuu_eN]]]uuuN*FFFLFFFd;;;|;;;};;;eFFFJ(1~d.|~,;;;^߰FFF^ GߥNjjxgJ ˚H #A(((333C# 8|333_m!{((( Q*(((huuuj+ >ˎ֚֗d,RRRZuuu_. *333i;;;h+ /semiii }^7(((\iiiuuuˎ1,iiie¿} U.OWVNVRRR]ߣߣ]]]Z</@UVA/(((a߯No~,Hˣ;;;j%]uuuiiik5*==( 9ˈˈ8 @y q*;;;b8 !`_ 1333i˛RRRߦR SSG]]]k֝ j1 uuuQu+5t]]]K:FFFgߦ_#HH1TgnlbF# ,OdggkniY6GFFFK3ez333lM!#MippppppiM#    ]ˣH  =YijbK+iiiXr3 ArM9\ hj j]=  doomsday-stable-1.15.7/doomsday/client/data/fonts/console14.dfn0000664000175000017500000033601612641367670023644 0ustar jaakkojaakkonf ! " # $( %2 &< 'F (P )Z *d +n ,x - . / 0 1 2 3 4 5 6 7 8 9 :  ; < =( >2 ?< @F AP BZ Cd Dn Ex F G H I J K L M N O P Q R, S , T, U, V(, W2, X<, YF, ZP, [Z, \d, ]n, ^x, _, `, a, b, c, d, e, f, g, h, i, j, kB l B mB nB o(B p2B q<B rFB sPB tZB udB vnB wxB xB yB zB {B |B }B ~B B B B B B X  X !5      =?3*% - 1% -Huuur333o2 '(((LFFFWH 0 KFFFX(((Q5 + 6A<(((5%- B333=GFFFY]]][ M%M C . OFFFY;;;YH + NFFFY333XHDD C X X M/ #LFFFY;;;Y GB X X X X FH;;;YFFFY M$9@>w _CDWV333BL˃ d_333^333>FFFS֙1wAj(3339uuub'D(((v= J( XFFFd%m;;;Hn [d XT;;;GC˃#iiid2.FFFE Ri$KQK|(((yY* |;;;tq 1>߫ 333@#O#VOj.y333f;;;9+ 5˃.;;;j333p/˒iii{ K3;;;\ FFF7FFFQiuuu}֝1 0RRRMh]]]ˑ('x;;;\ XFFF(((]333O 3[ z]]]]ID(((X(((X Uy]]]HGN6֘zSFFF<֧߮*!FFFVF˛ j!߫F]]]_N%s s MHG# ;;;RO9JOߪK*333yFFFc G(((;;;\  ]C 1f߰;;;\ Xy!,˓  iߩ6'ˑfS9ߩORuuuGO8Q2iii\|+ˎߨ8 kFFFj9/(((:֑1J֟>333o(((`  A֖#FcFFFN, auuun!W} dRRRM! 333[;;;\ b˔1 ]߫uuul8ߧ ;;;\ XlS C RRRZ(((T6$t(((jiiif踧>+}M!$߿]]]K/q f >n߰[( 333w֥g333aAW%]k0~RRRRRR|V3334xCYYF*|YXRRR]]]( 333[;;;\ %sRRRb;;;4333muuun|;;;[ ARRRcCߥ8OFRRR2$y> . 333iii N,;;;|D!xt( SAYRRRnX% 9\6#]]]ORRRiRRR]]]W 65;;;;U.XFFF;;;' 333[;;;\  o@ 333Y֡ߧ1H333*B[ DDRRRw˗333^FFFl]]]o8}(((~JuuuKx%y. 333TiiiY$pôKq{(((tYtY!  (((ic+x1]]]meVgH~s333[;;;Z `djJ;;;PuuuU8VA]r}=TRH z;;;Y RZ! MB+(((i;;;Y[߰;;;Yux9 |Q1aFFFc;;;EC5= (*! 8=D@H]5iiiv(((b #(((u(((w# K 2 jH:ߪߨ]]]OT߫N'}}2xuuuggy'U˚Q* ~;;;Y 6C31RRRBˍD'v֡uuuQuuu^ߪ(((֠55ߧ\$uuupT//G3C eRRRl%uRRRG)h֥߭d$'uuumiiiR$rl;;;s֠+/::333|%iiiFq333rˑ/ UFFFS/˓^ >RRRI6C/ʹwiFFFSYe5mJeߤ36~uuux}< bw.p֛2.' /%RRRRod5'$JO3 +LWH+,5+xFFFSA],J:> ,72O WFFYYYC8TYYV=6RWN, /'KWP3FWJ%91<T WH+ C֕y ?iQD%$ /       JFFFYRRRZK(N;;;Y;;;XJ8 S333WO, @ C= U X XJ%<333TFFFY(((YH= U XP3 D X X XB D X X X C>333UFFFY(((YH<= / D X X M-  /<=C@ / BB ,?+ ?A /JFFFZ]]]\O$ D X XK'JFFFYFFFYL$ ^e#![(((T Nz5A;;;T9l'U33399ߧ[;;;G.;;;G.V33399=;;;GC;;;3w;;;GC9=k/;;;GCCN1w<QC;;;G duuug$;;;Gr+ duuug$6;;; @ %, '.CRRR`*3338h% 0]]]Od #/ˎj333ziiib b/J FFFtN3ߧki}0J(((G 333\FFF(((\(((Y E 333\FFF(((\(((Y(((D3ߧki}0JN 333[Y d߭(((0 333\YJbg(((i 333\YXuuuon}%Jw%X;;;\ <]]]{V 333\FFFFFFt333M<]]]{V NQ*iiivW(z8(z= 1l%$iiiR]]]c]]]b]]]b]]]ciiiY 7?rR v%R f]]][ qF#sRHiN]]];;;UV֟F  H_={iiic 333[k  333[c V֟F  HY(((jY,X 333[YH333h 333[YX333}%FU];;;\ Y˕:xr 333[b%;;;[ Y˕:xrMtDFFF;;;Y 2>1 > Dː(((m1 3334xXFFFbN֞RRRZ 333W (((XF;;;333fHiii~U;;;VFFFkHNd$ 333[RRR(((^333XD 333[ ZM. ;;;VFFFkHRRRRRRhY%{T 333[YGFFFt! 333[YX(Hiiilx;;;\ ;;;V;;;jX' 333[j;;;W;;;V;;;jX'1֜趧;;;\  ? ?? A֨ (ir@ >@6ߦn,a˔;;; Y a.H˜N ];;;\ HNT޴( 333[. 333[˃ ];;;\ @ W M+ HY%{T 333[YF} 333[YX;;;uuu(Mޛ߬;;;\  ];;;\ Q޴( 333[F ];;;\ Q޴(K֛;;;\ FFFSvO /ˊRRR@ *h8 Ru2%uЎs (((V!tNH l]]]]333X333` HNX( 333[(((ZWB 333[]]];;;_FFFS4333X333`8xHbRRRY%{T 333[YG@ 333[YX(((tFFFW~ (Nާ(((iii;;;\ 333Y;;;^ Q޴( 333[|uuueK333Y;;;^ Q޴(1Oq;;;;;;X`֜(((d6W/uRRRj@333EO!uЂћ@Cu˛(((dHNp#(((Wiiir HM q! 333[\ 333[d (((Wiiir8ERRR(HO 333^Y$zT333cYHߪiiio! 333[YX;;;^d(N޴q;;;\ 333W]]]o\$ 333[y$333W]]]o\%_O8=5=6333o1>ONNOD$FFF<[ >h<;;;Y˝;;;W _|=#r,Hj8 Qf$8 Hrcߪ;;;^ 333[r 333[YQg#t*HN 333[Y9f 1xXH\ch 333[rX;;;\ X(N޴Hiiiq;;;\ TT.FFFwuuug 333[YTT.FFFwmFFF>p;;;zq%2A/GK;;;B :. [a P]]]RRRj(((f]]]aY$mc =MF(((T+333wuuuuiiiOFߦ9 333[op\$ 333[Y+333z#GK333[Y'k|(((7 1֕]]]mRRR|FGKRRRm8 333[oqa%Y;;;\ W'N޴,U;;;[ 1֜FFFP 333[Y1֜FFFVg92CFFFI 1sA333E2333EA >(((O35(((DC(((4e9FFFc356ˏ=333E3A;;;GCi:ݧl%m333FR333f333EAOiiiR (KYL+ 8<,J$ :' C}˅28!25RYU= +LYVD5RYJ%FYYYC/ +LYT@22.DYYK( CY WG22 .9FYYYC/5'1% %69S WF/6^y*QD +MWO. 5ow$ !3%  1: = U X XJ%A(((WFFFY333P2 + M X X X X XB<= /:> *58.  -#:(C B#8%B BC X X X X F + M F 2333< D WA5֛_iiiAu M<= / .(((M;;;Y F<= ;;;=3  ;;;=3 9l( N˃l.9=;;;GC*G'v@>ˎ,333EC uuuNr( Y/ iiiKj$ V1.FFFDwFFF5}O;;;3=333jA ;;;B@9<;;;FC(|/9=333:˅333:˅J FFFr Mvuuu~g N 2 ,L y֥;;;[CJO 333\Yiiin(((eKADG333`YGjC;;;nN[5FFFpD(((X(((\ O%} } fuuun!e֣QVFFF]]]k f;;;? (GT@JOC9TT83NQ(((Y FQ6 r߰l( ,KKF@$J{JF 6p~(((O6p~(((OHV.333FFFX##*|_HN 333[Y[, bt!,b9</uuugXj(((֝6FFFn@#;;;zw%%|X9J+}R(wjN` 1.{333`G߮FFFc(((`1Oߧ֧Y.uuuf]Q.=߬[Guuue#1ߧD1ߧDHwߪP]]]Z;;;rC$|YHN 333[Y>O$ta rn{U'tb@ jVU%{T;;;j333i%{T=},dˉ3338˔FF T 333`23ߧ֢Yuuug§FiiiHߩ˖֔*n֣333N FH-֕߫;;;T -֕߫;;;T H{*.oe%}YHN 333[YuuumdOG333e ]]]Hciii. j֞5'yFFFp%{TOߧ6%{T)]< FFFDiiiCGYtRRRYG< r<1++Q֠[K~YCuuu;;;XrߧX',˓(((!GD;;;}Y*;;;\ *;;;\ Hf %\@%}YHN 333[Y^zx%333WѴ2 t֟<<֡ pd>%{T$q_ %{T   UxߨYHQ\'R8XRRRj (((cYUFFFJ N޴($(((iii;;;RHO 333\YX;;;\ X;;;\ H333s :}W%}YGO333cYAuuub ]FFFw@;;;n'{Y5֜h%{T\w'%{TRRR[ߩYHO['R6X;;;j (((cYV;;;RRR]FFF_RRRM$N޴( u@HN 333[YX;;;\ X;;;\ H_ hY !5]]]Y%}Y<m8yY oGTqx333gRRRoX $|Y(((iH %{T1˒R%{T/uuuZHx,r@u SC<|YG}%!#N޴(r]]]XHN 333[YX;;;\ X;;;\ GK'v+RRRD֗;;;quuuxD#|Y pFFFF _z%K`G;;;WTV'RRRn}(#|Y5tqh+%zR gRRRl$zT*˒uuuYF;;; W uuug333w;;;gs*%JWT>%39S WG:@6:!2$65<%3:UYYYF#y˔2870ːH  F WRJ<5RYWH#$K WYCD WXVA (M WYC1% Mn#/1./ #=;;;Z Z333< 7ˌ+FFFHFFF^FFF^FFF^FFF^FFF^FFF_RRRJ=˘˔(((Uh(((L!DC<C* /1 Qϴ W[ϛn+CYYYYYYC (A@'  <=#    <= + M W@  DD *6 BG D RKC0'uuuYr$H;;;X;;;YFD(((XFFFY333V >$N;;;Z]]]YW:9=l=+w 0GFFF9wA.]]]V <#;;;^* R1 R%/f_JO$1 +j˛Q3FD8<= !.6JF 'KR6 '=FOC3OJ<.%+:N=%HXJ$x333zC !! %**(%,#+** '=A>=1333Wv*%|U{% Qߧ oQ˞ uiii~iRRRG~RRRdT<!uuubiii@HYAq(((3*}R>߬ߪ;;;Q'w֜uuue# =s _'z߫FFFcNߦ96诂}ߩ=6qUuuuLᅡO*+>9 <;;;VK333> A=W51͛m+333_(((; <;;;VK333>{ߪ5 333[f%{TR3(|˘]]]o! ={ _:\  333FHiii}豿}MH֜.%{T333V(@H,߫O@ T 1֛֣UR߭@f֡uuuwˍuuuLRRRB֛iiiFDH ;;;VUM9#sT;;;T [5|N,֚(((y|AM9#sTaߥ֟<333iY%{T=ߩG@G,߫Ov$wRRRf9Y޴b%F}:G߰w< %{T 333[>HD;;;}YMߪ\:;;;|jH= rOߪ]K~YX K:w kG |(((9JO 333\Y.ZKF]333nFFFV~tD_l.ZKF8qlJN%{T$%KUO1/HD;;;}YMߪ\:;;;|j2Q$w֚2Y˗y<Q֓<Fi %{T 333[zKHO 333\YYRRRn_%HQ\'XRRRm (((cYX3333PގsJX;;;Z HN 333[Y f]]]n xuuuo U;;;uuu´, zuuuu( f]]]n xuuuo ]]]iq''֙8%{T(((hC$g˒ n333w@HO 333\YYRRRn_%*;;;< @v֡ PHiiizMGFFFs%zR 333[}{NHN 333[YYFFFk[(HO[(X;;;i (((cYX(V:X;;;[ HN 333_YJ߬\Ji~RRR|$J߬\iJ%ˎ6%{T iA1333w=HN 333[YYFFFk[( fj:RRRv]]]W iiibFFF]]]]]]=Hk%z_ 333[}{NHN 333[YR˖F'xHx,xS@<|YX( 9֧SX333n$ @b8iiizY#pߨ6:333_ Ke$sߨ6K( GM%{T$3./Q333V(((U@HN 333[YR˖F'xp366(((bT/i`Fckc%|iiiy(((5333[|yKFK333[Y6FFF U G;;;W 16FFFd on(((D[_p515(((DCVFFFdFj% dYCiV;;;c6 7333RC6HuuuZM ]]]_C3ˍ6(h333<RR,2 333\`%{TJ޿315(((DCVFFFd/nsRRRx|+ tJ /1>5 (KM+ .#.$22/1.9S WGHWJ$D W` Y5'%JWWF6RWDFWTK<5<!219. $5<+:UYYYC;;;WRRRu$%|U~$/1.9S WG*$!>SM+ 1=5m֣HDF ;;;VV3339Y GFFF8 zJ.iiiW%ϧ+*ˉ+ A<:333\G;;;F!;;;=^'333CI#uuuZނj: #$ (66@RD!  AG! 5˃. @t~o vuuue! ;;;RO.ˏHFcFFFN, GY3334x9֧U#]]]ORRRiRRR]]]W 6;;;Zy|: (((hcFJ,1 K333S>333YY#iiiR]]]g{]]]xiiiY 5 (((3mDYYYYM+ doomsday-stable-1.15.7/doomsday/client/data/fonts/console18.dfn0000664000175000017500000053601612641367670023652 0ustar jaakkojaakkof !"#-$<%K&Z'i(x)*+,-./01234-5<6K7Z8i9x:;<=>?@AB:C:D:E-:F<:GK:HZ:Ii:Jx:K:L:M:N:O:P:Q:R:SWTWUWV-WW<WXKWYZWZiW[xW\W]W^W_W`WaWbWcWdtetftg-th<tiKtjZtkitlxtmtntotptqtrtstttuvwx-y<zK{Z|i}x~     /=5        :A% !>@6<8!<9'+3!  $T;;;siiiqd3 %M]YF#.9% 2U_ ]H#+6# *:.9C* #.! *=7 'K^a [C =333GH|@ JYRRRU333A%fe  YXKuuu_]]]C 5iiibU%Z333C6(((bl /l1 +A(((V`@% '(((Y,(i֙YV;;;^cX$eː(((\N]]]K 9uuuoUSr.5s;;;X.m333P+uuu`k$ /˄y;;;诂R# !.! KߥFFFd*VˏC];;;e!g_,V]]]xRRR(`RRR(((c+ Mq=  b߬~95z;;;[ _RRRn:M}ߦN 9֓*333>333>!oc+RRRht f];;;e!_U 7ex߮v : 333^ ;;;j/ M֩g333^uuuz5(FFF^(((U 9oߧV j333j*2uuur$$333X;;;X% >ws= D֛{X8d֝i,Y;;;c Dlfߠߟ9 iFFF9Y333|9 9}uuur< be51/;;;BC[|, 9RRRtߦJ 5iiil333d( /N(((}(((Q. do Xm`n]]]z< Nߦ(((b42+2/1 _ ʹiii_'  5uuug֢x3Z|1`uuuUYp8  !, cku` 6֦*'uuuNcuuur˙߫uuusaRRRE$ 2RRRm֛Jbˑ G Awg  F ˙ == y m. $Kj]]] bF 1{vA  ;;;bYgi Ag(((v W9 1/ 1O a d] F!T(((n*cFFF(((M5y\2 l S% @(((U= NY߭ r+ 333cNbk2iii>/ @Y xiiiuuu{WA l: %oYb֜ G %iiiPFkFFF4$333cFFFeMߧuuu_'%]]]`֠]]]xA333cߧRdk  8333j333j9RRRDߢ֛֚֙֓3  Fiiim6Yp ]q]]]{~: FR5Dz]]]߮߬iiirV, /333e(((333_*ou3,kdR .333`cod333S;;;Q   #<JKG3 $ji Gߦuuu{X9d֝uuug+/RRRToH,333~K$!Eq(((`O p3(fq(((Tcy#.(((wT =>%(((La N#  +FFFOsD8uuumC .RRRit fJ}*!;;;[vf@ˎ@ !333\˒uuu_'\ގߢ: DːvDTߧuuum5+ DF H~+O o$Y֚DGd'uuuK333TXG @|T#˂]FeN .h= #j t.9333u`MQFk((((@G*n֙[#HL5'5*':<  HriiisFFFlJ1<'(N\ZF  *N _ c [K:CRF! 9iiiiU$i(((n1 CFFF~< !GL6%>= (N_ fYC    6</  Jze2iiid= 3V   @uuuJ 3#FFF@RRRI98:             *KXD%MbebV2!Jac cU28TH#2VikgT1/Tac]C/TgkkkfN'  *N `c^F! .TbaW6C\ a_Y<<S[ZH' DT=9 jC  A]]]e9ARRRe9=qK'e^ 8333h9 X333I+ lߥb# 1 jiC <|F U f/ J֗@ $][A333\!Cδ;;;Z!' rg6wuuuO+ k: uuuN;;;[UH^˙]]]h3   1K>!!=H, >΂wuuu_'Nߣ֚ }s a$!mg%p`#DU]ߪ5!CU\˙3Qߧl :yq\K+RyXQ>+K\db(((`} c*m{uuu}[2:' 1=*%Q333o:  : qK#  ANMmuuu|21iiicqTd(((ߤD :vFFFp.0\_H]]]z= *c]]]~62333pk :x~=# lA$ Y˔}@ 333]˚\g3liiiyN2t(((f  5}]]]M  3uiiiU(@rߦw3  @ T Z Z Z Z Z T? 5ːn<RFFFk/Ntep[X߰֜J $t_ Dz~8 +[uuud*b(((i:xtV5,kt_D#  np!WiiiV3r{T1iiiii, C}( A5'`u oC /. D(((nuuupY#6 |;;;Z ]}333333W p ij_diiid*]]]K֢j =uuuoߨi3uuuugD5֛g+ =u֜KJ;;;@ +n  8 1e;;;H/]FFFR% Muuuzd/(RRRJRRR`RRReRRReRRReRRReRRRe]]]^333G$2j}G/ kD;;;V ߨ]]];;;X 8r˚;;;o+j_>]]]r\ e߬u$!m r!iiiR˕ߪuuug36uuut]f(((p*Xuuu{F]: ,5! +8';;;OuuuC%RqvvvvvjHHOY|T 333X ;;;X Vſ˓H j_ 1x333m13dRRR|ˏ> A8*FUqV5z}p.3RRRpY.g333]]]((([ +h֛z3  >} rA 9ߡߧߧߧߧߧ豴r,Fuo8 %iii]Y 333Y333| 333Vqgj_(mqC M(((VV֤֡iiiP /h/rn2X:X]]]o5 FߦRRRQ3il. #D`v֦;;;l%AptV  :/ X p=%p(((W' 333YFFF]]] V6uuupߴ333q+ %vk `˔f*=\Hz#1d! m|6R}:* sgM333F(_}3%He   +VRRRn<  Gbggggf[= :FFFoT(H YA;;;WFFF< TFFF`Ji~˒G 6bQFˑ߰}Y9 *GT[֠J #KeiuD#CTVuO[p.J֛x@ @ߥph+!DT[J'333G]]]O ;%(((La N#9i9  6̧sf6$(((Pp333S([ߥXmo#+sf/ˆ? 333R֓2 RRRG賿 g('io D;;;d+2FFFi߫a [n %(((c˙]Cl%D|( DF  6622  9C G`, *uuudN]333` 2@333F5 iiiJ i5 =H A;;;g9>]]]f333f2 iiiIˊ@8FFFgj,C n1  @c%MQ   6= %eRRR{zuuu`Q''˄]]]W+ 8pX>]jnnfJ! 'Nfkkj_A +Q a f `O+:=!%M ` faT/2UbbT. (@81Vb h `M( #K `c^N* CK2 CFFF~< @MC 3 f(2@+3@+     3V.T;;;XRRRYFFFVS9  8: #                       'Ogmi\@  !D]` daK%  'Oik_C! #Mgmkj_A#MgmkkdJ! !D]` daK%  #8,:@$ =]kniN' !@< #8, %DJ,!@>@TC DN31H=+6#  +Qac ]G# #Mgmj_C!  +Qae ]G#  'Ogmi\@ 333Ic('d֚;;;B333Iːk2K6 K@(g֛;;;B333Ba(=(((H1333AH@ 333Ba*+k[HC =J#Ol*'eߣG'Z333B /mf%Kːg+ /mf%333Ic(333`T[]]]F333`(((l1 eߠ3 e? ]]]]F333Y|9V;;;^.ˇ(((BdY333Y|A1mOd[UuJ(((r9 6zs/:s;;;XeTe]eT333`T 333c};;;^5uuum]QY F+  333cbkm]T:km_[C8p]QY F+  333_>_;;;e!=333b'j` 333_bnr*j`\= :~֞TJx;;;^! =|nFFFj,ko, =|nFFFj, 333c|;;;^ 333afM;;;_T]]]zF  333`T>o6k|6%! ku/V]]]zF  333`R1q;;;e!H|;;;h. j_ 333_֝y1 j_]> :~~_y;;;^![FFF}A!OߦG jq<n˙u5[FFF}A!OߦG  333`U:}߬;;;e  333c˚uuu [ez  333_@AuuuwOkqcX<kVF9# gz! 333c;;;g! :w;;;^!j_ 333a5 j__ˣˣ> :|}|;;;^!hy*333pYkOu/hy*333pY 333akY333` 333fFFFOkk 333_>$ r_mߠ2 m˛˒ˏ֕]]]Ekm!(! 333f;;;h! :x;;;^!j_ 333f333Tj__(((]]]> :ziii;;;^!kmp_mFFFRRRwߩ ^kmp_ 333e]]]T 333e߬ߩbki 333_> tbm2 mv!km*5iiiSiii_iiiX ; 333e;;;h! :x;;;^!j_ 333hߩ_j__ > :x(((uuu;;;_!kim_muuue9 kim_ 333f߫v+ 333cq w6kk 333_># q_ktf[=kqaP3kq3t/ 333cp;;;g! :x;;;^!j_ 333fiiiq>j__ ]iiiV d(((z> :w333~ ߴ;;;b!kmp_mFFFRRRfFFF\Z5kmo_ 333dRRR(((U 333`M%K;;;Ngz! 333_@@iiiwQkp% kq( g|58;;;^=  333`M+k;;;e! :w;;;^!k_ 333c v,j__s,2333e> :w;;;lp;;;e!hy*333pYk|5 hy*333pV 333`q# 333`N'NRRRQViiiyH 333`R=}s8jm# j_ViiizM*G(((@  333_>_;;;e!H{;;;h,  (z] 333`dr`jm#_k!333_> :w;;;dN(((y;;;h![FFF}A!OߦG jb[FFF}A!OD  333_byM 333cu< 8p`V`FFFL5 333cdkm_X>j_8pcn333=  333_>];;;e!<333`%:RVkFFFR 333_G9xuCkm_[F!]i 333_> :w;;;_6_;;;i! =|p FFFk,j_ =|p uuug, 333_H@;;;u333u3 333_c]˕˔x!333_333i2 e< cY_˖˖2333Y|9V;;;^.}(((A1ߠ˔֟r6 333Y|:Ar a!e ?Vc333X|: 6t;;;Y'6RRRs;;;beVcYec333Y|<Q֜V333Id+ (g֛(((K333I֛j3K= HA *iߦS333Ba(=(((H 1;;;A1֓ˎO333Ba( V'K333> =H(((Ch*'a333CX333J /mj'HA /m\333Ba( #guuuL 'Ogki\@  !D]b f_K%  'Ogk_D# #MgmkkdH!!@= #F]b d`H#  #8,:@$ >_kngN' <[ db\@ #8, CQ9#MgmkkfM% :=! #8.,8# #FG'  +Qa g]J$ !@= +Rm;;;ˉ5  #8, (JM+                 !N}(((6  < WYF$             <J6     #F^a `R2 $JcjmmkicJ$ #8,:@$ 3@+5@*:>! #8, %GF%  %FF$$FF$ 'GD# %MfkkkfN' 2VcM% %===]dN' H֘u@#iiiGiiiVP*  !6,%dߦe 333<(((;333Ba(=(((H*֑RRRW* 9wuuuN :J333Ea';;;?b b(((?(((A\!b ?333@333I'e333=(((AH/(((K1(((rq*._(((Ba(N]]]I:ߧ߫ߩߦ:333Y|9V;;;^,gߦK_(((aK ^%333ax2MK#KHQt@JˑF  A;;;Y6x>R p$+r;;;^_֧V[ߥ/  333X}D cdMC(<V333nQ< 333_>];;;e! oh% qc D|;;;b'6o]]]d,. s ~c ~t+5RRRptJwv+#G]miiiX :z333\! :pC 8v;;;b! >o˕FFFo6  %Vuߝ+ =Xf`F#  333`f`J' +Kcg\=;;;^@ Gz;;;g/  333_>_;;;e!X;;;n+>|F 5uFFFjFAJ>Rz;;;`!HxD\ߩ֜M5333u2 :x;;;g,mgFz;;;_!$ouQwi,+!R}ߦd% 333d踿 j,>333g֕8 Z˗{C  :w;;;^! 333_>_;;;e! :s˒H \(((r('333gxߤq֝ h if/(((uRRR q%KˑߧR :w;;;^!Gː]]]m3 :w;;;^! >֘uuum=Fzv8  2M 333g] _;;;e!!pj9qd333_h 8((((((6 RF,v r' :x;;;^!'pV :x;;;^! ;V_9 $RRRD333\ud 333f;;;}uuuj/`uuu{yg` D!: i k. :x;;;^! 333_>_;;;e![333vcuF i(((d'%*yo![}D :x;;;^!U q* :x;;;^!!?@! %DA ,G\{l 333c˕b,Kiiiy֜H 2v˔_%(RU :x;;;^! 333_>_;;;e! 333R֖@MMFFFB\\FFFB%Z333B%d333> :    @> !H`ddddddd_G    8=!.R_ _J# !6,#DD##DD# !6,!HcdK#  <ߡG+;;;_<(((Ba(FDFD(((Ba( <K %6/!`c  Y߬A  333X|CRHRH333X{9   9e          ;;;Ks8 (Mbmk9XcU6Mr6  $F_c[UJ/ 333`u`bO* 'Jqt9  'Jqt9  333_@#:: Hk5HTUHGTN.3<FYbO* A\dX98FO_`J%  (Mb]NC32:G\bG  =YfcM* Q(((J'  /il VskM;߯ߡ= *eߦ֟`# 333c f,iiiIߨߨ< iiiIߨߨ<  333_M<hߣFFF7jk >访ߩ;;;333c+Aiiiuy f,*fsV B͛|˔l+ /i= AFFF} ?%_iiiI 9ߢ߫iiiI enM@ (((<: Rd# 333fR dR dR 333_u }>_k]M;;;YRbM;;;Z] e];;;YANiiiI ˠ;;;e$%yq%(((b߫FFFd% 333cg6fh 'y_ 'y_ 333f`#_kk;;;` 333cg6fhU333C$MxA  333c˕b,Kiiiy˒G X;;;C2{k 333e|5  \iiidA .333lQ fvckQr$j_ ] (((Y 333`FF|dj_j_ 333h@_kk;;;u_ 333`FF|dgx' sV 333`|D' sXgxck 333aM A֘iiigK 333_> grfkQ˜RRRx]]]a]]]`]]]biiiV:j_c֡u=  333_>=xaj_j_ 333f֞__ij~;;;o_ 333_>=xafu%tX 333`A%(((pXgtfk 333_@ F333fˑˍ>  333_> [3+|n @uuuu@55,j_e֥uuu} w]/ 333_>:waj_j_ 333c|M_u#j~;;;o_ 333_>:waY8Giiix˒G  333c[%Fiiiw˒G [8,|n 333_>  5Riii] 333`J  C֚|rRRRn*]]]d]KOD#j`;;;^(((;;;vRRRf((([> 333_>:vaj`j_ 333_y;;;wiiimA]V' j};;;o_ 333_>:va @ːguuui/ 333fpuuuj/ C֛ߩtRRRn 333_> !AOOmc333`}T@!$ dߪ֠gRwߥAd[ k9 333X{96s\d[j_333X|KMo;;;`*ViiiJcy;;;hY333X{96s\! dд] 333h_%(((c߫֠n333Y{8Cߥr|M333Vˑ?9iiidK$`ːCHC %~˗|z֣T(((Ba( '`CJC k_(((Ba,M|2 =d Hg_333N`A (((Ba( '`C 2 il+ 333f k/ :iiidl333B['Cߥc%Dߣ >3U_ afcK# A\_a_J#!@>1|]]]c}U !6,+5!@> 2] !6,:QA@_fT1!@>.16*+A= !6,+5,Q_ f ]J%  333c jaT.3U_ nk #6+#J___]F# A [a_J#   1z{;;;}ˎ=     'RRRD qFFFR         333]N  'jg   (((T֙V3334s6 ;;;O~2KV +Wuuub||faD +uuuRs{]]]ZA 6ˁ;;;?+iiiNx5  +88.$33$!  #          2QJ% +6! /NM+.QYG96( + NiiiViiiD C] daK# 'J`a `S38T [X YO/ 3iiic333@%Z(((C^(((d+(;;;Z 讴k! b*YߤC+ hn!OjFFFdA         WD6s;;;XRRRNQ :uuuU! >֗˒] >AUiiiLOߣFFF]]]n@   !.! ,# #*23'2# .(*//8% .5 23'2# <NRRQM9;;;\ߪy+ :w;;;^! 8e>iiic(((iii` (n賴\#];;;jTC!(((`fMA( 6i˘֝333d+ (%'C[U5333>333>=]]]H! !uuuL9 6ߟH ,]]]T]]]D="Z//Z >(֐(((]2AiFFF< 8ߠH ,]]]T]]]D:ߦߧߩߧ:  333`~K :x;;;^!ikH]]] s/*jwV ky'  b_2R֝ aH5333Tfiii`q}ːuuue:$333X;;;X%;;;Tx52pU ARRRf/TRRRWZRTY*dx[;;;qI DRRRf/TRRRW333<F$333e}< :x;;;^![n;;;XRbM(y%bnN% ]]]]zJ3X|Y*iiiZ333Y! /N(((}(((Q.  333^~=:v`2]]]mR2pcdxMMO{gX˙;;;l23iiimR2pc $KiRRRn2=FFFuo2 :x;;;^!N/  $%  333duuu}e9ux FFFd(RFFF J cl*_(((f. Jy\ C֚]]]RRR{3'uuuNcuuur˙߫uuusaRRRE$ 333_>:waky\yD _֛c*uMmy\yD ,kߩ˒Q+x;;;c$ :x;;;^!8y˖i$8 Yiii^uuu`(((aY<,57#  333cg6fhU333C$MxA // $FFFbRRRuuuT\yFFFL @fˑV K ~9 1/  333_>iiinj#Jg :x;;;^!%A /cuuuoy]]]jFFF? 333`FF|dgx' sV!333OuuuRRRRRRM$1|j(((aMiߣA :RRRs/ @Y xiiiuuu{WA 333_>@yc+333oY =|D ={$+333oY .q r2 Jg :x;;;^!%@ 9iiirFFFtߦ C 333_>=xafu%tX 2, .u {߮;;;^,;;;d333iii333333 d$ WFFFZ9333i333j9 333`|M*_e`uuuq8,]]]m o2 gvD`uuuq8 fyR +x;;;c$ :x;;;^!8y˖g#!(((?Q@:N ghhV(  333_>:waY8Giiix˒G .2^˞`>g˗֠;;;d5  (O^ YJK(((\zsZ*  +;;;Z;;;Y+;;;^˗nFFFi ={o!333j˗;;;l$J߫t. >{oG߯pR:=FFFuo2 :x;;;^!N /  .5.  333_>:va @ːguuui/J֝~pon/ +dN< cߤˎd3 /. 5K` _G/W˙ߨd! qߧKi vMx h(;;;`~jyQ${ߨMFFFV߬8 $333e}: :x;;;^![n333X{96s\! dд]333MD CmiiiyM'D[bY@!  5֓ߥߧ֟˙֟ߧߤq, >G+!@= <>!<G. !AF(  =d/UjmjibH!;;;[֠u(        #333E s<  YC9u;;;[ RRRNR    0ngrH  4[ 5uuud333>+rFFFG^333d,;;;; j' +VyhV%3QJ%  1;;;9( 1OO. /iiiUˍߦߥuuuh], %2+   '>JC/ doomsday-stable-1.15.7/doomsday/client/data/fonts/normal18.dfn0000664000175000017500000055201612641367670023476 0ustar jaakkojaakkof ! " #$-%;&L'\(d )n *x +,- ./ 0 1 2 3 4 5 6  7 8' 94 :A;I <R=a>p? @ABCDE F G<H<I$< J-< K6<LF< MS<Nf<Oy<P<Q<R<S< T<U<V<WZXZY(ZZ7Z[GZ \QZ ][Z ^eZ _rZ`Z aZ bZ cZ dZeZ fZ gZ hZ iZjZkx l xmxn'x o4xpBx qOx r\x shx tsx u}x vx wxxx yx zx {x |x }x ~x  ) 5GX     598</        $91 5,       3=. Aʹ|FFFoʹu5 69 829 QP: D^eT,1E?1FH6  FFFMͿ~6 *@6<E(  Jg.$ CA#:Zc[=/@1:\heU.@^geY318% 2iZ';;;XR =ߠ@F˄+1c]]]wiiiwi9Rߤl9\qfA#  NCJsN'cj*=u(((m3  8֕iiif<Cf;;;FM~8 333d跧[]R. cr3/333hm3 3uy(((h*FT3~_`e#,;;;hX 6 333=!@(((A* !lK 9mq< 6v}: ;;;Oc  @;;;_$ ;;;[RRRO AnY;;;z(((>VH FߦߩJ auuu;;;^(FFFgT >sHXiiin9 6ID;;;J8nn8c߫e :z> ;;;G} < (iiiK333`333:  5z;;;h!Y>  Mu3'uuuNiiikiii{iiiyFFFN (((a(((˂t9 @]]] f 333^333a M9  nw%1333vd,xJ%k;;;m5Rl!*iiifYuuuj, :z> )RRR?C<q˚O %=D  /z;;;h!Q= 'FFF<>@FFF<6 1'((([ˣ\9(FFFYw3 dd% (FFF<6 <Vjuuuj..ˇD $JpRRR`Ai[5~ߩ[%Yߩ= :z>  \J  %];;;9(r;;;l' @|}5 !X˙GGw=1WnFFFd2 g(((| Y> Xv: Q˕D (˂֗RRR9333<3  /RRRiz> 8rDC|ߧH :z> '|o3D/`333G,uuubFFF_'Rx FFFt9 T]]]s]]]e3  <n]]]]]]miii_W8=FFFu.i333s( FV+AdU8;ߧ߭ߩ豴r.2UjiO' J֛(((m'8u~DCߧH :z> T˓dCuuuw: 9ߴŽe, NO 2֔߫ˎ\!.VY 2 ve3%FFF_Ѵ(((h'l;;;j# Dߧ`'3335 3 <\333mK1 !a;;;Cdd5x֞X$X֟> :z> AiiiqFFFo6 (YQ <5 DuuuQA 9ˁ$.Rp;;;^]FFFRRRR =֚_k;;;l$ C_  _;;;e+ $=6![333C'(((m֛J*uuugUk. :z>  8(((yd*38A|[ Gdlw]]]] %FFFOmY((߬rR/;;;I]]]eAsY @ 333˛k fRRRo. HT :֔ H Q֕A1TgfN'  /]]]L]]]L/  >zRRRi/d֢֢e :z> .{ Y8?iiim ;;;K(q;;;r/  <@ b[6fnC 333JN$c}qC +]]]d}9 T> X> 8=# * Y ;;;G;;;GQh :pp: 6v}: U{(P333g*X;;;_ 8֕9 Be2;;;>333I 1;;;`ߧf%+RRR`<1d_!Dr(((ߧ(((78sߨXp;;;i' %FFFi(((^ FGFGKssK'cj*\j' >ߣ;;;i9 >333G@O= 4/# /$ +[ U# 9D/ $H`_F! 9[hdM31GXG#jq%3FFFrVkV %GG%!?=9YgY9/@1/TjpppkX5A`lhT.<A%     XX     @zHY;;;l/(((?333Y.        #DD#c f!.FFFd֙M 2' .e' 3`#  (?3<A%           9Vink`D$    'OipkX33 @3 'OipppnfH  !Fah cN'  'N ch^A2MV99gsˑlA +JH% DcnnfO. A`hl kdK*Dcnpnk_G*DcnppkX5DcnppiM#333Ib$Huuuie1333E= $dߦm+,mː]!  *FFF\p=NuuulzX!% b` ARRRf>2kFFF\. A֜333mN# Aj% AD333`q%Hs< ;;;Eː= N[_K ,JY B#  *(((DU<  3(((\N;;;(((ߩߦUMߨ{@ Yuuuf16(((pKYb!Yr'YE 333d˛tW6 :iiim n( + M d sˣ v%333\uuuRRRg%*k]]] g  <`333pߦ= =QUUUUTJ2;;;CtpO/  Ug%   6RRRe## j333xߪx [Fv= _;;;\x[Q333Q* 3J @#_;;;s9 9pFFFl._;;;K/+#_333Q3.!333`uuu};;;gU* JߨX+ 9]]]vt(333^RRRFFFh%.|p2 F CDFFFG 1;;;`uuuoi>333<3 %T qy X$K ` Dː~uuuo{˓;;;`! r˟k_FFFQd]]]~G_;;;e! 'qJ _;;;kdN' _uuu ~e[D!;;;Lm( c g1 cߨOj߬r `n+(((B(((DF]]]I;;;HtF$ /`xT$2[(((@ *333iw> _FFF|333~]guuuwXRRRrd( @| uuuw< _U;;;e~#_;;;e!H~]_333C_9  /]]]P;;;r˕N;;;c]6RRRu s* vz# 1 i j  (E E( %E(((G.  Guuu~|X2 .333N(((i(((n(((n(((n(((n(((n(((eL% Ck(((: @`e֞ߨR5(((~qRuuuoiiia(k333333g_(((p.333gz _;;;e!H__333C_ÿ˅2 #6iiiie n }2bR/w ~2+XU   '[RRRpRRRgN;;;4/  +ZzqG @s.;;;\(((z1@]]]yߨjnߨ333`  :uuuu]]]u8 _;;;yQ(((a333A _;;;e# 'pM_;;;kdN' _333bF@/...[;;;i;;;c]֥95RRRs(((u,6{[߮:8}֢333p.#!::1Q nw9  *uuuOiii\iii`iii`iii`iii`iii_iiiYFFFC#333BFFFmdA$+RRRTwU333^(((F:}uuuiiiyXciiiyuuuhuuuhiiizb_;;;}M=qFFFbT G$/D>!_;;;s9 8kRRRl/_;;;K/+$_;;;l,  ;333qFFFX];;;v/[V.viiiRRR}2$ixF 3aW2 DI#>b333nߣ6  *+++++' (((@ooO/  [h]]]X1jRRR333r9 2FFFs G//HFFFu3_]]]~FFFT, o| |tu;;;vߦD_iiiqtRRRwߦQ_iiiplbF_;;;e!;;;Kuuuj5 D֚`RRRY333u/_ ` 8ˑU(((L(((H;;;Viii^! +<5 #:6!$kK[ iii}t: JߧQRMX]]]f1:RRRiKX֛]!X: X;;;_ BRRRiAVl, RRRJR . kj/ ,iii[xU(((EH cuuuc$   iiiRG 333f:6 k˒RRR[. >֜333jK  >9 >333GDblhU1=\hdJ%  *HA! 'KdjdK' /D8 'GF$X˒ W.KG#bv`guuuZ#!FJ+  +KG#CbnpncK( #D`knldM*Cbnpnk_F(Cbnppm`A<A%      C%  'HF#>C% >C% >C% 5TR2>C% AR=:UJ#@O:#FF# %Hcgpe`A #Mippj[9 %Gchji`A CbnncH% +R fidH!FcnpppppncF#FF# %C>!FJ+ .KD!2kߦ;;;Z, A333G(((KK A333G A333G A333IDuuujw A333G AˏRHxK >QKK9 oߧj2KrR9 oߧk2 >i1 .k I << KK333GC C(((`',FFF_@  5(((nHY;;;_333cgY;;;_Y;;;_Y;;;b2GiiioUY;;;_YwH>]]]rgXzNgg>]]]k r5 g֙C >]]]k(((p6 Xd#]FFFS == gg333`\ JOXD ( qtv333w֚C_;;;e! 333in_;;;e!_;;;e!_;;;uc]]]pq*_;;;e!_RRRrJG(((yn_tHnn1;;;m ~y o'n]]]֣d1;;;m ~yo(_FFFRRR֛A (uuuf333yFFFDEdz˞udFnn 333gc1FFFsu' ,(((v(((u+N K%,A=!_;;;q53333un_;;;e!_;;;e!_(((;;;t, _;;;e!_n_iiipCnnYJ%*R;;;}Kn;;;OXFFF^YJ%*Q333N_;;;{[O+uUA<$ (rR nn 333gdb˓J QY fiii|G!_;;;kjk;;;n_;;;e!_;;;e!_֩y. _;;;e!_iiin_RRRp>(qn333`;;;AK|gn;;; d333c;;;AK|h_333ߧD  333]T,_> nn 333gd 9iiitxJyFFFt2333e# %JdeO( _n_;;;e!_;;;e!_G_;;;e!_333 (((n_333 ;;;rR}n(((j|%(((enH!;;;i|%(((e_333o+>r֜j1 _> no 333gdj˖c333f| 333<;;;J_n_;;;e!_;;;e!__!_;;;e!_;;;qY(((tFFFo\333vn_;;;qY y n(((j{$(((enާyY!#]]]e{$(((e_q C(((nh'_> nr#333jd >wuuut: e;;;A;߫;;;b_FFFpnp;;;n_;;;e!_;;;e!_FFFb' _;;;e#_;;;g15(((LFFFL81333ln_;;;g1<(((v(((n;;;_(((>Huuu{hnFFFjX:333e(((=Huuu{g_;;;;;;z: !8`RRR|G _> mw% 5;;;z`omUF*6b;;;g!_;;;r65333vn_;;;e!_;;;e!_;;;֝m, _;;;s9_;;;e$ #333in_;;;e$<;;;rn[F#'M Nn;;;s6 [D#'N K_;;;sTvv((((2 CA>mFFFY_> `iiiU%.k֠Q DA .(((nzx(((;;;d _;;;e! 333in_;;;e!_;;;e!_;;;lD_x2 _iii o l cH!_;;;e! 333in_;;;e!>FFFrn2FFFlz~v ~o(n;;;i!1;;;lz~v o'_;;;e+2~[*ruuus˛R]> Gߧ~v˖uuul5#us!:FFFkFFFRX;;;_333agX;;;_`;;;e!X;;;_%!Ruf#X@ X;;;_333cgX;;;_A]]]qg>]]]k r5 g;;;b333G JK >333Gg;;;g! >333GG]]]k. >= >333G(((KK >333GDuuukK9 n֜xg1K(((K6 l֜֩q5 >333GV6 JwoH =e( ,fuuumT $YW# !A_jolfO/<A% %GF#<A% !};;;g <A% 3U]ACbnppndF<A%  'HF#<A% 5QH# $Fbjni[< #FH'  $Hu{赴}s<A%  DXD<\ki[8=H1 @_inhV6 #2#     =y;;;`       Ap333E      TX !Ab(((eiii\O+ Hi,   #CH*               !FJ* #HX@2MA9VQ/5RN,FYF! %K[D2VkppppppiO+ A`miM##@A$#MikY6#GM/ @ H6!@=:A% D WG$!A@F ^$UߤF 3uuub: $ߞ333l8@uuuhT 6\ 'i3 $auuuJ =F AQFu+XRRRa/ 3d3 HA =333GRߤGHC $DD#%FD#M֜J1FFFr r$_: ` z5<;;;sN1hT3`RRRb+$hNVF Fߥ(((i%F: 3RRRr[+;;;cNcYV;;;^5oHd[EDDD5]]]ss#UD/;;;ss% 2 z\r(R]]]}vG5Vkoux*]T#1]]]kz> #T> \333s/Ck > jn'  'p;;;e!  Vr*  jm%JK IKj{D2vn.TQ8(((v|1  c֝X 1p֡ z5 _Qj[M> /;;;qߧT19!#G`gX6kucM(  C]hcH#>[r;;;g! !D_f]@,߮(((Y'H`ig[=kmbO+G FG D Aub˓uuuw_|333s.=FFFu}5 *uj#Fuiiiq@_> U l  U s* Lk<mj. 'cːB#];;;h!'d֛ˏ\! Hd! 1(((a/m߭ g,QQQQ!s]]]]`(((N 6 .  8333u˒R_> :quuuk5 :z> #(((dycߥC %q(((\naXFFFBNߪ֥;;;h!U˞G HZ (((Uo1nTeefeK֝ÂvM333wRRRiiir8$n֣VmR .zg!_> #mO :z> $֑C, ]> !Uhk,mu.(]]]e rb(((E( 333_333x;;;g!#;;;g  \,>(i֥ }!m uhnnnn* sG#]kUuuupC`@ $ky, _> ]h :z> 5G8'FC!K}ww6kr5bu65~˙j% #]]]bu8k;;;e!,wFFFIgs'uuue]]]\kq6qlnnnnX u$ 6]]]uA MxߪFFFq=_> V֝(((N$ _> AFFFj, :z>   *;;;_A kr9i˗x33~֡r. !FFFcy>q;;;e!*uuuj wp@_ion8 j`%bknnnn3RRRuߨNj r#Duuuq˓`Cr(((u6 ]> DuomiW6_> (333lːG :z> @uuuק[jRRRiiic'%FFFdn J* \]]];;;g!(((c ߡ?_ijˢuuuwKj_$_knnnng(((t+ >wߦM+RRRac#,xb!V9 (333dq%_> dY :z> =ߥ߿g#aUQFFFDAˎ;;;bHߦGXc!|߰]]]_, cY!Xcggnn 9iMV(((V% 3i' 2w* =e(/b$_> AߡD $(((]uuuT Db%$_(((ENm333J [ːr6 >H =wa%HA>HKKon/6 1$ DYH$  +OY>=H1CbnppppppkX3_Q9>#N>  +QdikeT/Dcli_@@]jhO' 6XjniO' A_kk\9<>![]]]D !@= <>!#FF# |k    ]T# #R> BY______YB       fiiiO  6RRRxdUFD: 55 Q߫: JK <ߢDCl*+_gkkkkkkg_+*exQ =f(>]jdK#!JfiV3%25333352% %H`hg[<=D*        !FXD AXH##FF##FF#[= ,8#  :]KKKK <> 'a333C =ˏ> gggg Uuuu_!6t;;;Y ]iiiVnonn    .:2     _* :w;;;^!(cnp'/MG% nn!H]_`_RDK]bN( !Jbff`N* 1M\\M1%MdgbQ65QbgdM% !CQR\_H!>]f]@AFFFiFFFvj=!@< !CD#!DJ+.JA!DH(3D2*HD!CT>%GT=!DK,/MD =[fggcN( dn :x;;;^!ni #%  nT;;;hRRR<nnJ֠(((ߦf(Jh+N]]]g]]]gM Jςn\'#Xm(((KHiii BF֘ߡ: /uuue1 H@JK @(((a15FFF_: Ca/>ni=1aA 3ˎcKr+ @333c69RRRa> 2]]]Dri :x;;;^!iu/ Wiii_iiic333edNFN333=$n;;;=nnf˘NfRJߤ֙G333`[T;;;`e BFFFYˡ> :֓/cY%eg Cinˏ< H֜i{{jߧF +RRR^˗b# Cvy֚@ 2ߠN@ \ :x;;;^!] @ ]]]O˓iiiyDnuuu˓\nnm{((( enyh#RRR^RRRvxiii~333c# 333g(((k,(uuud;;;;;;e!njFFFG* ]]]Z;;; g1 +333m:j`*nn+ v 333 u%1;;;suuu333s.@RRRp v6 (uFFFRRRu'=kw,*]]]_F :x;;;^! D]]]`+%y˓Dn߯1 nnkq:}}@}nn|:vl1r˘k*,t˚}2 333gr6c95yߪm9n;;;e!n 6 T|9 i;;;o1kd+onUߧN]Y[iii(((OHߨNG333iiir>5= :x;;;^! <8  2ITTc iuuuek]]]^T$n>nnj`*oo.onno+dk1v˙q/2{}2 333gr8f85˙m8n;;;e!ns@Y];;;s6my@n.333sv*5RRRsFFFt2cX#uv($rߪk*!f֟O :x;;;^!N֠f$*,$n v9nnj`(nn.nnnn*`k#FFFa333e# 333gFFFh+(iiidFFF;;;g!noiiiJ|k$V]]](((H%h;;;nYRcbJxqFOߨOT֞]3 1d :x;;;^!d2 g333`(ggd['gg+gggg%XcMK 333h[V;;;h!gg%r;;;\ AߥGTg1;;;s(((u, 8]]]twDu]]]s8 .iiic]]]a,Mw(,]]]fh$nl :x;;;^!kpKV>p2KKHCKKKKKK>H XvsV  333h֛g('d֛;;;h!KK N|A!aF,lKRNX֖DAˋX 5ˎ[=_֙3 kߨO2a$dn :x;;;^!nd#FF$%JYD#FF#!@> #FF##FF##FF# <>!<\kk[: 333gw_D!  !D_t;;;g!#FF# %Mfk\: %MeeM#,TghgdK# =<#<9 6<!CT>@UC 9iiitv(DcnppkV2_+ :x;;;^!*_     333gr(  'q;;;e!      cߨO T]# :x;;;^! [uuu~U333_\[;;;_+RRRhv* 9|: 8u;;;Y 8: (((HC C333I.JKn谿}2 (h;;;C/s谧sN %C?@D' =P@1>2.7$ />2           CXQF:# : K@ <ZWA %M_aM#8U ]a]\C# DFFF333@ :y2Ny֖: #cN *\mˏf3 333I(((ITh'@v; DFFFT*j˕]]]~;;;e8  (FFFHY3Fߡuuuzߦm=333JuuujK% q N#[֞ H]߫ uuuk, !8NO6KFFFQ(by=9n`3O֝9fuuuV( :rߨ333T6 QRRRW333XdpprJ *r333v9Jj,N]]]i]]]gMFFFFXu]]]zV VߩFFFo}333c$2e˔֞s6  $Df |ZBfRJߤ֙G 8֖O,lyHb=>QNMwz3MuuuM;;;52 nyh#RRR^RRRvxiii~333c# C;;;B6xFFFiiii2buuuyq(,MOGv|8JH  0hr߬wliiiL'n|:vl1r˘k*,t˚}25RRRc( 1|֣T]_k֗r(((~i..]]]Zuuu_.(F(((T3!no+dk1v˙q/2{}2.(((A#  c`Muuu}a/OWVNVRRR[֖֗RRRW: ,n;;;n:nn*`k#FFFa333e# @((( urk333F'CT.;;;gRRR֙F(99' %Nr dDgg%XcMKRG[֝]]]v6F}RRRf! (((C: KK>H XvsV DE2v#Mmg+ (((C9 #FF# <>!<\kk[:!H`dgkngM#$Ny(((h:cjߥ貧w eK$  %NinnnnmbC   J֧FFF\ 2GQK9#UV  <333f/<\gU. doomsday-stable-1.15.7/doomsday/client/data/fonts/normalbold24.dfn0000664000175000017500000116201612641367670024332 0ustar jaakkojaakko8f## '! '"'#('$='%P'&h'' '(')'*'+', '-'. '/''0''1$''27''3J''4]''5p''6''7''8''9'':' ';' '<''=N'>N'?*N'@8N'ATN'BlN'CN'DN'EN'FN'GN'Hu'Iu'J(u'K6u'LNu'Mbu'N}u'Ou'Pu'Qu'Ru'S'T'U*'VC'WZ!'X{'Y'Z'['\']'^'_'`$ 'a0'bB'cV'dg'e{'f 'g'h'i 'j 'k'l 'm'n'o0'pD'qX'rl's|'t'u'v'w'x'y'z'{'|  '},'~:'O'c'w'''''  !'        %K`T.+HXXH53GVXH+$ K] J# @VN. DiiiZq W6  %HciX6.R[> :XimfN/!CV[T= %JccG#McT.#Kd\<$g333h/#Xiiib]]]a΂X$QG>ߣuuuQ %K o]]]~333hD!  1joO 3333j+ /dyRRRlMGC /q6 H s:Iˊ3  JX3w~6!(((lV#](((e /m˒ c(%jːD'r(((e%+ q֚M;;;^` /yj1Q;;;r: *cK 7 T ] I!c;;;_9}= Av˗Y5yw#lO H(((b3XߨGT]]]j/333hk%q˕U5]]]o(((t3 ' NiiiYkiiim[RRRM9+uGm(((l6|:#Gd֤iiiy(((2 FߧgbFFFoc]]]vfe߯J 333egVp$Qp$Rɂuuud/9{]n;;;g .nFFF;;;q/ H(((L_֧;;;ceuuuy/;;;d]]]RlU 8]]]s3336 $uߨO ^ˣH <ci;;;e k (((l!333[;;;[eiiixuuunEX蹛;;;vC(((c333֦֜A [}< g`D(((u,(((PC  Qu*] lQ\\Q;;;JFFFL[֥cR@! 8uﴴ_[p2 9;;;V!6uuusiiiv8$ yN 2\quuuuuu߰]]]uuuzi;;;J$Fblt{lbF F`#MigJ((JgiM# * T֤RRR((( Y*  =t<T֚iiiA D333;;;}b\gfO* :\gQ* Uu _l3d]]]K#  DD '588883%,RRRo˕G 2|߮RRR\Y3333`(((n3 F dnv]]]yRRR t h[8\iii;;; iiiI i` CRRRl( 5]]]iiiiYXX6fuyyyxu_1 l]]]n. (\333֩Z(  %\tc .An֩iiinHTW;;;d˖N.RRRr5Hu$GG;;;QUYv 333J;;;J *Ck(((9 :(((}裏q9 =iiir9 333hA #333n}9 @r(((y333U#G(((a(((i(((n ֦((((((u(((i(((aG#333fi bRRR֧+333];;;\#(((@R=<|߰M#pRRR\]˛X(((l> !333lx9Uo;;;[:Ql2 'Vy/J333]RRR` [@;;;_dJ333]RRR`[< 9rN333N333JG333{p333MQ˔(((ej˝.;;;dF '333r53<( =A(  =cJCLGHu9 Y;;;Z 5FFF]]]qO' []]]֩<  9FFFu(((FFFaeuuu / nU8vRRRo,  9|];;;Z(((e!%MdjjjibG (((]Zf(((ey |9X~KR333c$'v(((yHkdOߩb dkO֟;;;e ,wH333d5;;;`eT Whf$T;;;l% 3np>MHDː֛D *mˏ@K{'m` ;iiiYiii_333K%(((U< YX/;;;e|= ]]]NGGT9kw(((m@uuuL`\ߦ_  /i˕(((]]]?,333uF 1;;;zy= !#  uuur~6 #q֝TEf$ Nq% Cߦi#333<ߥ;;;lQ# Rx9 Km*  #@C+F]]]_˅1 3spY'  %8<.+6,               .TdT. :Xij]C# !DXXH*2OfmiV9 >YjniV9/KXO56QYYYYYYU@ @TK1 @UYYYYYYYYXH*(HbmnfQ2$C]kmcJ*.;;;dW /fy˒ k6 FuuuU#._]]]pގvc, (\ގvc, =uuueϿ6 6ˉC ,iˑRRRp\.C΂V @(((lߨiiinX%<l˓ߨ333lG`^. tRRRj9;;;^~6+333dq'Op' 6(((vURa /vSak'Atb DoQ+JZ333B$ 9]]]vC d m* 333h= MNcN+yb]e(rߥHgl*333lˑA 5]]]lw>    8](((wߨHio  :uO 333i> ]hS֡ gdd_L]֟c#N֞K GFFF^\h 6>1 6>1%Fnuuuu[ YFFF~ ;;;e /^gXcFFFbMd_iiiwqhK$ @w֥<'333J333c333i333j333z;;; v*Yl 333gFFFt1(aߦrV (aߦrV  3VxKmm evt/ 333i> @vd96r֣;;;g !%G(((ciRRRuߧ(((]( @yiiiv:  ;;;gmR=  333i> 55 N;;;c !N(((y߬V 1 z߰d_￿|(((jO%  CTTFFFw2 Cߧ;;;X(j333RGN;;;_a;;;_a#YyysK%  of%333p\CzJ  333i>  Ud C 9#nd_h'bF$tg+]]]~= !333i֣H ;;;`f;;;`f F;;;xc= DRRRv6 'kX=(((Q 333i> %z֝G O9 O֝n[V333e;;;c# Dy= ': ^9VOVOY R($t_$333o_AsM 333i> Os$ :֗]%]]]b(((=Diiif*#333n(((8mq @\ 8oFFFl'/333e f(/333f f(K }U1 J˔;;;v2 333ijM~@  333i>   3|֢p1 +6,#2v39|6# CYF./`333˓C #333mRRRXH֝D `w%!;;;g;;;N@D <fuuuuy=  Q333hRRRjgA!R;;;gRRRm fF %Kt}~ [$N֞(((w.b n# 333i>  ,ymW8'g z_]s3.w/FFFFyri֤>  f߬9.333wT%hFFFQCK+i;;;gHu9 JC!@gFFFvH*wT AX 333i> 'm~,6w赧uuuh*((( `ߩ< 9{\$,..C{%X c2FFFpR gb[g!+333eRRRo<;;;`e;;;b5%FmiiivM'(((l֝M 2(((prC333c9 =ˎ9 +wuuuj8 k\ @RRRi5 Gzk%huuut9 /333jl:  1RRRo@YX(((U<  8Y333\uuuO*, f$3 nߧ;;;lA(((Kf((((8f(A]]]f(((m=K> O]]]l k9C333mu`* 9;;;b˔\3l˓(((l=#Suuuo(((r= \ dO*  $Gc j keO, 'OikX3#JfnpppppppkX3.Mdn l kdM*#Mim`A1Oflm jcJ'+Md k khY:'F[I% !C`j l lldJ( 6T_R/9Z dV3#v333h$         ,OEf$333<ߥ;;;lQ#  #@C+              (Oim`F# !9TfnpndO3  AUY[XD#!CV[YYYYYVM:$ 9Tfnnj_K5 !CV[YYYYVQC1 !CV[YYYYYY[VD!!CV[YYYYYYYR9$=XinpniYC+ $(((]֜ h. +RqzuuurkCVZG货}333hN# #HlzߩriGG˓qlT,G FG֖:  %NpпRRRlW( $333B XK+  G֜ c @sf*  =u}@ ;;;\`  /n֝A ;;;\(((qA;;;\;;;^;;;\V2r333K Hߨ(((y_9\9Kq˗n'im 333h֛A +t[ 333hsF 333h;;;` 333h[ /s;;;cA`mppppppppnbC[uuuvoG'333CG GwRRRyjbfv\6]]]viiiv9  333iuuu~˘]_b 333i]]]n6  333i333J 333iC f;;;d  =A K˔wX3  %Q˓C 5RRRn ~;;;rFFFn333lx} u< ]b 333iOd(((d6uuur˖RRRt(((w]]]t֟Y 333iu˗g 333ixxxqU*  333itrrgG :wFFF|yqqxuuuxFFFMUX((((]ߧRRRui@ _r1iߨb.333v;;;v/ 333i=QVVQ1(2Ow>  333i~,,`t8 333ibNNNH6 333iO<<<6%Yn8!*A\RRRI/JK %Ksy~]$j\ 4`mqqof(((A/CJJJJJJJJJJC/'O [6|{.  g;;;˘mT r=y: Fvr˔H 333iiii~K;;;e333z. 333in z˚˚|@  333i~]]]Xmp(  ** 333in]֡b 333i333{kkjdK$ 333iiiiFFFvRRRiRRRiRRRiRRRa;;;I%mߪc#;;;M> XY$ [~}tK% Q;;;12q@ vVX333n$ij 333i1(\;;;i\˗r<!'Aguuueߟ8  333i}+$M(((N 333iA.,,*  333i:%%%! [d=H(((n333> DFHRRRui@!2iiijZ3z(((z]]]|<9FFF}]]]yg c5RRRw]]]v6 333i61fiiik ={]]]z yt(((v~U 333i}r~iiio2 333ir```]M+ 333ip EblmmmmmmmmmdH YvV3 U]]]h*.n333]=v z= [˚˕˕˕˕˚] 333i;;;(((z333y_ib 333i\ 333iuuuW$ 333ing> JuuuvnF'`֘2#333luuu ֢iiif+(((w]]]gKGGKgRRR333v, 333i˒C 1 r\ 333i t+ 333i~5 333in /w: (uuuN333\\9 OFFFd%gA Cߦn$ #mD 333ad 5 tC 333as3 333a8333ag5tn*  +(((fߤKJߨߪkG  @u: :u@  JiiipY'  +YFFFpߨ˔FFFjN JRRRp\, Jf( JK,[;;;sߨ贎ua9,Te^C( qiii;;;˓(((wv˒˓xFFF8 !Jfnk[99[kngJ!%NipppppppngR3,Kcl l lmj\D*%NipppppppndM/%NippppppppkX3%NipiM#,Jcl l lmkcO6 FnU@=J\jyr5     [ߧ֠RRRz |utv |;;;{Y!Vu}[#=m֝qC >] nFFFkswiiik mdG% %2:=<5*                             !CV[U@8Q[XD#@U[VC!3NYXH+!CV[U@8QY[VA !CV[U@!CV[UA#  #CUYU@!CV[UC# *HY[R9 :Ugnnj\D*!CV[YYYYYVK6  :Ugnnj\D+!CV[YYYYYXO:!GC 8֕(((ECG 5w΂X$GCM :GCGg**gCGk, !]]]Q֖:  #H oz˔;;;o[/G賛wiA #H oz˔FFFo_1GlA;;;\aT;;;^a;;;^R~6;;;\aDsC ;;;\a;;;\g!!ga;;;\q* 3}V .k֝(((q=;;;\O .k֝333q>;;;\M 333hl];;;h!m;;;h!]=  333hn$9333wt' 333hl 333h˔TU˔l 333hj% :}b'qiiil< 333hv< 'qiiil= 333ho9  333in_;;;i!n;;;i!_>  333iyG z z5  333in 333iuuuuF$Gun 333iߩb!<dX q+ 333i_X(((n+ 333i\ 333i|(#m;;;i!n;;;i!_>  333iiiis@ 333in 333i333n;;;|n 333i˔['Ad2]]]o֢߬T 333i{;;;c2]]]o֢߬T 333i}˛333d 333iq___m;;;i!n;;;i!_>  333i߬Q 333in 333iFFFn 333i\UdR`[r]]](((i  333i>OFFFf!R`[r]]](((i  333i@Q;;;g  333i;;;i!n;;;i!_>  333iˣg  333in 333in 333i~dhy1Oiiiq/ 333it;;;dhy1Oiiiq/ 333ir;;;b 333i;;;i!n;;;i!_>  333i6  333in 333in 333iiiid iFFF= #:  333ia iFFF= #: 333i\ 333i;;;i!n;;;i!_>  333i3 333in 333in 333id;;;e;;;s*r@  333i@ 333g;;;s*r>  333iuuur9  333i;;;i!n;;;i!_>  333i` 333in 333i֥(((n 333id(((h;;;8 |=  333i֛Y333e;;;8 |<  333iq  333i˟;;;i!n;;;i!_>  333iK 333ir 333i333n 333idkm( F;;;5 333iϿ(((lHlm( F;;;z2 333iˣ* 333iVCCCQ;;;i!n;;;i!_>  333iRRRs= 333i>+++*! 333izN(((y |O333qn 333izJdUxNF_ 333m$ 333iud`U>%YxNF_ o# 333iߨN 333iv! g;;;i!n;;;i!_>  333iv{5  333i]]]viiiliiiliiiliiiiiii[ = 333ip'>uuue;;;e:.333jn 333io$5 }d6uuur[ 333i|*  9tY 333iz=jt' 333in_;;;i!n;;;i!_>  333iv>rx+ 333i/ 333io/=+!333in 333in6zd_;;;o1 333inc;;;q1 333io 3333z֞K 333in_;;;i!n;;;h!_>  333io!1 zf 333i<  333in  333in 333in9(((wc+uwF 333in, u|F 333in]u#333agX;;;cg;;;bk> 333ag9;;;s< 333a: 333ag333ag333ag<;;;s\ 2 roK333ag 2s]]]c*333ag5]]]r9  JK >;;;JK(((K>=  JKAl 8 Jl* JK JK JK>]]]iC  *X333sߨςth9 JK (X;;;z \< JKX9 %NipiM#CbniO( #MipiO' 3h8%NipiM#6YkpnfJ#%NippppppppkX5%NipiM#%NipiM#%NipiM#2VkncF+Jcl l l kgT8 %NipiM#8q֝ߡ 6%NipiM##KgnndFXuuuj+    #M vAkd+T vߨr(il8 *Kpuuusn. MFFFkA5Rd YJ$ #M fjfR/                           %D]knj\D(!CV[YYYYYYYYYYYYR9!CX[R9@U[VC!:RYYO51MX[U><RYYN3<TXJ+AU[XG' ,JXY[XH* %FVYYXK.:RYYR9<R[YR<@UYYYYYYYYYYYYU>!DV[YY[XD#=_fO' +HXYYYYU@@(((i˓˓;;;jM F֖:  F֖: CG 6ϴy< 6g= 8ϧr9 JFFF^,RiiiJl j3 .lοw 1ˍMQ֙3 D8  F(((E,c$XCCtA 333^V333^Va;;;^ =icJ @f,333pX3iiin;;;YSx1,p(((T1suuur@Dv5aߤ< ;;;^;;;Z*RRRgF2ua*RRRd\333f_333hbm;;;h!+333ruuuu81FFFu]]]t5.;;;sRRRt2O w*XY 6FFFoyrߩ]]]o: _|<= {djo# 333hFFFO]u$,wl F֝`;;;XQ 333idn;;;i!]\TjcR$ u֞J%tiiiu8FuiiiN,zx/ WFFFv9  333iuuuvuuuj[ 32333w˔K333=auuumuuu{nX~N>ߢ˓˕˖˜˞˖˕˕֛m2  333icn;;;i!6]]]v v,*uz>  6u֞R 333iC,#Vw(%/M(((nV iKQ_V, 6FGO{VGG@, 333icn;;;i!j˔TQp p{Yv{XtC !fv*Q˔ߩV+:>@Ju333r$ 333is, wߨQ%333mn A˒֞xU9$ Fk  333ico;;;i! @}FH˔uuuRRR u$*x|3 #mt% /}333y6  333inQߨ w, 333in%333d˔o5 :~c 333ifr;;;i!!s]]]FFFu%'w ;;;Q 9 FFFD 1z(((y5 UߩM 333in(xV 333inY333d2FFFw]]]v6 A[=(((ym 1 ztD <c=iiiuJ/++( 333in#sc 333inJ֞ y|8 :~d_]]]˘Mfi#u;;;u1[C 'r;;;u: :~c*x֩RRRiiiqiiiliiiliiiliiifiiiR,  333in A|]]]w9  333in]r. :~d =x333n+ =xz> Nߩ_ 5RRRw u$_FFF|5  :~d]R 333inpj 333inYb :~c[Fop ,333viiiu8iQKYK(((yy+ :~c@vi 333in >xx=  333in AFFFj3 5y\$_˒sN DF_i =x333u.,RRRdb @iiiqR6z[ag 333inj_ 333inNFFFk֝֝i6$\C Gnޛzl<!aa! 3uuuek8 ^T 3m(JxU'f@ #֑K 333in 5iiifW 333in+G`k k ljbF$ 1UjncF#@\j k l khX:  * M g g M* 5 U d W8 'M f dK#FcnpngN( :\kppjR,2VknbC6[kppppppppppppiM# 333i}*3 T c Q. 2333un            333iyfN'   1Tg333n 333i333J%`n333e;;;^3wh;;;MFFFN+vO .RRRORRRdRRRiRRRiRRRiRRRiRRRe]]]Q0 9RRRWRRRfRRRiRRRiRRRiRRRiRRRcFFFM* $%%%%$$%%%%#     :RYU@            #DXU>.X8RYQ6*HYXC! =[jgM#8RYQ6C\N( C\N( 8R[R9+HXT= ]U']]]XyA 8֕ˊ8 !iiiRG +f~M 8֕ˊ8 G WG W 8֖֕: #XCFs< :(((tiTU2w;;;^!jdTU333X|.333X|.TV3w`#tm8 y֒' ]f   A;;;h!  C˒\]c ;;;Yr/;;;Yr/]b :}lGx= /\uuubuuu];;;> /=GH>+_MFC3!5DJF9$ .@GJk;;;i! 3CHC2k˞ˌ>  %+....,%_v99FF5 \ߩ o*\ߩ o*_d *..*  :~n#tm$$1Yno֞ߨ˒]]]k`1_FFF֝֝vhA (Q mw֝֞֝FFF_?2biiil֜֞֝iii߰;;;i! *Rns֝֞֜siDT߰t.8](((h]]]huuuluuumuuumuuumuuumuuuka(((@_;;;v֝ߧze1(((Uuuuiiik8 (((Uuuuiiik8 _f$6FFFVjuuumuuuluuubuuuL# :~nGx= 2]]]c333i/_O /q.9FFFj;;;i! 2 qːVG֘> Jo/_(((j,.yS.yS_qJ333p. :~n#tFFFmUY_w= #l: *p;;;i!'pF`T :t1_U9|i9|i_d  :~nGx= d›;;;`_h F֝|5N;;;i!M]]] k$TK]˞˚;;;W _ d <n <n_|/  :~nfT[]S {;;;h _ o,d333{RRRn֕uuuO!huuu;;;i! fˣ@ /a߂RRRS((((d(((}G_333;;;e :~n :~n_ˣ y6  :~n y`(.FFF_R 2b}}]]]};;;i!_v3Nuuu~9;;;c>%+/! ;;;dp5T;;;i!;;;dߛV6f' (((fX: _NV~;;;h! :~n :~n_RRRF :~n 4 T f dM+ 1 O e e O.%[(((~ߩ;;;i!_c+ }<  333i[ 333iN=;;;i! 333i[rD _;;;~2_p +333u;;;i! :~n :~n_3 :~n   =s;;;i!_n'<333~: ;;;e߬k'333h\'K;;;i!333hGn> M c _d!333j;;;i! :~n :~n_V :~nX߮333y2 _g5 diiixcg_(((D#333dg֤;;;i!(((dv2n> 'rk<_c 333i;;;i! :~n :~n_֩uD :~n;;;c333uuub'_ߛFFFj'XHb;;;i![(((333x1n> J趧tC_d 333i;;;i! :~n <n_;;;u: :~n;;;bK_\2RRRl\ A;;;h! 5]]]m~8n> Mt@_c 333i;;;i! :~n An_{2  :~n WTXr.DqQc;;;bGuw,g9 fuuumMX[333c;;;b6zgNߩnXiJ333v]6zg   2FFFfn8 >֤ߧ l3@(((mނd2'b}(((KA(((lߨRRRfAKf( 5= >@333J333J'fKbn >C@uuuj˃ 'fK=RUUUUUUUUR=1V g li`U\ d kiY6Cbnmfbf j jbG$ (Jd j lmhU2=]j jhdfkniO' (Hc j l lldM,#MikX3 _]CbkbC 'OiiO' 2VkiM#%nCbkbC5XkpkY52VkiM# ==      +pi  D˓e    @@ /s c\O!HciiiiiiiicH!$iii]HY;;;j,Crߧc# :tuuuj>9b333lsw]]]i gG! Juuugb8 #2<><3(((    #6, ,iݴq*         +r> +.+$ (9D>. /AF<' +.+$ (:GD3  2AHF9'  +.+'*8DD5  .>D:+%*.,! +.*%:C5 ':FF:( *qf6% +,%$,,! +.+  (.+  ,/+ %* #,,$*..*  *.,# *.,#%., *]]]OiuuunlRRRex;;;n֝˒mc@Aen֜֝ːFFFfK*]]]OiuuunlRRRex;;;n֝ߧuc/  (Q ks˓֞֝FFFk`5*]]]OiuuumlFFFm֝֝ziF5cuuul˒֝;;;uzkuuumuuuj[3*]]]OiuuumiRRRYc(((l֚֜Z' *X;;;l֝֝RRRiX(  *niiiuuuoa333> .uuuUuuuiuuula333> >`uuukuuujY/ *uuuRuuuguuumuuuiuuuX5 '(((OhuuumuuuguuuR**uuuRiuuunj]]]R.GcuuuhuuuY6@_uuukuuuj] 6%uuuNuuueuuumuuumjRRRU65FFFSjuuumuuuhX -!]]]Geuuumuuuk`C JfuuumuuuhW,KRRRߦOKk* 1q]]]lDKːU=]]]jFFFQKuuuG'n(((L#i1R1,wS @(((c'X@ @`(G֙333c1<n s}mr  suuu~F  6 z֞VYFFFx v*_;;;\_333 ;;;eb y3_]]]333s/g;;;i!_iho;!YrX5n>:~nH˔iiiFK֞]]] u'Auuuum$2FFFvU_OX֡kGc_OY;;;h!;;;dr6N]]]˔D _|8Oiii:;;;et9[;;;i!_;;;r֓<URRRu8$|Q n><n'wĴ u$+(((w\X֟2 c;;;u/_q ,333v3335gd_p ,333v;;;i! 333iN(|ߩO_d+ }=  333iN>;;;i!_]]]~N,/+1FFFjf n@ nDAnQߨN`uuuu81m :uuuv]_d!333j;;;l$`d_d!333j;;;i!333h\!8(((O_j#9333< 333gY#F;;;i!_x F {֛@ nV  nߪ_/`n.333v w* :w߰o : 3 mRRRu5_c 333i;;;i!_d_c 333i;;;i!(((dc{֢> _Yy3 fYv;;;i!_f!Kq(((Vnqh]]]H*lyFFFn]Xr˕H%rm# @|d_d 333i;;;i!_d_d 333i;;;i!\;;;j'_]]]߬333j$\RRR;;;i!_cD;;;[gPb˞n6]]]vFFFv2J֞;;;}ߩ(((u([ߩ˕T suuuv: _c 333i;;;i!_c_c 333i;;;i! 6iiimR_U :u;;;i!_cYKRd Jߨnjb* wkD333w\Fx踎iiis=QkX[333c;;;bX[X[333c;;;bJzm%_ q*Y;;;i!X[MFFFd,/RRRhV*;;;gg >{iiit8 [x> _uuus8(;;;dc!Nz> >@333J333J>@ >@333J333JC333jߨuuur_* _֝l1 #Yu;;;i! >@ ,RRR[333i9=RRRiςe3 >kiiiK`\1iiib] 3uuue\1]]]f@8 n(moCbkbC 'OiiO' CbkbCCbkbC 'OiiO' *Jc jo khU6_w k jbF# :\ h l p;;;i!CbkbC*KdloieO+1U fj kgR16[ h li[FG\kiM# %NiiN$2VdN% 3XdN%CbnkV2*Qjpm]< 9iiivD  _q(   M;;;i!   ct#_c :~;;;i!5RRRu˔H\]9|;;;d Ru'HH,vFFFM;;;EJ%;;;IRRRaRRRhRRR`333I%:RRRVRRRfRRRdRRRO/  -]]]ORRRdRRRh]]]\H !%! $$ $%     6KJ6%+>C( 8OYUA     3U]C5TggM##MbO' Dcj\@ Rx賎u֛]]]OFˌ(((3 5RfdJ! >\jnk]C#  :Ugnk]D'(l@ OpKK(((K Ad( @ˏk'1333rx5  '[uuupX1h˒f2 A kx˓(((pQ' 8z[ @ˏgg;;;bXY[(((dTuuuqJ(miiir8 , m333_% (`}s3 :~bbcn;;;h!TRRRh' N֣KiiiO;;;wG]URߦ= %j˗FFF333s2   Mq' !,........* 333dFn;;;i! :֗6 'V]]]h]]]qv(((]]]333y9. ]]]]w]]]k9  :tRRRDg߬> Xn#  6JK6@_jqxj_A3Zuuujuuumuuumuuumuuumuuumuuumuuumuuumg]]]K%333hUn;;;i!G=  *]]]Ouuumiii{iiiyFFFt;;;}֡zh/  (Q nv˗FFFm`5gtK( i֥RRR[( 6]]]riii~RRR֜J.Te`F.5`sᅴs]' => ;;;P5  333ir n;;;i!R> /Rfh]A$#'Kk* 1q]]]lD CRRRzF duuuh5\ g!.(((f֜XTV333_ n(%333pbn;;;i! <F GFFFntQ31N333_8 ]Q%on:  (S 6 j֥FFFc+ 333e333߬uz6S uuue*HM;;;JHARRR\n;;;i!9}b 9ߩFFFz T_fKi333KR/ ~ߨg'(RRRj(((yK_ߥ M_˓H ;;;Z˙HVRRR~8(]]]Olswysm]]]P* ( M j i>ߨK n;;;i!2y MTX_333 ;;;eb y3333_cQ[1s|333wj#MdQ.CxU333`uuu}t,>]]]o<  ,3<gD3. %Yuuu|1 5sq3n;;;i!#;;;e֙? Uuuu~uuu}A _OY;;;h!;;;dr6N]]]˔D ;;;ROe333q55s֠y333wcG]H*>iiitY333X 333bp|3Ag, y|FRun;;;i!Y^ =uuugcCA](((vߨwV_p ,333v;;;i! 333iN(|ߩO Aiii}FFFrFFFM*nn|O1w;;;sy5O֘ˌGURK(((`!9|_`p(Y˞mn;;;i!M333g1. +Jch h\<_d!333j;;;i!333h\!8(((O,V5$e dnRRR[$;;;jFFF e|> #Xuߥ֙;;;dYbst> 5{RCyuOA, FFFFl+n;;;i!`O  _c 333i;;;i!(((dc{֢> 3uuur;;;FFFroth(((C#Q{Yd~RRR333n'3C='< []]]]]]]^Z< %+/GFFFbiiilU3+% .{˘˕֛m2 !]֝F n;;;i!$;;;j˙e*_d 333i;;;i!\;;;j' J֞G,333lJ CFFFY  !! 'FFFLiiiciiikiiiliiip]]]}RRRRRRRRRiiisiiimiiikiiicFFFL'cQ!c֡Yn;;;i!2z|, _c 333i;;;i! 6iiimR_]@333J333JC333jߨuuur_*  GC  JiiiiiiJ8 suuumF @A A`mpppppppnbC 333i* n;;;i!]= CbkbC 'OiiO' *Jc jo khU6%Kdd]]bjnm kbFH333a;;;oR1gwm:CbnppppppppnbC333g `'n;;;i!!V;;;9  A]]]\ <]lFFFd;;;hFFFd gbC!  eNn;;;i!Guuuk,VT  !  Xdk;;;e Y`J9 1FFFfWYFFFTNv= %hR2]]]]h{u6 8~=._uuukb= 'Ojuuul{]]]i dA (59+,9.'89, ,8<5(   doomsday-stable-1.15.7/doomsday/client/data/fonts/normallight12.dfn0000664000175000017500000024201612641367670024514 0ustar jaakkojaakkoPf !" # $% &* '3(8)>*D+L ,V-Z.`/d0i1q2y3456789:;< = > ?@ A BC D EF%G, H7 IBJFKJLRMZ Ne Op P|Q R ST U V WX Y Z [\]^( _ (`(a(b(c'(d.(e6(f>(gC(hJ(iR(jV(kZ(la(me( np(ox( p(q(r(s(t(u(v(w( x(y(z({(|(}(~( (( <  << < '<      18  ## 33  ,=( (=1 $31  153.55!<21+CD.  333+]]]6'\333Wx,%T("uuu;Rj}]* Kߢ;;;QH֍ 5RRRQ_ F 1w, N%%OGuuuO@0 %P%Q OFFF2RRR6 :ˈݴxG$zߢP CJ,}֕֒]+]]]Guuu>]֖֒֓ˍa(((O(((O6eRuuuN]]]U(Pˇ֔O$RRRM]]]M[˄*AiiiN (((EQiiiJw#333Eb<ݛs333uOYFFFM*˔FFFFMNMFFFYFFFXKH趴2D;;;'JH HmiiikI $|ˆ'tˇ o8 %v azGc(K;;;lFFF>333+'uuu_ ;333+FFF?FFFB333qzCiiimiiilB'n(((ptD'QRRRE   !RRRHR%tFFFxG/kiiidvo]]]duuu_#KM;;;Fˆ15֔uuuvHT{֞֜o/!siiiu q * Mu֛ ロm.< T NJ%i>=谎h%uuuW˔4+˃FFF< VH!gA>yn%*x. #% vRu T A.St 333Y _ jd 333M_[ߥRRRM C333s.[FFFM   5Wk.]]]:RRRARRR?RRR?RRRA]]]91fU2 rVRRR[FFFiߡp;;;lRRRz J ;;;cFFFb ;;;=˅1RRR0AuuuH!-2rߥ]]]~>3谛uD1}֝uuuM'Q%J h fJ (((O2C{ˑFFFscD'hn(@;;;T333SC(w,\T:RRRuO6zߥ(((ˉ1GziiiVCH(((g/ 'mߣ5 T (((T5ߣsjFFFN M^M^ Zߦˊ^CˀpoopˀF`֖֛[ iiic>D z333eˏuuui]]]]p!FFFFu;;;xG ;;;9˂+  ?ߣFFFs5  Wˌ@ O_`` M333Tiii;Miii;M3334V<JGFH<[;;;4F bJ w(r i333E˄/ m(((m FFF,uuu@RRRr;;;uuuy;;;d=RRRxˌ.+x5%i;;;u,RTQP9FFF7/+CyˑRRRrdD333%NM !VNM;;;T 333RK(w,`W6 yN,ߡ*(((U333R((( vq$]]]a>333\ ;;;ZFm(((N!FFF^lY6 WWVUQU9Zr;;;^!333BF=ˉ Qau;;;P˅(6ߦ9  ! 6˖ߠ%%;;;H~OUFFFFG333v>{tQRQP+xFFF=  RRR.mw;;;' uuuY~./ˉiii^$$iii^ˉ/(w,Vޛp*RRRRQ =RY;;;oU[FFFT ;;;HH((((q9K333coߦKA~fh$  @\֖) VQOOPN+ˋ[<:֒5D(((U*iiilRRRO \ PP ^;;;0]]]83kj֜333j2$RRRfRRRu˓FFFD=ߣr zD@݂vjk$UXXVTFFF, FFF0]]]7##FFF/]]]6@(((Q(((M{{333N'v+@֞\8 C\ I FFF<˄/ ;;;,L`333yJB |rˏ(333cK;;;TFFFSVߥ@FFF/]]]6FFF0]]]72Y]" "VX/:}:F;;;;;;uFFFk\2֗z,*sߣ3]r$ߛ3334F=333N(((V uuuIN(RRRT1;;;Uk}3Jf hJe˄%]r>C+jk+[bV!(((/e(333,s#MFFFV%`K ']]]S3ˌ֗6!֗K]re˄%  66'kiiiΛ}˄9N L:  +N]]]jߤc /GR6FWJ$.$f@@ホi%Mb  +'KJ' %JYYT8DWJ% $JWF$JV@( .OO1/+  Mb,@((( ֝K ,,#KRRRZRRRZM    :FFFZ݂_G P%% P>QOA!## %3' (@=' '361 %36/'36* (@='             (DC% '3/ (A@$ '3,99(53135(         15533(*1(1 .ˈ֖ˇ(((LPエvG 3ˉ֕֓ߠ_M.ˈ֕֓ߞiii8  3ˉ֕֓ߜ Pエv H ,Z'(a( ,Z'&Y',Z'O˄+ ,Z' 0zG%iiiL]]]@ /pC FFF2RRR6OxݧrO 3ˉ֖֓]DRͧrO 3ˉ֖֓iiiW<%]]]ViiiO֎֖֓֕֓֎ ]]]3&Y',w2+k+,v1 0iiiC ]]]:t;;;GSߞ*$֏OSߞ(]֑֓֓֔ˏ֗ 24֕ߠuuu>.֏2֎֔g#K333vp(((QFFFWˌ(((X;;;R+;;;P333n333AFFFS333XK333n;;;A;;;9;;;P333o;;;A;;;4FFFWˌ(((X;;;Qx-;;;MKJJ;;;MKJJ;;;L[X1;;;MK333O֝@333c%333N˓=$|ˆ';;;W֗(((]333^ Z;;;P333pFFFS:FFFX֗((([333[(((Y;;;P333qRRRWߣ3FFFMRRRjRRR](;;;3;;;BRRRj333m;;;?3330 FFF9֐+JJ<;;;Y TA>(((V'uMKGRRRK ]cˋ'!x] \$ ';;;;;;;A;;;A(((i> ;;;F;;;^ ":(((LFFF4;;;_9R_[ˋ NFFFk8.5 ;;;Y] GuuupHRY  ;;;Y] LFFFk8.6 333\YUU 333\YUU 333X[ 333\Y 333YkF d֠. 333Yk,*x.J]]]k<FqJ ;;;Y_UJ]]]k<FqH ;;;Y_'QqQ( \Y  ;;;?ˆ2UU]]]a>,x+%hߣ5N333`(((`J*uuudomN JK  #n f ;;;B~= uuuZ. Fߠ<SuuubaG 333[XTFFFM SUH$ 333[X@]J 333[TTUR 333[YST333W֦\ 333[Y ;;;W]]]s(((lˇ/ ;;;WRRR\!+w,uuu_QXFFFM 333[Y;;;zHuuu_QXFFFN 333[gvCh|@RR ;;;=˅1RRR \R Y YV;;;^]]]:5ߣh%6߬Y[Yciii`$ ;;;<˅/VN:ߟ:X s$o1 333[Y6vFFFDXJ 333[* i֗6!C @ 333[Q 333[YST((([ 333[Y 333Yr;;;[ߥM;;;B˅1 333Yq((([((([Aw,$@9wFFFG 333[iiiZ %@9wFFFG 333[ v=(((VST ;;;<˅/TR+xNiiia<8谂fbiiibr]\Yx# ~\5 ;;;=˅1HW :ߟ:V_iiisCu8 333[Y>ߢFFFG VTG$ 333[X@lˍ<H@ 333[SSVR 333[YST 333Y￿G 333[Y 333[\$]]]E<;;;=˅1 333[[#]]]^y,iˏAAFFFH 333YRG qːACFFFG 333[ ~333i ,\yHST ;;;B9 WM Y;;;_333_T8֖uiiiv8(uuujK YYDߦO ;;;=˅1'hn(:ߟ:RRfV Tr 333\Y pIRR 333\Y Sp!S 333\YUT 333\YST ;;;Y֛N 333\Y 333\Y  ;;;=˅1 333\Y/p֣+ QplH 333YX Sp! kH 333\[ FFFbV iFFFSST333Oi.i֗2<߮MKː/](((` FFF\o{<XY.q` ;;;=˅1 VH:ߟ:S]333yF3֘(((m\\ b֐$ ;;;ZY`RRRgz,SYK( ;;;XV/(((l\[ K ;;;XVRR ;;;XVST ;;;Wkc1 ;;;ZYR5 ;;;XV ;;;<˅/ ;;;XV=x%/333i]`FFFhg' ;;;WU/333l]`RRRgf% ;;;XV<ˍ.uuu@iiiTf MRRG֜n`FFFl(((M333hp##qhFN((([;;;[di%XYFFFj pYYJ$ ;;;=˅1NM:ߟ:@RRRW>gs!(((ERRR^2 @a(((A= 6iii`ˋ/(((A=<<(((A=YU(((A=]3(((E(((A=;;;.s#(((A= K诛c 6iii`RRR\,(((A= :fFFF{G(((A=W9 ;;;>j(:<ZFFFX GOH>uuuVd'.iii[%]3AA/J ;;;<˅/,v,:ߟ:CYYG$JWXM+ DYYYGCYYK( ( HXXO. ('(333jH($DYYR5( ('  HXXG($M {iiiqv(,FYK''8RWF,'2 .,CXVXYYJ$ ;;;B~>   Hߠ<1a$H;;;Y֓\ ;;;EFFFZ((("RRR8RRR]93334(%1ˈ֓iii8 ֎ˇ_!','.     #      $iiiRRRRM !vK RRR/]]]2Q( RRR/iii1iii1 (((iii7,_uuuC&Y' uuuB_.CSR> =o ,(((Y[$ˈ8 FFF7֏( FFF8֐+Ad FFF8֏*(((M333M RRR(RRR) FFF9֐,$J$;;;C(((I JJHFFFE -. n9Y r333oY Cy+CU: ;;;B|uO5$JO1!Hb333nˈ/ (KJ%} .OYC ;;;EyJ( '(FFF)RRR,/ ;;;?ˆ23OF(/M@AXK(  HU:DYK( $JYJ%*C=DO1vz#  '$,''$3MRO5 333F<UU8wFFFG/{RRR f! i֛''iiiV%+a$ 19 333RC *a֐(uuu^|+2re(*6ˊ9 333Sq*333+]333+]FFF'RRR-1333X. ;;;=˅13337֘FFFpK (((Bq*%iii[W(((Bs6+b^3334/H֑,uuuD 333(TS333(9A5ߞ38<C֒1RRR5+FFFT$+uuuX#3G >9 iiiOˆ,ST*{iiiOE333OR<25(((Am*%iii[U+!+333PFFFL 333Tpߤ+FFFMsU 2FFFM rqz,]]]X(((S  zt[˕ˑ< 333S I FFF<˄. FFF<˄. FFF3;;;XRRRm(((V ;;;=˅1 333G 333yߦˎ}# 333P I;;;J qm@ ;;;Ltd#RRRO rqy* 333LFFFq333B ˈ}RRR>$ ;;;9˃,*xFFF: :RRR\+WC<333WAWKD(((V(((rRRRj R,ˊxF;;;^>3e b9l STlˋ:(((/uuuh;;;4 333O G;;;J od@*(((Oˏ;;;Q ;;;Eˉ<fK'ˆ]]]X%(ˇiii](333Eˆ/ oVST {]]]hkRRRl֕, ;;;B<<ˉRRRD ;;;>˅1 ;;;>˅1 ;;;Kw ;;;=˅1 FFF5]]]G'Vl#333Kˇ/ ;;;B:<ˉRRRD %|iiiY' kW ;;;A}6!;;;lG+ˇ]]]Z%333Eˆ/ ;;;F֖FZuuujNOP ;;;?|3,yFFF=333_yˋe%FFFYiiii|G 333lo W333` Xi' RRRT] ST ^FFFV @U[uuuZ֔]]]P$ ;;;B:<ˉRRRD %|iiiY' kWuuu\FFFy֛FFFQ ;;;>ˆ2 kJ(|RRRW!*ˇiii]';;;B˅/$]]]ykNN+ST(((WiiiP ;;;<˅/.zFFF? ;;;=˅1 ;;;=˅1 ;;;H333m* ;;;=˅1FFF)RRR-NN ;;;=˅1 ;;;<˅/.zFFF?(ˇRRRZ$fY ;;;=˅1;;;hJ1ߡ;;;W;;;A˅/ ;;;>ˆ2+333Y=OO ;;;H@9FFFD J;;;SKߪuuu_!(((n r*x@6ˎF ]]]T _ ST _uuuZ% ;;;<˅/.zFFF?(ˇRRRZ$fY2RRRo' ;;;E֙uާs(uuuW|U9]]]S yjl+iiiT{bRRRI(((#RR]x9 ;;;<˄/+yFFF< ;;;<˅/ ;;;=˅1 FFF0;;;Mpe% ;;;<˅/FFF(RRR,ML FFF=˅/ ;;;<˄/+yFFF< ]]]RzgvD ;;;F֙vΛm'avduuuj, ;;;<˄/ )dMM<;;;J ;;;T RRRfː1%q;;;`]R[333siiinY n333pRRRmb5ˌ> 9ˌ$2rk5z4:<pߢ]]]O;;;,h h;;;,;;;.s# ;;;=˅1 FFF RRR##`3;;;.s# FFF!RRR$ 87333,s#;;;,h h;;;,2r[ 333M֗=>ߣ|+;;;,h Rˋ.3$<333FKWPH'_,.iiiZ+5o$333dG1uuuRˇ,ST({uuuR;;;,h h;;;,2r[usrrsu GVXCDXN/ /NT: (MWM( +MXD'D~_FFFmG  ;;;P˅* ,  $   (MXD ;;;FˎO13N_;;;jˈ/ %JN/ 6N::SYF-, 2  $.H;;;bCXVQ5;;;H:ST8wFFFH  (MXDQVUUVQDiiis \333rF5iiiR FFF:֐, FFF9˃+ ;;;PJ;;;H Q JJ OFFFH Lߡ;;;K,֏8(((#uuu>(((#CK;;;?3ˆY*u+Rˆ51C3    !    %2 (8! /MR UHCt333.1x݂]$RˌRRR[+ 3֙;;;Y;;;L2J a$D;;;' Sp 333V|6#e]]]\EDST`kiC+˃FFF< Oˑuuuv<(((bz> Dxiii^y _֎]ˊRRRG G5C{ˑFFFscD% 52֗ˎ9Ou˅RRR[(!@UߠFFFE333TO(((.(((.!uuul(((pFiiiVAuiii_v jjˊRRRG G˕ߨ=CyˑRRRrdD$uuu_z5.ˍFFFqˇ%!dqE G;;;U_h֗֗M+xFFF=@uuu}mYM* R賎aJ a$((DtuFFFlcD>kXbRˌRRR[+(((.(((.CYYYK+ =Y /K P RHDXVXYD;;;8 G1iiiV! *8doomsday-stable-1.15.7/doomsday/client/data/fonts/console11.dfn0000664000175000017500000021201612641367670023632 0ustar jaakkojaakkoDf  ! " # $$ %- &6 '? (H )Q *Z +c ,l -u .~ / 0 1 2 3 4 5 6 7 8 9 : ; < =  > ? @$ A- B6 C? DH EQ FZ Gc Hl Iu J~ K L M N O P Q R S T U V W X" Y " Z" [" \$" ]-" ^6" _?" `H" aQ" bZ" cc" dl" eu" f~" g" h" i" j" k" l" m" n" o" p" q" r" s" t3 u 3 v3 w3 x$3 y-3 z63 {?3 |H3 }Q3 ~Z3 c3 l3 u3 ~3 3 3 3   ,+,+ !YxX'JC% +LG ,. $(#.FU=6<=TO1=TO15<'KYC=TECYYU:!GVA#HP6333C@(((=(((t@(nV;;;I(FFFZ(((.'ke\ +x]]]M333E@](((7xߡ<5seK333BFFFYQ JC(֘5(֗3<Af, UFFF4++ iii]O #uuu_D 333YX333O333Vm z<@ˣ{K333ߩ]]] [K ]]]xˈ';;;WXF޿1 T]]]b 333:33RRRdMJ|*zY(((<]333333R ;\((( QFFFdY'|ߨ B.ˏ DB Yt!H|tGiiivg 2/ 333[YE AH :AާRHFFFߧ6J]]]ߧv1 HDRRR[b FFFhJ ˞N kfCuuug#(((WUiiiyH ,Y`FFFX 1333UTY(|q(O333`. i^J]]]ߩ XrF֚5 iiiH8FFF>8 333XT .2+1yFFF]]]s!uuu^(((y  iiiZ9D;;;Oj-.8_ Y߫3339dvP=G5aV ;;;Viii]]]TXY qP|\%mYul,XˎL8֙29}uGzG 3338x, 3w, FFFC= `FFFq333W 3O$2d(f6A|(((B 'G :pcD S}' P(((d<֙9  iiiWKe63FFFTFFFYQ kk6FFF4-r.GF(ˌ3X333WFFFCJ*r W+f*T2iiiWK (5,++< jˊQ,$HDDWJ:D<!iii]2iiiK\]]]OC (5/FU= +MYM+ CYYFCWK( ,=UN/ @VF/ (KWD@VJ%%3]]]OC *]]]>  FFF,811 :US:CUC$3CYV=FWR5CYJ$8TYF8TYM+ FWR5,  +MYC,,1*2.3/*!GWC8TV=!GWDCYV=GWD +MYYYF,  .#/,# *%> WW!h MA M 333\!Af16FFF46k;;;\!ACdx!l,333C@AC ]FFF:<;;;B(((DRRR\J֙=AFFF_!;;;B=!iii^]6[!iii^]A QFFFX(((/k3330ACdx!.ݎc$D;;;B>;;;C1֓UcߡFFF9 '%% *uuueJ9V(((SFFFFwyFFF}CFuuuk XϿ!HpS3338Xuuurg#Oiii}e DO]]]~d M+ HqS3338XY(w, 8iii{ 333YXXq fv/R;;;W 333[TXM333YVL}ߦ3O]]]~ =L}ߦ3X֙/Aq C *X]]]DXY(w,#mH aYJ;;;[R;;;[:RRRb @!vjgg^iiiW^#~QRRRQr;;;M ]ˠDXx!333Wk X`<GQ333T:R333w=(333Wn Xq+ 333_Y 333[YX֜C T;;;X 333YQX n(((jY ;;;Xi$333rQR333jR]]]M ;;;Xi$333rQXuV:D|R 333^YXY*w, a _1x@5wKFFF:v333V*ߤ]]]yO #Y_]]uuuJ!biiiSYߥ5$˅iii}RRRA !q_X(((qFFFZUXY(((hWM(((.O贿֐%FFFZ](G8X* 333[Y 333[YX|T;;;X ;;;V޿OX(((XFFF\U333[YN޴uuunˍ.FFF\U333[YX}uuub uuu^(((Y  333[YXY*w,Huuufkiiif#sv~ \*֘FFF|K #uuuQiii[iiiYiiiXiiiH!_uuuV1ˏK'h 333=CuuujX333;;;uߦ5]]][UXY dYNާFFF^]]]G O˕f\ !]]][gc6XFFF y賴}* 333[Y 333[YX]]]j T;;;X 333WˈFFFTX T]]]\U((([YMߤa< ]]]\U(((\YX߭J#[ˑFFFQ 333[YXY*w+$l]FFFSU6;;;` 6!ˁrppiiiiN[#/{M#v˘˙= _]]]~;;;g֞AXc3 M 333Yj  X^/qJR333h R333l+ 333YuCuuuyQX`5z+ 333_Y333gTXw gT;;;\ 333[f$333fXX֜O ;;;Zd(((nQQ333w< ;;;Zd 333nRXg_֞ˌ% 333\YWh<|y* ]ˠ@333T333OUb333+$+(( ˁ333U=1a333C]ߩFFFY kVkZX s=HmWiiiCX]]]nf$Oiii}e K( R;;;V H|]]]MXY(w, 6iii{ Bv˓:X`JMOiii}e M, ;;;YX ;;;YXXghMKwߥ3R;;;V Kw5XaXߦ8333<Ys ;;;ZXH}޴yFiiii]A  3oHuuu~333i 0+͛i'DFFFDAZ333[ߟ#Ae16a<;;;B333\,ACdx!l,(((/;;;UACbY6v(((C@(((C@AC88 ]]][W<;;;B ]]][(AC a333<'H 333E@!uuu`R #kKA333TCߢx'6+ R*,#/CYU:FWQ3CYJ$8TYK( *FWN, ,  +MYCDXF,5%8TYN, ++,13 GV@*M;;;w0,1:US8,#HWC%5/'# 8Q= 333I;;;G'5/'5.CYYU:FC/ +H:Q֔=333>?,  CL, ,//,=Q8aT\ :aM Z :++333E,3RRRVl9Duuud#/6AAdw! GvAA.FFF7.FFF7AC'6Vwx֚3Yiiilbߥ5Bbq 333Y$jC 5RRRyR]]]d333[`4$JXDXTD:TJ%!Hu~+DR8Ky֤OGVR=XQD%c;;;d8%c;;;d8XY3+ GRRR}QANGK86FODFT::MRD!HYJ%>RH( FXD;;;d]%p;;; c+zR 333XVYa 333]XK;;;eTߤ֘1#OO X\]R#uuu_* ZF$J]]]Y+X֣ NR ER EXrd˄ 333]X(((B֠8@֟ NFFFYU@^#uuu_c(((BbFFFX.C֠RRRo#V, a333g 333YX2֘ˌ/ 333[Y iiiC:V9]]]9333X´| X˖ߩ>CߨqI (Kuuux*Gߛv% _ߪ߫l$G]]]~ (((uXߨ#'FFFW 'FFFW X(((K 333[Y 333PiiiSVߨ#Iw5V˖ߩ=Kuuux' 333Y˘333] . :PA˘RRRr##iiiuk :: 333[YcX 333[Y$,G;;;Us(Xq$;;;lVYu333Xf:w+W3.z2O]]]333JXxAˌ,X;;;Z X;;;Z Xr 333YX ;;;B YXxAˌ,333Wg!;;;nRXq$;;;lV333Wf:w+ ;;;[t/֘ˎ(((S#;;;d` 333^V333li 333[YFuuuh# 333\Y5uuu|д(Xn!;;;iUYq ;;;X`8}+W;;; W SA(w,G֡RRRc*XY+x,T;;;X T;;;X X֩;;;g 333XV ;;;:z {YXY+x,333Xb;;;jRXn!;;;jV333X`8}+ 333\Y8333XːRRRQUw{ߦ6 ;;;YVF߮ M, 333YXRRRfM ;;;ZYFiii,X֞<D˕rN +  NFFF| (GrP+ (v+O߰uuum333KXY(w,T;;;W T;;;Y Xp a 333Y ;;;:z {YXY(w, K]]]v6X֞> NFFF| ( 333XV=iЧp`Q^(((;333E@Av 333YuK333B((((Y,ߢ'A Y _k*ro(((\adm YDACdx!>333CUFFFW AJ<.333E,;;;-s ]CACdx!#uuu_YX [+yz(333A=(L %2/,CYYN, ;;;I#.uuuIF5DCCC>* ,LVN2CYWDDWL+ (KXM( FWK(  URRRF,  ,AZ, ,8FC#++,,  #HWCXYD +Lv+*=VW@%iiiOiiiJ ,]]]Q333I(ˁ=RRRW,CCC x˄$ RRRAFFFOFFFMFFFMFFFMFFFI3333> T WHDVD , !#    3:,': $CJ::(((. +LL+  HWD3KLA!uuuF(3ߣ,333E@]iiiSO(((5Ep+ykiiiY(((/ DߣRRR\!j;;;{K* ,' %/ $%,'CXUF$P u ;;;ZXD֘(#iiiWH,o|M;;;}R+ >uuu~rB.ˋߩ֡(((K* $D@33#(((/@@Rb333@D!iiiU*Je(< ; :aJ~֐333@D!iiiU*.ZYZ 333\YFFFTˋ9@˘ WFFF_U lgD˗n3J(((P]]]]˅'(((LuuujRRRnC kf uQVW'u+R333aXk FFFRj5rV3֚RRR RR333aXk B|R  yQ 333[Y ;;;Qc !HK' Sߨ#Iw5-RRRmA˕ߤF N(((<(((Lˆ/<ݛ;;;iiiSdvPV;;;_XY(w,3֛֙^(((Qς;;;VkFFFk 3֛֙^j]]]e! C֟> 333[Y \ uuuRp n[333*XxAˌ,333Wg!;;;nR8 'Ny C uuugl{#=ݛ;;;iiiS!m~`T;;;\W_8w+(((b˞9XK c333p(((b˞9 Oː9Aߩ@ 333[Y \ ֏(((a333f 4XY+x,333Xb;;;jR' %Dߧ֠# iii[֟J 333O͂lFFFoA nj Fuuux\* QFFF (J;;;dV֟8+wZJ;;;d( W( |Q 333[Y ;;;Qf :R@XY(w, K]]]v6/ː qU#Uߤ(((d%333SοO.#$C=G֝C$el1֗o uuufH=]]]^N֕t!5bYߟ3~Q5aYb 333\YFFFYˌ9ACdx!#uuu_Y5{25;;;Z 3>( ;;;1+%JM+ 1OYK( !2+$! ..,%rv(CYYK( J]]]j ;;;[YG﾿$,  #HWC16DUC B[;;;WFXVXCQO 'k/333FCP333M-D' ID'5/#2E WWCdoomsday-stable-1.15.7/doomsday/client/data/fonts/normal24.dfn0000664000175000017500000104401612641367670023470 0ustar jaakkojaakkof## '! '" '##'$6'%H'&]''p '(y ') '*'+', '- '. '/ '0'1'2''3''4 ''50''6@''7P''8`''9p'':' ';' '<''=''>''?' '@''AN'BN'C'N'D<N'ERN'FbN'GrN'HN'IN 'JN 'KN'LN'MN'Nu'Ou'P/u'Q@u'RXu'Sku'Tzu'Uu'Vu'Wu'Xu'Yu'Z'[ '\ ']+ '^7'_G'`Y 'af'bu'c'd'e'f 'g'h'i 'j 'k'l 'm'n1'oA'pS'qc'rs's't 'u'v'w'x'y'z'{ '| '} '~#'5'E'W'h'v'''    8C61=>'      9C/    AC!    DI/ @豧s quuuR#+9(#>9$D WP2  (Me f[8 *I J. 1:8( ?豧o3 :D/<G2JJ: G33X g gY6 'HH' K]]]^+^333}5*uuuWuuuNL8  #J{˓ c9 . lpJ (lP9 fs]]]h\+ ^QKˌm,iiic=(59nn85+  @֖˅'C]]]kuuulH(((K333JcKm(((= G֚RRR[!';;;eD  .k֝qJ`p:!YU :jo'm\ 9]]]n]%i s2 %uuuT(((rw诛e133c]]]f(>0%]]]_˝FFF;;;u6 (uuug [ =ߡ M F˓w .FFFtuuus6 Xuuur]]]rT5333v;;;u6l333p';;;d֟QQ֟;;;g! 333i;;;i! oK1<*%:># =>Qu( J֚333֝Q d|A 9@$ f]rT 9ˡ߰8 FdpFFFRRRqdG +;;;qk'333riiit36tRRRo, 333i;;;i!n@   >cg`uuutD .FFFj _+M]]]fߦ賿x.U֟jC@6 $333mF _j 8x|6 @A DQ1u333p%'333q5 333i;;;i!g5. zp%#g(((t2>iiijs6+CRk(((]]]gF:+ +tf]]]bߤߡ(((:1uiiiq2M˖333f>MMz|NM>FG`uuuq56|;;;j#!333lw9 333i;;;i!X;;;l' F;;;}˙t59<6k(((n.*puuuugX%%k M =;;;n% @;;;l$ QR *]]]Oiuuupuuu|֥֥uuu}uuupi]]]O* 'OippiM# qq 2z;;;n'%333q5 333i;;;i! Ad']]]KFFFw߬FFFRRRgRRRmiiiRRRRRRf]]]J+  $cXFz`O֜F G˕;;;i! <iiik*'333C333C' *2N(((333O2* (((IJ5uuuq`(333siiit55uuutRRRn+ 333i;;;i!%RRRNF :FFF:,$C((((((c.yiii:  ;;;c(((r t$ H;;;i! <r+  %333n;;;l';;;VYQD ;;;e֟OQ֟;;;g! 333i;;;i!9 C. .gxv֤ypi2 ?iKVͧ;;;e`֥Q,miiib8b(((]]]Q A;;;j# >333q'333c;;;c(1# GAj;;;q*f}//}f 333i;;;i! + T ^KC GGO6(^uuu}(((d@tiiixV/xi֤_6;;;s+ H;;;e 333K;;;K 5_֗;;;X,!CV[YT=HWM%(333qmM{{K 333i;;;i! Q= 3]]]m˒MpnaX'uU_(((;;;G $RRRd֣uF(RRRl= [ k (GG*  RU  FO AT*333l֥֥(((m* 333i;;;i!uuu]M >iiif/ aXG333m/Fg%/;;;g(((h(URRRFFFc+mUm\ 333W(((aWFFFWTo6Cws@333c;;;c Oߢ: < O]]]H8  O;;;oߩ m8;;;=(((h/ :FFFh333j8%dˑpRk3 Tp' w> ThFOGWJnuuulH333J;;;J 'M U@ AA'  '<4,Q[* % FI./ThgR. C_ hj_@!#FbbF3]]]sFFFu1 D p! AYG YN$# C F( 8YiiY6 'JJ(    Dzd   dTn˒G =]]]c2  !iiiP֔: 6RRRpw, @uo# /ߜ;;;P9'=5R֜M hRRRo9 $  #giiiK*ߟˍK (OS3331]]]@iiiND               .H`knnj]D' @_ij`F#  Hdl keN+.GA @_jjji_A6A/#Kdjjjjjji_C @]ijbG$  $Gbji\<: U[@<fRRRn֝˒ rO' 'c֜k.  I(((m9 5(((mD == #Yu]]]_2 J8  *dߧm1  1mߧx[$ :tY *`v p8Om%;;;VFFFj1 .|]VH$cߨDZˏ:  go**o[ *(%( OߦD +riiists r333vRRRl=cJ;;;HU$md]֦֛֠֞u2 \ߨߤ? Du% C˒֣߬OR֣x< $Djuuue֒5   2˄uuudjF%uuuF֘d f(((iK<3/6T;;;n3 Xjye (CRK]iX֞d_[KC.H|XAY__jQ]RRRpk(((i(((a bi]]]] /R vM(:ACCCCCCA<*HuT1  !M333`J֟}Y]l i(((c\HRu(((k  6ߝX/(n(((f$r֥nFwd_u(/333rzR ,{333v.h,*x;;;b333g˚c $ni!! :c;;;vA .de1 @;;;wf= %}n,(((r˖qXyߧFFFRRR> !O;;;d6h9;;;sc_M([˙m/6]]]v\d=8i333g˛g##d;;;e Huuu\]]]P1 Iuuu\uuuY= #FovRRRoR  DGQRRRnyrG# 3 \R}U zvm\[m M~߫U 1 z c_߮˓FFFjU( .333viJ% ]RRRw5Q߬ [(((a333nk;;;g >333L :b%'\˒xV2 9ߡ< 1Uw˒\'d> !oRRR{Vp˗߫\@333|k'|_ C: 'rFFFmYo+ K o5.333vi6(((]]]~= R(((h D333T @x*HiiixJ'>\giiiiiig]@'HxiiiH ' tpO˕> M < ]@ A]`(((n/Qy> 28*ok,iii[ߢC(;;;[ TQFFF]/$3999999993$/]FFFR_iiir: O\k|*O;;;q*t;;;b 5 n  3q[ 9yg'D`nm/i߬Y'wqNR 1l֜U,C:*FA#  8z tQ. ';;;J]]]aRRRiRRRiRRRiRRRiRRRiRRRiRRRiRRRiRRRa;;;I% .O t9 ;;;bߩY_sQ(((mJ Fp$(((pp#puuut< 5c(((;;;` D,*`˔G nmj;;;`H˔F (((a;;;yx;;;333a #Fdiiit6  <ny;;;u_3DD1]333wyo=;;;`},d333uM;;;q333|/Vߩb>]]]|dR˕\ b;;;h  /dyyyy;;;K2Tn߮r%Y;;;h  s u$'RRRib$#\]]]j(']g =fFFFuuuuc2 AA /iii`FFFvg= RVd333vQriiik*$y֞Q1qK>]]]t:#' `;;;h '5988M+ #!HFFFNh{(V;;;h  @|ߨN*p[Tiiiv,CRRRt]]]r8  !1( 1Uv˔N GdnnnnnnnndG J˔wV2  2]]]QFFFS9btX333u;;;<OuuuxUb;;;n, 3}j\F# 1puuudr`j;;;`ug 6˄kcUd 9 Vuuum`;;;`k w*!333i b`;;;i$GRRRtߨU#N[N$+333X֗`9'GqwG DxrH'(\lW( Tf(((nuCURRRzߨQ%t(((EN߬YncSпh 6uuup߬Y5iiisXY˚˚[ 5pn$MNO;;;[!<TD$!AT<  AM >q֡ ˣy1D;;;XK(((n/gYRuuuj9 U q. AFFFu2/333lRRRi2 =x. _(((XV}/  RRRRW'333m}p zpM*333<333I+FFF]l5K> 2iiia o=!Xt o31dX5 nߧo9.FFF_ߦn. JO Ao, >ߣOX p iiiyロ[$MgnpppppiO' *Mdl jcJ% #K[A/QglldM*:\ h jcJ% 3OG$ $Gb j kdK(.GA# !K \N$. dA WM%.(((q(((ytukQ56Tnvt|;;;P     +c֘F Aq֟ kK2$%5Mip=#ˀ]JDRRRnuuut(((qp k333ptiiinG#%6mߧ p: Cjiiioߨ֝uuun kJ%  1Kbkmmi]J1              2QK(  'Ndjji_J, !@]kmmk`H, %Mdjjjig]J1  'NdjjjjjfN'  %Mdjjjjj_A !@]l kmlbJ, 'HJ' >TA 'HH'  'HH'  'HH' :Y_F 'HJ' 1RV:C[M' 1iiic_!333IߨRRRiN>o֞RRRnX$(((KߨiiipiA333I333G(((K= =o֞FFFoV$333J333J =@ (((K333J(((K333J(((K333JNy4 333J333J$\xHXߦ(((K\F333cߦT!VuM333a`%333cFFFR333aJ TrK333c;;;bV[333c;;;b333c;;;b333c;;;bJvuuue/333c;;;b5y]]]q>K;;;b.333vp  333i֣߫p3[ߧп^ 333i֢ߩߩߪ` 333i֢֟֞֞֝ߢ@ 333i֛֡֟֞֞m2 V֜֟uuuyw] 333i;;;i!]c 333i;;;i! 333i;;;i! 333i;;;i$CiiiqzK 333i;;;i! :~{5 =RRRs;;;h!Qy>  333i333m`NDcNKXp333zJ 333i333iQQXg ֟G 333i333iNKKH8 333i333gNKJC,@xߩT<9Hb{G 333i;;;i!_d 333i;;;i! 333i;;;i! 333i;;;j2ARRRrU 333i;;;i! :~x,5{;;;i!$ ui 333i;;;o/<Y%sk2.F?! 333i;;;o.+[uuu{(((n' 333i;;;o.  333i;;;q/ # ob+  '=:  333i;;;i!_d 333i;;;i! 333i;;;i! 333i;;;s[333w֞_  333i;;;i! :~pM};;;i! C]]]v6 333i;;;s5!J(((N Dߧm%   333i;;;i![֟֝G  333i;;;r2  333i333{< @˒i#  333i;;;r2#md 333i;;;i! 333i;;;i! 333i(((i$ 333i;;;i! :~Ѵ;;;i!mFFF] 333i;;;|333r5\ 3  333i;;;i!+` 333i333yb__[C 333i m j j bH#X(((3  333i333yb____md 333i;;;i! 333i;;;i! 333it* 333i;;;i! :~(((;;;;;;i!6iiiuߩiu333v. 333i˟|(gr 333i;;;i!kl 333iD 333iDgv 2=@=/ 333id 333i;;;i! 333i;;;i! 333i9  333i;;;i! :~;;;wi|FFFyt;;;i!\bk]]]Q 333i;;;Dkj 333i;;;i!fn 333iU 333iDknFFFDˉ; 333id 333i;;;i! 333i;;;i! 333i9  333i;;;i! :~;;;l<_ߩ{RN;;;i!+(((wuuuzuuu}u% 333i֡iiii3 ho 333i;;;i!pk 333i@ 333i333;;;j;;;i;;;h;;;_ I$gs+w;;;W 333id 333i;;;i! 333i;;;i! 333ix.  333i;;;i! :~;;;i((n[%@;;;i!NߨF 333i333fO[[] . 333i;;;i!.b 333i333qXUUQ< 333i333A$!!\/%d;;;e  333i333yb____md 333i;;;i! 333i;;;i! 333i(((]]]y1  333i;;;i! :~;;;i# + XsT#<~;;;i!# rp  333i;;;o,1(((d D˓ߩ]  333i;;;i!_ߩH  333i;;;q/  333i;;;j$ Dߧ_3Y;;;;;;i! 333i;;;r2#md 333i;;;i! 333i;;;i! 333i;;;zf]]]r{2  333i;;;i! :~;;;i! ! :~;;;i! Arnns֣y>  333i;;;n,,;;;c% o|\( *A;! 333i;;;o, +]}t( 333i;;;n,  333i;;;i!((((puuu{X% C333;;;i! 333i;;;i!_d 333i;;;i! 333i;;;i! 333i;;;l5DRRRr}3  333i;;;o.  :~;;;i! :~;;;i!k |38}i 333i333bJTuiiihH˓֟V@=Mf}֛J 333i333cJKUg ֟˒H 333i333cHGGF9!  333i;;;i!M֝{M52AmFFF;;;i! 333i;;;i!_d 333i;;;i! 333i;;;i! 333i;;;i$@FFFr3  333i333iNKKJC, :~;;;i! :~;;;i!5]]]uߩNT]]]u5 333iĿ֠M gߪ|{˖^ 333iĿ˕˕ߪ_ 333iĿ˕˕˕˔֘RRRI 333i;;;i!!g]]]v]]]s;;;d 333i;;;i!_c 333i;;;i! 333j;;;i! 333i;;;i!>;;;s3  333i֢֛֟֟֟֞n2 :~;;;i! :~;;;h!M333r+.333uN333cn' *kM333cx[$333c.333c;;;b (d;;;L333c;;;bX\333c;;;b$333o;;;i!333c;;;b<333ue!333cJ6z;;;b6z;;;bFQOA333Juuup]*  $O(((q֝uuup[%333Isi>333Je'333J333JGq˓ϛx\, 333J333J >C 333J333J,iiin;;;i!333J333J:(((p+333J> %`333J'f(((K!K]H! G]K# 'OipnnkdQ3(Gbj l llfQ3 'NgmmmmkdQ6  'OipppppkX3 'HJ' %D` j l lniV:  'HJ' AVC 'HJ'  @;;;h! 'HJ' .Ti`@ 'OipppppncD1OK' 2QK'      f˚;;;e   =aXKOj% ,uuuP(((^Q'                   'JQ3 'HH' (Hc jomj_@! 1VjppniV9(Hc j l lj_C#  %MdjjgY= (KdljcG$ A`mpppppppppiO' >XJ##K[A!HXA *NT5#J[C#J_M%@YJ#/TbN(  %K`U2F_U3 %J`U3(((Kiiii>333J(((K !J rߨo=%`ގuc,  !J rߨp>(((K޴j69 mߧa' <333G <ߣKK> DH% b{$GGV]FߤE]p, 'jm 9iiig=%gx333c333v9333c;;;b +j˓rT6zm% ,j˓uV!333a]]]g6 1FFFiJFFFFQUggYM r(Kߨ]]]d%Qq$3]]]qt: # qRVk$`Z5w y5 Yߨ[ 333i|5  333i;;;h!%m赛֜U :~ѿ˘K'n赛ߧX 333i߫ dXߎV/eː˔˕￿˕˕˓֕=]nnc6iiit֝J!tg :vC X` Av: / z֞XNx2 ]v+Fw333u5  333iz.  333i;;;i!R֟YFHbu=  :~333fRn;;;gU֟YFHbx@  333i{dx1iTOrs@*>FHc((({OGD3_nndms  @|}@ rk'v(((w+gr:;;;uwH#A]]]sRRRr=(umT333zwF 333iq' 333i;;;i!/;;;suuu{[( 2p֠m  :~;;;o,(y333c1FFFruuu{[( 2n˗ l! 333iH#MFFF: ;;;cn% 53 +333pH _nnd Cz> it!J֞]]]t3 F֞J/FFFu֞JDu;;;|k {J 3|ߩ[ 333ib  333i;;;i!O֟[ *u< :~;;;q/,} gQ֟[ (r<  333iM'Riii8l(((K  333i> _nnd$ tg 3RRRuߨM+(((wVm r Q(((w+U˔;;;ߨ\@iiirq% 333i(((;;;T 333i;;;i!f + :FFFT :~333q]yiiiff, 9FFF}T 333ivuuui*U|j6 333i> _nndNߩRRRu5X(((w+`w.:iiiu˘˗w>, t`!ip%Q{1  333i;;;wduuuuwG#333i;;;i!lk{` :~߬߫Jmp{` 333i`,tFFFtN 333i> _nnd,333u]< w\ :wVf {Q|kU}w:  15 !jRRRw= 333i;;;l8RRRRr@/333j;;;i!ndpc :~k$nipc 333i 8 5 tˑX 333i> _nnd] t֟iiiu8r\'Y333r#y߯߯' 9333˙_ 333i;;;i'!_ߩ(((yV333s;;;i!nk{` :~˓RRRjV' mm{b 333i2 +_iiivA  333i> ]onc8iiiu˛kJ֞;;;iiiu85iiit֦(((֞JCiiix{H$333nD  333i;;;i! %m(((;;;i!g* 9FFF}T :~;;;}cVC+d( :FFFT 333i;;;[ ={_ 333i> [tr`kˣD +(((wmm(((v* 8(((y;;;v: 333i>  333i;;;i! ,yRRR;;;i!T˖V%p< :~;;;s3 NT(tz:  333iYK ~uuut> ,t;;;l$ 333i> N {+* U At$`F  F] 1|˚{3  333i>  333i;;;i! 3 |;;;i!2RRRr]]]|U% /j l! :~;;;i!.;;;qRRR|R$ /k˗l 333iACiiiww( .kK'J(((u* 333i> 9}Y  X= # rR :wv''vuuuu8(r{Kqx* 333i>  333i;;;i!8(((y;;;i!Y|T@A[ x@  :~;;;i!R{R@A[s=  333i>]VR˒{KDr;;;l$ 333i> #(((juuu~mD6Ckuuu~(((l$Nߨ;;;w/rXXo`|1,yd  333i>  333i;;;i!>]]]q;;;i!(s֠|}֜U :~;;;i!$k֠{˒R 333i> ,wRRRu9 b]]]\ 333i> Mޛ]]]x}M+(((wcJ֞]]]t55]]]u˔HN(((y8  5{Q 333i> 333c;;;bHw;;;b /pߨuuurR6z;;;b +i֝xV 333c8D{^Nx= 333a: #cc#[t< *(((rjj s(.iiieiiiq@=RRRruuug/333a: 333J333JQ333J $O(((tl:'f333J !M333p߰r<333Ja'\x'b]]]lG Jl* #Q;;;qFFFoR# 1iii[WNw< =M 5xJFs6  Jl* 'HJ' =XM' +Kd j l ki[= 2QK' +Ny~֤iiiz nRRRG$  'KQ2!H`V3 A_j khU3%JR5.Nd k l kdN.+6!8239F`[:9Y`F%JR5   !Ft4           (Orh'%A]j(((`S,              'OipppppppppnfJ##Kdji_AA I/A_jjdK#:TG!6RO2>R@1OH#=UG#=Q>  (((H:K= %`KUJ <ߢ> .A61A3;;;P֚> gD >ːߧM Dg oRRRr2_\VY6zg AːVUY._֖= 6p￿9 <֕˓˕˕˕˕˕˙p#nтoiiiR+,FFFpi*iiiRon @|Y 1xFFF] ]c :~n `= ]c DVNO3DGGGHViii|3 nj6 o(((q% 6ink(((v+:]]]`^_fAn%333nX_f 9֖K CD >vCnDXy< An 9iiiu;;;ߨN.=,#15,_}@5. %12*'2>in '22'Dz =   _x9/.!Mj],'YoY'$q[n> $ so :~n/;;;vuuuw]}D Jw>_N:;;;ll* 5 qn9FFFj n1 MH#_ːFFFE_ߥO+uHGG=]]]t333w6 n> gRRRp/ :~nRo/Vm;;;V d _r: ,333jy2%ln+(((lhJGJ(((I_h+9z]\] . yJn> K֝֝J :~na˒G1FFFrk/;;;K~2_]R }z֕^% Jv˞nNuuu= *y˝\#a8 _|RRRC <}c_c icn> /]]]of :~nY ^#U/ -iiiIVNm}9_3.k;;;b g6 '/$c8#OnhTJ~%(((e;;;]]333FFFh#_2.}U :}c_cNx+n> qs# :~n.NK* #HV>=`s{{< _g 8;;;;;;h 333e]]]CmT <n(((fT <oeiii||uuu333c_gf` :}d_d :FFFuFFFu: n> [u9 :~n   1m= _c,333w;;;h!(((j3333nF :~n;;;c֖< :~nV֤֣Y_c`c :}d_d+zY n> >}U :~n]]]]> _gHuuu~;;;d;;;bR i˙\>non]_gjR% :~n8RRRf2 _c_d :}d_dbdGCCCCA3n> '(((rn :~n.rxM_AKad(((U69C9YQ8_n]333fFKd k\1 :~n*§ k8_d_d :}d_dG|ﴴ֕=n> jFFFo+ :~n3;;;]]](((i+]FFFߧH F֜FFF}uuuv< 8uuupRRRv;;;n >FFF}uuuxK :~n2;;;K# ]c_c :}c_d%333cFFFQn> N= :~n'uuubC Nj#!gKQ֛g`G6zgORRR˕RRRob5V[X[6y\_d,333Gn> .uuu_: :~nJˎ֢= /uuu`~d*  *d~8 MRRRmK%_}_%%`K1;;;oiiie5 =@ >@ 'eC _d>_mppppppppppiO' n> 2HB  <n>]hfbg gaC1Tim l h]>>] h lj]:1QfnppiM#=]imk_A 1OH#U֣W>TAAUA2KA`cnU   Rn !!!!!!!!!!         juuu]b;;;b      p`n֦iI! Gh˞n%;;;G;;;^;;;c;;;c;;;c;;;c;;;c;;;c;;;c;;;c;;;c;;;c;;;[(((E#grt;;;_2(((RgC @h @: NV G{9 NC AN 9ߦߧߧߧߧߧߧߧߧߧߧߥs1 'hj+ >`% I d j j bH##H b j j d I%9MQOOOOOOOOOG1 $J fRRReuuuiuuui]]]fiO( KuuuWR'  $*+%    =RA1J> <ߢ@ 'e@ U[6y[]c :}c  _d :}d/;;;7)_d   :}d'1.+2+ %.,  ! (* +2, @FFFf;;;[8          _d<333S333\;;;C# :}d$ H;;;\ivRRRhsv bJGdjzm_1#H;;;\ gvRRRgrm(((cF !Dc;;;b nFFFceO*$ H;;;^333i333i;;;g333edH$  %Je;;;eFFFf333j333h333` H# 8;;;P333T\ gkl _<9amum(((iR$@RRRp(((~;;;W:#H;;;V G$$ H;;;V H$$333F;;;Y M( :;;;P;;;O 4%;;;G;;;YM' G;;;U(((K* A;;;R;;;K- #;;;C333];;;XF +S;;;^333W(((:%;;;F333[(((L* =;;;Q;;;R9#I;;;^333i333i333i333i333i333g333\(((@_g(Gp3 :}dGiii]]](((l.G֚N ,k˒o8Gߧ l3 2oG+vRRRu+ 5iJ+RRRb|*GGGG =_ 6kg AYJ`Aˎ]]]I 3֚V$/r' >d:q֒% C._uT]]]qj( :}d\X\h+$m;;;l1 \(((l.+n]9|ˋ1XZ 5r(\]\] :u<!bf A֛FFFs5+ uu=(o\'j|iy` <D(kj  @ l$_;;;FFFr< :}d_RRR(((d_]]]C Hߧ;;; `_ FFF|VR;;;c <RRR}\giiiuC \RRRiii\6_c_c$ tjG;;;x֞K( v`UjRx|@ 9333v]]]|. #tyYiiixߨO H a o xC_uuusD :}d_15d,G]]]~;;;g _3.|Ud>$5tiiii*_v2:|;;;_ b=1tc :~X2:=$ \iG! .333v;;;w8 _c_cNߨFFF v*X333s Aiiir{5 G v*'\b_[ :}d_frD%333p;;;h!_gd`;;;ciiiG 3 5_c=;;;;;;h 333gRRRA`d :~ (  =RRRtQ 333i;;;i!_c_d+(((vX2FFFwRRR֦333˔HVFFFF$u]]]U, zz._;;;U :}d_cn> 333i;;;i!_c_c m3333!v8_c+333w;;;h! 333i333z._d :~rRuy@ 333i;;;i!]c`dYRRRu3fw'K< H˔333v._|F_蹂;;;u= :}d_cn> 333i;;;i!_c_d;;;bO< 3_fFiii~;;;e;;;diii~Gbd :~o1U(((b  333i;;;m( Ykmd3RRRug =xߪ]]]V,|n%% uY >uuuun% _;;;s> :}d_dn> 333i;;;i!_d_da T8J]]]f'_9C _`A5xd :~n 0uuuPiiuuuw.333h333U>8Mߩ>Fdgz> o333wGpFFFw2!iߩ_Q˖FFFv2'yg@3%]}c{FFFq= :}c_cn> 333i;;;i!]c_c @;;;\_iiiFFFKJߧFFFFFFd :~n;;;Kiii ˟v.333a333vF9֣(((c =xr  F`!A}fT˔O @;;;bUyu_1 Vb.RFFFa*6y\X[g9 333c;;;bV\X[]p,_n%$ld6zg333O_VY (((`[m˒G%suuus9 !su< 1j|DF333vuuuf/Tߩiiiv9 +RRRfN =CQz2 'eC >@ Kf((((K333J =C >@ #Xuuurj/ _j,  ,id'fK $G`U12U`FH˔x= A`mpppppiM#            _t*  (qc      #tn_c`cD|@ ]_\_ep JJJK!jA(RRRKuuu^FFFL''FFFLuuu^RRRK(;;;:uuuZ_G #  # #!   A__D5U]D       Y֚< !DG% 'jߦ]=QG32<2$LYD!6YdS1 'Mdi]>!@\iljgV8  @ː@ J(((K(u֛C OߤߧRRRwߟ2 X 4 KuuumFFFb.,mˍA@oނqg6_]]]}S#e;;;b C]]]r_,hߣ9 1FFFqiiig1F{J\333V Vz]]]jC%HH' kiiiRn;;;h!KRRRl1;;;[% G֜T1333r￿=%iiid RRRFFFTT֛333]]]uuul@  J(((Kn333u*n;;;i!'333qo333=]]]Ock(((miiid333U2 @c$]֡Y@ .oqߢߡC>s |(((r sߪ333o2  6JK6333a;;;bo;;;i!n;;;i! 333ip  /53=MM85[_A!/FFFrv. .s;;;YA5$l} ֟`.Te`F.5`sᅴs]'!333j;;;j#r;;;h!n;;;i! 333im'8<3$ #I;;;^ jxRRRhso(((dG !Fi;;;l yFFFgfO*RFFF< 1(((< Dˑm]]]n1.(((f֜X =(((333> !333l;;;g n;;;i!;;;e;;;l#!QRRRfw333mb>'+D]]]B,G֚N ,k˒o8<+ C}~3 _q˒ ]]~MNuuuvMS uuue*#H(((a(((l (((l(((aH#=;;;333dn;;;i! fFFF>K{ FFFL\h+$m;;;l1 333>kuuu{@ _{/ kuiiizG3(((GF'*ub;;;Z˙HVRRR~8DD5_n;;;i!]5;;;WFFFY_]]]C Hߧ;;; `+vFFFM$;;;jRRRk myy|#qj333`uuu}t,>]]]o< CC !]]]W{@ n;;;i! >{RRRW!;;;Lߧ uuux P_3.|Ud>$5tiiii*'n333F*uA m{{|#35 rj333X 333bp|3# Icm uuu mc I#+/n;;;i!/, -OM1'<_333m|˓֛iiik['_gd`;;;ciiiG 3 5O(((߭U( %;;;l;;;t_kuiiizGMˊC5v_K(((`!9333z333{:333Xy> n;;;i! =uuuy]]]X# $6A=,_c_c m3333!v8 =iiiyiiiwAet k](((t fmFFF(((\YyߦH #Xuߥ֙;;;dYbst> 333i;;;i! 2]n;;;i!\5 _c_d;;;bO< 3Nߩ2 O 333a @˘yFFFl,3C='< []]]]]]]^Z< #333e;;;e$ =;;; fn;;;i!hFFF>_d_da T8J]]]f'ivYHAF= *q֦g!j ֝[  !!  .5FxwG5. !333l;;;g n;;;i!;;;e(((n!]c_c @;;;\$ sп}G8;;;rM 9iiil˗(((v(((vs.*iiiPmswwsmiiiR+r;;;h!n;;;i! 333irV\X[]p,2s]>RRRr.KiiiRRRj: CFo;;;i!n;;;i! 333jo =C >@ #Xuuurj/ /KNxyKiiil(((n< <@ n333u*n;;;i!(333rn@UCAUA8Yi l h_@ <[b__dkn kfM# 5Fp֠(((f8fuuunߧFFFm\.CbnppppppndFkiiiTn;;;i!O]]]l      R֖yu j 5QfnmcK._iii{T$n;;;i!Ciiis] 8ߣ ((((((a  @ː@ j;;;e (uːA <ߥTXː< SFFFR'j֚Y%;;;\m+@]`D /uuuQ[45V\A/ZiiicuuuhRRRb[,      $(!doomsday-stable-1.15.7/doomsday/client/data/fonts/normal12.dfn0000664000175000017500000024201612641367670023464 0ustar jaakkojaakkoPf !" # $ %" &, '6(:)@*F+M ,V-[.a/f0k1s2{3456789:;< = > ?@A B C  D EF&G. H9 IDJIKN LXM` Nk Ov PQ R ST U V WX Y Z [\]^(_( `(a(b(c&(d-(e5(f=(gB(hI(iQ(jV(k[(lc(mh( ns(o{( p(q(r(s(t(u(v(w( x(y(z({(|(}(~( (<  <<< #< .< / - ? B 7(2C( FK( /,<' / 8#,/ !RRR7!,$JJ%/1CXFCXH!,8TYC2 'KYXU> .OQ31OM+ CDCZRRRWiii[]]]Z W52/A333F!C=(((<2˄]]]> [֞PFFFVr>[8I֚ϴ|9 C(g%,uuu\$333< 333A*DC'gk(15333R;;;e9uuug#;;;`=52liiiO_58ˎߦA@֙}5  6 T cߦY=ߥ5X;;;[ %ЛRqtk i Q9:οiiiˏ1+֛߰f%|U_e333g[*/?>_X W֤߬(((YFKF/֕iiiHhNM˙r[$ aFFFM FFF>|,]]]Y˛i!o˚iii^2fˋW>JHJ>Wˋ f3 *! c ;;;F f ` X;;;\ uuuWݛ{:$f˔.q3333@ާRRR\ 8rg@3ߦDOˑ/(+]]uuufH%|}+HN]]]:HJ]]]|Y#KRRRY QKQ(((F:333w#!Te o%6x$#21:j֜WiiiNuuuRW֜h<{#=߬;;;RRR]1˒}*X;;;\ !59-/3i5!a]]]f +;;;b' '2Us$,N.. , N333333 O/ =￧v*@QMCHN FFFvW333cU5NMÛwR _GU:kuuux+333(uuuR@333C333CM}Z$uuuQX$Yx M$tRRRaVt豿%\֣VW;;;YqRRR߫vJb%K߮M!9 ˇ/333W;;;bRRRf]RRRDRRRElw88X(((^ JQHHHNND/T]]]l~5Gr Tiiip;;;j|z333T! FFFJADDˈ`GK]ˈJG 333_Hpqx+#t333iiipG J]]]k֡FFF}]]]dMuuuD333iˢuuu_!]]]c= 333[((([ 333^;;;W2v2 + M N- uuuRuuuR (((]XDQMDHN%o]]]h(((`Y2.8`uuu;;;X%}]]]]]]*JK*+ KRRR;;;`,/ Bߧ(((b. ]e ,(((aߧB L kk(((fYwiδz!KG1RRRR82+RRR7 WW XH6]]]iii.333[ ] 333^333X  OP$3cc%%p>+}˓.HNl˙\6Q(((333TDX z֤ | !,8;;;Z !|9333luuur!<< C($3 +Xuuuoo (((6emlme(((7 euuuoX+ 8vJu w2(((| (((333X333i˛qq333jAFFFI 9}G*yH5֟A(((X;;;^ ]]]i],v+[A'KK'RRRI8Fނg(((Y֤߭333[GM\ˡuuuU;;;BJ+}Tgߥ JRRRZ֢y$8X(((((Ih]]]N:[G#QRRRQFFF8  ;;;4RRRPQ#:֘e ;;;`ߪFFFːG 5ߨk i8 >uuuQRRR@: AC J]]]d  4333[=uuu`uuu_!(((]RRR}uuuJ Quuuq$3˒H  jWb=@K(kk*38֓* ERRRce<iiiRuuug%1vG <}+ JK ]]]J333fc=hFFFV  9*L333˙]]]vW:u('p<2#$,@FFFst .!HH!DVD'.<!,R`uuum!]]]RC'3-%JJ%351OYYU:DXG$/ HXH! (KQ66+ 5QR83'3 NH=8#uuub˘ a6( '5#FFF7 Y]]]ZmO!2 -(FFFa]]]a/(((I֎%ߝOJ(((^uuubiiiciii_(((]K DYT9DXXJ* DYYT=DYYM+ DYYFDXXJ* 1%3111=@15$6( 3 +# $JYXDDYV@$JXVDDYQ38TO. =VXVXYC/$/3%.3$ +86( 3< ,8>: ,8 (KYYXVJ'CYC/CYC(((DY 3]]]i|+(((Diiif9 (((Do(((D;;;9 3]]]h|+(((D(((EcA(((D(((E(((D(((E(((D(((Ec.(((D(((E(((Dk'eo(((Diiif$5ァk>p]]]i3 (((D333`>p]]]i3 (((DߦQHˌ,,3333ECe<% (((Xߪc' :: 333[333r*,fߪ ] 333[(((2 333[;;;^ [ߪ_2[333;;;^ 333[333h 1~Y 333[;;;\ 333[;;;\ 333[(((]]]vk 333[333u+ 333[;;;\N޴( 333[Y Dߨ|';;;^֠]$(kS 333[d ;;;\֠]$'iS 333[333ut`;;;0@H;;;YX;;;\ ;;;W;;;{/ gG1˕TAߩ6 c;;;j'uuul˕dX;;;[ /iii|: X;;;\ FFF_O 333[Y333[֤uuu|m2ːuuuxuuurߩ9333[֤uuuu/333[֤uuurh]]]M333[;;;[ 1ˏ]]]tRRRp߬FFFS 333[;;;[ #{Y333[;;;[ 333[;;;\ 333[;;;^2]]]ih333[߮vnuuuW 333[;;;Z M޴(333[Y[(9ߦiiiwy, 333[Y6֛iiiwxx*333[;;;[#(((m֜1~RRR֡KX;;;[ HzRRRvr% i*iiin333jAR333iT(;;;j8X;;;\ 333iuuuuuukuuuic;;;<X;;;\ ]X 333[Y(((D< =sˎ.(((D]]]f5 (((D+(((D333F :uuulߦ 8(((D333FcA(((D333F 333^;;;Y (((D333F (]]]g'(((D/(((D(((E8ʹu(((DChxAs]]]i3 333EC@suuux (((D333F .}9;;;Rn'A333Fas8CP MGiiiSs+/c*i=A;;;G*yX;;;[ @@333[YDYYM+ %JXXO. DYYU>DYYV=1!HXXQ31%31 RRRvY1D=DYYX@1.' .6+ %JXVD/%Uiiis /1 +6FXK'/9RXK'+, A@$3/=VYYYYN, X333=(<(((Y>G NFFFc|;;;RL,+N3ߡ Z,~'%t+ + *   3' /*+# (6///';;;IFFFB,FFF@;;;H' ,C=,Gw( 5;;;X333B=5ァk*u9333EA*;;;8 -3338333EA333EAuuu['AC%uuu\%,FFFiV*j, ;;;XUM޴(P֜1 333[YR/b/ 333[Y 333[Y*$}pXYi޿'*讴'@iiik%1%JR9 ;;;YS< ,NS8 +L( /NN, FFFu$8TYC 333[S: D(((\1 F _/ 333[\9( 333[YFYPDCODFYS:!HV>%JYJ%%JYK'@OK6DWCb |A//6( /3% 293@=A@6+ /@XYYC(z6XY2},+nfkJ%i= 333[J813}~'9ˍ/:3 G@ 333[@333FD333FC 333[|FFFcg 333[Y333Eߩ T333E@*uuufdcm1/mi333E+333R</.333EAC;;;G9u<333XC:kAiiiaOH#/nv+9|>333[C/15~+XY(z8 :mx;;;b_U;;;3333E@!ߢU Y333E3;;;R 333[ruuu_˙uuuuߡ.]]]\˗333(iii^ Y .w%nG 333[iii;;;U 333[Y 333[Y 333[;;;M 333[Y 333[FFF! 333[iii;;;URRRW赂H#{߫uuuciii`uuu' 333[ﴴˈ(}iii6.֝]]]P 333[YY;;;\ 2֜ߩJ5ߧuuuxRRRi;;;gi2֜JiiiOiii]]]nt| XYsxrˇ 333[iii;;;UFFF<333A$iiiB &\ 333\ 333Yqgߨ6 vx'##rb޴(!zd(((jivô, 333[qm;;;\ 333[Y 333[Y 333[ 333[Y 333[qp333zb貴~+ 333[qm;;;\ $6;;;W%|tq93ߨxk޴( 333[ 6# uuu\*(((nm 333[YY;;;\  ft%(((j߯R.uuuyFFFy$fq$ >, yXYx/uuuDUis J 333[qm;;;\  $i֤333d 333Xgc6!zu #n`޴(!z333333f333e= 333[YFFFb]]]R 333[YY;;;\ 333[Y 333[Y 333[ 333[Y 333[YX;;;[9z+ 333[YY;;;\ (ˑu(333X%|b\<6d\޴( 333[YAFFF| I;;;Xn 333\hk;;;\ >^FFFFt%/x]]]t'5֞X<333lnXYi;;;m  92 333[YY;;;\ :֦z'333YRRRhc˘FFFsk(RRRY333(uuu_333ߣ;;;;333[Y nJ 333[YY;;;\ 333[Y 333[Y 333[j 333[Y 333YXY;;;[3w, 333[YY;;;\ iii_˗FFF I%|;;;}m!j ( 333[YcvFFFRUa%;;;T]]];;;\ FFFl֞2iiipu^;;;h߭333g(((y*FFFmRRR]]]N<*XY%yˏ> 333[YY;;;\ 't3(((D::6/po8?(((DC.t(333EAC;;;G333EA 333^Y333E\l.333EA333C@A333F%nx!333EAC;;;G3v;;;c%|ˎ=<ˎ(333ECuuuQA83@;;;GJ WM R,aߢ3/jk/+}_ ,1*z6XY2}.333EAC;;;GCYYYYYC (KYYCDXN, ,MXC'KYK( ,NXD.N K///!]]]tV/@A/,/ // (MXF%|N/ /N*/!HT:5QC:TYF.,$@==@\˒1@XYYC#}pXYk޿'////DЎ~D<@!|KK%na]]]W*SS(iiiYFFFH;;;^;;;\;;;\;;;\;;;\;;;G Sˎ֙(((S,|333ORRRG֒/,˅]]]H!˅u,'RRRMiiiL88]]]HRRRM( +:.   #  ! 3%'H=3QC3QYO/ OY,r/6ߤBYߥˏX,_M;;;c›l$ QRRRB[((( 333Y  >@AA+kR<v!(((\333M8(((ˏ/Luuu]d u Vig*uuufdAFFFkoT֝Χo OG֢ߪ9 1]]]Viiiiii]]]W2 RRRW赂H;;;B<2ߧU Yq$:8kY333TQcm$6;;;WZuuun#9˜ uY {G333IR{XGߩ= (M N+ (ˑu(333XU˟i$ kFFF֢(H]]]˕}lyCE333S[gˌ֗Okkiii_˗FFF I Xiiipu+Jo iii_RRRuuuiiiiii;;;[ %' 2iiiYuuuuuuiiiY2 3v;;;c333B6th/hϿiii^%dd (MXFCTVXC(uuuZX%OiiiV_]]]VN! (KYYYK( FFFB߬D5ˌ;;;U (.doomsday-stable-1.15.7/doomsday/client/data/fonts/normallight18.dfn0000664000175000017500000053601612641367670024530 0ustar jaakkojaakkof ! " #$,%:&K'[(c )m *w +,- ./ 0 1 2 3 4 5 6  7 8' 94 :A;I<Q=a>q? @ABCDE F G:H:I":J*:K2:L@: MM:N`:Or:P:Q:R:S: T:U:V:WWXWY(WZ7W[GW \QW ][W ^eW_uW `W aW bWcW dWeW fW gW hW iWjWkt l tmtn&t o3tpAtqOtr]t sgt trt u|t vt wtxt yt zt {t |t}t ~t  ) 5HY                 . ,<>=.**$/$>NG/=T WM, %>61A>(68 %A<<A% '/% '/5QZQ6,8T\WF% 2O\]R3*2=333>'` h* 3xߜ1+FFFI>@(((`{֛]]]eQ$Q֕333[.'`' OgߣRRR[< =@ !\//\!:V333]i333<#  (]]]N9 FgkF ==8|b**auuucA9FFF_D(((P;;;R6s{: QAGzQ@ˉ> 6RFFFw]]]^(161FN+mh\;;;X!.mu1< eiiiiii ~333˗ h$*c;;;hH1TjR(((|mA333V;;;m֜HJKNYYNR/ [ݴ|>,FFFb c *iii_(((r63 siii_* 333WZ  $p1 (rߩT!]]]]a[;;;X! >ߝj֐(((?(,%_FFF;;;e1 <ˋ]]]tߦ333TY]]]x:+PP+!aRRRg55RRRga!=;;;zRRRRRR[( #8vާ].! D֗T5zgfz6 333WZh]]]_* $V ˎ@ Mˌ(((][;;;W!'*% H OT;;;\% URRR{< Yuuud:.5khgk5 5g߯Z#FFFBRRRURRRcFFF;;;RRR^RRRR;;;=\o: 9u\\ݴ|<  333WZ2;;;j_RRRC}*CkRRRrp333sk$[;;;U .vmqu. ']֜f1!CVi aO1 /yc \ : GߤYVߤJ(ߜr(((< -* (333ae$ 9u[Yݴ|<  333WZXTuuuL+*;;;b֡]]]KU WNFFF˙F O (((`(%Q]]]liii˕uuu\5  Y֢NOˎJJˏX*:(((D˂T:@VkdT:,HWU= @|X 9u\\ݴ|<  333WZ<]]]hiiie5 .fuuuzߤC 3'AC,'#m(((J 2iiiis֚U3t333~dt֥HRߤ}C Dy\ (k޴Q f+ X|@ 5zggz6 333WZ .pT %k֝YD ] a ]t]]](((K +]]];+ (;;;hiii]]]{˙iiia;;;> .g;;;sY'iߧpQa( F֙u/68 QˎKJˏXVݴz:   /333E(((U(((Q(((<   $e333a(*iii_(((q55(((qiii_* 333WZ%gm+ ]ߧ^8333gm% . ;.f˕xi]]]y}9 uuuBuuuQn333QT{xߧpQd( DGG֣]]]}JGߤYXߤJCz, %3330$   %3330$  :o\\mKn\ 333WZ_֝FFF_2!,1+>R 333X\;;;;FFF=UtJMˌTbޛߩc2 3i;;;b@Driiis֚U/e;;;sFFFq߫RRR[+3hihk5! 50;;;?FFFB;;;;FFF=K֖C 8i֛ߦi8 333PU>;;;RRRiRRR[RRRP(((; 1ﻂb(((f;;;oe3 333RU ;333:?FFFH,<z5 FFFM%FkrH ==]]]?% 2uuubA333== /! /$3/2` ]]]~j52/>SVN/9T][Q5!,C=M\\OY;;;P /! /%6P\R8, 'Mfib]\R9<U\\Q3 ,   N;;;L'      +(((diiif==iiig333e.3339A      -! Fˌ c%%f֗H+  X//\! #>:<A%                  1Q__XA  $>:8R\]]`cX88RZT9,M\]O2,JSA$@UWYWUA$88>X_]R9 =U\\\VA# >X_\\]VA$>X_]\XA!>Y_]\V>%](((.  .f3 %֒(HqxJ 2333_iii`>  #iiiLJ9a֗ߢ_6HJ <uX$1b|ˌ Q# <ˍd@ <;;;/ =,5t]]]}]]]d]]]W]]]G& *huuu\,333:]]]R]]][]]]\]]]aRRR|߫]]]b( 8mː(((ːr9 XRRRt333y]]]_/ 9M; <M8'ﺎiiii{uuu_*Dduuum vpt]]]niii]9%o o(T333]]]lK 5333e֙;;;g(((c]]]d֙1 T333zFFF];;;\RRRdˎ|OT333{FFF]FFFYFFFTFFFB% T333|FFF^FFFYFFFS;;;?! 9ux<'[333j= #'*:xߩbUߨxMt֝U1xMC{M(Fdk/%2666665/  /uuugcC'*H6=p{tp};;;[,Dˏ֚GYݧ|]Cj|;;;U(h uO/'2HU;;;=!Yݧ|Y+%1Kw|>Yݧ|Y,#! Yݧ|[,#   9ur2CFFFg> *lm< ]֝b1\^8up%Nuuut\, ,  3R g֖֗ O#'bloooooojK%333PˌgO1  +uz3' awFFF M!e f#Xݴ|CAFFFr;;;YJy< Xݴ|> 1q cXݴ|@Xݴ|D 8u(((333`U6' fwb. DˍdQdT1xF5f]]]a:(((::(((:<g]]]jkdD# +֍#'J fuRRRjc6 @o333_$ D RRRrFFFw  V >v |@ YݴY:dz(((X \zKXݴz<  6 yr1YݴU$ Yާ~d9.$*rhC G֛333o333YS2 gC <]]]tuuuuuut@  Y333wwa;;;?FFF@;;;?FFF@ 5xFFFqb< 5HMMMMMKD.#@dRRRrc/ dOU]]]s{KOv;;;Udߥcb֙f\ߩ333p˓ߩi;;;W;;;e+Xݴ{< i|: \ߩ_WS >#[uuuxgaX + 5Og uiiib1_iii_6  =qh$ < =  6]]]^V.] -.] - <|F!  %((((((% $J 8 8 l*Y]]]t|J!urk;;;V  :onm r< \ }1 333Y(((`#Xݴ{< `޴}= \;;;- [3333 :}Mfxw(((y\bKQ;;;333R1R]}= %Y333iX8 > QYYYYYY Q=:]FFFgwY#GァsFY q'o_k;;;V b֟֘֘֞c[˕j rwQ;;;W333f+Xݴ{< j|: [˕aYU@ Y޴bVC!R~[cFFFY.D y2 6iiii(((j+^iiiuT,MRRRs] 1jˑ d! Adjuuug]6*+<dliiifc> D;;;W,Yve 6FFFp]]]jYs333T3RRRmߪ֖֖֚֚ߪ]]]l6YݴQ#%RRRRu;;;W \zJXݴz<  8(((wm/YݴQ Xݴ~N  VX]FFFuD't|9[U(((W]]]m>6333nFFFTGjn<      9\;;;gh+ < R X X X X X XR> 3ˆ(((jV6  5nb%Y~v1*jr֘QXRCCR[Xݴ|>6;;;Z Ky< Xݴ|> /r aXݴ|> Xݴ{< .A<1GF Qt@Nr/.;;;hRRRh3Yg@]w XDiiifT %3330$  %3330$  /Nhˉ.  6y hK, 3] Y1 T]]]~iiixtq~tRRRc9 .333iuuulA@iiil;;;i.Yݧ|Y.5cy;;;U(g uN/'2HX]]]A$Yݧ{Y+$/Jw|>Yݧ{X*! Xݴz< up(((n]]]nc' 3uuub(((z]]]s[ C] Aߢ }zC/hb%;;;;FFF=;;;?FFFB$6288# @D @֗uu< Cc`C U(((~FFF_FFFhߥK8FFFcˎ;;;g333`]]]e֙2 U(((};;;^FFFYFFFdQU(((};;;\;;;X;;;UFFFG -Qݴw8 (((E c2 Cd(((a/ :\3 Uˋ֖V  3֕](  ;333< T;;;T   9= # `˖˖333zRRRy((((((b =f8 6`< >nV#3dˌ Q$ >֘ eC >\ :̧e' #DX]YJ*3Q]ZM+2,=T\U>99  /! Y;;;P68 5iii^{cjuuu{K5/.5@Y_][Q8 >UZ]]XA# @Y_\\]VC'@Y_\\XH+.(      3339A 9(((cuuukRRRduuuf֘X%        +.V]]]_uuu`[=.DTVRF1                    =U\\\VA# ,''..(,'.'/<(,'<A%  $@<9<# 22 <U\[]T8>Y_]YK. =U\]]T9>X_]YG* $DX]YC >V]`ffb]XH*, ,'2*112c|ˌ Q$ 8ﻧd'%]: :̧e' 8ﻧd' 9̛]' CaQ 8ﻧd' >j*%fA =c% :: /`vk\,  =]]]_C1c}m],  <333`9(c > .T=333= 8ﻧd' =iiiV1 >ߟ.6;;;d֙;;;g(((c]]]d֙2 Oݴw6 5pTQݴw8 Oݴw6 Qݴw9CuuugGOݴw6 V` YߧYVߧ[RU 3 gFFFh g]]]h j. T333|;;;eiiilm6  3(((fRRRg g]]]ii/ T333{;;;gq]]]_/O|333rpFFF<!FFF@FFFSFFF]333~;;;FFFbFFFVFFFG ,U;;;ROݴw6 FV! ci1(g(((tQ1'2HVRRR?#Xݴz< 9u\Xݴz< Xݴz< Xݴ|NJ]]]fc$Xݴz< \MGx_\K[_% g;;;rU3(8]iiiocYݧ|[2CU'fFFFpX5*8]iiioc Yݧ{[3KM;;;Uiiiv\9<<#  !,[x6![;;;X!Xݴz< 2]]]f333g+ >ve Kz= Xݴ|>    9u\[;;;X!Xݴ{< >vh! 3]]]f333g+333W333i.\ߩ(((](((V(((W(((V(((]֟\Xݴ{< Xݴ{< [{2 Xݴ{< Xݴ}gz xix\Xݴ}gxq3]`333W333n2 =r^[֟ g333l< 333W333o3 >s^\333;;;lw]]]c.Goe< 9u\[;;;X!Xݴ{< # gq=RY ]RRR[% >TWD$\]Xݴ{< Xݴ{< [;;;`Xݴ{< Xݴ|H@(((n;;;lAGu\Xݴ{H=pp@f` ]b+2333n^\sM ^c+3333o`\]]]\ RuuujvN 9u\[;;;Z#Yݴ{< Kd>iuuuh8 ;;;V333i.+G[˕bYYYb]Xݴ{< Xݴ{< Yv8Xݴ{< Xݴ{>9iiiT]<s^[֟bZT9;;;W333p5 @s^[޿yuuuv`8_333v{9 9u\];;;d,\ݴ|< .;;;hRRRpuc[M?m]]]xZYݴQ  Nz]Xݴ{< Xݴ{< Xޛ m8Xݴz< Xݴ{< :u\Xݴ{< >iiig_]ːR`VYݴR! ]֚TdVYݴQ.Qruuuj< =T 9u\\yDj{8]ˍDHx= >333n]Xݴ{= :u\Xݴ{< Xݴ{< Xݴp z n8Xݴ|> Xݴ{<  9u\Xݴ{< Jw]F333sAJ]]]q}= Xݴ|= G333uA Nuuurx= Xݴ{=]l'  g\ 9u\Op( =333vd+ r :̧e'%^= :̧e' +m@ 1b|m],  :̧e'3f֘|C :̧e'Y/ 2_fD%^=!Tl d8%(((TQ <TZ\\YG*.((..(f޴{9 .( (D=@Y_\\YJ..((/.( 'A< =UZ]ZT9.( #C`{P(((+.( #>93R]]R5(/8T[]YH* $/    /(((nq/          2`uuumH       9 U6U ^^uuuP2(RL*   '!             2( #,528A* <<9: +=.+HY\\\]`gdJ!#H_\J+2,@X\J% !:91>,, , 2J=, , , @RRRS,$(((QGD+(g5#Y2 3X 6333_v]= G] :]1 /333E![ߢR!˂FFF\5 =(((?=333= 6_/=(((?8333983339NOH֚(((l(%eb,![t2 U֙k3 /bˍQ3pV.FFFHFFFVFFFYFFFYFFFYFFFd(((ߥ> ]RRRliiiO1 @ߢO$]]]E]]]_RRR|;;;VNˍuF Ub U;;;QU;;;R(((T֙(((T$U;;;Q8;;;98;;;9 :nj$o˒G Ae 6333jp/ KxˎQNˎnOAro1 !#$/Y k#]333rH#.RRRad 8r;;;W Ai]]]|xiiia< /j+ [;;;^/  *h;;;X!  5j9  [;;;^/ !5' !5%  f}@ 9ohcߤH>]]]itbuuul_!!_RRR};;;j9 /us. [;;;[%`uuud3Y;;;T .FFF\z]b֗(((`*+ Diii?,JXQ6]333|iYUA# 2O]V</M]q;;;\!8T]O2 b5  6MX[Q3]333|k\R6 1# 1# Jߤ`Yk93]]]f333h+Gr֟RRRj(*nlC#d333l6 [;;;X!KKV;;;T 8U$'_8  (iiiPo=cߣa*Diiibߠ6 ARRR`333c NnuuubD1(((: %Vr֑$cmF:(((;: ;,;;;gRRRg63333hiiitY(R[Y2  3 RT֙uuujA[;;;X!2iiic`V;;;T  -jH# 'La+  /ˊ;;;wFFFX#gFFFrqY 9ruuutw8 8luuuu|;;;b!A֗iiin8 *]]]`uuuo6 Mzߨ333f%g]]]{h5 R;;;PR;;;P]\YߤkQiiii(((mNmm: J n U*DoˎO[;;;X!dRRRa.V;;;T  =Tf{5d֞rCR;;;xk3 WߦtJA9 XvKX;;;\!_߬VR{q!;;;R dynd{V{O[;;;W![;;;W! =r ˎG*c֛f  +rFFFq= >yg9;;;j`![;;;X!QߣGV;;;T @`(((miii= _333o8!mK'FFF_ x3 (]]]^ z5!f;;;Y!(f֛ߣ֜(((M .iii__!=RRRiM6rZ3r(((;;;i;;;Y;;;X;;;X;;;X;;;R;;;A# [;;;X! >zOV;;;T (hFFF ߨ֕,V(((t(((n֚TJ o;;;^w. A֖333q333w;;;ZDߢߥ(((r;;;jߢ3 5qUYuGU;;;Q;;;PTU;;;R[;;;X! 9tߠ>O Q 3d(CeR%YA 9333, [;;;X!!(((L<V;;;T Oe( >\%#Yˌ(((4Nn(((HRzuuuW+%^=#xL=(((?333<==333=[;;;X!+.- =A% 1=**3Gcib\\\\\VA [;;;[% #-Y;;;T <SWWWWWS<#DWZ`cU3>X]ZU==T\WD!8R\``M% :R\\O1(/ :ˍ˖]]]qRRRn˓y6 ,  +, `;;;X!   ]333rH$  9t;;;W ,,        J֟\`D (r;;;U\]]]kiiiP 1$]]]Fiiia]]]{;;;W<SWWWWWS< AFFFz opux6 :֕PF] /333E$ YK 8uuuW2 !G]\J+@Y]J%  $CUYYQ91+     *, 9=$ *% $=9<<=333= J֕;;;< 6諧c';;;<֕JRSU;;;R %FFF0!'`CNݴw6  Ca(XY [;;;X!        $;;;JQ    3s.Vݴz< ,|{8XY %8.[;;;X!2CR[O8/AUU=5FR[R6.M\YD% !H`c`T82Q]]XF#/6@NC 1O[K,U333N'#,! $'+  *%+35 !5..'''/JUVXU@ 9k`Xݴ{< b޴}@  X\'5a֎![;;;X!=;;;FFFwˎˌQ?iiixmF@FFFba3HpT GiiidߧH@u@ 3iii[]]]Q( 1. 9:(((55 = O$ 1Tߜ,<M! MG /uuuT2 /֓U#,_ߜ$ 6]]]Q1 8n֎'!ˀ< A|TXݴ{< [ːJ 'GXT=#YlR(((hP[;;;X![uuu}{֝< \]]]{h5 6gvy333a,`~uuuuK=zuuuuz;;;Z^~> Qߧiiiy1 (uuuYiii|uuuT' R;;;O;;;LOJߢGX֔5JˋAAFFFd.X: +]֘_Hm333O >\% _e+iiiBku}߬A TߤNXݴ{< VY'(((U|jXV [Z*\RRRuk. [;;;X!bߧvc(((`u֛UcyV{O U{NR N`;;;`Kt֜RRR]']yMX;;;\!g[=  iiiXx[A<(((tv1[;;;W! 333VY 8h f+5RRRce% :nc6g֜O<;;;e(((h*FpFFFd6* giiifFK_ 3Up'#qF Xݴ{< Hߥt$ 5w֚豿6 bp/ [;;;X!]x5'm˓]#:]]]f\_333r<Cr[%FFF_y8Giiip]\;;;_,1}6+j |6!f;;;Y!buuusK ;;;P}]5#333Y][;;;X! 333W[b|Q[Q!euuuifFFFqsfVUiiipCM|{n= O~RRRf9Czn5Xݴ{< 3kzC'M[OQhpˌ T#dX[;;;X!Y^[ݴ}A$333]\\;;;[$*;;;`\.wj,;;;e333U[;;;X!d޴|< 3hg[;;;X!\333c(9e c* 333V[[;;;X!#333[\ @{|RRRc1Gߣiiivuuue6  9** f]]]b 8FFFgHFFFI*Xݴ{< (FFFI 9OT@! `RRRs:[;;;X!XYXݴ{= 333XZ[;;;X!!333Y\(]]]^w, >;;;s \\;;;[% $nz62xp#`;;;X![;;;X!AkuM 333W_\333c,8333n\# f]]]]+333eRRRs贛`O~RRRs@Kߥx= +pc#Jiiik2Xݴ{< 1]]]kH  [ FFFc=[;;;X!XYXݴz< 333WZ[;;;X! 333W[Xd9AyR_333wJ2Uiiipuuu_*%RRR\]]]rR,@;;;[![;;;W!*CUFFFZ 333Yt3YuuuqX@p]Hߥy= XqNRRRl@ @uuug֝(((l5 ,n `O( +}֚F Xݴ{< F֙}+RaGk\,U;;;RRSOݴw6 333RUU;;;Q;;;PT@֖ߥ333rFFFriiia2dFFFsrRRt q333rߨ;;;`!U;;;QRRRRj(((O;;;U]]]xRRRVFFF3Huuur ~ߧY*(((q o# 6uuulO*in$*FFFZiOv]!({D6w(((;;;d;;;T;;;>!YVXݴ{< U[<=(Y2 =333=<< 8ﻧd'(((===(((?333<=NnRRRa>fˌ\%'_ߣ;;;`!=(((?;;;BRRRZ/I^%_@QHRFFFT*@֕J 2c*3 j* =ph! 9, F\Xݴ{< Y޴H *55, *,' ,,  +8R][O1`333v]T>  !AU\n;;;\!,  'J\\N.#HWJ, %GW[[Q<!=: 1'58<@%  *A9bz@ Gbf`\V> =vbXݴ{< _ݴ}>   \;;;b5  +j;;;Y!        ?;;;>uuuGS RFFF9=ݿx+;;;8R+ ,!  %2# #AA$ 2+ $AA#          '<JMC/   #>< !ATL11OZQ6 %M333`ˌuuub[22KWYQE / X3 %]ߣ~1iiiZs3  /gːRRRa@ '_y֏#>ˊuuu\.V;;;X4Q 7( fiiivh< ,aiiiB  .諧t333(((O !RRRIuuueG 9kuuunV*^iii{kN]]]>% ORRRmˎߦ d( 6NN3J}2 /`{@/XxRRRux<\Yb˕k/ 'RRR[]]]t{|豎tk}G2P;;;VWVclގiD[ݴ|>Ciii}rN@FFFbg58FFFl(((y9 j~U  9t}kiY333_DNiiikW.iii\˖f2 #8vާ].! \]]]{h5 6guy333a,5x֟@%sR Dߤp;;;cs/.9(1333`\K ߫ߪM#FFFBRRRURRRcFFF;;;RRR^RRRR;;;=cyV{O U{NR Nk_ 9ˌˌJG֙n333fk##1$.;;;[\T333FFF{R -*_333r<Cr[%FFF_y8Giiip]P;;;Z 0 Fߤniiib2 >x333hFFFqH5U@C333m\ C߭(((ˎ@ @VkdT:\;;;[$*;;;`\.wj,;;;e333UGuuusx/ >ߣTvO/k (((YiR# ](((333333W$(j޴O [;;;X!!333Y\(]]]^w, >;;;s \N\(FFF]{˔[` ˍFFFxk9  %J]Q:>YeeU. Yݴz= [;;;X! 333W[Xd9AyRcuuuxD1(% AoM 9i֜RRRlRRRj|FFF\ ((  1g|Q( U;;;Q;;;PT@֖ߥ333rFFFriiia2(((^d333aFFFXa'Jr;;;r2HsRRRw֛h+ 333=;;;Q;;;^333y{333p;;;Z;;;P(((<=(((?333<=NnRRRa>iiiL. JiiipuC;;;cu], +*,  +8R][O1 *M[YY\\R< (Yy f.M Y(((V(((V WU:>V\]__]\T< #FFF:K]FFFS  5֛R.[^3 2QYO2  doomsday-stable-1.15.7/doomsday/client/doxygen.css0000664000175000017500000005321412641367670021465 0ustar jaakkojaakko/* The standard CSS for doxygen */ body, table, div, p, dl { font: 400 14px/19px Roboto,sans-serif; } /* @group Heading Levels */ h1.groupheader { font-size: 150%; } .title { font-size: 150%; font-weight: bold; margin: 10px 2px; } h2.groupheader { border-bottom: 1px solid #879ECB; color: #354C7B; font-size: 150%; font-weight: normal; margin-top: 1.75em; padding-top: 8px; padding-bottom: 4px; width: 100%; } h3.groupheader { font-size: 100%; } h1, h2, h3, h4, h5, h6 { -webkit-transition: text-shadow 0.5s linear; -moz-transition: text-shadow 0.5s linear; -ms-transition: text-shadow 0.5s linear; -o-transition: text-shadow 0.5s linear; transition: text-shadow 0.5s linear; margin-right: 15px; } h1.glow, h2.glow, h3.glow, h4.glow, h5.glow, h6.glow { text-shadow: 0 0 15px cyan; } dt { font-weight: bold; } div.multicol { -moz-column-gap: 1em; -webkit-column-gap: 1em; -moz-column-count: 3; -webkit-column-count: 3; } p.startli, p.startdd, p.starttd { margin-top: 2px; } p.endli { margin-bottom: 0px; } p.enddd { margin-bottom: 4px; } p.endtd { margin-bottom: 2px; } /* @end */ caption { font-weight: bold; } span.legend { font-size: 70%; text-align: center; } h3.version { font-size: 90%; text-align: center; } div.qindex, div.navtab{ background-color: #EBEFF6; border: 1px solid #A3B4D7; text-align: center; } div.qindex, div.navpath { width: 100%; line-height: 140%; } div.navtab { margin-right: 15px; } /* @group Link Styling */ a { color: #3D578C; font-weight: normal; text-decoration: none; } .contents a:visited { color: #4665A2; } a:hover { text-decoration: underline; } a.qindex { font-weight: bold; } a.qindexHL { font-weight: bold; background-color: #9CAFD4; color: #ffffff; border: 1px double #869DCA; } .contents a.qindexHL:visited { color: #ffffff; } a.el { font-weight: bold; } a.elRef { } a.code, a.code:visited { color: #4665A2; } a.codeRef, a.codeRef:visited { color: #4665A2; } /* @end */ dl.el { margin-left: -1cm; } pre.fragment { border: 1px solid #C4CFE5; background-color: #FBFCFD; padding: 4px 6px; margin: 4px 8px 4px 2px; overflow: auto; word-wrap: break-word; font-size: 9pt; line-height: 125%; font-family: monospace, fixed; font-size: 105%; } div.fragment { padding: 4px; margin: 4px; background-color: #FBFCFD; border: 1px solid #C4CFE5; } div.line { font-family: monospace, fixed; font-size: 13px; min-height: 13px; line-height: 1.0; text-wrap: unrestricted; white-space: -moz-pre-wrap; /* Moz */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ white-space: pre-wrap; /* CSS3 */ word-wrap: break-word; /* IE 5.5+ */ text-indent: -53px; padding-left: 53px; padding-bottom: 0px; margin: 0px; -webkit-transition-property: background-color, box-shadow; -webkit-transition-duration: 0.5s; -moz-transition-property: background-color, box-shadow; -moz-transition-duration: 0.5s; -ms-transition-property: background-color, box-shadow; -ms-transition-duration: 0.5s; -o-transition-property: background-color, box-shadow; -o-transition-duration: 0.5s; transition-property: background-color, box-shadow; transition-duration: 0.5s; } div.line.glow { background-color: cyan; box-shadow: 0 0 10px cyan; } span.lineno { padding-right: 4px; text-align: right; border-right: 2px solid #0F0; background-color: #E8E8E8; white-space: pre; } span.lineno a { background-color: #D8D8D8; } span.lineno a:hover { background-color: #C8C8C8; } div.ah { background-color: black; font-weight: bold; color: #ffffff; margin-bottom: 3px; margin-top: 3px; padding: 0.2em; border: solid thin #333; border-radius: 0.5em; -webkit-border-radius: .5em; -moz-border-radius: .5em; box-shadow: 2px 2px 3px #999; -webkit-box-shadow: 2px 2px 3px #999; -moz-box-shadow: rgba(0, 0, 0, 0.15) 2px 2px 2px; background-image: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#000),color-stop(0.3, #444)); background-image: -moz-linear-gradient(center top, #eee 0%, #444 40%, #000); } div.groupHeader { margin-left: 16px; margin-top: 12px; font-weight: bold; } div.groupText { margin-left: 16px; font-style: italic; } body { background-color: white; color: black; margin: 0; } div.contents { margin-top: 10px; margin-left: 12px; margin-right: 8px; } td.indexkey { background-color: #EBEFF6; font-weight: bold; border: 1px solid #C4CFE5; margin: 2px 0px 2px 0; padding: 2px 10px; white-space: nowrap; vertical-align: top; } td.indexvalue { background-color: #EBEFF6; border: 1px solid #C4CFE5; padding: 2px 10px; margin: 2px 0px; } tr.memlist { background-color: #EEF1F7; } p.formulaDsp { text-align: center; } img.formulaDsp { } img.formulaInl { vertical-align: middle; } div.center { text-align: center; margin-top: 0px; margin-bottom: 0px; padding: 0px; } div.center img { border: 0px; } address.footer { text-align: right; padding-right: 12px; } img.footer { border: 0px; vertical-align: middle; } /* @group Code Colorization */ span.keyword { color: #008000 } span.keywordtype { color: #604020 } span.keywordflow { color: #e08000 } span.comment { color: #800000 } span.preprocessor { color: #806020 } span.stringliteral { color: #002080 } span.charliteral { color: #008080 } span.vhdldigit { color: #ff00ff } span.vhdlchar { color: #000000 } span.vhdlkeyword { color: #700070 } span.vhdllogic { color: #ff0000 } blockquote { background-color: #F7F8FB; border-left: 2px solid #9CAFD4; margin: 0 24px 0 4px; padding: 0 12px 0 16px; } /* @end */ /* .search { color: #003399; font-weight: bold; } form.search { margin-bottom: 0px; margin-top: 0px; } input.search { font-size: 75%; color: #000080; font-weight: normal; background-color: #e8eef2; } */ td.tiny { font-size: 75%; } .dirtab { padding: 4px; border-collapse: collapse; border: 1px solid #A3B4D7; } th.dirtab { background: #EBEFF6; font-weight: bold; } hr { height: 0px; border: none; border-top: 1px solid #4A6AAA; } hr.footer { height: 1px; } /* @group Member Descriptions */ table.memberdecls { border-spacing: 0px; padding: 0px; } .memberdecls td, .fieldtable tr { -webkit-transition-property: background-color, box-shadow; -webkit-transition-duration: 0.5s; -moz-transition-property: background-color, box-shadow; -moz-transition-duration: 0.5s; -ms-transition-property: background-color, box-shadow; -ms-transition-duration: 0.5s; -o-transition-property: background-color, box-shadow; -o-transition-duration: 0.5s; transition-property: background-color, box-shadow; transition-duration: 0.5s; } .memberdecls td.glow, .fieldtable tr.glow { background-color: cyan; box-shadow: 0 0 15px cyan; } .mdescLeft, .mdescRight, .memItemLeft, .memItemRight, .memTemplItemLeft, .memTemplItemRight, .memTemplParams { background-color: #F9FAFC; border: none; margin: 4px; padding: 1px 0 0 8px; } .mdescLeft, .mdescRight { padding: 0px 8px 4px 8px; color: #555; } .memSeparator { border-bottom: 1px solid #DEE4F0; line-height: 1px; margin: 0px; padding: 0px; } .memItemLeft, .memTemplItemLeft { white-space: nowrap; } .memItemRight { width: 100%; } .memTemplParams { color: #4665A2; white-space: nowrap; font-size: 80%; } /* @end */ /* @group Member Details */ /* Styles for detailed member documentation */ .memtemplate { font-size: 80%; color: #4665A2; font-weight: normal; margin-left: 9px; } .memnav { background-color: #EBEFF6; border: 1px solid #A3B4D7; text-align: center; margin: 2px; margin-right: 15px; padding: 2px; } .mempage { width: 100%; } .memitem { padding: 0; margin-bottom: 10px; margin-right: 5px; -webkit-transition: box-shadow 0.5s linear; -moz-transition: box-shadow 0.5s linear; -ms-transition: box-shadow 0.5s linear; -o-transition: box-shadow 0.5s linear; transition: box-shadow 0.5s linear; display: table !important; width: 100%; } .memitem.glow { box-shadow: 0 0 15px cyan; } .memname { font-weight: bold; margin-left: 6px; } .memname td { vertical-align: bottom; } dl.reflist dt { border-top: 1px solid #A8B8D9; border-left: 1px solid #A8B8D9; border-right: 1px solid #A8B8D9; padding: 6px 0px 6px 0px; color: #253555; font-weight: bold; text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.9); background-image:url('nav_f.png'); background-repeat:repeat-x; background-color: #E2E8F2; /* opera specific markup */ box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15); border-top-right-radius: 4px; border-top-left-radius: 4px; /* firefox specific markup */ -moz-box-shadow: rgba(0, 0, 0, 0.15) 5px 5px 5px; -moz-border-radius-topright: 4px; -moz-border-radius-topleft: 4px; /* webkit specific markup */ -webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15); -webkit-border-top-right-radius: 4px; -webkit-border-top-left-radius: 4px; } .memproto { border: 1px solid #A8B8D9; padding: 6px 0px 6px 0px; color: #253555; font-weight: bold; text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.9); background-image:url('nav_f.png'); background-repeat:repeat-x; background-color: #E2E8F2; /* opera specific markup */ box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15); border-top-right-radius: 4px; border-top-left-radius: 4px; /* firefox specific markup */ -moz-box-shadow: rgba(0, 0, 0, 0.15) 5px 5px 5px; -moz-border-radius-topright: 4px; -moz-border-radius-topleft: 4px; /* webkit specific markup */ -webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15); -webkit-border-top-right-radius: 4px; -webkit-border-top-left-radius: 4px; } dl.reflist dd { border-bottom: 1px solid #A8B8D9; border-left: 1px solid #A8B8D9; border-right: 1px solid #A8B8D9; padding: 6px 10px 2px 10px; background-color: #FBFCFD; border-top-width: 0; background-image:url('nav_g.png'); background-repeat:repeat-x; background-color: #FFFFFF; /* opera specific markup */ border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15); /* firefox specific markup */ -moz-border-radius-bottomleft: 4px; -moz-border-radius-bottomright: 4px; -moz-box-shadow: rgba(0, 0, 0, 0.15) 5px 5px 5px; /* webkit specific markup */ -webkit-border-bottom-left-radius: 4px; -webkit-border-bottom-right-radius: 4px; -webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15); } div.textblock { max-width: 900px; } .memdoc { max-width: 900px; /* border-bottom: 1px solid #A8B8D9; border-left: 1px solid #A8B8D9; border-right: 1px solid #A8B8D9; */ padding-left: 2em; padding-top: 1ex; padding-bottom: 1em; /*background-color: #FBFCFD;*/ border-top-width: 0; /* background-image:url('nav_g.png'); background-repeat:repeat-x;*/ background-color: transparent; /* opera specific markup */ border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; /*box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15);*/ /* firefox specific markup */ -moz-border-radius-bottomleft: 4px; -moz-border-radius-bottomright: 4px; /*-moz-box-shadow: rgba(0, 0, 0, 0.15) 5px 5px 5px;*/ /* webkit specific markup */ -webkit-border-bottom-left-radius: 4px; -webkit-border-bottom-right-radius: 4px; /*-webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.15);*/ } dl.reflist dt { padding: 5px; } dl.reflist dd { margin: 0px 0px 10px 0px; padding: 5px; } .paramkey { text-align: right; } .paramtype { white-space: nowrap; } .paramname { color: #602020; white-space: nowrap; } .paramname em { font-style: normal; } .paramname code { line-height: 14px; } .params, .retval, .exception, .tparams { margin-left: 0px; padding-left: 0px; } .params .paramname, .retval .paramname { font-weight: bold; vertical-align: top; } .params .paramtype { font-style: italic; vertical-align: top; } .params .paramdir { font-family: "courier new",courier,monospace; vertical-align: top; } table.mlabels { border-spacing: 0px; } td.mlabels-left { width: 100%; padding: 0px; } td.mlabels-right { vertical-align: bottom; padding: 0px; white-space: nowrap; } span.mlabels { margin-left: 8px; } span.mlabel { background-color: #728DC1; border-top:1px solid #5373B4; border-left:1px solid #5373B4; border-right:1px solid #C4CFE5; border-bottom:1px solid #C4CFE5; text-shadow: none; color: white; margin-right: 4px; padding: 2px 3px; border-radius: 3px; font-size: 7pt; white-space: nowrap; vertical-align: middle; } /* @end */ /* these are for tree view when not used as main index */ div.directory { margin: 10px 0px; border-top: 1px solid #A8B8D9; border-bottom: 1px solid #A8B8D9; width: 100%; } .directory table { border-collapse:collapse; } .directory td { margin: 0px; padding: 0px; vertical-align: top; } .directory td.entry { white-space: nowrap; padding-right: 6px; } .directory td.entry a { outline:none; } .directory td.entry a img { border: none; } .directory td.desc { width: 100%; padding-left: 6px; padding-right: 6px; padding-top: 3px; border-left: 1px solid rgba(0,0,0,0.05); } .directory tr.even { padding-left: 6px; background-color: #F7F8FB; } .directory img { vertical-align: -30%; } .directory .levels { white-space: nowrap; width: 100%; text-align: right; font-size: 9pt; } .directory .levels span { cursor: pointer; padding-left: 2px; padding-right: 2px; color: #3D578C; } div.dynheader { margin-top: 8px; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } address { font-style: normal; color: #2A3D61; } table.doxtable { border-collapse:collapse; margin-top: 4px; margin-bottom: 4px; } table.doxtable td, table.doxtable th { border: 1px solid #2D4068; padding: 3px 7px 2px; } table.doxtable th { background-color: #374F7F; color: #FFFFFF; font-size: 110%; padding-bottom: 4px; padding-top: 5px; } table.fieldtable { width: 100%; margin-bottom: 10px; border: 1px solid #A8B8D9; border-spacing: 0px; -moz-border-radius: 4px; -webkit-border-radius: 4px; border-radius: 4px; -moz-box-shadow: rgba(0, 0, 0, 0.15) 2px 2px 2px; -webkit-box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.15); box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.15); } .fieldtable td, .fieldtable th { padding: 3px 7px 2px; } .fieldtable td.fieldtype, .fieldtable td.fieldname { white-space: nowrap; border-right: 1px solid #A8B8D9; border-bottom: 1px solid #A8B8D9; vertical-align: top; } .fieldtable td.fielddoc { border-bottom: 1px solid #A8B8D9; width: 100%; } .fieldtable tr:last-child td { border-bottom: none; } .fieldtable th { background-image:url('nav_f.png'); background-repeat:repeat-x; background-color: #E2E8F2; font-size: 90%; color: #253555; padding-bottom: 4px; padding-top: 5px; text-align:left; -moz-border-radius-topleft: 4px; -moz-border-radius-topright: 4px; -webkit-border-top-left-radius: 4px; -webkit-border-top-right-radius: 4px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom: 1px solid #A8B8D9; } .tabsearch { top: 0px; left: 10px; height: 36px; background-image: url('tab_b.png'); z-index: 101; overflow: hidden; font-size: 13px; } .navpath ul { font-size: 11px; background-image:url('tab_b.png'); background-repeat:repeat-x; background-position: 0 -5px; height:30px; line-height:30px; color:#8AA0CC; border:solid 1px #C2CDE4; overflow:hidden; margin:0px; padding:0px; } .navpath li { list-style-type:none; float:left; padding-left:10px; padding-right:15px; background-image:url('bc_s.png'); background-repeat:no-repeat; background-position:right; color:#364D7C; } .navpath li.navelem a { height:32px; display:block; text-decoration: none; outline: none; color: #283A5D; font-family: 'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.9); text-decoration: none; } .navpath li.navelem a:hover { color:#6884BD; } .navpath li.footer { list-style-type:none; float:right; padding-left:10px; padding-right:15px; background-image:none; background-repeat:no-repeat; background-position:right; color:#364D7C; font-size: 8pt; } div.summary { float: right; font-size: 8pt; padding-right: 5px; width: 50%; text-align: right; } div.summary a { white-space: nowrap; } div.ingroups { font-size: 8pt; width: 50%; text-align: left; margin-top: 1ex; } div.ingroups a { white-space: nowrap; } div.header { background-image:url('nav_h.png'); background-repeat:repeat-x; background-color: #F9FAFC; margin: 0px; border-bottom: 1px solid #C4CFE5; } div.headertitle { padding: 5px 5px 5px 10px; } dl { padding: 0 0 0 10px; } /* dl.note, dl.warning, dl.attention, dl.pre, dl.post, dl.invariant, dl.deprecated, dl.todo, dl.test, dl.bug */ dl.section { margin-left: 0px; padding-left: 0px; } dl.note { margin-left:-7px; padding-left: 3px; border-left:4px solid; border-color: #D0C000; } dl.warning, dl.attention { margin-left:-7px; padding-left: 3px; border-left:4px solid; border-color: #FF0000; } dl.pre, dl.post, dl.invariant { margin-left:-7px; padding-left: 3px; border-left:4px solid; border-color: #00D000; } dl.deprecated { margin-left:-7px; padding-left: 3px; border-left:4px solid; border-color: #505050; } dl.todo { margin-left:-7px; padding-left: 3px; border-left:4px solid; border-color: #00C0E0; } dl.test { margin-left:-7px; padding-left: 3px; border-left:4px solid; border-color: #3030E0; } dl.bug { margin-left:-7px; padding-left: 3px; border-left:4px solid; border-color: #C08050; } dl.section dd { margin-bottom: 6px; } #projectlogo { text-align: center; vertical-align: bottom; border-collapse: separate; } #projectlogo img { border: 0px none; } #projectname { font: 300% Tahoma, Arial,sans-serif; margin: 0px; padding: 2px 0px; } #projectbrief { font: 120% Tahoma, Arial,sans-serif; margin: 0px; padding: 0px; } #projectnumber { font: 50% Tahoma, Arial,sans-serif; margin: 0px; padding: 0px; } #titlearea { padding: 0px; margin: 0px; width: 100%; border-bottom: 1px solid #5373B4; } .image { text-align: center; } .dotgraph { text-align: center; } .mscgraph { text-align: center; } .caption { font-weight: bold; } div.zoom { border: 1px solid #90A5CE; } dl.citelist { margin-bottom:50px; } dl.citelist dt { color:#334975; float:left; font-weight:bold; margin-right:10px; padding:5px; } dl.citelist dd { margin:2px 0; padding:5px 0; } div.toc { padding: 14px 25px; background-color: #F4F6FA; border: 1px solid #D8DFEE; border-radius: 7px 7px 7px 7px; float: right; height: auto; margin: 0 20px 10px 10px; width: 200px; } div.toc li { background: url("bdwn.png") no-repeat scroll 0 5px transparent; font: 10px/1.2 Verdana,DejaVu Sans,Geneva,sans-serif; margin-top: 5px; padding-left: 10px; padding-top: 2px; } div.toc h3 { font: bold 12px/1.2 Arial,FreeSans,sans-serif; color: #4665A2; border-bottom: 0 none; margin: 0; } div.toc ul { list-style: none outside none; border: medium none; padding: 0px; } div.toc li.level1 { margin-left: 0px; } div.toc li.level2 { margin-left: 15px; } div.toc li.level3 { margin-left: 30px; } div.toc li.level4 { margin-left: 45px; } .inherit_header { font-weight: bold; color: gray; cursor: pointer; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .inherit_header td { padding: 6px 0px 2px 5px; } .inherit { display: none; } tr.heading h2 { margin-top: 12px; margin-bottom: 4px; } @media print { #top { display: none; } #side-nav { display: none; } #nav-path { display: none; } body { overflow:visible; } h1, h2, h3, h4, h5, h6 { page-break-after: avoid; } .summary { display: none; } .memitem { page-break-inside: avoid; } #doc-content { margin-left:0 !important; height:auto !important; width:auto !important; overflow:inherit; display:inline; } } doomsday-stable-1.15.7/doomsday/client/doc/0000775000175000017500000000000012641367670020036 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/doc/LICENSE0000664000175000017500000003545112641367670021053 0ustar jaakkojaakko GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS doomsday-stable-1.15.7/doomsday/client/doc/changelogs/0000775000175000017500000000000012641367670022150 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/doc/changelogs/dhistory.txt0000664000175000017500000007530312641367670024566 0ustar jaakkojaakko Doomsday Engine Version History =============================== Legend: + added, - fixed, * improved/changed Version 1.7.7 ------------- + switch texture pairs (SW1/SW2) are precached at the same time + "Generator:Stage:Radius rnd": Randomness of particle radius + sound-info shows sound names as well as ID numbers + support for textured particles * flat particles stick to planes - high-resolution flats with 4 channels (alpha) were loaded incorrectly - sector lightlevel overflow in sectordelta (e.g. on jHexen map 26) - alternative texture path must be tried first when loading hires images (-texdir2) Version 1.7.6 ------------- + cvar rend-hud-fov-shift: HUD weapon model moved down if FOV > 90, defaults to 45 + dsCompat: DirectSound 6 sound driver, tries to be as compatible as possible + -csd: use dsCompat.dll as the sound driver + "Generator:Min spawn radius": minimum distance from the generator source for new particles, a nonzero value uses a more sophisticated algorithm that chooses the particle's coordinates somewhere from the surface of a sphere + "Generator:Damage mobj" creates a damage-triggered generator + ptf_force (stage flag 0x100): particle is affected by a sphere force, which attracts the particle toward the surface of a sphere while also rotating around an arbitrary axis + "Generator:Force axis": a 3D vector around which sphere force rotates particles, also sets the strength of the rotation + "Generator:Force radius": radius for sphere force + "Generator:Force": strength of the radial force that pushes or pulls particles towards the surface of the sphere + ptf_hflat (stage flag 0x10): render particle touching a plane as flat + ptf_vflat (stage flag 0x80): render particle touching a wall as flat + ptf_flat: horiz and vert flags combined + ptf_stagevtouch, ptf_stagehtouch (stage flags 0x20, 0x40): wall/flat- specific stagetouch for particles + gnf_scalerate: use damage as a factor for damage ptcgen spawn rate + df_idangle: random fixed angle offset for a model * EAX errors ignored after first report (if property unsupported) * non-moving particles contacting a wall (assumed stuck) are destroyed if the wall becomes an opening * -noeax recognized by dsCompat * smaller button steps for panel controls * sound buffer loaded before setting properties (needed by dsCompat) - absolute demo paths aren't prefixed with "demo\" - mixer is more careful about a failed init (div-by-zero was possible) - camera filter (pain/bonus) cleared at level exit - win32 CD player didn't stop track at shutdown - flat precacher used wrong indices; some raw screens (Heretic's FINAL*) were displayed as garbage Version 1.7.5 ------------- + console command "init-(levelid)" executed after level setup; for example "init-e2m3" could be aliased to do something useful + ccmd write: saves console variables, bindings and aliases to a file + -nopsf: don't set the primary sound buffer format + "Model:Sub:Frame range" added, works with df_idframe * model loader will automatically load a DMD if it exists, even if def uses MD2 * memory zone no longer used in texture processing (malloc only) * MIDI volume: channel volumes not set separately, more compatible * sounds stop playing based on time, not buffer position * -verbose (-v) prints all command line options to Doomsday.out * game window is not forced to be the topmost window - client didn't play sounds without an origin - -noeax option caused a crash at startup - FMOD: MIDI/etc music looping fixed, again - fixed a crash when lights, halos and lit sprites were all off - fixed: game window must not be shown in dedicated mode - crashing at startup was likely when lots of models/skins were missing - a minor correction to data\cphelp.txt Version 1.7.4 ------------- + minimizing possible in windowed mode + identification number always printed for loaded IWAD - Direct3D: depth buffer mode verified to work correctly - mouse wheel was confused with mouse buttons 5 and 6 - Control Panel yes/no buttons didn't reflect changes to cvars - dpDehRead: bad memory allocation led to heap corruption at shutdown - FMOD: MIDI/etc music looping fixed - fixed a rare potential crashing bug in the clipper (C_AddRange) Version 1.7.3 ------------- + startup message window + support for up to six mouse buttons + support for rotational joystick axes and two sliders + each joystick axis can be inverted (input-joy-*-inverse) + all textures in an animation sequence are precached at the same time + popup help window for (some) Control Panel options + ui-panel-help, ui-panel-tips * GL not inited until engine init is complete (no more -fullstart) * game window hidden until GL init begins * FMOD v3.6 * mixer used to set MIDI volume * Doomsday handles the deadzone of the joystick * brighter background for options frame in Control Panel - halos weren't affected by object movement smoothing - music was played even if music volume was set to zero - EAX initialization was sloppy - FMOD/CD: original volume restored correctly at shutdown - dedicated mode text console problems on WinXP (presumably Win2k, too) - OpenGL: mipmapped paletted textures had palette problems Version 1.7.2 ------------- + A3D support with the -a3d option (dsA3D.dll; also needs A3DAPI.dll) + -oal option for OpenAL support + -vtxar option: make the OpenGL renderer use vertex buffers + -anifilter option: use anisotropic texture filtering + -diag option: print lots of debug info about Direct3D * OpenGL renderer defaults to not using vertex buffers * slightly larger Z bias for dynamic light polys * detail textures can be PCX images * model skin dimensions are allowed to differ from what models expect * sounds are uncached after one minute of not being played * sounds prefer channels already loaded with the same sample - texture height queries weren't returned in fixed point Version 1.7.1 ------------- * multiple -command/-cmd options are allowed * console command line cleared when console closed * DED loader reallocates memory fewer times, faster performance - music def loading: later defs didn't patch earlier ones - WAV loader: confused all chunk lengths, loading failed - WAV loader: header read incorrectly, loading failed - if music volume was set to zero, it wasn't updated at startup and music played with full volume - -bpp was set after first GL init, caused WGL to choke on the second time (error 2000) - Direct3D renderer tried initing with bit depth zero, caused D3DERR_INVALIDCALL Version 1.7.0 ------------- * engine source code reorganized * games, renderers and plugins all use the same API to access engine routines + keymaps (DKM files in the Data\KeyMaps\ directory); default.dkm and fi.dkm included in the release * multiple -file options allowed + -iwad <=> -file * particle generators can be triggered by the map ID (Center = origin XYZ) + linelinks * safe P_(Un)LinkMobj * enhanced mobj/line/sector iterators - minor bugfixes Sound: + revised sound system; now completely handled by the engine + new sound flags (Audio.ded): sf_shift, sf_shift2, sf_repeat, sf_nodist, sf_exclude + sound exclusion groups (Sound:Group) + sounds can have external resources (mono PCM WAV, 8/16 bits, 11/22/44 KHz), using the Sound:Ext key + revised music system; completely handled by the engine, each interface (MUS/Ext/CD) can run a different driver, Win32 and FMOD implemented * no more A3D support Renderer: * interface between the rendering DLL and Doomsday revised (old rendering DLLs are now incompatible) * Direct3D renderer rewritten to use DirectX 8 * rendering DLL handles the creation of mipmaps * rendering lists moved into the engine, now independent of the rendering DLL * halos/flares don't read from the Z-buffer any more (source point clipped by geometry, resulting halo rendered sorted during DrawMasked) * halos can't be occluded by their source (yay!) * halos appear and disappear gradually (rend-halo-occlusion controls speed) + cvar rend-camera-smooth: filter camera movement between tics (removes FPS limit for camera movement) + DMD2 model format (MD2 with detail levels) + LOD rendering for DMD models + cvar rend-model-lod: distance to LOD level zero + mobj shadows (extremely simple, unclipped) + cvars rend-shadow, rend-shadow-darkness, rend-shadow-far, rend-shadow- radius-max + Model:Shadow radius (overrides automatic calculation) * changing the resolution shuts down and re-inits the renderer (will work with Direct3D, now) + clipper maintains information about planes that occlude visibility (occlusion ranges); -maxor command line option - clipper allows zero-length solid segments now, fixes a few clipping problems - fog is disabled while psprites are rendered Console: * new UI color scheme (blue/gray) + ccmd uicolor: change UI colors + Startup.cfg: startup script for configuring UI colors, the console font and keymap + Control Panel (quickly accessed with Shift-Esc) for easy configuration + Shift-Tilde (or Shift-[console activation key]) will toggle between fullscreen and halfscreen console modes + aliases are automatically saved to the default config file + horizontal rulers in the console + console text can be centered on a line * message produced by ccmd help updated Networking: + -server option: equals to "net init tcpip; net server start" right after startup + -connect option: after startup, connects to a server at the given IP address (e.g. "-connect 127.0.0.1") + ccmd connect: connects to a server at the specified IP address (synchronously; the connection is formed in startup mode) + the font console command can now change the console font (e.g. "font name fixed12"), can be done from startup.cfg/user.cfg * server does not halt while UI is active - clients don't expect to receive cmd info of remote players any more - clients update visible mobj angles and remote player selector - clients update player mobj states - visangles (rend-mobj-smooth-turn) won't be used in netgames or demos Input: * input code updated to work with DirectInput 8 Version 1.6.1 ------------- * model skins are precached by default (rend-model-precache) * precacher also binds all textures to make sure they'll be ready * one skin can be shared by many models (internal skin list) * when looking for a skin, the model's home directory is searched first (where the model is in) * reports of missing model files are summed up and shown only once * console line length adjusted to fit the game window (used to be 70) + rend-light-shrink: dynlight polys are shrunk horizontally so the case when they overlap and produce bright stray pixels is (mostly) gone + rend-particle-diffuse: if a particle is closer than the diffuse factor times its radius, it will receive extra alpha and disappear when close enough + rend-particle-visible-near: if a particle is closer than this, it won't be rendered at all + ccmd stopfinale: stops the currently playing interlude + view-bob-height: camera height bobbing factor (1=normal) + view-bob-weapon: player weapon bobbing factor (1=normal) - progress bar doesn't make the screen flicker any more with page flipping * clients remove mobjs automatically when mobjs reach the null state - fixed particle generation in demos/netgames Version 1.6.0 ------------- + definitions re-inited when (un)loading WADs + definitions can be read from WADs (DD_DEFNS lump, contains a DED file) + virtual files in WADs (translated using a DD_DIREC lump; use wadtool) + virtual files support: 3D models, image loaders (PNG, TGA, PCX), definitions, CFG files, DFN fonts * console variables renamed (old names work, too) * cvar con-completion: mode zero completes the unambiguous part * high-res textures and model skins can be PNG, TGA or PCX + color-keying for PNG, TGA, PCX ("filename-ck.png") + MD2 shiny skins (approximated cylindrical environment mapping) + "Shiny skin", "Shiny", "Shiny Color" added to Model\Md2 defn + "Color" added to Light defn ("Red", "Green", "Blue" combined) + "Finale" definitions + -texdir2: secondary hi-res texture directory (textures in the secondary directory override any textures in the normal hi-res texture directory) + -nopitch: disable sound effect pitch alterations + -nodop: disable 3D sounds' doppler effect * raw images (fullscreen 320x200 textures) deleted at level changes + client setup screen shows a warning about different IWADs + cvar client-pos-interval: number of tics between coordinate reports + cvar con-progress: progress indicator enabled/disabled + cvar rend-info-tris: (OpenGL) display rendering list triangle count - fixed sector lightlevel overflow (>255), caused black planes in bright sectors - sight blockmap traverser sometimes misses the target block, caused crashes in netgames (server) in certain maps - server was mixing up player deltas, fixed - negative player turndeltas became positive, fixed - server was not predicting clientside mobj animation, which prevented the server from correctly sending mobj state deltas - server destroyed a broadcasted chat message before it was relayed to everybody - client updated the wrong mobj when a new client joined; the new client was left invisible - absolute camera coordinates written in demos - very little plane movement speeds were truncated to zero - fixed a display gamma conflict with the D3D renderer - mobj-triggered particle generators restored after loading a saved game + KickStart: default profiles (KSS), option to run glBSP, quick resolutions list, updated options Version 1.5.5 ------------- + cvars "vid_Gamma", "vid_Contrast", "vid_Bright" + cmd "setvidramp": update display's hardware gamma ramp + Flags key added to the Light definition + lgf_nohalo (0x100): dynamic light is rendered with no halo (lens flare) + PNG support (using libpng) for high-resolution textures - df_autoscale was broken - masked textures are no longer clamped vertically - KickStart: fixed a problem with WAD paths - progress bar caused a div-by-zero when map had less than 10 sectors - footclipping is now done for models, too (standing in water) Version 1.5.4 ------------- + custom music formats detected (MP3, MOD, IT, XM, etc. can now be put in WAD files) + TGA images can be used to replace textures and flats (Data\*\Textures) + OpenGL: -texcomp option enables texture compression (OpenGL chooses the compression method) + df_selskin: model skin selection based on selector special byte + custom translucency can be set with three highest bits of the selector + cvars "net_MinSecUpdate", "net_FullSecUpdate", "net_MaxSecUpdate" * sector deltas cover a larger area (cvars "net_*SecUpdate") * EXT_texture_env_combine is accepted as well * KickStart: standard profiles are listed in a fixed order * internal handling of missing walls * 32 MB of RAM allocated by default - fixed -maxzone (-mem) - fixed the case where invis-plane hack created looped plane links (the "linked to self" error) - maximum number of rendering lists increased to 1024 - rendering lists cleared properly at level changes (not clearing leads to missing textures and messed up walls) - fixed sky fadeout color calculation (alpha was expected) - F_SKY1 was being bound repeatedly even if it's never used anywhere - reseting messed up detail textures (e.g. cmds "load", "unload") - dynamic lights weren't processed for subsectors that had no visible planes, even if the subsector had visible walls - dynamic light intensity diminishes gradually for lights that are on the "wrong" side of a floor/ceiling - dynamic lights on two-sided middle textures are now properly clipped - fixed a bug with animated textures - KickStart: -basedir is now quoted like other paths Version 1.5.3 ------------- ! BETA RELEASE + a simple plugin system (dp*.dll) + Dehacked reader plugin (dpDehRead.dll), reads .DEH files and DEHACKED lumps from loaded WADs + -bigmtex option: enlarge masked textures to fit patches that are too big (e.g. Aliens TC) + server can send lump name mappings to clients * better IWAD loading (IWAD always loaded first) * small changes in engine initialization * df_darkshadow affected by alpha (Model\Md2\Transparent) - relative paths with command line options fixed - clients sometimes displayed invalid plane textures (flats) - server/client crashed when psprite->state == NULL - sky fadeout color was incorrect in the first map that was played Version 1.5.2 ------------- ! BETA RELEASE + particle generators: "Generator" definition, vars: UseParticles, MaxParticles, PtcSpawnRate + detail textures: "Detail" definition, "detail" console command + OpenGL support for detail texture rendering (ARB_texture_env_combine) + "Light" definitions: customize dynamic lighting for specific States + CD-support for Music definitions (e.g. Ext = "cd:9";) with FMod + "IncludeIf", "ModelPath" DED directives + mobj selectors * directory structure reorganized (Doc\DSS.txt) * Short-Range Visual Offsets: smooth actor movement and turning, vars: r_UseSRVO and r_UseVisAngle * 3D models associated with States instead of Sprite/Frame pairs * "Model" definition restructured (no more Frames), see Doc\DEDDoc.txt * rewritten command line handling (see Doc\CmdLine.txt) * server sends mobj states instead of sprites and frames * "UseModels" can be used to disable specific model groups * server & client setup screens have better default buttons - sprite lumps must reside inside a S_START/S_END block or the sprite loader will ignore them (used to be a bit too liberal!) - model interpolation in netgames - A3D support should be working again - minor bugs Version 1.05 (= 1.5.0) ------------ * netframes are deltas * client state is persistent between frames - when there are duplicate model definitions, the last one will be used + OpenGL: support for GL_EXT_paletted_texture (-paltex) + 'setres': change display mode resolution or window size Version 1.04 ------------ + MIDI files embedded in WADs can be played (not looped) + ccmds 'login' and 'logout': remote access to the server + 'net_Password': password for remote access with 'login' * if DirectInput 5 init fails, DirectInput 3 is tried instead + DEDMan 1.13: DEH: initial player health, initial bullets, BFG cells/shot * new version of fmod.dll Version 1.03 ------------ + display warning when no IWAD has been loaded + inter-frames (Inter in Model definition) * UI uses Small Fonts in low-res modes - translated sprites had invalid texture coordinates (all zero) + DEDMan 1.11: "Model/Inter", skin ranges, "Model/Skin tics", won't write "Line Type/Ap9" twice - jtNet2: tried to access null pointer - DLaunch 1.04: no more directory mixups Version 1.02 ------------ + 'settics': set the number of game tics per second + 'repeat': repeats a command at given intervals + 'r_NoSpriteZ': disable Z writes for sprites + 'net_DontSleep': don't sleep while waiting for tics (redraw screen instead) + 'net_FrameInterval': minimum number of tics between sent frames + external music files (played with FMod: http://fmod.org/, which supports MOD, S3M, XM, IT, MID, RMI, SGT (DirectMusic segment), RAW, WAV, MP2, MP3, OGG, WMA and ASF) + 'playext': play a music file + DED Manager 1.1: (slightly limited) DeHackEd support + Line Type and Sector Type definitions in DED files (v3) + Music definitions can have a "File name" property + 'ConsoleKey': keycode of the console activation key (default is 96) + 'ConsoleShowKeys': show keycodes of all pressed keys in the console + -renorm option: recalculate all MD2 vertex normals when loading, takes some time but fixes models with incorrect normals + colored sector lighting + new model flags: df_movyaw (0x100) aligns model yaw angle with movement, df_nointerpol (0x200) disables interpolation for the frame, df_brightshadow2 (0x400) renders with additive blending and alpha=1, df_alignyaw (0x800) aligns model yaw angle with camera, df_alignpitch (0x1000) aligns model pitch angle with camera * server version incremented to 4 (old demos incompatible) * client interpolates demo view angles * flats (floor/ceiling textures) handling revised, now it's possible to use any loaded lump as a flat * size, color and Y offset of dynamic lights and flares are determined by analyzing the source sprite (adjust with r_dlMaxRad and r_dlRadFactor) * psprite models drawn differently, now won't go inside walls * sprites won't be stretched to fit whole texture, makes them sharper * model spin gets an offset based on thing type - client moves mobjs within sector when sector height changes - midtexture placement for segs with a missing toptexture - joystick init stopped at failed axis range setup - OpenGL: didn't make sure projected points were on the screen - sky mask wasn't applied to walls with zero-height backsector - there was a floorclip related bug in client's mobj unpacker Version 1.01a ------------- * large textures are allowed to have one alpha pixel (ZZZFACE3!) - midtexture placement in sectors that have an invisible floor - multi-pass skyfix Version 1.01 ------------ + when a 2-sided line's upper texture is missing, the middle texture (if present) is extended to full height + plane linking: for sectors whose all lines' both sidedefs point to self, planes are linked to surrounding sector + 'FlareZOffset': lens flare Z offset multiplier, greater values move flares nearer the camera (in Z buffer), also affects flare size + DED v2: a Values block for free-form string definitions + player sprites (HUD weapons) can be replaced with models + 'r_WeaponOffsetScale': multiplier for psprite (x,y) offsets with models * when a sector is missing lower textures for all its lines, the floor of the sector is drawn at the height of the surrounding sector's floor * masked polygons (sprites, masked walls, models) always drawn with Z writes enabled * Z bias of drOpenGL and drD3D slightly increased * WAD loading messages are a bit clearer - -paltex uploaded MD2 skins in the wrong format - newline no longer required after last line in .cfg files - if .GWA doesn't exist W_AddFile() isn't called for it - Y offset of middle textures of 2-sided lines was incorrect in some cases Version 1.0 ----------- + all new network code (complete client/server) + dedicated server (runs in a text mode console) + MD2 support (replace specific frames of sprites) + MD2s lit with dynamic lights + frame interpolation for MD2s + dynamic light blending mode zero (multiply) + configuration dialog for D3D renderer (driver, color/texture/Z depth) + new vertices are inserted in wall segments if needed, reduces stray pixels + subsector planes rendered as a triangle fan around midpoint, if necessary + Doomsday Engine Definition (DED) files + DED files can Include with wildcards (checks for recursion) + definitions in DEDs are mostly patchable (same ID overwrites old def) + Map Info definitions (map name, author, fog, gravity, sky, etc.) + Map Info with ID '*' will be used for maps that do not have Map Info + command-line option -out redirects output to a given file (Doomsday.out by default) + command-line option -defs specifies the main definition file + mouse-driven user interface routines + network service provider/protocol and server setup using graphical UI + client setup and connecting to servers using graphical UI + background for startup messages + startup messages rendered with shadows + console text rendered with shadows + 'ModelMaxZ': farther than this things are always rendered as sprites + copies of P_XYMovement and P_ZMovement used for client-side mov prediction + Pause key can be used in bindings (as 'pause') + network settings stored in console variables (npt_Active, npt_IPAddress, npt_IPPort, npt_Modem, npt_PhoneNum, npt_SerialPort, npt_SerialBaud, npt_SerialStopBits, npt_SerialParity, npt_SerialFlowCtrl) + 'r_FullSky': if nonzero, whole sky sphere is rendered if sky is visible + MFF_SKINTRANS uses sprite color translation as a skin index for MD2s + demo recording and playback done in the engine (demos are recorded server->client traffic) + demo recording can be started and stopped at any time + commands 'playdemo', 'recorddemo', 'stopdemo' and 'demolump' + subdirectories for DEDs and demos (defs\ and demo\) + each Doomsday client has a unique (random) ID number (in Client.ID) + LZSS compression used for demos (LZSS.DLL), demo file extension is .CDM + invisible floor/ceiling hack: if a sector has a missing lower/upper texture in all its lines, the floor/ceiling of the sector will be drawn at the same height with the surrounding sector's corresponding plane + planes with glowing textures cast dynamic lights on walls and things + 'WallGlow' and 'GlowHeight' control wall glow rendering + 'i_JoyDevice' chooses the joystick to use if there are many + 'ModelAspectMod' scales models vertically; the default value of .833334 negates the VGA aspect ratio used for the world + 'net_MSQ': keep monitoring the network send queue, print in console + 'net_Latencies': (for server) show client information (timing and ticcmd buffering) + 'net_Dev': (for server) show the net_Latencies info on-screen + 'net_StressMargin': (for server) max value for client lag stress, if stress becomes greater, move client's ticcmd run time forward or backward + 'net_StressDampen': (for server) tic interval at which client lag stress reduces by one unit + 'net_SkewDampen': (for client) max number of frames to allow time interpolation of frames, after this the frame continuum is reset + 'net_ShowSkew': (for client) print time skew details in console (delta of last frame and the current skew, in tics to interpolate the frame forward or backward in time) + 'kick' forces the given client to disconnect (for server) + 'ping': clients can ping the server, the server can ping any client + 'sv_MasterAware': (for server) if nonzero, TCP/IP servers inform DMaster that they're running + 'MasterAddress': TCP/IP address of the master server + 'MasterPort': TCP/IP port of the master server + DED Manager 1.0, a Windows app for doing DED related stuff, and most importantly, exporting Info.h, Sounds.h and action function link files + DMaster 0.1, a really simple master server (in fact a DirectPlay Lobby) * default memory zone increased to 24 megs (shown as Mb in startup) * command-line option -window is the same as -nofullscreen * Game DLL API changed * MIDI player uses MIDI streams, not single commands * wall orientation affects lighting (lighter walls facing east) * -maxzone accepts K and M as suffixes (kilobytes and megabytes) * fog on sky sphere ('r_SkyDist' sets sky sphere radius) * source code rearranged into a more logical directory structure * subsector visibility tests are more accurate, reduces stray pixels * masked walls sorted together with sprites and models * modeldef.txt no longer used, models defined with DEDs * textypes.txt no longer used, texture environments defined with DEDs * sprite lump handling much more relaxed, sprites can be patched * DDMF_SHADOW(2) flags result in more translucent sprites/models * only visible models rendered * dlBlend 1 is always used with fog (dlFactor multiplied by 0.6) * bamsAtan2 uses 64-bit integers to allow larger numbers as arguments * jtNet2 compiles the list of available modems during startup * buffering of local ticcmds no longer uses maketic (maketic has been removed) * all new input code (keyboard, mouse, joystick) * flares and dynamic lights are much brighter * renderers modify viewport Z range instead of scaling projection matrix to do Z bias, reduces stray pixels * some default values changed: dlFactor=0.6, dlBlend=0, FlareIntensity=50, r_FlareMinSize=300, r_dlMaxRad=256, r_dlRadFactor=3 * dynamic light texture made a bit more realistic * 'net connect' uses an index number to identify servers * 'net mconnect' connects to a server received from the master * .CFG file includes the Game DLL version string on the first line * 'load', 'unload' and 'reset' go to startup screen mode to show progress information * secondary lens flares diminish as they approach view borders - pitch bend in MIDI songs - skywalls (invisible Z buffer masks) no longer have dynamic lights on them - no more "long debug message has overwritten memory" errors - subsector plane vertices are verified for correct rendering (zero area) - lens flares won't be occluded by their source - font renderer character coordinates (rendered wrong at least with my TNT) - renderers no longer use whichever Doomsday window they happen to find - texcoords of masked textures clamped vertically, reduces gfx errors - texcoords of masked textures clamped horizontally when necessary - D3D renderer sometimes set the wrong texture parameters - sounds have proper handles, prevents mixups with is-playing tests - a division by zero was made if a texture was fully transparent - minor jtNet2 bugs Version 0.99.6 -------------- * game DLL API changed: old DLLs are incompatible * DLaunch: disabled parent -> children skipped (example: JHeretic's -warp option) + DLaunch: modifying an option's value checks the option + per mobj dl scaling: 1, .75, .5 or .25 to dl radius (won't affect flares) + spralign 3: align sprites to view plane, but not fully - console border + cvar consoleDump: all console text is dumped to Doomsday.out * numpad keys have their own key codes (apart from /,*,-,+ and enter) + scroll lock and num lock can be used in bindings (scrlock and numlock) - cvar i_mouseInvY: for some reason this wasn't a cvar earlier + mouse wheel support (cvar i_mWheelSensi controls sensitivity) * CalcSectorReverbs() and texture type handling moved into the engine Version 0.99.5 -------------- + console commands can set a return value (DD_CCMD_RETURN) * rendquad_t is 4 bytes smaller and doesn't use a nameless union/struct - jtNet displayed serial link config dialog (sheesh...) - netgame events that arrive during rendering are now handled correctly (could cause a crash when starting a netgame) + option -nowsk: disable Alt-Tab, Alt-Esc and Ctrl-Alt-Del + cvar r_dlMaxRad: maximum radius of a dynamic light + cvar r_dlRadFactor: a multiplier for dynamic light radii - clipping bug that sometimes caused subsectors to disappear when the camera was exactly atop one of their edges * view borders drawn using the actual width of the patches (Hexen and Heretic have view borders that are 4 pixels thick, Doom uses 3 pixels) Version 0.99.4 -------------- - "bind (event)" clears all +(event), -(event) and *(event) - 2-sided middle textures are rendered correctly - flares from sprites aligned to the viewplane won't flicker at certain angles + D3D: gamma correction using DirectDraw's gamma control (fast!) + option -nogamma: disable DirectDraw gamma, modify texture colors manually + cvar r_flareminsize: the minimum size for flares that can have secondary flares (200 is pretty good) + light source vertical offsets * ccmd listbindings can take a search parameter: "listbindings a" will list all bindings to events that begin with A * faster dynamic lights (better algorithm + clipping) + cvar dlclip: dynlights are clipped (subsector polygons *not* used when rendering floors/ceilings) * spralign 2: attempts to hide sprite flatness (like spralign 0, but stops slanting sprites if angle > r_maxSpriteAngle) + cvar r_maxSpriteAngle doomsday-stable-1.15.7/doomsday/client/doc/changelogs/changelog.txt0000664000175000017500000015014512641367670024646 0ustar jaakkojaakko Doomsday Engine Change Log ========================== Legend: + added, - fixed, * improved/changed *** *** -=- THIS CHANGELOG IS DEPRECATED -=- *** *** More recent history is at the Doomsday Engine Wiki: *** http://dengine.net/dew/index.php?title=Category:Releases *** Version 1.9.0 ------------- API: + DMU (Doomsday Map Update): map data objects are no longer directly accessible from plugins; all changes go through DMU, and notifications are routed to the correct places inside the engine for efficient operation User Interface: * tweaks to the console appearance (new defaults for background picture, color, and alpha) + console uses the Doomsday title bar, which is visible in the other UI screens * console: game information texts moved to the title bar + control panel UI fades away to display the game view when adjusting gamma, contrast, and brightness + control panel offers 16:10 resolutions - sliders now eat up/down keys (so the user isn't confused when the focus jumps to some other control when up/down is pressed in a slider) - game view window is now resized in the ticker - cursor offsets in menus, and save/load items + new console controls: F5: Clear console text buffer Alt + C: Clear command line Shift + cursorleft/cursorright: Move cursor to start/end of the command line Delete: Delete the character under the cursor Insert: Toggle character insert input mode. When off - new replaces old Shift + Tab: Use "other" unknown word autocompletion mode Right arrow: a "command history fill" to the command line (ala DOS command prompt) when the cursor is at the end of the command line + cvar 'con-move-speed': (0 - 1.0f) speed of console opening/closing * improved: console history buffer uses different colours for items based on their type (yellow for commands, white for variables) Resources: * removed: Doomsday.wad is obsolete, all data is now loaded from Doomsday.PK3 + new fixed and variable-width fonts (with normal, bold, and light variants) Renderer: + bias lighting: direct sources and smooth sector lighting - fixed usage of GL_ATI_texture_env_combine3 (caused problems with rendering shiny surfaces) * slightly more pronounced fakeradio (default darkness changed from 1.0 to 1.2) + cvar 'rend-bias-grid-multisample': multisample factor used when determining sector to lightgrid mapping. Larger values increase accuracy at the expense of map load times. + light range compression: increase light in dark sectors/decrease light in bright sectors + cvar 'rend-fog-default': default fog algorithm + Flaremaps: loaded from "Data/FlareMaps" - used for custom halos on objects (see Readme for details) + surface colours: individual walls and planes can be given a unqiue color. Wall section surface colors are blended between sections to achieve color fades. Surface colors and fades can be used to accurately recreate Doom64 style lighting. + alpha'd/blended midtextures on twosided linedefs + flag 'LUMF_DONTTURNHALO': disable the view angle rotation of halos and flares for this luminous object. + blendmode: BM_ZEROALPHA (GL_ONE, GL_ZERO) "draw with no translucency" + cvar 'rend-sprite-mode': 1 = draw sprites and masked walls with hard edges - fixed: light & halo not centered to sprite correctly (BatmanTC, wall torches) - fixed: mobj shadows sometimes rendered above the object casting the shadow - fixed: animated textures/flats would flicker after a console RESET - fixed: pegged middle textures "bouncing" on moving planes + cvar 'rend-dev-mobj-bbox': debugging aid - render mobj bounding boxes (sized to match the box used for collision detection) for debug. + cvar 'rend-dev-cull-subsectors': disable non-visible subsector culling for debug. + cvar 'rend-glow-scale': multiplier for wall glow height Map Data: * optimized allocation and handling of runtime map data (zblocks, levelstatic blocks, fast alloc mode), reduced map setup times significantly * changed: expanded map structure element limits. * changed: expanded blockmap limits by enlarging BLOCKMAP data at runtime + ccmd 'listmobjs': print a list of the mobj ids + names (if set via DED) to the console. * changed: BSP data is no longer required to load maps if runtime node building is enabled (cvar 'bsp-build' = 1). This allows playing eg maps created by SLIGE without generating the BSP data prior to loading the map in Doomsday * improved: map load time greatly improved with large maps * improved: emulation of DOOM.exe faked sector-over-sector tricks * improved: sectors now have three sound origins (center, floor, ceiling). Sector sounds are played from the most appropriate source. * improved: fakeradio now handles complex geometry and DOOM.exe tricks better * improved: fakeradio wall shadows are now affected by plane glows and the sky - fixed: missing textures/flats nolonger cause a fatal error. Surfaces with an unknown/missing textures/flats are now rendered with a "Missing" texture + unknown/missing textures/flats are reported in the console after loading a map + cvar 'blockmap-build': when/if new data is generated (default = when needed) Definitions: - removed fixed limit on the number of particle generator stages + Light -> Halo radius : radius of halo + Light -> Flare map: the flare map to use for this light's halo. Either a path and resource name OR a built-in flare resource number + Decoration -> Light -> Flare map: same as above for light decorations. * Decoration -> Light -> Flare texture: is depreciated. Use "Flare map" instead + Flag -> Info: text string containing a comment about this flag + DED reader: the "Copy"/"*" directive (used to initialize the current definition being read with the values of the last) can now be used with Mobj, State, Line and Sector (type) definitions. - fixed: minor memory leaks concerning definition data Engine: * memory zone expands as necessary - removed the "-maxzone" option as obsolete + option '-vdmap': map files in one directory to another directory (virtual directory mapping) + PK3 path mapping: files in 'keyword-named' folders in the root of a PK3 mapped to directories (see Readme for details) + subdirectory "Auto" of the runtime directory mapped automatically to both "Defs//Auto" and "Data//Auto" + new DFN font format that supports RGBA bitmaps + DeHackED patches (.deh files) are now loaded automatically from Data/Game/Auto and from the root folder in PK3/ZIP archives. - removed fixed limit on the number of opened files - less chance of file name collisions thanks to CRC32 in Dir_FileID - fixed: crash during startup when running with extra verbosity - fixed: vsprintf security vulnerabilities (buffer overflow) on all platforms Thanks to the Gentoo crew, Florian Westphal and Alexey Dobriyan for their part in helping to resolve these issues. * changed: always do a full texture reset after using the "lowres" ccmd * changed: cameras can now move above/below the ceiling/floor unrestricted. - fixed: auxiliary lump cache not checked for lump names when searching Input/Controllers: + binding classes: multiple commands can be bound to the same event + ccmd 'enablebindclass': Enable/disable/toggle binding classes or view their status + ccmd 'listbindclasses': Print a list of available binding classes * ccmd 'listbindings': List bindings in a specific binding class (eg 'listbindings map') * ccmd 'bind': Bind a command for a specific binding class to a key/button (eg 'bind map +f follow') * ccmd 'delbind': Delete a binding in a specific binding class (eg 'delbind map mpanup') All Games: + game status cvars read-only cvars that show the status of various player/game values ('player-health' 'map-name' etc) + automap options menu + weapons options menu + customisable weapon change order preferences: via weapons menu and cvars + cvar 'player-weapon-nextmode': use custom order for next/previous weapon cycle * changed: Automatic weapon switching has two modes: 1= If better 2= Always + The level title drawn when entering a map is now drawn with the patch used during intermissions (eg "WILV01") if it has been replaced in a PWAD. These custom patches take priority over patch replacement strings. + The level title patch draw during intermissions is drawn using the name set in the MapInfo defintion for the level. Intermission patches have priority over patch replacement strings. * Improved menu "Patch Replacement" feature to better support PWADS with custom menu patches + menu widget for changing colors + cvars 'msg-color-x': Color used for HUD messages + cvar 'msg-align': Set the horizontal alignment of HUD messages + Fullscreen view with no HUD + Fullscreen view with 'floating' status bar + cvar 'hud-status-alpha': Alpha value of statusbar background + cvar 'hud-status-icon-a': Alpha value of statusbar icons/counters + cvar 'hud-color-a': Alpha value of fullscreen HUD counters + cvar 'hud-icon-alpha': Alpha value of fullscreen HUD icons + cvars 'menu-color2-X': Secondary colors used for menu text + cvar 'menu-slam': 'Slam' menu when opening/closing + cvar 'map-huddisplay': Set the HUD display mode while in the automap (0 = none, 1 = current HUD, 2 = statusbar) + cvar 'menu-quick-ask': Ask me to confirm when quick saving/loading + cvar 'server-game-monster-meleeattack-nomaxz': to disable the fix added in the lxDoom source release to prevent monsters melee attacks having infinite z range. + cvar 'server-game-radiusattack-nomaxz': all radius attacks are infinitely tall in netgames + menu-fog mode 3: "Grey fog" + menu-fog mode 4: "Darken" * cheat 'reveal': 4 = show subsectors in automap * removed fixed limit on the number of active plats * removed fixed limit on the number of active switches * removed MAX_ADJOINING_SECTORS fixed limit * changed cvar 'msg-blink': number of tics to blink before fading to normal * changed cvar 'menu-patch-replacement': 2= Use built-in patch replacement when available * Improved cfg consistency between games * Automap keys are bindable * Automap colors can be customised via console, cfg files and the menu. * Menu navigation keys are bindable * Widget/Message response keys are bindable * Save & Load slots are now sync'd in the menu * changed: cvar 'ctl-look-speed' is now specified using a float value * changed: ccmd 'spawnmobj' now accepts the mobj name as well as the mobj id + cvar 'ctl-turn-speed': controls player turn rate - fixed: alignment in view border patch drawing jDoom: + DOOM.exe compatibility options/fixes maxskulls: remove max LostSoul spawn limit on Pain Elementals (off by default) skullsinwalls: prevent LostSouls from being spawned inside walls (on by default) raiseghosts: prevent Archviles from raising ghosts (on by default) anybossdeath666: The death of ANY boss monster triggers a 666 special (off by default) monsters-stuckindoors: Monsters can get stuck in doortracks (off by default) objects-hangoverledges: Only some objects can hang over tall ledges (on by default) objects-clipping: Use EXACTLY DOOM's clipping code (off by default) zombiescanexit: Zombie players can exit levels (off by default) player-wallrun-northonly: Players can only wallrun North (off by default) objects-falloff: Objects fall under their own weight (on by default) + support for most of jHeretic's MF2_ flags: includes floorclip, floatbob etc + support the "skill 0 trick" + BOOM THING flags: "Not Deathmatch","Not Coop" supported + BOOM LINEDEF flags: "Pass Thru", "Any Trigger" supported "Any Trigger" overrides XG activation type requirements unless the "ltf2_override_any" flag is set in Flags2 + BOOM "ANIMATED" lump support + BOOM "SWITCHES" lump support + ccmd 'suicide': commit suicide in non deathmatch games + ccmd 'exitlevel': exit the current level via the normal exit and go to the intermission + cvar 'map-babykeys': show key locations in the automap on easy skill + cvar 'hud-face': show Doom Guy's face in the fullscreen HUD + cvar 'server-game-mod-damage': Enemy (mob) damage modifier, multiplayer (1..100) + cvar 'server-game-mod-health': Enemy (mob) health modifier, multiplayer (1..20) + cvar 'server-game-respawn-monsters-nightmare': monsters respawn in Nightmare skill + cvar 'server-game-bfg-freeaim': Allow free-aim with BFG in multiplayer games + cvar 'player-berserkswitch': automatically switch to fists when collected + cvar 'player-death-lookup': makes the player look upwards when killed - fixed: automatic weapon switching now works in Co-op games - fixed: voodoo doll spawning bug exhibited on maps such as caesar.wad + Thing -> flags2 'mf2_radiusattacknomaxz': z dimension is ignored when calculating if a radius attack hits a mobj with this attribute (in PIT_RadiusAttack). + DeHackED: 'Misc' patches are fully supported + value 'Player|Green Armor Class': armor class of the Green Armor pickup + value 'Player|Blue Armor Class': armor class of the Blue Armor pickup + value 'Player|God Health': health displayed when in god mode + value 'Player|IDFA Armor': armor given with 'IDFA' cheat + value 'Player|IDKFA Armor': armor given with 'IDKFA' cheat + value 'Player|IDFA Armor Class': armor class given with 'IDFA' cheat + value 'Player|IDKFA Armor Class': armor class given with 'IDKFA' cheat + value 'MegaSphere|Give|Health': health given when MegaSphere collected + value 'SoulSphere|Give|Health': health given when SoulSphere collected + value 'SoulSphere|Give|Health Limit': max health when SoulSphere collected * ccmd 'give': number (0-NUMWEAPONS) = give an individual weapon (as if found) f = give the player the power of flight * removed fixed limit on number of boss brain spawnspots * removed fixed limit on number of archived mobjs - fixed: "Boss Brain doesn't die by rockets as easily" splash damage is considered to be infinetly tall when checking the boss brain - fixed: Doom2 MAP30 could not be completed with game-corpse-time = 1 - fixed: soundtarget data is now saved. Fixes bug where monsters would forget where they heard the player(s) after a save - fixed: tracer data is now saved. Fixes bug where the Archvile fire attack and the Revenant missile would loose their target when saving - fixed: animated textures in TNT and Plutonia were messed up. - fixed: DOOM logic error which prevented the GOTMEDINEED message from ever being used - fixed: DOOM inversed test which prevented the OUCH face from ever being used jHeretic: + Heretic.exe compatibility options/fixes monsters-stuckindoors: Monsters can get stuck in doortracks (off by default) objects-hangoverledges: Only some objects can hang over tall ledges (on by default) objects-clipping: Use EXACTLY DOOM's clipping code (off by default) player-wallrun-northonly: Players can only wallrun North (off by default) objects-falloff: Objects fall under their own weight (on by default) * Uses jDoom's Menu code All features, cvars, ccmds from jDoom supported * Uses jDoom's HUD message code (msg buffer) All features, cvars, ccmds from jDoom supported + BOOM "ANIMATED" lump support + BOOM "SWITCHES" lump support + ccmd 'suicide': commit suicide in non deathmatch games + ccmd 'hereticfont': Use the Heretic font in the console + ccmd 'exitlevel': exit the current level via the normal exit and go to the intermission + cvar 'map-babykeys': show key locations in the automap on easy skill + cvar 'hud-artifact': show the current artifact in the fullscreen HUD + cvar 'hud-keys': show keys in the fullscreen HUD + cvar 'hud-health': show health in the fullscreen HUD + cvar 'server-game-mod-damage': Enemy (mob) damage modifier, multiplayer (1..100) + cvar 'server-game-mod-health': Enemy (mob) health modifier, multiplayer (1..20) + cvar 'hud-inventory-timer': number of tics before the inventory auto-hides + Added patches for missing ASCII characters to jHeretic.wad - fixed: lineattack weapons hit planes - fixed: various alignment glitches in statusbar drawing - fixed: float bobbing would not activate until the player moved in the X/Y dimension(s). - fixed: soundtarget data is now saved. Fixes bug where monsters would forget where they heard the player(s) after a save. + Thing -> flags2 'mf2_radiusattacknomaxz': z dimension is ignored when calculating if a radius attack hits a mobj with this attribute (in PIT_RadiusAttack). * removed fixed limit on number of archived mobjs * changed: command-line option -ravpic is now -devparm jHexen: * Uses jDoom's Menu code All features, cvars, ccmds from jDoom supported * Uses jDoom's HUD message code (msg buffer) All features, cvars, ccmds from jDoom supported + ccmd 'hexenfont': Use the Hexen font in the console + cvar 'hud-artifact': show current artifact in fullscreen HUD + cvar 'hud-health': show health in fullscreen HUD + cvar 'hud-inventory-timer': number of tics before the inventory auto-hides + Added patches for missing ASCII characters to jHexen.wad - fixed: players would continue receiving damage after a telporting out of a damaging sector with a Chaos Device, until they moved. * changed: command-line option -ravpic is now -devparm XG: * changed: the DD_XGDATA lump is now depreciated. + Line (type): XG line type definitions now have alternate names for the iparms. (see wiki for more details - http://dew.dengine.net) + line class "ltc_line_teleport": BOOM-style line->line (silent) teleport + line class "ltc_teleport" * improved: line class "ltc_end_level": specify next level on exit (Ip3) * improved: line classes "ltc_wall_texture" & "ltc_plane_texture": set the surface color, blendmode and alpha properties of the referenced surfaces. * improved: line class "ltc_wall_texture" -> Set Mid If None: add/remove midtextures on twosided linedefs. + Added more XG_Dev messages. Reference types are printed by name instead of ID. * fixed: bug that prevented sector chains from working correctly under certain circumstances (due to dummy activators in sector functions) * fixed: XS_GetPlane - when a act_tag reference was specified it was incorrectly being compared to the regular sector tag during iteration. * fixed: XS_GetPlane was incorrectly referencing sector tag 0 when called during XS_Trav functions which implicitly set ref data to zero when used in conjunction with spref_tagged_ or spref_act_tagged_. Now checked with -1 and an XG_Dev warning message is printed. * changed: ltc_mimic_sector was using the line's Act Tag when the mimic target reference was set to SPREF_ACT_TAGGED_ This is the opposite to the way it works with all other classes which use these data references. New sprefs have been added which retain this functionality. IF YOU'VE USED SPREF_ACT_TAGGED_FLOOR/CEILING AS THE MIMIC TARGET REFERENCE IN YOUR PWAD: Your PWAD will no longer work as expected and you should update and redistribute it. * fixed: XSTrav_PlaneTexture - the XG_Dev message "couldn't find suitable" was printed even when a suitable reference WAS found. * fixed: ltc_plane_move sector type was incorrectly being set to type 0 when the move finished if no end type change was specified. Result was sectors "losing" their type after a plane move. + Line type -> Act type: Set line type on activation + Line type -> Deact type: Set line type on deactivation + Flag 'ltf2_override_any': override BOOM 'Any Trigger' Linedef flag + line->line references (lrefs): lref_none + line->plane references (lprefs): lpref_back_floor lpref_back_ceiling lpref_thing_exist_floors lpref_thing_exist_ceilings lpref_thing_none_floors lpref_thing_none_ceilings + sector->plane references (sprefs): spref_back_floor spref_back_ceiling + line->sector references (lsrefs): lsref_none lsref_back lsref_thing_exist lsref_thing_none + light level reference (lightref): lightref_back Multiplayer: - fixed: skill selection not working via Game Setup menu Linux: + SDL joystick support (by zachkeene) - fixed: disappearing enemies in jHeretic and jHexen (fix by zachkeene) * Build scripts require zip (package zip on Debian/Ubuntu) - fixed: fixed some segfaults caused by missing data in *Nix builds. Mac OS X: * optimized release builds now work correctly - fixed several endianness issues in model rendering, XG, WAV loading, buttons - fixed: network games (now compatible with Win + Linux) - fixed rendering of shiny models (multitex and non-multitex) - fixed many minor compilation issues Security: - fixed: CVE-2006-1618 buffer overflow. Version 1.8.6 ------------- + "Flats" external resource category: defaults to "Data/Game/Flats/" * external flats are searched first from "Data/Game/Flats/" * PK3s can now be compressed (normal ZIP files) * unlimited nesting of autoloads via Auto directory and virtual files + PK3 path mapping: files in the root of a PK3 mapped to the Auto directories (see Readme for details) + lump assembly prefixes (see Readme for details) + Reflection -> Min color: minimum RGB color for the reflection (to make the reflection visible even without regular sector light) - fixed: model file hash reset properly when running 'reset' - fixed: problem with multiple -file options SDL_mixer: * MUS data converted to MIDI is stored in the runtime directory instead of /tmp Mac OS X: + MiniStart launcher * music is played using QuickTime (MIDI problems resolved) - multiple endianness issues fixed in texture/PK3/model handling Version 1.8.5 ------------- + surface reflections (shiny effects) + 'Reflection' definition + cvar 'rend-tex-shiny': enable surface reflections + Model -> Sub -> Shiny reaction: how much shiny textures react to angle changes + cvar 'rend-model-shiny-strength': shininess factor for all models (0..1) + Model -> Sub -> Blending mode: select a blending mode (bm_* flag) + bm_* flags in Flags.ded (blending modes) + ccmds 'conopen', 'conclose', 'contoggle': open/close console prompt * detail textures can now use external resources instead of WAD lumps (Detail -> File) * external resources can be located in relation to Doomsday base path (e.g. Shiny map = "Data/LightMaps/Shine") - fixed: dynamic light clipping in the case where lights are in the same subsector with a polyobj jHexen: + cvar 'hud-scale': scaling factor for HUD graphics and messages Win32: + option '-nostwin': don't show the startup message dialog (quicker startup) * crashes are handled more gracefully - fixed: vcbuild.bat didn't include resources in Doomsday.exe and drD3D.dll, which meant that the startup window and the drD3D config dialog were missing Version 1.8.4 ------------- * shiny texture coordinates are calculated using a simplified algorithm - fixed: shiny texture coordinate 'warparound' artifact - fixed: FakeRadio hanged in maps with some unusual map geometry Win32: + vcbuild.bat: builds everything from the command line * built using Visual C++ Toolkit 2003, available from: http://msdn.microsoft.com/visualc/vctoolkit2003/ Version 1.8.3 ------------- + Generator -> Stage -> Spin resistance { 0 0 }: factors for slowing down particle spinning (0..1) - fixed: model skin wrapping - fixed: halo origin calculation for high-resolution sprites - keycode handling in console/UI didn't always use the keymap jDoom: - fixed: crushed barrels now explode correctly and don't become gibs Linux: * MIDI music player defaults to SDL_mixer's default implementation (DENG_MIDI_CMD can be used to specify an external player) * fixed-point math using inline assembler like in Windows (courtesy of Lukasz Stelmach) Mac OS X: * a native build that runs on Mac OS X 10.3 * now compiles with Xcode - fixed various glitches with regards to loading of plugins Version 1.8.2 ------------- - fixed: segfault when starting a new game during demo playback - fixed: instant camera position changes during demo playback - fixed: console font disappearance + added control panel setting for rend-tex-filter-smart - adjusted particle Z coordinate when touching a plane - fixed fatal error when building GL nodes for a map in the end of the data lump directory Mac OS X: - fixed endianness problem with MUS-to-MIDI conversion Version 1.8.1 ------------- * it is no longer possible to turn off camera smoothing - cvar 'rend-camera-smooth' was hidden - fixed: game event smoothing now operates seamlessly regardless of the framerate - fixed: camera position smoothing wasn't synced correctly to frames - fixed: segfault related to sight checking over large distances (e.g. TNT map 27) + dpMapLoad: glBSP 1.96 as a plugin, run on the fly when necessary + rend-halo-realistic: use only more realistic halos (slightly smaller, dimmer), no secondary lens flares; enabled by default - fixed: Map Info -> Execute - fixed: FakeRadio bug where vertical shadows were rendered above and under the left and right edge of an opening in a straight wall - GL must not be loaded at all in dedicated mode - file finder had problems descending into subdirectories - input events are processed immediately: mouse lag is gone both in the game and in the UI - mouse filter is more careful not to ever lose any mickeys - all pending ticcmds are always processed immediately (excessive buffering no longer a danger) - fixed several endianness issues to allow building on a big endian arch * tuned up handling of ticcmds during transfer from client to server * particle movement is smoothed (except flat particles that contact a surface) * mouse look is always as smooth as it can be * all games use the same keyboard look scheme (jDoom's was different) * framerate is measured more accurately + ccmd 'message': display a local game message Common: * more unified handling of ticcmds + cvar 'player-move-speed': player movement speed modifier (0..1) jDoom: - fixed: '-warp' made fonts disappear jHeretic/jHexen: - fixed: when a demo stops continue with a running InFine sequence Win32: - too many console messages caused a crash during startup (extra verbosity) - crash when starting in dedicated mode Linux: + environment variable DENG_MIDI_CMD: play MIDI music using this program (defaults to "timidity") + '--without-(game)' configuration option skips (game) + '--without-opengl' configuration option removes all OpenGL support from the build (for dedicated servers) - color adjustments are now working (vid-gamma et al.) - demo file names were formed incorrectly Mac OS X: - pretty much everything can be compiled and run on Mac OS X with the help of fink and Apple's X11 server (except netgames, don't go there) Version 1.8.0 ------------- - fixed: game clock must not be running while map is being loaded - fixed: mobj Z coordinate when sector has a fake floor Version 1.8.0-rc3 ----------------- * use TCP connection for sending reliable data (game setup, etc.) - ccmd 'listmaps' was missing - fixed: FakeRadio bug where narrow shadows stretch beyond parent edge - fixed: mouse wheel events in Linux - fixed: opening control panel in dedicated mode InFine: + 'XImage' command: set an external graphics resource to a Rect Version 1.8.0-rc2 ----------------- + control panel text has its own color (not just white) * maximum frame rate limited to 200 * movement of missiles and other such objects is now smoothed * network setup screens revised * modems, serial links and IPX no longer supported (only TCP/IP) - ccmd 'playsound' was missing - things didn't follow moving planes smoothly - fixed an input event processing bug that caused duplicated events Version 1.8.0-rc1 ----------------- ! Release Candidate ! features from alpha-2 and alpha-3 are *not included* * Linux now officially supported (no MIDI music, joysticks) + control panel settings for FakeRadio + rend-fakeradio-darkness * rend-camera-smooth now affects 3D model animation, monster movement * artifact bobbing (Heretic/Hexen) completely smooth * 3D model spinning completely smooth * key repeat uses milliseconds (input-key-delay) * network code uses TCP sockets and UDP packets, DirectPlay ditched - savegame directory creation bug fixed - PK3 reader supports files created with Info-ZIP that contain extra field data - OpenGL display resolution change sometimes didn't update viewport - FakeRadio shadows are not rendered when light amplification is active Version 1.8.alpha-3 ------------------- ! Alpha Release + multiple viewports + local players are automatically assigned to different viewports + ccmd 'viewgrid': set up the viewport grid + clientside turning/looking * basic toggle controls once again included in ticcmds * distinction between stick and pointer controller axes * engine handles all controls Version 1.8.alpha-2 ------------------- ! Alpha Release * controller axes can be bound to specific local players * engine handles all controls (incomplete) * 'walk' and 'sidestep' axis controls can be bound (partially implemented) + 'bindaxis' command: bind a controller axis to an axis control (e.g. "bindaxis mouse-y walk") - fixed: sound system (on Win32), may still crash during shutdown - removed obsolete settings from the Control Panel Version 1.8.alpha-1 ------------------- ! Alpha Release + Linux support + accurate measurement of time (variable-length ticks); game code still untouched, uses 35 Hz legacy timing * independent input->ticcmd generator (stub only) * now uses the SDL library (http://www.libsdl.org/) Renderer: + FakeRadio: simulated radiosity lighting (cvar 'rend-fakeradio') + smart texture filtering (modified hq2x, 'rend-tex-filter-smart') - removed smooth camera hack ('rend-camera-smooth') Network: * portable low-level implementation (TCP/UDP sockets), needs testing! Version 1.7.15 -------------- + merged some interesting stuff from 1.8/nix: FakeRadio, sky color * use the "no compression" hint when loading lightmaps * increased maximum number of particle generators to 256 - fixed State -> Execute definition patching jDoom: + option '-nodefaultfx': skip default Lights.ded and Particles.ded Version 1.7.14-4 ---------------- + cmd 'playsound': play a sound effect locally + Map Info -> Execute: execute a console command after map setup (i.e. also after loading a savegame) + Map Info -> Sky Model -> Execute: execute a console command every time the sky model's frame number changes + State -> Execute: execute a console command when a mobj enters this state (NOTE: player HUD weapons are not real mobjs, so this has no effect with psprite states) Version 1.7.14-3 ---------------- + cmd 'listmaps': print all loaded maps and the WAD files where they are from - fixed: 'df_worldtime' was behaving strangely - fixed: 873878: Ogg/Mp3 in PK3 Virtual Folder Structure Not Playing jDoom: + cvar 'menu-quitsound': play a random sound when quitting Version 1.7.14 -------------- ! Maintenance Release + Model -> Sub -> Skin file: file name of the skin (can be anything; use this if it isn't possible to use the model's skin list) + Map Info -> Sky Model -> Layer: associate sky model with a sky layer; model is hidden if sky layer is not enabled + Generator -> Submodel: submodel # to use as generator origin + particle generator sounds: Sound, Volume, Hit sound, Hit volume added to Generator -> Stage + Generator -> Init Vector Rnd: init-time random component of Vector + Generator -> Stage -> Force: linear force + model flag 'df_notexcomp': disable texture compression for all skins of the associated model + model flag 'df_worldtime': model's interpos depends on world time + generator flag 'gnf_srcdir': rotate particle vector + generator flag 'gnf_extra': additional generator for a mobj state + cvar 'rend-tex-filter': use bilinear filtering on textures * a particle generator can have up to 32 stages * models close to the viewpoint won't disappear * max number of data files increased from 128 to 1024 (-file) - fixed: crash when map doesn't have any REJECT data - fixed: confusion/crash when requesting a lump with no name - fixed: edge artifacts around translucent sprites - fixed: sfx sometimes pop with the default sound driver (DS8) - fixed: loading DEDs from virtual directories - fixed: particle center offset angle - fixed: loading order of 3D models (mixed up demon/spectre skins) - fixed: lighting on vertically inverted models - quietly allow the use of undefined sprite frames (object hidden) jDoom: * "Read This" screens are not affected by menu-scale, skull hidden jHeretic: - game can only be saved during normal game play - fixed: cycling down from Elvenwand skips Staff (bug 813773) jHexen: + lava and fire textures glow * support for the Hexen 4-level demo Version 1.7.13 -------------- ! Maintenance Release * model offset and scale are interpolated (Model -> Offset XYZ, Model -> Scale XYZ) * overridden model defs are no longer loaded at all (bug 760099) * number of screenshots is not limited to 100 * screenshot file names are selected according to game mode - fixed: semicolon in bindings (key name "smcln") - fixed: music volume = zero prevents songs from changing - fixed: halos flicker sometimes through doors - a shadow is not rendered if it would be above the object - fixed: crash when loading external resources of color-mapped sprites Network: - fixed: player gets stuck in enemies - fixed: player gets stuck in a moving plane (elevator, door) - fixed: crash in clmobj handling - fixed: recording demos with multiple local players - fixed: clientside jumping power, jumping enabled - fixed: player start spot selection (especially TNT/Plutonia) - fixed: mobj Z coordinates were sometimes incorrect on clientside - fixed: server didn't increase client's bandwidth rating, ever - fixed: WAD warning box in Client Setup screen was in the wrong place - fixed: halos weren't always displayed correctly on clientside drD3D: + option -triple: enable triple buffering in fullscreen mode XG: - XG should always be disabled on clientside jDoom: + cvar 'map-door-colors': show colors of locked doors in automap + cvar 'map-door-glow': locked doors have a glow around them (1=normal) * Plutonia/TNT will automatically set rend-sky-full - fixed: bullet puffs in face (bug 740767) - fixed: main menu item spacing too tight - fixed: STARMS was used in the status bar when in deathmatch jHeretic: + cvar 'player-jump-power' jHexen: + cvar 'player-jump-power' Version 1.7.12 -------------- + option -leaveramp: don't reset color settings back to previous values at shutdown + JWADs: IWAD supplements (uses normal WAD loading order) + Data\Graphics directory: Doomsday's graphics resources (UI textures) + cvars 'ui-cursor-width', 'ui-cursor-height': UI mouse cursor size + added new cvar controls to the Control Panel (e.g. multitex, HUD mirror) * the first incarnation of the Doomsday UI theme, "Gradient", was replaced with a much more polished theme, "Plastic" * UI mouse cursor size and movement depend on display resolution * music data will not be cached into the memory zone while loading (large MP3s wasted a lot of space) * paths that contain the base path are normally printed without the base - removed obsolete settings from Control Panel - removed detail texture maximum distance cvar and Control Panel slider - removed cvars 'rend-light-clip', 'rend-light-shrink' - fixed: possible crash in Con_Error() Definitions: + DED version 6: semicolons are optional (the default is 6) + added Defs/Doomsday.ded: include definitions shared by all games + 'Top map', 'Bottom map', 'Side map' added to Light defs and Decoration:Light defs (name of lightmap to use; empty means default, "-" is none) + particle types pt_modelNN: use 3D model ID = "ParticleNN" + particle flags ptf_zeroyaw, ptf_zeropitch, ptf_rndyaw, ptf_rndpitch + 'Frame', 'End frame', 'Spin' added to Generator:Stage + up to 32 sky models can be defined in a Map Info definition + added an ID key to the Model definition * flags can be defined using this syntax: Flags = flag1 | flag2 | flag3; (flag prefixes must be omitted) * Defs/Doomsday.ded is always read first * Flag definitions moved to Defs/Flags.ded * XG definitions moved to Defs/XG.ded * maximum number of submodels is now 8 * Map Info definitions can be copied * 'InFine' is an alternative name for the 'Finale' definition Network: - fixed: server increased a client's bandwidth rating too rapidly Sound: * sounds from PWADs are not replaced with automatic external resources Refresh: + named Model definitions (Model:ID), not assigned to any state + lightmap resources are read from the Data\x\LightMaps directory + texture/flat animation sequences defined using Group defs (Anim.ded) + "tgf_smooth": interpolate between steps in texture/flat animation + particle generator flag "gnf_group": triggered by all in the flat's animation group + particle generator flags "gnf_blendsub", "gnf_blendrsub": subtractive blending for particles + particle generator flags "gnf_blendmul", "gnf_blendimul": normal and inverse multiplicative blending for particles + option -nohighpat: disable high-resolution patches * weapon model skins are precached * texture/flat groups can be used purely for precaching * detail texture contrast is preprocessed: there will be multiple instances of the same texture with different contrast levels (needs a bit more memory but allows faster rendering) * 8-bit particle textures are interpreted as pure alpha data * PCX images aren't loaded redundantly from virtual files - fixed: PWAD test when loading highres flats and patches (-pwadtex) - fixed: particles sliding over ledges - fixed: Generator Center angle (Y) offset was ignored with mobj sources Renderer: + sky models + particle models + blending modes: subtract, reverse subtract, multiply, inverse multiply + cvar 'rend-glow-fog-bright': wall glow brightness in fog + cvar 'rend-model-shiny-multitex': render shiny skins using multitexturing + cvar 'rend-model-mirror-hud': mirror HUD weapon models + cvar 'rend-model-spin-speed': rotation speed for models with "df_spin" + cvar 'rend-dev-wireframe': render player view in wireframe mode + cvar 'rend-dev-freeze': stop updating rendering lists (for debugging) * renderer uses multitexturing for detail textures, dynamic lights, interpolated texture animation and model shiny skins * shiny skins are correctly masked by alpha in main skin * plane glow on models is stronger than before * if multitexturing is available, masked walls will get one dynamic light (previously masked walls were not lit by dynlights) * dynamic light polygons are no longer clipped * calculations to determine surfaces affected by a dynamic light are more accurate (light has a better chance to be visible) * light decoration brightness decreases as surface/view angle grows - removed option -maxor: clipper has now unlimited nodes - fixed: leaking lights (impossible route detection) - fixed: stray pixels along polygon edges in dynamically lit areas - fixed: detail texture blending when fog is enabled - fixed: object shadows weren't affected by fog - fixed: skymask holes in skyfixed sectors - fixed: shiny skins on HUD models - fixed: console text gibberish on the first time the console is drawn - fixed: cvar 'rend-tex' (render with textures) - fixed: shadows fade away smoothly at maximum distance drOpenGL: + support for multitexturing + NV_texture_env_combine4 or ATI_texture_env_combine3 required when rendering dynamic lights with multitexturing + support for SGIS_generate_mipmap + support for EXT_blend_subtract + support for EXT_texture_compression_s3tc + option -notexcomp: disable texture compression + option -novtxar: disable vertex arrays + option -nosgm: disable SGIS_generate_mipmap * vertex arrays are disabled by default if driver's OpenGL version is older than 1.3 (otherwise enabled) * texture compression disabled by default (use -texcomp to enable) drD3D: + support for multitexturing Games: + cvar 'server-game-cheat': allow cheat commands "god", "noclip", "give" in netgames jDoom: + "Patch Replacement" strings (see Defs\jDoom\Values.ded) + cvar 'menu-patch-replacement': enable or disable replacement strings + cvar 'menu-glitter': letters glow as they are typed in + fonts precached during startup + crosshair alpha slider added to the HUD menu + some light decorations (Doom1Lights.ded + BSTONE3, BRICKLIT) * titlescreen sequence defined as an InFine script (ID = "title") * menu text has properly sized capital letters - fixed: intermissions/finales in Plut/TNT jHeretic: + some light decorations (based on Isegrim's defs) * player mobj selector is set to match currently used weapon * titlescreen sequence defined as an InFine script (ID = "title") jHexen: + some light decorations (based on Isegrim's defs) * player mobj selector is set to match currently used weapon * titlescreen sequence defined as an InFine script (ID = "title") InFine: + cmd 'startinf (scriptid)' (and 'stopinf') + gradient rectangles + object rotation (pics, text objects, rectangles) + command "ScaleXY (handle) (x) (y)" + command "Rect (handle) (x) (y) (w) (h)" + command "FillColor (h) (TOP/BOTTOM/BOTH) (r) (g) (b) (a)" + command "EdgeColor (h) (TOP/BOTTOM/BOTH) (r) (g) (b) (a)" + condition "MODE:(game-mode)" + command "ELSE" + command "DO" ... ";" (nestable blocks) + command "PlayDemo (filename)" + command "Cmd (console command)" + command "OnKey (key-ident) (marker)": jump to marker when key pressed + command "UnsetKey (key-ident)" + commands "Events", "NoEvents": enable/disable interactive mode + commands "Trigger", "NoTrigger": enable/disable menu triggering mode * generic object commands: "Del", "X", "Y", "Sx", "Sy", "Scale", "Angle" "ScaleXY", "RGB", "Alpha" * old pic/text commands that should no longer be used: "DelPic", "DelText", "TextRGB", "TextAlpha", "Tx", "Ty", "TSx", "TSy", "TextScale" * InFine can run in overlay mode (e.g. during GS_LEVEL) * InFine states are nested (script -> demo -> script -> demo -> etc.) - fixed: "WaitText" timing with \w, \W, \p, \P - fixed: "SoundAt" volume XG: + line class "ltc_mimic_sector" + new sprefs + cvar 'xg-dev': print XG event messages to the console - fixed: sector floor/ceiling chain touch test Version 1.7.11 -------------- ! Bugfixes Only * revised server delta generation: fixed-size frames, prioritized contents, sounds are stored inside frames (not separate packets), redundant missile coordinates not sent * server refuses connection if new client's ID already in use * revised clientside handling of frames: collision detection for movement prediction, missiles hidden on impact * revised low-level networking: implement confirmed/ordered packets manually, detect duplicates, Huffman encoding (60%-70% compression) + cmd 'huffman': print Huffman efficiency and number of bytes sent * mobj translucency 0-255 (selector still overrides) * mobj floatbobbing is done by engine (clientside), DDMF_BOB added * client is allowed some movement while airborne (easier jumping) * by default, external resources are no longer loaded for textures from PWADs; use the -pwadtex option to change this (cvar 'rend-tex-external-always') * console command line cursor can be moved left and right * default key repeat interval is now 3 tics (was 4) * braces { and } are equivalent to quotes, e.g.: "alias init-map03 {after 1 {warp 5}}" * cmd 'listcmds' prints a description for each command + cmd 'toggle': toggle a cvar's value between zero and nonzero + cmd 'if': execute a command if the condition is true (tests cvar values) * max number of light decorations increased to 16 per texture - fixed: occlusion for planes exactly at eye-Z - fixed: client was sending too much data (now it's about 0.3 KB/s) - included DED files are read immediately after the Include directive has been encountered - fixed: pausing the game also stops spinning models, floatbob - external music was not loaded correctly from virtual files - fixed: clientside mobjs and players on moving planes - fixed: external sound resources reloaded every time the sound starts - fixed: tracking of currently playing sounds - fixed: floorclip values were restricted to 64 in mobj deltas - fixed: client stepup limit is now exactly 24 units - fixed: coord/offset problems if same sprite used in game and HUD - missing upper texture in a skyceiling sector replaced with the backsector's ceiling (was just blank white) - fixed: base path validation (could be missing a slash) - fixed: path of CPHelp.txt was sometimes not translated - fixed: mobj translucency didn't affect the shadow - fixed: incoming message queue protected by a mutex - music is not restarted if it's already playing - fixed: finding model files/skins with base-relative paths jDoom: * savegames stored in game mode specific subdirs (mixups now avoided) + cvar 'view-bob-weapon-switch-lower': if zero, HUD weapon is not lowered during a weapon switch + value 'Weapon Info|*|Static': if nonzero, HUD weapon is not lowered during a weapon switch * game-corpse-time: corpses fade smoothly - fixed: button deactivation sound - fixed: crash when "kill" used when not in a map jHeretic: * savegames stored in game mode specific subdirs (mixups now avoided) * game-corpse-time: corpses fade smoothly - fixed: button deactivation sound - fixed: Speed + Use Artifact made the player jump - fixed: on clientside, wind didn't affect player (e.g. E1M1) jHexen: * savegames stored in game mode specific subdirs (mixups now avoided) + option -savedir: set savegame directory - fixed: polyobj destination/speed for sliding doors (e.g. map05) - fixed: restoring polyobjs and hidden mobjs from saved map - fixed: problem with excessive sound sequence repeat (e.g. map12) - fixed: torch light for remote players - fixed: no mobjs spawned when dedicated server starts - clientside powers were not correctly set to zero at death, map change - clientside Wings of Wrath icon didn't rotate when flying - fixed: dedicated server deadlock when monsters don't find any players - fixed: crash when minotaur vanishes and master is dead - fixed: 'use artifact' sounds not audible on clientside - fixed: crash when "kill" used when not in a map - fixed: screen border flicker in fullscreen mode (again) - fixed: floatbobbing objects go through the floor - Deathkings map36: playerstart group >4 accepted InFine: * server sends condition truth values to clients (secret, leavehub) XG: - when loading a savegame, line activators were not correctly restored drD3D: - fixed a problem with mode selection (refresh rates) Installer: - fixed: pressing Enter after typing a path closed the installer dialog - glBSP run with the -fresh option to avoid bad BSP data Version 1.7.10 -------------- + support for ZIP/PK3 files (no compression!), can be loaded like WAD files (-file, load; no unload, though), contents added to the virtual file hierarchy (package root dir => Doomsday base dir) + after runtime directory has been searched, data files are also searched from the default data directory (e.g. Data\jDoom\): "-file Test.wad" will load Data\jDoom\Test.wad + WAD, LMP, PK3 and ZIP files from the Data\Game\Auto directory are always loaded automatically + WAD/PK3 files can be loaded from virtual files (inside PK3s) + IncludeIf and SkipIf directives can also test for game modes, e.g. "SkipIf Not doom1-ultimate" + light decorations (dynlights on textures/flats), "Decoration" defs + cvar group 'rend-light-decor' + sprite frames can be replaced with external resources (Patches) + raw screens can be replaced with external resources (Patches) + -maxtex option: set maximum texture size ("-maxtex 128") + sound flag 'sf_dontstop' (0x20): sound does not stop even if emitter is destroyed (sound cannot be stopped until it finishes normally) * faster 3D model loading during startup * rend-camera-smooth: Z-movement of planes is also smoothed * default dynlight brightness increased to 0.75 (rend-light-bright) * default halo brightness decreased to 35 (rend-halo-bright) - halo distance measured in 3D: no excess brightness when viewed from above/below - fixed: sprites were sometimes clipped by a sky ceiling - fixed: problems with rend-light-num jDoom: + light decorations for all switches, many textures and some flats of Doom 1 * barrel/rocket explosions continue even after barrel/rocket is gone (barexp uses sf_dontstop) * Pause key is now the default binding for pause (was P) jHeretic: - fixed: interlude screen wouldn't show seconds if they're zero jHexen: * Pause key is now the default binding for pause (was P) - fixed: weapon pieces bob only partially - fixed: screen border flicker in fullscreen mode Version 1.7.9 ------------- + external resource locator (in Data\Game\: Textures\, Patches\, Music\ and Sfx\) with game mode subdirs (see readme) + high-resolution patches (e.g. menu graphics, game fonts, background pictures) + netgame server info includes: game mode (e.g. doom1-ultimate, doom2-plut, hexen), game config, IWAD, PWADs, player names + info about network setup added to cphelp.txt, shows up during setup + cvar 'server-player-limit': maximum number of clients + cvar 'rend-light-wall-angle': intensity of angle-based wall lighting + cvar 'input-mouse-filter': average mouse X/Y axis values + model flag 'df_dim' (0x1000000): model is never rendered fullbright + cmds 'movefloor', 'moveceil', 'movesec': move a sector's plane(s) * low-level networking updated to DirectX 8 * improved network setup GUI * client can only connect to servers in the same game mode * cmd 'net' prints usage info * cvar 'net-ip-port' is the local TCP/IP port * "Filter mouse movement" added to Control Panel's Input page * -nohightex now only affects wall textures and flats * crosshair color alpha (cmd "crosshair color", var "view-cross-a") - fixed: client was able to connect to a server running a different game - fixed: aborted client connection crashed the server - potential problem handling client connections fixed - fixed: fullbrightness didn't affect particles - DirectSound 8: fixed a problem with buffer loading; using -csd should no longer be necessary - Sound definition patching fixed (Ext/Group keys were ignored) - fixed bug 712332: when server pauses a netgame, all clients will pause KickStart: * Cheb's KickStart v2.09 replaces the ancient v1.6 jDoom: + game modes: doom1-share, doom1, doom1-ultimate, doom2, doom2-plut, doom2-tnt + automap added to Options -> Controls (under Misc.) + cvar game-corpse-sliding: corpses slide down stairs and ledges (defaults to zero due to some bad behaviour; e.g. exit room of D2/22) * cvar game-corpsetime renamed to game-corpse-time - fixed bug 734892: sides with bogus sectors - A_Tracer() used to spawn puffs that were identical to bullet puffs, this caused complications with particle generators jHeretic: + game modes: heretic-share, heretic (normal registered), heretic-ext (has episodes 4, 5) jHexen: + game modes: hexen, hexen-dk (Death Kings of Dark Citadel) - fixed: sound sequence delays with repeating sounds (sequence updater didn't get correct information about currently playing sound effects) drOpenGL/drD3D: + -refresh option * closest available refresh rate selected Version 1.7.8 ------------- + new master server mechanism (uses HTTP) + cvar net-master-path (default: "/master.php"): location of the master server at 'net-master-address' (default: "www.doomsdayhq.com") + cmd "net announce": send a server announcement to the master (if server-public is nonzero, announcements are made automatically at two minute intervals) * cvar net-master-port (default: 0): usually zero or 80 * clientside player coords override serverside coords under normal circumstances (maxdif checks removed) * if one part of a psprite is fullbright, the whole psprite will be - fixed: psprites were rendered with depth testing enabled; Doom plasma rifle fire anim was broken - fixed: clientside player animation didn't finish anim sequence - fixed: line flags weren't updated on clientside (e.g. in Hexen map13) - fixed: minor edge artifact with multipart psprites (e.g. in jDoom) jDoom: * lineattack weapons hit planes (pistol, shotguns, chaingun) - fixed: lineattack check for hitting sky planes, skyhack walls - map31 and map32 no longer assumed to exist: Doom2.wad "00f6d407" should work - weapon psprite timings (in Objects.ded) adjusted for super shotgun, rocket launcher: muzzle flash was out-of-sync by 1-2 tics jHexen: - fixed: dedicated server tried to draw teleport gfx and crashed - fixed: scripted + yellow messages not shown on clientside - fixed: client crashes when changing level - fixed: status bar flicker jtNet2: - old master server stuff removed Version 1.7.7 ------------- + switch texture pairs (SW1/SW2) are precached at the same time + "Generator:Stage:Radius rnd": Randomness of particle radius + sound-info shows sound names as well as ID numbers + support for textured particles * flat particles stick to planes - high-resolution flats with 4 channels (alpha) were loaded incorrectly - sector lightlevel overflow in sectordelta (e.g. on jHexen map 26) - alternative texture path must be tried first when loading hires images (-texdir2) jDoom: * cvar player-air-movement: player movement speed while airborne (0-32); use only small values in netgames jHeretic: - blinking statbar borders fixed doomsday-stable-1.15.7/doomsday/client/client-win32.doxy0000664000175000017500000000124412641367670022415 0ustar jaakkojaakko# API documentation for the Doomsday Client @INCLUDE = ../doomsday.doxy PROJECT_NAME = "Doomsday Client" PROJECT_NUMBER = 1.15 PROJECT_BRIEF = "Internal documentation (Windows)" OUTPUT_DIRECTORY = ../apidoc/win32/ INPUT = ../api include src EXCLUDE = include/unix include/macx src/unix src/macx PREDEFINED = __DOOMSDAY__ __CLIENT__ WIN32 __cplusplus \ "DENG2_PIMPL(C)=typedef C Public; struct C::Instance : public de::Private" \ "DENG2_PIMPL_NOREF(C)=struct C::Instance : public de::IPrivate" \ "DENG2_PRIVATE(Var)=struct Instance; Instance *Var;" doomsday-stable-1.15.7/doomsday/client/include/0000775000175000017500000000000012641367670020714 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/Texture0000664000175000017500000000003612641367670022276 0ustar jaakkojaakko#include "resource/texture.h" doomsday-stable-1.15.7/doomsday/client/include/MapElement0000664000175000017500000000003612641367670022665 0ustar jaakkojaakko#include "world/mapelement.h" doomsday-stable-1.15.7/doomsday/client/include/de_defs.h0000664000175000017500000000234012641367670022455 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2009-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * de_defs.h: Definitions Subsystem */ #ifndef __DOOMSDAY_DEFINITIONS_H__ #define __DOOMSDAY_DEFINITIONS_H__ #include "def_share.h" #include "def_main.h" #include #include #include #include #include #include #endif doomsday-stable-1.15.7/doomsday/client/include/unix/0000775000175000017500000000000012641367670021677 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/unix/dd_uinit.h0000664000175000017500000000270712641367670023655 0ustar jaakkojaakko/** @file dd_uinit.h * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * Unix Initialization. */ #ifndef LIBDENG_UINIT_H #define LIBDENG_UINIT_H #include "dd_pinit.h" #ifdef __cplusplus extern "C" { #endif typedef struct { GETGAMEAPI GetGameAPI; /// @c true = We are using a custom user dir specified on the command line. dd_bool usingUserDir; #ifndef MACOSX /// @c true = We are using the user dir defined in the HOME environment. dd_bool usingHomeDir; #endif } application_t; extern application_t app; dd_bool DD_Unix_Init(void); void DD_Shutdown(void); #ifdef __cplusplus } // extern "C" #endif #endif /* LIBDENG_UINIT_H */ doomsday-stable-1.15.7/doomsday/client/include/Model0000664000175000017500000000003412641367670021674 0ustar jaakkojaakko#include "resource/model.h" doomsday-stable-1.15.7/doomsday/client/include/edit_bias.h0000664000175000017500000000347012641367670023014 0ustar jaakkojaakko/** @file edit_bias.h Shadow Bias editor UI. * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_SHADOWBIAS_EDITOR #define DENG_CLIENT_SHADOWBIAS_EDITOR #ifdef __CLIENT__ #include class HueCircle; DENG_EXTERN_C int editHidden; ///< cvar Is the GUI currently hidden? DENG_EXTERN_C int editBlink; ///< cvar Blinking the nearest source (unless grabbed)? DENG_EXTERN_C int editShowAll; ///< cvar Show all sources? DENG_EXTERN_C int editShowIndices; ///< cvar Show source indicies? /** * To be called to register the commands and variables of this module. */ void SBE_Register(); /** * Returns @c true if the Shadow Bias editor is currently active. */ bool SBE_Active(); /** * Draw the GUI widgets of the Shadow Bias editor. */ void SBE_DrawGui(); /** * Returns a pointer to the currently active HueCircle for the console player; * otherwise @c 0 if no hue circle is active. */ HueCircle *SBE_HueCircle(); #endif // __CLIENT__ #endif // DENG_CLIENT_SHADOWBIAS_EDITOR doomsday-stable-1.15.7/doomsday/client/include/alertmask.h0000664000175000017500000000226712641367670023057 0ustar jaakkojaakko/** @file alertmask.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_ALERTMASK_H #define DENG_CLIENT_ALERTMASK_H #include class AlertMask { public: AlertMask(); /** * Begins observing the Config.alert.* variables and updating the mask * accordingly. */ void init(); bool shouldRaiseAlert(de::duint32 entryMetadata) const; private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_ALERTMASK_H doomsday-stable-1.15.7/doomsday/client/include/WallEdge0000664000175000017500000000003512641367670022321 0ustar jaakkojaakko#include "render/walledge.h" doomsday-stable-1.15.7/doomsday/client/include/de_graphics.h0000664000175000017500000000226012641367670023335 0ustar jaakkojaakko/** @file de_graphics.h * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * Graphics Subsystem */ #ifndef LIBDENG_GRAPHICS #define LIBDENG_GRAPHICS #ifdef __CLIENT__ # include "gl/gl_main.h" # include "gl/gl_draw.h" # include "gl/texturecontent.h" # include "gl/gl_tex.h" # include "gl/gl_defer.h" //# include "gl/gl_texmanager.h" #endif #endif /* LIBDENG_GRAPHICS */ doomsday-stable-1.15.7/doomsday/client/include/m_profiler.h0000664000175000017500000000425412641367670023230 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /* * m_profiler.h: Utility Macros for Profiling */ #ifndef __DOOMSDAY_MISC_PROFILER_H__ #define __DOOMSDAY_MISC_PROFILER_H__ #include "dd_types.h" #include /* * This header defines some handy macros for profiling. * Define DD_PROFILE to active. */ typedef struct profiler_s { uint totalTime; uint startTime; uint startCount; } profiler_t; #define BEGIN_PROF_TIMERS() enum { #ifdef DD_PROFILE // Profiling is enabled. # define END_PROF_TIMERS() ,NUM_PROFS }; static profiler_t profiler_[NUM_PROFS]; # define BEGIN_PROF(x) (profiler_[x].startCount++, profiler_[x].startTime = Timer_RealMilliseconds()) # define END_PROF(x) (profiler_[x].totalTime += Timer_RealMilliseconds() - profiler_[x].startTime) # define PRINT_PROF(x) App_Log(DE2_LOG_DEV, "[%f ms] " #x ": %i ms (%i starts)", \ profiler_[x].startCount? profiler_[x].totalTime / \ (float) profiler_[x].startCount : 0, \ profiler_[x].totalTime, profiler_[x].startCount) #else // Profiling is disabled. # define END_PROF_TIMERS() ,NUM_PROFS }; # define BEGIN_PROF(x) # define END_PROF(x) # define PRINT_PROF(x) #endif // DD_PROFILE #endif doomsday-stable-1.15.7/doomsday/client/include/mesh.h0000664000175000017500000001236612641367670022031 0ustar jaakkojaakko/** @file mesh.h Mesh Geometry Data Structure. * * @authors Copyright © 2008-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_DATA_MESH_H #define DENG_DATA_MESH_H #include #include #include #include "MapElement" class Vertex; namespace de { class Face; class HEdge; /** * Two dimensioned mesh geometry data structure employing the half-edge model * (more formally known as "Doubly connected edge list" (DECL)). * * @see http://en.wikipedia.org/wiki/Doubly_connected_edge_list * * @ingroup data */ class Mesh { public: typedef QList Vertexs; typedef QList Faces; typedef QList HEdges; /** * Base class for all elements of a mesh. */ class Element { DENG2_NO_COPY (Element) DENG2_NO_ASSIGN(Element) public: /// Required map element is missing. @ingroup errors DENG2_ERROR(MissingMapElementError); public: explicit Element(Mesh &mesh); virtual ~Element() {} /** * Returns the mesh the element is a part of. */ Mesh &mesh() const; /** * Returns @c true iff a map element is attributed. */ bool hasMapElement() const; /** * Returns the map element attributed to the mesh element. * * @see hasMapElement() */ MapElement &mapElement(); /// @copydoc mapElement() MapElement const &mapElement() const; template MapElementType &mapElementAs() { return mapElement().as(); } template MapElementType const &mapElementAs() const { return mapElement().as(); } /** * Change the map element to which the mesh element is attributed. * * @param newMapElement MapElement to attribute to the mesh element. * Ownership is unaffected. Can be @c 0 (to * clear the attribution). * * @see mapElement() */ void setMapElement(MapElement const *newMapElement); private: DENG2_PRIVATE(d) }; public: Mesh(); ~Mesh(); /** * Clear the mesh destroying all geometry elements. */ void clear(); /** * Construct a new vertex. */ Vertex *newVertex(Vector2d const &origin = de::Vector2d()); /** * Construct a new half-edge. */ HEdge *newHEdge(Vertex &vertex); /** * Construct a new face. */ Face *newFace(); /** * Remove the specified @a vertex from the mesh, destroying the vertex. * If @a vertex is not owned by the mesh then nothing will happen. */ void removeVertex(Vertex &vertex); /** * Remove the specified @a hedge from the mesh, destroying the half-edge. * If @a hedge is not owned by the mesh then nothing will happen. */ void removeHEdge(HEdge &hedge); /** * Remove the specified @a face from the mesh, destroying the face. * If @a face is not owned by the mesh then nothing will happen. */ void removeFace(Face &face); /** * Returns the total number of vertexes in the mesh. */ inline int vertexCount() const { return vertexs().count(); } /** * Returns the total number of faces in the mesh. */ inline int faceCount() const { return faces().count(); } /** * Returns the total number of half-edges in the mesh. */ inline int hedgeCount() const { return hedges().count(); } /** * Returns @c true iff there are no vertexes in the mesh. */ inline bool vertexsIsEmpty() const { return vertexs().isEmpty(); } /** * Returns @c true iff there are no faces in the mesh. */ inline bool facesIsEmpty() const { return faces().isEmpty(); } /** * Returns @c true iff there are no half-edges in the mesh. */ inline bool hedgesIsEmpty() const { return hedges().isEmpty(); } /** * Provides access to the set of all vertexes in the mesh. */ Vertexs const &vertexs() const; /** * Provides access to the set of all faces in the mesh. */ Faces const &faces() const; /** * Provides access to the set of all half-edges in the mesh. */ HEdges const &hedges() const; private: DENG2_PRIVATE(d) }; typedef Mesh::Element MeshElement; } // namespace de #endif // DENG_DATA_MESH_H doomsday-stable-1.15.7/doomsday/client/include/CommandAction0000664000175000017500000000003612641367670023352 0ustar jaakkojaakko#include "ui/commandaction.h" doomsday-stable-1.15.7/doomsday/client/include/audio/0000775000175000017500000000000012641367670022015 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/audio/audiodriver_music.h0000664000175000017500000000445412641367670025712 0ustar jaakkojaakko/** @file audiodriver_music.h Low-level music interface of the audio driver. * * @ingroup audio * * The main purpose of this low-level thin layer is to group individual music * interfaces together as an aggregate that can be treated as one interface. * * The CD playback interface is part of the aggregate in addition to the * regular Music interfaces. * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_AUDIODRIVER_MUSIC_H #define CLIENT_AUDIODRIVER_MUSIC_H #include #include "dd_types.h" #include "audiodriver.h" #ifdef __cplusplus extern "C" { #endif void AudioDriver_Music_Set(int property, void const *ptr); int AudioDriver_Music_PlayNativeFile(char const *fileName, dd_bool looped); int AudioDriver_Music_PlayLump(lumpnum_t lump, dd_bool looped); int AudioDriver_Music_PlayFile(char const *virtualOrNativePath, dd_bool looped); int AudioDriver_Music_PlayCDTrack(int track, dd_bool looped); /** * Determines if music is currently playing on any of the Music or CD audio * interfaces. * * @return @c true if music is playing. */ dd_bool AudioDriver_Music_IsPlaying(void); /** * Tells the audio driver to choose a new name for the buffer filename, if a * buffer file is needed to play back a song. */ void AudioDriver_Music_SwitchBufferFilenames(void); AutoStr *AudioDriver_Music_ComposeTempBufferFilename(char const *ext); #ifdef __cplusplus } // extern "C" #endif #endif // CLIENT_AUDIODRIVER_MUSIC_H doomsday-stable-1.15.7/doomsday/client/include/audio/s_cache.h0000664000175000017500000000335512641367670023561 0ustar jaakkojaakko/** @file s_cache.h Sound Sample Cache * * The data is stored using M_Malloc(). * * To play a sound: * 1) Figure out the ID of the sound. * 2) Call Sfx_Cache() to get a sfxsample_t. * 3) Pass the sfxsample_t to Sfx_StartSound(). * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDENG_SOUND_CACHE_H #define LIBDENG_SOUND_CACHE_H #include "api_audiod_sfx.h" #ifdef __cplusplus extern "C" { #endif void Sfx_InitCache(void); void Sfx_ShutdownCache(void); /** * @return Ptr to the cached copy of the sample (give this ptr to * Sfx_StartSound(); otherwise @c 0 if invalid. */ sfxsample_t *Sfx_Cache(int id); void Sfx_CacheHit(int id); /** * @return The length of the sound (in milliseconds). */ uint Sfx_GetSoundLength(int id); /** * @return Number of bytes and samples cached. */ void Sfx_GetCacheInfo(uint *cacheBytes, uint *sampleCount); #ifdef __cplusplus } // extern "C" #endif #endif /* LIBDENG_SOUND_CACHE_H */ doomsday-stable-1.15.7/doomsday/client/include/audio/audiodriver.h0000664000175000017500000000667212641367670024516 0ustar jaakkojaakko/** * @file audiodriver.h * Audio driver loading and interface management. @ingroup audio * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_AUDIO_DRIVER_H #define LIBDENG_AUDIO_DRIVER_H #ifdef __SERVER__ # error "audio" is not available in a SERVER build #endif #include #include "api_audiod.h" #include "api_audiod_sfx.h" #include "api_audiod_mus.h" #ifdef __cplusplus #include de::String AudioDriver_InterfaceDescription(); extern "C" { #endif #define MAX_AUDIO_INTERFACES 16 // arbitrary dd_bool AudioDriver_Init(void); void AudioDriver_Shutdown(void); /** * Prints a list of the selected, active interfaces to the log. */ void AudioDriver_PrintInterfaces(void); /** * Retrieves the main interface of the audio driver to which @a audioInterface * belongs. * * @param anyAudioInterface Pointer to a SFX, Music, or CD interface. See * AudioDriver_SFX(), AudioDriver_Music() and AudioDriver_CD(). * * @return Audio driver interface, or @c NULL if the none of the loaded drivers * match. */ audiodriver_t* AudioDriver_Interface(void* anyAudioInterface); AutoStr* AudioDriver_InterfaceName(void* anyAudioInterface); audiointerfacetype_t AudioDriver_InterfaceType(void* anyAudioInterface); /** * Lists all active interfaces of a given type, in descending priority order: * the most important interface is listed first in the returned array. * Alternatively, counts the number of active interfaces of a given type. * * @param type Type of interface to look for. * @param listOfInterfaces Matching interfaces are written here. Points to an * array of pointers. If this is @c NULL, * just counts the number of matching interfaces. * * @return Number of matching interfaces. */ int AudioDriver_FindInterfaces(audiointerfacetype_t type, void** listOfInterfaces); /** * Returns the current active primary SFX interface. @c NULL is returned is no * SFX playback is available. */ audiointerface_sfx_generic_t* AudioDriver_SFX(void); /** * Determines if at least one music interface is available for music playback. */ dd_bool AudioDriver_Music_Available(void); /** * Returns the currently active CD playback interface. @c NULL is returned if * CD playback is not available. * * @note The CD interface is considered to belong in the music aggregate * interface (see audiodriver_music.h), and usually does not need to * be individually manipulated. */ audiointerface_cd_t* AudioDriver_CD(void); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_AUDIO_DRIVER_H doomsday-stable-1.15.7/doomsday/client/include/audio/s_mus.h0000664000175000017500000000430212641367670023313 0ustar jaakkojaakko/** @file s_mus.h Music Subsystem. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDENG_SOUND_MUSIC_H #define LIBDENG_SOUND_MUSIC_H #include "api_audiod_mus.h" #include // Music preference. If multiple resources are available, this setting // is used to determine which one to use (mus < ext < cd). enum { MUSP_MUS, MUSP_EXT, MUSP_CD }; /** * Register the console commands and variables of this module. */ void Mus_Register(); /** * Initialize the Mus module. * @return @c true, if no errors occur. */ bool Mus_Init(); void Mus_Shutdown(); /** * Set the general music volume. Affects all music played by all interfaces. */ void Mus_SetVolume(float vol); /** * Pauses or resumes the music. */ void Mus_Pause(bool doPause); /** * Called on each frame by S_StartFrame. */ void Mus_StartFrame(); /** * Start playing a song. The chosen interface depends on what's available * and what kind of resources have been associated with the song. * Any previously playing song is stopped. * * @return Non-zero if the song is successfully played. */ int Mus_Start(de::Record const *def, bool looped); /** * @return 1, if music was started. 0, if attempted to start but failed. * -1, if it was MUS data and @a canPlayMUS says we can't play it. */ int Mus_StartLump(lumpnum_t lump, bool looped, bool canPlayMUS); void Mus_Stop(); #endif // LIBDENG_SOUND_MUSIC_H doomsday-stable-1.15.7/doomsday/client/include/audio/s_sfx.h0000664000175000017500000001304012641367670023306 0ustar jaakkojaakko/** @file s_sfx.h Sound Effects * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * @authors Copyright © 2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifdef __CLIENT__ #ifndef DENG_CLIENT_SOUND_SFX_H #define DENG_CLIENT_SOUND_SFX_H #include "api_audiod.h" #include "api_audiod_sfx.h" #include "world/map.h" #ifdef __cplusplus extern "C" { #endif // Begin and end macros for Critical Operations. They are operations // that can't be done while a refresh is being made. No refreshing // will be done between BEGIN_COP and END_COP. #define BEGIN_COP Sfx_AllowRefresh(false) #define END_COP Sfx_AllowRefresh(true) // Channel flags. #define SFXCF_NO_ORIGIN (0x1) // Sound is coming from a mystical emitter. #define SFXCF_NO_ATTENUATION (0x2) // Sound is very, very loud. #define SFXCF_NO_UPDATE (0x4) // Channel update is skipped. typedef struct sfxchannel_s { int flags; sfxbuffer_t *buffer; struct mobj_s *emitter; // Mobj that is emitting the sound. coord_t origin[3]; // Emit from here (synced with emitter). float volume; // Sound volume: 1.0 is max. float frequency; // Frequency adjustment: 1.0 is normal. int startTime; // When was the channel last started? } sfxchannel_t; extern dd_bool sfxAvail; extern float sfxReverbStrength; extern int sfxMaxCacheKB, sfxMaxCacheTics; extern int sfx3D, sfx16Bit, sfxSampleRate; /** * Initialize the Sfx module. This includes setting up the available Sfx * drivers and the channels, and initializing the sound cache. Returns * true if the module is operational after the init. */ dd_bool Sfx_Init(void); /** * Shut down the whole Sfx module: drivers, channel buffers and the cache. */ void Sfx_Shutdown(void); /** * Stop all channels, clear the cache. */ void Sfx_Reset(void); /** * Enabling refresh is simple: the refresh thread is resumed. When * disabling refresh, first make sure a new refresh doesn't begin (using * allowRefresh). We still have to see if a refresh is being made and wait * for it to stop. Then we can suspend the refresh thread. */ void Sfx_AllowRefresh(dd_bool allow); /** * Must be done before the map is changed. */ void Sfx_MapChange(void); void Sfx_SetListener(struct mobj_s *mobj); /** * Periodical routines: channel updates, cache purge, cvar checks. */ void Sfx_StartFrame(void); void Sfx_EndFrame(void); /** * Called periodically by S_Ticker(). If the cache is too large, stopped * samples with the lowest hitcount will be uncached. */ void Sfx_PurgeCache(void); void Sfx_RefreshChannels(void); /** * Used by the high-level sound interface to play sounds on the local system. * * @param sample Sample to play. Ptr must be stored persistently! * No copying is done here. * @param volume Volume at which the sample should be played. * @param freq Relative and modifies the sample's rate. * @param emitter If @c NULL, @a fixedpos is checked for a position. * If both @a emitter and @a fixedpos are @c NULL, then * the sound is played as centered 2D. * @param fixedpos Fixed position where the sound if emitted, or @c NULL. * @param flags Additional flags (@ref soundPlayFlags). * * @return @c true, if a sound is started. */ int Sfx_StartSound(sfxsample_t *sample, float volume, float freq, struct mobj_s *emitter, coord_t *fixedpos, int flags); int Sfx_StopSound(int id, struct mobj_s *emitter); /** * Stops all channels that are playing the specified sound. * * @param id @c 0 = all sounds are stopped. * @param emitter If not @c NULL, then the channel's emitter mobj * must match it. * @param defPriority If >= 0, the currently playing sound must have * a lower priority than this to be stopped. Returns -1 * if the sound @a id has a lower priority than a * currently playing sound. * * @return The number of samples stopped. */ int Sfx_StopSoundWithLowerPriority(int id, struct mobj_s *emitter, ddboolean_t byPriority); /** * Stop all sounds of the group. If an emitter is specified, only it's * sounds are checked. */ void Sfx_StopSoundGroup(int group, struct mobj_s *emitter); /** * @return The number of channels the sound is playing on. */ int Sfx_CountPlaying(int id); /** * The specified sample will soon no longer exist. All channel buffers * loaded with the sample will be reset. */ void Sfx_UnloadSoundID(int id); /** * Requests listener reverb update at the end of the frame. */ void Sfx_UpdateReverb(void); void Sfx_DebugInfo(void); #ifdef __cplusplus } // extern "C" #endif #endif // DENG_CLIENT_SOUND_SFX_H #endif // __CLIENT__ doomsday-stable-1.15.7/doomsday/client/include/audio/m_mus2midi.h0000664000175000017500000000263512641367670024241 0ustar jaakkojaakko/** @file m_mus2midi.h MUS to MIDI conversion. * @ingroup audio * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef __DOOMSDAY_MUS2MIDI_H__ #define __DOOMSDAY_MUS2MIDI_H__ #ifdef __cplusplus extern "C" { #endif /** * Converts DOOM MUS format music into MIDI music. The output is written to a * native file. * * @param data The MUS data to convert. * @param length The length of the data in bytes. * @param outFile Name of the file the resulting MIDI data will be written to. */ dd_bool M_Mus2Midi(void* data, size_t length, const char* outFile); #ifdef __cplusplus } // extern "C" #endif #endif doomsday-stable-1.15.7/doomsday/client/include/audio/s_main.h0000664000175000017500000000562112641367670023440 0ustar jaakkojaakko/** @file s_main.h Audio Subsystem * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_AUDIO_S_MAIN_H #define DENG_AUDIO_S_MAIN_H #include "world/p_object.h" #include "def_main.h" #include "api_sound.h" #include "api_audiod.h" #ifdef __cplusplus extern "C" { #endif /** * @defgroup soundPlayFlags Sound Start Flags * @ingroup flags * @{ */ #define SF_RANDOM_SHIFT 0x1 ///< Random frequency shift. #define SF_RANDOM_SHIFT2 0x2 ///< 2x bigger random frequency shift. #define SF_GLOBAL_EXCLUDE 0x4 ///< Exclude all emitters. #define SF_NO_ATTENUATION 0x8 ///< Very, very loud... #define SF_REPEAT 0x10 ///< Repeats until stopped. #define SF_DONT_STOP 0x20 ///< Sound can't be stopped while playing. /// @} extern int showSoundInfo; extern int soundMinDist, soundMaxDist; extern int sfxVolume, musVolume; extern int sfxBits, sfxRate; extern byte sfxOneSoundPerEmitter; void S_Register(void); /** * Main sound system initialization. Inits both the Sfx and Mus modules. * * @return @c true, if there were no errors. */ dd_bool S_Init(void); /** * Shutdown the whole sound system (Sfx + Mus). */ void S_Shutdown(void); /** * Must be called after the map has been changed. */ void S_SetupForChangedMap(void); /** * Stop all channels and music, delete the entire sample cache. */ void S_Reset(void); void S_StartFrame(void); void S_EndFrame(void); /** * Gets information about a defined sound. Linked sounds are resolved. * * @param soundID ID number of the sound. * @param freq Defined frequency for the sound is returned here. May be @c NULL. * @param volume Defined volume for the sound is returned here. May be @c NULL. * * @return Sound info (from definitions). */ sfxinfo_t *S_GetSoundInfo(int soundID, float *freq, float *volume); /** * @return @c true if the specified ID is a repeating sound. */ dd_bool S_IsRepeating(int idFlags); /** * Usually the display player. */ mobj_t *S_GetListenerMobj(void); /** * Draws debug information on-screen. */ void S_Drawer(void); #ifdef __cplusplus } // extern "C" #endif #endif // DENG_AUDIO_S_MAIN_H doomsday-stable-1.15.7/doomsday/client/include/audio/sys_audio.h0000664000175000017500000000251112641367670024164 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * sys_audio.h: OS Specific Audio Drivers * * Not included in de_system.h because this is only needed by the * Sfx/Mus modules. */ #ifndef __DOOMSDAY_SYSTEM_AUDIO_H__ #define __DOOMSDAY_SYSTEM_AUDIO_H__ #include "api_audiod.h" #include "api_audiod_sfx.h" #include "api_audiod_mus.h" #include "sys_audiod_dummy.h" #ifndef DENG_DISABLE_SDLMIXER # include "sys_audiod_sdlmixer.h" #endif #endif doomsday-stable-1.15.7/doomsday/client/include/audio/sys_audiod_sdlmixer.h0000664000175000017500000000262112641367670026241 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * sys_audiod_sdlmixer.h: Default Audio Driver. * * Uses SDL_mixer, for SFX, Ext and Mus interfaces. */ #ifndef __DOOMSDAY_SYSTEM_AUDIO_SDLMIXER_H__ #define __DOOMSDAY_SYSTEM_AUDIO_SDLMIXER_H__ #include #include "api_audiod.h" #include "api_audiod_sfx.h" #include "api_audiod_mus.h" DENG_EXTERN_C audiodriver_t audiod_sdlmixer; DENG_EXTERN_C audiointerface_sfx_t audiod_sdlmixer_sfx; DENG_EXTERN_C audiointerface_music_t audiod_sdlmixer_music; #endif doomsday-stable-1.15.7/doomsday/client/include/audio/s_environ.h0000664000175000017500000000360312641367670024172 0ustar jaakkojaakko/** @file s_environ.h Audio environment management. * @ingroup audio * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_SOUND_ENVIRON #define DENG_SOUND_ENVIRON #include enum AudioEnvironmentId { AE_NONE = -1, AE_FIRST = 0, AE_METAL = AE_FIRST, AE_ROCK, AE_WOOD, AE_CLOTH, NUM_AUDIO_ENVIRONMENTS }; /** * Defines the properties of an audio environment. */ struct AudioEnvironment { char const name[9]; ///< Environment type name. int volumeMul; int decayMul; int dampingMul; }; /** * Lookup the symbolic name of the identified audio environment. */ char const *S_AudioEnvironmentName(AudioEnvironmentId id); /** * Lookup the identified audio environment. */ AudioEnvironment const &S_AudioEnvironment(AudioEnvironmentId id); /** * Lookup the audio environment associated with material @a uri. If no environment * is defined then @c AE_NONE is returned. */ AudioEnvironmentId S_AudioEnvironmentId(de::Uri const *uri); #endif // DENG_SOUND_ENVIRON doomsday-stable-1.15.7/doomsday/client/include/audio/sys_audiod_dummy.h0000664000175000017500000000232112641367670025542 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * sys_audiod_dummy.h: Dummy Music Driver. * * Used in dedicated server mode. */ #ifndef __DOOMSDAY_SYSTEM_AUDIO_DUMMY_H__ #define __DOOMSDAY_SYSTEM_AUDIO_DUMMY_H__ #include #include "api_audiod.h" #include "api_audiod_sfx.h" DENG_EXTERN_C audiodriver_t audiod_dummy; DENG_EXTERN_C audiointerface_sfx_t audiod_dummy_sfx; #endif doomsday-stable-1.15.7/doomsday/client/include/Lumobj0000664000175000017500000000003312641367670022063 0ustar jaakkojaakko#include "render/lumobj.h" doomsday-stable-1.15.7/doomsday/client/include/de_filesys.h0000664000175000017500000000257312641367670023222 0ustar jaakkojaakko/** * @file de_filesys.h * * File system. @ingroup fs * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDENG_FILESYS_H #define LIBDENG_FILESYS_H #include "dd_types.h" #include #include "resource/manifest.h" #include #include #include #include #include #include #include "api_filesys.h" #include #endif /* LIBDENG_FILESYS_H */ doomsday-stable-1.15.7/doomsday/client/include/color.h0000664000175000017500000000364712641367670022215 0ustar jaakkojaakko/** * @file color.h Color * * @author Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_DATA_COLOR_H #define LIBDENG_DATA_COLOR_H #ifdef __cplusplus extern "C" { #endif /** * ColorRawf. Color Raw (f)loating point. Is intended as a handy POD * structure for easy manipulation of four component, floating point * color plus alpha value sets. * * @ingroup data */ typedef struct ColorRawf_s { union { // Straight RGBA vector representation. float rgba[4]; // Hybrid RGB plus alpha component representation. struct { float rgb[3]; float alpha; }; // Component-wise representation. struct { float red; float green; float blue; float _alpha; }; }; #ifdef __cplusplus ColorRawf_s(float r = 0.f, float g = 0.f, float b = 0.f, float a = 0.f) : red(r), green(g), blue(b), _alpha(a) {} #endif } ColorRawf; float ColorRawf_AverageColor(ColorRawf* color); float ColorRawf_AverageColorMulAlpha(ColorRawf* color); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_DATA_COLOR_H doomsday-stable-1.15.7/doomsday/client/include/CommandBinding0000664000175000017500000000003712641367670023510 0ustar jaakkojaakko#include "ui/commandbinding.h" doomsday-stable-1.15.7/doomsday/client/include/Surface0000664000175000017500000000003312641367670022223 0ustar jaakkojaakko#include "world/surface.h" doomsday-stable-1.15.7/doomsday/client/include/ConvexSubspace0000664000175000017500000000004212641367670023563 0ustar jaakkojaakko#include "world/convexsubspace.h" doomsday-stable-1.15.7/doomsday/client/include/Sprite0000664000175000017500000000003512641367670022103 0ustar jaakkojaakko#include "resource/sprite.h" doomsday-stable-1.15.7/doomsday/client/include/m_decomp64.h0000664000175000017500000000201512641367670023020 0ustar jaakkojaakko/** @file * * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * m_decomp64.h: Decompression algorithm, used with various lumps of * DOOM64 data. */ #ifndef __M_DECOMPRESS64_H__ #define __M_DECOMPRESS64_H__ void M_Decompress64(byte* dst, const byte* src); #endif doomsday-stable-1.15.7/doomsday/client/include/Shard0000664000175000017500000000003212641367670021673 0ustar jaakkojaakko#include "render/shard.h" doomsday-stable-1.15.7/doomsday/client/include/Games0000664000175000017500000000002312641367670021666 0ustar jaakkojaakko#include "games.h" doomsday-stable-1.15.7/doomsday/client/include/DrawList0000664000175000017500000000003512641367670022366 0ustar jaakkojaakko#include "render/drawlist.h" doomsday-stable-1.15.7/doomsday/client/include/library.h0000664000175000017500000000767312641367670022546 0ustar jaakkojaakko/** * @file library.h * Dynamic libraries. @ingroup base * * These functions provide roughly the same functionality as the ltdl library. * Since the ltdl library appears to be broken on Mac OS X, these will be used * instead when loading plugin libraries. * * During startup Doomsday loads multiple game plugins. However, only one can * exist in memory at a time because they contain many of the same globally * visible symbols. When a game is started, all game plugins are first released * from memory after which the chosen game plugin is reloaded (see * Library_ReleaseGames()). * * @todo Implement and use this class for Windows. * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_SYSTEM_UTILS_DYNAMIC_LIBRARY_H #define LIBDENG_SYSTEM_UTILS_DYNAMIC_LIBRARY_H #ifdef __cplusplus extern "C" { #endif struct library_s; // The library instance (opaque). typedef struct library_s Library; /** * Initializes the library loader. */ void Library_Init(void); /** * Release all resources associated with dynamic libraries. Must be called * when shutting down the engine. */ void Library_Shutdown(void); /** * Returns the latest error message. */ const char* Library_LastError(void); /** * Provides the library with the engine's public APIs. * * @param lib Library instance. */ void Library_PublishAPIs(Library *lib); /** * Closes the library handles of all game plugins. The library will be * reopened automatically when needed. */ void Library_ReleaseGames(void); /** * Looks for dynamic libraries and calls @a func for each one. * * Arguments passed to the callback function @a func: * - @a libraryFile is a pointer to a de::LibraryFile instance. * - @a fileName is the name of the library file, including extension. * - @a absPath is the absolute (non-native) path of the file. * - @a data is the @a data from the caller. * * @return If all available libraries were iterated, returns 0. If @a func * returns a non-zero value to indicate aborting the iteration at some point, * that value is returned instead. */ int Library_IterateAvailableLibraries(int (*func)(void *libraryFile, const char* fileName, const char* absPath, void* data), void* data); /** * Loads a dynamic library. * * @param filePath Absolute path of the library to open. */ Library* Library_New(const char* filePath); void Library_Delete(Library* lib); /** * Returns the type identifier of the library. * @see de::Library * * @param lib Library instance. * * @return Type identifier string, e.g., "deng-plugin/game". */ const char* Library_Type(const Library* lib); /** * Looks up a symbol from the library. * * @param lib Library instance. * @param symbolName Name of the symbol. * * @return @c NULL if the symbol is not defined. Otherwise the address of * the symbol. */ void* Library_Symbol(Library* lib, const char* symbolName); #ifdef __cplusplus } // extern "C" #endif #ifdef __cplusplus #include de::LibraryFile& Library_File(Library* lib); #endif #endif /* LIBDENG_SYSTEM_UTILS_DYNAMIC_LIBRARY_H */ doomsday-stable-1.15.7/doomsday/client/include/FontScheme0000664000175000017500000000004112641367670022665 0ustar jaakkojaakko#include "resource/fontscheme.h" doomsday-stable-1.15.7/doomsday/client/include/Contact0000664000175000017500000000003312641367670022226 0ustar jaakkojaakko#include "world/contact.h" doomsday-stable-1.15.7/doomsday/client/include/m_misc.h0000664000175000017500000001013712641367670022336 0ustar jaakkojaakko/** @file m_misc.h * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * Miscellanous Routines. */ #ifndef LIBDENG_M_MISC_H #define LIBDENG_M_MISC_H #include #include "dd_types.h" #include #include #ifdef __cplusplus extern "C" { #endif struct aaboxd_s; /** * Reads x bits from the source stream and writes them to out. * * \warning Output buffer must be large enough to hold at least @a numBits! * * @param numBits Number of bits to be read. * @param src Current position in the source stream. * @param cb Current byte. Used for tracking the current byte being read. * @param out Read bits are ouput here. */ void M_ReadBits(uint numBits, const uint8_t** src, uint8_t* cb, uint8_t* out); // Text utilities. void M_WriteCommented(FILE* file, const char* text); void M_WriteTextEsc(FILE* file, const char* text); /** * Check the spatial relationship between the given box and a partitioning line. * * @param box Box being tested. * @param linePoint Point on the line. * @param lineDirection Direction of the line (slope). * * @return @c <0= bbox is wholly on the left side. * @c 0= line intersects bbox. * @c >0= bbox wholly on the right side. */ int M_BoxOnLineSide(const struct aaboxd_s* box, double const linePoint[2], double const lineDirection[2]); int M_BoxOnLineSide_FixedPrecision(const fixed_t box[], const fixed_t linePoint[], const fixed_t lineDirection[]); /** * Check the spatial relationship between the given box and a partitioning line. * * An alternative version of M_BoxOnLineSide() which allows specifying many of the * intermediate values used in the calculation a priori for performance reasons. * * @param box Box being tested. * @param linePoint Point on the line. * @param lineDirection Direction of the line (slope). * @param linePerp Perpendicular distance of the line. * @param lineLength Length of the line. * @param epsilon Points within this distance will be considered equal. * * @return @c <0= bbox is wholly on the left side. * @c 0= line intersects bbox. * @c >0= bbox wholly on the right side. */ int M_BoxOnLineSide2(const struct aaboxd_s* box, double const linePoint[2], double const lineDirection[2], double linePerp, double lineLength, double epsilon); typedef struct trigger_s { timespan_t duration; timespan_t accum; } trigger_t; /** * Advances time and return true if the trigger is triggered. * * @param trigger Time trigger. * @param advanceTime Amount of time to advance the trigger. * * @return @c true, if the trigger has accumulated enough time * to fill the trigger's time threshold. */ dd_bool M_RunTrigger(trigger_t* trigger, timespan_t advanceTime); /** * Checks if the trigger will trigger after @a advanceTime seconds. * The trigger itself is not modified in any way. * * @param trigger Time trigger. * @param advanceTime Amount of time to advance the trigger. * * @return @c true, if the trigger will accumulate enough time after @a advanceTime * to fill the trigger's time threshold. */ dd_bool M_CheckTrigger(const trigger_t* trigger, timespan_t advanceTime); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_M_MISC_H doomsday-stable-1.15.7/doomsday/client/include/de_resource.h0000664000175000017500000000272412641367670023371 0ustar jaakkojaakko/** @file de_resource.h Resource Subsystem. * @ingroup resource * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_RESOURCE_SUBSYSTEM_H #define DENG_RESOURCE_SUBSYSTEM_H #include #include "resource/abstractfont.h" #include "resource/compositetexture.h" #include "resource/rawtexture.h" #include "resource/sprite.h" #ifdef __CLIENT__ # include "resource/materialvariantspec.h" #endif #ifdef __cplusplus # include "resource/patch.h" # include "resource/texturemanifest.h" # include # include #endif #include "api_resource.h" #endif // DENG_RESOURCE_SUBSYSTEM_H doomsday-stable-1.15.7/doomsday/client/include/MaterialAnimator0000664000175000017500000000004712641367670024071 0ustar jaakkojaakko#include "resource/materialanimator.h" doomsday-stable-1.15.7/doomsday/client/include/LightDecoration0000664000175000017500000000004412641367670023714 0ustar jaakkojaakko#include "render/lightdecoration.h" doomsday-stable-1.15.7/doomsday/client/include/BspNode0000664000175000017500000000003312641367670022165 0ustar jaakkojaakko#include "world/bspnode.h" doomsday-stable-1.15.7/doomsday/client/include/SkyFixEdge0000664000175000017500000000003712641367670022641 0ustar jaakkojaakko#include "render/skyfixedge.h" doomsday-stable-1.15.7/doomsday/client/include/versioninfo.h0000664000175000017500000000462512641367670023435 0ustar jaakkojaakko#ifndef LIBDENG_VERSIOINFO_H #define LIBDENG_VERSIOINFO_H #include #include #include #include "dd_version.h" struct VersionInfo { int major; int minor; int revision; int patch; int build; /** * Version information. */ VersionInfo() : patch(0), build(de::Time().asBuildNumber()) { parseVersionString(DOOMSDAY_VERSION_BASE); #ifdef DOOMSDAY_BUILD_TEXT build = de::String(DOOMSDAY_BUILD_TEXT).toInt(); #endif } VersionInfo(de::String const &version, int buildNumber) : build(buildNumber) { parseVersionString(version); } QString base() const { return QString("%1.%2.%3").arg(major).arg(minor).arg(revision); } QString asText() const { if(patch > 0) { return base() + QString("-%1 (Build %2)").arg(patch).arg(build); } return base() + QString(" (Build %1)").arg(build); } void parseVersionString(de::String const &version) { major = minor = revision = patch = 0; QStringList const parts = version.split('.'); if(parts.size() > 0) { major = parts[0].toInt(); } if(parts.size() > 1) { minor = parts[1].toInt(); } if(parts.size() > 2) { if(parts[2].contains('-')) { QStringList rev = parts[2].split('-'); revision = rev[0].toInt(); patch = rev[1].toInt(); } else { revision = parts[2].toInt(); } } } bool operator < (VersionInfo const &other) const { if(major == other.major) { if(minor == other.minor) { if(revision == other.revision) { return build < other.build; } return revision < other.revision; } return minor < other.minor; } return major < other.major; } bool operator == (VersionInfo const &other) const { return major == other.major && minor == other.minor && revision == other.revision && build == other.build; } bool operator > (VersionInfo const &other) const { return !(*this < other || *this == other); } }; #endif // LIBDENG_VERSIOINFO_H doomsday-stable-1.15.7/doomsday/client/include/tab_anorms.h0000664000175000017500000001302012641367670023206 0ustar jaakkojaakko/** @file *\section License * License: GPL * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © id Software, Inc. * * From Quake. */ #ifdef WIN32_MSVC #pragma warning(disable:4305) #endif {-0.525731, 0.000000, 0.850651}, {-0.442863, 0.238856, 0.864188}, {-0.295242, 0.000000, 0.955423}, {-0.309017, 0.500000, 0.809017}, {-0.162460, 0.262866, 0.951056}, {0.000000, 0.000000, 1.000000}, {0.000000, 0.850651, 0.525731}, {-0.147621, 0.716567, 0.681718}, {0.147621, 0.716567, 0.681718}, {0.000000, 0.525731, 0.850651}, {0.309017, 0.500000, 0.809017}, {0.525731, 0.000000, 0.850651}, {0.295242, 0.000000, 0.955423}, {0.442863, 0.238856, 0.864188}, {0.162460, 0.262866, 0.951056}, {-0.681718, 0.147621, 0.716567}, {-0.809017, 0.309017, 0.500000}, {-0.587785, 0.425325, 0.688191}, {-0.850651, 0.525731, 0.000000}, {-0.864188, 0.442863, 0.238856}, {-0.716567, 0.681718, 0.147621}, {-0.688191, 0.587785, 0.425325}, {-0.500000, 0.809017, 0.309017}, {-0.238856, 0.864188, 0.442863}, {-0.425325, 0.688191, 0.587785}, {-0.716567, 0.681718, -0.147621}, {-0.500000, 0.809017, -0.309017}, {-0.525731, 0.850651, 0.000000}, {0.000000, 0.850651, -0.525731}, {-0.238856, 0.864188, -0.442863}, {0.000000, 0.955423, -0.295242}, {-0.262866, 0.951056, -0.162460}, {0.000000, 1.000000, 0.000000}, {0.000000, 0.955423, 0.295242}, {-0.262866, 0.951056, 0.162460}, {0.238856, 0.864188, 0.442863}, {0.262866, 0.951056, 0.162460}, {0.500000, 0.809017, 0.309017}, {0.238856, 0.864188, -0.442863}, {0.262866, 0.951056, -0.162460}, {0.500000, 0.809017, -0.309017}, {0.850651, 0.525731, 0.000000}, {0.716567, 0.681718, 0.147621}, {0.716567, 0.681718, -0.147621}, {0.525731, 0.850651, 0.000000}, {0.425325, 0.688191, 0.587785}, {0.864188, 0.442863, 0.238856}, {0.688191, 0.587785, 0.425325}, {0.809017, 0.309017, 0.500000}, {0.681718, 0.147621, 0.716567}, {0.587785, 0.425325, 0.688191}, {0.955423, 0.295242, 0.000000}, {1.000000, 0.000000, 0.000000}, {0.951056, 0.162460, 0.262866}, {0.850651, -0.525731, 0.000000}, {0.955423, -0.295242, 0.000000}, {0.864188, -0.442863, 0.238856}, {0.951056, -0.162460, 0.262866}, {0.809017, -0.309017, 0.500000}, {0.681718, -0.147621, 0.716567}, {0.850651, 0.000000, 0.525731}, {0.864188, 0.442863, -0.238856}, {0.809017, 0.309017, -0.500000}, {0.951056, 0.162460, -0.262866}, {0.525731, 0.000000, -0.850651}, {0.681718, 0.147621, -0.716567}, {0.681718, -0.147621, -0.716567}, {0.850651, 0.000000, -0.525731}, {0.809017, -0.309017, -0.500000}, {0.864188, -0.442863, -0.238856}, {0.951056, -0.162460, -0.262866}, {0.147621, 0.716567, -0.681718}, {0.309017, 0.500000, -0.809017}, {0.425325, 0.688191, -0.587785}, {0.442863, 0.238856, -0.864188}, {0.587785, 0.425325, -0.688191}, {0.688191, 0.587785, -0.425325}, {-0.147621, 0.716567, -0.681718}, {-0.309017, 0.500000, -0.809017}, {0.000000, 0.525731, -0.850651}, {-0.525731, 0.000000, -0.850651}, {-0.442863, 0.238856, -0.864188}, {-0.295242, 0.000000, -0.955423}, {-0.162460, 0.262866, -0.951056}, {0.000000, 0.000000, -1.000000}, {0.295242, 0.000000, -0.955423}, {0.162460, 0.262866, -0.951056}, {-0.442863, -0.238856, -0.864188}, {-0.309017, -0.500000, -0.809017}, {-0.162460, -0.262866, -0.951056}, {0.000000, -0.850651, -0.525731}, {-0.147621, -0.716567, -0.681718}, {0.147621, -0.716567, -0.681718}, {0.000000, -0.525731, -0.850651}, {0.309017, -0.500000, -0.809017}, {0.442863, -0.238856, -0.864188}, {0.162460, -0.262866, -0.951056}, {0.238856, -0.864188, -0.442863}, {0.500000, -0.809017, -0.309017}, {0.425325, -0.688191, -0.587785}, {0.716567, -0.681718, -0.147621}, {0.688191, -0.587785, -0.425325}, {0.587785, -0.425325, -0.688191}, {0.000000, -0.955423, -0.295242}, {0.000000, -1.000000, 0.000000}, {0.262866, -0.951056, -0.162460}, {0.000000, -0.850651, 0.525731}, {0.000000, -0.955423, 0.295242}, {0.238856, -0.864188, 0.442863}, {0.262866, -0.951056, 0.162460}, {0.500000, -0.809017, 0.309017}, {0.716567, -0.681718, 0.147621}, {0.525731, -0.850651, 0.000000}, {-0.238856, -0.864188, -0.442863}, {-0.500000, -0.809017, -0.309017}, {-0.262866, -0.951056, -0.162460}, {-0.850651, -0.525731, 0.000000}, {-0.716567, -0.681718, -0.147621}, {-0.716567, -0.681718, 0.147621}, {-0.525731, -0.850651, 0.000000}, {-0.500000, -0.809017, 0.309017}, {-0.238856, -0.864188, 0.442863}, {-0.262866, -0.951056, 0.162460}, {-0.864188, -0.442863, 0.238856}, {-0.809017, -0.309017, 0.500000}, {-0.688191, -0.587785, 0.425325}, {-0.681718, -0.147621, 0.716567}, {-0.442863, -0.238856, 0.864188}, {-0.587785, -0.425325, 0.688191}, {-0.309017, -0.500000, 0.809017}, {-0.147621, -0.716567, 0.681718}, {-0.425325, -0.688191, 0.587785}, {-0.162460, -0.262866, 0.951056}, {0.442863, -0.238856, 0.864188}, {0.162460, -0.262866, 0.951056}, {0.309017, -0.500000, 0.809017}, {0.147621, -0.716567, 0.681718}, {0.000000, -0.525731, 0.850651}, {0.425325, -0.688191, 0.587785}, {0.587785, -0.425325, 0.688191}, {0.688191, -0.587785, 0.425325}, {-0.955423, 0.295242, 0.000000}, {-0.951056, 0.162460, 0.262866}, {-1.000000, 0.000000, 0.000000}, {-0.850651, 0.000000, 0.525731}, {-0.955423, -0.295242, 0.000000}, {-0.951056, -0.162460, 0.262866}, {-0.864188, 0.442863, -0.238856}, {-0.951056, 0.162460, -0.262866}, {-0.809017, 0.309017, -0.500000}, {-0.864188, -0.442863, -0.238856}, {-0.951056, -0.162460, -0.262866}, {-0.809017, -0.309017, -0.500000}, {-0.681718, 0.147621, -0.716567}, {-0.681718, -0.147621, -0.716567}, {-0.850651, 0.000000, -0.525731}, {-0.688191, 0.587785, -0.425325}, {-0.587785, 0.425325, -0.688191}, {-0.425325, 0.688191, -0.587785}, {-0.425325, -0.688191, -0.587785}, {-0.587785, -0.425325, -0.688191}, {-0.688191, -0.587785, -0.425325}, doomsday-stable-1.15.7/doomsday/client/include/de_edit.h0000664000175000017500000000207112641367670022462 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * de_edit.h: Editor subsystems */ #ifndef __DOOMSDAY_EDITOR__ #define __DOOMSDAY_EDITOR__ #ifdef __CLIENT__ # include "edit_bias.h" #endif #include "edit_map.h" #include "api_mapedit.h" #endif doomsday-stable-1.15.7/doomsday/client/include/de_network.h0000664000175000017500000000321512641367670023227 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * Network Subsystem. */ #ifndef LIBDENG_NETWORK #define LIBDENG_NETWORK #include "network/net_main.h" #include "network/net_event.h" #include "network/net_msg.h" #include "network/net_buf.h" #include "network/protocol.h" #include "network/monitor.h" #ifdef __SERVER__ # include "server/sv_def.h" # include "server/sv_frame.h" # include "server/sv_pool.h" # include "server/sv_sound.h" # include "server/sv_missile.h" # include "server/sv_infine.h" #endif #ifdef __CLIENT__ # include "api_client.h" # include "network/net_demo.h" # include "client/cl_def.h" # include "client/cl_player.h" # include "client/cl_mobj.h" # include "client/cl_frame.h" # include "client/cl_sound.h" # include "client/cl_world.h" # include "client/cl_infine.h" #endif #endif /* LIBDENG_NETWORK */ doomsday-stable-1.15.7/doomsday/client/include/BiasTracker0000664000175000017500000000004012641367670023023 0ustar jaakkojaakko#include "render/biastracker.h" doomsday-stable-1.15.7/doomsday/client/include/MaterialVariantSpec0000664000175000017500000000005212641367670024532 0ustar jaakkojaakko#include "resource/materialvariantspec.h" doomsday-stable-1.15.7/doomsday/client/include/render/0000775000175000017500000000000012641367670022173 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/render/rend_model.h0000664000175000017500000001026612641367670024461 0ustar jaakkojaakko/** @file rend_model.h Model renderer (v2.1). * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_RENDER_MODEL_H #define CLIENT_RENDER_MODEL_H #include "ModelDef" #include "rend_main.h" #include #include class TextureVariantSpec; struct vissprite_t; /// Absolute maximum number of vertices per submodel supported by this module. #define RENDER_MAX_MODEL_VERTS 16192 /** * @todo Split this large inflexible structure into logical subcomponent pieces. * @ingroup render */ struct drawmodelparams_t { // Animation, frame interpolation: ModelDef *mf; ModelDef *nextMF; de::dfloat inter; dd_bool alwaysInterpolate; de::dint id; ///< For a unique skin offset. de::dint selector; // Appearance: de::dint flags; ///< Mobj flags. de::dint tmap; // Shiney texture mapping: de::dfloat shineYawOffset; de::dfloat shinePitchOffset; dd_bool shineTranslateWithViewerPos; dd_bool shinepspriteCoordSpace; ///< Use the psprite coordinate space hack. }; /// @ingroup render struct drawmodel2params_t { struct mobj_s const *object; de::ModelDrawable const *model; de::ModelDrawable::Animator const *animator; }; DENG_EXTERN_C de::dbyte useModels; DENG_EXTERN_C de::dint modelLight; DENG_EXTERN_C de::dint frameInter; DENG_EXTERN_C de::dfloat modelAspectMod; DENG_EXTERN_C de::dint mirrorHudModels; DENG_EXTERN_C de::dint modelShinyMultitex; DENG_EXTERN_C de::dfloat modelSpinSpeed; DENG_EXTERN_C de::dint maxModelDistance; DENG_EXTERN_C de::dfloat rendModelLOD; DENG_EXTERN_C de::dbyte precacheSkins; /** * Registers the console commands and variables used by this module. */ void Rend_ModelRegister(); /** * Initialize this module. */ void Rend_ModelInit(); /** * Shuts down this module. */ void Rend_ModelShutdown(); /** * Expand the render buffer to accommodate rendering models containing at most * this number of vertices. * * @note It is not actually necessary to call this. The vertex buffer will be * enlarged automatically at render time to accommodate a given model so * long as it contains less than RENDER_MAX_MODEL_VERTS. If not the model * will simply not be rendered at all. * * @note Buffer reallocation is deferred until necessary, so repeatedly calling * this routine during initialization is OK. * * @param numVertices New maximum number of vertices we'll be required to handle. * * @return @c true= successfully expanded. May fail if @a numVertices is larger * than RENDER_MAX_MODEL_VERTS. */ bool Rend_ModelExpandVertexBuffers(de::duint numVertices); /** * Lookup the texture specification for diffuse model skins. * * @param noCompression @c true= disable texture compression. * @return Specification to be used when preparing such textures. */ TextureVariantSpec const &Rend_ModelDiffuseTextureSpec(bool noCompression); /** * Lookup the texture specification for shiny model skins. * * @return Specification to be used when preparing such textures. */ TextureVariantSpec const &Rend_ModelShinyTextureSpec(); /** * Render all the submodels of a model. */ void Rend_DrawModel(vissprite_t const &spr); /** * Render a GL2 model. * * @param parms Parameters for the draw operation. */ void Rend_DrawModel2(vissprite_t const &spr); #endif // CLIENT_RENDER_MODEL_H doomsday-stable-1.15.7/doomsday/client/include/render/rend_halo.h0000664000175000017500000000544712641367670024311 0ustar jaakkojaakko/** @file rend_halo.h Halos and Lens Flares. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_RENDER_HALO_H #define DENG_CLIENT_RENDER_HALO_H #include #include "TextureVariantSpec" DENG_EXTERN_C int haloOccludeSpeed; DENG_EXTERN_C int haloMode, haloRealistic, haloBright, haloSize; DENG_EXTERN_C float haloFadeMax, haloFadeMin, minHaloSize; void H_Register(); /** * Returns the texture variant specification for halos. */ TextureVariantSpec const &Rend_HaloTextureSpec(); void H_SetupState(bool dosetup); /** * Render a halo. * * The caller must check that @c sourcevis, really has a ->light! (? -jk) * * @param origin Origin of the halo in map space. * @param size The precalculated radius of the primary halo. * @param tex Texture to use for the halo. * @param color Color for the halo. * @param distanceToViewer Distance to viewer. Used for fading the halo when far away. * @param occlusionFactor How much the halo is occluded by something. * @param brightnessFactor Overall brightness factor. * @param viewXOffset Horizontal offset for the halo (in viewspace, relative to @a x, @a y, @a z). * @param primary Type of halo: * - @c true: the primary halo. * - @c false: the secondary halo. Won't be clipped or occluded * by anything; they're drawn after everything else, * during a separate pass. The caller must setup the rendering state. * @param viewRelativeRotate @c true: Halo rotates in relation to its viewspace origin. * * @return @c true, iff a halo was rendered. */ bool H_RenderHalo(de::Vector3d const &origin, float size, DGLuint tex, de::Vector3f const &color, coord_t distanceToViewer, float occlusionFactor, float brightnessFactor, float viewXOffset, bool primary, bool viewRelativeRotate); // Console commands. D_CMD(FlareConfig); #endif // DENG_CLIENT_RENDER_HALO_H doomsday-stable-1.15.7/doomsday/client/include/render/walledge.h0000664000175000017500000000606712641367670024141 0ustar jaakkojaakko/** @file render/walledge.h Wall Edge Geometry. * * @authors Copyright © 2011-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RENDER_WALLEDGE #define DENG_RENDER_WALLEDGE #include #include #include #include "Line" #include "WallSpec" #include "TriangleStripBuilder" #include "IHPlane" class Surface; /// Maximum number of intercepts in a WallEdge. #define WALLEDGE_MAX_INTERCEPTS 64 namespace de { class HEdge; /** * Helper/utility class intended to simplify the process of generating * sections of wall geometry from a map Line segment. * * @ingroup world */ class WallEdge : public WorldEdge { DENG2_NO_COPY (WallEdge) DENG2_NO_ASSIGN(WallEdge) public: /// Invalid range geometry was found during prepare() @ingroup errors DENG2_ERROR(InvalidError); class Event : public WorldEdge::Event, public IHPlane::IIntercept { public: Event(WallEdge &owner, double distance = 0); bool operator < (Event const &other) const; double distance() const; Vector3d origin() const; private: DENG2_PRIVATE(d) }; typedef QList Events; public: /** * @param spec Geometry specification for the wall section. A copy is made. * * @param hedge Assumed to have a mapped LineSideSegment with sections. */ WallEdge(WallSpec const &spec, HEdge &hedge, int edge); inline Event const &operator [] (EventIndex index) const { return at(index); } Vector3d const &pOrigin() const; Vector3d const &pDirection() const; Vector2f materialOrigin() const; Vector3f normal() const; WallSpec const &spec() const; LineSide &mapLineSide() const; coord_t mapLineSideOffset() const; /// Implement IEdge. bool isValid() const; /// Implement IEdge. Event const &first() const; /// Implement IEdge. Event const &last() const; int divisionCount() const; EventIndex firstDivision() const; EventIndex lastDivision() const; inline Event const &bottom() const { return first(); } inline Event const &top() const { return last(); } Events const &events() const; Event const &at(EventIndex index) const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_RENDER_WALLEDGE doomsday-stable-1.15.7/doomsday/client/include/render/ilightsource.h0000664000175000017500000000540012641367670025044 0ustar jaakkojaakko/** @file ilightsource.h Interface for a point light source. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_ILIGHTSOURCE_H #define DENG_CLIENT_ILIGHTSOURCE_H #include #include /** * Interface for a light source. * * All sources of light should implement this interface. Through it, various * parts of the rendering subsystem can know where and what kind of light this * is. * * @ingroup render */ class ILightSource { public: /** * Unique identifier of the source. This can be used to uniquely identify a * source of light across multiple frames. */ typedef de::duint32 LightId; /** * RGB color of the emitted light. */ typedef de::Vector3f Colorf; public: virtual ~ILightSource() {} virtual LightId lightSourceId() const = 0; /** * Returns the color of the emitted light. The intensity of the light must * not be factored into the color values, but is instead returned separately * by lightSourceIntensity(). */ virtual Colorf lightSourceColorf() const = 0; /** * Returns the intensity of the light. * * @param viewPoint World point from where the light is being observed if * the intensity may vary depending on the relative direction * and/or position of the viewer. */ virtual de::dfloat lightSourceIntensity(de::Vector3d const &viewPoint) const = 0; }; /** * Interface for a point light source. * * @ingroup render */ class IPointLightSource : public ILightSource { public: typedef de::Vector3d Origin; public: virtual ~IPointLightSource() {} /** * Returns the position of the light source, in map units. */ virtual Origin lightSourceOrigin() const = 0; /** * Returns the radius of the emitter itself, in map units. A radius of * zero would mean that the light emitter is an infinitely small point. */ virtual de::dfloat lightSourceRadius() const = 0; }; #endif // DENG_CLIENT_ILIGHTSOURCE_H doomsday-stable-1.15.7/doomsday/client/include/render/biassource.h0000664000175000017500000001646012641367670024512 0ustar jaakkojaakko/** @file biassource.h Shadow Bias (light) source. * * @authors Copyright © 2005-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_RENDER_SHADOWBIAS_SOURCE_H #define DENG_RENDER_SHADOWBIAS_SOURCE_H #include #include #include #include #include #include "Grabbable" class BiasDigest; class BspLeaf; struct ded_light_s; // def_data.h /** * Infinite point light source in the Shadow Bias lighting model. * * Color and intensity change notifications are intended for "off-line" usage. */ class BiasSource : public Grabbable, public de::ISerializable { public: /* * Notified when the bias source is about to be deleted. */ DENG2_DEFINE_AUDIENCE(Deletion, void biasSourceBeingDeleted(BiasSource const &biasSource)) /* * Notified when the bias source intensity changes. */ DENG2_DEFINE_AUDIENCE(IntensityChange, void biasSourceIntensityChanged(BiasSource &biasSource, float oldIntensity)) /* * Notified when the bias source color changes. */ DENG2_DEFINE_AUDIENCE(ColorChange, void biasSourceColorChanged(BiasSource &biasSource, de::Vector3f const &oldColor, int changedComponents /*bit-field (0x1=Red, 0x2=Green, 0x4=Blue)*/)) public: /** * Construct a new bias light source. The Grabbable is locked automatically. * * @param origin Origin for the source in the map coordinate space. * @param intensity Light intensity (strength) multiplier. * @param color Light color strength factors. * @param minLight Minimum ambient light level [0..1]. * @param maxLight Maximum ambient light level [0..1]. */ BiasSource(de::Vector3d const &origin = de::Vector3d(), float intensity = 200, de::Vector3f const &color = de::Vector3f(1, 1, 1), float minLight = 0, float maxLight = 0); /** * Construct a new bias light source by duplicating @a other. * * @note The Grabbable state is @em not copied (default state is applied). */ BiasSource(BiasSource const &other); ~BiasSource(); /** * Construct a bias source initialized from a legacy light definition. */ static BiasSource fromDef(struct ded_light_s const &def); /** * Returns the origin of the source in the map coordinate space. The * OriginChange audience is notified whenever the origin changes. * * @see setOrigin() */ de::Vector3d const &origin() const; /** * Change the origin of the source in the map coordinate space. The * OriginChange audience is notified whenever the origin changes. * * @param newOrigin New origin coordinates to apply. * * @see origin() */ void setOrigin(de::Vector3d const &newOrigin); /** * Returns the map BSP leaf at the origin of the source (result cached). */ BspLeaf &bspLeafAtOrigin() const; /** * Returns the "primary" light intensity multiplier for the source. The * IntensityChange audience is notified whenever the intensity changes. * * @see setIntensity() */ float intensity() const; /** * Change the light intensity multiplier for the source. If changed the * source is marked and any affected surfaces will be updated at the * beginning of the @em next render frame. The IntensityChange audience is * notified whenever the intensity changes. * * @param newIntensity New intensity multiplier. * * @see intensity() */ BiasSource &setIntensity(float newIntensity); /** * Determine the effective light intensity for the source and factoring in * sector light level multipliers/scale-factors. */ float evaluateIntensity() const; /** * Returns the light color strength factors for the source. The ColorChange * audience is notified whenever the color changes. * * @see setColor() */ de::Vector3f const &color() const; /** * Change the light color strength factors for the source. If changed the * source is marked and any affected surfaces will be updated at the * beginning of the @em next render frame. The ColorChange audience is * notified whenever the color changes. * * @param newColor New color strength factors to apply. Note that this * value is first applified and then clamped so that all components are in * the range [0..1]. * * @see color() */ BiasSource &setColor(de::Vector3f const &newColor); /** * Returns the ambient light level threshold for the source. * * @param minLight The minimal light level is written here. * @param maxLight The maximal light level is written here. * * @see setLightLevels() */ void lightLevels(float &minLight, float &maxLight) const; /** * Change the ambient light level threshold for the source. Note that both * values are first clamped to the range [0..1]. * * @param newMinLight New minimal light level to apply. * @param newMaxLight New maximal light level to apply. * * @see lightLevels() */ BiasSource &setLightLevels(float newMinLight, float newMaxLight); /** * Returns the time in milliseconds when the source was last updated. */ uint lastUpdateTime() const; /** * Manually mark the source as needing a full update. Note that the actual * update job is deferred until the beginning of the @em next render frame. * * To be called when a surface which is affected by this source has moved. */ void forceUpdate(); /** * Analyze the bias source to determine whether any lighting-contributor * trackers must be informed. * * @param changes Digest in which to record changes. * @param digestIndex Index to use when updating the digest. * @param currentTime Current time in milliseconds. Will be used to mark * the bias source (if changes are found) to facilitate * interpolation (performed by BiasIllum when lighting * is next sampled for a given map point). * * @see BiasTracker */ bool trackChanges(BiasDigest &changes, uint digestIndex, uint currentTime); // Implements ISerializable. void operator >> (de::Writer &to) const; void operator << (de::Reader &from); private: DENG2_PRIVATE(d) }; #endif // DENG_RENDER_SHADOWBIAS_SOURCE_H doomsday-stable-1.15.7/doomsday/client/include/render/blockmapvisual.h0000664000175000017500000000246612641367670025370 0ustar jaakkojaakko/** @file blockmapvisual.h Blockmap debug visualizer. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_RENDER_BLOCKMAPVISUAL_H #define DENG_CLIENT_RENDER_BLOCKMAPVISUAL_H #include DENG_EXTERN_C byte bmapShowDebug; ///< cvar DENG_EXTERN_C float bmapDebugSize; ///< cvar /** * Render the Blockmap debugging visual. */ DENG_EXTERN_C void Rend_BlockmapDebug(); #endif // DENG_CLIENT_RENDER_BLOCKMAPVISUAL_H doomsday-stable-1.15.7/doomsday/client/include/render/biastracker.h0000664000175000017500000000774112641367670024647 0ustar jaakkojaakko/** @file biastracker.h Shadow Bias illumination change tracker. * * @authors Copyright © 2005-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_RENDER_SHADOWBIAS_TRACKER_H #define DENG_RENDER_SHADOWBIAS_TRACKER_H #include #include #include "BiasIllum" class BiasDigest; class BiasSource; /** * Map point illumination tracker for the Shadow Bias lighting model. * * @ingroup render */ class BiasTracker { public: /// An unknown light contributor was referenced @ingroup errors DENG2_ERROR(UnknownContributorError); /// Maximum number of light contributors. static int const MAX_CONTRIBUTORS = BiasIllum::MAX_CONTRIBUTORS; public: /** * Construct a new bias illumination tracker. */ BiasTracker(); /** * Remove all light contributors. Existing contributions are put into a * "latent" state, so that if they are added again the contribution is then * re-activated and no lighting changes will occur (appears seamless). * * @see addContributor() */ void clearContributors(); /** * Add a new light contributor. After which lighting changes at the source * will be tracked and routed to map point illuminations when necessary * (i.e., when lighting is next evaluated for the point). * * All contributors are assigned a unique index (when added) that can be * used to reference it (and the source) later. * * @note Contributors with intensity less than @ref BiasIllum::MIN_INTENSITY * will be ignored (nothing will happen). * * @note At most @ref MAX_CONTRIBUTORS can contribute lighting. Once this * capacity is reached adding a new contributor will result in the weakest * contributor (i.e., smallest intensity when added) being dropped and it's * index assigned to the 'new' contributor. If the weakest is the new * contributor then nothing will happen. * * @param source Source of the light contribution. * @param intensity Strength of the contribution from the source. * * @see contributor() * * @return Index of the contributor otherwise @c -1 (if rejected). Note * that this index may be subsequently reassigned (see above notes). */ int addContributor(BiasSource *source, float intensity); /** * Returns the source of a light contributor by @a index. * * @see addContributor() */ BiasSource &contributor(int index) const; /** * Determine the earliest time in milliseconds that an affecting source * was changed/deleted. * * @see applyChanges() */ uint timeOfLatestContributorUpdate() const; /** * Interpret the bias change digest and schedule illumination updates as * necessary (deferred, does not block). * * @param changes Digest of all changes to apply in the tracker. */ void applyChanges(BiasDigest &changes); public: /// @todo The following API should be replaced ------------------------- byte activeContributors() const; byte changedContributions() const; void updateAllContributors(); void markIllumUpdateCompleted(); private: DENG2_PRIVATE(d) }; #endif // DENG_RENDER_SHADOWBIAS_TRACKER_H doomsday-stable-1.15.7/doomsday/client/include/render/vissprite.h0000664000175000017500000001500612641367670024376 0ustar jaakkojaakko/** @file vissprite.h Projected visible sprite ("vissprite") management. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_RENDER_VISSPRITE_H #define CLIENT_RENDER_VISSPRITE_H #include #include "render/billboard.h" #include "rend_model.h" #define MAXVISSPRITES 8192 /** * These constants are used as the type of vissprite. * @ingroup render */ enum visspritetype_t { VSPR_SPRITE, VSPR_MASKED_WALL, VSPR_MODEL, VSPR_MODEL_GL2, ///< GL2 model (de::ModelDrawable) VSPR_FLARE }; #define MAX_VISSPRITE_LIGHTS (10) /// @ingroup render struct VisEntityPose { de::Vector3d origin; de::dfloat topZ; ///< Global top Z coordinate (origin Z is the bottom). de::Vector3d srvo; ///< Short-range visual offset. coord_t distance; ///< Distance from viewer. de::dfloat yaw; de::dfloat extraYawAngle; de::dfloat yawAngleOffset; ///< @todo We do not need three sets of angles... de::dfloat pitch; de::dfloat extraPitchAngle; de::dfloat pitchAngleOffset; de::dfloat extraScale; bool viewAligned; bool mirrored; ///< If true the model will be mirrored about its Z axis (in model space). VisEntityPose() { de::zap(*this); } VisEntityPose(de::Vector3d const &origin_, de::Vector3d const &visOffset, bool viewAlign_ = false, de::dfloat topZ_ = 0, de::dfloat yaw_ = 0, de::dfloat yawAngleOffset_ = 0, de::dfloat pitch_ = 0, de::dfloat pitchAngleOffset_= 0) : origin(origin_) , topZ(topZ_) , srvo(visOffset) , distance(Rend_PointDist2D(origin_)) , yaw(yaw_) , extraYawAngle(0) , yawAngleOffset(yawAngleOffset_) , pitch(pitch_) , extraPitchAngle(0) , pitchAngleOffset(pitchAngleOffset_) , extraScale(0) , viewAligned(viewAlign_) , mirrored(false) {} inline coord_t midZ() const { return (origin.z + topZ) / 2; } de::Vector3d mid() const { return de::Vector3d(origin.x, origin.y, midZ()); } }; /// @ingroup render struct VisEntityLighting { de::Vector4f ambientColor; de::duint vLightListIdx; VisEntityLighting() { de::zap(*this); } VisEntityLighting(de::Vector4f const &ambientColor, de::duint lightListIndex) : ambientColor(ambientColor) , vLightListIdx(lightListIndex) {} }; /** * vissprite_t is a mobj or masked wall that will be drawn refresh. * @ingroup render */ struct vissprite_t { vissprite_t *prev; vissprite_t *next; visspritetype_t type; VisEntityPose pose; VisEntityLighting light; // An anonymous union for the data. union vissprite_data_u { drawspriteparams_t sprite; drawmaskedwallparams_t wall; drawmodelparams_t model; drawmodel2params_t model2; drawflareparams_t flare; } data; }; #define VS_SPRITE(v) (&((v)->data.sprite)) #define VS_WALL(v) (&((v)->data.wall)) #define VS_MODEL(v) (&((v)->data.model)) #define VS_MODEL2(v) (&((v)->data.model2)) #define VS_FLARE(v) (&((v)->data.flare)) void VisSprite_SetupSprite(vissprite_t *spr, VisEntityPose const &pose, VisEntityLighting const &light, de::dfloat secFloor, de::dfloat secCeil, de::dfloat floorClip, de::dfloat top, Material &material, bool matFlipS, bool matFlipT, blendmode_t blendMode, de::dint tClass, de::dint tMap, BspLeaf *bspLeafAtOrigin, bool floorAdjust, bool fitTop, bool fitBottom); void VisSprite_SetupModel(vissprite_t *spr, VisEntityPose const &pose, VisEntityLighting const &light, ModelDef *mf, ModelDef *nextMF, de::dfloat inter, de::dint id, de::dint selector, BspLeaf *bspLeafAtOrigin, de::dint mobjDDFlags, de::dint tmap, bool fullBright, bool alwaysInterpolate); /// @ingroup render enum vispspritetype_t { VPSPR_SPRITE, VPSPR_MODEL }; /// @ingroup render struct vispsprite_t { vispspritetype_t type; ddpsprite_t *psp; de::Vector3d origin; union vispsprite_data_u { struct vispsprite_sprite_s { BspLeaf *bspLeaf; de::dfloat alpha; dd_bool isFullBright; } sprite; struct vispsprite_model_s { BspLeaf *bspLeaf; coord_t topZ; ///< global top for silhouette clipping de::dint flags; ///< for color translation and shadow draw de::duint id; de::dint selector; de::dint pClass; ///< player class (used in translation) coord_t floorClip; dd_bool stateFullBright; dd_bool viewAligned; ///< @c true= Align to view plane. coord_t secFloor; coord_t secCeil; de::dfloat alpha; de::ddouble visOff[3]; ///< Last-minute offset to coords. dd_bool floorAdjust; ///< Allow moving sprite to match visible floor. ModelDef *mf; ModelDef *nextMF; de::dfloat yaw; de::dfloat pitch; de::dfloat pitchAngleOffset; de::dfloat yawAngleOffset; de::dfloat inter; ///< Frame interpolation, 0..1 } model; } data; }; DENG_EXTERN_C vissprite_t visSprites[MAXVISSPRITES], *visSpriteP; DENG_EXTERN_C vissprite_t visSprSortedHead; DENG_EXTERN_C vispsprite_t visPSprites[DDMAXPSPRITES]; /// To be called at the start of the current render frame to clear the vissprite list. void R_ClearVisSprites(); vissprite_t *R_NewVisSprite(visspritetype_t type); void R_SortVisSprites(); #endif // CLIENT_RENDER_VISSPRITE_H doomsday-stable-1.15.7/doomsday/client/include/render/skyfixedge.h0000664000175000017500000000431312641367670024507 0ustar jaakkojaakko/** @file render/skyfixedge.h Sky Fix Edge Geometry. * * @authors Copyright © 2011-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RENDER_SKY_FIX_EDGE #define DENG_RENDER_SKY_FIX_EDGE #include #include "TriangleStripBuilder" /// @todo remove me namespace de { class HEdge; /** * @ingroup render */ class SkyFixEdge : public WorldEdge { public: enum FixType { Lower, Upper }; class Event : public WorldEdge::Event { public: Event(SkyFixEdge &owner, double distance = 0); bool operator < (Event const &other) const; double distance() const; Vector3d origin() const; private: DENG2_PRIVATE(d) }; public: /** * @param hedge HEdge from which to determine sky fix coordinates. * @param fixType Fix type. */ SkyFixEdge(HEdge &hedge, FixType fixType, int edge, float materialOffsetS = 0); Vector3d const &pOrigin() const; Vector3d const &pDirection() const; Vector2f materialOrigin() const; /// Implement IEdge. bool isValid() const; /// Implement IEdge. Event const &first() const; /// Implement IEdge. Event const &last() const; inline Event const &bottom() const { return first(); } inline Event const &top() const { return last(); } Event const &at(EventIndex index) const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_RENDER_SKY_FIX_EDGE doomsday-stable-1.15.7/doomsday/client/include/render/vectorlightdata.h0000664000175000017500000000276612641367670025543 0ustar jaakkojaakko/** @file vectorlightdata.h Vector light source data. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_RENDER_VECTORLIGHTDATA_H #define CLIENT_RENDER_VECTORLIGHTDATA_H #include /** * POD for a vector light source affection. * @ingroup render */ struct VectorLightData { de::dfloat approxDist; ///< Only an approximation. de::Vector3f direction; ///< Normalized vector from light origin to illumination point. de::Vector3f color; ///< How intense the light is (0..1, RGB). de::dfloat offset; de::dfloat lightSide; de::dfloat darkSide; ///< Factors for world light. bool affectedByAmbient; }; #endif // CLIENT_RENDER_VECTORLIGHTDATA_H doomsday-stable-1.15.7/doomsday/client/include/render/skydrawable.h0000664000175000017500000001056412641367670024662 0ustar jaakkojaakko/** @file skydrawable.h Drawable specialized for the sky. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_RENDER_SKYDRAWABLE_H #define DENG_CLIENT_RENDER_SKYDRAWABLE_H #include #include #include #include #include #include "MaterialVariantSpec" #include "ModelDef" class Sky; /** * Specialized drawable for Sky visualization. * * This version supports only two layers. (More would be a waste of resources?) * * @ingroup render */ class SkyDrawable { public: /** * Animates the drawable according to the configured Sky. */ class Animator { public: /// Required layer state is missing. @ingroup errors DENG2_ERROR(MissingLayerStateError); /// Required model state is missing. @ingroup errors DENG2_ERROR(MissingModelStateError); struct LayerState { float offset; }; struct ModelState { int frame; int timer; int maxTimer; float yaw; }; public: Animator(); Animator(SkyDrawable &sky); virtual ~Animator(); void setSky(SkyDrawable *sky); SkyDrawable &sky() const; /** * Determines whether the specified animation layer state @a index is valid. * @see layer() */ bool hasLayer(int index) const; /** * Lookup an animation layer state by it's unique @a index. * @see hasLayer() */ LayerState &layer(int index); /// @copydoc layer() LayerState const &layer(int index) const; /** * Determines whether the specified animation model state @a index is valid. * @see model() */ bool hasModel(int index) const; /** * Lookup an animation model state by it's unique @a index. * @see hasModel() */ ModelState &model(int index); /// @copydoc model() ModelState const &model(int index) const; /** * Advances the animation state. * * @param elapsed Duration of elapsed time. */ void advanceTime(timespan_t elapsed); public: DENG2_PRIVATE(d) }; public: /** * Create a new SkyDrawable for visualization of the given @a sky. * * @param sky Sky to visualize, if any (may be @c nullptr to configure layer). */ explicit SkyDrawable(Sky const *sky = nullptr); /** * Reconfigure the drawable for visualizing the given @a sky. * * @return Reference to this drawable, for caller convenience. */ SkyDrawable &configure(Sky const *sky = nullptr); /** * Returns a pointer to the configured sky, if any (may be @c nullptr). */ Sky const *sky() const; /** * Render the sky. */ void draw(Animator const *animator = nullptr) const; /** * Cache all the assets needed for visualizing the sky. */ void cacheAssets(); /** * Returns the definition for a configured model, if any (may be @a nullptr). * * @param modelIndex Unique index of the model. */ ModelDef *modelDef(int modelIndex) const; public: static de::MaterialVariantSpec const &layerMaterialSpec(bool masked); /// Register the console commands, variables, etc..., of this module. static void consoleRegister(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_RENDER_SKYDRAWABLE_H doomsday-stable-1.15.7/doomsday/client/include/render/fx/0000775000175000017500000000000012641367670022610 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/render/fx/lensflares.h0000664000175000017500000000266312641367670025126 0ustar jaakkojaakko/** @file lensflares.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_FX_LENSFLARES_H #define DENG_CLIENT_FX_LENSFLARES_H #include "render/consoleeffect.h" #include "render/ilightsource.h" namespace fx { /** * Draws lens flares for all visible light sources in the current frame. */ class LensFlares : public ConsoleEffect { public: LensFlares(int console); void clearLights(); void markLightPotentiallyVisibleForCurrentFrame(IPointLightSource const *lightSource); void glInit(); void glDeinit(); void beginFrame(); void draw(); static void consoleRegister(); private: DENG2_PRIVATE(d) }; } // namespace fx #endif // DENG_CLIENT_FX_LENSFLARES_H doomsday-stable-1.15.7/doomsday/client/include/render/fx/bloom.h0000664000175000017500000000257612641367670024103 0ustar jaakkojaakko/** @file fx/bloom.h Bloom camera lens effect. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_FX_BLOOM_H #define DENG_CLIENT_FX_BLOOM_H #include "render/consoleeffect.h" namespace fx { /** * Draws a bloom effect that spreads out light from bright pixels. The brightness * threshold and bloom intensity are configurable. */ class Bloom : public ConsoleEffect { public: Bloom(int console); void glInit(); void glDeinit(); void draw(); static void consoleRegister(); static bool isEnabled(); static float intensity(); private: DENG2_PRIVATE(d) }; } // namespace fx #endif // DENG_CLIENT_FX_BLOOM_H doomsday-stable-1.15.7/doomsday/client/include/render/fx/colorfilter.h0000664000175000017500000000216712641367670025313 0ustar jaakkojaakko/** @file colorfilter.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_FX_COLORFILTERCONSOLEEFFECT_H #define DENG_CLIENT_FX_COLORFILTERCONSOLEEFFECT_H #include "render/consoleeffect.h" namespace fx { class ColorFilter : public ConsoleEffect { public: ColorFilter(int console); void draw(); }; } // namespace fx #endif // DENG_CLIENT_FX_COLORFILTERCONSOLEEFFECT_H doomsday-stable-1.15.7/doomsday/client/include/render/fx/postprocessing.h0000664000175000017500000000452712641367670026053 0ustar jaakkojaakko/** @file postprocessing.h World frame post processing. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_FX_POSTPROCESSING_H #define DENG_CLIENT_FX_POSTPROCESSING_H #include "render/consoleeffect.h" #include namespace fx { /** * Post-processing of rendered camera lens frames. Maintains an offscreen * render target and provides a way to draw it back to the regular target * with shader effects applied. */ class PostProcessing : public ConsoleEffect { public: PostProcessing(int console); /** * Determines whether the effect is active. If it isn't, it can be skipped * altogether when post processing a frame. */ bool isActive() const; /** * Fades in, or immediately takes into use, a new post-processing shader. * Only shaders in the "fx.post" namespace can be used. * * If a shader is already in use, it will simply be swapped immediately * with the new shader rather than crossfaded. * * @param fxPostShader Name of the shader under "fx.post". * @param span Duration of the fade in animation. */ void fadeInShader(de::String const &fxPostShader, de::TimeDelta const &span); void fadeOut(de::TimeDelta const &span); /** * Sets a constant opacity factor that is applied in addition to the fade. * * @param opacity Opacity factor. 1.0 by default. */ void setOpacity(float opacity); void glInit(); void glDeinit(); void beginFrame(); void draw(); void endFrame(); private: DENG2_PRIVATE(d) }; } // namespace fx #endif // DENG_CLIENT_FX_POSTPROCESSING_H doomsday-stable-1.15.7/doomsday/client/include/render/fx/vignette.h0000664000175000017500000000274112641367670024612 0ustar jaakkojaakko/** * @file vignette.h * Renders a vignette for the player view. @ingroup render * * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_FX_VIGNETTE_H #define DENG_CLIENT_FX_VIGNETTE_H #include "render/consoleeffect.h" namespace fx { /** * Initializes the vignette rendering module. This includes registering console * commands. */ class Vignette : public ConsoleEffect { public: Vignette(int console); /** * Renders the vignette for the player's view. * * @param viewRect View window where to draw the vignette. */ void draw(); public: static void consoleRegister(); }; } // namespace fx #endif // DENG_CLIENT_FX_VIGNETTE_H doomsday-stable-1.15.7/doomsday/client/include/render/fx/resize.h0000664000175000017500000000310012641367670024254 0ustar jaakkojaakko/** @file fx/resize.h Change the size (pixel density) of the view. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_FX_RESIZE_H #define DENG_CLIENT_FX_RESIZE_H #include "render/consoleeffect.h" #include namespace fx { /** * Post-processing of rendered camera lens frames. Maintains an offscreen * render target and provides a way to draw it back to the regular target * with shader effects applied. */ class Resize : public ConsoleEffect { public: Resize(int console); /** * Determines whether the effect is active. If it isn't, it can be skipped * altogether when post processing a frame. */ bool isActive() const; void glInit(); void glDeinit(); void beginFrame(); void endFrame(); private: DENG2_PRIVATE(d) }; } // namespace fx #endif // DENG_CLIENT_FX_RESIZE_H doomsday-stable-1.15.7/doomsday/client/include/render/modelrenderer.h0000664000175000017500000000626012641367670025177 0ustar jaakkojaakko/** @file modelrenderer.h Model renderer. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_MODELRENDERER_H #define DENG_CLIENT_MODELRENDERER_H #include #include #include #include #include /** * The model renderer prepares available model assets for drawing (using ModelDrawable), * and keeps the set of needed ModelDrawable instances in memory. * * @todo Consider renaming the class: the term "renderer" has the connotation of actually * performing rendering, while in practice the ModelDrawables will be drawing themselves. * This is the top-level class responsible for model assets and all their associated * data. Perhaps the class should be instead portrayed more as a specialized Bank. -jk * * @ingroup render */ class ModelRenderer { public: struct AnimSequence { de::String name; de::Record const *def; AnimSequence(de::String const &n, de::Record const &d) : name(n), def(&d) {} }; typedef QList AnimSequences; struct StateAnims : public QMap {}; struct AuxiliaryData : public de::ModelBank::IUserData { StateAnims animations; de::Matrix4f transformation; }; public: ModelRenderer(); void glInit(); void glDeinit(); /** * Provides access to the bank containing available drawable models. */ de::ModelBank &bank(); StateAnims const *animations(de::DotPath const &modelId) const; /** * Sets up the transformation matrices. * * @param eyeDir Direction of the eye in local space (relative to object). * @param modelToLocal Transformation from model space to the object's local space * (object's local frame in world space). * @param localToView Transformation from local space to projected view space. */ void setTransformation(de::Vector3f const &eyeDir, de::Matrix4f const &modelToLocal, de::Matrix4f const &localToView); void setAmbientLight(de::Vector3f const &ambientIntensity); void clearLights(); void addLight(de::Vector3f const &direction, de::Vector3f const &intensity); public: static int identifierFromText(de::String const &text, std::function resolver); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_MODELRENDERER_H doomsday-stable-1.15.7/doomsday/client/include/render/rendpoly.h0000664000175000017500000000472312641367670024206 0ustar jaakkojaakko/** @file rendpoly.h RendPoly data buffers * @ingroup render * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RENDER_RENDPOLY_H #define DENG_RENDER_RENDPOLY_H #include #include DENG_EXTERN_C byte rendInfoRPolys; void R_PrintRendPoolInfo(); /** * @note Should be called at the start of each map. */ void R_InitRendPolyPools(); /** * Allocate a new contiguous range of position coordinates from the specialized * write-geometry pool * * @param num The number of coordinate sets required. */ de::Vector3f *R_AllocRendVertices(uint num); /** * Allocate a new contiguous range of color coordinates from the specialized * write-geometry pool * * @param num The number of coordinate sets required. */ de::Vector4f *R_AllocRendColors(uint num); /** * Allocate a new contiguous range of texture coordinates from the specialized * write-geometry pool * * @param num The number of coordinate sets required. */ de::Vector2f *R_AllocRendTexCoords(uint num); /** * @note Doesn't actually free anything (storage is pooled for reuse). * * @param posCoords Position coordinates to mark unused. */ void R_FreeRendVertices(de::Vector3f *posCoords); /** * @note Doesn't actually free anything (storage is pooled for reuse). * * @param colorCoords Color coordinates to mark unused. */ void R_FreeRendColors(de::Vector4f *colorCoords); /** * @note Doesn't actually free anything (storage is pooled for reuse). * * @param texCoords Texture coordinates to mark unused. */ void R_FreeRendTexCoords(de::Vector2f *texCoords); #endif // DENG_RENDER_RENDPOLY_H doomsday-stable-1.15.7/doomsday/client/include/render/biasillum.h0000664000175000017500000000721312641367670024330 0ustar jaakkojaakko/** @file biasillum.h Shadow Bias map point illumination. * * @authors Copyright © 2005-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_RENDER_BIASILLUM_H #define DENG_CLIENT_RENDER_BIASILLUM_H #include #include class BiasTracker; /** * Map point illumination sampler for the Shadow Bias lighting model. * * Used in conjunction with a BiasTracker (for routing change notifications). * * @ingroup render */ class BiasIllum { public: /// Required tracker is missing. @ingroup errors DENG2_ERROR(MissingTrackerError); /// Maximum number of light contributions/contributors. static int const MAX_CONTRIBUTORS = 6; /// Minimum light contribution intensity. static float const MIN_INTENSITY; // .005f public: /** * Construct a new bias illumination point. * * @param tracker Lighting-contributor tracker to assign to the new point. * Note that @ref assignTracker() can be used later. */ explicit BiasIllum(BiasTracker *tracker = 0); /** * (Re-)Evaluate lighting for the map point. Any queued changes to lighting * contributions will be applied at this time (note that this is however a * fast operation which does not block). * * @param point Point in the map to evaluate. It is assumed this * has @em not moved since the last call unless the * light source contributors have been redetermined. * (Failure to do so will result in briefly visible * artefacts/inconsistent lighting at worst.) * @param normalAtPoint Surface normal at @a point. Also assumed not to * have changed since the last call. * @param biasTime Time in milliseconds of the last bias frame update * used for interpolation. * * @return Current color at this time. */ de::Vector3f evaluate(de::Vector3d const &point, de::Vector3f const &normalAtPoint, uint biasTime); /** * Returns @c true iff a BiasTracker has been assigned for the illumination. * * @see setTracker() */ bool hasTracker() const; /** * Provides access to the currently assigned tracker. * * @see hasTracker(), setTracker() */ BiasTracker &tracker() const; /** * Assign the illumination point to the specified tracker. * * @param newTracker New illumination tracker to be assigned. Use @c 0 to * unassign any current tracker. * @see hasTracker() */ void setTracker(BiasTracker *newTracker); /** * To be called to register the commands and variables of this module. */ static void consoleRegister(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_RENDER_BIASILLUM_H doomsday-stable-1.15.7/doomsday/client/include/render/lightdecoration.h0000664000175000017500000000510512641367670025524 0ustar jaakkojaakko/** @file lightdecoration.h World surface light decoration. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_RENDER_LIGHTDECORATION_H #define CLIENT_RENDER_LIGHTDECORATION_H #include "Decoration" #include "Lumobj" /** * World surface light decoration. * @ingroup render */ class LightDecoration : public Decoration, public Lumobj::Source { public: /** * Construct a new light decoration. * * @param source Source of the decoration (a material). * @param origin Origin of the decoration in map space. */ LightDecoration(MaterialAnimator::Decoration const &source, de::Vector3d const &origin = de::Vector3d()); /** * To be called to register the commands and variables of this module. */ static void consoleRegister(); /** * Returns the current angle fade factor (user configurable). */ static float angleFadeFactor(); /** * Returns the current brightness scale factor (user configurable). */ static float brightFactor(); /** * Calculates an occlusion factor for the light source. Determined by the * relative position of the @a eye (the viewer) and the decoration. * * @param eye Position of the eye in map space. * * @return Occlusion factor in the range [0..1], where @c 0 is fully * occluded and @c 1 is fully visible. */ float occlusion(de::Vector3d const &eye) const; /** * Generates a new lumobj for the light decoration. A map surface must be * attributed to the decoration. * * @see Decoration::setSurface(), Decoration::hasSurface() */ Lumobj *generateLumobj() const; }; #endif // CLIENT_RENDER_LIGHTDECORATION_H doomsday-stable-1.15.7/doomsday/client/include/render/biasdigest.h0000664000175000017500000000305112641367670024461 0ustar jaakkojaakko/** @file biasdigest.h Shadow Bias change digest. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_RENDER_SHADOWBIAS_DIGEST_H #define DENG_RENDER_SHADOWBIAS_DIGEST_H #include /** * Change digest for updating trackers in the Shadow Bias lighting model. */ class BiasDigest { public: BiasDigest(); /** * Return the digest to an empty state (clear all changes). */ void reset(); /** * Mark the identified bias source as having changed. */ void markSourceChanged(uint index); /** * Returns @c true if the identified bias source is marked as changed. */ bool isSourceChanged(uint index) const; private: DENG2_PRIVATE(d) }; #endif // DENG_RENDER_SHADOWBIAS_DIGEST_H doomsday-stable-1.15.7/doomsday/client/include/render/rendersystem.h0000664000175000017500000001370712641367670025100 0ustar jaakkojaakko/** @file rendersystem.h Render subsystem. * * @authors Copyright © 2005-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_RENDERSYSTEM_H #define CLIENT_RENDERSYSTEM_H #include #include #include #include #include #include "DrawLists" #include "settingsregister.h" #include "projectedtexturedata.h" #include "vectorlightdata.h" class AngleClipper; class ModelRenderer; class SkyDrawable; /** * Geometry backing store (arrays). * @todo Replace with GLBuffer -ds */ struct Store { /// Texture coordinate array indices. enum { TCA_MAIN, // Main texture. TCA_BLEND, // Blendtarget texture. TCA_LIGHT, // Dynlight texture. NUM_TEXCOORD_ARRAYS }; de::Vector3f *posCoords; de::Vector2f *texCoords[NUM_TEXCOORD_ARRAYS]; de::Vector4ub *colorCoords; Store(); ~Store(); void rewind(); void clear(); uint allocateVertices(uint count); private: uint vertCount, vertMax; }; /// @todo make private to RenderSystem struct ProjectionList { struct Node { Node *next, *nextUsed; ProjectedTextureData projection; }; Node *head = nullptr; bool sortByLuma; ///< @c true= Sort from brightest to darkest. static void init(); static void rewind(); inline ProjectionList &operator << (ProjectedTextureData &texp) { return add(texp); } ProjectionList &add(ProjectedTextureData &texp); private: static Node *firstNode; static Node *cursorNode; static Node *newNode(); }; /// @todo make private to RenderSystem struct VectorLightList { struct Node { Node *next, *nextUsed; VectorLightData vlight; }; Node *head = nullptr; static void init(); static void rewind(); inline VectorLightList &operator << (VectorLightData &texp) { return add(texp); } VectorLightList &add(VectorLightData &texp); private: static Node *firstNode; static Node *cursorNode; static Node *newNode(); }; /** * Renderer subsystems, draw lists, etc... @ingroup render */ class RenderSystem : public de::System { public: RenderSystem(); // System. void timeChanged(de::Clock const &); void glInit(); void glDeinit(); de::GLShaderBank &shaders(); de::ImageBank &images(); SettingsRegister &settings(); SettingsRegister &appearanceSettings(); AngleClipper &angleClipper() const; ModelRenderer &modelRenderer(); SkyDrawable &sky(); /** * Provides access to the central map geometry buffer. */ Store &buffer(); /** * Provides access to the DrawLists collection for conveniently writing geometry. */ DrawLists &drawLists(); /** * To be called manually, to clear all persistent data held by/for the draw lists * (e.g., during re-initialization). * * @todo Use a de::Observers based mechanism. */ void clearDrawLists(); /** * @todo Use a de::Observers based mechanism. */ void worldSystemMapChanged(de::Map &map); /** * @todo Use a de::Observers based mechanism. */ void beginFrame(); public: // Texture => surface projection lists ----------------------------------- /** * Find/create a new projection list. * * @param listIdx Address holding the list index to retrieve. If the referenced * list index is non-zero return the associated list. Otherwise * allocate a new list and write it's index back to this address. * * @param sortByLuma @c true= Maintain a luminosity sorted order (descending). * * @return ProjectionList associated with the (possibly newly attributed) index. */ ProjectionList &findSurfaceProjectionList(de::duint *listIdx, bool sortByLuma = false); /** * Iterate through the referenced list of texture => surface projections. * * @param listIdx Unique identifier of the projection list to process. * @param func Callback to make for each TexProjection. */ de::LoopResult forAllSurfaceProjections(de::duint listIdx, std::function func) const; public: // VectorLight affection lists ------------------------------------------- /** * Find/create a new vector light list. * * @param listIdx Address holding the list index to retrieve. If the referenced * list index is non-zero return the associated list. Otherwise * allocate a new list and write it's index back to this address. * * @return VectorLightList associated with the (possibly newly attributed) index. */ VectorLightList &findVectorLightList(de::duint *listIdx); /** * Iterate through the referenced vector light list. * * @param listIdx Unique identifier of the list to process. * @param func Callback to make for each VectorLight. */ de::LoopResult forAllVectorLights(de::duint listIdx, std::function func); public: /** * Register the console commands, variables, etc..., of this module. */ static void consoleRegister(); private: DENG2_PRIVATE(d) }; #endif // CLIENT_RENDERSYSTEM_H doomsday-stable-1.15.7/doomsday/client/include/render/lightgrid.h0000664000175000017500000001363212641367670024326 0ustar jaakkojaakko/** @file lightgrid.h Light Grid (Smoothed ambient sector lighting). * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_LIGHTGRID_H #define DENG_CLIENT_LIGHTGRID_H #include #include #include #include "render/ilightsource.h" namespace de { class Map; /** * Simple global illumination method utilizing a 2D grid of block light sources, * achieving smoothed ambient lighting for sectors. * * @ingroup render */ class LightGrid { public: /** * Linear reference to a block in the grid (X + Y * Grid Width). */ typedef int Index; /** * Two dimensioned reference to a block in the grid [X, Y]. */ typedef Vector2i Ref; /** * Base class for a block illumination source. * * Derived classes are obliged to call @ref LightGrid::blockLightSourceChanged() * whenever the properties of the light source change, so that any necessary * updates can be scheduled. */ class IBlockLightSource : public ILightSource { public: virtual ~IBlockLightSource() {} /** * Determines the Z-axis bias scale factor for the illumination source. */ virtual int blockLightSourceZBias() = 0; }; public: /** * Construct and initialize an empty LightGrid. */ LightGrid(Vector2d const &origin, Vector2d const &dimensions); /** * To be called when the physical dimensions of the grid or the logical block * size changes to resize the light grid. Note that resizing inherently means * clearing of primary illumination sources, so they'll need to be initialized * again. * * @param newOrigin Origin of the grid in map space. * @param newDimensions Dimensions of the grid in map space units. */ void resizeAndClear(Vector2d const &newOrigin, Vector2d const &newDimensions); /** * Determine the smoothed ambient lighting properties for the given @a point * in the map coordinate space. * * @param point 3D point. * * @return Evaluated color at the specified point. * - [x, y, z] = RGB color with premultiplied light intensity factor * - [w] = light intensity factor (i.e., light level) */ Vector4f evaluate(Vector3d const &point); /** * Convenient method of determining the smoothed ambient lighting properties * for the given @a point in the map coordinate space and returns the intensity * factor directly. * * @param point 3D point. * * @return Evaluated light intensity at the specified point. * * @see evaluate */ inline float evaluateIntensity(Vector3d const &point) { return evaluate(point).w; } /** * To be called when an engine variable which affects the lightgrid changes. * @todo Only needed because of the sky light color. -ds */ void scheduleFullUpdate(); /** * Update the grid. Should be called periodically to update/refresh the grid * contents (e.g., when beginning a new render frame). */ void updateIfNeeded(); /** * Change the primary light source for the specified grid @a block. Whenever * a primary source is changed the necessary grid updates are scheduled. * * @param index Linear index of the block to change. * @param newSource New primary light source to set. Use @c 0 to clear. */ void setPrimarySource(Index block, IBlockLightSource *newSource); /** * Lookup the primary illumination source for the specified @a block. For debug. */ IBlockLightSource *primarySource(Index block) const; /** * IBlockLightSources are obliged to call this whenever the attributes of the * the light source have changed to schedule any necessary grid updates. */ void blockLightSourceChanged(IBlockLightSource *changedSource); /** * Register the console comands and variables of this module. */ static void consoleRegister(); public: // Utilities ----------------------------------------------------------- /// Returns the linear grid index for the given two-dimensioned grid reference. inline Index toIndex(int x, int y) { return y * dimensions().x + x; } /// @copydoc toIndex() inline Index toIndex(Ref const &gref) { return toIndex(gref.x, gref.y); } /// Returns the two-dimensioned grid reference for the given map space @a point. Ref toRef(Vector3d const &point); /// Returns the size of a grid block in map space units. int blockSize() const; /// Returns the origin of the grid in map space. Vector2d const &origin() const; /// Returns the dimensions of the grid in blocks. Vector2i const &dimensions() const; /// Returns the total number of non-null blocks in the grid. int numBlocks() const; /// Returns the total number of bytes used for non-null blocks in the grid. size_t blockStorageSize() const; /// Returns the "raw" color for the specified @a block. For debug. Vector3f const &rawColorRef(Index block) const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_CLIENT_LIGHTGRID_H doomsday-stable-1.15.7/doomsday/client/include/render/r_main.h0000664000175000017500000000307112641367670023612 0ustar jaakkojaakko/** @file r_main.h * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RENDER_R_MAIN_H #define DENG_RENDER_R_MAIN_H #include "dd_types.h" DENG_EXTERN_C int levelFullBright; DENG_EXTERN_C float pspOffset[2], pspLightLevelMultiplier; DENG_EXTERN_C int psp3d; DENG_EXTERN_C float weaponOffsetScale, weaponFOVShift; DENG_EXTERN_C int weaponOffsetScaleY; DENG_EXTERN_C byte weaponScaleMode; // cvar DENG_EXTERN_C fixed_t fineTangent[FINEANGLES / 2]; /** * Draws 2D HUD sprites. If they were already drawn 3D, this won't do anything. */ void Rend_Draw2DPlayerSprites(); /** * Draws 3D HUD models. */ void Rend_Draw3DPlayerSprites(); #endif // DENG_RENDER_R_MAIN_H doomsday-stable-1.15.7/doomsday/client/include/render/huecirclevisual.h0000664000175000017500000000232212641367670025532 0ustar jaakkojaakko/** @file huecirclevisual.h HueCircle visualizer. * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_RENDER_HUECIRCLEVISUAL_H #define DENG_RENDER_HUECIRCLEVISUAL_H #include class HueCircle; struct HueCircleVisual { static void draw(HueCircle &hueCircle, de::Vector3d const &viewOrigin, de::Vector3f const &viewFrontVec); }; #endif // DENG_RENDER_HUECIRCLEVISUAL_H doomsday-stable-1.15.7/doomsday/client/include/render/cameralensfx.h0000664000175000017500000000360712641367670025022 0ustar jaakkojaakko/** @file cameralensfx.h Camera lens effects. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_CAMERALENSFX_H #define DENG_CLIENT_CAMERALENSFX_H #include "ilightsource.h" void LensFx_Register(); void LensFx_Init(); void LensFx_Shutdown(); /** * Deinitializes all console effects. This is called when the viewport * configuration changes so that GL resources for unnecessary consoles are not * retained. */ void LensFx_GLRelease(); /** * Notifies camera lens FX that the rendering of a world view frame will begin. * All graphics until LensFx_EndFrame() are considered part of the the frame. * The render target may change during this call if additional post-procesing * effects will require it. * * @param playerNum Player/console number. */ void LensFx_BeginFrame(int playerNum); /** * Finishes camera lens FX rendering of a frame. The drawn frame may be * post-processed, and any additional effects (vignette, flares) are added on * top. */ void LensFx_EndFrame(); /** * Marks a light potentially visible in the current frame. */ void LensFx_MarkLightVisible(IPointLightSource const &lightSource); #endif // DENG_CLIENT_CAMERALENSFX_H doomsday-stable-1.15.7/doomsday/client/include/render/projectedtexturedata.h0000664000175000017500000000245412641367670026603 0ustar jaakkojaakko/** @file projectedtexturedata.h Projected texture data. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_RENDER_PROJECTEDTEXTUREDATA_H #define CLIENT_RENDER_PROJECTEDTEXTUREDATA_H #include #include "api_gl.h" // DGLuint /** * POD for a texture => surface projection. * @ingroup render */ struct ProjectedTextureData { DGLuint texture; de::Vector2f topLeft; de::Vector2f bottomRight; de::Vector4f color; }; #endif // CLIENT_RENDER_PROJECTEDTEXTUREDATA_H doomsday-stable-1.15.7/doomsday/client/include/render/shard.h0000664000175000017500000000633412641367670023453 0ustar jaakkojaakko/** @file shard.h 3D map geometry fragment. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_RENDER_SHARD_H #define DENG_CLIENT_RENDER_SHARD_H #include #include class BiasTracker; class SectorCluster; /** * 3D map geometry fragment. * * Shards are produced (and perhaps owned) by SectorClusters when the map geometry * is split into drawable geometry fragments. Shard ownership may be transferred * however a shard should never outlive the MapElement for which it was produced. */ class Shard { public: /** * Construct a new Shard of 3D map geometry. * * @param numBiasIllums Number of bias illumination points for the geometry. * @param owner SectorCluster which owns the shard (if any). */ Shard(int numBiasIllums, SectorCluster *owner = 0); /** * Perform bias lighting for the supplied vertex geometry. * * @note Important: It assumed that the given geometry buffers have at least * the same number of elements as there are bias illumination points. * * @param posCoords Position coords (in map space) for each vertex. * @param colorCoords Color coords for each vertex. Light contributions are * applied additively to the @em current values. Therefore * they should be suitably initialized with a base value * before calling (perhaps zeroed). * * @param tangentMatrix Tangent space matrix for the surface geometry. * @param biasTime Time in milliseconds of the last bias frame update, * used for interpolation. */ void lightWithBiasSources(de::Vector3f const *posCoords, de::Vector4f *colorCoords, de::Matrix3f const &tangentMatrix, uint biasTime); /** * Returns a pointer to the SectorCluster which owns the shard (if any). */ SectorCluster *cluster() const; /** * Change SectorCluster which owns the shard to @a newOwner. */ void setCluster(SectorCluster *newOwner); /** * Returns the BiasTracker for the shard. */ BiasTracker &biasTracker() const; /** * Schedule a bias lighting update for the Shard following a move/transform. */ void updateBiasAfterMove(); /** * To be called to register the commands and variables of this module. */ static void consoleRegister(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_RENDER_SHARD_H doomsday-stable-1.15.7/doomsday/client/include/render/rend_main.h0000664000175000017500000002075512641367670024311 0ustar jaakkojaakko/** @file rend_main.h Core of the rendering subsystem. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_RENDER_MAIN_H #define CLIENT_RENDER_MAIN_H #ifndef __CLIENT__ # error "render/rend_main.h only exists in the Client" #endif #include "dd_types.h" #include "def_main.h" #include "MaterialVariantSpec" #include "WallEdge" #include #include class Sector; class SectorCluster; struct VectorLightData; namespace de { class Map; class LightGrid; } // Multiplicative blending for dynamic lights? #define IS_MUL (dynlightBlend != 1 && !usingFog) #define MTEX_DETAILS_ENABLED (r_detail && useMultiTexDetails && \ defs.details.size() > 0) #define IS_MTEX_DETAILS (MTEX_DETAILS_ENABLED && numTexUnits > 1) #define IS_MTEX_LIGHTS (!IS_MTEX_DETAILS && !usingFog && useMultiTexLights \ && numTexUnits > 1 && envModAdd) #define GLOW_HEIGHT_MAX (1024.f) /// Absolute maximum #define OMNILIGHT_SURFACE_LUMINOSITY_ATTRIBUTION_MIN (.05f) #define SHADOW_SURFACE_LUMINOSITY_ATTRIBUTION_MIN (.05f) DENG_EXTERN_C de::Vector3d vOrigin; // Y/Z swizzled for drawing DENG_EXTERN_C float vang, vpitch, yfov; DENG_EXTERN_C float viewsidex, viewsidey; DENG_EXTERN_C float fogColor[4]; DENG_EXTERN_C byte smoothTexAnim, devMobjVLights; DENG_EXTERN_C dd_bool usingFog; DENG_EXTERN_C int renderTextures; /// @c 0= no textures, @c 1= normal mode, @c 2= lighting debug DENG_EXTERN_C int renderWireframe; DENG_EXTERN_C int useMultiTexLights; DENG_EXTERN_C int useMultiTexDetails; DENG_EXTERN_C int dynlightBlend; DENG_EXTERN_C int torchAdditive; DENG_EXTERN_C de::Vector3f torchColor; DENG_EXTERN_C int rAmbient; DENG_EXTERN_C float rendLightDistanceAttenuation; DENG_EXTERN_C int rendLightAttenuateFixedColormap; DENG_EXTERN_C float rendLightWallAngle; DENG_EXTERN_C byte rendLightWallAngleSmooth; DENG_EXTERN_C float rendSkyLight; // cvar DENG_EXTERN_C byte rendSkyLightAuto; // cvar DENG_EXTERN_C float lightModRange[255]; DENG_EXTERN_C int extraLight; DENG_EXTERN_C float extraLightDelta; DENG_EXTERN_C int devRendSkyMode; DENG_EXTERN_C int gameDrawHUD; DENG_EXTERN_C int useBias; DENG_EXTERN_C int useDynLights; DENG_EXTERN_C float dynlightFactor, dynlightFogBright; DENG_EXTERN_C int rendMaxLumobjs; DENG_EXTERN_C int useGlowOnWalls; DENG_EXTERN_C float glowFactor, glowHeightFactor; DENG_EXTERN_C int glowHeightMax; DENG_EXTERN_C int useShadows; DENG_EXTERN_C float shadowFactor; DENG_EXTERN_C int shadowMaxRadius; DENG_EXTERN_C int shadowMaxDistance; DENG_EXTERN_C byte useLightDecorations; DENG_EXTERN_C int useShinySurfaces; DENG_EXTERN_C float detailFactor, detailScale; DENG_EXTERN_C int ratioLimit; DENG_EXTERN_C int mipmapping, filterUI, texQuality, filterSprites; DENG_EXTERN_C int texMagMode, texAniso; DENG_EXTERN_C int useSmartFilter; DENG_EXTERN_C int texMagMode; DENG_EXTERN_C int glmode[6]; DENG_EXTERN_C dd_bool fillOutlines; DENG_EXTERN_C dd_bool noHighResTex; DENG_EXTERN_C dd_bool noHighResPatches; DENG_EXTERN_C dd_bool highResWithPWAD; DENG_EXTERN_C byte loadExtAlways; DENG_EXTERN_C int devNoCulling; DENG_EXTERN_C byte devRendSkyAlways; DENG_EXTERN_C byte rendInfoLums; DENG_EXTERN_C byte devDrawLums; DENG_EXTERN_C byte freezeRLs; void Rend_Register(); void Rend_Reset(); /// @return @c true iff multitexturing is currently enabled for lights. bool Rend_IsMTexLights(); /// @return @c true iff multitexturing is currently enabled for detail textures. bool Rend_IsMTexDetails(); void Rend_RenderMap(de::Map &map); float Rend_FieldOfView(); /** * @param inWorldSpace Apply viewer angles and head position to produce a transformation * from world space to view space. */ void Rend_ModelViewMatrix(bool inWorldSpace = true); de::Matrix4f Rend_GetModelViewMatrix(int consoleNum, bool inWorldSpace = true); de::Vector3d Rend_EyeOrigin(); #define Rend_PointDist2D(c) (fabs((vOrigin.z-(c)[VY])*viewsidex - (vOrigin.x-(c)[VX])*viewsidey)) /** * The DOOM lighting model applies a light level delta to everything when * e.g. the player shoots. * * @return Calculated delta. */ float Rend_ExtraLightDelta(); void Rend_ApplyTorchLight(float *color3, float distance); void Rend_ApplyTorchLight(de::Vector4f &color, float distance); /** * Apply range compression delta to @a lightValue. * @param lightValue The light level value to apply adaptation to. */ void Rend_ApplyLightAdaptation(float &lightValue); /// Same as Rend_ApplyLightAdaptation except the delta is returned. float Rend_LightAdaptationDelta(float lightvalue); /** * The DOOM lighting model applies distance attenuation to sector light * levels. * * @param distToViewer Distance from the viewer to this object. * @param lightLevel Sector lightLevel at this object's origin. * @return The specified lightLevel plus any attentuation. */ float Rend_AttenuateLightLevel(float distToViewer, float lightLevel); float Rend_ShadowAttenuationFactor(coord_t distance); /** * Updates the lightModRange which is used to applify sector light to help * compensate for the differences between the OpenGL lighting equation, * the software Doom lighting model and the light grid (ambient lighting). * * The offsets in the lightRangeModTables are added to the sector->lightLevel * during rendering (both positive and negative). */ void Rend_UpdateLightModMatrix(); /** * Draws the lightModRange (for debug) */ void Rend_DrawLightModMatrix(); /** * Draws the light grid debug visual. */ void Rend_LightGridVisual(de::LightGrid &lg); /** * Determines whether the sky light color tinting is enabled. */ bool Rend_SkyLightIsEnabled(); /** * Returns the effective sky light color. */ de::Vector3f Rend_SkyLightColor(); /** * Blend the given light value with the luminous object's color, applying any * applicable global modifiers and returns the result. * * @param color Source light color. * @param light Strength of the light on the illumination point. * * @return Calculated result. */ de::Vector3f Rend_LuminousColor(de::Vector3f const &color, float light); /** * Given an @a intensity determine the height of the plane glow, applying any * applicable global modifiers. * * @return Calculated result. */ coord_t Rend_PlaneGlowHeight(float intensity); /** * @param point World space point to evaluate. * @param ambientColor Ambient color of the object being lit. * @param subspace Subspace in which @a origin resides. * @param starkLight @c true= World light has a more pronounced affect. * * @todo Does not belong here. */ de::duint Rend_CollectAffectingLights(de::Vector3d const &point, de::Vector3f const &ambientColor = de::Vector3f(1, 1, 1), ConvexSubspace *subspace = nullptr, bool starkLight = false); void Rend_DrawVectorLight(VectorLightData const &vlight, dfloat alpha); /** * Selects a Material for the given map @a surface considering the current map * renderer configuration. */ Material *Rend_ChooseMapSurfaceMaterial(Surface const &surface); de::MaterialVariantSpec const &Rend_MapSurfaceMaterialSpec(); de::MaterialVariantSpec const &Rend_MapSurfaceMaterialSpec(int wrapS, int wrapT); TextureVariantSpec const &Rend_MapSurfaceLightmapTextureSpec(); TextureVariantSpec const &Rend_MapSurfaceShinyTextureSpec(); TextureVariantSpec const &Rend_MapSurfaceShinyMaskTextureSpec(); void R_DivVerts(de::Vector3f *dst, de::Vector3f const *src, de::WorldEdge const &leftEdge, de::WorldEdge const &rightEdge); void R_DivTexCoords(de::Vector2f *dst, de::Vector2f const *src, de::WorldEdge const &leftEdge, de::WorldEdge const &rightEdge); void R_DivVertColors(de::Vector4f *dst, de::Vector4f const *src, de::WorldEdge const &leftEdge, de::WorldEdge const &rightEdge); #endif // CLIENT_RENDER_MAIN_H doomsday-stable-1.15.7/doomsday/client/include/render/lumobj.h0000664000175000017500000001522612641367670023642 0ustar jaakkojaakko/** @file lumobj.h Luminous object. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_RENDER_LUMOBJ_H #define DENG_CLIENT_RENDER_LUMOBJ_H #include #include #include "MapObject" #include "Texture" /** * Luminous object. @ingroup render * * @todo This should implement ILightSource and be owned by ClientMobjThinkerData (or * Plane or anything else that acts as a light source). There is no need to keep * recreating light source information on each frame, when it can be kept in the * thinker/map data for the lifetime of the owner. -jk */ class Lumobj : public de::MapObject { public: /// Identifiers for attributing lightmaps (used during projection). enum LightmapSemantic { Side, Down, Up }; /** * Base for any class wishing to act as the source of the luminous object. */ class Source { public: virtual ~Source() {} /** * Calculate an occlusion factor for the light. The implementation should * return a value in the range [0..1], where @c 0 is fully occluded and * @c 1 is fully visible. * * The default implementation assumes the source is always visible. * * @param eye Position of the eye in map space. */ virtual float occlusion(de::Vector3d const &eye) const; }; public: /** * Construct a new luminous object. * * @param origin Origin in map space. * @param radius Radius in map space units. * @param color Color/intensity. */ Lumobj(de::Vector3d const &origin = de::Vector3d(), double radius = 256, de::Vector3f const &color = de::Vector3f(1, 1, 1)); /// Construct a new luminious object by copying @a other. Lumobj(Lumobj const &other); /** * To be called to register the commands and variables of this module. */ static void consoleRegister(); /** * Returns the current radius scale factor (user configurable). */ static float radiusFactor(); /** * Returns the current radius maximum (user configurable). */ static int radiusMax(); /** * Change the attributed source of the lumobj. * * @param newSource New source to attribute. Use @c 0 to clear. */ void setSource(Source const *newSource); /** * Returns the light color/intensity of the lumobj. * * @see setColor() */ de::Vector3f const &color() const; /** * Change the light color/intensity of the lumobj. * * @param newColor New color to apply. * * @see color() */ Lumobj &setColor(de::Vector3f const &newColor); /** * Returns the radius of the lumobj in map space units. * * @see setRadius() */ double radius() const; /** * Change the radius of the lumobj in map space units. * * @param newRadius New radius to apply. * * @see radius() */ Lumobj &setRadius(double newRadius); /** * Returns an axis-aligned bounding box for the lumobj in map space, centered * on the origin with dimensions equal to @code radius * 2 @endcode. * * @see radius() */ AABoxd aaBox() const; /** * Returns the z-offset of the lumobj. * * @see setZOffset() */ double zOffset() const; /** * Change the z-offset of the lumobj. * * @param newZOffset New z-offset to apply. * * @see zOffset() */ Lumobj &setZOffset(double newZOffset); /** * Returns the maximum distance at which the lumobj will be drawn. If no * maximum is configured then @c 0 is returned (default). * * @see setMaxDistance() */ double maxDistance() const; /** * Change the maximum distance at which the lumobj will drawn. For use with * surface decorations, which, should only de visible within a fairly small * radius around the viewer). * * @param newMaxDistance New maximum distance to apply. * * @see maxDistance() */ Lumobj &setMaxDistance(double newMaxDistance); /** * Returns the identified custom lightmap (if any). * * @param semantic Identifier of the lightmap. * * @see setLightmap() */ de::Texture *lightmap(LightmapSemantic semantic) const; /** * Change an attributed lightmap to the texture specified. * * @param semantic Identifier of the lightmap to change. * @param newTexture Lightmap texture to apply. Use @c 0 to clear. * * @see lightmap() */ Lumobj &setLightmap(LightmapSemantic semantic, de::Texture *newTexture); /** * Returns the current flare size of the lumobj. */ float flareSize() const; /** * Change the flare size of the lumobj. * * @param newFlareSize New flare size factor. */ Lumobj &setFlareSize(float newFlareSize); /** * Returns the current flare texture of the lumobj. */ DGLuint flareTexture() const; /** * Change the flare texture of the lumobj. * * @param newTexture New flare texture. */ Lumobj &setFlareTexture(DGLuint newTexture); /** * Calculate a distance attentuation factor for the lumobj. * * @param distFromEye Distance between the lumobj and the viewer. * * @return Attentuation factor [0..1]. */ float attenuation(double distFromEye) const; /** * Generates a VSPR_FLARE vissprite for the lumobj. * * @param eye Position of the viewer in map space. * @param distFromEye Distance between the lumobj and the viewer. */ void generateFlare(de::Vector3d const &eye, double distFromEye); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_RENDER_LUMOBJ_H doomsday-stable-1.15.7/doomsday/client/include/render/shadowedge.h0000664000175000017500000000530212641367670024456 0ustar jaakkojaakko/** @file render/shadowedge.h FakeRadio Shadow Edge Geometry * * @authors Copyright © 2004-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RENDER_FAKERADIO_SHADOWEDGE_H #define DENG_RENDER_FAKERADIO_SHADOWEDGE_H #include #include namespace de { static coord_t const SHADOWEDGE_OPEN_THRESHOLD = 8; // world units (Z axis) class HEdge; /** * @ingroup render */ class ShadowEdge { public: ShadowEdge(); void init(HEdge const &leftMostHEdge, int edge); void prepare(int planeIndex); /** * Returns the "side openness" factor for the shadow edge. This factor is * a measure of how open the @em open range is on "this" edge of the * shadowing surface. * * @see sectorOpenness() */ float openness() const; /** * Returns the "sector openness" factor for the shadow edge. This factor is * a measure of how open the @em open range is on the sector edge of the * shadowing surface. * * @see openness() */ float sectorOpenness() const; /** * Returns the origin of the @em outer vertex (that which is incident with * a map vertex from some line of the sector boundary). * * @see inner() */ Vector3d const &outer() const; /** * Returns the origin of the @em inner vertex (that which extends away from * the "outer" vertex and across the shadowed surface). * * @see outer() */ Vector3d const &inner() const; /** * Returns the length of the shadow edge, which is measured as the distance * from the outer vertex origin to the inner origin. * * @see outer(), inner() */ inline coord_t length() const { return Vector3d(inner() - outer()).abs().length(); } private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_RENDER_FAKERADIO_SHADOWEDGE_H doomsday-stable-1.15.7/doomsday/client/include/render/rend_console.h0000664000175000017500000000373112641367670025022 0ustar jaakkojaakko#error "rend_console.h is no longer in use" #if 0 * /** @file rend_console.h * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /* * Console Rendering. */ #ifndef LIBDENG_CONSOLE_RENDER_H #define LIBDENG_CONSOLE_RENDER_H #include #ifdef __cplusplus extern "C" { #endif extern byte consoleShowFPS; void Rend_ConsoleRegister(void); /** * Initialize the console renderer (allows re-init). */ void Rend_ConsoleInit(void); void Rend_ConsoleTicker(timespan_t time); /** * Try to fulfill any pending console resize request. * To be called after a resolution change to resize the console. * * @param force @c true= Force a new resize request. * @return @c true= A resize is still pending. */ boolean Rend_ConsoleResize(boolean force); void Rend_ConsoleOpen(int yes); void Rend_ConsoleMove(int numLines); void Rend_ConsoleToggleFullscreen(void); void Rend_ConsoleCursorResetBlink(void); void Rend_ConsoleUpdateTitle(void); void Rend_Console(void); /// @param origin Origin of the display (top right) in screen-space coordinates. void Rend_ConsoleFPS(const Point2Raw* origin); #ifdef __cplusplus } // extern "C" #endif #endif /* LIBDENG_CONSOLE_RENDER_H */ #endifdoomsday-stable-1.15.7/doomsday/client/include/render/r_draw.h0000664000175000017500000000334612641367670023630 0ustar jaakkojaakko/** @file r_draw.h Misc Drawing routines. * * @ingroup render * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_RENDER_MISC_H #define DENG_CLIENT_RENDER_MISC_H #include "Texture" #include "TextureVariantSpec" #include void R_InitViewWindow(); void R_ShutdownViewWindow(); /** * Draws the border around the view for different size windows. */ void R_DrawViewBorder(); TextureVariantSpec const &Rend_PatchTextureSpec(int flags = 0, de::gl::Wrapping wrapS = de::gl::ClampToEdge, de::gl::Wrapping wrapT = de::gl::ClampToEdge); void R_DrawPatch(de::Texture &texture, int x, int y); void R_DrawPatch(de::Texture &texture, int x, int y, int w, int h, bool useOffsets = true); void R_DrawPatchTiled(de::Texture &texture, int x, int y, int w, int h, de::gl::Wrapping wrapS, de::gl::Wrapping wrapT); #endif // DENG_CLIENT_RENDER_MISC_H doomsday-stable-1.15.7/doomsday/client/include/render/rend_font.h0000664000175000017500000000430212641367670024321 0ustar jaakkojaakko/** @file rend_font.h * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * Font Renderer */ #ifndef LIBDENG_FONT_RENDERER #define LIBDENG_FONT_RENDERER #ifdef __cplusplus extern "C" { #endif #define DEFAULT_INITIALCOUNT (0) ///< Used for animating type-in effects. #define DEFAULT_ALIGNFLAGS (ALIGN_TOPLEFT) #define DEFAULT_DRAWFLAGS (DTF_NO_EFFECTS) #define FR_FORMAT_ESCAPE_CHAR ((char)0x10) ///< ASCII data link escape /** * Rendering formatted text requires a temporary working buffer in order * to split up the larger blocks into individual text fragments. * * A "small text" buffer is reserved from the application's data segment * for the express purpose of manipulating shortish text strings without * incurring any dynamic allocation overhead. This value defines the size * of this fixed-size working buffer as the number of characters it may * potentially hold. */ #define FR_SMALL_TEXT_BUFFER_SIZE (160) /// Initialize this module. void FR_Init(void); /// Shutdown this module. void FR_Shutdown(void); /// @return @c true= Font renderer has been initialized and is available. dd_bool FR_Available(void); void FR_Ticker(timespan_t ticLength); void FR_SetNoFont(void); // Utility routines: int FR_SingleLineHeight(const char* text); int FR_GlyphTopToAscent(const char* text); #ifdef __cplusplus } // extern "C" #endif #endif /* LIBDENG_FONT_RENDERER */ doomsday-stable-1.15.7/doomsday/client/include/render/consoleeffect.h0000664000175000017500000000453112641367670025166 0ustar jaakkojaakko/** @file consoleeffect.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_CONSOLEEFFECT_H #define DENG_CLIENT_CONSOLEEFFECT_H #include #include #include /** * Draws camera lens effects for a particular player console. Maintains * console-specific state and carries out the actual GL operations. */ class ConsoleEffect { public: ConsoleEffect(int console); virtual ~ConsoleEffect(); /** * Returns the console number. */ int console() const; /** * Determines the console's view rectangle in window coordinates. */ de::Rectanglei const &viewRect() const; bool isInited() const; de::GLShaderBank &shaders() const; /** * Allocate and prepare GL resources for drawing. Derived classes must call * the base class method if they override it. */ virtual void glInit(); /** * Release GL resources. Derived classes must call the base class method if * they override it. */ virtual void glDeinit(); /** * Called for all console effects when a frame begins. The methods * are called in the console's stack order. */ virtual void beginFrame(); /** * Called for all console effects in stack order, after the raw frame has * been drawn. */ virtual void draw(); /** * Called for all console effects when a frame ends. The methods are called * in reverse stack order, after the draw() methods have all been called. */ virtual void endFrame(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_CONSOLEEFFECT_H doomsday-stable-1.15.7/doomsday/client/include/render/drawlist.h0000664000175000017500000001261412641367670024201 0ustar jaakkojaakko/** @file drawlist.h Drawable primitive list. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_RENDER_DRAWLIST_H #define DENG_CLIENT_RENDER_DRAWLIST_H #include "gl/gltextureunit.h" #include "api_gl.h" // blendmode_e #include #include #include /// Semantic geometry group identifiers. enum GeomGroup { UnlitGeom = 0, ///< Normal, unlit geometries. LitGeom, ///< Normal, lit goemetries. SkyMaskGeom, ///< Sky mask geometries. LightGeom, ///< Dynamic light geometries. ShadowGeom, ///< Map object and/or Fake Radio shadow geometries. ShineGeom ///< Surface reflection geometries. }; /// Logical drawing modes. enum DrawMode { DM_SKYMASK, DM_ALL, DM_LIGHT_MOD_TEXTURE, DM_FIRST_LIGHT, DM_TEXTURE_PLUS_LIGHT, DM_UNBLENDED_TEXTURE_AND_DETAIL, DM_BLENDED, DM_BLENDED_FIRST_LIGHT, DM_NO_LIGHTS, DM_WITHOUT_TEXTURE, DM_LIGHTS, DM_MOD_TEXTURE, DM_MOD_TEXTURE_MANY_LIGHTS, DM_UNBLENDED_MOD_TEXTURE_AND_DETAIL, DM_BLENDED_MOD_TEXTURE, DM_ALL_DETAILS, DM_BLENDED_DETAILS, DM_SHADOW, DM_SHINY, DM_MASKED_SHINY, DM_ALL_SHINY }; /// Virtual/logical texture unit indices. These map to real GL texture units. enum texunitid_t { TU_PRIMARY = 0, TU_PRIMARY_DETAIL, TU_INTER, TU_INTER_DETAIL, NUM_TEXTURE_UNITS }; typedef uint TexUnitMap[MAX_TEX_UNITS]; /** * A buffered list of drawable GL primitives and/or GL commands. */ class DrawList { public: struct Spec { GeomGroup group; de::GLTextureUnit texunits[NUM_TEXTURE_UNITS]; Spec(GeomGroup group = UnlitGeom) : group(group) {} inline de::GLTextureUnit &unit(int index) { DENG2_ASSERT(index >= 0 && index < NUM_TEXTURE_UNITS); return texunits[index]; } inline de::GLTextureUnit const &unit(int index) const { DENG2_ASSERT(index >= 0 && index < NUM_TEXTURE_UNITS); return texunits[index]; } }; public: /** * Construct a new draw list. * * @param spec List specification. A copy is made. */ DrawList(Spec const &spec); /** * Write the given geometry primitive to the list. * * @param primitive Type identifier for the GL primitive being written. * @param isLit @todo Retrieve from list specification? * @param vertCount Number of vertices being written. * @param posCoods Map space position coordinates for each vertex. * @param colorCoords Color coordinates for each vertex (if any). * @param texCoords @em Primary texture coordinates for each vertex (if any). * @param interTexCoords @em Inter texture coordinates for each vertex (if any). * @param modTexture GL name of the modulation texture (if any). * @param modColor Modulation color (if any). * @param modTexCoords Modulation texture coordinates for each vertex (if any). */ DrawList &write(de::gl::Primitive primitive, blendmode_e blendMode, de::Vector2f const &texScale, de::Vector2f const &texOffset, de::Vector2f const &detailTexScale, de::Vector2f const &detailTexOffset, bool isLit, uint vertCount, de::Vector3f const *posCoords, de::Vector4f const *colorCoords = 0, de::Vector2f const *texCoords = 0, de::Vector2f const *interTexCoords = 0, GLuint modTexture = 0, de::Vector3f const *modColor = 0, de::Vector2f const *modTexCoords = 0); void draw(DrawMode mode, TexUnitMap const &texUnitMap) const; /** * Returns @c true iff there are no commands/geometries in the list. */ bool isEmpty() const; /** * Clear the list of all buffered GL commands, returning it to the default, * empty state. */ void clear(); /** * Return the read/write cursor to the beginning of the list, retaining all * allocated storage for buffered GL commands so that it can be reused. * * To be called at the beginning of a new render frame before any geometry * is written to the list. */ void rewind(); /** * Provides mutable access to the list's specification. Note that any changes * to this configuration will affect @em all geometry in the list. */ Spec &spec(); /** * Provides immutable access to the list's specification. */ Spec const &spec() const; private: DENG2_PRIVATE(d) }; typedef DrawList::Spec DrawListSpec; #endif // DENG_CLIENT_RENDER_DRAWLIST_H doomsday-stable-1.15.7/doomsday/client/include/render/r_things.h0000664000175000017500000000241512641367670024163 0ustar jaakkojaakko/** @file r_things.h Map Object => Vissprite Projection. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifdef __CLIENT__ #ifndef DENG_CLIENT_RENDER_THINGS_H #define DENG_CLIENT_RENDER_THINGS_H #include "world/p_object.h" /** * Generates vissprite(s) for given map-object if they might be visible. */ void R_ProjectSprite(mobj_t &mob); #endif // DENG_CLIENT_RENDER_THINGS_H #endif // __CLIENT__ doomsday-stable-1.15.7/doomsday/client/include/render/drawlists.h0000664000175000017500000000405712641367670024366 0ustar jaakkojaakko/** @file drawlists.h Drawable primitive list collection/management. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_RENDER_DRAWLISTS_H #define DENG_CLIENT_RENDER_DRAWLISTS_H #include "DrawList" #include #include class DrawLists { public: typedef QVarLengthArray FoundLists; public: DrawLists(); /** * Locate an appropriate draw list for the given specification. * * @param spec Draw list specification. * * @return The chosen list. */ DrawList &find(DrawListSpec const &spec); /** * Finds all draw lists which match the given specification. Note that only * non-empty lists are collected. * * @param group Logical geometry group identifier. * @param found Set of draw lists which match the result. * * @return Number of draw lists found. */ int findAll(GeomGroup group, FoundLists &found); /** * To be called before rendering of a new frame begins. */ void reset(); /** * All lists will be destroyed. */ void clear(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_RENDER_DRAWLISTS_H doomsday-stable-1.15.7/doomsday/client/include/render/materialcontext.h0000664000175000017500000000243112641367670025547 0ustar jaakkojaakko/** @file materialcontext.h Material render-context identifiers. * * @authors Copyright © 2009-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_RENDER_MATERIALCONTEXT_H #define CLIENT_RENDER_MATERIALCONTEXT_H /** * Material render-context identifier. * * @ingroup render */ enum MaterialContextId { FirstMaterialContextId = 0, UiContext = FirstMaterialContextId, MapSurfaceContext, SpriteContext, ModelSkinContext, PSpriteContext, SkySphereContext, LastMaterialContextId = SkySphereContext }; #endif // CLIENT_RENDER_MATERIALCONTEXT_H doomsday-stable-1.15.7/doomsday/client/include/render/rend_particle.h0000664000175000017500000000452212641367670025162 0ustar jaakkojaakko/** @file rend_particle.h Particle effect rendering. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_RENDER_PARTICLE_H #define CLIENT_RENDER_PARTICLE_H #include "world/map.h" // Maximum number of particle textures (not instances). #define MAX_PTC_TEXTURES 300 // Maximum number of particle models (not instances). #define MAX_PTC_MODELS 100 /// @ingroup render enum ParticleType { PTC_NONE, PTC_POINT, PTC_LINE, // New types can be added here. PTC_TEXTURE = 100, // ...followed by MAX_PTC_TEXTURES types. PTC_MODEL = 1000 }; DENG_EXTERN_C de::dbyte useParticles; void Rend_ParticleRegister(); /** * Loads all system-level textures (i.e., those available by default) * for a minimal resource set needed for particle rendering. */ void Rend_ParticleLoadSystemTextures(); void Rend_ParticleReleaseSystemTextures(); /** * Load any custom particle textures. They are loaded from the * highres texture directory and are named "ParticleNN.(tga|png|pcx)". * The first is "Particle00". (based on Leesz' textured particles mod) */ void Rend_ParticleLoadExtraTextures(); void Rend_ParticleReleaseExtraTextures(); /** * Render all the visible particle generators. * We must render all particles ordered back->front, or otherwise * particles from one generator will obscure particles from another. * This would be especially bad with smoke trails. */ void Rend_RenderParticles(de::Map &map); #endif // CLIENT_RENDER_PARTICLE_H doomsday-stable-1.15.7/doomsday/client/include/render/rend_fakeradio.h0000664000175000017500000001120612641367670025301 0ustar jaakkojaakko/** @file rend_fakeradio.h Faked Radiosity Lighting. * * Perhaps the most distinctive characteristic of radiosity lighting is that * the corners of a room are slightly dimmer than the rest of the surfaces. * (It's not the only characteristic, however.) We will fake these shadowed * areas by generating shadow polygons for wall segments and determining which * BSP leaf vertices will be shadowed. * * In other words, walls use shadow polygons (over entire lines), while planes * use vertex lighting. As sectors are usually partitioned into a great many * BSP leafs (and tesselated into triangles), they are better suited for vertex * lighting. In some cases we will be forced to split a BSP leaf into smaller * pieces than strictly necessary in order to achieve better accuracy in the * shadow effect. * * @authors Copyright © 2004-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RENDER_FAKERADIO #define DENG_RENDER_FAKERADIO #include "Line" #include "Sector" #include "Vertex" #include "WallEdge" #include "render/rendpoly.h" // r_vertex_t class ConvexSubspace; /** * FakeRadio shadow data. * @ingroup render */ struct shadowcorner_t { float corner; Sector *proximity; float pOffset; float pHeight; }; /** * FakeRadio connected edge data. * @ingroup render */ struct edgespan_t { float length; float shift; }; /** * Stores the FakeRadio properties of a LineSide. * @ingroup render */ struct LineSideRadioData { /// Frame number of last update int updateCount; shadowcorner_t topCorners[2]; shadowcorner_t bottomCorners[2]; shadowcorner_t sideCorners[2]; /// [bottom, top] edgespan_t spans[2]; }; /** * Register the console commands, variables, etc..., of this module. */ void Rend_RadioRegister(); /** * To be called after map load to perform necessary initialization within this module. */ void Rend_RadioInitForMap(de::Map &map); /** * Returns @c true iff @a line qualifies for (edge) shadow casting. */ bool Rend_RadioLineCastsShadow(Line const &line); /** * Returns @c true iff @a plane qualifies for (wall) shadow casting. */ bool Rend_RadioPlaneCastsShadow(Plane const &plane); /** * Returns the FakeRadio data for the specified line @a side. */ LineSideRadioData &Rend_RadioDataForLineSide(LineSide &side); /** * To be called to update the shadow properties for the specified line @a side. */ void Rend_RadioUpdateForLineSide(LineSide &side); /** * Updates all the shadow offsets for the given vertex. * * @pre Lineowner rings must be set up. * * @param vtx Vertex to be updated. */ void Rend_RadioUpdateVertexShadowOffsets(Vertex &vtx); /** * Returns the global shadow darkness factor, derived from values in Config. * Assumes that light level adaptation has @em NOT yet been applied (it will be). */ float Rend_RadioCalcShadowDarkness(float lightLevel); /** * Render FakeRadio for the specified wall section. Generates and then draws all * shadow geometry for the wall section. * * Note that unlike Rend_RadioBspLeafEdges() there is no guard to ensure shadow * geometry is rendered only once per frame. * * @param leftEdge Geometry for the left edge of the wall section. * @param rightEdge Geometry for the right edge of the wall section. * @param shadowDark Shadow darkness scale factor. * @param shadowSize Shadow size scale factor. */ void Rend_RadioWallSection(de::WallEdge const &leftEdge, de::WallEdge const &rightEdge, float shadowDark, float shadowSize); /** * Render FakeRadio for the given subspace. Draws all shadow geometry linked to * the ConvexSubspace, that has not already been rendered. */ void Rend_RadioSubspaceEdges(ConvexSubspace const &subspace); /** * Render the shadow poly vertices, for debug. */ #ifdef DENG_DEBUG void Rend_DrawShadowOffsetVerts(); #endif #endif // DENG_RENDER_FAKERADIO doomsday-stable-1.15.7/doomsday/client/include/render/billboard.h0000664000175000017500000000777712641367670024320 0ustar jaakkojaakko/** @file billboard.h Rendering billboard "sprites". * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_RENDER_BILLBOARD_H #define CLIENT_RENDER_BILLBOARD_H #include "dd_types.h" #include "Material" #include "MaterialAnimator" #include "MaterialVariantSpec" class BspLeaf; struct vissprite_t; /** * Billboard drawing arguments for a masked wall. * * A sort of a sprite, I guess... Masked walls must be rendered sorted with * sprites, so no artifacts appear when sprites are seen behind masked walls. * * @ingroup render */ struct drawmaskedwallparams_t { MaterialAnimator *animator; blendmode_t blendMode; ///< Blendmode to be used when drawing (two sided mid textures only) struct wall_vertex_s { de::dfloat pos[3]; ///< x y and z coordinates. de::dfloat color[4]; } vertices[4]; de::ddouble texOffset[2]; de::dfloat texCoord[2][2]; ///< u and v coordinates. DGLuint modTex; ///< Texture to modulate with. de::dfloat modTexCoord[2][2]; ///< [top-left, bottom-right][x, y] de::dfloat modColor[4]; }; void Rend_DrawMaskedWall(drawmaskedwallparams_t const &parms); /** * Billboard drawing arguments for a "player" sprite (HUD sprite). * @ingroup render */ struct rendpspriteparams_t { de::dfloat pos[2]; ///< [X, Y] Screen-space position. de::dfloat width; de::dfloat height; Material *mat; de::dfloat texOffset[2]; dd_bool texFlip[2]; ///< [X, Y] Flip along the specified axis. de::dfloat ambientColor[4]; de::duint vLightListIdx; }; void Rend_DrawPSprite(rendpspriteparams_t const &parms); /** * Billboard drawing arguments for a map entity, sprite visualization. * @ingroup render */ struct drawspriteparams_t { dd_bool noZWrite; blendmode_t blendMode; MaterialAnimator *matAnimator; dd_bool matFlip[2]; ///< [S, T] Flip along the specified axis. BspLeaf *bspLeaf; }; void Rend_DrawSprite(vissprite_t const &spr); de::MaterialVariantSpec const &Rend_SpriteMaterialSpec(de::dint tclass = 0, de::dint tmap = 0); /** * @defgroup rendFlareFlags Flare renderer flags * @ingroup render * @{ */ #define RFF_NO_PRIMARY 0x1 ///< Do not draw a primary flare (aka halo). #define RFF_NO_TURN 0x2 ///< Flares do not turn in response to viewangle/viewdir /**@}*/ /** * Billboard drawing arguments for a lens flare. * * @see H_RenderHalo() * @ingroup render */ struct drawflareparams_t { de::dbyte flags; ///< @ref rendFlareFlags. de::dint size; de::dfloat color[3]; de::dbyte factor; de::dfloat xOff; DGLuint tex; ///< Flaremap if flareCustom ELSE (flaretexName id. Zero = automatical) de::dfloat mul; ///< Flare brightness factor. dd_bool isDecoration; de::dint lumIdx; }; DENG_EXTERN_C de::dint alwaysAlign; DENG_EXTERN_C de::dint spriteLight; DENG_EXTERN_C de::dint useSpriteAlpha; DENG_EXTERN_C de::dint useSpriteBlend; DENG_EXTERN_C de::dint noSpriteZWrite; DENG_EXTERN_C de::dbyte noSpriteTrans; DENG_EXTERN_C de::dbyte devNoSprites; DENG_EXTERN_C void Rend_SpriteRegister(); #endif // CLIENT_RENDER_BILLBOARD_H doomsday-stable-1.15.7/doomsday/client/include/render/surfacedecorator.h0000664000175000017500000000555312641367670025707 0ustar jaakkojaakko/** @file surfacedecorator.h World surface decorator. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_RENDER_SURFACEDECORATOR_H #define CLIENT_RENDER_SURFACEDECORATOR_H #include class Surface; /** * The decorator assumes responsibility for decorating surfaces according to * the defined material when the surface is assigned. When a material changes * (e.g., animation) the decorator automatically schedules these surfaces for * redecoration. * * Note that it is the responsibility of the user to inform the decorator when * a surface moves or a new material is assigned. Otherwise decorations may not * be updated or done so using an out of date material. * * @ingroup render */ class SurfaceDecorator { public: /** * Construct a new surface decorator. */ SurfaceDecorator(); /** * Perform one time decoration of a single @a surface. The surface will not * be remembered and any resulting decorations will not be updated later. */ void decorate(Surface &surface); /** * Perform scheduled (re)decoration work. */ void redecorate(); /** * Forget all known surfaces and previous decorations. */ void reset(); /** * Add the specified @a surface to the decorator's job list. If the surface * is already on this list then nothing will happen. If the surface lacks a * material or no decorations are defined for this material, then nothing * will happen. * * Any existing decorations are retained until the material next changes. * * @param surface Surface to add to the job list. */ void add(Surface *surface); /** * Remove the specified @a surface from the decorator's job list. If the * surface is not on this list then nothing will happen. * * Any existing decorations are retained and will no longer be updated. * * @param surface Surface to remove from the job list. */ void remove(Surface *surface); private: DENG2_PRIVATE(d) }; #endif // CLIENT_RENDER_SURFACEDECORATOR_H doomsday-stable-1.15.7/doomsday/client/include/render/angleclipper.h0000664000175000017500000001107512641367670025015 0ustar jaakkojaakko/** @file angleclipper.h Angle Clipper. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_RENDER_ANGLECLIPPER #define CLIENT_RENDER_ANGLECLIPPER #include #include #include "Face" /** * Inclusive > inclusive binary angle range. * * @ingroup data */ struct AngleRange { binangle_t from; binangle_t to; explicit AngleRange(binangle_t a = 0, binangle_t b = 0) : from(a), to(b) {} /** * @return @c 0= "this" completely includes @a other. * @c 1= "this" contains the beginning of @a other. * @c 2= "this" contains the end of @a other. * @c 3= @other completely contains "this". * @c -1= No meaningful relationship. */ de::dint relationship(AngleRange const &other) { if(from >= other.from && to <= other.to) return 0; if(from >= other.from && from < other.to) return 1; if(to > other.from && to <= other.to) return 2; if(from <= other.from && to >= other.to) return 3; return -1; } }; /** * 360 degree, polar angle(-range) clipper. * * The idea is to keep track of occluded angles around the camera. Since BSP leafs are * rendered front-to-back, the occlusion lists start a frame empty and eventually fill * up to cover the whole 360 degrees around the camera. * * Oranges (occlusion ranges) clip a half-space on an angle range. These are produced * by horizontal edges that have empty space behind. * * @ingroup render */ class AngleClipper { public: AngleClipper(); /** * Returns non-zero if clipnodes cover the whole range [0..360] degrees. */ de::dint isFull() const; /** * Returns non-zero if the given map-space @param angle is @em not-yet occluded. */ de::dint isAngleVisible(binangle_t angle) const; /** * Returns non-zero if the given map-space @a point is @em not-yet occluded. * * @param point Map-space coordinates to test. */ de::dint isPointVisible(de::Vector3d const &point) const; /** * Returns non-zero if @em any portion of the given map-space, convex face geometry * is @em not-yet occluded. * * @param poly Map-space convex face geometry to test. */ de::dint isPolyVisible(de::Face const &poly) const; public: // --------------------------------------------------------------------------- /** * */ void clearRanges(); /** * @param from * @param to */ de::dint safeAddRange(binangle_t from, binangle_t to); /** * Add a segment relative to the current viewpoint. * * @param from Map-space coordinates describing the start-point. * @param to Map-space coordinates describing the end-point. */ void addRangeFromViewRelPoints(de::Vector2d const &from, de::Vector2d const &to); /** * Add an occlusion segment relative to the current viewpoint. * * @param from Map-space coordinates for the start-point. * @param to Map-space coordinates for the end-point. * @param height * @param topHalf */ void addViewRelOcclusion(de::Vector2d const &from, de::Vector2d const &to, coord_t height, bool topHalf); /** * Check a segment relative to the current viewpoint. * * @param from Map-space coordinates for the start-point. * @param to Map-space coordinates for the end-point. */ de::dint checkRangeFromViewRelPoints(de::Vector2d const &from, de::Vector2d const &to); #ifdef DENG2_DEBUG /** * A debugging aid: checks if clipnode links are valid. */ void validate(); #endif private: DENG2_PRIVATE(d) }; #endif // CLIENT_RENDER_ANGLECLIPPER doomsday-stable-1.15.7/doomsday/client/include/render/mobjanimator.h0000664000175000017500000000273312641367670025033 0ustar jaakkojaakko/** @file mobjanimator.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_RENDER_MOBJANIMATOR_H #define DENG_CLIENT_RENDER_MOBJANIMATOR_H #include "render/modelrenderer.h" #include /** * Mobj-specific model animator. * * The state and movement of the mobj determine which animation sequences are started. */ class MobjAnimator : public de::ModelDrawable::Animator { public: MobjAnimator(de::DotPath const &id, de::ModelDrawable const &model); void triggerByState(de::String const &stateName); void advanceTime(de::TimeDelta const &elapsed); de::ddouble currentTime(int index) const; private: ModelRenderer::StateAnims const *_stateAnims; }; #endif // DENG_CLIENT_RENDER_MOBJANIMATOR_H doomsday-stable-1.15.7/doomsday/client/include/render/viewports.h0000664000175000017500000001343012641367670024407 0ustar jaakkojaakko/** @file viewports.h Player viewports and related low-level rendering. * * @author Copyright © 2003-2014 Jaakko Keränen * @author Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_VIEWPORTS_H #define DENG_CLIENT_VIEWPORTS_H #ifdef __SERVER__ # error "viewports.h is for the client only" #endif #include #include #include class ConvexSubspace; struct Generator; class Lumobj; struct viewport_t { int console; de::Rectanglei geometry; }; struct viewer_t { de::Vector3d origin; float pitch; viewer_t(de::Vector3d const &origin = de::Vector3d(), angle_t angle = 0, float pitch = 0) : origin(origin) , pitch(pitch) , _angle(angle) {} viewer_t(viewer_t const &other) : origin(other.origin) , pitch(other.pitch) , _angle(other._angle) {} viewer_t lerp(viewer_t const &end, float pos) const { return viewer_t(de::lerp(origin, end.origin, pos), _angle + int(pos * (int(end._angle) - int(_angle))), de::lerp(pitch, end.pitch, pos)); } angle_t angle() const; angle_t angleWithoutHeadTracking() const { return _angle; } void setAngle(angle_t a) { _angle = a; } private: angle_t _angle; }; struct viewdata_t { viewer_t current; viewer_t lastSharp[2]; ///< For smoothing. viewer_t latest; ///< "Sharp" values taken from here. /* * These vectors are in the DGL coordinate system, which is a left-handed * one (same as in the game, but Y and Z have been swapped). Anyone who uses * these must note that it might be necessary to fix the aspect ratio of the * Y axis by dividing the Y coordinate by 1.2. */ de::Vector3f frontVec, upVec, sideVec; /* to the left */ float viewCos, viewSin; de::Rectanglei window, windowTarget, windowOld; float windowInter; }; enum ViewPortLayer { Player3DViewLayer, ViewBorderLayer, HUDLayer }; DENG_EXTERN_C int rendInfoTris; DENG_EXTERN_C dd_bool firstFrameAfterLoad; /** * Register console variables. */ void Viewports_Register(); int R_FrameCount(); void R_ResetFrameCount(); /** * Render all view ports in the viewport grid. */ void R_RenderViewPorts(ViewPortLayer layer); /** * Render a blank view for the specified player. */ void R_RenderBlankView(); /** * Draw the border around the view window. */ void R_RenderPlayerViewBorder(); /// @return Current viewport; otherwise @c 0. viewport_t const *R_CurrentViewPort(); /** * Set the current GL viewport. */ void R_UseViewPort(viewport_t const *vp); viewdata_t const *R_ViewData(int consoleNum); void R_UpdateViewer(int consoleNum); void R_ResetViewer(); int R_NextViewer(); void R_ClearViewData(); /** * To be called at the beginning of a render frame to perform necessary initialization. */ void R_BeginFrame(); /** * Update the sharp world data by rotating the stored values of plane * heights and sharp camera positions. */ void R_NewSharpWorld(); /** * Returns @c true iff the subspace is marked as visible for the current frame. * * @see R_ViewerSubspaceMarkVisible() */ bool R_ViewerSubspaceIsVisible(ConvexSubspace const &subspace); /** * Mark the subspace as visible for the current frame. * * @see R_ViewerSubspaceIsVisible() */ void R_ViewerSubspaceMarkVisible(ConvexSubspace const &subspace, bool yes = true); /** * Returns @c true iff the (particle) generator is marked as visible for the current frame. * * @see R_ViewerGeneratorMarkVisible() */ bool R_ViewerGeneratorIsVisible(Generator const &generator); /** * Mark the (particle) generator as visible for the current frame. * * @see R_ViewerGeneratorIsVisible() */ void R_ViewerGeneratorMarkVisible(Generator const &generator, bool yes = true); /// @return Distance in map space units between the lumobj and viewer. double R_ViewerLumobjDistance(int idx); /// @return @c true if the lumobj is clipped for the viewer. bool R_ViewerLumobjIsClipped(int idx); /// @return @c true if the lumobj is hidden for the viewer. bool R_ViewerLumobjIsHidden(int idx); /** * Clipping strategy: * * If culling world surfaces with the angle clipper and the viewer is not in the * void; use the angle clipper. Otherwise, use the BSP-based LOS algorithm. */ void R_ViewerClipLumobj(Lumobj *lum); void R_ViewerClipLumobjBySight(Lumobj *lum, ConvexSubspace *subspace); /** * Attempt to set up a view grid and calculate the viewports. Set 'numCols' and * 'numRows' to zero to just update the viewport coordinates. */ bool R_SetViewGrid(int numCols, int numRows); void R_SetupDefaultViewWindow(int consoleNum); /** * Animates the view window towards the target values. */ void R_ViewWindowTicker(int consoleNum, timespan_t ticLength); /** * Returns the model-view-projection matrix for the camera position and orientation * in the current frame. Remains the same thoughtout rendering of the frame. * * @return MVP matrix. */ de::Matrix4f const &Viewer_Matrix(); #endif // DENG_CLIENT_VIEWPORTS_H doomsday-stable-1.15.7/doomsday/client/include/render/decoration.h0000664000175000017500000000474212641367670024502 0ustar jaakkojaakko/** @file decoration.h World surface decoration. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_RENDER_DECORATION_H #define CLIENT_RENDER_DECORATION_H #include #include #include "MapObject" #include "MaterialAnimator" class Surface; /// No decorations are visible beyond this. #define MAX_DECOR_DISTANCE (2048) /** * World surface decoration. * @ingroup render */ class Decoration : public de::MapObject { public: /// Required surface is missing. @ingroup errors DENG2_ERROR(MissingSurfaceError); public: /** * Construct a new decoration. * * @param source Source of the decoration (a material). * @param origin Origin of the decoration in map space. */ Decoration(MaterialAnimator::Decoration const &source, de::Vector3d const &origin = de::Vector3d()); virtual ~Decoration(); /** * Returns the source of the decoration. * * @see hasSource(), setSource() */ MaterialAnimator::Decoration const &source() const; /** * Returns @c true iff a surface is attributed for the decoration. * * @see surface(), setSurface() */ bool hasSurface() const; /** * Convenient method which returns the surface owner of the decoration. */ Surface &surface(); Surface const &surface() const; /** * Change the attributed surface of the decoration. * * @param newSurface Map surface to attribute. Use @c 0 to clear. */ void setSurface(Surface *newSurface); private: DENG2_PRIVATE(d) }; #endif // CLIENT_RENDER_DECORATION_H doomsday-stable-1.15.7/doomsday/client/include/render/trianglestripbuilder.h0000664000175000017500000001407512641367670026611 0ustar jaakkojaakko/** @file render/trianglestripbuilder.h Triangle Strip Geometry Builder. * * @authors Copyright © 2011-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RENDER_TRIANGLE_STRIP_BUILDER #define DENG_RENDER_TRIANGLE_STRIP_BUILDER #include /// @todo Remove me #include #include namespace de { /** * Abstract interface for a component that can be interpreted as an "edge" * geometry. * * @ingroup math */ class IEdge { public: class IEvent { public: virtual ~IEvent() {} virtual bool operator < (IEvent const &other) const { return distance() < other.distance(); } virtual double distance() const = 0; }; public: virtual ~IEdge() {} virtual bool isValid() const = 0; virtual IEvent const &first() const = 0; virtual IEvent const &last() const = 0; }; } // namespace de namespace de { /** * @ingroup render */ class AbstractEdge : public IEdge { public: typedef int EventIndex; /// Special identifier used to mark an invalid event index. enum { InvalidIndex = -1 }; class Event : public IEvent { public: virtual ~Event() {} virtual Vector3d origin() const = 0; }; public: virtual ~AbstractEdge() {} virtual Event const &first() const = 0; virtual Event const &last() const = 0; virtual Vector2f materialOrigin() const { return Vector2f(); } virtual Vector3f normal() const { return Vector3f(); } }; } // namespace de namespace de { /** * @ingroup world */ class WorldEdge : public AbstractEdge { public: class Event : public AbstractEdge::Event { public: virtual ~Event() {} virtual Vector3d origin() const = 0; inline double z() const { return origin().z; } }; public: WorldEdge(Vector2d origin_) : AbstractEdge(), _origin(origin_) {} virtual ~WorldEdge() {} /** * Returns the X|Y origin of the edge in the map coordinate space. */ Vector2d const &origin() const { return _origin; } virtual Event const &first() const = 0; virtual Event const &last() const = 0; virtual Event const &at(EventIndex index) const = 0; virtual int divisionCount() const { return 0; } virtual EventIndex firstDivision() const { return InvalidIndex; } virtual EventIndex lastDivision() const { return InvalidIndex; } private: Vector2d _origin; }; } // namespace de namespace de { typedef QVarLengthArray PositionBuffer; typedef QVarLengthArray TexCoordBuffer; /** * Abstract triangle strip geometry builder. * * Encapsulates the logic of constructing triangle strip geometries. * * @ingroup render * * @todo Separate the backing store with an external allocator mechanism. * (Geometry should not be owned by the builder.) * @todo Support custom vertex types. * (Once implemented move this component to libgui.) * @todo Support building strips by adding single vertices. */ class TriangleStripBuilder { public: /** * Construct a new triangle strip builder. * * @param buildTexCoords @c true= construct texture coordinates also. */ TriangleStripBuilder(bool buildTexCoords = false); /** * Begin construction of a new triangle strip geometry. Any existing unclaimed * geometry is discarded. * * Vertex layout: * 1--3 2--0 * | | or | | if @a direction @c =Anticlockwise * 0--2 3--1 * * @param direction Initial vertex winding direction. * * @param reserveElements Initial number of vertex elements to reserve. If the * user knows in advance roughly how many elements are required for the geometry * this number may be reserved from the outset, thereby improving performance * by minimizing dynamic memory allocations. If the estimate is off the only * side effect is reduced performance. */ void begin(ClockDirection direction, int reserveElements = 0); /** * Submit an edge geometry to extend the current triangle strip geometry. * * @param edge Edge geometry to extend with. */ void extend(AbstractEdge &edge); /** * Submit an edge geometry to extend the current triangle strip geometry. * * @param edge Edge geometry to extend with. * * @return Reference to this trangle strip builder. * * @see extend() */ inline TriangleStripBuilder &operator << (AbstractEdge &edge) { extend(edge); return *this; } /** * Returns the total number of vertex elements in the current strip geometry. * If no strip is currently being built @c 0 is returned. */ int numElements() const; /** * Take ownership of the last built strip of geometry. * * @param positions The address of the buffer containing the vertex position * values is written here. * @param texcoords The address of the buffer containing the texture coord * values is written here. Can be @c 0. * * @return Total number of vertex elements in the geometry (for convenience). */ int take(PositionBuffer **positions, TexCoordBuffer **texcoords = 0); private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_RENDER_TRIANGLE_STRIP_BUILDER doomsday-stable-1.15.7/doomsday/client/include/render/vr.h0000664000175000017500000000271212641367670022775 0ustar jaakkojaakko/** @file render/vr.h Stereoscopic rendering and Oculus Rift support. * * @authors Copyright (c) 2013 Christopher Bruns * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_RENDER_VR_H #define CLIENT_RENDER_VR_H #include "dd_types.h" #include de::VRConfig &vrCfg(); namespace VR { /// (UNUSED) Distance from player character to weapon sprite, in map units extern float weaponDistance; } /** * Register VR console variables. */ void VR_ConsoleRegister(); /** * Returns the horizontal field of view in Oculus Rift in degrees. */ //float VR_RiftFovX(); /** * Load Oculus Rift parameters via Rift SDK. */ //bool VR_LoadRiftParameters(); #endif // CLIENT_RENDER_VR_H doomsday-stable-1.15.7/doomsday/client/include/render/wallspec.h0000664000175000017500000000603712641367670024164 0ustar jaakkojaakko/** @file render/wallspec.h Wall Geometry Specification. * * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RENDER_WALLSPEC #define DENG_RENDER_WALLSPEC #include #include "Line" namespace de { /** * Wall geometry specification. The members are public for convenient access. */ class WallSpec { public: enum Flag { /// Force the geometry to be opaque, irrespective of material opacity. ForceOpaque = 0x001, /// Fade out the geometry the closer it is to the viewer. NearFade = 0x002, /** * Clip the geometry if the neighbor plane surface relevant for the * specified section (i.e., the floor if @c Side::Bottom or ceiling if * @c Side::Top) has a sky-masked material bound to it. */ SkyClip = 0x004, /// Sort the dynamic light projections by descending luminosity. SortDynLights = 0x008, /// Do not generate geometry for dynamic lights. NoDynLights = 0x010, /// Do not generate geometry for dynamic (mobj) shadows. NoDynShadows = 0x020, /// Do not generate geometry for faked radiosity. NoFakeRadio = 0x040, /// Do not apply angle based light level deltas. NoLightDeltas = 0x080, /// Do not intercept edges with neighboring geometries. NoEdgeDivisions = 0x100, /// Do not smooth edge normals. NoEdgeNormalSmoothing = 0x200, DefaultFlags = ForceOpaque | SkyClip }; Q_DECLARE_FLAGS(Flags, Flag) /// Specification flags. Flags flags; /// Wall section identifier. int section; /** * Construct a default wall geometry specification for the specifed @a section. */ WallSpec(int section, Flags flags = DefaultFlags) : flags(flags), section(section) {} /** * Construct a wall geometry specification appropriate for the specified * @a side and @a section of a map Line considering the current map renderer * configuration. */ static WallSpec fromMapSide(LineSide const &side, int section); }; Q_DECLARE_OPERATORS_FOR_FLAGS(WallSpec::Flags) } // namespace de #endif // DENG_RENDER_WALLSPEC doomsday-stable-1.15.7/doomsday/client/include/ui/0000775000175000017500000000000012641367670021331 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/ui/sys_input.h0000664000175000017500000000710312641367670023540 0ustar jaakkojaakko/** * @file sys_input.h * Keyboard and mouse input pre-processing. @ingroup input * * @see joystick.h * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_SYSTEM_INPUT_H #define LIBDENG_SYSTEM_INPUT_H #ifndef __CLIENT__ # error "Input requires __CLIENT__" #endif #include #include "joystick.h" #ifdef __cplusplus extern "C" { #endif // Key event types. enum { IKE_NONE, IKE_DOWN, IKE_UP, IKE_REPEAT }; // Mouse buttons. enum { IMB_LEFT, IMB_MIDDLE, IMB_RIGHT, IMB_MWHEELUP, // virtual button IMB_MWHEELDOWN, // virtual button IMB_EXTRA1, IMB_EXTRA2, // ...other buttons... IMB_MWHEELLEFT = 14, // virtual button IMB_MWHEELRIGHT = 15, // virtual button IMB_MAXBUTTONS = 16 }; // Mouse axes. enum { IMA_POINTER, IMA_WHEEL, IMA_MAXAXES }; typedef struct keyevent_s { byte type; ///< Type of the event. int ddkey; ///< DDKEY code. int native; ///< Native code (use this to check for physically equivalent keys). char text[8]; ///< For characters, latin1-encoded text to insert. /// @todo Unicode } keyevent_t; typedef struct mousestate_s { struct { int x; int y; } axis[IMA_MAXAXES]; ///< Relative X and Y. int buttonDowns[IMB_MAXBUTTONS]; ///< Button down count. int buttonUps[IMB_MAXBUTTONS]; ///< Button up count. } mousestate_t; typedef struct mouseinterface_s { int (*init)(void); ///< Initialize the mouse. void (*shutdown)(void); void (*poll)(void); ///< Polls the current state of the mouse. void (*getState)(mousestate_t*); void (*trap)(dd_bool); ///< Enable or disable mouse grabbing. } mouseinterface_t; void I_Register(void); /** * Initialize input. * * @return @c true, if successful. */ dd_bool I_InitInterfaces(void); void I_ShutdownInterfaces(void); /** * Submits a new key event for preprocessing. The event has likely just been * received from the windowing system. * * @param type Type of the event (IKE_*). * @param ddKey DDKEY code. * @param native Native code. Identifies the physical key. * @param text For characters, latin1-encoded text to insert. Otherwise @c NULL. */ void Keyboard_Submit(int type, int ddKey, int native, const char* text); size_t Keyboard_GetEvents(keyevent_t *evbuf, size_t bufsize); dd_bool Mouse_IsPresent(void); void Mouse_Trap(dd_bool enabled); /** * Polls the current state of the mouse. This is called at regular intervals. */ void Mouse_Poll(void); void Mouse_GetState(mousestate_t *state); #ifdef __cplusplus } // extern "C" #endif #endif doomsday-stable-1.15.7/doomsday/client/include/ui/joystick.h0000664000175000017500000000420212641367670023337 0ustar jaakkojaakko/** @file joystick.h Joystick input pre-processing. * * @see sys_input.h * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_SYSTEM_JOYSTICK_H #define LIBDENG_SYSTEM_JOYSTICK_H #ifdef __SERVER__ # error Joystick is not available in a SERVER build #endif #include "dd_types.h" #ifdef __cplusplus extern "C" { #endif #define IJOY_AXISMIN -10000 #define IJOY_AXISMAX 10000 #define IJOY_MAXAXES 32 #define IJOY_MAXBUTTONS 32 #define IJOY_MAXHATS 4 #define IJOY_POV_CENTER -1 typedef struct joystate_s { int numAxes; ///< Number of axes present. int numButtons; ///< Number of buttons present. int numHats; ///< Number of hats present. int axis[IJOY_MAXAXES]; int buttonDowns[IJOY_MAXBUTTONS]; ///< Button down count. int buttonUps[IJOY_MAXBUTTONS]; ///< Button up count. float hatAngle[IJOY_MAXHATS]; ///< 0 - 359 degrees. } joystate_t; void Joystick_Register(void); dd_bool Joystick_Init(void); void Joystick_Shutdown(void); dd_bool Joystick_IsPresent(void); void Joystick_GetState(joystate_t *state); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_SYSTEM_JOYSTICK_H doomsday-stable-1.15.7/doomsday/client/include/ui/zonedebug.h0000664000175000017500000000222512641367670023465 0ustar jaakkojaakko/** * @file zonedebug.h * Memory zone debug visualization. @ingroup memzone * * @authors Copyright (c) 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_ZONEDEBUG_H #define CLIENT_ZONEDEBUG_H #ifdef _DEBUG #ifdef __cplusplus extern "C" { #endif void Z_DebugDrawer(void); #ifdef __cplusplus } // extern "C" #endif #endif // _DEBUG #endif // CLIENT_ZONEDEBUG_H doomsday-stable-1.15.7/doomsday/client/include/ui/editors/0000775000175000017500000000000012641367670023002 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/ui/editors/rendererappearanceeditor.h0000664000175000017500000000323312641367670030211 0ustar jaakkojaakko/** @file rendererappearanceeditor.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_RENDERERAPPEARANCEEDITOR_H #define DENG_CLIENT_RENDERERAPPEARANCEEDITOR_H #include #include /** * Editor for modifying the settings for the renderer's visual appearance. * * Automatically installs itself into the main window's right sidebar. * * @see ClientApp::rendererAppearanceSettings() */ class RendererAppearanceEditor : public de::PanelWidget, public de::IPersistent { Q_OBJECT public: RendererAppearanceEditor(); void operator >> (de::PersistentState &toState) const; void operator << (de::PersistentState const &fromState); public slots: void foldAll(); void unfoldAll(); protected: void preparePanelForOpening(); void panelDismissed(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_RENDERERAPPEARANCEEDITOR_H doomsday-stable-1.15.7/doomsday/client/include/ui/bindcontext.h0000664000175000017500000001521012641367670024022 0ustar jaakkojaakko/** @file bindcontext.h Input system binding context. * * @authors Copyright © 2009-2013 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_INPUTSYSTEM_BINDCONTEXT_H #define CLIENT_INPUTSYSTEM_BINDCONTEXT_H #include #include #include #include "ImpulseBinding" // ibcontroltype_t /// @todo: Move to public API typedef int (*FallbackResponderFunc)(event_t *); typedef int (*DDFallbackResponderFunc)(ddevent_t const *); // todo ends struct PlayerImpulse; /** * Contextualized grouping of input (and windowing system) event bindings. * * @todo There should be one of these in every Widget that has bindable actions. * When that's done, many of the existing binding contexts become obsolete. There * should still be support for several alternative contexts within one widget, * for instance depending on the mode of the widget (e.g., automap pan). * * @todo: Conceptually the fallback responders don't belong; instead of "responding" * (immediately performing a reaction), we should be returning an Action instance. -jk * * @ingroup ui */ class BindContext { public: /// Notified when the active state of the context changes. DENG2_DEFINE_AUDIENCE2(ActiveChange, void bindContextActiveChanged(BindContext &context)) /// Notified when the list of devices to acquire changes. DENG2_DEFINE_AUDIENCE2(AcquireDeviceChange, void bindContextAcquireDeviceChanged(BindContext &context)) /// Notified whenever a new binding is made in this context. DENG2_DEFINE_AUDIENCE2(BindingAddition, void bindContextBindingAdded(BindContext &context, de::Record &binding, bool isCommand)) public: /** * @param name Symbolic name for the context. */ explicit BindContext(de::String const &name); ~BindContext(); /** * Returns @c true if the context is @em active, meaning, bindings in the context * are in effect and their associated action(s) will be executed if triggered. * * @see activate(), deactivate() */ bool isActive() const; /** * Returns @c true if the context is @em protected, meaning, it should not be * manually (de)activated by the end user, directly. * * @see protect(), unprotect() */ bool isProtected() const; /** * Change the @em protected state of the context. * * @param yes @c true= protected. * * @see isProtected() */ void protect(bool yes = true); inline void unprotect(bool yes = true) { protect(!yes); } /** * Returns the symbolic name of the context. */ de::String name() const; void setName(de::String const &newName); /** * (De)activate the context, causing re-evaluation of the binding context stack. * The effective bindings for events may change as a result of calling this. * * @param yes @c true= activate if inactive, and vice versa. */ void activate(bool yes = true); inline void deactivate(bool yes = true) { activate(!yes); } void acquire(int deviceId, bool yes = true); void acquireAll(bool yes = true); bool willAcquire(int deviceId) const; bool willAcquireAll() const; public: // Binding management: ------------------------------------------------------ void clearAllBindings(); /** * @param id Unique identifier of the binding to delete. * @return @c true if the binding was found and deleted. */ bool deleteBinding(int id); // Commands --------------------------------------------------------------------- de::Record *bindCommand(char const *eventDesc, char const *command); /** * @param deviceId (@c < 0 || >= NUM_INPUT_DEVICES) for wildcard search. */ de::Record *findCommandBinding(char const *command, int deviceId = -1) const; /** * Iterate through all the CommandBindings of the context. */ de::LoopResult forAllCommandBindings(std::function func) const; /** * Returns the total number of command bindings in the context. */ int commandBindingCount() const; // Impulses --------------------------------------------------------------------- /** * @param ctrlDesc Device-control descriptor. * @param impulse Player impulse to bind to. * @param localPlayer Local player number. * * @todo: Parse the the impulse descriptor here? -ds */ de::Record *bindImpulse(char const *ctrlDesc, PlayerImpulse const &impulse, int localPlayer); de::Record *findImpulseBinding(int deviceId, ibcontroltype_t bindType, int controlId) const; /** * Iterate through all the ImpulseBindings of the context. * * @param localPlayer (@c < 0 || >= DDMAXPLAYERS) for all local players. */ de::LoopResult forAllImpulseBindings(int localPlayer, std::function func) const; inline de::LoopResult forAllImpulseBindings(std::function func) const { return forAllImpulseBindings(-1/*all local players*/, func); } /** * Returns the total number of impulse bindings in the context. * * @param localPlayer (@c < 0 || >= DDMAXPLAYERS) for all local players. */ int impulseBindingCount(int localPlayer = -1) const; public: // Triggering: -------------------------------------------------------------- /** * @param event Event to be tried. * @param respectHigherContexts Bindings shadowed by higher active contexts. * * @return @c true if the event triggered an action or was eaten by a responder. */ bool tryEvent(ddevent_t const &event, bool respectHigherContexts = true) const; void setFallbackResponder(FallbackResponderFunc newResponderFunc); void setDDFallbackResponder(DDFallbackResponderFunc newResponderFunc); private: DENG2_PRIVATE(d) }; #endif // CLIENT_INPUTSYSTEM_BINDCONTEXT_H doomsday-stable-1.15.7/doomsday/client/include/ui/inputdevicebuttoncontrol.h0000664000175000017500000000367512641367670026671 0ustar jaakkojaakko/** @file inputdevicebuttoncontrol.h Button control for a logical input device. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_INPUTSYSTEM_INPUTDEVICEBUTTONCONTROL_H #define CLIENT_INPUTSYSTEM_INPUTDEVICEBUTTONCONTROL_H #include #include "inputdevice.h" /** * Models a button control on a "physical" input device (e.g., key on a keyboard). * * @ingroup ui */ class InputDeviceButtonControl : public InputDeviceControl { public: explicit InputDeviceButtonControl(de::String const &name = ""); virtual ~InputDeviceButtonControl(); /** * Returns @c true if the button is currently in the down (i.e., pressed) state. */ bool isDown() const; /** * Change the "down" state of the button. */ void setDown(bool yes); /** * When the state of the control last changed, in milliseconds since app init. */ de::duint time() const; de::String description() const; bool inDefaultState() const; void reset(); private: bool _isDown = false; ///< @c true= currently depressed. de::duint _time = 0; }; #endif // CLIENT_INPUTSYSTEM_INPUTDEVICEBUTTONCONTROL_H doomsday-stable-1.15.7/doomsday/client/include/ui/styledlogsinkformatter.h0000664000175000017500000000302212641367670026316 0ustar jaakkojaakko/** @file styledlogsinkformatter.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_STYLEDLOGSINKFORMATTER_H #define DENG_CLIENT_STYLEDLOGSINKFORMATTER_H #include #include /** * Formats log entries for styled output. */ class StyledLogSinkFormatter : public de::LogSink::IFormatter { public: StyledLogSinkFormatter(); StyledLogSinkFormatter(de::LogEntry::Flags const &formatFlags); Lines logEntryToTextLines(de::LogEntry const &entry); /** * Omits the log entry section information if the entry is marked as * a non-developer entry. The default is @c true. * * @param omit Omit section. */ void setOmitSectionIfNonDev(bool omit); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_STYLEDLOGSINKFORMATTER_H doomsday-stable-1.15.7/doomsday/client/include/ui/infine/0000775000175000017500000000000012641367670022601 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/ui/infine/infinesystem.h0000664000175000017500000000514412641367670025473 0ustar jaakkojaakko/** @file infinesystem.h Interactive animation sequence system. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_UI_INFINESYSTEM_H #define DENG_UI_INFINESYSTEM_H #include #include #include #include "finale.h" /** * InFine script system. * * @ingroup infine */ class InFineSystem { public: /// The referenced Finale could not be found. @ingroup errors DENG2_ERROR(MissingFinaleError); typedef QList Finales; public: InFineSystem(); void runTicks(timespan_t timeDelta); /** * Terminate and clear all running Finales. */ void reset(); /** * Returns @c true if one or more Finales are currently in progress. For the purpose of * this test, suspended scripts are interpreted as being in progress. */ bool finaleInProgess() const; /** * Add a new Finale to the system. * * @param flags @ref finaleFlags * @param script InFine script to be interpreted. * @param setupCmds InFine script for setting up the script environment on load. */ Finale &newFinale(int flags, de::String script, de::String const &setupCmds = ""); /** * Returns @c true if @a id references a known Finale. */ bool hasFinale(finaleid_t id) const; /** * Lookup a Finale by it's unique @a id. */ Finale &finale(finaleid_t id); /** * Provides a list of all the Finales in the system, in order, for efficient traversal. */ Finales const &finales() const; public: #ifdef __CLIENT__ static void initBindingContext(); static void deinitBindingContext(); #endif /** * Register the console commands and cvars of this module. */ static void consoleRegister(); private: DENG2_PRIVATE(d) }; #endif // DENG_UI_INFINESYSTEM_H doomsday-stable-1.15.7/doomsday/client/include/ui/infine/finale.h0000664000175000017500000000465012641367670024215 0ustar jaakkojaakko/** @file finale.h InFine animation systems, Finale script. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_UI_INFINE_FINALE_H #define DENG_UI_INFINE_FINALE_H #include #include #include "../ddevent.h" #include "api_infine.h" // finaleid_t class FinaleInterpreter; #define FINF_BEGIN 0x01 #define FINF_END 0x02 #define FINF_SCRIPT 0x04 // Script included. #define FINF_SKIP 0x10 /** * A Finale instance contains the high-level state of an InFine script. * * @see FinaleInterpreter (interactive script interpreter) * * @ingroup infine */ class Finale { public: /// Notified when the finale is about to be deleted. DENG2_DEFINE_AUDIENCE2(Deletion, void finaleBeingDeleted(Finale const &finale)) public: /** * @param flags @ref finaleFlags * @param id Unique identifier for the script. * @param script The InFine script to be interpreted (a copy is made). */ Finale(int flags, finaleid_t id, de::String const &script); int flags() const; finaleid_t id() const; bool isActive() const; bool isSuspended() const; void resume(); void suspend(); bool terminate(); /** * @return @c true if the end of the script was reached. */ bool runTicks(timespan_t timeDelta); int handleEvent(ddevent_t const &ev); bool requestSkip(); bool isMenuTrigger() const; /** * Provides access to the script interpreter. Mainly for debug purposes. */ FinaleInterpreter const &interpreter() const; private: DENG2_PRIVATE(d) }; #endif // DENG_UI_INFINE_FINALE_H doomsday-stable-1.15.7/doomsday/client/include/ui/infine/finalepagewidget.h0000664000175000017500000001043612641367670026255 0ustar jaakkojaakko/** @file finalepagewidget.h InFine animation system, FinalePageWidget. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_UI_INFINE_FINALEPAGEWIDGET_H #define DENG_UI_INFINE_FINALEPAGEWIDGET_H #include #include #include "finalewidget.h" class Material; /** * Finale page widget (layer). * * @ingroup infine */ class FinalePageWidget { public: /// An invalid color index was specified. @ingroup errors DENG2_ERROR(InvalidColorError); /// An invalid font index was specified. @ingroup errors DENG2_ERROR(InvalidFontError); typedef QList Children; public: FinalePageWidget(); virtual ~FinalePageWidget(); #ifdef __CLIENT__ virtual void draw() const; #endif virtual void runTicks(timespan_t timeDelta); void makeVisible(bool yes = true); void pause(bool yes = true); /** * Returns @c true if @a widget is present on the page. */ bool hasWidget(FinaleWidget *widget); /** * Add a child widget to the page, transferring ownership. If the widget is * already contained by the page then nothing happens. * * @param widgetToAdd Widget to be added. * * @return Same as @a widgetToAdd, for convenience. */ FinaleWidget *addChild(FinaleWidget *widgetToAdd); /** * Remove a child widget from the page, transferring ownership to the caller * if owned by the page. * * @param widgetToRemove Widget to be removed. * * @return Same as @a widgetToRemove, for convenience. */ FinaleWidget *removeChild(FinaleWidget *widgetToRemove); /** * Provides a list of all child widgets of the page, in addition order. */ Children const &children() const; FinalePageWidget &setOffset(de::Vector3f const &newOffset, int steps = 0); FinalePageWidget &setOffsetX(float newOffsetX, int steps = 0); FinalePageWidget &setOffsetY(float newOffsetY, int steps = 0); FinalePageWidget &setOffsetZ(float newOffsetZ, int steps = 0); /// Current background Material. Material *backgroundMaterial() const; /// Sets the background Material. FinalePageWidget &setBackgroundMaterial(Material *newMaterial); /// Sets the background top color. FinalePageWidget &setBackgroundTopColor(de::Vector3f const &newColor, int steps = 0); /// Sets the background top color and alpha. FinalePageWidget &setBackgroundTopColorAndAlpha(de::Vector4f const &newColorAndAlpha, int steps = 0); /// Sets the background bottom color. FinalePageWidget &setBackgroundBottomColor(de::Vector3f const &newColor, int steps = 0); /// Sets the background bottom color and alpha. FinalePageWidget &setBackgroundBottomColorAndAlpha(de::Vector4f const &newColorAndAlpha, int steps = 0); /// Sets the filter color and alpha. FinalePageWidget &setFilterColorAndAlpha(de::Vector4f const &newColorAndAlpha, int steps = 0); /// @return Animator which represents the identified predefined color. animatorvector3_t const *predefinedColor(uint idx); /// Sets a predefined color. FinalePageWidget &setPredefinedColor(uint idx, de::Vector3f const &newColor, int steps = 0); /// @return Unique identifier of the predefined font. fontid_t predefinedFont(uint idx); /// Sets a predefined font. FinalePageWidget &setPredefinedFont(uint idx, fontid_t font); private: DENG2_PRIVATE(d) }; #endif // DENG_UI_INFINE_FINALEPAGEWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/infine/finalewidget.h0000664000175000017500000000624412641367670025422 0ustar jaakkojaakko/** @file finalewidget.h InFine animation system, FinaleWidget. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_UI_INFINE_FINALEWIDGET_H #define DENG_UI_INFINE_FINALEWIDGET_H #include #include #include #include #include class FinalePageWidget; /** * Base class for Finale widgets. * * @ingroup infine */ class FinaleWidget { public: /// Notified when the InFine object is about to be deleted. DENG2_DEFINE_AUDIENCE(Deletion, void finaleWidgetBeingDeleted(FinaleWidget const &widget)) public: explicit FinaleWidget(de::String const &name = ""); virtual ~FinaleWidget(); DENG2_AS_IS_METHODS() #ifdef __CLIENT__ virtual void draw(de::Vector3f const &offset) = 0; #endif virtual void runTicks(/*timespan_t timeDelta*/); /** * Returns the unique identifier of the widget. */ de::Id id() const; /** * Returns the symbolic name of the widget. */ de::String name() const; FinaleWidget &setName(de::String const &newName); animatorvector3_t const &origin() const; FinaleWidget &setOrigin(de::Vector3f const &newOrigin, int steps = 0); FinaleWidget &setOriginX(float newX, int steps = 0); FinaleWidget &setOriginY(float newY, int steps = 0); FinaleWidget &setOriginZ(float newZ, int steps = 0); animator_t const &angle() const; FinaleWidget &setAngle(float newAngle, int steps = 0); animatorvector3_t const &scale() const; FinaleWidget &setScale(de::Vector3f const &newScale, int steps = 0); FinaleWidget &setScaleX(float newScaleX, int steps = 0); FinaleWidget &setScaleY(float newScaleY, int steps = 0); FinaleWidget &setScaleZ(float newScaleZ, int steps = 0); /** * Returns the FinalePageWidget to which the widget is attributed (if any). */ FinalePageWidget *page() const; /** * Change/setup a reverse link between this object and it's owning page. * @note Changing this relationship here does not complete the task of * linking an object with a page (not enough information). It is therefore * the page's responsibility to call this when adding/removing objects. */ FinaleWidget &setPage(FinalePageWidget *newPage); private: DENG2_PRIVATE(d) }; #endif // DENG_UI_INFINE_FINALEWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/infine/finaleinterpreter.h0000664000175000017500000001037312641367670026500 0ustar jaakkojaakko/** @file finaleinterpreter.h InFine animation system Finale script interpreter. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_UI_INFINE_FINALEINTERPRETER_H #define DENG_UI_INFINE_FINALEINTERPRETER_H #include #include #include "../ddevent.h" #include "api_infine.h" // finaleid_t class FinaleWidget; class FinaleAnimWidget; class FinaleTextWidget; class FinalePageWidget; /// Used with findWidget and findOrCreateWidget: /// @ingroup infine enum fi_obtype_e { FI_ANIM, FI_TEXT }; /** * Interpreter for finale scripts. An instance of which is created for each running * script and owned by the Finale. * * @par UI pages / drawing order * InFine imposes a strict object drawing order, which requires two pages; one for * animation objects (also used for the background) and another for Text objects * (also used for the filter). * * 1: Background. * 2: Picture objects in the order in which they were created. * 3: Text objects, in the order in which they were created. * 4: Filter. * * @see Finale * @ingroup infine */ class FinaleInterpreter { public: /// An unknown widget was referenced. @ingroup errors DENG2_ERROR(MissingWidgetError); /// An unknown page was referenced. @ingroup errors DENG2_ERROR(MissingPageError); enum PageIndex { Anims = 0, ///< @note Also used for the background. Texts = 1 ///< @note Also used for the filter. }; public: FinaleInterpreter(finaleid_t id); finaleid_t id() const; bool runTicks(timespan_t timeDelta, bool processCommands); int handleEvent(ddevent_t const &ev); void loadScript(char const *script); bool isSuspended() const; void resume(); void suspend(); void terminate(); bool isMenuTrigger() const; bool commandExecuted() const; bool canSkip() const; void allowSkip(bool yes = true); bool skip(); bool skipToMarker(de::String const &marker); bool skipInProgress() const; bool lastSkipped() const; #ifdef __CLIENT__ void addEventHandler(ddevent_t const &evTemplate, de::String const &gotoMarker); void removeEventHandler(ddevent_t const &evTemplate); #endif FinalePageWidget &page(PageIndex index); FinalePageWidget const &page(PageIndex index) const; FinaleWidget *tryFindWidget(de::String const &name); FinaleWidget &findWidget(fi_obtype_e type, de::String const &name); /** * Find an object of the specified type with the type-unique name. * * @param type FinaleWidget type identifier. * @param name Unique name of the object we are looking for. * * @return a) Existing object associated with unique @a name. * b) New object with unique @a name. */ FinaleWidget &findOrCreateWidget(fi_obtype_e type, de::String const &name); public: /// Script-level flow/state control (@todo make private): -------------------- void beginDoSkipMode(); void gotoEnd(); void pause(); void wait(int ticksToWait = 1); void foundSkipHere(); void foundSkipMarker(de::String const &marker); int inTime() const; void setInTime(int seconds); void setHandleEvents(bool yes = true); void setShowMenu(bool yes = true); void setSkip(bool allowed = true); void setSkipNext(bool yes = true); void setWaitAnim(FinaleAnimWidget *newWaitAnim); void setWaitText(FinaleTextWidget *newWaitText); private: DENG2_PRIVATE(d) }; #endif // DENG_UI_INFINE_FINALEINTERPRETER_H doomsday-stable-1.15.7/doomsday/client/include/ui/infine/finaleanimwidget.h0000664000175000017500000000742412641367670026270 0ustar jaakkojaakko/** @file finaleanimwidget.h InFine animation system, FinaleAnimWidget. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_UI_INFINE_FINALEANIMWIDGET_H #define DENG_UI_INFINE_FINALEANIMWIDGET_H #include #include "finalewidget.h" #include "Material" /** * Finale animation widget. Colored rectangles or image sequence animations. * * @ingroup infine */ class FinaleAnimWidget : public FinaleWidget { public: /** * Describes a frame in the animation sequence. */ struct Frame { enum Type { PFT_MATERIAL, PFT_PATCH, PFT_RAW, /// "Raw" graphic or PCX lump. PFT_XIMAGE /// External graphics resource. }; int tics; Type type; struct Flags { char flip:1; } flags; union { Material *material; patchid_t patch; lumpnum_t lumpNum; DGLuint tex; } texRef; short sound; Frame(); ~Frame(); }; typedef QList Frames; public: FinaleAnimWidget(de::String const &name); virtual ~FinaleAnimWidget(); /// @todo Observe instead. bool animationComplete() const; FinaleAnimWidget &setLooping(bool yes = true); int newFrame(Frame::Type type, int tics, void *texRef, short sound, bool flagFlipH); Frames const &allFrames() const; FinaleAnimWidget &clearAllFrames(); inline int frameCount() const { return allFrames().count(); } FinaleAnimWidget &resetAllColors(); animator_t const *color() const; FinaleAnimWidget &setColorAndAlpha(de::Vector4f const &newColorAndAlpha, int steps = 0); FinaleAnimWidget &setColor(de::Vector3f const &newColor, int steps = 0); FinaleAnimWidget &setAlpha(float newAlpha, int steps = 0); animator_t const *edgeColor() const; FinaleAnimWidget &setEdgeColorAndAlpha(de::Vector4f const &newColorAndAlpha, int steps = 0); FinaleAnimWidget &setEdgeColor(de::Vector3f const &newColor, int steps = 0); FinaleAnimWidget &setEdgeAlpha(float newAlpha, int steps = 0); animator_t const *otherColor() const; FinaleAnimWidget &setOtherColorAndAlpha(de::Vector4f const &newColorAndAlpha, int steps = 0); FinaleAnimWidget &setOtherColor(de::Vector3f const &newColor, int steps = 0); FinaleAnimWidget &setOtherAlpha(float newAlpha, int steps = 0); animator_t const *otherEdgeColor() const; FinaleAnimWidget &setOtherEdgeColorAndAlpha(de::Vector4f const &newColorAndAlpha, int steps = 0); FinaleAnimWidget &setOtherEdgeColor(de::Vector3f const &newColor, int steps = 0); FinaleAnimWidget &setOtherEdgeAlpha(float newAlpha, int steps = 0); protected: #ifdef __CLIENT__ void draw(de::Vector3f const &offset); #endif void runTicks(/*timespan_t timeDelta*/); private: DENG2_PRIVATE(d) }; typedef FinaleAnimWidget::Frame FinaleAnimWidgetFrame; #endif // DENG_UI_INFINE_FINALEANIMWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/infine/finaletextwidget.h0000664000175000017500000000523512641367670026326 0ustar jaakkojaakko/** @file finaletextwidget.h InFine animation system, FinaleTextWidget. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_UI_INFINE_FINALETEXTWIDGET_H #define DENG_UI_INFINE_FINALETEXTWIDGET_H #include #include #include "ui/infine/finalewidget.h" /** * Finale text widget. * * @ingroup infine */ class FinaleTextWidget : public FinaleWidget { public: FinaleTextWidget(de::String const &name); virtual ~FinaleTextWidget(); void accelerate(); FinaleTextWidget &setCursorPos(int newPos); bool animationComplete() const; /** * Returns the total number of @em currently-visible characters, excluding control/escape * sequence characters. */ int visLength(); char const *text() const; FinaleTextWidget &setText(char const *newText); fontid_t font() const; FinaleTextWidget &setFont(fontid_t newFont); /// @return @ref alignmentFlags int alignment() const; /// @param newAlignment @ref alignmentFlags FinaleTextWidget &setAlignment(int newAlignment); float lineHeight() const; FinaleTextWidget &setLineHeight(float newLineHeight); int scrollRate() const; FinaleTextWidget &setScrollRate(int newRateInTics); int typeInRate() const; FinaleTextWidget &setTypeInRate(int newRateInTics); FinaleTextWidget &setPageColor(uint id); FinaleTextWidget &setPageFont(uint id); FinaleTextWidget &setColorAndAlpha(de::Vector4f const &newColorAndAlpha, int steps = 0); FinaleTextWidget &setColor(de::Vector3f const &newColor, int steps = 0); FinaleTextWidget &setAlpha(float alpha, int steps = 0); protected: #ifdef __CLIENT__ void draw(de::Vector3f const &offset); #endif void runTicks(); public: DENG2_PRIVATE(d) }; #endif // DENG_UI_INFINE_FINALETEXTWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/ddevent.h0000664000175000017500000001032612641367670023135 0ustar jaakkojaakko/** @file ddevent.h Input system event. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_INPUTSYSTEM_DDEVENT_H #define CLIENT_INPUTSYSTEM_DDEVENT_H #include #include #include "api_event.h" // Input device identifiers: enum { IDEV_KEYBOARD, IDEV_MOUSE, IDEV_JOY1, IDEV_JOY2, IDEV_JOY3, IDEV_JOY4, IDEV_HEAD_TRACKER, NUM_INPUT_DEVICES ///< Theoretical maximum. }; enum ddeventtype_t { E_TOGGLE, ///< Two-state device E_AXIS, ///< Axis position E_ANGLE, ///< Hat angle E_SYMBOLIC, ///< Symbolic event E_FOCUS ///< Window focus }; enum ddevent_togglestate_t { ETOG_DOWN, ETOG_UP, ETOG_REPEAT }; enum ddevent_axistype_t { EAXIS_ABSOLUTE, ///< Absolute position on the axis EAXIS_RELATIVE ///< Offset relative to the previous position }; /** * Internal input event. * * These are used internally, a cutdown version containing * only need-to-know metadata is sent down the games' responder chain. * * @todo Replace with a de::Event-derived class. */ struct ddevent_t { int device; ///< e.g. IDEV_KEYBOARD ddeventtype_t type; ///< E_TOGGLE, E_AXIS, E_ANGLE, or E_SYMBOLIC union { struct { int id; ///< Button/key index number ddevent_togglestate_t state; ///< State of the toggle char text[8]; ///< For characters, latin1-encoded text to insert (or empty). } toggle; struct { int id; ///< Axis index number float pos; ///< Position of the axis ddevent_axistype_t type; ///< Type of the axis (absolute or relative) } axis; struct { int id; ///< Angle index number float pos; ///< Angle, or negative if centered } angle; struct { int id; ///< Console that originated the event. char const *name; ///< Symbolic name of the event. } symbolic; struct { dd_bool gained; ///< Gained or lost focus. int inWindow; ///< Window where the focus change occurred (index). } focus; }; }; // Convenience macros. #define IS_TOGGLE_DOWN(evp) ((evp)->type == E_TOGGLE && (evp)->toggle.state == ETOG_DOWN) #define IS_TOGGLE_DOWN_ID(evp, togid) ((evp)->type == E_TOGGLE && (evp)->toggle.state == ETOG_DOWN && (evp)->toggle.id == togid) #define IS_TOGGLE_UP(evp) ((evp)->type == E_TOGGLE && (evp)->toggle.state == ETOG_UP) #define IS_TOGGLE_REPEAT(evp) ((evp)->type == E_TOGGLE && (evp)->toggle.state == ETOG_REPEAT) #define IS_KEY_TOGGLE(evp) ((evp)->device == IDEV_KEYBOARD && (evp)->type == E_TOGGLE) #define IS_KEY_DOWN(evp) ((evp)->device == IDEV_KEYBOARD && (evp)->type == E_TOGGLE && (evp)->toggle.state == ETOG_DOWN) #define IS_KEY_PRESS(evp) ((evp)->device == IDEV_KEYBOARD && (evp)->type == E_TOGGLE && (evp)->toggle.state != ETOG_UP) #define IS_MOUSE_DOWN(evp) ((evp)->device == IDEV_MOUSE && IS_TOGGLE_DOWN(evp)) #define IS_MOUSE_UP(evp) ((evp)->device == IDEV_MOUSE && IS_TOGGLE_UP(evp)) #define IS_MOUSE_MOTION(evp) ((evp)->device == IDEV_MOUSE && (evp)->type == E_AXIS) #endif // CLIENT_INPUTSYSTEM_DDEVENT_H doomsday-stable-1.15.7/doomsday/client/include/ui/b_main.h0000664000175000017500000000204412641367670022727 0ustar jaakkojaakko/** @file b_main.h Event and device state bindings system. * * @authors Copyright © 2009-2013 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_INPUTSYSTEM_BINDINGS_H #define CLIENT_INPUTSYSTEM_BINDINGS_H void B_Init(); #endif // CLIENT_INPUTSYSTEM_BINDINGS_H doomsday-stable-1.15.7/doomsday/client/include/ui/inputdeviceaxiscontrol.h0000664000175000017500000000612012641367670026306 0ustar jaakkojaakko/** @file inputdeviceaxiscontrol.h Axis control for a logical input device. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_INPUTSYSTEM_INPUTDEVICEAXISCONTROL_H #define CLIENT_INPUTSYSTEM_INPUTDEVICEAXISCONTROL_H #include #include #include "inputdevice.h" // Input device axis flags for use with cvars: #define IDA_DISABLED 0x1 ///< Axis is always zero. #define IDA_INVERT 0x2 ///< Real input data should be inverted. #define IDA_RAW 0x4 ///< Do not smooth the input values; always use latest received value. /** * Models an axis control on a "physical" input device (e.g., mouse along one axis). * * @ingroup ui */ class InputDeviceAxisControl : public InputDeviceControl { public: enum Type { Stick, ///< Joysticks, gamepads Pointer ///< Mouse }; public: /** * @param name Symbolic name of the axis. * @param type Logical axis type. */ InputDeviceAxisControl(de::String const &name, Type type); virtual ~InputDeviceAxisControl(); Type type() const; void setRawInput(bool yes = true); bool isActive() const; bool isInverted() const; /** * Returns the current position of the axis. */ de::ddouble position() const; void setPosition(de::ddouble newPosition); /** * Update the position of the axis control from a "real" position. * * @param newPosition New position to be applied (maybe filtered, normalized, etc...). */ void applyRealPosition(de::dfloat newPosition); de::dfloat translateRealPosition(de::dfloat rawPosition) const; /** * Returns the current dead zone (0..1) limit for the axis. */ de::dfloat deadZone() const; void setDeadZone(de::dfloat newDeadZone); /** * Returns the current position scaling factor (applied to "real" positions). */ de::dfloat scale() const; void setScale(de::dfloat newScale); /** * When the state of the control last changed, in milliseconds since app init. */ de::duint time() const; void update(timespan_t ticLength); de::String description() const; bool inDefaultState() const; void reset(); void consoleRegister(); private: DENG2_PRIVATE(d) }; #endif // CLIENT_INPUTSYSTEM_INPUTDEVICEAXISCONTROL_H doomsday-stable-1.15.7/doomsday/client/include/ui/impulsebinding.h0000664000175000017500000000560512641367670024521 0ustar jaakkojaakko/** @file impulsebinding.h Impulse binding record accessor. * * @authors Copyright © 2009-2013 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_INPUTSYSTEM_IMPULSEBINDING_H #define CLIENT_INPUTSYSTEM_IMPULSEBINDING_H #include #include "Binding" #include "ddevent.h" enum ibcontroltype_t { IBD_TOGGLE = E_TOGGLE, IBD_AXIS = E_AXIS, IBD_ANGLE = E_ANGLE, NUM_IBD_TYPES }; #define EVTYPE_TO_IBDTYPE(evt) ((evt) == E_AXIS? IBD_AXIS : (evt) == E_TOGGLE? IBD_TOGGLE : IBD_ANGLE) #define IBDTYPE_TO_EVTYPE(cbt) ((cbt) == IBD_AXIS? E_AXIS : (cbt) == IBD_TOGGLE? E_TOGGLE : E_ANGLE) // Flags for impulse bindings. #define IBDF_INVERSE 0x1 #define IBDF_TIME_STAGED 0x2 /** * Utility for handling input-device-control => impulse binding records. * * @ingroup ui */ class ImpulseBinding : public Binding { public: ImpulseBinding() : Binding() {} ImpulseBinding(ImpulseBinding const &other) : Binding(other) {} ImpulseBinding(de::Record &d) : Binding(d) {} ImpulseBinding(de::Record const &d) : Binding(d) {} ImpulseBinding &operator = (de::Record const *d) { *static_cast(this) = d; return *this; } void resetToDefaults(); de::String composeDescriptor(); /** * Parse a device-control => player impulse trigger descriptor and (re)configure the * binding. * * @param ctrlDesc Descriptor for control information and any additional conditions. * @param impulseId Identifier of the player impulse to execute when triggered, if any. * @param localPlayer Local player number to execute the impulse for when triggered. * @param assignNewId @c true= assign a new unique identifier. * * @throws ConfigureError on failure. At which point @a binding should be considered * to be in an undefined state. The caller may choose to clear and then reconfigure * it using another descriptor. */ void configure(char const *ctrlDesc, int impulseId, int localPlayer, bool assignNewId = true); }; #endif // CLIENT_INPUTSYSTEM_IMPULSEBINDING_H doomsday-stable-1.15.7/doomsday/client/include/ui/inputdevice.h0000664000175000017500000002405312641367670024025 0ustar jaakkojaakko/** @file inputdevice.h Logical input device. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_INPUTSYSTEM_INPUTDEVICE_H #define CLIENT_INPUTSYSTEM_INPUTDEVICE_H #include #include #include #include #include class BindContext; /// @todo remove: class InputDeviceAxisControl; class InputDeviceButtonControl; class InputDeviceHatControl; /// end todo /** * Base class for modelling a "physical" input device. * * @ingroup ui */ class InputDevice { public: /// Referenced control is missing. @ingroup errors DENG2_ERROR(MissingControlError); /// Notified when the active state of the device changes. DENG2_DEFINE_AUDIENCE2(ActiveChange, void inputDeviceActiveChanged(InputDevice &device)) /** * Base class for all controls. * @todo Attribute a GUID, to simplify bookkeeping. -ds */ class Control { public: /// No InputDevice is associated with the control. @ingroup errors DENG2_ERROR(MissingDeviceError); /** * How the control state relates to binding contexts. */ enum BindContextAssociationFlag { /// The state has expired. The control is considered to remain in /// default state until the flag gets cleared (which happens when /// the real control state returns to its default). Expired = 0x1, /// The state has been triggered. This is cleared when someone checks /// the control state. (Only for buttons). Triggered = 0x2, DefaultFlags = 0 }; Q_DECLARE_FLAGS(BindContextAssociation, BindContextAssociationFlag) public: explicit Control(InputDevice *device = nullptr); virtual ~Control(); DENG2_AS_IS_METHODS() /** * Returns @c true if the control is presently in its default state. * (e.g., button is not pressed, axis is at center, etc...). */ virtual bool inDefaultState() const = 0; /** * Reset the control back to its default state. Note that any attributed * property values (name, device and binding association) are unaffected. * * The default implementation does nothing. */ virtual void reset() {} /** * Returns the symbolic name of the control. */ de::String name() const; /** * Change the symbolic name of the control to @a newName. */ void setName(de::String const &newName); /** * Compose the full symbolic name of the control including the device name * (if one is attributed), for example: * * @code * - => "mouse-x" * @endcode */ de::String fullName() const; /** * Returns information about the control as styled text. */ virtual de::String description() const = 0; /** * Returns the InputDevice attributed to the control. * * @see hasDevice(), setDevice() */ InputDevice &device() const; /** * Returns @c true if an InputDevice is attributed to the control. * * @see device(), setDevice() */ bool hasDevice() const; /** * Change the attributed InputDevice to @a newDevice. * * @param newDevice InputDevice to attribute. Ownership is unaffected. * * @see hasDevice(), device() */ void setDevice(InputDevice *newDevice); /** * Returns the BindContext attributed to the control; otherwise @c nullptr. * * @see hasBindContext(), setBindContext() */ BindContext *bindContext() const; /** * Returns @c true of a BindContext is attributed to the control. * * @see bindContext(), setBindContext() */ inline bool hasBindContext() const { return bindContext() != nullptr; } /** * Change the attributed BindContext to @a newContext. * * @param newContext BindContext to attribute. Ownership is unaffected. * * @see hasBindContext(), bindContext() */ void setBindContext(BindContext *newContext); /** * Returns the BindContextAssociation flags for the control. */ BindContextAssociation bindContextAssociation() const; /** * Change the BindContextAssociation flags for the control. * * @param flagsToChange Association flags to change. * @param op Logical operation to perform. */ void setBindContextAssociation(BindContextAssociation const &flagsToChange, de::FlagOp op = de::SetFlags); void clearBindContextAssociation(); void expireBindContextAssociationIfChanged(); /** * Register the console commands and variables of the control. */ virtual void consoleRegister() {} private: DENG2_PRIVATE(d) }; public: /** * @note InputDevices are not @em active by default. Call @ref activate() once * device configuration has been completed. * * @param name Symbolic name of the device. */ InputDevice(de::String const &name); virtual ~InputDevice(); /** * Returns @c true if the device is presently active. * @todo Document "active" status. */ bool isActive() const; /** * Change the active status of this device. * * @see isActive() */ void activate(bool yes = true); inline void deactivate() { activate(false); } /** * Returns the symbolic name of the device. */ de::String name() const; /** * Returns the title of the device, intended for human-readable descriptions. * * @see setTitle() */ de::String title() const; /** * Change the title of the device, intended for human-readable descriptions, * to @a newTitle. */ void setTitle(de::String const &newTitle); /** * Returns information about the device as styled text. */ de::String description() const; /** * Reset the state of all controls to their "initial" positions (i.e., buttons * in the up positions, axes at center, etc...). */ void reset(); /** * Iterate through all the controls of the device. */ de::LoopResult forAllControls(std::function func); /** * Translate a symbolic axis @a name to the associated unique axis id. * * @return Index of the named axis control if found, otherwise @c -1. */ de::dint toAxisId(de::String const &name) const; /** * Returns @c true if @a id is a known axis control. */ bool hasAxis(de::dint id) const; /** * Lookup an axis control by unique @a id. * * @param id Unique id of the axis control. * * @return Axis control associated with the given @a id. */ InputDeviceAxisControl &axis(de::dint id) const; /** * Add an @a axis control to the input device. * * @param axis Axis control to add. Ownership is given to the device. */ void addAxis(InputDeviceAxisControl *axis); /** * Returns the number of axis controls of the device. */ de::dint axisCount() const; /** * Translate a symbolic key @a name to the associated unique key id. * * @return Index of the named key control if found, otherwise @c -1. */ de::dint toButtonId(de::String const &name) const; /** * Returns @c true if @a id is a known button control. */ bool hasButton(de::dint id) const; /** * Lookup a button control by unique @a id. * * @param id Unique id of the button control. * * @return Button control associated with the given @a id. */ InputDeviceButtonControl &button(de::dint id) const; /** * Add a @a button control to the input device. * * @param button Button control to add. Ownership is given to the device. */ void addButton(InputDeviceButtonControl *button); /** * Returns the number of button controls of the device. */ de::dint buttonCount() const; /** * Returns @c true if @a id is a known hat control. */ bool hasHat(de::dint id) const; /** * Lookup a hat control by unique @a id. * * @param id Unique id of the hat control. * * @return Hat control associated with the given @a id. */ InputDeviceHatControl &hat(de::dint id) const; /** * Add a @a hat control to the input device. * * @param hat Hat control to add. Ownership is given to the device. */ void addHat(InputDeviceHatControl *hat); /** * Returns the number of hat controls of the device. */ de::dint hatCount() const; /** * Register the console commands and variables for this device and all controls. */ void consoleRegister(); private: DENG2_PRIVATE(d) }; Q_DECLARE_OPERATORS_FOR_FLAGS(InputDevice::Control::BindContextAssociation) typedef InputDevice::Control InputDeviceControl; #endif // CLIENT_INPUTSYSTEM_INPUTDEVICE_H doomsday-stable-1.15.7/doomsday/client/include/ui/dialogs/0000775000175000017500000000000012641367670022753 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/ui/dialogs/inputsettingsdialog.h0000664000175000017500000000260012641367670027222 0ustar jaakkojaakko/** @file inputsettingsdialog.h Dialog for input settings. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_INPUTSETTINGSDIALOG_H #define DENG_CLIENT_INPUTSETTINGSDIALOG_H #include /** * Dialog for modifying input settings. */ class InputSettingsDialog : public de::DialogWidget { Q_OBJECT public: InputSettingsDialog(de::String const &name = "inputsettings"); public slots: void resetToDefaults(); protected slots: void mouseTogglesChanged(); void mouseSensitivityChanged(double value); void showDeveloperPopup(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_INPUTSETTINGSDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/ui/dialogs/coloradjustmentdialog.h0000664000175000017500000000246512641367670027530 0ustar jaakkojaakko/** @file coloradjustmentdialog.h Dialog for gamma, etc. adjustments. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_COLORADJUSTMENTDIALOG_H #define DENG_CLIENT_COLORADJUSTMENTDIALOG_H #include /** * Dialog for gamma, etc. adjustments. */ class ColorAdjustmentDialog : public de::DialogWidget { Q_OBJECT public: ColorAdjustmentDialog(de::String const &name = "coloradjustment"); protected: void prepare(); public slots: void resetToDefaults(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_COLORADJUSTMENTDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/ui/dialogs/videosettingsdialog.h0000664000175000017500000000265312641367670027201 0ustar jaakkojaakko/** @file videosettingsdialog.h Dialog for video settings. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_VIDEOSETTINGSDIALOG_H #define DENG_CLIENT_VIDEOSETTINGSDIALOG_H #include /** * Dialog for modifying video settings. */ class VideoSettingsDialog : public de::DialogWidget { Q_OBJECT public: VideoSettingsDialog(de::String const &name = "videosettings"); protected slots: void resetToDefaults(); void changeMode(uint selected); void changeColorDepth(uint selected); void showColorAdjustments(); void showWindowMenu(); void applyModeToWindow(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_VIDEOSETTINGSDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/ui/dialogs/aboutdialog.h0000664000175000017500000000222312641367670025415 0ustar jaakkojaakko/** @file aboutdialog.h Information about the Doomsday Client. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_ABOUTDIALOG_H #define DENG_CLIENT_ABOUTDIALOG_H #include /** * Dialog that shows information about the client. */ class AboutDialog : public de::DialogWidget { Q_OBJECT public: AboutDialog(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_ABOUTDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/ui/dialogs/renderersettingsdialog.h0000664000175000017500000000253512641367670027700 0ustar jaakkojaakko/** @file renderersettingsdialog.h Settings for the renderer. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_RENDERERSETTINGSDIALOG_H #define DENG_CLIENT_RENDERERSETTINGSDIALOG_H #include /** * Dialog for modifying input settings. */ class RendererSettingsDialog : public de::DialogWidget { Q_OBJECT public: RendererSettingsDialog(de::String const &name = "renderersettings"); public slots: void resetToDefaults(); protected slots: void showDeveloperPopup(); void editProfile(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_RENDERERSETTINGSDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/ui/dialogs/vrsettingsdialog.h0000664000175000017500000000245712641367670026524 0ustar jaakkojaakko/** @file vrsettingsdialog.h Settings for virtual reality. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_VRSETTINGSDIALOG_H #define DENG_CLIENT_VRSETTINGSDIALOG_H #include /** * Dialog for modifying VR settings. */ class VRSettingsDialog : public de::DialogWidget { Q_OBJECT public: VRSettingsDialog(de::String const &name = "vrsettings"); public slots: void resetToDefaults(); void autoConfigForOculusRift(); void autoConfigForDesktop(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_VRSETTINGSDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/ui/dialogs/audiosettingsdialog.h0000664000175000017500000000244112641367670027167 0ustar jaakkojaakko/** @file audiosettingsdialog.h Dialog for audio settings. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_AUDIOSETTINGSDIALOG_H #define DENG_CLIENT_AUDIOSETTINGSDIALOG_H #include /** * Dialog for modifying video settings. */ class AudioSettingsDialog : public de::DialogWidget { Q_OBJECT public: AudioSettingsDialog(de::String const &name = "audiosettings"); public slots: void resetToDefaults(); void showDeveloperPopup(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_AUDIOSETTINGSDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/ui/dialogs/networksettingsdialog.h0000664000175000017500000000245712641367670027566 0ustar jaakkojaakko/** @file networksettingsdialog.h Dialog for network settings. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_NETWORKSETTINGSDIALOG_H #define DENG_CLIENT_NETWORKSETTINGSDIALOG_H #include /** * Dialog for modifying network settings. */ class NetworkSettingsDialog : public de::DialogWidget { Q_OBJECT public: NetworkSettingsDialog(de::String const &name = "networksettings"); public slots: void resetToDefaults(); void showDeveloperPopup(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_NETWORKSETTINGSDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/ui/dialogs/manualconnectiondialog.h0000664000175000017500000000422012641367670027637 0ustar jaakkojaakko/** @file manualconnectiondialog.h Dialog for connecting to a server. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_MANUALCONNECTIONDIALOG_H #define DENG_CLIENT_MANUALCONNECTIONDIALOG_H #include #include /** * Dialog for connecting to a multiplayer server manually using an IP address or domain * name. The TCP port number can also be optionally provided. * * The dialog stores the previously used address persistently. */ class ManualConnectionDialog : public de::InputDialog, public de::IPersistent { Q_OBJECT public: ManualConnectionDialog(de::String const &name = "manualconnection"); /** * Enables or disables joining the selected game when the user clicks on a * session. By default, this is enabled. * * @param joinWhenSelected @c true to allow auto-joining. */ void enableJoinWhenSelected(bool joinWhenSelected); de::Action *makeAction(de::ui::Item const &item); // Implements IPersistent. void operator >> (de::PersistentState &toState) const; void operator << (de::PersistentState const &fromState); signals: void selected(de::ui::Item const *); public slots: void queryOrConnect(); void contentChanged(); void validate(); void serverSelected(de::ui::Item const *); protected: void finish(int result); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_MANUALCONNECTIONDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/ui/dialogs/alertdialog.h0000664000175000017500000000425212641367670025416 0ustar jaakkojaakko/** @file alertdialog.h Dialog for listing recent alerts. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_ALERTDIALOG_H #define DENG_CLIENT_ALERTDIALOG_H #include /** * Dialog for listing recent alerts. * * Only one instance of each message is kept in the list. * * Use the static utility method ClientApp::alert() to conveniently add new alerts when * needed. * * @par Thread-safety * * Even though widgets in general should only be manipulated from the main thread, * adding new alerts is thread-safe. */ class AlertDialog : public de::DialogWidget { Q_OBJECT public: enum Level { Minor = -1, Normal = 0, Major = 1 }; public: AlertDialog(de::String const &name = "alerts"); /** * Adds a new alert. If the same alert is already in the list, the new one is ignored. * * Can be called from any thread. Alerts are put in a queue until the next time the * dialog's update() method is called. * * @param message Alert message. * @param level Severity level. */ void newAlert(de::String const &message, Level level = Normal); void update(); public slots: void showListOfAlerts(); void showLogFilterSettings(); void hideNotification(); void autohideTimeChanged(); protected: void finish(int result); void panelDismissed(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_ALERTDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/ui/dialogs/gamesdialog.h0000664000175000017500000000301512641367670025377 0ustar jaakkojaakko/** @file gamesdialog.h Dialog for viewing and loading available games. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_GAMESDIALOG_H #define DENG_CLIENT_GAMESDIALOG_H #include /** * Dialog for viewing and loading available games. */ class GamesDialog : public de::DialogWidget { Q_OBJECT public: enum Mode { ShowAll, ShowSingleplayerOnly, ShowMultiplayerOnly }; GamesDialog(Mode mode = ShowAll, de::String const &name = "games"); public slots: void showSettings(); void connectManually(); void selectSession(de::ui::Item const *); void sessionSelectedManually(de::ui::Item const *); protected: void preparePanelForOpening(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_GAMESDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/ui/dialogs/logsettingsdialog.h0000664000175000017500000000243212641367670026647 0ustar jaakkojaakko/** @file logsettingsdialog.h Dialog for Log settings. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_LOGSETTINGSDIALOG_H #define DENG_CLIENT_LOGSETTINGSDIALOG_H #include /** * Dialog for modifying log filter and alert settings. */ class LogSettingsDialog : public de::DialogWidget { Q_OBJECT public: LogSettingsDialog(de::String const &name = "logsettings"); public slots: void resetToDefaults(); void updateLogFilter(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_LOGSETTINGSDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/ui/clientwindowsystem.h0000664000175000017500000000372112641367670025460 0ustar jaakkojaakko/** @file clientwindowsystem.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENTWINDOWSYSTEM_H #define DENG_CLIENTWINDOWSYSTEM_H #include #include "SettingsRegister" #undef main class ClientWindow; /** * Client-side window system for managing ClientWindow instances. */ class ClientWindowSystem : public de::WindowSystem { public: ClientWindowSystem(); SettingsRegister &settings(); /** * Constructs a new window using the default configuration. Note that the * default configuration is saved persistently when the engine shuts down * and is restored when the engine is restarted. * * Command line options (e.g., -xpos) can be used to modify the window * configuration. * * @param id Identifier of the window ("main" for the main window). * * @return ClientWindow instance. Ownership is retained by the window system. */ ClientWindow *createWindow(de::String const &id = "main"); static ClientWindow &main(); static ClientWindow *mainPtr(); protected: void closingAllWindows(); bool rootProcessEvent(de::Event const &event); void rootUpdate(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENTWINDOWSYSTEM_H doomsday-stable-1.15.7/doomsday/client/include/ui/mouse_qt.h0000664000175000017500000000166112641367670023342 0ustar jaakkojaakko#ifndef MOUSE_QT_H #define MOUSE_QT_H #include "sys_input.h" #ifdef __cplusplus extern "C" { #endif extern mouseinterface_t qtMouse; /** * Submits a new mouse event for preprocessing. The event has likely just been * received from the windowing system. * * @param button Which button. * @param isDown Is the button pressed or released. */ void Mouse_Qt_SubmitButton(int button, dd_bool isDown); /** * Submits a new motion event for preprocessing. * * @param axis Which axis. * @param deltaX Horizontal delta. * @param deltaY Vertical delta. */ void Mouse_Qt_SubmitMotion(int axis, int deltaX, int deltaY); /** * Submits an absolute mouse position for the UI mouse mode. * * @param x X coordinate. 0 is at the left edge of the window. * @param y Y coordinate. 0 is at the top edge of the window. */ void Mouse_Qt_SubmitWindowPosition(int x, int y); #ifdef __cplusplus } // extern "C" #endif #endif // MOUSE_QT_H doomsday-stable-1.15.7/doomsday/client/include/ui/binding.h0000664000175000017500000000721312641367670023117 0ustar jaakkojaakko/** @file binding.h Base class for binding record accessors. * * @authors Copyright © 2009-2014 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_INPUTSYSTEM_BINDING_H #define CLIENT_INPUTSYSTEM_BINDING_H #include #include /** * Base class for binding record accessors. * * @ingroup ui */ class Binding : public de::RecordAccessor { public: /// Base class for binding configuration errors. @ingroup errors DENG2_ERROR(ConfigureError); enum ConditionType { Invalid, GlobalState, ///< Related to the high-level application/game state. AxisState, ///< An axis control is in a specific position. ButtonState, ///< A button control is in a specific state. HatState, ///< A hat control is pointing in a specific direction. ModifierState, ///< A control modifier is in a specific state. }; enum ControlTest { None, AxisPositionWithin, AxisPositionBeyond, AxisPositionBeyondPositive, AxisPositionBeyondNegative, ButtonStateAny, ButtonStateDown, ButtonStateRepeat, ButtonStateDownOrRepeat, ButtonStateUp }; public: Binding() : RecordAccessor(0) {} Binding(Binding const &other) : RecordAccessor(other) {} Binding(de::Record &d) : RecordAccessor(d) {} Binding(de::Record const &d) : RecordAccessor(d) {} virtual ~Binding() {} Binding &operator = (de::Record const *d) { setAccessedRecord(d); return *this; } de::Record &def(); de::Record const &def() const; /** * Determines if this binding accessor points to a record. */ operator bool() const; /** * Inserts the default members into the binding. All bindings are required to * implement this, as it is automatically called when configuring a binding. * * All bindings share some common members, so derived classes are required to * call this before inserting their own members. */ virtual void resetToDefaults(); /** * Generates a textual descriptor for the binding, including any state conditions. */ virtual de::String composeDescriptor() = 0; de::Record &addCondition(); int conditionCount() const; bool hasCondition(int index) const; de::Record &condition(int index); de::Record const &condition(int index) const; /** * Compare the binding conditions with @a other and return @c true if equivalent. */ bool equalConditions(Binding const &other) const; public: /** * Returns a new unique identifier. Never returns zero (not a valid Id). */ static int newIdentifier(); /** * Reset the unique identifier allocator, so that the next Id is @c 1. */ static void resetIdentifiers(); }; #endif // CLIENT_INPUTSYSTEM_BINDING_H doomsday-stable-1.15.7/doomsday/client/include/ui/clientwindow.h0000664000175000017500000001367012641367670024217 0ustar jaakkojaakko/** @file clientwindow.h Top-level window with UI widgets. * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_CLIENTWINDOW_H #define CLIENT_CLIENTWINDOW_H #include #include #include #include "ui/clientrootwidget.h" #include "resource/image.h" #include "ui/widgets/gamewidget.h" #undef main /** * Macro for conveniently accessing the current active window. There is always * one active window, so no need to worry about NULLs. The easiest way to get * information about the window where drawing is done. */ //#define DENG_WINDOW (&ClientWindow::main()) #define DENG_GAMEVIEW_X ClientWindow::main().game().rule().left().valuei() #define DENG_GAMEVIEW_Y ClientWindow::main().game().rule().top().valuei() #define DENG_GAMEVIEW_WIDTH ClientWindow::main().game().rule().width().valuei() #define DENG_GAMEVIEW_HEIGHT ClientWindow::main().game().rule().height().valuei() /** * A helpful macro that changes the origin of the window space coordinate system. */ #define FLIP(y) (ClientWindow::main().height() - ((y)+1)) class ConsoleWidget; class TaskBarWidget; class BusyWidget; class AlertDialog; /** * Top-level window that contains UI widgets. @ingroup gui */ class ClientWindow : public de::BaseWindow, DENG2_OBSERVES(de::Canvas, GLInit), DENG2_OBSERVES(de::Canvas, GLResize) { Q_OBJECT public: enum Mode { Normal, Busy }; enum SidebarLocation { RightEdge }; public: ClientWindow(de::String const &id = "main"); ClientRootWidget &root(); TaskBarWidget &taskBar(); de::GuiWidget &taskBarBlur(); ConsoleWidget &console(); de::NotificationAreaWidget ¬ifications(); GameWidget &game(); BusyWidget &busy(); AlertDialog &alerts(); /** * Adds a widget to the widget tree so that it will be displayed over * other widgets. * * @param widget Widget to add on top of others. Ownership of the * widget taken by the new parent. */ void addOnTop(de::GuiWidget *widget); /** * Installs a sidebar widget into the window. If there is an existing * sidebar, it will be deleted. Sidebar widgets are expected to control * their own width (on the right/left edges) or height (on the top/bottom * edges). * * @param location Location to attach the sidebar. Window takes ownership * of the widget. * @param sidebar Widget to install, or @c NULL to remove the sidebar. */ void setSidebar(SidebarLocation location, de::GuiWidget *sidebar); void unsetSidebar(SidebarLocation location) { setSidebar(location, 0); } bool hasSidebar(SidebarLocation location = RightEdge) const; /** * Sets the operating mode of the window. In Busy mode, the normal * widgets of the window will be replaced with a single BusyWidget. * * @param mode Mode. */ void setMode(Mode const &mode); /** * Must be called before any canvas windows are created. Defines the * default OpenGL format settings for the contained canvases. * * @return @c true, if the new format was applied. @c false, if the new * format remains the same because none of the settings have changed. */ static bool setDefaultGLFormat(); /** * Determines whether the contents of a window should be drawn during the * execution of the main loop callback, or should we wait for an update event * from the windowing system. */ bool shouldRepaintManually() const; /** * Grab the contents of the window into the supplied @a image. Ownership of * the image passes to the window for the duration of this call. * * @param image Image to fill with the grabbed frame contents. * @param halfSized If @c true, scales the image to half the full size. */ void grab(image_t &image, bool halfSized = false) const; /** * Draws the untransformed game-related contents of the window. The drawing * is done immediately; this must be called from the main/UI thread. * * The current render target is cleared before drawing. */ void drawGameContent(); void fadeInTaskBarBlur(de::TimeDelta span); void fadeOutTaskBarBlur(de::TimeDelta span); void updateCanvasFormat(); void updateRootSize(); // Notifications. bool isFPSCounterVisible() const; // Events. void closeEvent(QCloseEvent *); void canvasGLReady(de::Canvas &); void canvasGLInit(de::Canvas &); void canvasGLResized(de::Canvas &); // Implements BaseWindow. de::Vector2f windowContentSize() const; void drawWindowContent(); void preDraw(); void postDraw(); bool handleFallbackEvent(de::Event const &event); static ClientWindow &main(); static bool mainExists(); protected: bool prepareForDraw(); public slots: void toggleFPSCounter(); void showColorAdjustments(); void hideTaskBarBlur(); private: DENG2_PRIVATE(d) }; #endif // CANVASWINDOW_H doomsday-stable-1.15.7/doomsday/client/include/ui/b_util.h0000664000175000017500000000542712641367670022770 0ustar jaakkojaakko/** @file b_util.h Input system, binding utilities. * * @authors Copyright © 2009-2013 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_INPUTSYSTEM_BINDING_UTILITIES_H #define CLIENT_INPUTSYSTEM_BINDING_UTILITIES_H #include #include "dd_types.h" #include "ddevent.h" #include "Binding" class BindContext; bool B_ParseAxisPosition(Binding::ControlTest &test, float &pos, char const *desc); bool B_ParseButtonState(Binding::ControlTest &test, char const *desc); bool B_ParseHatAngle(float &angle, char const *desc); bool B_ParseBindingCondition(de::Record &cond, char const *desc); // --- de::String B_AxisPositionToString(Binding::ControlTest test, float pos); de::String B_ButtonStateToString(Binding::ControlTest test); de::String B_HatAngleToString(float angle); de::String B_ConditionToString(de::Record const &cond); de::String B_EventToString(ddevent_t const &ev); // --- bool B_CheckAxisPosition(Binding::ControlTest test, float testPos, float pos); /** * @param cond State condition to check. * @param localNum Local player number. * @param context Relevant binding context, if any (may be @c nullptr). */ bool B_CheckCondition(de::Record const *cond, int localNum, BindContext const *context); bool B_EqualConditions(de::Record const &a, de::Record const &b); // --------------------------------------------------------------------------------- extern byte zeroControlUponConflict; bool B_ParseKeyId(int &id, char const *desc); bool B_ParseMouseTypeAndId(ddeventtype_t &type, int &id, char const *desc); bool B_ParseJoystickTypeAndId(ddeventtype_t &type, int &id, int deviceId, char const *desc); de::String B_ControlDescToString(int deviceId, ddeventtype_t type, int id); void B_EvaluateImpulseBindings(BindContext const *context, int localNum, int impulseId, float *pos, float *relativeOffset, bool allowTriggered); char const *B_ShortNameForKey(int ddKey, bool forceLowercase = true); int B_KeyForShortName(char const *key); #endif // CLIENT_INPUTSYSTEM_BINDING_UTILITIES_H doomsday-stable-1.15.7/doomsday/client/include/ui/nativeui.h0000664000175000017500000000557112641367670023336 0ustar jaakkojaakko/** * @file nativeui.h * Native GUI functionality. @ingroup base * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_NATIVEUI_H #define LIBDENG_NATIVEUI_H #ifdef __cplusplus extern "C" { #endif // Message box types. typedef enum { MBT_GENERIC, ///< No icon. MBT_INFORMATION, MBT_QUESTION, MBT_WARNING, MBT_ERROR } messageboxtype_t; /** * Shows a native modal message dialog. * * @param type Type of the dialog (which icon to show). * @param title Title for the dialog. * @param msg Message. * @param detailedMsg Optional, possibly long extra content to show. Can be @c NULL. */ void Sys_MessageBox(messageboxtype_t type, const char* title, const char* msg, const char* detailedMsg); void Sys_MessageBox2(messageboxtype_t type, const char* title, const char* msg, const char* informativeMsg, const char* detailedMsg); int Sys_MessageBox3(messageboxtype_t type, const char* title, const char* msg, const char* informativeMsg, const char* detailedMsg, const char** buttons); void Sys_MessageBoxf(messageboxtype_t type, const char* title, const char* format, ...); int Sys_MessageBoxWithButtons(messageboxtype_t type, const char* title, const char* msg, const char* informativeMsg, const char** buttons); /** * Shows a native modal message dialog. The "more detail" content is read from a file. * * @param type Type of the dialog (which icon to show). * @param title Title for the dialog. * @param msg Message. * @param informativeMsg Additional information to show under the message * (perhaps using a smaller font). * @param detailsFileName Name of the file where to read details. */ void Sys_MessageBoxWithDetailsFromFile(messageboxtype_t type, const char* title, const char* msg, const char* informativeMsg, const char* detailsFileName); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_NATIVEUI_H doomsday-stable-1.15.7/doomsday/client/include/ui/progress.h0000664000175000017500000000334412641367670023352 0ustar jaakkojaakko/** @file progress.h Simple wrapper for the Busy progress bar. * @ingroup ui * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_CONSOLE_PROGRESSBAR_H #define LIBDENG_CONSOLE_PROGRESSBAR_H #include "dd_types.h" #ifdef __cplusplus extern "C" { #endif void Con_InitProgress(int maxProgress); /** * Initialize progress that only covers a part of the progress bar. * * @param maxProgress Maximum logical progress, for Con_SetProgress(). * @param start Start of normalized progress (default: 0). * @param end End of normalized progress (default: 1). */ void Con_InitProgress2(int maxProgress, float start, float end); /** * Updates the progress indicator. */ void Con_SetProgress(int progress); dd_bool Con_IsProgressAnimationCompleted(void); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_CONSOLE_PROGRESSBAR_H doomsday-stable-1.15.7/doomsday/client/include/ui/commandbinding.h0000664000175000017500000000601012641367670024450 0ustar jaakkojaakko/** @file commandbinding.h Command binding record accessor. * * @authors Copyright © 2009-2014 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_INPUTSYSTEM_COMMANDBINDING_H #define CLIENT_INPUTSYSTEM_COMMANDBINDING_H #include #include #include "Binding" #include "ddevent.h" class BindContext; /** * Utility for handling event => command binding records. * * @ingroup ui */ class CommandBinding : public Binding { public: CommandBinding() : Binding() {} CommandBinding(CommandBinding const &other) : Binding(other) {} CommandBinding(de::Record &d) : Binding(d) {} CommandBinding(de::Record const &d) : Binding(d) {} CommandBinding &operator = (de::Record const *d) { *static_cast(this) = d; return *this; } void resetToDefaults(); de::String composeDescriptor(); /** * Parse an event => command trigger descriptor and (re)configure the binding. * * eventparams{+cond}* * * @param eventDesc Descriptor for event information and any additional conditions. * @param command Console command to execute when triggered, if any. * @param assignNewId @c true= assign a new unique identifier. * * @throws ConfigureError on failure. At which point @a binding should be considered * to be in an undefined state. The caller may choose to clear and then reconfigure * it using another descriptor. */ void configure(char const *eventDesc, char const *command = nullptr, bool assignNewId = true); /** * Evaluate the given @a event according to the binding configuration, and if all * binding conditions pass - attempt to generate an Action. * * @param event Event to match against. * @param context Context in which the binding exists. * @param respectHigherContexts Bindings are shadowed by higher active contexts. * * @return Action instance (caller gets ownership), or @c nullptr if no matching. */ de::Action *makeAction(ddevent_t const &event, BindContext const &context, bool respectHigherContexts) const; }; #endif // CLIENT_INPUTSYSTEM_COMMANDBINDING_H doomsday-stable-1.15.7/doomsday/client/include/ui/clientrootwidget.h0000664000175000017500000000256212641367670025075 0ustar jaakkojaakko/** @file clientrootwidget.h GUI root widget tailored for the Doomsday Client. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENTROOTWIDGET_H #define DENG_CLIENTROOTWIDGET_H #include #include class ClientWindow; /** * GUI root widget tailored for the Doomsday Client. */ class ClientRootWidget : public de::GuiRootWidget { public: ClientRootWidget(de::CanvasWindow *window = 0); ClientWindow &window(); void addOnTop(de::GuiWidget *widget); void dispatchLatestMousePosition(); void handleEventAsFallback(de::Event const &event); }; #endif // DENG_CLIENTROOTWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/0000775000175000017500000000000012641367670022777 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/ui/widgets/gamewidget.h0000664000175000017500000000313212641367670025264 0ustar jaakkojaakko/** @file gamewidget.h Widget for legacy UI components. * @ingroup gui * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_GAMEWIDGET_H #define CLIENT_GAMEWIDGET_H #include /** * Widget for drawing the game world. * * @ingroup gui */ class GameWidget : public de::GuiWidget { public: GameWidget(de::String const &name = "game"); /** * Convenience method for changing and immediately applying a new GL * viewport. The viewport is automatically normalized in relation to the * root view size. * * This is only intended to support old graphics code that doesn't use libgui. */ void glApplyViewport(de::Rectanglei const &rect); void viewResized(); void update(); void drawContent(); bool handleEvent(de::Event const &event); private: DENG2_PRIVATE(d) }; #endif // CLIENT_GAMEWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/cvarlineeditwidget.h0000664000175000017500000000264712641367670027036 0ustar jaakkojaakko/** @file cvarlineeditwidget.h Console variable edit widget. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_CVARLINEEDITWIDGET_H #define DENG_CLIENT_CVARLINEEDITWIDGET_H #include #include "icvarwidget.h" /** * Console variable text editor. */ class CVarLineEditWidget : public de::LineEditWidget, public ICVarWidget { Q_OBJECT public: CVarLineEditWidget(char const *cvarPath); char const *cvarPath() const; public slots: void updateFromCVar(); void endEditing(); protected slots: void setCVarValueFromWidget(); protected: void contentChanged(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_CVARLINEEDITWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/cvarnativepathwidget.h0000664000175000017500000000377512641367670027407 0ustar jaakkojaakko/** @file cvarnativepathwidget.h Console variable native path widget. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_CVARNATIVEPATHWIDGET_H #define DENG_CLIENT_CVARNATIVEPATHWIDGET_H #include #include "icvarwidget.h" /** * Console variable that contains a native path. */ class CVarNativePathWidget : public de::AuxButtonWidget, public ICVarWidget { Q_OBJECT public: CVarNativePathWidget(char const *cvarPath); /** * Sets all the file types that can be selected using the widget. Each entry in * the list should be formatted as "Description (*.ext *.ext2)". * * The default is "All files (*)". * * @param filters Allowed file types. */ void setFilters(de::StringList const &filters); /** * Sets the text that is shown as the current selection when nothing has been * selected. * * @param text Blank text placeholder. */ void setBlankText(de::String const &text); char const *cvarPath() const; public slots: void updateFromCVar(); void chooseUsingNativeFileDialog(); void clearPath(); void showActionsPopup(); protected slots: void setCVarValueFromWidget(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_CVARNATIVEPATHWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/busywidget.h0000664000175000017500000000273112641367670025341 0ustar jaakkojaakko/** @file busywidget.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_BUSYWIDGET_H #define CLIENT_BUSYWIDGET_H #include #include /** * Widget that takes care of the UI while busy mode is active. */ class BusyWidget : public de::GuiWidget { public: BusyWidget(de::String const &name = ""); de::ProgressWidget &progress(); void renderTransitionFrame(); void releaseTransitionFrame(); de::GLTexture const *transitionFrame() const; // Events. void viewResized(); void update(); void drawContent(); bool handleEvent(de::Event const &event); protected: void glInit(); void glDeinit(); private: DENG2_PRIVATE(d) }; #endif // CLIENT_BUSYWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/sessionmenuwidget.h0000664000175000017500000000400012641367670026716 0ustar jaakkojaakko#ifndef DENG_CLIENT_SESSIONMENUWIDGET_H #define DENG_CLIENT_SESSIONMENUWIDGET_H #include "gamesessionwidget.h" #include "gamefilterwidget.h" #include #include /** * Specialized menu that contains items represented by GameSessionWidget * (or a class derived from it). * * Acts as the base class for the various game selection menus: singleplayer, * multiplayer, saved sessions. Common functionality such as sorting is * handled here in the base class. * * @ingroup ui */ class SessionMenuWidget : public de::MenuWidget, public de::ChildWidgetOrganizer::IWidgetFactory { Q_OBJECT public: /// Items in the menu should implement this interface to allow the /// base class to handle them. class SessionItem { public: SessionItem(SessionMenuWidget &owner); virtual ~SessionItem(); SessionMenuWidget &menu() const; /// Returns the title (label) for sorting. virtual de::String title() const = 0; /// Returns the game for sorting. virtual de::String gameIdentityKey() const = 0; virtual de::String sortKey() const; private: DENG2_PRIVATE(d) }; public: SessionMenuWidget(de::String const &name = ""); /** * Sets the game filter widget that defines the filter and sort * order for the menu. * * @param filter Filtering widget. */ void setFilter(GameFilterWidget *filter); GameFilterWidget &filter() const; void setColumns(int numberOfColumns); /** * Constructs an Action for activating a game session. * * @param item Item to activate. * * @return Action for selecting the item. Callers gets ownership (ref 1). */ virtual de::Action *makeAction(de::ui::Item const &item) = 0; signals: void availabilityChanged(); void sessionSelected(de::ui::Item const *item); public slots: void sort(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_SESSIONMENUWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/icvarwidget.h0000664000175000017500000000221212641367670025455 0ustar jaakkojaakko/** @file icvarwidget.h Interface for console variable widgets. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_ICVARWIDGET_H #define DENG_CLIENT_ICVARWIDGET_H /** * Interface for console variable widgets. */ class ICVarWidget { public: virtual ~ICVarWidget() {} virtual char const *cvarPath() const = 0; virtual void updateFromCVar() = 0; }; #endif // DENG_CLIENT_ICVARWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/consolecommandwidget.h0000664000175000017500000000332012641367670027353 0ustar jaakkojaakko/** @file consolecommandwidget.h Text editor with a history buffer. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_CONSOLECOMMANDWIDGET_H #define DENG_CLIENT_CONSOLECOMMANDWIDGET_H #include /** * Text editor with a history buffer. Entered commands are executed as console * commands. Uses the console Lexicon for word completion. * * Whenever the current game changes, the lexicon is automatically updated to * include the terms of the loaded game. * * @ingroup gui */ class ConsoleCommandWidget : public de::CommandWidget { public: ConsoleCommandWidget(de::String const &name = ""); // Events. void focusGained(); void focusLost(); bool handleEvent(de::Event const &event); protected: bool isAcceptedAsCommand(de::String const &text); void executeCommand(de::String const &text); void autoCompletionBegan(de::String const &prefix); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_CONSOLECOMMANDWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/multiplayermenuwidget.h0000664000175000017500000000252712641367670027616 0ustar jaakkojaakko/** @file multiplayermenuwidget.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_MULTIPLAYERMENUWIDGET_H #define DENG_CLIENT_MULTIPLAYERMENUWIDGET_H #include /** * Popup menu that appears in the task bar when joined to a multiplayer game. * * @ingroup ui */ class MultiplayerMenuWidget : public de::PopupMenuWidget { Q_OBJECT public: MultiplayerMenuWidget(); public slots: void updateElapsedTime(); protected: void preparePanelForOpening(); void panelClosing(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_MULTIPLAYERMENUWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/savedsessionmenuwidget.h0000664000175000017500000000301612641367670027747 0ustar jaakkojaakko/** @file savedsessionmenuwidget.h * * @authors Copyright © 2014 Jaakko Keränen * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_SAVEDSESSIONMENUWIDGET_H #define DENG_CLIENT_SAVEDSESSIONMENUWIDGET_H #include "sessionmenuwidget.h" #include /** * Menu that populates itself with available saved game sessions. * * @ingroup ui */ class SavedSessionMenuWidget : public SessionMenuWidget { public: SavedSessionMenuWidget(); void update(); de::Action *makeAction(de::ui::Item const &item); // Widget factory. GuiWidget *makeItemWidget(de::ui::Item const &, GuiWidget const *); void updateItemWidget(GuiWidget &, de::ui::Item const &); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_SAVEDSESSIONMENUWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/profilepickerwidget.h0000664000175000017500000000423612641367670027217 0ustar jaakkojaakko/** @file profilepickerwidget.h Widget for selecting/manipulating settings profiles. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_PROFILEPICKERWIDGET_H #define DENG_CLIENT_PROFILEPICKERWIDGET_H #include #include "SettingsRegister" /** * Widget for selecting/manipulating settings profiles. * * While ProfilePickerWidget owns its popup menu button, the user is * responsible for laying it out appropriately. However, by default it is * attached to the right edge of the profile picker choice widget. */ class ProfilePickerWidget : public de::ChoiceWidget { Q_OBJECT public: /** * Constructs a settings profile picker. * * @param settings Settings to operate on. * @param description Human-readable description of a profile, for instance * "appearance". Appears in the UI dialogs. * @param name Name for the widget. */ ProfilePickerWidget(SettingsRegister &settings, de::String const &description, de::String const &name = ""); ButtonWidget &button(); signals: void profileChanged(); void profileEditorRequested(); public slots: void openMenu(); void edit(); void rename(); void duplicate(); void reset(); void remove(); void applySelectedProfile(); protected: void updateStyle(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_PROFILEPICKERWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/taskbarwidget.h0000664000175000017500000000414312641367670026005 0ustar jaakkojaakko/** @file taskbarwidget.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_TASKBARWIDGET_H #define DENG_CLIENT_TASKBARWIDGET_H #include #include #include #include #include "consolewidget.h" /** * Task bar that acts as the primary UI element of the client's UI. * * @ingroup gui */ class TaskBarWidget : public de::GuiWidget { Q_OBJECT public: TaskBarWidget(); ConsoleWidget &console(); de::CommandWidget &commandLine(); de::ButtonWidget &logoButton(); bool isOpen() const; de::Rule const &shift(); // Events. void viewResized(); void update(); void drawContent(); bool handleEvent(de::Event const &event); public slots: void open(); void openAndPauseGame(); void close(); void openConfigMenu(); void closeConfigMenu(); void openMainMenu(); void closeMainMenu(); void chooseIWADFolder(); void openMultiplayerMenu(); void unloadGame(); void showAbout(); void showUpdaterSettings(); void switchGame(); void showMultiplayer(); void connectToServerManually(); void showTutorial(); protected slots: void updateCommandLineLayout(); signals: void opened(); void closed(); protected: void glInit(); void glDeinit(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_TASKBARWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/mpsessionmenuwidget.h0000664000175000017500000000313312641367670027261 0ustar jaakkojaakko/** @file mpsessionmenuwidget.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_MPSESSIONMENUWIDGET_H #define DENG_CLIENT_MPSESSIONMENUWIDGET_H #include "sessionmenuwidget.h" #include "network/net_main.h" /** * Game session menu that populates itself with available multiplayer games. * * @ingroup ui */ class MPSessionMenuWidget : public SessionMenuWidget { Q_OBJECT public: enum DiscoveryMode { NoDiscovery, DiscoverUsingMaster, DirectDiscoveryOnly }; public: MPSessionMenuWidget(DiscoveryMode discovery = NoDiscovery); de::Action *makeAction(de::ui::Item const &item); // Widget factory. GuiWidget *makeItemWidget(de::ui::Item const &, GuiWidget const *); void updateItemWidget(GuiWidget &, de::ui::Item const &); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_MPSESSIONMENUWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/cvarchoicewidget.h0000664000175000017500000000271012641367670026462 0ustar jaakkojaakko/** @file cvarchoicewidget.h Console variable choice. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_CVARCHOICEWIDGET_H #define DENG_CLIENT_CVARCHOICEWIDGET_H #include #include "icvarwidget.h" /** * Console variable choice for integer-type cvars with a limited number of * valid settings. The choice items' user data is used as the cvar value. */ class CVarChoiceWidget : public de::ChoiceWidget, public ICVarWidget { Q_OBJECT public: CVarChoiceWidget(char const *cvarPath); char const *cvarPath() const; public slots: void updateFromCVar(); protected slots: void setCVarValueFromWidget(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_CVARCHOICEWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/gameselectionwidget.h0000664000175000017500000000472412641367670027202 0ustar jaakkojaakko/** @file gameselectionwidget.h * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_GAMESELECTIONWIDGET_H #define DENG_CLIENT_GAMESELECTIONWIDGET_H #include #include #include #include #include "gamefilterwidget.h" /** * Menu for selecting */ class GameSelectionWidget : public de::ScrollAreaWidget, public de::IPersistent { Q_OBJECT public: GameSelectionWidget(de::String const &name = "gameselection"); void setTitleColor(de::DotPath const &colorId, de::DotPath const &hoverColorId, de::ButtonWidget::HoverColorMode mode = de::ButtonWidget::ModulateColor); void setTitleFont(de::DotPath const &fontId); /** * Returns the filter header widget. It can be placed manually by the user * of the widget. */ GameFilterWidget &filter(); de::FoldPanelWidget *subsetFold(de::String const &name); /** * Enables or disables execution of the session selection action by * pressing the menu items in the widget. By default, this is disabled. * * @param doAction @c true to allow action execution, @c false to disallow. */ void enableActionOnSelection(bool doAction); de::Action *makeAction(de::ui::Item const &item) const; // Events. void update(); // Implements IPersistent. void operator >> (de::PersistentState &toState) const; void operator << (de::PersistentState const &fromState); signals: void gameSessionSelected(de::ui::Item const *item); protected slots: void updateSubsetLayout(); void select(de::ui::Item const *item); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_GAMESELECTIONWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/consolewidget.h0000664000175000017500000000523112641367670026017 0ustar jaakkojaakko/** @file consolewidget.h Console commandline and message history. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_CONSOLEWIDGET_H #define DENG_CLIENT_CONSOLEWIDGET_H #include #include #include #include #include #include "consolecommandwidget.h" /** * Console command line and message history. * * ConsoleWidget expects to be bottom-left anchored. It resizes its height * automatically. The user can drag the right edge to resize the widget. * * @ingroup gui */ class ConsoleWidget : public de::GuiWidget, public de::IPersistent { Q_OBJECT public: ConsoleWidget(); GuiWidget &buttons(); de::CommandWidget &commandLine(); de::LogWidget &log(); de::Rule const &shift(); bool isLogOpen() const; /** * Enables or disables the console log background blur. * * @todo Blurring is presently forcibly disabled when a game is loaded. * * @param yes @c true to enable blur, otherwise @c false. */ void enableBlur(bool yes = true); // Events. void viewResized(); void update(); bool handleEvent(de::Event const &event); // Implements IPersistent. void operator >> (de::PersistentState &toState) const; void operator << (de::PersistentState const &fromState); signals: void commandModeChanged(); void commandLineGotFocus(); public slots: void openLog(); void closeLog(); void closeLogAndUnfocusCommandLine(); void clearLog(); void zeroLogHeight(); void showFullLog(); void setFullyOpaque(); void commandLineFocusGained(); void commandLineFocusLost(); void focusOnCommandLine(); void closeMenu(); void commandWasEntered(de::String const &); void copyLogPathToClipboard(); protected slots: void logContentHeightIncreased(int delta); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_CONSOLEWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/tutorialwidget.h0000664000175000017500000000247412641367670026226 0ustar jaakkojaakko/** @file tutorialwidget.h Introduction to the Doomsday UI. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_TUTORIALWIDGET_H #define DENG_CLIENT_TUTORIALWIDGET_H #include class TutorialWidget : public de::GuiWidget { Q_OBJECT public: TutorialWidget(); void start(); // Events. bool handleEvent(de::Event const &event); public slots: void continueToNextStep(); void backToPreviousStep(); void stop(); void dismiss(); void flashHighlight(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_TUTORIALWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/singleplayersessionmenuwidget.h0000664000175000017500000000306712641367670031351 0ustar jaakkojaakko/** @file singleplayersessionmenuwidget.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_SINGLEPLAYERSESSIONMENUWIDGET_H #define DENG_CLIENT_SINGLEPLAYERSESSIONMENUWIDGET_H #include "sessionmenuwidget.h" class SingleplayerSessionMenuWidget : public SessionMenuWidget { public: enum Mode { ShowAvailableGames, ShowGamesWithMissingResources }; SingleplayerSessionMenuWidget(Mode mode = ShowAvailableGames, de::String const &name = ""); Mode mode() const; de::Action *makeAction(de::ui::Item const &item); // Widget factory. GuiWidget *makeItemWidget(de::ui::Item const &, GuiWidget const *); void updateItemWidget(GuiWidget &, de::ui::Item const &); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_SINGLEPLAYERSESSIONMENUWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/keygrabberwidget.h0000664000175000017500000000242412641367670026473 0ustar jaakkojaakko/** @file keygrabberwidget.h Grabs key events and shows them. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_KEYGRABBERWIDGET_H #define DENG_CLIENT_KEYGRABBERWIDGET_H #include /** * When focused, grabs key events and shows the key event data. However, Esc is * never grabbed. */ class KeyGrabberWidget : public de::LabelWidget { public: KeyGrabberWidget(de::String const &name = ""); bool handleEvent(de::Event const &event); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_KEYGRABBERWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/gamefilterwidget.h0000664000175000017500000000513112641367670026473 0ustar jaakkojaakko/** @file gamefilterwidget.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_GAMEFILTERWIDGET_H #define DENG_CLIENT_GAMEFILTERWIDGET_H #include #include /** * Filtering and sorting parameters for a game selection widget. Allows picking the type * of games to show (singleplayer or multiplayer) and how the games are sorted. * * The widget defines its own height, but width must be set manually. * * The filter and sort setting is saved persistently. */ class GameFilterWidget : public de::GuiWidget, public de::IPersistent { Q_OBJECT public: enum FilterFlag { Singleplayer = 0x1, Multiplayer = 0x2, AllGames = Singleplayer | Multiplayer }; Q_DECLARE_FLAGS(Filter, FilterFlag) enum FilterMode { UserChangeable, Permanent }; enum SortOrder { SortByTitle, SortByIdentityKey }; public: GameFilterWidget(de::String const &name = "gamefilter"); void useInvertedStyle(); void setFilter(Filter flt, FilterMode mode = UserChangeable); /** * Enables a background for the filter. Opacity of the background is controlled * by the provided scroll position rule, so that the background is visible only * when the scroll position is greater than zero. * * @param scrollPositionRule Rule. */ void enableBackground(de::Rule const &scrollPositionRule); Filter filter() const; SortOrder sortOrder() const; // Events. void update(); // Implements IPersistent. void operator >> (de::PersistentState &toState) const; void operator << (de::PersistentState const &fromState); signals: void filterChanged(); void sortOrderChanged(); private: DENG2_PRIVATE(d) }; Q_DECLARE_OPERATORS_FOR_FLAGS(GameFilterWidget::Filter) #endif // DENG_CLIENT_GAMEFILTERWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/cvarsliderwidget.h0000664000175000017500000000250112641367670026510 0ustar jaakkojaakko/** @file cvarsliderwidget.h Slider for adjusting a cvar. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_CVARSLIDERWIDGET_H #define DENG_CLIENT_CVARSLIDERWIDGET_H #include #include "icvarwidget.h" /** * Console variable slider. */ class CVarSliderWidget : public de::SliderWidget, public ICVarWidget { Q_OBJECT public: CVarSliderWidget(char const *cvarPath); char const *cvarPath() const; public slots: void updateFromCVar(); void setCVarValueFromWidget(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_CVARSLIDERWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/cvartogglewidget.h0000664000175000017500000000263012641367670026512 0ustar jaakkojaakko/** @file cvartogglewidget.h Console variable toggle. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_CVARTOGGLEWIDGET_H #define DENG_CLIENT_CVARTOGGLEWIDGET_H #include #include "icvarwidget.h" /** * Console variable toggle for on/off type of cvars (value 0 or 1). */ class CVarToggleWidget : public de::ToggleWidget, public ICVarWidget { Q_OBJECT public: CVarToggleWidget(char const *cvarPath, de::String const &labelText = ""); char const *cvarPath() const; public slots: void updateFromCVar(); protected slots: void setCVarValueFromWidget(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_CVARTOGGLEWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/gamesessionwidget.h0000664000175000017500000000345512641367670026700 0ustar jaakkojaakko/** @file gamesessionwidget.h * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_GAMESESSIONWIDGET_H #define DENG_CLIENT_GAMESESSIONWIDGET_H #include #include #include /** * Widget for representing an item (game session) in a session menu (see * SessionMenuWidget). * * It has two buttons: one for starting the game and one for configuring it. */ class GameSessionWidget : public de::GuiWidget { public: enum PopupStyle { PopupDocument, PopupMenu }; public: GameSessionWidget(PopupStyle popupStyle = PopupDocument, de::ui::Direction popupOpeningDirection = de::ui::Up); PopupStyle popupStyle() const; de::ButtonWidget &loadButton(); de::ButtonWidget &infoButton(); de::ButtonWidget &menuButton(); de::DocumentWidget &document(); de::PopupMenuWidget &menu(); /** * Called immediately before the Info button is pressed. */ virtual void updateInfoContent(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_GAMESESSIONWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/gameuiwidget.h0000664000175000017500000000255512641367670025632 0ustar jaakkojaakko/** @file gameuiwidget.h Widget for legacy game UI elements. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_GAMEUIWIDGET_H #define DENG_CLIENT_GAMEUIWIDGET_H #include /** * Widget that encapsulates game-side UI elements. */ class GameUIWidget : public de::GuiWidget { public: GameUIWidget(); void drawContent(); /** * Determines if InFine animations will be drawn stretched to cover * the entire view. */ static bool finaleStretch(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_GAMEUIWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/widgets/inputbindingwidget.h0000664000175000017500000000376312641367670027057 0ustar jaakkojaakko/** @file inputbindingwidget.h Widget for creating input bindings. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_INPUTBINDINGWIDGET_H #define DENG_CLIENT_INPUTBINDINGWIDGET_H #include /** * Widget for viewing and changing an input binding. */ class InputBindingWidget : public de::AuxButtonWidget { public: InputBindingWidget(); void setDefaultBinding(de::String const &eventDesc); void setCommand(de::String const &command); /** * Enables or disables explicitly specified modifier conditions. Bindings can be * specified with or without modifiers -- if modifiers are not specified, the binding * can be triggered regardless of modifier state. * * @param mods @c true to includes modifiers in the binding. If @c false, modifiers * can be bound like any other input. */ void enableModifiers(bool mods); void setContext(QString const &bindingContext) { setContexts(QStringList() << bindingContext); } void setContexts(QStringList const &contexts); // Events. bool handleEvent(de::Event const &event); public: static InputBindingWidget *newTaskBarShortcut(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_INPUTBINDINGWIDGET_H doomsday-stable-1.15.7/doomsday/client/include/ui/inputdebug.h0000664000175000017500000000227012641367670023651 0ustar jaakkojaakko/** @file inputdebug.h Input debug visualization. * * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_INPUTDEBUG_H #define CLIENT_INPUTDEBUG_H #include #ifdef DENG2_DEBUG /** * Render a visual representation of the current state of all input devices. */ void I_DebugDrawer(); /** * Register the commands and variables of this module. */ void I_DebugDrawerConsoleRegister(); #endif // DENG2_DEBUG #endif // CLIENT_INPUTDEBUG_H doomsday-stable-1.15.7/doomsday/client/include/ui/ui_main.h0000664000175000017500000000717212641367670023132 0ustar jaakkojaakko/** @file ui_main.h Graphical User Interface (obsolete). * * Has ties to the console routines. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDENG_UI_MAIN_H #define LIBDENG_UI_MAIN_H #include #include "ddevent.h" #ifdef __CLIENT__ # include "MaterialVariantSpec" #endif enum fontstyle_t { FS_NORMAL, FS_BOLD, FS_LIGHT, FONTSTYLE_COUNT }; /// Numeric identifiers of predefined colors. /// @ingroup infine enum { UIC_TEXT, UIC_TITLE, UIC_SHADOW, UIC_BG_LIGHT, UIC_BG_MEDIUM, UIC_BG_DARK, UIC_BRD_HI, UIC_BRD_MED, UIC_BRD_LOW, UIC_HELP, NUM_UI_COLORS }; /// Standard dimensions. #define UI_WIDTH (1000.0f) #define UI_HEIGHT (1000.0f) #define UI_BORDER (UI_WIDTH/120) /// All borders are this wide. #define UI_SHADOW_OFFSET (MIN_OF(3, UI_WIDTH/320)) #define UI_SHADOW_STRENGTH (.6f) #define UI_BUTTON_BORDER (UI_BORDER) #define UI_BAR_WDH (UI_BORDER * 3) #define UI_BAR_BORDER (UI_BORDER / 2) #define UI_BAR_BUTTON_BORDER (3 * UI_BAR_BORDER / 2) #define UI_MAX_COLUMNS 10 /// Maximum columns for list box. typedef struct { float red, green, blue; } ui_color_t; DENG_EXTERN_C fontid_t fontFixed, fontVariable[FONTSTYLE_COUNT]; void UI_Register(void); void UI_LoadFonts(void); char const *UI_ChooseFixedFont(void); char const *UI_ChooseVariableFont(fontstyle_t style); /// @param id Id number of the color to return e.g. "UIC_TEXT". ui_color_t* UI_Color(uint id); /// @return Height of the current UI font. int UI_FontHeight(void); /** * Background with the "The Doomsday Engine" text superimposed. * * @param origin Screen-space coordinate origin (top left). * @param size Screen-space dimensions of the cursor in pixels. * @param alpha Alpha level to use when drawing the background. */ void UI_DrawDDBackground(Point2Raw const &origin, Size2Raw const &size, float alpha); void UI_MixColors(ui_color_t* a, ui_color_t* b, ui_color_t* dest, float amount); void UI_SetColorA(ui_color_t* color, float alpha); void UI_SetColor(ui_color_t* color); void UI_Gradient(const Point2Raw* origin, const Size2Raw* size, ui_color_t* top, ui_color_t* bottom, float topAlpha, float bottomAlpha); void UI_GradientEx(const Point2Raw* origin, const Size2Raw* size, int border, ui_color_t* top, ui_color_t* bottom, float topAlpha, float bottomAlpha); void UI_DrawRectEx(const Point2Raw* origin, const Size2Raw* size, int brd, dd_bool filled, ui_color_t* top, ui_color_t* bottom, float alpha, float bottomAlpha); /// Draw shadowed text. void UI_TextOutEx(const char* text, const Point2Raw* origin, ui_color_t* color, float alpha); void UI_TextOutEx2(const char* text, const Point2Raw* origin, ui_color_t* color, float alpha, int alignFlags, short textFlags); de::MaterialVariantSpec const &UI_MaterialSpec(int texSpecFlags = 0); #endif doomsday-stable-1.15.7/doomsday/client/include/ui/inputdevicehatcontrol.h0000664000175000017500000000363712641367670026130 0ustar jaakkojaakko/** @file inputdevicehatcontrol.h Hat control for a logical input device. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_INPUTSYSTEM_INPUTDEVICEHATCONTROL_H #define CLIENT_INPUTSYSTEM_INPUTDEVICEHATCONTROL_H #include "inputdevice.h" /** * Models a hat control on a "physical" input device (such as that found on joysticks). * * @ingroup ui */ class InputDeviceHatControl : public InputDeviceControl { public: explicit InputDeviceHatControl(de::String const &name = ""); virtual ~InputDeviceHatControl(); /** * Returns the current position of the hat. */ de::dint position() const; /** * @param newPosition @c -1= centered. */ void setPosition(de::dint newPosition); /** * When the state of the control last changed, in milliseconds since app init. */ de::duint time() const; de::String description() const; bool inDefaultState() const; private: de::dint _pos = -1; ///< Current position. @c -1= centered. de::duint _time = 0; ///< Timestamp of the latest change. }; #endif // CLIENT_INPUTSYSTEM_INPUTDEVICEHATCONTROL_H doomsday-stable-1.15.7/doomsday/client/include/ui/inputsystem.h0000664000175000017500000002067112641367670024114 0ustar jaakkojaakko/** @file inputsystem.h Input subsystem. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_INPUTSYSTEM_H #define CLIENT_INPUTSYSTEM_H #include #include #include #include #include "ddevent.h" #include "SettingsRegister" class BindContext; class InputDevice; #define DEFAULT_BINDING_CONTEXT_NAME "game" #define CONSOLE_BINDING_CONTEXT_NAME "console" #define UI_BINDING_CONTEXT_NAME "deui" #define GLOBAL_BINDING_CONTEXT_NAME "global" /** * Input devices, binding context stack and event tracking. * * @par Bindings * * Bindings are Record structures which describe an event => action trigger * relationship. The event being a specific observable state scenario (such as * a keypress on a keyboard) and the trigger, a more abstract action that can * be "bound" to it (such as executing a console command). * * However, it is important to note this relationship is modelled from the * @em action's perspective, rather than that of the event. This is to support * stronger decoupling of the origin from any possible action. * * Once configured (@see configure()), bindings may be freely moved between * contexts, assuming it makes sense to do so. The bindings themselves do not * reference the context in which they might reside. * * @todo Input drivers belong in this system. * * @ingroup ui */ class InputSystem : public de::System { public: InputSystem(); SettingsRegister &settings(); // System. void timeChanged(de::Clock const &); public: // Input devices ----------------------------------------------------- /// Required/referenced input device is missing. @ingroup errors DENG2_ERROR(MissingDeviceError); /* * Lookup an InputDevice by it's unique @a id. */ InputDevice &device(int id) const; /** * Lookup an InputDevice by it's unique @a id. * * @return Pointer to the associated InputDevice; otherwise @c nullptr. */ InputDevice *devicePtr(int id) const; /** * Iterate through all the InputDevices. */ de::LoopResult forAllDevices(std::function func) const; /** * Returns the total number of InputDevices initialized. */ int deviceCount() const; /** * (Re)initialize the input device models, returning all controls to their * default states. */ void initAllDevices(); /** * Returns @c true if the shift key of the keyboard is thought to be down. * @todo: Refactor away */ bool shiftDown() const; public: // Event processing -------------------------------------------------- /** * Clear the input event queue. */ void clearEvents(); bool ignoreEvents(bool yes = true); /** * @param ev A copy is made. */ void postEvent(ddevent_t *ev); /** * Process all incoming input for the given timestamp. * This is called only in the main thread, and also from the busy loop. * * This gets called at least 35 times per second. Usually more frequently * than that. */ void processEvents(timespan_t ticLength); void processSharpEvents(timespan_t ticLength); /** * If an action has been defined for the event, trigger it. * * This is meant to be used as a way for Widgets to take advantage of the * traditional bindings system for user-customizable actions. * * @param event Event instance. * @param context Name of the binding context. If empty, all contexts * all checked. * * @return @c true if an action was triggered, @c false otherwise. */ bool tryEvent(de::Event const &event, de::String const &context = ""); bool tryEvent(ddevent_t const &event, de::String const &context = ""); public: static bool convertEvent(ddevent_t const &from, event_t &to); static bool convertEvent(de::Event const &from, ddevent_t &to); /** * Updates virtual input device state. * * Normally this is called automatically at the appropriate time, however * if a widget eats an event before it is passed to the bindings system, * it might still wish to call this to ensure subsequent bindings are * correctly evaluated. * * @param event Input event. * * @todo make private (all widgets should belong to / own a BindContext). */ void trackEvent(de::Event const &event); void trackEvent(ddevent_t const &event); public: // Binding (context) management -------------------------------------- /// Required/referenced binding context is missing. @ingroup errors DENG2_ERROR(MissingContextError); void bindDefaults(); void bindGameDefaults(); /** * Try to make a new (console) command binding. * * @param eventDesc Textual descriptor for the event, with the relevant * context for the would-be binding encoded. * @param command Console command(s) which the binding will execute when * triggered, if a binding is created. * * @return Resultant command binding Record if any. */ de::Record *bindCommand(char const *eventDesc, char const *command); /** * Try to make a new (player) impulse binding. * * @param ctrlDesc Textual descriptor for the device-control event. * @param impulseDesc Player impulse which the binding will execute when * triggered, if a binding is created. Impulses are * associated with a context, which determines where * the binding will be linked, if a binding is created. * * @return Resultant impulse binding Record if any. */ de::Record *bindImpulse(char const *ctrlDesc, char const *impulseDesc); /** * Try to remove the one unique binding associated with @a id. * * @return @c true if that binding was removed. */ bool removeBinding(int id); /** * Remove all bindings in all contexts. */ void removeAllBindings(); // --- /** * Enable the contexts for the initial state. */ void initialContextActivations(); /** * Destroy all binding contexts and the bindings within the contexts. */ void clearAllContexts(); /** * Returns @c true if the symbolic @a name references a known context. */ bool hasContext(de::String const &name) const; /** * Creates a new binding context. The new context has the highest priority * of all existing contexts, and is inactive. * * @param name A unique, symbolic name for the bind context. * * @return Resultant BindContext. Ownership is retained. */ BindContext *newContext(de::String const &name); /** * Lookup a binding context by symbolic @a name. */ BindContext &context(de::String const &name) const; BindContext *contextPtr(de::String const &name) const; /** * Lookup a binding context by stack @a position. */ BindContext &contextAt(int position) const; /** * Returns the stack position of the specified binding @a context. */ int contextPositionOf(BindContext *context) const; /** * Iterate through all the BindContexts from highest to lowest priority. */ de::LoopResult forAllContexts(std::function func) const; /** * Returns the total number of binding contexts in the system. */ int contextCount() const; public: /** * Register the console commands and variables of this module. */ static void consoleRegister(); private: DENG2_PRIVATE(d) }; #endif // CLIENT_INPUTSYSTEM_H doomsday-stable-1.15.7/doomsday/client/include/ui/busyvisual.h0000664000175000017500000000407412641367670023715 0ustar jaakkojaakko/** * @file busyvisual.h * Busy Mode visualizer. @ingroup render * * @authors Copyright © 2007-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_RENDER_BUSYVISUAL_H #define LIBDENG_RENDER_BUSYVISUAL_H #include "api_busy.h" #ifdef __cplusplus extern "C" { #endif void BusyVisual_PrepareResources(void); /** * @todo Does the console transition animation really belong in the busy visual? */ /// Busy mode transition style. typedef enum { FIRST_TRANSITIONSTYLE, TS_CROSSFADE = FIRST_TRANSITIONSTYLE, ///< Basic opacity crossfade. TS_DOOMSMOOTH, ///< Emulates the DOOM "blood on wall" screen wipe (smoothed). TS_DOOM, ///< Emulates the DOOM "blood on wall" screen wipe. LAST_TRANSITIONSTYLE = TS_DOOM } transitionstyle_t; #ifdef __CLIENT__ extern int rTransition; extern int rTransitionTics; void Con_TransitionRegister(void); void Con_TransitionConfigure(void); void Con_TransitionBegin(void); /// @return @c true if a busy mode transition animation is currently in progress. dd_bool Con_TransitionInProgress(void); void Con_TransitionTicker(timespan_t ticLength); void Con_DrawTransition(void); #endif // __CLIENT__ #ifdef __cplusplus } // extern "C" #endif #endif /* LIBDENG_RENDER_BUSYVISUAL_H */ doomsday-stable-1.15.7/doomsday/client/include/ui/commandaction.h0000664000175000017500000000250012641367670024313 0ustar jaakkojaakko/** @file commandaction.h Action that executes a console command. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_COMMANDACTION_H #define CLIENT_COMMANDACTION_H #include "dd_share.h" #include #include /** * Action that executes a console command. * * @ingroup ui */ class CommandAction : public de::Action { public: CommandAction(de::String const &cmd, int commandSource = CMDS_DDAY); de::String command() const { return _command; } void trigger(); private: de::String _command; int _source; }; #endif // CLIENT_COMMANDACTION_H doomsday-stable-1.15.7/doomsday/client/include/updater.h0000664000175000017500000000531312641367670022533 0ustar jaakkojaakko/** * @file updater.h * Automatic updater that works with dengine.net. @ingroup base * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ /** * @defgroup updater Automatic Updater */ #ifndef LIBDENG_UPDATER_H #define LIBDENG_UPDATER_H #ifndef __CLIENT__ # error "updater.h is only for the client" #endif #include "dd_types.h" #include #include #include #include #include /** * Automatic updater. Communicates with dengine.net and coordinates the * download and reinstall procedure. */ class Updater : public QObject { Q_OBJECT public: enum CheckMode { AlwaysShowResult, OnlyShowResultIfUpdateAvailable }; public: /** * Initializes the automatic updater. If it is time to check for an update, * queries the latest version from http://dengine.net/ and determines the * need to update. */ Updater(); void setupUI(); de::ProgressWidget &progress(); public slots: void gotReply(QNetworkReply *); void downloadProgressed(int percentage); void downloadCompleted(int result); void downloadFailed(QString); void recheck(); /** * Shows the Updater Settings dialog. */ void showSettings(); void showCurrentDownload(); /** * Tells the updater to check for updates now. This is called when a manual * check is requested. * * @param notify Show the update notification dialog even though * the current version is up to date. */ void checkNow(CheckMode mode = AlwaysShowResult); void checkNowShowingProgress(); /** * Print in the console when the latest update check was made. */ void printLastUpdated(); protected slots: void downloadDialogClosed(); private: DENG2_PRIVATE(d) }; #endif // LIBDENG_UPDATER_H doomsday-stable-1.15.7/doomsday/client/include/MapObject0000664000175000017500000000003512641367670022501 0ustar jaakkojaakko#include "world/mapobject.h" doomsday-stable-1.15.7/doomsday/client/include/clientapp.h0000664000175000017500000000615712641367670023055 0ustar jaakkojaakko/** @file clientapp.h The client application. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENTAPP_H #define CLIENTAPP_H #include #include "settingsregister.h" #include "network/serverlink.h" #include "ui/inputsystem.h" #include "ui/clientwindowsystem.h" #include "ui/infine/infinesystem.h" #include "render/rendersystem.h" #include "resource/resourcesystem.h" #include "updater.h" #include "Games" #include "world/worldsystem.h" /** * The client application. */ class ClientApp : public de::BaseGuiApp { Q_OBJECT public: ClientApp(int &argc, char **argv); /** * Sets up all the subsystems of the application. Must be called before the * event loop is started. At the end of this method, the bootstrap script is * executed. */ void initialize(); void preFrame(); void postFrame(); /** * Reports a new alert to the user. * * @param msg Message to show. May contain style escapes. * @param level Importance of the message. */ static void alert(de::String const &msg, de::LogEntry::Level level = de::LogEntry::Message); public: static ClientApp &app(); static Updater &updater(); static SettingsRegister &logSettings(); static SettingsRegister &networkSettings(); static SettingsRegister &audioSettings(); ///< @todo Belongs in AudioSystem. static ServerLink &serverLink(); static InFineSystem &infineSystem(); static InputSystem &inputSystem(); static ClientWindowSystem &windowSystem(); static RenderSystem &renderSystem(); static ResourceSystem &resourceSystem(); static de::Games &games(); static de::WorldSystem &worldSystem(); static bool hasRenderSystem(); public slots: void openHomepageInBrowser(); void openInBrowser(QUrl url); /** * Enters the "native UI" mode that temporarily switches the main window to a * regular window and restores the desktop display mode. This allows the user to * access native UI widgets normally. * * Call this before showing native UI widgets. You must call endNativeUIMode() * afterwards. */ void beginNativeUIMode(); /** * Ends the "native UI" mode, restoring the previous main window properties. */ void endNativeUIMode(); private: DENG2_PRIVATE(d) }; #endif // CLIENTAPP_H doomsday-stable-1.15.7/doomsday/client/include/DrawLists0000664000175000017500000000003612641367670022552 0ustar jaakkojaakko#include "render/drawlists.h" doomsday-stable-1.15.7/doomsday/client/include/de_platform.h0000664000175000017500000000502112641367670023357 0ustar jaakkojaakko/** @file de_platform.h Platform Independence. * * Define either WIN32 or UNIX when compiling. * * Use this header file in source files which can be compiled on any * platform but still use some platform specific code. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef __DOOMSDAY_PLATFORM__ #define __DOOMSDAY_PLATFORM__ #include "dd_types.h" #include /* * The Win32 Platform */ #if defined(WIN32) #if __cplusplus // must be included before anything that defines open: /// @todo Windows: Get rid of the open macro. # include # include # include # ifndef DENG2_QT_5_0_OR_NEWER # include # endif #endif #define WIN32_LEAN_AND_MEAN #include #include #define INTEGER64 __int64 #define stricmp _stricmp #define strnicmp _strnicmp #define strlwr _strlwr #define strupr _strupr #define strdup _strdup #define spawnlp _spawnlp #endif // WIN32 /* * The Unix Platform */ #if defined(UNIX) #include #include #include #ifdef __cplusplus # include #endif #ifdef __GNUC__ # define GCC_VERSION (__GNUC__*10000 + __GNUC_MINOR__*100 + __GNUC_PATCHLEVEL__) #endif typedef long long int INTEGER64; typedef unsigned int DWORD; /* * Networking. */ #define INVALID_SOCKET (-1) #define SOCKET_ERROR (-1) /* * File system routines. */ #define _getdrive() (0) #define _chdrive(x) #define _getcwd getcwd #define _chdir chdir #endif // UNIX // Initialization code. #ifdef WIN32 # include "dd_winit.h" #else # ifdef UNIX # include "dd_uinit.h" # endif #endif #endif // __DOOMSDAY_PLATFORM__ doomsday-stable-1.15.7/doomsday/client/include/AbstractFont0000664000175000017500000000004312641367670023226 0ustar jaakkojaakko#include "resource/abstractfont.h" doomsday-stable-1.15.7/doomsday/client/include/Game0000664000175000017500000000002212641367670021502 0ustar jaakkojaakko#include "game.h" doomsday-stable-1.15.7/doomsday/client/include/Mesh0000664000175000017500000000002212641367670021525 0ustar jaakkojaakko#include "mesh.h" doomsday-stable-1.15.7/doomsday/client/include/HEdge0000664000175000017500000000002312641367670021606 0ustar jaakkojaakko#include "hedge.h" doomsday-stable-1.15.7/doomsday/client/include/FontManifest0000664000175000017500000000004312641367670023231 0ustar jaakkojaakko#include "resource/fontmanifest.h" doomsday-stable-1.15.7/doomsday/client/include/face.h0000664000175000017500000000557112641367670021773 0ustar jaakkojaakko/** @file face.h Mesh Geometry Face. * * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_DATA_MESH_FACE_H #define DENG_DATA_MESH_FACE_H #include #include #include "Mesh" namespace de { /** * Mesh face geometry. * * @ingroup data */ class Face : public MeshElement { public: /// @todo make private: /// Total number of half-edge's in the face geometry. int _hedgeCount; public: explicit Face(Mesh &mesh); /** * Total number of half-edges in the face geometry. */ int hedgeCount() const; /** * Returns a pointer to the first half-edge in the face geometry (note that * half-edges are sorted in a clockwise order). May return @c 0 if there is * no half-edge linked to the face. */ HEdge *hedge() const; /** * Change the first half-edge in the face geometry. */ void setHEdge(HEdge const *newHEdge); /** * Returns the axis-aligned bounding box which encompases all the vertexes * which define the face geometry. */ AABoxd const &aaBox() const; /** * Update the face geometry's axis-aligned bounding box to encompass all vertexes. */ void updateAABox(); /** * Returns the point described by the average origin coordinates of all the * vertexes which define the geometry. */ Vector2d const ¢er() const; /** * Update the center point of the geometry. * * @pre Axis-aligned bounding box must have been initialized. */ void updateCenter(); /** * Determines whether the face geometry is currently convex. * * @note Due to the potential computational complexity of determining convexity * this should be called sparingly/only when necessary. * * @todo Cache this result. */ bool isConvex() const; /** * Returns a textual human-readable description/representation of the face * suitable for writing to the application's output log. */ String description() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_DATA_MESH_FACE_H doomsday-stable-1.15.7/doomsday/client/include/TextureScheme0000664000175000017500000000004412641367670023422 0ustar jaakkojaakko#include "resource/texturescheme.h" doomsday-stable-1.15.7/doomsday/client/include/Plane0000664000175000017500000000003112641367670021670 0ustar jaakkojaakko#include "world/plane.h" doomsday-stable-1.15.7/doomsday/client/include/games.h0000664000175000017500000001065612641367670022171 0ustar jaakkojaakko/** @file games.h Specialized collection for a set of logical Games. * * @authors Copyright © 2012-2013 Daniel Swanson * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_GAMES_H #define LIBDENG_GAMES_H #include "game.h" #include "dd_share.h" #include #include #include #include namespace de { /** * Encapsulates a collection of de::Game instances and the logical operations * which are performed upon it (such as searches and various index printing * algorithms). * * @ingroup core */ class Games { public: /// The requested game does not exist in the collection. @ingroup errors DENG2_ERROR(NotFoundError); /// Used for returning the result of game searches. @see findAll() struct GameListItem { Game *game; GameListItem(Game *game = 0) : game(game) {} /// @return @c true= this game's title is lexically less than that of @a other. bool operator < (GameListItem const &other) const { return game->title().compareWithoutCase(other.game->title()) < 0; } }; typedef QList GameList; typedef QList All; /// Notified when a new game is added. DENG2_DEFINE_AUDIENCE2(Addition, void gameAdded(Game &game)) /// Notified after game resources have been located. DENG2_DEFINE_AUDIENCE2(Readiness, void gameReadinessUpdated()) public: Games(); /// @return The special "null" Game instance. Game &nullGame() const; /// @return Total number of registered games. inline int count() const { return all().count(); } /// @return Number of games marked as currently playable. int numPlayable() const; /** * @param game Game instance. * @return Unique identifier associated with @a game. */ gameid_t id(Game const &game) const; /** * @return Game associated with @a identityKey. * * @throws NotFoundError if no game is associated with @a identityKey. */ Game &byIdentityKey(String identityKey) const; /** * @return Game associated with @a gameId. * * @throws NotFoundError if no game is associated with @a gameId. */ Game &byId(gameid_t gameId) const; /** * @return Game associated with unique index @a idx. * * @deprecated Iterate games() instead. */ Game &byIndex(int idx) const; void clear(); /** * Add a new Game to this collection. If @a game is already present in the * collection this is no-op. * * @param game Game to be added. */ void add(Game &game); /** * Returns a list of all the Game instances in the collection. */ All const &all() const; /** * Try to locate all startup resources for all registered games. */ void locateAllResources(); /** * Forgets the previously located resources of all registered games. */ void forgetAllResources(); /** * Collects all games. * * @param collected List to be populated with the collected games. * @return Number of collected games. */ int collectAll(GameList &collected); /** * Find the first playable game in this collection (in registration order). * @return The found game else @c NULL. */ Game *firstPlayable() const; /** * Try to locate all startup resources for @a game. */ void locateStartupResources(Game &game); public: /// Register the console commands, variables, etc..., of this module. static void consoleRegister(); private: DENG2_PRIVATE(d) }; } // namespace de #endif // LIBDENG_GAMES_H doomsday-stable-1.15.7/doomsday/client/include/MapDef0000664000175000017500000000003512641367670021771 0ustar jaakkojaakko#include "resource/mapdef.h" doomsday-stable-1.15.7/doomsday/client/include/sdlnet_dummy.h0000664000175000017500000000274712641367670023603 0ustar jaakkojaakko#ifndef DENG_SDLNET_DUMMY_H #define DENG_SDLNET_DUMMY_H typedef void* TCPsocket; typedef struct { int host; short port; } IPaddress; typedef void* SDLNet_SocketSet; static int SDLNet_Init(void) { return 0; } static void SDLNet_Quit(void) {} static TCPsocket SDLNet_TCP_Open(IPaddress* ip) { return 0; } static void SDLNet_TCP_Close(TCPsocket sock) {} static TCPsocket SDLNet_TCP_Accept(TCPsocket server) { return 0; } static int SDLNet_TCP_Recv(TCPsocket sock, void* data, int maxlen) { return 0; } static int SDLNet_TCP_Send(TCPsocket sock, const void* data, int len) { return 0; } static IPaddress* SDLNet_TCP_GetPeerAddress(TCPsocket sock) { return 0; } static int SDLNet_Read16(void* area) { return 0; } static int SDLNet_Read32(void* area) { return 0; } static SDLNet_SocketSet SDLNet_AllocSocketSet(int max) { return 0; } static void SDLNet_FreeSocketSet(SDLNet_SocketSet set) {} //static int SDLNet_AddSocket(SDLNet_SocketSet set, void* sock) { return 0; } //static int SDLNet_DelSocket(SDLNet_SocketSet set, void* sock) { return 0; } static int SDLNet_TCP_AddSocket(SDLNet_SocketSet set, TCPsocket sock) { return 0; } static int SDLNet_TCP_DelSocket(SDLNet_SocketSet set, TCPsocket sock) { return 0; } static int SDLNet_CheckSockets(SDLNet_SocketSet set, int timeout) { return 0; } static int SDLNet_SocketReady(TCPsocket sock) { return 0; } static int SDLNet_ResolveHost(IPaddress* a, const char* host, int port) { return 0; } static const char* SDLNet_GetError() { return "dummy"; } #endif doomsday-stable-1.15.7/doomsday/client/include/settingsregister.h0000664000175000017500000001205712641367670024477 0ustar jaakkojaakko/** @file settingsregister.h Register of settings profiles. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_SETTINGSREGISTER_H #define DENG_SETTINGSREGISTER_H #include #include #include #include /** * Collection of settings (cvars, Config variables) of which there can be * several alternative profiles. When a register is created, it automatically * gets a profile called "User". * * The default values are stored separately, so that any profile can be reseted * back to the default values. * * All settings of a register should be defined before it gets used. * * The current profile is simply whatever values the identified cvars/variables * presently hold. These current values get stored persistently in the app's * Config (and via con_config) as usual. SettingsRegister is responsible for * storing the non-current profiles persistently. The (de)serialization occurs * whenever the game is (un)loaded, as all cvars are presently game-specific. * * It is possible to install new profiles via resource packs. The profiles should * be placed to /data/profiles/(persistentName)/. */ class SettingsRegister { public: enum SettingType { IntCVar, FloatCVar, StringCVar, ConfigVariable ///< Default value gotten from Config.setDefaults(). }; DENG2_DEFINE_AUDIENCE(ProfileChange, void currentProfileChanged(de::String const &name)) public: SettingsRegister(); /** * Sets the name this register will use for storing profiles persistently. * By default the register has no persistent name and thus will not be * stored persistently. * * In the Config, there will be a record called "Config.(persistentName)" * containing relevant information. * * @param name Persistent name for the register. Must be file name and * script variable name friendly. */ void setPersistentName(de::String const &name); /** * Defines a new setting in the profile. * * The default values of Config variables are taken from Config (so it must already * be initialized). * * @param type Type of setting. * @param settingName Name of the setting. * @param defaultValue Default value of the setting (for cvars). * * @return Reference to this instance. */ SettingsRegister &define(SettingType type, de::String const &settingName, QVariant const &defaultValue = QVariant()); de::String currentProfile() const; /** * Determines if a profile should be considered read-only. The UI should * not let the user modify profiles that are read-only. * * @param name Profile name. * * @return @c true, if the profile is read-only. */ bool isReadOnlyProfile(de::String const &name) const; /** * Current values of the settings are saved as a profile. If there is a * profile with the same name, it will be replaced. The current profile is * not changed. * * @param name Name of the profile. * * @return @c true if a new profile was created. @c false, if the operation * failed (e.g., name already in use). */ bool saveAsProfile(de::String const &name); /** * Changes the current settings profile. * * @param name Name of the profile to use. */ void setProfile(de::String const &name); /** * Resets the current profile to default values. */ void resetToDefaults(); /** * Resets one setting in the current profile to its default value. * * @param settingName Name of the setting. */ void resetSettingToDefaults(de::String const &settingName); /** * Renames the current profile. * * @param name New name of the profile. * * @return @c true, if renamed successfully. */ bool rename(de::String const &name); /** * Deletes a profile. The current profile cannot be deleted. * * @param name Name of the profile to delete. */ void deleteProfile(de::String const &name); /** * Lists the names of all the existing profiles. */ QList profiles() const; int profileCount() const; private: DENG2_PRIVATE(d) }; #endif // DENG_SETTINGSREGISTER_H doomsday-stable-1.15.7/doomsday/client/include/CompositeBitmapFont0000664000175000017500000000005212641367670024562 0ustar jaakkojaakko#include "resource/compositebitmapfont.h" doomsday-stable-1.15.7/doomsday/client/include/EntityDatabase0000664000175000017500000000004212641367670023534 0ustar jaakkojaakko#include "world/entitydatabase.h" doomsday-stable-1.15.7/doomsday/client/include/de_ui.h0000664000175000017500000000227612641367670022161 0ustar jaakkojaakko/** @file de_ui.h * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * User Interface Subsystem */ #ifndef LIBDENG_USER_INTERFACE_H #define LIBDENG_USER_INTERFACE_H #include "ui/infine/infinesystem.h" #ifdef __CLIENT__ # include # include "ui/clientwindowsystem.h" # include "ui/clientwindow.h" # include "ui/ui_main.h" #endif #endif /* LIBDENG_USER_INTERFACE_H */ doomsday-stable-1.15.7/doomsday/client/include/TextureVariantSpec0000664000175000017500000000005112641367670024433 0ustar jaakkojaakko#include "resource/texturevariantspec.h" doomsday-stable-1.15.7/doomsday/client/include/con_config.h0000664000175000017500000000266312641367670023200 0ustar jaakkojaakko/** @file con_config.h Config file IO. * @ingroup console * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CONSOLE_CONFIG_H #define DENG_CONSOLE_CONFIG_H #include "dd_share.h" #include // Flags for Con_ParseCommands: #define CPCF_SET_DEFAULT 0x1 #define CPCF_ALLOW_SAVE_STATE 0x2 #define CPCF_ALLOW_SAVE_BINDINGS 0x4 bool Con_ParseCommands(de::Path const &fileName, int flags = 0); /** * Saves all bindings, aliases and archiveable console variables. * The output file is a collection of console commands. */ void Con_SaveDefaults(); D_CMD(WriteConsole); #endif // DENG_CONSOLE_CONFIG_H doomsday-stable-1.15.7/doomsday/client/include/Sector0000664000175000017500000000003212641367670022071 0ustar jaakkojaakko#include "world/sector.h" doomsday-stable-1.15.7/doomsday/client/include/hedge.h0000664000175000017500000001264212641367670022146 0ustar jaakkojaakko/** @file hedge.h Mesh Geometry Half-Edge. * * @authors Copyright © 2011-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_DATA_MESH_HEDGE_H #define DENG_DATA_MESH_HEDGE_H #include #include #include "Mesh" #include "Vertex" namespace de { /** * Mesh half-edge geometry. * * @ingroup world */ class HEdge : public MeshElement { public: /// Required twin half-edge is missing. @ingroup errors DENG2_ERROR(MissingTwinError); /// Required face is missing. @ingroup errors DENG2_ERROR(MissingFaceError); /// Required neighbor half-edge is missing. @ingroup errors DENG2_ERROR(MissingNeighborError); public: HEdge(Mesh &mesh, Vertex &vertex); /** * Returns the vertex of the half-edge. */ Vertex &vertex() const; /** * Convenient accessor returning the origin coordinates for the vertex of * the half-edge. * * @see vertex() */ inline Vector2d const &origin() const { return vertex().origin(); } /** * Returns @c true iff a @em twin is linked to the half-edge. */ bool hasTwin() const; /** * Returns the linked @em twin of the half-edge. */ HEdge &twin() const; /** * Change the linked @em twin half-edge. * * @param newTwin New half-edge to attribute as the @em twin half-edge. * Ownership is unaffected. Can be @c 0 (to clear the * attribution). * * @see hasTwin(), twin() */ void setTwin(HEdge const *newTwin); /** * Returns @c true iff the half-edge is part of some Face geometry. */ bool hasFace() const; /** * Returns the Face geometry the half-edge is a part of. * * @see hasFace() */ Face &face() const; /** * Change the Face to which the half-edge is attributed. * * @param newFace New Face to attribute to the half-edge. Ownership is * unaffected. Can be @c 0 (to clear the attribution). * * @see hasFace(), face() */ void setFace(Face const *newFace); /** * Returns @c true iff the half-edge has a neighbor in the specifed direction * around the face of the polyon. */ bool hasNeighbor(ClockDirection direction) const; /** * Returns the neighbor half-edge in the specified @a direction around the * face of the polygon. * * @param direction Relative direction for desired neighbor half-edge. */ HEdge &neighbor(ClockDirection direction) const; /** * Change the neighbor half-edge in the specified @a direction around the * face of the polygon. * * @param direction Relative direction for the new neighbor half-edge. * @param newNeighbor Half-edge to attribute as the new neighbor. * Ownership is unaffected. * * @see hasNeighbor(), neighbor() */ void setNeighbor(ClockDirection direction, HEdge const *newNeighbor); /** * Returns @c true iff the half-edge has a next (clockwise) neighbor around * the face of the polygon. * * @see hasNeighbor() */ inline bool hasNext() const { return hasNeighbor(Clockwise); } /** * Returns the @em clockwise neighbor half-edge around the face of the * polygon. * * @see neighbor(), hasNext() */ inline HEdge &next() const { return neighbor(Clockwise); } /** * Change the HEdge attributed as the next (clockwise) neighbor of "this" * half-edge. * * @param newNext Half-edge to attribute as the new next (clockwise) * neighbor. Ownership is unaffected. * * @see setNeighbor(), next() */ inline void setNext(HEdge *newNext) { setNeighbor(Clockwise, newNext); } /** * Returns @c true iff the half-edge has a previous (anticlockwise) neighbor * around the face of the polygon. * * @see hasNeighbor() */ inline bool hasPrev() const { return hasNeighbor(Anticlockwise); } /** * Returns the @em anticlockwise neighbor half-edge around the face of the * polygon. * * @see neighbor(), hasPrev() */ inline HEdge &prev() const { return neighbor(Anticlockwise); } /** * Change the HEdge attributed as the next (clockwise) neighbor of "this" * half-edge. * * @param newPrev Half-edge to attribute as the new previous (anticlockwise) * neighbor. Ownership is unaffected. * * @see setNeighbor(), prev() */ inline void setPrev(HEdge *newPrev) { setNeighbor(Anticlockwise, newPrev); } private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_DATA_MESH_HEDGE_H doomsday-stable-1.15.7/doomsday/client/include/BiasSource0000664000175000017500000000003712641367670022676 0ustar jaakkojaakko#include "render/biassource.h" doomsday-stable-1.15.7/doomsday/client/include/TriangleStripBuilder0000664000175000017500000000005112641367670024731 0ustar jaakkojaakko#include "render/trianglestripbuilder.h" doomsday-stable-1.15.7/doomsday/client/include/template.h.template0000664000175000017500000000211312641367670024507 0ustar jaakkojaakko/** @file template.h.template Brief description of the header file. * @ingroup group * * Long summary of the functionality. * * @todo Update the fields above as appropriate. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDENG_ _H #define LIBDENG_ _H #endif // LIBDENG_ _H doomsday-stable-1.15.7/doomsday/client/include/de_misc.h0000664000175000017500000000233012641367670022466 0ustar jaakkojaakko/** @file de_misc.h * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDENG_MISC_H #define LIBDENG_MISC_H /** * Miscellaneous Services. */ #include "m_misc.h" #include "m_nodepile.h" #include "m_profiler.h" #include "m_decomp64.h" #include #include #include #include #include #include #endif /* LIBDENG_MISC_H */ doomsday-stable-1.15.7/doomsday/client/include/de_console.h0000664000175000017500000000246312641367670023204 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * Console Subsystem. */ #ifndef LIBDENG_CONSOLE #define LIBDENG_CONSOLE #ifdef WIN32 # include "de_platform.h" #endif #include #include #include #include #include "con_config.h" #ifdef __CLIENT__ # include "ui/progress.h" # include "ui/b_main.h" # include "BindContext" #endif #include "api_console.h" #endif /* LIBDENG_CONSOLE */ doomsday-stable-1.15.7/doomsday/client/include/Grabbable0000664000175000017500000000003512641367670022476 0ustar jaakkojaakko#include "world/grabbable.h" doomsday-stable-1.15.7/doomsday/client/include/SectorCluster0000664000175000017500000000004112641367670023433 0ustar jaakkojaakko#include "world/sectorcluster.h" doomsday-stable-1.15.7/doomsday/client/include/HueCircle0000664000175000017500000000003512641367670022500 0ustar jaakkojaakko#include "world/huecircle.h" doomsday-stable-1.15.7/doomsday/client/include/network/0000775000175000017500000000000012641367670022405 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/network/protocol.h0000664000175000017500000001660012641367670024422 0ustar jaakkojaakko/** * @file protocol.h * Network protocol. @ingroup network * * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_NETWORK_PROTOCOL_H #define LIBDENG_NETWORK_PROTOCOL_H /** * Server protocol version number. * @deprecated Will be replaced with the libcore serialization protocol version. */ #define SV_VERSION 24 // Prefer adding new flags inside the deltas instead of adding new delta types. typedef enum { DT_MOBJ = 0, DT_PLAYER = 1, //DT_SECTOR_R6 = 2, // 2 bytes for flags. DT_SIDE_SOUND = 3, DT_POLY = 4, DT_LUMP = 5, DT_SOUND = 6, // No emitter DT_MOBJ_SOUND = 7, DT_SECTOR_SOUND = 8, DT_POLY_SOUND = 9, DT_SECTOR = 10, // Flags in a packed long. // Special types: (only in the PSV_FRAME2 packet when written to message) DT_NULL_MOBJ = 11, // Mobj was removed (just type and ID). DT_CREATE_MOBJ = 12, // Regular DT_MOBJ, but the mobj was just created. DT_SIDE = 13, // Flags in a packed long. NUM_DELTA_TYPES } deltatype_t; // Mobj delta flags. These are used to determine what a delta contains. // (Which parts of a delta mobj_t are used.) #define MDF_ORIGIN_X 0x0001 #define MDF_ORIGIN_Y 0x0002 #define MDF_ORIGIN_Z 0x0004 #define MDF_ORIGIN 0x0007 #define MDF_MOM_X 0x0008 #define MDF_MOM_Y 0x0010 #define MDF_MOM_Z 0x0020 #define MDF_MOM 0x0038 #define MDF_ANGLE 0x0040 #define MDF_HEALTH 0x0080 #define MDF_MORE_FLAGS 0x0100 // A byte of extra flags follows. #define MDF_SELSPEC 0x0200 // Only during transfer. #define MDF_SELECTOR 0x0400 #define MDF_STATE 0x0800 #define MDF_RADIUS 0x1000 #define MDF_HEIGHT 0x2000 #define MDF_FLAGS 0x4000 #define MDF_FLOORCLIP 0x8000 #define MDF_EVERYTHING (MDF_ORIGIN | MDF_MOM | MDF_ANGLE | MDF_SELECTOR | MDF_STATE |\ MDF_RADIUS | MDF_HEIGHT | MDF_FLAGS | MDF_HEALTH | MDF_FLOORCLIP) // Extra flags for the Extra Flags byte. #define MDFE_FAST_MOM 0x01 // Momentum has 10.6 bits (+/- 512) #define MDFE_TRANSLUCENCY 0x02 #define MDFE_Z_FLOOR 0x04 // Mobj z is on the floor. #define MDFE_Z_CEILING 0x08 // Mobj z+hgt is in the ceiling. #define MDFE_FADETARGET 0x10 #define MDFE_TYPE 0x20 // Mobj type. // Player delta flags. #define PDF_MOBJ 0x0001 #define PDF_FORWARDMOVE 0x0002 #define PDF_SIDEMOVE 0x0004 #define PDF_ANGLE 0x0008 #define PDF_TURNDELTA 0x0010 #define PDF_FRICTION 0x0020 #define PDF_EXTRALIGHT 0x0040 // Plus fixedcolormap (same byte). #define PDF_FILTER 0x0080 //#define PDF_CLYAW 0x1000 // Sent in the player num byte. //#define PDF_CLPITCH 0x2000 // Sent in the player num byte. #define PDF_PSPRITES 0x4000 // Sent in the player num byte. // Written separately, stored in playerdelta flags 2 highest bytes. #define PSDF_STATEPTR 0x01 #define PSDF_OFFSET 0x08 #define PSDF_LIGHT 0x20 #define PSDF_ALPHA 0x40 #define PSDF_STATE 0x80 #define SDF_FLOOR_MATERIAL 0x00000001 #define SDF_CEILING_MATERIAL 0x00000002 #define SDF_LIGHT 0x00000004 #define SDF_FLOOR_TARGET 0x00000008 #define SDF_FLOOR_SPEED 0x00000010 #define SDF_CEILING_TARGET 0x00000020 #define SDF_CEILING_SPEED 0x00000040 #define SDF_FLOOR_TEXMOVE 0x00000080 //#define SDF_CEILING_TEXMOVE 0x00000100 // obsolete #define SDF_COLOR_RED 0x00000200 #define SDF_COLOR_GREEN 0x00000400 #define SDF_COLOR_BLUE 0x00000800 #define SDF_FLOOR_SPEED_44 0x00001000 // Used for sent deltas. #define SDF_CEILING_SPEED_44 0x00002000 // Used for sent deltas. #define SDF_FLOOR_HEIGHT 0x00004000 #define SDF_CEILING_HEIGHT 0x00008000 #define SDF_FLOOR_COLOR_RED 0x00010000 #define SDF_FLOOR_COLOR_GREEN 0x00020000 #define SDF_FLOOR_COLOR_BLUE 0x00040000 #define SDF_CEIL_COLOR_RED 0x00080000 #define SDF_CEIL_COLOR_GREEN 0x00100000 #define SDF_CEIL_COLOR_BLUE 0x00200000 #define SIDF_TOP_MATERIAL 0x0001 #define SIDF_MID_MATERIAL 0x0002 #define SIDF_BOTTOM_MATERIAL 0x0004 #define SIDF_LINE_FLAGS 0x0008 #define SIDF_TOP_COLOR_RED 0x0010 #define SIDF_TOP_COLOR_GREEN 0x0020 #define SIDF_TOP_COLOR_BLUE 0x0040 #define SIDF_MID_COLOR_RED 0x0080 #define SIDF_MID_COLOR_GREEN 0x0100 #define SIDF_MID_COLOR_BLUE 0x0200 #define SIDF_MID_COLOR_ALPHA 0x0400 #define SIDF_BOTTOM_COLOR_RED 0x0800 #define SIDF_BOTTOM_COLOR_GREEN 0x1000 #define SIDF_BOTTOM_COLOR_BLUE 0x2000 #define SIDF_MID_BLENDMODE 0x4000 #define SIDF_FLAGS 0x8000 #define PODF_DEST_X 0x01 #define PODF_DEST_Y 0x02 #define PODF_SPEED 0x04 #define PODF_DEST_ANGLE 0x08 #define PODF_ANGSPEED 0x10 #define PODF_PERPETUAL_ROTATE 0x20 // Special flag. #define LDF_INFO 0x01 // Sound delta flags. #define SNDDF_VOLUME 0x01 // 0=stop, 1=full, >1=no att. #define SNDDF_REPEAT 0x02 // Start repeating sound. #define SNDDF_PLANE_FLOOR 0x04 // Play sound from a sector's floor. #define SNDDF_PLANE_CEILING 0x08 // Play sound from a sector's ceiling. #define SNDDF_SIDE_TOP 0x10 // Play sound from a side's top part. #define SNDDF_SIDE_MIDDLE 0x20 // Play sound from a side's middle part. #define SNDDF_SIDE_BOTTOM 0x40 // Play sound from a side's bottom part. /** * @defgroup soundPacketFlags Sound Packet Flags * Used with PSV_SOUND packets. * @ingroup flags */ ///@{ #define SNDF_ORIGIN 0x01 ///< Sound has an origin. #define SNDF_SECTOR 0x02 ///< Originates from a degenmobj. #define SNDF_PLAYER 0x04 ///< Originates from a player. #define SNDF_VOLUME 0x08 ///< Volume included. #define SNDF_ID 0x10 ///< Mobj ID of the origin. #define SNDF_REPEATING 0x20 ///< Repeat sound indefinitely. #define SNDF_SHORT_SOUND_ID 0x40 ///< Sound ID is a short. ///@} /** * @defgroup stopSoundPacketFlags Stop Sound Packet Flags * Used with PSV_STOP_SOUND packets. * @ingroup flags */ ///@{ #define STOPSNDF_SOUND_ID 0x01 #define STOPSNDF_ID 0x02 #define STOPSNDF_SECTOR 0x04 ///@} #ifdef __cplusplus extern "C" { #endif /// Largest message sendable using the protocol. #define PROTOCOL_MAX_DATAGRAM_SIZE (1 << 22) // 4 MB #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_NETWORK_PROTOCOL_H doomsday-stable-1.15.7/doomsday/client/include/network/net_buf.h0000664000175000017500000000605712641367670024210 0ustar jaakkojaakko/** * @file net_buf.h * Network message handling and buffering. @ingroup network * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_NETWORK_BUFFER_H #define LIBDENG_NETWORK_BUFFER_H #include "dd_types.h" #ifdef __cplusplus extern "C" { #endif // Send Packet flags: #define SPF_REBOUND 0x00020000 // Write only to local loopback #define SPF_DONT_SEND 0x00040000 // Don't really send out anything #define NETBUFFER_MAXSIZE 0x7ffff // 512 KB // Incoming messages are stored in netmessage_s structs. typedef struct netmessage_s { struct netmessage_s *next; nodeid_t sender; uint player; // Set in N_GetMessage(). size_t size; byte *data; void *handle; double receivedAt; // Time when received (seconds). } netmessage_t; #pragma pack(1) typedef struct { byte type; // Type of the message. byte data[NETBUFFER_MAXSIZE]; } netdata_t; #pragma pack() typedef struct netbuffer_s { int player; // Recipient or sender. size_t length; // Number of bytes in the data buffer. size_t headerLength; // 1 byte at the moment. netdata_t msg; // The data buffer for sending and // receiving packets. } netbuffer_t; /** * Globally accessible data. */ extern netbuffer_t netBuffer; extern dd_bool allowSending; /** * Constructs a new reader. The reader will use the engine's netBuffer * as the reading buffer. The reader has to be destroyed with Reader_Delete() * after it is not needed any more. */ Reader* Reader_NewWithNetworkBuffer(void); /** * Functions. */ void N_Init(void); void N_Shutdown(void); void N_ClearMessages(void); void N_SendPacket(int flags); dd_bool N_GetPacket(void); int N_IdentifyPlayer(nodeid_t id); void N_PrintBufferInfo(void); void N_PrintTransmissionStats(void); void N_PostMessage(netmessage_t *msg); void N_AddSentBytes(size_t bytes); #ifdef __cplusplus } // extern "C" #endif #endif /* LIBDENG_NETWORK_BUFFER_H */ doomsday-stable-1.15.7/doomsday/client/include/network/net_event.h0000664000175000017500000000310512641367670024544 0ustar jaakkojaakko/** @file * * @authors Copyright © 2004-2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301 USA */ /* * net_event.h: Network Events */ #ifndef __DOOMSDAY_NETWORK_EVENT_H__ #define __DOOMSDAY_NETWORK_EVENT_H__ #include "net_main.h" // Net events. typedef enum neteventtype_e { NE_CLIENT_ENTRY, NE_CLIENT_EXIT, //NE_END_CONNECTION //NE_TERMINATE_NODE } neteventtype_t; typedef struct netevent_s { neteventtype_t type; nodeid_t id; } netevent_t; #ifdef __cplusplus extern "C" { #endif void N_MAPost(masteraction_t act); dd_bool N_MADone(void); void N_MAClear(void); void N_NEPost(netevent_t * nev); dd_bool N_NEPending(void); void N_NETicker(timespan_t time); void N_TerminateClient(int console); void N_Update(void); #ifdef __cplusplus } // extern "C" #endif #endif doomsday-stable-1.15.7/doomsday/client/include/network/net_msg.h0000664000175000017500000000303112641367670024207 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * net_msg.h: Network Messaging */ #ifndef __DOOMSDAY_NETMESSAGE_H__ #define __DOOMSDAY_NETMESSAGE_H__ #include #include DENG_EXTERN_C Writer* msgWriter; DENG_EXTERN_C Reader* msgReader; #ifdef __cplusplus extern "C" { #endif /** * Begin writing a message to netBuffer. If a message is currently being * read, the reading will be ended. */ void Msg_Begin(int type); void Msg_End(void); dd_bool Msg_BeingWritten(void); /** * Begin reading a message from netBuffer. If a message is currently being * written, the writing will be ended. */ void Msg_BeginRead(void); void Msg_EndRead(void); #ifdef __cplusplus } // extern "C" #endif #endif doomsday-stable-1.15.7/doomsday/client/include/network/sys_network.h0000664000175000017500000000315512641367670025151 0ustar jaakkojaakko/** * @file sys_network.h * Low-level network socket routines. @ingroup network * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_SYSTEM_NETWORK_H #define LIBDENG_SYSTEM_NETWORK_H #ifndef __CLIENT__ # error "sys_network.h requires __CLIENT__" #endif #include "dd_share.h" #include "net_buf.h" #include "net_main.h" #include "monitor.h" #include "serverlink.h" extern dd_bool allowSending; extern int maxQueuePackets; extern char *nptIPAddress; extern int nptIPPort; ServerLink &Net_ServerLink(void); void N_Register(void); void N_PrintInfo(void); int N_GetHostCount(void); dd_bool N_GetHostInfo(int index, struct serverinfo_s *info); void N_PrintNetworkStatus(void); #endif /* LIBDENG_SYSTEM_NETWORK_H */ doomsday-stable-1.15.7/doomsday/client/include/network/serverlink.h0000664000175000017500000000663012641367670024747 0ustar jaakkojaakko/** @file serverlink.h Network connection to a server. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_SERVERLINK_H #define CLIENT_SERVERLINK_H #include #include #include #include #include #include "network/net_main.h" /** * Network connection to a server. * @ingroup network */ class ServerLink : public de::shell::AbstractLink { Q_OBJECT public: DENG2_DEFINE_AUDIENCE(DiscoveryUpdate, void linkDiscoveryUpdate(ServerLink const &link)) DENG2_DEFINE_AUDIENCE(Join, void networkGameJoined()) DENG2_DEFINE_AUDIENCE(Leave, void networkGameLeft()) public: ServerLink(); void clear(); void connectDomain(de::String const &domain, de::TimeDelta const &timeout = 0); void connectHost(de::Address const &address); /** * Disconnect from the server. */ void disconnect(); /** * Attempts to connect to the specified address and asks for server * information if one happens to be running. * * @param domain */ void discover(de::String const &domain); /** * Asks the master server for information about currently running servers. */ void discoverUsingMaster(); bool isDiscovering() const; enum FoundMaskFlag { Direct = 0x1, LocalNetwork = 0x2, MasterServer = 0x4, Any = Direct | LocalNetwork | MasterServer }; Q_DECLARE_FLAGS(FoundMask, FoundMaskFlag) /** * @param mask Defines the sources that are enabled when querying for found servers. */ int foundServerCount(FoundMask mask = Any) const; /** * @param mask Defines the sources that are enabled when querying for found servers. */ QList foundServers(FoundMask mask = Any) const; bool isFound(de::Address const &host, FoundMask mask = Any) const; /** * @param mask Defines the sources that are enabled when querying for found servers. */ bool foundServerInfo(de::Address const &host, serverinfo_t *info, FoundMask mask = Any) const; /** * @param mask Defines the sources that are enabled when querying for found servers. */ bool foundServerInfo(int index, serverinfo_t *info, FoundMask mask = Any) const; signals: void serversDiscovered(); public slots: void handleIncomingPackets(); protected slots: void localServersFound(); void linkDisconnected(); protected: de::Packet *interpret(de::Message const &msg); void initiateCommunications(); private: DENG2_PRIVATE(d) }; Q_DECLARE_OPERATORS_FOR_FLAGS(ServerLink::FoundMask) #endif // CLIENT_LINK_H doomsday-stable-1.15.7/doomsday/client/include/network/net_demo.h0000664000175000017500000000340612641367670024353 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * Demos. */ #ifndef LIBDENG_DEMO_H #define LIBDENG_DEMO_H #ifdef __SERVER__ # error Demos are not available in a SERVER build #endif #ifdef __cplusplus extern "C" { #endif extern int playback; void Demo_Register(void); void Demo_Init(void); void Demo_Ticker(timespan_t time); dd_bool Demo_BeginRecording(const char* filename, int playernum); void Demo_StopRecording(int playernum); void Demo_PauseRecording(int playernum); void Demo_ResumeRecording(int playernum); void Demo_WritePacket(int playernum); void Demo_BroadcastPacket(void); void Demo_ReadLocalCamera(void); // PKT_DEMOCAM dd_bool Demo_BeginPlayback(const char* filename); dd_bool Demo_ReadPacket(void); void Demo_StopPlayback(void); #ifdef __cplusplus } // extern "C" #endif #endif /* LIBDENG_DEMO_H */ doomsday-stable-1.15.7/doomsday/client/include/network/masterserver.h0000664000175000017500000000553212641367670025305 0ustar jaakkojaakko/** * @file masterserver.h * Communication with the Master Server. @ingroup network * * The master server maintains a real-time list of running public servers. * * @authors Copyright © 2003-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_MASTER_SERVER_H #define LIBDENG_MASTER_SERVER_H #include "dd_share.h" #include #include #include #include /** * Network request worker for communicating with the master server. * @ingroup network */ class MasterWorker : public QObject { Q_OBJECT public: // Actions for the master worker. enum Action { NONE, REQUEST_SERVERS, ANNOUNCE }; public: MasterWorker(); void newJob(Action action, void* data); bool isAllDone() const; bool isOngoing() const; int serverCount() const; serverinfo_t server(int index) const; protected: void nextJob(); bool parseResponse(const QByteArray& response); public slots: void requestFinished(QNetworkReply* reply); private: DENG2_PRIVATE(d) }; /** * Called while initializing the low-level network subsystem. */ void N_MasterInit(void); /** * Called during engine shutdown. */ void N_MasterShutdown(void); /** * Sends a server announcement to the master. The announcement includes our * IP address and other information. * * @param isOpen If @c true, then the server will be * visible on the server list for other clients to * find by querying the server list. */ void N_MasterAnnounceServer(dd_bool isOpen); /** * Requests the list of open servers from the master. */ void N_MasterRequestList(void); /** * Returns information about the server @em N. * * @return @c 0, if communication with the master is currently in progress. If * param info is @c NULL, will return the number of known servers ELSE, will * return @c not zero, if param index was valid and the master returned info on * the requested server. */ int N_MasterGet(int index, serverinfo_t *info); #endif // LIBDENG_MASTER_SERVER_H doomsday-stable-1.15.7/doomsday/client/include/network/net_main.h0000664000175000017500000002103312641367670024347 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * Network Subsystem. */ #ifndef LIBDENG_NETWORK_H #define LIBDENG_NETWORK_H #include #include "lzss.h" #include "dd_share.h" #include "net_msg.h" #include #include #ifdef __cplusplus extern "C" { #endif #define BIT(x) (1 << (x)) #define NSP_BROADCAST -1 // For Net_SendBuffer. // Flags for console text from the server. // Change with server version? #define SV_CONSOLE_PRINT_FLAGS (CPF_WHITE|CPF_LIGHT|CPF_GREEN) #define PING_TIMEOUT 1000 // Ping timeout (ms). #define MAX_PINGS 10 // The default bandwidth rating for new clients. #define BWR_DEFAULT 40 // A modest acktime used by default for new clients (1 sec ping). #define ACK_DEFAULT 1000 #define MONITORTICS 7 #define LOCALTICS 10 // Built ticcmds are stored here. #define BACKUPTICS 70 // Two seconds worth of tics. // The number of mobjs that can be stored in the input/visible buffer. // The server won't send more mobjs than this. #define MAX_CLMOBJS 80 #define DEFAULT_TCP_PORT 13209 #define DEFAULT_UDP_PORT 13209 typedef void (*expectedresponder_t)(int, const byte*, int); // If a master action fails, the action queue is emptied. typedef enum { MAC_REQUEST, // Retrieve the list of servers from the master. MAC_WAIT, // Wait for the server list to arrive. MAC_LIST // Print the server list in the console. } masteraction_t; // Packet types. // PKT = sent by anyone // PSV = only sent by server // PCL = only sent by client enum { // Messages and responses. PCL_HELLO = 0, PKT_OK = 1, PKT_CANCEL = 2, // unused? PKT_PLAYER_INFO = 3, PKT_CHAT = 4, PSV_FINALE = 5, PKT_PING = 6, PSV_HANDSHAKE = 7, PSV_SERVER_CLOSE = 8, PSV_FRAME = 9, // obsolete PSV_PLAYER_EXIT = 10, PSV_CONSOLE_TEXT = 11, PCL_ACK_SHAKE = 12, PSV_SYNC = 13, PSV_MATERIAL_ARCHIVE = 14, PCL_FINALE_REQUEST = 15, PKT_LOGIN = 16, PCL_ACK_SETS = 17, PKT_COORDS = 18, PKT_DEMOCAM = 19, PKT_DEMOCAM_RESUME = 20, PCL_HELLO2 = 21, // Includes game ID PSV_FRAME2 = 22, // Frame packet v2 PSV_FIRST_FRAME2 = 23, // First PSV_FRAME2 after map change PSV_SOUND2 = 24, // unused? PSV_STOP_SOUND = 25, PCL_ACKS = 26, PSV_PLAYER_FIX_OBSOLETE = 27, // Fix angles/pos/mom (without console number). PCL_ACK_PLAYER_FIX = 28, // Acknowledge player fix. /* 28 */ PKT_COMMAND2 = 29, PSV_PLAYER_FIX = 30, // Fix angles/pos/mom. PCL_GOODBYE = 31, PSV_MOBJ_TYPE_ID_LIST = 32, PSV_MOBJ_STATE_ID_LIST = 33, // Game specific events. PKT_GAME_MARKER = DDPT_FIRST_GAME_EVENT, // 64 }; // Use the number defined in dd_share.h for sound packets. // This is for backwards compatibility. #define PSV_SOUND 71 /* DDPT_SOUND */ #define RESENDCOUNT 10 #define HANDSHAKECOUNT 17 //#define UPDATECOUNT 20 // These dd-flags are packed (i.e. included in mobj deltas). #define DDMF_PACK_MASK 0x3cfff1ff // A client's acknowledgement threshold depends on the average of his // acknowledgement times. #define NUM_ACK_TIMES 8 // The consolePlayer's camera position is written to the demo file // every 3rd tic. #define LOCALCAM_WRITE_TICS 3 // Maximum length of a token in the textual representation of // serverinfo. #define SVINFO_TOKEN_LEN 128 #define SVINFO_VALID_LABEL_LEN 16 typedef struct { // High tics when ping was sent (0 if pinger not used). int sent; // A record of the pings (negative time: no response). float times[MAX_PINGS]; // Total number of pings and the current one. int total; int current; } pinger_t; // Network information for a player. Corresponds the players array. typedef struct { // ID number. Each client has a unique ID number. ident_t id; int lastTransmit; // Seconds when the client entered the game (Sys_GetRealSeconds()). double enterTime; // Bandwidth rating for connection. Determines how much information // can be sent to the client. Determined dynamically. int bandwidthRating; // Clients use this to determine how long ago they received the // last update of this client. int age; // Is this client connected? (Might not be in the game yet.) Only // used by the server. int connected; // Clients are pinged by the server when they join the game. // This is the ping in milliseconds for this client. For the server. unsigned int shakePing; // If true, the server will send the player a handshake. The client must // acknowledge it before this flag is turned off. int handshake; // Server uses this to determine whether it's OK to send game packets // to the client. int ready; // The name of the player. char name[PLAYERNAMELEN]; // Field of view. Used in determining visible mobjs (default: 90). float fov; // The DirectPlay player that represents this client. unsigned int nodeID; // DP player ID. // Ping tracker for this client. pinger_t ping; // Demo recording file (being recorded if not NULL). LZFILE *demo; dd_bool recording; dd_bool recordPaused; // Movement smoother. Smoother* smoother; // View console. Which player this client is viewing? int viewConsole; } client_t; extern char *serverName, *serverInfo, *playerName; extern int serverData[]; extern dd_bool firstNetUpdate; extern int resendStart; // set when server needs our tics extern int resendCount; extern int oldEnterTics; extern int numClMobjs; extern dd_bool masterAware; extern int netGame; extern int realTics, availableTics; extern int isServer, isClient; extern dd_bool allowNetTraffic; // Should net traffic be allowed? extern float netSimulatedLatencySeconds; extern client_t clients[DDMAXPLAYERS]; extern int gotFrame; void Net_Register(void); void Net_Init(void); void Net_Shutdown(void); void Net_DestroyArrays(void); void Net_AllocClientBuffers(int clientId); dd_bool Net_GetPacket(void); void Net_SendBuffer(int to_player, int sp_flags); void Net_SendPlayerInfo(int srcPlrNum, int destPlrNum); void Net_InitGame(void); void Net_StartGame(void); void Net_StopGame(void); void Net_SendPing(int player, int count); void Net_PingResponse(void); void Net_ShowPingSummary(int player); void Net_WriteChatMessage(int from, int toMask, const char* message); void Net_ShowChatMessage(int plrNum, const char* message); int Net_TimeDelta(byte now, byte then); void Net_Update(void); void Net_ResetTimer(void); void Net_Ticker(timespan_t time); /** * Does drawing for the engine's HUD, not just the net. */ void Net_Drawer(void); dd_bool Net_IsLocalPlayer(int pNum); void ServerInfo_Print(serverinfo_t const *info, int index); /** * Converts textual data to a serverinfo struct. Returns true if the * label/value pair is recognized. */ dd_bool ServerInfo_FromString(serverinfo_t *info, char const *valuePair); void ServerInfo_FromRecord(serverinfo_t *info, de::Record const &rec); #ifdef __cplusplus } // extern "C" #endif de::String ServerInfo_AsStyledText(serverinfo_t const *sv); de::String Net_UserAgent(); #endif /* LIBDENG_NETWORK_H */ doomsday-stable-1.15.7/doomsday/client/include/network/monitor.h0000664000175000017500000000257612641367670024257 0ustar jaakkojaakko/** * @file monitor.h * Network traffic monitoring. @ingroup network * * Utilities for monitoring network traffic for development and debugging * purposes. * * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_NETWORK_MONITOR_H #define LIBDENG_NETWORK_MONITOR_H #include "dd_share.h" #ifdef _DEBUG /** * Updates monitored byte frequency counts with @a bytes. * * @param bytes Buffer whose contents to count. * @param size Size of the buffer. */ void Monitor_Add(const uint8_t* bytes, size_t size); D_CMD(NetFreqs); #endif #endif // LIBDENG_NETWORK_MONITOR_H doomsday-stable-1.15.7/doomsday/client/include/def_main.h0000664000175000017500000001544212641367670022635 0ustar jaakkojaakko/** @file def_main.h Definition subsystem. * * @ingroup defs * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_DEFINITIONS_MAIN_H #define LIBDENG_DEFINITIONS_MAIN_H #include #include #include #include #include "Material" template struct Array : public std::vector { Array() : _elements(0) {} bool isEmpty() const { return !size(); } int size() const { return std::vector::size(); } void clear() { _elements = nullptr; std::vector::clear(); } PODType *append(int count = 1) { DENG2_ASSERT(count >= 0); for(int i = 0; i < count; ++i) { std::vector::push_back(PODType()); } if(!isEmpty()) { _elements = &(*this)[0]; return &_elements[size() - count]; } return nullptr; } /// Determine the index of element @a elem. Performance is O(1). int indexOf(PODType const *elem) const { if(!elem) return 0; int index = elem - elements(); DENG_ASSERT(index >= 0 && index < size()); //if(index < 0 || index >= size()) return 0; return index; } PODType *elements() { return _elements; } PODType const *elements() const { return _elements; } PODType **elementsPtr() { return &_elements; } private: PODType *_elements; }; #ifdef __cplusplus extern "C" { #endif // the actual classes are game-side struct xgclass_s; struct sfxinfo_t { void *data; ///< Pointer to sound data. lumpnum_t lumpNum; char lumpName[9]; ///< Actual lump name of the sound (full name). char id[32]; ///< Identifier name (from the def). char name[32]; ///< Long name. sfxinfo_t *link; ///< Link to another sound. int linkPitch; int linkVolume; int priority; int channels; ///< Max. channels for the sound to occupy. int usefulness; ///< Used to determine when to cache out. int flags; int group; ddstring_t external; ///< Path to external file. }; extern ded_t defs; ///< Main definitions database (internal). struct stateinfo_t { mobjinfo_t *owner; ded_light_t *light; ded_ptcgen_t *ptcGens; }; /** * Definitions that have been preprocessed for runtime use. Some of these are * visible to the games via the InternalData API. */ struct RuntimeDefs { Array sprNames; ///< Sprite name list. Array mobjInfo; ///< Map object info database. Array states; ///< State list. Array stateInfo; Array sounds; ///< Sound effect list. Array texts; ///< Text string list. void clear(); }; extern RuntimeDefs runtimeDefs; /** * Initializes the definition databases. */ void Def_Init(void); /** * Retrieves the XG Class list from the Game. * XGFunc links are provided by the Game, who owns the actual * XG classes and their functions. */ int Def_GetGameClasses(void); /** * Finish definition database initialization. Initialization is split into two * phases either side of the texture manager, this being the post-phase. */ void Def_PostInit(void); /** * Destroy databases. */ void Def_Destroy(void); /** * Reads the specified definition files, and creates the sprite name, * state, mobjinfo, sound, music, text and mapinfo databases accordingly. */ void Def_Read(void); int Def_GetMobjNum(char const *id); int Def_GetMobjNumForName(char const *name); char const *Def_GetMobjName(int num); state_t *Def_GetState(int num); int Def_GetStateNum(char const *id); char const *Def_GetStateName(state_t *state); int Def_GetActionNum(char const *id); /** * Returns the unique sprite number associated with the specified sprite @a name; * otherwise @c -1 if not found. */ spritenum_t Def_GetSpriteNum(char const *name); int Def_GetModelNum(char const *id); int Def_GetMusicNum(char const *id); int Def_GetSoundNum(char const *id); ded_value_t *Def_GetValueById(char const *id); ded_value_t *Def_GetValueByUri(Uri const *uri); ded_compositefont_t *Def_GetCompositeFont(char const *uri); ded_light_t *Def_GetLightDef(int spr, int frame); #ifdef __cplusplus } // extern "C" #endif spritenum_t Def_GetSpriteNum(de::String const &name); ded_ptcgen_t *Def_GetGenerator(Uri const *uri); ded_ptcgen_t *Def_GetGenerator(de::Uri const &uri); #ifdef __cplusplus extern "C" { #endif ded_ptcgen_t *Def_GetDamageGenerator(int mobjType); int Def_EvalFlags(char const *string); /** * @return @c true= the definition was found. */ int Def_Get(int type, char const *id, void *out); /** * This is supposed to be the main interface for outside parties to * modify definitions (unless they want to do it manually with dedfile.h). */ int Def_Set(int type, int index, int value, void const *ptr); /** * Can we reach 'snew' if we start searching from 'sold'? * Take a maximum of 16 steps. */ dd_bool Def_SameStateSequence(state_t *snew, state_t *sold); /** * Compiles a list of all the defined mobj types. Indices in this list * match those in the @c mobjInfo array. * * @return StringArray instance. Caller gets ownership. */ StringArray *Def_ListMobjTypeIDs(void); /** * Compiles a list of all the defined mobj states. Indices in this list * match those in the @c states array. * * @return StringArray instance. Caller gets ownership. */ StringArray *Def_ListStateIDs(void); /** * Returns @c true iff @a def is compatible with the specified context. */ bool Def_IsAllowedReflection(ded_reflection_t const *def, /*bool hasExternal,*/ bool isCustom); /** * Returns @c true iff @a def is compatible with the specified context. */ bool Def_IsAllowedDetailTex(ded_detailtexture_t const *def, /*bool hasExternal,*/ bool isCustom); D_CMD(ListMobjs); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_DEFINITIONS_MAIN_H doomsday-stable-1.15.7/doomsday/client/include/Hand0000664000175000017500000000003012641367670021502 0ustar jaakkojaakko#include "world/hand.h" doomsday-stable-1.15.7/doomsday/client/include/ihplane.h0000664000175000017500000000755112641367670022515 0ustar jaakkojaakko/** @file ihplane.h Interface for a geometric half-plane. * * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_MATH_IHPLANE #define DENG_MATH_IHPLANE #include #include #include "partition.h" namespace de { /** * Interface for an interceptable geometric half-plane, which provides direct * access to the data/class instance used to model an intersection point. */ class IHPlane { public: /// An invalid reference to an intercept was specified. @ingroup errors DENG2_ERROR(UnknownInterceptError); /** * Interface for an intercept in the implementing half-plane. */ class IIntercept { public: IIntercept(ddouble distance) : _distance(distance) {} virtual ~IIntercept() {} /** * Determines the distance between "this" and the @a other intercept * along the half-plane. The default implementation simply subtracts * the other distance from that of "this". */ virtual double operator - (IIntercept const &other) const { return distance() - other.distance(); } /** * Determines whether the distance relative to the half-plane origin * for "this" intercept is logically less than that of @a other. The * default implementation simply compares the distance values. */ virtual bool operator < (IIntercept const &other) const { return distance() < other.distance(); } /** * Returns distance along the half-plane relative to the origin. * Implementors may override this for special functionality. */ virtual ddouble distance() const { return _distance; } protected: ddouble _distance; }; public: virtual ~IHPlane() {} /** * Reconfigure the half-plane according to the given Partition line. * * @param newPartition The "new" partition line to configure using. */ virtual void configure(Partition const &newPartition) = 0; /** * Returns the Partition (immutable) used to model the partitioning line * of the half-plane. */ virtual Partition const &partition() const = 0; /** * Clear the list of intercept "points" for the half-plane. */ virtual void clearIntercepts() = 0; /** * Attempt interception of the half-plane at @a distance from the origin. * * @param distance Distance along the half-plane to intersect. * * @return Resultant intercept if intersection occurs. Otherwise @c 0. */ virtual IIntercept const *intercept(ddouble distance) = 0; /** * Returns the total number of half-plane intercept points. */ virtual int interceptCount() const = 0; /** * Prepare the list of intercepts for search queries. */ virtual void sortAndMergeIntercepts() {} /** * @note Implementors are obligated to throw UnknownInterceptError if the * specified @a index is not valid. */ virtual IIntercept const &at(int index) const = 0; }; } // namespace de #endif // DENG_MATH_IHPLANE doomsday-stable-1.15.7/doomsday/client/include/ImpulseBinding0000664000175000017500000000003712641367670023550 0ustar jaakkojaakko#include "ui/impulsebinding.h" doomsday-stable-1.15.7/doomsday/client/include/MaterialContext0000664000175000017500000000004412641367670023740 0ustar jaakkojaakko#include "render/materialcontext.h" doomsday-stable-1.15.7/doomsday/client/include/Polyobj0000664000175000017500000000003312641367670022251 0ustar jaakkojaakko#include "world/polyobj.h" doomsday-stable-1.15.7/doomsday/client/include/ContactSpreader0000664000175000017500000000004312641367670023715 0ustar jaakkojaakko#include "world/contactspreader.h" doomsday-stable-1.15.7/doomsday/client/include/SurfaceDecorator0000664000175000017500000000004512641367670024071 0ustar jaakkojaakko#include "render/surfacedecorator.h" doomsday-stable-1.15.7/doomsday/client/include/de_system.h0000664000175000017500000000236712641367670023071 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDENG_SYSTEM_H #define LIBDENG_SYSTEM_H #include #include #include "sys_system.h" #include "network/masterserver.h" #ifdef __CLIENT__ # include "network/sys_network.h" # include "gl/sys_opengl.h" # include "ui/sys_input.h" # include "ui/clientwindow.h" #endif #ifdef __SERVER__ # include "serversystem.h" #endif #endif /* LIBDENG_SYSTEM_H */ doomsday-stable-1.15.7/doomsday/client/include/IHPlane0000664000175000017500000000002512641367670022114 0ustar jaakkojaakko#include "ihplane.h" doomsday-stable-1.15.7/doomsday/client/include/resource/0000775000175000017500000000000012641367670022543 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/resource/hq2x.h0000664000175000017500000000366212641367670023605 0ustar jaakkojaakko/** * @file hq2x.h High-Quality 2x Graphics Resizing. * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDENG_RESOURCE_HQ2X_H #define LIBDENG_RESOURCE_HQ2X_H #ifdef __cplusplus extern "C" { #endif /// @addtogroup resource ///@{ /** * Initialize the lookup tables used in the hq2x algorithm. */ void GL_InitSmartFilterHQ2x(void); /** * Upscales an image to 2x the original size using an intelligent scaling * algorithm that avoids blurriness. * * Based on the routine by Maxim Stepin * For more information, see: http://hiend3d.com/hq2x.html * * Uses 32-bit data and our native ABGR8888 pixel format. * Alpha is taken into account in the processing to preserve edges. * (Not quite as efficient as the original version.) * * @param src R8G8B8A8 source image to be scaled. * @param width Width of the source image in pixels. * @param height Height of the source image in pixels. * @param flags @ref imageConversionFlags */ uint8_t *GL_SmartFilterHQ2x(uint8_t const *src, int width, int height, int flags); ///@} #ifdef __cplusplus } // extern "C" #endif #endif /* LIBDENG_RESOURCE_HQ2X_H */ doomsday-stable-1.15.7/doomsday/client/include/resource/materialmanifest.h0000664000175000017500000001213612641367670026244 0ustar jaakkojaakko/** @file materialmanifest.h Description of a logical material resource. * * @authors Copyright © 2011-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_MATERIALMANIFEST_H #define DENG_RESOURCE_MATERIALMANIFEST_H #include #include #include #include #include #include "Material" namespace de { class MaterialScheme; } /** * Description for a would-be logical Material resource. * * Models a reference to and the associated metadata for a logical material * in the material resource collection. * * @see MaterialScheme, Material * @ingroup resource */ class MaterialManifest : public de::PathTree::Node { public: /// Required material instance is missing. @ingroup errors DENG2_ERROR(MissingMaterialError); DENG2_DEFINE_AUDIENCE(Deletion, void materialManifestBeingDeleted(MaterialManifest const &manifest)) DENG2_DEFINE_AUDIENCE(MaterialDerived, void materialManifestMaterialDerived(MaterialManifest &manifest, Material &material)) enum Flag { /// The manifest was automatically produced for a game/add-on resource. AutoGenerated, /// The manifest was not produced for an original game resource. Custom }; Q_DECLARE_FLAGS(Flags, Flag) public: MaterialManifest(de::PathTree::NodeArgs const &args); ~MaterialManifest(); /** * Derive a new logical Material instance by interpreting the manifest. * The first time a material is derived from the manifest, said material * is assigned to the manifest (ownership is assumed). */ Material *derive(); /** * Returns the owning scheme of the manifest. */ de::MaterialScheme &scheme() const; /// Convenience method for returning the name of the owning scheme. de::String const &schemeName() const; /** * Compose a URI of the form "scheme:path" for the material manifest. * * The scheme component of the URI will contain the symbolic name of * the scheme for the material manifest. * * The path component of the URI will contain the percent-encoded path * of the material manifest. */ inline de::Uri composeUri(QChar sep = '/') const { return de::Uri(schemeName(), path(sep)); } /** * Returns a textual description of the manifest. * * @return Human-friendly description the manifest. */ de::String description(de::Uri::ComposeAsTextFlags uriCompositionFlags = de::Uri::DefaultComposeAsTextFlags) const; /** * Returns a textual description of the source of the manifest. * * @return Human-friendly description of the source of the manifest. */ de::String sourceDescription() const; /** * Returns the unique identifier associated with the manifest. */ materialid_t id() const; void setId(materialid_t newId); /// @c true if the manifest was automatically produced for a game/add-on resource. inline bool isAutoGenerated() const { return isFlagged(AutoGenerated); } /// @c true if the manifest was not produced for an original game resource. inline bool isCustom() const { return isFlagged(Custom); } /** * Returns @c true if the manifest is flagged @a flagsToTest. */ inline bool isFlagged(Flags flagsToTest) const { return !!(flags() & flagsToTest); } /** * Returns the flags for the manifest. */ Flags flags() const; /** * Change the manifest's flags. * * @param flagsToChange Flags to change the value of. * @param operation Logical operation to perform on the flags. */ void setFlags(Flags flagsToChange, de::FlagOp operation = de::SetFlags); /** * Returns @c true if a Material is presently associated with the manifest. * * @see material(), materialPtr() */ bool hasMaterial() const; /** * Returns the logical Material associated with the manifest. * * @see hasMaterial() */ Material &material() const; Material *materialPtr() const; /** * Change the material associated with the manifest. * * @param newMaterial New material to associate with. */ void setMaterial(Material *newMaterial); private: DENG2_PRIVATE(d) }; Q_DECLARE_OPERATORS_FOR_FLAGS(MaterialManifest::Flags) #endif // DENG_RESOURCE_MATERIALMANIFEST_H doomsday-stable-1.15.7/doomsday/client/include/resource/materialanimator.h0000664000175000017500000001444412641367670026254 0ustar jaakkojaakko/** @file materialanimator.h Animator for a draw-context Material variant. * * @authors Copyright © 2009-2014 Daniel Swanson * @authors Copyright © 2009-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_RESOURCE_MATERIALANIMATOR_H #define CLIENT_RESOURCE_MATERIALANIMATOR_H #ifndef __CLIENT__ # error "MaterialAnimator only exists in the Client" #endif #include #include #include #include "Material" #include "MaterialContext" #include "MaterialVariantSpec" #include "Texture" #include "gl/gltextureunit.h" /** * Animator for a Material within a given client side usage context. * * Each usage context has it's own Material animator for an independent animation * timeline. Additionally, contexts define a @ref MaterialVariantSpec which dictates * how the various dependent resources are interpreted within that context. (This is * necessary because of the quirky behavior of the id Tech 1 map renderer, where the * texture dimensions are interpreted differently according to whether it is used on * a "floor" or "wall" map surface). */ class MaterialAnimator { public: /// The referenced (GL)texture unit does not exist. @ingroup errors DENG2_ERROR(MissingTextureUnitError); /// The referenced decoration does not exist. @ingroup errors DENG2_ERROR(MissingDecorationError); /// Notified whenever one or more decoration stage changes occur. DENG2_DEFINE_AUDIENCE(DecorationStageChange, void materialAnimatorDecorationStageChanged(MaterialAnimator &animator)) /** * (GL)Texture unit identifier: */ enum { TU_DETAIL, TU_DETAIL_INTER, TU_LAYER0, TU_LAYER0_INTER, TU_SHINE, TU_SHINE_MASK, NUM_TEXTUREUNITS }; /** * Animated Material::Decoration. */ class Decoration { public: /** * @param decor Material decoration to animate. */ Decoration(MaterialDecoration &decor); /** * Returns the MaterialDecoration being animated. */ MaterialDecoration &decor() const; de::Vector2f origin() const; float elevation() const; float radius() const; de::Vector3f color() const; void lightLevels(float &min, float &max) const; de::Texture *tex() const; de::Texture *ceilTex() const; de::Texture *floorTex() const; float flareSize() const; DGLuint flareTex() const; void rewind(); bool animate(); void update(); void reset(); private: DENG2_PRIVATE(d) }; public: /** * Construct a new MaterialAnimator. * * @param material Material to animate. * @param spec Draw-context variant specification. */ MaterialAnimator(Material &material, de::MaterialVariantSpec const &variantSpec); /** * Returns the Material being animated. */ Material &material() const; /** * Returns the MaterialVariantSpec for the associated usage context. */ de::MaterialVariantSpec const &variantSpec() const; /** * Process a system tick event. If not currently paused and still valid; the material's * layers and decorations are animated. * * @param ticLength Length of the tick in seconds. * * @see isPaused() */ void animate(timespan_t ticLength); /** * Restart the animation, resetting the staged animation point. The state of all layers * and decorations will be rewound to the beginning. */ void rewind(); /** * Returns @c true if animation is currently paused (e.g., the driver for the animation * is the world map-context, using the game timer and the client has paused the game). */ bool isPaused() const; /** * Prepare for drawing a frame (uploading GL textures if necessary and perhaps updating * the animation state snapshot). * * @param forceUpdate @c true= Perform a full update of the state snapshot. The snapshot * will be updated automatically when the animator is first asked to prepare assets for * drawing a new frame. The only time it is necessary to force an update is if the * material state subsequently changes during the same frame. * * @todo Fully internalize animation state updates and separate GL (de)init logics. -ds */ void prepare(bool forceUpdate = false); void cacheAssets(); /** * Returns @c true if the Material is currently thought to be fully "opaque", i.e., the * composited layer stack has no translucent gaps. */ bool isOpaque() const; /** * Returns the current dimension metrics for the animated Material. */ de::Vector2i const &dimensions() const; /** * Returns the current glow strength factor for the animated Material. */ float glowStrength() const; /** * Returns the current shine effect blending mode for the animated Material. */ blendmode_t shineBlendMode() const; /** * Returns the current shine effect minimum color for the animated Material. */ de::Vector3f const &shineMinColor() const; /** * Lookup a prepared GLTextureUnit by it's unique @a unitIndex. * * @see prepare() */ de::GLTextureUnit &texUnit(int unitIndex) const; /** * Lookup an animated Material Decoration by it's unique @a decorIndex. */ Decoration &decoration(int decorIndex) const; private: DENG2_PRIVATE(d) }; #endif // CLIENT_RESOURCE_MATERIALANIMATOR_H doomsday-stable-1.15.7/doomsday/client/include/resource/materialdetaillayer.h0000664000175000017500000000501112641367670026727 0ustar jaakkojaakko/** @file materialdetaillayer.h Logical material, detail-texture layer. * * @authors Copyright © 2011-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_RESOURCE_MATERIALDETAILLAYER_H #define CLIENT_RESOURCE_MATERIALDETAILLAYER_H #include #include #include "resource/materialtexturelayer.h" /** * Specialized MaterialTextureLayer for describing an animated detail texture layer. * * @ingroup resource */ class MaterialDetailLayer : public MaterialTextureLayer { public: /** * Stages describe texture change animations. */ class AnimationStage : public MaterialTextureLayer::AnimationStage { public: AnimationStage(de::Uri const &texture, int tics, float variance = 0, float scale = 1, float strength = 1, float maxDistance = 0); AnimationStage(AnimationStage const &other); virtual ~AnimationStage(); /** * Construct a new AnimationStage from the given @a definition. */ static AnimationStage *fromDef(ded_detail_stage_t const &definition); void resetToDefaults(); }; public: virtual ~MaterialDetailLayer() {} /** * Construct a new DetailTextureLayer from the given @a definition. */ static MaterialDetailLayer *fromDef(ded_detailtexture_t const &definition); /** * Add a new animation stage to the detail texture layer. * * @param stage New stage to add (a copy is made). * * @return Index of the newly added stage (0 based). */ int addStage(AnimationStage const &stage); de::String describe() const; }; #endif // CLIENT_RESOURCE_MATERIALDETAILLAYER_H doomsday-stable-1.15.7/doomsday/client/include/resource/compositebitmapfont.h0000664000175000017500000000461712641367670027012 0ustar jaakkojaakko/** @file compositebitmapfont.h Composite bitmap font. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_RESOURCE_COMPOSITEBITMAPFONT_H #define CLIENT_RESOURCE_COMPOSITEBITMAPFONT_H #include "abstractfont.h" #include "Texture" #include #include #include /** * Composite bitmap font. * * @ingroup resource */ class CompositeBitmapFont : public AbstractFont { public: struct Glyph { de::Rectanglei geometry; patchid_t patch; de::TextureVariant *tex; uint8_t border; bool haveSourceImage; }; public: CompositeBitmapFont(de::FontManifest &manifest); static CompositeBitmapFont *fromDef(de::FontManifest &manifest, ded_compositefont_t const &def); ded_compositefont_t *definition() const; void setDefinition(ded_compositefont_t *newDef); /** * Update the font according to the supplied definition. To be called after * an engine update/reset. * * @param def Definition to update using. * * @todo Should observe engine reset. */ void rebuildFromDef(ded_compositefont_t const &def); int ascent(); int descent(); int lineSpacing(); void glInit(); void glDeinit(); de::Rectanglei const &glyphPosCoords(uchar ch); de::Rectanglei const &glyphTexCoords(uchar ch); patchid_t glyphPatch(uchar ch); void glyphSetPatch(uchar ch, de::String encodedPatchName); de::TextureVariant *glyphTexture(uchar ch); uint glyphTextureBorder(uchar ch); private: DENG2_PRIVATE(d) }; #endif // CLIENT_RESOURCE_COMPOSITEBITMAPFONT_H doomsday-stable-1.15.7/doomsday/client/include/resource/image.h0000664000175000017500000001010712641367670023775 0ustar jaakkojaakko/** @file image.h Image objects and related routines. * * @ingroup resource * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_IMAGE_H #define DENG_RESOURCE_IMAGE_H #include "dd_share.h" // gfxmode_t #include #include #include #include /// @todo Should not depend on texture-level stuff here. class TextureVariantSpec; namespace de { class Texture; } namespace res { enum Source { None, ///< Not a valid source. Original, ///< An "original". External ///< An "external" replacement. }; } /** * @defgroup imageConversionFlags Image Conversion Flags * @ingroup flags */ /*@{*/ #define ICF_UPSCALE_SAMPLE_WRAPH (0x1) #define ICF_UPSCALE_SAMPLE_WRAPV (0x2) #define ICF_UPSCALE_SAMPLE_WRAP (ICF_UPSCALE_SAMPLE_WRAPH|ICF_UPSCALE_SAMPLE_WRAPV) /*@}*/ /** * @defgroup imageFlags Image Flags * @ingroup flags */ /*@{*/ #define IMGF_IS_MASKED (0x1) /*@}*/ struct image_t { typedef de::Vector2ui Size; /// @ref imageFlags int flags; /// Indentifier of the color palette used/assumed or @c 0 if none (1-based). uint paletteId; /// Size of the image in pixels. Size size; /// Bytes per pixel in the data buffer. int pixelSize; /// Pixel color/palette (+alpha) data buffer. uint8_t *pixels; }; /** * Initializes the previously allocated @a image for use. * @param image Image instance. */ void Image_Init(image_t &image); /** * Releases image pixel data, but does not delete @a image. * @param image Image instance. */ void Image_ClearPixelData(image_t &image); /** * Returns the size of the image in pixels. */ image_t::Size Image_Size(image_t const &image); /** * Returns a textual description of the image. * * @return Human-friendly description of the image. */ de::String Image_Description(image_t const &image); /** * Loads PCX, TGA and PNG images. The returned buffer must be freed * with M_Free. Color keying is done if "-ck." is found in the filename. * The allocated memory buffer always has enough space for 4-component * colors. */ uint8_t *Image_LoadFromFile(image_t &image, de::FileHandle &file); bool Image_LoadFromFileWithFormat(image_t &image, char const *format, de::FileHandle &file); bool Image_Save(image_t const &image, char const *filePath); /// @return @c true if the image pixel data contains alpha information. bool Image_HasAlpha(image_t const &image); /** * Converts the image by converting it to a luminance map and then moving * the resultant luminance data into the alpha channel. The color channel(s) * are then filled all-white. */ void Image_ConvertToAlpha(image_t &image, bool makeWhite = false); /** * Converts the image data to grayscale luminance in-place. */ void Image_ConvertToLuminance(image_t &image, bool retainAlpha = true); /// @todo Move into image_t uint8_t *GL_LoadImage(image_t &image, de::String nativePath); /// @todo Move into image_t res::Source GL_LoadExtImage(image_t &image, char const *searchPath, gfxmode_t mode); /// @todo Move into image_t res::Source GL_LoadSourceImage(image_t &image, de::Texture const &tex, TextureVariantSpec const &spec); #endif // DENG_RESOURCE_IMAGE_H doomsday-stable-1.15.7/doomsday/client/include/resource/compositetexture.h0000664000175000017500000001465512641367670026352 0ustar jaakkojaakko/** @file compositetexture.h Composite Texture. * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_COMPOSITETEXTURE_H #define DENG_RESOURCE_COMPOSITETEXTURE_H #include #include #include #include #include #include "dd_types.h" // For lumpnum_t #include "patchname.h" namespace de { /** * A logical texture composite of one or more @em component images. * * The component images are sorted according to the order in which they * should be composited, from bottom-most to top-most. * * @ingroup resource */ class CompositeTexture { public: /** * Flags denoting usage traits. */ enum Flag { Custom = 0x1 ///< The texture does not originate from the current game. }; Q_DECLARE_FLAGS(Flags, Flag) /** * Archived format variants. */ enum ArchiveFormat { DoomFormat, ///< Format used by most id Tech 1 games. StrifeFormat ///< Differs slightly from DoomFormat (omits unused values). }; /** * Component image. */ struct Component { protected: explicit Component(Vector2i const &origin = Vector2i()); public: /// Origin of the top left corner of the component (in texture space units). Vector2i const &origin() const; bool operator == (Component const &other) const; inline bool operator != (Component const &other) const { return !(*this == other); } /// X-axis origin of the top left corner of the component (in texture space units). inline int xOrigin() const { return origin().x; } /// Y-axis origin of the top left corner of the component (in texture space units). inline int yOrigin() const { return origin().y; } /// Returns the number of the lump (file) containing the associated /// image; otherwise @c -1 (not found). lumpnum_t lumpNum() const; friend class CompositeTexture; private: Vector2i _origin; ///< Top left corner in the texture coordinate space. lumpnum_t _lumpNum; ///< Index of the lump containing the associated image. }; typedef QList Components; public: /** * Construct a default composite texture. */ explicit CompositeTexture(String const &percentEncodedName = "", de::Vector2i logicalDimensions = de::Vector2i(), Flags flags = 0); /** * Construct a composite texture by deserializing an archived id-tech 1 * format definition from the Reader. Lump numbers will be looked up using * @a patchNames and any discrepancies or issues in the texture will be * logged about at this time. * * @param reader Reader. * @param patchNames List of component image names. * @param format Format of the archived data. * * @return The deserialized composite texture. Caller gets ownership. */ static CompositeTexture *constructFrom(Reader &reader, QList patchNames, ArchiveFormat format = DoomFormat); /** * Compare two composite texture definitions for equality. * * @return @c true if the definitions are equal. */ bool operator == (CompositeTexture const &other) const; inline bool operator != (CompositeTexture const &other) const { return !(*this == other); } /// Returns the percent-endcoded symbolic name of the texture. String percentEncodedName() const; /// Returns the percent-endcoded symbolic name of the texture. String const &percentEncodedNameRef() const; /// Returns the logical dimensions of the texture (in map space units). Vector2i const &logicalDimensions() const; /// Returns the logical width of the texture (in map space units). inline int logicalWidth() const { return logicalDimensions().x; } /// Returns the logical height of the texture (in map space units). inline int logicalHeight() const { return logicalDimensions().y; } /// Returns the pixel dimensions of the texture. Vector2i const &dimensions() const; /// Returns the pixel width of the texture. inline int width() const { return dimensions().x; } /// Returns the pixel height of the texture. inline int height() const { return dimensions().y; } /// Returns the associated "original index" for the texture. int origIndex() const; /// Change the "original index" value for the texture. void setOrigIndex(int newIndex); /// Returns the total number of component images. inline int componentCount() const { return components().count(); } /** * Provides access to the component images of the texture for efficent traversal. */ Components const &components() const; /** * Returns @c true if the texture has flagged @a flagsToTest. */ inline bool isFlagged(Flags flagsToTest) const { return (flags() & flagsToTest) != 0; } /** * Returns the flags for the composite texture. */ Flags flags() const; /** * Change the composite texture's flags. * * @param flagsToChange Flags to change the value of. * @param operation Logical operation to perform on the flags. */ void setFlags(Flags flagsToChange, de::FlagOp operation = de::SetFlags); private: DENG2_PRIVATE(d) }; Q_DECLARE_OPERATORS_FOR_FLAGS(CompositeTexture::Flags) typedef CompositeTexture::Component CompositeTextureComponent; } // namespace de #endif // DENG_RESOURCE_COMPOSITETEXTURE_H doomsday-stable-1.15.7/doomsday/client/include/resource/texturevariantspec.h0000664000175000017500000001310012641367670026647 0ustar jaakkojaakko/** @file texturevariantspec.h Texture resource, variant specification. * * @authors Copyright © 2011-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_TEXTUREVARIANTSPEC_H #define DENG_RESOURCE_TEXTUREVARIANTSPEC_H #ifndef __CLIENT__ # error "resource/texturevariantspec.h only exists in the Client" #endif #include "dd_types.h" //#include "gl/sys_opengl.h" #include typedef enum { TC_UNKNOWN = -1, TEXTUREVARIANTUSAGECONTEXT_FIRST = 0, TC_UI = TEXTUREVARIANTUSAGECONTEXT_FIRST, TC_MAPSURFACE_DIFFUSE, TC_MAPSURFACE_REFLECTION, TC_MAPSURFACE_REFLECTIONMASK, TC_MAPSURFACE_LIGHTMAP, TC_SPRITE_DIFFUSE, TC_MODELSKIN_DIFFUSE, TC_MODELSKIN_REFLECTION, TC_HALO_LUMINANCE, TC_PSPRITE_DIFFUSE, TC_SKYSPHERE_DIFFUSE, TEXTUREVARIANTUSAGECONTEXT_LAST = TC_SKYSPHERE_DIFFUSE } texturevariantusagecontext_t; #define TEXTUREVARIANTUSAGECONTEXT_COUNT (\ TEXTUREVARIANTUSAGECONTEXT_LAST + 1 - TEXTUREVARIANTUSAGECONTEXT_FIRST ) #define VALID_TEXTUREVARIANTUSAGECONTEXT(tc) (\ (tc) >= TEXTUREVARIANTUSAGECONTEXT_FIRST && (tc) <= TEXTUREVARIANTUSAGECONTEXT_LAST) /** * @defgroup textureVariantSpecificationFlags Texture Variant Specification Flags * @ingroup flags */ /*@{*/ #define TSF_ZEROMASK 0x1 // Set pixel alpha to fully opaque. #define TSF_NO_COMPRESSION 0x2 #define TSF_UPSCALE_AND_SHARPEN 0x4 #define TSF_MONOCHROME 0x8 #define TSF_INTERNAL_MASK 0xff000000 #define TSF_HAS_COLORPALETTE_XLAT 0x80000000 /*@}*/ struct variantspecification_t { texturevariantusagecontext_t context; int flags; /// @ref textureVariantSpecificationFlags byte border; /// In pixels, added to all four edges of the texture. int wrapS, wrapT; dd_bool mipmapped, gammaCorrection, noStretch, toAlpha; /** * Minification filter modes. Specified using either a logical * texture class id (actual mode used is then determined by the * user's preference for that class) or a constant value. * * Texture class: * -1: No class * * Constant: * 0: Nearest or Nearest-Mipmap-Nearest (if mipmapping) * 1: Linear or Linear-Mipmap-Nearest (if mipmapping) * 2: Nearest-Mipmap-Linear (mipmapping only) * 3: Linear-Mipmap-Linear (mipmapping only) */ int minFilter; /** * Magnification filter modes. Specified using either a logical * texture class id (actual mode used is then determined by the * user's preference for that class) or a constant value. * * Texture class: * -3: UI class * -2: Sprite class * -1: No class * * Constant: * 0: Nearest (in Manhattan distance) * 1: Linear (weighted average) */ int magFilter; /// -1: User preference else a logical DGL anisotropic filter level. int anisoFilter; /// Color palette translation. int tClass, tMap; int glMinFilter() const; int glMagFilter() const; int logicalAnisoLevel() const; variantspecification_t(); variantspecification_t(variantspecification_t const &other); /** * Magnification, Anisotropic filter level and GL texture wrap modes are * handled through dynamic changes to GL's texture environment state. * Consequently, they are ignored during spec equality comparison. */ bool operator == (variantspecification_t const &other) const; inline bool operator != (variantspecification_t const &other) const { return !(*this == other); } }; /** * Detail textures are faded to gray depending on the contrast factor. * The texture is also progressively faded towards gray in each mipmap * level uploaded. * * Contrast is quantized in order to reduce the number of variants to * a more sensible/manageable number per texture. */ #define DETAILTEXTURE_CONTRAST_QUANTIZATION_FACTOR (10) struct detailvariantspecification_t { uint8_t contrast; bool operator == (detailvariantspecification_t const &other) const; inline bool operator != (detailvariantspecification_t const &other) const { return !(*this == other); } }; enum texturevariantspecificationtype_t { TST_GENERAL, TST_DETAIL }; class TextureVariantSpec { public: texturevariantspecificationtype_t type; variantspecification_t variant; detailvariantspecification_t detailVariant; public: TextureVariantSpec(texturevariantspecificationtype_t type = TST_GENERAL); TextureVariantSpec(TextureVariantSpec const &other); bool operator == (TextureVariantSpec const &other) const; inline bool operator != (TextureVariantSpec const &other) const { return !(*this == other); } /** * Returns a textual, human-readable representation of the specification. */ de::String asText() const; }; #endif // DENG_RESOURCE_TEXTUREVARIANTSPEC_H doomsday-stable-1.15.7/doomsday/client/include/resource/mapdef.h0000664000175000017500000000443012641367670024151 0ustar jaakkojaakko/** @file mapdef.h Map asset/resource definition/manifest. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_RESOURCE_MAPDEF_H #define LIBDENG_RESOURCE_MAPDEF_H #include #include #include #include #include #include "Game" /** * Definition/manifeset for a map asset/resource. * * @ingroup resource */ class MapDef : public de::PathTree::Node, public de::Record { public: MapDef(de::PathTree::NodeArgs const &args); /** * Returns a textual description of the map definition. * * @return Human-friendly description the map definition. */ de::String description(de::Uri::ComposeAsTextFlags uriCompositionFlags = de::Uri::DefaultComposeAsTextFlags) const; /** * Returns the URI this resource will be known by. */ inline de::Uri composeUri() const { return de::Uri("Maps", gets("id")); } /** * Returns the id used to uniquely reference the map in some (old) definitions. */ de::String composeUniqueId(de::Game const ¤tGame) const; MapDef &setSourceFile(de::File1 *newSourceFile); de::File1 *sourceFile() const; MapDef &setRecognizer(de::Id1MapRecognizer *newRecognizer); de::Id1MapRecognizer const &recognizer() const; private: //String cachePath; //bool lastLoadAttemptFailed; de::File1 *_sourceFile; QScopedPointer _recognized; }; #endif /* LIBDENG_RESOURCE_MAPDEF_H */ doomsday-stable-1.15.7/doomsday/client/include/resource/abstractfont.h0000664000175000017500000000522412641367670025411 0ustar jaakkojaakko/** @file abstractfont.h Abstract font. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_RESOURCE_ABSTRACTFONT_H #define CLIENT_RESOURCE_ABSTRACTFONT_H #include #include #include #include namespace de { class FontManifest; } /// Special value used to signify an invalid font id. #define NOFONTID 0 /** * Abstract font resource. * * @em Clearing a font means any names bound to it are deleted and any GL textures * acquired for it are 'released' at this time). The Font instance record used * to represent it is also deleted. * * @em Releasing a font will release any GL textures acquired for it. * * @ingroup resource */ class AbstractFont { public: /// Notified when the resource is about to be deleted. DENG2_DEFINE_AUDIENCE(Deletion, void fontBeingDeleted(AbstractFont const &font)) enum Flag { Colorize = 0x1, ///< Can be colored. Shadowed = 0x2 ///< A shaodw is embedded in the font. }; Q_DECLARE_FLAGS(Flags, Flag) static int const MAX_CHARS = 256; // Normal 256 ANSI characters. public: /// Resource manifest for the font. de::FontManifest &_manifest; Flags _flags; AbstractFont(de::FontManifest &manifest); virtual ~AbstractFont(); DENG2_AS_IS_METHODS() /** * Returns the resource manifest for the font. */ de::FontManifest &manifest() const; /// @return Returns a copy of the font's flags. Flags flags() const; virtual int ascent(); virtual int descent(); virtual int lineSpacing(); virtual void glInit(); virtual void glDeinit(); virtual de::Rectanglei const &glyphPosCoords(uchar ch) = 0; virtual de::Rectanglei const &glyphTexCoords(uchar ch) = 0; }; Q_DECLARE_OPERATORS_FOR_FLAGS(AbstractFont::Flags) #endif // CLIENT_RESOURCE_ABSTRACTFONT_H doomsday-stable-1.15.7/doomsday/client/include/resource/rawtexture.h0000664000175000017500000000305312641367670025127 0ustar jaakkojaakko/** @file rawtexture.h Raw Texture. * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_RAWTEXTURE #define DENG_RESOURCE_RAWTEXTURE #include "dd_share.h" // For lumpnum_t #include /** * A rawtex is a lump raw graphic that has been prepared for render. */ struct rawtex_t { de::String name; ///< Percent-encoded. lumpnum_t lumpNum; DGLuint tex; /// Name of the associated DGL texture. short width, height; byte masked; rawtex_t(de::String name, lumpnum_t lumpNum) : name(name) , lumpNum(lumpNum) , tex(0) , width(0) , height(0) , masked(0) {} }; #endif // DENG_RESOURCE_RAWTEXTURE doomsday-stable-1.15.7/doomsday/client/include/resource/materiallightdecoration.h0000664000175000017500000000756412641367670027626 0ustar jaakkojaakko/** @file materiallightdecoration.h Logical material, light decoration. * * @authors Copyright © 2011-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_RESOURCE_MATERIALLIGHTDECORATION_H #define CLIENT_RESOURCE_MATERIALLIGHTDECORATION_H #ifndef __CLIENT__ # error "MaterialLightDecoration only exists in the Client" #endif #include #include #include #include "Material" #include "r_util.h" // LightRange /** * @ingroup resource */ class MaterialLightDecoration : public Material::Decoration { public: /** * Stages describe light change animations. */ class AnimationStage : public Stage { public: de::Vector2f origin; ///< Position in material space. float elevation; ///< Distance from the surface. de::Vector3f color; ///< Light color. float radius; ///< Dynamic light radius (-1 = no light). float haloRadius; ///< Halo radius (zero = no halo). LightRange lightLevels; ///< Fade by sector lightlevel. de::Texture *tex; de::Texture *floorTex; de::Texture *ceilTex; de::Texture *flareTex; int sysFlareIdx; ///< @todo Remove me public: AnimationStage(int tics, float variance, de::Vector2f const &origin, float elevation, de::Vector3f const &color, float radius, float haloRadius, LightRange const &lightLevels, de::Texture *ceilingTexture, de::Texture *floorTexture, de::Texture *texture, de::Texture *flareTexture, int sysFlareIdx = -1); AnimationStage(AnimationStage const &other); virtual ~AnimationStage() {} /** * Construct a new AnimationStage from the given @a stageDef. */ static AnimationStage *fromDef(de::Record const &stageDef); de::String description() const; }; public: MaterialLightDecoration(de::Vector2i const &patternSkip = de::Vector2i(), de::Vector2i const &patternOffset = de::Vector2i(), bool useInterpolation = true); virtual ~MaterialLightDecoration(); /** * Construct a new material decoration from the specified definition. */ static MaterialLightDecoration *fromDef(de::Record const &decorationDef); de::String describe() const; /** * Add a new animation stage to the material light decoration. * * @param stage New stage to add (a copy is made). * * @return Index of the newly added stage (0 based). */ int addStage(AnimationStage const &stage); /** * Lookup an AnimationStage by it's unique @a index. * * @param index Index of the AnimationStage to lookup. Will be cycled into valid range. */ AnimationStage &stage(int index) const; /** * Returns @c true if interpolation should be used with this decoration. */ bool useInterpolation() const; private: bool _useInterpolation; }; #endif // CLIENT_RESOURCE_MATERIALLIGHTDECORATION_H doomsday-stable-1.15.7/doomsday/client/include/resource/pcx.h0000664000175000017500000000311512641367670023506 0ustar jaakkojaakko/** @file pcx.h PCX image reader. * * Originally derived from the Q2 utils source (lbmlib.c). * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * @authors Copyright © 1997-2006 id Software, Inc * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_PCX_H #define DENG_RESOURCE_PCX_H #include #include /// @addtogroup resource ///@{ /** * Reads the given PCX image and returns a pointer to a planar RGBA buffer. * The caller must free the allocated buffer with Z_Free. */ uint8_t *PCX_Load(de::FileHandle &file, de::Vector2ui &outSize, int &pixelSize); /** * @return Textual message detailing the last error encountered else @c 0. */ char const *PCX_LastError(); ///@} #endif // DENG_RESOURCE_PCX_H doomsday-stable-1.15.7/doomsday/client/include/resource/resourcesystem.h0000664000175000017500000007253612641367670026025 0ustar jaakkojaakko/** @file resourcesystem.h Resource subsystem. * * @authors Copyright © 2013-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_RESOURCESYSTEM_H #define DENG_RESOURCESYSTEM_H #include #include #include "resource/animgroup.h" #include "resource/colorpalette.h" #include "MapDef" #ifdef __CLIENT__ # include "AbstractFont" # include "BitmapFont" # include "CompositeBitmapFont" # include "FontScheme" # include "MaterialVariantSpec" # include "Model" # include "ModelDef" #endif #include "Material" #include "MaterialScheme" #include "Sprite" #include "Texture" #include "TextureScheme" #include "resource/rawtexture.h" #include #include #include #include #include #include #include #include #include /** * Logical resources; materials, packages, textures, etc... * * Resource pointers are considered @em eternal in the sense that they will * continue to reference the same logical resource data, even after the engine * is reset. Public resource identifiers (e.g., materialid_t) are similarly * eternal. * * Resource names (paths) are semi-independant from the resources. There may be * multiple names for any given resource (aliases). The only requirement is that * their symbolic name must be unique among resources in the same scheme. * * @par Classification * * @em Runtime resources are not loaded until precached or actually needed. They * may be cleared, in which case they will be reloaded when needed. * * @em System resources are loaded at startup and remain in memory all the time. * After clearing they must be manually reloaded. * * @par Texture resources * * @em Clearing a texture is to 'undefine' it - any names bound to it will be * deleted and any GL textures acquired for it are 'released'. The logical * Texture instance used to represent it is also deleted. * * @em Releasing a texture will leave it defined (any names bound to it will * persist) but any GL textures acquired for it are 'released'. Note that the * logical Texture instance used to represent is NOT be deleted. * * @ingroup resource */ class ResourceSystem : public de::System { public: /// An unknown resource class identifier was specified. @ingroup errors DENG2_ERROR(UnknownResourceClassError); /// An unknown resource scheme was referenced. @ingroup errors DENG2_ERROR(UnknownSchemeError); /// The referenced manifest was not found. @ingroup errors DENG2_ERROR(MissingManifestError); /// The referenced resource was not found. @ingroup errors DENG2_ERROR(MissingResourceError); /// An unknown material group was referenced. @ingroup errors DENG2_ERROR(UnknownMaterialGroupError); /// The specified material id was invalid (out of range). @ingroup errors DENG2_ERROR(UnknownMaterialIdError); #ifdef __CLIENT__ /// The referenced model def was not found. @ingroup errors DENG2_ERROR(MissingModelDefError); /// The specified font id was invalid (out of range). @ingroup errors DENG2_ERROR(UnknownFontIdError); #endif typedef QSet MaterialManifestSet; typedef MaterialManifestSet MaterialManifestGroup; // Alias typedef QList MaterialManifestGroups; typedef QMap TextureSchemes; typedef QList AllTextures; #ifdef __CLIENT__ typedef QMap FontSchemes; typedef QList AllFonts; #endif typedef QList SpriteSet; typedef de::PathTreeT MapDefs; public: /** * Construct a new resource system, configuring all resource classes and * the associated resource collection schemes. */ ResourceSystem(); // System. void timeChanged(de::Clock const &); /** * Lookup a ResourceClass by symbolic @a name. */ ResourceClass &resClass(de::String name); /** * Lookup a ResourceClass by @a id. * @todo Refactor away. */ ResourceClass &resClass(resourceclassid_t id); /** * Gets the path from "Config.resource.iwadFolder" and makes it the sole override * path for the Packages scheme. */ void updateOverrideIWADPathFromConfig(); void clearAllResources(); void clearAllRuntimeResources(); void clearAllSystemResources(); /** * Returns @c true iff a sprite exists for the specified @a spriteId and @a frame; * * @param spriteId Unique identifier of the sprite set. * @param frame Frame number from the set to lookup. */ bool hasSprite(spritenum_t spriteId, int frame); /** * Lookup a sprite by unique identifier & frame number. * * @see hasSprite(), spritePtr() */ Sprite &sprite(spritenum_t spriteId, int frame); /** * Returns a pointer to the identified Sprite. * * @see hasSprite() */ inline Sprite *spritePtr(spritenum_t spriteId, int frame) { return hasSprite(spriteId, frame)? &sprite(spriteId, frame) : 0; } /** * Lookup the sprite set for the specified @a spriteId. * * @param spriteId Unique identifier of the sprite set. * @return The identified SpriteSet. */ SpriteSet const &spriteSet(spritenum_t spriteId); /** * Returns the total number of sprite @em sets. */ int spriteCount(); /** * Determines if a material exists for a @a path. * * @return @c true if a material exists; otherwise @a false. * * @see hasMaterialManifest(), MaterialManifest::hasMaterial() */ inline bool hasMaterial(de::Uri const &path) const { if(hasMaterialManifest(path)) return materialManifest(path).hasMaterial(); return false; } /** * Lookup a material resource for the specified @a path. * * @return The found material. * * @see MaterialManifest::material() */ inline Material &material(de::Uri const &path) const { return materialManifest(path).material(); } /** * Returns a pointer to the identified Material. * * @see hasMaterialManifest(), MaterialManifest::materialPtr() */ inline Material *materialPtr(de::Uri const &path) { if(hasMaterialManifest(path)) return materialManifest(path).materialPtr(); return 0; } /** * Determines if a manifest exists for a material on @a path. * * @return @c true if a manifest exists; otherwise @a false. */ bool hasMaterialManifest(de::Uri const &path) const; /** * Lookup a material manifest by it's unique resource @a path. * * @param path The path to search for. * @return Found material manifest. */ MaterialManifest &materialManifest(de::Uri const &path) const; /** * Lookup a manifest by unique identifier. * * @param id Unique identifier for the manifest to be looked up. Note * that @c 0 is not a valid identifier. * * @return The associated manifest. */ MaterialManifest &toMaterialManifest(materialid_t id) const; /** * Returns the total number of unique materials in the collection. */ int materialCount() const; /** * Returns @c true iff a MaterialScheme exists with the symbolic @a name. */ bool knownMaterialScheme(de::String name) const; /** * Lookup a material resource scheme by symbolic name. * * @param name Symbolic name of the scheme. * @return MaterialScheme associated with @a name. * * @throws UnknownSchemeError If @a name is unknown. * * @see knownMaterialScheme() */ de::MaterialScheme &materialScheme(de::String name) const; /** * Returns the total number of material manifest schemes in the collection. */ int materialSchemeCount() const; /** * Iterate through all the material resource schemes of the resource system. * * @param func Callback to make for each MaterialScheme. */ de::LoopResult forAllMaterialSchemes(std::function func) const; /** * Clear all materials (and their manifests) in all schemes. * * @see forAllMaterialSchemes(), MaterialScheme::clear(). */ inline void clearAllMaterialSchemes() { forAllMaterialSchemes([] (de::MaterialScheme &scheme) { scheme.clear(); return de::LoopContinue; }); DENG2_ASSERT(materialCount() == 0); // sanity check } /** * Lookup a material manifest group by unique @a number. */ MaterialManifestGroup &materialGroup(int number) const; /** * Create a new (empty) material manifest group. */ MaterialManifestGroup &newMaterialGroup(); /** * Destroys all material manifest groups. */ void clearAllMaterialGroups(); /** * Provides a list of all material manifest groups, for efficient traversal. */ MaterialManifestGroups const &allMaterialGroups() const; /** * Returns the total number of material manifest groups in the collection. */ inline int materialGroupCount() const { return allMaterialGroups().count(); } /** * Declare a material in the collection, producing a manifest for a logical * Material which will be defined later. If a manifest with the specified * @a uri already exists the existing manifest will be returned. * * @param uri Uri representing a path to the material in the virtual hierarchy. * * @return Manifest for this URI. */ inline MaterialManifest &declareMaterial(de::Uri const &uri) { return materialScheme(uri.scheme()).declare(uri.path()); } /** * Iterate through all the materials of the resource system. * * @param func Callback to make for each Material. */ de::LoopResult forAllMaterials(std::function func) const; /** * Determines if a texture exists for @a path. * * @return @c true, if a texture exists; otherwise @a false. * * @see hasTextureManifest(), TextureManifest::hasTexture() */ inline bool hasTexture(de::Uri const &path) const { if(hasTextureManifest(path)) return textureManifest(path).hasTexture(); return false; } /** * Lookup a texture resource for the specified @a path. * * @return The found texture. * * @see textureManifest(), TextureManifest::texture() */ inline de::Texture &texture(de::Uri const &path) const { return textureManifest(path).texture(); } /** * Returns a pointer to the identified Texture. * * @see hasTextureManifest(), TextureManifest::texturePtr() */ inline de::Texture *texturePtr(de::Uri const &path) { if(hasTextureManifest(path)) return textureManifest(path).texturePtr(); return NULL; } /** * Convenient method of searching the texture collection for a texture with * the specified @a schemeName and @a resourceUri. * * @param schemeName Unique name of the scheme in which to search. * @param resourceUri Path to the (image) resource to find the texture for. * * @return The found texture; otherwise @c nullptr. */ de::Texture *texture(de::String schemeName, de::Uri const &resourceUri); /** * Determines if a texture manifest exists for a declared texture on @a path. * * @return @c true, if a manifest exists; otherwise @a false. */ bool hasTextureManifest(de::Uri const &path) const; /** * Find the manifest for a declared texture. * * @param search The search term. * @return Found unique identifier. */ de::TextureManifest &textureManifest(de::Uri const &search) const; /** * Lookup a subspace scheme by symbolic name. * * @param name Symbolic name of the scheme. * @return Scheme associated with @a name. * * @throws UnknownSchemeError If @a name is unknown. */ de::TextureScheme &textureScheme(de::String name) const; /** * Returns @c true iff a Scheme exists with the symbolic @a name. */ bool knownTextureScheme(de::String name) const; /** * Returns a list of all the schemes for efficient traversal. */ TextureSchemes const &allTextureSchemes() const; /** * Returns the total number of manifest schemes in the collection. */ inline int textureSchemeCount() const { return allTextureSchemes().count(); } /** * Clear all textures in all schemes. * * @see Scheme::clear(). */ inline void clearAllTextureSchemes() { foreach(de::TextureScheme *scheme, allTextureSchemes()) { scheme->clear(); } } /** * Returns a list of all the unique texture instances in the collection, * from all schemes. */ AllTextures const &allTextures() const; /** * Declare a texture in the collection, producing a manifest for a logical * Texture which will be defined later. If a manifest with the specified * @a uri already exists the existing manifest will be returned. * * If any of the property values (flags, dimensions, etc...) differ from * that which is already defined in the pre-existing manifest, any texture * which is currently associated is released (any GL-textures acquired for * it are deleted). * * @param uri Uri representing a path to the texture in the * virtual hierarchy. * @param flags Texture flags property. * @param dimensions Logical dimensions property. * @param origin World origin offset property. * @param uniqueId Unique identifier property. * @param resourceUri Resource URI property. * * @return Manifest for this URI. */ inline de::TextureManifest &declareTexture(de::Uri const &uri, de::Texture::Flags flags, de::Vector2i const &dimensions, de::Vector2i const &origin, int uniqueId, de::Uri const *resourceUri = 0) { return textureScheme(uri.scheme()) .declare(uri.path(), flags, dimensions, origin, uniqueId, resourceUri); } de::Texture *defineTexture(de::String schemeName, de::Uri const &resourceUri, de::Vector2i const &dimensions = de::Vector2i()); patchid_t declarePatch(de::String encodedName); /** * Returns a rawtex_t for the given lump if one already exists; otherwise @c 0. */ rawtex_t *rawTexture(lumpnum_t lumpNum); /** * Get a rawtex_t data structure for a raw texture specified with a WAD lump * number. Allocates a new rawtex_t if it hasn't been loaded yet. */ rawtex_t *declareRawTexture(lumpnum_t lumpNum); /** * Returns a list of pointers to all the raw textures in the collection. */ QList collectRawTextures() const; #ifdef __CLIENT__ /** * Determines if a manifest exists for a resource on @a path. * * @return @c true, if a manifest exists; otherwise @a false. */ bool hasFont(de::Uri const &path) const; /** * Convenient method of looking up a concrete font resource in the collection * given it's unique identifier. * * @return The associated font resource. * * @see toFontManifest(), FontManifest::hasResource() */ inline AbstractFont &font(fontid_t id) const { return toFontManifest(id).resource(); } /** * Returns the total number of resource manifests in the collection. */ uint fontCount() const { return allFonts().count(); } /** * Find a resource manifest. * * @param search The search term. * @return Found unique identifier. */ de::FontManifest &fontManifest(de::Uri const &search) const; /** * Lookup a manifest by unique identifier. * * @param id Unique identifier for the manifest to be looked up. Note * that @c 0 is not a valid identifier. * * @return The associated manifest. */ de::FontManifest &toFontManifest(fontid_t id) const; /** * Lookup a subspace scheme by symbolic name. * * @param name Symbolic name of the scheme. * @return Scheme associated with @a name. * * @throws UnknownSchemeError If @a name is unknown. */ de::FontScheme &fontScheme(de::String name) const; /** * Returns @c true iff a Scheme exists with the symbolic @a name. */ bool knownFontScheme(de::String name) const; /** * Returns a list of all the schemes for efficient traversal. */ FontSchemes const &allFontSchemes() const; /** * Returns the total number of manifest schemes in the collection. */ inline int fontSchemeCount() const { return allFontSchemes().count(); } /** * Clear all resources in all schemes. * * @see allFontSchemes(), FontScheme::clear(). */ inline void clearAllFontSchemes() { foreach(de::FontScheme *scheme, allFontSchemes()) { scheme->clear(); } } /** * Returns a list of pointers to all the concrete resources in the collection, * from all schemes. */ AllFonts const &allFonts() const; /** * Declare a resource in the collection, producing a (possibly new) manifest * for a resource which may be defined later. If a manifest with the specified * @a uri already exists the existing manifest will be returned. * * @param uri Uri representing a path to the resource in the virtual hierarchy. * * @return The associated manifest for this URI. */ inline de::FontManifest &declareFont(de::Uri const &uri) { return fontScheme(uri.scheme()).declare(uri.path()); } /** * Lookup the unique index attributed to the given @a modelDef. * * @return Index of the definition; otherwise @c -1 if @a modelDef is unknown. */ int indexOf(ModelDef const *modelDef); /** * Convenient method of looking up a concrete model resource in the collection * given it's unique identifier. O(1) * * @return The associated model resource. */ Model &model(modelid_t id); /** * Determines if a model definition exists with the given @a id. O(n) * * @return @c true, if a definition exists; otherwise @a false. * * @see modelDef() */ bool hasModelDef(de::String id) const; /** * Retrieve a model definition by it's unique @a index. O(1) * * @return The associated model definition. * * @see modelDefCount() */ ModelDef &modelDef(int index); /** * Lookup a model definition by it's unique @a id. O(n) * * @return Found model definition. * * @see hasModelDef() */ ModelDef &modelDef(de::String id); /** * Lookup a model definition for the specified mobj @a stateIndex. * * @param stateIndex Index of the mobj state. * @param select Model selector argument. There may be multiple models * for a given mobj state. The selector determines which * is used according to some external selection criteria. * * @return Found model definition; otherwise @c 0. */ ModelDef *modelDefForState(int stateIndex, int select = 0); /** * Returns the total number of model definitions in the system. * * @see modelDef() */ int modelDefCount() const; /// @todo Refactor away. Used for animating particle/sky models. void setModelDefFrame(ModelDef &modelDef, int frame); /** * Release all GL-textures in all schemes. */ void releaseAllGLTextures(); /** * Release all GL-textures in schemes flagged 'runtime'. */ void releaseAllRuntimeGLTextures(); /** * Release all GL-textures in schemes flagged 'system'. */ void releaseAllSystemGLTextures(); /** * Release all GL-textures in the identified scheme. * * @param schemeName Symbolic name of the texture scheme to process. */ void releaseGLTexturesByScheme(de::String schemeName); /** * Prepare a material variant specification in accordance to the specified * usage context. If incomplete context information is supplied, suitable * default values will be chosen in their place. * * @param contextId Usage context identifier. * @param flags @ref textureVariantSpecificationFlags * @param border Border size in pixels (all edges). * @param tClass Color palette translation class. * @param tMap Color palette translation map. * @param wrapS GL texture wrap/clamp mode on the horizontal axis (texture-space). * @param wrapT GL texture wrap/clamp mode on the vertical axis (texture-space). * @param minFilter Logical DGL texture minification level. * @param magFilter Logical DGL texture magnification level. * @param anisoFilter @c -1= User preference else a logical DGL anisotropic filter level. * @param mipmapped @c true= use mipmapping. * @param gammaCorrection @c true= apply gamma correction to textures. * @param noStretch @c true= disallow stretching of textures. * @param toAlpha @c true= convert textures to alpha data. * * @return The interned copy of the rationalized specification. */ de::MaterialVariantSpec const &materialSpec(MaterialContextId contextId, int flags, byte border, int tClass, int tMap, int wrapS, int wrapT, int minFilter, int magFilter, int anisoFilter, bool mipmapped, bool gammaCorrection, bool noStretch, bool toAlpha); /** * Prepare a TextureVariantSpecification according to usage context. If the * specification is incomplete suitable defaults are chosen automatically. * * @param tc Usage context. * @param flags @ref textureVariantSpecificationFlags * @param border Border size in pixels (all edges). * @param tClass Color palette translation class. * @param tMap Color palette translation map. * * @return The interned copy of the rationalized specification. */ TextureVariantSpec const &textureSpec(texturevariantusagecontext_t tc, int flags, byte border, int tClass, int tMap, int wrapS, int wrapT, int minFilter, int magFilter, int anisoFilter, dd_bool mipmapped, dd_bool gammaCorrection, dd_bool noStretch, dd_bool toAlpha); /** * Prepare a TextureVariantSpecification according to usage context. If the * specification is incomplete suitable defaults are chosen automatically. * * @return A rationalized and valid TextureVariantSpecification. */ TextureVariantSpec &detailTextureSpec(float contrast); AbstractFont *newFontFromDef(ded_compositefont_t const &def); AbstractFont *newFontFromFile(de::Uri const &uri, de::String filePath); /** * Release all GL-textures for fonts in the identified scheme. * * @param schemeName Symbolic name of the font scheme to process. */ void releaseFontGLTexturesByScheme(de::String schemeName); #endif // __CLIENT__ /** * Convenient method of locating a MapDef for the given @a mapUri. */ MapDef *mapDef(de::Uri const &mapUri) const; /** * Provides immutable access to a list containing all MapDefs in the system, * for efficient traversal. */ MapDefs const &allMapDefs() const; /** * @overload */ MapDefs &allMapDefs(); /** * Returns the total number of MapDefs in the system. */ inline int mapDefCount() const { return allMapDefs().size(); } /** * Returns the total number of animation/precache groups. */ int animGroupCount(); /** * Destroys all the animation groups. */ void clearAllAnimGroups(); /** * Construct a new animation group. * * @param flags @ref animationGroupFlags */ de::AnimGroup &newAnimGroup(int flags); /** * Returns the AnimGroup associated with @a uniqueId (1-based); otherwise @c 0. */ de::AnimGroup *animGroup(int uniqueId); de::AnimGroup *animGroupForTexture(de::TextureManifest const &textureManifest); /** * Returns the total number of color palettes. */ int colorPaletteCount() const; /** * Destroys all the color palettes. */ void clearAllColorPalettes(); /** * Returns the ColorPalette associated with unique @a id. */ ColorPalette &colorPalette(colorpaletteid_t id) const; /** * Returns the symbolic name of the specified color @a palette. A zero-length * string is returned if no name is associated. */ de::String colorPaletteName(ColorPalette &palette) const; /** * Returns @c true iff a ColorPalette with the specified @a name is present. */ bool hasColorPalette(de::String name) const; /** * Returns the ColorPalette associated with @a name. * * @see hasColorPalette() */ ColorPalette &colorPalette(de::String name) const; /** * @param newPalette Color palette to add. Ownership of the palette is given * to the resource system. * @param name Symbolic name of the color palette. */ void addColorPalette(ColorPalette &newPalette, de::String const &name = de::String()); /** * Returns the unique identifier of the current default color palette. */ colorpaletteid_t defaultColorPalette() const; /** * Change the default color palette. * * @param newDefaultPalette The color palette to make default. */ void setDefaultColorPalette(ColorPalette *newDefaultPalette); #ifdef __CLIENT__ /** * Prepare resources for the current Map. */ void cacheForCurrentMap(); /** * Add a variant of @a material to the cache queue for deferred preparation. * * @param material Base material from which to derive a context variant. * @param spec Specification for the derivation of @a material. * @param cacheGroups @c true= variants for all materials in any applicable * groups are desired; otherwise just specified material. */ void cache(Material &material, de::MaterialVariantSpec const &spec, bool cacheGroups = true); /** * Cache all resources needed to visualize models using the given @a modelDef. */ void cache(ModelDef *modelDef); /** * Precache resources from the set associated with the specified @a spriteId. * * @param spriteId Unique identifier of the sprite set to cache. * @param materialSpec Specification to use when caching materials. */ void cache(spritenum_t spriteId, de::MaterialVariantSpec const &materialSpec); /** * Process all queued material cache tasks. */ void processCacheQueue(); /** * Cancel all queued material cache tasks. */ void purgeCacheQueue(); #endif // __CLIENT__ /** * Returns the native path of the root of the saved session repository */ de::NativePath nativeSavePath(); /** * Utility for scheduling legacy savegame conversion(s) (delegated to background Tasks). * * @param gameId Identity key of the game and corresponding subfolder name within * save repository to output the converted savegame to. Also used for * resolving ambiguous savegame formats. * @param sourcePath If a zero-length string then @em all legacy savegames located for * this game will be considered. Otherwise use the path of a single * legacy savegame file to schedule a single conversion. * * @return @c true if one or more conversion tasks were scheduled. */ bool convertLegacySavegames(de::String const &gameId, de::String const &sourcePath = ""); public: /// @todo Should be private: void initTextures(); void initSystemTextures(); void initMapDefs(); void initSprites(); #ifdef __CLIENT__ void initModels(); #endif void clearAllMapDefs(); void clearAllRawTextures(); void clearAllTextureSpecs(); void pruneUnusedTextureSpecs(); public: /** * Register the console commands, variables, etc..., of this module. */ static void consoleRegister(); static de::String resolveSymbol(de::String const &symbol); private: DENG2_PRIVATE(d) }; DENG_EXTERN_C byte precacheMapMaterials, precacheSprites; DENG_EXTERN_C byte texGammaLut[256]; void R_BuildTexGammaLut(); #endif // DENG_RESOURCESYSTEM_H doomsday-stable-1.15.7/doomsday/client/include/resource/manifest.h0000664000175000017500000000577112641367670024534 0ustar jaakkojaakko/** @file manifest.h Manifest for a logical resource. * * @authors Copyright © 2010-2013 Daniel Swanson * @authors Copyright © 2010-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_MANIFEST_H #define DENG_RESOURCE_MANIFEST_H #include "api_uri.h" #include #include namespace de { /** * Stores high-level metadata for and arbitrates/facilitates access to the * associated "physical" game data resource. * * @ingroup core */ class ResourceManifest { public: /** * @param rClass Class for the associated resource. * @param fFlags @ref fileFlags * @param name An expected name for the associated file. */ ResourceManifest(resourceclassid_t rClass, int fFlags, String *name = 0); /// @return Class of the associated resource. resourceclassid_t resourceClass() const; /// @return Flags for this file. int fileFlags() const; /** * Returns a list of "identity keys" used to identify the resource. */ QStringList const &identityKeys() const; /** * Add a new file segment identity key to the list for this manifest. * * @param newIdentityKey New identity key (e.g., a lump/file name). */ void addIdentityKey(String newIdentityKey); /** * Returns a list of known-names for the associated resource. */ QStringList const &names() const; /** * Add a new file name to the list of names for this manifest. * * @param newName New name for this file. Newer names have precedence. */ void addName(String newName); /** * Attempt to locate this file by systematically resolving and then * checking each search path. */ void locateFile(); /** * "Forget" the currently located file if one has been found. */ void forgetFile(); /** * Attempt to resolve a path to (and maybe locate) this file. * * @param tryLocate @c true= Attempt to locate the file now. * * @return Path to the found file or an empty string. * * @see locateFile() */ String const &resolvedPath(bool tryLocate = true); private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_RESOURCE_MANIFEST_H doomsday-stable-1.15.7/doomsday/client/include/resource/fontscheme.h0000664000175000017500000000754312641367670025060 0ustar jaakkojaakko/** @file fontscheme.h Font resource scheme. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_FONTSCHEME_H #define DENG_RESOURCE_FONTSCHEME_H #include "FontManifest" #include #include #include #include #include namespace de { /** * Font collection resource subspace. * * @see Fonts * @ingroup resource */ class FontScheme { typedef class FontManifest Manifest; public: /// The requested manifests could not be found in the index. DENG2_ERROR(NotFoundError); /// The specified path was not valid. @ingroup errors DENG2_ERROR(InvalidPathError); /// Notified whenever a new manifest is defined in the scheme. DENG2_DEFINE_AUDIENCE(ManifestDefined, void fontSchemeManifestDefined(FontScheme &scheme, Manifest &manifest)) /// Minimum length of a symbolic name. static int const min_name_length = DENG2_URI_MIN_SCHEME_LENGTH; /// Manifests in the scheme are placed into a tree. typedef PathTreeT Index; public: /** * Construct a new (empty) resource subspace scheme. * * @param symbolicName Symbolic name of the new subspace scheme. Must have * at least @ref min_name_length characters. */ FontScheme(String symbolicName); /// @return Symbolic name of this scheme (e.g., "System"). String const &name() const; /// @return Total number of manifests in the scheme. inline int size() const { return index().size(); } /// @return Total number of manifests in the scheme. Same as @ref size(). inline int count() const { return size(); } /** * Destroys all manifests (and any associated resources) in the scheme. */ void clear(); /** * Insert a new manifest at the given @a path into the scheme. If a manifest * already exists at this path, the existing manifest is returned. * * @param path Virtual path for the resultant manifest. * * @return The (possibly newly created) manifest at @a path. */ Manifest &declare(Path const &path); /** * Determines if a manifest exists on the given @a path. * * @return @c true if a manifest exists; otherwise @a false. */ bool has(Path const &path) const; /** * Search the scheme for a manifest matching @a path. * * @return Found manifest. */ Manifest const &find(Path const &path) const; /// @copydoc find() Manifest &find(Path const &path); /** * Search the scheme for a manifest whose associated unique identifier * matches @a uniqueId. * * @return Found manifest. */ Manifest const &findByUniqueId(int uniqueId) const; /// @copydoc findByUniqueId() Manifest &findByUniqueId(int uniqueId); /** * Provides access to the manifest index for efficient traversal. */ Index const &index() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_RESOURCE_FONTSCHEME_H doomsday-stable-1.15.7/doomsday/client/include/resource/materialshinelayer.h0000664000175000017500000000527512641367670026607 0ustar jaakkojaakko/** @file materialshinelayer.h Logical material, shine/reflection layer. * * @authors Copyright © 2011-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_RESOURCE_MATERIALSHINELAYER_H #define CLIENT_RESOURCE_MATERIALSHINELAYER_H #include #include #include "resource/materialtexturelayer.h" /** * Specialized MaterialTextureLayer for describing an animated shine/reflection layer. * * @ingroup resource */ class MaterialShineLayer : public MaterialTextureLayer { public: /** * Stages describe texture change animations. */ class AnimationStage : public MaterialTextureLayer::AnimationStage { public: AnimationStage(de::Uri const &texture, int tics, float variance, de::Uri const &maskTexture = de::Uri(), blendmode_t blendMode = BM_ADD, float opacity = 1, de::Vector3f const &minColor = de::Vector3f(0, 0, 0), de::Vector2f const &maskDimensions = de::Vector2f(1, 1)); AnimationStage(AnimationStage const &other); virtual ~AnimationStage(); /** * Construct a new AnimationStage from the given @a definition. */ static AnimationStage *fromDef(ded_shine_stage_t const &definition); void resetToDefaults(); }; public: MaterialShineLayer(); virtual ~MaterialShineLayer(); /** * Construct a new layer from the specified definition. */ static MaterialShineLayer *fromDef(ded_reflection_t const &def); /** * Add a new animation Stage to the layer. * * @param stage New Stage to add (a copy is made). * * @return Index of the newly added stage (0 based). */ int addStage(AnimationStage const &stage); de::String describe() const; }; #endif // CLIENT_RESOURCE_MATERIALSHINELAYER_H doomsday-stable-1.15.7/doomsday/client/include/resource/materialtexturelayer.h0000664000175000017500000000626012641367670027174 0ustar jaakkojaakko/** @file materialtexturelayer.h Logical material, texture layer. * * @authors Copyright © 2011-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_RESOURCE_MATERIALTEXTURELAYER_H #define CLIENT_RESOURCE_MATERIALTEXTURELAYER_H #include #include #include #include "Material" /** * Specialized MaterialLayer for describing an animated texture layer. * * @ingroup resource */ class MaterialTextureLayer : public MaterialLayer { public: /** * Stages describe texture change animations. */ class AnimationStage : public de::Record, public Stage { public: AnimationStage(de::Uri const &texture, int tics, float variance = 0, float glowStrength = 0, float glowStrengthVariance = 0, de::Vector2f const origin = de::Vector2f(), de::Uri const &maskTexture = de::Uri(), de::Vector2f const &maskDimensions = de::Vector2f(1, 1), blendmode_t blendMode = BM_NORMAL, float opacity = 1); AnimationStage(AnimationStage const &other); virtual ~AnimationStage(); virtual void resetToDefaults(); /** * Construct a new AnimationStage from the given @a stageDef. */ static AnimationStage *fromDef(de::Record const &stageDef); de::String description() const; }; public: virtual ~MaterialTextureLayer() {} /** * Construct a new TextureLayer from the given @a layerDef. */ static MaterialTextureLayer *fromDef(de::Record const &layerDef); /** * Returns @c true if glow is enabled for one or more animation stages. */ bool hasGlow() const; /** * Add a new animation stage to the texture layer. * * @param stage New stage to add (a copy is made). * * @return Index of the newly added stage (0 based). */ int addStage(AnimationStage const &stage); /** * Lookup an AnimationStage by it's unique @a index. * * @param index Index of the AnimationStage to lookup. Will be cycled into valid range. */ AnimationStage &stage(int index) const; de::String describe() const; }; #endif // CLIENT_RESOURCE_MATERIALTEXTURELAYER_H doomsday-stable-1.15.7/doomsday/client/include/resource/materialvariantspec.h0000664000175000017500000000536312641367670026761 0ustar jaakkojaakko/** @file materialvariantspec.h Logical material, draw-context variant specification. * * @authors Copyright © 2011-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CLIENT_RESOURCE_MATERIALVARIANTSPEC_H #define CLIENT_RESOURCE_MATERIALVARIANTSPEC_H #ifndef __CLIENT__ # error "resource/materialvariantspec.h only exists in the Client" #endif #include "MaterialContext" #include "Texture" // TextureVariantSpec namespace de { /** * Specialization specification for a variant material. * * Property values are public for user convenience. * * @see Material, MaterialVariant * @ingroup resource */ struct MaterialVariantSpec { public: /// Usage context identifier. MaterialContextId contextId { FirstMaterialContextId }; /// Interned specification for the primary texture. TextureVariantSpec const *primarySpec = nullptr; MaterialVariantSpec() {} MaterialVariantSpec(MaterialVariantSpec const &other) : contextId (other.contextId) , primarySpec(other.primarySpec) {} /** * Determines whether specification @a other is equal to this specification. * * @param other The other specification. * @return @c true if specifications are equal; otherwise @c false. * * Same as operator == */ bool compare(MaterialVariantSpec const &other) const { if(this == &other) return true; if(contextId != other.contextId) return false; return primarySpec == other.primarySpec; } /** * Determines whether specification @a other is equal to this specification. * @see compare() */ bool operator == (MaterialVariantSpec const &other) const { return compare(other); } /** * Determines whether specification @a other is NOT equal to this specification. * @see compare() */ bool operator != (MaterialVariantSpec const &other) const { return !(*this == other); } }; } // namespace de #endif // CLIENT_RESOURCE_MATERIALVARIANTSPEC_H doomsday-stable-1.15.7/doomsday/client/include/resource/animgroup.h0000664000175000017500000000734112641367670024722 0ustar jaakkojaakko/** @file animgroup.h Material animation group. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_ANIMATIONGROUP_H #define DENG_RESOURCE_ANIMATIONGROUP_H #include "dd_types.h" #include "TextureManifest" #include namespace de { /** * Material Animation group. * * @ingroup resource */ class AnimGroup { public: /** * A single frame in the animation. */ struct Frame { public: /** * Returns the texture manifest for the frame. */ TextureManifest &textureManifest() const; /** * Returns the duration of the frame in tics. */ ushort tics() const; /** * Returns the additional duration of the frame in tics. */ ushort randomTics() const; friend class AnimGroup; private: Frame(TextureManifest &textureManifest, ushort tics, ushort randomTics); TextureManifest *_textureManifest; ushort _tics; ushort _randomTics; }; typedef QList Frames; public: /** * Construct a new animation group. * * @param uniqueId Unique identifier to associate with the group. * @param flags @ref animationGroupFlags */ AnimGroup(int uniqueId, int flags = 0); /** * Returns the unique identifier associated with the animation. */ int id() const; /** * @return @ref animationGroupFlags */ int flags() const; /** * Returns @c true iff at least one frame in the animation uses the specified * @a textureManifest * * @see frames() */ bool hasFrameFor(TextureManifest const &textureManifest) const; /** * Append a new frame to the animation. * * @param texture Manifest for the texture to use during the frame. * @param tics Duration of the frame in tics. * @param randomTics Additional random duration of the frame in tics. * * @return The new frame. */ Frame &newFrame(TextureManifest &textureManifest, ushort tics, ushort randomTics = 0); /** * Clear all frames in the animation. */ void clearAllFrames(); /** * Returns the total number of frames in the animation. */ inline int frameCount() const { return allFrames().count(); } /** * Convenient method of returning a frame in the animation by @a index. * It is assumed that the index is within valid [0..frameCount) range. * * @see frameCount() */ inline Frame &frame(int index) const { return *allFrames().at(index); } /** * Provides access to the frame list for efficient traversal. * * @see frame() */ Frames const &allFrames() const; private: DENG2_PRIVATE(d) }; typedef AnimGroup::Frame AnimGroupFrame; } // namespace de #endif // DENG_RESOURCE_ANIMATIONGROUP_H doomsday-stable-1.15.7/doomsday/client/include/resource/texturemanifest.h0000664000175000017500000001547612641367670026160 0ustar jaakkojaakko/** @file texturemanifest.h Description of a logical texture resource. * * @authors Copyright © 2010-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_TEXTUREMANIFEST_H #define DENG_RESOURCE_TEXTUREMANIFEST_H #include "Texture" #include #include #include #include #include namespace de { class TextureScheme; /** * Description for a would-be logical Texture resource. * * Models a reference to and the associated metadata for a logical texture * in the texture resource collection. * * @see TextureScheme, Texture * @ingroup resource */ class TextureManifest : public PathTree::Node { public: /// Required texture instance is missing. @ingroup errors DENG2_ERROR(MissingTextureError); /// Required resource URI is not defined. @ingroup errors DENG2_ERROR(MissingResourceUriError); DENG2_DEFINE_AUDIENCE(Deletion, void textureManifestBeingDeleted(TextureManifest const &manifest)) DENG2_DEFINE_AUDIENCE(UniqueIdChange, void textureManifestUniqueIdChanged(TextureManifest &manifest)) DENG2_DEFINE_AUDIENCE(TextureDerived, void textureManifestTextureDerived(TextureManifest &manifest, Texture &texture)) public: TextureManifest(PathTree::NodeArgs const &args); /** * Derive a new logical Texture instance by interpreting the manifest. * The first time a texture is derived from the manifest, said texture * is assigned to the manifest (ownership is assumed). */ Texture *derive(); /** * Returns the owning scheme of the manifest. */ TextureScheme &scheme() const; /// Convenience method for returning the name of the owning scheme. String const &schemeName() const; /** * Compose a URI of the form "scheme:path" for the TextureManifest. * * The scheme component of the URI will contain the symbolic name of * the scheme for the TextureManifest. * * The path component of the URI will contain the percent-encoded path * of the TextureManifest. */ inline Uri composeUri(QChar sep = '/') const { return Uri(schemeName(), path(sep)); } /** * Compose a URN of the form "urn:scheme:uniqueid" for the texture * TextureManifest. * * The scheme component of the URI will contain the identifier 'urn'. * * The path component of the URI is a string which contains both the * symbolic name of the scheme followed by the unique id of the texture * TextureManifest, separated with a colon. * * @see uniqueId(), setUniqueId() */ inline Uri composeUrn() const { return Uri("urn", String("%1:%2").arg(schemeName()).arg(uniqueId(), 0, 10)); } /** * Returns a textual description of the manifest. * * @return Human-friendly description the manifest. */ String description(Uri::ComposeAsTextFlags uriCompositionFlags = Uri::DefaultComposeAsTextFlags) const; /** * Returns a textual description of the source of the manifest. * * @return Human-friendly description of the source of the manifest. */ String sourceDescription() const; /** * Returns @c true if a URI to an associated resource is defined. */ bool hasResourceUri() const; /** * Returns the URI to the associated resource. */ Uri resourceUri() const; /** * Change the resource URI associated with the manifest. * * @return @c true iff @a newUri differed to the existing URI, which * was subsequently changed. */ bool setResourceUri(Uri const &newUri); /** * Returns the scheme-unique identifier for the manifest. */ int uniqueId() const; /** * Change the unique identifier property of the manifest. * * @return @c true iff @a newUniqueId differed to the existing unique * identifier, which was subsequently changed. */ bool setUniqueId(int newUniqueId); /** * Returns the logical dimensions property of the manifest. */ Vector2i const &logicalDimensions() const; /** * Change the logical dimensions property of the manifest. * * @param newDimensions New logical dimensions. Components can be @c 0 in * which case their value will be inherited from the pixel dimensions of * the image at load time. */ bool setLogicalDimensions(Vector2i const &newDimensions); /** * Returns the world origin offset property of the manifest. */ Vector2i const &origin() const; /** * Change the world origin offest property of the manifest. * * @param newOrigin New origin offset. */ void setOrigin(Vector2i const &newOrigin); /** * Returns the texture flags property of the manifest. */ Texture::Flags flags() const; /** * Change the texture flags property of the manifest. * * @param flagsToChange Flags to change the value of. * @param operation Logical operation to perform on the flags. */ void setFlags(Texture::Flags flagsToChange, FlagOp operation = de::SetFlags); /** * Returns @c true if a Texture is presently associated with the manifest. * * @see texture(), texturePtr() */ bool hasTexture() const; /** * Returns the logical Texture associated with the manifest. * * @see hasTexture() */ Texture &texture() const; /** * Returns a pointer to the associated Texture resource; otherwise @c 0. * * @see hasTexture() */ inline Texture *texturePtr() const { return hasTexture()? &texture() : 0; } /** * Change the logical Texture associated with the manifest. * * @param newTexture New logical Texture to associate. */ void setTexture(Texture *newTexture); /** * Clear the logical Texture associated with the manifest. * * Same as @c setTexture(0) */ inline void clearTexture() { setTexture(0); } private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_RESOURCE_TEXTUREMANIFEST_H doomsday-stable-1.15.7/doomsday/client/include/resource/modeldef.h0000664000175000017500000001444612641367670024504 0ustar jaakkojaakko/** @file modeldef.h 3D model resource definition. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_MODELDEF_H #define DENG_RESOURCE_MODELDEF_H #include #include #include #include #include "resource/model.h" /** * @defgroup modelFrameFlags Model frame flags * @ingroup flags */ ///@{ #define MFF_FULLBRIGHT 0x00000001 #define MFF_SHADOW1 0x00000002 #define MFF_SHADOW2 0x00000004 #define MFF_BRIGHTSHADOW 0x00000008 #define MFF_MOVEMENT_PITCH 0x00000010 ///< Pitch aligned to movement. #define MFF_SPIN 0x00000020 ///< Spin around (for bonus items). #define MFF_SKINTRANS 0x00000040 ///< Color translation -> skins. #define MFF_AUTOSCALE 0x00000080 ///< Scale to match sprite height. #define MFF_MOVEMENT_YAW 0x00000100 #define MFF_DONT_INTERPOLATE 0x00000200 ///< Don't interpolate from the frame. #define MFF_BRIGHTSHADOW2 0x00000400 #define MFF_ALIGN_YAW 0x00000800 #define MFF_ALIGN_PITCH 0x00001000 #define MFF_DARKSHADOW 0x00002000 #define MFF_IDSKIN 0x00004000 ///< Mobj id -> skin in skin range #define MFF_DISABLE_Z_WRITE 0x00008000 #define MFF_NO_DISTANCE_CHECK 0x00010000 #define MFF_SELSKIN 0x00020000 #define MFF_PARTICLE_SUB1 0x00040000 ///< Sub1 center is particle origin. #define MFF_NO_PARTICLES 0x00080000 ///< No particles for this object. #define MFF_SHINY_SPECULAR 0x00100000 ///< Shiny skin rendered as additive. #define MFF_SHINY_LIT 0x00200000 ///< Shiny skin is not fullbright. #define MFF_IDFRAME 0x00400000 ///< Mobj id -> frame in frame range #define MFF_IDANGLE 0x00800000 ///< Mobj id -> static angle offset #define MFF_DIM 0x01000000 ///< Never fullbright. #define MFF_SUBTRACT 0x02000000 ///< Subtract blending. #define MFF_REVERSE_SUBTRACT 0x04000000 ///< Reverse subtract blending. #define MFF_TWO_SIDED 0x08000000 ///< Disable culling. #define MFF_NO_TEXCOMP 0x10000000 ///< Never compress skins. #define MFF_WORLD_TIME_ANIM 0x20000000 ///@} struct SubmodelDef { modelid_t modelId; short frame; char frameRange; int _flags; short skin; char skinRange; de::Vector3f offset; byte alpha; de::Texture *shinySkin; blendmode_t blendMode; SubmodelDef() : modelId(0) , frame(0) , frameRange(0) , _flags(0) , skin(0) , skinRange(0) , alpha(0) , shinySkin(0) , blendMode(BM_NORMAL) {} void setFlags(int newFlags) { _flags = newFlags; } /** * Tests if the flags in @a flag are all set for the submodel. * * @param flag One or more flags. * * @return @c true, if all the flags were set; otherwise @c false. */ bool testFlag(int flag) const { return (_flags & flag) == flag; } }; #define MODELDEF_ID_MAXLEN 32 struct ModelDef { char id[MODELDEF_ID_MAXLEN + 1]; /// Pointer to the states list. state_t *state = nullptr; int flags = 0; uint group = 0; int select = 0; short skinTics = 0; /// [0,1) When is this frame in effect? float interMark = 0; float interRange[2]; de::Vector3f offset; float resize = 0; de::Vector3f scale; typedef std::vector PtcOffsets; PtcOffsets _ptcOffset; float visualRadius = 0; float shadowRadius = 0; // if zero, visual radius used instead defn::Model def; /// Points to next inter-frame, or NULL. ModelDef *interNext = nullptr; /// Points to next selector, or NULL (only for "base" modeldefs). ModelDef *selectNext = nullptr; /// Submodels. typedef std::vector Subs; Subs _sub; ModelDef(char const *modelDefId = "") { de::zap(id); de::zap(interRange); strncpy(id, modelDefId, MODELDEF_ID_MAXLEN); } SubmodelDef *addSub() { _sub.push_back(SubmodelDef()); _ptcOffset.push_back(de::Vector3f()); return &_sub.back(); } void clearSubs() { _sub.clear(); _ptcOffset.clear(); } uint subCount() const { return _sub.size(); } bool testSubFlag(unsigned int subnum, int flag) const { if(!hasSub(subnum)) return false; return _sub[subnum].testFlag(flag); } modelid_t subModelId(unsigned int subnum) const { if(!hasSub(subnum)) return NOMODELID; return _sub[subnum].modelId; } SubmodelDef &subModelDef(unsigned int subnum) { DENG2_ASSERT(hasSub(subnum)); return _sub[subnum]; } SubmodelDef const &subModelDef(unsigned int subnum) const { DENG2_ASSERT(hasSub(subnum)); return _sub[subnum]; } bool hasSub(unsigned int subnum) const { return subnum < _sub.size(); } de::Vector3f particleOffset(unsigned int subnum) const { if(hasSub(subnum)) { DENG2_ASSERT(subnum < _ptcOffset.size()); return _ptcOffset[subnum]; } return de::Vector3f(); } void setParticleOffset(unsigned int subnum, de::Vector3f const &off) { DENG2_ASSERT(hasSub(subnum)); _ptcOffset[subnum] = off; } }; #endif // DENG_RESOURCE_MODELDEF_H doomsday-stable-1.15.7/doomsday/client/include/resource/materialarchive.h0000664000175000017500000000704412641367670026061 0ustar jaakkojaakko/** @file materialarchive.h Collection of identifier-material pairs. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_RESOURCE_MATERIALARCHIVE_H #define LIBDENG_RESOURCE_MATERIALARCHIVE_H #include #include #include class Material; namespace de { /** * Collection of identifier-material pairs. * * Used when saving map state (savegames) or sharing world changes with clients. * * @ingroup resource */ class MaterialArchive { public: /// Base class for all deserialization errors. @ingroup errors DENG2_ERROR(ReadError); public: /** * @param useSegments If @c true, the serialized archive will be preceded * by a segment id number. * @param recordSymbolicMaterials Add records for the symbolic materials * used to record special references in the serialized archive. */ MaterialArchive(int useSegments, bool recordSymbolicMaterials = true); /** * Returns the number of materials in the archive. */ int count() const; /** * Returns the number of materials in the archive. * Same as count() */ inline int size() const { return count(); } /** * @return A new (unused) SerialId for the specified material. */ materialarchive_serialid_t findUniqueSerialId(Material *mat) const; /** * Finds and returns a material with the identifier @a serialId. * * @param serialId SerialId of a material. * @param group Set to zero. Only used with the version 0 of MaterialArchive (now obsolete). * * @return Pointer to a material instance. Ownership not given. */ Material *find(materialarchive_serialid_t serialId, int group) const; /** * Insert the specified @a material into the archive. If this material * is already present the existing serial ID is returned and the archive * is unchanged. * * @param material The material to be recorded. * @return Unique SerialId of the recorded material. */ materialarchive_serialid_t addRecord(Material const &material); /** * Serializes the state of the archive using @a writer. * * @param writer Writer instance. */ void write(writer_s &writer) const; /** * Deserializes the state of the archive from @a reader. * * @param reader Reader instance. * @param forcedVersion Version to interpret as, not actual format version. * Use -1 to use whatever version is encountered. */ void read(reader_s &reader, int forcedVersion = -1); private: DENG2_PRIVATE(d) }; } // namespace de #endif /* LIBDENG_RESOURCE_MATERIALARCHIVE_H */ doomsday-stable-1.15.7/doomsday/client/include/resource/texture.h0000664000175000017500000003074512641367670024425 0ustar jaakkojaakko/** @file texture.h Logical texture resource. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_TEXTURE_H #define DENG_RESOURCE_TEXTURE_H #ifdef __CLIENT__ # include "resource/image.h" // res::Source # include "TextureVariantSpec" #endif #include #include #include #include #include namespace de { class TextureManifest; /** * Logical texture resource. * * @ingroup resource */ class Texture { struct Instance; // Needs to be friended by Variant. public: DENG2_DEFINE_AUDIENCE(Deletion, void textureBeingDeleted(Texture const &texture)) DENG2_DEFINE_AUDIENCE(DimensionsChange, void textureDimensionsChanged(Texture const &texture)) /** * Classification/processing flags. */ enum Flag { /// Texture is not to be drawn. NoDraw = 0x1, /// Texture is "custom" (i.e., not an original game resource). Custom = 0x2, /// Apply the monochrome filter to the processed image. Monochrome = 0x4, /// Apply the upscaleAndSharpen filter to the processed image. UpscaleAndSharpen = 0x8 }; Q_DECLARE_FLAGS(Flags, Flag) /** * Image analysis identifiers. */ enum AnalysisId { /// Color palette info. ColorPaletteAnalysis, /// Brightest point for automatic light sources. BrightPointAnalysis, /// Average color. AverageColorAnalysis, /// Average color amplified (max component ==1). AverageColorAmplifiedAnalysis, /// Average alpha. AverageAlphaAnalysis, /// Average top line color. AverageTopColorAnalysis, /// Average bottom line color. AverageBottomColorAnalysis }; #ifdef __CLIENT__ /** * Context-specialized variant. Encapsulates all context variant values * and logics pertaining to a specialized version of the @em superior * Texture instance. * * @see TextureVariantSpec */ class Variant { public: enum Flag { /// Texture contains alpha. /// @todo Does not belong here (is actually a source image analysis). Masked = 0x1 }; Q_DECLARE_FLAGS(Flags, Flag) private: /** * @param texture Base Texture from which the draw-context variant is derived. * @param spec Draw-context variant specification. */ Variant(Texture &texture, TextureVariantSpec const &spec); public: /** * Returns the base Texture for the draw-context variant. */ Texture &base() const; /// Returns @c true if the variant is "prepared". inline bool isPrepared() const { return glName() != 0; } /// Returns @c true if the variant is flagged as "masked". inline bool isMasked() const { return isFlagged(Masked); } /** * Prepare the texture variant for render. * * @note If a cache miss occurs texture content data may need to be * (re-)uploaded to GL. However, the actual upload will be deferred * if possible. This has the side effect that although the variant * is considered "prepared", attempts to render using the associated * GL texture will result in "uninitialized" white texels being used * instead. * * @return GL-name of the uploaded texture. */ uint prepare(); /** * Release any uploaded GL-texture and clear the associated GL-name * for the variant. */ void release(); /** * Returns the specification used to derive the variant. */ TextureVariantSpec const &spec() const; /** * Returns the source of the image used to prepare the uploaded GL-texture * for the variant. */ res::Source source() const; /** * Returns a textual description of the source of the variant. * * @return Human-friendly description of the source of the variant. */ String sourceDescription() const; /** * Returns the flags for the variant. */ Flags flags() const; /** * Returns @c true if the variant is flagged @a flagsToTest. */ inline bool isFlagged(Flags flagsToTest) const { return (flags() & flagsToTest) != 0; } /** * Returns the GL-name of the uploaded texture content for the variant; * otherwise @c 0 (not uploaded). */ uint glName() const; /** * Returns the prepared GL-texture coordinates for the variant. * * @param s S axis coordinate. * @param t T axis coordinate. */ void glCoords(float *s, float *t) const; friend class Texture; friend struct Texture::Instance; private: DENG2_PRIVATE(d) }; /// A list of variants. typedef QList Variants; /** * Logics for selecting a texture variant instance from the candidates. * * @see chooseVariant() */ enum ChooseVariantMethod { /// The variant specification of the candidate must match exactly. MatchSpec, /// The variant specification of the candidate must match however /// certain properties may vary (e.g., quality arguments) if it means /// we can avoid creating a new variant. FuzzyMatchSpec }; #endif // __CLIENT__ public: /** * @param manifest Manifest derived to yield the texture. */ Texture(TextureManifest &manifest); ~Texture(); /** * Returns the TextureManifest derived to yield the texture. */ TextureManifest &manifest() const; /** * Returns a brief textual description/overview of the texture. * * @return Human-friendly description/overview of the texture. */ String description() const; /** * Returns the world dimensions of the texture, in map coordinate space * units. The DimensionsChange audience is notified whenever dimensions * are changed. */ Vector2i const &dimensions() const; /** * Convenient accessor method for returning the X axis size (width) of * the world dimensions for the texture, in map coordinate space units. * * @see dimensions() */ inline int width() const { return dimensions().x; } /** * Convenient accessor method for returning the X axis size (height) of * the world dimensions for the texture, in map coordinate space units. * * @see dimensions() */ inline int height() const { return dimensions().y; } /** * Change the world dimensions of the texture. * @param newDimensions New dimensions in map coordinate space units. * * @todo Update any Materials (and thus Surfaces) which reference this. */ void setDimensions(Vector2i const &newDimensions); /** * Change the world width of the texture. * @param newWidth New width in map coordinate space units. * * @todo Update any Materials (and thus Surfaces) which reference this. */ void setWidth(int newWidth); /** * Change the world height of the texture. * @param newHeight New height in map coordinate space units. * * @todo Update any Materials (and thus Surfaces) which reference this. */ void setHeight(int newHeight); /** * Returns the world origin offset of texture in map coordinate space units. */ Vector2i const &origin() const; /** * Change the world origin offset of the texture. * @param newOrigin New origin in map coordinate space units. */ void setOrigin(Vector2i const &newOrigin); /** * Returns @c true if the texture is flagged @a flagsToTest. */ inline bool isFlagged(Flags flagsToTest) const { return !!(flags() & flagsToTest); } /** * Returns the flags for the texture. */ Flags flags() const; /** * Change the texture's flags. * * @param flagsToChange Flags to change the value of. * @param operation Logical operation to perform on the flags. */ void setFlags(Flags flagsToChange, de::FlagOp operation = de::SetFlags); #ifdef __CLIENT__ /** * Destroys all derived variants for the texture. */ void clearVariants(); /** * Choose/create a variant of the texture which fulfills @a spec. * * @param method Method of selection. * @param spec Texture specialization specification. * @param canCreate @c true= Create a new variant if no suitable one exists. * * @return Chosen variant; otherwise @c NULL if none suitable and not creating. */ Variant *chooseVariant(ChooseVariantMethod method, TextureVariantSpec const &spec, bool canCreate = false); /** * Choose/create a variant of the texture which fulfills @a spec and then * immediately prepare it for render. * * @note A convenient shorthand of the call tree: *

     *    chooseVariant(MatchSpec, @a spec, true)->prepareVariant();
     * 
* * @param spec Specification for the derivation of the texture. * * @return The prepared texture variant if successful; otherwise @c 0. * * @see chooseVariant() */ Variant *prepareVariant(TextureVariantSpec const &spec); /** * Provides access to the list of variant instances for efficent traversal. */ Variants const &variants() const; /** * Returns the number of variants for the texture. */ uint variantCount() const; /** * Release prepared GL-textures for identified variants. * * @param spec If non-zero release only for variants derived with this spec. */ void releaseGLTextures(TextureVariantSpec *spec = 0); #endif // __CLIENT__ /** * Destroys all analyses for the texture. */ void clearAnalyses(); /** * Retrieve the value of an identified @a analysisId data pointer. * @return Associated data pointer value. */ void *analysisDataPointer(AnalysisId analysisId) const; /** * Set the value of an identified @a analysisId data pointer. Ownership of * the data is not given to this instance. * * @note If already set the old value will be replaced (so if it points * to some dynamically constructed data/resource it is the caller's * responsibility to release it beforehand). * * @param analysisId Identifier of the data being attached. * @param data Data to be attached. */ void setAnalysisDataPointer(AnalysisId analysisId, void *data); /** * Retrieve the value of the associated user data pointer. * @return Associated data pointer value. */ void *userDataPointer() const; /** * Set the user data pointer value. Ownership of the data is not given to * this instance. * * @note If already set the old value will be replaced (so if it points * to some dynamically constructed data/resource it is the caller's * responsibility to release it beforehand). * * @param userData User data pointer value. */ void setUserDataPointer(void *userData); public: /// Register the console commands, variables, etc..., of this module. static void consoleRegister(); private: Instance *d; }; Q_DECLARE_OPERATORS_FOR_FLAGS(Texture::Flags) #ifdef __CLIENT__ // Alias. typedef Texture::Variant TextureVariant; #endif } // namespace de #endif // DENG_RESOURCE_TEXTURE_H doomsday-stable-1.15.7/doomsday/client/include/resource/materialscheme.h0000664000175000017500000000671712641367670025712 0ustar jaakkojaakko/** @file materialscheme.h Material collection subspace. * * @authors Copyright © 2010-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_MATERIALSCHEME_H #define DENG_RESOURCE_MATERIALSCHEME_H #include #include #include #include #include "MaterialManifest" namespace de { /** * Material collection subspace. * * @see Material * @ingroup resource */ class MaterialScheme { typedef class MaterialManifest Manifest; public: /// The requested manifest could not be found in the index. @ingroup errors DENG2_ERROR(NotFoundError); /// The specified path was not valid. @ingroup errors DENG2_ERROR(InvalidPathError); DENG2_DEFINE_AUDIENCE(ManifestDefined, void materialSchemeManifestDefined(MaterialScheme &scheme, Manifest &manifest)) /// Minimum length of a symbolic name. static int const min_name_length = DENG2_URI_MIN_SCHEME_LENGTH; /// Manifests in the scheme are placed into a tree. typedef PathTreeT Index; public: /** * Construct a new (empty) material subspace scheme. * * @param symbolicName Symbolic name of the new subspace scheme. Must * have at least @ref min_name_length characters. */ explicit MaterialScheme(String symbolicName); /// @return Symbolic name of this scheme (e.g., "Flats"). String const &name() const; /// @return Total number of manifests in the scheme. inline int size() const { return index().size(); } /// @return Total number of manifests in the scheme. Same as @ref size(). inline int count() const { return size(); } /** * Clear all manifests in the scheme. */ void clear(); /** * Insert a new manifest at the given @a path into the scheme. * If a manifest already exists at this path, the existing manifest is * returned and the call is a no-op. * * @param path Virtual path for the resultant manifest. * @return The (possibly newly created) manifest at @a path. */ Manifest &declare(Path const &path); /** * Determines if a manifest exists on the given @a path. * @return @c true if a manifest exists; otherwise @a false. */ bool has(Path const &path) const; /** * Search the scheme for a manifest matching @a path. * * @return Found manifest. */ Manifest const &find(Path const &path) const; /// @copydoc find() Manifest &find(Path const &path); /** * Provides access to the manifest index for efficient traversal. */ Index const &index() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_RESOURCE_MATERIALSCHEME_H doomsday-stable-1.15.7/doomsday/client/include/resource/bitmapfont.h0000664000175000017500000000340312641367670025057 0ustar jaakkojaakko/** @file bitmapfont.h Bitmap font. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_RESOURCE_BITMAPFONT_H #define CLIENT_RESOURCE_BITMAPFONT_H #include "abstractfont.h" #include #include #include /** * Bitmap font. * * @ingroup resource */ class BitmapFont : public AbstractFont { public: BitmapFont(de::FontManifest &manifest); static BitmapFont *fromFile(de::FontManifest &manifest, de::String resourcePath); void setFilePath(de::String resourcePath); /// @return GL-texture name. uint textureGLName() const; de::Vector2i const &textureDimensions() const; de::Vector2ui const &textureMargin() const; int ascent(); int descent(); int lineSpacing(); void glInit(); void glDeinit(); de::Rectanglei const &glyphPosCoords(uchar ch); de::Rectanglei const &glyphTexCoords(uchar ch); private: DENG2_PRIVATE(d) }; #endif // CLIENT_RESOURCE_BITMAPFONT_H doomsday-stable-1.15.7/doomsday/client/include/resource/patchname.h0000664000175000017500000000352112641367670024655 0ustar jaakkojaakko/** * @file patchname.h PatchName * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_RESOURCE_PATCHNAME_H #define LIBDENG_RESOURCE_PATCHNAME_H #include "dd_types.h" // For lumpnum_t #include #include #include namespace de { /** * @ingroup resource */ class PatchName : public IReadable { public: explicit PatchName(String percentEncodedName = "", lumpnum_t _lumpNum = -2); /// Returns the percent-endcoded symbolic name of the patch. String percentEncodedName() const; /// Returns the percent-endcoded symbolic name of the patch. String const &percentEncodedNameRef() const; /// Returns the lump number of the associated patch. /// @pre The global patchNames data is available. lumpnum_t lumpNum(); /// Implements IReadable. void operator << (Reader &from); private: String name; lumpnum_t lumpNum_; }; } // namespace de #endif /* LIBDENG_RESOURCE_PATCHNAME_H */ doomsday-stable-1.15.7/doomsday/client/include/resource/material.h0000664000175000017500000003334712641367670024524 0ustar jaakkojaakko/** @file material.h Logical material resource. * * @authors Copyright © 2009-2014 Daniel Swanson * @authors Copyright © 2009-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_MATERIAL_H #define DENG_RESOURCE_MATERIAL_H #include #include #include #include #include #include "audio/s_environ.h" #include "MapElement" #include "world/dmuargs.h" #ifdef __CLIENT__ # include "MaterialVariantSpec" #endif #include "Texture" class MaterialManifest; #ifdef __CLIENT__ class MaterialAnimator; #endif /** * Logical material resource. * * @par Dimensions * Material dimensions are interpreted relative to the coordinate space in which * the material is used. For example, the dimensions of a Material in the map-surface * usage context are thought to be in "map/world space" units. * * @ingroup resource */ class Material : public de::MapElement { public: /// Notified when the material is about to be deleted. DENG2_DEFINE_AUDIENCE2(Deletion, void materialBeingDeleted(Material const &material)) /// Notified whenever the logical dimensions change. DENG2_DEFINE_AUDIENCE2(DimensionsChange, void materialDimensionsChanged(Material &material)) public: /** * Construct a new Material and attribute it with the given resource @a manifest. */ Material(MaterialManifest &manifest); ~Material(); /** * Returns the attributed MaterialManifest for the material. */ MaterialManifest &manifest() const; /** * Returns the dimension metrics of the material. */ de::Vector2i const &dimensions() const; inline int height() const { return dimensions().y; } inline int width () const { return dimensions().x; } /** * Change the world dimensions of the material to @a newDimensions. */ void setDimensions(de::Vector2i const &newDimensions); void setHeight(int newHeight); void setWidth (int newWidth); /** * Returns @c true if the material is marked @em drawable. */ bool isDrawable() const; /** * Returns @c true if the material is marked @em sky-masked. */ bool isSkyMasked() const; /** * Returns @c true if the material is marked @em valid. * * Materials are invalidated only when dependent resources (such as the definition * from which it was produced) are destroyed as a result of runtime file unloading. * * These 'orphaned' materials cannot be immediately destroyed as the game may be * holding on to pointers (which are considered eternal). Therefore, materials are * invalidated (disabled) and will be ignored until they can actually be destroyed * (e.g., the current game is reset or changed). */ bool isValid() const; /** * Change the do-not-draw property of the material according to @a yes. */ void markDontDraw (bool yes = true); /** * Change the sky-masked property of the material according to @a yes. */ void markSkyMasked(bool yes = true); /** * Change the is-valid property of the material according to @a yes. */ void markValid (bool yes = true); /** * Returns a human-friendly, textual name for the object. */ de::String describe() const; /** * Returns a human-friendly, textual description of the full material configuration. */ de::String description() const; /** * Returns the attributed audio environment identifier for the material. */ AudioEnvironmentId audioEnvironment() const; /** * Change the attributed audio environment for the material to @a newEnvironment. */ void setAudioEnvironment(AudioEnvironmentId newEnvironment); public: // Layers -------------------------------------------------------------------- /// The referenced layer does not exist. @ingroup errors DENG2_ERROR(MissingLayerError); /** * Base class for modelling a logical layer. * * A layer in this context is a formalized extension mechanism for customizing the * visual composition of a material. Layers are primarily intended for the modelling * of animated texture layers. * * Each material is composed from one or more layers. Layers are arranged in a stack, * according to the order in which they should be drawn, from the bottom-most to * the top-most layer. */ class Layer { public: /// The referenced stage does not exist. @ingroup errors DENG2_ERROR(MissingStageError); /** * Base class for a logical layer animation stage. */ struct Stage { int tics; float variance; ///< Stage variance (time). Stage(int tics, float variance) : tics(tics), variance(variance) {} Stage(Stage const &other) : tics(other.tics), variance(other.variance) {} virtual ~Stage() {} DENG2_AS_IS_METHODS() /** * Returns a human-friendly, textual description of the animation stage * configuration. */ virtual de::String description() const = 0; }; public: virtual ~Layer(); DENG2_AS_IS_METHODS() /** * Returns a human-friendly, textual name for the type of material layer. */ virtual de::String describe() const; /** * Returns a human-friendly, textual synopsis of the material layer. */ de::String description() const; /** * Returns the total number of animation stages for the material layer. */ int stageCount() const; /** * Returns @c true if the material layer is animated; otherwise @c false. */ inline bool isAnimated() const { return stageCount() > 1; } /** * Lookup a material layer animation Stage by it's unique @a index. * * @param index Index of the stage to lookup. Will be cycled into valid range. */ Stage &stage(int index) const; protected: typedef QList Stages; Stages _stages; }; /** * Returns the number of material layers. */ int layerCount() const; /** * Add a new layer at the given layer stack position. * * @note As this alters the layer state, any existing client side MaterialAnimators * will need to be reconfigured/destroyed as they will no longer be valid. * * @param layer Layer to add. Material takes ownership. * @param index Numeric position in the layer stack at which to add the layer. */ void addLayerAt(Layer *layer, int index); /** * Lookup a Layer by it's unique @a index. */ Layer &layer (int index) const; Layer *layerPtr(int index) const; /** * Destroys all the material's layers. * * @note As this alters the layer state, any existing client side MaterialAnimators * will need to be reconfigured/destroyed as they will no longer be valid. */ void clearAllLayers(); #ifdef __CLIENT__ public: // Decorations --------------------------------------------------------------- /// The referenced decoration does not exist. @ingroup errors DENG2_ERROR(MissingDecorationError); /** * Base class for modelling a logical "decoration". * * A decoration in this context is a formalized extension mechanism for "attaching" * additional objects to the material. Each material may have any number of attached * decorations. * * @par Skip Patterns * Normally each material decoration is repeated as many times as the material. * Meaning that for each time the material dimensions repeat on a given axis, each * of the decorations will be repeated also. * * A skip pattern allows for sparser repeats to be configured. The X and Y axes of * a skip pattern correspond to the horizontal and vertical axes of the material, * respectively. */ class Decoration { public: /// The referenced stage does not exist. @ingroup errors DENG2_ERROR(MissingStageError); /** * Base class for a logical decoration animation stage. */ struct Stage { int tics; float variance; ///< Stage variance (time). Stage(int tics, float variance) : tics(tics), variance(variance) {} Stage(Stage const &other) : tics(other.tics), variance(other.variance) {} virtual ~Stage() {} DENG2_AS_IS_METHODS() /** * Returns a human-friendly, textual description of the animation stage * configuration. */ virtual de::String description() const = 0; }; public: /** * Construct a new material Decoration with the given skip pattern configuration. */ Decoration(de::Vector2i const &patternSkip = de::Vector2i(), de::Vector2i const &patternOffset = de::Vector2i()); virtual ~Decoration(); DENG2_AS_IS_METHODS() /** * Returns a human-friendly, textual name for the type of material decoration. */ virtual de::String describe() const; /** * Returns a human-friendly, textual synopsis of the material decoration. */ de::String description() const; /** * Returns the Material 'owner' of the material decoration. */ Material &material(); Material const &material() const; void setMaterial(Material *newOwner); /** * Returns the pattern skip configuration for the decoration. * * @see patternOffset() */ de::Vector2i const &patternSkip() const; /** * Returns the pattern offset configuration for the decoration. * * @see patternSkip() */ de::Vector2i const &patternOffset() const; /** * Returns the total number of animation Stages for the decoration. */ int stageCount() const; /** * Returns @c true if the material decoration is animated; otherwise @c false. */ inline bool isAnimated() const { return stageCount() > 1; } /** * Add a @em copy of the given animation @a stage to the decoration. Ownership is * unaffected. * * @return Index of the newly added stage (0 based). */ int addStage(Stage const &stage); /** * Lookup an animation Stage by @a index. * * @param index Index of the stage to lookup. Will be cycled into valid range. */ Stage &stage(int index) const; protected: typedef QList Stages; Stages _stages; private: DENG2_PRIVATE(d) }; /** * Returns the number of material Decorations. */ int decorationCount() const; /** * Returns @c true if the material has one or more Decorations. */ inline bool hasDecorations() const { return decorationCount() > 0; } /** * Iterate through the material Decorations. * * @param func Callback to make for each Decoration. */ de::LoopResult forAllDecorations(std::function func) const; /** * Add a new (light) decoration to the material. * * @param decor Decoration to add. Ownership is given to Material. */ void addDecoration(Decoration *decor); /** * Destroys all the material's decorations. */ void clearAllDecorations(); public: // Animators ----------------------------------------------------------------- /** * Returns the total number of MaterialAnimators for the material. */ int animatorCount() const; /** * Determines if a MaterialAnimator exists for a material variant which fulfills @a spec. */ bool hasAnimator(de::MaterialVariantSpec const &spec); /** * Find/create an MaterialAnimator for a material variant which fulfils @a spec * * @param spec Specification for a material draw-context variant. * * @return The (possibly, newly created) Animator. */ MaterialAnimator &getAnimator(de::MaterialVariantSpec const &spec); /** * Iterate through all the MaterialAnimators for the material. * * @param func Callback to make for each Animator. */ de::LoopResult forAllAnimators(std::function func) const; /** * Destroy all the MaterialAnimators for the material. */ void clearAllAnimators(); #endif // __CLIENT__ protected: int property(DmuArgs &args) const; public: /// Register the console commands and variables of this module. static void consoleRegister(); private: DENG2_PRIVATE(d) }; typedef Material::Layer MaterialLayer; #ifdef __CLIENT__ typedef Material::Decoration MaterialDecoration; #endif #endif // DENG_RESOURCE_MATERIAL_H doomsday-stable-1.15.7/doomsday/client/include/resource/model.h0000664000175000017500000002036112641367670024016 0ustar jaakkojaakko/** @file model.h 3D model resource * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_RESOURCE_MODEL_H #define DENG_RESOURCE_MODEL_H #include #include "Texture" #include #include #include #include #include /// Unique identifier associated with each model. typedef uint modelid_t; /// Special value used to signify an invalid model id. #define NOMODELID 0 /** * 3D model resource. * * @ingroup resource * * @todo Implement an API for building models programmatically. */ class Model { public: /// Referenced frame is missing. @ingroup errors DENG2_ERROR(MissingFrameError); /// Referenced skin is missing. @ingroup errors DENG2_ERROR(MissingSkinError); /// Referenced detail level is missing. @ingroup errors DENG2_ERROR(MissingDetailLevelError); /** * Classification/processing flags. */ enum Flag { NoTextureCompression = 0x1 ///< Do not compress skin textures. }; Q_DECLARE_FLAGS(Flags, Flag) /** * Animation key-frame. */ struct Frame { Model &model; struct Vertex { de::Vector3f pos; de::Vector3f norm; }; typedef QVector VertexBuf; VertexBuf vertices; de::Vector3f min; de::Vector3f max; de::String name; Frame(Model &model, de::String const &name = "") : model(model), name(name) {} void bounds(de::Vector3f &min, de::Vector3f &max) const; float horizontalRange(float *top, float *bottom) const; }; typedef QList Frames; /** * Texture => Skin assignment. */ struct Skin { de::String name; de::Texture *texture; // Not owned. Skin(de::String const &name = "", de::Texture *texture = 0) : name(name), texture(texture) {} }; typedef QList Skins; /** * Prepared model geometry uses lists of primitives. */ struct Primitive { struct Element { de::Vector2f texCoord; int index; ///< Index into the model's vertex mesh. }; typedef QVector Elements; Elements elements; bool triFan; ///< @c true= triangle fan; otherwise triangle strip. }; typedef QList Primitives; /** * Level of detail information. * * Used with DMD models to reduce complexity of the drawn model geometry. */ struct DetailLevel { Model &model; int level; Primitives primitives; DetailLevel(Model &model, int level) : model(model), level(level) {} /** * Returns @c true iff the specified vertex @a number is in use for this * detail level. */ bool hasVertex(int number) const; }; typedef QList DetailLevels; public: /** * Construct a new 3D model. */ Model(Flags flags = 0); /** * Determines whether the specified @a file appears to be in a recognised * model format. */ static bool recognise(de::FileHandle &file); /** * Attempt to load a new model resource from the specified @a file. * * @param file Handle for the model file to load from. * @param aspectScale Optionally apply y-aspect scaling. * * @return The new Model (if any). Ownership is given to the caller. */ static Model *loadFromFile(de::FileHandle &file, float aspectScale = 1); /** * Returns the unique identifier associated with the model. */ uint modelId() const; /** * Change the unique identifier associated with the model. * * @param newId New identifier to apply. */ void setModelId(uint newId); /** * Returns a copy of the current model flags. */ Flags flags() const; /** * Change the model's flags. * * @param flagsToChange Flags to change the value of. * @param operation Logical operation to perform on the flags. */ void setFlags(Flags flagsToChange, de::FlagOp operation = de::SetFlags); /** * Lookup a model animation frame by @a name. * * @return Unique number of the found frame; otherwise @c -1 (not found). */ int frameNumber(de::String name) const; /** * Convenient method of determining whether the specified model animation * frame @a number is valid (i.e., a frame is defined for it). */ inline bool hasFrame(int number) const { return (number >= 0 && number < frameCount()); } /** * Retrieve a model animation frame by it's unique frame @a number. */ Frame &frame(int number) const; /** * Returns the total number of model animation frames. */ inline int frameCount() const { return frames().count(); } /** * Provides access to the model animation frames, for efficient traversal. */ Frames const &frames() const; /** * Clear all model animation frames. */ void clearAllFrames(); /** * Lookup a model skin by @a name. * * @return Unique number of the found skin; otherwise @c -1 (not found). */ int skinNumber(de::String name) const; /** * Convenient method of determining whether the specified model skin @a number * is valid (i.e., a skin is defined for it). */ inline bool hasSkin(int number) const { return (number >= 0 && number < skinCount()); } /** * Retrieve a model skin by it's unique @a number. */ Skin &skin(int number) const; /** * Append a new skin with the given @a name to the model. If a skin already * exists with this name it will be returned instead. * * @return Reference to the (possibly new) skin. */ Skin &newSkin(de::String name); /** * Returns the total number of model skins. */ inline int skinCount() const { return skins().count(); } /** * Provides access to the model skins, for efficient traversal. */ Skins const &skins() const; /** * Clear all model skin assignments. */ void clearAllSkins(); /** * Convenient method of accessing the primitive list used for drawing the * model with the highest degree of geometric fidelity (i.e., detail level * zero). */ Primitives const &primitives() const; /** * Returns the total number of vertices used at detail level zero. */ int vertexCount() const; /** * Convenient method of determining whether the specified model detail * @a level is valid (i.e., detail information is defined for it). */ inline bool hasLod(int level) const { return (level >= 0 && level < lodCount()); } /** * Returns the total number of detail levels for the model. */ inline int lodCount() const { return lods().count(); } /** * Retrieve model detail information by it's unique @a level number. */ DetailLevel &lod(int level) const; /** * Provides readonly access to the level of detail information. */ DetailLevels const &lods() const; /// @todo Refactor away. QBitArray const &lodVertexUsage() const; private: DENG2_PRIVATE(d) }; Q_DECLARE_OPERATORS_FOR_FLAGS(Model::Flags) typedef Model::DetailLevel ModelDetailLevel; typedef Model::Frame ModelFrame; typedef Model::Skin ModelSkin; #endif // DENG_RESOURCE_MODEL_H doomsday-stable-1.15.7/doomsday/client/include/resource/patch.h0000664000175000017500000000732112641367670024016 0ustar jaakkojaakko/** @file patch.h Patch Image Format. * * @authors Copyright © 1999-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_PATCH_H #define DENG_RESOURCE_PATCH_H #include "resource/colorpalette.h" #include #include #include #include #include namespace de { /** * @em Patch is a raster image in the id Tech 1 picture format (Doom). * @ingroup resource * * @see http://doomwiki.org/wiki/Picture_format * * @note The height dimension value as declared in the patch header may * well differ to the actual height of the composited image. This is the * reason why map drawing in the id tech 1 software renderer can be seen * to "overdraw" posts - the wall column drawer is working with post pixel * ranges rather than the "logical" height declared in the header. */ class Patch { public: /** * Metadata which describes the patch. */ struct Metadata { /// Dimensions of the patch in pixels. Vector2i dimensions; /// Logical dimensions of the patch in pixels (@see Patch notes). Vector2i logicalDimensions; /// Origin offset (top left) in world coordinate space units. /// Used for various purposes depending on context. Vector2i origin; }; /** * Flags for @ref load() */ enum Flag { /// If the color of a pixel uses index #0 write the default color /// (black) as the color value and set the alpha to zero. MaskZero = 0x1, /// Clip the composited image to the logical dimensions of the patch /// ; otherwise perform no clipping (use the pixel dimensions). ClipToLogicalDimensions = 0x2 }; Q_DECLARE_FLAGS(Flags, Flag) public: /** * Attempt to read metadata from @a data. * @param data Data to read metadata from. */ static Metadata loadMetadata(IByteArray const &data); /** * Attempt to interpret @a data as a Patch. * @param data Data to interpret as a Patch. * @param flags Flags determining how the data should be interpreted. */ static Block load(IByteArray const &data, Flags = 0); /** * @copydoc load() * @param xlatTable If not @c NULL, use this translation table when * compositing final color palette indices. */ static Block load(IByteArray const &data, ColorPaletteTranslation const &xlatTable, Flags = 0); /** * Determines whether @a data looks like it can be interpreted as a Patch. * * @param data Data to check. * * @return @c true if the data looks like a patch; otherwise @c false. */ static bool recognize(IByteArray const &data); }; Q_DECLARE_OPERATORS_FOR_FLAGS(Patch::Flags) typedef Patch::Metadata PatchMetadata; } // namespace de #endif // DENG_RESOURCE_PATCH_H doomsday-stable-1.15.7/doomsday/client/include/resource/sprite.h0000664000175000017500000001134512641367670024226 0ustar jaakkojaakko/** @file sprite.h 3D-Sprite resource. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_SPRITE_H #define DENG_RESOURCE_SPRITE_H #include "dd_types.h" // angle_t #include #include #include class Material; #ifdef __CLIENT__ class Lumobj; #endif /** * A sprite is a map entity visualization which approximates a 3D object using * a set of 2D images. Each image represents a view of the entity, from a specific * view-angle. The illusion of 3D is successfully achieved through matching the * relative angle to the viewer with an image which depicts the entity from this * angle. * * Sprite animation sequences are defined elsewhere. * * @ingroup resource * * @todo Reimplement view angle addressing (spherical coords?). * @todo Remove fixed number of view angles. */ class Sprite { public: /// Required view angle is missing. @ingroup errors DENG2_ERROR(MissingViewAngleError); /// Maximum number of discreet view angles. @todo remove me static int const max_angles = 16; /** * One depiction of the entity as if viewed from the associated angle. */ struct ViewAngle { Material *material; bool mirrorX; ViewAngle() : material(0), mirrorX(false) {} }; typedef QVector ViewAngles; public: Sprite(); Sprite(Sprite const &other); Sprite &operator = (Sprite const &other); /** * @param rotation @c 0= front, @c 1= one angle turn clockwise, etc... */ void newViewAngle(Material *material, int rotation, bool mirrorX); /** * Returns @c true iff a view angle is defined for the specified @a rotation. * * @param rotation Rotation index/identifier to lookup the material for. The * valid range is [0..max_angles) */ bool hasViewAngle(int rotation) const; /** * Returns the view angle for the specified @a rotation. * * @param rotation Rotation index/identifier to lookup the material for. The * valid range is [0..max_angles) * * @return The viewAngle associated with the specified rotation. */ ViewAngle const &viewAngle(int rotation) const; /** * Select an appropriate view angle for visualizing the sprite given a mobj's * angle and relative angle with the viewer (the 'eye'). * * @param mobjAngle Angle of the mobj in the map coordinate space. * @param angleToEye Relative angle of the mobj from the view position. * @param noRotation @c true= Ignore rotations and always use the "front". * * @return The viewAngle associated with the chosen rotation. */ ViewAngle const &closestViewAngle(angle_t mobjAngle, angle_t angleToEye, bool noRotation = false) const; /** * Provides access to the view angles for efficient traversal. The order of * which should be considered undefined. */ ViewAngles const &viewAngles() const; /** * Returns the total number of defined view angles for the sprite. */ inline int viewAngleCount() const { return viewAngles().count(); } #ifdef __CLIENT__ /** * Returns the radius of the sprite as it would visually appear to be. * * @note Presently considers rotation 0 only! */ double visualRadius() const; /** * Produce a luminous object from the sprite configuration. The properties * of any resultant lumobj are configured in "sprite-local" space. This means * that it will positioned relative to the center of the sprite and must be * further configured before adding to the map (i.e., translated to the origin * in map space). * * @return Newly generated lumobj otherwise @c 0. */ Lumobj *generateLumobj() const; #endif private: DENG2_PRIVATE(d) }; typedef Sprite::ViewAngle SpriteViewAngle; #endif // DENG_RESOURCE_SPRITE_H doomsday-stable-1.15.7/doomsday/client/include/resource/tga.h0000664000175000017500000000632112641367670023471 0ustar jaakkojaakko/** @file tga.h Truevision TGA (a.k.a Targa) image reader/writer * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_TGA_H #define DENG_RESOURCE_TGA_H #include #include /// @addtogroup resource ///@{ /** * Saves the buffer (which is formatted rgb565) to a Targa 24 image file. * * @param file Handle to the open file to be written to. * @param w Width of the image in pixels. * @param h Height of the image in pixels. * @param buf Ptr to the image data to be written. * * @return Non-zero iff successful. */ int TGA_Save24_rgb565(FILE *file, int w, int h, uint16_t const *buf); /** * Save the rgb888 buffer as Targa 24. * * @param file Handle to the open file to be written to. * @param w Width of the image in pixels. * @param h Height of the image in pixels. * @param buf Ptr to the image data to be written. * * @return Non-zero iff successful. */ int TGA_Save24_rgb888(FILE *file, int w, int h, uint8_t const *buf); /** * Save the rgb8888 buffer as Targa 24. * * @param file Handle to the open file to be written to. * @param w Width of the image in pixels. * @param h Height of the image in pixels. * @param buf Ptr to the image data to be written. * * @return Non-zero iff successful. */ int TGA_Save24_rgba8888(FILE *file, int w, int h, uint8_t const *buf); /** * Save the rgb888 buffer as Targa 16. * * @param file Handle to the open file to be written to. * @param w Width of the image in pixels. * @param h Height of the image in pixels. * @param buf Ptr to the image data to be written. * * @return Non-zero iff successful. */ int TGA_Save16_rgb888(FILE *file, int w, int h, uint8_t const *buf); /** * Loads a 24-bit or a 32-bit image (24-bit color + 8-bit alpha). * * @warning: This is not a generic TGA loader. Only type 2, 24/32 pixel * size, attrbits 0/8 and lower left origin supported. * * @return Non-zero iff the image is loaded successfully. */ uint8_t *TGA_Load(de::FileHandle &file, de::Vector2ui &outSize, int &pixelSize); /** * @return Textual message detailing the last error encountered else @c 0. */ char const *TGA_LastError(); ///@} #endif // DENG_RESOURCE_TGA_H doomsday-stable-1.15.7/doomsday/client/include/resource/colorpalette.h0000664000175000017500000001263612641367670025421 0ustar jaakkojaakko/** @file colorpalette.h Color palette resource. * * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_COLORPALETTE_H #define DENG_RESOURCE_COLORPALETTE_H #include #include #include #include #include #include #ifdef __CLIENT__ #include #endif #include /** * Converts a sequence of bytes, given a color format descriptor, into a table * of colors (usable with ColorPalette). */ class ColorTableReader { public: /// Base class for color-format-related errors. @ingroup errors DENG2_ERROR(FormatError); public: /** * @param format Textual color format description of the format of each * discreet color value in @a colorData. * * Expected form: "C#C#C" * - 'C'= color component identifier, one of [R, G, B] * - '#'= number of bits for the identified component. * * @param colorCount Number of discreet colors in @a colorData. * @param colorData Color data (at least @a colorCount * 3 values). */ static QVector read(de::String format, int colorCount, de::dbyte const *colorData); }; /** * Color Palette. * * @ingroup resource */ class ColorPalette { public: /// An invalid translation id was specified. @ingroup errors DENG2_ERROR(InvalidTranslationIdError); /// Notified whenever the color table changes. DENG2_DEFINE_AUDIENCE(ColorTableChange, void colorPaletteColorTableChanged(ColorPalette &colorPalette)) /// Palette index translation mapping table. typedef QVector Translation; public: /** * Construct a new empty color palette. */ ColorPalette(); /** * Constructs a new color palette using the specified color table. * * @param colors Color table to initialize from. A copy is made. */ ColorPalette(QVector const &colors); /// @see color() inline de::Vector3ub operator [] (int colorIndex) const { return color(colorIndex); } /** * Returns the automatically generated, unique identifier of the color palette. */ de::Id id() const; /** * Returns the total number of colors in the palette. */ int colorCount() const; /** * Lookup a color in the palette by @a colorIndex. If the specified index is * out of valid [0..colorCount) range it will be clamped. * * @param colorIndex Index of the color in the palette. * * @return Associated R8G8B8 color triplet. * * @see colorf(), operator [] */ de::Vector3ub color(int colorIndex) const; /** * Same as @ref color() except the color is returned in [0..1] floating-point. */ de::Vector3f colorf(int colorIndex) const; #ifdef __CLIENT__ /** * Same as @ref color() except the color is returned as a QColor instance. */ inline QColor colorq(int colorIndex, int alpha = 255) const { de::Vector3ub rgb = color(colorIndex); return QColor(rgb.x, rgb.y, rgb.z, alpha); } #endif /** * Replace the entire color table. The ColorTableChange audience is notified * whenever the color table changes. * * If the new color table has a different number of colors, then any existing * translation maps will be cleared automatically. * * @param colorTable The replacement color table. A copy is made. */ ColorPalette &replaceColorTable(QVector const &colorTable); /** * Given an R8G8B8 color triplet return the closet matching color index. * * @param rgb R8G8B8 color to be matched. * * @return Closet matching color index or @c -1 if no colors in the palette. */ int nearestIndex(de::Vector3ub const &rgb) const; /** * Clear all translation maps. */ void clearTranslations(); /** * Lookup a translation map by it's unique @a id. * * @return Pointer to the identified translation; otherwise @c 0. */ Translation const *translation(de::String id) const; /** * Add/replace the identified translation map. * * @param id Unique identifier of the translation. * @param mappings Table of palette index mappings (a copy is made). It is * assumed that this table contains a mapping for each color * in the palette. * * @see colorCount() */ void newTranslation(de::String id, Translation const &mappings); private: DENG2_PRIVATE(d) }; typedef ColorPalette::Translation ColorPaletteTranslation; #endif // DENG_RESOURCE_COLORPALETTE_H doomsday-stable-1.15.7/doomsday/client/include/resource/fontmanifest.h0000664000175000017500000001063612641367670025417 0ustar jaakkojaakko/** @file fontmanifest.h Font resource manifest. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_FONTMANIFEST_H #define DENG_RESOURCE_FONTMANIFEST_H #include "AbstractFont" #include #include #include #include #include namespace de { class FontScheme; /** * Description for a would-be logical Font resource. * * Models a reference to and the associated metadata for a logical font in the * font resource collection. * * @see FontScheme, AbstractFont * @ingroup resource */ class FontManifest : public PathTree::Node { public: /// Required Font instance is missing. @ingroup errors DENG2_ERROR(MissingFontError); /// Notified when the manifest is about to be deleted. DENG2_DEFINE_AUDIENCE(Deletion, void fontManifestBeingDeleted(FontManifest const &manifest)) /// Notified whenever the unique identifier changes. DENG2_DEFINE_AUDIENCE(UniqueIdChange, void fontManifestUniqueIdChanged(FontManifest &manifest)) public: FontManifest(PathTree::NodeArgs const &args); /** * Returns the owning scheme of the manifest. */ FontScheme &scheme() const; /** * Convenient method of returning the name of the owning scheme. * * @see scheme(), FontScheme::name() */ String const &schemeName() const; /** * Compose a URI of the form "scheme:path" for the manifest. * * The scheme component of the URI will contain the symbolic name of * the scheme for the manifest. * * The path component of the URI will contain the percent-encoded path * of the manifest. */ inline Uri composeUri(QChar sep = '/') const { return Uri(schemeName(), path(sep)); } /** * Compose a URN of the form "urn:scheme:uniqueid" for the manifest. * * The scheme component of the URI will contain the identifier 'urn'. * * The path component of the URI is a string which contains both the * symbolic name of the scheme followed by the unique id of the font * manifest, separated with a colon. * * @see uniqueId(), setUniqueId() */ inline Uri composeUrn() const { return Uri("urn", String("%1:%2").arg(schemeName()).arg(uniqueId(), 0, 10)); } /** * Returns a textual description of the manifest. * * @return Human-friendly description the manifest. */ String description(Uri::ComposeAsTextFlags uriCompositionFlags = Uri::DefaultComposeAsTextFlags) const; /** * Returns the scheme-unique identifier for the manifest. */ int uniqueId() const; /** * Change the unique identifier property of the manifest. * * @return @c true iff @a newUniqueId differed to the existing unique * identifier, which was subsequently changed. */ bool setUniqueId(int newUniqueId); /** * Returns @c true if a resource is presently associated with the manifest. */ bool hasResource() const; /** * Returns the logical resource associated with the manifest. */ AbstractFont &resource() const; /** * Change the logical resource associated with the manifest. * * @param newResource New resource to associate. */ void setResource(AbstractFont *newResource); /** * Clear the logical resource associated with the manifest. * * Same as @c setResource(0) */ inline void clearResource() { setResource(0); } private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_RESOURCE_FONTMANIFEST_H doomsday-stable-1.15.7/doomsday/client/include/resource/texturescheme.h0000664000175000017500000001064612641367670025610 0ustar jaakkojaakko/** @file texturescheme.h Texture collection subspace. * * @authors Copyright © 2010-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_RESOURCE_TEXTURESCHEME_H #define DENG_RESOURCE_TEXTURESCHEME_H #include #include #include #include #include "TextureManifest" namespace de { /** * Texture collection subspace. * * @see Textures * @ingroup resource */ class TextureScheme { typedef class TextureManifest Manifest; public: /// The requested manifest could not be found in the index. DENG2_ERROR(NotFoundError); /// The specified path was not valid. @ingroup errors DENG2_ERROR(InvalidPathError); DENG2_DEFINE_AUDIENCE(ManifestDefined, void textureSchemeManifestDefined(TextureScheme &scheme, Manifest &manifest)) /// Minimum length of a symbolic name. static int const min_name_length = DENG2_URI_MIN_SCHEME_LENGTH; /// Manifests in the scheme are placed into a tree. typedef PathTreeT Index; public: /** * Construct a new (empty) texture subspace scheme. * * @param symbolicName Symbolic name of the new subspace scheme. Must * have at least @ref min_name_length characters. */ explicit TextureScheme(String symbolicName); ~TextureScheme(); /** * Returns the symbolic name of the scheme. */ String const &name() const; /** * Returns the total number of manifests in the scheme. */ inline int size() const { return index().size(); } inline int count() const { return size(); } /** * Clear all manifests in the scheme (any GL textures which have been acquired for * associated textures will be released). */ void clear(); /** * Insert a new manifest at the given @a path into the scheme. If a manifest already * exists at this path, the existing manifest is returned. * * If any of the property values (flags, dimensions, etc...) differ from that which * is already defined in the pre-existing manifest, any texture which is currently * associated is released (any GL-textures acquired for it are deleted). * * @param path Virtual path for the resultant manifest. * @param flags Texture flags property. * @param dimensions Logical dimensions property. * @param origin World origin offset property. * @param uniqueId Unique identifier property. * @param resourceUri Resource URI property. * * @return The (possibly newly created) manifest at @a path. */ Manifest &declare(Path const &path, Texture::Flags flags, Vector2i const &dimensions, Vector2i const &origin, int uniqueId, de::Uri const *resourceUri); /** * Returns @c true if a manifest exists on the given @a path. */ bool has(Path const &path) const; /** * Lookup a Manifest in the scheme with a matching @a path. */ Manifest &find(Path const &path); Manifest const &find(Path const &path) const; /** * Lookup a Manifest in the scheme with an associated resource URI matching @a uri. */ Manifest &findByResourceUri(Uri const &uri); Manifest const &findByResourceUri(Uri const &uri) const; /** * Lookup a Manifest in the scheme with an associated identifier matching @a uniqueId. */ Manifest &findByUniqueId(int uniqueId); Manifest const &findByUniqueId(int uniqueId) const; /** * Provides access to the manifest index for efficient traversal. */ Index const &index() const; private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_RESOURCE_TEXTURESCHEME_H doomsday-stable-1.15.7/doomsday/client/include/de_render.h0000664000175000017500000000270312641367670023016 0ustar jaakkojaakko/** * @file de_render.h * Rendering subsystem. @ingroup render * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DOOMSDAY_CLIENT_RENDERER #define DOOMSDAY_CLIENT_RENDERER #ifdef __CLIENT__ #include "render/viewports.h" #include "render/lightgrid.h" #include "render/r_draw.h" #include "render/r_main.h" #include "render/r_things.h" #include "render/rend_halo.h" #include "render/rend_main.h" #include "render/rend_model.h" #include "render/rend_fakeradio.h" #include "render/rend_font.h" #include "render/rendpoly.h" #include "render/billboard.h" #include "render/cameralensfx.h" #endif #include "r_util.h" #endif /* DOOMSDAY_CLIENT_RENDERER */ doomsday-stable-1.15.7/doomsday/client/include/dd_pinit.h0000664000175000017500000000364312641367670022665 0ustar jaakkojaakko/** * @file dd_pinit.h * Platform independent routines for initializing the engine. @ingroup base * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_PORTABLE_INIT_H #define LIBDENG_PORTABLE_INIT_H #include "api_gameexport.h" #include "api_internaldata.h" #include #ifdef __CLIENT__ #include /** * Compose the title for the main window. * @param title Title text for the window. */ de::String DD_ComposeMainWindowTitle(); #endif #ifdef __cplusplus extern "C" { #endif extern uint mainWindowIdx; /// Maximum allowed number of plugins. #define MAX_PLUGS 32 /** * Shuts down all subsystems. This is called from DD_Shutdown(). */ void DD_ShutdownAll(void); /** * Called early on during the startup process so that we can get the console * online ready for printing ASAP. */ void DD_ConsoleInit(void); void DD_InitAPI(void); /** * Define abbreviations and aliases for command line options. */ void DD_InitCommandLine(void); #ifdef __cplusplus } // extern "C" #endif #endif /* LIBDENG_PORTABLE_INIT_H */ doomsday-stable-1.15.7/doomsday/client/include/gl/0000775000175000017500000000000012641367670021316 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/gl/gl_tex.h0000664000175000017500000002545612641367670022765 0ustar jaakkojaakko/** @file gl_tex.h Image manipulation and evaluation algorithms. * * @ingroup gl * * @todo Belongs in the resource domain -- no ties to GL or related components. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_GL_IMAGE_MANIPULATION_H #define DENG_GL_IMAGE_MANIPULATION_H #include "color.h" class ColorPalette; typedef struct colorpalette_analysis_s { colorpaletteid_t paletteId; } colorpalette_analysis_t; typedef struct pointlight_analysis_s { float originX, originY, brightMul; ColorRawf color; } pointlight_analysis_t; typedef struct averagecolor_analysis_s { ColorRawf color; } averagecolor_analysis_t; typedef struct averagealpha_analysis_s { float alpha; ///< Result of the average. float coverage; ///< Fraction representing the ratio of alpha to non-alpha pixels. } averagealpha_analysis_t; /** * @param pixels Luminance image to be enhanced. * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param hasAlpha If @c true, @a pixels is assumed to contain luminance plus alpha data * (totaling 2 * @a width * @a height bytes). */ void AmplifyLuma(uint8_t* pixels, int width, int height, dd_bool hasAlpha); /** * Take the input buffer and convert to color keyed. A new buffer may be * needed if the input buffer has three color components. * Color keying is done for both (0,255,255) and (255,0,255). * * @return If the in buffer wasn't large enough will return a ptr to the * newly allocated buffer which must be freed with free(), else @a buf. */ uint8_t* ApplyColorKeying(uint8_t* pixels, int width, int height, int pixelSize); #if 0 // dj: Doesn't make sense, "darkness" applied to an alpha channel? /** * Sets the RGB color of transparent pixels along the image's non-transparent * areas to black. When the image is then drawn with magnification, * RGB interpolation along the edges will go to (0, 0, 0) while the alpha * value is interpolated to zero. The end result is that the edges are * highlighted against the background -- i.e., this is a cheap way to make * the edges of small sprites stand out more clearly. The effect was originally * used by jDoom on font characters and other such graphics to make them appear * less blurry. * * @param pixels RGBA data (in/out). * @param width Width of the image in pixels. * @param height Height of the image in pixels. */ void BlackOutlines(uint8_t* pixels, int width, int height); #endif /** * Spread the color of non-masked pixels outwards into the masked area. * This addresses the "black outlines" produced by texture filtering due to * sampling the default (black) color. * * @param pixels Paletted pixel data (in/out). The size of this array * is expected to be 2 * @a width * @a height bytes * (the first layer is for the color indices and the second * layer for mask values). * @param width Width of the image in pixels. * @param height Height of the image in pixels. */ void ColorOutlinesIdx(uint8_t* pixels, int width, int height); /** * @param pixels RGB(a) image to be desaturated (in/out). * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param pixelSize Size of each pixel. Handles 3 and 4. */ void Desaturate(uint8_t* pixels, int width, int height, int pixelSize); /** * @note Does not conform to any standard technique and adjustments * are applied symmetrically for all color components. * * @param pixels RGB(a) image to be enhanced (in/out). * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param pixelSize Size of each pixel. Handles 3 and 4. */ void EnhanceContrast(uint8_t* pixels, int width, int height, int pixelSize); /** * Equalize the specified luminance map such that the minimum and maximum * brightness covers the whole [0...255] range. * * @par Algorithm * Calculates shift deltas for bright and dark-side pixels by * averaging the luminosity of all pixels in the original image. * * @param pixels Luminance image to equalize. * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param rBaMul Calculated balance multiplier is written here. * @param rHiMul Calculated multiplier is written here. * @param rLoMul Calculated multiplier is written here. */ void EqualizeLuma(uint8_t* pixels, int width, int height, float* rBaMul, float* rHiMul, float* rLoMul); /** * @param pixels RGB(a) image to evaluate. * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param pixelSize Size of a pixel in bytes. * @param color Determined average color written here. */ void FindAverageColor(const uint8_t* pixels, int width, int height, int pixelSize, ColorRawf* color); /** * @param pixels Index-color image to evaluate. * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param palette Color palette to use. * @param hasAlpha @c true == @a pixels includes alpha data. * @param color Determined average color written here. */ void FindAverageColorIdx(uint8_t const *pixels, int width, int height, ColorPalette const &palette, dd_bool hasAlpha, ColorRawf *color); /** * @param pixels RGB(a) image to evaluate. * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param pixelSize Size of a pixel in bytes. * @param line Line to evaluate. * @param color Determined average color written here. */ void FindAverageLineColor(uint8_t const *pixels, int width, int height, int pixelSize, int line, ColorRawf *color); /** * @param pixels Index-color image to evaluate. * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param line Line to evaluate. * @param palette Color palette to use. * @param hasAlpha @c true == @a pixels includes alpha data. * @param color Determined average color written here. */ void FindAverageLineColorIdx(uint8_t const *pixels, int width, int height, int line, ColorPalette const &palette, dd_bool hasAlpha, ColorRawf *color); /** * @param pixels RGB(a) image to evaluate. * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param pixelSize Size of a pixel in bytes. * @param alpha Determined average alpha written here. * @param coverage Fraction representing the ratio of alpha to non-alpha pixels. */ void FindAverageAlpha(uint8_t const *pixels, int width, int height, int pixelSize, float *alpha, float *coverage); /** * @param pixels Index-color image to evaluate. * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param alpha Determined average alpha written here. * @param coverage Fraction representing the ratio of alpha to non-alpha pixels. */ void FindAverageAlphaIdx(uint8_t const *pixels, int width, int height, float *alpha, float *coverage); /** * Calculates a clip region for the image that excludes alpha pixels. * * @par Algorithm * Cross spread from bottom > top, right > left (inside out). * * @param pixels Image data to be processed. * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param pixelSize Size of each pixel. Handles 1 (==2), 3 and 4. * @param region Determined region written here. */ void FindClipRegionNonAlpha(const uint8_t* pixels, int width, int height, int pixelSize, int region[4]); /** * @param pixels RGB(a) image to be enhanced. * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param pixelSize Size of each pixel. Handles 3 and 4. */ void SharpenPixels(uint8_t* pixels, int width, int height, int pixelSize); uint8_t* GL_ScaleBuffer(const uint8_t* pixels, int width, int height, int pixelSize, int outWidth, int outHeight); void* GL_ScaleBufferEx(const void* datain, int width, int height, int pixelSize, /*GLint typein,*/ int rowLength, int alignment, int skiprows, int skipPixels, int outWidth, int outHeight, /*GLint typeout,*/ int outRowLength, int outAlignment, int outSkipRows, int outSkipPixels); uint8_t* GL_ScaleBufferNearest(const uint8_t* pixels, int width, int height, int pixelSize, int outWidth, int outHeight); /** * Works within the given data, reducing the size of the picture to half * its original. * * @param pixels RGB(A) pixel data to process (in/out). * @param width Width of the final texture, must be power of two. * @param height Height of the final texture, must be power of two. * @param pixelSize Size of a pixel in bytes. */ void GL_DownMipmap32(uint8_t* pixels, int width, int height, int pixelSize); /** * Works within the given data, reducing the size of the picture to half * its original. * * @param in Pixel data to process (paletted, in/out). * @param fadedOut Faded result image. * @param width Width of the final texture, must be power of two. * @param height Height of the final texture, must be power of two. * @param fade Fade factor (0..1). */ void GL_DownMipmap8(uint8_t* in, uint8_t* fadedOut, int width, int height, float fade); dd_bool GL_PalettizeImage(uint8_t *out, int outformat, ColorPalette const *palette, dd_bool gammaCorrect, uint8_t const *in, int informat, int width, int height); dd_bool GL_QuantizeImageToPalette(uint8_t *out, int outformat, ColorPalette const *palette, uint8_t const *in, int informat, int width, int height); /** * Desaturates the texture in the dest buffer by averaging the colour then * looking up the nearest match in the palette. Increases the brightness * to maximum. */ void GL_DeSaturatePalettedImage(uint8_t *buffer, ColorPalette const &palette, int width, int height); #endif // DENG_GL_IMAGE_MANIPULATION_H doomsday-stable-1.15.7/doomsday/client/include/gl/svg.h0000664000175000017500000000374112641367670022273 0ustar jaakkojaakko/** * @file svg.h * Scaleable Vector Graphic. @ingroup refresh * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_REFRESH_SVG_H #define LIBDENG_REFRESH_SVG_H #include "api_svg.h" struct svgline_s; typedef struct svgline_s SvgLine; /** * Svg. Scaleable Vector Graphic. */ struct svg_s; // The svg instance (opaque). typedef struct svg_s Svg; #ifdef __cplusplus extern "C" { #endif void R_InitSvgs(void); /** * Unload any resources needed for vector graphics. * Called during shutdown and before a renderer restart. */ void R_UnloadSvgs(void); void R_ShutdownSvgs(void); void Svg_Delete(Svg* svg); void Svg_Draw(Svg* svg); dd_bool Svg_Prepare(Svg* svg); void Svg_Unload(Svg* svg); /// @return Unique identifier associated with this. svgid_t Svg_UniqueId(Svg* svg); /** * Static members: */ /** * Try to construct a new Svg instance from specified definition. * * @return Newly created Svg instance if definition was valid else @a NULL */ Svg* Svg_FromDef(svgid_t uniqueId, const def_svgline_t* lines, uint numLines); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_REFRESH_SVG_H doomsday-stable-1.15.7/doomsday/client/include/gl/gl_texmanager.h0000664000175000017500000000510212641367670024302 0ustar jaakkojaakko/** @file gl_texmanager.h GL-Texture Management. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_GL_TEXMANAGER_H #define DENG_CLIENT_GL_TEXMANAGER_H #ifndef __CLIENT__ # error "GL Texture Manager only exists in the Client" #endif #include "api_gl.h" #include "gl/sys_opengl.h" #include "resource/rawtexture.h" #include /** * Textures used in the lighting system. */ typedef enum lightingtexid_e { LST_DYNAMIC, ///< Round dynamic light LST_GRADIENT, ///< Top-down gradient LST_RADIO_CO, ///< FakeRadio closed/open corner shadow LST_RADIO_CC, ///< FakeRadio closed/closed corner shadow LST_RADIO_OO, ///< FakeRadio open/open shadow LST_RADIO_OE, ///< FakeRadio open/edge shadow LST_CAMERA_VIGNETTE, NUM_LIGHTING_TEXTURES } lightingtexid_t; typedef enum flaretexid_e { FXT_ROUND, FXT_FLARE, FXT_BRFLARE, FXT_BIGFLARE, NUM_SYSFLARE_TEXTURES } flaretexid_t; void GL_InitTextureManager(); void GL_TexReset(); /* * Here follows miscellaneous routines currently awaiting refactoring into the * revised texture management APIs. */ void GL_LoadLightingSystemTextures(); void GL_ReleaseAllLightingSystemTextures(); GLuint GL_PrepareLSTexture(lightingtexid_t which); void GL_LoadFlareTextures(); void GL_ReleaseAllFlareTextures(); GLuint GL_PrepareFlaremap(de::Uri const &resourceUri); GLuint GL_PrepareSysFlaremap(flaretexid_t which); GLuint GL_PrepareRawTexture(rawtex_t &rawTex); /// Release all textures used with 'Raw Images'. void GL_ReleaseTexturesForRawImages(); /** * Change the GL minification filter for all prepared "raw" textures. */ void GL_SetRawTexturesMinFilter(int minFilter); #endif // DENG_CLIENT_GL_TEXMANAGER_H doomsday-stable-1.15.7/doomsday/client/include/gl/texturecontent.h0000664000175000017500000000666112641367670024573 0ustar jaakkojaakko/** @file texturecontent.h GL-texture content. * * @author Copyright © 2006-2013 Jaakko Keränen * @author Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_GL_TEXTURECONTENT_H #define DENG_CLIENT_GL_TEXTURECONTENT_H #include "api_gl.h" #include "gl/gl_defer.h" #include "TextureManifest" /** * @defgroup textureContentFlags Texture Content Flags * @ingroup flags */ /*@{*/ #define TXCF_NO_COMPRESSION 0x1 #define TXCF_MIPMAP 0x2 #define TXCF_GRAY_MIPMAP 0x4 #define TXCF_CONVERT_8BIT_TO_ALPHA 0x8 #define TXCF_APPLY_GAMMACORRECTION 0x10 #define TXCF_UPLOAD_ARG_NOSTRETCH 0x20 #define TXCF_UPLOAD_ARG_NOSMARTFILTER 0x40 #define TXCF_NEVER_DEFER 0x80 /*@}*/ /** * Defines the content of a GL texture. Used when creating textures either * immediately or in deferred mode (when busy). */ typedef struct texturecontent_s { dgltexformat_t format; GLuint name; uint8_t const *pixels; colorpaletteid_t paletteId; int width; int height; int minFilter; int magFilter; int anisoFilter; int wrap[2]; int grayMipmap; int flags; /// @ref textureContentFlags } texturecontent_t; /** * Initializes a texture content struct with default params. */ void GL_InitTextureContent(texturecontent_t *content); texturecontent_t *GL_ConstructTextureContentCopy(texturecontent_t const *other); void GL_DestroyTextureContent(texturecontent_t *content); /** * Prepare the texture content @a c, using the given image in accordance with * the supplied specification. The image data will be transformed in-place. * * @param c Texture content to be completed. * @param glTexName GL name for the texture we intend to upload. * @param image Source image containing the pixel data to be prepared. * @param spec Specification describing any transformations which * should be applied to the image. * * @param textureManifest Manifest for the logical texture being prepared. * (for informational purposes, i.e., logging) */ void GL_PrepareTextureContent(texturecontent_t &c, GLuint glTexName, image_t &image, TextureVariantSpec const &spec, de::TextureManifest const &textureManifest); /** * @param method GL upload method. By default the upload is deferred. * * @note Can be rather time-consuming due to forced scaling operations and * the generation of mipmaps. */ void GL_UploadTextureContent(texturecontent_t const &content, de::gl::UploadMethod method = de::gl::Deferred); #endif // DENG_CLIENT_GL_TEXTURECONTENT_H doomsday-stable-1.15.7/doomsday/client/include/gl/gl_draw.h0000664000175000017500000000477712641367670023125 0ustar jaakkojaakko/** * @file gl_draw.h * Basic GL-Drawing Routines. * * @ingroup gl * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_GRAPHICS_DRAW_H #define LIBDENG_GRAPHICS_DRAW_H #include #include #include #ifdef __cplusplus extern "C" { #endif void GL_DrawLine(float x1, float y1, float x2, float y2, float r, float g, float b, float a); void GL_DrawRect(de::Rectanglei const &rect); void GL_DrawRect2(int x, int y, int w, int h); /** * @param coords [topLeft, topRight, bottomRight, bottomLeft] */ void GL_DrawRectWithCoords(de::Rectanglei const &rect, de::Vector2i const coords[4]); void GL_DrawRectf(const RectRawf* rect); void GL_DrawRectf2(double x, double y, double w, double h); void GL_DrawRectfWithCoords(const RectRawf* rect, Point2Rawf coords[4]); void GL_DrawRectf2Color(double x, double y, double w, double h, float r, float g, float b, float a); void GL_DrawRectf2TextureColor(double x, double y, double w, double h, int texW, int texH, const float topColor[3], float topAlpha, const float bottomColor[3], float bottomAlpha); void GL_DrawRectf2Tiled(double x, double y, double w, double h, int tw, int th); /** * The cut rectangle must be inside the other one. */ void GL_DrawCutRectfTiled(const RectRawf* rect, int tw, int th, int txoff, int tyoff, const RectRawf* cutRect); void GL_DrawCutRectf2Tiled(double x, double y, double w, double h, int tw, int th, int txoff, int tyoff, double cx, double cy, double cw, double ch); // Filters: dd_bool GL_FilterIsVisible(void); void GL_DrawFilter(void); #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_GRAPHICS_DRAW_H doomsday-stable-1.15.7/doomsday/client/include/gl/gl_main.h0000664000175000017500000002267612641367670023112 0ustar jaakkojaakko/** @file gl_main.h GL-Graphics Subsystem. * @ingroup gl * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_GL_MAIN_H #define DENG_GL_MAIN_H #ifndef __CLIENT__ # error "gl only exists in the Client" #endif #ifndef __cplusplus # error "gl/gl_main.h requires C++" #endif #include "api_gl.h" #include "sys_opengl.h" #include "gl/gltextureunit.h" #include "render/viewports.h" #include "Texture" #include struct ColorRawf_s; struct material_s; struct texturevariant_s; class ColorPalette; class Material; #define TEXQ_BEST 8 #define MINTEXWIDTH 8 #define MINTEXHEIGHT 8 DENG_EXTERN_C int numTexUnits; DENG_EXTERN_C dd_bool envModAdd; DENG_EXTERN_C int viewph, viewpw, viewpx, viewpy; DENG_EXTERN_C float vid_gamma, vid_bright, vid_contrast; DENG_EXTERN_C int r_detail; #ifdef _DEBUG # define DENG_ASSERT_GL_CONTEXT_ACTIVE() {GL_AssertContextActive();} #else # define DENG_ASSERT_GL_CONTEXT_ACTIVE() #endif #ifdef _DEBUG # define LIBDENG_ASSERT_GL_TEXTURE_ISBOUND(tex) { \ GLint p; \ glGetIntegerv(GL_TEXTURE_BINDING_2D, &p); \ Sys_GLCheckError(); \ assert(p == (GLint)tex); \ } #else # define LIBDENG_ASSERT_GL_TEXTURE_ISBOUND(tex) #endif void GL_AssertContextActive(); /// Register the console commands, variables, etc..., of this module. void GL_Register(); /** * One-time initialization of GL and the renderer. This is done very early * on during engine startup and is supposed to be fast. All subsystems * cannot yet be initialized, such as the texture management, so any rendering * occuring before GL_Init() must be done with manually prepared textures. */ void GL_EarlyInit(); /** * Finishes GL initialization. This can be called once the virtual file * system has been fully loaded up, and the console variables have been * read from the config file. */ void GL_Init(); /** * Kills the graphics library for good. */ void GL_Shutdown(); /** * Returns @c true iff the graphics library is currently initialized * for basic drawing (using the OpenGL API directly). */ dd_bool GL_IsInited(); /** * Determines if the renderer is fully initialized (texture manager, deferring, * etc). */ dd_bool GL_IsFullyInited(); /** * GL is reset back to the state it was right after initialization. * Use GL_TotalRestore to bring back online. */ void GL_TotalReset(); /** * To be called after a GL_TotalReset to bring GL back online. */ void GL_TotalRestore(); /** * Initializes the renderer to 2D state. */ void GL_Init2DState(); void GL_SwitchTo3DState(dd_bool push_state, viewport_t const *port, viewdata_t const *viewData); void GL_Restore2DState(int step, viewport_t const *port, viewdata_t const *viewData); void GL_ProjectionMatrix(); /** * Returns the projection matrix that is used for rendering the current frame's * 3D portions. */ de::Matrix4f GL_GetProjectionMatrix(); /** * The first selected unit is active after this call. */ void GL_SelectTexUnits(int count); /** * Swaps buffers / blits the back buffer to the front. */ void GL_DoUpdate(); /** * Set the current GL blending mode. */ void GL_BlendMode(blendmode_t mode); /** * Utility for translating to a GL texture filter identifier. */ GLenum GL_Filter(de::gl::Filter f); /** * Utility for translating to a GL texture wrapping identifier. */ GLenum GL_Wrap(de::gl::Wrapping w); /** * Initializes the graphics library for refresh. Also called at update. */ void GL_InitRefresh(); /** * To be called once at final shutdown. */ void GL_ShutdownRefresh(); /** * Configure the GL state for the specified texture modulation mode. * * @param mode Modulation mode ident. */ void GL_ModulateTexture(int mode); /** * Enables or disables vsync. May cause the OpenGL surface to be recreated. * * @param on @c true to enable vsync, @c false to disable. */ void GL_SetVSync(dd_bool on); /** * Enables or disables multisampling when FSAA is available. You cannot enable * multisampling if FSAA has not been enabled in the Canvas. Never causes the GL surface * or pixel format to be modified; can be called at any time during the rendering of a * frame. * * @param on @c true to enable multisampling, @c false to disable. */ void GL_SetMultisample(dd_bool on); /** * Reconfigure GL fog according to the setup defined in the specified @a mapInfo definition. */ void GL_SetupFogFromMapInfo(de::Record const *mapInfo); //void GL_BlendOp(int op); dd_bool GL_NewList(DGLuint list, int mode); DGLuint GL_EndList(); void GL_CallList(DGLuint list); void GL_DeleteLists(DGLuint list, int range); void GL_SetMaterialUI2(Material *mat, de::gl::Wrapping wrapS, de::gl::Wrapping wrapT); void GL_SetMaterialUI(Material *mat); void GL_SetPSprite(Material *mat, int tclass, int tmap); void GL_SetRawImage(lumpnum_t lumpNum, de::gl::Wrapping wrapS, de::gl::Wrapping wrapT); /** * Bind this texture to the currently active texture unit. * The bind process may result in modification of the GL texture state * according to the specification used to define this variant. * * @param tex Texture::Variant object which represents the GL texture to be bound. */ void GL_BindTexture(de::Texture::Variant *tex); void GL_BindTextureUnmanaged(GLuint texname, de::gl::Wrapping wrapS = de::gl::Repeat, de::gl::Wrapping wrapT = de::gl::Repeat, de::gl::Filter = de::gl::Linear); /** * Bind the associated texture and apply the texture unit configuration to * the @em active GL texture unit. If no texture is associated then nothing * will happen. */ void GL_Bind(de::GLTextureUnit const &glTU); /** * Bind the associated texture and apply the texture unit configuration to * the specified GL texture @a unit, which, is made active during this call. * If no texture is associated then nothing will happen. */ void GL_BindTo(de::GLTextureUnit const &glTU, int unit); void GL_SetNoTexture(); /** * Given a logical anisotropic filtering level return an appropriate multiplier * according to the current GL state and user configuration. */ int GL_GetTexAnisoMul(int level); /** * How many mipmap levels are needed for a texture of the given dimensions? * * @param width Width of the texture in pixels. * @param height Height of the texture in pixels. * @return Number of mipmap levels required. */ int GL_NumMipmapLevels(int width, int height); /** * Determine the optimal size for a texture. Usually the dimensions are scaled * upwards to the next power of two. * * @param noStretch If @c true, the stretching can be skipped. * @param isMipMapped If @c true, we will require mipmaps (this has an effect * on the optimal size). */ dd_bool GL_OptimalTextureSize(int width, int height, dd_bool noStretch, dd_bool isMipMapped, int *optWidth, int *optHeight); /** * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param flags @ref imageConversionFlags. */ int GL_ChooseSmartFilter(int width, int height, int flags); GLuint GL_NewTextureWithParams(dgltexformat_t format, int width, int height, uint8_t const *pixels, int flags); GLuint GL_NewTextureWithParams(dgltexformat_t format, int width, int height, uint8_t const *pixels, int flags, int grayMipmap, int minFilter, int magFilter, int anisoFilter, int wrapS, int wrapT); /** * in/out format: * 1 = palette indices * 2 = palette indices followed by alpha values * 3 = RGB * 4 = RGBA */ uint8_t *GL_ConvertBuffer(uint8_t const *src, int width, int height, int informat, colorpaletteid_t paletteId, int outformat); /** * @param method Unique identifier of the smart filtering method to apply. * @param src Source image to be filtered. * @param width Width of the source image in pixels. * @param height Height of the source image in pixels. * @param flags @ref imageConversionFlags. * @param outWidth Width of resultant image in pixels. * @param outHeight Height of resultant image in pixels. * * @return Newly allocated version of the source image if filtered else @c == @a src. */ uint8_t *GL_SmartFilter(int method, uint8_t const *src, int width, int height, int flags, int *outWidth, int *outHeight); /** * Calculates the properties of a dynamic light that the given sprite frame * casts. Crop a boundary around the image to remove excess alpha'd pixels * from adversely affecting the calculation. * Handles pixel sizes; 1 (==2), 3 and 4. */ void GL_CalcLuminance(uint8_t const *buffer, int width, int height, int comps, colorpaletteid_t paletteId, float *brightX, float *brightY, struct ColorRawf_s *color, float *lumSize); void DGL_AssertNotInPrimitive(void); // Console commands. D_CMD(UpdateGammaRamp); #endif // DENG_GL_MAIN_H doomsday-stable-1.15.7/doomsday/client/include/gl/gl_deferredapi.h0000664000175000017500000000452112641367670024425 0ustar jaakkojaakko/** @file gl_deferredapi.h GL API deferring. * @ingroup gl * * Redefines GL API functions so that they're replaced with ones that defer the * call when needed. * * @note Only the GL API functions declared in this file are safe to call from * outside the main thread! * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_DEFERRED_GL_API_H #define LIBDENG_DEFERRED_GL_API_H #ifdef __CLIENT__ /** * @def LIBDENG_DISABLE_DEFERRED_GL_API * Disables the automatic rerouting of GL API calls to the deferring queue. * Put this in the beginning of a source file, before all #includes. */ #ifndef LIBDENG_DISABLE_DEFERRED_GL_API #include "sys_opengl.h" // ensure native OpenGL has been included #ifdef __cplusplus extern "C" { #endif #define glEnable(x) Deferred_glEnable(x) #define glDisable(x) Deferred_glDisable(x) #define glDeleteTextures(x, y) Deferred_glDeleteTextures(x, y) #define glFogi(x, y) Deferred_glFogi(x, y) #define glFogf(x, y) Deferred_glFogf(x, y) #define glFogfv(x, y) Deferred_glFogfv(x, y) void Deferred_glEnable(GLenum e); void Deferred_glDisable(GLenum e); void Deferred_glDeleteTextures(GLsizei num, const GLuint* names); void Deferred_glFogi(GLenum p, GLint v); void Deferred_glFogf(GLenum p, GLfloat v); void Deferred_glFogfv(GLenum p, const GLfloat* v); #ifdef __cplusplus } // extern "C" #endif #endif // __CLIENT__ #endif // LIBDENG_DISABLE_DEFERRED_GL_API #endif // LIBDENG_DEFERRED_GL_API_H doomsday-stable-1.15.7/doomsday/client/include/gl/sys_opengl.h0000664000175000017500000001050112641367670023646 0ustar jaakkojaakko/** @file sys_opengl.h * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * OpenGL interface, low-level. * * Get OpenGL header files from: * http://oss.sgi.com/projects/ogl-sample/ */ #ifndef LIBDENG_SYSTEM_OPENGL_H #define LIBDENG_SYSTEM_OPENGL_H #ifdef __SERVER__ # define GL_CLAMP_TO_EDGE 0 #endif #ifdef __CLIENT__ #ifdef WIN32 # include # define GL_CALL __stdcall #endif #if defined(UNIX) && !defined(MACOSX) # include # define GL_CALL #endif #if defined(UNIX) && defined(MACOSX) # define GL_GLEXT_PROTOTYPES # include # include # include # define GL_CALL #endif #endif // __CLIENT__ #ifndef GL_NV_texture_env_combine4 # define GL_NV_texture_env_combine4 1 # define GL_COMBINE4_NV 0x8503 # define GL_SOURCE3_RGB_NV 0x8583 # define GL_SOURCE3_ALPHA_NV 0x858B # define GL_OPERAND3_RGB_NV 0x8593 # define GL_OPERAND3_ALPHA_NV 0x859B #endif #include #ifdef __CLIENT__ # include "gl_deferredapi.h" # include "ui/clientwindow.h" #endif /** * Configure available features * \todo Move out of this header. */ #define USE_TEXTURE_COMPRESSION_S3 1 /** * High-level GL state information. */ typedef struct gl_state_s { /// Global config: int multisampleFormat; /// Current state: dd_bool currentUseFog; float currentLineWidth; float currentPointSize; /// Feature (abstract) availability bits: /// Vendor and implementation agnostic. struct { uint blendSubtract : 1; uint genMipmap : 1; uint multisample : 1; uint texCompression : 1; uint texFilterAniso : 1; uint texNonPowTwo : 1; uint vsync : 1; } features; } gl_state_t; typedef enum arraytype_e { AR_VERTEX, AR_COLOR, AR_TEXCOORD0, AR_TEXCOORD1, AR_TEXCOORD2, AR_TEXCOORD3, AR_TEXCOORD4, AR_TEXCOORD5, AR_TEXCOORD6, AR_TEXCOORD7 } arraytype_t; #ifdef __cplusplus extern "C" { #endif #ifdef __CLIENT__ extern gl_state_t GL_state; #ifdef WIN32 extern PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT; extern PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB; #endif #if defined(LIBGUI_USE_GLENTRYPOINTS) extern PFNGLBLENDEQUATIONEXTPROC glBlendEquationEXT; extern PFNGLLOCKARRAYSEXTPROC glLockArraysEXT; extern PFNGLUNLOCKARRAYSEXTPROC glUnlockArraysEXT; #endif #ifndef GL_ATI_texture_env_combine3 #define GL_MODULATE_ADD_ATI 0x8744 #define GL_MODULATE_SIGNED_ADD_ATI 0x8745 #define GL_MODULATE_SUBTRACT_ATI 0x8746 #endif #ifndef GL_ATI_texture_env_combine3 #define GL_ATI_texture_env_combine3 1 #endif dd_bool Sys_GLPreInit(void); /** * Initializes our OpenGL interface. Called once during engine statup. */ dd_bool Sys_GLInitialize(void); /** * Close our OpenGL interface for good. Called once during engine shutdown. */ void Sys_GLShutdown(void); /** * Configure the core features of OpenGL. Extensions are not configured here. */ void Sys_GLConfigureDefaultState(void); /** * Echo the full list of available GL extensions to the console. */ void Sys_GLPrintExtensions(void); dd_bool Sys_GLCheckError(void); #endif // __CLIENT__ #ifdef __cplusplus } // extern "C" /** * Information about the OpenGL driver and its capabilities. * * @return Styled text. */ de::String Sys_GLDescription(); #endif #endif /* LIBDENG_SYSTEM_OPENGL_H */ doomsday-stable-1.15.7/doomsday/client/include/gl/gltextureunit.h0000664000175000017500000001201112641367670024405 0ustar jaakkojaakko/** @file gltextureunit.h GL texture unit. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_GLTEXTUREUNIT_H #define DENG_CLIENT_GLTEXTUREUNIT_H #include "Texture" #include #include /** * Of the available GL texture units, only this many will be utilized. * * @todo Find a more suitable home (here because all other GL-domain headers * also include system headers, which aren't needed in components whose API(s) * do not depend on them (and pollute on Windows)). */ #define MAX_TEX_UNITS 2 // More aren't currently used. namespace de { /** * GL Texture unit config. * * @ingroup gl */ class GLTextureUnit { public: /// Managed GL textures encapsulate filter and wrapping management. TextureVariant *texture; /// Unmanged GL textures have an independent state. struct Unmanaged { GLuint glName; gl::Wrapping wrapS; gl::Wrapping wrapT; gl::Filter filter; Unmanaged(GLuint glName = 0, gl::Wrapping wrapS = gl::Repeat, gl::Wrapping wrapT = gl::Repeat, gl::Filter filter = gl::Linear) : glName(glName) , wrapS(wrapS) , wrapT(wrapT) , filter(filter) {} Unmanaged(Unmanaged const &other) : glName(other.glName) , wrapS(other.wrapS) , wrapT(other.wrapT) , filter(other.filter) {} Unmanaged &operator = (Unmanaged const &other) { glName = other.glName; wrapS = other.wrapS; wrapT = other.wrapT; filter = other.filter; return *this; } bool operator == (Unmanaged const &other) const { if(glName != other.glName) return false; if(wrapS != other.wrapS) return false; if(wrapT != other.wrapT) return false; if(filter != other.filter) return false; return true; } bool operator != (Unmanaged const &other) const { return !(*this == other); } } unmanaged; /// Shared properties: float opacity; Vector2f scale; Vector2f offset; GLTextureUnit() : texture(0) , opacity(1) , scale(1, 1) {} GLTextureUnit(TextureVariant &textureVariant, Vector2f const &scale = Vector2f(1, 1), Vector2f const &offset = Vector2f(0, 0), float opacity = 1) : texture(&textureVariant) , opacity(opacity) , scale(scale) , offset(offset) {} GLTextureUnit(GLuint textureGLName, gl::Wrapping textureGLWrapS = gl::Repeat, gl::Wrapping textureGLWrapT = gl::Repeat) : texture(0) , unmanaged(textureGLName, textureGLWrapS, textureGLWrapT) , opacity(1) , scale(1, 1) {} GLTextureUnit(GLTextureUnit const &other) : texture(other.texture) , unmanaged(other.unmanaged) , opacity(other.opacity) , scale(other.scale) , offset(other.offset) {} GLTextureUnit &operator = (GLTextureUnit const &other) { texture = other.texture; unmanaged = other.unmanaged; opacity = other.opacity; scale = other.scale; offset = other.offset; return *this; } bool operator == (GLTextureUnit const &other) const { if(texture) { if(texture != other.texture) return false; } else { if(unmanaged != other.unmanaged) return false; } if(!de::fequal(opacity, other.opacity)) return false; if(scale != other.scale) return false; if(offset != other.offset) return false; return true; } bool operator != (GLTextureUnit const other) const { return !(*this == other); } bool hasTexture() const { return (texture && texture->glName() != 0) || unmanaged.glName != 0; } GLuint getTextureGLName() const { return texture? texture->glName() : unmanaged.glName; } }; } // namespace de #endif // DENG_CLIENT_GLTEXTUREUNIT_H doomsday-stable-1.15.7/doomsday/client/include/gl/gl_defer.h0000664000175000017500000000702112641367670023236 0ustar jaakkojaakko/** @file gl_defer.h Deferred GL tasks. * * @ingroup gl * * GL is only available from the main thread. When accessed from other threads, * the operations need to be deferred for processing later in the main thread. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_GL_DEFERRED_H #define DENG_CLIENT_GL_DEFERRED_H #include "api_gl.h" #include "sys_opengl.h" namespace de { namespace gl { enum UploadMethod { Immediate, ///< Upload the data immediately. Deferred ///< Defer the data upload until convenient. }; } } struct texturecontent_s; /** * Initializes the deferred tasks module. */ void GL_InitDeferredTask(); /** * Shuts down the deferred tasks. */ void GL_ShutdownDeferredTask(); /** * Clears the currently queued GL tasks. They will not be executed. */ void GL_PurgeDeferredTasks(); /** * @return Number of GL tasks waiting to be carried out. */ int GL_DeferredTaskCount(); /** * Processes deferred GL tasks. This must be called from the main thread. * * @param timeOutMilliSeconds Processing will continue until this timeout expires. * Use zero for no timeout. */ void GL_ProcessDeferredTasks(uint timeOutMilliSeconds); DGLuint GL_GetReservedTextureName(); void GL_ReserveNames(); void GL_ReleaseReservedNames(); /** * Returns the chosen method for uploading the given texture @a content. */ de::gl::UploadMethod GL_ChooseUploadMethod(struct texturecontent_s const *content); /** * Adds a new deferred texture upload task to the queue. * * @param content Texture content to upload. Caller can free its copy of the content; * a copy is made for the deferred task. */ void GL_DeferTextureUpload(struct texturecontent_s const *content); void GL_DeferSetVSync(dd_bool enableVSync); #ifdef __cplusplus extern "C" { #endif // Deferring functions for various function signatures. #define LIBDENG_GL_DEFER1(form, x) void GL_Defer_##form(void (GL_CALL *ptr)(x), x) #define LIBDENG_GL_DEFER2(form, x, y) void GL_Defer_##form(void (GL_CALL* ptr)(x, y), x, y) #define LIBDENG_GL_DEFER3(form, x, y, z) void GL_Defer_##form(void (GL_CALL* ptr)(x, y, z), x, y, z) #define LIBDENG_GL_DEFER4(form, x, y, z, w) void GL_Defer_##form(void (GL_CALL* ptr)(x, y, z, w), x, y, z, w) LIBDENG_GL_DEFER1(e, GLenum e); LIBDENG_GL_DEFER2(i, GLenum e, GLint i); LIBDENG_GL_DEFER2(f, GLenum e, GLfloat f); LIBDENG_GL_DEFER2(fv4, GLenum e, const GLfloat* floatArrayFourValues); LIBDENG_GL_DEFER2(uintArray, GLsizei count, const GLuint* values); #ifdef __cplusplus } // extern "C" #endif #endif // DENG_CLIENT_GL_DEFERRED_H doomsday-stable-1.15.7/doomsday/client/include/sys_system.h0000664000175000017500000000361312641367670023312 0ustar jaakkojaakko/** @file sys_system.h Abstract interfaces for platform specific services. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CORE_SYSTEM_H #define DENG_CORE_SYSTEM_H #include #include #include "dd_types.h" extern de::dint novideo; void Sys_Init(); void Sys_Shutdown(); /** * Returns @c true if shutdown is in progress. */ bool Sys_IsShuttingDown(); #undef Sys_Quit DENG_EXTERN_C void Sys_Quit(); void Sys_HideMouseCursor(); de::NativePath Sys_SteamBasePath(); void Sys_Sleep(de::dint millisecs); /** * Blocks the thread for a very short period of time. If attempting to wait * until a time in the past (or for more than 50 ms), returns immediately. * * @param realTimeMs Block until this time is reached. * * @note Longer waits should use Sys_Sleep() -- this is a busy wait. */ void Sys_BlockUntilRealTime(de::duint realTimeMs); de::dint Sys_CriticalMessage(char const *msg); de::dint Sys_CriticalMessagef(char const *format, ...) PRINTF_F(1,2); #endif // DENG_CORE_SYSTEM_H doomsday-stable-1.15.7/doomsday/client/include/Decoration0000664000175000017500000000003712641367670022726 0ustar jaakkojaakko#include "render/decoration.h" doomsday-stable-1.15.7/doomsday/client/include/MaterialManifest0000664000175000017500000000004712641367670024065 0ustar jaakkojaakko#include "resource/materialmanifest.h" doomsday-stable-1.15.7/doomsday/client/include/de_infine.h0000664000175000017500000000173012641367670023006 0ustar jaakkojaakko/** @file * * @authors Copyright © 2010-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * The "In Fine" finale sequence system. */ #ifndef LIBDENG_INFINE #define LIBDENG_INFINE #include "api_infine.h" //#include "ui/infine/finale.h" #endif /* LIBDENG_INFINE */ doomsday-stable-1.15.7/doomsday/client/include/dd_def.h0000664000175000017500000000721712641367670022301 0ustar jaakkojaakko/** * @file dd_def.h * Internal macros and constants for Doomsday. @ingroup base * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef __DOOMSDAY_DEFS_H__ #define __DOOMSDAY_DEFS_H__ /** * @defgroup flags Flags (Internal) */ #include #include #include #include "dd_types.h" #include "api_gameexport.h" #include "api_internaldata.h" #ifdef __cplusplus extern "C" { #endif #ifdef WIN32_MSVC // Disable annoying MSVC warnings. // 4761: integral size mismatch in argument // 4244: conversion from 'type1' to 'type2', possible loss of data #pragma warning (disable:4761 4244) #endif // if rangecheck is undefined, most parameter validation debugging code // will not be compiled #ifndef DENG_NO_RANGECHECKING # define RANGECHECK #endif #ifdef RANGECHECK # define DOOMSDAY_VER_ID_RANGECHECK " +R" #else # define DOOMSDAY_VER_ID_RANGECHECK "" #endif #ifdef _DEBUG # define DOOMSDAY_VER_ID_DEBUG " +D" #else # define DOOMSDAY_VER_ID_DEBUG "" #endif #ifdef __64BIT__ # define DOOMSDAY_VER_ID_64BIT " 64-bit" #else # define DOOMSDAY_VER_ID_64BIT " 32-bit" #endif #if defined(DENG_STABLE) && defined(DOOMSDAY_BUILD_TEXT) # define DOOMSDAY_VER_ID_BUILD " #" DOOMSDAY_BUILD_TEXT #else # define DOOMSDAY_VER_ID_BUILD "" #endif #define DOOMSDAY_VER_ID DOOMSDAY_RELEASE_TYPE DOOMSDAY_VER_ID_64BIT DOOMSDAY_VER_ID_DEBUG DOOMSDAY_VER_ID_RANGECHECK DOOMSDAY_VER_ID_BUILD #define DOOMSDAY_VERSION_FULLTEXT DOOMSDAY_VERSION_TEXT " (" DOOMSDAY_VER_ID ") " __DATE__ " " __TIME__ #define SAFEDIV(x,y) (!(y) || !((x)/(y))? 1 : (x)/(y)) #define ORDER(x,y,a,b) ( (x)<(y)? ((a)=(x),(b)=(y)) : ((b)=(x),(a)=(y)) ) #ifdef _DEBUG # define ASSERT_64BIT(p) {if(sizeof(p) != 8) App_Error(#p " is not 64-bit in " __FILE__ " at line %i.\n", __LINE__);} # define ASSERT_NOT_64BIT(p) {if(sizeof(p) == 8) App_Error(#p " is 64-bit in " __FILE__ " at line %i.\n", __LINE__);} # define ASSERT_32BIT(p) {if(sizeof(p) != 4) App_Error(#p " is not 32-bit in " __FILE__ " at line %i.\n", __LINE__);} # define ASSERT_16BIT(p) {if(sizeof(p) != 2) App_Error(#p " is not 16-bit in " __FILE__ " at line %i.\n", __LINE__);} #else # define ASSERT_64BIT(p) # define ASSERT_NOT_64BIT(p) # define ASSERT_32BIT(p) # define ASSERT_16BIT(p) #endif #define MAXEVENTS 256 #define SBARHEIGHT 39 // status bar height at bottom of screen #define SECONDS_TO_TICKS(sec) ((int)((sec)*35)) // Heap relations. #define HEAP_PARENT(i) (((i) + 1)/2 - 1) #define HEAP_LEFT(i) (2*(i) + 1) #define HEAP_RIGHT(i) (2*(i) + 2) //enum { VX, VY, VZ }; // Vertex indices. enum { CR, CG, CB, CA }; // Color indices. extern float texGamma; // tab_tables.c extern fixed_t finesine[5 * FINEANGLES / 4]; extern fixed_t *fineCosine; #ifdef __cplusplus } // extern "C" #endif // dd_pinit.c DENG_EXTERN_C game_export_t __gx; #define gx __gx #endif doomsday-stable-1.15.7/doomsday/client/include/Generator0000664000175000017500000000003512641367670022563 0ustar jaakkojaakko#include "world/generator.h" doomsday-stable-1.15.7/doomsday/client/include/WallSpec0000664000175000017500000000003512641367670022347 0ustar jaakkojaakko#include "render/wallspec.h" doomsday-stable-1.15.7/doomsday/client/include/partition.h0000664000175000017500000000770512641367670023107 0ustar jaakkojaakko/** @file partition.h Infinite line of the form point + direction vector. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_MATH_PARTITION #define DENG_MATH_PARTITION #include #include #include #include namespace de { /** * An infinite line of the form point + direction vector. The members are public * for convenient access. * * @ingroup math */ class Partition { public: Vector2d direction; Vector2d origin; public: Partition(Vector2d const &direction = Vector2d(), Vector2d const &origin = Vector2d()) : direction(direction), origin(origin) {} Partition(Partition const &other) : direction(other.direction), origin(other.origin) {} /** * Where does the given @a point lie relative to the partition line? * * @param point The point to test. * * @return @c <0 Point is to the left of the line. * @c =0 Point lies directly on/incident with the line. * @c >0 Point is to the right of the line. */ ddouble pointOnSide(Vector2d const &point) const { return (origin.y - point.y) * direction.x - (origin.x - point.x) * direction.y; } /** * Returns @c true iff "this" line and @a other are parallel. In the special * case of either line having a zero-length direction, @c true is returned. */ bool isParallelTo(Partition const &other, ddouble epsilon = .99999999) const { ddouble len = direction.length(); if(len == 0) return true; ddouble otherLen = other.direction.length(); if(otherLen == 0) return true; ddouble dot = direction.dot(other.direction) / len / otherLen; // If it's close enough, we'll consider them parallel. epsilon = de::abs(epsilon); return dot > epsilon || dot < -epsilon; } /** * Determines how far along "this" line (relative to the origin) that the * @a other line and this intersect. * * @return Intersection point expressed as a scale factor, relative to the * line origin. In the special case of the two lines being parallel @c 0 is * returned. * * @see intercept() */ double intersection(Partition const &other) const { ddouble divsor = direction.x * other.direction.y - direction.y * other.direction.x; // Special case: parallel? if(divsor == 0) return 0; Vector2d delta = origin - other.origin; return (delta.y * other.direction.x - delta.x * other.direction.y) / divsor; } /** * Determine the intercept point where "this" line and @a other intersect * and return the Euclidean point at which the two intercept. */ inline Vector2d intercept(Partition const &other) const { return origin + direction * intersection(other); } String asText() const { String str; QTextStream s(&str); s << direction.x << "/" << direction.y << " " << origin.asText(); return str; } }; } // namespace de #endif // DENG_MATH_PARTITION doomsday-stable-1.15.7/doomsday/client/include/windows/0000775000175000017500000000000012641367670022406 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/windows/mouse_win32.h0000664000175000017500000000240412641367670024731 0ustar jaakkojaakko/** * @file mouse_win32.h * Mouse driver that gets mouse input from DirectInput on Windows. * @ingroup input * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_MOUSE_WIN32_H #define LIBDENG_MOUSE_WIN32_H #include "ui/sys_input.h" #ifdef __cplusplus extern "C" { #endif extern mouseinterface_t win32Mouse; #ifdef __cplusplus } // extern "C" #endif #endif // LIBDENG_MOUSE_WIN32_H doomsday-stable-1.15.7/doomsday/client/include/windows/directinput.h0000664000175000017500000000661512641367670025121 0ustar jaakkojaakko#ifndef LIBDENG_DIRECTINPUT_H #define LIBDENG_DIRECTINPUT_H #ifdef __CLIENT__ #define WIN32_LEAN_AND_MEAN #define DIRECTINPUT_VERSION 0x0800 //#define _WIN32_WINNT 0x0501 #include #include #include #define I_SAFE_RELEASE(d) { if(d) { (d)->Release(); (d) = NULL; } } #ifdef __cplusplus extern "C" { #endif /** * Attempt to initialize an application-global interface to DirectInput. First * the version 8 interface (if available on the host system) and if unsuccessful, * then the older version 3 interface. * * @note The caller must ensure that COM has been initialized else this will * fail and false will be returned. * * @return @c true if an interface was initialized successfully. */ int DirectInput_Init(void); /** * Shutdown the open DirectInput interface if initialized. */ void DirectInput_Shutdown(void); /** * Retrieve a handle to the version 8 interface. * @return Interface instance handle or @c NULL if not initialized. */ LPDIRECTINPUT8 DirectInput_IVersion8(); /** * Retrieve a handle to the version 3 interface. * @return Interface instance handle or @c NULL if not initialized. */ LPDIRECTINPUT DirectInput_IVersion3(); /** * Releases and then destroys a DirectInput device. * @param dev Address of the device instance to be destroyed. Can be @c NULL. */ void DirectInput_KillDevice(LPDIRECTINPUTDEVICE8* dev); /** * Retrieve a plain text explanation of a DirectInput error code suitable for * printing to the error log/displaying to the user. * * @param hr DirectInput Error code to be translated. * @return Plain text explanation. Always returns a valid cstring. */ const char* DirectInput_ErrorMsg(HRESULT hr); #ifdef __cplusplus } // extern "C" #endif #ifdef __cplusplus /** * A handy adaptor for manipulating a DIPROPDWORD structure. */ struct DIPropDWord : DIPROPDWORD { DIPropDWord(DWORD how=0, DWORD object=0, DWORD data=0) { initHeader(); setHow(how); setObject(object); setData(data); } operator DIPROPHEADER*() { return &diph; } operator const DIPROPHEADER*() const { return &diph; } inline DIPropDWord& setHow(DWORD how) { diph.dwHow = how; return *this; } inline DIPropDWord& setObject(DWORD obj) { diph.dwObj = obj; return *this; } inline DIPropDWord& setData(DWORD data) { dwData = data; return *this; } private: void initHeader() { diph.dwSize = sizeof(DIPROPDWORD); diph.dwHeaderSize = sizeof(diph); } }; /** * A handy adaptor for manipulating a DIPROPRANGE structure. */ struct DIPropRange : DIPROPRANGE { DIPropRange(DWORD how=0, DWORD object=0, int min=0, int max=0) { initHeader(); setHow(how); setObject(object); setMin(min); setMax(max); } operator DIPROPHEADER*() { return &diph; } operator const DIPROPHEADER*() const { return &diph; } inline DIPropRange& setHow(DWORD how) { diph.dwHow = how; return *this; } inline DIPropRange& setObject(DWORD obj) { diph.dwObj = obj; return *this; } inline DIPropRange& setMin(int min) { lMin = min; return *this; } inline DIPropRange& setMax(int max) { lMax = max; return *this; } private: void initHeader() { diph.dwSize = sizeof(DIPROPRANGE); diph.dwHeaderSize = sizeof(diph); } }; #endif // __cplusplus #endif // __CLIENT__ #endif // LIBDENG_DIRECTINPUT_H doomsday-stable-1.15.7/doomsday/client/include/windows/resource.h0000664000175000017500000000074412641367670024413 0ustar jaakkojaakko//{{NO_DEPENDENCIES}} // Microsoft Developer Studio generated include file. // Used by Doomsday.rc // #define IDI_MAIN 101 #define IDI_DOOMSDAY_ICON 106 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 117 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1005 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif doomsday-stable-1.15.7/doomsday/client/include/windows/dd_winit.h0000664000175000017500000000267112641367670024366 0ustar jaakkojaakko/** @file dd_winit.h Win32 Initialization. * * @authors Copyright © 2003-2014 Jaakko Kernen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDENG_WINIT_H #define LIBDENG_WINIT_H #define WIN32_LEAN_AND_MEAN #include "dd_pinit.h" #include #ifdef __cplusplus extern "C" { #endif typedef struct { HINSTANCE hInstance; /// @c true = We are using a custom user dir specified on the command line. BOOL usingUserDir; GETGAMEAPI GetGameAPI; } application_t; extern application_t app; dd_bool DD_Win32_Init(void); void DD_Shutdown(void); char const *DD_Win32_GetLastErrorMessage(void); #ifdef __cplusplus } // extern "C" #endif #endif /* LIBDENG_WINIT_H */ doomsday-stable-1.15.7/doomsday/client/include/BitmapFont0000664000175000017500000000004112641367670022675 0ustar jaakkojaakko#include "resource/bitmapfont.h" doomsday-stable-1.15.7/doomsday/client/include/Face0000664000175000017500000000002212641367670021467 0ustar jaakkojaakko#include "face.h" doomsday-stable-1.15.7/doomsday/client/include/SettingsRegister0000664000175000017500000000003612641367670024143 0ustar jaakkojaakko#include "settingsregister.h" doomsday-stable-1.15.7/doomsday/client/include/game.h0000664000175000017500000001721012641367670021777 0ustar jaakkojaakko/** @file game.h Game mode configuration (metadata, resource files, etc...). * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_GAME_H #define DENG_GAME_H #include "api_plugin.h" #include #include #include #include #include struct manifest_s; struct gamedef_s; namespace de { class File1; class ResourceManifest; /** * Records top-level game configurations registered by the loaded game plugin(s). * * @ingroup core */ class Game : public de::game::Game { public: /// Logical game status: enum Status { Loaded, Complete, Incomplete }; typedef QMultiMap Manifests; public: /** * @param identityKey Unique game mode key/identifier, 16 chars max (e.g., "doom1-ultimate"). * @param configDir Name of the config directory. * @param title Textual title for the game mode (intended for humans). * @param author Textual author for the game mode (intended for humans). * @param legacySavegameNameExp Regular expression used for matching legacy savegame names. * @param legacySavegameSubfoler Game-specific subdirectory of /home for legacy savegames. * @param mapMapInfo Base relative path to the main MAPINFO definition data. */ Game(String const &identityKey, Path const &configDir, String const &title = "Unnamed", String const &author = "Unknown", String const &legacySavegameNameExp = "", String const &legacySavegameSubfolder = "", String const &mainMapInfo = ""); virtual ~Game(); /** * Determines the status of the game. * * @see statusAsText() */ Status status() const; /** * Returns a textual representation of the current game status. * * @see status() */ String const &statusAsText() const; /** * Returns information about the game as styled text. Printed by "inspectgame", * for instance. */ String description() const; /** * Returns the unique identifier of the plugin which registered the game. */ pluginid_t pluginId() const; /** * Change the identfier of the plugin associated with this. * @param newId New identifier. */ void setPluginId(pluginid_t newId); /** * Returns the unique identity key of the game. */ de::String const &identityKey() const; /** * Returns the title of the game, as text. */ de::String title() const; /** * Returns the author of the game, as text. */ de::String const &author() const; /** * Returns the name of the main config file for the game. */ de::Path const &mainConfig() const; /** * Returns the name of the binding config file for the game. */ de::Path const &bindingConfig() const; /** * Returns the base relative path of the main MAPINFO definition data for the game (if any). */ de::Path const &mainMapInfo() const; /** * Returns the identifier of the Style logo image to represent this game. */ de::String logoImageId() const; /** * Returns the regular expression used for locating legacy savegame files. */ String legacySavegameNameExp() const; /** * Determine the absolute path to the legacy savegame folder for the game. If there is * no possibility of a legacy savegame existing (e.g., because the game is newer than * the introduction of the modern, package-based .save format) then a zero length string * is returned. */ String legacySavegamePath() const; /** * Add a new manifest to the list of manifests. * * @note Registration order defines load order (among files of the same class). * * @param manifest Manifest to add. */ void addManifest(ResourceManifest &manifest); bool allStartupFilesFound() const; /** * Provides access to the manifests for efficent traversals. */ Manifests const &manifests() const; /** * Is @a file required by this game? This decision is made by comparing the * absolute path of the specified file to those in the list of located, startup * resources for the game. If the file's path matches one of these it is therefore * "required" by this game. * * @param file File to be tested for required-status. Can be a contained file * (such as a lump from a Wad file), in which case the path of the * root (i.e., outermost file) file is used for testing this status. * * @return @c true iff @a file is required by this game. */ bool isRequiredFile(File1 &file); public: /** * Construct a new Game instance from the specified definition @a def. * * @note May fail if the definition is incomplete or invalid (@c NULL is returned). */ static Game *fromDef(GameDef const &def); /** * Print a game mode banner with rulers. * * @todo This has been moved here so that strings like the game title and author * can be overridden (e.g., via DEHACKED). Make it so! */ static void printBanner(Game const &game); /** * Composes a list of the resource files of the game. * * @param rflags Only consider files whose @ref fileFlags match * this value. If @c <0 the flags are ignored. * @param withStatus @c true to include the current availability/load status * of each file. */ String filesAsText(int rflags, bool withStatus = true) const; static void printFiles(Game const &game, int rflags, bool printStatus = true); /// Register the console commands, variables, etc..., of this module. static void consoleRegister(); private: DENG2_PRIVATE(d) }; typedef Game::Manifests GameManifests; /** * The special "null" Game object. * @todo Should employ the Singleton pattern. */ class NullGame : public Game { public: /// General exception for invalid action on a NULL object. @ingroup errors DENG2_ERROR(NullObjectError); public: NullGame(); void addManifest(struct manifest_s & /*record*/) { throw NullObjectError("NullGame::addResource", "Invalid action on null-object"); } bool isRequiredResource(char const * /*absolutePath*/) { return false; // Never. } bool allStartupFilesFound() const { return true; // Always. } struct manifest_s *const *manifests(resourceclassid_t /*classId*/, int * /*count*/) const { return 0; } static Game *fromDef(GameDef const & /*def*/) { throw NullObjectError("NullGame::fromDef", "Not valid for null-object"); } }; } // namespace de #endif /* DENG_GAME_H */ doomsday-stable-1.15.7/doomsday/client/include/MaterialScheme0000664000175000017500000000004512641367670023521 0ustar jaakkojaakko#include "resource/materialscheme.h" doomsday-stable-1.15.7/doomsday/client/include/dd_loop.h0000664000175000017500000000567212641367670022517 0ustar jaakkojaakko/** @file dd_loop.h Main loop and the core timer. * @ingroup base * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef __DOOMSDAY_BASELOOP_H__ #define __DOOMSDAY_BASELOOP_H__ #include "dd_types.h" DENG_EXTERN_C float frameTimePos; // 0...1: fractional part for sharp game tics DENG_EXTERN_C int rFrameCount; DENG_EXTERN_C timespan_t sysTime, gameTime, demoTime; DENG_EXTERN_C dd_bool tickFrame; #ifdef __cplusplus extern "C" { #endif /** * Register console variables for main loop. */ void DD_RegisterLoop(void); /** * Runs one or more tics depending on how much time has passed since the * previous call to this function. This gets called once per each main loop * iteration. Finishes as quickly as possible. */ void Loop_RunTics(void); /** * Waits until it's time to show the drawn frame on screen. The frame must be * ready before this is called. Ideally the updates would appear at a fixed * frequency; in practice, inaccuracies due to time measurement and background * processes may result in varying update intervals. * * Note that if the maximum refresh rate has been set to a value higher than * the vsync rate, this function does nothing but update the statistisc on * frame timing. */ void DD_WaitForOptimalUpdateTime(void); /** * Returns the current frame rate. */ float DD_GetFrameRate(void); /** * Reset the core timer so that on the next frame, it seems like be that no * time has passed. */ void DD_ResetTimer(void); /** * Determines whether frame time is advancing. */ dd_bool DD_IsFrameTimeAdvancing(void); /** * Returns the real time in seconds when the latest iteration of runTics() was * started. */ timespan_t DD_LatestRunTicsStartTime(void); /** * Returns how much time has elapsed during the current tick. */ timespan_t DD_CurrentTickDuration(void); /** * Sets the exit code for the main loop. Does not cause the main loop * to stop; you need to call Sys_Quit() to do that. */ void DD_SetGameLoopExitCode(int code); /** * @return Game loop exit code. */ int DD_GameLoopExitCode(void); #ifdef __cplusplus } // extern "C" #endif #endif doomsday-stable-1.15.7/doomsday/client/include/precompiled.h0000664000175000017500000000451012641367670023370 0ustar jaakkojaakko/** @file precompiled.h Precompiled headers for Doomsday Client. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include #include #include #include #ifdef WIN32 # define WIN32_LEAN_AND_MEAN # include # undef min # undef max #endif #ifdef __cplusplus // C++ standard library: #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Qt: #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Doomsday SDK: #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // libdoomsday: // unfortunately, the API macros conflict with precompiling #endif doomsday-stable-1.15.7/doomsday/client/include/r_util.h0000664000175000017500000001030612641367670022363 0ustar jaakkojaakko/** @file r_util.h Refresh Utility Routines. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_REFRESH_UTIL_H #define DENG_REFRESH_UTIL_H #include #include #include #include "api_gl.h" #ifdef __CLIENT__ #undef min #undef max /** * Description of an inclusive..inclusive light intensity range. * * @ingroup data */ struct LightRange { float min; float max; LightRange(float _min = 0, float _max = 0) : min(_min), max(_max) {} LightRange(float const minMax[2]) : min(minMax[0]), max(minMax[1]) {} LightRange(de::Vector2f const &minMax) : min(minMax.x), max(minMax.y) {} LightRange(LightRange const &other) : min(other.min), max(other.max) {} /// Returns a textual representation of the lightlevels. de::String asText() const { return de::String("(min: %1 max: %2)").arg(min, 0, 'f', 2).arg(max, 0, 'f', 2); } }; #endif float R_MovementYaw(float const mom[2]); float R_MovementXYYaw(float momx, float momy); float R_MovementPitch(float const mom[3]); float R_MovementXYZPitch(float momx, float momy, float momz); #ifdef __CLIENT__ /** * Get a global angle from Cartesian coordinates in the map coordinate space * relative to the viewer. * * @param point Map point to test. * * @return Angle between the test point and view x,y. */ angle_t R_ViewPointToAngle(de::Vector2d point); /// @copydoc R_ViewPointToAngle() inline angle_t R_ViewPointToAngle(coord_t x, coord_t y) { return R_ViewPointToAngle(de::Vector2d(x, y)); } /** * Determine distance to the specified point relative to the viewer. * * @param x X coordinate to test. * @param y Y coordinate to test. * * @return Distance from the viewer to the test point. */ coord_t R_ViewPointDistance(coord_t x, coord_t y); #endif de::Vector3d R_ClosestPointOnPlane(de::Vector3f const &planeNormal, de::Vector3d const &planePoint, de::Vector3d const &origin); #ifdef __CLIENT__ void R_ProjectViewRelativeLine2D(coord_t const center[2], dd_bool alignToViewPlane, coord_t width, coord_t offset, coord_t start[2], coord_t end[2]); void R_ProjectViewRelativeLine2D(de::Vector2d const center, bool alignToViewPlane, coord_t width, coord_t offset, de::Vector2d &start, de::Vector2d &end); #endif /** * Scale @a color uniformly so that the highest component becomes one. */ void R_AmplifyColor(de::Vector3f &color); void R_ScaleAmbientRGB(float *out, float const *in, float mul); /** * Generate texcoords on the surface centered on point. * * @param s Texture s coords written back here. * @param t Texture t coords written back here. * @param point Point on surface around which texture is centered. * @param xScale Scale multiplier on the horizontal axis. * @param yScale Scale multiplier on the vertical axis. * @param v1 Top left vertex of the surface being projected on. * @param v2 Bottom right vertex of the surface being projected on. * @param tangentMatrix Normalized tangent space matrix for the surface being projected to. * * @return @c true if the generated coords are within bounds. */ bool R_GenerateTexCoords(de::Vector2f &s, de::Vector2f &t, de::Vector3d const &point, float xScale, float yScale, de::Vector3d const &v1, de::Vector3d const &v2, de::Matrix3f const &tangentMatrix); char const *R_NameForBlendMode(blendmode_t mode); #endif // DENG_REFRESH_UTIL_H doomsday-stable-1.15.7/doomsday/client/include/Interceptor0000664000175000017500000000003712641367670023135 0ustar jaakkojaakko#include "world/interceptor.h" doomsday-stable-1.15.7/doomsday/client/include/Material0000664000175000017500000000003712641367670022375 0ustar jaakkojaakko#include "resource/material.h" doomsday-stable-1.15.7/doomsday/client/include/BiasIllum0000664000175000017500000000003612641367670022517 0ustar jaakkojaakko#include "render/biasillum.h" doomsday-stable-1.15.7/doomsday/client/include/Line0000664000175000017500000000003012641367670021517 0ustar jaakkojaakko#include "world/line.h" doomsday-stable-1.15.7/doomsday/client/include/updater/0000775000175000017500000000000012641367670022360 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/updater/downloaddialog.h0000664000175000017500000000415012641367670025520 0ustar jaakkojaakko/** * @file downloaddialog.h * Dialog that downloads a distribution package. @ingroup updater * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_DOWNLOADDIALOG_H #define DENG_CLIENT_DOWNLOADDIALOG_H #include class QNetworkReply; /** * Dialog for downloading an update in the background and then starting * the (re)installation process. */ class DownloadDialog : public de::DialogWidget { Q_OBJECT public: DownloadDialog(de::String downloadUri, de::String fallbackUri); ~DownloadDialog(); /** * Returns the path of the downloaded file. * * @return Path, or an empty string if the download did not finish * successfully. */ de::String downloadedFilePath() const; bool isReadyToInstall() const; bool isFailed() const; public: static bool isDownloadInProgress(); static DownloadDialog ¤tDownload(); static void showCompletedDownload(); signals: void downloadProgress(int percentage); void downloadFailed(QString uri); public slots: void replyMetaDataChanged(); void progress(qint64 received, qint64 total); void finished(QNetworkReply *); void cancel(); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_DOWNLOADDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/updater/processcheckdialog.h0000664000175000017500000000350112641367670026364 0ustar jaakkojaakko/** * @file processcheckdialog.h * Dialog for checking running processes on Windows. @ingroup updater * * Windows cannot overwrite files that are in use. Thus the updater needs * to ensure that all Snowberry is shut down before starting an update. * * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_PROCESSCHECKDIALOG_H #define DENG_CLIENT_PROCESSCHECKDIALOG_H #include // This is only for Windows. #ifdef WIN32 /** * Asks the user to stop a process if it is found to be running. * * @param processName Name of the process to check for (e.g., "snowberry.exe"). * @param message Message to display to the user if the process is * running. Should describe why the process needs to be * stopped. * * @return @c true, if the process has been stopped. Otherwise @c false, * the process is still running. */ dd_bool Updater_AskToStopProcess(char const *processName, char const *message); #endif // WIN32 #endif // DENG_CLIENT_PROCESSCHECKDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/updater/updatersettings.h0000664000175000017500000000515712641367670025766 0ustar jaakkojaakko/** * @file updatersettings.h * Persistent settings for automatic updates. @ingroup updater * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_UPDATERSETTINGS_H #define DENG_CLIENT_UPDATERSETTINGS_H #include #include #include /** * Convenient interface to the Updater settings. All changes to the settings * are immediately saved persistently. In practice, the values are stored in * Config.updater. */ class UpdaterSettings { public: enum Frequency { Daily = 0, Biweekly = 1, // Tuesday + Saturday Weekly = 2, // 7 days Monthly = 3, // 30 days AtStartup = 4 }; enum Channel { Stable = 0, Unstable = 1 }; public: UpdaterSettings(); Frequency frequency() const; Channel channel() const; de::Time lastCheckTime() const; bool onlyCheckManually() const; bool autoDownload() const; bool deleteAfterUpdate() const; bool isDefaultDownloadPath() const; de::NativePath downloadPath() const; de::NativePath pathToDeleteAtStartup() const; /** * @return Human-readable description of when the latest update * check was made. */ de::String lastCheckAgo() const; void setFrequency(Frequency freq); void setChannel(Channel channel); void setLastCheckTime(const de::Time& time); void setOnlyCheckManually(bool onlyManually); void setAutoDownload(bool autoDl); void setDeleteAfterUpdate(bool deleteAfter); void setDownloadPath(de::NativePath downloadPath); void useDefaultDownloadPath(); void setPathToDeleteAtStartup(de::NativePath deletePath); static de::NativePath defaultDownloadPath(); }; #endif // DENG_CLIENT_UPDATERSETTINGS_H doomsday-stable-1.15.7/doomsday/client/include/updater/updateavailabledialog.h0000664000175000017500000000335112641367670027036 0ustar jaakkojaakko/** * @file updateavailabledialog.h * Dialog for notifying the user about available updates. @ingroup updater * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_UPDATEAVAILABLEDIALOG_H #define DENG_CLIENT_UPDATEAVAILABLEDIALOG_H #include #include "versioninfo.h" class UpdateAvailableDialog : public de::MessageDialog { Q_OBJECT public: /// The dialog is initialized with the "Checking" page visible. UpdateAvailableDialog(); /// The dialog is initialized with the result page visible. UpdateAvailableDialog(VersionInfo const& latestVersion, de::String changeLogUri); public slots: void showResult(VersionInfo const &latestVersion, de::String changeLogUri); void showWhatsNew(); void editSettings(); signals: void checkAgain(); private: DENG2_PRIVATE(d) }; #endif // LIBDENG_UPDATEAVAILABLEDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/updater/updatersettingsdialog.h0000664000175000017500000000333612641367670027143 0ustar jaakkojaakko/** * @file updatersettingsdialog.h * Dialog for configuring automatic updates. @ingroup updater * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_UPDATERSETTINGSDIALOG_H #define DENG_CLIENT_UPDATERSETTINGSDIALOG_H #include /** * Dialog for configuring the settings of the automatic updater. */ class UpdaterSettingsDialog : public de::DialogWidget { Q_OBJECT public: enum Mode { Normal = 0, WithApplyAndCheckButton = 1 }; UpdaterSettingsDialog(Mode mode = Normal, de::String const &name = "updatersettings"); /** * Determines whether settings have changed. */ bool settingsHaveChanged() const; public slots: void apply(); void applyAndCheckNow(); protected: void finish(int result); private: DENG2_PRIVATE(d) }; #endif // DENG_CLIENT_UPDATERSETTINGSDIALOG_H doomsday-stable-1.15.7/doomsday/client/include/ModelDef0000664000175000017500000000003712641367670022316 0ustar jaakkojaakko#include "resource/modeldef.h" doomsday-stable-1.15.7/doomsday/client/include/TextureManifest0000664000175000017500000000004612641367670023766 0ustar jaakkojaakko#include "resource/texturemanifest.h" doomsday-stable-1.15.7/doomsday/client/include/BiasDigest0000664000175000017500000000003712641367670022655 0ustar jaakkojaakko#include "render/biasdigest.h" doomsday-stable-1.15.7/doomsday/client/include/busymode.h0000664000175000017500000000453312641367670022721 0ustar jaakkojaakko/** * @file busymode.h * Busy Mode @ingroup core * * The Busy Mode is intended for long-running tasks that would otherwise block * the main engine (UI) thread. During busy mode, a progress screen is shown by * the main thread while a background thread works on a long operation. The * normal Doomsday UI cannot be interacted with while the task is running. The * busy mode can be configured to show a progress bar, the console output, * and/or a description of the task being carried out. * * @todo Refactor: Remove the busy mode loop to prevent the app UI from * freezing while busy mode is running. Instead, busy mode should run in the * regular application event loop. During busy mode, the game loop callback * should not be called. * * @authors Copyright © 2009-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef LIBDENG_CORE_BUSYMODE_H #define LIBDENG_CORE_BUSYMODE_H #include "dd_share.h" #include "api_busy.h" /// Enables or disables busy mode; if disabled, work is done synchronously /// in the main thread. void BusyMode_SetAllowed(dd_bool allow); /// @return @c true if specified thread is the current busy task worker. dd_bool BusyMode_IsWorkerThread(uint threadId); /// @return @c true if the current thread is the busy task worker. dd_bool BusyMode_InWorkerThread(void); #ifdef __CLIENT__ dd_bool BusyMode_IsTransitionAnimated(void); #endif /// @return Current busy task, else @c NULL. BusyTask* BusyMode_CurrentTask(void); void BusyMode_Loop(void); #endif // LIBDENG_CORE_BUSYMODE_H doomsday-stable-1.15.7/doomsday/client/include/de_play.h0000664000175000017500000000265012641367670022505 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * de_play.h: Game World Events (Playsim) */ #ifndef __DOOMSDAY_PLAYSIM__ #define __DOOMSDAY_PLAYSIM__ #include "api_thinker.h" #include "BspNode" #ifdef __CLIENT__ # include "Contact" #endif #include "Generator" #include "Line" #include "Plane" #include "Polyobj" #include "Sector" #include "Surface" #include "Vertex" #include "world/dmuargs.h" #include "world/linesighttest.h" #include "world/p_object.h" #include "world/p_ticker.h" #include "world/p_players.h" #include "world/thinkers.h" #include "Material" #include "r_util.h" #include "api_map.h" #endif doomsday-stable-1.15.7/doomsday/client/include/BindContext0000664000175000017500000000003412641367670023055 0ustar jaakkojaakko#include "ui/bindcontext.h" doomsday-stable-1.15.7/doomsday/client/include/macx/0000775000175000017500000000000012641367670021644 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/macx/MusicPlayer.h0000664000175000017500000000213412641367670024252 0ustar jaakkojaakko/** @file * * @authors Copyright © 2011 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301 USA */ #import #import @interface MusicPlayer : NSObject { QTMovie* currentSong; float songVolume; } - (id)init; - (void)shutdown; - (int)playFile:(const char*)filename looping:(int)loop; - (void)setVolume:(float)volume; - (void)play; - (void)stop; - (void)rewind; @end doomsday-stable-1.15.7/doomsday/client/include/macx/cursor_macx.h0000664000175000017500000000207412641367670024345 0ustar jaakkojaakko/** @file cursor_macx.h Native OS X mouse cursor functions. * @ingroup ui * * @authors Copyright © 2015 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_MACX_CURSOR_H #define CLIENT_MACX_CURSOR_H /** * Show or hide the mouse cursor. * * @param show @c true to show, @c false to hide. */ void Cursor_Show(bool show); #endif // CLIENT_MACX_CURSOR_H doomsday-stable-1.15.7/doomsday/client/include/HueCircleVisual0000664000175000017500000000004412641367670023664 0ustar jaakkojaakko#include "render/huecirclevisual.h" doomsday-stable-1.15.7/doomsday/client/include/edit_map.h0000664000175000017500000000254712641367670022657 0ustar jaakkojaakko/** @file edit_map.h Internal runtime map editing interface. * * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_WORLD_EDITMAP_H #define DENG_WORLD_EDITMAP_H #include "world/map.h" /** * Provides access to the current map being built with the runtime map editing * interface. If no map is currently being built then @c 0 is returned. Ownership * of the map is @em NOT given to the caller. * * @see MPE_TakeMap() */ de::Map *MPE_Map(); /** * Take ownership of the last map built with the runtime map editing interface. * May return @c 0 if no such map exists. */ de::Map *MPE_TakeMap(); #endif // DENG_WORLD_EDITMAP_H doomsday-stable-1.15.7/doomsday/client/include/MaterialArchive0000664000175000017500000000004612641367670023677 0ustar jaakkojaakko#include "resource/materialarchive.h" doomsday-stable-1.15.7/doomsday/client/include/Binding0000664000175000017500000000003012641367670022202 0ustar jaakkojaakko#include "ui/binding.h" doomsday-stable-1.15.7/doomsday/client/include/dd_main.h0000664000175000017500000001137412641367670022466 0ustar jaakkojaakko/** @file dd_main.h * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_MAIN_H #define DENG_MAIN_H #include #include #include #include #include #include "api_plugin.h" #include "api_gameexport.h" #include "Games" #include "resource/resourcesystem.h" #include "world/worldsystem.h" #include "ui/infine/infinesystem.h" namespace de { class File1; } #ifndef WIN32 extern GETGAMEAPI GetGameAPI; #endif extern de::dint verbose; extern de::dint isDedicated; extern de::dint gameDataFormat; #ifdef __CLIENT__ extern de::dint symbolicEchoMode; #endif de::dint DD_EarlyInit(); void DD_FinishInitializationAfterWindowReady(); /** * Returns @c true if shutdown is in progress. */ bool DD_IsShuttingDown(); /** * Print an error message and quit. */ void App_Error(char const *error, ...); void App_AbnormalShutdown(char const *error); /// Returns the application's global InFineSystem. InFineSystem &App_InFineSystem(); /// Returns the application's global WorldSystem. de::WorldSystem &App_WorldSystem(); #undef Con_Open /** * Attempt to change the 'open' state of the console. * @note In dedicated mode the console cannot be closed. */ void Con_Open(de::dint yes); void DD_CheckTimeDemo(); void DD_UpdateEngineState(); // // Resources (logical) ------------------------------------------------------------ // /// Returns the application's global ResourceSystem. ResourceSystem &App_ResourceSystem(); /** * Convenient method of returning a resource class from the application's global * resource system. */ ResourceClass &App_ResourceClass(de::String className); /// @overload ResourceClass &App_ResourceClass(resourceclassid_t classId); // // Game modules ------------------------------------------------------------------- // /** * Switch to/activate the specified game. */ bool App_ChangeGame(de::Game &game, bool allowReload = false); /** * Returns @c true if a game module is presently loaded. */ dd_bool App_GameLoaded(); /** * Returns the application's global Games (collection). */ de::Games &App_Games(); /** * Returns the current game from the application's global collection. */ de::Game &App_CurrentGame(); /** * Frees the info structures for all registered games. */ void App_ClearGames(); // // Plugins ------------------------------------------------------------------------ // /** * Loads all the plugins from the library directory. Note that audio plugins * are not loaded here, they are managed by AudioDriver. */ void Plug_LoadAll(); /** * Unloads all plugins. */ void Plug_UnloadAll(); /** * @return Unique identifier of the currently active plugin. The currently * active plugin is tracked separately for each thread. */ pluginid_t DD_ActivePluginId(); /** * Sets the ID of the currently active plugin in the current thread. * * @param id Plugin id. */ void DD_SetActivePluginId(pluginid_t id); /** * Executes all the hooks of the given type. Bit zero of the return value * is set if a hook was executed successfully (returned true). Bit one is * set if all the hooks that were executed returned true. */ de::dint DD_CallHooks(de::dint hook_type, de::dint parm, void *context); de::LibraryFile const &Plug_FileForPlugin(pluginid_t id); bool DD_ExchangeGamePluginEntryPoints(pluginid_t pluginId); /** * Locate the address of the named, exported procedure in the plugin. */ void *DD_FindEntryPoint(pluginid_t pluginId, char const *fn); // // Misc/utils --------------------------------------------------------------------- // /** * Attempts to read help strings from the game-specific help file. */ void DD_ReadGameHelp(); /// @return Symbolic name of the material scheme associated with @a textureSchemeName. AutoStr *DD_MaterialSchemeNameForTextureScheme(Str const *textureSchemeName); /// @overload de::String DD_MaterialSchemeNameForTextureScheme(de::String textureSchemeName); fontschemeid_t DD_ParseFontSchemeName(char const *str); #endif // DENG_MAIN_H doomsday-stable-1.15.7/doomsday/client/include/BspLeaf0000664000175000017500000000003312641367670022147 0ustar jaakkojaakko#include "world/bspleaf.h" doomsday-stable-1.15.7/doomsday/client/include/de_base.h0000664000175000017500000000327712641367670022460 0ustar jaakkojaakko/** @file de_base.h Engine Core. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef LIBDENG_BASE_H #define LIBDENG_BASE_H // System headers needed everywhere. #include #include "de_platform.h" #include #include #include #include #include #include #include "dd_def.h" #include "dd_share.h" #include "games.h" #include "api_gameexport.h" #include "api_plugin.h" #include "dd_main.h" #include "dd_loop.h" #include "library.h" #include "busymode.h" #include "ui/ddevent.h" #include "ui/nativeui.h" #include "ui/zonedebug.h" #include #include #ifdef __SERVER__ // Many subsystems do not exist on the server. This is a temporary measure // to allow compilation without pulling everything apart just yet. # include "server_dummies.h" #endif #endif // LIBDENG_BASE_H doomsday-stable-1.15.7/doomsday/client/include/m_nodepile.h0000664000175000017500000000420512641367670023201 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /* * m_nodepile.h: Specialized Node Allocation */ #ifndef __DOOMSDAY_NODEPILE_H__ #define __DOOMSDAY_NODEPILE_H__ #define NP_ROOT_NODE ((void*) -1) #include "dd_types.h" /** * Linknodes are used when linking mobjs to lines. Each mobj has a ring * of linknodes, each node pointing to a line the mobj has been linked to. * Correspondingly each line has a ring of nodes, with pointers to the * mobjs that are linked to that particular line. This way it is possible * that a single mobj is linked simultaneously to multiple lines (which * is common). * * All these rings are maintained by Mobj_Link() and Mobj_Unlink(). * @ingroup mobj */ typedef struct linknode_s { nodeindex_t prev, next; void* ptr; int data; } linknode_t; typedef struct nodepile_s { int count; int pos; struct linknode_s *nodes; } nodepile_t; #ifdef __cplusplus extern "C" { #endif void NP_Init(nodepile_t *pile, int initial); nodeindex_t NP_New(nodepile_t *pile, void *ptr); void NP_Link(nodepile_t *pile, nodeindex_t node, nodeindex_t root); void NP_Unlink(nodepile_t *pile, nodeindex_t node); #define NP_Dismiss(pile, node) (pile->nodes[node].ptr = 0) #ifdef __cplusplus } // extern "C" #endif #endif doomsday-stable-1.15.7/doomsday/client/include/de_audio.h0000664000175000017500000000240512641367670022637 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * de_audio.h: Audio Subsystem */ #ifndef __DOOMSDAY_AUDIO__ #define __DOOMSDAY_AUDIO__ #ifdef __CLIENT__ # include "audio/audiodriver.h" # include "audio/audiodriver_music.h" # include "audio/s_sfx.h" # include "audio/s_mus.h" #endif #include "audio/s_main.h" #include "audio/s_cache.h" #include "audio/s_environ.h" #include #include #endif doomsday-stable-1.15.7/doomsday/client/include/client/0000775000175000017500000000000012641367670022172 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/client/clpolymover.h0000664000175000017500000000266512641367670024727 0ustar jaakkojaakko/** @file clpolymover.h Clientside polyobj mover (thinker). * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_POLYMOVER_H #define DENG_CLIENT_POLYMOVER_H #include "api_thinker.h" #include "Polyobj" #include /** * Polyobj movement thinker. * * @ingroup world */ class ClPolyMover : public ThinkerData { Polyobj *_polyobj; bool _move; bool _rotate; public: ClPolyMover(Polyobj &pobj, bool moving, bool rotating); ~ClPolyMover(); void think(); static thinker_s *newThinker(Polyobj &polyobj, bool moving, bool rotating); }; #endif // DENG_CLIENT_POLYMOVER_H doomsday-stable-1.15.7/doomsday/client/include/client/cl_mobj.h0000664000175000017500000000546412641367670023761 0ustar jaakkojaakko/** @file cl_mobj.h Clientside map objects. * @ingroup client * * @author Copyright © 2003-2013 Jaakko Keränen * @author Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_WORLD_MOBJ_H #define DENG_CLIENT_WORLD_MOBJ_H #include "world/p_object.h" #include "world/clientmobjthinkerdata.h" /// Asserts that a given mobj is a client mobj. #define CL_ASSERT_CLMOBJ(mo) DENG_ASSERT(Cl_IsClientMobj(mo)); /** * Make the real player mobj identical with the client mobj. * The client mobj is always unlinked. Only the *real* mobj is visible. * (The real mobj was created by the Game.) */ void Cl_UpdateRealPlayerMobj(mobj_t *localMobj, mobj_t *remoteClientMobj, int flags, dd_bool onFloor); ClientMobjThinkerData::RemoteSync *ClMobj_GetInfo(mobj_t *mo); /** * Call for Hidden client mobjs to make then visible. * If a sound is waiting, it's now played. * * @return @c true, if the mobj was revealed. */ dd_bool ClMobj_Reveal(mobj_t *cmo); /** * Links the mobj into sectorlinks and if the object is solid, the * blockmap. Linking to sectorlinks makes the mobj visible and linking * to the blockmap makes it possible to interact with it (collide). * If the client mobj is Hidden, it will not be linked anywhere. */ void ClMobj_Link(mobj_t *cmo); // needed? /** * Change the state of a mobj. * * @todo Should use the gameside function for this? */ void ClMobj_SetState(mobj_t *mo, int stnum); // needed? /** * Reads a single mobj delta (inside PSV_FRAME2 packet) from the message buffer * and applies it to the client mobj in question. * * For client mobjs that belong to players, updates the real player mobj * accordingly. */ void ClMobj_ReadDelta(); /** * Null mobjs deltas have their own type in a PSV_FRAME2 packet. * Here we remove the mobj in question. */ void ClMobj_ReadNullDelta(); /** * Determines whether a mobj is a client mobj. * * @param mo Mobj to check. * * @return @c true, if the mobj is a client mobj; otherwise @c false. */ dd_bool Cl_IsClientMobj(mobj_t const *mo); #endif // DENG_CLIENT_WORLD_MOBJ_H doomsday-stable-1.15.7/doomsday/client/include/client/cl_player.h0000664000175000017500000000537512641367670024327 0ustar jaakkojaakko/** @file cl_player.h Clientside player management. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_PLAYER_H #define DENG_CLIENT_PLAYER_H #include "cl_mobj.h" /** * Information about a client player. */ typedef struct clplayerstate_s { thid_t clMobjId; float forwardMove; float sideMove; int angle; angle_t turnDelta; int friction; int pendingFixes; thid_t pendingFixTargetClMobjId; angle_t pendingAngleFix; float pendingLookDirFix; coord_t pendingOriginFix[3]; coord_t pendingMomFix[3]; } clplayerstate_t; DENG_EXTERN_C float pspMoveSpeed; DENG_EXTERN_C float cplrThrustMul; /** * Clears the player state table. */ void Cl_InitPlayers(); /** * Used in DEMOS. (Not in regular netgames.) * Applies the given dx and dy to the local player's coordinates. * * @param dx Viewpoint X delta. * @param dy Viewpoint Y delta. * @param z Viewpoint absolute Z coordinate. * @param onground If @c true the mobj's Z will be set to floorz, and the player's * viewheight is set so that the viewpoint height is param 'z'. * If @c false the mobj's Z will be param 'z' and viewheight is zero. */ void ClPlayer_MoveLocal(coord_t dx, coord_t dy, coord_t z, bool onground); /** * Move the (hidden, unlinked) client player mobj to the same coordinates * where the real mobj of the player is. */ void ClPlayer_UpdateOrigin(int plrnum); void ClPlayer_HandleFix(); void ClPlayer_ApplyPendingFixes(int plrNum); /** * Reads a single PSV_FRAME2 player delta from the message buffer and * applies it to the player in question. */ void ClPlayer_ReadDelta(); clplayerstate_t *ClPlayer_State(int plrNum); /** * Returns the gameside local mobj of a player. */ mobj_t *ClPlayer_LocalGameMobj(int plrNum); /** * Returns @c true iff the player is free to move according to floorz and ceilingz. */ bool ClPlayer_IsFreeToMove(int plrnum); #endif // DENG_CLIENT_PLAYER_H doomsday-stable-1.15.7/doomsday/client/include/client/cl_world.h0000664000175000017500000000356612641367670024162 0ustar jaakkojaakko/** @file cl_world.h Clientside world management. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_WORLD_MAP_H #define DENG_CLIENT_WORLD_MAP_H void Cl_InitTransTables(); void Cl_ResetTransTables(); /** * Handles the PSV_MATERIAL_ARCHIVE packet sent by the server. The list of * materials is stored until the client disconnects. */ void Cl_ReadServerMaterials(); /** * Handles the PSV_MOBJ_TYPE_ID_LIST packet sent by the server. */ void Cl_ReadServerMobjTypeIDs(); /** * Handles the PSV_MOBJ_STATE_ID_LIST packet sent by the server. */ void Cl_ReadServerMobjStateIDs(); int Cl_LocalMobjType(int serverMobjType); int Cl_LocalMobjState(int serverMobjState); /** * Reads a sector delta from the PSV_FRAME2 message buffer and applies it to the world. */ void Cl_ReadSectorDelta(int deltaType); /** * Reads a side delta from the message buffer and applies it to the world. */ void Cl_ReadSideDelta(int deltaType); /** * Reads a poly delta from the message buffer and applies it to the world. */ void Cl_ReadPolyDelta(); #endif // DENG_CLIENT_WORLD_MAP_H doomsday-stable-1.15.7/doomsday/client/include/client/cl_def.h0000664000175000017500000000313212641367670023556 0ustar jaakkojaakko/** @file cl_def.h Client definitions. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_H #define DENG_CLIENT_H #include "world/p_object.h" #define SHORTP(x) (*(short*) (x)) #define USHORTP(x) (*(unsigned short*) (x)) DENG_EXTERN_C ident_t clientID; DENG_EXTERN_C int serverTime; DENG_EXTERN_C bool handshakeReceived; DENG_EXTERN_C int gameReady; DENG_EXTERN_C bool netLoggedIn; DENG_EXTERN_C int clientPaused; void Cl_InitID(); void Cl_CleanUp(); /** * Client's packet handler. Handles all the events the server sends. */ void Cl_GetPackets(); /** * Client-side game ticker. */ void Cl_Ticker(timespan_t ticLength); int Cl_GameReady(); /** * Sends a hello packet. * PCL_HELLO2 includes the Game ID (16 chars). */ void Cl_SendHello(); #endif // DENG_CLIENT_H doomsday-stable-1.15.7/doomsday/client/include/client/cl_frame.h0000664000175000017500000000221612641367670024114 0ustar jaakkojaakko/** @file cl_frame.h Client frame reception. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_FRAME_H #define DENG_CLIENT_FRAME_H void Cl_InitFrame(); void Cl_ResetFrame(); /** * Read a PSV_FRAME2/PSV_FIRST_FRAME2 packet. */ void Cl_Frame2Received(int packetType); float Cl_FrameGameTime(); #endif // DENG_CLIENT_FRAME_H doomsday-stable-1.15.7/doomsday/client/include/client/clplanemover.h0000664000175000017500000000336512641367670025041 0ustar jaakkojaakko/** @file clplanemover.h Clientside plane mover (thinker). * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_PLANEMOVER_H #define DENG_CLIENT_PLANEMOVER_H #include "api_thinker.h" #include "Plane" #include /** * Plane movement thinker. Makes changes to planes using DMU. * * @ingroup world */ class ClPlaneMover : public ThinkerData { Plane *_plane; coord_t _destination; float _speed; public: ClPlaneMover(Plane &plane, coord_t dest, float speed); ~ClPlaneMover(); void think(); /** * Constructs a new plane mover and adds its thinker to the map. * * @param plane Plane to move. * @param dest Destination height. * @param speed Speed of move. * * @return The mover thinker. Ownership retained by the Plane's Map. */ static thinker_s *newThinker(Plane &plane, coord_t dest, float speed); }; #endif // DENG_CLIENT_PLANEMOVER_H doomsday-stable-1.15.7/doomsday/client/include/client/cl_sound.h0000664000175000017500000000234512641367670024155 0ustar jaakkojaakko/** @file cl_sound.h Clientside sounds. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_SOUND_H #define DENG_CLIENT_SOUND_H #include "network/protocol.h" /** * Read a sound delta from the message buffer and play it. * Only used with PSV_FRAME2 packets. */ void Cl_ReadSoundDelta(deltatype_t type); /** * Called when a PSV_FRAME sound packet is received. */ void Cl_Sound(); #endif // DENG_CLIENT_SOUND_H doomsday-stable-1.15.7/doomsday/client/include/client/cl_infine.h0000664000175000017500000000222212641367670024267 0ustar jaakkojaakko/** @file cl_infine.h Clientside InFine. * * @authors Copyright © 2010 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_CLIENT_INFINE #define DENG_CLIENT_INFINE #include "api_infine.h" #include finaleid_t Cl_CurrentFinale(); /** * This is where clients start their InFine sequences. */ void Cl_Finale(Reader *msg); /** * Client sends a request to skip the finale. */ void Cl_RequestFinaleSkip(); #endif // DENG_CLIENT_INFINE doomsday-stable-1.15.7/doomsday/client/include/Vertex0000664000175000017500000000003212641367670022107 0ustar jaakkojaakko#include "world/vertex.h" doomsday-stable-1.15.7/doomsday/client/include/world/0000775000175000017500000000000012641367670022043 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/world/generator.h0000664000175000017500000002077012641367670024210 0ustar jaakkojaakko/** @file generator.h World map (particle) generator. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_CLIENT_WORLD_GENERATOR_H #define DENG_CLIENT_WORLD_GENERATOR_H #include "map.h" #include class BspLeaf; class Line; class Plane; struct mobj_s; /** * POD structure used when querying the current state of a particle. * * @ingroup world */ struct ParticleInfo { int stage; ///< -1 => particle doesn't exist short tics; fixed_t origin[3]; ///< Coordinates. fixed_t mov[3]; ///< Momentum. BspLeaf *bspLeaf; ///< Updated when needed. Line *contact; ///< Updated when lines hit/avoided. ushort yaw, pitch; ///< Rotation angles (0-65536 => 0-360). }; /** * Particle generator. * * @ingroup world */ struct Generator { /** * Particle animation is defined as a sequence of (perhaps interpolated) * property value stages. */ struct ParticleStage { enum Flag { StageTouch = 0x1, ///< Touching ends current stage. DieTouch = 0x2, ///< Dies from first touch. Bright = 0x4, ///< Fullbright. Shading = 0x8, ///< Pseudo-3D. PlaneFlat = 0x10, ///< Touches a plane => render as flat. StageWallTouch = 0x20, ///< Touch a wall => end stage. StageFlatTouch = 0x40, ///< Touch a flat => end stage. WallFlat = 0x80, ///< Touches a wall => render as flat. SphereForce = 0x100, ZeroYaw = 0x200, ///< Set particle yaw to zero. ZeroPitch = 0x400, ///< Set particle pitch to zero. RandomYaw = 0x800, RandomPitch = 0x1000 }; Q_DECLARE_FLAGS(Flags, Flag) short type; Flags flags; fixed_t resistance; fixed_t bounce; fixed_t radius; fixed_t gravity; }; enum Flag { Static = 0x1, ///< Can't be replaced by anything. RelativeVelocity = 0x2, ///< Particles inherit source's velocity. SpawnOnly = 0x4, ///< Generator is spawned only when source is being spawned. RelativeVector = 0x8, ///< Rotate spawn vector w/mobj angle. BlendAdditive = 0x10, ///< Render using additive blending. SpawnFloor = 0x20, ///< Flat-trig: spawn on floor. SpawnCeiling = 0x40, ///< Flat-trig: spawn on ceiling. SpawnSpace = 0x80, ///< Flat-trig: spawn in air. Density = 0x100, ///< Definition specifies a density. ModelOnly = 0x200, ///< Only spawn if source is a 3D model. ScaledRate = 0x400, ///< Spawn rate affected by a factor. Group = 0x800, ///< Triggered by all in anim group. BlendSubtract = 0x1000, ///< Subtractive blending. BlendReverseSubtract = 0x2000, ///< Reverse subtractive blending. BlendMultiply = 0x4000, ///< Multiplicative blending. BlendInverseMultiply = 0x8000, ///< Inverse multiplicative blending. StateChain = 0x10000 ///< Chain after existing state gen(s). }; Q_DECLARE_FLAGS(Flags, Flag) /// Unique identifier associated with each generator (1-based). typedef short Id; public: /// @todo make private: thinker_t thinker; ///< Func = P_PtcGenThinker Plane *plane; ///< Flat-triggered. ded_ptcgen_t const *def; ///< The definition of this generator. struct mobj_s *source; ///< If mobj-triggered. int srcid; ///< Source mobj ID. int type; ///< Type-triggered; mobj type number (-1=none). int type2; ///< Type-triggered; alternate type. fixed_t originAtSpawn[3]; ///< Used by untriggered/damage gens. fixed_t vector[3]; ///< Converted from the definition. float spawnRateMultiplier; int count; ///< Number of particles generated thus far. ParticleStage *stages; public: /** * Returns the map in which the generator exists. * * @see Thinker_Map() */ de::Map &map() const; /** * Returns the unique identifier of the generator. The identifier is 1-based. */ Id id() const; /** * Change the unique identifier of the generator. The identifier is 1-based. * * @param newId New identifier to apply. */ void setId(Id newId); /** * Set gen->count prior to calling this function. */ void configureFromDef(ded_ptcgen_t const *def); /** * Generate and/or move the particles. */ void runTick(); /** * Run the generator's thinker for the given number of @a tics. */ void presimulate(int tics); /** * Returns the age of the generator (time since spawn), in tics. */ int age() const; /** * Determine the @em approximate origin of the generator in map space. * * @note In the case of generator attached to a mobj this is the @em current, * unsmoothed origin of the mobj offset by the @em initial origin at generator * spawn time. For all other types of generator the initial origin at generator * spawn time is returned. */ de::Vector3d origin() const; /** * Returns @c true iff the generator is @em static, meaning it will not be * replaced under any circumstances. */ bool isStatic() const; /** * Returns @c true iff the generator is @em untriggered. */ bool isUntriggered() const; /** * Change the @em untriggered state of the generator. */ void setUntriggered(bool yes = true); /** * Returns the currently configured blending mode for the generator. */ blendmode_t blendmode() const; /** * Returns the total number of @em active particles for the generator. */ int activeParticleCount() const; /** * Provides readonly access to the generator particle info data. */ ParticleInfo const *particleInfo() const; public: /// @todo make private: /** * Clears all memory used for manipulating the generated particles. */ void clearParticles(); /** * Attempt to spawn a new particle. * * @return Index of the newly spawned particle; otherwise @c -1. */ int newParticle(); /** * The movement is done in two steps: * Z movement is done first. Skyflat kills the particle. * XY movement checks for hits with solid walls (no backsector). * This is supposed to be fast and simple (but not too simple). */ void moveParticle(int index); void spinParticle(ParticleInfo &pt); float particleZ(ParticleInfo const &pt) const; de::Vector3f particleOrigin(ParticleInfo const &pt) const; de::Vector3f particleMomentum(ParticleInfo const &pt) const; public: /** * Register the console commands, variables, etc..., of this module. */ static void consoleRegister(); private: Id _id; ///< Unique in the map. Flags _flags; int _age; ///< Time since spawn, in tics. float _spawnCount; bool _untriggered; ///< @c true= consider this as not yet triggered. int _spawnCP; ///< Particle spawn cursor. ParticleInfo *_pinfo; ///< Info about each generated particle. }; Q_DECLARE_OPERATORS_FOR_FLAGS(Generator::Flags) Q_DECLARE_OPERATORS_FOR_FLAGS(Generator::ParticleStage::Flags) typedef Generator::ParticleStage GeneratorParticleStage; void Generator_Delete(Generator *gen); void Generator_Thinker(Generator *gen); #endif // DENG_CLIENT_WORLD_GENERATOR_H doomsday-stable-1.15.7/doomsday/client/include/world/map.h0000664000175000017500000007233512641367670023003 0ustar jaakkojaakko/** @file map.h World map. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_MAP_H #define DENG_WORLD_MAP_H #include #include #include #include #include #include #include #include #include "Mesh" #include "BspNode" #include "Line" #include "Polyobj" #ifdef __CLIENT__ # include "world/p_object.h" # include "client/clplanemover.h" # include "client/clpolymover.h" # include "world/worldsystem.h" # include "Generator" # include "BiasSource" # include "Lumobj" #endif class MapDef; class BspLeaf; class ConvexSubspace; class LineBlockmap; class Plane; class Sector; class SectorCluster; class Sky; class Surface; class Vertex; #ifdef __CLIENT__ class BiasTracker; #endif namespace de { class Blockmap; class EntityDatabase; #ifdef __CLIENT__ class LightGrid; #endif class Thinkers; /** * World map. * * @ingroup world */ class Map #ifdef __CLIENT__ : DENG2_OBSERVES(WorldSystem, FrameBegin) #endif { DENG2_NO_COPY (Map) DENG2_NO_ASSIGN(Map) public: /// Base error for runtime map editing errors. @ingroup errors DENG2_ERROR(EditError); /// Required map element is missing. @ingroup errors DENG2_ERROR(MissingElementError); /// Required map object is missing. @ingroup errors DENG2_ERROR(MissingObjectError); /// Required blockmap is missing. @ingroup errors DENG2_ERROR(MissingBlockmapError); /// Required BSP data is missing. @ingroup errors DENG2_ERROR(MissingBspTreeError); /// Required thinker lists are missing. @ingroup errors DENG2_ERROR(MissingThinkersError); #ifdef __CLIENT__ /// Required light grid is missing. @ingroup errors DENG2_ERROR(MissingLightGridError); /// Attempted to add a new element/object when full. @ingroup errors DENG2_ERROR(FullError); #endif /// Notified when the map is about to be deleted. DENG2_DEFINE_AUDIENCE(Deletion, void mapBeingDeleted(Map const &map)) /// Notified when a one-way window construct is first found. DENG2_DEFINE_AUDIENCE(OneWayWindowFound, void oneWayWindowFound(Line &line, Sector &backFacingSector)) /// Notified when an unclosed sector is first found. DENG2_DEFINE_AUDIENCE(UnclosedSectorFound, void unclosedSectorFound(Sector §or, Vector2d const &nearPoint)) /* * Constants: */ #ifdef __CLIENT__ static dint const MAX_BIAS_SOURCES = 8 * 32; // Hard limit due to change tracking. /// Maximum number of generators per map. static dint const MAX_GENERATORS = 512; #endif typedef de::BinaryTree BspTree; #ifdef __CLIENT__ typedef QSet PlaneSet; typedef QSet SurfaceSet; typedef QHash ClMobjHash; #endif public: /// @todo make private: coord_t _globalGravity = 0; ///< The defined gravity for this map. coord_t _effectiveGravity = 0; ///< The effective gravity for this map. dint _ambientLightLevel = 0; ///< Ambient lightlevel for the current map. public: /** * Construct a new map initially configured in an editable state. Whilst * editable new map elements can be added, thereby allowing the map to be * constructed dynamically. When done editing @ref endEditing() should be * called to switch the map into a non-editable (i.e., playable) state. * * @param mapDefinition Definition for the map (Can be set later, @ref setDef). */ explicit Map(MapDef *mapDefinition = nullptr); /** * Returns the definition for the map. */ MapDef *def() const; /** * Change the definition associated with the map to @a newMapDefinition. */ void setDef(MapDef *newMapDefinition); /** * Returns the effective map-info definition Record for the map. * * @see WorldSystem::mapInfoForMapUri() */ de::Record const &mapInfo() const; /** * Returns the points which describe the boundary of the map coordinate * space, which, are defined by the minimal and maximal vertex coordinates * of the non-editable, non-polyobj line geometries). */ AABoxd const &bounds() const; inline Vector2d origin() const { return Vector2d(bounds().min); } inline Vector2d dimensions() const { return Vector2d(bounds().max) - Vector2d(bounds().min); } /** * Returns the minimum ambient light level for the whole map. */ dint ambientLightLevel() const; /** * Returns the currently effective gravity multiplier for the map. */ coord_t gravity() const; /** * Change the effective gravity multiplier for the map. * * @param newGravity New gravity multiplier. */ void setGravity(coord_t newGravity); /** * To be called following an engine reset to update the map state. */ void update(); #ifdef __CLIENT__ public: // Light sources --------------------------------------------------------- /** * Returns the total number of BiasSources in the map. */ dint biasSourceCount() const; /** * Attempt to add a new bias light source to the map (a copy is made). * * @note At most @ref MAX_BIAS_SOURCES are supported for technical reasons. * * @return Reference to the newly added bias source. * * @see biasSourceCount() * @throws FullError Once capacity is reached. */ BiasSource &addBiasSource(BiasSource const &biasSource = BiasSource()); /** * Removes the specified bias light source from the map. * * @see removeAllBiasSources() */ void removeBiasSource(dint which); /** * Remove all bias sources from the map. * * @see removeBiasSource() */ void removeAllBiasSources(); /** * Lookup a BiasSource by it's unique @a index. */ BiasSource &biasSource(dint index) const; BiasSource *biasSourcePtr(dint index) const; /** * Finds the bias source nearest to the specified map space @a point. * * @note This result is not cached. May return @c 0 if no bias sources exist. */ BiasSource *biasSourceNear(Vector3d const &point) const; /** * Iterate through the BiasSources of the map. * * @param func Callback to make for each BiasSource. */ LoopResult forAllBiasSources(std::function func) const; /** * Lookup the unique index for the given bias @a source. */ dint indexOf(BiasSource const &source) const; /** * Returns the time in milliseconds when the current render frame began. Used * for interpolation purposes. */ duint biasCurrentTime() const; /** * Returns the frameCount of the current render frame. Used for tracking changes * to bias sources/surfaces. */ duint biasLastChangeOnFrame() const; // Luminous-objects ----------------------------------------------------------- /** * Returns the total number of lumobjs in the map. */ dint lumobjCount() const; /** * Add a new lumobj to the map (a copy is made). * * @return Reference to the newly added lumobj. */ Lumobj &addLumobj(Lumobj const &lumobj = Lumobj()); /** * Removes the specified lumobj from the map. * * @see removeAllLumobjs() */ void removeLumobj(dint which); /** * Remove all lumobjs from the map. * * @see removeLumobj() */ void removeAllLumobjs(); /** * Lookup a Lumobj in the map by it's unique @a index. */ Lumobj &lumobj(dint index) const; Lumobj *lumobjPtr(dint index) const; /** * Iterate through the Lumpobjs of the map. * * @param func Callback to make for each Lumobj. */ LoopResult forAllLumobjs(std::function func) const; #endif // __CLIENT__ public: // Lines & Line-Sides ---------------------------------------------------- /** * Returns the total number of Lines in the map. */ dint lineCount() const; /** * Lookup a Line in the map by it's unique @a index. */ Line &line(dint index) const; Line *linePtr(dint index) const; /** * Iterate through the Lines of the map. * * @param func Callback to make for each Line. */ LoopResult forAllLines(std::function func) const; /** * Lines and Polyobj lines (note polyobj lines are iterated first). * * @note validCount should be incremented before calling this to begin a new * logical traversal. Otherwise Lines marked with a validCount equal to this will * be skipped over (can be used to avoid processing a line multiple times during * complex / non-linear traversals. * * @param box Axis-aligned bounding box in which Lines must be Blockmap-linked. * @param flags @ref lineIteratorFlags * @param func Callback to make for each Line. */ LoopResult forAllLinesInBox(AABoxd const &box, dint flags, std::function func) const; /** * @overload */ inline LoopResult forAllLinesInBox(AABoxd const &box, std::function func) const { return forAllLinesInBox(box, LIF_ALL, func); } /** * The callback function will be called once for each line that crosses the object. * This means all the lines will be two-sided. */ LoopResult forAllLinesTouchingMobj(struct mobj_s &mob, std::function func) const; // --- /** * Returns the total number of Line::Sides in the map. */ inline dint sideCount() const { return lineCount() * 2; } /** * Lookup a LineSide in the map by it's unique @a index. * * @see toSideIndex() */ LineSide &side(dint index) const; LineSide *sidePtr(dint index) const; /** * Helper function which returns the relevant side index given a @a lineIndex * and @a side identifier. * * Indices are produced as follows: * @code * lineIndex / 2 + (backSide? 1 : 0); * @endcode * * @param lineIndex Index of the line in the map. * @param side Side of the line. @c =0 the Line::Front else Line::Back * * @return Unique index for the identified side. */ static dint toSideIndex(dint lineIndex, dint side); public: // Map-objects ----------------------------------------------------------- LoopResult forAllMobjsTouchingLine(Line &line, std::function func) const; /** * Increment validCount before using this. 'func' is called for each mobj * that is (even partly) inside the sector. This is not a 3D test, the * mobjs may actually be above or under the sector. * * (Lovely name; actually this is a combination of SectorMobjs and * a bunch of LineMobjs iterations.) */ LoopResult forAllMobjsTouchingSector(Sector §or, std::function func) const; /** * Links a mobj into both a block and a BSP leaf based on it's (x,y). * Sets mobj->bspLeaf properly. Calling with flags==0 only updates * the BspLeaf pointer. Can be called without unlinking first. * Should be called AFTER mobj translation to (re-)insert the mobj. */ void link(struct mobj_s &mobj, dint flags); /** * Unlinks a mobj from everything it has been linked to. Should be called * BEFORE mobj translation to extract the mobj. * * @param mobj Mobj to be unlinked. * * @return DDLINK_* flags denoting what the mobj was unlinked from * (in case we need to re-link). */ dint unlink(struct mobj_s &mobj); #ifdef __CLIENT__ public: // Particle generators ------------------------------------------------------- /** * Returns the total number of @em active generators in the map. */ dint generatorCount() const; /** * Attempt to spawn a new (particle) generator for the map. If no free identifier * is available then @c nullptr is returned. */ Generator *newGenerator(); /** * Iterate over all generators in the map making a callback for each. * * @param func Callback to make for each Generator. */ LoopResult forAllGenerators(std::function func) const; /** * Iterate over all generators in the map which are present in the identified * sector making a callback for each. * * @param sector Sector containing the generators to process. * @param func Callback to make for each Generator. */ LoopResult forAllGeneratorsInSector(Sector const §or, std::function func) const; void unlink(Generator &generator); #endif // __CLIENT__ public: // Poly objects ---------------------------------------------------------- /** * Returns the total number of Polyobjs in the map. */ dint polyobjCount() const; /** * Lookup a Polyobj in the map by it's unique @a index. */ Polyobj &polyobj(dint index) const; Polyobj *polyobjPtr(dint index) const; /** * Iterate through the Polyobjs of the map. * * @param func Callback to make for each Polyobj. */ LoopResult forAllPolyobjs(std::function func) const; /** * Link the specified @a polyobj in any internal data structures for * bookkeeping purposes. Should be called AFTER Polyobj rotation and/or * translation to (re-)insert the polyobj. * * @param polyobj Poly-object to be linked. */ void link(Polyobj &polyobj); /** * Unlink the specified @a polyobj from any internal data structures for * bookkeeping purposes. Should be called BEFORE Polyobj rotation and/or * translation to extract the polyobj. * * @param polyobj Poly-object to be unlinked. */ void unlink(Polyobj &polyobj); public: // Sectors --------------------------------------------------------------- /** * Returns the total number of Sectors in the map. */ dint sectorCount() const; /** * Lookup a Sector in the map by it's unique @a index. */ Sector §or(dint index) const; Sector *sectorPtr(dint index) const; /** * Iterate through the Sectors of the map. * * @param func Callback to make for each Sector. */ LoopResult forAllSectors(std::function func) const; /** * Increment validCount before calling this routine. The callback function * will be called once for each sector the mobj is touching (totally or * partly inside). This is not a 3D check; the mobj may actually reside * above or under the sector. */ LoopResult forAllSectorsTouchingMobj(struct mobj_s &mob, std::function func) const; public: // Sector clusters ------------------------------------------------------- /** * Returns the total number of SectorClusters in the map. */ dint clusterCount() const; /** * Determine the SectorCluster which contains @a point and which is on the * back side of the BS partition that lies in front of @a point. * * @param point Map space coordinates to determine the BSP leaf for. * * @return SectorCluster containing the specified point if any or @c nullptr * if the clusters have not yet been built. */ SectorCluster *clusterAt(Vector2d const &point) const; /** * Iterate through the SectorClusters of the map. * * @param sector If not @c nullptr, traverse the clusters of this Sector only. * @param func Callback to make for each SectorCluster. */ LoopResult forAllClusters(Sector *sector, std::function func); /** * @overload */ inline LoopResult forAllClusters(std::function func) { return forAllClusters(nullptr, func); } public: // Skies ----------------------------------------------------------------- /** * Returns the logical sky for the map. */ Sky &sky() const; #ifdef __CLIENT__ coord_t skyFix(bool ceiling) const; inline coord_t skyFixFloor() const { return skyFix(false /*the floor*/); } inline coord_t skyFixCeiling() const { return skyFix(true /*the ceiling*/); } void setSkyFix(bool ceiling, coord_t newHeight); inline void setSkyFixFloor(coord_t newHeight) { setSkyFix(false /*the floor*/, newHeight); } inline void setSkyFixCeiling(coord_t newHeight) { setSkyFix(true /*the ceiling*/, newHeight); } #endif public: // Subspaces ------------------------------------------------------------- /** * Returns the total number of subspaces in the map. */ dint subspaceCount() const; /** * Lookup a Subspace in the map by it's unique @a index. */ ConvexSubspace &subspace(dint index) const; ConvexSubspace *subspacePtr(dint index) const; /** * Iterate through the ConvexSubspaces of the map. * * @param func Callback to make for each ConvexSubspace. */ LoopResult forAllSubspaces(std::function func) const; public: // Vertexs --------------------------------------------------------------- /** * Returns the total number of Vertexs in the map. */ dint vertexCount() const; /** * Lookup a Vertex in the map by it's unique @a index. */ Vertex &vertex(dint index) const; Vertex *vertexPtr(dint index) const; /** * Iterate through the Vertexs of the map. * * @param func Callback to make for each Vertex. */ LoopResult forAllVertexs(std::function func) const; public: // Data structures ------------------------------------------------------- /** * Provides access to the entity database. */ EntityDatabase &entityDatabase() const; /** * Provides access to the primary @ref Mesh geometry owned by the map. Note that * further meshes may be assigned to individual elements of the map should their * geometries not be representable as a manifold with the primary mesh (e.g., * polyobjs and BSP leaf "extra" meshes). */ Mesh const &mesh() const; /** * Provides access to the line blockmap. */ LineBlockmap const &lineBlockmap() const; /** * Provides access to the mobj blockmap. */ Blockmap const &mobjBlockmap() const; /** * Provides access to the polyobj blockmap. */ Blockmap const &polyobjBlockmap() const; /** * Provides access to the convex subspace blockmap. */ Blockmap const &subspaceBlockmap() const; /** * Provides access to the thinker lists for the map. */ Thinkers /*const*/ &thinkers() const; /** * Returns @c true iff a BSP tree is available for the map. */ bool hasBspTree() const; /** * Provides access to map's BSP tree, for efficient traversal. */ BspTree const &bspTree() const; /** * Determine the BSP leaf on the back side of the BS partition that lies * in front of the specified point within the map's coordinate space. * * @note Always returns a valid BspLeaf although the point may not actually * lay within it (however it is on the same side of the space partition)! * * @param point Map space coordinates to determine the BSP leaf for. * * @return BspLeaf instance for that BSP node's leaf. */ BspLeaf &bspLeafAt(Vector2d const &point) const; /** * @copydoc bspLeafAt() * * The test is carried out using fixed-point math for behavior compatible * with vanilla DOOM. Note that this means there is a maximum size for the * point: it cannot exceed the fixed-point 16.16 range (about 65k units). */ BspLeaf &bspLeafAt_FixedPrecision(Vector2d const &point) const; /** * Given an @a emitter origin, attempt to identify the map element * to which it belongs. * * @param emitter The sound emitter to be identified. * @param sector The identified sector if found is written here. * @param poly The identified polyobj if found is written here. * @param plane The identified plane if found is written here. * @param surface The identified line side surface if found is written here. * * @return @c true iff @a emitter is an identifiable map element. */ bool identifySoundEmitter(ddmobj_base_t const &emitter, Sector **sector, Polyobj **poly, Plane **plane, Surface **surface) const; #ifdef __CLIENT__ /** * Returns @c true iff a LightGrid has been initialized for the map. * * @see lightGrid() */ bool hasLightGrid(); /** * Provides access to the light grid for the map. * * @see hasLightGrid() */ LightGrid &lightGrid(); /** * (Re)-initialize the light grid used for smoothed sector lighting. * * If the grid has not yet been initialized block light sources are determined * at this time (SectorClusters must be built for this). * * If the grid has already been initialized calling this will perform a full update. * * @note Initialization may take some time depending on the complexity of the * map (physial dimensions, number of sectors) and should therefore be done * "off-line". */ void initLightGrid(); /** * Link the given @a surface in all material lists and surface sets which * the map maintains to improve performance. Only surfaces attributed to * the map will be linked (alien surfaces are ignored). * * @param surface The surface to be linked. */ void linkInMaterialLists(Surface *surface); /** * Unlink the given @a surface in all material lists and surface sets which * the map maintains to improve performance. * * @note The material currently attributed to the surface does not matter * for unlinking purposes and the surface will be unlinked from all lists * regardless. * * @param surface The surface to be unlinked. */ void unlinkInMaterialLists(Surface *surface); /** * Returns the set of scrolling surfaces for the map. */ SurfaceSet /*const*/ &scrollingSurfaces(); /** * $smoothmatoffset: Roll the surface material offset tracker buffers. */ void updateScrollingSurfaces(); /** * Returns the set of tracked planes for the map. */ PlaneSet /*const*/ &trackedPlanes(); /** * $smoothplane: Roll the height tracker buffers. */ void updateTrackedPlanes(); /** * Perform spreading of all contacts in the specified map space @a region. */ void spreadAllContacts(AABoxd const ®ion); #endif // __CLIENT__ public: /** * Returns a rich formatted, textual summary of the map's elements, suitable * for logging. */ String elementSummaryAsStyledText() const; /** * Returns a rich formatted, textual summary of the map's objects, suitable * for logging. */ String objectSummaryAsStyledText() const; /** * To be called to register the commands and variables of this module. */ static void consoleRegister(); /** * To be called to initialize the dummy element arrays (which are used with * the DMU API), with a fixed number of @em shared dummies. */ static void initDummies(); public: /// @todo Most of the following should be private: /** * Initialize the node piles and link rings. To be called after map load. */ void initNodePiles(); /** * Initialize all polyobjs in the map. To be called after map load. */ void initPolyobjs(); #ifdef __CLIENT__ /** * Fixing the sky means that for adjacent sky sectors the lower sky * ceiling is lifted to match the upper sky. The raising only affects * rendering, it has no bearing on gameplay. */ void initSkyFix(); /** * Rebuild the surface material lists. To be called when a full update is * necessary. */ void buildMaterialLists(); /** * Initializes bias lighting for the map. New light sources are initialized * from the loaded Light definitions. Map surfaces are prepared for tracking * rays. * * Must be called before rendering a frame with bias lighting enabled. */ void initBias(); /** * Initialize the map object => BSP leaf "contact" blockmaps. */ void initContactBlockmaps(); /** * Spawn all generators for the map which should be initialized automatically * during map setup. */ void initGenerators(); /** * Attempt to spawn all flat-triggered particle generators for the map. * To be called after map setup is completed. * * @note Cannot presently be done in @ref initGenerators() as this is called * during initial Map load and before any saved game has been loaded. */ void spawnPlaneParticleGens(); /** * Destroys all clientside clmobjs in the map. To be called when a network * game ends. */ void clearClMobjs(); /** * Deletes hidden, unpredictable or nulled mobjs for which we have not received * updates in a while. */ void expireClMobjs(); /** * Find/create a client mobj with the unique identifier @a id. Client mobjs are * just like normal mobjs, except they have additional network state. * * To check whether a given mobj is a client mobj, use Cl_IsClientMobj(). The network * state can then be accessed with ClMobj_GetInfo(). * * @param id Identifier of the client mobj. Every client mobj has a unique * identifier. * @param canCreate @c true= create a new client mobj if none existing. * * @return Pointer to the gameside mobj. */ struct mobj_s *clMobjFor(thid_t id, bool canCreate = false) const; /** * Iterate all client mobjs, making a callback for each. Iteration ends if a * callback returns a non-zero value. * * @param callback Function to callback for each client mobj. * @param context Data pointer passed to the callback. * * @return @c 0 if all callbacks return @c 0; otherwise the result of the last. */ dint clMobjIterator(dint (*callback) (struct mobj_s *, void *), void *context = nullptr); /** * Provides readonly access to the client mobj hash. */ ClMobjHash const &clMobjHash() const; protected: /// Observes WorldSystem FrameBegin void worldSystemFrameBegins(bool resetNextViewer); #endif // __CLIENT__ public: // Editing --------------------------------------------------------------- /** * Returns @c true iff the map is currently in an editable state. */ bool isEditable() const; /** * Switch the map from editable to non-editable (i.e., playable) state, * incorporating any new map elements, (re)building the BSP, etc... * * @return @c true= mode switch was completed successfully. */ bool endEditing(); /** * @see isEditable() */ Vertex *createVertex(Vector2d const &origin, dint archiveIndex = MapElement::NoIndex); /** * @see isEditable() */ Line *createLine(Vertex &v1, Vertex &v2, dint flags = 0, Sector *frontSector = nullptr, Sector *backSector = nullptr, dint archiveIndex = MapElement::NoIndex); /** * @see isEditable() */ Polyobj *createPolyobj(Vector2d const &origin); /** * @see isEditable() */ Sector *createSector(dfloat lightLevel, Vector3f const &lightColor, dint archiveIndex = MapElement::NoIndex); /** * Provides a list of all the editable lines in the map. */ typedef QList Lines; Lines const &editableLines() const; /** * Provides a list of all the editable polyobjs in the map. */ typedef QList Polyobjs; Polyobjs const &editablePolyobjs() const; /** * Provides a list of all the editable sectors in the map. */ typedef QList Sectors; Sectors const &editableSectors() const; inline dint editableLineCount() const { return editableLines().count(); } inline dint editablePolyobjCount() const { return editablePolyobjs().count(); } inline dint editableSectorCount() const { return editableSectors().count(); } private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_WORLD_MAP_H doomsday-stable-1.15.7/doomsday/client/include/world/linesighttest.h0000664000175000017500000000532512641367670025107 0ustar jaakkojaakko/** @file linesighttest.h World map line of sight testing. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_LINE_SIGHT_TEST_H #define DENG_WORLD_LINE_SIGHT_TEST_H #include #include #include "world/map.h" namespace de { /** * Models the logic, parameters and state of a line (of) sight (LOS) test. * * @todo fixme: The state of a discrete trace is not fully encapsulated here * due to the manipulation of the validCount properties of the various * map data elements. (Which is used to avoid testing the same element * multiple times during a trace.) * * @todo optimize: Make use of the blockmap to take advantage of the inherent * spatial locality in this data structure. * * @ingroup world */ class LineSightTest { public: typedef Map::BspTree BspTree; public: /** * Constructs a new line (of) sight test. * * @param from Trace origin point in the map coordinate space. * @param to Trace target point in the map coordinate space. * @param bottomSlope Lower limit to the Z axis angle/slope range. * @param topSlope Upper limit to the Z axis angle/slope range. * @param flags @ref lineSightFlags dictate trace behavior/logic. */ LineSightTest(Vector3d const &from, Vector3d const &to, dfloat bottomSlope = -1, dfloat topSlope = +1, dint flags = 0); /** * Execute the trace (i.e., cast the ray). * * @param bspRoot Root of BSP to be traced. * * @return @c true iff an uninterrupted path exists between the preconfigured * Start and End points of the trace line. */ bool trace(BspTree const &bspRoot); private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_WORLD_LINE_SIGHT_TEST_H doomsday-stable-1.15.7/doomsday/client/include/world/mapelement.h0000664000175000017500000001437412641367670024354 0ustar jaakkojaakko/** @file mapelement.h Base class for all world map elements. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_WORLD_MAPELEMENT_H #define DENG_WORLD_MAPELEMENT_H #include "dd_share.h" #include "world/dmuargs.h" #include namespace de { class Map; /** * Base class for all elements of a map. Provides runtime type information and * safe dynamic casting to various derived types. * * Maps are composed out of vertices, lines, sectors, etc. * * Abstract handling of map elements is particularly helpful in the public Map * Update (DMU) API, where objects can be referenced either by type and index * or by an opaque pointer. * * @ingroup world */ class MapElement { DENG2_NO_COPY (MapElement) DENG2_NO_ASSIGN(MapElement) public: /// No parent map element is configured. @ingroup errors DENG2_ERROR(MissingParentError); /// Attempted to configure an invalid parent element. @ingroup errors DENG2_ERROR(InvalidParentError); /// No map is attributed. @ingroup errors DENG2_ERROR(MissingMapError); /// The referenced property does not exist. @ingroup errors DENG2_ERROR(UnknownPropertyError); /// The referenced property is not writeable. @ingroup errors DENG2_ERROR(WritePropertyError); /// Special identifier used to mark an invalid index. enum { NoIndex = -1 }; public: /** * Construct a new MapElement * * @param type DMU type identifier. * @param parent Parent map element (if any). */ explicit MapElement(int t = DMU_NONE, MapElement *parent = 0); virtual ~MapElement(); /** * Returns the DMU_* type of the object. */ int type() const; DENG2_AS_IS_METHODS() /** * Returns @c true iff a parent is attributed to the map element. * * @see parent(), setParent() */ bool hasParent() const; /** * Returns the parent of the map element. * * @see hasParent(), setParent() */ MapElement &parent(); /// @copydoc parent() MapElement const &parent() const; /** * Change the parent of the map element. * * @param newParent New parent to attribute to the map element. Ownership * is unaffected. Can be @c 0 (to clear the attribution). * * @see hasParent(), parent() */ void setParent(MapElement *newParent); /** * Returns @c true iff a map is attributed to the map element. Note that * if the map element has a @em parent that this state is delegated to the * parent map element. * * @see map(), setMap(), hasParent() */ bool hasMap() const; /** * Returns the map attributed to the map element. Note that if the map * element has a @em parent that this property comes from the parent map * element (delegation). * * @see hasMap(), setMap(), hasParent() */ Map &map() const; inline Map *mapPtr() const { return hasMap()? &map() : nullptr; } /** * Change the map attributed to the map element. Note that if the map * element has a @em parent that attempting to change the map property of * "this" map element is an error (delegation). * * @param newMap * * @see hasMap(), map() */ void setMap(Map *newMap); /** * Returns the "in-map" index attributed to the map element. */ int indexInMap() const; /** * Change the "in-map" index attributed to the map element. * * @param newIndex New index to attribute to the map element. @c NoIndex * clears the attribution (not a valid index). */ void setIndexInMap(int newIndex = NoIndex); /** * Returns the archive index for the map element. The archive index is the * position of the relevant data or definition in the archived map. For * example, in the case of a DMU_SIDE element that is produced from an id * Tech 1 format map, this should be the index of the definition in the * SIDEDEFS data lump. * * @see setIndexInArchive() */ int indexInArchive() const; /** * Change the "archive index" of the map element to @a newIndex. * * @see indexInArchive() */ void setIndexInArchive(int newIndex = NoIndex); /** * Get a property value, selected by DMU_* name. * * Derived classes can override this to implement read access for additional * DMU properties. MapElement::property() must be called from an overridding * method if the named property is unknown/not handled, returning the result. * If the property is known and the read access is handled the overriding * method should return @c false. * * @param args Property arguments. * @return Always @c 0 (can be used as an iterator). */ virtual int property(DmuArgs &args) const; /** * Update a property value, selected by DMU_* name. * * Derived classes can override this to implement write access for additional * DMU properties. MapElement::setProperty() must be called from an overridding * method if the named property is unknown/not handled, returning the result. * If the property is known and the write access is handled the overriding * method should return @c false. * * @param args Property arguments. * @return Always @c 0 (can be used as an iterator). */ virtual int setProperty(DmuArgs const &args); private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_WORLD_MAPELEMENT_H doomsday-stable-1.15.7/doomsday/client/include/world/contact.h0000664000175000017500000001047612641367670023657 0ustar jaakkojaakko/** @file contact.h Map object => subspace "contact" and contact lists. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifdef __CLIENT__ #ifndef DENG_CLIENT_WORLD_CONTACT_H #define DENG_CLIENT_WORLD_CONTACT_H #include #include #include #include "world/map.h" struct Contact; class ConvexSubspace; class Lumobj; enum ContactType { ContactMobj, ContactLumobj, ContactTypeCount }; /// @todo Obviously, polymorphism is a better solution. struct Contact { Contact *nextUsed; ///< Next in the used list (if any, not owned). Contact *next; ///< Next in global list of contacts (if any, not owned). ContactType _type; ///< Logical identifier. void *_object; ///< The contacted object. ContactType type() const; void *objectPtr() const; template ObjectType &objectAs() const { DENG2_ASSERT(_object); return *static_cast(_object); } /** * Returns a copy of the linked object's origin in map space. */ de::Vector3d objectOrigin() const; /** * Returns the linked object's radius in map space. */ de::ddouble objectRadius() const; /** * Returns an axis-aligned bounding box for the linked object in map space. */ AABoxd objectAABox() const; /** * Returns the BSP leaf at the linked object's origin in map space. */ BspLeaf &objectBspLeafAtOrigin() const; }; struct ContactList { struct Node; // Start reusing list nodes. static void reset(); void link(Contact *contact); Node *begin() const; private: /** * Create a new list node. If there are none available in the list of * used objects a new one will be allocated and linked to the global list. */ static Node *newNode(void *object); Node *_head; }; /** * To be called during game change/on shutdown to destroy all contact lists. * This is necessary because the lists are allocated from the Zone using a * >= PU_MAP purge level and access to them is handled with global pointers. * * @todo Encapsulate allocation of and access to the lists in de::Map */ void R_DestroyContactLists(); /** * Initialize contact lists for the current map. */ void R_InitContactLists(de::Map &map); /** * To be called at the beginning of a render frame to clear all contact lists * ready for the new frame. */ void R_ClearContactLists(de::Map &map); /** * Add a new contact for the specified mobj, for spreading purposes. */ void R_AddContact(struct mobj_s &mobj); /** * Add a new contact for the specified lumobj, for spreading purposes. */ void R_AddContact(Lumobj &lumobj); /** * Returns the contact list for the specified @a subspace and contact @a type. */ ContactList &R_ContactList(ConvexSubspace &subspace, ContactType type); /** * Traverse the list of @em all contacts for the current render frame. */ de::LoopResult R_ForAllContacts(std::function func); /** * Traverse the list of mobj contacts linked directly to the specified @a subspace, * for the current render frame. */ de::LoopResult R_ForAllSubspaceMobContacts(ConvexSubspace &subspace, std::function func); /** * Traverse the list of lumobj contacts linked directly to the specified @a subspace, * for the current render frame. */ de::LoopResult R_ForAllSubspaceLumContacts(ConvexSubspace &subspace, std::function func); #endif // DENG_CLIENT_WORLD_CONTACT_H #endif // __CLIENT__ doomsday-stable-1.15.7/doomsday/client/include/world/plane.h0000664000175000017500000001504012641367670023313 0ustar jaakkojaakko/** @file plane.h World map plane. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_PLANE_H #define DENG_WORLD_PLANE_H #ifdef __CLIENT__ # include #endif #include #include #include "dd_share.h" // SoundEmitter #ifdef __CLIENT__ # include "def_main.h" // ded_ptcgen_t #endif #include "MapElement" class Sector; class Surface; #ifdef __CLIENT__ struct Generator; class ClPlaneMover; #endif /** * World map sector plane. * * @ingroup world */ class Plane : public de::MapElement { DENG2_NO_COPY (Plane) DENG2_NO_ASSIGN(Plane) public: #ifdef __CLIENT__ /// No generator is attached. @ingroup errors DENG2_ERROR(MissingGeneratorError); #endif /// Notified when the plane is about to be deleted. DENG2_DEFINE_AUDIENCE2(Deletion, void planeBeingDeleted(Plane const &plane)) /// Notified whenever a @em sharp height change occurs. DENG2_DEFINE_AUDIENCE2(HeightChange, void planeHeightChanged(Plane &plane)) #ifdef __CLIENT__ /// Notified whenever a @em smoothed height change occurs. DENG2_DEFINE_AUDIENCE2(HeightSmoothedChange, void planeHeightSmoothedChanged(Plane &plane)) #endif /// Maximum speed for a smoothed plane. static int const MAX_SMOOTH_MOVE = 64; public: /** * Construct a new plane. * * @param sector Sector parent which will own the plane. * @param normal Normal of the plane (will be normalized if necessary). * @param height Height of the plane in map space coordinates. */ Plane(Sector §or, de::Vector3f const &normal = de::Vector3f(0, 0, 1), coord_t height = 0); /** * Returns the owning Sector of the plane. */ Sector §or(); Sector const §or() const; /** * Returns the index of the plane within the owning sector. */ int indexInSector() const; /** * Change the index of the plane within the owning sector. * * @param newIndex New index to attribute the plane. */ void setIndexInSector(int newIndex); /** * Returns @c true iff this is the floor plane of the owning sector. */ bool isSectorFloor() const; /** * Returns @c true iff this is the ceiling plane of the owning sector. */ bool isSectorCeiling() const; /** * Returns the Surface of the plane. */ Surface &surface(); Surface const &surface() const; /** * Change the normal of the plane to @a newNormal (which if necessary will * be normalized before being assigned to the plane). * * @post The plane's tangent vectors and logical plane type will have been * updated also. */ void setNormal(de::Vector3f const &newNormal); /** * Returns the sound emitter for the plane. */ SoundEmitter &soundEmitter(); SoundEmitter const &soundEmitter() const; /** * Update the sound emitter origin according to the point defined by the center * of the plane's owning Sector (on the XY plane) and the Z height of the plane. */ void updateSoundEmitterOrigin(); /** * Returns the current @em sharp height of the plane relative to @c 0 on the * map up axis. The HeightChange audience is notified whenever the height * changes. */ coord_t height() const; /** * Returns the target height of the plane in the map coordinate space. * The target height is the destination height following a successful move. * Note that this may be the same as @ref height(), in which case the plane * is not currently moving. The HeightChange audience is notified whenever * the current @em sharp height changes. * * @see speed(), height() */ coord_t targetHeight() const; /** * Returns the rate at which the plane height will be updated (units per tic) * when moving to the target height in the map coordinate space. * * @see targetHeight(), height() */ coord_t speed() const; #ifdef __CLIENT__ /** * Returns the current smoothed height of the plane (interpolated) in the * map coordinate space. * * @see targetHeight(), height() */ coord_t heightSmoothed() const; /** * Returns the delta between current height and the smoothed height of the * plane in the map coordinate space. * * @see heightSmoothed(), targetHeight() */ coord_t heightSmoothedDelta() const; /** * Perform smoothed height interpolation. * * @see heightSmoothed(), targetHeight() */ void lerpSmoothedHeight(); /** * Reset the plane's height tracking buffer (for smoothing). * * @see heightSmoothed(), targetHeight() */ void resetSmoothedHeight(); /** * Roll the plane's height tracking buffer. * * @see targetHeight() */ void updateHeightTracking(); /** * Returns @c true iff a generator is attached to the plane. * * @see generator() */ bool hasGenerator() const; /** * Returns the generator attached to the plane. * * @see hasGenerator() */ Generator &generator() const; /** * Creates a new flat-triggered particle generator based on the given * definition. Note that it may @em not be "this" plane to which the resultant * generator is attached as the definition may override this. */ void spawnParticleGen(ded_ptcgen_t const *def); void addMover(ClPlaneMover &mover); void removeMover(ClPlaneMover &mover); #endif // __CLIENT__ protected: int property(DmuArgs &args) const; int setProperty(DmuArgs const &args); private: DENG2_PRIVATE(d) }; #endif // DENG_WORLD_PLANE_H doomsday-stable-1.15.7/doomsday/client/include/world/sky.h0000664000175000017500000001572412641367670023033 0ustar jaakkojaakko/** @file sky.h Sky behavior logic for the world system. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_SKY_H #define DENG_WORLD_SKY_H #include #include #include #include #include #include #include #include "MapElement" #include "Material" #include "ModelDef" #define DEFAULT_SKY_SPHERE_MATERIAL ( "Textures:SKY1" ) /** * Behavior logic for a sky in the world system. * * @ingroup world */ class Sky : public de::MapElement { public: /// Notified when the sky is about to be deleted. DENG2_DEFINE_AUDIENCE2(Deletion, void skyBeingDeleted(Sky const &sky)) /// Notified whenever the height changes. DENG2_DEFINE_AUDIENCE2(HeightChange, void skyHeightChanged(Sky &sky)) /// Notified whenever the horizon offset changes. DENG2_DEFINE_AUDIENCE2(HorizonOffsetChange, void skyHorizonOffsetChanged(Sky &sky)) /** * Multiple layers can be used for parallax effects. */ class Layer { public: /// Notified whenever the active-state changes. DENG2_DEFINE_AUDIENCE2(ActiveChange, void skyLayerActiveChanged(Layer &layer)) /// Notified whenever the masked-state changes. DENG2_DEFINE_AUDIENCE2(MaskedChange, void skyLayerMaskedChanged(Layer &layer)) /// Notified whenever the layer material changes. DENG2_DEFINE_AUDIENCE2(MaterialChange, void skyLayerMaterialChanged(Layer &layer)) public: /** * Construct a new sky layer. */ Layer(Sky &sky, Material *material = nullptr); /** * Returns the sky of which this is a layer. */ Sky &sky() const; /** * Returns @a true of the layer is currently active. * * @see setActive() */ bool isActive() const; /** * Change the 'active' state of the layer. The ActiveChange audience is * notified whenever the 'active' state changes. * * @see isActive() */ void setActive(bool yes); inline void enable() { setActive(true); } inline void disable() { setActive(false); } /** * Returns @c true if the layer's material will be masked. * * @see setMasked() */ bool isMasked() const; /** * Change the 'masked' state of the layer. The MaskedChange audience is * notified whenever the 'masked' state changes. * * @see isMasked() */ void setMasked(bool yes); /** * Returns the material currently assigned to the layer (if any). */ Material *material() const; /** * Change the material of the layer. The MaterialChange audience is notified * whenever the material changes. */ void setMaterial(Material *newMaterial); /** * Returns the horizontal offset for the layer. */ float offset() const; /** * Change the horizontal offset for the layer. * * @param newOffset New offset to apply. */ void setOffset(float newOffset); /** * Returns the fadeout limit for the layer. */ float fadeOutLimit() const; /** * Change the fadeout limit for the layer. * * @param newLimit New fadeout limit to apply. */ void setFadeoutLimit(float newLimit); private: DENG2_PRIVATE(d) }; typedef QList Layers; public: explicit Sky(defn::Sky const *definition = nullptr); /** * Reconfigure according to the specified @a definition if not @c NULL, * otherwise, reconfigure using the default values. * * @see configureDefault() */ void configure(defn::Sky const *definition = nullptr); /** * Reconfigure the sky, returning all values to their defaults. * * @see configure() */ inline void configureDefault() { configure(); } /** * Returns the definition used to configure the sky, if any (may return @c nullptr). */ de::Record const *def() const; /** * Provides access to the list of sky layers, for efficient traversal. */ Layers const &layers() const; /** * Convenient method of returning a sky layer by unique @a index. */ inline Layer *layer(int index) const { return layers().at(index); } /** * Returns the total number of sky layers (both active and inactive). */ inline int layerCount() const { return layers().count(); } /** * Returns the height of the sky as a scale factor [0..1] (@c 1 covers the view). * * @see setHeight() */ float height() const; /** * Change the height scale factor for the sky. * * @param newHeight New height scale factor to apply (will be normalized). * * @see height() */ void setHeight(float newHeight); /** * Returns the horizon offset for the sky. * * @see setHorizonOffset() */ float horizonOffset() const; /** * Change the horizon offset for the sky. * * @param newOffset New horizon offset to apply. * * @see horizonOffset() */ void setHorizonOffset(float newOffset); #ifdef __CLIENT__ /** * Returns the ambient color of the sky. The ambient color is automatically * calculated by averaging the color information in the configured layer * material textures. Alternatively, this color can be overridden manually * by calling @ref setAmbientColor(). */ de::Vector3f const &ambientColor() const; /** * Override the automatically calculated ambient color. * * @param newColor New ambient color to apply (will be normalized). * * @see ambientColor() */ void setAmbientColor(de::Vector3f const &newColor); #endif // __CLIENT__ protected: int property(DmuArgs &args) const; int setProperty(DmuArgs const &args); private: DENG2_PRIVATE(d) }; typedef Sky::Layer SkyLayer; #endif // DENG_WORLD_SKY_H doomsday-stable-1.15.7/doomsday/client/include/world/entitydef.h0000664000175000017500000000746412641367670024222 0ustar jaakkojaakko/** @file entitydef.h World map entity definitions. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_ENTITYDEF_H #define DENG_WORLD_ENTITYDEF_H #include "dd_share.h" #include "api_mapedit.h" #include "m_nodepile.h" #include #include struct mapentitydef_s; typedef struct mapentitypropertydef_s { /// Entity-unique identifier associated with this property. int id; /// Entity-unique name for this property. char *name; /// Value type identifier for this property. valuetype_t type; /// Entity definition which owns this property. struct mapentitydef_s *entity; } MapEntityPropertyDef; /** * @ingroup world */ typedef struct mapentitydef_s { /// Unique identifier associated with this entity. int id; /// Set of known properties for this entity. uint numProps; MapEntityPropertyDef *props; #ifdef __cplusplus mapentitydef_s(int _id) : id(_id), numProps(0), props(0) {} #endif } MapEntityDef; #ifdef __cplusplus extern "C" { #endif /** * Lookup a defined property by identifier. * * @param def MapEntityDef instance. * @param propertyId Entity-unique identifier for the property to lookup. * @param retDef If not @c NULL, the found property definition is * written here (else @c 0 if not found). * * @return Logical index of the found property (zero-based) else @c -1 if not found. */ int MapEntityDef_Property2(MapEntityDef *def, int propertyId, MapEntityPropertyDef **retDef = 0); /** * Lookup a defined property by name. * * @param def MapEntityDef instance. * @param propertyName Entity-unique name for the property to lookup. * @param retDef If not @c NULL, the found property definition is * written here (else @c 0 if not found). * * @return Logical index of the found property (zero-based) else @c -1 if not found. */ int MapEntityDef_PropertyByName(MapEntityDef *def, char const *propertyName, MapEntityPropertyDef **retDef = 0); /** * Lookup a MapEntityDef by unique identfier @a id. * * @note Performance is O(log n). * * @return Found MapEntityDef else @c NULL. */ MapEntityDef *P_MapEntityDef(int id); /** * Lookup a MapEntityDef by unique name. * * @note Performance is O(log n). * * @return Found MapEntityDef else @c NULL. */ MapEntityDef *P_MapEntityDefByName(char const *name); /** * Lookup the unique name associated with MapEntityDef @a def. * * @note Performance is O(n). * * @return Unique name associated with @a def if found, else a zero-length string. */ AutoStr *P_NameForMapEntityDef(MapEntityDef *def); /** * To be called to initialize the game map object defs. */ void P_InitMapEntityDefs(); /** * To be called to free all memory allocated for the map obj defs. */ void P_ShutdownMapEntityDefs(); #ifdef __cplusplus } // extern "C" #endif #endif // DENG_WORLD_ENTITYDEF_H doomsday-stable-1.15.7/doomsday/client/include/world/huecircle.h0000664000175000017500000000422512641367670024162 0ustar jaakkojaakko/** @file huecircle.h HueCircle manipulator, for runtime map editing. * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_HUECIRCLE_H #define DENG_WORLD_HUECIRCLE_H #include /** * Color manipulator for runtime map editing. * * @ingroup world */ class HueCircle { public: HueCircle(); /** * Determine the absolute origin of the hue circle in the map coordinate space * (which is relative to the origin of the viewer). */ de::Vector3d origin(de::Vector3d const &viewOrigin, double distance = 100) const; /** * Change the orientation of the hue circle. * * @param frontVec New front vector. * @param sideVec New side vector. * @param upVec New up vector. */ void setOrientation(de::Vector3f const &frontVec, de::Vector3f const &sideVec, de::Vector3f const &upVec); /** * Determine a color by comparing the relative direction of the specified * front vector (the viewer) relative to the orientation of the hue circle. */ de::Vector3f colorAt(de::Vector3f const &viewFrontVec, float *angle = 0, float *sat = 0) const; de::Vector3f offset(double angle) const; private: DENG2_PRIVATE(d) }; #endif // DENG_WORLD_HUECIRCLE_H doomsday-stable-1.15.7/doomsday/client/include/world/p_ticker.h0000664000175000017500000000211212641367670024010 0ustar jaakkojaakko/** @file p_ticker.h Timed world events. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_WORLD_P_TICKER_H #define DENG_WORLD_P_TICKER_H #include /** * Doomsday's own play-ticker. */ void P_Ticker(timespan_t time); #endif // DENG_WORLD_P_TICKER_H doomsday-stable-1.15.7/doomsday/client/include/world/polyobjdata.h0000664000175000017500000000406512641367670024531 0ustar jaakkojaakko/** @file polyobjdata.h Private data for a polyobj * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_WORLD_POLYOBJDATA_H #define DENG_WORLD_POLYOBJDATA_H #include "Polyobj" #include "world/map.h" #include #include #include #ifdef __CLIENT__ class ClPolyMover; #endif /** * Private data for a polyobj. * * Stored in the Polyobj's thinker.d (polyobjs are not normal thinkers). */ class PolyobjData : public Thinker::IData { public: /// Used to store the original/previous vertex coordinates. typedef QVector VertexCoords; public: PolyobjData(); ~PolyobjData(); void setThinker(thinker_s *thinker); void think(); IData *duplicate() const; #ifdef __CLIENT__ void addMover(ClPolyMover &mover); void removeMover(ClPolyMover &mover); ClPolyMover *mover() const; #endif public: int indexInMap; de::Mesh *mesh; Polyobj::Lines lines; Polyobj::Vertexes uniqueVertexes; VertexCoords originalPts; // Used as the base for the rotations. VertexCoords prevPts; // Use to restore the old point values. uint origIndex; private: polyobj_s *_polyobj; #ifdef __CLIENT__ ClPolyMover *_mover; #endif }; #endif // DENG_WORLD_POLYOBJDATA_H doomsday-stable-1.15.7/doomsday/client/include/world/p_object.h0000664000175000017500000001707612641367670024014 0ustar jaakkojaakko/** @file p_object.h World map objects. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_WORLD_P_OBJECT_H #define DENG_WORLD_P_OBJECT_H #if defined(__JDOOM__) || defined(__JHERETIC__) || defined(__JHEXEN__) # error Attempted to include internal Doomsday p_object.h from a game #endif #include "api_map.h" #include "dd_def.h" #ifdef __CLIENT__ # include "ModelDef" # include "Sprite" #endif #include #include #include #include #include class BspLeaf; class Plane; class SectorCluster; #define MOBJ_SIZE gx.mobjSize class MobjThinker : public ThinkerT { public: MobjThinker(AllocMethod alloc = AllocateStandard) : ThinkerT(MOBJ_SIZE, alloc) {} MobjThinker(mobj_t const &existingToCopy) : ThinkerT(existingToCopy, MOBJ_SIZE) {} MobjThinker(mobj_t *existingToTake) : ThinkerT(existingToTake, MOBJ_SIZE) {} static void zap(mobj_t &mobj) { ThinkerT::zap(mobj, MOBJ_SIZE); } }; #define DEFAULT_FRICTION FIX2FLT(0xe800) #define NOMOMENTUM_THRESHOLD (0.0001) #define IS_BLOCK_LINKED(mo) ((mo)->bNext != 0) DENG_EXTERN_C int useSRVO, useSRVOAngle; void P_InitUnusedMobjList(); /** * To be called to register the commands and variables of this module. */ void Mobj_ConsoleRegister(); mobj_t *P_MobjCreate(thinkfunc_t function, de::Vector3d const &origin, angle_t angle, coord_t radius, coord_t height, int ddflags); void P_MobjRecycle(mobj_t *mobj); /** * Returns the map in which the mobj exists. Note that a mobj may exist in a map * while not being @em linked into data structures such as the blockmap and sectors. * To determine whether the mobj is linked, call @ref Mobj_IsLinked(). * * @see Thinker_Map() */ de::Map &Mobj_Map(mobj_t const &mobj); /** * Returns @c true iff the mobj has been linked into the map. The only time this * is not true is if @ref Mobj_SetOrigin() has not yet been called. * * @param mobj Mobj instance. * * @todo Automatically link all new mobjs into the map (making this redundant). */ bool Mobj_IsLinked(mobj_t const &mobj); /** * Returns a copy of the mobj's map space origin. */ de::Vector3d Mobj_Origin(mobj_t const &mobj); /** * Returns the mobj's visual center (i.e., origin plus z-height offset). */ de::Vector3d Mobj_Center(mobj_t &mobj); /** * Sets a mobj's position. * * @return @c true if successful, @c false otherwise. The object's position is * not changed if the move fails. * * @note Internal to the engine. */ dd_bool Mobj_SetOrigin(mobj_t *mobj, coord_t x, coord_t y, coord_t z); /** * Returns the map BSP leaf at the origin of the mobj. Note that the mobj must * be linked in the map (i.e., @ref Mobj_SetOrigin() has been called). * * @param mobj Mobj instance. * * @see Mobj_IsLinked(), Mobj_SetOrigin() */ BspLeaf &Mobj_BspLeafAtOrigin(mobj_t const &mobj); /** * Returns @c true iff the BSP leaf at the mobj's origin is known (i.e., * it has been linked into the map by calling @ref Mobj_SetOrigin() and has a * convex geometry). * * @param mobj Mobj instance. */ bool Mobj_HasSubspace(mobj_t const &mobj); /** * Returns the sector cluster in which the mobj currently resides. * * @param mobj Mobj instance. * * @see Mobj_HasSubspace() */ SectorCluster &Mobj_Cluster(mobj_t const &mobj); /** * Returns a pointer to sector cluster in which the mobj currently resides, or * @c 0 if not linked or the BSP leaf at the origin has no convex geometry. * * @param mobj Mobj instance. * * @see Mobj_HasCluster() */ SectorCluster *Mobj_ClusterPtr(mobj_t const &mobj); /** * Creates a new mobj-triggered particle generator based on the given * definition. The generator is added to the list of active ptcgens. */ void Mobj_SpawnParticleGen(mobj_t *source, ded_ptcgen_t const *def); #ifdef __CLIENT__ /** * Determines whether the Z origin of the mobj lies above the visual ceiling, * or below the visual floor plane of the BSP leaf at the origin. This can be * used to determine whether this origin should be adjusted with respect to * smoothed plane movement. */ dd_bool Mobj_OriginBehindVisPlane(mobj_t *mobj); /** * To be called when lumobjs are disabled to perform necessary bookkeeping. */ void Mobj_UnlinkLumobjs(mobj_t *mobj); /** * Generates lumobjs for the mobj. * @note: This is called each frame for each luminous object! */ void Mobj_GenerateLumobjs(mobj_t *mobj); void Mobj_AnimateHaloOcclussion(mobj_t &mob); /** * Calculate the strength of the shadow this map-object should cast. * * @note Implemented using a greatly simplified version of the lighting equation; * no light diminishing or light range compression. */ de::dfloat Mobj_ShadowStrength(mobj_t const &mob); /** * Determines which of the available sprites is in effect for the current mobj * state and frame. May return @c 0 if the state and/or frame is not valid. */ Sprite *Mobj_Sprite(mobj_t const &mob); /** * Determines which of the available model definitions (if any), are in effect * for the current mobj state and frame. (Interlinks are resolved). * * @param nextModef If non-zero the model definition for the @em next frame is * written here. * @param interp If non-zero and both model definitions are found the current * interpolation point between the two is written here. * * @return Active model definition for the current frame (if any). */ ModelDef *Mobj_ModelDef(mobj_t const &mobj, ModelDef **nextModef = 0, float *interp = 0); /** * Determines the shadow radius of a mobj. Falls back to Mobj_VisualRadius(). * * @param mobj Map object. * * @return Radius for shadow. */ coord_t Mobj_ShadowRadius(mobj_t const &mobj); #endif // __CLIENT__ coord_t Mobj_ApproxPointDistance(mobj_t *start, coord_t const *point); dd_bool Mobj_IsSectorLinked(mobj_t *mobj); /** * Returns the current "float bob" offset for the given map-object @a mob (if enabled); otherwise @c 0. */ coord_t Mobj_BobOffset(mobj_t const &mob); de::dfloat Mobj_Alpha(mobj_t const &mob); /** * Returns the physical radius of the mobj. * * @param mob Map-object. * * @see Mobj_VisualRadius() */ coord_t Mobj_Radius(mobj_t const &mob); /** * Returns the radius of the mobj as it would visually appear to be, according * to the current visualization (either a sprite or a 3D model). * * @param mob Map-object. * * @see Mobj_Radius() */ coord_t Mobj_VisualRadius(mobj_t const &mob); /** * Returns an axis-aligned bounding box for the mobj in map space, centered * on the origin with dimensions equal to @code radius * 2 @endcode. * * @param mobj Mobj instance. * * @see Mobj_Radius() */ AABoxd Mobj_AABox(mobj_t const &mobj); #endif // DENG_WORLD_P_OBJECT_H doomsday-stable-1.15.7/doomsday/client/include/world/line.h0000664000175000017500000006741112641367670023154 0ustar jaakkojaakko/** @file line.h World map line. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_LINE_H #define DENG_WORLD_LINE_H #include #include #include #include #include #include "HEdge" #include "MapElement" #include "Polyobj" #include "Vertex" class LineOwner; class Sector; class Surface; #ifdef __CLIENT__ class BiasDigest; #endif /** * World map line. * * @attention This component has a notably different design and slightly different * purpose when compared to a Linedef in the id Tech 1 map format. The definitions * of which are not always interchangeable. * * DENG lines always have two logical sides, however they may not have a sector * attributed to either or both sides. * * @note Lines are @em not considered to define the geometry of a map. Instead * a line should be thought of as a finite line segment in the plane, according * to the standard definition of a line as used with an arrangement of lines in * computational geometry. * * @see http://en.wikipedia.org/wiki/Arrangement_of_lines * * @ingroup world */ class Line : public de::MapElement { DENG2_NO_COPY (Line) DENG2_NO_ASSIGN(Line) public: /// Required sector attribution is missing. @ingroup errors DENG2_ERROR(MissingSectorError); /// Required polyobj attribution is missing. @ingroup errors DENG2_ERROR(MissingPolyobjError); /// The given side section identifier is invalid. @ingroup errors DENG2_ERROR(InvalidSectionIdError); /// Notified whenever the flags change. DENG2_DEFINE_AUDIENCE(FlagsChange, void lineFlagsChanged(Line &line, int oldFlags)) // Logical edge identifiers: enum { From, To }; // Logical side identifiers: enum { Front, Back }; /** * Logical side of which there are always two (a front and a back). */ class Side : public de::MapElement { DENG2_NO_COPY (Side) DENG2_NO_ASSIGN(Side) public: // Section identifiers: enum { Middle, Bottom, Top }; /** * Flags used as Section identifiers: */ enum SectionFlag { MiddleFlag = 0x1, BottomFlag = 0x2, TopFlag = 0x4, AllSectionFlags = MiddleFlag | BottomFlag | TopFlag }; Q_DECLARE_FLAGS(SectionFlags, SectionFlag) /** * Side geometry segment on the XY plane. */ class Segment : public de::MapElement { DENG2_NO_COPY (Segment) DENG2_NO_ASSIGN(Segment) public: /** * Construct a new line side segment. * * @param lineSide Side parent which will own the segment. * @param hedge Half-edge from the map geometry mesh which the * new segment visualizes. */ Segment(Side &lineSide, de::HEdge &hedge); /** * Returns the line side owner of the segment. */ inline Side &lineSide() { return parent().as(); } inline Side const &lineSide() const { return parent().as(); } /** * Convenient accessor method for returning the line of the owning * line side. * * @see lineSide() */ inline Line &line() { return lineSide().line(); } inline Line const &line() const { return lineSide().line(); } /** * Returns the half-edge for the segment. */ de::HEdge &hedge() const; #ifdef __CLIENT__ /** * Returns the distance along the attributed map line at which the * from vertex vertex occurs. * * @see lineSide() */ coord_t lineSideOffset() const; /// @todo Refactor away. void setLineSideOffset(coord_t newOffset); /** * Returns the accurate length of the segment, from the 'from' * vertex to the 'to' vertex in map coordinate space units. */ coord_t length() const; /// @todo Refactor away. void setLength(coord_t newLength); /** * Returns @c true iff the segment is marked as "front facing". */ bool isFrontFacing() const; /** * Mark the current segment as "front facing". */ void setFrontFacing(bool yes = true); #endif // __CLIENT__ private: DENG2_PRIVATE(d) }; public: /** * Construct a new line side. * * @param line Line parent which will own the side. * @param sector Sector on "this" side of the line. Can be @c 0. Note * once attributed the sector cannot normally be changed. */ Side(Line &line, Sector *sector = 0); /** * Returns the Line owner of the side. */ inline Line &line() { return parent().as(); } inline Line const &line() const { return parent().as(); } /** * Returns the logical identifier for the side (Front or Back). */ int sideId() const; /** * Returns @c true iff this is the front side of the owning line. * * @see lineSideId() */ inline bool isFront() const { return sideId() == Front; } /** * Returns @c true iff this is the back side of the owning line. * * @see lineSideId(), isFront() */ inline bool isBack() const { return !isFront(); } /** * Returns the relative back Side from the Line owner. * * @see lineSideId(), line(), Line::side(), */ inline Side &back() { return line().side(sideId() ^ 1); } /// @copydoc back() inline Side const &back() const { return line().side(sideId() ^ 1); } /** * Determines whether "this" side of the respective line should be * considered as though there were no back sector. Primarily for use * with id Tech 1 format maps (which, supports partial suppression of * the back sector, for use with special case drawing and playsim * functionality). */ bool considerOneSided() const; /** * Returns the specified relative vertex from the Line owner. * * @see lineSideId(), line(), Line::vertex(), */ inline Vertex &vertex(int to) const { return line().vertex(sideId() ^ to); } /** * Returns the relative From Vertex for the side, from the Line owner. * * @see vertex(), to() */ inline Vertex &from() const { return vertex(From); } /** * Returns the relative To Vertex for the side, from the Line owner. * * @see vertex(), from() */ inline Vertex &to () const { return vertex(To); } /** * Returns @c true iff Sections are defined for the side. * * @see addSections() */ bool hasSections() const; /** * Add default sections to the side if they aren't already defined. * * @see hasSections() */ void addSections(); /** * Returns the specified surface of the side. * * @param sectionId Identifier of the surface to return. */ Surface &surface(int sectionId); Surface const &surface(int sectionId) const; /** * Returns the middle surface of the side. * * @see surface() */ inline Surface &middle() { return surface(Middle); } inline Surface const &middle() const { return surface(Middle); } /** * Returns the bottom surface of the side. * * @see surface() */ inline Surface &bottom() { return surface(Bottom); } inline Surface const &bottom() const { return surface(Bottom); } /** * Returns the top surface of the side. * * @see surface() */ inline Surface &top() { return surface(Top); } inline Surface const &top() const { return surface(Top); } /** * Returns the specified sound emitter of the side. * * @param sectionId Identifier of the sound emitter to return. * * @see Section::soundEmitter() */ SoundEmitter &soundEmitter(int sectionId); SoundEmitter const &soundEmitter(int sectionId) const; /** * Returns the middle sound emitter of the side. * * @see Section::soundEmitter() */ inline SoundEmitter &middleSoundEmitter() { return soundEmitter(Middle); } inline SoundEmitter const &middleSoundEmitter() const { return soundEmitter(Middle); } /** * Returns the bottom sound emitter (tee-hee) for the side. * * @see Section::soundEmitter() */ inline SoundEmitter &bottomSoundEmitter() { return soundEmitter(Bottom); } inline SoundEmitter const &bottomSoundEmitter() const { return soundEmitter(Bottom); } /** * Returns the top sound emitter for the side. * * @see Section::soundEmitter() */ inline SoundEmitter &topSoundEmitter() { return soundEmitter(Top); } inline SoundEmitter const &topSoundEmitter() const { return soundEmitter(Top); } /** * Update the sound emitter origin of the specified surface section. This * point is determined according to the center point of the owning line and * the current @em sharp heights of the sector on "this" side of the line. */ void updateSoundEmitterOrigin(int sectionId); /** * Update the @em middle sound emitter origin for the side. * @see updateSoundEmitterOrigin() */ inline void updateMiddleSoundEmitterOrigin() { updateSoundEmitterOrigin(Middle); } /** * Update the @em bottom sound emitter origin for the side. * @see updateSoundEmitterOrigin() */ inline void updateBottomSoundEmitterOrigin() { updateSoundEmitterOrigin(Bottom); } /** * Update the @em top sound emitter origin for the side. * @see updateSoundEmitterOrigin() */ inline void updateTopSoundEmitterOrigin() { updateSoundEmitterOrigin(Top); } /** * Update ALL sound emitter origins for the side. * @see updateSoundEmitterOrigin() */ void updateAllSoundEmitterOrigins(); /** * Returns @c true iff a Sector is attributed to the side. * * @see considerOneSided() */ bool hasSector() const; /** * Returns the Sector attributed to the side. * * @see hasSector() */ Sector §or() const; /** * Returns a pointer to the Sector attributed to the side; otherwise @c 0. * * @see hasSector() */ inline Sector *sectorPtr() const { return hasSector()? §or() : 0; } /** * Clears (destroys) all segments for the side. */ void clearSegments(); /** * Create a Segment for the specified half-edge. If an existing Segment * is present for the half-edge it will be returned instead (nothing will * happen). * * It is assumed that the half-edge is collinear with and represents a * subsection of the line geometry. It is also assumed that the half-edge * faces the same direction as this side. It is the caller's responsibility * to ensure these two requirements are met otherwise the segment list * will be ordered illogically. * * @param hedge Half-edge to create a new Segment for. * * @return Pointer to the (possibly newly constructed) Segment. */ Segment *addSegment(de::HEdge &hedge); /** * Convenient method of returning the half-edge of the left-most segment * on this side of the line; otherwise @c 0 (no segments exist). */ de::HEdge *leftHEdge() const; /** * Convenient method of returning the half-edge of the right-most segment * on this side of the line; otherwise @c 0 (no segments exist). */ de::HEdge *rightHEdge() const; /** * Update the tangent space normals of the side's surfaces according to the * points defined by the Line's vertices. If no Sections are defined this is * a no-op. */ void updateSurfaceNormals(); /** * Returns the @ref sdefFlags for the side. */ int flags() const; /** * Change the side's flags. * * @param flagsToChange Flags to change the value of. * @param operation Logical operation to perform on the flags. */ void setFlags(int flagsToChange, de::FlagOp operation = de::SetFlags); /** * Returns @c true iff the side is flagged @a flagsToTest. */ inline bool isFlagged(int flagsToTest) const { return (flags() & flagsToTest) != 0; } void chooseSurfaceTintColors(int sectionId, de::Vector3f const **topColor, de::Vector3f const **bottomColor) const; /** * Returns the frame number of the last time shadows were drawn for the side. */ int shadowVisCount() const; /** * Change the frame number of the last time shadows were drawn for the side. * * @param newCount New shadow vis count. */ void setShadowVisCount(int newCount); #ifdef __CLIENT__ /** * Do as in the original DOOM if the texture has not been defined - * extend the floor/ceiling to fill the space (unless it is skymasked). */ void fixMissingMaterials(); #endif // __CLIENT__ protected: int property(DmuArgs &args) const; int setProperty(DmuArgs const &args); private: DENG2_PRIVATE(d) }; public: /// @todo make private: /// Links to vertex line owner nodes: LineOwner *_vo1 = nullptr; LineOwner *_vo2 = nullptr; /// Sector of the map for which this line acts as a "One-way window". /// @todo Now unnecessary, refactor away -ds Sector *_bspWindowSector = nullptr; public: Line(Vertex &from, Vertex &to, int flags = 0, Sector *frontSector = nullptr, Sector *backSector = nullptr); /** * Returns the specified logical side of the line. * * @param back If not @c 0 return the Back side; otherwise the Front side. */ Side &side(int back); Side const &side(int back) const; /** * Returns the logical Front side of the line. */ inline Side &front() { return side(Front); } inline Side const &front() const { return side(Front); } /** * Returns the logical Back side of the line. */ inline Side &back() { return side(Back); } inline Side const &back() const { return side(Back); } /** * Returns @c true iff Side::Sections are defined for the specified side * of the line. * * @param back If not @c 0 test the Back side; otherwise the Front side. */ inline bool hasSections(int back) const { return side(back).hasSections(); } /** * Returns @c true iff Side::Sections are defined for the Front side of the line. */ inline bool hasFrontSections() const { return hasSections(Front); } /** * Returns @c true iff Side::Sections are defined for the Back side of the line. */ inline bool hasBackSections() const { return hasSections(Back); } /** * Returns @c true iff a sector is attributed to the specified side of the line. * * @param back If not @c 0 test the Back side; otherwise the Front side. */ inline bool hasSector(int back) const { return side(back).hasSector(); } /** * Returns @c true iff a sector is attributed to the Front side of the line. */ inline bool hasFrontSector() const { return hasSector(Front); } /** * Returns @c true iff a sector is attributed to the Back side of the line. */ inline bool hasBackSector() const { return hasSector(Back); } /** * Convenient accessor method for returning the sector attributed to the * specified side of the line. * * @param back If not @c 0 return the sector for the Back side; otherwise * the sector of the Front side. */ inline Sector §or(int back) const { return side(back).sector(); } /** * Convenient accessor method for returning a pointer to the sector attributed * to the specified side of the line. * * @param back If not @c 0 return the sector for the Back side; otherwise * the sector of the Front side. */ inline Sector *sectorPtr(int back) const { return side(back).sectorPtr(); } /** * Returns the sector attributed to the Front side of the line. */ inline Sector &frontSector() const { return sector(Front); } /** * Returns the sector attributed to the Back side of the line. */ inline Sector &backSector() const { return sector(Back); } /** * Convenient accessor method for returning a pointer to the sector attributed * to the front side of the line. */ inline Sector *frontSectorPtr() const { return sectorPtr(Front); } /** * Convenient accessor method for returning a pointer to the sector attributed * to the back side of the line. */ inline Sector *backSectorPtr() const { return sectorPtr(Back); } /** * Returns @c true iff the line is considered @em self-referencing. * In this context, self-referencing (a term whose origins stem from the * DOOM modding community) means a two-sided line (which is to say that * a Sector is attributed to both logical sides of the line) where the * attributed sectors for each logical side are the same. */ inline bool isSelfReferencing() const { return hasFrontSector() && frontSectorPtr() == backSectorPtr(); } /** * Returns the specified edge vertex of the line. * * @param to If not @c 0 return the To vertex; otherwise the From vertex. */ Vertex &vertex(int to) const; /** * Convenient accessor method for returning the origin of the specified * edge vertex for the line. * * @see vertex() */ inline de::Vector2d const &vertexOrigin(int to) const { return vertex(to).origin(); } /** * Returns the From/Start vertex for the line. */ inline Vertex &from() const { return vertex(From); } /** * Returns the To/End vertex for the line. */ inline Vertex &to() const { return vertex(To); } /** * Convenient accessor method for returning the origin of the From/Start * vertex for the line. * * @see from() */ inline de::Vector2d const &fromOrigin() const { return from().origin(); } /** * Convenient accessor method for returning the origin of the To/End * vertex for the line. * * @see to() */ inline de::Vector2d const &toOrigin() const { return to().origin(); } /** * Returns the point on the line which lies at the exact center of the * two vertexes. */ inline de::Vector2d center() const { return fromOrigin() + direction() / 2; } /** * Returns the binary angle of the line (which, is derived from the * direction vector). * * @see direction() */ binangle_t angle() const; /** * Returns a direction vector for the line from Start to End vertex. */ de::Vector2d const &direction() const; /** * Returns the logical @em slopetype for the line (which, is determined * according to the global direction of the line). * * @see direction() * @see M_SlopeType() */ slopetype_t slopeType() const; /** * Update the line's logical slopetype and direction according to the * points defined by the origins of it's vertexes. */ void updateSlopeType(); /** * Returns the accurate length of the line from Start to End vertex. */ coord_t length() const; /** * Returns @c true iff the line has a length equivalent to zero. */ inline bool hasZeroLength() const { return de::abs(length()) < 1.0 / 128.0; } /** * Returns the axis-aligned bounding box which encompases both vertex * origin points, in map coordinate space units. */ AABoxd const &aaBox() const; /** * Update the line's map space axis-aligned bounding box to encompass * the points defined by it's vertexes. */ void updateAABox(); /** * On which side of the line does the specified box lie? * * @param box Bounding box to test. * * @return One of the following: * - Negative: @a box is entirely on the left side. * - Zero: @a box intersects the line. * - Positive: @a box is entirely on the right side. */ int boxOnSide(AABoxd const &box) const; /** * On which side of the line does the specified box lie? The test is * carried out using fixed-point math for behavior compatible with * vanilla DOOM. Note that this means there is a maximum size for both * the bounding box and the line: neither can exceed the fixed-point * 16.16 range (about 65k units). * * @param box Bounding box to test. * * @return One of the following: * - Negative: @a box is entirely on the left side. * - Zero: @a box intersects the line. * - Positive: @a box is entirely on the right side. */ int boxOnSide_FixedPrecision(AABoxd const &box) const; /** * @param offset Returns the position of the nearest point along the line [0..1]. */ coord_t pointDistance(de::Vector2d const &point, coord_t *offset = nullptr) const; /** * Where does the given @a point lie relative to the line? Note that the * line is considered to extend to infinity for this test. * * @param point The point to test. * * @return @c <0 Point is to the left of the line. * @c =0 Point lies directly on/incident with the line. * @c >0 Point is to the right of the line. */ coord_t pointOnSide(de::Vector2d const &point) const; /** * Returns @c true iff the line defines a section of some Polyobj. */ bool definesPolyobj() const; /** * Returns the Polyobj for which the line is a defining section. * * @see definesPolyobj() */ Polyobj &polyobj() const; /** * Change the polyobj attributed to the line. * * @param newPolyobj New polyobj to attribute the line to. Can be @c 0, * to clear the attribution. (Note that the polyobj may * also represent this relationship, so the relevant * method(s) of Polyobj will also need to be called to * complete the job of clearing this relationship.) */ void setPolyobj(Polyobj *newPolyobj); /** * Returns @c true iff the line resulted in the creation of a BSP window * effect when partitioning the map. * * @todo Refactor away. The prescence of a BSP window effect can now be * trivially determined through inspection of the tree elements. */ bool isBspWindow() const; /** * Returns the public DDLF_* flags for the line. */ int flags() const; /** * Change the line's flags. The FlagsChange audience is notified whenever * the flags are changed. * * @param flagsToChange Flags to change the value of. * @param operation Logical operation to perform on the flags. */ void setFlags(int flagsToChange, de::FlagOp operation = de::SetFlags); /** * Returns @c true iff the line is flagged @a flagsToTest. */ inline bool isFlagged(int flagsToTest) const { return (flags() & flagsToTest) != 0; } /** * Returns @c true if the line is marked as @em mapped for @a playerNum. */ bool isMappedByPlayer(int playerNum) const; /** * Change the @em mapped by player state of the line. */ void markMappedByPlayer(int playerNum, bool yes = true); /** * Returns the @em validCount of the line. Used by some legacy iteration * algorithms for marking lines as processed/visited. * * @todo Refactor away. */ int validCount() const; /// @todo Refactor away. void setValidCount(int newValidCount); /** * Replace the specified edge vertex of the line. * * @attention Should only be called in map edit mode. * * @param to If not @c 0 replace the To vertex; otherwise the From vertex. * @param newVertex The replacement vertex. */ void replaceVertex(int to, Vertex &newVertex); inline void replaceFrom(Vertex &newVertex) { replaceVertex(From, newVertex); } inline void replaceTo(Vertex &newVertex) { replaceVertex(To, newVertex); } protected: int property(DmuArgs &args) const; int setProperty(DmuArgs const &args); public: /** * Returns a pointer to the line owner node for the specified edge vertex * of the line. * * @param to If not @c 0 return the owner for the To vertex; otherwise the * From vertex. * * @deprecated Will be replaced with half-edge ring iterator/rover. -ds */ LineOwner *vertexOwner(int to) const; /** * Returns a pointer to the line owner for the specified edge @a vertex * of the line. If the vertex is not an edge vertex for the line then * @c 0 will be returned. */ inline LineOwner *vertexOwner(Vertex const &vertex) const { if(&vertex == &from()) return v1Owner(); if(&vertex == &to()) return v2Owner(); return 0; } /** * Returns a pointer to the line owner node for the From/Start vertex of the line. * * @deprecated Will be replaced with half-edge ring iterator/rover. -ds */ inline LineOwner *v1Owner() const { return vertexOwner(From); } /** * Returns a pointer to the line owner node for the To/End vertex of the line. * * @deprecated Will be replaced with half-edge ring iterator/rover. -ds */ inline LineOwner *v2Owner() const { return vertexOwner(To); } private: DENG2_PRIVATE(d) }; typedef Line::Side LineSide; typedef Line::Side::Segment LineSideSegment; Q_DECLARE_OPERATORS_FOR_FLAGS(Line::Side::SectionFlags) #endif // DENG_WORLD_LINE_H doomsday-stable-1.15.7/doomsday/client/include/world/blockmap.h0000664000175000017500000001422112641367670024004 0ustar jaakkojaakko/** @file blockmap.h World map element blockmap. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_BLOCKMAP_H #define DENG_WORLD_BLOCKMAP_H #include #include #include #ifdef WIN32 # undef max # undef min #endif namespace de { /** * @ingroup world */ class Blockmap { public: typedef Vector2ui Cell; /** * POD structure for representing an inclusive-exclusive rectangular range * of cells. * * @todo Use Rectangleui instead -ds */ struct CellBlock { Cell min; Cell max; CellBlock(Cell const &min = Cell(), Cell const &max = Cell()) : min(min), max(max) {} CellBlock(CellBlock const &other) : min(other.min), max(other.max) {} }; public: /** * @param bounds Map space boundary. * @param cellSize Width and height of a cell in map space units. */ Blockmap(AABoxd const &bounds, uint cellSize = 128); virtual ~Blockmap(); /** * Returns the origin of the blockmap in map space. */ Vector2d origin() const; /** * Returns the bounds of the blockmap in map space. */ AABoxd const &bounds() const; /** * Returns the dimensions of the blockmap in cells. */ Cell const &dimensions() const; /** * Returns the width of the blockmap in cells. */ inline uint width() const { return dimensions().x; } /** * Returns the height of the blockmap in cells. */ inline uint height() const { return dimensions().y; } /** * Returns @c true iff the blockmap is of zero-area. */ inline bool isNull() const { return (width() * height()) == 0; } /** * Returns the size of a cell (width and height) in map space units. */ uint cellSize() const; /** * Utility function which returns the dimensions of a cell in map space units. */ Vector2d cellDimensions() const { return Vector2d(cellSize(), cellSize()); } /** * Utility function which returns the linear index of the specified cell. */ int toCellIndex(uint cellX, uint cellY) const; /** * Given map space XY coordinates @a pos, output the blockmap cell[x, y] it * resides in. If @a pos is outside the blockmap it will be clamped to the * nearest edge on one or more axes as necessary. * * @param point Map coordinate space point to be translated. * @param didClip Set to @c true iff clamping was necessary. */ Cell toCell(Vector2d const &point, bool *didClip = 0) const; /** * Given map space box XY coordinates @a box, output the blockmap cells[x, y] * they reside in. If any point defined by @a box lies outside the blockmap * it will be clamped to the nearest edge on one or more axes as necessary. * * @param box Map space coordinates to translate. * @param didClip Set to @c true iff clamping was necessary. */ CellBlock toCellBlock(AABoxd const &box, bool *didClip = 0) const; /** * Retrieve the number of elements linked in the specified @a cell. * * @param cell Cell to lookup. * * @return Number of unique objects linked into the cell, or @c 0 if invalid. */ int cellElementCount(Cell const &cell) const; bool link(Cell const &cell, void *elem); bool link(AABoxd const ®ion, void *elem); bool unlink(Cell const &cell, void *elem); bool unlink(AABoxd const ®ion, void *elem); void unlinkAll(); /** * Iterate through all objects in the given @a cell. */ LoopResult forAllInCell(Cell const &cell, std::function func) const; /** * Iterate through all objects in all cells which intercept the given map * space, axis-aligned bounding @a box. */ LoopResult forAllInBox(AABoxd const &box, std::function func) const; /** * Iterate over all objects in cells which intercept the line specified by * the two map space points @a from and @a to. Note that if an object is * processed/visited it does @em not mean that the line actually intercepts * the objects. Further testing between the line and the geometry of the map * object is necessary if this is a requirement. * * @param from Map space point defining the origin of the line. * @param to Map space point defining the destination of the line. */ LoopResult forAllInPath(Vector2d const &from, Vector2d const &to, std::function func) const; /** * Render a visual for this gridmap to assist in debugging (etc...). * * This visualizer assumes that the caller has already configured the GL * render state (projection matrices, scale, etc...) as desired prior to * calling. This function guarantees to restore the previous GL state if * any changes are made to it. * * @note Internally this visual uses fixed unit dimensions [1x1] for cells, * therefore the caller should scale the appropriate matrix to scale this * visual as desired. */ void drawDebugVisual() const; private: DENG2_PRIVATE(d) }; typedef Blockmap::Cell BlockmapCell; typedef Blockmap::CellBlock BlockmapCellBlock; } //namespace de #endif // DENG_WORLD_BLOCKMAP_H doomsday-stable-1.15.7/doomsday/client/include/world/sector.h0000664000175000017500000002110712641367670023514 0ustar jaakkojaakko/** @file sector.h World map sector. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_SECTOR_H #define DENG_WORLD_SECTOR_H #include #ifdef __CLIENT__ # include #endif #include #include #include #include "MapElement" #include "Line" #include "Plane" struct mobj_s; class Surface; /** * World map sector. * * @ingroup world */ class Sector : public de::MapElement { DENG2_NO_COPY (Sector) DENG2_NO_ASSIGN(Sector) public: /// Required/referenced plane is missing. @ingroup errors DENG2_ERROR(MissingPlaneError); /// Notified whenever a light level change occurs. DENG2_DEFINE_AUDIENCE(LightLevelChange, void sectorLightLevelChanged(Sector §or)) /// Notified whenever a light color change occurs. DENG2_DEFINE_AUDIENCE(LightColorChange, void sectorLightColorChanged(Sector §or)) // Plane identifiers: enum { Floor, Ceiling }; public: /** * Construct a new sector. * * @param lightLevel Ambient light level. * @param lightColor Ambient light color. */ Sector(float lightLevel = 1, de::Vector3f const &lightColor = de::Vector3f(1, 1, 1)); /** * Returns @c true if at least one Plane in the sector is sky-masked. * * @see Surface::hasSkyMaskedMaterial() */ bool hasSkyMaskedPlane() const; /** * Returns the total number of planes in/owned by the sector. */ int planeCount() const; /** * Lookup a Plane by it's sector-unique @a planeIndex. */ Plane &plane(int planeIndex); Plane const &plane(int planeIndex) const; /** * Returns the @em floor Plane of the sector. */ inline Plane &floor() { return plane(Floor); } inline Plane const &floor() const { return plane(Floor); } /** * Returns the @em ceiling Plane of the sector. */ inline Plane &ceiling() { return plane(Ceiling); } inline Plane const &ceiling() const { return plane(Ceiling); } /** * Add a new Plane to the sector. * * @param normal World space normal for the new plane. * @param height World space Z axis coordinate for the new plane. */ Plane *addPlane(de::Vector3f const &normal, coord_t height); /** * Iterate through the Planes of the sector. * * @param func Callback to make for each Plane. */ de::LoopResult forAllPlanes(std::function func) const; /** * Convenient accessor method for returning the surface of the specified * plane of the sector. */ inline Surface &planeSurface(int planeIndex) { return plane(planeIndex).surface(); } inline Surface const &planeSurface(int planeIndex) const { return plane(planeIndex).surface(); } /** * Convenient accessor method for returning the surface of the floor plane * of the sector. */ inline Surface &floorSurface() { return floor().surface(); } inline Surface const &floorSurface() const { return floor().surface(); } /** * Convenient accessor method for returning the surface of the ceiling plane * of the sector. */ inline Surface &ceilingSurface() { return ceiling().surface(); } inline Surface const &ceilingSurface() const { return ceiling().surface(); } /** * Returns the total number of Line::Sides which reference the sector. */ int sideCount() const; /** * Iterate through the Line::Sides of the sector. * * @param func Callback to make for each Line::Side. */ de::LoopResult forAllSides(std::function func) const; /** * (Re)Build the side list for the sector. * * @note In the special case of self-referencing line, only the front side * reference is added to this list. * * @attention The behavior of some algorithms used in the DOOM game logic * is dependant upon the order of this list. For example, EV_DoFloor and * EV_BuildStairs. That same order is used here, for compatibility. * * Order: Original @em line index, ascending. */ void buildSides(); /** * Returns the primary sound emitter for the sector. Other emitters in the * sector are linked to this, forming a chain which can be traversed using * the 'next' pointer of the emitter's thinker_t. */ SoundEmitter &soundEmitter(); SoundEmitter const &soundEmitter() const; /** * (Re)Build the sound emitter chains for the sector. These chains are used * for efficiently traversing all sound emitters in the sector (e.g., when * stopping all sounds emitted in the sector). To be called during map load * once planes and sides have been initialized. * * @see addPlane(), buildSides() */ void chainSoundEmitters(); /** * Returns the ambient light level in the sector. The LightLevelChange * audience is notified whenever the light level changes. * * @see setLightLevel() */ float lightLevel() const; /** * Change the ambient light level in the sector. The LightLevelChange * audience is notified whenever the light level changes. * * @param newLightLevel New ambient light level. * * @see lightLevel() */ void setLightLevel(float newLightLevel); /** * Returns the ambient light color in the sector. The LightColorChange * audience is notified whenever the light color changes. * * @see setLightColor() */ de::Vector3f const &lightColor() const; /** * Change the ambient light color in the sector. The LightColorChange * audience is notified whenever the light color changes. * * @param newLightColor New ambient light color. * * @see lightColor() */ void setLightColor(de::Vector3f const &newLightColor); /** * Returns the first mobj in the linked list of mobjs "in" the sector. */ struct mobj_s *firstMobj() const; /** * Unlink the mobj from the list of mobjs "in" the sector. * * @param mob Mobj to be unlinked. */ void unlink(struct mobj_s *mob); /** * Link the mobj to the head of the list of mobjs "in" the sector. Note that * mobjs in this list may not actually be inside the sector. This is because * the sector is determined by interpreting the BSP leaf as a half-space and * not a closed convex subspace (@ref de::Map::link()). * * @param mob Mobj to be linked. */ void link(struct mobj_s *mob); /** * Returns the @em validCount of the sector. Used by some legacy iteration * algorithms for marking sectors as processed/visited. * * @todo Refactor away. */ int validCount() const; /// @todo Refactor away. void setValidCount(int newValidCount); #ifdef __CLIENT__ /** * Returns the axis-aligned bounding box which encompases the geometry of * all BSP leafs attributed to the sector (map units squared). Note that if * no BSP leafs reference the sector the bounding box will be invalid (has * negative dimensions). * * @todo Refactor away (still used by light decoration and particle systems). */ AABoxd const &aaBox() const; /** * Returns a rough approximation of the total combined area of the geometry * for all BSP leafs attributed to the sector (map units squared). * * @todo Refactor away (still used by the particle system). */ coord_t roughArea() const; #endif // __CLIENT__ protected: int property(DmuArgs &args) const; int setProperty(DmuArgs const &args); private: DENG2_PRIVATE(d) }; #endif // DENG_WORLD_SECTOR_H doomsday-stable-1.15.7/doomsday/client/include/world/maputil.h0000664000175000017500000000360112641367670023667 0ustar jaakkojaakko/** @file maputil.h World map utilities. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifdef __CLIENT__ #ifndef DENG_WORLD_MAPUTIL_H #define DENG_WORLD_MAPUTIL_H #include #include "Line" class Sector; class LineOwner; /** * @param side LineSide instance. * @param ignoreOpacity @c true= do not consider Material opacity. * * @return @c true if this side is considered "closed" (i.e., there is no opening * through which the relative back Sector can be seen). Tests consider all Planes * which interface with this and the "middle" Material used on the "this" side. */ bool R_SideBackClosed(LineSide const &side, bool ignoreOpacity = true); /** * A neighbour is a line that shares a vertex with 'line', and faces the * specified sector. */ Line *R_FindLineNeighbor(Sector const *sector, Line const *line, LineOwner const *own, bool antiClockwise, binangle_t *diff = 0); Line *R_FindSolidLineNeighbor(Sector const *sector, Line const *line, LineOwner const *own, bool antiClockwise, binangle_t *diff = 0); #endif // DENG_WORLD_MAPUTIL_H #endif // __CLIENT__ doomsday-stable-1.15.7/doomsday/client/include/world/bspleaf.h0000664000175000017500000000662512641367670023641 0ustar jaakkojaakko/** @file bspleaf.h World map BSP leaf half-space. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_BSPLEAF_H #define DENG_WORLD_BSPLEAF_H #include #include "MapElement" #include "BspNode" class ConvexSubspace; class Sector; /** * Represents a leaf in the map's binary space partition (BSP) tree. Each leaf * defines a half-space of the parent space (a node, or the whole map space). * * A leaf may be attributed to a two-dimensioned ConvexSubspace geometry. * * Each leaf is attributed to a @ref Sector in the map regardless of whether a * convex geometry exists at the leaf. * * @see http://en.wikipedia.org/wiki/Binary_space_partitioning * * @ingroup world */ class BspLeaf : public BspElement { public: /// Required subspace is missing. @ingroup errors DENG2_ERROR(MissingSubspaceError); public: /** * Construct a new BSP leaf and optionally attribute it to @a sector. * Ownership is unaffected. */ explicit BspLeaf(Sector *sector = nullptr); /** * Determines whether a subspace geometry is attributed to the BSP leaf half-space. * * @see subspace(), setSubspace() */ bool hasSubspace() const; /** * Returns the subspace attributed to the BSP leaf half-space. * * @see hasSubspace() */ ConvexSubspace &subspace() const; /** * Convenient method returning a pointer to the ConvexSubspace attributed to * the BSP leaf half-space; otherwise @c 0 if no subspace is assigned. * * @see subspace(), hasSubspace() */ inline ConvexSubspace *subspacePtr() const { return hasSubspace()? &subspace() : nullptr; } /** * Change the subspace geometry attributed to the BSP leaf. * * @param newSubspace New subspace to attribute to the BSP leaf. Ownership * is unaffected. Use @c 0 to clear the attribution. * * @see hasSubspace(), subspace() */ void setSubspace(ConvexSubspace *newSubspace); /** * Convenient method returning a pointer to the Sector attributed to the BSP * leaf half-space. * * Note that this does @em not necessarily mean there is a subspace at this * leaf. Usually one should resolve the sector from the subspace. This method * is primarily intended for legacy compatibility logics which don't care if * subspace exists at the leaf or not. */ Sector *sectorPtr(); /// @copydoc sectorPtr() Sector const *sectorPtr() const; void setSector(Sector *newSector); private: Sector *_sector; ConvexSubspace *_subspace; }; #endif // DENG_WORLD_BSPLEAF_H doomsday-stable-1.15.7/doomsday/client/include/world/impulseaccumulator.h0000664000175000017500000000505712641367670026141 0ustar jaakkojaakko/** @file impulseaccumulator.h Player impulse accumulation. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef CLIENT_PLAY_IMPULSEACCUMULATOR_H #define CLIENT_PLAY_IMPULSEACCUMULATOR_H #include /** * Receives player interaction impulses and normalizes them for later consumption * by the player Brain (on game side). * * @todo The player Brain should have ownership of it's ImpulseAccumulators. * * @ingroup playsim */ class ImpulseAccumulator { public: enum AccumulatorType { Analog, Binary }; public: /** * @param impulseId Unique identifier of the player impulse to accumulate for. * @param type Logical accumulator type. * * @param expireBeforeSharpTick If the source of the accumulation has changed * state when a sharp tick occurs, the accumulation will expire automatically. * For example, if the key bound to "attack" is not held down when a sharp tick * occurs, it should not considered active even though it has been pressed and * released since the previous sharp tick. */ ImpulseAccumulator(int impulseId, AccumulatorType type, bool expireBeforeSharpTick); /** * Returns the unique identifier of the impulse. */ int impulseId() const; AccumulatorType type() const; bool expireBeforeSharpTick() const; void setPlayerNum(int newPlayerNum); // --- void receiveBinary(); int takeBinary(); #ifdef __CLIENT__ void takeAnalog(float *pos = nullptr, float *relOffset = nullptr); void clearAll(); public: /** * Register the console commands and variables of this module. */ static void consoleRegister(); #endif private: DENG2_PRIVATE(d) }; #endif // CLIENT_PLAY_IMPULSEACCUMULATOR_H doomsday-stable-1.15.7/doomsday/client/include/world/mapobject.h0000664000175000017500000000731212641367670024163 0ustar jaakkojaakko/** @file mapobject.h Base class for all world map objects. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2013-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_WORLD_MAPOBJECT_H #define DENG_WORLD_MAPOBJECT_H #include #include class BspLeaf; namespace de { class Map; /** * Base class for all map objects. * * While logically related to MapElement, a map object is considered a dynamic * and volatile entity (whereas a map element can be largely considered static). * * The lifetime of a map object may vary massively between instances and range * from only a few milliseconds to a few hours or longer. * * @ingroup world */ class MapObject { DENG2_NO_COPY (MapObject) DENG2_NO_ASSIGN(MapObject) public: /// No map is attributed. @ingroup errors DENG2_ERROR(MissingMapError); /// Special identifier used to mark an invalid index. enum { NoIndex = -1 }; public: MapObject(de::Vector3d const &origin = de::Vector3d()); virtual ~MapObject(); DENG2_AS_IS_METHODS() /** * Returns the map BSP leaf at the origin of the object (result cached). * Naturally a map must be attributed. * * @see setMap(), hasMap() */ BspLeaf &bspLeafAtOrigin() const; /** * Returns the origin of the object in map space. * * @see move(), setOrigin(), bspLeafAtOrigin() */ de::Vector3d const &origin() const; inline de::ddouble x() const { return origin().x; } inline de::ddouble y() const { return origin().y; } inline de::ddouble z() const { return origin().z; } /** * Change the origin of the object in map space. * * @param newOrigin New absolute origin to apply, in map units. * * @see move(), origin() */ virtual void setOrigin(de::Vector3d const &newOrigin); /** * Translate the origin of the object in map space. * * @param delta Movement delta. * * @see setOrigin(), origin() */ virtual void move(de::Vector3d const &delta); /** * Returns @c true iff a map is attributed to the object. * * @see map(), setMap() */ bool hasMap() const; /** * Returns the map attributed to the object. * * @see hasMap(), setMap() */ Map &map() const; /** * Change the map attributed to the map object. * * @param newMap * * @see hasMap(), map() */ void setMap(Map *newMap); /** * Returns the "in-map" index attributed to the map object. * * @see setIndexInMap() */ de::dint indexInMap() const; /** * Change the "in-map" index attributed to the map object. * * @param newIndex New index to attribute. Use @c NoIndex to clear the * attribution (not a valid index). * * @see indexInMap() */ void setIndexInMap(de::dint newIndex = NoIndex); private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_WORLD_MAPOBJECT_H doomsday-stable-1.15.7/doomsday/client/include/world/contactspreader.h0000664000175000017500000000253512641367670025402 0ustar jaakkojaakko/** @file contactspreader.h World object => BSP leaf "contact" spreader. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifdef __CLIENT__ #ifndef DENG_CLIENT_WORLD_CONTACTSPREADER_H #define DENG_CLIENT_WORLD_CONTACTSPREADER_H #include #include #include "world/blockmap.h" namespace de { /** * Performs contact spreading for the specified @a blockmap. */ void spreadContacts(Blockmap const &blockmap, AABoxd const ®ion, QBitArray *spreadBlocks = 0); } #endif // DENG_CLIENT_WORLD_CONTACTSPREADER_H #endif // __CLIENT__ doomsday-stable-1.15.7/doomsday/client/include/world/reject.h0000664000175000017500000000556712641367670023505 0ustar jaakkojaakko/** @file reject.h World map sector LOS reject LUT building. * * @authors Copyright © 2007-2013 Daniel Swanson * @authors Copyright © 2000-2007 Andrew Apted * @authors Copyright © 1998-2000 Colin Reed * @authors Copyright © 1998-2000 Lee Killough * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_REJECT_H #define DENG_WORLD_REJECT_H #if 0 // Needs updating class Map; /** * The REJECT resource is a LUT that provides the results of trivial * line-of-sight tests between sectors. This is done with a matrix of sector * pairs i.e. if a monster in sector 4 can see the player in sector 2; the * inverse should be true. * * Note however, some PWADS have carefully constructed REJECT data to create * special effects. For example it is possible to make a player completely * invissible in certain sectors. * * The format of the table is a simple matrix of dd_bool values, a (true) * value indicates that it is impossible for mobjs in sector A to see mobjs * in sector B (and vice-versa). A (false) value indicates that a * line-of-sight MIGHT be possible and a more accurate (thus more expensive) * calculation will have to be made. * * The table itself is constructed as follows: * * X = sector num player is in * Y = sector num monster is in * * X * * 0 1 2 3 4 -> * 0 1 - 1 - - * Y 1 - - 1 - - * 2 1 1 - - 1 * 3 - - - 1 - * \|/ * * These results are read left-to-right, top-to-bottom and are packed into * bytes (each byte represents eight results). As are all lumps in WAD the * data is in little-endian order. * * Thus the size of a valid REJECT lump can be calculated as: * * ceiling(numSectors^2) * * For now we only do very basic reject processing, limited to determining * all isolated sector groups (islands that are surrounded by void space). * * @note Algorithm: * Initially all sectors are in individual groups. Next, we scan the line * list. For each 2-sectored line, merge the two sector groups into one. */ byte *BuildRejectForMap(Map const &map); #endif #endif // DENG_WORLD_REJECT_H doomsday-stable-1.15.7/doomsday/client/include/world/entitydatabase.h0000664000175000017500000000646012641367670025223 0ustar jaakkojaakko/** @file entitydatabase.h World map entity property value database. * * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_ENTITYDATABASE_H #define DENG_WORLD_ENTITYDATABASE_H #include #include #include "api_mapedit.h" // valuetype_t #include "world/entitydef.h" #include "world/propertyvalue.h" namespace de { /** * An EntityDatabase is used in the process of transferring mobj spawn spot * information and stuff like line action specials from the wad map loader * plugin via the engine, through to the game plugin. * * The primary reason for its existence is that the engine does not know about * the game specific properties of the map data types. The engine does not care * at all about the values or indeed even what properties are registered; it is * simply a way of piping information from one part of the system to another. * * @todo C++ allows making this more generic: a set/map of polymorphic objects * e.g., QVariant. */ class EntityDatabase { public: EntityDatabase(); /// @return Total number of entities by definition @a entityDef. uint entityCount(MapEntityDef const *entityDef); /** * Returns @c true iff an entity with definition @a entityDef and * @a elementIndex is known/present. */ bool hasEntity(MapEntityDef const *entityDef, int elementIndex); /** * Lookup a known entity element property value in the database. * * @param def Definition of the property to lookup an element value for. * @param elementIndex Unique element index of the value to lookup. * * @return The found PropertyValue. */ PropertyValue const &property(MapEntityPropertyDef const *def, int elementIndex); /** * Replace/add a value for a known entity element property to the database. * * @param def Definition of the property to add an element value for. * @param elementIndex Unique element index for the value. * @param value The new PropertyValue. Ownership passes to this database. */ void setProperty(MapEntityPropertyDef const *def, int elementIndex, PropertyValue *value); /// @copydoc setProperty inline void setProperty(MapEntityPropertyDef const *def, int elementIndex, valuetype_t valueType, void *valueAdr) { setProperty(def, elementIndex, BuildPropertyValue(valueType, valueAdr)); } private: DENG2_PRIVATE(d) }; } // namespace de #endif // DENG_WORLD_ENTITYDATABASE_H doomsday-stable-1.15.7/doomsday/client/include/world/convexsubspace.h0000664000175000017500000002251512641367670025251 0ustar jaakkojaakko/** @file convexsubspace.h World map convex subspace. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_CONVEXSUBSPACE_H #define DENG_WORLD_CONVEXSUBSPACE_H #include #include #include #include "Mesh" #include "MapElement" #include "Line" #include "SectorCluster" struct polyobj_s; #ifdef __CLIENT__ class Lumobj; #endif /** * On client side a convex subspace also provides / links to various geometry * data assets and properties used to visualize the subspace. * * @ingroup world */ class ConvexSubspace : public de::MapElement { public: /// An invalid polygon was specified. @ingroup errors DENG2_ERROR(InvalidPolyError); /// Required BspLeaf attribution is missing. @ingroup errors DENG2_ERROR(MissingBspLeafError); /// Required sector cluster attribution is missing. @ingroup errors DENG2_ERROR(MissingClusterError); public: /** * Attempt to construct a ConvexSubspace from the Face geometry provided. * Before the geometry is accepted it is first conformance tested to ensure * that it is a simple convex polygon. * * @param poly Polygon to construct from. Ownership is unaffected. */ static ConvexSubspace *newFromConvexPoly(de::Face &poly, BspLeaf *bspLeaf = 0); /** * Determines whether the specified @a point in the map coordinate space * lies inside the convex polygon geometry of the subspace on the XY plane. * * @param point Map space point to test. * * @return @c true iff the point lies inside the subspace geometry. * * @see http://www.alienryderflex.com/polygon/ */ bool contains(de::Vector2d const &point) const; /** * Provides access to the attributed convex geometry (a polygon). */ de::Face &poly() const; /** * Assign an additional mesh geometry to the subspace. Such @em extra meshes * are used to represent geometry which would otherwise result in a * non-manifold mesh if incorporated in the primary mesh for the map. * * @param mesh New mesh to be assigned to the subspace. Ownership of the * mesh is given to ConvexSubspace. */ void assignExtraMesh(de::Mesh &mesh); /** * Iterate through the 'extra' meshes of the subspace. * * @param func Callback to make for each Mesh. */ de::LoopResult forAllExtraMeshes(std::function func) const; /** * Returns @c true iff a SectorCluster is attributed to the subspace. The * only time a cluster might not be attributed is during initial map setup. */ bool hasCluster() const; /** * Returns the SectorCluster attributed to the subspace. * * @see hasCluster() */ SectorCluster &cluster() const; SectorCluster *clusterPtr() const; /** * Change the sector cluster attributed to the subspace. * * @param newCluster New sector cluster to attribute to the subspace. * Ownership is unaffected. Use @c nullptr to clear. * * @see hasCluster(), cluster() */ void setCluster(SectorCluster *newCluster); /** * Convenient method returning Sector of the SectorCluster attributed to the * subspace. * * @see cluster() */ inline Sector §or() const { return cluster().sector(); } /** * Returns the BspLeaf to which the subspace is assigned. */ BspLeaf &bspLeaf() const; void setBspLeaf(BspLeaf *newBspLeaf); /** * Returns the @em validCount of the subspace. Used by some legacy iteration * algorithms for marking subspaces as processed/visited. * * @todo Refactor away. */ int validCount() const; void setValidCount(int newValidCount); #ifdef __CLIENT__ /** * Returns the vector described by the offset from the map coordinate space * origin to the top most, left most point of the geometry of the subspace. * * @see aaBox() */ de::Vector2d const &worldGridOffset() const; /** * Returns a pointer to the face geometry half-edge which has been chosen for * use as the base for a triangle fan GL primitive. May return @c nullptr if no * suitable base was determined. */ de::HEdge *fanBase() const; /** * Returns the number of vertices needed for a triangle fan GL primitive. * * @note When first called after a face geometry is assigned a new 'base' * half-edge for the triangle fan primitive will be determined. * * @see fanBase() */ int fanVertexCount() const; /** * Returns the frame number of the last time mobj sprite projection was performed * for the subspace. */ int lastSpriteProjectFrame() const; void setLastSpriteProjectFrame(int newFrameNumber); public: // Audio Environment (reverb) --------------------------------------------- /** * Audio environment characteristics. */ struct AudioEnvironmentData { // Final reverb factors. typedef uint ReverbFactors[NUM_REVERB_DATA]; ReverbFactors reverb; AudioEnvironmentData() { de::zap(reverb); } }; /** * Recalculate the environmental audio characteristics (reverb) of the subspace. */ bool updateAudioEnvironment(); /** * Provides access to the final environmental audio environment characteristics * of the subspace, for efficient accumulation. */ AudioEnvironmentData const &audioEnvironmentData() const; public: // Luminous objects ------------------------------------------------------- /** * Returns the total number of Lumobjs linked to the subspace. */ int lumobjCount() const; /** * Iterate through the Lumobjs of the subspace. * * @param func Callback to make for each Lumobj. */ de::LoopResult forAllLumobjs(std::function func) const; /** * Clear all lumobj links for the subspace. */ void unlinkAllLumobjs(); /** * Unlink the specified @a lumobj in the subspace. If the lumobj is not linked * then nothing will happen. * * @param lumobj Lumobj to unlink. * * @see link() */ void unlink(Lumobj &lumobj); /** * Link the specified @a lumobj in the subspace. If the lumobj is already linked * then nothing will happen. * * @param lumobj Lumobj to link. * * @see unlink() */ void link(Lumobj &lumobj); #endif // __CLIENT__ public: // Poly objects ----------------------------------------------------------- /** * Returns the total number of Polyobjs linked to the subspace. */ int polyobjCount() const; /** * Iterate through the Polyobjs of the subspace. * * @param func Callback to make for each Polyobj. */ de::LoopResult forAllPolyobjs(std::function func) const; /** * Remove the given @a polyobj from the set of those linked to the subspace. * * @return @c true= @a polyobj was linked and subsequently removed. */ bool unlink(struct polyobj_s const &polyobj); /** * Add the given @a polyobj to the set of those linked to the subspace. Ownership * is unaffected. If the polyobj is already linked in this set then nothing will * happen. */ void link(struct polyobj_s const &polyobj); #ifdef __CLIENT__ public: // Shadowing-lines (fakeradio) -------------------------------------------- /** * Returns the total number of shadow line sides linked in the subspace. */ int shadowLineCount() const; /** * Clear the list of fake radio shadow line sides for the subspace. */ void clearShadowLines(); /** * Iterate through the set of fake radio shadow lines for the subspace. * * @param func Callback to make for each LineSide. */ de::LoopResult forAllShadowLines(std::function func) const; /** * Add the specified line @a side to the set of fake radio shadow lines for the * subspace. If the line is already present in this set then nothing will happen. * * @param side Map line side to add to the set. */ void addShadowLine(LineSide &side); #endif // __CLIENT__ private: ConvexSubspace(de::Face &convexPolygon, BspLeaf *bspLeaf = nullptr); DENG2_PRIVATE(d) }; #ifdef __CLIENT__ typedef ConvexSubspace::AudioEnvironmentData ConvexSubspaceAudioEnvironmentData; #endif #endif // DENG_WORLD_CONVEXSUBSPACE_H doomsday-stable-1.15.7/doomsday/client/include/world/bspnode.h0000664000175000017500000000724512641367670023656 0ustar jaakkojaakko/** @file bspnode.h World map BSP node. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_BSPNODE_H #define DENG_WORLD_BSPNODE_H #include "MapElement" #include "partition.h" #include #include class BspElement { public: virtual ~BspElement() {} DENG2_AS_IS_METHODS() }; /** * Represents a node in the map's binary space partition (BSP) tree. Each node * defines a partition line which divides the subspace in two, a left child and * a right child subspace. Each child may be associated with either another node, * or a @ref BspLeaf. * * @todo There is a missing abstraction here. All BSP tree elements should be * derived from a common base class, thereby enforcing which objects can be * linked into the tree. -ds * * @see http://en.wikipedia.org/wiki/Binary_space_partitioning * * @ingroup world */ class BspNode : public BspElement { DENG2_NO_COPY (BspNode) DENG2_NO_ASSIGN(BspNode) public: /// Child element identifiers: enum { Right, Left }; public: /** * Construct a new BSP node, making a copy of all arguments. * * @param partition Half-plane partition which splits the parent space * into two 'child' half-spaces. * @param rightBounds Axis-aligned bounding box for the right half-space. * @param leftBounds Axis-aligned bounding box for the left half-space. */ BspNode(de::Partition const &partition = de::Partition(), AABoxd const &rightBounds = AABoxd(), AABoxd const &leftBounds = AABoxd()); /** * Returns the space partition line at the node. */ de::Partition const &partition() const; /** * Returns the axis-aligned bounding box for the specified child, which, * encompases all the vertexes which define the geometry of that subspace * of the BSP, in map coordinate space units. */ AABoxd const &childAABox(int left) const; /** * Returns the axis-aligned bounding box for the Right child, which, * encompases all the vertexes which define the geometry of that subspace * of the BSP, in map coordinate space units. */ inline AABoxd const &rightAABox() const { return childAABox(Right); } /** * Returns the axis-aligned bounding box for the Left child, which, * encompases all the vertexes which define the geometry of that subspace * of the BSP, in map coordinate space units. */ inline AABoxd const &leftAABox() const { return childAABox(Left); } void setChildAABox(int left, AABoxd const *newAABox); inline void setRightAABox(AABoxd const *newAABox) { setChildAABox(Right, newAABox); } inline void setLeftAABox(AABoxd const *newAABox) { setChildAABox(Left, newAABox); } private: DENG2_PRIVATE(d) }; #endif // DENG_WORLD_BSPNODE_H doomsday-stable-1.15.7/doomsday/client/include/world/surface.h0000664000175000017500000002235512641367670023653 0ustar jaakkojaakko/** @file surface.h World map surface. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_SURFACE_H #define DENG_WORLD_SURFACE_H #include #include #include #include #include #include #include "MapElement" #include "Material" #ifdef __CLIENT__ class Decoration; #endif /** * Models a "boundless" but otherwise geometric map surface. Boundless in the * sense that a surface has no edges. * * @ingroup world */ class Surface : public de::MapElement { DENG2_NO_COPY (Surface) DENG2_NO_ASSIGN(Surface) public: /// Required material is missing. @ingroup errors DENG2_ERROR(MissingMaterialError); /// Notified when the @em sharp material origin changes. DENG2_DEFINE_AUDIENCE2(MaterialOriginChange, void surfaceMaterialOriginChanged(Surface &surface)) /// Notified whenever the normal vector changes. DENG2_DEFINE_AUDIENCE2(NormalChange, void surfaceNormalChanged(Surface &surface)) /// Notified whenever the opacity changes. DENG2_DEFINE_AUDIENCE2(OpacityChange, void surfaceOpacityChanged(Surface &surface)) /// Notified whenever the tint color changes. DENG2_DEFINE_AUDIENCE2(TintColorChange, void surfaceTintColorChanged(Surface §or)) /// Maximum speed for a smoothed material offset. static int const MAX_SMOOTH_MATERIAL_MOVE = 8; public: /** * Construct a new surface. * * @param owner Map element which will own the surface. * @param opacity Default opacity strength (@c 1= fully opaque). * @param tintColor Default tint color. */ Surface(de::MapElement &owner, float opacity = 1, de::Vector3f const &tintColor = de::Vector3f(1, 1, 1)); /** * Returns the normalized tangent space matrix for the surface. * (col0: tangent, col1: bitangent, col2: normal) */ de::Matrix3f const &tangentMatrix() const; inline de::Vector3f tangent() const { return tangentMatrix().column(0); } inline de::Vector3f bitangent() const { return tangentMatrix().column(1); } inline de::Vector3f normal() const { return tangentMatrix().column(2); } /** * Change the tangent space normal vector for the surface. If changed, the * tangent vectors will be recalculated next time they are needed. The * NormalChange audience is notified whenever the normal changes. * * @param newNormal New normal vector (will be normalized if needed). */ Surface &setNormal(de::Vector3f const &newNormal); /** * Returns the opacity of the surface. The OpacityChange audience is notified * whenever the opacity changes. * * @see setOpacity() */ float opacity() const; Surface &setOpacity(float newOpacity); /** * Returns the tint color of the surface. The TintColorChange audience is * notified whenever the tint color changes. * * @see setTintColor() */ de::Vector3f const &tintColor() const; Surface &setTintColor(de::Vector3f const &newTintColor); /** * Returns the blendmode for the surface. */ blendmode_t blendMode() const; Surface &setBlendMode(blendmode_t newBlendMode); #ifdef __CLIENT__ /** * Determine the glow properties of the surface, which, are derived from the * bound material (averaged color). * * @param color Amplified glow color is written here. * * @return Glow strength/intensity or @c 0 if not presently glowing. */ float glow(de::Vector3f &color) const; public: // Decorations ------------------------------------------------------------ /** * Clear all surface decorations. */ void clearDecorations(); /** * Returns the total number of surface decorations. */ int decorationCount() const; /** * Add the specified decoration to the surface. * * @param decoration Decoration to add. Ownership is given to the surface. */ void addDecoration(Decoration *decoration); /** * Iterate through all the surface decorations. */ de::LoopResult forAllDecorations(std::function func) const; /** * Mark the surface as needing a decoration update. */ void markForDecorationUpdate(bool yes = true); /** * Returns @c true if the surface is marked for decoration update. */ bool needsDecorationUpdate() const; #endif // __CLIENT__ public: // Material --------------------------------------------------------------- /** * Returns @c true iff a material is bound to the surface. */ bool hasMaterial() const; /** * Returns @c true iff a @em fix material is bound to the surface, which was * chosen automatically where one was missing. Clients should not be notified * when a fix material is bound to the surface (as they should perform their * fixing, locally). However, if the fix material is later replaced with a * "normally-bound" material, clients should be notified as per usual. */ bool hasFixMaterial() const; /** * Convenient helper method for determining whether a sky-masked material * is bound to the surface. * * @return @c true iff a sky-masked material is bound. */ inline bool hasSkyMaskedMaterial() const { return hasMaterial() && material().isSkyMasked(); } /** * Convenient helper method for determining whether a drawable, non @em fix * material is bound to the surface. * * @return @c true iff drawable, non @em fix masked material is bound. * * @see hasMaterial(), hasFixMaterial(), Material::isDrawable() */ inline bool hasDrawableNonFixMaterial() const { return hasMaterial() && !hasFixMaterial() && material().isDrawable(); } /** * Returns the attributed material of the surface. * * @see hasMaterial(), hasFixMaterial() */ Material &material() const; Material *materialPtr() const; /** * Change the material attributed to the surface. On client side, any existing * decorations are cleared whenever the material changes and the surface is * marked for redecoration. * * @param newMaterial New material to apply. Use @c nullptr to clear. * @param isMissingFix @c true= this is a fix for a "missing" material. */ Surface &setMaterial(Material *newMaterial, bool isMissingFix = false); /** * Returns @c true if the surface material is mirrored on the X axis. */ bool materialMirrorX() const; /** * Returns @c true if the surface material is mirrored on the Y axis. */ bool materialMirrorY() const; /** * Returns the material origin offset for the surface. */ de::Vector2f const &materialOrigin() const; Surface &setMaterialOrigin(de::Vector2f const &newOrigin); /** * Returns the material scale factors for the surface. */ de::Vector2f materialScale() const; /** * Compose a URI for the surface's material. If no material is bound then a * default (i.e., empty) URI is returned. * * @see hasMaterial(), MaterialManifest::composeUri() */ de::Uri composeMaterialUri() const; #ifdef __CLIENT__ public: // Material origin animation/smoothing ------------------------------------ /** * Returns the current smoothed (interpolated) material origin for the * surface in the map coordinate space. * * @see setMaterialOrigin() */ de::Vector2f const &materialOriginSmoothed() const; /** * Returns the delta between current and the smoothed material origin for * the surface in the map coordinate space. * * @see setMaterialOrigin(), smoothMaterialOrigin() */ de::Vector2f const &materialOriginSmoothedAsDelta() const; /** * Perform smoothed material origin interpolation. * * @see materialOriginSmoothed() */ void lerpSmoothedMaterialOrigin(); /** * Reset the surface's material origin tracking. * * @see materialOriginSmoothed() */ void resetSmoothedMaterialOrigin(); /** * Roll the surface's material origin tracking buffer. */ void updateMaterialOriginTracking(); #endif // __CLIENT__ protected: int property(DmuArgs &args) const; int setProperty(DmuArgs const &args); private: DENG2_PRIVATE(d) }; #endif // DENG_WORLD_SURFACE_H doomsday-stable-1.15.7/doomsday/client/include/world/worldsystem.h0000664000175000017500000001240612641367670024613 0ustar jaakkojaakko/** @file worldsystem.h World subsystem. * * Ideas for improvement: * * "background loading" - it would be very cool if map loading happened in * another thread. This way we could be keeping busy while players watch the * intermission animations. * * "seamless world" - multiple concurrent maps with no perceivable delay when * players move between them. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLDSYSTEM_H #define DENG_WORLDSYSTEM_H #include #include #include #include #include #include #ifdef __CLIENT__ # include "render/skydrawable.h" #endif #ifdef __CLIENT__ class Hand; #endif namespace de { class Map; /** * @ingroup world */ class WorldSystem : public de::System { public: /// No map is currently loaded. @ingroup errors DENG2_ERROR(MapError); /// Notified whenever the "current" map changes. DENG2_DEFINE_AUDIENCE2(MapChange, void worldSystemMapChanged()) #ifdef __CLIENT__ /// Notified when a new frame begins. DENG2_DEFINE_AUDIENCE2(FrameBegin, void worldSystemFrameBegins(bool resetNextViewer)) /// Notified when the "current" frame ends. DENG2_DEFINE_AUDIENCE2(FrameEnd, void worldSystemFrameEnds()) #endif public: /** * Construct a new world system (no map is loaded by default). */ WorldSystem(); // System. void timeChanged(de::Clock const &); /** * To be called to reset the world back to the initial state. Any currently * loaded map will be unloaded and player states are re-initialized. * * @todo World should observe GameChange. */ void reset(); /** * To be called following an engine reset to update the world state. */ void update(); /** * Returns @c true if a map is currently loaded. */ bool hasMap() const; /** * Provides access to the currently loaded map. * * @see hasMap() */ Map &map() const; /** * Returns a pointer to the currently loaded map, if any. */ inline Map *mapPtr() const { return hasMap()? &map() : nullptr; } /** * @param uri Universal resource identifier (URI) for the map to change to. * If an empty URI is specified the current map will be unloaded. * * @return @c true= the map change completed successfully. */ bool changeMap(Uri const &uri); /** * Unload the currently loaded map (if any). * * @see changeMap() */ inline void unloadMap() { changeMap(Uri()); } /** * Returns the effective map-info definition Record associated with the given * @a mapUri (which may be the default definition, if invalid/unknown). * * @param mapUri Unique identifier for the map to lookup map-info data for. */ Record const &mapInfoForMapUri(Uri const &mapUri) const; /** * Advance time in the world. * * @param delta Time delta to apply. */ void advanceTime(timespan_t delta); /** * Returns the current world time. */ timespan_t time() const; void tick(timespan_t elapsed); #ifdef __CLIENT__ /** * To be called at the beginning of a render frame, so that we can prepare for * drawing view(s) of the current map. */ void beginFrame(bool resetNextViewer = false); /** * To be called at the end of a render frame, so that we can finish up any tasks * that must be completed after view(s) have been drawn. */ void endFrame(); SkyDrawable::Animator &skyAnimator() const; /** * Returns the hand of the "user" in the world. Used for manipulating elements * for the purposes of runtime map editing. * * @param distance The current distance of the hand from the viewer will be * written here if not @c 0. */ Hand &hand(coord_t *distance = nullptr) const; /** * Determines if a point is in the void. * * @param pos Point. * * @return @c true, if the point is outside any of the world's maps. */ bool isPointInVoid(de::Vector3d const &pos) const; #endif // __CLIENT__ public: /** * To be called to register the commands and variables of this module. */ static void consoleRegister(); private: DENG2_PRIVATE(d) }; } // namespace de DENG_EXTERN_C dd_bool ddMapSetup; DENG_EXTERN_C int validCount; #endif // DENG_WORLD_H doomsday-stable-1.15.7/doomsday/client/include/world/propertyvalue.h0000664000175000017500000001306112641367670025136 0ustar jaakkojaakko/** @file propertyvalue.h Data types for representing world map property values. * * @ingroup data * * Data type class hierarchy with integral RTTI mechanism and basic in-place * value/type conversions. * * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_DATA_PROPERTYVALUE_H #define DENG_DATA_PROPERTYVALUE_H #include "de_base.h" class PropertyValue { public: virtual ~PropertyValue() {} virtual valuetype_t type() const = 0; virtual char const* typeName() const = 0; virtual byte asByte() const = 0; virtual int16_t asInt16() const = 0; virtual int32_t asInt32() const = 0; virtual fixed_t asFixed() const = 0; virtual angle_t asAngle() const = 0; virtual float asFloat() const = 0; }; class PropertyByteValue : public PropertyValue { public: PropertyByteValue(byte value) : PropertyValue(), value_(value) {} valuetype_t type() const { return DDVT_BYTE; } char const* typeName() const { return "byte"; } byte value() const { return value_; } byte asByte() const { return value_; } int16_t asInt16() const { return value_; } int32_t asInt32() const { return value_; } fixed_t asFixed() const { return value_ << FRACBITS; } angle_t asAngle() const { return value_; } float asFloat() const { return value_; } private: byte value_; }; class PropertyInt16Value : public PropertyValue { public: PropertyInt16Value(int16_t value) : PropertyValue(), value_(value) {} valuetype_t type() const { return DDVT_SHORT; } char const* typeName() const { return "int16"; } int16_t value() const { return value_; } byte asByte() const { return value_; } int16_t asInt16() const { return value_; } int32_t asInt32() const { return value_; } fixed_t asFixed() const { return value_ << FRACBITS; } angle_t asAngle() const { return value_; } float asFloat() const { return value_; } private: int16_t value_; }; class PropertyInt32Value : public PropertyValue { public: PropertyInt32Value(int32_t value) : PropertyValue(), value_(value) {} valuetype_t type() const { return DDVT_INT; } char const* typeName() const { return "int32"; } int32_t value() const { return value_; } byte asByte() const { return value_; } int16_t asInt16() const { return value_; } int32_t asInt32() const { return value_; } fixed_t asFixed() const { return value_ << FRACBITS; } angle_t asAngle() const { return value_; } float asFloat() const { return value_; } private: int32_t value_; }; class PropertyFixedValue : public PropertyValue { public: PropertyFixedValue(fixed_t value) : PropertyValue(), value_(value) {} valuetype_t type() const { return DDVT_FIXED; } char const* typeName() const { return "fixed"; } fixed_t value() const { return value_; } byte asByte() const { return value_ >> FRACBITS; } int16_t asInt16() const { return value_ >> FRACBITS; } int32_t asInt32() const { return value_ >> FRACBITS; } fixed_t asFixed() const { return value_; } angle_t asAngle() const { return value_ >> FRACBITS; } float asFloat() const { return FIX2FLT(value_); } private: fixed_t value_; }; class PropertyAngleValue : public PropertyValue { public: PropertyAngleValue(angle_t value) : PropertyValue(), value_(value) {} valuetype_t type() const { return DDVT_ANGLE; } char const* typeName() const { return "angle"; } angle_t value() const { return value_; } byte asByte() const { return value_; } int16_t asInt16() const { return value_; } int32_t asInt32() const { return value_; } fixed_t asFixed() const { return value_ << FRACBITS; } angle_t asAngle() const { return value_; } float asFloat() const { return value_; } private: angle_t value_; }; class PropertyFloatValue : public PropertyValue { public: PropertyFloatValue(float value) : PropertyValue(), value_(value) {} valuetype_t type() const { return DDVT_FLOAT; } char const* typeName() const { return "float"; } float value() const { return value_; } byte asByte() const { return value_; } int16_t asInt16() const { return value_; } int32_t asInt32() const { return value_; } fixed_t asFixed() const { return FLT2FIX(value_); } angle_t asAngle() const { return value_; } float asFloat() const { return value_; } private: float value_; }; /** * Factory constructor for instantiation of new PropertyValues. * * @param type DDVT_* value type identifier for the value pointed at by @a valueAdr. * @param valueAdr Address of the value to be read into the new property value. * * @return Newly constructed PropertyValue-derived instance. */ PropertyValue* BuildPropertyValue(valuetype_t type, void* valueAdr); #endif // DENG_DATA_PROPERTYVALUE_H doomsday-stable-1.15.7/doomsday/client/include/world/lineowner.h0000664000175000017500000000737312641367670024230 0ustar jaakkojaakko/** @file lineowner.h World map line owner. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_LINEOWNER_H #define DENG_WORLD_LINEOWNER_H #include #include class Line; /** * @ingroup world * * @deprecated Will be replaced with half-edge ring iterator/rover. -ds */ class LineOwner { public: /// @todo Make private: Line *_line; /// {Anitclockwise, Clockwise} LineOwner *_link[2]; /// Angle between this and the next line owner, clockwise. binangle_t _angle; struct ShadowVert { de::Vector2d inner; de::Vector2d extended; } _shadowOffsets; public: /*LineOwner() : _line(0), _angle(0) { _link[Previous] = 0; _link[Next] = 0; }*/ /** * Returns @c true iff the previous line owner in the ring (anticlockwise) * is not the same as this LineOwner. * * @see prev() */ inline bool hasPrev() const { return &prev() != this; } /** * Returns @c true iff the next line owner in the ring (clockwise) is not * the same as this LineOwner. * * @see next() */ inline bool hasNext() const { return &next() != this; } /** * Navigate to the adjacent line owner in the ring (if any). Note this may * be the same LineOwner. */ LineOwner &navigate(de::ClockDirection dir = de::Anticlockwise) { return *_link[dir]; } /// @copydoc navigate() LineOwner const &navigate(de::ClockDirection dir = de::Anticlockwise) const { return *_link[dir]; } /** * Returns the previous line owner in the ring (anticlockwise). Note that * this may be the same LineOwner. * * @see hasPrev() */ inline LineOwner &prev() { return navigate(de::Anticlockwise); } /// @copydoc prev() inline LineOwner const &prev() const { return navigate(de::Anticlockwise); } /** * Returns the next line owner in the ring (clockwise). Note that this may * be the same LineOwner. * * @see hasNext() */ inline LineOwner &next() { return navigate(de::Clockwise); } /// @copydoc next() inline LineOwner const &next() const { return navigate(de::Clockwise); } inline LineOwner *prevPtr() { return _link[de::Anticlockwise]; } inline LineOwner *nextPtr() { return _link[de::Clockwise]; } /** * Returns the line "owner". */ Line &line() const { return *_line; } /** * Returns the angle between the line owner and the next in the ring (clockwise). */ binangle_t angle() const { return _angle; } /** * Returns the inner shadow offset of the line owner. */ de::Vector2d const &innerShadowOffset() const { return _shadowOffsets.inner; } /** * Returns the extended shadow offset of the line owner. */ de::Vector2d const &extendedShadowOffset() const { return _shadowOffsets.extended; } }; #endif // DENG_WORLD_LINEOWNER_H doomsday-stable-1.15.7/doomsday/client/include/world/clientmobjthinkerdata.h0000664000175000017500000000701712641367670026566 0ustar jaakkojaakko/** @file clientmobjthinkerdata.h Private client-side data for mobjs. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_WORLD_CLIENTMOBJTHINKERDATA_H #define DENG_WORLD_CLIENTMOBJTHINKERDATA_H #include #include #include /** * @defgroup clMobjFlags Client Mobj Flags * @ingroup flags */ ///@{ #define CLMF_HIDDEN 0x01 ///< Not officially created yet #define CLMF_UNPREDICTABLE 0x02 ///< Temporarily hidden (until next delta) #define CLMF_SOUND 0x04 ///< Sound is queued for playing on unhide. #define CLMF_NULLED 0x08 ///< Once nulled, it can't be updated. #define CLMF_STICK_FLOOR 0x10 ///< Mobj will stick to the floor. #define CLMF_STICK_CEILING 0x20 ///< Mobj will stick to the ceiling. #define CLMF_LOCAL_ACTIONS 0x40 ///< Allow local action execution. ///@} // Clmobj knowledge flags. This keeps track of the information that has been // received. #define CLMF_KNOWN_X 0x10000 #define CLMF_KNOWN_Y 0x20000 #define CLMF_KNOWN_Z 0x40000 #define CLMF_KNOWN_STATE 0x80000 #define CLMF_KNOWN 0xf0000 ///< combination of all the KNOWN-flags // Magic number for client mobj information. //#define CLM_MAGIC1 0xdecafed1 //#define CLM_MAGIC2 0xcafedeb8 /** * Private client-side data for mobjs. This includes any per-object state for rendering * and client-side network state. * * @todo Lumobj objects (light source interfaces) should be moved into this class, so * that they can exist across frames. -jk */ class ClientMobjThinkerData : public MobjThinkerData { public: struct RemoteSync { int flags; uint time; ///< Time of last update. int sound; ///< Queued sound ID. float volume; ///< Volume for queued sound. RemoteSync() : flags(0) , time(Timer_RealMilliseconds()) , sound(0) , volume(0) {} }; public: ClientMobjThinkerData(); ClientMobjThinkerData(ClientMobjThinkerData const &other); void think(); IData *duplicate() const; void stateChanged(state_t const *previousState); int stateIndex() const; bool hasRemoteSync() const; /** * Returns the network state of the mobj. This state is not allocated until this is * called for the first time. */ RemoteSync &remoteSync(); /** * If the object is represented by a model, returns the current state of the * object's animation. * * @return Animation state, or @c NULL if not drawn as a model. */ de::ModelDrawable::Animator *animator(); de::ModelDrawable::Animator const *animator() const; de::Matrix4f const &modelTransformation() const; private: DENG2_PRIVATE(d) }; #endif // DENG_WORLD_CLIENTMOBJTHINKERDATA_H doomsday-stable-1.15.7/doomsday/client/include/world/p_players.h0000664000175000017500000000700212641367670024211 0ustar jaakkojaakko/** @file p_players.h World player entities. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_WORLD_P_PLAYERS_H #define DENG_WORLD_P_PLAYERS_H #include "api_player.h" #include /** * Describes a player interaction impulse. * * @ingroup playsim */ struct PlayerImpulse { int id = 0; impulsetype_t type = IT_ANALOG; de::String name; ///< Symbolic. Used when resolving or generating textual binding descriptors. de::String bindContextName; ///< Symbolic name of the associated binding context. }; struct player_t { byte extraLightCounter; // Num tics to go till extraLight is disabled. int extraLight; int targetExtraLight; ddplayer_t shared; // The public player data. }; extern player_t *viewPlayer; extern player_t ddPlayers[DDMAXPLAYERS]; extern int consolePlayer; extern int displayPlayer; /** * Determine which console is used by the given local player. Local players * are numbered starting from zero. */ int P_LocalToConsole(int localPlayer); /** * Determine the local player number used by a particular console. Local * players are numbered starting from zero. * * @param playerNum Console number. * * @return Local player number. Returns -1 if @a playerNum does not correspond * to any local player. */ int P_ConsoleToLocal(int playerNum); /** * Given a ptr to ddplayer_t, return it's logical index. */ int P_GetDDPlayerIdx(ddplayer_t *ddpl); /** * Do we THINK the given (camera) player is currently in the void. * The method used to test this is to compare the position of the mobj * each time it is linked into a BSP leaf. * * @note Cannot be 100% accurate so best not to use it for anything critical... * * @param player The player to test. * * @return @c true if the player is thought to be in the void. */ bool P_IsInVoid(player_t *player); short P_LookDirToShort(float lookDir); float P_ShortToLookDir(short s); /** * Remove all the player impulse definitions and destroy the associated accumulators * of all players. */ void P_ClearPlayerImpulses(); /** * Lookup a player impulse defintion by it's unique @a id. * * @param id Unique identifier of the player impulse definition to lookup. * * @return The associated PlayerImpulse if found; otherwise @c nullptr. */ PlayerImpulse *P_PlayerImpulsePtr(int id); /** * Lookup a player impulse defintion by it's symbolic @a name. * * @param name Symbolic name of the player impulse definition to lookup. * * @return The associated PlayerImpulse if found; otherwise @c nullptr. */ PlayerImpulse *P_PlayerImpulseByName(de::String const &name); /** * Register the console commands and variables of this module. */ void P_ConsoleRegister(); #endif // DENG_WORLD_P_PLAYERS_H doomsday-stable-1.15.7/doomsday/client/include/world/grabbable.h0000664000175000017500000001436412641367670024125 0ustar jaakkojaakko/** @file grabbable.h Grabbable. * * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_GRABBABLE_H #define DENG_WORLD_GRABBABLE_H #include #include #include /** * Abstract base for any class whose instances can be manipulated and/or moved * by proxy once "grabbed". Conceptually a grabbable is similar to a reference * counter combined with an API which imposes additional restrictions to the * behavior/semantics of interactions with the derived instance(s). * * All grabbables have a map coordinate space origin. Each instance may be put * into a "locked" state where this origin is considered immutable. It should * be noted that this lock is a @em logical concept which is only enforced by * this interface (it may still be moved by some other means provided either * the OriginChange audience is notified, or the old origin is respected). */ class Grabbable { DENG2_NO_COPY (Grabbable) DENG2_NO_ASSIGN(Grabbable) public: /// Base class for all grab errors. @ingroup errors DENG2_ERROR(GrabError); /// Base class for all ungrab errors. @ingroup errors DENG2_ERROR(UngrabError); /// Base class for all lock errors. @ingroup errors DENG2_ERROR(LockError); /// Base class for all unlock errors. @ingroup errors DENG2_ERROR(UnlockError); /// Notified when the grabbable is about to be deleted. DENG2_DEFINE_AUDIENCE(Deletion, void grabbableBeingDeleted(Grabbable &grabbable)) /// Notified whenever the lock state of the grabbable changes. DENG2_DEFINE_AUDIENCE(LockChange, void grabbableLockChanged(Grabbable &grabbable)) /// Notified whenever the origin of the grabbable changes. DENG2_DEFINE_AUDIENCE(OriginChange, void grabbableOriginChanged(Grabbable &grabbable)) public: /** * Construct a new grabbable (initial state is ungrabbed and unlocked). */ Grabbable(); virtual ~Grabbable(); DENG2_AS_IS_METHODS() /** * Returns @c true iff the grabbable is currently grabbed. */ bool isGrabbed() const; /** * Attempt to grab the grabbable (ownership is unaffected). The default * implementation assumes no preconditions therefore the grab succeeds. * * Derived classes may overide this for specialized grab behavior. If the * grab succeeds the implementor should call @ref addGrab() otherwise; * throw a new GrabError. */ virtual void grab(); /** * Attempt to ungrab the grabbable (ownership is unaffected). The default * implementation assumes no preconditions therefore the ungrab succeeds. * * Derived classes may overide this for specialized ungrab behavior. If the * ungrab succeeds the implementor should call @ref decGrab() otherwise; * throw a new UngrabError. */ virtual void ungrab(); /** * Returns @c true iff the grabbable is currently locked. The LockChange * audience is notified whenever the lock state changes. * * @see setLock() */ bool isLocked() const; /** * Lock the grabbable if unlocked (preventing it from being moved). The * default implementation assumes no further preconditions and therefore * the lock succeeds. * * Derived classes may overide this for specialized lock behavior. If the * lock succeeds the implementor should use @code setLock(true) @endcode to * enable the lock otherwise; throw a new LockError. * * @see isLocked(), unlock() */ virtual void lock() { setLock(true); } /** * Unlock the grabbable if locked (allowing it to be moved). The default * implementation assumes no further preconditions and therefore the unlock * succeeds. * * Derived classes may overide this for specialized unlock behavior. If the * unlock succeeds the implementor should use @code setLock(false) @endcode * to disable the lock otherwise; throw a new UnlockError. * * @see lock(), unlock() */ virtual void unlock() { setLock(false); } /** * Attempt to move the grabbable. Note that the move will be denied if the * grabbable is currently locked (nothing will happen). */ void move(de::Vector3d const &newOrigin); /** * Returns the origin of the grabbable in the map coordinate space. The * OriginChange audience must be notified whenever the origin changes. * * @see setOrigin() */ virtual de::Vector3d const &origin() const = 0; /** * Change the origin of the grabbable in the map coordinate space. The * OriginChange audience must be notified whenever the origin changes. * The default implementation assumes the grabbable cannot be moved and * does nothing. * * @param newOrigin New origin coordinates to apply. * * @see origin() */ virtual void setOrigin(de::Vector3d const & /*newOrigin*/) {} protected: /** * Increment the grab count. Derived classes must call this when a grab * attempt is deemed to succeed. */ void addGrab(); /** * Decrement the grab count. Derived classes must call this when an ungrab * attempt is deemed to succeed. */ void decGrab(); /** * Change the lock state of the grabbable. Derived classes use this to * manipulate the lock state. Repeat attempts to enable/disable the lock * are ignored. */ void setLock(bool enable = true); private: int _grabs; bool _locked; }; #endif // DENG_WORLD_GRABBABLE_H doomsday-stable-1.15.7/doomsday/client/include/world/dmuargs.h0000664000175000017500000000420312641367670023655 0ustar jaakkojaakko/** @file dmuargs.h Doomsday Map Update (DMU) API arguments. * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_DMUARGS_H #define DENG_WORLD_DMUARGS_H #include "api_mapedit.h" // valuetype_t /** * Encapsulates the arguments used when routing DMU API calls to map elements. * * @ingroup world */ class DmuArgs { public: /// @todo make private int type; uint prop; int modifiers; /// Property modifiers (e.g., line of sector) valuetype_t valueType; dd_bool *booleanValues; byte *byteValues; int *intValues; fixed_t *fixedValues; float *floatValues; double *doubleValues; angle_t *angleValues; void **ptrValues; DmuArgs(int type, uint prop); /** * Read the value of an argument. Does some basic type checking so that * incompatible types are not assigned. Simple conversions are also done, * e.g., float to fixed. */ void value(valuetype_t valueType, void *dst, uint index) const; /** * Change the value of an argument. Does some basic type checking so that * incompatible types are not assigned. Simple conversions are also done, * e.g., float to fixed. */ void setValue(valuetype_t valueType, void const *src, uint index); }; #endif // DENG_WORLD_DMUARGS_H doomsday-stable-1.15.7/doomsday/client/include/world/polyobj.h0000664000175000017500000001512612641367670023677 0ustar jaakkojaakko/** @file polyobj.h World map polyobj. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_POLYOBJ_H #define DENG_WORLD_POLYOBJ_H #include "dd_share.h" #include "Mesh" #include #include class BspLeaf; class Line; class Vertex; class PolyobjData; /// Storage needed for a polyobj_s instance, plus the user data section (if any). #define POLYOBJ_SIZE gx.polyobjSize /** * World polyobj. Moveable Polygonal Map-Object (Polyobj). * * @ingroup world */ typedef struct polyobj_s { public: /// The polyobj is not presently linked in the BSP. @ingroup errors DENG2_ERROR(NotLinkedError); typedef QList Lines; typedef QList Vertexes; static void setCollisionCallback(void (*func) (struct mobj_s *mobj, void *line, void *polyobj)); public: DD_BASE_POLYOBJ_ELEMENTS() public: polyobj_s(de::Vector2d const &origin = de::Vector2d()); /// @note: Does nothing about the user data section. ~polyobj_s(); PolyobjData &data(); PolyobjData const &data() const; /** * Returns the map in which the polyobj exists. */ de::Map &map() const; /** * Provides access to the mesh owned by the polyobj. */ de::Mesh &mesh() const; /** * Returns @c true if the polyobj is presently linked in the owning Map. */ bool isLinked(); /** * (Re)link the polyobj in the owning Map. Ownership is not affected. To be * called @em after rotation and/or translation to re-link the polyobj and * thereby complete the process. * * Linking only occurs if the polyobj is not presently linked (subsequent * calls are ignored). */ void link(); /** * Unlink the polyobj in the owning Map. To be called @em before attempting * to rotate and/or translate the polyobj to initiate the process. * * Unlinking only occurs if the polyobj is presently linked (subsequent * calls are ignored). */ void unlink(); /** * Returns @c true iff a BspLeaf is linked to the polyobj. */ bool hasBspLeaf() const; /** * Returns the BSP leaf in which the polyobj is presently linked. * * @see isLinked(); */ BspLeaf &bspLeaf() const; /** * Convenience accessor which determines whether a BspLeaf with an attributed * sector is linked to the polyobj. * * @see hasBspLeaf(), BspLeaf::hasSector() */ bool hasSector() const; /** * Convenience accessor which returns the Sector of the BspLeaf linked to the * polyobj. * * @see bspLeaf(), BspLeaf::sector() */ Sector §or() const; /** * Convenience accessor which returns a pointer to the Sector of the BspLeaf * linked to the polyobj. * * @return Sector attributed to the linked BspLeaf; otherwise @c 0. * * @see hasBspLeaf(), BspLeaf::sectorPtr() */ Sector *sectorPtr() const; /** * Returns the sound emitter for the polyobj. */ SoundEmitter &soundEmitter(); /// @copydoc soundEmitter() SoundEmitter const &soundEmitter() const; /** * Provides access to the list of Lines for the polyobj. */ Lines const &lines() const; /** * Returns the total number of Lines for the polyobj. */ inline uint lineCount() const { return lines().count(); } /** * To be called once all Lines have been added in order to compile the list * of unique vertexes for the polyobj. A vertex referenced by multiple lines * is only included once in this list. */ void buildUniqueVertexes(); /** * Provides access to the list of unique vertexes for the polyobj. * * @see buildUniqueVertex() */ Vertexes const &uniqueVertexes() const; /** * Returns the total number of unique Vertexes for the polyobj. * * @see buildUniqueVertexes() */ inline uint uniqueVertexCount() const { return uniqueVertexes().count(); } /** * Update the original coordinates of all vertexes using the current coordinate * values. To be called once initialization has completed to finalize the polyobj. * * @pre Unique vertex list has already been built. * * @see buildUniqueVertexes() */ void updateOriginalVertexCoords(); /** * Translate the origin of the polyobj in the map coordinate space. * * @param delta Movement delta on the X|Y plane. */ bool move(de::Vector2d const &delta); /** * @overload */ inline bool move(coord_t x, coord_t y) { return move(de::Vector2d(x, y)); } /** * Rotate the angle of the polyobj in the map coordinate space. * * @param angle World angle delta. */ bool rotate(angle_t angle); /** * Update the axis-aligned bounding box for the polyobj (map coordinate * space) to encompass the points defined by it's vertices. * * @todo Should be private. */ void updateAABox(); /** * Update the tangent space vectors for all surfaces of the polyobj, * according to the points defined by the relevant Line's vertices. */ void updateSurfaceTangents(); /** * Change the tag associated with the polyobj. * * @param newTag New tag. */ void setTag(int newTag); /** * Change the associated sequence type of the polyobj. * * @param newType New sequence type. */ void setSequenceType(int newType); /** * Returns the original index of the polyobj. */ int indexInMap() const; /** * Change the original index of the polyobj. * * @param newIndex New original index. */ void setIndexInMap(int newIndex); } Polyobj; #endif // DENG_WORLD_POLYOBJ_H doomsday-stable-1.15.7/doomsday/client/include/world/hand.h0000664000175000017500000001177712641367670023143 0ustar jaakkojaakko/** @file hand.h Hand (metaphor) for the manipulation of "grabbables". * * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_HAND_H #define DENG_WORLD_HAND_H #include "Grabbable" #include "world/worldsystem.h" #include #include #include namespace de { namespace internal { template inline bool cannotCastGrabbableTo(Grabbable *gabbable) { return dynamic_cast(gabbable) == NULL; } } } /** * Represents the "hand" of the user in the client-side world. Facilitates the * manipulation of so-called "grabbables" for the purposes of runtime editing. * * As one might derive from the name, the hand is a metaphor for the will of * the user. Although the hand has a presence in the world it should not however * be considered a map element (such as a mobj). * * @ingroup world */ class Hand : DENG2_OBSERVES(de::WorldSystem, FrameEnd) { DENG2_NO_COPY (Hand) DENG2_NO_ASSIGN(Hand) public: /// Notified whenever a grabbable is grabbed. DENG2_DEFINE_AUDIENCE(Grabbed, void handGrabbed(Hand &hand, Grabbable &grabbable)) /// Notified whenever a grabbable is ungrabbed. DENG2_DEFINE_AUDIENCE(Ungrabbed, void handUngrabbed(Hand &hand, Grabbable &grabbable)) typedef QList Grab; public: /** * Construct a new hand. */ Hand(de::Vector3d const &origin = de::Vector3d()); /** * Returns the origin of the hand in the map coordinate space. * * @see setOrigin() */ de::Vector3d const &origin() const; /** * Change the origin of the hand in the map coordinate space. * * @param newOrigin New origin coordinates to apply. * * @see origin() */ void setOrigin(de::Vector3d const &newOrigin); /** * Returns @c true off the hand is empty (i.e., nothing grabbed). * * @see grabbed() */ bool isEmpty() const; /** * Returns @c true iff the hand has grabbed the specified @a grabbable. * If you only need to know if the grabbable has been grabbed or not (rather * than by whom) then @ref Grabbable::isGrabbed() should be used instead as * this is faster. * * @see grabbed(), grab(), ungrab() */ bool hasGrabbed(Grabbable const &grabbable) const; /** * Grab the specified @a grabbable, releasing the current grab. If already * grabbed then nothing will happen. * * @see grabMulti(), ungrab(), hasGrabbed() */ void grab(Grabbable &grabbable); /** * Extend the grab by appending the specified @a grabbable to the LIFO stack * of grabbables maintained by the hand (the order in which grabbables are * grabbed is remembered by the hand). * * @see grab(), hasGrabbed(), grabbed() */ void grabMulti(Grabbable &grabbable); /** * Release the specified @a grabbable if grabbed by the hand. If not grabbed * then nothing will happen. * * @see grab(), grabMulti(), grabbed() */ void ungrab(Grabbable &grabbable); /** * Release anything currently grabbed by the hand. The grabbables are released * in reverse order (modelled as a LIFO stack). * * @see grab(), grabMulti(), grabbed() */ void ungrab(); /** * Provides access to the grab list of everything currently held by the hand. * * @see grabbedCount() */ Grab const &grabbed() const; /** * Convenient method of returning the total number of grabbed elements. * * @see grabbed() */ inline int grabbedCount() const { return grabbed().count(); } /** * Provides the averaged origin coordinates (in the map coordinate space) of * everything currently grabbed by the hand. If nothing is grabbed then a * default (0, 0, 0) vector is returned. * * @see grabbed() */ de::Vector3d const &grabbedOrigin() const; float editIntensity() const; de::Vector3f const &editColor() const; void setEditIntensity(float newIntensity); void setEditColor(de::Vector3f const &newColor); #ifdef __CLIENT__ protected: /// Observes WorldSystem FrameEnd void worldSystemFrameEnds(); #endif private: DENG2_PRIVATE(d) }; #endif // DENG_WORLD_HAND_H doomsday-stable-1.15.7/doomsday/client/include/world/thinkers.h0000664000175000017500000000767512641367670024062 0ustar jaakkojaakko/** @file thinkers.h World map thinkers. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_WORLD_THINKERS_H #define DENG_WORLD_THINKERS_H #include #include #include "api_thinker.h" namespace de { /** * World map thinker lists / collection. * * @ingroup world * * @todo Replace with new mechanism from the old 'new-order' branch(es). */ class Thinkers { public: Thinkers(); /** * Returns @c true iff the thinker lists been initialized. */ bool isInited() const; /** * Init the thinker lists. * * @param flags @c 0x1 = Init public thinkers. * @c 0x2 = Init private (engine-internal) thinkers. */ void initLists(dbyte flags); /** * @param thinker Thinker to be added. * @param makePublic @c true = @a thinker will be visible publically * via the Doomsday public API thinker interface(s). */ void add(thinker_t &thinker, bool makePublic = true); /** * Deallocation is lazy -- it will not actually be freed until its * thinking turn comes up. */ void remove(thinker_t &thinker); /** * Iterate the list of thinkers making a callback for each. * * @param flags Thinker filter flags. * @param callback Callback to make for each thinker_t. */ de::LoopResult forAll(dbyte flags, std::function func) const; /** * Iterate the list of thinkers making a callback for each. * * @param thinkFunc Only make a callback for thinkers whose function matches this. * @param flags Thinker filter flags. * @param callback Callback to make for each thinker_t. * * @overload */ de::LoopResult forAll(thinkfunc_t thinkFunc, dbyte flags, std::function func) const; /** * Locates a mobj by it's unique identifier in the map. * * @param id Unique id of the mobj to lookup. */ struct mobj_s *mobjById(dint id); /** * @param id Thinker id to test. */ bool isUsedMobjId(thid_t id); /** * @param id New thinker id. * @param inUse In-use state of @a id. @c true = the id is in use. */ void setMobjId(thid_t id, bool inUse = true); /** * Returns the total number of thinkers (of any type) in the collection. * * @param numInStasis If not @c nullptr, the number of thinkers in stasis will * be added to the current value (caller must ensure to * initialize this). */ dint count(dint *numInStasis = nullptr) const; private: DENG2_PRIVATE(d) }; } // namespace de dd_bool Thinker_IsMobjFunc(thinkfunc_t func); de::Map &Thinker_Map(thinker_t const &th); /** * Initializes the private data object of a thinker. The type of private data is chosen * based on whether the thinker is on the client or server, and possibly based on other * factors. * * Only call this when the thinker does not have a private data object. * * @param th Thinker. */ void Thinker_InitPrivateData(thinker_t *th); #endif // DENG_WORLD_THINKERS_H doomsday-stable-1.15.7/doomsday/client/include/world/lineblockmap.h0000664000175000017500000000334512641367670024661 0ustar jaakkojaakko/** @file lineblockmap.h Specialized world map line blockmap. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_LINEBLOCKMAP_H #define DENG_WORLD_LINEBLOCKMAP_H #include #include "world/blockmap.h" #include "Line" /** * Specializes Blockmap for use with map Lines, implementing a linkage algorithm * that replicates the quirky behavior of doom.exe * * @ingroup world */ class LineBlockmap : public de::Blockmap { public: /** * @param bounds Map space boundary. * @param cellSize Width and height of a cell in map space units. */ LineBlockmap(AABoxd const &bounds, uint cellSize = 128); /// @note Assumes @a line is not yet linked! void link(Line &line); /// @note Assumes none of the specified @a lines are yet linked! void link(QList const &lines); }; #endif // DENG_WORLD_LINEBLOCKMAP_H doomsday-stable-1.15.7/doomsday/client/include/world/vertex.h0000664000175000017500000001036012641367670023531 0ustar jaakkojaakko/** @file vertex.h World map vertex. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_VERTEX_H #define DENG_WORLD_VERTEX_H #include #include #include "MapElement" #include "Mesh" class Line; class LineOwner; /** * World map geometry vertex. * * An @em owner in this context is any line whose start or end points are * defined as the vertex. * * @ingroup world */ class Vertex : public de::MapElement, public de::MeshElement { DENG2_NO_COPY (Vertex) DENG2_NO_ASSIGN(Vertex) public: /// Notified whenever the origin changes. DENG2_DEFINE_AUDIENCE(OriginChange, void vertexOriginChanged(Vertex &vertex)) public: /// @todo Move to the map loader: /// Head of the LineOwner rings (an array of [numLineOwners] size). The /// owner ring is a doubly, circularly linked list. The head is the owner /// with the lowest angle and the next-most being that with greater angle. LineOwner *_lineOwners; uint _numLineOwners; ///< Total number of line owners. // Total numbers of line owners. uint _onesOwnerCount; uint _twosOwnerCount; public: Vertex(de::Mesh &mesh, de::Vector2d const &origin = de::Vector2d()); /** * Returns the origin (i.e., position) of the vertex in the map coordinate space. */ de::Vector2d const &origin() const; /** * Returns the X axis origin (i.e., position) of the vertex in the map coordinate space. */ inline coord_t x() const { return origin().x; } /** * Returns the Y axis origin (i.e., position) of the vertex in the map coordinate space. */ inline coord_t y() const { return origin().y; } /** * Change the origin (i.e., position) of the vertex in the map coordinate * space. The OriginChange audience is notified whenever the origin changes. * * @param newOrigin New origin in map coordinate space units. */ void setOrigin(de::Vector2d const &newOrigin); /** * @copydoc setOrigin() * * @param x New X origin in map coordinate space units. * @param y New Y origin in map coordinate space units. */ inline void setOrigin(float x, float y) { return setOrigin(de::Vector2d(x, y)); } public: // Deprecated ---------------------------------------------------------- /** * Returns the total number of Line owners for the vertex. * * @see countLineOwners() * * @deprecated Will be replaced with half-edge ring iterator/rover. -ds */ uint lineOwnerCount() const; /** * Utility function for determining the number of one and two-sided Line * owners for the vertex. * * @note That if only the combined total is desired, it is more efficent to * call lineOwnerCount() instead. * * @pre Line owner rings must have already been calculated. * @pre @a oneSided and/or @a twoSided must have already been initialized. * * @todo Optimize: Cache this result. * * @deprecated Will be replaced with half-edge ring iterator/rover. -ds */ void countLineOwners(); /** * Returns the first Line owner for the vertex; otherwise @c 0 if unowned. * * @deprecated Will be replaced with half-edge ring iterator/rover. -ds */ LineOwner *firstLineOwner() const; protected: int property(DmuArgs &args) const; private: DENG2_PRIVATE(d) }; #endif // DENG_WORLD_VERTEX_H doomsday-stable-1.15.7/doomsday/client/include/world/sectorcluster.h0000664000175000017500000003236412641367670025125 0ustar jaakkojaakko/** @file sectorcluster.h World map sector cluster. * * @authors Copyright © 2013-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_SECTORCLUSTER_H #define DENG_WORLD_SECTORCLUSTER_H #include "dd_share.h" // AudioEnvironmentFactors #include "HEdge" #include "MapElement" #include "Line" #include "Plane" #include "Sector" #ifdef __CLIENT__ # include "render/lightgrid.h" #endif #include #include #include #include class ConvexSubspace; #ifdef __CLIENT__ class BiasDigest; class Shard; #endif /** * Adjacent subspaces in the sector (i.e., those which share one or more common * edge) are grouped into a "cluster". Clusters are never empty and will always * contain at least one subspace. * * @ingroup world */ class SectorCluster #ifdef __CLIENT__ : public de::LightGrid::IBlockLightSource #endif { public: /// Notified when the cluster is about to be deleted. DENG2_DEFINE_AUDIENCE(Deletion, void sectorClusterBeingDeleted(SectorCluster const &cluster)) typedef QList Subspaces; public: /** * Construct a new sector cluster comprised of the specified set of subspaces. * It is assumed that all subspaces in the list are attributed to the same * sector and there is always at least one. * * @param subspaces Set of subspaces comprising the resulting cluster. */ SectorCluster(Subspaces const &subspaces); virtual ~SectorCluster(); /** * Determines whether the specified @a hedge is an "internal" edge: * * - both the half-edge and it's twin have a face. * - both faces are assigned to a subspace. * - both of the assigned subspaces are in the same cluster. * * @param hedge Half-edge to test. * * @return @c true= @a hedge is a cluster internal edge. */ static bool isInternalEdge(de::HEdge *hedge); /** * Returns the parent sector of the cluster. */ Sector §or(); Sector const §or() const; /** * Returns the identified @em physical plane of the parent sector. Note * that this is not the same as the "visual" plane which may well be * defined by another sector. * * @param planeIndex Index of the plane to return. */ Plane &plane(int planeIndex); Plane const &plane(int planeIndex) const; /** * Returns the sector plane which defines the @em physical floor of the * cluster. * @see hasSector(), plane() */ inline Plane &floor() { return plane(Sector::Floor); } inline Plane const &floor() const { return plane(Sector::Floor); } /** * Returns the sector plane which defines the @em physical ceiling of * the cluster. * @see hasSector(), plane() */ inline Plane &ceiling() { return plane(Sector::Ceiling); } inline Plane const &ceiling() const { return plane(Sector::Ceiling); } /** * Returns the identified @em visual sector plane for the cluster (which * may or may not be the same as the physical plane). * * @param planeIndex Index of the plane to return. */ Plane &visPlane(int planeIndex); Plane const &visPlane(int planeIndex) const; /** * Returns the sector plane which defines the @em visual floor of the * cluster. * @see hasSector(), floor() */ inline Plane &visFloor() { return visPlane(Sector::Floor); } inline Plane const &visFloor() const { return visPlane(Sector::Floor); } /** * Returns the sector plane which defines the @em visual ceiling of the * cluster. * @see hasSector(), ceiling() */ inline Plane &visCeiling() { return visPlane(Sector::Ceiling); } inline Plane const &visCeiling() const { return visPlane(Sector::Ceiling); } /** * Returns the total number of @em visual planes in the cluster. */ inline int visPlaneCount() const { return sector().planeCount(); } /** * To be called to force re-evaluation of mapped visual planes. This is only * necessary when a surface material change occurs on boundary line of the * cluster. */ void markVisPlanesDirty(); /** * Returns @c true iff at least one of the mapped visual planes of the cluster * presently has a sky-masked material bound. * * @see Surface::hasSkyMaskedMaterial() */ bool hasSkyMaskedPlane() const; /** * Provides access to the list of all subspaces in the cluster, for efficient * traversal. */ Subspaces const &subspaces() const; /** * Returns the total number of subspaces in the cluster. */ inline int subspaceCount() const { return subspaces().count(); } /** * Returns the axis-aligned bounding box of the cluster. */ AABoxd const &aaBox() const; /** * Returns the point defined by the center of the axis-aligned bounding * box in the map coordinate space. */ inline de::Vector2d center() const { return (de::Vector2d(aaBox().min) + de::Vector2d(aaBox().max)) / 2; } #ifdef __CLIENT__ /** * Determines whether the cluster has a positive world volume, i.e., the * height of floor is lower than that of the ceiling plane. * * @param useSmoothedHeights @c true= use the @em smoothed plane heights * instead of the @em sharp heights. */ bool hasWorldVolume(bool useSmoothedHeights = true) const; /** * Returns a rough approximation of the total combined area of the geometry * for all the subspaces which define the cluster (map units squared). */ coord_t roughArea() const; /** * Request re-calculation of environmental audio (reverb) characteristics for * the cluster (update is deferred until next accessed). * * To be called whenever any of the properties governing reverb properties * have changed (i.e., wall/plane material changes). */ void markReverbDirty(bool yes = true); /** * Returns the final environmental audio characteristics (reverb) of the * cluster. Note that if a reverb update is scheduled it will be done at * this time (@ref markReverbDirty()). */ AudioEnvironmentFactors const &reverb() const; /** * Returns the unique identifier of the light source. */ LightId lightSourceId() const; /** * Returns the final ambient light color for the source (which, may be affected * by the sky light color if one or more Plane Surfaces in the cluster are using * a sky-masked Material). */ de::Vector3f lightSourceColorf() const; /** * Returns the final ambient light intensity for the source. * @see lightSourceColorf() */ de::dfloat lightSourceIntensity(de::Vector3d const &viewPoint = de::Vector3d(0, 0, 0)) const; /** * Returns the final ambient light color and intensity for the source. * @see lightSourceColorf() */ inline de::Vector4f lightSourceColorfIntensity() { return de::Vector4f(lightSourceColorf(), lightSourceIntensity()); } /** * Returns the Z-axis bias scale factor for the light grid, block light source. */ int blockLightSourceZBias(); /** * Returns the geometry Shard for the specified @a mapElement and geometry * group identifier @a geomId; otherwise @c 0. */ Shard *findShard(de::MapElement &mapElement, int geomId); /** * Generate/locate the geometry Shard for the specified @a mapElement and * geometry group identifier @a geomId. */ Shard &shard(de::MapElement &mapElement, int geomId); /** * Shards owned by the SectorCluster should call this periodically to update * their bias lighting contributions. * * @param shard Shard to be updated (owned by the SectorCluster). * * @return @c true if one or more BiasIllum contributors was updated. */ bool updateBiasContributors(Shard *shard); /** * Apply bias lighting changes to @em all geometry Shards within the cluster. * * @param changes Digest of lighting changes to be applied. */ void applyBiasDigest(BiasDigest &changes); /** * Convenient method of determining the frameCount of the current bias render * frame. Used for tracking changes to bias sources/surfaces. * * @see Map::biasLastChangeOnFrame() */ uint biasLastChangeOnFrame() const; #endif // __CLIENT__ private: DENG2_PRIVATE(d) }; /** * Specialized sector cluster half-edge circulator. Used like an iterator, for * circumnavigating the boundary half-edges of a cluster. * * Cluster-internal edges (i.e., where both half-edge faces reference the same * cluster) are automatically skipped during traversal. Otherwise behavior is * the same as a "regular" half-edge face circulator. * * Also provides static search utilities for convenient, one-time use of this * specialized search logic (avoiding circulator instantiation). * * @ingroup world */ class SectorClusterCirculator { public: /// Attempt to dereference a NULL circulator. @ingroup errors DENG2_ERROR(NullError); public: /** * Construct a new sector cluster circulator. * * @param hedge Half-edge to circulate. It is assumed the half-edge lies on * the @em boundary of the cluster and is not an "internal" edge. */ SectorClusterCirculator(de::HEdge *hedge = 0) : _hedge(hedge) , _current(hedge) , _cluster(hedge? getCluster(*hedge) : 0) {} /** * Intended as a convenient way to employ the specialized circulator logic * to locate the relative back of the next/previous neighboring half-edge. * Particularly useful when a geometry traversal requires a switch from the * cluster to face boundary, or when navigating the so-called "one-ring" of * a vertex. */ static de::HEdge &findBackNeighbor(de::HEdge const &hedge, de::ClockDirection direction) { return getNeighbor(hedge, direction, getCluster(hedge)).twin(); } /** * Returns the neighbor half-edge in the specified @a direction around the * boundary of the cluster. * * @param direction Relative direction of the desired neighbor. */ de::HEdge &neighbor(de::ClockDirection direction) { _current = &getNeighbor(*_current, direction, _cluster); return *_current; } /// Returns the next half-edge (clockwise) and advances the circulator. inline de::HEdge &next() { return neighbor(de::Clockwise); } /// Returns the previous half-edge (anticlockwise) and advances the circulator. inline de::HEdge &previous() { return neighbor(de::Anticlockwise); } /// Advance to the next half-edge (clockwise). inline SectorClusterCirculator &operator ++ () { next(); return *this; } /// Advance to the previous half-edge (anticlockwise). inline SectorClusterCirculator &operator -- () { previous(); return *this; } /// Returns @c true iff @a other references the same half-edge as "this" /// circulator; otherwise returns false. inline bool operator == (SectorClusterCirculator const &other) const { return _current == other._current; } inline bool operator != (SectorClusterCirculator const &other) const { return !(*this == other); } /// Returns @c true iff the range of the circulator [c, c) is not empty. inline operator bool () const { return _hedge != 0; } /// Makes the circulator operate on @a hedge. SectorClusterCirculator &operator = (de::HEdge &hedge) { _hedge = _current = &hedge; _cluster = getCluster(hedge); return *this; } /// Returns the current half-edge of a non-empty sequence. de::HEdge &operator * () const { if(!_current) { /// @throw NullError Attempted to dereference a "null" circulator. throw NullError("SectorClusterCirculator::operator *", "Circulator references an empty sequence"); } return *_current; } /// Returns a pointer to the current half-edge (might be @c NULL, meaning the /// circulator references an empty sequence). de::HEdge *operator -> () { return _current; } private: static SectorCluster *getCluster(de::HEdge const &hedge); static de::HEdge &getNeighbor(de::HEdge const &hedge, de::ClockDirection direction, SectorCluster const *cluster = 0); de::HEdge *_hedge; de::HEdge *_current; SectorCluster *_cluster; }; #endif // DENG_WORLD_SECTORCLUSTER_H doomsday-stable-1.15.7/doomsday/client/include/world/bsp/0000775000175000017500000000000012641367670022627 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/include/world/bsp/edgetip.h0000664000175000017500000001412512641367670024424 0ustar jaakkojaakko/** @file edgetip.h World BSP Edge Tip. * * Originally based on glBSP 2.24 (in turn, based on BSP 2.3) * @see http://sourceforge.net/projects/glbsp/ * * @authors Copyright © 2007-2014 Daniel Swanson * @authors Copyright © 2000-2007 Andrew Apted * @authors Copyright © 1998-2000 Colin Reed * @authors Copyright © 1998-2000 Lee Killough * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_BSP_EDGETIP_H #define DENG_WORLD_BSP_EDGETIP_H #include #include "world/bsp/linesegment.h" namespace de { namespace bsp { /** * A "edge tip" is where the edge of a line segment and the relevant vertex meet. * * @ingroup bsp */ class EdgeTip { public: /// Logical side identifiers. enum Side { Front, Back }; public: EdgeTip() {} EdgeTip(LineSegmentSide &side) : _angle(side.angle()) , _front(side.hasSector()? &side : nullptr) , _back(side.back().hasSector()? &side.back() : nullptr) {} coord_t angle() const { return _angle; } void setAngle(coord_t newAngle) { _angle = newAngle; } bool hasSide(Side sid) const { return sid == Front? _front != nullptr : _back != nullptr; } inline bool hasFront() const { return hasSide(Front); } inline bool hasBack() const { return hasSide(Back); } LineSegmentSide &side(Side sid) const { if(sid == Front) { DENG2_ASSERT(_front != nullptr); return *_front; } else { DENG2_ASSERT(_back != nullptr); return *_back; } } inline LineSegmentSide &front() const { return side(Front); } inline LineSegmentSide &back() const { return side(Back); } inline LineSegmentSide *frontPtr() const { return hasFront()? &front() : nullptr; } inline LineSegmentSide *backPtr() const { return hasBack() ? &back() : nullptr; } void setSide(Side sid, LineSegmentSide *lineSeg) { if(sid == Front) { _front = lineSeg; } else { _back = lineSeg; } } inline void setFront(LineSegmentSide *lineSeg) { setSide(Front, lineSeg); } inline void setBack(LineSegmentSide *lineSeg) { setSide(Back, lineSeg); } private: /// Angle that line makes at vertex (degrees; 0 is E, 90 is N). coord_t _angle = 0; /// Line segments on each side of the tip. Front is the side of increasing /// angles, back is the side of decreasing angles. Either may be @c nullptr. LineSegmentSide *_front = nullptr; LineSegmentSide *_back = nullptr; }; /** * Provides an always-sorted EdgeTip data set. * * @ingroup bsp */ class EdgeTips { public: ~EdgeTips() { clear(); } /// @see insert() inline EdgeTips &operator << (EdgeTip const &tip) { insert(tip); return *this; } /** * Returns @c true iff the set contains zero edge tips. */ bool isEmpty() const { return _tips.empty(); } /** * Insert a copy of @a tip into the set, in it's rightful place according to * an anti-clockwise (increasing angle) order. * * @param epsilon Angle equivalence threshold (in degrees). */ void insert(EdgeTip const &tip, ddouble epsilon = 1.0 / 128) { Tips::reverse_iterator after = _tips.rbegin(); while(after != _tips.rend() && tip.angle() + epsilon < (*after).angle()) { after++; } _tips.insert(after.base(), tip); } /** * Returns the tip from the set with the smallest angle if not empty. */ EdgeTip const *smallest() const { return _tips.empty()? nullptr : &_tips.front(); } /** * Returns the tip from the set with the largest angle if not empty. */ EdgeTip const *largest() const { return _tips.empty()? nullptr : &_tips.back(); } /** * @param epsilon Angle equivalence threshold (in degrees). */ EdgeTip const *at(ddouble angle, ddouble epsilon = 1.0 / 128) const { for(EdgeTip const &tip : _tips) { coord_t delta = de::abs(tip.angle() - angle); if(delta < epsilon || delta > (360.0 - epsilon)) { return &tip; } } return nullptr; } /** * @param epsilon Angle equivalence threshold (in degrees). */ EdgeTip const *after(ddouble angle, ddouble epsilon = 1.0 / 128) const { for(EdgeTip const &tip : _tips) { if(angle + epsilon < tip.angle()) { return &tip; } } return nullptr; } /** * Clear all tips in the set. */ void clear() { _tips.clear(); } /** * Clear all tips attributed to the specified line segment @a seg. */ void clearByLineSegment(LineSegment &seg) { Tips::iterator it = _tips.begin(); while(it != _tips.end()) { EdgeTip &tip = *it; if((tip.hasFront() && &tip.front().line() == &seg) || (tip.hasBack() && &tip.back().line() == &seg)) { it = _tips.erase(it); } else { ++it; } } } private: typedef std::list Tips; Tips _tips; }; } // namespace bsp } // namespace de #endif // DENG_WORLD_BSP_EDGETIP_H doomsday-stable-1.15.7/doomsday/client/include/world/bsp/partitionevaluator.h0000664000175000017500000000342412641367670026737 0ustar jaakkojaakko/** @file partitionevaluator.h Evaluator for a would-be BSP. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_BSP_PARTITIONEVALUATOR_H #define DENG_WORLD_BSP_PARTITIONEVALUATOR_H #include "world/bsp/superblockmap.h" namespace de { namespace bsp { /** * Evaluates the suitability of a would-be partition, determining a cost metric * which takes into account the number of splits and the resulting tree balance * post partitioning. */ class PartitionEvaluator { public: /** * @param splitCostFactor Split cost multiplier. */ PartitionEvaluator(int splitCostFactor); /** * Find the best line segment to use as the next partition. * * @param node Block tree node containing the remaining line segments. * * @return The chosen partition line. */ LineSegmentSide *choose(LineSegmentBlockTreeNode &node); private: DENG2_PRIVATE(d) }; } // namespace bsp } // namespace de #endif // DENG_WORLD_BSP_PARTITIONEVALUATOR_H doomsday-stable-1.15.7/doomsday/client/include/world/bsp/hplane.h0000664000175000017500000002457712641367670024266 0ustar jaakkojaakko/** @file hplane.h World BSP Half-plane. * * Originally based on glBSP 2.24 (in turn, based on BSP 2.3) * @see http://sourceforge.net/projects/glbsp/ * * @authors Copyright © 2007-2014 Daniel Swanson * @authors Copyright © 2000-2007 Andrew Apted * @authors Copyright © 1998-2000 Colin Reed * @authors Copyright © 1998-2000 Lee Killough * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_BSP_HPLANE_H #define DENG_WORLD_BSP_HPLANE_H #include #include #include "world/bsp/linesegment.h" #include "partition.h" /// Two intercepts whose distance is inclusive of this bound will be merged. #define HPLANE_INTERCEPT_MERGE_DISTANCE_EPSILON 1.0 / 128 class Sector; class Vertex; namespace de { namespace bsp { class EdgeTips; /** * Models the partitioning binary space half-plane. * * @ingroup bsp */ class HPlane { public: /** * Used to model an intercept in the list of intersections. */ class Intercept { public: /** * Construct a new intercept. * * @param distance Distance from the origin of the partition line. * @param lineSeg The intercepted line segment. * @param edge Relative edge of the line segment nearest to the * interception point. */ Intercept(double distance, LineSegmentSide &lineSeg, int edge); bool operator < (Intercept const &other) const { return _distance < other._distance; } /** * Determine the distance between "this" and the @a other intercept. */ double operator - (Intercept const &other) const { return _distance - other._distance; } /** * Returns distance along the half-plane relative to the origin. */ double distance() const { return _distance; } /** * Returns the intercepted line segment. */ LineSegmentSide &lineSegment() const; inline bool lineSegmentIsSelfReferencing() const { LineSegmentSide &seg = lineSegment(); return seg.hasMapLine() && seg.mapLine().isSelfReferencing(); } /** * Returns the identifier for the relevant edge of the intercepted * line segment. */ int lineSegmentEdge() const; /** * Returns the relative vertex from the intercepted line segment. * * @see lineSegment(), lineSegmentEdge() */ inline Vertex &vertex() const { return lineSegment().vertex(lineSegmentEdge()); } Sector *before() const; Sector *after() const; LineSegmentSide *beforeLineSegment() const; LineSegmentSide *afterLineSegment() const; #ifdef DENG2_DEBUG void debugPrint() const; #endif friend class HPlane; private: /// Sector on each side of the vertex (along the partition), or @c 0 /// if that direction is "closed" (i.e., the intercept point is along /// a map line that has no Sector on the relevant side). LineSegmentSide *_before; LineSegmentSide *_after; /// Distance along the half-plane relative to the origin. double _distance; // The intercepted line segment and edge identifier. LineSegmentSide *_lineSeg; int _edge; }; typedef QList Intercepts; public: /** * Construct a new half-plane from the given @a partition line. */ explicit HPlane(Partition const &partition = Partition()); /** * Reconfigure the half-plane according to the given line segment. * * @param newLineSeg The "new" line segment to configure using. */ void configure(LineSegmentSide const &newLineSeg); /** * Perform intersection of the half-plane with the specified @a lineSeg * to determine the distance (along the partition line) at which the * @a edge vertex can be found. * * @param lineSeg Line segment to perform intersection with. * @param edge Line segment edge identifier of the vertex to use * for intersection. * * @return Distance to intersection point along the half-plane (relative * to the origin). */ double intersect(LineSegmentSide const &lineSeg, int edge); /** * Perform intersection of the half-plane with the specified @a lineSeg. * If the two are found to intersect -- a new intercept will be added to * the list of intercepts. If a previous intersection for the specified * @a lineSeg @a edge has already been found then no new intercept will * be created and @c 0 is returned. * * @param lineSeg Line segment to perform intersection with. * @param edge Line segment edge identifier of the vertex to associate * with any resulting intercept. * * @param edgeTips Set of EdgeTips for the identified @a edge of * @a lineSeg. (@todo Refactor away -ds) * * @return The resultant new intercept; otherwise @c nullptr. */ Intercept *intercept(LineSegmentSide const &lineSeg, int edge, EdgeTips const &edgeTips); /** * Sort and then merge near-intercepts from the given list. * * @todo fixme: Logically this is very suspect. Implementing this logic by * merging near-intercepts at hplane level is wrong because this does * nothing about any intercepting half-edge vertices. Consequently, rather * than moving the existing vertices and welding them, this will result in * the creation of new gaps gaps along the partition and result in holes * (which buildHEdgesAtIntersectionGaps() will then warn about). * * This should be redesigned so that near-intercepting vertices are welded * in a stable manner (i.e., not incrementally, which can result in vertices * drifting away from the hplane). Logically, therefore, this should not * be done prior to creating hedges along the partition - instead this * should happen afterwards. -ds */ void sortAndMergeIntercepts(); /** * Clear the list of intercept "points" for the half-plane. */ void clearIntercepts(); #ifdef DENG2_DEBUG void printIntercepts() const; #endif /** * Returns the Partition (immutable) used to model the partitioning line of * the half-plane. * * @see configure() */ Partition const &partition() const; /** * Returns the world angle of the partition line (which, is derived from the * direction vector). * * @see inverseAngle(), Partition::direction */ coord_t angle() const; /** * Returns the inverted world angle for the partition line (i.e., rotated 180 degrees). * * @see angle() */ coord_t inverseAngle() const; /** * Returns the logical @em slopetype for the partition line (which, is determined * according to the world direction). * * @see direction() * @see M_SlopeType() */ slopetype_t slopeType() const; /** * Returns a pointer to the map Line attributed to the line segment which was * chosen as the half-plane partition. May return @c nullptr (if no map line * was attributed). */ LineSegmentSide *lineSegment() const; /** * Calculate @em perpendicular distances from one or both of the vertexe(s) * of "this" line segment to the @a other line segment. For this operation * the @a other line segment is interpreted as an infinite line. The vertexe(s) * of "this" line segment are projected onto the conceptually infinite line * defined by @a other and the length of the resultant vector(s) are then * determined. * * @param other Other line segment to determine vertex distances to. * * Return values: * @param fromDist Perpendicular distance from the "from" vertex. Can be @c nullptr. * @param toDist Perpendicular distance from the "to" vertex. Can be @c nullptr. */ void distance(LineSegmentSide const &lineSegment, coord_t *fromDist = nullptr, coord_t *toDist = nullptr) const; /** * Determine the logical relationship between the partition line and the given * @a lineSegment. In doing so the perpendicular distance for the vertexes of * the line segment are calculated (and optionally returned). * * @param lineSegment Line segment to determine relationship to. * * Return values: * @param fromDist Perpendicular distance from the "from" vertex. Can be @c nullptr. * @param toDist Perpendicular distance from the "to" vertex. Can be @c nullptr. * * @return LineRelationship between the partition line and the line segment. */ LineRelationship relationship(LineSegmentSide const &lineSegment, coord_t *retFromDist = nullptr, coord_t *retToDist = nullptr) const; /** * Returns the list of intercepts for the half-plane for efficient traversal. * * @note This list may or may not yet be sorted. If a sorted list is desired * then sortAndMergeIntercepts() should first be called. * * @see interceptLineSegmentSide() */ Intercepts const &intercepts() const; /** * Returns the current number of half-plane intercepts. */ inline int interceptCount() const { return intercepts().count(); } private: DENG2_PRIVATE(d) }; typedef HPlane::Intercept HPlaneIntercept; } // namespace bsp } // namespace de #endif // DENG_WORLD_BSP_HPLANE_H doomsday-stable-1.15.7/doomsday/client/include/world/bsp/partitioner.h0000664000175000017500000000751112641367670025344 0ustar jaakkojaakko/** @file partitioner.h World map binary space partitioner. * * @authors Copyright © 2007-2014 Daniel Swanson * @authors Copyright © 2000-2007 Andrew Apted * @authors Copyright © 1998-2000 Colin Reed * @authors Copyright © 1998-2000 Lee Killough * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_BSP_PARTITIONER_H #define DENG_WORLD_BSP_PARTITIONER_H #include #include #include #include "world/map.h" class Line; class Sector; namespace de { class Mesh; namespace bsp { /// Minimum length of a half-edge post partitioning. Used in cost evaluation. static coord_t const SHORT_HEDGE_EPSILON = 4.0; /// Smallest distance between two points before being considered equal. static coord_t const DIST_EPSILON = 1.0 / 128.0; /** * World map binary space partitioner (BSP). * * Originally based on glBSP 2.24 (in turn, based on BSP 2.3). * @see http://sourceforge.net/projects/glbsp/ * * @ingroup bsp */ class Partitioner { public: /* * Observers to be notified when an unclosed sector is found. */ DENG2_DEFINE_AUDIENCE(UnclosedSectorFound, void unclosedSectorFound(Sector §or, Vector2d const &nearPoint)) typedef Map::BspTree BspTree; typedef QSet LineSet; public: /** * Construct a new binary space partitioner. * * @param splitCostFactor Cost factor attributed to splitting a half-edge. */ Partitioner(int splitCostFactor = 7); /** * Set the cost factor associated with splitting an existing half-edge. * * @param newFactor New split cost factor. */ void setSplitCostFactor(int newFactor); /** * Build a new BspTree for the given geometry. * * @param lines Set of lines to construct a BSP for. A copy of the set is * made however the caller must ensure that line data remains * accessible until the build process has completed (ownership * is unaffected). * * @param mesh Mesh from which to assign new geometries. The caller must * ensure that the mesh remains accessible until the build * process has completed (ownership is unaffected). * * @return Root tree node of the resultant BSP; otherwise @c nullptr if no * usable tree data was produced. */ BspTree *makeBspTree(LineSet const &lines, Mesh &mesh); /** * Retrieve the number of Segments owned by the partitioner. When the build * completes this number will be the total number of line segments that were * produced during that process. Note that as BspLeaf ownership is claimed * this number will decrease respectively. * * @return Current number of Segments owned by the partitioner. */ int segmentCount(); /** * Retrieve the total number of Vertexes produced during the build process. */ int vertexCount(); private: DENG2_PRIVATE(d) }; } // namespace bsp } // namespace de #endif // DENG_WORLD_BSP_PARTITIONER_H doomsday-stable-1.15.7/doomsday/client/include/world/bsp/linesegment.h0000664000175000017500000004335112641367670025320 0ustar jaakkojaakko/** @file linesegment.h World BSP Line Segment. * * Originally based on glBSP 2.24 (in turn, based on BSP 2.3) * @see http://sourceforge.net/projects/glbsp/ * * @authors Copyright © 2007-2014 Daniel Swanson * @authors Copyright © 2000-2007 Andrew Apted * @authors Copyright © 1998-2000 Colin Reed * @authors Copyright © 1998-2000 Lee Killough * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_BSP_LINESEGMENT_H #define DENG_WORLD_BSP_LINESEGMENT_H #include #include #include "HEdge" #include "Line" #include "Vertex" /// Rounding threshold within which two points are considered as co-incident. #define LINESEGMENT_INCIDENT_DISTANCE_EPSILON 1.0 / 128 namespace de { namespace bsp { class ConvexSubspaceProxy; /** * LineRelationship delineates the possible logical relationships between two * line (segments) in the plane. * * @ingroup bsp */ enum LineRelationship { Collinear, Right, RightIntercept, ///< Right vertex intercepts. Left, LeftIntercept, ///< Left vertex intercepts. Intersects }; /// @todo Might be a useful global utility function? -ds LineRelationship lineRelationship(coord_t fromDist, coord_t toDist); /** * Models a finite line segment in the plane. * * @ingroup bsp */ class LineSegment { DENG2_NO_COPY(LineSegment) DENG2_NO_ASSIGN(LineSegment) public: /// Required sector attribution is missing. @ingroup errors DENG2_ERROR(MissingSectorError); /// Logical side identifiers: enum { Front, Back }; /// Vertex identifiers: enum { From, To }; /// Edge identifiers: enum { Left, Right }; /** * Logical side of which there are always two (a front and a back). */ class Side { DENG2_NO_COPY(Side) DENG2_NO_ASSIGN(Side) public: /// Required neighbor segment is missing. @ingroup errors DENG2_ERROR(MissingNeighborError); /// Required map line side attribution is missing. @ingroup errors DENG2_ERROR(MissingMapSideError); /// Required half-edge is missing. @ingroup errors DENG2_ERROR(MissingHEdgeError); public: Side(LineSegment &line); /** * Returns the specified relative vertex from the LineSegment owner. * * @see lineSideId(), line(), LineSegment::vertex(), */ inline Vertex &vertex(int to) const { return line().vertex(lineSideId() ^ to); } /** * Returns the relative From Vertex for "this" side, from the LineSegment owner. * * @see vertex(), to() */ inline Vertex &from() const { return vertex(From); } /** * Returns the relative To Vertex for "this" side, from the LineSegment owner. * * @see vertex(), from() */ inline Vertex &to() const { return vertex(To); } /** * Returns the LineSegment owner of the side. */ LineSegment &line() const; /** * Returns the logical identifier for "this" side (Front or Back). */ int lineSideId() const; /** * Returns @c true iff this is the front side of the owning line segment. * * @see lineSideId() */ inline bool isFront() const { return lineSideId() == Front; } /** * Returns @c true iff this is the back side of the owning line segment. * * @see lineSideId(), isFront() */ inline bool isBack() const { return !isFront(); } /** * Returns the relative back Side from the line segment owner. * * @see lineSideId(), line(), LineSegment::side(), */ inline Side &back() const { return line().side(lineSideId() ^ 1); } /** * Returns @c true iff a map LineSide is attributed to "this" side of * the line segment. */ bool hasMapSide() const; /// @copydoc hasMapSide() inline bool hasMapLine() { return hasMapSide(); } /** * Returns the map LineSide attributed to "this" side of the line segment. * * @see hasMapSide() */ LineSide &mapSide() const; /** * Returns a pointer to the map LineSide attributed to this side of the * line segment; otherwise @c nullptr */ inline LineSide *mapSidePtr() const { return hasMapSide()? &mapSide() : nullptr; } /** * Change the map LineSide attributed to the "this" side of the line * segment. * * @param newMapSide New map line side to attribute. Can be @c nullptr. */ void setMapSide(LineSide *newMapSide); /** * Returns a pointer to the @em partition map Line attributed to "this" * side of the line segment (if any). */ Line *partitionMapLine() const; /** * Change the @em partition map line attributed to "this" side of the * line segment. * * @param newMapLine New map "partition" line. Can be @c nullptr. */ void setPartitionMapLine(Line *newMapLine); /** * Convenient accessor method for returning the map Line of the LineSide * is attributed to "this" side of the line segment. * * @see hasMapSide(), mapSide() */ inline Line &mapLine() const { return mapSide().line(); } /** * Returns true iff the specified @a edge neighbor segment side is configured. * * @param edge If non-zero test the Right neighbor, otherwise the Left. * * @see neighbor() */ bool hasNeighbor(int edge) const; /** * Returns true iff a @em Left edge neighbor is configured. */ inline bool hasLeft() const { return hasNeighbor(Left); } /** * Returns true iff a @em Right edge neighbor is configured. */ inline bool hasRight() const { return hasNeighbor(Right); } /** * Returns the specified @a edge neighbor of "this" side of the line segment. * * @param edge If non-zero retrieve the @em Right neighbor, otherwise the * @em Left. * * @see hasNeighbor() */ Side &neighbor(int edge) const; /** * Returns the @em Left neighbor of "this" side of the line segment. * @see neighbor(), hasLeft() */ inline Side &left() const { return neighbor(Left); } /** * Returns the @em Right neighbor of "this" side of the line segment. * @see neighbor(), hasRight() */ inline Side &right() const { return neighbor(Right); } /** * Change the specified @a edge neighbor of "this" side of the line segment. * * @param edge If non-zero change the @em Right neighbor, otherwise the @em Left. * * @param newNeighbor New line segment side to set as the neighbor. Can be @c nullptr. */ void setNeighbor(int edge, Side *newNeighbor); /** * Change the @em Left neighbor of the "this" side of the line segment. * * @param newLeft New left neighbor line segment side. Can be @c nullptr. * * @see setNeighbor() */ inline void setLeft(Side *newLeft) { setNeighbor(Left, newLeft); } /** * Change the @em Right neighbor of the "this" side of the line segment. * * @param newRight New right neighbor line segment side. Can be @c nullptr. * * @see setNeighbor() */ inline void setRight(Side *newRight) { setNeighbor(Right, newRight); } /** * Returns the line segment block tree node that contains "this" side of * the line segment; otherwise @c nullptr if not contained. */ /*LineSegmentBlockTreeNode*/ void *blockTreeNodePtr() const; /** * Change the line segment block tree node to which "this" side of the * line segment is associated. * * @param newBMapBlock New blockmap block. Use @c nullptr to clear. */ void setBlockTreeNode(/*LineSegmentBlockTreeNode*/ void *newNode); /** * Returns @c true iff a map sector is attributed to "this" side of the * line segment. */ bool hasSector() const; /** * Returns the map sector attributed to "this" side of the line segment. * * @see hasSector() */ Sector §or() const; /** * Returns a pointer to the Sector attributed to "this" side of the line * segment; otherwise @c nullptr. * * @see hasSector() */ inline Sector *sectorPtr() const { return hasSector()? §or() : nullptr; } /** * Change the sector attributed to "this" side of the line segment. * * @param newSector New sector to attribute. Ownership is unaffected. * Use @c nullptr to clear. */ void setSector(Sector *newSector); /** * Returns a direction vector for "this" side of the line segment, * from the From/Start vertex origin to the To/End vertex origin. */ Vector2d const &direction() const; /** * Returns the logical @em slopetype for "this" side of the line * segment (which, is determined from the world direction). * * @see direction() * @see M_SlopeType() */ slopetype_t slopeType() const; /** * Returns the accurate length of the line segment from the From/Start to * vertex origin to the To/End vertex origin. */ coord_t length() const; /** * Returns the world angle of "this" side of the line segment (which, * is derived from the direction vector). * * @see inverseAngle(), direction() */ coord_t angle() const; /** * Calculates the @em parallel distance from "this" side of the line * segment to the specified @a point in the plane (i.e., in the * direction of this side). * * @return Distance to the point expressed as a fraction/scale factor. */ coord_t distance(Vector2d point) const; /** * Calculate @em perpendicular distances from one or both of the * vertexe(s) of "this" side of the line segment to the @a other line * segment side. For this operation the @a other line segment is * interpreted as an infinite line. The vertexe(s) of "this" side of * the line segment are projected onto the conceptually infinite line * defined by @a other and the length of the resultant vector(s) are * then determined. * * @param other Other line segment side to determine distances to. * * Return values: * @param fromDist Perpendicular distance from the "from" vertex. * Can be @c nullptr. * @param toDist Perpendicular distance from the "to" vertex. * Can be @c nullptr. */ void distance(Side const &other, coord_t *fromDist = nullptr, coord_t *toDist = nullptr) const; /** * Determine the logical relationship between "this" line segment side * and the @a other. In doing so the perpendicular distance for the * vertexes of the line segment side are calculated (and optionally * returned). * * @param other Other line segment side to determine relationship to. * * Return values: * @param retFromDist Perpendicular distance from the "from" vertex. * Can be @c nullptr. * @param retToDist Perpendicular distance from the "to" vertex. * Can be @c nullptr. * * @return LineRelationship between the line segments. * * @see distance() */ LineRelationship relationship(Side const &other, coord_t *retFromDist, coord_t *retToDist) const; /// @see M_BoxOnLineSide2() int boxOnSide(AABoxd const &box) const; /** * Returns the axis-aligned bounding box of the line segment (derived * from the coordinates of the two vertexes). */ inline AABoxd aaBox() const { return line().aaBox(); } /** * Returns @c true iff a built half-edge is linked to "this" side of * the line segment. * * @see hedge() */ bool hasHEdge() const; /** * Returns the built half-edge for "this" side of the line segment. * * @see hasHEdge() */ HEdge &hedge() const; /** * Returns a pointer to the built half-edge linked to "this" side of * the line segment. otherwise @c nullptr. * * @see hasHEdge() */ inline HEdge *hedgePtr() const { return hasHEdge()? &hedge() : nullptr; } /** * Change the built half-edge linked to "this" side of the line segment. * * @param newHEdge New half-edge. Can be @c nullptr. * * @see hedge() */ void setHEdge(HEdge *newHEdge); /** * Returns a pointer to the ConvexSubspaceProxy to which "this" side of the * line segment is attributed. May return @c nullptr if not yet attributed. */ ConvexSubspaceProxy *convexSubspace() const; /** * Change the convex subspace to which "this" side of the line segment * is attributed. * * @param newConvexSubspace ConvexSubspace to attribute. Use @c nullptr to * clear the attribution. */ void setConvexSubspace(ConvexSubspaceProxy *newConvexSubspace); /** * To be called to update precalculated vectors, distances, etc... * following a dependent vertex origin change notification. * * @todo Optimize: defer until next accessed. -ds * @todo Make private. -ds */ void updateCache(); private: DENG2_PRIVATE(d) }; public: LineSegment(Vertex &from, Vertex &to); /** * Returns the specified logical side of the line segment. * * @param back If not @c nullptr return the Back side; otherwise the Front side. */ Side &side(int back); /// @copydoc side() Side const &side(int back) const; /** * Returns the logical Front side of the line segment. */ inline Side &front() { return side(Front); } /// @copydoc front() inline Side const &front() const { return side(Front); } /** * Returns the logical Back side of the line segment. */ inline Side &back() { return side(Back); } /// @copydoc back() inline Side const &back() const { return side(Back); } /** * Returns the specified edge vertex of the line segment. * * @param to If not @c nullptr return the To vertex; otherwise the From vertex. */ Vertex &vertex(int to) const; /** * Convenient accessor method for returning the origin of the specified * edge vertex for the line segment. * * @see vertex() */ inline Vector2d const &vertexOrigin(int to) const { return vertex(to).origin(); } /** * Returns the From/Start vertex for the line segment. */ inline Vertex &from() const { return vertex(From); } /** * Convenient accessor method for returning the origin of the From/Start * vertex for the line segment. * * @see from() */ inline Vector2d const &fromOrigin() const { return from().origin(); } /** * Returns the To/End vertex for the line segment. */ inline Vertex &to() const { return vertex(To); } /** * Convenient accessor method for returning the origin of the To/End vertex * for the line segment. * * @see to() */ inline Vector2d const &toOrigin() const { return to().origin(); } /** * Returns the axis-aligned bounding box of the line segment (derived from * the coordinates of the two vertexes). * * @todo Cache this result. */ AABoxd aaBox() const; /** * Replace the specified edge vertex of the line segment. * * @param to If not @c nullptr replace the To vertex; otherwise the From vertex. * @param newVertex The replacement vertex. */ void replaceVertex(int to, Vertex &newVertex); inline void replaceFrom(Vertex &newVertex) { replaceVertex(From, newVertex); } inline void replaceTo(Vertex &newVertex) { replaceVertex(To, newVertex); } private: DENG2_PRIVATE(d) }; typedef LineSegment::Side LineSegmentSide; } // namespace bsp } // namespace de #endif // DENG_WORLD_BSP_LINESEGMENT_H doomsday-stable-1.15.7/doomsday/client/include/world/bsp/superblockmap.h0000664000175000017500000000535212641367670025654 0ustar jaakkojaakko/** @file superblockmap.h World BSP line segment block. * * Originally based on glBSP 2.24 (in turn, based on BSP 2.3) * @see http://sourceforge.net/projects/glbsp/ * * @authors Copyright © 2007-2014 Daniel Swanson * @authors Copyright © 2000-2007 Andrew Apted * @authors Copyright © 1998-2000 Colin Reed * @authors Copyright © 1998-2000 Lee Killough * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_BSP_LINESEGMENTBLOCK_H #define DENG_WORLD_BSP_LINESEGMENTBLOCK_H #include #include #include #include "world/bsp/linesegment.h" namespace de { namespace bsp { /** * @ingroup bsp */ class LineSegmentBlock { public: typedef QList All; public: LineSegmentBlock(AABox const &bounds); /** * Retrieve the axis-aligned bounding box of the block in the blockmap. * Not to be confused with the bounds defined by the line segment geometry * which is determined by @ref findSegmentBounds(). * * @return Axis-aligned bounding box of the block. */ AABox const &bounds() const; void link(LineSegmentSide &seg); void addRef(LineSegmentSide const &seg); void decRef(LineSegmentSide const &seg); /** * Pop (unlink) the next line segment from the FIFO list of segments * linked to the node. * * @return Previous top-most line segment; otherwise @c nullptr (empty). */ LineSegmentSide *pop(); int mapCount() const; int partCount() const; /** * Returns the total number of line segments in this and all child blocks. */ int totalCount() const; /** * Provides access to the list of line segments in the block, for efficient * traversal. */ All const &all() const; private: DENG2_PRIVATE(d) }; typedef de::BinaryTree LineSegmentBlockTreeNode; } // namespace bsp } // namespace de #endif // DENG_WORLD_BSP_LINESEGMENTBLOCK_H doomsday-stable-1.15.7/doomsday/client/include/world/bsp/convexsubspaceproxy.h0000664000175000017500000001462412641367670027141 0ustar jaakkojaakko/** @file convexsubspaceproxy.h BSP builder convex subspace proxy. * * @authors Copyright © 2013-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef DENG_WORLD_BSP_CONVEXSUBSPACEPROXY_H #define DENG_WORLD_BSP_CONVEXSUBSPACEPROXY_H #include #include #ifdef DENG2_DEBUG # include #endif #include "world/bsp/linesegment.h" class BspLeaf; class Sector; namespace de { class Mesh; namespace bsp { /** * @ingroup bsp */ struct OrderedSegment { LineSegmentSide *segment; ddouble fromAngle; ddouble toAngle; bool operator == (OrderedSegment const &other) const { return de::fequal(fromAngle, other.fromAngle) && de::fequal(toAngle, other.toAngle); } #ifdef DENG2_DEBUG void debugPrint() const { LOGDEV_MAP_MSG("%p Angle: %1.6f %s -> Angle: %1.6f %s") << this << fromAngle << (segment? segment->from().origin().asText() : "(null)") << toAngle << (segment? segment->to().origin().asText() : "(null)"); } #endif }; typedef QList OrderedSegments; /** * Models a @em logical convex subspace in the partition plane, providing the * analysis functionality necessary to classify and then separate the segments * into unique geometries. * * Acts as staging area for the future construction of a ConvexSubspace. * * Here infinity (i.e., a subspace containing no segments) is considered to be * convex. Accordingly any segments linked to the subspace are @em not "owned" * by it. * * @note Important: It is the user's responsibility to ensure convexity else * many of the operations on the set of segments will be illogical. * * @todo This functionality could be merged into ConvexSubspace -ds * * @ingroup bsp */ class ConvexSubspaceProxy { public: /** * Construct an empty convex subspace proxy. */ ConvexSubspaceProxy(); /** * Construct a convex subspace proxy from a list of line @a segments. * * @param segments List of line segments which are assumed to define a * convex subspace in the plane. Ownership of the segments * is @em NOT given to the subspace. Note that duplicates * are pruned automatically. */ ConvexSubspaceProxy(QList const &segments); /** * Construct a convex subspace by duplicating @a other. */ ConvexSubspaceProxy(ConvexSubspaceProxy const &other); ConvexSubspaceProxy &operator = (ConvexSubspaceProxy const &); /** * Returns the total number of segments in the subspace. */ int segmentCount() const; /** * Returns @c true iff the subspace is "empty", which is to say there are * zero line segments in the set. * * Equivalent to @code segmentCount() == 0 @endcode */ inline bool isEmpty() const { return segmentCount() == 0; } /** * Add more line segments to the subspace. It is assumed that the new set * conforms to, or is compatible with the subspace. * * @param segments List of line segments to add to the subspace. Ownership * of the segments is @em NOT given to the subspace. Note * that duplicates or any which are already present are * pruned automatically. * * @see addOneSegment() */ void addSegments(QList const &segments); /** * Add a single line segment to the subspace which is assumed to conform to, * or is compatible with the subspace. * * @param segment Line segment to add. Ownership is @em NOT given to the * subspace. Note that if the segment is already present in * the set it will be pruned (nothing will happen). * * @see operator<<(), addSegments() */ void addOneSegment(LineSegmentSide const &segment); /** * Add @a segment to the subspace which is assumed to conform to, or is * compatible with the subspace. * * @param segment Line segment to add. Ownership is @em NOT given to the * subspace. Note that if the segment is already present in * the set it will be pruned (nothing will happen). * * @return Reference to this subspace. * * @see addOneSegment() */ inline ConvexSubspaceProxy &operator << (LineSegmentSide const &segment) { addOneSegment(segment); return *this; } /** * Build and assign all geometries to the BSP leaf specified. Note that * any existing geometries will be replaced (thus destroyed by BspLeaf). * Also, a map sector is chosen and attributed to the BSP leaf. * * @param bspLeaf BSP leaf to build geometry for. * @param mesh Mesh from which to assign geometry. */ void buildGeometry(BspLeaf &bspLeaf, Mesh &mesh) const; /** * The BspLeaf to which the subspace has been attributed if any. * * @see setBspLeaf() * * @todo Refactor away. */ BspLeaf *bspLeaf() const; /** * Change the BspLeaf to which the subspace is attributed. * * @param newBspLeaf BSP leaf to attribute (ownership is unaffected). * Use @c nullptr to clear. * * @see bspLeaf() * * @todo Refactor away. */ void setBspLeaf(BspLeaf *newBspLeaf); /** * Provides a clockwise ordered list of the line segments in the subspace. */ OrderedSegments const &segments() const; private: DENG2_PRIVATE(d) }; } // namespace bsp } // namespace de #endif // DENG_WORLD_BSP_CONVEXSUBSPACEPROXY_H doomsday-stable-1.15.7/doomsday/client/include/world/interceptor.h0000664000175000017500000000561112641367670024555 0ustar jaakkojaakko/** @file interceptor.h World map element/object ray trace interceptor. * * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifndef DENG_WORLD_MAP_INTERCEPTOR_H #define DENG_WORLD_MAP_INTERCEPTOR_H #include "world/map.h" #include "Line" #include /** * Provides a mechanism for tracing line / world map object/element interception. * * Note: For technical reasons it is not presently possible to nest one or more * interceptor traces. * * @ingroup world */ class Interceptor { public: /** * Construct a new interceptor. * * @param callback Will be called for each intercepted map element/object. * @param from Map space origin of the trace. * @param to Map space destination of the trace. * @param flags @ref pathTraverseFlags * @param context Passed to the @a callback. */ Interceptor(traverser_t callback, de::Vector2d const &from = de::Vector2d(), de::Vector2d const &to = de::Vector2d(), int flags = PTF_ALL, void *context = 0); /** * Provides read-only access to the map space origin of the trace. */ coord_t const *origin() const; /** * Provides read-only access to the map space direction of the trace. */ coord_t const *direction() const; /** * Provides read-only access to the current map space opening of the trace. */ LineOpening const &opening() const; /** * Update the "opening" state for the trace in accordance with the heights * defined by the minimal planes which intercept @a line. Only lines within * the map specified at @ref trace() call time will be considered. * * @return @c true iff after the adjustment the opening range is positive, * i.e., the top Z coordinate is greater than the bottom Z. */ bool adjustOpening(Line const *line); /** * Execute the trace (i.e., cast the ray). * * @param map World map in which to execute. * * @return Callback return value. */ int trace(de::Map const &map); private: DENG2_PRIVATE(d) }; #endif // DENG_WORLD_MAP_INTERCEPTOR_H doomsday-stable-1.15.7/doomsday/client/src/0000775000175000017500000000000012641367670020060 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/unix/0000775000175000017500000000000012641367670021043 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/unix/joystick.cpp0000664000175000017500000001403612641367670023412 0ustar jaakkojaakko/** @file joystick.cpp Joystick input pre-processing for Unix. * @ingroup input * * Uses SDL. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * @authors Copyright © 2005 Zachary Keene * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #ifndef DENG_NO_SDL # include #endif #include "de_base.h" #include "de_console.h" #include "de_system.h" #include "de_misc.h" #define CONVCONST ((IJOY_AXISMAX - IJOY_AXISMIN) / 65535.0) int joydevice = 0; // Joystick index to use (cvar) byte useJoystickCvar = true; // Joystick input enabled? (cvar) static dd_bool joyInited; static byte joyAvailable; // Input enabled from a source? static dd_bool joyButtonWasDown[IJOY_MAXBUTTONS]; #ifndef DENG_NO_SDL static SDL_Joystick *joy; #endif void Joystick_Register(void) { #ifndef DENG_NO_SDL C_VAR_INT("input-joy-device", &joydevice, CVF_NO_MAX | CVF_PROTECTED, 0, 0); C_VAR_BYTE("input-joy", &useJoystickCvar, 0, 0, 1); #endif } #ifndef DENG_NO_SDL static void initialize(void) { int joycount; if(isDedicated || CommandLine_Check("-nojoy")) return; #ifdef SOLARIS /** * @note Solaris has no Joystick support according to * https://sourceforge.net/tracker/?func=detail&atid=542099&aid=1732554&group_id=74815 */ return; #endif if(SDL_InitSubSystem(SDL_INIT_JOYSTICK)) { LOG_INPUT_ERROR("SDL init failed for joystick: %s") << SDL_GetError(); } if((joycount = SDL_NumJoysticks()) > 0) { if(joydevice > joycount) { LOG_INPUT_WARNING("Using the default joystick instead of joystick #%i") << joydevice; joy = SDL_JoystickOpen(0); } else joy = SDL_JoystickOpen(joydevice); } if(joy) { // Show some info. LOG_INPUT_MSG("Joystick name: %s" ) << SDL_JoystickName(joy); // We'll handle joystick events manually SDL_JoystickEventState(SDL_ENABLE); LOG_INPUT_VERBOSE("Joystick reports %i axes, %i buttons, %i hats, and %i trackballs") << SDL_JoystickNumAxes(joy) << SDL_JoystickNumButtons(joy) << SDL_JoystickNumHats(joy) << SDL_JoystickNumBalls(joy); joyAvailable = true; } else { LOG_INPUT_NOTE("No joysticks found"); joyAvailable = false; } } #endif dd_bool Joystick_Init(void) { #ifndef DENG_NO_SDL if(joyInited) return true; // Already initialized. LOG_AS("Joystick_Init"); initialize(); joyInited = true; #endif return true; } void Joystick_Shutdown(void) { #ifndef DENG_NO_SDL if(!joyInited) return; // Not initialized. if(joy) { SDL_JoystickClose(joy); joy = 0; } joyInited = false; #endif } dd_bool Joystick_IsPresent(void) { return joyAvailable; } void Joystick_GetState(joystate_t *state) { #ifndef DENG_NO_SDL int i, pov; memset(state, 0, sizeof(*state)); // Initialization has not been done. if(!Joystick_IsPresent() || !useJoystickCvar || !joyInited) return; // Update joysticks SDL_JoystickUpdate(); // What do we have available to us? state->numAxes = MIN_OF( SDL_JoystickNumAxes(joy), IJOY_MAXAXES ); state->numButtons = MIN_OF( SDL_JoystickNumButtons(joy), IJOY_MAXBUTTONS ); state->numHats = MIN_OF( SDL_JoystickNumHats(joy), IJOY_MAXHATS ); for(i = 0; i < state->numAxes; ++i) { int value = SDL_JoystickGetAxis(joy, i); // SDL returns a value between -32768 and 32767, but Doomsday is expecting // -10000 to 10000. We'll convert as we go. value = ((value + 32768) * CONVCONST) + IJOY_AXISMIN; state->axis[i] = value; } for(i = 0; i < state->numButtons; ++i) { int isDown = SDL_JoystickGetButton(joy, i); if(isDown && !joyButtonWasDown[i]) { state->buttonDowns[i] = 1; state->buttonUps[i] = 0; } else if(!isDown && joyButtonWasDown[i]) { state->buttonDowns[i] = 0; state->buttonUps[i] = 1; } joyButtonWasDown[i] = isDown; } for(i = 0; i < state->numHats; ++i) { pov = SDL_JoystickGetHat(joy, i); switch(pov) { case SDL_HAT_UP: state->hatAngle[i] = 0; break; case SDL_HAT_RIGHT: state->hatAngle[i] = 90; break; case SDL_HAT_DOWN: state->hatAngle[i] = 180; break; case SDL_HAT_LEFT: state->hatAngle[i] = 270; break; case SDL_HAT_RIGHTUP: state->hatAngle[i] = 45; break; case SDL_HAT_RIGHTDOWN: state->hatAngle[i] = 135; break; case SDL_HAT_LEFTUP: state->hatAngle[i] = 315; break; case SDL_HAT_LEFTDOWN: state->hatAngle[i] = 225; break; default: state->hatAngle[i] = IJOY_POV_CENTER; } } #else memset(state, 0, sizeof(*state)); #endif } doomsday-stable-1.15.7/doomsday/client/src/unix/dd_uinit.cpp0000664000175000017500000001055312641367670023352 0ustar jaakkojaakko/** @file dd_uinit.cpp Engine Initialization (Unix). * * @authors Copyright © 2004-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include #include #include #include #include #include #include #ifdef UNIX # include "library.h" #endif #include "de_base.h" #include "de_graphics.h" #include "de_console.h" #include "de_system.h" #include "de_play.h" #include "de_network.h" #include "de_misc.h" #include #include #include "dd_uinit.h" #include #ifdef __CLIENT__ # include #endif application_t app; #ifdef __CLIENT__ static int initDGL(void) { return (int) Sys_GLPreInit(); } #endif static void determineGlobalPaths(application_t* app) { assert(app); // By default, make sure the working path is the home folder. de::App::setCurrentWorkPath(de::App::app().nativeHomePath()); #ifndef MACOSX if(getenv("HOME")) { filename_t homePath; directory_t* temp; dd_snprintf(homePath, FILENAME_T_MAXLEN, "%s/%s/runtime/", getenv("HOME"), DENG2_APP->unixHomeFolderName().toLatin1().constData()); temp = Dir_New(homePath); Dir_mkpath(Dir_Path(temp)); app->usingHomeDir = Dir_SetCurrent(Dir_Path(temp)); if(app->usingHomeDir) { DD_SetRuntimePath(Dir_Path(temp)); } Dir_Delete(temp); } #endif // The -userdir option sets the working directory. if(CommandLine_CheckWith("-userdir", 1)) { filename_t runtimePath; directory_t* temp; strncpy(runtimePath, CommandLine_NextAsPath(), FILENAME_T_MAXLEN); Dir_CleanPath(runtimePath, FILENAME_T_MAXLEN); // Ensure the path is closed with a directory separator. F_AppendMissingSlashCString(runtimePath, FILENAME_T_MAXLEN); temp = Dir_New(runtimePath); app->usingUserDir = Dir_SetCurrent(Dir_Path(temp)); if(app->usingUserDir) { DD_SetRuntimePath(Dir_Path(temp)); #ifndef MACOSX app->usingHomeDir = false; #endif } Dir_Delete(temp); } #ifndef MACOSX if(!app->usingHomeDir && !app->usingUserDir) #else if(!app->usingUserDir) #endif { // The current working directory is the runtime dir. directory_t* temp = Dir_NewFromCWD(); DD_SetRuntimePath(Dir_Path(temp)); Dir_Delete(temp); } // libcore has determined the native base path, so let FS1 know about it. DD_SetBasePath(DENG2_APP->nativeBasePath().toUtf8()); } dd_bool DD_Unix_Init(void) { dd_bool failed = true; memset(&app, 0, sizeof(app)); // We wish to use U.S. English formatting for time and numbers. setlocale(LC_ALL, "en_US.UTF-8"); DD_InitCommandLine(); Library_Init(); // Determine our basedir and other global paths. determineGlobalPaths(&app); if(!DD_EarlyInit()) { Sys_MessageBox(MBT_ERROR, DOOMSDAY_NICENAME, "Error during early init.", 0); } #ifdef __CLIENT__ else if(!initDGL()) { Sys_MessageBox(MBT_ERROR, DOOMSDAY_NICENAME, "Error initializing DGL.", 0); } #endif else { // Everything okay so far. failed = false; } return !failed; } /** * Shuts down the engine. */ void DD_Shutdown(void) { // Shutdown all subsystems. DD_ShutdownAll(); Plug_UnloadAll(); Library_Shutdown(); #ifdef __CLIENT__ DisplayMode_Shutdown(); #endif } doomsday-stable-1.15.7/doomsday/client/src/hedge.cpp0000664000175000017500000000625312641367670021646 0ustar jaakkojaakko/** @file hedge.cpp Mesh Geometry Half-Edge. * * @authors Copyright © 2011-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include "Face" #include "hedge.h" namespace de { DENG2_PIMPL_NOREF(HEdge) { /// Vertex of the half-edge. Vertex *vertex; /// Linked @em twin half-edge (that on the other side of "this" half-edge). HEdge *twin; /// Face geometry to which the half-edge is attributed (if any). Face *face; /// Next half-edge (clockwise) around the @em face. HEdge *next; /// Previous half-edge (anticlockwise) around the @em face. HEdge *prev; Instance(Vertex &vertex) : vertex(&vertex), twin(0), face(0), next(0), prev(0) {} inline HEdge **neighborAdr(ClockDirection direction) { return direction == Clockwise? &next : &prev; } }; HEdge::HEdge(Mesh &mesh, Vertex &vertex) : MeshElement(mesh), d(new Instance(vertex)) {} Vertex &HEdge::vertex() const { return *d->vertex; } bool HEdge::hasTwin() const { return d->twin != 0; } HEdge &HEdge::twin() const { if(d->twin) { return *d->twin; } /// @throw MissingTwinError Attempted with no twin associated. throw MissingTwinError("HEdge::twin", "No twin half-edge is associated"); } void HEdge::setTwin(HEdge const *newTwin) { d->twin = const_cast(newTwin); } bool HEdge::hasFace() const { return d->face != 0; } Face &HEdge::face() const { if(d->face) { return *d->face; } /// @throw MissingFaceError Attempted with no Face attributed. throw MissingFaceError("HEdge::face", "No face is attributed"); } void HEdge::setFace(Face const *newFace) { d->face = const_cast(newFace); } bool HEdge::hasNeighbor(ClockDirection direction) const { return (*d->neighborAdr(direction)) != 0; } HEdge &HEdge::neighbor(ClockDirection direction) const { HEdge **neighborAdr = d->neighborAdr(direction); if(*neighborAdr) { return **neighborAdr; } /// @throw MissingNeighborError Attempted with no relevant neighbor attributed. throw MissingNeighborError("HEdge::neighbor", QString("No %1 neighbor is attributed").arg(direction == Clockwise? "Clockwise" : "Anticlockwise")); } void HEdge::setNeighbor(ClockDirection direction, HEdge const *newNeighbor) { *d->neighborAdr(direction) = const_cast(newNeighbor); } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/api_filesys.cpp0000664000175000017500000000304412641367670023074 0ustar jaakkojaakko/** @file api_filesys.cpp Public FS1 API. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include #define DENG_NO_API_MACROS_FILESYS #include "api_filesys.h" /* extern int F_FileExists(char const *path); extern dd_bool F_MakePath(char const *path); extern const char* F_PrettyPath(char const *path); */ // m_misc.c DENG_EXTERN_C size_t M_ReadFile(char const *name, char **buffer); DENG_EXTERN_C AutoStr* M_ReadFileIntoString(ddstring_t const *path, dd_bool *isCustom); DENG_EXTERN_C dd_bool M_WriteFile(char const *name, char const *source, size_t length); DENG_DECLARE_API(F) = { { DE_API_FILE_SYSTEM }, F_Access, F_FileExists, F_MakePath, F_PrettyPath, M_ReadFile, M_ReadFileIntoString, M_WriteFile, F_LumpIndex }; doomsday-stable-1.15.7/doomsday/client/src/game.cpp0000664000175000017500000002506312641367670021503 0ustar jaakkojaakko/** @file game.cpp Game mode configuration (metadata, resource files, etc...). * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "game.h" #include #include "resource/manifest.h" #include #include #include #include #include #include namespace de { DENG2_PIMPL(Game) { pluginid_t pluginId; ///< Unique identifier of the registering plugin. Manifests manifests; ///< Required resource files (e.g., doomu.wad). String identityKey; ///< Unique game mode identifier (e.g., "doom1-ultimate"). String title; ///< Formatted default title, suitable for printing (e.g., "The Ultimate DOOM"). String author; ///< Formatted default author suitable for printing (e.g., "id Software"). Path mainConfig; ///< Config file name (e.g., "configs/doom/game.cfg"). Path bindingConfig; ///< Control binding file name (set automatically). Path mainMapInfo; ///< Base relative path to the main MAPINFO definition data. String legacySavegameNameExp; String legacySavegameSubfolder; Instance(Public &a, String const &identityKey, Path const &configDir, String const &title, String const &author) : Base(a) , pluginId (0) // Not yet assigned. , identityKey (identityKey) , title (title) , author (author) , mainConfig (Path("configs") / configDir / "game.cfg") , bindingConfig(Path("configs") / configDir / "player/bindings.cfg") {} ~Instance() { qDeleteAll(manifests); } }; Game::Game(String const &identityKey, Path const &configDir, String const &title, String const &author, String const &legacySavegameNameExp_, String const &legacySavegameSubfolder, String const &mainMapInfo) : game::Game(identityKey) , d(new Instance(*this, identityKey, configDir, title, author)) { d->legacySavegameNameExp = legacySavegameNameExp_; d->legacySavegameSubfolder = legacySavegameSubfolder; d->mainMapInfo = mainMapInfo; } Game::~Game() {} void Game::addManifest(ResourceManifest &manifest) { // Ensure we don't add duplicates. Manifests::const_iterator found = d->manifests.find(manifest.resourceClass(), &manifest); if(found == d->manifests.end()) { d->manifests.insert(manifest.resourceClass(), &manifest); } } bool Game::allStartupFilesFound() const { foreach(ResourceManifest *manifest, d->manifests) { int const flags = manifest->fileFlags(); if((flags & FF_STARTUP) && !(flags & FF_FOUND)) return false; } return true; } Game::Status Game::status() const { if(App_GameLoaded() && &App_CurrentGame() == this) { return Loaded; } if(allStartupFilesFound()) { return Complete; } return Incomplete; } String const &Game::statusAsText() const { static String const statusTexts[] = { "Loaded", "Playable", "Not playable (incomplete resources)", }; return statusTexts[int(status())]; } String Game::description() const { return String(_E(b) "%1 - %2\n" _E(.) _E(l) "IdentityKey: " _E(.) "%3 " _E(l) "PluginId: " _E(.) "%4\n" _E(D)_E(b) "Startup resources:\n" _E(.)_E(.) "%5\n" _E(D)_E(b) "Other resources:\n" _E(.)_E(.) "%6\n" _E(D)_E(b) "Status: " _E(.) "%7") .arg(title()) .arg(author()) .arg(identityKey()) .arg(int(pluginId())) .arg(filesAsText(FF_STARTUP)) .arg(filesAsText(0, false)) .arg(statusAsText()); } pluginid_t Game::pluginId() const { return d->pluginId; } void Game::setPluginId(pluginid_t newId) { d->pluginId = newId; } String const &Game::identityKey() const { return d->identityKey; } String Game::logoImageId() const { String idKey = identityKey(); /// @todo The name of the plugin should be accessible via the plugin loader. String plugName; if(idKey.contains("heretic")) { plugName = "libheretic"; } else if(idKey.contains("hexen")) { plugName = "libhexen"; } else { plugName = "libdoom"; } return "logo.game." + plugName; } String Game::legacySavegameNameExp() const { return d->legacySavegameNameExp; } String Game::legacySavegamePath() const { NativePath nativeSavePath = App_ResourceSystem().nativeSavePath(); if(nativeSavePath.isEmpty()) return ""; if(isNull()) return ""; if(App::commandLine().has("-savedir")) { // A custom path. The savegames are in the root of this folder. return nativeSavePath; } // The default save path. The savegames are in a game-specific folder. if(!d->legacySavegameSubfolder.isEmpty()) { return App::app().nativeHomePath() / d->legacySavegameSubfolder / identityKey(); } return ""; } Path const &Game::mainConfig() const { return d->mainConfig; } Path const &Game::bindingConfig() const { return d->bindingConfig; } Path const &Game::mainMapInfo() const { return d->mainMapInfo; } String Game::title() const { return d->title; } String const &Game::author() const { return d->author; } Game::Manifests const &Game::manifests() const { return d->manifests; } bool Game::isRequiredFile(File1 &file) { // If this resource is from a container we must use the path of the // root file container instead. File1 &rootFile = file; while(rootFile.isContained()) { rootFile = rootFile.container(); } String absolutePath = rootFile.composePath(); bool isRequired = false; for(Manifests::const_iterator i = d->manifests.find(RC_PACKAGE); i != d->manifests.end() && i.key() == RC_PACKAGE; ++i) { ResourceManifest &manifest = **i; if(!(manifest.fileFlags() & FF_STARTUP)) continue; if(!manifest.resolvedPath(true/*try locate*/).compare(absolutePath, Qt::CaseInsensitive)) { isRequired = true; break; } } return isRequired; } Game *Game::fromDef(GameDef const &def) { return new Game(def.identityKey, NativePath(def.configDir).expand().withSeparators('/'), def.defaultTitle, def.defaultAuthor, def.legacySavegameNameExp, def.legacySavegameSubfolder, def.mainMapInfo); } void Game::printBanner(Game const &game) { LOG_MSG(_E(R) "\n"); LOG_MSG(_E(1)) << game.title(); LOG_MSG(_E(R) "\n"); } String Game::filesAsText(int rflags, bool withStatus) const { String text; // Group output by resource class. Manifests const &manifs = manifests(); for(uint i = 0; i < RESOURCECLASS_COUNT; ++i) { resourceclassid_t const classId = resourceclassid_t(i); for(Manifests::const_iterator i = manifs.find(classId); i != manifs.end() && i.key() == classId; ++i) { ResourceManifest &manifest = **i; if(rflags >= 0 && (rflags & manifest.fileFlags())) { bool const resourceFound = (manifest.fileFlags() & FF_FOUND) != 0; if(!text.isEmpty()) text += "\n" _E(0); if(withStatus) { text += (resourceFound? " - " : _E(1) " ! " _E(.)); } // Format the resource name list. text += String(_E(>) "%1%2") .arg(!resourceFound? _E(D) : "") .arg(manifest.names().join(_E(l) " or " _E(.))); if(withStatus) { text += String(": ") + _E(>) + (!resourceFound? _E(b) "missing " _E(.) : ""); if(resourceFound) { text += String(_E(C) "\"%1\"" _E(.)).arg(NativePath(manifest.resolvedPath(false/*don't try to locate*/)).expand().pretty()); } text += _E(<); } text += _E(<); } } } if(text.isEmpty()) return " none"; return text; } void Game::printFiles(Game const &game, int rflags, bool printStatus) { LOG_RES_MSG("") << game.filesAsText(rflags, printStatus); } D_CMD(InspectGame) { DENG2_UNUSED(src); Game *game = 0; if(argc < 2) { // No game identity key was specified - assume the current game. if(!App_GameLoaded()) { LOG_WARNING("No game is currently loaded.\nPlease specify the identity-key of the game to inspect."); return false; } game = &App_CurrentGame(); } else { String idKey = argv[1]; try { game = &App_Games().byIdentityKey(idKey); } catch(Games::NotFoundError const &) { LOG_WARNING("Unknown game '%s'") << idKey; return false; } } DENG2_ASSERT(!game->isNull()); LOG_MSG("") << game->description(); /* LOG_MSG(_E(b) "%s - %s") << game->title() << game->author(); LOG_MSG(_E(l) "IdentityKey: " _E(.) _E(i) "%s " _E(.) _E(l) "PluginId: " _E(.) _E(i) "%s") << game->identityKey() << int(game->pluginId()); LOG_MSG(_E(D) "Startup resources:"); Game::printFiles(*game, FF_STARTUP); LOG_MSG(_E(D) "Other resources:"); Game::printFiles(*game, 0, false); LOG_MSG(_E(D) "Status: " _E(.) _E(b)) << game->statusAsText(); */ return true; } void Game::consoleRegister() //static { C_CMD("inspectgame", "", InspectGame); C_CMD("inspectgame", "s", InspectGame); } NullGame::NullGame() : Game("" /*null*/, "doomsday", "null-game", "null-game") {} } // namespace de doomsday-stable-1.15.7/doomsday/client/src/audio/0000775000175000017500000000000012641367670021161 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/audio/m_mus2midi.cpp0000664000175000017500000002152112641367670023733 0ustar jaakkojaakko/** @file m_mus2midi.cpp MUS to MIDI conversion. * @ingroup audio * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "audio/m_mus2midi.h" #include #include #include #include #include // MUS event types. enum { MUS_EV_RELEASE_NOTE, MUS_EV_PLAY_NOTE, MUS_EV_PITCH_WHEEL, MUS_EV_SYSTEM, ///< Valueless controller. MUS_EV_CONTROLLER, MUS_EV_FIVE, MUS_EV_SCORE_END, MUS_EV_SEVEN }; // MUS controllers. enum { MUS_CTRL_INSTRUMENT, MUS_CTRL_BANK, MUS_CTRL_MODULATION, MUS_CTRL_VOLUME, MUS_CTRL_PAN, MUS_CTRL_EXPRESSION, MUS_CTRL_REVERB, MUS_CTRL_CHORUS, MUS_CTRL_SUSTAIN_PEDAL, MUS_CTRL_SOFT_PEDAL, // The valueless controllers. MUS_CTRL_SOUNDS_OFF, MUS_CTRL_NOTES_OFF, MUS_CTRL_MONO, MUS_CTRL_POLY, MUS_CTRL_RESET_ALL, NUM_MUS_CTRLS }; #pragma pack(1) struct mus_header { char ID[4]; ///< Identifier "MUS" 0x1A. ushort scoreLen; ushort scoreStart; ushort channels; ///< Number of primary channels. ushort secondaryChannels; ///< Number of secondary channels. ushort instrCnt; ushort padding; // The instrument list begins here. }; #pragma pack() typedef struct mus_event_s { byte channel; byte ev; // event. byte last; } mus_event_t; typedef struct midi_event_s { uint deltaTime; byte command; byte size; byte parms[2]; } midi_event_t; static int readTime; // In ticks. static byte* readPos; static byte chanVols[16]; // Last volume for each channel. static char ctrlMus2Midi[NUM_MUS_CTRLS] = { 0, ///< Not used. 0, ///< Bank select. 1, ///< Modulation. 7, ///< Volume. 10, ///< Pan. 11, ///< Expression. 91, ///< Reverb. 93, ///< Chorus. 64, ///< Sustain pedal. 67, ///< Soft pedal. // The valueless controllers: 120, ///< All sounds off. 123, ///< All notes off. 126, ///< Mono. 127, ///< Poly. 121 ///< Reset all controllers. }; static dd_bool getNextEvent(midi_event_t* ev) { int i; mus_event_t evDesc; byte musEvent; ev->deltaTime = readTime; readTime = 0; musEvent = *readPos++; evDesc.channel = musEvent & 0xf; evDesc.ev = (musEvent >> 4) & 0x7; evDesc.last = (musEvent >> 7) & 0x1; ev->command = 0; ev->size = 0; memset(ev->parms, 0, sizeof(ev->parms)); // Construct the MIDI event. switch(evDesc.ev) { case MUS_EV_PLAY_NOTE: ev->command = 0x90; ev->size = 2; // Which note? ev->parms[0] = *readPos++; // Is the volume there, too? if(ev->parms[0] & 0x80) chanVols[evDesc.channel] = *readPos++; ev->parms[0] &= 0x7f; if((i = chanVols[evDesc.channel]) > 127) i = 127; ev->parms[1] = i; break; case MUS_EV_RELEASE_NOTE: ev->command = 0x80; ev->size = 2; // Which note? ev->parms[0] = *readPos++; break; case MUS_EV_CONTROLLER: ev->command = 0xb0; ev->size = 2; ev->parms[0] = *readPos++; ev->parms[1] = *readPos++; // The instrument control is mapped to another kind of MIDI event. if(ev->parms[0] == MUS_CTRL_INSTRUMENT) { ev->command = 0xc0; ev->size = 1; ev->parms[0] = ev->parms[1]; } else { // Use the conversion table. ev->parms[0] = ctrlMus2Midi[ev->parms[0]]; } break; // 2 bytes, 14 bit value. 0x2000 is the center. // First seven bits go to parm1, the rest to parm2. case MUS_EV_PITCH_WHEEL: ev->command = 0xe0; ev->size = 2; i = *readPos++ << 6; ev->parms[0] = i & 0x7f; ev->parms[1] = i >> 7; break; case MUS_EV_SYSTEM: // Is this ever used? ev->command = 0xb0; ev->size = 2; ev->parms[0] = ctrlMus2Midi[*readPos++]; break; case MUS_EV_SCORE_END: // We're done. return false; default: LOG_RES_WARNING("Invalid MUS format music data"); LOGDEV_RES_WARNING("Unknown MUS event %d while converting MUS to MIDI") << evDesc.ev; return false; } // Choose the channel. i = evDesc.channel; // Redirect MUS channel 16 to MIDI channel 10 (percussion). if(i == 15) i = 9; else if(i == 9) i = 15; ev->command |= i; // Check if this was the last event in a group. if(!evDesc.last) return true; // Read the time delta. readTime = 0; do { i = *readPos++; readTime = (readTime << 7) + (i & 0x7f); } while(i & 0x80); return true; } dd_bool M_Mus2Midi(void* data, size_t length, const char* outFile) { unsigned char buffer[80]; int i, trackSizeOffset, trackSize; struct mus_header* header; ddstring_t nativePath; midi_event_t ev; FILE* file; DENG_UNUSED(length); LOG_AS("M_Mus2Midi"); if(!outFile || !outFile[0]) return false; Str_Init(&nativePath); Str_Set(&nativePath, outFile); F_ToNativeSlashes(&nativePath, &nativePath); /// @todo Reimplement using higher level methods for file IO. file = fopen(Str_Text(&nativePath), "wb"); if(!file) { LOG_RES_WARNING("Failed opening output file \"%s\"") << de::NativePath(Str_Text(&nativePath)).pretty(); Str_Free(&nativePath); return false; } Str_Free(&nativePath); // Start with the MIDI header. strcpy((char*)buffer, "MThd"); fwrite(buffer, 4, 1, file); // Header size. memset(buffer, 0, 3); buffer[3] = 6; fwrite(buffer, 4, 1, file); // Format (single track). buffer[0] = 0; buffer[1] = 0; // Number of tracks. buffer[2] = 0; buffer[3] = 1; // Delta ticks per quarter note (140). buffer[4] = 0; buffer[5] = 140; fwrite(buffer, 6, 1, file); // Track header. strcpy((char*)buffer, "MTrk"); fwrite(buffer, 4, 1, file); // Length of the track in bytes. memset(buffer, 0, 4); trackSizeOffset = ftell(file); fwrite(buffer, 4, 1, file); // Updated later. // The first MIDI ev sets the tempo. buffer[0] = 0; // No delta ticks. buffer[1] = 0xff; buffer[2] = 0x51; buffer[3] = 3; buffer[4] = 0xf; // Exactly one second per quarter note. buffer[5] = 0x42; buffer[6] = 0x40; fwrite(buffer, 7, 1, file); header = (struct mus_header *) data; readPos = (byte*)data + USHORT(header->scoreStart); readTime = 0; // Init channel volumes. for(i = 0; i < 16; ++i) chanVols[i] = 64; while(getNextEvent(&ev)) { // Delta time. Split into 7-bit segments. if(ev.deltaTime == 0) { buffer[0] = 0; fwrite(buffer, 1, 1, file); } else { i = -1; while(ev.deltaTime > 0) { buffer[++i] = ev.deltaTime & 0x7f; if(i > 0) buffer[i] |= 0x80; ev.deltaTime >>= 7; } // The bytes are written starting from the MSB. for(; i >= 0; --i) fwrite(&buffer[i], 1, 1, file); } // The ev data. fwrite(&ev.command, 1, 1, file); fwrite(&ev.parms, 1, ev.size, file); } // End of track. buffer[0] = 0; buffer[1] = 0xff; buffer[2] = 0x2f; buffer[3] = 0; fwrite(buffer, 4, 1, file); // All the MIDI data has now been written. Update the track length. trackSize = ftell(file) - trackSizeOffset - 4; fseek(file, trackSizeOffset, SEEK_SET); buffer[3] = trackSize & 0xff; buffer[2] = (trackSize >> 8) & 0xff; buffer[1] = (trackSize >> 16) & 0xff; buffer[0] = trackSize >> 24; fwrite(buffer, 4, 1, file); fclose(file); return true; // Success! } doomsday-stable-1.15.7/doomsday/client/src/audio/sys_audiod_dummy.cpp0000664000175000017500000002114012641367670025241 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * sys_audiod_dummy.c: Dummy Audio Driver. * * Used in dedicated server mode, when it's necessary to simulate * sound playing but not actually play anything. */ // HEADER FILES ------------------------------------------------------------ #include "de_base.h" #include "de_console.h" #include "de_system.h" #include "de_misc.h" #include "api_audiod.h" #include "api_audiod_sfx.h" // MACROS ------------------------------------------------------------------ // TYPES ------------------------------------------------------------------- // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- // PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- int DS_DummyInit(void); void DS_DummyShutdown(void); void DS_DummyEvent(int type); int DS_Dummy_SFX_Init(void); sfxbuffer_t* DS_Dummy_SFX_CreateBuffer(int flags, int bits, int rate); void DS_Dummy_SFX_DestroyBuffer(sfxbuffer_t* buf); void DS_Dummy_SFX_Load(sfxbuffer_t* buf, struct sfxsample_s* sample); void DS_Dummy_SFX_Reset(sfxbuffer_t* buf); void DS_Dummy_SFX_Play(sfxbuffer_t* buf); void DS_Dummy_SFX_Stop(sfxbuffer_t* buf); void DS_Dummy_SFX_Refresh(sfxbuffer_t* buf); void DS_Dummy_SFX_Set(sfxbuffer_t* buf, int prop, float value); void DS_Dummy_SFX_Setv(sfxbuffer_t* buf, int prop, float* values); void DS_Dummy_SFX_Listener(int prop, float value); void DS_Dummy_SFX_Listenerv(int prop, float* values); int DS_Dummy_SFX_Getv(int prop, void* values); // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- // EXTERNAL DATA DECLARATIONS ---------------------------------------------- // PUBLIC DATA DEFINITIONS ------------------------------------------------- DENG_EXTERN_C audiodriver_t audiod_dummy; DENG_EXTERN_C audiointerface_sfx_t audiod_dummy_sfx; audiodriver_t audiod_dummy = { DS_DummyInit, DS_DummyShutdown, DS_DummyEvent, 0 }; audiointerface_sfx_t audiod_dummy_sfx = { { DS_Dummy_SFX_Init, DS_Dummy_SFX_CreateBuffer, DS_Dummy_SFX_DestroyBuffer, DS_Dummy_SFX_Load, DS_Dummy_SFX_Reset, DS_Dummy_SFX_Play, DS_Dummy_SFX_Stop, DS_Dummy_SFX_Refresh, DS_Dummy_SFX_Set, DS_Dummy_SFX_Setv, DS_Dummy_SFX_Listener, DS_Dummy_SFX_Listenerv, DS_Dummy_SFX_Getv } }; // PRIVATE DATA DEFINITIONS ------------------------------------------------ static dd_bool inited; // CODE -------------------------------------------------------------------- /** * Initialization of the sound driver. * @return @c true if successful. */ int DS_DummyInit(void) { if(inited) return true; // Already initialized. inited = true; return true; } /** * Shut everything down. */ void DS_DummyShutdown(void) { inited = false; } /** * The Event function is called to tell the driver about certain critical * events like the beginning and end of an update cycle. * * @param type Type of event. */ void DS_DummyEvent(int /*type*/) { // Do nothing... } int DS_Dummy_SFX_Init(void) { return inited; } sfxbuffer_t* DS_Dummy_SFX_CreateBuffer(int flags, int bits, int rate) { sfxbuffer_t* buf; // Clear the buffer. buf = (sfxbuffer_t *) Z_Calloc(sizeof(*buf), PU_APPSTATIC, 0); buf->bytes = bits / 8; buf->rate = rate; buf->flags = flags; buf->freq = rate; // Modified by calls to Set(SFXBP_FREQUENCY). return buf; } void DS_Dummy_SFX_DestroyBuffer(sfxbuffer_t* buf) { if(!buf) return; // Free the memory allocated for the buffer. Z_Free(buf); } /** * Prepare the buffer for playing a sample by filling the buffer with as * much sample data as fits. The pointer to sample is saved, so the caller * mustn't free it while the sample is loaded. * * @param buf Sound buffer. * @param sample Sample data. */ void DS_Dummy_SFX_Load(sfxbuffer_t* buf, struct sfxsample_s* sample) { if(!buf || !sample) return; // Now the buffer is ready for playing. buf->sample = sample; buf->written = sample->size; buf->flags &= ~SFXBF_RELOAD; } /** * Stops the buffer and makes it forget about its sample. * * @param buf Sound buffer. */ void DS_Dummy_SFX_Reset(sfxbuffer_t* buf) { if(!buf) return; DS_Dummy_SFX_Stop(buf); buf->sample = NULL; buf->flags &= ~SFXBF_RELOAD; } /** * @param buf Sound buffer. * @return The length of the buffer in milliseconds. */ unsigned int DS_DummyBufferLength(sfxbuffer_t* buf) { if(!buf) return 0; return 1000 * buf->sample->numSamples / buf->freq; } void DS_Dummy_SFX_Play(sfxbuffer_t* buf) { // Playing is quite impossible without a sample. if(!buf || !buf->sample) return; // Do we need to reload? if(buf->flags & SFXBF_RELOAD) DS_Dummy_SFX_Load(buf, buf->sample); // The sound starts playing now? if(!(buf->flags & SFXBF_PLAYING)) { // Calculate the end time (milliseconds). buf->endTime = Timer_RealMilliseconds() + DS_DummyBufferLength(buf); } // The buffer is now playing. buf->flags |= SFXBF_PLAYING; } void DS_Dummy_SFX_Stop(sfxbuffer_t* buf) { if(!buf) return; // Clear the flag that tells the Sfx module about playing buffers. buf->flags &= ~SFXBF_PLAYING; // If the sound is started again, it needs to be reloaded. buf->flags |= SFXBF_RELOAD; } /** * Buffer streamer. Called by the Sfx refresh thread. * @param buf Sound buffer. */ void DS_Dummy_SFX_Refresh(sfxbuffer_t* buf) { // Can only be done if there is a sample and the buffer is playing. if(!buf || !buf->sample || !(buf->flags & SFXBF_PLAYING)) return; // Have we passed the predicted end of sample? if(!(buf->flags & SFXBF_REPEAT) && Timer_RealMilliseconds() >= buf->endTime) { // Time for the sound to stop. DS_Dummy_SFX_Stop(buf); } } /** * @param buf Sound buffer. * @param prop Buffer property: * - SFXBP_VOLUME (if negative, interpreted as attenuation) * - SFXBP_FREQUENCY * - SFXBP_PAN (-1..1) * - SFXBP_MIN_DISTANCE * - SFXBP_MAX_DISTANCE * - SFXBP_RELATIVE_MODE * @param value Value for the property. */ void DS_Dummy_SFX_Set(sfxbuffer_t* buf, int prop, float value) { if(!buf) return; switch(prop) { case SFXBP_FREQUENCY: buf->freq = buf->rate * value; break; default: break; } } /** * Coordinates specified in world coordinate system, converted to DSound's: * +X to the right, +Y up and +Z away (Y and Z swapped, i.e.). * * @param property SFXBP_POSITION * SFXBP_VELOCITY */ void DS_Dummy_SFX_Setv(sfxbuffer_t* /*buf*/, int /*prop*/, float* /*values*/) { // Nothing to do. } /** * @param property SFXLP_UNITS_PER_METER * SFXLP_DOPPLER * SFXLP_UPDATE */ void DS_Dummy_SFX_Listener(int /*prop*/, float /*value*/) { // Nothing to do. } /** * Values use SRD_* for indices. */ void DS_DummyListenerEnvironment(float* /*rev*/) { // Nothing to do. } /** * Call SFXLP_UPDATE at the end of every channel update. */ void DS_Dummy_SFX_Listenerv(int /*prop*/, float* /*values*/) { // Nothing to do. } /** * Gets a driver property. * * @param prop Property (SFXP_*). * @param values Pointer to return value(s). */ int DS_Dummy_SFX_Getv(int prop, void* values) { switch(prop) { case SFXIP_DISABLE_CHANNEL_REFRESH: { /// The return value is a single 32-bit int. int* wantDisable = (int*) values; if(wantDisable) { // We are not playing any audio. *wantDisable = true; } break; } default: return false; } return true; } doomsday-stable-1.15.7/doomsday/client/src/audio/s_sfx.cpp0000664000175000017500000010370112641367670023011 0ustar jaakkojaakko/** @file s_sfx.cpp Sound Effects. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "de_console.h" #include "de_system.h" #include "de_play.h" #include "de_defs.h" #include "de_graphics.h" #include "de_audio.h" #include "de_misc.h" #include "de_render.h" #include "Sector" #include "SectorCluster" #include "audio/sys_audio.h" #include "api_fontrender.h" #include "audio/s_sfx.h" #ifdef __CLIENT__ # include "ui/ui_main.h" #endif #include #define SFX_MAX_CHANNELS (256) #define SFX_LOWEST_PRIORITY (-1000) void Sfx_3DMode(dd_bool activate); void Sfx_SampleFormat(int newBits, int newRate); dd_bool sfxAvail = false; int sfxMaxChannels = 16; int sfxDedicated2D = 4; float sfxReverbStrength = .5f; // Console variables: int sfx3D; int sfx16Bit; int sfxSampleRate = 11025; static int numChannels; static sfxchannel_t *channels; static mobj_t *listener; static SectorCluster *listenerCluster; static thread_t refreshHandle; static volatile dd_bool allowRefresh, refreshing; static byte refMonitor; void Sfx_UpdateReverb() { listenerCluster = nullptr; } #ifdef __CLIENT__ /** * This is a high-priority thread that periodically checks if the channels * need to be updated with more data. The thread terminates when it notices * that the channels have been destroyed. The Sfx audio driver maintains a 250ms * buffer for each channel, which means the refresh must be done often * enough to keep them filled. * * @todo Use a real mutex, will you? */ int C_DECL Sfx_ChannelRefreshThread(void *parm) { DENG_UNUSED(parm); // We'll continue looping until the Sfx module is shut down. while(sfxAvail && channels) { // The bit is swapped on each refresh (debug info). refMonitor ^= 1; if(allowRefresh) { // Do the refresh. refreshing = true; sfxchannel_t *ch = channels; for(int i = 0; i < numChannels; ++i, ch++) { if(!ch->buffer || !(ch->buffer->flags & SFXBF_PLAYING)) continue; AudioDriver_SFX()->Refresh(ch->buffer); } refreshing = false; // Let's take a nap. Sys_Sleep(200); } else { // Refreshing is not allowed, so take a shorter nap while // waiting for allowRefresh. Sys_Sleep(150); } } // Time to end this thread. return 0; } #endif // __CLIENT__ void Sfx_AllowRefresh(dd_bool allow) { if(!sfxAvail) return; if(allowRefresh == allow) return; // No change. allowRefresh = allow; // If we're denying refresh, let's make sure that if it's currently // running, we don't continue until it has stopped. if(!allow) { while(refreshing) { Sys_Sleep(0); } } // Sys_SuspendThread(refreshHandle, !allow); } void Sfx_StopSoundGroup(int group, mobj_t *emitter) { if(!sfxAvail) return; sfxchannel_t *ch = channels; for(int i = 0; i < numChannels; ++i, ch++) { if(!ch->buffer || !(ch->buffer->flags & SFXBF_PLAYING)) continue; if(ch->buffer->sample->group != group || (emitter && ch->emitter != emitter)) continue; // This channel must stop. AudioDriver_SFX()->Stop(ch->buffer); } } int Sfx_StopSound(int id, mobj_t *emitter) { return Sfx_StopSoundWithLowerPriority(id, emitter, -1); } int Sfx_StopSoundWithLowerPriority(int id, mobj_t *emitter, int defPriority) { if(!sfxAvail) return false; int stopCount = 0; sfxchannel_t *ch = channels; for(int i = 0; i < numChannels; ++i, ch++) { if(!ch->buffer || !(ch->buffer->flags & SFXBF_PLAYING)) continue; if((id && ch->buffer->sample->id != id) || (emitter && ch->emitter != emitter)) continue; // Can it be stopped? if(ch->buffer->flags & SFXBF_DONT_STOP) { // The emitter might get destroyed... ch->emitter = NULL; ch->flags |= SFXCF_NO_UPDATE | SFXCF_NO_ORIGIN; continue; } // Check the priority. if(defPriority >= 0) { int oldPrio = defs.sounds[ch->buffer->sample->id].priority; if(oldPrio < defPriority) // Old is more important. return -1; } // This channel must be stopped! AudioDriver_SFX()->Stop(ch->buffer); ++stopCount; } return stopCount; } #if 0 // Currently unused. int Sfx_IsPlaying(int id, mobj_t* emitter) { int i; sfxchannel_t* ch; if(!sfxAvail) return false; for(i = 0, ch = channels; i < numChannels; ++i, ch++) { if(!ch->buffer || !(ch->buffer->flags & SFXBF_PLAYING) || ch->emitter != emitter || id && ch->buffer->sample->id != id) continue; // Once playing, repeating sounds don't stop. if(ch->buffer->flags & SFXBF_REPEAT) return true; // Check time. The flag is updated after a slight delay // (only at refresh). if(Sys_GetTime() - ch->starttime < ch->buffer->sample->numsamples / (float) ch->buffer->freq * TICSPERSEC) return true; } return false; } #endif void Sfx_UnloadSoundID(int id) { if(!sfxAvail) return; BEGIN_COP; sfxchannel_t *ch = channels; for(int i = 0; i < numChannels; ++i, ch++) { if(!ch->buffer || !ch->buffer->sample || ch->buffer->sample->id != id) continue; // Stop and unload. AudioDriver_SFX()->Reset(ch->buffer); } END_COP; } int Sfx_CountPlaying(int id) { if(!sfxAvail) return 0; int count = 0; sfxchannel_t *ch = channels; for(int i = 0; i < numChannels; i++, ch++) { if(!ch->buffer || !ch->buffer->sample) continue; if(ch->buffer->sample->id != id || !(ch->buffer->flags & SFXBF_PLAYING)) continue; count++; } return count; } /** * The priority of a sound is affected by distance, volume and age. */ float Sfx_Priority(mobj_t *emitter, coord_t *point, float volume, int startTic) { // In five seconds all priority of a sound is gone. float timeoff = 1000 * (Timer_Ticks() - startTic) / (5.0f * TICSPERSEC); coord_t *origin; if(!listener || (!emitter && !point)) { // The sound does not have an origin. return 1000 * volume - timeoff; } // The sound has an origin, base the points on distance. if(emitter) { origin = emitter->origin; } else { // No emitter mobj, use the fixed source position. origin = point; } return 1000 * volume - Mobj_ApproxPointDistance(listener, origin) / 2 - timeoff; } /** * Calculate priority points for a sound playing on a channel. * They are used to determine which sounds can be cancelled by new sounds. * Zero is the lowest priority. */ float Sfx_ChannelPriority(sfxchannel_t* ch) { if(!ch->buffer || !(ch->buffer->flags & SFXBF_PLAYING)) return SFX_LOWEST_PRIORITY; if(ch->flags & SFXCF_NO_ORIGIN) return Sfx_Priority(0, 0, ch->volume, ch->startTime); // ch->pos is set to emitter->xyz during updates. return Sfx_Priority(0, ch->origin, ch->volume, ch->startTime); } /** * @return The actual 3D coordinates of the listener. */ void Sfx_GetListenerXYZ(float* origin) { if(!listener) return; /// @todo Make it exactly eye-level! (viewheight). origin[VX] = listener->origin[VX]; origin[VY] = listener->origin[VY]; origin[VZ] = listener->origin[VZ] + listener->height - 5; } /** * Updates the channel buffer's properties based on 2D/3D position * calculations. Listener might be NULL. Sounds emitted from the listener * object are considered to be inside the listener's head. */ void Sfx_ChannelUpdate(sfxchannel_t* ch) { sfxbuffer_t* buf = ch->buffer; float normdist, dist, pan, angle, vec[3]; if(!buf || (ch->flags & SFXCF_NO_UPDATE)) return; // Copy the emitter's position (if any), to the pos coord array. if(ch->emitter) { ch->origin[VX] = ch->emitter->origin[VX]; ch->origin[VY] = ch->emitter->origin[VY]; ch->origin[VZ] = ch->emitter->origin[VZ]; // If this is a mobj, center the Z pos. if(Thinker_IsMobjFunc(ch->emitter->thinker.function)) { // Sounds originate from the center. ch->origin[VZ] += ch->emitter->height / 2; } } // Frequency is common to both 2D and 3D sounds. AudioDriver_SFX()->Set(buf, SFXBP_FREQUENCY, ch->frequency); if(buf->flags & SFXBF_3D) { // Volume is affected only by maxvol. AudioDriver_SFX()->Set(buf, SFXBP_VOLUME, ch->volume * sfxVolume / 255.0f); if(ch->emitter && ch->emitter == listener) { // Emitted by the listener object. Go to relative position mode // and set the position to (0,0,0). vec[VX] = vec[VY] = vec[VZ] = 0; AudioDriver_SFX()->Set(buf, SFXBP_RELATIVE_MODE, true); AudioDriver_SFX()->Setv(buf, SFXBP_POSITION, vec); } else { // Use the channel's map space origin. float origin[3]; V3f_Copyd(origin, ch->origin); AudioDriver_SFX()->Set(buf, SFXBP_RELATIVE_MODE, false); AudioDriver_SFX()->Setv(buf, SFXBP_POSITION, origin); } // If the sound is emitted by the listener, speed is zero. if(ch->emitter && ch->emitter != listener && Thinker_IsMobjFunc(ch->emitter->thinker.function)) { vec[VX] = ch->emitter->mom[MX] * TICSPERSEC; vec[VY] = ch->emitter->mom[MY] * TICSPERSEC; vec[VZ] = ch->emitter->mom[MZ] * TICSPERSEC; AudioDriver_SFX()->Setv(buf, SFXBP_VELOCITY, vec); } else { // Not moving. vec[VX] = vec[VY] = vec[VZ] = 0; AudioDriver_SFX()->Setv(buf, SFXBP_VELOCITY, vec); } } else { // This is a 2D buffer. if((ch->flags & SFXCF_NO_ORIGIN) || (ch->emitter && ch->emitter == listener)) { dist = 1; pan = 0; } else { // Calculate roll-off attenuation. [.125/(.125+x), x=0..1] dist = Mobj_ApproxPointDistance(listener, ch->origin); if(dist < soundMinDist || (ch->flags & SFXCF_NO_ATTENUATION)) { // No distance attenuation. dist = 1; } else if(dist > soundMaxDist) { // Can't be heard. dist = 0; } else { normdist = (dist - soundMinDist) / (soundMaxDist - soundMinDist); // Apply the linear factor so that at max distance there // really is silence. dist = .125f / (.125f + normdist) * (1 - normdist); } // And pan, too. Calculate angle from listener to emitter. if(listener) { angle = (M_PointToAngle2(listener->origin, ch->origin) - listener->angle) / (float) ANGLE_MAX *360; // We want a signed angle. if(angle > 180) angle -= 360; // Front half. if(angle <= 90 && angle >= -90) { pan = -angle / 90; } else { // Back half. pan = (angle + (angle > 0 ? -180 : 180)) / 90; // Dampen sounds coming from behind. dist *= (1 + (pan > 0 ? pan : -pan)) / 2; } } else { // No listener mobj? Can't pan, then. pan = 0; } } AudioDriver_SFX()->Set(buf, SFXBP_VOLUME, ch->volume * dist * sfxVolume / 255.0f); AudioDriver_SFX()->Set(buf, SFXBP_PAN, pan); } } void Sfx_SetListener(mobj_t *mobj) { listener = mobj; } void Sfx_ListenerUpdate() { // No volume means no sound. if(!sfxAvail || !sfx3D || !sfxVolume) return; // Update the listener mobj. Sfx_SetListener(S_GetListenerMobj()); if(listener) { // Position. At eye-level. float vec[4]; Sfx_GetListenerXYZ(vec); AudioDriver_SFX()->Listenerv(SFXLP_POSITION, vec); // Orientation. (0,0) will produce front=(1,0,0) and up=(0,0,1). vec[VX] = listener->angle / (float) ANGLE_MAX *360; vec[VY] = (listener->dPlayer? LOOKDIR2DEG(listener->dPlayer->lookDir) : 0); AudioDriver_SFX()->Listenerv(SFXLP_ORIENTATION, vec); // Velocity. The unit is world distance units per second. vec[VX] = listener->mom[MX] * TICSPERSEC; vec[VY] = listener->mom[MY] * TICSPERSEC; vec[VZ] = listener->mom[MZ] * TICSPERSEC; AudioDriver_SFX()->Listenerv(SFXLP_VELOCITY, vec); // Reverb effects. Has the current sector cluster changed? SectorCluster *newCluster = Mobj_ClusterPtr(*listener); if(newCluster && (!listenerCluster || listenerCluster != newCluster)) { listenerCluster = newCluster; // It may be necessary to recalculate the reverb properties... AudioEnvironmentFactors const &envFactors = listenerCluster->reverb(); for(int i = 0; i < NUM_REVERB_DATA; ++i) { vec[i] = envFactors[i]; if(i == SRD_VOLUME) { vec[i] *= sfxReverbStrength; } } AudioDriver_SFX()->Listenerv(SFXLP_REVERB, vec); } } // Update all listener properties. AudioDriver_SFX()->Listener(SFXLP_UPDATE, 0); } void Sfx_ListenerNoReverb(void) { float rev[4] = { 0, 0, 0, 0 }; if(!sfxAvail) return; listenerCluster = NULL; AudioDriver_SFX()->Listenerv(SFXLP_REVERB, rev); AudioDriver_SFX()->Listener(SFXLP_UPDATE, 0); } /** * Stops the sound playing on the channel. * \note Just stopping a buffer doesn't affect refresh. */ void Sfx_ChannelStop(sfxchannel_t* ch) { if(!ch->buffer) return; AudioDriver_SFX()->Stop(ch->buffer); } void Sfx_GetChannelPriorities(float* prios) { int i; for(i = 0; i < numChannels; ++i) prios[i] = Sfx_ChannelPriority(channels + i); } sfxchannel_t* Sfx_ChannelFindVacant(dd_bool use3D, int bytes, int rate, int sampleID) { int i; sfxchannel_t* ch; for(i = 0, ch = channels; i < numChannels; ++i, ch++) { if(!ch->buffer || (ch->buffer->flags & SFXBF_PLAYING) || use3D != ((ch->buffer->flags & SFXBF_3D) != 0) || ch->buffer->bytes != bytes || ch->buffer->rate != rate) continue; // What about the sample? if(sampleID > 0) { if(!ch->buffer->sample || ch->buffer->sample->id != sampleID) continue; } else if(sampleID == 0) { // We're trying to find a channel with no sample already loaded. if(ch->buffer->sample) continue; } // This is perfect, take this! return ch; } return NULL; } int Sfx_StartSound(sfxsample_t *sample, float volume, float freq, mobj_t *emitter, coord_t *fixedOrigin, int flags) { bool const play3D = sfx3D && (emitter || fixedOrigin); LOG_AS("Sfx_StartSound"); if(!sfxAvail) return false; if(sample->id < 1 || sample->id >= defs.sounds.size()) return false; if(volume <= 0 || !sample->size) return false; if(emitter && sfxOneSoundPerEmitter) { // Stop any other sounds from the same emitter. if(Sfx_StopSoundWithLowerPriority(0, emitter, defs.sounds[sample->id].priority) < 0) { // Something with a higher priority is playing, can't start now. LOG_AUDIO_MSG("Cannot start ID %i (prio%i), overridden (emitter %i)") << sample->id << defs.sounds[sample->id].priority << emitter->thinker.id; return false; } } // Calculate the new sound's priority. int const nowTime = Timer_Ticks(); float const myPrio = Sfx_Priority(emitter, fixedOrigin, volume, nowTime); bool haveChannelPrios = false; float channelPrios[SFX_MAX_CHANNELS]; float lowPrio = 0; // Ensure there aren't already too many channels playing this sample. sfxinfo_t *info = &runtimeDefs.sounds[sample->id]; if(info->channels > 0) { // The decision to stop channels is based on priorities. Sfx_GetChannelPriorities(channelPrios); haveChannelPrios = true; int count = Sfx_CountPlaying(sample->id); while(count >= info->channels) { // Stop the lowest priority sound of the playing instances, again // noting sounds that are more important than us. sfxchannel_t *ch = channels; sfxchannel_t *selCh = 0; for(int i = 0; i < numChannels; ++i, ch++) { if(!ch->buffer) continue; if(!(ch->buffer->flags & SFXBF_PLAYING)) continue; DENG2_ASSERT(ch->buffer->sample != 0); if(ch->buffer->sample->id != sample->id) continue; if(myPrio >= channelPrios[i] && (!selCh || channelPrios[i] <= lowPrio)) { selCh = ch; lowPrio = channelPrios[i]; } } if(!selCh) { // The new sound can't be played because we were unable to stop // enough channels to accommodate the limitation. LOG_AUDIO_XVERBOSE("Not playing #%i because all channels are busy") << sample->id; return false; } // Stop this one. count--; Sfx_ChannelStop(selCh); } } // Hit count tells how many times the cached sound has been used. Sfx_CacheHit(sample->id); /* * Pick a channel for the sound. We will do our best to play the sound, * cancelling existing ones if need be. The ideal choice is a free channel * that is already loaded with the sample, in the correct format and mode. */ sfxchannel_t *selCh = 0; BEGIN_COP; // First look through the stopped channels. At this stage we're very picky: // only the perfect choice will be good enough. selCh = Sfx_ChannelFindVacant(play3D, sample->bytesPer, sample->rate, sample->id); if(!selCh) { // Perhaps there is a vacant channel (with any sample, but preferably one // with no sample already loaded). selCh = Sfx_ChannelFindVacant(play3D, sample->bytesPer, sample->rate, 0); } if(!selCh) { // Try any non-playing channel in the correct format. selCh = Sfx_ChannelFindVacant(play3D, sample->bytesPer, sample->rate, -1); } if(!selCh) { // A perfect channel could not be found. // We must use a channel with the wrong format or decide which one of the // playing ones gets stopped. if(!haveChannelPrios) { Sfx_GetChannelPriorities(channelPrios); } // All channels with a priority less than or equal to ours can be stopped. sfxchannel_t *ch = channels; sfxchannel_t *prioCh = 0; for(int i = 0; i < numChannels; ++i, ch++) { // No buffer? if(!ch->buffer) continue; // Wrong mode? if(play3D != ((ch->buffer->flags & SFXBF_3D) != 0)) continue; if(!(ch->buffer->flags & SFXBF_PLAYING)) { // This channel is not playing, just take it! selCh = ch; break; } // Are we more important than this sound? // We want to choose the lowest priority sound. if(myPrio >= channelPrios[i] && (!prioCh || channelPrios[i] <= lowPrio)) { prioCh = ch; lowPrio = channelPrios[i]; } } // If a good low-priority channel was found, use it. if(prioCh) { selCh = prioCh; Sfx_ChannelStop(selCh); } } if(!selCh) { // A suitable channel was not found. END_COP; LOG_AUDIO_XVERBOSE("Failed to find suitable channel for sample %i") << sample->id; return false; } // Does our channel need to be reformatted? if(selCh->buffer->rate != sample->rate || selCh->buffer->bytes != sample->bytesPer) { AudioDriver_SFX()->Destroy(selCh->buffer); // Create a new buffer with the correct format. selCh->buffer = AudioDriver_SFX()->Create(play3D ? SFXBF_3D : 0, sample->bytesPer * 8, sample->rate); } // Clear flags. selCh->buffer->flags &= ~(SFXBF_REPEAT | SFXBF_DONT_STOP); // Set buffer flags. if(flags & SF_REPEAT) selCh->buffer->flags |= SFXBF_REPEAT; if(flags & SF_DONT_STOP) selCh->buffer->flags |= SFXBF_DONT_STOP; // Init the channel information. selCh->flags &= ~(SFXCF_NO_ORIGIN | SFXCF_NO_ATTENUATION | SFXCF_NO_UPDATE); selCh->volume = volume; selCh->frequency = freq; if(!emitter && !fixedOrigin) { selCh->flags |= SFXCF_NO_ORIGIN; selCh->emitter = NULL; } else { selCh->emitter = emitter; if(fixedOrigin) memcpy(selCh->origin, fixedOrigin, sizeof(selCh->origin)); } if(flags & SF_NO_ATTENUATION) { // The sound can be heard from any distance. selCh->flags |= SFXCF_NO_ATTENUATION; } /** * Load in the sample. Must load prior to setting properties, because * the audio driver might actually create the real buffer only upon loading. * * @note The sample is not reloaded if a sample with the same ID is already * loaded on the channel. */ if(!selCh->buffer->sample || selCh->buffer->sample->id != sample->id) { AudioDriver_SFX()->Load(selCh->buffer, sample); } // Update channel properties. Sfx_ChannelUpdate(selCh); // 3D sounds need a few extra properties set up. if(play3D) { // Init the buffer's min/max distances. // This is only done once, when the sound is started (i.e., here). AudioDriver_SFX()->Set(selCh->buffer, SFXBP_MIN_DISTANCE, (selCh->flags & SFXCF_NO_ATTENUATION)? 10000 : soundMinDist); AudioDriver_SFX()->Set(selCh->buffer, SFXBP_MAX_DISTANCE, (selCh->flags & SFXCF_NO_ATTENUATION)? 20000 : soundMaxDist); } // This'll commit all the deferred properties. AudioDriver_SFX()->Listener(SFXLP_UPDATE, 0); // Start playing. AudioDriver_SFX()->Play(selCh->buffer); END_COP; // Take note of the start time. selCh->startTime = nowTime; // Sound successfully started. return true; } /** * Update channel and listener properties. */ void Sfx_Update(void) { int i; sfxchannel_t* ch; // If the display player doesn't have a mobj, no positioning is done. listener = S_GetListenerMobj(); // Update channels. for(i = 0, ch = channels; i < numChannels; ++i, ch++) { if(!ch->buffer || !(ch->buffer->flags & SFXBF_PLAYING)) continue; // Not playing sounds on this... Sfx_ChannelUpdate(ch); } // Update listener. Sfx_ListenerUpdate(); } void Sfx_StartFrame() { LOG_AS("Sfx_StartFrame"); static int old16Bit = false; static int oldRate = 11025; if(!sfxAvail) return; // Tell the audio driver that the sound frame begins. AudioDriver_Interface(AudioDriver_SFX())->Event(SFXEV_BEGIN); // Have there been changes to the cvar settings? Sfx_3DMode(sfx3D); // Check that the rate is valid. if(sfxSampleRate != 11025 && sfxSampleRate != 22050 && sfxSampleRate != 44100) { LOG_AUDIO_WARNING("\"sound-rate\" corrected to 11025 from invalid value (%i)") << sfxSampleRate; sfxSampleRate = 11025; } // Do we need to change the sample format? if(old16Bit != sfx16Bit || oldRate != sfxSampleRate) { Sfx_SampleFormat(sfx16Bit ? 16 : 8, sfxSampleRate); old16Bit = sfx16Bit; oldRate = sfxSampleRate; } // Should we purge the cache (to conserve memory)? Sfx_PurgeCache(); } void Sfx_EndFrame(void) { if(!sfxAvail) return; if(!BusyMode_Active()) { Sfx_Update(); } // The sound frame ends. AudioDriver_Interface(AudioDriver_SFX())->Event(SFXEV_END); } /** * Creates the buffers for the channels. * * @param num2D Number of 2D the rest will be 3D. * @param bits Bits per sample. * @param rate Sample rate (Hz). */ static void createChannels(int num2D, int bits, int rate) { LOG_AS("Sfx_CreateChannels"); // Change the primary buffer's format to match the channel format. float parm[2] = { float(bits), float(rate) }; AudioDriver_SFX()->Listenerv(SFXLP_PRIMARY_FORMAT, parm); // Try to create a buffer for each channel. sfxchannel_t *ch = channels; for(int i = 0; i < numChannels; ++i, ch++) { ch->buffer = AudioDriver_SFX()->Create(num2D-- > 0 ? 0 : SFXBF_3D, bits, rate); if(!ch->buffer) { LOG_AUDIO_WARNING("Failed to create buffer for #%i") << i; continue; } } } /** * Stop all channels and destroy their buffers. */ void Sfx_DestroyChannels() { BEGIN_COP; for(int i = 0; i < numChannels; ++i) { Sfx_ChannelStop(channels + i); if(channels[i].buffer) AudioDriver_SFX()->Destroy(channels[i].buffer); channels[i].buffer = 0; } END_COP; } void Sfx_InitChannels() { numChannels = sfxMaxChannels; // The -sfxchan option can be used to change the number of channels. if(CommandLine_CheckWith("-sfxchan", 1)) { numChannels = strtol(CommandLine_Next(), 0, 0); if(numChannels < 1) numChannels = 1; if(numChannels > SFX_MAX_CHANNELS) numChannels = SFX_MAX_CHANNELS; LOG_AUDIO_NOTE("Initialized %i sound effect channels") << numChannels; } // Allocate and init the channels. channels = (sfxchannel_t *) Z_Calloc(sizeof(*channels) * numChannels, PU_APPSTATIC, 0); // Create channels according to the current mode. createChannels(sfx3D? sfxDedicated2D : numChannels, sfxBits, sfxRate); } /** * Frees all memory allocated for the channels. */ void Sfx_ShutdownChannels(void) { Sfx_DestroyChannels(); if(channels) Z_Free(channels); channels = NULL; numChannels = 0; } /** * Start the channel refresh thread. It will stop on its own when it * notices that the rest of the sound system is going down. */ void Sfx_StartRefresh() { LOG_AS("Sfx_StartRefresh"); int disableRefresh = false; refreshing = false; allowRefresh = true; if(!AudioDriver_SFX()) goto noRefresh; // Nothing to refresh. if(AudioDriver_SFX()->Getv) AudioDriver_SFX()->Getv(SFXIP_DISABLE_CHANNEL_REFRESH, &disableRefresh); if(!disableRefresh) { // Start the refresh thread. It will run until the Sfx module is shut down. refreshHandle = Sys_StartThread(Sfx_ChannelRefreshThread, NULL); if(!refreshHandle) { throw de::Error("Sfx_StartRefresh", "Failed to start refresh thread."); } } else { noRefresh: LOGDEV_AUDIO_NOTE("Audio driver does not require a refresh thread"); } } dd_bool Sfx_Init() { // Already initialized? if(sfxAvail) return true; // Check if sound has been disabled with a command line option. if(CommandLine_Exists("-nosfx")) { LOG_AUDIO_NOTE("Sound Effects disabled"); return true; } LOG_AUDIO_VERBOSE("Initializing Sound Effects subsystem..."); // No interface for SFX playback? if(!AudioDriver_SFX()) return false; // This is based on the scientific calculations that if the DOOM marine // is 56 units tall, 60 is about two meters. //// @todo Derive from the viewheight. AudioDriver_SFX()->Listener(SFXLP_UNITS_PER_METER, 30); AudioDriver_SFX()->Listener(SFXLP_DOPPLER, 1.5f); // The audio driver is working, let's create the channels. Sfx_InitChannels(); // Init the sample cache. Sfx_InitCache(); // The Sfx module is now available. sfxAvail = true; // Initialize reverb effects to off. Sfx_ListenerNoReverb(); // Finally, start the refresh thread. Sfx_StartRefresh(); return true; } void Sfx_Shutdown(void) { if(!sfxAvail) return; // Not initialized. // These will stop further refreshing. sfxAvail = false; allowRefresh = false; if(refreshHandle) { // Wait for the sfx refresh thread to stop. Sys_WaitThread(refreshHandle, 2000, NULL); refreshHandle = 0; } // Destroy the sample cache. Sfx_ShutdownCache(); // Destroy channels. Sfx_ShutdownChannels(); } void Sfx_Reset(void) { if(!sfxAvail) return; listenerCluster = 0; // Stop all channels. for(int i = 0; i < numChannels; ++i) { Sfx_ChannelStop(&channels[i]); } // Free all samples. Sfx_ShutdownCache(); } /** * Destroys all channels and creates them again. */ void Sfx_RecreateChannels(void) { Sfx_DestroyChannels(); createChannels(sfx3D ? sfxDedicated2D : numChannels, sfxBits, sfxRate); } /** * Swaps between 2D and 3D sound modes. Called automatically by * Sfx_StartFrame when cvar changes. */ void Sfx_3DMode(dd_bool activate) { static int old3DMode = false; if(old3DMode == activate) return; // No change; do nothing. sfx3D = old3DMode = activate; // To make the change effective, re-create all channels. Sfx_RecreateChannels(); // If going to 2D, make sure the reverb is off. if(!sfx3D) { Sfx_ListenerNoReverb(); } } /** * Reconfigures the sample bits and rate. Called automatically by * Sfx_StartFrame when changes occur. */ void Sfx_SampleFormat(int newBits, int newRate) { if(sfxBits == newBits && sfxRate == newRate) return; // No change; do nothing. // Set the new buffer format. sfxBits = newBits; sfxRate = newRate; Sfx_RecreateChannels(); // The cache just became useless, clear it. Sfx_ShutdownCache(); } void Sfx_MapChange(void) { sfxchannel_t *ch = channels; for(int i = 0; i < numChannels; ++i, ch++) { if(ch->emitter) { // Mobjs are about to be destroyed. ch->emitter = 0; // Stop all channels with an origin. Sfx_ChannelStop(ch); } } // Sectors, too, for that matter. listenerCluster = 0; } void Sfx_DebugInfo(void) { #ifdef __CLIENT__ int i, lh; sfxchannel_t* ch; char buf[200]; uint cachesize, ccnt; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glEnable(GL_TEXTURE_2D); FR_SetFont(fontFixed); FR_LoadDefaultAttrib(); FR_SetColorAndAlpha(1, 1, 0, 1); lh = FR_SingleLineHeight("Q"); if(!sfxAvail) { FR_DrawTextXY("Sfx disabled", 0, 0); glDisable(GL_TEXTURE_2D); return; } if(refMonitor) FR_DrawTextXY("!", 0, 0); // Sample cache information. Sfx_GetCacheInfo(&cachesize, &ccnt); sprintf(buf, "Cached:%i (%i)", cachesize, ccnt); FR_SetColor(1, 1, 1); FR_DrawTextXY(buf, 10, 0); // Print a line of info about each channel. for(i = 0, ch = channels; i < numChannels; ++i, ch++) { if(ch->buffer && (ch->buffer->flags & SFXBF_PLAYING)) { FR_SetColor(1, 1, 1); } else { FR_SetColor(1, 1, 0); } sprintf(buf, "%02i: %c%c%c v=%3.1f f=%3.3f st=%i et=%u mobj=%i", i, !(ch->flags & SFXCF_NO_ORIGIN) ? 'O' : '.', !(ch->flags & SFXCF_NO_ATTENUATION) ? 'A' : '.', ch->emitter ? 'E' : '.', ch->volume, ch->frequency, ch->startTime, ch->buffer ? ch->buffer->endTime : 0, ch->emitter? ch->emitter->thinker.id : 0); FR_DrawTextXY(buf, 5, lh * (1 + i * 2)); if(!ch->buffer) continue; sprintf(buf, " %c%c%c%c id=%03i/%-8s ln=%05i b=%i rt=%2i bs=%05i " "(C%05i/W%05i)", (ch->buffer->flags & SFXBF_3D) ? '3' : '.', (ch->buffer->flags & SFXBF_PLAYING) ? 'P' : '.', (ch->buffer->flags & SFXBF_REPEAT) ? 'R' : '.', (ch->buffer->flags & SFXBF_RELOAD) ? 'L' : '.', ch->buffer->sample ? ch->buffer->sample->id : 0, ch->buffer->sample ? defs.sounds[ch->buffer->sample->id]. id : "", ch->buffer->sample ? ch->buffer->sample->size : 0, ch->buffer->bytes, ch->buffer->rate / 1000, ch->buffer->length, ch->buffer->cursor, ch->buffer->written); FR_DrawTextXY(buf, 5, lh * (2 + i * 2)); } glDisable(GL_TEXTURE_2D); #endif } doomsday-stable-1.15.7/doomsday/client/src/audio/s_cache.cpp0000664000175000017500000004553312641367670023264 0ustar jaakkojaakko/** @file s_cache.cpp Sound Sample Cache * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include "de_base.h" #include "de_console.h" #include "de_system.h" #include "de_filesys.h" #include "de_audio.h" #include "de_misc.h" using namespace de; #ifdef __SERVER__ # define BEGIN_COP # define END_COP #endif // The cached samples are stored in a hash. When a sample is purged, its // data will stay in the hash (sample lengths needed by the Logical Sound // Manager). #define CACHE_HASH_SIZE (64) #define PURGE_TIME (10 * TICSPERSEC) // Convert an unsigned byte to signed short (for resampling). #define U8_S16(b) (((byte)(b) - 0x80) << 8) struct SfxCache { SfxCache *next, *prev; int hits; int lastUsed; // Tic the sample was last hit. sfxsample_t sample; }; struct CacheHash { SfxCache *first, *last; }; // 1 Mb = about 12 sec of 44KHz 16bit sound in the cache. int sfxMaxCacheKB = 4096; // Even one minute of silence is quite a long time during gameplay. int sfxMaxCacheTics = TICSPERSEC * 60 * 4; // 4 minutes. static CacheHash scHash[CACHE_HASH_SIZE]; static void Sfx_Uncache(SfxCache *node); void Sfx_InitCache() { // The cache is empty in the beginning. std::memset(scHash, 0, sizeof(scHash)); } void Sfx_ShutdownCache() { // Uncache all the samples in the cache. for(int i = 0; i < CACHE_HASH_SIZE; ++i) { while(scHash[i].first) { Sfx_Uncache(scHash[i].first); } } } static CacheHash *Sfx_CacheHash(int id) { return &scHash[uint(id) % CACHE_HASH_SIZE]; } /** * If the sound is cached, return a pointer to it. */ static SfxCache *Sfx_GetCached(int id) { for(SfxCache *it = Sfx_CacheHash(id)->first; it; it = it->next) { if(it->sample.id == id) return it; } return 0; } /** * Simple linear resampling with possible conversion to 16 bits. * The destination sample must be initialized and it must have a large * enough buffer. We won't reduce rate or bits here. * * @note This is not a clean way to resample a sound. If you read about * DSP a bit, you'll find out that interpolation adds a lot of extra * frequencies in the sample. It should be low-pass filtered after the * interpolation. */ static void resample(void *dst, int dstBytesPer, int dstRate, void const *src, int srcBytesPer, int srcRate, int srcNumSamples, uint srcSize) { DENG_ASSERT(src); DENG_ASSERT(dst); // Let's first check for the easy cases. if(dstRate == srcRate) { if(srcBytesPer == dstBytesPer) { // A simple copy will suffice. std::memcpy(dst, src, srcSize); } else if(srcBytesPer == 1 && dstBytesPer == 2) { // Just changing the bytes won't do much good... unsigned char const *sp = (unsigned char const *) src; short *dp = (short *) dst; for(int i = 0; i < srcNumSamples; ++i) { *dp++ = (*sp++ - 0x80) << 8; } } return; } // 2x resampling. if(dstRate == 2 * srcRate) { if(dstBytesPer == 1) { // The source has a byte per sample as well. unsigned char const *sp = (unsigned char const *) src; unsigned char *dp = (unsigned char *) dst; for(int i = 0; i < srcNumSamples - 1; ++i, sp++) { *dp++ = *sp; *dp++ = (*sp + sp[1]) >> 1; } // Fill in the last two as well. dp[0] = dp[1] = *sp; } else if(srcBytesPer == 1) { // Destination is signed 16bit. Source is 8bit. unsigned char const *sp = (unsigned char const *) src; short *dp = (short *) dst; short first; for(int i = 0; i < srcNumSamples - 1; ++i, sp++) { *dp++ = first = U8_S16(*sp); *dp++ = (first + U8_S16(sp[1])) >> 1; } // Fill in the last two as well. dp[0] = dp[1] = U8_S16(*sp); } else if(srcBytesPer == 2) { // Destination is signed 16bit. Source is 16bit. short const *sp = (short const *) src; short *dp = (short *) dst; for(int i = 0; i < srcNumSamples - 1; ++i, sp++) { *dp++ = *sp; *dp++ = (*sp + sp[1]) >> 1; } dp[0] = dp[1] = *sp; } return; } // 4x resampling (11Khz => 44KHz only). if(dstRate == 4 * srcRate) { if(dstBytesPer == 1) { // The source has a byte per sample as well. unsigned char const *sp = (unsigned char const *) src; unsigned char *dp = (unsigned char *) dst; unsigned char mid; for(int i = 0; i < srcNumSamples - 1; ++i, sp++) { mid = (*sp + sp[1]) >> 1; *dp++ = *sp; *dp++ = (*sp + mid) >> 1; *dp++ = mid; *dp++ = (mid + sp[1]) >> 1; } // Fill in the last four as well. dp[0] = dp[1] = dp[2] = dp[3] = *sp; } else if(srcBytesPer == 1) { // Destination is signed 16bit. Source is 8bit. unsigned char const *sp = (unsigned char const *) src; short *dp = (short *) dst; short first, mid, last; for(int i = 0; i < srcNumSamples - 1; ++i, sp++) { first = U8_S16(*sp); last = U8_S16(sp[1]); mid = (first + last) >> 1; *dp++ = first; *dp++ = (first + mid) >> 1; *dp++ = mid; *dp++ = (mid + last) >> 1; } // Fill in the last four as well. dp[0] = dp[1] = dp[2] = dp[3] = U8_S16(*sp); } else if(srcBytesPer == 2) { // Destination is signed 16bit. Source is 16bit. short const *sp = (short const *) src; short *dp = (short *) dst; short mid; for(int i = 0; i < srcNumSamples - 1; ++i, sp++) { mid = (*sp + sp[1]) >> 1; *dp++ = *sp; *dp++ = (*sp + mid) >> 1; *dp++ = mid; *dp++ = (mid + sp[1]) >> 1; } // Fill in the last four as well. dp[0] = dp[1] = dp[2] = dp[3] = *sp; } } } #ifdef __CLIENT__ /** * Determines whether the audio SFX driver wants all samples to use the same * sampler rate. * * @return @c true, if resampling is required; otherwise @c false. */ static bool sfxMustUpsampleToSfxRate() { int anySampleRateAccepted = 0; if(AudioDriver_SFX()->Getv) { AudioDriver_SFX()->Getv(SFXIP_ANY_SAMPLE_RATE_ACCEPTED, &anySampleRateAccepted); } return (anySampleRateAccepted? false : true); } #endif /** * Caches a copy of the given sample. If it's already in the cache and has * the same format, nothing is done. * * @param id Id number of the sound sample. * @param data Actual sample data. * @param size Size in bytes. * @param numSamples Number of samples. * @param bytesPer Bytes per sample (1 or 2). * @param rate Samples per second. * @param group Exclusion group (0, if none). * * @returns Ptr to the cached sample. Always valid. */ static SfxCache *Sfx_CacheInsert(int id, void const *data, uint size, int numSamples, int bytesPer, int rate, int group) { int rsfactor = 1; /** * First convert the sample to the minimum resolution and bits, set * by sfxRate and sfxBits. */ #ifdef __CLIENT__ // The (up)resampling factor. if(sfxMustUpsampleToSfxRate()) { rsfactor = de::max(1, sfxRate / rate); } #endif /** * If the sample is already in the right format, just make a copy of it. * If necessary, resample the sound upwards, but not downwards. * (You can play higher resolution sounds than the current setting, but * not lower resolution ones.) */ sfxsample_t cached; cached.size = numSamples * bytesPer * rsfactor; if(sfxBits == 16 && bytesPer == 1) { cached.bytesPer = 2; cached.size *= 2; // Will be resampled to 16bit. } else { cached.bytesPer = bytesPer; } cached.rate = rsfactor * rate; cached.numSamples = numSamples * rsfactor; cached.id = id; cached.group = group; // Check if this kind of a sample already exists. SfxCache *node = Sfx_GetCached(id); if(node) { // The sound is already in the cache. Is it in the right format? if(cached.bytesPer * 8 == sfxBits && cached.rate == sfxRate) return node; // This will do. #ifdef __CLIENT__ // Stop all sounds using this sample (we are going to destroy the // existing sample data). Sfx_UnloadSoundID(node->sample.id); #endif // It's in the wrong format! We'll reuse this node. M_Free(node->sample.data); } else { // Get a new node and link it in. node = reinterpret_cast(M_Calloc(sizeof(SfxCache))); CacheHash *hash = Sfx_CacheHash(id); if(hash->last) { hash->last->next = node; node->prev = hash->last; } hash->last = node; if(!hash->first) hash->first = node; } void *buf = M_Malloc(cached.size); // Do the resampling, if necessary. resample(buf, cached.bytesPer, cached.rate, data, bytesPer, rate, numSamples, size); cached.data = buf; // Hits keep count of how many times the cached sound has been played. // The purger will remove samples with the lowest hitcount first. node->hits = 0; std::memcpy(&node->sample, &cached, sizeof(cached)); return node; } static void Sfx_Uncache(SfxCache *node) { DENG2_ASSERT(node); BEGIN_COP; #ifdef __CLIENT__ // Reset all channels loaded with this sample. Sfx_UnloadSoundID(node->sample.id); #endif CacheHash *hash = Sfx_CacheHash(node->sample.id); // Unlink the node. if(hash->last == node) hash->last = node->prev; if(hash->first == node) hash->first = node->next; if(node->next) node->next->prev = node->prev; if(node->prev) node->prev->next = node->next; END_COP; // Free all memory allocated for the node. M_Free(node->sample.data); M_Free(node); } void Sfx_PurgeCache() { static int lastPurge = 0; #ifdef __CLIENT__ if(!sfxAvail) return; #endif // Is it time for a purge? int nowTime = Timer_Ticks(); if(nowTime - lastPurge < PURGE_TIME) return; // No. lastPurge = nowTime; // Count the total size of the cache. // Also get rid of all sounds that have timed out. int totalSize = 0; SfxCache *next = 0; for(int i = 0; i < CACHE_HASH_SIZE; i++) { for(SfxCache *it = scHash[i].first; it; it = next) { next = it->next; if(nowTime - it->lastUsed > sfxMaxCacheTics) { // This sound hasn't been used in a looong time. Sfx_Uncache(it); continue; } totalSize += it->sample.size + sizeof(*it); } } int const maxSize = sfxMaxCacheKB * 1024; int lowHits = 0; SfxCache *lowest; while(totalSize > maxSize) { /** * The cache is too large! Find the stopped sample with the lowest * hitcount and get rid of it. Repeat until cache size is within * limits or there are no more stopped sounds. */ lowest = 0; for(int i = 0; i < CACHE_HASH_SIZE; i++) { for(SfxCache *it = scHash[i].first; it; it = it->next) { #ifdef __CLIENT__ // If the sample is playing we won't remove it now. if(Sfx_CountPlaying(it->sample.id)) continue; #endif // This sample could be removed, let's check the hits. if(!lowest || it->hits < lowHits) { lowest = it; lowHits = it->hits; } } } // No more samples to remove? if(!lowest) break; // Stop and uncache this cached sample. totalSize -= lowest->sample.size + sizeof(*lowest); Sfx_Uncache(lowest); } } void Sfx_GetCacheInfo(uint *cacheBytes, uint *sampleCount) { uint size = 0, count = 0; for(int i = 0; i < CACHE_HASH_SIZE; ++i) { for(SfxCache *it = scHash[i].first; it; it = it->next, count++) { size += it->sample.size; } } if(cacheBytes) *cacheBytes = size; if(sampleCount) *sampleCount = count; } void Sfx_CacheHit(int id) { SfxCache *node = Sfx_GetCached(id); if(node) { node->hits++; node->lastUsed = Timer_Ticks(); } } static sfxsample_t *cacheSample(int id, sfxinfo_t const *info) { LOG_AS("Sfx_Cache"); LOG_AUDIO_VERBOSE("Caching sample '%s' (#%i)...") << info->id << id; int bytesPer = 0, rate = 0, numSamples = 0; /** * Figure out where to get the sample data for this sound. It might be * from a data file such as a WAD or external sound resources. * The definition and the configuration settings will help us in making * the decision. */ void *data = 0; /// Has an external sound file been defined? /// @note Path is relative to the base path. if(!Str_IsEmpty(&info->external)) { String searchPath = App_BasePath() / String(Str_Text(&info->external)); // Try loading. data = WAV_Load(searchPath.toUtf8().constData(), &bytesPer, &rate, &numSamples); if(data) { bytesPer /= 8; // Was returned as bits. } } // If external didn't succeed, let's try the default resource dir. if(!data) { /** * If the sound has an invalid lumpname, search external anyway. * If the original sound is from a PWAD, we won't look for an * external resource (probably a custom sound). * @todo should be a cvar. */ if(info->lumpNum < 0 || !App_FileSystem().lump(info->lumpNum).container().hasCustom()) { try { String foundPath = App_FileSystem().findPath(de::Uri(info->lumpName, RC_SOUND), RLF_DEFAULT, App_ResourceClass(RC_SOUND)); foundPath = App_BasePath() / foundPath; // Ensure the path is absolute. data = WAV_Load(foundPath.toUtf8().constData(), &bytesPer, &rate, &numSamples); if(data) { // Loading was successful. bytesPer /= 8; // Was returned as bits. } } catch(FS1::NotFoundError const&) {} // Ignore this error. } } // No sample loaded yet? if(!data) { // Try loading from the lump. if(info->lumpNum < 0) { LOG_AUDIO_WARNING("Failed to locate lump resource '%s' for sound '%s'") << info->lumpName << info->id; return 0; } File1 &lump = App_FileSystem().lump(info->lumpNum); if(lump.size() <= 8) return 0; char hdr[12]; lump.read((uint8_t *)hdr, 0, 12); // Is this perhaps a WAV sound? if(WAV_CheckFormat(hdr)) { // Load as WAV, then. uint8_t const *sp = lump.cache(); data = WAV_MemoryLoad((byte const *) sp, lump.size(), &bytesPer, &rate, &numSamples); lump.unlock(); if(!data) { // Abort... LOG_AUDIO_WARNING("Unknown WAV format in lump '%s'") << info->lumpName; return 0; } bytesPer /= 8; } } if(data) // Loaded! { // Insert a copy of this into the cache. SfxCache *node = Sfx_CacheInsert(id, data, bytesPer * numSamples, numSamples, bytesPer, rate, info->group); Z_Free(data); return &node->sample; } // Probably an old-fashioned DOOM sample. size_t lumpLength = 0; if(info->lumpNum >= 0) { File1 &lump = App_FileSystem().lump(info->lumpNum); if(lump.size() > 8) { uint8_t hdr[8]; lump.read(hdr, 0, 8); int head = SHORT(*(short const *) (hdr)); rate = SHORT(*(short const *) (hdr + 2)); numSamples = de::max(0, LONG(*(int const *) (hdr + 4))); bytesPer = 1; // 8-bit. if(head == 3 && numSamples > 0 && (unsigned) numSamples <= lumpLength - 8) { // The sample data can be used as-is - load directly from the lump cache. uint8_t const *data = lump.cache() + 8; // Skip the header. // Insert a copy of this into the cache. SfxCache *node = Sfx_CacheInsert(id, data, bytesPer * numSamples, numSamples, bytesPer, rate, info->group); lump.unlock(); return &node->sample; } } } LOG_AUDIO_WARNING("Unknown lump '%s' sound format") << info->lumpName; return 0; } sfxsample_t *Sfx_Cache(int id) { LOG_AS("Sfx_Cache"); #ifdef __CLIENT__ if(!sfxAvail || !id) return 0; #endif // Are we so lucky that the sound is already cached? if(SfxCache *node = Sfx_GetCached(id)) { return &node->sample; } // Get the sound decription. if(sfxinfo_t *info = S_GetSoundInfo(id, 0, 0)) { return cacheSample(id, info); } LOG_AUDIO_WARNING("Ignoring id:%i (missing sfxinfo_t)") << id; return 0; } uint Sfx_GetSoundLength(int id) { sfxsample_t *sample = Sfx_Cache(id & ~DDSF_FLAG_MASK); if(sample) { return (1000 * sample->numSamples) / sample->rate; } return 0; } doomsday-stable-1.15.7/doomsday/client/src/audio/sys_audiod_sdlmixer.cpp0000664000175000017500000003403212641367670025741 0ustar jaakkojaakko/**\file sys_audiod_sdlmixer.cpp * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * SDL_mixer, for SFX, Ext and Mus interfaces. */ #ifndef DENG_DISABLE_SDLMIXER // HEADER FILES ------------------------------------------------------------ #include #include #include #include #undef main #include "de_base.h" #include "de_console.h" #include "de_system.h" #include "de_misc.h" #include "api_audiod.h" #include "api_audiod_sfx.h" #include "api_audiod_mus.h" #include "audio/sys_audiod_sdlmixer.h" #include // MACROS ------------------------------------------------------------------ #define DEFAULT_MIDI_COMMAND "" //"timidity" // TYPES ------------------------------------------------------------------- // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- // PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- int DS_SDLMixerInit(void); void DS_SDLMixerShutdown(void); void DS_SDLMixerEvent(int type); int DS_SDLMixer_SFX_Init(void); sfxbuffer_t* DS_SDLMixer_SFX_CreateBuffer(int flags, int bits, int rate); void DS_SDLMixer_SFX_DestroyBuffer(sfxbuffer_t* buf); void DS_SDLMixer_SFX_Load(sfxbuffer_t* buf, struct sfxsample_s* sample); void DS_SDLMixer_SFX_Reset(sfxbuffer_t* buf); void DS_SDLMixer_SFX_Play(sfxbuffer_t* buf); void DS_SDLMixer_SFX_Stop(sfxbuffer_t* buf); void DS_SDLMixer_SFX_Refresh(sfxbuffer_t* buf); void DS_SDLMixer_SFX_Set(sfxbuffer_t* buf, int prop, float value); void DS_SDLMixer_SFX_Setv(sfxbuffer_t* buf, int prop, float* values); void DS_SDLMixer_SFX_Listener(int prop, float value); void DS_SDLMixer_SFX_Listenerv(int prop, float* values); // The music interface. int DS_SDLMixer_Music_Init(void); void DS_SDLMixer_Music_Update(void); void DS_SDLMixer_Music_Set(int prop, float value); int DS_SDLMixer_Music_Get(int prop, void* value); void DS_SDLMixer_Music_Pause(int pause); void DS_SDLMixer_Music_Stop(void); int DS_SDLMixer_Music_PlayFile(const char* fileName, int looped); // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- // EXTERNAL DATA DECLARATIONS ---------------------------------------------- // PUBLIC DATA DEFINITIONS ------------------------------------------------- dd_bool sdlInitOk = false; audiodriver_t audiod_sdlmixer = { DS_SDLMixerInit, DS_SDLMixerShutdown, DS_SDLMixerEvent }; audiointerface_sfx_t audiod_sdlmixer_sfx = { { DS_SDLMixer_SFX_Init, DS_SDLMixer_SFX_CreateBuffer, DS_SDLMixer_SFX_DestroyBuffer, DS_SDLMixer_SFX_Load, DS_SDLMixer_SFX_Reset, DS_SDLMixer_SFX_Play, DS_SDLMixer_SFX_Stop, DS_SDLMixer_SFX_Refresh, DS_SDLMixer_SFX_Set, DS_SDLMixer_SFX_Setv, DS_SDLMixer_SFX_Listener, DS_SDLMixer_SFX_Listenerv } }; audiointerface_music_t audiod_sdlmixer_music = { { DS_SDLMixer_Music_Init, NULL, DS_SDLMixer_Music_Update, DS_SDLMixer_Music_Set, DS_SDLMixer_Music_Get, DS_SDLMixer_Music_Pause, DS_SDLMixer_Music_Stop }, NULL, NULL, DS_SDLMixer_Music_PlayFile, }; // PRIVATE DATA DEFINITIONS ------------------------------------------------ static int numChannels; static dd_bool* usedChannels; static Mix_Music* lastMusic; //static dd_bool playingMusic = false; // CODE -------------------------------------------------------------------- /** * This is the hook we ask SDL_mixer to call when music playback finishes. */ #if _DEBUG static void musicPlaybackFinished(void) { LOG_AUDIO_VERBOSE("[SDLMixer] Music playback finished"); } #endif static int getFreeChannel(void) { int i; for(i = 0; i < numChannels; ++i) { if(!usedChannels[i]) return i; } return -1; } /** * @return Length of the buffer in milliseconds. */ static unsigned int getBufLength(sfxbuffer_t* buf) { if(!buf) return 0; return 1000 * buf->sample->numSamples / buf->freq; } int DS_SDLMixerInit(void) { int freq, channels; uint16_t format; SDL_version compVer; const SDL_version* linkVer; if(sdlInitOk) return true; if(SDL_InitSubSystem(SDL_INIT_AUDIO)) { LOG_AUDIO_ERROR("Error initializing SDL audio: %s") << SDL_GetError(); return false; } SDL_MIXER_VERSION(&compVer); linkVer = Mix_Linked_Version(); if(SDL_VERSIONNUM(linkVer->major, linkVer->minor, linkVer->patch) > SDL_VERSIONNUM(compVer.major, compVer.minor, compVer.patch)) { LOG_AUDIO_WARNING("Linked version of SDL_mixer (%u.%u.%u) is " "newer than expected (%u.%u.%u)") << linkVer->major << linkVer->minor << linkVer->patch << compVer.major << compVer.minor << compVer.patch; } if(Mix_OpenAudio(MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT, 2, 1024)) { LOG_AUDIO_ERROR("Failed initializing SDL_mixer: %s") << Mix_GetError(); return false; } Mix_QuerySpec(&freq, &format, &channels); // Announce capabilites. LOG_AUDIO_VERBOSE("SDLMixer configuration:"); LOG_AUDIO_VERBOSE(" " _E(>) "Output: %s\n" "Format: %x (%x)\n" "Frequency: %iHz (%iHz)\n" "Initial Channels: %i") << (channels > 1? "stereo" : "mono") << format << (uint16_t) AUDIO_S16LSB << freq << (int) MIX_DEFAULT_FREQUENCY << MIX_CHANNELS; // Prepare to play simultaneous sounds. /*numChannels =*/ Mix_AllocateChannels(MIX_CHANNELS); usedChannels = NULL; // Everything is OK. sdlInitOk = true; return true; } void DS_SDLMixerShutdown(void) { if(!sdlInitOk) return; if(usedChannels) M_Free(usedChannels); if(lastMusic) { Mix_HaltMusic(); Mix_FreeMusic(lastMusic); } lastMusic = NULL; Mix_CloseAudio(); SDL_QuitSubSystem(SDL_INIT_AUDIO); sdlInitOk = false; } void DS_SDLMixerEvent(int) { // Not supported. } int DS_SDLMixer_SFX_Init(void) { // No extra init needed. return sdlInitOk; } sfxbuffer_t* DS_SDLMixer_SFX_CreateBuffer(int flags, int bits, int rate) { sfxbuffer_t* buf; // Create the buffer. buf = (sfxbuffer_t *) Z_Calloc(sizeof(*buf), PU_APPSTATIC, 0); buf->bytes = bits / 8; buf->rate = rate; buf->flags = flags; buf->freq = rate; // Modified by calls to Set(SFXBP_FREQUENCY). // The cursor is used to keep track of the channel on which the sample // is playing. buf->cursor = getFreeChannel(); if((int)buf->cursor < 0) { buf->cursor = numChannels++; usedChannels = (ddboolean_t *) M_Realloc(usedChannels, sizeof(usedChannels[0]) * numChannels); // Make sure we have enough channels allocated. Mix_AllocateChannels(numChannels); Mix_UnregisterAllEffects(buf->cursor); } usedChannels[buf->cursor] = true; return buf; } void DS_SDLMixer_SFX_DestroyBuffer(sfxbuffer_t* buf) { Mix_HaltChannel(buf->cursor); usedChannels[buf->cursor] = false; if(buf) Z_Free(buf); } void DS_SDLMixer_SFX_Load(sfxbuffer_t* buf, struct sfxsample_s* sample) { static char localBuf[0x40000]; char* conv = NULL; size_t size; if(!buf || !sample) return; // Wha? // Does the buffer already have a sample loaded? if(buf->sample) { // Is the same one? if(buf->sample->id == sample->id) return; // Free the existing data. buf->sample = NULL; Mix_FreeChunk((Mix_Chunk *) buf->ptr); } size = 8 + 4 + 8 + 16 + 8 + sample->size; if(size <= sizeof(localBuf)) { conv = localBuf; } else { conv = (char *) M_Malloc(size); } // Transfer the sample to SDL_mixer by converting it to WAVE format. strcpy(conv, "RIFF"); *(Uint32 *) (conv + 4) = ULONG(4 + 8 + 16 + 8 + sample->size); strcpy(conv + 8, "WAVE"); // Format chunk. strcpy(conv + 12, "fmt "); *(Uint32 *) (conv + 16) = ULONG(16); /** * WORD wFormatTag; // Format category * WORD wChannels; // Number of channels * uint dwSamplesPerSec; // Sampling rate * uint dwAvgBytesPerSec; // For buffer estimation * WORD wBlockAlign; // Data block size * WORD wBitsPerSample; // Sample size */ *(Uint16 *) (conv + 20) = USHORT(1); *(Uint16 *) (conv + 22) = USHORT(1); *(Uint32 *) (conv + 24) = ULONG(sample->rate); *(Uint32 *) (conv + 28) = ULONG(sample->rate * sample->bytesPer); *(Uint16 *) (conv + 32) = USHORT(sample->bytesPer); *(Uint16 *) (conv + 34) = USHORT(sample->bytesPer * 8); // Data chunk. strcpy(conv + 36, "data"); *(Uint32 *) (conv + 40) = ULONG(sample->size); memcpy(conv + 44, sample->data, sample->size); buf->ptr = Mix_LoadWAV_RW(SDL_RWFromMem(conv, 44 + sample->size), 1); if(!buf->ptr) { LOG_AS("DS_SDLMixer_SFX_Load"); LOG_AUDIO_WARNING("Failed loading sample: %s") << Mix_GetError(); } if(conv != localBuf) { M_Free(conv); } buf->sample = sample; } /** * Stops the buffer and makes it forget about its sample. */ void DS_SDLMixer_SFX_Reset(sfxbuffer_t* buf) { if(!buf) return; DS_SDLMixer_SFX_Stop(buf); buf->sample = NULL; // Unallocate the resources of the source. Mix_FreeChunk((Mix_Chunk *) buf->ptr); buf->ptr = NULL; } void DS_SDLMixer_SFX_Play(sfxbuffer_t* buf) { // Playing is quite impossible without a sample. if(!buf || !buf->sample) return; // Update the volume at which the sample will be played. Mix_Volume(buf->cursor, buf->written); Mix_PlayChannel(buf->cursor, (Mix_Chunk *) buf->ptr, (buf->flags & SFXBF_REPEAT ? -1 : 0)); // Calculate the end time (milliseconds). buf->endTime = Timer_RealMilliseconds() + getBufLength(buf); // The buffer is now playing. buf->flags |= SFXBF_PLAYING; } void DS_SDLMixer_SFX_Stop(sfxbuffer_t* buf) { if(!buf || !buf->sample) return; Mix_HaltChannel(buf->cursor); //usedChannels[buf->cursor] = false; buf->flags &= ~SFXBF_PLAYING; } void DS_SDLMixer_SFX_Refresh(sfxbuffer_t* buf) { unsigned int nowTime; // Can only be done if there is a sample and the buffer is playing. if(!buf || !buf->sample || !(buf->flags & SFXBF_PLAYING)) return; nowTime = Timer_RealMilliseconds(); /** * Have we passed the predicted end of sample? * \note This test fails if the game has been running for about 50 days, * since the millisecond counter overflows. It only affects sounds that * are playing while the overflow happens, though. */ if(!(buf->flags & SFXBF_REPEAT) && nowTime >= buf->endTime) { // Time for the sound to stop. buf->flags &= ~SFXBF_PLAYING; } } void DS_SDLMixer_SFX_Set(sfxbuffer_t* buf, int prop, float value) { int right; if(!buf) return; switch(prop) { case SFXBP_VOLUME: // 'written' is used for storing the volume of the channel. buf->written = (unsigned int) (value * MIX_MAX_VOLUME); Mix_Volume(buf->cursor, buf->written); break; case SFXBP_PAN: // -1 ... +1 right = (int) ((value + 1) * 127); Mix_SetPanning(buf->cursor, 254 - right, right); break; default: break; } } void DS_SDLMixer_SFX_Setv(sfxbuffer_t *, int , float *) { // Not supported. } void DS_SDLMixer_SFX_Listener(int, float) { // Not supported. } void SetEnvironment(float *) { // Not supported. } void DS_SDLMixer_SFX_Listenerv(int, float *) { // Not supported. } int DS_SDLMixer_Music_Init(void) { #if _DEBUG Mix_HookMusicFinished(musicPlaybackFinished); #endif return sdlInitOk; } void DS_SDLMixer_Music_Update(void) { // Nothing to update. } void DS_SDLMixer_Music_Set(int prop, float value) { if(!sdlInitOk) return; switch(prop) { case MUSIP_VOLUME: Mix_VolumeMusic((int) (MIX_MAX_VOLUME * value)); break; default: break; } } int DS_SDLMixer_Music_Get(int prop, void* value) { if(!sdlInitOk) return false; switch(prop) { case MUSIP_ID: strcpy((char *) value, "SDLMixer::Music"); break; case MUSIP_PLAYING: return Mix_PlayingMusic(); default: return false; } return true; } void DS_SDLMixer_Music_Pause(int pause) { if(!sdlInitOk) return; if(pause) Mix_PauseMusic(); else Mix_ResumeMusic(); } void DS_SDLMixer_Music_Stop(void) { if(!sdlInitOk) return; Mix_HaltMusic(); } int DS_SDLMixer_Music_PlayFile(const char* filename, int looped) { if(!sdlInitOk) return false; // Free any previously loaded music. if(lastMusic) { Mix_HaltMusic(); Mix_FreeMusic(lastMusic); } if(!(lastMusic = Mix_LoadMUS(filename))) { LOG_AS("DS_SDLMixer_Music_PlayFile"); LOG_AUDIO_ERROR("Failed to load music: %s") << Mix_GetError(); return false; } return !Mix_PlayMusic(lastMusic, looped ? -1 : 1); } #endif // DENG_DISABLE_SDLMIXER doomsday-stable-1.15.7/doomsday/client/src/audio/s_main.cpp0000664000175000017500000003716512641367670023147 0ustar jaakkojaakko/** @file s_main.cpp Audio Subsystem. * * Interface to the Sfx and Mus modules. High-level (and exported) audio control. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #define DENG_NO_API_MACROS_SOUND #include "audio/s_main.h" #ifdef __CLIENT__ # include #endif #include #include #include #include #include #ifdef __CLIENT__ # include "audio/audiodriver.h" #endif #include "audio/s_cache.h" #include "audio/s_mus.h" #include "audio/s_sfx.h" #include "audio/sys_audio.h" #include "world/p_players.h" #include "Sector" #include "dd_main.h" // isDedicated #include "m_profiler.h" #ifdef __CLIENT__ # include "gl/gl_main.h" # include "ui/clientwindow.h" #endif #ifdef __SERVER__ # include "server/sv_sound.h" #endif using namespace de; BEGIN_PROF_TIMERS() PROF_SOUND_STARTFRAME END_PROF_TIMERS() audiodriver_t *audioDriver; int showSoundInfo; int soundMinDist = 256; // No distance attenuation this close. int soundMaxDist = 2025; // Setting these variables is enough to adjust the volumes. // S_StartFrame() will call the actual routines to change the volume // when there are changes. int sfxVolume = 255 * 2/3; int musVolume = 255 * 2/3; int sfxBits = 8; int sfxRate = 11025; byte sfxOneSoundPerEmitter; // Traditional Doomsday behavior: allows sounds to overlap. static bool noRndPitch; dd_bool S_Init() { #ifdef __CLIENT__ dd_bool sfxOK, musOK; #endif Sfx_Logical_SetSampleLengthCallback(Sfx_GetSoundLength); if(CommandLine_Exists("-nosound") || CommandLine_Exists("-noaudio")) return true; // Disable random pitch changes? noRndPitch = CommandLine_Exists("-norndpitch"); #ifdef __CLIENT__ // Try to load the audio driver plugin(s). if(!AudioDriver_Init()) { LOG_AUDIO_NOTE("Music and sound effects are disabled"); return false; } sfxOK = Sfx_Init(); musOK = Mus_Init(); if(!sfxOK || !musOK) { LOG_AUDIO_NOTE("Errors during audio subsystem initialization"); return false; } #endif return true; } void S_Shutdown() { #ifdef __CLIENT__ Sfx_Shutdown(); Mus_Shutdown(); // Finally, close the audio driver. AudioDriver_Shutdown(); #endif } void S_MapChange() { // Stop everything in the LSM. Sfx_InitLogical(); #ifdef __CLIENT__ Sfx_MapChange(); #endif } void S_SetupForChangedMap() { #ifdef __CLIENT__ // Update who is listening now. Sfx_SetListener(S_GetListenerMobj()); #endif } void S_Reset() { #ifdef __CLIENT__ Sfx_Reset(); #endif _api_S.StopMusic(); } void S_StartFrame() { #ifdef DD_PROFILE static int i; if(++i > 40) { i = 0; PRINT_PROF( PROF_SOUND_STARTFRAME ); } #endif BEGIN_PROF( PROF_SOUND_STARTFRAME ); #ifdef __CLIENT__ static int oldMusVolume = -1; if(musVolume != oldMusVolume) { oldMusVolume = musVolume; Mus_SetVolume(musVolume / 255.0f); } // Update all channels (freq, 2D:pan,volume, 3D:position,velocity). Sfx_StartFrame(); Mus_StartFrame(); #endif // Remove stopped sounds from the LSM. Sfx_Logical_SetOneSoundPerEmitter(sfxOneSoundPerEmitter); Sfx_PurgeLogical(); END_PROF( PROF_SOUND_STARTFRAME ); } void S_EndFrame() { #ifdef __CLIENT__ Sfx_EndFrame(); #endif } mobj_t *S_GetListenerMobj() { return ddPlayers[displayPlayer].shared.mo; } sfxinfo_t *S_GetSoundInfo(int soundID, float *freq, float *volume) { if(soundID <= 0 || soundID >= defs.sounds.size()) return nullptr; float dummy = 0; if(!freq) freq = &dummy; if(!volume) volume = &dummy; /** * Traverse all links when getting the definition. * (But only up to 10, which is certainly enough and prevents endless * recursion.) Update the sound id at the same time. * The links were checked in Def_Read() so there can't be any bogus ones. */ int i; sfxinfo_t *info = &runtimeDefs.sounds[soundID]; for(i = 0; info->link && i < 10; info = info->link, *freq = (info->linkPitch > 0 ? info->linkPitch / 128.0f : *freq), *volume += (info->linkVolume != -1? info->linkVolume / 127.0f : 0), soundID = runtimeDefs.sounds.indexOf(info), i++) {} DENG2_ASSERT(soundID < defs.sounds.size()); return info; } dd_bool S_IsRepeating(int idFlags) { if(idFlags & DDSF_REPEAT) return true; if(sfxinfo_t *info = S_GetSoundInfo(idFlags & ~DDSF_FLAG_MASK, nullptr, nullptr)) { return (info->flags & SF_REPEAT) != 0; } return false; } int S_LocalSoundAtVolumeFrom(int soundIdAndFlags, mobj_t *origin, coord_t *point, float volume) { #ifdef __CLIENT__ // A dedicated server never starts any local sounds (only logical sounds in the LSM). if(isDedicated || BusyMode_Active()) return false; int soundId = (soundIdAndFlags & ~DDSF_FLAG_MASK); if(soundId <= 0 || soundId >= defs.sounds.size()) return false; if(sfxVolume <= 0 || volume <= 0) return false; // This won't play... LOG_AS("S_LocalSoundAtVolumeFrom"); if(volume > 1) { LOGDEV_AUDIO_WARNING("Volume is too high (%f > 1)") << volume; } float freq = 1; // This is the sound we're going to play. sfxinfo_t *info = S_GetSoundInfo(soundId, &freq, &volume); if(!info) return false; // Hmm? This ID is not defined. bool const isRepeating = S_IsRepeating(soundIdAndFlags); // Check the distance (if applicable). if(!(info->flags & SF_NO_ATTENUATION) && !(soundIdAndFlags & DDSF_NO_ATTENUATION)) { // If origin is too far, don't even think about playing the sound. coord_t *fixPoint = (origin ? origin->origin : point); if(Mobj_ApproxPointDistance(S_GetListenerMobj(), fixPoint) > soundMaxDist) return false; } // Load the sample. sfxsample_t *sample = Sfx_Cache(soundId); if(!sample) { if(sfxAvail) { LOG_AUDIO_VERBOSE("S_LocalSoundAtVolumeFrom: Caching of sound %i failed") << soundId; } return false; } // Random frequency alteration? (Multipliers chosen to match original // sound code.) if(!noRndPitch) { if(info->flags & SF_RANDOM_SHIFT) { freq += (RNG_RandFloat() - RNG_RandFloat()) * (7.0f / 255); } if(info->flags & SF_RANDOM_SHIFT2) { freq += (RNG_RandFloat() - RNG_RandFloat()) * (15.0f / 255); } } // If the sound has an exclusion group, either all or the same emitter's // iterations of this sound will stop. if(info->group) { mobj_t *emitter = ((info->flags & SF_GLOBAL_EXCLUDE) ? nullptr : origin); Sfx_StopSoundGroup(info->group, emitter); } // Let's play it. int flags = 0; flags |= (((info->flags & SF_NO_ATTENUATION) || (soundIdAndFlags & DDSF_NO_ATTENUATION)) ? SF_NO_ATTENUATION : 0); flags |= (isRepeating ? SF_REPEAT : 0); flags |= ((info->flags & SF_DONT_STOP) ? SF_DONT_STOP : 0); return Sfx_StartSound(sample, volume, freq, origin, point, flags); #else DENG2_UNUSED4(soundIdAndFlags, origin, point, volume); return false; #endif } int S_LocalSoundAtVolume(int soundID, mobj_t *origin, float volume) { return S_LocalSoundAtVolumeFrom(soundID, origin, nullptr, volume); } int S_LocalSound(int soundID, mobj_t *origin) { // Play local sound at max volume. return S_LocalSoundAtVolumeFrom(soundID, origin, nullptr, 1); } int S_LocalSoundFrom(int soundID, coord_t *fixedPos) { return S_LocalSoundAtVolumeFrom(soundID, nullptr, fixedPos, 1); } int S_StartSound(int soundID, mobj_t *origin) { #ifdef __SERVER__ // The sound is audible to everybody. Sv_Sound(soundID, origin, SVSF_TO_ALL); #endif Sfx_StartLogical(soundID, origin, S_IsRepeating(soundID)); return S_LocalSound(soundID, origin); } int S_StartSoundEx(int soundID, mobj_t *origin) { #ifdef __SERVER__ Sv_Sound(soundID, origin, SVSF_TO_ALL | SVSF_EXCLUDE_ORIGIN); #endif Sfx_StartLogical(soundID, origin, S_IsRepeating(soundID)); return S_LocalSound(soundID, origin); } int S_StartSoundAtVolume(int soundID, mobj_t *origin, float volume) { #ifdef __SERVER__ Sv_SoundAtVolume(soundID, origin, volume, SVSF_TO_ALL); #endif Sfx_StartLogical(soundID, origin, S_IsRepeating(soundID)); // The sound is audible to everybody. return S_LocalSoundAtVolume(soundID, origin, volume); } int S_ConsoleSound(int soundID, mobj_t *origin, int targetConsole) { #ifdef __SERVER__ Sv_Sound(soundID, origin, targetConsole); #endif // If it's for us, we can hear it. if(targetConsole == consolePlayer) { S_LocalSound(soundID, origin); } return true; } /** * @param sectorEmitter Sector in which to stop sounds. * @param soundID Unique identifier of the sound to be stopped. * If @c 0, ID not checked. * @param flags @ref soundStopFlags */ static void stopSectorSounds(ddmobj_base_t *sectorEmitter, int soundID, int flags) { if(!sectorEmitter || !flags) return; // Are we stopping with this sector's emitter? if(flags & SSF_SECTOR) { _api_S.StopSound(soundID, (mobj_t *)sectorEmitter); } // Are we stopping with linked emitters? if(!(flags & SSF_SECTOR_LINKED_SURFACES)) return; // Process the rest of the emitter chain. ddmobj_base_t *base = sectorEmitter; while((base = (ddmobj_base_t *)base->thinker.next)) { // Stop sounds from this emitter. _api_S.StopSound(soundID, (mobj_t *)base); } } void S_StopSound(int soundID, mobj_t *emitter) { #ifdef __CLIENT__ // No special stop behavior. // Sfx provides a routine for this. Sfx_StopSound(soundID, emitter); #endif // Notify the LSM. if(Sfx_StopLogical(soundID, emitter)) { #ifdef __SERVER__ // In netgames, the server is responsible for telling clients // when to stop sounds. The LSM will tell us if a sound was // stopped somewhere in the world. Sv_StopSound(soundID, emitter); #endif } } void S_StopSound2(int soundID, mobj_t *emitter, int flags) { // Are we performing any special stop behaviors? if(emitter && flags) { if(emitter->thinker.id) { // Emitter is a real Mobj. stopSectorSounds(&Mobj_Sector(emitter)->soundEmitter(), soundID, flags); return; } // The head of the chain is the sector. Find it. while(emitter->thinker.prev) { emitter = (mobj_t *)emitter->thinker.prev; } stopSectorSounds((ddmobj_base_t *)emitter, soundID, flags); return; } // A regular stop. S_StopSound(soundID, emitter); } int S_IsPlaying(int soundID, mobj_t *emitter) { // The Logical Sound Manager (under Sfx) provides a routine for this. return Sfx_IsPlaying(soundID, emitter); } int S_StartMusicNum(int id, dd_bool looped) { #ifdef __CLIENT__ // Don't play music if the volume is at zero. if(isDedicated) return true; if(id < 0 || id >= defs.musics.size()) return false; Record const *def = &defs.musics[id]; LOG_AUDIO_MSG("Starting music '%s'") << def->gets("id"); return Mus_Start(def, looped); #else DENG2_UNUSED2(id, looped); return false; #endif } int S_StartMusic(char const *musicID, dd_bool looped) { LOG_AS("S_StartMusic"); int idx = Def_GetMusicNum(musicID); if(idx < 0) { if(musicID && qstrlen(musicID)) { LOG_AUDIO_WARNING("Song \"%s\" not defined, cannot start playback") << musicID; } return false; } return S_StartMusicNum(idx, looped); } void S_StopMusic() { #ifdef __CLIENT__ Mus_Stop(); #endif } void S_PauseMusic(dd_bool paused) { #ifdef __CLIENT__ Mus_Pause(paused); #else DENG2_UNUSED(paused); #endif } void S_Drawer() { #ifdef __CLIENT__ if(!showSoundInfo) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // Go into screen projection mode. glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, DENG_GAMEVIEW_WIDTH, DENG_GAMEVIEW_HEIGHT, 0, -1, 1); Sfx_DebugInfo(); // Back to the original. glMatrixMode(GL_PROJECTION); glPopMatrix(); #endif // __CLIENT__ } /** * Console command for playing a (local) sound effect. */ D_CMD(PlaySound) { DENG2_UNUSED(src); if(argc < 2) { LOG_SCR_NOTE("Usage: %s (id) (volume) at (x) (y) (z)") << argv[0]; LOG_SCR_MSG("(volume) must be in 0..1, but may be omitted."); LOG_SCR_MSG("'at (x) (y) (z)' may also be omitted."); LOG_SCR_MSG("The sound is always played locally."); return true; } int p = 0; // The sound ID is always first. int const id = Def_GetSoundNum(argv[1]); // The second argument may be a volume. float volume = 1; if(argc >= 3 && String(argv[2]).compareWithoutCase("at")) { volume = String(argv[2]).toFloat(); p = 3; } else { p = 2; } bool useFixedPos = false; coord_t fixedPos[3]; if(argc >= p + 4 && !String(argv[p]).compareWithoutCase("at")) { useFixedPos = true; fixedPos[VX] = strtod(argv[p + 1], nullptr); fixedPos[VY] = strtod(argv[p + 2], nullptr); fixedPos[VZ] = strtod(argv[p + 3], nullptr); } // Check that the volume is valid. volume = de::clamp(0.f, volume, 1.f); if(de::fequal(volume, 0)) return true; if(useFixedPos) { S_LocalSoundAtVolumeFrom(id, nullptr, fixedPos, volume); } else { S_LocalSoundAtVolume(id, nullptr, volume); } return true; } #ifdef __CLIENT__ static void S_ReverbVolumeChanged() { Sfx_UpdateReverb(); } #endif void S_Register() { C_VAR_BYTE ("sound-overlap-stop", &sfxOneSoundPerEmitter, 0, 0, 1); #ifdef __CLIENT__ C_VAR_INT ("sound-volume", &sfxVolume, 0, 0, 255); C_VAR_INT ("sound-info", &showSoundInfo, 0, 0, 1); C_VAR_INT ("sound-rate", &sfxSampleRate, 0, 11025, 44100); C_VAR_INT ("sound-16bit", &sfx16Bit, 0, 0, 1); C_VAR_INT ("sound-3d", &sfx3D, 0, 0, 1); C_VAR_FLOAT2("sound-reverb-volume", &sfxReverbStrength, 0, 0, 1.5f, S_ReverbVolumeChanged); C_CMD_FLAGS("playsound", nullptr, PlaySound, CMDF_NO_DEDICATED); Mus_Register(); #endif } DENG_DECLARE_API(S) = { { DE_API_SOUND }, S_MapChange, S_LocalSoundAtVolumeFrom, S_LocalSoundAtVolume, S_LocalSound, S_LocalSoundFrom, S_StartSound, S_StartSoundEx, S_StartSoundAtVolume, S_ConsoleSound, S_StopSound, S_StopSound2, S_IsPlaying, S_StartMusic, S_StartMusicNum, S_StopMusic, S_PauseMusic }; doomsday-stable-1.15.7/doomsday/client/src/audio/s_environ.cpp0000664000175000017500000000505112641367670023670 0ustar jaakkojaakko/** @file s_environ.cpp Environmental audio effects. * @ingroup audio * * Calculation of the aural properties of sectors. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "de_audio.h" #include "Sector" #include "audio/s_environ.h" using namespace de; static AudioEnvironment envInfo[1 + NUM_AUDIO_ENVIRONMENTS] = { {"", 0, 0, 0}, {"Metal", 255, 255, 25}, {"Rock", 200, 160, 100}, {"Wood", 80, 50, 200}, {"Cloth", 5, 5, 255} }; char const *S_AudioEnvironmentName(AudioEnvironmentId id) { DENG_ASSERT(id >= AE_NONE && id < NUM_AUDIO_ENVIRONMENTS); return envInfo[1 + int(id)].name; } AudioEnvironment const &S_AudioEnvironment(AudioEnvironmentId id) { DENG_ASSERT(id >= AE_NONE && id < NUM_AUDIO_ENVIRONMENTS); return envInfo[1 + int(id)]; } AudioEnvironmentId S_AudioEnvironmentId(de::Uri const *uri) { if(uri) { for(int i = 0; i < defs.textureEnv.size(); ++i) { ded_tenviron_t const *env = &defs.textureEnv[i]; for(int k = 0; k < env->materials.size(); ++k) { de::Uri *ref = env->materials[k].uri; if(!ref || *ref != *uri) continue; // Is this a known environment? for(int m = 0; m < NUM_AUDIO_ENVIRONMENTS; ++m) { AudioEnvironment const &envInfo = S_AudioEnvironment(AudioEnvironmentId(m)); if(!stricmp(env->id, envInfo.name)) return AudioEnvironmentId(m); } return AE_NONE; } } } return AE_NONE; } doomsday-stable-1.15.7/doomsday/client/src/audio/audiodriver_music.cpp0000664000175000017500000001657212641367670025415 0ustar jaakkojaakko/** @file audiodriver_music.cpp Low-level music interface of the audio driver. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "audio/audiodriver_music.h" #include #include #include #include "dd_main.h" using namespace de; #define BUFFERED_MUSIC_FILE "dd-buffered-song" static bool needBufFileSwitch; static AutoStr *composeBufferedMusicFilename(int id, char const *ext) { if(ext && ext[0]) { return Str_Appendf(AutoStr_NewStd(), "%s%i%s", BUFFERED_MUSIC_FILE, id, ext); } return Str_Appendf(AutoStr_NewStd(), "%s%i", BUFFERED_MUSIC_FILE, id); } static void musicSet(audiointerface_music_t *iMusic, int property, void const *ptr) { audiodriver_t *d = AudioDriver_Interface(iMusic); if(!d || !d->Set) return; d->Set(property, ptr); } static int musicPlayNativeFile(audiointerface_music_t *iMusic, char const *fileName, bool looped) { DENG2_ASSERT(iMusic); if(!iMusic->PlayFile) return 0; return iMusic->PlayFile(fileName, looped); } static int musicPlayLump(audiointerface_music_t *iMusic, lumpnum_t lumpNum, bool looped) { DENG2_ASSERT(iMusic); if(!iMusic->Play || !iMusic->SongBuffer) { // Music interface does not offer buffer playback. // Write this lump to disk and play from there. AutoStr *musicFile = AudioDriver_Music_ComposeTempBufferFilename(nullptr); if(!F_DumpFile(App_FileSystem().lump(lumpNum), Str_Text(musicFile))) { // Failed to write the lump... return 0; } return musicPlayNativeFile(iMusic, Str_Text(musicFile), looped); } // Buffer the data using the driver's facilities. try { std::unique_ptr hndl(&App_FileSystem().openLump(App_FileSystem().lump(lumpNum))); size_t const length = hndl->length(); hndl->read((uint8_t *) iMusic->SongBuffer(length), length); App_FileSystem().releaseFile(hndl->file()); return iMusic->Play(looped); } catch(LumpIndex::NotFoundError const &) {} // Ignore error. return 0; } static int musicPlayFile(audiointerface_music_t *iMusic, char const *virtualOrNativePath, bool looped) { DENG2_ASSERT(iMusic); try { // Relative paths are relative to the native working directory. String path = (NativePath::workPath() / NativePath(virtualOrNativePath).expand()).withSeparators('/'); std::unique_ptr hndl(&App_FileSystem().openFile(path, "rb")); size_t const len = hndl->length(); if(!iMusic->Play || !iMusic->SongBuffer) { // Music interface does not offer buffer playback. // Write to disk and play from there. AutoStr *fileName = AudioDriver_Music_ComposeTempBufferFilename(nullptr); uint8_t *buf = (uint8_t *)M_Malloc(len); hndl->read(buf, len); F_Dump(buf, len, Str_Text(fileName)); M_Free(buf); buf = nullptr; App_FileSystem().releaseFile(hndl->file()); // Music maestro, if you please! return musicPlayNativeFile(iMusic, Str_Text(fileName), looped); } // Music interface offers buffered playback. Use it. hndl->read((uint8_t *) iMusic->SongBuffer(len), len); App_FileSystem().releaseFile(hndl->file()); return iMusic->Play(looped); } catch(FS1::NotFoundError const &) {} // Ignore error. return 0; } static int musicPlayCDTrack(audiointerface_cd_t *iCD, int track, bool looped) { DENG2_ASSERT(iCD); return iCD->Play(track, looped); } static dd_bool musicIsPlaying(audiointerface_music_t *iMusic) { DENG2_ASSERT(iMusic); return iMusic->gen.Get(MUSIP_PLAYING, 0); } void AudioDriver_Music_SwitchBufferFilenames() { needBufFileSwitch = true; } AutoStr *AudioDriver_Music_ComposeTempBufferFilename(char const *ext) { static int currentBufFile = 0; // Switch the name of the buffered song file? if(needBufFileSwitch) { currentBufFile ^= 1; needBufFileSwitch = false; } return composeBufferedMusicFilename(currentBufFile, ext); } void AudioDriver_Music_Set(int property, void const *ptr) { void *ifs[MAX_AUDIO_INTERFACES]; int const count = AudioDriver_FindInterfaces(AUDIO_IMUSIC, ifs); for(int i = 0; i < count; ++i) { musicSet((audiointerface_music_t *) ifs[i], property, ptr); } if(property == AUDIOP_SOUNDFONT_FILENAME) { char const *fn = (char const *) ptr; if(!fn || !fn[0]) return; // No path. if(F_FileExists(fn)) { LOG_AUDIO_MSG("Current soundfont set to: \"%s\"") << fn; } else { LOG_AUDIO_WARNING("Soundfont \"%s\" not found") << fn; } } } int AudioDriver_Music_PlayNativeFile(char const *fileName, dd_bool looped) { void *ifs[MAX_AUDIO_INTERFACES]; int const count = AudioDriver_FindInterfaces(AUDIO_IMUSIC, ifs); for(int i = 0; i < count; ++i) { if(musicPlayNativeFile((audiointerface_music_t *) ifs[i], fileName, looped)) return true; } return false; } int AudioDriver_Music_PlayLump(lumpnum_t lump, dd_bool looped) { void *ifs[MAX_AUDIO_INTERFACES]; int i, count = AudioDriver_FindInterfaces(AUDIO_IMUSIC, ifs); for(i = 0; i < count; ++i) { if(musicPlayLump((audiointerface_music_t *) ifs[i], lump, looped)) return true; } return false; } int AudioDriver_Music_PlayFile(char const *virtualOrNativePath, dd_bool looped) { void *ifs[MAX_AUDIO_INTERFACES]; int const count = AudioDriver_FindInterfaces(AUDIO_IMUSIC, ifs); for(int i = 0; i < count; ++i) { if(musicPlayFile((audiointerface_music_t *) ifs[i], virtualOrNativePath, looped)) return true; } return false; } int AudioDriver_Music_PlayCDTrack(int track, dd_bool looped) { void *ifs[MAX_AUDIO_INTERFACES]; int const count = AudioDriver_FindInterfaces(AUDIO_ICD, ifs); for(int i = 0; i < count; ++i) { if(musicPlayCDTrack((audiointerface_cd_t *) ifs[i], track, looped)) return true; } return false; } dd_bool AudioDriver_Music_IsPlaying() { void *ifs[MAX_AUDIO_INTERFACES]; int const count = AudioDriver_FindInterfaces(AUDIO_IMUSIC_OR_ICD, ifs); for(int i = 0; i < count; ++i) { if(musicIsPlaying((audiointerface_music_t *) ifs[i])) return true; } return false; } doomsday-stable-1.15.7/doomsday/client/src/audio/s_mus.cpp0000664000175000017500000003154712641367670023025 0ustar jaakkojaakko/** @file s_mus.cpp Music subsystem. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifdef __SERVER__ # error "audio" is not available in a SERVER build #endif #include "de_platform.h" #include "clientapp.h" #include "audio/s_mus.h" #include #include #include #include #include #include "dd_main.h" // isDedicated #include "audio/audiodriver.h" #include "audio/audiodriver_music.h" #include "audio/m_mus2midi.h" #include "audio/sys_audio.h" #include "audio/s_main.h" using namespace de; D_CMD(PlayMusic); D_CMD(PauseMusic); D_CMD(StopMusic); static void Mus_UpdateSoundFont(void); static int musPreference = MUSP_EXT; static char* soundFontPath = (char*) ""; static dd_bool musAvail = false; static dd_bool musicPaused = false; static String currentSong; static int getInterfaces(audiointerface_music_generic_t** ifs) { return AudioDriver_FindInterfaces(AUDIO_IMUSIC_OR_ICD, (void**) ifs); } void Mus_Register() { // Variables: C_VAR_INT ("music-volume", &musVolume, 0, 0, 255); C_VAR_INT ("music-source", &musPreference, 0, 0, 2); C_VAR_CHARPTR2("music-soundfont", &soundFontPath, 0, 0, 0, Mus_UpdateSoundFont); // Commands: C_CMD_FLAGS ("playmusic", NULL, PlayMusic, CMDF_NO_DEDICATED); C_CMD_FLAGS ("pausemusic", NULL, PauseMusic, CMDF_NO_DEDICATED); C_CMD_FLAGS ("stopmusic", "", StopMusic, CMDF_NO_DEDICATED); } bool Mus_Init() { // Already initialized? if(musAvail) return true; if(isDedicated || CommandLine_Exists("-nomusic")) { LOG_AUDIO_NOTE("Music disabled"); return true; } LOG_AUDIO_VERBOSE("Initializing Music subsystem..."); // Let's see which interfaces are available for music playback. audiointerface_music_generic_t *iMusic[MAX_AUDIO_INTERFACES]; int count = getInterfaces(iMusic); currentSong = ""; // No interfaces for Music playback? if(!count) return false; // Initialize each interface. for(int i = 0; i < count; ++i) { if(!iMusic[i]->Init()) { LOG_AUDIO_WARNING("Failed to initialize %s for music playback") << Str_Text(AudioDriver_InterfaceName(iMusic[i])); } } // Tell the audio driver about our soundfont config. Mus_UpdateSoundFont(); musAvail = true; return true; } void Mus_Shutdown() { if(!musAvail) return; musAvail = false; // Shutdown interfaces. audiointerface_music_generic_t *iMusic[MAX_AUDIO_INTERFACES]; int count = getInterfaces(iMusic); for(int i = 0; i < count; ++i) { if(iMusic[i]->Shutdown) iMusic[i]->Shutdown(); } } void Mus_StartFrame() { if(!musAvail) return; // Update all interfaces. audiointerface_music_generic_t *iMusic[MAX_AUDIO_INTERFACES]; int count = getInterfaces(iMusic); for(int i = 0; i < count; ++i) { iMusic[i]->Update(); } } void Mus_SetVolume(float vol) { if(!musAvail) return; // Set volume of all available interfaces. audiointerface_music_generic_t *iMusic[MAX_AUDIO_INTERFACES]; int count = getInterfaces(iMusic); for(int i = 0; i < count; ++i) { iMusic[i]->Set(MUSIP_VOLUME, vol); } } void Mus_Pause(bool doPause) { if(!musAvail) return; // Pause all interfaces. audiointerface_music_generic_t *iMusic[MAX_AUDIO_INTERFACES]; int count = getInterfaces(iMusic); for(int i = 0; i < count; ++i) { iMusic[i]->Pause(doPause); } } void Mus_Stop() { if(!musAvail) return; currentSong = ""; // Stop all interfaces. audiointerface_music_generic_t *iMusic[MAX_AUDIO_INTERFACES]; int count = getInterfaces(iMusic); for(int i = 0; i < count; ++i) { iMusic[i]->Stop(); } } /** * @return: @c true, if the specified lump contains a MUS song. */ static bool Mus_IsMUSLump(lumpnum_t lumpNum) { try { char buf[4]; App_FileSystem().lump(lumpNum).read((uint8_t *)buf, 0, 4); // ASCII "MUS" and CTRL-Z (hex 4d 55 53 1a) return !strncmp(buf, "MUS\x01a", 4); } catch(LumpIndex::NotFoundError const&) {} // Ignore error. return false; } /** * Check for the existence of an "external" music file. * Songs can be either in external files or non-MUS lumps. * * @return Non-zero if an external file of that name exists. */ static int Mus_GetExt(Record const *rec, ddstring_t *retPath) { LOG_AS("Mus_GetExt"); if(!musAvail || !AudioDriver_Music_Available() || !rec) return false; defn::Music musicDef(*rec); de::Uri songUri(musicDef.gets("path"), RC_NULL); if(!songUri.path().isEmpty()) { // All external music files are specified relative to the base path. String fullPath = App_BasePath() / songUri.path(); if(F_Access(fullPath.toUtf8().constData())) { if(retPath) Str_Set(retPath, fullPath.toUtf8().constData()); return true; } LOG_AUDIO_WARNING("Music file \"%s\" not found (id '%s')") << songUri << musicDef.gets("id"); } // Try the resource locator? String const lumpName = musicDef.gets("lumpName"); if(!lumpName.isEmpty()) { try { String foundPath = App_FileSystem().findPath(de::Uri(lumpName, RC_MUSIC), RLF_DEFAULT, App_ResourceClass(RC_MUSIC)); foundPath = App_BasePath() / foundPath; // Ensure the path is absolute. // Does the caller want to know the matched path? if(retPath) { Str_Set(retPath, foundPath.toUtf8().constData()); } return true; } catch(FS1::NotFoundError const&) {} // Ignore this error. } return false; } /** * @return The track number if successful else zero. */ static int Mus_GetCD(Record const *rec) { if(!musAvail || !AudioDriver_CD() || !rec) return 0; defn::Music musicDef(*rec); int cdTrack = musicDef.geti("cdTrack"); if(cdTrack) return cdTrack; String path = musicDef.gets("path"); if(!path.compareWithoutCase("cd")) { bool ok; cdTrack = path.toInt(&ok); if(ok) return cdTrack; } return 0; } int Mus_StartLump(lumpnum_t lumpNum, bool looped, bool canPlayMUS) { if(!AudioDriver_Music_Available() || lumpNum < 0) return 0; if(Mus_IsMUSLump(lumpNum)) { // Lump is in DOOM's MUS format. We must first convert it to MIDI. if(!canPlayMUS) return -1; AutoStr *srcFile = AudioDriver_Music_ComposeTempBufferFilename(".mid"); // Read the lump, convert to MIDI and output to a temp file in the // working directory. Use a filename with the .mid extension so that // any player which relies on the it for format recognition works as // expected. File1 &lump = App_FileSystem().lump(lumpNum); uint8_t *buf = (uint8_t *) M_Malloc(lump.size()); lump.read(buf, 0, lump.size()); M_Mus2Midi((void *)buf, lump.size(), Str_Text(srcFile)); M_Free(buf); return AudioDriver_Music_PlayNativeFile(Str_Text(srcFile), looped); } else { return AudioDriver_Music_PlayLump(lumpNum, looped); } } int Mus_Start(Record const *rec, bool looped) { if(!musAvail || !rec) return false; String songID = rec->gets("id"); LOG_AS("Mus_Start"); LOG_AUDIO_VERBOSE("Starting ID:%s looped:%b, currentSong ID:%s") << songID << looped << currentSong; // We will not restart the currently playing song. if(songID == currentSong && AudioDriver_Music_IsPlaying()) { return false; } // Stop the currently playing song. Mus_Stop(); AudioDriver_Music_SwitchBufferFilenames(); // This is the song we're playing now. currentSong = songID; // Choose the order in which to try to start the song. int order[3]; order[0] = musPreference; switch(musPreference) { case MUSP_CD: order[1] = MUSP_EXT; order[2] = MUSP_MUS; break; case MUSP_EXT: order[1] = MUSP_MUS; order[2] = MUSP_CD; break; default: // MUSP_MUS order[1] = MUSP_EXT; order[2] = MUSP_CD; break; } // Try to start the song. ddstring_t path; for(int i = 0; i < 3; ++i) { bool canPlayMUS = true; switch(order[i]) { case MUSP_CD: if(Mus_GetCD(rec)) { if(AudioDriver_Music_PlayCDTrack(Mus_GetCD(rec), looped)) return true; } break; case MUSP_EXT: Str_Init(&path); if(Mus_GetExt(rec, &path)) { LOG_AUDIO_VERBOSE("Attempting to play song '%s' (file \"%s\")") << rec->gets("id") << NativePath(Str_Text(&path)).pretty(); // Its an external file. if(AudioDriver_Music_PlayFile(Str_Text(&path), looped)) return true; } // Next, try non-MUS lumps. canPlayMUS = false; // Note: Intentionally falls through to MUSP_MUS. case MUSP_MUS: if(AudioDriver_Music_Available()) { lumpnum_t const lumpNum = App_FileSystem().lumpNumForName(rec->gets("lumpName")); if(Mus_StartLump(lumpNum, looped, canPlayMUS) == 1) return true; } break; default: DENG2_ASSERT(!"Mus_Start: Invalid value for order[i]"); break; } } // No song was started. return false; } static void Mus_UpdateSoundFont() { de::NativePath path(soundFontPath); #ifdef MACOSX // On OS X we can try to use the basic DLS soundfont that's part of CoreAudio. if(path.isEmpty()) { path = "/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls"; } #endif AudioDriver_Music_Set(AUDIOP_SOUNDFONT_FILENAME, path.expand().toString().toLatin1().constData()); } /** * CCmd: Play a music track. */ D_CMD(PlayMusic) { DENG2_UNUSED(src); LOG_AS("playmusic (Cmd)"); if(!musAvail) { LOGDEV_SCR_ERROR("Music subsystem is not available"); return false; } switch(argc) { default: LOG_SCR_NOTE("Usage:\n %s (music-def)") << argv[0]; LOG_SCR_MSG(" %s lump (lumpname)") << argv[0]; LOG_SCR_MSG(" %s file (filename)") << argv[0]; LOG_SCR_MSG(" %s cd (track)") << argv[0]; break; case 2: { int musIdx = Def_GetMusicNum(argv[1]); if(musIdx < 0) { LOG_RES_WARNING("Music '%s' not defined") << argv[1]; return false; } Mus_Start(&defs.musics[musIdx], true); break; } case 3: if(!stricmp(argv[1], "lump")) { lumpnum_t lump = App_FileSystem().lumpNumForName(argv[2]); if(lump < 0) return false; // No such lump. Mus_Stop(); return AudioDriver_Music_PlayLump(lump, true); } else if(!stricmp(argv[1], "file")) { Mus_Stop(); return AudioDriver_Music_PlayFile(argv[2], true); } else { // Perhaps a CD track? if(!stricmp(argv[1], "cd")) { if(!AudioDriver_CD()) { LOG_AUDIO_WARNING("No CD audio interface available"); return false; } Mus_Stop(); return AudioDriver_Music_PlayCDTrack(atoi(argv[2]), true); } } break; } return true; } D_CMD(StopMusic) { DENG2_UNUSED3(src, argc, argv); Mus_Stop(); return true; } D_CMD(PauseMusic) { DENG2_UNUSED3(src, argc, argv); musicPaused = !musicPaused; Mus_Pause(musicPaused); return true; } doomsday-stable-1.15.7/doomsday/client/src/audio/audiodriver.cpp0000664000175000017500000004445712641367670024220 0ustar jaakkojaakko/** @file audiodriver.cpp Audio driver loading and interface management. * @ingroup audio * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "de_console.h" #include "de_misc.h" #include "de_audio.h" #include "audio/sys_audio.h" #include #include typedef struct driver_s { Library* library; audiodriver_t interface; audiointerface_sfx_t sfx; audiointerface_music_t music; audiointerface_cd_t cd; } driver_t; static driver_t drivers[AUDIODRIVER_COUNT]; static const char* driverIdentifier[AUDIODRIVER_COUNT] = { "dummy", "sdlmixer", "openal", "fmod", "fluidsynth", "dsound", "winmm" }; // The active/loaded interfaces. typedef struct audiointerface_s { audiointerfacetype_t type; union { void* any; audiointerface_sfx_t* sfx; audiointerface_music_t* music; audiointerface_cd_t* cd; } i; /** * @todo The audio interface could also declare which audio formats it is * capable of playing (e.g., MIDI only, CD tracks only). */ } audiointerface_t; static audiointerface_t activeInterfaces[MAX_AUDIO_INTERFACES]; #ifdef MACOSX /// Built-in QuickTime audio interface implemented by MusicPlayer.m DENG_EXTERN_C audiointerface_music_t audiodQuickTimeMusic; #endif static void importInterfaces(driver_t* d) { de::Library& lib = Library_File(d->library).library(); lib.setSymbolPtr( d->interface.Init, "DS_Init"); lib.setSymbolPtr( d->interface.Shutdown, "DS_Shutdown"); lib.setSymbolPtr( d->interface.Event, "DS_Event"); lib.setSymbolPtr( d->interface.Set, "DS_Set", de::Library::OptionalSymbol); if(lib.hasSymbol("DS_SFX_Init")) { lib.setSymbolPtr( d->sfx.gen.Init, "DS_SFX_Init"); lib.setSymbolPtr( d->sfx.gen.Create, "DS_SFX_CreateBuffer"); lib.setSymbolPtr( d->sfx.gen.Destroy, "DS_SFX_DestroyBuffer"); lib.setSymbolPtr( d->sfx.gen.Load, "DS_SFX_Load"); lib.setSymbolPtr( d->sfx.gen.Reset, "DS_SFX_Reset"); lib.setSymbolPtr( d->sfx.gen.Play, "DS_SFX_Play"); lib.setSymbolPtr( d->sfx.gen.Stop, "DS_SFX_Stop"); lib.setSymbolPtr( d->sfx.gen.Refresh, "DS_SFX_Refresh"); lib.setSymbolPtr( d->sfx.gen.Set, "DS_SFX_Set"); lib.setSymbolPtr( d->sfx.gen.Setv, "DS_SFX_Setv"); lib.setSymbolPtr( d->sfx.gen.Listener, "DS_SFX_Listener"); lib.setSymbolPtr( d->sfx.gen.Listenerv, "DS_SFX_Listenerv"); lib.setSymbolPtr( d->sfx.gen.Getv, "DS_SFX_Getv", de::Library::OptionalSymbol); } if(lib.hasSymbol("DM_Music_Init")) { lib.setSymbolPtr( d->music.gen.Init, "DM_Music_Init"); lib.setSymbolPtr( d->music.gen.Update, "DM_Music_Update"); lib.setSymbolPtr( d->music.gen.Get, "DM_Music_Get"); lib.setSymbolPtr( d->music.gen.Set, "DM_Music_Set"); lib.setSymbolPtr( d->music.gen.Pause, "DM_Music_Pause"); lib.setSymbolPtr( d->music.gen.Stop, "DM_Music_Stop"); lib.setSymbolPtr( d->music.SongBuffer, "DM_Music_SongBuffer", de::Library::OptionalSymbol); lib.setSymbolPtr( d->music.Play, "DM_Music_Play", de::Library::OptionalSymbol); lib.setSymbolPtr( d->music.PlayFile, "DM_Music_PlayFile", de::Library::OptionalSymbol); } if(lib.hasSymbol("DM_CDAudio_Init")) { lib.setSymbolPtr( d->cd.gen.Init, "DM_CDAudio_Init"); lib.setSymbolPtr( d->cd.gen.Update, "DM_CDAudio_Update"); lib.setSymbolPtr( d->cd.gen.Set, "DM_CDAudio_Set"); lib.setSymbolPtr( d->cd.gen.Get, "DM_CDAudio_Get"); lib.setSymbolPtr( d->cd.gen.Pause, "DM_CDAudio_Pause"); lib.setSymbolPtr( d->cd.gen.Stop, "DM_CDAudio_Stop"); lib.setSymbolPtr( d->cd.Play, "DM_CDAudio_Play"); } } static int audioPluginFinder(void* libFile, const char* fileName, const char* absPath, void* ptr) { de::LibraryFile* lib = reinterpret_cast(libFile); Str* path = (Str*) ptr; DENG2_UNUSED(fileName); if(lib->hasUnderscoreName(Str_Text(path))) { Str_Set(path, absPath); return true; } return false; // Keep looking... } static AutoStr* findAudioPluginPath(const char* name) { AutoStr* path = AutoStr_FromText(name); if(Library_IterateAvailableLibraries(audioPluginFinder, path)) { // The full path of the library was returned in @a path. return path; } return 0; } static dd_bool loadAudioDriver(driver_t* driver, const char* name) { LOG_AS("loadAudioDriver"); dd_bool ok = false; if(name && name[0]) { AutoStr* libPath = findAudioPluginPath(name); // Load the audio driver library and import symbols. if(libPath && (driver->library = Library_New(Str_Text(libPath))) != 0) { importInterfaces(driver); ok = true; } else { LOG_AUDIO_WARNING("Loading of \"%s\" failed") << name; } } return ok; } static const char* getDriverName(audiodriverid_t id) { static const char* audioDriverNames[AUDIODRIVER_COUNT] = { /* AUDIOD_DUMMY */ "Dummy", /* AUDIOD_SDL_MIXER */ "SDLMixer", /* AUDIOD_OPENAL */ "OpenAL", /* AUDIOD_FMOD */ "FMOD", /* AUDIOD_FLUIDSYNTH */ "FluidSynth", /* AUDIOD_DSOUND */ "DirectSound", // Win32 only /* AUDIOD_WINMM */ "Windows Multimedia" // Win32 only }; if(VALID_AUDIODRIVER_IDENTIFIER(id)) return audioDriverNames[id]; DENG2_ASSERT(!"S_GetDriverName: Unknown driver id"); return ""; // Unreachable. } static audiodriverid_t identifierToDriverId(const char* name) { for(int i = 0; i < AUDIODRIVER_COUNT; ++i) { if(!stricmp(name, driverIdentifier[i])) return (audiodriverid_t) i; } LOG_AUDIO_ERROR("'%s' is not a valid audio driver name") << name; return AUDIOD_INVALID; } static dd_bool isDriverInited(audiodriverid_t id) { if(!VALID_AUDIODRIVER_IDENTIFIER(id)) return false; return drivers[id].interface.Init != 0; } /** * Initializes the audio driver interfaces. * * @return @c true iff successful. */ static dd_bool initDriver(audiodriverid_t id) { driver_t* d = &drivers[id]; DENG2_ASSERT(VALID_AUDIODRIVER_IDENTIFIER(id)); assert(!isDriverInited(id)); memset(d, 0, sizeof(*d)); switch(id) { case AUDIOD_DUMMY: // built-in memcpy(&d->interface, &audiod_dummy, sizeof(d->interface)); memcpy(&d->sfx, &audiod_dummy_sfx, sizeof(d->sfx)); break; #ifndef DENG_DISABLE_SDLMIXER case AUDIOD_SDL_MIXER: // built-in memcpy(&d->interface, &audiod_sdlmixer, sizeof(d->interface)); memcpy(&d->sfx, &audiod_sdlmixer_sfx, sizeof(d->sfx)); memcpy(&d->music, &audiod_sdlmixer_music, sizeof(d->music)); break; #endif case AUDIOD_OPENAL: if(!loadAudioDriver(d, "openal")) return false; break; case AUDIOD_FMOD: if(!loadAudioDriver(d, "fmod")) return false; break; case AUDIOD_FLUIDSYNTH: if(!loadAudioDriver(d, "fluidsynth")) return false; break; #ifdef WIN32 case AUDIOD_DSOUND: if(!loadAudioDriver(d, "directsound")) return false; break; case AUDIOD_WINMM: if(!loadAudioDriver(d, "winmm")) return false; break; #endif default: DENG2_ASSERT(!"initDriver: Unknown audio driver id"); return false; } // All loaded drivers are automatically initialized so they are ready for use. assert(d->interface.Init != 0); return d->interface.Init(); } /** * Chooses the default audio driver based on configuration options. */ static audiodriverid_t chooseAudioDriver(void) { // No audio output? if(isDedicated || CommandLine_Exists("-dummy")) return AUDIOD_DUMMY; if(CommandLine_Exists("-fmod")) return AUDIOD_FMOD; if(CommandLine_Exists("-oal") || CommandLine_Exists("-openal")) return AUDIOD_OPENAL; #ifdef WIN32 // DirectSound with 3D sound support, EAX effects? if(CommandLine_Exists("-dsound")) return AUDIOD_DSOUND; // Windows Multimedia? if(CommandLine_Exists("-winmm")) return AUDIOD_WINMM; #endif #ifndef DENG_DISABLE_SDLMIXER if(CommandLine_Exists("-sdlmixer")) return AUDIOD_SDL_MIXER; #endif // The default audio driver. return AUDIOD_FMOD; } static audiodriverid_t initDriverIfNeeded(const char* identifier) { audiodriverid_t drvId = identifierToDriverId(identifier); if(!isDriverInited(drvId)) { initDriver(drvId); } DENG2_ASSERT(VALID_AUDIODRIVER_IDENTIFIER(drvId)); return drvId; } static void appendInterface(audiointerface_t** pos, audiointerfacetype_t type, void* ptr) { (*pos)->type = type; (*pos)->i.any = ptr; (*pos)++; } /** * Choose the SFX, Music, and CD audio interfaces to use. * * @param defaultDriverId Default audio driver to use unless overridden. */ static void selectInterfaces(audiodriverid_t defaultDriverId) { driver_t* defaultDriver = &drivers[defaultDriverId]; audiodriverid_t drvId; audiointerface_t* pos = activeInterfaces; int p; // The default driver goes on the bottom of the stack. if(defaultDriver->sfx.gen.Init) appendInterface(&pos, AUDIO_ISFX, &defaultDriver->sfx); if(defaultDriver->music.gen.Init) { appendInterface(&pos, AUDIO_IMUSIC, &defaultDriver->music); } #ifdef MACOSX else if(defaultDriverId != AUDIOD_DUMMY) { // On the Mac, use the built-in QuickTime interface as the fallback for music. appendInterface(&pos, AUDIO_IMUSIC, &audiodQuickTimeMusic); } #endif #ifndef WIN32 // At the moment, dsFMOD supports streaming samples so we can // automatically load dsFluidSynth for MIDI music. if(defaultDriverId == AUDIOD_FMOD) { initDriverIfNeeded("fluidsynth"); if(isDriverInited(AUDIOD_FLUIDSYNTH)) { appendInterface(&pos, AUDIO_IMUSIC, &drivers[AUDIOD_FLUIDSYNTH].music); } } #endif if(defaultDriver->cd.gen.Init) appendInterface(&pos, AUDIO_ICD, &defaultDriver->cd); for(p = 1; p < CommandLine_Count() - 1 && pos < activeInterfaces + MAX_AUDIO_INTERFACES; p++) { if(!CommandLine_IsOption(p)) continue; // Check for SFX override. if(CommandLine_IsMatchingAlias("-isfx", CommandLine_At(p))) { drvId = initDriverIfNeeded(CommandLine_At(++p)); if(!drivers[drvId].sfx.gen.Init) { throw de::Error("selectInterfaces", QString("Audio driver '%1' does not provide an SFX interface") .arg(getDriverName(drvId))); } appendInterface(&pos, AUDIO_ISFX, &drivers[drvId].sfx); continue; } // Check for Music override. if(CommandLine_IsMatchingAlias("-imusic", CommandLine_At(p))) { drvId = initDriverIfNeeded(CommandLine_At(++p)); if(!drivers[drvId].music.gen.Init) { throw de::Error("selectInterfaces", QString("Audio driver '%1' does not provide a Music interface") .arg(getDriverName(drvId))); } appendInterface(&pos, AUDIO_IMUSIC, &drivers[drvId].music); continue; } // Check for CD override. if(CommandLine_IsMatchingAlias("-icd", CommandLine_At(p))) { drvId = initDriverIfNeeded(CommandLine_At(++p)); if(!drivers[drvId].cd.gen.Init) { throw de::Error("selectInterfaces", QString("Audio driver '%1' does not provide a CD interface") .arg(getDriverName(drvId))); } appendInterface(&pos, AUDIO_ICD, &drivers[drvId].cd); continue; } } AudioDriver_PrintInterfaces(); // Let the music driver(s) know of the primary sfx interface, in case they // want to play audio through it. AudioDriver_Music_Set(AUDIOP_SFX_INTERFACE, AudioDriver_SFX()); } de::String AudioDriver_InterfaceDescription() { de::String str; QTextStream os(&str); os << _E(b) "Audio configuration:\n" _E(.); for(int i = MAX_AUDIO_INTERFACES - 1; i >= 0; --i) { audiointerface_t* a = &activeInterfaces[i]; if(a->type == AUDIO_IMUSIC || a->type == AUDIO_ICD) { os << _E(Ta) _E(l) " " << (a->type == AUDIO_IMUSIC? "Music" : "CD") << ": " << _E(.) _E(Tb) << Str_Text(AudioDriver_InterfaceName(a->i.any)) << "\n"; } else if(a->type == AUDIO_ISFX) { os << _E(Ta) _E(l) << " SFX: " << _E(.) _E(Tb) << Str_Text(AudioDriver_InterfaceName(a->i.sfx)) << "\n"; } } return str.rightStrip(); } void AudioDriver_PrintInterfaces(void) { LOG_AUDIO_MSG("%s") << AudioDriver_InterfaceDescription(); } /* static dd_bool initInterface(audiointerface_base_t* interface) { if(!interface) return true; if(interface->Init) { return interface->Init(); } return false; } */ dd_bool AudioDriver_Init(void) { audiodriverid_t defaultDriverId; dd_bool ok = false; memset(activeInterfaces, 0, sizeof(activeInterfaces)); if(CommandLine_Exists("-nosound")) return false; defaultDriverId = chooseAudioDriver(); ok = initDriver(defaultDriverId); if(!ok) { LOG_AUDIO_WARNING("Failed initializing audio driver \"%s\"") << getDriverName(defaultDriverId); } // Fallback option for the default driver. #ifndef DENG_DISABLE_SDLMIXER if(!ok) { defaultDriverId = AUDIOD_SDL_MIXER; ok = initDriver(defaultDriverId); } #endif if(ok) { // Choose the interfaces to use. selectInterfaces(defaultDriverId); } return ok; } void AudioDriver_Shutdown(void) { int i; // Shut down all the loaded drivers. (Note: reverse order) for(i = AUDIODRIVER_COUNT - 1; i >= 0; --i) { driver_t* d = &drivers[i]; if(d->interface.Shutdown) d->interface.Shutdown(); } // Unload the plugins after everything has been shut down. for(i = 0; i < AUDIODRIVER_COUNT; ++i) { driver_t* d = &drivers[i]; if(d->library) { Library_Delete(d->library); } memset(d, 0, sizeof(*d)); } // No more interfaces available. memset(activeInterfaces, 0, sizeof(activeInterfaces)); } audiodriver_t* AudioDriver_Interface(void* anyAudioInterface) { int i; for(i = 0; i < AUDIODRIVER_COUNT; ++i) { driver_t* d = &drivers[i]; if((void*)&d->sfx == anyAudioInterface || (void*)&d->music == anyAudioInterface || (void*)&d->cd == anyAudioInterface) { return &d->interface; } } return 0; } int AudioDriver_FindInterfaces(audiointerfacetype_t type, void** listOfInterfaces) { int i, count = 0; // Least important interfaces are listed first in the stack. for(i = MAX_AUDIO_INTERFACES - 1; i >= 0; --i) { if(activeInterfaces[i].type == type || (type == AUDIO_IMUSIC_OR_ICD && (activeInterfaces[i].type == AUDIO_IMUSIC || activeInterfaces[i].type == AUDIO_ICD))) { if(listOfInterfaces) { *listOfInterfaces++ = activeInterfaces[i].i.any; } ++count; } } return count; } audiointerface_sfx_generic_t* AudioDriver_SFX(void) { void* ifs[MAX_AUDIO_INTERFACES]; if(!AudioDriver_FindInterfaces(AUDIO_ISFX, ifs)) return 0; // No such interface loaded. // The primary interface is the first one returned. return (audiointerface_sfx_generic_t*) ifs[0]; } dd_bool AudioDriver_Music_Available(void) { return AudioDriver_FindInterfaces(AUDIO_IMUSIC, NULL) > 0; } audiointerface_cd_t* AudioDriver_CD(void) { void* ifs[MAX_AUDIO_INTERFACES]; if(!AudioDriver_FindInterfaces(AUDIO_ICD, ifs)) return 0; // No such interface loaded. // The primary interface is the first one returned. return (audiointerface_cd_t*) ifs[0]; } audiointerfacetype_t AudioDriver_InterfaceType(void* anyAudioInterface) { int i; for(i = 0; i < AUDIODRIVER_COUNT; ++i) { driver_t* d = &drivers[i]; if((void*)&d->sfx == anyAudioInterface) return AUDIO_ISFX; if((void*)&d->music == anyAudioInterface) return AUDIO_IMUSIC; if((void*)&d->cd == anyAudioInterface) return AUDIO_ICD; } return AUDIO_INONE; } AutoStr* AudioDriver_InterfaceName(void* anyAudioInterface) { int i; for(i = 0; i < AUDIODRIVER_COUNT; ++i) { driver_t* d = &drivers[i]; if((void*)&d->sfx == anyAudioInterface) { /// @todo SFX interfaces can't be named yet. return AutoStr_FromText(getDriverName(audiodriverid_t(i))); } if((void*)&d->music == anyAudioInterface || (void*)&d->cd == anyAudioInterface) { char buf[256]; /// @todo This could easily overflow... audiointerface_music_generic_t* gen = (audiointerface_music_generic_t*) anyAudioInterface; if(gen->Get(MUSIP_ID, buf)) { return AutoStr_FromTextStd(buf); } else { return AutoStr_FromText("[MUSIP_ID not defined]"); } } } return AutoStr_FromText("[invalid audio interface]"); } doomsday-stable-1.15.7/doomsday/client/src/mesh.cpp0000664000175000017500000000675012641367670021530 0ustar jaakkojaakko/** @file mesh.cpp Mesh Geometry Data Structure. * * @authors Copyright © 2008-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include "HEdge" #include "Face" #include "Vertex" #include "mesh.h" namespace de { DENG2_PIMPL_NOREF(Mesh::Element) { Mesh *mesh = nullptr; ///< Owner of the element. MapElement *mapElement = nullptr; ///< Attributed MapElement if any (not owned). }; Mesh::Element::Element(Mesh &mesh) : d(new Instance) { d->mesh = &mesh; } Mesh &Mesh::Element::mesh() const { DENG2_ASSERT(d->mesh); return *d->mesh; } bool Mesh::Element::hasMapElement() const { return d->mapElement != nullptr; } MapElement &Mesh::Element::mapElement() { if(d->mapElement) return *d->mapElement; /// @throw MissingMapElement Attempted with no map element attributed. throw MissingMapElementError("Mesh::Element::mapElement", "No map element is attributed"); } MapElement const &Mesh::Element::mapElement() const { return const_cast(this)->mapElement(); } void Mesh::Element::setMapElement(MapElement const *newMapElement) { d->mapElement = const_cast(newMapElement); } DENG2_PIMPL_NOREF(Mesh) { Vertexs vertexs; ///< All vertexs in the mesh. HEdges hedges; ///< All half-edges in the mesh. Faces faces; ///< All faces in the mesh. }; Mesh::Mesh() : d(new Instance) {} Mesh::~Mesh() { clear(); } void Mesh::clear() { qDeleteAll(d->vertexs); d->vertexs.clear(); qDeleteAll(d->hedges); d->hedges.clear(); qDeleteAll(d->faces); d->faces.clear(); } Vertex *Mesh::newVertex(Vector2d const &origin) { Vertex *vtx = new Vertex(*this, origin); d->vertexs.append(vtx); return vtx; } HEdge *Mesh::newHEdge(Vertex &vertex) { HEdge *hedge = new HEdge(*this, vertex); d->hedges.append(hedge); return hedge; } Face *Mesh::newFace() { Face *face = new Face(*this); d->faces.append(face); return face; } void Mesh::removeVertex(Vertex &vertex) { int sizeBefore = d->vertexs.size(); d->vertexs.removeOne(&vertex); if(sizeBefore != d->vertexs.size()) { delete &vertex; } } void Mesh::removeHEdge(HEdge &hedge) { int sizeBefore = d->hedges.size(); d->hedges.removeOne(&hedge); if(sizeBefore != d->hedges.size()) { delete &hedge; } } void Mesh::removeFace(Face &face) { int sizeBefore = d->faces.size(); d->faces.removeOne(&face); if(sizeBefore != d->faces.size()) { delete &face; } } Mesh::Vertexs const &Mesh::vertexs() const { return d->vertexs; } Mesh::Faces const &Mesh::faces() const { return d->faces; } Mesh::HEdges const &Mesh::hedges() const { return d->hedges; } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/dd_plugin.cpp0000664000175000017500000002200412641367670022527 0ustar jaakkojaakko/** @file dd_plugin.cpp Plugin subsystem. * @ingroup base * * @todo Convert to C++, rename. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2009-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #define DENG_NO_API_MACROS_PLUGIN #include "de_platform.h" #include "api_plugin.h" #include "de_console.h" #include "de_defs.h" #include "dd_main.h" #include "dd_pinit.h" #include "library.h" #ifdef __CLIENT__ # include "updater/downloaddialog.h" #endif #include #include #include #define HOOKMASK(x) ((x) & 0xffffff) using namespace de; struct hookreg_t { int exclude; struct { hookfunc_t func; pluginid_t pluginId; } list[MAX_HOOKS]; /// @todo Remove arbitrary MAX_HOOKS. }; typedef ::Library *PluginHandle; static ::Library *hInstPlug[MAX_PLUGS]; /// @todo Remove arbitrary MAX_PLUGS. static hookreg_t hooks[NUM_HOOK_TYPES]; struct ThreadState { pluginid_t currentPlugin; ThreadState() : currentPlugin(0) {} }; #ifndef DENG2_QT_4_8_OR_NEWER // Qt 4.7 requires a pointer as the local data type #define DENG_LOCAL_DATA_POINTER static QThreadStorage pluginState; ///< Thread-local plugin state. static void initLocalData() { if(!pluginState.hasLocalData()) pluginState.setLocalData(new ThreadState); } #else static QThreadStorage pluginState; ///< Thread-local plugin state. #endif static PluginHandle *findFirstUnusedPluginHandle() { for(int i = 0; i < MAX_PLUGS; ++i) { if(!hInstPlug[i]) { return &hInstPlug[i]; } } return 0; // none available } static int loadPlugin(void * /*libraryFile*/, char const *fileName, char const *pluginPath, void *) { typedef void (*PluginInitializer)(void); DENG_UNUSED(fileName); DENG2_ASSERT(fileName != 0 && fileName[0]); DENG2_ASSERT(pluginPath != 0 && pluginPath[0]); if(strcasestr("/bin/audio_", pluginPath)) { // Do not touch audio plugins at this point. return true; } ::Library *plugin = Library_New(pluginPath); if(!plugin) { #ifdef UNIX String const fn = Path(pluginPath).fileName(); if(fn.contains("libfmodex") || fn.contains("libassimp")) { // No need to warn about these shared libs. return 0; } #endif LOG_RES_WARNING("Failed to load \"%s\": %s") << pluginPath << Library_LastError(); return 0; // Continue iteration. } if(!strcmp(Library_Type(plugin), "deng-plugin/audio")) { // Audio plugins will be loaded later, on demand. Library_Delete(plugin); return 0; } PluginInitializer initializer = de::function_cast(Library_Symbol(plugin, "DP_Initialize")); if(!initializer) { LOG_RES_WARNING("Cannot load plugin \"%s\": no entrypoint called 'DP_Initialize'") << pluginPath; // Clearly not a Doomsday plugin. Library_Delete(plugin); return 0; // Continue iteration. } // Assign a handle and ID to the plugin. PluginHandle *handle = findFirstUnusedPluginHandle(); pluginid_t plugId = handle - hInstPlug + 1; if(!handle) { LOG_RES_WARNING("Cannot load \"%s\": too many plugins loaded already loaded") << pluginPath; Library_Delete(plugin); return 0; // Continue iteration. } // This seems to be a Doomsday plugin. LOGDEV_MSG("Plugin id:%i name:%s") << plugId << String(pluginPath).fileNameWithoutExtension(); *handle = plugin; DD_SetActivePluginId(plugId); initializer(); DD_SetActivePluginId(0); return 0; // Continue iteration. } static bool unloadPlugin(PluginHandle *handle) { DENG2_ASSERT(handle != 0); if(!*handle) return false; Library_Delete(*handle); *handle = 0; return true; } void Plug_LoadAll() { LOG_RES_VERBOSE("Initializing plugins..."); Library_IterateAvailableLibraries(loadPlugin, 0); } void Plug_UnloadAll() { for(int i = 0; i < MAX_PLUGS && hInstPlug[i]; ++i) { unloadPlugin(&hInstPlug[i]); } } LibraryFile const &Plug_FileForPlugin(pluginid_t id) { DENG2_ASSERT(id > 0 && id <= MAX_PLUGS); return Library_File(hInstPlug[id - 1]); } #undef Plug_AddHook DENG_EXTERN_C int Plug_AddHook(int hookType, hookfunc_t hook) { int const type = HOOKMASK(hookType); // The current plugin must be set before calling this. The engine has the // responsibility to call DD_SetActivePluginId() whenever it passes control // to a plugin, and then set it back to zero after it gets control back. DENG2_ASSERT(DD_ActivePluginId() != 0); // The type must be good. if(type < 0 || type >= NUM_HOOK_TYPES) return false; // Exclusive hooks. if(hookType & HOOKF_EXCLUSIVE) { hooks[type].exclude = true; std::memset(hooks[type].list, 0, sizeof(hooks[type].list)); } else if(hooks[type].exclude) { // An exclusive hook has closed down this list. return false; } int i; for(i = 0; i < MAX_HOOKS && hooks[type].list[i].func; ++i) {}; if(i == MAX_HOOKS) return false; // No more hooks allowed! // Add the hook. If the plugin is unidentified the ID will be zero. hooks[type].list[i].func = hook; hooks[type].list[i].pluginId = DD_ActivePluginId(); return true; } #undef Plug_RemoveHook DENG_EXTERN_C int Plug_RemoveHook(int hookType, hookfunc_t hook) { int const type = HOOKMASK(hookType); // The type must be good. if(type < 0 || type >= NUM_HOOK_TYPES) return false; for(int i = 0; i < MAX_HOOKS; ++i) { if(hooks[type].list[i].func != hook) continue; hooks[type].list[i].func = 0; hooks[type].list[i].pluginId = 0; if(hookType & HOOKF_EXCLUSIVE) { // Exclusive hook removed; allow normal hooks. hooks[type].exclude = false; } return true; } return false; } #undef Plug_CheckForHook DENG_EXTERN_C int Plug_CheckForHook(int hookType) { for(int i = 0; i < MAX_HOOKS; ++i) { if(hooks[hookType].list[i].func) return true; } return false; } void DD_SetActivePluginId(pluginid_t id) { #ifdef DENG_LOCAL_DATA_POINTER initLocalData(); pluginState.localData()->currentPlugin = id; #else pluginState.localData().currentPlugin = id; #endif } pluginid_t DD_ActivePluginId() { #ifdef DENG_LOCAL_DATA_POINTER initLocalData(); return pluginState.localData()->currentPlugin; #else return pluginState.localData().currentPlugin; #endif } int DD_CallHooks(int hookType, int parm, void *data) { int ret = 0; bool allGood = true; pluginid_t oldPlugin = DD_ActivePluginId(); // Try all the hooks. for(int i = 0; i < MAX_HOOKS; ++i) { if(!hooks[hookType].list[i].func) continue; DD_SetActivePluginId(hooks[hookType].list[i].pluginId); if(hooks[hookType].list[i].func(hookType, parm, data)) { // One hook executed; return nonzero from this routine. ret = 1; } else { allGood = false; } } DD_SetActivePluginId(oldPlugin); if(ret && allGood) ret |= 2; return ret; } void *DD_FindEntryPoint(pluginid_t pluginId, char const *fn) { int const plugIndex = pluginId - 1; DENG2_ASSERT(plugIndex >= 0 && plugIndex < MAX_PLUGS); void *addr = Library_Symbol(hInstPlug[plugIndex], fn); if(!addr) { LOGDEV_RES_WARNING("Error getting address of \"%s\": %s") << fn << Library_LastError(); } return addr; } #undef Plug_Notify DENG_EXTERN_C void Plug_Notify(int notification, void *) { #ifdef __CLIENT__ switch(notification) { case DD_NOTIFY_GAME_SAVED: // If an update has been downloaded and is ready to go, we should // re-show the dialog now that the user has saved the game as prompted. LOG_DEBUG("Plug_Notify: Game saved"); DownloadDialog::showCompletedDownload(); break; } #else DENG2_UNUSED(notification); #endif } DENG_DECLARE_API(Plug) = { { DE_API_PLUGIN }, Plug_AddHook, Plug_RemoveHook, Plug_CheckForHook, Plug_Notify }; doomsday-stable-1.15.7/doomsday/client/src/def_main.cpp0000664000175000017500000020520312641367670022330 0ustar jaakkojaakko/** @file def_main.cpp Definition subsystem. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2005-2015 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #define DENG_NO_API_MACROS_DEFINITIONS #include "def_main.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dd_main.h" #include "dd_def.h" #include "api_def.h" #include "api_plugin.h" #include "api_sound.h" #include "xgclass.h" #include "Generator" #ifdef __CLIENT__ # include "render/rend_particle.h" #endif #include "resource/manifest.h" #include "resource/materialdetaillayer.h" #include "resource/materialshinelayer.h" #include "resource/materialtexturelayer.h" #ifdef __CLIENT__ # include "resource/materiallightdecoration.h" #endif using namespace de; #define LOOPi(n) for(i = 0; i < (n); ++i) #define LOOPk(n) for(k = 0; k < (n); ++k) struct actionlink_t { char *name; ///< Name of the routine. void (*func)(); ///< Pointer to the function. }; ded_t defs; // The main definitions database. RuntimeDefs runtimeDefs; static bool defsInited; static mobjinfo_t *gettingFor; static xgclass_t nullXgClassLinks; ///< Used when none defined. static xgclass_t *xgClassLinks; static inline FS1 &fileSys() { return App_FileSystem(); } static inline ResourceSystem &resSys() { return App_ResourceSystem(); } void RuntimeDefs::clear() { for(int i = 0; i < sounds.size(); ++i) { Str_Free(&sounds[i].external); } sounds.clear(); sprNames.clear(); mobjInfo.clear(); states.clear(); texts.clear(); stateInfo.clear(); } int Def_GetGameClasses() { xgClassLinks = nullptr; if(gx.GetVariable) { xgClassLinks = (xgclass_t *) gx.GetVariable(DD_XGFUNC_LINK); } if(!xgClassLinks) { de::zap(nullXgClassLinks); xgClassLinks = &nullXgClassLinks; } // Let the parser know of the XG classes. DED_SetXGClassLinks(xgClassLinks); return 1; } void Def_Init() { runtimeDefs.clear(); defs.clear(); // Make the definitions visible in the global namespace. App::app().scriptSystem().addNativeModule("Defs", defs.names); } void Def_Destroy() { App::app().scriptSystem().removeNativeModule("Defs"); defs.clear(); // Destroy the databases. runtimeDefs.clear(); defsInited = false; } spritenum_t Def_GetSpriteNum(String const &name) { return Def_GetSpriteNum(name.toLatin1()); } spritenum_t Def_GetSpriteNum(char const *name) { if(name && name[0]) { for(int i = 0; i < runtimeDefs.sprNames.size(); ++i) { if(!qstricmp(runtimeDefs.sprNames[i].name, name)) return i; } } return -1; // Not found. } int Def_GetMobjNum(char const *id) { return defs.getMobjNum(id); } int Def_GetMobjNumForName(char const *name) { return defs.getMobjNumForName(name); } char const *Def_GetMobjName(int num) { return defs.getMobjName(num); } state_t *Def_GetState(int num) { if(num >= 0 && num < defs.states.size()) { return &runtimeDefs.states[num]; } return nullptr; // Not found. } int Def_GetStateNum(char const *id) { return defs.getStateNum(id); } int Def_GetModelNum(char const *id) { return defs.getModelNum(id); } int Def_GetSoundNum(char const *id) { return defs.getSoundNum(id); } int Def_GetMusicNum(char const *id) { return defs.getMusicNum(id); } acfnptr_t Def_GetActionPtr(char const *name) { if(!name || !name[0]) return nullptr; if(!App_GameLoaded()) return nullptr; // Action links are provided by the game, who owns the actual action functions. auto *links = (actionlink_t *) gx.GetVariable(DD_ACTION_LINK); for(actionlink_t *linkIt = links; linkIt && linkIt->name; linkIt++) { actionlink_t *link = linkIt; if(!qstricmp(name, link->name)) return link->func; } return nullptr; } int Def_GetActionNum(char const *name) { if(name && name[0] && App_GameLoaded()) { // Action links are provided by the game, who owns the actual action functions. auto *links = (actionlink_t *) gx.GetVariable(DD_ACTION_LINK); for(actionlink_t *linkIt = links; linkIt && linkIt->name; linkIt++) { actionlink_t *link = linkIt; if(!qstricmp(name, link->name)) return linkIt - links; } } return -1; // Not found. } ded_value_t *Def_GetValueById(char const *id) { return defs.getValueById(id); } ded_value_t *Def_GetValueByUri(struct uri_s const *_uri) { if(!_uri) return nullptr; return defs.getValueByUri(*reinterpret_cast(_uri)); } ded_compositefont_t *Def_GetCompositeFont(char const *uri) { return defs.getCompositeFont(uri); } /// @todo $revise-texture-animation static ded_reflection_t *tryFindReflection(de::Uri const &uri, /* bool hasExternal,*/ bool isCustom) { for(int i = defs.reflections.size() - 1; i >= 0; i--) { ded_reflection_t *def = &defs.reflections[i]; if(def->material && *def->material == uri) { // Is this suitable? if(Def_IsAllowedReflection(def, /*hasExternal,*/ isCustom)) return def; } } return nullptr; // None found. } /// @todo $revise-texture-animation static ded_detailtexture_t *tryFindDetailTexture(de::Uri const &uri, /*bool hasExternal,*/ bool isCustom) { for(int i = defs.details.size() - 1; i >= 0; i--) { ded_detailtexture_t *def = &defs.details[i]; if(def->material1 && *def->material1 == uri) { // Is this suitable? if(Def_IsAllowedDetailTex(def, /*hasExternal,*/ isCustom)) return def; } if(def->material2 && *def->material2 == uri) { // Is this suitable? if(Def_IsAllowedDetailTex(def, /*hasExternal,*/ isCustom)) return def; } } return nullptr; // Not found. } ded_ptcgen_t *Def_GetGenerator(de::Uri const &uri) { if(uri.isEmpty()) return nullptr; for(int i = 0; i < defs.ptcGens.size(); ++i) { ded_ptcgen_t *def = &defs.ptcGens[i]; if(!def->material) continue; // Is this suitable? if(*def->material == uri) return def; #if 0 /// @todo $revise-texture-animation if(def->flags & PGF_GROUP) { /** * Generator triggered by all materials in the (animation) group. * A search is necessary only if we know both the used material and * the specified material in this definition are in *a* group. */ if(Material_IsGroupAnimated(defMat) && Material_IsGroupAnimated(mat) && &Material_AnimGroup(defMat) == &Material_AnimGroup(mat)) { // Both are in this group! This def will do. return def; } } #endif } return nullptr; // None found. } ded_ptcgen_t *Def_GetGenerator(uri_s const *uri) { if(!uri) return nullptr; return Def_GetGenerator(reinterpret_cast(*uri)); } ded_ptcgen_t *Def_GetDamageGenerator(int mobjType) { // Search for a suitable definition. for(int i = 0; i < defs.ptcGens.size(); ++i) { ded_ptcgen_t *def = &defs.ptcGens[i]; // It must be for this type of mobj. if(def->damageNum == mobjType) return def; } return nullptr; } #undef Def_EvalFlags int Def_EvalFlags(char const *ptr) { return defs.evalFlags2(ptr); } /** * The following escape sequences are un-escaped: *
 *     \\n   Newline
 *     \\r   Carriage return
 *     \\t   Tab
 *     \\\_   Space
 *     \\s   Space
 * 
*/ static void Def_InitTextDef(ddtext_t *txt, char const *str) { DENG2_ASSERT(txt); // Handle null pointers with "". if(!str) str = ""; txt->text = (char *) M_Calloc(qstrlen(str) + 1); char const *in = str; char *out = txt->text; for(; *in; out++, in++) { if(*in == '\\') { in++; if(*in == 'n') *out = '\n'; // Newline. else if(*in == 'r') *out = '\r'; // Carriage return. else if(*in == 't') *out = '\t'; // Tab. else if(*in == '_' || *in == 's') *out = ' '; // Space. else { *out = *in; } } else { *out = *in; } } // Adjust buffer to fix exactly. txt->text = (char *) M_Realloc(txt->text, qstrlen(txt->text) + 1); } /** * Prints a count with a 2-space indentation. */ static String defCountMsg(int count, String const &label) { if(!verbose && !count) return ""; // Don't print zeros if not verbose. return String(_E(Ta) " %1 " _E(Tb) "%2\n").arg(count).arg(label); } /** * Read all DD_DEFNS lumps in the primary lump index. */ static void Def_ReadLumpDefs() { LOG_AS("Def_ReadLumpDefs"); LumpIndex const &lumpIndex = fileSys().nameIndex(); LumpIndex::FoundIndices foundDefns; lumpIndex.findAll("DD_DEFNS.lmp", foundDefns); DENG2_FOR_EACH_CONST(LumpIndex::FoundIndices, i, foundDefns) { if(!DED_ReadLump(&defs, *i)) { QByteArray path = NativePath(lumpIndex[*i].container().composePath()).pretty().toUtf8(); App_Error("Def_ReadLumpDefs: Parse error reading \"%s:DD_DEFNS\".\n", path.constData()); } } int const numProcessedLumps = foundDefns.size(); if(verbose && numProcessedLumps > 0) { LOG_RES_NOTE("Processed %i %s") << numProcessedLumps << (numProcessedLumps != 1 ? "lumps" : "lump"); } } /** * Uses gettingFor. Initializes the state-owners information. */ int Def_StateForMobj(char const *state) { int num = Def_GetStateNum(state); if(num < 0) num = 0; // State zero is the NULL state. if(num > 0) { runtimeDefs.stateInfo[num].owner = gettingFor; // Scan forward at most 'count' states, or until we hit a state with // an owner, or the NULL state. int st, count = 16; for(st = runtimeDefs.states[num].nextState; st > 0 && count-- && !runtimeDefs.stateInfo[st].owner; st = runtimeDefs.states[st].nextState) { runtimeDefs.stateInfo[st].owner = gettingFor; } } return num; } int Def_GetIntValue(char *val, int *returned_val) { // First look for a DED Value char *data; if(Def_Get(DD_DEF_VALUE, val, &data) >= 0) { if(returned_val) *returned_val = strtol(data, 0, 0); return true; } // Convert the literal string if(returned_val) *returned_val = strtol(val, 0, 0); return false; } static void readDefinitionFile(String path) { if(path.isEmpty()) return; LOG_RES_VERBOSE("Reading \"%s\"") << NativePath(path).pretty(); Def_ReadProcessDED(&defs, path); } /** * Attempt to prepend the current work path. If @a src is already absolute do nothing. * * @param dst Absolute path written here. * @param src Original path. */ static void prependWorkPath(ddstring_t *dst, ddstring_t const *src) { DENG2_ASSERT(dst && src); if(!F_IsAbsolute(src)) { char *curPath = Dir_CurrentPath(); Str_Prepend(dst, curPath); Dir_CleanPathStr(dst); free(curPath); return; } // Do we need to copy anyway? if(dst != src) { Str_Set(dst, Str_Text(src)); } } /** * Returns a URN list (in load order) for all lumps whose name matches the pattern "MAPINFO.lmp". */ static QStringList allMapInfoUrns() { QStringList foundPaths; // The game's main MAPINFO definitions should be processed first. bool ignoreNonCustom = false; try { String mainMapInfo = fileSys().findPath(de::Uri(App_CurrentGame().mainMapInfo()), RLF_MATCH_EXTENSION); if(!mainMapInfo.isEmpty()) { foundPaths << mainMapInfo; ignoreNonCustom = true; } } catch(FS1::NotFoundError &) {} // Ignore this error. // Process all other lumps named MAPINFO.lmp LumpIndex const &lumpIndex = fileSys().nameIndex(); LumpIndex::FoundIndices foundLumps; lumpIndex.findAll("MAPINFO.lmp", foundLumps); for(auto const &lumpNumber : foundLumps) { // Ignore MAPINFO definition data in IWADs? if(ignoreNonCustom) { File1 const &file = lumpIndex[lumpNumber]; /// @todo Custom status for contained files is not inherited from the container? if(file.isContained()) { if(!file.container().hasCustom()) continue; } else if(!file.hasCustom()) continue; } foundPaths << String("LumpIndex:%1").arg(lumpNumber); } return foundPaths; } /** * @param mapInfoUrns MAPINFO definitions to translate, in load order. */ static void translateMapInfos(QStringList const &mapInfoUrns, String &xlat, String &xlatCustom) { xlat.clear(); xlatCustom.clear(); String delimitedPaths = mapInfoUrns.join(";"); if(delimitedPaths.isEmpty()) return; ddhook_mapinfo_convert_t parm; Str_InitStd(&parm.paths); Str_InitStd(&parm.translated); Str_InitStd(&parm.translatedCustom); try { Str_Set(&parm.paths, delimitedPaths.toUtf8().constData()); if(DD_CallHooks(HOOK_MAPINFO_CONVERT, 0, &parm)) { xlat = Str_Text(&parm.translated); xlatCustom = Str_Text(&parm.translatedCustom); } } catch(...) {} Str_Free(&parm.translatedCustom); Str_Free(&parm.translated); Str_Free(&parm.paths); } static void readAllDefinitions() { Time begunAt; /* * Start with engine's own top-level definition file. */ /* String foundPath = fileSys().findPath(de::Uri("doomsday.ded", RC_DEFINITION), RLF_DEFAULT, App_ResourceClass(RC_DEFINITION)); foundPath = App_BasePath() / foundPath; // Ensure the path is absolute. readDefinitionFile(foundPath); */ readDefinitionFile(App::packageLoader().package("net.dengine.base").root() .locate("defs/doomsday.ded").path()); if(App_GameLoaded()) { de::Game &game = App_CurrentGame(); // Some games use definitions that are translated to DED. QStringList mapInfoUrns = allMapInfoUrns(); if(!mapInfoUrns.isEmpty()) { String xlat, xlatCustom; translateMapInfos(mapInfoUrns, xlat, xlatCustom); if(!xlat.isEmpty()) { LOGDEV_MAP_VERBOSE("Non-custom translated MAPINFO definitions:\n") << xlat; if(!DED_ReadData(&defs, xlat.toUtf8().constData(), "[TranslatedMapInfos]", false /*not custom*/)) { App_Error("readAllDefinitions: DED parse error:\n%s", DED_Error()); } } if(!xlatCustom.isEmpty()) { LOGDEV_MAP_VERBOSE("Custom translated MAPINFO definitions:\n") << xlatCustom; if(!DED_ReadData(&defs, xlatCustom.toUtf8().constData(), "[TranslatedMapInfos]", true /*custom*/)) { App_Error("readAllDefinitions: DED parse error:\n%s", DED_Error()); } } } // Now any startup definition files required by the game. Game::Manifests const &gameResources = game.manifests(); int packageIdx = 0; for(Game::Manifests::const_iterator i = gameResources.find(RC_DEFINITION); i != gameResources.end() && i.key() == RC_DEFINITION; ++i, ++packageIdx) { ResourceManifest &record = **i; /// Try to locate this resource now. QString const &path = record.resolvedPath(true/*try to locate*/); if(path.isEmpty()) { QByteArray names = record.names().join(";").toUtf8(); App_Error("readAllDefinitions: Error, failed to locate required game definition \"%s\".", names.constData()); } readDefinitionFile(path); } // Next are definition files in the games' /auto directory. if(!CommandLine_Exists("-noauto")) { FS1::PathList foundPaths; if(fileSys().findAllPaths(de::Uri("$(App.DefsPath)/$(GamePlugin.Name)/auto/*.ded", RC_NULL).resolved(), 0, foundPaths)) { foreach(FS1::PathListItem const &found, foundPaths) { // Ignore directories. if(found.attrib & A_SUBDIR) continue; readDefinitionFile(found.path); } } } } // Next are any definition files specified on the command line. AutoStr *buf = AutoStr_NewStd(); for(int p = 0; p < CommandLine_Count(); ++p) { char const *arg = CommandLine_At(p); if(!CommandLine_IsMatchingAlias("-def", arg) && !CommandLine_IsMatchingAlias("-defs", arg)) continue; while(++p != CommandLine_Count() && !CommandLine_IsOption(p)) { char const *searchPath = CommandLine_PathAt(p); Str_Clear(buf); Str_Set(buf, searchPath); F_FixSlashes(buf, buf); F_ExpandBasePath(buf, buf); // We must have an absolute path. If we still do not have one then // prepend the current working directory if necessary. prependWorkPath(buf, buf); readDefinitionFile(String(Str_Text(buf))); } p--; /* For ArgIsOption(p) necessary, for p==Argc() harmless */ } // Last are DD_DEFNS definition lumps from loaded add-ons. /// @todo Shouldn't these be processed before definitions on the command line? Def_ReadLumpDefs(); LOG_RES_VERBOSE("readAllDefinitions: Completed in %.2f seconds") << begunAt.since(); } static void defineFlaremap(de::Uri const &resourceUri) { if(resourceUri.isEmpty()) return; // Reference to none? if(!resourceUri.path().toStringRef().compareWithoutCase("-")) return; // Reference to a "built-in" flaremap? String const &resourcePathStr = resourceUri.path().toStringRef(); if(resourcePathStr.length() == 1 && resourcePathStr.first() >= '0' && resourcePathStr.first() <= '4') return; resSys().defineTexture("Flaremaps", resourceUri); } static void defineLightmap(de::Uri const &resourceUri) { if(resourceUri.isEmpty()) return; // Reference to none? if(!resourceUri.path().toStringRef().compareWithoutCase("-")) return; resSys().defineTexture("Lightmaps", resourceUri); } static void generateMaterialDefForTexture(TextureManifest const &manifest) { LOG_AS("generateMaterialDefForTexture"); Record &mat = defs.materials[defs.addMaterial()]; mat.set("autoGenerated", true); de::Uri const texUri = manifest.composeUri(); mat.set("id", de::Uri(DD_MaterialSchemeNameForTextureScheme(texUri.scheme()), texUri.path()).compose()); if(manifest.hasTexture()) { Texture &tex = manifest.texture(); mat.set("dimensions", new ArrayValue(tex.dimensions())); mat.set("flags", int(tex.isFlagged(Texture::NoDraw)? MATF_NO_DRAW : 0)); } else { LOGDEV_RES_MSG("Texture \"%s\" not yet defined, resultant Material will inherit dimensions") << texUri; } // The first layer and stage is implicit. defn::Material matDef(mat); defn::MaterialLayer layerDef(matDef.addLayer()); Record &st0 = layerDef.addStage(); st0.set("texture", texUri.compose()); // Is there an animation for this? AnimGroup const *anim = resSys().animGroupForTexture(manifest); if(anim && anim->frameCount() > 1) { // Determine the start frame. int startFrame = 0; while(&anim->frame(startFrame).textureManifest() != &manifest) { startFrame++; } // Just animate the first in the sequence? if(startFrame && (anim->flags() & AGF_FIRST_ONLY)) return; // Complete configuration of the first stage. AnimGroupFrame const &animFrame0 = anim->frame(startFrame); st0.set("tics", int( animFrame0.tics() + animFrame0.randomTics()) ); if(animFrame0.randomTics()) { st0.set("variance", animFrame0.randomTics() / st0.getf("tics")); } // Add further stages according to the animation group. startFrame++; for(int i = 0; i < anim->frameCount() - 1; ++i) { AnimGroupFrame const &animFrame = anim->frame(de::wrap(startFrame + i, 0, anim->frameCount())); TextureManifest const &frameManifest = animFrame.textureManifest(); Record &st = layerDef.addStage(); st.set("texture", frameManifest.composeUrn().compose()); st.set("tics", int( animFrame.tics() + animFrame.randomTics() )); if(animFrame.randomTics()) { st.set("variance", animFrame.randomTics() / st.getf("tics")); } } } } static void generateMaterialDefsForAllTexturesInScheme(TextureScheme &scheme) { PathTreeIterator iter(scheme.index().leafNodes()); while(iter.hasNext()) generateMaterialDefForTexture(iter.next()); } static inline void generateMaterialDefsForAllTexturesInScheme(String const &schemeName) { generateMaterialDefsForAllTexturesInScheme(resSys().textureScheme(schemeName)); } static void generateMaterialDefs() { generateMaterialDefsForAllTexturesInScheme("Textures"); generateMaterialDefsForAllTexturesInScheme("Flats"); generateMaterialDefsForAllTexturesInScheme("Sprites"); } #ifdef __CLIENT__ /** * Returns @c true iff @a decorDef is compatible with the specified context. */ static bool decorationIsCompatible(Record const &decorDef, de::Uri const &textureUri, bool materialIsCustom) { if(de::Uri(decorDef.gets("texture"), RC_NULL) != textureUri) return false; if(materialIsCustom) { return (decorDef.geti("flags") & DCRF_PWAD) != 0; } return (decorDef.geti("flags") & DCRF_NO_IWAD) == 0; } /** * (Re)Decorate the given @a material according to definition @a def. Any existing * decorations will be cleared in the process. * * @param material The material being (re)decorated. * @param def Definition to apply. */ static void redecorateMaterial(Material &material, Record const &def) { defn::Material matDef(def); material.clearAllDecorations(); // Prefer decorations defined within the material. for(int i = 0; i < matDef.decorationCount(); ++i) { defn::MaterialDecoration decorDef(matDef.decoration(i)); for(int k = 0; k < decorDef.stageCount(); ++k) { Record const &st = decorDef.stage(k); defineLightmap(de::Uri(st.gets("lightmapUp"), RC_NULL)); defineLightmap(de::Uri(st.gets("lightmapDown"), RC_NULL)); defineLightmap(de::Uri(st.gets("lightmapSide"), RC_NULL)); defineFlaremap(de::Uri(st.gets("haloTexture"), RC_NULL)); } material.addDecoration(MaterialLightDecoration::fromDef(decorDef.def())); } if(material.hasDecorations()) return; // Perhaps old style linked decoration definitions? if(material.layerCount()) { // The animation configuration of layer0 determines decoration animation. auto const &decorationsByTexture = defs.decorations.lookup("texture").elements(); MaterialTextureLayer const &layer0 = material.layer(0).as(); bool haveDecorations = false; QVector stageDecorations(layer0.stageCount()); for(int i = 0; i < layer0.stageCount(); ++i) { MaterialTextureLayer::AnimationStage const &stage = layer0.stage(i); try { TextureManifest &texManifest = resSys().textureManifest(de::Uri(stage.gets("texture"), RC_NULL)); de::Uri const texUri = texManifest.composeUri(); for(auto const &pair : decorationsByTexture) { Record const &rec = *pair.second->as().record(); if(decorationIsCompatible(rec, texUri, material.manifest().isCustom())) { stageDecorations[i] = &rec; haveDecorations = true; break; } } } catch(ResourceSystem::MissingManifestError const &) {} // Ignore this error } if(!haveDecorations) return; for(int i = 0; i < layer0.stageCount(); ++i) { if(!stageDecorations[i]) continue; defn::Decoration mainDef(*stageDecorations[i]); for(int k = 0; k < mainDef.lightCount(); ++k) { defn::MaterialDecoration decorDef(mainDef.light(k)); DENG2_ASSERT(decorDef.stageCount() == 1); // sanity check. std::unique_ptr decor( new MaterialLightDecoration(Vector2i(decorDef.geta("patternSkip")), Vector2i(decorDef.geta("patternOffset")), false /*don't use interpolation*/)); std::unique_ptr definedDecorStage( MaterialLightDecoration::AnimationStage::fromDef(decorDef.stage(0))); definedDecorStage->tics = layer0.stage(i).tics; for(int m = 0; m < i; ++m) { MaterialLightDecoration::AnimationStage preStage(*definedDecorStage); preStage.tics = layer0.stage(m).tics; preStage.color = Vector3f(); decor->addStage(preStage); // makes a copy. } decor->addStage(*definedDecorStage); for(int m = i + 1; m < layer0.stageCount(); ++m) { MaterialLightDecoration::AnimationStage postStage(*definedDecorStage); postStage.tics = layer0.stage(m).tics; postStage.color = Vector3f(); decor->addStage(postStage); } material.addDecoration(decor.release()); // takes ownership. } } } } #endif // __CLIENT__ static ded_group_t *findGroupForMaterialLayerAnimation(de::Uri const &uri) { if(uri.isEmpty()) return nullptr; // Reverse iteration (later defs override earlier ones). for(int i = defs.groups.size(); i--> 0; ) { ded_group_t &grp = defs.groups[i]; // We aren't interested in precache groups. if(grp.flags & AGF_PRECACHE) continue; // Or empty/single-frame groups. if(grp.members.size() < 2) continue; // The referenced material must be a member. if(!grp.tryFindFirstMemberWithMaterial(uri)) continue; // Only consider groups where each frame has a valid duration. int k; for(k = 0; k < grp.members.size(); ++k) { if(grp.members[k].tics < 0) break; } if(k < grp.members.size()) continue; // Found a suitable Group. return &grp; } return nullptr; // Not found. } static void configureMaterial(Material &mat, Record const &definition) { defn::Material matDef(definition); de::Uri const materialUri(matDef.gets("id"), RC_NULL); // Reconfigure basic properties. mat.setDimensions(Vector2i(matDef.geta("dimensions"))); mat.markDontDraw((matDef.geti("flags") & MATF_NO_DRAW) != 0); mat.markSkyMasked((matDef.geti("flags") & MATF_SKYMASK) != 0); #ifdef __CLIENT__ mat.setAudioEnvironment(S_AudioEnvironmentId(&materialUri)); #endif // Reconfigure the layers. mat.clearAllLayers(); for(int i = 0; i < matDef.layerCount(); ++i) { mat.addLayerAt(MaterialTextureLayer::fromDef(matDef.layer(i)), mat.layerCount()); } if(mat.layerCount() && mat.layer(0).stageCount()) { MaterialTextureLayer &layer0 = mat.layer(0).as(); MaterialTextureLayer::AnimationStage &stage0 = layer0.stage(0); if(!stage0.gets("texture").isEmpty()) { // We may need to interpret the layer animation from the now // deprecated Group definitions. if(matDef.getb("autoGenerated") && layer0.stageCount() == 1) { de::Uri const textureUri(stage0.gets("texture"), RC_NULL); // Possibly; see if there is a compatible definition with // a member named similarly to the texture for layer #0. if(ded_group_t const *grp = findGroupForMaterialLayerAnimation(textureUri)) { // Determine the start frame. int startFrame = 0; while(!grp->members[startFrame].material || *grp->members[startFrame].material != textureUri) { startFrame++; } // Configure the first stage. ded_group_member_t const &gm0 = grp->members[startFrame]; stage0.tics = gm0.tics; stage0.variance = de::max(gm0.randomTics, 0) / float( gm0.tics ); // Add further stages for each frame in the group. startFrame++; for(int i = 0; i < grp->members.size() - 1; ++i) { int const frame = de::wrap(startFrame + i, 0, grp->members.size()); ded_group_member_t const &gm = grp->members[frame]; if(gm.material) { int const tics = gm.tics; float const variance = de::max(gm.randomTics, 0) / float( gm.tics ); layer0.addStage(MaterialTextureLayer::AnimationStage(*gm.material, tics, variance)); } } } } // Are there Detail definitions we need to produce a layer for? MaterialDetailLayer *dlayer = nullptr; for(int i = 0; i < layer0.stageCount(); ++i) { MaterialTextureLayer::AnimationStage &stage = layer0.stage(i); ded_detailtexture_t const *detailDef = tryFindDetailTexture(de::Uri(stage.gets("texture"), RC_NULL), /*UNKNOWN VALUE,*/ mat.manifest().isCustom()); if(!detailDef || !detailDef->stage.texture) continue; if(!dlayer) { // Add a new detail layer. mat.addLayerAt(dlayer = MaterialDetailLayer::fromDef(*detailDef), 0); } else { // Add a new stage. try { TextureManifest &texture = resSys().textureScheme("Details").findByResourceUri(*detailDef->stage.texture); dlayer->addStage(MaterialDetailLayer::AnimationStage(texture.composeUri(), stage.tics, stage.variance, detailDef->stage.scale, detailDef->stage.strength, detailDef->stage.maxDistance)); if(dlayer->stageCount() == 2) { // Update the first stage with timing info. MaterialTextureLayer::AnimationStage const &stage0 = layer0.stage(0); MaterialTextureLayer::AnimationStage &dstage0 = dlayer->stage(0); dstage0.tics = stage0.tics; dstage0.variance = stage0.variance; } } catch(ResourceSystem::MissingManifestError const &) {} // Ignore this error. } } // Are there Reflection definition we need to produce a layer for? MaterialShineLayer *slayer = nullptr; for(int i = 0; i < layer0.stageCount(); ++i) { MaterialTextureLayer::AnimationStage &stage = layer0.stage(i); ded_reflection_t const *shineDef = tryFindReflection(de::Uri(stage.gets("texture"), RC_NULL), /*UNKNOWN VALUE,*/ mat.manifest().isCustom()); if(!shineDef || !shineDef->stage.texture) continue; if(!slayer) { // Add a new shine layer. mat.addLayerAt(slayer = MaterialShineLayer::fromDef(*shineDef), mat.layerCount()); } else { // Add a new stage. try { TextureManifest &texture = resSys().textureScheme("Reflections") .findByResourceUri(*shineDef->stage.texture); TextureManifest *maskTexture = nullptr; if(shineDef->stage.maskTexture) { try { maskTexture = &resSys().textureScheme("Masks") .findByResourceUri(*shineDef->stage.maskTexture); } catch(ResourceSystem::MissingManifestError const &) {} // Ignore this error. } slayer->addStage(MaterialShineLayer::AnimationStage(texture.composeUri(), stage.tics, stage.variance, maskTexture->composeUri(), shineDef->stage.blendMode, shineDef->stage.shininess, Vector3f(shineDef->stage.minColor), Vector2f(shineDef->stage.maskWidth, shineDef->stage.maskHeight))); if(slayer->stageCount() == 2) { // Update the first stage with timing info. MaterialTextureLayer::AnimationStage const &stage0 = layer0.stage(0); MaterialTextureLayer::AnimationStage &sstage0 = slayer->stage(0); sstage0.tics = stage0.tics; sstage0.variance = stage0.variance; } } catch(ResourceSystem::MissingManifestError const &) {} // Ignore this error. } } } } #ifdef __CLIENT__ redecorateMaterial(mat, definition); #endif // At this point we know the material is usable. mat.markValid(); } static void interpretMaterialDef(Record const &definition) { LOG_AS("interpretMaterialDef"); defn::Material matDef(definition); de::Uri const materialUri(matDef.gets("id"), RC_NULL); try { // Create/retrieve a manifest for the would-be material. MaterialManifest *manifest = &resSys().declareMaterial(materialUri); // Update manifest classification: manifest->setFlags(MaterialManifest::AutoGenerated, matDef.getb("autoGenerated")? SetFlags : UnsetFlags); manifest->setFlags(MaterialManifest::Custom, UnsetFlags); if(matDef.layerCount()) { defn::MaterialLayer layerDef(matDef.layer(0)); if(layerDef.stageCount() > 0) { de::Uri const textureUri(layerDef.stage(0).gets("texture"), RC_NULL); try { TextureManifest &texManifest = resSys().textureManifest(textureUri); if(texManifest.hasTexture() && texManifest.texture().isFlagged(Texture::Custom)) { manifest->setFlags(MaterialManifest::Custom); } } catch(ResourceSystem::MissingManifestError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING("Ignoring unknown texture \"%s\" in Material \"%s\" (layer 0 stage 0): %s") << textureUri << materialUri << er.asText(); } } } // (Re)configure the material. /// @todo Defer until necessary. configureMaterial(*manifest->derive(), definition); } catch(ResourceSystem::UnknownSchemeError const &er) { LOG_RES_WARNING("Failed to declare material \"%s\": %s") << materialUri << er.asText(); } catch(MaterialScheme::InvalidPathError const &er) { LOG_RES_WARNING("Failed to declare material \"%s\": %s") << materialUri << er.asText(); } } static void invalidateAllMaterials() { resSys().forAllMaterials([] (Material &material) { material.markValid(false); return LoopContinue; }); } #ifdef __CLIENT__ static void clearFontDefinitionLinks() { for(AbstractFont *font : resSys().allFonts()) { if(CompositeBitmapFont *compFont = font->maybeAs()) { compFont->setDefinition(nullptr); } } } #endif // __CLIENT__ void Def_Read() { LOG_AS("Def_Read"); if(defsInited) { // We've already initialized the definitions once. // Get rid of everything. FS1::Scheme &scheme = fileSys().scheme(App_ResourceClass("RC_MODEL").defaultScheme()); scheme.reset(); invalidateAllMaterials(); #ifdef __CLIENT__ clearFontDefinitionLinks(); #endif Def_Destroy(); } // Now we can clear all existing definitions and re-init. defs.clear(); // Generate definitions. generateMaterialDefs(); // Read all definitions files and lumps. LOG_RES_MSG("Parsing definition files..."); readAllDefinitions(); // Any definition hooks? DD_CallHooks(HOOK_DEFS, 0, &defs); #ifdef __CLIENT__ // Composite fonts. for(int i = 0; i < defs.compositeFonts.size(); ++i) { resSys().newFontFromDef(defs.compositeFonts[i]); } #endif // Sprite names. runtimeDefs.sprNames.append(defs.sprites.size()); for(int i = 0; i < runtimeDefs.sprNames.size(); ++i) { qstrcpy(runtimeDefs.sprNames[i].name, defs.sprites[i].id); } // States. runtimeDefs.states.append(defs.states.size()); for(int i = 0; i < runtimeDefs.states.size(); ++i) { ded_state_t *dst = &defs.states[i]; // Make sure duplicate IDs overwrite the earliest. int stateNum = Def_GetStateNum(dst->id); if(stateNum == -1) continue; ded_state_t *dstNew = &defs.states[stateNum]; state_t *st = &runtimeDefs.states[stateNum]; st->sprite = Def_GetSpriteNum(dst->sprite.id); st->flags = dst->flags; st->frame = dst->frame; st->tics = dst->tics; st->action = Def_GetActionPtr(dst->action); st->nextState = Def_GetStateNum(dst->nextState); for(int k = 0; k < NUM_STATE_MISC; ++k) { st->misc[k] = dst->misc[k]; } // Replace the older execute string. if(dst != dstNew) { M_Free(dstNew->execute); dstNew->execute = dst->execute; dst->execute = nullptr; } } runtimeDefs.stateInfo.append(defs.states.size()); // Mobj info. runtimeDefs.mobjInfo.append(defs.mobjs.size()); for(int i = 0; i < runtimeDefs.mobjInfo.size(); ++i) { ded_mobj_t *dmo = &defs.mobjs[i]; // Make sure duplicate defs overwrite the earliest. mobjinfo_t *mo = &runtimeDefs.mobjInfo[Def_GetMobjNum(dmo->id)]; gettingFor = mo; mo->doomEdNum = dmo->doomEdNum; mo->spawnHealth = dmo->spawnHealth; mo->reactionTime = dmo->reactionTime; mo->painChance = dmo->painChance; mo->speed = dmo->speed; mo->radius = dmo->radius; mo->height = dmo->height; mo->mass = dmo->mass; mo->damage = dmo->damage; mo->flags = dmo->flags[0]; mo->flags2 = dmo->flags[1]; mo->flags3 = dmo->flags[2]; for(int k = 0; k < STATENAMES_COUNT; ++k) { mo->states[k] = Def_StateForMobj(dmo->states[k]); } mo->seeSound = Def_GetSoundNum(dmo->seeSound); mo->attackSound = Def_GetSoundNum(dmo->attackSound); mo->painSound = Def_GetSoundNum(dmo->painSound); mo->deathSound = Def_GetSoundNum(dmo->deathSound); mo->activeSound = Def_GetSoundNum(dmo->activeSound); for(int k = 0; k < NUM_MOBJ_MISC; ++k) { mo->misc[k] = dmo->misc[k]; } } // Decorations. (Define textures). for(int i = 0; i < defs.decorations.size(); ++i) { defn::Decoration decorDef(defs.decorations[i]); for(int k = 0; k < decorDef.lightCount(); ++k) { Record const &st = defn::MaterialDecoration(decorDef.light(k)).stage(0); if(Vector3f(st.geta("color")) != Vector3f(0, 0, 0)) { defineLightmap(de::Uri(st["lightmapUp"], RC_NULL)); defineLightmap(de::Uri(st["lightmapDown"], RC_NULL)); defineLightmap(de::Uri(st["lightmapSide"], RC_NULL)); defineFlaremap(de::Uri(st["haloTexture"], RC_NULL)); } } } // Detail textures (Define textures). resSys().textureScheme("Details").clear(); for(int i = 0; i < defs.details.size(); ++i) { ded_detailtexture_t *dtl = &defs.details[i]; // Ignore definitions which do not specify a material. if((!dtl->material1 || dtl->material1->isEmpty()) && (!dtl->material2 || dtl->material2->isEmpty())) continue; if(!dtl->stage.texture) continue; resSys().defineTexture("Details", *dtl->stage.texture); } // Surface reflections (Define textures). resSys().textureScheme("Reflections").clear(); resSys().textureScheme("Masks").clear(); for(int i = 0; i < defs.reflections.size(); ++i) { ded_reflection_t *ref = &defs.reflections[i]; // Ignore definitions which do not specify a material. if(!ref->material || ref->material->isEmpty()) continue; if(ref->stage.texture) { resSys().defineTexture("Reflections", *ref->stage.texture); } if(ref->stage.maskTexture) { resSys().defineTexture("Masks", *ref->stage.maskTexture, Vector2i(ref->stage.maskWidth, ref->stage.maskHeight)); } } // Materials. for(int i = 0; i < defs.materials.size(); ++i) { interpretMaterialDef(defs.materials[i]); } //DED_NewEntries((void **) &stateLights, &countStateLights, sizeof(*stateLights), defs.states.size()); // Dynamic lights. Update the sprite numbers. for(int i = 0; i < defs.lights.size(); ++i) { int const stateIdx = Def_GetStateNum(defs.lights[i].state); if(stateIdx < 0) { // It's probably a bias light definition, then? if(!defs.lights[i].uniqueMapID[0]) { LOG_RES_WARNING("Undefined state '%s' in Light definition") << defs.lights[i].state; } continue; } runtimeDefs.stateInfo[stateIdx].light = &defs.lights[i]; } // Sound effects. runtimeDefs.sounds.append(defs.sounds.size()); for(int i = 0; i < runtimeDefs.sounds.size(); ++i) { ded_sound_t *snd = &defs.sounds[i]; // Make sure duplicate defs overwrite the earliest. sfxinfo_t *si = &runtimeDefs.sounds[Def_GetSoundNum(snd->id)]; qstrcpy(si->id, snd->id); qstrcpy(si->lumpName, snd->lumpName); si->lumpNum = (qstrlen(snd->lumpName) > 0? fileSys().lumpNumForName(snd->lumpName) : -1); qstrcpy(si->name, snd->name); int const soundIdx = Def_GetSoundNum(snd->link); si->link = (soundIdx >= 0 ? &runtimeDefs.sounds[soundIdx] : 0); si->linkPitch = snd->linkPitch; si->linkVolume = snd->linkVolume; si->priority = snd->priority; si->channels = snd->channels; si->flags = snd->flags; si->group = snd->group; Str_Init(&si->external); if(snd->ext) { Str_Set(&si->external, snd->ext->pathCStr()); } } // Music. for(int i = 0; i < defs.musics.size(); ++i) { Record *mus = &defs.musics[i]; // Make sure duplicate defs overwrite the earliest. Record *earliest = &defs.musics[Def_GetMusicNum(mus->gets("id").toUtf8().constData())]; if(earliest == mus) continue; earliest->set("lumpName", mus->gets("lumpName")); earliest->set("cdTrack", mus->geti("cdTrack")); if(!mus->gets("path").isEmpty()) { earliest->set("path", mus->gets("path")); } else if(!earliest->gets("path").isEmpty()) { earliest->set("path", ""); } } // Text. runtimeDefs.texts.append(defs.text.size()); for(int i = 0; i < defs.text.size(); ++i) { Def_InitTextDef(&runtimeDefs.texts[i], defs.text[i].text); } // Handle duplicate strings. for(int i = 0; i < runtimeDefs.texts.size(); ++i) { if(!runtimeDefs.texts[i].text) continue; for(int k = i + 1; k < runtimeDefs.texts.size(); ++k) { if(!runtimeDefs.texts[k].text) continue; // Already done. if(qstricmp(defs.text[i].id, defs.text[k].id)) continue; // ID mismatch. // Update the earlier string. runtimeDefs.texts[i].text = (char *) M_Realloc(runtimeDefs.texts[i].text, qstrlen(runtimeDefs.texts[k].text) + 1); qstrcpy(runtimeDefs.texts[i].text, runtimeDefs.texts[k].text); // Free the later string, it isn't used (>NUMTEXT). M_Free(runtimeDefs.texts[k].text); runtimeDefs.texts[k].text = nullptr; } } //DED_NewEntries((void **) &statePtcGens, &countStatePtcGens, sizeof(*statePtcGens), defs.states.size()); // Particle generators. for(int i = 0; i < defs.ptcGens.size(); ++i) { ded_ptcgen_t *pg = &defs.ptcGens[i]; int st = Def_GetStateNum(pg->state); if(!qstrcmp(pg->type, "*")) pg->typeNum = DED_PTCGEN_ANY_MOBJ_TYPE; else pg->typeNum = Def_GetMobjNum(pg->type); pg->type2Num = Def_GetMobjNum(pg->type2); pg->damageNum = Def_GetMobjNum(pg->damage); // Figure out embedded sound ID numbers. for(int k = 0; k < pg->stages.size(); ++k) { if(pg->stages[k].sound.name[0]) { pg->stages[k].sound.id = Def_GetSoundNum(pg->stages[k].sound.name); } if(pg->stages[k].hitSound.name[0]) { pg->stages[k].hitSound.id = Def_GetSoundNum(pg->stages[k].hitSound.name); } } if(st <= 0) continue; // Not state triggered, then... stateinfo_t *stinfo = &runtimeDefs.stateInfo[st]; // Link the definition to the state. if(pg->flags & Generator::StateChain) { // Add to the chain. pg->stateNext = stinfo->ptcGens; stinfo->ptcGens = pg; } else { // Make sure the previously built list is unlinked. while(stinfo->ptcGens) { ded_ptcgen_t *temp = stinfo->ptcGens->stateNext; stinfo->ptcGens->stateNext = nullptr; stinfo->ptcGens = temp; } stinfo->ptcGens = pg; pg->stateNext = nullptr; } } // Map infos. for(int i = 0; i < defs.mapInfos.size(); ++i) { Record &mi = defs.mapInfos[i]; /** * Historically, the map info flags field was used for sky flags, * here we copy those flags to the embedded sky definition for * backward-compatibility. */ if(mi.geti("flags") & MIF_DRAW_SPHERE) { mi.set("sky.flags", mi.geti("sky.flags") | SIF_DRAW_SPHERE); } } // Log a summary of the definition database. LOG_RES_MSG(_E(b) "Definitions:"); String str; QTextStream os(&str); os << defCountMsg(defs.episodes.size(), "episodes"); os << defCountMsg(defs.groups.size(), "animation groups"); os << defCountMsg(defs.compositeFonts.size(), "composite fonts"); os << defCountMsg(defs.details.size(), "detail textures"); os << defCountMsg(defs.finales.size(), "finales"); os << defCountMsg(defs.lights.size(), "lights"); os << defCountMsg(defs.lineTypes.size(), "line types"); os << defCountMsg(defs.mapInfos.size(), "map infos"); int nonAutoGeneratedCount = 0; for(int i = 0; i < defs.materials.size(); ++i) { if(!defs.materials[i].getb("autoGenerated")) ++nonAutoGeneratedCount; } os << defCountMsg(nonAutoGeneratedCount, "materials"); os << defCountMsg(defs.models.size(), "models"); os << defCountMsg(defs.ptcGens.size(), "particle generators"); os << defCountMsg(defs.skies.size(), "skies"); os << defCountMsg(defs.sectorTypes.size(), "sector types"); os << defCountMsg(defs.musics.size(), "songs"); os << defCountMsg(runtimeDefs.sounds.size(), "sound effects"); os << defCountMsg(runtimeDefs.sprNames.size(), "sprite names"); os << defCountMsg(runtimeDefs.states.size(), "states"); os << defCountMsg(defs.decorations.size(), "surface decorations"); os << defCountMsg(defs.reflections.size(), "surface reflections"); os << defCountMsg(runtimeDefs.texts.size(), "text strings"); os << defCountMsg(defs.textureEnv.size(), "texture environments"); os << defCountMsg(runtimeDefs.mobjInfo.size(), "things"); LOG_RES_MSG("%s") << str.rightStrip(); defsInited = true; } static void initMaterialGroup(ded_group_t &def) { ResourceSystem::MaterialManifestGroup *group = nullptr; for(int i = 0; i < def.members.size(); ++i) { ded_group_member_t *gm = &def.members[i]; if(!gm->material) continue; try { MaterialManifest &manifest = resSys().materialManifest(*gm->material); if(def.flags & AGF_PRECACHE) // A precache group. { // Only create the group once the first material has been found. if(!group) { group = &resSys().newMaterialGroup(); } group->insert(&manifest); } #if 0 /// @todo $revise-texture-animation else // An animation group. { // Only create the group once the first material has been found. if(animNumber == -1) { animNumber = resSys().newAnimGroup(def.flags & ~AGF_PRECACHE); } resSys().animGroup(animNumber).addFrame(manifest.material(), gm->tics, gm->randomTics); } #endif } catch(ResourceSystem::MissingManifestError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING("Unknown material \"%s\" in group def %i: %s") << *gm->material << i << er.asText(); } } } void Def_PostInit() { #ifdef __CLIENT__ // Particle generators: model setup. for(int i = 0; i < defs.ptcGens.size(); ++i) { ded_ptcgen_t *gen = &defs.ptcGens[i]; for(int k = 0; k < gen->stages.size(); ++k) { ded_ptcstage_t *st = &gen->stages[k]; if(st->type < PTC_MODEL || st->type >= PTC_MODEL + MAX_PTC_MODELS) continue; st->model = -1; try { ModelDef &modef = resSys().modelDef(String("Particle%1").arg(st->type - PTC_MODEL, 2, 10, QChar('0'))); if(modef.subModelId(0) == NOMODELID) { continue; } Model &mdl = resSys().model(modef.subModelId(0)); st->model = resSys().indexOf(&modef); st->frame = mdl.frameNumber(st->frameName); if(st->frame < 0) st->frame = 0; if(st->endFrameName[0]) { st->endFrame = mdl.frameNumber(st->endFrameName); if(st->endFrame < 0) st->endFrame = 0; } else { st->endFrame = -1; } } catch(ResourceSystem::MissingModelDefError const &) {} // Ignore this error. } } #endif // __CLIENT__ // Lights. for(int i = 0; i < defs.lights.size(); ++i) { ded_light_t &lightDef = defs.lights[i]; if(lightDef.up) defineLightmap(*lightDef.up); if(lightDef.down) defineLightmap(*lightDef.down); if(lightDef.sides) defineLightmap(*lightDef.sides); if(lightDef.flare) defineFlaremap(*lightDef.flare); } // Material groups (e.g., for precaching). resSys().clearAllMaterialGroups(); for(int i = 0; i < defs.groups.size(); ++i) { initMaterialGroup(defs.groups[i]); } } dd_bool Def_SameStateSequence(state_t *snew, state_t *sold) { if(!snew || !sold) return false; if(snew == sold) return true; // Trivial. int const target = runtimeDefs.states.indexOf(snew); int const start = runtimeDefs.states.indexOf(sold); int count = 0; for(int it = sold->nextState; it >= 0 && it != start && count < 16; it = runtimeDefs.states[it].nextState, ++count) { if(it == target) return true; if(it == runtimeDefs.states[it].nextState) break; } return false; } char const *Def_GetStateName(state_t *state) { int idx = runtimeDefs.states.indexOf(state); if(!state) return "(nullptr)"; //if(idx < 0) return "(<0)"; //if(idx >= defs.states.size()) return "(>states)"; return defs.states[idx].id; } static int Friendly(int num) { if(num < 0) num = 0; return num; } /** * Converts a DED line type to the internal format. * Bit of a nuisance really... */ void Def_CopyLineType(linetype_t *l, ded_linetype_t *def) { DENG2_ASSERT(l && def); l->id = def->id; l->flags = def->flags[0]; l->flags2 = def->flags[1]; l->flags3 = def->flags[2]; l->lineClass = def->lineClass; l->actType = def->actType; l->actCount = def->actCount; l->actTime = def->actTime; l->actTag = def->actTag; for(int i = 0; i < 10; ++i) { if(i == 9) l->aparm[i] = Def_GetMobjNum(def->aparm9); else l->aparm[i] = def->aparm[i]; } l->tickerStart = def->tickerStart; l->tickerEnd = def->tickerEnd; l->tickerInterval = def->tickerInterval; l->actSound = Friendly(Def_GetSoundNum(def->actSound)); l->deactSound = Friendly(Def_GetSoundNum(def->deactSound)); l->evChain = def->evChain; l->actChain = def->actChain; l->deactChain = def->deactChain; l->actLineType = def->actLineType; l->deactLineType = def->deactLineType; l->wallSection = def->wallSection; if(def->actMaterial) { try { l->actMaterial = resSys().materialManifest(*def->actMaterial).id(); } catch(ResourceSystem::MissingManifestError const &) {} // Ignore this error. } if(def->deactMaterial) { try { l->deactMaterial = resSys().materialManifest(*def->deactMaterial).id(); } catch(ResourceSystem::MissingManifestError const &) {} // Ignore this error. } l->actMsg = def->actMsg; l->deactMsg = def->deactMsg; l->materialMoveAngle = def->materialMoveAngle; l->materialMoveSpeed = def->materialMoveSpeed; int i; LOOPi(20) l->iparm[i] = def->iparm[i]; LOOPi(20) l->fparm[i] = def->fparm[i]; LOOPi(5) l->sparm[i] = def->sparm[i]; // Some of the parameters might be strings depending on the line class. // Find the right mapping table. for(int k = 0; k < 20; ++k) { int const a = xgClassLinks[l->lineClass].iparm[k].map; if(a < 0) continue; if(a & MAP_SND) { l->iparm[k] = Friendly(Def_GetSoundNum(def->iparmStr[k])); } else if(a & MAP_MATERIAL) { if(def->iparmStr[k][0]) { if(!qstricmp(def->iparmStr[k], "-1")) { l->iparm[k] = -1; } else { try { l->iparm[k] = resSys().materialManifest(de::Uri(def->iparmStr[k], RC_NULL)).id(); } catch(ResourceSystem::MissingManifestError const &) {} // Ignore this error. } } } else if(a & MAP_MUS) { int temp = Friendly(Def_GetMusicNum(def->iparmStr[k])); if(temp == 0) { temp = Def_EvalFlags(def->iparmStr[k]); if(temp) l->iparm[k] = temp; } else { l->iparm[k] = Friendly(Def_GetMusicNum(def->iparmStr[k])); } } else { int temp = Def_EvalFlags(def->iparmStr[k]); if(temp) l->iparm[k] = temp; } } } /** * Converts a DED sector type to the internal format. */ void Def_CopySectorType(sectortype_t *s, ded_sectortype_t *def) { DENG2_ASSERT(s && def); int i, k; s->id = def->id; s->flags = def->flags; s->actTag = def->actTag; LOOPi(5) { s->chain[i] = def->chain[i]; s->chainFlags[i] = def->chainFlags[i]; s->start[i] = def->start[i]; s->end[i] = def->end[i]; LOOPk(2) s->interval[i][k] = def->interval[i][k]; s->count[i] = def->count[i]; } s->ambientSound = Friendly(Def_GetSoundNum(def->ambientSound)); LOOPi(2) { s->soundInterval[i] = def->soundInterval[i]; s->materialMoveAngle[i] = def->materialMoveAngle[i]; s->materialMoveSpeed[i] = def->materialMoveSpeed[i]; } s->windAngle = def->windAngle; s->windSpeed = def->windSpeed; s->verticalWind = def->verticalWind; s->gravity = def->gravity; s->friction = def->friction; s->lightFunc = def->lightFunc; LOOPi(2) s->lightInterval[i] = def->lightInterval[i]; LOOPi(3) { s->colFunc[i] = def->colFunc[i]; LOOPk(2) s->colInterval[i][k] = def->colInterval[i][k]; } s->floorFunc = def->floorFunc; s->floorMul = def->floorMul; s->floorOff = def->floorOff; LOOPi(2) s->floorInterval[i] = def->floorInterval[i]; s->ceilFunc = def->ceilFunc; s->ceilMul = def->ceilMul; s->ceilOff = def->ceilOff; LOOPi(2) s->ceilInterval[i] = def->ceilInterval[i]; } int Def_Get(int type, char const *id, void *out) { switch(type) { case DD_DEF_MOBJ: return Def_GetMobjNum(id); case DD_DEF_MOBJ_BY_NAME: return Def_GetMobjNumForName(id); case DD_DEF_STATE: return Def_GetStateNum(id); case DD_DEF_ACTION: return Def_GetActionNum(id); case DD_DEF_SPRITE: return Def_GetSpriteNum(id); case DD_DEF_SOUND: return Def_GetSoundNum(id); case DD_DEF_SOUND_BY_NAME: return defs.getSoundNumForName(id); case DD_DEF_SOUND_LUMPNAME: { int32_t i = *((int32_t *) id); if(i < 0 || i >= runtimeDefs.sounds.size()) return false; qstrcpy((char *)out, runtimeDefs.sounds[i].lumpName); return true; } case DD_DEF_VALUE: { int idx = -1; // Not found. if(id && id[0]) { // Read backwards to allow patching. for(idx = defs.values.size() - 1; idx >= 0; idx--) { if(!qstricmp(defs.values[idx].id, id)) break; } } if(out) *(char **) out = (idx >= 0? defs.values[idx].text : 0); return idx; } case DD_DEF_VALUE_BY_INDEX: { int32_t idx = *((int32_t *) id); if(idx >= 0 && idx < defs.values.size()) { if(out) *(char **) out = defs.values[idx].text; return true; } if(out) *(char **) out = 0; return false; } case DD_DEF_LINE_TYPE: { int typeId = strtol(id, (char **)nullptr, 10); for(int i = defs.lineTypes.size() - 1; i >= 0; i--) { if(defs.lineTypes[i].id != typeId) continue; if(out) Def_CopyLineType((linetype_t *)out, &defs.lineTypes[i]); return true; } return false; } case DD_DEF_SECTOR_TYPE: { int typeId = strtol(id, (char **)nullptr, 10); for(int i = defs.sectorTypes.size() - 1; i >= 0; i--) { if(defs.sectorTypes[i].id != typeId) continue; if(out) Def_CopySectorType((sectortype_t *)out, &defs.sectorTypes[i]); return true; } return false; } default: return false; } } int Def_Set(int type, int index, int value, void const *ptr) { LOG_AS("Def_Set"); switch(type) { case DD_DEF_STATE: { ded_state_t *stateDef; if(index < 0 || index >= defs.states.size()) { DENG2_ASSERT(!"Def_Set: State index is invalid"); return false; } stateDef = &defs.states[index]; switch(value) { case DD_SPRITE: { int sprite = *(int *) ptr; if(sprite < 0 || sprite >= defs.sprites.size()) { LOGDEV_RES_WARNING("Unknown sprite index %i") << sprite; break; } qstrcpy((char *) stateDef->sprite.id, defs.sprites[value].id); break; } case DD_FRAME: stateDef->frame = *(int *)ptr; break; default: break; } break; } case DD_DEF_SOUND: if(index < 0 || index >= runtimeDefs.sounds.size()) { DENG2_ASSERT(!"Sound index is invalid"); return false; } switch(value) { case DD_LUMP: S_StopSound(index, 0); qstrcpy(runtimeDefs.sounds[index].lumpName, (char const *) ptr); if(qstrlen(runtimeDefs.sounds[index].lumpName)) { runtimeDefs.sounds[index].lumpNum = fileSys().lumpNumForName(runtimeDefs.sounds[index].lumpName); if(runtimeDefs.sounds[index].lumpNum < 0) { LOG_RES_WARNING("Unknown sound lump name \"%s\"; sound #%i will be inaudible") << runtimeDefs.sounds[index].lumpName << index; } } else { runtimeDefs.sounds[index].lumpNum = 0; } break; default: break; } break; default: return false; } return true; } StringArray *Def_ListMobjTypeIDs() { StringArray *array = StringArray_New(); for(int i = 0; i < defs.mobjs.size(); ++i) { StringArray_Append(array, defs.mobjs[i].id); } return array; } StringArray *Def_ListStateIDs() { StringArray *array = StringArray_New(); for(int i = 0; i < defs.states.size(); ++i) { StringArray_Append(array, defs.states[i].id); } return array; } bool Def_IsAllowedReflection(ded_reflection_t const *def, /*bool hasExternal,*/ bool isCustom) { //if(hasExternal) return (def->flags & REFF_EXTERNAL) != 0; if(!isCustom) return (def->flags & REFF_NO_IWAD) == 0; return (def->flags & REFF_PWAD) != 0; } bool Def_IsAllowedDetailTex(ded_detailtexture_t const *def, /*bool hasExternal,*/ bool isCustom) { //if(hasExternal) return (def->flags & DTLF_EXTERNAL) != 0; if(!isCustom) return (def->flags & DTLF_NO_IWAD) == 0; return (def->flags & DTLF_PWAD) != 0; } /** * Prints a list of all the registered mobjs to the console. * @todo Does this belong here? */ D_CMD(ListMobjs) { DENG2_UNUSED3(src, argc, argv); if(defs.mobjs.size() <= 0) { LOG_RES_MSG("No mobjtypes defined/loaded"); return true; } LOG_RES_MSG(_E(b) "Registered Mobjs (ID | Name):"); for(int i = 0; i < defs.mobjs.size(); ++i) { if(defs.mobjs[i].name[0]) LOG_RES_MSG(" %s | %s") << defs.mobjs[i].id << defs.mobjs[i].name; else LOG_RES_MSG(" %s | " _E(l) "(Unnamed)") << defs.mobjs[i].id; } return true; } DENG_DECLARE_API(Def) = { { DE_API_DEFINITIONS }, Def_Get, Def_Set, Def_EvalFlags }; doomsday-stable-1.15.7/doomsday/client/src/clientapp.cpp0000664000175000017500000003765212641367670022560 0ustar jaakkojaakko/** @file clientapp.cpp The client application. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "clientapp.h" #include "alertmask.h" #include "dd_main.h" #include "dd_def.h" #include "dd_loop.h" #include "de_audio.h" #include "def_main.h" #include "sys_system.h" #include "audio/s_main.h" #include "gl/gl_main.h" #include "gl/gl_texmanager.h" #include "ui/inputsystem.h" #include "ui/b_main.h" #include "ui/sys_input.h" #include "ui/clientwindowsystem.h" #include "ui/clientwindow.h" #include "ui/widgets/taskbarwidget.h" #include "ui/dialogs/alertdialog.h" #include "ui/styledlogsinkformatter.h" #include "updater.h" #if WIN32 # include "dd_winit.h" #elif UNIX # include "dd_uinit.h" #endif #include using namespace de; static ClientApp *clientAppSingleton = 0; static void handleLegacyCoreTerminate(char const *msg) { App_Error("Application terminated due to exception:\n%s\n", msg); } static void continueInitWithEventLoopRunning() { if(!ClientWindowSystem::mainExists()) return; // Show the main window. This causes initialization to finish (in busy mode) // as the canvas is visible and ready for initialization. ClientWindowSystem::main().show(); ClientApp::updater().setupUI(); } static Value *Function_App_GamePlugin(Context &, Function::ArgumentValues const &) { if(App_CurrentGame().isNull()) { // The null game has no plugin. return 0; } String name = Plug_FileForPlugin(App_CurrentGame().pluginId()).name().fileNameWithoutExtension(); if(name.startsWith("lib")) name.remove(0, 3); return new TextValue(name); } static Value *Function_App_Quit(Context &, Function::ArgumentValues const &) { Sys_Quit(); return 0; } DENG2_PIMPL(ClientApp) { Binder binder; QScopedPointer updater; SettingsRegister audioSettings; SettingsRegister networkSettings; SettingsRegister logSettings; QMenuBar *menuBar; InputSystem *inputSys; RenderSystem *rendSys; ResourceSystem *resourceSys; ClientWindowSystem *winSys; InFineSystem infineSys; // instantiated at construction time ServerLink *svLink; Games games; WorldSystem *worldSys; /** * Log entry sink that passes warning messages to the main window's alert * notification dialog. */ struct LogWarningAlarm : public LogSink { AlertMask alertMask; StyledLogSinkFormatter formatter; LogWarningAlarm() : LogSink(formatter) , formatter(LogEntry::Styled | LogEntry::OmitLevel | LogEntry::Simple) { //formatter.setOmitSectionIfNonDev(false); // always show section setMode(OnlyWarningEntries); } LogSink &operator << (LogEntry const &entry) { if(alertMask.shouldRaiseAlert(entry.metadata())) { // Don't raise alerts if the console history is open; the // warning/error will be shown there. if(ClientWindow::mainExists() && ClientWindow::main().taskBar().isOpen() && ClientWindow::main().taskBar().console().isLogOpen()) { return *this; } // We don't want to raise alerts about problems in id/Raven WADs, // since these just have to be accepted by the user. if((entry.metadata() & LogEntry::Map) && ClientApp::worldSystem().hasMap()) { if(MapDef *mapDef = ClientApp::worldSystem().map().def()) { if(!mapDef->sourceFile()->hasCustom()) { return *this; } } } foreach(String msg, formatter.logEntryToTextLines(entry)) { ClientApp::alert(msg, entry.level()); } } return *this; } LogSink &operator << (String const &plainText) { ClientApp::alert(plainText); return *this; } void flush() {} // not buffered }; LogWarningAlarm logAlarm; Instance(Public *i) : Base(i) , menuBar (0) , inputSys (0) , rendSys (0) , resourceSys(0) , winSys (0) //, infineSys (0) , svLink (0) , worldSys (0) { clientAppSingleton = thisPublic; LogBuffer::get().addSink(logAlarm); } ~Instance() { LogBuffer::get().removeSink(logAlarm); self.vr().oculusRift().deinit(); Sys_Shutdown(); DD_Shutdown(); updater.reset(); delete worldSys; //delete infineSys; delete winSys; delete svLink; delete rendSys; delete resourceSys; delete inputSys; delete menuBar; clientAppSingleton = 0; } /** * Set up an application-wide menu. */ void setupAppMenu() { #ifdef MACOSX menuBar = new QMenuBar; QMenu *gameMenu = menuBar->addMenu("&Game"); QAction *checkForUpdates = gameMenu->addAction("Check For &Updates...", updater.data(), SLOT(checkNowShowingProgress())); checkForUpdates->setMenuRole(QAction::ApplicationSpecificRole); #endif } void initSettings() { typedef SettingsRegister SReg; // convenience // Log filter and alert settings. for(int i = LogEntry::FirstDomainBit; i <= LogEntry::LastDomainBit; ++i) { String const name = LogFilter::domainRecordName(LogEntry::Context(1 << i)); logSettings .define(SReg::ConfigVariable, String("log.filter.%1.minLevel").arg(name)) .define(SReg::ConfigVariable, String("log.filter.%1.allowDev").arg(name)) .define(SReg::ConfigVariable, String("alert.%1").arg(name)); } /// @todo These belong in their respective subsystems. networkSettings .define(SReg::StringCVar, "net-master-address", "www.dengine.net") .define(SReg::StringCVar, "net-master-path", "/master.php") .define(SReg::IntCVar, "net-master-port", 0) .define(SReg::IntCVar, "net-dev", 0); audioSettings .define(SReg::IntCVar, "sound-volume", 255 * 2/3) .define(SReg::IntCVar, "music-volume", 255 * 2/3) .define(SReg::FloatCVar, "sound-reverb-volume", 0.5f) .define(SReg::IntCVar, "sound-info", 0) .define(SReg::IntCVar, "sound-rate", 11025) .define(SReg::IntCVar, "sound-16bit", 0) .define(SReg::IntCVar, "sound-3d", 0) .define(SReg::IntCVar, "sound-overlap-stop", 0) .define(SReg::IntCVar, "music-source", MUSP_EXT) .define(SReg::StringCVar, "music-soundfont", ""); } #ifdef UNIX void printVersionToStdOut() { printf("%s\n", String("%1 %2") .arg(DOOMSDAY_NICENAME) .arg(DOOMSDAY_VERSION_FULLTEXT) .toLatin1().constData()); } void printHelpToStdOut() { printVersionToStdOut(); printf("Usage: %s [options]\n", self.commandLine().at(0).toLatin1().constData()); printf(" -iwad (dir) Set directory containing IWAD files.\n"); printf(" -file (f) Load one or more PWAD files at startup.\n"); printf(" -game (id) Set game to load at startup.\n"); printf(" -nomaximize Do not maximize window at startup.\n"); printf(" -wnd Start in windowed mode.\n"); printf(" -wh (w) (h) Set window width and height.\n"); printf(" --version Print current version.\n"); printf("For more options and information, see \"man doomsday\".\n"); } #endif }; ClientApp::ClientApp(int &argc, char **argv) : BaseGuiApp(argc, argv), d(new Instance(this)) { novideo = false; // Use the host system's proxy configuration. QNetworkProxyFactory::setUseSystemConfiguration(true); // Metadata. setMetadata("Deng Team", "dengine.net", "Doomsday Engine", DOOMSDAY_VERSION_BASE); setUnixHomeFolderName(".doomsday"); setTerminateFunc(handleLegacyCoreTerminate); // We must presently set the current game manually (the collection is global). setGame(d->games.nullGame()); d->binder.init(scriptSystem().nativeModule("App")) << DENG2_FUNC_NOARG (App_GamePlugin, "gamePlugin") << DENG2_FUNC_NOARG (App_Quit, "quit"); } void ClientApp::initialize() { Libdeng_Init(); #ifdef UNIX // Some common Unix command line options. if(commandLine().has("--version") || commandLine().has("-version")) { d->printVersionToStdOut(); ::exit(0); } if(commandLine().has("--help") || commandLine().has("-h") || commandLine().has("-?")) { d->printHelpToStdOut(); ::exit(0); } #endif d->svLink = new ServerLink; // Config needs DisplayMode, so let's initialize it before the libcore // subsystems and Config. DisplayMode_Init(); // Initialize definitions before the files are indexed. Def_Init(); addInitPackage("net.dengine.base"); addInitPackage("net.dengine.client"); initSubsystems(); // loads Config // Set up the log alerts (observes Config variables). d->logAlarm.alertMask.init(); // Create the user's configurations and settings folder, if it doesn't exist. fileSystem().makeFolder("/home/configs"); d->initSettings(); // Initialize. #if WIN32 if(!DD_Win32_Init()) { throw Error("ClientApp::initialize", "DD_Win32_Init failed"); } #elif UNIX if(!DD_Unix_Init()) { throw Error("ClientApp::initialize", "DD_Unix_Init failed"); } #endif // Create the render system. d->rendSys = new RenderSystem; addSystem(*d->rendSys); // Create the window system. d->winSys = new ClientWindowSystem; WindowSystem::setAppWindowSystem(*d->winSys); addSystem(*d->winSys); // Check for updates automatically. d->updater.reset(new Updater); d->setupAppMenu(); // Create the resource system. d->resourceSys = new ResourceSystem; addSystem(*d->resourceSys); Plug_LoadAll(); // Create the main window. d->winSys->createWindow()->setWindowTitle(DD_ComposeMainWindowTitle()); // Create the input system. d->inputSys = new InputSystem; addSystem(*d->inputSys); B_Init(); //d->infineSys = new InFineSystem; //addSystem(*d->infineSys); // Create the world system. d->worldSys = new WorldSystem; addSystem(*d->worldSys); // Finally, run the bootstrap script. scriptSystem().importModule("bootstrap"); App_Timer(1, continueInitWithEventLoopRunning); } void ClientApp::preFrame() { // Frame syncronous I/O operations. S_StartFrame(); /// @todo Move to AudioSystem::timeChanged(). if(gx.BeginFrame) /// @todo Move to GameSystem::timeChanged(). { gx.BeginFrame(); } } void ClientApp::postFrame() { /// @todo Should these be here? Consider multiple windows, each having a postFrame? /// Or maybe the frames need to be synced? Or only one of them has a postFrame? // We will arrive here always at the same time in relation to the displayed // frame: it is a good time to update the mouse state. Mouse_Poll(); if(gx.EndFrame) { gx.EndFrame(); } S_EndFrame(); // This is a good time to recycle unneeded memory allocations, as we're just // finished and shown a frame and there might be free time before we have to // begin drawing the next frame. Garbage_Recycle(); } void ClientApp::alert(String const &msg, LogEntry::Level level) { if(ClientWindow::mainExists()) { ClientWindow::main().alerts() .newAlert(msg, level >= LogEntry::Error? AlertDialog::Major : level == LogEntry::Warning? AlertDialog::Normal : AlertDialog::Minor); } /** * @todo If there is no window, the alert could be stored until the window becomes * available. -jk */ } ClientApp &ClientApp::app() { DENG2_ASSERT(clientAppSingleton != 0); return *clientAppSingleton; } Updater &ClientApp::updater() { DENG2_ASSERT(!app().d->updater.isNull()); return *app().d->updater; } SettingsRegister &ClientApp::logSettings() { return app().d->logSettings; } SettingsRegister &ClientApp::networkSettings() { return app().d->networkSettings; } SettingsRegister &ClientApp::audioSettings() { return app().d->audioSettings; } ServerLink &ClientApp::serverLink() { ClientApp &a = ClientApp::app(); DENG2_ASSERT(a.d->svLink != 0); return *a.d->svLink; } InFineSystem &ClientApp::infineSystem() { ClientApp &a = ClientApp::app(); //DENG2_ASSERT(a.d->infineSys != 0); return a.d->infineSys; } InputSystem &ClientApp::inputSystem() { ClientApp &a = ClientApp::app(); DENG2_ASSERT(a.d->inputSys != 0); return *a.d->inputSys; } RenderSystem &ClientApp::renderSystem() { ClientApp &a = ClientApp::app(); DENG2_ASSERT(hasRenderSystem()); return *a.d->rendSys; } bool ClientApp::hasRenderSystem() { return ClientApp::app().d->rendSys != 0; } ResourceSystem &ClientApp::resourceSystem() { ClientApp &a = ClientApp::app(); DENG2_ASSERT(a.d->resourceSys != 0); return *a.d->resourceSys; } ClientWindowSystem &ClientApp::windowSystem() { ClientApp &a = ClientApp::app(); DENG2_ASSERT(a.d->winSys != 0); return *a.d->winSys; } Games &ClientApp::games() { return app().d->games; } WorldSystem &ClientApp::worldSystem() { ClientApp &a = ClientApp::app(); DENG2_ASSERT(a.d->worldSys != 0); return *a.d->worldSys; } void ClientApp::openHomepageInBrowser() { openInBrowser(QUrl(DOOMSDAY_HOMEURL)); } void ClientApp::openInBrowser(QUrl url) { // Get out of fullscreen mode. int windowed[] = { ClientWindow::Fullscreen, false, ClientWindow::End }; ClientWindow::main().changeAttributes(windowed); QDesktopServices::openUrl(url); } void ClientApp::beginNativeUIMode() { // Switch temporarily to windowed mode. Not needed on OS X because the display mode // is never changed on that platform. #ifndef MACOSX auto &win = ClientWindow::main(); win.saveState(); int const windowedMode[] = { ClientWindow::Fullscreen, false, ClientWindow::End }; win.changeAttributes(windowedMode); #endif } void ClientApp::endNativeUIMode() { #ifndef MACOSX ClientWindow::main().restoreState(); #endif } doomsday-stable-1.15.7/doomsday/client/src/edit_bias.cpp0000664000175000017500000005000212641367670022504 0ustar jaakkojaakko/** @file edit_bias.cpp Shadow Bias editor UI. * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifdef __CLIENT__ #include "de_base.h" #include "de_console.h" #include "de_filesys.h" #include "de_ui.h" #include "world/map.h" #include "world/p_players.h" // viewPlayer #include "ConvexSubspace" #include "Hand" #include "HueCircle" #include "render/viewports.h" #include "render/rend_main.h" // gameDrawHUD/vOrigin, remove me #include "edit_bias.h" #include #include using namespace de; D_CMD(BLEditor); /* * Editor variables: */ int editHidden; int editBlink; int editShowAll; int editShowIndices = true; /* * Editor status: */ static bool editActive; // Edit mode active? static bool editHueCircle; static HueCircle *hueCircle; void SBE_Register() { // Variables. C_VAR_INT("edit-bias-blink", &editBlink, 0, 0, 1); C_VAR_INT("edit-bias-hide", &editHidden, 0, 0, 1); C_VAR_INT("edit-bias-show-sources", &editShowAll, 0, 0, 1); C_VAR_INT("edit-bias-show-indices", &editShowIndices, 0, 0, 1); // Commands. C_CMD_FLAGS("bledit", "", BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("blquit", "", BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("blclear", "", BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("blsave", NULL, BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("blnew", "", BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("bldel", "", BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("bllock", NULL, BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("blunlock", NULL, BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("blgrab", NULL, BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("blungrab", NULL, BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("bldup", "", BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("blc", "fff", BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("bli", NULL, BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("bllevels", NULL, BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD_FLAGS("blhue", NULL, BLEditor, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); } bool SBE_Active() { return editActive; } HueCircle *SBE_HueCircle() { if(!editActive || !editHueCircle) return 0; return hueCircle; } void SBE_SetHueCircle(bool activate = true) { if(!editActive) return; // Any change in state? if(activate == editHueCircle) return; // The circle can only be activated when something is grabbed. if(activate && App_WorldSystem().hand().isEmpty()) return; editHueCircle = activate; if(activate) { viewdata_t const &viewer = *R_ViewData(viewPlayer - ddPlayers); hueCircle->setOrientation(viewer.frontVec, viewer.sideVec, viewer.upVec); } } /* * Editor Functionality: */ static void SBE_Begin() { if(editActive) return; #ifdef __CLIENT__ // Advise the game not to draw any HUD displays gameDrawHUD = false; #endif editActive = true; editHueCircle = false; hueCircle = new HueCircle; LOG_AS("Bias"); LOG_VERBOSE("Editing begins"); } static void SBE_End() { if(!editActive) return; App_WorldSystem().hand().ungrab(); delete hueCircle; hueCircle = 0; editHueCircle = false; editActive = false; #ifdef __CLIENT__ // Advise the game it can safely draw any HUD displays again gameDrawHUD = true; #endif LOG_AS("Bias"); LOG_VERBOSE("Editing ends."); } static void SBE_Clear() { DENG_ASSERT(editActive); App_WorldSystem().map().removeAllBiasSources(); } static void SBE_Delete(int which) { DENG_ASSERT(editActive); App_WorldSystem().map().removeBiasSource(which); } static BiasSource *SBE_New() { DENG_ASSERT(editActive); try { Hand &hand = App_WorldSystem().hand(); BiasSource &source = App_WorldSystem().map().addBiasSource(hand.origin()); // Update the edit properties. hand.setEditIntensity(source.intensity()); hand.setEditColor(source.color()); hand.grab(source); // As this is a new source -- unlock immediately. source.unlock(); return &source; } catch(Map::FullError const &) {} // Ignore this error. return 0; } static BiasSource *SBE_Dupe(BiasSource const &other) { DENG_ASSERT(editActive); try { Hand &hand = App_WorldSystem().hand(); BiasSource &source = App_WorldSystem().map().addBiasSource(other); // A copy is made. source.setOrigin(hand.origin()); // Update the edit properties. hand.setEditIntensity(source.intensity()); hand.setEditColor(source.color()); hand.grab(source); // As this is a new source -- unlock immediately. source.unlock(); return &source; } catch(Map::FullError const &) {} // Ignore this error. return 0; } static void SBE_Grab(int which) { DENG_ASSERT(editActive); Hand &hand = App_WorldSystem().hand(); if(BiasSource *source = App_WorldSystem().map().biasSourcePtr(which)) { if(hand.isEmpty()) { // Update the edit properties. hand.setEditIntensity(source->intensity()); hand.setEditColor(source->color()); } hand.grabMulti(*source); } } static void SBE_Ungrab(int which) { DENG_ASSERT(editActive); Hand &hand = App_WorldSystem().hand(); if(BiasSource *source = App_WorldSystem().map().biasSourcePtr(which)) { hand.ungrab(*source); } else { hand.ungrab(); } } static void SBE_SetLock(int which, bool enable = true) { DENG_ASSERT(editActive); Hand &hand = App_WorldSystem().hand(); if(BiasSource *source = App_WorldSystem().map().biasSourcePtr(which)) { if(enable) source->lock(); else source->unlock(); return; } for(Grabbable *grabbable : hand.grabbed()) { if(enable) grabbable->lock(); else grabbable->unlock(); } } static bool SBE_Save(char const *name = 0) { DENG2_ASSERT(editActive); LOG_AS("Bias"); Map &map = App_WorldSystem().map(); ddstring_t fileName; Str_Init(&fileName); if(!name || !name[0]) { Str_Set(&fileName, String(map.def()? map.def()->composeUri().path() : "unknownmap").toUtf8().constData()); } else { Str_Set(&fileName, name); F_ExpandBasePath(&fileName, &fileName); } // Do we need to append an extension? if(String(Str_Text(&fileName)).fileNameExtension().isEmpty()) { Str_Append(&fileName, ".ded"); } F_ToNativeSlashes(&fileName, &fileName); FILE *file = fopen(Str_Text(&fileName), "wt"); if(!file) { LOG_RES_WARNING("Failed to save light sources to \"%s\": could not open file") << NativePath(Str_Text(&fileName)).pretty(); Str_Free(&fileName); return false; } LOG_RES_VERBOSE("Saving to \"%s\"...") << NativePath(Str_Text(&fileName)).pretty(); String uid = (map.def()? map.def()->composeUniqueId(App_CurrentGame()) : "(unknown map)"); fprintf(file, "# %i Bias Lights for %s", map.biasSourceCount(), uid.toUtf8().constData()); // Since there can be quite a lot of these, make sure we'll skip // the ones that are definitely not suitable. fprintf(file, "\n\nSkipIf Not %s", App_CurrentGame().identityKey().toUtf8().constData()); map.forAllBiasSources([&file, &uid] (BiasSource &bsrc) { float minLight, maxLight; bsrc.lightLevels(minLight, maxLight); fprintf(file, "\n\nLight {"); fprintf(file, "\n Map = \"%s\"", uid.toUtf8().constData()); fprintf(file, "\n Origin { %g %g %g }", bsrc.origin().x, bsrc.origin().y, bsrc.origin().z); fprintf(file, "\n Color { %g %g %g }", bsrc.color().x, bsrc.color().y, bsrc.color().z); fprintf(file, "\n Intensity = %g", bsrc.intensity()); fprintf(file, "\n Sector levels { %g %g }", minLight, maxLight); fprintf(file, "\n}"); return LoopContinue; }); fclose(file); Str_Free(&fileName); return true; } /* * Editor commands. */ D_CMD(BLEditor) { DENG_UNUSED(src); char *cmd = argv[0] + 2; if(!qstricmp(cmd, "edit")) { SBE_Begin(); return true; } if(!editActive) { LOG_WARNING("The bias lighting editor is not active"); return false; } if(!qstricmp(cmd, "quit")) { SBE_End(); return true; } if(!qstricmp(cmd, "save")) { return SBE_Save(argc >= 2 ? argv[1] : 0); } if(!qstricmp(cmd, "clear")) { SBE_Clear(); return true; } if(!qstricmp(cmd, "hue")) { int activate = (argc >= 2 ? stricmp(argv[1], "off") : !editHueCircle); SBE_SetHueCircle(activate); return true; } Map &map = App_WorldSystem().map(); coord_t handDistance; Hand &hand = App_WorldSystem().hand(&handDistance); if(!qstricmp(cmd, "new")) { return SBE_New() != 0; } if(!qstricmp(cmd, "c")) { // Update the edit properties. hand.setEditColor(Vector3f(argc > 1? strtod(argv[1], 0) : 1, argc > 2? strtod(argv[2], 0) : 1, argc > 3? strtod(argv[3], 0) : 1)); return true; } if(!qstricmp(cmd, "i")) { hand.setEditIntensity(argc > 1? strtod(argv[1], 0) : 200); return true; } if(!qstricmp(cmd, "grab")) { SBE_Grab(map.indexOf(*map.biasSourceNear(hand.origin()))); return true; } if(!qstricmp(cmd, "ungrab")) { SBE_Ungrab(argc > 1? atoi(argv[1]) : -1); return true; } if(!qstricmp(cmd, "lock")) { SBE_SetLock(argc > 1? atoi(argv[1]) : -1); return true; } if(!qstricmp(cmd, "unlock")) { SBE_SetLock(argc > 1? atoi(argv[1]) : -1, false); return true; } // Has a source index been given as an argument? int which = -1; if(!hand.isEmpty()) { which = map.indexOf(hand.grabbed().first()->as()); } else { which = map.indexOf(*map.biasSourceNear(hand.origin())); } if(argc > 1) { which = atoi(argv[1]); } if(which < 0 || which >= map.biasSourceCount()) { LOG_SCR_WARNING("Invalid bias light source index %i") << which; return false; } if(!qstricmp(cmd, "del")) { SBE_Delete(which); return true; } if(!qstricmp(cmd, "dup")) { return SBE_Dupe(map.biasSource(which)) != nullptr; } if(!qstricmp(cmd, "levels")) { float minLight = 0, maxLight = 0; if(argc >= 2) { minLight = strtod(argv[1], 0) / 255.0f; maxLight = argc >= 3? strtod(argv[2], 0) / 255.0f : minLight; } map.biasSource(which).setLightLevels(minLight, maxLight); return true; } return false; } /* * Editor visuals (would-be widgets): */ #include "api_fontrender.h" #include "world/map.h" #include "world/p_players.h" #include "BspLeaf" #include "SectorCluster" #include "render/rend_font.h" static void drawBoxBackground(Vector2i const &origin_, Vector2i const &size_, ui_color_t *color) { Point2Raw origin(origin_.x, origin_.y); Size2Raw size(size_.x, size_.y); UI_GradientEx(&origin, &size, 6, color ? color : UI_Color(UIC_BG_MEDIUM), color ? color : UI_Color(UIC_BG_LIGHT), .2f, .4f); UI_DrawRectEx(&origin, &size, 6, false, color ? color : UI_Color(UIC_BRD_HI), NULL, .4f, -1); } static void drawText(String const &text, Vector2i const &origin_, ui_color_t *color, float alpha, int align = ALIGN_LEFT, short flags = DTF_ONLY_SHADOW) { Point2Raw origin(origin_.x, origin_.y); UI_TextOutEx2(text.toUtf8().constData(), &origin, color, alpha, align, flags); } /** * - index #, lock status * - origin * - distance from eye * - intensity, light level threshold * - color */ static void drawInfoBox(BiasSource *s, int rightX, String const title, float alpha) { int const precision = 3; if(!s) return; FR_SetFont(fontFixed); FR_LoadDefaultAttrib(); FR_SetShadowOffset(UI_SHADOW_OFFSET, UI_SHADOW_OFFSET); FR_SetShadowStrength(UI_SHADOW_STRENGTH); int th = FR_SingleLineHeight("Info"); Vector2i size(16 + FR_TextWidth("Color:(0.000, 0.000, 0.000)"), 16 + th * 6); Vector2i origin(DENG_GAMEVIEW_WIDTH - 10 - size.x - rightX, DENG_GAMEVIEW_HEIGHT - 10 - size.y); ui_color_t color; color.red = s->color().x; color.green = s->color().y; color.blue = s->color().z; DENG_ASSERT_IN_MAIN_THREAD(); glEnable(GL_TEXTURE_2D); drawBoxBackground(origin, size, &color); origin.x += 8; origin.y += 8 + th/2; drawText(title, origin, UI_Color(UIC_TITLE), alpha); origin.y += th; int sourceIndex = App_WorldSystem().map().indexOf(*s); coord_t distance = (s->origin() - vOrigin.xzy()).length(); float minLight, maxLight; s->lightLevels(minLight, maxLight); String text1 = String("#%1").arg(sourceIndex, 3, 10, QLatin1Char('0')) + (s->isLocked()? " (locked)" : ""); drawText(text1, origin, UI_Color(UIC_TEXT), alpha); origin.y += th; String text2 = String("Origin:") + s->origin().asText(); drawText(text2, origin, UI_Color(UIC_TEXT), alpha); origin.y += th; String text3 = String("Distance:%1").arg(distance, 5, 'f', precision, QLatin1Char('0')); drawText(text3, origin, UI_Color(UIC_TEXT), alpha); origin.y += th; String text4 = String("Intens:%1").arg(s->intensity(), 5, 'f', precision, QLatin1Char('0')); if(!de::fequal(minLight, 0) || !de::fequal(maxLight, 0)) text4 += String(" L:%2/%3").arg(int(255.0f * minLight), 3).arg(int(255.0f * maxLight), 3); drawText(text4, origin, UI_Color(UIC_TEXT), alpha); origin.y += th; String text5 = String("Color:(%1, %2, %3)").arg(s->color().x, 0, 'f', precision).arg(s->color().y, 0, 'f', precision).arg(s->color().z, 0, 'f', precision); drawText(text5, origin, UI_Color(UIC_TEXT), alpha); origin.y += th; glDisable(GL_TEXTURE_2D); } static void drawLightGauge(Vector2i const &origin, int height = 255) { static float minLevel = 0, maxLevel = 0; static SectorCluster *lastCluster = 0; Hand &hand = App_WorldSystem().hand(); Map &map = App_WorldSystem().map(); BiasSource *source; if(!hand.isEmpty()) source = &hand.grabbed().first()->as(); else source = map.biasSourceNear(hand.origin()); if(ConvexSubspace *subspace = source->bspLeafAtOrigin().subspacePtr()) { if(subspace->hasCluster() && lastCluster != subspace->clusterPtr()) { lastCluster = &subspace->cluster(); minLevel = maxLevel = lastCluster->lightSourceIntensity(); } } float const lightLevel = lastCluster? lastCluster->lightSourceIntensity() : 0; if(lightLevel < minLevel) minLevel = lightLevel; if(lightLevel > maxLevel) maxLevel = lightLevel; FR_SetFont(fontFixed); FR_LoadDefaultAttrib(); FR_SetShadowOffset(UI_SHADOW_OFFSET, UI_SHADOW_OFFSET); FR_SetShadowStrength(UI_SHADOW_STRENGTH); int off = FR_TextWidth("000"); int minY = 0, maxY = 0; glBegin(GL_LINES); glColor4f(1, 1, 1, .5f); glVertex2f(origin.x + off, origin.y); glVertex2f(origin.x + off, origin.y + height); // Normal light level. int secY = origin.y + height * (1.0f - lightLevel); glVertex2f(origin.x + off - 4, secY); glVertex2f(origin.x + off, secY); if(maxLevel != minLevel) { // Max light level. maxY = origin.y + height * (1.0f - maxLevel); glVertex2f(origin.x + off + 4, maxY); glVertex2f(origin.x + off, maxY); // Min light level. minY = origin.y + height * (1.0f - minLevel); glVertex2f(origin.x + off + 4, minY); glVertex2f(origin.x + off, minY); } // Current min/max bias sector level. float minLight, maxLight; source->lightLevels(minLight, maxLight); if(minLight > 0 || maxLight > 0) { glColor3f(1, 0, 0); int p = origin.y + height * (1.0f - minLight); glVertex2f(origin.x + off + 2, p); glVertex2f(origin.x + off - 2, p); glColor3f(0, 1, 0); p = origin.y + height * (1.0f - maxLight); glVertex2f(origin.x + off + 2, p); glVertex2f(origin.x + off - 2, p); } glEnd(); glEnable(GL_TEXTURE_2D); // The number values. drawText(String::number(int(255.0f * lightLevel)), Vector2i(origin.x, secY), UI_Color(UIC_TITLE), .7f, 0, DTF_ONLY_SHADOW); if(maxLevel != minLevel) { drawText(String::number(int(255.0f * maxLevel)), Vector2i(origin.x + 2*off, maxY), UI_Color(UIC_TEXT), .7f, 0, DTF_ONLY_SHADOW); drawText(String::number(int(255.0f * minLevel)), Vector2i(origin.x + 2*off, minY), UI_Color(UIC_TEXT), .7f, 0, DTF_ONLY_SHADOW); } glDisable(GL_TEXTURE_2D); } void SBE_DrawGui() { float const opacity = .8f; if(!editActive || editHidden) return; if(!App_WorldSystem().hasMap()) return; Map &map = App_WorldSystem().map(); Hand &hand = App_WorldSystem().hand(); DENG_ASSERT_IN_MAIN_THREAD(); // Go into screen projection mode. glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, DENG_GAMEVIEW_WIDTH, DENG_GAMEVIEW_HEIGHT, 0, -1, 1); glEnable(GL_TEXTURE_2D); // Overall stats: numSources / MAX (left) String text = String("%1 / %2 (%3 free)") .arg(map.biasSourceCount()) .arg(Map::MAX_BIAS_SOURCES) .arg(Map::MAX_BIAS_SOURCES - map.biasSourceCount()); FR_SetFont(fontFixed); FR_LoadDefaultAttrib(); FR_SetShadowOffset(UI_SHADOW_OFFSET, UI_SHADOW_OFFSET); FR_SetShadowStrength(UI_SHADOW_STRENGTH); Vector2i size(FR_TextWidth(text.toUtf8().constData()) + 16, FR_SingleLineHeight(text.toUtf8().constData()) + 16); int top = DENG_GAMEVIEW_HEIGHT - 10 - size.y; Vector2i origin(10, top); drawBoxBackground(origin, size, 0); origin.x += 8; origin.y += size.y / 2; drawText(text, origin, UI_Color(UIC_TITLE), opacity); origin.y = top - size.y / 2; // The map ID. String label = (map.def()? map.def()->composeUniqueId(App_CurrentGame()) : "(unknown map)"); drawText(label, origin, UI_Color(UIC_TITLE), opacity); glDisable(GL_TEXTURE_2D); if(map.biasSourceCount()) { // Stats for nearest & grabbed: drawInfoBox(map.biasSourceNear(hand.origin()), 0, "Nearest", opacity); if(!hand.isEmpty()) { FR_SetFont(fontFixed); int x = FR_TextWidth("0") * 30; drawInfoBox(&hand.grabbed().first()->as(), x, "Grabbed", opacity); } drawLightGauge(Vector2i(20, DENG_GAMEVIEW_HEIGHT/2 - 255/2)); } glMatrixMode(GL_PROJECTION); glPopMatrix(); } #endif // __CLIENT__ doomsday-stable-1.15.7/doomsday/client/src/render/0000775000175000017500000000000012641367670021337 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/render/lightgrid.cpp0000664000175000017500000004622012641367670024024 0ustar jaakkojaakko/** @file lightgrid.cpp Light Grid (Large-Scale FakeRadio). * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "render/lightgrid.h" #include #include #include #include #include #include // Cvars: static int lgEnabled = false; static int lgBlockSize = 31; namespace de { namespace internal { enum LightBlockFlag { Changed = 0x1, ///< Primary contribution has changed. Contributor = 0x2, ///< Secondary contribution has changed. AllFlags = Changed | Contributor }; Q_DECLARE_FLAGS(LightBlockFlags, LightBlockFlag) /** * Determines if the bit in @a bitfield (assumed 32-bit) is set for the given * grid reference @a gref. */ static bool hasIndexBit(LightGrid::Ref const &gref, int gridWidth, uint *bitfield) { uint const index = gref.x + gref.y * gridWidth; return (bitfield[index >> 5] & (1 << (index & 0x1f))) != 0; } /** * Sets the bit in a bitfield (assumed 32-bit) for the given grid reference @a gref. * @param count If set, will be incremented when a zero bit is changed to one. */ static void addIndexBit(LightGrid::Ref const &gref, int gridWidth, uint *bitfield, int *count) { uint const index = gref.x + gref.y * gridWidth; // Are we counting when bits are set? if(count && !hasIndexBit(LightGrid::Ref(index, 0), gridWidth, bitfield)) { (*count)++; } bitfield[index >> 5] |= (1 << (index & 0x1f)); } } Q_DECLARE_OPERATORS_FOR_FLAGS(internal::LightBlockFlags) using namespace internal; static Vector4f const black; DENG2_PIMPL(LightGrid) { Vector2d origin; ///< Grid origin in map space. int blockSize; ///< In map coordinate space units. Vector2i dimensions; ///< Grid dimensions in blocks. /** * Grid coverage data for a light source. */ struct LightCoverage { int primaryBlockCount; QVector blocks; LightCoverage() : primaryBlockCount(0) {} }; typedef QMap Coverages; Coverages coverage; bool needUpdateCoverage; /** * Grid illumination point. * * Light contributions come from sources of one of two logical types: * * - @em Primary contributors are the main light source and are linked to the * block directly so that their contribution to neighbors can be tracked. * * - @em Secondary contributors are additional light sources which contribute * to neighbor blocks. Secondary contributors are not linked to the block as * their contributions can be inferred from primarys at update time. */ struct LightBlock { LightBlockFlags flags; ///< Internal state flags. char bias; ///< If positive the source is shining up from floor. IBlockLightSource *source; ///< Primary illumination source (if any). Vector3f color; ///< Accumulated light color (from all sources). Vector3f oldColor; ///< Used if the color has changed and an update is pending. /** * Construct a new light block using the source specified as the @em primary * illumination source for the block. * * @param primarySource Primary illumination. Use @c 0 to create a "null-block". */ LightBlock(IBlockLightSource *primarySource = 0) : bias(0), source(primarySource) {} /** * Change the flags of the light block. * * @param flagsToChange Flags to change the value of. * @param operation Logical operation to perform on the flags. */ void setFlags(LightBlockFlags flagsToChange, FlagOp operation = SetFlags) { if(!source) return; applyFlagOperation(flags, flagsToChange, operation); } /** * Evaluate the ambient color for the light block. * @note Blocks with no primary illumination source are always black. */ Vector4f evaluate(/*coord_t height*/) const { if(!source) return black; /** * Biased light dimming disabled because this does not work well enough. * The problem is that two points on a given surface may be determined to * be in different blocks and as the height is taken from the block linked * sector this results in very uneven lighting. * * Biasing is a good idea but the plane heights must come from the sector * at the exact X|Y coordinates of the sample point, not the "quantized" * references in the light grid. -ds */ /* coord_t dz = 0; if(_bias < 0) { // Calculate Z difference to the ceiling. dz = source->visCeiling().height() - height; } else if(_bias > 0) { // Calculate Z difference to the floor. dz = height - source->visFloor().height(); } dz -= 50; if(dz < 0) dz = 0;*/ // If we are awaiting an updated value use the old color. Vector4f colorDimmed = flags.testFlag(Changed)? oldColor : color; // Biased ambient light causes a dimming on the Z axis. /*if(dz && _bias) { float dimming = 1 - (dz * (float) de::abs(d->bias)) / 35000.0f; if(dimming < .5f) dimming = .5f; colorDimmed *= dimming; } */ // Set the luminance factor. colorDimmed.w = (colorDimmed.x + colorDimmed.y + colorDimmed.z) / 3; return colorDimmed; } bool markChanged(bool isContributor = false) { if(!source) return false; if(isContributor) { // Changes by contributor sectors are simply flagged until an update. flags |= Contributor; return true; } // The color will be recalculated. if(!(flags & Changed)) { // Remember the color in case we receive any queries before the update. oldColor = color; } flags |= Changed; flags |= Contributor; // Init to black in preparation for the update. color = Vector3f(0, 0, 0); return true; } /** * Apply an illumination to the block. */ void applyLightingChanges(Vector4f const &contrib, int sourceBias, float factor) { if(!source) return; // Apply a bias to the light level. float level = contrib.w; level -= (0.95f - level); if(level < 0) level = 0; level *= factor; if(level <= 0) return; for(int i = 0; i < 3; ++i) { float c = de::clamp(0.f, contrib[i] * level, 1.f); if(color[i] + c > 1) { color[i] = 1; } else { color[i] += c; } } // Influenced by the source bias. bias = de::clamp(-0x80, int(bias * (1 - factor) + sourceBias * factor), 0x7f); } }; /// The One "null" block takes the place of empty blocks in the grid. LightBlock nullBlock; /// The grid of LightBlocks. All unused point at @var nullBlock. typedef QVector Blocks; Blocks blocks; bool needUpdate; int numBlocks; ///< Total number of non-null blocks. Instance(Public *i) : Base(i) , blockSize(0) , needUpdateCoverage(false) , needUpdate(false) , numBlocks(0) {} ~Instance() { clearBlocks(); } inline LightBlock &block(Index index) { return *blocks[index]; } inline LightBlock &block(Ref const &gref) { return block(self.toIndex(gref)); } void clearBlocks() { for(int i = 0; i < blocks.count(); ++i) { if(blocks[i] != &nullBlock) { delete blocks[i]; blocks[i] = &nullBlock; } } // A grid of null blocks needs no coverage data or future updates. coverage.clear(); needUpdate = needUpdateCoverage = false; } void resizeAndClearBlocks(Vector2i const &newDimensions) { dimensions = newDimensions; blocks.resize(dimensions.x * dimensions.y); clearBlocks(); } // Find the affected and contributed blocks of all light sources. void updateCoverageIfNeeded() { if(!needUpdateCoverage) return; needUpdateCoverage = false; // Bitfields for marking affected blocks. Make sure each bit is in a word. size_t const bitfieldSize = 4 * (31 + dimensions.x * dimensions.y) / 32; uint *primaryBitfield = (uint *) M_Calloc(bitfieldSize); uint *contribBitfield = (uint *) M_Calloc(bitfieldSize); // Reset the coverage data for all primary light sources. coverage.clear(); foreach(LightBlock *block, blocks) { if(block->source && !coverage.contains(block->source)) { coverage.insert(block->source, LightCoverage()); } } for(Coverages::iterator it = coverage.begin(); it != coverage.end(); ++it) { IBlockLightSource *source = it.key(); LightCoverage &covered = it.value(); // Determine blocks for which this is the primary source. int primaryCount = 0; std::memset(primaryBitfield, 0, bitfieldSize); for(int y = 0; y < dimensions.y; ++y) for(int x = 0; x < dimensions.x; ++x) { // Does this block have a different primary source? if(source != block(Ref(x, y)).source) { continue; } /// Primary sources affect near neighbors due to smoothing. /// @todo Determine min/max a/b before going into the loop. for(int b = -2; b <= 2; ++b) { if(y + b < 0 || y + b >= dimensions.y) continue; for(int a = -2; a <= 2; ++a) { if(x + a < 0 || x + a >= dimensions.x) continue; addIndexBit(Ref(x + a, y + b), dimensions.x, primaryBitfield, &primaryCount); } } } // Determine blocks for which this is the secondary contributor. int contribCount = 0; std::memset(contribBitfield, 0, bitfieldSize); for(int y = 0; y < dimensions.y; ++y) for(int x = 0; x < dimensions.x; ++x) { if(!hasIndexBit(Ref(x, y), dimensions.x, primaryBitfield)) continue; // Add the contributor blocks. for(int b = -2; b <= 2; ++b) { if(y + b < 0 || y + b >= dimensions.y) continue; for(int a = -2; a <= 2; ++a) { if(x + a < 0 || x + a >= dimensions.x) continue; if(!hasIndexBit(Ref(x + a, y + b), dimensions.x, primaryBitfield)) { addIndexBit(Ref(x + a, y + b), dimensions.x, contribBitfield, &contribCount); } } } } // Remember grid coverage for this illumination source. int const blockCount = primaryCount + contribCount; covered.primaryBlockCount = primaryCount; covered.blocks.resize(blockCount); if(blockCount > 0) { int a = 0, b = primaryCount; for(int x = 0; x < dimensions.x * dimensions.y; ++x) { if(hasIndexBit(Ref(x, 0), dimensions.x, primaryBitfield)) { covered.blocks[a++] = x; } else if(hasIndexBit(Ref(x, 0), dimensions.x, contribBitfield)) { covered.blocks[b++] = x; } } DENG2_ASSERT(a == primaryCount); // sanity check } } M_Free(primaryBitfield); M_Free(contribBitfield); // A full update is needed after this. self.scheduleFullUpdate(); } }; LightGrid::LightGrid(Vector2d const &origin, Vector2d const &dimensions) : d(new Instance(this)) { resizeAndClear(origin, dimensions); } void LightGrid::resizeAndClear(Vector2d const &newOrigin, Vector2d const &newDimensions) { d->origin = newOrigin; d->blockSize = lgBlockSize; // Determine the dimensions of the grid (in blocks) Vector2d const blockDimensions = newDimensions / d->blockSize; // (Re)-initialize an empty light grid. d->resizeAndClearBlocks(Vector2i(de::round(blockDimensions.x) + 1, de::round(blockDimensions.y) + 1)); } Vector4f LightGrid::evaluate(Vector3d const &point) { // If not enabled there is no lighting to evaluate; return black. if(!lgEnabled) return black; return d->block(toRef(point)).evaluate(); } void LightGrid::scheduleFullUpdate() { if(d->blocks.isEmpty()) return; d->updateCoverageIfNeeded(); // Mark all non-null blocks. foreach(Instance::LightBlock *block, d->blocks) { block->markChanged(); block->markChanged(true); } d->needUpdate = true; } void LightGrid::updateIfNeeded() { // Updates are unnecessary if not enabled. if(!lgEnabled) return; d->updateCoverageIfNeeded(); // Any work to do? if(!d->needUpdate) return; d->needUpdate = false; static float const factors[5 * 5] = { .1f, .2f, .25f, .2f, .1f, .2f, .4f, .5f, .4f, .2f, .25f, .5f, 1.f, .5f, .25f, .2f, .4f, .5f, .4f, .2f, .1f, .2f, .25f, .2f, .1f }; for(int y = 0; y < d->dimensions.y; ++y) for(int x = 0; x < d->dimensions.x; ++x) { Instance::LightBlock &blockAtRef = d->block(Ref(x, y)); // No contribution? if(!blockAtRef.flags.testFlag(Contributor)) continue; // Determine the ambient light properties of this block. IBlockLightSource &source = *blockAtRef.source; Vector4f const color = Vector4f(source.lightSourceColorf(), source.lightSourceIntensity(Vector3d(0, 0, 0))); int const bias = source.blockLightSourceZBias(); /// @todo Calculate min/max for a and b. for(int a = -2; a <= 2; ++a) for(int b = -2; b <= 2; ++b) { if(x + a < 0 || y + b < 0 || x + a > d->dimensions.x - 1 || y + b > d->dimensions.y - 1) continue; Instance::LightBlock &other = d->block(Ref(x + a, y + b)); if(!other.flags.testFlag(Changed)) continue; other.applyLightingChanges(color, bias, factors[(b + 2) * 5 + a + 2] / 8); } } // Clear all changed and contribution flags for all non-null blocks. foreach(Instance::LightBlock *block, d->blocks) { block->setFlags(AllFlags, UnsetFlags); } } void LightGrid::setPrimarySource(Index index, IBlockLightSource *newSource) { Instance::LightBlock *block = &d->block(index); if(newSource == block->source) return; if(newSource && !block->source) { // Replace the "null block" with a new light block. d->blocks[index] = block = new Instance::LightBlock(newSource); d->numBlocks++; } else if(!newSource && block->source) { // Replace the existing light block with the "null block". delete block; d->blocks[index] = block = &d->nullBlock; d->numBlocks--; } block->source = newSource; // A full update is needed. d->needUpdate = d->needUpdateCoverage = true; } LightGrid::IBlockLightSource *LightGrid::primarySource(Index index) const { return d->block(index).source; } void LightGrid::blockLightSourceChanged(IBlockLightSource *changed) { // Updates are unnecessary if not enabled. if(!lgEnabled) return; if(!changed) return; d->updateCoverageIfNeeded(); Instance::Coverages::const_iterator covered = d->coverage.constFind(changed); if(covered == d->coverage.constEnd()) return; if(covered->blocks.count()) { // Mark primary and contributed blocks. for(int i = 0; i < covered->primaryBlockCount; ++i) { if(d->block(covered->blocks[i]).markChanged()) { d->needUpdate = true; } } for(int i = 0; i < covered->blocks.count(); ++i) { if(d->block(covered->blocks[i]).markChanged(true /*is contributor*/)) { d->needUpdate = true; } } } } LightGrid::Ref LightGrid::toRef(Vector3d const &point) { int x = de::round((point.x - d->origin.x) / d->blockSize); int y = de::round((point.y - d->origin.y) / d->blockSize); return Ref(de::clamp(1, x, dimensions().x - 2), de::clamp(1, y, dimensions().y - 2)); } int LightGrid::blockSize() const { return d->blockSize; } Vector2d const &LightGrid::origin() const { return d->origin; } Vector2i const &LightGrid::dimensions() const { return d->dimensions; } int LightGrid::numBlocks() const { return d->numBlocks; } size_t LightGrid::blockStorageSize() const { return sizeof(Instance::LightBlock) * d->numBlocks; } Vector3f const &LightGrid::rawColorRef(Index index) const { return d->block(index).color; } void LightGrid::consoleRegister() // static { C_VAR_INT("rend-bias-grid", &lgEnabled, 0, 0, 1); C_VAR_INT("rend-bias-grid-blocksize", &lgBlockSize, 0, 8, 1024); } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/render/biassource.cpp0000664000175000017500000002207612641367670024211 0ustar jaakkojaakko/** @file biassource.cpp Shadow Bias (light) source. * * @authors Copyright © 2005-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "world/worldsystem.h" #include "world/map.h" #include "BspLeaf" #include "ConvexSubspace" #include "SectorCluster" #include "BiasDigest" #include "render/biassource.h" using namespace de; DENG2_PIMPL(BiasSource) { /// Origin of the source in the map coordinate space. Vector3d origin; /// BSP leaf at the origin. BspLeaf *bspLeaf; bool inVoid; ///< Set to @c true if the origin is in the void. /// Intensity of the emitted light. float primaryIntensity; /// Effective intensity of the light scaled by the ambient level threshold. float intensity; /// Color strength factors of the emitted light. Vector3f color; /// Ambient light level threshold. float minLight, maxLight; /// Time in milliseconds of the last update. uint lastUpdateTime; /// Flags: bool changed; Instance(Public *i, Vector3d const &origin, float intensity, Vector3f const &color, float minLight, float maxLight) : Base(i) , origin (origin) , bspLeaf (0) , inVoid (true) , primaryIntensity(intensity) , intensity (intensity) , color (color) , minLight (minLight) , maxLight (maxLight) , lastUpdateTime (0) // Force an update. , changed (true) {} Instance(Public *i, Instance const &other) : Base(i) , origin (other.origin) , bspLeaf (other.bspLeaf) , inVoid (other.inVoid) , primaryIntensity(other.primaryIntensity) , intensity (other.intensity) , color (other.color) , minLight (other.minLight) , maxLight (other.maxLight) , lastUpdateTime (0) // Force an update. , changed (true) {} void updateBspLocation() { if(bspLeaf) return; /// @todo Do not assume the current map. bspLeaf = &App_WorldSystem().map().bspLeafAt(origin); bool newInVoidState = !(bspLeaf->hasSubspace() && bspLeaf->subspace().contains(origin)); if(inVoid != newInVoidState) { inVoid = newInVoidState; intensity = inVoid? 0 : primaryIntensity; changed = true; } } bool needToObserveSectorLightLevelChanges() { updateBspLocation(); return !inVoid && (maxLight > 0 || minLight > 0); } void notifyOriginChanged() { DENG2_FOR_PUBLIC_AUDIENCE(OriginChange, i) { i->grabbableOriginChanged(self); } } void notifyIntensityChanged(float oldIntensity) { DENG2_FOR_PUBLIC_AUDIENCE(IntensityChange, i) { i->biasSourceIntensityChanged(self, oldIntensity); } } void notifyColorChanged(Vector3f const &oldColor) { // Predetermine which components have changed. int changedComponents = 0; for(int i = 0; i < 3; ++i) { if(!de::fequal(color[i], oldColor[i])) changedComponents |= (1 << i); } DENG2_FOR_PUBLIC_AUDIENCE(ColorChange, i) { i->biasSourceColorChanged(self, oldColor, changedComponents); } } }; BiasSource::BiasSource(Vector3d const &origin, float intensity, Vector3f const &color, float minLight, float maxLight) : Grabbable(), ISerializable(), d(new Instance(this, origin, intensity, color, minLight, maxLight)) {} BiasSource::BiasSource(BiasSource const &other) : Grabbable() /*grabbable state is not copied*/, ISerializable(), d(new Instance(this, *other.d)) {} BiasSource BiasSource::fromDef(ded_light_t const &def) //static { return BiasSource(Vector3f(def.offset), def.size, Vector3f(def.color), def.lightLevel[0], def.lightLevel[1]); } BiasSource::~BiasSource() { DENG2_FOR_AUDIENCE(Deletion, i) i->biasSourceBeingDeleted(*this); } Vector3d const &BiasSource::origin() const { return d->origin; } void BiasSource::setOrigin(Vector3d const &newOrigin) { if(d->origin != newOrigin) { d->changed = true; d->origin = newOrigin; d->bspLeaf = 0; // Notify interested parties of the change. d->notifyOriginChanged(); } } BspLeaf &BiasSource::bspLeafAtOrigin() const { d->updateBspLocation(); return *d->bspLeaf; } void BiasSource::lightLevels(float &minLight, float &maxLight) const { minLight = d->minLight; maxLight = d->maxLight; } BiasSource &BiasSource::setLightLevels(float newMinLight, float newMaxLight) { float newMinLightClamped = de::clamp(0.f, newMinLight, 1.f); float newMaxLightClamped = de::clamp(0.f, newMaxLight, 1.f); if(!de::fequal(d->minLight, newMinLightClamped)) { d->minLight = newMinLightClamped; d->changed = true; } if(!de::fequal(d->maxLight, newMaxLightClamped)) { d->maxLight = newMaxLightClamped; d->changed = true; } return *this; } Vector3f const &BiasSource::color() const { return d->color; } BiasSource &BiasSource::setColor(Vector3f const &newColor) { // Amplify the new color (but replace black with white). float largest = newColor[newColor.maxAxis()]; Vector3f newColorAmplified = (largest > 0? newColor / largest : Vector3f(1, 1, 1)); // Clamp. for(int i = 0; i < 3; ++i) { newColorAmplified[i] = de::clamp(0.f, newColorAmplified[i], 1.f); } if(d->color != newColorAmplified) { Vector3f oldColor = d->color; d->color = newColorAmplified; d->changed = true; // Notify interested parties of the change. d->notifyColorChanged(oldColor); } return *this; } float BiasSource::intensity() const { return d->primaryIntensity; } BiasSource &BiasSource::setIntensity(float newIntensity) { if(!de::fequal(d->primaryIntensity, newIntensity)) { float oldIntensity = d->primaryIntensity; d->primaryIntensity = newIntensity; if(!d->inVoid) { d->intensity = d->primaryIntensity; d->changed = true; } // Notify interested parties of the change. d->notifyIntensityChanged(oldIntensity); } return *this; } float BiasSource::evaluateIntensity() const { return d->intensity; } bool BiasSource::trackChanges(BiasDigest &changes, uint digestIndex, uint currentTime) { if(d->needToObserveSectorLightLevelChanges()) { /// @todo Should observe Sector::LightLevelChange float const oldIntensity = intensity(); float newIntensity = 0; if(ConvexSubspace *subspace = d->bspLeaf->subspacePtr()) { SectorCluster &cluster = subspace->cluster(); // Lower intensities are useless for light emission. if(cluster.lightSourceIntensity() >= d->maxLight) { newIntensity = d->primaryIntensity; } if(cluster.lightSourceIntensity() >= d->minLight && d->minLight != d->maxLight) { newIntensity = d->primaryIntensity * (cluster.lightSourceIntensity() - d->minLight) / (d->maxLight - d->minLight); } } if(newIntensity != oldIntensity) { d->intensity = newIntensity; d->changed = true; } } if(!d->changed) return false; d->changed = false; d->lastUpdateTime = currentTime; // Used for interpolation. changes.markSourceChanged(digestIndex); return true; // Changes were applied. } uint BiasSource::lastUpdateTime() const { return d->lastUpdateTime; } void BiasSource::forceUpdate() { d->changed = true; } void BiasSource::operator >> (de::Writer &to) const { to << d->origin << d->primaryIntensity << d->color << d->minLight << d->maxLight; } void BiasSource::operator << (de::Reader &from) { Vector3d newOrigin; from >> newOrigin; setOrigin(newOrigin); float newIntensity; from >> newIntensity; setIntensity(newIntensity); Vector3f newColor; from >> newColor; setColor(newColor); float minLight, maxLight; from >> minLight >> maxLight; setLightLevels(minLight, maxLight); } doomsday-stable-1.15.7/doomsday/client/src/render/rendersystem.cpp0000664000175000017500000005062312641367670024575 0ustar jaakkojaakko/** @file rendersystem.cpp Renderer subsystem. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "render/rendersystem.h" #include #include #include #include "clientapp.h" #include "render/rend_main.h" #include "render/rend_halo.h" #include "render/angleclipper.h" #include "render/modelrenderer.h" #include "render/skydrawable.h" #include "gl/gl_main.h" #include "gl/gl_texmanager.h" #include "ConvexSubspace" #include "SectorCluster" #include "Surface" #include "Contact" #include "r_util.h" using namespace de; Store::Store() : posCoords(0), colorCoords(0), vertCount(0), vertMax(0) { zap(texCoords); } Store::~Store() { clear(); } void Store::rewind() { vertCount = 0; } void Store::clear() { vertCount = vertMax = 0; M_Free(posCoords); posCoords = 0; M_Free(colorCoords); colorCoords = 0; for(dint i = 0; i < NUM_TEXCOORD_ARRAYS; ++i) { M_Free(texCoords[i]); texCoords[i] = 0; } } duint Store::allocateVertices(duint count) { duint const base = vertCount; // Do we need to allocate more memory? vertCount += count; while(vertCount > vertMax) { if(vertMax == 0) { vertMax = 16; } else { vertMax *= 2; } posCoords = (Vector4f *) M_Realloc(posCoords, sizeof(*posCoords) * vertMax); colorCoords = (Vector4ub *) M_Realloc(colorCoords, sizeof(*colorCoords) * vertMax); for(dint i = 0; i < NUM_TEXCOORD_ARRAYS; ++i) { texCoords[i] = (Vector2f *) M_Realloc(texCoords[i], sizeof(Vector2f) * vertMax); } } return base; } ProjectionList::Node *ProjectionList::firstNode = nullptr; ProjectionList::Node *ProjectionList::cursorNode = nullptr; void ProjectionList::init() // static { static bool firstTime = true; if(firstTime) { firstNode = 0; cursorNode = 0; firstTime = false; } } void ProjectionList::rewind() // static { // Start reusing nodes. cursorNode = firstNode; } /// Averaged-color * alpha. static dfloat luminosity(ProjectedTextureData const &pt) // static { return (pt.color.x + pt.color.y + pt.color.z) / 3 * pt.color.w; } ProjectionList &ProjectionList::add(ProjectedTextureData &texp) { Node *node = newNode(); node->projection = texp; if(head && sortByLuma) { dfloat luma = luminosity(node->projection); Node *iter = head; Node *last = iter; do { // Is this brighter than that being added? if(luminosity(iter->projection) > luma) { last = iter; iter = iter->next; } else { // Insert it here. node->next = last->next; last->next = node; return *this; } } while(iter); } node->next = head; head = node; return *this; } ProjectionList::Node *ProjectionList::newNode() // static { Node *node; // Do we need to allocate more nodes? if(!cursorNode) { node = (Node *) Z_Malloc(sizeof(*node), PU_APPSTATIC, nullptr); // Link the new node to the list. node->nextUsed = firstNode; firstNode = node; } else { node = cursorNode; cursorNode = cursorNode->nextUsed; } node->next = nullptr; return node; } VectorLightList::Node *VectorLightList::firstNode = nullptr; VectorLightList::Node *VectorLightList::cursorNode = nullptr; void VectorLightList::init() // static { static bool firstTime = true; if(firstTime) { firstNode = 0; cursorNode = 0; firstTime = false; } } void VectorLightList::rewind() // static { // Start reusing nodes from the first one in the list. cursorNode = firstNode; } VectorLightList &VectorLightList::add(VectorLightData &vlight) { Node *node = newNode(); node->vlight = vlight; if(head) { Node *iter = head; Node *last = iter; do { VectorLightData *vlight = &node->vlight; // Is this closer than the one being added? if(node->vlight.approxDist > vlight->approxDist) { last = iter; iter = iter->next; } else { // Insert it here. node->next = last->next; last->next = node; return *this; } } while(iter); } node->next = head; head = node; return *this; } VectorLightList::Node *VectorLightList::newNode() // static { Node *node; // Do we need to allocate more nodes? if(!cursorNode) { node = (Node *) Z_Malloc(sizeof(*node), PU_APPSTATIC, nullptr); // Link the new node to the list. node->nextUsed = firstNode; firstNode = node; } else { node = cursorNode; cursorNode = cursorNode->nextUsed; } node->next = nullptr; return node; } DENG2_PIMPL(RenderSystem) { ModelRenderer models; SkyDrawable sky; SettingsRegister settings; SettingsRegister appearanceSettings; ImageBank images; AngleClipper clipper; Store buffer; DrawLists drawLists; // Texture => world surface projection lists. struct ProjectionLists { duint listCount = 0; duint cursorList = 0; ProjectionList *lists = nullptr; void init() { ProjectionList::init(); // All memory for the lists is allocated from Zone so we can "forget" it. lists = nullptr; listCount = 0; cursorList = 0; } void reset() { ProjectionList::rewind(); // start reusing list nodes. // Clear the lists. cursorList = 0; if(listCount) { std::memset(lists, 0, listCount * sizeof *lists); } } ProjectionList *tryFindList(duint listIdx) const { if(listIdx > 0 && listIdx <= listCount) { return &lists[listIdx - 1]; } return nullptr; // not found. } ProjectionList &findList(duint listIdx) const { if(ProjectionList *found = tryFindList(listIdx)) return *found; /// @throw MissingListError Invalid index specified. throw Error("RenderSystem::projector::findList", "Invalid index #" + String::number(listIdx)); } ProjectionList &findOrCreateList(duint *listIdx, bool sortByLuma) { DENG2_ASSERT(listIdx); // Do we need to allocate a list? if(!(*listIdx)) { // Do we need to allocate more lists? if(++cursorList >= listCount) { listCount *= 2; if(!listCount) listCount = 2; lists = (ProjectionList *) Z_Realloc(lists, listCount * sizeof(*lists), PU_MAP); } ProjectionList *list = &lists[cursorList - 1]; list->head = nullptr; list->sortByLuma = sortByLuma; *listIdx = cursorList; } return lists[(*listIdx) - 1]; // 1-based index. } } projector; /// VectorLight => object affection lists. struct VectorLights { duint listCount = 0; duint cursorList = 0; VectorLightList *lists = nullptr; void init() { VectorLightList::init(); // All memory for the lists is allocated from Zone so we can "forget" it. lists = nullptr; listCount = 0; cursorList = 0; } void reset() { VectorLightList::rewind(); // start reusing list nodes. // Clear the lists. cursorList = 0; if(listCount) { std::memset(lists, 0, listCount * sizeof *lists); } } VectorLightList *tryFindList(duint listIdx) const { if(listIdx > 0 && listIdx <= listCount) { return &lists[listIdx - 1]; } return nullptr; // not found. } VectorLightList &findList(duint listIdx) const { if(VectorLightList *found = tryFindList(listIdx)) return *found; /// @throw MissingListError Invalid index specified. throw Error("RenderSystem::vlights::findList", "Invalid index #" + String::number(listIdx)); } VectorLightList &findOrCreateList(duint *listIdx) { DENG2_ASSERT(listIdx); // Do we need to allocate a list? if(!(*listIdx)) { // Do we need to allocate more lists? if(++cursorList >= listCount) { listCount *= 2; if(!listCount) listCount = 2; lists = (VectorLightList *) Z_Realloc(lists, listCount * sizeof(*lists), PU_MAP); } VectorLightList *list = &lists[cursorList - 1]; list->head = nullptr; *listIdx = cursorList; } return lists[(*listIdx) - 1]; // 1-based index. } } vlights; Instance(Public *i) : Base(i) { LOG_AS("RenderSystem"); // Load the required packages. App::packageLoader().load("net.dengine.client.renderer"); App::packageLoader().load("net.dengine.client.renderer.lensflares"); // -=- DEVEL -=- //App::packageLoader().load("net.dengine.client.testmodel"); /*Package::Asset asset = App::asset("model.thing.possessed"); qDebug() << asset.accessedRecord().asText(); qDebug() << asset.getPath("path");*/ loadAllShaders(); loadImages(); typedef SettingsRegister SReg; // Initialize settings. settings.define(SReg::FloatCVar, "rend-camera-fov", 95.f) .define(SReg::ConfigVariable, "render.pixelDensity") .define(SReg::IntCVar, "rend-model-mirror-hud", 0) .define(SReg::IntCVar, "rend-model-precache", 1) .define(SReg::IntCVar, "rend-sprite-precache", 1) .define(SReg::IntCVar, "rend-light-multitex", 1) .define(SReg::IntCVar, "rend-model-shiny-multitex", 1) .define(SReg::IntCVar, "rend-tex-detail-multitex", 1) .define(SReg::IntCVar, "rend-tex", 1) .define(SReg::IntCVar, "rend-dev-wireframe", 0) .define(SReg::IntCVar, "rend-dev-thinker-ids", 0) .define(SReg::IntCVar, "rend-dev-mobj-bbox", 0) .define(SReg::IntCVar, "rend-dev-polyobj-bbox", 0) .define(SReg::IntCVar, "rend-dev-sector-show-indices", 0) .define(SReg::IntCVar, "rend-dev-vertex-show-indices", 0) .define(SReg::IntCVar, "rend-dev-generator-show-indices", 0); appearanceSettings.setPersistentName("renderer"); appearanceSettings .define(SReg::IntCVar, "rend-light", 1) .define(SReg::IntCVar, "rend-light-decor", 1) .define(SReg::IntCVar, "rend-light-blend", 0) .define(SReg::IntCVar, "rend-light-num", 0) .define(SReg::FloatCVar, "rend-light-bright", .5f) .define(SReg::FloatCVar, "rend-light-fog-bright", .15f) .define(SReg::FloatCVar, "rend-light-radius-scale", 4.24f) .define(SReg::IntCVar, "rend-light-radius-max", 256) .define(SReg::IntCVar, "rend-light-ambient", 0) .define(SReg::FloatCVar, "rend-light-compression", 0) .define(SReg::IntCVar, "rend-light-attenuation", 924) .define(SReg::IntCVar, "rend-light-sky-auto", 1) .define(SReg::FloatCVar, "rend-light-sky", .273f) .define(SReg::IntCVar, "rend-light-wall-angle-smooth", 1) .define(SReg::FloatCVar, "rend-light-wall-angle", 1.2f) .define(SReg::IntCVar, "rend-vignette", 1) .define(SReg::FloatCVar, "rend-vignette-darkness", 1) .define(SReg::FloatCVar, "rend-vignette-width", 1) .define(SReg::IntCVar, "rend-halo-realistic", 1) .define(SReg::IntCVar, "rend-halo", 5) .define(SReg::IntCVar, "rend-halo-bright", 45) .define(SReg::IntCVar, "rend-halo-size", 80) .define(SReg::IntCVar, "rend-halo-occlusion", 48) .define(SReg::FloatCVar, "rend-halo-radius-min", 20) .define(SReg::FloatCVar, "rend-halo-secondary-limit", 1) .define(SReg::FloatCVar, "rend-halo-dim-near", 10) .define(SReg::FloatCVar, "rend-halo-dim-far", 100) .define(SReg::FloatCVar, "rend-halo-zmag-div", 62) .define(SReg::FloatCVar, "rend-glow", .8f) .define(SReg::IntCVar, "rend-glow-height", 100) .define(SReg::FloatCVar, "rend-glow-scale", 3) .define(SReg::IntCVar, "rend-glow-wall", 1) .define(SReg::ConfigVariable, "render.fx.resize.factor") .define(SReg::IntCVar, "rend-bloom", 1) .define(SReg::FloatCVar, "rend-bloom-intensity", .65f) .define(SReg::FloatCVar, "rend-bloom-threshold", .35f) .define(SReg::FloatCVar, "rend-bloom-dispersion", 1) .define(SReg::IntCVar, "rend-fakeradio", 1) .define(SReg::FloatCVar, "rend-fakeradio-darkness", 1.2f) .define(SReg::IntCVar, "rend-shadow", 1) .define(SReg::FloatCVar, "rend-shadow-darkness", 1.2f) .define(SReg::IntCVar, "rend-shadow-far", 1000) .define(SReg::IntCVar, "rend-shadow-radius-max", 80) .define(SReg::IntCVar, "rend-tex-shiny", 1) .define(SReg::IntCVar, "rend-tex-mipmap", 5) .define(SReg::IntCVar, "rend-tex-quality", TEXQ_BEST) .define(SReg::IntCVar, "rend-tex-anim-smooth", 1) .define(SReg::IntCVar, "rend-tex-filter-smart", 0) .define(SReg::IntCVar, "rend-tex-filter-sprite", 1) .define(SReg::IntCVar, "rend-tex-filter-mag", 1) .define(SReg::IntCVar, "rend-tex-filter-ui", 1) .define(SReg::IntCVar, "rend-tex-filter-anisotropic", -1) .define(SReg::IntCVar, "rend-tex-detail", 1) .define(SReg::FloatCVar, "rend-tex-detail-scale", 4) .define(SReg::FloatCVar, "rend-tex-detail-strength", .5f) .define(SReg::IntCVar, "rend-mobj-smooth-move", 2) .define(SReg::IntCVar, "rend-mobj-smooth-turn", 1) .define(SReg::IntCVar, "rend-model", 1) .define(SReg::IntCVar, "rend-model-inter", 1) .define(SReg::IntCVar, "rend-model-distance", 1500) .define(SReg::FloatCVar, "rend-model-lod", 256) .define(SReg::FloatCVar, "rend-model-lights", 4) .define(SReg::IntCVar, "rend-sprite-mode", 0) .define(SReg::IntCVar, "rend-sprite-blend", 1) .define(SReg::IntCVar, "rend-sprite-lights", 4) .define(SReg::IntCVar, "rend-sprite-align", 0) .define(SReg::IntCVar, "rend-sprite-noz", 0) .define(SReg::IntCVar, "rend-particle", 1) .define(SReg::IntCVar, "rend-particle-max", 0) .define(SReg::FloatCVar, "rend-particle-rate", 1) .define(SReg::FloatCVar, "rend-particle-diffuse", 4) .define(SReg::IntCVar, "rend-particle-visible-near", 0) .define(SReg::FloatCVar, "rend-sky-distance", 1600); } /** * Reads all shader definitions and sets up a Bank where the actual * compiled shaders are stored once they're needed. * * @todo This should be reworked to support unloading packages, and * loading of new shaders from any newly loaded packages. -jk */ void loadAllShaders() { // Load all the shader program definitions. FS::FoundFiles found; App::findInPackages("shaders.dei", found); DENG2_FOR_EACH(FS::FoundFiles, i, found) { LOG_MSG("Loading shader definitions from %s") << (*i)->description(); ClientApp::shaders().addFromInfo(**i); } } /** * Reads the renderer's image definitions and sets up a Bank for caching them * when they're needed. */ void loadImages() { //Folder const &renderPack = App::fileSystem().find("renderer.pack"); //images.addFromInfo(renderPack.locate("images.dei")); } }; RenderSystem::RenderSystem() : d(new Instance(this)) {} void RenderSystem::glInit() { d->models.glInit(); } void RenderSystem::glDeinit() { d->models.glDeinit(); } GLShaderBank &RenderSystem::shaders() { return BaseGuiApp::shaders(); } ImageBank &RenderSystem::images() { return d->images; } ModelRenderer &RenderSystem::modelRenderer() { return d->models; } SkyDrawable &RenderSystem::sky() { return d->sky; } void RenderSystem::timeChanged(Clock const &) { // Nothing to do. } SettingsRegister &RenderSystem::settings() { return d->settings; } SettingsRegister &RenderSystem::appearanceSettings() { return d->appearanceSettings; } AngleClipper &RenderSystem::angleClipper() const { return d->clipper; } Store &RenderSystem::buffer() { return d->buffer; } void RenderSystem::clearDrawLists() { d->drawLists.clear(); // Clear the global vertex buffer, also. d->buffer.clear(); } DrawLists &RenderSystem::drawLists() { return d->drawLists; } void RenderSystem::worldSystemMapChanged(de::Map &) { d->projector.init(); d->vlights.init(); } void RenderSystem::beginFrame() { // Clear the draw lists ready for new geometry. d->drawLists.reset(); d->buffer.rewind(); // Start reallocating storage from the global vertex buffer. // Clear the clipper - we're drawing from a new point of view. d->clipper.clearRanges(); // Recycle view dependent list data. d->projector.reset(); d->vlights.reset(); R_BeginFrame(); } ProjectionList &RenderSystem::findSurfaceProjectionList(duint *listIdx, bool sortByLuma) { return d->projector.findOrCreateList(listIdx, sortByLuma); } LoopResult RenderSystem::forAllSurfaceProjections(duint listIdx, std::function func) const { if(ProjectionList *list = d->projector.tryFindList(listIdx)) { for(ProjectionList::Node *node = list->head; node; node = node->next) { if(auto result = func(node->projection)) return result; } } return LoopContinue; } VectorLightList &RenderSystem::findVectorLightList(duint *listIdx) { return d->vlights.findOrCreateList(listIdx); } LoopResult RenderSystem::forAllVectorLights(duint listIdx, std::function func) { if(VectorLightList *list = d->vlights.tryFindList(listIdx)) { for(VectorLightList::Node *node = list->head; node; node = node->next) { if(auto result = func(node->vlight)) return result; } } return LoopContinue; } void RenderSystem::consoleRegister() { Viewports_Register(); Rend_Register(); H_Register(); } doomsday-stable-1.15.7/doomsday/client/src/render/cameralensfx.cpp0000664000175000017500000001202012641367670024506 0ustar jaakkojaakko/** @file cameralensfx.cpp Camera lens effects. * * Renders camera lens effects, i.e., special effects applied to a "raw" world * frame. ConsoleEffect-derived isntances are put onto a stack; each console * has its own effect stack. * * Given the following stack of effects: * - A * - B * - C * * The following sequence of methods is called during the rendering of a frame: * 1. A.beginFrame * 2. B.beginFrame * 3. C.beginFrame * 4. A.draw * 5. B.draw * 6. C.draw * 7. C.endFrame <-- reverse order * 8. B.endFrame * 9. A.endFrame * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "render/cameralensfx.h" #include "render/rend_main.h" #include "render/viewports.h" #include "render/fx/bloom.h" #include "render/fx/colorfilter.h" #include "render/fx/lensflares.h" #include "render/fx/postprocessing.h" #include "render/fx/resize.h" #include "render/fx/vignette.h" #include "ui/clientwindow.h" #include #include #include #include #include #include using namespace de; static int fxFramePlayerNum; ///< Player view currently being drawn. struct ConsoleEffectStack { /// Dynamic stack of effects. Used currently as a fixed array, though. typedef QList EffectList; EffectList effects; ~ConsoleEffectStack() { clear(); } void clear() { qDeleteAll(effects); effects.clear(); } }; static ConsoleEffectStack fxConsole[DDMAXPLAYERS]; #define IDX_LENS_FLARES 3 #define IDX_POST_PROCESSING 5 D_CMD(PostFx) { DENG2_UNUSED(src); int console = String(argv[1]).toInt(); String const shader = argv[2]; TimeDelta const span = (argc == 4? String(argv[3]).toFloat() : 0); if(console < 0 || console >= DDMAXPLAYERS) { LOG_SCR_WARNING("Invalid console %i") << console; return false; } fx::PostProcessing *post = static_cast(fxConsole[console].effects[IDX_POST_PROCESSING]); // Special case to clear out the current shader. if(shader == "none") { post->fadeOut(span); return true; } else if(shader == "opacity") // Change opacity. { post->setOpacity(span); return true; } post->fadeInShader(shader, span); return true; } void LensFx_Register() { C_CMD("postfx", "is", PostFx); C_CMD("postfx", "isf", PostFx); } void LensFx_Init() { for(int i = 0; i < DDMAXPLAYERS; ++i) { ConsoleEffectStack &stack = fxConsole[i]; stack.effects << new fx::Resize(i) << new fx::Bloom(i) << new fx::Vignette(i) << new fx::LensFlares(i) // IDX_LENS_FLARES << new fx::ColorFilter(i) << new fx::PostProcessing(i); // IDX_POST_PROCESSING } } void LensFx_Shutdown() { LensFx_GLRelease(); for(int i = 0; i < DDMAXPLAYERS; ++i) { fxConsole[i].clear(); } } void LensFx_GLRelease() { for(int i = 0; i < DDMAXPLAYERS; ++i) { foreach(ConsoleEffect *effect, fxConsole[i].effects) { if(effect->isInited()) { effect->glDeinit(); } } } } void LensFx_BeginFrame(int playerNum) { fxFramePlayerNum = playerNum; ConsoleEffectStack::EffectList const &effects = fxConsole[fxFramePlayerNum].effects; // Initialize these effects if they currently are not. foreach(ConsoleEffect *effect, effects) { if(!effect->isInited()) { effect->glInit(); } } foreach(ConsoleEffect *effect, effects) { effect->beginFrame(); } } void LensFx_EndFrame() { ConsoleEffectStack::EffectList const &effects = fxConsole[fxFramePlayerNum].effects; foreach(ConsoleEffect *effect, effects) { effect->draw(); } for(int i = effects.size() - 1; i >= 0; --i) { effects.at(i)->endFrame(); } } void LensFx_MarkLightVisibleInFrame(IPointLightSource const &lightSource) { ConsoleEffectStack::EffectList const &effects = fxConsole[fxFramePlayerNum].effects; static_cast(effects.at(IDX_LENS_FLARES))-> markLightPotentiallyVisibleForCurrentFrame(&lightSource); } doomsday-stable-1.15.7/doomsday/client/src/render/api_render.cpp0000664000175000017500000001633112641367670024157 0ustar jaakkojaakko/** @file api_render.cpp Public API of the renderer. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #define DENG_NO_API_MACROS_RENDER #include "de_platform.h" #include "api_render.h" #include "dd_main.h" // App_ResourceSystem #include "def_main.h" #include "sys_system.h" // novideo #include "gl/sys_opengl.h" #include "render/r_main.h" #include "render/billboard.h" // Rend_SpriteMaterialSpec #include "render/rend_model.h" #include "resource/resourcesystem.h" #ifdef __CLIENT__ # include "MaterialVariantSpec" #endif #include #include using namespace de; // m_misc.c DENG_EXTERN_C int M_ScreenShot(const char* name, int bits); #undef Models_CacheForState DENG_EXTERN_C void Models_CacheForState(int stateIndex) { #ifdef __CLIENT__ if(ModelDef *modelDef = App_ResourceSystem().modelDefForState(stateIndex)) { App_ResourceSystem().cache(modelDef); } #endif } // r_draw.cpp DENG_EXTERN_C void R_SetBorderGfx(struct uri_s const *const *paths); #undef Rend_CacheForMobjType DENG_EXTERN_C void Rend_CacheForMobjType(int num) { LOG_AS("Rend.CacheForMobjType"); if(novideo) return; if(!((useModels && precacheSkins) || precacheSprites)) return; if(num < 0 || num >= defs.mobjs.size()) return; de::MaterialVariantSpec const &spec = Rend_SpriteMaterialSpec(); /// @todo Optimize: Traverses the entire state list! for(int i = 0; i < defs.states.size(); ++i) { if(runtimeDefs.stateInfo[i].owner != &runtimeDefs.mobjInfo[num]) continue; Models_CacheForState(i); if(precacheSprites) { state_t *state = Def_GetState(i); DENG2_ASSERT(state != 0); App_ResourceSystem().cache(state->sprite, spec); } /// @todo What about sounds? } } // r_main.cpp DENG_EXTERN_C void R_RenderPlayerView(int num); DENG_EXTERN_C void R_SetViewOrigin(int consoleNum, coord_t const origin[3]); DENG_EXTERN_C void R_SetViewAngle(int consoleNum, angle_t angle); DENG_EXTERN_C void R_SetViewPitch(int consoleNum, float pitch); DENG_EXTERN_C int R_ViewWindowGeometry(int consoleNum, RectRaw *geometry); DENG_EXTERN_C int R_ViewWindowOrigin(int consoleNum, Point2Raw *origin); DENG_EXTERN_C int R_ViewWindowSize(int consoleNum, Size2Raw *size); DENG_EXTERN_C void R_SetViewWindowGeometry(int consoleNum, RectRaw const *geometry, dd_bool interpolate); DENG_EXTERN_C int R_ViewPortGeometry(int consoleNum, RectRaw *geometry); DENG_EXTERN_C int R_ViewPortOrigin(int consoleNum, Point2Raw *origin); DENG_EXTERN_C int R_ViewPortSize(int consoleNum, Size2Raw *size); DENG_EXTERN_C void R_SetViewPortPlayer(int consoleNum, int viewPlayer); // sky.cpp DENG_EXTERN_C void R_SkyParams(int layer, int param, void *data); #ifdef __CLIENT__ static inline MaterialVariantSpec const &pspriteMaterialSpec() { return App_ResourceSystem().materialSpec(PSpriteContext, 0, 1, 0, 0, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 0, 1, -1, false, true, true, false); } #endif #undef R_GetSpriteInfo DENG_EXTERN_C dd_bool R_GetSpriteInfo(int spriteId, int frame, spriteinfo_t *info) { LOG_AS("Rend.GetSpriteInfo"); if(!info) return false; de::zapPtr(info); if(!App_ResourceSystem().hasSprite(spriteId, frame)) { LOG_RES_WARNING("Invalid sprite id (%i) and/or frame index (%i)") << spriteId << frame; return false; } SpriteViewAngle const &sprViewAngle = App_ResourceSystem().sprite(spriteId, frame).viewAngle(0); info->material = sprViewAngle.material; info->flip = sprViewAngle.mirrorX; if(novideo) { // We can't prepare the material. return true; } #ifdef __CLIENT__ /// @todo fixme: We should not be using the PSprite spec here. -ds MaterialAnimator &matAnimator = info->material->getAnimator(pspriteMaterialSpec()); // Ensure we have up to date info about the material. matAnimator.prepare(); Vector2i const &matDimensions = matAnimator.dimensions(); TextureVariant *tex = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; Vector2i const &texDimensions = tex->base().origin(); int const texBorder = tex->spec().variant.border; info->geometry.origin.x = -texDimensions.x + -texBorder; info->geometry.origin.y = -texDimensions.y + texBorder; info->geometry.size.width = matDimensions.x + texBorder * 2; info->geometry.size.height = matDimensions.y + texBorder * 2; tex->glCoords(&info->texCoord[0], &info->texCoord[1]); #else Texture &tex = *info->material->layer(0).stage(0).texture; info->geometry.origin.x = -tex.origin().x; info->geometry.origin.y = -tex.origin().y; info->geometry.size.width = info->material->width(); info->geometry.size.height = info->material->height(); #endif return true; } // r_util.c DENG_EXTERN_C dd_bool R_ChooseAlignModeAndScaleFactor(float* scale, int width, int height, int availWidth, int availHeight, scalemode_t scaleMode); DENG_EXTERN_C scalemode_t R_ChooseScaleMode2(int width, int height, int availWidth, int availHeight, scalemode_t overrideMode, float stretchEpsilon); DENG_EXTERN_C scalemode_t R_ChooseScaleMode(int width, int height, int availWidth, int availHeight, scalemode_t overrideMode); #undef R_SetupFog DENG_EXTERN_C void R_SetupFog(float start, float end, float density, float *rgb) { Con_Execute(CMDS_DDAY, "fog on", true, false); Con_Executef(CMDS_DDAY, true, "fog start %f", start); Con_Executef(CMDS_DDAY, true, "fog end %f", end); Con_Executef(CMDS_DDAY, true, "fog density %f", density); Con_Executef(CMDS_DDAY, true, "fog color %.0f %.0f %.0f", rgb[0] * 255, rgb[1] * 255, rgb[2] * 255); } #undef R_SetupFogDefaults DENG_EXTERN_C void R_SetupFogDefaults() { // Go with the defaults. Con_Execute(CMDS_DDAY,"fog off", true, false); } DENG_DECLARE_API(Rend) = { { DE_API_RENDER }, R_SetupFogDefaults, R_SetupFog, Rend_CacheForMobjType, Models_CacheForState, R_RenderPlayerView, R_SetViewOrigin, R_SetViewAngle, R_SetViewPitch, R_ViewWindowGeometry, R_ViewWindowOrigin, R_ViewWindowSize, R_SetViewWindowGeometry, R_SetBorderGfx, R_ViewPortGeometry, R_ViewPortOrigin, R_ViewPortSize, R_SetViewPortPlayer, R_ChooseAlignModeAndScaleFactor, R_ChooseScaleMode2, R_ChooseScaleMode, R_GetSpriteInfo, R_SkyParams, M_ScreenShot }; doomsday-stable-1.15.7/doomsday/client/src/render/r_draw.cpp0000664000175000017500000002154412641367670023327 0ustar jaakkojaakko/** @file r_draw.cpp Misc Drawing Routines. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include "clientapp.h" #include "sys_system.h" #include "render/r_main.h" #include "render/r_draw.h" #include "gl/gl_draw.h" #include "gl/gl_main.h" #include "gl/sys_opengl.h" #include "api_resource.h" #include "MaterialAnimator" #include "world/p_players.h" // displayPlayer using namespace de; // A logical ordering (twice around). enum { BG_BACKGROUND, BG_TOP, BG_RIGHT, BG_BOTTOM, BG_LEFT, BG_TOPLEFT, BG_TOPRIGHT, BG_BOTTOMRIGHT, BG_BOTTOMLEFT }; static bool inited = false; static int borderSize; static de::Uri *borderGraphicsNames[9]; /// @todo Declare the patches with URNs to avoid unnecessary duplication here -ds static patchid_t borderPatches[9]; static inline ResourceSystem &resSys() { return ClientApp::resourceSystem(); } static void loadViewBorderPatches() { borderPatches[0] = 0; for(uint i = 1; i < 9; ++i) { borderPatches[i] = resSys().declarePatch(borderGraphicsNames[i]->path()); } // Detemine the view border size. borderSize = 0; patchinfo_t info; if(!R_GetPatchInfo(borderPatches[BG_TOP], &info)) { return; } borderSize = info.geometry.size.height; } static Texture &borderTexture(int borderComp) { TextureScheme &patches = resSys().textureScheme("Patches"); DENG2_ASSERT(borderComp >= 0 && borderComp < 9); return patches.findByUniqueId(borderPatches[borderComp]).texture(); } #undef R_SetBorderGfx DENG_EXTERN_C void R_SetBorderGfx(struct uri_s const *const *paths) { DENG2_ASSERT(inited); if(!paths) return; for(uint i = 0; i < 9; ++i) { if(paths[i]) { if(!borderGraphicsNames[i]) { borderGraphicsNames[i] = new de::Uri; } *(borderGraphicsNames[i]) = *reinterpret_cast(paths[i]); } else { if(borderGraphicsNames[i]) { delete borderGraphicsNames[i]; } borderGraphicsNames[i] = 0; } } loadViewBorderPatches(); } void R_InitViewWindow() { if(Sys_IsShuttingDown()) return; for(int i = 0; i < DDMAXPLAYERS; ++i) { R_SetupDefaultViewWindow(i); } if(inited) { for(int i = 0; i < 9; ++i) { if(borderGraphicsNames[i]) { delete borderGraphicsNames[i]; } } } de::zap(borderGraphicsNames); de::zap(borderPatches); borderSize = 0; inited = true; } void R_ShutdownViewWindow() { if(!inited) return; for(int i = 0; i < 9; ++i) { if(borderGraphicsNames[i]) { delete borderGraphicsNames[i]; } } de::zap(borderGraphicsNames); inited = false; } TextureVariantSpec const &Rend_PatchTextureSpec(int flags, gl::Wrapping wrapS, gl::Wrapping wrapT) { return resSys().textureSpec(TC_UI, flags, 0, 0, 0, GL_Wrap(wrapS), GL_Wrap(wrapT), 0, -3, 0, false, false, false, false); } void R_DrawPatch(Texture &texture, int x, int y, int w, int h, bool useOffsets) { if(texture.manifest().schemeName().compareWithoutCase("Patches")) { LOG_AS("R_DrawPatch3"); LOGDEV_GL_WARNING("Cannot draw a non-patch [%p]") << dintptr(&texture); return; } TextureVariantSpec const &texSpec = Rend_PatchTextureSpec(0 | (texture.isFlagged(Texture::Monochrome) ? TSF_MONOCHROME : 0) | (texture.isFlagged(Texture::UpscaleAndSharpen) ? TSF_UPSCALE_AND_SHARPEN : 0)); GL_BindTexture(texture.prepareVariant(texSpec)); if(useOffsets) { x += texture.origin().x; y += texture.origin().y; } GL_DrawRectf2Color(x, y, w, h, 1, 1, 1, 1); } void R_DrawPatch(Texture &tex, int x, int y) { R_DrawPatch(tex, x, y, tex.width(), tex.height()); } void R_DrawPatchTiled(Texture &texture, int x, int y, int w, int h, gl::Wrapping wrapS, gl::Wrapping wrapT) { TextureVariantSpec const &spec = Rend_PatchTextureSpec(0 | (texture.isFlagged(Texture::Monochrome) ? TSF_MONOCHROME : 0) | (texture.isFlagged(Texture::UpscaleAndSharpen) ? TSF_UPSCALE_AND_SHARPEN : 0), wrapS, wrapT); GL_BindTexture(texture.prepareVariant(spec)); GL_DrawRectf2Tiled(x, y, w, h, texture.width(), texture.height()); } static MaterialVariantSpec const &bgMaterialSpec() { return resSys().materialSpec(UiContext, 0, 0, 0, 0, GL_REPEAT, GL_REPEAT, 0, -3, 0, false, false, false, false); } /// @todo Optimize: Do not search for resources (materials, textures) each frame. void R_DrawViewBorder() { DENG2_ASSERT(inited); viewport_t const *port = R_CurrentViewPort(); viewdata_t const *vd = R_ViewData(displayPlayer); DENG2_ASSERT(port != 0 && vd != 0); if(vd->window.isNull()) return; if(vd->window.size() >= port->geometry.size()) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glEnable(GL_TEXTURE_2D); glMatrixMode(GL_TEXTURE); glPushMatrix(); // Scale from viewport space to fixed 320x200 space. int border; if(port->geometry.width() >= port->geometry.height()) { glScalef(float(SCREENHEIGHT) / port->geometry.height(), float(SCREENHEIGHT) / port->geometry.height(), 1); border = float(borderSize) / SCREENHEIGHT * port->geometry.height(); } else { glScalef(float(SCREENWIDTH) / port->geometry.width(), float(SCREENWIDTH) / port->geometry.width(), 1); border = float(borderSize) / SCREENWIDTH * port->geometry.width(); } glColor4f(1, 1, 1, 1); // View background. try { MaterialAnimator &matAnimator = resSys().material(*borderGraphicsNames[BG_BACKGROUND]) .getAnimator(bgMaterialSpec()); // Ensure we've up to date info about the material. matAnimator.prepare(); GL_BindTexture(matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture); Vector2i const &matDimensions = matAnimator.dimensions(); GL_DrawCutRectf2Tiled(0, 0, port->geometry.width(), port->geometry.height(), matDimensions.x, matDimensions.y, 0, 0, vd->window.topLeft.x - border, vd->window.topLeft.y - border, vd->window.width() + 2 * border, vd->window.height() + 2 * border); } catch(MaterialManifest::MissingMaterialError const &) {} // Ignore this error. if(border) { R_DrawPatchTiled(borderTexture(BG_TOP), vd->window.topLeft.x, vd->window.topLeft.y - border, vd->window.width(), border, gl::Repeat, gl::ClampToEdge); R_DrawPatchTiled(borderTexture(BG_BOTTOM), vd->window.topLeft.x, vd->window.bottomRight.y, vd->window.width(), border, gl::Repeat, gl::ClampToEdge); R_DrawPatchTiled(borderTexture(BG_LEFT), vd->window.topLeft.x - border, vd->window.topLeft.y, border, vd->window.height(), gl::ClampToEdge, gl::Repeat); R_DrawPatchTiled(borderTexture(BG_RIGHT), vd->window.topRight().x, vd->window.topRight().y, border, vd->window.height(), gl::ClampToEdge, gl::Repeat); } glMatrixMode(GL_TEXTURE); glPopMatrix(); if(border) { R_DrawPatch(borderTexture(BG_TOPLEFT), vd->window.topLeft.x - border, vd->window.topLeft.y - border, border, border, false); R_DrawPatch(borderTexture(BG_TOPRIGHT), vd->window.topRight().x, vd->window.topLeft.y - border, border, border, false); R_DrawPatch(borderTexture(BG_BOTTOMRIGHT), vd->window.bottomRight.x, vd->window.bottomRight.y, border, border, false); R_DrawPatch(borderTexture(BG_BOTTOMLEFT), vd->window.bottomLeft().x - border, vd->window.bottomRight.y, border, border, false); } glDisable(GL_TEXTURE_2D); } doomsday-stable-1.15.7/doomsday/client/src/render/drawlist.cpp0000664000175000017500000007407512641367670023711 0ustar jaakkojaakko/** @file drawlist.cpp Drawable primitive list. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "render/drawlist.h" #include "gl/gl_main.h" #include "render/rend_main.h" #include "clientapp.h" #include #include using namespace de; /** * Drawing condition flags. * * @todo Most of these are actually list specification parameters. Rather than * set them each time an identified list is drawn it would be better to record * in the list itself. -ds */ enum DrawCondition { NoBlend = 0x00000001, Blend = 0x00000002, SetLightEnv0 = 0x00000004, SetLightEnv1 = 0x00000008, JustOneLight = 0x00000010, ManyLights = 0x00000020, SetBlendMode = 0x00000040, // Primitive-specific blending. SetMatrixDTexture0 = 0x00000080, SetMatrixDTexture1 = 0x00000100, SetMatrixTexture0 = 0x00000200, SetMatrixTexture1 = 0x00000400, NoColor = 0x00000800, Skip = 0x80000000, SetLightEnv = SetLightEnv0 | SetLightEnv1, SetMatrixDTexture = SetMatrixDTexture0 | SetMatrixDTexture1, SetMatrixTexture = SetMatrixTexture0 | SetMatrixTexture1 }; Q_DECLARE_FLAGS(DrawConditions, DrawCondition) Q_DECLARE_OPERATORS_FOR_FLAGS(DrawConditions) DENG2_PIMPL(DrawList) { /** * Each Element begins a block of GL commands/geometry to apply/transfer. */ struct Element { // Must be an offset since the list is sometimes reallocated. uint size; ///< Size of this element (zero = n/a). struct Data { Store *buffer; gl::Primitive type; // Element indices into the global backing store for the geometry. // These are always contiguous and all are used (some are shared): // indices[0] is the base, and indices[1...n] > indices[0]. uint numIndices; uint *indices; bool oneLight; bool manyLights; blendmode_t blendMode; DGLuint modTexture; Vector3f modColor; Vector2f texOffset; Vector2f texScale; Vector2f dtexOffset; Vector2f dtexScale; /** * Draw the geometry for this element. */ void draw(DrawConditions const &conditions, TexUnitMap const &texUnitMap) { if(conditions & SetLightEnv) { // Use the correct texture and color for the light. glActiveTexture((conditions & SetLightEnv0)? GL_TEXTURE0 : GL_TEXTURE1); GL_BindTextureUnmanaged(!renderTextures? 0 : modTexture, gl::ClampToEdge, gl::ClampToEdge); float modColorV[4] = { modColor.x, modColor.y, modColor.z, 0 }; glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, modColorV); } if(conditions & SetMatrixTexture) { // Primitive-specific texture translation & scale. if(conditions & SetMatrixTexture0) { glActiveTexture(GL_TEXTURE0); glMatrixMode(GL_TEXTURE); glPushMatrix(); glLoadIdentity(); glTranslatef(texOffset.x * texScale.x, texOffset.y * texScale.y, 1); glScalef(texScale.x, texScale.y, 1); } if(conditions & SetMatrixTexture1) { glActiveTexture(GL_TEXTURE1); glMatrixMode(GL_TEXTURE); glPushMatrix(); glLoadIdentity(); glTranslatef(texOffset.x * texScale.x, texOffset.y * texScale.y, 1); glScalef(texScale.x, texScale.y, 1); } } if(conditions & SetMatrixDTexture) { // Primitive-specific texture translation & scale. if(conditions & SetMatrixDTexture0) { glActiveTexture(GL_TEXTURE0); glMatrixMode(GL_TEXTURE); glPushMatrix(); glLoadIdentity(); glTranslatef(dtexOffset.x * dtexScale.x, dtexOffset.y * dtexScale.y, 1); glScalef(dtexScale.x, dtexScale.y, 1); } if(conditions & SetMatrixDTexture1) { glActiveTexture(GL_TEXTURE1); glMatrixMode(GL_TEXTURE); glPushMatrix(); glLoadIdentity(); glTranslatef(dtexOffset.x * dtexScale.x, dtexOffset.y * dtexScale.y, 1); glScalef(dtexScale.x, dtexScale.y, 1); } } if(conditions & SetBlendMode) { // Primitive-specific blending. Not used in all lists. GL_BlendMode(blendMode); } glBegin(type == gl::TriangleStrip? GL_TRIANGLE_STRIP : GL_TRIANGLE_FAN); for(uint i = 0; i < numIndices; ++i) { uint const index = indices[i]; for(int j = 0; j < numTexUnits; ++j) { if(texUnitMap[j]) { Vector2f const &tc = buffer->texCoords[texUnitMap[j] - 1][index]; glMultiTexCoord2f(GL_TEXTURE0 + j, tc.x, tc.y); } } if(!(conditions & NoColor)) { Vector4ub const &color = buffer->colorCoords[index]; glColor4ub(color.x, color.y, color.z, color.w); } Vector3f const &pos = buffer->posCoords[index]; glVertex3f(pos.x, pos.z, pos.y); } glEnd(); // Restore the texture matrix if changed. if(conditions & SetMatrixDTexture) { if(conditions & SetMatrixDTexture0) { glActiveTexture(GL_TEXTURE0); glMatrixMode(GL_TEXTURE); glPopMatrix(); } if(conditions & SetMatrixDTexture1) { glActiveTexture(GL_TEXTURE1); glMatrixMode(GL_TEXTURE); glPopMatrix(); } } if(conditions & SetMatrixTexture) { if(conditions & SetMatrixTexture0) { glActiveTexture(GL_TEXTURE0); glMatrixMode(GL_TEXTURE); glPopMatrix(); } if(conditions & SetMatrixTexture1) { glActiveTexture(GL_TEXTURE1); glMatrixMode(GL_TEXTURE); glPopMatrix(); } } } } data; Element *next() { if(!size) return 0; Element *elem = (Element *) ((byte *) (this) + size); if(!elem->size) return 0; return elem; } }; Spec spec; ///< List specification. size_t dataSize; ///< Number of bytes allocated for the data. byte *data; ///< Data for a number of polygons (The List). byte *cursor; ///< Data pointer for reading/writing. Element *last; ///< Last element (if any). Instance(Public *i, Spec const &spec) : Base(i) , spec(spec) , dataSize(0) , data(0) , cursor(0) , last(0) {} ~Instance() { clearAllData(); } void clearAllData() { if(data) { // All the list data will be destroyed. Z_Free(data); data = 0; #ifdef DENG_DEBUG Z_CheckHeap(); #endif } cursor = 0; last = 0; dataSize = 0; } /** * @return Start of the allocated data. */ void *allocateData(uint bytes) { // Number of extra bytes to keep allocated in the end of each list. int const PADDING = 16; if(!bytes) return 0; // We require the extra bytes because we want that the end of the list // data is always safe for writing-in-advance. This is needed when the // 'end of data' marker is written. int const startOffset = cursor - data; size_t const required = startOffset + bytes + PADDING; // First check that the data buffer of the list is large enough. if(required > dataSize) { // Offsets must be preserved. byte *oldData = data; int const cursorOffset = (cursor? cursor - oldData : -1); int const lastOffset = (last? (byte *) last - oldData : -1); // Allocate more memory for the data buffer. if(dataSize == 0) { dataSize = 1024; } while(dataSize < required) { dataSize *= 2; } data = (byte *) Z_Realloc(data, dataSize, PU_APPSTATIC); // Restore main pointers. cursor = (cursorOffset >= 0? data + cursorOffset : data); last = (lastOffset >= 0? (Element *) (data + lastOffset) : 0); // Restore in-list pointers. // When the list is resized, pointers in the primitives need to be // restored so that they point to the new list data. if(oldData) { for(Element *elem = first(); elem && elem <= last; elem = elem->next()) { if(elem->data.indices) { elem->data.indices = (uint *) (data + ((byte *) elem->data.indices - oldData)); } } } } // Advance the cursor. cursor += bytes; return data + startOffset; } void allocateIndices(uint numIndices, uint base) { // Note that last may be reallocated during allocateData. last->data.numIndices = numIndices; // Temporary variable to avoid segfault on Ubuntu linux CMB uint * lti = (uint *) allocateData(sizeof(uint) * numIndices); last->data.indices = lti; for(uint i = 0; i < numIndices; ++i) { last->data.indices[i] = base + i; } } Element *newElement(Store &buffer, gl::Primitive primitive) { // This becomes the new last element. last = (Element *) allocateData(sizeof(Element)); last->size = 0; last->data.buffer = &buffer; last->data.type = primitive; last->data.indices = 0; last->data.numIndices = 0; last->data.oneLight = last->data.manyLights = false; return last; } void endWrite() { // The element has been written, update the size in the header. last->size = cursor - (byte *) last; // Write the end marker (will be overwritten by the next write). The // idea is that this zero is interpreted as the size of the following // Element. *(int *) cursor = 0; } /// Returns a pointer to the first element in the list; otherwise @c 0. Element *first() const { Element *elem = (Element *)data; if(!elem->size) return 0; return elem; } /** * Configure GL state for drawing in this @a mode. * * @return The conditions to select primitives. */ DrawConditions pushGLState(DrawMode mode) { switch(mode) { case DM_SKYMASK: DENG2_ASSERT(spec.group == SkyMaskGeom); // Render all primitives on the list without discrimination. return NoColor; case DM_ALL: // All surfaces. DENG2_ASSERT(spec.group == UnlitGeom || spec.group == LitGeom); // Should we do blending? if(spec.unit(TU_INTER).hasTexture()) { // Blend between two textures, modulate with primary color. DENG2_ASSERT(numTexUnits >= 2); GL_SelectTexUnits(2); GL_BindTo(spec.unit(TU_PRIMARY), 0); GL_BindTo(spec.unit(TU_INTER), 1); GL_ModulateTexture(2); float color[4] = { 0, 0, 0, spec.unit(TU_INTER).opacity }; glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color); } else if(!spec.unit(TU_PRIMARY).hasTexture()) { // Opaque texture-less surface. return 0; } else { // Normal modulation. GL_SelectTexUnits(1); GL_Bind(spec.unit(TU_PRIMARY)); GL_ModulateTexture(1); } if(spec.unit(TU_INTER).hasTexture()) { return SetMatrixTexture0 | SetMatrixTexture1; } return SetMatrixTexture0; case DM_LIGHT_MOD_TEXTURE: DENG2_ASSERT(spec.group == LitGeom); // Modulate sector light, dynamic light and regular texture. GL_BindTo(spec.unit(TU_PRIMARY), 1); return SetMatrixTexture1 | SetLightEnv0 | JustOneLight | NoBlend; case DM_TEXTURE_PLUS_LIGHT: DENG2_ASSERT(spec.group == LitGeom); GL_BindTo(spec.unit(TU_PRIMARY), 0); return SetMatrixTexture0 | SetLightEnv1 | NoBlend; case DM_FIRST_LIGHT: DENG2_ASSERT(spec.group == LitGeom); // Draw all primitives with more than one light // and all primitives which will have a blended texture. return SetLightEnv0 | ManyLights | Blend; case DM_BLENDED: { DENG2_ASSERT(spec.group == UnlitGeom || spec.group == LitGeom); // Only render the blended surfaces. if(!spec.unit(TU_INTER).hasTexture()) { return Skip; } DENG2_ASSERT(numTexUnits >= 2); GL_SelectTexUnits(2); GL_BindTo(spec.unit(TU_PRIMARY), 0); GL_BindTo(spec.unit(TU_INTER), 1); GL_ModulateTexture(2); float color[4] = { 0, 0, 0, spec.unit(TU_INTER).opacity }; glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color); return SetMatrixTexture0 | SetMatrixTexture1; } case DM_BLENDED_FIRST_LIGHT: DENG2_ASSERT(spec.group == LitGeom); // Only blended surfaces. if(!spec.unit(TU_INTER).hasTexture()) { return Skip; } return SetMatrixTexture1 | SetLightEnv0; case DM_WITHOUT_TEXTURE: DENG2_ASSERT(spec.group == LitGeom); // Only render geometries affected by dynlights. return 0; case DM_LIGHTS: DENG2_ASSERT(spec.group == LightGeom); // These lists only contain light geometries. GL_Bind(spec.unit(TU_PRIMARY)); return 0; case DM_BLENDED_MOD_TEXTURE: DENG2_ASSERT(spec.group == LitGeom); // Blending required. if(!spec.unit(TU_INTER).hasTexture()) { break; } // Intentional fall-through. case DM_MOD_TEXTURE: case DM_MOD_TEXTURE_MANY_LIGHTS: DENG2_ASSERT(spec.group == LitGeom); // Texture for surfaces with (many) dynamic lights. // Should we do blending? if(spec.unit(TU_INTER).hasTexture()) { // Mode 3 actually just disables the second texture stage, // which would modulate with primary color. DENG2_ASSERT(numTexUnits >= 2); GL_SelectTexUnits(2); GL_BindTo(spec.unit(TU_PRIMARY), 0); GL_BindTo(spec.unit(TU_INTER), 1); GL_ModulateTexture(3); float color[4] = { 0, 0, 0, spec.unit(TU_INTER).opacity }; glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color); // Render all geometry. return SetMatrixTexture0 | SetMatrixTexture1; } // No modulation at all. GL_SelectTexUnits(1); GL_Bind(spec.unit(TU_PRIMARY)); GL_ModulateTexture(0); if(mode == DM_MOD_TEXTURE_MANY_LIGHTS) { return SetMatrixTexture0 | ManyLights; } return SetMatrixTexture0; case DM_UNBLENDED_MOD_TEXTURE_AND_DETAIL: DENG2_ASSERT(spec.group == LitGeom); // Blending is not done now. if(spec.unit(TU_INTER).hasTexture()) { break; } if(spec.unit(TU_PRIMARY_DETAIL).hasTexture()) { GL_SelectTexUnits(2); GL_ModulateTexture(9); // Tex+Detail, no color. GL_BindTo(spec.unit(TU_PRIMARY), 0); GL_BindTo(spec.unit(TU_PRIMARY_DETAIL), 1); return SetMatrixTexture0 | SetMatrixDTexture1; } else { GL_SelectTexUnits(1); GL_ModulateTexture(0); GL_Bind(spec.unit(TU_PRIMARY)); return SetMatrixTexture0; } break; case DM_ALL_DETAILS: DENG2_ASSERT(spec.group == UnlitGeom || spec.group == LitGeom); if(spec.unit(TU_PRIMARY_DETAIL).hasTexture()) { GL_Bind(spec.unit(TU_PRIMARY_DETAIL)); return SetMatrixDTexture0; } break; case DM_UNBLENDED_TEXTURE_AND_DETAIL: DENG2_ASSERT(spec.group == UnlitGeom || spec.group == LitGeom); // Only unblended. Details are optional. if(spec.unit(TU_INTER).hasTexture()) { break; } if(spec.unit(TU_PRIMARY_DETAIL).hasTexture()) { GL_SelectTexUnits(2); GL_ModulateTexture(8); GL_BindTo(spec.unit(TU_PRIMARY), 0); GL_BindTo(spec.unit(TU_PRIMARY_DETAIL), 1); return SetMatrixTexture0 | SetMatrixDTexture1; } else { // Normal modulation. GL_SelectTexUnits(1); GL_ModulateTexture(1); GL_Bind(spec.unit(TU_PRIMARY)); return SetMatrixTexture0; } break; case DM_BLENDED_DETAILS: { DENG2_ASSERT(spec.group == UnlitGeom || spec.group == LitGeom); // We'll only render blended primitives. if(!spec.unit(TU_INTER).hasTexture()) { break; } if(!spec.unit(TU_PRIMARY_DETAIL).hasTexture() || !spec.unit(TU_INTER_DETAIL).hasTexture()) { break; } GL_BindTo(spec.unit(TU_PRIMARY_DETAIL), 0); GL_BindTo(spec.unit(TU_INTER_DETAIL), 1); float color[4] = { 0, 0, 0, spec.unit(TU_INTER_DETAIL).opacity }; glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color); return SetMatrixDTexture0 | SetMatrixDTexture1; } case DM_SHADOW: DENG2_ASSERT(spec.group == ShadowGeom); if(spec.unit(TU_PRIMARY).hasTexture()) { GL_Bind(spec.unit(TU_PRIMARY)); } else { GL_BindTextureUnmanaged(0); } if(!spec.unit(TU_PRIMARY).hasTexture()) { // Apply a modelview shift. glMatrixMode(GL_MODELVIEW); glPushMatrix(); // Scale towards the viewpoint to avoid Z-fighting. glTranslatef(vOrigin.x, vOrigin.y, vOrigin.z); glScalef(.99f, .99f, .99f); glTranslatef(-vOrigin.x, -vOrigin.y, -vOrigin.z); } return 0; case DM_MASKED_SHINY: DENG2_ASSERT(spec.group == ShineGeom); if(spec.unit(TU_INTER).hasTexture()) { GL_SelectTexUnits(2); // The intertex holds the info for the mask texture. GL_BindTo(spec.unit(TU_INTER), 1); float color[4] = { 0, 0, 0, 1 }; glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color); } // Intentional fall-through. case DM_ALL_SHINY: case DM_SHINY: DENG2_ASSERT(spec.group == ShineGeom); GL_BindTo(spec.unit(TU_PRIMARY), 0); if(!spec.unit(TU_INTER).hasTexture()) { GL_SelectTexUnits(1); } // Render all primitives. if(mode == DM_ALL_SHINY) { return SetBlendMode; } if(mode == DM_MASKED_SHINY) { return SetBlendMode | SetMatrixTexture1; } return SetBlendMode | NoBlend; default: break; } // Draw nothing for the specified mode. return Skip; } /** * Restore GL state after drawing in the specified @a mode. */ void popGLState(DrawMode mode) { switch(mode) { default: break; case DM_ALL: if(spec.unit(TU_INTER).hasTexture()) { GL_SelectTexUnits(1); GL_ModulateTexture(1); } break; case DM_BLENDED: if(spec.unit(TU_INTER).hasTexture()) { GL_SelectTexUnits(1); GL_ModulateTexture(1); } break; case DM_BLENDED_MOD_TEXTURE: case DM_MOD_TEXTURE: case DM_MOD_TEXTURE_MANY_LIGHTS: if(spec.unit(TU_INTER).hasTexture()) { GL_SelectTexUnits(1); GL_ModulateTexture(1); } else if(mode != DM_BLENDED_MOD_TEXTURE) { GL_ModulateTexture(1); } break; case DM_UNBLENDED_MOD_TEXTURE_AND_DETAIL: if(!spec.unit(TU_INTER).hasTexture()) { if(spec.unit(TU_PRIMARY_DETAIL).hasTexture()) { GL_SelectTexUnits(1); GL_ModulateTexture(1); } else { GL_ModulateTexture(1); } } break; case DM_UNBLENDED_TEXTURE_AND_DETAIL: if(!spec.unit(TU_INTER).hasTexture() && spec.unit(TU_PRIMARY_DETAIL).hasTexture()) { GL_SelectTexUnits(1); GL_ModulateTexture(1); } break; case DM_SHADOW: if(!spec.unit(TU_PRIMARY).hasTexture()) { // Restore original modelview matrix. glMatrixMode(GL_MODELVIEW); glPopMatrix(); } break; case DM_SHINY: case DM_ALL_SHINY: case DM_MASKED_SHINY: GL_BlendMode(BM_NORMAL); if(mode == DM_MASKED_SHINY && spec.unit(TU_INTER).hasTexture()) { GL_SelectTexUnits(1); } break; } } }; DrawList::DrawList(Spec const &spec) : d(new Instance(this, spec)) {} bool DrawList::isEmpty() const { return d->last == 0; } DrawList &DrawList::write(gl::Primitive primitive, blendmode_t blendMode, Vector2f const &texScale, Vector2f const &texOffset, Vector2f const &detailTexScale, Vector2f const &detailTexOffset, bool isLit, uint vertCount, Vector3f const *posCoords, Vector4f const *colorCoords, Vector2f const *texCoords, Vector2f const *interTexCoords, GLuint modTexture, Vector3f const *modColor, Vector2f const *modTexCoords) { DENG2_ASSERT(vertCount >= 3); // Rationalize write arguments. if(d->spec.group == SkyMaskGeom || d->spec.group == LightGeom || d->spec.group == ShadowGeom) { isLit = false; modTexture = 0; modColor = 0; } Instance::Element *elem = d->newElement(ClientApp::renderSystem().buffer(), primitive); // Is the geometry lit? if(modTexture && !isLit) { elem->data.oneLight = true; // Using modulation. } else if(modTexture || isLit) { elem->data.manyLights = true; } // Configure the GL state to be applied when this geometry is drawn later. elem->data.blendMode = blendMode; elem->data.modTexture = modTexture; elem->data.modColor = modColor? *modColor : Vector3f(); elem->data.texScale = texScale; elem->data.texOffset = texOffset; elem->data.dtexScale = detailTexScale; elem->data.dtexOffset = detailTexOffset; // Allocate geometry from the backing store. uint base = elem->data.buffer->allocateVertices(vertCount); // Setup the indices. d->allocateIndices(vertCount, base); for(uint i = 0; i < vertCount; ++i) { elem->data.buffer->posCoords[base + i] = posCoords[i]; // Sky masked polys need nothing more. if(d->spec.group == SkyMaskGeom) continue; // Primary texture coordinates. if(d->spec.unit(TU_PRIMARY).hasTexture()) { DENG2_ASSERT(texCoords != 0); elem->data.buffer->texCoords[Store::TCA_MAIN][base + i] = texCoords[i]; } // Secondary texture coordinates. if(d->spec.unit(TU_INTER).hasTexture()) { DENG2_ASSERT(interTexCoords != 0); elem->data.buffer->texCoords[Store::TCA_BLEND][base + i] = interTexCoords[i]; } // First light texture coordinates. if((elem->data.oneLight || elem->data.manyLights) && IS_MTEX_LIGHTS) { DENG2_ASSERT(modTexCoords != 0); elem->data.buffer->texCoords[Store::TCA_LIGHT][base + i] = modTexCoords[i]; } // Color. Vector4ub &color = elem->data.buffer->colorCoords[base + i]; if(colorCoords) { Vector4f const &srcColor = colorCoords[i]; // We should not be relying on clamping at this late stage... DENG2_ASSERT(INRANGE_OF(srcColor.x, 0.f, 1.f)); DENG2_ASSERT(INRANGE_OF(srcColor.y, 0.f, 1.f)); DENG2_ASSERT(INRANGE_OF(srcColor.z, 0.f, 1.f)); DENG2_ASSERT(INRANGE_OF(srcColor.w, 0.f, 1.f)); color = Vector4ub(dbyte(255 * de::clamp(0.f, srcColor.x, 1.f)), dbyte(255 * de::clamp(0.f, srcColor.y, 1.f)), dbyte(255 * de::clamp(0.f, srcColor.z, 1.f)), dbyte(255 * de::clamp(0.f, srcColor.w, 1.f))); } else { color = Vector4ub(255, 255, 255, 255); } } d->endWrite(); return *this; } void DrawList::draw(DrawMode mode, TexUnitMap const &texUnitMap) const { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // Setup GL state for this list. DrawConditions conditions = d->pushGLState(mode); // Should we just skip all this? if(conditions & Skip) return; // Assume no state changes were made. bool bypass = false; if(d->spec.unit(TU_INTER).hasTexture()) { // Is blending allowed? if(conditions.testFlag(NoBlend)) { return; } // Should all blended primitives be included? if(conditions.testFlag(Blend)) { // The other conditions will be bypassed. bypass = true; } } // Check conditions dependant on primitive-specific values once before // entering the loop. If none of the conditions are true for this list // then we can bypass the skip tests completely during iteration. if(!bypass) { if(!conditions.testFlag(JustOneLight) && !conditions.testFlag(ManyLights)) { bypass = true; } } bool skip = false; for(Instance::Element *elem = d->first(); elem; elem = elem->next()) { // Check for skip conditions. if(!bypass) { skip = false; if(conditions.testFlag(JustOneLight) && elem->data.manyLights) { skip = true; } else if(conditions.testFlag(ManyLights) && elem->data.oneLight) { skip = true; } } if(!skip) { elem->data.draw(conditions, texUnitMap); DENG2_ASSERT(!Sys_GLCheckError()); } } // Some modes require cleanup. d->popGLState(mode); } DrawList::Spec &DrawList::spec() { return d->spec; } DrawList::Spec const &DrawList::spec() const { return d->spec; } void DrawList::clear() { d->clearAllData(); } void DrawList::rewind() { d->cursor = d->data; d->last = 0; } doomsday-stable-1.15.7/doomsday/client/src/render/angleclipper.cpp0000664000175000017500000007333312641367670024521 0ustar jaakkojaakko/** @file angleclipper.cpp Angle Clipper. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "render/angleclipper.h" #include #include #include #include #include "dd_def.h" #include "render/rend_main.h" using namespace de; namespace internal { /** * @param point @em View-relative point in map-space. */ static inline binangle_t pointToAngle(Vector2d const &point) { // Shift for more accuracy; return bamsAtan2(dint(point.y * 100), dint(point.x * 100)); } /** * Simple data structure for pooling POD elements. Note that unlike a traditional * ObjectPool (pattern), the pooled elements are @em not owned by the pool! */ class ElementPool { public: /** * Base for POD elements. */ struct Element { private: Element *_prev, *_next; friend class ElementPool; }; /** * Begin reusing elements in the pool. */ void rewind() { _rover = _first; } /** * Add a new @em unused object to the pool. * * @param elem Element to be linked in the pool. Ownership is unaffected. */ void add(Element *elem) { // Link it to the start of the rover's list. if(!_last) _last = elem; if(_first) _first->_prev = elem; elem->_next = _first; elem->_prev = nullptr; _first = elem; } /** * Returns a pointer to the next unused element in the pool; otherwise @c nullptr. */ void *get() { if(!_rover) return nullptr; // We'll use this. Element *next = _rover; _rover = _rover->_next; return next; } /** * Release the element @a elem (@important which is assumed to have been added * previously!), moving it to the list of used elements, for later reuse. */ void release(Element *elem) { DENG2_ASSERT(_last); if(elem == _last) { DENG2_ASSERT(!_rover); // We can only remove the last if all elements are already in use. _rover = elem; return; } DENG2_ASSERT(elem->_next); // Unlink from the list entirely. elem->_next->_prev = elem->_prev; if(elem->_prev) { elem->_prev->_next = elem->_next; } else { _first = _first->_next; _first->_prev = nullptr; } // Put it back to the end of the list. _last->_next = elem; elem->_prev = _last; elem->_next = nullptr; _last = elem; // If all were in use, set the rover here. Otherwise the rover can stay // where it is. if(!_rover) { _rover = _last; } } private: Element *_first = nullptr; Element *_last = nullptr; Element *_rover = nullptr; }; } // namespace internal using namespace ::internal; DENG2_PIMPL_NOREF(AngleClipper) { /// Specialized AngleRange for half-space clipping. struct Clipper : public ElementPool::Element, AngleRange { Clipper *prev; Clipper *next; }; ElementPool clipNodes; ///< The list of clipnodes. Clipper *clipHead = nullptr; ///< Head of the clipped-range list. /// Specialized AngleRange for half-space occlusion. struct Occluder : public ElementPool::Element, AngleRange { Occluder *prev; Occluder *next; bool topHalf; ///< @c true= top, rather than bottom, half. Vector3f normal; ///< Of the occlusion plane. }; ElementPool occNodes; ///< The list of occlusion nodes. Occluder *occHead = nullptr; ///< Head of the occlusion-range list. QVector angleBuf; ///< Scratch buffer for sorting angles. ~Instance() { clearRangeList(&clipHead); clearRangeList(&occHead); } template void clearRangeList(NodeType **head) { DENG2_ASSERT(head); while(*head) { auto *next = static_cast((*head)->next); delete *head; *head = next; } } /** * The specified range must be safe! */ dint isRangeVisible(binangle_t from, binangle_t to) const { for(Clipper *i = clipHead; i; i = i->next) { if(from >= i->from && to <= i->to) return false; } // No clip-node fully contained the specified range. return true; } /** * @return Non-zero iff the range is not entirely clipped; otherwise @c 0. */ dint safeCheckRange(binangle_t from, binangle_t to) const { if(from > to) { // The range wraps around. return (isRangeVisible(from, BANG_MAX) || isRangeVisible(0, to)); } return isRangeVisible(from, to); } void removeRange(Clipper *crange) { // If this is the head, move it. if(clipHead == crange) clipHead = crange->next; if(crange->prev) crange->prev->next = crange->next; if(crange->next) crange->next->prev = crange->prev; // We're done with this range - mark it as free for reuse. clipNodes.release(crange); } Clipper *newClipNode(binangle_t from, binangle_t to) { // Perhaps a previously-used clip-node can be reused? auto *crange = reinterpret_cast(clipNodes.get()); if(!crange) { // No, allocate another. clipNodes.add(crange = new Clipper); } // (Re)Configure. crange->from = from; crange->to = to; crange->prev = nullptr; crange->next = nullptr; return crange; } void addRange(binangle_t from, binangle_t to) { // This range becomes a solid segment: cut everything away from the // corresponding occlusion range. cutOcclusionRange(from, to); // If there is no head, this will be the first range. if(!clipHead) { clipHead = newClipNode(from, to); /* LOG_AS("AngleClipper::addRange"); LOG_DEBUG(String("New head added: %1 => %2") .arg(clipHead->from, 0, 16) .arg(clipHead->to, 0, 16)); */ return; } // There are previous ranges. Check that the new range isn't contained // by any of them. for(Clipper *i = clipHead; i; i = i->next) { /* LOG_AS("AngleClipper::addRange"); LOG_DEBUG(String("0x%1: %2 => %3") .arg((quintptr)i, QT_POINTER_SIZE * 2, 16, QChar('0')) .arg(i->from, 0, 16) .arg(i->to, 0, 16)); */ if(from >= i->from && to <= i->to) { /* LOG_AS("AngleClipper::addRange"); LOG_DEBUG("Range already exists"); */ return; // The new range already exists. } #ifdef DENG2_DEBUG if(i == i->next) throw Error("AngleClipper::addRange", String("loop1 0x%1 linked to itself: %2 => %3") .arg((quintptr)i, QT_POINTER_SIZE * 2, 16, QChar('0')) .arg(i->from, 0, 16) .arg(i->to, 0, 16)); #endif } // Now check if any of the old ranges are contained by the new one. for(Clipper *i = clipHead; i;) { if(i->from >= from && i->to <= to) { Clipper *contained = i; /* LOG_AS("AngleClipper::addRange"); LOG_DEBUG(String("Removing contained range %1 => %2") .arg(contained->from, 0, 16) .arg(contained->to, 0, 16)); */ i = i->next; removeRange(contained); continue; } i = i->next; } // Now it is possible that the new range overlaps one or two old ranges. // If two are overlapped, they are consecutive. First we'll try to find // a range that overlaps the beginning. Clipper *crange = nullptr; for(Clipper *i = clipHead; i; i = i->next) { // In preparation for the next stage, find a good spot for the range. if(i->from < to) { // After this one. crange = i; } if(i->from >= from && i->from <= to) { // New range's end and i's beginning overlap. i's end is outside. // Otherwise it would have been already removed. // It suffices to adjust i. /* LOG_AS("AngleClipper::addRange"); LOG_DEBUG(String("Overlapping start: %1 => %2 - adjusting to %3 => %4") .arg(i->from, 0, 16) .arg(i->to, 0, 16) .arg(from, 0, 16) .arg(i->to, 0, 16)); */ i->from = from; return; } // Check an overlapping end. if(i->to >= from && i->to <= to) { // Now it's possible that the i->next's beginning overlaps the // new range's end. In that case there will be a merger. /* LOG_AS("AngleClipper::addRange"); LOG_DEBUG(String("Overlapping end: %1 => %2") .arg(i->from, 0, 16) .arg(i->to, 0, 16)); */ crange = i->next; if(!crange) { i->to = to; /* LOG_AS("AngleClipper::addRange"); LOG_DEBUG(String("No next, adjusting end (now %1 => %2)") .arg(i->from, 0, 16) .arg(i->to, 0, 16)); */ } else { if(crange->from <= to) { // A fusion will commence. Ci will eat the new range // *and* crange. i->to = crange->to; /* LOG_AS("AngleClipper::addRange"); LOG_DEBUG(String("merging with the next (%1 => %2)") .arg(crange->from, 0, 16) .arg(crange->to, 0, 16)); */ removeRange(crange); } else { // Not overlapping. i->to = to; /* LOG_AS("AngleClipper::addRange"); LOG_DEBUG(String("Not merger w/next (now %1 => %2)") .arg(i->from, 0, 16) .arg(i->to, 0, 16)); */ } } return; } } // Still here? Now we know for sure that the range is disconnected from // the others. We still need to find a good place for it. Crange will // mark the spot. if(!crange) { // We have a new head. crange = clipHead; clipHead = newClipNode(from, to); clipHead->next = crange; if(crange) crange->prev = clipHead; } else { // Add the new range after crange. Clipper *added = newClipNode(from, to); added->next = crange->next; if(added->next) added->next->prev = added; added->prev = crange; crange->next = added; } } void removeOcclusionRange(Occluder *orange) { // If this is the head, move it. if(occHead == orange) occHead = orange->next; if(orange->prev) orange->prev->next = orange->next; if(orange->next) orange->next->prev = orange->prev; // We're done with this range - mark it as free for reuse. occNodes.release(orange); } Occluder *newOcclusionRange(binangle_t from, binangle_t to, Vector3f const &normal, bool topHalf) { // Perhaps a previously-used occluder can be reused? auto *orange = reinterpret_cast(occNodes.get()); if(!orange) { // No, allocate another. occNodes.add(orange = new Occluder); } // (Re)Configure. orange->from = from; orange->to = to; orange->prev = nullptr; orange->next = nullptr; orange->topHalf = topHalf; orange->normal = normal; return orange; } /** * @pre The given range is "safe". */ void addOcclusionRange(binangle_t from, binangle_t to, Vector3f const &normal, bool topHalf) { // Is the range valid? if(from > to) return; // A new range will be added. Occluder *newor = newOcclusionRange(from, to, normal, topHalf); // Are there any previous occlusion nodes? if(!occHead) { // No; this is the first. occHead = newor; occHead->next = occHead->prev = nullptr; return; } /// @todo Optimize: Remove existing oranges that are fully contained by /// the new orange. But how to do the check efficiently? // Add the new occlusion range to the appropriate position. Occluder *after = nullptr; for(Occluder *orange = occHead; orange; orange = orange->next) { // The list of oranges is sorted by the start angle. // Find the first range whose start is greater than the new one. if(orange->from > from) { // Add before this one. newor->next = orange; newor->prev = orange->prev; orange->prev = newor; if(newor->prev) newor->prev->next = newor; else occHead = newor; // We have a new head. return; } after = orange; } // Add the new range to the end of the list. after->next = newor; newor->prev = after; newor->next = nullptr; } /** * If necessary, cut the given range in two. */ void safeAddOcclusionRange(binangle_t startAngle, binangle_t endAngle, Vector3f const &normal, bool tophalf) { // Is this range already clipped? if(!safeCheckRange(startAngle, endAngle)) return; if(startAngle > endAngle) { // The range has to be added in two parts. addOcclusionRange(startAngle, BANG_MAX, normal, tophalf); DENG2_DEBUG_ONLY(occlusionRanger(3)); addOcclusionRange(0, endAngle, normal, tophalf); DENG2_DEBUG_ONLY(occlusionRanger(4)); } else { // Add the range as usual. addOcclusionRange(startAngle, endAngle, normal, tophalf); DENG2_DEBUG_ONLY(occlusionRanger(5)); } } /** * Attempts to merge the two given occnodes. * * @return @c 0= Could not be merged. * @c 1= orange was merged into other. * @c 2= other was merged into orange. */ dint tryMergeOccludes(Occluder *orange, Occluder *other) { // We can't test this steep planes. if(!orange->normal.z) return 0; // Where do they cross? Vector3f cross = orange->normal.cross(other->normal); if(!cross.x && !cross.y && !cross.z) { // These two planes are exactly the same! Remove one. removeOcclusionRange(orange); return 1; } // The cross angle must be outside the range. binangle_t crossAngle = bamsAtan2(dint(cross.y), dint(cross.x)); if(crossAngle >= orange->from && crossAngle <= orange->to) return 0; // Inside the range, can't do a thing. /// @todo Is it not possible to consistently determine the direction at /// which cross (vector) is pointing? crossAngle += BANG_180; if(crossAngle >= orange->from && crossAngle <= orange->to) return 0; // Inside the range, can't do a thing. // Now we must determine which plane occludes which. // Pick a point in the middle of the range. crossAngle = (orange->from + orange->to) >> (1 + BAMS_BITS - 13); cross.x = 100 * FIX2FLT(fineCosine[crossAngle]); cross.y = 100 * FIX2FLT(finesine [crossAngle]); cross.z = -(orange->normal.x * cross.x + orange->normal.y * cross.y) / orange->normal.z; // Is orange occluded by the other one? if(cross.dot(other->normal) < 0) { // No; then the other one is occluded by us. Remove it instead. removeOcclusionRange(other); return 2; } else { removeOcclusionRange(orange); return 1; } } /** * Try to merge oranges with matching ranges. (Quite a number may be produced * as a result of the cuts.) */ void mergeOccludes() { for(Occluder *orange = occHead; orange && orange->next; ) { // As orange might be removed - remember the next one. auto *next = orange->next; // Find a good one to test with. for(Occluder *other = next; other && orange->from == other->from; other = other->next) { if(other->topHalf != orange->topHalf) continue; if(orange->to != other->to) continue; // It is a candidate for merging. dint result = tryMergeOccludes(orange, other); if(result == 2) { next = next->next; } break; } orange = next; } } /** * Everything in the given range is removed from the occlusion nodes. */ void cutOcclusionRange(binangle_t from, binangle_t to) { DENG2_DEBUG_ONLY(occlusionRanger(1)); // Find the range after which it's OK to add oranges cut in half. // (Must preserve the ascending order of the start angles.) We want the // orange with the smallest start angle, but one that starts after the // cut range has ended. Occluder *after = nullptr; for(Occluder *orange = occHead; orange && orange->from < to; orange = orange->next) { after = orange; } for(Occluder *orange = occHead; orange; ) { // As orange might be removed - remember the next one. auto *next = orange->next; // Does the cut range include this orange? if(from <= orange->to) { // No more cuts possible? if(orange->from >= to) break; // Four options: switch(orange->relationship(AngleRange(from, to))) { case 0: // The cut range completely includes this orange. // Fully contained; this orange will be removed. removeOcclusionRange(orange); break; case 1: // The cut range contains the beginning of the orange. // Cut away the beginning of this orange. orange->from = to; // Even though the start angle is modified, we don't need to // move this orange anywhere. This is because after the cut // there will be no oranges beginning inside the cut range. break; case 2: // The cut range contains the end of the orange. // Cut away the end of this orange. orange->to = from; break; case 3: { // The orange contains the whole cut range. // The orange gets cut in two parts. Create a new orange that // represents the end, and add it after the 'after' range, or // to the head of the list. Occluder *part = newOcclusionRange(to, orange->to, orange->normal, orange->topHalf); part->prev = after; if(after) { part->next = after->next; after->next = part; } else { // Add to the head. part->next = occHead; occHead = part; } if(part->next) part->next->prev = part; // Modify the start part. orange->to = from; break; } default: // No meaningful relationship (in this context). break; } } orange = next; } DENG2_DEBUG_ONLY(occlusionRanger(2)); mergeOccludes(); DENG2_DEBUG_ONLY(occlusionRanger(6)); } #ifdef DENG2_DEBUG void occlusionLister() { for(Occluder *orange = occHead; orange; orange = orange->next) { LOG_MSG(String("from: %1 to: %2 topHalf: %3") .arg(orange->from, 0, 16) .arg(orange->to, 0, 16) .arg(DENG2_BOOL_YESNO(orange->topHalf))); } } void occlusionRanger(int mark) { for(Occluder *orange = occHead; orange; orange = orange->next) { if(orange->prev && orange->prev->from > orange->from) { occlusionLister(); throw Error("AngleClipper::occlusionRanger", String("Order %1 has failed").arg(mark)); } } } #endif }; AngleClipper::AngleClipper() : d(new Instance) {} dint AngleClipper::isFull() const { if(::devNoCulling) return false; return d->clipHead && d->clipHead->from == 0 && d->clipHead->to == BANG_MAX; } dint AngleClipper::isAngleVisible(binangle_t bang) const { if(::devNoCulling) return true; for(Instance::Clipper const *crange = d->clipHead; crange; crange = crange->next) { if(bang > crange->from && bang < crange->to) return false; } return true; // Not occluded. } dint AngleClipper::isPointVisible(Vector3d const &point) const { if(::devNoCulling) return true; Vector3d const viewRelPoint = point - Rend_EyeOrigin().xzy(); binangle_t const angle = pointToAngle(viewRelPoint); if(!isAngleVisible(angle)) return false; // Not clipped by the clipnodes. Perhaps it's occluded by an orange. for(Instance::Occluder const *orange = d->occHead; orange; orange = orange->next) { if(angle >= orange->from && angle <= orange->to) { if(orange->from > angle) return true; // No more possibilities. // On which side of the occlusion plane is it? // The positive side is the occluded one. if(viewRelPoint.dot(orange->normal) > 0) return false; } } return true; // Not occluded. } dint AngleClipper::isPolyVisible(Face const &poly) const { DENG2_ASSERT(poly.isConvex()); if(::devNoCulling) return true; // Do we need to resize the angle list buffer? if(poly.hedgeCount() > d->angleBuf.count()) { d->angleBuf.resize(poly.hedgeCount()); } // Find angles to all corners. Vector2d const eyeOrigin = Rend_EyeOrigin().xz(); dint n = 0; HEdge const *hedge = poly.hedge(); do { d->angleBuf[n++] = pointToAngle(hedge->origin() - eyeOrigin); } while((hedge = &hedge->next()) != poly.hedge()); // Check each of the ranges defined by the edges. The last edge won't be checked. // This is because the edges define a closed, convex polygon and the last edge's // range is always already covered by the previous edges. for(dint i = 0; i < poly.hedgeCount() - 1; ++i) { // If even one of the edges is not contained by a clipnode, the leaf is at // least partially visible. binangle_t angLen = d->angleBuf.at(i + 1) - d->angleBuf.at(i); // The viewer is on an edge, the leaf should be visible. if(angLen == BANG_180) return true; // Choose the start and end points so that length is < 180. if(angLen < BANG_180) { if(d->safeCheckRange(d->angleBuf.at(i), d->angleBuf.at(i + 1))) return true; } else { if(d->safeCheckRange(d->angleBuf.at(i + 1), d->angleBuf.at(i))) return true; } } return false; // Completely occluded. } void AngleClipper::clearRanges() { d->clipHead = nullptr; d->clipNodes.rewind(); // Start reusing ranges. d->occHead = nullptr; d->occNodes.rewind(); // Start reusing ranges. } dint AngleClipper::safeAddRange(binangle_t from, binangle_t to) { // The range may wrap around. if(from > to) { // The range has to added in two parts. d->addRange(from, BANG_MAX); d->addRange(0, to); } else { // Add the range as usual. d->addRange(from, to); } return true; } void AngleClipper::addRangeFromViewRelPoints(Vector2d const &from, Vector2d const &to) { Vector2d const eyeOrigin = Rend_EyeOrigin().xz(); safeAddRange(pointToAngle(to - eyeOrigin), pointToAngle(from - eyeOrigin)); } /// @todo Optimize:: Check if the given line is already occluded? void AngleClipper::addViewRelOcclusion(Vector2d const &from, Vector2d const &to, coord_t height, bool topHalf) { // Calculate the occlusion plane normal. // We'll use the game's coordinate system (left-handed, but Y and Z are swapped). Vector3d const eyeOrigin = Rend_EyeOrigin().xzy(); auto const eyeToV1 = Vector3d(from, height) - eyeOrigin; auto const eyeToV2 = Vector3d(to, height) - eyeOrigin; binangle_t const startAngle = pointToAngle(eyeToV2); binangle_t const endAngle = pointToAngle(eyeToV1); // Do not attempt to occlude with a zero-length range. if(startAngle == endAngle) return; // The normal points to the half we want to occlude. Vector3f const normal = (topHalf? eyeToV2 : eyeToV1).cross(topHalf? eyeToV1 : eyeToV2); #ifdef DENG2_DEBUG if(Vector3f(0, 0, (topHalf ? 1000 : -1000)).dot(normal) < 0) { LOG_AS("AngleClipper::addViewRelOcclusion"); LOGDEV_GL_WARNING("Wrong side v1:%s v2:%s eyeOrigin:%s!") << from.asText() << to.asText() << Vector2d(eyeOrigin).asText(); DENG2_ASSERT(!"Failed AngleClipper::addViewRelOcclusion: Side test"); } #endif // Try to add this range. d->safeAddOcclusionRange(startAngle, endAngle, normal, topHalf); } dint AngleClipper::checkRangeFromViewRelPoints(Vector2d const &from, Vector2d const &to) { if(::devNoCulling) return true; Vector2d const eyeOrigin = Rend_EyeOrigin().xz(); return d->safeCheckRange(pointToAngle(to - eyeOrigin) - BANG_45/90, pointToAngle(from - eyeOrigin) + BANG_45/90); } #ifdef DENG2_DEBUG void AngleClipper::validate() { for(Instance::Clipper *i = d->clipHead; i; i = i->next) { if(i == d->clipHead) { if(i->prev) throw Error("AngleClipper::validate", "Cliphead->prev != NULL"); } // Confirm that the links to prev and next are OK. if(i->prev) { if(i->prev->next != i) throw Error("AngleClipper::validate", "Prev->next != this"); } else if(i != d->clipHead) { throw Error("AngleClipper::validate", "prev == NULL, this isn't clipHead"); } if(i->next) { if(i->next->prev != i) throw Error("AngleClipper::validate", "Next->prev != this"); } } } #endif doomsday-stable-1.15.7/doomsday/client/src/render/wallspec.cpp0000664000175000017500000000562512641367670023665 0ustar jaakkojaakko/** @file render/wallspec.cpp Wall Geometry Specification. * * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "Sector" #include "Surface" #include "world/p_players.h" // viewPlayer #include "render/rend_main.h" #include "render/walledge.h" using namespace de; /** * Should angle based light level deltas be applied? */ static bool useWallSectionLightLevelDeltas(LineSide const &side, int section) { // Disabled? if(rendLightWallAngle <= 0) return false; // Never if the surface's material was chosen as a HOM fix (lighting must // be consistent with that applied to the relative back sector plane). if(side.surface(section).hasFixMaterial() && side.hasSector() && side.back().hasSector()) { Sector &backSector = side.back().sector(); if(backSector.floor().height() < backSector.ceiling().height()) return false; } return true; } WallSpec WallSpec::fromMapSide(LineSide const &side, int section) // static { bool const isTwoSidedMiddle = (section == LineSide::Middle && !side.considerOneSided()); WallSpec spec(section); if(side.line().definesPolyobj() || isTwoSidedMiddle) { spec.flags &= ~WallSpec::ForceOpaque; spec.flags |= WallSpec::NoEdgeDivisions; } if(isTwoSidedMiddle) { if(viewPlayer && ((viewPlayer->shared.flags & (DDPF_NOCLIP|DDPF_CAMERA)) || !side.line().isFlagged(DDLF_BLOCKING))) spec.flags |= WallSpec::NearFade; spec.flags |= WallSpec::SortDynLights; } // Suppress the sky clipping in debug mode. if(devRendSkyMode) spec.flags &= ~WallSpec::SkyClip; if(side.line().definesPolyobj()) spec.flags |= WallSpec::NoFakeRadio; bool useLightLevelDeltas = useWallSectionLightLevelDeltas(side, section); if(!useLightLevelDeltas) spec.flags |= WallSpec::NoLightDeltas; // We can skip normal smoothing if light level delta smoothing won't be done. if(!useLightLevelDeltas || !rendLightWallAngleSmooth) spec.flags |= WallSpec::NoEdgeNormalSmoothing; return spec; } doomsday-stable-1.15.7/doomsday/client/src/render/drawlists.cpp0000664000175000017500000001561312641367670024065 0ustar jaakkojaakko/** @file drawlists.cpp Drawable primitive list collection/management. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "render/drawlists.h" #include #include #include #include using namespace de; typedef QMultiHash DrawListHash; DENG2_PIMPL(DrawLists) { QScopedPointer skyMaskList; DrawListHash unlitHash; DrawListHash litHash; DrawListHash dynHash; DrawListHash shinyHash; DrawListHash shadowHash; Instance(Public *i) : Base(i) { DrawListSpec newSpec; newSpec.group = SkyMaskGeom; skyMaskList.reset(new DrawList(newSpec)); } /// Choose the correct draw list hash table. DrawListHash &listHash(GeomGroup group) { switch(group) { case UnlitGeom: return unlitHash; case LitGeom: return litHash; case LightGeom: return dynHash; case ShadowGeom: return shadowHash; case ShineGeom: return shinyHash; case SkyMaskGeom: break; // n/a? } DENG2_ASSERT(false); return unlitHash; } }; DrawLists::DrawLists() : d(new Instance(this)) {} static void clearAllLists(DrawListHash &hash) { foreach(DrawList *list, hash) { list->clear(); } qDeleteAll(hash); hash.clear(); } void DrawLists::clear() { clearAllLists(d->unlitHash); clearAllLists(d->litHash); clearAllLists(d->dynHash); clearAllLists(d->shadowHash); clearAllLists(d->shinyHash); d->skyMaskList->clear(); } static void resetList(DrawList &list) { list.rewind(); // Reset the list specification. // The interpolation target must be explicitly set. DrawListSpec &listSpec = list.spec(); listSpec.unit(TU_INTER).unmanaged.glName = 0; listSpec.unit(TU_INTER).texture = 0; listSpec.unit(TU_INTER).opacity = 0; listSpec.unit(TU_INTER_DETAIL).unmanaged.glName = 0; listSpec.unit(TU_INTER_DETAIL).texture = 0; listSpec.unit(TU_INTER_DETAIL).opacity = 0; } static void resetAllLists(DrawListHash &hash) { foreach(DrawList *list, hash) { resetList(*list); } } void DrawLists::reset() { resetAllLists(d->unlitHash); resetAllLists(d->litHash); resetAllLists(d->dynHash); resetAllLists(d->shadowHash); resetAllLists(d->shinyHash); resetList(*d->skyMaskList); } /** * Specialized texture unit comparision function that ignores properties which * are applied per-primitive and which should not result in list separation. * * (These properties are written to the primitive header and applied dynamically * when reading back the draw list.) */ static bool compareTexUnit(GLTextureUnit const &lhs, GLTextureUnit const &rhs) { if(lhs.texture) { if(lhs.texture != rhs.texture) return false; } else { if(lhs.unmanaged != rhs.unmanaged) return false; } if(!de::fequal(lhs.opacity, rhs.opacity)) return false; // Other properties are applied per-primitive and should not affect the outcome. return true; } DrawList &DrawLists::find(DrawListSpec const &spec) { // Sky masked geometry is never textured; therefore no draw list hash. /// @todo Make hash management dynamic. -ds if(spec.group == SkyMaskGeom) { return *d->skyMaskList; } DrawList *convertable = 0; // Find/create a list in the hash. GLuint const key = spec.unit(TU_PRIMARY).getTextureGLName(); DrawListHash &hash = d->listHash(spec.group); for(DrawListHash::const_iterator it = hash.find(key); it != hash.end() && it.key() == key; ++it) { DrawList *list = it.value(); DrawListSpec const &listSpec = list->spec(); if((spec.group == ShineGeom && compareTexUnit(listSpec.unit(TU_PRIMARY), spec.unit(TU_PRIMARY))) || (spec.group != ShineGeom && compareTexUnit(listSpec.unit(TU_PRIMARY), spec.unit(TU_PRIMARY)) && compareTexUnit(listSpec.unit(TU_PRIMARY_DETAIL), spec.unit(TU_PRIMARY_DETAIL)))) { if(!listSpec.unit(TU_INTER).hasTexture() && !spec.unit(TU_INTER).hasTexture()) { // This will do great. return *list; } // Is this eligible for conversion to a blended list? if(list->isEmpty() && !convertable && spec.unit(TU_INTER).hasTexture()) { // If necessary, this empty list will be selected. convertable = list; } // Possibly an exact match? if((spec.group == ShineGeom && compareTexUnit(listSpec.unit(TU_INTER), spec.unit(TU_INTER))) || (spec.group != ShineGeom && compareTexUnit(listSpec.unit(TU_INTER), spec.unit(TU_INTER)) && compareTexUnit(listSpec.unit(TU_INTER_DETAIL), spec.unit(TU_INTER_DETAIL)))) { return *list; } } } // Did we find a convertable list? if(convertable) { // This list is currently empty. if(spec.group == ShineGeom) { convertable->spec().unit(TU_INTER) = spec.unit(TU_INTER); } else { convertable->spec().unit(TU_INTER) = spec.unit(TU_INTER); convertable->spec().unit(TU_INTER_DETAIL) = spec.unit(TU_INTER_DETAIL); } return *convertable; } // Create a new list. return *hash.insert(key, new DrawList(spec)).value(); } int DrawLists::findAll(GeomGroup group, FoundLists &found) { LOG_AS("DrawLists::findAll"); found.clear(); if(group == SkyMaskGeom) { if(!d->skyMaskList->isEmpty()) { found.append(d->skyMaskList.data()); } } else { DrawListHash const &hash = d->listHash(group); foreach(DrawList *list, hash) { if(!list->isEmpty()) { found.append(list); } } } return found.count(); } doomsday-stable-1.15.7/doomsday/client/src/render/decoration.cpp0000664000175000017500000000424712641367670024201 0ustar jaakkojaakko/** @file decoration.cpp World surface decoration. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "render/decoration.h" #include "Surface" using namespace de; DENG2_PIMPL_NOREF(Decoration) { MaterialAnimator::Decoration const *source = nullptr; Surface *surface = nullptr; }; Decoration::Decoration(MaterialAnimator::Decoration const &source, Vector3d const &origin) : MapObject(origin) , d(new Instance) { d->source = &source; } Decoration::~Decoration() {} MaterialAnimator::Decoration const &Decoration::source() const { DENG2_ASSERT(d->source); return *d->source; } bool Decoration::hasSurface() const { return d->surface != nullptr; } Surface &Decoration::surface() { if(hasSurface()) return *d->surface; /// @throw MissingSurfaceError Attempted with no surface attributed. throw MissingSurfaceError("Decoration::surface", "No surface is attributed"); } Surface const &Decoration::surface() const { if(hasSurface()) return *d->surface; /// @throw MissingSurfaceError Attempted with no surface attributed. throw MissingSurfaceError("Decoration::surface", "No surface is attributed"); } void Decoration::setSurface(Surface *newSurface) { d->surface = newSurface; } doomsday-stable-1.15.7/doomsday/client/src/render/surfacedecorator.cpp0000664000175000017500000002721412641367670025404 0ustar jaakkojaakko/** @file surfacedecorator.cpp World surface decorator. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "render/surfacedecorator.h" #include #include #include #include #include "world/map.h" #include "BspLeaf" #include "ConvexSubspace" #include "Sector" #include "SectorCluster" #include "Surface" #include "render/rend_main.h" // Rend_MapSurfaceMaterialSpec() #include "LightDecoration" #include "WallEdge" using namespace de; typedef QSet SurfaceSet; typedef QMap MaterialSurfaceMap; DENG2_PIMPL_NOREF(SurfaceDecorator) , DENG2_OBSERVES(Material, DimensionsChange) , DENG2_OBSERVES(MaterialAnimator, DecorationStageChange) { MaterialSurfaceMap decorated; ///< All surfaces being looked after. ~Instance() { for(SurfaceSet const &set : decorated) for(Surface *surface : set) { observeMaterial(surface->material(), false); } } void observeMaterial(Material &material, bool yes = true) { if(yes) { material.audienceForDimensionsChange() += this; material.getAnimator(Rend_MapSurfaceMaterialSpec()).audienceForDecorationStageChange += this; } else { material.audienceForDimensionsChange() -= this; material.getAnimator(Rend_MapSurfaceMaterialSpec()).audienceForDecorationStageChange -= this; } } void updateDecorations(Surface &suf, MaterialAnimator &matAnimator, Vector2f const &materialOrigin, Vector3d const &topLeft, Vector3d const &bottomRight, Sector *containingSector = nullptr) { Vector3d delta = bottomRight - topLeft; if(de::fequal(delta.length(), 0)) return; Material &material = matAnimator.material(); int const axis = suf.normal().maxAxis(); Vector2d sufDimensions; if(axis == 0 || axis == 1) { sufDimensions.x = std::sqrt(de::squared(delta.x) + de::squared(delta.y)); sufDimensions.y = delta.z; } else { sufDimensions.x = std::sqrt(de::squared(delta.x)); sufDimensions.y = delta.y; } if(sufDimensions.x < 0) sufDimensions.x = -sufDimensions.x; if(sufDimensions.y < 0) sufDimensions.y = -sufDimensions.y; // Generate a number of decorations. int decorIndex = 0; material.forAllDecorations([&suf, &matAnimator, &materialOrigin , &topLeft, &bottomRight, &containingSector , &delta, &axis, &sufDimensions, &decorIndex] (MaterialDecoration &decor) { Vector2i const &matDimensions = matAnimator.material().dimensions(); MaterialAnimator::Decoration const &decorSS = matAnimator.decoration(decorIndex); // Skip values must be at least one. Vector2i skip = Vector2i(decor.patternSkip().x + 1, decor.patternSkip().y + 1) .max(Vector2i(1, 1)); Vector2f repeat = matDimensions * skip; if(repeat == Vector2f(0, 0)) return LoopAbort; Vector3d origin = topLeft + suf.normal() * decorSS.elevation(); float s = de::wrap(decorSS.origin().x - matDimensions.x * decor.patternOffset().x + materialOrigin.x, 0.f, repeat.x); // Plot decorations. for(; s < sufDimensions.x; s += repeat.x) { // Determine the topmost point for this row. float t = de::wrap(decorSS.origin().y - matDimensions.y * decor.patternOffset().y + materialOrigin.y, 0.f, repeat.y); for(; t < sufDimensions.y; t += repeat.y) { float const offS = s / sufDimensions.x; float const offT = t / sufDimensions.y; Vector3d patternOffset(offS, axis == VZ? offT : offS, axis == VZ? offS : offT); Vector3d decorOrigin = origin + delta * patternOffset; ConvexSubspace *subspace = suf.map().bspLeafAt(decorOrigin).subspacePtr(); if(!subspace) continue; if(!subspace->contains(decorOrigin)) continue; if(containingSector) { // The point must be in the correct sector. if(containingSector != &subspace->sector()) continue; } suf.addDecoration(new LightDecoration(decorSS, decorOrigin)); } } decorIndex += 1; return LoopContinue; }); } void markSurfacesForRedecoration(Material &material) { MaterialSurfaceMap::const_iterator found = decorated.constFind(&material); if(found != decorated.constEnd()) for(Surface *surface : found.value()) { surface->markForDecorationUpdate(); } } void materialDimensionsChanged(Material &material) { markSurfacesForRedecoration(material); } void materialAnimatorDecorationStageChanged(MaterialAnimator &animator) { markSurfacesForRedecoration(animator.material()); } }; SurfaceDecorator::SurfaceDecorator() : d(new Instance) {} static bool prepareGeometry(Surface &surface, Vector3d &topLeft, Vector3d &bottomRight, Vector2f &materialOrigin) { if(surface.parent().type() == DMU_SIDE) { LineSide &side = surface.parent().as(); int section = &side.middle() == &surface? LineSide::Middle : &side.bottom() == &surface? LineSide::Bottom : LineSide::Top; if(!side.hasSections()) return false; HEdge *leftHEdge = side.leftHEdge(); HEdge *rightHEdge = side.rightHEdge(); if(!leftHEdge || !rightHEdge) return false; // Is the wall section potentially visible? WallSpec const wallSpec = WallSpec::fromMapSide(side, section); WallEdge leftEdge (wallSpec, *leftHEdge, Line::From); WallEdge rightEdge(wallSpec, *rightHEdge, Line::To); if(!leftEdge.isValid() || !rightEdge.isValid() || de::fequal(leftEdge.bottom().z(), rightEdge.top().z())) return false; topLeft = leftEdge.top().origin(); bottomRight = rightEdge.bottom().origin(); materialOrigin = -leftEdge.materialOrigin(); return true; } if(surface.parent().type() == DMU_PLANE) { Plane &plane = surface.parent().as(); AABoxd const §orAABox = plane.sector().aaBox(); topLeft = Vector3d(sectorAABox.minX, plane.isSectorFloor()? sectorAABox.maxY : sectorAABox.minY, plane.heightSmoothed()); bottomRight = Vector3d(sectorAABox.maxX, plane.isSectorFloor()? sectorAABox.minY : sectorAABox.maxY, plane.heightSmoothed()); materialOrigin = Vector2f(-fmod(sectorAABox.minX, 64) - surface.materialOriginSmoothed().x, -fmod(sectorAABox.minY, 64) - surface.materialOriginSmoothed().y); return true; } return false; } static inline Sector *containingSector(Surface &surface) { if(surface.parent().type() == DMU_PLANE) return &surface.parent().as().sector(); return nullptr; } void SurfaceDecorator::decorate(Surface &surface) { if(!surface.hasMaterial()) return; // Huh? if(!surface.needsDecorationUpdate()) return; surface.markForDecorationUpdate(false); surface.clearDecorations(); Vector3d topLeft, bottomRight; Vector2f materialOrigin; if(prepareGeometry(surface, topLeft, bottomRight, materialOrigin)) { MaterialAnimator &matAnimator = surface.material().getAnimator(Rend_MapSurfaceMaterialSpec()); d->updateDecorations(surface, matAnimator, materialOrigin, topLeft, bottomRight, containingSector(surface)); } } void SurfaceDecorator::redecorate() { MaterialSurfaceMap::iterator i = d->decorated.begin(); while(i != d->decorated.end()) { MaterialAnimator *matAnimator = nullptr; SurfaceSet const &surfaceSet = i.value(); for(Surface *surface : surfaceSet) { if(!surface->needsDecorationUpdate()) continue; // Time to prepare the material? if(!matAnimator) { Material &material = *i.key(); matAnimator = &material.getAnimator(Rend_MapSurfaceMaterialSpec()); } surface->markForDecorationUpdate(false); surface->clearDecorations(); Vector3d topLeft, bottomRight; Vector2f materialOrigin; if(prepareGeometry(*surface, topLeft, bottomRight, materialOrigin)) { d->updateDecorations(*surface, *matAnimator, materialOrigin, topLeft, bottomRight, containingSector(*surface)); } } ++i; } } void SurfaceDecorator::reset() { d->decorated.clear(); } void SurfaceDecorator::remove(Surface *surface) { if(!surface) return; // First try the set for the currently assigned material. if(surface->hasMaterial()) { MaterialSurfaceMap::iterator found = d->decorated.find(&surface->material()); if(found != d->decorated.end()) { SurfaceSet &surfaceSet = found.value(); if(surfaceSet.remove(surface)) { if(surfaceSet.isEmpty()) { d->decorated.remove(&surface->material()); d->observeMaterial(surface->material(), false); } return; } } } // The material may have changed. MaterialSurfaceMap::iterator i = d->decorated.begin(); while(i != d->decorated.end()) { SurfaceSet &surfaceSet = i.value(); if(surfaceSet.remove(surface)) { if(surfaceSet.isEmpty()) { Material *material = i.key(); d->decorated.remove(material); d->observeMaterial(surface->material(), false); } return; } ++i; } } void SurfaceDecorator::add(Surface *surface) { if(!surface) return; remove(surface); if(!surface->hasMaterial()) return; if(!surface->material().hasDecorations()) return; d->decorated[&surface->material()].insert(surface); /// @todo Defer until first decorated? d->observeMaterial(surface->material()); } doomsday-stable-1.15.7/doomsday/client/src/render/mobjanimator.cpp0000664000175000017500000000575212641367670024536 0ustar jaakkojaakko/** @file mobjanimator.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "render/mobjanimator.h" #include "render/rendersystem.h" #include "clientapp.h" #include "dd_loop.h" using namespace de; static String const DEF_PROBABILITY("prob"); static String const DEF_ROOT_NODE ("node"); MobjAnimator::MobjAnimator(DotPath const &id, ModelDrawable const &model) : ModelDrawable::Animator(model) , _stateAnims(ClientApp::renderSystem().modelRenderer().animations(id)) {} void MobjAnimator::triggerByState(String const &stateName) { // No animations can be triggered if none are available. if(!_stateAnims) return; auto found = _stateAnims->constFind(stateName); if(found == _stateAnims->constEnd()) return; foreach(ModelRenderer::AnimSequence const &seq, found.value()) { // Test for the probability of this animation. float chance = seq.def->getf(DEF_PROBABILITY, 1.f); if(frand() > chance) continue; // Start the animation on the specified node (defaults to root), // unless it is already running. String const node = seq.def->gets(DEF_ROOT_NODE, ""); int animId = ModelRenderer::identifierFromText(seq.name, [this] (String const &name) { return model().animationIdForName(name); }); // Do not restart running sequences. // TODO: Only restart if the current state is not the expected one. if(isRunning(animId, node)) continue; start(animId, node); qDebug() << "starting" << seq.name; } } void MobjAnimator::advanceTime(TimeDelta const &elapsed) { ModelDrawable::Animator::advanceTime(elapsed); for(int i = 0; i < count(); ++i) { Animation &anim = at(i); ddouble factor = 1.0; // TODO: Determine actual time factor. // Advance the sequence. anim.time += factor * elapsed; //qDebug() << "advancing" << anim.animId << "time" << anim.time; } } ddouble MobjAnimator::currentTime(int index) const { // Mobjs think on sharp ticks only, however we need to ensure time advances on // every frame for smooth animation. return ModelDrawable::Animator::currentTime(index);// + frameTimePos; /// @todo Should prevent time from passing the end of the sequence? } doomsday-stable-1.15.7/doomsday/client/src/render/lumobj.cpp0000664000175000017500000001441012641367670023333 0ustar jaakkojaakko/** @file lumobj.cpp Luminous object. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "render/lumobj.h" #include #include #include "render/rend_halo.h" #include "render/vissprite.h" #include "world/map.h" using namespace de; static dint radiusMax = 256; ///< Absolute maximum lumobj radius (cvar). static dfloat radiusScale = 4.24f; ///< Radius scale factor (cvar). dfloat Lumobj::Source::occlusion(Vector3d const & /*eye*/) const { return 1; // Fully visible. } DENG2_PIMPL_NOREF(Lumobj) { Source const *source = nullptr; ///< Source of the lumobj (if any, not owned). ddouble maxDistance = 0; ///< Used when rendering to limit the number drawn lumobjs. Vector3f color = Vector3f(1, 1, 1); ///< Light color/intensity. ddouble radius = 256; ///< Radius in map space units. ddouble zOffset = 0; ///< Z-axis origin offset in map space units. dfloat flareSize = 0; ///< Scale factor. DGLuint flareTex = 0; ///< Custom flare texture (@todo should be Texture ptr). /// Custom lightmaps (if any, not owned): Texture *sideTex = nullptr; Texture *downTex = nullptr; Texture *upTex = nullptr; Instance() {} Instance(Instance const &other) : de::IPrivate() , source (other.source) , maxDistance(other.maxDistance) , color (other.color) , radius (other.radius) , zOffset (other.zOffset) , flareSize (other.flareSize) , flareTex (other.flareTex) , sideTex (other.sideTex) , downTex (other.downTex) , upTex (other.upTex) {} }; Lumobj::Lumobj(Vector3d const &origin, ddouble radius, Vector3f const &color) : MapObject(origin), d(new Instance()) { setRadius(radius); setColor(color); } Lumobj::Lumobj(Lumobj const &other) : MapObject(other.origin()), d(new Instance(*other.d)) {} void Lumobj::setSource(Source const *newSource) { d->source = newSource; } Vector3f const &Lumobj::color() const { return d->color; } Lumobj &Lumobj::setColor(Vector3f const &newColor) { if(d->color != newColor) { d->color = newColor; } return *this; } ddouble Lumobj::radius() const { return d->radius; } Lumobj &Lumobj::setRadius(ddouble newRadius) { // Apply the global scale factor. newRadius *= 40 * ::radiusScale; // Normalize. newRadius = de::clamp(.0001, de::abs(newRadius), ::radiusMax); if(d->radius != newRadius) { d->radius = newRadius; } return *this; } AABoxd Lumobj::aaBox() const { return AABoxd(origin().x - d->radius, origin().y - d->radius, origin().x + d->radius, origin().y + d->radius); } ddouble Lumobj::zOffset() const { return d->zOffset; } Lumobj &Lumobj::setZOffset(ddouble newZOffset) { d->zOffset = newZOffset; return *this; } ddouble Lumobj::maxDistance() const { return d->maxDistance; } Lumobj &Lumobj::setMaxDistance(ddouble newMaxDistance) { d->maxDistance = newMaxDistance; return *this; } Texture *Lumobj::lightmap(LightmapSemantic semantic) const { switch(semantic) { case Side: return d->sideTex; case Down: return d->downTex; case Up: return d->upTex; }; DENG2_ASSERT(false); return d->sideTex; } Lumobj &Lumobj::setLightmap(LightmapSemantic semantic, Texture *newTexture) { switch(semantic) { case Side: d->sideTex = newTexture; break; case Down: d->downTex = newTexture; break; case Up: d->upTex = newTexture; break; }; return *this; } dfloat Lumobj::flareSize() const { return d->flareSize; } Lumobj &Lumobj::setFlareSize(dfloat newFlareSize) { d->flareSize = de::max(0.f, newFlareSize); return *this; } DGLuint Lumobj::flareTexture() const { return d->flareTex; } Lumobj &Lumobj::setFlareTexture(DGLuint newTexture) { d->flareTex = newTexture; return *this; } dfloat Lumobj::attenuation(ddouble distFromEye) const { if(distFromEye > 0 && d->maxDistance > 0) { if(distFromEye > d->maxDistance) return 0; if(distFromEye > .67 * d->maxDistance) return (d->maxDistance - distFromEye) / (.33 * d->maxDistance); } return 1; } void Lumobj::generateFlare(Vector3d const &eye, ddouble distFromEye) { // Is the point in range? if(d->maxDistance > 0 && distFromEye > d->maxDistance) return; /// @todo Remove this limitation. if(!d->source) return; vissprite_t *vis = R_NewVisSprite(VSPR_FLARE); vis->pose.origin = origin(); vis->pose.distance = distFromEye; V3f_Set(vis->data.flare.color, d->color.x, d->color.y, d->color.z); vis->data.flare.mul = d->source->occlusion(eye) * attenuation(distFromEye); vis->data.flare.size = d->flareSize > 0? de::max(1.f, d->flareSize * 60 * (50 + haloSize) / 100.0f) : 0; vis->data.flare.tex = d->flareTex; vis->data.flare.lumIdx = indexInMap(); vis->data.flare.isDecoration = true; } dint Lumobj::radiusMax() // static { return ::radiusMax; } dfloat Lumobj::radiusFactor() // static { return ::radiusScale; } void Lumobj::consoleRegister() // static { C_VAR_INT ("rend-light-radius-max", &::radiusMax, 0, 64, 512); C_VAR_FLOAT("rend-light-radius-scale", &::radiusScale, 0, .1f, 10); } doomsday-stable-1.15.7/doomsday/client/src/render/biasillum.cpp0000664000175000017500000002227212641367670024031 0ustar jaakkojaakko/** @file biasillum.cpp Shadow Bias map point illumination. * * @authors Copyright © 2005-2014 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "render/biasillum.h" #include "world/map.h" #include "world/linesighttest.h" #include "BspLeaf" #include "ConvexSubspace" #include "SectorCluster" #include "Surface" #include "BiasTracker" #include #include using namespace de; static int lightSpeed = 130; //cvar static int devUseSightCheck = true; //cvar DENG2_PIMPL_NOREF(BiasIllum) { struct InterpolateInfo { Vector3f dest; ///< Destination light color (interpolated to). uint updateTime; ///< When the value was calculated. }; BiasTracker *tracker; ///< Controlling tracker. Vector3f color; ///< Current light color. QScopedPointer lerpInfo; /** * Cast lighting contributions from each source that affects the map point. * Order is the same as that in the tracker. */ Vector3f casted[MAX_CONTRIBUTORS]; Instance() : tracker(0) {} /** * Returns a previous light contribution by unique contributor @a index. */ inline Vector3f &contribution(int index) { DENG_ASSERT(index >= 0 && index < MAX_CONTRIBUTORS); return casted[index]; } /** * Update any changed lighting contributions. * * @param activeContributors Bit field denoting the active contributors. * @param biasTime Time in milliseconds of the last bias frame update. */ void applyLightingChanges(byte activeContributors, uint biasTime) { #define COLOR_CHANGE_THRESHOLD 0.1f // Ignore small variations for perf DENG_ASSERT(tracker != 0); // Determine the new color (initially, black). Vector3f newColor; // Do we need to re-accumulate light contributions? if(activeContributors) { // Maximum accumulated color strength. static Vector3f const saturated(1, 1, 1); for(int i = 0; i < MAX_CONTRIBUTORS; ++i) { if(activeContributors & (1 << i)) { newColor += contribution(i); // Stop once fully saturated. if(newColor >= saturated) break; } } // Clamp. newColor = newColor.min(saturated); } // Is there a new destination? Vector3f const ¤tColor = lerpInfo.isNull()? color : lerpInfo->dest; if(!activeContributors || (!de::fequal(currentColor.x, newColor.x, COLOR_CHANGE_THRESHOLD) || !de::fequal(currentColor.y, newColor.y, COLOR_CHANGE_THRESHOLD) || !de::fequal(currentColor.z, newColor.z, COLOR_CHANGE_THRESHOLD))) { if(!lerpInfo.isNull()) { // Must not lose the half-way interpolation. // This is current color at this very moment. color = lerp(biasTime, true /*retain InterpolateInfo*/); } else { lerpInfo.reset(new InterpolateInfo()); } // This is what we will be interpolating to. lerpInfo->dest = newColor; lerpInfo->updateTime = tracker->timeOfLatestContributorUpdate(); } #undef COLOR_CHANGE_THRESHOLD } /** * Update lighting contribution for the specified contributor @a index. * * @param index Unique index of the contributor. * @param point Point in the map to evaluate. * @param normalAtPoint Surface normal at @a point. * @param bspRoot Root BSP element for the map. */ void updateContribution(int index, Vector3d const &point, Vector3f const &normalAtPoint, Map::BspTree const &bspRoot) { DENG_ASSERT(tracker != 0); BiasSource const &source = tracker->contributor(index); Vector3f &casted = contribution(index); /// @todo LineSightTest should (optionally) perform this test. ConvexSubspace *subspace = source.bspLeafAtOrigin().subspacePtr(); if(!subspace) { // This affecting source does not contribute any light. casted = Vector3f(); return; } SectorCluster &cluster = subspace->cluster(); if((!cluster.visFloor().surface().hasSkyMaskedMaterial() && source.origin().z < cluster.visFloor().heightSmoothed()) || (!cluster.visCeiling().surface().hasSkyMaskedMaterial() && source.origin().z > cluster.visCeiling().heightSmoothed())) { casted = Vector3f(); return; } Vector3d sourceToPoint = source.origin() - point; double distance = sourceToPoint.length(); double dot = sourceToPoint.normalize().dot(normalAtPoint); // The point faces away from the light? if(dot < 0) { casted = Vector3f(); return; } if(devUseSightCheck && !LineSightTest(source.origin(), point + sourceToPoint / 100) .trace(bspRoot)) { // LOS fail. casted = Vector3f(); return; } // Apply light casted from this source. float strength = dot * source.evaluateIntensity() / distance; casted = source.color() * de::clamp(0.f, strength, 1.f); } /** * Interpolate color from current to destination. * * @param result Interpolated color will be written here. * @param currentTime Time in milliseconds of the last bias frame update. * @param retainLerpInfo @c true= do not free the interpolation info if completed. */ Vector3f lerp(uint currentTime, bool retainLerpInfo = false) { if(lerpInfo.isNull()) { // Not interpolating -- use the current color. return color; } float inter = (currentTime - lerpInfo->updateTime) / float( lightSpeed ); if(inter > 1) { color = lerpInfo->dest; if(!retainLerpInfo) lerpInfo.reset(); return color; } else { return color + (lerpInfo->dest - color) * inter; } } }; float const BiasIllum::MIN_INTENSITY = .005f; BiasIllum::BiasIllum(BiasTracker *tracker) : d(new Instance()) { setTracker(tracker); } bool BiasIllum::hasTracker() const { return d->tracker != 0; } BiasTracker &BiasIllum::tracker() const { if(d->tracker != 0) { return *d->tracker; } /// @throw MissingTrackerError Attempted with no tracker assigned. throw MissingTrackerError("BiasIllum::tracker", "No tracker is assigned"); } void BiasIllum::setTracker(BiasTracker *newTracker) { d->tracker = newTracker; } Vector3f BiasIllum::evaluate(Vector3d const &point, Vector3f const &normalAtPoint, uint biasTime) { if(d->tracker) { // Does the tracker have any lighting changes to apply? byte activeContributors = d->tracker->activeContributors(); byte changedContributions = d->tracker->changedContributions(); if(changedContributions) { if(activeContributors & changedContributions) { /// @todo Do not assume the current map. Map &map = App_WorldSystem().map(); /* * Recalculate the contribution for each changed light source. * Continue using the previously calculated value otherwise. */ for(int i = 0; i < MAX_CONTRIBUTORS; ++i) { if(activeContributors & changedContributions & (1 << i)) { d->updateContribution(i, point, normalAtPoint, map.bspTree()); } } } // Accumulate light contributions and initiate interpolation. d->applyLightingChanges(activeContributors, biasTime); } } // Factor in the current color (and perform interpolation if needed). return d->lerp(biasTime); } void BiasIllum::consoleRegister() // static { C_VAR_INT("rend-bias-lightspeed", &lightSpeed, 0, 0, 5000); // Development variables. C_VAR_INT("rend-dev-bias-sight", &devUseSightCheck, CVF_NO_ARCHIVE, 0, 1); } doomsday-stable-1.15.7/doomsday/client/src/render/billboard.cpp0000664000175000017500000005111012641367670023773 0ustar jaakkojaakko/** @file billboard.cpp Rendering billboard "sprites". * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "render/billboard.h" #include #include #include #include "clientapp.h" #include "gl/gl_main.h" #include "r_util.h" #include "render/rend_main.h" #include "render/vissprite.h" #include "MaterialVariantSpec" #include "Texture" #include "resource/sprite.h" #include "world/p_players.h" // viewPlayer, ddPlayers using namespace de; dint spriteLight = 4; dfloat maxSpriteAngle = 60; // If true - use the "no translucency" blending mode for sprites/masked walls dbyte noSpriteTrans; dint useSpriteAlpha = 1; dint useSpriteBlend = 1; dint alwaysAlign; dint noSpriteZWrite; dbyte devNoSprites; static inline RenderSystem &rendSys() { return ClientApp::renderSystem(); } static inline ResourceSystem &resSys() { return ClientApp::resourceSystem(); } static inline void drawQuad(dgl_vertex_t *v, dgl_color_t *c, dgl_texcoord_t *tc) { glBegin(GL_QUADS); glColor4ubv(c[0].rgba); glTexCoord2fv(tc[0].st); glVertex3fv(v[0].xyz); glColor4ubv(c[1].rgba); glTexCoord2fv(tc[1].st); glVertex3fv(v[1].xyz); glColor4ubv(c[2].rgba); glTexCoord2fv(tc[2].st); glVertex3fv(v[2].xyz); glColor4ubv(c[3].rgba); glTexCoord2fv(tc[3].st); glVertex3fv(v[3].xyz); glEnd(); } void Rend_DrawMaskedWall(drawmaskedwallparams_t const &parms) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); TextureVariant *tex = nullptr; if(::renderTextures) { MaterialAnimator *matAnimator = parms.animator; DENG2_ASSERT(matAnimator); // Ensure we have up to date info about the material. matAnimator->prepare(); tex = matAnimator->texUnit(MaterialAnimator::TU_LAYER0).texture; } // Do we have a dynamic light to blend with? // This only happens when multitexturing is enabled. bool withDyn = false; dint normal = 0, dyn = 1; if(parms.modTex && ::numTexUnits > 1) { if(IS_MUL) { normal = 1; dyn = 0; } else { normal = 0; dyn = 1; } GL_SelectTexUnits(2); GL_ModulateTexture(IS_MUL ? 4 : 5); // The dynamic light. glActiveTexture(IS_MUL ? GL_TEXTURE0 : GL_TEXTURE1); /// @todo modTex may be the name of a "managed" texture. GL_BindTextureUnmanaged(renderTextures ? parms.modTex : 0, gl::ClampToEdge, gl::ClampToEdge); glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, parms.modColor); // The actual texture. glActiveTexture(IS_MUL ? GL_TEXTURE1 : GL_TEXTURE0); GL_BindTexture(tex); withDyn = true; } else { GL_ModulateTexture(1); glEnable(GL_TEXTURE_2D); GL_BindTexture(tex); normal = 0; } GL_BlendMode(parms.blendMode); GLenum normalTarget = normal? GL_TEXTURE1 : GL_TEXTURE0; GLenum dynTarget = dyn? GL_TEXTURE1 : GL_TEXTURE0; // Draw one quad. This is obviously not a very efficient way to render // lots of masked walls, but since 3D models and sprites must be // rendered interleaved with masked walls, there's not much that can be // done about this. if(withDyn) { glBegin(GL_QUADS); glColor4fv(parms.vertices[0].color); glMultiTexCoord2f(normalTarget, parms.texCoord[0][0], parms.texCoord[1][1]); glMultiTexCoord2f(dynTarget, parms.modTexCoord[0][0], parms.modTexCoord[1][1]); glVertex3f(parms.vertices[0].pos[0], parms.vertices[0].pos[2], parms.vertices[0].pos[1]); glColor4fv(parms.vertices[1].color); glMultiTexCoord2f(normalTarget, parms.texCoord[0][0], parms.texCoord[0][1]); glMultiTexCoord2f(dynTarget, parms.modTexCoord[0][0], parms.modTexCoord[0][1]); glVertex3f(parms.vertices[1].pos[0], parms.vertices[1].pos[2], parms.vertices[1].pos[1]); glColor4fv(parms.vertices[3].color); glMultiTexCoord2f(normalTarget, parms.texCoord[1][0], parms.texCoord[0][1]); glMultiTexCoord2f(dynTarget, parms.modTexCoord[1][0], parms.modTexCoord[0][1]); glVertex3f(parms.vertices[3].pos[0], parms.vertices[3].pos[2], parms.vertices[3].pos[1]); glColor4fv(parms.vertices[2].color); glMultiTexCoord2f(normalTarget, parms.texCoord[1][0], parms.texCoord[1][1]); glMultiTexCoord2f(dynTarget, parms.modTexCoord[1][0], parms.modTexCoord[1][1]); glVertex3f(parms.vertices[2].pos[0], parms.vertices[2].pos[2], parms.vertices[2].pos[1]); glEnd(); // Restore normal GL state. GL_SelectTexUnits(1); GL_ModulateTexture(1); } else { glBegin(GL_QUADS); glColor4fv(parms.vertices[0].color); glTexCoord2f(parms.texCoord[0][0], parms.texCoord[1][1]); glVertex3f(parms.vertices[0].pos[0], parms.vertices[0].pos[2], parms.vertices[0].pos[1]); glColor4fv(parms.vertices[1].color); glTexCoord2f(parms.texCoord[0][0], parms.texCoord[0][1]); glVertex3f(parms.vertices[1].pos[0], parms.vertices[1].pos[2], parms.vertices[1].pos[1]); glColor4fv(parms.vertices[3].color); glTexCoord2f(parms.texCoord[1][0], parms.texCoord[0][1]); glVertex3f(parms.vertices[3].pos[0], parms.vertices[3].pos[2], parms.vertices[3].pos[1]); glColor4fv(parms.vertices[2].color); glTexCoord2f(parms.texCoord[1][0], parms.texCoord[1][1]); glVertex3f(parms.vertices[2].pos[0], parms.vertices[2].pos[2], parms.vertices[2].pos[1]); glEnd(); } glDisable(GL_TEXTURE_2D); GL_BlendMode(BM_NORMAL); } /** * Set all the colors in the array to that specified. */ static void applyUniformColor(dint count, dgl_color_t *colors, dfloat const *rgba) { for(; count-- > 0; colors++) { colors->rgba[0] = dbyte(255 * rgba[0]); colors->rgba[1] = dbyte(255 * rgba[1]); colors->rgba[2] = dbyte(255 * rgba[2]); colors->rgba[3] = dbyte(255 * rgba[3]); } } /** * Calculate vertex lighting. */ static void Spr_VertexColors(dint count, dgl_color_t *out, dgl_vertex_t *normalIt, duint lightListIdx, duint maxLights, dfloat const *ambient) { DENG2_ASSERT(out && normalIt); Vector3f const saturated(1, 1, 1); for(dint i = 0; i < count; ++i, out++, normalIt++) { Vector3f const normal(normalIt->xyz); // Accumulate contributions from all affecting lights. Vector3f accum[2]; // Begin with total darkness [color, extra]. dint numProcessed = 0; rendSys().forAllVectorLights(lightListIdx, [&maxLights, &normal , &accum, &numProcessed] (VectorLightData const &vlight) { numProcessed += 1; dfloat strength = vlight.direction.dot(normal) + vlight.offset; // Shift toward the light a little. // Ability to both light and shade. if(strength > 0) strength *= vlight.lightSide; else strength *= vlight.darkSide; accum[vlight.affectedByAmbient? 0 : 1] += vlight.color * de::clamp(-1.f, strength, 1.f); // Time to stop? return (maxLights && numProcessed == maxLights); }); // Check for ambient and convert to ubyte. Vector3f color = (accum[0].max(ambient) + accum[1]).min(saturated); out->rgba[0] = dbyte( 255 * color.x ); out->rgba[1] = dbyte( 255 * color.y ); out->rgba[2] = dbyte( 255 * color.z ); out->rgba[3] = dbyte( 255 * ambient[3] ); } } MaterialVariantSpec const &PSprite_MaterialSpec() { return resSys().materialSpec(SpriteContext, 0, 0, 0, 0, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 1, -2, 0, false, true, true, false); } void Rend_DrawPSprite(rendpspriteparams_t const &parms) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); if(::renderTextures == 1) { GL_SetPSprite(parms.mat, 0, 0); glEnable(GL_TEXTURE_2D); } else if(::renderTextures == 2) { // For lighting debug, render all solid surfaces using the gray texture. MaterialAnimator &matAnimator = resSys().material(de::Uri("System", Path("gray"))) .getAnimator(PSprite_MaterialSpec()); // Ensure we have up to date info about the material. matAnimator.prepare(); GL_BindTexture(matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture); glEnable(GL_TEXTURE_2D); } // 0---1 // | | Vertex layout. // 3---2 dfloat v1[] = { parms.pos[0], parms.pos[1] }; dfloat v2[] = { parms.pos[0] + parms.width, parms.pos[1] }; dfloat v3[] = { parms.pos[0] + parms.width, parms.pos[1] + parms.height }; dfloat v4[] = { parms.pos[0], parms.pos[1] + parms.height }; // All psprite vertices are co-plannar, so just copy the view front vector. // @todo: Can we do something better here? Vector3f const &frontVec = R_ViewData(viewPlayer - ddPlayers)->frontVec; dgl_vertex_t quadNormals[4]; for(dint i = 0; i < 4; ++i) { quadNormals[i].xyz[0] = frontVec.x; quadNormals[i].xyz[1] = frontVec.z; quadNormals[i].xyz[2] = frontVec.y; } dgl_color_t quadColors[4]; if(!parms.vLightListIdx) { // Lit uniformly. applyUniformColor(4, quadColors, parms.ambientColor); } else { // Lit normally. Spr_VertexColors(4, quadColors, quadNormals, parms.vLightListIdx, spriteLight + 1, parms.ambientColor); } dgl_texcoord_t tcs[4], *tc = tcs; dgl_color_t *c = quadColors; tc[0].st[0] = parms.texOffset[0] * (parms.texFlip[0]? 1:0); tc[0].st[1] = parms.texOffset[1] * (parms.texFlip[1]? 1:0); tc[1].st[0] = parms.texOffset[0] * (!parms.texFlip[0]? 1:0); tc[1].st[1] = parms.texOffset[1] * (parms.texFlip[1]? 1:0); tc[2].st[0] = parms.texOffset[0] * (!parms.texFlip[0]? 1:0); tc[2].st[1] = parms.texOffset[1] * (!parms.texFlip[1]? 1:0); tc[3].st[0] = parms.texOffset[0] * (parms.texFlip[0]? 1:0); tc[3].st[1] = parms.texOffset[1] * (!parms.texFlip[1]? 1:0); glBegin(GL_QUADS); glColor4ubv(c[0].rgba); glTexCoord2fv(tc[0].st); glVertex2fv(v1); glColor4ubv(c[1].rgba); glTexCoord2fv(tc[1].st); glVertex2fv(v2); glColor4ubv(c[2].rgba); glTexCoord2fv(tc[2].st); glVertex2fv(v3); glColor4ubv(c[3].rgba); glTexCoord2fv(tc[3].st); glVertex2fv(v4); glEnd(); if(renderTextures) { glDisable(GL_TEXTURE_2D); } } MaterialVariantSpec const &Rend_SpriteMaterialSpec(dint tclass, dint tmap) { return resSys().materialSpec(SpriteContext, 0, 1, tclass, tmap, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 1, -2, -1, true, true, true, false); } void Rend_DrawSprite(vissprite_t const &spr) { drawspriteparams_t const &parm = *VS_SPRITE(&spr); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); TextureVariant *tex = nullptr; Vector2f size; dfloat viewOffsetX = 0; ///< View-aligned offset to center point. dfloat s = 1, t = 1; ///< Bottom right coords. // Many sprite properties are inherited from the material. if(MaterialAnimator *matAnimator = parm.matAnimator) { // Ensure we have up to date info about the material. matAnimator->prepare(); tex = matAnimator->texUnit(MaterialAnimator::TU_LAYER0).texture; dint const texBorder = tex->spec().variant.border; size = matAnimator->dimensions() + Vector2i(texBorder * 2, texBorder * 2); viewOffsetX = -size.x / 2 + -tex->base().origin().x; tex->glCoords(&s, &t); } // We may want to draw using another material variant instead. if(renderTextures == 2) { // For lighting debug, render all solid surfaces using the gray texture. Material &debugMaterial = resSys().material(de::Uri("System", Path("gray"))); MaterialAnimator &matAnimator = debugMaterial.getAnimator(Rend_SpriteMaterialSpec()); // Ensure we have up to date info about the material. matAnimator.prepare(); tex = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; } if(renderTextures) { GL_BindTexture(tex); glEnable(GL_TEXTURE_2D); } else { GL_SetNoTexture(); } // Coordinates to the center of the sprite (game coords). coord_t spriteCenter[3] = { spr.pose.origin[0] + spr.pose.srvo[0], spr.pose.origin[1] + spr.pose.srvo[1], spr.pose.origin[2] + spr.pose.srvo[2] }; coord_t v1[3], v2[3], v3[3], v4[3]; R_ProjectViewRelativeLine2D(spriteCenter, spr.pose.viewAligned, size.x, viewOffsetX, v1, v4); v2[0] = v1[0]; v2[1] = v1[1]; v3[0] = v4[0]; v3[1] = v4[1]; v1[2] = v4[2] = spriteCenter[2] - size.y / 2; v2[2] = v3[2] = spriteCenter[2] + size.y / 2; // Calculate the surface normal. coord_t surfaceNormal[3]; V3d_PointCrossProduct(surfaceNormal, v2, v1, v3); V3d_Normalize(surfaceNormal); /*#if _DEBUG // Draw the surface normal. glBegin(GL_LINES); glColor4f(1, 0, 0, 1); glVertex3f(spriteCenter[0], spriteCenter[2], spriteCenter[1]); glColor4f(1, 0, 0, 0); glVertex3f(spriteCenter[0] + surfaceNormal[0] * 10, spriteCenter[2] + surfaceNormal[2] * 10, spriteCenter[1] + surfaceNormal[1] * 10); glEnd(); #endif*/ // All sprite vertices are co-plannar, so just copy the surface normal. // @todo: Can we do something better here? dgl_color_t quadColors[4]; dgl_vertex_t quadNormals[4]; for(dint i = 0; i < 4; ++i) { V3f_Copyd(quadNormals[i].xyz, surfaceNormal); } if(!spr.light.vLightListIdx) { // Lit uniformly. applyUniformColor(4, quadColors, &spr.light.ambientColor[0]); } else { // Lit normally. Spr_VertexColors(4, quadColors, quadNormals, spr.light.vLightListIdx, spriteLight + 1, &spr.light.ambientColor[0]); } // Do we need to do some aligning? bool restoreMatrix = false; bool restoreZ = false; if(spr.pose.viewAligned || alwaysAlign >= 2) { // We must set up a modelview transformation matrix. restoreMatrix = true; glMatrixMode(GL_MODELVIEW); glPushMatrix(); // Rotate around the center of the sprite. glTranslatef(spriteCenter[0], spriteCenter[2], spriteCenter[1]); if(!spr.pose.viewAligned) { dfloat s_dx = v1[0] - v2[0]; dfloat s_dy = v1[1] - v2[1]; if(alwaysAlign == 2) { // Restricted camera alignment. dfloat dx = spriteCenter[0] - vOrigin.x; dfloat dy = spriteCenter[1] - vOrigin.z; dfloat spriteAngle = BANG2DEG( bamsAtan2(spriteCenter[2] - vOrigin.y, std::sqrt(dx * dx + dy * dy))); if(spriteAngle > 180) spriteAngle -= 360; if(fabs(spriteAngle) > maxSpriteAngle) { dfloat turnAngle = (spriteAngle > 0? spriteAngle - maxSpriteAngle : spriteAngle + maxSpriteAngle); // Rotate along the sprite edge. glRotatef(turnAngle, s_dx, 0, s_dy); } } else { // Restricted view plane alignment. // This'll do, for now... Really it should notice both the // sprite angle and vpitch. glRotatef(vpitch * .5f, s_dx, 0, s_dy); } } else { // Normal rotation perpendicular to the view plane. glRotatef(vpitch, viewsidex, 0, viewsidey); } glTranslatef(-spriteCenter[0], -spriteCenter[2], -spriteCenter[1]); } // Need to change blending modes? if(parm.blendMode != BM_NORMAL) { GL_BlendMode(parm.blendMode); } // Transparent sprites shouldn't be written to the Z buffer. if(parm.noZWrite || spr.light.ambientColor[3] < .98f || !(parm.blendMode == BM_NORMAL || parm.blendMode == BM_ZEROALPHA)) { restoreZ = true; glDepthMask(GL_FALSE); } dgl_vertex_t vs[4], *v = vs; dgl_texcoord_t tcs[4], *tc = tcs; // 1---2 // | | Vertex layout. // 0---3 v[0].xyz[0] = v1[0]; v[0].xyz[1] = v1[2]; v[0].xyz[2] = v1[1]; v[1].xyz[0] = v2[0]; v[1].xyz[1] = v2[2]; v[1].xyz[2] = v2[1]; v[2].xyz[0] = v3[0]; v[2].xyz[1] = v3[2]; v[2].xyz[2] = v3[1]; v[3].xyz[0] = v4[0]; v[3].xyz[1] = v4[2]; v[3].xyz[2] = v4[1]; tc[0].st[0] = s * (parm.matFlip[0]? 1:0); tc[0].st[1] = t * (!parm.matFlip[1]? 1:0); tc[1].st[0] = s * (parm.matFlip[0]? 1:0); tc[1].st[1] = t * (parm.matFlip[1]? 1:0); tc[2].st[0] = s * (!parm.matFlip[0]? 1:0); tc[2].st[1] = t * (parm.matFlip[1]? 1:0); tc[3].st[0] = s * (!parm.matFlip[0]? 1:0); tc[3].st[1] = t * (!parm.matFlip[1]? 1:0); drawQuad(v, quadColors, tc); if(renderTextures) { glDisable(GL_TEXTURE_2D); } if(devMobjVLights && spr.light.vLightListIdx) { // Draw the vlight vectors, for debug. glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glTranslatef(spr.pose.origin[0], spr.pose.origin[2], spr.pose.origin[1]); coord_t const distFromViewer = de::abs(spr.pose.distance); rendSys().forAllVectorLights(spr.light.vLightListIdx, [&distFromViewer] (VectorLightData const &vlight) { if(distFromViewer < 1600 - 8) { Rend_DrawVectorLight(vlight, 1 - distFromViewer / 1600); } return LoopContinue; }); glMatrixMode(GL_MODELVIEW); glPopMatrix(); glEnable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); } // Need to restore the original modelview matrix? if(restoreMatrix) { glPopMatrix(); } // Change back to normal blending? if(parm.blendMode != BM_NORMAL) { GL_BlendMode(BM_NORMAL); } // Enable Z-writing again? if(restoreZ) { glDepthMask(GL_TRUE); } } void Rend_SpriteRegister() { C_VAR_INT ("rend-sprite-align", &alwaysAlign, 0, 0, 3); C_VAR_FLOAT ("rend-sprite-align-angle", &maxSpriteAngle, 0, 0, 90); C_VAR_INT ("rend-sprite-alpha", &useSpriteAlpha, 0, 0, 1); C_VAR_INT ("rend-sprite-blend", &useSpriteBlend, 0, 0, 1); C_VAR_INT ("rend-sprite-lights", &spriteLight, 0, 0, 10); C_VAR_BYTE ("rend-sprite-mode", &noSpriteTrans, 0, 0, 1); C_VAR_INT ("rend-sprite-noz", &noSpriteZWrite, 0, 0, 1); C_VAR_BYTE ("rend-sprite-precache", &precacheSprites, 0, 0, 1); C_VAR_BYTE ("rend-dev-nosprite", &devNoSprites, CVF_NO_ARCHIVE, 0, 1); } doomsday-stable-1.15.7/doomsday/client/src/render/rend_halo.cpp0000664000175000017500000003050612641367670024002 0ustar jaakkojaakko/** @file rend_halo.cpp Halos and Lens Flares. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "dd_main.h" #include "render/rend_halo.h" #include "render/rend_main.h" #include "render/fx/bloom.h" #include "gl/gl_main.h" #include "gl/gl_texmanager.h" #include "world/p_players.h" #include #include #include #include #define NUM_FLARES 5 using namespace de; struct flare_t { float offset; float size; float alpha; int texture; // 0=round, 1=flare, 2=brflare, 3=bigflare }; D_CMD(FlareConfig); int haloMode = 5, haloBright = 45, haloSize = 80; int haloRealistic = true; int haloOccludeSpeed = 48; float haloZMagDiv = 62, haloMinRadius = 20; float haloDimStart = 10, haloDimEnd = 100; float haloFadeMax = 0, haloFadeMin = 0, minHaloSize = 1; flare_t flares[NUM_FLARES] = { {0, 1, 1, 0}, // Primary flare. {1, .41f, .5f, 0}, // Main secondary flare. {1.5f, .29f, .333f, 1}, {-.6f, .24f, .333f, 0}, {.4f, .29f, .25f, 0} }; void H_Register(void) { cvartemplate_t cvars[] = { {"rend-halo", 0, CVT_INT, &haloMode, 0, 5}, {"rend-halo-realistic", 0, CVT_INT, &haloRealistic, 0, 1}, {"rend-halo-bright", 0, CVT_INT, &haloBright, 0, 100}, {"rend-halo-occlusion", CVF_NO_MAX, CVT_INT, &haloOccludeSpeed, 0, 0}, {"rend-halo-size", 0, CVT_INT, &haloSize, 0, 100}, {"rend-halo-secondary-limit", CVF_NO_MAX, CVT_FLOAT, &minHaloSize, 0, 0}, {"rend-halo-fade-far", CVF_NO_MAX, CVT_FLOAT, &haloFadeMax, 0, 0}, {"rend-halo-fade-near", CVF_NO_MAX, CVT_FLOAT, &haloFadeMin, 0, 0}, {"rend-halo-zmag-div", CVF_NO_MAX, CVT_FLOAT, &haloZMagDiv, 1, 1}, {"rend-halo-radius-min", CVF_NO_MAX, CVT_FLOAT, &haloMinRadius, 0, 0}, {"rend-halo-dim-near", CVF_NO_MAX, CVT_FLOAT, &haloDimStart, 0, 0}, {"rend-halo-dim-far", CVF_NO_MAX, CVT_FLOAT, &haloDimEnd, 0, 0}, {NULL} }; Con_AddVariableList(cvars); C_CMD_FLAGS("flareconfig", NULL, FlareConfig, CMDF_NO_DEDICATED); } TextureVariantSpec const &Rend_HaloTextureSpec() { return App_ResourceSystem().textureSpec(TC_HALO_LUMINANCE, TSF_NO_COMPRESSION, 0, 0, 0, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 1, 1, 0, false, false, false, true); } void H_SetupState(bool dosetup) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); if(dosetup) { glDepthMask(GL_FALSE); glDisable(GL_DEPTH_TEST); GL_BlendMode(BM_ADD); } else { GL_BlendMode(BM_NORMAL); glEnable(GL_DEPTH_TEST); glDepthMask(GL_TRUE); } } static inline float distanceDimFactorAt(coord_t distToViewer, float size) { if(haloDimStart && haloDimStart < haloDimEnd && distToViewer / size > haloDimStart) { return 1 - (distToViewer / size - haloDimStart) / (haloDimEnd - haloDimStart); } return 1; } static inline float fadeFactorAt(coord_t distToViewer) { if(haloFadeMax && haloFadeMax != haloFadeMin && distToViewer < haloFadeMax && distToViewer >= haloFadeMin) { return (distToViewer - haloFadeMin) / (haloFadeMax - haloFadeMin); } return 1; } bool H_RenderHalo(Vector3d const &origin, float size, DGLuint tex, Vector3f const &color, coord_t distanceToViewer, float occlusionFactor, float brightnessFactor, float viewXOffset, bool doPrimary, bool viewRelativeRotate) { // In realistic mode we don't render secondary halos. if(!doPrimary && haloRealistic) return false; if(distanceToViewer <= 0 || occlusionFactor == 0 || (haloFadeMax && distanceToViewer > haloFadeMax)) return false; occlusionFactor = (1 + occlusionFactor) / 2; // viewSideVec is to the left. viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); Vector3f const leftOff = viewData->upVec + viewData->sideVec; Vector3f const rightOff = viewData->upVec - viewData->sideVec; // Calculate the center of the flare. // Apply the flare's X offset. (Positive is to the right.) Vector3f const center = Vector3f(origin.x, origin.z, origin.y) - viewData->sideVec * viewXOffset; // Calculate the mirrored position. // Project viewtocenter vector onto viewSideVec. Vector3f const viewToCenter = center - Rend_EyeOrigin(); // Calculate the 'mirror' vector. float const scale = viewToCenter.dot(viewData->frontVec) / viewData->frontVec.dot(viewData->frontVec); Vector3f const mirror = (viewData->frontVec * scale - viewToCenter) * 2; // Calculate dimming factors. float const fadeFactor = fadeFactorAt(distanceToViewer); float const secFadeFactor = viewToCenter.normalize().dot(viewData->frontVec); // Calculate texture turn angle. float turnAngle = 0; if(viewRelativeRotate) { // Normalize the mirror vector so that both are on the view plane. Vector3f haloPos = mirror.normalize(); if(haloPos.length()) { turnAngle = de::clamp(-1, haloPos.dot(viewData->upVec), 1); if(turnAngle >= 1) turnAngle = 0; else if(turnAngle <= -1) turnAngle = float(de::PI); else turnAngle = acos(turnAngle); // On which side of the up vector (left or right)? if(haloPos.dot(viewData->sideVec) < 0) turnAngle = -turnAngle; } } // The overall brightness of the flare (average color). float const luminosity = (color.x + color.y + color.z) / 3; // Small flares have stronger dimming. float const distanceDim = distanceDimFactorAt(distanceToViewer, size); // Setup GL state. if(doPrimary) H_SetupState(true); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // Prepare the texture rotation matrix. glMatrixMode(GL_TEXTURE); glPushMatrix(); glLoadIdentity(); // Rotate around the center of the texture. glTranslatef(0.5f, 0.5f, 0); glRotatef(turnAngle / float(de::PI) * 180, 0, 0, 1); glTranslatef(-0.5f, -0.5f, 0); flare_t *fl = flares; for(int i = 0; i < haloMode && i < NUM_FLARES; ++i, fl++) { bool const secondary = i != 0; if(doPrimary && secondary) break; if(!doPrimary && !secondary) continue; // Determine visibility. float alpha = fl->alpha * occlusionFactor * fadeFactor + luminosity * luminosity / 5; // Apply a dimming factor (secondary flares receive stronger dimming). alpha *= (!secondary? 1 : de::min(minHaloSize * size / distanceToViewer, 1)) * distanceDim * brightnessFactor; // Apply the global dimming factor. alpha *= .8f * haloBright / 100.0f; if(fx::Bloom::isEnabled()) { // Bloom will make bright areas even brighter, which means halos // should be dimmer to compensate. Otherwise there will be // oversaturation. alpha *= clamp(0.1f, (1.f - fx::Bloom::intensity() * .8f), 1.f); } // Secondary flares are a little bolder. if(secondary) { alpha *= luminosity - 8 * (1 - secFadeFactor); } // In the realistic mode, halos are slightly dimmer. if(haloRealistic) { alpha *= .6f; } // Not visible? if(alpha <= 0) break; // Determine radius. float radius = size * (1 - luminosity / 3) + distanceToViewer / haloZMagDiv; if(radius < haloMinRadius) radius = haloMinRadius; radius *= occlusionFactor; // In the realistic mode, halos are slightly smaller. if(haloRealistic) { radius *= 0.8f; } if(haloRealistic) { // The 'realistic' halos just use the blurry round // texture unless custom. if(!tex) tex = GL_PrepareSysFlaremap(FXT_ROUND); } else { if(!(doPrimary && tex)) { if(size > 45 || (luminosity > .90 && size > 20)) { // The "Very Bright" condition. radius *= .65f; if(!secondary) tex = GL_PrepareSysFlaremap(FXT_BIGFLARE); else tex = GL_PrepareSysFlaremap(flaretexid_t(fl->texture)); } else { if(!secondary) tex = GL_PrepareSysFlaremap(FXT_ROUND); else tex = GL_PrepareSysFlaremap(flaretexid_t(fl->texture)); } } } // Determine the final position of the flare. Vector3f pos = center; // Secondary halos are mirrored according to the flare table. if(secondary) { pos += mirror * fl->offset; } GL_BindTextureUnmanaged(renderTextures? tex : 0, gl::ClampToEdge, gl::ClampToEdge); glEnable(GL_TEXTURE_2D); float const radX = radius * fl->size; float const radY = radX / 1.2f; // Aspect correction. glColor4f(color.x, color.y, color.z, alpha); glBegin(GL_QUADS); glTexCoord2f(0, 0); glVertex3f(pos.x + radX * leftOff.x, pos.y + radY * leftOff.y, pos.z + radX * leftOff.z); glTexCoord2f(1, 0); glVertex3f(pos.x + radX * rightOff.x, pos.y + radY * rightOff.y, pos.z + radX * rightOff.z); glTexCoord2f(1, 1); glVertex3f(pos.x - radX * leftOff.x, pos.y - radY * leftOff.y, pos.z - radX * leftOff.z); glTexCoord2f(0, 1); glVertex3f(pos.x - radX * rightOff.x, pos.y - radY * rightOff.y, pos.z - radX * rightOff.z); glEnd(); glDisable(GL_TEXTURE_2D); } glMatrixMode(GL_TEXTURE); glPopMatrix(); // Restore previous GL state. if(doPrimary) H_SetupState(false); return true; } /** * flareconfig list * flareconfig (num) pos/size/alpha/tex (val) */ D_CMD(FlareConfig) { DENG2_UNUSED(src); int i; float val; if(argc == 2) { if(!stricmp(argv[1], "list")) { for(i = 0; i < NUM_FLARES; ++i) { LOG_MSG("%i: pos:%f s:%.2f a:%.2f tex:%i") << i << flares[i].offset << flares[i].size << flares[i].alpha << flares[i].texture; } } } else if(argc == 4) { i = atoi(argv[1]); val = strtod(argv[3], NULL); if(i < 0 || i >= NUM_FLARES) return false; if(!stricmp(argv[2], "pos")) { flares[i].offset = val; } else if(!stricmp(argv[2], "size")) { flares[i].size = val; } else if(!stricmp(argv[2], "alpha")) { flares[i].alpha = val; } else if(!stricmp(argv[2], "tex")) { flares[i].texture = (int) val; } } else { LOG_SCR_NOTE("Usage:\n" " %s list\n" " %s (num) pos/size/alpha/tex (val)") << argv[0] << argv[0]; } return true; } doomsday-stable-1.15.7/doomsday/client/src/render/fx/0000775000175000017500000000000012641367670021754 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/render/fx/colorfilter.cpp0000664000175000017500000000223412641367670025005 0ustar jaakkojaakko/** @file colorfilter.cpp * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "render/fx/colorfilter.h" #include "gl/gl_draw.h" using namespace de; namespace fx { ColorFilter::ColorFilter(int console) : ConsoleEffect(console) {} void ColorFilter::draw() { /// @todo Color filter should be console-specific. // The colored filter. if(GL_FilterIsVisible()) { GL_DrawFilter(); } } } // namespace fx doomsday-stable-1.15.7/doomsday/client/src/render/fx/lensflares.cpp0000664000175000017500000004127512641367670024627 0ustar jaakkojaakko/** @file lensflares.cpp * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "render/fx/lensflares.h" #include "render/ilightsource.h" #include "render/viewports.h" #include "render/rend_main.h" #include "world/p_players.h" #include "gl/gl_main.h" #include "clientapp.h" #include #include #include #include #include #include #include #include #include //#define FX_TEST_LIGHT // draw a test light (positioned for Doom E1M1) using namespace de; namespace fx { /** * Shared GL resources for rendering lens flares. */ struct FlareData { ImageBank images; AtlasTexture atlas; enum FlareId { Burst, Circle, Exponent, Halo, Ring, Star, MAX_FLARES }; enum Corner { TopLeft, TopRight, BottomRight, BottomLeft }; NoneId flare[MAX_FLARES]; FlareData() : atlas(Atlas::BackingStore, Atlas::Size(1024, 1024)) { try { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); images.addFromInfo(App::rootFolder().locate("/packs/feature.lensflares/images.dei")); atlas.setAllocator(new KdTreeAtlasAllocator); flare[Exponent] = atlas.alloc(flareImage("exponent")); flare[Star] = atlas.alloc(flareImage("star")); flare[Halo] = atlas.alloc(flareImage("halo")); flare[Circle] = atlas.alloc(flareImage("circle")); flare[Ring] = atlas.alloc(flareImage("ring")); flare[Burst] = atlas.alloc(flareImage("burst")); } catch(Error const &er) { LOG_GL_ERROR("Failed to initialize shared lens flare resources: %s") << er.asText(); } } ~FlareData() { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); LOGDEV_GL_XVERBOSE("Releasing shared data"); } Image const &flareImage(String const &name) { return images.image("fx.lensflares." + name); } Rectanglef uvRect(FlareId id) const { return atlas.imageRectf(flare[id]); } Vector2f flareCorner(FlareId id, Corner corner) const { Vector2f p; switch(corner) { case TopLeft: p = Vector2f(-1, -1); break; case TopRight: p = Vector2f( 1, -1); break; case BottomRight: p = Vector2f( 1, 1); break; case BottomLeft: p = Vector2f(-1, 1); break; } if(id == Burst) { // Non-square. p *= Vector2f(4, .25f); } return p; } }; #ifdef FX_TEST_LIGHT struct TestLight : public IPointLightSource { public: float radius; Colorf color; float intensity; TestLight() : radius(1), color(1, 1, 1), intensity(1) {} LightId lightSourceId() const { return 1; } Origin lightSourceOrigin() const { //return Origin(0, 0, 0); return Origin(782, -3227, 30); } dfloat lightSourceRadius() const { return radius; } Colorf lightSourceColorf() const { return color; } dfloat lightSourceIntensity(de::Vector3d const &) const { return intensity; } }; static TestLight testLight; D_CMD(TestLight) { String prop = argv[1]; float value = String(argv[2]).toFloat(); if(!prop.compareWithoutCase("rd")) { fx::testLight.radius = value; } else if(!prop.compareWithoutCase("in")) { fx::testLight.intensity = value; } else if(!prop.compareWithoutCase("cl") && argc >= 5) { fx::testLight.color = ILightSource::Colorf(value, String(argv[3]).toFloat(), String(argv[4]).toFloat()); } else { return false; } return true; } #endif static float linearRangeFactor(float value, Rangef const &low, Rangef const &high) { if(low.size() > 0) { if(value < low.start) { return 0; } if(low.contains(value)) { return (value - low.start) / low.size(); } } if(high.size() > 0) { if(value > high.end) { return 0; } if(high.contains(value)) { return 1 - (value - high.start) / high.size(); } } return 1; } DENG2_PIMPL(LensFlares) { typedef Shared SharedFlareData; SharedFlareData *res; /** * Current state of a potentially visible light. */ struct PVLight { IPointLightSource const *light; int seenFrame; // R_FrameCount() PVLight() : light(0), seenFrame(0) {} }; typedef QHash PVSet; PVSet pvs; Vector3f eyeFront; typedef GLBufferT VBuf; VBuf *buffer; Drawable drawable; GLUniform uMvpMatrix; GLUniform uViewUnit; GLUniform uPixelAsUv; GLUniform uActiveRect; GLUniform uAtlas; GLUniform uDepthBuf; Instance(Public *i) : Base(i) , res(0) , buffer(0) , uMvpMatrix ("uMvpMatrix", GLUniform::Mat4) , uViewUnit ("uViewUnit", GLUniform::Vec2) , uPixelAsUv ("uPixelAsUv", GLUniform::Vec2) , uActiveRect("uActiveRect", GLUniform::Vec4) , uAtlas ("uTex", GLUniform::Sampler2D) , uDepthBuf ("uDepthBuf", GLUniform::Sampler2D) {} ~Instance() { DENG2_ASSERT(res == 0); // should have been deinited releaseRef(res); clearPvs(); } void glInit() { // Acquire a reference to the shared flare data. res = SharedFlareData::hold(); buffer = new VBuf; drawable.addBuffer(buffer); self.shaders().build(drawable.program(), "fx.lensflares") << uMvpMatrix << uViewUnit << uPixelAsUv << uActiveRect << uAtlas << uDepthBuf; uAtlas = res->atlas; } void glDeinit() { drawable.clear(); buffer = 0; clearPvs(); releaseRef(res); } void clearPvs() { qDeleteAll(pvs); pvs.clear(); } void addToPvs(IPointLightSource const *light) { PVSet::iterator found = pvs.find(light->lightSourceId()); if(found == pvs.end()) { found = pvs.insert(light->lightSourceId(), new PVLight); } PVLight *pvl = found.value(); pvl->light = light; pvl->seenFrame = R_FrameCount(); } void makeFlare(VBuf::Vertices & verts, VBuf::Indices & idx, FlareData::FlareId id, float axisPos, float radius, Vector4f color, PVLight const * pvl) { Rectanglef const uvRect = res->uvRect(id); int const firstIdx = verts.size(); VBuf::Type vtx; vtx.pos = pvl->light->lightSourceOrigin().xzy(); vtx.rgba = Vector4f(pvl->light->lightSourceColorf(), 1.f) * color; vtx.texCoord[2] = Vector2f(axisPos, 0); vtx.texCoord[0] = uvRect.topLeft; vtx.texCoord[1] = res->flareCorner(id, FlareData::TopLeft) * radius; verts << vtx; vtx.texCoord[0] = uvRect.topRight(); vtx.texCoord[1] = res->flareCorner(id, FlareData::TopRight) * radius; verts << vtx; vtx.texCoord[0] = uvRect.bottomRight; vtx.texCoord[1] = res->flareCorner(id, FlareData::BottomRight) * radius; verts << vtx; vtx.texCoord[0] = uvRect.bottomLeft(); vtx.texCoord[1] = res->flareCorner(id, FlareData::BottomLeft) * radius; verts << vtx; // Make two triangles. idx << firstIdx << firstIdx + 1 << firstIdx + 2 << firstIdx << firstIdx + 2 << firstIdx + 3; } void makeVerticesForPVS() { int const thisFrame = R_FrameCount(); // The vertex buffer will contain a number of quads. VBuf::Vertices verts; VBuf::Indices idx; VBuf::Type vtx; for(PVSet::const_iterator i = pvs.constBegin(); i != pvs.constEnd(); ++i) { PVLight const *pvl = i.value(); // Skip lights that are not visible right now. /// @todo If so, it might be time to purge it from the PVS. if(pvl->seenFrame != thisFrame) continue; coord_t const distanceSquared = (Rend_EyeOrigin() - pvl->light->lightSourceOrigin().xzy()).lengthSquared(); coord_t const distance = std::sqrt(distanceSquared); // Light intensity is always quadratic per distance. float intensity = pvl->light->lightSourceIntensity(Rend_EyeOrigin()) / distanceSquared; // Projected radius of the light. float const RADIUS_FACTOR = 128; // Light radius of 1 at this distance produces a visible radius of 1. /// @todo The factor should be FOV-dependent. float radius = pvl->light->lightSourceRadius() / distance * RADIUS_FACTOR; float const dot = (pvl->light->lightSourceOrigin().xzy() - Rend_EyeOrigin()).normalize().dot(eyeFront); float const angle = radianToDegree(std::acos(dot)); //qDebug() << "i:" << intensity << "r:" << radius << "IR:" << radius*intensity; /* * The main flare. * - small + bright => burst * - big + bright => star * - small + dim => exponent * - big + dim => exponent */ struct Spec { float axisPos; FlareData::FlareId id; Vector4f color; float size; Rangef minIntensity; Rangef maxIntensity; Rangef minRadius; Rangef maxRadius; Rangef minAngle; Rangef maxAngle; }; typedef Rangef Rgf; static Spec const specs[] = { // axisPos id color size intensity min/max radius min/max angle min/max { 1, FlareData::Burst, Vector4f(1, 1, 1, 1), 1, Rgf(1.0e-8f, 1.0e-6f), Rgf(), Rgf(), Rgf(.5f, .8f), Rgf(), Rgf() }, { 1, FlareData::Star, Vector4f(1, 1, 1, 1), 1, Rgf(1.0e-6f, 1.0e-5f), Rgf(), Rgf(.5f, .7f), Rgf(), Rgf(), Rgf() }, { 1, FlareData::Exponent, Vector4f(1, 1, 1, 1), 2.5f, Rgf(1.0e-6f, 1.0e-5f), Rgf(), Rgf(.1f, .2f), Rgf(), Rgf(), Rgf() }, { .8f, FlareData::Halo, Vector4f(1, 1, 1, .5f), 1, Rgf(5.0e-6f, 5.0e-5f), Rgf(), Rgf(.5f, .7f), Rgf(), Rgf(), Rgf(30, 60) }, { -.8f, FlareData::Ring, Vector4f(.4f, 1, .4f, .26f), .4f, Rgf(1.0e-5f, 1.0e-4f), Rgf(), Rgf(.1f, .5f), Rgf(), Rgf(5, 20), Rgf(40, 50) }, { -1, FlareData::Circle, Vector4f(.4f, .4f, 1, .30f), .5f, Rgf(4.0e-6f, 4.0e-5f), Rgf(), Rgf(.08f, .45f), Rgf(), Rgf(0, 23), Rgf(30, 60) }, { -1.2f , FlareData::Ring, Vector4f(1, .4f, .4f, .26f), .56f, Rgf(1.0e-5f, 1.0e-4f), Rgf(), Rgf(.1f, .5f), Rgf(), Rgf(10, 25), Rgf(35, 50) }, { 1.333f, FlareData::Ring, Vector4f(.5f, .5f, 1, .1f), 1.2f, Rgf(1.0e-8f, 1.0e-7f), Rgf(), Rgf(.1f, .5f), Rgf(), Rgf(10, 25), Rgf(25, 45) }, { 1.45f, FlareData::Ring, Vector4f(1, .5f, .5f, .15f), 1.15f, Rgf(1.0e-8f, 1.0e-7f), Rgf(), Rgf(.1f, .5f), Rgf(), Rgf(10, 25), Rgf(25, 45) }, { -1.45f, FlareData::Ring, Vector4f(1, 1, .9f, .25f), .2f, Rgf(1.0e-5f, 1.0e-4f), Rgf(), Rgf(.1f, .4f), Rgf(), Rgf(5, 10), Rgf(15, 30) }, { -.2f, FlareData::Circle, Vector4f(1, 1, .9f, .2f), .23f, Rgf(1.0e-5f, 1.0e-4f), Rgf(), Rgf(.1f, .4f), Rgf(), Rgf(5, 10), Rgf(15, 30) }, }; for(uint i = 0; i < sizeof(specs)/sizeof(specs[0]); ++i) { Spec const &spec = specs[i]; float size = radius * spec.size; Vector4f color = spec.color; // Apply limits. color.w *= linearRangeFactor(intensity, spec.minIntensity, spec.maxIntensity); color.w *= linearRangeFactor(radius, spec.minRadius, spec.maxRadius); color.w *= linearRangeFactor(angle, spec.minAngle, spec.maxAngle); //qDebug() << linearRangeFactor(intensity, spec.minIntensity, spec.maxIntensity); //qDebug() << linearRangeFactor(radius, spec.minRadius, spec.maxRadius); makeFlare(verts, idx, spec.id, spec.axisPos, size, color, pvl); } //makeFlare(verts, idx, FlareData::Halo, -1, size, pvl); /* // Project viewtocenter vector onto viewSideVec. Vector3f const eyeToFlare = pvl->lightSourceOrigin() - eyePos; // Calculate the 'mirror' vector. float const scale = viewToCenter.dot(viewData->frontVec) / Vector3f(viewData->frontVec).dot(viewData->frontVec); Vector3f const mirror = (Vector3f(viewData->frontVec) * scale - viewToCenter) * 2; */ } buffer->setVertices(verts, gl::Dynamic); buffer->setIndices(gl::Triangles, idx, gl::Dynamic); } }; LensFlares::LensFlares(int console) : ConsoleEffect(console), d(new Instance(this)) {} void LensFlares::clearLights() { d->clearPvs(); } void LensFlares::markLightPotentiallyVisibleForCurrentFrame(IPointLightSource const *lightSource) { d->addToPvs(lightSource); } void LensFlares::glInit() { LOG_AS("fx::LensFlares"); ConsoleEffect::glInit(); d->glInit(); } void LensFlares::glDeinit() { LOG_AS("fx::LensFlares"); d->glDeinit(); ConsoleEffect::glDeinit(); } void fx::LensFlares::beginFrame() { #ifdef FX_TEST_LIGHT markLightPotentiallyVisibleForCurrentFrame(&testLight); // testing #endif d->makeVerticesForPVS(); } void LensFlares::draw() { if(!ClientApp::worldSystem().hasMap()) { // Flares are not visbile unless a map is loaded. return; } if(!viewPlayer) return; /// @todo How'd we get here? -ds viewdata_t const *viewData = R_ViewData(console()); d->eyeFront = Vector3f(viewData->frontVec); Rectanglef const rect = viewRect(); float const aspect = rect.height() / rect.width(); Canvas &canvas = ClientWindow::main().canvas(); d->uViewUnit = Vector2f(aspect, 1.f); d->uPixelAsUv = Vector2f(1.f / canvas.width(), 1.f / canvas.height()); d->uMvpMatrix = Viewer_Matrix(); //GL_GetProjectionMatrix() * Rend_GetModelViewMatrix(console()); DENG2_ASSERT(console() == displayPlayer); //DENG2_ASSERT(viewPlayer - ddPlayers == displayPlayer); if(viewPlayer - ddPlayers != displayPlayer) { qDebug() << "LensFrames::draw: viewPlayer != displayPlayer"; return; } // Depth information is required for occlusion. GLTarget &target = GLState::current().target(); GLTexture *depthTex = target.attachedTexture(GLTarget::Depth); /** * @todo Handle the situation when depth information is not available in the target. */ d->uDepthBuf = depthTex; // The active rectangle is specified with top/left coordinates, but the shader // works with bottom/left ones. Vector4f active(target.activeRectScale(), target.activeRectNormalizedOffset()); active.w = 1 - (active.w + active.y); // flip y d->uActiveRect = active; GLState::push() .setCull(gl::None) .setDepthTest(false) .setDepthWrite(false) .setBlend(true) .setBlendFunc(gl::SrcAlpha, gl::One); d->drawable.draw(); GLState::pop().apply(); } void LensFlares::consoleRegister() { #ifdef FX_TEST_LIGHT C_CMD("testlight", "sf*", TestLight) #endif } } // namespace fx DENG2_SHARED_INSTANCE(fx::FlareData) doomsday-stable-1.15.7/doomsday/client/src/render/fx/bloom.cpp0000664000175000017500000002140112641367670023566 0ustar jaakkojaakko/** @file bloom.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "render/fx/bloom.h" #include "clientapp.h" #include #include #include using namespace de; namespace fx { static int bloomEnabled = true; static float bloomIntensity = .65f; static float bloomThreshold = .35f; static float bloomDispersion = 1; static int bloomComplexity = 1; DENG2_PIMPL(Bloom) { typedef GLBufferT VBuf; Drawable bloom; GLFramebuffer workFB; GLUniform uMvpMatrix; GLUniform uTex; GLUniform uColor; GLUniform uBlurStep; GLUniform uWindow; GLUniform uThreshold; GLUniform uIntensity; Instance(Public *i) : Base(i) , uMvpMatrix("uMvpMatrix", GLUniform::Mat4) , uTex ("uTex", GLUniform::Sampler2D) , uColor ("uColor", GLUniform::Vec4) , uBlurStep ("uBlurStep", GLUniform::Vec2) , uWindow ("uWindow", GLUniform::Vec4) , uThreshold("uThreshold", GLUniform::Float) , uIntensity("uIntensity", GLUniform::Float) {} void glInit() { // Geometry for drawing with: a single quad. VBuf *buf = new VBuf; buf->setVertices(gl::TriangleStrip, VBuf::Builder().makeQuad( Rectanglef(0, 0, 1, 1), Rectanglef(0, 0, 1, 1)), gl::Static); bloom.addBuffer(buf); // The work buffer does not need alpha because the result will be additively // blended back to the framebuffer. workFB.setColorFormat(Image::RGB_888); workFB.setSampleCount(1); workFB.glInit(); ClientApp::shaders().build(bloom.program(), "fx.bloom.horizontal") << uMvpMatrix << uTex << uBlurStep << uWindow << uThreshold << uIntensity; bloom.addProgram("vert"); ClientApp::shaders().build(bloom.program("vert"), "fx.bloom.vertical") << uMvpMatrix << uTex << uBlurStep << uWindow; uMvpMatrix = Matrix4f::ortho(0, 1, 0, 1); } void glDeinit() { bloom.clear(); workFB.glDeinit(); } /** * Takes the current rendered frame buffer contents and applies bloom on it. */ void draw() { GLTarget &target = GLState::current().target(); GLTexture *colorTex = target.attachedTexture(GLTarget::Color); // Must have access to the color texture containing the frame buffer contents. if(!colorTex) return; // Determine the dimensions of the viewport and the target. Rectanglef const rectf = GLState::current().normalizedViewport(); Vector2ui const targetSize = (rectf.size() * target.rectInUse().size()).toVector2ui(); // Quarter resolution is used for better efficiency (without significant loss // of quality). Vector2ui blurSize = (targetSize / 4).max(Vector2ui(1, 1)); // Update the size of the work buffer if needed. Also ensure linear filtering // is used for better-quality blurring. workFB.resize(blurSize); workFB.colorTexture().setFilter(gl::Linear, gl::Linear, gl::MipNone); GLState::push() .setDepthWrite(false) // don't mess with depth information .setDepthTest(false); switch(bloomComplexity) { case 1: // Two passes result in a better glow effect: combining multiple Gaussian curves // ensures that the middle peak is higher/sharper. drawBloomPass(rectf, targetSize, *colorTex, .5f, .75f); drawBloomPass(rectf, targetSize, *colorTex, 1.f, 1.f); break; default: // Single-pass for HW with slow fill rate. drawBloomPass(rectf, targetSize, *colorTex, 1.f, 1.75f); break; } GLState::pop().apply(); } /** * Draws a bloom pass that takes the contents of the framebuffer, applies blurring * and thresholding, and blends the result additively back to the framebuffer. * * @param rectf Normalized viewport rectangle within the target. * @param targetSize Size of the actual area in pixels (affected by target * active rectangle and viewport). * @param colorTarget Texture containing the frame buffer colors. * @param bloomSize Size factor for the effect: at most 1.0; smaller values * cause more blurring/less quality as the work resolution * reduces. * @param weight Weight factor for intensity. * @param targetOp Blending factor (should be gl::One unless debugging). */ void drawBloomPass(Rectanglef const &rectf, Vector2ui const &/*targetSize*/, GLTexture &colorTarget, float bloomSize, float weight, gl::Blend targetOp = gl::One) { uThreshold = bloomThreshold * (1 + bloomSize) / 2.f; uIntensity = bloomIntensity * weight; // Initialize the work buffer for this pass. workFB.target().clear(GLTarget::Color); // Divert rendering to the work area (full or partial area used). GLTarget &target = GLState::current().target(); Vector2ui const workSize = workFB.size() * bloomSize; GLState::push() .setTarget(workFB.target()) .setViewport(Rectangleui::fromSize(workSize)); // Normalized active rectangle of the target. Vector4f const active(target.activeRectScale(), target.activeRectNormalizedOffset()); /* * Draw step #1: thresholding and horizontal blur. */ uTex = colorTarget; // Window in the color buffer: area occupied by the viewport. Top needs to // be flipped because the shader uses the bottom left corner as UV origin. // Also need to apply the active rectangle as it affects where the viewport // ends up inside the frame buffer. uWindow = Vector4f(rectf.left() * active.x + active.z, 1 - (rectf.bottom() * active.y + active.w), rectf.width() * active.x, rectf.height() * active.y); // Spread out or contract the texture sampling of the Gaussian blur kernel. // If dispersion is too large, the quality of the blur will suffer. uBlurStep = Vector2f(bloomDispersion / workFB.size().x, bloomDispersion / workFB.size().y); bloom.setProgram(bloom.program()); // horizontal shader bloom.draw(); GLState::pop(); /* * Draw step #2: vertical blur and blending back to the real framebuffer. */ GLState::push() .setBlend(true) .setBlendFunc(gl::One, targetOp); // Use the work buffer's texture as the source. uTex = workFB.colorTexture(); uWindow = Vector4f(0, 1 - bloomSize, bloomSize, bloomSize); bloom.setProgram("vert"); // vertical shader bloom.draw(); GLState::pop(); } }; Bloom::Bloom(int console) : ConsoleEffect(console) , d(new Instance(this)) {} void Bloom::glInit() { d->glInit(); ConsoleEffect::glInit(); } void Bloom::glDeinit() { ConsoleEffect::glDeinit(); d->glDeinit(); } void Bloom::draw() { if(!ClientApp::worldSystem().hasMap()) { return; } if(!bloomEnabled || bloomIntensity <= 0) { return; } d->draw(); } void Bloom::consoleRegister() { C_VAR_INT ("rend-bloom", &bloomEnabled, 0, 0, 1); C_VAR_FLOAT("rend-bloom-threshold", &bloomThreshold, 0, 0, 1); C_VAR_FLOAT("rend-bloom-intensity", &bloomIntensity, 0, 0, 10); C_VAR_FLOAT("rend-bloom-dispersion", &bloomDispersion, 0, 0, 3.5f); C_VAR_INT ("rend-bloom-complexity", &bloomComplexity, 0, 0, 1); } bool Bloom::isEnabled() // static { return bloomEnabled; } float Bloom::intensity() { return bloomIntensity; } } // namespace fx doomsday-stable-1.15.7/doomsday/client/src/render/fx/resize.cpp0000664000175000017500000001205312641367670023762 0ustar jaakkojaakko/** @file fx/resize.cpp Change the size (pixel density) of the view. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "render/fx/resize.h" #include "ui/clientwindow.h" #include "clientapp.h" #include #include #include #include #include using namespace de; namespace fx { static Ranged const FACTOR_RANGE(1.0 / 16.0, 1.0); DENG2_PIMPL(Resize) { mutable Variable const *pixelDensity = nullptr; mutable Variable const *resizeFactor = nullptr; GLFramebuffer framebuf; Drawable frame; GLUniform uMvpMatrix { "uMvpMatrix", GLUniform::Mat4 }; GLUniform uFrame { "uTex", GLUniform::Sampler2D }; typedef GLBufferT VBuf; Instance(Public *i) : Base(i) {} GuiRootWidget &root() const { return ClientWindow::main().game().root(); } void getConfig() const { if(!pixelDensity) { // Config variables. pixelDensity = &App::config("render.pixelDensity"); resizeFactor = &App::config("render.fx.resize.factor"); } } float factor() const { getConfig(); double const rf = (*resizeFactor > 0? 1.0 / *resizeFactor : 1.0); return FACTOR_RANGE.clamp(*pixelDensity * rf); } /// Determines if the post-processing shader will be applied. bool isActive() const { // This kind of scaling is not compatible with Oculus Rift -- LibOVR does its // own pixel density scaling. if(ClientApp::vr().mode() == VRConfig::OculusRift) return false; return !fequal(factor(), 1.f); } void glInit() { framebuf.glInit(); uFrame = framebuf.colorTexture(); // Drawable for drawing stuff back to the original target. VBuf *buf = new VBuf; buf->setVertices(gl::TriangleStrip, VBuf::Builder().makeQuad(Rectanglef(0, 0, 1, 1), Rectanglef(0, 1, 1, -1)), gl::Static); frame.addBuffer(buf); ClientApp::shaders().build(frame.program(), "generic.texture") << uMvpMatrix << uFrame; } void glDeinit() { LOGDEV_GL_XVERBOSE("Releasing GL resources"); framebuf.glDeinit(); frame.clear(); } void update() { framebuf.resize(GLState::current().target().rectInUse().size() * factor()); framebuf.setSampleCount(GLFramebuffer::defaultMultisampling()); } void begin() { if(!isActive()) return; update(); GLState::push() .setTarget(framebuf.target()) .setViewport(Rectangleui::fromSize(framebuf.size())) .setColorMask(gl::WriteAll) .apply(); framebuf.target().clear(GLTarget::ColorDepthStencil); } void end() { if(!isActive()) return; GLState::pop().apply(); } void draw() { if(!isActive()) return; glEnable(GL_TEXTURE_2D); glDisable(GL_ALPHA_TEST); Rectanglef const vp = GLState::current().viewport(); Vector2f targetSize = GLState::current().target().size(); uMvpMatrix = Matrix4f::ortho(vp.left() / targetSize.x, vp.right() / targetSize.x, vp.top() / targetSize.y, vp.bottom() / targetSize.y); GLState::push() .setBlend(false) .setDepthTest(false) .apply(); frame.draw(); GLState::pop().apply(); glEnable(GL_ALPHA_TEST); glDisable(GL_TEXTURE_2D); glEnable(GL_BLEND); } }; Resize::Resize(int console) : ConsoleEffect(console), d(new Instance(this)) {} bool Resize::isActive() const { return d->isActive(); } void Resize::glInit() { if(!d->isActive()) return; LOG_AS("fx::Resize"); ConsoleEffect::glInit(); d->glInit(); } void Resize::glDeinit() { LOG_AS("fx::Resize"); d->glDeinit(); ConsoleEffect::glDeinit(); } void Resize::beginFrame() { d->begin(); } void Resize::endFrame() { LOG_AS("fx::Resize"); d->end(); d->draw(); if(!d->isActive() && isInited()) { glDeinit(); } } } // namespace fx doomsday-stable-1.15.7/doomsday/client/src/render/fx/vignette.cpp0000664000175000017500000000663312641367670024315 0ustar jaakkojaakko/** @file vignette.cpp Renders a vignette for the player view. * @ingroup render * * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "render/fx/vignette.h" #include "clientapp.h" #include "gl/gl_main.h" #include "gl/gl_texmanager.h" #include "render/rend_main.h" #include #include #include using namespace de; namespace fx { static byte vignetteEnabled = true; static float vignetteDarkness = 1.0f; static float vignetteWidth = 1.0f; static void Vignette_Render(Rectanglei const &viewRect, float fov) { const int DIVS = 60; vec2f_t vec; float cx, cy, outer, inner; float alpha; int i; if(!vignetteEnabled) return; // Center point. cx = viewRect.left() + viewRect.width() / 2.f; cy = viewRect.top() + viewRect.height() / 2.f; // Radius. V2f_Set(vec, viewRect.width() / 2.f, viewRect.height() / 2.f); outer = V2f_Length(vec) + 1; // Extra pixel to account for a possible gap. if(fov < 100) { // Small FOV angles cause the vignette to be thinner/lighter. outer *= (1 + 100.f/fov) / 2; } inner = outer * vignetteWidth * .32f; if(fov > 100) { // High FOV angles cause the vignette to be wider. inner *= 100.f/fov; } alpha = vignetteDarkness * .6f; if(fov > 100) { // High FOV angles cause the vignette to be darker. alpha *= fov/100.f; } GL_BindTextureUnmanaged(GL_PrepareLSTexture(LST_CAMERA_VIGNETTE), gl::Repeat, gl::ClampToEdge); glEnable(GL_TEXTURE_2D); glBegin(GL_TRIANGLE_STRIP); for(i = 0; i <= DIVS; ++i) { float ang = (float)(2 * de::PI * i) / (float)DIVS; float dx = cos(ang); float dy = sin(ang); glColor4f(0, 0, 0, alpha); glTexCoord2f(0, 1); glVertex2f(cx + outer * dx, cy + outer * dy); glColor4f(0, 0, 0, 0); glTexCoord2f(0, 0); glVertex2f(cx + inner * dx, cy + inner * dy); } glEnd(); glDisable(GL_TEXTURE_2D); } Vignette::Vignette(int console) : ConsoleEffect(console) {} void Vignette::draw() { if(!ClientApp::worldSystem().hasMap()) { return; } /// @todo Field of view should be console-specific. Vignette_Render(viewRect(), Rend_FieldOfView()); } void Vignette::consoleRegister() { C_VAR_BYTE ("rend-vignette", &vignetteEnabled, 0, 0, 1); C_VAR_FLOAT("rend-vignette-darkness", &vignetteDarkness, CVF_NO_MAX, 0, 0); C_VAR_FLOAT("rend-vignette-width", &vignetteWidth, 0, 0, 2); } } // namespace fx doomsday-stable-1.15.7/doomsday/client/src/render/fx/postprocessing.cpp0000664000175000017500000001555112641367670025551 0ustar jaakkojaakko/** @file postprocessing.cpp World frame post processing. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "render/fx/postprocessing.h" #include "ui/clientwindow.h" #include "clientapp.h" #include #include #include #include using namespace de; namespace fx { DENG2_PIMPL(PostProcessing) { GLFramebuffer framebuf; Drawable frame; GLUniform uMvpMatrix; GLUniform uFrame; GLUniform uFadeInOut; Animation fade; float opacity; struct QueueEntry { String shaderName; float fade; TimeDelta span; QueueEntry(String const &name, float f, TimeDelta const &s) : shaderName(name), fade(f), span(s) {} }; typedef QList Queue; Queue queue; typedef GLBufferT VBuf; Instance(Public *i) : Base(i) , uMvpMatrix("uMvpMatrix", GLUniform::Mat4) , uFrame ("uTex", GLUniform::Sampler2D) , uFadeInOut("uFadeInOut", GLUniform::Float) , fade(0, Animation::Linear) , opacity(1.f) {} GuiRootWidget &root() const { return ClientWindow::main().game().root(); } #if 0 Vector2ui consoleSize() const { /** * @todo The offscreen target should simply use the viewport area, not * the full canvas size. This way the shader could, for instance, * easily mirror texture coordinates. However, this would require * drawing the frame without applying a further GL viewport in the game * widgets. -jk */ //return self.viewRect().size(); return root().window().canvas().size(); } #endif bool setShader(String const &name) { try { self.shaders().build(frame.program(), "fx.post." + name); LOG_GL_MSG("Post-processing shader \"fx.post.%s\"") << name; return true; } catch(Error const &er) { LOG_GL_WARNING("Failed to set shader to \"fx.post.%s\":\n%s") << name << er.asText(); } return false; } /// Determines if the post-processing shader will be applied. bool isActive() const { return !fade.done() || fade > 0 || !queue.isEmpty(); } void glInit() { //LOG_DEBUG("Allocating texture and target, size %s") << consoleSize().asText(); framebuf.glInit(); //framebuf.setColorFormat(Image::RGBA_8888); //framebuf.resize(consoleSize()); uFrame = framebuf.colorTexture(); // Drawable for drawing stuff back to the original target. VBuf *buf = new VBuf; buf->setVertices(gl::TriangleStrip, VBuf::Builder().makeQuad(Rectanglef(0, 0, 1, 1), Rectanglef(0, 1, 1, -1)), gl::Static); frame.addBuffer(buf); frame.program() << uMvpMatrix << uFrame << uFadeInOut; } void glDeinit() { LOGDEV_GL_XVERBOSE("Releasing GL resources"); framebuf.glDeinit(); } void update() { framebuf.resize(GLState::current().target().rectInUse().size()); framebuf.setSampleCount(GLFramebuffer::defaultMultisampling()); } void checkQueue() { // An ongoing fade? if(!fade.done()) return; // Let's check back later. if(!queue.isEmpty()) { QueueEntry entry = queue.takeFirst(); if(!entry.shaderName.isEmpty()) { if(!setShader(entry.shaderName)) { fade = 0; return; } } fade.setValue(entry.fade, entry.span); LOGDEV_GL_VERBOSE("Shader '%s' fade:%s") << entry.shaderName << fade.asText(); } } void begin() { if(!isActive()) return; update(); GLState::push() .setTarget(framebuf.target()) .setViewport(Rectangleui::fromSize(framebuf.size())) .setColorMask(gl::WriteAll) .apply(); framebuf.target().clear(GLTarget::ColorDepthStencil); } void end() { if(!isActive()) return; GLState::pop().apply(); } void draw() { if(!isActive()) return; glEnable(GL_TEXTURE_2D); glDisable(GL_ALPHA_TEST); Rectanglef const vp = GLState::current().viewport(); Vector2f targetSize = GLState::current().target().size(); uMvpMatrix = Matrix4f::ortho(vp.left() / targetSize.x, vp.right() / targetSize.x, vp.top() / targetSize.y, vp.bottom() / targetSize.y); uFadeInOut = fade * opacity; GLState::push() .setBlend(false) .setDepthTest(false) .apply(); frame.draw(); GLState::pop().apply(); glEnable(GL_ALPHA_TEST); glDisable(GL_TEXTURE_2D); glEnable(GL_BLEND); } }; PostProcessing::PostProcessing(int console) : ConsoleEffect(console), d(new Instance(this)) {} bool PostProcessing::isActive() const { return d->isActive(); } void PostProcessing::fadeInShader(String const &fxPostShader, TimeDelta const &span) { d->queue.append(Instance::QueueEntry(fxPostShader, 1, span)); } void PostProcessing::fadeOut(TimeDelta const &span) { d->queue.append(Instance::QueueEntry("", 0, span)); } void PostProcessing::setOpacity(float opacity) { d->opacity = opacity; } void PostProcessing::glInit() { if(!d->isActive()) return; LOG_AS("fx::PostProcessing"); ConsoleEffect::glInit(); d->glInit(); } void PostProcessing::glDeinit() { LOG_AS("fx::PostProcessing"); d->glDeinit(); ConsoleEffect::glDeinit(); } void PostProcessing::beginFrame() { d->begin(); } void PostProcessing::draw() { d->end(); d->draw(); } void PostProcessing::endFrame() { LOG_AS("fx::PostProcessing"); if(!d->isActive() && isInited()) { glDeinit(); } d->checkQueue(); } } // namespace fx doomsday-stable-1.15.7/doomsday/client/src/render/trianglestripbuilder.cpp0000664000175000017500000000706312641367670026307 0ustar jaakkojaakko/** @file render/trianglestripbuilder.cpp Triangle Strip Geometry Builder. * * @authors Copyright © 2011-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include "render/trianglestripbuilder.h" namespace de { DENG2_PIMPL(TriangleStripBuilder) { ClockDirection direction; bool buildTexCoords; int initialReserveElements; QScopedPointer positions; QScopedPointer texcoords; Instance(Public *i, bool buildTexCoords) : Base(i), direction(Clockwise), buildTexCoords(buildTexCoords), initialReserveElements(0), positions(0), texcoords(0) {} void reserveElements(int num) { if(num < 0) return; // Huh? // Time to allocate the buffers? if(positions.isNull()) { positions.reset(new PositionBuffer()); if(buildTexCoords) texcoords.reset(new TexCoordBuffer()); // The user may already know how many elements they will require. num += initialReserveElements; } // Reserve this many new elements. positions->reserve(positions->size() + num); if(buildTexCoords) texcoords->reserve(texcoords->size() + num); } }; TriangleStripBuilder::TriangleStripBuilder(bool buildTexCoords) : d(new Instance(this, buildTexCoords)) {} void TriangleStripBuilder::begin(ClockDirection direction, int reserveElements) { d->direction = direction; d->initialReserveElements = de::max(reserveElements, 0); // Destroy any existing unclaimed strip geometry. d->positions.reset(); d->texcoords.reset(); } void TriangleStripBuilder::extend(AbstractEdge &edge) { // Silently ignore invalid edges. if(!edge.isValid()) return; AbstractEdge::Event const &from = edge.first(); AbstractEdge::Event const &to = edge.last(); d->reserveElements(2); d->positions->append((d->direction == Anticlockwise? to : from).origin()); d->positions->append((d->direction == Anticlockwise? from : to).origin()); if(d->buildTexCoords) { double edgeLength = to.origin().z - from.origin().z; d->texcoords->append(edge.materialOrigin() + Vector2f(0, (d->direction == Anticlockwise? 0 : edgeLength))); d->texcoords->append(edge.materialOrigin() + Vector2f(0, (d->direction == Anticlockwise? edgeLength : 0))); } } int TriangleStripBuilder::numElements() const { return d->positions.isNull()? 0 : d->positions->size(); } int TriangleStripBuilder::take(PositionBuffer **positions, TexCoordBuffer **texcoords) { int retNumElements = numElements(); *positions = d->positions.take(); if(texcoords) { *texcoords = d->texcoords.take(); } return retNumElements; } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/render/vr.cpp0000664000175000017500000001441212641367670022474 0ustar jaakkojaakko/** @file render/vr.cpp Stereoscopic rendering and Oculus Rift support. * * @authors Copyright (c) 2013-2014 Jaakko Keränen * @authors Copyright (c) 2013 Christopher Bruns * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_console.h" #include "render/vr.h" #include "ui/clientwindow.h" #include #include using namespace de; namespace VR { float weaponDistance = 10.f; // global } static int vrMode = VRConfig::Mono; //static float vrRiftFovX = 114.8f; //static float vrNonRiftFovX = 95.f; //static float vrRiftLatency; //static byte autoLoadRiftParams = 1; static int vrRiftFBSamples; static float vrHudDistance; static float vrPlayerHeight; static float vrIpd; static byte vrSwapEyes; static float vrDominantEye; VRConfig &vrCfg() { DENG2_ASSERT(DENG2_BASE_GUI_APP != 0); return DENG2_BASE_GUI_APP->vr(); } /* float VR_RiftFovX() { return vrRiftFovX; } */ static void vrConfigVariableChanged() { vrCfg().setDominantEye(vrDominantEye); vrCfg().setScreenDistance(vrHudDistance); vrCfg().setInterpupillaryDistance(vrIpd); vrCfg().setPhysicalPlayerHeight(vrPlayerHeight); //vrCfg().oculusRift().setPredictionLatency(vrRiftLatency); vrCfg().setRiftFramebufferSampleCount(vrRiftFBSamples); vrCfg().setSwapEyes(vrSwapEyes); } // Interplay among vrNonRiftFovX, vrRiftFovX, and cameraFov depends on vrMode // see also rend_main.cpp static void vrModeChanged() { /* if(vrMode == VRConfig::OculusRift && !vrCfg().oculusRift().isReady()) { // Can't activate Oculus Rift mode unless the device is connected. vrMode = VRConfig::Mono; LOG_WARNING("Oculus Rift not connected, reverting to normal 3D mode"); }*/ vrCfg().setMode(VRConfig::StereoMode(vrMode)); if(ClientWindow::mainExists()) { // The logical UI size may need to be changed. ClientWindow &win = ClientWindow::main(); win.updateRootSize(); win.updateCanvasFormat(); // possibly changes pixel format } /* // Update FOV cvar accordingly. if(vrMode == VRConfig::OculusRift) { if(Con_GetFloat("rend-camera-fov") != vrRiftFovX) Con_SetFloat("rend-camera-fov", vrRiftFovX); // Update prediction latency. vrCfg().oculusRift().setPredictionLatency(vrRiftLatency); } else { if(Con_GetFloat("rend-camera-fov") != vrNonRiftFovX) Con_SetFloat("rend-camera-fov", vrNonRiftFovX); }*/ } /* static void vrRiftFovXChanged() { if(vrCfg().mode() == VRConfig::OculusRift) { if(Con_GetFloat("rend-camera-fov") != vrRiftFovX) Con_SetFloat("rend-camera-fov", vrRiftFovX); } } static void vrNonRiftFovXChanged() { if(vrCfg().mode() != VRConfig::OculusRift) { if(Con_GetFloat("rend-camera-fov") != vrNonRiftFovX) Con_SetFloat("rend-camera-fov", vrNonRiftFovX); } } D_CMD(LoadRiftParams) { DENG2_UNUSED3(src, argc, argv); return VR_LoadRiftParameters(); }*/ D_CMD(ResetRiftPose) { DENG2_UNUSED3(src, argc, argv); vrCfg().oculusRift().resetTracking(); LOG_INPUT_MSG("Reset Oculus Rift position tracking"); return true; } void VR_ConsoleRegister() { // Get the built-in defaults. vrDominantEye = vrCfg().dominantEye(); vrHudDistance = vrCfg().screenDistance(); vrIpd = vrCfg().interpupillaryDistance(); vrPlayerHeight = vrCfg().physicalPlayerHeight(); //vrRiftLatency = vrCfg().oculusRift().predictionLatency(); vrRiftFBSamples = vrCfg().riftFramebufferSampleCount(); vrSwapEyes = vrCfg().swapEyes(); /** * @todo When old-style console variables become obsolete, VRConfig should expose * these settings as a Record that can be attached under Config. */ C_VAR_INT2 ("rend-vr-mode", &vrMode, 0, 0, VRConfig::NUM_STEREO_MODES - 1, vrModeChanged); //C_VAR_BYTE ("rend-vr-autoload-rift-params", &autoLoadRiftParams, 0, 0, 1); //C_VAR_FLOAT2("rend-vr-nonrift-fovx", &vrNonRiftFovX, 0, 5.0f, 270.0f, vrNonRiftFovXChanged); //C_VAR_FLOAT2("rend-vr-rift-fovx", &vrRiftFovX, 0, 5.0f, 270.0f, vrRiftFovXChanged); //C_VAR_FLOAT2("rend-vr-rift-latency", &vrRiftLatency, 0, 0.0f, 0.100f, vrConfigVariableChanged); C_VAR_FLOAT2("rend-vr-dominant-eye", &vrDominantEye, 0, -1.0f, 1.0f, vrConfigVariableChanged); C_VAR_FLOAT2("rend-vr-hud-distance", &vrHudDistance, 0, 0.01f, 40.0f, vrConfigVariableChanged); C_VAR_FLOAT2("rend-vr-ipd", &vrIpd, 0, 0.02f, 0.1f, vrConfigVariableChanged); C_VAR_FLOAT2("rend-vr-player-height", &vrPlayerHeight, 0, 1.0f, 2.4f, vrConfigVariableChanged); C_VAR_INT2 ("rend-vr-rift-samples", &vrRiftFBSamples, 0, 1, 4, vrConfigVariableChanged); C_VAR_BYTE2 ("rend-vr-swap-eyes", &vrSwapEyes, 0, 0, 1, vrConfigVariableChanged); //C_CMD("loadriftparams", NULL, LoadRiftParams); C_CMD("resetriftpose", NULL, ResetRiftPose); } #if 0 bool VR_LoadRiftParameters() { de::OculusRift &ovr = vrCfg().oculusRift(); if(ovr.isReady()) { Con_SetFloat("rend-vr-ipd", ovr.interpupillaryDistance()); // from Oculus SDK Con_SetFloat("rend-vr-rift-fovx", ovr.fovX()); // I think this field of view is unreliable... CMB /* float fov = 180.0f / de::PI * 2.0f * (atan2( info.EyeToScreenDistance, 0.5f * (info.HScreenSize - info.InterpupillaryDistance))); */ return true; } return false; } #endif doomsday-stable-1.15.7/doomsday/client/src/render/r_fakeradio.cpp0000664000175000017500000002212512641367670024313 0ustar jaakkojaakko/** @file r_fakeradio.cpp Faked Radiosity Lighting. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ //#include #include /// @todo remove me #include #include #include #include "de_base.h" #include "de_render.h" #include "world/blockmap.h" #include "world/lineowner.h" #include "world/map.h" #include "ConvexSubspace" #include "Face" #include "SectorCluster" #include "Surface" #include "Vertex" #include "render/rend_fakeradio.h" using namespace de; static LineSideRadioData *lineSideRadioData; bool Rend_RadioLineCastsShadow(Line const &line) { if(line.definesPolyobj()) return false; if(line.isSelfReferencing()) return false; // Lines with no other neighbor do not qualify for shadowing. if(&line.v1Owner()->next().line() == &line || &line.v2Owner()->next().line() == &line) return false; return true; } bool Rend_RadioPlaneCastsShadow(Plane const &plane) { if(plane.surface().hasMaterial()) { MaterialAnimator &matAnimator = plane.surface().material().getAnimator(Rend_MapSurfaceMaterialSpec()); // Ensure we have up to date info about the material. matAnimator.prepare(); if(!matAnimator.material().isDrawable()) return false; if(matAnimator.material().isSkyMasked()) return false; if(matAnimator.glowStrength() > 0) return false; } return true; } LineSideRadioData &Rend_RadioDataForLineSide(LineSide &side) { return lineSideRadioData[side.line().indexInMap() * 2 + (side.isBack()? 1 : 0)]; } /** * Given two lines "connected" by shared origin coordinates (0, 0) at a "corner" * vertex, calculate the point which lies @a distA away from @a lineA and also * @a distB from @a lineB. The point should also be the nearest point to the * origin (in case of parallel lines). * * @param lineADirection Direction vector for the "left" line. * @param dist1 Distance from @a lineA to offset the corner point. * @param lineBDirection Direction vector for the "right" line. * @param dist2 Distance from @a lineB to offset the corner point. * * Return values: * @param point Coordinates for the corner point are written here. Can be @c 0. * @param lp Coordinates for the "extended" point are written here. Can be @c 0. */ static void cornerNormalPoint(Vector2d const &lineADirection, double dist1, Vector2d const &lineBDirection, double dist2, Vector2d *point, Vector2d *lp) { // Any work to be done? if(!point && !lp) return; // Length of both lines. double len1 = lineADirection.length(); double len2 = lineBDirection.length(); // Calculate normals for both lines. Vector2d norm1(-lineADirection.y / len1 * dist1, lineADirection.x / len1 * dist1); Vector2d norm2( lineBDirection.y / len2 * dist2, -lineBDirection.x / len2 * dist2); // Do we need to calculate the extended points, too? Check that // the extension does not bleed too badly outside the legal shadow // area. if(lp) { *lp = lineBDirection / len2 * dist2; } // Do we need to determine the intercept point? if(!point) return; // Normal shift to produce the lines we need to find the intersection. Partition lineA(lineADirection, norm1); Partition lineB(lineBDirection, norm2); if(!lineA.isParallelTo(lineB)) { *point = lineA.intercept(lineB); return; } // Special case: parallel // There will be no intersection at any point therefore it will not be // possible to determine our corner point (so just use a normal as the // point instead). *point = norm1; } /** * @return The width (world units) of the shadow edge. It is scaled depending on * the length of @a edge. */ static double shadowEdgeWidth(Vector2d const &edge) { double const normalWidth = 20; //16; double const maxWidth = 60; // A long edge? double length = edge.length(); if(length > 600) { double w = length - 600; if(w > 1000) w = 1000; return normalWidth + w / 1000 * maxWidth; } return normalWidth; } void Rend_RadioUpdateVertexShadowOffsets(Vertex &vtx) { if(!vtx.lineOwnerCount()) return; Vector2d leftDir, rightDir; LineOwner *base = vtx.firstLineOwner(); LineOwner *own = base; do { Line const &lineB = own->line(); Line const &lineA = own->next().line(); if(&lineB.from() == &vtx) { rightDir = lineB.direction(); } else { rightDir = -lineB.direction(); } if(&lineA.from() == &vtx) { leftDir = -lineA.direction(); } else { leftDir = lineA.direction(); } // The left side is always flipped. leftDir *= -1; cornerNormalPoint(leftDir, shadowEdgeWidth(leftDir), rightDir, shadowEdgeWidth(rightDir), &own->_shadowOffsets.inner, &own->_shadowOffsets.extended); own = &own->next(); } while(own != base); } void Rend_RadioInitForMap(Map &map) { Time begunAt; LOG_AS("Rend_RadioInitForMap"); lineSideRadioData = reinterpret_cast( Z_Calloc(sizeof(*lineSideRadioData) * map.sideCount(), PU_MAP, 0)); map.forAllVertexs([] (Vertex &vertex) { Rend_RadioUpdateVertexShadowOffsets(vertex); return LoopContinue; }); /** * The algorithm: * * 1. Use the BSP leaf blockmap to look for all the blocks that are * within the line's shadow bounding box. * * 2. Check the ConvexSubspaces whose sector is the same as the line. * * 3. If any of the shadow points are in the subspace, or any of the * shadow edges cross one of the subspace's edges (not parallel), * link the line to the ConvexSubspace. */ map.forAllLines([] (Line &line) { if(Rend_RadioLineCastsShadow(line)) { // For each side of the line. for(int i = 0; i < 2; ++i) { LineSide &side = line.side(i); if(!side.hasSector()) continue; if(!side.hasSections()) continue; Vertex const &vtx0 = line.vertex(i); Vertex const &vtx1 = line.vertex(i ^ 1); LineOwner const &vo0 = line.vertexOwner(i)->next(); LineOwner const &vo1 = line.vertexOwner(i ^ 1)->prev(); AABoxd box = line.aaBox(); // Use the extended points, they are wider than inoffsets. Vector2d point = vtx0.origin() + vo0.extendedShadowOffset(); V2d_AddToBoxXY(box.arvec2, point.x, point.y); point = vtx1.origin() + vo1.extendedShadowOffset(); V2d_AddToBoxXY(box.arvec2, point.x, point.y); // Link the shadowing line to all the subspaces whose axis-aligned // bounding box intersects 'bounds'. validCount++; int const localValidCount = validCount; line.map().subspaceBlockmap().forAllInBox(box, [&box, &side, &localValidCount] (void *object) { ConvexSubspace &sub = *(ConvexSubspace *)object; if(sub.validCount() != localValidCount) // not yet processed { sub.setValidCount(localValidCount); if(&sub.sector() == side.sectorPtr()) { // Check the bounds. AABoxd const &polyBox = sub.poly().aaBox(); if(!(polyBox.maxX < box.minX || polyBox.minX > box.maxX || polyBox.minY > box.maxY || polyBox.maxY < box.minY)) { sub.addShadowLine(side); } } } return LoopContinue; }); } } return LoopContinue; }); LOGDEV_GL_MSG("Completed in %.2f seconds") << begunAt.since(); } doomsday-stable-1.15.7/doomsday/client/src/render/shadowedge.cpp0000664000175000017500000002437512641367670024170 0ustar jaakkojaakko/** @file shadowedge.cpp FakeRadio Shadow Edge Geometry * * @authors Copyright © 2004-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "render/shadowedge.h" #include "Face" #include "HEdge" #include "ConvexSubspace" #include "Plane" #include "Sector" #include "SectorCluster" #include "Surface" #include "world/lineowner.h" #include "render/rend_main.h" #include "MaterialAnimator" #include "WallEdge" namespace de { DENG2_PIMPL_NOREF(ShadowEdge) { HEdge const *leftMostHEdge; int edge; Vector3d inner; Vector3d outer; float sectorOpenness; float openness; }; ShadowEdge::ShadowEdge() : d(new Instance) {} void ShadowEdge::init(HEdge const &leftMostHEdge, int edge) { d->leftMostHEdge = &leftMostHEdge; d->edge = edge; d->inner = d->outer = Vector3d(); d->sectorOpenness = d->openness = 0; } /** * Returns a value in the range of 0..2, representing how 'open' the edge is. * * @c =0 Completely closed, it is facing a wall or is relatively distant from * the edge on the other side. * @c >0 && <1 How near the 'other' edge is. * @c =1 At the same height as "this" one. * @c >1 The 'other' edge is past our height (clearly 'open'). */ static float opennessFactor(float fz, float bz, float bhz) { if(fz <= bz - SHADOWEDGE_OPEN_THRESHOLD || fz >= bhz) return 0; // Fully closed. if(fz >= bhz - SHADOWEDGE_OPEN_THRESHOLD) return (bhz - fz) / SHADOWEDGE_OPEN_THRESHOLD; if(fz <= bz) return 1 - (bz - fz) / SHADOWEDGE_OPEN_THRESHOLD; if(fz <= bz + SHADOWEDGE_OPEN_THRESHOLD) return 1 + (fz - bz) / SHADOWEDGE_OPEN_THRESHOLD; // Fully open! return 2; } /// @todo fixme: Should use the visual plane heights of sector clusters. static bool middleMaterialCoversOpening(LineSide const &side) { if(!side.hasSector()) return false; // Never. if(!side.hasSections()) return false; if(!side.middle().hasMaterial()) return false; MaterialAnimator &matAnimator = side.middle().material().getAnimator(Rend_MapSurfaceMaterialSpec()); // Ensure we have up to date info about the material. matAnimator.prepare(); // Might the material cover the opening? if(matAnimator.isOpaque() && !side.middle().blendMode() && side.middle().opacity() >= 1) { // Stretched middles always cover the opening. if(side.isFlagged(SDF_MIDDLE_STRETCH)) return true; Sector const &frontSec = side.sector(); Sector const *backSec = side.back().sectorPtr(); // Determine the opening between the visual sector planes at this edge. coord_t openBottom; if(backSec && backSec->floor().heightSmoothed() > frontSec.floor().heightSmoothed()) { openBottom = backSec->floor().heightSmoothed(); } else { openBottom = frontSec.floor().heightSmoothed(); } coord_t openTop; if(backSec && backSec->ceiling().heightSmoothed() < frontSec.ceiling().heightSmoothed()) { openTop = backSec->ceiling().heightSmoothed(); } else { openTop = frontSec.ceiling().heightSmoothed(); } if(matAnimator.dimensions().y >= openTop - openBottom) { // Possibly; check the placement. if(side.leftHEdge()) // possibility of degenerate BSP leaf { WallEdge edge(WallSpec::fromMapSide(side, LineSide::Middle), *side.leftHEdge(), Line::From); return (edge.isValid() && edge.top().z() > edge.bottom().z() && edge.top().z() >= openTop && edge.bottom().z() <= openBottom); } } } return false; } void ShadowEdge::prepare(int planeIndex) { int const otherPlaneIndex = planeIndex == Sector::Floor? Sector::Ceiling : Sector::Floor; HEdge const &hedge = *d->leftMostHEdge; SectorCluster const &cluster = hedge.face().mapElementAs().cluster(); Plane const &plane = cluster.visPlane(planeIndex); LineSide const &lineSide = hedge.mapElementAs().lineSide(); d->sectorOpenness = 0; // Default is fully closed. d->openness = 0; // Default is fully closed. // Determine the 'openness' of the wall edge sector. If the sector is open, // there won't be a shadow at all. Open neighbor sectors cause some changes // in the polygon corner vertices (placement, opacity). if(hedge.twin().hasFace() && hedge.twin().face().mapElementAs().hasCluster()) { SectorCluster const &backCluster = hedge.twin().face().mapElementAs().cluster(); Plane const &backPlane = backCluster.visPlane(planeIndex); Surface const &wallEdgeSurface = lineSide.back().hasSector()? lineSide.surface(planeIndex == Sector::Ceiling? LineSide::Top : LineSide::Bottom) : lineSide.middle(); // Figure out the relative plane heights. coord_t fz = plane.heightSmoothed(); if(planeIndex == Sector::Ceiling) fz = -fz; coord_t bz = backPlane.heightSmoothed(); if(planeIndex == Sector::Ceiling) bz = -bz; coord_t bhz = backCluster.plane(otherPlaneIndex).heightSmoothed(); if(planeIndex == Sector::Ceiling) bhz = -bhz; // Determine openness. if(fz < bz && !wallEdgeSurface.hasMaterial()) { d->sectorOpenness = 2; // Consider it fully open. } // Is the back sector a closed yet sky-masked surface? else if(cluster.visFloor().heightSmoothed() >= backCluster.visCeiling().heightSmoothed() && cluster.visPlane(otherPlaneIndex).surface().hasSkyMaskedMaterial() && backCluster.visPlane(otherPlaneIndex).surface().hasSkyMaskedMaterial()) { d->sectorOpenness = 2; // Consider it fully open. } else { // Does the middle material completely cover the open range (we do // not want to give away the location of any secret areas)? if(!middleMaterialCoversOpening(lineSide)) { d->sectorOpenness = opennessFactor(fz, bz, bhz); } } } // Only calculate the remaining values when the edge is at least partially open. if(d->sectorOpenness >= 1) return; // Find the neighbor of this wall section and determine the relative // 'openness' of it's plane heights vs those of "this" wall section. /// @todo fixme: Should use the visual plane heights of sector clusters. int const edge = lineSide.sideId() ^ d->edge; LineOwner const *vo = &lineSide.line().vertexOwner(edge)->navigate(ClockDirection(d->edge ^ 1)); Line const &neighborLine = vo->line(); if(&neighborLine == &lineSide.line()) { d->openness = 1; // Fully open. } else if(neighborLine.isSelfReferencing()) /// @todo Skip over these? -ds { d->openness = 1; } else { // Choose the correct side of the neighbor (determined by which vertex is shared). LineSide const &neighborLineSide = neighborLine.side(&lineSide.line().vertex(edge) == &neighborLine.from()? d->edge ^ 1 : d->edge); if(!neighborLineSide.hasSections() && neighborLineSide.back().hasSector()) { // A one-way window, open side. d->openness = 1; } else if(!neighborLineSide.hasSector() || (neighborLineSide.back().hasSector() && middleMaterialCoversOpening(neighborLineSide))) { d->openness = 0; } else if(neighborLineSide.back().hasSector()) { // Its a normal neighbor. Sector const *backSec = neighborLineSide.back().sectorPtr(); if(backSec != &cluster.sector() && !((plane.isSectorFloor() && backSec->ceiling().heightSmoothed() <= plane.heightSmoothed()) || (plane.isSectorCeiling() && backSec->floor().height() >= plane.heightSmoothed()))) { // Figure out the relative plane heights. coord_t fz = plane.heightSmoothed(); if(planeIndex == Sector::Ceiling) fz = -fz; coord_t bz = backSec->plane(planeIndex).heightSmoothed(); if(planeIndex == Sector::Ceiling) bz = -bz; coord_t bhz = backSec->plane(otherPlaneIndex).heightSmoothed(); if(planeIndex == Sector::Ceiling) bhz = -bhz; d->openness = opennessFactor(fz, bz, bhz); } } } if(d->openness < 1) { LineOwner *vo = lineSide.line().vertexOwner(lineSide.sideId() ^ d->edge); if(d->edge) vo = &vo->prev(); d->inner = Vector3d(lineSide.vertex(d->edge).origin() + vo->innerShadowOffset(), plane.heightSmoothed()); } else { d->inner = Vector3d(lineSide.vertex(d->edge).origin() + vo->extendedShadowOffset(), plane.heightSmoothed()); } d->outer = Vector3d(lineSide.vertex(d->edge).origin(), plane.heightSmoothed()); } Vector3d const &ShadowEdge::inner() const { return d->inner; } Vector3d const &ShadowEdge::outer() const { return d->outer; } float ShadowEdge::openness() const { return d->openness; } float ShadowEdge::sectorOpenness() const { return d->sectorOpenness; } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/render/rend_console.cpp0000664000175000017500000006661312641367670024531 0ustar jaakkojaakko#if 0 * /** @file rend_console.cpp Console Rendering. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include "de_base.h" #include "de_console.h" #include "de_graphics.h" #include "de_render.h" #include "de_resource.h" #include "de_ui.h" #include "MaterialSnapshot" #include "cbuffer.h" using namespace de; // Console (display) Modes: typedef enum { CM_HALFSCREEN, // Half vertical window height. CM_FULLSCREEN, // Full window height. CM_SINGLELINE, // Line height x1. CM_CUSTOM // Some other offset positioned by the user. } consolemode_t; void Rend_ConsoleUpdateBackground(); float ConsoleOpenY; // Where the console bottom is when open. float consoleMoveSpeed = .5f; // Speed of console opening/closing. float consoleBackgroundAlpha = .75f; float consoleBackgroundLight = .14f; struct uri_s *consoleBackgroundMaterialUri; int consoleBackgroundTurn = 0; // The rotation variable. float consoleBackgroundZoom = 1.0f; byte consoleTextShadow = false; byte consoleShowFPS = false; static boolean inited = false; static consolemode_t consoleMode; static boolean needResize = false; /// @c true= We are waiting on a successful resize to draw. static int moveLineDelta; // Number of lines to move the console when we are next able. static float ConsoleY; // Where the console bottom is currently? static float ConsoleDestY; // Where the console bottom should be? static float ConsoleBlink; // Cursor blink timer (35 Hz tics). static boolean openingOrClosing; static float consoleAlpha, consoleAlphaTarget; static Material *consoleBackgroundMaterial; static float fontSy; // Font size Y. static float funnyAng; static float const CcolYellow[3] = { 1, .85f, .3f }; static char const *consoleTitle = DOOMSDAY_NICENAME " " DOOMSDAY_VERSION_TEXT; static char secondaryTitleText[256]; static char statusText[256]; void Rend_ConsoleRegister() { C_VAR_FLOAT ("con-background-alpha", &consoleBackgroundAlpha, 0, 0, 1); C_VAR_FLOAT ("con-background-light", &consoleBackgroundLight, 0, 0, 1); C_VAR_URIPTR2("con-background-material", &consoleBackgroundMaterialUri, 0, 0, 0, Rend_ConsoleUpdateBackground); C_VAR_INT ("con-background-turn", &consoleBackgroundTurn, CVF_NO_MIN|CVF_NO_MAX, 0, 0); C_VAR_FLOAT ("con-background-zoom", &consoleBackgroundZoom, 0, 0.1f, 100.0f); C_VAR_BYTE ("con-fps", &consoleShowFPS, 0, 0, 1); C_VAR_FLOAT ("con-move-speed", &consoleMoveSpeed, 0, 0, 1); C_VAR_BYTE ("con-text-shadow", &consoleTextShadow, 0, 0, 1); } static float calcConsoleTitleBarHeight() { int oldFont, border = DENG_WINDOW->width() / 120, height; DENG_ASSERT(inited); oldFont = FR_Font(); FR_SetFont(fontVariable[FS_BOLD]); height = FR_SingleLineHeight("Con") + border; FR_SetFont(oldFont); return height; } static inline int calcConsoleMinHeight() { DENG_ASSERT(inited); return fontSy * 1.5f + calcConsoleTitleBarHeight() / DENG_WINDOW->height() * SCREENHEIGHT; } void Rend_ConsoleInit() { if(!inited) { // First init. consoleMode = CM_HALFSCREEN; ConsoleY = 0; ConsoleOpenY = SCREENHEIGHT/2; ConsoleDestY = 0; moveLineDelta = 0; openingOrClosing = false; consoleAlpha = 0; consoleAlphaTarget = 0; funnyAng = 0; ConsoleBlink = 0; std::memset(secondaryTitleText, 0, sizeof(secondaryTitleText)); std::memset(statusText, 0, sizeof(statusText)); } consoleBackgroundMaterial = 0; funnyAng = 0; if(inited) { Rend_ConsoleUpdateTitle(); Rend_ConsoleUpdateBackground(); } needResize = true; inited = true; } boolean Rend_ConsoleResize(boolean force) { if(!inited) return false; // Are we forcing a resize? if(force) needResize = true; // If there is no pending resize we can get out of here. if(!needResize) return false; // We can only resize if the font renderer is available. if(FR_Available()) { float scale[2], fontScaledY, gtosMulY; int lineHeight; FR_SetFont(Con_Font()); FR_LoadDefaultAttrib(); FR_SetTracking(Con_FontTracking()); gtosMulY = DENG_WINDOW->height() / 200.0f; lineHeight = FR_SingleLineHeight("Con"); Con_FontScale(&scale[0], &scale[1]); fontScaledY = lineHeight * Con_FontLeading() * scale[1]; fontSy = fontScaledY / gtosMulY; if(consoleMode == CM_SINGLELINE) { ConsoleDestY = calcConsoleMinHeight(); } // Rendering of the console can now continue. needResize = false; } return needResize; } void Rend_ConsoleCursorResetBlink() { if(!inited) return; ConsoleBlink = 0; } // Calculate the average of the given color flags. static void calcAvgColor(int fl, float rgb[3]) { DENG_ASSERT(inited && rgb); rgb[CR] = rgb[CG] = rgb[CB] = 0; int count = 0; if(fl & CBLF_BLACK) { ++count; } if(fl & CBLF_BLUE) { rgb[CB] += 1; ++count; } if(fl & CBLF_GREEN) { rgb[CG] += 1; ++count; } if(fl & CBLF_CYAN) { rgb[CG] += 1; rgb[CB] += 1; ++count; } if(fl & CBLF_RED) { rgb[CR] += 1; ++count; } if(fl & CBLF_MAGENTA) { rgb[CR] += 1; rgb[CB] += 1; ++count; } if(fl & CBLF_YELLOW) { rgb[CR] += CcolYellow[0]; rgb[CG] += CcolYellow[1]; rgb[CB] += CcolYellow[2]; ++count; } if(fl & CBLF_WHITE) { rgb[CR] += 1; rgb[CG] += 1; rgb[CB] += 1; ++count; } // Calculate the average. if(count > 1) { rgb[CR] /= count; rgb[CG] /= count; rgb[CB] /= count; } if(fl & CBLF_LIGHT) { rgb[CR] += (1 - rgb[CR]) / 2; rgb[CG] += (1 - rgb[CG]) / 2; rgb[CB] += (1 - rgb[CB]) / 2; } } static void drawRuler(int x, int y, int lineWidth, int lineHeight, float alpha) { DENG_ASSERT(inited); int xoff = 3; int yoff = lineHeight / 4; int rh = MIN_OF(5, lineHeight / 2); Point2Raw origin(x + xoff, y + yoff + (lineHeight - rh) / 2); Size2Raw size(lineWidth - 2 * xoff, rh); UI_GradientEx(&origin, &size, rh / 3, UI_Color(UIC_SHADOW), UI_Color(UIC_BG_DARK), alpha / 2, alpha); UI_DrawRectEx(&origin, &size, -rh / 3, false, UI_Color(UIC_BRD_HI), 0, 0, alpha / 3); } /** * Initializes the Doomsday console user interface. This is called when * engine startup is complete. */ void Rend_ConsoleUpdateTitle() { if(isDedicated || !inited) return; // Update the secondary title and the game status. if(App_GameLoaded()) { dd_snprintf(secondaryTitleText, sizeof(secondaryTitleText)-1, "%s", (char *) gx.GetVariable(DD_PLUGIN_NICENAME)); strncpy(statusText, Str_Text(App_CurrentGame().title()), sizeof(statusText) - 1); return; } // No game currently loaded. std::memset(secondaryTitleText, 0, sizeof(secondaryTitleText)); std::memset(statusText, 0, sizeof(statusText)); } void Rend_ConsoleUpdateBackground() { DENG_ASSERT(inited); if(!consoleBackgroundMaterialUri || Str_IsEmpty(Uri_Path(consoleBackgroundMaterialUri))) return; consoleBackgroundMaterial = 0; try { consoleBackgroundMaterial = &App_Materials().find(*reinterpret_cast(consoleBackgroundMaterialUri)).material(); } catch(MaterialManifest::MissingMaterialError const &) {} // Ignore this error. catch(Materials::NotFoundError const &) {} // Ignore this error. } void Rend_ConsoleToggleFullscreen() { if(isDedicated || !inited) return; if(needResize) { /// @todo enqueue toggle (don't resize here, do it in the ticker). return; } // Cycle to the next mode. consoleMode = consolemode_t(int(consoleMode) + 1); if(consoleMode > CM_SINGLELINE) consoleMode = CM_HALFSCREEN; float y; switch(consoleMode) { case CM_HALFSCREEN: default: y = SCREENHEIGHT/2; break; case CM_FULLSCREEN: y = SCREENHEIGHT; break; case CM_SINGLELINE: y = calcConsoleMinHeight(); break; } ConsoleDestY = ConsoleOpenY = y; } void Rend_ConsoleOpen(int yes) { if(isDedicated || !inited) return; if(yes) { consoleAlphaTarget = 1; ConsoleDestY = ConsoleOpenY; Rend_ConsoleCursorResetBlink(); } else { consoleAlphaTarget = 0; ConsoleDestY = 0; } } void Rend_ConsoleMove(int numLines) { if(isDedicated || !inited) return; moveLineDelta += numLines; if(needResize || moveLineDelta == 0) return; consoleMode = CM_CUSTOM; if(moveLineDelta < 0) { ConsoleOpenY -= fontSy * -moveLineDelta; } else { ConsoleOpenY += fontSy * moveLineDelta; } if(INRANGE_OF(ConsoleOpenY, SCREENHEIGHT/2, 2)) { ConsoleOpenY = SCREENHEIGHT/2; consoleMode = CM_HALFSCREEN; } else if(ConsoleOpenY >= SCREENHEIGHT) { ConsoleOpenY = SCREENHEIGHT; consoleMode = CM_FULLSCREEN; } else { int minHeight = calcConsoleMinHeight(); if(ConsoleOpenY <= minHeight) { ConsoleOpenY = minHeight; consoleMode = CM_SINGLELINE; } } moveLineDelta = 0; ConsoleDestY = ConsoleOpenY; } void Rend_ConsoleTicker(timespan_t time) { if(isDedicated || !inited) return; float step = time * 35; // Move the console alpha to the target. if(consoleAlphaTarget > consoleAlpha) { float diff = MAX_OF(consoleAlphaTarget - consoleAlpha, .0001f) * consoleMoveSpeed; consoleAlpha += diff * step; if(consoleAlpha > consoleAlphaTarget) consoleAlpha = consoleAlphaTarget; } else if(consoleAlphaTarget < consoleAlpha) { float diff = MAX_OF(consoleAlpha - consoleAlphaTarget, .0001f) * consoleMoveSpeed; consoleAlpha -= diff * step; if(consoleAlpha < consoleAlphaTarget) consoleAlpha = consoleAlphaTarget; } if(ConsoleY == 0) openingOrClosing = true; if(!needResize) { // Move the console to the destination Y. if(ConsoleDestY > ConsoleY) { float diff = (ConsoleDestY - ConsoleY) * consoleMoveSpeed; if(diff < 1) diff = 1; ConsoleY += diff * step; if(ConsoleY > ConsoleDestY) ConsoleY = ConsoleDestY; } else if(ConsoleDestY < ConsoleY) { float diff = (ConsoleY - ConsoleDestY) * consoleMoveSpeed; if(diff < 1) diff = 1; ConsoleY -= diff * step; if(ConsoleY < ConsoleDestY) ConsoleY = ConsoleDestY; } } if(ConsoleY == ConsoleOpenY) openingOrClosing = false; if(!Con_IsActive()) return; // We have nothing further to do here. if(consoleBackgroundTurn != 0) funnyAng += step * consoleBackgroundTurn / 10000; ConsoleBlink += step; // Cursor blink timer (0 = visible). } void Rend_ConsoleFPS(Point2Raw const *origin) { DENG_ASSERT(origin); if(isDedicated || !inited) return; if(!consoleShowFPS) return; // Try to fulfill any pending resize. if(Rend_ConsoleResize(false/*no force*/)) return; // No FPS counter for you... char buf[160]; dd_snprintf(buf, 160, "%.1f FPS", DD_GetFrameRate()); FR_SetFont(fontFixed); FR_PushAttrib(); FR_LoadDefaultAttrib(); FR_SetShadowOffset(UI_SHADOW_OFFSET, UI_SHADOW_OFFSET); FR_SetShadowStrength(UI_SHADOW_STRENGTH); Size2Raw size(FR_TextWidth(buf) + 16, FR_SingleLineHeight(buf) + 16); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glEnable(GL_TEXTURE_2D); Point2Raw topLeft(origin->x - size.width, origin->y); UI_GradientEx(&topLeft, &size, 6, UI_Color(UIC_BG_MEDIUM), UI_Color(UIC_BG_LIGHT), .5f, .8f); UI_DrawRectEx(&topLeft, &size, 6, false, UI_Color(UIC_BRD_HI), UI_Color(UIC_BG_MEDIUM), .2f, -1); Point2Raw labelOrigin(origin->x - 8, origin->y + size.height / 2); UI_SetColor(UI_Color(UIC_TEXT)); UI_TextOutEx2(buf, &labelOrigin, UI_Color(UIC_TITLE), 1, ALIGN_RIGHT, DTF_ONLY_SHADOW); FR_PopAttrib(); glDisable(GL_TEXTURE_2D); } static void drawConsoleTitleBar(float alpha) { DENG_ASSERT(inited); if(alpha < .0001f) return; int border = DENG_WINDOW->width() / 120; int barHeight = calcConsoleTitleBarHeight(); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glMatrixMode(GL_PROJECTION); glPushMatrix(); glEnable(GL_TEXTURE_2D); Point2Raw origin(0, 0); Size2Raw size(DENG_WINDOW->width(), barHeight); UI_Gradient(&origin, &size, UI_Color(UIC_BG_MEDIUM), UI_Color(UIC_BG_LIGHT), .95f * alpha, alpha); origin.x = 0; origin.y = barHeight; size.width = DENG_WINDOW->width(); size.height = border; UI_Gradient(&origin, &size, UI_Color(UIC_SHADOW), UI_Color(UIC_BG_DARK), .6f * alpha, 0); origin.x = 0; origin.y = barHeight; size.width = DENG_WINDOW->width(); size.height = border*2; UI_Gradient(&origin, &size, UI_Color(UIC_BG_DARK), UI_Color(UIC_SHADOW), .2f * alpha, 0); FR_SetFont(fontVariable[FS_BOLD]); FR_PushAttrib(); FR_LoadDefaultAttrib(); FR_SetShadowOffset(UI_SHADOW_OFFSET, UI_SHADOW_OFFSET); FR_SetShadowStrength(UI_SHADOW_STRENGTH); origin.x = border; origin.y = barHeight / 2; UI_TextOutEx2(consoleTitle, &origin, UI_Color(UIC_TITLE), alpha, ALIGN_LEFT, DTF_ONLY_SHADOW); if(secondaryTitleText[0]) { int width = FR_TextWidth(consoleTitle) + FR_TextWidth(" "); FR_SetFont(fontVariable[FS_LIGHT]); origin.x = border + width; origin.y = barHeight / 2; UI_TextOutEx2(secondaryTitleText, &origin, UI_Color(UIC_TEXT), .33f * alpha, ALIGN_LEFT, DTF_ONLY_SHADOW); } if(statusText[0]) { FR_SetFont(fontVariable[FS_LIGHT]); origin.x = DENG_WINDOW->width() - border; origin.y = barHeight / 2; UI_TextOutEx2(statusText, &origin, UI_Color(UIC_TEXT), .75f * alpha, ALIGN_RIGHT, DTF_ONLY_SHADOW); } FR_PopAttrib(); glDisable(GL_TEXTURE_2D); glMatrixMode(GL_PROJECTION); glPopMatrix(); } static void drawConsoleBackground(Point2Raw const *origin, Size2Raw const *size, float closeFade) { DENG_ASSERT(inited); int bgX = 0, bgY = 0; if(consoleBackgroundMaterial) { MaterialVariantSpec const &spec = App_Materials().variantSpec(UiContext, 0, 0, 0, 0, GL_REPEAT, GL_REPEAT, 0, 1, 0, false, false, false, false); MaterialSnapshot const &ms = consoleBackgroundMaterial->prepare(spec); GL_BindTexture(&ms.texture(MTU_PRIMARY)); bgX = int(ms.width() * consoleBackgroundZoom); bgY = int(ms.height() * consoleBackgroundZoom); glEnable(GL_TEXTURE_2D); if(consoleBackgroundTurn != 0) { glMatrixMode(GL_TEXTURE); glPushMatrix(); glLoadIdentity(); glTranslatef(2 * sin(funnyAng / 4), 2 * cos(funnyAng / 4), 0); glRotatef(funnyAng * 3, 0, 0, 1); } } glColor4f(consoleBackgroundLight, consoleBackgroundLight, consoleBackgroundLight, closeFade * consoleBackgroundAlpha); GL_DrawRectf2Tiled(origin->x, origin->y, size->width, size->height, bgX, bgY); if(consoleBackgroundMaterial) { if(consoleBackgroundTurn != 0) { glMatrixMode(GL_TEXTURE); glPopMatrix(); } glDisable(GL_TEXTURE_2D); } } /** * Draw a 'side' text in the console. This is intended for extra * information about the current game mode. */ #if 0 static void drawSideText(char const *text, int line, float alpha) { DENG_ASSERT(inited); float const gtosMulY = DENG_WINDOW->height() / 200.0f; FR_SetFont(Con_Font()); FR_PushAttrib(); FR_LoadDefaultAttrib(); float scale[2]; Con_FontScale(&scale[0], &scale[1]); float fontScaledY = FR_SingleLineHeight("Con") * scale[1]; float y = ConsoleY * gtosMulY - fontScaledY * (1 + line); if(y > -fontScaledY) { con_textfilter_t printFilter = Con_PrintFilter(); // Scaled screen width. int ssw = int(DENG_WINDOW->width() / scale[0]); char buf[300]; if(printFilter) { strncpy(buf, text, sizeof(buf)); printFilter(buf); text = buf; } FR_SetColorAndAlpha(CcolYellow[0], CcolYellow[1], CcolYellow[2], alpha * .75f); FR_DrawTextXY3(text, ssw - 3, y / scale[1], ALIGN_TOPRIGHT, DTF_NO_TYPEIN|DTF_NO_GLITTER|(!consoleTextShadow?DTF_NO_SHADOW:0)); } FR_PopAttrib(); } #endif static void escapeFormatting(ddstring_t *dest, char const *src, int maxSourceLen) { if(!src) return; Str_Clear(dest); for(int i = 0; *src; ++src, ++i) { if(maxSourceLen && i == maxSourceLen) break; if(*src == '{') { Str_AppendChar(dest, FR_FORMAT_ESCAPE_CHAR); } Str_AppendChar(dest, *src); } } static void applyFilter(char *buff) { con_textfilter_t printFilter = Con_PrintFilter(); ddstring_t *escaped = Str_New(); escapeFormatting(escaped, buff, 0); strcpy(buff, Str_Text(escaped)); Str_Delete(escaped); if(printFilter) { printFilter(buff); } } /** * @note Slightly messy... */ static void drawConsole(float consoleAlpha) { DENG_ASSERT(inited); int const XORIGIN = 0; int const YORIGIN = 0; int const PADDING = 2; int const LOCALBUFFSIZE = (CMDLINE_SIZE +1/*prompt length*/ +1/*terminator*/); static cbline_t const **lines = 0; static int bufferSize = 0; CBuffer *buffer = Con_HistoryBuffer(); uint cmdCursor = Con_CommandLineCursorPosition(); char *cmdLine = Con_CommandLine(); float scale[2], y, fontScaledY, gtosMulY = DENG_WINDOW->height() / 200.0f; char buff[LOCALBUFFSIZE]; font_t *cfont; int lineHeight, textOffsetY; uint reqLines, maxLineLength; Point2Raw origin; Size2Raw size; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); FR_SetFont(Con_Font()); FR_PushAttrib(); FR_LoadDefaultAttrib(); FR_SetTracking(Con_FontTracking()); FR_SetColorAndAlpha(1, 1, 1, consoleAlpha); cfont = Fonts_ToFont(FR_Font()); lineHeight = FR_SingleLineHeight("Con"); Con_FontScale(&scale[0], &scale[1]); fontScaledY = lineHeight * Con_FontLeading() * scale[1]; textOffsetY = PADDING + fontScaledY / 4; origin.x = XORIGIN; origin.y = YORIGIN + (int) (ConsoleY * gtosMulY); size.width = DENG_WINDOW->width(); size.height = -DENG_WINDOW->height(); drawConsoleBackground(&origin, &size, consoleAlpha); // The border. origin.x = XORIGIN; origin.y = YORIGIN + (int) ((ConsoleY - 10) * gtosMulY); size.width = DENG_WINDOW->width(); size.height = 10 * gtosMulY; UI_Gradient(&origin, &size, UI_Color(UIC_BG_DARK), UI_Color(UIC_BRD_HI), 0, consoleAlpha * consoleBackgroundAlpha * .06f); origin.x = XORIGIN; origin.y = YORIGIN + (int) (ConsoleY * gtosMulY); size.width = DENG_WINDOW->width(); size.height = 2; UI_Gradient(&origin, &size, UI_Color(UIC_BG_LIGHT), UI_Color(UIC_BG_LIGHT), consoleAlpha * consoleBackgroundAlpha, -1); origin.x = XORIGIN; origin.y = YORIGIN + (int) (ConsoleY * gtosMulY); size.width = DENG_WINDOW->width(); size.height = 2 * gtosMulY; UI_Gradient(&origin, &size, UI_Color(UIC_SHADOW), UI_Color(UIC_SHADOW), consoleAlpha * consoleBackgroundAlpha * .75f, 0); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glScalef(scale[0], scale[1], 1); // The console history log is drawn from bottom to top. y = ConsoleY * gtosMulY - (lineHeight * scale[1] + fontScaledY) - textOffsetY; reqLines = de::max(0, de::ceil(y / fontScaledY) + 1); if(reqLines != 0) { uint count, totalLines = CBuffer_NumLines(buffer); int firstIdx; firstIdx = -((long)(reqLines + Con_HistoryOffset())); if(firstIdx < -((long)totalLines)) firstIdx = -((long)totalLines); // Need to enlarge the buffer? if(reqLines > (uint) bufferSize) { lines = (cbline_t const **) Z_Realloc((void *) lines, sizeof(cbline_t *) * (reqLines + 1), PU_APPSTATIC); bufferSize = reqLines; } count = CBuffer_GetLines2(buffer, reqLines, firstIdx, lines, BLF_OMIT_EMPTYLINE); if(count != 0) { glEnable(GL_TEXTURE_2D); for(uint i = count; i-- > 0;) { cbline_t const *line = lines[i]; if(line->flags & CBLF_RULER) { // Draw a ruler here, and nothing else. drawRuler(XORIGIN + PADDING, (YORIGIN + y) / scale[1], DENG_WINDOW->width() / scale[0] - PADDING*2, lineHeight, consoleAlpha); } else { int alignFlags = 0; short textFlags = DTF_NO_TYPEIN|DTF_NO_GLITTER|(!consoleTextShadow?DTF_NO_SHADOW:0); float xOffset; std::memset(buff, 0, sizeof(buff)); strncpy(buff, line->text, LOCALBUFFSIZE-1); if(line->flags & CBLF_CENTER) { alignFlags |= ALIGN_TOP; xOffset = (DENG_WINDOW->width() / scale[0]) / 2; } else { alignFlags |= ALIGN_TOPLEFT; xOffset = 0; } // Escape any visual formatting characters in the text. applyFilter(buff); // Set the color. if(Font_Flags(cfont) & FF_COLORIZE) { float rgb[3]; calcAvgColor(line->flags, rgb); FR_SetColorv(rgb); } FR_DrawTextXY3(buff, XORIGIN + PADDING + xOffset, YORIGIN + y / scale[1], alignFlags, textFlags); } // Move up. y -= fontScaledY; } glDisable(GL_TEXTURE_2D); } } // The command line. boolean abbrevLeft = 0, abbrevRight = 0; int offset = 0; uint cmdLineLength; y = ConsoleY * gtosMulY - (lineHeight * scale[1]) - textOffsetY; cmdLineLength = (uint)strlen(cmdLine); maxLineLength = CBuffer_MaxLineLength(buffer) - 1/*prompt length*/; if(cmdLineLength >= maxLineLength) { maxLineLength -= 5; /*abbrev vis length*/ if((signed)cmdCursor - (signed)maxLineLength > 0 || cmdCursor > maxLineLength) { abbrevLeft = true; maxLineLength -= 5; /*abbrev vis length*/ } offset = MAX_OF(0, (signed)cmdCursor - (signed)maxLineLength); abbrevRight = (offset + maxLineLength < cmdLineLength); if(!abbrevRight) { maxLineLength += 5; /*abbrev vis length*/ offset = MAX_OF(0, (signed)cmdCursor - (signed)maxLineLength); } } // Apply filtering. /// @todo Clean this up; use a common applyFilter() function. ddstring_t *escaped = Str_New(); escapeFormatting(escaped, cmdLine + offset, maxLineLength); dd_snprintf(buff, LOCALBUFFSIZE - 1/*terminator*/, ">%s%s%s", abbrevLeft? "{alpha=.5}[...]{alpha=1}" : "", Str_Text(escaped), abbrevRight? "{alpha=.5}[...]" : ""); Str_Delete(escaped); if(Con_PrintFilter()) (Con_PrintFilter())(buff); glEnable(GL_TEXTURE_2D); if(Font_Flags(cfont) & FF_COLORIZE) { FR_SetColorAndAlpha(CcolYellow[0], CcolYellow[1], CcolYellow[2], consoleAlpha); } else { FR_SetColorAndAlpha(1, 1, 1, consoleAlpha); } FR_DrawTextXY3(buff, XORIGIN + PADDING, YORIGIN + y / scale[1], ALIGN_TOPLEFT, DTF_NO_TYPEIN|DTF_NO_GLITTER|(!consoleTextShadow?DTF_NO_SHADOW:0)); glDisable(GL_TEXTURE_2D); // Draw the cursor in the appropriate place. if(Con_IsActive() && !Con_IsLocked()) { float width, height, halfInterlineHeight = (lineHeight * scale[1]) / 8.f; int xOffset, yOffset = 2 * scale[1]; char temp[LOCALBUFFSIZE]; // Where is the cursor? std::memset(temp, 0, sizeof(temp)); //strncpy(temp, cmdLine + offset, MIN_OF(LOCALBUFFSIZE -1/*prompt length*/ /*-1*//*vis clamp*/, cmdCursor-offset + (abbrevLeft? 24/*abbrev length*/:0) + 1)); strcpy(temp, ">"); if(abbrevLeft) strcat(temp, "[...]"); strncat(temp, cmdLine + offset, MIN_OF(LOCALBUFFSIZE - 1, cmdCursor - offset)); applyFilter(temp); xOffset = FR_TextWidth(temp); if(Con_InputMode()) { height = lineHeight * scale[1]; yOffset += halfInterlineHeight; } else { height = halfInterlineHeight; yOffset += lineHeight * scale[1]; } // Size of the current character. width = FR_CharWidth(cmdLine[cmdCursor] == '\0'? ' ' : cmdLine[cmdCursor]); glColor4f(CcolYellow[0], CcolYellow[1], CcolYellow[2], consoleAlpha * (((int) ConsoleBlink) & 0x10 ? .2f : .5f)); GL_DrawRectf2(XORIGIN + PADDING + xOffset, (int)((YORIGIN + y + yOffset) / scale[1]), (int)width, MAX_OF(1, (int)(height / scale[1]))); } FR_PopAttrib(); // Restore the original matrices. glMatrixMode(GL_MODELVIEW); glPopMatrix(); } void Rend_Console() { boolean consoleShow; if(isDedicated || !inited) return; // Try to fulfill any pending resize. if(Rend_ConsoleResize(false/*no force*/)) return; // No console on this frame at least... consoleShow = (ConsoleY > 0);// || openingOrClosing); if(!consoleShow && !consoleShowFPS) return; // Go into screen projection mode. glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, DENG_WINDOW->width(), DENG_WINDOW->height(), 0, -1, 1); if(consoleShow) { drawConsole(consoleAlpha); drawConsoleTitleBar(consoleAlpha); } if(consoleShowFPS && !UI_IsActive()) { Point2Raw origin(DENG_WINDOW->width() - 10, 10 + (ConsoleY > 0? ROUND(consoleAlpha * calcConsoleTitleBarHeight()) : 0)); Rend_ConsoleFPS(&origin); } // Restore original matrix. glMatrixMode(GL_PROJECTION); glPopMatrix(); } #endif doomsday-stable-1.15.7/doomsday/client/src/render/rend_main.cpp0000664000175000017500000060670312641367670024013 0ustar jaakkojaakko/** @file rend_main.cpp World Map Renderer. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "render/rend_main.h" #include "de_console.h" #include "de_render.h" #include "de_resource.h" #include "de_graphics.h" #include "de_ui.h" #include #include #include #include #include //#include #include #include #include #include #include "clientapp.h" #include "sys_system.h" #include "api_fontrender.h" #include "edit_bias.h" /// @todo remove me //#include "network/net_main.h" /// @todo remove me #include "MaterialVariantSpec" #include "Texture" #include "Face" #include "world/map.h" #include "world/blockmap.h" #include "world/lineowner.h" #include "world/p_object.h" #include "world/p_players.h" #include "world/sky.h" #include "world/thinkers.h" #include "BspLeaf" #include "BspNode" #include "Contact" #include "ConvexSubspace" #include "Hand" #include "SectorCluster" #include "Surface" #include "BiasIllum" #include "HueCircleVisual" #include "LightDecoration" #include "Lumobj" #include "Shard" #include "SkyFixEdge" #include "SurfaceDecorator" #include "TriangleStripBuilder" #include "WallEdge" #include "gl/gl_texmanager.h" #include "gl/sys_opengl.h" #include "render/fx/bloom.h" #include "render/fx/vignette.h" #include "render/fx/lensflares.h" #include "render/rend_particle.h" #include "render/angleclipper.h" #include "render/blockmapvisual.h" #include "render/billboard.h" #include "render/vissprite.h" #include "render/skydrawable.h" #include "render/vr.h" #include "ui/editors/rendererappearanceeditor.h" using namespace de; // Surface (tangent-space) Vector Flags. #define SVF_TANGENT 0x01 #define SVF_BITANGENT 0x02 #define SVF_NORMAL 0x04 /** * @defgroup soundOriginFlags Sound Origin Flags * Flags for use with the sound origin debug display. * @ingroup flags */ ///@{ #define SOF_SECTOR 0x01 #define SOF_PLANE 0x02 #define SOF_SIDE 0x04 ///@} void Rend_DrawBBox(Vector3d const &pos, coord_t w, coord_t l, coord_t h, dfloat a, dfloat const color[3], dfloat alpha, dfloat br, bool alignToBase = true); void Rend_DrawArrow(Vector3d const &pos, dfloat a, dfloat s, dfloat const color3f[3], dfloat alpha); D_CMD(OpenRendererAppearanceEditor); D_CMD(LowRes); D_CMD(MipMap); D_CMD(TexReset); dint useBias; ///< Shadow Bias enabled? cvar dd_bool usingFog; ///< Is the fog in use? dfloat fogColor[4]; dfloat fieldOfView = 95.0f; dbyte smoothTexAnim = true; dint renderTextures = true; dint renderWireframe; dint useMultiTexLights = true; dint useMultiTexDetails = true; dint dynlightBlend; Vector3f torchColor(1, 1, 1); dint torchAdditive = true; dint useShinySurfaces = true; dint useDynLights = true; dfloat dynlightFactor = .5f; dfloat dynlightFogBright = .15f; dint useGlowOnWalls = true; dfloat glowFactor = .8f; dfloat glowHeightFactor = 3; ///< Glow height as a multiplier. dint glowHeightMax = 100; ///< 100 is the default (0-1024). dint useShadows = true; dfloat shadowFactor = 1.2f; dint shadowMaxRadius = 80; dint shadowMaxDistance = 1000; dbyte useLightDecorations = true; ///< cvar dfloat detailFactor = .5f; dfloat detailScale = 4; dint mipmapping = 5; dint filterUI = 1; dint texQuality = TEXQ_BEST; dint ratioLimit; ///< Zero if none. dd_bool fillOutlines = true; dint useSmartFilter; ///< Smart filter mode (cvar: 1=hq2x) dint filterSprites = true; dint texMagMode = 1; ///< Linear. dint texAniso = -1; ///< Use best. dd_bool noHighResTex; dd_bool noHighResPatches; dd_bool highResWithPWAD; dbyte loadExtAlways; ///< Always check for extres (cvar) dfloat texGamma; dint glmode[6] = // Indexed by 'mipmapping'. { GL_NEAREST, GL_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_LINEAR }; Vector3d vOrigin; dfloat vang, vpitch; dfloat viewsidex, viewsidey; dbyte freezeRLs; dint devNoCulling; ///< @c 1= disabled (cvar). dint devRendSkyMode; dbyte devRendSkyAlways; // Ambient lighting, rAmbient is used within the renderer, ambientLight is // used to store the value of the ambient light cvar. // The value chosen for rAmbient occurs in Rend_UpdateLightModMatrix // for convenience (since we would have to recalculate the matrix anyway). dint rAmbient, ambientLight; dint viewpw, viewph; ///< Viewport size, in pixels. dint viewpx, viewpy; ///< Viewpoint top left corner, in pixels. dfloat yfov; dint gameDrawHUD = 1; ///< Set to zero when we advise that the HUD should not be drawn /** * Implements a pre-calculated LUT for light level limiting and range * compression offsets, arranged such that it may be indexed with a * light level value. Return value is an appropriate delta (considering * all applicable renderer properties) which has been pre-clamped such * that when summed with the original light value the result remains in * the normalized range [0..1]. */ dfloat lightRangeCompression; dfloat lightModRange[255]; byte devLightModRange; dfloat rendLightDistanceAttenuation = 924; dint rendLightAttenuateFixedColormap = 1; dfloat rendLightWallAngle = 1.2f; ///< Intensity of angle-based wall lighting. dbyte rendLightWallAngleSmooth = true; dfloat rendSkyLight = .273f; ///< Intensity factor. dbyte rendSkyLightAuto = true; dint rendMaxLumobjs; ///< Max lumobjs per viewer, per frame. @c 0= no maximum. dint extraLight; ///< Bumped light from gun blasts. dfloat extraLightDelta; DGLuint dlBBox; ///< Display list id for the active-textured bbox model. /* * Debug/Development cvars: */ dbyte devMobjVLights; ///< @c 1= Draw mobj vertex lighting vector. dint devMobjBBox; ///< @c 1= Draw mobj bounding boxes. dint devPolyobjBBox; ///< @c 1= Draw polyobj bounding boxes. dbyte devVertexIndices; ///< @c 1= Draw vertex indices. dbyte devVertexBars; ///< @c 1= Draw vertex position bars. dbyte devDrawGenerators; ///< @c 1= Draw active generators. dbyte devSoundEmitters; ///< @c 1= Draw sound emitters. dbyte devSurfaceVectors; ///< @c 1= Draw tangent space vectors for surfaces. dbyte devNoTexFix; ///< @c 1= Draw "missing" rather than fix materials. dbyte devSectorIndices; ///< @c 1= Draw sector indicies. dbyte devThinkerIds; ///< @c 1= Draw (mobj) thinker indicies. dbyte rendInfoLums; ///< @c 1= Print lumobj debug info to the console. dbyte devDrawLums; ///< @c 1= Draw lumobjs origins. dbyte devLightGrid; ///< @c 1= Draw lightgrid debug visual. dfloat devLightGridSize = 1.5f; ///< Lightgrid debug visual size factor. static void drawMobjBoundingBoxes(Map &map); static void drawSoundEmitters(Map &map); static void drawGenerators(Map &map); static void drawAllSurfaceTangentVectors(Map &map); static void drawBiasEditingVisuals(Map &map); static void drawLumobjs(Map &map); static void drawSectors(Map &map); static void drawThinkers(Map &map); static void drawVertexes(Map &map); // Draw state: static Vector3d eyeOrigin; ///< Viewer origin. static ConvexSubspace *curSubspace; ///< Subspace currently being drawn. static Vector3f curSectorLightColor; static dfloat curSectorLightLevel; static bool firstSubspace; ///< No range checking for the first one. static inline RenderSystem &rendSys() { return ClientApp::renderSystem(); } static inline ResourceSystem &resSys() { return ClientApp::resourceSystem(); } static inline WorldSystem &worldSys() { return ClientApp::worldSystem(); } static void scheduleFullLightGridUpdate() { if(App_WorldSystem().hasMap()) { Map &map = App_WorldSystem().map(); if(map.hasLightGrid()) map.lightGrid().scheduleFullUpdate(); } } static void unlinkMobjLumobjs() { if(!worldSys().hasMap()) return; worldSys().map().thinkers() .forAll(reinterpret_cast(gx.MobjThinker), 0x1, [] (thinker_t *th) { Mobj_UnlinkLumobjs(reinterpret_cast(th)); return LoopContinue; }); } /* static void fieldOfViewChanged() { if(vrCfg().mode() == VRConfig::OculusRift) { if(Con_GetFloat("rend-vr-rift-fovx") != fieldOfView) Con_SetFloat("rend-vr-rift-fovx", fieldOfView); } else { if(Con_GetFloat("rend-vr-nonrift-fovx") != fieldOfView) Con_SetFloat("rend-vr-nonrift-fovx", fieldOfView); } }*/ static void detailFactorChanged() { App_ResourceSystem().releaseGLTexturesByScheme("Details"); } static void loadExtAlwaysChanged() { GL_TexReset(); } static void useSmartFilterChanged() { GL_TexReset(); } static void texGammaChanged() { R_BuildTexGammaLut(); GL_TexReset(); LOG_GL_MSG("Texture gamma correction set to %f") << texGamma; } static void mipmappingChanged() { GL_TexReset(); } static void texQualityChanged() { GL_TexReset(); } void Rend_Register() { C_VAR_INT ("rend-bias", &useBias, 0, 0, 1); C_VAR_FLOAT ("rend-camera-fov", &fieldOfView, 0, 1, 179); C_VAR_FLOAT ("rend-glow", &glowFactor, 0, 0, 2); C_VAR_INT ("rend-glow-height", &glowHeightMax, 0, 0, 1024); C_VAR_FLOAT ("rend-glow-scale", &glowHeightFactor, 0, 0.1f, 10); C_VAR_INT ("rend-glow-wall", &useGlowOnWalls, 0, 0, 1); C_VAR_BYTE ("rend-info-lums", &rendInfoLums, 0, 0, 1); C_VAR_INT2 ("rend-light", &useDynLights, 0, 0, 1, unlinkMobjLumobjs); C_VAR_INT2 ("rend-light-ambient", &ambientLight, 0, 0, 255, Rend_UpdateLightModMatrix); C_VAR_FLOAT ("rend-light-attenuation", &rendLightDistanceAttenuation, CVF_NO_MAX, 0, 0); C_VAR_INT ("rend-light-blend", &dynlightBlend, 0, 0, 2); C_VAR_FLOAT ("rend-light-bright", &dynlightFactor, 0, 0, 1); C_VAR_FLOAT2("rend-light-compression", &lightRangeCompression, 0, -1, 1, Rend_UpdateLightModMatrix); C_VAR_BYTE ("rend-light-decor", &useLightDecorations, 0, 0, 1); C_VAR_FLOAT ("rend-light-fog-bright", &dynlightFogBright, 0, 0, 1); C_VAR_INT ("rend-light-multitex", &useMultiTexLights, 0, 0, 1); C_VAR_INT ("rend-light-num", &rendMaxLumobjs, CVF_NO_MAX, 0, 0); C_VAR_FLOAT2("rend-light-sky", &rendSkyLight, 0, 0, 1, scheduleFullLightGridUpdate); C_VAR_BYTE2 ("rend-light-sky-auto", &rendSkyLightAuto, 0, 0, 1, scheduleFullLightGridUpdate); C_VAR_FLOAT ("rend-light-wall-angle", &rendLightWallAngle, CVF_NO_MAX, 0, 0); C_VAR_BYTE ("rend-light-wall-angle-smooth", &rendLightWallAngleSmooth, 0, 0, 1); C_VAR_BYTE ("rend-map-material-precache", &precacheMapMaterials, 0, 0, 1); C_VAR_INT ("rend-shadow", &useShadows, 0, 0, 1); C_VAR_FLOAT ("rend-shadow-darkness", &shadowFactor, 0, 0, 2); C_VAR_INT ("rend-shadow-far", &shadowMaxDistance, CVF_NO_MAX, 0, 0); C_VAR_INT ("rend-shadow-radius-max", &shadowMaxRadius, CVF_NO_MAX, 0, 0); C_VAR_INT ("rend-tex", &renderTextures, CVF_NO_ARCHIVE, 0, 2); C_VAR_BYTE ("rend-tex-anim-smooth", &smoothTexAnim, 0, 0, 1); C_VAR_INT ("rend-tex-detail", &r_detail, 0, 0, 1); C_VAR_INT ("rend-tex-detail-multitex", &useMultiTexDetails, 0, 0, 1); C_VAR_FLOAT ("rend-tex-detail-scale", &detailScale, CVF_NO_MIN | CVF_NO_MAX, 0, 0); C_VAR_FLOAT2("rend-tex-detail-strength", &detailFactor, 0, 0, 5, detailFactorChanged); C_VAR_BYTE2 ("rend-tex-external-always", &loadExtAlways, 0, 0, 1, loadExtAlwaysChanged); C_VAR_INT ("rend-tex-filter-anisotropic", &texAniso, 0, -1, 4); C_VAR_INT ("rend-tex-filter-mag", &texMagMode, 0, 0, 1); C_VAR_INT2 ("rend-tex-filter-smart", &useSmartFilter, 0, 0, 1, useSmartFilterChanged); C_VAR_INT ("rend-tex-filter-sprite", &filterSprites, 0, 0, 1); C_VAR_INT ("rend-tex-filter-ui", &filterUI, 0, 0, 1); C_VAR_FLOAT2("rend-tex-gamma", &texGamma, 0, 0, 1, texGammaChanged); C_VAR_INT2 ("rend-tex-mipmap", &mipmapping, CVF_PROTECTED, 0, 5, mipmappingChanged); C_VAR_INT2 ("rend-tex-quality", &texQuality, 0, 0, 8, texQualityChanged); C_VAR_INT ("rend-tex-shiny", &useShinySurfaces, 0, 0, 1); C_VAR_BYTE ("rend-bias-grid-debug", &devLightGrid, CVF_NO_ARCHIVE, 0, 1); C_VAR_FLOAT ("rend-bias-grid-debug-size", &devLightGridSize, 0, .1f, 100); C_VAR_BYTE ("rend-dev-blockmap-debug", &bmapShowDebug, CVF_NO_ARCHIVE, 0, 4); C_VAR_FLOAT ("rend-dev-blockmap-debug-size", &bmapDebugSize, CVF_NO_ARCHIVE, .1f, 100); C_VAR_INT ("rend-dev-cull-leafs", &devNoCulling, CVF_NO_ARCHIVE, 0, 1); C_VAR_BYTE ("rend-dev-freeze", &freezeRLs, CVF_NO_ARCHIVE, 0, 1); C_VAR_BYTE ("rend-dev-generator-show-indices", &devDrawGenerators, CVF_NO_ARCHIVE, 0, 1); C_VAR_BYTE ("rend-dev-light-mod", &devLightModRange, CVF_NO_ARCHIVE, 0, 1); C_VAR_BYTE ("rend-dev-lums", &devDrawLums, CVF_NO_ARCHIVE, 0, 1); C_VAR_INT ("rend-dev-mobj-bbox", &devMobjBBox, CVF_NO_ARCHIVE, 0, 1); C_VAR_BYTE ("rend-dev-mobj-show-vlights", &devMobjVLights, CVF_NO_ARCHIVE, 0, 1); C_VAR_INT ("rend-dev-polyobj-bbox", &devPolyobjBBox, CVF_NO_ARCHIVE, 0, 1); C_VAR_BYTE ("rend-dev-sector-show-indices", &devSectorIndices, CVF_NO_ARCHIVE, 0, 1); C_VAR_INT ("rend-dev-sky", &devRendSkyMode, CVF_NO_ARCHIVE, 0, 1); C_VAR_BYTE ("rend-dev-sky-always", &devRendSkyAlways, CVF_NO_ARCHIVE, 0, 1); C_VAR_BYTE ("rend-dev-soundorigins", &devSoundEmitters, CVF_NO_ARCHIVE, 0, 7); C_VAR_BYTE ("rend-dev-surface-show-vectors", &devSurfaceVectors, CVF_NO_ARCHIVE, 0, 7); C_VAR_BYTE ("rend-dev-thinker-ids", &devThinkerIds, CVF_NO_ARCHIVE, 0, 1); C_VAR_BYTE ("rend-dev-tex-showfix", &devNoTexFix, CVF_NO_ARCHIVE, 0, 1); C_VAR_BYTE ("rend-dev-vertex-show-bars", &devVertexBars, CVF_NO_ARCHIVE, 0, 1); C_VAR_BYTE ("rend-dev-vertex-show-indices", &devVertexIndices, CVF_NO_ARCHIVE, 0, 1); C_CMD ("rendedit", "", OpenRendererAppearanceEditor); C_CMD_FLAGS ("lowres", "", LowRes, CMDF_NO_DEDICATED); C_CMD_FLAGS ("mipmap", "i", MipMap, CMDF_NO_DEDICATED); C_CMD_FLAGS ("texreset", "", TexReset, CMDF_NO_DEDICATED); C_CMD_FLAGS ("texreset", "s", TexReset, CMDF_NO_DEDICATED); BiasIllum::consoleRegister(); LightDecoration::consoleRegister(); LightGrid::consoleRegister(); Lumobj::consoleRegister(); SkyDrawable::consoleRegister(); Rend_ModelRegister(); Rend_ParticleRegister(); Generator::consoleRegister(); Rend_RadioRegister(); Rend_SpriteRegister(); LensFx_Register(); fx::Bloom::consoleRegister(); fx::Vignette::consoleRegister(); fx::LensFlares::consoleRegister(); Shard::consoleRegister(); VR_ConsoleRegister(); } static void reportWallSectionDrawn(Line &line) { // Already been here? dint playerNum = viewPlayer - ddPlayers; if(line.isMappedByPlayer(playerNum)) return; // Mark as drawn. line.markMappedByPlayer(playerNum); // Send a status report. if(gx.HandleMapObjectStatusReport) { gx.HandleMapObjectStatusReport(DMUSC_LINE_FIRSTRENDERED, line.indexInMap(), DMU_LINE, &playerNum); } } /// World/map renderer reset. void Rend_Reset() { R_ClearViewData(); if(App_WorldSystem().hasMap()) { App_WorldSystem().map().removeAllLumobjs(); } if(dlBBox) { GL_DeleteLists(dlBBox, 1); dlBBox = 0; } } bool Rend_IsMTexLights() { return IS_MTEX_LIGHTS; } bool Rend_IsMTexDetails() { return IS_MTEX_DETAILS; } dfloat Rend_FieldOfView() { if(vrCfg().mode() == VRConfig::OculusRift) { // OVR tells us which FOV to use. return vrCfg().oculusRift().fovX(); } else { // Correction is applied for wide screens so that when the FOV is kept // at a certain value (e.g., the default FOV), a 16:9 view has a wider angle // than a 4:3, but not just scaled linearly since that would go too far // into the fish eye territory. dfloat widescreenCorrection = dfloat(viewpw) / dfloat(viewph) / (4.f / 3.f); if(widescreenCorrection < 1.5) // up to ~16:9 { widescreenCorrection = (1 + 2 * widescreenCorrection) / 3; return de::clamp(1.f, widescreenCorrection * fieldOfView, 179.f); } // This is an unusually wide (perhaps multimonitor) setup, so just use the // configured FOV as is. return de::clamp(1.f, fieldOfView, 179.f); } } static Vector3d vEyeOrigin; Vector3d Rend_EyeOrigin() { return vEyeOrigin; } Matrix4f Rend_GetModelViewMatrix(dint consoleNum, bool inWorldSpace) { viewdata_t const *viewData = R_ViewData(consoleNum); dfloat bodyAngle = viewData->current.angleWithoutHeadTracking() / (dfloat) ANGLE_MAX * 360 - 90; /// @todo vOrigin et al. shouldn't be changed in a getter function. -jk vOrigin = viewData->current.origin.xzy(); vang = viewData->current.angle() / (dfloat) ANGLE_MAX * 360 - 90; // head tracking included vpitch = viewData->current.pitch * 85.0 / 110.0; vEyeOrigin = vOrigin; OculusRift &ovr = vrCfg().oculusRift(); bool const applyHead = (vrCfg().mode() == VRConfig::OculusRift && ovr.isReady()); Matrix4f modelView; Matrix4f headOrientation; Matrix4f headOffset; if(applyHead) { Vector3f headPos = swizzle(Matrix4f::rotate(bodyAngle, Vector3f(0, 1, 0)) * ovr.headPosition() * vrCfg().mapUnitsPerMeter(), AxisNegX, AxisNegY, AxisZ); headOffset = Matrix4f::translate(headPos); vEyeOrigin -= headPos; } if(inWorldSpace) { dfloat yaw = vang; dfloat pitch = vpitch; dfloat roll = 0; /// @todo Elevate roll angle use into viewer_t, and maybe all the way up into player /// model. // Pitch and yaw can be taken directly from the head tracker, as the game is aware of // these values and is syncing with them independently (however, game has more // latency). if(applyHead) { // Use angles directly from the Rift for best response. Vector3f const pry = ovr.headOrientation(); roll = -radianToDegree(pry[1]); pitch = radianToDegree(pry[0]); } headOrientation = Matrix4f::rotate(roll, Vector3f(0, 0, 1)) * Matrix4f::rotate(pitch, Vector3f(1, 0, 0)) * Matrix4f::rotate(yaw, Vector3f(0, 1, 0)); modelView = headOrientation * headOffset; } if(applyHead) { // Apply the current eye offset to the eye origin. vEyeOrigin -= headOrientation.inverse() * (ovr.eyeOffset() * vrCfg().mapUnitsPerMeter()); } return (modelView * Matrix4f::scale(Vector3f(1.0f, 1.2f, 1.0f)) * // This is the aspect correction. Matrix4f::translate(-vOrigin)); } void Rend_ModelViewMatrix(bool inWorldSpace) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glMatrixMode(GL_MODELVIEW); glLoadMatrixf(Rend_GetModelViewMatrix(viewPlayer - ddPlayers, inWorldSpace).values()); } static inline ddouble viewFacingDot(Vector2d const &v1, Vector2d const &v2) { // The dot product. return (v1.y - v2.y) * (v1.x - Rend_EyeOrigin().x) + (v2.x - v1.x) * (v1.y - Rend_EyeOrigin().z); } dfloat Rend_ExtraLightDelta() { return extraLightDelta; } void Rend_ApplyTorchLight(Vector4f &color, dfloat distance) { ddplayer_t *ddpl = &viewPlayer->shared; // Disabled? if(!ddpl->fixedColorMap) return; // Check for torch. if(!rendLightAttenuateFixedColormap || distance < 1024) { // Colormap 1 is the brightest. I'm guessing 16 would be // the darkest. dfloat d = (16 - ddpl->fixedColorMap) / 15.0f; if(rendLightAttenuateFixedColormap) { d *= (1024 - distance) / 1024.0f; } if(torchAdditive) { color += torchColor * d; } else { color += ((color * torchColor) - color) * d; } } } void Rend_ApplyTorchLight(dfloat *color3, dfloat distance) { Vector4f tmp(color3, 0); Rend_ApplyTorchLight(tmp, distance); for(dint i = 0; i < 3; ++i) { color3[i] = tmp[i]; } } dfloat Rend_AttenuateLightLevel(dfloat distToViewer, dfloat lightLevel) { if(distToViewer > 0 && rendLightDistanceAttenuation > 0) { dfloat real = lightLevel - (distToViewer - 32) / rendLightDistanceAttenuation * (1 - lightLevel); dfloat minimum = de::max(0.f, de::squared(lightLevel) + (lightLevel - .63f) * .5f); if(real < minimum) real = minimum; // Clamp it. return de::min(real, 1.f); } return lightLevel; } dfloat Rend_ShadowAttenuationFactor(coord_t distance) { if(shadowMaxDistance > 0 && distance > 3 * shadowMaxDistance / 4) { return (shadowMaxDistance - distance) / (shadowMaxDistance / 4); } return 1; } static Vector3f skyLightColor; static Vector3f oldSkyAmbientColor(-1.f, -1.f, -1.f); static dfloat oldRendSkyLight = -1; bool Rend_SkyLightIsEnabled() { return rendSkyLight > .001f; } Vector3f Rend_SkyLightColor() { if(Rend_SkyLightIsEnabled() && ClientApp::worldSystem().hasMap()) { Sky &sky = ClientApp::worldSystem().map().sky(); Vector3f const &ambientColor = sky.ambientColor(); if(rendSkyLight != oldRendSkyLight || !INRANGE_OF(ambientColor.x, oldSkyAmbientColor.x, .001f) || !INRANGE_OF(ambientColor.y, oldSkyAmbientColor.y, .001f) || !INRANGE_OF(ambientColor.z, oldSkyAmbientColor.z, .001f)) { skyLightColor = ambientColor; R_AmplifyColor(skyLightColor); // Apply the intensity factor cvar. for(dint i = 0; i < 3; ++i) { skyLightColor[i] = skyLightColor[i] + (1 - rendSkyLight) * (1.f - skyLightColor[i]); } // When the sky light color changes we must update the light grid. scheduleFullLightGridUpdate(); oldSkyAmbientColor = ambientColor; } oldRendSkyLight = rendSkyLight; return skyLightColor; } return Vector3f(1, 1, 1); } /** * Determine the effective ambient light color for the given @a sector. Usually * one would obtain this info from SectorCluster, however in some situations the * correct light color is *not* that of the cluster (e.g., where map hacks use * mapped planes to reference another sector). */ static Vector3f Rend_AmbientLightColor(Sector const §or) { if(Rend_SkyLightIsEnabled() && sector.hasSkyMaskedPlane()) { return Rend_SkyLightColor(); } // A non-skylight sector (i.e., everything else!) // Return the sector's ambient light color. return sector.lightColor(); } Vector3f Rend_LuminousColor(Vector3f const &color, dfloat light) { light = de::clamp(0.f, light, 1.f) * dynlightFactor; // In fog additive blending is used; the normal fog color is way too bright. if(usingFog) light *= dynlightFogBright; // Multiply light with (ambient) color. return color * light; } coord_t Rend_PlaneGlowHeight(dfloat intensity) { return de::clamp(0, GLOW_HEIGHT_MAX * intensity * glowHeightFactor, glowHeightMax); } Material *Rend_ChooseMapSurfaceMaterial(Surface const &surface) { switch(renderTextures) { case 0: // No texture mode. case 1: // Normal mode. if(!(devNoTexFix && surface.hasFixMaterial())) { if(surface.hasMaterial() || surface.parent().type() != DMU_PLANE) return surface.materialPtr(); } // Use special "missing" material. return &resSys().material(de::Uri("System", Path("missing"))); case 2: // Lighting debug mode. if(surface.hasMaterial() && !(!devNoTexFix && surface.hasFixMaterial())) { if(!surface.hasSkyMaskedMaterial() || devRendSkyMode) { // Use the special "gray" material. return &resSys().material(de::Uri("System", Path("gray"))); } } break; default: break; } // No material, then. return nullptr; } static void lightVertex(Vector4f &color, Vector3f const &vtx, dfloat lightLevel, Vector3f const &ambientColor) { dfloat const dist = Rend_PointDist2D(vtx); // Apply distance attenuation. lightLevel = Rend_AttenuateLightLevel(dist, lightLevel); // Add extra light. lightLevel = de::clamp(0.f, lightLevel + Rend_ExtraLightDelta(), 1.f); Rend_ApplyLightAdaptation(lightLevel); for(dint i = 0; i < 3; ++i) { color[i] = lightLevel * ambientColor[i]; } } static void lightVertices(duint num, Vector4f *colors, Vector3f const *verts, dfloat lightLevel, Vector3f const &ambientColor) { for(duint i = 0; i < num; ++i) { lightVertex(colors[i], verts[i], lightLevel, ambientColor); } } /** * This doesn't create a rendering primitive but a vissprite! The vissprite * represents the masked poly and will be rendered during the rendering * of sprites. This is necessary because all masked polygons must be * rendered back-to-front, or there will be alpha artifacts along edges. */ void Rend_AddMaskedPoly(Vector3f const *rvertices, Vector4f const *rcolors, coord_t wallLength, MaterialAnimator *matAnimator, Vector2f const &materialOrigin, blendmode_t blendMode, duint lightListIdx, dfloat glow) { vissprite_t *vis = R_NewVisSprite(VSPR_MASKED_WALL); vis->pose.origin = (rvertices[0] + rvertices[3]) / 2; vis->pose.distance = Rend_PointDist2D(vis->pose.origin); VS_WALL(vis)->texOffset[0] = materialOrigin[0]; VS_WALL(vis)->texOffset[1] = materialOrigin[1]; // Masked walls are sometimes used for special effects like arcs, // cobwebs and bottoms of sails. In order for them to look right, // we need to disable texture wrapping on the horizontal axis (S). // Most masked walls need wrapping, though. What we need to do is // look at the texture coordinates and see if they require texture // wrapping. if(renderTextures) { // Ensure we've up to date info about the material. matAnimator->prepare(); Vector2i const &matDimensions = matAnimator->dimensions(); VS_WALL(vis)->texCoord[0][0] = VS_WALL(vis)->texOffset[0] / matDimensions.x; VS_WALL(vis)->texCoord[1][0] = VS_WALL(vis)->texCoord[0][0] + wallLength / matDimensions.x; VS_WALL(vis)->texCoord[0][1] = VS_WALL(vis)->texOffset[1] / matDimensions.y; VS_WALL(vis)->texCoord[1][1] = VS_WALL(vis)->texCoord[0][1] + (rvertices[3].z - rvertices[0].z) / matDimensions.y; dint wrapS = GL_REPEAT, wrapT = GL_REPEAT; if(!matAnimator->isOpaque()) { if(!(VS_WALL(vis)->texCoord[0][0] < 0 || VS_WALL(vis)->texCoord[0][0] > 1 || VS_WALL(vis)->texCoord[1][0] < 0 || VS_WALL(vis)->texCoord[1][0] > 1)) { // Visible portion is within the actual [0..1] range. wrapS = GL_CLAMP_TO_EDGE; } // Clamp on the vertical axis if the coords are in the normal [0..1] range. if(!(VS_WALL(vis)->texCoord[0][1] < 0 || VS_WALL(vis)->texCoord[0][1] > 1 || VS_WALL(vis)->texCoord[1][1] < 0 || VS_WALL(vis)->texCoord[1][1] > 1)) { wrapT = GL_CLAMP_TO_EDGE; } } // Choose a specific variant for use as a middle wall section. matAnimator = &matAnimator->material().getAnimator(Rend_MapSurfaceMaterialSpec(wrapS, wrapT)); } VS_WALL(vis)->animator = matAnimator; VS_WALL(vis)->blendMode = blendMode; for(dint i = 0; i < 4; ++i) { VS_WALL(vis)->vertices[i].pos[0] = rvertices[i].x; VS_WALL(vis)->vertices[i].pos[1] = rvertices[i].y; VS_WALL(vis)->vertices[i].pos[2] = rvertices[i].z; for(dint c = 0; c < 4; ++c) { /// @todo Do not clamp here. VS_WALL(vis)->vertices[i].color[c] = de::clamp(0.f, rcolors[i][c], 1.f); } } /// @todo Semitransparent masked polys arn't lit atm if(glow < 1 && lightListIdx && numTexUnits > 1 && envModAdd && !(rcolors[0].w < 1)) { // The dynlights will have already been sorted so that the brightest // and largest of them is first in the list. So grab that one. ProjectedTextureData const *dyn = nullptr; rendSys().forAllSurfaceProjections(lightListIdx, [&dyn] (ProjectedTextureData const &tp) { dyn = &tp; return LoopAbort; }); VS_WALL(vis)->modTex = dyn->texture; VS_WALL(vis)->modTexCoord[0][0] = dyn->topLeft.x; VS_WALL(vis)->modTexCoord[0][1] = dyn->topLeft.y; VS_WALL(vis)->modTexCoord[1][0] = dyn->bottomRight.x; VS_WALL(vis)->modTexCoord[1][1] = dyn->bottomRight.y; for(dint c = 0; c < 4; ++c) { VS_WALL(vis)->modColor[c] = dyn->color[c]; } } else { VS_WALL(vis)->modTex = 0; } } static void quadTexCoords(Vector2f *tc, Vector3f const *rverts, coord_t wallLength, Vector3d const &topLeft) { tc[0].x = tc[1].x = rverts[0].x - topLeft.x; tc[3].y = tc[1].y = rverts[0].y - topLeft.y; tc[3].x = tc[2].x = tc[0].x + wallLength; tc[2].y = tc[3].y + (rverts[1].z - rverts[0].z); tc[0].y = tc[3].y + (rverts[3].z - rverts[2].z); } static void quadLightCoords(Vector2f *tc, Vector2f const &topLeft, Vector2f const &bottomRight) { tc[1].x = tc[0].x = topLeft.x; tc[1].y = tc[3].y = topLeft.y; tc[3].x = tc[2].x = bottomRight.x; tc[2].y = tc[0].y = bottomRight.y; } static dfloat shinyVertical(dfloat dy, dfloat dx) { return ((std::atan(dy/dx) / (PI/2)) + 1) / 2; } static void quadShinyTexCoords(Vector2f *tc, Vector3f const *topLeft, Vector3f const *bottomRight, coord_t wallLength) { // Quad surface vector. vec2f_t surface; V2f_Set(surface, (bottomRight->x - topLeft->x) / wallLength, (bottomRight->y - topLeft->y) / wallLength); vec2f_t normal; V2f_Set(normal, surface[1], -surface[0]); // Calculate coordinates based on viewpoint and surface normal. dfloat prevAngle = 0; for(duint i = 0; i < 2; ++i) { // View vector. vec2f_t view; V2f_Set(view, Rend_EyeOrigin().x - (i == 0? topLeft->x : bottomRight->x), Rend_EyeOrigin().z - (i == 0? topLeft->y : bottomRight->y)); dfloat distance = V2f_Normalize(view); vec2f_t projected; V2f_Project(projected, view, normal); vec2f_t s; V2f_Subtract(s, projected, view); V2f_Scale(s, 2); vec2f_t reflected; V2f_Sum(reflected, view, s); dfloat angle = std::acos(reflected[1]) / PI; if(reflected[0] < 0) { angle = 1 - angle; } if(i == 0) { prevAngle = angle; } else { if(angle > prevAngle) angle -= 1; } // Horizontal coordinates. tc[ (i == 0 ? 1 : 2) ].x = tc[ (i == 0 ? 0 : 3) ].x = angle + .3f; /*std::acos(-dot)/PI*/ // Vertical coordinates. tc[ (i == 0 ? 0 : 2) ].y = shinyVertical(Rend_EyeOrigin().y - bottomRight->z, distance); tc[ (i == 0 ? 1 : 3) ].y = shinyVertical(Rend_EyeOrigin().y - topLeft->z, distance); } } static void flatShinyTexCoords(Vector2f *tc, Vector3f const &point) { DENG2_ASSERT(tc); // Determine distance to viewer. dfloat distToEye = Vector2f(Rend_EyeOrigin().x - point.x, Rend_EyeOrigin().z - point.y) .normalize().length(); if(distToEye < 10) { // Too small distances cause an ugly 'crunch' below and above // the viewpoint. distToEye = 10; } // Offset from the normal view plane. Vector2f start(Rend_EyeOrigin().x, Rend_EyeOrigin().z); dfloat offset = ((start.y - point.y) * sin(.4f)/*viewFrontVec[0]*/ - (start.x - point.x) * cos(.4f)/*viewFrontVec[2]*/); tc->x = ((shinyVertical(offset, distToEye) - .5f) * 2) + .5f; tc->y = shinyVertical(Rend_EyeOrigin().y - point.z, distToEye); } /// Paramaters for drawProjectedLights (POD). struct drawprojectedlights_parameters_t { duint lastIdx; Vector3f const *rvertices; duint numVertices, realNumVertices; Vector3d const *topLeft; Vector3d const *bottomRight; bool isWall; struct { WallEdge const *leftEdge; WallEdge const *rightEdge; } wall; }; /** * Render all dynlights in projection list @a listIdx according to @a paramaters * writing them to the renderering lists for the current frame. * * @note If multi-texturing is being used for the first light; it is skipped. * * @return Number of lights rendered. */ static duint drawProjectedLights(duint listIdx, drawprojectedlights_parameters_t &parm) { duint numDrawn = parm.lastIdx; // Generate a new primitive for each light projection. rendSys().forAllSurfaceProjections(listIdx, [&parm] (ProjectedTextureData const &tp) { // If multitexturing is in use we skip the first. if(!(Rend_IsMTexLights() && parm.lastIdx == 0)) { // Allocate enough for the divisions too. Vector3f *verts = R_AllocRendVertices(parm.realNumVertices); Vector2f *texCoords = R_AllocRendTexCoords(parm.realNumVertices); Vector4f *colorCoords = R_AllocRendColors(parm.realNumVertices); bool const mustSubdivide = (parm.isWall && (parm.wall.leftEdge->divisionCount() || parm.wall.rightEdge->divisionCount() )); for(duint i = 0; i < parm.numVertices; ++i) { colorCoords[i] = tp.color; } if(parm.isWall) { WallEdge const &leftEdge = *parm.wall.leftEdge; WallEdge const &rightEdge = *parm.wall.rightEdge; texCoords[1].x = texCoords[0].x = tp.topLeft.x; texCoords[1].y = texCoords[3].y = tp.topLeft.y; texCoords[3].x = texCoords[2].x = tp.bottomRight.x; texCoords[2].y = texCoords[0].y = tp.bottomRight.y; if(mustSubdivide) { // Need to swap indices around into fans set the position // of the division vertices, interpolate texcoords and color. Vector3f origVerts[4]; std::memcpy(origVerts, parm.rvertices, sizeof(Vector3f) * 4); Vector2f origTexCoords[4]; std::memcpy(origTexCoords, texCoords, sizeof(Vector2f) * 4); Vector4f origColors[4]; std::memcpy(origColors, colorCoords, sizeof(Vector4f) * 4); R_DivVerts(verts, origVerts, leftEdge, rightEdge); R_DivTexCoords(texCoords, origTexCoords, leftEdge, rightEdge); R_DivVertColors(colorCoords, origColors, leftEdge, rightEdge); } else { std::memcpy(verts, parm.rvertices, sizeof(Vector3f) * parm.numVertices); } } else { // It's a flat. dfloat const width = parm.bottomRight->x - parm.topLeft->x; dfloat const height = parm.bottomRight->y - parm.topLeft->y; for(duint i = 0; i < parm.numVertices; ++i) { texCoords[i].x = ((parm.bottomRight->x - parm.rvertices[i].x) / width * tp.topLeft.x) + ((parm.rvertices[i].x - parm.topLeft->x) / width * tp.bottomRight.x); texCoords[i].y = ((parm.bottomRight->y - parm.rvertices[i].y) / height * tp.topLeft.y) + ((parm.rvertices[i].y - parm.topLeft->y) / height * tp.bottomRight.y); } std::memcpy(verts, parm.rvertices, sizeof(Vector3f) * parm.numVertices); } DrawListSpec listSpec; listSpec.group = LightGeom; listSpec.texunits[TU_PRIMARY] = GLTextureUnit(tp.texture, gl::ClampToEdge, gl::ClampToEdge); DrawList &lightList = rendSys().drawLists().find(listSpec); if(mustSubdivide) { WallEdge const &leftEdge = *parm.wall.leftEdge; WallEdge const &rightEdge = *parm.wall.rightEdge; lightList.write(gl::TriangleFan, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, 3 + rightEdge.divisionCount(), verts + 3 + leftEdge.divisionCount(), colorCoords + 3 + leftEdge.divisionCount(), texCoords + 3 + leftEdge.divisionCount()) .write(gl::TriangleFan, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, 3 + leftEdge.divisionCount(), verts, colorCoords, texCoords); } else { lightList.write(parm.isWall? gl::TriangleStrip : gl::TriangleFan, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, parm.numVertices, verts, colorCoords, texCoords); } R_FreeRendVertices(verts); R_FreeRendTexCoords(texCoords); R_FreeRendColors(colorCoords); } parm.lastIdx++; return LoopContinue; }); numDrawn = parm.lastIdx - numDrawn; if(Rend_IsMTexLights()) numDrawn -= 1; return numDrawn; } /// Parameters for drawProjectedShadows (POD). struct drawprojectedshadows_parameters_t { duint lastIdx; Vector3f const *rvertices; duint numVertices, realNumVertices; Vector3d const *topLeft; Vector3d const *bottomRight; bool isWall; struct { WallEdge const *leftEdge; WallEdge const *rightEdge; } wall; }; /** * Draw all shadows in projection list @a listIdx according to @a parameters * writing them to the renderering lists for the current frame. */ static void drawProjectedShadows(duint listIdx, drawprojectedshadows_parameters_t &parm) { DrawListSpec listSpec; listSpec.group = ShadowGeom; listSpec.texunits[TU_PRIMARY] = GLTextureUnit(GL_PrepareLSTexture(LST_DYNAMIC), gl::ClampToEdge, gl::ClampToEdge); // Write shadows to the draw lists. DrawList &shadowList = rendSys().drawLists().find(listSpec); rendSys().forAllSurfaceProjections(listIdx, [&shadowList, &parm] (ProjectedTextureData const &tp) { // Allocate enough for the divisions too. Vector3f *verts = R_AllocRendVertices(parm.realNumVertices); Vector2f *texCoords = R_AllocRendTexCoords(parm.realNumVertices); Vector4f *colorCoords = R_AllocRendColors(parm.realNumVertices); bool const mustSubdivide = (parm.isWall && (parm.wall.leftEdge->divisionCount() || parm.wall.rightEdge->divisionCount() )); for(duint i = 0; i < parm.numVertices; ++i) { colorCoords[i] = tp.color; } if(parm.isWall) { WallEdge const &leftEdge = *parm.wall.leftEdge; WallEdge const &rightEdge = *parm.wall.rightEdge; texCoords[1].x = texCoords[0].x = tp.topLeft.x; texCoords[1].y = texCoords[3].y = tp.topLeft.y; texCoords[3].x = texCoords[2].x = tp.bottomRight.x; texCoords[2].y = texCoords[0].y = tp.bottomRight.y; if(mustSubdivide) { // Need to swap indices around into fans set the position of the // division vertices, interpolate texcoords and color. Vector3f origVerts[4]; std::memcpy(origVerts, parm.rvertices, sizeof(Vector3f) * 4); Vector2f origTexCoords[4]; std::memcpy(origTexCoords, texCoords, sizeof(Vector2f) * 4); Vector4f origColors[4]; std::memcpy(origColors, colorCoords, sizeof(Vector4f) * 4); R_DivVerts(verts, origVerts, leftEdge, rightEdge); R_DivTexCoords(texCoords, origTexCoords, leftEdge, rightEdge); R_DivVertColors(colorCoords, origColors, leftEdge, rightEdge); } else { std::memcpy(verts, parm.rvertices, sizeof(Vector3f) * parm.numVertices); } } else { // It's a flat. dfloat const width = parm.bottomRight->x - parm.topLeft->x; dfloat const height = parm.bottomRight->y - parm.topLeft->y; for(duint i = 0; i < parm.numVertices; ++i) { texCoords[i].x = ((parm.bottomRight->x - parm.rvertices[i].x) / width * tp.topLeft.x) + ((parm.rvertices[i].x - parm.topLeft->x) / width * tp.bottomRight.x); texCoords[i].y = ((parm.bottomRight->y - parm.rvertices[i].y) / height * tp.topLeft.y) + ((parm.rvertices[i].y - parm.topLeft->y) / height * tp.bottomRight.y); } std::memcpy(verts, parm.rvertices, sizeof(Vector3f) * parm.numVertices); } if(mustSubdivide) { WallEdge const &leftEdge = *parm.wall.leftEdge; WallEdge const &rightEdge = *parm.wall.rightEdge; shadowList.write(gl::TriangleFan, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, 3 + rightEdge.divisionCount(), verts + 3 + leftEdge.divisionCount(), colorCoords + 3 + leftEdge.divisionCount(), texCoords + 3 + leftEdge.divisionCount()) .write(gl::TriangleFan, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, 3 + leftEdge.divisionCount(), verts, colorCoords, texCoords); } else { shadowList.write(parm.isWall? gl::TriangleStrip : gl::TriangleFan, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, parm.numVertices, verts, colorCoords, texCoords); } R_FreeRendVertices(verts); R_FreeRendTexCoords(texCoords); R_FreeRendColors(colorCoords); return LoopContinue; }); } struct rendworldpoly_params_t { bool skyMasked; blendmode_t blendMode; Vector3d const *topLeft; Vector3d const *bottomRight; Vector2f const *materialOrigin; Vector2f const *materialScale; dfloat alpha; dfloat surfaceLightLevelDL; dfloat surfaceLightLevelDR; Vector3f const *surfaceColor; Matrix3f const *surfaceTangentMatrix; duint lightListIdx; ///< List of lights that affect this poly. duint shadowListIdx; ///< List of shadows that affect this poly. dfloat glowing; bool forceOpaque; MapElement *mapElement; dint geomGroup; bool isWall; // Wall only: struct { coord_t sectionWidth; Vector3f const *surfaceColor2; ///< Secondary color. WallEdge const *leftEdge; WallEdge const *rightEdge; } wall; }; static bool renderWorldPoly(Vector3f *posCoords, duint numVertices, rendworldpoly_params_t const &p, MaterialAnimator &matAnimator) { DENG2_ASSERT(posCoords); SectorCluster &cluster = curSubspace->cluster(); // Ensure we've up to date info about the material. matAnimator.prepare(); duint const realNumVertices = (p.isWall? 3 + p.wall.leftEdge->divisionCount() + 3 + p.wall.rightEdge->divisionCount() : numVertices); bool const mustSubdivide = (p.isWall && (p.wall.leftEdge->divisionCount() || p.wall.rightEdge->divisionCount())); bool const skyMaskedMaterial = (p.skyMasked || (matAnimator.material().isSkyMasked())); bool const drawAsVisSprite = (!p.forceOpaque && !p.skyMasked && (!matAnimator.isOpaque() || p.alpha < 1 || p.blendMode > 0)); bool useLights = false, useShadows = false, hasDynlights = false; // Map RTU configuration. GLTextureUnit const *detailRTU = (r_detail && !p.skyMasked && matAnimator.texUnit(MaterialAnimator::TU_DETAIL).hasTexture())? &matAnimator.texUnit(MaterialAnimator::TU_DETAIL) : nullptr; GLTextureUnit const *detailInterRTU = (r_detail && !p.skyMasked && matAnimator.texUnit(MaterialAnimator::TU_DETAIL_INTER).hasTexture())? &matAnimator.texUnit(MaterialAnimator::TU_DETAIL_INTER) : nullptr; GLTextureUnit const *layer0RTU = (!p.skyMasked)? &matAnimator.texUnit(MaterialAnimator::TU_LAYER0) : nullptr; GLTextureUnit const *layer0InterRTU = (!p.skyMasked && matAnimator.texUnit(MaterialAnimator::TU_LAYER0_INTER).hasTexture())? &matAnimator.texUnit(MaterialAnimator::TU_LAYER0_INTER) : nullptr; GLTextureUnit const *shineRTU = (useShinySurfaces && !p.skyMasked && matAnimator.texUnit(MaterialAnimator::TU_SHINE).hasTexture())? &matAnimator.texUnit(MaterialAnimator::TU_SHINE) : nullptr; GLTextureUnit const *shineMaskRTU = (useShinySurfaces && !p.skyMasked && matAnimator.texUnit(MaterialAnimator::TU_SHINE).hasTexture() && matAnimator.texUnit(MaterialAnimator::TU_SHINE_MASK).hasTexture())? &matAnimator.texUnit(MaterialAnimator::TU_SHINE_MASK) : nullptr; Vector4f *colorCoords = !skyMaskedMaterial? R_AllocRendColors(realNumVertices) : nullptr; Vector2f *primaryCoords = R_AllocRendTexCoords(realNumVertices); Vector2f *interCoords = layer0InterRTU? R_AllocRendTexCoords(realNumVertices) : nullptr; Vector4f *shinyColors = nullptr; Vector2f *shinyTexCoords = nullptr; Vector2f *modCoords = nullptr; DGLuint modTex = 0; Vector2f modTexSt[2]; // [topLeft, bottomRight] Vector3f modColor; if(!skyMaskedMaterial) { // ShinySurface? if(shineRTU && !drawAsVisSprite) { // We'll reuse the same verts but we need new colors. shinyColors = R_AllocRendColors(realNumVertices); // The normal texcoords are used with the mask. // New texcoords are required for shiny texture. shinyTexCoords = R_AllocRendTexCoords(realNumVertices); } if(p.glowing < 1) { useLights = (p.lightListIdx ? true : false); useShadows = (p.shadowListIdx? true : false); // If multitexturing is enabled and there is at least one dynlight // affecting this surface, grab the parameters needed to draw it. if(useLights && Rend_IsMTexLights()) { ProjectedTextureData const *dyn = nullptr; rendSys().forAllSurfaceProjections(p.lightListIdx, [&dyn] (ProjectedTextureData const &tp) { dyn = &tp; return LoopAbort; }); modTex = dyn->texture; modCoords = R_AllocRendTexCoords(realNumVertices); modColor = dyn->color; modTexSt[0] = dyn->topLeft; modTexSt[1] = dyn->bottomRight; } } } if(p.isWall) { // Primary texture coordinates. quadTexCoords(primaryCoords, posCoords, p.wall.sectionWidth, *p.topLeft); // Blend texture coordinates. if(layer0InterRTU && !drawAsVisSprite) quadTexCoords(interCoords, posCoords, p.wall.sectionWidth, *p.topLeft); // Shiny texture coordinates. if(shineRTU && !drawAsVisSprite) quadShinyTexCoords(shinyTexCoords, &posCoords[1], &posCoords[2], p.wall.sectionWidth); // First light texture coordinates. if(modTex && Rend_IsMTexLights()) quadLightCoords(modCoords, modTexSt[0], modTexSt[1]); } else { for(duint i = 0; i < numVertices; ++i) { Vector3f const &vtx = posCoords[i]; Vector3f const delta(vtx - *p.topLeft); // Primary texture coordinates. if(layer0RTU) { primaryCoords[i] = Vector2f(delta.x, -delta.y); } // Blend primary texture coordinates. if(layer0InterRTU) { interCoords[i] = Vector2f(delta.x, -delta.y); } // Shiny texture coordinates. if(shineRTU) { flatShinyTexCoords(&shinyTexCoords[i], vtx); } // First light texture coordinates. if(modTex && Rend_IsMTexLights()) { dfloat const width = p.bottomRight->x - p.topLeft->x; dfloat const height = p.bottomRight->y - p.topLeft->y; modCoords[i] = Vector2f(((p.bottomRight->x - vtx.x) / width * modTexSt[0].x) + (delta.x / width * modTexSt[1].x), ((p.bottomRight->y - vtx.y) / height * modTexSt[0].y) + (delta.y / height * modTexSt[1].y)); } } } // Light this polygon. if(!skyMaskedMaterial) { if(levelFullBright || !(p.glowing < 1)) { // Uniform color. Apply to all vertices. dfloat ll = de::clamp(0.f, curSectorLightLevel + (levelFullBright? 1 : p.glowing), 1.f); Vector4f *colorIt = colorCoords; for(duint i = 0; i < numVertices; ++i, colorIt++) { colorIt->x = colorIt->y = colorIt->z = ll; } } else { // Non-uniform color. if(useBias) { Map &map = cluster.sector().map(); Shard &shard = cluster.shard(*p.mapElement, p.geomGroup); // Apply the ambient light term from the grid (if available). if(map.hasLightGrid()) { Vector3f const *posIt = posCoords; Vector4f *colorIt = colorCoords; for(duint i = 0; i < numVertices; ++i, posIt++, colorIt++) { *colorIt = map.lightGrid().evaluate(*posIt); } } // Apply bias light source contributions. shard.lightWithBiasSources(posCoords, colorCoords, *p.surfaceTangentMatrix, map.biasCurrentTime()); // Apply surface glow. if(p.glowing > 0) { Vector4f const glow(p.glowing, p.glowing, p.glowing, 0); Vector4f *colorIt = colorCoords; for(duint i = 0; i < numVertices; ++i, colorIt++) { *colorIt += glow; } } // Apply light range compression and clamp. Vector3f const *posIt = posCoords; Vector4f *colorIt = colorCoords; for(duint i = 0; i < numVertices; ++i, posIt++, colorIt++) { for(dint i = 0; i < 3; ++i) { (*colorIt)[i] = de::clamp(0.f, (*colorIt)[i] + Rend_LightAdaptationDelta((*colorIt)[i]), 1.f); } } } else { dfloat llL = de::clamp(0.f, curSectorLightLevel + p.surfaceLightLevelDL + p.glowing, 1.f); dfloat llR = de::clamp(0.f, curSectorLightLevel + p.surfaceLightLevelDR + p.glowing, 1.f); // Calculate the color for each vertex, blended with plane color? if(p.surfaceColor->x < 1 || p.surfaceColor->y < 1 || p.surfaceColor->z < 1) { // Blend sector light+color+surfacecolor Vector3f vColor = (*p.surfaceColor) * curSectorLightColor; if(p.isWall && llL != llR) { lightVertex(colorCoords[0], posCoords[0], llL, vColor); lightVertex(colorCoords[1], posCoords[1], llL, vColor); lightVertex(colorCoords[2], posCoords[2], llR, vColor); lightVertex(colorCoords[3], posCoords[3], llR, vColor); } else { lightVertices(numVertices, colorCoords, posCoords, llL, vColor); } } else { // Use sector light+color only. if(p.isWall && llL != llR) { lightVertex(colorCoords[0], posCoords[0], llL, curSectorLightColor); lightVertex(colorCoords[1], posCoords[1], llL, curSectorLightColor); lightVertex(colorCoords[2], posCoords[2], llR, curSectorLightColor); lightVertex(colorCoords[3], posCoords[3], llR, curSectorLightColor); } else { lightVertices(numVertices, colorCoords, posCoords, llL, curSectorLightColor); } } // Bottom color (if different from top)? if(p.isWall && p.wall.surfaceColor2) { // Blend sector light+color+surfacecolor Vector3f vColor = (*p.wall.surfaceColor2) * curSectorLightColor; lightVertex(colorCoords[0], posCoords[0], llL, vColor); lightVertex(colorCoords[2], posCoords[2], llR, vColor); } } // Apply torch light? if(viewPlayer->shared.fixedColorMap) { Vector3f const *posIt = posCoords; Vector4f *colorIt = colorCoords; for(duint i = 0; i < numVertices; ++i, colorIt++, posIt++) { Rend_ApplyTorchLight(*colorIt, Rend_PointDist2D(*posIt)); } } } if(shineRTU && !drawAsVisSprite) { // Strength of the shine. Vector3f const &minColor = matAnimator.shineMinColor(); for(duint i = 0; i < numVertices; ++i) { Vector4f &color = shinyColors[i]; color = Vector3f(colorCoords[i]).max(minColor); color.w = shineRTU->opacity; } } // Apply uniform alpha (overwritting luminance factors). Vector4f *colorIt = colorCoords; for(duint i = 0; i < numVertices; ++i, colorIt++) { colorIt->w = p.alpha; } } if(useLights || useShadows) { // Surfaces lit by dynamic lights may need to be rendered differently // than non-lit surfaces. Determine the average light level of this rend // poly, if too bright; do not bother with lights. dfloat avgLightlevel = 0; for(duint i = 0; i < numVertices; ++i) { avgLightlevel += colorCoords[i].x; avgLightlevel += colorCoords[i].y; avgLightlevel += colorCoords[i].z; } avgLightlevel /= numVertices * 3; if(avgLightlevel > 0.98f) { useLights = false; } if(avgLightlevel < 0.02f) { useShadows = false; } } if(drawAsVisSprite) { DENG2_ASSERT(p.isWall); // Masked polys (walls) get a special treatment (=> vissprite). This is // needed because all masked polys must be sorted (sprites are masked // polys). Otherwise there will be artifacts. Rend_AddMaskedPoly(posCoords, colorCoords, p.wall.sectionWidth, &matAnimator, *p.materialOrigin, p.blendMode, p.lightListIdx, p.glowing); R_FreeRendTexCoords(primaryCoords); R_FreeRendColors(colorCoords); R_FreeRendTexCoords(interCoords); R_FreeRendTexCoords(modCoords); R_FreeRendTexCoords(shinyTexCoords); R_FreeRendColors(shinyColors); return false; // We HAD to use a vissprite, so it MUST not be opaque. } if(useLights) { // Render all lights projected onto this surface. drawprojectedlights_parameters_t parm; de::zap(parm); parm.rvertices = posCoords; parm.numVertices = numVertices; parm.realNumVertices = realNumVertices; parm.lastIdx = 0; parm.topLeft = p.topLeft; parm.bottomRight = p.bottomRight; parm.isWall = p.isWall; if(parm.isWall) { parm.wall.leftEdge = p.wall.leftEdge; parm.wall.rightEdge = p.wall.rightEdge; } hasDynlights = (0 != drawProjectedLights(p.lightListIdx, parm)); } if(useShadows) { // Render all shadows projected onto this surface. drawprojectedshadows_parameters_t parm; de::zap(parm); parm.rvertices = posCoords; parm.numVertices = numVertices; parm.realNumVertices = realNumVertices; parm.topLeft = p.topLeft; parm.bottomRight = p.bottomRight; parm.isWall = p.isWall; if(parm.isWall) { parm.wall.leftEdge = p.wall.leftEdge; parm.wall.rightEdge = p.wall.rightEdge; } drawProjectedShadows(p.shadowListIdx, parm); } // Write multiple polys depending on rend params. if(mustSubdivide) { WallEdge const &leftEdge = *p.wall.leftEdge; WallEdge const &rightEdge = *p.wall.rightEdge; // Need to swap indices around into fans set the position of the division // vertices, interpolate texcoords and color. Vector3f origVerts[4]; std::memcpy(origVerts, posCoords, sizeof(origVerts)); Vector2f origTexCoords[4]; std::memcpy(origTexCoords, primaryCoords, sizeof(origTexCoords)); Vector4f origColors[4]; if(colorCoords || shinyColors) { std::memcpy(origColors, colorCoords, sizeof(origColors)); } R_DivVerts(posCoords, origVerts, leftEdge, rightEdge); R_DivTexCoords(primaryCoords, origTexCoords, leftEdge, rightEdge); if(colorCoords) { R_DivVertColors(colorCoords, origColors, leftEdge, rightEdge); } if(interCoords) { Vector2f origTexCoords2[4]; std::memcpy(origTexCoords2, interCoords, sizeof(origTexCoords2)); R_DivTexCoords(interCoords, origTexCoords2, leftEdge, rightEdge); } if(modCoords) { Vector2f origTexCoords5[4]; std::memcpy(origTexCoords5, modCoords, sizeof(origTexCoords5)); R_DivTexCoords(modCoords, origTexCoords5, leftEdge, rightEdge); } if(shinyTexCoords) { Vector2f origShinyTexCoords[4]; std::memcpy(origShinyTexCoords, shinyTexCoords, sizeof(origShinyTexCoords)); R_DivTexCoords(shinyTexCoords, origShinyTexCoords, leftEdge, rightEdge); } if(shinyColors) { Vector4f origShinyColors[4]; std::memcpy(origShinyColors, shinyColors, sizeof(origShinyColors)); R_DivVertColors(shinyColors, origShinyColors, leftEdge, rightEdge); } if(p.skyMasked) { ClientApp::renderSystem().drawLists() .find(DrawListSpec(SkyMaskGeom)) .write(gl::TriangleFan, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, 3 + rightEdge.divisionCount(), posCoords + 3 + leftEdge.divisionCount()) .write(gl::TriangleFan, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, 3 + leftEdge.divisionCount(), posCoords); } else { DrawListSpec listSpec((modTex || hasDynlights)? LitGeom : UnlitGeom); if(layer0RTU) { listSpec.texunits[TU_PRIMARY] = *layer0RTU; if(p.materialOrigin) { listSpec.texunits[TU_PRIMARY].offset += *p.materialOrigin; } if(p.materialScale) { listSpec.texunits[TU_PRIMARY].scale *= *p.materialScale; listSpec.texunits[TU_PRIMARY].offset *= *p.materialScale; } } if(detailRTU) { listSpec.texunits[TU_PRIMARY_DETAIL] = *detailRTU; if(p.materialOrigin) { listSpec.texunits[TU_PRIMARY_DETAIL].offset += *p.materialOrigin; } } if(layer0InterRTU) { listSpec.texunits[TU_INTER] = *layer0InterRTU; if(p.materialOrigin) { listSpec.texunits[TU_INTER].offset += *p.materialOrigin; } if(p.materialScale) { listSpec.texunits[TU_INTER].scale *= *p.materialScale; listSpec.texunits[TU_INTER].offset *= *p.materialScale; } } if(detailInterRTU) { listSpec.texunits[TU_INTER_DETAIL] = *detailInterRTU; if(p.materialOrigin) { listSpec.texunits[TU_INTER_DETAIL].offset += *p.materialOrigin; } } ClientApp::renderSystem().drawLists() .find(listSpec) .write(gl::TriangleFan, BM_NORMAL, listSpec.unit(TU_PRIMARY ).scale, listSpec.unit(TU_PRIMARY ).offset, listSpec.unit(TU_PRIMARY_DETAIL).scale, listSpec.unit(TU_PRIMARY_DETAIL).offset, hasDynlights, 3 + rightEdge.divisionCount(), posCoords + 3 + leftEdge.divisionCount(), colorCoords? colorCoords + 3 + leftEdge.divisionCount() : 0, primaryCoords + 3 + leftEdge.divisionCount(), interCoords? interCoords + 3 + leftEdge.divisionCount() : 0, modTex, &modColor, modCoords? modCoords + 3 + leftEdge.divisionCount() : 0) .write(gl::TriangleFan, BM_NORMAL, listSpec.unit(TU_PRIMARY ).scale, listSpec.unit(TU_PRIMARY ).offset, listSpec.unit(TU_PRIMARY_DETAIL).scale, listSpec.unit(TU_PRIMARY_DETAIL).offset, hasDynlights, 3 + leftEdge.divisionCount(), posCoords, colorCoords, primaryCoords, interCoords, modTex, &modColor, modCoords); if(shineRTU) { DrawListSpec listSpec(ShineGeom); listSpec.texunits[TU_PRIMARY] = *shineRTU; if(shineMaskRTU) { listSpec.texunits[TU_INTER] = *shineMaskRTU; if(p.materialOrigin) { listSpec.texunits[TU_INTER].offset += *p.materialOrigin; } if(p.materialScale) { listSpec.texunits[TU_INTER].scale *= *p.materialScale; listSpec.texunits[TU_INTER].offset *= *p.materialScale; } } ClientApp::renderSystem().drawLists() .find(listSpec) .write(gl::TriangleFan, matAnimator.shineBlendMode(), listSpec.unit(TU_INTER).scale, listSpec.unit(TU_INTER).offset, Vector2f(1, 1), Vector2f(0, 0), false, 3 + rightEdge.divisionCount(), posCoords + 3 + leftEdge.divisionCount(), shinyColors + 3 + leftEdge.divisionCount(), shinyTexCoords? shinyTexCoords + 3 + leftEdge.divisionCount() : 0, shineMaskRTU? primaryCoords + 3 + leftEdge.divisionCount() : 0) .write(gl::TriangleFan, matAnimator.shineBlendMode(), listSpec.unit(TU_INTER).scale, listSpec.unit(TU_INTER).offset, Vector2f(1, 1), Vector2f(0, 0), false, 3 + leftEdge.divisionCount(), posCoords, shinyColors, shinyTexCoords, shineMaskRTU? primaryCoords : 0); } } } else { if(p.skyMasked) { ClientApp::renderSystem().drawLists() .find(DrawListSpec(SkyMaskGeom)) .write(p.isWall? gl::TriangleStrip : gl::TriangleFan, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, numVertices, posCoords); } else { DrawListSpec listSpec((modTex || hasDynlights)? LitGeom : UnlitGeom); if(layer0RTU) { listSpec.texunits[TU_PRIMARY] = *layer0RTU; if(p.materialOrigin) { listSpec.texunits[TU_PRIMARY].offset += *p.materialOrigin; } if(p.materialScale) { listSpec.texunits[TU_PRIMARY].scale *= *p.materialScale; listSpec.texunits[TU_PRIMARY].offset *= *p.materialScale; } } if(detailRTU) { listSpec.texunits[TU_PRIMARY_DETAIL] = *detailRTU; if(p.materialOrigin) { listSpec.texunits[TU_PRIMARY_DETAIL].offset += *p.materialOrigin; } } if(layer0InterRTU) { listSpec.texunits[TU_INTER] = *layer0InterRTU; if(p.materialOrigin) { listSpec.texunits[TU_INTER].offset += *p.materialOrigin; } if(p.materialScale) { listSpec.texunits[TU_INTER].scale *= *p.materialScale; listSpec.texunits[TU_INTER].offset *= *p.materialScale; } } if(detailInterRTU) { listSpec.texunits[TU_INTER_DETAIL] = *detailInterRTU; if(p.materialOrigin) { listSpec.texunits[TU_INTER_DETAIL].offset += *p.materialOrigin; } } ClientApp::renderSystem().drawLists() .find(listSpec) .write(p.isWall? gl::TriangleStrip : gl::TriangleFan, BM_NORMAL, listSpec.unit(TU_PRIMARY ).scale, listSpec.unit(TU_PRIMARY ).offset, listSpec.unit(TU_PRIMARY_DETAIL).scale, listSpec.unit(TU_PRIMARY_DETAIL).offset, hasDynlights, numVertices, posCoords, colorCoords, primaryCoords, interCoords, modTex, &modColor, modCoords); if(shineRTU) { DrawListSpec listSpec(ShineGeom); listSpec.texunits[TU_PRIMARY] = *shineRTU; if(shineMaskRTU) { listSpec.texunits[TU_INTER] = *shineMaskRTU; if(p.materialOrigin) { listSpec.texunits[TU_INTER].offset += *p.materialOrigin; } if(p.materialScale) { listSpec.texunits[TU_INTER].scale *= *p.materialScale; listSpec.texunits[TU_INTER].offset *= *p.materialScale; } } ClientApp::renderSystem().drawLists() .find(listSpec) .write(p.isWall? gl::TriangleStrip : gl::TriangleFan, matAnimator.shineBlendMode(), listSpec.unit(TU_INTER ).scale, listSpec.unit(TU_INTER ).offset, listSpec.unit(TU_PRIMARY_DETAIL).scale, listSpec.unit(TU_PRIMARY_DETAIL).offset, false, numVertices, posCoords, shinyColors, shinyTexCoords, shineMaskRTU? primaryCoords : 0); } } } R_FreeRendTexCoords(primaryCoords); R_FreeRendTexCoords(interCoords); R_FreeRendTexCoords(modCoords); R_FreeRendTexCoords(shinyTexCoords); R_FreeRendColors(colorCoords); R_FreeRendColors(shinyColors); return (p.forceOpaque || skyMaskedMaterial || !(p.alpha < 1 || !matAnimator.isOpaque() || p.blendMode > 0)); } static Lumobj::LightmapSemantic lightmapForSurface(Surface const &surface) { if(surface.parent().type() == DMU_SIDE) return Lumobj::Side; // Must be a plane then. auto const &plane = surface.parent().as(); return plane.isSectorFloor()? Lumobj::Down : Lumobj::Up; } static DGLuint prepareLightmap(Texture *tex = nullptr) { if(tex) { if(TextureVariant *variant = tex->prepareVariant(Rend_MapSurfaceLightmapTextureSpec())) { return variant->glName(); } // Dang... } // Prepare the default/fallback lightmap instead. return GL_PrepareLSTexture(LST_DYNAMIC); } static bool projectDynlight(Vector3d const &topLeft, Vector3d const &bottomRight, Lumobj const &lum, Surface const &surface, dfloat blendFactor, ProjectedTextureData &projected) { if(blendFactor < OMNILIGHT_SURFACE_LUMINOSITY_ATTRIBUTION_MIN) return false; // Has this already been occluded? if(R_ViewerLumobjIsHidden(lum.indexInMap())) return false; // No lightmap texture? DGLuint tex = prepareLightmap(lum.lightmap(lightmapForSurface(surface))); if(!tex) return false; Vector3d lumCenter = lum.origin(); lumCenter.z += lum.zOffset(); // On the right side? Vector3d topLeftToLum = topLeft - lumCenter; if(topLeftToLum.dot(surface.tangentMatrix().column(2)) > 0.f) return false; // Calculate 3D distance between surface and lumobj. Vector3d pointOnPlane = R_ClosestPointOnPlane(surface.tangentMatrix().column(2)/*normal*/, topLeft, lumCenter); coord_t distToLum = (lumCenter - pointOnPlane).length(); if(distToLum <= 0 || distToLum > lum.radius()) return false; // Calculate the final surface light attribution factor. dfloat luma = 1.5f - 1.5f * distToLum / lum.radius(); // Fade out as distance from viewer increases. luma *= lum.attenuation(R_ViewerLumobjDistance(lum.indexInMap())); // Would this be seen? if(luma * blendFactor < OMNILIGHT_SURFACE_LUMINOSITY_ATTRIBUTION_MIN) return false; // Project, counteracting aspect correction slightly. Vector2f s, t; dfloat const scale = 1.0f / ((2.f * lum.radius()) - distToLum); if(!R_GenerateTexCoords(s, t, pointOnPlane, scale, scale * 1.08f, topLeft, bottomRight, surface.tangentMatrix())) return false; de::zap(projected); projected.texture = tex; projected.topLeft = Vector2f(s[0], t[0]); projected.bottomRight = Vector2f(s[1], t[1]); projected.color = Vector4f(Rend_LuminousColor(lum.color(), luma), blendFactor); return true; } static bool projectPlaneGlow(Vector3d const &topLeft, Vector3d const &bottomRight, Plane const &plane, Vector3d const &pointOnPlane, dfloat blendFactor, ProjectedTextureData &projected) { if(blendFactor < OMNILIGHT_SURFACE_LUMINOSITY_ATTRIBUTION_MIN) return false; Surface const &surface = plane.surface(); Vector3f color; dfloat intensity = surface.glow(color); // Is the material glowing at this moment? if(intensity < .05f) return false; coord_t glowHeight = Rend_PlaneGlowHeight(intensity); if(glowHeight < 2) return false; // Not too small! // Calculate coords. dfloat bottom, top; if(surface.normal().z < 0) { // Cast downward. bottom = (pointOnPlane.z - topLeft.z) / glowHeight; top = bottom + (topLeft.z - bottomRight.z) / glowHeight; } else { // Cast upward. top = (bottomRight.z - pointOnPlane.z) / glowHeight; bottom = top + (topLeft.z - bottomRight.z) / glowHeight; } // Within range on the Z axis? if(!(bottom <= 1 || top >= 0)) return false; de::zap(projected); projected.texture = GL_PrepareLSTexture(LST_GRADIENT); projected.topLeft = Vector2f(0, bottom); projected.bottomRight = Vector2f(1, top); projected.color = Vector4f(Rend_LuminousColor(color, intensity), blendFactor); return true; } static bool projectShadow(Vector3d const &topLeft, Vector3d const &bottomRight, mobj_t const &mob, Surface const &surface, dfloat blendFactor, ProjectedTextureData &projected) { static Vector3f const black; // shadows are black coord_t mobOrigin[3]; Mobj_OriginSmoothed(const_cast(&mob), mobOrigin); // Is this too far? coord_t distanceFromViewer = 0; if(shadowMaxDistance > 0) { distanceFromViewer = Rend_PointDist2D(mobOrigin); if(distanceFromViewer > shadowMaxDistance) return LoopContinue; } // Should this mobj even have a shadow? dfloat shadowStrength = Mobj_ShadowStrength(mob) * ::shadowFactor; if(::usingFog) shadowStrength /= 2; if(shadowStrength <= 0) return LoopContinue; coord_t shadowRadius = Mobj_ShadowRadius(mob); if(shadowRadius > ::shadowMaxRadius) shadowRadius = ::shadowMaxRadius; if(shadowRadius <= 0) return LoopContinue; mobOrigin[2] -= mob.floorClip; if(mob.ddFlags & DDMF_BOB) mobOrigin[2] -= Mobj_BobOffset(mob); coord_t mobHeight = mob.height; if(!mobHeight) mobHeight = 1; // If this were a light this is where we would check whether the origin is on // the right side of the surface. However this is a shadow and light is moving // in the opposite direction (inward toward the mobj's origin), therefore this // has "volume/depth". // Calculate 3D distance between surface and mobj. Vector3d point = R_ClosestPointOnPlane(surface.tangentMatrix().column(2)/*normal*/, topLeft, mobOrigin); coord_t distFromSurface = (Vector3d(mobOrigin) - Vector3d(point)).length(); // Too far above or below the shadowed surface? if(distFromSurface > mob.height) return false; if(mobOrigin[2] + mob.height < point.z) return false; if(distFromSurface > shadowRadius) return false; // Calculate the final strength of the shadow's attribution to the surface. shadowStrength *= 1.5f - 1.5f * distFromSurface / shadowRadius; // Fade at half mobj height for smooth fade out when embedded in the surface. coord_t halfMobjHeight = mobHeight / 2; if(distFromSurface > halfMobjHeight) { shadowStrength *= 1 - (distFromSurface - halfMobjHeight) / (mobHeight - halfMobjHeight); } // Fade when nearing the maximum distance? shadowStrength *= Rend_ShadowAttenuationFactor(distanceFromViewer); shadowStrength *= blendFactor; // Would this shadow be seen? if(shadowStrength < SHADOW_SURFACE_LUMINOSITY_ATTRIBUTION_MIN) return false; // Project, counteracting aspect correction slightly. Vector2f s, t; dfloat const scale = 1.0f / ((2.f * shadowRadius) - distFromSurface); if(!R_GenerateTexCoords(s, t, point, scale, scale * 1.08f, topLeft, bottomRight, surface.tangentMatrix())) return false; de::zap(projected); projected.texture = GL_PrepareLSTexture(LST_DYNAMIC); projected.topLeft = Vector2f(s[0], t[0]); projected.bottomRight = Vector2f(s[1], t[1]); projected.color = Vector4f(black, shadowStrength); return true; } /** * @pre The coordinates of the given quad must be contained wholly within the subspoce * specified. This is due to an optimization within the lumobj management which separates * them by subspace. */ static void projectDynamics(Surface const &surface, dfloat glowStrength, Vector3d const &topLeft, Vector3d const &bottomRight, bool noLights, bool noShadows, bool sortLights, duint &lightListIdx, duint &shadowListIdx) { DENG2_ASSERT(curSubspace); if(levelFullBright) return; if(glowStrength >= 1) return; // lights? if(!noLights) { dfloat const blendFactor = 1; if(::useDynLights) { // Project all lumobjs affecting the given quad (world space), calculate // coordinates (in texture space) then store into a new list of projections. R_ForAllSubspaceLumContacts(*curSubspace, [&topLeft, &bottomRight, &surface , &blendFactor, &sortLights , &lightListIdx] (Lumobj &lum) { ProjectedTextureData projected; if(projectDynlight(topLeft, bottomRight, lum, surface, blendFactor, projected)) { rendSys().findSurfaceProjectionList(&lightListIdx, sortLights) << projected; // a copy is made } return LoopContinue; }); } if(::useGlowOnWalls && surface.parent().type() == DMU_SIDE && bottomRight.z < topLeft.z) { // Project all plane glows affecting the given quad (world space), calculate // coordinates (in texture space) then store into a new list of projections. SectorCluster const &cluster = curSubspace->cluster(); for(dint i = 0; i < cluster.visPlaneCount(); ++i) { Plane const &plane = cluster.visPlane(i); Vector3d const pointOnPlane(cluster.center(), plane.heightSmoothed()); ProjectedTextureData projected; if(projectPlaneGlow(topLeft, bottomRight, plane, pointOnPlane, blendFactor, projected)) { rendSys().findSurfaceProjectionList(&lightListIdx, sortLights) << projected; // a copy is made. } } } } // Shadows? if(!noShadows && ::useShadows) { // Glow inversely diminishes shadow strength. dfloat blendFactor = 1 - glowStrength; if(blendFactor >= SHADOW_SURFACE_LUMINOSITY_ATTRIBUTION_MIN) { blendFactor = de::clamp(0.f, blendFactor, 1.f); // Project all mobj shadows affecting the given quad (world space), calculate // coordinates (in texture space) then store into a new list of projections. R_ForAllSubspaceMobContacts(*curSubspace, [&topLeft, &bottomRight, &surface , &blendFactor, &shadowListIdx] (mobj_t &mob) { ProjectedTextureData projected; if(projectShadow(topLeft, bottomRight, mob, surface, blendFactor, projected)) { rendSys().findSurfaceProjectionList(&shadowListIdx) << projected; // a copy is made. } return LoopContinue; }); } } } /** * World light can both light and shade. Normal objects get more shade than light * (preventing them from ending up too bright compared to the ambient light). */ static bool lightWithWorldLight(Vector3d const & /*point*/, Vector3f const &ambientColor, bool starkLight, VectorLightData &vlight) { static Vector3f const worldLight(-.400891f, -.200445f, .601336f); de::zap(vlight); vlight.direction = worldLight; vlight.color = ambientColor; vlight.affectedByAmbient = false; vlight.approxDist = 0; if(starkLight) { vlight.lightSide = .35f; vlight.darkSide = .5f; vlight.offset = 0; } else { vlight.lightSide = .2f; vlight.darkSide = .8f; vlight.offset = .3f; } return true; } static bool lightWithLumobj(Vector3d const &point, Lumobj const &lum, VectorLightData &vlight) { Vector3d const lumCenter(lum.x(), lum.y(), lum.z() + lum.zOffset()); // Is this light close enough? ddouble const dist = M_ApproxDistance(M_ApproxDistance(lumCenter.x - point.x, lumCenter.y - point.y), point.z - lumCenter.z); dfloat intensity = 0; if(dist < Lumobj::radiusMax()) { intensity = de::clamp(0.f, dfloat(1 - dist / lum.radius()) * 2, 1.f); } if(intensity < .05f) return false; de::zap(vlight); vlight.direction = (lumCenter - point) / dist; vlight.color = lum.color() * intensity; vlight.affectedByAmbient = true; vlight.approxDist = dist; vlight.lightSide = 1; vlight.darkSide = 0; vlight.offset = 0; return true; } static bool lightWithPlaneGlow(Vector3d const &point, SectorCluster const &cluster, dint visPlaneIndex, VectorLightData &vlight) { Plane const &plane = cluster.visPlane(visPlaneIndex); Surface const &surface = plane.surface(); // Glowing at this moment? Vector3f glowColor; dfloat intensity = surface.glow(glowColor); if(intensity < .05f) return false; coord_t const glowHeight = Rend_PlaneGlowHeight(intensity); if(glowHeight < 2) return false; // Not too small! // In front of the plane? Vector3d const pointOnPlane(cluster.center(), plane.heightSmoothed()); ddouble const dist = (point - pointOnPlane).dot(surface.normal()); if(dist < 0) return false; intensity *= 1 - dist / glowHeight; if(intensity < .05f) return false; Vector3f const color = Rend_LuminousColor(glowColor, intensity); if(color == Vector3f()) return false; de::zap(vlight); vlight.direction = Vector3f(surface.normal().x, surface.normal().y, -surface.normal().z); vlight.color = color; vlight.affectedByAmbient = true; vlight.approxDist = dist; vlight.lightSide = 1; vlight.darkSide = 0; vlight.offset = 0.3f; return true; } duint Rend_CollectAffectingLights(Vector3d const &point, Vector3f const &ambientColor, ConvexSubspace *subspace, bool starkLight) { duint lightListIdx = 0; // Always apply an ambient world light. { VectorLightData vlight; if(lightWithWorldLight(point, ambientColor, starkLight, vlight)) { rendSys().findVectorLightList(&lightListIdx) << vlight; // a copy is made. } } // Add extra light by interpreting nearby sources. if(subspace) { // Interpret lighting from luminous-objects near the origin and which // are in contact the specified subspace and add them to the identified list. R_ForAllSubspaceLumContacts(*subspace, [&point, &lightListIdx] (Lumobj &lum) { VectorLightData vlight; if(lightWithLumobj(point, lum, vlight)) { rendSys().findVectorLightList(&lightListIdx) << vlight; // a copy is made. } return LoopContinue; }); // Interpret vlights from glowing planes at the origin in the specfified // subspace and add them to the identified list. SectorCluster const &cluster = subspace->cluster(); for(dint i = 0; i < cluster.sector().planeCount(); ++i) { VectorLightData vlight; if(lightWithPlaneGlow(point, cluster, i, vlight)) { rendSys().findVectorLightList(&lightListIdx) << vlight; // a copy is made. } } } return lightListIdx; } /** * Fade the specified @a opacity value to fully transparent the closer the view * player is to the geometry. * * @note When the viewer is close enough we should NOT try to occlude with this * section in the angle clipper, otherwise HOM would occur when directly on top * of the wall (e.g., passing through an opaque waterfall). * * @return @c true= fading was applied (see above note), otherwise @c false. */ static bool nearFadeOpacity(WallEdge const &leftEdge, WallEdge const &rightEdge, dfloat &opacity) { if(Rend_EyeOrigin().y < leftEdge.bottom().z() || Rend_EyeOrigin().y > rightEdge.top().z()) return false; mobj_t const *mo = viewPlayer->shared.mo; Line const &line = leftEdge.mapLineSide().line(); coord_t linePoint[2] = { line.fromOrigin().x, line.fromOrigin().y }; coord_t lineDirection[2] = { line.direction().x, line.direction().y }; vec2d_t result; ddouble pos = V2d_ProjectOnLine(result, mo->origin, linePoint, lineDirection); if(!(pos > 0 && pos < 1)) return false; coord_t const maxDistance = Mobj_Radius(*mo) * .8f; auto delta = Vector2d(result) - Vector2d(mo->origin); coord_t distance = delta.length(); if(de::abs(distance) > maxDistance) return false; if(distance > 0) { opacity = (opacity / maxDistance) * distance; opacity = de::clamp(0.f, opacity, 1.f); } return true; } /** * The DOOM lighting model applies a sector light level delta when drawing * walls based on their 2D world angle. * * @todo WallEdge should encapsulate. */ static dfloat calcLightLevelDelta(Vector3f const &normal) { return (1.0f / 255) * (normal.x * 18) * ::rendLightWallAngle; } static void wallSectionLightLevelDeltas(WallEdge const &leftEdge, WallEdge const &rightEdge, dfloat &leftDelta, dfloat &rightDelta) { leftDelta = calcLightLevelDelta(leftEdge.normal()); if(leftEdge.normal() == rightEdge.normal()) { rightDelta = leftDelta; } else { rightDelta = calcLightLevelDelta(rightEdge.normal()); // Linearly interpolate to find the light level delta values for the // vertical edges of this wall section. coord_t const lineLength = leftEdge.mapLineSide().line().length(); coord_t const sectionOffset = leftEdge.mapLineSideOffset(); coord_t const sectionWidth = de::abs(Vector2d(rightEdge.origin() - leftEdge.origin()).length()); dfloat deltaDiff = rightDelta - leftDelta; rightDelta = leftDelta + ((sectionOffset + sectionWidth) / lineLength) * deltaDiff; leftDelta += (sectionOffset / lineLength) * deltaDiff; } } static void writeWallSection(HEdge &hedge, dint section, bool *retWroteOpaque = nullptr, coord_t *retBottomZ = nullptr, coord_t *retTopZ = nullptr) { SectorCluster &cluster = curSubspace->cluster(); auto &segment = hedge.mapElementAs(); DENG2_ASSERT(segment.isFrontFacing() && segment.lineSide().hasSections()); if(retWroteOpaque) *retWroteOpaque = false; if(retBottomZ) *retBottomZ = 0; if(retTopZ) *retTopZ = 0; LineSide &side = segment.lineSide(); Surface &surface = side.surface(section); // Skip nearly transparent surfaces. dfloat opacity = surface.opacity(); if(opacity < .001f) return; // Determine which Material to use. Material *material = Rend_ChooseMapSurfaceMaterial(surface); // A drawable material is required. if(!material || !material->isDrawable()) return; // Generate edge geometries. auto const wallSpec = WallSpec::fromMapSide(side, section); WallEdge leftEdge(wallSpec, hedge, Line::From); WallEdge rightEdge(wallSpec, hedge, Line::To); // Do the edge geometries describe a valid polygon? if(!leftEdge.isValid() || !rightEdge.isValid() || de::fequal(leftEdge.bottom().z(), rightEdge.top().z())) return; // Apply a fade out when the viewer is near to this geometry? bool didNearFade = false; if(wallSpec.flags.testFlag(WallSpec::NearFade)) { didNearFade = nearFadeOpacity(leftEdge, rightEdge, opacity); } bool const skyMasked = material->isSkyMasked() && !devRendSkyMode; bool const twoSidedMiddle = (wallSpec.section == LineSide::Middle && !side.considerOneSided()); MaterialAnimator &matAnimator = material->getAnimator(Rend_MapSurfaceMaterialSpec()); Vector2f const materialScale = surface.materialScale(); rendworldpoly_params_t parm; zap(parm); Vector3f materialOrigin = leftEdge.materialOrigin(); Vector3d topLeft = leftEdge.top().origin(); Vector3d bottomRight = rightEdge.bottom().origin(); parm.skyMasked = skyMasked; parm.mapElement = &segment; parm.geomGroup = wallSpec.section; parm.topLeft = &topLeft; parm.bottomRight = &bottomRight; parm.forceOpaque = wallSpec.flags.testFlag(WallSpec::ForceOpaque); parm.alpha = parm.forceOpaque? 1 : opacity; parm.surfaceTangentMatrix = &surface.tangentMatrix(); // Calculate the light level deltas for this wall section? if(!wallSpec.flags.testFlag(WallSpec::NoLightDeltas)) { wallSectionLightLevelDeltas(leftEdge, rightEdge, parm.surfaceLightLevelDL, parm.surfaceLightLevelDR); } parm.blendMode = BM_NORMAL; parm.materialOrigin = &materialOrigin; parm.materialScale = &materialScale; parm.isWall = true; parm.wall.sectionWidth = de::abs(Vector2d(rightEdge.origin() - leftEdge.origin()).length()); parm.wall.leftEdge = &leftEdge; parm.wall.rightEdge = &rightEdge; if(!parm.skyMasked) { if(glowFactor > .0001f) { if(material == surface.materialPtr()) { parm.glowing = matAnimator.glowStrength(); } else { Material *actualMaterial = surface.hasMaterial()? surface.materialPtr() : &resSys().material(de::Uri("System", Path("missing"))); parm.glowing = actualMaterial->getAnimator(Rend_MapSurfaceMaterialSpec()).glowStrength(); } parm.glowing *= ::glowFactor; } projectDynamics(surface, parm.glowing, *parm.topLeft, *parm.bottomRight, wallSpec.flags.testFlag(WallSpec::NoDynLights), wallSpec.flags.testFlag(WallSpec::NoDynShadows), wallSpec.flags.testFlag(WallSpec::SortDynLights), parm.lightListIdx, parm.shadowListIdx); if(twoSidedMiddle) { parm.blendMode = surface.blendMode(); if(parm.blendMode == BM_NORMAL && noSpriteTrans) parm.blendMode = BM_ZEROALPHA; // "no translucency" mode } side.chooseSurfaceTintColors(wallSpec.section, &parm.surfaceColor, &parm.wall.surfaceColor2); } // // Geometry write/drawing begins. // if(twoSidedMiddle && side.sectorPtr() != &cluster.sector()) { // Temporarily modify the draw state. curSectorLightColor = Rend_AmbientLightColor(side.sector()); curSectorLightLevel = side.sector().lightLevel(); } // Allocate position coordinates. Vector3f *posCoords; if(leftEdge.divisionCount() || rightEdge.divisionCount()) { // Two fans plus edge divisions. posCoords = R_AllocRendVertices(3 + leftEdge.divisionCount() + 3 + rightEdge.divisionCount()); } else { // One quad. posCoords = R_AllocRendVertices(4); } posCoords[0] = leftEdge.bottom().origin(); posCoords[1] = leftEdge.top().origin(); posCoords[2] = rightEdge.bottom().origin(); posCoords[3] = rightEdge.top().origin(); // Draw this section. bool wroteOpaque = renderWorldPoly(posCoords, 4, parm, matAnimator); if(wroteOpaque) { // Render FakeRadio for this section? if(!wallSpec.flags.testFlag(WallSpec::NoFakeRadio) && !skyMasked && !(parm.glowing > 0) && curSectorLightLevel > 0) { Rend_RadioUpdateForLineSide(side); // Determine the shadow properties. /// @todo Make cvars out of constants. dfloat shadowSize = 2 * (8 + 16 - curSectorLightLevel * 16); dfloat shadowDark = Rend_RadioCalcShadowDarkness(curSectorLightLevel); Rend_RadioWallSection(leftEdge, rightEdge, shadowDark, shadowSize); } } if(twoSidedMiddle && side.sectorPtr() != &cluster.sector()) { // Undo temporary draw state changes. Vector4f const color = cluster.lightSourceColorfIntensity(); curSectorLightColor = color.toVector3f(); curSectorLightLevel = color.w; } R_FreeRendVertices(posCoords); if(retWroteOpaque) *retWroteOpaque = wroteOpaque && !didNearFade; if(retBottomZ) *retBottomZ = leftEdge.bottom().z(); if(retTopZ) *retTopZ = rightEdge.top().z(); } /** * Prepare a trifan geometry according to the edges of the current subspace. * If a fan base HEdge has been chosen it will be used as the center of the * trifan, else the mid point of this leaf will be used instead. * * @param direction Vertex winding direction. * @param height Z map space height coordinate to be set for each vertex. * @param verts Built position coordinates are written here. It is the * responsibility of the caller to release this storage with * @ref R_FreeRendVertices() when done. * * @return Number of built vertices. */ static duint buildSubspacePlaneGeometry(ClockDirection direction, coord_t height, Vector3f **verts) { DENG2_ASSERT(verts); Face const &poly = curSubspace->poly(); HEdge *fanBase = curSubspace->fanBase(); duint const totalVerts = poly.hedgeCount() + (!fanBase? 2 : 0); *verts = R_AllocRendVertices(totalVerts); duint n = 0; if(!fanBase) { (*verts)[n] = Vector3f(poly.center(), height); n++; } // Add the vertices for each hedge. HEdge *baseNode = fanBase? fanBase : poly.hedge(); HEdge *node = baseNode; do { (*verts)[n] = Vector3f(node->origin(), height); n++; } while((node = &node->neighbor(direction)) != baseNode); // The last vertex is always equal to the first. if(!fanBase) { (*verts)[n] = Vector3f(poly.hedge()->origin(), height); } return totalVerts; } static void writeSubspacePlane(Plane &plane) { Face const &poly = curSubspace->poly(); Surface const &surface = plane.surface(); // Skip nearly transparent surfaces. dfloat const opacity = surface.opacity(); if(opacity < .001f) return; // Determine which Material to use. Material *material = Rend_ChooseMapSurfaceMaterial(surface); // A drawable material is required. if(!material) return; if(!material->isDrawable()) return; // Skip planes with a sky-masked material? if(!devRendSkyMode) { if(surface.hasSkyMaskedMaterial() && plane.indexInSector() <= Sector::Ceiling) { return; // Not handled here (drawn with the mask geometry). } } MaterialAnimator &matAnimator = material->getAnimator(Rend_MapSurfaceMaterialSpec()); Vector2f materialOrigin = curSubspace->worldGridOffset() // Align to the worldwide grid. + surface.materialOriginSmoothed(); // Add the Y offset to orient the Y flipped material. /// @todo fixme: What is this meant to do? -ds if(plane.isSectorCeiling()) { materialOrigin.y -= poly.aaBox().maxY - poly.aaBox().minY; } materialOrigin.y = -materialOrigin.y; Vector2f const materialScale = surface.materialScale(); // Set the texture origin, Y is flipped for the ceiling. Vector3d topLeft(poly.aaBox().minX, poly.aaBox().arvec2[plane.isSectorFloor()? 1 : 0][1], plane.heightSmoothed()); Vector3d bottomRight(poly.aaBox().maxX, poly.aaBox().arvec2[plane.isSectorFloor()? 0 : 1][1], plane.heightSmoothed()); rendworldpoly_params_t parm; de::zap(parm); parm.mapElement = curSubspace; parm.geomGroup = plane.indexInSector(); parm.topLeft = &topLeft; parm.bottomRight = &bottomRight; parm.materialOrigin = &materialOrigin; parm.materialScale = &materialScale; parm.surfaceLightLevelDL = parm.surfaceLightLevelDR = 0; parm.surfaceColor = &surface.tintColor(); parm.surfaceTangentMatrix = &surface.tangentMatrix(); if(material->isSkyMasked()) { // In devRendSkyMode mode we render all polys destined for the // skymask as regular world polys (with a few obvious properties). if(devRendSkyMode) { parm.blendMode = BM_NORMAL; parm.forceOpaque = true; } else { // We'll mask this. parm.skyMasked = true; } } else if(plane.indexInSector() <= Sector::Ceiling) { parm.blendMode = BM_NORMAL; parm.forceOpaque = true; } else { parm.blendMode = surface.blendMode(); if(parm.blendMode == BM_NORMAL && noSpriteTrans) { parm.blendMode = BM_ZEROALPHA; // "no translucency" mode } parm.alpha = surface.opacity(); } if(!parm.skyMasked) { if(glowFactor > .0001f) { if(material == surface.materialPtr()) { parm.glowing = matAnimator.glowStrength(); } else { Material *actualMaterial = surface.hasMaterial()? surface.materialPtr() : &resSys().material(de::Uri("System", Path("missing"))); parm.glowing = actualMaterial->getAnimator(Rend_MapSurfaceMaterialSpec()).glowStrength(); } parm.glowing *= ::glowFactor; } projectDynamics(surface, parm.glowing, *parm.topLeft, *parm.bottomRight, false /*do light*/, false /*do shadow*/, false /*don't sort*/, parm.lightListIdx, parm.shadowListIdx); } // // Geometry write/drawing begins. // if(&plane.sector() != &curSubspace->sector()) { // Temporarily modify the draw state. curSectorLightColor = Rend_AmbientLightColor(plane.sector()); curSectorLightLevel = plane.sector().lightLevel(); } // Allocate position coordinates. Vector3f *posCoords; duint vertCount = buildSubspacePlaneGeometry((plane.isSectorCeiling())? Anticlockwise : Clockwise, plane.heightSmoothed(), &posCoords); // Draw this section. renderWorldPoly(posCoords, vertCount, parm, matAnimator); if(&plane.sector() != &curSubspace->sector()) { // Undo temporary draw state changes. Vector4f const color = curSubspace->cluster().lightSourceColorfIntensity(); curSectorLightColor = color.toVector3f(); curSectorLightLevel = color.w; } R_FreeRendVertices(posCoords); } static void writeSkyMaskStrip(dint vertCount, Vector3f const *posCoords, Vector2f const *texCoords, Material *material) { DENG2_ASSERT(posCoords); if(!devRendSkyMode) { ClientApp::renderSystem().drawLists() .find(DrawListSpec(SkyMaskGeom)) .write(gl::TriangleStrip, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, vertCount, posCoords); } else { DENG2_ASSERT(texCoords); DrawListSpec listSpec; listSpec.group = UnlitGeom; if(renderTextures != 2) { DENG2_ASSERT(material); MaterialAnimator &matAnimator = material->getAnimator(Rend_MapSurfaceMaterialSpec()); // Ensure we've up to date info about the material. matAnimator.prepare(); // Map RTU configuration from the sky surface material. listSpec.texunits[TU_PRIMARY] = matAnimator.texUnit(MaterialAnimator::TU_LAYER0); listSpec.texunits[TU_PRIMARY_DETAIL] = matAnimator.texUnit(MaterialAnimator::TU_DETAIL); listSpec.texunits[TU_INTER] = matAnimator.texUnit(MaterialAnimator::TU_LAYER0_INTER); listSpec.texunits[TU_INTER_DETAIL] = matAnimator.texUnit(MaterialAnimator::TU_DETAIL_INTER); } ClientApp::renderSystem().drawLists() .find(listSpec) .write(gl::TriangleStrip, BM_NORMAL, listSpec.unit(TU_PRIMARY ).scale, listSpec.unit(TU_PRIMARY ).offset, listSpec.unit(TU_PRIMARY_DETAIL).scale, listSpec.unit(TU_PRIMARY_DETAIL).offset, 0, vertCount, posCoords, 0, texCoords); } } static void writeSubspaceSkyMaskStrips(SkyFixEdge::FixType fixType) { // Determine strip generation behavior. ClockDirection const direction = Clockwise; bool const buildTexCoords = CPP_BOOL(devRendSkyMode); bool const splitOnMaterialChange = (devRendSkyMode && renderTextures != 2); // Configure the strip builder wrt vertex attributes. TriangleStripBuilder stripBuilder(buildTexCoords); // Configure the strip build state (we'll most likely need to break // edge loop into multiple strips). HEdge *startNode = nullptr; coord_t startZBottom = 0; coord_t startZTop = 0; Material *startMaterial = nullptr; dfloat startMaterialOffset = 0; // Determine the relative sky plane (for monitoring material changes). dint relPlane = fixType == SkyFixEdge::Upper? Sector::Ceiling : Sector::Floor; // Begin generating geometry. HEdge *base = curSubspace->poly().hedge(); HEdge *hedge = base; forever { // Are we monitoring material changes? Material *skyMaterial = nullptr; if(splitOnMaterialChange) { skyMaterial = hedge->face().mapElementAs() .cluster().visPlane(relPlane).surface().materialPtr(); } // Add a first (left) edge to the current strip? if(!startNode && hedge->hasMapElement()) { startMaterialOffset = hedge->mapElementAs().lineSideOffset(); // Prepare the edge geometry SkyFixEdge skyEdge(*hedge, fixType, (direction == Anticlockwise)? Line::To : Line::From, startMaterialOffset); if(skyEdge.isValid() && skyEdge.bottom().z() < skyEdge.top().z()) { // A new strip begins. stripBuilder.begin(direction); stripBuilder << skyEdge; // Update the strip build state. startNode = hedge; startZBottom = skyEdge.bottom().z(); startZTop = skyEdge.top().z(); startMaterial = skyMaterial; } } bool beginNewStrip = false; // Add the i'th (right) edge to the current strip? if(startNode) { // Stop if we've reached a "null" edge. bool endStrip = false; if(hedge->hasMapElement()) { startMaterialOffset += hedge->mapElementAs().length() * (direction == Anticlockwise? -1 : 1); // Prepare the edge geometry SkyFixEdge skyEdge(*hedge, fixType, (direction == Anticlockwise)? Line::From : Line::To, startMaterialOffset); if(!(skyEdge.isValid() && skyEdge.bottom().z() < skyEdge.top().z())) { endStrip = true; } // Must we split the strip here? else if(hedge != startNode && (!de::fequal(skyEdge.bottom().z(), startZBottom) || !de::fequal(skyEdge.top().z(), startZTop) || (splitOnMaterialChange && skyMaterial != startMaterial))) { endStrip = true; beginNewStrip = true; // We'll continue from here. } else { // Extend the strip geometry. stripBuilder << skyEdge; } } else { endStrip = true; } if(endStrip || &hedge->neighbor(direction) == base) { // End the current strip. startNode = nullptr; // Take ownership of the built geometry. PositionBuffer *positions = nullptr; TexCoordBuffer *texcoords = nullptr; dint numVerts = stripBuilder.take(&positions, &texcoords); // Write the strip geometry to the render lists. writeSkyMaskStrip(numVerts, positions->constData(), texcoords? texcoords->constData() : nullptr, startMaterial); delete positions; delete texcoords; } } // Start a new strip from the current node? if(beginNewStrip) continue; // On to the next node. hedge = &hedge->neighbor(direction); // Are we done? if(hedge == base) break; } } /** * @defgroup skyCapFlags Sky Cap Flags * @ingroup flags */ ///@{ #define SKYCAP_LOWER 0x1 #define SKYCAP_UPPER 0x2 ///@} static coord_t skyPlaneZ(dint skyCap) { SectorCluster &cluster = curSubspace->cluster(); dint const relPlane = (skyCap & SKYCAP_UPPER)? Sector::Ceiling : Sector::Floor; if(!P_IsInVoid(viewPlayer)) { return cluster.sector().map().skyFix(relPlane == Sector::Ceiling); } return cluster.visPlane(relPlane).heightSmoothed(); } /// @param skyCap @ref skyCapFlags. static void writeSubspaceSkyMaskCap(dint skyCap) { // Caps are unnecessary in sky debug mode (will be drawn as regular planes). if(devRendSkyMode) return; if(!skyCap) return; Vector3f *posCoords; duint vertCount = buildSubspacePlaneGeometry((skyCap & SKYCAP_UPPER)? Anticlockwise : Clockwise, skyPlaneZ(skyCap), &posCoords); ClientApp::renderSystem().drawLists() .find(DrawListSpec(SkyMaskGeom)) .write(gl::TriangleFan, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, vertCount, posCoords); R_FreeRendVertices(posCoords); } /// @param skyCap @ref skyCapFlags static void writeSubspaceSkyMask(dint skyCap = SKYCAP_LOWER | SKYCAP_UPPER) { SectorCluster &cluster = curSubspace->cluster(); // Any work to do? // Sky caps are only necessary in sectors with sky-masked planes. if((skyCap & SKYCAP_LOWER) && !cluster.visFloor().surface().hasSkyMaskedMaterial()) { skyCap &= ~SKYCAP_LOWER; } if((skyCap & SKYCAP_UPPER) && !cluster.visCeiling().surface().hasSkyMaskedMaterial()) { skyCap &= ~SKYCAP_UPPER; } if(!skyCap) return; // Lower? if(skyCap & SKYCAP_LOWER) { writeSubspaceSkyMaskStrips(SkyFixEdge::Lower); writeSubspaceSkyMaskCap(SKYCAP_LOWER); } // Upper? if(skyCap & SKYCAP_UPPER) { writeSubspaceSkyMaskStrips(SkyFixEdge::Upper); writeSubspaceSkyMaskCap(SKYCAP_UPPER); } } static bool coveredOpenRange(HEdge &hedge, coord_t middleBottomZ, coord_t middleTopZ, bool wroteOpaqueMiddle) { LineSide const &front = hedge.mapElementAs().lineSide(); if(front.considerOneSided()) { return wroteOpaqueMiddle; } /// @todo fixme: This additional test should not be necessary. For the obove /// test to fail and this to pass means that the geometry produced by the BSP /// builder is not correct (see: eternall.wad MAP10; note mapping errors). if(!hedge.twin().hasFace()) { return wroteOpaqueMiddle; } SectorCluster const &cluster = hedge.face().mapElementAs().cluster(); SectorCluster const &backCluster = hedge.twin().face().mapElementAs().cluster(); coord_t const ffloor = cluster.visFloor().heightSmoothed(); coord_t const fceil = cluster.visCeiling().heightSmoothed(); coord_t const bfloor = backCluster.visFloor().heightSmoothed(); coord_t const bceil = backCluster.visCeiling().heightSmoothed(); bool middleCoversOpening = false; if(wroteOpaqueMiddle) { coord_t xbottom = de::max(bfloor, ffloor); coord_t xtop = de::min(bceil, fceil); Surface const &middle = front.middle(); xbottom += middle.materialOriginSmoothed().y; xtop += middle.materialOriginSmoothed().y; middleCoversOpening = (middleTopZ >= xtop && middleBottomZ <= xbottom); } if(wroteOpaqueMiddle && middleCoversOpening) return true; if( (bceil <= ffloor && (front.top().hasMaterial() || front.middle().hasMaterial())) || (bfloor >= fceil && (front.bottom().hasMaterial() || front.middle().hasMaterial()))) { Surface const &ffloorSurface = cluster.visFloor().surface(); Surface const &fceilSurface = cluster.visCeiling().surface(); Surface const &bfloorSurface = backCluster.visFloor().surface(); Surface const &bceilSurface = backCluster.visCeiling().surface(); // A closed gap? if(de::fequal(fceil, bfloor)) { return (bceil <= bfloor) || !(fceilSurface.hasSkyMaskedMaterial() && bceilSurface.hasSkyMaskedMaterial()); } if(de::fequal(ffloor, bceil)) { return (bfloor >= bceil) || !(ffloorSurface.hasSkyMaskedMaterial() && bfloorSurface.hasSkyMaskedMaterial()); } return true; } /// @todo Is this still necessary? if(bceil <= bfloor || (!(bceil - bfloor > 0) && bfloor > ffloor && bceil < fceil && front.top().hasMaterial() && front.bottom().hasMaterial())) { // A zero height back segment return true; } return false; } static void writeAllWallSections(HEdge *hedge) { // Edges without a map line segment implicitly have no surfaces. if(!hedge || !hedge->hasMapElement()) return; // We are only interested in front facing segments with sections. LineSideSegment &seg = hedge->mapElementAs(); if(!seg.isFrontFacing() || !seg.lineSide().hasSections()) return; // Done here because of the logic of doom.exe wrt the automap. reportWallSectionDrawn(seg.line()); bool wroteOpaqueMiddle = false; coord_t middleBottomZ = 0, middleTopZ = 0; writeWallSection(*hedge, LineSide::Bottom); writeWallSection(*hedge, LineSide::Top); writeWallSection(*hedge, LineSide::Middle, &wroteOpaqueMiddle, &middleBottomZ, &middleTopZ); // We can occlude the angle range defined by the X|Y origins of the // line segment if the open range has been covered (when the viewer // is not in the void). if(!P_IsInVoid(viewPlayer) && coveredOpenRange(*hedge, middleBottomZ, middleTopZ, wroteOpaqueMiddle)) { rendSys().angleClipper().addRangeFromViewRelPoints(hedge->origin(), hedge->twin().origin()); } } static void writeSubspaceWallSections() { HEdge *base = curSubspace->poly().hedge(); HEdge *hedge = base; do { writeAllWallSections(hedge); } while((hedge = &hedge->next()) != base); curSubspace->forAllExtraMeshes([] (Mesh &mesh) { for(HEdge *hedge : mesh.hedges()) { writeAllWallSections(hedge); } return LoopContinue; }); curSubspace->forAllPolyobjs([] (Polyobj &pob) { for(HEdge *hedge : pob.mesh().hedges()) { writeAllWallSections(hedge); } return LoopContinue; }); } static void writeSubspacePlanes() { SectorCluster &cluster = curSubspace->cluster(); for(dint i = 0; i < cluster.visPlaneCount(); ++i) { Plane &plane = cluster.visPlane(i); // Skip planes facing away from the viewer. Vector3d const pointOnPlane(cluster.center(), plane.heightSmoothed()); if((eyeOrigin - pointOnPlane).dot(plane.surface().normal()) < 0) continue; writeSubspacePlane(plane); } } static void markFrontFacingWalls(HEdge *hedge) { if(!hedge || !hedge->hasMapElement()) return; auto &seg = hedge->mapElementAs(); // Which way is the line segment facing? seg.setFrontFacing(viewFacingDot(hedge->origin(), hedge->twin().origin()) >= 0); } static void markSubspaceFrontFacingWalls() { HEdge *base = curSubspace->poly().hedge(); HEdge *hedge = base; do { markFrontFacingWalls(hedge); } while((hedge = &hedge->next()) != base); curSubspace->forAllExtraMeshes([] (Mesh &mesh) { for(HEdge *hedge : mesh.hedges()) { markFrontFacingWalls(hedge); } return LoopContinue; }); curSubspace->forAllPolyobjs([] (Polyobj &pob) { for(HEdge *hedge : pob.mesh().hedges()) { markFrontFacingWalls(hedge); } return LoopContinue; }); } static inline bool canOccludeEdgeBetweenPlanes(Plane &frontPlane, Plane const &backPlane) { // Do not create an occlusion between two sky-masked planes. // Only because the open range does not account for the sky plane height? -ds return !(frontPlane.surface().hasSkyMaskedMaterial() && backPlane.surface().hasSkyMaskedMaterial()); } /** * Add angle clipper occlusion ranges for the edges of the current subspace. */ static void occludeSubspace(bool frontFacing) { if(devNoCulling) return; if(P_IsInVoid(viewPlayer)) return; AngleClipper &clipper = rendSys().angleClipper(); SectorCluster &cluster = curSubspace->cluster(); HEdge *base = curSubspace->poly().hedge(); HEdge *hedge = base; do { // Edges without a line segment can never occlude. if(!hedge->hasMapElement()) continue; auto &seg = hedge->mapElementAs(); // Edges without line segment surface sections can never occlude. if(!seg.lineSide().hasSections()) continue; // Only front-facing edges can occlude. if(frontFacing != seg.isFrontFacing()) continue; // Occlusions should only happen where two sectors meet. if(!hedge->hasTwin() || !hedge->twin().hasFace() || !hedge->twin().face().hasMapElement()) continue; SectorCluster &backCluster = hedge->twin().face().mapElementAs().cluster(); // Determine the opening between the visual sector planes at this edge. coord_t openBottom; if(backCluster.visFloor().heightSmoothed() > cluster.visFloor().heightSmoothed()) { openBottom = backCluster.visFloor().heightSmoothed(); } else { openBottom = cluster.visFloor().heightSmoothed(); } coord_t openTop; if(backCluster.visCeiling().heightSmoothed() < cluster.visCeiling().heightSmoothed()) { openTop = backCluster.visCeiling().heightSmoothed(); } else { openTop = cluster.visCeiling().heightSmoothed(); } // Choose start and end vertexes so that it's facing forward. Vertex const &from = frontFacing? hedge->vertex() : hedge->twin().vertex(); Vertex const &to = frontFacing? hedge->twin().vertex() : hedge->vertex(); // Does the floor create an occlusion? if(((openBottom > cluster.visFloor().heightSmoothed() && Rend_EyeOrigin().y <= openBottom) || (openBottom > backCluster.visFloor().heightSmoothed() && Rend_EyeOrigin().y >= openBottom)) && canOccludeEdgeBetweenPlanes(cluster.visFloor(), backCluster.visFloor())) { clipper.addViewRelOcclusion(from.origin(), to.origin(), openBottom, false); } // Does the ceiling create an occlusion? if(((openTop < cluster.visCeiling().heightSmoothed() && Rend_EyeOrigin().y >= openTop) || (openTop < backCluster.visCeiling().heightSmoothed() && Rend_EyeOrigin().y <= openTop)) && canOccludeEdgeBetweenPlanes(cluster.visCeiling(), backCluster.visCeiling())) { clipper.addViewRelOcclusion(from.origin(), to.origin(), openTop, true); } } while((hedge = &hedge->next()) != base); } static void clipSubspaceLumobjs() { DENG2_ASSERT(curSubspace); curSubspace->forAllLumobjs([] (Lumobj &lob) { R_ViewerClipLumobj(&lob); return LoopContinue; }); } /** * In the situation where a subspace contains both lumobjs and a polyobj, lumobjs * must be clipped more carefully. Here we check if the line of sight intersects * any of the polyobj half-edges facing the viewer. */ static void clipSubspaceLumobjsBySight() { // Any work to do? DENG2_ASSERT(curSubspace); if(!curSubspace->polyobjCount()) return; curSubspace->forAllLumobjs([] (Lumobj &lob) { R_ViewerClipLumobjBySight(&lob, curSubspace); return LoopContinue; }); } /// If not front facing this is no-op. static void clipFrontFacingWalls(HEdge *hedge) { if(!hedge || !hedge->hasMapElement()) return; LineSideSegment &seg = hedge->mapElementAs(); if(seg.isFrontFacing()) { if(!rendSys().angleClipper().checkRangeFromViewRelPoints(hedge->origin(), hedge->twin().origin())) { seg.setFrontFacing(false); } } } static void clipSubspaceFrontFacingWalls() { HEdge *base = curSubspace->poly().hedge(); HEdge *hedge = base; do { clipFrontFacingWalls(hedge); } while((hedge = &hedge->next()) != base); curSubspace->forAllExtraMeshes([] (Mesh &mesh) { for(HEdge *hedge : mesh.hedges()) { clipFrontFacingWalls(hedge); } return LoopContinue; }); curSubspace->forAllPolyobjs([] (Polyobj &pob) { for(HEdge *hedge : pob.mesh().hedges()) { clipFrontFacingWalls(hedge); } return LoopContinue; }); } static void projectSubspaceSprites() { // Do not use validCount because other parts of the renderer may change it. if(curSubspace->lastSpriteProjectFrame() == R_FrameCount()) return; // Already added. R_ForAllSubspaceMobContacts(*curSubspace, [] (mobj_t &mob) { SectorCluster &cluster = curSubspace->cluster(); if(mob.addFrameCount != R_FrameCount()) { mob.addFrameCount = R_FrameCount(); R_ProjectSprite(mob); // Hack: Sprites have a tendency to extend into the ceiling in // sky sectors. Here we will raise the skyfix dynamically, to make sure // that no sprites get clipped by the sky. if(cluster.visCeiling().surface().hasSkyMaskedMaterial()) { if(Sprite *sprite = Mobj_Sprite(mob)) { if(sprite->hasViewAngle(0)) { Material *material = sprite->viewAngle(0).material; if(!(mob.dPlayer && (mob.dPlayer->flags & DDPF_CAMERA)) && mob.origin[2] <= cluster.visCeiling().heightSmoothed() && mob.origin[2] >= cluster.visFloor().heightSmoothed()) { coord_t visibleTop = mob.origin[2] + material->height(); if(visibleTop > cluster.sector().map().skyFixCeiling()) { // Raise skyfix ceiling. cluster.sector().map().setSkyFixCeiling(visibleTop + 16/*leeway*/); } } } } } } return LoopContinue; }); curSubspace->setLastSpriteProjectFrame(R_FrameCount()); } /** * @pre Assumes the subspace is at least partially visible. */ static void drawCurrentSubspace() { Sector §or = curSubspace->sector(); // Mark the leaf as visible for this frame. R_ViewerSubspaceMarkVisible(*curSubspace); markSubspaceFrontFacingWalls(); // Perform contact spreading for this map region. sector.map().spreadAllContacts(curSubspace->poly().aaBox()); Rend_RadioSubspaceEdges(*curSubspace); // Before clip testing lumobjs (for halos), range-occlude the back facing edges. // After testing, range-occlude the front facing edges. Done before drawing wall // sections so that opening occlusions cut out unnecessary oranges. occludeSubspace(false /* back facing */); clipSubspaceLumobjs(); occludeSubspace(true /* front facing */); clipSubspaceFrontFacingWalls(); clipSubspaceLumobjsBySight(); // Mark generators in the sector visible. if(useParticles) { sector.map().forAllGeneratorsInSector(sector, [] (Generator &gen) { R_ViewerGeneratorMarkVisible(gen); return LoopContinue; }); } // Sprites for this subspace have to be drawn. // // Must be done BEFORE the wall segments of this subspace are added to the // clipper. Otherwise the sprites would get clipped by them, and that wouldn't // be right. // // Must be done AFTER the lumobjs have been clipped as this affects the projection // of halos. projectSubspaceSprites(); writeSubspaceSkyMask(); writeSubspaceWallSections(); writeSubspacePlanes(); } /** * Change the current subspace (updating any relevant draw state properties * accordingly). * * @param subspace The new subspace to make current. */ static void makeCurrent(ConvexSubspace &subspace) { bool clusterChanged = (!curSubspace || curSubspace->clusterPtr() != subspace.clusterPtr()); curSubspace = &subspace; // Update draw state. if(clusterChanged) { Vector4f const color = subspace.cluster().lightSourceColorfIntensity(); curSectorLightColor = color.toVector3f(); curSectorLightLevel = color.w; } } static void traverseBspTreeAndDrawSubspaces(Map::BspTree const *bspTree) { DENG2_ASSERT(bspTree); AngleClipper &clipper = rendSys().angleClipper(); while(!bspTree->isLeaf()) { // Descend deeper into the nodes. BspNode &bspNode = bspTree->userData()->as(); // Decide which side the view point is on. dint eyeSide = bspNode.partition().pointOnSide(eyeOrigin) < 0; // Recursively divide front space. traverseBspTreeAndDrawSubspaces(bspTree->childPtr(Map::BspTree::ChildId(eyeSide))); // If the clipper is full we're pretty much done. This means no geometry // will be visible in the distance because every direction has already // been fully covered by geometry. if(!firstSubspace && clipper.isFull()) return; // ...and back space. bspTree = bspTree->childPtr(Map::BspTree::ChildId(eyeSide ^ 1)); } // We've arrived at a leaf. // Only leafs with a convex subspace geometry have drawable geometries. if(ConvexSubspace *subspace = bspTree->userData()->as().subspacePtr()) { DENG2_ASSERT(subspace->hasCluster()); // Skip zero-volume subspaces. // (Neighbors handle the angle clipper ranges.) if(!subspace->cluster().hasWorldVolume()) return; // Is this subspace visible? if(!firstSubspace && !clipper.isPolyVisible(subspace->poly())) return; // This is now the current subspace. makeCurrent(*subspace); drawCurrentSubspace(); // This is no longer the first subspace. firstSubspace = false; } } /** * Project all the non-clipped decorations. They become regular vissprites. */ static void generateDecorationFlares(Map &map) { Vector3d const viewPos = Rend_EyeOrigin().xzy(); map.forAllLumobjs([&viewPos] (Lumobj &lob) { lob.generateFlare(viewPos, R_ViewerLumobjDistance(lob.indexInMap())); /// @todo mark these light sources visible for LensFx return LoopContinue; }); } /** * Setup GL state for an entire rendering pass (compassing multiple lists). */ static void pushGLStateForPass(DrawMode mode, TexUnitMap &texUnitMap) { static dfloat const black[] = { 0, 0, 0, 0 }; de::zap(texUnitMap); switch(mode) { case DM_SKYMASK: GL_SelectTexUnits(0); glDisable(GL_ALPHA_TEST); glDepthMask(GL_TRUE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); break; case DM_BLENDED: GL_SelectTexUnits(2); // Intentional fall-through. case DM_ALL: // The first texture unit is used for the main texture. texUnitMap[0] = Store::TCA_MAIN + 1; texUnitMap[1] = Store::TCA_BLEND + 1; glDisable(GL_ALPHA_TEST); glDepthMask(GL_TRUE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); // Fog is allowed during this pass. if(usingFog) { glEnable(GL_FOG); } // All of the surfaces are opaque. glDisable(GL_BLEND); break; case DM_LIGHT_MOD_TEXTURE: case DM_TEXTURE_PLUS_LIGHT: // Modulate sector light, dynamic light and regular texture. GL_SelectTexUnits(2); if(mode == DM_LIGHT_MOD_TEXTURE) { texUnitMap[0] = Store::TCA_LIGHT + 1; texUnitMap[1] = Store::TCA_MAIN + 1; GL_ModulateTexture(4); // Light * texture. } else { texUnitMap[0] = Store::TCA_MAIN + 1; texUnitMap[1] = Store::TCA_LIGHT + 1; GL_ModulateTexture(5); // Texture + light. } glDisable(GL_ALPHA_TEST); glDepthMask(GL_TRUE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); // Fog is allowed during this pass. if(usingFog) { glEnable(GL_FOG); } // All of the surfaces are opaque. glDisable(GL_BLEND); break; case DM_FIRST_LIGHT: // One light, no texture. GL_SelectTexUnits(1); texUnitMap[0] = Store::TCA_LIGHT + 1; GL_ModulateTexture(6); glDisable(GL_ALPHA_TEST); glDepthMask(GL_TRUE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); // All of the surfaces are opaque. glDisable(GL_BLEND); break; case DM_BLENDED_FIRST_LIGHT: // One additive light, no texture. GL_SelectTexUnits(1); texUnitMap[0] = Store::TCA_LIGHT + 1; GL_ModulateTexture(7); // Add light, no color. glEnable(GL_ALPHA_TEST); glAlphaFunc(GL_GREATER, 1 / 255.0f); glDepthMask(GL_FALSE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); // All of the surfaces are opaque. glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE); break; case DM_WITHOUT_TEXTURE: GL_SelectTexUnits(0); GL_ModulateTexture(1); glDisable(GL_ALPHA_TEST); glDepthMask(GL_TRUE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); // All of the surfaces are opaque. glDisable(GL_BLEND); break; case DM_LIGHTS: GL_SelectTexUnits(1); texUnitMap[0] = Store::TCA_MAIN + 1; GL_ModulateTexture(1); glEnable(GL_ALPHA_TEST); glAlphaFunc(GL_GREATER, 1 / 255.0f); glDepthMask(GL_FALSE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); if(usingFog) { glEnable(GL_FOG); glFogfv(GL_FOG_COLOR, black); } glEnable(GL_BLEND); GL_BlendMode(BM_ADD); break; case DM_MOD_TEXTURE: case DM_MOD_TEXTURE_MANY_LIGHTS: case DM_BLENDED_MOD_TEXTURE: // The first texture unit is used for the main texture. texUnitMap[0] = Store::TCA_MAIN + 1; texUnitMap[1] = Store::TCA_BLEND + 1; glDisable(GL_ALPHA_TEST); glDepthMask(GL_FALSE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); // All of the surfaces are opaque. glEnable(GL_BLEND); glBlendFunc(GL_DST_COLOR, GL_ZERO); break; case DM_UNBLENDED_TEXTURE_AND_DETAIL: texUnitMap[0] = Store::TCA_MAIN + 1; texUnitMap[1] = Store::TCA_MAIN + 1; glDisable(GL_ALPHA_TEST); glDepthMask(GL_TRUE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); // All of the surfaces are opaque. glDisable(GL_BLEND); // Fog is allowed. if(usingFog) { glEnable(GL_FOG); } break; case DM_UNBLENDED_MOD_TEXTURE_AND_DETAIL: texUnitMap[0] = Store::TCA_MAIN + 1; texUnitMap[1] = Store::TCA_MAIN + 1; glDisable(GL_ALPHA_TEST); glDepthMask(GL_FALSE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); // All of the surfaces are opaque. glEnable(GL_BLEND); glBlendFunc(GL_DST_COLOR, GL_ZERO); break; case DM_ALL_DETAILS: GL_SelectTexUnits(1); texUnitMap[0] = Store::TCA_MAIN + 1; GL_ModulateTexture(0); glDisable(GL_ALPHA_TEST); glDepthMask(GL_FALSE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); // All of the surfaces are opaque. glEnable(GL_BLEND); glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR); // Use fog to fade the details, if fog is enabled. if(usingFog) { glEnable(GL_FOG); dfloat const midGray[] = { .5f, .5f, .5f, fogColor[3] }; // The alpha is probably meaningless? glFogfv(GL_FOG_COLOR, midGray); } break; case DM_BLENDED_DETAILS: GL_SelectTexUnits(2); texUnitMap[0] = Store::TCA_MAIN + 1; texUnitMap[1] = Store::TCA_BLEND + 1; GL_ModulateTexture(3); glDisable(GL_ALPHA_TEST); glDepthMask(GL_FALSE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); // All of the surfaces are opaque. glEnable(GL_BLEND); glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR); // Use fog to fade the details, if fog is enabled. if(usingFog) { glEnable(GL_FOG); dfloat const midGray[] = { .5f, .5f, .5f, fogColor[3] }; // The alpha is probably meaningless? glFogfv(GL_FOG_COLOR, midGray); } break; case DM_SHADOW: // A bit like 'negative lights'. GL_SelectTexUnits(1); texUnitMap[0] = Store::TCA_MAIN + 1; GL_ModulateTexture(1); glEnable(GL_ALPHA_TEST); glAlphaFunc(GL_GREATER, 1 / 255.0f); glDepthMask(GL_FALSE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); // Set normal fog, if it's enabled. if(usingFog) { glEnable(GL_FOG); glFogfv(GL_FOG_COLOR, fogColor); } glEnable(GL_BLEND); GL_BlendMode(BM_NORMAL); break; case DM_SHINY: GL_SelectTexUnits(1); texUnitMap[0] = Store::TCA_MAIN + 1; GL_ModulateTexture(1); // 8 for multitexture glDisable(GL_ALPHA_TEST); glDepthMask(GL_FALSE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); if(usingFog) { // Fog makes the shininess diminish in the distance. glEnable(GL_FOG); glFogfv(GL_FOG_COLOR, black); } glEnable(GL_BLEND); GL_BlendMode(BM_ADD); // Purely additive. break; case DM_MASKED_SHINY: GL_SelectTexUnits(2); texUnitMap[0] = Store::TCA_MAIN + 1; texUnitMap[1] = Store::TCA_BLEND + 1; // the mask GL_ModulateTexture(8); // same as with details glDisable(GL_ALPHA_TEST); glDepthMask(GL_FALSE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); if(usingFog) { // Fog makes the shininess diminish in the distance. glEnable(GL_FOG); glFogfv(GL_FOG_COLOR, black); } glEnable(GL_BLEND); GL_BlendMode(BM_ADD); // Purely additive. break; default: break; } } static void popGLStateForPass(DrawMode mode) { switch(mode) { default: break; case DM_SKYMASK: GL_SelectTexUnits(1); glEnable(GL_ALPHA_TEST); glDisable(GL_DEPTH_TEST); break; case DM_BLENDED: GL_SelectTexUnits(1); // Intentional fall-through. case DM_ALL: glEnable(GL_ALPHA_TEST); glDisable(GL_DEPTH_TEST); if(usingFog) { glDisable(GL_FOG); } glEnable(GL_BLEND); break; case DM_LIGHT_MOD_TEXTURE: case DM_TEXTURE_PLUS_LIGHT: GL_SelectTexUnits(1); GL_ModulateTexture(1); glEnable(GL_ALPHA_TEST); glDisable(GL_DEPTH_TEST); if(usingFog) { glDisable(GL_FOG); } glEnable(GL_BLEND); break; case DM_FIRST_LIGHT: GL_ModulateTexture(1); glEnable(GL_ALPHA_TEST); glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); break; case DM_BLENDED_FIRST_LIGHT: GL_ModulateTexture(1); glDisable(GL_DEPTH_TEST); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); break; case DM_WITHOUT_TEXTURE: GL_SelectTexUnits(1); GL_ModulateTexture(1); glEnable(GL_ALPHA_TEST); glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); break; case DM_LIGHTS: glDisable(GL_DEPTH_TEST); if(usingFog) { glDisable(GL_FOG); } GL_BlendMode(BM_NORMAL); break; case DM_MOD_TEXTURE: case DM_MOD_TEXTURE_MANY_LIGHTS: case DM_BLENDED_MOD_TEXTURE: glEnable(GL_ALPHA_TEST); glDisable(GL_DEPTH_TEST); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); break; case DM_UNBLENDED_TEXTURE_AND_DETAIL: glEnable(GL_ALPHA_TEST); glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); if(usingFog) { glDisable(GL_FOG); } break; case DM_UNBLENDED_MOD_TEXTURE_AND_DETAIL: glEnable(GL_ALPHA_TEST); glDisable(GL_DEPTH_TEST); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); break; case DM_ALL_DETAILS: GL_ModulateTexture(1); glEnable(GL_ALPHA_TEST); glDisable(GL_DEPTH_TEST); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); if(usingFog) { glDisable(GL_FOG); } break; case DM_BLENDED_DETAILS: GL_SelectTexUnits(1); GL_ModulateTexture(1); glEnable(GL_ALPHA_TEST); glDisable(GL_DEPTH_TEST); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); if(usingFog) { glDisable(GL_FOG); } break; case DM_SHADOW: glDisable(GL_DEPTH_TEST); if(usingFog) { glDisable(GL_FOG); } break; case DM_SHINY: glEnable(GL_ALPHA_TEST); glDisable(GL_DEPTH_TEST); if(usingFog) { glDisable(GL_FOG); } GL_BlendMode(BM_NORMAL); break; case DM_MASKED_SHINY: GL_SelectTexUnits(1); GL_ModulateTexture(1); glEnable(GL_ALPHA_TEST); glDisable(GL_DEPTH_TEST); if(usingFog) { glDisable(GL_FOG); } GL_BlendMode(BM_NORMAL); break; } } static void drawLists(DrawLists::FoundLists const &lists, DrawMode mode) { if(lists.isEmpty()) return; // If the first list is empty -- do nothing. if(lists.at(0)->isEmpty()) return; // Setup GL state that's common to all the lists in this mode. TexUnitMap texUnitMap; pushGLStateForPass(mode, texUnitMap); // Draw each given list. for(dint i = 0; i < lists.count(); ++i) { lists.at(i)->draw(mode, texUnitMap); } popGLStateForPass(mode); } static void drawSky() { DrawLists::FoundLists lists; rendSys().drawLists().findAll(SkyMaskGeom, lists); if(!devRendSkyAlways && lists.isEmpty()) { return; } // We do not want to update color and/or depth. GLState::push() .setDepthTest(false) .setDepthWrite(false) .setColorMask(gl::WriteNone) .apply(); // Mask out stencil buffer, setting the drawn areas to 1. glEnable(GL_STENCIL_TEST); glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE); glStencilFunc(GL_ALWAYS, 1, 0xffffffff); if(!devRendSkyAlways) { drawLists(lists, DM_SKYMASK); } else { glClearStencil(1); glClear(GL_STENCIL_BUFFER_BIT); } // Restore previous GL state. GLState::pop().apply(); glDisable(GL_STENCIL_TEST); // Now, only render where the stencil is set to 1. glEnable(GL_STENCIL_TEST); glStencilFunc(GL_EQUAL, 1, 0xffffffff); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); rendSys().sky().draw(&worldSys().skyAnimator()); if(!devRendSkyAlways) { glClearStencil(0); } // Return GL state to normal. glDisable(GL_STENCIL_TEST); } static bool generateHaloForVisSprite(vissprite_t const *spr, bool primary = false) { dfloat occlusionFactor; if(primary && (spr->data.flare.flags & RFF_NO_PRIMARY)) { return false; } if(spr->data.flare.isDecoration) { // Surface decorations do not yet persist over frames, so we do // not smoothly occlude their flares. Instead, we will have to // put up with them instantly appearing/disappearing. occlusionFactor = R_ViewerLumobjIsClipped(spr->data.flare.lumIdx)? 0 : 1; } else { occlusionFactor = (spr->data.flare.factor & 0x7f) / 127.0f; } return H_RenderHalo(spr->pose.origin, spr->data.flare.size, spr->data.flare.tex, spr->data.flare.color, spr->pose.distance, occlusionFactor, spr->data.flare.mul, spr->data.flare.xOff, primary, (spr->data.flare.flags & RFF_NO_TURN) == 0); } /** * Render sprites, 3D models, masked wall segments and halos, ordered back to * front. Halos are rendered with Z-buffer tests and writes disabled, so they * don't go into walls or interfere with real objects. It means that halos can * be partly occluded by objects that are closer to the viewpoint, but that's * the price to pay for not having access to the actual Z-buffer per-pixel depth * information. The other option would be for halos to shine through masked walls, * sprites and models, which looks even worse. (Plus, they are *halos*, not real * lens flares...) */ static void drawMasked() { if(devNoSprites) return; R_SortVisSprites(); if(visSpriteP && visSpriteP > visSprites) { bool primaryHaloDrawn = false; // Draw all vissprites back to front. // Sprites look better with Z buffer writes turned off. for(vissprite_t *spr = visSprSortedHead.next; spr != &visSprSortedHead; spr = spr->next) { switch(spr->type) { default: break; case VSPR_MASKED_WALL: // A masked wall is a specialized sprite. Rend_DrawMaskedWall(spr->data.wall); break; case VSPR_SPRITE: // Render an old fashioned sprite, ah the nostalgia... Rend_DrawSprite(*spr); break; case VSPR_MODEL: Rend_DrawModel(*spr); break; case VSPR_MODEL_GL2: Rend_DrawModel2(*spr); break; case VSPR_FLARE: if(generateHaloForVisSprite(spr, true)) { primaryHaloDrawn = true; } break; } } // Draw secondary halos? if(primaryHaloDrawn && haloMode > 1) { // Now we can setup the state only once. H_SetupState(true); for(vissprite_t *spr = visSprSortedHead.next; spr != &visSprSortedHead; spr = spr->next) { if(spr->type == VSPR_FLARE) { generateHaloForVisSprite(spr); } } // And we're done... H_SetupState(false); } } } /** * We have several different paths to accommodate both multitextured details and * dynamic lights. Details take precedence (they always cover entire primitives * and usually *all* of the surfaces in a scene). */ static void drawAllLists(Map &map) { DENG2_ASSERT(!Sys_GLCheckError()); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); drawSky(); // Render the real surfaces of the visible world. // // Pass: Unlit geometries (all normal lists). // DrawLists::FoundLists lists; ClientApp::renderSystem().drawLists().findAll(UnlitGeom, lists); if(IS_MTEX_DETAILS) { // Draw details for unblended surfaces in this pass. drawLists(lists, DM_UNBLENDED_TEXTURE_AND_DETAIL); // Blended surfaces. drawLists(lists, DM_BLENDED); } else { // Blending is done during this pass. drawLists(lists, DM_ALL); } // // Pass: Lit geometries. // ClientApp::renderSystem().drawLists().findAll(LitGeom, lists); // If multitexturing is available, we'll use it to our advantage when // rendering lights. if(IS_MTEX_LIGHTS && dynlightBlend != 2) { if(IS_MUL) { // All (unblended) surfaces with exactly one light can be // rendered in a single pass. drawLists(lists, DM_LIGHT_MOD_TEXTURE); // Render surfaces with many lights without a texture, just // with the first light. drawLists(lists, DM_FIRST_LIGHT); } else // Additive ('foggy') lights. { drawLists(lists, DM_TEXTURE_PLUS_LIGHT); // Render surfaces with blending. drawLists(lists, DM_BLENDED); // Render the first light for surfaces with blending. // (Not optimal but shouldn't matter; texture is changed for // each primitive.) drawLists(lists, DM_BLENDED_FIRST_LIGHT); } } else // Multitexturing is not available for lights. { if(IS_MUL) { // Render all lit surfaces without a texture. drawLists(lists, DM_WITHOUT_TEXTURE); } else { if(IS_MTEX_DETAILS) // Draw detail textures using multitexturing. { // Unblended surfaces with a detail. drawLists(lists, DM_UNBLENDED_TEXTURE_AND_DETAIL); // Blended surfaces without details. drawLists(lists, DM_BLENDED); // Details for blended surfaces. drawLists(lists, DM_BLENDED_DETAILS); } else { drawLists(lists, DM_ALL); } } } // // Pass: All light geometries (always additive). // if(dynlightBlend != 2) { ClientApp::renderSystem().drawLists().findAll(LightGeom, lists); drawLists(lists, DM_LIGHTS); } // // Pass: Geometries with texture modulation. // if(IS_MUL) { // Finish the lit surfaces that didn't yet get a texture. ClientApp::renderSystem().drawLists().findAll(LitGeom, lists); if(IS_MTEX_DETAILS) { drawLists(lists, DM_UNBLENDED_MOD_TEXTURE_AND_DETAIL); drawLists(lists, DM_BLENDED_MOD_TEXTURE); drawLists(lists, DM_BLENDED_DETAILS); } else { if(IS_MTEX_LIGHTS && dynlightBlend != 2) { drawLists(lists, DM_MOD_TEXTURE_MANY_LIGHTS); } else { drawLists(lists, DM_MOD_TEXTURE); } } } // // Pass: Geometries with details & modulation. // // If multitexturing is not available for details, we need to apply them as // an extra pass over all the detailed surfaces. // if(r_detail) { // Render detail textures for all surfaces that need them. ClientApp::renderSystem().drawLists().findAll(UnlitGeom, lists); if(IS_MTEX_DETAILS) { // Blended detail textures. drawLists(lists, DM_BLENDED_DETAILS); } else { drawLists(lists, DM_ALL_DETAILS); ClientApp::renderSystem().drawLists().findAll(LitGeom, lists); drawLists(lists, DM_ALL_DETAILS); } } // // Pass: Shiny geometries. // // If we have two texture units, the shiny masks will be enabled. Otherwise // the masks are ignored. The shine is basically specular environmental // additive light, multiplied by the mask so that black texels from the mask // produce areas without shine. // ClientApp::renderSystem().drawLists().findAll(ShineGeom, lists); if(numTexUnits > 1) { // Render masked shiny surfaces in a separate pass. drawLists(lists, DM_SHINY); drawLists(lists, DM_MASKED_SHINY); } else { drawLists(lists, DM_ALL_SHINY); } // // Pass: Shadow geometries (objects and Fake Radio). // dint const oldRenderTextures = renderTextures; renderTextures = true; ClientApp::renderSystem().drawLists().findAll(ShadowGeom, lists); drawLists(lists, DM_SHADOW); renderTextures = oldRenderTextures; glDisable(GL_TEXTURE_2D); // The draw lists do not modify these states -ds glEnable(GL_BLEND); glDepthMask(GL_TRUE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glEnable(GL_ALPHA_TEST); glAlphaFunc(GL_GREATER, 0); if(usingFog) { glEnable(GL_FOG); glFogfv(GL_FOG_COLOR, fogColor); } // Draw masked walls, sprites and models. drawMasked(); // Draw particles. Rend_RenderParticles(map); if(usingFog) { glDisable(GL_FOG); } DENG2_ASSERT(!Sys_GLCheckError()); } void Rend_RenderMap(Map &map) { GL_SetMultisample(true); // Setup the modelview matrix. Rend_ModelViewMatrix(); if(!freezeRLs) { // Prepare for rendering. rendSys().beginFrame(); // Make vissprites of all the visible decorations. generateDecorationFlares(map); viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); eyeOrigin = viewData->current.origin; // Add the backside clipping range (if vpitch allows). if(vpitch <= 90 - yfov / 2 && vpitch >= -90 + yfov / 2) { AngleClipper &clipper = rendSys().angleClipper(); dfloat a = de::abs(vpitch) / (90 - yfov / 2); binangle_t startAngle = binangle_t(BANG_45 * Rend_FieldOfView() / 90) * (1 + a); binangle_t angLen = BANG_180 - startAngle; binangle_t viewside = (viewData->current.angle() >> (32 - BAMS_BITS)) + startAngle; clipper.safeAddRange(viewside, viewside + angLen); clipper.safeAddRange(viewside + angLen, viewside + 2 * angLen); } // The viewside line for the depth cue. viewsidex = -viewData->viewSin; viewsidey = viewData->viewCos; // We don't want BSP clip checking for the first subspace. firstSubspace = true; // No current subspace as of yet. curSubspace = nullptr; // Draw the world! traverseBspTreeAndDrawSubspaces(&map.bspTree()); } drawAllLists(map); // Draw various debugging displays: drawAllSurfaceTangentVectors(map); drawLumobjs(map); drawMobjBoundingBoxes(map); drawSectors(map); drawVertexes(map); drawThinkers(map); drawSoundEmitters(map); drawGenerators(map); drawBiasEditingVisuals(map); GL_SetMultisample(false); } static void drawStar(Vector3d const &origin, dfloat size, Vector4f const &color) { static dfloat const black[] = { 0, 0, 0, 0 }; glBegin(GL_LINES); glColor4fv(black); glVertex3f(origin.x - size, origin.z, origin.y); glColor4f(color.x, color.y, color.z, color.w); glVertex3f(origin.x, origin.z, origin.y); glVertex3f(origin.x, origin.z, origin.y); glColor4fv(black); glVertex3f(origin.x + size, origin.z, origin.y); glVertex3f(origin.x, origin.z - size, origin.y); glColor4f(color.x, color.y, color.z, color.w); glVertex3f(origin.x, origin.z, origin.y); glVertex3f(origin.x, origin.z, origin.y); glColor4fv(black); glVertex3f(origin.x, origin.z + size, origin.y); glVertex3f(origin.x, origin.z, origin.y - size); glColor4f(color.x, color.y, color.z, color.w); glVertex3f(origin.x, origin.z, origin.y); glVertex3f(origin.x, origin.z, origin.y); glColor4fv(black); glVertex3f(origin.x, origin.z, origin.y + size); glEnd(); } static void drawLabel(Vector3d const &origin, String const &label, dfloat scale, dfloat alpha) { if(label.isEmpty()) return; glDisable(GL_DEPTH_TEST); glEnable(GL_TEXTURE_2D); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glTranslatef(origin.x, origin.z, origin.y); glRotatef(-vang + 180, 0, 1, 0); glRotatef(vpitch, 1, 0, 0); glScalef(-scale, -scale, 1); FR_SetFont(fontFixed); FR_LoadDefaultAttrib(); FR_SetShadowOffset(UI_SHADOW_OFFSET, UI_SHADOW_OFFSET); FR_SetShadowStrength(UI_SHADOW_STRENGTH); Point2Raw offset(2, 2); UI_TextOutEx(label.toUtf8().constData(), &offset, UI_Color(UIC_TITLE), alpha); glMatrixMode(GL_MODELVIEW); glPopMatrix(); glEnable(GL_DEPTH_TEST); glDisable(GL_TEXTURE_2D); } static void drawLabel(Vector3d const &origin, String const &label) { ddouble distToEye = (Rend_EyeOrigin().xzy() - origin).length(); drawLabel(origin, label, distToEye / (DENG_GAMEVIEW_WIDTH / 2), 1 - distToEye / 2000); } /* * Visuals for Shadow Bias editing: */ static String labelForSource(BiasSource *s) { if(!s || !editShowIndices) return String(); /// @todo Don't assume the current map. return String::number(App_WorldSystem().map().indexOf(*s)); } static void drawSource(BiasSource *s) { if(!s) return; ddouble distToEye = (s->origin() - eyeOrigin).length(); drawStar(s->origin(), 25 + s->evaluateIntensity() / 20, Vector4f(s->color(), 1.0f / de::max(float((distToEye - 100) / 1000), 1.f))); drawLabel(s->origin(), labelForSource(s)); } static void drawLock(Vector3d const &origin, ddouble unit, ddouble t) { glColor4f(1, 1, 1, 1); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glTranslatef(origin.x, origin.z, origin.y); glRotatef(t / 2, 0, 0, 1); glRotatef(t, 1, 0, 0); glRotatef(t * 15, 0, 1, 0); glBegin(GL_LINES); glVertex3f(-unit, 0, -unit); glVertex3f(+unit, 0, -unit); glVertex3f(+unit, 0, -unit); glVertex3f(+unit, 0, +unit); glVertex3f(+unit, 0, +unit); glVertex3f(-unit, 0, +unit); glVertex3f(-unit, 0, +unit); glVertex3f(-unit, 0, -unit); glEnd(); glPopMatrix(); } static void drawBiasEditingVisuals(Map &map) { if(freezeRLs) return; if(!SBE_Active() || editHidden) return; if(!map.biasSourceCount()) return; ddouble const t = Timer_RealMilliseconds() / 100.0f; if(HueCircle *hueCircle = SBE_HueCircle()) { viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glTranslatef(Rend_EyeOrigin().x, Rend_EyeOrigin().y, Rend_EyeOrigin().z); glScalef(1, 1.0f/1.2f, 1); glTranslatef(-Rend_EyeOrigin().x, -Rend_EyeOrigin().y, -Rend_EyeOrigin().z); HueCircleVisual::draw(*hueCircle, Rend_EyeOrigin(), viewData->frontVec); glMatrixMode(GL_MODELVIEW); glPopMatrix(); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); } coord_t handDistance; Hand &hand = App_WorldSystem().hand(&handDistance); // Grabbed sources blink yellow. Vector4f grabbedColor; if(!editBlink || map.biasCurrentTime() & 0x80) grabbedColor = Vector4f(1, 1, .8f, .5f); else grabbedColor = Vector4f(.7f, .7f, .5f, .4f); BiasSource *nearSource = map.biasSourceNear(hand.origin()); DENG2_ASSERT(nearSource); if((hand.origin() - nearSource->origin()).length() > 2 * handDistance) { // Show where it is. glDisable(GL_DEPTH_TEST); } // The nearest cursor phases blue. drawStar(nearSource->origin(), 10000, nearSource->isGrabbed()? grabbedColor : Vector4f(.0f + sin(t) * .2f, .2f + sin(t) * .15f, .9f + sin(t) * .3f, .8f - sin(t) * .2f)); glDisable(GL_DEPTH_TEST); drawLabel(nearSource->origin(), labelForSource(nearSource)); if(nearSource->isLocked()) drawLock(nearSource->origin(), 2 + (nearSource->origin() - eyeOrigin).length() / 100, t); for(Grabbable *grabbable : hand.grabbed()) { if(de::internal::cannotCastGrabbableTo(grabbable)) continue; BiasSource *s = &grabbable->as(); if(s == nearSource) continue; drawStar(s->origin(), 10000, grabbedColor); drawLabel(s->origin(), labelForSource(s)); if(s->isLocked()) drawLock(s->origin(), 2 + (s->origin() - eyeOrigin).length() / 100, t); } /*BiasSource *s = hand.nearestBiasSource(); if(s && !hand.hasGrabbed(*s)) { glDisable(GL_DEPTH_TEST); drawLabel(s->origin(), labelForSource(s)); }*/ // Show all sources? if(editShowAll) { map.forAllBiasSources([&nearSource] (BiasSource &source) { if(&source != nearSource && !source.isGrabbed()) { drawSource(&source); } return LoopContinue; }); } glEnable(GL_DEPTH_TEST); } void Rend_UpdateLightModMatrix() { if(novideo) return; de::zap(lightModRange); if(!App_WorldSystem().hasMap()) { rAmbient = 0; return; } dint mapAmbient = App_WorldSystem().map().ambientLightLevel(); if(mapAmbient > ambientLight) { rAmbient = mapAmbient; } else { rAmbient = ambientLight; } for(dint i = 0; i < 255; ++i) { // Adjust the white point/dark point? dfloat lightlevel = 0; if(lightRangeCompression != 0) { if(lightRangeCompression >= 0) { // Brighten dark areas. lightlevel = dfloat(255 - i) * lightRangeCompression; } else { // Darken bright areas. lightlevel = dfloat(-i) * -lightRangeCompression; } } // Lower than the ambient limit? if(rAmbient != 0 && i+lightlevel <= rAmbient) { lightlevel = rAmbient - i; } // Clamp the result as a modifier to the light value (j). if((i + lightlevel) >= 255) { lightlevel = 255 - i; } else if((i + lightlevel) <= 0) { lightlevel = -i; } // Insert it into the matrix. lightModRange[i] = lightlevel / 255.0f; // Ensure the resultant value never exceeds the expected [0..1] range. DENG2_ASSERT(INRANGE_OF(i / 255.0f + lightModRange[i], 0.f, 1.f)); } } dfloat Rend_LightAdaptationDelta(dfloat val) { dint clampedVal = de::clamp(0, de::roundi(255.0f * val), 254); return lightModRange[clampedVal]; } void Rend_ApplyLightAdaptation(dfloat &val) { val += Rend_LightAdaptationDelta(val); } void Rend_DrawLightModMatrix() { #define BLOCK_WIDTH ( 1.0f ) #define BLOCK_HEIGHT ( BLOCK_WIDTH * 255.0f ) #define BORDER ( 20 ) // Disabled? if(!devLightModRange) return; glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, DENG_GAMEVIEW_WIDTH, DENG_GAMEVIEW_HEIGHT, 0, -1, 1); glTranslatef(BORDER, BORDER, 0); // Draw an outside border. glColor4f(1, 1, 0, 1); glBegin(GL_LINES); glVertex2f(-1, -1); glVertex2f(255 + 1, -1); glVertex2f(255 + 1, -1); glVertex2f(255 + 1, BLOCK_HEIGHT + 1); glVertex2f(255 + 1, BLOCK_HEIGHT + 1); glVertex2f(-1, BLOCK_HEIGHT + 1); glVertex2f(-1, BLOCK_HEIGHT + 1); glVertex2f(-1, -1); glEnd(); glBegin(GL_QUADS); dfloat c = 0; for(dint i = 0; i < 255; ++i, c += (1.0f/255.0f)) { // Get the result of the source light level + offset. dfloat off = lightModRange[i]; glColor4f(c + off, c + off, c + off, 1); glVertex2f(i * BLOCK_WIDTH, 0); glVertex2f(i * BLOCK_WIDTH + BLOCK_WIDTH, 0); glVertex2f(i * BLOCK_WIDTH + BLOCK_WIDTH, BLOCK_HEIGHT); glVertex2f(i * BLOCK_WIDTH, BLOCK_HEIGHT); } glEnd(); glMatrixMode(GL_PROJECTION); glPopMatrix(); #undef BORDER #undef BLOCK_HEIGHT #undef BLOCK_WIDTH } static DGLuint constructBBox(DGLuint name, dfloat br) { if(GL_NewList(name, GL_COMPILE)) { glBegin(GL_QUADS); { // Top glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f+br, 1.0f,-1.0f-br); // TR glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f-br, 1.0f,-1.0f-br); // TL glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f-br, 1.0f, 1.0f+br); // BL glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f+br, 1.0f, 1.0f+br); // BR // Bottom glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f+br,-1.0f, 1.0f+br); // TR glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f-br,-1.0f, 1.0f+br); // TL glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f-br,-1.0f,-1.0f-br); // BL glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f+br,-1.0f,-1.0f-br); // BR // Front glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f+br, 1.0f+br, 1.0f); // TR glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f-br, 1.0f+br, 1.0f); // TL glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f-br,-1.0f-br, 1.0f); // BL glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f+br,-1.0f-br, 1.0f); // BR // Back glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f+br,-1.0f-br,-1.0f); // TR glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f-br,-1.0f-br,-1.0f); // TL glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f-br, 1.0f+br,-1.0f); // BL glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f+br, 1.0f+br,-1.0f); // BR // Left glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f+br, 1.0f+br); // TR glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f+br,-1.0f-br); // TL glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,-1.0f-br,-1.0f-br); // BL glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f,-1.0f-br, 1.0f+br); // BR // Right glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f+br,-1.0f-br); // TR glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f+br, 1.0f+br); // TL glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f,-1.0f-br, 1.0f+br); // BL glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,-1.0f-br,-1.0f-br); // BR } glEnd(); return GL_EndList(); } return 0; } /** * Draws a textured cube using the currently bound gl texture. * Used to draw mobj bounding boxes. * * @param pos Coordinates of the center of the box (in map space units). * @param w Width of the box. * @param l Length of the box. * @param h Height of the box. * @param a Angle of the box. * @param color Color to make the box (uniform vertex color). * @param alpha Alpha to make the box (uniform vertex color). * @param br Border amount to overlap box faces. * @param alignToBase @c true= align the base of the box to the Z coordinate. */ void Rend_DrawBBox(Vector3d const &pos, coord_t w, coord_t l, coord_t h, dfloat a, dfloat const color[3], dfloat alpha, dfloat br, bool alignToBase) { glMatrixMode(GL_MODELVIEW); glPushMatrix(); if(alignToBase) // The Z coordinate is to the bottom of the object. glTranslated(pos.x, pos.z + h, pos.y); else glTranslated(pos.x, pos.z, pos.y); glRotatef(0, 0, 0, 1); glRotatef(0, 1, 0, 0); glRotatef(a, 0, 1, 0); glScaled(w - br - br, h - br - br, l - br - br); glColor4f(color[0], color[1], color[2], alpha); GL_CallList(dlBBox); glMatrixMode(GL_MODELVIEW); glPopMatrix(); } /** * Draws a textured triangle using the currently bound gl texture. * Used to draw mobj angle direction arrow. * * @param pos World space coordinates of the center of the base of the triangle. * @param a Angle to point the triangle in. * @param s Scale of the triangle. * @param color Color to make the box (uniform vertex color). * @param alpha Alpha to make the box (uniform vertex color). */ void Rend_DrawArrow(Vector3d const &pos, dfloat a, dfloat s, dfloat const color[3], dfloat alpha) { glMatrixMode(GL_MODELVIEW); glPushMatrix(); glTranslated(pos.x, pos.z, pos.y); glRotatef(0, 0, 0, 1); glRotatef(0, 1, 0, 0); glRotatef(a, 0, 1, 0); glScalef(s, 0, s); glBegin(GL_TRIANGLES); { glColor4f(0.0f, 0.0f, 0.0f, 0.5f); glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f,-1.0f); // L glColor4f(color[0], color[1], color[2], alpha); glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f,-1.0f); // Point glColor4f(0.0f, 0.0f, 0.0f, 0.5f); glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // R } glEnd(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); } static void drawMobjBBox(mobj_t &mob) { static dfloat const red [] = { 1, 0.2f, 0.2f }; // non-solid objects static dfloat const green [] = { 0.2f, 1, 0.2f }; // solid objects static dfloat const yellow[] = { 0.7f, 0.7f, 0.2f }; // missiles // We don't want the console player. if(&mob == ddPlayers[consolePlayer].shared.mo) return; // Is it vissible? if(!Mobj_IsLinked(mob)) return; BspLeaf const &bspLeaf = Mobj_BspLeafAtOrigin(mob); if(!bspLeaf.hasSubspace() || !R_ViewerSubspaceIsVisible(bspLeaf.subspace())) return; ddouble const distToEye = (eyeOrigin - Mobj_Origin(mob)).length(); dfloat alpha = 1 - ((distToEye / (DENG_GAMEVIEW_WIDTH/2)) / 4); if(alpha < .25f) alpha = .25f; // Don't make them totally invisible. // Draw a bounding box in an appropriate color. coord_t size = Mobj_Radius(mob); Rend_DrawBBox(mob.origin, size, size, mob.height/2, 0, (mob.ddFlags & DDMF_MISSILE)? yellow : (mob.ddFlags & DDMF_SOLID)? green : red, alpha, .08f); Rend_DrawArrow(mob.origin, ((mob.angle + ANG45 + ANG90) / (dfloat) ANGLE_MAX *-360), size*1.25, (mob.ddFlags & DDMF_MISSILE)? yellow : (mob.ddFlags & DDMF_SOLID)? green : red, alpha); } /** * Renders bounding boxes for all mobj's (linked in sec->mobjList, except * the console player) in all sectors that are currently marked as vissible. * * Depth test is disabled to show all mobjs that are being rendered, regardless * if they are actually vissible (hidden by previously drawn map geometry). */ static void drawMobjBoundingBoxes(Map &map) { //static dfloat const red [] = { 1, 0.2f, 0.2f }; // non-solid objects static dfloat const green [] = { 0.2f, 1, 0.2f }; // solid objects static dfloat const yellow[] = { 0.7f, 0.7f, 0.2f }; // missiles if(!devMobjBBox && !devPolyobjBBox) return; #ifndef _DEBUG // Bounding boxes are not allowed in non-debug netgames. if(netGame) return; #endif if(!dlBBox) dlBBox = constructBBox(0, .08f); glDisable(GL_DEPTH_TEST); glEnable(GL_TEXTURE_2D); glDisable(GL_CULL_FACE); MaterialAnimator &matAnimator = resSys().material(de::Uri("System", Path("bbox"))) .getAnimator(Rend_SpriteMaterialSpec()); // Ensure we've up to date info about the material. matAnimator.prepare(); GL_BindTexture(matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture); GL_BlendMode(BM_ADD); if(devMobjBBox) { map.thinkers().forAll(reinterpret_cast(gx.MobjThinker), 0x1, [] (thinker_t *th) { drawMobjBBox(*reinterpret_cast(th)); return LoopContinue; }); } if(devPolyobjBBox) { map.forAllPolyobjs([] (Polyobj &pob) { Sector const &sec = pob.sector(); coord_t width = (pob.aaBox.maxX - pob.aaBox.minX)/2; coord_t length = (pob.aaBox.maxY - pob.aaBox.minY)/2; coord_t height = (sec.ceiling().height() - sec.floor().height())/2; Vector3d pos(pob.aaBox.minX + width, pob.aaBox.minY + length, sec.floor().height()); ddouble const distToEye = (eyeOrigin - pos).length(); dfloat alpha = 1 - ((distToEye / (DENG_GAMEVIEW_WIDTH/2)) / 4); if(alpha < .25f) alpha = .25f; // Don't make them totally invisible. Rend_DrawBBox(pos, width, length, height, 0, yellow, alpha, .08f); for(Line *line : pob.lines()) { Vector3d pos(line->center(), sec.floor().height()); Rend_DrawBBox(pos, 0, line->length() / 2, height, BANG2DEG(BANG_90 - line->angle()), green, alpha, 0); } return LoopContinue; }); } GL_BlendMode(BM_NORMAL); glEnable(GL_CULL_FACE); glDisable(GL_TEXTURE_2D); glEnable(GL_DEPTH_TEST); } static void drawVector(Vector3f const &vector, dfloat scalar, dfloat const color[3]) { static dfloat const black[] = { 0, 0, 0, 0 }; glBegin(GL_LINES); glColor4fv(black); glVertex3f(scalar * vector.x, scalar * vector.z, scalar * vector.y); glColor3fv(color); glVertex3f(0, 0, 0); glEnd(); } static void drawTangentVectorsForSurface(Surface const &suf, Vector3d const &origin) { static dint const VISUAL_LENGTH = 20; static dfloat const red [] = { 1, 0, 0 }; static dfloat const green[] = { 0, 1, 0 }; static dfloat const blue [] = { 0, 0, 1 }; glMatrixMode(GL_MODELVIEW); glPushMatrix(); glTranslatef(origin.x, origin.z, origin.y); if(devSurfaceVectors & SVF_TANGENT) drawVector(suf.tangent(), VISUAL_LENGTH, red); if(devSurfaceVectors & SVF_BITANGENT) drawVector(suf.bitangent(), VISUAL_LENGTH, green); if(devSurfaceVectors & SVF_NORMAL) drawVector(suf.normal(), VISUAL_LENGTH, blue); glMatrixMode(GL_MODELVIEW); glPopMatrix(); } /** * @todo Determine Z-axis origin from a WallEdge. */ static void drawTangentVectorsForWallSections(HEdge const *hedge) { if(!hedge || !hedge->hasMapElement()) return; LineSideSegment const &seg = hedge->mapElementAs(); LineSide const &lineSide = seg.lineSide(); Line const &line = lineSide.line(); Vector2d const center = (hedge->twin().origin() + hedge->origin()) / 2; if(lineSide.considerOneSided()) { SectorCluster &cluster = (line.definesPolyobj()? line.polyobj().bspLeaf().subspace() : hedge->face().mapElementAs()).cluster(); coord_t const bottom = cluster. visFloor().heightSmoothed(); coord_t const top = cluster.visCeiling().heightSmoothed(); drawTangentVectorsForSurface(lineSide.middle(), Vector3d(center, bottom + (top - bottom) / 2)); } else { SectorCluster &cluster = (line.definesPolyobj()? line.polyobj().bspLeaf().subspace() : hedge->face().mapElementAs()).cluster(); SectorCluster &backCluster = (line.definesPolyobj()? line.polyobj().bspLeaf().subspace() : hedge->twin().face().mapElementAs()).cluster(); if(lineSide.middle().hasMaterial()) { coord_t const bottom = cluster. visFloor().heightSmoothed(); coord_t const top = cluster.visCeiling().heightSmoothed(); drawTangentVectorsForSurface(lineSide.middle(), Vector3d(center, bottom + (top - bottom) / 2)); } if(backCluster.visCeiling().heightSmoothed() < cluster.visCeiling().heightSmoothed() && !(cluster. visCeiling().surface().hasSkyMaskedMaterial() && backCluster.visCeiling().surface().hasSkyMaskedMaterial())) { coord_t const bottom = backCluster.visCeiling().heightSmoothed(); coord_t const top = cluster. visCeiling().heightSmoothed(); drawTangentVectorsForSurface(lineSide.top(), Vector3d(center, bottom + (top - bottom) / 2)); } if(backCluster.visFloor().heightSmoothed() > cluster.visFloor().heightSmoothed() && !(cluster. visFloor().surface().hasSkyMaskedMaterial() && backCluster.visFloor().surface().hasSkyMaskedMaterial())) { coord_t const bottom = cluster. visFloor().heightSmoothed(); coord_t const top = backCluster.visFloor().heightSmoothed(); drawTangentVectorsForSurface(lineSide.bottom(), Vector3d(center, bottom + (top - bottom) / 2)); } } } /** * @todo Use drawTangentVectorsForWallSections() for polyobjs too. */ static void drawSurfaceTangentVectors(SectorCluster &cluster) { for(ConvexSubspace *subspace : cluster.subspaces()) { HEdge const *base = subspace->poly().hedge(); HEdge const *hedge = base; do { drawTangentVectorsForWallSections(hedge); } while((hedge = &hedge->next()) != base); subspace->forAllExtraMeshes([] (Mesh &mesh) { for(HEdge *hedge : mesh.hedges()) { drawTangentVectorsForWallSections(hedge); } return LoopContinue; }); subspace->forAllPolyobjs([] (Polyobj &pob) { for(HEdge *hedge : pob.mesh().hedges()) { drawTangentVectorsForWallSections(hedge); } return LoopContinue; }); } dint const planeCount = cluster.sector().planeCount(); for(dint i = 0; i < planeCount; ++i) { Plane const &plane = cluster.visPlane(i); coord_t height = 0; if(plane.surface().hasSkyMaskedMaterial() && (plane.isSectorFloor() || plane.isSectorCeiling())) { height = plane.map().skyFix(plane.isSectorCeiling()); } else { height = plane.heightSmoothed(); } drawTangentVectorsForSurface(plane.surface(), Vector3d(cluster.center(), height)); } } /** * Draw the surface tangent space vectors, primarily for debug. */ static void drawAllSurfaceTangentVectors(Map &map) { if(!devSurfaceVectors) return; glDisable(GL_CULL_FACE); map.forAllClusters([] (SectorCluster &cluster) { drawSurfaceTangentVectors(cluster); return LoopContinue; }); glEnable(GL_CULL_FACE); } static void drawLumobjs(Map &map) { static dfloat const black[] = { 0, 0, 0, 0 }; if(!devDrawLums) return; glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); map.forAllLumobjs([] (Lumobj &lob) { if(rendMaxLumobjs > 0 && R_ViewerLumobjIsHidden(lob.indexInMap())) return LoopContinue; glMatrixMode(GL_MODELVIEW); glPushMatrix(); glTranslated(lob.origin().x, lob.origin().z + lob.zOffset(), lob.origin().y); glBegin(GL_LINES); { glColor4fv(black); glVertex3f(-lob.radius(), 0, 0); glColor4f(lob.color().x, lob.color().y, lob.color().z, 1); glVertex3f(0, 0, 0); glVertex3f(0, 0, 0); glColor4fv(black); glVertex3f(lob.radius(), 0, 0); glVertex3f(0, -lob.radius(), 0); glColor4f(lob.color().x, lob.color().y, lob.color().z, 1); glVertex3f(0, 0, 0); glVertex3f(0, 0, 0); glColor4fv(black); glVertex3f(0, lob.radius(), 0); glVertex3f(0, 0, -lob.radius()); glColor4f(lob.color().x, lob.color().y, lob.color().z, 1); glVertex3f(0, 0, 0); glVertex3f(0, 0, 0); glColor4fv(black); glVertex3f(0, 0, lob.radius()); } glEnd(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); return LoopContinue; }); glEnable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); } static void drawSoundEmitter(SoundEmitter &emitter, String const &label) { #define MAX_SOUNDORIGIN_DIST 384 Vector3d const &origin(emitter.origin); ddouble const distToEye = (eyeOrigin - origin).length(); if(distToEye < MAX_SOUNDORIGIN_DIST) { drawLabel(origin, label, distToEye / (DENG_GAMEVIEW_WIDTH / 2), 1 - distToEye / MAX_SOUNDORIGIN_DIST); } #undef MAX_SOUNDORIGIN_DIST } /** * Debugging aid for visualizing sound origins. */ static void drawSoundEmitters(Map &map) { if(!devSoundEmitters) return; if(devSoundEmitters & SOF_SIDE) { map.forAllLines([] (Line &line) { for(dint i = 0; i < 2; ++i) { LineSide &side = line.side(i); if(!side.hasSections()) continue; drawSoundEmitter(side.middleSoundEmitter(), String("Line #%1 (%2, middle)") .arg(line.indexInMap()) .arg(i? "back" : "front")); drawSoundEmitter(side.bottomSoundEmitter(), String("Line #%1 (%2, bottom)") .arg(line.indexInMap()) .arg(i? "back" : "front")); drawSoundEmitter(side.topSoundEmitter(), String("Line #%1 (%2, top)") .arg(line.indexInMap()) .arg(i? "back" : "front")); } return LoopContinue; }); } if(devSoundEmitters & (SOF_SECTOR | SOF_PLANE)) { map.forAllSectors([] (Sector §or) { if(devSoundEmitters & SOF_PLANE) { sector.forAllPlanes([] (Plane &plane) { drawSoundEmitter(plane.soundEmitter(), String("Sector #%1 (pln:%2)") .arg(plane.sector().indexInMap()) .arg(plane.indexInSector())); return LoopContinue; }); } if(devSoundEmitters & SOF_SECTOR) { drawSoundEmitter(sector.soundEmitter(), String("Sector #%1").arg(sector.indexInMap())); } return LoopContinue; }); } } void Rend_DrawVectorLight(VectorLightData const &vlight, dfloat alpha) { if(alpha < .0001f) return; dfloat const unitLength = 100; glBegin(GL_LINES); glColor4f(vlight.color.x, vlight.color.y, vlight.color.z, alpha); glVertex3f(unitLength * vlight.direction.x, unitLength * vlight.direction.z, unitLength * vlight.direction.y); glColor4f(vlight.color.x, vlight.color.y, vlight.color.z, 0); glVertex3f(0, 0, 0); glEnd(); } static String labelForGenerator(Generator const &gen) { return String("%1").arg(gen.id()); } static void drawGenerator(Generator const &gen) { static dint const MAX_GENERATOR_DIST = 2048; if(gen.source || gen.isUntriggered()) { Vector3d const origin = gen.origin(); ddouble const distToEye = (eyeOrigin - origin).length(); if(distToEye < MAX_GENERATOR_DIST) { drawLabel(origin, labelForGenerator(gen), distToEye / (DENG_GAMEVIEW_WIDTH / 2), 1 - distToEye / MAX_GENERATOR_DIST); } } } /** * Debugging aid; Draw all active generators. */ static void drawGenerators(Map &map) { if(!devDrawGenerators) return; map.forAllGenerators([] (Generator &gen) { drawGenerator(gen); return LoopContinue; }); } static void drawPoint(Vector3d const &origin, dfloat opacity) { glBegin(GL_POINTS); glColor4f(.7f, .7f, .2f, opacity * 2); glVertex3f(origin.x, origin.z, origin.y); glEnd(); } static void drawBar(Vector3d const &origin, coord_t height, dfloat opacity) { static dint const EXTEND_DIST = 64; static dfloat const black[] = { 0, 0, 0, 0 }; glBegin(GL_LINES); glColor4fv(black); glVertex3f(origin.x, origin.z - EXTEND_DIST, origin.y); glColor4f(1, 1, 1, opacity); glVertex3f(origin.x, origin.z, origin.y); glVertex3f(origin.x, origin.z, origin.y); glVertex3f(origin.x, origin.z + height, origin.y); glVertex3f(origin.x, origin.z + height, origin.y); glColor4fv(black); glVertex3f(origin.x, origin.z + height + EXTEND_DIST, origin.y); glEnd(); } static String labelForVertex(Vertex const *vtx) { DENG2_ASSERT(vtx); return String("%1").arg(vtx->indexInMap()); } struct drawvertexvisual_parameters_t { dint maxDistance; bool drawOrigin; bool drawBar; bool drawLabel; QBitArray *drawnVerts; }; static void drawVertexVisual(Vertex const &vertex, ddouble minHeight, ddouble maxHeight, drawvertexvisual_parameters_t &parms) { if(!parms.drawOrigin && !parms.drawBar && !parms.drawLabel) return; // Skip vertexes produced by the space partitioner. if(vertex.indexInArchive() == MapElement::NoIndex) return; // Skip already processed verts? if(parms.drawnVerts) { if(parms.drawnVerts->testBit(vertex.indexInArchive())) return; parms.drawnVerts->setBit(vertex.indexInArchive()); } // Distance in 2D determines visibility/opacity. ddouble distToEye = (Vector2d(eyeOrigin.x, eyeOrigin.y) - vertex.origin()).length(); if(distToEye >= parms.maxDistance) return; Vector3d const origin(vertex.origin(), minHeight); dfloat const opacity = 1 - distToEye / parms.maxDistance; if(parms.drawBar) { drawBar(origin, maxHeight - minHeight, opacity); } if(parms.drawOrigin) { drawPoint(origin, opacity * 2); } if(parms.drawLabel) { drawLabel(origin, labelForVertex(&vertex), distToEye / (DENG_GAMEVIEW_WIDTH / 2), opacity); } } /** * Find the relative next minmal and/or maximal visual height(s) of all sector * planes which "interface" at the half-edge, edge vertex. * * @param base Base half-edge to find heights for. * @param edge Edge of the half-edge. * @param min Current minimal height to use as a base (will be overwritten). * Use DDMAXFLOAT if the base is unknown. * @param min Current maximal height to use as a base (will be overwritten). * Use DDMINFLOAT if the base is unknown. * * @todo Don't stop when a zero-volume back neighbor is found; process all of * the neighbors at the specified vertex (the half-edge geometry will need to * be linked such that "outside" edges are neighbor-linked similarly to those * with a face). */ static void findMinMaxPlaneHeightsAtVertex(HEdge *base, dint edge, ddouble &min, ddouble &max) { if(!base) return; if(!base->hasFace() || !base->face().hasMapElement()) return; if(!base->face().mapElementAs().hasCluster()) return; // Process neighbors? if(!SectorCluster::isInternalEdge(base)) { ClockDirection const direction = edge? Clockwise : Anticlockwise; HEdge *hedge = base; while((hedge = &SectorClusterCirculator::findBackNeighbor(*hedge, direction)) != base) { // Stop if there is no back subspace. ConvexSubspace *subspace = hedge->hasFace()? &hedge->face().mapElementAs() : nullptr; if(!subspace) break; if(subspace->cluster().visFloor().heightSmoothed() < min) min = subspace->cluster().visFloor().heightSmoothed(); if(subspace->cluster().visCeiling().heightSmoothed() > max) max = subspace->cluster().visCeiling().heightSmoothed(); } } } static void drawSubspaceVertexs(ConvexSubspace &sub, drawvertexvisual_parameters_t &parms) { SectorCluster &cluster = sub.cluster(); ddouble const min = cluster. visFloor().heightSmoothed(); ddouble const max = cluster.visCeiling().heightSmoothed(); HEdge *base = sub.poly().hedge(); HEdge *hedge = base; do { ddouble edgeMin = min; ddouble edgeMax = max; findMinMaxPlaneHeightsAtVertex(hedge, 0 /*left edge*/, edgeMin, edgeMax); drawVertexVisual(hedge->vertex(), min, max, parms); } while((hedge = &hedge->next()) != base); sub.forAllExtraMeshes([&min, &max, &parms] (Mesh &mesh) { for(HEdge *hedge : mesh.hedges()) { drawVertexVisual(hedge->vertex(), min, max, parms); drawVertexVisual(hedge->twin().vertex(), min, max, parms); } return LoopContinue; }); sub.forAllPolyobjs([&min, &max, &parms] (Polyobj &pob) { for(Line *line : pob.lines()) { drawVertexVisual(line->from(), min, max, parms); drawVertexVisual(line->to(), min, max, parms); } return LoopContinue; }); } /** * Draw the various vertex debug aids. */ static void drawVertexes(Map &map) { #define MAX_DISTANCE 1280 ///< From the viewer. dfloat oldLineWidth = -1; if(!devVertexBars && !devVertexIndices) return; AABoxd box(eyeOrigin.x - MAX_DISTANCE, eyeOrigin.y - MAX_DISTANCE, eyeOrigin.x + MAX_DISTANCE, eyeOrigin.y + MAX_DISTANCE); QBitArray drawnVerts(map.vertexCount()); drawvertexvisual_parameters_t parms; parms.maxDistance = MAX_DISTANCE; parms.drawnVerts = &drawnVerts; if(devVertexBars) { glDisable(GL_DEPTH_TEST); glEnable(GL_LINE_SMOOTH); oldLineWidth = DGL_GetFloat(DGL_LINE_WIDTH); DGL_SetFloat(DGL_LINE_WIDTH, 2); parms.drawBar = true; parms.drawLabel = parms.drawOrigin = false; map.subspaceBlockmap().forAllInBox(box, [&box, &parms] (void *object) { // Check the bounds. auto &subspace = *(ConvexSubspace *)object; AABoxd const &polyBox = subspace.poly().aaBox(); if(!(polyBox.maxX < box.minX || polyBox.minX > box.maxX || polyBox.minY > box.maxY || polyBox.maxY < box.minY)) { drawSubspaceVertexs(subspace, parms); } return LoopContinue; }); glEnable(GL_DEPTH_TEST); } // Draw the vertex origins. dfloat const oldPointSize = DGL_GetFloat(DGL_POINT_SIZE); glEnable(GL_POINT_SMOOTH); DGL_SetFloat(DGL_POINT_SIZE, 6); glDisable(GL_DEPTH_TEST); parms.drawnVerts->fill(false); // Process all again. parms.drawOrigin = true; parms.drawBar = parms.drawLabel = false; map.subspaceBlockmap().forAllInBox(box, [&box, &parms] (void *object) { // Check the bounds. auto &subspace = *(ConvexSubspace *)object; AABoxd const &polyBox = subspace.poly().aaBox(); if(!(polyBox.maxX < box.minX || polyBox.minX > box.maxX || polyBox.minY > box.maxY || polyBox.maxY < box.minY)) { drawSubspaceVertexs(subspace, parms); } return LoopContinue; }); glEnable(GL_DEPTH_TEST); if(devVertexIndices) { parms.drawnVerts->fill(false); // Process all again. parms.drawLabel = true; parms.drawBar = parms.drawOrigin = false; map.subspaceBlockmap().forAllInBox(box, [&box, &parms] (void *object) { auto &subspace = *(ConvexSubspace *)object; // Check the bounds. AABoxd const &polyBox = subspace.poly().aaBox(); if(!(polyBox.maxX < box.minX || polyBox.minX > box.maxX || polyBox.minY > box.maxY || polyBox.maxY < box.minY)) { drawSubspaceVertexs(subspace, parms); } return LoopContinue; }); } // Restore previous state. if(devVertexBars) { DGL_SetFloat(DGL_LINE_WIDTH, oldLineWidth); glDisable(GL_LINE_SMOOTH); } DGL_SetFloat(DGL_POINT_SIZE, oldPointSize); glDisable(GL_POINT_SMOOTH); #undef MAX_VERTEX_POINT_DIST } static String labelForCluster(SectorCluster const &cluster) { return String::number(cluster.sector().indexInMap()); } /** * Draw the sector cluster debugging aids. */ static void drawSectors(Map &map) { #define MAX_LABEL_DIST 1280 if(!devSectorIndices) return; // Draw per-cluster sector labels: map.forAllClusters([] (SectorCluster &cluster) { Vector3d const origin(cluster.center(), cluster.visPlane(Sector::Floor).heightSmoothed()); ddouble const distToEye = (eyeOrigin - origin).length(); if(distToEye < MAX_LABEL_DIST) { drawLabel(origin, labelForCluster(cluster), distToEye / (DENG_GAMEVIEW_WIDTH / 2), 1 - distToEye / MAX_LABEL_DIST); } return LoopContinue; }); #undef MAX_LABEL_DIST } static String labelForThinker(thinker_t *thinker) { DENG2_ASSERT(thinker); return String("%1").arg(thinker->id); } /** * Debugging aid for visualizing thinker IDs. */ static void drawThinkers(Map &map) { static coord_t const MAX_THINKER_DIST = 2048; if(!devThinkerIds) return; map.thinkers().forAll(0x1 | 0x2, [] (thinker_t *th) { // Ignore non-mobjs. if(Thinker_IsMobjFunc(th->function)) { Vector3d const origin = Mobj_Center(*(mobj_t *)th); ddouble const distToEye = (eyeOrigin - origin).length(); if(distToEye < MAX_THINKER_DIST) { drawLabel(origin, labelForThinker(th), distToEye / (DENG_GAMEVIEW_WIDTH / 2), 1 - distToEye / MAX_THINKER_DIST); } } return LoopContinue; }); } void Rend_LightGridVisual(LightGrid &lg) { static Vector3f const red(1, 0, 0); static dint blink = 0; // Disabled? if(!devLightGrid) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // Determine the grid reference of the view player. LightGrid::Index viewerGridIndex = 0; if(viewPlayer) { blink++; viewerGridIndex = lg.toIndex(lg.toRef(viewPlayer->shared.mo->origin)); } // Go into screen projection mode. glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, DENG_GAMEVIEW_WIDTH, DENG_GAMEVIEW_HEIGHT, 0, -1, 1); for(dint y = 0; y < lg.dimensions().y; ++y) { glBegin(GL_QUADS); for(dint x = 0; x < lg.dimensions().x; ++x) { LightGrid::Index gridIndex = lg.toIndex(x, lg.dimensions().y - 1 - y); bool const isViewerIndex = (viewPlayer && viewerGridIndex == gridIndex); Vector3f const *color = 0; if(isViewerIndex && (blink & 16)) { color = &red; } else if(lg.primarySource(gridIndex)) { color = &lg.rawColorRef(gridIndex); } if(!color) continue; glColor3f(color->x, color->y, color->z); glVertex2f(x * devLightGridSize, y * devLightGridSize); glVertex2f(x * devLightGridSize + devLightGridSize, y * devLightGridSize); glVertex2f(x * devLightGridSize + devLightGridSize, y * devLightGridSize + devLightGridSize); glVertex2f(x * devLightGridSize, y * devLightGridSize + devLightGridSize); } glEnd(); } glMatrixMode(GL_PROJECTION); glPopMatrix(); } MaterialVariantSpec const &Rend_MapSurfaceMaterialSpec(dint wrapS, dint wrapT) { return resSys().materialSpec(MapSurfaceContext, 0, 0, 0, 0, wrapS, wrapT, -1, -1, -1, true, true, false, false); } MaterialVariantSpec const &Rend_MapSurfaceMaterialSpec() { return Rend_MapSurfaceMaterialSpec(GL_REPEAT, GL_REPEAT); } /// Returns the texture variant specification for lightmaps. TextureVariantSpec const &Rend_MapSurfaceLightmapTextureSpec() { return resSys().textureSpec(TC_MAPSURFACE_LIGHTMAP, 0, 0, 0, 0, GL_CLAMP, GL_CLAMP, 1, -1, -1, false, false, false, true); } TextureVariantSpec const &Rend_MapSurfaceShinyTextureSpec() { return resSys().textureSpec(TC_MAPSURFACE_REFLECTION, TSF_NO_COMPRESSION, 0, 0, 0, GL_REPEAT, GL_REPEAT, 1, 1, -1, false, false, false, false); } TextureVariantSpec const &Rend_MapSurfaceShinyMaskTextureSpec() { return resSys().textureSpec(TC_MAPSURFACE_REFLECTIONMASK, 0, 0, 0, 0, GL_REPEAT, GL_REPEAT, -1, -1, -1, true, false, false, false); } D_CMD(OpenRendererAppearanceEditor) { DENG2_UNUSED3(src, argc, argv); if(!App_GameLoaded()) { LOG_ERROR("A game must be loaded before the Renderer Appearance editor can be opened"); return false; } if(!ClientWindow::main().hasSidebar()) { // The editor sidebar will give its ownership automatically // to the window. RendererAppearanceEditor *editor = new RendererAppearanceEditor; editor->open(); } return true; } D_CMD(LowRes) { DENG2_UNUSED3(src, argv, argc); // Set everything as low as they go. filterSprites = 0; filterUI = 0; texMagMode = 0; //GL_SetAllTexturesMinFilter(GL_NEAREST); GL_SetRawTexturesMinFilter(GL_NEAREST); // And do a texreset so everything is updated. GL_TexReset(); return true; } D_CMD(TexReset) { DENG2_UNUSED(src); if(argc == 2 && !String(argv[1]).compareWithoutCase("raw")) { // Reset just raw images. GL_ReleaseTexturesForRawImages(); } else { // Reset everything. GL_TexReset(); } return true; } D_CMD(MipMap) { DENG2_UNUSED2(src, argc); dint newMipMode = String(argv[1]).toInt(); if(newMipMode < 0 || newMipMode > 5) { LOG_SCR_ERROR("Invalid mipmapping mode %i; the valid range is 0...5") << newMipMode; return false; } mipmapping = newMipMode; //GL_SetAllTexturesMinFilter(glmode[mipmapping]); return true; } doomsday-stable-1.15.7/doomsday/client/src/render/rend_particle.cpp0000664000175000017500000006371512641367670024672 0ustar jaakkojaakko/** @file rend_particle.cpp Particle effect rendering. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "render/rend_particle.h" #include #include #include #include #include #include "clientapp.h" #include "r_util.h" #include "sys_system.h" // novideo #include "gl/gl_main.h" #include "gl/gl_texmanager.h" #include "gl/texturecontent.h" #include "world/map.h" #include "world/p_players.h" #include "BspLeaf" #include "ConvexSubspace" #include "Line" #include "Plane" #include "SectorCluster" #include "resource/image.h" #include "render/r_main.h" #include "render/viewports.h" #include "render/rend_main.h" #include "render/rend_model.h" #include "render/vissprite.h" using namespace de; // Point + custom textures. #define NUM_TEX_NAMES (MAX_PTC_TEXTURES) static DGLuint pointTex, ptctexname[MAX_PTC_TEXTURES]; static bool hasPoints, hasLines, hasModels, hasNoBlend, hasAdditive; static bool hasPointTexs[NUM_TEX_NAMES]; struct OrderedParticle { Generator const *generator; dint particleId; dfloat distance; }; static OrderedParticle *order; static size_t orderSize; static size_t numParts; /* * Console variables: */ dbyte useParticles = true; static dint maxParticles; ///< @c 0= Unlimited. static dint particleNearLimit; static dfloat particleDiffuse = 4; static dfloat pointDist(fixed_t const c[3]) { viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); dfloat dist = ((viewData->current.origin.y - FIX2FLT(c[1])) * -viewData->viewSin) - ((viewData->current.origin.x - FIX2FLT(c[0])) * viewData->viewCos); return de::abs(dist); // Always return positive. } static Path tryFindImage(String name) { // // First look for a colorkeyed version. // try { String foundPath = App_FileSystem().findPath(de::Uri("Textures", name + "-ck"), RLF_DEFAULT, App_ResourceClass(RC_GRAPHIC)); // Ensure the path is absolute. return App_BasePath() / foundPath; } catch(FS1::NotFoundError const&) {} // Ignore this error. // // Look for the regular version. // try { String foundPath = App_FileSystem().findPath(de::Uri("Textures", name), RLF_DEFAULT, App_ResourceClass(RC_GRAPHIC)); // Ensure the path is absolute. return App_BasePath() / foundPath; } catch(FS1::NotFoundError const&) {} // Ignore this error. return Path(); // Not found. } // Try to load the texture. static dbyte loadParticleTexture(duint particleTex) { DENG2_ASSERT(particleTex < MAX_PTC_TEXTURES); auto particleImageName = String("Particle%1").arg(particleTex, 2, 10, QChar('0')); Path foundPath = tryFindImage(particleImageName); if(foundPath.isEmpty()) return 0; image_t image; if(!GL_LoadImage(image, foundPath.toUtf8().constData())) { LOG_RES_WARNING("Failed to load \"%s\"") << NativePath(foundPath).pretty(); return 0; } // If 8-bit with no alpha, generate alpha automatically. if(image.pixelSize == 1) Image_ConvertToAlpha(image, true); // Create a new texture and upload the image. ptctexname[particleTex] = GL_NewTextureWithParams( image.pixelSize == 4 ? DGL_RGBA : image.pixelSize == 2 ? DGL_LUMINANCE_PLUS_A8 : DGL_RGB, image.size.x, image.size.y, image.pixels, TXCF_NO_COMPRESSION); // Free the buffer. Image_ClearPixelData(image); return 2; // External } void Rend_ParticleLoadSystemTextures() { if(novideo) return; if(!pointTex) { // Load the default "zeroth" texture (a blurred point). /// @todo Create a logical Texture in the "System" scheme for this. image_t image; if(GL_LoadExtImage(image, "Zeroth", LGM_WHITE_ALPHA)) { // Loaded successfully and converted accordingly. // Upload the image to GL. pointTex = GL_NewTextureWithParams( ( image.pixelSize == 2 ? DGL_LUMINANCE_PLUS_A8 : image.pixelSize == 3 ? DGL_RGB : image.pixelSize == 4 ? DGL_RGBA : DGL_LUMINANCE ), image.size.x, image.size.y, image.pixels, ( TXCF_MIPMAP | TXCF_NO_COMPRESSION ), 0, glmode[mipmapping], GL_LINEAR, 0 /*no anisotropy*/, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE); DENG2_ASSERT(pointTex != 0); } Image_ClearPixelData(image); } } void Rend_ParticleLoadExtraTextures() { if(novideo) return; Rend_ParticleReleaseExtraTextures(); if(!App_GameLoaded()) return; QList loaded; for(dint i = 0; i < MAX_PTC_TEXTURES; ++i) { if(loadParticleTexture(i)) { loaded.append(i); } } if(!loaded.isEmpty()) { LOG_RES_NOTE("Loaded textures for particle IDs: %s") << Rangei::contiguousRangesAsText(loaded); } } void Rend_ParticleReleaseSystemTextures() { if(novideo) return; glDeleteTextures(1, (GLuint const *) &pointTex); pointTex = 0; } void Rend_ParticleReleaseExtraTextures() { if(novideo) return; glDeleteTextures(NUM_TEX_NAMES, (GLuint const *) ptctexname); de::zap(ptctexname); } /** * Sorts in descending order. */ static dint comparePOrder(void const *a, void const *b) { auto const &ptA = *(OrderedParticle const *) a; auto const &ptB = *(OrderedParticle const *) b; if(ptA.distance > ptB.distance) return -1; if(ptA.distance < ptB.distance) return 1; return 0; // Highly unlikely (but possible). } /** * Allocate more memory for the particle ordering buffer, if necessary. */ static void expandOrderBuffer(size_t max) { size_t currentSize = orderSize; if(!orderSize) { orderSize = MAX_OF(max, 256); } else { while(max > orderSize) { orderSize *= 2; } } if(orderSize > currentSize) { order = (OrderedParticle *) Z_Realloc(order, sizeof(OrderedParticle) * orderSize, PU_APPSTATIC); } } /** * Determines whether the given particle is potentially visible for the current viewer. */ static bool particlePVisible(ParticleInfo const &pinfo) { // Never if it has already expired. if(pinfo.stage < 0) return false; // Never if the origin lies outside the map. if(!pinfo.bspLeaf || !pinfo.bspLeaf->hasSubspace()) return false; // Potentially, if the subspace at the origin is visible. return R_ViewerSubspaceIsVisible(pinfo.bspLeaf->subspace()); } /** * @return @c true if there are particles to be drawn. */ static dint listVisibleParticles(Map &map) { ::hasPoints = ::hasModels = ::hasLines = false; ::hasAdditive = ::hasNoBlend = false; de::zap(::hasPointTexs); // Count the total number of particles used by generators marked 'visible'. ::numParts = 0; map.forAllGenerators([] (Generator &gen) { if(R_ViewerGeneratorIsVisible(gen)) { ::numParts += gen.activeParticleCount(); } return LoopContinue; }); if(!::numParts) return false; // Allocate the particle depth sort buffer. expandOrderBuffer(::numParts); // Populate the particle sort buffer and determine what type(s) of // particle (model/point/line/etc...) we'll need to draw. size_t numVisibleParts = 0; map.forAllGenerators([&numVisibleParts] (Generator &gen) { if(!R_ViewerGeneratorIsVisible(gen)) return LoopContinue; // Skip. for(dint i = 0; i < gen.count; ++i) { ParticleInfo const &pinfo = gen.particleInfo()[i]; if(!particlePVisible(pinfo)) continue; // Skip. // Skip particles too far from, or near to, the viewer. dfloat const dist = de::max(pointDist(pinfo.origin), 1.f); if(gen.def->maxDist != 0 && dist > gen.def->maxDist) continue; if(dist < dfloat( ::particleNearLimit )) continue; // This particle is visible. Add it to the sort buffer. OrderedParticle *slot = &::order[numVisibleParts++]; slot->generator = &gen; slot->particleId = i; slot->distance = dist; // Determine what type of particle this is, as this will affect how // we go order our render passes and manipulate the render state. dint const psType = gen.stages[pinfo.stage].type; if(psType == PTC_POINT) { ::hasPoints = true; } else if(psType == PTC_LINE) { ::hasLines = true; } else if(psType >= PTC_TEXTURE && psType < PTC_TEXTURE + MAX_PTC_TEXTURES) { if(::ptctexname[psType - PTC_TEXTURE]) { ::hasPointTexs[psType - PTC_TEXTURE] = true; } else { ::hasPoints = true; } } else if(psType >= PTC_MODEL && psType < PTC_MODEL + MAX_PTC_MODELS) { ::hasModels = true; } if(gen.blendmode() == BM_ADD) { ::hasAdditive = true; } else { ::hasNoBlend = true; } } return LoopContinue; }); // No visible particles? if(!numVisibleParts) return false; // This is the real number of possibly visible particles. ::numParts = numVisibleParts; // Sort the order list back->front. A quicksort is fast enough. qsort(::order, ::numParts, sizeof(OrderedParticle), comparePOrder); return true; } static void setupModelParamsForParticle(vissprite_t &spr, ParticleInfo const *pinfo, GeneratorParticleStage const *st, ded_ptcstage_t const *dst, Vector3f const &origin, dfloat dist, dfloat size, dfloat mark, dfloat alpha) { drawmodelparams_t &parm = *VS_MODEL(&spr); spr.pose.origin = Vector3d(origin.xz(), spr.pose.topZ = origin.y); spr.pose.distance = dist; spr.pose.extraScale = size; // Extra scaling factor. parm.mf = &ClientApp::resourceSystem().modelDef(dst->model); parm.alwaysInterpolate = true; dint frame; if(dst->endFrame < 0) { frame = dst->frame; parm.inter = 0; } else { frame = dst->frame + (dst->endFrame - dst->frame) * mark; parm.inter = M_CycleIntoRange(mark * (dst->endFrame - dst->frame), 1); } ClientApp::resourceSystem().setModelDefFrame(*parm.mf, frame); // Set the correct orientation for the particle. if(parm.mf->testSubFlag(0, MFF_MOVEMENT_YAW)) { spr.pose.yaw = R_MovementXYYaw(FIX2FLT(pinfo->mov[0]), FIX2FLT(pinfo->mov[1])); } else { spr.pose.yaw = pinfo->yaw / 32768.0f * 180; } if(parm.mf->testSubFlag(0, MFF_MOVEMENT_PITCH)) { spr.pose.pitch = R_MovementXYZPitch(FIX2FLT(pinfo->mov[0]), FIX2FLT(pinfo->mov[1]), FIX2FLT(pinfo->mov[2])); } else { spr.pose.pitch = pinfo->pitch / 32768.0f * 180; } spr.light.ambientColor.w = alpha; if(st->flags.testFlag(GeneratorParticleStage::Bright) || levelFullBright) { spr.light.ambientColor.x = spr.light.ambientColor.y = spr.light.ambientColor.z = 1; spr.light.vLightListIdx = 0; } else { Map &map = pinfo->bspLeaf->subspace().sector().map(); if(useBias && map.hasLightGrid()) { Vector4f color = map.lightGrid().evaluate(spr.pose.origin); // Apply light range compression. for(dint i = 0; i < 3; ++i) { color[i] += Rend_LightAdaptationDelta(color[i]); } spr.light.ambientColor.x = color.x; spr.light.ambientColor.y = color.y; spr.light.ambientColor.z = color.z; } else { Vector4f const color = pinfo->bspLeaf->subspace().cluster().lightSourceColorfIntensity(); dfloat lightLevel = color.w; // Apply distance attenuation. lightLevel = Rend_AttenuateLightLevel(spr.pose.distance, lightLevel); // Add extra light. lightLevel += Rend_ExtraLightDelta(); // The last step is to compress the resultant light value by // the global lighting function. Rend_ApplyLightAdaptation(lightLevel); // Determine the final ambientColor. for(dint i = 0; i < 3; ++i) { spr.light.ambientColor[i] = lightLevel * color[i]; } } Rend_ApplyTorchLight(spr.light.ambientColor, spr.pose.distance); spr.light.vLightListIdx = Rend_CollectAffectingLights(spr.pose.origin, spr.light.ambientColor, map.bspLeafAt(spr.pose.origin).subspacePtr()); } } /** * Calculate a unit vector parallel to @a line. * * @todo No longer needed (Surface has tangent space vectors). * * @param unitVect Unit vector is written here. */ static Vector2f lineUnitVector(Line const &line) { coord_t len = M_ApproxDistance(line.direction().x, line.direction().y); if(len) { return line.direction() / len; } return Vector2f(); } static void drawParticles(dint rtype, bool withBlend) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); Vector3f const leftoff = viewData->upVec + viewData->sideVec; Vector3f const rightoff = viewData->upVec - viewData->sideVec; // Should we use a texture? DGLuint tex = 0; if(rtype == PTC_POINT || (rtype >= PTC_TEXTURE && rtype < PTC_TEXTURE + MAX_PTC_TEXTURES)) { if(renderTextures) { if(rtype == PTC_POINT || 0 == ptctexname[rtype - PTC_TEXTURE]) tex = pointTex; else tex = ptctexname[rtype - PTC_TEXTURE]; } } ushort primType = GL_QUADS; if(rtype == PTC_MODEL) { glDepthMask(GL_TRUE); glEnable(GL_DEPTH_TEST); } else if(tex != 0) { glDepthMask(GL_FALSE); glDisable(GL_CULL_FACE); GL_BindTextureUnmanaged(tex, gl::ClampToEdge, gl::ClampToEdge); glEnable(GL_TEXTURE_2D); glDepthFunc(GL_LEQUAL); glBegin(primType = GL_QUADS); } else { glBegin(primType = GL_LINES); } // How many particles will be drawn? size_t i = 0; if(maxParticles) { i = numParts - (unsigned) maxParticles; } blendmode_t mode = BM_NORMAL, newMode; for(; i < numParts; ++i) { OrderedParticle const *slot = &order[i]; Generator const *gen = slot->generator; ParticleInfo const *pinfo = &gen->particleInfo()[slot->particleId]; GeneratorParticleStage const *st = &gen->stages[pinfo->stage]; ded_ptcstage_t const *stDef = &gen->def->stages[pinfo->stage]; dshort stageType = st->type; if(stageType >= PTC_TEXTURE && stageType < PTC_TEXTURE + MAX_PTC_TEXTURES && 0 == ptctexname[stageType - PTC_TEXTURE]) { stageType = PTC_POINT; } // Only render one type of particles. if((rtype == PTC_MODEL && stDef->model < 0) || (rtype != PTC_MODEL && stageType != rtype)) { continue; } if(rtype >= PTC_TEXTURE && rtype < PTC_TEXTURE + MAX_PTC_TEXTURES && 0 == ptctexname[rtype - PTC_TEXTURE]) continue; if((gen->blendmode() != BM_ADD) == withBlend) continue; if(rtype != PTC_MODEL && !withBlend) { // We may need to change the blending mode. newMode = gen->blendmode(); if(newMode != mode) { glEnd(); GL_BlendMode(mode = newMode); glBegin(primType); } } // Is there a next stage for this particle? ded_ptcstage_t const *nextStDef; if(pinfo->stage >= gen->def->stages.size() - 1 || !gen->stages[pinfo->stage + 1].type) { // There is no "next stage". Use the current one. nextStDef = &gen->def->stages[pinfo->stage]; } else { nextStDef = &gen->def->stages[pinfo->stage + 1]; } // Where is intermark? dfloat const inter = 1 - dfloat( pinfo->tics ) / stDef->tics; // Calculate size and color. dfloat size = de::lerp( stDef->particleRadius(slot->particleId), nextStDef->particleRadius(slot->particleId), inter); // Infinitely small? if(!size) continue; Vector4f color = de::lerp(Vector4f(stDef->color), Vector4f(nextStDef->color), inter); if(!st->flags.testFlag(GeneratorParticleStage::Bright) && !levelFullBright) { // This is a simplified version of sectorlight (no distance // attenuation or range compression). if(ConvexSubspace *subspace = pinfo->bspLeaf->subspacePtr()) { dfloat const intensity = subspace->cluster().lightSourceIntensity(); color *= Vector4f(intensity, intensity, intensity, 1); } } dfloat const maxDist = gen->def->maxDist; dfloat const dist = order[i].distance; // Far diffuse? if(maxDist) { if(dist > maxDist * .75f) { color.w *= 1 - (dist - maxDist * .75f) / (maxDist * .25f); } } // Near diffuse? if(particleDiffuse > 0) { if(dist < particleDiffuse * size) { color.w -= 1 - dist / (particleDiffuse * size); } } // Fully transparent? if(color.w <= 0) continue; glColor4f(color.x, color.y, color.z, color.w); bool const nearWall = (pinfo->contact && !pinfo->mov[0] && !pinfo->mov[1]); bool nearPlane = false; if(ConvexSubspace *subspace = pinfo->bspLeaf->subspacePtr()) { SectorCluster &cluster = subspace->cluster(); if(FLT2FIX(cluster. visFloor().heightSmoothed()) + 2 * FRACUNIT >= pinfo->origin[2] || FLT2FIX(cluster.visCeiling().heightSmoothed()) - 2 * FRACUNIT <= pinfo->origin[2]) { nearPlane = true; } } bool flatOnPlane = false, flatOnWall = false; if(stageType == PTC_POINT || (stageType >= PTC_TEXTURE && stageType < PTC_TEXTURE + MAX_PTC_TEXTURES)) { if(st->flags.testFlag(GeneratorParticleStage::PlaneFlat) && nearPlane) flatOnPlane = true; if(st->flags.testFlag(GeneratorParticleStage::WallFlat) && nearWall) flatOnWall = true; } Vector3f center = gen->particleOrigin(*pinfo).xzy(); if(!flatOnPlane && !flatOnWall) { Vector3f offset(frameTimePos, nearPlane? 0 : frameTimePos, frameTimePos); center += offset * gen->particleMomentum(*pinfo).xzy(); } // Model particles are rendered using the normal model rendering routine. if(rtype == PTC_MODEL && stDef->model >= 0) { vissprite_t temp; de::zap(temp); setupModelParamsForParticle(temp, pinfo, st, stDef, center, dist, size, inter, color.w); Rend_DrawModel(temp); continue; } // The vertices, please. if(tex != 0) { // Should the particle be flat against a plane? if(flatOnPlane) { glTexCoord2f(0, 0); glVertex3f(center.x - size, center.y, center.z - size); glTexCoord2f(1, 0); glVertex3f(center.x + size, center.y, center.z - size); glTexCoord2f(1, 1); glVertex3f(center.x + size, center.y, center.z + size); glTexCoord2f(0, 1); glVertex3f(center.x - size, center.y, center.z + size); } // Flat against a wall, then? else if(flatOnWall) { vec2d_t origin, projected; // There will be a slight approximation on the XY plane since // the particles aren't that accurate when it comes to wall // collisions. // Calculate a new center point (project onto the wall). V2d_Set(origin, FIX2FLT(pinfo->origin[0]), FIX2FLT(pinfo->origin[1])); coord_t linePoint[2] = { pinfo->contact->fromOrigin().x, pinfo->contact->fromOrigin().y }; coord_t lineDirection[2] = { pinfo->contact->direction().x, pinfo->contact->direction().y }; V2d_ProjectOnLine(projected, origin, linePoint, lineDirection); // Move away from the wall to avoid the worst Z-fighting. ddouble const gap = -1; // 1 map unit. ddouble diff[2], dist; V2d_Subtract(diff, projected, origin); if((dist = V2d_Length(diff)) != 0) { projected[0] += diff[0] / dist * gap; projected[1] += diff[1] / dist * gap; } DENG2_ASSERT(pinfo->contact); Vector2f unitVec = lineUnitVector(*pinfo->contact); glTexCoord2f(0, 0); glVertex3d(projected[0] - size * unitVec.x, center.y - size, projected[1] - size * unitVec.y); glTexCoord2f(1, 0); glVertex3d(projected[0] - size * unitVec.x, center.y + size, projected[1] - size * unitVec.y); glTexCoord2f(1, 1); glVertex3d(projected[0] + size * unitVec.x, center.y + size, projected[1] + size * unitVec.y); glTexCoord2f(0, 1); glVertex3d(projected[0] + size * unitVec.x, center.y - size, projected[1] + size * unitVec.y); } else { glTexCoord2f(0, 0); glVertex3f(center.x + size * leftoff.x, center.y + size * leftoff.y / 1.2f, center.z + size * leftoff.z); glTexCoord2f(1, 0); glVertex3f(center.x + size * rightoff.x, center.y + size * rightoff.y / 1.2f, center.z + size * rightoff.z); glTexCoord2f(1, 1); glVertex3f(center.x - size * leftoff.x, center.y - size * leftoff.y / 1.2f, center.z - size * leftoff.z); glTexCoord2f(0, 1); glVertex3f(center.x - size * rightoff.x, center.y - size * rightoff.y / 1.2f, center.z - size * rightoff.z); } } else // It's a line. { glVertex3f(center.x, center.y, center.z); glVertex3f(center.x - FIX2FLT(pinfo->mov[0]), center.y - FIX2FLT(pinfo->mov[2]), center.z - FIX2FLT(pinfo->mov[1])); } } if(rtype != PTC_MODEL) { glEnd(); if(tex != 0) { glEnable(GL_CULL_FACE); glDepthMask(GL_TRUE); glDepthFunc(GL_LESS); glDisable(GL_TEXTURE_2D); } } if(!withBlend) { // We may have rendered subtractive stuff. GL_BlendMode(BM_NORMAL); } } static void renderPass(bool useBlending) { DENG2_ASSERT(!Sys_GLCheckError()); // Set blending mode. if(useBlending) { GL_BlendMode(BM_ADD); } if(hasModels) { drawParticles(PTC_MODEL, useBlending); } if(hasLines) { drawParticles(PTC_LINE, useBlending); } if(hasPoints) { drawParticles(PTC_POINT, useBlending); } for(dint i = 0; i < NUM_TEX_NAMES; ++i) { if(hasPointTexs[i]) { drawParticles(PTC_TEXTURE + i, useBlending); } } // Restore blending mode. if(useBlending) { GL_BlendMode(BM_NORMAL); } DENG2_ASSERT(!Sys_GLCheckError()); } void Rend_RenderParticles(Map &map) { if(!useParticles) return; // No visible particles at all? if(!listVisibleParticles(map)) return; // Render all the visible particles. if(hasNoBlend) { renderPass(false); } if(hasAdditive) { // A second pass with additive blending. // This makes the additive particles 'glow' through all other // particles. renderPass(true); } } void Rend_ParticleRegister() { C_VAR_BYTE ("rend-particle", &useParticles, 0, 0, 1); C_VAR_INT ("rend-particle-max", &maxParticles, CVF_NO_MAX, 0, 0); C_VAR_FLOAT("rend-particle-diffuse", &particleDiffuse, CVF_NO_MAX, 0, 0); C_VAR_INT ("rend-particle-visible-near", &particleNearLimit, CVF_NO_MAX, 0, 0); } doomsday-stable-1.15.7/doomsday/client/src/render/modelrenderer.cpp0000664000175000017500000002553112641367670024700 0ustar jaakkojaakko/** @file modelrenderer.cpp Model renderer. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "render/modelrenderer.h" #include "gl/gl_main.h" #include "render/rend_main.h" #include "world/p_players.h" #include "clientapp.h" #include #include #include #include using namespace de; static String const DEF_ANIMATION ("animation"); static String const DEF_MATERIAL ("material"); static String const DEF_UP_VECTOR ("up"); static String const DEF_FRONT_VECTOR("front"); DENG2_PIMPL(ModelRenderer) , DENG2_OBSERVES(filesys::AssetObserver, Availability) , DENG2_OBSERVES(Bank, Load) , DENG2_OBSERVES(ModelDrawable, AboutToGLInit) { #define MAX_LIGHTS 4 filesys::AssetObserver observer { "model\\..*" }; ModelBank bank; std::unique_ptr atlas; GLProgram program; /// @todo Specific models may want to use a custom program. GLUniform uMvpMatrix { "uMvpMatrix", GLUniform::Mat4 }; GLUniform uTex { "uTex", GLUniform::Sampler2D }; GLUniform uEyeDir { "uEyeDir", GLUniform::Vec3 }; GLUniform uAmbientLight { "uAmbientLight", GLUniform::Vec4 }; GLUniform uLightDirs { "uLightDirs", GLUniform::Vec3Array, MAX_LIGHTS }; GLUniform uLightIntensities { "uLightIntensities", GLUniform::Vec4Array, MAX_LIGHTS }; Matrix4f inverseLocal; int lightCount = 0; Id defaultNormals { Id::None }; Id defaultEmission { Id::None }; Id defaultSpecular { Id::None }; Instance(Public *i) : Base(i) { observer.audienceForAvailability() += this; bank.audienceForLoad() += this; } void init() { ClientApp::shaders().build(program, "model.skeletal.normal_specular_emission") << uMvpMatrix << uTex << uEyeDir << uAmbientLight << uLightDirs << uLightIntensities; atlas.reset(AtlasTexture::newWithKdTreeAllocator( Atlas::DefaultFlags, GLTexture::maximumSize().min(GLTexture::Size(4096, 4096)))); atlas->setBorderSize(1); atlas->setMarginSize(0); // Fallback normal map for models who don't provide one. QImage img(QSize(1, 1), QImage::Format_ARGB32); img.fill(qRgba(127, 127, 255, 255)); // z+ defaultNormals = atlas->alloc(img); // Fallback emission map for models who don't have one. img.fill(qRgba(0, 0, 0, 0)); defaultEmission = atlas->alloc(img); // Fallback specular map. img.fill(qRgba(128, 128, 128, 180)); defaultSpecular = atlas->alloc(img); uTex = *atlas; /* // All loaded items should use this atlas. bank.iterate([this] (DotPath const &path) { if(bank.isLoaded(path)) { setupModel(bank.model(path)); } });*/ } void deinit() { // GL resources must be accessed from the main thread only. bank.unloadAll(Bank::ImmediatelyInCurrentThread); atlas.reset(); program.clear(); } void assetAvailabilityChanged(String const &identifier, filesys::AssetObserver::Event event) { //qDebug() << "loading model:" << identifier << event; if(event == filesys::AssetObserver::Added) { bank.add(identifier, App::asset(identifier).absolutePath("path")); // Begin loading the model right away. bank.load(identifier); } else { bank.remove(identifier); } } /** * Configures a ModelDrawable with the appropriate atlas and GL program. * * @param model Model to configure. */ void setupModel(ModelDrawable &model) { if(atlas) { model.setAtlas(*atlas); model.setDefaultTexture(ModelDrawable::Normals, defaultNormals); model.setDefaultTexture(ModelDrawable::Emissive, defaultEmission); model.setDefaultTexture(ModelDrawable::Specular, defaultSpecular); // Use the texture mapping specified in the shader. This has to be done // only now because earlier we may not have the shader available yet. Record const &def = ClientApp::shaders()["model.skeletal.normal_specular_emission"]; if(def.has("textureMapping")) { ModelDrawable::Mapping mapping; for(Value const *value : def.geta("textureMapping").elements()) { mapping << ModelDrawable::textToTextureMap(value->asText()); } //qDebug() << "using mapping" << mapping; model.setTextureMapping(mapping); } } else { model.unsetAtlas(); } model.setProgram(program); } void modelAboutToGLInit(ModelDrawable &model) { setupModel(model); } /** * When model assets have been loaded, we can parse their metadata to see if there * are any animation sequences defined. If so, we'll set up a shared lookup table * that determines which sequences to start in which mobj states. * * @param path Model asset id. */ void bankLoaded(DotPath const &path) { // Models use the shared atlas. ModelDrawable &model = bank.model(path); model.audienceForAboutToGLInit() += this; auto const asset = App::asset(path); std::unique_ptr aux(new AuxiliaryData); // Determine the coordinate system of the model. Vector3f front(0, 0, 1); Vector3f up (0, 1, 0); if(asset.has(DEF_FRONT_VECTOR)) { front = Vector3f(asset.geta(DEF_FRONT_VECTOR)); } if(asset.has(DEF_UP_VECTOR)) { up = Vector3f(asset.geta(DEF_UP_VECTOR)); } aux->transformation = Matrix4f::frame(front, up); // Custom texture maps. if(asset.has(DEF_MATERIAL)) { auto mats = asset.subrecord(DEF_MATERIAL).subrecords(); DENG2_FOR_EACH_CONST(Record::Subrecords, mat, mats) { handleMaterialTexture(model, mat.key(), *mat.value(), "diffuseMap", ModelDrawable::Diffuse); handleMaterialTexture(model, mat.key(), *mat.value(), "normalMap", ModelDrawable::Normals); handleMaterialTexture(model, mat.key(), *mat.value(), "heightMap", ModelDrawable::Height); handleMaterialTexture(model, mat.key(), *mat.value(), "specularMap", ModelDrawable::Specular); handleMaterialTexture(model, mat.key(), *mat.value(), "emissiveMap", ModelDrawable::Emissive); } } // Set up the animation sequences for states. if(asset.has(DEF_ANIMATION)) { auto states = ScriptedInfo::subrecordsOfType("state", asset.subrecord(DEF_ANIMATION)); DENG2_FOR_EACH_CONST(Record::Subrecords, state, states) { // Note that the sequences are added in alphabetical order. auto seqs = ScriptedInfo::subrecordsOfType("sequence", *state.value()); DENG2_FOR_EACH_CONST(Record::Subrecords, seq, seqs) { aux->animations[state.key()] << AnimSequence(seq.key(), *seq.value()); } } // TODO: Check for a possible timeline and calculate time factors accordingly. } // Store the additional information in the bank. bank.setUserData(path, aux.release()); } void handleMaterialTexture(ModelDrawable &model, String const &matName, Record const &matDef, String const &textureName, ModelDrawable::TextureMap map) { if(matDef.has(textureName)) { String path = ScriptedInfo::absolutePathInContext(matDef, matDef.gets(textureName)); int matId = identifierFromText(matName, [&model] (String const &text) { return model.materialId(text); }); model.setTexturePath(matId, map, path); } } }; ModelRenderer::ModelRenderer() : d(new Instance(this)) {} void ModelRenderer::glInit() { d->init(); } void ModelRenderer::glDeinit() { d->deinit(); } ModelBank &ModelRenderer::bank() { return d->bank; } ModelRenderer::StateAnims const *ModelRenderer::animations(DotPath const &modelId) const { if(auto const *aux = d->bank.userData(modelId)->maybeAs()) { if(!aux->animations.isEmpty()) { return &aux->animations; } } return 0; } void ModelRenderer::setTransformation(Vector3f const &eyeDir, Matrix4f const &modelToLocal, Matrix4f const &localToView) { d->uMvpMatrix = localToView * modelToLocal; d->inverseLocal = modelToLocal.inverse(); d->uEyeDir = (d->inverseLocal * eyeDir).normalize(); } void ModelRenderer::setAmbientLight(Vector3f const &ambientIntensity) { d->uAmbientLight = Vector4f(ambientIntensity, 1.f); } void ModelRenderer::clearLights() { d->lightCount = 0; for(int i = 0; i < MAX_LIGHTS; ++i) { d->uLightDirs .set(i, Vector3f()); d->uLightIntensities.set(i, Vector4f()); } } void ModelRenderer::addLight(Vector3f const &direction, Vector3f const &intensity) { if(d->lightCount == MAX_LIGHTS) return; int idx = d->lightCount; d->uLightDirs .set(idx, (d->inverseLocal * direction).normalize()); d->uLightIntensities.set(idx, Vector4f(intensity, intensity.max())); d->lightCount++; } int ModelRenderer::identifierFromText(String const &text, std::function resolver) // static { /// @todo This might be useful on a more general level, outside ModelRenderer. -jk int id = 0; if(text.beginsWith('@')) { id = text.mid(1).toInt(); } else { id = resolver(text); } return id; } doomsday-stable-1.15.7/doomsday/client/src/render/viewports.cpp0000664000175000017500000012327312641367670024115 0ustar jaakkojaakko/** @file viewports.cpp Player viewports and related low-level rendering. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "render/viewports.h" #include #include #include #include #include #include #include "clientapp.h" #include "api_console.h" #include "dd_main.h" #include "dd_loop.h" #include "edit_bias.h" #include "gl/gl_main.h" #include "api_render.h" #include "render/r_draw.h" #include "render/r_main.h" #include "render/fx/bloom.h" #include "render/angleclipper.h" #include "render/cameralensfx.h" #include "render/rendpoly.h" #include "render/skydrawable.h" #include "render/vissprite.h" #include "render/vr.h" #include "network/net_demo.h" #include "world/linesighttest.h" #include "world/thinkers.h" #include "world/p_object.h" #include "world/p_players.h" #include "world/sky.h" #include "BspLeaf" #include "ConvexSubspace" #include "SectorCluster" #include "Surface" #include "Contact" #include "ui/ui_main.h" #include "ui/clientwindow.h" #include "ui/widgets/gameuiwidget.h" using namespace de; #ifdef LIBDENG_CAMERA_MOVEMENT_ANALYSIS dfloat devCameraMovementStartTime; ///< sysTime dfloat devCameraMovementStartTimeRealSecs; #endif dd_bool firstFrameAfterLoad; static dint loadInStartupMode; static dint rendCameraSmooth = true; ///< Smoothed by default. static dbyte showFrameTimePos; static dbyte showViewAngleDeltas; static dbyte showViewPosDeltas; dint rendInfoTris; static viewport_t *currentViewport; static coord_t *luminousDist; static dbyte *luminousClipped; static duint *luminousOrder; static QBitArray subspacesVisible; static QBitArray generatorsVisible(Map::MAX_GENERATORS); static viewdata_t viewDataOfConsole[DDMAXPLAYERS]; ///< Indexed by console number. static dint frameCount; static dint gridCols, gridRows; static viewport_t viewportOfLocalPlayer[DDMAXPLAYERS]; static dint resetNextViewer = true; static inline RenderSystem &rendSys() { return ClientApp::renderSystem(); } static inline WorldSystem &worldSys() { return ClientApp::worldSystem(); } dint R_FrameCount() { return frameCount; } void R_ResetFrameCount() { frameCount = 0; } #undef R_SetViewOrigin DENG_EXTERN_C void R_SetViewOrigin(dint consoleNum, coord_t const origin[3]) { if(consoleNum < 0 || consoleNum >= DDMAXPLAYERS) return; viewDataOfConsole[consoleNum].latest.origin = Vector3d(origin); } #undef R_SetViewAngle DENG_EXTERN_C void R_SetViewAngle(dint consoleNum, angle_t angle) { if(consoleNum < 0 || consoleNum >= DDMAXPLAYERS) return; viewDataOfConsole[consoleNum].latest.setAngle(angle); } #undef R_SetViewPitch DENG_EXTERN_C void R_SetViewPitch(dint consoleNum, dfloat pitch) { if(consoleNum < 0 || consoleNum >= DDMAXPLAYERS) return; viewDataOfConsole[consoleNum].latest.pitch = pitch; } void R_SetupDefaultViewWindow(dint consoleNum) { viewdata_t *vd = &viewDataOfConsole[consoleNum]; if(consoleNum < 0 || consoleNum >= DDMAXPLAYERS) return; vd->window = vd->windowOld = vd->windowTarget = Rectanglei::fromSize(Vector2i(0, 0), Vector2ui(DENG_GAMEVIEW_WIDTH, DENG_GAMEVIEW_HEIGHT)); vd->windowInter = 1; } void R_ViewWindowTicker(dint consoleNum, timespan_t ticLength) { viewdata_t *vd = &viewDataOfConsole[consoleNum]; if(consoleNum < 0 || consoleNum >= DDMAXPLAYERS) { return; } vd->windowInter += dfloat(.4 * ticLength * TICRATE); if(vd->windowInter >= 1) { vd->window = vd->windowTarget; } else { vd->window.moveTopLeft(Vector2i(de::roundf(de::lerp(vd->windowOld.topLeft.x, vd->windowTarget.topLeft.x, vd->windowInter)), de::roundf(de::lerp(vd->windowOld.topLeft.y, vd->windowTarget.topLeft.y, vd->windowInter)))); vd->window.setSize(Vector2ui(de::roundf(de::lerp(vd->windowOld.width(), vd->windowTarget.width(), vd->windowInter)), de::roundf(de::lerp(vd->windowOld.height(), vd->windowTarget.height(), vd->windowInter)))); } } #undef R_ViewWindowGeometry DENG_EXTERN_C dint R_ViewWindowGeometry(dint player, RectRaw *geometry) { if(!geometry) return false; if(player < 0 || player >= DDMAXPLAYERS) return false; viewdata_t const &vd = viewDataOfConsole[player]; geometry->origin.x = vd.window.topLeft.x; geometry->origin.y = vd.window.topLeft.y; geometry->size.width = vd.window.width(); geometry->size.height = vd.window.height(); return true; } #undef R_ViewWindowOrigin DENG_EXTERN_C dint R_ViewWindowOrigin(dint player, Point2Raw *origin) { if(!origin) return false; if(player < 0 || player >= DDMAXPLAYERS) return false; viewdata_t const &vd = viewDataOfConsole[player]; origin->x = vd.window.topLeft.x; origin->y = vd.window.topLeft.y; return true; } #undef R_ViewWindowSize DENG_EXTERN_C dint R_ViewWindowSize(dint player, Size2Raw *size) { if(!size) return false; if(player < 0 || player >= DDMAXPLAYERS) return false; viewdata_t const &vd = viewDataOfConsole[player]; size->width = vd.window.width(); size->height = vd.window.height(); return true; } /** * @note Do not change values used during refresh here because we might be * partway through rendering a frame. Changes should take effect on next * refresh only. */ #undef R_SetViewWindowGeometry DENG_EXTERN_C void R_SetViewWindowGeometry(dint player, RectRaw const *geometry, dd_bool interpolate) { dint p = P_ConsoleToLocal(player); if(p < 0) return; viewport_t const *vp = &viewportOfLocalPlayer[p]; viewdata_t *vd = &viewDataOfConsole[player]; Rectanglei newGeom = Rectanglei::fromSize(Vector2i(de::clamp(0, geometry->origin.x, vp->geometry.width()), de::clamp(0, geometry->origin.y, vp->geometry.height())), Vector2ui(de::abs(geometry->size.width), de::abs(geometry->size.height))); if((unsigned) newGeom.bottomRight.x > vp->geometry.width()) { newGeom.setWidth(vp->geometry.width() - newGeom.topLeft.x); } if((unsigned) newGeom.bottomRight.y > vp->geometry.height()) { newGeom.setHeight(vp->geometry.height() - newGeom.topLeft.y); } // Already at this target? if(vd->window == newGeom) { return; } // Record the new target. vd->windowTarget = newGeom; // Restart or advance the interpolation timer? // If dimensions have not yet been set - do not interpolate. if(interpolate && vd->window.size() != Vector2ui(0, 0)) { vd->windowOld = vd->window; vd->windowInter = 0; } else { vd->windowOld = vd->windowTarget; vd->windowInter = 1; // Update on next frame. } } #undef R_ViewPortGeometry DENG_EXTERN_C dint R_ViewPortGeometry(dint player, RectRaw *geometry) { if(!geometry) return false; dint p = P_ConsoleToLocal(player); if(p == -1) return false; viewport_t const &vp = viewportOfLocalPlayer[p]; geometry->origin.x = vp.geometry.topLeft.x; geometry->origin.y = vp.geometry.topLeft.y; geometry->size.width = vp.geometry.width(); geometry->size.height = vp.geometry.height(); return true; } #undef R_ViewPortOrigin DENG_EXTERN_C dint R_ViewPortOrigin(dint player, Point2Raw *origin) { if(!origin) return false; dint p = P_ConsoleToLocal(player); if(p == -1) return false; viewport_t const &vp = viewportOfLocalPlayer[p]; origin->x = vp.geometry.topLeft.x; origin->y = vp.geometry.topLeft.y; return true; } #undef R_ViewPortSize DENG_EXTERN_C dint R_ViewPortSize(dint player, Size2Raw *size) { if(!size) return false; dint p = P_ConsoleToLocal(player); if(p == -1) return false; viewport_t const &vp = viewportOfLocalPlayer[p]; size->width = vp.geometry.width(); size->height = vp.geometry.height(); return true; } #undef R_SetViewPortPlayer DENG_EXTERN_C void R_SetViewPortPlayer(dint consoleNum, dint viewPlayer) { dint p = P_ConsoleToLocal(consoleNum); if(p != -1) { viewportOfLocalPlayer[p].console = viewPlayer; } } /** * Calculate the placement and dimensions of a specific viewport. * Assumes that the grid has already been configured. */ void R_UpdateViewPortGeometry(viewport_t *port, dint col, dint row) { DENG2_ASSERT(port); Rectanglei newGeom = Rectanglei(Vector2i(DENG_GAMEVIEW_X + col * DENG_GAMEVIEW_WIDTH / gridCols, DENG_GAMEVIEW_Y + row * DENG_GAMEVIEW_HEIGHT / gridRows), Vector2i(DENG_GAMEVIEW_X + (col+1) * DENG_GAMEVIEW_WIDTH / gridCols, DENG_GAMEVIEW_Y + (row+1) * DENG_GAMEVIEW_HEIGHT / gridRows)); ddhook_viewport_reshape_t p; if(port->geometry == newGeom) return; bool doReshape = false; if(port->console != -1 && Plug_CheckForHook(HOOK_VIEWPORT_RESHAPE)) { p.oldGeometry.origin.x = port->geometry.topLeft.x; p.oldGeometry.origin.y = port->geometry.topLeft.y; p.oldGeometry.size.width = port->geometry.width(); p.oldGeometry.size.height = port->geometry.height(); doReshape = true; } port->geometry = newGeom; if(doReshape) { p.geometry.origin.x = port->geometry.topLeft.x; p.geometry.origin.y = port->geometry.topLeft.y; p.geometry.size.width = port->geometry.width(); p.geometry.size.height = port->geometry.height(); DD_CallHooks(HOOK_VIEWPORT_RESHAPE, port->console, (void *)&p); } } bool R_SetViewGrid(dint numCols, dint numRows) { if(numCols > 0 && numRows > 0) { if(numCols * numRows > DDMAXPLAYERS) { return false; } if(numCols != gridCols || numRows != gridRows) { // The number of consoles has changes; LensFx needs to reallocate resources // only for the consoles in use. /// @todo This could be done smarter, only for the affected viewports. -jk LensFx_GLRelease(); } if(numCols > DDMAXPLAYERS) numCols = DDMAXPLAYERS; if(numRows > DDMAXPLAYERS) numRows = DDMAXPLAYERS; gridCols = numCols; gridRows = numRows; } dint p = 0; for(dint y = 0; y < gridRows; ++y) for(dint x = 0; x < gridCols; ++x) { // The console number is -1 if the viewport belongs to no one. viewport_t *vp = &viewportOfLocalPlayer[p]; dint const console = P_LocalToConsole(p); if(console != -1) { vp->console = clients[console].viewConsole; } else { vp->console = -1; } R_UpdateViewPortGeometry(vp, x, y); ++p; } return true; } void R_ResetViewer() { resetNextViewer = 1; } dint R_NextViewer() { return resetNextViewer; } viewdata_t const *R_ViewData(dint consoleNum) { DENG2_ASSERT(consoleNum >= 0 && consoleNum < DDMAXPLAYERS); return &viewDataOfConsole[consoleNum]; } /** * The components whose difference is too large for interpolation will be * snapped to the sharp values. */ void R_CheckViewerLimits(viewer_t *src, viewer_t *dst) { dint const MAXMOVE = 32; /// @todo Remove this snapping. The game should determine this and disable the /// the interpolation as required. if(fabs(dst->origin.x - src->origin.x) > MAXMOVE || fabs(dst->origin.y - src->origin.y) > MAXMOVE) { src->origin = dst->origin; } /* if(abs(dint(dst->angle) - dint(src->angle)) >= ANGLE_45) { LOG_DEBUG("R_CheckViewerLimits: Snap camera angle to %08x.") << dst->angle; src->angle = dst->angle; } */ } /** * Retrieve the current sharp camera position. */ viewer_t R_SharpViewer(player_t &player) { DENG2_ASSERT(player.shared.mo); ddplayer_t const &ddpl = player.shared; viewer_t view(viewDataOfConsole[&player - ddPlayers].latest); if((ddpl.flags & DDPF_CHASECAM) && !(ddpl.flags & DDPF_CAMERA)) { // STUB // This needs to be fleshed out with a proper third person // camera control setup. Currently we simply project the viewer's // position a set distance behind the ddpl. dfloat const distance = 90; duint angle = view.angle() >> ANGLETOFINESHIFT; duint pitch = angle_t(LOOKDIR2DEG(view.pitch) / 360 * ANGLE_MAX) >> ANGLETOFINESHIFT; view.origin -= Vector3d(FIX2FLT(fineCosine[angle]), FIX2FLT(finesine[angle]), FIX2FLT(finesine[pitch])) * distance; } // Check that the viewZ doesn't go too high or low. // Cameras are not restricted. if(!(ddpl.flags & DDPF_CAMERA)) { if(view.origin.z > ddpl.mo->ceilingZ - 4) { view.origin.z = ddpl.mo->ceilingZ - 4; } if(view.origin.z < ddpl.mo->floorZ + 4) { view.origin.z = ddpl.mo->floorZ + 4; } } return view; } void R_NewSharpWorld() { if(resetNextViewer) { resetNextViewer = 2; } for(dint i = 0; i < DDMAXPLAYERS; ++i) { viewdata_t *vd = &viewDataOfConsole[i]; player_t *plr = &ddPlayers[i]; if(/*(plr->shared.flags & DDPF_LOCAL) &&*/ (!plr->shared.inGame || !plr->shared.mo)) { continue; } viewer_t sharpView = R_SharpViewer(*plr); // The game tic has changed, which means we have an updated sharp // camera position. However, the position is at the beginning of // the tic and we are most likely not at a sharp tic boundary, in // time. We will move the viewer positions one step back in the // buffer. The effect of this is that [0] is the previous sharp // position and [1] is the current one. vd->lastSharp[0] = vd->lastSharp[1]; vd->lastSharp[1] = sharpView; R_CheckViewerLimits(vd->lastSharp, &sharpView); } if(worldSys().hasMap()) { Map &map = worldSys().map(); map.updateTrackedPlanes(); map.updateScrollingSurfaces(); } } void R_UpdateViewer(dint consoleNum) { DENG2_ASSERT(consoleNum >= 0 && consoleNum < DDMAXPLAYERS); dint const VIEWPOS_MAX_SMOOTHDISTANCE = 172; viewdata_t *vd = viewDataOfConsole + consoleNum; player_t *player = ddPlayers + consoleNum; if(!player->shared.inGame) return; if(!player->shared.mo) return; viewer_t sharpView = R_SharpViewer(*player); if(resetNextViewer || (sharpView.origin - vd->current.origin).length() > VIEWPOS_MAX_SMOOTHDISTANCE) { // Keep reseting until a new sharp world has arrived. if(resetNextViewer > 1) { resetNextViewer = 0; } // Just view from the sharp position. vd->current = sharpView; vd->lastSharp[0] = vd->lastSharp[1] = sharpView; } // While the game is paused there is no need to calculate any // time offsets or interpolated camera positions. else //if(!clientPaused) { // Calculate the smoothed camera position, which is somewhere between // the previous and current sharp positions. This introduces a slight // delay (max. 1/35 sec) to the movement of the smoothed camera. viewer_t smoothView = vd->lastSharp[0].lerp(vd->lastSharp[1], frameTimePos); // Use the latest view angles known to us if the interpolation flags // are not set. The interpolation flags are used when the view angles // are updated during the sharp tics and need to be smoothed out here. // For example, view locking (dead or camera setlock). /*if(!(player->shared.flags & DDPF_INTERYAW)) smoothView.angle = sharpView.angle;*/ /*if(!(player->shared.flags & DDPF_INTERPITCH)) smoothView.pitch = sharpView.pitch;*/ vd->current = smoothView; // Monitor smoothness of yaw/pitch changes. if(showViewAngleDeltas) { struct OldAngle { ddouble time; dfloat yaw; dfloat pitch; }; static OldAngle oldAngle[DDMAXPLAYERS]; OldAngle *old = &oldAngle[viewPlayer - ddPlayers]; dfloat yaw = (ddouble)smoothView.angle() / ANGLE_MAX * 360; LOGDEV_MSG("(%i) F=%.3f dt=%-10.3f dx=%-10.3f dy=%-10.3f " "Rdx=%-10.3f Rdy=%-10.3f") << SECONDS_TO_TICKS(gameTime) << frameTimePos << sysTime - old->time << yaw - old->yaw << smoothView.pitch - old->pitch << (yaw - old->yaw) / (sysTime - old->time) << (smoothView.pitch - old->pitch) / (sysTime - old->time); old->yaw = yaw; old->pitch = smoothView.pitch; old->time = sysTime; } // The Rdx and Rdy should stay constant when moving. if(showViewPosDeltas) { struct OldPos { ddouble time; Vector3f pos; }; static OldPos oldPos[DDMAXPLAYERS]; OldPos *old = &oldPos[viewPlayer - ddPlayers]; LOGDEV_MSG("(%i) F=%.3f dt=%-10.3f dx=%-10.3f dy=%-10.3f dz=%-10.3f dx/dt=%-10.3f dy/dt=%-10.3f") << SECONDS_TO_TICKS(gameTime) << frameTimePos << sysTime - old->time << smoothView.origin.x - old->pos.x << smoothView.origin.y - old->pos.y << smoothView.origin.z - old->pos.z << (smoothView.origin.x - old->pos.x) / (sysTime - old->time) << (smoothView.origin.y - old->pos.y) / (sysTime - old->time); old->pos = smoothView.origin; old->time = sysTime; } } // Update viewer. angle_t const viewYaw = vd->current.angle(); duint const an = viewYaw >> ANGLETOFINESHIFT; vd->viewSin = FIX2FLT(finesine[an]); vd->viewCos = FIX2FLT(fineCosine[an]); // Calculate the front, up and side unit vectors. dfloat const yawRad = ((viewYaw / (dfloat) ANGLE_MAX) *2) * PI; dfloat const pitchRad = vd->current.pitch * 85 / 110.f / 180 * PI; // The front vector. vd->frontVec.x = cos(yawRad) * cos(pitchRad); vd->frontVec.z = sin(yawRad) * cos(pitchRad); vd->frontVec.y = sin(pitchRad); // The up vector. vd->upVec.x = -cos(yawRad) * sin(pitchRad); vd->upVec.z = -sin(yawRad) * sin(pitchRad); vd->upVec.y = cos(pitchRad); // The side vector is the cross product of the front and up vectors. vd->sideVec = vd->frontVec.cross(vd->upVec); } /** * Prepare rendering the view of the given player. */ void R_SetupFrame(player_t *player) { #define MINEXTRALIGHTFRAMES 2 // This is now the current view player. viewPlayer = player; // Reset the GL triangle counter. //polyCounter = 0; if(showFrameTimePos) { LOGDEV_VERBOSE("frametime = %f") << frameTimePos; } // Handle extralight (used to light up the world momentarily (used for // e.g. gun flashes). We want to avoid flickering, so when ever it is // enabled; make it last for a few frames. if(player->targetExtraLight != player->shared.extraLight) { player->targetExtraLight = player->shared.extraLight; player->extraLightCounter = MINEXTRALIGHTFRAMES; } if(player->extraLightCounter > 0) { player->extraLightCounter--; if(player->extraLightCounter == 0) player->extraLight = player->targetExtraLight; } // Why? validCount++; extraLight = player->extraLight; extraLightDelta = extraLight / 16.0f; if(!freezeRLs) { R_ClearVisSprites(); } #undef MINEXTRALIGHTFRAMES } void R_RenderPlayerViewBorder() { R_DrawViewBorder(); } void R_UseViewPort(viewport_t const *vp) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); if(!vp) { currentViewport = nullptr; ClientWindow::main().game().glApplyViewport( Rectanglei::fromSize(Vector2i(DENG_GAMEVIEW_X, DENG_GAMEVIEW_Y), Vector2ui(DENG_GAMEVIEW_WIDTH, DENG_GAMEVIEW_HEIGHT))); } else { currentViewport = const_cast(vp); ClientWindow::main().game().glApplyViewport(vp->geometry); } } viewport_t const *R_CurrentViewPort() { return currentViewport; } void R_RenderBlankView() { UI_DrawDDBackground(Point2Raw(0, 0), Size2Raw(320, 200), 1); } static void setupPlayerSprites() { psp3d = false; // Cameramen have no psprites. ddplayer_t *ddpl = &viewPlayer->shared; if((ddpl->flags & DDPF_CAMERA) || (ddpl->flags & DDPF_CHASECAM)) return; if(!ddpl->mo) return; mobj_t *mob = ddpl->mo; if(!Mobj_HasSubspace(*mob)) return; SectorCluster &cluster = Mobj_Cluster(*mob); // Determine if we should be drawing all the psprites full bright? dd_bool isFullBright = (levelFullBright != 0); if(!isFullBright) { ddpsprite_t *psp = ddpl->pSprites; for(dint i = 0; i < DDMAXPSPRITES; ++i, psp++) { if(!psp->statePtr) continue; // If one of the psprites is fullbright, both are. if(psp->statePtr->flags & STF_FULLBRIGHT) isFullBright = true; } } viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); ddpsprite_t *psp = ddpl->pSprites; for(dint i = 0; i < DDMAXPSPRITES; ++i, psp++) { vispsprite_t *spr = &visPSprites[i]; spr->type = VPSPR_SPRITE; spr->psp = psp; if(!psp->statePtr) continue; // First, determine whether this is a model or a sprite. bool isModel = false; ModelDef *mf = nullptr, *nextmf = nullptr; dfloat inter = 0; if(useModels) { // Is there a model for this frame? MobjThinker dummy; // Setup a dummy for the call to R_CheckModelFor. dummy->state = psp->statePtr; dummy->tics = psp->tics; mf = Mobj_ModelDef(dummy, &nextmf, &inter); if(mf) isModel = true; } if(isModel) { // Yes, draw a 3D model (in Rend_Draw3DPlayerSprites). // There are 3D psprites. psp3d = true; spr->type = VPSPR_MODEL; spr->origin = viewData->current.origin; spr->data.model.bspLeaf = &Mobj_BspLeafAtOrigin(*mob); spr->data.model.flags = 0; // 32 is the raised weapon height. spr->data.model.topZ = viewData->current.origin.z; spr->data.model.secFloor = cluster.visFloor().heightSmoothed(); spr->data.model.secCeil = cluster.visCeiling().heightSmoothed(); spr->data.model.pClass = 0; spr->data.model.floorClip = 0; spr->data.model.mf = mf; spr->data.model.nextMF = nextmf; spr->data.model.inter = inter; spr->data.model.viewAligned = true; // Offsets to rotation angles. spr->data.model.yawAngleOffset = psp->pos[0] * weaponOffsetScale - 90; spr->data.model.pitchAngleOffset = (32 - psp->pos[1]) * weaponOffsetScale * weaponOffsetScaleY / 1000.0f; // Is the FOV shift in effect? if(weaponFOVShift > 0 && Rend_FieldOfView() > 90) spr->data.model.pitchAngleOffset -= weaponFOVShift * (Rend_FieldOfView() - 90) / 90; // Real rotation angles. spr->data.model.yaw = viewData->current.angle() / (dfloat) ANGLE_MAX *-360 + spr->data.model.yawAngleOffset + 90; spr->data.model.pitch = viewData->current.pitch * 85 / 110 + spr->data.model.yawAngleOffset; std::memset(spr->data.model.visOff, 0, sizeof(spr->data.model.visOff)); spr->data.model.alpha = psp->alpha; spr->data.model.stateFullBright = (psp->flags & DDPSPF_FULLBRIGHT)!=0; } else { // No, draw a 2D sprite (in Rend_DrawPlayerSprites). spr->type = VPSPR_SPRITE; // Adjust the center slightly so an angle can be calculated. spr->origin = viewData->current.origin; spr->data.sprite.bspLeaf = &Mobj_BspLeafAtOrigin(*mob); spr->data.sprite.alpha = psp->alpha; spr->data.sprite.isFullBright = (psp->flags & DDPSPF_FULLBRIGHT) != 0; } } } static Matrix4f frameViewMatrix; static void setupViewMatrix() { // This will be the view matrix for the current frame. frameViewMatrix = GL_GetProjectionMatrix() * Rend_GetModelViewMatrix(viewPlayer - ddPlayers); } Matrix4f const &Viewer_Matrix() { return frameViewMatrix; } #undef R_RenderPlayerView DENG_EXTERN_C void R_RenderPlayerView(dint num) { if(num < 0 || num >= DDMAXPLAYERS) return; // Huh? player_t *player = &ddPlayers[num]; if(!player->shared.inGame) return; if(!player->shared.mo) return; if(firstFrameAfterLoad) { // Don't let the clock run yet. There may be some texture // loading still left to do that we have been unable to // predetermine. firstFrameAfterLoad = false; DD_ResetTimer(); } // Too early? Game has not configured the view window? viewdata_t *vd = &viewDataOfConsole[num]; if(vd->window.isNull()) return; // Setup for rendering the frame. R_SetupFrame(player); vrCfg().setEyeHeightInMapUnits(Con_GetInteger("player-eyeheight")); setupViewMatrix(); setupPlayerSprites(); if(ClientApp::vr().mode() == VRConfig::OculusRift && worldSys().isPointInVoid(Rend_EyeOrigin().xzy())) { // Putting one's head in the wall will cause a blank screen. GLState::current().target().clear(GLTarget::Color); return; } // Hide the viewPlayer's mobj? dint oldFlags = 0; if(!(player->shared.flags & DDPF_CHASECAM)) { oldFlags = player->shared.mo->ddFlags; player->shared.mo->ddFlags |= DDMF_DONTDRAW; } // Go to wireframe mode? if(renderWireframe) { glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); } // GL is in 3D transformation state only during the frame. GL_SwitchTo3DState(true, currentViewport, vd); if(worldSys().hasMap()) { Rend_RenderMap(worldSys().map()); } // Orthogonal projection to the view window. GL_Restore2DState(1, currentViewport, vd); // Don't render in wireframe mode with 2D psprites. if(renderWireframe) { glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } Rend_Draw2DPlayerSprites(); // If the 2D versions are needed. if(renderWireframe) { glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); } // Do we need to render any 3D psprites? if(psp3d) { GL_SwitchTo3DState(false, currentViewport, vd); Rend_Draw3DPlayerSprites(); } // Restore fullscreen viewport, original matrices and state: back to normal 2D. GL_Restore2DState(2, currentViewport, vd); // Back from wireframe mode? if(renderWireframe) { glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } // Now we can show the viewPlayer's mobj again. if(!(player->shared.flags & DDPF_CHASECAM)) { player->shared.mo->ddFlags = oldFlags; } R_PrintRendPoolInfo(); #ifdef LIBDENG_CAMERA_MOVEMENT_ANALYSIS { static dfloat prevPos[3] = { 0, 0, 0 }; static dfloat prevSpeed = 0; static dfloat prevTime; dfloat delta[2] = { vd->current.pos[0] - prevPos[0], vd->current.pos[1] - prevPos[1] }; dfloat speed = V2f_Length(delta); dfloat time = sysTime - devCameraMovementStartTime; dfloat elapsed = time - prevTime; LOGDEV_MSG("%f,%f,%f,%f,%f") << Sys_GetRealSeconds() - devCameraMovementStartTimeRealSecs << time << elapsed << speed/elapsed << speed/elapsed - prevSpeed; V3f_Copy(prevPos, vd->current.pos); prevSpeed = speed/elapsed; prevTime = time; } #endif } /** * Should be called when returning from a game-side drawing method to ensure * that our assumptions of the GL state are valid. This is necessary because * DGL affords the user the posibility of modifiying the GL state. * * @todo: A cleaner approach would be a DGL state stack which could simply pop. */ static void restoreDefaultGLState() { // Here we use the DGL methods as this ensures it's state is kept in sync. DGL_Disable(DGL_FOG); DGL_Disable(DGL_SCISSOR_TEST); DGL_Disable(DGL_TEXTURE_2D); DGL_Enable(DGL_LINE_SMOOTH); DGL_Enable(DGL_POINT_SMOOTH); } static void clearViewPorts() { GLbitfield bits = GL_DEPTH_BUFFER_BIT; if(fx::Bloom::isEnabled() || (App_InFineSystem().finaleInProgess() && !GameUIWidget::finaleStretch()) || ClientApp::vr().mode() == VRConfig::OculusRift) { // Parts of the previous frame might leak in the bloom unless we clear the color // buffer. Not doing this would result in very bright HOMs in map holes and game // UI elements glowing in the frame (UI elements are normally on a separate layer // and should not affect bloom). bits |= GL_COLOR_BUFFER_BIT; } if(!devRendSkyMode) bits |= GL_STENCIL_BUFFER_BIT; if(freezeRLs) { bits |= GL_COLOR_BUFFER_BIT; } else { for(dint i = 0; i < DDMAXPLAYERS; ++i) { player_t *plr = &ddPlayers[i]; if(!plr->shared.inGame || !(plr->shared.flags & DDPF_LOCAL)) continue; if(P_IsInVoid(plr) || !worldSys().hasMap()) { bits |= GL_COLOR_BUFFER_BIT; break; } } } DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // This is all the clearing we'll do. glClear(bits); } void R_RenderViewPorts(ViewPortLayer layer) { dint oldDisplay = displayPlayer; // First clear the viewport. if(layer == Player3DViewLayer) { clearViewPorts(); } // Draw a view for all players with a visible viewport. for(dint p = 0, y = 0; y < gridRows; ++y) for(dint x = 0; x < gridCols; x++, ++p) { viewport_t const *vp = &viewportOfLocalPlayer[p]; displayPlayer = vp->console; R_UseViewPort(vp); if(displayPlayer < 0 || (ddPlayers[displayPlayer].shared.flags & DDPF_UNDEFINED_ORIGIN)) { if(layer == Player3DViewLayer) { R_RenderBlankView(); } continue; } glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); // Use an orthographic projection in real pixel dimensions. glOrtho(0, vp->geometry.width(), vp->geometry.height(), 0, -1, 1); viewdata_t const *vd = &viewDataOfConsole[vp->console]; RectRaw vpGeometry(vp->geometry.topLeft.x, vp->geometry.topLeft.y, vp->geometry.width(), vp->geometry.height()); RectRaw vdWindow(vd->window.topLeft.x, vd->window.topLeft.y, vd->window.width(), vd->window.height()); switch(layer) { case Player3DViewLayer: R_UpdateViewer(vp->console); LensFx_BeginFrame(vp->console); gx.DrawViewPort(p, &vpGeometry, &vdWindow, displayPlayer, 0/*layer #0*/); LensFx_EndFrame(); break; case ViewBorderLayer: R_RenderPlayerViewBorder(); break; case HUDLayer: gx.DrawViewPort(p, &vpGeometry, &vdWindow, displayPlayer, 1/*layer #1*/); break; } restoreDefaultGLState(); glMatrixMode(GL_PROJECTION); glPopMatrix(); } if(layer == Player3DViewLayer) { // Increment the internal frame count. This does not // affect the window's FPS counter. frameCount++; // Keep reseting until a new sharp world has arrived. if(resetNextViewer > 1) resetNextViewer = 0; } // Restore things back to normal. displayPlayer = oldDisplay; R_UseViewPort(nullptr); } void R_ClearViewData() { M_Free(luminousDist); luminousDist = nullptr; M_Free(luminousClipped); luminousClipped = nullptr; M_Free(luminousOrder); luminousOrder = nullptr; } /** * Viewer specific override controlling whether a given sky layer is enabled. * * @todo The override should be applied at SkyDrawable level. We have Raven to * thank for this nonsense (Hexen's sector special 200)... -ds */ #undef R_SkyParams DENG_EXTERN_C void R_SkyParams(dint layerIndex, dint param, void * /*data*/) { LOG_AS("R_SkyParams"); if(!worldSys().hasMap()) { LOG_GL_WARNING("No map currently loaded, ignoring"); return; } Sky &sky = worldSys().map().sky(); if(layerIndex >= 0 && layerIndex < sky.layerCount()) { SkyLayer *layer = sky.layer(layerIndex); switch(param) { case DD_ENABLE: layer->enable(); break; case DD_DISABLE: layer->disable(); break; default: // Log but otherwise ignore this error. LOG_GL_WARNING("Failed configuring layer #%i: bad parameter %i") << layerIndex << param; } return; } LOG_GL_WARNING("Invalid layer #%i") << + layerIndex; } bool R_ViewerSubspaceIsVisible(ConvexSubspace const &subspace) { DENG2_ASSERT(subspace.indexInMap() != MapElement::NoIndex); return subspacesVisible.testBit(subspace.indexInMap()); } void R_ViewerSubspaceMarkVisible(ConvexSubspace const &subspace, bool yes) { DENG2_ASSERT(subspace.indexInMap() != MapElement::NoIndex); subspacesVisible.setBit(subspace.indexInMap(), yes); } bool R_ViewerGeneratorIsVisible(Generator const &generator) { return generatorsVisible.testBit(generator.id() - 1 /* id is 1-based index */); } void R_ViewerGeneratorMarkVisible(Generator const &generator, bool yes) { generatorsVisible.setBit(generator.id() - 1 /* id is 1-based index */, yes); } ddouble R_ViewerLumobjDistance(dint idx) { /// @todo Do not assume the current map. if(idx >= 0 && idx < worldSys().map().lumobjCount()) { return luminousDist[idx]; } return 0; } bool R_ViewerLumobjIsClipped(dint idx) { // If we are not yet prepared for this, just say everything is clipped. if(!luminousClipped) return true; /// @todo Do not assume the current map. if(idx >= 0 && idx < worldSys().map().lumobjCount()) { return CPP_BOOL(luminousClipped[idx]); } return false; } bool R_ViewerLumobjIsHidden(dint idx) { // If we are not yet prepared for this, just say everything is hidden. if(!luminousClipped) return true; /// @todo Do not assume the current map. if(idx >= 0 && idx < worldSys().map().lumobjCount()) { return luminousClipped[idx] == 2; } return false; } static void markLumobjClipped(Lumobj const &lob, bool yes = true) { dint const index = lob.indexInMap(); DENG2_ASSERT(index >= 0 && index < lob.map().lumobjCount()); luminousClipped[index] = yes? 1 : 0; } /// Used to sort lumobjs by distance from viewpoint. static dint lumobjSorter(void const *e1, void const *e2) { coord_t a = luminousDist[*(duint const *) e1]; coord_t b = luminousDist[*(duint const *) e2]; if(a > b) return 1; if(a < b) return -1; return 0; } void R_BeginFrame() { Map &map = worldSys().map(); subspacesVisible.resize(map.subspaceCount()); subspacesVisible.fill(false); // Clear all generator visibility flags. generatorsVisible.fill(false); dint numLuminous = map.lumobjCount(); if(!(numLuminous > 0)) return; // Resize the associated buffers used for per-frame stuff. dint maxLuminous = numLuminous; luminousDist = (coord_t *) M_Realloc(luminousDist, sizeof(*luminousDist) * maxLuminous); luminousClipped = (dbyte *) M_Realloc(luminousClipped, sizeof(*luminousClipped) * maxLuminous); luminousOrder = (duint *) M_Realloc(luminousOrder, sizeof(*luminousOrder) * maxLuminous); // Update viewer => lumobj distances ready for linking and sorting. viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); map.forAllLumobjs([&viewData] (Lumobj &lob) { // Approximate the distance in 3D. Vector3d delta = lob.origin() - viewData->current.origin; luminousDist[lob.indexInMap()] = M_ApproxDistance3(delta.x, delta.y, delta.z * 1.2 /*correct aspect*/); return LoopContinue; }); if(rendMaxLumobjs > 0 && numLuminous > rendMaxLumobjs) { // Sort lumobjs by distance from the viewer. Then clip all lumobjs // so that only the closest are visible (max loMaxLumobjs). // Init the lumobj indices, sort array. for(dint i = 0; i < numLuminous; ++i) { luminousOrder[i] = i; } qsort(luminousOrder, numLuminous, sizeof(duint), lumobjSorter); // Mark all as hidden. std::memset(luminousClipped, 2, numLuminous * sizeof(*luminousClipped)); dint n = 0; for(dint i = 0; i < numLuminous; ++i) { if(n++ > rendMaxLumobjs) break; // Unhide this lumobj. luminousClipped[luminousOrder[i]] = 1; } } else { // Mark all as clipped. std::memset(luminousClipped, 1, numLuminous * sizeof(*luminousClipped)); } } void R_ViewerClipLumobj(Lumobj *lum) { if(!lum) return; // Has this already been occluded? dint lumIdx = lum->indexInMap(); if(luminousClipped[lumIdx] > 1) return; markLumobjClipped(*lum, false); /// @todo Determine the exact centerpoint of the light in addLuminous! Vector3d const origin(lum->x(), lum->y(), lum->z() + lum->zOffset()); if(!(devNoCulling || P_IsInVoid(&ddPlayers[displayPlayer]))) { if(!rendSys().angleClipper().isPointVisible(origin)) { markLumobjClipped(*lum); // Won't have a halo. } } else { markLumobjClipped(*lum); Vector3d const eye = Rend_EyeOrigin().xzy(); if(LineSightTest(eye, origin, -1, 1, LS_PASSLEFT | LS_PASSOVER | LS_PASSUNDER) .trace(lum->map().bspTree())) { markLumobjClipped(*lum, false); // Will have a halo. } } } void R_ViewerClipLumobjBySight(Lumobj *lob, ConvexSubspace *subspace) { if(!lob || !subspace) return; // Already clipped? if(luminousClipped[lob->indexInMap()]) return; // We need to figure out if any of the polyobj's segments lies // between the viewpoint and the lumobj. Vector3d const eye = Rend_EyeOrigin().xzy(); subspace->forAllPolyobjs([&lob, &eye] (Polyobj &pob) { for(HEdge *hedge : pob.mesh().hedges()) { // Is this on the back of a one-sided line? if(!hedge->hasMapElement()) continue; // Ignore half-edges facing the wrong way. if(hedge->mapElementAs().isFrontFacing()) { coord_t eyeV1[2] = { eye.x, eye.y }; coord_t lumOriginV1[2] = { lob->origin().x, lob->origin().y }; coord_t fromV1[2] = { hedge->origin().x, hedge->origin().y }; coord_t toV1[2] = { hedge->twin().origin().x, hedge->twin().origin().y }; if(V2d_Intercept2(lumOriginV1, eyeV1, fromV1, toV1, 0, 0, 0)) { markLumobjClipped(*lob); break; } } } return LoopContinue; }); } angle_t viewer_t::angle() const { angle_t a = _angle; if(DD_GetInteger(DD_USING_HEAD_TRACKING)) { // Apply the actual, current yaw offset. The game has omitted the "body yaw" // portion from the value already. a += fixed_t(radianToDegree(vrCfg().oculusRift().headOrientation().z) / 180 * ANGLE_180); } return a; } D_CMD(ViewGrid) { DENG2_UNUSED2(src, argc); // Recalculate viewports. return R_SetViewGrid(String(argv[1]).toInt(), String(argv[2]).toInt()); } void Viewports_Register() { C_VAR_INT ("con-show-during-setup", &loadInStartupMode, 0, 0, 1); C_VAR_INT ("rend-camera-smooth", &rendCameraSmooth, CVF_HIDE, 0, 1); C_VAR_BYTE("rend-info-deltas-angles", &showViewAngleDeltas, 0, 0, 1); C_VAR_BYTE("rend-info-deltas-pos", &showViewPosDeltas, 0, 0, 1); C_VAR_BYTE("rend-info-frametime", &showFrameTimePos, 0, 0, 1); C_VAR_BYTE("rend-info-rendpolys", &rendInfoRPolys, CVF_NO_ARCHIVE, 0, 1); //C_VAR_INT ("rend-info-tris", &rendInfoTris, 0, 0, 1); // not implemented atm C_CMD("viewgrid", "ii", ViewGrid); } doomsday-stable-1.15.7/doomsday/client/src/render/shard.cpp0000664000175000017500000000731312641367670023150 0ustar jaakkojaakko/** @file shard.cpp 3D map geometry shard. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "render/shard.h" #include #include #include #include "BiasIllum" #include "BiasTracker" #include "SectorCluster" using namespace de; static int devUpdateBiasContributors = true; //cvar DENG2_PIMPL_NOREF(Shard) { SectorCluster *owner; typedef QVector BiasIllums; BiasIllums biasIllums; BiasTracker biasTracker; uint biasLastUpdateFrame; Instance() : owner(0), biasLastUpdateFrame(0) {} ~Instance() { qDeleteAll(biasIllums); } /** * Determines whether it is time to update bias lighting contributors. */ bool needBiasContributorUpdate() { // Are updates disabled? if(!devUpdateBiasContributors) return false; // Unowned shards cannot be updated. if(!owner) return false; // If the data is already up to date, nothing needs to be done. uint lastChangeFrame = owner->biasLastChangeOnFrame(); if(biasLastUpdateFrame == lastChangeFrame) return false; // Mark the data as having been updated (it will be soon). biasLastUpdateFrame = lastChangeFrame; return true; } }; Shard::Shard(int numBiasIllums, SectorCluster *owner) : d(new Instance) { setCluster(owner); if(numBiasIllums) { d->biasIllums.reserve(numBiasIllums); for(int i = 0; i < numBiasIllums; ++i) { d->biasIllums << new BiasIllum(&d->biasTracker); } } } void Shard::lightWithBiasSources(Vector3f const *posCoords, Vector4f *colorCoords, Matrix3f const &tangentMatrix, uint biasTime) { DENG2_ASSERT(posCoords != 0 && colorCoords != 0); Vector3f const sufNormal = tangentMatrix.column(2); if(d->biasIllums.isEmpty()) return; // Is it time to update bias contributors? bool biasUpdated = false; if(d->needBiasContributorUpdate()) { // Perhaps our owner has updated lighting contributions for us? biasUpdated = d->owner->updateBiasContributors(this); } // Light the given geometry. Vector3f const *posIt = posCoords; Vector4f *colorIt = colorCoords; for(int i = 0; i < d->biasIllums.count(); ++i, posIt++, colorIt++) { *colorIt += d->biasIllums[i]->evaluate(*posIt, sufNormal, biasTime); } if(biasUpdated) { // Any changes from contributors will have now been applied. d->biasTracker.markIllumUpdateCompleted(); } } SectorCluster *Shard::cluster() const { return d->owner; } void Shard::setCluster(SectorCluster *newOwner) { d->owner = newOwner; } BiasTracker &Shard::biasTracker() const { return d->biasTracker; } void Shard::updateBiasAfterMove() { d->biasTracker.updateAllContributors(); } void Shard::consoleRegister() // static { #ifdef __CLIENT__ // Development variables. C_VAR_INT("rend-dev-bias-affected", &devUpdateBiasContributors, CVF_NO_ARCHIVE, 0, 1); #endif } doomsday-stable-1.15.7/doomsday/client/src/render/r_things.cpp0000664000175000017500000004732312641367670023671 0ustar jaakkojaakko/** @file r_things.cpp Map Object => Vissprite Projection. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * @authors Copyright © 1993-1996 by id Software, Inc. * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "render/r_things.h" #include #include #include "clientapp.h" #include "de_render.h" #include "dd_main.h" // App_WorldSystem() #include "dd_loop.h" // frameTimePos #include "def_main.h" // states #include "gl/gl_tex.h" #include "gl/gl_texmanager.h" // GL_PrepareFlaremap #include "network/net_main.h" // clients[] #include "render/angleclipper.h" #include "render/mobjanimator.h" #include "render/vissprite.h" #include "world/map.h" #include "world/p_object.h" #include "world/p_players.h" #include "world/clientmobjthinkerdata.h" #include "BspLeaf" #include "ConvexSubspace" #include "SectorCluster" using namespace de; static inline RenderSystem &rendSys() { return ClientApp::renderSystem(); } static void evaluateLighting(Vector3d const &origin, ConvexSubspace &subspaceAtOrigin, coord_t distToEye, bool fullbright, Vector4f &ambientColor, duint *vLightListIdx) { if(fullbright) { ambientColor = Vector3f(1, 1, 1); *vLightListIdx = 0; } else { SectorCluster &cluster = subspaceAtOrigin.cluster(); Map &map = cluster.sector().map(); if(useBias && map.hasLightGrid()) { // Evaluate the position in the light grid. Vector4f color = map.lightGrid().evaluate(origin); // Apply light range compression. for(dint i = 0; i < 3; ++i) { color[i] += Rend_LightAdaptationDelta(color[i]); } ambientColor = color; } else { Vector4f const color = cluster.lightSourceColorfIntensity(); dfloat lightLevel = color.w; /* if(spr->type == VSPR_DECORATION) { // Wall decorations receive an additional light delta. lightLevel += R_WallAngleLightLevelDelta(line, side); } */ // Apply distance attenuation. lightLevel = Rend_AttenuateLightLevel(distToEye, lightLevel); // Add extra light. lightLevel = de::clamp(0.f, lightLevel + Rend_ExtraLightDelta(), 1.f); Rend_ApplyLightAdaptation(lightLevel); // Determine the final color. ambientColor = color * lightLevel; } Rend_ApplyTorchLight(ambientColor, distToEye); *vLightListIdx = Rend_CollectAffectingLights(origin, ambientColor, &subspaceAtOrigin); } } /// @todo use Mobj_OriginSmoothed static Vector3d mobjOriginSmoothed(mobj_t *mob) { DENG2_ASSERT(mob); coord_t origin[] = { mob->origin[0], mob->origin[1], mob->origin[2] }; // The client may have a Smoother for this object. if(isClient && mob->dPlayer && P_GetDDPlayerIdx(mob->dPlayer) != consolePlayer) { Smoother_Evaluate(clients[P_GetDDPlayerIdx(mob->dPlayer)].smoother, origin); } return origin; } /** * Determine the correct Z coordinate for the mobj. The visible Z coordinate * may be slightly different than the actual Z coordinate due to smoothed * plane movement. * * @todo fixme: Should use the visual plane heights of sector clusters. */ static void findMobjZOrigin(mobj_t &mob, bool floorAdjust, vissprite_t &vis) { validCount++; Mobj_Map(mob).forAllSectorsTouchingMobj(mob, [&mob, &floorAdjust, &vis] (Sector §or) { if(floorAdjust && mob.origin[2] == sector.floor().height()) { vis.pose.origin.z = sector.floor().heightSmoothed(); } if(mob.origin[2] + mob.height == sector.ceiling().height()) { vis.pose.origin.z = sector.ceiling().heightSmoothed() - mob.height; } return LoopContinue; }); } void R_ProjectSprite(mobj_t &mob) { /// @todo Lots of stuff here! This needs to be broken down into multiple functions /// and/or classes that handle preprocessing of visible entities. Keep in mind that /// data/state can persist across frames in the mobjs' private data. -jk // Not all objects can/will be visualized. Skip this object if: // ...hidden? if((mob.ddFlags & DDMF_DONTDRAW)) return; // ...not linked into the map? if(!Mobj_HasSubspace(mob)) return; // ...in an invalid state? if(!mob.state || !runtimeDefs.states.indexOf(mob.state)) return; // ...no sprite frame is defined? Sprite *sprite = Mobj_Sprite(mob); if(!sprite) return; // ...fully transparent? dfloat const alpha = Mobj_Alpha(mob); if(alpha <= 0) return; // ...origin lies in a sector with no volume? ConvexSubspace &subspace = Mobj_BspLeafAtOrigin(mob).subspace(); SectorCluster &cluster = subspace.cluster(); if(!cluster.hasWorldVolume()) return; ClientMobjThinkerData const *mobjData = THINKER_DATA_MAYBE(mob.thinker, ClientMobjThinkerData); // Determine distance to object. Vector3d const moPos = mobjOriginSmoothed(&mob); coord_t const distFromEye = Rend_PointDist2D(moPos); // Should we use a 3D model? ModelDef *mf = nullptr, *nextmf = nullptr; dfloat interp = 0; ModelDrawable::Animator const *animator = nullptr; // GL2 model present? if(useModels) { mf = Mobj_ModelDef(mob, &nextmf, &interp); if(mf) { // Use a sprite if the object is beyond the maximum model distance. if(maxModelDistance && !(mf->flags & MFF_NO_DISTANCE_CHECK) && distFromEye > maxModelDistance) { mf = nextmf = nullptr; interp = -1; } } if(mobjData) { animator = mobjData->animator(); } } bool const hasModel = (mf || animator); // Decide which material to use according to the sprite's angle and position // relative to that of the viewer. Material *mat = nullptr; bool matFlipS = false; bool matFlipT = false; try { SpriteViewAngle const &sprViewAngle = sprite->closestViewAngle(mob.angle, R_ViewPointToAngle(mob.origin), !!mf); mat = sprViewAngle.material; matFlipS = sprViewAngle.mirrorX; } catch(Sprite::MissingViewAngleError const &er) { // Log but otherwise ignore this error. LOG_GL_WARNING("Projecting sprite '%i' frame '%i': %s") << mob.sprite << mob.frame << er.asText(); } if(!mat) return; MaterialAnimator &matAnimator = mat->getAnimator(Rend_SpriteMaterialSpec(mob.tclass, mob.tmap)); // Ensure we've up to date info about the material. matAnimator.prepare(); Vector2i const &matDimensions = matAnimator.dimensions(); TextureVariant *tex = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; // A valid sprite texture in the "Sprites" scheme is required. if(!tex || tex->base().manifest().schemeName().compareWithoutCase("Sprites")) { return; } bool const fullbright = ((mob.state->flags & STF_FULLBRIGHT) != 0 || levelFullBright); // Align to the view plane? (Means scaling down Z with models) bool const viewAlign = (!mf && ((mob.ddFlags & DDMF_VIEWALIGN) || alwaysAlign == 1)) || alwaysAlign == 3; // Perform visibility checking by projecting a view-aligned line segment // relative to the viewer and determining if the whole of the segment has // been clipped away according to the 360 degree angle clipper. coord_t const visWidth = Mobj_VisualRadius(mob) * 2; /// @todo ignorant of rotation... Vector2d v1, v2; R_ProjectViewRelativeLine2D(moPos, mf || viewAlign, visWidth, (mf? 0 : coord_t(-tex->base().origin().x) - (visWidth / 2.0f)), v1, v2); // Not visible? if(!rendSys().angleClipper().checkRangeFromViewRelPoints(v1, v2)) { coord_t const MAX_OBJECT_RADIUS = 128; // Sprite visibility is absolute. if(!hasModel) return; // If the model is close to the viewpoint we should still to draw it, // otherwise large models are likely to disappear too early. viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); Vector2d delta(distFromEye, moPos.z + (mob.height / 2) - viewData->current.origin.z); if(M_ApproxDistance(delta.x, delta.y) > MAX_OBJECT_RADIUS) return; } // Store information in a vissprite. vissprite_t *vis = R_NewVisSprite(animator? VSPR_MODEL_GL2 : mf? VSPR_MODEL : VSPR_SPRITE); vis->pose.origin = moPos; vis->pose.distance = distFromEye; // The Z origin of the visual should match that of the mobj. When smoothing // is enabled this requires examining all touched sector planes in the vicinity. Plane &floor = cluster.visFloor(); Plane &ceiling = cluster.visCeiling(); bool floorAdjust = false; if(!Mobj_OriginBehindVisPlane(&mob)) { floorAdjust = de::abs(floor.heightSmoothed() - floor.height()) < 8; findMobjZOrigin(mob, floorAdjust, *vis); } coord_t topZ = vis->pose.origin.z + -tex->base().origin().y; // global z top // Determine floor clipping. coord_t floorClip = mob.floorClip; if(mob.ddFlags & DDMF_BOB) { // Bobbing is applied using floorclip. floorClip += Mobj_BobOffset(mob); } // Determine angles. /// @todo Surely this can be done in a subclass/function. -jk dfloat yaw = 0, pitch = 0; if(animator) { // TODO: More angle options with GL2 models. yaw = Mobj_AngleSmoothed(&mob) / dfloat( ANGLE_MAX ) * -360; } else if(mf) { // Determine the rotation angles (in degrees). if(mf->testSubFlag(0, MFF_ALIGN_YAW)) { // Transform the origin point. viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); Vector2d delta(moPos.y - viewData->current.origin.y, moPos.x - viewData->current.origin.x); yaw = 90 - (BANG2RAD(bamsAtan2(delta.x * 10, delta.y * 10)) - PI / 2) / PI * 180; } else if(mf->testSubFlag(0, MFF_SPIN)) { yaw = modelSpinSpeed * 70 * App_WorldSystem().time() + MOBJ_TO_ID(&mob) % 360; } else if(mf->testSubFlag(0, MFF_MOVEMENT_YAW)) { yaw = R_MovementXYYaw(mob.mom[0], mob.mom[1]); } else { yaw = Mobj_AngleSmoothed(&mob) / dfloat( ANGLE_MAX ) * -360; } // How about a unique offset? if(mf->testSubFlag(0, MFF_IDANGLE)) { yaw += MOBJ_TO_ID(&mob) % 360; // arbitrary } if(mf->testSubFlag(0, MFF_ALIGN_PITCH)) { viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); Vector2d delta(vis->pose.midZ() - viewData->current.origin.z, distFromEye); pitch = -BANG2DEG(bamsAtan2(delta.x * 10, delta.y * 10)); } else if(mf->testSubFlag(0, MFF_MOVEMENT_PITCH)) { pitch = R_MovementXYZPitch(mob.mom[0], mob.mom[1], mob.mom[2]); } else { pitch = 0; } } // Determine possible short-range visual offset. Vector3d visOff; if((hasModel && useSRVO > 0) || (!hasModel && useSRVO > 1)) { if(mob.tics >= 0) { visOff = Vector3d(mob.srvo) * (mob.tics - frameTimePos) / (float) mob.state->tics; } if(!INRANGE_OF(mob.mom[0], 0, NOMOMENTUM_THRESHOLD) || !INRANGE_OF(mob.mom[1], 0, NOMOMENTUM_THRESHOLD) || !INRANGE_OF(mob.mom[2], 0, NOMOMENTUM_THRESHOLD)) { // Use the object's speed to calculate a short-range offset. visOff += Vector3d(mob.mom) * frameTimePos; } } // Will it be drawn as a 2D sprite? if(!hasModel) { bool const brightShadow = (mob.ddFlags & DDMF_BRIGHTSHADOW) != 0; bool const fitTop = (mob.ddFlags & DDMF_FITTOP) != 0; bool const fitBottom = (mob.ddFlags & DDMF_NOFITBOTTOM) == 0; // Additive blending? blendmode_t blendMode; if(brightShadow) { blendMode = BM_ADD; } // Use the "no translucency" blending mode? else if(noSpriteTrans && alpha >= .98f) { blendMode = BM_ZEROALPHA; } else { blendMode = BM_NORMAL; } // We must find the correct positioning using the sector floor // and ceiling heights as an aid. if(matDimensions.y < ceiling.heightSmoothed() - floor.heightSmoothed()) { // Sprite fits in, adjustment possible? if(fitTop && topZ > ceiling.heightSmoothed()) topZ = ceiling.heightSmoothed(); if(floorAdjust && fitBottom && topZ - matDimensions.y < floor.heightSmoothed()) topZ = floor.heightSmoothed() + matDimensions.y; } // Adjust by the floor clip. topZ -= floorClip; Vector3d const origin(vis->pose.origin.x, vis->pose.origin.y, topZ - matDimensions.y / 2.0f); Vector4f ambientColor; duint vLightListIdx = 0; evaluateLighting(origin, subspace, vis->pose.distance, fullbright, ambientColor, &vLightListIdx); // Apply uniform alpha (overwritting intensity factor). ambientColor.w = alpha; VisSprite_SetupSprite(vis, VisEntityPose(origin, visOff, viewAlign), VisEntityLighting(ambientColor, vLightListIdx), floor.heightSmoothed(), ceiling.heightSmoothed(), floorClip, topZ, *mat, matFlipS, matFlipT, blendMode, mob.tclass, mob.tmap, &Mobj_BspLeafAtOrigin(mob), floorAdjust, fitTop, fitBottom); } else // It will be drawn as a 3D model. { Vector4f ambientColor; duint vLightListIdx = 0; evaluateLighting(vis->pose.origin, subspace, vis->pose.distance, fullbright, ambientColor, &vLightListIdx); // Apply uniform alpha (overwritting intensity factor). ambientColor.w = alpha; if(animator) { // Set up a GL2 model for drawing. vis->pose = VisEntityPose(vis->pose.origin, Vector3d(visOff.x, visOff.y, visOff.z - floorClip), viewAlign, topZ, yaw, 0, pitch, 0); vis->light = VisEntityLighting(ambientColor, vLightListIdx); vis->data.model2.object = &mob; vis->data.model2.animator = animator; vis->data.model2.model = &animator->model(); } else { DENG2_ASSERT(mf); VisSprite_SetupModel(vis, VisEntityPose(vis->pose.origin, Vector3d(visOff.x, visOff.y, visOff.z - floorClip), viewAlign, topZ, yaw, 0, pitch, 0), VisEntityLighting(ambientColor, vLightListIdx), mf, nextmf, interp, mob.thinker.id, mob.selector, &Mobj_BspLeafAtOrigin(mob), mob.ddFlags, mob.tmap, fullbright && !(mf && mf->testSubFlag(0, MFF_DIM)), false); } } // Do we need to project a flare source too? if(mob.lumIdx != Lumobj::NoIndex && haloMode > 0) { /// @todo mark this light source visible for LensFx try { SpriteViewAngle const &sprViewAngle = sprite->closestViewAngle(mob.angle, R_ViewPointToAngle(mob.origin)); DENG2_ASSERT(sprViewAngle.material); MaterialAnimator &matAnimator = sprViewAngle.material->getAnimator(Rend_SpriteMaterialSpec(mob.tclass, mob.tmap)); // Ensure we've up to date info about the material. matAnimator.prepare(); Vector2i const &matDimensions = matAnimator.dimensions(); TextureVariant *tex = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; // A valid sprite texture in the "Sprites" scheme is required. if(!tex || tex->base().manifest().schemeName().compareWithoutCase("Sprites")) { return; } auto const *pl = (pointlight_analysis_t const *) tex->base().analysisDataPointer(Texture::BrightPointAnalysis); DENG2_ASSERT(pl); Lumobj const &lob = cluster.sector().map().lumobj(mob.lumIdx); vissprite_t *vis = R_NewVisSprite(VSPR_FLARE); vis->pose.distance = distFromEye; // Determine the exact center of the flare. vis->pose.origin = moPos + visOff; vis->pose.origin.z += lob.zOffset(); dfloat flareSize = pl->brightMul; // X offset to the flare position. dfloat xOffset = matDimensions.x * pl->originX - -tex->base().origin().x; // Does the mobj have an active light definition? ded_light_t const *def = (mob.state? runtimeDefs.stateInfo[runtimeDefs.states.indexOf(mob.state)].light : 0); if(def) { if(def->size) flareSize = def->size; if(def->haloRadius) flareSize = def->haloRadius; if(def->offset[0]) xOffset = def->offset[0]; vis->data.flare.flags = def->flags; } vis->data.flare.size = flareSize * 60 * (50 + haloSize) / 100.0f; if(vis->data.flare.size < 8) vis->data.flare.size = 8; // Color is taken from the associated lumobj. V3f_Set(vis->data.flare.color, lob.color().x, lob.color().y, lob.color().z); vis->data.flare.factor = mob.haloFactors[viewPlayer - ddPlayers]; vis->data.flare.xOff = xOffset; vis->data.flare.mul = 1; vis->data.flare.tex = 0; if(def && def->flare) { de::Uri const &flaremapResourceUri = *def->flare; if(flaremapResourceUri.path().toStringRef().compareWithoutCase("-")) { vis->data.flare.tex = GL_PrepareFlaremap(flaremapResourceUri); } } } catch(Sprite::MissingViewAngleError const &er) { // Log but otherwise ignore this error. LOG_GL_WARNING("Projecting flare source for sprite '%i' frame '%i': %s") << mob.sprite << mob.frame << er.asText(); } } } doomsday-stable-1.15.7/doomsday/client/src/render/blockmapvisual.cpp0000664000175000017500000004366212641367670025072 0ustar jaakkojaakko/** @file blockmapvisual.cpp Graphical Blockmap Visual. * @ingroup world * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include "de_base.h" #include "de_graphics.h" #include "de_render.h" #include "de_ui.h" #include "api_fontrender.h" #include "Face" #include "HEdge" #include "gl/gl_texmanager.h" #include "world/blockmap.h" #include "world/lineblockmap.h" #include "world/map.h" #include "world/p_object.h" #include "world/p_players.h" #include "ConvexSubspace" #include "render/blockmapvisual.h" using namespace de; byte bmapShowDebug; // 1 = mobjs, 2 = lines, 3 = BSP leafs, 4 = polyobjs. cvar float bmapDebugSize = 1.5f; // cvar static void drawMobj(mobj_t const &mobj) { AABoxd const bounds = Mobj_AABox(mobj); glVertex2f(bounds.minX, bounds.minY); glVertex2f(bounds.maxX, bounds.minY); glVertex2f(bounds.maxX, bounds.maxY); glVertex2f(bounds.minX, bounds.maxY); } static void drawLine(Line const &line) { glVertex2f(line.fromOrigin().x, line.fromOrigin().y); glVertex2f( line.toOrigin().x, line.toOrigin().y); } static void drawSubspace(ConvexSubspace const &subspace) { float const scale = de::max(bmapDebugSize, 1.f); float const width = (DENG_GAMEVIEW_WIDTH / 16) / scale; Face const &poly = subspace.poly(); HEdge *base = poly.hedge(); HEdge *hedge = base; do { Vector2d start = hedge->origin(); Vector2d end = hedge->twin().origin(); glBegin(GL_LINES); glVertex2f(start.x, start.y); glVertex2f(end.x, end.y); glEnd(); ddouble length = (end - start).length(); if(length > 0) { Vector2d const unit = (end - start) / length; Vector2d const normal(-unit.y, unit.x); GL_BindTextureUnmanaged(GL_PrepareLSTexture(LST_DYNAMIC)); glEnable(GL_TEXTURE_2D); GL_BlendMode(BM_ADD); glBegin(GL_QUADS); glTexCoord2f(0.75f, 0.5f); glVertex2f(start.x, start.y); glTexCoord2f(0.75f, 0.5f); glVertex2f(end.x, end.y); glTexCoord2f(0.75f, 1); glVertex2f(end.x - normal.x * width, end.y - normal.y * width); glTexCoord2f(0.75f, 1); glVertex2f(start.x - normal.x * width, start.y - normal.y * width); glEnd(); glDisable(GL_TEXTURE_2D); GL_BlendMode(BM_NORMAL); } // Draw a bounding box for the leaf's face geometry. start = Vector2d(poly.aaBox().minX, poly.aaBox().minY); end = Vector2d(poly.aaBox().maxX, poly.aaBox().maxY); glBegin(GL_LINES); glVertex2f(start.x, start.y); glVertex2f( end.x, start.y); glVertex2f( end.x, start.y); glVertex2f( end.x, end.y); glVertex2f( end.x, end.y); glVertex2f(start.x, end.y); glVertex2f(start.x, end.y); glVertex2f(start.x, start.y); glEnd(); } while((hedge = &hedge->next()) != base); } static int drawCellLines(Blockmap const &bmap, BlockmapCell const &cell, void *) { glBegin(GL_LINES); bmap.forAllInCell(cell, [] (void *object) { Line &line = *(Line *)object; if(line.validCount() != validCount) { line.setValidCount(validCount); drawLine(line); } return LoopContinue; }); glEnd(); return false; // Continue iteration. } static int drawCellPolyobjs(Blockmap const &bmap, BlockmapCell const &cell, void *context) { glBegin(GL_LINES); bmap.forAllInCell(cell, [&context] (void *object) { Polyobj &pob = *(Polyobj *)object; for(Line *line : pob.lines()) { if(line->validCount() != validCount) { line->setValidCount(validCount); drawLine(*line); } } return LoopContinue; }); glEnd(); return false; // Continue iteration. } static int drawCellMobjs(Blockmap const &bmap, BlockmapCell const &cell, void *) { glBegin(GL_QUADS); bmap.forAllInCell(cell, [] (void *object) { mobj_t &mob = *(mobj_t *)object; if(mob.validCount != validCount) { mob.validCount = validCount; drawMobj(mob); } return LoopContinue; }); glEnd(); return false; // Continue iteration. } static int drawCellSubspaces(Blockmap const &bmap, BlockmapCell const &cell, void *) { bmap.forAllInCell(cell, [] (void *object) { ConvexSubspace *sub = (ConvexSubspace *)object; if(sub->validCount() != validCount) { sub->setValidCount(validCount); drawSubspace(*sub); } return LoopContinue; }); return false; // Continue iteration. } static void drawBackground(Blockmap const &bmap) { BlockmapCell const &dimensions = bmap.dimensions(); // Scale modelview matrix so we can express cell geometry // using a cell-sized unit coordinate space. glMatrixMode(GL_MODELVIEW); glPushMatrix(); glScalef(bmap.cellSize(), bmap.cellSize(), 1); /* * Draw the translucent quad which represents the "used" cells. */ glColor4f(.25f, .25f, .25f, .66f); glBegin(GL_QUADS); glVertex2f(0, 0); glVertex2f(dimensions.x, 0); glVertex2f(dimensions.x, dimensions.y); glVertex2f(0, dimensions.y); glEnd(); /* * Draw the "null cells" over the top. */ glColor4f(0, 0, 0, .95f); BlockmapCell cell; for(cell.y = 0; cell.y < dimensions.y; ++cell.y) for(cell.x = 0; cell.x < dimensions.x; ++cell.x) { if(bmap.cellElementCount(cell)) continue; glBegin(GL_QUADS); glVertex2f(cell.x, cell.y); glVertex2f(cell.x + 1, cell.y); glVertex2f(cell.x + 1, cell.y + 1); glVertex2f(cell.x, cell.y + 1); glEnd(); } // Restore previous GL state. glMatrixMode(GL_MODELVIEW); glPopMatrix(); } static void drawCellInfo(Vector2d const &origin_, char const *info) { glEnable(GL_TEXTURE_2D); FR_SetFont(fontFixed); FR_LoadDefaultAttrib(); FR_SetShadowOffset(UI_SHADOW_OFFSET, UI_SHADOW_OFFSET); FR_SetShadowStrength(UI_SHADOW_STRENGTH); Size2Raw size(16 + FR_TextWidth(info), 16 + FR_SingleLineHeight(info)); Point2Raw origin(origin_.x, origin_.y); origin.x -= size.width / 2; UI_GradientEx(&origin, &size, 6, UI_Color(UIC_BG_MEDIUM), UI_Color(UIC_BG_LIGHT), .5f, .5f); UI_DrawRectEx(&origin, &size, 6, false, UI_Color(UIC_BRD_HI), NULL, .5f, -1); origin.x += 8; origin.y += size.height / 2; UI_SetColor(UI_Color(UIC_TEXT)); UI_TextOutEx2(info, &origin, UI_Color(UIC_TITLE), 1, ALIGN_LEFT, DTF_ONLY_SHADOW); glDisable(GL_TEXTURE_2D); } static void drawBlockmapInfo(Vector2d const &origin_, Blockmap const &blockmap) { glEnable(GL_TEXTURE_2D); Point2Raw origin(origin_.x, origin_.y); FR_SetFont(fontFixed); FR_LoadDefaultAttrib(); FR_SetShadowOffset(UI_SHADOW_OFFSET, UI_SHADOW_OFFSET); FR_SetShadowStrength(UI_SHADOW_STRENGTH); Size2Raw size; size.width = 16 + FR_TextWidth("(+000.0, +000.0) (+000.0, +000.0)"); int th = FR_SingleLineHeight("Info"); size.height = th * 4 + 16; origin.x -= size.width; origin.y -= size.height; UI_GradientEx(&origin, &size, 6, UI_Color(UIC_BG_MEDIUM), UI_Color(UIC_BG_LIGHT), .5f, .5f); UI_DrawRectEx(&origin, &size, 6, false, UI_Color(UIC_BRD_HI), NULL, .5f, -1); origin.x += 8; origin.y += 8 + th/2; UI_TextOutEx2("Blockmap", &origin, UI_Color(UIC_TITLE), 1, ALIGN_LEFT, DTF_ONLY_SHADOW); origin.y += th; Vector2ui const &bmapDimensions = blockmap.dimensions(); char buf[80]; dd_snprintf(buf, 80, "Dimensions:(%u, %u) #%li", bmapDimensions.x, bmapDimensions.y, (long) bmapDimensions.y * bmapDimensions.x); UI_TextOutEx2(buf, &origin, UI_Color(UIC_TEXT), 1, ALIGN_LEFT, DTF_ONLY_SHADOW); origin.y += th; dd_snprintf(buf, 80, "Cell dimensions:(%.3f, %.3f)", blockmap.cellSize(), blockmap.cellSize()); UI_TextOutEx2(buf, &origin, UI_Color(UIC_TEXT), 1, ALIGN_LEFT, DTF_ONLY_SHADOW); origin.y += th; dd_snprintf(buf, 80, "(%+06.0f, %+06.0f) (%+06.0f, %+06.0f)", blockmap.bounds().minX, blockmap.bounds().minY, blockmap.bounds().maxX, blockmap.bounds().maxY); UI_TextOutEx2(buf, &origin, UI_Color(UIC_TEXT), 1, ALIGN_LEFT, DTF_ONLY_SHADOW); glDisable(GL_TEXTURE_2D); } static void drawCellInfoBox(Vector2d const &origin, Blockmap const &blockmap, char const *objectTypeName, BlockmapCell const &cell) { uint count = blockmap.cellElementCount(cell); char info[160]; dd_snprintf(info, 160, "Cell:(%u, %u) %s:#%u", cell.x, cell.y, objectTypeName, count); drawCellInfo(origin, info); } /** * @param bmap Blockmap to be rendered. * @param followMobj Mobj to center/focus the visual on. Can be @c 0. * @param cellDrawer Blockmap cell content drawing callback. Can be @c 0. */ static void drawBlockmap(Blockmap const &bmap, mobj_t *followMobj, int (*cellDrawer) (Blockmap const &, BlockmapCell const &, void *)) { BlockmapCellBlock vCellBlock; BlockmapCell vCell; BlockmapCell const &dimensions = bmap.dimensions(); Vector2d const cellDimensions = bmap.cellDimensions(); if(followMobj) { // Determine the followed Mobj's blockmap coords. bool didClip; vCell = bmap.toCell(followMobj->origin, &didClip); if(didClip) followMobj = 0; // Outside the blockmap. if(followMobj) { // Determine the extended blockmap coords for the followed // Mobj's "touch" range. AABoxd aaBox = Mobj_AABox(*followMobj); aaBox.minX -= DDMOBJ_RADIUS_MAX * 2; aaBox.minY -= DDMOBJ_RADIUS_MAX * 2; aaBox.maxX += DDMOBJ_RADIUS_MAX * 2; aaBox.maxY += DDMOBJ_RADIUS_MAX * 2; vCellBlock = bmap.toCellBlock(aaBox); } } if(followMobj) { // Orient on the center of the followed Mobj. glTranslated(-(vCell.x * cellDimensions.x), -(vCell.y * cellDimensions.y), 0); } else { // Orient on center of the Blockmap. glTranslated(-(cellDimensions.x * dimensions.x)/2, -(cellDimensions.y * dimensions.y)/2, 0); } // First we'll draw a background showing the "null" cells. drawBackground(bmap); if(followMobj) { // Highlight cells the followed Mobj "touches". glBegin(GL_QUADS); BlockmapCell cell; for(cell.y = vCellBlock.min.y; cell.y < vCellBlock.max.y; ++cell.y) for(cell.x = vCellBlock.min.x; cell.x < vCellBlock.max.x; ++cell.x) { if(cell == vCell) { // The cell the followed Mobj is actually in. glColor4f(.66f, .66f, 1, .66f); } else { // A cell within the followed Mobj's extended collision range. glColor4f(.33f, .33f, .66f, .33f); } Vector2d const start = cellDimensions * cell; Vector2d const end = start + cellDimensions; glVertex2d(start.x, start.y); glVertex2d( end.x, start.y); glVertex2d( end.x, end.y); glVertex2d(start.x, end.y); } glEnd(); } /** * Draw the Gridmap visual. * @note Gridmap uses a cell unit size of [width:1,height:1], so we need to * scale it up so it aligns correctly. */ glMatrixMode(GL_MODELVIEW); glPushMatrix(); glScaled(cellDimensions.x, cellDimensions.y, 1); bmap.drawDebugVisual(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); /* * Draw the blockmap-linked data. * Translate the modelview matrix so that objects can be drawn using * the map space coordinates directly. */ glMatrixMode(GL_MODELVIEW); glPushMatrix(); glTranslated(-bmap.origin().x, -bmap.origin().y, 0); if(cellDrawer) { if(followMobj) { /* * Draw cell contents color coded according to their range from the * followed Mobj. */ // First, the cells outside the "touch" range (crimson). validCount++; glColor4f(.33f, 0, 0, .75f); BlockmapCell cell; for(cell.y = 0; cell.y < dimensions.y; ++cell.y) for(cell.x = 0; cell.x < dimensions.x; ++cell.x) { if(cell.x >= vCellBlock.min.x && cell.x < vCellBlock.max.x && cell.y >= vCellBlock.min.y && cell.y < vCellBlock.max.y) { continue; } if(!bmap.cellElementCount(cell)) continue; cellDrawer(bmap, cell, 0/*no params*/); } // Next, the cells within the "touch" range (orange). validCount++; glColor3f(1, .5f, 0); for(cell.y = vCellBlock.min.y; cell.y < vCellBlock.max.y; ++cell.y) for(cell.x = vCellBlock.min.x; cell.x < vCellBlock.max.x; ++cell.x) { if(cell == vCell) continue; if(!bmap.cellElementCount(cell)) continue; cellDrawer(bmap, cell, 0/*no params*/); } // Lastly, the cell the followed Mobj is in (yellow). validCount++; glColor3f(1, 1, 0); if(bmap.cellElementCount(vCell)) { cellDrawer(bmap, vCell, 0/*no params*/); } } else { // Draw all cells without color coding. validCount++; glColor4f(.33f, 0, 0, .75f); BlockmapCell cell; for(cell.y = 0; cell.y < dimensions.y; ++cell.y) for(cell.x = 0; cell.x < dimensions.x; ++cell.x) { if(!bmap.cellElementCount(cell)) continue; cellDrawer(bmap, cell, 0/*no params*/); } } } /* * Draw the followed mobj, if any. */ if(followMobj) { validCount++; glColor3f(0, 1, 0); glBegin(GL_QUADS); drawMobj(*followMobj); glEnd(); } // Undo the map coordinate space translation. glMatrixMode(GL_MODELVIEW); glPopMatrix(); } void Rend_BlockmapDebug() { // Disabled? if(!bmapShowDebug || bmapShowDebug > 4) return; if(!App_WorldSystem().hasMap()) return; Map &map = App_WorldSystem().map(); Blockmap const *blockmap; int (*cellDrawer) (Blockmap const &, BlockmapCell const &, void *); char const *objectTypeName; switch(bmapShowDebug) { default: // Mobjs. blockmap = &map.mobjBlockmap(); cellDrawer = drawCellMobjs; objectTypeName = "Mobjs"; break; case 2: // Lines. blockmap = &map.lineBlockmap(); cellDrawer = drawCellLines; objectTypeName = "Lines"; break; case 3: // BSP leafs. blockmap = &map.subspaceBlockmap(); cellDrawer = drawCellSubspaces; objectTypeName = "Subspaces"; break; case 4: // Polyobjs. blockmap = &map.polyobjBlockmap(); cellDrawer = drawCellPolyobjs; objectTypeName = "Polyobjs"; break; } DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); /* * Draw the blockmap. */ glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, DENG_GAMEVIEW_WIDTH, DENG_GAMEVIEW_HEIGHT, 0, -1, 1); // Orient on the center of the window. glTranslatef((DENG_GAMEVIEW_WIDTH / 2), (DENG_GAMEVIEW_HEIGHT / 2), 0); // Uniform scaling factor for this visual. float scale = bmapDebugSize / de::max(DENG_GAMEVIEW_HEIGHT / 100, 1); glScalef(scale, -scale, 1); // If possible we'll tailor what we draw relative to the viewPlayer. mobj_t *followMobj = 0; if(viewPlayer && viewPlayer->shared.mo) followMobj = viewPlayer->shared.mo; // Draw! drawBlockmap(*blockmap, followMobj, cellDrawer); glMatrixMode(GL_PROJECTION); glPopMatrix(); /* * Draw HUD info. */ glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, DENG_GAMEVIEW_WIDTH, DENG_GAMEVIEW_HEIGHT, 0, -1, 1); if(followMobj) { // About the cell the followed Mobj is in. bool didClip; BlockmapCell cell = blockmap->toCell(followMobj->origin, &didClip); if(!didClip) { drawCellInfoBox(Vector2d(DENG_GAMEVIEW_WIDTH / 2, 30), *blockmap, objectTypeName, cell); } } // About the Blockmap itself. drawBlockmapInfo(Vector2d(DENG_GAMEVIEW_WIDTH - 10, DENG_GAMEVIEW_HEIGHT - 10), *blockmap); glMatrixMode(GL_PROJECTION); glPopMatrix(); } doomsday-stable-1.15.7/doomsday/client/src/render/skydrawable.cpp0000664000175000017500000006106412641367670024362 0ustar jaakkojaakko/** @file skydrawable.cpp Drawable specialized for the sky. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "render/skydrawable.h" #include #include #include #include #include #include #include "clientapp.h" #include "client/cl_def.h" // clientPaused #include "gl/gl_main.h" #include "gl/gl_tex.h" #include "MaterialVariantSpec" #include "ModelDef" #include "render/rend_main.h" #include "render/rend_model.h" #include "render/vissprite.h" #include "Texture" #include "world/sky.h" #define MAX_LAYERS 2 #define MAX_MODELS 32 using namespace de; namespace internal { /// Console variables: static int sphereDetail = 6; static float sphereDistance = 1600; ///< Map units. static int sphereRows = 3; ///< Per hemisphere. static inline ResourceSystem &resSys() { return ClientApp::resourceSystem(); } /** * Effective layer configuration used with both sphere and model drawing. */ struct LayerData { bool active; ///< @c true= the layer is active/enabled (will be drawn). float offset; ///< Layer material offset (along the horizon). }; /** * Geometry used with the sky sphere. The crest of the hemisphere is up (i.e., y+). * * @todo Should be a subcomponent of SkyDrawable (cleaner interface). -ds */ struct Hemisphere { enum SphereComponent { UpperHemisphere, LowerHemisphere }; int rows = 3; int columns = 4 * 6; float height = 0.49f; float horizonOffset = -0.105f; typedef QVector VBuf; VBuf verts; bool needRebuild = true; // Look up the precalculated vertex. inline Vector3f const &vertex(int r, int c) const { return verts[r * columns + c % columns]; } /** * Determine the material to use for the given sky @a layer. */ static Material *chooseMaterialForSkyLayer(SkyLayer const *layer) { DENG2_ASSERT(layer); if(renderTextures == 0) { return nullptr; } if(renderTextures == 2) { return resSys().materialPtr(de::Uri("System", Path("gray"))); } if(Material *mat = layer->material()) { return mat; } return resSys().materialPtr(de::Uri("System", Path("missing"))); } /** * Determine the cap/fadeout color to use for the given sky @a layer. */ static Vector3f chooseCapColor(SphereComponent hemisphere, SkyLayer const *layer, bool *needFadeOut = nullptr) { DENG2_ASSERT(layer); if(Material *mat = chooseMaterialForSkyLayer(layer)) { MaterialAnimator &matAnimator = mat->getAnimator(SkyDrawable::layerMaterialSpec(layer->isMasked())); // Ensure we've up to date info about the material. matAnimator.prepare(); Texture &pTex = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture->base(); averagecolor_analysis_t const *avgColor = reinterpret_cast (pTex.analysisDataPointer((hemisphere == UpperHemisphere? Texture::AverageTopColorAnalysis : Texture::AverageBottomColorAnalysis))); if(!avgColor) { de::Uri const pTexUri = pTex.manifest().composeUri(); throw Error("Hemisphere::capColor", QString("Texture \"%1\" has no Average%2ColorAnalysis") .arg(pTexUri) .arg(hemisphere == UpperHemisphere? "Top" : "Bottom")); } // Is the colored fadeout in use? Vector3f color(avgColor->color.rgb); float const fadeOutLimit = layer->fadeOutLimit(); if(color.x >= fadeOutLimit || color.y >= fadeOutLimit || color.z >= fadeOutLimit) { if(needFadeOut) *needFadeOut = true; return color; } } return Vector3f(); // Default color is black. } void drawCap(Vector3f const &color, bool drawFadeOut) const { GL_SetNoTexture(); glColor3f(color.x, color.y, color.z); // Draw the cap. glBegin(GL_TRIANGLE_FAN); for(int c = 0; c < columns; ++c) { Vector3f const &vtx = vertex(0, c); glVertex3f(vtx.x, vtx.y, vtx.z); } glEnd(); // Are we doing a colored fadeout? if(!drawFadeOut) return; // We must fill the background for the top row since it'll be translucent. glBegin(GL_TRIANGLE_STRIP); Vector3f const *vtx = &vertex(0, 0); glVertex3f(vtx->x, vtx->y, vtx->z); int c = 0; for(; c < columns; ++c) { // One step down. vtx = &vertex(1, c); glVertex3f(vtx->x, vtx->y, vtx->z); // And one step right. vtx = &vertex(0, c + 1); glVertex3f(vtx->x, vtx->y, vtx->z); } vtx = &vertex(1, c); glVertex3f(vtx->x, vtx->y, vtx->z); glEnd(); } void draw(SphereComponent hemisphere, Sky const &sky, int firstActiveLayer, LayerData const *layerData) const { DENG2_ASSERT(layerData); if(verts.isEmpty()) return; if(firstActiveLayer < 0) return; bool const yflip = (hemisphere == LowerHemisphere); if(yflip) { // The lower hemisphere must be flipped. glMatrixMode(GL_MODELVIEW); glPushMatrix(); glScalef(1.0f, -1.0f, 1.0f); } // First draw the cap and the background for fadeouts, if needed. bool drawFadeOut = true; drawCap(chooseCapColor(hemisphere, sky.layer(firstActiveLayer), &drawFadeOut), drawFadeOut); for(int i = firstActiveLayer; i < MAX_LAYERS; ++i) { SkyLayer const *skyLayer = sky.layer(i); LayerData const &ldata = layerData[i]; if(!ldata.active) continue; TextureVariant *layerTex = nullptr; if(Material *mat = chooseMaterialForSkyLayer(skyLayer)) { MaterialAnimator &matAnimator = mat->getAnimator(SkyDrawable::layerMaterialSpec(skyLayer->isMasked())); // Ensure we've up to date info about the material. matAnimator.prepare(); layerTex = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; GL_BindTexture(layerTex); glEnable(GL_TEXTURE_2D); glMatrixMode(GL_TEXTURE); glPushMatrix(); glLoadIdentity(); Vector2i const &texSize = layerTex->base().dimensions(); if(texSize.x > 0) { glTranslatef(ldata.offset / texSize.x, 0, 0); glScalef(1024.f / texSize.x, 1, 1); } if(yflip) { glScalef(1, -1, 1); glTranslatef(0, -1, 0); } } else { GL_SetNoTexture(); } #define WRITESKYVERTEX(r_, c_) { \ svtx = &vertex(r_, c_); \ if(layerTex) \ { \ glTexCoord2f((c_) / float(columns), (r_) / float(rows)); \ } \ if(drawFadeOut) \ { \ if((r_) == 0) glColor4f(1, 1, 1, 0); \ else glColor3f(1, 1, 1); \ } \ else \ { \ if((r_) == 0) glColor3f(0, 0, 0); \ else glColor3f(1, 1, 1); \ } \ glVertex3f(svtx->x, svtx->y, svtx->z); \ } Vector3f const *svtx; for(int r = 0; r < rows; ++r) { glBegin(GL_TRIANGLE_STRIP); WRITESKYVERTEX(r, 0); WRITESKYVERTEX(r + 1, 0); for(int c = 1; c <= columns; ++c) { WRITESKYVERTEX(r, c); WRITESKYVERTEX(r + 1, c); } glEnd(); } if(layerTex) { glMatrixMode(GL_TEXTURE); glPopMatrix(); glDisable(GL_TEXTURE_2D); } #undef WRITESKYVERTEX } if(yflip) { glMatrixMode(GL_MODELVIEW); glPopMatrix(); } } /** * The top row (row 0) is the one that's faded out. * There must be at least 4 columns. The preferable number is 4n, where * n is 1, 2, 3... There should be at least two rows because the first * one is always faded. * * The total number of triangles per hemisphere can be calculated thus: * * Sum: rows * columns * 2 + (hemisphere) * rows * 2 + (fadeout) * rows - 2 (cap) */ void makeVertices(float height, float horizonOffset) { DENG2_ASSERT(height > 0); rows = de::max(sphereRows, 1); columns = de::max(sphereDetail, 1) * 4; verts.resize(columns * (rows + 1)); float const maxSideAngle = float(de::PI / 2 * height); float const sideOffset = float(de::PI / 2 * horizonOffset); for(int r = 0; r < rows + 1; ++r) for(int c = 0; c < columns; ++c) { Vector3f &svtx = verts[r * columns + c % columns]; float const topAngle = ((c / float(columns)) * 2) * PI; float const sideAngle = sideOffset + maxSideAngle * (rows - r) / float(rows); float const radius = cos(sideAngle); svtx = Vector3f(radius * cos(topAngle), sin(sideAngle), // The height. radius * sin(topAngle)); } } }; // All SkyDrawables use the same hemisphere model. static Hemisphere hemisphere; } // namespace internal using namespace ::internal; DENG2_PIMPL(SkyDrawable) , DENG2_OBSERVES(Sky, Deletion) , DENG2_OBSERVES(Sky, HeightChange) , DENG2_OBSERVES(Sky, HorizonOffsetChange) { Sky const *sky = nullptr; float height = 1; bool needHeightUpdate = false; float horizonOffset = 0; bool needHorizonOffsetUpdate = false; LayerData layers[MAX_LAYERS]; int firstActiveLayer = -1; ///< @c -1= no active layers. bool needBuildHemisphere = true; struct ModelData { ModelDef *modef = nullptr; }; ModelData models[MAX_MODELS]; bool haveModels = false; bool alwaysDrawSphere = false; Instance(Public *i) : Base(i) { de::zap(models); } ~Instance() { // Stop observing Sky change notifications (if observing). self.configure(); } /** * Prepare for drawing; determine layer configuration, sphere dimensions, etc.. */ void prepare(Animator const *animator) { // Determine the layer configuration. Note that this is also used for sky // models, even if the sphere is not being drawn. firstActiveLayer = -1; for(int i = 0; i < MAX_LAYERS; ++i) { SkyLayer const *skyLayer = sky->layer(i); layers[i].active = skyLayer->isActive(); layers[i].offset = skyLayer->offset() + animator->layer(i).offset; if(firstActiveLayer == -1 && layers[i].active) { firstActiveLayer = i; } } updateHeightIfNeeded(); updateHorizonOffsetIfNeeded(); if(needBuildHemisphere || hemisphere.needRebuild) { needBuildHemisphere = false; hemisphere.makeVertices(height, horizonOffset); } } void drawSphere() const { if(haveModels && !alwaysDrawSphere) return; // We don't want anything written in the depth buffer. glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); // Disable culling, all triangles face the viewer. glDisable(GL_CULL_FACE); // Setup a proper matrix. glMatrixMode(GL_MODELVIEW); glPushMatrix(); glTranslatef(vOrigin.x, vOrigin.y, vOrigin.z); glScalef(sphereDistance, sphereDistance, sphereDistance); // Always draw both hemispheres. hemisphere.draw(Hemisphere::LowerHemisphere, *sky, firstActiveLayer, layers); hemisphere.draw(Hemisphere::UpperHemisphere, *sky, firstActiveLayer, layers); glMatrixMode(GL_MODELVIEW); glPopMatrix(); // Restore assumed default GL state. glEnable(GL_CULL_FACE); glDepthMask(GL_TRUE); glEnable(GL_DEPTH_TEST); } void drawModels(Animator const *animator) const { if(!haveModels) return; // Sky models use depth testing, but they won't interfere with world geometry. glEnable(GL_DEPTH_TEST); glDepthMask(GL_TRUE); glClear(GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glPushMatrix(); // Setup basic translation. glTranslatef(vOrigin.x, vOrigin.y, vOrigin.z); for(int i = 0; i < MAX_MODELS; ++i) { ModelData const &mdata = models[i]; // Is this model in use? ModelDef *modef = mdata.modef; if(!modef) continue; // If the associated layer is not active then the model won't be drawn. Record const &skyModelDef = defn::Sky(*sky->def()).model(i); int const layerNum = skyModelDef.geti("layer"); if(layerNum > 0 && layerNum <= MAX_LAYERS) { if(!layers[layerNum - 1].active) continue; } Animator::ModelState const &mstate = animator->model(i); // Prepare a vissprite for ordered drawing. vissprite_t vis; de::zap(vis); vis.pose.origin = vOrigin.xzy() * -Vector3f(skyModelDef.get("originOffset")).xzy(); vis.pose.topZ = vis.pose.origin.z; vis.pose.distance = 1; Vector2f rotate(skyModelDef.get("rotate")); vis.pose.yaw = mstate.yaw; vis.pose.extraYawAngle = vis.pose.yawAngleOffset = rotate.x; vis.pose.extraPitchAngle = vis.pose.pitchAngleOffset = rotate.y; drawmodelparams_t &visModel = *VS_MODEL(&vis); visModel.inter = (mstate.maxTimer > 0 ? mstate.timer / float(mstate.maxTimer) : 0); visModel.mf = modef; visModel.alwaysInterpolate = true; visModel.shineTranslateWithViewerPos = true; resSys().setModelDefFrame(*modef, mstate.frame); vis.light.ambientColor = Vector4f(skyModelDef.get("color"), 1); Rend_DrawModel(vis); } glMatrixMode(GL_MODELVIEW); glPopMatrix(); // We don't want that anything in the world geometry interferes with what was // drawn in the sky. glClear(GL_DEPTH_BUFFER_BIT); } void setupModels(Record const *def) { // Normally the sky sphere is not drawn if models are in use. alwaysDrawSphere = (def && (def->geti("flags") & SIF_DRAW_SPHERE) != 0); // The normal sphere is used if no models will be set up. haveModels = false; de::zap(models); if(!def) return; defn::Sky const skyDef(*def); for(int i = 0; i < skyDef.modelCount(); ++i) { ModelData &mdata = models[i]; Record const &skyModelDef = skyDef.model(i); try { if(ModelDef *modef = &resSys().modelDef(skyModelDef.gets("id"))) { if(modef->subCount()) { mdata.modef = modef; haveModels = true; // There is at least one model here. } } } catch(ResourceSystem::MissingModelDefError const &) {} // Ignore this error. } } void updateHeightIfNeeded() { if(!needHeightUpdate) return; needHeightUpdate = false; float newHeight = (sky? sky->height() : 1); if(!de::fequal(height, newHeight)) { height = newHeight; needBuildHemisphere = true; } } void updateHorizonOffsetIfNeeded() { if(!needHorizonOffsetUpdate) return; needHorizonOffsetUpdate = false; float newHorizonOffset = (sky? sky->horizonOffset() : 0); if(!de::fequal(horizonOffset, newHorizonOffset)) { horizonOffset = newHorizonOffset; needBuildHemisphere = true; } } /// Observes Sky Deletion void skyBeingDeleted(Sky const &) { // Stop observing Sky change notifications. self.configure(); } /// Observes Sky HeightChange void skyHeightChanged(Sky &) { // Defer the update (we may be part way through drawing a frame). needHeightUpdate = true; } /// Observes Sky HeightChange void skyHorizonOffsetChanged(Sky &) { // Defer the update (we may be part way through drawing a frame). needHorizonOffsetUpdate = true; } }; SkyDrawable::SkyDrawable(Sky const *sky) : d(new Instance(this)) { configure(sky); } SkyDrawable &SkyDrawable::configure(Sky const *sky) { if(d->sky) { d->sky->audienceForDeletion() -= d; d->sky->audienceForHeightChange() -= d; d->sky->audienceForHorizonOffsetChange() -= d; } d->sky = sky; d->needHeightUpdate = true; d->needHorizonOffsetUpdate = true; if(d->sky) { d->sky->audienceForHorizonOffsetChange() += d; d->sky->audienceForHeightChange() += d; d->sky->audienceForDeletion() += d; } // Models are set up using the data in the definition (will override the sphere by default). d->setupModels(d->sky? d->sky->def() : 0); return *this; } Sky const *SkyDrawable::sky() const { return d->sky; } void SkyDrawable::cacheAssets() { if(!d->sky) return; for(int i = 0; i < MAX_LAYERS; ++i) { SkyLayer const *layer = d->sky->layer(i); if(Material *mat = layer->material()) { resSys().cache(*mat, layerMaterialSpec(layer->isMasked())); } } if(!d->haveModels) return; for(int i = 0; i < MAX_MODELS; ++i) { // Is this model in use? if(ModelDef *modef = d->models[i].modef) { resSys().cache(modef); } } } ModelDef *SkyDrawable::modelDef(int modelIndex) const { if(modelIndex >= 0 && modelIndex < MAX_MODELS) { return d->models[modelIndex].modef; } return nullptr; } void SkyDrawable::draw(Animator const *animator) const { DENG2_ASSERT(animator); DENG2_ASSERT(&animator->sky() == this && d->sky == animator->sky().sky()); DENG2_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); d->prepare(animator); // Only drawn when at least one layer is active. if(d->firstActiveLayer < 0) return; if(usingFog) glEnable(GL_FOG); d->drawSphere(); d->drawModels(animator); if(usingFog) glDisable(GL_FOG); } MaterialVariantSpec const &SkyDrawable::layerMaterialSpec(bool masked) // static { return resSys().materialSpec(SkySphereContext, TSF_NO_COMPRESSION | (masked? TSF_ZEROMASK : 0), 0, 0, 0, GL_REPEAT, GL_CLAMP_TO_EDGE, 0, -1, -1, false, true, false, false); } static void markSphereForRebuild() { // Defer this task until draw time, when we can be sure we are in the correct thread. hemisphere.needRebuild = true; } void SkyDrawable::consoleRegister() // static { C_VAR_INT2 ("rend-sky-detail", &sphereDetail, 0, 3, 7, markSphereForRebuild); C_VAR_INT2 ("rend-sky-rows", &sphereRows, 0, 1, 8, markSphereForRebuild); C_VAR_FLOAT("rend-sky-distance", &sphereDistance, CVF_NO_MAX, 1, 0); } //--------------------------------------------------------------------------------------- DENG2_PIMPL_NOREF(SkyDrawable::Animator) { SkyDrawable *sky = nullptr; LayerState layers[MAX_LAYERS]; ModelState models[MAX_MODELS]; Instance() { de::zap(layers); de::zap(models); } }; SkyDrawable::Animator::Animator() : d(new Instance) {} SkyDrawable::Animator::Animator(SkyDrawable &sky) : d(new Instance) { d->sky = &sky; } SkyDrawable::Animator::~Animator() {} void SkyDrawable::Animator::setSky(SkyDrawable *sky) { d->sky = sky; // (Re)Initalize animation states. de::zap(d->layers); de::zap(d->models); if(!d->sky) return; defn::Sky const skyDef(*d->sky->sky()->def()); for(int i = 0; i < MAX_MODELS; ++i) { ModelState &mstate = model(i); // Is this model in use? if(ModelDef const *modef = d->sky->modelDef(i)) { Record const &skyModelDef = skyDef.model(i); mstate.frame = modef->subModelDef(0).frame; mstate.maxTimer = int(TICSPERSEC * skyModelDef.getf("frameInterval")); mstate.yaw = skyModelDef.getf("yaw"); } } } SkyDrawable &SkyDrawable::Animator::sky() const { DENG2_ASSERT(d->sky); return *d->sky; } bool SkyDrawable::Animator::hasLayer(int index) const { return (index >= 0 && index < MAX_LAYERS); } SkyDrawable::Animator::LayerState &SkyDrawable::Animator::layer(int index) { if(hasLayer(index)) return d->layers[index]; /// @throw MissingLayerStateError An invalid layer state index was specified. throw MissingLayerStateError("SkyDrawable::Animator::layer", "Invalid layer state index #" + String::number(index) + "."); } SkyDrawable::Animator::LayerState const &SkyDrawable::Animator::layer(int index) const { return const_cast(this)->layer(index); } bool SkyDrawable::Animator::hasModel(int index) const { return (index >= 0 && index < MAX_MODELS); } SkyDrawable::Animator::ModelState &SkyDrawable::Animator::model(int index) { if(hasModel(index)) return d->models[index]; /// @throw MissingModelStateError An invalid model state index was specified. throw MissingModelStateError("SkyDrawable::Animator::model", "Invalid model state index #" + String::number(index) + "."); } SkyDrawable::Animator::ModelState const &SkyDrawable::Animator::model(int index) const { return const_cast(this)->model(index); } void SkyDrawable::Animator::advanceTime(timespan_t /*elapsed*/) { LOG_AS("SkyDrawable::Animator"); if(!d->sky) return; if(!sky().sky()) return; if(clientPaused) return; if(!DD_IsSharpTick()) return; // Animate layers. defn::Sky const skyDef(*sky().sky()->def()); for(int i = 0; i < MAX_LAYERS; ++i) { Record const &skyLayerDef = skyDef.layer(i); LayerState &lstate = layer(i); // Translate the layer origin. lstate.offset += skyLayerDef.getf("offsetSpeed") / TICSPERSEC; } // Animate models. for(int i = 0; i < MAX_MODELS; ++i) { // Is this model in use? ModelDef const *modef = sky().modelDef(i); if(!modef) continue; Record const &skyModelDef = skyDef.model(i); ModelState &mstate = model(i); // Rotate the model. mstate.yaw += skyModelDef.getf("yawSpeed") / TICSPERSEC; // Is it time to advance to the next frame? if(mstate.maxTimer > 0 && ++mstate.timer >= mstate.maxTimer) { mstate.frame++; mstate.timer = 0; // Execute a console command? String const execute = skyModelDef.gets("execute"); if(!execute.isEmpty()) { Con_Execute(CMDS_SCRIPT, execute.toUtf8().constData(), true, false); } } } } doomsday-stable-1.15.7/doomsday/client/src/render/skyfixedge.cpp0000664000175000017500000001713512641367670024214 0ustar jaakkojaakko/** @file skyfixedge.cpp Sky Fix Edge Geometry. * * @authors Copyright © 2011-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "Face" #include "ConvexSubspace" #include "Plane" #include "Sector" #include "SectorCluster" #include "Surface" #include "world/map.h" #include "world/maputil.h" // R_SideBackClosed, remove me #include "world/p_players.h" #include "render/rend_main.h" #include "render/skyfixedge.h" namespace de { static coord_t skyFixFloorZ(Plane const *frontFloor, Plane const *backFloor) { DENG_UNUSED(backFloor); if(devRendSkyMode || P_IsInVoid(viewPlayer)) return frontFloor->heightSmoothed(); return frontFloor->map().skyFixFloor(); } static coord_t skyFixCeilZ(Plane const *frontCeil, Plane const *backCeil) { DENG_UNUSED(backCeil); if(devRendSkyMode || P_IsInVoid(viewPlayer)) return frontCeil->heightSmoothed(); return frontCeil->map().skyFixCeiling(); } DENG2_PIMPL_NOREF(SkyFixEdge::Event) { SkyFixEdge &owner; double distance; Instance(SkyFixEdge &owner, double distance) : owner(owner), distance(distance) {} }; SkyFixEdge::Event::Event(SkyFixEdge &owner, double distance) : WorldEdge::Event(), d(new Instance(owner, distance)) {} bool SkyFixEdge::Event::operator < (Event const &other) const { return d->distance < other.distance(); } double SkyFixEdge::Event::distance() const { return d->distance; } Vector3d SkyFixEdge::Event::origin() const { return d->owner.pOrigin() + d->owner.pDirection() * distance(); } DENG2_PIMPL(SkyFixEdge) { HEdge *hedge; FixType fixType; int edge; Vector3d pOrigin; Vector3d pDirection; coord_t lo, hi; Event bottom; Event top; bool isValid; Vector2f materialOrigin; Instance(Public *i, HEdge &hedge, FixType fixType, int edge, Vector2f const &materialOrigin) : Base(i), hedge(&hedge), fixType(fixType), edge(edge), bottom(*i, 0), top(*i, 1), isValid(false), materialOrigin(materialOrigin) {} /** * Determines whether a sky fix is actually necessary. */ bool wallSectionNeedsSkyFix() const { DENG_ASSERT(hedge->hasFace()); bool const lower = fixType == SkyFixEdge::Lower; // Only edges with line segments need fixes. if(!hedge->hasMapElement()) return false; SectorCluster const *cluster = hedge->face().mapElementAs().clusterPtr(); SectorCluster const *backCluster = hedge->twin().hasFace()? hedge->twin().face().mapElementAs() .clusterPtr() : 0; if(backCluster && &backCluster->sector() == &cluster->sector()) return false; // Select the relative planes for the fix type. int relPlane = lower? Sector::Floor : Sector::Ceiling; Plane const *front = &cluster->visPlane(relPlane); Plane const *back = backCluster? &backCluster->visPlane(relPlane) : 0; if(!front->surface().hasSkyMaskedMaterial()) return false; LineSide const &lineSide = hedge->mapElementAs().lineSide(); bool const hasClosedBack = R_SideBackClosed(lineSide); if(!devRendSkyMode) { if(!P_IsInVoid(viewPlayer) && !(hasClosedBack || !(back && back->surface().hasSkyMaskedMaterial()))) return false; } else { int relSection = lower? LineSide::Bottom : LineSide::Top; if(lineSide.surface(relSection).hasMaterial() || !(hasClosedBack || (back && back->surface().hasSkyMaskedMaterial()))) return false; } // Figure out the relative plane heights. coord_t fz = front->heightSmoothed(); if(relPlane == Sector::Ceiling) fz = -fz; coord_t bz = 0; if(back) { bz = back->heightSmoothed(); if(relPlane == Sector::Ceiling) bz = -bz; } coord_t planeZ = (back && back->surface().hasSkyMaskedMaterial() && fz < bz? bz : fz); coord_t skyZ = lower? skyFixFloorZ(front, back) : -skyFixCeilZ(front, back); return (planeZ > skyZ); } void prepare() { if(!wallSectionNeedsSkyFix()) { isValid = false; return; } SectorCluster const *cluster = hedge->face().mapElementAs().clusterPtr(); SectorCluster const *backCluster = hedge->twin().hasFace()? hedge->twin().face().mapElementAs().clusterPtr() : 0; Plane const *ffloor = &cluster->visFloor(); Plane const *fceil = &cluster->visCeiling(); Plane const *bceil = backCluster? &backCluster->visCeiling() : 0; Plane const *bfloor = backCluster? &backCluster->visFloor() : 0; if(fixType == Upper) { hi = skyFixCeilZ(fceil, bceil); lo = de::max((backCluster && bceil->surface().hasSkyMaskedMaterial())? bceil->heightSmoothed() : fceil->heightSmoothed(), ffloor->heightSmoothed()); } else { hi = de::min((backCluster && bfloor->surface().hasSkyMaskedMaterial())? bfloor->heightSmoothed() : ffloor->heightSmoothed(), fceil->heightSmoothed()); lo = skyFixFloorZ(ffloor, bfloor); } isValid = hi > lo; if(!isValid) return; pOrigin = Vector3d(self.origin(), lo); pDirection = Vector3d(0, 0, hi - lo); } }; SkyFixEdge::SkyFixEdge(HEdge &hedge, FixType fixType, int edge, float materialOffsetS) : WorldEdge((edge? hedge.twin() : hedge).origin()), d(new Instance(this, hedge, fixType, edge, Vector2f(materialOffsetS, 0))) { /// @todo Defer until necessary. d->prepare(); } Vector3d const &SkyFixEdge::pOrigin() const { return d->pOrigin; } Vector3d const &SkyFixEdge::pDirection() const { return d->pDirection; } Vector2f SkyFixEdge::materialOrigin() const { return d->materialOrigin; } bool SkyFixEdge::isValid() const { return d->isValid; } SkyFixEdge::Event const &SkyFixEdge::first() const { return d->bottom; } SkyFixEdge::Event const &SkyFixEdge::last() const { return d->top; } SkyFixEdge::Event const &SkyFixEdge::at(EventIndex index) const { if(index >= 0 && index < 2) { return index == 0? d->bottom : d->top; } /// @throw InvalidIndexError The specified event index is not valid. throw Error("SkyFixEdge::at", QString("Index '%1' does not map to a known event (count: 2)").arg(index)); } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/render/rend_model.cpp0000664000175000017500000010612112641367670024154 0ustar jaakkojaakko/** @file rend_model.cpp 3D Model Rendering. * * @note Light vectors and triangle normals are in an entirely independent, * right-handed coordinate system. * * There is some more confusion with Y and Z axes as the game uses Z as the * vertical axis and the rendering code and model definitions use the Y axis. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "clientapp.h" #include "dd_def.h" #include "dd_main.h" // App_WorldSystem() #include "world/p_players.h" #include "world/clientmobjthinkerdata.h" #include "render/rend_model.h" #include "render/rend_main.h" #include "render/vissprite.h" #include "render/modelrenderer.h" #include "gl/gl_main.h" #include "gl/gl_texmanager.h" #include "MaterialVariantSpec" #include "Texture" #include #include #include #include #include #include #include #include #include using namespace de; #define QATAN2(y,x) qatan2(y,x) #define QASIN(x) asin(x) // @todo Precalculate arcsin. static inline float qatan2(float y, float x) { float ang = BANG2RAD(bamsAtan2(y * 512, x * 512)); if(ang > PI) ang -= 2 * (float) PI; return ang; } enum rendcmd_t { RC_COMMAND_COORDS, RC_OTHER_COORDS, RC_BOTH_COORDS }; byte useModels = true; int modelLight = 4; int frameInter = true; float modelAspectMod = 1 / 1.2f; //.833334f; int mirrorHudModels; int modelShinyMultitex = true; float modelShinyFactor = 1.0f; float modelSpinSpeed = 1; int maxModelDistance = 1500; float rend_model_lod = 256; byte precacheSkins = true; static bool inited; struct array_t { bool enabled; void *data; }; #define MAX_ARRAYS (2 + MAX_TEX_UNITS) static array_t arrays[MAX_ARRAYS]; // The global vertex render buffer. static Vector3f *modelPosCoords; static Vector3f *modelNormCoords; static Vector4ub *modelColorCoords; static Vector2f *modelTexCoords; // Global variables for ease of use. (Egads!) static Vector3f modelCenter; static ModelDetailLevel *activeLod; static uint vertexBufferMax; ///< Maximum number of vertices we'll be required to render per submodel. static uint vertexBufferSize; ///< Current number of vertices supported by the render buffer. #ifdef DENG_DEBUG static bool announcedVertexBufferMaxBreach; ///< @c true if an attempt has been made to expand beyond our capability. #endif static inline RenderSystem &rendSys() { return ClientApp::renderSystem(); } static inline ResourceSystem &resSys() { return ClientApp::resourceSystem(); } /*static void modelAspectModChanged() { /// @todo Reload and resize all models. }*/ void Rend_ModelRegister() { C_VAR_BYTE ("rend-model", &useModels, 0, 0, 1); C_VAR_INT ("rend-model-lights", &modelLight, 0, 0, 10); C_VAR_INT ("rend-model-inter", &frameInter, 0, 0, 1); C_VAR_FLOAT("rend-model-aspect", &modelAspectMod, CVF_NO_MAX | CVF_NO_MIN, 0, 0); C_VAR_INT ("rend-model-distance", &maxModelDistance, CVF_NO_MAX, 0, 0); C_VAR_BYTE ("rend-model-precache", &precacheSkins, 0, 0, 1); C_VAR_FLOAT("rend-model-lod", &rend_model_lod, CVF_NO_MAX, 0, 0); C_VAR_INT ("rend-model-mirror-hud", &mirrorHudModels, 0, 0, 1); C_VAR_FLOAT("rend-model-spin-speed", &modelSpinSpeed, CVF_NO_MAX | CVF_NO_MIN, 0, 0); C_VAR_INT ("rend-model-shiny-multitex", &modelShinyMultitex, 0, 0, 1); C_VAR_FLOAT("rend-model-shiny-strength", &modelShinyFactor, 0, 0, 10); } void Rend_ModelInit() { if(inited) return; // Already been here. modelPosCoords = 0; modelNormCoords = 0; modelColorCoords = 0; modelTexCoords = 0; vertexBufferMax = vertexBufferSize = 0; #ifdef DENG_DEBUG announcedVertexBufferMaxBreach = false; #endif inited = true; } void Rend_ModelShutdown() { if(!inited) return; M_Free(modelPosCoords); modelPosCoords = 0; M_Free(modelNormCoords); modelNormCoords = 0; M_Free(modelColorCoords); modelColorCoords = 0; M_Free(modelTexCoords); modelTexCoords = 0; vertexBufferMax = vertexBufferSize = 0; #ifdef DENG_DEBUG announcedVertexBufferMaxBreach = false; #endif inited = false; } bool Rend_ModelExpandVertexBuffers(uint numVertices) { DENG2_ASSERT(inited); LOG_AS("Rend_ModelExpandVertexBuffers"); if(numVertices <= vertexBufferMax) return true; // Sanity check a sane maximum... if(numVertices >= RENDER_MAX_MODEL_VERTS) { #ifdef DENG_DEBUG if(!announcedVertexBufferMaxBreach) { LOGDEV_GL_WARNING("Attempted to expand to %u vertices (max %u)") << numVertices << RENDER_MAX_MODEL_VERTS; announcedVertexBufferMaxBreach = true; } #endif return false; } // Defer resizing of the render buffer until draw time as it may be repeatedly expanded. vertexBufferMax = numVertices; return true; } /// @return @c true= Vertex buffer is large enough to handle @a numVertices. static bool resizeVertexBuffer(uint numVertices) { // Mark the vertex buffer if a resize is necessary. Rend_ModelExpandVertexBuffers(numVertices); // Do we need to resize the buffers? if(vertexBufferMax != vertexBufferSize) { /// @todo Align access to this memory along a 4-byte boundary? modelPosCoords = (Vector3f *) M_Realloc(modelPosCoords, sizeof(*modelPosCoords) * vertexBufferMax); modelNormCoords = (Vector3f *) M_Realloc(modelNormCoords, sizeof(*modelNormCoords) * vertexBufferMax); modelColorCoords = (Vector4ub *) M_Realloc(modelColorCoords, sizeof(*modelColorCoords) * vertexBufferMax); modelTexCoords = (Vector2f *) M_Realloc(modelTexCoords, sizeof(*modelTexCoords) * vertexBufferMax); vertexBufferSize = vertexBufferMax; } // Is the buffer large enough? return vertexBufferSize >= numVertices; } static void disableArrays(int vertices, int colors, int coords) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); if(vertices) { arrays[AR_VERTEX].enabled = false; } if(colors) { arrays[AR_COLOR].enabled = false; } for(int i = 0; i < numTexUnits; ++i) { if(coords & (1 << i)) { arrays[AR_TEXCOORD0 + i].enabled = false; } } DENG_ASSERT(!Sys_GLCheckError()); } static inline void enableTexUnit(byte id) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glActiveTexture(GL_TEXTURE0 + id); glEnable(GL_TEXTURE_2D); } static inline void disableTexUnit(byte id) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glActiveTexture(GL_TEXTURE0 + id); glDisable(GL_TEXTURE_2D); // Implicit disabling of texcoord array. disableArrays(0, 0, 1 << id); } /** * The first selected unit is active after this call. */ static void selectTexUnits(int count) { for(int i = numTexUnits - 1; i >= count; i--) { disableTexUnit(i); } // Enable the selected units. for(int i = count - 1; i >= 0; i--) { if(i >= numTexUnits) continue; enableTexUnit(i); } } /** * Enable, set and optionally lock all enabled arrays. */ static void configureArrays(void *vertices, void *colors, int numCoords = 0, void **coords = 0) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); if(vertices) { arrays[AR_VERTEX].enabled = true; arrays[AR_VERTEX].data = vertices; } if(colors) { arrays[AR_COLOR].enabled = true; arrays[AR_COLOR].data = colors; } for(int i = 0; i < numCoords && i < MAX_TEX_UNITS; ++i) { if(coords[i]) { arrays[AR_TEXCOORD0 + i].enabled = true; arrays[AR_TEXCOORD0 + i].data = coords[i]; } } DENG_ASSERT(!Sys_GLCheckError()); } static void drawArrayElement(int index) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); for(int i = 0; i < numTexUnits; ++i) { if(!arrays[AR_TEXCOORD0 + i].enabled) continue; Vector2f const &texCoord = ((Vector2f const *)arrays[AR_TEXCOORD0 + i].data)[index]; glMultiTexCoord2f(GL_TEXTURE0 + i, texCoord.x, texCoord.y); } if(arrays[AR_COLOR].enabled) { Vector4ub const &colorCoord = ((Vector4ub const *) arrays[AR_COLOR].data)[index]; glColor4ub(colorCoord.x, colorCoord.y, colorCoord.z, colorCoord.w); } if(arrays[AR_VERTEX].enabled) { Vector3f const &posCoord = ((Vector3f const *) arrays[AR_VERTEX].data)[index]; glVertex3f(posCoord.x, posCoord.y, posCoord.z); } } /** * Return a pointer to the visible model frame. */ static ModelFrame &visibleModelFrame(ModelDef &modef, int subnumber, int mobjId) { if(subnumber >= int(modef.subCount())) { throw Error("Rend_DrawModel.visibleFrame", QString("Model has %1 submodels, but submodel #%2 was requested") .arg(modef.subCount()).arg(subnumber)); } SubmodelDef const &sub = modef.subModelDef(subnumber); int curFrame = sub.frame; if(modef.flags & MFF_IDFRAME) { curFrame += mobjId % sub.frameRange; } return App_ResourceSystem().model(sub.modelId).frame(curFrame); } /** * Render a set of 3D model primitives using the given data. */ static void drawPrimitives(rendcmd_t mode, Model::Primitives const &primitives, Vector3f *posCoords, Vector4ub *colorCoords, Vector2f *texCoords = 0) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // Disable all vertex arrays. disableArrays(true, true, DDMAXINT); // Load the vertex array. void *coords[2]; switch(mode) { case RC_OTHER_COORDS: coords[0] = texCoords; configureArrays(posCoords, colorCoords, 1, coords); break; case RC_BOTH_COORDS: coords[0] = NULL; coords[1] = texCoords; configureArrays(posCoords, colorCoords, 2, coords); break; default: configureArrays(posCoords, colorCoords); break; } foreach(Model::Primitive const &prim, primitives) { // The type of primitive depends on the sign. glBegin(prim.triFan? GL_TRIANGLE_FAN : GL_TRIANGLE_STRIP); foreach(Model::Primitive::Element const &elem, prim.elements) { if(mode != RC_OTHER_COORDS) { glTexCoord2f(elem.texCoord.x, elem.texCoord.y); } drawArrayElement(elem.index); } // The primitive is complete. glEnd(); } } /** * Interpolate linearly between two sets of vertices. */ static void Mod_LerpVertices(float inter, int count, ModelFrame const &from, ModelFrame const &to, Vector3f *posOut, Vector3f *normOut) { DENG2_ASSERT(&from.model == &to.model); // sanity check. DENG2_ASSERT(!activeLod || &activeLod->model == &from.model); // sanity check. DENG2_ASSERT(from.vertices.count() == to.vertices.count()); // sanity check. ModelFrame::VertexBuf::const_iterator startIt = from.vertices.begin(); ModelFrame::VertexBuf::const_iterator endIt = to.vertices.begin(); if(&from == &to || de::fequal(inter, 0)) { for(int i = 0; i < count; ++i, startIt++, posOut++, normOut++) { if(!activeLod || activeLod->hasVertex(i)) { *posOut = startIt->pos; *normOut = startIt->norm; } } } else { for(int i = 0; i < count; ++i, startIt++, endIt++, posOut++, normOut++) { if(!activeLod || activeLod->hasVertex(i)) { *posOut = de::lerp(startIt->pos, endIt->pos, inter); *normOut = de::lerp(startIt->norm, endIt->norm, inter); } } } } static void Mod_MirrorCoords(dint count, Vector3f *coords, dint axis) { DENG2_ASSERT(coords); for(; count-- > 0; coords++) { (*coords)[axis] = -(*coords)[axis]; } } /** * Rotate a VectorLight direction vector from world space to model space. * * @param vlight Light to process. * @param yaw Yaw rotation angle. * @param pitch Pitch rotation angle. * @param invert @c true= flip light normal (for use with inverted models). * * @todo Construct a rotation matrix once and use it for all lights. */ static Vector3f rotateLightVector(VectorLightData const &vlight, dfloat yaw, dfloat pitch, bool invert = false) { dfloat rotated[3]; vlight.direction.decompose(rotated); M_RotateVector(rotated, yaw, pitch); // Quick hack: Flip light normal if model inverted. if(invert) { rotated[0] = -rotated[0]; rotated[1] = -rotated[1]; } return Vector3f(rotated); } /** * Calculate vertex lighting. */ static void Mod_VertexColors(Vector4ub *out, dint count, Vector3f const *normCoords, duint lightListIdx, duint maxLights, Vector4f const &ambient, bool invert, dfloat rotateYaw, dfloat rotatePitch) { Vector4f const saturated(1, 1, 1, 1); for(dint i = 0; i < count; ++i, out++, normCoords++) { if(activeLod && !activeLod->hasVertex(i)) continue; Vector3f const &normal = *normCoords; // Accumulate contributions from all affecting lights. dint numProcessed = 0; Vector3f accum[2]; // Begin with total darkness [color, extra]. rendSys().forAllVectorLights(lightListIdx, [&maxLights, &invert, &rotateYaw , &rotatePitch, &normal , &accum, &numProcessed] (VectorLightData const &vlight) { numProcessed += 1; // We must transform the light vector to model space. Vector3f const lightDirection = rotateLightVector(vlight, rotateYaw, rotatePitch, invert); dfloat strength = lightDirection.dot(normal) + vlight.offset; // Shift a bit towards the light. // Ability to both light and shade. if(strength > 0) strength *= vlight.lightSide; else strength *= vlight.darkSide; accum[vlight.affectedByAmbient? 0 : 1] += vlight.color * de::clamp(-1.f, strength, 1.f); // Time to stop? return (maxLights && numProcessed == maxLights); }); // Check for ambient and convert to ubyte. Vector4f color(accum[0].max(ambient) + accum[1], ambient[3]); *out = (color.min(saturated) * 255).toVector4ub(); } } /** * Set all the colors in the array to bright white. */ static void Mod_FullBrightVertexColors(dint count, Vector4ub *colorCoords, dfloat alpha) { DENG2_ASSERT(colorCoords); for(; count-- > 0; colorCoords++) { *colorCoords = Vector4ub(255, 255, 255, 255 * alpha); } } /** * Set all the colors into the array to the same values. */ static void Mod_FixedVertexColors(dint count, Vector4ub *colorCoords, Vector4ub const &color) { DENG2_ASSERT(colorCoords); for(; count-- > 0; colorCoords++) { *colorCoords = color; } } /** * Calculate cylindrically mapped, shiny texture coordinates. */ static void Mod_ShinyCoords(Vector2f *out, int count, Vector3f const *normCoords, float normYaw, float normPitch, float shinyAng, float shinyPnt, float reactSpeed) { for(int i = 0; i < count; ++i, out++, normCoords++) { if(activeLod && !activeLod->hasVertex(i)) continue; float rotatedNormal[3] = { normCoords->x, normCoords->y, normCoords->z }; // Rotate the normal vector so that it approximates the // model's orientation compared to the viewer. M_RotateVector(rotatedNormal, (shinyPnt + normYaw) * 360 * reactSpeed, (shinyAng + normPitch - .5f) * 180 * reactSpeed); *out = Vector2f(rotatedNormal[0] + 1, rotatedNormal[2]); } } static int chooseSelSkin(ModelDef &mf, int submodel, int selector) { if(mf.def.hasSub(submodel)) { Record &subDef = mf.def.sub(submodel); int i = (selector >> DDMOBJ_SELECTOR_SHIFT) & subDef.geti("selSkinMask"); int c = subDef.geti("selSkinShift"); if(c > 0) i >>= c; else i <<= -c; if(i > 7) i = 7; // Maximum number of skins for selskin. if(i < 0) i = 0; // Improbable (impossible?), but doesn't hurt. return subDef.geta("selSkins")[i].asInt(); } return 0; } static int chooseSkin(ModelDef &mf, int submodel, int id, int selector, int tmap) { if(submodel >= int(mf.subCount())) { return 0; } SubmodelDef &smf = mf.subModelDef(submodel); Model &mdl = App_ResourceSystem().model(smf.modelId); int skin = smf.skin; // Selskin overrides the skin range. if(smf.testFlag(MFF_SELSKIN)) { skin = chooseSelSkin(mf, submodel, selector); } // Is there a skin range for this frame? // (During model setup skintics and skinrange are set to >0.) if(smf.skinRange > 1) { // What rule to use for determining the skin? int offset; if(smf.testFlag(MFF_IDSKIN)) { offset = id; } else { offset = SECONDS_TO_TICKS(App_WorldSystem().time()) / mf.skinTics; } skin += offset % smf.skinRange; } // Need translation? if(smf.testFlag(MFF_SKINTRANS)) { skin = tmap; } if(skin < 0 || skin >= mdl.skinCount()) { skin = 0; } return skin; } static inline MaterialVariantSpec const &modelSkinMaterialSpec() { return resSys().materialSpec(ModelSkinContext, 0, 0, 0, 0, GL_REPEAT, GL_REPEAT, 1, -2, -1, true, true, false, false); } static void drawSubmodel(uint number, vissprite_t const &spr) { drawmodelparams_t const &parm = *VS_MODEL(&spr); int const zSign = (spr.pose.mirrored? -1 : 1); ModelDef *mf = parm.mf, *mfNext = parm.nextMF; SubmodelDef const &smf = mf->subModelDef(number); Model &mdl = App_ResourceSystem().model(smf.modelId); // Do not bother with infinitely small models... if(mf->scale == Vector3f(0, 0, 0)) return; float alpha = spr.light.ambientColor[CA]; // Is the submodel-defined alpha multiplier in effect? // With df_brightshadow2, the alpha multiplier will be applied anyway. if(smf.testFlag(MFF_BRIGHTSHADOW2) || !(parm.flags & (DDMF_BRIGHTSHADOW|DDMF_SHADOW|DDMF_ALTSHADOW))) { alpha *= smf.alpha / 255.f; } // Would this be visible? if(alpha <= 0) return; blendmode_t blending = smf.blendMode; // Is the submodel-defined blend mode in effect? if(parm.flags & DDMF_BRIGHTSHADOW) { blending = BM_ADD; } int useSkin = chooseSkin(*mf, number, parm.id, parm.selector, parm.tmap); // Scale interpos. Intermark becomes zero and endmark becomes one. // (Full sub-interpolation!) But only do it for the standard // interrange. If a custom one is defined, don't touch interpos. float endPos = 0; float inter = parm.inter; if((mf->interRange[0] == 0 && mf->interRange[1] == 1) || smf.testFlag(MFF_WORLD_TIME_ANIM)) { endPos = (mf->interNext ? mf->interNext->interMark : 1); inter = (parm.inter - mf->interMark) / (endPos - mf->interMark); } ModelFrame *frame = &visibleModelFrame(*mf, number, parm.id); ModelFrame *nextFrame = 0; // Do we have a sky/particle model here? if(parm.alwaysInterpolate) { // Always interpolate, if there's animation. // Used with sky and particle models. nextFrame = &mdl.frame((smf.frame + 1) % mdl.frameCount()); mfNext = mf; } else { // Check for possible interpolation. if(frameInter && mfNext && !smf.testFlag(MFF_DONT_INTERPOLATE)) { if(mfNext->hasSub(number) && mfNext->subModelId(number) == smf.modelId) { nextFrame = &visibleModelFrame(*mfNext, number, parm.id); } } } // Clamp interpolation. inter = de::clamp(0.f, inter, 1.f); if(!nextFrame) { // If not interpolating, use the same frame as interpolation target. // The lerp routines will recognize this special case. nextFrame = frame; mfNext = mf; } // Determine the total number of vertices we have. int numVerts = mdl.vertexCount(); // Ensure our vertex render buffers can accommodate this. if(!resizeVertexBuffer(numVerts)) { // No can do, we aint got the power! return; } // Setup transformation. glMatrixMode(GL_MODELVIEW); glPushMatrix(); // Model space => World space glTranslatef(spr.pose.origin[VX] + spr.pose.srvo[VX] + de::lerp(mf->offset.x, mfNext->offset.x, inter), spr.pose.origin[VZ] + spr.pose.srvo[VZ] + de::lerp(mf->offset.y, mfNext->offset.y, inter), spr.pose.origin[VY] + spr.pose.srvo[VY] + zSign * de::lerp(mf->offset.z, mfNext->offset.z, inter)); if(spr.pose.extraYawAngle || spr.pose.extraPitchAngle) { // Sky models have an extra rotation. glScalef(1, 200 / 240.0f, 1); glRotatef(spr.pose.extraYawAngle, 1, 0, 0); glRotatef(spr.pose.extraPitchAngle, 0, 0, 1); glScalef(1, 240 / 200.0f, 1); } // Model rotation. glRotatef(spr.pose.viewAligned? spr.pose.yawAngleOffset : spr.pose.yaw, 0, 1, 0); glRotatef(spr.pose.viewAligned? spr.pose.pitchAngleOffset : spr.pose.pitch, 0, 0, 1); // Scaling and model space offset. glScalef(de::lerp(mf->scale.x, mfNext->scale.x, inter), de::lerp(mf->scale.y, mfNext->scale.y, inter), de::lerp(mf->scale.z, mfNext->scale.z, inter)); if(spr.pose.extraScale) { // Particle models have an extra scale. glScalef(spr.pose.extraScale, spr.pose.extraScale, spr.pose.extraScale); } glTranslatef(smf.offset.x, smf.offset.y, smf.offset.z); // Determine the suitable LOD. if(mdl.lodCount() > 1 && rend_model_lod != 0) { float lodFactor = rend_model_lod * DENG_GAMEVIEW_WIDTH / 640.0f / (Rend_FieldOfView() / 90.0f); if(!de::fequal(lodFactor, 0)) { lodFactor = 1 / lodFactor; } // Determine the LOD we will be using. activeLod = &mdl.lod(de::clamp(0, lodFactor * spr.pose.distance, mdl.lodCount() - 1)); } else { activeLod = 0; } // Interpolate vertices and normals. Mod_LerpVertices(inter, numVerts, *frame, *nextFrame, modelPosCoords, modelNormCoords); if(zSign < 0) { Mod_MirrorCoords(numVerts, modelPosCoords, 2); Mod_MirrorCoords(numVerts, modelNormCoords, 1); } // Coordinates to the center of the model (game coords). modelCenter = Vector3f(spr.pose.origin[VX], spr.pose.origin[VY], spr.pose.midZ()) + Vector3d(spr.pose.srvo) + Vector3f(mf->offset.x, mf->offset.z, mf->offset.y); // Calculate lighting. Vector4f ambient; if(smf.testFlag(MFF_FULLBRIGHT) && !smf.testFlag(MFF_DIM)) { // Submodel-specific lighting override. ambient = Vector4f(1, 1, 1, 1); Mod_FullBrightVertexColors(numVerts, modelColorCoords, alpha); } else if(!spr.light.vLightListIdx) { // Lit uniformly. ambient = Vector4f(spr.light.ambientColor, alpha); Mod_FixedVertexColors(numVerts, modelColorCoords, (ambient * 255).toVector4ub()); } else { // Lit normally. ambient = Vector4f(spr.light.ambientColor, alpha); Mod_VertexColors(modelColorCoords, numVerts, modelNormCoords, spr.light.vLightListIdx, modelLight + 1, ambient, (mf->scale[VY] < 0), -spr.pose.yaw, -spr.pose.pitch); } TextureVariant *shinyTexture = 0; float shininess = 0; if(mf->def.hasSub(number)) { shininess = float(de::clamp(0.0, mf->def.sub(number).getd("shiny") * modelShinyFactor, 1.0)); // Ensure we've prepared the shiny skin. if(shininess > 0) { if(Texture *tex = mf->subModelDef(number).shinySkin) { shinyTexture = tex->prepareVariant(Rend_ModelShinyTextureSpec()); } else { shininess = 0; } } } Vector4f color; if(shininess > 0) { // Calculate shiny coordinates. Vector3f shinyColor = mf->def.sub(number).get("shinyColor"); // With psprites, add the view angle/pitch. float offset = parm.shineYawOffset; // Calculate normalized (0,1) model yaw and pitch. float normYaw = M_CycleIntoRange(((spr.pose.viewAligned? spr.pose.yawAngleOffset : spr.pose.yaw) + offset) / 360, 1); offset = parm.shinePitchOffset; float normPitch = M_CycleIntoRange(((spr.pose.viewAligned? spr.pose.pitchAngleOffset : spr.pose.pitch) + offset) / 360, 1); float shinyAng = 0; float shinyPnt = 0; if(parm.shinepspriteCoordSpace) { // This is a hack to accommodate the psprite coordinate space. shinyPnt = 0.5; } else { Vector3f delta = modelCenter; if(!parm.shineTranslateWithViewerPos) { delta -= Rend_EyeOrigin().xzy(); } shinyAng = QATAN2(delta.z, M_ApproxDistancef(delta.x, delta.y)) / PI + 0.5f; // shinyAng is [0,1] shinyPnt = QATAN2(delta.y, delta.x) / (2 * PI); } Mod_ShinyCoords(modelTexCoords, numVerts, modelNormCoords, normYaw, normPitch, shinyAng, shinyPnt, mf->def.sub(number).getf("shinyReact")); // Shiny color. if(smf.testFlag(MFF_SHINY_LIT)) { color = Vector4f(ambient * shinyColor, shininess); } else { color = Vector4f(shinyColor, shininess); } } TextureVariant *skinTexture = 0; if(renderTextures == 2) { // For lighting debug, render all surfaces using the gray texture. MaterialAnimator &matAnimator = resSys().material(de::Uri("System", Path("gray"))) .getAnimator(modelSkinMaterialSpec()); // Ensure we've up to date info about the material. matAnimator.prepare(); skinTexture = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; } else { skinTexture = 0; if(Texture *tex = mdl.skin(useSkin).texture) { skinTexture = tex->prepareVariant(Rend_ModelDiffuseTextureSpec(mdl.flags().testFlag(Model::NoTextureCompression))); } } // If we mirror the model, triangles have a different orientation. if(zSign < 0) { glFrontFace(GL_CCW); } // Twosided models won't use backface culling. if(smf.testFlag(MFF_TWO_SIDED)) { glDisable(GL_CULL_FACE); } glEnable(GL_TEXTURE_2D); Model::Primitives const &primitives = activeLod? activeLod->primitives : mdl.primitives(); // Render using multiple passes? if(!modelShinyMultitex || shininess <= 0 || alpha < 1 || blending != BM_NORMAL || !smf.testFlag(MFF_SHINY_SPECULAR) || numTexUnits < 2 || !envModAdd) { // The first pass can be skipped if it won't be visible. if(shininess < 1 || smf.testFlag(MFF_SHINY_SPECULAR)) { selectTexUnits(1); GL_BlendMode(blending); GL_BindTexture(renderTextures? skinTexture : 0); drawPrimitives(RC_COMMAND_COORDS, primitives, modelPosCoords, modelColorCoords); } if(shininess > 0) { glDepthFunc(GL_LEQUAL); // Set blending mode, two choices: reflected and specular. if(smf.testFlag(MFF_SHINY_SPECULAR)) GL_BlendMode(BM_ADD); else GL_BlendMode(BM_NORMAL); // Shiny color. Mod_FixedVertexColors(numVerts, modelColorCoords, (color * 255).toVector4ub()); if(numTexUnits > 1 && modelShinyMultitex) { // We'll use multitexturing to clear out empty spots in // the primary texture. selectTexUnits(2); GL_ModulateTexture(11); glActiveTexture(GL_TEXTURE1); GL_BindTexture(renderTextures? shinyTexture : 0); glActiveTexture(GL_TEXTURE0); GL_BindTexture(renderTextures? skinTexture : 0); drawPrimitives(RC_BOTH_COORDS, primitives, modelPosCoords, modelColorCoords, modelTexCoords); selectTexUnits(1); GL_ModulateTexture(1); } else { // Empty spots will get shine, too. selectTexUnits(1); GL_BindTexture(renderTextures? shinyTexture : 0); drawPrimitives(RC_OTHER_COORDS, primitives, modelPosCoords, modelColorCoords, modelTexCoords); } } } else { // A special case: specular shininess on an opaque object. // Multitextured shininess with the normal blending. GL_BlendMode(blending); selectTexUnits(2); // Tex1*Color + Tex2RGB*ConstRGB GL_ModulateTexture(10); glActiveTexture(GL_TEXTURE1); GL_BindTexture(renderTextures? shinyTexture : 0); // Multiply by shininess. float colorv1[] = { color.x * color.w, color.y * color.w, color.z * color.w, color.w }; glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, colorv1); glActiveTexture(GL_TEXTURE0); GL_BindTexture(renderTextures? skinTexture : 0); drawPrimitives(RC_BOTH_COORDS, primitives, modelPosCoords, modelColorCoords, modelTexCoords); selectTexUnits(1); GL_ModulateTexture(1); } // We're done! glDisable(GL_TEXTURE_2D); glMatrixMode(GL_MODELVIEW); glPopMatrix(); // Normally culling is always enabled. if(smf.testFlag(MFF_TWO_SIDED)) { glEnable(GL_CULL_FACE); } if(zSign < 0) { glFrontFace(GL_CW); } glDepthFunc(GL_LESS); GL_BlendMode(BM_NORMAL); } void Rend_DrawModel(vissprite_t const &spr) { drawmodelparams_t const &parm = *VS_MODEL(&spr); DENG2_ASSERT(inited); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); if(!parm.mf) return; // Render all the submodels of this model. for(uint i = 0; i < parm.mf->subCount(); ++i) { if(parm.mf->subModelId(i)) { bool disableZ = (parm.mf->flags & MFF_DISABLE_Z_WRITE || parm.mf->testSubFlag(i, MFF_DISABLE_Z_WRITE)); if(disableZ) { glDepthMask(GL_FALSE); } drawSubmodel(i, spr); if(disableZ) { glDepthMask(GL_TRUE); } } } if(devMobjVLights && spr.light.vLightListIdx) { // Draw the vlight vectors, for debug. glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glTranslatef(spr.pose.origin[0], spr.pose.origin[2], spr.pose.origin[1]); coord_t const distFromViewer = de::abs(spr.pose.distance); rendSys().forAllVectorLights(spr.light.vLightListIdx, [&distFromViewer] (VectorLightData const &vlight) { if(distFromViewer < 1600 - 8) { Rend_DrawVectorLight(vlight, 1 - distFromViewer / 1600); } return LoopContinue; }); glMatrixMode(GL_MODELVIEW); glPopMatrix(); glEnable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); } } TextureVariantSpec const &Rend_ModelDiffuseTextureSpec(bool noCompression) { return resSys().textureSpec(TC_MODELSKIN_DIFFUSE, (noCompression? TSF_NO_COMPRESSION : 0), 0, 0, 0, GL_REPEAT, GL_REPEAT, 1, -2, -1, true, true, false, false); } TextureVariantSpec const &Rend_ModelShinyTextureSpec() { return resSys().textureSpec(TC_MODELSKIN_REFLECTION, TSF_NO_COMPRESSION, 0, 0, 0, GL_REPEAT, GL_REPEAT, 1, -2, -1, false, false, false, false); } //--------------------------------------------------------------------------------------- // Work in progress: // Here is the contact point between the old renderer and the new GL2 model renderer. // In the future, vissprites should form a class hierarchy, and the entire drawing // operation should be encapsulated within. This will allow drawing a model (or a // sprite, etc.) by creating a VisSprite instance and telling it to draw itself. void Rend_DrawModel2(vissprite_t const &spr) { ModelRenderer &rend = ClientApp::renderSystem().modelRenderer(); drawmodel2params_t const &p = spr.data.model2; Matrix4f viewMat = Viewer_Matrix() * Matrix4f::translate((spr.pose.origin + spr.pose.srvo).xzy()); Matrix4f localMat = Matrix4f::rotate(spr.pose.viewAligned? spr.pose.yawAngleOffset : spr.pose.yaw, Vector3f(0, 1, 0) /* vertical axis for yaw */); if(p.object) { localMat = localMat * THINKER_DATA(p.object->thinker, ClientMobjThinkerData) .modelTransformation(); } // Set up a suitable matrix for the pose. rend.setTransformation(Rend_EyeOrigin() - spr.pose.mid().xzy(), localMat, viewMat); // Ambient color and lighting vectors. rend.setAmbientLight(spr.light.ambientColor * .8f); rend.clearLights(); rendSys().forAllVectorLights(spr.light.vLightListIdx, [&rend] (VectorLightData const &vlight) { // Use this when drawing the model. rend.addLight(vlight.direction.xzy(), vlight.color); return LoopContinue; }); // Draw the model using the current animation state. p.model->draw(p.animator); } doomsday-stable-1.15.7/doomsday/client/src/render/walledge.cpp0000664000175000017500000005710012641367670023632 0ustar jaakkojaakko/** @file walledge.cpp Wall Edge Geometry. * * @authors Copyright © 2011-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "render/walledge.h" #include "BspLeaf" #include "ConvexSubspace" #include "Sector" #include "Surface" #include "Face" #include "world/lineowner.h" #include "world/p_players.h" #include "world/maputil.h" #include "SectorCluster" #include "render/rend_main.h" // devRendSkyMode, remove me #include using namespace de; /** * Determines whether normal smoothing should be performed for the given pair of * map surfaces (which are assumed to share an edge). * * Yes if the angle between the two surfaces is less than 45 degrees. * @todo Should be user customizable with a Material property. -ds * * @param sufA The "left" map surface which shares an edge with @a sufB. * @param sufB The "right" map surface which shares an edge with @a sufA. * @param angleDiff Angle difference (i.e., normal delta) between the two surfaces. */ static bool shouldSmoothNormals(Surface &sufA, Surface &sufB, binangle_t angleDiff) { DENG2_UNUSED2(sufA, sufB); return INRANGE_OF(angleDiff, BANG_180, BANG_45); } DENG2_PIMPL_NOREF(WallEdge::Event) { /// Wall edge instance which owns "this" event. WallEdge &owner; Instance(WallEdge &owner) : owner(owner) {} }; WallEdge::Event::Event(WallEdge &owner, double distance) : WorldEdge::Event(), IHPlane::IIntercept(distance), d(new Instance(owner)) {} bool WallEdge::Event::operator < (Event const &other) const { return distance() < other.distance(); } double WallEdge::Event::distance() const { return IHPlane::IIntercept::distance(); } Vector3d WallEdge::Event::origin() const { return d->owner.pOrigin() + d->owner.pDirection() * distance(); } static bool eventSorter(WorldEdge::Event *a, WorldEdge::Event *b) { return *a < *b; } static inline coord_t lineSideOffset(LineSideSegment &seg, int edge) { return seg.lineSideOffset() + (edge? seg.length() : 0); } DENG2_PIMPL(WallEdge), public IHPlane { WallSpec spec; int edge; HEdge *wallHEdge; /// The half-plane which partitions the surface coordinate space. Partition hplane; Vector3d pOrigin; Vector3d pDirection; coord_t lo, hi; /// Events for the special termination points are allocated with "this". Event bottom; Event top; /// All events along the partition line. Events *events; bool needSortEvents; Vector2f materialOrigin; Vector3f normal; bool needUpdateNormal; Instance(Public *i, WallSpec const &spec, HEdge &hedge, int edge) : Base(i), spec(spec), edge(edge), wallHEdge(&hedge), lo(0), hi(0), bottom(*i, 0), top(*i, 1), events(0), needSortEvents(false), needUpdateNormal(true) { // Determine the map space Z coordinates of the wall section. LineSideSegment &seg = lineSideSegment(); Line const &line = seg.line(); bool const unpegBottom = (line.flags() & DDLF_DONTPEGBOTTOM) != 0; bool const unpegTop = (line.flags() & DDLF_DONTPEGTOP) != 0; SectorCluster const *cluster = (line.definesPolyobj()? &line.polyobj().bspLeaf().subspace() : &wallHEdge->face().mapElementAs())->clusterPtr(); if(seg.lineSide().considerOneSided() || // Mapping errors may result in a line segment missing a back face. (!line.definesPolyobj() && !wallHEdge->twin().hasFace())) { if(spec.section == LineSide::Middle) { lo = cluster->visFloor().heightSmoothed(); hi = cluster->visCeiling().heightSmoothed(); } else { lo = hi = cluster->visFloor().heightSmoothed(); } materialOrigin = seg.lineSide().middle().materialOriginSmoothed(); if(unpegBottom) { materialOrigin.y -= hi - lo; } } else { // Two sided. SectorCluster const *backCluster = line.definesPolyobj()? cluster : wallHEdge->twin().face().mapElementAs().clusterPtr(); Plane const *ffloor = &cluster->visFloor(); Plane const *fceil = &cluster->visCeiling(); Plane const *bfloor = &backCluster->visFloor(); Plane const *bceil = &backCluster->visCeiling(); switch(spec.section) { case LineSide::Top: // Self-referencing lines only ever get a middle. if(!line.isSelfReferencing()) { // Can't go over front ceiling (would induce geometry flaws). if(bceil->heightSmoothed() < ffloor->heightSmoothed()) lo = ffloor->heightSmoothed(); else lo = bceil->heightSmoothed(); hi = fceil->heightSmoothed(); if(spec.flags.testFlag(WallSpec::SkyClip) && fceil->surface().hasSkyMaskedMaterial() && bceil->surface().hasSkyMaskedMaterial()) { hi = lo; } materialOrigin = seg.lineSide().middle().materialOriginSmoothed(); if(!unpegTop) { // Align with normal middle texture. materialOrigin.y -= fceil->heightSmoothed() - bceil->heightSmoothed(); } } break; case LineSide::Bottom: // Self-referencing lines only ever get a middle. if(!line.isSelfReferencing()) { bool const raiseToBackFloor = (fceil->surface().hasSkyMaskedMaterial() && bceil->surface().hasSkyMaskedMaterial() && fceil->heightSmoothed() < bceil->heightSmoothed() && bfloor->heightSmoothed() > fceil->heightSmoothed()); coord_t t = bfloor->heightSmoothed(); lo = ffloor->heightSmoothed(); // Can't go over the back ceiling, would induce polygon flaws. if(bfloor->heightSmoothed() > bceil->heightSmoothed()) t = bceil->heightSmoothed(); // Can't go over front ceiling, would induce polygon flaws. // In the special case of a sky masked upper we must extend the bottom // section up to the height of the back floor. if(t > fceil->heightSmoothed() && !raiseToBackFloor) t = fceil->heightSmoothed(); hi = t; if(spec.flags.testFlag(WallSpec::SkyClip) && ffloor->surface().hasSkyMaskedMaterial() && bfloor->surface().hasSkyMaskedMaterial()) { lo = hi; } materialOrigin = seg.lineSide().bottom().materialOriginSmoothed(); if(bfloor->heightSmoothed() > fceil->heightSmoothed()) { materialOrigin.y -= (raiseToBackFloor? t : fceil->heightSmoothed()) - bfloor->heightSmoothed(); } if(unpegBottom) { // Align with normal middle texture. materialOrigin.y += (raiseToBackFloor? t : fceil->heightSmoothed()) - bfloor->heightSmoothed(); } } break; case LineSide::Middle: { LineSide const &lineSide = seg.lineSide(); Surface const &middle = lineSide.middle(); if(!line.isSelfReferencing() && ffloor == &cluster->sector().floor()) { lo = de::max(bfloor->heightSmoothed(), ffloor->heightSmoothed()); } else { // Use the unmapped heights for positioning purposes. lo = lineSide.sector().floor().heightSmoothed(); } if(!line.isSelfReferencing() && fceil == &cluster->sector().ceiling()) { hi = de::min(bceil->heightSmoothed(), fceil->heightSmoothed()); } else { // Use the unmapped heights for positioning purposes. hi = lineSide.back().sector().ceiling().heightSmoothed(); } materialOrigin = Vector2f(middle.materialOriginSmoothed().x, 0); // Perform clipping. if(middle.hasMaterial() && !seg.lineSide().isFlagged(SDF_MIDDLE_STRETCH)) { coord_t openBottom, openTop; if(!line.isSelfReferencing()) { openBottom = lo; openTop = hi; } else { openBottom = ffloor->heightSmoothed(); openTop = fceil->heightSmoothed(); } if(openTop > openBottom) { if(unpegBottom) { lo += middle.materialOriginSmoothed().y; hi = lo + middle.material().height(); } else { hi += middle.materialOriginSmoothed().y; lo = hi - middle.material().height(); } if(hi > openTop) { materialOrigin.y = hi - openTop; } // Clip it? bool const clipBottom = !(!(devRendSkyMode || P_IsInVoid(viewPlayer)) && ffloor->surface().hasSkyMaskedMaterial() && bfloor->surface().hasSkyMaskedMaterial()); bool const clipTop = !(!(devRendSkyMode || P_IsInVoid(viewPlayer)) && fceil->surface().hasSkyMaskedMaterial() && bceil->surface().hasSkyMaskedMaterial()); if(clipTop || clipBottom) { if(clipBottom && lo < openBottom) lo = openBottom; if(clipTop && hi > openTop) hi = openTop; } if(!clipTop) { materialOrigin.y = 0; } } } break; } } } materialOrigin += Vector2f(lineSideOffset(seg, edge), 0); pOrigin = Vector3d(self.origin(), lo); pDirection = Vector3d(0, 0, hi - lo); } ~Instance() { clearIntercepts(); } inline LineSideSegment &lineSideSegment() { return wallHEdge->mapElementAs(); } void verifyValid() const { if(!self.isValid()) { /// @throw InvalidError Invalid range geometry was specified. throw InvalidError("WallEdge::verifyValid", "Range geometry is not valid (top < bottom)"); } } EventIndex toEventIndex(double distance) { DENG_ASSERT(events != 0); for(EventIndex i = 0; i < events->count(); ++i) { Event *icpt = (*events)[i]; if(de::fequal(icpt->distance(), distance)) return i; } return InvalidIndex; } inline bool haveEvent(double distance) { return toEventIndex(distance) != InvalidIndex; } Event &createEvent(double distance) { return *intercept(distance); } // Implements IHPlane void configure(Partition const &newPartition) { hplane = newPartition; } // Implements IHPlane Partition const &partition() const { return hplane; } // Implements IHPlane Event *intercept(double distance) { DENG_ASSERT(events != 0); Event *newEvent = new Event(self, distance); events->append(newEvent); // We'll need to resort the events. needSortEvents = true; return newEvent; } // Implements IHPlane void sortAndMergeIntercepts() { DENG_ASSERT(events != 0); // Any work to do? if(!needSortEvents) return; qSort(events->begin(), events->end(), eventSorter); needSortEvents = false; } // Implements IHPlane void clearIntercepts() { if(events) { while(!events->isEmpty()) { Event *event = events->takeLast(); if(!(event == &bottom || event == &top)) delete event; } delete events; events = 0; } // An empty event list is logically sorted. needSortEvents = false; } // Implements IHPlane Event const &at(EventIndex index) const { DENG_ASSERT(events != 0); if(index >= 0 && index < interceptCount()) { return *(*events)[index]; } /// @throw UnknownInterceptError The specified intercept index is not valid. throw UnknownInterceptError("WallEdge::at", QString("Index '%1' does not map to a known intercept (count: %2)") .arg(index).arg(interceptCount())); } // Implements IHPlane int interceptCount() const { DENG_ASSERT(events != 0); return events->count(); } #ifdef DENG_DEBUG void printIntercepts() const { DENG_ASSERT(events != 0); EventIndex index = 0; foreach(Event const *icpt, *events) { LOGDEV_MAP_MSG(" %u: >%1.2f ") << (index++) << icpt->distance(); } } #endif /** * Ensure all intercepts do not exceed the specified closed range. */ void assertInterceptsInRange(double low, double hi) const { #ifdef DENG_DEBUG DENG_ASSERT(events != 0); foreach(Event const *icpt, *events) { DENG2_ASSERT(icpt->distance() >= low && icpt->distance() <= hi); } #else DENG2_UNUSED2(low, hi); #endif } inline double distanceTo(coord_t worldHeight) const { return (worldHeight - lo) / (hi - lo); } void addNeighborIntercepts(coord_t bottom, coord_t top) { ClockDirection const direction = edge? Clockwise : Anticlockwise; HEdge const *hedge = wallHEdge; while((hedge = &SectorClusterCirculator::findBackNeighbor(*hedge, direction)) != wallHEdge) { // Stop if there is no back subspace. ConvexSubspace const *backSubspace = hedge->hasFace()? &hedge->face().mapElementAs() : 0; if(!backSubspace) break; SectorCluster const &cluster = backSubspace->cluster(); if(cluster.hasWorldVolume()) { for(int i = 0; i < cluster.visPlaneCount(); ++i) { Plane const &plane = cluster.visPlane(i); if(plane.heightSmoothed() > bottom && plane.heightSmoothed() < top) { ddouble distance = distanceTo(plane.heightSmoothed()); if(!haveEvent(distance)) { createEvent(distance); // Have we reached the div limit? if(interceptCount() == WALLEDGE_MAX_INTERCEPTS) return; } } // Clip a range bound to this height? if(plane.isSectorFloor() && plane.heightSmoothed() > bottom) bottom = plane.heightSmoothed(); else if(plane.isSectorCeiling() && plane.heightSmoothed() < top) top = plane.heightSmoothed(); // All clipped away? if(bottom >= top) return; } } else { /* * A neighbor with zero volume is a special case -- the potential * division is at the height of the back ceiling. This is because * elsewhere we automatically fix the case of a floor above a * ceiling by lowering the floor. */ coord_t z = cluster.visCeiling().heightSmoothed(); if(z > bottom && z < top) { ddouble distance = distanceTo(z); if(!haveEvent(distance)) { createEvent(distance); // All clipped away. return; } } } } } /** * Determines whether the wall edge should be intercepted with neighboring * planes from other sector clusters. */ bool shouldInterceptNeighbors() { if(spec.flags & WallSpec::NoEdgeDivisions) return false; if(de::fequal(hi, lo)) return false; // Cluster-internal edges won't be intercepted. This is because such an // edge only ever produces middle wall sections, which, do not support // divisions in any case (they become vissprites). if(SectorCluster::isInternalEdge(wallHEdge)) return false; return true; } void prepareEvents() { DENG_ASSERT(self.isValid()); clearIntercepts(); events = new Events; #ifdef DENG2_QT_4_7_OR_NEWER events->reserve(2 + 2); #endif // The first event is the bottom termination event. events->append(&bottom); // The last event is the top termination event. events->append(&top); // Add intecepts for neighbor planes? if(shouldInterceptNeighbors()) { configure(Partition(Vector2d(0, hi - lo))); // Add intercepts (the "divisions") in ascending distance order. addNeighborIntercepts(lo, hi); // Sorting may be required. This shouldn't take too long... // There seldom are more than two or three intercepts. sortAndMergeIntercepts(); } // Sanity check. assertInterceptsInRange(0, 1); } /** * Find the neighbor surface for the edge which we will use to calculate the * "blend" properties (e.g., smoothed edge normal). * * @todo: Use the half-edge rings instead of LineOwners. */ Surface *findBlendNeighbor(binangle_t &diff) { diff = 0; // Are we not blending? if(spec.flags.testFlag(WallSpec::NoEdgeNormalSmoothing)) return 0; LineSide const &lineSide = lineSideSegment().lineSide(); // Polyobj lines have no owner rings. if(lineSide.line().definesPolyobj()) return 0; LineOwner const *farVertOwner = lineSide.line().vertexOwner(lineSide.sideId() ^ edge); Line *neighbor; if(R_SideBackClosed(lineSide)) { neighbor = R_FindSolidLineNeighbor(lineSide.sectorPtr(), &lineSide.line(), farVertOwner, edge, &diff); } else { neighbor = R_FindLineNeighbor(lineSide.sectorPtr(), &lineSide.line(), farVertOwner, edge, &diff); } // No suitable line neighbor? if(!neighbor) return 0; // Choose the correct side of the neighbor (determined by which vertex is shared). LineSide *otherSide; if(&neighbor->vertex(edge ^ 1) == &lineSide.vertex(edge)) otherSide = &neighbor->front(); else otherSide = &neighbor->back(); // We can only blend if the neighbor has a surface. if(!otherSide->hasSections()) return 0; /// @todo Do not assume the neighbor is the middle section of @var otherSide. return &otherSide->middle(); } /** * Determine the (possibly smoothed) edge normal. * @todo Cache the smoothed normal value somewhere... */ void updateNormal() { needUpdateNormal = false; LineSide &lineSide = lineSideSegment().lineSide(); Surface &surface = lineSide.surface(spec.section); binangle_t angleDiff; Surface *blendSurface = findBlendNeighbor(angleDiff); if(blendSurface && shouldSmoothNormals(surface, *blendSurface, angleDiff)) { // Average normals. normal = Vector3f(surface.normal() + blendSurface->normal()) / 2; } else { normal = surface.normal(); } } }; WallEdge::WallEdge(WallSpec const &spec, HEdge &hedge, int edge) : WorldEdge((edge? hedge.twin() : hedge).origin()), d(new Instance(this, spec, hedge, edge)) {} Vector3d const &WallEdge::pOrigin() const { return d->pOrigin; } Vector3d const &WallEdge::pDirection() const { return d->pDirection; } Vector2f WallEdge::materialOrigin() const { return d->materialOrigin; } Vector3f WallEdge::normal() const { if(d->needUpdateNormal) { d->updateNormal(); } return d->normal; } WallSpec const &WallEdge::spec() const { return d->spec; } LineSide &WallEdge::mapLineSide() const { return d->lineSideSegment().lineSide(); } coord_t WallEdge::mapLineSideOffset() const { return lineSideOffset(d->lineSideSegment(), d->edge); } int WallEdge::divisionCount() const { if(!isValid()) return 0; if(!d->events) { d->prepareEvents(); } return d->interceptCount() - 2; } WallEdge::EventIndex WallEdge::firstDivision() const { return divisionCount()? 1 : InvalidIndex; } WallEdge::EventIndex WallEdge::lastDivision() const { return divisionCount()? d->interceptCount() - 2 : InvalidIndex; } WallEdge::Events const &WallEdge::events() const { d->verifyValid(); if(!d->events) { d->prepareEvents(); } return *d->events; } WallEdge::Event const &WallEdge::at(EventIndex index) const { return *events().at(index); } bool WallEdge::isValid() const { return d->hi > d->lo; } WallEdge::Event const &WallEdge::first() const { return d->bottom; } WallEdge::Event const &WallEdge::last() const { return d->top; } doomsday-stable-1.15.7/doomsday/client/src/render/huecirclevisual.cpp0000664000175000017500000000711712641367670025240 0ustar jaakkojaakko/** @file huecirclevisual.cpp HueCircle visualizer. * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include // M_HSVToRGB(), remove me (use QColor) #include "de_base.h" #include "de_graphics.h" #include "HueCircle" #include "render/huecirclevisual.h" using namespace de; void HueCircleVisual::draw(HueCircle &hueCircle, Vector3d const &viewOrigin, Vector3f const &viewFrontVec) // static { float const steps = 32; float const inner = 10; float const outer = 30; // Determine the origin of the circle in the map coordinate space. Vector3d center = hueCircle.origin(viewOrigin); // Draw the circle. glBegin(GL_QUAD_STRIP); for(int i = 0; i <= steps; ++i) { Vector3f off = hueCircle.offset(float(2 * de::PI) * i/steps); // Determine the RGB color for this angle. float color[3]; M_HSVToRGB(color, i/steps, 1, 1); glColor4f(color[0], color[1], color[2], .5f); glVertex3f(center.x + outer * off.x, center.y + outer * off.y, center.z + outer * off.z); // Saturation decreases toward the center. glColor4f(1, 1, 1, .15f); glVertex3f(center.x + inner * off.x, center.y + inner * off.y, center.z + inner * off.z); } glEnd(); // Draw the current hue. float hue, saturation; Vector3f sel = hueCircle.colorAt(viewFrontVec, &hue, &saturation); Vector3f off = hueCircle.offset(float(2 * de::PI) * hue); glBegin(GL_LINES); if(saturation > 0) { glColor4f(sel.x, sel.y, sel.z, 1.f); glVertex3f(center.x + outer * off.x, center.y + outer * off.y, center.z + outer * off.z); glVertex3f(center.x + inner * off.x, center.y + inner * off.y, center.z + inner * off.z); } // Draw the edges. for(int i = 0; i < steps; ++i) { Vector3f off = hueCircle.offset(float(2 * de::PI) * i/steps); Vector3f off2 = hueCircle.offset(float(2 * de::PI) * (i + 1)/steps); // Determine the RGB color for this angle. float color[3]; M_HSVToRGB(color, i/steps, 1, 1); glColor4f(color[0], color[1], color[2], 1.f); glVertex3f(center.x + outer * off.x, center.y + outer * off.y, center.z + outer * off.z); glVertex3f(center.x + outer * off2.x, center.y + outer * off2.y, center.z + outer * off2.z); // Saturation decreases in the center. float alpha = (de::fequal(saturation, 0)? 0 : 1 - de::abs(M_CycleIntoRange(hue - i/steps + .5f, 1) - .5f) * 2.5f); glColor4f(sel.x, sel.y, sel.z, alpha); float s = inner + (outer - inner) * saturation; glVertex3f(center.x + s * off.x, center.y + s * off.y, center.z + s * off.z); glVertex3f(center.x + s * off2.x, center.y + s * off2.y, center.z + s * off2.z); } glEnd(); } doomsday-stable-1.15.7/doomsday/client/src/render/lightdecoration.cpp0000664000175000017500000001006312641367670025222 0ustar jaakkojaakko/** @file lightdecoration.cpp World surface light decoration. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "render/lightdecoration.h" #include #include "def_main.h" #include "world/map.h" #include "BspLeaf" #include "ConvexSubspace" #include "SectorCluster" #include "Surface" #include "render/rend_main.h" // Rend_ApplyLightAdaptation using namespace de; static float angleFadeFactor = .1f; ///< cvar static float brightFactor = 1; ///< cvar LightDecoration::LightDecoration(MaterialAnimator::Decoration const &source, Vector3d const &origin) : Decoration(source, origin) , Source() {} float LightDecoration::occlusion(Vector3d const &eye) const { // Halo brightness drops as the angle gets too big. if(source().elevation() < 2 && ::angleFadeFactor > 0) // Close the surface? { Vector3d const vecFromOriginToEye = (origin() - eye).normalize(); float dot = float( -surface().normal().dot(vecFromOriginToEye) ); if(dot < ::angleFadeFactor / 2) { return 0; } else if(dot < 3 * ::angleFadeFactor) { return (dot - ::angleFadeFactor / 2) / (2.5f * ::angleFadeFactor); } } return 1; } /** * @return @c > 0 if @a lightlevel passes the min max limit condition. */ static float checkLightLevel(float lightlevel, float min, float max) { // Has a limit been set? if(de::fequal(min, max)) return 1; return de::clamp(0.f, (lightlevel - min) / float(max - min), 1.f); } Lumobj *LightDecoration::generateLumobj() const { // Decorations with zero color intensity produce no light. if(source().color() == Vector3f(0, 0, 0)) return nullptr; ConvexSubspace *subspace = bspLeafAtOrigin().subspacePtr(); if(!subspace) return nullptr; // Does it pass the ambient light limitation? float intensity = subspace->cluster().lightSourceIntensity(); Rend_ApplyLightAdaptation(intensity); float lightLevels[2]; source().lightLevels(lightLevels[0], lightLevels[1]); intensity = checkLightLevel(intensity, lightLevels[0], lightLevels[1]); if(intensity < .0001f) return nullptr; // Apply the brightness factor (was calculated using sector lightlevel). float fadeMul = intensity * ::brightFactor; if(fadeMul <= 0) return nullptr; Lumobj *lum = new Lumobj(origin(), source().radius(), source().color() * fadeMul); lum->setSource(this); lum->setMaxDistance (MAX_DECOR_DISTANCE) .setLightmap (Lumobj::Side, source().tex()) .setLightmap (Lumobj::Down, source().floorTex()) .setLightmap (Lumobj::Up, source().ceilTex()) .setFlareSize (source().flareSize()) .setFlareTexture(source().flareTex()); return lum; } void LightDecoration::consoleRegister() // static { C_VAR_FLOAT("rend-light-decor-angle", &::angleFadeFactor, 0, 0, 1); C_VAR_FLOAT("rend-light-decor-bright", &::brightFactor, 0, 0, 10); } float LightDecoration::angleFadeFactor() // static { return ::angleFadeFactor; } float LightDecoration::brightFactor() // static { return ::brightFactor; } doomsday-stable-1.15.7/doomsday/client/src/render/rendpoly.cpp0000664000175000017500000002562112641367670023705 0ustar jaakkojaakko/** @file rendpoly.cpp RendPoly data buffers * @ingroup render * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "de_console.h" #include "de_render.h" #include "color.h" #include "WallEdge" #include "render/rendpoly.h" using namespace de; enum RPolyDataType { RPT_VERT, RPT_COLOR, RPT_TEXCOORD }; struct RPolyData { dd_bool inUse; RPolyDataType type; uint num; void* data; }; byte rendInfoRPolys = 0; static unsigned int numrendpolys = 0; static unsigned int maxrendpolys = 0; static RPolyData** rendPolys; void R_PrintRendPoolInfo() { if(!rendInfoRPolys) return; LOGDEV_GL_MSG("RP Count: %-4i") << numrendpolys; for(uint i = 0; i < numrendpolys; ++i) { RPolyData* rp = rendPolys[i]; LOGDEV_GL_VERBOSE(_E(m) "RP: %-4u %c (vtxs=%i t=%c)") << i << (rp->inUse? 'Y':'N') << rp->num << (rp->type == RPT_VERT? 'v' : rp->type == RPT_COLOR? 'c' : 't'); } } void R_InitRendPolyPools() { numrendpolys = maxrendpolys = 0; rendPolys = 0; Vector3f *rvertices = R_AllocRendVertices(24); Vector4f *rcolors = R_AllocRendColors(24); Vector2f *rtexcoords = R_AllocRendTexCoords(24); // Mark unused. R_FreeRendVertices(rvertices); R_FreeRendColors(rcolors); R_FreeRendTexCoords(rtexcoords); } Vector3f *R_AllocRendVertices(uint num) { uint idx; dd_bool found = false; for(idx = 0; idx < maxrendpolys; ++idx) { if(rendPolys[idx]->inUse) continue; if(rendPolys[idx]->type != RPT_VERT) continue; if(rendPolys[idx]->num >= num) { // Use this one. rendPolys[idx]->inUse = true; return (Vector3f *) rendPolys[idx]->data; } else if(rendPolys[idx]->num == 0) { // There is an unused one but we haven't allocated verts yet. numrendpolys++; found = true; break; } } if(!found) { // We may need to allocate more. if(++numrendpolys > maxrendpolys) { uint i, newCount; RPolyData *newPolyData, *ptr; maxrendpolys = (maxrendpolys > 0? maxrendpolys * 2 : 8); rendPolys = (RPolyData **) Z_Realloc(rendPolys, sizeof(RPolyData *) * maxrendpolys, PU_MAP); newCount = maxrendpolys - numrendpolys + 1; newPolyData = (RPolyData *) Z_Malloc(sizeof(RPolyData) * newCount, PU_MAP, 0); ptr = newPolyData; for(i = numrendpolys-1; i < maxrendpolys; ++i, ptr++) { ptr->inUse = false; ptr->num = 0; ptr->data = NULL; ptr->type = RPT_VERT; rendPolys[i] = ptr; } } idx = numrendpolys - 1; } rendPolys[idx]->inUse = true; rendPolys[idx]->type = RPT_VERT; rendPolys[idx]->num = num; rendPolys[idx]->data = Z_Malloc(sizeof(Vector3f) * num, PU_MAP, 0); return (Vector3f *) rendPolys[idx]->data; } Vector4f *R_AllocRendColors(uint num) { uint idx; dd_bool found = false; for(idx = 0; idx < maxrendpolys; ++idx) { if(rendPolys[idx]->inUse) continue; if(rendPolys[idx]->type != RPT_COLOR) continue; if(rendPolys[idx]->num >= num) { // Use this one. rendPolys[idx]->inUse = true; return (Vector4f *) rendPolys[idx]->data; } else if(rendPolys[idx]->num == 0) { // There is an unused one but we haven't allocated verts yet. numrendpolys++; found = true; break; } } if(!found) { // We may need to allocate more. if(++numrendpolys > maxrendpolys) { uint i, newCount; RPolyData *newPolyData, *ptr; maxrendpolys = (maxrendpolys > 0? maxrendpolys * 2 : 8); rendPolys = (RPolyData **) Z_Realloc(rendPolys, sizeof(RPolyData *) * maxrendpolys, PU_MAP); newCount = maxrendpolys - numrendpolys + 1; newPolyData = (RPolyData *) Z_Malloc(sizeof(RPolyData) * newCount, PU_MAP, 0); ptr = newPolyData; for(i = numrendpolys-1; i < maxrendpolys; ++i, ptr++) { ptr->inUse = false; ptr->num = 0; ptr->data = NULL; ptr->type = RPT_COLOR; rendPolys[i] = ptr; } } idx = numrendpolys - 1; } rendPolys[idx]->inUse = true; rendPolys[idx]->type = RPT_COLOR; rendPolys[idx]->num = num; rendPolys[idx]->data = Z_Malloc(sizeof(Vector4f) * num, PU_MAP, 0); return (Vector4f *) rendPolys[idx]->data; } Vector2f *R_AllocRendTexCoords(uint num) { uint idx; dd_bool found = false; for(idx = 0; idx < maxrendpolys; ++idx) { if(rendPolys[idx]->inUse) continue; if(rendPolys[idx]->type != RPT_TEXCOORD) continue; if(rendPolys[idx]->num >= num) { // Use this one. rendPolys[idx]->inUse = true; return (Vector2f *) rendPolys[idx]->data; } else if(rendPolys[idx]->num == 0) { // There is an unused one but we haven't allocated verts yet. numrendpolys++; found = true; break; } } if(!found) { // We may need to allocate more. if(++numrendpolys > maxrendpolys) { uint i, newCount; RPolyData *newPolyData, *ptr; maxrendpolys = (maxrendpolys > 0? maxrendpolys * 2 : 8); rendPolys = (RPolyData **) Z_Realloc(rendPolys, sizeof(RPolyData *) * maxrendpolys, PU_MAP); newCount = maxrendpolys - numrendpolys + 1; newPolyData = (RPolyData *) Z_Malloc(sizeof(RPolyData) * newCount, PU_MAP, 0); ptr = newPolyData; for(i = numrendpolys-1; i < maxrendpolys; ++i, ptr++) { ptr->inUse = false; ptr->num = 0; ptr->data = NULL; ptr->type = RPT_TEXCOORD; rendPolys[i] = ptr; } } idx = numrendpolys - 1; } rendPolys[idx]->inUse = true; rendPolys[idx]->type = RPT_TEXCOORD; rendPolys[idx]->num = num; rendPolys[idx]->data = Z_Malloc(sizeof(Vector2f) * num, PU_MAP, 0); return (Vector2f *) rendPolys[idx]->data; } void R_FreeRendVertices(Vector3f *rvertices) { if(!rvertices) return; for(uint i = 0; i < numrendpolys; ++i) { if(rendPolys[i]->data == rvertices) { rendPolys[i]->inUse = false; return; } } LOGDEV_GL_WARNING("R_FreeRendPoly: Dangling poly ptr!"); } void R_FreeRendColors(Vector4f *rcolors) { if(!rcolors) return; for(uint i = 0; i < numrendpolys; ++i) { if(rendPolys[i]->data == rcolors) { rendPolys[i]->inUse = false; return; } } LOGDEV_GL_WARNING("R_FreeRendPoly: Dangling poly ptr!"); } void R_FreeRendTexCoords(Vector2f *rtexcoords) { if(!rtexcoords) return; for(uint i = 0; i < numrendpolys; ++i) { if(rendPolys[i]->data == rtexcoords) { rendPolys[i]->inUse = false; return; } } LOGDEV_GL_WARNING("R_FreeRendPoly: Dangling poly ptr!"); } void R_DivVerts(Vector3f *dst, Vector3f const *src, WorldEdge const &leftEdge, WorldEdge const &rightEdge) { int const numR = 3 + rightEdge.divisionCount(); int const numL = 3 + leftEdge.divisionCount(); if(numR + numL == 6) return; // Nothing to do. // Right fan: dst[numL + 0] = src[0]; dst[numL + 1] = src[3]; dst[numL + numR - 1] = src[2]; for(int n = 0; n < rightEdge.divisionCount(); ++n) { WorldEdge::Event const &icpt = rightEdge.at(rightEdge.lastDivision() - n); dst[numL + 2 + n] = icpt.origin(); } // Left fan: dst[0] = src[3]; dst[1] = src[0]; dst[numL - 1] = src[1]; for(int n = 0; n < leftEdge.divisionCount(); ++n) { WorldEdge::Event const &icpt = leftEdge.at(leftEdge.firstDivision() + n); dst[2 + n] = icpt.origin(); } } void R_DivTexCoords(Vector2f *dst, Vector2f const *src, WorldEdge const &leftEdge, WorldEdge const &rightEdge) { int const numR = 3 + rightEdge.divisionCount(); int const numL = 3 + leftEdge.divisionCount(); if(numR + numL == 6) return; // Nothing to do. // Right fan: dst[numL + 0] = src[0]; dst[numL + 1] = src[3]; dst[numL + numR-1] = src[2]; for(int n = 0; n < rightEdge.divisionCount(); ++n) { WorldEdge::Event const &icpt = rightEdge.at(rightEdge.lastDivision() - n); dst[numL + 2 + n].x = src[3].x; dst[numL + 2 + n].y = src[2].y + (src[3].y - src[2].y) * icpt.distance(); } // Left fan: dst[0] = src[3]; dst[1] = src[0]; dst[numL - 1] = src[1]; for(int n = 0; n < leftEdge.divisionCount(); ++n) { WorldEdge::Event const &icpt = leftEdge.at(leftEdge.firstDivision() + n); dst[2 + n].x = src[0].x; dst[2 + n].y = src[0].y + (src[1].y - src[0].y) * icpt.distance(); } } void R_DivVertColors(Vector4f *dst, Vector4f const *src, WorldEdge const &leftEdge, WorldEdge const &rightEdge) { int const numR = 3 + rightEdge.divisionCount(); int const numL = 3 + leftEdge.divisionCount(); if(numR + numL == 6) return; // Nothing to do. // Right fan: dst[numL + 0] = src[0]; dst[numL + 1] = src[3]; dst[numL + numR-1] = src[2]; for(int n = 0; n < rightEdge.divisionCount(); ++n) { WorldEdge::Event const &icpt = rightEdge.at(rightEdge.lastDivision() - n); dst[numL + 2 + n] = src[2] + (src[3] - src[2]) * icpt.distance(); } // Left fan: dst[0] = src[3]; dst[1] = src[0]; dst[numL - 1] = src[1]; for(int n = 0; n < leftEdge.divisionCount(); ++n) { WorldEdge::Event const &icpt = leftEdge.at(leftEdge.firstDivision() + n); dst[2 + n] = src[0] + (src[1] - src[0]) * icpt.distance(); } } doomsday-stable-1.15.7/doomsday/client/src/render/vissprite.cpp0000664000175000017500000001224712641367670024101 0ustar jaakkojaakko/** @file vissprite.cpp Projected visible sprite ("vissprite") management. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * @authors Copyright © 1993-1996 by id Software, Inc. * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "render/vissprite.h" using namespace de; /// @todo This should not be a fixed-size array. -jk vissprite_t visSprites[MAXVISSPRITES], *visSpriteP; vispsprite_t visPSprites[DDMAXPSPRITES]; vissprite_t visSprSortedHead; static vissprite_t overflowVisSprite; void R_ClearVisSprites() { visSpriteP = visSprites; } vissprite_t *R_NewVisSprite(visspritetype_t type) { vissprite_t *spr; if(visSpriteP == &visSprites[MAXVISSPRITES]) { spr = &overflowVisSprite; } else { visSpriteP++; spr = visSpriteP - 1; } de::zapPtr(spr); spr->type = type; return spr; } void VisSprite_SetupSprite(vissprite_t *spr, VisEntityPose const &pose, VisEntityLighting const &light, dfloat /*secFloor*/, dfloat /*secCeil*/, dfloat /*floorClip*/, dfloat /*top*/, Material &material, bool matFlipS, bool matFlipT, blendmode_t blendMode, dint tClass, dint tMap, BspLeaf *bspLeafAtOrigin, bool /*floorAdjust*/, bool /*fitTop*/, bool /*fitBottom*/) { drawspriteparams_t &p = *VS_SPRITE(spr); MaterialVariantSpec const &spec = Rend_SpriteMaterialSpec(tClass, tMap); MaterialAnimator *matAnimator = &material.getAnimator(spec); DENG2_ASSERT((tClass == 0 && tMap == 0) || (spec.primarySpec->variant.flags & TSF_HAS_COLORPALETTE_XLAT)); spr->pose = pose; p.bspLeaf = bspLeafAtOrigin; p.noZWrite = noSpriteZWrite; p.matAnimator = matAnimator; p.matFlip[0] = matFlipS; p.matFlip[1] = matFlipT; p.blendMode = (useSpriteBlend? blendMode : BM_NORMAL); spr->light = light; spr->light.ambientColor[3] = (useSpriteAlpha? light.ambientColor.w : 1); } void VisSprite_SetupModel(vissprite_t *spr, VisEntityPose const &pose, VisEntityLighting const &light, ModelDef *mf, ModelDef *nextMF, dfloat inter, dint id, dint selector, BspLeaf * /*bspLeafAtOrigin*/, dint mobjDDFlags, dint tmap, bool /*fullBright*/, bool alwaysInterpolate) { drawmodelparams_t &p = *VS_MODEL(spr); p.mf = mf; p.nextMF = nextMF; p.inter = inter; p.alwaysInterpolate = alwaysInterpolate; p.id = id; p.selector = selector; p.flags = mobjDDFlags; p.tmap = tmap; spr->pose = pose; spr->light = light; p.shineYawOffset = 0; p.shinePitchOffset = 0; p.shineTranslateWithViewerPos = p.shinepspriteCoordSpace = false; } void R_SortVisSprites() { if(!visSpriteP) return; dint const count = visSpriteP - visSprites; if(!count) return; vissprite_t unsorted; unsorted.next = unsorted.prev = &unsorted; for(vissprite_t *ds = visSprites; ds < visSpriteP; ds++) { ds->next = ds + 1; ds->prev = ds - 1; } visSprites[0].prev = &unsorted; unsorted.next = &visSprites[0]; (visSpriteP - 1)->next = &unsorted; unsorted.prev = visSpriteP - 1; // Pull the vissprites out by distance. visSprSortedHead.next = visSprSortedHead.prev = &visSprSortedHead; /// @todo Optimize: /// Profile results from nuts.wad show over 25% of total execution time /// was spent sorting vissprites (nuts.wad map01 is a perfect pathological /// test case). /// /// Rather than try to speed up the sort, it would make more sense to /// actually construct the vissprites in z order if it can be done in /// linear time. vissprite_t *best = nullptr; for(dint i = 0; i < count; ++i) { coord_t bestdist = 0; for(vissprite_t *ds = unsorted.next; ds != &unsorted; ds = ds->next) { if(ds->pose.distance >= bestdist) { bestdist = ds->pose.distance; best = ds; } } best->next->prev = best->prev; best->prev->next = best->next; best->next = &visSprSortedHead; best->prev = visSprSortedHead.prev; visSprSortedHead.prev->next = best; visSprSortedHead.prev = best; } } doomsday-stable-1.15.7/doomsday/client/src/render/biasdigest.cpp0000664000175000017500000000305112641367670024160 0ustar jaakkojaakko/** @file biasdigest.h Shadow Bias change digest. * * @authors Copyright © 2005-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "render/biasdigest.h" #include "world/map.h" using namespace de; static int const FIELDSIZE = (Map::MAX_BIAS_SOURCES / 8); DENG2_PIMPL_NOREF(BiasDigest) { uint changes[FIELDSIZE]; Instance() { de::zap(changes); } }; BiasDigest::BiasDigest() : d(new Instance()) {} void BiasDigest::reset() { zap(d->changes); } void BiasDigest::markSourceChanged(uint index) { // Assume 32-bit uint. d->changes[index >> 5] |= (1 << (index & 0x1f)); } bool BiasDigest::isSourceChanged(uint index) const { // Assume 32-bit uint. return (d->changes[index >> 5] & (1 << (index & 0x1f))) != 0; } doomsday-stable-1.15.7/doomsday/client/src/render/r_main.cpp0000664000175000017500000002371312641367670023316 0ustar jaakkojaakko/** @file r_main.cpp * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "render/r_main.h" #include #include #include "dd_def.h" // finesine #include "clientapp.h" #include "render/billboard.h" #include "render/rend_main.h" #include "render/rend_model.h" #include "render/vissprite.h" #include "world/map.h" #include "world/p_players.h" #include "BspLeaf" #include "ConvexSubspace" #include "SectorCluster" using namespace de; dint levelFullBright; dint weaponOffsetScaleY = 1000; dint psp3d; dfloat pspLightLevelMultiplier = 1; dfloat pspOffset[2]; /* * Console variables: */ dfloat weaponFOVShift = 45; dfloat weaponOffsetScale = 0.3183f; // 1/Pi dbyte weaponScaleMode = SCALEMODE_SMART_STRETCH; static inline RenderSystem &rendSys() { return ClientApp::renderSystem(); } static inline ResourceSystem &resSys() { return ClientApp::resourceSystem(); } static MaterialVariantSpec const &pspriteMaterialSpec() { return resSys().materialSpec(PSpriteContext, 0, 1, 0, 0, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 0, -2, 0, false, true, true, false); } static void setupPSpriteParams(rendpspriteparams_t *params, vispsprite_t *spr) { static dint const WEAPONTOP = 32; /// @todo Currently hardcoded here and in the plugins. ddpsprite_t *psp = spr->psp; dfloat const offScaleY = weaponOffsetScaleY / 1000.0f; dint const spriteIdx = psp->statePtr->sprite; dint const frameIdx = psp->statePtr->frame; Sprite const &sprite = resSys().sprite(spriteIdx, frameIdx); SpriteViewAngle const &sprViewAngle = sprite.viewAngle(0); MaterialAnimator &matAnimator = sprViewAngle.material->getAnimator(pspriteMaterialSpec()); // Ensure we've up to date info about the material. matAnimator.prepare(); Vector2i const &matDimensions = matAnimator.dimensions(); TextureVariant const &tex = *matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; Vector2i const &texOrigin = tex.base().origin(); variantspecification_t const &texSpec = tex.spec().variant; params->pos[0] = psp->pos[0] + texOrigin.x + pspOffset[0] - texSpec.border; params->pos[1] = WEAPONTOP + offScaleY * (psp->pos[1] - WEAPONTOP) + texOrigin.y + pspOffset[1] - texSpec.border; params->width = matDimensions.x + texSpec.border * 2; params->height = matDimensions.y + texSpec.border * 2; tex.glCoords(¶ms->texOffset[0], ¶ms->texOffset[1]); params->texFlip[0] = sprViewAngle.mirrorX; params->texFlip[1] = false; params->mat = &matAnimator.material(); params->ambientColor[3] = spr->data.sprite.alpha; if(spr->data.sprite.isFullBright) { params->ambientColor[0] = params->ambientColor[1] = params->ambientColor[2] = 1; params->vLightListIdx = 0; } else { Map &map = ClientApp::worldSystem().map(); if(useBias && map.hasLightGrid()) { // Evaluate the position in the light grid. Vector4f color = map.lightGrid().evaluate(spr->origin); // Apply light range compression. for(dint i = 0; i < 3; ++i) { color[i] += Rend_LightAdaptationDelta(color[i]); } V3f_Set(params->ambientColor, color.x, color.y, color.z); } else { Vector4f const color = spr->data.sprite.bspLeaf->subspace().cluster().lightSourceColorfIntensity(); // No need for distance attentuation. dfloat lightLevel = color.w; // Add extra light plus bonus. lightLevel += Rend_ExtraLightDelta(); lightLevel *= pspLightLevelMultiplier; // The last step is to compress the resultant light value by // the global lighting function. Rend_ApplyLightAdaptation(lightLevel); // Determine the final ambientColor. for(dint i = 0; i < 3; ++i) { params->ambientColor[i] = lightLevel * color[i]; } } Rend_ApplyTorchLight(params->ambientColor, 0); params->vLightListIdx = Rend_CollectAffectingLights(spr->origin, Vector3f(params->ambientColor), spr->data.sprite.bspLeaf->subspacePtr()); } } void Rend_Draw2DPlayerSprites() { ddplayer_t *ddpl = &viewPlayer->shared; // Cameramen have no HUD sprites. if(ddpl->flags & DDPF_CAMERA) return; if(ddpl->flags & DDPF_CHASECAM) return; if(usingFog) { glEnable(GL_FOG); } // Check for fullbright. int i; ddpsprite_t *psp; for(i = 0, psp = ddpl->pSprites; i < DDMAXPSPRITES; ++i, psp++) { vispsprite_t *spr = &visPSprites[i]; // Should this psprite be drawn? if(spr->type != VPSPR_SPRITE) continue; // No... // Draw as separate sprites. if(spr->psp && spr->psp->statePtr) { rendpspriteparams_t params; setupPSpriteParams(¶ms, spr); Rend_DrawPSprite(params); } } if(usingFog) { glDisable(GL_FOG); } } static void setupModelParamsForVisPSprite(vissprite_t &vis, vispsprite_t const *spr) { drawmodelparams_t *params = VS_MODEL(&vis); params->mf = spr->data.model.mf; params->nextMF = spr->data.model.nextMF; params->inter = spr->data.model.inter; params->alwaysInterpolate = false; params->id = spr->data.model.id; params->selector = spr->data.model.selector; params->flags = spr->data.model.flags; vis.pose.origin = spr->origin; vis.pose.srvo[0] = spr->data.model.visOff[0]; vis.pose.srvo[1] = spr->data.model.visOff[1]; vis.pose.srvo[2] = spr->data.model.visOff[2] - spr->data.model.floorClip; vis.pose.topZ = spr->data.model.topZ; vis.pose.distance = -10; vis.pose.yaw = spr->data.model.yaw; vis.pose.extraYawAngle = 0; vis.pose.yawAngleOffset = spr->data.model.yawAngleOffset; vis.pose.pitch = spr->data.model.pitch; vis.pose.extraPitchAngle = 0; vis.pose.pitchAngleOffset = spr->data.model.pitchAngleOffset; vis.pose.extraScale = 0; vis.pose.viewAligned = spr->data.model.viewAligned; vis.pose.mirrored = (mirrorHudModels? true : false); params->shineYawOffset = -vang; params->shinePitchOffset = vpitch + 90; params->shineTranslateWithViewerPos = false; params->shinepspriteCoordSpace = true; vis.light.ambientColor[3] = spr->data.model.alpha; if((levelFullBright || spr->data.model.stateFullBright) && !spr->data.model.mf->testSubFlag(0, MFF_DIM)) { vis.light.ambientColor[0] = vis.light.ambientColor[1] = vis.light.ambientColor[2] = 1; vis.light.vLightListIdx = 0; } else { Map &map = ClientApp::worldSystem().map(); if(useBias && map.hasLightGrid()) { Vector4f color = map.lightGrid().evaluate(vis.pose.origin); // Apply light range compression. for(dint i = 0; i < 3; ++i) { color[i] += Rend_LightAdaptationDelta(color[i]); } vis.light.ambientColor.x = color.x; vis.light.ambientColor.y = color.y; vis.light.ambientColor.z = color.z; } else { Vector4f const color = spr->data.model.bspLeaf->subspace().cluster().lightSourceColorfIntensity(); // No need for distance attentuation. dfloat lightLevel = color.w; // Add extra light. lightLevel += Rend_ExtraLightDelta(); // The last step is to compress the resultant light value by // the global lighting function. Rend_ApplyLightAdaptation(lightLevel); // Determine the final ambientColor. for(dint i = 0; i < 3; ++i) { vis.light.ambientColor[i] = lightLevel * color[i]; } } Rend_ApplyTorchLight(vis.light.ambientColor, vis.pose.distance); vis.light.vLightListIdx = Rend_CollectAffectingLights(spr->origin, vis.light.ambientColor, spr->data.model.bspLeaf->subspacePtr(), true /*stark world light*/); } } void Rend_Draw3DPlayerSprites() { // Setup the modelview matrix. Rend_ModelViewMatrix(false /* don't apply view angle rotation */); static GLTexture localDepth; // note: static! GLTarget::AlternativeBuffer altDepth(GLState::current().target(), localDepth, GLTarget::DepthStencil); for(dint i = 0; i < DDMAXPSPRITES; ++i) { vispsprite_t *spr = &visPSprites[i]; if(spr->type != VPSPR_MODEL) continue; // Not used. if(altDepth.init()) { // Clear the depth before first use. altDepth.target().clear(GLTarget::DepthStencil); } //drawmodelparams_t parms; zap(parms); vissprite_t temp; de::zap(temp); setupModelParamsForVisPSprite(temp, spr); Rend_DrawModel(temp); } } doomsday-stable-1.15.7/doomsday/client/src/render/rend_font.cpp0000664000175000017500000012631012641367670024024 0ustar jaakkojaakko/** @file rend_font.cpp Font Renderer. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include #include #define DENG_NO_API_MACROS_FONT_RENDER #include "de_base.h" #include "de_console.h" #include "de_graphics.h" #include "de_render.h" #include "de_system.h" #include "de_ui.h" #include "api_fontrender.h" #include "m_misc.h" #include "BitmapFont" #include "CompositeBitmapFont" #include "gl/gl_texmanager.h" #include using namespace de; /** * @ingroup drawTextFlags * @{ */ #define DTF_INTERNAL_MASK 0xff00 #define DTF_NO_CHARACTER 0x8000 /// Only draw text decorations. /**@}*/ typedef struct fr_state_attributes_s { int tracking; float leading; float rgba[4]; int shadowOffsetX, shadowOffsetY; float shadowStrength; float glitterStrength; dd_bool caseScale; } fr_state_attributes_t; // Used with FR_LoadDefaultAttribs static fr_state_attributes_t defaultAttribs = { FR_DEF_ATTRIB_TRACKING, FR_DEF_ATTRIB_LEADING, { FR_DEF_ATTRIB_COLOR_RED, FR_DEF_ATTRIB_COLOR_GREEN, FR_DEF_ATTRIB_COLOR_BLUE, FR_DEF_ATTRIB_ALPHA }, FR_DEF_ATTRIB_SHADOW_XOFFSET, FR_DEF_ATTRIB_SHADOW_YOFFSET, FR_DEF_ATTRIB_SHADOW_STRENGTH, FR_DEF_ATTRIB_GLITTER_STRENGTH, FR_DEF_ATTRIB_CASE_SCALE }; typedef struct { fontid_t fontNum; int attribStackDepth; fr_state_attributes_t attribStack[FR_MAX_ATTRIB_STACK_DEPTH]; } fr_state_t; static fr_state_t fr; fr_state_t* frState = &fr; typedef struct { fontid_t fontNum; float scaleX, scaleY; float offX, offY; float angle; float rgba[4]; float glitterStrength, shadowStrength; int shadowOffsetX, shadowOffsetY; int tracking; float leading; int lastLineHeight; dd_bool typeIn; dd_bool caseScale; struct { float scale, offset; } caseMod[2]; // 1=upper, 0=lower } drawtextstate_t; static void drawChar(uchar ch, float posX, float posY, AbstractFont *font, int alignFlags, short textFlags); static void drawFlash(Point2Raw const *origin, Size2Raw const *size, bool bright); static int inited = false; static char smallTextBuffer[FR_SMALL_TEXT_BUFFER_SIZE+1]; static char *largeTextBuffer = NULL; static size_t largeTextBufferSize = 0; static int typeInTime; static void errorIfNotInited(const char* callerName) { if(inited) return; App_Error("%s: font renderer module is not presently initialized.", callerName); // Unreachable. Prevents static analysers from getting rather confused, poor things. exit(1); } static int topToAscent(AbstractFont *font) { int lineHeight = font->lineSpacing(); if(lineHeight == 0) return 0; return lineHeight - font->ascent(); } static inline fr_state_attributes_t *currentAttribs(void) { return fr.attribStack + fr.attribStackDepth; } void FR_Shutdown(void) { if(!inited) return; inited = false; } dd_bool FR_Available(void) { return inited; } void FR_Ticker(timespan_t /*ticLength*/) { if(!inited) return; // Restricted to fixed 35 Hz ticks. /// @todo We should not be synced to the games' fixed "sharp" timing. /// This font renderer is used by the engine's UI also. if(!DD_IsSharpTick()) return; // It's too soon. ++typeInTime; } #undef FR_ResetTypeinTimer void FR_ResetTypeinTimer(void) { errorIfNotInited("FR_ResetTypeinTimer"); typeInTime = 0; } #undef FR_SetFont void FR_SetFont(fontid_t num) { errorIfNotInited("FR_SetFont"); if(num != NOFONTID) { try { App_ResourceSystem().toFontManifest(num); fr.fontNum = num; return; } catch(ResourceSystem::UnknownFontIdError const &) {} } else { fr.fontNum = num; } } void FR_SetNoFont(void) { errorIfNotInited("FR_SetNoFont"); fr.fontNum = 0; } #undef FR_Font fontid_t FR_Font(void) { errorIfNotInited("FR_Font"); return fr.fontNum; } #undef FR_LoadDefaultAttrib void FR_LoadDefaultAttrib(void) { errorIfNotInited("FR_LoadDefaultAttrib"); memcpy(currentAttribs(), &defaultAttribs, sizeof(defaultAttribs)); } #undef FR_PushAttrib void FR_PushAttrib(void) { errorIfNotInited("FR_PushAttrib"); if(fr.attribStackDepth+1 == FR_MAX_ATTRIB_STACK_DEPTH) { App_Error("FR_PushAttrib: STACK_OVERFLOW."); exit(1); // Unreachable. } memcpy(fr.attribStack + fr.attribStackDepth + 1, fr.attribStack + fr.attribStackDepth, sizeof(fr.attribStack[0])); ++fr.attribStackDepth; } #undef FR_PopAttrib void FR_PopAttrib(void) { errorIfNotInited("FR_PopAttrib"); if(fr.attribStackDepth == 0) { App_Error("FR_PopAttrib: STACK_UNDERFLOW."); exit(1); // Unreachable. } --fr.attribStackDepth; } #undef FR_Leading float FR_Leading(void) { errorIfNotInited("FR_Leading"); return currentAttribs()->leading; } #undef FR_SetLeading void FR_SetLeading(float value) { errorIfNotInited("FR_SetLeading"); currentAttribs()->leading = value; } #undef FR_Tracking int FR_Tracking(void) { errorIfNotInited("FR_Tracking"); return currentAttribs()->tracking; } #undef FR_SetTracking void FR_SetTracking(int value) { errorIfNotInited("FR_SetTracking"); currentAttribs()->tracking = value; } #undef FR_ColorAndAlpha void FR_ColorAndAlpha(float rgba[4]) { errorIfNotInited("FR_ColorAndAlpha"); memcpy(rgba, currentAttribs()->rgba, sizeof(rgba[0]) * 4); } #undef FR_SetColor void FR_SetColor(float red, float green, float blue) { fr_state_attributes_t* sat = currentAttribs(); errorIfNotInited("FR_SetColor"); sat->rgba[CR] = red; sat->rgba[CG] = green; sat->rgba[CB] = blue; } #undef FR_SetColorv void FR_SetColorv(const float rgba[3]) { fr_state_attributes_t* sat = currentAttribs(); errorIfNotInited("FR_SetColorv"); sat->rgba[CR] = rgba[CR]; sat->rgba[CG] = rgba[CG]; sat->rgba[CB] = rgba[CB]; } #undef FR_SetColorAndAlpha void FR_SetColorAndAlpha(float red, float green, float blue, float alpha) { fr_state_attributes_t* sat = currentAttribs(); errorIfNotInited("FR_SetColorAndAlpha"); sat->rgba[CR] = red; sat->rgba[CG] = green; sat->rgba[CB] = blue; sat->rgba[CA] = alpha; } #undef FR_SetColorAndAlphav void FR_SetColorAndAlphav(const float rgba[4]) { fr_state_attributes_t* sat = currentAttribs(); errorIfNotInited("FR_SetColorAndAlphav"); sat->rgba[CR] = rgba[CR]; sat->rgba[CG] = rgba[CG]; sat->rgba[CB] = rgba[CB]; sat->rgba[CA] = rgba[CA]; } #undef FR_ColorRed float FR_ColorRed(void) { errorIfNotInited("FR_ColorRed"); return currentAttribs()->rgba[CR]; } #undef FR_SetColorRed void FR_SetColorRed(float value) { errorIfNotInited("FR_SetColorRed"); currentAttribs()->rgba[CR] = value; } #undef FR_ColorGreen float FR_ColorGreen(void) { errorIfNotInited("FR_ColorGreen"); return currentAttribs()->rgba[CG]; } #undef FR_SetColorGreen void FR_SetColorGreen(float value) { errorIfNotInited("FR_SetColorGreen"); currentAttribs()->rgba[CG] = value; } #undef FR_ColorBlue float FR_ColorBlue(void) { errorIfNotInited("FR_ColorBlue"); return currentAttribs()->rgba[CB]; } #undef FR_SetColorBlue void FR_SetColorBlue(float value) { errorIfNotInited("FR_SetColorBlue"); currentAttribs()->rgba[CB] = value; } #undef FR_Alpha float FR_Alpha(void) { errorIfNotInited("FR_Alpha"); return currentAttribs()->rgba[CA]; } #undef FR_SetAlpha void FR_SetAlpha(float value) { errorIfNotInited("FR_SetAlpha"); currentAttribs()->rgba[CA] = value; } #undef FR_ShadowOffset void FR_ShadowOffset(int* offsetX, int* offsetY) { fr_state_attributes_t* sat = currentAttribs(); errorIfNotInited("FR_ShadowOffset"); if(NULL != offsetX) *offsetX = sat->shadowOffsetX; if(NULL != offsetY) *offsetY = sat->shadowOffsetY; } #undef FR_SetShadowOffset void FR_SetShadowOffset(int offsetX, int offsetY) { fr_state_attributes_t* sat = currentAttribs(); errorIfNotInited("FR_SetShadowOffset"); sat->shadowOffsetX = offsetX; sat->shadowOffsetY = offsetY; } #undef FR_ShadowStrength float FR_ShadowStrength(void) { errorIfNotInited("FR_ShadowStrength"); return currentAttribs()->shadowStrength; } #undef FR_SetShadowStrength void FR_SetShadowStrength(float value) { errorIfNotInited("FR_SetShadowStrength"); currentAttribs()->shadowStrength = MINMAX_OF(0, value, 1); } #undef FR_GlitterStrength float FR_GlitterStrength(void) { errorIfNotInited("FR_GlitterStrength"); return currentAttribs()->glitterStrength; } #undef FR_SetGlitterStrength void FR_SetGlitterStrength(float value) { errorIfNotInited("FR_SetGlitterStrength"); currentAttribs()->glitterStrength = MINMAX_OF(0, value, 1); } #undef FR_CaseScale dd_bool FR_CaseScale(void) { errorIfNotInited("FR_CaseScale"); return currentAttribs()->caseScale; } #undef FR_SetCaseScale void FR_SetCaseScale(dd_bool value) { errorIfNotInited("FR_SetCaseScale"); currentAttribs()->caseScale = value; } #undef FR_CharSize void FR_CharSize(Size2Raw *size, uchar ch) { errorIfNotInited("FR_CharSize"); if(size) { Vector2ui dimensions = App_ResourceSystem().font(fr.fontNum).glyphPosCoords(ch).size(); size->width = dimensions.x; size->height = dimensions.y; } } #undef FR_CharWidth int FR_CharWidth(uchar ch) { errorIfNotInited("FR_CharWidth"); if(fr.fontNum != 0) return App_ResourceSystem().font(fr.fontNum).glyphPosCoords(ch).width(); return 0; } #undef FR_CharHeight int FR_CharHeight(uchar ch) { errorIfNotInited("FR_CharHeight"); if(fr.fontNum != 0) return App_ResourceSystem().font(fr.fontNum).glyphPosCoords(ch).height(); return 0; } int FR_SingleLineHeight(char const *text) { errorIfNotInited("FR_SingleLineHeight"); if(fr.fontNum == 0 || !text) return 0; AbstractFont &font = App_ResourceSystem().font(fr.fontNum); int ascent = font.ascent(); if(ascent != 0) return ascent; return font.glyphPosCoords((uchar)text[0]).height(); } int FR_GlyphTopToAscent(char const *text) { errorIfNotInited("FR_GlyphTopToAscent"); if(fr.fontNum == 0 || !text) return 0; AbstractFont &font = App_ResourceSystem().font(fr.fontNum); int lineHeight = font.lineSpacing(); if(lineHeight == 0) return 0; return lineHeight - font.ascent(); } static int textFragmentWidth(char const *fragment) { DENG2_ASSERT(fragment != 0); if(fr.fontNum == 0) { App_Error("textFragmentHeight: Cannot determine height without a current font."); exit(1); } int width = 0; // Just add them together. size_t len = strlen(fragment); size_t i = 0; char const *ch = fragment; uchar c; while(i++ < len && (c = *ch++) != 0 && c != '\n') { width += FR_CharWidth(c); } return int( width + currentAttribs()->tracking * (len-1) ); } static int textFragmentHeight(char const *fragment) { DENG2_ASSERT(fragment != 0); if(fr.fontNum == 0) { App_Error("textFragmentHeight: Cannot determine height without a current font."); exit(1); } int height = 0; // Find the greatest height. uint i = 0; size_t len = strlen(fragment); char const *ch = fragment; uchar c; while(i++ < len && (c = *ch++) != 0 && c != '\n') { height = de::max(height, FR_CharHeight(c)); } return topToAscent(&App_ResourceSystem().font(fr.fontNum)) + height; } /* static void textFragmentSize(int* width, int* height, const char* fragment) { if(width) *width = textFragmentWidth(fragment); if(height) *height = textFragmentHeight(fragment); } */ static void textFragmentDrawer(const char* fragment, int x, int y, int alignFlags, short textFlags, int initialCount) { DENG2_ASSERT(fragment != 0 && fragment[0]); AbstractFont *font = &App_ResourceSystem().font(fr.fontNum); fr_state_attributes_t* sat = currentAttribs(); dd_bool noTypein = (textFlags & DTF_NO_TYPEIN) != 0; dd_bool noGlitter = (sat->glitterStrength <= 0 || (textFlags & DTF_NO_GLITTER) != 0); dd_bool noShadow = (sat->shadowStrength <= 0 || (textFlags & DTF_NO_SHADOW) != 0 || font->flags().testFlag(AbstractFont::Shadowed)); dd_bool noCharacter = (textFlags & DTF_NO_CHARACTER) != 0; float glitter = (noGlitter? 0 : sat->glitterStrength), glitterMul; float shadow = (noShadow ? 0 : sat->shadowStrength), shadowMul; float flashColor[3] = { 0, 0, 0 }; int w, h, cx, cy, count, yoff; unsigned char c; const char* ch; if(alignFlags & ALIGN_RIGHT) x -= textFragmentWidth(fragment); else if(!(alignFlags & ALIGN_LEFT)) x -= textFragmentWidth(fragment)/2; if(alignFlags & ALIGN_BOTTOM) y -= textFragmentHeight(fragment); else if(!(alignFlags & ALIGN_TOP)) y -= textFragmentHeight(fragment)/2; if(!(noTypein && noGlitter)) { flashColor[CR] = (1 + 2 * sat->rgba[CR]) / 3; flashColor[CG] = (1 + 2 * sat->rgba[CG]) / 3; flashColor[CB] = (1 + 2 * sat->rgba[CB]) / 3; } if(renderWireframe > 1) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glDisable(GL_TEXTURE_2D); } if(BitmapFont *bmapFont = font->maybeAs()) { if(bmapFont->textureGLName()) { GL_BindTextureUnmanaged(bmapFont->textureGLName(), gl::ClampToEdge, gl::ClampToEdge, filterUI? gl::Linear : gl::Nearest); glMatrixMode(GL_TEXTURE); glPushMatrix(); glLoadIdentity(); glScalef(1.f / bmapFont->textureDimensions().x, 1.f / bmapFont->textureDimensions().y, 1.f); } } for(int pass = (noShadow? 1 : 0); pass < (noCharacter && noGlitter? 1 : 2); ++pass) { count = initialCount; ch = fragment; cx = x + (pass == 0? sat->shadowOffsetX : 0); cy = y + (pass == 0? sat->shadowOffsetY : 0); for(;;) { c = *ch++; yoff = 0; glitter = (noGlitter? 0 : sat->glitterStrength); glitterMul = 0; shadow = (noShadow? 0 : sat->shadowStrength); shadowMul = (noShadow? 0 : sat->rgba[CA]); // Do the type-in effect? if(!noTypein && (pass || (!noShadow && !pass))) { int maxCount = (typeInTime > 0? typeInTime * 2 : 0); if(pass) { if(!noGlitter) { if(count == maxCount) { glitterMul = 1; flashColor[CR] = sat->rgba[CR]; flashColor[CG] = sat->rgba[CG]; flashColor[CB] = sat->rgba[CB]; } else if(count + 1 == maxCount) { glitterMul = 0.88f; flashColor[CR] = (1 + sat->rgba[CR]) / 2; flashColor[CG] = (1 + sat->rgba[CG]) / 2; flashColor[CB] = (1 + sat->rgba[CB]) / 2; } else if(count + 2 == maxCount) { glitterMul = 0.75f; flashColor[CR] = sat->rgba[CR]; flashColor[CG] = sat->rgba[CG]; flashColor[CB] = sat->rgba[CB]; } else if(count + 3 == maxCount) { glitterMul = 0.5f; flashColor[CR] = sat->rgba[CR]; flashColor[CG] = sat->rgba[CG]; flashColor[CB] = sat->rgba[CB]; } else if(count > maxCount) { break; } } else if(count > maxCount) { break; } } else { if(count == maxCount) { shadowMul = 0; } else if(count + 1 == maxCount) { shadowMul *= .25f; } else if(count + 2 == maxCount) { shadowMul *= .5f; } else if(count + 3 == maxCount) { shadowMul *= .75f; } else if(count > maxCount) { break; } } } count++; if(!c || c == '\n') break; w = FR_CharWidth(c); h = FR_CharHeight(c); if(' ' != c) { // A non-white-space character we have a glyph for. if(pass) { if(!noCharacter) { // The character itself. glColor4fv(sat->rgba); drawChar(c, cx, cy + yoff, font, ALIGN_TOPLEFT, DTF_NO_EFFECTS); } if(!noGlitter && glitter > 0) { // Do something flashy. Point2Raw origin; Size2Raw size; origin.x = cx; origin.y = cy + yoff; size.width = w; size.height = h; glColor4f(flashColor[CR], flashColor[CG], flashColor[CB], glitter * glitterMul); drawFlash(&origin, &size, true); } } else if(!noShadow) { Point2Raw origin; Size2Raw size; origin.x = cx; origin.y = cy + yoff; size.width = w; size.height = h; glColor4f(1, 1, 1, shadow * shadowMul); drawFlash(&origin, &size, false); } } cx += w + sat->tracking; } } // Restore previous GL-state. if(BitmapFont *bmapFont = font->maybeAs()) { if(bmapFont->textureGLName()) { glMatrixMode(GL_TEXTURE); glPopMatrix(); } } if(renderWireframe > 1) { /// @todo do not assume previous state. glEnable(GL_TEXTURE_2D); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } } static void drawChar(uchar ch, float x, float y, AbstractFont *font, int alignFlags, short /*textFlags*/) { if(alignFlags & ALIGN_RIGHT) { x -= font->glyphPosCoords(ch).width(); } else if(!(alignFlags & ALIGN_LEFT)) { x -= font->glyphPosCoords(ch).width() / 2; } int const ascent = font->ascent(); int const lineHeight = ascent? ascent : font->glyphPosCoords(ch).height(); if(alignFlags & ALIGN_BOTTOM) { y -= topToAscent(font) + lineHeight; } else if(!(alignFlags & ALIGN_TOP)) { y -= (topToAscent(font) + lineHeight) / 2; } glMatrixMode(GL_MODELVIEW); glTranslatef(x, y, 0); Rectanglei geometry = font->glyphPosCoords(ch); if(BitmapFont *bmapFont = font->maybeAs()) { /// @todo Filtering should be determined at a higher level. /// @todo We should not need to re-bind this texture here. GL_BindTextureUnmanaged(bmapFont->textureGLName(), gl::ClampToEdge, gl::ClampToEdge, filterUI? gl::Linear : gl::Nearest); geometry = geometry.expanded(bmapFont->textureMargin().toVector2i()); } else if(CompositeBitmapFont *compFont = font->maybeAs()) { GL_BindTexture(compFont->glyphTexture(ch)); geometry = geometry.expanded(compFont->glyphTextureBorder(ch)); } Vector2i coords[4] = { font->glyphTexCoords(ch).topLeft, font->glyphTexCoords(ch).topRight(), font->glyphTexCoords(ch).bottomRight, font->glyphTexCoords(ch).bottomLeft() }; GL_DrawRectWithCoords(geometry, coords); if(font->is()) { GL_SetNoTexture(); } glMatrixMode(GL_MODELVIEW); glTranslatef(-x, -y, 0); } static void drawFlash(Point2Raw const *origin, Size2Raw const *size, bool bright) { float fsize = 4.f + bright; float fw = fsize * size->width / 2.0f; float fh = fsize * size->height / 2.0f; int x, y, w, h; // Don't draw anything for very small letters. if(size->height <= 4) return; x = origin->x + (int) (size->width / 2.0f - fw / 2); y = origin->y + (int) (size->height / 2.0f - fh / 2); w = (int) fw; h = (int) fh; GL_BindTextureUnmanaged(GL_PrepareLSTexture(LST_DYNAMIC), gl::ClampToEdge, gl::ClampToEdge); GLState::current().setBlendFunc(bright? gl::SrcAlpha : gl::Zero, bright? gl::One : gl::OneMinusSrcAlpha) .apply(); glBegin(GL_QUADS); glTexCoord2f(0, 0); glVertex2f(x, y); glTexCoord2f(1, 0); glVertex2f(x + w, y); glTexCoord2f(1, 1); glVertex2f(x + w, y + h); glTexCoord2f(0, 1); glVertex2f(x, y + h); glEnd(); GLState::current().setBlendFunc(gl::SrcAlpha, gl::OneMinusSrcAlpha) .apply(); } /** * Expected: *
 *     " =  "
 * 
*/ static float parseFloat(char** str) { float value; char* end; *str = M_SkipWhite(*str); if(**str != '=') return 0; // Now I'm confused! *str = M_SkipWhite(*str + 1); value = (float) strtod(*str, &end); *str = end; return value; } /** * Expected: *
 *      " =  [|"][|"]"
 * 
*/ static dd_bool parseString(char** str, char* buf, size_t bufLen) { size_t len; char* end; if(!buf || bufLen == 0) return false; *str = M_SkipWhite(*str); if(**str != '=') return false; // Now I'm confused! // Skip over any leading whitespace. *str = M_SkipWhite(*str + 1); // Skip over any opening '"' character. if(**str == '"') (*str)++; // Find the end of the string. end = *str; while(*end && *end != '}' && *end != ',' && *end !='"') { end++; } len = end - *str; if(len != 0) { dd_snprintf(buf, MIN_OF(len+1, bufLen), "%s", *str); *str = end; } // Skip over any closing '"' character. if(**str == '"') (*str)++; return true; } static void parseParamaterBlock(char** strPtr, drawtextstate_t* state, int* numBreaks) { LOG_AS("parseParamaterBlock"); (*strPtr)++; while(*(*strPtr) && *(*strPtr) != '}') { (*strPtr) = M_SkipWhite((*strPtr)); // What do we have here? if(!strnicmp((*strPtr), "flash", 5)) { (*strPtr) += 5; state->typeIn = true; } else if(!strnicmp((*strPtr), "noflash", 7)) { (*strPtr) += 7; state->typeIn = false; } else if(!strnicmp((*strPtr), "case", 4)) { (*strPtr) += 4; state->caseScale = true; } else if(!strnicmp((*strPtr), "nocase", 6)) { (*strPtr) += 6; state->caseScale = false; } else if(!strnicmp((*strPtr), "ups", 3)) { (*strPtr) += 3; state->caseMod[1].scale = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "upo", 3)) { (*strPtr) += 3; state->caseMod[1].offset = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "los", 3)) { (*strPtr) += 3; state->caseMod[0].scale = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "loo", 3)) { (*strPtr) += 3; state->caseMod[0].offset = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "break", 5)) { (*strPtr) += 5; ++(*numBreaks); } else if(!strnicmp((*strPtr), "r", 1)) { (*strPtr)++; state->rgba[CR] = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "g", 1)) { (*strPtr)++; state->rgba[CG] = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "b", 1)) { (*strPtr)++; state->rgba[CB] = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "a", 1)) { (*strPtr)++; state->rgba[CA] = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "x", 1)) { (*strPtr)++; state->offX = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "y", 1)) { (*strPtr)++; state->offY = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "scalex", 6)) { (*strPtr) += 6; state->scaleX = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "scaley", 6)) { (*strPtr) += 6; state->scaleY = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "scale", 5)) { (*strPtr) += 5; state->scaleX = state->scaleY = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "angle", 5)) { (*strPtr) += 5; state->angle = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "glitter", 7)) { (*strPtr) += 7; state->glitterStrength = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "shadow", 6)) { (*strPtr) += 6; state->shadowStrength = parseFloat(&(*strPtr)); } else if(!strnicmp((*strPtr), "tracking", 8)) { (*strPtr) += 8; state->tracking = parseFloat(&(*strPtr)); } else { // Perhaps a font name? if(!strnicmp((*strPtr), "font", 4)) { char buf[80]; (*strPtr) += 4; if(parseString(&(*strPtr), buf, 80)) { try { state->fontNum = App_ResourceSystem().fontManifest(de::Uri(buf, RC_NULL)).uniqueId(); continue; } catch(ResourceSystem::MissingManifestError const &) {} } LOG_GL_WARNING("Unknown font '%s'") << *strPtr; continue; } // Unknown, skip it. if(*(*strPtr) != '}') { (*strPtr)++; } } } // Skip over the closing brace. if(*(*strPtr)) (*strPtr)++; } static void initDrawTextState(drawtextstate_t* state, short textFlags) { fr_state_attributes_t* sat = currentAttribs(); state->typeIn = (textFlags & DTF_NO_TYPEIN) == 0; state->fontNum = fr.fontNum; memcpy(state->rgba, sat->rgba, sizeof(state->rgba)); state->tracking = sat->tracking; state->glitterStrength = sat->glitterStrength; state->shadowStrength = sat->shadowStrength; state->shadowOffsetX = sat->shadowOffsetX; state->shadowOffsetY = sat->shadowOffsetY; state->leading = sat->leading; state->caseScale = sat->caseScale; state->scaleX = state->scaleY = 1; state->offX = state->offY = 0; state->angle = 0; state->typeIn = true; state->caseMod[0].scale = 1; state->caseMod[0].offset = 3; state->caseMod[1].scale = 1.25f; state->caseMod[1].offset = 0; state->lastLineHeight = FR_CharHeight('A') * state->scaleY * (1+state->leading); FR_PushAttrib(); } static char* enlargeTextBuffer(size_t lengthMinusTerminator) { if(lengthMinusTerminator <= FR_SMALL_TEXT_BUFFER_SIZE) { return smallTextBuffer; } if(largeTextBuffer == NULL || lengthMinusTerminator > largeTextBufferSize) { largeTextBufferSize = lengthMinusTerminator; largeTextBuffer = (char*)realloc(largeTextBuffer, largeTextBufferSize+1); if(largeTextBuffer == NULL) App_Error("FR_EnlargeTextBuffer: Failed on reallocation of %lu bytes.", (unsigned long)(lengthMinusTerminator+1)); } return largeTextBuffer; } static void freeTextBuffer(void) { if(largeTextBuffer == NULL) return; free(largeTextBuffer); largeTextBuffer = 0; largeTextBufferSize = 0; } #undef FR_TextWidth int FR_TextWidth(const char* string) { int w, maxWidth = -1; dd_bool skipping = false, escaped = false; const char* ch; size_t i, len; errorIfNotInited("FR_TextWidth"); if(!string || !string[0]) return 0; /// @todo All visual format parsing should be done in one place. w = 0; len = strlen(string); ch = string; for(i = 0; i < len; ++i, ch++) { unsigned char c = *ch; if(c == FR_FORMAT_ESCAPE_CHAR) { escaped = true; continue; } if(!escaped && c == '{') { skipping = true; } else if(skipping && c == '}') { skipping = false; continue; } if(skipping) continue; escaped = false; if(c == '\n') { if(w > maxWidth) maxWidth = w; w = 0; continue; } w += FR_CharWidth(c); if(i != len - 1) { w += FR_Tracking(); } else if(maxWidth == -1) { maxWidth = w; } } return maxWidth; } #undef FR_TextHeight int FR_TextHeight(const char* string) { int h, currentLineHeight; dd_bool skip = false; const char* ch; size_t i, len; if(!string || !string[0]) return 0; errorIfNotInited("FR_TextHeight"); currentLineHeight = 0; len = strlen(string); h = 0; ch = string; for(i = 0; i < len; ++i, ch++) { unsigned char c = *ch; int charHeight; if(c == '{') { skip = true; } else if(c == '}') { skip = false; continue; } if(skip) continue; if(c == '\n') { h += currentLineHeight == 0? (FR_CharHeight('A') * (1+FR_Leading())) : currentLineHeight; currentLineHeight = 0; continue; } charHeight = FR_CharHeight(c) * (1+FR_Leading()); if(charHeight > currentLineHeight) currentLineHeight = charHeight; } h += currentLineHeight; return h; } #undef FR_TextSize void FR_TextSize(Size2Raw* size, const char* text) { if(!size) return; size->width = FR_TextWidth(text); size->height = FR_TextHeight(text); } #undef FR_DrawText3 void FR_DrawText3(const char* text, const Point2Raw* _origin, int alignFlags, short _textFlags) { fontid_t origFont = FR_Font(); float cx, cy, extraScale; drawtextstate_t state; const char* fragment; int pass, curCase; Point2Raw origin; Size2Raw textSize; size_t charCount; float origColor[4]; char* str, *end; dd_bool escaped = false; errorIfNotInited("FR_DrawText"); if(!text || !text[0]) return; origin.x = _origin? _origin->x : 0; origin.y = _origin? _origin->y : 0; _textFlags &= ~(DTF_INTERNAL_MASK); // If we aren't aligning to top-left we need to know the dimensions. if(alignFlags & ALIGN_RIGHT) FR_TextSize(&textSize, text); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // We need to change the current color, so remember for restore. glGetFloatv(GL_CURRENT_COLOR, origColor); for(pass = ((_textFlags & DTF_NO_SHADOW) != 0? 1 : 0); pass < ((_textFlags & DTF_NO_GLITTER) != 0? 2 : 3); ++pass) { short textFlags = 0; // Configure the next pass. cx = (float) origin.x; cy = (float) origin.y; curCase = -1; charCount = 0; switch(pass) { case 0: textFlags = _textFlags | (DTF_NO_GLITTER|DTF_NO_CHARACTER); break; case 1: textFlags = _textFlags | (DTF_NO_SHADOW |DTF_NO_GLITTER); break; case 2: textFlags = _textFlags | (DTF_NO_SHADOW |DTF_NO_CHARACTER); break; } // Apply defaults. initDrawTextState(&state, textFlags); str = (char*)text; while(*str) { if(*str == FR_FORMAT_ESCAPE_CHAR) { escaped = true; ++str; continue; } if(!escaped && *str == '{') // Paramaters included? { fontid_t lastFont = state.fontNum; int lastTracking = state.tracking; float lastLeading = state.leading; float lastShadowStrength = state.shadowStrength; float lastGlitterStrength = state.glitterStrength; dd_bool lastCaseScale = state.caseScale; float lastRGBA[4]; int numBreaks = 0; lastRGBA[CR] = state.rgba[CR]; lastRGBA[CG] = state.rgba[CG]; lastRGBA[CB] = state.rgba[CB]; lastRGBA[CA] = state.rgba[CA]; parseParamaterBlock(&str, &state, &numBreaks); if(numBreaks != 0) { do { cx = (float) origin.x; cy += state.lastLineHeight * (1+lastLeading); } while(--numBreaks > 0); } if(state.fontNum != lastFont) FR_SetFont(state.fontNum); if(state.tracking != lastTracking) FR_SetTracking(state.tracking); if(state.leading != lastLeading) FR_SetLeading(state.leading); if(state.rgba[CR] != lastRGBA[CR] || state.rgba[CG] != lastRGBA[CG] || state.rgba[CB] != lastRGBA[CB] || state.rgba[CA] != lastRGBA[CA]) FR_SetColorAndAlphav(state.rgba); if(state.shadowStrength != lastShadowStrength) FR_SetShadowStrength(state.shadowStrength); if(state.glitterStrength != lastGlitterStrength) FR_SetGlitterStrength(state.glitterStrength); if(state.caseScale != lastCaseScale) FR_SetCaseScale(state.caseScale); } for(end = str; *end && *end != FR_FORMAT_ESCAPE_CHAR && (escaped || *end != '{');) { int newlines = 0, fragmentAlignFlags; float alignx = 0; // Find the end of the next fragment. if(FR_CaseScale()) { curCase = -1; // Select a substring with characters of the same case (or whitespace). for(; *end && *end != FR_FORMAT_ESCAPE_CHAR && (escaped || *end != '{') && *end != '\n'; end++) { escaped = false; // We can skip whitespace. if(isspace(*end)) continue; if(curCase < 0) curCase = (isupper(*end) != 0); else if(curCase != (isupper(*end) != 0)) break; } } else { curCase = 0; for(; *end && *end != FR_FORMAT_ESCAPE_CHAR && (escaped || *end != '{') && *end != '\n'; end++) { escaped = false; } } // No longer escaped. escaped = false; { char* buffer = enlargeTextBuffer(end - str); memcpy(buffer, str, end - str); buffer[end - str] = '\0'; fragment = buffer; } while(*end == '\n') { newlines++; end++; } // Continue from here. str = end; if(!(alignFlags & (ALIGN_LEFT|ALIGN_RIGHT))) { fragmentAlignFlags = alignFlags; } else { // We'll take care of horizontal positioning of the fragment so align left. fragmentAlignFlags = (alignFlags & ~(ALIGN_RIGHT)) | ALIGN_LEFT; if(alignFlags & ALIGN_RIGHT) alignx = -textSize.width * state.scaleX; } // Setup the scaling. glMatrixMode(GL_MODELVIEW); glPushMatrix(); // Rotate. if(state.angle != 0) { // The origin is the specified (x,y) for the patch. // We'll undo the aspect ratio (otherwise the result would be skewed). /// @todo Do not assume the aspect ratio and therefore whether // correction is even needed. glTranslatef((float)origin.x, (float)origin.y, 0); glScalef(1, 200.0f / 240.0f, 1); glRotatef(state.angle, 0, 0, 1); glScalef(1, 240.0f / 200.0f, 1); glTranslatef(-(float)origin.x, -(float)origin.y, 0); } glTranslatef(cx + state.offX + alignx, cy + state.offY + (FR_CaseScale() ? state.caseMod[curCase].offset : 0), 0); extraScale = (FR_CaseScale() ? state.caseMod[curCase].scale : 1); glScalef(state.scaleX, state.scaleY * extraScale, 1); // Draw it. if(fr.fontNum) { textFragmentDrawer(fragment, 0, 0, fragmentAlignFlags, textFlags, state.typeIn ? (int) charCount : DEFAULT_INITIALCOUNT); } charCount += strlen(fragment); // Advance the current position? if(newlines == 0) { cx += ((float) textFragmentWidth(fragment) + currentAttribs()->tracking) * state.scaleX; } else { if(strlen(fragment) > 0) state.lastLineHeight = textFragmentHeight(fragment); cx = (float) origin.x; cy += newlines * (float) state.lastLineHeight * (1+FR_Leading()); } glMatrixMode(GL_MODELVIEW); glPopMatrix(); } } FR_PopAttrib(); } freeTextBuffer(); FR_SetFont(origFont); glColor4fv(origColor); } #undef FR_DrawText2 void FR_DrawText2(const char* text, const Point2Raw* origin, int alignFlags) { FR_DrawText3(text, origin, alignFlags, DEFAULT_DRAWFLAGS); } #undef FR_DrawText void FR_DrawText(const char* text, const Point2Raw* origin) { FR_DrawText2(text, origin, DEFAULT_ALIGNFLAGS); } #undef FR_DrawTextXY3 void FR_DrawTextXY3(const char* text, int x, int y, int alignFlags, short flags) { Point2Raw origin; origin.x = x; origin.y = y; FR_DrawText3(text, &origin, alignFlags, flags); } #undef FR_DrawTextXY2 void FR_DrawTextXY2(const char* text, int x, int y, int alignFlags) { FR_DrawTextXY3(text, x, y, alignFlags, DEFAULT_DRAWFLAGS); } #undef FR_DrawTextXY void FR_DrawTextXY(const char* text, int x, int y) { FR_DrawTextXY2(text, x, y, DEFAULT_ALIGNFLAGS); } #undef FR_DrawChar3 void FR_DrawChar3(unsigned char ch, const Point2Raw* origin, int alignFlags, short textFlags) { char str[2]; str[0] = ch; str[1] = '\0'; FR_DrawText3(str, origin, alignFlags, textFlags); } #undef FR_DrawChar2 void FR_DrawChar2(unsigned char ch, const Point2Raw* origin, int alignFlags) { FR_DrawChar3(ch, origin, alignFlags, DEFAULT_DRAWFLAGS); } #undef FR_DrawChar void FR_DrawChar(unsigned char ch, const Point2Raw* origin) { FR_DrawChar2(ch, origin, DEFAULT_ALIGNFLAGS); } #undef FR_DrawCharXY3 void FR_DrawCharXY3(unsigned char ch, int x, int y, int alignFlags, short textFlags) { Point2Raw origin; origin.x = x; origin.y = y; FR_DrawChar3(ch, &origin, alignFlags, textFlags); } #undef FR_DrawCharXY2 void FR_DrawCharXY2(unsigned char ch, int x, int y, int alignFlags) { FR_DrawCharXY3(ch, x, y, alignFlags, DEFAULT_DRAWFLAGS); } #undef FR_DrawCharXY void FR_DrawCharXY(unsigned char ch, int x, int y) { FR_DrawCharXY2(ch, x, y, DEFAULT_ALIGNFLAGS); } void FR_Init(void) { // No reinitializations... if(inited) return; if(isDedicated) return; inited = true; fr.fontNum = 0; FR_LoadDefaultAttrib(); typeInTime = 0; } #undef Fonts_ResolveUri DENG_EXTERN_C fontid_t Fonts_ResolveUri(uri_s const *uri) { if(!uri) return NOFONTID; try { return App_ResourceSystem().fontManifest(*reinterpret_cast(uri)).uniqueId(); } catch(ResourceSystem::MissingManifestError const &) {} return NOFONTID; } DENG_DECLARE_API(FR) = { { DE_API_FONT_RENDER }, Fonts_ResolveUri, FR_Font, FR_SetFont, FR_PushAttrib, FR_PopAttrib, FR_LoadDefaultAttrib, FR_Leading, FR_SetLeading, FR_Tracking, FR_SetTracking, FR_ColorAndAlpha, FR_SetColor, FR_SetColorv, FR_SetColorAndAlpha, FR_SetColorAndAlphav, FR_ColorRed, FR_SetColorRed, FR_ColorGreen, FR_SetColorGreen, FR_ColorBlue, FR_SetColorBlue, FR_Alpha, FR_SetAlpha, FR_ShadowOffset, FR_SetShadowOffset, FR_ShadowStrength, FR_SetShadowStrength, FR_GlitterStrength, FR_SetGlitterStrength, FR_CaseScale, FR_SetCaseScale, FR_DrawText, FR_DrawText2, FR_DrawText3, FR_DrawTextXY3, FR_DrawTextXY2, FR_DrawTextXY, FR_TextSize, FR_TextWidth, FR_TextHeight, FR_DrawChar3, FR_DrawChar2, FR_DrawChar, FR_DrawCharXY3, FR_DrawCharXY2, FR_DrawCharXY, FR_CharSize, FR_CharWidth, FR_CharHeight, FR_ResetTypeinTimer }; doomsday-stable-1.15.7/doomsday/client/src/render/consoleeffect.cpp0000664000175000017500000000333012641367670024661 0ustar jaakkojaakko/** @file consoleeffect.cpp * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "render/consoleeffect.h" #include "render/viewports.h" #include "clientapp.h" using namespace de; DENG2_PIMPL_NOREF(ConsoleEffect) { int console; bool inited; Instance() : console(0), inited(false) {} }; ConsoleEffect::ConsoleEffect(int console) : d(new Instance) { d->console = console; } ConsoleEffect::~ConsoleEffect() {} int ConsoleEffect::console() const { return d->console; } Rectanglei const &ConsoleEffect::viewRect() const { viewdata_t const *vd = R_ViewData(d->console); return vd->window; } bool ConsoleEffect::isInited() const { return d->inited; } GLShaderBank &ConsoleEffect::shaders() const { return ClientApp::renderSystem().shaders(); } void ConsoleEffect::glInit() { d->inited = true; } void ConsoleEffect::glDeinit() { d->inited = false; } void ConsoleEffect::beginFrame() {} void ConsoleEffect::draw() {} void ConsoleEffect::endFrame() {} doomsday-stable-1.15.7/doomsday/client/src/render/rend_fakeradio.cpp0000664000175000017500000014535412641367670025014 0ustar jaakkojaakko/** @file rend_fakeradio.cpp Faked Radiosity Lighting. * * @authors Copyright © 2004-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "de_console.h" #include "de_render.h" #include "de_graphics.h" #include "de_misc.h" #include "de_play.h" #include "clientapp.h" #include "gl/gl_texmanager.h" #include "gl/sys_opengl.h" #include "MaterialVariantSpec" #include "Face" #include "ConvexSubspace" #include "SectorCluster" #include "WallEdge" #include "world/map.h" #include "world/maputil.h" #include "world/lineowner.h" #include "render/rendpoly.h" #include "render/shadowedge.h" #include "render/rend_fakeradio.h" #include #include #include #include using namespace de; #define MIN_OPEN (.1f) #define MINDIFF (8) // min plane height difference (world units) #define INDIFF (8) // max plane height for indifference offset #define BOTTOM (0) #define TOP (1) typedef struct edge_s { dd_bool done; Line *line; Sector *sector; float length; binangle_t diff; } edge_t; static void scanEdges(shadowcorner_t topCorners[2], shadowcorner_t bottomCorners[2], shadowcorner_t sideCorners[2], edgespan_t spans[2], LineSide const &side); int rendFakeRadio = true; ///< cvar float rendFakeRadioDarkness = 1.2f; ///< cvar static byte devFakeRadioUpdate = true; ///< cvar void Rend_RadioRegister() { C_VAR_INT ("rend-fakeradio", &rendFakeRadio, 0, 0, 2); C_VAR_FLOAT("rend-fakeradio-darkness", &rendFakeRadioDarkness, 0, 0, 2); C_VAR_BYTE ("rend-dev-fakeradio-update", &devFakeRadioUpdate, CVF_NO_ARCHIVE, 0, 1); } float Rend_RadioCalcShadowDarkness(float lightLevel) { lightLevel += Rend_LightAdaptationDelta(lightLevel); return (0.6f - lightLevel * 0.4f) * 0.65f * rendFakeRadioDarkness; } void Rend_RadioUpdateForLineSide(LineSide &side) { // Disabled completely? if(!rendFakeRadio || levelFullBright) return; // Updates are disabled? if(!devFakeRadioUpdate) return; // Sides without sectors don't need updating. $degenleaf if(!side.hasSector()) return; // Have already determined the shadow properties on this side? LineSideRadioData &frData = Rend_RadioDataForLineSide(side); if(frData.updateCount == R_FrameCount()) return; // Not yet - Calculate now. for(uint i = 0; i < 2; ++i) { frData.spans[i].length = side.line().length(); frData.spans[i].shift = 0; } scanEdges(frData.topCorners, frData.bottomCorners, frData.sideCorners, frData.spans, side); frData.updateCount = R_FrameCount(); // Mark as done. } /** * Set the vertex colors in the rendpoly. */ static void setRendpolyColor(Vector4f *colorCoords, uint num, float darkness) { DENG_ASSERT(colorCoords != 0); // Shadows are black. Vector4f const shadowColor(0, 0, 0, de::clamp(0.f, darkness, 1.f)); Vector4f *colorIt = colorCoords; for(uint i = 0; i < num; ++i, colorIt++) { *colorIt = shadowColor; } } /// @return @c true, if there is open space in the sector. static inline bool isSectorOpen(Sector const *sector) { return (sector && sector->ceiling().height() > sector->floor().height()); } /** * Set the rendpoly's X offset and texture size. * * @param lineLength If negative; implies that the texture is flipped horizontally. * @param segOffset Offset to the start of the segment. */ static inline float calcTexCoordX(float lineLength, float segOffset) { if(lineLength > 0) return segOffset; return lineLength + segOffset; } /** * Set the rendpoly's Y offset and texture size. * * @param z Z height of the vertex. * @param bottom Z height of the bottom of the wall section. * @param top Z height of the top of the wall section. * @param texHeight If negative; implies that the texture is flipped vertically. */ static inline float calcTexCoordY(float z, float bottom, float top, float texHeight) { if(texHeight > 0) return top - z; return bottom - z; } /// @todo fixme: Should be rewritten to work at half-edge level. /// @todo fixme: Should use the visual plane heights of sector clusters. static void scanNeighbor(bool scanTop, LineSide const &side, edge_t *edge, bool toLeft) { int const SEP = 10; ClockDirection const direction = toLeft? Clockwise : Anticlockwise; Line *iter; binangle_t diff = 0; coord_t lengthDelta = 0, gap = 0; coord_t iFFloor, iFCeil; coord_t iBFloor, iBCeil; int scanSecSide = side.sideId(); Sector const *startSector = side.sectorPtr(); Sector const *scanSector; bool stopScan = false; bool closed; coord_t fFloor = startSector->floor().heightSmoothed(); coord_t fCeil = startSector->ceiling().heightSmoothed(); // Retrieve the start owner node. LineOwner *own = side.line().vertexOwner(side.vertex((int)!toLeft)); do { // Select the next line. diff = (direction == Clockwise? own->angle() : own->prev().angle()); iter = &own->navigate(direction).line(); scanSecSide = (iter->hasFrontSector() && iter->frontSectorPtr() == startSector)? Line::Back : Line::Front; // Step over selfreferencing lines. while((!iter->hasFrontSector() && !iter->hasBackSector()) || // $degenleaf iter->isSelfReferencing()) { own = &own->navigate(direction); diff += (direction == Clockwise? own->angle() : own->prev().angle()); iter = &own->navigate(direction).line(); scanSecSide = (iter->frontSectorPtr() == startSector); } // Determine the relative backsector. if(iter->side(scanSecSide).hasSector()) scanSector = iter->side(scanSecSide).sectorPtr(); else scanSector = 0; // Pick plane heights for relative offset comparison. if(!stopScan) { iFFloor = iter->frontSector().floor().heightSmoothed(); iFCeil = iter->frontSector().ceiling().heightSmoothed(); if(iter->hasBackSector()) { iBFloor = iter->backSector().floor().heightSmoothed(); iBCeil = iter->backSector().ceiling().heightSmoothed(); } else iBFloor = iBCeil = 0; } lengthDelta = 0; if(!stopScan) { // This line will attribute to this segment's shadow edge. // Store identity for later use. edge->diff = diff; edge->line = iter; edge->sector = const_cast(scanSector); closed = false; if(side.isFront() && iter->hasBackSector()) { if(scanTop) { if(iBFloor >= fCeil) closed = true; // Compared to "this" sector anyway } else { if(iBCeil <= fFloor) closed = true; // Compared to "this" sector anyway } } // Does this line's length contribute to the alignment of the // texture on the segment shadow edge being rendered? if(scanTop) { if(iter->hasBackSector() && ((side.isFront() && iter->backSectorPtr() == side.line().frontSectorPtr() && iFCeil >= fCeil) || (side.isBack() && iter->backSectorPtr() == side.line().backSectorPtr() && iFCeil >= fCeil) || (side.isFront() && closed == false && iter->backSectorPtr() != side.line().frontSectorPtr() && iBCeil >= fCeil && isSectorOpen(iter->backSectorPtr())))) { gap += iter->length(); // Should we just mark it done instead? } else { edge->length += iter->length() + gap; gap = 0; } } else { if(iter->hasBackSector() && ((side.isFront() && iter->backSectorPtr() == side.line().frontSectorPtr() && iFFloor <= fFloor) || (side.isBack() && iter->backSectorPtr() == side.line().backSectorPtr() && iFFloor <= fFloor) || (side.isFront() && closed == false && iter->backSectorPtr() != side.line().frontSectorPtr() && iBFloor <= fFloor && isSectorOpen(iter->backSectorPtr())))) { gap += iter->length(); // Should we just mark it done instead? } else { lengthDelta = iter->length() + gap; gap = 0; } } } // Time to stop? if(iter == &side.line()) { stopScan = true; } else { // Is this line coalignable? if(!(diff >= BANG_180 - SEP && diff <= BANG_180 + SEP)) { stopScan = true; // no. } else if(scanSector) { // Perhaps its a closed edge? if(!isSectorOpen(scanSector)) { stopScan = true; } else { // A height difference from the start sector? if(scanTop) { if(scanSector->ceiling().heightSmoothed() != fCeil && scanSector->floor().heightSmoothed() < startSector->ceiling().heightSmoothed()) stopScan = true; } else { if(scanSector->floor().heightSmoothed() != fFloor && scanSector->ceiling().heightSmoothed() > startSector->floor().heightSmoothed()) stopScan = true; } } } } // Swap to the iter line's owner node (i.e: around the corner)? if(!stopScan) { // Around the corner. if(&own->navigate(direction) == iter->v2Owner()) own = iter->v1Owner(); else if(&own->navigate(direction) == iter->v1Owner()) own = iter->v2Owner(); // Skip into the back neighbor sector of the iter line if // heights are within accepted range. if(scanSector && side.back().hasSector() && scanSector != side.back().sectorPtr() && ((scanTop && scanSector->ceiling().heightSmoothed() == startSector->ceiling().heightSmoothed()) || (!scanTop && scanSector->floor().heightSmoothed() == startSector->floor().heightSmoothed()))) { // If the map is formed correctly, we should find a back // neighbor attached to this line. However, if this is not // the case and a line which SHOULD be two sided isn't, we // need to check whether there is a valid neighbor. Line *backNeighbor = R_FindLineNeighbor(startSector, iter, own, !toLeft); if(backNeighbor && backNeighbor != iter) { // Into the back neighbor sector. own = &own->navigate(direction); startSector = scanSector; } } // The last line was co-alignable so apply any length delta. edge->length += lengthDelta; } } while(!stopScan); // Now we've found the furthest coalignable neighbor, select the back // neighbor if present for "edge open-ness" comparison. if(edge->sector) // the back sector of the coalignable neighbor. { // Since we have the details of the backsector already, simply // get the next neighbor (it IS the backneighbor). edge->line = R_FindLineNeighbor(edge->sector, edge->line, edge->line->vertexOwner(int(edge->line->hasBackSector() && edge->line->backSectorPtr() == edge->sector) ^ (int)!toLeft), !toLeft, &edge->diff); } } /// @todo fixme: Should use the visual plane heights of sector clusters. static void scanNeighbors(shadowcorner_t top[2], shadowcorner_t bottom[2], LineSide const &side, edgespan_t spans[2], bool toLeft) { if(side.line().isSelfReferencing()) return; coord_t fFloor = side.sector().floor().heightSmoothed(); coord_t fCeil = side.sector().ceiling().heightSmoothed(); edge_t edges[2]; // {bottom, top} std::memset(edges, 0, sizeof(edges)); scanNeighbor(false, side, &edges[0], toLeft); scanNeighbor(true, side, &edges[1], toLeft); for(uint i = 0; i < 2; ++i) { shadowcorner_t *corner = (i == 0 ? &bottom[(int)!toLeft] : &top[(int)!toLeft]); edge_t *edge = &edges[i]; edgespan_t *span = &spans[i]; // Increment the apparent line length/offset. span->length += edge->length; if(toLeft) span->shift += edge->length; // Compare the relative angle difference of this edge to determine // an "open-ness" factor. if(edge->line && edge->line != &side.line()) { if(edge->diff > BANG_180) { // The corner between the walls faces outwards. corner->corner = -1; } else if(edge->diff == BANG_180) { // Perfectly coaligned? Great. corner->corner = 0; } else if(edge->diff < BANG_45 / 5) { // The difference is too small, there won't be a shadow. corner->corner = 0; } // 90 degrees is the largest effective difference. else if(edge->diff > BANG_90) { corner->corner = (float) BANG_90 / edge->diff; } else { corner->corner = (float) edge->diff / BANG_90; } } else { // Consider it coaligned. corner->corner = 0; } // Determine relative height offsets (affects shadow map selection). if(edge->sector) { corner->proximity = edge->sector; if(i == 0) // Floor. { corner->pOffset = corner->proximity->floor().heightSmoothed() - fFloor; corner->pHeight = corner->proximity->floor().heightSmoothed(); } else // Ceiling. { corner->pOffset = corner->proximity->ceiling().heightSmoothed() - fCeil; corner->pHeight = corner->proximity->ceiling().heightSmoothed(); } } else { corner->proximity = NULL; corner->pOffset = 0; corner->pHeight = 0; } } } /** * To determine the dimensions of a shadow, we'll need to scan edges. Edges * are composed of aligned lines. It's important to note that the scanning * is done separately for the top/bottom edges (both in the left and right * direction) and the left/right edges. * * The length of the top/bottom edges are returned in the array 'spans'. * * This may look like a complicated operation (performed for all wall polys) * but in most cases this won't take long. Aligned neighbours are relatively * rare. */ static void scanEdges(shadowcorner_t topCorners[2], shadowcorner_t bottomCorners[2], shadowcorner_t sideCorners[2], edgespan_t spans[2], LineSide const &side) { int const lineSideId = side.sideId(); std::memset(sideCorners, 0, sizeof(shadowcorner_t) * 2); // Find the sidecorners first: left and right neighbour. for(int i = 0; i < 2; ++i) { binangle_t diff = 0; LineOwner *vo = side.line().vertexOwner(i ^ lineSideId); Line *other = R_FindSolidLineNeighbor(side.sectorPtr(), &side.line(), vo, CPP_BOOL(i), &diff); if(other && other != &side.line()) { if(diff > BANG_180) { // The corner between the walls faces outwards. sideCorners[i].corner = -1; } else if(diff == BANG_180) { sideCorners[i].corner = 0; } else if(diff < BANG_45 / 5) { // The difference is too small, there won't be a shadow. sideCorners[i].corner = 0; } else if(diff > BANG_90) { // 90 degrees is the largest effective difference. sideCorners[i].corner = (float) BANG_90 / diff; } else { sideCorners[i].corner = (float) diff / BANG_90; } } else { sideCorners[i].corner = 0; } scanNeighbors(topCorners, bottomCorners, side, spans, !i); } } typedef struct { lightingtexid_t texture; bool horizontal; float shadowMul; float shadowDark; Vector2f texOrigin; Vector2f texDimensions; float sectionWidth; } rendershadowseg_params_t; static void setTopShadowParams(rendershadowseg_params_t *p, float shadowSize, float shadowDark, coord_t top, coord_t xOffset, coord_t sectionWidth, coord_t fFloor, coord_t fCeil, LineSideRadioData const &frData) { p->shadowDark = shadowDark; p->shadowMul = 1; p->horizontal = false; p->texDimensions.y = shadowSize; p->texOrigin.y = calcTexCoordY(top, fFloor, fCeil, p->texDimensions.y); p->sectionWidth = sectionWidth; p->texture = LST_RADIO_OO; // Corners without a neighbor back sector if(frData.sideCorners[0].corner == -1 || frData.sideCorners[1].corner == -1) { // At least one corner faces outwards p->texture = LST_RADIO_OO; p->texDimensions.x = frData.spans[TOP].length; p->texOrigin.x = calcTexCoordX(frData.spans[TOP].length, frData.spans[TOP].shift + xOffset); if((frData.sideCorners[0].corner == -1 && frData.sideCorners[1].corner == -1) || (frData.topCorners[0].corner == -1 && frData.topCorners[1].corner == -1)) { // Both corners face outwards p->texture = LST_RADIO_OO;//CC; } else if(frData.sideCorners[1].corner == -1) { // right corner faces outwards if(-frData.topCorners[0].pOffset < 0 && frData.bottomCorners[0].pHeight < fCeil) { // Must flip horizontally! p->texDimensions.x = -frData.spans[TOP].length; p->texOrigin.x = calcTexCoordX(-frData.spans[TOP].length, frData.spans[TOP].shift + xOffset); p->texture = LST_RADIO_OE; } } else { // left corner faces outwards if(-frData.topCorners[1].pOffset < 0 && frData.bottomCorners[1].pHeight < fCeil) { p->texture = LST_RADIO_OE; } } } else { // Corners WITH a neighbor back sector p->texDimensions.x = frData.spans[TOP].length; p->texOrigin.x = calcTexCoordX(frData.spans[TOP].length, frData.spans[TOP].shift + xOffset); if(frData.topCorners[0].corner == -1 && frData.topCorners[1].corner == -1) { // Both corners face outwards p->texture = LST_RADIO_OO;//CC; } else if(frData.topCorners[1].corner == -1 && frData.topCorners[0].corner > MIN_OPEN) { // Right corner faces outwards p->texture = LST_RADIO_OO; } else if(frData.topCorners[0].corner == -1 && frData.topCorners[1].corner > MIN_OPEN) { // Left corner faces outwards p->texture = LST_RADIO_OO; } // Open edges else if(frData.topCorners[0].corner <= MIN_OPEN && frData.topCorners[1].corner <= MIN_OPEN) { // Both edges are open p->texture = LST_RADIO_OO; if(frData.topCorners[0].proximity && frData.topCorners[1].proximity) { if(-frData.topCorners[0].pOffset >= 0 && -frData.topCorners[1].pOffset < 0) { p->texture = LST_RADIO_CO; // The shadow can't go over the higher edge. if(shadowSize > -frData.topCorners[0].pOffset) { if(-frData.topCorners[0].pOffset < INDIFF) { p->texture = LST_RADIO_OE; } else { p->texDimensions.y = -frData.topCorners[0].pOffset; p->texOrigin.y = calcTexCoordY(top, fFloor, fCeil, p->texDimensions.y); } } } else if(-frData.topCorners[0].pOffset < 0 && -frData.topCorners[1].pOffset >= 0) { // Must flip horizontally! p->texture = LST_RADIO_CO; p->texDimensions.x = -frData.spans[TOP].length; p->texOrigin.x = calcTexCoordX(-frData.spans[TOP].length, frData.spans[TOP].shift + xOffset); // The shadow can't go over the higher edge. if(shadowSize > -frData.topCorners[1].pOffset) { if(-frData.topCorners[1].pOffset < INDIFF) { p->texture = LST_RADIO_OE; } else { p->texDimensions.y = -frData.topCorners[1].pOffset; p->texOrigin.y = calcTexCoordY(top, fFloor, fCeil, p->texDimensions.y); } } } } else { if(-frData.topCorners[0].pOffset < -MINDIFF) { // Must flip horizontally! p->texture = LST_RADIO_OE; p->texDimensions.x = -frData.spans[BOTTOM].length; p->texOrigin.x = calcTexCoordX(-frData.spans[BOTTOM].length, frData.spans[BOTTOM].shift + xOffset); } else if(-frData.topCorners[1].pOffset < -MINDIFF) { p->texture = LST_RADIO_OE; } } } else if(frData.topCorners[0].corner <= MIN_OPEN) { if(-frData.topCorners[0].pOffset < 0) p->texture = LST_RADIO_CO; else p->texture = LST_RADIO_OO; // Must flip horizontally! p->texDimensions.x = -frData.spans[TOP].length; p->texOrigin.x = calcTexCoordX(-frData.spans[TOP].length, frData.spans[TOP].shift + xOffset); } else if(frData.topCorners[1].corner <= MIN_OPEN) { if(-frData.topCorners[1].pOffset < 0) p->texture = LST_RADIO_CO; else p->texture = LST_RADIO_OO; } else // C/C ??? { p->texture = LST_RADIO_OO; } } } static void setBottomShadowParams(rendershadowseg_params_t *p, float shadowSize, float shadowDark, coord_t top, coord_t xOffset, coord_t sectionWidth, coord_t fFloor, coord_t fCeil, LineSideRadioData const &frData) { p->shadowDark = shadowDark; p->shadowMul = 1; p->horizontal = false; p->texDimensions.y = -shadowSize; p->texOrigin.y = calcTexCoordY(top, fFloor, fCeil, p->texDimensions.y); p->sectionWidth = sectionWidth; p->texture = LST_RADIO_OO; // Corners without a neighbor back sector if(frData.sideCorners[0].corner == -1 || frData.sideCorners[1].corner == -1) { // At least one corner faces outwards p->texture = LST_RADIO_OO; p->texDimensions.x = frData.spans[BOTTOM].length; p->texOrigin.x = calcTexCoordX(frData.spans[BOTTOM].length, frData.spans[BOTTOM].shift + xOffset); if((frData.sideCorners[0].corner == -1 && frData.sideCorners[1].corner == -1) || (frData.bottomCorners[0].corner == -1 && frData.bottomCorners[1].corner == -1) ) { // Both corners face outwards p->texture = LST_RADIO_OO;//CC; } else if(frData.sideCorners[1].corner == -1) // right corner faces outwards { if(frData.bottomCorners[0].pOffset < 0 && frData.topCorners[0].pHeight > fFloor) { // Must flip horizontally! p->texDimensions.x = -frData.spans[BOTTOM].length; p->texOrigin.x = calcTexCoordX(-frData.spans[BOTTOM].length, frData.spans[BOTTOM].shift + xOffset); p->texture = LST_RADIO_OE; } } else { // left corner faces outwards if(frData.bottomCorners[1].pOffset < 0 && frData.topCorners[1].pHeight > fFloor) { p->texture = LST_RADIO_OE; } } } else { // Corners WITH a neighbor back sector p->texDimensions.x = frData.spans[BOTTOM].length; p->texOrigin.x = calcTexCoordX(frData.spans[BOTTOM].length, frData.spans[BOTTOM].shift + xOffset); if(frData.bottomCorners[0].corner == -1 && frData.bottomCorners[1].corner == -1) { // Both corners face outwards p->texture = LST_RADIO_OO;//CC; } else if(frData.bottomCorners[1].corner == -1 && frData.bottomCorners[0].corner > MIN_OPEN) { // Right corner faces outwards p->texture = LST_RADIO_OO; } else if(frData.bottomCorners[0].corner == -1 && frData.bottomCorners[1].corner > MIN_OPEN) { // Left corner faces outwards p->texture = LST_RADIO_OO; } // Open edges else if(frData.bottomCorners[0].corner <= MIN_OPEN && frData.bottomCorners[1].corner <= MIN_OPEN) { // Both edges are open p->texture = LST_RADIO_OO; if(frData.bottomCorners[0].proximity && frData.bottomCorners[1].proximity) { if(frData.bottomCorners[0].pOffset >= 0 && frData.bottomCorners[1].pOffset < 0) { p->texture = LST_RADIO_CO; // The shadow can't go over the higher edge. if(shadowSize > frData.bottomCorners[0].pOffset) { if(frData.bottomCorners[0].pOffset < INDIFF) { p->texture = LST_RADIO_OE; } else { p->texDimensions.y = -frData.bottomCorners[0].pOffset; p->texOrigin.y = calcTexCoordY(top, fFloor, fCeil, p->texDimensions.y); } } } else if(frData.bottomCorners[0].pOffset < 0 && frData.bottomCorners[1].pOffset >= 0) { // Must flip horizontally! p->texture = LST_RADIO_CO; p->texDimensions.x = -frData.spans[BOTTOM].length; p->texOrigin.x = calcTexCoordX(-frData.spans[BOTTOM].length, frData.spans[BOTTOM].shift + xOffset); if(shadowSize > frData.bottomCorners[1].pOffset) { if(frData.bottomCorners[1].pOffset < INDIFF) { p->texture = LST_RADIO_OE; } else { p->texDimensions.y = -frData.bottomCorners[1].pOffset; p->texOrigin.y = calcTexCoordY(top, fFloor, fCeil, p->texDimensions.y); } } } } else { if(frData.bottomCorners[0].pOffset < -MINDIFF) { // Must flip horizontally! p->texture = LST_RADIO_OE; p->texDimensions.x = -frData.spans[BOTTOM].length; p->texOrigin.x = calcTexCoordX(-frData.spans[BOTTOM].length, frData.spans[BOTTOM].shift + xOffset); } else if(frData.bottomCorners[1].pOffset < -MINDIFF) { p->texture = LST_RADIO_OE; } } } else if(frData.bottomCorners[0].corner <= MIN_OPEN) // Right Corner is Closed { if(frData.bottomCorners[0].pOffset < 0) p->texture = LST_RADIO_CO; else p->texture = LST_RADIO_OO; // Must flip horizontally! p->texDimensions.x = -frData.spans[BOTTOM].length; p->texOrigin.x = calcTexCoordX(-frData.spans[BOTTOM].length, frData.spans[BOTTOM].shift + xOffset); } else if(frData.bottomCorners[1].corner <= MIN_OPEN) // Left Corner is closed { if(frData.bottomCorners[1].pOffset < 0) p->texture = LST_RADIO_CO; else p->texture = LST_RADIO_OO; } else // C/C ??? { p->texture = LST_RADIO_OO; } } } static void setSideShadowParams(rendershadowseg_params_t *p, float shadowSize, float shadowDark, coord_t bottom, coord_t top, int rightSide, bool haveBottomShadower, bool haveTopShadower, coord_t xOffset, coord_t sectionWidth, coord_t fFloor, coord_t fCeil, bool hasBackSector, coord_t bFloor, coord_t bCeil, coord_t lineLength, LineSideRadioData const &frData) { p->shadowDark = shadowDark; p->shadowMul = frData.sideCorners[rightSide? 1 : 0].corner * .8f; p->shadowMul *= p->shadowMul * p->shadowMul; p->horizontal = true; p->texOrigin.y = bottom - fFloor; p->texDimensions.y = fCeil - fFloor; p->sectionWidth = sectionWidth; if(rightSide) { // Right shadow. p->texOrigin.x = -lineLength + xOffset; // Make sure the shadow isn't too big if(shadowSize > lineLength) { if(frData.sideCorners[0].corner <= MIN_OPEN) p->texDimensions.x = -lineLength; else p->texDimensions.x = -(lineLength / 2); } else { p->texDimensions.x = -shadowSize; } } else { // Left shadow. p->texOrigin.x = xOffset; // Make sure the shadow isn't too big if(shadowSize > lineLength) { if(frData.sideCorners[1].corner <= MIN_OPEN) p->texDimensions.x = lineLength; else p->texDimensions.x = lineLength / 2; } else { p->texDimensions.x = shadowSize; } } if(hasBackSector) { // There is a back sector. if(bFloor > fFloor && bCeil < fCeil) { if(haveBottomShadower && haveTopShadower) { p->texture = LST_RADIO_CC; } else if(!haveBottomShadower) { p->texOrigin.y = bottom - fCeil; p->texDimensions.y = -(fCeil - fFloor); p->texture = LST_RADIO_CO; } else { p->texture = LST_RADIO_CO; } } else if(bFloor > fFloor) { if(haveBottomShadower && haveTopShadower) { p->texture = LST_RADIO_CC; } else if(!haveBottomShadower) { p->texOrigin.y = bottom - fCeil; p->texDimensions.y = -(fCeil - fFloor); p->texture = LST_RADIO_CO; } else { p->texture = LST_RADIO_CO; } } else if(bCeil < fCeil) { if(haveBottomShadower && haveTopShadower) { p->texture = LST_RADIO_CC; } else if(!haveBottomShadower) { p->texOrigin.y = bottom - fCeil; p->texDimensions.y = -(fCeil - fFloor); p->texture = LST_RADIO_CO; } else { p->texture = LST_RADIO_CO; } } } else { if(!haveBottomShadower) { p->texDimensions.y = -(fCeil - fFloor); p->texOrigin.y = calcTexCoordY(top, fFloor, fCeil, p->texDimensions.y); p->texture = LST_RADIO_CO; } else if(!haveTopShadower) { p->texture = LST_RADIO_CO; } else { p->texture = LST_RADIO_CC; } } } static void quadTexCoords(Vector2f *tc, Vector3f const *rverts, float wallLength, Vector3f const &texTopLeft, Vector3f const &texBottomRight, Vector2f const &texOrigin, Vector2f const &texDimensions, bool horizontal) { if(horizontal) { // Special horizontal coordinates for wall shadows. tc[0].x = tc[2].x = rverts[0].x - texTopLeft.x + texOrigin.y / texDimensions.y; tc[0].y = tc[1].y = rverts[0].y - texTopLeft.y + texOrigin.x / texDimensions.x; tc[1].x = tc[0].x + (rverts[1].z - texBottomRight.z) / texDimensions.y; tc[3].x = tc[0].x + (rverts[3].z - texBottomRight.z) / texDimensions.y; tc[3].y = tc[0].y + wallLength / texDimensions.x; tc[2].y = tc[0].y + wallLength / texDimensions.x; return; } tc[0].x = tc[1].x = rverts[0].x - texTopLeft.x + texOrigin.x / texDimensions.x; tc[3].y = tc[1].y = rverts[0].y - texTopLeft.y + texOrigin.y / texDimensions.y; tc[3].x = tc[2].x = tc[0].x + wallLength / texDimensions.x; tc[2].y = tc[3].y + (rverts[1].z - rverts[0].z) / texDimensions.y; tc[0].y = tc[3].y + (rverts[3].z - rverts[2].z) / texDimensions.y; } static void drawWallSectionShadow(Vector3f const *origVertices, WallEdge const &leftEdge, WallEdge const &rightEdge, rendershadowseg_params_t const &wsParms) { DENG_ASSERT(origVertices); bool const mustSubdivide = (leftEdge.divisionCount() || rightEdge.divisionCount()); uint realNumVertices = 4; if(mustSubdivide) realNumVertices = 3 + leftEdge.divisionCount() + 3 + rightEdge.divisionCount(); else realNumVertices = 4; // Allocate enough for the divisions too. Vector2f *rtexcoords = R_AllocRendTexCoords(realNumVertices); Vector4f *rcolors = R_AllocRendColors(realNumVertices); quadTexCoords(rtexcoords, origVertices, wsParms.sectionWidth, leftEdge.top().origin(), rightEdge.bottom().origin(), wsParms.texOrigin, wsParms.texDimensions, wsParms.horizontal); setRendpolyColor(rcolors, 4, wsParms.shadowDark * wsParms.shadowMul); if(rendFakeRadio != 2) { // Write multiple polys depending on rend params. DrawListSpec listSpec; listSpec.group = ShadowGeom; listSpec.texunits[TU_PRIMARY] = GLTextureUnit(GL_PrepareLSTexture(wsParms.texture), gl::ClampToEdge, gl::ClampToEdge); DrawList &shadowList = ClientApp::renderSystem().drawLists().find(listSpec); if(mustSubdivide) { /* * Need to swap indices around into fans set the position * of the division vertices, interpolate texcoords and * color. */ Vector3f *rvertices = R_AllocRendVertices(realNumVertices); Vector2f origTexCoords[4]; std::memcpy(origTexCoords, rtexcoords, sizeof(Vector2f) * 4); Vector4f origColors[4]; std::memcpy(origColors, rcolors, sizeof(Vector4f) * 4); R_DivVerts(rvertices, origVertices, leftEdge, rightEdge); R_DivTexCoords(rtexcoords, origTexCoords, leftEdge, rightEdge); R_DivVertColors(rcolors, origColors, leftEdge, rightEdge); shadowList.write(gl::TriangleFan, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, 3 + rightEdge.divisionCount(), rvertices + 3 + leftEdge.divisionCount(), rcolors + 3 + leftEdge.divisionCount(), rtexcoords + 3 + leftEdge.divisionCount()) .write(gl::TriangleFan, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, 3 + leftEdge.divisionCount(), rvertices, rcolors, rtexcoords); R_FreeRendVertices(rvertices); } else { shadowList.write(gl::TriangleStrip, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, 4, origVertices, rcolors, rtexcoords); } } R_FreeRendTexCoords(rtexcoords); R_FreeRendColors(rcolors); } /// @todo fixme: Should use the visual plane heights of sector clusters. void Rend_RadioWallSection(WallEdge const &leftEdge, WallEdge const &rightEdge, float shadowDark, float shadowSize) { // Disabled? if(!rendFakeRadio || levelFullBright) return; if(shadowSize <= 0) return; LineSide &side = leftEdge.mapLineSide(); HEdge const *hedge = side.leftHEdge(); SectorCluster const *cluster = &hedge->face().mapElementAs().cluster(); SectorCluster const *backCluster = 0; if(leftEdge.spec().section != LineSide::Middle && hedge->twin().hasFace()) { backCluster = hedge->twin().face().mapElementAs().clusterPtr(); } bool const haveBottomShadower = Rend_RadioPlaneCastsShadow(cluster->visFloor()); bool const haveTopShadower = Rend_RadioPlaneCastsShadow(cluster->visCeiling()); // Walls unaffected by floor and ceiling shadow casters receive no // side shadows either. We could do better here... if(!haveBottomShadower && !haveTopShadower) return; coord_t const lineLength = side.line().length(); coord_t const sectionOffset = leftEdge.mapLineSideOffset(); coord_t const sectionWidth = de::abs(Vector2d(rightEdge.origin() - leftEdge.origin()).length()); LineSideRadioData &frData = Rend_RadioDataForLineSide(side); coord_t const fFloor = cluster->visFloor().heightSmoothed(); coord_t const fCeil = cluster->visCeiling().heightSmoothed(); coord_t const bFloor = (backCluster? backCluster->visFloor().heightSmoothed() : 0); coord_t const bCeil = (backCluster? backCluster->visCeiling().heightSmoothed() : 0); Vector3f rvertices[4] = { leftEdge.bottom().origin(), leftEdge.top().origin(), rightEdge.bottom().origin(), rightEdge.top().origin() }; // Top Shadow? if(haveTopShadower) { if(rightEdge.top().z() > fCeil - shadowSize && leftEdge.bottom().z() < fCeil) { rendershadowseg_params_t parms; setTopShadowParams(&parms, shadowSize, shadowDark, leftEdge.top().z(), sectionOffset, sectionWidth, fFloor, fCeil, frData); drawWallSectionShadow(rvertices, leftEdge, rightEdge, parms); } } // Bottom Shadow? if(haveBottomShadower) { if(leftEdge.bottom().z() < fFloor + shadowSize && rightEdge.top().z() > fFloor) { rendershadowseg_params_t parms; setBottomShadowParams(&parms, shadowSize, shadowDark, leftEdge.top().z(), sectionOffset, sectionWidth, fFloor, fCeil, frData); drawWallSectionShadow(rvertices, leftEdge, rightEdge, parms); } } // Left Shadow? if(frData.sideCorners[0].corner > 0 && sectionOffset < shadowSize) { rendershadowseg_params_t parms; setSideShadowParams(&parms, shadowSize, shadowDark, leftEdge.bottom().z(), leftEdge.top().z(), false, haveBottomShadower, haveTopShadower, sectionOffset, sectionWidth, fFloor, fCeil, backCluster != 0, bFloor, bCeil, lineLength, frData); drawWallSectionShadow(rvertices, leftEdge, rightEdge, parms); } // Right Shadow? if(frData.sideCorners[1].corner > 0 && sectionOffset + sectionWidth > lineLength - shadowSize) { rendershadowseg_params_t parms; setSideShadowParams(&parms, shadowSize, shadowDark, leftEdge.bottom().z(), leftEdge.top().z(), true, haveBottomShadower, haveTopShadower, sectionOffset, sectionWidth, fFloor, fCeil, backCluster != 0, bFloor, bCeil, lineLength, frData); drawWallSectionShadow(rvertices, leftEdge, rightEdge, parms); } } /** * Construct and write a new shadow polygon to the rendering lists. */ static void writeShadowSection2(ShadowEdge const &leftEdge, ShadowEdge const &rightEdge, bool isFloor, float shadowDark) { static uint const floorIndices[][4] = {{0, 1, 2, 3}, {1, 2, 3, 0}}; static uint const ceilIndices[][4] = {{0, 3, 2, 1}, {1, 0, 3, 2}}; float const outerLeftAlpha = de::min(shadowDark * (1 - leftEdge.sectorOpenness()), 1.f); float const outerRightAlpha = de::min(shadowDark * (1 - rightEdge.sectorOpenness()), 1.f); if(!(outerLeftAlpha > .0001 && outerRightAlpha > .0001)) return; // What vertex winding order? (0 = left, 1 = right) // (for best results, the cross edge should always be the shortest). uint winding = (rightEdge.length() > leftEdge.length()? 1 : 0); uint const *idx = (isFloor ? floorIndices[winding] : ceilIndices[winding]); Vector3f rvertices[4]; // Left outer. rvertices[idx[0]] = leftEdge.outer(); // Right outer. rvertices[idx[1]] = rightEdge.outer(); // Right inner. rvertices[idx[2]] = rightEdge.inner(); // Left inner. rvertices[idx[3]] = leftEdge.inner(); Vector4f rcolors[4]; if(renderWireframe) { // Draw shadow geometry white to assist visual debugging. static const Vector4f white(1, 1, 1, 1); for(uint i = 0; i < 4; ++i) { rcolors[idx[i]] = white; } } // Left outer. rcolors[idx[0]].w = outerLeftAlpha; if(leftEdge.openness() < 1) rcolors[idx[0]].w *= 1 - leftEdge.openness(); // Right outer. rcolors[idx[1]].w = outerRightAlpha; if(rightEdge.openness() < 1) rcolors[idx[1]].w *= 1 - rightEdge.openness(); if(rendFakeRadio == 2) return; ClientApp::renderSystem().drawLists() .find(DrawListSpec(renderWireframe? UnlitGeom : ShadowGeom)) .write(gl::TriangleFan, BM_NORMAL, Vector2f(1, 1), Vector2f(0, 0), Vector2f(1, 1), Vector2f(0, 0), 0, 4, rvertices, rcolors); } static void writeShadowSection(int planeIndex, LineSide const &side, float shadowDark) { DENG2_ASSERT(side.hasSections()); DENG2_ASSERT(!side.line().definesPolyobj()); if(!(shadowDark > .0001)) return; if(!side.leftHEdge()) return; HEdge const *leftHEdge = side.leftHEdge(); Plane const &plane = side.sector().plane(planeIndex); Surface const *suf = &plane.surface(); // Surfaces with a missing material don't shadow. if(!suf->hasMaterial()) return; // Surfaces with a sky-masked material don't shadow. if(suf->material().isSkyMasked()) return; // Ensure we have up to date info about the material. MaterialAnimator &matAnimator = suf->material().getAnimator(Rend_MapSurfaceMaterialSpec()); matAnimator.prepare(); // Surfaces with a glowing material don't shadow. if(matAnimator.glowStrength() > 0) return; // If the sector containing the shadowing line section is fully closed (i.e., volume // is not positive) then skip shadow drawing entirely. /// @todo Encapsulate this logic in ShadowEdge -ds if(!leftHEdge->hasFace() || !leftHEdge->face().hasMapElement()) return; if(!leftHEdge->face().mapElementAs().cluster().hasWorldVolume()) return; static ShadowEdge leftEdge; // this function is called often; keep these around static ShadowEdge rightEdge; leftEdge.init(*leftHEdge, Line::From); rightEdge.init(*leftHEdge, Line::To); leftEdge.prepare(planeIndex); rightEdge.prepare(planeIndex); if(leftEdge.sectorOpenness() >= 1 && rightEdge.sectorOpenness() >= 1) return; writeShadowSection2(leftEdge, rightEdge, suf->normal()[VZ] > 0, shadowDark); } /** * @attention Do not use the global radio state in here, as @a subspace can be * part of any Sector, not the one chosen for wall rendering. */ void Rend_RadioSubspaceEdges(ConvexSubspace const &subspace) { if(!rendFakeRadio) return; if(levelFullBright) return; if(!subspace.shadowLineCount()) return; SectorCluster &cluster = subspace.cluster(); float sectorlight = cluster.lightSourceIntensity(); // Determine the shadow properties. /// @todo Make cvars out of constants. //float shadowWallSize = 2 * (8 + 16 - sectorlight * 16); float shadowDark = Rend_RadioCalcShadowDarkness(sectorlight); // Any need to continue? if(shadowDark < .0001f) return; Vector3f const eyeToSurface(Vector2d(Rend_EyeOrigin().x, Rend_EyeOrigin().z) - subspace.poly().center()); // We need to check all the shadow lines linked to this subspace for // the purpose of fakeradio shadowing. subspace.forAllShadowLines([&cluster, &shadowDark, &eyeToSurface] (LineSide &side) { // Already rendered during the current frame? We only want to // render each shadow once per frame. if(side.shadowVisCount() != R_FrameCount()) { side.setShadowVisCount(R_FrameCount()); for(int pln = 0; pln < cluster.visPlaneCount(); ++pln) { Plane const &plane = cluster.visPlane(pln); if(Vector3f(eyeToSurface, Rend_EyeOrigin().y - plane.heightSmoothed()) .dot(plane.surface().normal()) >= 0) { writeShadowSection(pln, side, shadowDark); } } } return LoopContinue; }); } #ifdef DENG_DEBUG static void drawPoint(Vector3d const &point, int radius, float const color[4]) { viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); Vector3d const leftOff = viewData->upVec + viewData->sideVec; Vector3d const rightOff = viewData->upVec - viewData->sideVec; //Vector3d const viewToCenter = point - Rend_EyeOrigin(); //float scale = float(viewToCenter.dot(viewData->frontVec)) / // viewData->frontVec.dot(viewData->frontVec); Vector3d finalPos( point.x, point.z, point.y ); // The final radius. float radX = radius * 1; float radY = radX / 1.2f; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glColor4fv(color); glBegin(GL_QUADS); glTexCoord2f(0, 0); glVertex3d(finalPos.x + radX * leftOff.x, finalPos.y + radY * leftOff.y, finalPos.z + radX * leftOff.z); glTexCoord2f(1, 0); glVertex3d(finalPos.x + radX * rightOff.x, finalPos.y + radY * rightOff.y, finalPos.z + radX * rightOff.z); glTexCoord2f(1, 1); glVertex3d(finalPos.x - radX * leftOff.x, finalPos.y - radY * leftOff.y, finalPos.z - radX * leftOff.z); glTexCoord2f(0, 1); glVertex3d(finalPos.x - radX * rightOff.x, finalPos.y - radY * rightOff.y, finalPos.z - radX * rightOff.z); glEnd(); } void Rend_DrawShadowOffsetVerts() { static const float red[4] = { 1.f, .2f, .2f, 1.f}; static const float yellow[4] = {.7f, .7f, .2f, 1.f}; if(!App_WorldSystem().hasMap()) return; Map &map = App_WorldSystem().map(); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glDepthMask(GL_FALSE); glDisable(GL_DEPTH_TEST); GL_BindTextureUnmanaged(GL_PrepareLSTexture(LST_DYNAMIC), gl::ClampToEdge, gl::ClampToEdge); glEnable(GL_TEXTURE_2D); /// @todo fixme: Should use the visual plane heights of sector clusters. map.forAllLines([] (Line &line) { for(int i = 0; i < 2; ++i) { Vertex &vtx = line.vertex(i); LineOwner const *base = vtx.firstLineOwner(); LineOwner const *own = base; do { Vector2d xy = vtx.origin() + own->extendedShadowOffset(); coord_t z = own->line().frontSector().floor().heightSmoothed(); drawPoint(Vector3d(xy.x, xy.y, z), 1, yellow); xy = vtx.origin() + own->innerShadowOffset(); drawPoint(Vector3d(xy.x, xy.y, z), 1, red); own = &own->next(); } while(own != base); } return LoopContinue; }); glDisable(GL_TEXTURE_2D); glDepthMask(GL_TRUE); glEnable(GL_DEPTH_TEST); } #endif doomsday-stable-1.15.7/doomsday/client/src/render/biastracker.cpp0000664000175000017500000001553712641367670024350 0ustar jaakkojaakko/** @file biastracker.cpp Shadow Bias illumination tracker. * * @authors Copyright © 2005-2014 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "render/biastracker.h" #include "dd_main.h" #include "world/map.h" #include "BiasDigest" #include "BiasSource" #include using namespace de; struct Contributor { BiasSource *source; float influence; }; /** * @todo Do not observe source deletion. A better solution would represent any * source deletions in BiasDigest. */ DENG2_PIMPL_NOREF(BiasTracker) , DENG2_OBSERVES(BiasSource, Deletion) { Contributor contributors[MAX_CONTRIBUTORS]; byte activeContributors; byte changedContributions; uint lastSourceDeletion; // Milliseconds. Instance() : activeContributors(0) , changedContributions(0) , lastSourceDeletion(0) { de::zap(contributors); } /// Observes BiasSource Deletion void biasSourceBeingDeleted(BiasSource const &source) { Contributor *ctbr = contributors; for(int i = 0; i < MAX_CONTRIBUTORS; ++i, ctbr++) { if(ctbr->source == &source) { ctbr->source = 0; activeContributors &= ~(1 << i); changedContributions |= 1 << i; // Remember the current time (used for interpolation). /// @todo Do not assume the 'current' map. lastSourceDeletion = App_WorldSystem().map().biasCurrentTime(); break; } } } }; BiasTracker::BiasTracker() : d(new Instance()) {} void BiasTracker::clearContributors() { d->activeContributors = 0; } int BiasTracker::addContributor(BiasSource *source, float intensity) { if(!source) return -1; // If its too weak we will ignore it entirely. if(intensity < BiasIllum::MIN_INTENSITY) return -1; int firstUnusedSlot = -1; int slot = -1; // Do we have a latent contribution or an unused slot? Contributor *ctbr = d->contributors; for(int i = 0; i < MAX_CONTRIBUTORS; ++i, ctbr++) { if(!ctbr->source) { // Remember the first unused slot. if(firstUnusedSlot == -1) firstUnusedSlot = i; } // A latent contribution? else if(ctbr->source == source) { slot = i; break; } } if(slot == -1) { if(firstUnusedSlot != -1) { slot = firstUnusedSlot; } else { // Dang, we'll need to drop the weakest. int weakest = -1; Contributor *ctbr = d->contributors; for(int i = 0; i < MAX_CONTRIBUTORS; ++i, ctbr++) { DENG_ASSERT(ctbr->source != 0); if(i == 0 || ctbr->influence < d->contributors[weakest].influence) { weakest = i; } } if(intensity <= d->contributors[weakest].influence) return - 1; slot = weakest; ctbr->source->audienceForDeletion -= d; ctbr->source = 0; } } DENG_ASSERT(slot >= 0 && slot < MAX_CONTRIBUTORS); ctbr = &d->contributors[slot]; // When reactivating a latent contribution if the intensity has not // changed we don't need to force an update. if(!(ctbr->source == source && de::fequal(ctbr->influence, intensity))) d->changedContributions |= (1 << slot); if(!ctbr->source) source->audienceForDeletion += d; ctbr->source = source; ctbr->influence = intensity; // (Re)activate this contributor. d->activeContributors |= 1 << slot; return slot; } BiasSource &BiasTracker::contributor(int index) const { if(index >= 0 && index < MAX_CONTRIBUTORS && (d->activeContributors & (1 << index))) { DENG_ASSERT(d->contributors[index].source != 0); return *d->contributors[index].source; } /// @throw UnknownContributorError An invalid contributor index was specified. throw UnknownContributorError("BiasTracker::lightContributor", QString("Index %1 invalid/out of range").arg(index)); } uint BiasTracker::timeOfLatestContributorUpdate() const { uint latest = 0; if(d->changedContributions) { Contributor const *ctbr = d->contributors; for(int i = 0; i < MAX_CONTRIBUTORS; ++i, ctbr++) { if(!(d->changedContributions & (1 << i))) continue; if(!ctbr->source && !(d->activeContributors & (1 << i))) { // The source of the contribution was deleted. if(latest < d->lastSourceDeletion) latest = d->lastSourceDeletion; } else if(latest < ctbr->source->lastUpdateTime()) { latest = ctbr->source->lastUpdateTime(); } } } return latest; } byte BiasTracker::activeContributors() const { return d->activeContributors; } byte BiasTracker::changedContributions() const { return d->changedContributions; } void BiasTracker::updateAllContributors() { Contributor *ctbr = d->contributors; for(int i = 0; i < MAX_CONTRIBUTORS; ++i, ctbr++) { if(ctbr->source) { ctbr->source->forceUpdate(); } } } void BiasTracker::applyChanges(BiasDigest &changes) { // All contributions from changed sources will need to be updated. Contributor *ctbr = d->contributors; for(int i = 0; i < MAX_CONTRIBUTORS; ++i, ctbr++) { if(!ctbr->source) continue; /// @todo optimize: This O(n) lookup can be avoided if we 1) reference /// sources by unique in-map index, and 2) re-index source references /// here upon deletion. The assumption being that affection changes /// occur far more frequently. if(changes.isSourceChanged(App_WorldSystem().map().indexOf(*ctbr->source))) { d->changedContributions |= 1 << i; } } } void BiasTracker::markIllumUpdateCompleted() { d->changedContributions = 0; } doomsday-stable-1.15.7/doomsday/client/src/ui/0000775000175000017500000000000012641367670020475 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/ui/mouse_qt.cpp0000664000175000017500000001354312641367670023043 0ustar jaakkojaakko/** @file mouse_qt.cpp * * Mouse driver that gets mouse input from the Qt based canvas widget. * @ingroup input * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "ui/sys_input.h" #include "ui/clientwindowsystem.h" #include "ui/clientwindow.h" #include "ui/mouse_qt.h" #include #include #include #include #include #include #ifdef MACOSX # include "cursor_macx.h" #endif typedef struct clicker_s { int down; // Count for down events. int up; // Count for up events. } clicker_t; //static int mousePosX, mousePosY; // Window position. static struct { int dx, dy; } mouseDelta[IMA_MAXAXES]; static clicker_t mouseClickers[IMB_MAXBUTTONS]; static bool mouseTrapped = false; static bool cursorHidden = false; static QPoint prevMousePos; static int Mouse_Qt_Init(void) { memset(&mouseDelta, 0, sizeof(mouseDelta)); memset(&mouseClickers, 0, sizeof(mouseClickers)); mouseTrapped = false; cursorHidden = false; prevMousePos = QPoint(); return true; } static void Mouse_Qt_Shutdown(void) { // nothing to do } static void Mouse_Qt_Poll() { if(!mouseTrapped) return; ClientWindow *win = ClientWindowSystem::mainPtr(); if(!win) return; // Hmm? QPoint curPos = win->mapFromGlobal(QCursor::pos()); if(!prevMousePos.isNull()) { QPoint delta = curPos - prevMousePos; if(!delta.isNull()) { Mouse_Qt_SubmitMotion(IMA_POINTER, delta.x(), delta.y()); // Keep the cursor centered. QPoint mid(win->width() / 2, win->height() / 2); #ifdef DENG2_QT_5_0_OR_NEWER mid /= qApp->devicePixelRatio(); #endif QCursor::setPos(win->mapToGlobal(mid)); prevMousePos = mid; } } else { prevMousePos = curPos; } } static void Mouse_Qt_GetState(mousestate_t *state) { int i; memset(state, 0, sizeof(*state)); // Position and wheel. for(i = 0; i < IMA_MAXAXES; ++i) { state->axis[i].x = mouseDelta[i].dx; state->axis[i].y = mouseDelta[i].dy; // Reset. mouseDelta[i].dx = mouseDelta[i].dy = 0; } // Button presses and releases. for(i = 0; i < IMB_MAXBUTTONS; ++i) { state->buttonDowns[i] = mouseClickers[i].down; state->buttonUps[i] = mouseClickers[i].up; // Reset counters. mouseClickers[i].down = mouseClickers[i].up = 0; } } static void Mouse_Qt_ShowCursor(bool yes) { #ifndef MACOSX de::Canvas &canvas = ClientWindowSystem::main().canvas(); #endif LOG_INPUT_VERBOSE("%s cursor (presently visible? %b)") << (yes? "showing" : "hiding") << !cursorHidden; if(!yes && !cursorHidden) { cursorHidden = true; #ifdef MACOSX Cursor_Show(false); #else canvas.setCursor(QCursor(Qt::BlankCursor)); qApp->setOverrideCursor(QCursor(Qt::BlankCursor)); #endif } else if(yes && cursorHidden) { cursorHidden = false; #ifdef MACOSX Cursor_Show(true); #else qApp->restoreOverrideCursor(); canvas.setCursor(QCursor(Qt::ArrowCursor)); // Default cursor. #endif } } static void Mouse_Qt_InitTrap() { de::Canvas &canvas = ClientWindowSystem::main().canvas(); QCursor::setPos(canvas.mapToGlobal(canvas.rect().center())); canvas.grabMouse(); Mouse_Qt_ShowCursor(false); } static void Mouse_Qt_DeinitTrap() { ClientWindowSystem::main().canvas().releaseMouse(); Mouse_Qt_ShowCursor(true); } static void Mouse_Qt_Trap(dd_bool enabled) { if(mouseTrapped == CPP_BOOL(enabled)) return; mouseTrapped = enabled; prevMousePos = QPoint(); if(enabled) { Mouse_Qt_InitTrap(); } else { Mouse_Qt_DeinitTrap(); } } void Mouse_Qt_SubmitButton(int button, dd_bool isDown) { if(button < 0 || button >= IMB_MAXBUTTONS) return; // Ignore... if(isDown) mouseClickers[button].down++; else mouseClickers[button].up++; } void Mouse_Qt_SubmitMotion(int axis, int deltaX, int deltaY) { if(axis < 0 || axis >= IMA_MAXAXES) return; // Ignore... /// @todo It would likely be better to directly post a ddevent out of this. if(axis == IMA_WHEEL) { int idx = ( deltaX < 0? IMB_MWHEELLEFT : deltaX > 0? IMB_MWHEELRIGHT : deltaY < 0? IMB_MWHEELUP : IMB_MWHEELDOWN); // We are not yet equipped to handle finer wheel motions. Mouse_Qt_SubmitButton(idx, true); Mouse_Qt_SubmitButton(idx, false); } else { mouseDelta[axis].dx += deltaX; mouseDelta[axis].dy += deltaY; } } void Mouse_Qt_SubmitWindowPosition(int x, int y) { // Absolute coordintes. mouseDelta[IMA_POINTER].dx = x; mouseDelta[IMA_POINTER].dy = y; } // The global interface. mouseinterface_t qtMouse = { Mouse_Qt_Init, Mouse_Qt_Shutdown, Mouse_Qt_Poll, Mouse_Qt_GetState, Mouse_Qt_Trap }; doomsday-stable-1.15.7/doomsday/client/src/ui/inputdevicehatcontrol.cpp0000664000175000017500000000355612641367670025627 0ustar jaakkojaakko/** @file inputdevicehatcontrol.cpp Hat control for a logical input device. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "ui/inputdevicehatcontrol.h" #include // Timer_RealMilliseconds() using namespace de; InputDeviceHatControl::InputDeviceHatControl(String const &name) { setName(name); } InputDeviceHatControl::~InputDeviceHatControl() {} dint InputDeviceHatControl::position() const { return _pos; } void InputDeviceHatControl::setPosition(dint newPosition) { _pos = newPosition; _time = Timer_RealMilliseconds(); // Remember when the change occured. // We can clear the expiration when centered. if(_pos < 0) { setBindContextAssociation(Expired, UnsetFlags); } } duint InputDeviceHatControl::time() const { return _time; } String InputDeviceHatControl::description() const { return String(_E(b) "%1 " _E(.) "(Hat)").arg(fullName()); } bool InputDeviceHatControl::inDefaultState() const { return _pos < 0; // Centered? } doomsday-stable-1.15.7/doomsday/client/src/ui/inputdevice.cpp0000664000175000017500000002222212641367670023520 0ustar jaakkojaakko/** @file inputdevice.cpp Logical input device. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "ui/inputdevice.h" #include #include #include /// @todo: remove: #include "ui/inputdeviceaxiscontrol.h" #include "ui/inputdevicebuttoncontrol.h" #include "ui/inputdevicehatcontrol.h" /// end todo using namespace de; DENG2_PIMPL_NOREF(InputDevice::Control) { String name; ///< Symbolic InputDevice *device = nullptr; BindContextAssociation flags = DefaultFlags; BindContext *bindContext = nullptr; BindContext *prevBindContext = nullptr; }; InputDevice::Control::Control(InputDevice *device) : d(new Instance) { setDevice(device); } InputDevice::Control::~Control() {} String InputDevice::Control::name() const { return d->name; } void InputDevice::Control::setName(String const &newName) { d->name = newName; } String InputDevice::Control::fullName() const { String desc; if(hasDevice()) desc += device().name() + "-"; desc += (d->name.isEmpty()? "" : d->name); return desc; } InputDevice &InputDevice::Control::device() const { if(d->device) return *d->device; /// @throw MissingDeviceError Missing InputDevice attribution. throw MissingDeviceError("InputDevice::Control::device", "No InputDevice is attributed"); } bool InputDevice::Control::hasDevice() const { return d->device != nullptr; } void InputDevice::Control::setDevice(InputDevice *newDevice) { d->device = newDevice; } BindContext *InputDevice::Control::bindContext() const { return d->bindContext; } void InputDevice::Control::setBindContext(BindContext *newContext) { d->bindContext = newContext; } InputDevice::Control::BindContextAssociation InputDevice::Control::bindContextAssociation() const { return d->flags; } void InputDevice::Control::setBindContextAssociation(BindContextAssociation const &flagsToChange, FlagOp op) { applyFlagOperation(d->flags, flagsToChange, op); } void InputDevice::Control::clearBindContextAssociation() { d->prevBindContext = d->bindContext; d->bindContext = nullptr; setBindContextAssociation(Triggered, UnsetFlags); } void InputDevice::Control::expireBindContextAssociationIfChanged() { // No change? if(d->bindContext == d->prevBindContext) return; // No longer valid. setBindContextAssociation(Expired); setBindContextAssociation(Triggered, UnsetFlags); // Not any more. } DENG2_PIMPL(InputDevice) { bool active = false; ///< Initially inactive. String title; ///< Human-friendly title. String name; ///< Symbolic name. typedef QList Axes; Axes axes; typedef QList Buttons; Buttons buttons; typedef QList Hats; Hats hats; Instance(Public *i) : Base(i) {} ~Instance() { qDeleteAll(hats); qDeleteAll(buttons); qDeleteAll(axes); } DENG2_PIMPL_AUDIENCE(ActiveChange) }; DENG2_AUDIENCE_METHOD(InputDevice, ActiveChange) InputDevice::InputDevice(String const &name) : d(new Instance(this)) { DENG2_ASSERT(!name.isEmpty()); d->name = name; } InputDevice::~InputDevice() {} bool InputDevice::isActive() const { return d->active; } void InputDevice::activate(bool yes) { if(d->active != yes) { d->active = yes; // Notify interested parties. DENG2_FOR_AUDIENCE2(ActiveChange, i) i->inputDeviceActiveChanged(*this); } } String InputDevice::name() const { return d->name; } String InputDevice::title() const { return (d->title.isEmpty()? d->name : d->title); } void InputDevice::setTitle(String const &newTitle) { d->title = newTitle; } /// @todo: Device title should be updated to include the product name if known (joysticks). String InputDevice::description() const { String desc; if(!d->title.isEmpty()) { desc += String(_E(D)_E(b) "%1" _E(.)_E(.) " - ").arg(d->title); } desc += String(_E(b) "%1" _E(.)_E(l) " (%2)" _E(.)).arg(name()).arg(isActive()? "active" : "inactive"); if(int const count = axisCount()) { desc += String("\n " _E(b) "%1 axes:" _E(.)).arg(count); int idx = 0; for(Control const *axis : d->axes) { desc += String("\n [%1] " _E(>) "%2" _E(<)).arg(idx++, 3).arg(axis->description()); } } if(int const count = buttonCount()) { desc += String("\n " _E(b) "%1 buttons:" _E(.)).arg(count); int idx = 0; for(Control const *button : d->buttons) { desc += String("\n [%1] " _E(>) "%2" _E(<)).arg(idx++, 3).arg(button->description()); } } if(int const count = hatCount()) { desc += String("\n " _E(b) "%1 hats:" _E(.)).arg(count); int idx = 0; for(Control const *hat : d->hats) { desc += String("\n [%1] " _E(>) "%2" _E(<)).arg(idx++, 3).arg(hat->description()); } } return desc; } void InputDevice::reset() { LOG_AS("InputDevice"); for(Control *axis : d->axes) { axis->reset(); } for(Control *button : d->buttons) { button->reset(); } for(Control *hat : d->hats) { hat->reset(); } if(!d->name.compareWithCase("key")) { extern bool shiftDown, altDown; altDown = shiftDown = false; } LOG_INPUT_VERBOSE(_E(b) "'%s'" _E(.) " controls reset") << title(); } LoopResult InputDevice::forAllControls(std::function func) { for(Control *axis : d->axes) { if(auto result = func(*axis)) return result; } for(Control *button : d->buttons) { if(auto result = func(*button)) return result; } for(Control *hat : d->hats) { if(auto result = func(*hat)) return result; } return LoopContinue; } dint InputDevice::toAxisId(String const &name) const { if(!name.isEmpty()) { for(int i = 0; i < d->axes.count(); ++i) { if(!d->axes.at(i)->name().compareWithoutCase(name)) return i; } } return -1; } dint InputDevice::toButtonId(String const &name) const { if(!name.isEmpty()) { for(int i = 0; i < d->buttons.count(); ++i) { if(!d->buttons.at(i)->name().compareWithoutCase(name)) return i; } } return -1; } bool InputDevice::hasAxis(de::dint id) const { return (id >= 0 && id < d->axes.count()); } InputDeviceAxisControl &InputDevice::axis(dint id) const { if(hasAxis(id)) return *d->axes.at(id); /// @throw MissingControlError The given id is invalid. throw MissingControlError("InputDevice::axis", "Invalid id:" + String::number(id)); } void InputDevice::addAxis(InputDeviceAxisControl *axis) { if(!axis) return; d->axes.append(axis); axis->setDevice(this); } int InputDevice::axisCount() const { return d->axes.count(); } bool InputDevice::hasButton(de::dint id) const { return (id >= 0 && id < d->buttons.count()); } InputDeviceButtonControl &InputDevice::button(dint id) const { if(hasButton(id)) return *d->buttons.at(id); /// @throw MissingControlError The given id is invalid. throw MissingControlError("InputDevice::button", "Invalid id:" + String::number(id)); } void InputDevice::addButton(InputDeviceButtonControl *button) { if(!button) return; d->buttons.append(button); button->setDevice(this); } int InputDevice::buttonCount() const { return d->buttons.count(); } bool InputDevice::hasHat(de::dint id) const { return (id >= 0 && id < d->hats.count()); } InputDeviceHatControl &InputDevice::hat(dint id) const { if(hasHat(id)) return *d->hats.at(id); /// @throw MissingControlError The given id is invalid. throw MissingControlError("InputDevice::hat", "Invalid id:" + String::number(id)); } void InputDevice::addHat(InputDeviceHatControl *hat) { if(!hat) return; d->hats.append(hat); hat->setDevice(this); } int InputDevice::hatCount() const { return d->hats.count(); } void InputDevice::consoleRegister() { for(Control *axis : d->axes) { axis->consoleRegister(); } for(Control *button : d->buttons) { button->consoleRegister(); } for(Control *hat : d->hats) { hat->consoleRegister(); } } doomsday-stable-1.15.7/doomsday/client/src/ui/editors/0000775000175000017500000000000012641367670022146 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/ui/editors/rendererappearanceeditor.cpp0000664000175000017500000006527612641367670027727 0ustar jaakkojaakko/** @file rendererappearanceeditor.cpp * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/editors/rendererappearanceeditor.h" #include "ui/dialogs/renderersettingsdialog.h" #include "ui/widgets/profilepickerwidget.h" #include "ui/widgets/cvarchoicewidget.h" #include "ui/widgets/cvarsliderwidget.h" #include "ui/widgets/cvartogglewidget.h" #include "ui/clientwindow.h" #include "clientapp.h" #include #include #include #include #include #include #include using namespace de; using namespace ui; DENG_GUI_PIMPL(RendererAppearanceEditor), DENG2_OBSERVES(SettingsRegister, ProfileChange), DENG2_OBSERVES(App, GameChange) { /** * Opens a popup menu for folding/unfolding all settings groups. */ struct RightClickHandler : public GuiWidget::IEventHandler { Instance *d; RightClickHandler(Instance *inst) : d(inst) {} bool handleEvent(GuiWidget &widget, Event const &event) { switch(widget.handleMouseClick(event, MouseEvent::Right)) { case MouseClickFinished: { PopupMenuWidget *pop = new PopupMenuWidget; pop->setDeleteAfterDismissed(true); d->self.add(pop); pop->setAnchorAndOpeningDirection(widget.rule(), ui::Left); pop->items() << new ActionItem(tr("Fold All"), new SignalAction(&d->self, SLOT(foldAll()))) << new ActionItem(tr("Unfold All"), new SignalAction(&d->self, SLOT(unfoldAll()))); pop->open(); return true; } case MouseClickUnrelated: return false; default: return true; } } }; /** * Foldable group of settings. */ class Group : public FoldPanelWidget { /// Action for reseting the group's settings to defaults. struct ResetAction : public Action { Group *group; ResetAction(Group *groupToReset) : group(groupToReset) {} void trigger() { Action::trigger(); group->resetToDefaults(); } }; public: Group(RendererAppearanceEditor::Instance *inst, String const &name, String const &titleText) : FoldPanelWidget(name), d(inst), _firstColumnWidth(0) { _group = new GuiWidget; setContent(_group); makeTitle(titleText); // Set up a context menu for right-clicking. title().addEventHandler(new RightClickHandler(d)); // We want the first column of all groups to be aligned with each other. _layout.setColumnFixedWidth(0, *d->firstColumnWidth); _layout.setGridSize(2, 0); _layout.setColumnAlignment(0, AlignRight); _layout.setLeftTop(_group->rule().left(), _group->rule().top()); // Button for reseting this group to defaults. _resetButton = new ButtonWidget; _resetButton->setText(tr("Reset")); _resetButton->setAction(new ResetAction(this)); _resetButton->rule() .setInput(Rule::Right, d->container->contentRule().right()) .setInput(Rule::AnchorY, title().rule().top() + title().rule().height() / 2) .setAnchorPoint(Vector2f(0, .5f)); _resetButton->disable(); d->container->add(&title()); d->container->add(_resetButton); d->container->add(this); } ~Group() { releaseRef(_firstColumnWidth); } ButtonWidget &resetButton() { return *_resetButton; } void preparePanelForOpening() { FoldPanelWidget::preparePanelForOpening(); if(!d->settings.isReadOnlyProfile(d->settings.currentProfile())) { _resetButton->enable(); } } void panelClosing() { FoldPanelWidget::panelClosing(); _resetButton->disable(); } void addSpace() { _layout << Const(0); } void addLabel(String const &text) { _layout << *LabelWidget::newWithText(text, _group); } CVarToggleWidget *addToggle(char const *cvar, String const &label) { CVarToggleWidget *w = new CVarToggleWidget(cvar, label); _group->add(w); _layout << *w; return w; } CVarChoiceWidget *addChoice(char const *cvar, ui::Direction opening = ui::Up) { CVarChoiceWidget *w = new CVarChoiceWidget(cvar); w->setOpeningDirection(opening); _group->add(w); _layout << *w; return w; } CVarSliderWidget *addSlider(char const *cvar) { auto *w = new CVarSliderWidget(cvar); _group->add(w); _layout << *w; return w; } CVarSliderWidget *addSlider(char const *cvar, Ranged const &range, double step, int precision) { auto *w = addSlider(cvar); w->setRange(range, step); w->setPrecision(precision); return w; } VariableSliderWidget *addSlider(Variable &var, Ranged const &range, double step, int precision) { auto *w = new VariableSliderWidget(var, range, step); w->setPrecision(precision); _group->add(w); _layout << *w; return w; } void commit() { _group->rule().setSize(_layout.width(), _layout.height()); // Extend the title all the way to the button. title().rule().setInput(Rule::Right, _resetButton->rule().left()); // Calculate the maximum rule for the first column items. for(int i = 0; i < _layout.gridSize().y; ++i) { GuiWidget *w = _layout.at(Vector2i(0, i)); if(w) { changeRef(_firstColumnWidth, OperatorRule::maximum(w->rule().width(), _firstColumnWidth)); } } } void fetch() { foreach(Widget *child, _group->childWidgets()) { if(ICVarWidget *w = child->maybeAs()) { w->updateFromCVar(); } } } void resetToDefaults() { foreach(Widget *child, _group->childWidgets()) { if(ICVarWidget *w = child->maybeAs()) { d->settings.resetSettingToDefaults(w->cvarPath()); w->updateFromCVar(); } } } Rule const &firstColumnWidth() const { return *_firstColumnWidth; } private: RendererAppearanceEditor::Instance *d; ButtonWidget *_resetButton; GuiWidget *_group; GridLayout _layout; Rule const *_firstColumnWidth; }; SettingsRegister &settings; DialogContentStylist stylist; ScrollAreaWidget *container; IndirectRule *firstColumnWidth; ///< Shared by all groups. ButtonWidget *close; ProfilePickerWidget *profile; Group *skyGroup; Group *shadowGroup; Group *lightGroup; Group *volLightGroup; Group *glowGroup; Group *lensGroup; Group *matGroup; Group *modelGroup; Group *spriteGroup; Group *objectGroup; Group *partGroup; Instance(Public *i) : Base(i) , settings(ClientApp::renderSystem().appearanceSettings()) , firstColumnWidth(new IndirectRule) { // The editor will close automatically when going to Ring Zero. App::app().audienceForGameChange() += this; settings.audienceForProfileChange += this; // The contents of the editor will scroll. container = new ScrollAreaWidget; container->enableIndicatorDraw(true); stylist.setContainer(*container); container->add(close = new ButtonWidget); container->add(profile = new ProfilePickerWidget(settings, tr("appearance"))); close->setImage(style().images().image("close.ringless")); close->setImageColor(style().colors().colorf("altaccent")); close->setOverrideImageSize(style().fonts().font("title").height().valuei()); close->setAction(new SignalAction(thisPublic, SLOT(close()))); // Sky settings. skyGroup = new Group(this, "sky", tr("Sky")); skyGroup->addLabel(tr("Sky Sphere Radius:")); skyGroup->addSlider("rend-sky-distance", Ranged(0, 8000), 10, 0); skyGroup->commit(); // Shadow settings. shadowGroup = new Group(this, "shadow", tr("Shadows")); shadowGroup->addSpace(); shadowGroup->addToggle("rend-fakeradio", tr("Ambient Occlusion")); shadowGroup->addLabel(tr("Occlusion Darkness:")); shadowGroup->addSlider("rend-fakeradio-darkness"); shadowGroup->addSpace(); shadowGroup->addToggle("rend-shadow", tr("Objects Cast Shadows")); shadowGroup->addLabel(tr("Shadow Darkness:")); shadowGroup->addSlider("rend-shadow-darkness"); shadowGroup->addLabel(tr("Max Visible Distance:")); shadowGroup->addSlider("rend-shadow-far", Ranged(0, 3000), 10, 0); shadowGroup->addLabel(tr("Maximum Radius:")); shadowGroup->addSlider("rend-shadow-radius-max", Ranged(1, 128), 1, 0); shadowGroup->commit(); // Dynamic light settings. lightGroup = new Group(this, "plight", tr("Point Lighting")); lightGroup->addSpace(); lightGroup->addToggle("rend-light", tr("Dynamic Lights")); lightGroup->addSpace(); lightGroup->addToggle("rend-light-decor", tr("Light Decorations")); lightGroup->addLabel(tr("Blending Mode:")); lightGroup->addChoice("rend-light-blend")->items() << new ChoiceItem(tr("Multiply"), 0) << new ChoiceItem(tr("Add"), 1) << new ChoiceItem(tr("Process without drawing"), 2); lightGroup->addLabel(tr("Number of Lights:")); lightGroup->addSlider("rend-light-num", Ranged(0, 2000), 1, 0)->setMinLabel(tr("Max")); lightGroup->addLabel(tr("Brightness:")); lightGroup->addSlider("rend-light-bright"); lightGroup->addLabel(tr("Brightness in Fog:")); lightGroup->addSlider("rend-light-fog-bright"); lightGroup->addLabel(tr("Light Radius Factor:")); lightGroup->addSlider("rend-light-radius-scale"); lightGroup->addLabel(tr("Light Max Radius:")); lightGroup->addSlider("rend-light-radius-max"); lightGroup->commit(); // Volume lighting group. volLightGroup = new Group(this, "vlight", tr("Volume Lighting")); volLightGroup->addSpace(); volLightGroup->addToggle("rend-light-sky-auto", tr("Apply Sky Color")); volLightGroup->addLabel(tr("Sky Color Factor:")); volLightGroup->addSlider("rend-light-sky"); volLightGroup->addLabel(tr("Attenuation Distance:")); volLightGroup->addSlider("rend-light-attenuation", Ranged(0, 4000), 1, 0)->setMinLabel(tr("Off")); volLightGroup->addLabel(tr("Light Compression:")); volLightGroup->addSlider("rend-light-compression"); volLightGroup->addLabel(tr("Ambient Light:")); volLightGroup->addSlider("rend-light-ambient"); volLightGroup->addLabel(tr("Wall Angle Factor:")); volLightGroup->addSlider("rend-light-wall-angle", Ranged(0, 3), .01, 2); volLightGroup->addSpace(); volLightGroup->addToggle("rend-light-wall-angle-smooth", tr("Smoothed Angle")); volLightGroup->commit(); // Glow settings. glowGroup = new Group(this, "glow", tr("Surface Glow")); glowGroup->addLabel(tr("Material Glow:")); glowGroup->addSlider("rend-glow"); glowGroup->addLabel(tr("Max Glow Height:")); glowGroup->addSlider("rend-glow-height"); glowGroup->addLabel(tr("Glow Height Factor:")); glowGroup->addSlider("rend-glow-scale"); glowGroup->addSpace(); glowGroup->addToggle("rend-glow-wall", tr("Glow Visible on Walls")); glowGroup->commit(); // Camera lens settings. lensGroup = new Group(this, "lens", tr("Camera Lens")); lensGroup->addLabel(tr("Pixel Doubling:")); lensGroup->addSlider(App::config("render.fx.resize.factor"), Ranged(1, 8), .1, 1); lensGroup->addSpace(); lensGroup->addToggle("rend-bloom", tr("Bloom")); lensGroup->addLabel(tr("Bloom Threshold:")); lensGroup->addSlider("rend-bloom-threshold"); lensGroup->addLabel(tr("Bloom Intensity:")); lensGroup->addSlider("rend-bloom-intensity"); lensGroup->addLabel(tr("Bloom Dispersion:")); lensGroup->addSlider("rend-bloom-dispersion"); lensGroup->addSpace(); lensGroup->addToggle("rend-vignette", tr("Vignetting")); lensGroup->addLabel(tr("Vignette Darkness:")); lensGroup->addSlider("rend-vignette-darkness", Ranged(0, 2), .01, 2); lensGroup->addLabel(tr("Vignette Width:")); lensGroup->addSlider("rend-vignette-width"); lensGroup->addSpace(); lensGroup->addToggle("rend-halo-realistic", tr("Realistic Halos")); lensGroup->addLabel(tr("Flares per Halo:")); lensGroup->addSlider("rend-halo")->setMinLabel(tr("None")); lensGroup->addLabel(tr("Halo Brightness:")); lensGroup->addSlider("rend-halo-bright", Ranged(0, 100), 1, 0); lensGroup->addLabel(tr("Halo Size Factor:")); lensGroup->addSlider("rend-halo-size"); lensGroup->addLabel(tr("Occlusion Fading:")); lensGroup->addSlider("rend-halo-occlusion", Ranged(1, 256), 1, 0); lensGroup->addLabel(tr("Min Halo Radius:")); lensGroup->addSlider("rend-halo-radius-min", Ranged(1, 80), .1, 1); lensGroup->addLabel(tr("Min Halo Size:")); lensGroup->addSlider("rend-halo-secondary-limit", Ranged(0, 10), .1, 1); lensGroup->addLabel(tr("Halo Fading Start:")); lensGroup->addSlider("rend-halo-dim-near", Ranged(0, 200), .1, 1); lensGroup->addLabel(tr("Halo Fading End:")); lensGroup->addSlider("rend-halo-dim-far", Ranged(0, 200), .1, 1); lensGroup->addLabel(tr("Z-Mag Divisor:")); lensGroup->addSlider("rend-halo-zmag-div", Ranged(1, 100), .1, 1); lensGroup->commit(); // Material settings. matGroup = new Group(this, "material", tr("Materials")); matGroup->addSpace(); matGroup->addToggle("rend-tex-shiny", tr("Shiny Surfaces")); matGroup->addSpace(); matGroup->addToggle("rend-tex-anim-smooth", tr("Smooth Animation")); matGroup->addLabel(tr("Texture Quality:")); matGroup->addSlider("rend-tex-quality"); matGroup->addLabel(tr("Texture Filtering:")); matGroup->addChoice("rend-tex-mipmap")->items() << new ChoiceItem(tr("None"), 0) << new ChoiceItem(tr("Linear filter, no mip"), 1) << new ChoiceItem(tr("No filter, nearest mip"), 2) << new ChoiceItem(tr("Linear filter, nearest mip"), 3) << new ChoiceItem(tr("No filter, linear mip"), 4) << new ChoiceItem(tr("Linear filter, linear mip"), 5); matGroup->addSpace(); matGroup->addToggle("rend-tex-filter-smart", tr("2x Smart Filtering")); matGroup->addLabel(tr("Bilinear Filtering:")); matGroup->addToggle("rend-tex-filter-sprite", tr("Sprites")); matGroup->addSpace(); matGroup->addToggle("rend-tex-filter-mag", tr("World Surfaces")); matGroup->addSpace(); matGroup->addToggle("rend-tex-filter-ui", tr("User Interface")); matGroup->addLabel(tr("Anisotropic Filter:")); matGroup->addChoice("rend-tex-filter-anisotropic")->items() << new ChoiceItem(tr("Best available"), -1) << new ChoiceItem(tr("Off"), 0) << new ChoiceItem(tr("2x"), 1) << new ChoiceItem(tr("4x"), 2) << new ChoiceItem(tr("8x"), 3) << new ChoiceItem(tr("16x"), 4); matGroup->addSpace(); matGroup->addToggle("rend-tex-detail", tr("Detail Textures")); matGroup->addLabel(tr("Scaling Factor:")); matGroup->addSlider("rend-tex-detail-scale", Ranged(0, 16), .01, 2); matGroup->addLabel(tr("Contrast:")); matGroup->addSlider("rend-tex-detail-strength"); matGroup->commit(); // Model settings. modelGroup = new Group(this, "model", tr("3D Models")); modelGroup->addSpace(); modelGroup->addToggle("rend-model", tr("3D Models")); modelGroup->addSpace(); modelGroup->addToggle("rend-model-inter", tr("Interpolate Frames")); modelGroup->addSpace(); modelGroup->addToggle("rend-model-mirror-hud", tr("Mirror Player Weapon")); modelGroup->addLabel(tr("Max Visible Distance:")); modelGroup->addSlider("rend-model-distance", Ranged(0, 3000), 10, 0)->setMinLabel(tr("Inf")); modelGroup->addLabel(tr("LOD #0 Distance:")); modelGroup->addSlider("rend-model-lod", Ranged(0, 1000), 1, 0)->setMinLabel(tr("No LOD")); modelGroup->addLabel(tr("Number of Lights:")); modelGroup->addSlider("rend-model-lights"); modelGroup->commit(); // Sprite settings. spriteGroup = new Group(this, "sprite", tr("Sprites")); spriteGroup->addSpace(); spriteGroup->addToggle("rend-sprite-blend", tr("Additive Blending")); spriteGroup->addLabel(tr("Number of Lights:")); spriteGroup->addSlider("rend-sprite-lights")->setMinLabel(tr("Inf")); spriteGroup->addLabel(tr("Sprite Alignment:")); spriteGroup->addChoice("rend-sprite-align")->items() << new ChoiceItem(tr("Camera"), 0) << new ChoiceItem(tr("View plane"), 1) << new ChoiceItem(tr("Camera (limited)"), 2) << new ChoiceItem(tr("View plane (limited)"), 3); spriteGroup->addSpace(); spriteGroup->addToggle("rend-sprite-mode", tr("Sharp Edges")); spriteGroup->addSpace(); spriteGroup->addToggle("rend-sprite-noz", tr("Disable Z-Write")); spriteGroup->commit(); // Object settings. objectGroup = new Group(this, "object", tr("Objects")); objectGroup->addLabel(tr("Smooth Movement:")); objectGroup->addChoice("rend-mobj-smooth-move")->items() << new ChoiceItem(tr("Disabled"), 0) << new ChoiceItem(tr("Models only"), 1) << new ChoiceItem(tr("Models and sprites"), 2); objectGroup->addSpace(); objectGroup->addToggle("rend-mobj-smooth-turn", tr("Smooth Turning")); objectGroup->commit(); // Particle settings. partGroup = new Group(this, "ptcfx", tr("Particle Effects")); partGroup->addSpace(); partGroup->addToggle("rend-particle", tr("Particle Effects")); partGroup->addLabel(tr("Max Particles:")); partGroup->addSlider("rend-particle-max", Ranged(0, 10000), 100, 0)->setMinLabel(tr("Inf")); partGroup->addLabel(tr("Spawn Rate:")); partGroup->addSlider("rend-particle-rate"); partGroup->addLabel(tr("Diffusion:")); partGroup->addSlider("rend-particle-diffuse", Ranged(0, 20), .01, 2); partGroup->addLabel(tr("Near Clip Distance:")); partGroup->addSlider("rend-particle-visible-near", Ranged(0, 1000), 1, 0)->setMinLabel(tr("None")); partGroup->commit(); // Now we can define the first column width. firstColumnWidth->setSource(maximumOfAllGroupFirstColumns()); } ~Instance() { App::app().audienceForGameChange() -= this; settings.audienceForProfileChange -= this; releaseRef(firstColumnWidth); } void currentGameChanged(game::Game const &newGame) { if(newGame.isNull()) { // Entering Ring Zero -- persistent cvars are not available. self.close(); } } void currentProfileChanged(String const &) { // Update with values from the new profile. fetch(); } Rule const &maximumOfAllGroupFirstColumns() { Rule const *max = 0; foreach(Widget *child, container->childWidgets()) { if(Group *g = child->maybeAs()) { changeRef(max, OperatorRule::maximum(g->firstColumnWidth(), max)); } } return *refless(max); } void fetch() { bool const isReadOnly = settings.isReadOnlyProfile(settings.currentProfile()); foreach(Widget *child, container->childWidgets()) { if(Group *g = child->maybeAs()) { g->fetch(); g->resetButton().enable(!isReadOnly && g->isOpen()); // Enable or disable settings based on read-onlyness. foreach(Widget *w, g->content().childWidgets()) { if(GuiWidget *st = w->maybeAs()) { st->enable(!isReadOnly); } } } } } void foldAll(bool fold) { foreach(Widget *child, container->childWidgets()) { if(Group *g = child->maybeAs()) { if(fold) g->close(0); else g->open(); } } } void saveFoldState(PersistentState &toState) { foreach(Widget *child, container->childWidgets()) { if(Group *g = child->maybeAs()) { toState.names().set(self.name() + "." + g->name() + ".open", g->isOpen()); } } } void restoreFoldState(PersistentState const &fromState) { bool gotState = false; foreach(Widget *child, container->childWidgets()) { if(Group *g = child->maybeAs()) { String const var = self.name() + "." + g->name() + ".open"; if(fromState.names().has(var)) { gotState = true; if(fromState.names().getb(var)) g->open(); else g->close(0); } } } if(!gotState) { // Could be the first time. lightGroup->open(); } } }; RendererAppearanceEditor::RendererAppearanceEditor() : PanelWidget("rendererappearanceeditor"), d(new Instance(this)) { setSizePolicy(Fixed); setOpeningDirection(Left); set(Background(style().colors().colorf("background")).withSolidFillOpacity(1)); d->profile->setOpeningDirection(Down); // Set up the editor UI. LabelWidget *title = LabelWidget::newWithText("Renderer Appearance", d->container); title->setFont("title"); title->setTextColor("accent"); LabelWidget *profLabel = LabelWidget::newWithText(tr("Profile:"), d->container); // Layout. RuleRectangle const &area = d->container->contentRule(); title->rule() .setInput(Rule::Top, area.top()) .setInput(Rule::Left, area.left()); d->close->rule() .setInput(Rule::Right, area.right()) .setInput(Rule::Bottom, title->rule().bottom()); SequentialLayout layout(area.left(), title->rule().bottom(), Down); layout.append(*profLabel, SequentialLayout::IgnoreMinorAxis); d->profile->rule() .setInput(Rule::Left, profLabel->rule().right()) .setInput(Rule::Top, profLabel->rule().top()); layout << d->lightGroup->title() << *d->lightGroup << d->volLightGroup->title() << *d->volLightGroup << d->glowGroup->title() << *d->glowGroup << d->shadowGroup->title() << *d->shadowGroup << d->lensGroup->title() << *d->lensGroup << d->matGroup->title() << *d->matGroup << d->objectGroup->title() << *d->objectGroup << d->modelGroup->title() << *d->modelGroup << d->spriteGroup->title() << *d->spriteGroup << d->partGroup->title() << *d->partGroup << d->skyGroup->title() << *d->skyGroup; // Update container size. d->container->setContentSize(OperatorRule::maximum(layout.width(), profLabel->rule().width() + d->profile->rule().width() + d->profile->button().rule().width(), style().rules().rule("rendererappearance.width")), title->rule().height() + layout.height()); d->container->rule().setSize(d->container->contentRule().width() + d->container->margins().width(), rule().height()); setContent(d->container); d->fetch(); // Install the editor. ClientWindow::main().setSidebar(ClientWindow::RightEdge, this); } void RendererAppearanceEditor::foldAll() { d->foldAll(true); } void RendererAppearanceEditor::unfoldAll() { d->foldAll(false); } void RendererAppearanceEditor::operator >> (PersistentState &toState) const { d->saveFoldState(toState); } void RendererAppearanceEditor::operator << (PersistentState const &fromState) { d->restoreFoldState(fromState); } void RendererAppearanceEditor::preparePanelForOpening() { PanelWidget::preparePanelForOpening(); } void RendererAppearanceEditor::panelDismissed() { PanelWidget::panelDismissed(); ClientWindow::main().unsetSidebar(ClientWindow::RightEdge); } doomsday-stable-1.15.7/doomsday/client/src/ui/progress.cpp0000664000175000017500000000363212641367670023051 0ustar jaakkojaakko/** @file progress.cpp Simple wrapper for the Busy progress bar. * @ingroup console * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "ui/progress.h" #include "ui/clientwindow.h" #include "ui/widgets/busywidget.h" #include using namespace de; // Time for the progress to reach the new target (seconds). static de::TimeDelta const PROGRESS_DELTA_TIME = 0.5; static ProgressWidget &progress() { return ClientWindow::main().busy().progress(); } void Con_InitProgress2(int maxProgress, float start, float end) { progress().setRange(Rangei(0, maxProgress), Rangef(start, end)); progress().setProgress(0, 0); } void Con_InitProgress(int maxProgress) { Con_InitProgress2(maxProgress, 0.f, 1.f); } dd_bool Con_IsProgressAnimationCompleted(void) { return !progress().isAnimating(); } void Con_SetProgress(int progressValue) { progress().setProgress(progressValue, progressValue < progress().range().end? PROGRESS_DELTA_TIME : TimeDelta(PROGRESS_DELTA_TIME / 2)); } doomsday-stable-1.15.7/doomsday/client/src/ui/ui_main.cpp0000664000175000017500000003354412641367670022633 0ustar jaakkojaakko/** @file ui_main.cpp Graphical User Interface * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include "de_base.h" #include "de_console.h" #include "de_filesys.h" #include "de_system.h" #include "de_graphics.h" #include "de_ui.h" #include "de_misc.h" #include "clientapp.h" #include "api_fontrender.h" #include "resource/image.h" #include "gl/texturecontent.h" #include "render/rend_main.h" #include "render/rend_font.h" #include "MaterialAnimator" #include "ui/ui_main.h" using namespace de; D_CMD(UIColor); fontid_t fontFixed, fontVariable[FONTSTYLE_COUNT]; static int uiFontHgt; /// Height of the UI font. /// Modify these colors to change the look of the UI. static ui_color_t ui_colors[NUM_UI_COLORS] = { /* UIC_TEXT */ { .85f, .87f, 1 }, /* UIC_TITLE */ { 1, 1, 1 }, /* UIC_SHADOW */ { 0, 0, 0 }, /* UIC_BG_LIGHT */ { .18f, .18f, .22f }, /* UIC_BG_MEDIUM */ { .4f, .4f, .52f }, /* UIC_BG_DARK */ { .28f, .28f, .33f }, /* UIC_BRD_HI */ { 1, 1, 1 }, /* UIC_BRD_MED */ { 0, 0, 0 }, /* UIC_BRD_LOW */ { .25f, .25f, .55f }, /* UIC_HELP */ { .4f, .4f, .52f } }; static inline ResourceSystem &resSys() { return ClientApp::resourceSystem(); } void UI_Register(void) { C_CMD_FLAGS("uicolor", "sfff", UIColor, CMDF_NO_DEDICATED); } char const *UI_ChooseFixedFont() { if(DENG_GAMEVIEW_WIDTH < 300) return "console11"; if(DENG_GAMEVIEW_WIDTH > 768) return "console18"; return "console14"; } char const *UI_ChooseVariableFont(fontstyle_t style) { int const resY = DENG_GAMEVIEW_HEIGHT; int const SMALL_LIMIT = 500; int const MED_LIMIT = 800; switch(style) { default: return (resY < SMALL_LIMIT ? "normal12" : resY < MED_LIMIT ? "normal18" : "normal24"); case FS_LIGHT: return (resY < SMALL_LIMIT ? "normallight12" : resY < MED_LIMIT ? "normallight18" : "normallight24"); case FS_BOLD: return (resY < SMALL_LIMIT ? "normalbold12" : resY < MED_LIMIT ? "normalbold18" : "normalbold24"); } } static AbstractFont *loadSystemFont(char const *name) { DENG2_ASSERT(name != 0 && name[0]); // Compose the resource name. de::Uri uri = de::Uri("System:", RC_NULL).setPath(name); // Compose the resource data path. ddstring_t resourcePath; Str_InitStd(&resourcePath); Str_Appendf(&resourcePath, "}data/Fonts/%s.dfn", name); #if defined(UNIX) && !defined(MACOSX) // Case-sensitive file system. /// @todo Unkludge this: handle in a more generic manner. strlwr(resourcePath.str); #endif F_ExpandBasePath(&resourcePath, &resourcePath); AbstractFont *font = resSys().newFontFromFile(uri, Str_Text(&resourcePath)); if(!font) { App_Error("loadSystemFont: Failed loading font \"%s\".", name); exit(1); // Unreachable. } Str_Free(&resourcePath); return font; } static void loadFontIfNeeded(char const *uri, fontid_t *fid) { *fid = NOFONTID; if(uri && uri[0]) { try { FontManifest &manifest = resSys().fontManifest(de::Uri(uri, RC_NULL)); if(manifest.hasResource()) { *fid = fontid_t(manifest.uniqueId()); } } catch(ResourceSystem::MissingManifestError const &) {} } if(*fid == NOFONTID) { *fid = loadSystemFont(uri)->manifest().uniqueId(); } } void UI_LoadFonts() { if(isDedicated) return; loadFontIfNeeded(UI_ChooseFixedFont(), &fontFixed); loadFontIfNeeded(UI_ChooseVariableFont(FS_NORMAL), &fontVariable[FS_NORMAL]); loadFontIfNeeded(UI_ChooseVariableFont(FS_BOLD), &fontVariable[FS_BOLD]); loadFontIfNeeded(UI_ChooseVariableFont(FS_LIGHT), &fontVariable[FS_LIGHT]); } de::MaterialVariantSpec const &UI_MaterialSpec(int texSpecFlags) { return resSys().materialSpec(UiContext, texSpecFlags | TSF_NO_COMPRESSION, 0, 0, 0, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 1, 1, 0, false, false, false, false); } int UI_FontHeight(void) { return uiFontHgt; } ui_color_t* UI_Color(uint id) { if(id >= NUM_UI_COLORS) return NULL; return &ui_colors[id]; } void UI_MixColors(ui_color_t* a, ui_color_t* b, ui_color_t* dest, float amount) { dest->red = (1 - amount) * a->red + amount * b->red; dest->green = (1 - amount) * a->green + amount * b->green; dest->blue = (1 - amount) * a->blue + amount * b->blue; } void UI_SetColorA(ui_color_t* color, float alpha) { glColor4f(color->red, color->green, color->blue, alpha); } void UI_SetColor(ui_color_t* color) { glColor3f(color->red, color->green, color->blue); } void UI_Gradient(const Point2Raw* origin, const Size2Raw* size, ui_color_t* topColor, ui_color_t* bottomColor, float topAlpha, float bottomAlpha) { UI_GradientEx(origin, size, 0, topColor, bottomColor, topAlpha, bottomAlpha); } void UI_GradientEx(const Point2Raw* origin, const Size2Raw* size, int border, ui_color_t* topColor, ui_color_t* bottomColor, float topAlpha, float bottomAlpha) { UI_DrawRectEx(origin, size, border, true, topColor, bottomColor, topAlpha, bottomAlpha); } void UI_TextOutEx2(const char* text, const Point2Raw* origin, ui_color_t* color, float alpha, int alignFlags, short textFlags) { assert(origin); //alpha *= uiAlpha; if(alpha <= 0) return; FR_SetColorAndAlpha(color->red, color->green, color->blue, alpha); FR_DrawText3(text, origin, alignFlags, textFlags); } void UI_TextOutEx(const char* text, const Point2Raw* origin, ui_color_t* color, float alpha) { UI_TextOutEx2(text, origin, color, alpha, DEFAULT_ALIGNFLAGS, DEFAULT_DRAWFLAGS); } void UI_DrawRectEx(const Point2Raw* origin, const Size2Raw* size, int border, dd_bool filled, ui_color_t* topColor, ui_color_t* bottomColor, float alpha, float bottomAlpha) { DENG2_ASSERT(origin && size); float s[2] = { 0, 1 }; float t[2] = { 0, 1 }; //alpha *= uiAlpha; //bottomAlpha *= uiAlpha; if(alpha <= 0 && bottomAlpha <= 0) return; if(border < 0) { border = -border; s[0] = t[0] = 1; s[1] = t[1] = 0; } if(bottomAlpha < 0) bottomAlpha = alpha; if(!bottomColor) bottomColor = topColor; MaterialAnimator &matAnimator = resSys().material(de::Uri("System", (filled? Path("ui/boxfill") : Path("ui/boxcorner")))) .getAnimator(UI_MaterialSpec()); // Ensure we've up to date info about the material. matAnimator.prepare(); GL_BindTexture(matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture); // The fill comes first, if there's one. glBegin(GL_QUADS); if(filled) { glTexCoord2f(0.5f, 0.5f); UI_SetColorA(topColor, alpha); glVertex2f(origin->x + border, origin->y + border); glVertex2f(origin->x + size->width - border, origin->y + border); UI_SetColorA(bottomColor, bottomAlpha); glVertex2f(origin->x + size->width - border, origin->y + size->height - border); glVertex2f(origin->x + border, origin->y + size->height - border); } if(!filled || border > 0) { // Top Left. UI_SetColorA(topColor, alpha); glTexCoord2f(s[0], t[0]); glVertex2f(origin->x, origin->y); glTexCoord2f(0.5f, t[0]); glVertex2f(origin->x + border, origin->y); glTexCoord2f(0.5f, 0.5f); glVertex2f(origin->x + border, origin->y + border); glTexCoord2f(s[0], 0.5f); glVertex2f(origin->x, origin->y + border); // Top. glTexCoord2f(0.5f, t[0]); glVertex2f(origin->x + border, origin->y); glTexCoord2f(0.5f, t[0]); glVertex2f(origin->x + size->width - border, origin->y); glTexCoord2f(0.5f, 0.5f); glVertex2f(origin->x + size->width - border, origin->y + border); glTexCoord2f(0.5f, 0.5f); glVertex2f(origin->x + border, origin->y + border); // Top Right. glTexCoord2f(0.5f, t[0]); glVertex2f(origin->x + size->width - border, origin->y); glTexCoord2f(s[1], t[0]); glVertex2f(origin->x + size->width, origin->y); glTexCoord2f(s[1], 0.5f); glVertex2f(origin->x + size->width, origin->y + border); glTexCoord2f(0.5f, 0.5f); glVertex2f(origin->x + size->width - border, origin->y + border); // Right. glTexCoord2f(0.5f, 0.5f); glVertex2f(origin->x + size->width - border, origin->y + border); glTexCoord2f(s[1], 0.5f); glVertex2f(origin->x + size->width, origin->y + border); UI_SetColorA(bottomColor, bottomAlpha); glTexCoord2f(s[1], 0.5f); glVertex2f(origin->x + size->width, origin->y + size->height - border); glTexCoord2f(0.5f, 0.5f); glVertex2f(origin->x + size->width - border, origin->y + size->height - border); // Bottom Right. glTexCoord2f(0.5f, 0.5f); glVertex2f(origin->x + size->width - border, origin->y + size->height - border); glTexCoord2f(s[1], 0.5f); glVertex2f(origin->x + size->width, origin->y + size->height - border); glTexCoord2f(s[1], t[1]); glVertex2f(origin->x + size->width, origin->y + size->height); glTexCoord2f(0.5f, t[1]); glVertex2f(origin->x + size->width - border, origin->y + size->height); // Bottom. glTexCoord2f(0.5f, 0.5f); glVertex2f(origin->x + border, origin->y + size->height - border); glTexCoord2f(0.5f, 0.5f); glVertex2f(origin->x + size->width - border, origin->y + size->height - border); glTexCoord2f(0.5f, t[1]); glVertex2f(origin->x + size->width - border, origin->y + size->height); glTexCoord2f(0.5f, t[1]); glVertex2f(origin->x + border, origin->y + size->height); // Bottom Left. glTexCoord2f(s[0], 0.5f); glVertex2f(origin->x, origin->y + size->height - border); glTexCoord2f(0.5f, 0.5f); glVertex2f(origin->x + border, origin->y + size->height - border); glTexCoord2f(0.5f, t[1]); glVertex2f(origin->x + border, origin->y + size->height); glTexCoord2f(s[0], t[1]); glVertex2f(origin->x, origin->y + size->height); // Left. UI_SetColorA(topColor, alpha); glTexCoord2f(s[0], 0.5f); glVertex2f(origin->x, origin->y + border); glTexCoord2f(0.5f, 0.5f); glVertex2f(origin->x + border, origin->y + border); UI_SetColorA(bottomColor, bottomAlpha); glTexCoord2f(0.5f, 0.5f); glVertex2f(origin->x + border, origin->y + size->height - border); glTexCoord2f(s[0], 0.5f); glVertex2f(origin->x, origin->y + size->height - border); } glEnd(); } void UI_DrawDDBackground(Point2Raw const &origin, Size2Raw const &dimensions, float alpha) { /* ui_color_t const *dark = UI_Color(UIC_BG_DARK); ui_color_t const *light = UI_Color(UIC_BG_LIGHT); float const mul = 1.5f; // Background gradient picture. MaterialSnapshot const &ms = resSys().material(de::Uri("System", Path("ui/background"))) .prepare(UI_MaterialSpec(TSF_MONOCHROME)); GL_BindTexture(&ms.texture(MTU_PRIMARY)); */ glDisable(GL_TEXTURE_2D); if(alpha < 1.0) { GL_BlendMode(BM_NORMAL); } else { glDisable(GL_BLEND); } glColor4f(0, 0, 0, alpha); glBegin(GL_QUADS); // Top color. //glColor4f(dark->red * mul, dark->green * mul, dark->blue * mul, alpha); //glTexCoord2f(0, 0); glVertex2f(origin.x, origin.y); //glTexCoord2f(1, 0); glVertex2f(origin.x + dimensions.width, origin.y); // Bottom color. //glColor4f(light->red * mul, light->green * mul, light->blue * mul, alpha); //glTexCoord2f(1, 1); glVertex2f(origin.x + dimensions.width, origin.y + dimensions.height); //glTexCoord2f(0, 1); glVertex2f(0, origin.y + dimensions.height); glEnd(); glEnable(GL_BLEND); glDisable(GL_TEXTURE_2D); } /** * CCmd: Change the UI colors. */ D_CMD(UIColor) { DENG2_UNUSED2(argc, src); struct colorname_s { char const *str; uint colorIdx; } colors[] = { { "text", UIC_TEXT }, { "title", UIC_TITLE }, { "shadow", UIC_SHADOW }, { "bglight", UIC_BG_LIGHT }, { "bgmed", UIC_BG_MEDIUM }, { "bgdark", UIC_BG_DARK }, { "borhigh", UIC_BRD_HI }, { "bormed", UIC_BRD_MED }, { "borlow", UIC_BRD_LOW }, { "help", UIC_HELP }, { 0, 0 } }; for(int i = 0; colors[i].str; ++i) { if(stricmp(argv[1], colors[i].str)) continue; uint idx = colors[i].colorIdx; ui_colors[idx].red = strtod(argv[2], 0); ui_colors[idx].green = strtod(argv[3], 0); ui_colors[idx].blue = strtod(argv[4], 0); return true; } LOG_SCR_ERROR("Unknown UI color '%s'") << argv[1]; return false; } doomsday-stable-1.15.7/doomsday/client/src/ui/infine/0000775000175000017500000000000012641367670021745 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/ui/infine/finale.cpp0000664000175000017500000000707612641367670023721 0ustar jaakkojaakko/** @file finale.cpp InFine animation system, Finale script. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include "ui/infine/finale.h" #include "BindContext" #include "ui/infine/finaleinterpreter.h" #include "network/net_main.h" #ifdef __SERVER__ # include "server/sv_infine.h" #endif using namespace de; DENG2_PIMPL(Finale) { bool active; int flags; ///< @ref finaleFlags finaleid_t id; FinaleInterpreter interpreter; Instance(Public *i, int flags, finaleid_t id) : Base(i) , active (false) , flags (flags) , id (id) , interpreter(id) {} ~Instance() { DENG2_FOR_PUBLIC_AUDIENCE2(Deletion, i) i->finaleBeingDeleted(self); } void loadScript(String const &script) { if(script.isEmpty()) return; LOGDEV_SCR_MSG("Begin Finale - id:%i '%.30s'") << id << script; Block const scriptAsUtf8 = script.toUtf8(); interpreter.loadScript(scriptAsUtf8.constData()); #ifdef __SERVER__ if(!(flags & FF_LOCAL) && ::isServer) { // Instruct clients to start playing this Finale. Sv_Finale(id, FINF_BEGIN | FINF_SCRIPT, scriptAsUtf8.constData()); } #endif active = true; } DENG2_PIMPL_AUDIENCE(Deletion) }; DENG2_AUDIENCE_METHOD(Finale, Deletion) Finale::Finale(int flags, finaleid_t id, String const &script) : d(new Instance(this, flags, id)) { d->loadScript(script); } int Finale::flags() const { return d->flags; } finaleid_t Finale::id() const { return d->id; } bool Finale::isActive() const { return d->active; } bool Finale::isSuspended() const { return d->interpreter.isSuspended(); } void Finale::resume() { d->active = true; d->interpreter.resume(); } void Finale::suspend() { d->active = false; d->interpreter.suspend(); } bool Finale::terminate() { if(!d->active) return false; LOGDEV_SCR_VERBOSE("Terminating finaleid %i") << d->id; d->active = false; d->interpreter.terminate(); return true; } bool Finale::runTicks(timespan_t timeDelta) { if(d->interpreter.runTicks(timeDelta, d->active && DD_IsSharpTick())) { // The script has ended! terminate(); return true; } return false; } int Finale::handleEvent(ddevent_t const &ev) { if(!d->active) return false; return d->interpreter.handleEvent(ev); } bool Finale::requestSkip() { if(!d->active) return false; return d->interpreter.skip(); } bool Finale::isMenuTrigger() const { if(!d->active) return false; LOG_SCR_XVERBOSE("IsMenuTrigger: %i") << d->interpreter.isMenuTrigger(); return d->interpreter.isMenuTrigger(); } FinaleInterpreter const &Finale::interpreter() const { return d->interpreter; } doomsday-stable-1.15.7/doomsday/client/src/ui/infine/finalewidget.cpp0000664000175000017500000000715712641367670025125 0ustar jaakkojaakko/** @file finalewidget.cpp InFine animation system, FinaleWidget. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "ui/infine/finalewidget.h" #include "ui/infine/finalepagewidget.h" using namespace de; DENG2_PIMPL_NOREF(FinaleWidget) { Id id; String name; animatorvector3_t pos; animator_t angle; animatorvector3_t scale; FinalePageWidget *page = nullptr; Instance() { AnimatorVector3_Init(pos, 0, 0, 0); Animator_Init(&angle, 0); AnimatorVector3_Init(scale, 1, 1, 1); } }; FinaleWidget::FinaleWidget(de::String const &name) : d(new Instance) { setName(name); } FinaleWidget::~FinaleWidget() { DENG2_FOR_AUDIENCE(Deletion, i) i->finaleWidgetBeingDeleted(*this); } Id FinaleWidget::id() const { return d->id; } String FinaleWidget::name() const { return d->name; } FinaleWidget &FinaleWidget::setName(String const &newName) { d->name = newName; return *this; } animatorvector3_t const &FinaleWidget::origin() const { return d->pos; } FinaleWidget &FinaleWidget::setOrigin(Vector3f const &newPos, int steps) { AnimatorVector3_Set(d->pos, newPos.x, newPos.y, newPos.z, steps); return *this; } FinaleWidget &FinaleWidget::setOriginX(float newX, int steps) { Animator_Set(&d->pos[0], newX, steps); return *this; } FinaleWidget &FinaleWidget::setOriginY(float newY, int steps) { Animator_Set(&d->pos[1], newY, steps); return *this; } FinaleWidget &FinaleWidget::setOriginZ(float newZ, int steps) { Animator_Set(&d->pos[2], newZ, steps); return *this; } animator_t const &FinaleWidget::angle() const { return d->angle; } FinaleWidget &FinaleWidget::setAngle(float newAngle, int steps) { Animator_Set(&d->angle, newAngle, steps); return *this; } animatorvector3_t const &FinaleWidget::scale() const { return d->scale; } FinaleWidget &FinaleWidget::setScale(Vector3f const &newScale, int steps) { AnimatorVector3_Set(d->scale, newScale.x, newScale.y, newScale.z, steps); return *this; } FinaleWidget &FinaleWidget::setScaleX(float newXScale, int steps) { Animator_Set(&d->scale[0], newXScale, steps); return *this; } FinaleWidget &FinaleWidget::setScaleY(float newYScale, int steps) { Animator_Set(&d->scale[1], newYScale, steps); return *this; } FinaleWidget &FinaleWidget::setScaleZ(float newZScale, int steps) { Animator_Set(&d->scale[2], newZScale, steps); return *this; } FinalePageWidget *FinaleWidget::page() const { return d->page; } FinaleWidget &FinaleWidget::setPage(FinalePageWidget *newPage) { d->page = newPage; return *this; } void FinaleWidget::runTicks(/*timespan_t timeDelta*/) { AnimatorVector3_Think(d->pos); AnimatorVector3_Think(d->scale); Animator_Think(&d->angle); } doomsday-stable-1.15.7/doomsday/client/src/ui/infine/finaleinterpreter.cpp0000664000175000017500000017575712641367670026221 0ustar jaakkojaakko/** @file finaleinterpreter.cpp InFine animation system Finale script interpreter. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include #include #include "de_base.h" #include "ui/infine/finaleinterpreter.h" #include "de_filesys.h" #include "de_ui.h" #include "dd_main.h" #include "dd_def.h" #include "Game" #include "api_material.h" #include "api_render.h" #include "api_resource.h" #include "audio/s_main.h" #include "network/net_main.h" #include "ui/infine/finalewidget.h" #include "ui/infine/finaleanimwidget.h" #include "ui/infine/finalepagewidget.h" #include "ui/infine/finaletextwidget.h" #ifdef __CLIENT__ # include "api_fontrender.h" # include "client/cl_infine.h" # include "gl/gl_main.h" # include "gl/gl_texmanager.h" # include "gl/texturecontent.h" # include "gl/sys_opengl.h" // TODO: get rid of this #endif #ifdef __SERVER__ # include "server/sv_infine.h" #endif #define MAX_TOKEN_LENGTH (8192) #define FRACSECS_TO_TICKS(sec) (int(sec * TICSPERSEC + 0.5)) using namespace de; enum fi_operand_type_t { FVT_INT, FVT_FLOAT, FVT_STRING, FVT_URI }; static fi_operand_type_t operandTypeForCharCode(char code) { switch(code) { case 'i': return FVT_INT; case 'f': return FVT_FLOAT; case 's': return FVT_STRING; case 'u': return FVT_URI; default: throw Error("operandTypeForCharCode", String("Unknown char-code %1").arg(code)); } } // Helper macro for accessing the value of an operand. #define OP_INT(n) (ops[n].data.integer) #define OP_FLOAT(n) (ops[n].data.flt) #define OP_CSTRING(n) (ops[n].data.cstring) #define OP_URI(n) (ops[n].data.uri) struct fi_operand_t { fi_operand_type_t type; union { int integer; float flt; char const *cstring; uri_s *uri; } data; }; // Helper macro for defining infine command functions. #define DEFFC(name) void FIC_##name(command_t const &cmd, fi_operand_t const *ops, FinaleInterpreter &fi) /** * @defgroup finaleInterpreterCommandDirective Finale Interpreter Command Directive * @ingroup infine */ /*@{*/ #define FID_NORMAL 0 #define FID_ONLOAD 0x1 /*@}*/ struct command_t { char const *token; char const *operands; typedef void (*Func) (command_t const &, fi_operand_t const *, FinaleInterpreter &); Func func; struct command_flags_s { char when_skipping:1; char when_condition_skipping:1; // Skipping because condition failed. } flags; /// Command execution directives NOT supported by this command. /// @see finaleInterpreterCommandDirective int excludeDirectives; }; // Command functions. DEFFC(Do); DEFFC(End); DEFFC(BGMaterial); DEFFC(NoBGMaterial); DEFFC(Wait); DEFFC(WaitText); DEFFC(WaitAnim); DEFFC(Tic); DEFFC(InTime); DEFFC(Color); DEFFC(ColorAlpha); DEFFC(Pause); DEFFC(CanSkip); DEFFC(NoSkip); DEFFC(SkipHere); DEFFC(Events); DEFFC(NoEvents); DEFFC(OnKey); DEFFC(UnsetKey); DEFFC(If); DEFFC(IfNot); DEFFC(Else); DEFFC(GoTo); DEFFC(Marker); DEFFC(Image); DEFFC(ImageAt); DEFFC(XImage); DEFFC(Delete); DEFFC(Patch); DEFFC(SetPatch); DEFFC(Anim); DEFFC(AnimImage); DEFFC(StateAnim); DEFFC(Repeat); DEFFC(ClearAnim); DEFFC(PicSound); DEFFC(ObjectOffX); DEFFC(ObjectOffY); DEFFC(ObjectOffZ); DEFFC(ObjectScaleX); DEFFC(ObjectScaleY); DEFFC(ObjectScaleZ); DEFFC(ObjectScale); DEFFC(ObjectScaleXY); DEFFC(ObjectScaleXYZ); DEFFC(ObjectRGB); DEFFC(ObjectAlpha); DEFFC(ObjectAngle); DEFFC(Rect); DEFFC(FillColor); DEFFC(EdgeColor); DEFFC(OffsetX); DEFFC(OffsetY); DEFFC(Sound); DEFFC(SoundAt); DEFFC(SeeSound); DEFFC(DieSound); DEFFC(Music); DEFFC(MusicOnce); DEFFC(Filter); DEFFC(Text); DEFFC(TextFromDef); DEFFC(TextFromLump); DEFFC(SetText); DEFFC(SetTextDef); DEFFC(DeleteText); DEFFC(Font); DEFFC(FontA); DEFFC(FontB); DEFFC(PredefinedColor); DEFFC(PredefinedFont); DEFFC(TextRGB); DEFFC(TextAlpha); DEFFC(TextOffX); DEFFC(TextOffY); DEFFC(TextCenter); DEFFC(TextNoCenter); DEFFC(TextScroll); DEFFC(TextPos); DEFFC(TextRate); DEFFC(TextLineHeight); DEFFC(NoMusic); DEFFC(TextScaleX); DEFFC(TextScaleY); DEFFC(TextScale); DEFFC(PlayDemo); DEFFC(Command); DEFFC(ShowMenu); DEFFC(NoShowMenu); /** * Time is measured in seconds. * Colors are floating point and [0..1]. * * @todo This data should be pre-processed (i.e., parsed) during module init * and any symbolic references or other indirections resolved once * rather than repeatedly during script interpretation. * * At this time the command names could also be hashed and chained to * improve performance. -ds */ static command_t const *findCommand(char const *name) { static command_t const commands[] = { // Run Control { "DO", "", FIC_Do, true, true }, { "END", "", FIC_End }, { "IF", "s", FIC_If }, // if (value-id) { "IFNOT", "s", FIC_IfNot }, // ifnot (value-id) { "ELSE", "", FIC_Else }, { "GOTO", "s", FIC_GoTo }, // goto (marker) { "MARKER", "s", FIC_Marker, true }, { "in", "f", FIC_InTime }, // in (time) { "pause", "", FIC_Pause }, { "tic", "", FIC_Tic }, { "wait", "f", FIC_Wait }, // wait (time) { "waittext", "s", FIC_WaitText }, // waittext (id) { "waitanim", "s", FIC_WaitAnim }, // waitanim (id) { "canskip", "", FIC_CanSkip }, { "noskip", "", FIC_NoSkip }, { "skiphere", "", FIC_SkipHere, true }, { "events", "", FIC_Events }, { "noevents", "", FIC_NoEvents }, { "onkey", "ss", FIC_OnKey }, // onkey (keyname) (marker) { "unsetkey", "s", FIC_UnsetKey }, // unsetkey (keyname) // Screen Control { "color", "fff", FIC_Color }, // color (red) (green) (blue) { "coloralpha", "ffff", FIC_ColorAlpha }, // coloralpha (r) (g) (b) (a) { "flat", "u(flats:)", FIC_BGMaterial }, // flat (flat-id) { "texture", "u(textures:)", FIC_BGMaterial }, // texture (texture-id) { "noflat", "", FIC_NoBGMaterial }, { "notexture", "", FIC_NoBGMaterial }, { "offx", "f", FIC_OffsetX }, // offx (x) { "offy", "f", FIC_OffsetY }, // offy (y) { "filter", "ffff", FIC_Filter }, // filter (r) (g) (b) (a) // Audio { "sound", "s", FIC_Sound }, // sound (snd) { "soundat", "sf", FIC_SoundAt }, // soundat (snd) (vol:0-1) { "seesound", "s", FIC_SeeSound }, // seesound (mobjtype) { "diesound", "s", FIC_DieSound }, // diesound (mobjtype) { "music", "s", FIC_Music }, // music (musicname) { "musiconce", "s", FIC_MusicOnce }, // musiconce (musicname) { "nomusic", "", FIC_NoMusic }, // Objects { "del", "s", FIC_Delete }, // del (wi) { "x", "sf", FIC_ObjectOffX }, // x (wi) (x) { "y", "sf", FIC_ObjectOffY }, // y (wi) (y) { "z", "sf", FIC_ObjectOffZ }, // z (wi) (z) { "sx", "sf", FIC_ObjectScaleX }, // sx (wi) (x) { "sy", "sf", FIC_ObjectScaleY }, // sy (wi) (y) { "sz", "sf", FIC_ObjectScaleZ }, // sz (wi) (z) { "scale", "sf", FIC_ObjectScale }, // scale (wi) (factor) { "scalexy", "sff", FIC_ObjectScaleXY }, // scalexy (wi) (x) (y) { "scalexyz", "sfff", FIC_ObjectScaleXYZ }, // scalexyz (wi) (x) (y) (z) { "rgb", "sfff", FIC_ObjectRGB }, // rgb (wi) (r) (g) (b) { "alpha", "sf", FIC_ObjectAlpha }, // alpha (wi) (alpha) { "angle", "sf", FIC_ObjectAngle }, // angle (wi) (degrees) // Rects { "rect", "sffff", FIC_Rect }, // rect (hndl) (x) (y) (w) (h) { "fillcolor", "ssffff", FIC_FillColor }, // fillcolor (wi) (top/bottom/both) (r) (g) (b) (a) { "edgecolor", "ssffff", FIC_EdgeColor }, // edgecolor (wi) (top/bottom/both) (r) (g) (b) (a) // Pics { "image", "ss", FIC_Image }, // image (id) (raw-image-lump) { "imageat", "sffs", FIC_ImageAt }, // imageat (id) (x) (y) (raw) { "ximage", "ss", FIC_XImage }, // ximage (id) (ext-gfx-filename) { "patch", "sffs", FIC_Patch }, // patch (id) (x) (y) (patch) { "set", "ss", FIC_SetPatch }, // set (id) (lump) { "clranim", "s", FIC_ClearAnim }, // clranim (wi) { "anim", "ssf", FIC_Anim }, // anim (id) (patch) (time) { "imageanim", "ssf", FIC_AnimImage }, // imageanim (id) (raw-img) (time) { "picsound", "ss", FIC_PicSound }, // picsound (id) (sound) { "repeat", "s", FIC_Repeat }, // repeat (id) { "states", "ssi", FIC_StateAnim }, // states (id) (state) (count) // Text { "text", "sffs", FIC_Text }, // text (hndl) (x) (y) (string) { "textdef", "sffs", FIC_TextFromDef }, // textdef (hndl) (x) (y) (txt-id) { "textlump", "sffs", FIC_TextFromLump }, // textlump (hndl) (x) (y) (lump) { "settext", "ss", FIC_SetText }, // settext (id) (newtext) { "settextdef", "ss", FIC_SetTextDef }, // settextdef (id) (txt-id) { "center", "s", FIC_TextCenter }, // center (id) { "nocenter", "s", FIC_TextNoCenter }, // nocenter (id) { "scroll", "sf", FIC_TextScroll }, // scroll (id) (speed) { "pos", "si", FIC_TextPos }, // pos (id) (pos) { "rate", "si", FIC_TextRate }, // rate (id) (rate) { "font", "su", FIC_Font }, // font (id) (font) { "linehgt", "sf", FIC_TextLineHeight }, // linehgt (hndl) (hgt) // Game Control { "playdemo", "s", FIC_PlayDemo }, // playdemo (filename) { "cmd", "s", FIC_Command }, // cmd (console command) { "trigger", "", FIC_ShowMenu }, { "notrigger", "", FIC_NoShowMenu }, // Misc. { "precolor", "ifff", FIC_PredefinedColor }, // precolor (num) (r) (g) (b) { "prefont", "iu", FIC_PredefinedFont }, // prefont (num) (font) // Deprecated Font commands { "fonta", "s", FIC_FontA }, // fonta (id) { "fontb", "s", FIC_FontB }, // fontb (id) // Deprecated Pic commands { "delpic", "s", FIC_Delete }, // delpic (wi) // Deprecated Text commands { "deltext", "s", FIC_DeleteText }, // deltext (wi) { "textrgb", "sfff", FIC_TextRGB }, // textrgb (id) (r) (g) (b) { "textalpha", "sf", FIC_TextAlpha }, // textalpha (id) (alpha) { "tx", "sf", FIC_TextOffX }, // tx (id) (x) { "ty", "sf", FIC_TextOffY }, // ty (id) (y) { "tsx", "sf", FIC_TextScaleX }, // tsx (id) (x) { "tsy", "sf", FIC_TextScaleY }, // tsy (id) (y) { "textscale", "sf", FIC_TextScale }, // textscale (id) (x) (y) { nullptr, 0, nullptr } // Terminate. }; for(size_t i = 0; commands[i].token; ++i) { command_t const *cmd = &commands[i]; if(!qstricmp(cmd->token, name)) { return cmd; } } return nullptr; // Not found. } DENG2_PIMPL(FinaleInterpreter) { struct Flags { char stopped:1; char suspended:1; char paused:1; char can_skip:1; char eat_events:1; /// Script will eat all input events. char show_menu:1; } flags; finaleid_t id = 0; ///< Unique identifier. ddstring_t *script = nullptr; ///< The script to be interpreted. char const *scriptBegin = nullptr; ///< Beginning of the script (after any directive blocks). char const *cp = nullptr; ///< Current position in the script. char token[MAX_TOKEN_LENGTH]; ///< Script token read/parse buffer. /// Pages containing the widgets used to visualize the script objects. std::unique_ptr pages[2]; bool cmdExecuted = false; ///< Set to true after first command is executed. bool skipping = false; bool lastSkipped = false; bool skipNext = false; bool gotoEnd = false; bool gotoSkip = false; String gotoTarget; int doLevel = 0; ///< Level of DO-skipping. uint timer = 0; int wait = 0; int inTime = 0; FinaleAnimWidget *waitAnim = nullptr; FinaleTextWidget *waitText = nullptr; #ifdef __CLIENT__ struct EventHandler { ddevent_t ev; // Template. String gotoMarker; explicit EventHandler(ddevent_t const *evTemplate = nullptr, String const &gotoMarker = "") : gotoMarker(gotoMarker) { std::memcpy(&ev, &evTemplate, sizeof(ev)); } EventHandler(EventHandler const &other) : gotoMarker(other.gotoMarker) { std::memcpy(&ev, &other.ev, sizeof(ev)); } }; typedef QList EventHandlers; EventHandlers eventHandlers; #endif // __CLIENT__ Instance(Public *i, finaleid_t id) : Base(i), id(id) { de::zap(flags); de::zap(token); } ~Instance() { stop(); releaseScript(); } void initDefaultState() { flags.suspended = false; flags.paused = false; flags.show_menu = true; // Unhandled events will show a menu. flags.can_skip = true; // By default skipping is enabled. cmdExecuted = false; // Nothing is drawn until a cmd has been executed. skipping = false; wait = 0; // Not waiting for anything. inTime = 0; // Interpolation is off. timer = 0; gotoSkip = false; gotoEnd = false; skipNext = false; waitText = nullptr; waitAnim = nullptr; gotoTarget.clear(); #ifdef __CLIENT__ eventHandlers.clear(); #endif } void releaseScript() { Str_Delete(script); script = nullptr; scriptBegin = nullptr; cp = nullptr; } void stop() { if(flags.stopped) return; flags.stopped = true; LOGDEV_SCR_MSG("Finale End - id:%i '%.30s'") << id << scriptBegin; #ifdef __SERVER__ if(::isServer && !(FI_ScriptFlags(id) & FF_LOCAL)) { // Tell clients to stop the finale. Sv_Finale(id, FINF_END, 0); } #endif // Any hooks? DD_CallHooks(HOOK_FINALE_SCRIPT_STOP, id, 0); } bool atEnd() const { DENG2_ASSERT(script); return (cp - Str_Text(script)) >= Str_Length(script); } void findBegin() { char const *tok; while(!gotoEnd && 0 != (tok = nextToken()) && qstricmp(tok, "{")) {} } void findEnd() { char const *tok; while(!gotoEnd && 0 != (tok = nextToken()) && qstricmp(tok, "}")) {} } char const *nextToken() { // Skip whitespace. while(!atEnd() && isspace(*cp)) { cp++; } // Have we reached the end? if(atEnd()) return nullptr; char *out = token; if(*cp == '"') // A string? { for(cp++; !atEnd(); cp++) { if(*cp == '"') { cp++; // Convert double quotes to single ones. if(*cp != '"') break; } *out++ = *cp; } } else { while(!isspace(*cp) && !atEnd()) { *out++ = *cp++; } } *out++ = 0; return token; } /// @return @c true if the end of the script was reached. bool executeNextCommand() { if(char const *tok = nextToken()) { executeCommand(tok, FID_NORMAL); // Time to unhide the object page(s)? if(cmdExecuted) { pages[Anims]->makeVisible(); pages[Texts]->makeVisible(); } return false; } return true; } static inline char const *findDefaultValueEnd(char const *str) { char const *defaultValueEnd; for(defaultValueEnd = str; defaultValueEnd && *defaultValueEnd != ')'; defaultValueEnd++) {} DENG2_ASSERT(defaultValueEnd < str + qstrlen(str)); return defaultValueEnd; } static char const *nextOperand(char const *operands) { if(operands && operands[0]) { // Some operands might include a default value. int len = qstrlen(operands); if(len > 1 && operands[1] == '(') { // A default value begins. Find the end. return findDefaultValueEnd(operands + 2) + 1; } return operands + 1; } return nullptr; // No more operands. } /// @return Total number of command operands in the control string @a operands. static int countCommandOperands(char const *operands) { int count = 0; while(operands && operands[0]) { count += 1; operands = nextOperand(operands); } return count; } /** * Prepare the command operands from the script. If successfull, a ptr to a new * vector of @c fi_operand_t objects is returned. Ownership of the vector is * given to the caller. * * @return Array of @c fi_operand_t else @c nullptr. Must be free'd with M_Free(). */ fi_operand_t *prepareCommandOperands(command_t const *cmd, int *count) { DENG2_ASSERT(cmd); char const *origCursorPos = cp; int const operandCount = countCommandOperands(cmd->operands); if(operandCount <= 0) return nullptr; fi_operand_t *operands = (fi_operand_t *) M_Malloc(sizeof(*operands) * operandCount); char const *opRover = cmd->operands; for(fi_operand_t *op = operands; opRover && opRover[0]; opRover = nextOperand(opRover), op++) { char const charCode = *opRover; op->type = operandTypeForCharCode(charCode); bool const opHasDefaultValue = (opRover < cmd->operands + (qstrlen(cmd->operands) - 2) && opRover[1] == '('); bool const haveValue = !!nextToken(); if(!haveValue && !opHasDefaultValue) { cp = origCursorPos; if(operands) M_Free(operands); if(count) *count = 0; App_Error("prepareCommandOperands: Too few operands for command '%s'.\n", cmd->token); return 0; // Unreachable. } switch(op->type) { case FVT_INT: { char const *valueStr = haveValue? token : nullptr; if(!valueStr) { // Use the default. int const defaultValueLen = (findDefaultValueEnd(opRover + 2) - opRover) - 1; AutoStr *defaultValue = Str_PartAppend(AutoStr_NewStd(), opRover + 2, 0, defaultValueLen); valueStr = Str_Text(defaultValue); } op->data.integer = strtol(valueStr, nullptr, 0); break; } case FVT_FLOAT: { char const *valueStr = haveValue? token : nullptr; if(!valueStr) { // Use the default. int const defaultValueLen = (findDefaultValueEnd(opRover + 2) - opRover) - 1; AutoStr *defaultValue = Str_PartAppend(AutoStr_NewStd(), opRover + 2, 0, defaultValueLen); valueStr = Str_Text(defaultValue); } op->data.flt = strtod(valueStr, nullptr); break; } case FVT_STRING: { char const *valueStr = haveValue? token : nullptr; int valueLen = haveValue? qstrlen(token) : 0; if(!valueStr) { // Use the default. int const defaultValueLen = (findDefaultValueEnd(opRover + 2) - opRover) - 1; AutoStr *defaultValue = Str_PartAppend(AutoStr_NewStd(), opRover + 2, 0, defaultValueLen); valueStr = Str_Text(defaultValue); valueLen = defaultValueLen; } op->data.cstring = (char *)M_Malloc(valueLen + 1); qstrcpy((char *)op->data.cstring, token); break; } case FVT_URI: { uri_s *uri = Uri_New(); // Always apply the default as it may contain a default scheme. if(opHasDefaultValue) { int const defaultValueLen = (findDefaultValueEnd(opRover + 2) - opRover) - 1; AutoStr *defaultValue = Str_PartAppend(AutoStr_NewStd(), opRover + 2, 0, defaultValueLen); Uri_SetUri2(uri, Str_Text(defaultValue), RC_NULL); } if(haveValue) { Uri_SetUri2(uri, token, RC_NULL); } op->data.uri = uri; break; } default: break; // Unreachable. } } if(count) *count = operandCount; return operands; } bool skippingCommand(command_t const *cmd) { DENG2_ASSERT(cmd); if((skipNext && !cmd->flags.when_condition_skipping) || ((skipping || gotoSkip) && !cmd->flags.when_skipping)) { // While not DO-skipping, the condskip has now been done. if(!doLevel) { if(skipNext) lastSkipped = true; skipNext = false; } return true; } return false; } /** * Execute one (the next) command, advance script cursor. */ bool executeCommand(char const *commandString, int directive) { DENG2_ASSERT(commandString); bool didSkip = false; // Semicolon terminates DO-blocks. if(!qstrcmp(commandString, ";")) { if(doLevel > 0) { if(--doLevel == 0) { // The DO-skip has been completed. skipNext = false; lastSkipped = true; } } return true; // Success! } // We're now going to execute a command. cmdExecuted = true; // Is this a command we know how to execute? if(command_t const *cmd = findCommand(commandString)) { bool const requiredOperands = (cmd->operands && cmd->operands[0]); // Is this command supported for this directive? if(directive != 0 && cmd->excludeDirectives != 0 && (cmd->excludeDirectives & directive) == 0) App_Error("executeCommand: Command \"%s\" is not supported for directive %i.", cmd->token, directive); // Check that there are enough operands. /// @todo Dynamic memory allocation during script interpretation should be avoided. int numOps = 0; fi_operand_t *ops = nullptr; if(!requiredOperands || (ops = prepareCommandOperands(cmd, &numOps))) { // Should we skip this command? if(!(didSkip = skippingCommand(cmd))) { // Execute forthwith! cmd->func(*cmd, ops, self); } } if(!didSkip) { if(gotoEnd) { wait = 1; } else { // Now we've executed the latest command. lastSkipped = false; } } if(ops) { for(int i = 0; i < numOps; ++i) { fi_operand_t *op = &ops[i]; switch(op->type) { case FVT_STRING: M_Free((char *)op->data.cstring); break; case FVT_URI: Uri_Delete(op->data.uri); break; default: break; } } M_Free(ops); } } return !didSkip; } static inline PageIndex choosePageFor(FinaleWidget &widget) { return widget.is()? Anims : Texts; } static inline PageIndex choosePageFor(fi_obtype_e type) { return type == FI_ANIM? Anims : Texts; } FinaleWidget *locateWidget(fi_obtype_e type, String const &name) { if(!name.isEmpty()) { FinalePageWidget::Children const &children = pages[choosePageFor(type)]->children(); for(FinaleWidget *widget : children) { if(!widget->name().compareWithoutCase(name)) { return widget; } } } return nullptr; // Not found. } FinaleWidget *makeWidget(fi_obtype_e type, String const &name) { if(type == FI_ANIM) { return new FinaleAnimWidget(name); } if(type == FI_TEXT) { auto *wi = new FinaleTextWidget(name); // Configure the text to use the Page's font and color. wi->setPageFont(1) .setPageColor(1); return wi; } return nullptr; } #if __CLIENT__ EventHandler *findEventHandler(ddevent_t const &ev) const { for(EventHandler const &eh : eventHandlers) { if(eh.ev.device != ev.device && eh.ev.type != ev.type) continue; switch(eh.ev.type) { case E_TOGGLE: if(eh.ev.toggle.id != ev.toggle.id) continue; break; case E_AXIS: if(eh.ev.axis.id != ev.axis.id) continue; break; case E_ANGLE: if(eh.ev.angle.id != ev.angle.id) continue; break; default: App_Error("Internal error: Invalid event template (type=%i) in finale event handler.", int(eh.ev.type)); } return &const_cast(eh); } return nullptr; } #endif // __CLIENT__ }; FinaleInterpreter::FinaleInterpreter(finaleid_t id) : d(new Instance(this, id)) {} finaleid_t FinaleInterpreter::id() const { return d->id; } void FinaleInterpreter::loadScript(char const *script) { DENG2_ASSERT(script && script[0]); d->pages[Anims].reset(new FinalePageWidget); d->pages[Texts].reset(new FinalePageWidget); // Hide our pages until command interpretation begins. d->pages[Anims]->makeVisible(false); d->pages[Texts]->makeVisible(false); // Take a copy of the script. d->script = Str_Set(Str_NewStd(), script); d->scriptBegin = Str_Text(d->script); d->cp = Str_Text(d->script); // Init cursor. d->initDefaultState(); // Locate the start of the script. if(d->nextToken()) { // The start of the script may include blocks of event directive // commands. These commands are automatically executed in response // to their associated events. if(!qstricmp(d->token, "OnLoad")) { d->findBegin(); forever { d->nextToken(); if(!qstricmp(d->token, "}")) goto end_read; if(!d->executeCommand(d->token, FID_ONLOAD)) App_Error("FinaleInterpreter::LoadScript: Unknown error" "occured executing directive \"OnLoad\"."); } d->findEnd(); end_read: // Skip over any trailing whitespace to position the read cursor // on the first token. while(*d->cp && isspace(*d->cp)) { d->cp++; } // Calculate the new script entry point and restore default state. d->scriptBegin = Str_Text(d->script) + (d->cp - Str_Text(d->script)); d->cp = d->scriptBegin; d->initDefaultState(); } } // Any hooks? DD_CallHooks(HOOK_FINALE_SCRIPT_BEGIN, d->id, 0); } void FinaleInterpreter::resume() { if(!d->flags.suspended) return; d->flags.suspended = false; d->pages[Anims]->pause(false); d->pages[Texts]->pause(false); // Do we need to unhide any pages? if(d->cmdExecuted) { d->pages[Anims]->makeVisible(); d->pages[Texts]->makeVisible(); } } void FinaleInterpreter::suspend() { LOG_AS("FinaleInterpreter"); if(d->flags.suspended) return; d->flags.suspended = true; // While suspended, all pages will be paused and hidden. d->pages[Anims]->pause(); d->pages[Anims]->makeVisible(false); d->pages[Texts]->pause(); d->pages[Texts]->makeVisible(false); } void FinaleInterpreter::terminate() { d->stop(); #ifdef __CLIENT__ d->eventHandlers.clear(); #endif d->releaseScript(); } bool FinaleInterpreter::isMenuTrigger() const { if(d->flags.paused || d->flags.can_skip) { // We want events to be used for unpausing/skipping. return false; } // If skipping is not allowed, we should show the menu, too. return (d->flags.show_menu != 0); } bool FinaleInterpreter::isSuspended() const { return (d->flags.suspended != 0); } void FinaleInterpreter::allowSkip(bool yes) { d->flags.can_skip = yes; } bool FinaleInterpreter::canSkip() const { return (d->flags.can_skip != 0); } bool FinaleInterpreter::commandExecuted() const { return d->cmdExecuted; } static bool runOneTick(FinaleInterpreter &fi) { ddhook_finale_script_ticker_paramaters_t parm; de::zap(parm); parm.runTick = true; parm.canSkip = fi.canSkip(); DD_CallHooks(HOOK_FINALE_SCRIPT_TICKER, fi.id(), &parm); return parm.runTick; } bool FinaleInterpreter::runTicks(timespan_t timeDelta, bool processCommands) { LOG_AS("FinaleInterpreter"); // All pages tick unless paused. page(Anims).runTicks(timeDelta); page(Texts).runTicks(timeDelta); if(!processCommands) return false; if(d->flags.stopped) return false; if(d->flags.suspended) return false; d->timer++; if(!runOneTick(*this)) return false; // If waiting do not execute commands. if(d->wait && --d->wait) return false; // If paused there is nothing further to do. if(d->flags.paused) return false; // If waiting on a text to finish typing, do nothing. if(d->waitText) { if(!d->waitText->animationComplete()) return false; d->waitText = nullptr; } // Waiting for an animation to reach its end? if(d->waitAnim) { if(!d->waitAnim->animationComplete()) return false; d->waitAnim = nullptr; } // Execute commands until a wait time is set or we reach the end of // the script. If the end is reached, the finale really ends (terminates). bool foundEnd = false; while(!d->gotoEnd && !d->wait && !d->waitText && !d->waitAnim && !foundEnd) { foundEnd = d->executeNextCommand(); } return (d->gotoEnd || (foundEnd && d->flags.can_skip)); } bool FinaleInterpreter::skip() { LOG_AS("FinaleInterpreter"); if(d->waitText && d->flags.can_skip && !d->flags.paused) { // Instead of skipping, just complete the text. d->waitText->accelerate(); return true; } // Stop waiting for objects. d->waitText = nullptr; d->waitAnim = nullptr; if(d->flags.paused) { d->flags.paused = false; d->wait = 0; return true; } if(d->flags.can_skip) { d->skipping = true; // Start skipping ahead. d->wait = 0; return true; } return (d->flags.eat_events != 0); } bool FinaleInterpreter::skipToMarker(String const &marker) { LOG_AS("FinaleInterpreter"); if(marker.isEmpty()) return false; d->gotoTarget = marker; d->gotoSkip = true; // Start skipping until the marker is found. d->wait = 0; // Stop any waiting. d->waitText = nullptr; d->waitAnim = nullptr; // Rewind the script so we can jump anywhere. d->cp = d->scriptBegin; return true; } bool FinaleInterpreter::skipInProgress() const { return d->skipNext; } bool FinaleInterpreter::lastSkipped() const { return d->lastSkipped; } int FinaleInterpreter::handleEvent(ddevent_t const &ev) { LOG_AS("FinaleInterpreter"); if(d->flags.suspended) return false; // During the first ~second disallow all events/skipping. if(d->timer < 20) return false; if(!::isClient) { #ifdef __CLIENT__ // Any handlers for this event? if(IS_KEY_DOWN(&ev)) { if(Instance::EventHandler *eh = d->findEventHandler(ev)) { skipToMarker(eh->gotoMarker); // Never eat up events. if(IS_TOGGLE_UP(&ev)) return false; return (d->flags.eat_events != 0); } } #endif } // If we can't skip, there's no interaction of any kind. if(!d->flags.can_skip && !d->flags.paused) return false; // We are only interested in key/button down presses. if(!IS_TOGGLE_DOWN(&ev)) return false; #ifdef __CLIENT__ if(::isClient) { // Request skip from the server. Cl_RequestFinaleSkip(); return true; } #endif #ifdef __SERVER__ // Tell clients to skip. Sv_Finale(d->id, FINF_SKIP, 0); #endif return skip(); } #ifdef __CLIENT__ void FinaleInterpreter::addEventHandler(ddevent_t const &evTemplate, String const &gotoMarker) { // Does a handler already exist for this? if(d->findEventHandler(evTemplate)) return; d->eventHandlers.append(Instance::EventHandler(&evTemplate, gotoMarker)); } void FinaleInterpreter::removeEventHandler(ddevent_t const &evTemplate) { if(Instance::EventHandler *eh = d->findEventHandler(evTemplate)) { int index = 0; while(&d->eventHandlers.at(index) != eh) { index++; } d->eventHandlers.removeAt(index); } } #endif // __CLIENT__ FinalePageWidget &FinaleInterpreter::page(PageIndex index) { if(index >= Anims && index <= Texts) { DENG2_ASSERT(d->pages[index]); return *d->pages[index]; } throw MissingPageError("FinaleInterpreter::page", "Unknown page #" + String::number(int(index))); } FinalePageWidget const &FinaleInterpreter::page(PageIndex index) const { return const_cast(const_cast(this)->page(index)); } FinaleWidget *FinaleInterpreter::tryFindWidget(String const &name) { // Perhaps an Anim? if(FinaleWidget *found = d->locateWidget(FI_ANIM, name)) { return found; } // Perhaps a Text? if(FinaleWidget *found = d->locateWidget(FI_TEXT, name)) { return found; } return nullptr; } FinaleWidget &FinaleInterpreter::findWidget(fi_obtype_e type, String const &name) { if(FinaleWidget *foundWidget = d->locateWidget(type, name)) { return *foundWidget; } throw MissingWidgetError("FinaleInterpeter::findWidget", "Failed locating widget for name:'" + name + "'"); } FinaleWidget &FinaleInterpreter::findOrCreateWidget(fi_obtype_e type, String const &name) { DENG2_ASSERT(type >= FI_ANIM && type <= FI_TEXT); DENG2_ASSERT(!name.isEmpty()); if(FinaleWidget *foundWidget = d->locateWidget(type, name)) { return *foundWidget; } FinaleWidget *newWidget = d->makeWidget(type, name); if(!newWidget) throw Error("FinaleInterpreter::findOrCreateWidget", "Failed making widget for type:" + String::number(int(type))); return *page(d->choosePageFor(*newWidget)).addChild(newWidget); } void FinaleInterpreter::beginDoSkipMode() { if(!skipInProgress()) return; // A conditional skip has been issued. // We'll go into DO-skipping mode. skipnext won't be cleared // until the matching semicolon is found. d->doLevel++; } void FinaleInterpreter::gotoEnd() { d->gotoEnd = true; } void FinaleInterpreter::pause() { d->flags.paused = true; wait(1); } void FinaleInterpreter::wait(int ticksToWait) { d->wait = ticksToWait; } void FinaleInterpreter::foundSkipHere() { d->skipping = false; } void FinaleInterpreter::foundSkipMarker(String const &marker) { // Does it match the current goto torget? if(!d->gotoTarget.compareWithoutCase(marker)) { d->gotoSkip = false; } } int FinaleInterpreter::inTime() const { return d->inTime; } void FinaleInterpreter::setInTime(int seconds) { d->inTime = seconds; } void FinaleInterpreter::setHandleEvents(bool yes) { d->flags.eat_events = yes; } void FinaleInterpreter::setShowMenu(bool yes) { d->flags.show_menu = yes; } void FinaleInterpreter::setSkip(bool allowed) { d->flags.can_skip = allowed; } void FinaleInterpreter::setSkipNext(bool yes) { d->skipNext = yes; } void FinaleInterpreter::setWaitAnim(FinaleAnimWidget *newWaitAnim) { d->waitAnim = newWaitAnim; } void FinaleInterpreter::setWaitText(FinaleTextWidget *newWaitText) { d->waitText = newWaitText; } /// @note This command is called even when condition-skipping. DEFFC(Do) { DENG2_UNUSED2(cmd, ops); fi.beginDoSkipMode(); } DEFFC(End) { DENG2_UNUSED2(cmd, ops); fi.gotoEnd(); } static void changePageBackground(FinalePageWidget &page, Material *newMaterial) { // If the page does not yet have a background set we must setup the color+alpha. if(newMaterial && !page.backgroundMaterial()) { page.setBackgroundTopColorAndAlpha (Vector4f(1, 1, 1, 1)) .setBackgroundBottomColorAndAlpha(Vector4f(1, 1, 1, 1)); } page.setBackgroundMaterial(newMaterial); } DEFFC(BGMaterial) { DENG2_UNUSED(cmd); // First attempt to resolve as a Values URI (which defines the material URI). Material *material = nullptr; try { if(ded_value_t *value = Def_GetValueByUri(OP_URI(0))) { material = &App_ResourceSystem().material(de::Uri(value->text, RC_NULL)); } else { material = &App_ResourceSystem().material(*reinterpret_cast(OP_URI(0))); } } catch(MaterialManifest::MissingMaterialError const &) {} // Ignore this error. catch(ResourceSystem::MissingManifestError const &) {} // Ignore this error. changePageBackground(fi.page(FinaleInterpreter::Anims), material); } DEFFC(NoBGMaterial) { DENG2_UNUSED2(cmd, ops); changePageBackground(fi.page(FinaleInterpreter::Anims), 0); } DEFFC(InTime) { DENG2_UNUSED(cmd); fi.setInTime(FRACSECS_TO_TICKS(OP_FLOAT(0))); } DEFFC(Tic) { DENG2_UNUSED2(cmd, ops); fi.wait(); } DEFFC(Wait) { DENG2_UNUSED(cmd); fi.wait(FRACSECS_TO_TICKS(OP_FLOAT(0))); } DEFFC(WaitText) { DENG2_UNUSED(cmd); fi.setWaitText(&fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as()); } DEFFC(WaitAnim) { DENG2_UNUSED(cmd); fi.setWaitAnim(&fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as()); } DEFFC(Color) { DENG2_UNUSED(cmd); fi.page(FinaleInterpreter::Anims) .setBackgroundTopColor (Vector3f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2)), fi.inTime()) .setBackgroundBottomColor(Vector3f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2)), fi.inTime()); } DEFFC(ColorAlpha) { DENG2_UNUSED(cmd); fi.page(FinaleInterpreter::Anims) .setBackgroundTopColorAndAlpha (Vector4f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()) .setBackgroundBottomColorAndAlpha(Vector4f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()); } DEFFC(Pause) { DENG2_UNUSED2(cmd, ops); fi.pause(); } DEFFC(CanSkip) { DENG2_UNUSED2(cmd, ops); fi.setSkip(true); } DEFFC(NoSkip) { DENG2_UNUSED2(cmd, ops); fi.setSkip(false); } DEFFC(SkipHere) { DENG2_UNUSED2(cmd, ops); fi.foundSkipHere(); } DEFFC(Events) { DENG2_UNUSED2(cmd, ops); fi.setHandleEvents(); } DEFFC(NoEvents) { DENG2_UNUSED2(cmd, ops); fi.setHandleEvents(false); } DEFFC(OnKey) { #ifdef __CLIENT__ DENG2_UNUSED(cmd); // Construct a template event for this handler. ddevent_t ev; de::zap(ev); ev.device = IDEV_KEYBOARD; ev.type = E_TOGGLE; ev.toggle.id = DD_GetKeyCode(OP_CSTRING(0)); ev.toggle.state = ETOG_DOWN; fi.addEventHandler(ev, OP_CSTRING(1)); #else DENG2_UNUSED3(cmd, ops, fi); #endif } DEFFC(UnsetKey) { #ifdef __CLIENT__ DENG2_UNUSED(cmd); // Construct a template event for what we want to "unset". ddevent_t ev; de::zap(ev); ev.device = IDEV_KEYBOARD; ev.type = E_TOGGLE; ev.toggle.id = DD_GetKeyCode(OP_CSTRING(0)); ev.toggle.state = ETOG_DOWN; fi.removeEventHandler(ev); #else DENG2_UNUSED3(cmd, ops, fi); #endif } DEFFC(If) { DENG2_UNUSED2(cmd, ops); LOG_AS("FIC_If"); char const *token = OP_CSTRING(0); bool val = false; // Built-in conditions. if(!qstricmp(token, "netgame")) { val = netGame; } else if(!qstrnicmp(token, "mode:", 5)) { if(App_GameLoaded()) val = !String(token + 5).compareWithoutCase(App_CurrentGame().identityKey()); else val = 0; } // Any hooks? else if(Plug_CheckForHook(HOOK_FINALE_EVAL_IF)) { ddhook_finale_script_evalif_paramaters_t p; de::zap(p); p.token = token; p.returnVal = 0; if(DD_CallHooks(HOOK_FINALE_EVAL_IF, fi.id(), (void *) &p)) { val = p.returnVal; LOG_SCR_XVERBOSE("HOOK_FINALE_EVAL_IF: %s => %i") << token << val; } else { LOG_SCR_XVERBOSE("HOOK_FINALE_EVAL_IF: no hook (for %s)") << token; } } else { LOG_SCR_WARNING("Unknown condition '%s'") << token; } // Skip the next command if the value is false. fi.setSkipNext(!val); } DEFFC(IfNot) { FIC_If(cmd, ops, fi); fi.setSkipNext(!fi.skipInProgress()); } /// @note The only time the ELSE condition does not skip is immediately after a skip. DEFFC(Else) { DENG2_UNUSED2(cmd, ops); fi.setSkipNext(!fi.lastSkipped()); } DEFFC(GoTo) { DENG2_UNUSED(cmd); fi.skipToMarker(OP_CSTRING(0)); } DEFFC(Marker) { DENG2_UNUSED(cmd); fi.foundSkipMarker(OP_CSTRING(0)); } DEFFC(Delete) { DENG2_UNUSED(cmd); delete fi.tryFindWidget(OP_CSTRING(0)); } DEFFC(Image) { DENG2_UNUSED(cmd); LOG_AS("FIC_Image"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); char const *name = OP_CSTRING(1); lumpnum_t lumpNum = App_FileSystem().lumpNumForName(name); anim.clearAllFrames(); if(rawtex_t *rawTex = App_ResourceSystem().declareRawTexture(lumpNum)) { anim.newFrame(FinaleAnimWidget::Frame::PFT_RAW, -1, &rawTex->lumpNum, 0, false); return; } LOG_SCR_WARNING("Missing lump '%s'") << name; } DEFFC(ImageAt) { DENG2_UNUSED(cmd); LOG_AS("FIC_ImageAt"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); float x = OP_FLOAT(1); float y = OP_FLOAT(2); char const *name = OP_CSTRING(3); lumpnum_t lumpNum = App_FileSystem().lumpNumForName(name); anim.clearAllFrames() .setOrigin(Vector2f(x, y)); if(rawtex_t *rawTex = App_ResourceSystem().declareRawTexture(lumpNum)) { anim.newFrame(FinaleAnimWidget::Frame::PFT_RAW, -1, &rawTex->lumpNum, 0, false); return; } LOG_SCR_WARNING("Missing lump '%s'") << name; } #ifdef __CLIENT__ static DGLuint loadAndPrepareExtTexture(char const *fileName) { image_t image; DGLuint glTexName = 0; if(GL_LoadExtImage(image, fileName, LGM_NORMAL)) { // Loaded successfully and converted accordingly. // Upload the image to GL. glTexName = GL_NewTextureWithParams( ( image.pixelSize == 2 ? DGL_LUMINANCE_PLUS_A8 : image.pixelSize == 3 ? DGL_RGB : image.pixelSize == 4 ? DGL_RGBA : DGL_LUMINANCE ), image.size.x, image.size.y, image.pixels, (image.size.x < 128 && image.size.y < 128? TXCF_NO_COMPRESSION : 0), 0, GL_LINEAR, GL_LINEAR, 0 /*no anisotropy*/, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE); Image_ClearPixelData(image); } return glTexName; } #endif // __CLIENT__ DEFFC(XImage) { DENG2_UNUSED(cmd); LOG_AS("FIC_XImage"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); #ifdef __CLIENT__ char const *fileName = OP_CSTRING(1); #endif anim.clearAllFrames(); #ifdef __CLIENT__ // Load the external resource. if(DGLuint tex = loadAndPrepareExtTexture(fileName)) { anim.newFrame(FinaleAnimWidget::Frame::PFT_XIMAGE, -1, &tex, 0, false); } else { LOG_SCR_WARNING("Missing graphic '%s'") << fileName; } #endif // __CLIENT__ } DEFFC(Patch) { DENG2_UNUSED(cmd); LOG_AS("FIC_Patch"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); char const *encodedName = OP_CSTRING(3); anim.setOrigin(Vector2f(OP_FLOAT(1), OP_FLOAT(2))); anim.clearAllFrames(); patchid_t patchId = R_DeclarePatch(encodedName); if(patchId) { anim.newFrame(FinaleAnimWidget::Frame::PFT_PATCH, -1, (void *)&patchId, 0, 0); } else { LOG_SCR_WARNING("Missing Patch '%s'") << encodedName; } } DEFFC(SetPatch) { DENG2_UNUSED(cmd); LOG_AS("FIC_SetPatch"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); char const *encodedName = OP_CSTRING(1); patchid_t patchId = R_DeclarePatch(encodedName); if(patchId == 0) { LOG_SCR_WARNING("Missing Patch '%s'") << encodedName; return; } if(!anim.frameCount()) { anim.newFrame(FinaleAnimWidget::Frame::PFT_PATCH, -1, (void *)&patchId, 0, false); return; } // Convert the first frame. FinaleAnimWidget::Frame *f = anim.allFrames().first(); f->type = FinaleAnimWidget::Frame::PFT_PATCH; f->texRef.patch = patchId; f->tics = -1; f->sound = 0; } DEFFC(ClearAnim) { DENG2_UNUSED(cmd); if(FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { if(FinaleAnimWidget *anim = wi->maybeAs()) { anim->clearAllFrames(); } } } DEFFC(Anim) { DENG2_UNUSED(cmd); LOG_AS("FIC_Anim"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); char const *encodedName = OP_CSTRING(1); int const tics = FRACSECS_TO_TICKS(OP_FLOAT(2)); patchid_t patchId = R_DeclarePatch(encodedName); if(!patchId) { LOG_SCR_WARNING("Patch '%s' not found") << encodedName; return; } anim.newFrame(FinaleAnimWidget::Frame::PFT_PATCH, tics, (void *)&patchId, 0, false); } DEFFC(AnimImage) { DENG2_UNUSED(cmd); LOG_AS("FIC_AnimImage"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); char const *encodedName = OP_CSTRING(1); int const tics = FRACSECS_TO_TICKS(OP_FLOAT(2)); lumpnum_t lumpNum = App_FileSystem().lumpNumForName(encodedName); if(rawtex_t *rawTex = App_ResourceSystem().declareRawTexture(lumpNum)) { anim.newFrame(FinaleAnimWidget::Frame::PFT_RAW, tics, &rawTex->lumpNum, 0, false); return; } LOG_SCR_WARNING("Lump '%s' not found") << encodedName; } DEFFC(Repeat) { DENG2_UNUSED(cmd); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); anim.setLooping(); } DEFFC(StateAnim) { DENG2_UNUSED(cmd); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); #if !defined(__CLIENT__) DENG2_UNUSED(anim); #endif int stateId = Def_Get(DD_DEF_STATE, OP_CSTRING(1), 0); int count = OP_INT(2); // Animate N states starting from the given one. for(; count > 0 && stateId > 0; count--) { state_t *st = &runtimeDefs.states[stateId]; #ifdef __CLIENT__ spriteinfo_t sinf; R_GetSpriteInfo(st->sprite, st->frame & 0x7fff, &sinf); anim.newFrame(FinaleAnimWidget::Frame::PFT_MATERIAL, (st->tics <= 0? 1 : st->tics), sinf.material, 0, sinf.flip); #endif // Go to the next state. stateId = st->nextState; } } DEFFC(PicSound) { DENG2_UNUSED(cmd); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); int const sound = Def_Get(DD_DEF_SOUND, OP_CSTRING(1), 0); if(!anim.frameCount()) { anim.newFrame(FinaleAnimWidget::Frame::PFT_MATERIAL, -1, 0, sound, false); return; } anim.allFrames().at(anim.frameCount() - 1)->sound = sound; } DEFFC(ObjectOffX) { DENG2_UNUSED(cmd); if(FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setOriginX(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectOffY) { DENG2_UNUSED(cmd); if(FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setOriginY(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectOffZ) { DENG2_UNUSED(cmd); if(FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setOriginZ(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectRGB) { DENG2_UNUSED(cmd); if(FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { Vector3f const color(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)); if(FinaleTextWidget *text = wi->maybeAs()) { text->setColor(color, fi.inTime()); } if(FinaleAnimWidget *anim = wi->maybeAs()) { // This affects all the colors. anim->setColor (color, fi.inTime()) .setEdgeColor (color, fi.inTime()) .setOtherColor (color, fi.inTime()) .setOtherEdgeColor(color, fi.inTime()); } } } DEFFC(ObjectAlpha) { DENG2_UNUSED(cmd); if(FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { float const alpha = OP_FLOAT(1); if(FinaleTextWidget *text = wi->maybeAs()) { text->setAlpha(alpha, fi.inTime()); } if(FinaleAnimWidget *anim = wi->maybeAs()) { anim->setAlpha (alpha, fi.inTime()) .setOtherAlpha(alpha, fi.inTime()); } } } DEFFC(ObjectScaleX) { DENG2_UNUSED(cmd); if(FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setScaleX(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectScaleY) { DENG2_UNUSED(cmd); if(FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setScaleY(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectScaleZ) { DENG2_UNUSED(cmd); if(FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setScaleZ(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectScale) { DENG2_UNUSED(cmd); if(FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setScaleX(OP_FLOAT(1), fi.inTime()) .setScaleY(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectScaleXY) { DENG2_UNUSED(cmd); if(FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setScaleX(OP_FLOAT(1), fi.inTime()) .setScaleY(OP_FLOAT(2), fi.inTime()); } } DEFFC(ObjectScaleXYZ) { DENG2_UNUSED(cmd); if(FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setScale(Vector3f(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()); } } DEFFC(ObjectAngle) { DENG2_UNUSED(cmd); if(FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setAngle(OP_FLOAT(1), fi.inTime()); } } DEFFC(Rect) { DENG2_UNUSED(cmd); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); /// @note We may be converting an existing Pic to a Rect, so re-init the expected /// default state accordingly. anim.clearAllFrames() .resetAllColors() .setLooping(false) // Yeah? .setOrigin(Vector3f(OP_FLOAT(1), OP_FLOAT(2), 0)) .setScale(Vector3f(OP_FLOAT(3), OP_FLOAT(4), 1)); } DEFFC(FillColor) { DENG2_UNUSED(cmd); FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)); if(!wi || !wi->is()) return; FinaleAnimWidget &anim = wi->as(); // Which colors to modify? int which = 0; if(!qstricmp(OP_CSTRING(1), "top")) which |= 1; else if(!qstricmp(OP_CSTRING(1), "bottom")) which |= 2; else which = 3; Vector4f color; for(int i = 0; i < 4; ++i) { color[i] = OP_FLOAT(2 + i); } if(which & 1) anim.setColorAndAlpha(color, fi.inTime()); if(which & 2) anim.setOtherColorAndAlpha(color, fi.inTime()); } DEFFC(EdgeColor) { DENG2_UNUSED(cmd); FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)); if(!wi || !wi->is()) return; FinaleAnimWidget &anim = wi->as(); // Which colors to modify? int which = 0; if(!qstricmp(OP_CSTRING(1), "top")) which |= 1; else if(!qstricmp(OP_CSTRING(1), "bottom")) which |= 2; else which = 3; Vector4f color; for(int i = 0; i < 4; ++i) { color[i] = OP_FLOAT(2 + i); } if(which & 1) anim.setEdgeColorAndAlpha(color, fi.inTime()); if(which & 2) anim.setOtherEdgeColorAndAlpha(color, fi.inTime()); } DEFFC(OffsetX) { DENG2_UNUSED(cmd); fi.page(FinaleInterpreter::Anims).setOffsetX(OP_FLOAT(0), fi.inTime()); } DEFFC(OffsetY) { DENG2_UNUSED(cmd); fi.page(FinaleInterpreter::Anims).setOffsetY(OP_FLOAT(0), fi.inTime()); } DEFFC(Sound) { DENG2_UNUSED2(cmd, fi); S_LocalSound(Def_Get(DD_DEF_SOUND, OP_CSTRING(0), nullptr), nullptr); } DEFFC(SoundAt) { DENG2_UNUSED2(cmd, fi); int const soundId = Def_Get(DD_DEF_SOUND, OP_CSTRING(0), nullptr); float vol = de::min(OP_FLOAT(1), 1.f); S_LocalSoundAtVolume(soundId, nullptr, vol); } DEFFC(SeeSound) { DENG2_UNUSED2(cmd, fi); int num = Def_Get(DD_DEF_MOBJ, OP_CSTRING(0), nullptr); if(num < 0 || runtimeDefs.mobjInfo[num].seeSound <= 0) return; S_LocalSound(runtimeDefs.mobjInfo[num].seeSound, nullptr); } DEFFC(DieSound) { DENG2_UNUSED2(cmd, fi); int num = Def_Get(DD_DEF_MOBJ, OP_CSTRING(0), nullptr); if(num < 0 || runtimeDefs.mobjInfo[num].deathSound <= 0) return; S_LocalSound(runtimeDefs.mobjInfo[num].deathSound, nullptr); } DEFFC(Music) { DENG2_UNUSED3(cmd, ops, fi); S_StartMusic(OP_CSTRING(0), true); } DEFFC(MusicOnce) { DENG2_UNUSED3(cmd, ops, fi); S_StartMusic(OP_CSTRING(0), false); } DEFFC(Filter) { DENG2_UNUSED(cmd); fi.page(FinaleInterpreter::Texts).setFilterColorAndAlpha(Vector4f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()); } DEFFC(Text) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setText(OP_CSTRING(3)) .setCursorPos(0) // Restart the text. .setOrigin(Vector3f(OP_FLOAT(1), OP_FLOAT(2), 0)); } DEFFC(TextFromDef) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); int textIdx = defs.getTextNum((char *)OP_CSTRING(3)); text.setText(textIdx >= 0? defs.text[textIdx].text : "(undefined)") .setCursorPos(0) // Restart the type-in animation (if any). .setOrigin(Vector3f(OP_FLOAT(1), OP_FLOAT(2), 0)); } DEFFC(TextFromLump) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setOrigin(Vector3f(OP_FLOAT(1), OP_FLOAT(2), 0)); lumpnum_t lumpNum = App_FileSystem().lumpNumForName(OP_CSTRING(3)); if(lumpNum >= 0) { File1 &lump = App_FileSystem().lump(lumpNum); uint8_t const *rawStr = lump.cache(); AutoStr *str = AutoStr_NewStd(); Str_Reserve(str, lump.size() * 2); char *out = Str_Text(str); for(size_t i = 0; i < lump.size(); ++i) { char ch = (char)(rawStr[i]); if(ch == '\r') continue; if(ch == '\n') { *out++ = '\\'; *out++ = 'n'; } else { *out++ = ch; } } lump.unlock(); text.setText(Str_Text(str)); } else { text.setText("(not found)"); } text.setCursorPos(0); // Restart. } DEFFC(SetText) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setText(OP_CSTRING(1)); } DEFFC(SetTextDef) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); int textIdx = defs.getTextNum((char *)OP_CSTRING(1)); text.setText(textIdx >= 0? defs.text[textIdx].text : "(undefined)"); } DEFFC(DeleteText) { DENG2_UNUSED(cmd); delete fi.tryFindWidget(OP_CSTRING(0)); } DEFFC(PredefinedColor) { DENG2_UNUSED(cmd); fi.page(FinaleInterpreter::Texts) .setPredefinedColor(de::clamp(1, OP_INT(0), FIPAGE_NUM_PREDEFINED_COLORS) - 1, Vector3f(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()); fi.page(FinaleInterpreter::Anims) .setPredefinedColor(de::clamp(1, OP_INT(0), FIPAGE_NUM_PREDEFINED_COLORS) - 1, Vector3f(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()); } DEFFC(PredefinedFont) { #ifdef __CLIENT__ DENG2_UNUSED(cmd); LOG_AS("FIC_PredefinedFont"); fontid_t const fontNum = Fonts_ResolveUri(OP_URI(1)); if(fontNum) { int const idx = de::clamp(1, OP_INT(0), FIPAGE_NUM_PREDEFINED_FONTS) - 1; fi.page(FinaleInterpreter::Anims).setPredefinedFont(idx, fontNum); fi.page(FinaleInterpreter::Texts).setPredefinedFont(idx, fontNum); return; } AutoStr *fontPath = Uri_ToString(OP_URI(1)); LOG_SCR_WARNING("Unknown font '%s'") << Str_Text(fontPath); #else DENG2_UNUSED3(cmd, ops, fi); #endif } DEFFC(TextRGB) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setColor(Vector3f(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()); } DEFFC(TextAlpha) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setAlpha(OP_FLOAT(1), fi.inTime()); } DEFFC(TextOffX) { DENG2_UNUSED(cmd); FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)); wi.setOriginX(OP_FLOAT(1), fi.inTime()); } DEFFC(TextOffY) { DENG2_UNUSED(cmd); FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)); wi.setOriginY(OP_FLOAT(1), fi.inTime()); } DEFFC(TextCenter) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setAlignment(text.alignment() & ~(ALIGN_LEFT | ALIGN_RIGHT)); } DEFFC(TextNoCenter) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setAlignment(text.alignment() | ALIGN_LEFT); } DEFFC(TextScroll) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setScrollRate(OP_INT(1)); } DEFFC(TextPos) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setCursorPos(OP_INT(1)); } DEFFC(TextRate) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setTypeInRate(OP_INT(1)); } DEFFC(TextLineHeight) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setLineHeight(OP_FLOAT(1)); } DEFFC(Font) { #ifdef __CLIENT__ DENG2_UNUSED(cmd); LOG_AS("FIC_Font"); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); fontid_t fontNum = Fonts_ResolveUri(OP_URI(1)); if(fontNum) { text.setFont(fontNum); return; } AutoStr *fontPath = Uri_ToString(OP_URI(1)); LOG_SCR_WARNING("Unknown font '%s'") << Str_Text(fontPath); #else DENG2_UNUSED3(cmd, ops, fi); #endif } DEFFC(FontA) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setFont(fi.page(FinaleInterpreter::Texts).predefinedFont(0)); } DEFFC(FontB) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setFont(fi.page(FinaleInterpreter::Texts).predefinedFont(1)); } DEFFC(NoMusic) { DENG2_UNUSED3(cmd, ops, fi); S_StopMusic(); } DEFFC(TextScaleX) { DENG2_UNUSED(cmd); FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)); wi.setScaleX(OP_FLOAT(1), fi.inTime()); } DEFFC(TextScaleY) { DENG2_UNUSED(cmd); FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)); wi.setScaleY(OP_FLOAT(1), fi.inTime()); } DEFFC(TextScale) { DENG2_UNUSED(cmd); FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)); wi.setScaleX(OP_FLOAT(1), fi.inTime()) .setScaleY(OP_FLOAT(2), fi.inTime()); } DEFFC(PlayDemo) { /// @todo Demos are not supported at the moment. -jk #if 0 // While playing a demo we suspend command interpretation. fi.suspend(); // Start the demo. if(!Con_Executef(CMDS_DDAY, true, "playdemo \"%s\"", OP_CSTRING(0))) { // Demo playback failed. Here we go again... fi.resume(); } #else DENG2_UNUSED3(cmd, ops, fi); #endif } DEFFC(Command) { DENG2_UNUSED2(cmd, fi); Con_Executef(CMDS_SCRIPT, false, "%s", OP_CSTRING(0)); } DEFFC(ShowMenu) { DENG2_UNUSED2(cmd, ops); fi.setShowMenu(); } DEFFC(NoShowMenu) { DENG2_UNUSED2(cmd, ops); fi.setShowMenu(false); } doomsday-stable-1.15.7/doomsday/client/src/ui/infine/finalepagewidget.cpp0000664000175000017500000002466312641367670025763 0ustar jaakkojaakko/** @file finalepagewidget.cpp InFine animation system, FinalePageWidget. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "ui/infine/finalepagewidget.h" #include #include "dd_main.h" // App_ResourceSystem() #include "Material" #ifdef __CLIENT__ # include "gl/gl_draw.h" # include "gl/gl_main.h" # include "render/rend_main.h" // renderWireframe # include "MaterialAnimator" #endif using namespace de; DENG2_PIMPL_NOREF(FinalePageWidget) , DENG2_OBSERVES(FinaleWidget, Deletion) { struct Flags { char hidden:1; ///< Not drawn. char paused:1; ///< Does not tick. char showBackground:1; } flags; Children children; ///< Child widgets (owned). uint timer = 0; animatorvector3_t offset; ///< Offset the world origin. animatorvector4_t filter; animatorvector3_t preColor[FIPAGE_NUM_PREDEFINED_COLORS]; fontid_t preFont[FIPAGE_NUM_PREDEFINED_FONTS]; struct Background { Material *material; animatorvector4_t topColor; animatorvector4_t bottomColor; Background() : material(nullptr) { AnimatorVector4_Init(topColor, 1, 1, 1, 0); AnimatorVector4_Init(bottomColor, 1, 1, 1, 0); } } bg; Instance() { de::zap(flags); flags.showBackground = true; /// Draw background by default. AnimatorVector3_Init(offset, 0, 0, 0); AnimatorVector4_Init(filter, 0, 0, 0, 0); for(int i = 0; i < FIPAGE_NUM_PREDEFINED_COLORS; ++i) { AnimatorVector3_Init(preColor[i], 1, 1, 1); } de::zap(preFont); } ~Instance() { qDeleteAll(children); DENG2_ASSERT(children.isEmpty()); } void finaleWidgetBeingDeleted(FinaleWidget const &widget) { DENG2_ASSERT(children.contains(&const_cast(widget))); children.removeOne(&const_cast(widget)); } }; FinalePageWidget::FinalePageWidget() : d(new Instance) {} FinalePageWidget::~FinalePageWidget() {} #ifdef __CLIENT__ static inline MaterialVariantSpec const &uiMaterialSpec() { return App_ResourceSystem().materialSpec(UiContext, 0, 0, 0, 0, GL_REPEAT, GL_REPEAT, 0, 1, 0, false, false, false, false); } void FinalePageWidget::draw() const { if(d->flags.hidden) return; // Draw a background? if(d->flags.showBackground) { vec3f_t topColor; V3f_Set(topColor, d->bg.topColor[0].value, d->bg.topColor[1].value, d->bg.topColor[2].value); float topAlpha = d->bg.topColor[3].value; vec3f_t bottomColor; V3f_Set(bottomColor, d->bg.bottomColor[0].value, d->bg.bottomColor[1].value, d->bg.bottomColor[2].value); float bottomAlpha = d->bg.bottomColor[3].value; if(topAlpha > 0 && bottomAlpha > 0) { if(Material *material = d->bg.material) { MaterialAnimator &matAnimator = material->getAnimator(uiMaterialSpec()); // Ensure we've up to date info about the material. matAnimator.prepare(); GL_BindTexture(matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture); glEnable(GL_TEXTURE_2D); } if(d->bg.material || topAlpha < 1.0 || bottomAlpha < 1.0) { GL_BlendMode(BM_NORMAL); } else { glDisable(GL_BLEND); } GL_DrawRectf2TextureColor(0, 0, SCREENWIDTH, SCREENHEIGHT, 64, 64, topColor, topAlpha, bottomColor, bottomAlpha); GL_SetNoTexture(); glEnable(GL_BLEND); } } // Now lets go into 3D mode for drawing the p objects. glMatrixMode(GL_MODELVIEW); glPushMatrix(); //glLoadIdentity(); GL_SetMultisample(true); // Clear Z buffer (prevent the objects being clipped by nearby polygons). glClear(GL_DEPTH_BUFFER_BIT); if(renderWireframe > 1) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glEnable(GL_ALPHA_TEST); Vector3f worldOrigin(/*-SCREENWIDTH/2*/ - d->offset[VX].value, /*-SCREENHEIGHT/2*/ - d->offset[VY].value, 0/*.05f - d->offset[VZ].value*/); for(FinaleWidget *widget : d->children) { widget->draw(worldOrigin); } // Restore original matrices and state: back to normal 2D. glDisable(GL_ALPHA_TEST); // Back from wireframe mode? if(renderWireframe > 1) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // Filter on top of everything. Only draw if necessary. if(d->filter[3].value > 0) { GL_DrawRectf2Color(0, 0, SCREENWIDTH, SCREENHEIGHT, d->filter[0].value, d->filter[1].value, d->filter[2].value, d->filter[3].value); } GL_SetMultisample(false); glMatrixMode(GL_MODELVIEW); glPopMatrix(); } #endif void FinalePageWidget::makeVisible(bool yes) { d->flags.hidden = !yes; } void FinalePageWidget::pause(bool yes) { d->flags.paused = yes; } void FinalePageWidget::runTicks(timespan_t /*timeDelta*/) { /// @todo Interpolate in fractional time. if(!DD_IsSharpTick()) return; // A new 'sharp' tick has begun. d->timer++; for(FinaleWidget *widget : d->children) { widget->runTicks(/*timeDelta*/); } AnimatorVector3_Think(d->offset); AnimatorVector4_Think(d->bg.topColor); AnimatorVector4_Think(d->bg.bottomColor); AnimatorVector4_Think(d->filter); for(int i = 0; i < FIPAGE_NUM_PREDEFINED_COLORS; ++i) { AnimatorVector3_Think(d->preColor[i]); } } bool FinalePageWidget::hasWidget(FinaleWidget *widget) { if(!widget) return false; return d->children.contains(widget); } FinaleWidget *FinalePageWidget::addChild(FinaleWidget *widgetToAdd) { if(!hasWidget(widgetToAdd)) { d->children.append(widgetToAdd); widgetToAdd->setPage(this); widgetToAdd->audienceForDeletion += d; } return widgetToAdd; } FinaleWidget *FinalePageWidget::removeChild(FinaleWidget *widgetToRemove) { if(hasWidget(widgetToRemove)) { widgetToRemove->audienceForDeletion -= d; widgetToRemove->setPage(nullptr); d->children.removeOne(widgetToRemove); } return widgetToRemove; } FinalePageWidget::Children const &FinalePageWidget::children() const { return d->children; } Material *FinalePageWidget::backgroundMaterial() const { return d->bg.material; } FinalePageWidget &FinalePageWidget::setBackgroundMaterial(Material *newMaterial) { d->bg.material = newMaterial; return *this; } FinalePageWidget &FinalePageWidget::setBackgroundTopColor(Vector3f const &newColor, int steps) { AnimatorVector3_Set(d->bg.topColor, newColor.x, newColor.y, newColor.z, steps); return *this; } FinalePageWidget &FinalePageWidget::setBackgroundTopColorAndAlpha(Vector4f const &newColorAndAlpha, int steps) { AnimatorVector4_Set(d->bg.topColor, newColorAndAlpha.x, newColorAndAlpha.y, newColorAndAlpha.z, newColorAndAlpha.w, steps); return *this; } FinalePageWidget &FinalePageWidget::setBackgroundBottomColor(Vector3f const &newColor, int steps) { AnimatorVector3_Set(d->bg.bottomColor, newColor.x, newColor.y, newColor.z, steps); return *this; } FinalePageWidget &FinalePageWidget::setBackgroundBottomColorAndAlpha(Vector4f const &newColorAndAlpha, int steps) { AnimatorVector4_Set(d->bg.bottomColor, newColorAndAlpha.x, newColorAndAlpha.y, newColorAndAlpha.z, newColorAndAlpha.w, steps); return *this; } FinalePageWidget &FinalePageWidget::setOffset(Vector3f const &newOffset, int steps) { AnimatorVector3_Set(d->offset, newOffset.x, newOffset.y, newOffset.z, steps); return *this; } FinalePageWidget &FinalePageWidget::setOffsetX(float x, int steps) { Animator_Set(&d->offset[VX], x, steps); return *this; } FinalePageWidget &FinalePageWidget::setOffsetY(float y, int steps) { Animator_Set(&d->offset[VY], y, steps); return *this; } FinalePageWidget &FinalePageWidget::setOffsetZ(float y, int steps) { Animator_Set(&d->offset[VZ], y, steps); return *this; } FinalePageWidget &FinalePageWidget::setFilterColorAndAlpha(Vector4f const &newColorAndAlpha, int steps) { AnimatorVector4_Set(d->filter, newColorAndAlpha.x, newColorAndAlpha.y, newColorAndAlpha.z, newColorAndAlpha.w, steps); return *this; } FinalePageWidget &FinalePageWidget::setPredefinedColor(uint idx, Vector3f const &newColor, int steps) { if(VALID_FIPAGE_PREDEFINED_COLOR(idx)) { AnimatorVector3_Set(d->preColor[idx], newColor.x, newColor.y, newColor.z, steps); } else { throw InvalidColorError("FinalePageWidget::setPredefinedColor", "Invalid color #" + String::number(idx)); } return *this; } animatorvector3_t const *FinalePageWidget::predefinedColor(uint idx) { if(VALID_FIPAGE_PREDEFINED_COLOR(idx)) { return &d->preColor[idx]; } throw InvalidColorError("FinalePageWidget::predefinedColor", "Invalid color #" + String::number(idx)); } FinalePageWidget &FinalePageWidget::setPredefinedFont(uint idx, fontid_t fontNum) { if(VALID_FIPAGE_PREDEFINED_FONT(idx)) { d->preFont[idx] = fontNum; } else { throw InvalidFontError("FinalePageWidget::setPredefinedFont", "Invalid font #" + String::number(idx)); } return *this; } fontid_t FinalePageWidget::predefinedFont(uint idx) { if(VALID_FIPAGE_PREDEFINED_FONT(idx)) { return d->preFont[idx]; } throw InvalidFontError("FinalePageWidget::predefinedFont", "Invalid font #" + String::number(idx)); } doomsday-stable-1.15.7/doomsday/client/src/ui/infine/finaleanimwidget.cpp0000664000175000017500000004532112641367670025765 0ustar jaakkojaakko/** @file finaleanimwidget.cpp InFine animation system, FinaleAnimWidget. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include "ui/infine/finaleanimwidget.h" #include "dd_main.h" // App_ResourceSystem() #include "api_sound.h" #ifdef __CLIENT__ # include "gl/gl_main.h" # include "gl/gl_texmanager.h" // GL_PrepareRawTexture() # include "render/r_draw.h" // Rend_PatchTextureSpec() # include "render/rend_main.h" // filterUI # include "MaterialAnimator" #endif using namespace de; FinaleAnimWidget::Frame::Frame() : tics (0) , type (PFT_MATERIAL) , sound(0) { de::zap(flags); de::zap(texRef); } FinaleAnimWidget::Frame::~Frame() { #ifdef __CLIENT__ if(type == PFT_XIMAGE) { DGL_DeleteTextures(1, (DGLuint *)&texRef.tex); } #endif } DENG2_PIMPL_NOREF(FinaleAnimWidget) { bool animComplete = true; bool animLooping = false; ///< @c true= loop back to the start when the end is reached. int tics = 0; int curFrame = 0; Frames frames; animatorvector4_t color; /// For rectangle-objects. animatorvector4_t otherColor; animatorvector4_t edgeColor; animatorvector4_t otherEdgeColor; Instance() { AnimatorVector4_Init(color, 1, 1, 1, 1); AnimatorVector4_Init(otherColor, 0, 0, 0, 0); AnimatorVector4_Init(edgeColor, 0, 0, 0, 0); AnimatorVector4_Init(otherEdgeColor, 0, 0, 0, 0); } static Frame *makeFrame(Frame::Type type, int tics, void *texRef, short sound, bool flagFlipH) { Frame *f = new Frame; f->flags.flip = flagFlipH; f->type = type; f->tics = tics; f->sound = sound; switch(f->type) { case Frame::PFT_MATERIAL: f->texRef.material = ((Material *)texRef); break; case Frame::PFT_PATCH: f->texRef.patch = *((patchid_t *)texRef); break; case Frame::PFT_RAW: f->texRef.lumpNum = *((lumpnum_t *)texRef); break; case Frame::PFT_XIMAGE: f->texRef.tex = *((DGLuint *)texRef); break; default: throw Error("FinaleAnimWidget::makeFrame", "Unknown frame type #" + String::number(type)); } return f; } }; FinaleAnimWidget::FinaleAnimWidget(String const &name) : FinaleWidget(name) , d(new Instance) {} FinaleAnimWidget::~FinaleAnimWidget() { clearAllFrames(); } bool FinaleAnimWidget::animationComplete() const { return d->animComplete; } FinaleAnimWidget &FinaleAnimWidget::setLooping(bool yes) { d->animLooping = yes; return *this; } #ifdef __CLIENT__ static void useColor(animator_t const *color, int components) { if(components == 3) { glColor3f(color[0].value, color[1].value, color[2].value); } else if(components == 4) { glColor4f(color[0].value, color[1].value, color[2].value, color[3].value); } } static int buildGeometry(float const /*dimensions*/[3], dd_bool flipTextureS, Vector4f const &bottomColor, Vector4f const &topColor, Vector3f **posCoords, Vector4f **colorCoords, Vector2f **texCoords) { static Vector3f posCoordBuf[4]; static Vector4f colorCoordBuf[4]; static Vector2f texCoordBuf[4]; // 0 - 1 // | / | Vertex layout // 2 - 3 posCoordBuf[0] = Vector3f(0, 0, 0); posCoordBuf[1] = Vector3f(1, 0, 0); posCoordBuf[2] = Vector3f(0, 1, 0); posCoordBuf[3] = Vector3f(1, 1, 0); texCoordBuf[0] = Vector2f((flipTextureS? 1:0), 0); texCoordBuf[1] = Vector2f((flipTextureS? 0:1), 0); texCoordBuf[2] = Vector2f((flipTextureS? 1:0), 1); texCoordBuf[3] = Vector2f((flipTextureS? 0:1), 1); colorCoordBuf[0] = bottomColor; colorCoordBuf[1] = bottomColor; colorCoordBuf[2] = topColor; colorCoordBuf[3] = topColor; *posCoords = posCoordBuf; *texCoords = texCoordBuf; *colorCoords = colorCoordBuf; return 4; } static void drawGeometry(int numVerts, Vector3f const *posCoords, Vector4f const *colorCoords, Vector2f const *texCoords) { glBegin(GL_TRIANGLE_STRIP); Vector3f const *posIt = posCoords; Vector4f const *colorIt = colorCoords; Vector2f const *texIt = texCoords; for(int i = 0; i < numVerts; ++i, posIt++, colorIt++, texIt++) { if(texCoords) glTexCoord2f(texIt->x, texIt->y); if(colorCoords) glColor4f(colorIt->x, colorIt->y, colorIt->z, colorIt->w); glVertex3f(posIt->x, posIt->y, posIt->z); } glEnd(); } static inline MaterialVariantSpec const &uiMaterialSpec() { return App_ResourceSystem().materialSpec(UiContext, 0, 0, 0, 0, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 0, -3, 0, false, false, false, false); } static void drawPicFrame(FinaleAnimWidget *p, uint frame, float const _origin[3], float /*const*/ scale[3], float const rgba[4], float const rgba2[4], float angle, Vector3f const &worldOffset) { vec3f_t offset = { 0, 0, 0 }, dimensions, origin, originOffset, center; vec2f_t texScale = { 1, 1 }; vec2f_t rotateCenter = { .5f, .5f }; dd_bool showEdges = true, flipTextureS = false; dd_bool mustPopTextureMatrix = false; dd_bool textureEnabled = false; int numVerts; Vector3f *posCoords; Vector4f *colorCoords; Vector2f *texCoords; if(p->frameCount()) { /// @todo Optimize: Texture/Material searches should be NOT be done here -ds FinaleAnimWidget::Frame *f = p->allFrames().at(frame); flipTextureS = (f->flags.flip != 0); showEdges = false; switch(f->type) { case FinaleAnimWidget::Frame::PFT_RAW: { rawtex_t *rawTex = App_ResourceSystem().declareRawTexture(f->texRef.lumpNum); if(rawTex) { DGLuint glName = GL_PrepareRawTexture(*rawTex); V3f_Set(offset, 0, 0, 0); // Raw images are always considered to have a logical size of 320x200 // even though the actual texture resolution may be different. V3f_Set(dimensions, 320 /*rawTex->width*/, 200 /*rawTex->height*/, 0); // Rotation occurs around the center of the screen. V2f_Set(rotateCenter, 160, 100); GL_BindTextureUnmanaged(glName, gl::ClampToEdge, gl::ClampToEdge, (filterUI ? gl::Linear : gl::Nearest)); if(glName) { glEnable(GL_TEXTURE_2D); textureEnabled = true; } } break; } case FinaleAnimWidget::Frame::PFT_XIMAGE: V3f_Set(offset, 0, 0, 0); V3f_Set(dimensions, 1, 1, 0); V2f_Set(rotateCenter, .5f, .5f); GL_BindTextureUnmanaged(f->texRef.tex, gl::ClampToEdge, gl::ClampToEdge, (filterUI ? gl::Linear : gl::Nearest)); if(f->texRef.tex) { glEnable(GL_TEXTURE_2D); textureEnabled = true; } break; case FinaleAnimWidget::Frame::PFT_MATERIAL: if(Material *mat = f->texRef.material) { /// @todo Utilize *all* properties of the Material. MaterialAnimator &matAnimator = mat->getAnimator(uiMaterialSpec()); // Ensure we've up to date info about the material. matAnimator.prepare(); Vector2i const &matDimensions = matAnimator.dimensions(); TextureVariant *tex = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; int const texBorder = tex->spec().variant.border; GL_BindTexture(tex); glEnable(GL_TEXTURE_2D); textureEnabled = true; V3f_Set(dimensions, matDimensions.x + texBorder * 2, matDimensions.y + texBorder * 2, 0); V2f_Set(rotateCenter, dimensions[VX] / 2, dimensions[VY] / 2); tex->glCoords(&texScale[VX], &texScale[VY]); // Apply a sprite-texture origin offset? bool const texIsSprite = !tex->base().manifest().scheme().name().compareWithoutCase("Sprites"); if(texIsSprite) { V3f_Set(offset, tex->base().origin().x, tex->base().origin().y, 0); } else { V3f_Set(offset, 0, 0, 0); } } break; case FinaleAnimWidget::Frame::PFT_PATCH: { TextureManifest &manifest = App_ResourceSystem().textureScheme("Patches") .findByUniqueId(f->texRef.patch); if(manifest.hasTexture()) { Texture &tex = manifest.texture(); TextureVariantSpec const &texSpec = Rend_PatchTextureSpec(0 | (tex.isFlagged(Texture::Monochrome) ? TSF_MONOCHROME : 0) | (tex.isFlagged(Texture::UpscaleAndSharpen) ? TSF_UPSCALE_AND_SHARPEN : 0)); GL_BindTexture(tex.prepareVariant(texSpec)); glEnable(GL_TEXTURE_2D); textureEnabled = true; V3f_Set(offset, tex.origin().x, tex.origin().y, 0); V3f_Set(dimensions, tex.width(), tex.height(), 0); V2f_Set(rotateCenter, dimensions[VX]/2, dimensions[VY]/2); } break; } default: App_Error("drawPicFrame: Invalid FI_PIC frame type %i.", int(f->type)); } } // If we've not chosen a texture by now set some defaults. /// @todo This is some seriously funky logic... refactor or remove. if(!textureEnabled) { V3f_Copy(dimensions, scale); V3f_Set(scale, 1, 1, 1); V2f_Set(rotateCenter, dimensions[VX] / 2, dimensions[VY] / 2); } V3f_Set(center, dimensions[VX] / 2, dimensions[VY] / 2, dimensions[VZ] / 2); V3f_Sum(origin, _origin, center); V3f_Subtract(origin, origin, offset); for(int i = 0; i < 3; ++i) { origin[i] += worldOffset[i]; } V3f_Subtract(originOffset, offset, center); offset[VX] *= scale[VX]; offset[VY] *= scale[VY]; offset[VZ] *= scale[VZ]; V3f_Sum(originOffset, originOffset, offset); numVerts = buildGeometry(dimensions, flipTextureS, rgba, rgba2, &posCoords, &colorCoords, &texCoords); // Setup the transformation. glMatrixMode(GL_MODELVIEW); glPushMatrix(); //glScalef(.1f/SCREENWIDTH, .1f/SCREENWIDTH, 1); // Move to the object origin. glTranslatef(origin[VX], origin[VY], origin[VZ]); // Translate to the object center. /// @todo Remove this; just go to origin directly. Rotation origin is /// now separately in 'rotateCenter'. -jk glTranslatef(originOffset[VX], originOffset[VY], originOffset[VZ]); glScalef(scale[VX], scale[VY], scale[VZ]); if(angle != 0) { glTranslatef(rotateCenter[VX], rotateCenter[VY], 0); // With rotation we must counter the VGA aspect ratio. glScalef(1, 200.0f / 240.0f, 1); glRotatef(angle, 0, 0, 1); glScalef(1, 240.0f / 200.0f, 1); glTranslatef(-rotateCenter[VX], -rotateCenter[VY], 0); } glMatrixMode(GL_MODELVIEW); // Scale up our unit-geometry to the desired dimensions. glScalef(dimensions[VX], dimensions[VY], dimensions[VZ]); if(texScale[0] != 1 || texScale[1] != 1) { glMatrixMode(GL_TEXTURE); glPushMatrix(); glScalef(texScale[0], texScale[1], 1); mustPopTextureMatrix = true; } drawGeometry(numVerts, posCoords, colorCoords, texCoords); GL_SetNoTexture(); if(mustPopTextureMatrix) { glMatrixMode(GL_TEXTURE); glPopMatrix(); } if(showEdges) { glBegin(GL_LINES); useColor(p->edgeColor(), 4); glVertex2f(0, 0); glVertex2f(1, 0); glVertex2f(1, 0); useColor(p->otherEdgeColor(), 4); glVertex2f(1, 1); glVertex2f(1, 1); glVertex2f(0, 1); glVertex2f(0, 1); useColor(p->edgeColor(), 4); glVertex2f(0, 0); glEnd(); } // Restore original transformation. glMatrixMode(GL_MODELVIEW); glPopMatrix(); } void FinaleAnimWidget::draw(Vector3f const &offset) { // Fully transparent pics will not be drawn. if(!(d->color[3].value > 0)) return; vec3f_t _scale, _origin; V3f_Set(_origin, origin()[VX].value, origin()[VY].value, origin()[VZ].value); V3f_Set(_scale, scale()[VX].value, scale()[VY].value, scale()[VZ].value); vec4f_t rgba, rgba2; V4f_Set(rgba, d->color[0].value, d->color[1].value, d->color[2].value, d->color[3].value); if(!frameCount()) { V4f_Set(rgba2, d->otherColor[0].value, d->otherColor[1].value, d->otherColor[2].value, d->otherColor[3].value); } drawPicFrame(this, d->curFrame, _origin, _scale, rgba, (!frameCount()? rgba2 : rgba), angle().value, offset); } #endif void FinaleAnimWidget::runTicks(/*timespan_t timeDelta*/) { FinaleWidget::runTicks(/*timeDelta*/); AnimatorVector4_Think(d->color); AnimatorVector4_Think(d->otherColor); AnimatorVector4_Think(d->edgeColor); AnimatorVector4_Think(d->otherEdgeColor); if(!(d->frames.count() > 1)) return; // If animating, decrease the sequence timer. if(d->frames.at(d->curFrame)->tics > 0) { if(--d->tics <= 0) { Frame *f; // Advance the sequence position. k = next pos. uint next = d->curFrame + 1; if(next == uint(d->frames.count())) { // This is the end. d->animComplete = true; // Stop the sequence? if(d->animLooping) { next = 0; // Rewind back to beginning. } else // Yes. { d->frames.at(next = d->curFrame)->tics = 0; } } // Advance to the next pos. f = d->frames.at(d->curFrame = next); d->tics = f->tics; // Play a sound? if(f->sound > 0) S_LocalSound(f->sound, 0); } } } int FinaleAnimWidget::newFrame(Frame::Type type, int tics, void *texRef, short sound, bool flagFlipH) { d->frames.append(d->makeFrame(type, tics, texRef, sound, flagFlipH)); // The addition of a new frame means the animation has not yet completed. d->animComplete = false; return d->frames.count(); } FinaleAnimWidget::Frames const &FinaleAnimWidget::allFrames() const { return d->frames; } FinaleAnimWidget &FinaleAnimWidget::clearAllFrames() { qDeleteAll(d->frames); d->frames.clear(); d->curFrame = 0; d->animComplete = true; // Nothing to animate. d->animLooping = false; // Yeah? return *this; } FinaleAnimWidget &FinaleAnimWidget::resetAllColors() { // Default colors. AnimatorVector4_Init(d->color, 1, 1, 1, 1); AnimatorVector4_Init(d->otherColor, 1, 1, 1, 1); // Edge alpha is zero by default. AnimatorVector4_Init(d->edgeColor, 1, 1, 1, 0); AnimatorVector4_Init(d->otherEdgeColor, 1, 1, 1, 0); return *this; } animator_t const *FinaleAnimWidget::color() const { return d->color; } FinaleAnimWidget &FinaleAnimWidget::setColor(Vector3f const &newColor, int steps) { AnimatorVector3_Set(d->color, newColor.x, newColor.y, newColor.z, steps); return *this; } FinaleAnimWidget &FinaleAnimWidget::setAlpha(float newAlpha, int steps) { Animator_Set(&d->color[3], newAlpha, steps); return *this; } FinaleAnimWidget &FinaleAnimWidget::setColorAndAlpha(Vector4f const &newColorAndAlpha, int steps) { AnimatorVector4_Set(d->color, newColorAndAlpha.x, newColorAndAlpha.y, newColorAndAlpha.z, newColorAndAlpha.w, steps); return *this; } animator_t const *FinaleAnimWidget::edgeColor() const { return d->edgeColor; } FinaleAnimWidget &FinaleAnimWidget::setEdgeColor(Vector3f const &newColor, int steps) { AnimatorVector3_Set(d->edgeColor, newColor.x, newColor.y, newColor.z, steps); return *this; } FinaleAnimWidget &FinaleAnimWidget::setEdgeAlpha(float newAlpha, int steps) { Animator_Set(&d->edgeColor[3], newAlpha, steps); return *this; } FinaleAnimWidget &FinaleAnimWidget::setEdgeColorAndAlpha(Vector4f const &newColorAndAlpha, int steps) { AnimatorVector4_Set(d->edgeColor, newColorAndAlpha.x, newColorAndAlpha.y, newColorAndAlpha.z, newColorAndAlpha.w, steps); return *this; } animator_t const *FinaleAnimWidget::otherColor() const { return d->otherColor; } FinaleAnimWidget &FinaleAnimWidget::setOtherColor(de::Vector3f const &newColor, int steps) { AnimatorVector3_Set(d->otherColor, newColor.x, newColor.y, newColor.z, steps); return *this; } FinaleAnimWidget &FinaleAnimWidget::setOtherAlpha(float newAlpha, int steps) { Animator_Set(&d->otherColor[3], newAlpha, steps); return *this; } FinaleAnimWidget &FinaleAnimWidget::setOtherColorAndAlpha(Vector4f const &newColorAndAlpha, int steps) { AnimatorVector4_Set(d->otherColor, newColorAndAlpha.x, newColorAndAlpha.y, newColorAndAlpha.z, newColorAndAlpha.w, steps); return *this; } animator_t const *FinaleAnimWidget::otherEdgeColor() const { return d->otherEdgeColor; } FinaleAnimWidget &FinaleAnimWidget::setOtherEdgeColor(de::Vector3f const &newColor, int steps) { AnimatorVector3_Set(d->otherEdgeColor, newColor.x, newColor.y, newColor.z, steps); return *this; } FinaleAnimWidget &FinaleAnimWidget::setOtherEdgeAlpha(float newAlpha, int steps) { Animator_Set(&d->otherEdgeColor[3], newAlpha, steps); return *this; } FinaleAnimWidget &FinaleAnimWidget::setOtherEdgeColorAndAlpha(Vector4f const &newColorAndAlpha, int steps) { AnimatorVector4_Set(d->otherEdgeColor, newColorAndAlpha.x, newColorAndAlpha.y, newColorAndAlpha.z, newColorAndAlpha.w, steps); return *this; } doomsday-stable-1.15.7/doomsday/client/src/ui/infine/finaletextwidget.cpp0000664000175000017500000002471212641367670026026 0ustar jaakkojaakko/** @file finaletextwidget.cpp InFine animation system, FinaleTextWidget. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "ui/infine/finaletextwidget.h" #include // memcpy, memmove #include #include #include #include "api_fontrender.h" #include "ui/infine/finalepagewidget.h" #ifdef __CLIENT__ # include "gl/gl_main.h" # include "gl/sys_opengl.h" // TODO: get rid of this #endif using namespace de; DENG2_PIMPL_NOREF(FinaleTextWidget) { animatorvector4_t color; uint pageColor = 0; ///< 1-based page color index. @c 0= Use our own color. uint pageFont = 0; ///< 1-based page font index. @c 0= Use our own font. int alignFlags = ALIGN_TOPLEFT; ///< @ref alignmentFlags short textFlags = DTF_ONLY_SHADOW; ///< @ref drawTextFlags int scrollWait = 0; int scrollTimer = 0; ///< Automatic scrolling upwards. int cursorPos = 0; int wait = 3; int timer = 0; float lineHeight = 11.f / 7 - 1; fontid_t fontNum = 0; char *text = nullptr; bool animComplete = true; Instance() { AnimatorVector4_Init(color, 1, 1, 1, 1); } ~Instance() { Z_Free(text); } #ifdef __CLIENT__ static int textLineWidth(char const *text) { int width = 0; for(; *text; text++) { if(*text == '\\') { if(!*++text) break; if(*text == 'n') break; if(*text >= '0' && *text <= '9') continue; if(*text == 'w' || *text == 'W' || *text == 'p' || *text == 'P') continue; } width += FR_CharWidth(*text); } return width; } #endif }; FinaleTextWidget::FinaleTextWidget(String const &name) : FinaleWidget(name) , d(new Instance) {} FinaleTextWidget::~FinaleTextWidget() {} void FinaleTextWidget::accelerate() { // Fill in the rest very quickly. d->wait = -10; } FinaleTextWidget &FinaleTextWidget::setCursorPos(int newPos) { d->cursorPos = de::max(0, newPos); return *this; } void FinaleTextWidget::runTicks(/*timespan_t timeDelta*/) { FinaleWidget::runTicks(/*timeDelta*/); AnimatorVector4_Think(d->color); if(d->wait) { if(--d->timer <= 0) { if(d->wait > 0) { // Positive wait: move cursor one position, wait again. d->cursorPos++; d->timer = d->wait; } else { // Negative wait: move cursor several positions, don't wait. d->cursorPos += ABS(d->wait); d->timer = 1; } } } if(d->scrollWait) { if(--d->scrollTimer <= 0) { d->scrollTimer = d->scrollWait; setOriginY(origin()[1].target - 1, d->scrollWait); //pos[1].target -= 1; //pos[1].steps = d->scrollWait; } } // Is the text object fully visible? d->animComplete = (!d->wait || d->cursorPos >= visLength()); } #ifdef __CLIENT__ void FinaleTextWidget::draw(Vector3f const &offset) { if(!d->text) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glMatrixMode(GL_MODELVIEW); glPushMatrix(); //glScalef(.1f/SCREENWIDTH, .1f/SCREENWIDTH, 1); glTranslatef(origin()[0].value + offset.x, origin()[1].value + offset.y, origin()[2].value + offset.z); if(angle().value != 0) { // Counter the VGA aspect ratio. glScalef(1, 200.0f / 240.0f, 1); glRotatef(angle().value, 0, 0, 1); glScalef(1, 240.0f / 200.0f, 1); } glScalef(scale()[0].value, scale()[1].value, scale()[2].value); glEnable(GL_TEXTURE_2D); FR_SetFont(d->pageFont? page()->predefinedFont(d->pageFont - 1) : d->fontNum); // Set the normal color. animatorvector3_t const *color; if(d->pageColor == 0) color = (animatorvector3_t const *)&d->color; else color = page()->predefinedColor(d->pageColor - 1); FR_SetColor((*color)[CR].value, (*color)[CG].value, (*color)[CB].value); FR_SetAlpha(d->color[CA].value); int x = 0, y = 0, ch, linew = -1; char *ptr = d->text; for(int cnt = 0; *ptr && (!d->wait || cnt < d->cursorPos); ptr++) { if(linew < 0) linew = d->textLineWidth(ptr); ch = *ptr; if(*ptr == '\\') // Escape? { if(!*++ptr) break; // Change of color? if(*ptr >= '0' && *ptr <= '9') { uint colorIdx = *ptr - '0'; if(colorIdx == 0) color = (animatorvector3_t const *)&d->color; else color = page()->predefinedColor(colorIdx - 1); FR_SetColor((*color)[CR].value, (*color)[CG].value, (*color)[CB].value); FR_SetAlpha(d->color[CA].value); continue; } // 'w' = half a second wait, 'W' = second'f wait if(*ptr == 'w' || *ptr == 'W') // Wait? { if(d->wait) cnt += int(float(TICRATE) / d->wait / (*ptr == 'w' ? 2 : 1)); continue; } // 'p' = 5 second wait, 'P' = 10 second wait if(*ptr == 'p' || *ptr == 'P') // Longer pause? { if(d->wait) cnt += int(float(TICRATE) / d->wait * (*ptr == 'p' ? 5 : 10)); continue; } if(*ptr == 'n' || *ptr == 'N') // Newline? { x = 0; y += FR_CharHeight('A') * (1 + d->lineHeight); linew = -1; cnt++; // Include newlines in the wait count. continue; } if(*ptr == '_') ch = ' '; } // Let'f do Y-clipping (in case of tall text blocks). if(scale()[1].value * y + origin()[1].value >= -scale()[1].value * d->lineHeight && scale()[1].value * y + origin()[1].value < SCREENHEIGHT) { FR_DrawCharXY(ch, (d->alignFlags & ALIGN_LEFT) ? x : x - linew / 2, y); x += FR_CharWidth(ch); } ++cnt; } glDisable(GL_TEXTURE_2D); glMatrixMode(GL_MODELVIEW); glPopMatrix(); } #endif bool FinaleTextWidget::animationComplete() const { return d->animComplete; } /// @todo optimize: Cache this result. int FinaleTextWidget::visLength() { int count = 0; if(d->text) { float const secondLen = (d->wait? TICRATE / d->wait : 0); for(char const *ptr = d->text; *ptr; ptr++) { if(*ptr == '\\') // Escape? { if(!*++ptr) break; switch(*ptr) { case 'w': count += secondLen / 2; break; case 'W': count += secondLen; break; case 'p': count += 5 * secondLen; break; case 'P': count += 10 * secondLen; break; default: if((*ptr >= '0' && *ptr <= '9') || *ptr == 'n' || *ptr == 'N') continue; } } count++; } } return count; } char const *FinaleTextWidget::text() const { return d->text; } FinaleTextWidget &FinaleTextWidget::setText(char const *newText) { Z_Free(d->text); d->text = nullptr; if(newText && newText[0]) { int len = (int)qstrlen(newText) + 1; d->text = (char *) Z_Malloc(len, PU_APPSTATIC, 0); std::memcpy(d->text, newText, len); } int const visLen = visLength(); if(d->cursorPos > visLen) { d->cursorPos = visLen; } return *this; } fontid_t FinaleTextWidget::font() const { return d->fontNum; } FinaleTextWidget &FinaleTextWidget::setFont(fontid_t newFont) { d->fontNum = newFont; d->pageFont = 0; return *this; } int FinaleTextWidget::alignment() const { return d->alignFlags; } FinaleTextWidget &FinaleTextWidget::setAlignment(int newAlignment) { d->alignFlags = newAlignment; return *this; } float FinaleTextWidget::lineHeight() const { return d->lineHeight; } FinaleTextWidget &FinaleTextWidget::setLineHeight(float newLineHeight) { d->lineHeight = newLineHeight; return *this; } int FinaleTextWidget::scrollRate() const { return d->scrollWait; } FinaleTextWidget &FinaleTextWidget::setScrollRate(int newRateInTics) { d->scrollWait = newRateInTics; d->scrollTimer = 0; return *this; } int FinaleTextWidget::typeInRate() const { return d->wait; } FinaleTextWidget &FinaleTextWidget::setTypeInRate(int newRateInTics) { d->wait = newRateInTics; return *this; } FinaleTextWidget &FinaleTextWidget::setColor(Vector3f const &newColor, int steps) { AnimatorVector3_Set(*((animatorvector3_t *)d->color), newColor.x, newColor.y, newColor.z, steps); d->pageColor = 0; return *this; } FinaleTextWidget &FinaleTextWidget::setAlpha(float alpha, int steps) { Animator_Set(&d->color[CA], alpha, steps); return *this; } FinaleTextWidget &FinaleTextWidget::setColorAndAlpha(Vector4f const &newColorAndAlpha, int steps) { AnimatorVector4_Set(d->color, newColorAndAlpha.x, newColorAndAlpha.y, newColorAndAlpha.z, newColorAndAlpha.w, steps); d->pageColor = 0; return *this; } FinaleTextWidget &FinaleTextWidget::setPageColor(uint id) { d->pageColor = id; return *this; } FinaleTextWidget &FinaleTextWidget::setPageFont(uint id) { d->pageFont = id; return *this; } doomsday-stable-1.15.7/doomsday/client/src/ui/infine/infinesystem.cpp0000664000175000017500000002022312641367670025165 0ustar jaakkojaakko/** @file infinesystem.cpp Interactive animation sequence system. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #define DENG_NO_API_MACROS_INFINE #include #include #include #include "de_base.h" #include "ui/infine/infinesystem.h" #ifdef __CLIENT__ # include "clientapp.h" #endif #include "BindContext" #include "ui/infine/finale.h" #include "ui/infine/finaleinterpreter.h" using namespace de; DENG2_PIMPL_NOREF(InFineSystem) , DENG2_OBSERVES(Finale, Deletion) { Finales finales; ~Instance() { qDeleteAll(finales); } Finale *finaleForId(finaleid_t id) { if(id != 0) { for(Finale const *f : finales) { if(f->id() == id) return const_cast(f); } } return nullptr; } finaleid_t nextUnusedId() { finaleid_t id = 0; while(finaleForId(++id)) {} return id; } void finaleBeingDeleted(Finale const &finale) { finales.removeOne(const_cast(&finale)); } }; InFineSystem::InFineSystem() : d(new Instance) {} void InFineSystem::reset() { LOG_AS("InFineSystem"); while(!d->finales.isEmpty()) { std::unique_ptr finale(d->finales.takeFirst()); finale->terminate(); } } bool InFineSystem::finaleInProgess() const { for(Finale *finale : d->finales) { if(finale->isActive() || finale->isSuspended()) return true; } return false; } void InFineSystem::runTicks(timespan_t timeDelta) { LOG_AS("InFineSystem"); for(int i = 0; i < d->finales.count(); ++i) { Finale *finale = d->finales[i]; if(finale->runTicks(timeDelta)) { // The script has terminated. delete finale; } } } Finale &InFineSystem::newFinale(int flags, String script, String const &setupCmds) { LOG_AS("InFineSystem"); if(!setupCmds.isEmpty()) { // Setup commands are included. We must prepend these to the script // in a special control block that will be executed immediately. script.prepend("OnLoad {\n" + setupCmds + "}\n"); } d->finales << new Finale(flags, d->nextUnusedId(), script); auto *finale = d->finales.last(); finale->audienceForDeletion() += d; return *finale; } bool InFineSystem::hasFinale(finaleid_t id) const { return d->finaleForId(id) != nullptr; } Finale &InFineSystem::finale(finaleid_t id) { Finale *finale = d->finaleForId(id); if(finale) return *finale; /// @throw MissingFinaleError The given id does not reference a Finale throw MissingFinaleError("finale", "No Finale known by id:" + String::number(id)); } InFineSystem::Finales const &InFineSystem::finales() const { return d->finales; } #ifdef __CLIENT__ static bool inited; void InFineSystem::initBindingContext() // static { // Already been here? if(inited) return; inited = true; BindContext &context = ClientApp::inputSystem().context("finale"); context.setDDFallbackResponder(de::function_cast(gx.FinaleResponder)); context.activate(); // always on } void InFineSystem::deinitBindingContext() // static { // Not yet initialized? if(!inited) return; BindContext &context = ClientApp::inputSystem().context("finale"); context.setDDFallbackResponder(nullptr); context.deactivate(); inited = false; } #endif // __CLIENT__ namespace { byte scaleMode = SCALEMODE_SMART_STRETCH; } void InFineSystem::consoleRegister() // static { C_VAR_BYTE("rend-finale-stretch", &scaleMode, 0, SCALEMODE_FIRST, SCALEMODE_LAST); } // Public API (C Wrapper) --------------------------------------------------------------- finaleid_t FI_Execute2(char const *script, int flags, char const *setupCmds) { LOG_AS("InFine.Execute"); if(!script || !script[0]) { LOGDEV_SCR_WARNING("Attempted to play an empty script"); return 0; } if((flags & FF_LOCAL) && isDedicated) { // Dedicated servers do not play local Finales. LOGDEV_SCR_WARNING("No local finales in dedicated mode"); return 0; } return App_InFineSystem().newFinale(flags, script, setupCmds).id(); } finaleid_t FI_Execute(char const *script, int flags) { return FI_Execute2(script, flags, 0); } void FI_ScriptTerminate(finaleid_t id) { LOG_AS("InFine.ScriptTerminate"); if(App_InFineSystem().hasFinale(id)) { Finale &finale = App_InFineSystem().finale(id); if(finale.terminate()) { delete &finale; } return; } LOGDEV_SCR_WARNING("Unknown finaleid %i") << id; } dd_bool FI_ScriptActive(finaleid_t id) { LOG_AS("InFine.ScriptActive"); if(App_InFineSystem().hasFinale(id)) { return App_InFineSystem().finale(id).isActive(); } LOGDEV_SCR_WARNING("Unknown finaleid %i") << id; return false; } void FI_ScriptSuspend(finaleid_t id) { LOG_AS("InFine.ScriptSuspend"); if(App_InFineSystem().hasFinale(id)) { App_InFineSystem().finale(id).suspend(); return; } LOGDEV_SCR_WARNING("Unknown finaleid %i") << id; } void FI_ScriptResume(finaleid_t id) { LOG_AS("InFine.ScriptResume"); if(App_InFineSystem().hasFinale(id)) { App_InFineSystem().finale(id).resume(); return; } LOGDEV_SCR_WARNING("Unknown finaleid %i") << id; } dd_bool FI_ScriptSuspended(finaleid_t id) { LOG_AS("InFine.ScriptSuspended"); if(App_InFineSystem().hasFinale(id)) { return App_InFineSystem().finale(id).interpreter().isSuspended(); } LOGDEV_SCR_WARNING("Unknown finaleid %i") << id; return false; } int FI_ScriptFlags(finaleid_t id) { LOG_AS("InFine.ScriptFlags"); if(App_InFineSystem().hasFinale(id)) { return App_InFineSystem().finale(id).flags(); } LOGDEV_SCR_WARNING("Unknown finaleid %i") << id; return 0; } int FI_ScriptResponder(finaleid_t id, void const *ev) { DENG2_ASSERT(ev); LOG_AS("InFine.ScriptResponder"); if(App_InFineSystem().hasFinale(id)) { return App_InFineSystem().finale(id).handleEvent(*static_cast(ev)); } LOGDEV_SCR_WARNING("Unknown finaleid %i") << id; return false; } dd_bool FI_ScriptCmdExecuted(finaleid_t id) { LOG_AS("InFine.CmdExecuted"); if(App_InFineSystem().hasFinale(id)) { return App_InFineSystem().finale(id).interpreter().commandExecuted(); } LOGDEV_SCR_WARNING("Unknown finaleid %i") << id; return false; } dd_bool FI_ScriptRequestSkip(finaleid_t id) { LOG_AS("InFine.ScriptRequestSkip"); if(App_InFineSystem().hasFinale(id)) { return App_InFineSystem().finale(id).requestSkip(); } LOGDEV_SCR_WARNING("Unknown finaleid %i") << id; return false; } dd_bool FI_ScriptIsMenuTrigger(finaleid_t id) { LOG_AS("InFine.ScriptIsMenuTrigger"); if(App_InFineSystem().hasFinale(id)) { return App_InFineSystem().finale(id).isMenuTrigger(); } LOGDEV_SCR_WARNING("Unknown finaleid %i") << id; return false; } DENG_DECLARE_API(Infine) = { { DE_API_INFINE }, FI_Execute2, FI_Execute, FI_ScriptActive, FI_ScriptFlags, FI_ScriptTerminate, FI_ScriptSuspend, FI_ScriptResume, FI_ScriptSuspended, FI_ScriptRequestSkip, FI_ScriptCmdExecuted, FI_ScriptIsMenuTrigger, FI_ScriptResponder }; doomsday-stable-1.15.7/doomsday/client/src/ui/inputdevicebuttoncontrol.cpp0000664000175000017500000000441512641367670026361 0ustar jaakkojaakko/** @file inputdevicebuttoncontrol.cpp Button control for a logical input device. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "ui/inputdevicebuttoncontrol.h" #include // Timer_RealMilliseconds() using namespace de; InputDeviceButtonControl::InputDeviceButtonControl(String const &name) { setName(name); } InputDeviceButtonControl::~InputDeviceButtonControl() {} bool InputDeviceButtonControl::isDown() const { return _isDown; } void InputDeviceButtonControl::setDown(bool yes) { bool const oldDown = _isDown; _isDown = yes; if(_isDown != oldDown) { // Remember when the change occurred. _time = Timer_RealMilliseconds(); } if(_isDown) { // This will get cleared after the state is checked by someone. setBindContextAssociation(Triggered); } else { // We can clear the expiration when the key is released. setBindContextAssociation(Expired, UnsetFlags); } } String InputDeviceButtonControl::description() const { return String(_E(b) "%1 " _E(.) "(Button)").arg(fullName()); } bool InputDeviceButtonControl::inDefaultState() const { return !_isDown; // Not depressed? } void InputDeviceButtonControl::reset() { setBindContextAssociation(Triggered | Expired, UnsetFlags); _isDown = false; _time = 0; } duint InputDeviceButtonControl::time() const { return _time; } doomsday-stable-1.15.7/doomsday/client/src/ui/clientrootwidget.cpp0000664000175000017500000000303212641367670024565 0ustar jaakkojaakko/** @file clientrootwidget.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/clientrootwidget.h" #include "ui/clientwindow.h" #include "clientapp.h" using namespace de; ClientRootWidget::ClientRootWidget(CanvasWindow *window) : GuiRootWidget(window) {} ClientWindow &ClientRootWidget::window() { return GuiRootWidget::window().as(); } void ClientRootWidget::addOnTop(GuiWidget *widget) { // The window knows what is the correct top to add to. window().addOnTop(widget); } void ClientRootWidget::dispatchLatestMousePosition() { ClientApp::windowSystem().dispatchLatestMousePosition(); } void ClientRootWidget::handleEventAsFallback(Event const &event) { // The bindings might have use for this event. ClientApp::inputSystem().tryEvent(event, "global"); } doomsday-stable-1.15.7/doomsday/client/src/ui/clientwindowsystem.cpp0000664000175000017500000000604312641367670025157 0ustar jaakkojaakko/** @file clientwindowsystem.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "ui/clientwindowsystem.h" #include "clientapp.h" #include "dd_main.h" #include "gl/gl_main.h" using namespace de; DENG2_PIMPL(ClientWindowSystem) { SettingsRegister settings; struct ClientStyle : public Style { bool isBlurringAllowed() const { return !App_GameLoaded(); } GuiWidget *sharedBlurWidget() const { if(!ClientWindow::mainExists()) return nullptr; return &ClientWindow::main().taskBarBlur(); } }; Instance(Public *i) : Base(i) { self.setStyle(new ClientStyle); self.style().load(App::packageLoader().load("net.dengine.client.defaultstyle")); using SReg = SettingsRegister; settings.define(SReg::ConfigVariable, "window.main.showFps") .define(SReg::ConfigVariable, "window.main.fsaa") .define(SReg::ConfigVariable, "window.main.vsync") .define(SReg::IntCVar, "rend-finale-stretch", SCALEMODE_SMART_STRETCH) .define(SReg::IntCVar, "rend-hud-stretch", SCALEMODE_SMART_STRETCH) .define(SReg::IntCVar, "inlude-stretch", SCALEMODE_SMART_STRETCH) .define(SReg::IntCVar, "menu-stretch", SCALEMODE_SMART_STRETCH); } }; ClientWindowSystem::ClientWindowSystem() : WindowSystem() , d(new Instance(this)) { ClientWindow::setDefaultGLFormat(); } SettingsRegister &ClientWindowSystem::settings() { return d->settings; } ClientWindow *ClientWindowSystem::createWindow(String const &id) { return newWindow(id); } ClientWindow &ClientWindowSystem::main() { return WindowSystem::main().as(); } ClientWindow *ClientWindowSystem::mainPtr() { return static_cast(WindowSystem::mainPtr()); } void ClientWindowSystem::closingAllWindows() { // We can't get rid of the windows without tearing down GL stuff first. GL_Shutdown(); WindowSystem::closingAllWindows(); } bool ClientWindowSystem::rootProcessEvent(Event const &event) { /// @todo Multiwindow? -jk return main().root().processEvent(event); } void ClientWindowSystem::rootUpdate() { /// @todo Multiwindow? -jk main().root().update(); } doomsday-stable-1.15.7/doomsday/client/src/ui/b_main.cpp0000664000175000017500000001134212641367670022427 0ustar jaakkojaakko/** @file b_main.cpp Event and device state bindings system. * * @todo Pretty much everything in this file belongs in InputSystem. * * @authors Copyright © 2009-2013 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/b_main.h" #include "dd_main.h" // App_GameLoaded #include "dd_def.h" #include "clientapp.h" #include "BindContext" using namespace de; /** * Binding context fallback for the "global" context. * * @param ddev Event being processed. * * @return @c true if the event was eaten and can be processed by the rest of the * binding context stack. */ static int globalContextFallback(ddevent_t const *ddev) { if(App_GameLoaded() && !BusyMode_Active()) { event_t ev; if(InputSystem::convertEvent(*ddev, ev)) { // The game's normal responder only returns true if the bindings can't // be used (like when chatting). Note that if the event is eaten here, // the rest of the bindings contexts won't get a chance to process the // event. if(gx.Responder && gx.Responder(&ev)) { return true; } } } return false; } /// @note Called once on init. void B_Init() { InputSystem &isys = ClientApp::inputSystem(); // In dedicated mode we have fewer binding contexts available. // The contexts are defined in reverse order, with the context of lowest // priority defined first. isys.newContext(DEFAULT_BINDING_CONTEXT_NAME); // Game contexts. /// @todo Game binding context setup obviously belong to the game plugin, so shouldn't be here. isys.newContext("map"); isys.newContext("map-freepan"); isys.newContext("finale"); // uses a fallback responder to handle script events isys.newContext("menu")->acquireAll(); isys.newContext("gameui"); isys.newContext("shortcut"); isys.newContext("chat")->acquire(IDEV_KEYBOARD); isys.newContext("message")->acquireAll(); // Binding context for the console. BindContext *bc = isys.newContext(CONSOLE_BINDING_CONTEXT_NAME); bc->protect(); // Only we can (de)activate. bc->acquire(IDEV_KEYBOARD); // Console takes over all keyboard events. // UI doesn't let anything past it. isys.newContext(UI_BINDING_CONTEXT_NAME)->acquireAll(); // Top-level context that is always active and overrides every other context. // To be used only for system-level functionality. bc = isys.newContext(GLOBAL_BINDING_CONTEXT_NAME); bc->protect(); bc->setDDFallbackResponder(globalContextFallback); bc->activate(); /* B_BindCommand("joy-hat-angle3", "print {angle 3}"); B_BindCommand("joy-hat-center", "print center"); B_BindCommand("game:key-m-press", "print hello"); B_BindCommand("key-codex20-up", "print {space released}"); B_BindCommand("key-up-down + key-shift + key-ctrl", "print \"shifted and controlled up\""); B_BindCommand("key-up-down + key-shift", "print \"shifted up\""); B_BindCommand("mouse-left", "print mbpress"); B_BindCommand("mouse-right-up", "print \"right mb released\""); B_BindCommand("joy-x-neg1.0 + key-ctrl-up", "print \"joy x negative without ctrl\""); B_BindCommand("joy-x- within 0.1 + joy-y-pos1", "print \"joy x centered\""); B_BindCommand("joy-x-pos1.0", "print \"joy x positive\""); B_BindCommand("joy-x-neg1.0", "print \"joy x negative\""); B_BindCommand("joy-z-pos1.0", "print \"joy z positive\""); B_BindCommand("joy-z-neg1.0", "print \"joy z negative\""); B_BindCommand("joy-w-pos1.0", "print \"joy w positive\""); B_BindCommand("joy-w-neg1.0", "print \"joy w negative\""); */ /*B_BindControl("turn", "key-left-staged-inverse"); B_BindControl("turn", "key-right-staged"); B_BindControl("turn", "mouse-x"); B_BindControl("turn", "joy-x + key-shift-up + joy-hat-center + key-code123-down"); */ // Bind all the defaults for the engine only. isys.bindDefaults(); isys.initialContextActivations(); } doomsday-stable-1.15.7/doomsday/client/src/ui/styledlogsinkformatter.cpp0000664000175000017500000000557112641367670026030 0ustar jaakkojaakko/** @file styledlogsinkformatter.cpp * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/styledlogsinkformatter.h" #include #include #include using namespace de; static char const *VAR_METADATA = "log.showMetadata"; DENG2_PIMPL(StyledLogSinkFormatter) , DENG2_OBSERVES(Variable, Change) { LogEntry::Flags format; bool observe; bool omitSectionIfNonDev; bool showMetadata; Instance(Public *i, bool observeVars) : Base(i) , observe(observeVars) , omitSectionIfNonDev(true) , showMetadata(false) { if(observe) { showMetadata = App::config().getb(VAR_METADATA); App::config()[VAR_METADATA].audienceForChange() += this; } } ~Instance() { if(observe) { App::config()[VAR_METADATA].audienceForChange() -= this; } } void variableValueChanged(Variable &, Value const &newValue) { showMetadata = newValue.isTrue(); } }; StyledLogSinkFormatter::StyledLogSinkFormatter() : d(new Instance(this, true /*observe*/)) { d->format = LogEntry::Styled | LogEntry::OmitLevel; } StyledLogSinkFormatter::StyledLogSinkFormatter(LogEntry::Flags const &formatFlags) : d(new Instance(this, false /*don't observe*/)) { d->format = formatFlags; } LogSink::IFormatter::Lines StyledLogSinkFormatter::logEntryToTextLines(LogEntry const &entry) { LogEntry::Flags form = d->format; if(!d->showMetadata) { form |= LogEntry::Simple | LogEntry::OmitDomain; } if(d->omitSectionIfNonDev && !(entry.context() & LogEntry::Dev)) { // The sections refer to names of native code functions, etc. // These are relevant only to developers. Non-dev messages must be // clear enough to understand without the sections. form |= LogEntry::OmitSection; } // This will form a single long line. The line wrapper will // then determine how to wrap it onto the available width. return Lines() << entry.asText(form); } void StyledLogSinkFormatter::setOmitSectionIfNonDev(bool omit) { d->omitSectionIfNonDev = omit; } doomsday-stable-1.15.7/doomsday/client/src/ui/nativeui.cpp0000664000175000017500000001055512641367670023033 0ustar jaakkojaakko/** @file nativeui.cpp Native GUI functionality. * @ingroup base * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include #include "dd_share.h" #include "sys_system.h" #include #include #include #include #include #include #include "ui/nativeui.h" #include "ui/clientwindow.h" void Sys_MessageBox(messageboxtype_t type, const char* title, const char* msg, const char* detailedMsg) { Sys_MessageBox2(type, title, msg, 0, detailedMsg); } void Sys_MessageBox2(messageboxtype_t type, const char* title, const char* msg, const char* informativeMsg, const char* detailedMsg) { Sys_MessageBox3(type, title, msg, informativeMsg, detailedMsg, 0); } int Sys_MessageBox3(messageboxtype_t type, const char* title, const char* msg, const char* informativeMsg, const char* detailedMsg, const char** buttons) { if(novideo) { // There's no GUI... qWarning("%s", msg); return 0; } if(ClientWindow::mainExists()) { ClientWindow::main().hide(); } QMessageBox box; box.setWindowTitle(title); box.setText(msg); switch(type) { case MBT_INFORMATION: box.setIcon(QMessageBox::Information); break; case MBT_QUESTION: box.setIcon(QMessageBox::Question); break; case MBT_WARNING: box.setIcon(QMessageBox::Warning); break; case MBT_ERROR: box.setIcon(QMessageBox::Critical); break; default: break; } if(detailedMsg) { /// @todo Making the dialog a bit wider would be nice, but it seems one has to /// derive a new message box class for that -- the default one has a fixed size. box.setDetailedText(detailedMsg); } if(informativeMsg) { box.setInformativeText(informativeMsg); } if(buttons) { for(int i = 0; buttons[i]; ++i) { box.addButton(buttons[i], i == 0? QMessageBox::AcceptRole : i == 1? QMessageBox::RejectRole : QMessageBox::ActionRole); } } return box.exec(); } void Sys_MessageBoxf(messageboxtype_t type, const char* title, const char* format, ...) { char buffer[1024]; va_list args; va_start(args, format); dd_vsnprintf(buffer, sizeof(buffer), format, args); va_end(args); Sys_MessageBox(type, title, buffer, 0); } int Sys_MessageBoxWithButtons(messageboxtype_t type, const char* title, const char* msg, const char* informativeMsg, const char** buttons) { return Sys_MessageBox3(type, title, msg, informativeMsg, 0, buttons); } void Sys_MessageBoxWithDetailsFromFile(messageboxtype_t type, const char* title, const char* msg, const char* informativeMsg, const char* detailsFileName) { try { de::Block details; de::ByteArrayFile const &file = de::App::rootFolder().locate(detailsFileName); file >> details; // This will be used as a null-terminated string. details.append('\0'); Sys_MessageBox2(type, title, msg, informativeMsg, details.constData()); } catch(de::Error const &er) { qWarning() << "Could not read" << detailsFileName << ":" << er.asText().toLatin1().constData(); // Show it without the details, then. Sys_MessageBox2(type, title, msg, informativeMsg, 0); } } doomsday-stable-1.15.7/doomsday/client/src/ui/inputdebug.cpp0000664000175000017500000005535012641367670023357 0ustar jaakkojaakko/** @file inputdebug.cpp Input debug visualizer. * * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" // strdup macro #include "ui/inputdebug.h" #ifdef DENG2_DEBUG #include #include #include // SECONDSPERTIC #include #include #include #include "clientapp.h" #include "dd_def.h" #include "dd_main.h" #include "sys_system.h" // novideo #include "ui/b_main.h" #include "ui/joystick.h" #include "ui/infine/finale.h" #include "ui/inputdevice.h" #include "ui/inputdeviceaxiscontrol.h" #include "ui/inputdevicebuttoncontrol.h" #include "ui/inputdevicehatcontrol.h" #include "ui/sys_input.h" #include "ui/ui_main.h" #include "ui/b_util.h" #include #include "de_graphics.h" #include "api_fontrender.h" using namespace de; static byte devRendJoyState; static byte devRendKeyState; static byte devRendMouseState; #define MAX_KEYMAPPINGS 256 static uchar shiftKeyMappings[MAX_KEYMAPPINGS]; // Initialize key mapping table. static void initKeyMappingsOnce() { // Already been here? if(shiftKeyMappings[1] == 1) return; uchar defaultShiftTable[96] = // Contains characters 32 to 127. { /* 32 */ ' ', 0, 0, 0, 0, 0, 0, '"', /* 40 */ 0, 0, 0, 0, '<', '_', '>', '?', ')', '!', /* 50 */ '@', '#', '$', '%', '^', '&', '*', '(', 0, ':', /* 60 */ 0, '+', 0, 0, 0, 'a', 'b', 'c', 'd', 'e', /* 70 */ 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', /* 80 */ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', /* 90 */ 'z', '{', '|', '}', 0, 0, '~', 'A', 'B', 'C', /* 100 */ 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', /* 110 */ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', /* 120 */ 'X', 'Y', 'Z', 0, 0, 0, 0, 0 }; /// @todo does not belong at this level. for(int i = 0; i < 256; ++i) { if(i >= 32 && i <= 127) shiftKeyMappings[i] = defaultShiftTable[i - 32] ? defaultShiftTable[i - 32] : i; else shiftKeyMappings[i] = i; } } static inline InputSystem &inputSys() { return ClientApp::inputSystem(); } static void initDrawStateForVisual(Point2Raw const *origin) { FR_PushAttrib(); // Ignore zero offsets. if(origin && !(origin->x == 0 && origin->y == 0)) { glMatrixMode(GL_MODELVIEW); glPushMatrix(); glTranslatef(origin->x, origin->y, 0); } } static void endDrawStateForVisual(Point2Raw const *origin) { // Ignore zero offsets. if(origin && !(origin->x == 0 && origin->y == 0)) { glMatrixMode(GL_MODELVIEW); glPopMatrix(); } FR_PopAttrib(); } /** * Apply all active modifiers to the key. */ static uchar modKey(int ddkey) { extern bool shiftDown; initKeyMappingsOnce(); if(shiftDown) { DENG2_ASSERT(ddkey >= 0 && ddkey < MAX_KEYMAPPINGS); ddkey = shiftKeyMappings[ddkey]; } if(ddkey >= DDKEY_NUMPAD7 && ddkey <= DDKEY_NUMPAD0) { static uchar const numPadKeys[10] = { '7', '8', '9', '4', '5', '6', '1', '2', '3', '0' }; return numPadKeys[ddkey - DDKEY_NUMPAD7]; } else if(ddkey == DDKEY_DIVIDE) { return '/'; } else if(ddkey == DDKEY_SUBTRACT) { return '-'; } else if(ddkey == DDKEY_ADD) { return '+'; } else if(ddkey == DDKEY_DECIMAL) { return '.'; } else if(ddkey == DDKEY_MULTIPLY) { return '*'; } return uchar(ddkey); } void Rend_RenderButtonStateVisual(InputDevice &device, int buttonID, Point2Raw const *_origin, RectRaw *geometry) { #define BORDER 4 float const upColor[] = { .3f, .3f, .3f, .6f }; float const downColor[] = { .3f, .3f, 1, .6f }; float const expiredMarkColor[] = { 1, 0, 0, 1 }; float const triggeredMarkColor[] = { 1, 0, 1, 1 }; if(geometry) { geometry->origin.x = geometry->origin.y = 0; geometry->size.width = geometry->size.height = 0; } InputDeviceButtonControl const &button = device.button(buttonID); Point2Raw origin; origin.x = _origin? _origin->x : 0; origin.y = _origin? _origin->y : 0; // Compose the label. String label; if(!button.name().isEmpty()) { // Use the symbolic name. label = button.name(); } else if(&device == ClientApp::inputSystem().devicePtr(IDEV_KEYBOARD)) { // Perhaps a printable ASCII character? // Apply all active modifiers to the key. uchar asciiCode = modKey(buttonID); if(asciiCode > 32 && asciiCode < 127) { label = String("%1").arg(QChar(asciiCode)); } // Is there symbolic name in the bindings system? if(label.isEmpty()) { label = B_ShortNameForKey(buttonID, false/*do not force lowercase*/); } } if(label.isEmpty()) { label = String("#%1").arg(buttonID, 3, 10, QChar('0')); } initDrawStateForVisual(&origin); // Calculate the size of the visual according to the dimensions of the text. Size2Raw textSize; FR_TextSize(&textSize, label.toUtf8().constData()); // Enlarge by BORDER pixels. Rectanglei textGeom = Rectanglei::fromSize(Vector2i(0, 0), Vector2ui(textSize.width + BORDER * 2, textSize.height + BORDER * 2)); // Draw a background. glColor4fv(button.isDown()? downColor : upColor); GL_DrawRect(textGeom); // Draw the text. glEnable(GL_TEXTURE_2D); Point2Raw const textOffset(BORDER, BORDER); FR_DrawText(label.toUtf8().constData(), &textOffset); glDisable(GL_TEXTURE_2D); // Mark expired? if(button.bindContextAssociation() & InputDeviceControl::Expired) { int const markSize = .5f + de::min(textGeom.width(), textGeom.height()) / 3.f; glColor3fv(expiredMarkColor); glBegin(GL_TRIANGLES); glVertex2i(textGeom.width(), 0); glVertex2i(textGeom.width(), markSize); glVertex2i(textGeom.width() - markSize, 0); glEnd(); } // Mark triggered? if(button.bindContextAssociation() & InputDeviceControl::Triggered) { int const markSize = .5f + de::min(textGeom.width(), textGeom.height()) / 3.f; glColor3fv(triggeredMarkColor); glBegin(GL_TRIANGLES); glVertex2i(0, 0); glVertex2i(markSize, 0); glVertex2i(0, markSize); glEnd(); } endDrawStateForVisual(&origin); if(geometry) { std::memcpy(&geometry->origin, &origin, sizeof(geometry->origin)); geometry->size.width = textGeom.width(); geometry->size.height = textGeom.height(); } #undef BORDER } void Rend_RenderAxisStateVisual(InputDevice & /*device*/, int /*axisID*/, Point2Raw const *origin, RectRaw *geometry) { if(geometry) { geometry->origin.x = geometry->origin.y = 0; geometry->size.width = geometry->size.height = 0; } //inputdevaxis_t const &axis = device.axis(axisID); initDrawStateForVisual(origin); endDrawStateForVisual(origin); } void Rend_RenderHatStateVisual(InputDevice & /*device*/, int /*hatID*/, Point2Raw const *origin, RectRaw *geometry) { if(geometry) { geometry->origin.x = geometry->origin.y = 0; geometry->size.width = geometry->size.height = 0; } //inputdevhat_t const &hat = device.hat(hatID); initDrawStateForVisual(origin); endDrawStateForVisual(origin); } // Input device control types: enum inputdev_controltype_t { IDC_KEY, IDC_AXIS, IDC_HAT, NUM_INPUT_DEVICE_CONTROL_TYPES }; struct inputdev_layout_control_t { inputdev_controltype_t type; uint id; }; struct inputdev_layout_controlgroup_t { inputdev_layout_control_t *controls; uint numControls; }; // Defines the order of controls in the visual. struct inputdev_layout_t { inputdev_layout_controlgroup_t *groups; uint numGroups; }; static void drawControlGroup(InputDevice &device, inputdev_layout_controlgroup_t const *group, Point2Raw *_origin, RectRaw *retGeometry) { #define SPACING 2 if(retGeometry) { retGeometry->origin.x = retGeometry->origin.y = 0; retGeometry->size.width = retGeometry->size.height = 0; } if(!group) return; Point2Raw origin; origin.x = _origin? _origin->x : 0; origin.y = _origin? _origin->y : 0; Rect *grpGeom = nullptr; RectRaw ctrlGeom; for(uint i = 0; i < group->numControls; ++i) { inputdev_layout_control_t const *ctrl = group->controls + i; switch(ctrl->type) { case IDC_AXIS: Rend_RenderAxisStateVisual(device, ctrl->id, &origin, &ctrlGeom); break; case IDC_KEY: Rend_RenderButtonStateVisual(device, ctrl->id, &origin, &ctrlGeom); break; case IDC_HAT: Rend_RenderHatStateVisual(device, ctrl->id, &origin, &ctrlGeom); break; default: App_Error("drawControlGroup: Unknown inputdev_controltype_t: %i.", (int)ctrl->type); exit(1); // Unreachable. } if(ctrlGeom.size.width > 0 && ctrlGeom.size.height > 0) { origin.x += ctrlGeom.size.width + SPACING; if(grpGeom) Rect_UniteRaw(grpGeom, &ctrlGeom); else grpGeom = Rect_NewFromRaw(&ctrlGeom); } } if(grpGeom) { if(retGeometry) { Rect_Raw(grpGeom, retGeometry); } Rect_Delete(grpGeom); } #undef SPACING } /** * Render a visual representation of the current state of the specified device. */ void Rend_RenderInputDeviceStateVisual(InputDevice &device, inputdev_layout_t const *layout, Point2Raw const *origin, Size2Raw *retVisualDimensions) { #define SPACING 2 DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); if(retVisualDimensions) { retVisualDimensions->width = retVisualDimensions->height = 0; } if(novideo || isDedicated) return; // Not for us. if(!layout) return; // Init render state. FR_SetFont(fontFixed); FR_PushAttrib(); FR_LoadDefaultAttrib(); FR_SetLeading(0); FR_SetColorAndAlpha(1, 1, 1, 1); initDrawStateForVisual(origin); Point2Raw offset; Rect *visualGeom = nullptr; // Draw device name first. if(!device.title().isEmpty()) { Size2Raw size; glEnable(GL_TEXTURE_2D); Block const fullName(device.title().toUtf8()); FR_DrawText(fullName.constData(), nullptr/*no offset*/); glDisable(GL_TEXTURE_2D); FR_TextSize(&size, fullName.constData()); visualGeom = Rect_NewWithOriginSize2(offset.x, offset.y, size.width, size.height); offset.y += size.height + SPACING; } // Draw control groups. for(uint i = 0; i < layout->numGroups; ++i) { inputdev_layout_controlgroup_t const *grp = &layout->groups[i]; RectRaw grpGeometry; drawControlGroup(device, grp, &offset, &grpGeometry); if(grpGeometry.size.width > 0 && grpGeometry.size.height > 0) { if(visualGeom) Rect_UniteRaw(visualGeom, &grpGeometry); else visualGeom = Rect_NewFromRaw(&grpGeometry); offset.y = Rect_Y(visualGeom) + Rect_Height(visualGeom) + SPACING; } } // Back to previous render state. endDrawStateForVisual(origin); FR_PopAttrib(); // Return the united geometry dimensions? if(visualGeom && retVisualDimensions) { retVisualDimensions->width = Rect_Width(visualGeom); retVisualDimensions->height = Rect_Height(visualGeom); } #undef SPACING } void I_DebugDrawer() { #define SPACING 2 #define NUMITEMS(x) (sizeof(x) / sizeof((x)[0])) // Keyboard (Standard US English layout): static inputdev_layout_control_t keyGroup1[] = { { IDC_KEY, 27 }, // escape { IDC_KEY, 132 }, // f1 { IDC_KEY, 133 }, // f2 { IDC_KEY, 134 }, // f3 { IDC_KEY, 135 }, // f4 { IDC_KEY, 136 }, // f5 { IDC_KEY, 137 }, // f6 { IDC_KEY, 138 }, // f7 { IDC_KEY, 139 }, // f8 { IDC_KEY, 140 }, // f9 { IDC_KEY, 141 }, // f10 { IDC_KEY, 142 }, // f11 { IDC_KEY, 143 } // f12 }; static inputdev_layout_control_t keyGroup2[] = { { IDC_KEY, 96 }, // tilde { IDC_KEY, 49 }, // 1 { IDC_KEY, 50 }, // 2 { IDC_KEY, 51 }, // 3 { IDC_KEY, 52 }, // 4 { IDC_KEY, 53 }, // 5 { IDC_KEY, 54 }, // 6 { IDC_KEY, 55 }, // 7 { IDC_KEY, 56 }, // 8 { IDC_KEY, 57 }, // 9 { IDC_KEY, 48 }, // 0 { IDC_KEY, 45 }, // - { IDC_KEY, 61 }, // = { IDC_KEY, 127 } // backspace }; static inputdev_layout_control_t keyGroup3[] = { { IDC_KEY, 9 }, // tab { IDC_KEY, 113 }, // q { IDC_KEY, 119 }, // w { IDC_KEY, 101 }, // e { IDC_KEY, 114 }, // r { IDC_KEY, 116 }, // t { IDC_KEY, 121 }, // y { IDC_KEY, 117 }, // u { IDC_KEY, 105 }, // i { IDC_KEY, 111 }, // o { IDC_KEY, 112 }, // p { IDC_KEY, 91 }, // { { IDC_KEY, 93 }, // } { IDC_KEY, 92 }, // bslash }; static inputdev_layout_control_t keyGroup4[] = { { IDC_KEY, 145 }, // capslock { IDC_KEY, 97 }, // a { IDC_KEY, 115 }, // s { IDC_KEY, 100 }, // d { IDC_KEY, 102 }, // f { IDC_KEY, 103 }, // g { IDC_KEY, 104 }, // h { IDC_KEY, 106 }, // j { IDC_KEY, 107 }, // k { IDC_KEY, 108 }, // l { IDC_KEY, 59 }, // semicolon { IDC_KEY, 39 }, // apostrophe { IDC_KEY, 13 } // return }; static inputdev_layout_control_t keyGroup5[] = { { IDC_KEY, 159 }, // shift { IDC_KEY, 122 }, // z { IDC_KEY, 120 }, // x { IDC_KEY, 99 }, // c { IDC_KEY, 118 }, // v { IDC_KEY, 98 }, // b { IDC_KEY, 110 }, // n { IDC_KEY, 109 }, // m { IDC_KEY, 44 }, // comma { IDC_KEY, 46 }, // period { IDC_KEY, 47 }, // fslash { IDC_KEY, 159 }, // shift }; static inputdev_layout_control_t keyGroup6[] = { { IDC_KEY, 160 }, // ctrl { IDC_KEY, 0 }, // ??? { IDC_KEY, 161 }, // alt { IDC_KEY, 32 }, // space { IDC_KEY, 161 }, // alt { IDC_KEY, 0 }, // ??? { IDC_KEY, 0 }, // ??? { IDC_KEY, 160 } // ctrl }; static inputdev_layout_control_t keyGroup7[] = { { IDC_KEY, 170 }, // printscrn { IDC_KEY, 146 }, // scrolllock { IDC_KEY, 158 } // pause }; static inputdev_layout_control_t keyGroup8[] = { { IDC_KEY, 162 }, // insert { IDC_KEY, 166 }, // home { IDC_KEY, 164 } // pageup }; static inputdev_layout_control_t keyGroup9[] = { { IDC_KEY, 163 }, // delete { IDC_KEY, 167 }, // end { IDC_KEY, 165 }, // pagedown }; static inputdev_layout_control_t keyGroup10[] = { { IDC_KEY, 130 }, // up { IDC_KEY, 129 }, // left { IDC_KEY, 131 }, // down { IDC_KEY, 128 }, // right }; static inputdev_layout_control_t keyGroup11[] = { { IDC_KEY, 144 }, // numlock { IDC_KEY, 172 }, // divide { IDC_KEY, DDKEY_MULTIPLY }, // multiply { IDC_KEY, 168 } // subtract }; static inputdev_layout_control_t keyGroup12[] = { { IDC_KEY, 147 }, // pad 7 { IDC_KEY, 148 }, // pad 8 { IDC_KEY, 149 }, // pad 9 { IDC_KEY, 169 }, // add }; static inputdev_layout_control_t keyGroup13[] = { { IDC_KEY, 150 }, // pad 4 { IDC_KEY, 151 }, // pad 5 { IDC_KEY, 152 } // pad 6 }; static inputdev_layout_control_t keyGroup14[] = { { IDC_KEY, 153 }, // pad 1 { IDC_KEY, 154 }, // pad 2 { IDC_KEY, 155 }, // pad 3 { IDC_KEY, 171 } // enter }; static inputdev_layout_control_t keyGroup15[] = { { IDC_KEY, 156 }, // pad 0 { IDC_KEY, 157 } // pad . }; static inputdev_layout_controlgroup_t keyGroups[] = { { keyGroup1, NUMITEMS(keyGroup1) }, { keyGroup2, NUMITEMS(keyGroup2) }, { keyGroup3, NUMITEMS(keyGroup3) }, { keyGroup4, NUMITEMS(keyGroup4) }, { keyGroup5, NUMITEMS(keyGroup5) }, { keyGroup6, NUMITEMS(keyGroup6) }, { keyGroup7, NUMITEMS(keyGroup7) }, { keyGroup8, NUMITEMS(keyGroup8) }, { keyGroup9, NUMITEMS(keyGroup9) }, { keyGroup10, NUMITEMS(keyGroup10) }, { keyGroup11, NUMITEMS(keyGroup11) }, { keyGroup12, NUMITEMS(keyGroup12) }, { keyGroup13, NUMITEMS(keyGroup13) }, { keyGroup14, NUMITEMS(keyGroup14) }, { keyGroup15, NUMITEMS(keyGroup15) } }; static inputdev_layout_t keyLayout = { keyGroups, NUMITEMS(keyGroups) }; // Mouse: static inputdev_layout_control_t mouseGroup1[] = { { IDC_AXIS, 0 }, // x-axis { IDC_AXIS, 1 }, // y-axis }; static inputdev_layout_control_t mouseGroup2[] = { { IDC_KEY, 0 }, // left { IDC_KEY, 1 }, // middle { IDC_KEY, 2 } // right }; static inputdev_layout_control_t mouseGroup3[] = { { IDC_KEY, 3 }, // wheelup { IDC_KEY, 4 } // wheeldown }; static inputdev_layout_control_t mouseGroup4[] = { { IDC_KEY, 5 }, { IDC_KEY, 6 }, { IDC_KEY, 7 }, { IDC_KEY, 8 }, { IDC_KEY, 9 }, { IDC_KEY, 10 }, { IDC_KEY, 11 }, { IDC_KEY, 12 }, { IDC_KEY, 13 }, { IDC_KEY, 14 }, { IDC_KEY, 15 } }; static inputdev_layout_controlgroup_t mouseGroups[] = { { mouseGroup1, NUMITEMS(mouseGroup1) }, { mouseGroup2, NUMITEMS(mouseGroup2) }, { mouseGroup3, NUMITEMS(mouseGroup3) }, { mouseGroup4, NUMITEMS(mouseGroup4) } }; static inputdev_layout_t mouseLayout = { mouseGroups, NUMITEMS(mouseGroups) }; // Joystick: static inputdev_layout_control_t joyGroup1[] = { { IDC_AXIS, 0 }, // x-axis { IDC_AXIS, 1 }, // y-axis { IDC_AXIS, 2 }, // z-axis { IDC_AXIS, 3 } // w-axis }; static inputdev_layout_control_t joyGroup2[] = { { IDC_AXIS, 4 }, { IDC_AXIS, 5 }, { IDC_AXIS, 6 }, { IDC_AXIS, 7 }, { IDC_AXIS, 8 }, { IDC_AXIS, 9 }, { IDC_AXIS, 10 }, { IDC_AXIS, 11 }, { IDC_AXIS, 12 }, { IDC_AXIS, 13 }, { IDC_AXIS, 14 }, { IDC_AXIS, 15 }, { IDC_AXIS, 16 }, { IDC_AXIS, 17 } }; static inputdev_layout_control_t joyGroup3[] = { { IDC_AXIS, 18 }, { IDC_AXIS, 19 }, { IDC_AXIS, 20 }, { IDC_AXIS, 21 }, { IDC_AXIS, 22 }, { IDC_AXIS, 23 }, { IDC_AXIS, 24 }, { IDC_AXIS, 25 }, { IDC_AXIS, 26 }, { IDC_AXIS, 27 }, { IDC_AXIS, 28 }, { IDC_AXIS, 29 }, { IDC_AXIS, 30 }, { IDC_AXIS, 31 } }; static inputdev_layout_control_t joyGroup4[] = { { IDC_HAT, 0 }, { IDC_HAT, 1 }, { IDC_HAT, 2 }, { IDC_HAT, 3 } }; static inputdev_layout_control_t joyGroup5[] = { { IDC_KEY, 0 }, { IDC_KEY, 1 }, { IDC_KEY, 2 }, { IDC_KEY, 3 }, { IDC_KEY, 4 }, { IDC_KEY, 5 }, { IDC_KEY, 6 }, { IDC_KEY, 7 } }; static inputdev_layout_control_t joyGroup6[] = { { IDC_KEY, 8 }, { IDC_KEY, 9 }, { IDC_KEY, 10 }, { IDC_KEY, 11 }, { IDC_KEY, 12 }, { IDC_KEY, 13 }, { IDC_KEY, 14 }, { IDC_KEY, 15 }, }; static inputdev_layout_control_t joyGroup7[] = { { IDC_KEY, 16 }, { IDC_KEY, 17 }, { IDC_KEY, 18 }, { IDC_KEY, 19 }, { IDC_KEY, 20 }, { IDC_KEY, 21 }, { IDC_KEY, 22 }, { IDC_KEY, 23 }, }; static inputdev_layout_control_t joyGroup8[] = { { IDC_KEY, 24 }, { IDC_KEY, 25 }, { IDC_KEY, 26 }, { IDC_KEY, 27 }, { IDC_KEY, 28 }, { IDC_KEY, 29 }, { IDC_KEY, 30 }, { IDC_KEY, 31 }, }; static inputdev_layout_controlgroup_t joyGroups[] = { { joyGroup1, NUMITEMS(joyGroup1) }, { joyGroup2, NUMITEMS(joyGroup2) }, { joyGroup3, NUMITEMS(joyGroup3) }, { joyGroup4, NUMITEMS(joyGroup4) }, { joyGroup5, NUMITEMS(joyGroup5) }, { joyGroup6, NUMITEMS(joyGroup6) }, { joyGroup7, NUMITEMS(joyGroup7) }, { joyGroup8, NUMITEMS(joyGroup8) } }; static inputdev_layout_t joyLayout = { joyGroups, NUMITEMS(joyGroups) }; Point2Raw origin(2, 2); Size2Raw dimensions; if(novideo || isDedicated) return; // Not for us. DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // Disabled? if(!devRendKeyState && !devRendMouseState && !devRendJoyState) return; glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, DENG_GAMEVIEW_WIDTH, DENG_GAMEVIEW_HEIGHT, 0, -1, 1); if(devRendKeyState) { Rend_RenderInputDeviceStateVisual(inputSys().device(IDEV_KEYBOARD), &keyLayout, &origin, &dimensions); origin.y += dimensions.height + SPACING; } if(devRendMouseState) { Rend_RenderInputDeviceStateVisual(inputSys().device(IDEV_MOUSE), &mouseLayout, &origin, &dimensions); origin.y += dimensions.height + SPACING; } if(devRendJoyState) { Rend_RenderInputDeviceStateVisual(inputSys().device(IDEV_JOY1), &joyLayout, &origin, &dimensions); } glMatrixMode(GL_PROJECTION); glPopMatrix(); #undef NUMITEMS #undef SPACING } void I_DebugDrawerConsoleRegister() { C_VAR_BYTE("rend-dev-input-joy-state", &devRendJoyState, CVF_NO_ARCHIVE, 0, 1); C_VAR_BYTE("rend-dev-input-key-state", &devRendKeyState, CVF_NO_ARCHIVE, 0, 1); C_VAR_BYTE("rend-dev-input-mouse-state", &devRendMouseState, CVF_NO_ARCHIVE, 0, 1); } #endif // DENG2_DEBUG doomsday-stable-1.15.7/doomsday/client/src/ui/dialogs/0000775000175000017500000000000012641367670022117 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/ui/dialogs/vrsettingsdialog.cpp0000664000175000017500000002007412641367670026216 0ustar jaakkojaakko/** @file vrsettingsdialog.cpp Settings for virtual reality. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/dialogs/vrsettingsdialog.h" #include "ui/widgets/cvarsliderwidget.h" #include "ui/widgets/cvartogglewidget.h" #include "ui/widgets/cvarchoicewidget.h" #include "render/vr.h" #include "clientapp.h" #include "api_console.h" #include #include #include #include "CommandAction" using namespace de; using namespace ui; DENG_GUI_PIMPL(VRSettingsDialog) { CVarChoiceWidget *mode; CVarToggleWidget *swapEyes; CVarSliderWidget *dominantEye; CVarSliderWidget *humanHeight; CVarSliderWidget *ipd; VariableSliderWidget *riftDensity; CVarSliderWidget *riftSamples; ButtonWidget *riftReset; ButtonWidget *riftSetup; ButtonWidget *desktopSetup; Instance(Public *i) : Base(i) , riftReset(0) , riftSetup(0) { ScrollAreaWidget &area = self.area(); area.add(mode = new CVarChoiceWidget("rend-vr-mode")); mode->items() << new ChoiceItem(tr("No stereo"), VRConfig::Mono) << new ChoiceItem(tr("Anaglyph (green/magenta)"), VRConfig::GreenMagenta) << new ChoiceItem(tr("Anaglyph (red/cyan)"), VRConfig::RedCyan) << new ChoiceItem(tr("Left eye only"), VRConfig::LeftOnly) << new ChoiceItem(tr("Right eye only"), VRConfig::RightOnly) << new ChoiceItem(tr("Top/bottom"), VRConfig::TopBottom) << new ChoiceItem(tr("Side-by-side"), VRConfig::SideBySide) << new ChoiceItem(tr("Parallel"), VRConfig::Parallel) << new ChoiceItem(tr("Cross-eye"), VRConfig::CrossEye) << new ChoiceItem(tr("Hardware stereo"), VRConfig::QuadBuffered); if(vrCfg().oculusRift().isEnabled()) { mode->items() << new ChoiceItem(tr("Oculus Rift"), VRConfig::OculusRift); } area.add(swapEyes = new CVarToggleWidget("rend-vr-swap-eyes", tr("Swap Eyes"))); area.add(dominantEye = new CVarSliderWidget("rend-vr-dominant-eye")); area.add(humanHeight = new CVarSliderWidget("rend-vr-player-height")); area.add(riftSamples = new CVarSliderWidget("rend-vr-rift-samples")); area.add(ipd = new CVarSliderWidget("rend-vr-ipd")); ipd->setDisplayFactor(1000); ipd->setPrecision(1); if(vrCfg().oculusRift().isReady()) { area.add(riftDensity = new VariableSliderWidget(App::config("vr.oculusRift.pixelDensity"), Ranged(0.5, 1.0), .01)); riftDensity->setPrecision(2); area.add(riftReset = new ButtonWidget); riftReset->setText(tr("Recenter Tracking")); riftReset->setAction(new CommandAction("resetriftpose")); area.add(riftSetup = new ButtonWidget); riftSetup->setText(tr("Apply Rift Settings")); riftSetup->setAction(new SignalAction(thisPublic, SLOT(autoConfigForOculusRift()))); } area.add(desktopSetup = new ButtonWidget); desktopSetup->setText(tr("Apply Desktop Settings")); desktopSetup->setAction(new SignalAction(thisPublic, SLOT(autoConfigForDesktop()))); } void fetch() { foreach(Widget *child, self.area().childWidgets()) { if(ICVarWidget *w = child->maybeAs()) { w->updateFromCVar(); } } } }; VRSettingsDialog::VRSettingsDialog(String const &name) : DialogWidget(name, WithHeading), d(new Instance(this)) { heading().setText(tr("3D & VR Settings")); LabelWidget *modeLabel = LabelWidget::newWithText(tr("Stereo Mode:"), &area()); LabelWidget *heightLabel = LabelWidget::newWithText(tr("Height (m):"), &area()); LabelWidget *ipdLabel = LabelWidget::newWithText(tr("IPD (mm):"), &area()); LabelWidget *dominantLabel = LabelWidget::newWithText(tr("Dominant Eye:"), &area()); // Layout. GridLayout layout(area().contentRule().left(), area().contentRule().top()); layout.setGridSize(2, 0); layout.setColumnAlignment(0, ui::AlignRight); layout << *modeLabel << *d->mode << *heightLabel << *d->humanHeight << *ipdLabel << *d->ipd << *dominantLabel << *d->dominantEye << Const(0) << *d->swapEyes; LabelWidget *ovrLabel = LabelWidget::newWithText(_E(D) + tr("Oculus Rift"), &area()); LabelWidget *sampleLabel = LabelWidget::newWithText(tr("Multisampling:"), &area()); ovrLabel->setFont("separator.label"); ovrLabel->margins().setTop("gap"); sampleLabel->setTextLineAlignment(ui::AlignRight); layout.setCellAlignment(Vector2i(0, 5), ui::AlignLeft); layout.append(*ovrLabel, 2); LabelWidget *utilLabel = LabelWidget::newWithText(tr("Utilities:"), &area()); if(vrCfg().oculusRift().isReady()) { layout << *sampleLabel << *d->riftSamples << *LabelWidget::newWithText(tr("Pixel Density:"), &area()) << *d->riftDensity; layout << *utilLabel << *d->riftReset << Const(0) << *d->riftSetup << Const(0) << *d->desktopSetup; } else { layout << *utilLabel << *d->desktopSetup; } area().setContentSize(layout.width(), layout.height()); buttons() << new DialogButtonItem(DialogWidget::Default | DialogWidget::Accept, tr("Close")) << new DialogButtonItem(DialogWidget::Action, tr("Reset to Defaults"), new SignalAction(this, SLOT(resetToDefaults()))); d->fetch(); } void VRSettingsDialog::resetToDefaults() { Con_SetInteger("rend-vr-mode", VRConfig::Mono); Con_SetInteger("rend-vr-swap-eyes", 0); Con_SetFloat ("rend-vr-dominant-eye", 0); Con_SetFloat ("rend-vr-player-height", 1.75f); Con_SetFloat ("rend-vr-ipd", 0.064f); Con_SetFloat ("rend-vr-rift-latency", 0.030f); Con_SetInteger("rend-vr-rift-samples", 2); d->fetch(); } void VRSettingsDialog::autoConfigForOculusRift() { //Con_Execute(CMDS_DDAY, "setfullres 1280 800", false, false); Con_Execute(CMDS_DDAY, "bindcontrol lookpitch head-pitch", false, false); Con_Execute(CMDS_DDAY, "bindcontrol yawbody head-yaw", false, false); /// @todo This would be a good use case for cvar overriding. -jk Con_SetInteger("rend-vr-mode", VRConfig::OculusRift); App::config().set("window.main.fsaa", false); Con_SetFloat ("vid-gamma", 1.176f); Con_SetFloat ("vid-contrast", 1.186f); Con_SetFloat ("vid-bright", .034f); Con_SetFloat ("view-bob-height", .2f); Con_SetFloat ("msg-scale", 1); Con_SetFloat ("hud-scale", 1); d->fetch(); ClientApp::vr().oculusRift().moveWindowToScreen(OculusRift::HMDScreen); } void VRSettingsDialog::autoConfigForDesktop() { Con_SetInteger("rend-vr-mode", VRConfig::Mono); Con_SetFloat ("vid-gamma", 1); Con_SetFloat ("vid-contrast", 1); Con_SetFloat ("vid-bright", 0); Con_SetFloat ("view-bob-height", 1); Con_SetFloat ("msg-scale", .8f); Con_SetFloat ("hud-scale", .6f); d->fetch(); ClientApp::vr().oculusRift().moveWindowToScreen(OculusRift::DefaultScreen); } doomsday-stable-1.15.7/doomsday/client/src/ui/dialogs/logsettingsdialog.cpp0000664000175000017500000002102712641367670026347 0ustar jaakkojaakko/** @file logsettingsdialog.cpp Dialog for Log settings. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/dialogs/logsettingsdialog.h" #include "clientapp.h" #include #include #include #include using namespace de; struct DomainText { char const *name; char const *label; }; static DomainText const domainText[] = { { "generic", "Minimum Level" }, { "resource", "Resources" }, { "map", "Map" }, { "script", "Scripting" }, { "gl", "Graphics" }, { "audio", "Audio" }, { "input", "Input" }, { "network", "Network" } }; #define NUM_DOMAINS (sizeof(domainText)/sizeof(domainText[0])) DENG2_PIMPL(LogSettingsDialog) , DENG2_OBSERVES(ToggleWidget, Toggle) { ui::ListData levels; VariableToggleWidget *separately; FoldPanelWidget *fold; GridLayout foldLayout; IndirectRule *columnWidth; ///< Sync column width in and out of the fold. struct DomainWidgets { LabelWidget *label; VariableChoiceWidget *level; VariableToggleWidget *dev; VariableToggleWidget *alert; }; DomainWidgets domWidgets[NUM_DOMAINS]; Instance(Public *i) : Base(i) { try { zap(domWidgets); columnWidth = new IndirectRule; self.area().add(separately = new VariableToggleWidget(tr("Filter by Subsystem"), App::config("log.filterBySubsystem"))); levels << new ChoiceItem( tr("1 - X.Verbose"), LogEntry::XVerbose) << new ChoiceItem( tr("2 - Verbose"), LogEntry::Verbose ) << new ChoiceItem( tr("3 - Message"), LogEntry::Message ) << new ChoiceItem( tr("4 - Note"), LogEntry::Note ) << new ChoiceItem(_E(D) + tr("5 - Warning"), LogEntry::Warning ) << new ChoiceItem(_E(D) + tr("6 - Error"), LogEntry::Error ) << new ChoiceItem(_E(D) + tr("7 - Critical"), LogEntry::Critical); // Folding panel for the per-domain settings. self.area().add(fold = new FoldPanelWidget); fold->setContent(new GuiWidget); foldLayout.setLeftTop(fold->content().rule().left(), fold->content().rule().top()); foldLayout.setGridSize(4, 0); foldLayout.setColumnFixedWidth(1, *columnWidth); foldLayout.setColumnAlignment(0, ui::AlignRight); for(uint i = 0; i < NUM_DOMAINS; ++i) { initDomain(domainText[i], domWidgets[i], i == 0? &self.area() : &fold->content()); } // This'll keep the dialog's size fixed even though the choices change size. columnWidth->setSource(domWidgets[0].level->maximumWidth()); separately->audienceForToggle() += this; } catch(Error const &er) { LOGDEV_ERROR("") << er.asText(); deinit(); throw; } } ~Instance() { deinit(); releaseRef(columnWidth); } void initDomain(DomainText const &dom, DomainWidgets &wgt, GuiWidget *parent) { // Text label. wgt.label = LabelWidget::newWithText(tr(dom.label) + ":", parent); // Minimum level for log entries. parent->add(wgt.level = new VariableChoiceWidget(App::config()[String("log.filter.%1.minLevel").arg(dom.name)])); wgt.level->setItems(levels); wgt.level->updateFromVariable(); QObject::connect(wgt.level, SIGNAL(selectionChangedByUser(uint)), thisPublic, SLOT(updateLogFilter())); // Developer messages? parent->add(wgt.dev = new VariableToggleWidget(tr("Dev"), App::config()[String("log.filter.%1.allowDev").arg(dom.name)])); QObject::connect(wgt.dev, SIGNAL(stateChangedByUser(ToggleWidget::ToggleState)), thisPublic, SLOT(updateLogFilter())); // Raise alerts? parent->add(wgt.alert = new VariableToggleWidget(tr("Alerts"), App::config()[String("alert.") + dom.name])); wgt.alert->setActiveValue(LogEntry::Warning); wgt.alert->setInactiveValue(LogEntry::HighestLogLevel + 1); // Lay out the folding panel's contents. if(parent == &fold->content()) { foldLayout << *wgt.label << *wgt.level << *wgt.dev << *wgt.alert; } } void deinit() { // The common 'levels' will be deleted soon. for(uint i = 0; i < NUM_DOMAINS; ++i) { if(domWidgets[i].level) { domWidgets[i].level->useDefaultItems(); } } } void overrideWithGeneric() { Config &cfg = App::config(); LogFilter &logf = App::logFilter(); // Check the generic filter settings. LogEntry::Level minLevel = LogEntry::Level(domWidgets[0].level->selectedItem().data().toInt()); bool allowDev = domWidgets[0].dev->isActive(); bool alerts = domWidgets[0].alert->isActive(); // Override the individual filters with the generic one. logf.setMinLevel(LogEntry::AllDomains, minLevel); logf.setAllowDev(LogEntry::AllDomains, allowDev); // Update the variables (UI updated automatically). logf.write(cfg.names().subrecord("log.filter")); for(uint i = 0; i < NUM_DOMAINS; ++i) { char const *name = domainText[i].name; cfg.set(String("alert.") + name, int(alerts? LogEntry::Warning : (LogEntry::HighestLogLevel + 1))); } } void toggleStateChanged(ToggleWidget &toggle) { if(toggle.isActive()) { overrideWithGeneric(); fold->open(); } else { fold->close(); overrideWithGeneric(); } } void updateLogFilter() { if(separately->isInactive()) { overrideWithGeneric(); } // Re-read from Config, which has been changed via the widgets. applyFilterFromConfig(); } void applyFilterFromConfig() { App::logFilter().read(App::config().names().subrecord("log.filter")); } }; LogSettingsDialog::LogSettingsDialog(String const &name) : DialogWidget(name, WithHeading), d(new Instance(this)) { heading().setText(tr("Log Filter & Alerts")); // Layout. GridLayout layout(area().contentRule().left(), area().contentRule().top()); layout.setGridSize(4, 0); layout.setColumnFixedWidth(1, *d->columnWidth); layout.setColumnAlignment(0, ui::AlignRight); layout << *d->domWidgets[0].label << *d->domWidgets[0].level << *d->domWidgets[0].dev << *d->domWidgets[0].alert << Const(0); layout.append(*d->separately, 3); layout.append(*d->fold, 4) << Const(0); // Fold's layout is complete. d->fold->content().rule().setSize(d->foldLayout.width(), d->foldLayout.height()); // Dialog content size. area().setContentSize(layout.width(), layout.height()); buttons() << new DialogButtonItem(DialogWidget::Default | DialogWidget::Accept, tr("Close")) << new DialogButtonItem(DialogWidget::Action, tr("Reset to Defaults"), new SignalAction(this, SLOT(resetToDefaults()))); if(d->separately->isActive()) { d->fold->open(); } } void LogSettingsDialog::resetToDefaults() { ClientApp::logSettings().resetToDefaults(); d->applyFilterFromConfig(); } void LogSettingsDialog::updateLogFilter() { d->updateLogFilter(); } doomsday-stable-1.15.7/doomsday/client/src/ui/dialogs/gamesdialog.cpp0000664000175000017500000001164612641367670025107 0ustar jaakkojaakko/** @file gamesdialog.cpp Dialog for viewing and loading available games. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/dialogs/gamesdialog.h" #include "ui/widgets/gameselectionwidget.h" #include "ui/dialogs/networksettingsdialog.h" #include "ui/dialogs/manualconnectiondialog.h" #include "CommandAction" #include using namespace de; DENG_GUI_PIMPL(GamesDialog) { Mode mode; GameSelectionWidget *gameSel; //MenuWidget *list; Instance(Public *i, Mode m) : Base(i) , mode(m) { self.area().add(gameSel = new GameSelectionWidget("games")); gameSel->enableScrolling(false); gameSel->setTitleFont("heading"); gameSel->setTitleColor("accent", "text", ButtonWidget::ReplaceColor); gameSel->rule().setInput(Rule::Height, gameSel->contentRule().height()); } }; GamesDialog::GamesDialog(Mode mode, String const &name) : DialogWidget(name/*, WithHeading*/) , d(new Instance(this, mode)) { connect(d->gameSel, SIGNAL(gameSessionSelected(de::ui::Item const *)), this, SLOT(selectSession(de::ui::Item const *))); GridLayout layout(area().contentRule().left(), area().contentRule().top()); layout.setGridSize(1, 0); if(d->mode == ShowAll) { // Include the filter in the layout. d->gameSel->filter().rule().setInput(Rule::Width, d->gameSel->rule().width()); layout << d->gameSel->filter(); // Disallow changing the filter. d->gameSel->filter().setFilter(GameFilterWidget::AllGames, GameFilterWidget::Permanent); } else { // Disallow changing the filter. d->gameSel->filter().hide(); d->gameSel->filter().setFilter(d->mode == ShowSingleplayerOnly? GameFilterWidget::Singleplayer : GameFilterWidget::Multiplayer, GameFilterWidget::Permanent); switch(d->mode) { case ShowSingleplayerOnly: d->gameSel->subsetFold("available")->open(); d->gameSel->subsetFold("incomplete")->close(); break; case ShowMultiplayerOnly: d->gameSel->subsetFold("multi")->open(); break; default: break; } } layout << *d->gameSel; area().setContentSize(layout.width(), layout.height()); // Buttons appropriate for the mode: buttons() << new DialogButtonItem(Default | Accept, tr("Close")); if(d->mode != ShowSingleplayerOnly) { buttons() << new DialogButtonItem(Action | Id2, tr("Connect Manually..."), new SignalAction(this, SLOT(connectManually()))); // Multiplayer settings. buttons() << new DialogButtonItem(Action | Id1, style().images().image("gear"), new SignalAction(this, SLOT(showSettings()))); } } void GamesDialog::showSettings() { NetworkSettingsDialog *dlg = new NetworkSettingsDialog; dlg->setAnchorAndOpeningDirection(buttonWidget(Id1)->rule(), ui::Up); dlg->setDeleteAfterDismissed(true); dlg->exec(root()); } void GamesDialog::connectManually() { ManualConnectionDialog *dlg = new ManualConnectionDialog; dlg->setAnchorAndOpeningDirection(buttonWidget(Id2)->rule(), ui::Up); dlg->setDeleteAfterDismissed(true); dlg->enableJoinWhenSelected(false); // we'll do it ourselves connect(dlg, SIGNAL(selected(de::ui::Item const *)), this, SLOT(sessionSelectedManually(de::ui::Item const *))); dlg->exec(root()); } void GamesDialog::selectSession(ui::Item const *item) { setAcceptanceAction(d->gameSel->makeAction(*item)); accept(); } void GamesDialog::sessionSelectedManually(ui::Item const *item) { ManualConnectionDialog *dlg = (ManualConnectionDialog *) sender(); setAcceptanceAction(dlg->makeAction(*item)); accept(); } void GamesDialog::preparePanelForOpening() { DialogWidget::preparePanelForOpening(); d->gameSel->rule() .setInput(Rule::Width, OperatorRule::minimum( style().rules().rule("gameselection.max.width"), root().viewWidth() - margins().width())); } doomsday-stable-1.15.7/doomsday/client/src/ui/dialogs/alertdialog.cpp0000664000175000017500000003001212641367670025106 0ustar jaakkojaakko/** @file alertdialog.cpp Dialog for listing recent alerts. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/dialogs/alertdialog.h" #include "ui/dialogs/logsettingsdialog.h" #include "ui/clientwindow.h" #include "clientapp.h" #include #include #include #include #include #include #include #include #include #include using namespace de; static String const VAR_AUTOHIDE = "alert.autoHide"; DENG_GUI_PIMPL(AlertDialog) , DENG2_OBSERVES(ChildWidgetOrganizer, WidgetCreation) , DENG2_OBSERVES(ChildWidgetOrganizer, WidgetUpdate) , DENG2_OBSERVES(Variable, Change) { /// Data model item representing an alert in the list. class AlertItem : public ui::ActionItem { public: AlertItem(String const &msg, Level level) : ui::ActionItem(ShownAsLabel, msg) , _level(level) {} Level level() const { return _level; } private: Level _level; }; /// Rich font styling for alert labels. struct TextStyling : public Font::RichFormat::IStyle { Color richStyleColor(int /*index*/) const { return Color(255, 255, 255, 255); // not used by LabelWidget } void richStyleFormat(int contentStyle, float &sizeFactor, Font::RichFormat::Weight &fontWeight, Font::RichFormat::Style &fontStyle, int &colorIndex) const { ClientApp::windowSystem().style() .richStyleFormat(contentStyle, sizeFactor, fontWeight, fontStyle, colorIndex); if(contentStyle == Font::RichFormat::MajorStyle || contentStyle == Font::RichFormat::MajorMetaStyle) { // Keep the major style normal-weight. fontWeight = Font::RichFormat::Normal; } } }; UniqueWidgetPtr notification; MenuWidget *alerts; bool clearOnDismiss; TextStyling styling; QTimer hideTimer; ///< Automatically hides the notification. ChoiceWidget *autohideTimes; DialogContentStylist stylist; dsize maxCount; typedef FIFO Pending; Pending pending; Instance(Public *i) : Base(i) , clearOnDismiss(false) , maxCount(100) { notification.reset(new ButtonWidget); notification->setSizePolicy(ui::Expand, ui::Expand); notification->setImage(style().images().image("alert")); notification->setOverrideImageSize(style().fonts().font("default").height().value()); notification->setAction(new SignalAction(thisPublic, SLOT(showListOfAlerts()))); // The menu expands with all the alerts, and the dialog's scroll area allows // browsing it up and down. ScrollAreaWidget &area = self.area(); alerts = new MenuWidget; alerts->enableScrolling(false); alerts->setGridSize(1, ui::Expand, 0, ui::Expand); alerts->rule().setLeftTop(area.contentRule().left(), area.contentRule().top()); area.setContentSize(alerts->rule().width(), alerts->rule().height()); area.add(alerts); area.enableIndicatorDraw(true); alerts->organizer().audienceForWidgetCreation() += this; alerts->organizer().audienceForWidgetUpdate() += this; // Set up the automatic hide timer. QObject::connect(&hideTimer, SIGNAL(timeout()), thisPublic, SLOT(hideNotification())); hideTimer.setSingleShot(true); App::config(VAR_AUTOHIDE).audienceForChange() += this; } ~Instance() { App::config(VAR_AUTOHIDE).audienceForChange() -= this; } NotificationAreaWidget ¬ifs() { return ClientWindow::main().notifications(); } int autoHideAfterSeconds() const { return App::config().geti(VAR_AUTOHIDE, 3 * 60); } void variableValueChanged(Variable &, Value const &) { // Update the auto-hide timer. if(!autoHideAfterSeconds()) { // Never autohide. hideTimer.stop(); } else { hideTimer.setInterval(autoHideAfterSeconds() * 1000); if(!hideTimer.isActive()) hideTimer.start(); } } void queueAlert(String const &msg, Level level) { pending.put(new AlertItem(msg, level)); } bool addPendingAlerts() { bool changed = false; while(AlertItem *alert = pending.take()) { add(alert); changed = true; } // Remove excess alerts. while(alerts->items().size() > maxCount) { alerts->items().remove(alerts->items().size() - 1); changed = true; } return changed; } void add(AlertItem *alert) { DENG2_ASSERT_IN_MAIN_THREAD(); // If we already have this, don't re-add. for(ui::Data::Pos i = 0; i < alerts->items().size(); ++i) { if(!alerts->items().at(i).label().compareWithoutCase(alert->label())) return; } alerts->items().insert(0, alert); } void widgetCreatedForItem(GuiWidget &widget, ui::Item const &item) { // Background is provided by the popup. widget.set(Background()); LabelWidget &label = widget.as(); // Each alert has an icon identifying the originating subsystem and the level // of the alert. label.setBehavior(ContentClipping); // allow clip-culling label.setTextStyle(&styling); label.setMaximumTextWidth(style().rules().rule("alerts.width").valuei()); label.setSizePolicy(ui::Expand, ui::Expand); label.setAppearanceAnimation(LabelWidget::AppearGrowVertically, 0.5); label.setAlignment(ui::AlignBottom); label.setImage(style().images().image("alert")); label.setOverrideImageSize(style().fonts().font("default").height().value()); label.setImageAlignment(ui::AlignTop); label.setTextAlignment(ui::AlignRight); label.margins() .setLeft("") .setRight("") .setBottom(""); AlertItem const &alert = item.as(); switch(alert.level()) { case Minor: label.setImageColor(Vector4f(style().colors().colorf("text"), .5f)); break; case Normal: label.setImageColor(style().colors().colorf("text")); break; case Major: label.setTextStyle(0); // use default styling with bold weights label.setImageColor(style().colors().colorf("accent")); break; } } void widgetUpdatedForItem(GuiWidget &widget, ui::Item const &item) { DENG2_UNUSED(widget); DENG2_UNUSED(item); } void showNotification() { // Change color to indicate new alerts. notification->setImageColor(style().colors().colorf("accent")); notifs().showOrHide(*notification, true); // Restart the autohiding timer. if(autoHideAfterSeconds() > 0) { hideTimer.start(autoHideAfterSeconds() * 1000); } } void hideNotification() { notifs().hideChild(*notification); } /** * @returns @c true, if the notification was hidden due to the alerts list being * empty. */ bool hideIfEmpty() { if(alerts->items().isEmpty()) { // No alerts to show. hideNotification(); return true; // was hidden } return false; } void updateAutohideTimeSelection() { int const time = autoHideAfterSeconds(); ui::DataPos pos = autohideTimes->items().findData(time); if(pos != ui::Data::InvalidPos) { autohideTimes->setSelected(pos); } else { autohideTimes->setSelected(autohideTimes->items().findData(0)); } } }; AlertDialog::AlertDialog(String const &/*name*/) : d(new Instance(this)) { // The dialog is connected to the notification icon. setAnchorAndOpeningDirection(d->notification->rule(), ui::Down); buttons() << new DialogButtonItem(DialogWidget::Accept | DialogWidget::Default, tr("Clear All")) << new DialogButtonItem(DialogWidget::Action | DialogWidget::Id1, style().images().image("gear"), new SignalAction(this, SLOT(showLogFilterSettings()))); // Auto-hide setting, positioned next to the Gear button. // These are not part of the dialog proper, but should be styled like regular // dialog contents. d->stylist.setContainer(*this); ButtonWidget const &gearButton = *buttonWidget(DialogWidget::Id1); auto *lab = LabelWidget::newWithText(tr("Hide After:"), this); add(d->autohideTimes = new ChoiceWidget); d->autohideTimes->items() << new ChoiceItem(tr("1 min"), 60) << new ChoiceItem(tr("3 mins"), 3 * 60) << new ChoiceItem(tr("5 mins"), 5 * 60) << new ChoiceItem(tr("10 mins"), 10 * 60) << new ChoiceItem(tr("Never"), 0); d->updateAutohideTimeSelection(); lab->rule() .setInput(Rule::Left, gearButton.rule().right()) .setMidAnchorY(gearButton.rule().midY()); d->autohideTimes->rule() .setInput(Rule::Left, lab->rule().right()) .setInput(Rule::Top, lab->rule().top()); // Tell the dialog about the additional space requirements. setMinimumContentWidth(extraButtonsMenu().rule().width() + buttonsMenu().rule().width() + lab->rule().width() + d->autohideTimes->rule().width()); connect(d->autohideTimes, SIGNAL(selectionChangedByUser(uint)), this, SLOT(autohideTimeChanged())); } void AlertDialog::newAlert(String const &message, Level level) { d->queueAlert(message, level); } void AlertDialog::update() { DialogWidget::update(); if(d->addPendingAlerts()) { d->showNotification(); } } void AlertDialog::showListOfAlerts() { if(isOpen() || d->hideIfEmpty()) return; // Restore the normal color. d->notification->setImageColor(style().colors().colorf("text")); area().scrollToTop(0); open(); } void AlertDialog::showLogFilterSettings() { LogSettingsDialog *st = new LogSettingsDialog; st->setAnchorAndOpeningDirection(buttonWidget(DialogWidget::Id1)->rule(), ui::Left); st->setDeleteAfterDismissed(true); connect(this, SIGNAL(closed()), st, SLOT(close())); st->exec(root()); } void AlertDialog::hideNotification() { d->hideNotification(); } void AlertDialog::autohideTimeChanged() { App::config().set(VAR_AUTOHIDE, d->autohideTimes->selectedItem().data().toInt()); } void AlertDialog::finish(int result) { DialogWidget::finish(result); // The alerts will be cleared if the dialog is accepted. d->clearOnDismiss = (result != 0); } void AlertDialog::panelDismissed() { if(d->clearOnDismiss) { d->clearOnDismiss = false; d->alerts->items().clear(); d->hideIfEmpty(); } DialogWidget::panelDismissed(); } doomsday-stable-1.15.7/doomsday/client/src/ui/dialogs/networksettingsdialog.cpp0000664000175000017500000000642012641367670027257 0ustar jaakkojaakko/** @file networksettingsdialog.cpp Dialog for network settings. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/dialogs/networksettingsdialog.h" #include "ui/widgets/cvarsliderwidget.h" #include "ui/widgets/cvartogglewidget.h" #include "ui/widgets/cvarchoicewidget.h" #include "clientapp.h" #include "de_audio.h" #include #include #include using namespace de; using namespace ui; DENG_GUI_PIMPL(NetworkSettingsDialog) { VariableLineEditWidget *masterApi; GridPopupWidget *devPopup; CVarToggleWidget *devInfo; Instance(Public *i) : Base(i) { ScrollAreaWidget &area = self.area(); area.add(masterApi = new VariableLineEditWidget(App::config("masterServer.apiUrl"))); // Developer options. self.add(devPopup = new GridPopupWidget); devPopup->layout().setGridSize(1, 0); *devPopup << (devInfo = new CVarToggleWidget("net-dev")); devPopup->commit(); } void fetch() { foreach(Widget *w, self.area().childWidgets() + devPopup->content().childWidgets()) { if(ICVarWidget *cv = w->maybeAs()) { cv->updateFromCVar(); } } } }; NetworkSettingsDialog::NetworkSettingsDialog(String const &name) : DialogWidget(name, WithHeading), d(new Instance(this)) { heading().setText(tr("Network Settings")); d->devInfo->setText(tr("Developer Info")); LabelWidget *masterApiLabel = LabelWidget::newWithText(tr("Master API URL:"), &area()); // Layout. GridLayout layout(area().contentRule().left(), area().contentRule().top()); layout.setGridSize(2, 0); layout.setColumnAlignment(0, ui::AlignRight); layout << *masterApiLabel << *d->masterApi; area().setContentSize(layout.width(), layout.height()); buttons() << new DialogButtonItem(Default | Accept, tr("Close")) << new DialogButtonItem(Action, tr("Reset to Defaults"), new SignalAction(this, SLOT(resetToDefaults()))) << new DialogButtonItem(Action | Id1, style().images().image("gauge"), new SignalAction(this, SLOT(showDeveloperPopup()))); d->devPopup->setAnchorAndOpeningDirection(buttonWidget(Id1)->rule(), ui::Up); d->fetch(); } void NetworkSettingsDialog::resetToDefaults() { ClientApp::networkSettings().resetToDefaults(); d->fetch(); } void NetworkSettingsDialog::showDeveloperPopup() { d->devPopup->open(); } doomsday-stable-1.15.7/doomsday/client/src/ui/dialogs/inputsettingsdialog.cpp0000664000175000017500000002052212641367670026724 0ustar jaakkojaakko/** @file inputsettingsdialog.cpp Dialog for input settings. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/dialogs/inputsettingsdialog.h" #include "ui/widgets/cvarsliderwidget.h" #include "ui/widgets/cvartogglewidget.h" #include "ui/widgets/keygrabberwidget.h" #include "ui/inputdeviceaxiscontrol.h" #include "clientapp.h" #include "api_console.h" #include #include #include using namespace de; using namespace ui; DENG_GUI_PIMPL(InputSettingsDialog) { VariableToggleWidget *syncMouse; CVarToggleWidget *syncInput; CVarSliderWidget *mouseSensiX; CVarSliderWidget *mouseSensiY; ToggleWidget *mouseDisableX; ToggleWidget *mouseDisableY; ToggleWidget *mouseInvertX; ToggleWidget *mouseInvertY; ToggleWidget *mouseFilterX; ToggleWidget *mouseFilterY; CVarToggleWidget *joyEnable; GridPopupWidget *devPopup; Instance(Public *i) : Base(i) { ScrollAreaWidget &area = self.area(); area.add(syncMouse = new VariableToggleWidget(App::config("input.mouse.syncSensitivity"))); area.add(syncInput = new CVarToggleWidget("input-sharp")); area.add(mouseSensiX = new CVarSliderWidget("input-mouse-x-scale")); area.add(mouseSensiY = new CVarSliderWidget("input-mouse-y-scale")); area.add(mouseDisableX = new ToggleWidget); area.add(mouseDisableY = new ToggleWidget); area.add(mouseInvertX = new ToggleWidget); area.add(mouseInvertY = new ToggleWidget); area.add(mouseFilterX = new ToggleWidget); area.add(mouseFilterY = new ToggleWidget); area.add(joyEnable = new CVarToggleWidget("input-joy")); // Developer options. self.add(devPopup = new GridPopupWidget); *devPopup << LabelWidget::newWithText(tr("Key Grabber:")) << new KeyGrabberWidget; devPopup->commit(); } void fetch() { syncInput->updateFromCVar(); mouseSensiX->updateFromCVar(); mouseSensiY->updateFromCVar(); joyEnable->updateFromCVar(); mouseDisableX->setActive(Con_GetInteger("input-mouse-x-flags") & IDA_DISABLED); mouseDisableY->setActive(Con_GetInteger("input-mouse-y-flags") & IDA_DISABLED); mouseInvertX->setActive(Con_GetInteger("input-mouse-x-flags") & IDA_INVERT); mouseInvertY->setActive(Con_GetInteger("input-mouse-y-flags") & IDA_INVERT); mouseFilterX->setInactive(Con_GetInteger("input-mouse-x-flags") & IDA_RAW); mouseFilterY->setInactive(Con_GetInteger("input-mouse-y-flags") & IDA_RAW); enableOrDisable(); } void enableOrDisable() { mouseSensiX->disable(mouseDisableX->isActive()); mouseSensiY->disable(mouseDisableY->isActive()); mouseInvertX->disable(mouseDisableX->isActive()); mouseInvertY->disable(mouseDisableY->isActive()); mouseFilterX->disable(mouseDisableX->isActive()); mouseFilterY->disable(mouseDisableY->isActive()); } void updateMouseFlags() { Con_SetInteger("input-mouse-x-flags", (mouseDisableX->isActive()? IDA_DISABLED : 0) | (mouseInvertX->isActive()? IDA_INVERT : 0) | (mouseFilterX->isInactive()? IDA_RAW : 0)); Con_SetInteger("input-mouse-y-flags", (mouseDisableY->isActive()? IDA_DISABLED : 0) | (mouseInvertY->isActive()? IDA_INVERT : 0) | (mouseFilterY->isInactive()? IDA_RAW : 0)); enableOrDisable(); } }; InputSettingsDialog::InputSettingsDialog(String const &name) : DialogWidget(name, WithHeading), d(new Instance(this)) { heading().setText(tr("Input Settings")); d->syncInput->setText(tr("Vanilla 35Hz Input Rate")); d->syncMouse->setText(tr("Uniform Mouse Axis Sensitivity")); LabelWidget *mouseXLabel = LabelWidget::newWithText(_E(D) + tr("Mouse: Horizontal"), &area()); LabelWidget *mouseYLabel = LabelWidget::newWithText(_E(D) + tr("Mouse: Vertical"), &area()); mouseXLabel->setFont("separator.label"); mouseYLabel->setFont("separator.label"); mouseXLabel->margins().setTop(style().rules().rule("gap")); mouseYLabel->margins().setTop(style().rules().rule("gap")); // The sensitivity cvars are unlimited. d->mouseSensiX->setRange(Rangef(.00005f, .0075f)); d->mouseSensiX->setDisplayFactor(1000); d->mouseSensiY->setRange(Rangef(.00005f, .0075f)); d->mouseSensiY->setDisplayFactor(1000); connect(d->mouseSensiX, SIGNAL(valueChangedByUser(double)), this, SLOT(mouseSensitivityChanged(double))); connect(d->mouseSensiY, SIGNAL(valueChangedByUser(double)), this, SLOT(mouseSensitivityChanged(double))); d->mouseInvertX->setText(tr("Invert Axis")); d->mouseDisableX->setText(tr("Disable Axis")); d->mouseFilterX->setText(tr("Filter Axis")); d->mouseInvertY->setText(tr("Invert Axis")); d->mouseDisableY->setText(tr("Disable Axis")); d->mouseFilterY->setText(tr("Filter Axis")); connect(d->mouseInvertX, SIGNAL(stateChangedByUser(ToggleWidget::ToggleState)), this, SLOT(mouseTogglesChanged())); connect(d->mouseInvertY, SIGNAL(stateChangedByUser(ToggleWidget::ToggleState)), this, SLOT(mouseTogglesChanged())); connect(d->mouseDisableX, SIGNAL(stateChangedByUser(ToggleWidget::ToggleState)), this, SLOT(mouseTogglesChanged())); connect(d->mouseDisableY, SIGNAL(stateChangedByUser(ToggleWidget::ToggleState)), this, SLOT(mouseTogglesChanged())); connect(d->mouseFilterX, SIGNAL(stateChangedByUser(ToggleWidget::ToggleState)), this, SLOT(mouseTogglesChanged())); connect(d->mouseFilterY, SIGNAL(stateChangedByUser(ToggleWidget::ToggleState)), this, SLOT(mouseTogglesChanged())); d->joyEnable->setText(tr("Joystick Enabled")); // Layout. GridLayout layout(area().contentRule().left(), area().contentRule().top()); layout.setGridSize(2, 0); //layout.setColumnAlignment(0, ui::AlignRight); layout.append(*d->syncInput, 2) .append(*d->joyEnable, 2) .append(*d->syncMouse, 2); layout << *mouseXLabel << *mouseYLabel << *d->mouseSensiX << *d->mouseSensiY << *d->mouseInvertX << *d->mouseInvertY << *d->mouseFilterX << *d->mouseFilterY << *d->mouseDisableX << *d->mouseDisableY; area().setContentSize(layout.width(), layout.height()); buttons() << new DialogButtonItem(Default | Accept, tr("Close")) << new DialogButtonItem(Action, tr("Reset to Defaults"), new SignalAction(this, SLOT(resetToDefaults()))) << new DialogButtonItem(Action | Id1, style().images().image("gauge"), new SignalAction(this, SLOT(showDeveloperPopup()))); d->devPopup->setAnchorAndOpeningDirection(buttonWidget(Id1)->rule(), ui::Up); d->fetch(); } void InputSettingsDialog::resetToDefaults() { ClientApp::inputSystem().settings().resetToDefaults(); d->fetch(); } void InputSettingsDialog::mouseTogglesChanged() { d->updateMouseFlags(); } void InputSettingsDialog::mouseSensitivityChanged(double value) { // Keep mouse axes synced? if(d->syncMouse->isActive()) { if(sender() == d->mouseSensiX) { d->mouseSensiY->setValue(value); d->mouseSensiY->setCVarValueFromWidget(); } else { d->mouseSensiX->setValue(value); d->mouseSensiX->setCVarValueFromWidget(); } } } void InputSettingsDialog::showDeveloperPopup() { d->devPopup->open(); } doomsday-stable-1.15.7/doomsday/client/src/ui/dialogs/aboutdialog.cpp0000664000175000017500000001033112641367670025113 0ustar jaakkojaakko/** @file aboutdialog.cpp Information about the Doomsday Client. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/dialogs/aboutdialog.h" #include "gl/sys_opengl.h" #include "audio/audiodriver.h" #include "clientapp.h" #include "versioninfo.h" #include "dd_def.h" #include #include #include #include #include #include using namespace de; DENG2_PIMPL(AboutDialog) { DocumentPopupWidget *glPopup; DocumentPopupWidget *audioPopup; Instance(Public *i) : Base(i) { // Popup with GL info. glPopup = new DocumentPopupWidget; glPopup->document().setText(Sys_GLDescription()); self.add(glPopup); // Popup with audio info. audioPopup = new DocumentPopupWidget; audioPopup->document().setText(AudioDriver_InterfaceDescription()); self.add(audioPopup); } }; AboutDialog::AboutDialog() : DialogWidget("about"), d(new Instance(this)) { /* * Construct the widgets. */ LabelWidget *logo = new LabelWidget; logo->setImage(style().images().image("logo.px256")); logo->setSizePolicy(ui::Fixed, ui::Expand); VersionInfo version; de::Version ver2; // Set up the contents of the widget. LabelWidget *title = LabelWidget::newWithText(String("%1 %2.%3") .arg(DOOMSDAY_NICENAME) .arg(version.major) .arg(version.minor)); title->margins().set(""); title->setFont("title"); title->setTextColor("accent"); title->setSizePolicy(ui::Fixed, ui::Expand); LabelWidget *info = new LabelWidget; String txt = String(_E(b) "%4 %5 #%6" _E(.) "\n%7\n\n%1 (%2-%8)%3") .arg(ver2.operatingSystem() == "windows"? tr("Windows") : ver2.operatingSystem() == "macx"? tr("Mac OS X") : tr("Unix")) .arg(ver2.cpuBits()) .arg(ver2.isDebugBuild()? tr(" Debug") : "") .arg(DOOMSDAY_RELEASE_TYPE) .arg(version.base()) .arg(ver2.build) .arg(Time::fromText(__DATE__ " " __TIME__, Time::CompilerDateTime) .asDateTime().toString(Qt::SystemLocaleShortDate)) .arg(tr("bit")); info->setText(txt); info->setSizePolicy(ui::Fixed, ui::Expand); area().add(logo); area().add(title); area().add(info); // Layout. RuleRectangle const &cont = area().contentRule(); SequentialLayout layout(cont.left(), cont.top()); layout.setOverrideWidth(style().rules().rule("dialog.about.width")); layout << *logo << *title << *info; // Total size of the dialog's content. area().setContentSize(layout.width(), layout.height()); buttons() << new DialogButtonItem(DialogWidget::Accept | DialogWidget::Default, tr("Close")) << new DialogButtonItem(DialogWidget::Action, tr("GL"), new SignalAction(d->glPopup, SLOT(open()))) << new DialogButtonItem(DialogWidget::Action, tr("Audio"), new SignalAction(d->audioPopup, SLOT(open()))) << new DialogButtonItem(DialogWidget::Action, tr("Homepage..."), new SignalAction(&ClientApp::app(), SLOT(openHomepageInBrowser()))); // The popups are anchored to their button. d->glPopup->setAnchorAndOpeningDirection(buttonWidget(tr("GL")).rule(), ui::Up); d->audioPopup->setAnchorAndOpeningDirection(buttonWidget(tr("Audio")).rule(), ui::Up); } doomsday-stable-1.15.7/doomsday/client/src/ui/dialogs/manualconnectiondialog.cpp0000664000175000017500000001520012641367670027336 0ustar jaakkojaakko/** @file manualconnectiondialog.cpp Dialog for connecting to a server. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/dialogs/manualconnectiondialog.h" #include "ui/widgets/mpsessionmenuwidget.h" #include "clientapp.h" #include #include #include using namespace de; DENG2_PIMPL(ManualConnectionDialog) , DENG2_OBSERVES(ServerLink, DiscoveryUpdate) { String usedAddress; FoldPanelWidget *fold; MPSessionMenuWidget *games; ProgressWidget *progress; bool querying; bool joinWhenEnterPressed; bool autoJoin; Instance(Public *i) : Base(i) , querying(false) , joinWhenEnterPressed(false) , autoJoin(true) { ClientApp::serverLink().audienceForDiscoveryUpdate += this; } ~Instance() { ClientApp::serverLink().audienceForDiscoveryUpdate -= this; } void linkDiscoveryUpdate(ServerLink const &link) { if(querying) { // Time to show what we found. querying = false; progress->setRotationSpeed(0); self.editor().enable(); self.validate(); if(link.foundServerCount(ServerLink::Direct) > 0) { progress->hide(); fold->open(); if(link.foundServerCount(ServerLink::Direct) == 1) { joinWhenEnterPressed = true; } } else { fold->close(0); progress->setRotationSpeed(0); progress->setText(_E(l) + tr("No response")); progress->setOpacity(0, 4, 2); } } } ButtonWidget &connectButton() { return self.buttonWidget(tr("Connect")); } }; ManualConnectionDialog::ManualConnectionDialog(String const &name) : InputDialog(name), d(new Instance(this)) { add(d->progress = new ProgressWidget); d->progress->useMiniStyle("altaccent"); d->progress->setWidthPolicy(ui::Expand); d->progress->setTextAlignment(ui::AlignLeft); d->progress->hide(); // The found games are shown inside a fold panel. d->fold = new FoldPanelWidget; d->games = new MPSessionMenuWidget(MPSessionMenuWidget::DirectDiscoveryOnly); connect(d->games, SIGNAL(sessionSelected(de::ui::Item const *)), this, SIGNAL(selected(de::ui::Item const *))); connect(d->games, SIGNAL(sessionSelected(de::ui::Item const *)), this, SLOT (serverSelected(de::ui::Item const *))); d->games->rule().setInput(Rule::Width, rule().width() - margins().width()); d->fold->setContent(d->games); area().add(d->fold); title().setText(tr("Connect to Server")); message().setText(tr("Enter the IP address or domain name of the multiplayer server you want to connect to. " "Optionally, you may include a TCP port number, for example " _E(b) "10.0.1.1:13209" _E(.) ".")); buttons().clear() << new DialogButtonItem(Default, tr("Connect"), new SignalAction(this, SLOT(queryOrConnect()))) << new DialogButtonItem(Reject); d->connectButton().disable(); d->progress->rule() .setInput(Rule::Top, buttonsMenu().rule().top()) .setInput(Rule::Right, buttonsMenu().rule().left()) .setInput(Rule::Height, buttonsMenu().rule().height() - margins().bottom()); disconnect(&editor(), SIGNAL(enterPressed(QString)), this, SLOT(accept())); connect(&editor(), SIGNAL(enterPressed(QString)), this, SLOT(queryOrConnect())); connect(&editor(), SIGNAL(editorContentChanged()), this, SLOT(validate())); connect(&editor(), SIGNAL(editorContentChanged()), this, SLOT(contentChanged())); updateLayout(); } void ManualConnectionDialog::enableJoinWhenSelected(bool joinWhenSelected) { d->autoJoin = joinWhenSelected; } Action *ManualConnectionDialog::makeAction(const ui::Item &item) { return d->games->makeAction(item); } void ManualConnectionDialog::operator >> (PersistentState &toState) const { toState.names().set(name() + ".address", d->usedAddress); } void ManualConnectionDialog::operator << (PersistentState const &fromState) { d->usedAddress = fromState.names()[name() + ".address"]; editor().setText(d->usedAddress); validate(); } void ManualConnectionDialog::queryOrConnect() { if(d->connectButton().isDisabled()) { // Should not try now. return; } if(!d->querying) { // Automatically connect if there is a single choice. if(d->joinWhenEnterPressed) { emit selected(&d->games->items().at(0)); serverSelected(&d->games->items().at(0)); return; } d->joinWhenEnterPressed = false; d->querying = true; d->progress->setText(_E(l) + tr("Looking for host...")); d->progress->setRotationSpeed(40); d->progress->show(); d->progress->setOpacity(1); editor().disable(); validate(); ClientApp::serverLink().discover(editor().text()); } } void ManualConnectionDialog::contentChanged() { d->joinWhenEnterPressed = false; } void ManualConnectionDialog::validate() { bool valid = true; if(d->querying) { valid = false; } if(editor().text().isEmpty() || editor().text().contains(';') || editor().text().endsWith(":") || editor().text().startsWith(":")) { valid = false; } d->connectButton().enable(valid); } void ManualConnectionDialog::serverSelected(ui::Item const *item) { if(d->autoJoin) { setAcceptanceAction(makeAction(*item)); } accept(); } void ManualConnectionDialog::finish(int result) { if(result) { // The dialog was accepted. d->usedAddress = editor().text(); } InputDialog::finish(result); } doomsday-stable-1.15.7/doomsday/client/src/ui/dialogs/coloradjustmentdialog.cpp0000664000175000017500000000642712641367670027231 0ustar jaakkojaakko/** @file coloradjustmentdialog.cpp Dialog for gamma, etc. adjustments. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/dialogs/coloradjustmentdialog.h" #include "ui/widgets/cvarsliderwidget.h" #include #include #include "api_console.h" using namespace de; using namespace ui; DENG_GUI_PIMPL(ColorAdjustmentDialog) { CVarSliderWidget *gamma; CVarSliderWidget *contrast; CVarSliderWidget *brightness; Instance(Public *i) : Base(i) { ScrollAreaWidget &area = self.area(); LabelWidget *gammaLabel = LabelWidget::newWithText(tr("Gamma:"), &area); LabelWidget *contrastLabel = LabelWidget::newWithText(tr("Contrast:"), &area); LabelWidget *brightnessLabel = LabelWidget::newWithText(tr("Brightness:"), &area); area.add(gamma = new CVarSliderWidget("vid-gamma")); area.add(contrast = new CVarSliderWidget("vid-contrast")); area.add(brightness = new CVarSliderWidget("vid-bright")); Rule const &sliderWidth = style().rules().rule("coloradjustment.slider"); gamma->rule().setInput(Rule::Width, sliderWidth); contrast->rule().setInput(Rule::Width, sliderWidth); brightness->rule().setInput(Rule::Width, sliderWidth); GridLayout layout(area.contentRule().left(), area.contentRule().top()); layout.setGridSize(2, 3); layout.setColumnAlignment(0, ui::AlignRight); layout << *gammaLabel << *gamma << *contrastLabel << *contrast << *brightnessLabel << *brightness; area.setContentSize(layout.width(), layout.height()); } void fetch() { gamma->updateFromCVar(); contrast->updateFromCVar(); brightness->updateFromCVar(); } }; ColorAdjustmentDialog::ColorAdjustmentDialog(String const &name) : DialogWidget(name, WithHeading), d(new Instance(this)) { heading().setText(tr("Color Adjustments")); buttons() << new DialogButtonItem(DialogWidget::Default | DialogWidget::Accept, tr("Close")) << new DialogButtonItem(DialogWidget::Action, tr("Reset to Defaults"), new SignalAction(this, SLOT(resetToDefaults()))); } void ColorAdjustmentDialog::prepare() { DialogWidget::prepare(); d->fetch(); } void ColorAdjustmentDialog::resetToDefaults() { Con_SetFloat("vid-gamma", 1); Con_SetFloat("vid-contrast", 1); Con_SetFloat("vid-bright", 0); d->gamma->updateFromCVar(); d->contrast->updateFromCVar(); d->brightness->updateFromCVar(); } doomsday-stable-1.15.7/doomsday/client/src/ui/dialogs/audiosettingsdialog.cpp0000664000175000017500000001405312641367670026670 0ustar jaakkojaakko/** @file audiosettingsdialog.cpp Dialog for audio settings. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/dialogs/audiosettingsdialog.h" #include "ui/widgets/cvarsliderwidget.h" #include "ui/widgets/cvartogglewidget.h" #include "ui/widgets/cvarchoicewidget.h" #include "ui/widgets/cvarnativepathwidget.h" #include "clientapp.h" #include "de_audio.h" #include #include using namespace de; using namespace de::ui; DENG_GUI_PIMPL(AudioSettingsDialog) { CVarSliderWidget *sfxVolume; CVarSliderWidget *musicVolume; CVarSliderWidget *reverbVolume; CVarToggleWidget *sound3D; CVarToggleWidget *overlapStop; CVarToggleWidget *sound16bit; CVarChoiceWidget *sampleRate; CVarChoiceWidget *musicSource; CVarNativePathWidget *musicSoundfont; CVarToggleWidget *soundInfo; GridPopupWidget *devPopup; Instance(Public *i) : Base(i) { ScrollAreaWidget &area = self.area(); area.add(sfxVolume = new CVarSliderWidget ("sound-volume")); area.add(musicVolume = new CVarSliderWidget ("music-volume")); area.add(reverbVolume = new CVarSliderWidget ("sound-reverb-volume")); area.add(sound3D = new CVarToggleWidget ("sound-3d")); area.add(overlapStop = new CVarToggleWidget ("sound-overlap-stop")); area.add(sound16bit = new CVarToggleWidget ("sound-16bit")); area.add(sampleRate = new CVarChoiceWidget ("sound-rate")); area.add(musicSource = new CVarChoiceWidget ("music-source")); area.add(musicSoundfont = new CVarNativePathWidget("music-soundfont")); musicSoundfont->setBlankText(tr("System default")); musicSoundfont->setFilters(StringList() << "SF2 soundfonts (*.sf2)" << "DLS soundfonts (*.dls)" << "All files (*)"); // Display volumes on a 0...100 scale. sfxVolume ->setDisplayFactor(100.0 / 255.0); musicVolume->setDisplayFactor(100.0 / 255.0); sfxVolume ->setStep(1.0 / sfxVolume->displayFactor()); musicVolume->setStep(1.0 / musicVolume->displayFactor()); // Developer options. self.add(devPopup = new GridPopupWidget); soundInfo = new CVarToggleWidget("sound-info", tr("Sound Channel Status")); *devPopup << soundInfo; devPopup->commit(); } void fetch() { foreach(Widget *w, self.area().childWidgets() + devPopup->content().childWidgets()) { if(ICVarWidget *cv = w->maybeAs()) { cv->updateFromCVar(); } } } }; AudioSettingsDialog::AudioSettingsDialog(String const &name) : DialogWidget(name, WithHeading), d(new Instance(this)) { heading().setText(tr("Audio Settings")); auto *sfxVolLabel = LabelWidget::newWithText(tr("SFX Volume:"), &area()); auto *musicVolLabel = LabelWidget::newWithText(tr("Music Volume:"), &area()); auto *rvbVolLabel = LabelWidget::newWithText(tr("Reverb Strength:"), &area()); d->sound3D->setText(tr("3D Effects & Reverb")); d->overlapStop->setText(tr("One Sound per Emitter")); d->sound16bit->setText(tr("16-bit Resampling")); auto *rateLabel = LabelWidget::newWithText(tr("Resampling:"), &area()); d->sampleRate->items() << new ChoiceItem(tr("1x @ 11025 Hz"), 11025) << new ChoiceItem(tr("2x @ 22050 Hz"), 22050) << new ChoiceItem(tr("4x @ 44100 Hz"), 44100); auto *musSrcLabel = LabelWidget::newWithText(tr("Preferred Music:"), &area()); d->musicSource->items() << new ChoiceItem(tr("MUS lumps"), MUSP_MUS) << new ChoiceItem(tr("External files"), MUSP_EXT) << new ChoiceItem(tr("CD"), MUSP_CD); auto *sfLabel = LabelWidget::newWithText(tr("MIDI Sound Font:"), &area()); // Layout. GridLayout layout(area().contentRule().left(), area().contentRule().top()); layout.setGridSize(2, 0); layout.setColumnAlignment(0, ui::AlignRight); layout << *sfxVolLabel << *d->sfxVolume << *musicVolLabel << *d->musicVolume << *rvbVolLabel << *d->reverbVolume << Const(0) << *d->sound3D << Const(0) << *d->overlapStop << *rateLabel << *d->sampleRate << Const(0) << *d->sound16bit << *musSrcLabel << *d->musicSource << *sfLabel << *d->musicSoundfont; area().setContentSize(layout.width(), layout.height()); buttons() << new DialogButtonItem(DialogWidget::Default | DialogWidget::Accept, tr("Close")) << new DialogButtonItem(DialogWidget::Action, tr("Reset to Defaults"), new SignalAction(this, SLOT(resetToDefaults()))) << new DialogButtonItem(DialogWidget::Action | Id1, style().images().image("gauge"), new SignalAction(this, SLOT(showDeveloperPopup()))); d->devPopup->setAnchorAndOpeningDirection(buttonWidget(Id1)->rule(), ui::Up); d->fetch(); } void AudioSettingsDialog::resetToDefaults() { ClientApp::audioSettings().resetToDefaults(); d->fetch(); } void AudioSettingsDialog::showDeveloperPopup() { d->devPopup->open(); } doomsday-stable-1.15.7/doomsday/client/src/ui/dialogs/videosettingsdialog.cpp0000664000175000017500000002754012641367670026702 0ustar jaakkojaakko/** @file videosettingsdialog.cpp Dialog for video settings. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/dialogs/videosettingsdialog.h" #include "ui/widgets/taskbarwidget.h" #include "ui/widgets/cvarchoicewidget.h" #include "ui/clientwindow.h" #include "CommandAction" #include "clientapp.h" #include "dd_main.h" #include #include #include #include #include #include #include #include #include using namespace de; using namespace ui; #ifndef MACOSX # define USE_COLOR_DEPTH_CHOICE #endif DENG2_PIMPL(VideoSettingsDialog), DENG2_OBSERVES(PersistentCanvasWindow, AttributeChange) { ClientWindow &win; VariableToggleWidget *showFps; ToggleWidget *fullscreen; ToggleWidget *maximized; ToggleWidget *centered; VariableToggleWidget *fsaa; VariableToggleWidget *vsync; ChoiceWidget *modes; ButtonWidget *windowButton; #ifdef USE_COLOR_DEPTH_CHOICE ChoiceWidget *depths; #endif ListData stretchChoices; CVarChoiceWidget *finaleAspect = nullptr; CVarChoiceWidget *hudAspect = nullptr; CVarChoiceWidget *inludeAspect = nullptr; CVarChoiceWidget *menuAspect = nullptr; Instance(Public *i) : Base(i) , win(ClientWindow::main()) { ScrollAreaWidget &area = self.area(); area.add(showFps = new VariableToggleWidget(App::config("window.main.showFps"))); area.add(fullscreen = new ToggleWidget); area.add(maximized = new ToggleWidget); area.add(centered = new ToggleWidget); area.add(fsaa = new VariableToggleWidget(App::config("window.main.fsaa"))); area.add(vsync = new VariableToggleWidget(App::config("window.main.vsync"))); area.add(modes = new ChoiceWidget); area.add(windowButton = new ButtonWidget); #ifdef USE_COLOR_DEPTH_CHOICE area.add(depths = new ChoiceWidget); #endif win.audienceForAttributeChange() += this; if(App_GameLoaded()) { stretchChoices << new ChoiceItem(tr("Smart"), SCALEMODE_SMART_STRETCH) << new ChoiceItem(tr("Original 1:1"), SCALEMODE_NO_STRETCH) << new ChoiceItem(tr("Stretched"), SCALEMODE_STRETCH); area.add(finaleAspect = new CVarChoiceWidget("rend-finale-stretch")); area.add(hudAspect = new CVarChoiceWidget("rend-hud-stretch")); area.add(inludeAspect = new CVarChoiceWidget("inlude-stretch")); area.add(menuAspect = new CVarChoiceWidget("menu-stretch")); finaleAspect->setItems(stretchChoices); hudAspect->setItems(stretchChoices); inludeAspect->setItems(stretchChoices); menuAspect->setItems(stretchChoices); } } ~Instance() { // The common stretchChoices is being deleted now, before the widget tree. if(finaleAspect) { finaleAspect->useDefaultItems(); hudAspect->useDefaultItems(); inludeAspect->useDefaultItems(); menuAspect->useDefaultItems(); } win.audienceForAttributeChange() -= this; } /** * Updates the widgets with the actual current state. */ void fetch() { fullscreen->setActive(win.isFullScreen()); maximized->setActive(win.isMaximized()); centered->setActive(win.isCentered()); windowButton->enable(!win.isFullScreen() && !win.isMaximized()); // Select the current resolution/size in the mode list. Canvas::Size current = win.fullscreenSize(); // Update selected display mode. ui::Data::Pos closest = ui::Data::InvalidPos; int delta = 0; for(ui::Data::Pos i = 0; i < modes->items().size(); ++i) { QPoint const res = modes->items().at(i).data().toPoint(); int dx = res.x() - current.x; int dy = res.y() - current.y; int d = dx*dx + dy*dy; if(closest == ui::Data::InvalidPos || d < delta) { closest = i; delta = d; } } modes->setSelected(closest); #ifdef USE_COLOR_DEPTH_CHOICE // Select the current color depth in the depth list. depths->setSelected(depths->items().findData(win.colorDepthBits())); #endif foreach(Widget *child, self.area().childWidgets()) { if(ICVarWidget *cw = child->maybeAs()) cw->updateFromCVar(); } } void windowAttributesChanged(PersistentCanvasWindow &) { fetch(); } }; VideoSettingsDialog::VideoSettingsDialog(String const &name) : DialogWidget(name, WithHeading), d(new Instance(this)) { bool const gotDisplayMode = DisplayMode_Count() > 0; heading().setText(tr("Video Settings")); // Toggles for video/window options. d->fullscreen->setText(tr("Fullscreen")); d->fullscreen->setAction(new CommandAction("togglefullscreen")); d->maximized->setText(tr("Maximized")); d->maximized->setAction(new CommandAction("togglemaximized")); d->centered->setText(tr("Center Window")); d->centered->setAction(new CommandAction("togglecentered")); d->showFps->setText(tr("Show FPS")); d->fsaa->setText(tr("Antialias")); d->vsync->setText(tr("VSync")); #ifdef USE_COLOR_DEPTH_CHOICE LabelWidget *colorLabel = 0; #endif if(gotDisplayMode) { // Choice of display modes + 16/32-bit color depth. d->modes->setOpeningDirection(ui::Up); if(DisplayMode_Count() > 10) { d->modes->popup().menu().setGridSize(2, ui::Expand, 0, ui::Expand); } for(int i = 0; i < DisplayMode_Count(); ++i) { DisplayMode const *m = DisplayMode_ByIndex(i); QPoint const res(m->width, m->height); if(d->modes->items().findData(res) != ui::Data::InvalidPos) { // Got this already. continue; } String desc = String("%1 x %2 (%3:%4)") .arg(m->width).arg(m->height) .arg(m->ratioX).arg(m->ratioY); d->modes->items() << new ChoiceItem(desc, res); } #ifdef USE_COLOR_DEPTH_CHOICE colorLabel = new LabelWidget; colorLabel->setText(tr("Colors:")); area().add(colorLabel); d->depths->items() << new ChoiceItem(tr("32-bit"), 32) << new ChoiceItem(tr("24-bit"), 24) << new ChoiceItem(tr("16-bit"), 16); #endif } buttons() << new DialogButtonItem(DialogWidget::Accept | DialogWidget::Default, tr("Close")) << new DialogButtonItem(DialogWidget::Action, tr("Reset to Defaults"), new SignalAction(this, SLOT(resetToDefaults()))); d->windowButton->setImage(style().images().image("window.icon")); d->windowButton->setOverrideImageSize(style().fonts().font("default").height().valuei()); d->windowButton->setAction(new SignalAction(this, SLOT(showWindowMenu()))); // Layout all widgets. Rule const &gap = style().rules().rule("dialog.gap"); GridLayout layout(area().contentRule().left(), area().contentRule().top(), GridLayout::RowFirst); layout.setGridSize(2, 3); layout.setColumnPadding(style().rules().rule("unit")); layout << *d->showFps << *d->fsaa << *d->vsync << *d->fullscreen << *d->maximized << *d->centered; GridLayout modeLayout(d->vsync->rule().left(), d->vsync->rule().bottom() + gap); modeLayout.setGridSize(2, 0); modeLayout.setColumnAlignment(0, ui::AlignRight); if(gotDisplayMode) { modeLayout << *LabelWidget::newWithText(tr("Resolution:"), &area()); modeLayout.append(*d->modes, d->modes->rule().width() + d->windowButton->rule().width()); d->windowButton->rule() .setInput(Rule::Top, d->modes->rule().top()) .setInput(Rule::Left, d->modes->rule().right()); #ifdef USE_COLOR_DEPTH_CHOICE modeLayout << *colorLabel << *d->depths; #endif auto *adjustButton = new ButtonWidget; adjustButton->setText(tr("Color Adjustments...")); adjustButton->setAction(new SignalAction(this, SLOT(showColorAdjustments()))); area().add(adjustButton); modeLayout << Const(0) << *adjustButton; } if(d->inludeAspect) { // Aspect ratio options. auto *aspectLabel = LabelWidget::newWithText(_E(D) + tr("Aspect Ratios"), &area()); aspectLabel->setFont("separator.label"); aspectLabel->margins().setTop("gap"); modeLayout.setCellAlignment(Vector2i(0, modeLayout.gridSize().y), ui::AlignLeft); modeLayout.append(*aspectLabel, 2) << *LabelWidget::newWithText(tr("Player Weapons:"), &area()) << *d->hudAspect << *LabelWidget::newWithText(tr("Intermissions:"), &area()) << *d->inludeAspect << *LabelWidget::newWithText(tr("Finales:"), &area()) << *d->finaleAspect << *LabelWidget::newWithText(tr("Menus:"), &area()) << *d->menuAspect; } area().setContentSize(OperatorRule::maximum(layout.width(), modeLayout.width()), layout.height() + gap + modeLayout.height()); d->fetch(); connect(d->modes, SIGNAL(selectionChangedByUser(uint)), this, SLOT(changeMode(uint))); #ifdef USE_COLOR_DEPTH_CHOICE connect(d->depths, SIGNAL(selectionChangedByUser(uint)), this, SLOT(changeColorDepth(uint))); #endif } void VideoSettingsDialog::resetToDefaults() { ClientApp::windowSystem().settings().resetToDefaults(); d->fetch(); } void VideoSettingsDialog::changeMode(uint selected) { QPoint const res = d->modes->items().at(selected).data().toPoint(); int attribs[] = { ClientWindow::FullscreenWidth, int(res.x()), ClientWindow::FullscreenHeight, int(res.y()), ClientWindow::End }; d->win.changeAttributes(attribs); } void VideoSettingsDialog::changeColorDepth(uint selected) { #ifdef USE_COLOR_DEPTH_CHOICE Con_Executef(CMDS_DDAY, true, "setcolordepth %i", d->depths->items().at(selected).data().toInt()); #else DENG2_UNUSED(selected); #endif } void VideoSettingsDialog::showColorAdjustments() { d->win.showColorAdjustments(); d->win.taskBar().closeConfigMenu(); } void VideoSettingsDialog::showWindowMenu() { PopupMenuWidget *menu = new PopupMenuWidget; menu->setDeleteAfterDismissed(true); add(menu); menu->setAnchorAndOpeningDirection(d->windowButton->rule(), ui::Up); menu->items() << new ActionItem(tr("Apply to Window"), new SignalAction(this, SLOT(applyModeToWindow()))); menu->open(); } void VideoSettingsDialog::applyModeToWindow() { QPoint const res = d->modes->selectedItem().data().toPoint(); int attribs[] = { ClientWindow::Width, res.x(), ClientWindow::Height, res.y(), ClientWindow::End }; d->win.changeAttributes(attribs); } doomsday-stable-1.15.7/doomsday/client/src/ui/dialogs/renderersettingsdialog.cpp0000664000175000017500000001704412641367670027400 0ustar jaakkojaakko/** @file renderersettingsdialog.cpp Settings for the renderer. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/dialogs/renderersettingsdialog.h" #include "ui/widgets/cvarsliderwidget.h" #include "ui/widgets/cvartogglewidget.h" #include "ui/widgets/cvarchoicewidget.h" #include "ui/widgets/taskbarwidget.h" #include "ui/editors/rendererappearanceeditor.h" #include "ui/widgets/profilepickerwidget.h" #include "clientapp.h" #include "ui/clientwindow.h" #include #include #include #include #include #include using namespace de; using namespace ui; DENG_GUI_PIMPL(RendererSettingsDialog) { ProfilePickerWidget *appear; CVarSliderWidget *fov; CVarToggleWidget *precacheModels; CVarToggleWidget *precacheSprites; CVarToggleWidget *multiLight; CVarToggleWidget *multiShiny; CVarToggleWidget *multiDetail; // Developer settings. GridPopupWidget *devPopup; Instance(Public *i) : Base(i) { ScrollAreaWidget &area = self.area(); area.add(appear = new ProfilePickerWidget(ClientApp::renderSystem().appearanceSettings(), tr("appearance"), "profile-picker")); appear->setOpeningDirection(ui::Down); area.add(fov = new CVarSliderWidget("rend-camera-fov")); fov->setPrecision(0); fov->setRange(Ranged(30, 160)); // Set up a separate popup for developer settings. self.add(devPopup = new GridPopupWidget); CVarChoiceWidget *rendTex = new CVarChoiceWidget("rend-tex"); rendTex->items() << new ChoiceItem(tr("Materials"), 1) << new ChoiceItem(tr("Plain white"), 0) << new ChoiceItem(tr("Plain gray"), 2); CVarChoiceWidget *wireframe = new CVarChoiceWidget("rend-dev-wireframe"); wireframe->items() << new ChoiceItem(tr("Nothing"), 0) << new ChoiceItem(tr("Game world"), 1) << new ChoiceItem(tr("Game world and UI"), 2); precacheModels = new CVarToggleWidget("rend-model-precache", tr("3D Models")); precacheSprites = new CVarToggleWidget("rend-sprite-precache", tr("Sprites")); multiLight = new CVarToggleWidget("rend-light-multitex", tr("Dynamic Lights")); multiShiny = new CVarToggleWidget("rend-model-shiny-multitex", tr("3D Model Shiny Surfaces")); multiDetail = new CVarToggleWidget("rend-tex-detail-multitex", tr("Surface Details")); devPopup->addSeparatorLabel(tr("Behavior")); *devPopup << LabelWidget::newWithText(tr("Precaching:")) << precacheModels << Const(0) << precacheSprites << LabelWidget::newWithText(tr("Multitexturing:")) << multiLight << Const(0) << multiShiny << Const(0) << multiDetail; devPopup->addSeparatorLabel(tr("Diagnosis")); *devPopup << LabelWidget::newWithText(tr("Surface Texturing:")) << rendTex << LabelWidget::newWithText(tr("Draw as Wireframe:")) << wireframe << LabelWidget::newWithText(tr("Bounds:")) << new CVarToggleWidget("rend-dev-mobj-bbox", tr("Mobj Bounding Boxes")) << Const(0) << new CVarToggleWidget("rend-dev-polyobj-bbox", tr("Polyobj Bounding Boxes")) << LabelWidget::newWithText(tr("Identifiers:")) << new CVarToggleWidget("rend-dev-thinker-ids", tr("Thinker IDs")) << Const(0) << new CVarToggleWidget("rend-dev-sector-show-indices", tr("Sector Indices")) << Const(0) << new CVarToggleWidget("rend-dev-vertex-show-indices", tr("Vertex Indices")) << Const(0) << new CVarToggleWidget("rend-dev-generator-show-indices", tr("Particle Generator Indices")); devPopup->commit(); } void fetch() { /// @todo These widgets should be intelligent enough to fetch their /// cvar values when they need to.... foreach(Widget *child, self.area().childWidgets() + devPopup->content().childWidgets()) { if(ICVarWidget *w = child->maybeAs()) { w->updateFromCVar(); } } } }; RendererSettingsDialog::RendererSettingsDialog(String const &name) : DialogWidget(name, WithHeading), d(new Instance(this)) { heading().setText(tr("Renderer Settings")); LabelWidget *appearLabel = LabelWidget::newWithText(tr("Appearance:"), &area()); appearLabel->setName("appearance-label"); // for lookup from tutorial LabelWidget *fovLabel = LabelWidget::newWithText(tr("Field of View:"), &area()); // Layout. GridLayout layout(area().contentRule().left(), area().contentRule().top()); layout.setGridSize(2, 0); layout.setColumnAlignment(0, ui::AlignRight); layout << *appearLabel; // The profile button must be included in the layout. layout.append(*d->appear, d->appear->rule().width() + d->appear->button().rule().width()); layout << *fovLabel << *d->fov; // Slider for modifying the global pixel density factor. This allows slower // GPUs to compensate for large resolutions. { auto *pd = new VariableSliderWidget(App::config("render.pixelDensity"), Ranged(0, 1), .05); pd->setPrecision(2); area().add(pd); layout << *LabelWidget::newWithText(tr("Pixel Density:"), &area()) << *pd; } area().setContentSize(layout.width(), layout.height()); buttons() << new DialogButtonItem(DialogWidget::Default | DialogWidget::Accept, tr("Close")) << new DialogButtonItem(DialogWidget::Action, tr("Reset to Defaults"), new SignalAction(this, SLOT(resetToDefaults()))) << new DialogButtonItem(DialogWidget::Action | Id1, style().images().image("gauge"), new SignalAction(this, SLOT(showDeveloperPopup()))); // Identifiers popup opens from the button. d->devPopup->setAnchorAndOpeningDirection(buttonWidget(Id1)->rule(), ui::Up); connect(this, SIGNAL(closed()), d->devPopup, SLOT(close())); connect(d->appear, SIGNAL(profileEditorRequested()), this, SLOT(editProfile())); d->fetch(); } void RendererSettingsDialog::resetToDefaults() { ClientApp::renderSystem().settings().resetToDefaults(); d->fetch(); } void RendererSettingsDialog::showDeveloperPopup() { d->devPopup->open(); } void RendererSettingsDialog::editProfile() { RendererAppearanceEditor *editor = new RendererAppearanceEditor; editor->open(); ClientWindow::main().taskBar().closeConfigMenu(); } doomsday-stable-1.15.7/doomsday/client/src/ui/zonedebug.cpp0000664000175000017500000001312612641367670023166 0ustar jaakkojaakko/** @file zonedebug.cpp Memory zone debug visualization. * @ingroup memzone * * Shows the contents of the memory zone as on-screen visualization. This is * only available in debug builds and provides a view to the layout of the * allocated memory inside the zone. * * @authors Copyright (c) 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifdef _DEBUG #include "de_base.h" #include "de_graphics.h" #include #include #include #include /// @todo Find a better way to access the private data of the zone /// (e.g., move this into the library and use an abstract graphics interface). #include "../../liblegacy/src/memoryzone_private.h" using namespace de; static void drawRegion(memvolume_t &volume, Rectanglei const &rect, size_t start, size_t size, float const color[4]) { DENG2_ASSERT(start + size <= volume.size); int const bytesPerRow = (volume.size - sizeof(memzone_t)) / rect.height(); float const toPixelScale = (float)rect.width() / (float)bytesPerRow; size_t const edge = rect.topLeft.x + rect.width(); int x = (start % bytesPerRow) * toPixelScale + rect.topLeft.x; int y = start / bytesPerRow + rect.topLeft.y; int pixels = de::max(1, std::ceil(size * toPixelScale)); while(pixels > 0) { int const availPixels = edge - x; int const usedPixels = de::min(availPixels, pixels); glColor4fv(color); glVertex2i(x, y); glVertex2i(x + usedPixels, y); pixels -= usedPixels; // Move to the next row. y++; x = rect.topLeft.x; } } void Z_DebugDrawVolume(MemoryZonePrivateData *pd, memvolume_t *volume, Rectanglei const &rect) { float const opacity = .85f; float const colAppStatic[4] = { 1, 1, 1, .65f }; float const colGameStatic[4] = { 1, 0, 0, .65f }; float const colMap[4] = { 0, 1, 0, .65f }; float const colMapStatic[4] = { 0, .5f, 0, .65f }; float const colCache[4] = { 1, 0, 1, .65f }; float const colOther[4] = { 0, 0, 1, .65f }; char *base = ((char *)volume->zone) + sizeof(memzone_t); // Clear the background. glColor4f(0, 0, 0, opacity); GL_DrawRect(rect); // Outline. glLineWidth(1); glColor4f(1, 1, 1, opacity/2); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); GL_DrawRect(rect); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glBegin(GL_LINES); // Visualize each block. for(memblock_t *block = volume->zone->blockList.next; block != &volume->zone->blockList; block = block->next) { float const *color = colOther; if(!block->user) continue; // Free is black. // Choose the color for this block. switch(block->tag) { case PU_GAMESTATIC: color = colGameStatic; break; case PU_MAP: color = colMap; break; case PU_MAPSTATIC: color = colMapStatic; break; case PU_APPSTATIC: color = colAppStatic; break; default: if(block->tag >= PU_PURGELEVEL) color = colCache; break; } drawRegion(*volume, rect, (char *)block - base, block->size, color); } glEnd(); if(pd->isVolumeTooFull(volume)) { glLineWidth(2); glColor4f(1, 0, 0, 1); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); GL_DrawRect(rect); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } } void Z_DebugDrawer(void) { MemoryZonePrivateData pd; memvolume_t* volume; int i, volCount, h; if(!CommandLine_Exists("-zonedebug")) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glDisable(GL_CULL_FACE); glDisable(GL_DEPTH_TEST); // Go into screen projection mode. glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, DENG_GAMEVIEW_WIDTH, DENG_GAMEVIEW_HEIGHT, 0, -1, 1); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); Z_GetPrivateData(&pd); // Draw each volume. pd.lock(); // Make sure all the volumes fit vertically. volCount = pd.volumeCount; h = 200; if(h * volCount + 10*(volCount - 1) > DENG_GAMEVIEW_HEIGHT) { h = (DENG_GAMEVIEW_HEIGHT - 10*(volCount - 1))/volCount; } i = 0; for(volume = pd.volumeRoot; volume; volume = volume->next, ++i) { int size = de::min(400, DENG_GAMEVIEW_WIDTH); Z_DebugDrawVolume(&pd, volume, Rectanglei::fromSize(Vector2i(DENG_GAMEVIEW_WIDTH - size - 1, DENG_GAMEVIEW_HEIGHT - size * (i+1) - 10*i - 1), Vector2ui(size, size))); } pd.unlock(); // Cleanup. glMatrixMode(GL_MODELVIEW); glPopMatrix(); glMatrixMode(GL_PROJECTION); glPopMatrix(); } #endif // _DEBUG doomsday-stable-1.15.7/doomsday/client/src/ui/busyvisual.cpp0000664000175000017500000001617712641367670023423 0ustar jaakkojaakko/** @file busyvisual.cpp Busy Mode visualizer. * @ingroup render * * @authors Copyright © 2007-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * @authors Copyright © 2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include "de_base.h" #include "de_console.h" #include "de_system.h" #include "de_graphics.h" #include "de_ui.h" #include "ui/busyvisual.h" #include "ui/widgets/busywidget.h" static void releaseScreenshotTexture() { ClientWindow::main().busy().releaseTransitionFrame(); } void BusyVisual_PrepareResources(void) { BusyTask* task = BusyMode_CurrentTask(); if(!task) return; if(task->mode & BUSYF_STARTUP) { // Not in startup, so take a copy of the current frame contents. releaseScreenshotTexture(); } } /** * Transition effect: */ #include "de_misc.h" using namespace de; #define DOOMWIPESINE_NUMSAMPLES 320 static void seedDoomWipeSine(void); int rTransition = (int) TS_CROSSFADE; ///< cvar Default transition style. int rTransitionTics = 28; ///< cvar Default transition duration (in tics). typedef struct { dd_bool inProgress; /// @c true= a transition is presently being animated. transitionstyle_t style; /// Style of transition (cross-fade, wipe, etc...). uint startTime; /// Time at the moment the transition began (in 35hz tics). uint tics; /// Time duration of the animation. float position; /// Animation interpolation point [0..1]. } transitionstate_t; static transitionstate_t transition; static float doomWipeSine[DOOMWIPESINE_NUMSAMPLES]; static float doomWipeSamples[SCREENWIDTH+1]; void Con_TransitionRegister(void) { C_VAR_INT("con-transition", &rTransition, 0, FIRST_TRANSITIONSTYLE, LAST_TRANSITIONSTYLE); C_VAR_INT("con-transition-tics", &rTransitionTics, 0, 0, 60); } void Con_TransitionConfigure(void) { transition.tics = rTransitionTics; transition.style = (transitionstyle_t) rTransition; if(transition.style == TS_DOOM || transition.style == TS_DOOMSMOOTH) { seedDoomWipeSine(); } transition.inProgress = true; } void Con_TransitionBegin(void) { transition.startTime = Timer_Ticks(); transition.position = 0; } dd_bool Con_TransitionInProgress(void) { return transition.inProgress; } static void Con_EndTransition(void) { if(!transition.inProgress) return; releaseScreenshotTexture(); transition.inProgress = false; } void Con_TransitionTicker(timespan_t /*ticLength*/) { if(isDedicated) return; if(!Con_TransitionInProgress()) return; transition.position = (float)(Timer_Ticks() - transition.startTime) / transition.tics; if(transition.position >= 1) { Con_EndTransition(); } } static void seedDoomWipeSine(void) { int i; doomWipeSine[0] = (128 - RNG_RandByte()) / 128.f; for(i = 1; i < DOOMWIPESINE_NUMSAMPLES; ++i) { float r = (128 - RNG_RandByte()) / 512.f; doomWipeSine[i] = MINMAX_OF(-1, doomWipeSine[i-1] + r, 1); } } static float sampleDoomWipeSine(float point) { float sample = doomWipeSine[(uint)ROUND((DOOMWIPESINE_NUMSAMPLES-1) * MINMAX_OF(0, point, 1))]; float offset = SCREENHEIGHT * transition.position * transition.position; return offset + (SCREENHEIGHT/2) * (transition.position + sample) * transition.position * transition.position; } static void sampleDoomWipe(void) { int i; for(i = 0; i <= SCREENWIDTH; ++i) { doomWipeSamples[i] = MAX_OF(0, sampleDoomWipeSine((float) i / SCREENWIDTH)); } } void Con_DrawTransition(void) { if(isDedicated) return; if(!Con_TransitionInProgress()) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, SCREENWIDTH, SCREENHEIGHT, 0, -1, 1); DENG2_ASSERT(ClientWindow::main().busy().transitionFrame() != 0); GLuint const texScreenshot = ClientWindow::main().busy().transitionFrame()->glName(); GL_BindTextureUnmanaged(texScreenshot, gl::ClampToEdge, gl::ClampToEdge); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glDisable(GL_ALPHA_TEST); glEnable(GL_TEXTURE_2D); switch(transition.style) { case TS_DOOMSMOOTH: { float topAlpha, s, div, colWidth = 1.f / SCREENWIDTH; int x, y, h, i; sampleDoomWipe(); div = 1 - .25f * transition.position; topAlpha = 1 - transition.position; topAlpha *= topAlpha; h = SCREENHEIGHT * (1 - div); x = 0; s = 0; glBegin(GL_QUAD_STRIP); for(i = 0; i <= SCREENWIDTH; ++i, x++, s += colWidth) { y = doomWipeSamples[i]; glColor4f(1, 1, 1, topAlpha); glTexCoord2f(s, 1); glVertex2i(x, y); glColor4f(1, 1, 1, 1); glTexCoord2f(s, div); glVertex2i(x, y + h); } glEnd(); x = 0; s = 0; glColor4f(1, 1, 1, 1); glBegin(GL_QUAD_STRIP); for(i = 0; i <= SCREENWIDTH; ++i, x++, s += colWidth) { y = doomWipeSamples[i] + h; glTexCoord2f(s, div); glVertex2i(x, y); glTexCoord2f(s, 0); glVertex2i(x, y + (SCREENHEIGHT - h)); } glEnd(); break; } case TS_DOOM: { // As above but drawn with whole pixel columns. float s = 0, colWidth = 1.f / SCREENWIDTH; int x = 0, y, i; sampleDoomWipe(); glColor4f(1, 1, 1, 1); glBegin(GL_QUADS); for(i = 0; i <= SCREENWIDTH; ++i, x++, s+= colWidth) { y = doomWipeSamples[i]; glTexCoord2f(s, 1); glVertex2i(x, y); glTexCoord2f(s+colWidth, 1); glVertex2i(x+1, y); glTexCoord2f(s+colWidth, 0); glVertex2i(x+1, y+SCREENHEIGHT); glTexCoord2f(s, 0); glVertex2i(x, y+SCREENHEIGHT); } glEnd(); break; } case TS_CROSSFADE: glColor4f(1, 1, 1, 1 - transition.position); glBegin(GL_QUADS); glTexCoord2f(0, 1); glVertex2f(0, 0); glTexCoord2f(0, 0); glVertex2f(0, SCREENHEIGHT); glTexCoord2f(1, 0); glVertex2f(SCREENWIDTH, SCREENHEIGHT); glTexCoord2f(1, 1); glVertex2f(SCREENWIDTH, 0); glEnd(); break; } GL_SetNoTexture(); glEnable(GL_ALPHA_TEST); glMatrixMode(GL_PROJECTION); glPopMatrix(); } doomsday-stable-1.15.7/doomsday/client/src/ui/impulsebinding.cpp0000664000175000017500000001367612641367670024227 0ustar jaakkojaakko/** @file impulsebinding.cpp Impulse binding record accessor. * * @authors Copyright © 2009-2014 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "dd_share.h" // DDMAXPLAYERS #include "ui/impulsebinding.h" #include #include #include #include "ui/b_util.h" using namespace de; void ImpulseBinding::resetToDefaults() { Binding::resetToDefaults(); def().addNumber("deviceId", -1); def().addNumber("controlId", -1); def().addNumber("type", int(IBD_TOGGLE)); ///< Type of event. def().addNumber("angle", 0); def().addNumber("flags", 0); def().addNumber("impulseId", 0); ///< Identifier of the bound player impulse. def().addNumber("localPlayer", -1); ///< Local player number. } String ImpulseBinding::composeDescriptor() { LOG_AS("ImpulseBinding"); if(!*this) return ""; String str = B_ControlDescToString(geti("deviceId"), IBDTYPE_TO_EVTYPE(geti("type")), geti("controlId")); if(geti("type") == IBD_ANGLE) { str += B_HatAngleToString(getf("angle")); } // Additional flags. if(geti("flags") & IBDF_TIME_STAGED) { str += "-staged"; } if(geti("flags") & IBDF_INVERSE) { str += "-inverse"; } // Append any state conditions. ArrayValue const &conds = def().geta("condition"); DENG2_FOR_EACH_CONST(ArrayValue::Elements, i, conds.elements()) { str += " + " + B_ConditionToString(*(*i)->as().record()); } return str; } static bool doConfigure(ImpulseBinding &bind, char const *ctrlDesc, int impulseId, int localPlayer) { DENG2_ASSERT(ctrlDesc); bind.resetToDefaults(); bind.def().set("impulseId", impulseId); bind.def().set("localPlayer", localPlayer); // Parse the control descriptor. AutoStr *str = AutoStr_NewStd(); // First, the device name. ctrlDesc = Str_CopyDelim(str, ctrlDesc, '-'); if(!Str_CompareIgnoreCase(str, "key")) { bind.def().set("deviceId", IDEV_KEYBOARD); bind.def().set("type", int(IBD_TOGGLE)); // Next part defined button. ctrlDesc = Str_CopyDelim(str, ctrlDesc, '-'); int keyId; bool ok = B_ParseKeyId(keyId, Str_Text(str)); if(!ok) return false; bind.def().set("controlId", keyId); } else if(!Str_CompareIgnoreCase(str, "mouse")) { bind.def().set("deviceId", IDEV_MOUSE); ctrlDesc = Str_CopyDelim(str, ctrlDesc, '-'); ddeventtype_t type; int controlId = 0; bool ok = B_ParseMouseTypeAndId(type, controlId, Str_Text(str)); if(!ok) return false; bind.def().set("controlId", controlId); bind.def().set("type", int(EVTYPE_TO_IBDTYPE(type))); } else if(!Str_CompareIgnoreCase(str, "joy") || !Str_CompareIgnoreCase(str, "head")) { bind.def().set("deviceId", (!Str_CompareIgnoreCase(str, "joy")? IDEV_JOY1 : IDEV_HEAD_TRACKER)); // Next part defined button, axis, or hat. ctrlDesc = Str_CopyDelim(str, ctrlDesc, '-'); ddeventtype_t type; int controlId = 0; bool ok = B_ParseJoystickTypeAndId(type, controlId, bind.geti("deviceId"), Str_Text(str)); if(!ok) return false; bind.def().set("controlId", controlId); bind.def().set("type", int(EVTYPE_TO_IBDTYPE(type))); // Hats include the angle. if(type == E_ANGLE) { ctrlDesc = Str_CopyDelim(str, ctrlDesc, '-'); float angle; ok = B_ParseHatAngle(angle, Str_Text(str)); if(!ok) return false; bind.def().set("angle", angle); } } // Finally, there may be some flags at the end. while(ctrlDesc) { ctrlDesc = Str_CopyDelim(str, ctrlDesc, '-'); if(!Str_CompareIgnoreCase(str, "inverse")) { bind.def().set("flags", bind.geti("flags") | IBDF_INVERSE); } else if(!Str_CompareIgnoreCase(str, "staged")) { bind.def().set("flags", bind.geti("flags") | IBDF_TIME_STAGED); } else { LOG_INPUT_WARNING("Unrecognized \"%s\"") << ctrlDesc; return false; } } return true; } void ImpulseBinding::configure(char const *ctrlDesc, int impulseId, int localPlayer, bool assignNewId) { DENG2_ASSERT(ctrlDesc); DENG2_ASSERT(localPlayer >= 0 && localPlayer < DDMAXPLAYERS); LOG_AS("ImpulseBinding"); // The first part specifies the device-control condition. AutoStr *str = AutoStr_NewStd(); ctrlDesc = Str_CopyDelim(str, ctrlDesc, '+'); if(!doConfigure(*this, Str_Text(str), impulseId, localPlayer)) { throw ConfigureError("ImpulseBinding::configure", "Descriptor parse error"); } // Any conditions? def()["condition"].value().clear(); while(ctrlDesc) { // A new condition. ctrlDesc = Str_CopyDelim(str, ctrlDesc, '+'); Record &cond = addCondition(); if(!B_ParseBindingCondition(cond, Str_Text(str))) { throw ConfigureError("ImpulseBinding::configure", "Descriptor parse error"); } } if(assignNewId) { def().set("id", newIdentifier()); } } doomsday-stable-1.15.7/doomsday/client/src/ui/bindcontext.cpp0000664000175000017500000003503712641367670023532 0ustar jaakkojaakko/** @file bindcontext.cpp Input system, binding context. * * @authors Copyright © 2009-2013 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/bindcontext.h" #include #include #include #include #include "clientapp.h" #include "world/p_players.h" #include "CommandBinding" #include "ImpulseBinding" #include "ui/inputdevice.h" using namespace de; DENG2_PIMPL(BindContext) { bool active = false; ///< @c true= Bindings are active. bool protect = false; ///< @c true= Prevent explicit end user (de)activation. String name; ///< Symbolic. // Acquired device states, unless higher-priority contexts override. typedef QSet DeviceIds; DeviceIds acquireDevices; bool acquireAllDevices = false; ///< @c true= will ignore @var acquireDevices. typedef QList CommandBindings; CommandBindings commandBinds; typedef QList ImpulseBindings; ImpulseBindings impulseBinds[DDMAXPLAYERS]; ///< Group bindings for each local player. DDFallbackResponderFunc ddFallbackResponder = nullptr; FallbackResponderFunc fallbackResponder = nullptr; Instance(Public *i) : Base(i) {} /** * Look through the context for a binding that matches either of @a matchCmd or * @a matchImp. * * @param matchCmd Command binding record to match, if any. * @param matchImp Impulse binding record to match, if any. * @param cmdResult The address of any matching command binding is written here. * @param impResult The address of any matching impulse binding is written here. * * @return @c true if a match is found. */ bool findMatchingBinding(Record const *matchCmdRec, Record const *matchImpRec, Record **cmdResult, Record **impResult) const { DENG2_ASSERT(cmdResult && impResult); *cmdResult = nullptr; *impResult = nullptr; if(!matchCmdRec && !matchImpRec) return false; CommandBinding matchCmd; if(matchCmdRec) matchCmd = matchCmdRec; ImpulseBinding matchImp; if(matchImpRec) matchImp = matchImpRec; for(Record const *rec : commandBinds) { CommandBinding bind(*rec); if(matchCmd && matchCmd.geti("id") != rec->geti("id")) { if(matchCmd.geti("type") == bind.geti("type") && matchCmd.geti("test") == bind.geti("test") && matchCmd.geti("deviceId") == bind.geti("deviceId") && matchCmd.geti("controlId") == bind.geti("controlId") && matchCmd.equalConditions(bind)) { *cmdResult = const_cast(rec); return true; } } if(matchImp) { if(matchImp.geti("type") == bind.geti("type") && matchImp.geti("deviceId") == bind.geti("deviceId") && matchImp.geti("controlId") == bind.geti("controlId") && matchImp.equalConditions(bind)) { *cmdResult = const_cast(rec); return true; } } } for(int i = 0; i < DDMAXPLAYERS; ++i) for(Record const *rec : impulseBinds[i]) { ImpulseBinding bind(*rec); if(matchCmd) { if(matchCmd.geti("type") == bind.geti("type") && matchCmd.geti("deviceId") == bind.geti("deviceId") && matchCmd.geti("controlId") == bind.geti("controlId") && matchCmd.equalConditions(bind)) { *impResult = const_cast(rec); return true; } } if(matchImp && matchImp.geti("id") != bind.geti("id")) { if(matchImp.geti("type") == bind.geti("type") && matchImp.geti("deviceId") == bind.geti("deviceId") && matchImp.geti("controlId") == bind.geti("controlId") && matchImp.equalConditions(bind)) { *impResult = const_cast(rec); return true; } } } // Nothing found. return false; } /** * Delete all other bindings matching either @a cmdBinding or @a impBinding. */ void deleteMatching(Record const *cmdBinding, Record const *impBinding) { Record *foundCmd = nullptr; Record *foundImp = nullptr; while(findMatchingBinding(cmdBinding, impBinding, &foundCmd, &foundImp)) { // Only either foundCmd or foundImp is returned as non-NULL. int bindId = (foundCmd? foundCmd->geti("id") : (foundImp? foundImp->geti("id") : 0)); if(bindId) { LOG_INPUT_VERBOSE("Deleting binding %i, it has been overridden by binding %i") << bindId << (cmdBinding? cmdBinding->geti("id") : impBinding->geti("id")); self.deleteBinding(bindId); } } } DENG2_PIMPL_AUDIENCE(ActiveChange) DENG2_PIMPL_AUDIENCE(AcquireDeviceChange) DENG2_PIMPL_AUDIENCE(BindingAddition) }; DENG2_AUDIENCE_METHOD(BindContext, ActiveChange) DENG2_AUDIENCE_METHOD(BindContext, AcquireDeviceChange) DENG2_AUDIENCE_METHOD(BindContext, BindingAddition) BindContext::BindContext(String const &name) : d(new Instance(this)) { setName(name); } BindContext::~BindContext() { clearAllBindings(); } bool BindContext::isActive() const { return d->active; } bool BindContext::isProtected() const { return d->protect; } void BindContext::protect(bool yes) { d->protect = yes; } String BindContext::name() const { return d->name; } void BindContext::setName(String const &newName) { d->name = newName; } void BindContext::activate(bool yes) { if(d->active == yes) return; LOG_AS("BindContext"); LOG_INPUT_VERBOSE("%s " _E(b) "'%s'") << (yes? "Activating" : "Deactivating") << d->name; d->active = yes; // Notify interested parties. DENG2_FOR_AUDIENCE2(ActiveChange, i) i->bindContextActiveChanged(*this); } void BindContext::acquire(int deviceId, bool yes) { DENG2_ASSERT(deviceId >= 0 && deviceId < NUM_INPUT_DEVICES); int const countBefore = d->acquireDevices.count(); if(yes) d->acquireDevices.insert(deviceId); else d->acquireDevices.remove(deviceId); if(countBefore != d->acquireDevices.count()) { // Notify interested parties. DENG2_FOR_AUDIENCE2(AcquireDeviceChange, i) i->bindContextAcquireDeviceChanged(*this); } } void BindContext::acquireAll(bool yes) { if(d->acquireAllDevices != yes) { d->acquireAllDevices = yes; // Notify interested parties. DENG2_FOR_AUDIENCE2(AcquireDeviceChange, i) i->bindContextAcquireDeviceChanged(*this); } } bool BindContext::willAcquire(int deviceId) const { return d->acquireAllDevices || d->acquireDevices.contains(deviceId); } bool BindContext::willAcquireAll() const { return d->acquireAllDevices; } void BindContext::setDDFallbackResponder(DDFallbackResponderFunc newResponderFunc) { d->ddFallbackResponder = newResponderFunc; } void BindContext::setFallbackResponder(FallbackResponderFunc newResponderFunc) { d->fallbackResponder = newResponderFunc; } void BindContext::clearAllBindings() { LOG_AS("BindContext"); qDeleteAll(d->commandBinds); d->commandBinds.clear(); for(int i = 0; i < DDMAXPLAYERS; ++i) { qDeleteAll(d->impulseBinds[i]); d->impulseBinds[i].clear(); } LOG_INPUT_VERBOSE(_E(b) "'%s'" _E(.) " cleared") << d->name; } Record *BindContext::bindCommand(char const *eventDesc, char const *command) { DENG2_ASSERT(eventDesc && command && command[0]); LOG_AS("BindContext"); try { std::unique_ptr newBind(new Record); CommandBinding bind(*newBind.get()); bind.configure(eventDesc, command); // Assign a new unique identifier. d->commandBinds.prepend(newBind.release()); LOG_INPUT_VERBOSE("Command " _E(b) "\"%s\"" _E(.) " now bound to " _E(b) "\"%s\"" _E(.) " in " _E(b) "'%s'" _E(.) " with binding Id " _E(b) "%i") << command << bind.composeDescriptor() << d->name << bind.geti("id"); /// @todo: In interactive binding mode, should ask the user if the /// replacement is ok. For now, just delete the other bindings. d->deleteMatching(&bind.def(), nullptr); // Notify interested parties. DENG2_FOR_AUDIENCE2(BindingAddition, i) i->bindContextBindingAdded(*this, bind.def(), true/*is-command*/); return &bind.def(); } catch(Binding::ConfigureError const &) {} return nullptr; } Record *BindContext::bindImpulse(char const *ctrlDesc, PlayerImpulse const &impulse, int localPlayer) { DENG2_ASSERT(ctrlDesc); DENG2_ASSERT(localPlayer >= 0 && localPlayer < DDMAXPLAYERS); LOG_AS("BindContext"); try { std::unique_ptr newBind(new Record); ImpulseBinding bind(*newBind.get()); bind.configure(ctrlDesc, impulse.id, localPlayer); // Assign a new unique identifier. d->impulseBinds[localPlayer].append(newBind.release()); LOG_INPUT_VERBOSE("Impulse " _E(b) "'%s'" _E(.) " of player%i now bound to \"%s\" in " _E(b) "'%s'" _E(.) " with binding Id " _E(b) "%i") << impulse.name << (localPlayer + 1) << bind.composeDescriptor() << d->name << bind.geti("id"); /// @todo: In interactive binding mode, should ask the user if the /// replacement is ok. For now, just delete the other bindings. d->deleteMatching(nullptr, &bind.def()); // Notify interested parties. DENG2_FOR_AUDIENCE2(BindingAddition, i) i->bindContextBindingAdded(*this, bind.def(), false/*is-impulse*/); return &bind.def(); } catch(Binding::ConfigureError const &) {} return nullptr; } Record *BindContext::findCommandBinding(char const *command, int deviceId) const { if(command && command[0]) { for(Record const *rec : d->commandBinds) { CommandBinding bind(*rec); if(bind.gets("command").compareWithoutCase(command)) continue; if((deviceId < 0 || deviceId >= NUM_INPUT_DEVICES) || bind.geti("deviceId") == deviceId) { return const_cast(rec); } } } return nullptr; } Record *BindContext::findImpulseBinding(int deviceId, ibcontroltype_t bindType, int controlId) const { for(int i = 0; i < DDMAXPLAYERS; ++i) for(Record const *rec : d->impulseBinds[i]) { ImpulseBinding bind(*rec); if(bind.geti("type") == bindType && bind.geti("deviceId") == deviceId && bind.geti("controlId") == controlId) { return const_cast(rec); } } return nullptr; } bool BindContext::deleteBinding(int id) { // Check if it is one of the command bindings. for(int i = 0; i < d->commandBinds.count(); ++i) { Record *rec = d->commandBinds.at(i); if(rec->geti("id") == id) { d->commandBinds.removeAt(i); delete rec; return true; } } // How about one of the impulse bindings? for(int i = 0; i < DDMAXPLAYERS; ++i) for(int k = 0; k < d->impulseBinds[i].count(); ++k) { Record *rec = d->impulseBinds[i].at(k); if(rec->geti("id") == id) { d->impulseBinds[i].removeAt(k); delete rec; return true; } } return false; } bool BindContext::tryEvent(ddevent_t const &event, bool respectHigherContexts) const { LOG_AS("BindContext"); // Inactive contexts never respond. if(!isActive()) return false; // Is this event bindable to an action? if(event.type != E_FOCUS) { // See if the command bindings will have it. for(Record const *rec : d->commandBinds) { CommandBinding bind(*rec); AutoRef act(bind.makeAction(event, *this, respectHigherContexts)); if(act.get()) { act->trigger(); return true; // Triggered! } } } // Try the fallback responders. if(d->ddFallbackResponder) { if(d->ddFallbackResponder(&event)) return true; } if(d->fallbackResponder) { event_t ev; if(/*bool const validGameEvent =*/ InputSystem::convertEvent(event, ev)) { if(d->fallbackResponder(&ev)) return true; } } return false; } LoopResult BindContext::forAllCommandBindings( std::function func) const { for(Record *rec : d->commandBinds) { if(auto result = func(*rec)) return result; } return LoopContinue; } LoopResult BindContext::forAllImpulseBindings(int localPlayer, std::function func) const { for(int i = 0; i < DDMAXPLAYERS; ++i) { if((localPlayer < 0 || localPlayer >= DDMAXPLAYERS) || localPlayer == i) { for(Record *rec : d->impulseBinds[i]) { if(auto result = func(*rec)) return result; } } } return LoopContinue; } int BindContext::commandBindingCount() const { return d->commandBinds.count(); } int BindContext::impulseBindingCount(int localPlayer) const { int count = 0; for(int i = 0; i < DDMAXPLAYERS; ++i) { if((localPlayer < 0 || localPlayer >= DDMAXPLAYERS) || localPlayer == i) { count += d->impulseBinds[i].count(); } } return count; } doomsday-stable-1.15.7/doomsday/client/src/ui/sys_input.cpp0000664000175000017500000000771012641367670023243 0ustar jaakkojaakko/** @file sys_input.cpp Keyboard and mouse input pre-processing. * @ingroup input * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * @authors Copyright © 2005 Zachary Keene * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #ifdef WIN32 # include "directinput.h" # include "mouse_win32.h" #endif #include "de_platform.h" #include "de_console.h" #include "de_system.h" #include "de_misc.h" #include "ui/mouse_qt.h" // portable #define EVBUFSIZE 64 #define KEYBUFSIZE 32 static dd_bool initOk; static byte useMouse; // Input enabled from mouse? static mouseinterface_t* iMouse; ///< Current mouse interface. static keyevent_t keyEvents[EVBUFSIZE]; static int evHead, evTail; void I_Register(void) { #ifdef __CLIENT__ Joystick_Register(); #endif } static keyevent_t *newKeyEvent(void) { keyevent_t *ev = keyEvents + evHead; evHead = (evHead + 1) % EVBUFSIZE; memset(ev, 0, sizeof(*ev)); return ev; } /** * @return The oldest event from the buffer. */ static keyevent_t *getKeyEvent(void) { keyevent_t *ev; if(evHead == evTail) return NULL; // No more... ev = keyEvents + evTail; evTail = (evTail + 1) % EVBUFSIZE; return ev; } static void Mouse_Init(void) { if(CommandLine_Check("-nomouse") || novideo) return; LOG_AS("Mouse_Init"); DENG_ASSERT(iMouse); iMouse->init(); // Init was successful. useMouse = true; } dd_bool I_InitInterfaces(void) { if(initOk) return true; // Already initialized. #ifdef __CLIENT__ // Select drivers. iMouse = &qtMouse; #ifdef WIN32 iMouse = &win32Mouse; DirectInput_Init(); #endif Mouse_Init(); Joystick_Init(); #endif // __CLIENT__ initOk = true; return true; } void I_ShutdownInterfaces() { if(!initOk) return; // Not initialized. #ifdef __CLIENT__ if(useMouse) iMouse->shutdown(); useMouse = false; Joystick_Shutdown(); # ifdef WIN32 DirectInput_Shutdown(); # endif #endif initOk = false; } void Keyboard_Submit(int type, int ddKey, int native, const char* text) { if(ddKey != 0) { keyevent_t* e = newKeyEvent(); e->type = type; e->ddkey = ddKey; e->native = native; if(text) { strncpy(e->text, text, sizeof(e->text) - 1); } } } size_t Keyboard_GetEvents(keyevent_t *evbuf, size_t bufsize) { keyevent_t *e; size_t i = 0; if(!initOk) return 0; // Get the events. for(i = 0; i < bufsize; ++i) { e = getKeyEvent(); if(!e) break; // No more events. memcpy(&evbuf[i], e, sizeof(*e)); } return i; } dd_bool Mouse_IsPresent(void) { //if(!initOk) I_InitInterfaces(); return useMouse; } void Mouse_Poll(void) { if(useMouse) { iMouse->poll(); } } void Mouse_GetState(mousestate_t *state) { if(useMouse) { iMouse->getState(state); } } void Mouse_Trap(dd_bool enabled) { if(useMouse) { iMouse->trap(enabled); } } doomsday-stable-1.15.7/doomsday/client/src/ui/commandaction.cpp0000664000175000017500000000230412641367670024014 0ustar jaakkojaakko/** @file commandaction.cpp Action that executes a console command. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "CommandAction" #include using namespace de; CommandAction::CommandAction(String const &cmd, int commandSource) : _command(cmd), _source(commandSource) {} void CommandAction::trigger() { Action::trigger(); Con_Execute(_source, _command.toUtf8(), false /*silent*/, false /*net*/); } doomsday-stable-1.15.7/doomsday/client/src/ui/inputdeviceaxiscontrol.cpp0000664000175000017500000002115012641367670026005 0ustar jaakkojaakko/** @file inputdeviceaxiscontrol.cpp Axis control for a logical input device. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "ui/inputdeviceaxiscontrol.h" #include #include // SECONDSPERTIC #include #include #include "dd_loop.h" // DD_LatestRunTicsStartTime() using namespace de; DENG2_PIMPL_NOREF(InputDeviceAxisControl) { Type type = Pointer; dint flags = 0; ddouble position = 0; ///< Current translated position (-1..1) including any filtering. ddouble realPosition = 0; ///< The actual latest position (-1..1). dfloat scale = 1; ///< Scaling factor for real input values. dfloat deadZone = 0; ///< Dead zone in (0..1) range. ddouble sharpPosition = 0; ///< Current sharp (accumulated) position, entered into the Smoother. Smoother *smoother = nullptr; ///< Smoother for the input values (owned). ddouble prevSmoothPos = 0; ///< Previous evaluated smooth position (needed for producing deltas). duint time = 0; ///< Timestamp of the last position update. Instance() { Smoother_SetMaximumPastNowDelta(smoother = Smoother_New(), 2 * SECONDSPERTIC); } ~Instance() { Smoother_Delete(smoother); } #if 0 static float filter(int grade, float *accumulation, float ticLength) { DENG2_ASSERT(accumulation); int dir = de::sign(*accumulation); float avail = fabs(*accumulation); // Determine the target velocity. float target = avail * (MAX_AXIS_FILTER - de::clamp(1, grade, 39)); /* // test: clamp if(target < -.7) target = -.7; else if(target > .7) target = .7; else target = 0; */ // Determine the amount of mickeys to send. It depends on the // current mouse velocity, and how much time has passed. float used = target * ticLength; // Don't go past the available motion. if(used > avail) { *accumulation = nullptr; used = avail; } else { if(*accumulation > nullptr) *accumulation -= used; else *accumulation += used; } // This is the new (filtered) axis position. return dir * used; } #endif }; InputDeviceAxisControl::InputDeviceAxisControl(String const &name, Type type) : d(new Instance) { setName(name); d->type = type; } InputDeviceAxisControl::~InputDeviceAxisControl() {} InputDeviceAxisControl::Type InputDeviceAxisControl::type() const { return d->type; } void InputDeviceAxisControl::setRawInput(bool yes) { if(yes) d->flags |= IDA_RAW; else d->flags &= ~IDA_RAW; } bool InputDeviceAxisControl::isActive() const { return (d->flags & IDA_DISABLED) == 0; } bool InputDeviceAxisControl::isInverted() const { return (d->flags & IDA_INVERT) != 0; } void InputDeviceAxisControl::update(timespan_t ticLength) { Smoother_Advance(d->smoother, ticLength); if(d->type == Stick) { if(d->flags & IDA_RAW) { // The axis is supposed to be unfiltered. d->position = d->realPosition; } else { // Absolute positions are straightforward to evaluate. Smoother_EvaluateComponent(d->smoother, 0, &d->position); } } else if(d->type == Pointer) { if(d->flags & IDA_RAW) { // The axis is supposed to be unfiltered. d->position += d->realPosition; d->realPosition = 0; } else { // Apply smoothing by converting back into a delta. coord_t smoothPos = d->prevSmoothPos; Smoother_EvaluateComponent(d->smoother, 0, &smoothPos); d->position += smoothPos - d->prevSmoothPos; d->prevSmoothPos = smoothPos; } } // We can clear the expiration now that an updated value is available. setBindContextAssociation(Expired, UnsetFlags); } ddouble InputDeviceAxisControl::position() const { return d->position; } void InputDeviceAxisControl::setPosition(ddouble newPosition) { d->position = newPosition; } void InputDeviceAxisControl::applyRealPosition(dfloat pos) { dfloat const oldRealPos = d->realPosition; dfloat const transformed = translateRealPosition(pos); // The unfiltered position. d->realPosition = transformed; if(oldRealPos != d->realPosition) { // Mark down the time of the change. d->time = DD_LatestRunTicsStartTime(); } if(d->type == Stick) { d->sharpPosition = d->realPosition; } else // Cumulative. { // Convert the delta to an absolute position for smoothing. d->sharpPosition += d->realPosition; } Smoother_AddPosXY(d->smoother, DD_LatestRunTicsStartTime(), d->sharpPosition, 0); } dfloat InputDeviceAxisControl::translateRealPosition(dfloat realPos) const { // An inactive axis is always zero. if(!isActive()) return 0; // Apply scaling, deadzone and clamping. float outPos = realPos * d->scale; if(d->type == Stick) // Only stick axes are dead-zoned and clamped. { if(fabs(outPos) <= d->deadZone) { outPos = 0; } else { outPos -= d->deadZone * de::sign(outPos); // Remove the dead zone. outPos *= 1.0f/(1.0f - d->deadZone); // Normalize. outPos = de::clamp(-1.0f, outPos, 1.0f); } } if(isInverted()) { // Invert the axis position. outPos = -outPos; } return outPos; } dfloat InputDeviceAxisControl::deadZone() const { return d->deadZone; } void InputDeviceAxisControl::setDeadZone(dfloat newDeadZone) { d->deadZone = newDeadZone; } dfloat InputDeviceAxisControl::scale() const { return d->scale; } void InputDeviceAxisControl::setScale(dfloat newScale) { d->scale = newScale; } duint InputDeviceAxisControl::time() const { return d->time; } String InputDeviceAxisControl::description() const { QStringList flags; if(!isActive()) flags << "disabled"; if(isInverted()) flags << "inverted"; String flagsString; if(!flags.isEmpty()) { String flagsAsText = flags.join("|"); flagsString = String(_E(l) " Flags :" _E(.)_E(i) "%1" _E(.)).arg(flagsAsText); } return String(_E(b) "%1 " _E(.) "(Axis-%2)" //_E(l) " Filter: " _E(.)_E(i) "%3" _E(.) _E(l) " Dead Zone: " _E(.)_E(i) "%3" _E(.) _E(l) " Scale: " _E(.)_E(i) "%4" _E(.) "%5") .arg(fullName()) .arg(d->type == Stick? "Stick" : "Pointer") //.arg(d->filter) .arg(d->deadZone) .arg(d->scale) .arg(flagsString); } bool InputDeviceAxisControl::inDefaultState() const { return d->position == 0; // Centered? } void InputDeviceAxisControl::reset() { if(d->type == Pointer) { // Clear the accumulation. d->position = 0; d->sharpPosition = 0; d->prevSmoothPos = 0; } Smoother_Clear(d->smoother); } void InputDeviceAxisControl::consoleRegister() { DENG2_ASSERT(hasDevice() && !name().isEmpty()); String controlName = String("input-%1-%2").arg(device().name()).arg(name()); Block scale = (controlName + "-scale").toUtf8(); C_VAR_FLOAT(scale.constData(), &d->scale, CVF_NO_MAX, 0, 0); Block flags = (controlName + "-flags").toUtf8(); C_VAR_INT(flags.constData(), &d->flags, 0, 0, 7); if(d->type == Stick) { Block deadzone = (controlName + "-deadzone").toUtf8(); C_VAR_FLOAT(deadzone.constData(), &d->deadZone, 0, 0, 1); } } doomsday-stable-1.15.7/doomsday/client/src/ui/binding.cpp0000664000175000017500000000645212641367670022622 0ustar jaakkojaakko/** @file binding.h Base class for binding record accessors. * * @authors Copyright © 2009-2014 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/binding.h" #include #include "ui/b_util.h" // B_EqualConditions using namespace de; static int idCounter = 0; Record &Binding::def() { return const_cast(accessedRecord()); } Record const &Binding::def() const { return accessedRecord(); } Binding::operator bool() const { return accessedRecordPtr() != 0; } void Binding::resetToDefaults() { def().addNumber("id", 0); ///< Unique identifier. def().addArray("condition", new ArrayValue); } Record &Binding::addCondition() { Record *cond = new Record; cond->addNumber("type", Invalid); cond->addNumber("test", None); cond->addNumber("device", -1); cond->addNumber("id", -1); cond->addNumber("pos", 0); cond->addBoolean("negate", false); cond->addBoolean("multiplayer", false); def()["condition"].value() .add(new RecordValue(cond, RecordValue::OwnsRecord)); return *cond; } int Binding::conditionCount() const { return int(geta("condition").size()); } bool Binding::hasCondition(int index) const { return index >= 0 && index < conditionCount(); } Record &Binding::condition(int index) { return *def().geta("condition")[index].as().record(); } Record const &Binding::condition(int index) const { return *geta("condition")[index].as().record(); } bool Binding::equalConditions(Binding const &other) const { // Quick test (assumes there are no duplicated conditions). if(def()["condition"].value().elements().count() != other.geta("condition").elements().count()) { return false; } ArrayValue const &conds = def().geta("condition"); DENG2_FOR_EACH_CONST(ArrayValue::Elements, i, conds.elements()) { Record const &a = *(*i)->as().record(); bool found = false; ArrayValue const &conds2 = other.geta("condition"); DENG2_FOR_EACH_CONST(ArrayValue::Elements, i, conds2.elements()) { Record const &b = *(*i)->as().record(); if(B_EqualConditions(a, b)) { found = true; break; } } if(!found) return false; } return true; } void Binding::resetIdentifiers() // static { idCounter = 0; } int Binding::newIdentifier() // static { int id = 0; while(!id) { id = ++idCounter; } return id; } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/0000775000175000017500000000000012641367670022143 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/ui/widgets/inputbindingwidget.cpp0000664000175000017500000001713312641367670026552 0ustar jaakkojaakko/** @file inputbindingwidget.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/inputbindingwidget.h" #include #include #include "clientapp.h" #include "BindContext" #include "CommandBinding" // #include "ImpulseBinding" #include "ui/b_util.h" using namespace de; #ifdef MACOSX # define CONTROL_MOD KeyEvent::Meta # define CONTROL_CHAR DENG2_CHAR_MAC_CONTROL_KEY #else # define CONTROL_MOD KeyEvent::Control # define CONTROL_CHAR DENG2_CHAR_CONTROL_KEY #endif static inline InputSystem &inputSys() { return ClientApp::inputSystem(); } DENG_GUI_PIMPL(InputBindingWidget) , DENG2_OBSERVES(ButtonWidget, Press) { String defaultEvent; String command; QStringList contexts; int device = IDEV_KEYBOARD; bool useModifiers = false; Instance(Public *i) : Base(i) { //self.setTextLineAlignment(ui::AlignLeft); self.setSizePolicy(ui::Fixed, ui::Expand); self.auxiliary().setText(_E(l) + tr("Reset")); self.audienceForPress() += this; self.auxiliary().audienceForPress() += this; } String prettyKey(String const &eventDesc) { if(!eventDesc.startsWith("key-")) { // Doesn't look like a key. return eventDesc; } String name = eventDesc.substr(Rangei(4, eventDesc.indexOf("-", 4))); name = name.left(1).toUpper() + name.mid(1).toLower(); // Any modifiers? int idx = eventDesc.indexOf("+"); if(idx > 0) { String const conds = eventDesc.mid(idx + 1); if(conds.contains("key-alt-down")) { name = String(DENG2_CHAR_ALT_KEY) + name; } if(conds.contains("key-ctrl-down") || conds.contains("key-control-down")) { name = String(CONTROL_CHAR) + name; } if(conds.contains("key-shift-down")) { name = String(DENG2_CHAR_SHIFT_KEY) + name; } } return name; } /// Checks the current binding and updates the label to show which event/input /// is bound. void updateLabel() { String text = _E(l) + tr("(not bound)"); // Check all the contexts associated with this widget. foreach(QString bcName, contexts) { if(!inputSys().hasContext(bcName)) continue; BindContext const &context = inputSys().context(bcName); if(Record const *rec = context.findCommandBinding(command.toLatin1(), device)) { // This'll do. CommandBinding bind(*rec); text = prettyKey(bind.composeDescriptor()); break; } } self.setText(_E(b) + text); } void bindCommand(String const &eventDesc) { Block const cmd = command.toLatin1(); inputSys().forAllContexts([&cmd] (BindContext &context) { while(Record *bind = context.findCommandBinding(cmd.constData())) { context.deleteBinding(bind->geti("id")); } return LoopContinue; }); foreach(QString bcName, contexts) { String ev = String("%1:%2").arg(bcName, eventDesc); inputSys().bindCommand(ev.toLatin1(), command.toLatin1()); } } void buttonPressed(ButtonWidget &button) { if(&button == thisPublic) { if(!self.hasFocus()) { focus(); } else { unfocus(); } } else { // The reset button. bindCommand(defaultEvent); updateLabel(); } } void focus() { root().setFocus(thisPublic); self.auxiliary().disable(); self.invertStyle(); } void unfocus() { root().setFocus(0); self.auxiliary().enable(); self.invertStyle(); } }; InputBindingWidget::InputBindingWidget() : d(new Instance(this)) { auxiliary().hide(); } void InputBindingWidget::setDefaultBinding(String const &eventDesc) { d->defaultEvent = eventDesc; auxiliary().show(); } void InputBindingWidget::setCommand(String const &command) { d->command = command; d->updateLabel(); } void InputBindingWidget::enableModifiers(bool mods) { d->useModifiers = mods; } void InputBindingWidget::setContexts(QStringList const &contexts) { d->contexts = contexts; d->updateLabel(); } bool InputBindingWidget::handleEvent(Event const &event) { if(hasFocus()) { if(KeyEvent const *key = event.maybeAs()) { if(key->state() != KeyEvent::Pressed) return false; // Include modifier keys if they will be included in the binding. if(d->useModifiers && key->isModifier()) { return false; } if(key->ddKey() == DDKEY_ESCAPE) { d->unfocus(); return true; } ddevent_t ev; InputSystem::convertEvent(event, ev); String desc = B_EventToString(ev); // Apply current modifiers as conditions. if(d->useModifiers) { if(key->modifiers().testFlag(KeyEvent::Shift)) { desc += " + key-shift-down"; } else { desc += " + key-shift-up"; } if(key->modifiers().testFlag(KeyEvent::Alt)) { desc += " + key-alt-down"; } else { desc += " + key-alt-up"; } if(key->modifiers().testFlag(CONTROL_MOD)) { desc += " + key-ctrl-down"; } else { desc += " + key-ctrl-up"; } } d->bindCommand(desc); d->updateLabel(); d->unfocus(); return true; } if(MouseEvent const *mouse = event.maybeAs()) { if(mouse->type() == Event::MouseButton && mouse->state() == MouseEvent::Released && !hitTest(event)) { // Clicking outside clears focus. d->unfocus(); return true; } } } return AuxButtonWidget::handleEvent(event); } InputBindingWidget *InputBindingWidget::newTaskBarShortcut() { InputBindingWidget *bind = new InputBindingWidget; bind->setCommand("taskbar"); bind->setDefaultBinding("key-tilde-down + key-shift-up"); bind->enableModifiers(true); bind->setContexts(QStringList() << "global" << "console"); return bind; } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/cvarnativepathwidget.cpp0000664000175000017500000000757712641367670027112 0ustar jaakkojaakko/** @file cvarnativepathwidget.cpp Console variable with a native path. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * LGPL: http://www.gnu.org/licenses/lgpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or (at your * option) any later version. 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 Lesser * General Public License for more details. You should have received a copy of * the GNU Lesser General Public License along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/cvarnativepathwidget.h" #include "ui/clientwindow.h" #include "clientapp.h" #include #include #include #include using namespace de; DENG2_PIMPL_NOREF(CVarNativePathWidget) { char const *cvar; NativePath path; QStringList filters; PopupMenuWidget *menu; String blankText = "(not set)"; cvar_t *var() const { cvar_t *cv = Con_FindVariable(cvar); DENG2_ASSERT(cv != 0); return cv; } String labelText() const { if(path.isEmpty()) { return String(_E(l)) + blankText + _E(.); } return path.fileName(); } }; CVarNativePathWidget::CVarNativePathWidget(char const *cvarPath) : d(new Instance) { add(d->menu = new PopupMenuWidget); d->menu->setAnchorAndOpeningDirection(rule(), ui::Up); d->menu->items() << new ui::ActionItem(tr("Browse..."), new SignalAction(this, SLOT(chooseUsingNativeFileDialog()))) << new ui::ActionItem(style().images().image("close.ring"), tr("Clear"), new SignalAction(this, SLOT(clearPath()))); d->cvar = cvarPath; updateFromCVar(); auxiliary().setText(tr("Browse")); connect(&auxiliary(), SIGNAL(pressed()), this, SLOT(chooseUsingNativeFileDialog())); connect(this, SIGNAL(pressed()), this, SLOT(showActionsPopup())); } void CVarNativePathWidget::setFilters(StringList const &filters) { d->filters.clear(); for(auto const &f : filters) { d->filters << f; } } void CVarNativePathWidget::setBlankText(String const &text) { d->blankText = text; } char const *CVarNativePathWidget::cvarPath() const { return d->cvar; } void CVarNativePathWidget::updateFromCVar() { d->path = CVar_String(d->var()); setText(d->labelText()); } void CVarNativePathWidget::chooseUsingNativeFileDialog() { ClientApp::app().beginNativeUIMode(); // Use a native dialog to pick the path. QDir dir(d->path); if(d->path.isEmpty()) dir = QDir::home(); QFileDialog dlg(&ClientWindow::main(), tr("Select File for \"%1\"").arg(d->cvar), dir.absolutePath()); if(!d->filters.isEmpty()) { dlg.setNameFilters(d->filters); } dlg.setFileMode(QFileDialog::ExistingFile); dlg.setOption(QFileDialog::ReadOnly, true); dlg.setLabelText(QFileDialog::Accept, tr("Select")); if(dlg.exec()) { d->path = dlg.selectedFiles().at(0); setCVarValueFromWidget(); setText(d->labelText()); } ClientApp::app().endNativeUIMode(); } void CVarNativePathWidget::clearPath() { d->path.clear(); setCVarValueFromWidget(); setText(d->labelText()); } void CVarNativePathWidget::showActionsPopup() { if(!d->menu->isOpen()) { d->menu->open(); } else { d->menu->close(0); } } void CVarNativePathWidget::setCVarValueFromWidget() { CVar_SetString(d->var(), d->path.toString().toUtf8()); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/savedsessionmenuwidget.cpp0000664000175000017500000002453712641367670027461 0ustar jaakkojaakko/** @file savedsessionmenuwidget.cpp * * @authors Copyright © 2014 Jaakko Keränen * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/savedsessionmenuwidget.h" #include "ui/widgets/taskbarwidget.h" #include "ui/widgets/gamesessionwidget.h" #include "clientapp.h" #include "ui/clientwindow.h" #include "dd_main.h" #include #include #include #include #include #include using namespace de; using de::game::Session; using de::game::SavedSession; DENG_GUI_PIMPL(SavedSessionMenuWidget) , DENG2_OBSERVES(Games, Readiness) , DENG2_OBSERVES(Session::SavedIndex, AvailabilityUpdate) , DENG2_OBSERVES(Loop, Iteration) // deferred refresh { /** * Action for loading a saved session. */ class LoadAction : public de::Action { String gameId; String cmd; public: LoadAction(SavedSession const &session) { gameId = session.metadata().gets("gameIdentityKey"); cmd = "loadgame " + session.name().fileNameWithoutExtension() + " confirm"; } void trigger() { Action::trigger(); BusyMode_FreezeGameForBusyMode(); ClientWindow::main().taskBar().close(); App_ChangeGame(App_Games().byIdentityKey(gameId), false /*no reload*/); Con_Execute(CMDS_DDAY, cmd.toLatin1(), false, false); } }; /** * Data item with information about a saved game session. */ class SavegameListItem : public ui::Item, public SessionItem { public: SavegameListItem(SavedSession const &session, SessionMenuWidget &owner) : SessionItem(owner) { setData(session.path().toLower()); _session = &session; } SavedSession const &savedSession() const { DENG2_ASSERT(_session != 0); return *_session; } String title() const { return savedSession().metadata().gets("userDescription"); } String gameIdentityKey() const { return savedSession().metadata().gets("gameIdentityKey"); } private: SavedSession const *_session; }; /** * Widget representing a SavegameItem in the dialog's menu. */ struct SavegameWidget : public GameSessionWidget { /// Action that deletes a savegame folder. struct DeleteAction : public Action { SavegameWidget *widget; String savePath; DeleteAction(SavegameWidget *wgt, String const &path) : widget(wgt), savePath(path) {} void trigger() { widget->menu().close(0); App::rootFolder().removeFile(savePath); App::fileSystem().refresh(); } }; SavegameWidget() : GameSessionWidget(PopupMenu, ui::Up) { loadButton().disable(); // Show all available information without clipping. loadButton().setHeightPolicy(ui::Expand); } void updateFromItem(SavegameListItem const &item) { try { SavedSession const &session = item.savedSession(); Game const &sGame = App_Games().byIdentityKey(item.gameIdentityKey()); if(style().images().has(sGame.logoImageId())) { loadButton().setImage(style().images().image(sGame.logoImageId())); } loadButton().disable(sGame.status() == Game::Incomplete); loadButton().setText(String("%2\n" _E(l)_E(F) "%3 %1") .arg(sGame.identityKey()) .arg(item.title()) .arg(tr("saved in"))); menuButton().setImage(style().images().image("close.ringless")); menuButton().setImageScale(toDevicePixels(.375f)); menuButton().setImageColor(style().colors().colorf("altaccent")); // Metadata. document().setText(session.metadata().asStyledText() + "\n" + String(_E(D)_E(b) "Resource: " _E(.)_E(.)_E(C) "\"%1\"\n" _E(.) _E(l) "Modified: " _E(.) "%2") .arg(session.path()) .arg(session.status().modifiedAt.asText(Time::FriendlyFormat))); // Actions to be done on the saved game. menu().items() << new ui::Item(ui::Item::Separator, tr("Really delete the savegame?")) << new ui::ActionItem(tr("Delete Savegame"), new DeleteAction(this, session.path())) << new ui::ActionItem(tr("Cancel"), new SignalAction(&menu(), SLOT(close()))); } catch(Error const &) { /// @todo } } }; Instance(Public *i) : Base(i) { App_Games().audienceForReadiness() += this; game::Session::savedIndex().audienceForAvailabilityUpdate() += this; } ~Instance() { Loop::get().audienceForIteration() -= this; App_Games().audienceForReadiness() -= this; game::Session::savedIndex().audienceForAvailabilityUpdate() -= this; } void gameReadinessUpdated() { // Startup resources for all games have been located. // We can now determine which of the saved sessions are loadable. for(ui::Data::Pos idx = 0; idx < self.items().size(); ++idx) { ui::Item const &item = self.items().at(idx); self.updateItemWidget(*self.organizer().itemWidget(item), item); } emit self.availabilityChanged(); } QSet pendingDismiss; void updateItemsFromSavedIndex() { bool changed = false; // Remove obsolete entries. for(ui::Data::Pos idx = 0; idx < self.items().size(); ++idx) { String const savePath = self.items().at(idx).data().toString(); if(!Session::savedIndex().find(savePath)) { if(!pendingDismiss.contains(savePath)) { // Make this item disappear after a delay. pendingDismiss.insert(savePath); auto &wgt = self.itemWidget(self.items().at(idx)); wgt.setBehavior(DisableEventDispatch); // can't trigger any more wgt.setOpacity(0, .5f); } } } // Add new entries. DENG2_FOR_EACH_CONST(Session::SavedIndex::All, i, Session::savedIndex().all()) { ui::Data::Pos found = self.items().findData(i.key()); if(found == ui::Data::InvalidPos) { SavedSession &session = *i.value(); if(session.path().beginsWith("/home/savegames")) // Ignore non-user savegames. { // Needs to be added. self.items().append(new SavegameListItem(session, self)); changed = true; } } } if(changed) { // Let others know that one or more games have appeared or disappeared from the menu. emit self.availabilityChanged(); } } void removeDismissedItems() { if(pendingDismiss.isEmpty()) return; bool changed = false; QMutableSetIterator iter(pendingDismiss); while(iter.hasNext()) { String const path = iter.next(); ui::DataPos idx = self.items().findData(path); if(idx == ui::Data::InvalidPos) { iter.remove(); // It's already gone? continue; } auto &wgt = self.itemWidget(self.items().at(idx)); if(fequal(wgt.opacity(), 0.f)) { // Time to erase this item. self.items().remove(idx); changed = true; } } if(changed) { // Let others know that one or more games have appeared or disappeared from the menu. emit self.availabilityChanged(); } } void savedIndexAvailabilityUpdate(Session::SavedIndex const &) { if(!App::inMainThread()) { // We'll have to defer the update for now. deferUpdate(); return; } updateItemsFromSavedIndex(); } void deferUpdate() { Loop::get().audienceForIteration() += this; } void loopIteration() { Loop::get().audienceForIteration() -= this; updateItemsFromSavedIndex(); } }; SavedSessionMenuWidget::SavedSessionMenuWidget() : SessionMenuWidget("savegame-session-menu"), d(new Instance(this)) { d->updateItemsFromSavedIndex(); } void SavedSessionMenuWidget::update() { SessionMenuWidget::update(); d->removeDismissedItems(); } Action *SavedSessionMenuWidget::makeAction(ui::Item const &item) { return new Instance::LoadAction(item.as().savedSession()); } GuiWidget *SavedSessionMenuWidget::makeItemWidget(ui::Item const &, GuiWidget const *) { return new Instance::SavegameWidget; } void SavedSessionMenuWidget::updateItemWidget(GuiWidget &widget, ui::Item const &item) { Instance::SavegameWidget &w = widget.as(); w.updateFromItem(item.as()); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/cvartogglewidget.cpp0000664000175000017500000000330212641367670026206 0ustar jaakkojaakko/** @file cvartogglewidget.cpp Console variable toggle. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/cvartogglewidget.h" #include using namespace de; using namespace ui; DENG2_PIMPL_NOREF(CVarToggleWidget) { char const *cvar; cvar_t *var() const { cvar_t *cv = Con_FindVariable(cvar); DENG2_ASSERT(cv != 0); return cv; } }; CVarToggleWidget::CVarToggleWidget(char const *cvarPath, String const &labelText) : d(new Instance) { setText(labelText); d->cvar = cvarPath; updateFromCVar(); connect(this, SIGNAL(stateChangedByUser(ToggleWidget::ToggleState)), this, SLOT(setCVarValueFromWidget())); } char const *CVarToggleWidget::cvarPath() const { return d->cvar; } void CVarToggleWidget::updateFromCVar() { setActive(CVar_Integer(d->var()) != 0); } void CVarToggleWidget::setCVarValueFromWidget() { CVar_SetInteger(d->var(), isActive()? 1 : 0); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/cvarlineeditwidget.cpp0000664000175000017500000000354012641367670026526 0ustar jaakkojaakko/** @file cvarlineeditwidget.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/cvarlineeditwidget.h" #include using namespace de; using namespace ui; DENG2_PIMPL_NOREF(CVarLineEditWidget) { char const *cvar; cvar_t *var() const { cvar_t *cv = Con_FindVariable(cvar); DENG2_ASSERT(cv != 0); return cv; } }; CVarLineEditWidget::CVarLineEditWidget(char const *cvarPath) : d(new Instance) { setSignalOnEnter(true); connect(this, SIGNAL(enterPressed(QString)), this, SLOT(endEditing())); d->cvar = cvarPath; updateFromCVar(); } void CVarLineEditWidget::contentChanged() { LineEditWidget::contentChanged(); if(String(CVar_String(d->var())) != text()) { setCVarValueFromWidget(); } } char const *CVarLineEditWidget::cvarPath() const { return d->cvar; } void CVarLineEditWidget::updateFromCVar() { setText(CVar_String(d->var())); } void CVarLineEditWidget::endEditing() { root().setFocus(0); } void CVarLineEditWidget::setCVarValueFromWidget() { CVar_SetString(d->var(), text().toUtf8()); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/gameselectionwidget.cpp0000664000175000017500000003422412641367670026677 0ustar jaakkojaakko/** @file gameselectionwidget.cpp * * @authors Copyright © 2013-2014 Jaakko Keränen * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/gameselectionwidget.h" #include "ui/widgets/gamesessionwidget.h" #include "ui/widgets/gamefilterwidget.h" #include "ui/widgets/singleplayersessionmenuwidget.h" #include "ui/widgets/mpsessionmenuwidget.h" #include "ui/widgets/savedsessionmenuwidget.h" #include "CommandAction" #include "clientapp.h" #include "games.h" #include "dd_main.h" #include #include #include #include #include #include #include "CommandAction" using namespace de; DENG_GUI_PIMPL(GameSelectionWidget) , DENG2_OBSERVES(App, GameChange) { /** * Foldable group of games. */ struct SubsetWidget : public FoldPanelWidget , DENG2_OBSERVES(ui::Data, Addition) { enum Type { AvailableGames, IncompleteGames, MultiplayerGames, SavedGames }; String titleText; Type type; SessionMenuWidget *menu; LabelWidget *noGames; int numCols; SubsetWidget(String const &widgetName, Type selType, String const &headingText, GameSelectionWidget::Instance *owner) : FoldPanelWidget(widgetName) , titleText(headingText) , type(selType) , numCols(3) { owner->self.add(makeTitle(headingText)); title().setFont("title"); title().setTextColor("inverted.text"); title().setHoverTextColor("inverted.text", ButtonWidget::ReplaceColor); title().margins().setLeft("").setRight(""); switch(type) { case AvailableGames: menu = new SingleplayerSessionMenuWidget(SingleplayerSessionMenuWidget::ShowAvailableGames); break; case IncompleteGames: menu = new SingleplayerSessionMenuWidget(SingleplayerSessionMenuWidget::ShowGamesWithMissingResources); break; case MultiplayerGames: menu = new MPSessionMenuWidget(MPSessionMenuWidget::DiscoverUsingMaster); break; case SavedGames: menu = new SavedSessionMenuWidget; break; } QObject::connect(menu, SIGNAL(sessionSelected(de::ui::Item const *)), &owner->self, SLOT (select(de::ui::Item const *))); QObject::connect(menu, SIGNAL(availabilityChanged()), &owner->self, SLOT (updateSubsetLayout())); menu->items().audienceForAddition() += this; setContent(menu); menu->margins().set(""); menu->layout().setColumnPadding(owner->style().rules().rule("unit")); menu->rule().setInput(Rule::Width, owner->self.rule().width() - owner->self.margins().width()); setColumns(3); // This will be shown if there are no games in the subset. // Note that this label is one of the menu's children, too, // in addition to the selectable sessions. noGames = LabelWidget::newWithText(_E(b) + tr("No games"), menu); noGames->margins() .setTop (style().rules().rule("gap") * 2) .setBottom(noGames->margins().top()); noGames->setFont("heading"); noGames->setTextColor("inverted.text"); noGames->setOpacity(.4f); noGames->hide(); } void dataItemAdded(ui::Data::Pos, ui::Item const &) { // Time to get rid of the notice. noGames->hide(); } void setColumns(int cols) { numCols = cols; // However, if the subset is empty, just use a single column for noGames. if(items().isEmpty()) { cols = 1; } menu->setColumns(cols); } ui::Data &items() { return menu->items(); } String textForTitle(bool whenOpen) const { if(whenOpen) return titleText; int count = menu->count(); if(!noGames->behavior().testFlag(Widget::Hidden)) count = 0; return QString("%1 (%2)").arg(titleText).arg(count); } void preparePanelForOpening() { FoldPanelWidget::preparePanelForOpening(); title().setText(textForTitle(true)); } void panelClosing() { FoldPanelWidget::panelClosing(); title().setText(textForTitle(false)); } void updateTitleText() { title().setText(textForTitle(isOpen())); } void setTitleColor(DotPath const &colorId, DotPath const &hoverColorId, ButtonWidget::HoverColorMode mode) { title().setTextColor(colorId); title().setHoverTextColor(hoverColorId, mode); noGames->setTextColor(colorId); } }; SequentialLayout superLayout; GameFilterWidget *filter; SubsetWidget *available; SubsetWidget *incomplete; SubsetWidget *multi; SubsetWidget *saved; QList subsets; // not owned bool doAction; Instance(Public *i) : Base(i) , superLayout(i->contentRule().left(), i->contentRule().top(), ui::Down) , doAction(false) { // Menu of available games. self.add(available = new SubsetWidget("available", SubsetWidget::AvailableGames, App_GameLoaded()? tr("Switch Game") : tr("Available Games"), this)); // Menu of incomplete games. self.add(incomplete = new SubsetWidget("incomplete", SubsetWidget::IncompleteGames, tr("Games with Missing Resources"), this)); // Menu of multiplayer games. self.add(multi = new SubsetWidget("multi", SubsetWidget::MultiplayerGames, tr("Multiplayer Games"), this)); // Menu of saved games. self.add(saved = new SubsetWidget("saved", SubsetWidget::SavedGames, tr("Saved Games"), this)); // Keep all sets in a handy list. subsets << available << incomplete << multi << saved; self.add(filter = new GameFilterWidget); foreach(SubsetWidget *sub, subsets) sub->menu->setFilter(filter); superLayout.setOverrideWidth(self.rule().width() - self.margins().width()); updateSubsetLayout(); App::app().audienceForGameChange() += this; } ~Instance() { foreach(SubsetWidget *sub, subsets) sub->menu->setFilter(0); App::app().audienceForGameChange() -= this; } /** * Determines if a subset should be visible according to the current filter value. */ bool isSubsetVisible(SubsetWidget const *sub) const { GameFilterWidget::Filter flt = filter->filter(); if(sub == available || sub == incomplete || sub == saved) { return flt.testFlag(GameFilterWidget::Singleplayer); } if(sub == multi) { return flt.testFlag(GameFilterWidget::Multiplayer); } return false; } void updateSubsetVisibility() { foreach(SubsetWidget *sub, subsets) { bool shown = isSubsetVisible(sub); sub->show(shown); sub->title().show(shown); } } /** * Subsets are visible only when they have games in them. The title and content * of a subset are hidden when empty. */ void updateSubsetLayout() { superLayout.clear(); QList order; if(!App_GameLoaded()) { order << available << multi << saved << incomplete; } else { order << multi << available << saved << incomplete; } updateSubsetVisibility(); // Filter out the requested subsets. GameFilterWidget::Filter flt = filter->filter(); if(!flt.testFlag(GameFilterWidget::Singleplayer)) { order.removeOne(available); order.removeOne(incomplete); order.removeOne(saved); } if(!flt.testFlag(GameFilterWidget::Multiplayer)) { order.removeOne(multi); } foreach(SubsetWidget *s, order) { s->updateTitleText(); superLayout << s->title() << *s; // Show a notice when there are no games in the group. if(s->items().isEmpty()) { // Go to one-column layout for the "no games" indicator. s->menu->setGridSize(1, ui::Filled, 1, ui::Expand); s->noGames->show(); } else { // Restore the correct number of columns. s->setColumns(s->numCols); s->noGames->hide(); } } self.setContentSize(superLayout.width(), superLayout.height()); } void currentGameChanged(game::Game const &) { updateSubsetLayout(); } void updateLayoutForWidth(int width) { // If the view is too small, we'll want to reduce the number of items in the menu. int const maxWidth = style().rules().rule("gameselection.max.width").valuei(); int const suitable = clamp(1, 4 * width / maxWidth, 3); foreach(SubsetWidget *s, subsets) { s->setColumns(suitable); } } }; GameSelectionWidget::GameSelectionWidget(String const &name) : ScrollAreaWidget(name), d(new Instance(this)) { enableIndicatorDraw(true); setScrollBarColor("inverted.accent"); // By default attach the filter above the widget. d->filter->rule() .setInput(Rule::Width, rule().width()) .setInput(Rule::Bottom, rule().top()) .setInput(Rule::Left, rule().left()); // Default open/closed folds. d->multi->open(); if(!App_GameLoaded()) { d->available->open(); //d->incomplete->open(); } // We want the full menu to be visible even when it doesn't fit the // designated area. unsetBehavior(ChildVisibilityClipping); unsetBehavior(ChildHitClipping); connect(d->filter, SIGNAL(filterChanged()), this, SLOT(updateSubsetLayout())); } void GameSelectionWidget::setTitleColor(DotPath const &colorId, DotPath const &hoverColorId, ButtonWidget::HoverColorMode mode) { foreach(Instance::SubsetWidget *s, d->subsets) { s->setTitleColor(colorId, hoverColorId, mode); } } void GameSelectionWidget::setTitleFont(DotPath const &fontId) { foreach(Instance::SubsetWidget *s, d->subsets) { s->title().setFont(fontId); } } GameFilterWidget &GameSelectionWidget::filter() { return *d->filter; } FoldPanelWidget *GameSelectionWidget::subsetFold(String const &name) { return find(name)->maybeAs(); } void GameSelectionWidget::enableActionOnSelection(bool doAction) { d->doAction = doAction; } Action *GameSelectionWidget::makeAction(ui::Item const &item) const { // Find the session menu that owns this item. foreach(Instance::SubsetWidget *sub, d->subsets) { ui::DataPos const pos = sub->menu->items().find(item); if(pos != ui::Data::InvalidPos) { return sub->menu->makeAction(item); } } DENG2_ASSERT(!"GameSelectionWidget: Item does not belong in any subset"); return 0; } void GameSelectionWidget::update() { ScrollAreaWidget::update(); // Adapt grid layout for the widget width. Rectanglei rect; if(hasChangedPlace(rect)) { d->updateLayoutForWidth(rect.width()); } } void GameSelectionWidget::operator >> (PersistentState &toState) const { Record &st = toState.names(); foreach(Instance::SubsetWidget *s, d->subsets) { // Save the fold open/closed state. st.set(name() + "." + s->name() + ".open", s->isOpen()); } } void GameSelectionWidget::operator << (PersistentState const &fromState) { Record const &st = fromState.names(); foreach(Instance::SubsetWidget *s, d->subsets) { // Restore the fold open/closed state. if(st[name() + "." + s->name() + ".open"].value().isTrue()) { s->open(); // automatically shows the panel } else { s->close(0); // instantly } } // Ensure subsets that are supposed to be hidden stay that way. d->updateSubsetVisibility(); } void GameSelectionWidget::updateSubsetLayout() { d->updateSubsetLayout(); } void GameSelectionWidget::select(ui::Item const *item) { if(!item) return; // Should we perform an action afterwards? The signal handling may lead to // destruction of the widget, so we'll hold a ref to the action. AutoRef postAction(d->doAction? makeAction(*item) : nullptr); // Notify. emit gameSessionSelected(item); if(bool(postAction)) { postAction->trigger(); } } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/tutorialwidget.cpp0000664000175000017500000004110112641367670025713 0ustar jaakkojaakko/** @file tutorialwidget.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/tutorialwidget.h" #include "ui/clientwindow.h" #include "ui/widgets/taskbarwidget.h" #include "ui/widgets/inputbindingwidget.h" #include "dd_version.h" #include "dd_main.h" #include #include #include #include #include #include #include using namespace de; static TimeDelta const FLASH_SPAN = 0.6; DENG_GUI_PIMPL(TutorialWidget) { enum Step { Welcome, HomeScreen, Notifications, TaskBar, DEMenu, ConfigMenus, RendererAppearance, ConsoleKey, Finish }; Step current = Welcome; MessageDialog *dlg = nullptr; LabelWidget *highlight = nullptr; NotificationAreaWidget *notifs = nullptr; ///< Fake notifications just for an example. UniqueWidgetPtr exampleAlert; QTimer flashing; bool taskBarInitiallyOpen; Untrapper untrapper; Instance(Public *i) : Base(i) , taskBarInitiallyOpen(ClientWindow::main().taskBar().isOpen()) , untrapper(ClientWindow::main()) { // Create an example alert (lookalike). /// @todo There could be a class for an alert notification widget. -jk exampleAlert.reset(new LabelWidget); exampleAlert->setSizePolicy(ui::Expand, ui::Expand); exampleAlert->setImage(style().images().image("alert")); exampleAlert->setOverrideImageSize(style().fonts().font("default").height().value()); exampleAlert->setImageColor(style().colors().colorf("accent")); // Highlight rectangle. self.add(highlight = new LabelWidget); highlight->set(Background(Background::GradientFrame, style().colors().colorf("accent"), 6)); highlight->setOpacity(0); flashing.setSingleShot(false); flashing.setInterval(FLASH_SPAN.asMilliSeconds()); } void startHighlight(GuiWidget const &w) { highlight->rule().setRect(w.rule()); highlight->setOpacity(0); highlight->show(); flashing.start(); flash(); } /** * Animates the highlight flash rectangle. Called periodically. */ void flash() { if(highlight->opacity().target() == 0) { highlight->setOpacity(.8f, FLASH_SPAN + .1, .1); } else if(highlight->opacity().target() > .5f) { highlight->setOpacity(.2f, FLASH_SPAN); } else { highlight->setOpacity(.8f, FLASH_SPAN); } } void stopHighlight() { highlight->hide(); flashing.stop(); } /** * Counts the total number of steps currently available. */ int stepCount() const { int count = 0; for(Step s = Welcome; s != Finish; s = advanceStep(s)) count++; return count; } int stepOrdinal(Step s) const { int ord = stepCount() - 1; for(; s != Finish; s = advanceStep(s)) ord--; return ord; } /** * Determines which step follows step @a s. * @param s Step. * @return The next step. */ Step advanceStep(Step s) const { s = Instance::Step(s + 1); validateStep(s); return s; } Step previousStep(Step s) const { if(s == Welcome) return s; Step prev = Welcome; while(prev != Finish) { Step following = advanceStep(prev); if(following == s) break; prev = following; } return prev; } /** * Checks if step @a s is valid for the current engine state and if not, * skips to the next valid state. * * @param s Current step. */ void validateStep(Step &s) const { forever { bool skip = false; if(!App_GameLoaded()) // in Ring Zero { if(s == RendererAppearance) skip = true; } else // A game is loaded. { if(s == HomeScreen) skip = true; } if(!skip) break; s = Step(s + 1); } } void initStep(Step s) { deinitStep(); // Jump to the next valid step, if necessary. validateStep(s); if(s == Finish) { self.stop(); return; } current = s; bool const isFirstStep = (current == Welcome); bool const isLastStep = (current == Finish - 1); dlg = new MessageDialog; dlg->useInfoStyle(); dlg->setDeleteAfterDismissed(true); dlg->setClickToClose(false); QObject::connect(dlg, SIGNAL(accepted(int)), thisPublic, SLOT(continueToNextStep())); QObject::connect(dlg, SIGNAL(rejected(int)), thisPublic, SLOT(stop())); dlg->buttons() << new DialogButtonItem(DialogWidget::Accept | DialogWidget::Default | DialogWidget::Id1, isLastStep? tr("Done") : tr("Next")); if(!isFirstStep) { dlg->buttons() << new DialogButtonItem(DialogWidget::Action | DialogWidget::Id2, "", new SignalAction(thisPublic, SLOT(backToPreviousStep()))); auto &prevBtn = *dlg->buttonWidget(DialogWidget::Id2); prevBtn.setImage(new StyleProceduralImage("fold", prevBtn, 90)); prevBtn.setImageColor(style().colors().colorf("inverted.text")); } if(!isLastStep) { dlg->buttons() << new DialogButtonItem(DialogWidget::Reject | DialogWidget::Action, tr("Close")); auto &nextBtn = *dlg->buttonWidget(DialogWidget::Id1); nextBtn.setImage(new StyleProceduralImage("fold", nextBtn, -90)); nextBtn.setImageColor(style().colors().colorf("inverted.text")); nextBtn.setTextAlignment(ui::AlignLeft); } // Insert the content for the dialog. ClientWindow &win = ClientWindow::main(); switch(current) { case Welcome: dlg->title().setText(tr("Welcome to Doomsday")); dlg->message().setText(tr("This tutorial will give you a brief walkthrough of the " "major features of Doomsday's UI. You will also get a " "chance to pick a shortcut key for opening the console.\n\n" "The tutorial can be restarted later via the application menu.")); //.arg(_E(b) DOOMSDAY_NICENAME _E(.))); dlg->setAnchor(self.rule().midX(), self.rule().top()); dlg->setOpeningDirection(ui::Down); break; case HomeScreen: dlg->title().setText(tr("Home Screen")); dlg->message().setText(tr("This is where you end up if no game gets loaded at startup. " "Here you can browse all available games " "and configure engine settings. You can unload the current game at " "any time to get back to the Home Screen.")); startHighlight(*root().guiFind("background")); break; case Notifications: // Fake notification area that doesn't have any the real currently showed // notifications. notifs = new NotificationAreaWidget("tutorial-notifications"); notifs->useDefaultPlacement(ClientWindow::main().game().rule()); root().addOnTop(notifs); notifs->showChild(*exampleAlert); dlg->title().setText(tr("Notifications")); dlg->message().setText(tr("The notification area shows the current notifications. " "For example, this one here is an example of a warning or an error " "that has occurred. You can click on the notification icons to " "get more information.\n\nOther possible notifications include the current " "FPS, ongoing downloads, and available updates.")); dlg->setAnchorAndOpeningDirection(exampleAlert->rule(), ui::Down); startHighlight(*exampleAlert); break; case TaskBar: dlg->title().setText(tr("Task Bar")); dlg->message().setText(tr("The task bar is where you find all the important functionality: loading " "and switching games, joining a multiplayer game, " "configuration settings, " "and a console command line for advanced users.\n\n" "Press %1 to access the task bar at any time.") .arg(_E(b) "Shift-Esc" _E(.))); win.taskBar().open(); win.taskBar().closeMainMenu(); win.taskBar().closeConfigMenu(); dlg->setAnchor(self.rule().midX(), win.taskBar().rule().top()); dlg->setOpeningDirection(ui::Up); startHighlight(win.taskBar()); break; case DEMenu: dlg->title().setText(tr("Application Menu")); dlg->message().setText(tr("Click the DE icon in the bottom right corner to open " "the application menu. " "You can check for available updates, switch games, or look for " "ongoing multiplayer games. You can also unload the current game " "and return to Doomsday's Home Screen.")); win.taskBar().openMainMenu(); dlg->setAnchorAndOpeningDirection(root().guiFind("de-menu")->rule(), ui::Left); startHighlight(*root().guiFind("de-button")); break; case ConfigMenus: dlg->title().setText(tr("Settings")); dlg->message().setText(tr("Configuration menus are found under buttons with a gear icon. " "The task bar's configuration button has the settings for " "all of Doomsday's subsystems.")); win.taskBar().openConfigMenu(); dlg->setAnchorAndOpeningDirection(root().guiFind("conf-menu")->rule(), ui::Left); startHighlight(*root().guiFind("conf-button")); break; case RendererAppearance: dlg->title().setText(tr("Appearance")); dlg->message().setText(tr("By default Doomsday applies many visual " "embellishments to how the game world appears. These " "can be configured individually in the Renderer " "Appearance editor, or you can use one of the built-in " "default profiles: %1, %2, or %3.") .arg(_E(b) "Defaults" _E(.)) .arg(_E(b) "Vanilla" _E(.)) .arg(_E(b) "Amplified" _E(.))); win.taskBar().openConfigMenu(); win.root().guiFind("conf-menu")->as().menu() .organizer().itemWidget(tr("Renderer"))->as().trigger(); dlg->setAnchorAndOpeningDirection( win.root().guiFind("renderersettings")->guiFind("appearance-label")->rule(), ui::Left); startHighlight(*root().guiFind("profile-picker")); break; case ConsoleKey: { dlg->title().setText(tr("Console")); String msg = tr("The console is a \"Quake style\" command line prompt where " "you enter commands and change variable values. To get started, " "try typing %1 in the console.").arg(_E(b) "help" _E(.)); if(App_GameLoaded()) { // Event bindings are currently stored per-game, so we can't set a // binding unless a game is loaded. msg += tr("\n\nBelow you can see the current keyboard shortcut for accessing the console quickly. " "To change it, click in the box and then press the key or key combination you " "want to assign as the shortcut."); InputBindingWidget *bind = InputBindingWidget::newTaskBarShortcut(); bind->invertStyle(); dlg->area().add(bind); } dlg->message().setText(msg); dlg->setAnchor(win.taskBar().console().commandLine().rule().left() + style().rules().rule("gap"), win.taskBar().rule().top()); dlg->setOpeningDirection(ui::Up); dlg->updateLayout(); startHighlight(win.taskBar().console().commandLine()); break; } default: break; } // Progress indication. auto *progress = new ProgressWidget; progress->setColor("inverted.text"); progress->setRange(Rangei(0, stepCount())); progress->setProgress(stepOrdinal(current) + 1, 0); progress->setMode(ProgressWidget::Dots); progress->rule() .setInput(Rule::Top, dlg->buttonsMenu().rule().top()) .setInput(Rule::Bottom, dlg->buttonsMenu().rule().bottom() - dlg->buttonsMenu().margins().bottom()) .setInput(Rule::Left, dlg->rule().left()) .setInput(Rule::Right, dlg->rule().right()); dlg->add(progress); GuiRootWidget &root = self.root(); // Keep the tutorial above any dialogs etc. that might've been opened. root.moveToTop(self); root.addOnTop(dlg); dlg->open(); } /** * Cleans up after a tutorial step is done. */ void deinitStep() { // Get rid of the previous dialog. if(dlg) { dlg->close(0); dlg = 0; } stopHighlight(); ClientWindow &win = ClientWindow::main(); switch(current) { case Notifications: notifs->hideChild(*exampleAlert); QTimer::singleShot(500, notifs, SLOT(guiDeleteLater())); notifs = 0; break; case DEMenu: win.taskBar().closeMainMenu(); break; case ConfigMenus: win.taskBar().closeConfigMenu(); break; case RendererAppearance: win.taskBar().closeConfigMenu(); break; default: break; } } }; TutorialWidget::TutorialWidget() : GuiWidget("tutorial"), d(new Instance(this)) { connect(&d->flashing, SIGNAL(timeout()), this, SLOT(flashHighlight())); } void TutorialWidget::start() { // Blur the rest of the view. ClientWindow::main().fadeInTaskBarBlur(.5); d->initStep(Instance::Welcome); } void TutorialWidget::stop() { if(!d->taskBarInitiallyOpen) { ClientWindow::main().taskBar().close(); } d->deinitStep(); // Animate away and unfade darkening. ClientWindow::main().fadeOutTaskBarBlur(.5); QTimer::singleShot(500, this, SLOT(dismiss())); } void TutorialWidget::dismiss() { hide(); guiDeleteLater(); } void TutorialWidget::flashHighlight() { d->flash(); } bool TutorialWidget::handleEvent(Event const &event) { GuiWidget::handleEvent(event); // Eat everything! return true; } void TutorialWidget::continueToNextStep() { d->initStep(d->advanceStep(d->current)); } void TutorialWidget::backToPreviousStep() { d->initStep(d->previousStep(d->current)); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/mpsessionmenuwidget.cpp0000664000175000017500000002306312641367670026764 0ustar jaakkojaakko/** @file mpsessionmenuwidget.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/mpsessionmenuwidget.h" #include "ui/widgets/taskbarwidget.h" #include "ui/widgets/gamesessionwidget.h" #include "network/serverlink.h" #include "clientapp.h" #include "ui/clientwindow.h" #include "dd_main.h" #include #include #include #include #include #include #include using namespace de; DENG_GUI_PIMPL(MPSessionMenuWidget) , DENG2_OBSERVES(App, GameChange) , DENG2_OBSERVES(Games, Readiness) , DENG2_OBSERVES(ServerLink, DiscoveryUpdate) { static ServerLink &link() { return ClientApp::serverLink(); } static String hostId(serverinfo_t const &sv) { return String("%1:%2").arg(sv.address).arg(sv.port); } /** * Action for joining a game on a multiplayer server. */ class JoinAction : public de::Action { String gameId; String cmd; public: JoinAction(serverinfo_t const &sv) { gameId = sv.gameIdentityKey; cmd = String("connect %1 %2").arg(sv.address).arg(sv.port); } void trigger() { Action::trigger(); BusyMode_FreezeGameForBusyMode(); ClientWindow::main().taskBar().close(); // Automatically leave the current MP game. if(netGame && isClient) { ClientApp::serverLink().disconnect(); } App_ChangeGame(App_Games().byIdentityKey(gameId), false /*no reload*/); Con_Execute(CMDS_DDAY, cmd.toLatin1(), false, false); } }; /** * Data item with information about a found server. */ class ServerListItem : public ui::Item, public SessionItem { public: ServerListItem(serverinfo_t const &serverInfo, SessionMenuWidget &owner) : SessionItem(owner) { setData(hostId(serverInfo)); _info = serverInfo; } serverinfo_t const &info() const { return _info; } void setInfo(serverinfo_t const &serverInfo) { _info = serverInfo; notifyChange(); } String title() const { return _info.name; } String gameIdentityKey() const { return _info.gameIdentityKey; } private: serverinfo_t _info; }; /** * Widget representing a ServerListItem in the dialog's menu. */ struct ServerWidget : public GameSessionWidget { ServerListItem const *svItem = nullptr; ServerWidget() { loadButton().disable(); } Game const *game() const { if(!svItem) return nullptr; return &App_Games().byIdentityKey(svItem->info().gameIdentityKey); } void updateFromItem(ServerListItem const &item) { try { svItem = &item; Game const &svGame = *game(); if(style().images().has(svGame.logoImageId())) { loadButton().setImage(style().images().image(svGame.logoImageId())); } serverinfo_t const &sv = item.info(); loadButton().setText(String(_E(F)_E(s) "%2\n" _E(.)_E(.) _E(1) "%1" _E(.)_E(C) "%4" _E(.)_E(D)_E(l) "\n%5 %3") .arg(sv.name) .arg(svGame.title()) .arg(sv.gameConfig) .arg(sv.numPlayers? QString(" " DENG2_CHAR_MDASH " %1").arg(sv.numPlayers) : QString()) .arg(sv.map)); // Extra information. document().setText(ServerInfo_AsStyledText(&sv)); updateAvailability(); } catch(Error const &) { svItem = nullptr; /// @todo } } void updateAvailability() { loadButton().enable(svItem && svItem->info().canJoin && svItem->info().version == DOOMSDAY_VERSION && game()->allStartupFilesFound()); } }; DiscoveryMode mode; ServerLink::FoundMask mask; IndirectRule *maxHeightRule = new IndirectRule; Instance(Public *i) : Base(i) , mask(ServerLink::Any) { link().audienceForDiscoveryUpdate += this; App::app().audienceForGameChange() += this; App_Games().audienceForReadiness() += this; } ~Instance() { releaseRef(maxHeightRule); link().audienceForDiscoveryUpdate -= this; App::app().audienceForGameChange() -= this; App_Games().audienceForReadiness() -= this; } /** * Puts together a rule that determines the tallest load button of those present * in the menu. This will be used to size all the buttons uniformly. */ void updateItemMaxHeight() { // Form a rule that is the maximum of all load button heights. Rule const *maxHgt = nullptr; foreach(Widget *w, self.childWidgets()) { if(ServerWidget *sw = w->maybeAs()) { auto const &itemHeight = sw->loadButton().contentHeight(); if(!maxHgt) { maxHgt = holdRef(itemHeight); } else { changeRef(maxHgt, OperatorRule::maximum(*maxHgt, itemHeight)); } } } if(maxHgt) { maxHeightRule->setSource(*maxHgt); } else { maxHeightRule->unsetSource(); } releaseRef(maxHgt); } void linkDiscoveryUpdate(ServerLink const &link) { bool changed = false; // Remove obsolete entries. for(ui::Data::Pos idx = 0; idx < self.items().size(); ++idx) { String const id = self.items().at(idx).data().toString(); if(!link.isFound(Address::parse(id), mask)) { self.items().remove(idx--); changed = true; } } // Add new entries and update existing ones. foreach(de::Address const &host, link.foundServers(mask)) { serverinfo_t info; if(!link.foundServerInfo(host, &info, mask)) continue; ui::Data::Pos found = self.items().findData(hostId(info)); if(found == ui::Data::InvalidPos) { // Needs to be added. self.items().append(new ServerListItem(info, self)); changed = true; } else { // Update the info. self.items().at(found).as().setInfo(info); } } if(changed) { updateItemMaxHeight(); self.sort(); // Let others know that one or more games have appeared or disappeared // from the menu. emit self.availabilityChanged(); } } void currentGameChanged(game::Game const &newGame) { if(newGame.isNull() && mode == DiscoverUsingMaster) { // If the session menu exists across game changes, it's good to // keep it up to date. link().discoverUsingMaster(); } } void gameReadinessUpdated() { foreach(Widget *w, self.childWidgets()) { if(ServerWidget *sw = w->maybeAs()) { sw->updateAvailability(); } } } }; MPSessionMenuWidget::MPSessionMenuWidget(DiscoveryMode discovery) : SessionMenuWidget("mp-session-menu"), d(new Instance(this)) { d->mode = discovery; switch(discovery) { case DiscoverUsingMaster: d->link().discoverUsingMaster(); break; case DirectDiscoveryOnly: // Only show servers found via direct connection. d->mask = ServerLink::Direct; break; default: break; } } Action *MPSessionMenuWidget::makeAction(ui::Item const &item) { return new Instance::JoinAction(item.as().info()); } GuiWidget *MPSessionMenuWidget::makeItemWidget(ui::Item const &, GuiWidget const *) { auto *sw = new Instance::ServerWidget; sw->rule().setInput(Rule::Height, *d->maxHeightRule); return sw; } void MPSessionMenuWidget::updateItemWidget(GuiWidget &widget, ui::Item const &item) { Instance::ServerWidget &sv = widget.as(); sv.updateFromItem(item.as()); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/singleplayersessionmenuwidget.cpp0000664000175000017500000001366312641367670031053 0ustar jaakkojaakko/** @file singleplayersessionmenuwidget.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/singleplayersessionmenuwidget.h" #include "dd_main.h" #include "CommandAction" #include #include #include #include using namespace de; DENG_GUI_PIMPL(SingleplayerSessionMenuWidget) , DENG2_OBSERVES(Games, Addition) , DENG2_OBSERVES(Games, Readiness) , DENG2_OBSERVES(Loop, Iteration) // deferred updates , DENG2_OBSERVES(App, GameChange) { /// ActionItem with a Game member, for loading a particular game. struct GameItem : public ui::ImageItem, public SessionItem { Game const &game; GameItem(Game const &gameRef, de::String const &label, SingleplayerSessionMenuWidget &owner) : ui::ImageItem(ShownAsButton, label) , SessionItem(owner) , game(gameRef) { setData(&gameRef); } String title() const { return game.title(); } String gameIdentityKey() const { return game.identityKey(); } }; struct GameWidget : public GameSessionWidget { Game const *game; GameWidget() : game(0) {} void updateInfoContent() { DENG2_ASSERT(game != 0); document().setText(game->description()); } }; Mode mode; FIFO pendingGames; Instance(Public *i) : Base(i) { App_Games().audienceForAddition() += this; App_Games().audienceForReadiness() += this; App::app().audienceForGameChange() += this; } ~Instance() { Loop::get().audienceForIteration() -= this; App_Games().audienceForAddition() -= this; App_Games().audienceForReadiness() -= this; App::app().audienceForGameChange() -= this; } void gameAdded(Game &game) { // Called from a non-UI thread. pendingGames.put(&game); // Update from main thread later. Loop::get().audienceForIteration() += this; } void addExistingGames() { for(int i = 0; i < App_Games().count(); ++i) { gameAdded(App_Games().byIndex(i)); } } bool shouldBeShown(Game const &game) { bool const isReady = game.allStartupFilesFound(); return ((mode == ShowAvailableGames && isReady) || (mode == ShowGamesWithMissingResources && !isReady)); } void loopIteration() { Loop::get().audienceForIteration() -= this; addPendingGames(); updateGameAvailability(); } void addPendingGames() { if(pendingGames.isEmpty()) return; while(Game *game = pendingGames.take()) { ui::Item *item = makeItemForGame(*game); self.items() << item; updateWidgetWithGameStatus(*item); } self.sort(); emit self.availabilityChanged(); } ui::Item *makeItemForGame(Game &game) { String const idKey = game.identityKey(); String label = String(_E(b) "%1" _E(.) "\n" _E(l)_E(D) "%2") .arg(game.title()) .arg(idKey); GameItem *item = new GameItem(game, label, self); if(style().images().has(game.logoImageId())) { item->setImage(style().images().image(game.logoImageId())); } return item; } void updateWidgetWithGameStatus(ui::Item const &menuItem) { GameItem const &item = menuItem.as(); GameSessionWidget &w = self.itemWidget(item); w.show(shouldBeShown(item.game)); bool const isCurrentLoadedGame = (&App_CurrentGame() == &item.game); // Can be loaded? w.loadButton().enable(item.game.allStartupFilesFound() && !isCurrentLoadedGame); } void updateGameAvailability() { for(uint i = 0; i < self.items().size(); ++i) { updateWidgetWithGameStatus(self.items().at(i)); } self.sort(); emit self.availabilityChanged(); } void gameReadinessUpdated() { updateGameAvailability(); } void currentGameChanged(game::Game const &) { Loop::get().audienceForIteration() += this; } }; SingleplayerSessionMenuWidget::SingleplayerSessionMenuWidget(Mode mode, String const &name) : SessionMenuWidget(name), d(new Instance(this)) { d->mode = mode; // Maybe there are games loaded already. d->addExistingGames(); } SingleplayerSessionMenuWidget::Mode SingleplayerSessionMenuWidget::mode() const { return d->mode; } Action *SingleplayerSessionMenuWidget::makeAction(ui::Item const &item) { return new CommandAction("load " + item.as().gameIdentityKey()); } GuiWidget *SingleplayerSessionMenuWidget::makeItemWidget(ui::Item const &, GuiWidget const *) { return new Instance::GameWidget; } void SingleplayerSessionMenuWidget::updateItemWidget(GuiWidget &widget, de::ui::Item const &item) { Instance::GameWidget &w = widget.as(); Instance::GameItem const &it = item.as(); w.game = &it.game; w.loadButton().setImage(it.image()); w.loadButton().setText(it.label()); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/keygrabberwidget.cpp0000664000175000017500000000561512641367670026177 0ustar jaakkojaakko/** @file keygrabberwidget.cpp * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/keygrabberwidget.h" #include "clientapp.h" #include "ui/ddevent.h" #include "ui/b_util.h" #include #include using namespace de; using namespace ui; DENG_GUI_PIMPL(KeyGrabberWidget) { Instance(Public *i) : Base(i) {} void focus() { root().setFocus(thisPublic); self.set(Background(Background::GradientFrame, style().colors().colorf("accent"), 6)); self.setText(tr("Waiting for a key...")); } void unfocus() { root().setFocus(0); self.set(Background()); self.setText(tr("Click to focus")); } }; KeyGrabberWidget::KeyGrabberWidget(String const &name) : LabelWidget(name), d(new Instance(this)) { setTextLineAlignment(AlignLeft); setText(tr("Click to focus")); } bool KeyGrabberWidget::handleEvent(Event const &event) { if(!hasFocus()) { switch(handleMouseClick(event)) { case MouseClickFinished: d->focus(); return true; default: return false; } } else { if(KeyEvent const *key = event.maybeAs()) { if(key->ddKey() == DDKEY_ESCAPE) { d->unfocus(); return true; } ddevent_t ev; InputSystem::convertEvent(event, ev); String info = String("DD:%1 Qt:0x%2 Native:0x%3\n" _E(m) "%4") .arg(key->ddKey()) .arg(key->qtKey(), 0, 16) .arg(key->nativeCode(), 0, 16) .arg(B_EventToString(ev)); setText(info); return true; } if(MouseEvent const *mouse = event.maybeAs()) { if(mouse->type() == Event::MouseButton && mouse->state() == MouseEvent::Released && !hitTest(event)) { // Clicking outside clears focus. d->unfocus(); return true; } } } return false; } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/cvarchoicewidget.cpp0000664000175000017500000000322712641367670026165 0ustar jaakkojaakko/** @file cvarchoicewidget.cpp Console variable choice. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/cvarchoicewidget.h" #include using namespace de; using namespace ui; DENG2_PIMPL_NOREF(CVarChoiceWidget) { char const *cvar; cvar_t *var() const { cvar_t *cv = Con_FindVariable(cvar); DENG2_ASSERT(cv != 0); return cv; } }; CVarChoiceWidget::CVarChoiceWidget(char const *cvarPath) : d(new Instance) { d->cvar = cvarPath; updateFromCVar(); connect(this, SIGNAL(selectionChangedByUser(uint)), this, SLOT(setCVarValueFromWidget())); } char const *CVarChoiceWidget::cvarPath() const { return d->cvar; } void CVarChoiceWidget::updateFromCVar() { setSelected(items().findData(CVar_Integer(d->var()))); } void CVarChoiceWidget::setCVarValueFromWidget() { CVar_SetInteger(d->var(), selectedItem().data().toInt()); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/busywidget.cpp0000664000175000017500000001305112641367670025035 0ustar jaakkojaakko/** @file busywidget.cpp * * @authors Copyright (c) 2013-2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "ui/widgets/busywidget.h" #include "ui/busyvisual.h" #include "ui/ui_main.h" #include "ui/clientwindow.h" #include "gl/gl_main.h" #include "render/r_main.h" #include "busymode.h" #include "sys_system.h" #include #include #include #include #include using namespace de; DENG_GUI_PIMPL(BusyWidget) { typedef DefaultVertexBuf VertexBuf; ProgressWidget *progress; Time frameDrawnAt; GLFramebuffer transitionFrame; Drawable drawable; GLUniform uTex { "uTex", GLUniform::Sampler2D }; GLUniform uMvpMatrix { "uMvpMatrix", GLUniform::Mat4 }; Instance(Public *i) : Base(i) { progress = new ProgressWidget; progress->setAlignment(ui::AlignCenter, LabelWidget::AlignOnlyByImage); progress->setRange(Rangei(0, 200)); progress->setImageScale(.2f); progress->rule().setRect(self.rule()); self.add(progress); } void glInit() { //transitionFrame.setColorFormat(Image::RGB_888); VertexBuf *buf = new VertexBuf; VertexBuf::Builder verts; verts.makeQuad(Rectanglef(0, 0, 1, 1), Vector4f(1, 1, 1, 1), Rectanglef(0, 0, 1, 1)); buf->setVertices(gl::TriangleStrip, verts, gl::Static); drawable.addBuffer(buf); shaders().build(drawable.program(), "generic.textured.color") << uMvpMatrix << uTex; } void glDeinit() { drawable.clear(); transitionFrame.glDeinit(); } bool haveTransitionFrame() const { return transitionFrame.isReady(); } }; BusyWidget::BusyWidget(String const &name) : GuiWidget(name), d(new Instance(this)) { requestGeometry(false); } ProgressWidget &BusyWidget::progress() { return *d->progress; } void BusyWidget::viewResized() { GuiWidget::viewResized(); } void BusyWidget::update() { GuiWidget::update(); if(BusyMode_Active()) { BusyMode_Loop(); } } void BusyWidget::drawContent() { if(!BusyMode_Active()) { d->progress->hide(); if(Con_TransitionInProgress()) { GLState::push() .setViewport(Rectangleui::fromSize(GLState::current().target().size())) .apply(); Con_DrawTransition(); GLState::pop().apply(); } else { // Time to hide the busy widget, the transition has ended (or // was never started). hide(); releaseTransitionFrame(); } return; } if(d->haveTransitionFrame()) { GLState::current().apply(); glDisable(GL_ALPHA_TEST); /// @todo get rid of these glDisable(GL_BLEND); glEnable(GL_TEXTURE_2D); // Draw the texture. Rectanglei pos = rule().recti(); d->uMvpMatrix = Matrix4f::scale(Vector3f(1, -1, 1)) * root().projMatrix2D() * Matrix4f::scaleThenTranslate(pos.size(), pos.topLeft); d->drawable.draw(); glEnable(GL_ALPHA_TEST); glEnable(GL_BLEND); glDisable(GL_TEXTURE_2D); } } bool BusyWidget::handleEvent(Event const &) { // Eat events and ignore them. return true; } void BusyWidget::renderTransitionFrame() { LOG_AS("BusyWidget"); if(d->haveTransitionFrame()) { // We already have a valid frame, no need to render again. LOGDEV_GL_VERBOSE("Skipping rendering of transition frame (got one already)"); return; } // We'll have an up-to-date frame after this. d->frameDrawnAt = Time(); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); Rectanglei grabRect = Rectanglei::fromSize(root().window().canvas().size()); LOGDEV_GL_VERBOSE("Rendering transition frame, size ") << grabRect.size().asText(); d->transitionFrame.resize(grabRect.size()); if(!d->transitionFrame.isReady()) { d->transitionFrame.glInit(); } GLState::push() .setTarget(d->transitionFrame.target()) .setViewport(Rectangleui::fromSize(d->transitionFrame.size())) .apply(); root().window().as().drawGameContent(); GLState::pop().apply(); d->uTex = d->transitionFrame.colorTexture(); } void BusyWidget::releaseTransitionFrame() { if(d->haveTransitionFrame()) { LOGDEV_GL_VERBOSE("Releasing transition frame"); d->transitionFrame.glDeinit(); } } GLTexture const *BusyWidget::transitionFrame() const { if(d->haveTransitionFrame()) { return &d->transitionFrame.colorTexture(); } return 0; } void BusyWidget::glInit() { d->glInit(); } void BusyWidget::glDeinit() { d->glDeinit(); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/gamefilterwidget.cpp0000664000175000017500000001411612641367670026175 0ustar jaakkojaakko/** @file gamefilterwidget.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/gamefilterwidget.h" #include #include #include #include #include #include using namespace de; static TimeDelta const BACKGROUND_FADE_SPAN = 0.25; static float const BACKGROUND_FILL_OPACITY = 0.8f; DENG_GUI_PIMPL(GameFilterWidget) { LabelWidget *background = nullptr; Rule const *bgOpacityRule = nullptr; Animation bgOpacity { 0, Animation::Linear }; bool animatingOpacity = false; TabWidget *tabs; LabelWidget *sortLabel; ChoiceWidget *sortBy; DialogContentStylist stylist; FilterMode filterMode = UserChangeable; Instance(Public *i) : Base(i) { stylist.setContainer(self); // Optional background. self.add(background = new LabelWidget); background->set(Background(Vector4f(style().colors().colorf("text"), 0), Background::BlurredWithSolidFill)); background->setOpacity(0); background->setAttribute(IndependentOpacity); background->hide(); // hidden by default // Create widgets. self.add(tabs = new TabWidget); sortLabel = LabelWidget::newWithText(tr("Sort By:"), &self); self.add(sortBy = new ChoiceWidget); tabs->items() << new TabItem(tr("Singleplayer"), Singleplayer) << new TabItem(tr("Multiplayer"), Multiplayer) << new TabItem(tr("All Games"), AllGames); sortLabel->setFont("small"); sortLabel->margins().setLeft(""); sortBy->setFont("small"); sortBy->setOpeningDirection(ui::Down); sortBy->items() << new ChoiceItem(tr("Title"), SortByTitle) << new ChoiceItem(tr("Identity key"), SortByIdentityKey); SequentialLayout layout(self.rule().left(), self.rule().midY() - sortBy->rule().height()/2, ui::Right); layout << *sortLabel << *sortBy; tabs->rule() .setInput(Rule::Width, self.rule().width()) .setInput(Rule::Left, self.rule().left()) .setInput(Rule::Top, self.rule().top()); } ~Instance() { releaseRef(bgOpacityRule); } String persistId(String const &name) const { return self.name() + "." + name; } void updateBackgroundOpacity() { float opacity = (bgOpacityRule->value() > 1? 1 : 0); if(!fequal(bgOpacity.target(), opacity)) { bgOpacity.setValue(opacity, BACKGROUND_FADE_SPAN); background->setOpacity(opacity, BACKGROUND_FADE_SPAN); animatingOpacity = true; } if(animatingOpacity) { Background bg = background->background(); bg.solidFill.w = bgOpacity * BACKGROUND_FILL_OPACITY; background->set(bg); if(bgOpacity.done()) animatingOpacity = false; } } }; GameFilterWidget::GameFilterWidget(String const &name) : GuiWidget(name), d(new Instance(this)) { connect(d->tabs, SIGNAL(currentTabChanged()), this, SIGNAL(filterChanged())); connect(d->sortBy, SIGNAL(selectionChanged(uint)), this, SIGNAL(sortOrderChanged())); rule().setInput(Rule::Height, d->tabs->rule().height()); } void GameFilterWidget::useInvertedStyle() { d->tabs->useInvertedStyle(); d->sortLabel->setTextColor("inverted.text"); d->sortBy->useInfoStyle(); } void GameFilterWidget::setFilter(Filter flt, FilterMode mode) { ui::DataPos pos = d->tabs->items().findData(duint(flt)); if(pos != ui::Data::InvalidPos) { d->tabs->setCurrent(pos); } d->filterMode = mode; if(d->filterMode == Permanent) { d->tabs->disable(); } } void GameFilterWidget::enableBackground(Rule const &scrollPositionRule) { DENG2_ASSERT(hasRoot()); d->bgOpacityRule = holdRef(scrollPositionRule); d->background->rule() .setInput(Rule::Left, root().viewLeft()) .setInput(Rule::Right, root().viewRight()) .setInput(Rule::Top, d->tabs->rule().top() - style().rules().rule("gap")) .setInput(Rule::Bottom, d->tabs->rule().bottom() + style().rules().rule("gap")); d->background->show(); } GameFilterWidget::Filter GameFilterWidget::filter() const { return Filter(d->tabs->currentItem().data().toUInt()); } GameFilterWidget::SortOrder GameFilterWidget::sortOrder() const { return SortOrder(d->sortBy->selectedItem().data().toInt()); } void GameFilterWidget::update() { GuiWidget::update(); if(d->background->isVisible()) { d->updateBackgroundOpacity(); } } void GameFilterWidget::operator >> (PersistentState &toState) const { Record &st = toState.names(); if(d->filterMode != Permanent) { st.set(d->persistId("filter"), dint(filter())); } st.set(d->persistId("order"), dint(sortOrder())); } void GameFilterWidget::operator << (PersistentState const &fromState) { Record const &st = fromState.names(); if(d->filterMode != Permanent) { d->tabs->setCurrent(d->tabs->items().findData(int(st[d->persistId("filter")]))); } d->sortBy->setSelected(d->sortBy->items().findData(int(st[d->persistId("order" )]))); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/gameuiwidget.cpp0000664000175000017500000001011712641367670025322 0ustar jaakkojaakko/** @file gameuiwidget.cpp Widget for legacy game UI elements. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2014-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "ui/widgets/gameuiwidget.h" #include #include "api_console.h" #include "edit_bias.h" #include "audio/s_main.h" #include "network/net_main.h" #include "gl/gl_main.h" #include "world/map.h" #include "render/rend_main.h" #include "ui/busyvisual.h" #include "ui/infine/finaleinterpreter.h" #include "ui/infine/finalepagewidget.h" using namespace de; static void setupProjectionForFinale(dgl_borderedprojectionstate_t *bp) { GL_ConfigureBorderedProjection(bp, BPF_OVERDRAW_CLIP, SCREENWIDTH, SCREENHEIGHT, DENG_GAMEVIEW_WIDTH, DENG_GAMEVIEW_HEIGHT, scalemode_t(Con_GetByte("rend-finale-stretch"))); } DENG2_PIMPL(GameUIWidget) { Instance(Public *i) : Base(i) {} void draw() { if(App_GameLoaded()) { R_RenderViewPorts(HUDLayer); // Draw finales. if(App_InFineSystem().finaleInProgess()) { dgl_borderedprojectionstate_t bp; //dd_bool bordered; setupProjectionForFinale(&bp); GL_BeginBorderedProjection(&bp); /*bordered = (FI_ScriptActive() && FI_ScriptCmdExecuted()); if(bordered) { // Draw using the special bordered projection. GL_ConfigureBorderedProjection(&borderedProjection); GL_BeginBorderedProjection(&borderedProjection); }*/ for(Finale *finale : App_InFineSystem().finales()) { finale->interpreter().page(FinaleInterpreter::Anims).draw(); finale->interpreter().page(FinaleInterpreter::Texts).draw(); } GL_EndBorderedProjection(&bp); //if(bordered) // GL_EndBorderedProjection(&borderedProjection); } // Draw any full window game graphics. if(gx.DrawWindow) { Size2Raw dimensions(DENG_GAMEVIEW_WIDTH, DENG_GAMEVIEW_HEIGHT); gx.DrawWindow(&dimensions); } } // Draw the widgets of the Shadow Bias Editor (if active). SBE_DrawGui(); /* * Draw debug information. */ if(App_WorldSystem().hasMap() && App_WorldSystem().map().hasLightGrid()) { Rend_LightGridVisual(App_WorldSystem().map().lightGrid()); } Net_Drawer(); S_Drawer(); DGL_End(); } }; GameUIWidget::GameUIWidget() : GuiWidget("game_ui"), d(new Instance(this)) {} void GameUIWidget::drawContent() { if(isDisabled() || !GL_IsFullyInited()) return; GLState::push().apply(); /* Rectanglei pos; if(hasChangedPlace(pos)) { // Automatically update if the widget is resized. d->updateSize(); }*/ d->draw(); GLState::considerNativeStateUndefined(); GLState::pop().apply(); } bool GameUIWidget::finaleStretch() //static { dgl_borderedprojectionstate_t bp; setupProjectionForFinale(&bp); return (bp.scaleMode == SCALEMODE_STRETCH); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/profilepickerwidget.cpp0000664000175000017500000002105712641367670026716 0ustar jaakkojaakko/** @file profilepickerwidget.cpp * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/profilepickerwidget.h" #include "ui/clientwindow.h" #include #include #include using namespace de; using namespace ui; static int const MAX_VISIBLE_PROFILE_NAME = 50; static int const MAX_PROFILE_NAME = 100; DENG_GUI_PIMPL(ProfilePickerWidget) , DENG2_OBSERVES(PanelWidget, Close) { SettingsRegister &settings; String description; ButtonWidget *button; PopupMenuWidget *menu; Instance(Public *i, SettingsRegister& reg) : Base(i) , settings(reg) , button(0) , menu(0) { self.add(button = new ButtonWidget); button->setAction(new SignalAction(thisPublic, SLOT(openMenu()))); updateStyle(); } ~Instance() { if(menu) menu->audienceForClose() -= this; } void updateStyle() { button->setSizePolicy(ui::Expand, ui::Expand); button->setImage(style().images().image("gear")); button->setOverrideImageSize(style().fonts().font("default").height().valuei()); } void populate() { self.items().clear(); foreach(String prof, settings.profiles()) { self.items() << new ChoiceItem(prof.left(MAX_VISIBLE_PROFILE_NAME), prof); } // The items are alphabetically ordered. self.items().sort(); self.setSelected(self.items().findData(settings.currentProfile())); } String currentProfile() const { return self.selectedItem().data().toString(); } void panelBeingClosed(PanelWidget &) { menu = 0; } }; ProfilePickerWidget::ProfilePickerWidget(SettingsRegister &settings, String const &description, String const &name) : ChoiceWidget(name), d(new Instance(this, settings)) { d->description = description; d->populate(); // Attach the button next to the widget. d->button->margins().setAll(margins()); d->button->rule() .setInput(Rule::Left, rule().right()) .setInput(Rule::Top, rule().top()); connect(this, SIGNAL(selectionChangedByUser(uint)), this, SLOT(applySelectedProfile())); } ButtonWidget &ProfilePickerWidget::button() { return *d->button; } void ProfilePickerWidget::updateStyle() { ChoiceWidget::updateStyle(); d->updateStyle(); } void ProfilePickerWidget::openMenu() { if(d->menu) { // Already open, just close it. d->menu->close(); return; } SettingsRegister ® = d->settings; d->menu = new PopupMenuWidget; d->menu->items() << new ActionItem(tr("Edit"), new SignalAction(this, SLOT(edit()))) << new ActionItem(tr("Rename..."), new SignalAction(this, SLOT(rename()))) << new ui::Item(Item::Separator) << new ActionItem(tr("Duplicate..."), new SignalAction(this, SLOT(duplicate()))) << new ui::Item(Item::Separator) << new ActionItem(tr("Reset to Defaults..."), new SignalAction(this, SLOT(reset()))) << new ActionItem(style().images().image("close.ring"), tr("Delete..."), new SignalAction(this, SLOT(remove()))); add(d->menu); ChildWidgetOrganizer const &org = d->menu->menu().organizer(); // Enable or disable buttons depending on the selected profile. String selProf = selectedItem().data().toString(); if(reg.isReadOnlyProfile(selProf)) { // Read-only profiles can only be duplicated. d->menu->items().at(0).setLabel("View"); org.itemWidget(1)->disable(); org.itemWidget(5)->disable(); org.itemWidget(6)->disable(); } if(reg.profileCount() == 1) { // The last profile cannot be deleted. org.itemWidget(6)->disable(); } if(root().window().as().hasSidebar()) { // The sidebar is already open, so don't allow editing. org.itemWidget(0)->disable(); } d->menu->setDeleteAfterDismissed(true); d->menu->setAnchorAndOpeningDirection(d->button->rule(), ui::Down); d->menu->audienceForClose() += d; d->menu->open(); } void ProfilePickerWidget::edit() { emit profileEditorRequested(); } void ProfilePickerWidget::rename() { InputDialog *dlg = new InputDialog; dlg->setDeleteAfterDismissed(true); dlg->title().setText(tr("Renaming \"%1\"").arg(d->currentProfile())); dlg->message().setText(tr("Enter a new name for the %1 profile:").arg(d->description)); dlg->defaultActionItem()->setLabel(tr("Rename Profile")); dlg->editor().setText(d->currentProfile()); if(dlg->exec(root())) { String newName = dlg->editor().text().trimmed().left(MAX_PROFILE_NAME); if(!newName.isEmpty() && d->currentProfile() != newName) { if(d->settings.rename(newName)) { ui::Item &item = items().at(selected()); item.setLabel(newName.left(MAX_VISIBLE_PROFILE_NAME)); item.setData(newName); // Keep the list sorted. items().sort(); setSelected(items().findData(newName)); } else { LOG_WARNING("Failed to rename profile to \"%s\"") << newName; } } } } void ProfilePickerWidget::duplicate() { InputDialog *dlg = new InputDialog; dlg->setDeleteAfterDismissed(true); dlg->title().setText(tr("Duplicating \"%1\"").arg(d->currentProfile())); dlg->message().setText(tr("Enter a name for the new %1 profile:").arg(d->description)); dlg->defaultActionItem()->setLabel(tr("Duplicate Profile")); if(dlg->exec(root())) { String newName = dlg->editor().text().trimmed().left(MAX_PROFILE_NAME); if(!newName.isEmpty()) { if(d->settings.saveAsProfile(newName)) { d->settings.setProfile(newName); items().append(new ChoiceItem(newName.left(MAX_VISIBLE_PROFILE_NAME), newName)).sort(); setSelected(items().findData(newName)); applySelectedProfile(); } else { LOG_WARNING("Failed to duplicate current profile to create \"%s\"") << newName; } } } } void ProfilePickerWidget::reset() { MessageDialog *dlg = new MessageDialog; dlg->setDeleteAfterDismissed(true); dlg->title().setText(tr("Reset?")); dlg->message().setText(tr("Are you sure you want to reset the %1 profile %2 to the default values?") .arg(d->description) .arg(_E(b) + d->currentProfile() + _E(.))); dlg->buttons() << new DialogButtonItem(DialogWidget::Default | DialogWidget::Reject) << new DialogButtonItem(DialogWidget::Accept, tr("Reset Profile")); if(dlg->exec(root())) { d->settings.resetToDefaults(); } } void ProfilePickerWidget::remove() { MessageDialog *dlg = new MessageDialog; dlg->setDeleteAfterDismissed(true); dlg->title().setText(tr("Delete?")); dlg->message().setText( tr("Are you sure you want to delete the %1 profile %2? This cannot be undone.") .arg(d->description) .arg(_E(b) + d->currentProfile() + _E(.))); dlg->buttons() << new DialogButtonItem(DialogWidget::Default | DialogWidget::Reject) << new DialogButtonItem(DialogWidget::Accept, tr("Delete Profile")); if(!dlg->exec(root())) return; // We've got the permission. String const profToDelete = d->currentProfile(); items().remove(selected()); // Switch to the new selection. applySelectedProfile(); d->settings.deleteProfile(profToDelete); } void ProfilePickerWidget::applySelectedProfile() { d->settings.setProfile(d->currentProfile()); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/gamewidget.cpp0000664000175000017500000001460112641367670024766 0ustar jaakkojaakko/** @file gamewidget.cpp * * @authors Copyright © 2013-2014 Jaakko Keränen * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" // must be included first #include "ui/widgets/gamewidget.h" #include "clientapp.h" #include "ui/ddevent.h" #include "ui/ui_main.h" #include "ui/sys_input.h" #include "ui/busyvisual.h" #include "ui/clientwindowsystem.h" #include "ui/widgets/taskbarwidget.h" #include "ui/widgets/busywidget.h" #include "dd_def.h" #include "dd_main.h" #include "dd_loop.h" #include "sys_system.h" #include "edit_bias.h" #include "world/map.h" #include "network/net_main.h" #include "render/r_main.h" #include "render/rend_main.h" #include "render/cameralensfx.h" #include "audio/s_main.h" #include "render/lightgrid.h" #include "gl/gl_main.h" #include "gl/sys_opengl.h" #include "gl/gl_defer.h" #include /** * Maximum number of milliseconds spent uploading textures at the beginning * of a frame. Note that non-uploaded textures will appear as pure white * until their content gets uploaded (you should precache them). */ #define FRAME_DEFERRED_UPLOAD_TIMEOUT 20 using namespace de; DENG2_PIMPL(GameWidget) { Instance(Public *i) : Base(i) {} void draw() { bool cannotDraw = (self.isDisabled() || !GL_IsFullyInited()); if(renderWireframe || cannotDraw) { // When rendering is wireframe mode, we must clear the screen // before rendering a frame. glClear(GL_COLOR_BUFFER_BIT); } if(cannotDraw) return; if(App_GameLoaded()) { // Notify the world that a new render frame has begun. App_WorldSystem().beginFrame(CPP_BOOL(R_NextViewer())); R_RenderViewPorts(Player3DViewLayer); R_RenderViewPorts(ViewBorderLayer); // End any open DGL sequence. DGL_End(); // Notify the world that we've finished rendering the frame. App_WorldSystem().endFrame(); } // End any open DGL sequence. DGL_End(); } void updateSize() { LOG_AS("GameWidget"); LOG_GL_XVERBOSE("View resized to ") << self.rule().recti().size().asText(); // Update viewports. R_SetViewGrid(0, 0); if(!App_GameLoaded()) { // Update for busy mode. R_UseViewPort(0); } UI_LoadFonts(); } }; GameWidget::GameWidget(String const &name) : GuiWidget(name), d(new Instance(this)) { requestGeometry(false); } void GameWidget::glApplyViewport(Rectanglei const &rect) { GLState::current() .setNormalizedViewport(normalizedRect(rect)) .apply(); } void GameWidget::viewResized() { GuiWidget::viewResized(); /* if(BusyMode_Active() || isDisabled() || Sys_IsShuttingDown() || !ClientApp::windowSystem().hasMain()) { return; } d->updateSize();*/ } void GameWidget::update() { GuiWidget::update(); if(isDisabled() || BusyMode_Active()) return; // We may be performing GL operations. ClientWindow::main().glActivate(); // Run at least one (fractional) tic. Loop_RunTics(); // We may have received a Quit message from the windowing system // during events/tics processing. if(Sys_IsShuttingDown()) return; GL_ProcessDeferredTasks(FRAME_DEFERRED_UPLOAD_TIMEOUT); // Release the busy transition frame now when we can be sure that busy mode // is over / didn't start at all. if(!Con_TransitionInProgress()) { ClientWindow::main().busy().releaseTransitionFrame(); } } void GameWidget::drawContent() { if(isDisabled() || !GL_IsFullyInited()) return; GLState::push(); Rectanglei pos; if(hasChangedPlace(pos)) { // Automatically update if the widget is resized. d->updateSize(); } d->draw(); GLState::considerNativeStateUndefined(); GLState::pop(); } bool GameWidget::handleEvent(Event const &event) { /** * @todo Event processing should occur here, not during Loop_RunTics(). * However, care must be taken to reproduce the vanilla behavior of * controls with regard to response times. * * @todo Input drivers need to support Unicode text; for now we have to * submit as Latin1. */ ClientWindow &window = root().window().as(); if(event.type() == Event::MouseButton && !root().window().canvas().isMouseTrapped()) { if(!window.hasSidebar()) { // If the mouse is not trapped, we will just eat button clicks which // will prevent them from reaching the legacy input system. return true; } // If the sidebar is open, we must explicitly click on the GameWidget to // cause input to be trapped. switch(handleMouseClick(event)) { case MouseClickFinished: // Click completed on the widget, trap the mouse. window.canvas().trapMouse(); window.taskBar().close(); root().setFocus(0); // Allow input to reach here. break; default: // Just ignore the event. return true; } } if(event.type() == Event::KeyPress || event.type() == Event::KeyRepeat || event.type() == Event::KeyRelease) { KeyEvent const &ev = event.as(); Keyboard_Submit(ev.state() == KeyEvent::Pressed? IKE_DOWN : ev.state() == KeyEvent::Repeat? IKE_REPEAT : IKE_UP, ev.ddKey(), ev.nativeCode(), ev.text().toLatin1()); } return false; } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/consolewidget.cpp0000664000175000017500000004554412641367670025531 0ustar jaakkojaakko/** @file consolewidget.cpp * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/consolewidget.h" #include "CommandAction" #include "ui/widgets/consolecommandwidget.h" #include "ui/widgets/inputbindingwidget.h" #include "ui/dialogs/logsettingsdialog.h" #include "ui/clientwindow.h" #include "ui/styledlogsinkformatter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace de; static TimeDelta const LOG_OPEN_CLOSE_SPAN = 0.2; DENG_GUI_PIMPL(ConsoleWidget), DENG2_OBSERVES(Variable, Change) { GuiWidget *buttons = nullptr; ButtonWidget *button = nullptr; ButtonWidget *promptButton = nullptr; PopupMenuWidget *logMenu = nullptr; PopupMenuWidget *promptMenu = nullptr; ConsoleCommandWidget *cmdLine = nullptr; ScriptCommandWidget *scriptCmd = nullptr; LogWidget *log = nullptr; ScalarRule *horizShift; ScalarRule *height; ScalarRule *width; StyledLogSinkFormatter formatter; bool opened = true; bool scriptMode = false; int grabWidth = 0; enum GrabEdge { NotGrabbed = 0, RightEdge, TopEdge }; GrabEdge grabHover = NotGrabbed; GrabEdge grabbed = NotGrabbed; Instance(Public *i) : Base(i) { horizShift = new ScalarRule(0); width = new ScalarRule(style().rules().rule("console.width").valuei()); height = new ScalarRule(0); grabWidth = style().rules().rule("gap").valuei(); App::config("console.script").audienceForChange() += this; } ~Instance() { App::config("console.script").audienceForChange() -= this; releaseRef(horizShift); releaseRef(width); releaseRef(height); } void expandLog(int delta, bool useOffsetAnimation) { // Cannot expand if the user is grabbing the top edge. if(grabbed == TopEdge) return; Animation::Style style = useOffsetAnimation? Animation::EaseOut : Animation::Linear; if(height->animation().target() == 0) { // On the first expansion make sure the margins are taken into account. delta += log->margins().height().valuei(); } height->set(height->animation().target() + delta, .25f); height->setStyle(style); if(useOffsetAnimation && opened) { // Sync the log content with the height animation. log->setContentYOffset(Animation::range(style, log->contentYOffset().value() + delta, 0, height->animation().remainingTime())); } } bool handleMousePositionEvent(MouseEvent const &mouse) { if(grabbed == RightEdge) { // Adjust width. width->set(mouse.pos().x - self.rule().left().valuei(), .1f); return true; } else if(grabbed == TopEdge) { if(mouse.pos().y < self.rule().bottom().valuei()) { height->set(self.rule().bottom().valuei() - mouse.pos().y, .1f); log->enablePageKeys(false); } return true; } // Check for grab at the right edge. Rectanglei pos = self.rule().recti(); pos.topLeft.x = pos.bottomRight.x - grabWidth; if(pos.contains(mouse.pos())) { if(grabHover != RightEdge) { grabHover = RightEdge; self.root().window().canvas().setCursor(Qt::SizeHorCursor); } } else { // Maybe a grab at the top edge, then. pos = self.rule().recti(); pos.bottomRight.y = pos.topLeft.y + grabWidth; if(pos.contains(mouse.pos())) { if(grabHover != TopEdge) { grabHover = TopEdge; self.root().window().canvas().setCursor(Qt::SizeVerCursor); } } else if(grabHover != NotGrabbed) { grabHover = NotGrabbed; self.root().window().canvas().setCursor(Qt::ArrowCursor); } } return false; } void variableValueChanged(Variable &, Value const &newValue) { // We are listening to the 'console.script' variable. setScriptMode(newValue.isTrue()); } void setScriptMode(bool yes) { CommandWidget *current; CommandWidget *next; if(scriptMode) { current = scriptCmd; } else { current = cmdLine; } if(yes) { next = scriptCmd; } else { next = cmdLine; } // Update the prompt to reflect the mode. promptButton->setText(yes? _E(b)_E(F) "$" : ">"); // Bottom of the console must follow the active command line height. self.rule().setInput(Rule::Bottom, next->rule().top() - style().rules().rule("unit")); if(scriptMode == yes) { return; // No need to change anything else. } scriptCmd->setAttribute(AnimateOpacityWhenEnabledOrDisabled, UnsetFlags); cmdLine ->setAttribute(AnimateOpacityWhenEnabledOrDisabled, UnsetFlags); scriptCmd->show(yes); scriptCmd->enable(yes); cmdLine->show(!yes); cmdLine->disable(yes); // Transfer focus. if(current->hasFocus()) { root().setFocus(next); } scriptMode = yes; emit self.commandModeChanged(); } struct RightClick : public GuiWidget::IEventHandler { Instance *d; RightClick(Instance *inst) : d(inst) {} bool handleEvent(GuiWidget &widget, Event const &event) { switch(widget.handleMouseClick(event, MouseEvent::Right)) { case MouseClickFinished: // Toggle the script mode. App::config().set("console.script", !d->scriptMode); return true; case MouseClickUnrelated: break; // not consumed default: return true; } return false; } }; }; static PopupWidget *consoleShortcutPopup() { auto *pop = new PopupWidget; // The 'padding' widget will provide empty margins around the content. // Popups normally do not provide any margins. auto *padding = new GuiWidget; padding->margins().set("dialog.gap"); auto *bind = InputBindingWidget::newTaskBarShortcut(); bind->setSizePolicy(ui::Expand, ui::Expand); padding->add(bind); // Place the binding inside the padding. bind->rule() .setLeftTop(padding->rule().left() + padding->margins().left(), padding->rule().top() + padding->margins().top()); padding->rule() .setInput(Rule::Width, bind->rule().width() + padding->margins().width()) .setInput(Rule::Height, bind->rule().height() + padding->margins().height()); pop->setContent(padding); return pop; } ConsoleWidget::ConsoleWidget() : GuiWidget("console"), d(new Instance(this)) { d->buttons = new GuiWidget; add(d->buttons); // Button for opening the Log History menu. d->button = new ButtonWidget; d->button->setSizePolicy(ui::Expand, ui::Expand); d->button->setImage(style().images().image("log")); d->button->setOverrideImageSize(style().fonts().font("default").height().valuei()); d->buttons->add(d->button); d->button->rule() .setInput(Rule::Top, d->buttons->rule().top()) .setInput(Rule::Left, d->buttons->rule().left()) .setInput(Rule::Height, d->buttons->rule().height()) .setInput(Rule::Width, d->button->rule().height()); // square // Button for opening the console prompt menu. d->promptButton = new ButtonWidget; d->promptButton->addEventHandler(new Instance::RightClick(d)); d->promptButton->rule() .setInput(Rule::Width, d->promptButton->rule().height()) .setInput(Rule::Height, d->buttons->rule().height()) .setInput(Rule::Left, d->button->rule().right()) .setInput(Rule::Top, d->buttons->rule().top()); d->buttons->add(d->promptButton); // Width of the button area is known; the task bar will define the rest. d->buttons->rule().setInput(Rule::Width, d->button->rule().width() + d->promptButton->rule().width()); d->cmdLine = new ConsoleCommandWidget("commandline"); d->cmdLine->setEmptyContentHint(tr("Enter commands here") /* " _E(r)_E(l)_E(t) "SHIFT-ESC" */); add(d->cmdLine); d->scriptCmd = new ScriptCommandWidget("script"); d->scriptCmd->setEmptyContentHint(tr("Enter scripts here")); add(d->scriptCmd); d->buttons->setOpacity(.75f); d->cmdLine->setOpacity(.75f); d->scriptCmd->setOpacity(.75f); d->log = new LogWidget("log"); d->log->setLogFormatter(d->formatter); d->log->rule() .setInput(Rule::Left, rule().left()) .setInput(Rule::Right, rule().right()) .setInput(Rule::Bottom, rule().bottom()) .setInput(Rule::Top, OperatorRule::maximum(rule().top(), Const(0))); add(d->log); // Blur the log background. enableBlur(); // Width of the console is defined by the style. rule() .setInput(Rule::Height, *d->height) .setInput(Rule::Width, OperatorRule::minimum(ClientWindow::main().root().viewWidth(), OperatorRule::maximum(*d->width, Const(320)))); closeLog(); // Menu for log history related features. d->logMenu = new PopupMenuWidget; d->logMenu->setAnchor(d->button->rule().midX(), d->button->rule().top()); d->logMenu->items() << new ui::ActionItem(tr("Show Full Log"), new SignalAction(this, SLOT(showFullLog()))) << new ui::ActionItem(tr("Close Log"), new SignalAction(this, SLOT(closeLogAndUnfocusCommandLine()))) << new ui::ActionItem(tr("Go to Latest"), new SignalAction(d->log, SLOT(scrollToBottom()))) << new ui::ActionItem(tr("Copy Path to Clipboard"), new SignalAction(this, SLOT(copyLogPathToClipboard()))) << new ui::Item(ui::Item::Annotation, tr("Copies the location of the log output file to the OS clipboard.")) << new ui::Item(ui::Item::Separator) << new ui::ActionItem(style().images().image("close.ring"), tr("Clear Log"), new CommandAction("clear")) << new ui::Item(ui::Item::Separator) << new ui::VariableToggleItem(tr("Snap to Latest Entry"), App::config("console.snap")) << new ui::Item(ui::Item::Annotation, tr("Running a command or script causes the log to scroll down to the latest entry.")) << new ui::VariableToggleItem(tr("Entry Metadata"), App::config("log.showMetadata")) << new ui::Item(ui::Item::Annotation, tr("Time and subsystem of each new entry is printed.")) << new ui::Item(ui::Item::Separator) << new ui::SubwidgetItem(tr("Log Filter & Alerts..."), ui::Right, makePopup); add(d->logMenu); d->button->setAction(new SignalAction(d->logMenu, SLOT(open()))); // Menu for console prompt behavior. d->promptMenu = new PopupMenuWidget; d->promptMenu->setAnchor(d->promptButton->rule().midX(), d->promptButton->rule().top()); d->promptMenu->items() << new ui::SubwidgetItem(tr("Shortcut Key..."), ui::Right, consoleShortcutPopup) << new ui::Item(ui::Item::Separator) << new ui::VariableToggleItem(tr("Doomsday Script"), App::config("console.script")) << new ui::Item(ui::Item::Annotation, tr("The command prompt becomes an interactive script " "process with access to all the runtime DS modules.")); add(d->promptMenu); d->promptButton->setAction(new SignalAction(d->promptMenu, SLOT(open()))); d->setScriptMode(App::config().getb("console.script")); // Signals. connect(d->log, SIGNAL(contentHeightIncreased(int)), this, SLOT(logContentHeightIncreased(int))); connect(d->cmdLine, SIGNAL(gotFocus()), this, SLOT(commandLineFocusGained())); connect(d->cmdLine, SIGNAL(lostFocus()), this, SLOT(commandLineFocusLost())); connect(d->cmdLine, SIGNAL(commandEntered(de::String)), this, SLOT(commandWasEntered(de::String))); connect(d->scriptCmd, SIGNAL(gotFocus()), this, SLOT(commandLineFocusGained())); connect(d->scriptCmd, SIGNAL(lostFocus()), this, SLOT(commandLineFocusLost())); connect(d->scriptCmd, SIGNAL(commandEntered(de::String)), this, SLOT(commandWasEntered(de::String))); } GuiWidget &ConsoleWidget::buttons() { return *d->buttons; } CommandWidget &ConsoleWidget::commandLine() { if(d->scriptMode) return *d->scriptCmd; return *d->cmdLine; } LogWidget &ConsoleWidget::log() { return *d->log; } Rule const &ConsoleWidget::shift() { return *d->horizShift; } bool ConsoleWidget::isLogOpen() const { return d->opened; } void ConsoleWidget::enableBlur(bool yes) { Background logBg = d->log->background(); if(yes) { logBg.type = Background::SharedBlur; logBg.blur = &ClientWindow::main().taskBarBlur(); } else { logBg.type = Background::None; } d->log->set(logBg); } void ConsoleWidget::viewResized() { GuiWidget::viewResized(); if(!d->opened) { // Make sure it stays shifted out of the view. d->horizShift->set(-rule().width().valuei() - 1); } } void ConsoleWidget::update() { GuiWidget::update(); if(rule().top().valuei() <= 0) { // The expansion has reached the top of the screen, so we // can enable PageUp/Dn keys for the log. d->log->enablePageKeys(true); } } bool ConsoleWidget::handleEvent(Event const &event) { // Hovering over the right edge shows the <-> cursor. if(event.type() == Event::MousePosition) { if(d->handleMousePositionEvent(event.as())) { return true; } } // Dragging an edge resizes the widget. if(d->grabHover != Instance::NotGrabbed && event.type() == Event::MouseButton) { switch(handleMouseClick(event)) { case MouseClickStarted: d->grabbed = d->grabHover; return true; case MouseClickAborted: case MouseClickFinished: d->grabbed = Instance::NotGrabbed; return true; default: break; } } if(event.type() == Event::MouseButton && hitTest(event)) { // Prevent clicks from leaking through. return true; } if(event.type() == Event::KeyPress) { KeyEvent const &key = event.as(); if(key.qtKey() == Qt::Key_PageUp || key.qtKey() == Qt::Key_PageDown) { if(!isLogOpen()) openLog(); showFullLog(); return true; } if(key.qtKey() == Qt::Key_F5) // Clear history. { clearLog(); return true; } } return false; } void ConsoleWidget::operator >> (PersistentState &toState) const { toState.names().set("console.width", d->width->value()); } void ConsoleWidget::operator << (PersistentState const &fromState) { d->width->set(fromState.names()["console.width"]); if(!d->opened) { // Make sure it stays out of the view. d->horizShift->set(-rule().width().valuei() - 1); } } void ConsoleWidget::openLog() { if(d->opened) return; d->opened = true; d->horizShift->set(0, LOG_OPEN_CLOSE_SPAN); d->log->unsetBehavior(DisableEventDispatch); } void ConsoleWidget::closeLog() { if(!d->opened) return; d->opened = false; d->horizShift->set(-rule().width().valuei() - 1, LOG_OPEN_CLOSE_SPAN); d->log->setBehavior(DisableEventDispatch); } void ConsoleWidget::closeLogAndUnfocusCommandLine() { closeLog(); root().setFocus(nullptr); } void ConsoleWidget::clearLog() { zeroLogHeight(); d->log->clear(); } void ConsoleWidget::zeroLogHeight() { d->height->set(0); d->log->scrollToBottom(); d->log->enablePageKeys(false); } void ConsoleWidget::showFullLog() { openLog(); d->log->enablePageKeys(true); d->expandLog(rule().top().valuei(), false); } void ConsoleWidget::logContentHeightIncreased(int delta) { d->expandLog(delta, true); } void ConsoleWidget::setFullyOpaque() { d->scriptCmd->setAttribute(AnimateOpacityWhenEnabledOrDisabled); d->cmdLine->setAttribute(AnimateOpacityWhenEnabledOrDisabled); d->buttons->setOpacity(1, .25f); d->cmdLine->setOpacity(1, .25f); d->scriptCmd->setOpacity(1, .25f); } void ConsoleWidget::commandLineFocusGained() { setFullyOpaque(); openLog(); emit commandLineGotFocus(); } void ConsoleWidget::commandLineFocusLost() { d->scriptCmd->setAttribute(AnimateOpacityWhenEnabledOrDisabled); d->cmdLine->setAttribute(AnimateOpacityWhenEnabledOrDisabled); d->buttons->setOpacity(.75f, .25f); d->cmdLine->setOpacity(.75f, .25f); d->scriptCmd->setOpacity(.75f, .25f); closeLog(); } void ConsoleWidget::focusOnCommandLine() { root().setFocus(&commandLine()); } void ConsoleWidget::closeMenu() { d->logMenu->close(); d->promptMenu->close(); } void ConsoleWidget::commandWasEntered(String const &) { if(App::config().getb("console.snap") && !d->log->isAtBottom()) { d->log->scrollToBottom(); } } void ConsoleWidget::copyLogPathToClipboard() { if(NativeFile *native = App::rootFolder().tryLocate(LogBuffer::get().outputFile())) { qApp->clipboard()->setText(native->nativePath()); } } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/taskbarwidget.cpp0000664000175000017500000006132512641367670025511 0ustar jaakkojaakko/** @file taskbarwidget.cpp * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/taskbarwidget.h" #include "ui/widgets/consolecommandwidget.h" #include "ui/widgets/multiplayermenuwidget.h" #include "ui/widgets/tutorialwidget.h" #include "ui/dialogs/aboutdialog.h" #include "ui/dialogs/videosettingsdialog.h" #include "ui/dialogs/audiosettingsdialog.h" #include "ui/dialogs/inputsettingsdialog.h" #include "ui/dialogs/networksettingsdialog.h" #include "ui/dialogs/renderersettingsdialog.h" #include "ui/dialogs/manualconnectiondialog.h" #include "ui/dialogs/vrsettingsdialog.h" #include "ui/dialogs/gamesdialog.h" #include "updater/updatersettingsdialog.h" #include "ui/clientwindow.h" #include "ui/clientrootwidget.h" #include "clientapp.h" #include "CommandAction" #include "client/cl_def.h" // clientPaused #include "ui/ui_main.h" #include "ui/progress.h" #include "versioninfo.h" #include "dd_main.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace de; using namespace ui; static TimeDelta OPEN_CLOSE_SPAN = 0.2; enum MenuItemPositions { // DE menu: POS_GAMES = 0, POS_UNLOAD = 1, POS_IWAD_FOLDER = 2, POS_GAMES_SEPARATOR = 3, POS_MULTIPLAYER = 4, POS_CONNECT = 5, // Config menu: POS_RENDERER_SETTINGS = 0, POS_VR_SETTINGS = 1, POS_CONFIG_SEPARATOR = 2, POS_AUDIO_SETTINGS = 4, POS_INPUT_SETTINGS = 5 }; DENG_GUI_PIMPL(TaskBarWidget) , DENG2_OBSERVES(App, GameChange) , DENG2_OBSERVES(ServerLink, Join) , DENG2_OBSERVES(ServerLink, Leave) { typedef DefaultVertexBuf VertexBuf; enum LayoutMode { NormalLayout, ///< Taskbar widgets are full-sized. CompressedLayout, ///< Cull some redundant information. ExtraCompressedLayout ///< Hide current game indicator. }; LayoutMode layoutMode; bool opened; GuiWidget *backBlur; ConsoleWidget *console; ButtonWidget *logo; ButtonWidget *conf; ButtonWidget *multi; LabelWidget *status; PopupMenuWidget *mainMenu; PopupMenuWidget *configMenu; MultiplayerMenuWidget *multiMenu; ScalarRule *vertShift; bool mouseWasTrappedWhenOpening; int minSpace; int maxSpace; // GL objects: Drawable drawable; GLUniform uMvpMatrix; GLUniform uColor; Matrix4f projMatrix; Instance(Public *i) : Base(i) , layoutMode(NormalLayout) , opened(true) , logo(0) , conf(0) , status(0) , mainMenu(0) , configMenu(0) , multiMenu(0) , mouseWasTrappedWhenOpening(false) , uMvpMatrix("uMvpMatrix", GLUniform::Mat4) , uColor ("uColor", GLUniform::Vec4) { uColor = Vector4f(1, 1, 1, 1); self.set(Background(style().colors().colorf("background"))); vertShift = new ScalarRule(0); App::app().audienceForGameChange() += this; ClientApp::serverLink().audienceForJoin += this; ClientApp::serverLink().audienceForLeave += this; updateStyle(); } ~Instance() { App::app().audienceForGameChange() -= this; ClientApp::serverLink().audienceForJoin -= this; ClientApp::serverLink().audienceForLeave -= this; releaseRef(vertShift); } void updateStyle() { minSpace = style().rules().rule("console.commandline.width.min").valuei(); maxSpace = style().rules().rule("console.commandline.width.max").valuei(); } void updateLayoutMode() { LayoutMode wanted = layoutMode; // Does the command line have enough space? if(console->commandLine().rule().width().valuei() < minSpace) { wanted = (layoutMode == NormalLayout? CompressedLayout : layoutMode == CompressedLayout? ExtraCompressedLayout : layoutMode); } else if(console->commandLine().rule().width().valuei() > maxSpace) { wanted = (layoutMode == CompressedLayout? NormalLayout : layoutMode == ExtraCompressedLayout? CompressedLayout : layoutMode); } if(layoutMode != wanted) { layoutMode = wanted; updateLogoButtonText(); // Adjust widget visibility and rules. switch(layoutMode) { case NormalLayout: case CompressedLayout: status->show(); break; case ExtraCompressedLayout: status->hide(); break; } self.updateCommandLineLayout(); self.requestGeometry(); console->commandLine().requestGeometry(); } } void glInit() { drawable.addBuffer(new VertexBuf); shaders().build(drawable.program(), "generic.color_ucolor") << uMvpMatrix << uColor; updateProjection(); } void glDeinit() { drawable.clear(); } void updateLogoButtonText() { String text; if(layoutMode == NormalLayout) { VersionInfo currentVersion; if(String(DOOMSDAY_RELEASE_TYPE) == "Stable") { text = _E(b) + currentVersion.base(); } else { text = _E(b) + currentVersion.base() + " " + _E(l) + String("#%1").arg(currentVersion.build); } } else { // Remove the version number if we're short of space. } logo->setText(text); } void updateProjection() { uMvpMatrix = root().projMatrix2D(); } void updateGeometry() { Rectanglei pos; if(self.hasChangedPlace(pos) || self.geometryRequested()) { self.requestGeometry(false); VertexBuf::Builder verts; self.glMakeGeometry(verts); drawable.buffer().setVertices(gl::TriangleStrip, verts, gl::Static); } } GuiWidget &itemWidget(PopupMenuWidget *menu, uint pos) const { return *menu->menu().organizer().itemWidget(pos); } void showOrHideMenuItems() { de::Game &game = App_CurrentGame(); itemWidget(mainMenu, POS_GAMES) .show(!game.isNull()); itemWidget(mainMenu, POS_UNLOAD) .show(!game.isNull()); itemWidget(mainMenu, POS_IWAD_FOLDER) .show(game.isNull()); //itemWidget(mainMenu, POS_GAMES_SEPARATOR) .show(!game.isNull()); itemWidget(mainMenu, POS_MULTIPLAYER) .show(!game.isNull()); itemWidget(mainMenu, POS_CONNECT) .show(game.isNull()); itemWidget(configMenu, POS_RENDERER_SETTINGS).show(!game.isNull()); itemWidget(configMenu, POS_VR_SETTINGS) .show(!game.isNull()); itemWidget(configMenu, POS_CONFIG_SEPARATOR) .show(!game.isNull()); itemWidget(configMenu, POS_AUDIO_SETTINGS) .show(!game.isNull()); itemWidget(configMenu, POS_INPUT_SETTINGS) .show(!game.isNull()); if(self.hasRoot()) { configMenu->menu().updateLayout(); mainMenu->menu().updateLayout(); // Include/exclude shown/hidden menu items. } } void currentGameChanged(game::Game const &) { updateStatus(); showOrHideMenuItems(); backBlur->show(style().isBlurringAllowed()); } void networkGameJoined() { multi->show(); self.updateCommandLineLayout(); } void networkGameLeft() { multi->hide(); self.updateCommandLineLayout(); } void updateStatus() { if(App_GameLoaded()) { status->setText(App_CurrentGame().identityKey()); } else { status->setText(tr("No game loaded")); } } }; PopupWidget *makeUpdaterSettings() { return new UpdaterSettingsDialog(UpdaterSettingsDialog::WithApplyAndCheckButton); } TaskBarWidget::TaskBarWidget() : GuiWidget("taskbar"), d(new Instance(this)) { #if 0 // GameWidget is presently too inefficient with blurring. BlurWidget *blur = new BlurWidget("taskbar_blur"); add(blur); Background bg(*blur, style().colors().colorf("background")); #else Background bg(style().colors().colorf("background")); #endif Rule const &gap = style().rules().rule("gap"); d->backBlur = new LabelWidget; d->backBlur->rule() .setInput(Rule::Left, rule().left()) .setInput(Rule::Bottom, rule().bottom()) .setInput(Rule::Right, rule().right()) .setInput(Rule::Top, rule().top()); d->backBlur->set(Background(ClientWindow::main().taskBarBlur(), Vector4f(1, 1, 1, 1))); add(d->backBlur); d->console = new ConsoleWidget; d->console->rule() .setInput(Rule::Left, rule().left() + d->console->shift()); add(d->console); //d->console->log().set(bg); // Position the console button and command line in the task bar. d->console->buttons().rule() .setInput(Rule::Left, rule().left()) .setInput(Rule::Bottom, rule().bottom()) .setInput(Rule::Height, rule().height()); // DE logo. d->logo = new ButtonWidget("de-button"); d->logo->setImage(style().images().image("logo.px128")); d->logo->setImageScale(.475f); d->logo->setImageFit(FitToHeight | OriginalAspectRatio); d->updateLogoButtonText(); d->logo->setWidthPolicy(ui::Expand); d->logo->setTextAlignment(AlignLeft); d->logo->rule().setInput(Rule::Height, rule().height()); add(d->logo); // Settings. ButtonWidget *conf = new ButtonWidget("conf-button"); d->conf = conf; conf->setImage(style().images().image("gear")); conf->setSizePolicy(ui::Expand, ui::Filled); conf->rule().setInput(Rule::Height, rule().height()); add(conf); // Currently loaded game. d->status = new LabelWidget; d->status->set(bg); d->status->setWidthPolicy(ui::Expand); d->status->rule().setInput(Rule::Height, rule().height()); add(d->status); d->updateStatus(); // Multiplayer. d->multi = new ButtonWidget; d->multi->hide(); // hidden when not connected d->multi->setAction(new SignalAction(this, SLOT(openMultiplayerMenu()))); d->multi->setImage(style().images().image("network")); d->multi->setTextAlignment(ui::AlignRight); d->multi->setText(tr("MP")); d->multi->setSizePolicy(ui::Expand, ui::Filled); d->multi->rule().setInput(Rule::Height, rule().height()); add(d->multi); // Taskbar height depends on the font size. rule().setInput(Rule::Height, style().fonts().font("default").height() + gap * 2); // Settings menu. add(d->configMenu = new PopupMenuWidget("conf-menu")); d->configMenu->setAnchorAndOpeningDirection(conf->rule(), ui::Up); // Multiplayer menu. add(d->multiMenu = new MultiplayerMenuWidget); d->multiMenu->setAnchorAndOpeningDirection(d->multi->rule(), ui::Up); // The DE menu. add(d->mainMenu = new PopupMenuWidget("de-menu")); d->mainMenu->setAnchorAndOpeningDirection(d->logo->rule(), ui::Up); // Game unloading confirmation submenu. ui::SubmenuItem *unloadMenu = new ui::SubmenuItem(style().images().image("close.ring"), tr("Unload Game"), ui::Left); unloadMenu->items() << new ui::Item(ui::Item::Separator, tr("Really unload the game?")) << new ui::ActionItem(tr("Unload") + " " _E(b) + tr("(discard progress)"), new SignalAction(this, SLOT(unloadGame()))) << new ui::ActionItem(tr("Cancel"), new SignalAction(&d->mainMenu->menu(), SLOT(dismissPopups()))); /* * Set up items for the config and DE menus. Some of these are shown/hidden * depending on whether a game is loaded. */ d->configMenu->items() << new ui::SubwidgetItem(style().images().image("renderer"), tr("Renderer"), ui::Left, makePopup) << new ui::SubwidgetItem(style().images().image("vr"), tr("3D & VR"), ui::Left, makePopup) << new ui::Item(ui::Item::Separator) << new ui::SubwidgetItem(style().images().image("display"), tr("Video"), ui::Left, makePopup) << new ui::SubwidgetItem(style().images().image("audio"), tr("Audio"), ui::Left, makePopup) << new ui::SubwidgetItem(style().images().image("input"), tr("Input"), ui::Left, makePopup) << new ui::SubwidgetItem(style().images().image("network"), tr("Network"), ui::Left, makePopup) << new ui::SubwidgetItem(style().images().image("updater"), tr("Updater"), ui::Left, makeUpdaterSettings); d->mainMenu->items() << new ui::ActionItem(tr("Switch Game..."), new SignalAction(this, SLOT(switchGame()))) << unloadMenu // hidden with null-game << new ui::ActionItem(tr("IWAD Folder..."), new SignalAction(this, SLOT(chooseIWADFolder()))) << new ui::Item(ui::Item::Separator) << new ui::ActionItem(tr("Multiplayer Games..."), new SignalAction(this, SLOT(showMultiplayer()))) << new ui::ActionItem(tr("Connect to Server..."), new SignalAction(this, SLOT(connectToServerManually()))) << new ui::Item(ui::Item::Separator) //<< new ui::Item(ui::Item::Separator, tr("Help")) << new ui::ActionItem(tr("Show Tutorial"), new SignalAction(this, SLOT(showTutorial()))) << new ui::VariableToggleItem(tr("Menu Annotations"), App::config("ui.showAnnotations")) << new ui::Item(ui::Item::Annotation, tr("Annotations briefly describe menu functions.")) << new ui::Item(ui::Item::Separator) << new ui::Item(ui::Item::Separator, tr("Application")) << new ui::ActionItem(tr("Check for Updates..."), new CommandAction("updateandnotify")) << new ui::ActionItem(tr("About Doomsday"), new SignalAction(this, SLOT(showAbout()))) << new ui::Item(ui::Item::Separator) << new ui::ActionItem(tr("Quit Doomsday"), new CommandAction("quit")); d->showOrHideMenuItems(); conf->setAction(new SignalAction(this, SLOT(openConfigMenu()))); d->logo->setAction(new SignalAction(this, SLOT(openMainMenu()))); // Set the initial command line layout. updateCommandLineLayout(); connect(d->console, SIGNAL(commandModeChanged()), this, SLOT(updateCommandLineLayout())); connect(d->console, SIGNAL(commandLineGotFocus()), this, SLOT(closeMainMenu())); connect(d->console, SIGNAL(commandLineGotFocus()), this, SLOT(closeConfigMenu())); } ConsoleWidget &TaskBarWidget::console() { return *d->console; } CommandWidget &TaskBarWidget::commandLine() { return d->console->commandLine(); } ButtonWidget &TaskBarWidget::logoButton() { return *d->logo; } bool TaskBarWidget::isOpen() const { return d->opened; } Rule const &TaskBarWidget::shift() { return *d->vertShift; } void TaskBarWidget::glInit() { LOG_AS("TaskBarWidget"); d->glInit(); } void TaskBarWidget::glDeinit() { d->glDeinit(); } void TaskBarWidget::viewResized() { GuiWidget::viewResized(); d->updateProjection(); } void TaskBarWidget::update() { GuiWidget::update(); d->updateLayoutMode(); } void TaskBarWidget::drawContent() { d->updateGeometry(); } bool TaskBarWidget::handleEvent(Event const &event) { Canvas &canvas = root().window().canvas(); ClientWindow &window = root().window().as(); if(!canvas.isMouseTrapped() && event.type() == Event::MouseButton && !window.hasSidebar()) { // Clicking outside the taskbar will trap the mouse automatically. MouseEvent const &mouse = event.as(); if(mouse.state() == MouseEvent::Released && !hitTest(mouse.pos())) { if(root().focus()) { // First click will remove UI focus, allowing GameWidget // to receive events. root().setFocus(0); return true; } if(App_GameLoaded()) { // Allow game to use the mouse. canvas.trapMouse(); } window.taskBar().close(); return true; } } if(event.type() == Event::MouseButton) { // Eat all button events occurring inside the task bar area. if(hitTest(event)) { return true; } } // Don't let modifier keys fall through to the game. if(isOpen() && event.isKey() && event.as().isModifier()) { // However, let the bindings system know about the modifier state. ClientApp::inputSystem().trackEvent(event); return true; } if(event.type() == Event::KeyPress) { KeyEvent const &key = event.as(); // Shift-Esc opens and closes the task bar. if(key.ddKey() == DDKEY_ESCAPE) { if(isOpen()) { // First press of Esc will just dismiss the console. if(d->console->isLogOpen() && !key.modifiers().testFlag(KeyEvent::Shift)) { d->console->commandLine().setText(""); d->console->closeLog(); root().setFocus(0); return true; } // Also closes the console log. close(); return true; } else { // Task bar is closed, so let's open it. if(key.modifiers().testFlag(KeyEvent::Shift) || !App_GameLoaded()) { if(!window.hasSidebar()) { // Automatically focus the command line, unless an editor is open. root().setFocus(&d->console->commandLine()); } open(); return true; } } return false; } } return false; } void TaskBarWidget::open() { if(!d->opened) { d->opened = true; unsetBehavior(DisableEventDispatchToChildren); d->console->zeroLogHeight(); d->vertShift->set(0, OPEN_CLOSE_SPAN); setOpacity(1, OPEN_CLOSE_SPAN); emit opened(); } // Untrap the mouse if it is trapped. if(hasRoot()) { Canvas &canvas = root().window().canvas(); d->mouseWasTrappedWhenOpening = canvas.isMouseTrapped(); if(canvas.isMouseTrapped()) { canvas.trapMouse(false); } if(!App_GameLoaded()) { root().setFocus(&d->console->commandLine()); } } } void TaskBarWidget::openAndPauseGame() { if(App_GameLoaded() && !clientPaused) { Con_Execute(CMDS_DDAY, "pause", true, false); } open(); } void TaskBarWidget::close() { if(d->opened) { d->opened = false; setBehavior(DisableEventDispatchToChildren); // Slide the task bar down. d->vertShift->set(rule().height().valuei() + style().rules().rule("unit").valuei(), OPEN_CLOSE_SPAN); setOpacity(0, OPEN_CLOSE_SPAN); d->console->closeLog(); d->console->closeMenu(); d->console->commandLine().dismissContentToHistory(); closeMainMenu(); closeConfigMenu(); // Clear focus now; callbacks/signal handlers may set the focus elsewhere. if(hasRoot()) root().setFocus(0); emit closed(); // Retrap the mouse if it was trapped when opening. if(hasRoot() && App_GameLoaded() && !root().window().as().hasSidebar()) { Canvas &canvas = root().window().canvas(); if(d->mouseWasTrappedWhenOpening) { canvas.trapMouse(); } } } } void TaskBarWidget::openConfigMenu() { d->mainMenu->close(0); d->configMenu->open(); } void TaskBarWidget::closeConfigMenu() { d->configMenu->close(); } void TaskBarWidget::openMainMenu() { d->configMenu->close(0); d->mainMenu->open(); } void TaskBarWidget::closeMainMenu() { d->mainMenu->close(); } void TaskBarWidget::chooseIWADFolder() { DENG2_ASSERT(!App_GameLoaded()); bool reload = false; // Use a native dialog to select the IWAD folder. ClientApp::app().beginNativeUIMode(); QFileDialog dlg(&ClientWindow::main(), tr("Select IWAD Folder"), App::config().gets("resource.iwadFolder", "")); dlg.setFileMode(QFileDialog::Directory); dlg.setReadOnly(true); dlg.setNameFilter("*.wad"); dlg.setLabelText(QFileDialog::Accept, tr("Select")); if(dlg.exec()) { App::config().set("resource.iwadFolder", dlg.selectedFiles().at(0)); reload = true; } ClientApp::app().endNativeUIMode(); // Reload packages and recheck for game availability. if(reload) { ClientApp::resourceSystem().updateOverrideIWADPathFromConfig(); ClientWindow::main().console().closeLogAndUnfocusCommandLine(); Con_InitProgress(200); App_Games().forgetAllResources(); App_Games().locateAllResources(); } } void TaskBarWidget::openMultiplayerMenu() { d->multiMenu->open(); } void TaskBarWidget::unloadGame() { Con_Execute(CMDS_DDAY, "unload", false, false); d->mainMenu->close(); } void TaskBarWidget::showAbout() { AboutDialog *about = new AboutDialog; about->setDeleteAfterDismissed(true); root().addOnTop(about); about->open(); } void TaskBarWidget::showUpdaterSettings() { /// @todo This has actually little to do with the taskbar. -jk UpdaterSettingsDialog *dlg = new UpdaterSettingsDialog(UpdaterSettingsDialog::WithApplyAndCheckButton); dlg->setDeleteAfterDismissed(true); root().addOnTop(dlg); dlg->open(); } void TaskBarWidget::switchGame() { GamesDialog *games = new GamesDialog(GamesDialog::ShowSingleplayerOnly); games->setDeleteAfterDismissed(true); games->exec(root()); } void TaskBarWidget::showMultiplayer() { GamesDialog *games = new GamesDialog(GamesDialog::ShowMultiplayerOnly); games->setDeleteAfterDismissed(true); if(isOpen()) { games->exec(root()); } else { root().addOnTop(games); games->open(); } } void TaskBarWidget::connectToServerManually() { ManualConnectionDialog *dlg = new ManualConnectionDialog; dlg->setDeleteAfterDismissed(true); dlg->exec(root()); } void TaskBarWidget::showTutorial() { if(BusyMode_Active()) return; // The widget will dispose of itself when finished. TutorialWidget *tutorial = new TutorialWidget; root().addOnTop(tutorial); tutorial->rule().setRect(root().viewRule()); tutorial->start(); } void TaskBarWidget::updateCommandLineLayout() { SequentialLayout layout(rule().right(), rule().top(), ui::Left); layout << *d->logo << *d->conf; if(!d->multi->behavior().testFlag(Hidden)) { layout << *d->multi; } if(!d->status->behavior().testFlag(Hidden)) { layout << *d->status; } // The command line extends the rest of the way. RuleRectangle &cmdRule = d->console->commandLine().rule(); cmdRule.setInput(Rule::Left, d->console->buttons().rule().right()) .setInput(Rule::Bottom, rule().bottom()) .setInput(Rule::Right, layout.widgets().last()->as().rule().left()); // Just use a plain background for this editor. d->console->commandLine().set(Background(style().colors().colorf("background"))); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/consolecommandwidget.cpp0000664000175000017500000000616412641367670027063 0ustar jaakkojaakko/** @file consolecommandwidget.h Text editor with a history buffer. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/consolecommandwidget.h" #include #include #include #include #include "clientapp.h" #include "dd_main.h" #include "BindContext" using namespace de; DENG_GUI_PIMPL(ConsoleCommandWidget), DENG2_OBSERVES(App, StartupComplete), DENG2_OBSERVES(App, GameChange) { Instance(Public *i) : Base(i) { App::app().audienceForStartupComplete() += this; App::app().audienceForGameChange() += this; } ~Instance() { App::app().audienceForStartupComplete() -= this; App::app().audienceForGameChange() -= this; } void appStartupCompleted() { updateLexicon(); } void currentGameChanged(game::Game const &) { updateLexicon(); } void updateLexicon() { self.setLexicon(Con_Lexicon()); } }; ConsoleCommandWidget::ConsoleCommandWidget(String const &name) : CommandWidget(name), d(new Instance(this)) { d->updateLexicon(); } void ConsoleCommandWidget::focusGained() { CommandWidget::focusGained(); ClientApp::inputSystem().context("console").activate(); } void ConsoleCommandWidget::focusLost() { CommandWidget::focusLost(); ClientApp::inputSystem().context("console").deactivate(); } bool ConsoleCommandWidget::handleEvent(Event const &event) { if(isDisabled()) return false; if(hasFocus()) { // Console bindings override normal event handling. if(ClientApp::inputSystem().tryEvent(event, "console")) { // Eaten by bindings. return true; } } return CommandWidget::handleEvent(event); } bool ConsoleCommandWidget::isAcceptedAsCommand(String const &) { // Everything is OK for a console command. return true; } void ConsoleCommandWidget::executeCommand(String const &text) { LOG_SCR_NOTE(_E(1) "> ") << text; // Execute the command right away. Con_Execute(CMDS_CONSOLE, text.toUtf8(), false, false); } void ConsoleCommandWidget::autoCompletionBegan(String const &) { // Prepare a list of annotated completions to show in the popup. QStringList const compls = suggestedCompletions(); if(compls.size() > 1) { showAutocompletionPopup(Con_AnnotatedConsoleTerms(compls)); } } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/cvarsliderwidget.cpp0000664000175000017500000000450412641367670026214 0ustar jaakkojaakko/** @file cvarsliderwidget.cpp Slider for adjusting a cvar. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/cvarsliderwidget.h" #include using namespace de; using namespace ui; DENG2_PIMPL_NOREF(CVarSliderWidget) { char const *cvar; cvar_t *var() const { cvar_t *cv = Con_FindVariable(cvar); DENG2_ASSERT(cv != 0); return cv; } }; CVarSliderWidget::CVarSliderWidget(char const *cvarPath) : d(new Instance) { d->cvar = cvarPath; // Default range and precision for floating point variables (may be altered later). if(d->var()->type == CVT_FLOAT) { if(!(d->var()->flags & (CVF_NO_MIN | CVF_NO_MAX))) { setRange(Rangef(d->var()->min, d->var()->max)); } setPrecision(2); } else { if(!(d->var()->flags & (CVF_NO_MIN | CVF_NO_MAX))) { setRange(Rangei(d->var()->min, d->var()->max)); } } updateFromCVar(); connect(this, SIGNAL(valueChangedByUser(double)), this, SLOT(setCVarValueFromWidget())); } char const *CVarSliderWidget::cvarPath() const { return d->cvar; } void CVarSliderWidget::updateFromCVar() { cvar_t *var = d->var(); if(var->type == CVT_FLOAT) { setValue(CVar_Float(var)); } else { setValue(CVar_Integer(var)); } } void CVarSliderWidget::setCVarValueFromWidget() { cvar_t *var = d->var(); if(var->type == CVT_FLOAT) { CVar_SetFloat(d->var(), value()); } else { CVar_SetInteger(d->var(), round(value())); } } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/multiplayermenuwidget.cpp0000664000175000017500000000612712641367670027315 0ustar jaakkojaakko/** @file multiplayermenuwidget.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/multiplayermenuwidget.h" #include "network/serverlink.h" #include "clientapp.h" #include "CommandAction" #include #include #include using namespace de; enum { POS_STATUS = 1 }; DENG_GUI_PIMPL(MultiplayerMenuWidget) , DENG2_OBSERVES(ServerLink, Join) , DENG2_OBSERVES(ServerLink, Leave) { QTimer timer; Instance(Public *i) : Base(i) { timer.setInterval(1000); link().audienceForJoin += this; link().audienceForLeave += this; } ~Instance() { link().audienceForJoin -= this; link().audienceForLeave -= this; } void networkGameJoined() { /*self.menu().organizer().itemWidget(POS_SERVER_ADDRESS)->as() .setText(_E(l) + tr("Server:") + _E(.) + " " + link().address().asText());*/ } void networkGameLeft() { } static ServerLink &link() { return ClientApp::serverLink(); } }; MultiplayerMenuWidget::MultiplayerMenuWidget() : PopupMenuWidget("multiplayer-menu"), d(new Instance(this)) { connect(&d->timer, SIGNAL(timeout()), this, SLOT(updateElapsedTime())); items() << new ui::ActionItem(tr("Disconnect"), new CommandAction("net disconnect")) //<< new ui::Item(ui::Item::Separator, tr("Connection:")) << new ui::Item(ui::Item::ShownAsLabel, ""); // sv address & time } void MultiplayerMenuWidget::updateElapsedTime() { if(d->link().status() != ServerLink::Connected) return; TimeDelta const elapsed = d->link().connectedAt().since(); items().at(POS_STATUS).setLabel( _E(s)_E(l) + tr("Server:") + _E(.) " " + d->link().address().asText() + "\n" _E(l) + tr("Connected:") + _E(.) + String(" %1:%2:%3") .arg(int(elapsed.asHours())) .arg(int(elapsed.asMinutes()) % 60, 2, 10, QLatin1Char('0')) .arg(int(elapsed) % 60, 2, 10, QLatin1Char('0'))); } void MultiplayerMenuWidget::preparePanelForOpening() { d->timer.start(); updateElapsedTime(); PopupMenuWidget::preparePanelForOpening(); } void MultiplayerMenuWidget::panelClosing() { d->timer.stop(); PopupMenuWidget::panelClosing(); } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/sessionmenuwidget.cpp0000664000175000017500000000760112641367670026427 0ustar jaakkojaakko/** @file sessionmenuwidget.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/sessionmenuwidget.h" #include using namespace de; DENG_GUI_PIMPL(SessionMenuWidget) , DENG2_OBSERVES(ChildWidgetOrganizer, WidgetCreation) , DENG2_OBSERVES(ButtonWidget, Press) { GameFilterWidget *filter; Instance(Public *i) : Base(i) , filter(0) {} static bool sorter(ui::Item const &a, ui::Item const &b) { SessionItem const *x = a.maybeAs(); SessionItem const *y = b.maybeAs(); if(x && y) { int cmp = x->sortKey().compareWithoutCase(y->sortKey()); if(cmp < 0) return true; if(cmp > 0) return false; // Secondarily by title. if(x->menu().filter().sortOrder() != GameFilterWidget::SortByTitle) { return x->title().compareWithoutCase(y->title()) < 0; } } return false; } void sortSessions() { self.items().stableSort(sorter); } void widgetCreatedForItem(GuiWidget &widget, ui::Item const &) { // Observe the button for presses. widget.as().loadButton().audienceForPress() += this; } void buttonPressed(ButtonWidget &button) { ui::Item const *item = self.organizer().findItemForWidget(button.parentWidget()->as()); DENG2_ASSERT(item != 0); emit self.sessionSelected(item); } }; SessionMenuWidget::SessionMenuWidget(String const &name) : MenuWidget(name), d(new Instance(this)) { enableScrolling(false); setGridSize(1, ui::Filled, 0, ui::Expand); organizer().setWidgetFactory(*this); organizer().audienceForWidgetCreation() += d; } void SessionMenuWidget::setFilter(GameFilterWidget *filter) { if(d->filter) { disconnect(d->filter, SIGNAL(sortOrderChanged()), this, SLOT(sort())); } d->filter = filter; if(filter) { connect(filter, SIGNAL(sortOrderChanged()), this, SLOT(sort())); } } GameFilterWidget &SessionMenuWidget::filter() const { DENG2_ASSERT(d->filter != 0); return *d->filter; } void SessionMenuWidget::setColumns(int numberOfColumns) { if(layout().maxGridSize().x != numberOfColumns) { setGridSize(numberOfColumns, ui::Filled, 0, ui::Expand); } } void SessionMenuWidget::sort() { if(d->filter) { d->sortSessions(); } } //-------------------------------------------------------------------------------------- DENG2_PIMPL_NOREF(SessionMenuWidget::SessionItem) { SessionMenuWidget *owner; }; SessionMenuWidget::SessionItem::SessionItem(SessionMenuWidget &owner) : d(new Instance) { d->owner = &owner; } SessionMenuWidget::SessionItem::~SessionItem() {} SessionMenuWidget &SessionMenuWidget::SessionItem::menu() const { return *d->owner; } String SessionMenuWidget::SessionItem::sortKey() const { switch(menu().filter().sortOrder()) { case GameFilterWidget::SortByTitle: return title(); case GameFilterWidget::SortByIdentityKey: return gameIdentityKey(); } return ""; } doomsday-stable-1.15.7/doomsday/client/src/ui/widgets/gamesessionwidget.cpp0000664000175000017500000001171412641367670026374 0ustar jaakkojaakko/** @file gamesessionwidget.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/widgets/gamesessionwidget.h" #include #include using namespace de; DENG2_PIMPL(GameSessionWidget) , DENG2_OBSERVES(de::ButtonWidget, Press) , DENG2_OBSERVES(App, GameUnload) { PopupStyle popupStyle; ButtonWidget *load; ButtonWidget *info; ButtonWidget *funcs = nullptr; DocumentPopupWidget *doc = nullptr; PopupMenuWidget *menu = nullptr; Instance(Public *i, PopupStyle ps, ui::Direction popupOpeningDirection) : Base(i) , popupStyle(ps) { // Set up the buttons. self.add(load = new ButtonWidget); self.add(info = new ButtonWidget); if(popupStyle == PopupMenu) { self.add(funcs = new ButtonWidget); funcs->audienceForPress() += this; } load->disable(); load->setBehavior(Widget::ContentClipping); load->setAlignment(ui::AlignLeft); load->setTextAlignment(ui::AlignRight); load->setTextLineAlignment(ui::AlignLeft); load->setImageScale(self.toDevicePixels(1)); /// @todo We don't have 2x game logos. info->setWidthPolicy(ui::Expand); info->setAlignment(ui::AlignBottom); info->setText(_E(s)_E(B) + tr("...")); info->audienceForPress() += this; // Set up the info/actions popup widget. self.add(doc = new DocumentPopupWidget); doc->document().setMaximumLineWidth(doc->style().rules().rule("document.popup.width").valuei()); if(popupStyle == PopupMenu) { self.add(menu = new PopupMenuWidget); menu->setAnchorAndOpeningDirection(funcs->rule(), ui::Right); } doc->setAnchorAndOpeningDirection(info->rule(), popupOpeningDirection); App::app().audienceForGameUnload() += this; } ~Instance() { App::app().audienceForGameUnload() -= this; if(menu) menu->dismiss(); doc->dismiss(); } void aboutToUnloadGame(game::Game const &) { doc->close(0); if(menu) menu->close(0); } void buttonPressed(ButtonWidget &btn) { if(&btn == info) { self.updateInfoContent(); doc->open(); } else { menu->open(); } } }; GameSessionWidget::GameSessionWidget(PopupStyle ps, ui::Direction popupOpeningDirection) : d(new Instance(this, ps, popupOpeningDirection)) { Font const &font = style().fonts().font("default"); rule().setInput(Rule::Height, OperatorRule::maximum(font.lineSpacing() * 3 + font.height() + margins().height(), d->load->contentHeight())); // Button for extra information. d->load->rule() .setInput(Rule::Left, rule().left()) .setInput(Rule::Top, rule().top()) .setInput(Rule::Bottom, rule().bottom()) .setInput(Rule::Right, d->info->rule().left()); d->info->rule() .setInput(Rule::Top, rule().top()) .setInput(Rule::Right, rule().right()) .setInput(Rule::Bottom, rule().bottom()); if(d->popupStyle == PopupMenu) { d->funcs->rule() .setInput(Rule::Top, rule().top()) .setInput(Rule::Right, rule().right()) .setInput(Rule::Height, d->info->rule().width()) .setInput(Rule::Width, d->info->rule().width()); d->info->rule().setInput(Rule::Top, d->funcs->rule().bottom()); } } GameSessionWidget::PopupStyle GameSessionWidget::popupStyle() const { return d->popupStyle; } ButtonWidget &GameSessionWidget::loadButton() { return *d->load; } ButtonWidget &GameSessionWidget::infoButton() { return *d->info; } ButtonWidget &GameSessionWidget::menuButton() { return *d->funcs; } DocumentWidget &GameSessionWidget::document() { DENG2_ASSERT(d->doc); return d->doc->document(); } PopupMenuWidget &GameSessionWidget::menu() { DENG2_ASSERT(d->menu); return *d->menu; } void GameSessionWidget::updateInfoContent() { // overridden by derived classes } doomsday-stable-1.15.7/doomsday/client/src/ui/commandbinding.cpp0000664000175000017500000003275712641367670024170 0ustar jaakkojaakko/** @file commandbinding.cpp Command binding record accessor. * * @authors Copyright © 2009-2014 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/commandbinding.h" #include #include #include #include #include "CommandAction" #include "clientapp.h" #include "world/p_players.h" // P_ConsoleToLocal #include "ui/b_util.h" #include "ui/inputdeviceaxiscontrol.h" #include "ui/inputdevicebuttoncontrol.h" #include "ui/inputdevicehatcontrol.h" using namespace de; static InputSystem &inputSys() { return ClientApp::inputSystem(); } void CommandBinding::resetToDefaults() { Binding::resetToDefaults(); def().addNumber("deviceId", -1); def().addNumber("controlId", -1); def().addNumber("type", int(E_TOGGLE)); ///< Type of event. def().addText ("symbolicName", ""); def().addNumber("test", int(None)); def().addNumber("pos", 0); def().addText ("command", ""); ///< Command to execute. } String CommandBinding::composeDescriptor() { LOG_AS("CommandBinding"); if(!*this) return ""; String str = B_ControlDescToString(geti("deviceId"), ddeventtype_t(geti("type")), geti("controlId")); switch(geti("type")) { case E_TOGGLE: str += B_ButtonStateToString(ControlTest(geti("test"))); break; case E_AXIS: str += B_AxisPositionToString(ControlTest(geti("test")), getf("pos")); break; case E_ANGLE: str += B_HatAngleToString(getf("pos")); break; case E_SYMBOLIC: str += "-" + gets("symbolicName"); break; default: DENG2_ASSERT(!"CommandBinding::composeDescriptor: Unknown bind.type"); break; } // Append any state conditions. ArrayValue const &conds = def().geta("condition"); DENG2_FOR_EACH_CONST(ArrayValue::Elements, i, conds.elements()) { str += " + " + B_ConditionToString(*(*i)->as().record()); } return str; } /** * Parse the main part of the event descriptor, with no conditions included. */ static bool doConfigure(CommandBinding &bind, char const *eventDesc, char const *command) { DENG2_ASSERT(eventDesc); //InputSystem &isys = ClientApp::inputSystem(); bind.resetToDefaults(); // Take a copy of the command string. bind.def().set("command", String(command)); // Parse the event descriptor. AutoStr *str = AutoStr_NewStd(); // First, we expect to encounter a device name. eventDesc = Str_CopyDelim(str, eventDesc, '-'); if(!Str_CompareIgnoreCase(str, "key")) { bind.def().set("deviceId", IDEV_KEYBOARD); bind.def().set("type", int(E_TOGGLE)); // Keyboards only have toggles (as far as we know). // Parse the key. eventDesc = Str_CopyDelim(str, eventDesc, '-'); int controlId = 0; bool ok = B_ParseKeyId(controlId, Str_Text(str)); if(!ok) return false; bind.def().set("controlId", controlId); // The final part of a key event is the state of the key toggle. eventDesc = Str_CopyDelim(str, eventDesc, '-'); Binding::ControlTest test = Binding::ControlTest::None; ok = B_ParseButtonState(test, Str_Text(str)); if(!ok) return false; bind.def().set("test", int(test)); } else if(!Str_CompareIgnoreCase(str, "mouse")) { bind.def().set("deviceId", IDEV_MOUSE); // Next comes a button or axis name. eventDesc = Str_CopyDelim(str, eventDesc, '-'); ddeventtype_t type = E_TOGGLE; int controlId = 0; bool ok = B_ParseMouseTypeAndId(type, controlId, Str_Text(str)); if(!ok) return false; bind.def().set("type", type); bind.def().set("controlId", controlId); // The last part determines the toggle state or the axis position. eventDesc = Str_CopyDelim(str, eventDesc, '-'); switch(bind.geti("type")) { case E_TOGGLE: { Binding::ControlTest test = Binding::ControlTest::None; ok = B_ParseButtonState(test, Str_Text(str)); if(!ok) return false; bind.def().set("test", int(test)); break; } case E_AXIS: { Binding::ControlTest test = Binding::ControlTest::None; float pos; ok = B_ParseAxisPosition(test, pos, Str_Text(str)); if(!ok) return false; bind.def().set("test", int(test)); bind.def().set("pos", pos); break; } default: DENG2_ASSERT(!"InputSystem::configure: Invalid bind.type"); break; } } else if(!Str_CompareIgnoreCase(str, "joy") || !Str_CompareIgnoreCase(str, "head")) { bind.def().set("deviceId", (!Str_CompareIgnoreCase(str, "joy")? IDEV_JOY1 : IDEV_HEAD_TRACKER)); // Next part defined button, axis, or hat. eventDesc = Str_CopyDelim(str, eventDesc, '-'); ddeventtype_t type = E_TOGGLE; int controlId = 0; bool ok = B_ParseJoystickTypeAndId(type, controlId, bind.geti("deviceId"), Str_Text(str)); if(!ok) return false; bind.def().set("type", type); bind.def().set("controlId", controlId); // What is the state of the toggle, axis, or hat? eventDesc = Str_CopyDelim(str, eventDesc, '-'); switch(bind.geti("type")) { case E_TOGGLE: { Binding::ControlTest test = Binding::ControlTest::None; ok = B_ParseButtonState(test, Str_Text(str)); if(!ok) return false; bind.def().set("test", int(test)); break; } case E_AXIS: { Binding::ControlTest test = Binding::ControlTest::None; float pos; ok = B_ParseAxisPosition(test, pos, Str_Text(str)); if(!ok) return false; bind.def().set("test", int(test)); bind.def().set("pos", pos); break; } case E_ANGLE: { float pos; ok = B_ParseHatAngle(pos, Str_Text(str)); if(!ok) return false; bind.def().set("pos", pos); break; } default: DENG2_ASSERT(!"InputSystem::configure: Invalid bind.type") break; } } else if(!Str_CompareIgnoreCase(str, "sym")) { // A symbolic event. bind.def().set("type", int(E_SYMBOLIC)); bind.def().set("deviceId", -1); bind.def().set("symbolicName", eventDesc); eventDesc = nullptr; } else { LOG_INPUT_WARNING("Unknown device \"%s\"") << Str_Text(str); return false; } // Anything left that wasn't used? if(eventDesc) { LOG_INPUT_WARNING("Unrecognized \"%s\"") << eventDesc; return false; } // No errors detected. return true; } void CommandBinding::configure(char const *eventDesc, char const *command, bool assignNewId) { DENG2_ASSERT(eventDesc); LOG_AS("CommandBinding"); // The first part specifies the event condition. AutoStr *str = AutoStr_NewStd(); eventDesc = Str_CopyDelim(str, eventDesc, '+'); if(!doConfigure(*this, Str_Text(str), command)) { throw ConfigureError("CommandBinding::configure", "Descriptor parse error"); } // Any conditions? def()["condition"].value().clear(); while(eventDesc) { // A new condition. eventDesc = Str_CopyDelim(str, eventDesc, '+'); Record &cond = addCondition(); if(!B_ParseBindingCondition(cond, Str_Text(str))) { throw ConfigureError("CommandBinding::configure", "Descriptor parse error"); } } if(assignNewId) { def().set("id", newIdentifier()); } } /** * Substitute placeholders in a command string. Placeholders consist of two characters, * the first being a %. Use %% to output a plain % character. * * - %i: id member of the event * - %p: (symbolic events only) local player number * * @param command Original command string with the placeholders. * @param event Event data. * @param out String with placeholders replaced. */ static void substituteInCommand(String const &command, ddevent_t const &event, ddstring_t *out) { DENG2_ASSERT(out); Block const str = command.toUtf8(); for(char const *ptr = str.constData(); *ptr; ptr++) { if(*ptr == '%') { // Escape. ptr++; // Must have another character in the placeholder. if(!*ptr) break; if(*ptr == 'i') { int id = 0; switch(event.type) { case E_TOGGLE: id = event.toggle.id; break; case E_AXIS: id = event.axis.id; break; case E_ANGLE: id = event.angle.id; break; case E_SYMBOLIC: id = event.symbolic.id; break; default: break; } Str_Appendf(out, "%i", id); } else if(*ptr == 'p') { int id = 0; if(event.type == E_SYMBOLIC) { id = P_ConsoleToLocal(event.symbolic.id); } Str_Appendf(out, "%i", id); } else if(*ptr == '%') { Str_AppendChar(out, *ptr); } continue; } Str_AppendChar(out, *ptr); } } Action *CommandBinding::makeAction(ddevent_t const &event, BindContext const &context, bool respectHigherContexts) const { if(geti("type") != event.type) return nullptr; InputDevice const *dev = nullptr; if(event.type != E_SYMBOLIC) { if(geti("deviceId") != event.device) return nullptr; dev = inputSys().devicePtr(geti("deviceId")); if(!dev || !dev->isActive()) { // The device is not active, there is no way this could get executed. return nullptr; } } switch(event.type) { case E_TOGGLE: { if(geti("controlId") != event.toggle.id) return nullptr; DENG2_ASSERT(dev); InputDeviceButtonControl &button = dev->button(geti("controlId")); if(respectHigherContexts) { if(button.bindContext() != &context) return nullptr; // Shadowed by a more important active class. } // We're checking it, so clear the triggered flag. button.setBindContextAssociation(InputDeviceControl::Triggered, UnsetFlags); // Is the state as required? switch(ControlTest(geti("test"))) { case ButtonStateAny: // Passes no matter what. break; case ButtonStateDown: if(event.toggle.state != ETOG_DOWN) return nullptr; break; case ButtonStateUp: if(event.toggle.state != ETOG_UP) return nullptr; break; case ButtonStateRepeat: if(event.toggle.state != ETOG_REPEAT) return nullptr; break; case ButtonStateDownOrRepeat: if(event.toggle.state == ETOG_UP) return nullptr; break; default: return nullptr; } break; } case E_AXIS: if(geti("controlId") != event.axis.id) return nullptr; DENG2_ASSERT(dev); if(dev->axis(geti("controlId")).bindContext() != &context) return nullptr; // Shadowed by a more important active class. // Is the position as required? if(!B_CheckAxisPosition(ControlTest(geti("test")), getf("pos"), inputSys().device(event.device).axis(event.axis.id) .translateRealPosition(event.axis.pos))) return nullptr; break; case E_ANGLE: if(geti("controlId") != event.angle.id) return nullptr; DENG2_ASSERT(dev); if(dev->hat(geti("controlId")).bindContext() != &context) return nullptr; // Shadowed by a more important active class. // Is the position as required? if(event.angle.pos != getf("pos")) return nullptr; break; case E_SYMBOLIC: if(gets("symbolicName").compareWithCase(event.symbolic.name)) return nullptr; break; default: return nullptr; } // Any conditions on the current state of the input devices? ArrayValue const &conds = def().geta("condition"); DENG2_FOR_EACH_CONST(ArrayValue::Elements, i, conds.elements()) { if(!B_CheckCondition((*i)->as().record(), 0, &context)) return nullptr; } // Substitute parameters in the command. AutoStr *command = Str_Reserve(AutoStr_NewStd(), gets("command").length()); substituteInCommand(gets("command"), event, command); return new CommandAction(Str_Text(command), CMDS_BIND); } doomsday-stable-1.15.7/doomsday/client/src/ui/b_util.cpp0000664000175000017500000007062012641367670022464 0ustar jaakkojaakko/** @file b_util.cpp Input system, binding utilities. * * @authors Copyright © 2009-2013 Jaakko Keränen * @authors Copyright © 2007-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "ui/b_util.h" #include #include #include "clientapp.h" #include "BindContext" #include "CommandBinding" #include "ImpulseBinding" #include "ui/inputdevice.h" #include "ui/inputdeviceaxiscontrol.h" #include "ui/inputdevicebuttoncontrol.h" #include "ui/inputdevicehatcontrol.h" #include "network/net_main.h" // netGame using namespace de; byte zeroControlUponConflict = true; static float STAGE_THRESHOLD = 6.f/35; static float STAGE_FACTOR = .5f; static inline InputSystem &inputSys() { return ClientApp::inputSystem(); } bool B_ParseButtonState(Binding::ControlTest &test, char const *toggleName) { DENG2_ASSERT(toggleName); if(!qstrlen(toggleName) || !qstricmp(toggleName, "down")) { test = Binding::ButtonStateDown; // this is the default, if omitted return true; } if(!qstricmp(toggleName, "undefined")) { test = Binding::ButtonStateAny; return true; } if(!qstricmp(toggleName, "repeat")) { test = Binding::ButtonStateRepeat; return true; } if(!qstricmp(toggleName, "press")) { test = Binding::ButtonStateDownOrRepeat; return true; } if(!qstricmp(toggleName, "up")) { test = Binding::ButtonStateUp; return true; } LOG_INPUT_WARNING("\"%s\" is not a toggle state") << toggleName; return false; // Not recognized. } bool B_ParseAxisPosition(Binding::ControlTest &test, float &pos, char const *desc) { DENG2_ASSERT(desc); if(!qstrnicmp(desc, "within", 6) && qstrlen(desc) > 6) { test = Binding::AxisPositionWithin; pos = String((desc + 6)).toFloat(); return true; } if(!qstrnicmp(desc, "beyond", 6) && qstrlen(desc) > 6) { test = Binding::AxisPositionBeyond; pos = String((desc + 6)).toFloat(); return true; } if(!qstrnicmp(desc, "pos", 3) && qstrlen(desc) > 3) { test = Binding::AxisPositionBeyondPositive; pos = String((desc + 3)).toFloat(); return true; } if(!qstrnicmp(desc, "neg", 3) && qstrlen(desc) > 3) { test = Binding::AxisPositionBeyondNegative; pos = -String((desc + 3)).toFloat(); return true; } LOG_INPUT_WARNING("Axis position \"%s\" is invalid") << desc; return false; } bool B_ParseModifierId(int &id, char const *desc) { DENG2_ASSERT(desc); id = String(desc).toInt() - 1 + CTL_MODIFIER_1; return (id >= CTL_MODIFIER_1 && id <= CTL_MODIFIER_4); } bool B_ParseKeyId(int &id, char const *desc) { DENG2_ASSERT(desc); LOG_AS("B_ParseKeyId"); // The possibilies: symbolic key name, or "codeNNN". if(!qstrnicmp(desc, "code", 4) && qstrlen(desc) == 7) { // Hexadecimal? if(desc[4] == 'x' || desc[4] == 'X') { id = String((desc + 5)).toInt(nullptr, 16); return true; } // Decimal. id = String((desc + 4)).toInt(); if(id > 0 && id <= 255) return true; LOGDEV_INPUT_WARNING("Key code %i out of range") << id; return false; } // Symbolic key name. id = B_KeyForShortName(desc); if(id) return true; LOG_INPUT_WARNING("Unknown key \"%s\"") << desc; return false; } bool B_ParseMouseTypeAndId(ddeventtype_t &type, int &id, char const *desc) { DENG2_ASSERT(desc); InputDevice const &mouse = inputSys().device(IDEV_MOUSE); // Maybe it's one of the named buttons? id = mouse.toButtonId(desc); if(id >= 0) { type = E_TOGGLE; return true; } // Perhaps a generic button? if(!qstrnicmp(desc, "button", 6) && qstrlen(desc) > 6) { type = E_TOGGLE; id = String((desc + 6)).toInt() - 1; if(mouse.hasButton(id)) return true; LOG_INPUT_WARNING("\"%s\" button %i does not exist") << mouse.title() << id; return false; } // Must be an axis, then. type = E_AXIS; id = mouse.toAxisId(desc); if(id >= 0) return true; LOG_INPUT_WARNING("\"%s\" axis \"%s\" does not exist") << mouse.title() << desc; return false; } bool B_ParseDeviceAxisTypeAndId(ddeventtype_t &type, int &id, InputDevice const &device, char const *desc) { DENG2_ASSERT(desc); type = E_AXIS; id = device.toAxisId(desc); if(id >= 0) return true; LOG_INPUT_WARNING("Axis \"%s\" is not defined in device '%s'") << desc << device.name(); return false; } bool B_ParseJoystickTypeAndId(ddeventtype_t &type, int &id, int deviceId, char const *desc) { InputDevice &device = inputSys().device(deviceId); if(!qstrnicmp(desc, "button", 6) && qstrlen(desc) > 6) { type = E_TOGGLE; id = String((desc + 6)).toInt() - 1; if(device.hasButton(id)) return true; LOG_INPUT_WARNING("\"%s\" button %i does not exist") << device.title() << id; return false; } if(!qstrnicmp(desc, "hat", 3) && qstrlen(desc) > 3) { type = E_ANGLE; id = String((desc + 3)).toInt() - 1; if(device.hasHat(id)) return true; LOG_INPUT_WARNING("\"%s\" hat %i does not exist") << device.title() << id; return false; } if(!qstricmp(desc, "hat")) { type = E_ANGLE; id = 0; return true; } // Try to find the axis. return B_ParseDeviceAxisTypeAndId(type, id, device, desc); } bool B_ParseHatAngle(float &pos, char const *desc) { DENG2_ASSERT(desc); if(!qstricmp(desc, "center")) { pos = -1; return true; } if(!qstrnicmp(desc, "angle", 5) && qstrlen(desc) > 5) { pos = String((desc + 5)).toFloat(); return true; } LOG_INPUT_WARNING("Angle position \"%s\" is invalid") << desc; return false; } bool B_ParseBindingCondition(Record &cond, char const *desc) { DENG2_ASSERT(desc); // First, we expect to encounter a device name. AutoStr *str = AutoStr_NewStd(); desc = Str_CopyDelim(str, desc, '-'); if(!Str_CompareIgnoreCase(str, "multiplayer")) { // This is only intended for multiplayer games. cond.set("type", int(Binding::GlobalState)); cond.set("multiplayer", true); } else if(!Str_CompareIgnoreCase(str, "modifier")) { cond.set("type", int(Binding::ModifierState)); cond.set("device", -1); // not used // Parse the modifier number. desc = Str_CopyDelim(str, desc, '-'); int id = 0; bool ok = B_ParseModifierId(id, Str_Text(str)); if(!ok) return false; cond.set("id", id); // The final part of a modifier is the state. desc = Str_CopyDelim(str, desc, '-'); Binding::ControlTest test = Binding::None; ok = B_ParseButtonState(test, Str_Text(str)); if(!ok) return false; cond.set("test", int(test)); } else if(!Str_CompareIgnoreCase(str, "key")) { cond.set("type", int(Binding::ButtonState)); cond.set("device", int(IDEV_KEYBOARD)); // Parse the key. desc = Str_CopyDelim(str, desc, '-'); int id = 0; bool ok = B_ParseKeyId(id, Str_Text(str)); if(!ok) return false; cond.set("id", id); // The final part of a key event is the state of the key toggle. desc = Str_CopyDelim(str, desc, '-'); Binding::ControlTest test = Binding::None; ok = B_ParseButtonState(test, Str_Text(str)); if(!ok) return false; cond.set("test", int(test)); } else if(!Str_CompareIgnoreCase(str, "mouse")) { cond.set("device", int(IDEV_MOUSE)); // What is being targeted? desc = Str_CopyDelim(str, desc, '-'); ddeventtype_t type = E_TOGGLE; int id = 0; bool ok = B_ParseMouseTypeAndId(type, id, Str_Text(str)); if(!ok) return false; cond.set("type", int(type)); cond.set("id", id); desc = Str_CopyDelim(str, desc, '-'); if(type == E_TOGGLE) { cond.set("type", int(Binding::ButtonState)); Binding::ControlTest test = Binding::None; ok = B_ParseButtonState(test, Str_Text(str)); if(!ok) return false; cond.set("test", int(test)); } else if(type == E_AXIS) { cond.set("type", int(Binding::AxisState)); Binding::ControlTest test = Binding::None; float pos; ok = B_ParseAxisPosition(test, pos, Str_Text(str)); if(!ok) return false; cond.set("test", int(test)); cond.set("pos", pos); } } else if(!Str_CompareIgnoreCase(str, "joy") || !Str_CompareIgnoreCase(str, "head")) { cond.set("device", int(!Str_CompareIgnoreCase(str, "joy")? IDEV_JOY1 : IDEV_HEAD_TRACKER)); // What is being targeted? desc = Str_CopyDelim(str, desc, '-'); ddeventtype_t type = E_TOGGLE; int id = 0; bool ok = B_ParseJoystickTypeAndId(type, id, cond.geti("device"), Str_Text(str)); if(!ok) return false; cond.set("type", int(type)); desc = Str_CopyDelim(str, desc, '-'); if(type == E_TOGGLE) { cond.set("type", int(Binding::ButtonState)); Binding::ControlTest test = Binding::None; ok = B_ParseButtonState(test, Str_Text(str)); if(!ok) return false; cond.set("test", int(test)); } else if(type == E_AXIS) { cond.set("type", int(Binding::AxisState)); Binding::ControlTest test = Binding::None; float pos = 0; ok = B_ParseAxisPosition(test, pos, Str_Text(str)); if(!ok) return false; cond.set("test", int(test)); cond.set("pos", pos); } else // Angle. { cond.set("type", int(Binding::HatState)); float pos = 0; ok = B_ParseHatAngle(pos, Str_Text(str)); if(!ok) return false; cond.set("pos", pos); } } else { LOG_INPUT_WARNING("Unknown input device \"%s\"") << Str_Text(str); return false; } // Check for valid button state tests. if(cond.geti("type") == Binding::ButtonState) { if(cond.geti("test") != Binding::ButtonStateUp && cond.geti("test") != Binding::ButtonStateDown) { LOG_INPUT_WARNING("\"%s\": Button condition can only be 'up' or 'down'") << desc; return false; } } // Finally, there may be the negation at the end. desc = Str_CopyDelim(str, desc, '-'); if(!Str_CompareIgnoreCase(str, "not")) { cond.set("negate", true); } // Anything left that wasn't used? if(!desc) return true; // No errors detected. LOG_INPUT_WARNING("Unrecognized condition \"%s\"") << desc; return false; } bool B_CheckAxisPosition(Binding::ControlTest test, float testPos, float pos) { switch(test) { case Binding::AxisPositionWithin: return !((pos > 0 && pos > testPos) || (pos < 0 && pos < -testPos)); case Binding::AxisPositionBeyond: return ((pos > 0 && pos >= testPos) || (pos < 0 && pos <= -testPos)); case Binding::AxisPositionBeyondPositive: return !(pos < testPos); case Binding::AxisPositionBeyondNegative: return !(pos > -testPos); default: break; } return false; } bool B_CheckCondition(Record const *cond, int localNum, BindContext const *context) { DENG2_ASSERT(cond); bool const fulfilled = !cond->getb("negate"); switch(cond->geti("type")) { case Binding::GlobalState: if(cond->getb("multiplayer") && netGame) return fulfilled; break; case Binding::AxisState: { InputDeviceAxisControl const &axis = inputSys().device(cond->geti("device")).axis(cond->geti("id")); if(B_CheckAxisPosition(Binding::ControlTest(cond->geti("test")), cond->getf("pos"), axis.position())) { return fulfilled; } break; } case Binding::ButtonState: { InputDeviceButtonControl const &button = inputSys().device(cond->geti("device")).button(cond->geti("id")); bool isDown = button.isDown(); if(( isDown && cond->geti("test") == Binding::ButtonStateDown) || (!isDown && cond->geti("test") == Binding::ButtonStateUp)) { return fulfilled; } break; } case Binding::HatState: { InputDeviceHatControl const &hat = inputSys().device(cond->geti("device")).hat(cond->geti("id")); if(hat.position() == cond->getf("pos")) { return fulfilled; } break; } case Binding::ModifierState: if(context) { // Evaluate the current state of the modifier (in this context). float pos = 0, relative = 0; B_EvaluateImpulseBindings(context, localNum, cond->geti("id"), &pos, &relative, false /*no triggered*/); if((cond->geti("test") == Binding::ButtonStateDown && fabs(pos) > .5) || (cond->geti("test") == Binding::ButtonStateUp && fabs(pos) < .5)) { return fulfilled; } } break; default: DENG2_ASSERT(!"B_CheckCondition: Unknown cond->type"); break; } return !fulfilled; } bool B_EqualConditions(Record const &a, Record const &b) { return (a.geti("type") == b.geti("type") && a.geti("test") == b.geti("test") && a.geti("device") == b.geti("device") && a.geti("id") == b.geti("id") && de::fequal(a.getf("pos"), b.getf("pos")) && a.getb("negate") == b.getb("negate") && a.getb("multiplayer") == b.getb("multiplayer")); } /// @todo: Belongs in BindContext? -ds void B_EvaluateImpulseBindings(BindContext const *context, int localNum, int impulseId, float *pos, float *relativeOffset, bool allowTriggered) { DENG2_ASSERT(context); // Why call without one? DENG2_ASSERT(pos && relativeOffset); *pos = 0; *relativeOffset = 0; if(localNum < 0 || localNum >= DDMAXPLAYERS) return; // No local player specified. uint const nowTime = Timer_RealMilliseconds(); bool conflicted[NUM_IBD_TYPES]; de::zap(conflicted); bool appliedState[NUM_IBD_TYPES]; de::zap(appliedState); context->forAllImpulseBindings(localNum, [&] (Record &rec) { // Wrong impulse? ImpulseBinding bind(rec); if(bind.geti("impulseId") != impulseId) return LoopContinue; // If the binding has conditions, they may prevent using it. bool skip = false; ArrayValue const &conds = bind.geta("condition"); DENG2_FOR_EACH_CONST(ArrayValue::Elements, i, conds.elements()) { if(!B_CheckCondition((*i)->as().record(), localNum, context)) { skip = true; break; } } if(skip) return LoopContinue; // Get the device. InputDevice const *device = inputSys().devicePtr(bind.geti("deviceId")); if(!device || !device->isActive()) return LoopContinue; // Not available. // Get the control. InputDeviceControl *ctrl = nullptr; switch(bind.geti("type")) { case IBD_AXIS: ctrl = &device->axis (bind.geti("controlId")); break; case IBD_TOGGLE: ctrl = &device->button(bind.geti("controlId")); break; case IBD_ANGLE: ctrl = &device->hat (bind.geti("controlId")); break; default: DENG2_ASSERT(!"B_EvaluateImpulseBindings: Invalid bind.type"); break; } float devicePos = 0; float deviceOffset = 0; uint deviceTime = 0; if(auto *axis = ctrl->maybeAs()) { if(context && axis->bindContext() != context) { if(axis->hasBindContext() && !axis->bindContext()->findImpulseBinding(bind.geti("deviceId"), IBD_AXIS, bind.geti("controlId"))) { // The overriding context doesn't bind to the axis, though. if(axis->type() == InputDeviceAxisControl::Pointer) { // Reset the relative accumulation. axis->setPosition(0); } } return LoopContinue; // Shadowed by a more important active class. } // Expired? if(!(axis->bindContextAssociation() & InputDeviceControl::Expired)) { if(axis->type() == InputDeviceAxisControl::Pointer) { deviceOffset = axis->position(); axis->setPosition(0); } else { devicePos = axis->position(); } deviceTime = axis->time(); } } if(auto *button = ctrl->maybeAs()) { if(context && button->bindContext() != context) return LoopContinue; // Shadowed by a more important active context. // Expired? if(!(button->bindContextAssociation() & InputDeviceControl::Expired)) { devicePos = (button->isDown() || (allowTriggered && (button->bindContextAssociation() & InputDeviceControl::Triggered))? 1.0f : 0.0f); deviceTime = button->time(); // We've checked it, so clear the flag. button->setBindContextAssociation(InputDeviceControl::Triggered, UnsetFlags); } } if(auto *hat = ctrl->maybeAs()) { if(context && hat->bindContext() != context) return LoopContinue; // Shadowed by a more important active class. // Expired? if(!(hat->bindContextAssociation() & InputDeviceControl::Expired)) { devicePos = (hat->position() == bind.getf("angle")? 1.0f : 0.0f); deviceTime = hat->time(); } } // Apply further modifications based on flags. if(bind.geti("flags") & IBDF_INVERSE) { devicePos = -devicePos; deviceOffset = -deviceOffset; } if(bind.geti("flags") & IBDF_TIME_STAGED) { if(nowTime - deviceTime < STAGE_THRESHOLD * 1000) { devicePos *= STAGE_FACTOR; } } *pos += devicePos; *relativeOffset += deviceOffset; // Is this state contributing to the outcome? if(!de::fequal(devicePos, 0.f)) { if(appliedState[bind.geti("type")]) { // Another binding already influenced this; we have a conflict. conflicted[bind.geti("type")] = true; } // We've found one effective binding that influences this control. appliedState[bind.geti("type")] = true; } return LoopContinue; }); if(zeroControlUponConflict) { for(int i = 0; i < NUM_IBD_TYPES; ++i) { if(conflicted[i]) *pos = 0; } } // Clamp to the normalized range. *pos = de::clamp(-1.0f, *pos, 1.0f); } String B_ControlDescToString(int deviceId, ddeventtype_t type, int id) { InputDevice *device = nullptr; String str; if(type != E_SYMBOLIC) { device = &inputSys().device(deviceId); // Name of the device. str += device->name() + "-"; } switch(type) { case E_TOGGLE: { DENG2_ASSERT(device); InputDeviceButtonControl &button = device->button(id); if(!button.name().isEmpty()) { str += button.name(); } else if(device == inputSys().devicePtr(IDEV_KEYBOARD)) { char const *name = B_ShortNameForKey(id); if(name) { str += name; } else { str += String("code%1").arg(id, 3, 10, QChar('0')); } } else { str += "button" + String::number(id + 1); } break; } case E_AXIS: DENG2_ASSERT(device); str += device->axis(id).name(); break; case E_ANGLE: str += "hat" + String::number(id + 1); break; case E_SYMBOLIC: str += "sym"; break; default: DENG2_ASSERT(!"B_ControlDescToString: Invalid event type"); break; } return str; } String B_ButtonStateToString(Binding::ControlTest test) { switch(test) { case Binding::ButtonStateAny: return "-undefined"; case Binding::ButtonStateDown: return "-down"; case Binding::ButtonStateRepeat: return "-repeat"; case Binding::ButtonStateDownOrRepeat: return "-press"; case Binding::ButtonStateUp: return "-up"; default: DENG2_ASSERT(!"B_ButtonStateToString: Unknown test"); return ""; } } String B_AxisPositionToString(Binding::ControlTest test, float pos) { switch(test) { case Binding::AxisPositionWithin: return String("-within%1").arg(pos); case Binding::AxisPositionBeyond: return String("-beyond%1").arg(pos); case Binding::AxisPositionBeyondPositive: return String("-pos%1" ).arg(pos); case Binding::AxisPositionBeyondNegative: return String("-neg%1").arg(-pos); default: DENG2_ASSERT(!"B_AxisPositionToString: Unknown test"); return ""; } } String B_HatAngleToString(float pos) { return (pos < 0? "-center" : String("-angle") + String::number(pos)); } String B_ConditionToString(Record const &cond) { String str; if(cond.geti("type") == Binding::GlobalState) { if(cond.getb("multiplayer")) { str += "multiplayer"; } } else if(cond.geti("type") == Binding::ModifierState) { str += "modifier-" + String::number(cond.geti("id") - CTL_MODIFIER_1 + 1); } else { str += B_ControlDescToString(cond.geti("device"), ( cond.geti("type") == Binding::ButtonState? E_TOGGLE : cond.geti("type") == Binding::AxisState? E_AXIS : E_ANGLE), cond.geti("id")); } switch(cond.geti("type")) { case Binding::ButtonState: case Binding::ModifierState: str += B_ButtonStateToString(Binding::ControlTest(cond.geti("test"))); break; case Binding::AxisState: str += B_AxisPositionToString(Binding::ControlTest(cond.geti("test")), cond.getf("pos")); break; case Binding::HatState: str += B_HatAngleToString(cond.getf("pos")); break; default: break; } // Flags. if(cond.getb("negate")) { str += "-not"; } return str; } String B_EventToString(ddevent_t const &ev) { String str = B_ControlDescToString(ev.device, ev.type, ( ev.type == E_TOGGLE ? ev.toggle.id : ev.type == E_AXIS ? ev.axis.id : ev.type == E_ANGLE ? ev.angle.id : ev.type == E_SYMBOLIC? ev.symbolic.id : 0)); switch(ev.type) { case E_TOGGLE: str += B_ButtonStateToString( ev.toggle.state == ETOG_DOWN? Binding::ButtonStateDown : ev.toggle.state == ETOG_UP ? Binding::ButtonStateUp : Binding::ButtonStateUp); break; case E_AXIS: str += B_AxisPositionToString((ev.axis.pos >= 0? Binding::AxisPositionBeyondPositive : Binding::AxisPositionBeyondNegative), ev.axis.pos); break; case E_ANGLE: str += B_HatAngleToString(ev.angle.pos); break; case E_SYMBOLIC: str += "-" + String(ev.symbolic.name); break; default: break; } return str; } struct keyname_t { int key; ///< DDKEY char const *name; }; static keyname_t const keyNames[] = { { DDKEY_PAUSE, "pause" }, { DDKEY_ESCAPE, "escape" }, { DDKEY_ESCAPE, "esc" }, { DDKEY_RIGHTARROW, "right" }, { DDKEY_LEFTARROW, "left" }, { DDKEY_UPARROW, "up" }, { DDKEY_DOWNARROW, "down" }, { DDKEY_RETURN, "return" }, { DDKEY_TAB, "tab" }, { DDKEY_RSHIFT, "shift" }, { DDKEY_RCTRL, "ctrl" }, { DDKEY_RCTRL, "control" }, { DDKEY_RALT, "alt" }, { DDKEY_INS, "insert" }, { DDKEY_INS, "ins" }, { DDKEY_DEL, "delete" }, { DDKEY_DEL, "del" }, { DDKEY_PGUP, "pageup" }, { DDKEY_PGUP, "pgup" }, { DDKEY_PGDN, "pagedown" }, { DDKEY_PGDN, "pgdown" }, { DDKEY_PGDN, "pgdn" }, { DDKEY_HOME, "home" }, { DDKEY_END, "end" }, { DDKEY_BACKSPACE, "backspace" }, { DDKEY_BACKSPACE, "bkspc" }, { '/', "slash" }, { DDKEY_BACKSLASH, "backslash" }, { '[', "sqbracketleft" }, { ']', "sqbracketright" }, { '+', "plus" }, { '-', "minus" }, { '+', "plus" }, { '=', "equals" }, { ' ', "space" }, { ';', "semicolon" }, { ',', "comma" }, { '.', "period" }, { '\'', "apostrophe" }, { DDKEY_F10, "f10" }, { DDKEY_F11, "f11" }, { DDKEY_F12, "f12" }, { DDKEY_F1, "f1" }, { DDKEY_F2, "f2" }, { DDKEY_F3, "f3" }, { DDKEY_F4, "f4" }, { DDKEY_F5, "f5" }, { DDKEY_F6, "f6" }, { DDKEY_F7, "f7" }, { DDKEY_F8, "f8" }, { DDKEY_F9, "f9" }, { '`', "tilde" }, { DDKEY_NUMLOCK, "numlock" }, { DDKEY_CAPSLOCK, "capslock" }, { DDKEY_SCROLL, "scrlock" }, { DDKEY_NUMPAD0, "pad0" }, { DDKEY_NUMPAD1, "pad1" }, { DDKEY_NUMPAD2, "pad2" }, { DDKEY_NUMPAD3, "pad3" }, { DDKEY_NUMPAD4, "pad4" }, { DDKEY_NUMPAD5, "pad5" }, { DDKEY_NUMPAD6, "pad6" }, { DDKEY_NUMPAD7, "pad7" }, { DDKEY_NUMPAD8, "pad8" }, { DDKEY_NUMPAD9, "pad9" }, { DDKEY_DECIMAL, "decimal" }, { DDKEY_DECIMAL, "padcomma" }, { DDKEY_SUBTRACT, "padminus" }, { DDKEY_ADD, "padplus" }, { DDKEY_PRINT, "print" }, { DDKEY_PRINT, "prtsc" }, { DDKEY_ENTER, "enter" }, { DDKEY_DIVIDE, "divide" }, { DDKEY_MULTIPLY, "multiply" }, { DDKEY_SECTION, "section" }, { DDKEY_WINMENU, "winmenu" }, { 0, nullptr} }; char const *B_ShortNameForKey(int ddKey, bool forceLowercase) { static char nameBuffer[40]; for(uint idx = 0; keyNames[idx].key; ++idx) { if(ddKey == keyNames[idx].key) return keyNames[idx].name; } if(isalnum(ddKey)) { // Printable character, fabricate a single-character name. nameBuffer[0] = forceLowercase? tolower(ddKey) : ddKey; nameBuffer[1] = 0; return nameBuffer; } return nullptr; } int B_KeyForShortName(char const *key) { DENG2_ASSERT(key); for(uint idx = 0; keyNames[idx].key; ++idx) { if(!qstricmp(key, keyNames[idx].name)) return keyNames[idx].key; } if(qstrlen(key) == 1 && isalnum(key[0])) { // ASCII char. return tolower(key[0]); } return 0; } doomsday-stable-1.15.7/doomsday/client/src/ui/clientwindow.cpp0000664000175000017500000010540112641367670023710 0ustar jaakkojaakko/** @file clientwindow.cpp Top-level window with UI widgets. * @ingroup base * * @todo Platform-specific behavior should be encapsulated in subclasses, e.g., * MacWindowBehavior. This would make the code easier to follow and more adaptable * to the quirks of each platform. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * @authors Copyright © 2008 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "ui/clientwindow.h" #include "ui/clientrootwidget.h" #include "clientapp.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "api_console.h" #include "gl/sys_opengl.h" #include "gl/gl_main.h" #include "ui/widgets/gamewidget.h" #include "ui/widgets/gameuiwidget.h" #include "ui/widgets/busywidget.h" #include "ui/widgets/taskbarwidget.h" #include "ui/widgets/consolewidget.h" #include "ui/widgets/gameselectionwidget.h" #include "ui/dialogs/coloradjustmentdialog.h" #include "ui/dialogs/alertdialog.h" #include "ui/inputdevice.h" #include "CommandAction" #include "ui/mouse_qt.h" #include "dd_main.h" #include "render/vr.h" using namespace de; static inline InputSystem &inputSys() { return ClientApp::inputSystem(); } static ClientWindow *mainWindow = nullptr; // The main window, set after fully constructed. DENG2_PIMPL(ClientWindow) , DENG2_OBSERVES(MouseEventSource, MouseStateChange) , DENG2_OBSERVES(Canvas, FocusChange) , DENG2_OBSERVES(App, GameChange) , DENG2_OBSERVES(App, StartupComplete) , DENG2_OBSERVES(Games, Readiness) , DENG2_OBSERVES(Variable, Change) { bool needMainInit = true; bool needRecreateCanvas = false; bool needRootSizeUpdate = false; Mode mode = Normal; /// Root of the nomal UI widgets of this window. ClientRootWidget root; CompositorWidget *compositor = nullptr; GameWidget *game = nullptr; GameUIWidget *gameUI = nullptr; TaskBarWidget *taskBar = nullptr; LabelWidget *taskBarBlur = nullptr; ///< Blur everything below the task bar. NotificationAreaWidget *notifications = nullptr; AlertDialog *alerts = nullptr; ColorAdjustmentDialog *colorAdjust = nullptr; LabelWidget *background = nullptr; GuiWidget *iwadNotice = nullptr; GameSelectionWidget *gameSelMenu = nullptr; BusyWidget *busy = nullptr; GuiWidget *sidebar = nullptr; LabelWidget *cursor = nullptr; ConstantRule *cursorX; ConstantRule *cursorY; bool cursorHasBeenHidden = false; // FPS notifications. UniqueWidgetPtr fpsCounter; float oldFps = 0; /// @todo Switch dynamically between VR and plain. VRWindowTransform contentXf; Instance(Public *i) : Base(i) , root(thisPublic) , cursorX(new ConstantRule(0)) , cursorY(new ConstantRule(0)) , contentXf(*i) { self.setTransform(contentXf); /// @todo The decision whether to receive input notifications from the /// canvas is really a concern for the input drivers. App::app().audienceForGameChange() += this; App::app().audienceForStartupComplete() += this; App_Games().audienceForReadiness() += this; // Listen to input. self.canvas().audienceForMouseStateChange() += this; foreach(String s, configVariableNames()) { App::config(s).audienceForChange() += this; } } ~Instance() { foreach(String s, configVariableNames()) { App::config(s).audienceForChange() -= this; } App::app().audienceForGameChange() -= this; App::app().audienceForStartupComplete() -= this; App_Games().audienceForReadiness() -= this; self.canvas().audienceForFocusChange() -= this; self.canvas().audienceForMouseStateChange() -= this; releaseRef(cursorX); releaseRef(cursorY); if(thisPublic == mainWindow) { mainWindow = nullptr; } } StringList configVariableNames() const { return StringList() << self.configName("fsaa") << self.configName("vsync"); } Widget &container() { if(compositor) { return *compositor; } return root; } void setupUI() { Style &style = ClientApp::windowSystem().style(); // Background for Ring Zero. background = new LabelWidget("background"); background->setImageColor(Vector4f(0, 0, 0, 1)); background->setImage(style.images().image("window.background")); background->setImageFit(ui::FitToSize); background->setSizePolicy(ui::Filled, ui::Filled); background->margins().set(""); background->rule().setRect(root.viewRule()); root.add(background); game = new GameWidget; game->rule().setRect(root.viewRule()); // Initially the widget is disabled. It will be enabled when the window // is visible and ready to be drawn. game->disable(); root.add(game); gameUI = new GameUIWidget; gameUI->rule().setRect(root.viewRule()); gameUI->disable(); container().add(gameUI); // Busy widget shows progress indicator and frozen game content. busy = new BusyWidget; busy->hide(); // normally hidden busy->rule().setRect(root.viewRule()); root.add(busy); // Game selection. gameSelMenu = new GameSelectionWidget; gameSelMenu->enableActionOnSelection(true); gameSelMenu->rule() .setInput(Rule::AnchorX, root.viewRule().midX()) .setInput(Rule::Width, root.viewWidth()) .setAnchorPoint(Vector2f(.5f, .5f)); AutoRef pad(OperatorRule::maximum(style.rules().rule("gap"), (root.viewWidth() - style.rules().rule("gameselection.max.width")) / 2)); gameSelMenu->margins().setLeft(pad).setRight(pad); gameSelMenu->filter().useInvertedStyle(); gameSelMenu->filter().setOpacity(.9f); gameSelMenu->filter().rule() .setInput(Rule::Left, gameSelMenu->rule().left() + gameSelMenu->margins().left()) .setInput(Rule::Width, gameSelMenu->rule().width() - gameSelMenu->margins().width()) .setInput(Rule::Top, root.viewTop() + style.rules().rule("gap")); container().add(gameSelMenu); gameSelMenu->filter().enableBackground(gameSelMenu->scrollPositionY()); // As an alternative to game selection, a notice to pick the IWAD folder. ButtonWidget *chooseIwad = nullptr; iwadNotice = new GuiWidget; { LabelWidget *notice = LabelWidget::newWithText(_E(b) + tr("No playable games were found.\n") + _E(.) + tr("Please select the folder where you have one or more game WAD files."), iwadNotice); notice->setTextColor("inverted.text"); notice->setSizePolicy(ui::Expand, ui::Expand); notice->rule() .setMidAnchorX(root.viewRule().midX()) .setInput(Rule::Bottom, root.viewRule().midY()); chooseIwad = new ButtonWidget; chooseIwad->setText(tr("Select IWAD Folder...")); chooseIwad->setSizePolicy(ui::Expand, ui::Expand); chooseIwad->rule() .setMidAnchorX(root.viewRule().midX()) .setInput(Rule::Top, notice->rule().bottom()); iwadNotice->add(chooseIwad); iwadNotice->rule().setRect(root.viewRule()); iwadNotice->hide(); container().add(iwadNotice); } // Common notification area. notifications = new NotificationAreaWidget; notifications->useDefaultPlacement(game->rule()); container().add(notifications); // Alerts notification and popup. alerts = new AlertDialog; root.add(alerts); // FPS counter for the notification area. fpsCounter.reset(new LabelWidget); fpsCounter->setSizePolicy(ui::Expand, ui::Expand); fpsCounter->setAlignment(ui::AlignRight); // Everything behind the task bar can be blurred with this widget. taskBarBlur = new LabelWidget("taskbar-blur"); taskBarBlur->set(GuiWidget::Background(Vector4f(1, 1, 1, 1), GuiWidget::Background::Blurred)); taskBarBlur->rule().setRect(root.viewRule()); taskBarBlur->setAttribute(GuiWidget::DontDrawContent); container().add(taskBarBlur); // Taskbar is over almost everything else. taskBar = new TaskBarWidget; taskBar->rule() .setInput(Rule::Left, root.viewLeft()) .setInput(Rule::Bottom, root.viewBottom() + taskBar->shift()) .setInput(Rule::Width, root.viewWidth()); container().add(taskBar); // The game selection's height depends on the taskbar. AutoRef availHeight = taskBar->rule().top() - gameSelMenu->filter().rule().height(); gameSelMenu->rule() .setInput(Rule::AnchorY, gameSelMenu->filter().rule().height() + availHeight / 2) .setInput(Rule::Height, OperatorRule::minimum(availHeight, gameSelMenu->contentRule().height() + gameSelMenu->margins().height())); // Color adjustment dialog. colorAdjust = new ColorAdjustmentDialog; colorAdjust->setAnchor(root.viewWidth() / 2, root.viewTop()); colorAdjust->setOpeningDirection(ui::Down); root.add(colorAdjust); taskBar->hide(); // Task bar provides the IWAD selection feature. chooseIwad->setAction(new SignalAction(taskBar, SLOT(chooseIWADFolder()))); // Mouse cursor is used with transformed content. cursor = new LabelWidget; cursor->setBehavior(Widget::Unhittable); cursor->margins().set(""); // no margins cursor->setImage(style.images().image("window.cursor")); cursor->setAlignment(ui::AlignTopLeft); cursor->rule() .setSize(Const(48), Const(48)) .setLeftTop(*cursorX, *cursorY); cursor->hide(); container().add(cursor); } void appStartupCompleted() { // Allow the background image to show. background->setImageColor(Vector4f(1, 1, 1, 1)); taskBar->show(); // Show the tutorial if it hasn't been automatically shown yet. if(!App::config().getb("tutorial.shown", false)) { App::config().set("tutorial.shown", true); LOG_NOTE("Starting tutorial (not shown before)"); QTimer::singleShot(500, taskBar, SLOT(showTutorial())); } } void gameReadinessUpdated() { DENG2_ASSERT(!App_GameLoaded()); showGameSelectionMenu(true); } void showGameSelectionMenu(bool show) { bool gotPlayable = App_Games().numPlayable(); if(show && gotPlayable) { gameSelMenu->show(); iwadNotice->hide(); } else if(show && !gotPlayable) { gameSelMenu->hide(); iwadNotice->show(); } else if(!show) { gameSelMenu->hide(); iwadNotice->hide(); } } void currentGameChanged(game::Game const &newGame) { if(newGame.isNull()) { //game->hide(); background->show(); showGameSelectionMenu(true); gameSelMenu->restoreState(); } else { //game->show(); background->hide(); showGameSelectionMenu(false); gameSelMenu->saveState(); } // Check with Style if blurring is allowed. taskBar->console().enableBlur(taskBar->style().isBlurringAllowed()); self.hideTaskBarBlur(); // update background blur mode activateOculusRiftModeIfConnected(); } void activateOculusRiftModeIfConnected() { if(vrCfg().oculusRift().isHMDConnected() && vrCfg().mode() != VRConfig::OculusRift) { LOG_NOTE("HMD connected, automatically switching to Oculus Rift mode"); Con_SetInteger("rend-vr-mode", VRConfig::OculusRift); vrCfg().oculusRift().moveWindowToScreen(OculusRift::HMDScreen); } else if(!vrCfg().oculusRift().isHMDConnected() && vrCfg().mode() == VRConfig::OculusRift) { LOG_NOTE("HMD not connected, disabling VR mode"); Con_SetInteger("rend-vr-mode", VRConfig::Mono); vrCfg().oculusRift().moveWindowToScreen(OculusRift::DefaultScreen); } } void setMode(Mode const &newMode) { LOG_DEBUG("Switching to %s mode") << (newMode == Busy? "Busy" : "Normal"); // Hide and show widgets as appropriate. switch(newMode) { case Busy: game->hide(); game->disable(); gameUI->hide(); gameUI->disable(); showGameSelectionMenu(false); taskBar->disable(); busy->show(); busy->enable(); break; default: //busy->hide(); // The busy widget will hide itself after a possible transition has finished. busy->disable(); game->show(); game->enable(); gameUI->show(); gameUI->enable(); if(!App_GameLoaded()) showGameSelectionMenu(true); taskBar->enable(); break; } mode = newMode; } /** * Completes the initialization of the main window. This is called only after the * window is created and visible, so that OpenGL operations and actions on the native * window can be performed without restrictions. */ void finishMainWindowInit() { #ifdef MACOSX if(self.isFullScreen()) { // The window must be manually raised above the shielding window put up by // the fullscreen display capture. DisplayMode_Native_Raise(self.nativeHandle()); } #endif self.raise(); self.activateWindow(); /* // Automatically grab the mouse from the get-go if in fullscreen mode. if(Mouse_IsPresent() && self.isFullScreen()) { self.canvas().trapMouse(); } */ self.canvas().audienceForFocusChange() += this; #ifdef WIN32 if(self.isFullScreen()) { // It would seem we must manually give our canvas focus. Bug in Qt? self.canvas().setFocus(); } #endif self.canvas().makeCurrent(); DD_FinishInitializationAfterWindowReady(); vrCfg().oculusRift().glPreInit(); contentXf.glInit(); } void mouseStateChanged(MouseEventSource::State state) { Mouse_Trap(state == MouseEventSource::Trapped); } /** * Handles an event that BaseWindow (and thus WindowSystem) didn't have use for. * * @param event Event to handle. */ bool handleFallbackEvent(Event const &ev) { if(MouseEvent const *mouse = ev.maybeAs()) { // Fall back to legacy handling. switch(ev.type()) { case Event::MouseButton: Mouse_Qt_SubmitButton( mouse->button() == MouseEvent::Left? IMB_LEFT : mouse->button() == MouseEvent::Middle? IMB_MIDDLE : mouse->button() == MouseEvent::Right? IMB_RIGHT : mouse->button() == MouseEvent::XButton1? IMB_EXTRA1 : mouse->button() == MouseEvent::XButton2? IMB_EXTRA2 : IMB_MAXBUTTONS, mouse->state() == MouseEvent::Pressed); return true; case Event::MouseMotion: Mouse_Qt_SubmitMotion(IMA_POINTER, mouse->pos().x, mouse->pos().y); return true; case Event::MouseWheel: if(mouse->wheelMotion() == MouseEvent::Step) { // The old input system can only do wheel step events. Mouse_Qt_SubmitMotion(IMA_WHEEL, mouse->wheel().x, mouse->wheel().y); } return true; default: break; } } return false; } void canvasFocusChanged(Canvas &canvas, bool hasFocus) { LOG_DEBUG("canvasFocusChanged focus:%b fullscreen:%b hidden:%b minimized:%b") << hasFocus << self.isFullScreen() << self.isHidden() << self.isMinimized(); if(!hasFocus) { inputSys().forAllDevices([] (InputDevice &device) { device.reset(); return LoopContinue; }); inputSys().clearEvents(); canvas.trapMouse(false); } else if(self.isFullScreen() && !taskBar->isOpen()) { // Trap the mouse again in fullscreen mode. canvas.trapMouse(); } // Generate an event about this. ddevent_t ev; de::zap(ev); ev.device = uint(-1); ev.type = E_FOCUS; ev.focus.gained = hasFocus; ev.focus.inWindow = 1; /// @todo Ask WindowSystem for an identifier number. inputSys().postEvent(&ev); } void updateFpsNotification(float fps) { notifications->showOrHide(*fpsCounter, self.isFPSCounterVisible()); if(!fequal(oldFps, fps)) { fpsCounter->setText(QString("%1 " _E(l) + tr("FPS")).arg(fps, 0, 'f', 1)); oldFps = fps; } } void variableValueChanged(Variable &variable, Value const &newValue) { if(variable.name() == "fsaa") { self.updateCanvasFormat(); } else if(variable.name() == "vsync") { #ifdef WIN32 self.updateCanvasFormat(); DENG2_UNUSED(newValue); #else GL_SetVSync(newValue.isTrue()); #endif } } void installSidebar(SidebarLocation location, GuiWidget *widget) { // Get rid of the old sidebar. if(sidebar) { uninstallSidebar(location); } if(!widget) return; DENG2_ASSERT(sidebar == NULL); // Attach the widget. switch(location) { case RightEdge: widget->rule() .setInput(Rule::Top, root.viewTop()) .setInput(Rule::Right, root.viewRight()) .setInput(Rule::Bottom, taskBar->rule().top()); game->rule() .setInput(Rule::Right, widget->rule().left()); gameUI->rule() .setInput(Rule::Right, widget->rule().left()); break; } sidebar = widget; container().insertBefore(sidebar, *notifications); } void uninstallSidebar(SidebarLocation location) { DENG2_ASSERT(sidebar != NULL); switch(location) { case RightEdge: game->rule().setInput(Rule::Right, root.viewRight()); gameUI->rule().setInput(Rule::Right, root.viewRight()); break; } container().remove(*sidebar); sidebar->guiDeleteLater(); sidebar = 0; } enum DeferredTaskResult { Continue, AbortFrame }; DeferredTaskResult performDeferredTasks() { if(BusyMode_Active()) { // Let's not do anything risky in busy mode. return Continue; } // The canvas needs to be recreated when the GL format has changed // (e.g., multisampling). if(needRecreateCanvas) { needRecreateCanvas = false; if(self.setDefaultGLFormat()) { self.recreateCanvas(); // Wait until the new Canvas is ready (note: loop remains paused!). return AbortFrame; } } return Continue; } void updateRootSize() { DENG_ASSERT_IN_MAIN_THREAD(); needRootSizeUpdate = false; Vector2ui const size = contentXf.logicalRootSize(self.canvas().size()); // Tell the widgets. root.setViewSize(size); } void enableCompositor(bool enable) { DENG_ASSERT_IN_MAIN_THREAD(); if((enable && compositor) || (!enable && !compositor)) { return; } // All the children of the compositor need to be relocated. container().remove(*gameUI); container().remove(*gameSelMenu); container().remove(*iwadNotice); if(sidebar) container().remove(*sidebar); container().remove(*notifications); container().remove(*taskBarBlur); container().remove(*taskBar); container().remove(*cursor); Widget::Children additional; // Relocate all popups to the new container (which need to stay on top). foreach(Widget *w, container().children()) { if(PopupWidget *pop = w->maybeAs()) { additional.append(pop); container().remove(*pop); } } if(enable && !compositor) { LOG_GL_VERBOSE("Offscreen UI composition enabled"); compositor = new CompositorWidget; compositor->rule().setRect(root.viewRule()); root.add(compositor); } else { DENG2_ASSERT(compositor != 0); DENG2_ASSERT(!compositor->childCount()); root.remove(*compositor); compositor->guiDeleteLater(); compositor = 0; LOG_GL_VERBOSE("Offscreen UI composition disabled"); } container().add(gameUI); if(&container() == &root) { // Make sure the game UI doesn't show up over the busy transition. container().moveChildBefore(gameUI, *busy); } container().add(gameSelMenu); container().add(iwadNotice); if(sidebar) container().add(sidebar); container().add(notifications); container().add(taskBarBlur); container().add(taskBar); // Also the other widgets. foreach(Widget *w, additional) { container().add(w); } // Fake cursor must be on top. container().add(cursor); if(mode == Normal) { root.update(); } } void updateCompositor() { DENG_ASSERT_IN_MAIN_THREAD(); if(!compositor) return; if(vrCfg().mode() == VRConfig::OculusRift) { /// @todo Adjustable compositor depth/size. float uiDistance = 40; float uiSize = 50; auto const &ovr = vrCfg().oculusRift(); Vector3f const pry = ovr.headOrientation(); compositor->setCompositeProjection( GL_GetProjectionMatrix() * Matrix4f::rotate(radianToDegree(pry[1]), Vector3f(0, 0, -1)) * Matrix4f::rotate(radianToDegree(pry[0]), Vector3f(1, 0, 0)) * Matrix4f::rotate(radianToDegree(pry[2]), Vector3f(0, 1, 0)) * Matrix4f::translate(swizzle(ovr.headPosition() * vrCfg().mapUnitsPerMeter(), AxisNegX, AxisNegY, AxisZ)) * Matrix4f::scale(Vector3f(uiSize, -uiSize/ovr.aspect(), 1.f)) * Matrix4f::translate(Vector3f(-.5f, -.5f, uiDistance))); } else { // We'll simply cover the entire view. compositor->useDefaultCompositeProjection(); } } void updateMouseCursor() { // The cursor is only needed if the content is warped. cursor->show(!self.canvas().isMouseTrapped() && VRConfig::modeAppliesDisplacement(vrCfg().mode())); // Show or hide the native mouse cursor. if(cursor->isVisible()) { if(!cursorHasBeenHidden) { qApp->setOverrideCursor(QCursor(Qt::BlankCursor)); cursorHasBeenHidden = true; } Vector2i cp = ClientApp::windowSystem().latestMousePosition(); cursorX->set(cp.x); cursorY->set(cp.y); } else { if(cursorHasBeenHidden) { qApp->restoreOverrideCursor(); } cursorHasBeenHidden = false; } } }; ClientWindow::ClientWindow(String const &id) : BaseWindow(id) , d(new Instance(this)) { canvas().audienceForGLResize() += this; canvas().audienceForGLInit() += this; #ifdef WIN32 // Set an icon for the window. Path iconPath = DENG2_APP->nativeBasePath() / "data\\graphics\\doomsday.ico"; LOG_DEBUG("Window icon: ") << NativePath(iconPath).pretty(); setWindowIcon(QIcon(iconPath)); #endif d->setupUI(); // The first window is the main window. if(!mainWindow) { mainWindow = this; } } Vector2f ClientWindow::windowContentSize() const { return Vector2f(d->root.viewWidth().value(), d->root.viewHeight().value()); } ClientRootWidget &ClientWindow::root() { return d->root; } TaskBarWidget &ClientWindow::taskBar() { return *d->taskBar; } GuiWidget &ClientWindow::taskBarBlur() { return *d->taskBarBlur; } ConsoleWidget &ClientWindow::console() { return d->taskBar->console(); } NotificationAreaWidget &ClientWindow::notifications() { return *d->notifications; } GameWidget &ClientWindow::game() { return *d->game; } BusyWidget &ClientWindow::busy() { return *d->busy; } AlertDialog &ClientWindow::alerts() { return *d->alerts; } bool ClientWindow::isFPSCounterVisible() const { return App::config().getb(configName("showFps")); } void ClientWindow::setMode(Mode const &mode) { LOG_AS("ClientWindow"); d->setMode(mode); } void ClientWindow::closeEvent(QCloseEvent *ev) { if(!BusyMode_Active()) { LOG_DEBUG("Window is about to close, executing 'quit'"); /// @todo autosave and quit? Con_Execute(CMDS_DDAY, "quit", true, false); } // We are not authorizing immediate closing of the window; // engine shutdown will take care of it later. ev->ignore(); // don't close } void ClientWindow::canvasGLReady(Canvas &canvas) { // Update the capability flags. GL_state.features.multisample = GLFramebuffer::defaultMultisampling() > 1; LOGDEV_GL_MSG("GL feature: Multisampling: %b") << GL_state.features.multisample; if(vrCfg().needsStereoGLFormat() && !canvas.format().stereo()) { LOG_GL_WARNING("Current VR mode needs a stereo buffer, but it isn't supported"); } BaseWindow::canvasGLReady(canvas); // Now that the Canvas is ready for drawing we can enable the GameWidget. d->game->enable(); d->gameUI->enable(); // Configure a viewport immediately. GLState::current().setViewport(Rectangleui(0, 0, canvas.width(), canvas.height())).apply(); LOG_DEBUG("GameWidget enabled"); if(d->needMainInit) { d->needMainInit = false; d->finishMainWindowInit(); } } void ClientWindow::canvasGLInit(Canvas &) { Sys_GLConfigureDefaultState(); GL_Init2DState(); } void ClientWindow::preDraw() { // NOTE: This occurs during the Canvas paintGL event. ClientApp::app().preFrame(); /// @todo what about multiwindow? DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // Cursor position (if cursor is visible). d->updateMouseCursor(); if(d->needRootSizeUpdate) { d->updateRootSize(); } BaseWindow::preDraw(); } void ClientWindow::drawWindowContent() { d->updateCompositor(); root().draw(); LIBGUI_ASSERT_GL_OK(); } void ClientWindow::postDraw() { /// @note This method is called during the Canvas paintGL event. // OVR will handle presentation in Oculus Rift mode. if(ClientApp::vr().mode() != VRConfig::OculusRift) { // Finish GL drawing and swap it on to the screen. Blocks until buffers // swapped. GL_DoUpdate(); } BaseWindow::postDraw(); ClientApp::app().postFrame(); /// @todo what about multiwindow? d->updateFpsNotification(frameRate()); } void ClientWindow::canvasGLResized(Canvas &canvas) { LOG_AS("ClientWindow"); Canvas::Size size = canvas.size(); LOG_TRACE("Canvas resized to ") << size.asText(); GLState::current().setViewport(Rectangleui(0, 0, size.x, size.y)); d->updateRootSize(); } bool ClientWindow::setDefaultGLFormat() // static { LOG_AS("DefaultGLFormat"); // Configure the GL settings for all subsequently created canvases. QGLFormat fmt; fmt.setProfile(QGLFormat::CompatibilityProfile); fmt.setVersion(2, 1); fmt.setDepth(false); // depth and stencil handled in GLFramebuffer fmt.setStencil(false); //fmt.setDepthBufferSize(16); //fmt.setStencilBufferSize(8); fmt.setDoubleBuffer(true); if(vrCfg().needsStereoGLFormat()) { // Only use a stereo format for modes that require it. LOG_GL_MSG("Using a stereoscopic frame buffer format"); fmt.setStereo(true); } #ifdef WIN32 if(CommandLine_Exists("-novsync") || !App::config().getb("window.main.vsync")) { fmt.setSwapInterval(0); } else { fmt.setSwapInterval(1); } #endif int sampleCount = 1; bool configured = App::config().getb("window.main.fsaa"); if(CommandLine_Exists("-nofsaa") || !configured) { LOG_GL_VERBOSE("Multisampling off"); } else { sampleCount = 4; // four samples is fine? LOG_GL_VERBOSE("Multisampling on (%i samples)") << sampleCount; } GLFramebuffer::setDefaultMultisampling(sampleCount); if(fmt != QGLFormat::defaultFormat()) { LOG_GL_VERBOSE("Applying new format..."); QGLFormat::setDefaultFormat(fmt); return true; } else { LOG_GL_XVERBOSE("New format is the same as before"); return false; } } bool ClientWindow::prepareForDraw() { if(!BaseWindow::prepareForDraw()) { return false; } // Offscreen composition is only needed in Oculus Rift mode. d->enableCompositor(vrCfg().mode() == VRConfig::OculusRift); if(d->performDeferredTasks() == Instance::AbortFrame) { // Shouldn't draw right now. return false; } return true; // Go ahead. } bool ClientWindow::shouldRepaintManually() const { // When the mouse is not trapped, allow the system to regulate window // updates (e.g., for window manipulation). if(isFullScreen()) return true; return !Mouse_IsPresent() || canvas().isMouseTrapped(); } void ClientWindow::grab(image_t &img, bool halfSized) const { DENG_ASSERT_IN_MAIN_THREAD(); QSize outputSize = (halfSized? QSize(width()/2, height()/2) : QSize()); QImage grabbed = canvas().grabImage(outputSize); Image_Init(img); img.size = Vector2ui(grabbed.width(), grabbed.height()); img.pixelSize = grabbed.depth()/8; img.pixels = (uint8_t *) malloc(grabbed.byteCount()); std::memcpy(img.pixels, grabbed.constBits(), grabbed.byteCount()); LOGDEV_GL_MSG("Grabbed Canvas contents %i x %i, byteCount:%i depth:%i format:%i") << grabbed.width() << grabbed.height() << grabbed.byteCount() << grabbed.depth() << grabbed.format(); DENG_ASSERT(img.pixelSize != 0); } void ClientWindow::drawGameContent() { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); GLState::current().target().clear(GLTarget::ColorDepthStencil); d->root.drawUntil(*d->gameSelMenu); } void ClientWindow::fadeInTaskBarBlur(TimeDelta span) { d->taskBarBlur->setAttribute(GuiWidget::DontDrawContent, UnsetFlags); d->taskBarBlur->setOpacity(0); d->taskBarBlur->setOpacity(1, span); } void ClientWindow::fadeOutTaskBarBlur(TimeDelta span) { d->taskBarBlur->setOpacity(0, span); QTimer::singleShot(span.asMilliSeconds(), this, SLOT(hideTaskBarBlur())); } void ClientWindow::hideTaskBarBlur() { d->taskBarBlur->setAttribute(GuiWidget::DontDrawContent); if(d->taskBar->style().isBlurringAllowed()) { d->taskBarBlur->setOpacity(1); } else { d->taskBarBlur->setOpacity(0); } } void ClientWindow::updateCanvasFormat() { d->needRecreateCanvas = true; } void ClientWindow::updateRootSize() { // This will be done a bit later as the call may originate from another thread. d->needRootSizeUpdate = true; } ClientWindow &ClientWindow::main() { return static_cast(BaseWindow::main()); } bool ClientWindow::mainExists() { return mainWindow != nullptr; } void ClientWindow::toggleFPSCounter() { App::config().set(configName("showFps"), !isFPSCounterVisible()); } void ClientWindow::showColorAdjustments() { d->colorAdjust->open(); } void ClientWindow::addOnTop(GuiWidget *widget) { d->container().add(widget); // Make sure the cursor remains the topmost widget. d->container().moveChildToLast(*d->cursor); } void ClientWindow::setSidebar(SidebarLocation location, GuiWidget *sidebar) { DENG2_ASSERT(location == RightEdge); d->installSidebar(location, sidebar); } bool ClientWindow::hasSidebar(SidebarLocation location) const { DENG2_ASSERT(location == RightEdge); DENG2_UNUSED(location); return d->sidebar != 0; } bool ClientWindow::handleFallbackEvent(Event const &event) { return d->handleFallbackEvent(event); } #if defined(UNIX) && !defined(MACOSX) void GL_AssertContextActive() { DENG_ASSERT(QGLContext::currentContext() != 0); } #endif doomsday-stable-1.15.7/doomsday/client/src/ui/inputsystem.cpp0000664000175000017500000014766112641367670023624 0ustar jaakkojaakko/** @file inputsystem.cpp Input subsystem. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #define DENG_NO_API_MACROS_BINDING #include "de_platform.h" // strdup macro #include "ui/inputsystem.h" #include #include #include // SECONDSPERTIC #include #include #include #include #include #include "clientapp.h" #include "dd_def.h" // MAXEVENTS, ASSERT_NOT_64BIT #include "dd_main.h" // App_GameLoaded() #include "dd_loop.h" // DD_IsFrameTimeAdvancing() #include "m_misc.h" // M_WriteTextEsc #include "render/vr.h" #include "world/p_players.h" #include "BindContext" #include "CommandBinding" #include "ImpulseBinding" #include "ui/ddevent.h" #include "ui/b_util.h" #include "ui/clientwindow.h" #include "ui/clientwindowsystem.h" #include "ui/inputdebug.h" // Debug visualization. #include "ui/inputdevice.h" #include "ui/inputdeviceaxiscontrol.h" #include "ui/inputdevicebuttoncontrol.h" #include "ui/inputdevicehatcontrol.h" #include "ui/sys_input.h" #include "sys_system.h" // novideo using namespace de; #define DEFAULT_JOYSTICK_DEADZONE .05f ///< 5% #define MAX_AXIS_FILTER 40 static InputDevice *makeKeyboard(String const &name, String const &title = "") { InputDevice *keyboard = new InputDevice(name); keyboard->setTitle(title); // DDKEYs are used as button indices. for(int i = 0; i < 256; ++i) { keyboard->addButton(new InputDeviceButtonControl); } return keyboard; } static InputDevice *makeMouse(String const &name, String const &title = "") { InputDevice *mouse = new InputDevice(name); mouse->setTitle(title); for(int i = 0; i < IMB_MAXBUTTONS; ++i) { mouse->addButton(new InputDeviceButtonControl); } // Some of the mouse buttons have symbolic names. mouse->button(IMB_LEFT ).setName("left"); mouse->button(IMB_MIDDLE ).setName("middle"); mouse->button(IMB_RIGHT ).setName("right"); mouse->button(IMB_MWHEELUP ).setName("wheelup"); mouse->button(IMB_MWHEELDOWN ).setName("wheeldown"); mouse->button(IMB_MWHEELLEFT ).setName("wheelleft"); mouse->button(IMB_MWHEELRIGHT).setName("wheelright"); // The mouse wheel is translated to keys, so there is no need to // create an axis for it. InputDeviceAxisControl *axis; mouse->addAxis(axis = new InputDeviceAxisControl("x", InputDeviceAxisControl::Pointer)); //axis->setFilter(1); // On by default. axis->setScale(1.f/1000); mouse->addAxis(axis = new InputDeviceAxisControl("y", InputDeviceAxisControl::Pointer)); //axis->setFilter(1); // On by default. axis->setScale(1.f/1000); return mouse; } static InputDevice *makeJoystick(String const &name, String const &title = "") { InputDevice *joy = new InputDevice(name); joy->setTitle(title); for(int i = 0; i < IJOY_MAXBUTTONS; ++i) { joy->addButton(new InputDeviceButtonControl); } for(int i = 0; i < IJOY_MAXAXES; ++i) { char name[32]; if(i < 4) { strcpy(name, i == 0? "x" : i == 1? "y" : i == 2? "z" : "w"); } else { sprintf(name, "axis%02i", i + 1); } auto *axis = new InputDeviceAxisControl(name, InputDeviceAxisControl::Stick); joy->addAxis(axis); axis->setScale(1.0f / IJOY_AXISMAX); axis->setDeadZone(DEFAULT_JOYSTICK_DEADZONE); } for(int i = 0; i < IJOY_MAXHATS; ++i) { joy->addHat(new InputDeviceHatControl); } return joy; } static InputDevice *makeHeadTracker(String const &name, String const &title) { InputDevice *head = new InputDevice(name); head->setTitle(title); auto *axis = new InputDeviceAxisControl("yaw", InputDeviceAxisControl::Stick); head->addAxis(axis); axis->setRawInput(); head->addAxis(axis = new InputDeviceAxisControl("pitch", InputDeviceAxisControl::Stick)); axis->setRawInput(); head->addAxis(axis = new InputDeviceAxisControl("roll", InputDeviceAxisControl::Stick)); axis->setRawInput(); return head; } static Value *Function_InputSystem_BindEvent(Context &, Function::ArgumentValues const &args) { String eventDesc = args[0]->asText(); String command = args[1]->asText(); if(ClientApp::inputSystem().bindCommand(eventDesc.toLatin1(), command.toLatin1())) { // Success. return new NumberValue(true); } // Failed to create the binding... return new NumberValue(false); } #if 0 struct repeater_t { int key; ///< The DDKEY code (@c 0 if not in use). int native; ///< Used to determine which key is repeating. char text[8]; ///< Text to insert. timespan_t timer; ///< How's the time? int count; ///< How many times has been repeated? }; // The initial and secondary repeater delays (tics). int repWait1 = 15; int repWait2 = 3; #endif bool shiftDown; bool altDown; #ifdef OLD_FILTER static uint mouseFreq; #endif static float oldPOV = IJOY_POV_CENTER; static char *eventStrings[MAXEVENTS]; /** * Returns a copy of the string @a str. The caller does not get ownership of * the string. The string is valid until it gets overwritten by a new * allocation. There are at most MAXEVENTS strings allocated at a time. * * These are intended for strings in ddevent_t that are valid during the * processing of an event. */ static char const *allocEventString(char const *str) { DENG2_ASSERT(str); static int eventStringRover = 0; DENG2_ASSERT(eventStringRover >= 0 && eventStringRover < MAXEVENTS); M_Free(eventStrings[eventStringRover]); char const *returnValue = eventStrings[eventStringRover] = strdup(str); if(++eventStringRover >= MAXEVENTS) { eventStringRover = 0; } return returnValue; } static void clearEventStrings() { for(int i = 0; i < MAXEVENTS; ++i) { M_Free(eventStrings[i]); eventStrings[i] = nullptr; } } struct eventqueue_t { ddevent_t events[MAXEVENTS]; int head; int tail; }; /** * Gets the next event from an input event queue. * @param q Event queue. * @return @c NULL if no more events are available. */ static ddevent_t *nextFromQueue(eventqueue_t *q) { DENG2_ASSERT(q); if(q->head == q->tail) return nullptr; ddevent_t *ev = &q->events[q->tail]; q->tail = (q->tail + 1) & (MAXEVENTS - 1); return ev; } static void clearQueue(eventqueue_t *q) { DENG2_ASSERT(q); q->head = q->tail; } static void postToQueue(eventqueue_t *q, ddevent_t *ev) { DENG2_ASSERT(q && ev); q->events[q->head] = *ev; if(ev->type == E_SYMBOLIC) { // Allocate a throw-away string from our buffer. q->events[q->head].symbolic.name = allocEventString(ev->symbolic.name); } q->head++; q->head &= MAXEVENTS - 1; } static eventqueue_t queue; static eventqueue_t sharpQueue; static byte useSharpInputEvents = true; ///< cvar DENG2_PIMPL(InputSystem) , DENG2_OBSERVES(BindContext, ActiveChange) , DENG2_OBSERVES(BindContext, AcquireDeviceChange) , DENG2_OBSERVES(BindContext, BindingAddition) { bool ignoreInput = false; SettingsRegister settings; Binder binder; Record *scriptBindings = nullptr; typedef QList Devices; Devices devices; typedef QList BindContexts; BindContexts contexts; ///< Ordered from highest to lowest priority. Instance(Public *i) : Base(i) { // Initialize settings. settings.define(SettingsRegister::ConfigVariable, "input.mouse.syncSensitivity") .define(SettingsRegister::FloatCVar, "input-mouse-x-scale", 1.f/1000.f) .define(SettingsRegister::FloatCVar, "input-mouse-y-scale", 1.f/1000.f) .define(SettingsRegister::IntCVar, "input-mouse-x-flags", 0) .define(SettingsRegister::IntCVar, "input-mouse-y-flags", 0) .define(SettingsRegister::IntCVar, "input-joy", 1) .define(SettingsRegister::IntCVar, "input-sharp", 1); // Initialize script bindings. binder.initNew() << DENG2_FUNC(InputSystem_BindEvent, "bindEvent", "event" << "command"); App::scriptSystem().addNativeModule("Input", binder.module()); // Initialize system APIs. I_InitInterfaces(); } ~Instance() { self.clearAllContexts(); clearAllDevices(); // Shutdown system APIs. I_ShutdownInterfaces(); } void clearAllDevices() { qDeleteAll(devices); devices.clear(); } /** * @param device InputDevice to add. * @return Same as @a device for caller convenience. */ InputDevice *addDevice(InputDevice *device) { if(device) { if(!devices.contains(device)) { // Ensure the name is unique. for(InputDevice *otherDevice : devices) { if(!otherDevice->name().compareWithoutCase(device->name())) { throw Error("InputSystem::addDevice", "Multiple devices with name:" + device->name() + " cannot coexist"); } } // Add this device to the collection. devices.append(device); } } return device; } void echoSymbolicEvent(ddevent_t const &ev) { DENG2_ASSERT(symbolicEchoMode); // Some event types are never echoed. if(ev.type == E_SYMBOLIC || ev.type == E_FOCUS) return; // Input device axis controls can be pretty sensitive (or misconfigured) // so we need to do some filtering to determine if the motion is strong // enough for an echo. if(ev.type == E_AXIS) { InputDevice const &device = self.device(ev.device); float const pos = device.axis(ev.axis.id).translateRealPosition(ev.axis.pos); if((ev.axis.type == EAXIS_ABSOLUTE && fabs(pos) < .5f) || (ev.axis.type == EAXIS_RELATIVE && fabs(pos) < .02f)) { return; // Not significant enough. } } // Echo the event. String name = "echo-" + B_EventToString(ev); Block const nameUtf8 = name.toUtf8(); ddevent_t echo; de::zap(echo); echo.device = uint(-1); echo.type = E_SYMBOLIC; echo.symbolic.id = 0; echo.symbolic.name = nameUtf8.constData(); LOG_INPUT_XVERBOSE("Symbolic echo: %s") << name; self.postEvent(&echo); } /** * Send all the events of the given timestamp down the responder chain. */ void dispatchEvents(eventqueue_t *q, timespan_t ticLength, bool updateAxes = true) { bool const callGameResponders = App_GameLoaded(); ddevent_t *ddev; while((ddev = nextFromQueue(q))) { // Update the state of the input device tracking table. self.trackEvent(*ddev); if(ignoreInput && ddev->type != E_FOCUS) continue; event_t ev; bool validGameEvent = false; if(callGameResponders) { // Events must first be converted for the game responders. validGameEvent = self.convertEvent(*ddev, ev); } if(callGameResponders && validGameEvent && gx.PrivilegedResponder) { // Does the game's special responder use this event? This is // intended for grabbing events when creating bindings in the // Controls menu. if(gx.PrivilegedResponder(&ev)) { continue; } } if(symbolicEchoMode) { echoSymbolicEvent(*ddev); continue; } // Try the binding system to see if we need to respond to the event // and if so, trigger any associated actions. if(self.tryEvent(*ddev)) { continue; } // Try the "fallback" responder, which, gets the event if no one else // is interested. if(callGameResponders && validGameEvent && gx.FallbackResponder) { gx.FallbackResponder(&ev); } } if(updateAxes) { // An event may have modified device-control state: update the axis positions. for(InputDevice *device : devices) { device->forAllControls([&ticLength] (InputDeviceControl &ctrl) { if(auto *axis = ctrl.maybeAs()) { axis->update(ticLength); } return LoopContinue; }); } } } /** * Poll all event sources (i.e., input devices) and post events. */ void postEventsForAllDevices() { readKeyboard(); readMouse(); readJoystick(); readHeadTracker(); } /** * Check the current keyboard state, generate input events based on pressed/held * keys and poss them. * * @todo Does not belong at this level. */ void readKeyboard() { #define QUEUESIZE 32 if(novideo) return; ddevent_t ev; de::zap(ev); ev.device = IDEV_KEYBOARD; ev.type = E_TOGGLE; ev.toggle.state = ETOG_REPEAT; // Read the new keyboard events, convert to ddevents and post them. keyevent_t keyevs[QUEUESIZE]; size_t const numkeyevs = Keyboard_GetEvents(keyevs, QUEUESIZE); for(size_t n = 0; n < numkeyevs; ++n) { keyevent_t *ke = &keyevs[n]; // Check the type of the event. switch(ke->type) { case IKE_DOWN: ev.toggle.state = ETOG_DOWN; break; case IKE_REPEAT: ev.toggle.state = ETOG_REPEAT; break; case IKE_UP: ev.toggle.state = ETOG_UP; break; default: break; } ev.toggle.id = ke->ddkey; // Text content to insert? DENG2_ASSERT(sizeof(ev.toggle.text) == sizeof(ke->text)); std::memcpy(ev.toggle.text, ke->text, sizeof(ev.toggle.text)); LOG_INPUT_XVERBOSE("toggle.id: %i/%c [%s:%u] (state:%i)") << ev.toggle.id << char(ev.toggle.id) << ev.toggle.text << strlen(ev.toggle.text) << ev.toggle.state; self.postEvent(&ev); } #undef QUEUESIZE } /** * Check the current mouse state (axis, buttons and wheel). * Generates events and mickeys and posts them. * * @todo Does not belong at this level. */ void readMouse() { if(!Mouse_IsPresent()) return; mousestate_t mouse; #ifdef OLD_FILTER // Should we test the mouse input frequency? if(mouseFreq > 0) { static uint lastTime = 0; uint nowTime = Timer_RealMilliseconds(); if(nowTime - lastTime < 1000 / mouseFreq) { // Don't ask yet. std::memset(&mouse, 0, sizeof(mouse)); } else { lastTime = nowTime; Mouse_GetState(&mouse); } } else #endif { // Get the mouse state. Mouse_GetState(&mouse); } ddevent_t ev; de::zap(ev); ev.device = IDEV_MOUSE; ev.type = E_AXIS; float xpos = mouse.axis[IMA_POINTER].x; float ypos = mouse.axis[IMA_POINTER].y; /*if(uiMouseMode) { ev.axis.type = EAXIS_ABSOLUTE; } else*/ { ev.axis.type = EAXIS_RELATIVE; ypos = -ypos; } // Post an event per axis. // Don't post empty events. if(xpos) { ev.axis.id = 0; ev.axis.pos = xpos; self.postEvent(&ev); } if(ypos) { ev.axis.id = 1; ev.axis.pos = ypos; self.postEvent(&ev); } // Some very verbose output about mouse buttons. int i = 0; for(; i < IMB_MAXBUTTONS; ++i) { if(mouse.buttonDowns[i] || mouse.buttonUps[i]) break; } if(i < IMB_MAXBUTTONS) { for(i = 0; i < IMB_MAXBUTTONS; ++i) { LOGDEV_INPUT_XVERBOSE("[%02i] %i/%i") << i << mouse.buttonDowns[i] << mouse.buttonUps[i]; } } // Post mouse button up and down events. ev.type = E_TOGGLE; for(i = 0; i < IMB_MAXBUTTONS; ++i) { ev.toggle.id = i; while(mouse.buttonDowns[i] > 0 || mouse.buttonUps[i] > 0) { if(mouse.buttonDowns[i]-- > 0) { ev.toggle.state = ETOG_DOWN; LOG_INPUT_XVERBOSE("Mouse button %i down") << i; self.postEvent(&ev); } if(mouse.buttonUps[i]-- > 0) { ev.toggle.state = ETOG_UP; LOG_INPUT_XVERBOSE("Mouse button %i up") << i; self.postEvent(&ev); } } } } /** * Check the current joystick state (axis, sliders, hat and buttons). * Generates events and posts them. Axis clamps and dead zone is done * here. * * @todo Does not belong at this level. */ void readJoystick() { if(!Joystick_IsPresent()) return; joystate_t state; Joystick_GetState(&state); // Joystick buttons. ddevent_t ev; de::zap(ev); ev.device = IDEV_JOY1; ev.type = E_TOGGLE; for(int i = 0; i < state.numButtons; ++i) { ev.toggle.id = i; while(state.buttonDowns[i] > 0 || state.buttonUps[i] > 0) { if(state.buttonDowns[i]-- > 0) { ev.toggle.state = ETOG_DOWN; self.postEvent(&ev); LOG_INPUT_XVERBOSE("Joy button %i down") << i; } if(state.buttonUps[i]-- > 0) { ev.toggle.state = ETOG_UP; self.postEvent(&ev); LOG_INPUT_XVERBOSE("Joy button %i up") << i; } } } if(state.numHats > 0) { // Check for a POV change. /// @todo: Some day it would be nice to support multiple hats here. -jk if(state.hatAngle[0] != oldPOV) { ev.type = E_ANGLE; ev.angle.id = 0; if(state.hatAngle[0] < 0) { ev.angle.pos = -1; } else { // The new angle becomes active. ev.angle.pos = ROUND(state.hatAngle[0] / 45); } self.postEvent(&ev); oldPOV = state.hatAngle[0]; } } // Send joystick axis events, one per axis. ev.type = E_AXIS; for(int i = 0; i < state.numAxes; ++i) { ev.axis.id = i; ev.axis.pos = state.axis[i]; ev.axis.type = EAXIS_ABSOLUTE; self.postEvent(&ev); } } /// @todo Does not belong at this level. void readHeadTracker() { // These values are for the input subsystem and gameplay. The renderer will // check the head orientation independently, with as little latency as possible. // If a head tracking device is connected, the device is marked active. if(!DD_GetInteger(DD_USING_HEAD_TRACKING)) { self.device(IDEV_HEAD_TRACKER).deactivate(); return; } self.device(IDEV_HEAD_TRACKER).activate(); // Get the latest values. //vrCfg().oculusRift().allowUpdate(); //vrCfg().oculusRift().update(); ddevent_t ev; de::zap(ev); ev.device = IDEV_HEAD_TRACKER; ev.type = E_AXIS; ev.axis.type = EAXIS_ABSOLUTE; Vector3f const pry = vrCfg().oculusRift().headOrientation(); // Yaw (1.0 means 180 degrees). ev.axis.id = 0; // Yaw. ev.axis.pos = de::radianToDegree(pry[2]) * 1.0 / 180.0; self.postEvent(&ev); ev.axis.id = 1; // Pitch (1.0 means 85 degrees). ev.axis.pos = de::radianToDegree(pry[0]) * 1.0 / 85.0; self.postEvent(&ev); // So I'll assume that if roll ever gets used, 1.0 will mean 180 degrees there too. ev.axis.id = 2; // Roll. ev.axis.pos = de::radianToDegree(pry[1]) * 1.0 / 180.0; self.postEvent(&ev); } /** * Mark all device states with the highest-priority binding context to which they * have a connection via device bindings. This ensures that if a high-priority context * is using a particular device state, lower-priority contexts will not be using the * same state for their own controls. * * Called automatically whenever a context is activated or deactivated. */ void updateAllDeviceStateAssociations() { // Clear all existing associations. for(InputDevice *device : devices) { device->forAllControls([] (InputDeviceControl &ctrl) { ctrl.clearBindContextAssociation(); return LoopContinue; }); } // Mark all bindings in all contexts. for(BindContext *context : contexts) { // Skip inactive contexts. if(!context->isActive()) continue; context->forAllCommandBindings([this, &context] (Record &rec) { CommandBinding bind(rec); InputDeviceControl *ctrl = nullptr; switch(bind.geti("type")) { case E_AXIS: ctrl = &self.device(bind.geti("deviceId")).axis (bind.geti("controlId")); break; case E_TOGGLE: ctrl = &self.device(bind.geti("deviceId")).button(bind.geti("controlId")); break; case E_ANGLE: ctrl = &self.device(bind.geti("deviceId")).hat (bind.geti("controlId")); break; case E_SYMBOLIC: break; default: DENG2_ASSERT(!"InputSystem::updateAllDeviceStateAssociations: Invalid bind.type"); break; } if(ctrl && !ctrl->hasBindContext()) { ctrl->setBindContext(context); } return LoopContinue; }); // Associate all the device bindings. context->forAllImpulseBindings([this, &context] (Record &rec) { ImpulseBinding bind(rec); InputDevice &dev = self.device(bind.geti("deviceId")); InputDeviceControl *ctrl = nullptr; switch(bind.geti("type")) { case IBD_AXIS: ctrl = &dev.axis (bind.geti("controlId")); break; case IBD_TOGGLE: ctrl = &dev.button(bind.geti("controlId")); break; case IBD_ANGLE: ctrl = &dev.hat (bind.geti("controlId")); break; default: DENG2_ASSERT(!"InputSystem::updateAllDeviceStateAssociations: Invalid bind.type"); break; } if(ctrl && !ctrl->hasBindContext()) { ctrl->setBindContext(context); } return LoopContinue; }); // If the context have made a broad device acquisition, mark all relevant states. for(int i = 0; i < devices.count(); ++i) { InputDevice *device = devices.at(i); int const deviceId = i; if(device->isActive() && context->willAcquire(deviceId)) { device->forAllControls([&context] (InputDeviceControl &ctrl) { if(!ctrl.hasBindContext()) { ctrl.setBindContext(context); } return LoopContinue; }); } }; } // Now that we know what are the updated context associations, let's check // the devices and see if any of the states need to be expired. for(InputDevice *device : devices) { device->forAllControls([] (InputDeviceControl &ctrl) { if(!ctrl.inDefaultState()) { ctrl.expireBindContextAssociationIfChanged(); } return LoopContinue; }); }; } /** * When the active state of a binding context changes we'll re-evaluate the * effective bindings to avoid wasting time by looking them up repeatedly. */ void bindContextActiveChanged(BindContext &context) { updateAllDeviceStateAssociations(); for(int i = 0; i < devices.count(); ++i) { InputDevice *device = devices.at(i); int const deviceId = i; if(context.willAcquire(deviceId)) { device->reset(); } } } void bindContextAcquireDeviceChanged(BindContext &) { updateAllDeviceStateAssociations(); } void bindContextBindingAdded(BindContext &, Record & /*binding*/, bool /*isCommand*/) { updateAllDeviceStateAssociations(); } }; InputSystem::InputSystem() : d(new Instance(this)) { initAllDevices(); } void InputSystem::timeChanged(Clock const &) {} SettingsRegister &InputSystem::settings() { return d->settings; } InputDevice &InputSystem::device(int id) const { if(id >= 0 && id < d->devices.count()) { return *d->devices.at(id); } /// @throw MissingDeviceError Given id is not valid. throw MissingDeviceError("InputSystem::device", "Unknown id:" + String::number(id)); } InputDevice *InputSystem::devicePtr(int id) const { if(id >= 0 && id < d->devices.count()) { return d->devices.at(id); } return nullptr; } LoopResult InputSystem::forAllDevices(std::function func) const { for(InputDevice *device : d->devices) { if(auto result = func(*device)) return result; } return LoopContinue; } int InputSystem::deviceCount() const { return d->devices.count(); } void InputSystem::initAllDevices() { d->clearAllDevices(); // Initialize devices. d->addDevice(makeKeyboard("key", "Keyboard"))->activate(); // A keyboard is assumed to always be present. d->addDevice(makeMouse("mouse", "Mouse"))->activate(Mouse_IsPresent()); // A mouse may not be present. d->addDevice(makeJoystick("joy", "Joystick"))->activate(Joystick_IsPresent()); // A joystick may not be present. /// @todo: Add support for multiple joysticks (just some generics, for now). d->addDevice(new InputDevice("joy2")); d->addDevice(new InputDevice("joy3")); d->addDevice(new InputDevice("joy4")); d->addDevice(makeHeadTracker("head", "Head Tracker")); // Head trackers are activated later. // Register console variables for the controls of all devices. for(InputDevice *device : d->devices) device->consoleRegister(); } bool InputSystem::ignoreEvents(bool yes) { LOG_AS("InputSystem"); bool const oldIgnoreInput = d->ignoreInput; d->ignoreInput = yes; LOG_INPUT_VERBOSE("Ignoring events: %b") << yes; if(!yes) { // Clear all the event buffers. d->postEventsForAllDevices(); clearEvents(); // Also reset input device state so that controls don't get stuck if they // are released during the ignoring. for(InputDevice *dev : d->devices) dev->reset(); } return oldIgnoreInput; } void InputSystem::clearEvents() { clearQueue(&queue); clearQueue(&sharpQueue); clearEventStrings(); } /// @note Called by the I/O functions when input is detected. void InputSystem::postEvent(ddevent_t *ev) { DENG2_ASSERT(ev);// && ev->device < NUM_INPUT_DEVICES); eventqueue_t *q = &queue; if(useSharpInputEvents && (ev->type == E_TOGGLE || ev->type == E_AXIS || ev->type == E_ANGLE)) { q = &sharpQueue; } // Cleanup: make sure only keyboard toggles can have a text insert. if(ev->type == E_TOGGLE && ev->device != IDEV_KEYBOARD) { std::memset(ev->toggle.text, 0, sizeof(ev->toggle.text)); } postToQueue(q, ev); #ifdef LIBDENG_CAMERA_MOVEMENT_ANALYSIS if(ev->device == IDEV_KEYBOARD && ev->type == E_TOGGLE && ev->toggle.state == ETOG_DOWN) { extern float devCameraMovementStartTime; extern float devCameraMovementStartTimeRealSecs; // Restart timer on each key down. devCameraMovementStartTime = sysTime; devCameraMovementStartTimeRealSecs = Sys_GetRealSeconds(); } #endif } void InputSystem::processEvents(timespan_t ticLength) { // Poll all event sources (i.e., input devices) and post events. d->postEventsForAllDevices(); // Dispatch all accumulated events down the responder chain. d->dispatchEvents(&queue, ticLength, !useSharpInputEvents); } void InputSystem::processSharpEvents(timespan_t ticLength) { // Sharp ticks may have some events queued on the side. if(DD_IsSharpTick() || !DD_IsFrameTimeAdvancing()) { d->dispatchEvents(&sharpQueue, DD_IsFrameTimeAdvancing()? SECONDSPERTIC : ticLength); } } bool InputSystem::tryEvent(ddevent_t const &event, String const &namedContext) { if(namedContext.isEmpty()) { // Try all active contexts in order. for(BindContext const *context : d->contexts) { if(context->tryEvent(event)) return true; } return false; } // Check a specific binding context for an action (regardless of its activation status). if(hasContext(namedContext)) { return context(namedContext).tryEvent(event, false/*this context only*/); } return false; } bool InputSystem::tryEvent(Event const &event, String const &namedContext) { ddevent_t ddev; convertEvent(event, ddev); return tryEvent(ddev, namedContext); } void InputSystem::trackEvent(ddevent_t const &event) { if(event.type == E_FOCUS || event.type == E_SYMBOLIC) return; // Not a tracked device state. InputDevice *dev = devicePtr(event.device); if(!dev || !dev->isActive()) return; // Track the state of Shift and Alt. if(event.device == IDEV_KEYBOARD && event.type == E_TOGGLE) { if(event.toggle.id == DDKEY_RSHIFT) { if(event.toggle.state == ETOG_DOWN) { ::shiftDown = true; } else if(event.toggle.state == ETOG_UP) { ::shiftDown = false; } } else if(event.toggle.id == DDKEY_RALT) { if(event.toggle.state == ETOG_DOWN) { ::altDown = true; //qDebug() << "Alt down"; } else if(event.toggle.state == ETOG_UP) { ::altDown = false; //qDebug() << "Alt up"; } } } // Update the state table. switch(event.type) { case E_AXIS: dev->axis(event.axis.id).applyRealPosition(event.axis.pos); break; case E_TOGGLE: dev->button(event.toggle.id).setDown(event.toggle.state == ETOG_DOWN || event.toggle.state == ETOG_REPEAT); break; case E_ANGLE: dev->hat(event.angle.id).setPosition(event.angle.pos); break; default: break; } } void InputSystem::trackEvent(Event const &event) { ddevent_t ddev; convertEvent(event, ddev); trackEvent(ddev); } bool InputSystem::convertEvent(Event const &from, ddevent_t &to) // static { de::zap(to); switch(from.type()) { case Event::KeyPress: case Event::KeyRelease: { KeyEvent const &kev = from.as(); to.device = IDEV_KEYBOARD; to.type = E_TOGGLE; to.toggle.id = kev.ddKey(); to.toggle.state = (kev.state() == KeyEvent::Pressed? ETOG_DOWN : ETOG_UP); qstrcpy(to.toggle.text, kev.text().toLatin1()); break; } default: break; } return true; } bool InputSystem::convertEvent(ddevent_t const &from, event_t &to) // static { // Copy the essentials into a cutdown version for the game. // Ensure the format stays the same for future compatibility! // /// @todo This is probably broken! (DD_MICKEY_ACCURACY=1000 no longer used...) // de::zap(to); if(from.type == E_SYMBOLIC) { to.type = EV_SYMBOLIC; #ifdef __64BIT__ ASSERT_64BIT(from.symbolic.name); to.data_u64 = (duint64) from.symbolic.name; #else ASSERT_NOT_64BIT(from.symbolic.name); to.data1 = (int) from.symbolic.name; to.data2 = 0; #endif } else if(from.type == E_FOCUS) { to.type = EV_FOCUS; to.data1 = from.focus.gained; to.data2 = from.focus.inWindow; } else { switch(from.device) { case IDEV_KEYBOARD: to.type = EV_KEY; if(from.type == E_TOGGLE) { to.state = ( from.toggle.state == ETOG_UP ? EVS_UP : from.toggle.state == ETOG_DOWN? EVS_DOWN : EVS_REPEAT ); to.data1 = from.toggle.id; } break; case IDEV_MOUSE: if(from.type == E_AXIS) { to.type = EV_MOUSE_AXIS; } else if(from.type == E_TOGGLE) { to.type = EV_MOUSE_BUTTON; to.data1 = from.toggle.id; to.state = ( from.toggle.state == ETOG_UP ? EVS_UP : from.toggle.state == ETOG_DOWN? EVS_DOWN : EVS_REPEAT ); } break; case IDEV_JOY1: case IDEV_JOY2: case IDEV_JOY3: case IDEV_JOY4: if(from.type == E_AXIS) { int *data = &to.data1; to.type = EV_JOY_AXIS; to.state = (evstate_t) 0; if(from.axis.id >= 0 && from.axis.id < 6) { data[from.axis.id] = from.axis.pos; } /// @todo The other dataN's must contain up-to-date information /// as well. Read them from the current joystick status. } else if(from.type == E_TOGGLE) { to.type = EV_JOY_BUTTON; to.state = ( from.toggle.state == ETOG_UP ? EVS_UP : from.toggle.state == ETOG_DOWN? EVS_DOWN : EVS_REPEAT ); to.data1 = from.toggle.id; } else if(from.type == E_ANGLE) { to.type = EV_POV; } break; case IDEV_HEAD_TRACKER: // No game-side equivalent exists. return false; default: DENG2_ASSERT(!"InputSystem::convertEvent: Unknown device ID in ddevent_t"); return false; } } return true; } bool InputSystem::shiftDown() const { return ::shiftDown; } void InputSystem::initialContextActivations() { // Deactivate all contexts. for(BindContext *context : d->contexts) context->deactivate(); // These are the contexts active by default. context(GLOBAL_BINDING_CONTEXT_NAME ).activate(); context(DEFAULT_BINDING_CONTEXT_NAME).activate(); } void InputSystem::clearAllContexts() { if(d->contexts.isEmpty()) return; qDeleteAll(d->contexts); d->contexts.clear(); // We can restart the id counter, all the old bindings were removed. Binding::resetIdentifiers(); } int InputSystem::contextCount() const { return d->contexts.count(); } bool InputSystem::hasContext(String const &name) const { return contextPtr(name) != nullptr; } BindContext &InputSystem::context(String const &name) const { if(BindContext *context = contextPtr(name)) { return *context; } /// @throw MissingContextError Specified name is unknown. throw MissingContextError("InputSystem::context", "Unknown name:" + name); } /// @todo: Optimize O(n) search... BindContext *InputSystem::contextPtr(String const &name) const { for(BindContext const *context : d->contexts) { if(!context->name().compareWithoutCase(name)) { return const_cast(context); } } return nullptr; } BindContext &InputSystem::contextAt(int position) const { if(position >= 0 && position < d->contexts.count()) { return *d->contexts.at(position); } /// @throw MissingContextError Specified position is invalid. throw MissingContextError("InputSystem::contextAt", "Invalid position:" + String::number(position)); } int InputSystem::contextPositionOf(BindContext *context) const { return (context? d->contexts.indexOf(context) : -1); } BindContext *InputSystem::newContext(String const &name) { // Ensure the given name is unique. if(hasContext(name)) { LOG_AS("InputSystem::newContext"); LOG_INPUT_WARNING("Name '%s' is not unique - Won't replace") << name; return nullptr; } BindContext *context = new BindContext(name); d->contexts.prepend(context); context->audienceForActiveChange() += d; context->audienceForAcquireDeviceChange() += d; context->audienceForBindingAddition() += d; return context; } LoopResult InputSystem::forAllContexts(std::function func) const { for(BindContext *context : d->contexts) { if(auto result = func(*context)) return result; } return LoopContinue; } // --------------------------------------------------------------------------- void InputSystem::bindDefaults() { // Engine's highest priority context: opening control panel, opening the console. bindCommand("global:key-f11-down + key-alt-down", "releasemouse"); bindCommand("global:key-f11-down", "togglefullscreen"); bindCommand("global:key-tilde-down + key-shift-up", "taskbar"); #ifdef WIN32 bindCommand("global:key-winmenu-down", "taskbar"); #endif // Console bindings (when open). bindCommand("console:key-tilde-down + key-shift-up", "taskbar"); // without this, key would be entered into command line // Bias editor. } void InputSystem::bindGameDefaults() { if(!App_GameLoaded()) return; Con_Executef(CMDS_DDAY, false, "defaultgamebindings"); } static char const *parseContext(String &context, char const *desc) { DENG2_ASSERT(desc); if(!strchr(desc, ':')) { // No context defined. context = ""; return desc; } AutoStr *str = AutoStr_NewStd(); desc = Str_CopyDelim(str, desc, ':'); context = Str_Text(str); return desc; } Record *InputSystem::bindCommand(char const *eventDesc, char const *command) { DENG2_ASSERT(eventDesc && command); LOG_AS("InputSystem"); // The event descriptor may begin with a symbolic binding context name. String contextName; eventDesc = parseContext(contextName, eventDesc); if(BindContext *context = contextPtr(contextName.isEmpty()? DEFAULT_BINDING_CONTEXT_NAME : contextName)) { return context->bindCommand(eventDesc, command); } return nullptr; } Record *InputSystem::bindImpulse(char const *ctrlDesc, char const *impulseDesc) { DENG2_ASSERT(ctrlDesc && impulseDesc); LOG_AS("InputSystem"); // The impulse descriptor may begin with the local player number. int localPlayer = 0; AutoStr *str = AutoStr_NewStd(); char const *ptr = Str_CopyDelim(str, impulseDesc, '-'); if(!qstrnicmp(Str_Text(str), "local", 5) && Str_Length(str) > 5) { localPlayer = String((Str_Text(str) + 5)).toInt() - 1; if(localPlayer < 0 || localPlayer >= DDMAXPLAYERS) { LOG_INPUT_WARNING("Local player number %i is invalid") << localPlayer; return nullptr; } // Skip past it. impulseDesc = ptr; } // The next part must be the impulse name. impulseDesc = Str_CopyDelim(str, impulseDesc, '-'); PlayerImpulse const *impulse = P_PlayerImpulseByName(Str_Text(str)); if(!impulse) { LOG_INPUT_WARNING("Player impulse \"%s\" not defined") << Str_Text(str); return nullptr; } BindContext *context = contextPtr(impulse->bindContextName); if(!context) { context = contextPtr(DEFAULT_BINDING_CONTEXT_NAME); } DENG2_ASSERT(context); return context->bindImpulse(ctrlDesc, *impulse, localPlayer); } bool InputSystem::removeBinding(int id) { LOG_AS("InputSystem"); for(BindContext *context : d->contexts) { if(bool result = context->deleteBinding(id)) return result; } return false; } void InputSystem::removeAllBindings() { for(BindContext *context : d->contexts) { context->clearAllBindings(); }; // We can restart the id counter, all the old bindings were removed. Binding::resetIdentifiers(); } D_CMD(ListDevices) { DENG2_UNUSED3(src, argc, argv); InputSystem &isys = ClientApp::inputSystem(); LOG_INPUT_MSG(_E(b) "%i input devices initalized:") << isys.deviceCount(); isys.forAllDevices([] (InputDevice &device) { LOG_INPUT_MSG("") << device.description(); return LoopContinue; }); return true; } D_CMD(ReleaseMouse) { DENG2_UNUSED3(src, argc, argv); if(WindowSystem::mainExists()) { ClientWindowSystem::main().canvas().trapMouse(false); return true; } return false; } D_CMD(ListContexts) { DENG2_UNUSED3(src, argc, argv); InputSystem &isys = ClientApp::inputSystem(); LOG_INPUT_MSG(_E(b) "%i binding contexts defined:") << isys.contextCount(); int idx = 0; isys.forAllContexts([&idx] (BindContext &context) { LOG_INPUT_MSG(" [%2i] " _E(>) "%s%s" _E(.) _E(2) " (%s)") << (idx++) << (context.isActive()? _E(b) : _E(w)) << context.name() << (context.isActive()? "active" : "inactive"); return LoopContinue; }); return true; } /* D_CMD(ClearContexts) { ClientApp::inputSystem().clearAllContexts(); return true; } */ D_CMD(ActivateContext) { DENG2_UNUSED2(src, argc); InputSystem &isys = ClientApp::inputSystem(); bool const doActivate = !String(argv[0]).compareWithoutCase("activatebcontext"); String const name = argv[1]; if(!isys.hasContext(name)) { LOG_INPUT_WARNING("Unknown binding context '%s'") << name; return false; } BindContext &context = isys.context(name); if(context.isProtected()) { LOG_INPUT_ERROR("Binding context " _E(b) "'%s'" _E(.) " is " _E(2) "protected" _E(.) " and cannot be manually %s") << context.name() << (doActivate? "activated" : "deactivated"); return false; } context.activate(doActivate); return true; } D_CMD(BindCommand) { DENG2_UNUSED2(src, argc); return ClientApp::inputSystem().bindCommand(argv[1], argv[2]) != nullptr; } D_CMD(BindImpulse) { DENG2_UNUSED2(src, argc); return ClientApp::inputSystem().bindImpulse(argv[2], argv[1]) != nullptr; } D_CMD(ListBindings) { DENG2_UNUSED3(src, argc, argv); InputSystem &isys = ClientApp::inputSystem(); int totalBindCount = 0; isys.forAllContexts([&totalBindCount] (BindContext &context) { totalBindCount += context.commandBindingCount() + context.impulseBindingCount(); return LoopContinue; }); LOG_INPUT_MSG(_E(b) "Bindings"); LOG_INPUT_MSG("There are " _E(b) "%i" _E(.) " bindings, in " _E(b) "%i" _E(.) " contexts") << totalBindCount << isys.contextCount(); isys.forAllContexts([] (BindContext &context) { int const cmdCount = context.commandBindingCount(); int const impCount = context.impulseBindingCount(); // Skip empty contexts. if(cmdCount + impCount == 0) return LoopContinue; LOG_INPUT_MSG(_E(D)_E(b) "%s" _E(.) " context: " _E(l) "(%i %s)") << context.name() << (cmdCount + impCount) << (context.isActive()? "active" : "inactive"); // Commands. if(cmdCount) { LOG_INPUT_MSG(" " _E(b) "%i command bindings:") << cmdCount; } context.forAllCommandBindings([] (Record &rec) { CommandBinding bind(rec); LOG_INPUT_MSG(" [%3i] " _E(>) _E(b) "%s" _E(.) " %s") << bind.geti("id") << bind.composeDescriptor() << bind.gets("command"); return LoopContinue; }); // Impulses. if(impCount) { LOG_INPUT_MSG(" " _E(b) "%i impulse bindings:") << impCount; } for(int pl = 0; pl < DDMAXPLAYERS; ++pl) context.forAllImpulseBindings(pl, [&pl] (Record &rec) { ImpulseBinding bind(rec); PlayerImpulse const *impulse = P_PlayerImpulsePtr(bind.geti("impulseId")); DENG2_ASSERT(impulse); LOG_INPUT_MSG(" [%3i] " _E(>) _E(b) "%s" _E(.) " player%i %s") << bind.geti("id") << bind.composeDescriptor() << (pl + 1) << impulse->name; return LoopContinue; }); return LoopContinue; }); return true; } D_CMD(ClearBindings) { DENG2_UNUSED3(src, argc, argv); ClientApp::inputSystem().removeAllBindings(); return true; } D_CMD(RemoveBinding) { DENG2_UNUSED2(src, argc); int const id = String(argv[1]).toInt(); if(ClientApp::inputSystem().removeBinding(id)) { LOG_INPUT_MSG("Binding " _E(b) "%i" _E(.) " deleted") << id; } else { LOG_INPUT_ERROR("Unknown binding #%i") << id; } return true; } D_CMD(DefaultBindings) { DENG2_UNUSED3(src, argc, argv); InputSystem &isys = ClientApp::inputSystem(); isys.bindDefaults(); isys.bindGameDefaults(); return true; } void InputSystem::consoleRegister() // static { #define PROTECTED_FLAGS (CMDF_NO_DEDICATED | CMDF_DED | CMDF_CLIENT) // Variables: C_VAR_BYTE("input-conflict-zerocontrol", &zeroControlUponConflict, 0, 0, 1); C_VAR_BYTE("input-sharp", &useSharpInputEvents, 0, 0, 1); // Commands: C_CMD_FLAGS("activatebcontext", "s", ActivateContext, PROTECTED_FLAGS); C_CMD_FLAGS("bindevent", "ss", BindCommand, PROTECTED_FLAGS); C_CMD_FLAGS("bindcontrol", "ss", BindImpulse, PROTECTED_FLAGS); //C_CMD_FLAGS("clearbcontexts", "", ClearContexts, PROTECTED_FLAGS); C_CMD_FLAGS("clearbindings", "", ClearBindings, PROTECTED_FLAGS); C_CMD_FLAGS("deactivatebcontext", "s", ActivateContext, PROTECTED_FLAGS); C_CMD_FLAGS("defaultbindings", "", DefaultBindings, PROTECTED_FLAGS); C_CMD_FLAGS("delbind", "i", RemoveBinding, PROTECTED_FLAGS); C_CMD_FLAGS("listbcontexts", nullptr, ListContexts, PROTECTED_FLAGS); C_CMD_FLAGS("listbindings", nullptr, ListBindings, PROTECTED_FLAGS); C_CMD ("listinputdevices", "", ListDevices); C_CMD ("releasemouse", "", ReleaseMouse); //C_CMD_FLAGS("setaxis", "s", AxisPrintConfig, CMDF_NO_DEDICATED); //C_CMD_FLAGS("setaxis", "ss", AxisChangeOption, CMDF_NO_DEDICATED); //C_CMD_FLAGS("setaxis", "sss", AxisChangeValue, CMDF_NO_DEDICATED); #ifdef DENG2_DEBUG I_DebugDrawerConsoleRegister(); #endif #undef PROTECTED_FLAGS } DENG_EXTERN_C void B_SetContextFallback(char const *name, int (*responderFunc)(event_t *)) { InputSystem &isys = ClientApp::inputSystem(); if(isys.hasContext(name)) { isys.context(name).setFallbackResponder(responderFunc); } } DENG_EXTERN_C int B_BindingsForCommand(char const *commandCString, char *outBuf, size_t outBufSize) { DENG2_ASSERT(commandCString && outBuf); String const command = commandCString; *outBuf = 0; if(command.isEmpty()) return 0; if(!outBufSize) return 0; InputSystem &isys = ClientApp::inputSystem(); String out; int numFound = 0; isys.forAllContexts([&] (BindContext &context) { context.forAllCommandBindings([&] (Record &rec) { CommandBinding bind(rec); if(!bind.gets("command").compareWithCase(command)) { if(numFound) out += " "; // Separator. out += String::number(bind.geti("id")) + "@" + context.name() + ":" + bind.composeDescriptor(); numFound++; } return LoopContinue; }); return LoopContinue; }); // Copy the result to the return buffer. std::memset(outBuf, 0, outBufSize); qstrncpy(outBuf, out.toUtf8().constData(), outBufSize - 1); return numFound; } DENG_EXTERN_C int B_BindingsForControl(int localPlayer, char const *impulseNameCString, int inverse, char *outBuf, size_t outBufSize) { DENG2_ASSERT(impulseNameCString && outBuf); String const impulseName = impulseNameCString; *outBuf = 0; if(localPlayer < 0 || localPlayer >= DDMAXPLAYERS) return 0; if(impulseName.isEmpty()) return 0; if(!outBufSize) return 0; InputSystem &isys = ClientApp::inputSystem(); String out; int numFound = 0; isys.forAllContexts([&] (BindContext &context) { context.forAllImpulseBindings(localPlayer, [&] (Record &rec) { ImpulseBinding bind(rec); DENG2_ASSERT(bind.geti("localPlayer") == localPlayer); PlayerImpulse const *impulse = P_PlayerImpulsePtr(bind.geti("impulseId")); DENG2_ASSERT(impulse); if(!impulse->name.compareWithoutCase(impulseName)) { if(inverse == BFCI_BOTH || (inverse == BFCI_ONLY_NON_INVERSE && !(bind.geti("flags") & IBDF_INVERSE)) || (inverse == BFCI_ONLY_INVERSE && (bind.geti("flags") & IBDF_INVERSE))) { if(numFound) out += " "; out += String::number(bind.geti("id")) + "@" + context.name() + ":" + bind.composeDescriptor(); numFound++; } } return LoopContinue; }); return LoopContinue; }); // Copy the result to the return buffer. std::memset(outBuf, 0, outBufSize); qstrncpy(outBuf, out.toUtf8().constData(), outBufSize - 1); return numFound; } DENG_EXTERN_C int DD_GetKeyCode(char const *key) { DENG2_ASSERT(key); int code = B_KeyForShortName(key); return (code? code : key[0]); } DENG_DECLARE_API(B) = { { DE_API_BINDING }, B_SetContextFallback, B_BindingsForCommand, B_BindingsForControl, DD_GetKeyCode }; doomsday-stable-1.15.7/doomsday/client/src/settingsregister.cpp0000664000175000017500000004246712641367670024206 0ustar jaakkojaakko/** @file settingsregister.cpp Register of settings profiles. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "settingsregister.h" #include "api_console.h" #include #include #include #include #include #include #include #include #include #include #include using namespace de; static String const CUSTOM_PROFILE = "Custom"; DENG2_PIMPL(SettingsRegister), DENG2_OBSERVES(App, GameUnload), DENG2_OBSERVES(App, GameChange) { struct Setting { SettingType type; String name; Setting() : type(IntCVar) {} Setting(SettingType t, String n) : type(t), name(n) {} /** * Changes the current value of the setting. * * @param val New value. */ void setValue(QVariant const &val) const { switch(type) { case IntCVar: Con_SetInteger(name.toLatin1(), val.toInt()); break; case FloatCVar: Con_SetFloat(name.toLatin1(), val.toFloat()); break; case StringCVar: Con_SetString(name.toLatin1(), val.toString().toUtf8()); break; case ConfigVariable: App::config()[name].set(NumberValue(val.toDouble())); break; } } }; typedef QMap Settings; Settings settings; struct Profile { bool readOnly; ///< Profile has been loaded from a read-only file, won't be serialized. typedef QMap Values; Values values; Profile() : readOnly(false) {} }; typedef QMap Profiles; Profiles profiles; Profile defaults; String persistentName; String current; Instance(Public *i) : Base(i), current(CUSTOM_PROFILE) { App::app().audienceForGameUnload() += this; App::app().audienceForGameChange() += this; addProfile(current); } ~Instance() { App::app().audienceForGameUnload() -= this; App::app().audienceForGameChange() -= this; clearProfiles(); } Profile *addProfile(String const &name) { if(profiles.contains(name)) { delete profiles[name]; } Profile *p = new Profile; profiles.insert(name, p); return p; } void clearProfiles() { qDeleteAll(profiles.values()); profiles.clear(); } Profile ¤tProfile() const { DENG2_ASSERT(profiles.contains(current)); return *profiles[current]; } QVariant getDefaultFromConfig(String const &name) { try { // Get defaults for script values. Script script("record d; import Config; Config.setDefaults(d); d"); Process proc(script); proc.execute(); Record const &confDefaults = *proc.context().evaluator().result() .as().record(); DENG2_ASSERT(confDefaults.has(name)); Variable const &var = confDefaults[name]; if(var.value().is()) { return var.value().asNumber(); } else { // Oops, we don't support this yet. DENG2_ASSERT(false); } } catch(Error const &er) { LOG_WARNING("Failed to find default for \"%s\": %s") << name << er.asText(); } return QVariant(); } /** * Gets the current values of the settings and saves them to a profile. */ void fetch(String const &profileName) { DENG2_ASSERT(profiles.contains(profileName)); Profile &prof = *profiles[profileName]; if(prof.readOnly) return; foreach(Setting const &st, settings.values()) { QVariant val; switch(st.type) { case IntCVar: val = Con_GetInteger(st.name.toLatin1()); break; case FloatCVar: val = Con_GetFloat(st.name.toLatin1()); break; case StringCVar: val = QString(Con_GetString(st.name.toLatin1())); break; case ConfigVariable: val = App::config()[st.name].value().asNumber(); break; } prof.values[st.name] = val; } } void setCurrent(String const &name) { current = name; if(!persistentName.isEmpty()) { App::config().set(confName(), name); } } void apply(String const &profileName) { DENG2_ASSERT(profiles.contains(profileName)); Profile const &prof = *profiles[profileName]; foreach(Setting const &st, settings.values()) { QVariant const &val = prof.values[st.name]; st.setValue(val); } } void changeTo(String const &profileName) { LOG_AS("SettingsRegister"); DENG2_ASSERT(profiles.contains(profileName)); if(current == profileName) return; if(!persistentName.isEmpty()) { LOG_MSG("Changing %s profile to '%s'") << persistentName << profileName; } // First update the old profile. fetch(current); // Then set the values from the other profile. setCurrent(profileName); apply(current); DENG2_FOR_PUBLIC_AUDIENCE(ProfileChange, i) { i->currentProfileChanged(profileName); } } void reset() { if(!currentProfile().readOnly) { currentProfile().values = defaults.values; apply(current); } } void resetSetting(String const &settingName) const { DENG2_ASSERT(settings.contains(settingName)); DENG2_ASSERT(defaults.values.contains(settingName)); settings[settingName].setValue(defaults.values[settingName]); } /** * For a persistent register, determines the name of the Config variable * that stores the name of the currently selected profile. */ String confName() const { if(persistentName.isEmpty()) return ""; return persistentName + ".profile"; } /** * For a persistent register, determines the file name of the Info file * where all the profile values are written to and read from. */ String fileName() const { if(persistentName.isEmpty()) return ""; return String("/home/configs/%1.dei").arg(persistentName); } QVariant textToSettingValue(String const &text, String const &settingName) const { DENG2_ASSERT(settings.contains(settingName)); Setting const &st = settings[settingName]; switch(st.type) { case IntCVar: return text.toInt(); case FloatCVar: return text.toFloat(); case StringCVar: return text; case ConfigVariable: return text.toDouble(); } DENG2_ASSERT(false); return QVariant(); } void loadProfilesFromInfo(File const &file, bool markReadOnly) { try { LOG_RES_VERBOSE("Reading setting profiles from %s") << file.description(); Block raw; file >> raw; de::Info info; info.setAllowDuplicateBlocksOfType(QStringList() << "profile"); info.parse(String::fromUtf8(raw)); foreach(de::Info::Element const *elem, info.root().contentsInOrder()) { if(!elem->isBlock()) continue; // There may be multiple profiles in the file. de::Info::BlockElement const &profBlock = elem->as(); if(profBlock.blockType() == "profile") { String profileName = profBlock.keyValue("name").text; if(profileName.isEmpty()) continue; // Skip this one... LOG_VERBOSE("Reading profile '%s'") << profileName; Profile *prof = addProfile(profileName); if(markReadOnly) prof->readOnly = true; // Use the default settings for anything not defined in the file. prof->values = defaults.values; // Read all the setting values from the profile block. foreach(de::Info::Element const *e, profBlock.contentsInOrder()) { if(!e->isBlock()) continue; de::Info::BlockElement const &setBlock = e->as(); if(settings.contains(setBlock.name())) // ignore unknown settings { prof->values[setBlock.name()] = textToSettingValue(setBlock.keyValue("value").text, setBlock.name()); } } } } } catch(Error const &er) { LOG_RES_WARNING("Failed to load setting profiles from %s:\n%s") << file.description() << er.asText(); } } bool addCustomProfileIfMissing() { if(!profiles.contains(CUSTOM_PROFILE)) { addProfile(CUSTOM_PROFILE); // Use whatever values are currently in effect. fetch(CUSTOM_PROFILE); return true; } return false; // nothing added } /** * Deserializes all the profiles (see aboutToUnloadGame()). In addition, * fixed/built-in profiles are loaded from /data/profiles/(persistentName)/ * * @param newGame New current game. */ void currentGameChanged(game::Game const &newGame) { if(persistentName.isEmpty() || newGame.isNull()) return; LOG_AS("SettingsRegister"); LOG_DEBUG("Game has been loaded, deserializing %s profiles") << persistentName; clearProfiles(); // Read all fixed profiles from */profiles/(persistentName)/ FS::FoundFiles folders; App::fileSystem().findAll(String("profiles") / persistentName, folders); DENG2_FOR_EACH(FS::FoundFiles, i, folders) { if(Folder const *folder = (*i)->maybeAs()) { // Let's see if it contains any .dei files. DENG2_FOR_EACH_CONST(Folder::Contents, k, folder->contents()) { if(k->first.fileNameExtension() == ".dei") { // Load this profile. loadProfilesFromInfo(*k->second, true /* read-only */); } } } } // Read /home/configs/(persistentName).dei if(File const *file = App::rootFolder().tryLocate(fileName())) { loadProfilesFromInfo(*file, false /* modifiable */); } else { // Settings haven't previously been created -- make sure we at least // have the Custom profile. if(addCustomProfileIfMissing()) { current = CUSTOM_PROFILE; } } // Still nothing? addCustomProfileIfMissing(); if(App::config().names().has(confName())) { // Update current profile. current = App::config()[confName()].value().asText(); } if(!profiles.contains(current)) { // Fall back to the one profile we know is available. if(profiles.contains(CUSTOM_PROFILE)) { current = CUSTOM_PROFILE; } else { current = profiles.keys().first(); } } // Make sure these are the values now in use. apply(current); App::config().set(confName(), current); } /** * Serializes all the profiles: * - Config.(persistentName).profile stores the name of the current profile * - /home/configs/(persistentName).dei contains all the existing profile * values, with one file per profile * * @param gameBeingUnloaded Current game. */ void aboutToUnloadGame(game::Game const &gameBeingUnloaded) { if(persistentName.isEmpty() || gameBeingUnloaded.isNull()) return; LOG_AS("SettingsRegister"); LOG_DEBUG("Game being unloaded, serializing %s profiles") << persistentName; // Update the current profile. fetch(current); // Remember which profile is the current one. App::config().set(confName(), current); // We will write one Info file with all the profiles. String info; QTextStream os(&info); os.setCodec("UTF-8"); os << "# Autogenerated Info file based on " << persistentName << " settings\n"; // Write /home/configs/(persistentName).dei with all non-readonly profiles. int count = 0; DENG2_FOR_EACH_CONST(Profiles, i, profiles) { if(i.value()->readOnly) continue; ++count; os << "\nprofile {\n name: " << i.key() << "\n"; DENG2_FOR_EACH_CONST(Profile::Values, val, i.value()->values) { DENG2_ASSERT(settings.contains(val.key())); Setting const &st = settings[val.key()]; String valueText; switch(st.type) { case IntCVar: case FloatCVar: case StringCVar: case ConfigVariable: // QVariant can handle this. valueText = val.value().toString(); break; } os << " setting \"" << st.name << "\" {\n" << " value: " << valueText << "\n" << " }\n"; } os << "}\n"; } // Create the pack and update the file system. File &outFile = App::rootFolder().replaceFile(fileName()); outFile << info.toUtf8(); outFile.flush(); // we're done LOG_VERBOSE("Wrote \"%s\" with %i profile%s") << fileName() << count << (count != 1? "s" : ""); } }; SettingsRegister::SettingsRegister() : d(new Instance(this)) {} void SettingsRegister::setPersistentName(String const &name) { d->persistentName = name; } SettingsRegister &SettingsRegister::define(SettingType type, String const &settingName, QVariant const &defaultValue) { d->settings.insert(settingName, Instance::Setting(type, settingName)); QVariant def; if(type == ConfigVariable) { def = d->getDefaultFromConfig(settingName); } else { def = defaultValue; } d->defaults.values[settingName] = def; return *this; } String SettingsRegister::currentProfile() const { return d->current; } bool SettingsRegister::isReadOnlyProfile(String const &name) const { if(d->profiles.contains(name)) { return d->profiles[name]->readOnly; } DENG2_ASSERT(false); return false; } bool SettingsRegister::saveAsProfile(String const &name) { if(!d->profiles.contains(name) && !name.isEmpty()) { d->addProfile(name); d->fetch(name); return true; } return false; } void SettingsRegister::setProfile(String const &name) { d->changeTo(name); } void SettingsRegister::resetToDefaults() { d->reset(); DENG2_FOR_AUDIENCE(ProfileChange, i) { i->currentProfileChanged(d->current); } } void SettingsRegister::resetSettingToDefaults(String const &settingName) { d->resetSetting(settingName); } bool SettingsRegister::rename(String const &name) { if(!d->profiles.contains(name) && !name.isEmpty()) { Instance::Profile *p = d->profiles.take(d->current); d->profiles.insert(name, p); d->setCurrent(name); DENG2_FOR_AUDIENCE(ProfileChange, i) { i->currentProfileChanged(name); } return true; } return false; } void SettingsRegister::deleteProfile(String const &name) { // Can't delete the current profile. if(name == d->current) return; delete d->profiles.take(name); } QList SettingsRegister::profiles() const { return d->profiles.keys(); } int SettingsRegister::profileCount() const { return d->profiles.size(); } doomsday-stable-1.15.7/doomsday/client/src/con_config.cpp0000664000175000017500000002061312641367670022672 0ustar jaakkojaakko/** @file con_config.cpp Config file IO. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "con_config.h" #include #include #include #include #include #include #include #include #include #include #include "dd_main.h" #include "dd_def.h" #include "m_misc.h" #include "Games" #ifdef __CLIENT__ # include "clientapp.h" # include "world/p_players.h" # include "BindContext" # include "CommandBinding" # include "ImpulseBinding" #endif using namespace de; static Path cfgFile; static int flagsAllow; static void writeHeaderComment(FILE *file) { if(!App_GameLoaded()) { fprintf(file, "# " DOOMSDAY_NICENAME " " DOOMSDAY_VERSION_TEXT "\n"); } else { fprintf(file, "# %s %s / " DOOMSDAY_NICENAME " " DOOMSDAY_VERSION_TEXT "\n", (char *) gx.GetVariable(DD_PLUGIN_NAME), (char *) gx.GetVariable(DD_PLUGIN_VERSION_SHORT)); } fprintf(file, "# This configuration file is generated automatically. Each line is a\n"); fprintf(file, "# console command. Lines beginning with # are comments. Use autoexec.cfg\n"); fprintf(file, "# for your own startup commands.\n\n"); } static int writeVariableToFileWorker(knownword_t const *word, void *context) { FILE *file = (FILE *)context; DENG_ASSERT(file != 0); cvar_t *var = (cvar_t *)word->data; DENG2_ASSERT(var != 0); // Don't archive this cvar? if(var->flags & CVF_NO_ARCHIVE) return 0; AutoStr const *path = CVar_ComposePath(var); // First print the comment (help text). if(char const *str = DH_GetString(DH_Find(Str_Text(path)), HST_DESCRIPTION)) { M_WriteCommented(file, str); } fprintf(file, "%s ", Str_Text(path)); if(var->flags & CVF_PROTECTED) fprintf(file, "force "); switch(var->type) { case CVT_BYTE: fprintf(file, "%d", *(byte *) var->ptr); break; case CVT_INT: fprintf(file, "%d", *(int *) var->ptr); break; case CVT_FLOAT: fprintf(file, "%s", M_TrimmedFloat(*(float *) var->ptr)); break; case CVT_CHARPTR: fprintf(file, "\""); if(CV_CHARPTR(var)) { M_WriteTextEsc(file, CV_CHARPTR(var)); } fprintf(file, "\""); break; case CVT_URIPTR: fprintf(file, "\""); if(CV_URIPTR(var)) { fprintf(file, "%s", CV_URIPTR(var)->compose().toUtf8().constData()); } fprintf(file, "\""); break; default: break; } fprintf(file, "\n\n"); return 0; // Continue iteration. } static void writeVariablesToFile(FILE *file) { Con_IterateKnownWords(0, WT_CVAR, writeVariableToFileWorker, file); } static int writeAliasToFileWorker(knownword_t const *word, void *context) { FILE *file = (FILE *) context; DENG2_ASSERT(file != 0); calias_t *cal = (calias_t *) word->data; DENG2_ASSERT(cal != 0); fprintf(file, "alias \""); M_WriteTextEsc(file, cal->name); fprintf(file, "\" \""); M_WriteTextEsc(file, cal->command); fprintf(file, "\"\n"); return 0; // Continue iteration. } static void writeAliasesToFile(FILE *file) { Con_IterateKnownWords(0, WT_CALIAS, writeAliasToFileWorker, file); } static bool writeConsoleState(Path const &filePath) { if(filePath.isEmpty()) return false; // Ensure the destination directory exists. String fileDir = filePath.toString().fileNamePath(); if(!fileDir.isEmpty()) { F_MakePath(fileDir.toUtf8().constData()); } if(FILE *file = fopen(filePath.toUtf8().constData(), "wt")) { LOG_SCR_VERBOSE("Writing state to \"%s\"...") << NativePath(filePath).pretty(); writeHeaderComment(file); fprintf(file, "#\n# CONSOLE VARIABLES\n#\n\n"); writeVariablesToFile(file); fprintf(file, "\n#\n# ALIASES\n#\n\n"); writeAliasesToFile(file); fclose(file); return true; } LOG_SCR_WARNING("Failed opening \"%s\" for writing") << NativePath(filePath).pretty(); return false; } #ifdef __CLIENT__ static bool writeBindingsState(Path const &filePath) { if(filePath.isEmpty()) return false; // Ensure the destination directory exists. String fileDir = filePath.toString().fileNamePath(); if(!fileDir.isEmpty()) { F_MakePath(fileDir.toUtf8().constData()); } if(FILE *file = fopen(filePath.toUtf8().constData(), "wt")) { InputSystem &isys = ClientApp::inputSystem(); LOG_SCR_VERBOSE("Writing bindings to \"%s\"...") << NativePath(filePath).pretty(); writeHeaderComment(file); // Start with a clean slate when restoring the bindings. fprintf(file, "clearbindings\n\n"); isys.forAllContexts([&isys, &file] (BindContext &context) { // Commands. context.forAllCommandBindings([&file, &context] (Record &rec) { CommandBinding bind(rec); fprintf(file, "bindevent \"%s:%s\" \"", context.name().toUtf8().constData(), bind.composeDescriptor().toUtf8().constData()); M_WriteTextEsc(file, bind.gets("command").toUtf8().constData()); fprintf(file, "\"\n"); return LoopContinue; }); // Impulses. context.forAllImpulseBindings([&file, &context] (Record &rec) { ImpulseBinding bind(rec); PlayerImpulse const *impulse = P_PlayerImpulsePtr(bind.geti("impulseId")); DENG2_ASSERT(impulse); fprintf(file, "bindcontrol local%i-%s \"%s\"\n", bind.geti("localPlayer") + 1, impulse->name.toUtf8().constData(), bind.composeDescriptor().toUtf8().constData()); return LoopContinue; }); return LoopContinue; }); fclose(file); return true; } LOG_SCR_WARNING("Failed opening \"%s\" for writing") << NativePath(filePath).pretty(); return false; } #endif // __CLIENT__ static bool writeState(Path const &filePath, Path const &bindingsFileName = "") { if(!filePath.isEmpty() && (flagsAllow & CPCF_ALLOW_SAVE_STATE)) { writeConsoleState(filePath); } #ifdef __CLIENT__ if(!bindingsFileName.isEmpty() && (flagsAllow & CPCF_ALLOW_SAVE_BINDINGS)) { // Bindings go into a separate file. writeBindingsState(bindingsFileName); } #else DENG2_UNUSED(bindingsFileName); #endif return true; } bool Con_ParseCommands(Path const &fileName, int flags) { bool const setDefault = (flags & CPCF_SET_DEFAULT) != 0; // Is this supposed to be the default? if(setDefault) { cfgFile = fileName; } // Update the allowed operations. flagsAllow |= flags & (CPCF_ALLOW_SAVE_STATE | CPCF_ALLOW_SAVE_BINDINGS); LOG_SCR_VERBOSE("Parsing \"%s\" (setdef:%b)") << NativePath(fileName).pretty() << setDefault; return Con_Parse(fileName, setDefault /* => silently */); } void Con_SaveDefaults() { writeState(cfgFile, (!isDedicated && App_GameLoaded()? App_CurrentGame().bindingConfig() : "")); } D_CMD(WriteConsole) { DENG2_UNUSED2(src, argc); Path filePath = Path(NativePath(argv[1]).expand().withSeparators('/')); LOG_SCR_MSG("Writing to \"%s\"...") << filePath; return !writeState(filePath); } doomsday-stable-1.15.7/doomsday/client/src/dd_pinit.cpp0000664000175000017500000001117712641367670022365 0ustar jaakkojaakko/** @file dd_pinit.cpp Platform independent routines for initializing the engine. * @ingroup base * * @todo Move these to dd_init.cpp. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifdef WIN32 # define WIN32_LEAN_AND_MEAN # include #endif #include "de_base.h" #include "de_console.h" #include "de_system.h" #include "de_play.h" #include "de_network.h" #include "de_ui.h" #include "de_filesys.h" #ifdef __CLIENT__ # include "clientapp.h" #endif #include "def_main.h" #include "gl/svg.h" #ifdef __CLIENT__ # include "render/r_draw.h" # include "render/r_main.h" # include "render/rend_main.h" # include "updater.h" #endif #include "api_internaldata.h" #include #include using namespace de; /* * The game imports and exports. */ DENG_DECLARE_API(InternalData) = { { DE_API_INTERNAL_DATA }, runtimeDefs.mobjInfo.elementsPtr(), runtimeDefs.states .elementsPtr(), runtimeDefs.sprNames.elementsPtr(), runtimeDefs.texts .elementsPtr(), &validCount }; game_export_t __gx; #ifdef __CLIENT__ de::String DD_ComposeMainWindowTitle() { de::String title = DOOMSDAY_NICENAME " " DOOMSDAY_VERSION_TEXT; if(App_GameLoaded() && gx.GetVariable) { title = App_CurrentGame().title() + " - " + title; } return title; } #endif void DD_InitAPI() { GETGAMEAPI GetGameAPI = app.GetGameAPI; zap(__gx); if(GetGameAPI) { game_export_t *gameExPtr = GetGameAPI(); std::memcpy(&__gx, gameExPtr, MIN_OF(sizeof(__gx), gameExPtr->apiSize)); } } void DD_InitCommandLine() { CommandLine_Alias("-game", "-g"); CommandLine_Alias("-defs", "-d"); CommandLine_Alias("-width", "-w"); CommandLine_Alias("-height", "-h"); CommandLine_Alias("-winsize", "-wh"); CommandLine_Alias("-bpp", "-b"); CommandLine_Alias("-window", "-wnd"); CommandLine_Alias("-nocenter", "-noc"); CommandLine_Alias("-file", "-f"); CommandLine_Alias("-config", "-cfg"); CommandLine_Alias("-parse", "-p"); CommandLine_Alias("-cparse", "-cp"); CommandLine_Alias("-command", "-cmd"); CommandLine_Alias("-fontdir", "-fd"); CommandLine_Alias("-modeldir", "-md"); CommandLine_Alias("-basedir", "-bd"); CommandLine_Alias("-stdbasedir", "-sbd"); CommandLine_Alias("-userdir", "-ud"); CommandLine_Alias("-texdir", "-td"); CommandLine_Alias("-texdir2", "-td2"); CommandLine_Alias("-anifilter", "-ani"); CommandLine_Alias("-verbose", "-v"); } static void App_AddKnownWords() { // Add games as known words. foreach(Game *game, App_Games().all()) { Con_AddKnownWord(WT_GAME, game); } } void DD_ConsoleInit() { // Get the console online ASAP. Con_Init(); Con_SetApplicationKnownWordCallback(App_AddKnownWords); LOG_NOTE("Executable: " DOOMSDAY_NICENAME " " DOOMSDAY_VERSION_FULLTEXT); // Print the used command line. LOG_MSG("Command line options:"); for(int p = 0; p < CommandLine_Count(); ++p) { LOG_MSG(" %i: " _E(>) "%s") << p << CommandLine_At(p); } } void DD_ShutdownAll() { App_InFineSystem().reset(); #ifdef __CLIENT__ App_InFineSystem().deinitBindingContext(); #endif Con_Shutdown(); DD_ShutdownHelp(); #ifdef WIN32 // Enables Alt-Tab, Alt-Esc, Ctrl-Alt-Del. SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, FALSE, 0, 0); #endif #ifdef __CLIENT__ // Stop all demo recording. for(int i = 0; i < DDMAXPLAYERS; ++i) { Demo_StopRecording(i); } #endif P_ClearPlayerImpulses(); #ifdef __SERVER__ Sv_Shutdown(); #endif R_ShutdownSvgs(); #ifdef __CLIENT__ R_ShutdownViewWindow(); if(ClientApp::hasRenderSystem()) { ClientApp::renderSystem().clearDrawLists(); } #endif Def_Destroy(); F_Shutdown(); Libdeng_Shutdown(); } doomsday-stable-1.15.7/doomsday/client/src/color.cpp0000664000175000017500000000223712641367670021706 0ustar jaakkojaakko/** @file color.cpp Color * * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "color.h" float ColorRawf_AverageColor(ColorRawf* c) { DENG_ASSERT(c); return (c->red + c->green + c->blue) / 3; } float ColorRawf_AverageColorMulAlpha(ColorRawf* c) { DENG_ASSERT(c); return (c->red + c->green + c->blue) / 3 * c->alpha; } doomsday-stable-1.15.7/doomsday/client/src/template.c.template0000664000175000017500000000174212641367670023655 0ustar jaakkojaakko/** @file template.c.template Brief description of the source file. * @ingroup group * * @todo Update the fields above as appropriate. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ doomsday-stable-1.15.7/doomsday/client/src/dd_loop.cpp0000664000175000017500000003031412641367670022205 0ustar jaakkojaakko/** @file dd_loop.cpp Main loop and the core timer. * @ingroup base * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "de_base.h" #include "de_console.h" #include "de_system.h" #include "de_network.h" #include "de_render.h" #include "de_play.h" #include "de_graphics.h" #include "de_audio.h" #include "de_ui.h" #include "de_misc.h" #include #ifdef __SERVER__ # include #endif #ifdef __CLIENT__ # include "clientapp.h" # include "ui/busyvisual.h" # include "ui/clientwindow.h" #endif /// Development utility: on sharp tics, print player 0 movement state. //#define LIBDENG_PLAYER0_MOVEMENT_ANALYSIS /** * There needs to be at least this many tics per second. A smaller value * is likely to cause unpredictable changes in playsim. */ #define MIN_TIC_RATE 35 /** * The length of one tic can be at most this. */ #define MAX_FRAME_TIME (1.0/MIN_TIC_RATE) /** * If the loop is stuck for more than this number of seconds, the elapsed time is * ignored. The assumption is that the app was suspended or was not able to run, * so no point in running tics. */ #define MAX_ELAPSED_TIME 5 float frameTimePos; // 0...1: fractional part for sharp game tics. int maxFrameRate = 120; // Zero means 'unlimited'. // Refresh frame count (independant of the viewport-specific frameCount). int rFrameCount = 0; byte devShowFrameTimeDeltas = false; byte processSharpEventsAfterTickers = true; timespan_t sysTime, gameTime, demoTime; //timespan_t frameStartTime; //dd_bool stopTime = false; // If true the time counters won't be incremented //dd_bool tickUI = false; // If true the UI will be tick'd dd_bool tickFrame = true; // If false frame tickers won't be tick'd (unless netGame) static int gameLoopExitCode = 0; static double lastRunTicsTime; static dd_bool firstTic = true; static dd_bool tickIsSharp = false; #define NUM_FRAMETIME_DELTAS 200 static int timeDeltas[NUM_FRAMETIME_DELTAS]; static int timeDeltasIndex = 0; static float realFrameTimePos = 0; void DD_RegisterLoop(void) { C_VAR_BYTE("input-sharp-lateprocessing", &processSharpEventsAfterTickers, 0, 0, 1); C_VAR_INT ("refresh-rate-maximum", &maxFrameRate, 0, 35, 1000); C_VAR_INT ("rend-dev-framecount", &rFrameCount, CVF_NO_ARCHIVE | CVF_PROTECTED, 0, 0); C_VAR_BYTE("rend-info-deltas-frametime", &devShowFrameTimeDeltas, CVF_NO_ARCHIVE, 0, 1); } void DD_SetGameLoopExitCode(int code) { gameLoopExitCode = code; } int DD_GameLoopExitCode(void) { return gameLoopExitCode; } float DD_GetFrameRate() { #ifdef __CLIENT__ return ClientWindow::main().frameRate(); #else return 0; #endif } #undef DD_IsSharpTick DENG_EXTERN_C dd_bool DD_IsSharpTick(void) { return tickIsSharp; } dd_bool DD_IsFrameTimeAdvancing(void) { if(BusyMode_Active()) return false; return tickFrame || netGame; } void DD_CheckSharpTick(timespan_t time) { // Sharp ticks are the ones that occur 35 per second. The rest are // interpolated (smoothed) somewhere in between. tickIsSharp = false; if(DD_IsFrameTimeAdvancing()) { /** * realFrameTimePos will be reduced when new sharp world positions are * calculated, so that frametime always stays within the range 0..1. */ realFrameTimePos += time * TICSPERSEC; // When one full tick has passed, it is time to do a sharp tick. if(realFrameTimePos >= 1) { tickIsSharp = true; } } } /** * This is the main ticker of the engine. We'll call all the other tickers * from here. * * @param time Duration of the tick. This will never be longer than 1.0/TICSPERSEC. */ static void baseTicker(timespan_t time) { if(DD_IsFrameTimeAdvancing()) { #ifdef __CLIENT__ // Demo ticker. Does stuff like smoothing of view angles. Demo_Ticker(time); #endif P_Ticker(time); #ifdef __CLIENT__ FR_Ticker(time); #endif // InFine ticks whenever it's active. App_InFineSystem().runTicks(time); // Game logic. if(App_GameLoaded() && gx.Ticker) { gx.Ticker(time); } #ifdef __CLIENT__ // Windowing system ticks. for(int i = 0; i < DDMAXPLAYERS; ++i) { R_ViewWindowTicker(i, time); } if(isClient) { Cl_Ticker(time); } #elif __SERVER__ Sv_Ticker(time); #endif if(DD_IsSharpTick()) { // Set frametime back by one tick (to stay in the 0..1 range). realFrameTimePos -= 1; #ifdef __CLIENT__ // Camera smoothing: now that the world tic has occurred, the next sharp // position can be processed. R_NewSharpWorld(); #endif #ifdef LIBDENG_PLAYER0_MOVEMENT_ANALYSIS if(ddPlayers[0].shared.inGame && ddPlayers[0].shared.mo) { mobj_t* mo = ddPlayers[0].shared.mo; static coord_t prevPos[3] = { 0, 0, 0 }; static coord_t prevSpeed = 0; coord_t speed = V2d_Length(mo->mom); coord_t actualMom[2] = { mo->origin[0] - prevPos[0], mo->origin[1] - prevPos[1] }; coord_t actualSpeed = V2d_Length(actualMom); LOG_NOTE("%i,%f,%f,%f,%f") << SECONDS_TO_TICKS(sysTime + time) << ddPlayers[0].shared.forwardMove << speed << actualSpeed << speed - prevSpeed; V3d_Copy(prevPos, mo->origin); prevSpeed = speed; } #endif } #ifdef __CLIENT__ // While paused, don't modify frametime so things keep still. if(!clientPaused) #endif { frameTimePos = realFrameTimePos; } } // Console is always ticking. Con_Ticker(time); if(tickFrame) { Con_TransitionTicker(time); } // Plugins tick always. DD_CallHooks(HOOK_TICKER, 0, &time); // The netcode gets to tick, too. Net_Ticker(time); } /** * Advance time counters. */ static void advanceTime(timespan_t delta) { int oldGameTic = 0; sysTime += delta; oldGameTic = SECONDS_TO_TICKS(gameTime); // The difference between gametic and demotic is that demotic // is not altered at any point. Gametic changes at handshakes. gameTime += delta; demoTime += delta; if(DD_IsSharpTick()) { // When a new sharp tick begins, we want that the 35 Hz tick // calculated from gameTime also changes. If this is not the // case, we will adjust gameTime slightly so that it syncs again. if(oldGameTic == SECONDS_TO_TICKS(gameTime)) { LOGDEV_XVERBOSE("Syncing gameTime with sharp ticks (tic=%i pos=%f)") << oldGameTic << frameTimePos; // Realign. gameTime = (SECONDS_TO_TICKS(gameTime) + 1) / 35.f; } } // World time always advances unless a local game is paused on client-side. App_WorldSystem().advanceTime(delta); } void DD_ResetTimer(void) { firstTic = true; Net_ResetTimer(); } static void timeDeltaStatistics(int deltaMs) { timeDeltas[timeDeltasIndex++] = deltaMs; if(timeDeltasIndex == NUM_FRAMETIME_DELTAS) { timeDeltasIndex = 0; if(devShowFrameTimeDeltas) { int maxDelta = timeDeltas[0], minDelta = timeDeltas[0]; float average = 0, variance = 0; int lateCount = 0; int i; for(i = 0; i < NUM_FRAMETIME_DELTAS; ++i) { maxDelta = MAX_OF(timeDeltas[i], maxDelta); minDelta = MIN_OF(timeDeltas[i], minDelta); average += timeDeltas[i]; variance += timeDeltas[i] * timeDeltas[i]; if(timeDeltas[i] > 0) lateCount++; } average /= NUM_FRAMETIME_DELTAS; variance /= NUM_FRAMETIME_DELTAS; LOGDEV_MSG("Time deltas [%i frames]: min=%-6i max=%-6i avg=%-11.7f late=%5.1f%% var=%12.10f") << NUM_FRAMETIME_DELTAS << minDelta << maxDelta << average << lateCount/(float)NUM_FRAMETIME_DELTAS*100 << variance; } } } void DD_WaitForOptimalUpdateTime(void) { // All times are in milliseconds. static uint prevUpdateTime = 0; uint nowTime, elapsed = 0; uint targetUpdateTime; // optimalDelta is integer on purpose: we're measuring time at a 1 ms // accuracy, so we can't use fractions of a millisecond. const uint optimalDelta = (maxFrameRate > 0? 1000/maxFrameRate : 1); if(Sys_IsShuttingDown()) return; // No need for finesse. // This is when we would ideally like to make the update. targetUpdateTime = prevUpdateTime + optimalDelta; // Check the current time. nowTime = Timer_RealMilliseconds(); elapsed = nowTime - prevUpdateTime; if(elapsed < optimalDelta) { uint needSleepMs = optimalDelta - elapsed; // We need to wait until the optimal time has passed. if(needSleepMs > 5) { // Longer sleep, yield to other threads. Sys_Sleep(needSleepMs - 3); // Leave some room for inaccuracies. } // Attempt to make sure we really wait until the optimal time. Sys_BlockUntilRealTime(targetUpdateTime); nowTime = Timer_RealMilliseconds(); elapsed = nowTime - prevUpdateTime; } // The time for this update. prevUpdateTime = nowTime; timeDeltaStatistics((int)elapsed - (int)optimalDelta); } timespan_t DD_LatestRunTicsStartTime(void) { if(BusyMode_Active()) return Timer_Seconds(); return lastRunTicsTime; } static double ticLength; timespan_t DD_CurrentTickDuration() { return ticLength; } void Loop_RunTics(void) { double elapsedTime, nowTime; // Do a network update first. N_Update(); Net_Update(); // Check the clock. if(firstTic) { // On the first tic, no time actually passes. firstTic = false; lastRunTicsTime = Timer_Seconds(); return; } // Let's see how much time has passed. This is affected by "settics". nowTime = Timer_Seconds(); elapsedTime = nowTime - lastRunTicsTime; if(elapsedTime > MAX_ELAPSED_TIME) { // It was too long ago, no point in running individual ticks. Just do one. elapsedTime = MAX_FRAME_TIME; } // Remember when this frame started. lastRunTicsTime = nowTime; // Tic until all the elapsed time has been processed. while(elapsedTime > 0) { ticLength = MIN_OF(MAX_FRAME_TIME, elapsedTime); elapsedTime -= ticLength; // Will this be a sharp tick? DD_CheckSharpTick(ticLength); #ifdef __CLIENT__ // Process input events. ClientApp::inputSystem().processEvents(ticLength); if(!processSharpEventsAfterTickers) { // We are allowed to process sharp events before tickers. ClientApp::inputSystem().processSharpEvents(ticLength); } #endif // Call all the tickers. baseTicker(ticLength); #ifdef __CLIENT__ if(processSharpEventsAfterTickers) { // This is done after tickers for compatibility with ye olde game logic. ClientApp::inputSystem().processSharpEvents(ticLength); } #endif // Various global variables are used for counting time. advanceTime(ticLength); } } doomsday-stable-1.15.7/doomsday/client/src/main_client.cpp0000664000175000017500000000651112641367670023051 0ustar jaakkojaakko/** @file main_client.cpp Client application entrypoint. * @ingroup base * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ /** * @page mainFlow Engine Control Flow * * The main Qt application instance is ClientApp, based on de::GuiApp, a slightly * modified version of the normal QApplication: it catches stray exceptions and forces a * clean shutdown of the application. * * The application's event loop is started as soon as the main window has been created * (but not shown yet). After the window appears with a fully functional OpenGL drawing * surface, the rest of the engine initialization is completed. This is done via a * callback in the Canvas class that gets called when the window actually appears on * screen (with empty contents). * * The application's refresh loop is controlled by de::Loop. Before each frame, clock * time advances and de::Loop's iteration audience is notified. This is observed by * de::WindowSystem, which updates all widgets. When the GameWidget is updated, it runs * game tics and requests a redraw of the window contents. * * During startup the engine goes through a series of busy mode tasks. While a busy task * is running, the application's primary event loop is blocked. However, BusyTask starts * another loop that continues handling events received by the application. */ #include "clientapp.h" #include "dd_loop.h" #include #include #include /** * Application entry point. */ int main(int argc, char** argv) { int exitCode = 0; { ClientApp clientApp(argc, argv); /** * @todo Translations are presently disabled because lupdate can't seem to * parse tr strings from inside private implementation classes. Workaround * or fix is needed? */ #if 0 // Load the current locale's translation. QTranslator translator; translator.load(QString("client_") + QLocale::system().name()); clientApp.installTranslator(&translator); #endif try { clientApp.initialize(); exitCode = clientApp.execLoop(); } catch(de::Error const &er) { qWarning() << "App init failed:\n" << er.asText(); QMessageBox::critical(0, DOOMSDAY_NICENAME, "App init failed:\n" + er.asText()); return -1; } } // Check that all reference-counted objects have been deleted. DENG2_ASSERT(de::Counted::totalCount == 0); return exitCode; } doomsday-stable-1.15.7/doomsday/client/src/network/0000775000175000017500000000000012641367670021551 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/network/monitor.cpp0000664000175000017500000000564212641367670023753 0ustar jaakkojaakko/** @file monitor.cpp Implementation of network traffic monitoring. * @ingroup network * * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "de_console.h" #if _DEBUG static uint monitor[256]; static uint monitoredBytes; static uint monitoredPackets; static size_t monitorMaxSize; static void Monitor_Start(int maxPacketSize) { monitorMaxSize = maxPacketSize; monitoredBytes = monitoredPackets = 0; memset(&monitor, 0, sizeof(monitor)); } static void Monitor_Stop(void) { monitorMaxSize = 0; } void Monitor_Add(const uint8_t* bytes, size_t size) { if(size <= monitorMaxSize) { uint i; monitoredPackets++; monitoredBytes += size; for(i = 0; i < size; ++i) monitor[bytes[i]]++; } } static void Monitor_Print(void) { if(!monitoredBytes) { LOGDEV_NET_MSG("Nothing has been sent yet"); return; } LOGDEV_NET_MSG("%i bytes sent (%i packets)") << monitoredBytes << monitoredPackets; int i, k; for(i = 0, k = 0; i < 256; ++i) { // LogBuffer_Printf uses manual newlines. if(!k) LogBuffer_Printf(DE2_LOG_DEV, " "); LogBuffer_Printf(DE2_LOG_DEV, "%10.10f", (double)(monitor[i]) / (double)monitoredBytes); // Break lines. if(++k == 4) { k = 0; LogBuffer_Printf(DE2_LOG_DEV, ",\n"); } else { LogBuffer_Printf(DE2_LOG_DEV, ", "); } } if(k) LogBuffer_Printf(DE2_LOG_DEV, "\n"); } D_CMD(NetFreqs) { DENG2_UNUSED(src); if(argc == 1) // No args? { LOG_SCR_NOTE("Usage:\n %s start (maxsize)\n %s stop\n %s print/show") << argv[0] << argv[0] << argv[0]; return true; } if(argc == 3 && !strcmp(argv[1], "start")) { Monitor_Start(strtoul(argv[2], 0, 10)); return true; } if(argc == 2 && !strcmp(argv[1], "stop")) { Monitor_Stop(); return true; } if(argc == 2 && (!strcmp(argv[1], "print") || !strcmp(argv[1], "show"))) { Monitor_Print(); return true; } return false; } #endif // _DEBUG doomsday-stable-1.15.7/doomsday/client/src/network/net_demo.cpp0000664000175000017500000004533212641367670024056 0ustar jaakkojaakko/** @file net_demo.cpp * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * Handling of demo recording and playback. * Opening of, writing to, reading from and closing of demo files. */ // HEADER FILES ------------------------------------------------------------ #include "de_base.h" #include "de_console.h" #include "de_system.h" #include "de_filesys.h" #include "de_network.h" #include "de_misc.h" #include "render/viewports.h" #include "render/rend_main.h" #include "world/p_players.h" // MACROS ------------------------------------------------------------------ #define DEMOTIC SECONDS_TO_TICKS(demoTime) // Local Camera flags. #define LCAMF_ONGROUND 0x1 #define LCAMF_FOV 0x2 // FOV has changed (short). #define LCAMF_CAMERA 0x4 // Camera mode. // TYPES ------------------------------------------------------------------- #pragma pack(1) typedef struct { ushort length; } demopacket_header_t; #pragma pack() typedef struct { dd_bool first; int begintime; dd_bool canwrite; /// @c false until Handshake packet. int cameratimer; int pausetime; float fov; } demotimer_t; // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- // PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- D_CMD(DemoLump); D_CMD(PauseDemo); D_CMD(PlayDemo); D_CMD(RecordDemo); D_CMD(StopDemo); void Demo_WriteLocalCamera(int plnum); // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- // EXTERNAL DATA DECLARATIONS ---------------------------------------------- extern float netConnectTime; // PUBLIC DATA DEFINITIONS ------------------------------------------------- filename_t demoPath = "demo/"; LZFILE* playdemo = 0; int playback = false; int viewangleDelta = 0; float lookdirDelta = 0; float posDelta[3]; float demoFrameZ, demoZ; dd_bool demoOnGround; // PRIVATE DATA DEFINITIONS ------------------------------------------------ static demotimer_t writeInfo[DDMAXPLAYERS]; static demotimer_t readInfo; static float startFOV; static int demoStartTic; // CODE -------------------------------------------------------------------- void Demo_Register(void) { C_CMD_FLAGS("demolump", "ss", DemoLump, CMDF_NO_NULLGAME); C_CMD_FLAGS("pausedemo", NULL, PauseDemo, CMDF_NO_NULLGAME); C_CMD_FLAGS("playdemo", "s", PlayDemo, CMDF_NO_NULLGAME); C_CMD_FLAGS("recorddemo", NULL, RecordDemo, CMDF_NO_NULLGAME); C_CMD_FLAGS("stopdemo", NULL, StopDemo, CMDF_NO_NULLGAME); } void Demo_Init(void) { // Make sure the demo path is there. F_MakePath(demoPath); } /** * Open a demo file and begin recording. * Returns false if the recording can't be begun. */ dd_bool Demo_BeginRecording(const char* fileName, int plrNum) { DENG_UNUSED(fileName); DENG_UNUSED(plrNum); return false; #if 0 client_t* cl = &clients[plrNum]; player_t* plr = &ddPlayers[plrNum]; ddstring_t buf; // Is a demo already being recorded for this client? if(cl->recording || playback || (isDedicated && !plrNum) || !plr->shared.inGame) return false; // Compose the real file name. Str_InitStd(&buf); Str_Appendf(&buf, "%s%s", demoPath, fileName); F_ExpandBasePath(&buf, &buf); F_ToNativeSlashes(&buf, &buf); // Open the demo file. cl->demo = lzOpen(Str_Text(&buf), "wp"); Str_Free(&buf); if(!cl->demo) { return false; // Couldn't open it! } cl->recording = true; cl->recordPaused = false; writeInfo[plrNum].first = true; writeInfo[plrNum].canwrite = false; writeInfo[plrNum].cameratimer = 0; writeInfo[plrNum].fov = -1; // Must be written in the first packet. if(isServer) { // Playing demos alters gametic. This'll make sure we're going to // get updates. clients[0].lastTransmit = -1; // Servers need to send a handshake packet. // It only needs to recorded in the demo file, though. allowSending = false; Sv_Handshake(plrNum, false); // Enable sending to network. allowSending = true; } else { // Clients need a Handshake packet. // Request a new one from the server. Cl_SendHello(); } // The operation is a success. return true; #endif } void Demo_PauseRecording(int playerNum) { client_t *cl = clients + playerNum; // A demo is not being recorded? if(!cl->recording || cl->recordPaused) return; // All packets will be written for the same tic. writeInfo[playerNum].pausetime = SECONDS_TO_TICKS(demoTime); cl->recordPaused = true; } /** * Resumes a paused recording. */ void Demo_ResumeRecording(int playerNum) { client_t *cl = clients + playerNum; // Not recording or not paused? if(!cl->recording || !cl->recordPaused) return; Demo_WriteLocalCamera(playerNum); cl->recordPaused = false; // When the demo is read there can't be a jump in the timings, so we // have to make it appear the pause never happened; begintime is // moved forwards. writeInfo[playerNum].begintime += DEMOTIC - writeInfo[playerNum].pausetime; } /** * Stop recording a demo. */ void Demo_StopRecording(int playerNum) { client_t *cl = clients + playerNum; // A demo is not being recorded? if(!cl->recording) return; // Close demo file. lzClose(cl->demo); cl->demo = 0; cl->recording = false; } void Demo_WritePacket(int playerNum) { LZFILE *file; demopacket_header_t hdr; demotimer_t *inf = writeInfo + playerNum; byte ptime; if(playerNum < 0) { Demo_BroadcastPacket(); return; } // Is this client recording? if(!clients[playerNum].recording) return; if(!inf->canwrite) { if(netBuffer.msg.type != PSV_HANDSHAKE) return; // The handshake has arrived. Now we can begin writing. inf->canwrite = true; } if(clients[playerNum].recordPaused) { // Some types of packet are not written in record-paused mode. if(netBuffer.msg.type == PSV_SOUND || netBuffer.msg.type == DDPT_MESSAGE) return; } // This counts as an update. (We know the client is alive.) //clients[playerNum].updateCount = UPDATECOUNT; file = clients[playerNum].demo; #if _DEBUG if(!file) App_Error("Demo_WritePacket: No demo file!\n"); #endif if(!inf->first) { ptime = (clients[playerNum].recordPaused ? inf->pausetime : DEMOTIC) - inf->begintime; } else { ptime = 0; inf->first = false; inf->begintime = DEMOTIC; } lzWrite(&ptime, 1, file); // The header. #if _DEBUG if(netBuffer.length >= sizeof(hdr.length)) App_Error("Demo_WritePacket: Write buffer too large!\n"); #endif hdr.length = (ushort) 1 + netBuffer.length; lzWrite(&hdr, sizeof(hdr), file); // Write the packet itself. lzPutC(netBuffer.msg.type, file); lzWrite(netBuffer.msg.data, (long) netBuffer.length, file); } void Demo_BroadcastPacket(void) { int i; // Write packet to all recording demo files. for(i = 0; i < DDMAXPLAYERS; ++i) Demo_WritePacket(i); } dd_bool Demo_BeginPlayback(const char* fileName) { ddstring_t buf; if(playback) return false; // Already in playback. if(netGame || isClient) return false; // Can't do it. // Check that we aren't recording anything. { int i; for(i = 0; i < DDMAXPLAYERS; ++i) { if(clients[i].recording) return false; }} // Compose the real file name. Str_InitStd(&buf); Str_Set(&buf, fileName); if(!F_IsAbsolute(&buf)) { Str_Prepend(&buf, demoPath); } F_ExpandBasePath(&buf, &buf); F_ToNativeSlashes(&buf, &buf); // Open the demo file. playdemo = lzOpen(Str_Text(&buf), "rp"); Str_Free(&buf); if(!playdemo) return false; // OK, let's begin the demo. playback = true; isServer = false; isClient = true; readInfo.first = true; viewangleDelta = 0; lookdirDelta = 0; demoFrameZ = 1; demoZ = 0; startFOV = 95; //Rend_FieldOfView(); demoStartTic = DEMOTIC; memset(posDelta, 0, sizeof(posDelta)); // Start counting frames from here. /* if(ArgCheck("-timedemo")) r_framecounter = 0; */ return true; } void Demo_StopPlayback(void) { //float diff; if(!playback) return; LOG_MSG("Demo was %.2f seconds (%i tics) long.") << ((DEMOTIC - demoStartTic) / (float) TICSPERSEC) << (DEMOTIC - demoStartTic); playback = false; lzClose(playdemo); playdemo = 0; //fieldOfView = startFOV; Net_StopGame(); /* if(ArgCheck("-timedemo")) { diff = Sys_GetSeconds() - netConnectTime; if(!diff) diff = 1; // Print summary and exit. LOG_MSG("Timedemo results: %i game tics in %.1f seconds", r_framecounter, diff); LOG_MSG("%f FPS", r_framecounter / diff); Sys_Quit(); } */ // "Play demo once" mode? if(CommandLine_Check("-playdemo")) Sys_Quit(); } dd_bool Demo_ReadPacket(void) { static byte ptime; int nowtime = DEMOTIC; demopacket_header_t hdr; if(!playback) return false; if(lzEOF(playdemo)) { Demo_StopPlayback(); // Any interested parties? DD_CallHooks(HOOK_DEMO_STOP, false, 0); return false; } if(readInfo.first) { readInfo.first = false; readInfo.begintime = nowtime; ptime = lzGetC(playdemo); } // Check if the packet can be read. if(Net_TimeDelta(nowtime - readInfo.begintime, ptime) < 0) return false; // Can't read yet. // Read the packet. lzRead(&hdr, sizeof(hdr), playdemo); // Get the packet. netBuffer.length = hdr.length - 1; netBuffer.player = 0; // From the server. netBuffer.msg.type = lzGetC(playdemo); lzRead(netBuffer.msg.data, (long) netBuffer.length, playdemo); //netBuffer.cursor = netBuffer.msg.data; // Read the next packet time. ptime = lzGetC(playdemo); return true; } /** * Writes a view angle and coords packet. Doesn't send the packet outside. */ void Demo_WriteLocalCamera(int plrNum) { player_t* plr = &ddPlayers[plrNum]; ddplayer_t* ddpl = &plr->shared; mobj_t* mo = ddpl->mo; fixed_t x, y, z; byte flags; dd_bool incfov = false; //(writeInfo[plrNum].fov != fieldOfView); const viewdata_t* viewData = R_ViewData(plrNum); if(!mo) return; Msg_Begin(clients[plrNum].recordPaused ? PKT_DEMOCAM_RESUME : PKT_DEMOCAM); // Flags. flags = (mo->origin[VZ] <= mo->floorZ ? LCAMF_ONGROUND : 0) // On ground? | (incfov ? LCAMF_FOV : 0); if(ddpl->flags & DDPF_CAMERA) { flags &= ~LCAMF_ONGROUND; flags |= LCAMF_CAMERA; } Writer_WriteByte(msgWriter, flags); // Coordinates. x = FLT2FIX(mo->origin[VX]); y = FLT2FIX(mo->origin[VY]); Writer_WriteInt16(msgWriter, x >> 16); Writer_WriteByte(msgWriter, x >> 8); Writer_WriteInt16(msgWriter, y >> 16); Writer_WriteByte(msgWriter, y >> 8); z = FLT2FIX(mo->origin[VZ] + viewData->current.origin.z); Writer_WriteInt16(msgWriter, z >> 16); Writer_WriteByte(msgWriter, z >> 8); Writer_WriteInt16(msgWriter, mo->angle /*ddpl->clAngle*/ >> 16); /* $unifiedangles */ Writer_WriteInt16(msgWriter, ddpl->lookDir / 110 * DDMAXSHORT /* $unifiedangles */); // Field of view is optional. /*if(incfov) { Writer_WriteInt16(msgWriter, fieldOfView / 180 * DDMAXSHORT); writeInfo[plrNum].fov = fieldOfView; }*/ Msg_End(); Net_SendBuffer(plrNum, SPF_DONT_SEND); } /** * Read a view angle and coords packet. NOTE: The Z coordinate of the * camera is the real eye Z coordinate, not the player mobj's Z coord. */ void Demo_ReadLocalCamera(void) { ddplayer_t *pl = &ddPlayers[consolePlayer].shared; mobj_t *mo = pl->mo; int flags; float z; int intertics = LOCALCAM_WRITE_TICS; int dang; float dlook; if(!mo) return; if(netBuffer.msg.type == PKT_DEMOCAM_RESUME) { intertics = 1; } // Framez keeps track of the current camera Z. demoFrameZ += demoZ; flags = Reader_ReadByte(msgReader); demoOnGround = (flags & LCAMF_ONGROUND) != 0; if(flags & LCAMF_CAMERA) pl->flags |= DDPF_CAMERA; else pl->flags &= ~DDPF_CAMERA; // X and Y coordinates are easy. Calculate deltas to the new coords. posDelta[VX] = (FIX2FLT((Reader_ReadInt16(msgReader) << 16) + (Reader_ReadByte(msgReader) << 8)) - mo->origin[VX]) / intertics; posDelta[VY] = (FIX2FLT((Reader_ReadInt16(msgReader) << 16) + (Reader_ReadByte(msgReader) << 8)) - mo->origin[VY]) / intertics; // The Z coordinate is a bit trickier. We are tracking the *camera's* // Z coordinate (z+viewheight), not the player mobj's Z. z = FIX2FLT((Reader_ReadInt16(msgReader) << 16) + (Reader_ReadByte(msgReader) << 8)); posDelta[VZ] = (z - demoFrameZ) / LOCALCAM_WRITE_TICS; // View angles. dang = Reader_ReadInt16(msgReader) << 16; dlook = Reader_ReadInt16(msgReader) * 110.0f / DDMAXSHORT; // FOV included? /* if(flags & LCAMF_FOV) fieldOfView = Reader_ReadInt16(msgReader) * 180.0f / DDMAXSHORT; */ if(intertics == 1 || demoFrameZ == 1) { // Immediate change. /*pl->clAngle = dang; pl->clLookDir = dlook;*/ pl->mo->angle = dang; pl->lookDir = dlook; /* $unifiedangles */ viewangleDelta = 0; lookdirDelta = 0; } else { viewangleDelta = (dang - pl->mo->angle) / intertics; lookdirDelta = (dlook - pl->lookDir) / intertics; /* $unifiedangles */ } // The first one gets no delta. if(demoFrameZ == 1) { // This must be the first democam packet. // Initialize framez to the height we just read. demoFrameZ = z; posDelta[VZ] = 0; } // demo_z is the offset to demo_framez for the current tic. // It is incremented by pos_delta[VZ] every tic. demoZ = 0; if(intertics == 1) { // Instantaneous move. R_ResetViewer(); demoFrameZ = z; ClPlayer_MoveLocal(posDelta[VX], posDelta[VY], z, demoOnGround); posDelta[VX] = posDelta[VY] = posDelta[VZ] = 0; } } /** * Called once per tic. */ void Demo_Ticker(timespan_t /*time*/) { if(!DD_IsSharpTick()) return; // Only playback is handled. if(playback) { player_t *plr = &ddPlayers[consolePlayer]; ddplayer_t *ddpl = &plr->shared; ddpl->mo->angle += viewangleDelta; ddpl->lookDir += lookdirDelta; /* $unifiedangles */ // Move player (i.e. camera). ClPlayer_MoveLocal(posDelta[VX], posDelta[VY], demoFrameZ + demoZ, demoOnGround); // Interpolate camera Z offset (to framez). demoZ += posDelta[VZ]; } else { int i; for(i = 0; i < DDMAXPLAYERS; ++i) { player_t *plr = &ddPlayers[i]; ddplayer_t *ddpl = &plr->shared; client_t *cl = &clients[i]; if(ddpl->inGame && cl->recording && !cl->recordPaused && ++writeInfo[i].cameratimer >= LOCALCAM_WRITE_TICS) { // It's time to write local view angles and coords. writeInfo[i].cameratimer = 0; Demo_WriteLocalCamera(i); } } } } D_CMD(PlayDemo) { DENG2_UNUSED2(src, argc); LOG_MSG("Playing demo \"%s\"...") << argv[1]; return Demo_BeginPlayback(argv[1]); } D_CMD(RecordDemo) { DENG2_UNUSED(src); int plnum = consolePlayer; if(argc == 3 && isClient) { LOG_ERROR("Clients can only record the consolePlayer"); return true; } if(isClient && argc != 2) { LOG_SCR_NOTE("Usage: %s (fileName)") << argv[0]; return true; } if(isServer && (argc < 2 || argc > 3)) { LOG_SCR_NOTE("Usage: %s (fileName) (plnum)") << argv[0]; LOG_SCR_MSG("(plnum) is the player which will be recorded."); return true; } if(argc == 3) plnum = atoi(argv[2]); LOG_MSG("Recording demo of player %i to \"%s\"") << plnum << argv[1]; return Demo_BeginRecording(argv[1], plnum); } D_CMD(PauseDemo) { DENG2_UNUSED(src); int plnum = consolePlayer; if(argc >= 2) plnum = atoi(argv[1]); if(!clients[plnum].recording) { LOG_ERROR("Not recording for player %i") << plnum; return false; } if(clients[plnum].recordPaused) { Demo_ResumeRecording(plnum); LOG_MSG("Demo recording of player %i resumed") << plnum; } else { Demo_PauseRecording(plnum); LOG_MSG("Demo recording of player %i paused") << plnum; } return true; } D_CMD(StopDemo) { DENG2_UNUSED(src); int plnum = consolePlayer; if(argc > 2) { LOG_SCR_NOTE("Usage: stopdemo (plrnum)"); return true; } if(argc == 2) plnum = atoi(argv[1]); if(!playback && !clients[plnum].recording) return true; LOG_MSG("Demo %s of player %i stopped.") << (clients[plnum].recording ? "recording" : "playback") << plnum; if(playback) { // Aborted. Demo_StopPlayback(); // Any interested parties? DD_CallHooks(HOOK_DEMO_STOP, true, 0); } else Demo_StopRecording(plnum); return true; } /** * Make a demo lump. */ D_CMD(DemoLump) { DENG2_UNUSED2(src, argc); char buf[64]; memset(buf, 0, sizeof(buf)); strncpy(buf, argv[1], 64); return M_WriteFile(argv[2], buf, 64); } doomsday-stable-1.15.7/doomsday/client/src/network/masterserver.cpp0000664000175000017500000001757312641367670025014 0ustar jaakkojaakko/** @file masterserver.cpp Communication with the Master Server. * @ingroup network * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include "de_platform.h" #include #include #include "network/masterserver.h" #include "network/net_main.h" #include "network/protocol.h" #ifdef __SERVER__ # include "server/sv_def.h" #endif #include "dd_main.h" #include "m_misc.h" #include using namespace de; // Maximum time allowed time for a master server operation to take (seconds). #define RESPONSE_TIMEOUT 15 typedef struct job_s { MasterWorker::Action act; void* data; } job_t; static String const DEFAULT_API_URL = "www.dengine.net/master.php"; dd_bool masterAware = false; // cvar static QString masterUrl(const char* suffix = 0) { String u = App::config().gets("masterServer.apiUrl", DEFAULT_API_URL); if(!u.startsWith("http")) u = "http://" + u; if(suffix) u += suffix; return u; } DENG2_PIMPL_NOREF(MasterWorker) { QNetworkAccessManager* network; typedef std::list Jobs; Jobs jobs; MasterWorker::Action currentAction; typedef std::vector Servers; Servers servers; Instance() : network(0), currentAction(NONE) {} ~Instance() { delete network; } }; MasterWorker::MasterWorker() : d(new Instance) { d->network = new QNetworkAccessManager(this); connect(d->network, SIGNAL(finished(QNetworkReply*)), this, SLOT(requestFinished(QNetworkReply*))); } void MasterWorker::newJob(Action action, void* data) { LOG_AS("MasterWorker"); job_t job; job.act = action; job.data = data; d->jobs.push_back(job); // Let's get to it! nextJob(); } bool MasterWorker::isAllDone() const { return d->jobs.empty() && !isOngoing(); } bool MasterWorker::isOngoing() const { return d->currentAction != NONE; } int MasterWorker::serverCount() const { return (int) d->servers.size(); } serverinfo_t MasterWorker::server(int index) const { assert(index >= 0 && index < serverCount()); return d->servers[index]; } void MasterWorker::nextJob() { if(isOngoing() || isAllDone()) return; // Not a good time, or nothing to do. // Get the next job from the queue. job_t job = d->jobs.front(); d->jobs.pop_front(); d->currentAction = job.act; // Let's form an HTTP request. QNetworkRequest req(masterUrl(d->currentAction == REQUEST_SERVERS? "?list" : 0)); req.setRawHeader("User-Agent", Net_UserAgent().toLatin1()); #ifdef __SERVER__ if(d->currentAction == ANNOUNCE) { req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-deng-announce"); // Include the server info. ddstring_t* msg = Str_NewStd(); Sv_InfoToString((serverinfo_t*)job.data, msg); LOGDEV_NET_VERBOSE("POST request ") << req.url().toString(); foreach(const QByteArray& hdr, req.rawHeaderList()) { LOGDEV_NET_VERBOSE("%s: %s") << QString(hdr) << QString(req.rawHeader(hdr)); } LOGDEV_NET_VERBOSE("Request contents:\n%s") << Str_Text(msg); d->network->post(req, QString(Str_Text(msg)).toUtf8()); Str_Delete(msg); } else #endif { LOGDEV_NET_VERBOSE("GET request ") << req.url().toString(); foreach(const QByteArray& hdr, req.rawHeaderList()) { LOGDEV_NET_VERBOSE("%s: %s") << QString(hdr) << QString(req.rawHeader(hdr)); } d->network->get(req); } // Ownership of the data was given to us, so get rid of it now. if(job.data) M_Free(job.data); } void MasterWorker::requestFinished(QNetworkReply* reply) { LOG_AS("MasterWorker"); // Make sure the reply gets deleted afterwards. reply->deleteLater(); if(reply->error() == QNetworkReply::NoError) { LOG_NET_XVERBOSE("Got reply"); if(d->currentAction == REQUEST_SERVERS) { parseResponse(reply->readAll()); } } else { LOG_NET_WARNING(reply->errorString()); } // Continue with the next job. d->currentAction = NONE; nextJob(); } /** * Attempts to parse a list of servers from the given text string. * * @param response The string to be parsed. * * @return @c true, if successful. */ bool MasterWorker::parseResponse(const QByteArray& response) { ddstring_t msg; ddstring_t line; serverinfo_t* info = NULL; Str_InitStd(&msg); Str_PartAppend(&msg, response.constData(), 0, response.size()); Str_InitStd(&line); d->servers.clear(); // The syntax of the response is simple: // label:value // One or more empty lines separate servers. const char* pos = Str_Text(&msg); while(*pos) { pos = Str_GetLine(&line, pos); if(Str_Length(&line) && !info) { // A new server begins. d->servers.push_back(serverinfo_t()); info = &d->servers.back(); memset(info, 0, sizeof(*info)); } else if(!Str_Length(&line) && info) { // No more current server. info = NULL; } if(info) { ServerInfo_FromString(info, Str_Text(&line)); } } LOG_NET_MSG("Received %i servers from master") << serverCount(); Str_Free(&line); Str_Free(&msg); return true; } static MasterWorker* worker; void N_MasterInit(void) { assert(worker == 0); worker = new MasterWorker; } void N_MasterShutdown(void) { if(!worker) return; delete worker; worker = 0; } void N_MasterAnnounceServer(dd_bool isOpen) { #ifdef __SERVER__ // Must be a server. if(isClient) return; LOG_AS("N_MasterAnnounceServer"); if(isOpen && !strlen(netPassword)) { LOG_NET_WARNING("Cannot announce server as public: no shell password set! " "You must set one with the 'server-password' cvar."); return; } LOG_NET_MSG("Announcing server (open:%b)") << isOpen; // This will be freed by the worker after the request has been made. serverinfo_t *info = (serverinfo_t*) M_Calloc(sizeof(*info)); // Let's figure out what we want to tell about ourselves. Sv_GetInfo(info); if(!isOpen) { info->canJoin = false; } assert(worker); worker->newJob(MasterWorker::ANNOUNCE, info); #else DENG_UNUSED(isOpen); #endif } void N_MasterRequestList(void) { assert(worker); worker->newJob(MasterWorker::REQUEST_SERVERS, 0); } int N_MasterGet(int index, serverinfo_t *info) { assert(worker); if(!worker->isAllDone()) { // Not done yet. return -1; } if(!info) { return worker->serverCount(); } else { if(index >= 0 && index < worker->serverCount()) { *info = worker->server(index); return true; } else { memset(info, 0, sizeof(*info)); return false; } } } doomsday-stable-1.15.7/doomsday/client/src/network/sys_network.cpp0000664000175000017500000000437612641367670024656 0ustar jaakkojaakko/** @file sys_network.cpp Low-level network socket routines (deprecated). * @ingroup network * * @todo Remove this source file entirely once dependent code is revised. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_console.h" #include "clientapp.h" #include "network/net_buf.h" using namespace de; char *nptIPAddress = (char *) ""; ///< Address to connect to by default (cvar). int nptIPPort = 0; ///< Port to connect to by default (cvar). #ifdef _DEBUG D_CMD(NetFreqs); #endif void N_Register(void) { C_VAR_CHARPTR("net-ip-address", &nptIPAddress, 0, 0, 0); C_VAR_INT("net-ip-port", &nptIPPort, CVF_NO_MAX, 0, 0); #ifdef _DEBUG C_CMD("netfreq", NULL, NetFreqs); #endif } ServerLink &Net_ServerLink(void) { return ClientApp::app().serverLink(); } dd_bool N_GetHostInfo(int index, struct serverinfo_s *info) { return Net_ServerLink().foundServerInfo(index, info); } int N_GetHostCount(void) { return Net_ServerLink().foundServerCount(); } /** * Called from "net info" (client-side). */ void N_PrintNetworkStatus(void) { if(isClient) { LOG_NET_NOTE(_E(b) "CLIENT: " _E(.) "Connected to server at %s") << Net_ServerLink().address(); } else { LOG_NET_NOTE(_E(b) "OFFLINE: " _E(.) "Single-player mode"); } N_PrintBufferInfo(); } doomsday-stable-1.15.7/doomsday/client/src/network/net_main.cpp0000664000175000017500000011163612641367670024057 0ustar jaakkojaakko/** @file net_main.cpp Client/server networking. * * Player number zero is always the server. In single-player games there is only * the server present. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include // for atoi() #include "de_platform.h" #include "de_console.h" #include "de_system.h" #include "de_network.h" #include "de_graphics.h" #include "de_misc.h" #include "de_ui.h" #ifdef _DEBUG # include "ui/zonedebug.h" #endif #ifdef __CLIENT__ # include "api_fontrender.h" //# include "render/rend_console.h" # include "render/rend_main.h" # include "render/lightgrid.h" # include "render/blockmapvisual.h" # include "edit_bias.h" # include "ui/inputdebug.h" # include "ui/widgets/taskbarwidget.h" #endif #ifdef __SERVER__ # include "serversystem.h" #endif #include "dd_def.h" #include "dd_main.h" #include "dd_loop.h" #include "world/p_players.h" #include #include #include // MACROS ------------------------------------------------------------------ #define OBSOLETE CVF_NO_ARCHIVE|CVF_HIDE // Old ccmds. // The threshold is the average ack time * mul. #define ACK_THRESHOLD_MUL 1.5f // Never wait a too short time for acks. #define ACK_MINIMUM_THRESHOLD 50 // TYPES ------------------------------------------------------------------- // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- #ifdef __CLIENT__ D_CMD(Login); // in cl_main.c #endif #ifdef __SERVER__ D_CMD(Logout); // in sv_main.c #endif D_CMD(Ping); // in net_ping.c int Sv_GetRegisteredMobj(struct pool_s *, thid_t, struct mobjdelta_s *); // PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- D_CMD(Chat); D_CMD(MakeCamera); D_CMD(Net); D_CMD(SetTicks); #ifdef __CLIENT__ D_CMD(Connect); D_CMD(SetConsole); D_CMD(SetName); #endif #ifdef __SERVER__ D_CMD(Kick); #endif // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- // EXTERNAL DATA DECLARATIONS ---------------------------------------------- // PUBLIC DATA DEFINITIONS ------------------------------------------------- char *serverName = (char *) "Doomsday"; char *serverInfo = (char *) "Multiplayer Host"; char *playerName = (char *) "Player"; int serverData[3]; // Some parameters passed to master server. client_t clients[DDMAXPLAYERS]; // All network data for the players. int netGame; // true if a netGame is in progress int isServer; // true if this computer is an open server. int isClient; // true if this computer is a client // Gotframe is true if a frame packet has been received. int gotFrame = false; dd_bool firstNetUpdate = true; byte monitorMsgQueue = false; byte netShowLatencies = false; byte netDev = false; float netConnectTime; //int netCoordTime = 17; float netConnectTimeout = 10; float netSimulatedLatencySeconds = 0; // Local packets are stored into this buffer. dd_bool reboundPacket; netbuffer_t reboundStore; // PRIVATE DATA DEFINITIONS ------------------------------------------------ #ifdef __CLIENT__ static int coordTimer = 0; #endif // CODE -------------------------------------------------------------------- void Net_Register(void) { // Cvars C_VAR_BYTE("net-queue-show", &monitorMsgQueue, 0, 0, 1); C_VAR_BYTE("net-dev", &netDev, 0, 0, 1); #ifdef _DEBUG C_VAR_FLOAT("net-dev-latency", &netSimulatedLatencySeconds, CVF_NO_MAX, 0, 0); #endif //C_VAR_BYTE("net-nosleep", &netDontSleep, 0, 0, 1); //C_VAR_CHARPTR("net-master-address", &masterAddress, 0, 0, 0); //C_VAR_INT("net-master-port", &masterPort, 0, 0, 65535); //C_VAR_CHARPTR("net-master-path", &masterPath, 0, 0, 0); C_VAR_CHARPTR("net-name", &playerName, 0, 0, 0); #ifdef __CLIENT__ // Cvars (client) C_VAR_FLOAT("client-connect-timeout", &netConnectTimeout, CVF_NO_MAX, 0, 0); #endif #ifdef __SERVER__ // Cvars (server) C_VAR_CHARPTR("server-name", &serverName, 0, 0, 0); C_VAR_CHARPTR("server-info", &serverInfo, 0, 0, 0); C_VAR_INT("server-public", &masterAware, 0, 0, 1); C_VAR_CHARPTR("server-password", &netPassword, 0, 0, 0); C_VAR_BYTE("server-latencies", &netShowLatencies, 0, 0, 1); C_VAR_INT("server-frame-interval", &frameInterval, CVF_NO_MAX, 0, 0); C_VAR_INT("server-player-limit", &svMaxPlayers, 0, 0, DDMAXPLAYERS); #endif // Ccmds C_CMD_FLAGS("chat", NULL, Chat, CMDF_NO_NULLGAME); C_CMD_FLAGS("chatnum", NULL, Chat, CMDF_NO_NULLGAME); C_CMD_FLAGS("chatto", NULL, Chat, CMDF_NO_NULLGAME); C_CMD_FLAGS("conlocp", "i", MakeCamera, CMDF_NO_NULLGAME); #ifdef __CLIENT__ C_CMD_FLAGS("connect", NULL, Connect, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); /// @todo Must reimplement using libshell. -jk //C_CMD_FLAGS("login", NULL, Login, CMDF_NO_NULLGAME); #endif #ifdef __SERVER__ //C_CMD_FLAGS("logout", "", Logout, CMDF_NO_NULLGAME); C_CMD_FLAGS("kick", "i", Kick, CMDF_NO_NULLGAME); #endif C_CMD_FLAGS("net", NULL, Net, CMDF_NO_NULLGAME); C_CMD_FLAGS("ping", NULL, Ping, CMDF_NO_NULLGAME); C_CMD_FLAGS("say", NULL, Chat, CMDF_NO_NULLGAME); C_CMD_FLAGS("saynum", NULL, Chat, CMDF_NO_NULLGAME); C_CMD_FLAGS("sayto", NULL, Chat, CMDF_NO_NULLGAME); #ifdef __CLIENT__ C_CMD("setname", "s", SetName); C_CMD("setcon", "i", SetConsole); #endif C_CMD("settics", "i", SetTicks); #ifdef __CLIENT__ N_Register(); #endif #ifdef __SERVER__ Server_Register(); #endif } void Net_Init(void) { int i; for(i = 0; i < DDMAXPLAYERS; ++i) { memset(clients + i, 0, sizeof(clients[i])); clients[i].viewConsole = -1; Net_AllocClientBuffers(i); } memset(&netBuffer, 0, sizeof(netBuffer)); netBuffer.headerLength = netBuffer.msg.data - (byte *) &netBuffer.msg; // The game is always started in single-player mode. netGame = false; } void Net_Shutdown(void) { netGame = false; N_Shutdown(); Net_DestroyArrays(); } #undef Net_GetPlayerName DENG_EXTERN_C const char* Net_GetPlayerName(int player) { return clients[player].name; } #undef Net_GetPlayerID DENG_EXTERN_C ident_t Net_GetPlayerID(int player) { if(!clients[player].connected) return 0; return clients[player].id; } /** * Sends the contents of the netBuffer. */ void Net_SendBuffer(int toPlayer, int spFlags) { #ifdef __CLIENT__ // Don't send anything during demo playback. if(playback) return; #endif netBuffer.player = toPlayer; // A rebound packet? if(spFlags & SPF_REBOUND) { reboundStore = netBuffer; reboundPacket = true; return; } #ifdef __CLIENT__ Demo_WritePacket(toPlayer); #endif // Can we send the packet? if(spFlags & SPF_DONT_SEND) return; // Send the packet to the network. N_SendPacket(spFlags); } /** * @return @c false, if there are no packets waiting. */ dd_bool Net_GetPacket(void) { if(reboundPacket) // Local packets rebound. { netBuffer = reboundStore; netBuffer.player = consolePlayer; //netBuffer.cursor = netBuffer.msg.data; reboundPacket = false; return true; } #ifdef __CLIENT__ if(playback) { // We're playing a demo. This overrides all other packets. return Demo_ReadPacket(); } #endif if(!netGame) { // Packets cannot be received. return false; } if(!N_GetPacket()) return false; #ifdef __CLIENT__ // Are we recording a demo? if(isClient && clients[consolePlayer].recording) { Demo_WritePacket(consolePlayer); } #endif return true; } #undef Net_PlayerSmoother DENG_EXTERN_C Smoother* Net_PlayerSmoother(int player) { if(player < 0 || player >= DDMAXPLAYERS) return 0; return clients[player].smoother; } void Net_SendPlayerInfo(int srcPlrNum, int destPlrNum) { size_t nameLen; DENG_ASSERT(srcPlrNum >= 0 && srcPlrNum < DDMAXPLAYERS); nameLen = strlen(clients[srcPlrNum].name); LOG_AS("Net_SendPlayerInfo"); LOGDEV_NET_VERBOSE("src=%i dest=%i name=%s") << srcPlrNum << destPlrNum << clients[srcPlrNum].name; Msg_Begin(PKT_PLAYER_INFO); Writer_WriteByte(msgWriter, srcPlrNum); Writer_WriteUInt16(msgWriter, nameLen); Writer_Write(msgWriter, clients[srcPlrNum].name, nameLen); Msg_End(); Net_SendBuffer(destPlrNum, 0); } /** * This is the public interface of the message sender. */ #undef Net_SendPacket DENG_EXTERN_C void Net_SendPacket(int to_player, int type, const void* data, size_t length) { unsigned int flags = 0; #ifndef DENG_WRITER_TYPECHECK Msg_Begin(type); if(data) Writer_Write(msgWriter, data, length); Msg_End(); #else assert(length <= NETBUFFER_MAXSIZE); netBuffer.msg.type = type; netBuffer.length = length; if(data) memcpy(netBuffer.msg.data, data, length); #endif if(isClient) { // As a client we can only send messages to the server. Net_SendBuffer(0, flags); } else { // The server can send packets to any player. // Only allow sending to the sixteen possible players. Net_SendBuffer(to_player & DDSP_ALL_PLAYERS ? NSP_BROADCAST : (to_player & 0xf), flags); } } /** * Prints the message in the console. */ void Net_ShowChatMessage(int plrNum, const char* message) { const char* fromName = (plrNum > 0? clients[plrNum].name : "[sysop]"); const char* sep = (plrNum > 0? ":" : ""); LOG_NOTE("%s%s%s %s") << (!plrNum? _E(1) : _E(D)) << fromName << sep << message; } /** * After a long period with no updates (map setup), calling this will reset * the tictimer so that no time seems to have passed. */ void Net_ResetTimer(void) { int i; firstNetUpdate = true; for(i = 0; i < DDMAXPLAYERS; ++i) { if(/*!clients[i].connected ||*/ !clients[i].smoother) continue; Smoother_Clear(clients[i].smoother); } } /** * @return @c true, if the specified player is a real, local player. */ dd_bool Net_IsLocalPlayer(int plrNum) { player_t *plr = &ddPlayers[plrNum]; return plr->shared.inGame && (plr->shared.flags & DDPF_LOCAL); } /** * Send the local player(s) ticcmds to the server. */ void Net_SendCommands(void) { } static void Net_DoUpdate(void) { static int lastTime = 0; int nowTime, newTics; /** * This timing is only used by the client when it determines if it is * time to send ticcmds or coordinates to the server. */ // Check time. nowTime = Timer_Ticks(); // Clock reset? if(firstNetUpdate) { firstNetUpdate = false; lastTime = nowTime; } newTics = nowTime - lastTime; if(newTics <= 0) return; // Nothing new to update. lastTime = nowTime; // This is as far as dedicated servers go. #ifdef __CLIENT__ /** * Clients will periodically send their coordinates to the server so * any prediction errors can be fixed. Client movement is almost * entirely local. */ coordTimer -= newTics; if(isClient && coordTimer <= 0 && ddPlayers[consolePlayer].shared.mo) { mobj_t *mo = ddPlayers[consolePlayer].shared.mo; coordTimer = 1; //netCoordTime; // 35/2 Msg_Begin(PKT_COORDS); Writer_WriteFloat(msgWriter, gameTime); Writer_WriteFloat(msgWriter, mo->origin[VX]); Writer_WriteFloat(msgWriter, mo->origin[VY]); if(mo->origin[VZ] == mo->floorZ) { // This'll keep us on the floor even in fast moving sectors. Writer_WriteInt32(msgWriter, DDMININT); } else { Writer_WriteInt32(msgWriter, FLT2FIX(mo->origin[VZ])); } // Also include angles. Writer_WriteUInt16(msgWriter, mo->angle >> 16); Writer_WriteInt16(msgWriter, P_LookDirToShort(ddPlayers[consolePlayer].shared.lookDir)); // Control state. Writer_WriteChar(msgWriter, FLT2FIX(ddPlayers[consolePlayer].shared.forwardMove) >> 13); Writer_WriteChar(msgWriter, FLT2FIX(ddPlayers[consolePlayer].shared.sideMove) >> 13); Msg_End(); Net_SendBuffer(0, 0); } #endif // __CLIENT__ } /** * Handle incoming packets, clients send ticcmds and coordinates to * the server. */ void Net_Update(void) { Net_DoUpdate(); // Check for received packets. #ifdef __CLIENT__ Cl_GetPackets(); #endif } void Net_AllocClientBuffers(int clientId) { if(clientId < 0 || clientId >= DDMAXPLAYERS) return; assert(!clients[clientId].smoother); // Movement smoother. clients[clientId].smoother = Smoother_New(); } void Net_DestroyArrays(void) { int i; for(i = 0; i < DDMAXPLAYERS; ++i) { if(clients[i].smoother) { Smoother_Delete(clients[i].smoother); } } memset(clients, 0, sizeof(clients)); } /** * This is the network one-time initialization (into single-player mode). */ void Net_InitGame(void) { #ifdef __CLIENT__ Cl_InitID(); #endif // In single-player mode there is only player number zero. consolePlayer = displayPlayer = 0; // We're in server mode if we aren't a client. isServer = true; // Netgame is true when we're aware of the network (i.e. other players). netGame = false; ddPlayers[0].shared.inGame = true; ddPlayers[0].shared.flags |= DDPF_LOCAL; #ifdef __CLIENT__ clients[0].id = clientID; #endif clients[0].ready = true; clients[0].connected = true; clients[0].viewConsole = 0; clients[0].lastTransmit = -1; } void Net_StopGame() { LOG_AS("Net_StopGame"); #ifdef __SERVER__ if(isServer) { // We are an open server. // This means we should inform all the connected clients that the // server is about to close. Msg_Begin(PSV_SERVER_CLOSE); Msg_End(); Net_SendBuffer(NSP_BROADCAST, 0); } #endif #ifdef __CLIENT__ LOGDEV_NET_MSG("Sending PCL_GOODBYE"); // We are a connected client. Msg_Begin(PCL_GOODBYE); Msg_End(); Net_SendBuffer(0, 0); // Must stop recording, we're disconnecting. Demo_StopRecording(consolePlayer); Cl_CleanUp(); isClient = false; netLoggedIn = false; #endif // Netgame has ended. netGame = false; isServer = true; allowSending = false; #ifdef __SERVER__ // No more remote users. netRemoteUser = 0; #endif // All remote players are forgotten. for(int i = 0; i < DDMAXPLAYERS; ++i) { player_t *plr = &ddPlayers[i]; client_t *cl = &clients[i]; plr->shared.inGame = false; cl->ready = cl->connected = false; cl->id = 0; cl->nodeID = 0; cl->viewConsole = -1; plr->shared.flags &= ~(DDPF_CAMERA | DDPF_CHASECAM | DDPF_LOCAL); } // We're about to become player zero, so update it's view angles to // match our current ones. if(ddPlayers[0].shared.mo) { /* $unifiedangles */ ddPlayers[0].shared.mo->angle = ddPlayers[consolePlayer].shared.mo->angle; ddPlayers[0].shared.lookDir = ddPlayers[consolePlayer].shared.lookDir; } LOGDEV_NET_NOTE("Reseting console and view players to zero"); consolePlayer = displayPlayer = 0; ddPlayers[0].shared.inGame = true; clients[0].ready = true; clients[0].connected = true; clients[0].viewConsole = 0; ddPlayers[0].shared.flags |= DDPF_LOCAL; } /** * @return Delta based on 'now' (- future, + past). */ int Net_TimeDelta(byte now, byte then) { int delta; if(now >= then) { // Simple case. delta = now - then; } else { // There's a wraparound. delta = 256 - then + now; } // The time can be in the future. We'll allow one second. if(delta > 220) delta -= 256; return delta; } #ifdef __CLIENT__ /// @return @c true iff a demo is currently being recorded. static dd_bool recordingDemo(void) { int i; for(i = 0; i < DDMAXPLAYERS; ++i) { if(ddPlayers[i].shared.inGame && clients[i].recording) return true; } return false; } #endif #ifdef __CLIENT__ void Net_DrawDemoOverlay(void) { char buf[160], tmp[40]; int x = DENG_GAMEVIEW_WIDTH - 10, y = 10; if(!recordingDemo() || !(SECONDS_TO_TICKS(gameTime) & 8)) return; strcpy(buf, "["); { int i, c; for(i = c = 0; i < DDMAXPLAYERS; ++i) { if(!(!ddPlayers[i].shared.inGame || !clients[i].recording)) { // This is a "real" player (or camera). if(c++) strcat(buf, ","); sprintf(tmp, "%i:%s", i, clients[i].recordPaused ? "-P-" : "REC"); strcat(buf, tmp); } }} strcat(buf, "]"); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // Go into screen projection mode. glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, DENG_GAMEVIEW_WIDTH, DENG_GAMEVIEW_HEIGHT, 0, -1, 1); glEnable(GL_TEXTURE_2D); FR_SetFont(fontFixed); FR_LoadDefaultAttrib(); FR_SetColorAndAlpha(1, 1, 1, 1); FR_DrawTextXY3(buf, x, y, ALIGN_TOPRIGHT, DTF_NO_EFFECTS); glDisable(GL_TEXTURE_2D); // Restore original matrix. glMatrixMode(GL_PROJECTION); glPopMatrix(); } #endif // __CLIENT__ void Net_Drawer() { #ifdef __CLIENT__ // Draw the blockmap debug display. Rend_BlockmapDebug(); // Draw the light range debug display. Rend_DrawLightModMatrix(); # ifdef DENG2_DEBUG // Draw the input debug display. I_DebugDrawer(); # endif // Draw the demo recording overlay. Net_DrawDemoOverlay(); # ifdef DENG2_DEBUG Z_DebugDrawer(); # endif #endif // __CLIENT__ } void Net_Ticker(timespan_t time) { int i; client_t *cl; // Network event ticker. N_NETicker(time); #ifdef __SERVER__ if(netDev) { static int printTimer = 0; if(printTimer++ > TICSPERSEC) { printTimer = 0; for(i = 0; i < DDMAXPLAYERS; ++i) { if(Sv_IsFrameTarget(i)) { LOGDEV_NET_MSG("%i(rdy%i): avg=%05ims thres=%05ims " "bwr=%05i maxfs=%05ib unakd=%05i") << i << clients[i].ready << 0 << 0 << clients[i].bandwidthRating << Sv_GetMaxFrameSize(i) << Sv_CountUnackedDeltas(i); } } } } #endif // __SERVER__ // The following stuff is only for netgames. if(!netGame) return; // Check the pingers. for(i = 0, cl = clients; i < DDMAXPLAYERS; ++i, cl++) { // Clients can only ping the server. if(!(isClient && i) && i != consolePlayer) { if(cl->ping.sent) { // The pinger is active. if(Timer_RealMilliseconds() - cl->ping.sent > PING_TIMEOUT) // Timed out? { cl->ping.times[cl->ping.current] = -1; Net_SendPing(i, 0); } } } } } de::String ServerInfo_AsStyledText(serverinfo_t const *sv) { #define TABBED(A, B) _E(Ta)_E(l) " " A _E(.) " " _E(\t) B "\n" return de::String(_E(b) "%1" _E(.) "\n%2\n" _E(T`) TABBED("Joinable:", "%5") TABBED("Players:", "%3 / %4%12") TABBED("Game:", "%8\n%9\n%11 %10") TABBED("PWADs:", "%13") TABBED("Address:", "%6:%7") /*TABBED("Ping:", "%8 ms (approx)")*/) .arg(sv->name) .arg(sv->description) .arg(sv->numPlayers) .arg(sv->maxPlayers) .arg(sv->canJoin? "Yes" : "No") // 5 .arg(sv->address) .arg(sv->port) //.arg(sv->ping) .arg(sv->plugin) .arg(sv->gameIdentityKey) // 10 .arg(sv->gameConfig) .arg(sv->map) .arg(!de::String(sv->clientNames).isEmpty()? de::String(_E(2) " (%1)" _E(.)).arg(sv->clientNames) : "") .arg(de::String(sv->pwads).isEmpty()? de::String(DENG2_CHAR_MDASH) : de::String(sv->pwads)); // 14 #undef TABBED } /** * Prints server/host information into the console. The header line is * printed if 'info' is NULL. */ void ServerInfo_Print(serverinfo_t const *info, int index) { /// @todo Update table for de::Log. -jk /// if(!info) { LOG_NET_MSG(_E(m)" %-20s P/M L Ver: Game: Location:") << "Name:"; } else { LOG_NET_MSG(_E(m)"%-2i: %-20s %i/%-2i %c %-5i %-16s %s:%i") << index << info->name << info->numPlayers << info->maxPlayers << (info->canJoin? ' ' : '*') << info->version << info->plugin << info->address << info->port; LOG_NET_MSG(" %s p:%ims %-40s") << info->map << info->ping << info->description; LOG_NET_MSG(" %s (CRC:%x) %s") << info->gameIdentityKey << info->loadedFilesCRC << info->gameConfig; // Optional: PWADs in use. if(info->pwads[0]) LOG_NET_MSG(" PWADs: %s") << info->pwads; // Optional: names of players. if(info->clientNames[0]) LOG_NET_MSG(" Players: %s") << info->clientNames; // Optional: data values. if(info->data[0] || info->data[1] || info->data[2]) { LOG_NET_MSG(" Data: (%08x, %08x, %08x)") << info->data[0] << info->data[1] << info->data[2]; } } } /** * Composes a PKT_CHAT network message. */ void Net_WriteChatMessage(int from, int toMask, const char* message) { size_t len = strlen(message); len = MIN_OF(len, 0xffff); Msg_Begin(PKT_CHAT); Writer_WriteByte(msgWriter, from); Writer_WriteUInt32(msgWriter, toMask); Writer_WriteUInt16(msgWriter, len); Writer_Write(msgWriter, message, len); Msg_End(); } /** * All arguments are sent out as a chat message. */ D_CMD(Chat) { DENG2_UNUSED(src); char buffer[100]; int i, mode = !stricmp(argv[0], "chat") || !stricmp(argv[0], "say") ? 0 : !stricmp(argv[0], "chatNum") || !stricmp(argv[0], "sayNum") ? 1 : 2; unsigned short mask = 0; if(argc == 1) { LOG_SCR_NOTE("Usage: %s %s(text)") << argv[0] << (!mode ? "" : mode == 1 ? "(plr#) " : "(name) "); LOG_SCR_MSG("Chat messages are max 80 characters long. Use quotes to get around " "arg processing."); return true; } LOG_AS("chat (Cmd)"); // Chatting is only possible when connected. if(!netGame) return false; // Too few arguments? if(mode && argc < 3) return false; // Assemble the chat message. strcpy(buffer, argv[!mode ? 1 : 2]); for(i = (!mode ? 2 : 3); i < argc; ++i) { strcat(buffer, " "); strncat(buffer, argv[i], 80 - (strlen(buffer) + strlen(argv[i]) + 1)); } buffer[80] = 0; // Send the message. switch(mode) { case 0: // chat mask = ~0; break; case 1: // chatNum mask = 1 << atoi(argv[1]); break; case 2: // chatTo { dd_bool found = false; for(i = 0; i < DDMAXPLAYERS && !found; ++i) { if(!stricmp(clients[i].name, argv[1])) { mask = 1 << i; found = true; } } break; } default: LOG_SCR_ERROR("Invalid value, mode = %i") << mode; break; } Net_WriteChatMessage(consolePlayer, mask, buffer); if(!isClient) { if(mask == (unsigned short) ~0) { Net_SendBuffer(NSP_BROADCAST, 0); } else { for(i = 1; i < DDMAXPLAYERS; ++i) if(ddPlayers[i].shared.inGame && (mask & (1 << i))) Net_SendBuffer(i, 0); } } else { Net_SendBuffer(0, 0); } // Show the message locally. Net_ShowChatMessage(consolePlayer, buffer); // Inform the game, too. gx.NetPlayerEvent(consolePlayer, DDPE_CHAT_MESSAGE, buffer); return true; } #ifdef __SERVER__ D_CMD(Kick) { DENG2_UNUSED2(src, argc); int num; LOG_AS("kick (Cmd)") if(!netGame) { LOG_SCR_ERROR("This is not a network game"); return false; } if(!isServer) { LOG_SCR_ERROR("Only allowed on the server"); return false; } num = atoi(argv[1]); if(num < 1 || num >= DDMAXPLAYERS) { LOG_NET_ERROR("Invalid client number"); return false; } if(netRemoteUser == num) { LOG_NET_ERROR("Can't kick the client who's logged in"); return false; } Sv_Kick(num); return true; } #endif // __SERVER__ #ifdef __CLIENT__ D_CMD(SetName) { DENG2_UNUSED2(src, argc); Con_SetString("net-name", argv[1]); if(!netGame) return true; // The server does not have a name. if(!isClient) return false; memset(clients[consolePlayer].name, 0, sizeof(clients[consolePlayer].name)); strncpy(clients[consolePlayer].name, argv[1], PLAYERNAMELEN - 1); Net_SendPlayerInfo(consolePlayer, 0); return true; } #endif D_CMD(SetTicks) { DENG2_UNUSED2(src, argc); // extern double lastSharpFrameTime; firstNetUpdate = true; Timer_SetTicksPerSecond(strtod(argv[1], 0)); // lastSharpFrameTime = Sys_GetTimef(); return true; } D_CMD(MakeCamera) { DENG2_UNUSED2(src, argc); /* int cp; mobj_t *mo; ddplayer_t *conp = players + consolePlayer; if(argc < 2) return true; cp = atoi(argv[1]); clients[cp].connected = true; clients[cp].ready = true; clients[cp].updateCount = UPDATECOUNT; ddPlayers[cp].flags |= DDPF_CAMERA; ddPlayers[cp].inGame = true; // !!! Sv_InitPoolForClient(cp); mo = Z_Malloc(sizeof(mobj_t), PU_MAP, 0); memset(mo, 0, sizeof(*mo)); mo->origin[VX] = conp->mo->origin[VX]; mo->origin[VY] = conp->mo->origin[VY]; mo->origin[VZ] = conp->mo->origin[VZ]; mo->bspLeaf = conp->mo->bspLeaf; ddPlayers[cp].mo = mo; displayPlayer = cp; */ // Create a new local player. int cp; LOG_AS("makecam (Cmd)"); cp = atoi(argv[1]); if(cp < 0 || cp >= DDMAXPLAYERS) return false; if(clients[cp].connected) { LOG_ERROR("Client %i already connected") << cp; return false; } clients[cp].connected = true; clients[cp].ready = true; clients[cp].viewConsole = cp; ddPlayers[cp].shared.flags |= DDPF_LOCAL; Smoother_Clear(clients[cp].smoother); #ifdef __SERVER__ Sv_InitPoolForClient(cp); #endif #ifdef __CLIENT__ R_SetupDefaultViewWindow(cp); // Update the viewports. R_SetViewGrid(0, 0); #endif return true; } #ifdef __CLIENT__ D_CMD(SetConsole) { DENG2_UNUSED2(src, argc); int cp = atoi(argv[1]); if(ddPlayers[cp].shared.inGame) { consolePlayer = displayPlayer = cp; } // Update the viewports. R_SetViewGrid(0, 0); return true; } int Net_StartConnection(const char* address, int port) { LOG_AS("Net_StartConnection"); LOG_NET_MSG("Connecting to %s (port %i)...") << address << port; // Start searching at the specified location. Net_ServerLink().connectDomain(de::String("%1:%2").arg(address).arg(port), 7 /*timeout*/); return true; } /** * Intelligently connect to a server. Just provide an IP address and the * rest is automatic. */ D_CMD(Connect) { DENG2_UNUSED(src); char *ptr; int port = 0; if(argc < 2 || argc > 3) { LOG_SCR_NOTE("Usage: %s (ip-address) [port]") << argv[0]; LOG_SCR_MSG("A TCP/IP connection is created to the given server. If a port is not " "specified port zero will be used."); return true; } if(netGame) { LOG_NET_ERROR("Already connected"); return false; } // If there is a port specified in the address, use it. port = 0; if((ptr = strrchr(argv[1], ':'))) { port = strtol(ptr + 1, 0, 0); *ptr = 0; } if(argc == 3) { port = strtol(argv[2], 0, 0); } return Net_StartConnection(argv[1], port); } #endif // __CLIENT__ /** * The 'net' console command. */ D_CMD(Net) { DENG2_UNUSED(src); dd_bool success = true; if(argc == 1) // No args? { LOG_SCR_NOTE("Usage: %s (cmd/args)") << argv[0]; LOG_SCR_MSG("Commands:"); LOG_SCR_MSG(" init"); LOG_SCR_MSG(" shutdown"); LOG_SCR_MSG(" info"); LOG_SCR_MSG(" request"); #ifdef __CLIENT__ LOG_SCR_MSG(" setup client"); LOG_SCR_MSG(" search (address) [port] (local or targeted query)"); LOG_SCR_MSG(" servers (asks the master server)"); LOG_SCR_MSG(" connect (idx)"); LOG_SCR_MSG(" mconnect (m-idx)"); LOG_SCR_MSG(" disconnect"); #endif #ifdef __SERVER__ LOG_SCR_MSG(" announce"); #endif return true; } if(argc == 2) // One argument? { if(!stricmp(argv[1], "announce")) { N_MasterAnnounceServer(true); } else if(!stricmp(argv[1], "request")) { N_MasterRequestList(); } else if(!stricmp(argv[1], "servers")) { N_MAPost(MAC_REQUEST); N_MAPost(MAC_WAIT); N_MAPost(MAC_LIST); } else if(!stricmp(argv[1], "info")) { N_PrintNetworkStatus(); LOG_NET_MSG("Network game: %b") << netGame; LOG_NET_MSG("This is console %i (local player %i)") << consolePlayer << P_ConsoleToLocal(consolePlayer); } #ifdef __CLIENT__ else if(!stricmp(argv[1], "disconnect")) { if(!netGame) { LOG_NET_ERROR("This client is not connected to a server"); return false; } if(!isClient) { LOG_NET_ERROR("This is not a client"); return false; } Net_ServerLink().disconnect(); LOG_NET_NOTE("Disconnected"); } #endif else { LOG_SCR_ERROR("Invalid arguments"); return false; // Bad args. } } if(argc == 3) // Two arguments? { #ifdef __CLIENT__ if(!stricmp(argv[1], "search")) { Net_ServerLink().discover(argv[2]); } else if(!stricmp(argv[1], "connect")) { if(netGame) { LOG_NET_ERROR("Already connected"); return false; } int index = strtoul(argv[2], 0, 10); serverinfo_t info; if(Net_ServerLink().foundServerInfo(index, &info)) { ServerInfo_Print(&info, index); Net_ServerLink().connectDomain(de::String("%1:%2").arg(info.address).arg(info.port), 5); } } else if(!stricmp(argv[1], "mconnect")) { serverinfo_t info; if(N_MasterGet(strtol(argv[2], 0, 0), &info)) { // Connect using TCP/IP. return Con_Executef(CMDS_CONSOLE, false, "connect %s %i", info.address, info.port); } else return false; } else if(!stricmp(argv[1], "setup")) { // Start network setup. if(!stricmp(argv[2], "client")) { ClientWindow::main().taskBar().close(); ClientWindow::main().taskBar().showMultiplayer(); } else return false; } #endif } #ifdef __CLIENT__ if(argc == 4) { if(!stricmp(argv[1], "search")) { //success = N_LookForHosts(argv[2], strtol(argv[3], 0, 0), 0); Net_ServerLink().discover(de::String(argv[2]) + ":" + argv[3]); } } #endif return success; } /** * Extracts the label and value from a string. 'max' is the maximum * allowed length of a token, including terminating \0. */ static dd_bool tokenize(char const *line, char *label, char *value, int max) { const char *src = line; const char *colon = strchr(src, ':'); // The colon must exist near the beginning. if(!colon || colon - src >= SVINFO_VALID_LABEL_LEN) return false; // Copy the label. memset(label, 0, max); strncpy(label, src, MIN_OF(colon - src, max - 1)); // Copy the value. memset(value, 0, max); strncpy(value, colon + 1, MIN_OF(strlen(line) - (colon - src + 1), (unsigned) max - 1)); // Everything is OK. return true; } void ServerInfo_FromRecord(serverinfo_t *info, de::Record const &rec) { de::zapPtr(info); info->port = (int) rec["port"].value().asNumber(); info->version = (int) rec["ver" ].value().asNumber(); info->loadedFilesCRC = (uint) rec["wcrc"].value().asNumber(); info->numPlayers = (int) rec["nump"].value().asNumber(); info->maxPlayers = (int) rec["maxp"].value().asNumber(); info->canJoin = rec["open"].value().isTrue(); #define COPY_STR(Member, VarName) \ strncpy(Member, rec[VarName].value().asText().toUtf8(), sizeof(Member) - 1); COPY_STR(info->name, "name" ); COPY_STR(info->description, "info" ); COPY_STR(info->plugin, "game" ); COPY_STR(info->gameIdentityKey, "mode" ); COPY_STR(info->gameConfig, "setup"); COPY_STR(info->iwad, "iwad" ); COPY_STR(info->pwads, "pwads"); COPY_STR(info->map, "map" ); COPY_STR(info->clientNames, "plrn" ); #undef COPY_STR } dd_bool ServerInfo_FromString(serverinfo_t *info, char const *valuePair) { char label[SVINFO_TOKEN_LEN], value[SVINFO_TOKEN_LEN]; // Extract the label and value. The maximum length of a value is // TOKEN_LEN. Labels are returned in lower case. if(!tokenize(valuePair, label, value, sizeof(value))) { // Badly formed lines are ignored. return false; } if(!strcmp(label, "at")) { strncpy(info->address, value, sizeof(info->address) - 1); } else if(!strcmp(label, "port")) { info->port = strtol(value, 0, 0); } else if(!strcmp(label, "ver")) { info->version = strtol(value, 0, 0); } else if(!strcmp(label, "map")) { strncpy(info->map, value, sizeof(info->map) - 1); } else if(!strcmp(label, "game")) { strncpy(info->plugin, value, sizeof(info->plugin) - 1); } else if(!strcmp(label, "name")) { strncpy(info->name, value, sizeof(info->name) - 1); } else if(!strcmp(label, "info")) { strncpy(info->description, value, sizeof(info->description) - 1); } else if(!strcmp(label, "nump")) { info->numPlayers = strtol(value, 0, 0); } else if(!strcmp(label, "maxp")) { info->maxPlayers = strtol(value, 0, 0); } else if(!strcmp(label, "open")) { info->canJoin = strtol(value, 0, 0); } else if(!strcmp(label, "mode")) { strncpy(info->gameIdentityKey, value, sizeof(info->gameIdentityKey) - 1); } else if(!strcmp(label, "setup")) { strncpy(info->gameConfig, value, sizeof(info->gameConfig) - 1); } else if(!strcmp(label, "iwad")) { strncpy(info->iwad, value, sizeof(info->iwad) - 1); } else if(!strcmp(label, "wcrc")) { info->loadedFilesCRC = strtol(value, 0, 0); } else if(!strcmp(label, "pwads")) { strncpy(info->pwads, value, sizeof(info->pwads) - 1); } else if(!strcmp(label, "plrn")) { strncpy(info->clientNames, value, sizeof(info->clientNames) - 1); } else if(!strcmp(label, "data0")) { info->data[0] = strtol(value, 0, 16); } else if(!strcmp(label, "data1")) { info->data[1] = strtol(value, 0, 16); } else if(!strcmp(label, "data2")) { info->data[2] = strtol(value, 0, 16); } else { // Unknown labels are ignored. return false; } return true; } de::String Net_UserAgent() { return QString(DOOMSDAY_NICENAME " " DOOMSDAY_VERSION_TEXT) + " (" + de::Version().operatingSystem() + ")"; } doomsday-stable-1.15.7/doomsday/client/src/network/net_event.cpp0000664000175000017500000001433512641367670024252 0ustar jaakkojaakko/** @file network/net_event.cpp Network Events. * * Network events include clients joining and leaving. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "de_system.h" #include "de_console.h" #include "de_network.h" #ifdef __SERVER__ # include "serversystem.h" # include "dd_main.h" #endif using namespace de; #define MASTER_QUEUE_LEN 16 #define NETEVENT_QUEUE_LEN 32 #define MASTER_HEARTBEAT 120 // seconds #define MASTER_UPDATETIME 3 // seconds // The master action queue. static masteraction_t masterQueue[MASTER_QUEUE_LEN]; static int mqHead, mqTail; // The net event queue (player arrive/leave). static netevent_t netEventQueue[NETEVENT_QUEUE_LEN]; static int neqHead, neqTail; #ifdef __SERVER__ // Countdown for master updates. static timespan_t masterHeartbeat = 0; #endif /** * Add a master action command to the queue. The master action stuff really * doesn't belong in this file... */ void N_MAPost(masteraction_t act) { masterQueue[mqHead++] = act; mqHead %= MASTER_QUEUE_LEN; } /** * Get a master action command from the queue. */ dd_bool N_MAGet(masteraction_t *act) { // Empty queue? if(mqHead == mqTail) return false; *act = masterQueue[mqTail]; return true; } /** * Remove a master action command from the queue. */ void N_MARemove(void) { if(mqHead != mqTail) { mqTail = (mqTail + 1) % MASTER_QUEUE_LEN; } } /** * Clear the master action command queue. */ void N_MAClear(void) { mqHead = mqTail = 0; } /** * @return @c true, if the master action command queue is empty. */ dd_bool N_MADone(void) { return (mqHead == mqTail); } /** * Add a net event to the queue, to wait for processing. */ void N_NEPost(netevent_t * nev) { netEventQueue[neqHead] = *nev; neqHead = (neqHead + 1) % NETEVENT_QUEUE_LEN; } /** * Are there any net events awaiting processing? * * \note N_GetPacket() will not return a packet until all net events have * processed. * * @return @c true if there are net events waiting to be * processed. */ dd_bool N_NEPending(void) { return neqHead != neqTail; } /** * Get a net event from the queue. * * @return @c true, if an event was returned. */ dd_bool N_NEGet(netevent_t *nev) { // Empty queue? if(!N_NEPending()) return false; *nev = netEventQueue[neqTail]; neqTail = (neqTail + 1) % NETEVENT_QUEUE_LEN; return true; } /** * Handles low-level net tick stuff: communication with the master server. */ void N_NETicker(timespan_t time) { #if !defined(__SERVER__) DENG2_UNUSED(time); #endif #ifdef __SERVER__ if(netGame) { masterHeartbeat -= time; // Update master every 2 minutes. if(masterAware && App_ServerSystem().isListening() && App_WorldSystem().hasMap() && masterHeartbeat < 0) { masterHeartbeat = MASTER_HEARTBEAT; N_MasterAnnounceServer(true); } } #endif // Is there a master action to worry about? masteraction_t act; if(N_MAGet(&act)) { switch(act) { case MAC_REQUEST: // Send the request for servers. N_MasterRequestList(); N_MARemove(); break; case MAC_WAIT: // Handle incoming messages. if(N_MasterGet(0, 0) >= 0) { // The list has arrived! N_MARemove(); } break; case MAC_LIST: { ServerInfo_Print(NULL, 0); int num = N_MasterGet(0, 0); int i = num; while(--i >= 0) { serverinfo_t info; N_MasterGet(i, &info); ServerInfo_Print(&info, i); } LOG_NET_VERBOSE("%i server%s found") << num << (num != 1 ? "s were" : " was"); N_MARemove(); break; } default: DENG_ASSERT(!"N_NETicker: Invalid value for 'act'"); break; } } } /** * The event list is checked for arrivals and exits, and the 'clients' * and 'players' arrays are updated accordingly. */ void N_Update(void) { #ifdef __SERVER__ netevent_t nevent; // Are there any events to process? while(N_NEGet(&nevent)) { switch(nevent.type) { case NE_CLIENT_ENTRY: { // Assign a console to the new player. Sv_PlayerArrives(nevent.id, App_ServerSystem().user(nevent.id).name().toUtf8()); // Update the master. masterHeartbeat = MASTER_UPDATETIME; break; } case NE_CLIENT_EXIT: Sv_PlayerLeaves(nevent.id); // Update the master. masterHeartbeat = MASTER_UPDATETIME; break; default: DENG_ASSERT(!"N_Update: Invalid value"); break; } } #endif // __SERVER__ } /** * The client is removed from the game without delay. This is used when the * server needs to terminate a client's connection abnormally. */ void N_TerminateClient(int console) { #ifdef __SERVER__ if(!clients[console].connected) return; LOG_NET_NOTE("Terminating connection to console %i (player '%s')") << console << clients[console].name; App_ServerSystem().terminateNode(clients[console].nodeID); // Update the master. masterHeartbeat = MASTER_UPDATETIME; #else DENG2_UNUSED(console); #endif } doomsday-stable-1.15.7/doomsday/client/src/network/net_buf.cpp0000664000175000017500000002357712641367670023715 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * Network Message Handling and Buffering */ #include "de_platform.h" #include "de_system.h" #include "de_network.h" #include "de_console.h" #include "de_misc.h" #include #include #include #define MSG_MUTEX_NAME "MsgQueueMutex" dd_bool allowSending; netbuffer_t netBuffer; // The message queue: list of incoming messages waiting for processing. static netmessage_t *msgHead, *msgTail; static int msgCount; // A mutex is used to protect the addition and removal of messages from // the message queue. static mutex_t msgMutex; // Number of bytes of outgoing data transmitted. static size_t numOutBytes; // Number of bytes sent over the network (compressed). static size_t numSentBytes; Reader* Reader_NewWithNetworkBuffer(void) { return Reader_NewWithBuffer((const byte*) netBuffer.msg.data, netBuffer.length); } /** * Initialize the low-level network subsystem. This is called always * during startup (via Sys_Init()). */ void N_Init(void) { // Create a mutex for the message queue. msgMutex = Sys_CreateMutex(MSG_MUTEX_NAME); allowSending = false; //N_SockInit(); N_MasterInit(); } /** * Shut down the low-level network interface. Called during engine * shutdown (not before). */ void N_Shutdown(void) { // Any queued messages will be destroyed. N_ClearMessages(); N_MasterShutdown(); allowSending = false; // Close the handle of the message queue mutex. Sys_DestroyMutex(msgMutex); msgMutex = 0; } /** * Acquire or release ownership of the message queue mutex. * * @return @c true, if successful. */ dd_bool N_LockQueue(dd_bool doAcquire) { if(doAcquire) Sys_Lock(msgMutex); else Sys_Unlock(msgMutex); return true; } /** * Adds the given netmessage_s to the queue of received messages. * We use a mutex to synchronize access to the message queue. * * @note This is called in the network receiver thread. */ void N_PostMessage(netmessage_t *msg) { N_LockQueue(true); // This will be the latest message. msg->next = NULL; // Set the timestamp for reception. msg->receivedAt = Timer_RealSeconds(); if(msgTail) { // There are previous messages. msgTail->next = msg; } // The tail pointer points to the last message. msgTail = msg; // If there is no head, this'll be the first message. if(msgHead == NULL) msgHead = msg; // One new message available. msgCount++; N_LockQueue(false); } /** * Extracts the next message from the queue of received messages. * The caller must release the message when it's no longer needed, * using N_ReleaseMessage(). * * We use a mutex to synchronize access to the message queue. This is * called in the Doomsday thread. * * @return @c NULL, if no message is found; */ netmessage_t *N_GetMessage(void) { // This is the message we'll return. netmessage_t *msg = NULL; N_LockQueue(true); if(msgHead != NULL) { msg = msgHead; // Check for simulated latency. if(netSimulatedLatencySeconds > 0 && (Timer_RealSeconds() - msg->receivedAt < netSimulatedLatencySeconds)) { // This message has not been received yet. msg = NULL; } else { // If there are no more messages, the tail pointer must be // cleared, too. if(!msgHead->next) msgTail = NULL; // Advance the head pointer. msgHead = msgHead->next; if(msg) { // One less message available. msgCount--; } } } N_LockQueue(false); // Identify the sender. if(msg) { msg->player = N_IdentifyPlayer(msg->sender); } return msg; } /** * Frees the message. */ void N_ReleaseMessage(netmessage_t *msg) { if(msg->handle) { delete [] reinterpret_cast(msg->handle); msg->handle = 0; } M_Free(msg); } /** * Empties the message buffers. */ void N_ClearMessages(void) { if(!msgMutex) return; // Not initialized yet. netmessage_t *msg; float oldSim = netSimulatedLatencySeconds; // No simulated latency now. netSimulatedLatencySeconds = 0; while((msg = N_GetMessage()) != NULL) N_ReleaseMessage(msg); netSimulatedLatencySeconds = oldSim; // The queue is now empty. msgHead = msgTail = NULL; msgCount = 0; } /** * Send the data in the netbuffer. The message is sent using an * unreliable, nonsequential (i.e. fast) method. * * Handles broadcasts using recursion. * Clients can only send stuff to the server. */ void N_SendPacket(int flags) { #ifdef __SERVER__ uint dest = 0; #else DENG2_UNUSED(flags); #endif // Is the network available? if(!allowSending) return; // Figure out the destination DPNID. #ifdef __SERVER__ { if(netBuffer.player >= 0 && netBuffer.player < DDMAXPLAYERS) { if(/*(ddpl->flags & DDPF_LOCAL) ||*/ !clients[netBuffer.player].connected) { // Do not send anything to local or disconnected players. return; } dest = clients[netBuffer.player].nodeID; } else { // Broadcast to all non-local players, using recursive calls. for(int i = 0; i < DDMAXPLAYERS; ++i) { netBuffer.player = i; N_SendPacket(flags); } // Reset back to -1 to notify of the broadcast. netBuffer.player = NSP_BROADCAST; return; } } #endif // This is what will be sent. numOutBytes += netBuffer.headerLength + netBuffer.length; try { #ifdef __CLIENT__ de::Transmitter &out = Net_ServerLink(); #else de::Transmitter &out = App_ServerSystem().user(dest); #endif out << de::ByteRefArray(&netBuffer.msg, netBuffer.headerLength + netBuffer.length); } catch(de::Error const &er) { LOGDEV_NET_WARNING("N_SendPacket failed: ") << er.asText(); } } void N_AddSentBytes(size_t bytes) { numSentBytes += bytes; } /** * @return The player number that corresponds network node @a id. */ int N_IdentifyPlayer(nodeid_t id) { #ifdef __SERVER__ // What is the corresponding player number? Only the server keeps // a list of all the IDs. for(int i = 0; i < DDMAXPLAYERS; ++i) { if(clients[i].nodeID == id) return i; } return -1; #else DENG2_UNUSED(id); #endif // Clients receive messages only from the server. return 0; } /** * Retrieves the next incoming message. * * @return The next message waiting in the incoming message queue. * When the message is no longer needed you must call * N_ReleaseMessage() to delete it. */ netmessage_t *N_GetNextMessage(void) { netmessage_t *msg; while((msg = N_GetMessage()) != NULL) { return msg; } return NULL; // There are no more messages. } /** * An attempt is made to extract a message from the message queue. * * @return @c true, if a message successfull. */ dd_bool N_GetPacket(void) { netmessage_t *msg; // If there are net events pending, let's not return any packets // yet. The net events may need to be processed before the // packets. if(N_NEPending()) return false; netBuffer.player = -1; netBuffer.length = 0; if((msg = N_GetNextMessage()) == NULL) { // No messages at this time. return false; } // There was a packet! netBuffer.player = msg->player; netBuffer.length = msg->size - netBuffer.headerLength; if(sizeof(netBuffer.msg) >= msg->size) { memcpy(&netBuffer.msg, msg->data, msg->size); } else { LOG_NET_ERROR("Received an oversized packet with %i bytes") << msg->size; N_ReleaseMessage(msg); return false; } // The message can now be freed. N_ReleaseMessage(msg); // We have no idea who sent this (on serverside). if(netBuffer.player == -1) return false; return true; } /** * Print low-level information about the network buffer. */ void N_PrintBufferInfo(void) { N_PrintTransmissionStats(); } /** * Print status information about the workings of data compression * in the network buffer. * * @note Currently numOutBytes excludes transmission header, while * numSentBytes includes every byte written to the socket. * In other words, the efficiency includes protocol overhead. */ void N_PrintTransmissionStats(void) { if(numOutBytes == 0) { LOG_NET_MSG("Transmission efficiency: Nothing has been sent yet"); } else { LOG_NET_MSG("Transmission efficiency: %.3f%% (data: %i bytes, sent: %i " "bytes)") << (100 - (100.0f * numSentBytes) / numOutBytes) << numOutBytes << numSentBytes; } } doomsday-stable-1.15.7/doomsday/client/src/network/net_msg.cpp0000664000175000017500000000545312641367670023720 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * net_msg.c: Network Messaging * * Buffer overflow checks *ARE NOT* made ifndef _DEBUG * The caller must know what it's doing. * The data is stored using little-endian ordering. * * Note that negative values are not good for the packed write/read routines, * as they always have the high bits set. */ // HEADER FILES ------------------------------------------------------------ #include "de_platform.h" #include "de_network.h" #include "de_console.h" #include Writer* msgWriter; Reader* msgReader; /// An ongoing writer is pushed here if a new one is started before the /// earlier one is finished. static QList pendingWriters; void Msg_Begin(int type) { if(msgReader) { // End reading the netbuffer automatically. Msg_EndRead(); } // An ongoing writer will have to wait. if(msgWriter) { pendingWriters.prepend(msgWriter); msgWriter = 0; } // Allocate a new writer. msgWriter = Writer_NewWithDynamicBuffer(1 /*type*/ + NETBUFFER_MAXSIZE); Writer_WriteByte(msgWriter, type); } dd_bool Msg_BeingWritten(void) { return msgWriter != NULL; } void Msg_End(void) { DENG_ASSERT(msgWriter != 0); // Finalize the netbuffer. // Message type is included as the first byte. netBuffer.length = Writer_Size(msgWriter) - 1 /*type*/; memcpy(&netBuffer.msg, Writer_Data(msgWriter), Writer_Size(msgWriter)); Writer_Delete(msgWriter); msgWriter = 0; // Pop a pending writer off the stack. if(!pendingWriters.isEmpty()) { msgWriter = pendingWriters.takeFirst(); } } void Msg_BeginRead(void) { if(msgWriter) { // End writing the netbuffer automatically. Msg_End(); } // Start reading from the netbuffer. assert(msgReader == NULL); msgReader = Reader_NewWithNetworkBuffer(); } void Msg_EndRead(void) { if(msgReader) { Reader_Delete(msgReader); msgReader = 0; } } doomsday-stable-1.15.7/doomsday/client/src/network/serverlink.cpp0000664000175000017500000003026412641367670024446 0ustar jaakkojaakko/** @file serverlink.cpp Network connection to a server. * * @authors Copyright (c) 2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "network/serverlink.h" #include "network/masterserver.h" #include "network/net_main.h" #include "network/net_buf.h" #include "network/net_demo.h" #include "network/net_event.h" #include "network/protocol.h" #include "client/cl_def.h" #include "dd_def.h" #include #include #include #include #include #include #include #include using namespace de; enum LinkState { None, Discovering, WaitingForInfoResponse, Joining, WaitingForJoinResponse, InGame }; DENG2_PIMPL(ServerLink) , DENG2_OBSERVES(Loop, Iteration) { shell::ServerFinder finder; ///< Finding local servers. LinkState state; bool fetching; typedef QMap Servers; Servers discovered; Servers fromMaster; Instance(Public *i) : Base(i) , state(None) , fetching(false) {} ~Instance() { Loop::get().audienceForIteration() -= this; } void notifyDiscoveryUpdate() { DENG2_FOR_PUBLIC_AUDIENCE(DiscoveryUpdate, i) i->linkDiscoveryUpdate(self); emit self.serversDiscovered(); } bool handleInfoResponse(Block const &reply) { DENG2_ASSERT(state == WaitingForInfoResponse); // Address of the server where the info was received. Address svAddress = self.address(); // Local addresses are all represented as "localhost". if(svAddress.isLocal()) svAddress.setHost(QHostAddress::LocalHost); // Close the connection; that was all the information we need. self.disconnect(); // Did we receive what we expected to receive? if(reply.size() >= 5 && reply.startsWith("Info\n")) { const char *ch; ddstring_t *line; ddstring_t* response = Str_New(); // Make a null-terminated copy of the response text. Str_PartAppend(response, (char const *) reply.data(), 0, reply.size()); serverinfo_t svInfo; // Convert the string into a serverinfo_s. line = Str_New(); ch = Str_Text(response); do { ch = Str_GetLine(line, ch); ServerInfo_FromString(&svInfo, Str_Text(line)); } while(*ch); Str_Delete(line); Str_Delete(response); LOG_NET_VERBOSE("Discovered server at ") << svAddress; // Update with the correct address. strncpy(svInfo.address, svAddress.host().toString().toLatin1(), sizeof(svInfo.address) - 1); discovered.insert(svAddress, svInfo); // Show the information in the console. LOG_NET_NOTE("%i server%s been found") << discovered.size() << (discovered.size() != 1 ? "s have" : " has"); ServerInfo_Print(NULL, 0); ServerInfo_Print(&svInfo, 0); notifyDiscoveryUpdate(); } else { LOG_NET_WARNING("Reply from %s was invalid") << svAddress; } return false; } /** * Handles the server's response to a client's join request. * @return @c false to stop processing further messages. */ bool handleJoinResponse(Block const &reply) { if(reply.size() < 5 || reply != "Enter") { LOG_NET_WARNING("Server refused connection"); LOGDEV_NET_WARNING("Received %i bytes instead of \"Enter\")") << reply.size(); self.disconnect(); return false; } // We'll switch to joined mode. // Clients are allowed to send packets to the server. state = InGame; handshakeReceived = false; allowSending = true; netGame = true; // Allow sending/receiving of packets. isServer = false; isClient = true; // Call game's NetConnect. gx.NetConnect(false); DENG2_FOR_PUBLIC_AUDIENCE(Join, i) i->networkGameJoined(); // G'day mate! The client is responsible for beginning the // handshake. Cl_SendHello(); return true; } void fetchFromMaster() { if(fetching) return; fetching = true; N_MAPost(MAC_REQUEST); N_MAPost(MAC_WAIT); Loop::get().audienceForIteration() += this; } void loopIteration() { DENG2_ASSERT(fetching); if(N_MADone()) { fetching = false; Loop::get().audienceForIteration() -= this; fromMaster.clear(); int const count = N_MasterGet(0, 0); for(int i = 0; i < count; i++) { serverinfo_t info; N_MasterGet(i, &info); fromMaster.insert(Address::parse(info.address, info.port), info); } notifyDiscoveryUpdate(); } } Servers allFound(FoundMask const &mask) const { Servers all; if(mask.testFlag(Direct)) { all = discovered; } if(mask.testFlag(MasterServer)) { // Append from master (if available). DENG2_FOR_EACH_CONST(Servers, i, fromMaster) { all.insert(i.key(), i.value()); } } if(mask.testFlag(LocalNetwork)) { // Append the ones from the server finder. foreach(Address const &sv, finder.foundServers()) { serverinfo_t info; ServerInfo_FromRecord(&info, finder.messageFromServer(sv)); // Update the address in the info, which is missing because this // information didn't come from the master. strncpy(info.address, sv.host().toString().toLatin1(), sizeof(info.address) - 1); all.insert(sv, info); } } return all; } }; ServerLink::ServerLink() : d(new Instance(this)) { connect(&d->finder, SIGNAL(updated()), this, SLOT(localServersFound())); connect(this, SIGNAL(packetsReady()), this, SLOT(handleIncomingPackets())); connect(this, SIGNAL(disconnected()), this, SLOT(linkDisconnected())); } void ServerLink::clear() { d->finder.clear(); // TODO: clear all found servers } void ServerLink::connectDomain(String const &domain, TimeDelta const &timeout) { LOG_AS("ServerLink::connectDomain"); AbstractLink::connectDomain(domain, timeout); d->state = Joining; } void ServerLink::connectHost(Address const &address) { LOG_AS("ServerLink::connectHost"); AbstractLink::connectHost(address); d->state = Joining; } void ServerLink::linkDisconnected() { LOG_AS("ServerLink"); if(d->state != None) { LOG_NET_NOTE("Connection to server was disconnected"); // Update our state and notify the game. disconnect(); } } void ServerLink::disconnect() { if(d->state == None) return; LOG_AS("ServerLink::disconnect"); if(d->state == InGame) { d->state = None; // Tell the Game that a disconnection is about to happen. if(gx.NetDisconnect) gx.NetDisconnect(true); DENG2_FOR_AUDIENCE(Leave, i) i->networkGameLeft(); LOG_NET_NOTE("Link to server %s disconnected") << address(); AbstractLink::disconnect(); Net_StopGame(); // Tell the Game that the disconnection is now complete. if(gx.NetDisconnect) gx.NetDisconnect(false); } else { d->state = None; LOG_NET_NOTE("Connection attempts aborted"); AbstractLink::disconnect(); } } void ServerLink::discover(String const &domain) { AbstractLink::connectDomain(domain, 5 /*timeout*/); d->discovered.clear(); d->state = Discovering; } void ServerLink::discoverUsingMaster() { d->fetchFromMaster(); } bool ServerLink::isDiscovering() const { return (d->state == Discovering || d->state == WaitingForInfoResponse || d->fetching); } int ServerLink::foundServerCount(FoundMask mask) const { return d->allFound(mask).size(); } QList
ServerLink::foundServers(FoundMask mask) const { return d->allFound(mask).keys(); } bool ServerLink::isFound(Address const &host, FoundMask mask) const { Address addr = host; if(!addr.port()) { // Zero means default port. addr.setPort(shell::DEFAULT_PORT); } return d->allFound(mask).contains(addr); } bool ServerLink::foundServerInfo(int index, serverinfo_t *info, FoundMask mask) const { Instance::Servers all = d->allFound(mask); QList
listed = all.keys(); if(index < 0 || index >= listed.size()) return false; memcpy(info, &all[listed[index]], sizeof(*info)); return true; } bool ServerLink::foundServerInfo(de::Address const &host, serverinfo_t *info, FoundMask mask) const { Instance::Servers all = d->allFound(mask); if(!all.contains(host)) return false; memcpy(info, &all[host], sizeof(*info)); return true; } Packet *ServerLink::interpret(Message const &msg) { return new BlockPacket(msg); } void ServerLink::initiateCommunications() { if(d->state == Discovering) { // Ask for the serverinfo. *this << ByteRefArray("Info?", 5); d->state = WaitingForInfoResponse; } else if(d->state == Joining) { Demo_StopPlayback(); BusyMode_FreezeGameForBusyMode(); // Tell the Game that a connection is about to happen. // The counterpart (false) call will occur after joining is successfully done. gx.NetConnect(true); // Connect by issuing: "Join (myname)" String pName = (playerName? playerName : ""); if(pName.isEmpty()) { pName = "Player"; } String req = String("Join %1 %2").arg(SV_VERSION, 4, 16, QChar('0')).arg(pName); *this << req.toUtf8(); d->state = WaitingForJoinResponse; } else { DENG2_ASSERT(false); } } void ServerLink::localServersFound() { d->notifyDiscoveryUpdate(); } void ServerLink::handleIncomingPackets() { if(d->state == Discovering || d->state == Joining) return; LOG_AS("ServerLink"); forever { // Only BlockPackets received (see interpret()). QScopedPointer packet(static_cast(nextPacket())); if(packet.isNull()) break; switch(d->state) { case WaitingForInfoResponse: if(!d->handleInfoResponse(*packet)) return; break; case WaitingForJoinResponse: if(!d->handleJoinResponse(*packet)) return; break; case InGame: { /// @todo The incoming packets should go be handled immediately. // Post the data into the queue. netmessage_t *msg = (netmessage_t *) M_Calloc(sizeof(netmessage_t)); msg->sender = 0; // the server msg->data = new byte[packet->size()]; memcpy(msg->data, packet->data(), packet->size()); msg->size = packet->size(); msg->handle = msg->data; // needs delete[] // The message queue will handle the message from now on. N_PostMessage(msg); break; } default: // Ignore any packets left. break; } } } doomsday-stable-1.15.7/doomsday/client/src/network/net_ping.cpp0000664000175000017500000001154712641367670024070 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * net_ping.c: Pinging Clients and the Server * * Warning: This is not a very accurate ping. */ // HEADER FILES ------------------------------------------------------------ #include "de_platform.h" #include "de_console.h" #include "de_system.h" #include "de_network.h" #include "world/p_players.h" // MACROS ------------------------------------------------------------------ // TYPES ------------------------------------------------------------------- // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- // PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- // EXTERNAL DATA DECLARATIONS ---------------------------------------------- // PUBLIC DATA DEFINITIONS ------------------------------------------------- // PRIVATE DATA DEFINITIONS ------------------------------------------------ // CODE -------------------------------------------------------------------- void Net_ShowPingSummary(int player) { client_t *cl = clients + player; pinger_t *ping = &cl->ping; float avgTime = 0; int i, goodCount = 0; if(player < 0 && ping->total > 0) return; for(i = 0; i < ping->total; ++i) { if(ping->times[i] < 0) continue; goodCount++; avgTime += ping->times[i]; } avgTime /= goodCount; LOG_NET_NOTE("Player %i (%s): average ping %.0f ms") << player << cl->name << (avgTime * 1000); } void Net_SendPing(int player, int count) { client_t *cl = clients + player; // Valid destination? if((player == consolePlayer) || (isClient && player)) return; if(count) { // We can't start a new ping run until the old one is done. if(cl->ping.sent) return; // Start a new ping session. if(count > MAX_PINGS) count = MAX_PINGS; cl->ping.current = 0; cl->ping.total = count; } else { // Continue or finish the current pinger. if(++cl->ping.current >= cl->ping.total) { // We're done. cl->ping.sent = 0; // Print a summary (average ping, loss %). Net_ShowPingSummary(netBuffer.player); return; } } // Send a new ping. Msg_Begin(PKT_PING); cl->ping.sent = Timer_RealMilliseconds(); Writer_WriteUInt32(msgWriter, cl->ping.sent); Msg_End(); // Update the length of the message. netBuffer.player = player; N_SendPacket(10000); } // Called when a ping packet comes in. void Net_PingResponse(void) { client_t* cl = &clients[netBuffer.player]; int time = Reader_ReadUInt32(msgReader); // Is this a response to our ping? if(time == cl->ping.sent) { // Record the time and send the next ping. cl->ping.times[cl->ping.current] = (Timer_RealMilliseconds() - time) / 1000.0f; // Send the next ping. Net_SendPing(netBuffer.player, 0); } else { // Not ours, just respond. Net_SendBuffer(netBuffer.player, 10000); } } D_CMD(Ping) { DENG2_UNUSED(src); int dest, count = 4; if(!netGame) { LOG_SCR_ERROR("Ping is only for netgames"); return true; } if(isServer && argc == 1) { LOG_SCR_NOTE("Usage: %s (plrnum) (count)") << argv[0]; LOG_SCR_MSG("(count) is optional. 4 pings are sent by default."); return true; } if(isServer) { dest = atoi(argv[1]); if(argc >= 3) count = atoi(argv[2]); } else { dest = 0; if(argc >= 2) count = atoi(argv[1]); } // Check that the given parameters are valid. if(count <= 0 || count > MAX_PINGS || dest < 0 || dest >= DDMAXPLAYERS || dest == consolePlayer || (dest && !ddPlayers[dest].shared.inGame)) return false; Net_SendPing(dest, count); return true; } doomsday-stable-1.15.7/doomsday/client/src/network/protocol.cpp0000664000175000017500000000441012641367670024115 0ustar jaakkojaakko/** @file protocol.cpp Implementation of the network protocol. * @ingroup network * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #if 0 #include #include #include #include "de_platform.h" #include "de_console.h" #include "de_misc.h" #include "de_network.h" boolean Protocol_Receive(nodeid_t from) { int size = 0; int sock = N_GetNodeSocket(from); byte* packet = 0; packet = LegacyNetwork_Receive(sock, &size); if(!packet) { // Failed to receive anything. return false; } // Post the received message. { netmessage_t *msg = (netmessage_t *) M_Calloc(sizeof(netmessage_t)); msg->sender = from; msg->data = packet; msg->size = size; msg->handle = packet; // needs to be freed // The message queue will handle the message from now on. N_PostMessage(msg); } return true; } void Protocol_FreeBuffer(void *handle) { if(handle) { LegacyNetwork_FreeBuffer((byte *) handle); } } void Protocol_Send(void *data, size_t size, nodeid_t destination) { if(size == 0 || !N_GetNodeSocket(destination) || !N_HasNodeJoined(destination)) return; if(size > DDMAXINT) { App_Error("Protocol_Send: Trying to send an oversized data buffer.\n" " Attempted size is %u bytes.\n", (unsigned long) size); } #ifdef _DEBUG Monitor_Add((uint8_t const *) data, size); #endif LegacyNetwork_Send(N_GetNodeSocket(destination), data, size); } #endif 0 doomsday-stable-1.15.7/doomsday/client/src/alertmask.cpp0000664000175000017500000000447412641367670022560 0ustar jaakkojaakko/** @file alertmask.cpp * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "alertmask.h" #include "clientapp.h" using namespace de; DENG2_PIMPL_NOREF(AlertMask) , DENG2_OBSERVES(Variable, Change) { duint32 mask[LogEntry::HighestLogLevel + 1]; Instance() { zap(mask); // By default, alerts are enabled for Warnings and above. mask[LogEntry::Warning] = mask[LogEntry::Error] = mask[LogEntry::Critical] = LogEntry::AllDomains; } void variableValueChanged(Variable &, Value const &) { updateMask(); } void updateMask() { zap(mask); Config const &cfg = App::config(); for(int bit = LogEntry::FirstDomainBit; bit <= LogEntry::LastDomainBit; ++bit) { int const alertLevel = cfg.geti(String("alert.") + LogFilter::domainRecordName(LogEntry::Context(1 << bit))); for(int i = LogEntry::LowestLogLevel; i <= LogEntry::HighestLogLevel; ++i) { if(alertLevel <= i) { mask[i] |= (1 << bit); } } } } }; AlertMask::AlertMask() : d(new Instance) {} void AlertMask::init() { foreach(Variable const *var, App::config().names().subrecord("alert").members()) { var->audienceForChange() += d; } d->updateMask(); } bool AlertMask::shouldRaiseAlert(duint32 entryMetadata) const { int const level = entryMetadata & LogEntry::LevelMask; return ((entryMetadata & LogEntry::DomainMask) & d->mask[level]) != 0; } doomsday-stable-1.15.7/doomsday/client/src/library.cpp0000664000175000017500000001536312641367670022240 0ustar jaakkojaakko/** @file library.cpp Dynamic libraries. * @ingroup base * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include #include "de_base.h" #include "Sector" #include "m_misc.h" #include "api_def.h" #include "api_console.h" #include "api_filesys.h" #include "api_internaldata.h" #include "api_material.h" #include "api_materialarchive.h" #include "api_map.h" #include "api_mapedit.h" #include "api_resource.h" #include "api_sound.h" #include "api_fontrender.h" #include "api_svg.h" #ifdef __CLIENT__ # include "api_client.h" # include "api_render.h" #endif #ifdef __SERVER__ # include "api_server.h" #endif struct library_s { // typedef Library Str* path; ///< de::FS path of the library (e.g., "/bin/doom.dll"). de::LibraryFile* file; ///< File where the plugin has been loaded from. bool isGamePlugin; ///< Is this a game plugin? (only one should be in use at a time) std::string typeId; ///< Library type ID e.g., "deng-plugin/game". library_s() : path(0), file(0), isGamePlugin(false) {} }; static ddstring_t* lastError; typedef QList LoadedLibs; static LoadedLibs loadedLibs; void Library_Init(void) { lastError = Str_NewStd(); } void Library_Shutdown(void) { Str_Delete(lastError); lastError = 0; /// @todo Unload all remaining libraries? } void Library_ReleaseGames(void) { #ifdef UNIX LOG_AS("Library_ReleaseGames"); foreach(Library* lib, loadedLibs) { if(lib->isGamePlugin) { LOGDEV_RES_VERBOSE("Closing '%s'") << Str_Text(lib->path); // Close the Library. DENG_ASSERT(lib->file); lib->file->clear(); } } #endif } #ifdef UNIX static void reopenLibraryIfNeeded(Library* lib) { DENG_ASSERT(lib); if(!lib->file->loaded()) { LOGDEV_RES_XVERBOSE("Re-opening '%s'") << Str_Text(lib->path); // Make sure the Library gets opened again now. lib->file->library(); DENG_ASSERT(lib->file->loaded()); Library_PublishAPIs(lib); } } #endif Library* Library_New(const char* filePath) { try { Str_Clear(lastError); de::LibraryFile &libFile = de::App::rootFolder().locate(filePath); if(libFile.library().type() == de::Library::DEFAULT_TYPE) { // This is just a shared library, not a plugin. // We don't have to keep it loaded. libFile.clear(); Str_Set(lastError, "not a Doomsday plugin"); return 0; } // Create the Library instance. Library* lib = new Library; lib->file = &libFile; lib->path = Str_Set(Str_NewStd(), filePath); lib->typeId = libFile.library().type().toStdString(); loadedLibs.append(lib); // Symbols from game plugins conflict with each other, so we have to // keep track of games. if(libFile.library().type() == "deng-plugin/game") { lib->isGamePlugin = true; } Library_PublishAPIs(lib); return lib; } catch(const de::Error& er) { Str_Set(lastError, er.asText().toLatin1().constData()); LOG_RES_WARNING("Library_New: Error opening \"%s\": ") << filePath << er.asText(); return 0; } } void Library_Delete(Library *lib) { DENG_ASSERT(lib); // Unload the library from memory. lib->file->clear(); Str_Delete(lib->path); loadedLibs.removeOne(lib); delete lib; } const char* Library_Type(const Library* lib) { DENG_ASSERT(lib); return &lib->typeId[0]; } de::LibraryFile& Library_File(Library* lib) { DENG_ASSERT(lib); return *lib->file; } void Library_PublishAPIs(Library *lib) { de::Library &library = lib->file->library(); if(library.hasSymbol("deng_API")) { de::Library::deng_API setAPI = library.DENG2_SYMBOL(deng_API); #define PUBLISH(X) setAPI(X.api.id, &X) PUBLISH(_api_Base); PUBLISH(_api_Busy); PUBLISH(_api_Con); PUBLISH(_api_Def); PUBLISH(_api_F); PUBLISH(_api_Infine); PUBLISH(_api_InternalData); PUBLISH(_api_Map); PUBLISH(_api_MPE); PUBLISH(_api_Material); PUBLISH(_api_MaterialArchive); PUBLISH(_api_Player); PUBLISH(_api_Plug); PUBLISH(_api_R); PUBLISH(_api_S); PUBLISH(_api_Thinker); PUBLISH(_api_Uri); #ifdef __CLIENT__ // Client-only APIs. PUBLISH(_api_B); PUBLISH(_api_Client); PUBLISH(_api_FR); PUBLISH(_api_GL); PUBLISH(_api_Rend); PUBLISH(_api_Svg); #endif #ifdef __SERVER__ // Server-only APIs. PUBLISH(_api_Server); #endif } } void* Library_Symbol(Library* lib, const char* symbolName) { try { LOG_AS("Library_Symbol"); DENG_ASSERT(lib); #ifdef UNIX reopenLibraryIfNeeded(lib); #endif return lib->file->library().address(symbolName); } catch(de::Library::SymbolMissingError const &er) { Str_Set(lastError, er.asText().toLatin1().constData()); return 0; } } const char* Library_LastError(void) { return Str_Text(lastError); } int Library_IterateAvailableLibraries(int (*func)(void *, const char *, const char *, void *), void *data) { using de::FS; using de::App; using de::LibraryFile; using de::NativeFile; FS::Index const &libs = App::fileSystem().indexFor(DENG2_TYPE_NAME(LibraryFile)); DENG2_FOR_EACH_CONST(FS::Index, i, libs) { LibraryFile &lib = i->second->as(); NativeFile const *src = lib.source()->maybeAs(); if(src) { int result = func(&lib, src->name().toUtf8().constData(), lib.path().toUtf8().constData(), data); if(result) return result; } } return 0; } doomsday-stable-1.15.7/doomsday/client/src/resource/0000775000175000017500000000000012641367670021707 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/resource/hq2x.cpp0000664000175000017500000017224512641367670023310 0ustar jaakkojaakko/** @file hq2x.cpp High-Quality 2x Graphics Resizing. * @ingroup resource * * @authors Copyright © 2003 Maxim Stepin * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include #include "de_platform.h" #include "dd_main.h" #include "dd_types.h" #include "dd_share.h" #include "de_console.h" #include "resource/image.h" #include "resource/hq2x.h" /* * RGB color space. */ #define BGR888_PACK(b, g, r) ( ((uint32_t)(b) << 16) | ((uint32_t)(g) << 8) | (uint32_t)(r) ) #define BGR888_COMP(n, c) ( ((c) >> ((n) << 3)) & 0xFF ) #define BGR888_Bmask ((int)0xFF0000) #define BGR888_Gmask ((int)0x00FF00) #define BGR888_Rmask ((int)0x0000FF) #define BGR888toBGR565(c) (((c & 0xF8) >> 3) | ((c & 0xFC00) >> 5) | ((c & 0xF80000) >> 8)) #define ABGR8888_PACK(a, b, g, r) ( ((uint32_t)(a) << 24) | BGR888_PACK((b), (g), (r)) ) #define ABGR8888_COMP BGR888_COMP #define ABGR8888_Amask ((int)0xFF000000) #define ABGR8888_Bmask BGR888_Bmask #define ABGR8888_Gmask BGR888_Gmask #define ABGR8888_Rmask BGR888_Rmask #define ABGR8888_RGBmask ((int)0x00FFFFFF) // YUV conversion. #define BGR888toYUV888(v) (lutBGR888toYUV888[BGR888toBGR565(v)]) #define ABGR8888toYUV888(v) (BGR888toYUV888(v & ABGR8888_RGBmask)) #define ABGR8888toAYUV8888(v) (ABGR8888toYUV888(v) | (((v) & ABGR8888_Amask) << 24)) #define RGB888_PACK(r, g, b) ( ((uint32_t)(r) << 16) | ((uint32_t)(g) << 8) | (uint32_t)(b) ) #define RGB888_COMP(n, c) ( ((c) >> ((n) << 3)) & 0xFF ) #define RGB888_Rmask ((int)0xFF0000) #define RGB888_Gmask ((int)0x00FF00) #define RGB888_Bmask ((int)0x0000FF) #define RGB888toRGB565(c) (((c & 0xF80000) >> 8) | ((c & 0xFC00) >> 5) | ((c & 0xF8) >> 3)) #define ARGB8888_PACK(a, r, g, b) ( ((uint32_t)(a) << 24) | RGB888_PACK((r), (g), (b)) ) #define ARGB8888_COMP RGB888_COMP #define ARGB8888_Amask ((int)0xFF000000) #define ARGB8888_Rmask RGB888_Rmask #define ARGB8888_Gmask RGB888_Gmask #define ARGB8888_Bmask RGB888_Bmask #define ARGB8888_RGBmask ((int)0x00FFFFFF) /* // YUV conversion. #define RGB888toYUV888(v) (BGR888toYUV888(RGB888toBGR888(v))) #define ARGB8888toYUV888(v) (RGB888toYUV888((v) & ARGB8888_RGBmask]) #define ARGB8888toAYUV8888(v) (ARGB8888toYUV888(v) | (((v) & ARGB8888_Amask) << 24)) */ #define BGR565_PACK(b, g, r) ( ((uint16_t)(b) << 11) | ((uint16_t)(g) << 5) | (uint16_t)(r) ) #define BGR565_Bmask ((int)0xF800) #define BGR565_Gmask ((int)0x7E0) #define BGR565_Rmask ((int)0x1F) /* * YUV color space. */ #define YUV888_PACK(y, u, v) ( ((uint32_t)(y) << 16) | ((uint32_t)(u) << 8) | (uint32_t)(v) ) #define YUV888_COMP(n, c) ( ((c) >> ((n) << 3)) & 0xFF ) #define YUV888_Ymask ((int)0xFF0000) #define YUV888_Umask ((int)0x00FF00) #define YUV888_Vmask ((int)0x0000FF) #define AYUV8888_PACK(y, u, v, a) ( ((uint32_t)(a) << 24) | YUV888_PACK((y), (u), (v)) ) #define AYUV8888_COMP YUV888_COMP #define AYUV8888_Amask ((int)0xFF000000) #define AYUV8888_Ymask YUV888_Ymask #define AYUV8888_Umask YUV888_Umask #define AYUV8888_Vmask YUV888_Vmask #define AYUV8888_YUVmask ((int)0x00FFFFFF) #define trY (48) #define trU (7) #define trV (6) #define PIXEL00_0 Transl(pOut, w[5]); #define PIXEL00_10 Interp1(pOut, w[5], w[1]); #define PIXEL00_11 Interp1(pOut, w[5], w[4]); #define PIXEL00_12 Interp1(pOut, w[5], w[2]); #define PIXEL00_20 Interp2(pOut, w[5], w[4], w[2]); #define PIXEL00_21 Interp2(pOut, w[5], w[1], w[2]); #define PIXEL00_22 Interp2(pOut, w[5], w[1], w[4]); #define PIXEL00_60 Interp6(pOut, w[5], w[2], w[4]); #define PIXEL00_61 Interp6(pOut, w[5], w[4], w[2]); #define PIXEL00_70 Interp7(pOut, w[5], w[4], w[2]); #define PIXEL00_90 Interp9(pOut, w[5], w[4], w[2]); #define PIXEL00_100 Interp10(pOut, w[5], w[4], w[2]); #define PIXEL01_0 Transl(pOut+4, w[5]); #define PIXEL01_10 Interp1(pOut+4, w[5], w[3]); #define PIXEL01_11 Interp1(pOut+4, w[5], w[2]); #define PIXEL01_12 Interp1(pOut+4, w[5], w[6]); #define PIXEL01_20 Interp2(pOut+4, w[5], w[2], w[6]); #define PIXEL01_21 Interp2(pOut+4, w[5], w[3], w[6]); #define PIXEL01_22 Interp2(pOut+4, w[5], w[3], w[2]); #define PIXEL01_60 Interp6(pOut+4, w[5], w[6], w[2]); #define PIXEL01_61 Interp6(pOut+4, w[5], w[2], w[6]); #define PIXEL01_70 Interp7(pOut+4, w[5], w[2], w[6]); #define PIXEL01_90 Interp9(pOut+4, w[5], w[2], w[6]); #define PIXEL01_100 Interp10(pOut+4, w[5], w[2], w[6]); #define PIXEL10_0 Transl(pOut+BpL, w[5]); #define PIXEL10_10 Interp1(pOut+BpL, w[5], w[7]); #define PIXEL10_11 Interp1(pOut+BpL, w[5], w[8]); #define PIXEL10_12 Interp1(pOut+BpL, w[5], w[4]); #define PIXEL10_20 Interp2(pOut+BpL, w[5], w[8], w[4]); #define PIXEL10_21 Interp2(pOut+BpL, w[5], w[7], w[4]); #define PIXEL10_22 Interp2(pOut+BpL, w[5], w[7], w[8]); #define PIXEL10_60 Interp6(pOut+BpL, w[5], w[4], w[8]); #define PIXEL10_61 Interp6(pOut+BpL, w[5], w[8], w[4]); #define PIXEL10_70 Interp7(pOut+BpL, w[5], w[8], w[4]); #define PIXEL10_90 Interp9(pOut+BpL, w[5], w[8], w[4]); #define PIXEL10_100 Interp10(pOut+BpL, w[5], w[8], w[4]); #define PIXEL11_0 Transl(pOut+BpL+4, w[5]); #define PIXEL11_10 Interp1(pOut+BpL+4, w[5], w[9]); #define PIXEL11_11 Interp1(pOut+BpL+4, w[5], w[6]); #define PIXEL11_12 Interp1(pOut+BpL+4, w[5], w[8]); #define PIXEL11_20 Interp2(pOut+BpL+4, w[5], w[6], w[8]); #define PIXEL11_21 Interp2(pOut+BpL+4, w[5], w[9], w[8]); #define PIXEL11_22 Interp2(pOut+BpL+4, w[5], w[9], w[6]); #define PIXEL11_60 Interp6(pOut+BpL+4, w[5], w[8], w[6]); #define PIXEL11_61 Interp6(pOut+BpL+4, w[5], w[6], w[8]); #define PIXEL11_70 Interp7(pOut+BpL+4, w[5], w[6], w[8]); #define PIXEL11_90 Interp9(pOut+BpL+4, w[5], w[6], w[8]); #define PIXEL11_100 Interp10(pOut+BpL+4, w[5], w[6], w[8]); static uint32_t lutBGR888toYUV888[32*64*32]; static uint32_t YUV1, YUV2; void LerpColor(uint8_t* pc, uint32_t c1, uint32_t c2, uint32_t c3, uint32_t f1, uint32_t f2, uint32_t f3) { uint32_t out[4] = { 0, 0, 0, 0 }, total = f1 + f2 + f3; if(0 != f1) { out[0] += f1 * ABGR8888_COMP(0, c1); out[1] += f1 * ABGR8888_COMP(1, c1); out[2] += f1 * ABGR8888_COMP(2, c1); out[3] += f1 * ABGR8888_COMP(3, c1); } if(0 != f2) { out[0] += f2 * ABGR8888_COMP(0, c2); out[1] += f2 * ABGR8888_COMP(1, c2); out[2] += f2 * ABGR8888_COMP(2, c2); out[3] += f2 * ABGR8888_COMP(3, c2); } if(0 != f3) { out[0] += f3 * ABGR8888_COMP(0, c3); out[1] += f3 * ABGR8888_COMP(1, c3); out[2] += f3 * ABGR8888_COMP(2, c3); out[3] += f3 * ABGR8888_COMP(3, c3); } if(0 != total) { out[0] /= total; out[1] /= total; out[2] /= total; out[3] /= total; } *((uint32_t*)pc) = ABGR8888_PACK(out[3], out[2], out[1], out[0]); } static __inline int Diff(uint32_t c1, uint32_t c2) { YUV1 = ABGR8888toYUV888(c1); YUV2 = ABGR8888toYUV888(c2); return ( ((ABGR8888_COMP(3, c1) != 0) != ((ABGR8888_COMP(3, c2) != 0))) || (abs(int(YUV1 & YUV888_Ymask) - int(YUV2 & YUV888_Ymask)) > ((trY & (int)0xFF) << 16)) || (abs(int(YUV1 & YUV888_Umask) - int(YUV2 & YUV888_Umask)) > ((trU & (int)0xFF) << 8)) || (abs(int(YUV1 & YUV888_Vmask) - int(YUV2 & YUV888_Vmask)) > ((trV & (int)0xFF)) )); } static __inline void Transl(uint8_t* pc, uint32_t c) { pc[0] = ABGR8888_COMP(0, c); pc[1] = ABGR8888_COMP(1, c); pc[2] = ABGR8888_COMP(2, c); pc[3] = ABGR8888_COMP(3, c); } static __inline void Interp1(uint8_t* pc, uint32_t c1, uint32_t c2) { if(c1 == c2) { Transl(pc, c1); return; } LerpColor(pc, c1, c2, 0, 3, 1, 0); } static __inline void Interp2(uint8_t* pc, uint32_t c1, uint32_t c2, uint32_t c3) { LerpColor(pc, c1, c2, c3, 2, 1, 1); } static __inline void Interp6(uint8_t* pc, uint32_t c1, uint32_t c2, uint32_t c3) { LerpColor(pc, c1, c2, c3, 5, 2, 1); } static __inline void Interp7(uint8_t* pc, uint32_t c1, uint32_t c2, uint32_t c3) { LerpColor(pc, c1, c2, c3, 6, 1, 1); } static __inline void Interp9(uint8_t* pc, uint32_t c1, uint32_t c2, uint32_t c3) { LerpColor(pc, c1, c2, c3, 2, 3, 3); } static __inline void Interp10(uint8_t* pc, uint32_t c1, uint32_t c2, uint32_t c3) { LerpColor(pc, c1, c2, c3, 14, 1, 1); } void GL_InitSmartFilterHQ2x(void) { // Initalize RGB to YUV lookup table. uint32_t r, g, b, y, u, v; int i, j, k; for(i = 0; i < 32; ++i) for(j = 0; j < 64; ++j) for(k = 0; k < 32; ++k) { r = i << 3; g = j << 2; b = k << 3; y = (uint32_t)( 0.299*r + 0.587*g + 0.114*b); u = (uint32_t)(-0.169*r - 0.331*g + 0.5 *b) + 128; v = (uint32_t)( 0.5 *r - 0.419*g - 0.081*b) + 128; lutBGR888toYUV888[BGR565_PACK(k, j, i)] = YUV888_PACK(y, u, v); } } uint8_t* GL_SmartFilterHQ2x(const uint8_t* src, int width, int height, int flags) { #define BPP (4) // Bytes Per Pixel. #define OFFSET(x, y) (BPP*(y)*width + BPP*(x)) assert(src); { dd_bool wrapH = (flags & ICF_UPSCALE_SAMPLE_WRAPH) != 0; dd_bool wrapV = (flags & ICF_UPSCALE_SAMPLE_WRAPV) != 0; int pattern, flag, BpL, xA, xB, yA, yB; uint8_t* pOut, *dst; uint32_t w[10]; if(width <= 0 || height <= 0) return 0; // +----+----+----+ // | w1 | w2 | w3 | // +----+----+----+ // | w4 | w5 | w6 | // +----+----+----+ // | w7 | w8 | w9 | // +----+----+----+ if(0 == (dst = (uint8_t *) M_Malloc(BPP * 2 * width * height * 2))) App_Error("GL_SmartFilterHQ2x: Failed on allocation of %lu bytes for " "output buffer.", (unsigned long) (BPP * 2 * width * height * 2)); pOut = dst; BpL = BPP * 2 * width; // (Out) Bytes per Line. { int y; for(y = 0; y < height; ++y) { { int x; for(x = 0; x < width; ++x) { w[5] = ULONG( *( (uint32_t*)(src + OFFSET(x, y)) ) ); // Horizontal neighbors. if(wrapH) { w[4] = ULONG( *( (uint32_t*)(src + OFFSET(x == 0? width-1 : x-1, y)) ) ); w[6] = ULONG( *( (uint32_t*)(src + OFFSET(x == width-1? 0 : x+1, y)) ) ); } else { if(x != 0) w[4] = ULONG( *( (uint32_t*)(src + OFFSET(x-1, y)) ) ); else w[4] = w[5]; if(x != width-1) w[6] = ULONG( *( (uint32_t*)(src + OFFSET(x+1, y)) ) ); else w[6] = w[5]; } // Vertical neighbors. if(wrapV) { w[2] = ULONG( *( (uint32_t*)(src + OFFSET(x, y == 0? height-1 : y-1)) ) ); w[8] = ULONG( *( (uint32_t*)(src + OFFSET(x, y == height-1? 0 : y+1)) ) ); } else { if(y != 0) w[2] = ULONG( *( (uint32_t*)(src + OFFSET(x, y-1)) ) ); else w[2] = w[5]; if(y != height-1) w[8] = ULONG( *( (uint32_t*)(src + OFFSET(x, y+1)) ) ); else w[8] = w[5]; } // Corners. xA = x == 0? ( wrapH? width-1 : 0) : x-1; xB = x == width-1? (!wrapH? width-1 : 0) : x+1; yA = y == 0? ( wrapV? height-1 : 0) : y-1; yB = y == height-1? (!wrapV? height-1 : 0) : y+1; w[1] = ULONG( *( (uint32_t*)(src + OFFSET(xA, yA)) ) ); w[7] = ULONG( *( (uint32_t*)(src + OFFSET(xA, yB)) ) ); w[3] = ULONG( *( (uint32_t*)(src + OFFSET(xB, yA)) ) ); w[9] = ULONG( *( (uint32_t*)(src + OFFSET(xB, yB)) ) ); pattern = 0; flag = 1; YUV1 = ABGR8888toYUV888(w[5]); { int k; for(k = 1; k <= 9; ++k) { if(k == 5) continue; if(w[k] != w[5]) { YUV2 = ABGR8888toYUV888(w[k]); if(((ABGR8888_COMP(3, w[5]) != 0) != (ABGR8888_COMP(3, w[k]) != 0)) || (abs(int(YUV1 & YUV888_Ymask) - int(YUV2 & YUV888_Ymask)) > ((trY & (int)0xFF) << 16)) || (abs(int(YUV1 & YUV888_Umask) - int(YUV2 & YUV888_Umask)) > ((trU & (int)0xFF) << 8)) || (abs(int(YUV1 & YUV888_Vmask) - int(YUV2 & YUV888_Vmask)) > ((trV & (int)0xFF) )) ) pattern |= flag; } flag <<= 1; }} switch(pattern) { case 0: case 1: case 4: case 32: case 128: case 5: case 132: case 160: case 33: case 129: case 36: case 133: case 164: case 161: case 37: case 165: { PIXEL00_20 PIXEL01_20 PIXEL10_20 PIXEL11_20 break; } case 2: case 34: case 130: case 162: { PIXEL00_22 PIXEL01_21 PIXEL10_20 PIXEL11_20 break; } case 16: case 17: case 48: case 49: { PIXEL00_20 PIXEL01_22 PIXEL10_20 PIXEL11_21 break; } case 64: case 65: case 68: case 69: { PIXEL00_20 PIXEL01_20 PIXEL10_21 PIXEL11_22 break; } case 8: case 12: case 136: case 140: { PIXEL00_21 PIXEL01_20 PIXEL10_22 PIXEL11_20 break; } case 3: case 35: case 131: case 163: { PIXEL00_11 PIXEL01_21 PIXEL10_20 PIXEL11_20 break; } case 6: case 38: case 134: case 166: { PIXEL00_22 PIXEL01_12 PIXEL10_20 PIXEL11_20 break; } case 20: case 21: case 52: case 53: { PIXEL00_20 PIXEL01_11 PIXEL10_20 PIXEL11_21 break; } case 144: case 145: case 176: case 177: { PIXEL00_20 PIXEL01_22 PIXEL10_20 PIXEL11_12 break; } case 192: case 193: case 196: case 197: { PIXEL00_20 PIXEL01_20 PIXEL10_21 PIXEL11_11 break; } case 96: case 97: case 100: case 101: { PIXEL00_20 PIXEL01_20 PIXEL10_12 PIXEL11_22 break; } case 40: case 44: case 168: case 172: { PIXEL00_21 PIXEL01_20 PIXEL10_11 PIXEL11_20 break; } case 9: case 13: case 137: case 141: { PIXEL00_12 PIXEL01_20 PIXEL10_22 PIXEL11_20 break; } case 18: case 50: { PIXEL00_22 if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_20} PIXEL10_20 PIXEL11_21 break; } case 80: case 81: { PIXEL00_20 PIXEL01_22 PIXEL10_21 if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_20} break; } case 72: case 76: { PIXEL00_21 PIXEL01_20 if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_20} PIXEL11_22 break; } case 10: case 138: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_20} PIXEL01_21 PIXEL10_22 PIXEL11_20 break; } case 66: { PIXEL00_22 PIXEL01_21 PIXEL10_21 PIXEL11_22 break; } case 24: { PIXEL00_21 PIXEL01_22 PIXEL10_22 PIXEL11_21 break; } case 7: case 39: case 135: { PIXEL00_11 PIXEL01_12 PIXEL10_20 PIXEL11_20 break; } case 148: case 149: case 180: { PIXEL00_20 PIXEL01_11 PIXEL10_20 PIXEL11_12 break; } case 224: case 228: case 225: { PIXEL00_20 PIXEL01_20 PIXEL10_12 PIXEL11_11 break; } case 41: case 169: case 45: { PIXEL00_12 PIXEL01_20 PIXEL10_11 PIXEL11_20 break; } case 22: case 54: { PIXEL00_22 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} PIXEL10_20 PIXEL11_21 break; } case 208: case 209: { PIXEL00_20 PIXEL01_22 PIXEL10_21 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 104: case 108: { PIXEL00_21 PIXEL01_20 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} PIXEL11_22 break; } case 11: case 139: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} PIXEL01_21 PIXEL10_22 PIXEL11_20 break; } case 19: case 51: { if(Diff(w[2], w[6])) { PIXEL00_11 PIXEL01_10} else { PIXEL00_60 PIXEL01_90} PIXEL10_20 PIXEL11_21 break; } case 146: case 178: { PIXEL00_22 if(Diff(w[2], w[6])) { PIXEL01_10 PIXEL11_12} else { PIXEL01_90 PIXEL11_61} PIXEL10_20 break; } case 84: case 85: { PIXEL00_20 if(Diff(w[6], w[8])) { PIXEL01_11 PIXEL11_10} else { PIXEL01_60 PIXEL11_90} PIXEL10_21 break; } case 112: case 113: { PIXEL00_20 PIXEL01_22 if(Diff(w[6], w[8])) { PIXEL10_12 PIXEL11_10} else { PIXEL10_61 PIXEL11_90} break; } case 200: case 204: { PIXEL00_21 PIXEL01_20 if(Diff(w[8], w[4])) { PIXEL10_10 PIXEL11_11} else { PIXEL10_90 PIXEL11_60} break; } case 73: case 77: { if(Diff(w[8], w[4])) { PIXEL00_12 PIXEL10_10} else { PIXEL00_61 PIXEL10_90} PIXEL01_20 PIXEL11_22 break; } case 42: case 170: { if(Diff(w[4], w[2])) { PIXEL00_10 PIXEL10_11} else { PIXEL00_90 PIXEL10_60} PIXEL01_21 PIXEL11_20 break; } case 14: case 142: { if(Diff(w[4], w[2])) { PIXEL00_10 PIXEL01_12} else { PIXEL00_90 PIXEL01_61} PIXEL10_22 PIXEL11_20 break; } case 67: { PIXEL00_11 PIXEL01_21 PIXEL10_21 PIXEL11_22 break; } case 70: { PIXEL00_22 PIXEL01_12 PIXEL10_21 PIXEL11_22 break; } case 28: { PIXEL00_21 PIXEL01_11 PIXEL10_22 PIXEL11_21 break; } case 152: { PIXEL00_21 PIXEL01_22 PIXEL10_22 PIXEL11_12 break; } case 194: { PIXEL00_22 PIXEL01_21 PIXEL10_21 PIXEL11_11 break; } case 98: { PIXEL00_22 PIXEL01_21 PIXEL10_12 PIXEL11_22 break; } case 56: { PIXEL00_21 PIXEL01_22 PIXEL10_11 PIXEL11_21 break; } case 25: { PIXEL00_12 PIXEL01_22 PIXEL10_22 PIXEL11_21 break; } case 26: case 31: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} PIXEL10_22 PIXEL11_21 break; } case 82: case 214: { PIXEL00_22 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} PIXEL10_21 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 88: case 248: { PIXEL00_21 PIXEL01_22 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 74: case 107: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} PIXEL01_21 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} PIXEL11_22 break; } case 27: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} PIXEL01_10 PIXEL10_22 PIXEL11_21 break; } case 86: { PIXEL00_22 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} PIXEL10_21 PIXEL11_10 break; } case 216: { PIXEL00_21 PIXEL01_22 PIXEL10_10 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 106: { PIXEL00_10 PIXEL01_21 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} PIXEL11_22 break; } case 30: { PIXEL00_10 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} PIXEL10_22 PIXEL11_21 break; } case 210: { PIXEL00_22 PIXEL01_10 PIXEL10_21 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 120: { PIXEL00_21 PIXEL01_22 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} PIXEL11_10 break; } case 75: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} PIXEL01_21 PIXEL10_10 PIXEL11_22 break; } case 29: { PIXEL00_12 PIXEL01_11 PIXEL10_22 PIXEL11_21 break; } case 198: { PIXEL00_22 PIXEL01_12 PIXEL10_21 PIXEL11_11 break; } case 184: { PIXEL00_21 PIXEL01_22 PIXEL10_11 PIXEL11_12 break; } case 99: { PIXEL00_11 PIXEL01_21 PIXEL10_12 PIXEL11_22 break; } case 57: { PIXEL00_12 PIXEL01_22 PIXEL10_11 PIXEL11_21 break; } case 71: { PIXEL00_11 PIXEL01_12 PIXEL10_21 PIXEL11_22 break; } case 156: { PIXEL00_21 PIXEL01_11 PIXEL10_22 PIXEL11_12 break; } case 226: { PIXEL00_22 PIXEL01_21 PIXEL10_12 PIXEL11_11 break; } case 60: { PIXEL00_21 PIXEL01_11 PIXEL10_11 PIXEL11_21 break; } case 195: { PIXEL00_11 PIXEL01_21 PIXEL10_21 PIXEL11_11 break; } case 102: { PIXEL00_22 PIXEL01_12 PIXEL10_12 PIXEL11_22 break; } case 153: { PIXEL00_12 PIXEL01_22 PIXEL10_22 PIXEL11_12 break; } case 58: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_70} if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_70} PIXEL10_11 PIXEL11_21 break; } case 83: { PIXEL00_11 if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_70} PIXEL10_21 if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_70} break; } case 92: { PIXEL00_21 PIXEL01_11 if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_70} if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_70} break; } case 202: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_70} PIXEL01_21 if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_70} PIXEL11_11 break; } case 78: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_70} PIXEL01_12 if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_70} PIXEL11_22 break; } case 154: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_70} if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_70} PIXEL10_22 PIXEL11_12 break; } case 114: { PIXEL00_22 if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_70} PIXEL10_12 if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_70} break; } case 89: { PIXEL00_12 PIXEL01_22 if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_70} if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_70} break; } case 90: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_70} if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_70} if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_70} if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_70} break; } case 55: case 23: { if(Diff(w[2], w[6])) { PIXEL00_11 PIXEL01_0} else { PIXEL00_60 PIXEL01_90} PIXEL10_20 PIXEL11_21 break; } case 182: case 150: { PIXEL00_22 if(Diff(w[2], w[6])) { PIXEL01_0 PIXEL11_12} else { PIXEL01_90 PIXEL11_61} PIXEL10_20 break; } case 213: case 212: { PIXEL00_20 if(Diff(w[6], w[8])) { PIXEL01_11 PIXEL11_0} else { PIXEL01_60 PIXEL11_90} PIXEL10_21 break; } case 241: case 240: { PIXEL00_20 PIXEL01_22 if(Diff(w[6], w[8])) { PIXEL10_12 PIXEL11_0} else { PIXEL10_61 PIXEL11_90} break; } case 236: case 232: { PIXEL00_21 PIXEL01_20 if(Diff(w[8], w[4])) { PIXEL10_0 PIXEL11_11} else { PIXEL10_90 PIXEL11_60} break; } case 109: case 105: { if(Diff(w[8], w[4])) { PIXEL00_12 PIXEL10_0} else { PIXEL00_61 PIXEL10_90} PIXEL01_20 PIXEL11_22 break; } case 171: case 43: { if(Diff(w[4], w[2])) { PIXEL00_0 PIXEL10_11} else { PIXEL00_90 PIXEL10_60} PIXEL01_21 PIXEL11_20 break; } case 143: case 15: { if(Diff(w[4], w[2])) { PIXEL00_0 PIXEL01_12} else { PIXEL00_90 PIXEL01_61} PIXEL10_22 PIXEL11_20 break; } case 124: { PIXEL00_21 PIXEL01_11 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} PIXEL11_10 break; } case 203: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} PIXEL01_21 PIXEL10_10 PIXEL11_11 break; } case 62: { PIXEL00_10 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} PIXEL10_11 PIXEL11_21 break; } case 211: { PIXEL00_11 PIXEL01_10 PIXEL10_21 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 118: { PIXEL00_22 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} PIXEL10_12 PIXEL11_10 break; } case 217: { PIXEL00_12 PIXEL01_22 PIXEL10_10 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 110: { PIXEL00_10 PIXEL01_12 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} PIXEL11_22 break; } case 155: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} PIXEL01_10 PIXEL10_22 PIXEL11_12 break; } case 188: { PIXEL00_21 PIXEL01_11 PIXEL10_11 PIXEL11_12 break; } case 185: { PIXEL00_12 PIXEL01_22 PIXEL10_11 PIXEL11_12 break; } case 61: { PIXEL00_12 PIXEL01_11 PIXEL10_11 PIXEL11_21 break; } case 157: { PIXEL00_12 PIXEL01_11 PIXEL10_22 PIXEL11_12 break; } case 103: { PIXEL00_11 PIXEL01_12 PIXEL10_12 PIXEL11_22 break; } case 227: { PIXEL00_11 PIXEL01_21 PIXEL10_12 PIXEL11_11 break; } case 230: { PIXEL00_22 PIXEL01_12 PIXEL10_12 PIXEL11_11 break; } case 199: { PIXEL00_11 PIXEL01_12 PIXEL10_21 PIXEL11_11 break; } case 220: { PIXEL00_21 PIXEL01_11 if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_70} if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 158: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_70} if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} PIXEL10_22 PIXEL11_12 break; } case 234: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_70} PIXEL01_21 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} PIXEL11_11 break; } case 242: { PIXEL00_22 if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_70} PIXEL10_12 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 59: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_70} PIXEL10_11 PIXEL11_21 break; } case 121: { PIXEL00_12 PIXEL01_22 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_70} break; } case 87: { PIXEL00_11 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} PIXEL10_21 if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_70} break; } case 79: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} PIXEL01_12 if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_70} PIXEL11_22 break; } case 122: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_70} if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_70} if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_70} break; } case 94: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_70} if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_70} if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_70} break; } case 218: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_70} if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_70} if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_70} if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 91: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_70} if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_70} if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_70} break; } case 229: { PIXEL00_20 PIXEL01_20 PIXEL10_12 PIXEL11_11 break; } case 167: { PIXEL00_11 PIXEL01_12 PIXEL10_20 PIXEL11_20 break; } case 173: { PIXEL00_12 PIXEL01_20 PIXEL10_11 PIXEL11_20 break; } case 181: { PIXEL00_20 PIXEL01_11 PIXEL10_20 PIXEL11_12 break; } case 186: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_70} if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_70} PIXEL10_11 PIXEL11_12 break; } case 115: { PIXEL00_11 if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_70} PIXEL10_12 if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_70} break; } case 93: { PIXEL00_12 PIXEL01_11 if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_70} if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_70} break; } case 206: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_70} PIXEL01_12 if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_70} PIXEL11_11 break; } case 205: case 201: { PIXEL00_12 PIXEL01_20 if(Diff(w[8], w[4])) { PIXEL10_10} else { PIXEL10_70} PIXEL11_11 break; } case 174: case 46: { if(Diff(w[4], w[2])) { PIXEL00_10} else { PIXEL00_70} PIXEL01_12 PIXEL10_11 PIXEL11_20 break; } case 179: case 147: { PIXEL00_11 if(Diff(w[2], w[6])) { PIXEL01_10} else { PIXEL01_70} PIXEL10_20 PIXEL11_12 break; } case 117: case 116: { PIXEL00_20 PIXEL01_11 PIXEL10_12 if(Diff(w[6], w[8])) { PIXEL11_10} else { PIXEL11_70} break; } case 189: { PIXEL00_12 PIXEL01_11 PIXEL10_11 PIXEL11_12 break; } case 231: { PIXEL00_11 PIXEL01_12 PIXEL10_12 PIXEL11_11 break; } case 126: { PIXEL00_10 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} PIXEL11_10 break; } case 219: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} PIXEL01_10 PIXEL10_10 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 125: { if(Diff(w[8], w[4])) { PIXEL00_12 PIXEL10_0} else { PIXEL00_61 PIXEL10_90} PIXEL01_11 PIXEL11_10 break; } case 221: { PIXEL00_12 if(Diff(w[6], w[8])) { PIXEL01_11 PIXEL11_0} else { PIXEL01_60 PIXEL11_90} PIXEL10_10 break; } case 207: { if(Diff(w[4], w[2])) { PIXEL00_0 PIXEL01_12} else { PIXEL00_90 PIXEL01_61} PIXEL10_10 PIXEL11_11 break; } case 238: { PIXEL00_10 PIXEL01_12 if(Diff(w[8], w[4])) { PIXEL10_0 PIXEL11_11} else { PIXEL10_90 PIXEL11_60} break; } case 190: { PIXEL00_10 if(Diff(w[2], w[6])) { PIXEL01_0 PIXEL11_12} else { PIXEL01_90 PIXEL11_61} PIXEL10_11 break; } case 187: { if(Diff(w[4], w[2])) { PIXEL00_0 PIXEL10_11} else { PIXEL00_90 PIXEL10_60} PIXEL01_10 PIXEL11_12 break; } case 243: { PIXEL00_11 PIXEL01_10 if(Diff(w[6], w[8])) { PIXEL10_12 PIXEL11_0} else { PIXEL10_61 PIXEL11_90} break; } case 119: { if(Diff(w[2], w[6])) { PIXEL00_11 PIXEL01_0} else { PIXEL00_60 PIXEL01_90} PIXEL10_12 PIXEL11_10 break; } case 237: case 233: { PIXEL00_12 PIXEL01_20 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_100} PIXEL11_11 break; } case 175: case 47: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_100} PIXEL01_12 PIXEL10_11 PIXEL11_20 break; } case 183: case 151: { PIXEL00_11 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_100} PIXEL10_20 PIXEL11_12 break; } case 245: case 244: { PIXEL00_20 PIXEL01_11 PIXEL10_12 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_100} break; } case 250: { PIXEL00_10 PIXEL01_10 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 123: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} PIXEL01_10 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} PIXEL11_10 break; } case 95: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} PIXEL10_10 PIXEL11_10 break; } case 222: { PIXEL00_10 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} PIXEL10_10 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 252: { PIXEL00_21 PIXEL01_11 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_100} break; } case 249: { PIXEL00_12 PIXEL01_22 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_100} if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 235: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} PIXEL01_21 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_100} PIXEL11_11 break; } case 111: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_100} PIXEL01_12 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} PIXEL11_22 break; } case 63: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_100} if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} PIXEL10_11 PIXEL11_21 break; } case 159: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_100} PIXEL10_22 PIXEL11_12 break; } case 215: { PIXEL00_11 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_100} PIXEL10_21 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 246: { PIXEL00_22 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} PIXEL10_12 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_100} break; } case 254: { PIXEL00_10 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_100} break; } case 253: { PIXEL00_12 PIXEL01_11 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_100} if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_100} break; } case 251: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} PIXEL01_10 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_100} if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 239: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_100} PIXEL01_12 if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_100} PIXEL11_11 break; } case 127: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_100} if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_20} if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_20} PIXEL11_10 break; } case 191: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_100} if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_100} PIXEL10_11 PIXEL11_12 break; } case 223: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_20} if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_100} PIXEL10_10 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_20} break; } case 247: { PIXEL00_11 if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_100} PIXEL10_12 if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_100} break; } case 255: { if(Diff(w[4], w[2])) { PIXEL00_0} else { PIXEL00_100} if(Diff(w[2], w[6])) { PIXEL01_0} else { PIXEL01_100} if(Diff(w[8], w[4])) { PIXEL10_0} else { PIXEL10_100} if(Diff(w[6], w[8])) { PIXEL11_0} else { PIXEL11_100} break; } default: App_Error("GL_SmartFilterHQ2x: Invalid pattern %i.", pattern); break; } pOut += 2 * BPP; }} pOut += BpL; }} return dst; } #undef OFFSET #undef BPP } doomsday-stable-1.15.7/doomsday/client/src/resource/compositetexture.cpp0000664000175000017500000002120112641367670026032 0ustar jaakkojaakko/** @file compositetexture.cpp Composite Texture. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "resource/compositetexture.h" #include #include #include #include #include #include #include #include "resource/patch.h" #include "resource/patchname.h" namespace de { namespace internal { static String readAndPercentEncodeRawName(de::Reader &from) { /// @attention The raw ASCII name is not necessarily terminated. char asciiName[9]; for(int i = 0; i < 8; ++i) { from >> asciiName[i]; } asciiName[8] = 0; // WAD format allows characters not typically permitted in native paths. // To achieve uniformity we apply a percent encoding to the "raw" names. return QString(QByteArray(asciiName).toPercentEncoding()); } } // namespace internal using namespace internal; CompositeTexture::Component::Component(Vector2i const &origin) : _origin (origin) , _lumpNum(-1) {} bool CompositeTexture::Component::operator == (Component const &other) const { if(lumpNum() != other.lumpNum()) return false; if(origin() != other.origin()) return false; return true; } Vector2i const &CompositeTexture::Component::origin() const { return _origin; } lumpnum_t CompositeTexture::Component::lumpNum() const { return _lumpNum; } DENG2_PIMPL_NOREF(CompositeTexture) { String name; ///< Symbolic, percent encoded. Flags flags; ///< Usage traits. Vector2i logicalDimensions; ///< In map space units. Vector2i dimensions; ///< In pixels. int origIndex; ///< Determined by the original game logic. Components components; ///< Images to be composited. Instance() : origIndex(-1) {} }; CompositeTexture::CompositeTexture(String const &percentEncodedName, Vector2i logicalDimensions, Flags flags) : d(new Instance) { d->name = percentEncodedName; d->flags = flags; d->logicalDimensions = logicalDimensions; } bool CompositeTexture::operator == (CompositeTexture const &other) const { if(dimensions() != other.dimensions()) return false; if(logicalDimensions() != other.logicalDimensions()) return false; if(componentCount() != other.componentCount()) return false; // Check each component also. for(int i = 0; i < componentCount(); ++i) { if(components()[i] != other.components()[i]) return false; } return true; } String CompositeTexture::percentEncodedName() const { return d->name; } String const &CompositeTexture::percentEncodedNameRef() const { return d->name; } Vector2i const &CompositeTexture::logicalDimensions() const { return d->logicalDimensions; } Vector2i const &CompositeTexture::dimensions() const { return d->dimensions; } CompositeTexture::Components const &CompositeTexture::components() const { return d->components; } CompositeTexture::Flags CompositeTexture::flags() const { return d->flags; } void CompositeTexture::setFlags(CompositeTexture::Flags flagsToChange, FlagOp operation) { applyFlagOperation(d->flags, flagsToChange, operation); } int CompositeTexture::origIndex() const { return d->origIndex; } void CompositeTexture::setOrigIndex(int newIndex) { d->origIndex = newIndex; } CompositeTexture *CompositeTexture::constructFrom(de::Reader &reader, QList patchNames, ArchiveFormat format) { CompositeTexture *pctex = new CompositeTexture; // First is the raw name. pctex->d->name = readAndPercentEncodeRawName(reader); // Next is some unused junk from a previous format version. dint16 unused16; reader >> unused16; // Next up are scale and logical dimensions. byte scale[2]; /** @todo ZDoom defines these otherwise unused bytes as a scale factor (div 8). We could interpret this also. */ dint16 dimensions[2]; reader >> scale[0] >> scale[1] >> dimensions[0] >> dimensions[1]; // We'll initially accept these values as logical dimensions. However // we may need to adjust once we've checked the patch dimensions. pctex->d->logicalDimensions = pctex->d->dimensions = Vector2i(dimensions[0], dimensions[1]); if(format == DoomFormat) { // Next is some more unused junk from a previous format version. dint32 unused32; reader >> unused32; } /* * Finally, read the component images. * In the process we'll determine the final logical dimensions of the * texture by compositing the geometry of the component images. */ dint16 componentCount; reader >> componentCount; QRect geom(QPoint(0, 0), QSize(pctex->d->logicalDimensions.x, pctex->d->logicalDimensions.y)); int foundComponentCount = 0; for(dint16 i = 0; i < componentCount; ++i) { Component comp; dint16 origin16[2]; reader >> origin16[0] >> origin16[1]; comp._origin = Vector2i(origin16[0], origin16[1]); dint16 pnamesIndex; reader >> pnamesIndex; if(pnamesIndex < 0 || pnamesIndex >= patchNames.count()) { LOG_RES_WARNING("Invalid PNAMES index %i in composite texture \"%s\", ignoring.") << pnamesIndex << pctex->d->name; } else { comp._lumpNum = patchNames[pnamesIndex].lumpNum(); if(comp.lumpNum() >= 0) { /// There is now one more found component. foundComponentCount += 1; File1 &file = App_FileSystem().lump(comp.lumpNum()); // If this a "custom" component - the whole texture is. if(file.container().hasCustom()) { pctex->d->flags |= Custom; } // If this is a Patch - unite the geometry of the component. ByteRefArray fileData = ByteRefArray(file.cache(), file.size()); if(Patch::recognize(fileData)) { try { Patch::Metadata info = Patch::loadMetadata(fileData); geom |= QRect(QPoint(comp.origin().x, comp.origin().y), QSize(info.dimensions.x, info.dimensions.y)); } catch(IByteArray::OffsetError const &) { LOG_RES_WARNING("Component image \"%s\" (#%i) does not appear to be a valid Patch. " "It may be missing from composite texture \"%s\".") << patchNames[pnamesIndex].percentEncodedNameRef() << i << pctex->d->name; } } file.unlock(); } else { LOG_RES_WARNING("Missing component image \"%s\" (#%i) in composite texture \"%s\", ignoring.") << patchNames[pnamesIndex].percentEncodedNameRef() << i << pctex->d->name; } } // Skip the unused "step dir" and "color map" values. reader >> unused16 >> unused16; // Add this component. pctex->d->components.push_back(comp); } // Clip and apply the final height. if(geom.top() < 0) geom.setTop(0); if(geom.height() > pctex->d->logicalDimensions.y) { pctex->d->dimensions.y = geom.height(); } if(!foundComponentCount) { LOG_RES_WARNING("Zero valid component images in composite texture %s (will be ignored).") << pctex->d->name; } return pctex; } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/resource/api_material.cpp0000664000175000017500000000423212641367670025043 0ustar jaakkojaakko#define DENG_NO_API_MACROS_MATERIALS #include "de_base.h" #include "de_resource.h" #include "api_material.h" using namespace de; #undef DD_MaterialForTextureUri DENG_EXTERN_C Material *DD_MaterialForTextureUri(uri_s const *textureUri) { if(!textureUri) return 0; // Not found. try { de::Uri uri = App_ResourceSystem().textureManifest(reinterpret_cast(*textureUri)).composeUri(); uri.setScheme(DD_MaterialSchemeNameForTextureScheme(uri.scheme())); return &App_ResourceSystem().material(uri); } catch(MaterialManifest::MissingMaterialError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ", ignoring."); } catch(ResourceSystem::UnknownSchemeError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ", ignoring."); } catch(ResourceSystem::MissingManifestError const &) {} // Ignore this error. return 0; // Not found. } #undef Materials_ComposeUri DENG_EXTERN_C struct uri_s *Materials_ComposeUri(materialid_t materialId) { MaterialManifest &manifest = App_ResourceSystem().toMaterialManifest(materialId); return reinterpret_cast(new de::Uri(manifest.composeUri())); } #undef Materials_ResolveUri DENG_EXTERN_C materialid_t Materials_ResolveUri(struct uri_s const *uri) { try { return App_ResourceSystem().materialManifest(*reinterpret_cast(uri)).id(); } catch(ResourceSystem::MissingManifestError const &) {} // Ignore this error. return NOMATERIALID; } #undef Materials_ResolveUriCString DENG_EXTERN_C materialid_t Materials_ResolveUriCString(char const *uriCString) { if(uriCString && uriCString[0]) { try { return App_ResourceSystem().materialManifest(de::Uri(uriCString, RC_NULL)).id(); } catch(ResourceSystem::MissingManifestError const &) {} // Ignore this error. } return NOMATERIALID; } DENG_DECLARE_API(Material) = { { DE_API_MATERIALS }, DD_MaterialForTextureUri, Materials_ComposeUri, Materials_ResolveUri, Materials_ResolveUriCString }; doomsday-stable-1.15.7/doomsday/client/src/resource/materialscheme.cpp0000664000175000017500000000576012641367670025406 0ustar jaakkojaakko/** @file materialscheme.cpp Material system subspace scheme. * * @authors Copyright © 2010-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "resource/materialscheme.h" #include "MaterialManifest" using namespace de; DENG2_PIMPL(MaterialScheme) { /// Symbolic name of the scheme. String name; /// Mappings from paths to manifests. MaterialScheme::Index index; Instance(Public *i, String symbolicName) : Base(i), name(symbolicName) {} ~Instance() { self.clear(); DENG2_ASSERT(index.isEmpty()); } }; MaterialScheme::MaterialScheme(String symbolicName) : d(new Instance(this, symbolicName)) {} void MaterialScheme::clear() { d->index.clear(); } String const &MaterialScheme::name() const { return d->name; } MaterialManifest &MaterialScheme::declare(Path const &path) { LOG_AS("MaterialScheme::declare"); if(path.isEmpty()) { /// @throw InvalidPathError An empty path was specified. throw InvalidPathError("MaterialScheme::declare", "Missing/zero-length path was supplied"); } int const sizeBefore = d->index.size(); Manifest *newManifest = &d->index.insert(path); DENG2_ASSERT(newManifest); if(d->index.size() != sizeBefore) { // Notify interested parties that a new manifest was defined in the scheme. DENG2_FOR_AUDIENCE(ManifestDefined, i) i->materialSchemeManifestDefined(*this, *newManifest); } return *newManifest; } bool MaterialScheme::has(Path const &path) const { return d->index.has(path, Index::NoBranch | Index::MatchFull); } MaterialManifest const &MaterialScheme::find(Path const &path) const { if(has(path)) { return d->index.find(path, Index::NoBranch | Index::MatchFull); } /// @throw NotFoundError Failed to locate a matching manifest. throw NotFoundError("MaterialScheme::find", "Failed to locate a manifest matching \"" + path.asText() + "\""); } MaterialManifest &MaterialScheme::find(Path const &path) { Index::Node const &found = const_cast(this)->find(path); return const_cast(found); } MaterialScheme::Index const &MaterialScheme::index() const { return d->index; } doomsday-stable-1.15.7/doomsday/client/src/resource/materialmanifest.cpp0000664000175000017500000001153712641367670025747 0ustar jaakkojaakko/** @file materialmanifest.cpp Description of a logical material resource. * * @authors Copyright © 2011-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "resource/materialmanifest.h" #include "dd_main.h" // App_ResourceSystem() using namespace de; DENG2_PIMPL_NOREF(MaterialManifest) , DENG2_OBSERVES(Material, Deletion) { Flags flags; ///< Classification flags. materialid_t id = 0; ///< Globally unique identifier. std::unique_ptr material; ///< Associated resource (if any). ~Instance() { if(material) material->audienceForDeletion() -= this; } void materialBeingDeleted(Material const &) { material.release(); } }; MaterialManifest::MaterialManifest(PathTree::NodeArgs const &args) : Node(args) , d(new Instance) {} MaterialManifest::~MaterialManifest() { DENG2_FOR_AUDIENCE(Deletion, i) i->materialManifestBeingDeleted(*this); } Material *MaterialManifest::derive() { if(!hasMaterial()) { // Instantiate and associate the new material with this. setMaterial(new Material(*this)); // Notify interested parties that a new material was derived from the manifest. DENG2_FOR_AUDIENCE(MaterialDerived, i) i->materialManifestMaterialDerived(*this, material()); } return &material(); } materialid_t MaterialManifest::id() const { return d->id; } void MaterialManifest::setId(materialid_t id) { d->id = id; } MaterialScheme &MaterialManifest::scheme() const { LOG_AS("MaterialManifest::scheme"); /// @todo Optimize: MaterialManifest should contain a link to the owning MaterialScheme. MaterialScheme *found = nullptr; App_ResourceSystem().forAllMaterialSchemes([this, &found] (MaterialScheme &scheme) { if(&scheme.index() == &tree()) { found = &scheme; return LoopAbort; } return LoopContinue; }); if(found) return *found; /// @throw Error Failed to determine the scheme of the manifest (should never happen...). throw Error("MaterialManifest::scheme", String("Failed to determine scheme for manifest [%1]").arg(de::dintptr(this))); } String const &MaterialManifest::schemeName() const { return scheme().name(); } String MaterialManifest::description(de::Uri::ComposeAsTextFlags uriCompositionFlags) const { String info = String("%1 %2") .arg(composeUri().compose(uriCompositionFlags | de::Uri::DecodePath), ( uriCompositionFlags.testFlag(de::Uri::OmitScheme)? -14 : -22 ) ) .arg(sourceDescription(), -7); #ifdef __CLIENT__ info += String("x%1").arg(!hasMaterial()? 0 : material().animatorCount()); #endif return info; } String MaterialManifest::sourceDescription() const { if(!isCustom()) return "game"; if(isAutoGenerated()) return "add-on"; // Unintuitive but correct. return "def"; } MaterialManifest::Flags MaterialManifest::flags() const { return d->flags; } void MaterialManifest::setFlags(MaterialManifest::Flags flagsToChange, FlagOp operation) { applyFlagOperation(d->flags, flagsToChange, operation); } bool MaterialManifest::hasMaterial() const { return bool(d->material); } Material &MaterialManifest::material() const { if(hasMaterial()) return *d->material; /// @throw MissingMaterialError The manifest is not presently associated with a material. throw MissingMaterialError("MaterialManifest::material", "Missing required material"); } Material *MaterialManifest::materialPtr() const { return hasMaterial()? &material() : nullptr; } void MaterialManifest::setMaterial(Material *newMaterial) { if(d->material.get() != newMaterial) { if(d->material) { // Cancel notifications about the existing material. d->material->audienceForDeletion() -= d; } d->material.reset(newMaterial); if(d->material) { // We want notification when the new material is about to be deleted. d->material->audienceForDeletion() += d; } } } doomsday-stable-1.15.7/doomsday/client/src/resource/materialanimator.cpp0000664000175000017500000006574612641367670025766 0ustar jaakkojaakko/** @file materialanimator.cpp Animator for a draw-context Material variant. * * @authors Copyright © 2011-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "resource/materialanimator.h" #include #include #include "clientapp.h" #include "client/cl_def.h" // playback / clientPaused #include "MaterialVariantSpec" #include "gl/gl_texmanager.h" #include "render/r_main.h" // frameCount, frameTimePos #include "render/rend_main.h" #include "render/rend_halo.h" #include "render/viewports.h" #include "resource/materialdetaillayer.h" #include "resource/materiallightdecoration.h" #include "resource/materialshinelayer.h" #include "resource/materialtexturelayer.h" using namespace de; static inline ResourceSystem &resSys() { return ClientApp::resourceSystem(); } /** * Attempt to locate and prepare a flare texture. Somewhat more complicated than * it needs to be due to the fact there are two different selection methods. * * @param texture Logical texture to prepare an variant of. * @param oldIdx Old method of flare texture selection, by id. * * @return @c 0= Use the automatic selection logic. */ static DGLuint prepareFlaremap(Texture *texture, int oldIdx) { if(texture) { if(TextureVariant const *variant = texture->prepareVariant(Rend_HaloTextureSpec())) { return variant->glName(); } // Dang... } else if(oldIdx > 0 && oldIdx < NUM_SYSFLARE_TEXTURES) { return GL_PrepareSysFlaremap(flaretexid_t(oldIdx - 1)); } return 0; // Use the automatic selection logic. } DENG2_PIMPL_NOREF(MaterialAnimator::Decoration) { MaterialDecoration *matDecor = nullptr; int stage = 0; ///< Animation stage else @c -1 => decoration not in use. short tics = 0; ///< Remaining (sharp) tics in the current stage. float inter = 0; ///< Intermark from the current stage to the next [0..1]. // State snapshot: Vector2f origin; ///< Relative position in material space. Vector3f color; ///< Light color. float elevation = 0; ///< Distance from the surface. float radius = 0; ///< Dynamic light radius (-1 = no light). float lightLevels[2]; ///< Fade by sector lightlevel. float flareSize = 0; ///< Halo radius (zero = no halo). DGLuint flareTex = 0; Texture *tex = nullptr; Texture *ceilTex = nullptr; Texture *floorTex = nullptr; Instance() { de::zap(lightLevels); } bool useInterpolation() const { DENG2_ASSERT(matDecor); if(auto const *light = matDecor->maybeAs()) { return light->useInterpolation(); } return true; } }; MaterialAnimator::Decoration::Decoration(MaterialDecoration &decor) : d(new Instance) { d->matDecor = &decor; } MaterialDecoration &MaterialAnimator::Decoration::decor() const { DENG2_ASSERT(d->matDecor); return *d->matDecor; } Vector2f MaterialAnimator::Decoration::origin() const { return d->origin; } Vector3f MaterialAnimator::Decoration::color() const { return d->color; } float MaterialAnimator::Decoration::elevation() const { return d->elevation; } float MaterialAnimator::Decoration::radius() const { return d->radius; } void MaterialAnimator::Decoration::lightLevels(float &min, float &max) const { min = d->lightLevels[0]; max = d->lightLevels[1]; } float MaterialAnimator::Decoration::flareSize() const { return d->flareSize; } DGLuint MaterialAnimator::Decoration::flareTex() const { return d->flareTex; } Texture *MaterialAnimator::Decoration::tex() const { return d->tex; } Texture *MaterialAnimator::Decoration::ceilTex() const { return d->ceilTex; } Texture *MaterialAnimator::Decoration::floorTex() const { return d->floorTex; } void MaterialAnimator::Decoration::rewind() { d->stage = 0; d->tics = decor().stage(0).tics; d->inter = 0; } bool MaterialAnimator::Decoration::animate() { if(decor().isAnimated()) { d->inter = 0; if(DD_IsSharpTick() && d->tics-- <= 0) { // Advance to next stage. if(++d->stage == decor().stageCount()) { // Loop back to the beginning. d->stage = 0; } MaterialDecoration::Stage const &stage = decor().stage(d->stage); if(stage.variance != 0) d->tics = stage.tics * (1 - stage.variance * RNG_RandFloat()); else d->tics = stage.tics; return true; } if(d->useInterpolation()) { MaterialDecoration::Stage const &stage = decor().stage(d->stage); d->inter = 1.f - d->tics / float( stage.tics ); } } return false; } void MaterialAnimator::Decoration::update() { if(auto *lightDecor = decor().maybeAs()) { MaterialLightDecoration::AnimationStage const &stage = lightDecor->stage(d->stage); MaterialLightDecoration::AnimationStage const &next = lightDecor->stage(d->stage + 1); d->origin = de::lerp(stage.origin, next.origin, d->inter); d->elevation = de::lerp(stage.elevation, next.elevation, d->inter); d->radius = de::lerp(stage.radius, next.radius, d->inter); d->flareSize = de::lerp(stage.haloRadius, next.haloRadius, d->inter); d->lightLevels[0] = de::lerp(stage.lightLevels.min, next.lightLevels.min, d->inter); d->lightLevels[1] = de::lerp(stage.lightLevels.max, next.lightLevels.max, d->inter); d->color = de::lerp(stage.color, next.color, d->inter); d->tex = stage.tex; d->ceilTex = stage.ceilTex; d->floorTex = stage.floorTex; d->flareTex = prepareFlaremap(stage.flareTex, stage.sysFlareIdx); } } void MaterialAnimator::Decoration::reset() { d->origin = Vector2i(0, 0); d->color = Vector3f(0, 0, 0); d->elevation = 0; d->radius = 0; de::zap(d->lightLevels); d->flareSize = 0; d->flareTex = 0; d->tex = nullptr; d->ceilTex = nullptr; d->floorTex = nullptr; } // ------------------------------------------------------------------------------------ /** * Returns the Texture in effect for the given animation stage, if any. * * @todo optimize: Perform this lookup once (when assets are cached). */ static Texture *findTextureForAnimationStage(MaterialTextureLayer::AnimationStage const &stage, String const &propertyName = "texture") { try { return &resSys().texture(de::Uri(stage.gets(propertyName, ""), RC_NULL)); } catch(TextureManifest::MissingTextureError &) {} catch(ResourceSystem::MissingManifestError &) {} return nullptr; } DENG2_PIMPL(MaterialAnimator) { Material *material = nullptr; ///< Material to animate (not owned). MaterialVariantSpec const *spec = nullptr; ///< Variant specification. /// Current state of a layer animation. struct LayerState { int stage; ///< Animation stage else @c -1 => layer not in use. short tics; ///< Remaining (sharp) tics in the current stage. float inter; ///< Intermark from the current stage to the next [0..1]. String synopsis() const { return String("stage: %1 tics: %2 inter: %3").arg(stage).arg(tics).arg(inter); } }; /// Layer animation states. QList layers; /** * Cached animation state snapshot. * * Stage-animated or interpolated material property values are cached in a * per-frame data store to avoid repeat calculation. All other values that do * not change should be obtained directly from the Material. */ struct Snapshot { bool opaque; float glowStrength; Vector2i dimensions; blendmode_t shineBlendMode; Vector3f shineMinColor; /// Textures for each logical texture unit. std::array textures; /// Prepared GL texture unit configurations. These are mapped directly by /// the renderer's DrawLists module. std::array units; Snapshot() { clear(); } void clear() { dimensions = Vector2i(0, 0); shineBlendMode = BM_NORMAL; shineMinColor = Vector3f(0, 0, 0); opaque = true; glowStrength = 0; textures.fill(nullptr); units.fill(GLTextureUnit()); } }; std::unique_ptr snapshot; int lastSnapshotUpdate = -1; ///< Frame count of last snapshot update. /// Animated material decorations. QList decorations; Instance(Public *i) : Base(i) {} ~Instance() { clearLayers(); clearDecorations(); } void clearLayers() { qDeleteAll(layers); layers.clear(); } void initLayers() { clearLayers(); for(int i = 0; i < self.material().layerCount(); ++i) { layers << new LayerState; } } void clearDecorations() { qDeleteAll(decorations); decorations.clear(); } void initDecorations() { clearDecorations(); self.material().forAllDecorations([this] (MaterialDecoration &decor) { decorations << new Decoration(decor); return LoopContinue; }); } void attachMissingSnapshot() { // Already been here? if(snapshot) return; snapshot.reset(new Snapshot); lastSnapshotUpdate = -1; // Force an update. } /// @todo Implement more useful methods of interpolation. (What do we want/need here?) void updateSnapshotIfNeeded(bool force = false) { attachMissingSnapshot(); // Time to update? if(!force && lastSnapshotUpdate == R_FrameCount()) return; lastSnapshotUpdate = R_FrameCount(); snapshot->clear(); for(Decoration *decor : decorations) decor->reset(); /* * Ensure all resources needed to visualize this have been prepared. If * skymasked, we only need to update the primary tex unit (due to it being * visible when skymask debug drawing is enabled). */ if(!material->isSkyMasked() || ::devRendSkyMode) { int texLayerIndex = 0; for(int i = 0; i < material->layerCount(); ++i) { MaterialLayer const &layer = material->layer(i); LayerState const &ls = *layers[i]; if(auto const *detailLayer = layer.maybeAs()) { MaterialTextureLayer::AnimationStage const &stage = detailLayer->stage(ls.stage); MaterialTextureLayer::AnimationStage const &next = detailLayer->stage(ls.stage + 1); if(Texture *tex = findTextureForAnimationStage(stage)) { float const contrast = de::clamp(0.f, stage.getf("strength"), 1.f) * ::detailFactor /*Global strength multiplier*/; snapshot->textures[TU_DETAIL] = tex->prepareVariant(resSys().detailTextureSpec(contrast)); } // Smooth Texture Animation? if(::smoothTexAnim && &stage != &next) { if(Texture *tex = findTextureForAnimationStage(next)) { float const contrast = de::clamp(0.f, next.getf("strength"), 1.f) * ::detailFactor /*Global strength multiplier*/; snapshot->textures[TU_DETAIL_INTER] = tex->prepareVariant(resSys().detailTextureSpec(contrast)); } } } else if(layer.is()) { MaterialTextureLayer::AnimationStage const &stage = layer.as().stage(ls.stage); //MaterialTextureLayer::AnimationStage const &next = layer.stage(l.stage + 1); if(Texture *tex = findTextureForAnimationStage(stage)) { snapshot->textures[TU_SHINE] = tex->prepareVariant(Rend_MapSurfaceShinyTextureSpec()); // We are only interested in a mask if we have a shiny texture. if(Texture *maskTex = findTextureForAnimationStage(stage, "maskTexture")) { snapshot->textures[TU_SHINE_MASK] = maskTex->prepareVariant(Rend_MapSurfaceShinyMaskTextureSpec()); } } } else if(auto const *texLayer = layer.maybeAs()) { MaterialTextureLayer::AnimationStage const &stage = texLayer->stage(ls.stage); MaterialTextureLayer::AnimationStage const &next = texLayer->stage(ls.stage + 1); if(Texture *tex = findTextureForAnimationStage(stage)) { snapshot->textures[TU_LAYER0 + texLayerIndex] = tex->prepareVariant(*spec->primarySpec); } // Smooth Texture Animation? if(::smoothTexAnim && &stage != &next) { if(Texture *tex = findTextureForAnimationStage(next)) { snapshot->textures[TU_LAYER0_INTER + texLayerIndex] = tex->prepareVariant(*spec->primarySpec); } } texLayerIndex += 1; } } } snapshot->dimensions = material->dimensions(); snapshot->opaque = (snapshot->textures[TU_LAYER0] && !snapshot->textures[TU_LAYER0]->isMasked()); if(snapshot->dimensions == Vector2i(0, 0)) return; if(material->isSkyMasked() && !::devRendSkyMode) return; int texLayerIndex = 0; for(int i = 0; i < material->layerCount(); ++i) { MaterialLayer const &layer = material->layer(i); LayerState const &ls = *layers[i]; if(auto const *detailLayer = layer.maybeAs()) { if(TextureVariant *tex = snapshot->textures[TU_DETAIL]) { MaterialTextureLayer::AnimationStage const &stage = detailLayer->stage(ls.stage); MaterialTextureLayer::AnimationStage const &next = detailLayer->stage(ls.stage + 1); float scale = de::lerp(stage.getf("scale"), next.getf("scale"), ls.inter); if(::detailScale > .0001f) scale *= ::detailScale; // Global scale factor. snapshot->units[TU_DETAIL] = GLTextureUnit(*tex, Vector2f(1, 1) / tex->base().dimensions() * scale); // Setup the inter detail texture unit. if(TextureVariant *tex = snapshot->textures[TU_DETAIL_INTER]) { // If fog is active, inter=0 is accepted as well. Otherwise // flickering may occur if the rendering passes don't match for // blended and unblended surfaces. if(!(!::usingFog && ls.inter == 0)) { snapshot->units[TU_DETAIL_INTER] = GLTextureUnit(*tex, snapshot->units[TU_DETAIL].scale, snapshot->units[TU_DETAIL].offset, de::clamp(0.f, ls.inter, 1.f)); } } } } else if(layer.is()) { if(TextureVariant *tex = snapshot->textures[TU_SHINE]) { MaterialTextureLayer::AnimationStage const &stage = layer.as().stage(ls.stage); MaterialTextureLayer::AnimationStage const &next = layer.as().stage(ls.stage + 1); Vector2f origin; for(int k = 0; k < 2; ++k) { origin[k] = de::lerp(stage.geta("origin")[k].asNumber(), next.geta("origin")[k].asNumber(), ls.inter); } Vector3f minColor; for(int k = 0; k < 3; ++k) { minColor[k] = de::lerp(stage.geta("minColor")[k].asNumber(), next.geta("minColor")[k].asNumber(), ls.inter); } float const opacity = de::lerp(stage.getf("opacity"), next.getf("opacity"), ls.inter); snapshot->shineBlendMode = blendmode_t( stage.geti("blendMode") ); snapshot->shineMinColor = minColor.min(Vector3f(1, 1, 1)).max(Vector3f(0, 0, 0)); snapshot->units[TU_SHINE] = GLTextureUnit(*tex, Vector2f(1, 1), origin, de::clamp(0.0f, opacity, 1.0f)); // Setup the shine mask texture unit. if(TextureVariant *maskTex = snapshot->textures[TU_SHINE_MASK]) { snapshot->units[TU_SHINE_MASK] = GLTextureUnit(*maskTex, Vector2f(1, 1) / (snapshot->dimensions * maskTex->base().dimensions()), snapshot->units[TU_LAYER0].offset); } } } else if(auto const *texLayer = layer.maybeAs()) { if(TextureVariant *tex = snapshot->textures[TU_LAYER0 + texLayerIndex]) { MaterialTextureLayer::AnimationStage const &stage = texLayer->stage(ls.stage); MaterialTextureLayer::AnimationStage const &next = texLayer->stage(ls.stage + 1); Vector2f const scale = Vector2f(1, 1) / snapshot->dimensions; Vector2f origin; for(int k = 0; k < 2; ++k) { origin[k] = de::lerp(stage.geta("origin")[k].asNumber(), next.geta("origin")[k].asNumber(), ls.inter); } float const opacity = de::lerp(stage.getf("opacity"), next.getf("opacity"), ls.inter); snapshot->units[TU_LAYER0 + texLayerIndex] = GLTextureUnit(*tex, scale, origin, de::clamp(0.0f, opacity, 1.0f)); // Glow strength is taken from texture layer #0. if(texLayerIndex == 0) { snapshot->glowStrength = de::lerp(stage.getf("glowStrength"), next.getf("glowStrength"), ls.inter); } // Setup the inter texture unit. if(TextureVariant *tex = snapshot->textures[TU_LAYER0_INTER + texLayerIndex]) { // If fog is active, inter=0 is accepted as well. Otherwise // flickering may occur if the rendering passes don't match for // blended and unblended surfaces. if(!(!usingFog && ls.inter == 0)) { snapshot->units[TU_LAYER0_INTER + texLayerIndex] = GLTextureUnit(*tex, snapshot->units[TU_LAYER0 + texLayerIndex].scale, snapshot->units[TU_LAYER0 + texLayerIndex].offset, de::clamp(0.f, ls.inter, 1.f)); } } texLayerIndex += 1; } } } if(!material->isSkyMasked()) for(Decoration *decor : decorations) { decor->update(); } } void rewindLayer(LayerState &ls, MaterialLayer const &layer) { ls.stage = 0; ls.tics = layer.stage(0).tics; ls.inter = 0; } void animateLayer(LayerState &ls, MaterialLayer const &layer) { if(DD_IsSharpTick() && ls.tics-- <= 0) { // Advance to next stage. if(++ls.stage == layer.stageCount()) { // Loop back to the beginning. ls.stage = 0; } ls.inter = 0; MaterialLayer::Stage const &stage = layer.stage(ls.stage); if(stage.variance != 0) ls.tics = stage.tics * (1 - stage.variance * RNG_RandFloat()); else ls.tics = stage.tics; } else { MaterialLayer::Stage const &stage = layer.stage(ls.stage); ls.inter = 1.f - ls.tics / float( stage.tics ); } } }; MaterialAnimator::MaterialAnimator(Material &material, MaterialVariantSpec const &spec) : d(new Instance(this)) { d->material = &material; d->spec = &spec; d->initLayers(); d->initDecorations(); // Prepare for animation. rewind(); } Material &MaterialAnimator::material() const { DENG2_ASSERT(d->material); return *d->material; } MaterialVariantSpec const &MaterialAnimator::variantSpec() const { DENG2_ASSERT(d->spec); return *d->spec; } bool MaterialAnimator::isPaused() const { // Depending on the usage context, the animation should only progress // when the game is not paused. MaterialContextId context = variantSpec().contextId; return (clientPaused && (context == MapSurfaceContext || context == SpriteContext || context == ModelSkinContext || context == PSpriteContext || context == SkySphereContext)); } void MaterialAnimator::animate(timespan_t /*ticLength*/) { // Animation ceases once the material is no longer valid. if(!material().isValid()) return; // Animation will only progress when not paused. if(isPaused()) return; /* * Animate layers: */ for(int i = 0; i < material().layerCount(); ++i) { MaterialLayer const &layer = material().layer(i); if(layer.isAnimated() && layer.is()) { d->animateLayer(*d->layers[i], layer); } } /* * Animate decorations: */ bool decorationStageChanged = false; for(Decoration *decor : d->decorations) { if(decor->animate()) { decorationStageChanged = true; } } if(decorationStageChanged) { // Notify interested parties. DENG2_FOR_AUDIENCE(DecorationStageChange, i) i->materialAnimatorDecorationStageChanged(*this); } } void MaterialAnimator::rewind() { // Animation ceases once the material is no longer valid. if(!material().isValid()) return; for(int i = 0; i < material().layerCount(); ++i) { d->rewindLayer(*d->layers[i], material().layer(i)); } for(int i = 0; i < material().decorationCount(); ++i) { d->decorations[i]->rewind(); } } void MaterialAnimator::prepare(bool fullUpdate) { d->updateSnapshotIfNeeded(fullUpdate); } void MaterialAnimator::cacheAssets() { prepare(true); if(material().isSkyMasked() && !::devRendSkyMode) return; for(int i = 0; i < material().layerCount(); ++i) { if(MaterialTextureLayer *layer = material().layer(i).maybeAs()) { for(int k = 0; k < layer->stageCount(); ++k) { MaterialTextureLayer::AnimationStage &stage = layer->stage(k); if(Texture *tex = findTextureForAnimationStage(stage)) { if(layer->is()) { float const contrast = de::clamp(0.f, stage.getf("strength"), 1.f) * detailFactor /*Global strength multiplier*/; tex->prepareVariant(resSys().detailTextureSpec(contrast)); } else if(layer->is()) { tex->prepareVariant(Rend_MapSurfaceShinyTextureSpec()); if(Texture *maskTex = findTextureForAnimationStage(stage, "maskTexture")) { maskTex->prepareVariant(Rend_MapSurfaceShinyMaskTextureSpec()); } } else { tex->prepareVariant(*variantSpec().primarySpec); } } } } } } bool MaterialAnimator::isOpaque() const { d->updateSnapshotIfNeeded(); return d->snapshot->opaque; } Vector2i const &MaterialAnimator::dimensions() const { d->updateSnapshotIfNeeded(); return d->snapshot->dimensions; } float MaterialAnimator::glowStrength() const { d->updateSnapshotIfNeeded(); return d->snapshot->glowStrength; } blendmode_t MaterialAnimator::shineBlendMode() const { d->updateSnapshotIfNeeded(); return d->snapshot->shineBlendMode; } Vector3f const &MaterialAnimator::shineMinColor() const { d->updateSnapshotIfNeeded(); return d->snapshot->shineMinColor; } GLTextureUnit &MaterialAnimator::texUnit(int unitIndex) const { d->updateSnapshotIfNeeded(); if(unitIndex >= 0 && unitIndex < NUM_TEXTUREUNITS) return d->snapshot->units[unitIndex]; /// @throw MissingTextureUnitError Invalid GL-texture unit reference. throw MissingTextureUnitError("MaterialAnimator::glTextureUnit", "Unknown GL texture unit #" + String::number(unitIndex)); } MaterialAnimator::Decoration &MaterialAnimator::decoration(int decorIndex) const { d->updateSnapshotIfNeeded(); if(decorIndex >= 0 && decorIndex < d->decorations.count()) return *d->decorations[decorIndex]; /// @throw MissingDecorationError Invalid decoration reference. throw MissingDecorationError("MaterialAnimator::decoration", "Unknown decoration #" + String::number(decorIndex)); } doomsday-stable-1.15.7/doomsday/client/src/resource/manifest.cpp0000664000175000017500000002306512641367670024227 0ustar jaakkojaakko/** @file manifest.cpp Game resource manifest. * * @authors Copyright © 2010-2013 Daniel Swanson * @authors Copyright © 2010-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "resource/manifest.h" #include "dd_main.h" #include #include #include #include #include #include #include using namespace de; DENG2_PIMPL(ResourceManifest) { resourceclassid_t classId; int flags; ///< @ref fileFlags. QStringList names; ///< Known names in precedence order. /// Vector of resource identifier keys (e.g., file or lump names). /// Used for identification purposes. QStringList identityKeys; /// Index (in Manifest::Instance::names) of the name used to locate /// this resource if found. Set during resource location. int foundNameIndex; /// Fully resolved absolute path to the located resource if found. /// Set during resource location. String foundPath; Instance(Public *i, resourceclassid_t rclass, int rflags) : Base(i) , classId(rclass) , flags(rflags & ~FF_FOUND) , names() , identityKeys() , foundNameIndex(-1) , foundPath() {} }; ResourceManifest::ResourceManifest(resourceclassid_t resClass, int fFlags, String *name) : d(new Instance(this, resClass, fFlags)) { if(name) addName(*name); } void ResourceManifest::addName(String newName) { if(newName.isEmpty()) return; // Is this name unique? We don't want duplicates. if(!d->names.contains(newName, Qt::CaseInsensitive)) { d->names.prepend(newName); } } void ResourceManifest::addIdentityKey(String newIdKey) { if(newIdKey.isEmpty()) return; // Is this key unique? We don't want duplicates. if(!d->identityKeys.contains(newIdKey, Qt::CaseInsensitive)) { d->identityKeys.append(newIdKey); } } enum lumpsizecondition_t { LSCOND_NONE, LSCOND_EQUAL, LSCOND_GREATER_OR_EQUAL, LSCOND_LESS_OR_EQUAL }; /** * Modifies the idKey so that the size condition is removed. */ static void checkSizeConditionInIdentityKey(String &idKey, lumpsizecondition_t *pCond, size_t *pSize) { DENG_ASSERT(pCond != 0); DENG_ASSERT(pSize != 0); *pCond = LSCOND_NONE; *pSize = 0; int condPos = -1; int argPos = -1; if((condPos = idKey.indexOf("==")) >= 0) { *pCond = LSCOND_EQUAL; argPos = condPos + 2; } else if((condPos = idKey.indexOf(">=")) >= 0) { *pCond = LSCOND_GREATER_OR_EQUAL; argPos = condPos + 2; } else if((condPos = idKey.indexOf("<=")) >= 0) { *pCond = LSCOND_LESS_OR_EQUAL; argPos = condPos + 2; } if(condPos < 0) return; // Get the argument. *pSize = idKey.mid(argPos).toULong(); // Remove it from the name. idKey.truncate(condPos); } static lumpnum_t lumpNumForIdentityKey(LumpIndex const &lumpIndex, String idKey) { if(idKey.isEmpty()) return -1; // The key may contain a size condition (==, >=, <=). lumpsizecondition_t sizeCond; size_t refSize; checkSizeConditionInIdentityKey(idKey, &sizeCond, &refSize); // We should now be left with just the name. String name = idKey; // Append a .lmp extension if none is specified. if(idKey.fileNameExtension().isEmpty()) { name += ".lmp"; } lumpnum_t lumpNum = lumpIndex.findLast(Path(name)); if(lumpNum < 0) return -1; // Check the condition. size_t lumpSize = lumpIndex[lumpNum].info().size; switch(sizeCond) { case LSCOND_EQUAL: if(lumpSize != refSize) return -1; break; case LSCOND_GREATER_OR_EQUAL: if(lumpSize < refSize) return -1; break; case LSCOND_LESS_OR_EQUAL: if(lumpSize > refSize) return -1; break; default: break; } return lumpNum; } /// @return @c true, iff the resource appears to be what we think it is. static bool validateWad(String const &filePath, QStringList const &identityKeys) { bool validated = true; try { FileHandle &hndl = App_FileSystem().openFile(filePath, "rb", 0/*baseOffset*/, true /*allow duplicates*/); if(Wad *wad = hndl.file().maybeAs()) { // Ensure all identity lumps are present. if(identityKeys.count()) { if(wad->isEmpty()) { // Clear not what we are looking for. validated = false; } else { // Publish lumps to a temporary index. LumpIndex lumpIndex; for(int i = 0; i < wad->lumpCount(); ++i) { lumpIndex.catalogLump(wad->lump(i)); } // Check each lump. DENG2_FOR_EACH_CONST(QStringList, i, identityKeys) { if(lumpNumForIdentityKey(lumpIndex, *i) < 0) { validated = false; break; } } } } } else { validated = false; } // We're done with the file. App_FileSystem().releaseFile(hndl.file()); delete &hndl; } catch(FS1::NotFoundError const &) {} // Ignore this error. return validated; } /// @return @c true, iff the resource appears to be what we think it is. static bool validateZip(String const &filePath, QStringList const & /*identityKeys*/) { try { FileHandle &hndl = App_FileSystem().openFile(filePath, "rbf"); bool result = Zip::recognise(hndl); /// @todo Check files. We should implement an auxiliary zip lump index... App_FileSystem().releaseFile(hndl.file()); delete &hndl; return result; } catch(FS1::NotFoundError const &) {} // Ignore error. return false; } void ResourceManifest::locateFile() { // Already found? if(d->flags & FF_FOUND) return; // Perform the search. int nameIndex = 0; for(QStringList::const_iterator i = d->names.constBegin(); i != d->names.constEnd(); ++i, ++nameIndex) { StringList candidates; // Attempt to resolve a path to the named resource using FS1. try { String foundPath = App_FileSystem().findPath(de::Uri(*i, d->classId), RLF_DEFAULT, App_ResourceClass(d->classId)); foundPath = App_BasePath() / foundPath; // Ensure the path is absolute. candidates << foundPath; } catch(FS1::NotFoundError const &) {} // Ignore this error. // Also check what FS2 has to offer. FS1 can't access FS2's files, so we'll // restrict this to native files. App::fileSystem().forAll(*i, [&candidates] (File &f) { // We ignore interpretations and go straight to the source. if(NativeFile const *native = f.source()->maybeAs()) { candidates << native->nativePath(); } return LoopContinue; }); for(String foundPath : candidates) { // Perform identity validation. bool validated = false; if(d->classId == RC_PACKAGE) { /// @todo The identity configuration should declare the type of resource... validated = validateWad(foundPath, d->identityKeys); if(!validated) validated = validateZip(foundPath, d->identityKeys); } else { // Other resource types are not validated. validated = true; } if(validated) { // This is the resource we've been looking for. d->flags |= FF_FOUND; d->foundPath = foundPath; d->foundNameIndex = nameIndex; return; } } } } void ResourceManifest::forgetFile() { if(d->flags & FF_FOUND) { d->foundPath.clear(); d->foundNameIndex = -1; d->flags &= ~FF_FOUND; } } String const &ResourceManifest::resolvedPath(bool tryLocate) { if(tryLocate) { locateFile(); } return d->foundPath; } resourceclassid_t ResourceManifest::resourceClass() const { return d->classId; } int ResourceManifest::fileFlags() const { return d->flags; } QStringList const &ResourceManifest::identityKeys() const { return d->identityKeys; } QStringList const &ResourceManifest::names() const { return d->names; } doomsday-stable-1.15.7/doomsday/client/src/resource/animgroup.cpp0000664000175000017500000000510512641367670024415 0ustar jaakkojaakko/** @file animgroup.cpp Material animation group. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "resource/animgroup.h" #include #include using namespace de; AnimGroup::Frame::Frame(TextureManifest &textureManifest, ushort tics, ushort randomTics) : _textureManifest(&textureManifest) , _tics(tics) , _randomTics(randomTics) {} TextureManifest &AnimGroup::Frame::textureManifest() const { return *_textureManifest; } ushort AnimGroup::Frame::tics() const { return _tics; } ushort AnimGroup::Frame::randomTics() const { return _randomTics; } DENG2_PIMPL(AnimGroup) { Frames frames; int uniqueId; int flags; ///< @ref animationGroupFlags Instance(Public *i) : Base(i) , uniqueId(0) , flags(0) {} ~Instance() { self.clearAllFrames(); } }; AnimGroup::AnimGroup(int uniqueId, int flags) : d(new Instance(this)) { d->uniqueId = uniqueId; d->flags = flags; } void AnimGroup::clearAllFrames() { qDeleteAll(d->frames); d->frames.clear(); } int AnimGroup::id() const { return d->uniqueId; } int AnimGroup::flags() const { return d->flags; } bool AnimGroup::hasFrameFor(TextureManifest const &textureManifest) const { foreach(Frame *frame, d->frames) { if(&frame->textureManifest() == &textureManifest) return true; } return false; } AnimGroup::Frame &AnimGroup::newFrame(TextureManifest &textureManifest, ushort tics, ushort randomTics) { d->frames.append(new Frame(textureManifest, tics, randomTics)); return *d->frames.last(); } AnimGroup::Frames const &AnimGroup::allFrames() const { return d->frames; } doomsday-stable-1.15.7/doomsday/client/src/resource/fontmanifest.cpp0000664000175000017500000000755212641367670025121 0ustar jaakkojaakko/** @file fontmanifest.cpp Font resource manifest. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "resource/fontmanifest.h" #include "dd_main.h" // App_Fonts(), remove me #include "FontScheme" #include using namespace de; DENG2_PIMPL(FontManifest), DENG2_OBSERVES(AbstractFont, Deletion) { int uniqueId; QScopedPointer(resource); ///< Associated resource (if any). Instance(Public *i) : Base(i) , uniqueId(0) {} ~Instance() { DENG2_FOR_PUBLIC_AUDIENCE(Deletion, i) i->fontManifestBeingDeleted(self); } // Observes AbstractFont::Deletion. void fontBeingDeleted(AbstractFont const & /*resource*/) { resource.reset(); } }; FontManifest::FontManifest(PathTree::NodeArgs const &args) : Node(args), d(new Instance(this)) {} FontScheme &FontManifest::scheme() const { LOG_AS("FontManifest"); /// @todo Optimize: FontManifest should contain a link to the owning FontScheme. foreach(FontScheme *scheme, App_ResourceSystem().allFontSchemes()) { if(&scheme->index() == &tree()) return *scheme; } /// @throw Error Failed to determine the scheme of the manifest (should never happen...). throw Error("FontManifest::scheme", String("Failed to determine scheme for manifest [%1]").arg(de::dintptr(this))); } String const &FontManifest::schemeName() const { return scheme().name(); } String FontManifest::description(de::Uri::ComposeAsTextFlags uriCompositionFlags) const { return String("%1").arg(composeUri().compose(uriCompositionFlags | Uri::DecodePath), ( uriCompositionFlags.testFlag(Uri::OmitScheme)? -14 : -22 ) ); } int FontManifest::uniqueId() const { return d->uniqueId; } bool FontManifest::setUniqueId(int newUniqueId) { LOG_AS("FontManifest"); if(d->uniqueId == newUniqueId) return false; d->uniqueId = newUniqueId; // Notify interested parties that the uniqueId has changed. DENG2_FOR_AUDIENCE(UniqueIdChange, i) i->fontManifestUniqueIdChanged(*this); return true; } bool FontManifest::hasResource() const { return !d->resource.isNull(); } AbstractFont &FontManifest::resource() const { if(hasResource()) { return *d->resource.data(); } /// @throw MissingFontError No resource is associated with the manifest. throw MissingFontError("FontManifest::resource", "No resource is associated"); } void FontManifest::setResource(AbstractFont *newResource) { LOG_AS("FontManifest"); if(d->resource.data() != newResource) { if(AbstractFont *curFont = d->resource.data()) { // Cancel notifications about the existing resource. curFont->audienceForDeletion -= d; } d->resource.reset(newResource); if(AbstractFont *curFont = d->resource.data()) { // We want notification when the new resource is about to be deleted. curFont->audienceForDeletion += d; } } } doomsday-stable-1.15.7/doomsday/client/src/resource/texturevariant.cpp0000664000175000017500000005036212641367670025506 0ustar jaakkojaakko/** @file texturevariant.cpp Context specialized texture variant. * * @authors Copyright © 1999-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "resource/texture.h" #include "r_util.h" #include "gl/gl_defer.h" #include "gl/gl_main.h" #include "gl/gl_tex.h" #include "gl/texturecontent.h" #include "resource/image.h" // GL_LoadSourceImage #include "render/rend_main.h" // misc global vars awaiting new home #include #include // M_CeilPow using namespace de; variantspecification_t::variantspecification_t() : context(TC_UNKNOWN) , flags(0) , border(0) , wrapS(GL_REPEAT) , wrapT(GL_REPEAT) , mipmapped(false) , gammaCorrection(true) , noStretch(false) , toAlpha(false) , minFilter(GL_LINEAR) , magFilter(GL_LINEAR) , anisoFilter(0) , tClass(0) , tMap(0) {} variantspecification_t::variantspecification_t(variantspecification_t const &other) : context(other.context) , flags(other.flags) , border(other.border) , wrapS(other.wrapS) , wrapT(other.wrapT) , mipmapped(other.mipmapped) , gammaCorrection(other.gammaCorrection) , noStretch(other.noStretch) , toAlpha(other.toAlpha) , minFilter(other.minFilter) , magFilter(other.magFilter) , anisoFilter(other.anisoFilter) , tClass(other.tClass) , tMap(other.tMap) {} bool variantspecification_t::operator == (variantspecification_t const &other) const { if(this == &other) return 1; /// @todo We can be a bit cleverer here... if(context != other.context) return 0; if(flags != other.flags) return 0; if(wrapS != other.wrapS || wrapT != other.wrapT) return 0; //if(magFilter != b.magFilter) return 0; //if(anisoFilter != b.anisoFilter) return 0; if(mipmapped != other.mipmapped) return 0; if(noStretch != other.noStretch) return 0; if(gammaCorrection != other.gammaCorrection) return 0; if(toAlpha != other.toAlpha) return 0; if(border != other.border) return 0; if(flags & TSF_HAS_COLORPALETTE_XLAT) { if(tClass != other.tClass) return 0; if(tMap != other.tMap) return 0; } return 1; // Equal. } int variantspecification_t::glMinFilter() const { if(minFilter >= 0) // Constant logical value. { return (mipmapped? GL_NEAREST_MIPMAP_NEAREST : GL_NEAREST) + minFilter; } // "No class" preference. return mipmapped? glmode[mipmapping] : GL_LINEAR; } int variantspecification_t::glMagFilter() const { if(magFilter >= 0) // Constant logical value. { return GL_NEAREST + magFilter; } // Preference for texture class id. switch(abs(magFilter)-1) { case 1: // Sprite class. return filterSprites? GL_LINEAR : GL_NEAREST; case 2: // UI class. return filterUI? GL_LINEAR : GL_NEAREST; default: // "No class" preference. return glmode[texMagMode]; } } int variantspecification_t::logicalAnisoLevel() const { return anisoFilter < 0? texAniso : anisoFilter; } TextureVariantSpec::TextureVariantSpec(texturevariantspecificationtype_t type) : type(type) {} TextureVariantSpec::TextureVariantSpec(TextureVariantSpec const &other) : type(other.type) , variant(other.variant) , detailVariant(other.detailVariant) {} bool detailvariantspecification_t::operator == (detailvariantspecification_t const &other) const { if(this == &other) return true; return contrast == other.contrast; // Equal. } bool TextureVariantSpec::operator == (TextureVariantSpec const &other) const { if(this == &other) return true; if(type != other.type) return false; switch(type) { case TST_GENERAL: return variant == other.variant; case TST_DETAIL: return detailVariant == other.detailVariant; } DENG2_ASSERT(false); return false; } static String nameForGLTextureWrapMode(int mode) { if(mode == GL_REPEAT) return "repeat"; if(mode == GL_CLAMP) return "clamp"; if(mode == GL_CLAMP_TO_EDGE) return "clamp_edge"; return "(unknown)"; } String TextureVariantSpec::asText() const { static String const textureUsageContextNames[1 + TEXTUREVARIANTUSAGECONTEXT_COUNT] = { /* TC_UNKNOWN */ "unknown", /* TC_UI */ "ui", /* TC_MAPSURFACE_DIFFUSE */ "mapsurface_diffuse", /* TC_MAPSURFACE_REFLECTION */ "mapsurface_reflection", /* TC_MAPSURFACE_REFLECTIONMASK */ "mapsurface_reflectionmask", /* TC_MAPSURFACE_LIGHTMAP */ "mapsurface_lightmap", /* TC_SPRITE_DIFFUSE */ "sprite_diffuse", /* TC_MODELSKIN_DIFFUSE */ "modelskin_diffuse", /* TC_MODELSKIN_REFLECTION */ "modelskin_reflection", /* TC_HALO_LUMINANCE */ "halo_luminance", /* TC_PSPRITE_DIFFUSE */ "psprite_diffuse", /* TC_SKYSPHERE_DIFFUSE */ "skysphere_diffuse" }; static String const textureSpecificationTypeNames[2] = { /* TST_GENERAL */ "general", /* TST_DETAIL */ "detail" }; static String const filterModeNames[] = { "ui", "sprite", "noclass", "const" }; static String const glFilterNames[] = { "nearest", "linear", "nearest_mipmap_nearest", "linear_mipmap_nearest", "nearest_mipmap_linear", "linear_mipmap_linear" }; String text = String("Type:%1").arg(textureSpecificationTypeNames[type]); switch(type) { case TST_DETAIL: { detailvariantspecification_t const &spec = detailVariant; text += " Contrast:" + String::number(int(.5f + spec.contrast / 255.f * 100)) + "%"; break; } case TST_GENERAL: { variantspecification_t const &spec = variant; texturevariantusagecontext_t tc = spec.context; DENG2_ASSERT(tc == TC_UNKNOWN || VALID_TEXTUREVARIANTUSAGECONTEXT(tc)); int glMinFilterNameIdx; if(spec.minFilter >= 0) // Constant logical value. { glMinFilterNameIdx = (spec.mipmapped? 2 : 0) + spec.minFilter; } else // "No class" preference. { glMinFilterNameIdx = spec.mipmapped? mipmapping : 1; } int glMagFilterNameIdx; if(spec.magFilter >= 0) // Constant logical value. { glMagFilterNameIdx = spec.magFilter; } else { // Preference for texture class id. switch(abs(spec.magFilter)-1) { // "No class" preference. default: glMagFilterNameIdx = texMagMode; break; // "Sprite" class. case 1: glMagFilterNameIdx = filterSprites; break; // "UI" class. case 2: glMagFilterNameIdx = filterUI; break; } } text += " Context:" + textureUsageContextNames[tc-TEXTUREVARIANTUSAGECONTEXT_FIRST + 1] + " Flags:" + String::number(spec.flags & ~TSF_INTERNAL_MASK) + " Border:" + String::number(spec.border) + " MinFilter:" + filterModeNames[3 + de::clamp(-1, spec.minFilter, 0)] + "|" + glFilterNames[glMinFilterNameIdx] + " MagFilter:" + filterModeNames[3 + de::clamp(-3, spec.magFilter, 0)] + "|" + glFilterNames[glMagFilterNameIdx] + " AnisoFilter:" + String::number(spec.anisoFilter) + " WrapS:" + nameForGLTextureWrapMode(spec.wrapS) + " WrapT:" + nameForGLTextureWrapMode(spec.wrapT) + " CorrectGamma:" + (spec.gammaCorrection? "yes" : "no") + " NoStretch:" + (spec.noStretch? "yes" : "no") + " ToAlpha:" + (spec.toAlpha? "yes" : "no"); if(spec.flags & TSF_HAS_COLORPALETTE_XLAT) { text += " Translated:(tclass:" + String::number(spec.tClass) + " tmap:" + String::number(spec.tMap) + ")"; } break; } } return text; } DENG2_PIMPL(Texture::Variant) { Texture &texture; /// The base for which "this" is a context derivative. TextureVariantSpec const &spec; /// Usage context specification. Flags flags; res::Source texSource; ///< Logical source of the image. /// Name of the associated GL texture object. /// @todo Use GLTexture uint glTexName; /// Prepared coordinates for the bottom right of the texture minus border. float s, t; Instance(Public *i, Texture &generalCase, TextureVariantSpec const &spec) : Base(i) , texture(generalCase) , spec(spec) , flags(0) , texSource(res::None) , glTexName(0) , s(0) , t(0) {} ~Instance() { // Release any GL texture we may have prepared. self.release(); } }; Texture::Variant::Variant(Texture &generalCase, TextureVariantSpec const &spec) : d(new Instance(this, generalCase, spec)) {} /** * Perform analyses of the @a image pixel data and record this information * for reference later. * * @param image Image data to be analyzed. * @param context Context in which the uploaded image will be used. * @param tex Logical texture which will hold the analysis data. * @param forceUpdate Force an update of the recorded analysis data. */ static void performImageAnalyses(image_t const &image, texturevariantusagecontext_t context, Texture &tex, bool forceUpdate) { // Do we need color palette info? if(image.paletteId != 0) { colorpalette_analysis_t *cp = reinterpret_cast(tex.analysisDataPointer(Texture::ColorPaletteAnalysis)); bool firstInit = (!cp); if(firstInit) { cp = (colorpalette_analysis_t *) M_Malloc(sizeof(*cp)); tex.setAnalysisDataPointer(Texture::ColorPaletteAnalysis, cp); } if(firstInit || forceUpdate) cp->paletteId = image.paletteId; } // Calculate a point light source for Dynlight and/or Halo? if(context == TC_SPRITE_DIFFUSE) { pointlight_analysis_t *pl = reinterpret_cast(tex.analysisDataPointer(Texture::BrightPointAnalysis)); bool firstInit = (!pl); if(firstInit) { pl = (pointlight_analysis_t *) M_Malloc(sizeof *pl); tex.setAnalysisDataPointer(Texture::BrightPointAnalysis, pl); } if(firstInit || forceUpdate) { GL_CalcLuminance(image.pixels, image.size.x, image.size.y, image.pixelSize, image.paletteId, &pl->originX, &pl->originY, &pl->color, &pl->brightMul); } } // Average alpha? if(context == TC_SPRITE_DIFFUSE || context == TC_UI) { averagealpha_analysis_t *aa = reinterpret_cast(tex.analysisDataPointer(Texture::AverageAlphaAnalysis)); bool firstInit = (!aa); if(firstInit) { aa = (averagealpha_analysis_t *) M_Malloc(sizeof(*aa)); tex.setAnalysisDataPointer(Texture::AverageAlphaAnalysis, aa); } if(firstInit || forceUpdate) { if(!image.paletteId) { FindAverageAlpha(image.pixels, image.size.x, image.size.y, image.pixelSize, &aa->alpha, &aa->coverage); } else { if(image.flags & IMGF_IS_MASKED) { FindAverageAlphaIdx(image.pixels, image.size.x, image.size.y, &aa->alpha, &aa->coverage); } else { // It has no mask, so it must be opaque. aa->alpha = 1; aa->coverage = 0; } } } } // Average color for sky ambient color? if(context == TC_SKYSPHERE_DIFFUSE) { averagecolor_analysis_t *ac = reinterpret_cast(tex.analysisDataPointer(Texture::AverageColorAnalysis)); bool firstInit = (!ac); if(firstInit) { ac = (averagecolor_analysis_t *) M_Malloc(sizeof(*ac)); tex.setAnalysisDataPointer(Texture::AverageColorAnalysis, ac); } if(firstInit || forceUpdate) { if(0 == image.paletteId) { FindAverageColor(image.pixels, image.size.x, image.size.y, image.pixelSize, &ac->color); } else { FindAverageColorIdx(image.pixels, image.size.x, image.size.y, App_ResourceSystem().colorPalette(image.paletteId), false, &ac->color); } } } // Amplified average color for plane glow? if(context == TC_MAPSURFACE_DIFFUSE) { averagecolor_analysis_t *ac = reinterpret_cast(tex.analysisDataPointer(Texture::AverageColorAmplifiedAnalysis)); bool firstInit = (!ac); if(firstInit) { ac = (averagecolor_analysis_t *) M_Malloc(sizeof(*ac)); tex.setAnalysisDataPointer(Texture::AverageColorAmplifiedAnalysis, ac); } if(firstInit || forceUpdate) { if(0 == image.paletteId) { FindAverageColor(image.pixels, image.size.x, image.size.y, image.pixelSize, &ac->color); } else { FindAverageColorIdx(image.pixels, image.size.x, image.size.y, App_ResourceSystem().colorPalette(image.paletteId), false, &ac->color); } Vector3f color(ac->color.rgb); R_AmplifyColor(color); for(int i = 0; i < 3; ++i) { ac->color.rgb[i] = color[i]; } } } // Average top line color for sky sphere fadeout? if(context == TC_SKYSPHERE_DIFFUSE) { averagecolor_analysis_t *ac = reinterpret_cast(tex.analysisDataPointer(Texture::AverageTopColorAnalysis)); bool firstInit = (!ac); if(firstInit) { ac = (averagecolor_analysis_t *) M_Malloc(sizeof(*ac)); tex.setAnalysisDataPointer(Texture::AverageTopColorAnalysis, ac); } if(firstInit || forceUpdate) { if(0 == image.paletteId) { FindAverageLineColor(image.pixels, image.size.x, image.size.y, image.pixelSize, 0, &ac->color); } else { FindAverageLineColorIdx(image.pixels, image.size.x, image.size.y, 0, App_ResourceSystem().colorPalette(image.paletteId), false, &ac->color); } } } // Average bottom line color for sky sphere fadeout? if(context == TC_SKYSPHERE_DIFFUSE) { averagecolor_analysis_t *ac = reinterpret_cast(tex.analysisDataPointer(Texture::AverageBottomColorAnalysis)); bool firstInit = (!ac); if(firstInit) { ac = (averagecolor_analysis_t *) M_Malloc(sizeof(*ac)); tex.setAnalysisDataPointer(Texture::AverageBottomColorAnalysis, ac); } if(firstInit || forceUpdate) { if(0 == image.paletteId) { FindAverageLineColor(image.pixels, image.size.x, image.size.y, image.pixelSize, image.size.y - 1, &ac->color); } else { FindAverageLineColorIdx(image.pixels, image.size.x, image.size.y, image.size.y - 1, App_ResourceSystem().colorPalette(image.paletteId), false, &ac->color); } } } } uint Texture::Variant::prepare() { LOG_AS("TextureVariant::prepare"); // Have we already prepared this? if(isPrepared()) return d->glTexName; // Load the source image data. image_t image; res::Source source = GL_LoadSourceImage(image, d->texture, d->spec); if(source == res::None) return 0; // Do we need to perform any image pixel data analyses? if(d->spec.type == TST_GENERAL) { performImageAnalyses(image, d->spec.variant.context, d->texture, true /*force update*/); } // Are we preparing a new GL texture? if(d->glTexName == 0) { // Acquire a new GL texture name. d->glTexName = GL_GetReservedTextureName(); // Record the source of the image. d->texSource = source; } // Prepare texture content for uploading. texturecontent_t c; GL_PrepareTextureContent(c, d->glTexName, image, d->spec, d->texture.manifest()); /** * Calculate GL texture coordinates based on the image dimensions. The * coordinates are calculated as width / CeilPow2(width), or 1 if larger * than the maximum texture size. * * @todo fixme: Image dimensions may not be the same as the uploaded * texture - defer this logic until all processing has been completed. */ if((c.flags & TXCF_UPLOAD_ARG_NOSTRETCH) && (!GL_state.features.texNonPowTwo || (c.flags & TXCF_MIPMAP))) { d->s = image.size.x / float( de::ceilPow2(image.size.x) ); d->t = image.size.y / float( de::ceilPow2(image.size.y) ); } else { d->s = 1; d->t = 1; } if(image.flags & IMGF_IS_MASKED) { d->flags |= TextureVariant::Masked; } // Submit the content for uploading (possibly deferred). gl::UploadMethod uploadMethod = GL_ChooseUploadMethod(&c); GL_UploadTextureContent(c, uploadMethod); LOGDEV_RES_VERBOSE("Prepared \"%s\" variant (glName:%u)%s") << d->texture.manifest().composeUri() << uint(d->glTexName) << (uploadMethod == gl::Immediate? " while not busy!" : ""); LOGDEV_RES_VERBOSE(" Content: %s") << Image_Description(image); LOGDEV_RES_VERBOSE(" Specification %p: %s") << &d->spec << d->spec.asText(); // Are we setting the logical dimensions to the pixel dimensions // of the source image? if(d->texture.width() == 0 && d->texture.height() == 0) { LOG_RES_VERBOSE("World dimensions for \"%s\" taken from image pixels %s") << d->texture.manifest().composeUri() << image.size.asText(); d->texture.setDimensions(image.size.toVector2i()); } // We're done with the image data. Image_ClearPixelData(image); return d->glTexName; } void Texture::Variant::release() { if(!isPrepared()) return; glDeleteTextures(1, (GLuint const *) &d->glTexName); d->glTexName = 0; } Texture &Texture::Variant::base() const { return d->texture; } TextureVariantSpec const &Texture::Variant::spec() const { return d->spec; } res::Source Texture::Variant::source() const { return d->texSource; } String Texture::Variant::sourceDescription() const { if(d->texSource == res::Original) return "original"; if(d->texSource == res::External) return "external"; return "none"; } Texture::Variant::Flags Texture::Variant::flags() const { return d->flags; } void Texture::Variant::glCoords(float *outS, float *outT) const { if(outS) *outS = d->s; if(outT) *outT = d->t; } uint Texture::Variant::glName() const { return d->glTexName; } doomsday-stable-1.15.7/doomsday/client/src/resource/materialtexturelayer.cpp0000664000175000017500000000770112641367670026674 0ustar jaakkojaakko/** @file materialtexturelayer.cpp Logical material, texture layer. * * @authors Copyright © 2011-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "resource/materialtexturelayer.h" #include #include "dd_main.h" #include "r_util.h" // R_NameForBlendMode using namespace de; MaterialTextureLayer::AnimationStage::AnimationStage(de::Uri const &texture, int tics, float variance, float glowStrength, float glowStrengthVariance, Vector2f const origin, de::Uri const &maskTexture, Vector2f const &maskDimensions, blendmode_t blendMode, float opacity) : Record() , Stage(tics, variance) { resetToDefaults(); set("origin", new ArrayValue(origin)); set("texture", texture.compose()); set("maskTexture", maskTexture.compose()); set("maskDimensions", new ArrayValue(maskDimensions)); set("blendMode", blendMode); set("opacity", opacity); set("glowStrength", glowStrength); set("glowStrengthVariance", glowStrengthVariance); } MaterialTextureLayer::AnimationStage::AnimationStage(AnimationStage const &other) : Record(other) , Stage(other) {} MaterialTextureLayer::AnimationStage::~AnimationStage() {} void MaterialTextureLayer::AnimationStage::resetToDefaults() { addArray ("origin", new ArrayValue(Vector2f(0, 0))); addText ("texture", ""); addText ("maskTexture", ""); addArray ("maskDimensions", new ArrayValue(Vector2f(0, 0))); addNumber("blendMode", BM_NORMAL); addNumber("opacity", 1); addNumber("glowStrength", 0); addNumber("glowStrengthVariance", 0); } MaterialTextureLayer::AnimationStage * MaterialTextureLayer::AnimationStage::fromDef(Record const &stageDef) { return new AnimationStage(de::Uri(stageDef.gets("texture"), RC_NULL), stageDef.geti("tics"), stageDef.getf("variance"), stageDef.getf("glowStrength"), stageDef.getf("glowStrengthVariance"), Vector2f(stageDef.geta("texOrigin"))); } String MaterialTextureLayer::AnimationStage::description() const { /// @todo Record::asText() formatting is not intended for end users. return asText(); } // ------------------------------------------------------------------------------------ MaterialTextureLayer *MaterialTextureLayer::fromDef(Record const &definition) { defn::MaterialLayer layerDef(definition); auto *layer = new MaterialTextureLayer(); for(int i = 0; i < layerDef.stageCount(); ++i) { layer->_stages.append(AnimationStage::fromDef(layerDef.stage(i))); } return layer; } int MaterialTextureLayer::addStage(MaterialTextureLayer::AnimationStage const &stageToCopy) { _stages.append(new AnimationStage(stageToCopy)); return _stages.count() - 1; } MaterialTextureLayer::AnimationStage &MaterialTextureLayer::stage(int index) const { return static_cast(Layer::stage(index)); } bool MaterialTextureLayer::hasGlow() const { for(int i = 0; i < stageCount(); ++i) { if(stage(i).getf("glowStrength") > .0001f) return true; } return false; } String MaterialTextureLayer::describe() const { return "Texture layer"; } doomsday-stable-1.15.7/doomsday/client/src/resource/image.cpp0000664000175000017500000007074412641367670023511 0ustar jaakkojaakko/** @file image.cpp Image objects and related routines. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "resource/image.h" #include "de_console.h" #include "de_filesys.h" #include "dd_main.h" #include "m_misc.h" #ifdef __CLIENT__ # include "resource/compositetexture.h" # include "resource/patch.h" # include "resource/pcx.h" # include "resource/tga.h" # include "gl/gl_tex.h" # include "render/rend_main.h" // misc global vars awaiting new home # include # include # include #endif #include #include #ifndef DENG2_QT_4_7_OR_NEWER // older than 4.7? # define constBits bits #endif using namespace de; using namespace res; #ifdef __CLIENT__ struct GraphicFileType { /// Symbolic name of the resource type. String name; /// Known file extension. String ext; bool (*interpretFunc)(FileHandle &hndl, String filePath, image_t &img); char const *(*getLastErrorFunc)(); ///< Can be NULL. }; static bool interpretPcx(FileHandle &hndl, String /*filePath*/, image_t &img) { Image_Init(img); img.pixels = PCX_Load(hndl, img.size, img.pixelSize); return (0 != img.pixels); } static bool interpretJpg(FileHandle &hndl, String /*filePath*/, image_t &img) { return Image_LoadFromFileWithFormat(img, "JPG", hndl); } static bool interpretPng(FileHandle &hndl, String /*filePath*/, image_t &img) { return Image_LoadFromFileWithFormat(img, "PNG", hndl); } static bool interpretTga(FileHandle &hndl, String /*filePath*/, image_t &img) { Image_Init(img); img.pixels = TGA_Load(hndl, img.size, img.pixelSize); return (0 != img.pixels); } // Graphic resource types. static GraphicFileType const graphicTypes[] = { { "PNG", "png", interpretPng, 0 }, { "JPG", "jpg", interpretJpg, 0 }, // TODO: add alternate "jpeg" extension { "TGA", "tga", interpretTga, TGA_LastError }, { "PCX", "pcx", interpretPcx, PCX_LastError }, { "", "", 0, 0 } // Terminate. }; static GraphicFileType const *guessGraphicFileTypeFromFileName(String fileName) { // The path must have an extension for this. String ext = fileName.fileNameExtension(); if(!ext.isEmpty()) { for(int i = 0; !graphicTypes[i].ext.isEmpty(); ++i) { GraphicFileType const &type = graphicTypes[i]; if(!ext.compareWithoutCase(type.ext)) { return &type; } } } return 0; // Unknown. } static void interpretGraphic(FileHandle &hndl, String filePath, image_t &img) { // Firstly try the interpreter for the guessed resource types. GraphicFileType const *rtypeGuess = guessGraphicFileTypeFromFileName(filePath); if(rtypeGuess) { rtypeGuess->interpretFunc(hndl, filePath, img); } // If not yet interpreted - try each recognisable format in order. if(!img.pixels) { // Try each recognisable format instead. /// @todo Order here should be determined by the resource locator. for(int i = 0; !graphicTypes[i].name.isEmpty(); ++i) { GraphicFileType const *graphicType = &graphicTypes[i]; // Already tried this? if(graphicType == rtypeGuess) continue; graphicTypes[i].interpretFunc(hndl, filePath, img); if(img.pixels) break; } } } /// @return @c true if the file name in @a path ends with the "color key" suffix. static inline bool isColorKeyed(String path) { return path.fileNameWithoutExtension().endsWith("-ck", Qt::CaseInsensitive); } #endif // __CLIENT__ void Image_Init(image_t &img) { img.size = Vector2ui(0, 0); img.pixelSize = 0; img.flags = 0; img.paletteId = 0; img.pixels = 0; } void Image_ClearPixelData(image_t &img) { M_Free(img.pixels); img.pixels = 0; } image_t::Size Image_Size(image_t const &img) { return img.size; } String Image_Description(image_t const &img) { return String("Dimensions:%1 Flags:%2 %3:%4") .arg(img.size.asText()) .arg(img.flags) .arg(0 != img.paletteId? "ColorPalette" : "PixelSize") .arg(0 != img.paletteId? img.paletteId : img.pixelSize); } void Image_ConvertToLuminance(image_t &img, bool retainAlpha) { LOG_AS("Image_ConvertToLuminance"); uint8_t *alphaChannel = 0, *ptr = 0; // Is this suitable? if(0 != img.paletteId || (img.pixelSize < 3 && (img.flags & IMGF_IS_MASKED))) { LOG_RES_WARNING("Unknown paletted/masked image format"); return; } long numPels = img.size.x * img.size.y; // Do we need to relocate the alpha data? if(retainAlpha && img.pixelSize == 4) { // Yes. Take a copy. alphaChannel = reinterpret_cast(M_Malloc(numPels)); ptr = img.pixels; for(long p = 0; p < numPels; ++p, ptr += img.pixelSize) { alphaChannel[p] = ptr[3]; } } // Average the RGB colors. ptr = img.pixels; for(long p = 0; p < numPels; ++p, ptr += img.pixelSize) { int min = de::min(ptr[0], de::min(ptr[1], ptr[2])); int max = de::max(ptr[0], de::max(ptr[1], ptr[2])); img.pixels[p] = (min == max? min : (min + max) / 2); } // Do we need to relocate the alpha data? if(alphaChannel) { std::memcpy(img.pixels + numPels, alphaChannel, numPels); img.pixelSize = 2; M_Free(alphaChannel); return; } img.pixelSize = 1; } void Image_ConvertToAlpha(image_t &img, bool makeWhite) { Image_ConvertToLuminance(img); long total = img.size.x * img.size.y; for(long p = 0; p < total; ++p) { img.pixels[total + p] = img.pixels[p]; if(makeWhite) img.pixels[p] = 255; } img.pixelSize = 2; } bool Image_HasAlpha(image_t const &img) { LOG_AS("Image_HasAlpha"); if(0 != img.paletteId || (img.flags & IMGF_IS_MASKED)) { LOG_RES_WARNING("Unknown paletted/masked image format"); return false; } if(img.pixelSize == 3) { return false; } if(img.pixelSize == 4) { long const numpels = img.size.x * img.size.y; uint8_t const *in = img.pixels; for(long i = 0; i < numpels; ++i, in += 4) { if(in[3] < 255) { return true; } } } return false; } uint8_t *Image_LoadFromFile(image_t &img, FileHandle &file) { #ifdef __CLIENT__ LOG_AS("Image_LoadFromFile"); String filePath = file.file().composePath(); Image_Init(img); interpretGraphic(file, filePath, img); // Still not interpreted? if(!img.pixels) { LOG_RES_XVERBOSE("\"%s\" unrecognized, trying fallback loader...") << NativePath(filePath).pretty(); return 0; // Not a recognised format. It may still be loadable, however. } // How about some color-keying? if(isColorKeyed(filePath)) { uint8_t *out = ApplyColorKeying(img.pixels, img.size.x, img.size.y, img.pixelSize); if(out != img.pixels) { // Had to allocate a larger buffer, free the old and attach the new. M_Free(img.pixels); img.pixels = out; } // Color keying is done; now we have 4 bytes per pixel. img.pixelSize = 4; } // Any alpha pixels? if(Image_HasAlpha(img)) { img.flags |= IMGF_IS_MASKED; } LOG_RES_VERBOSE("Loaded image from file \"%s\", size %s") << NativePath(filePath).pretty() << img.size.asText(); return img.pixels; #else // Server does not load image files. DENG2_UNUSED2(img, file); return NULL; #endif } bool Image_LoadFromFileWithFormat(image_t &img, char const *format, FileHandle &hndl) { #ifdef __CLIENT__ LOG_AS("Image_LoadFromFileWithFormat"); /// @todo There are too many copies made here. It would be best if image_t /// contained an instance of QImage. -jk // It is assumed that file's position stays the same (could be trying multiple interpreters). size_t initPos = hndl.tell(); Image_Init(img); // Load the file contents to a memory buffer. QByteArray data; data.resize(hndl.length() - initPos); hndl.read(reinterpret_cast(data.data()), data.size()); QImage image = QImage::fromData(data, format); if(image.isNull()) { // Back to the original file position. hndl.seek(initPos, SeekSet); return false; } //LOG_TRACE("Loading \"%s\"...") << NativePath(hndl->file().composePath()).pretty(); // Convert paletted images to RGB. if(image.colorCount()) { image = image.convertToFormat(QImage::Format_ARGB32); DENG_ASSERT(!image.colorCount()); DENG_ASSERT(image.depth() == 32); } // Swap the red and blue channels for GL. image = image.rgbSwapped(); img.size = Vector2ui(image.width(), image.height()); img.pixelSize = image.depth() / 8; LOGDEV_RES_VERBOSE("size:%s depth:%i alpha:%b bytes:%i") << img.size.asText() << img.pixelSize << image.hasAlphaChannel() << image.byteCount(); img.pixels = reinterpret_cast(M_MemDup(image.constBits(), image.byteCount())); // Back to the original file position. hndl.seek(initPos, SeekSet); return true; #else // Server does not load image files. DENG2_UNUSED3(img, format, hndl); return false; #endif } bool Image_Save(image_t const &img, char const *filePath) { #ifdef __CLIENT__ // Compose the full path. String fullPath = String(filePath); if(fullPath.isEmpty()) { static int n = 0; fullPath = String("image%1x%2-%3").arg(img.size.x).arg(img.size.y).arg(n++, 3); } if(fullPath.fileNameExtension().isEmpty()) { fullPath += ".png"; } // Swap red and blue channels then save. QImage image = QImage(img.pixels, img.size.x, img.size.y, QImage::Format_ARGB32); image = image.rgbSwapped(); return image.save(NativePath(fullPath)); #else // Server does not save images. DENG2_UNUSED2(img, filePath); return false; #endif } #ifdef __CLIENT__ uint8_t *GL_LoadImage(image_t &image, String nativePath) { try { // Relative paths are relative to the native working directory. String path = (NativePath::workPath() / NativePath(nativePath).expand()).withSeparators('/'); FileHandle &hndl = App_FileSystem().openFile(path, "rb"); uint8_t *pixels = Image_LoadFromFile(image, hndl); App_FileSystem().releaseFile(hndl.file()); delete &hndl; return pixels; } catch(FS1::NotFoundError const&) {} // Ignore error. return 0; // Not loaded. } Source GL_LoadExtImage(image_t &image, char const *_searchPath, gfxmode_t mode) { DENG_ASSERT(_searchPath); try { String foundPath = App_FileSystem().findPath(de::Uri(RC_GRAPHIC, _searchPath), RLF_DEFAULT, App_ResourceClass(RC_GRAPHIC)); // Ensure the found path is absolute. foundPath = App_BasePath() / foundPath; if(GL_LoadImage(image, foundPath)) { // Force it to grayscale? if(mode == LGM_GRAYSCALE_ALPHA || mode == LGM_WHITE_ALPHA) { Image_ConvertToAlpha(image, mode == LGM_WHITE_ALPHA); } else if(mode == LGM_GRAYSCALE) { Image_ConvertToLuminance(image); } return External; } } catch(FS1::NotFoundError const&) {} // Ignore this error. return None; } static dd_bool palettedIsMasked(uint8_t const *pixels, int width, int height) { DENG2_ASSERT(pixels != 0); // Jump to the start of the alpha data. pixels += width * height; for(int i = 0; i < width * height; ++i) { if(255 != pixels[i]) { return true; } } return false; } static Source loadExternalTexture(image_t &image, String encodedSearchPath, String optionalSuffix = "") { // First look for a version with an optional suffix. try { String foundPath = App_FileSystem().findPath(de::Uri(encodedSearchPath + optionalSuffix, RC_GRAPHIC), RLF_DEFAULT, App_ResourceClass(RC_GRAPHIC)); // Ensure the found path is absolute. foundPath = App_BasePath() / foundPath; return GL_LoadImage(image, foundPath)? External : None; } catch(FS1::NotFoundError const&) {} // Ignore this error. // Try again without the suffix? if(!optionalSuffix.empty()) { try { String foundPath = App_FileSystem().findPath(de::Uri(encodedSearchPath, RC_GRAPHIC), RLF_DEFAULT, App_ResourceClass(RC_GRAPHIC)); // Ensure the found path is absolute. foundPath = App_BasePath() / foundPath; return GL_LoadImage(image, foundPath)? External : None; } catch(FS1::NotFoundError const&) {} // Ignore this error. } return None; } /** * Draw the component image @a src into the composite @a dst. * * @param dst The composite buffer (drawn to). * @param dstDimensions Pixel dimensions of @a dst. * @param src The component image to be composited (read from). * @param srcDimensions Pixel dimensions of @a src. * @param origin Coordinates (topleft) in @a dst to draw @a src. * * @todo Optimize: Should be redesigned to composite whole rows -ds */ static void compositePaletted(dbyte *dst, Vector2ui const &dstDimensions, IByteArray const &src, Vector2i const &srcDimensions, Vector2i const &origin) { if(dstDimensions == Vector2ui()) return; if(srcDimensions <= Vector2i()) return; int const srcW = srcDimensions.x; int const srcH = srcDimensions.y; size_t const srcPels = srcW * srcH; int const dstW = dstDimensions.x; int const dstH = dstDimensions.y; size_t const dstPels = dstW * dstH; int dstX, dstY; for(int srcY = 0; srcY < srcH; ++srcY) for(int srcX = 0; srcX < srcW; ++srcX) { dstX = origin.x + srcX; dstY = origin.y + srcY; if(dstX < 0 || dstX >= dstW) continue; if(dstY < 0 || dstY >= dstH) continue; dbyte srcAlpha; src.get(srcY * srcW + srcX + srcPels, &srcAlpha, 1); if(srcAlpha) { src.get(srcY * srcW + srcX, &dst[dstY * dstW + dstX], 1); dst[dstY * dstW + dstX + dstPels] = srcAlpha; } } } /// Returns a palette translation id for the given class and map. /// Note that a zero-length id is returned when @a tclass =0 and @a tmap =0 static String toTranslationId(int tclass, int tmap) { #define NUM_TRANSLATION_CLASSES 3 #define NUM_TRANSLATION_MAPS_PER_CLASS 7 // Is translation unnecessary? if(!tclass && !tmap) return String(); int trans = de::max(0, NUM_TRANSLATION_MAPS_PER_CLASS * tclass + tmap - 1); LOGDEV_RES_XVERBOSE("tclass=%i tmap=%i => TransPal# %i") << tclass << tmap << trans; return String::number(trans); #undef NUM_TRANSLATION_MAPS_PER_CLASS #undef NUM_TRANSLATION_CLASSES } static Block loadAndTranslatePatch(IByteArray const &data, colorpaletteid_t palId, int tclass = 0, int tmap = 0) { ColorPalette &palette = App_ResourceSystem().colorPalette(palId); if(ColorPaletteTranslation const *xlat = palette.translation(toTranslationId(tclass, tmap))) { return Patch::load(data, *xlat, Patch::ClipToLogicalDimensions); } else { return Patch::load(data, Patch::ClipToLogicalDimensions); } } static Source loadPatch(image_t &image, FileHandle &hndl, int tclass = 0, int tmap = 0, int border = 0) { LOG_AS("image_t::loadPatch"); if(Image_LoadFromFile(image, hndl)) { return External; } File1 &file = hndl.file(); ByteRefArray fileData = ByteRefArray(file.cache(), file.size()); // A DOOM patch? if(Patch::recognize(fileData)) { try { colorpaletteid_t colorPaletteId = App_ResourceSystem().defaultColorPalette(); Block patchImg = loadAndTranslatePatch(fileData, colorPaletteId, tclass, tmap); PatchMetadata info = Patch::loadMetadata(fileData); Image_Init(image); image.size = Vector2ui(info.logicalDimensions.x + border*2, info.logicalDimensions.y + border*2); image.pixelSize = 1; image.paletteId = colorPaletteId; image.pixels = (uint8_t *) M_Calloc(2 * image.size.x * image.size.y); compositePaletted(image.pixels, image.size, patchImg, info.logicalDimensions, Vector2i(border, border)); if(palettedIsMasked(image.pixels, image.size.x, image.size.y)) { image.flags |= IMGF_IS_MASKED; } return Original; } catch(IByteArray::OffsetError const &) { LOG_RES_WARNING("File \"%s:%s\" does not appear to be a valid Patch") << NativePath(file.container().composePath()).pretty() << NativePath(file.composePath()).pretty(); } } file.unlock(); return None; } static Source loadPatchComposite(image_t &image, Texture const &tex, bool maskZero = false, bool useZeroOriginIfOneComponent = false) { LOG_AS("image_t::loadPatchComposite"); Image_Init(image); image.pixelSize = 1; image.size = Vector2ui(tex.width(), tex.height()); image.paletteId = App_ResourceSystem().defaultColorPalette(); image.pixels = (uint8_t *) M_Calloc(2 * image.size.x * image.size.y); CompositeTexture const &texDef = *reinterpret_cast(tex.userDataPointer()); DENG2_FOR_EACH_CONST(CompositeTexture::Components, i, texDef.components()) { File1 &file = App_FileSystem().lump(i->lumpNum()); ByteRefArray fileData = ByteRefArray(file.cache(), file.size()); // A DOOM patch? if(Patch::recognize(fileData)) { try { Patch::Flags loadFlags; if(maskZero) loadFlags |= Patch::MaskZero; Block patchImg = Patch::load(fileData, loadFlags); PatchMetadata info = Patch::loadMetadata(fileData); Vector2i origin = i->origin(); if(useZeroOriginIfOneComponent && texDef.componentCount() == 1) { origin = Vector2i(0, 0); } // Draw the patch in the buffer. compositePaletted(image.pixels, image.size, patchImg, info.dimensions, origin); } catch(IByteArray::OffsetError const &) {} // Ignore this error. } file.unlock(); } if(maskZero || palettedIsMasked(image.pixels, image.size.x, image.size.y)) { image.flags |= IMGF_IS_MASKED; } // For debug: // GL_DumpImage(&image, Str_Text(GL_ComposeCacheNameForTexture(tex))); return Original; } static Source loadFlat(image_t &image, FileHandle &hndl) { if(Image_LoadFromFile(image, hndl)) { return External; } // A DOOM flat. Image_Init(image); /// @todo not all flats are 64x64! image.size = Vector2ui(64, 64); image.pixelSize = 1; image.paletteId = App_ResourceSystem().defaultColorPalette(); File1 &file = hndl.file(); size_t fileLength = hndl.length(); size_t bufSize = de::max(fileLength, (size_t) image.size.x * image.size.y); image.pixels = (uint8_t *) M_Malloc(bufSize); if(fileLength < bufSize) { std::memset(image.pixels, 0, bufSize); } // Load the raw image data. file.read(image.pixels, 0, fileLength); return Original; } static Source loadDetail(image_t &image, FileHandle &hndl) { if(Image_LoadFromFile(image, hndl)) { return Original; } // It must be an old-fashioned "raw" image. Image_Init(image); // How big is it? File1 &file = hndl.file(); size_t fileLength = hndl.length(); switch(fileLength) { case 256 * 256: image.size.x = image.size.y = 256; break; case 128 * 128: image.size.x = image.size.y = 128; break; case 64 * 64: image.size.x = image.size.y = 64; break; default: throw Error("image_t::loadDetail", "Must be 256x256, 128x128 or 64x64."); } image.pixelSize = 1; size_t bufSize = (size_t) image.size.x * image.size.y; image.pixels = (uint8_t *) M_Malloc(bufSize); if(fileLength < bufSize) { std::memset(image.pixels, 0, bufSize); } // Load the raw image data. file.read(image.pixels, fileLength); return Original; } Source GL_LoadSourceImage(image_t &image, Texture const &tex, TextureVariantSpec const &spec) { de::FS1 &fileSys = App_FileSystem(); Source source = None; variantspecification_t const &vspec = spec.variant; if(!tex.manifest().schemeName().compareWithoutCase("Textures")) { // Attempt to load an external replacement for this composite texture? if(!noHighResTex && (loadExtAlways || highResWithPWAD || !tex.isFlagged(Texture::Custom))) { // First try the textures scheme. de::Uri uri = tex.manifest().composeUri(); source = loadExternalTexture(image, uri.compose(), "-ck"); } if(source == None) { if(TC_SKYSPHERE_DIFFUSE != vspec.context) { source = loadPatchComposite(image, tex); } else { bool const zeroMask = (vspec.flags & TSF_ZEROMASK) != 0; bool const useZeroOriginIfOneComponent = true; source = loadPatchComposite(image, tex, zeroMask, useZeroOriginIfOneComponent); } } } else if(!tex.manifest().schemeName().compareWithoutCase("Flats")) { // Attempt to load an external replacement for this flat? if(!noHighResTex && (loadExtAlways || highResWithPWAD || !tex.isFlagged(Texture::Custom))) { // First try the flats scheme. de::Uri uri = tex.manifest().composeUri(); source = loadExternalTexture(image, uri.compose(), "-ck"); if(source == None) { // How about the old-fashioned "flat-name" in the textures scheme? source = loadExternalTexture(image, "Textures:flat-" + uri.path().toStringRef(), "-ck"); } } if(source == None) { if(tex.manifest().hasResourceUri()) { de::Uri resourceUri = tex.manifest().resourceUri(); if(!resourceUri.scheme().compareWithoutCase("LumpIndex")) { try { lumpnum_t const lumpNum = resourceUri.path().toString().toInt(); FileHandle &hndl = fileSys.openLump(fileSys.lump(lumpNum)); source = loadFlat(image, hndl); fileSys.releaseFile(hndl.file()); delete &hndl; } catch(LumpIndex::NotFoundError const&) {} // Ignore this error. } } } } else if(!tex.manifest().schemeName().compareWithoutCase("Patches")) { int tclass = 0, tmap = 0; if(vspec.flags & TSF_HAS_COLORPALETTE_XLAT) { tclass = vspec.tClass; tmap = vspec.tMap; } // Attempt to load an external replacement for this patch? if(!noHighResTex && (loadExtAlways || highResWithPWAD || !tex.isFlagged(Texture::Custom))) { de::Uri uri = tex.manifest().composeUri(); source = loadExternalTexture(image, uri.compose(), "-ck"); } if(source == None) { if(tex.manifest().hasResourceUri()) { de::Uri resourceUri = tex.manifest().resourceUri(); if(!resourceUri.scheme().compareWithoutCase("LumpIndex")) { try { lumpnum_t const lumpNum = resourceUri.path().toString().toInt(); FileHandle &hndl = fileSys.openLump(fileSys.lump(lumpNum)); source = loadPatch(image, hndl, tclass, tmap, vspec.border); fileSys.releaseFile(hndl.file()); delete &hndl; } catch(LumpIndex::NotFoundError const&) {} // Ignore this error. } } } } else if(!tex.manifest().schemeName().compareWithoutCase("Sprites")) { int tclass = 0, tmap = 0; if(vspec.flags & TSF_HAS_COLORPALETTE_XLAT) { tclass = vspec.tClass; tmap = vspec.tMap; } // Attempt to load an external replacement for this sprite? if(!noHighResPatches) { de::Uri uri = tex.manifest().composeUri(); // Prefer psprite or translated versions if available. if(TC_PSPRITE_DIFFUSE == vspec.context) { source = loadExternalTexture(image, "Patches:" + uri.path() + "-hud", "-ck"); } else if(tclass || tmap) { source = loadExternalTexture(image, "Patches:" + uri.path() + String("-table%1%2").arg(tclass).arg(tmap), "-ck"); } if(!source) { source = loadExternalTexture(image, "Patches:" + uri.path(), "-ck"); } } if(source == None) { if(tex.manifest().hasResourceUri()) { de::Uri resourceUri = tex.manifest().resourceUri(); if(!resourceUri.scheme().compareWithoutCase("LumpIndex")) { try { lumpnum_t const lumpNum = resourceUri.path().toString().toInt(); FileHandle &hndl = fileSys.openLump(fileSys.lump(lumpNum)); source = loadPatch(image, hndl, tclass, tmap, vspec.border); fileSys.releaseFile(hndl.file()); delete &hndl; } catch(LumpIndex::NotFoundError const&) {} // Ignore this error. } } } } else if(!tex.manifest().schemeName().compareWithoutCase("Details")) { if(tex.manifest().hasResourceUri()) { de::Uri resourceUri = tex.manifest().resourceUri(); if(resourceUri.scheme().compareWithoutCase("Lumps")) { source = loadExternalTexture(image, resourceUri.compose()); } else { lumpnum_t const lumpNum = fileSys.lumpNumForName(resourceUri.path()); try { File1 &lump = fileSys.lump(lumpNum); FileHandle &hndl = fileSys.openLump(lump); source = loadDetail(image, hndl); fileSys.releaseFile(hndl.file()); delete &hndl; } catch(LumpIndex::NotFoundError const&) {} // Ignore this error. } } } else { if(tex.manifest().hasResourceUri()) { de::Uri resourceUri = tex.manifest().resourceUri(); source = loadExternalTexture(image, resourceUri.compose()); } } return source; } #endif // __CLIENT__ doomsday-stable-1.15.7/doomsday/client/src/resource/compositebitmapfont.cpp0000664000175000017500000001560512641367670026510 0ustar jaakkojaakko/** @file compositebitmapfont.cpp Composite bitmap font. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "clientapp.h" #include "resource/compositebitmapfont.h" #include "api_resource.h" // R_GetPatchInfo #include "dd_main.h" // App_ResourceSystem(), isDedicated #include "sys_system.h" // novideo #include "gl/gl_texmanager.h" // GL_TextureVariantSpec using namespace de; DENG2_PIMPL(CompositeBitmapFont) { ded_compositefont_t *def; /// Definition on which "this" font is derived (if any). bool needGLInit; /// Font metrics. int leading; int ascent; int descent; Glyph glyphs[MAX_CHARS]; Glyph missingGlyph; Instance(Public *i) : Base(i) , def(0) , needGLInit(true) , leading(0) , ascent(0) , descent(0) { zap(glyphs); zap(missingGlyph); self._flags |= AbstractFont::Colorize; } ~Instance() { self.glDeinit(); } Glyph &glyph(uchar ch) { //if(ch >= MAX_CHARS) return missingGlyph; if(!glyphs[ch].haveSourceImage) return missingGlyph; return glyphs[ch]; } }; CompositeBitmapFont::CompositeBitmapFont(FontManifest &manifest) : AbstractFont(manifest), d(new Instance(this)) {} int CompositeBitmapFont::ascent() { glInit(); return d->ascent; } int CompositeBitmapFont::descent() { glInit(); return d->descent; } int CompositeBitmapFont::lineSpacing() { glInit(); return d->leading; } Rectanglei const &CompositeBitmapFont::glyphPosCoords(uchar ch) { glInit(); return d->glyph(ch).geometry; } Rectanglei const &CompositeBitmapFont::glyphTexCoords(uchar /*ch*/) { static Rectanglei coords(Vector2i(0, 0), Vector2i(1, 1)); glInit(); return coords; } uint CompositeBitmapFont::glyphTextureBorder(uchar ch) { glInit(); return d->glyph(ch).border; } TextureVariant *CompositeBitmapFont::glyphTexture(uchar ch) { glInit(); return d->glyph(ch).tex; } patchid_t CompositeBitmapFont::glyphPatch(uchar ch) { glInit(); return d->glyph(ch).patch; } void CompositeBitmapFont::glyphSetPatch(uchar ch, String encodedPatchName) { //if(ch >= MAX_CHARS) return; d->glyphs[ch].patch = App_ResourceSystem().declarePatch(encodedPatchName); // We'll need to rebuild the prepared GL resources. d->needGLInit = true; } /// @todo fixme: Do not assume the texture-usage context is @c TC_UI. static TextureVariantSpec const &glyphTextureSpec() { return ClientApp::resourceSystem().textureSpec(TC_UI, TSF_MONOCHROME | TSF_UPSCALE_AND_SHARPEN, 0, 0, 0, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 0, -3, 0, false, false, false, false); } void CompositeBitmapFont::glInit() { LOG_AS("CompositeBitmapFont"); if(!d->needGLInit) return; if(novideo || isDedicated || BusyMode_Active()) return; glDeinit(); ResourceSystem &resSys = App_ResourceSystem(); int foundGlyphs = 0; Vector2ui avgSize; for(int i = 0; i < MAX_CHARS; ++i) { Glyph *ch = &d->glyphs[i]; patchid_t patch = ch->patch; ch->haveSourceImage = patch != 0; if(!ch->haveSourceImage) continue; try { Texture &tex = resSys.textureScheme("Patches").findByUniqueId(patch).texture(); ch->tex = tex.prepareVariant(glyphTextureSpec()); ch->geometry = Rectanglei::fromSize(tex.origin(), tex.dimensions().toVector2ui()); ch->border = 0; if(ch->tex && ch->tex->source() == res::Original) { // Upscale & Sharpen will have been applied. ch->border = 1; } avgSize += ch->geometry.size(); ++foundGlyphs; } catch(TextureManifest::MissingTextureError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ", ignoring."); } catch(TextureScheme::NotFoundError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ", ignoring."); } } d->missingGlyph.geometry.setSize(avgSize / (foundGlyphs? foundGlyphs : 1)); // We have prepared all patches. d->needGLInit = false; } void CompositeBitmapFont::glDeinit() { if(novideo || isDedicated) return; d->needGLInit = true; if(BusyMode_Active()) return; for(int i = 0; i < 256; ++i) { Glyph *ch = &d->glyphs[i]; if(!ch->tex) continue; ch->tex->release(); ch->tex = 0; } } CompositeBitmapFont *CompositeBitmapFont::fromDef(FontManifest &manifest, ded_compositefont_t const &def) // static { LOG_AS("CompositeBitmapFont::fromDef"); CompositeBitmapFont *font = new CompositeBitmapFont(manifest); font->setDefinition(const_cast(&def)); for(int i = 0; i < def.charMap.size(); ++i) { if(!def.charMap[i].path) continue; try { String glyphPatchPath = def.charMap[i].path->resolved(); font->glyphSetPatch(def.charMap[i].ch, glyphPatchPath); } catch(de::Uri::ResolveError const &er) { LOG_RES_WARNING(er.asText()); } } // Lets try to prepare it right away. font->glInit(); return font; } ded_compositefont_t *CompositeBitmapFont::definition() const { return d->def; } void CompositeBitmapFont::setDefinition(ded_compositefont_t *newDef) { d->def = newDef; } void CompositeBitmapFont::rebuildFromDef(ded_compositefont_t const &newDef) { LOG_AS("CompositeBitmapFont::rebuildFromDef"); setDefinition(const_cast(&newDef)); if(!d->def) return; for(int i = 0; i < d->def->charMap.size(); ++i) { if(!d->def->charMap[i].path) continue; try { String glyphPatchPath = d->def->charMap[i].path->resolved(); glyphSetPatch(d->def->charMap[i].ch, glyphPatchPath); } catch(de::Uri::ResolveError const& er) { LOG_RES_WARNING(er.asText()); } } } doomsday-stable-1.15.7/doomsday/client/src/resource/mapdef.cpp0000664000175000017500000000437112641367670023654 0ustar jaakkojaakko/** @file mapdef.cpp Map asset/resource definition/manifest. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "resource/mapdef.h" #include using namespace de; MapDef::MapDef(PathTree::NodeArgs const &args) : Node(args), Record() {} String MapDef::description(de::Uri::ComposeAsTextFlags uriCompositionFlags) const { String info = String("%1").arg(composeUri().compose(uriCompositionFlags | de::Uri::DecodePath), ( uriCompositionFlags.testFlag(de::Uri::OmitScheme)? -14 : -22 ) ); if(_sourceFile) { info += String(" " _E(C) "\"%1\"" _E(.)).arg(NativePath(sourceFile()->composePath()).pretty()); } return info; } String MapDef::composeUniqueId(Game const ¤tGame) const { return String("%1|%2|%3|%4") .arg(gets("id").fileNameWithoutExtension()) .arg(sourceFile()->name().fileNameWithoutExtension()) .arg(sourceFile()->hasCustom()? "pwad" : "iwad") .arg(currentGame.identityKey()) .toLower(); } MapDef &MapDef::setSourceFile(File1 *newSourceFile) { _sourceFile = newSourceFile; return *this; } File1 *MapDef::sourceFile() const { return _sourceFile; } MapDef &MapDef::setRecognizer(Id1MapRecognizer *newRecognizer) { _recognized.reset(newRecognizer); return *this; } Id1MapRecognizer const &MapDef::recognizer() const { DENG2_ASSERT(!_recognized.isNull()); return *_recognized; } doomsday-stable-1.15.7/doomsday/client/src/resource/resourcesystem.cpp0000664000175000017500000044425112641367670025521 0ustar jaakkojaakko/** @file resourcesystem.cpp Resource subsystem. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2005-2015 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "resource/resourcesystem.h" #ifdef __CLIENT__ # include "clientapp.h" # include "ui/progress.h" # include "sys_system.h" // novideo #endif #include "def_main.h" #include "dd_main.h" #include "dd_def.h" #include "resource/compositetexture.h" #include "resource/patch.h" #include "resource/patchname.h" #ifdef __CLIENT__ # include "gl/gl_tex.h" # include "gl/gl_texmanager.h" # include "render/rend_model.h" # include "render/rend_particle.h" // Rend_ParticleReleaseSystemTextures // For smart caching logics: # include "network/net_demo.h" // playback # include "render/rend_main.h" // Rend_MapSurfaceMaterialSpec # include "render/billboard.h" // Rend_SpriteMaterialSpec # include "render/skydrawable.h" # include "world/worldsystem.h" # include "world/map.h" # include "world/p_object.h" # include "world/sky.h" # include "world/thinkers.h" # include "Sector" # include "Surface" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __CLIENT__ # include # include # include #endif #include /// @todo remove me #include #include #include #include namespace de { namespace internal { /** * Compose the path to the data resource. * @note We do not use the lump name, instead we use the logical lump index * in the global LumpIndex. This is necessary because of the way id tech 1 * manages graphic references in animations (intermediate frames are chosen * by their 'original indices' rather than by name). */ static inline de::Uri composeLumpIndexResourceUrn(lumpnum_t lumpNum) { return de::Uri("LumpIndex", Path(String("%1").arg(lumpNum))); } /// Returns a value in the range [0..Sprite::max_angles] if @a rotCode can be /// interpreted as a sprite rotation number; otherwise @c -1 static int toSpriteRotation(QChar rotCode) { int rot = -1; // Unknown. if(rotCode.isDigit()) { rot = rotCode.digitValue(); } else if(rotCode.isLetter()) { char charCodeLatin1 = rotCode.toUpper().toLatin1(); if(charCodeLatin1 >= 'A') { rot = charCodeLatin1 - 'A' + 10; } } if(rot > Sprite::max_angles) { rot = -1; } else if(rot > 0) { if(rot <= Sprite::max_angles / 2) { rot = (rot - 1) * 2 + 1; } else { rot = (rot - 9) * 2 + 2; } } return rot; } /// Returns @c true iff @a name is a well-formed sprite name. static bool validateSpriteName(String name) { if(name.length() < 6) return false; // Character at position 5 is a rotation number. if(toSpriteRotation(name.at(5)) < 0) { return false; } // If defined, the character at position 7 is also a rotation number. if(name.length() > 7) { if(toSpriteRotation(name.at(7)) < 0) { return false; } } return true; } /// Ensure a texture has been derived for @a manifest. static Texture *deriveTexture(TextureManifest &manifest) { LOG_AS("deriveTexture"); Texture *tex = manifest.derive(); if(!tex) { LOGDEV_RES_WARNING("Failed to derive a Texture for \"%s\", ignoring") << manifest.composeUri(); } return tex; } static QList collectPatchCompositeDefinitionFiles() { QList result; /* * Precedence order of definitions is defined by id tech1 which processes * the TEXTURE1/2 lumps in the following order: * * (last)TEXTURE2 > (last)TEXTURE1 */ LumpIndex const &index = App_FileSystem().nameIndex(); lumpnum_t firstTexLump = App_FileSystem().lumpNumForName("TEXTURE1"); lumpnum_t secondTexLump = App_FileSystem().lumpNumForName("TEXTURE2"); /* * Also process all other lumps named TEXTURE1/2. */ for(int i = 0; i < index.size(); ++i) { File1 &file = index[i]; // Will this be processed anyway? if(i == firstTexLump) continue; if(i == secondTexLump) continue; String fileName = file.name().fileNameWithoutExtension(); if(fileName.compareWithoutCase("TEXTURE1") && fileName.compareWithoutCase("TEXTURE2")) { continue; } result.append(&file); } if(firstTexLump >= 0) { result.append(&index[firstTexLump]); } if(secondTexLump >= 0) { result.append(&index[secondTexLump]); } return result; } typedef QList CompositeTextures; typedef QList PatchNames; static PatchNames readPatchNames(File1 &file) { LOG_AS("readPatchNames"); PatchNames names; if(file.size() < 4) { LOG_RES_WARNING("File \"%s\" does not appear to be valid PNAMES data") << NativePath(file.composeUri().asText()).pretty(); return names; } ByteRefArray lumpData(file.cache(), file.size()); de::Reader from(lumpData); // The data begins with the total number of patch names. dint32 numNames; from >> numNames; // Followed by the names (eight character ASCII strings). if(numNames > 0) { if((unsigned) numNames > (file.size() - 4) / 8) { // The data appears to be truncated. LOG_RES_WARNING("File \"%s\" appears to be truncated (%u bytes, expected %u)") << NativePath(file.composeUri().asText()).pretty() << file.size() << (numNames * 8 + 4); // We'll only read this many names. numNames = (file.size() - 4) / 8; } // Read the names. for(int i = 0; i < numNames; ++i) { PatchName name; from >> name; names.append(name); } } file.unlock(); return names; } /** * Reads patch composite texture definitions from @a file. * * @param file File to be read. * @param origIndexBase Base value for the "original index" logic. * @param archiveCount Will be updated with the total number of definitions * in the file (which may not necessarily equal the total * number of definitions which are actually read). */ static CompositeTextures readCompositeTextureDefs(File1 &file, PatchNames const &patchNames, int origIndexBase, int &archiveCount) { LOG_AS("readCompositeTextureDefs"); CompositeTextures result; ///< The resulting set of validated definitions. // The game data format determines the format of the archived data. CompositeTexture::ArchiveFormat format = (gameDataFormat == 0? CompositeTexture::DoomFormat : CompositeTexture::StrifeFormat); ByteRefArray data(file.cache(), file.size()); de::Reader reader(data); // First is a count of the total number of definitions. dint32 definitionCount; reader >> definitionCount; // Next is directory of offsets to the definitions. typedef QMap Offsets; Offsets offsets; for(int i = 0; i < definitionCount; ++i) { dint32 offset; reader >> offset; // Ensure the offset is within valid range. if(offset < 0 || (unsigned) offset < definitionCount * sizeof(offset) || (dsize) offset > reader.source()->size()) { LOG_RES_WARNING("Ignoring definition #%i: invalid offset %i") << i << offset; } else { offsets.insert(offset, origIndexBase + i); } } // Seek to each offset and deserialize the definition. DENG2_FOR_EACH_CONST(Offsets, i, offsets) { // Read the next definition. reader.setOffset(i.key()); CompositeTexture *def = CompositeTexture::constructFrom(reader, patchNames, format); // Attribute the "original index". def->setOrigIndex(i.value()); // If the composite contains at least one known component image it is // considered valid and we will therefore produce a Texture for it. DENG2_FOR_EACH_CONST(CompositeTexture::Components, it, def->components()) { if(it->lumpNum() >= 0) { // Its valid - include in the result. result.append(def); def = 0; break; } } // Failed to validate? Dump it. if(def) delete def; } file.unlock(); // We have now finished with this file. archiveCount = definitionCount; return result; } #ifdef __CLIENT__ static int hashDetailTextureSpec(detailvariantspecification_t const &spec) { return (spec.contrast * (1/255.f) * DETAILTEXTURE_CONTRAST_QUANTIZATION_FACTOR + .5f); } static variantspecification_t &configureTextureSpec(variantspecification_t &spec, texturevariantusagecontext_t tc, int flags, byte border, int tClass, int tMap, int wrapS, int wrapT, int minFilter, int magFilter, int anisoFilter, dd_bool mipmapped, dd_bool gammaCorrection, dd_bool noStretch, dd_bool toAlpha) { DENG2_ASSERT(tc == TC_UNKNOWN || VALID_TEXTUREVARIANTUSAGECONTEXT(tc)); flags &= ~TSF_INTERNAL_MASK; spec.context = tc; spec.flags = flags; spec.border = (flags & TSF_UPSCALE_AND_SHARPEN)? 1 : border; spec.mipmapped = mipmapped; spec.wrapS = wrapS; spec.wrapT = wrapT; spec.minFilter = de::clamp(-1, minFilter, spec.mipmapped? 3:1); spec.magFilter = de::clamp(-3, magFilter, 1); spec.anisoFilter = de::clamp(-1, anisoFilter, 4); spec.gammaCorrection = gammaCorrection; spec.noStretch = noStretch; spec.toAlpha = toAlpha; if(tClass || tMap) { spec.flags |= TSF_HAS_COLORPALETTE_XLAT; spec.tClass = de::max(0, tClass); spec.tMap = de::max(0, tMap); } return spec; } static detailvariantspecification_t &configureDetailTextureSpec( detailvariantspecification_t &spec, float contrast) { int const quantFactor = DETAILTEXTURE_CONTRAST_QUANTIZATION_FACTOR; spec.contrast = 255 * de::clamp(0, contrast * quantFactor + .5f, quantFactor) * (1 / float(quantFactor)); return spec; } #endif // __CLIENT__ } // namespace internal } // namespace de #ifdef __CLIENT__ /// @c TST_DETAIL type specifications are stored separately into a set of /// buckets. Bucket selection is determined by their quantized contrast value. #define DETAILVARIANT_CONTRAST_HASHSIZE (DETAILTEXTURE_CONTRAST_QUANTIZATION_FACTOR+1) #endif using namespace de; using namespace internal; /** * Native Doomsday Script utility for scheduling conversion of a single legacy savegame. */ Value *Function_SavedSession_Convert(Context &, Function::ArgumentValues const &args) { String gameId = args[0]->asText(); String sourcePath = args[1]->asText(); return new NumberValue(App_ResourceSystem().convertLegacySavegames(gameId, sourcePath)); } /** * Native Doomsday Script utility for scheduling conversion of @em all legacy savegames * for the specified gameId. */ Value *Function_SavedSession_ConvertAll(Context &, Function::ArgumentValues const &args) { String gameId = args[0]->asText(); return new NumberValue(App_ResourceSystem().convertLegacySavegames(gameId)); } DENG2_PIMPL(ResourceSystem) , DENG2_OBSERVES(Loop, Iteration) // post savegame conversion FS population , DENG2_OBSERVES(Games, Addition) // savegames folder setup , DENG2_OBSERVES(MaterialScheme, ManifestDefined) , DENG2_OBSERVES(MaterialManifest, MaterialDerived) , DENG2_OBSERVES(MaterialManifest, Deletion) , DENG2_OBSERVES(Material, Deletion) , DENG2_OBSERVES(TextureScheme, ManifestDefined) , DENG2_OBSERVES(TextureManifest, TextureDerived) , DENG2_OBSERVES(Texture, Deletion) #ifdef __CLIENT__ , DENG2_OBSERVES(FontScheme, ManifestDefined) , DENG2_OBSERVES(FontManifest, Deletion) , DENG2_OBSERVES(AbstractFont, Deletion) , DENG2_OBSERVES(ColorPalette, ColorTableChange) #endif { typedef QList ResourceClasses; ResourceClasses resClasses; NullResourceClass nullResourceClass; typedef QList AnimGroups; AnimGroups animGroups; typedef QMap ColorPalettes; ColorPalettes colorPalettes; typedef QMap ColorPaletteNames; ColorPaletteNames colorPaletteNames; colorpaletteid_t defaultColorPalette; /// System subspace schemes containing the textures. TextureSchemes textureSchemes; QList textureSchemeCreationOrder; /// All texture instances in the system (from all schemes). AllTextures textures; typedef QHash RawTextureHash; RawTextureHash rawTexHash; /// System subspace schemes containing the manifests/resources. QMap materialSchemes; QList materialSchemeCreationOrder; QList materials; ///< From all schemes. int materialManifestCount = 0; ///< Total number of material manifests (in all schemes). MaterialManifestGroups materialGroups; uint materialManifestIdMapSize; MaterialManifest **materialManifestIdMap; ///< Index with materialid_t-1 MapDefs mapDefs; #ifdef __CLIENT__ /// System subspace schemes containing the manifests/resources. FontSchemes fontSchemes; QList fontSchemeCreationOrder; AllFonts fonts; ///< From all schemes. uint fontManifestCount; ///< Total number of font manifests (in all schemes). uint fontManifestIdMapSize; FontManifest **fontManifestIdMap; ///< Index with fontid_t-1 typedef QVector ModelDefs; ModelDefs modefs; QVector stateModefs; ///< Index to the modefs array. typedef StringPool ModelRepository; ModelRepository *modelRepository; ///< Owns Model instances. /// A list of specifications for material variants. typedef QList MaterialSpecs; MaterialSpecs materialSpecs; typedef QList TextureSpecs; TextureSpecs textureSpecs; TextureSpecs detailTextureSpecs[DETAILVARIANT_CONTRAST_HASHSIZE]; struct CacheTask { virtual ~CacheTask() {} virtual void run() = 0; }; /** * Stores the arguments for a resource cache work item. */ struct MaterialCacheTask : public CacheTask { Material *material; MaterialVariantSpec const *spec; /// Interned context specification. MaterialCacheTask(Material &resource, MaterialVariantSpec const &contextSpec) : CacheTask() , material(&resource) , spec(&contextSpec) {} void run() { // Cache all dependent assets and upload GL textures if necessary. material->getAnimator(*spec).cacheAssets(); } }; /// A FIFO queue of material variant caching tasks. /// Implemented as a list because we may need to remove tasks from the queue if /// the material is destroyed in the mean time. typedef QList CacheQueue; CacheQueue cacheQueue; #endif QMap spritesByFrame; NativePath nativeSavePath; Binder binder; Record savedSessionModule; // SavedSession: manipulation, conversion, etc... (based on native class SavedSession) Instance(Public *i) : Base(i) , defaultColorPalette (0) , materialManifestIdMapSize(0) , materialManifestIdMap (0) #ifdef __CLIENT__ , fontManifestCount (0) , fontManifestIdMapSize (0) , fontManifestIdMap (0) , modelRepository (0) #endif , nativeSavePath (App::app().nativeHomePath() / "savegames") // default { de::Uri::setResolverFunc(ResourceSystem::resolveSymbol); LOG_AS("ResourceSystem"); resClasses.append(new ResourceClass("RC_PACKAGE", "Packages")); resClasses.append(new ResourceClass("RC_DEFINITION", "Defs")); resClasses.append(new ResourceClass("RC_GRAPHIC", "Graphics")); resClasses.append(new ResourceClass("RC_MODEL", "Models")); resClasses.append(new ResourceClass("RC_SOUND", "Sfx")); resClasses.append(new ResourceClass("RC_MUSIC", "Music")); resClasses.append(new ResourceClass("RC_FONT", "Fonts")); /// @note Order here defines the ambigious-URI search order. createTextureScheme("Sprites"); createTextureScheme("Textures"); createTextureScheme("Flats"); createTextureScheme("Patches"); createTextureScheme("System"); createTextureScheme("Details"); createTextureScheme("Reflections"); createTextureScheme("Masks"); createTextureScheme("ModelSkins"); createTextureScheme("ModelReflectionSkins"); createTextureScheme("Lightmaps"); createTextureScheme("Flaremaps"); /// @note Order here defines the ambigious-URI search order. createMaterialScheme("Sprites"); createMaterialScheme("Textures"); createMaterialScheme("Flats"); createMaterialScheme("System"); #ifdef __CLIENT__ /// @note Order here defines the ambigious-URI search order. createFontScheme("System"); createFontScheme("Game"); #endif #ifdef __CLIENT__ // Setup the SavedSession module. binder.init(savedSessionModule) << DENG2_FUNC(SavedSession_Convert, "convert", "gameId" << "savegamePath") << DENG2_FUNC(SavedSession_ConvertAll, "convertAll", "gameId"); App::scriptSystem().addNativeModule("SavedSession", savedSessionModule); // Determine the root directory of the saved session repository. if(int arg = App::commandLine().check("-savedir", 1)) { // Using a custom root save directory. App::commandLine().makeAbsolutePath(arg + 1); nativeSavePath = App::commandLine().at(arg + 1); } // Else use the default. #endif App_Games().audienceForAddition() += this; // Create the user saved session folder in the local FS if it doesn't yet exist. // Once created, any SavedSessions in this folder will be found and indexed // automatically into the file system. App::fileSystem().makeFolder("/home/savegames"); // Create the legacy savegame folder. App::fileSystem().makeFolder("/legacysavegames"); App::packageLoader().loadFromCommandLine(); } ~Instance() { convertSavegameTasks.waitForDone(); App_Games().audienceForAddition() -= this; qDeleteAll(resClasses); self.clearAllAnimGroups(); #ifdef __CLIENT__ self.clearAllFontSchemes(); clearFontManifests(); #endif self.clearAllTextureSchemes(); clearTextureManifests(); #ifdef __CLIENT__ self.clearAllRawTextures(); #endif #ifdef __CLIENT__ self.purgeCacheQueue(); #endif self.clearAllMaterialGroups(); self.clearAllMaterialSchemes(); clearMaterialManifests(); #ifdef __CLIENT__ clearAllTextureSpecs(); clearMaterialSpecs(); #endif clearSprites(); #ifdef __CLIENT__ clearModels(); #endif self.clearAllColorPalettes(); self.clearAllMapDefs(); } inline de::FS1 &fileSys() { return App_FileSystem(); } void gameAdded(Game &game) { // Called from a non-UI thread. LOG_AS("ResourceSystem"); // Make the /home/savegames/ subfolder in the local FS if it does not yet exist. App::fileSystem().makeFolder(String("/home/savegames") / game.id()); } void clearMaterialManifests() { qDeleteAll(materialSchemes); materialSchemes.clear(); materialSchemeCreationOrder.clear(); // Clear the manifest index/map. if(materialManifestIdMap) { M_Free(materialManifestIdMap); materialManifestIdMap = 0; materialManifestIdMapSize = 0; } materialManifestCount = 0; } void createMaterialScheme(String name) { DENG2_ASSERT(name.length() >= MaterialScheme::min_name_length); // Create a new scheme. MaterialScheme *newScheme = new MaterialScheme(name); materialSchemes.insert(name.toLower(), newScheme); materialSchemeCreationOrder.append(newScheme); // We want notification when a new manifest is defined in this scheme. newScheme->audienceForManifestDefined += this; } void clearTextureManifests() { qDeleteAll(textureSchemes); textureSchemes.clear(); textureSchemeCreationOrder.clear(); } void createTextureScheme(String name) { DENG2_ASSERT(name.length() >= TextureScheme::min_name_length); // Create a new scheme. TextureScheme *newScheme = new TextureScheme(name); textureSchemes.insert(name.toLower(), newScheme); textureSchemeCreationOrder.append(newScheme); // We want notification when a new manifest is defined in this scheme. newScheme->audienceForManifestDefined += this; } #ifdef __CLIENT__ void clearFontManifests() { qDeleteAll(fontSchemes); fontSchemes.clear(); fontSchemeCreationOrder.clear(); // Clear the manifest index/map. if(fontManifestIdMap) { M_Free(fontManifestIdMap); fontManifestIdMap = 0; fontManifestIdMapSize = 0; } fontManifestCount = 0; } void createFontScheme(String name) { DENG2_ASSERT(name.length() >= FontScheme::min_name_length); // Create a new scheme. FontScheme *newScheme = new FontScheme(name); fontSchemes.insert(name.toLower(), newScheme); fontSchemeCreationOrder.append(newScheme); // We want notification when a new manifest is defined in this scheme. newScheme->audienceForManifestDefined += this; } #endif void clearSprites() { for(SpriteSet &sprFrames : spritesByFrame) { qDeleteAll(sprFrames); } spritesByFrame.clear(); } inline bool hasSpriteFrameSet(spritenum_t spriteId) const { return spritesByFrame.contains(spriteId); } SpriteSet *tryFindSpriteFrameSet(spritenum_t spriteId) { auto found = spritesByFrame.find(spriteId); if(found != spritesByFrame.end()) { return &found.value(); } return nullptr; } SpriteSet &findSpriteFrameSet(spritenum_t spriteId) { if(SpriteSet *sprFrames = tryFindSpriteFrameSet(spriteId)) return *sprFrames; /// @throw MissingResourceError An unknown/invalid id was specified. throw MissingResourceError("ResourceSystem::findSpriteFrameSet", "Unknown sprite id " + String::number(spriteId)); } SpriteSet &newSpriteFrameSet(spritenum_t spriteId) { DENG2_ASSERT(!tryFindSpriteFrameSet(spriteId)); // sanity check. return spritesByFrame.insert(spriteId, SpriteSet()).value(); } #ifdef __CLIENT__ void clearRuntimeFonts() { self.fontScheme("Game").clear(); self.pruneUnusedTextureSpecs(); } void clearSystemFonts() { self.fontScheme("System").clear(); self.pruneUnusedTextureSpecs(); } void clearMaterialSpecs() { qDeleteAll(materialSpecs); materialSpecs.clear(); } MaterialVariantSpec *findMaterialSpec(MaterialVariantSpec const &tpl, bool canCreate) { foreach(MaterialVariantSpec *spec, materialSpecs) { if(spec->compare(tpl)) return spec; } if(!canCreate) return 0; materialSpecs.append(new MaterialVariantSpec(tpl)); return materialSpecs.back(); } MaterialVariantSpec &getMaterialSpecForContext(MaterialContextId contextId, int flags, byte border, int tClass, int tMap, int wrapS, int wrapT, int minFilter, int magFilter, int anisoFilter, bool mipmapped, bool gammaCorrection, bool noStretch, bool toAlpha) { static MaterialVariantSpec tpl; texturevariantusagecontext_t primaryContext = TC_UNKNOWN; switch(contextId) { case UiContext: primaryContext = TC_UI; break; case MapSurfaceContext: primaryContext = TC_MAPSURFACE_DIFFUSE; break; case SpriteContext: primaryContext = TC_SPRITE_DIFFUSE; break; case ModelSkinContext: primaryContext = TC_MODELSKIN_DIFFUSE; break; case PSpriteContext: primaryContext = TC_PSPRITE_DIFFUSE; break; case SkySphereContext: primaryContext = TC_SKYSPHERE_DIFFUSE; break; default: DENG2_ASSERT(false); } TextureVariantSpec const &primarySpec = self.textureSpec(primaryContext, flags, border, tClass, tMap, wrapS, wrapT, minFilter, magFilter, anisoFilter, mipmapped, gammaCorrection, noStretch, toAlpha); // Apply the normalized spec to the template. tpl.contextId = contextId; tpl.primarySpec = &primarySpec; return *findMaterialSpec(tpl, true); } TextureVariantSpec &linkTextureSpec(TextureVariantSpec *spec) { DENG2_ASSERT(spec != 0); switch(spec->type) { case TST_GENERAL: textureSpecs.append(spec); break; case TST_DETAIL: { int hash = hashDetailTextureSpec(spec->detailVariant); detailTextureSpecs[hash].append(spec); break; } } return *spec; } TextureVariantSpec *findTextureSpec(TextureVariantSpec const &tpl, bool canCreate) { // Do we already have a concrete version of the template specification? switch(tpl.type) { case TST_GENERAL: { foreach(TextureVariantSpec *varSpec, textureSpecs) { if(*varSpec == tpl) { return varSpec; } } break; } case TST_DETAIL: { int hash = hashDetailTextureSpec(tpl.detailVariant); foreach(TextureVariantSpec *varSpec, detailTextureSpecs[hash]) { if(*varSpec == tpl) { return varSpec; } } break; } } // Not found, can we create? if(canCreate) { return &linkTextureSpec(new TextureVariantSpec(tpl)); } return 0; } TextureVariantSpec *textureSpec(texturevariantusagecontext_t tc, int flags, byte border, int tClass, int tMap, int wrapS, int wrapT, int minFilter, int magFilter, int anisoFilter, dd_bool mipmapped, dd_bool gammaCorrection, dd_bool noStretch, dd_bool toAlpha) { static TextureVariantSpec tpl; tpl.type = TST_GENERAL; configureTextureSpec(tpl.variant, tc, flags, border, tClass, tMap, wrapS, wrapT, minFilter, magFilter, anisoFilter, mipmapped, gammaCorrection, noStretch, toAlpha); // Retrieve a concrete version of the rationalized specification. return findTextureSpec(tpl, true); } TextureVariantSpec *detailTextureSpec(float contrast) { static TextureVariantSpec tpl; tpl.type = TST_DETAIL; configureDetailTextureSpec(tpl.detailVariant, contrast); return findTextureSpec(tpl, true); } bool textureSpecInUse(TextureVariantSpec const &spec) { for(Texture *texture : textures) for(TextureVariant *variant : texture->variants()) { if(&variant->spec() == &spec) { return true; // Found one; stop. } } return false; } int pruneUnusedTextureSpecs(TextureSpecs &list) { int numPruned = 0; QMutableListIterator it(list); while(it.hasNext()) { TextureVariantSpec *spec = it.next(); if(!textureSpecInUse(*spec)) { it.remove(); delete spec; numPruned += 1; } } return numPruned; } int pruneUnusedTextureSpecs(texturevariantspecificationtype_t specType) { switch(specType) { case TST_GENERAL: return pruneUnusedTextureSpecs(textureSpecs); case TST_DETAIL: { int numPruned = 0; for(int i = 0; i < DETAILVARIANT_CONTRAST_HASHSIZE; ++i) { numPruned += pruneUnusedTextureSpecs(detailTextureSpecs[i]); } return numPruned; } } return 0; } void clearAllTextureSpecs() { qDeleteAll(textureSpecs); textureSpecs.clear(); for(int i = 0; i < DETAILVARIANT_CONTRAST_HASHSIZE; ++i) { qDeleteAll(detailTextureSpecs[i]); detailTextureSpecs[i].clear(); } } #endif // __CLIENT__ void clearRuntimeTextures() { self.textureScheme("Flats").clear(); self.textureScheme("Textures").clear(); self.textureScheme("Patches").clear(); self.textureScheme("Sprites").clear(); self.textureScheme("Details").clear(); self.textureScheme("Reflections").clear(); self.textureScheme("Masks").clear(); self.textureScheme("ModelSkins").clear(); self.textureScheme("ModelReflectionSkins").clear(); self.textureScheme("Lightmaps").clear(); self.textureScheme("Flaremaps").clear(); #ifdef __CLIENT__ self.pruneUnusedTextureSpecs(); #endif } void clearSystemTextures() { self.textureScheme("System").clear(); #ifdef __CLIENT__ self.pruneUnusedTextureSpecs(); #endif } #ifdef __CLIENT__ void processCacheQueue() { while(!cacheQueue.isEmpty()) { QScopedPointer task(cacheQueue.takeFirst()); task->run(); } } void queueCacheTasksForMaterial(Material &material, MaterialVariantSpec const &contextSpec, bool cacheGroups = true) { // Already in the queue? bool alreadyQueued = false; foreach(CacheTask *baseTask, cacheQueue) { if(MaterialCacheTask *task = dynamic_cast(baseTask)) { if(&material == task->material && &contextSpec == task->spec) { alreadyQueued = true; break; } } } if(!alreadyQueued) { cacheQueue.append(new MaterialCacheTask(material, contextSpec)); } if(!cacheGroups) return; // If the material is part of one or more groups enqueue cache tasks // for all other materials within the same group(s). Although we could // use a flag in the task and have it find the groups come prepare time, // this way we can be sure there are no overlapping tasks. foreach(MaterialManifestGroup *group, materialGroups) { if(!group->contains(&material.manifest())) { continue; } foreach(MaterialManifest *manifest, *group) { if(!manifest->hasMaterial()) continue; // Have we already enqueued this material? if(&manifest->material() == &material) continue; queueCacheTasksForMaterial(manifest->material(), contextSpec, false /* do not cache groups */); } } } void queueCacheTasksForSprite(spritenum_t spriteId, MaterialVariantSpec const &contextSpec, bool cacheGroups = true) { if(!hasSpriteFrameSet(spriteId)) return; for(Sprite *sprite : findSpriteFrameSet(spriteId)) for(SpriteViewAngle const &viewAngle : sprite->viewAngles()) { if(Material *material = viewAngle.material) { queueCacheTasksForMaterial(*material, contextSpec, cacheGroups); } } } void queueCacheTasksForModel(ModelDef &modelDef) { if(!useModels) return; for(uint sub = 0; sub < modelDef.subCount(); ++sub) { SubmodelDef &subdef = modelDef.subModelDef(sub); Model *mdl = modelForId(subdef.modelId); if(!mdl) continue; // Load all skins. for(ModelSkin const &skin : mdl->skins()) { if(Texture *tex = skin.texture) { tex->prepareVariant(Rend_ModelDiffuseTextureSpec(mdl->flags().testFlag(Model::NoTextureCompression))); } } // Load the shiny skin too. if(Texture *shinyTex = subdef.shinySkin) { shinyTex->prepareVariant(Rend_ModelShinyTextureSpec()); } } } #endif void deriveAllTexturesInScheme(String schemeName) { TextureScheme &scheme = self.textureScheme(schemeName); PathTreeIterator iter(scheme.index().leafNodes()); while(iter.hasNext()) { TextureManifest &manifest = iter.next(); deriveTexture(manifest); } } CompositeTextures loadCompositeTextureDefs() { LOG_AS("loadCompositeTextureDefs"); typedef QMultiMap CompositeTextureMap; // Load the patch names from the PNAMES lump. PatchNames pnames; try { pnames = readPatchNames(fileSys().lump(fileSys().lumpNumForName("PNAMES"))); } catch(LumpIndex::NotFoundError const &er) { if(App_GameLoaded()) { LOGDEV_RES_WARNING(er.asText()); } } // If no patch names - there is no point continuing further. if(!pnames.count()) return CompositeTextures(); // Collate an ordered list of all the definition files we intend to process. QList defFiles = collectPatchCompositeDefinitionFiles(); /** * Definitions are read into two discreet sets. * * Older add-ons contain copies of the original games' texture definitions, * with their own new definitions appended on the end. However, Doomsday needs * to classify all definitions according to whether they originate from the * original game data. To achieve the correct user-expected results, we must * compare each definition originating from an add-on to determine whether it * should instead be classified as "original" data. */ CompositeTextures defs, customDefs; // Process each definition file. int origIndexBase = 0; DENG2_FOR_EACH_CONST(QList, i, defFiles) { File1 &file = **i; LOG_RES_VERBOSE("Processing \"%s:%s\"...") << NativePath(file.container().composeUri().asText()).pretty() << NativePath(file.composeUri().asText()).pretty(); // Buffer the file and read the next set of definitions. int archiveCount; CompositeTextures newDefs = readCompositeTextureDefs(file, pnames, origIndexBase, archiveCount); // In which set do these belong? CompositeTextures *existingDefs = (file.container().hasCustom()? &customDefs : &defs); if(!existingDefs->isEmpty()) { // Merge with the existing definitions. existingDefs->append(newDefs); } else { *existingDefs = newDefs; } // Maintain the original index. origIndexBase += archiveCount; // Print a summary. LOG_RES_MSG("Loaded %s texture definitions from \"%s:%s\"") << (newDefs.count() == archiveCount? String("all %1").arg(newDefs.count()) : String("%1 of %1").arg(newDefs.count()).arg(archiveCount)) << NativePath(file.container().composeUri().asText()).pretty() << NativePath(file.composeUri().asText()).pretty(); } if(!customDefs.isEmpty()) { // Custom definitions were found - we must cross compare them. // Map the definitions for O(log n) lookup performance, CompositeTextureMap mappedCustomDefs; foreach(CompositeTexture *custom, customDefs) { mappedCustomDefs.insert(custom->percentEncodedNameRef(), custom); } // Perform reclassification of replaced texture definitions. for(int i = 0; i < defs.count(); ++i) { CompositeTexture *orig = defs[i]; // Does a potential replacement exist for this original definition? CompositeTextureMap::const_iterator found = mappedCustomDefs.constFind(orig->percentEncodedNameRef()); if(found == mappedCustomDefs.constEnd()) continue; // Definition 'custom' is destined to replace 'orig'. CompositeTexture *custom = found.value(); bool haveReplacement = false; if(custom->isFlagged(CompositeTexture::Custom)) { haveReplacement = true; // Uses a custom patch. } else if(*orig != *custom) { haveReplacement = true; } if(haveReplacement) { custom->setFlags(CompositeTexture::Custom); // Let the PWAD "copy" override the IWAD original. defs.takeAt(i); delete orig; --i; // Process the new next definition item. } } /* * List 'defs' now contains only those definitions which are not * superceeded by those in the 'customDefs' list. */ // Add definitions from the custom list to the end of the main set. defs.append(customDefs); } return defs; } void initCompositeTextures() { Time begunAt; LOG_RES_VERBOSE("Initializing CompositeTextures..."); // Load texture definitions from TEXTURE1/2 lumps. CompositeTextures allDefs = loadCompositeTextureDefs(); while(!allDefs.isEmpty()) { CompositeTexture &def = *allDefs.takeFirst(); de::Uri uri("Textures", Path(def.percentEncodedName())); Texture::Flags flags; if(def.isFlagged(CompositeTexture::Custom)) flags |= Texture::Custom; /* * The id Tech 1 implementation of the texture collection has a flaw * which results in the first texture being used dually as a "NULL" * texture. */ if(def.origIndex() == 0) flags |= Texture::NoDraw; try { TextureManifest &manifest = self.declareTexture(uri, flags, def.logicalDimensions(), Vector2i(), def.origIndex()); // Are we redefining an existing texture? if(manifest.hasTexture()) { // Yes. Destroy the existing definition (*should* exist). Texture &tex = manifest.texture(); CompositeTexture *oldDef = reinterpret_cast(tex.userDataPointer()); if(oldDef) { tex.setUserDataPointer(0); delete oldDef; } // Attach the new definition. tex.setUserDataPointer((void *)&def); continue; } // A new texture. else if(Texture *tex = manifest.derive()) { tex->setUserDataPointer((void *)&def); continue; } } catch(TextureScheme::InvalidPathError const &er) { LOG_RES_WARNING("Failed declaring texture \"%s\": %s") << uri << er.asText(); } delete &def; } LOG_RES_VERBOSE("initCompositeTextures: Completed in %.2f seconds") << begunAt.since(); } void initFlatTextures() { Time begunAt; LOG_RES_VERBOSE("Initializing Flat textures..."); LumpIndex const &index = fileSys().nameIndex(); lumpnum_t firstFlatMarkerLumpNum = index.findFirst(Path("F_START.lmp")); if(firstFlatMarkerLumpNum >= 0) { lumpnum_t lumpNum; File1 *blockContainer = 0; for(lumpNum = index.size(); lumpNum --> firstFlatMarkerLumpNum + 1;) { File1 &file = index[lumpNum]; String percentEncodedName = file.name().fileNameWithoutExtension(); if(blockContainer && blockContainer != &file.container()) { blockContainer = 0; } if(!blockContainer) { if(!percentEncodedName.compareWithoutCase("F_END") || !percentEncodedName.compareWithoutCase("FF_END")) { blockContainer = &file.container(); } continue; } if(!percentEncodedName.compareWithoutCase("F_START")) { blockContainer = 0; continue; } // Ignore extra marker lumps. if(!percentEncodedName.compareWithoutCase("FF_START") || !percentEncodedName.compareWithoutCase("F_END") || !percentEncodedName.compareWithoutCase("FF_END")) continue; de::Uri uri("Flats", Path(percentEncodedName)); if(self.hasTextureManifest(uri)) continue; Texture::Flags flags; if(file.container().hasCustom()) flags |= Texture::Custom; /* * Kludge Assume 64x64 else when the flat is loaded it will inherit the * pixel dimensions of the graphic, which, if it has been replaced with * a hires version - will be much larger than it should be. * * @todo Always determine size from the lowres original. */ Vector2i dimensions(64, 64); Vector2i origin(0, 0); int const uniqueId = lumpNum - (firstFlatMarkerLumpNum + 1); de::Uri resourceUri = composeLumpIndexResourceUrn(lumpNum); self.declareTexture(uri, flags, dimensions, origin, uniqueId, &resourceUri); } } // Define any as yet undefined flat textures. /// @todo Defer until necessary (manifest texture is first referenced). deriveAllTexturesInScheme("Flats"); LOG_RES_VERBOSE("Flat textures initialized in %.2f seconds") << begunAt.since(); } void initSpriteTextures() { Time begunAt; LOG_RES_VERBOSE("Initializing Sprite textures..."); int uniqueId = 1/*1-based index*/; /// @todo fixme: Order here does not respect id Tech 1 logic. ddstack_t *stack = Stack_New(); LumpIndex const &index = fileSys().nameIndex(); for(int i = 0; i < index.size(); ++i) { File1 &file = index[i]; String fileName = file.name().fileNameWithoutExtension(); if(fileName.beginsWith('S', Qt::CaseInsensitive) && fileName.length() >= 5) { if(fileName.endsWith("_START", Qt::CaseInsensitive)) { // We've arrived at *a* sprite block. Stack_Push(stack, NULL); continue; } if(fileName.endsWith("_END", Qt::CaseInsensitive)) { // The sprite block ends. Stack_Pop(stack); continue; } } if(!Stack_Height(stack)) continue; String decodedFileName = QString(QByteArray::fromPercentEncoding(fileName.toUtf8())); if(!validateSpriteName(decodedFileName)) { LOG_RES_NOTE("Ignoring invalid sprite name '%s'") << decodedFileName; continue; } de::Uri uri("Sprites", Path(fileName)); Texture::Flags flags = 0; // If this is from an add-on flag it as "custom". if(file.container().hasCustom()) { flags |= Texture::Custom; } Vector2i dimensions; Vector2i origin; if(file.size()) { // If this is a Patch read the world dimension and origin offset values. ByteRefArray fileData = ByteRefArray(file.cache(), file.size()); if(Patch::recognize(fileData)) { try { PatchMetadata info = Patch::loadMetadata(fileData); dimensions = info.logicalDimensions; origin = -info.origin; } catch(IByteArray::OffsetError const &) { LOG_RES_WARNING("File \"%s:%s\" does not appear to be a valid Patch. " "World dimension and origin offset not set for sprite \"%s\".") << NativePath(file.container().composePath()).pretty() << NativePath(file.composePath()).pretty() << uri; } } file.unlock(); } de::Uri resourceUri = composeLumpIndexResourceUrn(i); try { self.declareTexture(uri, flags, dimensions, origin, uniqueId, &resourceUri); uniqueId++; } catch(TextureScheme::InvalidPathError const &er) { LOG_RES_WARNING("Failed declaring texture \"%s\": %s") << uri << er.asText(); } } while(Stack_Height(stack)) { Stack_Pop(stack); } Stack_Delete(stack); // Define any as yet undefined sprite textures. /// @todo Defer until necessary (manifest texture is first referenced). deriveAllTexturesInScheme("Sprites"); LOG_RES_VERBOSE("Sprite textures initialized in %.2f seconds") << begunAt.since(); } #ifdef __CLIENT__ void clearModels() { /// @todo Why only centralized memory deallocation? Bad (lazy) design... modefs.clear(); stateModefs.clear(); clearModelList(); if(modelRepository) { delete modelRepository; modelRepository = 0; } } Model *modelForId(modelid_t modelId) { DENG2_ASSERT(modelRepository); return reinterpret_cast(modelRepository->userPointer(modelId)); } inline String const &findModelPath(modelid_t id) { return modelRepository->stringRef(id); } /** * Create a new modeldef or find an existing one. This is for ID'd models. */ ModelDef *getModelDefWithId(String id) { if(id.isEmpty()) return 0; // First try to find an existing modef. if(self.hasModelDef(id)) { return &self.modelDef(id); } // Get a new entry. modefs.append(ModelDef(id.toUtf8().constData())); return &modefs.last(); } /** * Create a new modeldef or find an existing one. There can be only one model * definition associated with a state/intermark pair. */ ModelDef *getModelDef(int state, float interMark, int select) { // Is this a valid state? if(state < 0 || state >= runtimeDefs.states.size()) { return 0; } // First try to find an existing modef. foreach(ModelDef const &modef, modefs) { if(modef.state == &runtimeDefs.states[state] && fequal(modef.interMark, interMark) && modef.select == select) { // Models are loaded in reverse order; this one already has a model. return 0; } } modefs.append(ModelDef()); ModelDef *md = &modefs.last(); // Set initial data. md->state = &runtimeDefs.states[state]; md->interMark = interMark; md->select = select; return md; } String findSkinPath(Path const &skinPath, Path const &modelFilePath) { DENG2_ASSERT(!skinPath.isEmpty()); // Try the "first choice" directory first. if(!modelFilePath.isEmpty()) { // The "first choice" directory is that in which the model file resides. try { return fileSys().findPath(de::Uri("Models", modelFilePath.toString().fileNamePath() / skinPath.fileName()), RLF_DEFAULT, self.resClass(RC_GRAPHIC)); } catch(FS1::NotFoundError const &) {} // Ignore this error. } /// @throws FS1::NotFoundError if no resource was found. return fileSys().findPath(de::Uri("Models", skinPath), RLF_DEFAULT, self.resClass(RC_GRAPHIC)); } /** * Allocate room for a new skin file name. */ short defineSkinAndAddToModelIndex(Model &mdl, Path const &skinPath) { if(Texture *tex = self.defineTexture("ModelSkins", de::Uri(skinPath))) { // A duplicate? (return existing skin number) for(int i = 0; i < mdl.skinCount(); ++i) { if(mdl.skin(i).texture == tex) return i; } // Add this new skin. mdl.newSkin(skinPath.toString()).texture = tex; return mdl.skinCount() - 1; } return -1; } void defineAllSkins(Model &mdl) { String const &modelFilePath = findModelPath(mdl.modelId()); int numFoundSkins = 0; for(int i = 0; i < mdl.skinCount(); ++i) { ModelSkin &skin = mdl.skin(i); try { de::Uri foundResourceUri(Path(findSkinPath(skin.name, modelFilePath))); skin.texture = self.defineTexture("ModelSkins", foundResourceUri); // We have found one more skin for this model. numFoundSkins += 1; } catch(FS1::NotFoundError const &) { LOG_RES_WARNING("Failed to locate \"%s\" (#%i) for model \"%s\"") << skin.name << i << NativePath(modelFilePath).pretty(); } } if(!numFoundSkins) { // Lastly try a skin named similarly to the model in the same directory. de::Uri searchPath(modelFilePath.fileNamePath() / modelFilePath.fileNameWithoutExtension(), RC_GRAPHIC); try { String foundPath = fileSys().findPath(searchPath, RLF_DEFAULT, self.resClass(RC_GRAPHIC)); // Ensure the found path is absolute. foundPath = App_BasePath() / foundPath; defineSkinAndAddToModelIndex(mdl, foundPath); // We have found one more skin for this model. numFoundSkins = 1; LOG_RES_MSG("Assigned fallback skin \"%s\" to index #0 for model \"%s\"") << NativePath(foundPath).pretty() << NativePath(modelFilePath).pretty(); } catch(FS1::NotFoundError const&) {} // Ignore this error. } if(!numFoundSkins) { LOG_RES_MSG("No skins found for model \"%s\" (it may use a custom skin specified in a DED)") << NativePath(modelFilePath).pretty(); } #ifdef DENG2_DEBUG LOGDEV_RES_XVERBOSE("Model \"%s\" skins:") << NativePath(modelFilePath).pretty(); int skinIdx = 0; for(ModelSkin const &skin : mdl.skins()) { TextureManifest const *texManifest = skin.texture? &skin.texture->manifest() : 0; LOGDEV_RES_XVERBOSE(" %i: %s %s") << (skinIdx++) << skin.name << (texManifest? (String("\"") + texManifest->composeUri() + "\"") : "(missing texture)") << (texManifest? (String(" => \"") + NativePath(texManifest->resourceUri().compose()).pretty() + "\"") : ""); } #endif } /** * Scales the given model so that it'll be 'destHeight' units tall. Measurements * are based on submodel zero. Scale is applied uniformly. */ void scaleModel(ModelDef &mf, float destHeight, float offset) { if(!mf.subCount()) return; SubmodelDef &smf = mf.subModelDef(0); // No model to scale? if(!smf.modelId) return; // Find the top and bottom heights. float top, bottom; float height = self.model(smf.modelId).frame(smf.frame).horizontalRange(&top, &bottom); if(!height) height = 1; float scale = destHeight / height; mf.scale = Vector3f(scale, scale, scale); mf.offset.y = -bottom * scale + offset; } void scaleModelToSprite(ModelDef &mf, Sprite *sprite) { if(!sprite) return; if(!sprite->hasViewAngle(0)) return; MaterialAnimator &matAnimator = sprite->viewAngle(0).material->getAnimator(Rend_SpriteMaterialSpec()); // Ensure we've up to date info about the material. matAnimator.prepare(); Texture const &texture = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture->base(); int off = de::max(0, -texture.origin().y - matAnimator.dimensions().y); scaleModel(mf, matAnimator.dimensions().y, off); } float calcModelVisualRadius(ModelDef *def) { if(!def || !def->subModelId(0)) return 0; // Use the first frame bounds. Vector3f min, max; float maxRadius = 0; for(uint i = 0; i < def->subCount(); ++i) { if(!def->subModelId(i)) break; SubmodelDef &sub = def->subModelDef(i); self.model(sub.modelId).frame(sub.frame).bounds(min, max); // Half the distance from bottom left to top right. float radius = (def->scale.x * (max.x - min.x) + def->scale.z * (max.z - min.z)) / 3.5f; if(radius > maxRadius) { maxRadius = radius; } } return maxRadius; } /** * Creates a modeldef based on the given DED info. A pretty straightforward * operation. No interlinks are set yet. Autoscaling is done and the scale * factors set appropriately. After this has been called for all available * Model DEDs, each State that has a model will have a pointer to the one * with the smallest intermark (start of a chain). */ void setupModel(defn::Model const &def) { LOG_AS("setupModel"); int const modelScopeFlags = def.geti("flags") | defs.modelFlags; int const statenum = defs.getStateNum(def.gets("state")); // Is this an ID'd model? ModelDef *modef = getModelDefWithId(def.gets("id")); if(!modef) { // No, normal State-model. if(statenum < 0) return; modef = getModelDef(statenum + def.geti("off"), def.getf("interMark"), def.geti("selector")); if(!modef) return; // Overridden or invalid definition. } // Init modef info (state & intermark already set). modef->def = def; modef->group = def.getui("group"); modef->flags = modelScopeFlags; modef->offset = Vector3f(def.get("offset")); modef->offset.y += defs.modelOffset; // Common Y axis offset. modef->scale = Vector3f(def.get("scale")); modef->scale.y *= defs.modelScale; // Common Y axis scaling. modef->resize = def.getf("resize"); modef->skinTics = de::max(def.geti("skinTics"), 1); for(int i = 0; i < 2; ++i) { modef->interRange[i] = float(def.geta("interRange")[i].asNumber()); } // Submodels. modef->clearSubs(); for(int i = 0; i < def.subCount(); ++i) { Record const &subdef = def.sub(i); SubmodelDef *sub = modef->addSub(); sub->modelId = 0; if(subdef.gets("filename").isEmpty()) continue; de::Uri const searchPath(subdef.gets("filename")); if(searchPath.isEmpty()) continue; try { String foundPath = fileSys().findPath(searchPath, RLF_DEFAULT, self.resClass(RC_MODEL)); // Ensure the found path is absolute. foundPath = App_BasePath() / foundPath; // Have we already loaded this? modelid_t modelId = modelRepository->intern(foundPath); Model *mdl = modelForId(modelId); if(!mdl) { // Attempt to load it in now. QScopedPointer hndl(&fileSys().openFile(foundPath, "rb")); mdl = Model::loadFromFile(*hndl, modelAspectMod); // We're done with the file. fileSys().releaseFile(hndl->file()); // Loaded? if(mdl) { // Add it to the repository, mdl->setModelId(modelId); modelRepository->setUserPointer(modelId, mdl); defineAllSkins(*mdl); // Enlarge the vertex buffers in preparation for drawing of this model. if(!Rend_ModelExpandVertexBuffers(mdl->vertexCount())) { LOG_RES_WARNING("Model \"%s\" contains more than %u max vertices (%i), it will not be rendered") << NativePath(foundPath).pretty() << uint(RENDER_MAX_MODEL_VERTS) << mdl->vertexCount(); } } } // Loaded? if(!mdl) continue; sub->modelId = mdl->modelId(); sub->frame = mdl->frameNumber(subdef.gets("frame")); if(sub->frame < 0) sub->frame = 0; sub->frameRange = de::max(1, subdef.geti("frameRange")); // Frame range must always be greater than zero. sub->alpha = byte(de::clamp(0, int(255 - subdef.getf("alpha") * 255), 255)); sub->blendMode = blendmode_t(subdef.geti("blendMode")); // Submodel-specific flags cancel out model-scope flags! sub->setFlags(modelScopeFlags ^ subdef.geti("flags")); // Flags may override alpha and/or blendmode. if(sub->testFlag(MFF_BRIGHTSHADOW)) { sub->alpha = byte(256 * .80f); sub->blendMode = BM_ADD; } else if(sub->testFlag(MFF_BRIGHTSHADOW2)) { sub->blendMode = BM_ADD; } else if(sub->testFlag(MFF_DARKSHADOW)) { sub->blendMode = BM_DARK; } else if(sub->testFlag(MFF_SHADOW2)) { sub->alpha = byte(256 * .2f); } else if(sub->testFlag(MFF_SHADOW1)) { sub->alpha = byte(256 * .62f); } // Extra blendmodes: if(sub->testFlag(MFF_REVERSE_SUBTRACT)) { sub->blendMode = BM_REVERSE_SUBTRACT; } else if(sub->testFlag(MFF_SUBTRACT)) { sub->blendMode = BM_SUBTRACT; } if(!subdef.gets("skinFilename").isEmpty()) { // A specific file name has been given for the skin. String const &skinFilePath = de::Uri(subdef.gets("skinFilename")).path(); String const &modelFilePath = findModelPath(sub->modelId); try { Path foundResourcePath(findSkinPath(skinFilePath, modelFilePath)); sub->skin = defineSkinAndAddToModelIndex(*mdl, foundResourcePath); } catch(FS1::NotFoundError const &) { LOG_RES_WARNING("Failed to locate skin \"%s\" for model \"%s\"") << subdef.gets("skinFilename") << NativePath(modelFilePath).pretty(); } } else { sub->skin = subdef.geti("skin"); } // Skin range must always be greater than zero. sub->skinRange = de::max(subdef.geti("skinRange"), 1); // Offset within the model. sub->offset = subdef.get("offset"); if(!subdef.gets("shinySkin").isEmpty()) { String const &skinFilePath = de::Uri(subdef.gets("shinySkin")).path(); String const &modelFilePath = findModelPath(sub->modelId); try { de::Uri foundResourceUri(Path(findSkinPath(skinFilePath, modelFilePath))); sub->shinySkin = self.defineTexture("ModelReflectionSkins", foundResourceUri); } catch(FS1::NotFoundError const &) { LOG_RES_WARNING("Failed to locate skin \"%s\" for model \"%s\"") << skinFilePath << NativePath(modelFilePath).pretty(); } } else { sub->shinySkin = 0; } // Should we allow texture compression with this model? if(sub->testFlag(MFF_NO_TEXCOMP)) { // All skins of this model will no longer use compression. mdl->setFlags(Model::NoTextureCompression); } } catch(FS1::NotFoundError const &) { LOG_RES_WARNING("Failed to locate \"%s\"") << searchPath; } } // Do scaling, if necessary. if(modef->resize) { scaleModel(*modef, modef->resize, modef->offset.y); } else if(modef->state && modef->testSubFlag(0, MFF_AUTOSCALE)) { spritenum_t sprNum = Def_GetSpriteNum(def.gets("sprite")); int sprFrame = def.geti("spriteFrame"); if(sprNum < 0) { // No sprite ID given. sprNum = modef->state->sprite; sprFrame = modef->state->frame; } if(Sprite *sprite = self.spritePtr(sprNum, sprFrame)) { scaleModelToSprite(*modef, sprite); } } if(modef->state) { int stateNum = runtimeDefs.states.indexOf(modef->state); // Associate this modeldef with its state. if(stateModefs[stateNum] < 0) { // No modef; use this. stateModefs[stateNum] = self.indexOf(modef); } else { // Must check intermark; smallest wins! ModelDef *other = self.modelDefForState(stateNum); if((modef->interMark <= other->interMark && // Should never be == modef->select == other->select) || modef->select < other->select) // Smallest selector? { stateModefs[stateNum] = self.indexOf(modef); } } } // Calculate the particle offset for each submodel. Vector3f min, max; for(uint i = 0; i < modef->subCount(); ++i) { SubmodelDef *sub = &modef->subModelDef(i); if(sub->modelId && sub->frame >= 0) { self.model(sub->modelId).frame(sub->frame).bounds(min, max); modef->setParticleOffset(i, ((max + min) / 2 + sub->offset) * modef->scale + modef->offset); } } modef->visualRadius = calcModelVisualRadius(modef); // based on geometry bounds // Shadow radius can be specified manually. modef->shadowRadius = def.getf("shadowRadius"); } static int destroyModelInRepository(StringPool::Id id, void *context) { ModelRepository &modelRepository = *static_cast(context); if(Model *model = reinterpret_cast(modelRepository.userPointer(id))) { modelRepository.setUserPointer(id, 0); delete model; } return 0; } void clearModelList() { if(!modelRepository) return; modelRepository->iterate(destroyModelInRepository, modelRepository); } #endif /// Observes MaterialScheme ManifestDefined. void materialSchemeManifestDefined(MaterialScheme & /*scheme*/, MaterialManifest &manifest) { /// Number of elements to block-allocate in the material index to material manifest map. int const MANIFESTIDMAP_BLOCK_ALLOC = 32; // We want notification when the manifest is derived to produce a material. manifest.audienceForMaterialDerived += this; // We want notification when the manifest is about to be deleted. manifest.audienceForDeletion += this; // Acquire a new unique identifier for the manifest. materialid_t const id = ++materialManifestCount; // 1-based. manifest.setId(id); // Add the new manifest to the id index/map. if(materialManifestCount > (int)materialManifestIdMapSize) { // Allocate more memory. materialManifestIdMapSize += MANIFESTIDMAP_BLOCK_ALLOC; materialManifestIdMap = (MaterialManifest **) M_Realloc(materialManifestIdMap, sizeof(*materialManifestIdMap) * materialManifestIdMapSize); } materialManifestIdMap[materialManifestCount - 1] = &manifest; } /// Observes MaterialManifest MaterialDerived. void materialManifestMaterialDerived(MaterialManifest & /*manifest*/, Material &material) { // Include this new material in the scheme-agnostic list of instances. materials.append(&material); // We want notification when the material is about to be deleted. material.audienceForDeletion() += this; } /// Observes MaterialManifest Deletion. void materialManifestBeingDeleted(MaterialManifest const &manifest) { foreach(MaterialManifestGroup *group, materialGroups) { group->remove(const_cast(&manifest)); } materialManifestIdMap[manifest.id() - 1 /*1-based*/] = 0; // There will soon be one fewer manifest in the system. materialManifestCount -= 1; } /// Observes Material Deletion. void materialBeingDeleted(Material const &material) { materials.removeOne(const_cast(&material)); } /// Observes TextureScheme ManifestDefined. void textureSchemeManifestDefined(TextureScheme & /*scheme*/, TextureManifest &manifest) { // We want notification when the manifest is derived to produce a texture. manifest.audienceForTextureDerived += this; } /// Observes TextureManifest TextureDerived. void textureManifestTextureDerived(TextureManifest & /*manifest*/, Texture &texture) { // Include this new texture in the scheme-agnostic list of instances. textures.append(&texture); // We want notification when the texture is about to be deleted. texture.audienceForDeletion += this; } /// Observes Texture Deletion. void textureBeingDeleted(Texture const &texture) { textures.removeOne(const_cast(&texture)); } #ifdef __CLIENT__ /// Observes FontScheme ManifestDefined. void fontSchemeManifestDefined(FontScheme & /*scheme*/, FontManifest &manifest) { // We want notification when the manifest is derived to produce a resource. //manifest.audienceForFontDerived += this; // We want notification when the manifest is about to be deleted. manifest.audienceForDeletion += this; // Acquire a new unique identifier for the manifest. fontid_t const id = ++fontManifestCount; // 1-based. manifest.setUniqueId(id); // Add the new manifest to the id index/map. if(fontManifestCount > fontManifestIdMapSize) { // Allocate more memory. fontManifestIdMapSize += 32; fontManifestIdMap = (FontManifest **) M_Realloc(fontManifestIdMap, sizeof(*fontManifestIdMap) * fontManifestIdMapSize); } fontManifestIdMap[fontManifestCount - 1] = &manifest; } #if 0 /// Observes FontManifest FontDerived. void fontManifestFontDerived(FontManifest & /*manifest*/, AbstractFont &font) { // Include this new font in the scheme-agnostic list of instances. fonts.append(&font); // We want notification when the font is about to be deleted. font.audienceForDeletion += this; } #endif /// Observes FontManifest Deletion. void fontManifestBeingDeleted(FontManifest const &manifest) { fontManifestIdMap[manifest.uniqueId() - 1 /*1-based*/] = 0; // There will soon be one fewer manifest in the system. fontManifestCount -= 1; } /// Observes AbstractFont Deletion. void fontBeingDeleted(AbstractFont const &font) { fonts.removeOne(const_cast(&font)); } /// Observes ColorPalette ColorTableChange void colorPaletteColorTableChanged(ColorPalette &colorPalette) { // Release all GL-textures prepared using @a colorPalette. foreach(Texture *texture, textures) { colorpalette_analysis_t *cp = reinterpret_cast(texture->analysisDataPointer(Texture::ColorPaletteAnalysis)); if(cp && cp->paletteId == colorpaletteid_t(colorPalette.id())) { texture->releaseGLTextures(); } } } #endif // __CLIENT__ /** * Asynchronous task that attempts conversion of a legacy savegame. Each converter * plugin is tried in turn. */ class ConvertSavegameTask : public Task { ddhook_savegame_convert_t parm; public: ConvertSavegameTask(String const &sourcePath, String const &gameId) { // Ensure the game is defined (sanity check). /*Game &game = */ App_Games().byIdentityKey(gameId); // Ensure the output folder exists if it doesn't already. String const outputPath = String("/home/savegames") / gameId; App::fileSystem().makeFolder(outputPath); Str_Set(Str_InitStd(&parm.sourcePath), sourcePath.toUtf8().constData()); Str_Set(Str_InitStd(&parm.outputPath), outputPath.toUtf8().constData()); Str_Set(Str_InitStd(&parm.fallbackGameId), gameId.toUtf8().constData()); } ~ConvertSavegameTask() { Str_Free(&parm.sourcePath); Str_Free(&parm.outputPath); Str_Free(&parm.fallbackGameId); } void runTask() { DD_CallHooks(HOOK_SAVEGAME_CONVERT, 0, &parm); } }; TaskPool convertSavegameTasks; void loopIteration() { if(convertSavegameTasks.isDone()) { LOG_AS("ResourceSystem"); Loop::get().audienceForIteration() -= this; try { // The newly converted savegame(s) should now be somewhere in /home/savegames App::rootFolder().locate("/home/savegames").populate(); } catch(Folder::NotFoundError const &) {} // Ignore. } } void beginConvertLegacySavegame(String const &sourcePath, String const &gameId) { LOG_AS("ResourceSystem"); LOG_TRACE("Scheduling legacy savegame conversion for %s (gameId:%s)") << sourcePath << gameId; Loop::get().audienceForIteration() += this; convertSavegameTasks.start(new ConvertSavegameTask(sourcePath, gameId)); } void locateLegacySavegames(String const &gameId) { LOG_AS("ResourceSystem"); String const legacySavePath = String("/legacysavegames") / gameId; if(Folder *oldSaveFolder = App::rootFolder().tryLocate(legacySavePath)) { // Add any new legacy savegames which may have appeared in this folder. oldSaveFolder->populate(Folder::PopulateOnlyThisFolder /* no need to go deep */); } else { try { // Make and setup a feed for the /legacysavegames/ subfolder if the game // might have legacy savegames we may need to convert later. NativePath const oldSavePath = App_Games().byIdentityKey(gameId).legacySavegamePath(); if(oldSavePath.exists() && oldSavePath.isReadable()) { App::fileSystem().makeFolderWithFeed(legacySavePath, new DirectoryFeed(oldSavePath), Folder::PopulateOnlyThisFolder /* no need to go deep */); } } catch(Games::NotFoundError const &) {} // Ignore this error } } }; ResourceSystem::ResourceSystem() : d(new Instance(this)) {} void ResourceSystem::timeChanged(Clock const &) { // Nothing to do. } ResourceClass &ResourceSystem::resClass(String name) { if(!name.isEmpty()) { foreach(ResourceClass *resClass, d->resClasses) { if(!resClass->name().compareWithoutCase(name)) return *resClass; } } return d->nullResourceClass; // Not found. } ResourceClass &ResourceSystem::resClass(resourceclassid_t id) { if(id == RC_NULL) return d->nullResourceClass; if(VALID_RESOURCECLASSID(id)) { return *d->resClasses.at(uint(id)); } /// @throw UnknownResourceClass Attempted with an unknown id. throw UnknownResourceClassError("ResourceSystem::toClass", QString("Invalid id '%1'").arg(int(id))); } void ResourceSystem::updateOverrideIWADPathFromConfig() { String path = App::config().gets("resource.iwadFolder", ""); if(!path.isEmpty()) { LOG_RES_NOTE("Using user-selected primary IWAD folder: \"%s\"") << path; FS1::Scheme &ps = App_FileSystem().scheme(App_ResourceClass("RC_PACKAGE").defaultScheme()); ps.clearSearchPathGroup(FS1::OverridePaths); ps.addSearchPath(SearchPath(de::Uri::fromNativeDirPath(path, RC_PACKAGE), SearchPath::NoDescend), FS1::OverridePaths); } } void ResourceSystem::clearAllResources() { clearAllRuntimeResources(); clearAllSystemResources(); } void ResourceSystem::clearAllRuntimeResources() { #ifdef __CLIENT__ d->clearRuntimeFonts(); #endif d->clearRuntimeTextures(); } void ResourceSystem::clearAllSystemResources() { #ifdef __CLIENT__ d->clearSystemFonts(); #endif d->clearSystemTextures(); } int ResourceSystem::spriteCount() { return d->spritesByFrame.count(); } bool ResourceSystem::hasSprite(spritenum_t spriteId, int frame) { if(d->hasSpriteFrameSet(spriteId)) { if(frame >= 0 && frame < d->findSpriteFrameSet(spriteId).count()) return true; } return false; } Sprite &ResourceSystem::sprite(spritenum_t spriteId, int frame) { return *d->findSpriteFrameSet(spriteId).at(frame); } ResourceSystem::SpriteSet const &ResourceSystem::spriteSet(spritenum_t spriteId) { return d->findSpriteFrameSet(spriteId); } void ResourceSystem::initTextures() { LOG_AS("ResourceSystem"); d->initCompositeTextures(); d->initFlatTextures(); d->initSpriteTextures(); } void ResourceSystem::initSystemTextures() { LOG_AS("ResourceSystem"); static const struct TexDef { String const graphicName; String const path; } texDefs[] = { { "unknown", "unknown" }, { "missing", "missing" }, { "bbox", "bbox" }, { "gray", "gray" }, { "boxcorner", "ui/boxcorner" }, { "boxfill", "ui/boxfill" }, { "boxshade", "ui/boxshade" }, { "", "" } }; LOG_RES_VERBOSE("Initializing System textures..."); for(uint i = 0; !texDefs[i].graphicName.isEmpty(); ++i) { struct TexDef const &def = texDefs[i]; int uniqueId = i + 1/*1-based index*/; de::Uri resourceUri("Graphics", Path(def.graphicName)); declareTexture(de::Uri("System", Path(def.path)), Texture::Custom, Vector2i(), Vector2i(), uniqueId, &resourceUri); } // Define any as yet undefined system textures. /// @todo Defer until necessary (manifest texture is first referenced). d->deriveAllTexturesInScheme("System"); } Texture *ResourceSystem::texture(String schemeName, de::Uri const &resourceUri) { if(!resourceUri.isEmpty()) { if(!resourceUri.path().toStringRef().compareWithoutCase("-")) { return nullptr; } try { return &textureScheme(schemeName).findByResourceUri(resourceUri).texture(); } catch(TextureManifest::MissingTextureError const &) {} // Ignore this error. catch(TextureScheme::NotFoundError const &) {} // Ignore this error. } return nullptr; } Texture *ResourceSystem::defineTexture(String schemeName, de::Uri const &resourceUri, Vector2i const &dimensions) { LOG_AS("ResourceSystem::defineTexture"); if(resourceUri.isEmpty()) return 0; // Have we already created one for this? TextureScheme &scheme = textureScheme(schemeName); try { return &scheme.findByResourceUri(resourceUri).texture(); } catch(TextureManifest::MissingTextureError const &) {} // Ignore this error. catch(TextureScheme::NotFoundError const &) {} // Ignore this error. int uniqueId = scheme.count() + 1; // 1-based index. if(M_NumDigits(uniqueId) > 8) { LOG_RES_WARNING("Failed declaring texture manifest in scheme %s (max:%i)") << schemeName << DDMAXINT; return 0; } de::Uri uri(scheme.name(), Path(String("%1").arg(uniqueId, 8, 10, QChar('0')))); try { TextureManifest &manifest = declareTexture(uri, Texture::Custom, dimensions, Vector2i(), uniqueId, &resourceUri); /// @todo Defer until necessary (manifest texture is first referenced). return deriveTexture(manifest); } catch(TextureScheme::InvalidPathError const &er) { LOG_RES_WARNING("Failed declaring texture \"%s\": %s") << uri << er.asText(); } return 0; } patchid_t ResourceSystem::declarePatch(String encodedName) { LOG_AS("ResourceSystem::declarePatch"); if(encodedName.isEmpty()) return 0; de::Uri uri("Patches", Path(encodedName)); // Already defined as a patch? try { TextureManifest &manifest = textureManifest(uri); /// @todo We should instead define Materials from patches and return the material id. return patchid_t( manifest.uniqueId() ); } catch(MissingManifestError const &) {} // Ignore this error. Path lumpPath = uri.path() + ".lmp"; if(!d->fileSys().nameIndex().contains(lumpPath)) { LOG_RES_WARNING("Failed to locate lump for \"%s\"") << uri; return 0; } lumpnum_t const lumpNum = d->fileSys().nameIndex().findLast(lumpPath); File1 &file = d->fileSys().lump(lumpNum); Texture::Flags flags; if(file.container().hasCustom()) flags |= Texture::Custom; Vector2i dimensions; Vector2i origin; // If this is a Patch (the format) read the world dimension and origin offset values. ByteRefArray fileData = ByteRefArray(file.cache(), file.size()); if(Patch::recognize(fileData)) { try { Patch::Metadata info = Patch::loadMetadata(fileData); dimensions = info.logicalDimensions; origin = Vector2i(-info.origin.x, -info.origin.y); } catch(IByteArray::OffsetError const &) { LOG_RES_WARNING("File \"%s:%s\" does not appear to be a valid Patch. " "World dimension and origin offset not set for patch \"%s\".") << NativePath(file.container().composePath()).pretty() << NativePath(file.composePath()).pretty() << uri; } } file.unlock(); int uniqueId = textureScheme("Patches").count() + 1; // 1-based index. de::Uri resourceUri = composeLumpIndexResourceUrn(lumpNum); try { TextureManifest &manifest = declareTexture(uri, flags, dimensions, origin, uniqueId, &resourceUri); /// @todo Defer until necessary (manifest texture is first referenced). deriveTexture(manifest); return uniqueId; } catch(TextureScheme::InvalidPathError const &er) { LOG_RES_WARNING("Failed declaring texture \"%s\": %s") << uri << er.asText(); } return 0; } rawtex_t *ResourceSystem::rawTexture(lumpnum_t lumpNum) { LOG_AS("ResourceSystem::rawTexture"); if(-1 == lumpNum || lumpNum >= App_FileSystem().lumpCount()) { LOGDEV_RES_WARNING("LumpNum #%i out of bounds (%i), returning 0") << lumpNum << App_FileSystem().lumpCount(); return 0; } Instance::RawTextureHash::iterator found = d->rawTexHash.find(lumpNum); if(found != d->rawTexHash.end()) { return found.value(); } return 0; } rawtex_t *ResourceSystem::declareRawTexture(lumpnum_t lumpNum) { LOG_AS("ResourceSystem::rawTexture"); if(-1 == lumpNum || lumpNum >= App_FileSystem().lumpCount()) { LOGDEV_RES_WARNING("LumpNum #%i out of range %s, returning 0") << lumpNum << Rangeui(0, App_FileSystem().lumpCount()).asText(); return 0; } // Has this raw texture already been declared? rawtex_t *raw = rawTexture(lumpNum); if(!raw) { // An entirely new raw texture. String const &name = App_FileSystem().lump(lumpNum).name(); raw = new rawtex_t(name, lumpNum); d->rawTexHash.insert(lumpNum, raw); } return raw; } QList ResourceSystem::collectRawTextures() const { return d->rawTexHash.values(); } void ResourceSystem::clearAllRawTextures() { qDeleteAll(d->rawTexHash); d->rawTexHash.clear(); } MaterialScheme &ResourceSystem::materialScheme(String name) const { LOG_AS("ResourceSystem::materialScheme"); if(!name.isEmpty()) { auto found = d->materialSchemes.find(name.toLower()); if(found != d->materialSchemes.end()) return **found; } /// @throw UnknownSchemeError An unknown scheme was referenced. throw UnknownSchemeError("ResourceSystem::materialScheme", "No scheme found matching '" + name + "'"); } bool ResourceSystem::knownMaterialScheme(String name) const { if(!name.isEmpty()) { return d->materialSchemes.contains(name.toLower()); } return false; } int ResourceSystem::materialSchemeCount() const { return d->materialSchemes.count(); } LoopResult ResourceSystem::forAllMaterialSchemes(std::function func) const { for(MaterialScheme *scheme : d->materialSchemes) { if(auto result = func(*scheme)) return result; } return LoopContinue; } MaterialManifest &ResourceSystem::toMaterialManifest(materialid_t id) const { duint32 idx = id - 1; // 1-based index. if(idx < (duint32)d->materialManifestCount) { if(d->materialManifestIdMap[idx]) { return *d->materialManifestIdMap[idx]; } // Internal bookeeping error. DENG2_ASSERT(false); } /// @throw InvalidMaterialIdError The specified material id is invalid. throw UnknownMaterialIdError("ResourceSystem::toMaterialManifest", "Invalid material ID " + String::number(id) + ", valid range " + Rangei(1, d->materialManifestCount + 1).asText()); } bool ResourceSystem::hasMaterialManifest(de::Uri const &path) const { try { materialManifest(path); return true; } catch(MissingManifestError const &) {} // Ignore this error. return false; } MaterialManifest &ResourceSystem::materialManifest(de::Uri const &uri) const { LOG_AS("ResourceSystem::materialManifest"); // Does the user want a manifest in a specific scheme? if(!uri.scheme().isEmpty()) { MaterialScheme &specifiedScheme = materialScheme(uri.scheme()); if(specifiedScheme.has(uri.path())) { return specifiedScheme.find(uri.path()); } } else { // No, check each scheme in priority order. foreach(MaterialScheme *scheme, d->materialSchemeCreationOrder) { if(scheme->has(uri.path())) { return scheme->find(uri.path()); } } } /// @throw NotFoundError Failed to locate a matching manifest. throw MissingManifestError("ResourceSystem::materialManifest", "Failed to locate a manifest matching \"" + uri.asText() + "\""); } int ResourceSystem::materialCount() const { return d->materials.count(); } LoopResult ResourceSystem::forAllMaterials(std::function func) const { for(Material *mat : d->materials) { if(auto result = func(*mat)) return result; } return LoopContinue; } ResourceSystem::MaterialManifestGroup &ResourceSystem::newMaterialGroup() { // Allocating one by one is inefficient, but it doesn't really matter. d->materialGroups.append(new MaterialManifestGroup()); return *d->materialGroups.back(); } ResourceSystem::MaterialManifestGroup &ResourceSystem::materialGroup(int groupIdx) const { groupIdx -= 1; // 1-based index. if(groupIdx >= 0 && groupIdx < d->materialGroups.count()) { return *d->materialGroups[groupIdx]; } /// @throw UnknownMaterialGroupError An unknown material group was referenced. throw UnknownMaterialGroupError("ResourceSystem::materialGroup", "Invalid group #" + String::number(groupIdx+1) + ", valid range " + Rangeui(1, d->materialGroups.count() + 1).asText()); } ResourceSystem::MaterialManifestGroups const &ResourceSystem::allMaterialGroups() const { return d->materialGroups; } void ResourceSystem::clearAllMaterialGroups() { qDeleteAll(d->materialGroups); d->materialGroups.clear(); } TextureScheme &ResourceSystem::textureScheme(String name) const { LOG_AS("ResourceSystem::textureScheme"); if(!name.isEmpty()) { TextureSchemes::iterator found = d->textureSchemes.find(name.toLower()); if(found != d->textureSchemes.end()) { return **found; } } /// @throw UnknownSchemeError An unknown scheme was referenced. throw UnknownSchemeError("ResourceSystem::textureScheme", "No scheme found matching '" + name + "'"); } bool ResourceSystem::knownTextureScheme(String name) const { if(!name.isEmpty()) { return d->textureSchemes.contains(name.toLower()); } return false; } ResourceSystem::TextureSchemes const& ResourceSystem::allTextureSchemes() const { return d->textureSchemes; } bool ResourceSystem::hasTextureManifest(de::Uri const &path) const { try { textureManifest(path); return true; } catch(MissingManifestError const &) {} // Ignore this error. return false; } TextureManifest &ResourceSystem::textureManifest(de::Uri const &uri) const { LOG_AS("ResourceSystem::findTexture"); // Perform the search. // Is this a URN? (of the form "urn:schemename:uniqueid") if(!uri.scheme().compareWithoutCase("urn")) { String const &pathStr = uri.path().toStringRef(); int uIdPos = pathStr.indexOf(':'); if(uIdPos > 0) { String schemeName = pathStr.left(uIdPos); int uniqueId = pathStr.mid(uIdPos + 1 /*skip delimiter*/).toInt(); try { return textureScheme(schemeName).findByUniqueId(uniqueId); } catch(TextureScheme::NotFoundError const &) {} // Ignore, we'll throw our own... } } else { // No, this is a URI. String const &path = uri.path(); // Does the user want a manifest in a specific scheme? if(!uri.scheme().isEmpty()) { try { return textureScheme(uri.scheme()).find(path); } catch(TextureScheme::NotFoundError const &) {} // Ignore, we'll throw our own... } else { // No, check each scheme in priority order. foreach(TextureScheme *scheme, d->textureSchemeCreationOrder) { try { return scheme->find(path); } catch(TextureScheme::NotFoundError const &) {} // Ignore, we'll throw our own... } } } /// @throw MissingManifestError Failed to locate a matching manifest. throw MissingManifestError("ResourceSystem::findTexture", "Failed to locate a manifest matching \"" + uri.asText() + "\""); } ResourceSystem::AllTextures const &ResourceSystem::allTextures() const { return d->textures; } #ifdef __CLIENT__ void ResourceSystem::releaseAllSystemGLTextures() { if(novideo) return; LOG_AS("ResourceSystem"); LOG_RES_VERBOSE("Releasing system textures..."); // The rendering lists contain persistent references to texture names. // Which, obviously, can't persist any longer... ClientApp::renderSystem().clearDrawLists(); GL_ReleaseAllLightingSystemTextures(); GL_ReleaseAllFlareTextures(); releaseGLTexturesByScheme("System"); Rend_ParticleReleaseSystemTextures(); releaseFontGLTexturesByScheme("System"); pruneUnusedTextureSpecs(); } void ResourceSystem::releaseAllRuntimeGLTextures() { if(novideo) return; LOG_AS("ResourceSystem"); LOG_RES_VERBOSE("Releasing runtime textures..."); // The rendering lists contain persistent references to texture names. // Which, obviously, can't persist any longer... ClientApp::renderSystem().clearDrawLists(); // texture-wrapped GL textures; textures, flats, sprites... releaseGLTexturesByScheme("Flats"); releaseGLTexturesByScheme("Textures"); releaseGLTexturesByScheme("Patches"); releaseGLTexturesByScheme("Sprites"); releaseGLTexturesByScheme("Details"); releaseGLTexturesByScheme("Reflections"); releaseGLTexturesByScheme("Masks"); releaseGLTexturesByScheme("ModelSkins"); releaseGLTexturesByScheme("ModelReflectionSkins"); releaseGLTexturesByScheme("Lightmaps"); releaseGLTexturesByScheme("Flaremaps"); GL_ReleaseTexturesForRawImages(); Rend_ParticleReleaseExtraTextures(); releaseFontGLTexturesByScheme("Game"); pruneUnusedTextureSpecs(); } void ResourceSystem::releaseAllGLTextures() { releaseAllRuntimeGLTextures(); releaseAllSystemGLTextures(); } void ResourceSystem::releaseGLTexturesByScheme(String schemeName) { if(schemeName.isEmpty()) return; PathTreeIterator iter(textureScheme(schemeName).index().leafNodes()); while(iter.hasNext()) { TextureManifest &manifest = iter.next(); if(manifest.hasTexture()) { manifest.texture().releaseGLTextures(); } } } void ResourceSystem::clearAllTextureSpecs() { d->clearAllTextureSpecs(); } void ResourceSystem::pruneUnusedTextureSpecs() { if(Sys_IsShuttingDown()) return; int numPruned = 0; numPruned += d->pruneUnusedTextureSpecs(TST_GENERAL); numPruned += d->pruneUnusedTextureSpecs(TST_DETAIL); LOGDEV_RES_VERBOSE("Pruned %i unused texture variant %s") << numPruned << (numPruned == 1? "specification" : "specifications"); } TextureVariantSpec const &ResourceSystem::textureSpec(texturevariantusagecontext_t tc, int flags, byte border, int tClass, int tMap, int wrapS, int wrapT, int minFilter, int magFilter, int anisoFilter, dd_bool mipmapped, dd_bool gammaCorrection, dd_bool noStretch, dd_bool toAlpha) { TextureVariantSpec *tvs = d->textureSpec(tc, flags, border, tClass, tMap, wrapS, wrapT, minFilter, magFilter, anisoFilter, mipmapped, gammaCorrection, noStretch, toAlpha); #ifdef DENG_DEBUG if(tClass || tMap) { DENG2_ASSERT(tvs->variant.flags & TSF_HAS_COLORPALETTE_XLAT); DENG2_ASSERT(tvs->variant.tClass == tClass); DENG2_ASSERT(tvs->variant.tMap == tMap); } #endif return *tvs; } TextureVariantSpec &ResourceSystem::detailTextureSpec(float contrast) { return *d->detailTextureSpec(contrast); } FontScheme &ResourceSystem::fontScheme(String name) const { LOG_AS("ResourceSystem::fontScheme"); if(!name.isEmpty()) { FontSchemes::iterator found = d->fontSchemes.find(name.toLower()); if(found != d->fontSchemes.end()) { return **found; } } /// @throw UnknownSchemeError An unknown scheme was referenced. throw UnknownSchemeError("ResourceSystem::fontScheme", "No scheme found matching '" + name + "'"); } bool ResourceSystem::knownFontScheme(String name) const { if(!name.isEmpty()) { return d->fontSchemes.contains(name.toLower()); } return false; } ResourceSystem::FontSchemes const &ResourceSystem::allFontSchemes() const { return d->fontSchemes; } bool ResourceSystem::hasFont(de::Uri const &path) const { try { fontManifest(path); return true; } catch(MissingManifestError const &) {} // Ignore this error. return false; } FontManifest &ResourceSystem::fontManifest(de::Uri const &uri) const { LOG_AS("ResourceSystem::findFont"); // Perform the search. // Is this a URN? (of the form "urn:schemename:uniqueid") if(!uri.scheme().compareWithoutCase("urn")) { String const &pathStr = uri.path().toStringRef(); int uIdPos = pathStr.indexOf(':'); if(uIdPos > 0) { String schemeName = pathStr.left(uIdPos); int uniqueId = pathStr.mid(uIdPos + 1 /*skip delimiter*/).toInt(); try { return fontScheme(schemeName).findByUniqueId(uniqueId); } catch(FontScheme::NotFoundError const &) {} // Ignore, we'll throw our own... } } else { // No, this is a URI. String const &path = uri.path(); // Does the user want a manifest in a specific scheme? if(!uri.scheme().isEmpty()) { try { return fontScheme(uri.scheme()).find(path); } catch(FontScheme::NotFoundError const &) {} // Ignore, we'll throw our own... } else { // No, check each scheme in priority order. foreach(FontScheme *scheme, d->fontSchemeCreationOrder) { try { return scheme->find(path); } catch(FontScheme::NotFoundError const &) {} // Ignore, we'll throw our own... } } } /// @throw MissingManifestError Failed to locate a matching manifest. throw MissingManifestError("ResourceSystem::findFont", "Failed to locate a manifest matching \"" + uri.asText() + "\""); } FontManifest &ResourceSystem::toFontManifest(fontid_t id) const { if(id > 0 && id <= d->fontManifestCount) { duint32 idx = id - 1; // 1-based index. if(d->fontManifestIdMap[idx]) { return *d->fontManifestIdMap[idx]; } // Internal bookeeping error. DENG2_ASSERT(false); } /// @throw UnknownIdError The specified manifest id is invalid. throw UnknownFontIdError("ResourceSystem::toFontManifest", QString("Invalid font ID %1, valid range [1..%2)").arg(id).arg(d->fontManifestCount + 1)); } ResourceSystem::AllFonts const &ResourceSystem::allFonts() const { return d->fonts; } AbstractFont *ResourceSystem::newFontFromDef(ded_compositefont_t const &def) { LOG_AS("ResourceSystem::newFontFromDef"); if(!def.uri) return 0; de::Uri const &uri = *def.uri; try { // Create/retrieve a manifest for the would-be font. FontManifest &manifest = declareFont(uri); if(manifest.hasResource()) { if(CompositeBitmapFont *compFont = manifest.resource().maybeAs()) { /// @todo Do not update fonts here (not enough knowledge). We should /// instead return an invalid reference/signal and force the caller /// to implement the necessary update logic. LOGDEV_RES_XVERBOSE("Font with uri \"%s\" already exists, returning existing") << manifest.composeUri(); compFont->rebuildFromDef(def); } return &manifest.resource(); } // A new font. manifest.setResource(CompositeBitmapFont::fromDef(manifest, def)); if(manifest.hasResource()) { if(verbose >= 1) { LOG_RES_VERBOSE("New font \"%s\"") << manifest.composeUri(); } return &manifest.resource(); } LOG_RES_WARNING("Failed defining new Font for \"%s\"") << NativePath(uri.asText()).pretty(); } catch(UnknownSchemeError const &er) { LOG_RES_WARNING("Failed declaring font \"%s\": %s") << NativePath(uri.asText()).pretty() << er.asText(); } catch(FontScheme::InvalidPathError const &er) { LOG_RES_WARNING("Failed declaring font \"%s\": %s") << NativePath(uri.asText()).pretty() << er.asText(); } return 0; } AbstractFont *ResourceSystem::newFontFromFile(de::Uri const &uri, String filePath) { LOG_AS("ResourceSystem::newFontFromFile"); if(!d->fileSys().accessFile(de::Uri::fromNativePath(filePath))) { LOGDEV_RES_WARNING("Ignoring invalid filePath: ") << filePath; return 0; } try { // Create/retrieve a manifest for the would-be font. FontManifest &manifest = declareFont(uri); if(manifest.hasResource()) { if(BitmapFont *bmapFont = manifest.resource().maybeAs()) { /// @todo Do not update fonts here (not enough knowledge). We should /// instead return an invalid reference/signal and force the caller /// to implement the necessary update logic. LOGDEV_RES_XVERBOSE("Font with uri \"%s\" already exists, returning existing") << manifest.composeUri(); bmapFont->setFilePath(filePath); } return &manifest.resource(); } // A new font. manifest.setResource(BitmapFont::fromFile(manifest, filePath)); if(manifest.hasResource()) { if(verbose >= 1) { LOG_RES_VERBOSE("New font \"%s\"") << manifest.composeUri(); } return &manifest.resource(); } LOG_RES_WARNING("Failed defining new Font for \"%s\"") << NativePath(uri.asText()).pretty(); } catch(UnknownSchemeError const &er) { LOG_RES_WARNING("Failed declaring font \"%s\": %s") << NativePath(uri.asText()).pretty() << er.asText(); } catch(FontScheme::InvalidPathError const &er) { LOG_RES_WARNING("Failed declaring font \"%s\": %s") << NativePath(uri.asText()).pretty() << er.asText(); } return 0; } void ResourceSystem::releaseFontGLTexturesByScheme(String schemeName) { if(schemeName.isEmpty()) return; PathTreeIterator iter(fontScheme(schemeName).index().leafNodes()); while(iter.hasNext()) { FontManifest &manifest = iter.next(); if(manifest.hasResource()) { manifest.resource().glDeinit(); } } } Model &ResourceSystem::model(modelid_t id) { if(Model *model = d->modelForId(id)) { return *model; } /// @throw MissingResourceError An unknown/invalid id was specified. throw MissingResourceError("ResourceSystem::model", "Invalid id " + String::number(id)); } bool ResourceSystem::hasModelDef(de::String id) const { if(!id.isEmpty()) { foreach(ModelDef const &modef, d->modefs) { if(!id.compareWithoutCase(modef.id)) { return true; } } } return false; } ModelDef &ResourceSystem::modelDef(int index) { if(index >= 0 && index < modelDefCount()) { return d->modefs[index]; } /// @throw MissingModelDefError An unknown model definition was referenced. throw MissingModelDefError("ResourceSystem::modelDef", "Invalid index #" + String::number(index) + ", valid range " + Rangeui(0, modelDefCount()).asText()); } ModelDef &ResourceSystem::modelDef(String id) { if(!id.isEmpty()) { foreach(ModelDef const &modef, d->modefs) { if(!id.compareWithoutCase(modef.id)) { return const_cast(modef); } } } /// @throw MissingModelDefError An unknown model definition was referenced. throw MissingModelDefError("ResourceSystem::modelDef", "Invalid id '" + id + "'"); } ModelDef *ResourceSystem::modelDefForState(int stateIndex, int select) { if(stateIndex < 0 || stateIndex >= defs.states.size()) { return 0; } if(stateIndex < 0 || stateIndex >= d->stateModefs.count()) { return 0; } if(d->stateModefs[stateIndex] < 0) { return 0; } DENG2_ASSERT(d->stateModefs[stateIndex] >= 0); DENG2_ASSERT(d->stateModefs[stateIndex] < d->modefs.count()); ModelDef *def = &d->modefs[d->stateModefs[stateIndex]]; if(select) { // Choose the correct selector, or selector zero if the given one not available. int const mosel = select & DDMOBJ_SELECTOR_MASK; for(ModelDef *it = def; it; it = it->selectNext) { if(it->select == mosel) { return it; } } } return def; } int ResourceSystem::modelDefCount() const { return d->modefs.count(); } void ResourceSystem::initModels() { LOG_AS("ResourceSystem"); if(CommandLine_Check("-nomd2")) { LOG_RES_NOTE("3D models are disabled"); return; } LOG_RES_VERBOSE("Initializing Models..."); Time begunAt; d->clearModelList(); d->modefs.clear(); delete d->modelRepository; d->modelRepository = new StringPool(); // There can't be more modeldefs than there are DED Models. d->modefs.resize(defs.models.size()); // Clear the stateid => modeldef LUT. d->stateModefs.resize(runtimeDefs.states.size()); for(int i = 0; i < runtimeDefs.states.size(); ++i) { d->stateModefs[i] = -1; } // Read in the model files and their data. // Use the latest definition available for each sprite ID. for(int i = int(defs.models.size()) - 1; i >= 0; --i) { if(!(i % 100)) { // This may take a while, so keep updating the progress. Con_SetProgress(130 + 70*(defs.models.size() - i)/defs.models.size()); } d->setupModel(defs.models[i]); } // Create interlinks. Note that the order in which the defs were loaded // is important. We want to allow "patch" definitions, right? // For each modeldef we will find the "next" def. for(int i = d->modefs.count() - 1; i >= 0; --i) { ModelDef *me = &d->modefs[i]; float minmark = 2; // max = 1, so this is "out of bounds". ModelDef *closest = 0; for(int k = d->modefs.count() - 1; k >= 0; --k) { ModelDef *other = &d->modefs[k]; /// @todo Need an index by state. -jk if(other->state != me->state) continue; // Same state and a bigger order are the requirements. if(other->def.order() > me->def.order() && // Defined after me. other->interMark > me->interMark && other->interMark < minmark) { minmark = other->interMark; closest = other; } } me->interNext = closest; } // Create selectlinks. for(int i = d->modefs.count() - 1; i >= 0; --i) { ModelDef *me = &d->modefs[i]; int minsel = DDMAXINT; ModelDef *closest = 0; // Start scanning from the next definition. for(int k = d->modefs.count() - 1; k >= 0; --k) { ModelDef *other = &d->modefs[k]; // Same state and a bigger order are the requirements. if(other->state == me->state && other->def.order() > me->def.order() && // Defined after me. other->select > me->select && other->select < minsel && other->interMark >= me->interMark) { minsel = other->select; closest = other; } } me->selectNext = closest; } LOG_RES_MSG("Model init completed in %.2f seconds") << begunAt.since(); } int ResourceSystem::indexOf(ModelDef const *modelDef) { int index = int(modelDef - &d->modefs[0]); if(index >= 0 && index < d->modefs.count()) { return index; } return -1; } void ResourceSystem::setModelDefFrame(ModelDef &modef, int frame) { for(uint i = 0; i < modef.subCount(); ++i) { SubmodelDef &subdef = modef.subModelDef(i); if(subdef.modelId == NOMODELID) continue; // Modify the modeldef itself: set the current frame. subdef.frame = frame % model(subdef.modelId).frameCount(); } } #endif // __CLIENT__ void ResourceSystem::initMapDefs() { clearAllMapDefs(); // Locate all the maps using the central lump index: /// @todo Locate new maps each time a package is loaded rather than rely on /// the central lump index. LumpIndex const &lumpIndex = App_FileSystem().nameIndex(); lumpnum_t lastLump = -1; while(lastLump < lumpIndex.size()) { QScopedPointer recognizer(new Id1MapRecognizer(lumpIndex, lastLump)); lastLump = recognizer->lastLump(); if(recognizer->format() != Id1MapRecognizer::UnknownFormat) { File1 *sourceFile = recognizer->sourceFile(); String const mapId = recognizer->id(); MapDef &mapDef = d->mapDefs.insert(mapId); mapDef.set("id", mapId); mapDef.setSourceFile(sourceFile) .setRecognizer(recognizer.take()); } } } void ResourceSystem::clearAllMapDefs() { d->mapDefs.clear(); } MapDef *ResourceSystem::mapDef(de::Uri const &mapUri) const { // Only one resource scheme is known for maps. if(mapUri.scheme().compareWithoutCase("Maps")) return 0; return d->mapDefs.tryFind(mapUri.path(), MapDefs::MatchFull | MapDefs::NoBranch); } ResourceSystem::MapDefs const &ResourceSystem::allMapDefs() const { return d->mapDefs; } ResourceSystem::MapDefs &ResourceSystem::allMapDefs() { return d->mapDefs; } void ResourceSystem::clearAllAnimGroups() { qDeleteAll(d->animGroups); d->animGroups.clear(); } int ResourceSystem::animGroupCount() { return d->animGroups.count(); } AnimGroup &ResourceSystem::newAnimGroup(int flags) { LOG_AS("ResourceSystem"); int const uniqueId = d->animGroups.count() + 1; // 1-based. // Allocating one by one is inefficient but it doesn't really matter. d->animGroups.append(new AnimGroup(uniqueId, flags)); return *d->animGroups.last(); } AnimGroup *ResourceSystem::animGroup(int uniqueId) { LOG_AS("ResourceSystem::animGroup"); if(uniqueId > 0 && uniqueId <= d->animGroups.count()) { return d->animGroups.at(uniqueId - 1); } LOGDEV_RES_WARNING("Invalid group #%i, returning NULL") << uniqueId; return nullptr; } AnimGroup *ResourceSystem::animGroupForTexture(TextureManifest const &textureManifest) { // Group ids are 1-based. // Search backwards to allow patching. for(int i = animGroupCount(); i > 0; i--) { AnimGroup *group = animGroup(i); if(group->hasFrameFor(textureManifest)) { return group; } } return nullptr; // Not found. } struct SpriteFrameDef { byte frame[2]; byte rotation[2]; Material *mat; }; struct SpriteDef { String name; typedef QList Frames; Frames frames; SpriteDef(String const &name = "") : name(name) {} SpriteFrameDef &addFrame(SpriteFrameDef const &frame = SpriteFrameDef()) { frames.append(frame); return frames.last(); } }; // Tempory storage, used when reading sprite definitions. typedef QHash SpriteDefs; /** * In DOOM, a sprite frame is a patch texture contained in a lump existing * between the S_START and S_END marker lumps (in WAD) whose lump name matches * the following pattern: * * NAME|A|R(A|R) (for example: "TROOA0" or "TROOA2A8") * * NAME: Four character name of the sprite. * A: Animation frame ordinal 'A'... (ASCII). * R: Rotation angle 0...G * 0 : Use this frame for ALL angles. * 1...8: Angle of rotation in 45 degree increments. * A...G: Angle of rotation in 22.5 degree increments. * * The second set of (optional) frame and rotation characters instruct that the * same sprite frame is to be used for an additional frame but that the sprite * patch should be flipped horizontally (right to left) during the loading phase. * * Sprite rotation 0 is facing the viewer, rotation 1 is one half-angle turn * CLOCKWISE around the axis. This is not the same as the angle, which increases * counter clockwise (protractor). */ static SpriteDefs buildSpriteDefsForTextures(TextureScheme::Index const &texIndex) { SpriteDefs sprDefs; sprDefs.reserve(texIndex.leafNodes().count() / 8); // overestimate PathTreeIterator iter(texIndex.leafNodes()); while(iter.hasNext()) { TextureManifest &manifest = iter.next(); String const desc = QString(QByteArray::fromPercentEncoding(manifest.path().toUtf8())); String const name = desc.left(4).toLower(); // Have we already encountered this name? SpriteDef *def = &sprDefs[name]; if(def->frames.isEmpty()) { // An entirely new sprite. def->name = name; } // Add the frame(s). int const frameNumber = desc.at(4).toUpper().unicode() - QChar('A').unicode() + 1; int const rotationNumber = toSpriteRotation(desc.at(5)); SpriteFrameDef *frameDef = 0; for(int i = 0; i < def->frames.count(); ++i) { SpriteFrameDef &cand = def->frames[i]; if(cand.frame[0] == frameNumber && cand.rotation[0] == rotationNumber) { frameDef = &cand; break; } } if(!frameDef) { // A new frame. frameDef = &def->addFrame(); } frameDef->mat = &App_ResourceSystem().material(de::Uri("Sprites", manifest.path())); frameDef->frame[0] = frameNumber; frameDef->rotation[0] = rotationNumber; // Mirrored? if(desc.length() >= 8) { frameDef->frame[1] = desc.at(6).toUpper().unicode() - QChar('A').unicode() + 1; frameDef->rotation[1] = toSpriteRotation(desc.at(7)); } else { frameDef->frame[1] = 0; frameDef->rotation[1] = 0; } } return sprDefs; } /** * Generates a Sprite frame set from the given sprite @a definition. * * @note Gaps in the frame number range used in @a definition will be filled * with dummy Sprite instances (no view angles added). * * @param definition SpriteDef description of one or more Sprites. * * @return Built Sprites in frame order. Ownership of the Sprite instances * is given to the caller. */ static QList buildSpritesFromDefinition(SpriteDef const &definition) { // Build sprite frames and add view angles. QMap spritesByFrame; for(SpriteFrameDef const &frameDef : definition.frames) for(int i = 0; i < 2; ++i) { int const frame = frameDef.frame[i] - 1; if(frame < 0) continue; auto const found = spritesByFrame.find(frame); Sprite *sprite; if(found != spritesByFrame.end()) { sprite = found.value(); } else { sprite = spritesByFrame.insert(frame, new Sprite).value(); } sprite->newViewAngle(frameDef.mat, frameDef.rotation[i], i == 1); } // Duplicate view angles to complete rotation sets (if defined). for(Sprite *sprite : spritesByFrame) { if(sprite->viewAngleCount() < 2) continue; for(int rot = 0; rot < Sprite::max_angles / 2; ++rot) { if(!sprite->hasViewAngle(rot * 2 + 1)) { auto const &src = sprite->viewAngle(rot * 2); sprite->newViewAngle(src.material, rot * 2 + 2, src.mirrorX); } if(!sprite->hasViewAngle(rot * 2)) { auto const &src = sprite->viewAngle(rot * 2 + 1); sprite->newViewAngle(src.material, rot * 2 + 1, src.mirrorX); } } } // Output a frame ordered list. QList sprites; int lastFrame = -1; auto it = spritesByFrame.constBegin(); while(it != spritesByFrame.constEnd()) { int frame = it.key(); // Insert dummy sprites to fill any gaps in the frame set. for(int i = lastFrame + 1; i < frame; ++i) { sprites << new Sprite; } sprites << it.value(); lastFrame = frame; ++it; } return sprites; } void ResourceSystem::initSprites() { Time begunAt; LOG_AS("ResourceSystem"); LOG_RES_VERBOSE("Building sprites..."); d->clearSprites(); /// @todo It should no longer be necessary to split this into two phases -ds SpriteDefs spriteDefs = buildSpriteDefsForTextures(App_ResourceSystem().textureScheme("Sprites").index()); if(!spriteDefs.isEmpty()) { // Build Sprite frame sets from their definitions. int customIdx = 0; for(SpriteDef const &def : spriteDefs) { // Format or generate an id for the sprite. spritenum_t spriteId = Def_GetSpriteNum(def.name.toUtf8().constData()); if(spriteId == -1) spriteId = (runtimeDefs.sprNames.size() + customIdx++); // Append another frame set to the relevant sprite. d->newSpriteFrameSet(spriteId).append(buildSpritesFromDefinition(def)); } } // We're done with the definitions. spriteDefs.clear(); LOG_RES_VERBOSE("Sprites built in %.2f seconds") << begunAt.since(); } void ResourceSystem::clearAllColorPalettes() { d->colorPaletteNames.clear(); qDeleteAll(d->colorPalettes); d->colorPalettes.clear(); d->defaultColorPalette = 0; } int ResourceSystem::colorPaletteCount() const { return d->colorPalettes.count(); } ColorPalette &ResourceSystem::colorPalette(colorpaletteid_t id) const { // Choose the default palette? if(!id) { id = d->defaultColorPalette; } Instance::ColorPalettes::const_iterator found = d->colorPalettes.find(id); if(found != d->colorPalettes.end()) { return *found.value(); } /// @throw MissingResourceError An unknown/invalid id was specified. throw MissingResourceError("ResourceSystem::colorPalette", "Invalid id " + String::number(id)); } String ResourceSystem::colorPaletteName(ColorPalette &palette) const { QList names = d->colorPaletteNames.keys(&palette); if(!names.isEmpty()) { return names.first(); } return String(); } bool ResourceSystem::hasColorPalette(String name) const { return d->colorPaletteNames.contains(name); } ColorPalette &ResourceSystem::colorPalette(String name) const { Instance::ColorPaletteNames::const_iterator found = d->colorPaletteNames.find(name); if(found != d->colorPaletteNames.end()) { return *found.value(); } /// @throw MissingResourceError An unknown name was specified. throw MissingResourceError("ResourceSystem::colorPalette", "Unknown name '" + name + "'"); } void ResourceSystem::addColorPalette(ColorPalette &newPalette, String const &name) { // Do we already own this palette? Instance::ColorPalettes::const_iterator found = d->colorPalettes.find(newPalette.id()); if(found != d->colorPalettes.end()) { return; } d->colorPalettes.insert(newPalette.id(), &newPalette); #ifdef __CLIENT__ // Observe changes to the color table so we can schedule texture updates. newPalette.audienceForColorTableChange += d; #endif if(!name.isEmpty()) { d->colorPaletteNames.insert(name, &newPalette); } // If this is the first palette automatically set it as the default. if(d->colorPalettes.count() == 1) { d->defaultColorPalette = newPalette.id(); } } colorpaletteid_t ResourceSystem::defaultColorPalette() const { return d->defaultColorPalette; } void ResourceSystem::setDefaultColorPalette(ColorPalette *newDefaultPalette) { d->defaultColorPalette = newDefaultPalette? newDefaultPalette->id().asUInt32() : 0; } #ifdef __CLIENT__ void ResourceSystem::purgeCacheQueue() { qDeleteAll(d->cacheQueue); d->cacheQueue.clear(); } void ResourceSystem::processCacheQueue() { d->processCacheQueue(); } void ResourceSystem::cache(Material &material, MaterialVariantSpec const &spec, bool cacheGroups) { d->queueCacheTasksForMaterial(material, spec, cacheGroups); } void ResourceSystem::cache(spritenum_t spriteId, MaterialVariantSpec const &spec) { d->queueCacheTasksForSprite(spriteId, spec); } void ResourceSystem::cache(ModelDef *modelDef) { if(!modelDef) return; d->queueCacheTasksForModel(*modelDef); } MaterialVariantSpec const &ResourceSystem::materialSpec(MaterialContextId contextId, int flags, byte border, int tClass, int tMap, int wrapS, int wrapT, int minFilter, int magFilter, int anisoFilter, bool mipmapped, bool gammaCorrection, bool noStretch, bool toAlpha) { return d->getMaterialSpecForContext(contextId, flags, border, tClass, tMap, wrapS, wrapT, minFilter, magFilter, anisoFilter, mipmapped, gammaCorrection, noStretch, toAlpha); } void ResourceSystem::cacheForCurrentMap() { // Don't precache when playing a demo (why not? -ds). if(playback) return; Map &map = App_WorldSystem().map(); if(precacheMapMaterials) { MaterialVariantSpec const &spec = Rend_MapSurfaceMaterialSpec(); map.forAllLines([this, &spec] (Line &line) { for(int i = 0; i < 2; ++i) { LineSide &side = line.side(i); if(!side.hasSections()) continue; if(side.middle().hasMaterial()) cache(side.middle().material(), spec); if(side.top().hasMaterial()) cache(side.top().material(), spec); if(side.bottom().hasMaterial()) cache(side.bottom().material(), spec); } return LoopContinue; }); map.forAllSectors([this, &spec] (Sector §or) { // Skip sectors with no line sides as their planes will never be drawn. if(sector.sideCount()) { sector.forAllPlanes([this, &spec] (Plane &plane) { if(plane.surface().hasMaterial()) { cache(plane.surface().material(), spec); } return LoopContinue; }); } return LoopContinue; }); } if(precacheSprites) { MaterialVariantSpec const &matSpec = Rend_SpriteMaterialSpec(); for(dint i = 0; i < spriteCount(); ++i) { auto const sprite = spritenum_t(i); // Is this sprite used by a state of at least one mobj? LoopResult found = map.thinkers().forAll(reinterpret_cast(gx.MobjThinker), 0x1/*public*/, [&sprite] (thinker_t *th) { auto const &mob = *reinterpret_cast(th); if(mob.type >= 0 && mob.type < defs.mobjs.size()) { /// @todo optimize: traverses the entire state list! for(dint k = 0; k < defs.states.size(); ++k) { if(runtimeDefs.stateInfo[k].owner != &runtimeDefs.mobjInfo[mob.type]) continue; if(Def_GetState(k)->sprite == sprite) { return LoopAbort; // Found one. } } } return LoopContinue; }); if(found) { cache(sprite, matSpec); } } } // Precache model skins? /// @note The skins are also bound here once so they should be ready /// for use the next time they are needed. if(useModels && precacheSkins) { map.thinkers().forAll(reinterpret_cast(gx.MobjThinker), 0x1/*public*/, [this] (thinker_t *th) { auto const &mob = *reinterpret_cast(th); // Check through all the model definitions. for(dint i = 0; i < modelDefCount(); ++i) { ModelDef &modef = modelDef(i); if(!modef.state) continue; if(mob.type < 0 || mob.type >= defs.mobjs.size()) continue; // Hmm? if(runtimeDefs.stateInfo[runtimeDefs.states.indexOf(modef.state)].owner != &runtimeDefs.mobjInfo[mob.type]) continue; cache(&modef); } return LoopContinue; }); } } #endif // __CLIENT__ NativePath ResourceSystem::nativeSavePath() { return d->nativeSavePath; } bool ResourceSystem::convertLegacySavegames(String const &gameId, String const &sourcePath) { // A converter plugin is required. if(!Plug_CheckForHook(HOOK_SAVEGAME_CONVERT)) return false; // Populate /legacysavegames/ with new savegames which may have appeared. d->locateLegacySavegames(gameId); bool didSchedule = false; if(sourcePath.isEmpty()) { // Process all legacy savegames. if(Folder const *saveFolder = App::rootFolder().tryLocate(String("legacysavegames") / gameId)) { /// @todo File name pattern matching should not be done here. This is to prevent /// attempting to convert Hexen's map state side car files separately when this /// is called from Doomsday Script (in bootstrap.de). Game const &game = App_Games().byIdentityKey(gameId); QRegExp namePattern(game.legacySavegameNameExp(), Qt::CaseInsensitive); if(namePattern.isValid() && !namePattern.isEmpty()) { DENG2_FOR_EACH_CONST(Folder::Contents, i, saveFolder->contents()) { if(namePattern.exactMatch(i->first.fileName())) { // Schedule the conversion task. d->beginConvertLegacySavegame(i->second->path(), gameId); didSchedule = true; } } } } } // Just the one legacy savegame. else if(App::rootFolder().has(sourcePath)) { // Schedule the conversion task. d->beginConvertLegacySavegame(sourcePath, gameId); didSchedule = true; } return didSchedule; } byte precacheMapMaterials = true; byte precacheSprites = true; byte texGammaLut[256]; void R_BuildTexGammaLut() { #ifdef __SERVER__ double invGamma = 1.0f; #else double invGamma = 1.0f - de::clamp(0.f, texGamma, 1.f); // Clamp to a sane range. #endif for(int i = 0; i < 256; ++i) { texGammaLut[i] = byte(255.0f * pow(double(i / 255.0f), invGamma)); } } template static bool pathBeginsWithComparator(ManifestType const &manifest, void *context) { Path const *path = reinterpret_cast(context); /// @todo Use PathTree::Node::compare() return manifest.path().toStringRef().beginsWith(*path, Qt::CaseInsensitive); } /** * Decode and then lexicographically compare the two manifest paths, * returning @c true if @a is less than @a b. */ template static bool comparePathTreeNodePathsAscending(PathTreeNodeType const *a, PathTreeNodeType const *b) { String pathA(QString(QByteArray::fromPercentEncoding(a->path().toUtf8()))); String pathB(QString(QByteArray::fromPercentEncoding(b->path().toUtf8()))); return pathA.compareWithoutCase(pathB) < 0; } /** * @param like Map path search term. * @param composeUriFlags Flags governing how URIs should be composed. */ static int printMapsIndex2(Path const &like, de::Uri::ComposeAsTextFlags composeUriFlags) { ResourceSystem::MapDefs::FoundNodes found; App_ResourceSystem().allMapDefs().findAll(found, pathBeginsWithComparator, const_cast(&like)); if(found.isEmpty()) return 0; //bool const printSchemeName = !(composeUriFlags & de::Uri::OmitScheme); // Print a heading. String heading = "Known maps"; //if(!printSchemeName && scheme) // heading += " in scheme '" + scheme->name() + "'"; if(!like.isEmpty()) heading += " like \"" _E(b) + like.toStringRef() + _E(.) "\""; LOG_RES_MSG(_E(D) "%s:" _E(.)) << heading; // Print the result index. qSort(found.begin(), found.end(), comparePathTreeNodePathsAscending); int const numFoundDigits = de::max(3/*idx*/, M_NumDigits(found.count())); int idx = 0; foreach(MapDef *mapDef, found) { String info = String("%1: " _E(1) "%2" _E(.)) .arg(idx, numFoundDigits) .arg(mapDef->description(composeUriFlags)); LOG_RES_MSG(" " _E(>)) << info; idx++; } return found.count(); } /** * @param scheme Material subspace scheme being printed. Can be @c NULL in * which case textures are printed from all schemes. * @param like Material path search term. * @param composeUriFlags Flags governing how URIs should be composed. */ static int printMaterialIndex2(MaterialScheme *scheme, Path const &like, de::Uri::ComposeAsTextFlags composeUriFlags) { MaterialScheme::Index::FoundNodes found; if(scheme) // Consider resources in the specified scheme only. { scheme->index().findAll(found, pathBeginsWithComparator, const_cast(&like)); } else // Consider resources in any scheme. { App_ResourceSystem().forAllMaterialSchemes([&found, &like] (MaterialScheme &scheme) { scheme.index().findAll(found, pathBeginsWithComparator, const_cast(&like)); return LoopContinue; }); } if(found.isEmpty()) return 0; bool const printSchemeName = !(composeUriFlags & de::Uri::OmitScheme); // Print a heading. String heading = "Known materials"; if(!printSchemeName && scheme) heading += " in scheme '" + scheme->name() + "'"; if(!like.isEmpty()) heading += " like \"" _E(b) + like.toStringRef() + _E(.) "\""; LOG_RES_MSG(_E(D) "%s:" _E(.)) << heading; // Print the result index. qSort(found.begin(), found.end(), comparePathTreeNodePathsAscending); int const numFoundDigits = de::max(3/*idx*/, M_NumDigits(found.count())); int idx = 0; foreach(MaterialManifest *manifest, found) { String info = String("%1: %2%3" _E(.)) .arg(idx, numFoundDigits) .arg(manifest->hasMaterial()? _E(1) : _E(2)) .arg(manifest->description(composeUriFlags)); LOG_RES_MSG(" " _E(>)) << info; idx++; } return found.count(); } /** * @param scheme Texture subspace scheme being printed. Can be @c NULL in * which case textures are printed from all schemes. * @param like Texture path search term. * @param composeUriFlags Flags governing how URIs should be composed. */ static int printTextureIndex2(TextureScheme *scheme, Path const &like, de::Uri::ComposeAsTextFlags composeUriFlags) { TextureScheme::Index::FoundNodes found; if(scheme) // Consider resources in the specified scheme only. { scheme->index().findAll(found, pathBeginsWithComparator, const_cast(&like)); } else // Consider resources in any scheme. { foreach(TextureScheme *scheme, App_ResourceSystem().allTextureSchemes()) { scheme->index().findAll(found, pathBeginsWithComparator, const_cast(&like)); } } if(found.isEmpty()) return 0; bool const printSchemeName = !(composeUriFlags & de::Uri::OmitScheme); // Print a heading. String heading = "Known textures"; if(!printSchemeName && scheme) heading += " in scheme '" + scheme->name() + "'"; if(!like.isEmpty()) heading += " like \"" _E(b) + like.toStringRef() + _E(.) "\""; LOG_RES_MSG(_E(D) "%s:" _E(.)) << heading; // Print the result index key. qSort(found.begin(), found.end(), comparePathTreeNodePathsAscending); int numFoundDigits = de::max(3/*idx*/, M_NumDigits(found.count())); int idx = 0; foreach(TextureManifest *manifest, found) { String info = String("%1: %2%3") .arg(idx, numFoundDigits) .arg(manifest->hasTexture()? _E(0) : _E(2)) .arg(manifest->description(composeUriFlags)); LOG_RES_MSG(" " _E(>)) << info; idx++; } return found.count(); } #ifdef __CLIENT__ /** * @param scheme Resource subspace scheme being printed. Can be @c NULL in * which case resources are printed from all schemes. * @param like Resource path search term. * @param composeUriFlags Flags governing how URIs should be composed. */ static int printFontIndex2(FontScheme *scheme, Path const &like, de::Uri::ComposeAsTextFlags composeUriFlags) { FontScheme::Index::FoundNodes found; if(scheme) // Only resources in this scheme. { scheme->index().findAll(found, pathBeginsWithComparator, const_cast(&like)); } else // Consider resources in any scheme. { foreach(FontScheme *scheme, App_ResourceSystem().allFontSchemes()) { scheme->index().findAll(found, pathBeginsWithComparator, const_cast(&like)); } } if(found.isEmpty()) return 0; bool const printSchemeName = !(composeUriFlags & de::Uri::OmitScheme); // Print a heading. String heading = "Known fonts"; if(!printSchemeName && scheme) heading += " in scheme '" + scheme->name() + "'"; if(!like.isEmpty()) heading += " like \"" _E(b) + like.toStringRef() + _E(.) "\""; LOG_RES_MSG(_E(D) "%s:" _E(.)) << heading; // Print the result index. qSort(found.begin(), found.end(), comparePathTreeNodePathsAscending); int numFoundDigits = de::max(3/*idx*/, M_NumDigits(found.count())); int idx = 0; foreach(FontManifest *manifest, found) { String info = String("%1: %2%3" _E(.)) .arg(idx, numFoundDigits) .arg(manifest->hasResource()? _E(1) : _E(2)) .arg(manifest->description(composeUriFlags)); LOG_RES_MSG(" " _E(>)) << info; idx++; } return found.count(); } #endif // __CLIENT__ static void printMaterialIndex(de::Uri const &search, de::Uri::ComposeAsTextFlags flags = de::Uri::DefaultComposeAsTextFlags) { int printTotal = 0; // Collate and print results from all schemes? if(search.scheme().isEmpty() && !search.path().isEmpty()) { printTotal = printMaterialIndex2(0/*any scheme*/, search.path(), flags & ~de::Uri::OmitScheme); LOG_RES_MSG(_E(R)); } // Print results within only the one scheme? else if(App_ResourceSystem().knownMaterialScheme(search.scheme())) { printTotal = printMaterialIndex2(&App_ResourceSystem().materialScheme(search.scheme()), search.path(), flags | de::Uri::OmitScheme); LOG_RES_MSG(_E(R)); } else { // Collect and sort results in each scheme separately. App_ResourceSystem().forAllMaterialSchemes([&search, &flags, &printTotal] (MaterialScheme &scheme) { int numPrinted = printMaterialIndex2(&scheme, search.path(), flags | de::Uri::OmitScheme); if(numPrinted) { LOG_MSG(_E(R)); printTotal += numPrinted; } return LoopContinue; }); } LOG_RES_MSG("Found " _E(b) "%i" _E(.) " %s.") << printTotal << (printTotal == 1? "material" : "materials in total"); } static void printMapsIndex(de::Uri const &search, de::Uri::ComposeAsTextFlags flags = de::Uri::DefaultComposeAsTextFlags) { int printTotal = printMapsIndex2(search.path(), flags | de::Uri::OmitScheme); LOG_RES_MSG(_E(R)); LOG_RES_MSG("Found " _E(b) "%i" _E(.) " %s.") << printTotal << (printTotal == 1? "map" : "maps in total"); } static void printTextureIndex(de::Uri const &search, de::Uri::ComposeAsTextFlags flags = de::Uri::DefaultComposeAsTextFlags) { int printTotal = 0; // Collate and print results from all schemes? if(search.scheme().isEmpty() && !search.path().isEmpty()) { printTotal = printTextureIndex2(0/*any scheme*/, search.path(), flags & ~de::Uri::OmitScheme); LOG_RES_MSG(_E(R)); } // Print results within only the one scheme? else if(App_ResourceSystem().knownTextureScheme(search.scheme())) { printTotal = printTextureIndex2(&App_ResourceSystem().textureScheme(search.scheme()), search.path(), flags | de::Uri::OmitScheme); LOG_RES_MSG(_E(R)); } else { // Collect and sort results in each scheme separately. foreach(TextureScheme *scheme, App_ResourceSystem().allTextureSchemes()) { int numPrinted = printTextureIndex2(scheme, search.path(), flags | de::Uri::OmitScheme); if(numPrinted) { LOG_RES_MSG(_E(R)); printTotal += numPrinted; } } } LOG_RES_MSG("Found " _E(b) "%i" _E(.) " %s") << printTotal << (printTotal == 1? "texture" : "textures in total"); } #ifdef __CLIENT__ static void printFontIndex(de::Uri const &search, de::Uri::ComposeAsTextFlags flags = de::Uri::DefaultComposeAsTextFlags) { int printTotal = 0; // Collate and print results from all schemes? if(search.scheme().isEmpty() && !search.path().isEmpty()) { printTotal = printFontIndex2(0/*any scheme*/, search.path(), flags & ~de::Uri::OmitScheme); LOG_RES_MSG(_E(R)); } // Print results within only the one scheme? else if(App_ResourceSystem().knownFontScheme(search.scheme())) { printTotal = printFontIndex2(&App_ResourceSystem().fontScheme(search.scheme()), search.path(), flags | de::Uri::OmitScheme); LOG_RES_MSG(_E(R)); } else { // Collect and sort results in each scheme separately. foreach(FontScheme *scheme, App_ResourceSystem().allFontSchemes()) { int numPrinted = printFontIndex2(scheme, search.path(), flags | de::Uri::OmitScheme); if(numPrinted) { LOG_MSG(_E(R)); printTotal += numPrinted; } } } LOG_RES_MSG("Found " _E(b) "%i" _E(.) " %s.") << printTotal << (printTotal == 1? "font" : "fonts in total"); } #endif // __CLIENT__ static bool isKnownMaterialSchemeCallback(String name) { return App_ResourceSystem().knownMaterialScheme(name); } static bool isKnownTextureSchemeCallback(String name) { return App_ResourceSystem().knownTextureScheme(name); } #ifdef __CLIENT__ static bool isKnownFontSchemeCallback(String name) { return App_ResourceSystem().knownFontScheme(name); } #endif /** * Print a list of all currently available maps and the location of the source * file which contains them. * * @todo Improve output: find common map id prefixes, find "suffixed" numerical * ranges, etc... (Do not assume *anything* about the formatting of map ids - * ZDoom allows the mod author to give a map any id they wish, which, is then * specified in MAPINFO when defining the map progression, clusters, etc...). */ D_CMD(ListMaps) { DENG2_UNUSED(src); de::Uri search = de::Uri::fromUserInput(&argv[1], argc - 1); if(search.scheme().isEmpty()) search.setScheme("Maps"); if(!search.scheme().isEmpty() && search.scheme().compareWithoutCase("Maps")) { LOG_RES_WARNING("Unknown scheme %s") << search.scheme(); return false; } printMapsIndex(search); return true; } D_CMD(ListMaterials) { DENG2_UNUSED(src); de::Uri search = de::Uri::fromUserInput(&argv[1], argc - 1, &isKnownMaterialSchemeCallback); if(!search.scheme().isEmpty() && !App_ResourceSystem().knownMaterialScheme(search.scheme())) { LOG_RES_WARNING("Unknown scheme %s") << search.scheme(); return false; } printMaterialIndex(search); return true; } D_CMD(ListTextures) { DENG2_UNUSED(src); de::Uri search = de::Uri::fromUserInput(&argv[1], argc - 1, &isKnownTextureSchemeCallback); if(!search.scheme().isEmpty() && !App_ResourceSystem().knownTextureScheme(search.scheme())) { LOG_RES_WARNING("Unknown scheme %s") << search.scheme(); return false; } printTextureIndex(search); return true; } #ifdef __CLIENT__ D_CMD(ListFonts) { DENG2_UNUSED(src); de::Uri search = de::Uri::fromUserInput(&argv[1], argc - 1, &isKnownFontSchemeCallback); if(!search.scheme().isEmpty() && !App_ResourceSystem().knownFontScheme(search.scheme())) { LOG_RES_WARNING("Unknown scheme %s") << search.scheme(); return false; } printFontIndex(search); return true; } #endif // __CLIENT__ #ifdef DENG_DEBUG D_CMD(PrintMaterialStats) { DENG2_UNUSED3(src, argc, argv); LOG_MSG(_E(b) "Material Statistics:"); App_ResourceSystem().forAllMaterialSchemes([] (MaterialScheme &scheme) { MaterialScheme::Index const &index = scheme.index(); uint count = index.count(); LOG_MSG("Scheme: %s (%u %s)") << scheme.name() << count << (count == 1? "material" : "materials"); index.debugPrintHashDistribution(); index.debugPrint(); return LoopContinue; }); return true; } D_CMD(PrintTextureStats) { DENG2_UNUSED3(src, argc, argv); LOG_MSG(_E(b) "Texture Statistics:"); foreach(TextureScheme *scheme, App_ResourceSystem().allTextureSchemes()) { TextureScheme::Index const &index = scheme->index(); uint const count = index.count(); LOG_MSG("Scheme: %s (%u %s)") << scheme->name() << count << (count == 1? "texture" : "textures"); index.debugPrintHashDistribution(); index.debugPrint(); } return true; } # ifdef __CLIENT__ D_CMD(PrintFontStats) { DENG2_UNUSED3(src, argc, argv); LOG_MSG(_E(b) "Font Statistics:"); foreach(FontScheme *scheme, App_ResourceSystem().allFontSchemes()) { FontScheme::Index const &index = scheme->index(); uint const count = index.count(); LOG_MSG("Scheme: %s (%u %s)") << scheme->name() << count << (count == 1? "font" : "fonts"); index.debugPrintHashDistribution(); index.debugPrint(); } return true; } # endif // __CLIENT__ #endif // DENG_DEBUG D_CMD(InspectSavegame) { DENG2_UNUSED2(src, argc); String savePath = argv[1]; // Append a .save extension if none exists. if(savePath.fileNameExtension().isEmpty()) { savePath += ".save"; } // If a game is loaded assume the user is referring to those savegames if not specified. if(savePath.fileNamePath().isEmpty() && App_GameLoaded()) { savePath = game::Session::savePath() / savePath; } if(game::SavedSession const *saved = App::rootFolder().tryLocate(savePath)) { LOG_SCR_MSG("%s") << saved->metadata().asStyledText(); LOG_SCR_MSG(_E(D) "Resource: " _E(.)_E(i) "\"%s\"") << saved->path(); return true; } LOG_WARNING("Failed to locate savegame with \"%s\"") << savePath; return false; } void ResourceSystem::consoleRegister() // static { C_CMD("listtextures", "ss", ListTextures) C_CMD("listtextures", "s", ListTextures) C_CMD("listtextures", "", ListTextures) #ifdef DENG_DEBUG C_CMD("texturestats", NULL, PrintTextureStats) #endif C_CMD("inspectsavegame", "s", InspectSavegame) #ifdef __CLIENT__ C_CMD("listfonts", "ss", ListFonts) C_CMD("listfonts", "s", ListFonts) C_CMD("listfonts", "", ListFonts) # ifdef DENG_DEBUG C_CMD("fontstats", NULL, PrintFontStats) # endif #endif C_CMD("listmaterials", "ss", ListMaterials) C_CMD("listmaterials", "s", ListMaterials) C_CMD("listmaterials", "", ListMaterials) #ifdef DENG_DEBUG C_CMD("materialstats", NULL, PrintMaterialStats) #endif C_CMD("listmaps", "s", ListMaps) C_CMD("listmaps", "", ListMaps) Texture::consoleRegister(); Material::consoleRegister(); } String ResourceSystem::resolveSymbol(String const &symbol) // static { if(!symbol.compare("App.DataPath", Qt::CaseInsensitive)) { return "data"; } else if(!symbol.compare("App.DefsPath", Qt::CaseInsensitive)) { return "defs"; } else if(!symbol.compare("Game.IdentityKey", Qt::CaseInsensitive)) { if(!App_GameLoaded()) { /// @throw de::Uri::ResolveSymbolError An unresolveable symbol was encountered. throw de::Uri::ResolveSymbolError("ResourceSystem::resolveSymbol", "Symbol 'Game' did not resolve (no game loaded)"); } return App_CurrentGame().identityKey(); } else if(!symbol.compare("GamePlugin.Name", Qt::CaseInsensitive)) { if(!App_GameLoaded() || !gx.GetVariable) { /// @throw de::Uri::ResolveSymbolError An unresolveable symbol was encountered. throw de::Uri::ResolveSymbolError("ResourceSystem::resolveSymbol", "Symbol 'GamePlugin' did not resolve (no game plugin loaded)"); } return String((char *)gx.GetVariable(DD_PLUGIN_NAME)); } else { /// @throw UnknownSymbolError An unknown symbol was encountered. throw de::Uri::UnknownSymbolError("ResourceSystem::resolveSymbol", "Symbol '" + symbol + "' is unknown"); } } doomsday-stable-1.15.7/doomsday/client/src/resource/pcx.cpp0000664000175000017500000000772712641367670023222 0ustar jaakkojaakko/** @file pcx.cpp PCX image reader. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * @authors Copyright © 1997-2006 by id Software, Inc. * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "dd_share.h" // endianness conversion macros #include "resource/pcx.h" #include using namespace de; #pragma pack(1) typedef struct { int8_t manufacturer; int8_t version; int8_t encoding; int8_t bits_per_pixel; uint16_t xmin, ymin, xmax, ymax; uint16_t hres, vres; uint8_t palette[48]; int8_t reserved; int8_t color_planes; uint16_t bytes_per_line; uint16_t palette_type; int8_t filler[58]; } header_t; #pragma pack() static char *lastErrorMsg = 0; /// @todo potentially never free'd static void setLastError(char const *msg) { size_t len; if(0 == msg || 0 == (len = strlen(msg))) { if(lastErrorMsg != 0) { M_Free(lastErrorMsg); } lastErrorMsg = 0; return; } lastErrorMsg = (char *) M_Realloc(lastErrorMsg, len+1); strcpy(lastErrorMsg, msg); } static bool load(FileHandle &file, int width, int height, uint8_t *dstBuf) { DENG2_ASSERT(dstBuf != 0); int x, y, dataByte, runLength; bool result = false; size_t const len = file.length(); uint8_t *raw = (uint8_t *) M_Malloc(len); file.read(raw, len); uint8_t const *srcPos = raw; uint8_t const *palette = srcPos + len - 768; // Palette is at the end. srcPos += sizeof(header_t); for(y = 0; y < height; ++y, dstBuf += width * 3) { for(x = 0; x < width;) { dataByte = *srcPos++; if((dataByte & 0xC0) == 0xC0) { runLength = dataByte & 0x3F; dataByte = *srcPos++; } else { runLength = 1; } while(runLength-- > 0) { std::memcpy(dstBuf + x++ * 3, palette + dataByte * 3, 3); } } } if(!((size_t) (srcPos - (uint8_t *) raw) > len)) { setLastError(0); // Success. result = true; } else { setLastError("RLE inflation failed."); } M_Free(raw); return result; } char const *PCX_LastError() { if(lastErrorMsg) { return lastErrorMsg; } return 0; } uint8_t *PCX_Load(FileHandle &file, de::Vector2ui &outSize, int &pixelSize) { uint8_t *dstBuf = 0; size_t const initPos = file.tell(); header_t hdr; if(file.read((uint8_t *)&hdr, sizeof(hdr)) >= sizeof(hdr)) { size_t dstBufSize; if(hdr.manufacturer != 0x0a || hdr.version != 5 || hdr.encoding != 1 || hdr.bits_per_pixel != 8) { setLastError("Unsupported format."); file.seek(initPos, SeekSet); return 0; } outSize = Vector2ui(SHORT(hdr.xmax) + 1, SHORT(hdr.ymax) + 1); pixelSize = 3; dstBufSize = 4 * outSize.x * outSize.y; dstBuf = (uint8_t *) M_Malloc(dstBufSize); file.rewind(); if(!load(file, outSize.x, outSize.y, dstBuf)) { M_Free(dstBuf); dstBuf = 0; } } file.seek(initPos, SeekSet); return dstBuf; } doomsday-stable-1.15.7/doomsday/client/src/resource/fontscheme.cpp0000664000175000017500000001655012641367670024555 0ustar jaakkojaakko/** @file fontscheme.cpp Font resource scheme. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "resource/fontscheme.h" #include "dd_types.h" #include #include using namespace de; DENG2_PIMPL(FontScheme), DENG2_OBSERVES(FontManifest, UniqueIdChange), DENG2_OBSERVES(FontManifest, Deletion) { String name; ///< Symbolic. Index index; ///< Mappings from paths to manifests. QList uniqueIdLut; ///< Index with uniqueId - uniqueIdBase. bool uniqueIdLutDirty; int uniqueIdBase; Instance(Public *i) : Base(i) , uniqueIdLutDirty(false) , uniqueIdBase(0) {} ~Instance() { self.clear(); DENG2_ASSERT(index.isEmpty()); // sanity check. } bool inline uniqueIdInLutRange(int uniqueId) const { return uniqueId - uniqueIdBase >= 0 && (uniqueId - uniqueIdBase) < uniqueIdLut.size(); } void findUniqueIdRange(int *minId, int *maxId) { if(!minId && !maxId) return; if(minId) *minId = DDMAXINT; if(maxId) *maxId = DDMININT; PathTreeIterator iter(index.leafNodes()); while(iter.hasNext()) { Manifest &manifest = iter.next(); int const uniqueId = manifest.uniqueId(); if(minId && uniqueId < *minId) *minId = uniqueId; if(maxId && uniqueId > *maxId) *maxId = uniqueId; } } void deindex(Manifest &manifest) { /// @todo Only destroy the resource if this is the last remaining reference. manifest.clearResource(); unlinkInUniqueIdLut(manifest); } /// @pre uniqueIdMap is large enough if initialized! void unlinkInUniqueIdLut(Manifest const &manifest) { DENG2_ASSERT(&manifest.scheme() == thisPublic); // sanity check. // If the lut is already considered 'dirty' do not unlink. if(!uniqueIdLutDirty) { int uniqueId = manifest.uniqueId(); DENG2_ASSERT(uniqueIdInLutRange(uniqueId)); uniqueIdLut[uniqueId - uniqueIdBase] = 0; } } /// @pre uniqueIdLut has been initialized and is large enough! void linkInUniqueIdLut(Manifest &manifest) { DENG2_ASSERT(&manifest.scheme() == thisPublic); // sanity check. int uniqueId = manifest.uniqueId(); DENG_ASSERT(uniqueIdInLutRange(uniqueId)); uniqueIdLut[uniqueId - uniqueIdBase] = &manifest; } void rebuildUniqueIdLut() { // Is a rebuild necessary? if(!uniqueIdLutDirty) return; // Determine the size of the LUT. int minId, maxId; findUniqueIdRange(&minId, &maxId); int lutSize = 0; if(minId > maxId) // None found? { uniqueIdBase = 0; } else { uniqueIdBase = minId; lutSize = maxId - minId + 1; } // Fill the LUT with initial values. #ifdef DENG2_QT_4_7_OR_NEWER uniqueIdLut.reserve(lutSize); #endif int i = 0; for(; i < uniqueIdLut.size(); ++i) { uniqueIdLut[i] = 0; } for(; i < lutSize; ++i) { uniqueIdLut.push_back(0); } if(lutSize) { // Populate the LUT. PathTreeIterator iter(index.leafNodes()); while(iter.hasNext()) { linkInUniqueIdLut(iter.next()); } } uniqueIdLutDirty = false; } // Observes FontManifest UniqueIdChange void fontManifestUniqueIdChanged(Manifest & /*manifest*/) { // We'll need to rebuild the id map. uniqueIdLutDirty = true; } // Observes FontManifest Deletion. void fontManifestBeingDeleted(Manifest const &manifest) { deindex(const_cast(manifest)); } }; FontScheme::FontScheme(String symbolicName) : d(new Instance(this)) { d->name = symbolicName; } void FontScheme::clear() { /*PathTreeIterator iter(d->index.leafNodes()); while(iter.hasNext()) { d->deindex(iter.next()); }*/ d->index.clear(); d->uniqueIdLutDirty = true; } String const &FontScheme::name() const { return d->name; } FontScheme::Manifest &FontScheme::declare(Path const &path) { LOG_AS("FontScheme::declare"); if(path.isEmpty()) { /// @throw InvalidPathError An empty path was specified. throw InvalidPathError("FontScheme::declare", "Missing/zero-length path was supplied"); } int const sizeBefore = d->index.size(); Manifest *newManifest = &d->index.insert(path); DENG2_ASSERT(newManifest != 0); if(d->index.size() != sizeBefore) { // We want notification if/when the manifest's uniqueId changes. newManifest->audienceForUniqueIdChange += d; // We want notification when the manifest is about to be deleted. newManifest->audienceForDeletion += d; // Notify interested parties that a new manifest was defined in the scheme. DENG2_FOR_AUDIENCE(ManifestDefined, i) i->fontSchemeManifestDefined(*this, *newManifest); } return *newManifest; } bool FontScheme::has(Path const &path) const { return d->index.has(path, Index::NoBranch | Index::MatchFull); } FontScheme::Manifest const &FontScheme::find(Path const &path) const { if(has(path)) { return d->index.find(path, Index::NoBranch | Index::MatchFull); } /// @throw NotFoundError Failed to locate a matching manifest. throw NotFoundError("FontScheme::find", "Failed to locate a manifest matching \"" + path.asText() + "\""); } FontScheme::Manifest &FontScheme::find(Path const &path) { Manifest const &found = const_cast(this)->find(path); return const_cast(found); } FontScheme::Manifest const &FontScheme::findByUniqueId(int uniqueId) const { d->rebuildUniqueIdLut(); if(d->uniqueIdInLutRange(uniqueId)) { Manifest *manifest = d->uniqueIdLut[uniqueId - d->uniqueIdBase]; if(manifest) return *manifest; } /// @throw NotFoundError No manifest was found with a matching resource URI. throw NotFoundError("FontScheme::findByUniqueId", "No manifest found with a unique ID matching \"" + QString("%1").arg(uniqueId) + "\""); } FontScheme::Manifest &FontScheme::findByUniqueId(int uniqueId) { Manifest const &found = const_cast(this)->findByUniqueId(uniqueId); return const_cast(found); } FontScheme::Index const &FontScheme::index() const { return d->index; } doomsday-stable-1.15.7/doomsday/client/src/resource/abstractfont.cpp0000664000175000017500000000306112641367670025105 0ustar jaakkojaakko/** @file abstractfont.cpp Abstract font. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "resource/abstractfont.h" using namespace de; AbstractFont::AbstractFont(FontManifest &manifest) : _manifest(manifest) , _flags(0) {} AbstractFont::~AbstractFont() { DENG2_FOR_AUDIENCE(Deletion, i) i->fontBeingDeleted(*this); } void AbstractFont::glInit() {} void AbstractFont::glDeinit() {} FontManifest &AbstractFont::manifest() const { return _manifest; } AbstractFont::Flags AbstractFont::flags() const { return _flags; } int AbstractFont::ascent() { return 0; } int AbstractFont::descent() { return 0; } int AbstractFont::lineSpacing() { return 0; } doomsday-stable-1.15.7/doomsday/client/src/resource/patchname.cpp0000664000175000017500000000455112641367670024360 0ustar jaakkojaakko/** @file patchname.cpp PatchName * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include #include "resource/patchname.h" namespace de { PatchName::PatchName(String percentEncodedName, lumpnum_t _lumpNum) : name(percentEncodedName), lumpNum_(_lumpNum) {} lumpnum_t PatchName::lumpNum() { // Have we already searched for this lump? if(lumpNum_ == -2) { // Mark as not found. lumpNum_ = -1; // Perform the search. try { lumpNum_ = App_FileSystem().lumpNumForName(name); } catch(FS1::NotFoundError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ", ignoring."); } } return lumpNum_; } void PatchName::operator << (Reader &from) { // The raw ASCII name is not necessarily terminated. char asciiName[9]; for(int i = 0; i < 8; ++i) { from >> asciiName[i]; } asciiName[8] = 0; // WAD format allows characters not normally permitted in native paths. // To achieve uniformity we apply a percent encoding to the "raw" names. name = QString(QByteArray(asciiName).toPercentEncoding()); // The cached found lump number is no longer valid. lumpNum_ = -2; } String PatchName::percentEncodedName() const { return name; } String const &PatchName::percentEncodedNameRef() const { return name; } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/resource/materialshinelayer.cpp0000664000175000017500000000706612641367670026306 0ustar jaakkojaakko/** @file materialshinelayer.cpp Logical material, shine/reflection layer. * * @authors Copyright © 2011-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "resource/materialshinelayer.h" #include "dd_main.h" #include "TextureScheme" using namespace de; static de::Uri findTextureForShineStage(ded_shine_stage_t const &def, bool findMask) { if(de::Uri *resourceUri = (findMask? def.maskTexture : def.texture)) { try { return App_ResourceSystem() .textureScheme(findMask? "Masks" : "Reflections") .findByResourceUri(*resourceUri) .composeUri(); } catch(TextureScheme::NotFoundError const &) {} // Ignore this error. } return de::Uri(); } MaterialShineLayer::AnimationStage::AnimationStage(de::Uri const &texture, int tics, float variance, de::Uri const &maskTexture, blendmode_t blendMode, float opacity, Vector3f const &minColor, Vector2f const &maskDimensions) : MaterialTextureLayer::AnimationStage(texture, tics, variance, 0, 0, Vector2f(0, 0), maskTexture, maskDimensions, blendMode, opacity) { set("minColor", new ArrayValue(minColor)); } MaterialShineLayer::AnimationStage::AnimationStage(AnimationStage const &other) : MaterialTextureLayer::AnimationStage(other) {} MaterialShineLayer::AnimationStage::~AnimationStage() {} void MaterialShineLayer::AnimationStage::resetToDefaults() { MaterialTextureLayer::AnimationStage::resetToDefaults(); addArray("minColor", new ArrayValue(Vector3f(0, 0, 0))); } MaterialShineLayer::AnimationStage * MaterialShineLayer::AnimationStage::fromDef(ded_shine_stage_t const &def) { de::Uri const texture = findTextureForShineStage(def, false/*not mask*/); de::Uri const maskTexture = findTextureForShineStage(def, true/*mask*/); return new AnimationStage(texture, def.tics, def.variance, maskTexture, def.blendMode, def.shininess, Vector3f(def.minColor), Vector2f(def.maskWidth, def.maskHeight)); } // ------------------------------------------------------------------------------------ MaterialShineLayer::MaterialShineLayer() : MaterialTextureLayer() {} MaterialShineLayer::~MaterialShineLayer() {} MaterialShineLayer *MaterialShineLayer::fromDef(ded_reflection_t const &layerDef) { auto *layer = new MaterialShineLayer(); // Only the one stage. layer->_stages.append(AnimationStage::fromDef(layerDef.stage)); return layer; } int MaterialShineLayer::addStage(MaterialShineLayer::AnimationStage const &stageToCopy) { _stages.append(new AnimationStage(stageToCopy)); return _stages.count() - 1; } String MaterialShineLayer::describe() const { return "Shine layer"; } doomsday-stable-1.15.7/doomsday/client/src/resource/materiallightdecoration.cpp0000664000175000017500000001337212641367670027317 0ustar jaakkojaakko/** @file materiallightdecoration.cpp Logical material, light decoration. * * @authors Copyright © 2011-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "resource/materiallightdecoration.h" #include #include "clientapp.h" using namespace de; static inline ResourceSystem &resSys() { return ClientApp::resourceSystem(); } MaterialLightDecoration::AnimationStage::AnimationStage(int tics, float variance, Vector2f const &origin, float elevation, Vector3f const &color, float radius, float haloRadius, LightRange const &lightLevels, Texture *ceilingTexture, Texture *floorTexture, Texture *texture, Texture *flareTexture, int sysFlareIdx) : Stage(tics, variance) , origin (origin) , elevation (elevation) , color (color) , radius (radius) , haloRadius (haloRadius) , lightLevels(lightLevels) , tex (texture) , floorTex (floorTexture) , ceilTex (ceilingTexture) , flareTex (flareTexture) , sysFlareIdx(sysFlareIdx) {} MaterialLightDecoration::AnimationStage::AnimationStage(AnimationStage const &other) : Stage(other) , origin (other.origin) , elevation (other.elevation) , color (other.color) , radius (other.radius) , haloRadius (other.haloRadius) , lightLevels(other.lightLevels) , tex (other.tex) , floorTex (other.floorTex) , ceilTex (other.ceilTex) , flareTex (other.flareTex) , sysFlareIdx(other.sysFlareIdx) {} MaterialLightDecoration::AnimationStage * MaterialLightDecoration::AnimationStage::fromDef(Record const &stageDef) { Texture *lightmapUp = resSys().texture("Lightmaps", de::Uri(stageDef.gets("lightmapUp" ), RC_NULL)); Texture *lightmapDown = resSys().texture("Lightmaps", de::Uri(stageDef.gets("lightmapDown"), RC_NULL)); Texture *lightmapSide = resSys().texture("Lightmaps", de::Uri(stageDef.gets("lightmapSide"), RC_NULL)); int haloTextureIndex = stageDef.geti("haloTextureIndex"); Texture *haloTexture = nullptr; de::Uri const haloTextureUri(stageDef.gets("haloTexture"), RC_NULL); if(!haloTextureUri.isEmpty()) { // Select a system flare by numeric identifier? if(haloTextureUri.path().length() == 1 && haloTextureUri.path().toStringRef().first().isDigit()) { haloTextureIndex = haloTextureUri.path().toStringRef().first().digitValue(); } else { haloTexture = resSys().texture("Flaremaps", haloTextureUri); } } return new AnimationStage(stageDef.geti("tics"), stageDef.getf("variance"), Vector2f(stageDef.geta("origin")), stageDef.getf("elevation"), Vector3f(stageDef.geta("color")), stageDef.getf("radius"), stageDef.getf("haloRadius"), LightRange(Vector2f(stageDef.geta("lightLevels"))), lightmapUp, lightmapDown, lightmapSide, haloTexture, haloTextureIndex); } String MaterialLightDecoration::AnimationStage::description() const { return String(_E(l) "Tics: ") + _E(.) + (tics > 0? String("%1 (~%2)").arg(tics).arg(variance, 0, 'g', 2) : "-1") + _E(l) " Origin: " _E(.) + origin.asText() + _E(l) " Elevation: " _E(.) + String::number(elevation, 'f', 2) + _E(l) " LightLevels: " _E(.) + lightLevels.asText() + _E(l) "\nColor: " _E(.) + color.asText() + _E(l) " Radius: " _E(.) + String::number(radius, 'f', 2) + _E(l) " HaloRadius: " _E(.) + String::number(haloRadius, 'f', 2); } // ------------------------------------------------------------------------------------ MaterialLightDecoration::MaterialLightDecoration(Vector2i const &patternSkip, Vector2i const &patternOffset, bool useInterpolation) : Decoration(patternSkip, patternOffset) , _useInterpolation(useInterpolation) {} MaterialLightDecoration::~MaterialLightDecoration() {} MaterialLightDecoration *MaterialLightDecoration::fromDef(Record const &definition) { defn::MaterialDecoration decorDef(definition); auto *decor = new MaterialLightDecoration(Vector2i(decorDef.geta("patternSkip")), Vector2i(decorDef.geta("patternOffset"))); for(int i = 0; i < decorDef.stageCount(); ++i) { decor->_stages.append(AnimationStage::fromDef(decorDef.stage(i))); } return decor; } int MaterialLightDecoration::addStage(AnimationStage const &stageToCopy) { _stages.append(new AnimationStage(stageToCopy)); return _stages.count() - 1; } MaterialLightDecoration::AnimationStage &MaterialLightDecoration::stage(int index) const { return static_cast(Decoration::stage(index)); } String MaterialLightDecoration::describe() const { return "Light decoration"; } bool MaterialLightDecoration::useInterpolation() const { return _useInterpolation; } doomsday-stable-1.15.7/doomsday/client/src/resource/materialdetaillayer.cpp0000664000175000017500000000602212641367670026431 0ustar jaakkojaakko/** @file materialdetaillayer.cpp Logical material, detail-texture layer. * * @authors Copyright © 2011-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "resource/materialdetaillayer.h" #include "TextureScheme" #include "dd_main.h" using namespace de; static de::Uri findTextureForDetailStage(ded_detail_stage_t const &def) { try { if(def.texture) { return App_ResourceSystem().textureScheme("Details") .findByResourceUri(*def.texture) .composeUri(); } } catch(TextureScheme::NotFoundError const &) {} // Ignore this error. return de::Uri(); } MaterialDetailLayer::AnimationStage::AnimationStage(de::Uri const &texture, int tics, float variance, float scale, float strength, float maxDistance) : MaterialTextureLayer::AnimationStage(texture, tics, variance) { set("scale", scale); set("strength", strength); set("maxDistance", maxDistance); } MaterialDetailLayer::AnimationStage::AnimationStage(AnimationStage const &other) : MaterialTextureLayer::AnimationStage(other) {} MaterialDetailLayer::AnimationStage::~AnimationStage() {} void MaterialDetailLayer::AnimationStage::resetToDefaults() { MaterialTextureLayer::AnimationStage::resetToDefaults(); addNumber("scale", 1); addNumber("strength", 1); addNumber("maxDistance", 0); } MaterialDetailLayer::AnimationStage * MaterialDetailLayer::AnimationStage::fromDef(ded_detail_stage_t const &def) { de::Uri const texture = findTextureForDetailStage(def); return new AnimationStage(texture, def.tics, def.variance, def.scale, def.strength, def.maxDistance); } // ------------------------------------------------------------------------------------ MaterialDetailLayer *MaterialDetailLayer::fromDef(ded_detailtexture_t const &layerDef) { auto *layer = new MaterialDetailLayer(); // Only the one stage. layer->_stages.append(AnimationStage::fromDef(layerDef.stage)); return layer; } int MaterialDetailLayer::addStage(MaterialDetailLayer::AnimationStage const &stageToCopy) { _stages.append(new AnimationStage(stageToCopy)); return _stages.count() - 1; } String MaterialDetailLayer::describe() const { return "Detail layer"; } doomsday-stable-1.15.7/doomsday/client/src/resource/material.cpp0000664000175000017500000003742412641367670024223 0ustar jaakkojaakko/** @file material.cpp Logical material resource. * * @authors Copyright © 2009-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "resource/material.h" #include #include #include #include #include "dd_main.h" #include "MaterialManifest" #ifdef __CLIENT__ # include "MaterialAnimator" #endif #include "resource/materialdetaillayer.h" #include "resource/materialtexturelayer.h" #include "resource/materialshinelayer.h" namespace internal { enum MaterialFlag { //Unused1 = MATF_UNUSED1, DontDraw = MATF_NO_DRAW, ///< Map surfaces using the material should never be drawn. SkyMasked = MATF_SKYMASK, ///< Apply sky masking for map surfaces using the material. Valid = 0x8, ///< Marked as @em valid. DefaultFlags = Valid }; Q_DECLARE_FLAGS(MaterialFlags, MaterialFlag) Q_DECLARE_OPERATORS_FOR_FLAGS(MaterialFlags) } using namespace internal; using namespace de; int Material::Layer::stageCount() const { return _stages.count(); } Material::Layer::~Layer() { qDeleteAll(_stages); } Material::Layer::Stage &Material::Layer::stage(int index) const { if(stageCount()) { index = de::wrap(index, 0, _stages.count()); return *_stages[index]; } /// @throw MissingStageError No stages are defined. throw MissingStageError("Material::Layer::stage", "Layer has no stages"); } String Material::Layer::describe() const { return "abstract Layer"; } String Material::Layer::description() const { int const numStages = stageCount(); String str = _E(b) + describe() + _E(.) + " (" + String::number(numStages) + " stage" + DENG2_PLURAL_S(numStages) + "):"; for(int i = 0; i < numStages; ++i) { str += String("\n [%1] ").arg(i, 2) + _E(>) + stage(i).description() + _E(<); } return str; } // ------------------------------------------------------------------------------------ #ifdef __CLIENT__ DENG2_PIMPL_NOREF(Material::Decoration) { Material *material = nullptr; ///< Owning Material. Vector2i patternSkip; ///< Pattern skip intervals. Vector2i patternOffset; ///< Pattern skip interval offsets. }; Material::Decoration::Decoration(Vector2i const &patternSkip, Vector2i const &patternOffset) : d(new Instance) { d->patternSkip = patternSkip; d->patternOffset = patternOffset; } Material::Decoration::~Decoration() { qDeleteAll(_stages); } Material &Material::Decoration::material() { DENG2_ASSERT(d->material); return *d->material; } Material const &Material::Decoration::material() const { DENG2_ASSERT(d->material); return *d->material; } void Material::Decoration::setMaterial(Material *newOwner) { d->material = newOwner; } Vector2i const &Material::Decoration::patternSkip() const { return d->patternSkip; } Vector2i const &Material::Decoration::patternOffset() const { return d->patternOffset; } int Material::Decoration::stageCount() const { return _stages.count(); } Material::Decoration::Stage &Material::Decoration::stage(int index) const { if(stageCount()) { index = de::wrap(index, 0, _stages.count()); return *_stages[index]; } /// @throw MissingStageError No stages are defined. throw MissingStageError("Material::Decoration::stage", "Decoration has no stages"); } String Material::Decoration::describe() const { return "abstract Decoration"; } String Material::Decoration::description() const { int const numStages = stageCount(); String str = _E(b) + describe() + _E(.) + " (" + String::number(numStages) + " stage" + DENG2_PLURAL_S(numStages) + "):"; for(int i = 0; i < numStages; ++i) { str += String("\n [%1] ").arg(i, 2) + _E(>) + stage(i).description() + _E(<); } return str; } #endif // __CLIENT__ // ------------------------------------------------------------------------------------ DENG2_PIMPL(Material) , DENG2_OBSERVES(Texture, Deletion) , DENG2_OBSERVES(Texture, DimensionsChange) { MaterialManifest *manifest = nullptr; ///< Source manifest (always valid, not owned). Vector2i dimensions; ///< World dimensions in map coordinate space units. MaterialFlags flags = DefaultFlags; AudioEnvironmentId audioEnvironment { AE_NONE }; /// Layers (owned), from bottom-most to top-most draw order. QList layers; #ifdef __CLIENT__ /// Decorations (owned), to be projected into the world (relative to a Surface). QList decorations; /// Set of draw-context animators (owned). QList animators; #endif Instance(Public *i) : Base(i) {} ~Instance() { #ifdef __CLIENT__ self.clearAllAnimators(); self.clearAllDecorations(); #endif self.clearAllLayers(); } inline bool haveValidDimensions() const { return dimensions.x > 0 && dimensions.y > 0; } MaterialTextureLayer *firstTextureLayer() const { for(Layer *layer : layers) { if(layer->is()) continue; if(layer->is()) continue; if(auto *texLayer = layer->maybeAs()) { return texLayer; } } return nullptr; } /** * Determines which texture we would be interested in obtaining our world dimensions * from if our own dimensions are undefined. */ Texture *inheritDimensionsTexture() const { if(auto const *texLayer = firstTextureLayer()) { if(texLayer->stageCount() >= 1) { try { return &App_ResourceSystem().texture(de::Uri(texLayer->stage(0).gets("texture"), RC_NULL)); } catch(TextureManifest::MissingTextureError &) {} catch(ResourceSystem::MissingManifestError &) {} } } return nullptr; } /** * Determines whether the world dimensions are now defined and if so cancels future * notifications about changes to texture dimensions. */ void maybeCancelTextureDimensionsChangeNotification() { // Both dimensions must still be undefined. if(haveValidDimensions()) return; Texture *inheritanceTexture = inheritDimensionsTexture(); if(!inheritanceTexture) return; inheritanceTexture->audienceForDimensionsChange -= this; // Thusly, we are no longer interested in deletion notification either. inheritanceTexture->audienceForDeletion -= this; } // Observes Texture DimensionsChange. void textureDimensionsChanged(Texture const &texture) { DENG2_ASSERT(!haveValidDimensions()); // Sanity check. self.setDimensions(texture.dimensions()); } // Observes Texture Deletion. void textureBeingDeleted(Texture const &texture) { // If here it means the texture we were planning to inherit dimensions from is // being deleted and therefore we won't be able to. DENG2_ASSERT(!haveValidDimensions()); // Sanity check. DENG2_ASSERT(inheritDimensionsTexture() == &texture); // Sanity check. /// @todo kludge: Clear the association so we don't try to cancel notifications later. firstTextureLayer()->stage(0).set("texture", ""); #if !defined(DENG2_DEBUG) DENG2_UNUSED(texture); #endif } #ifdef __CLIENT__ MaterialAnimator *findAnimator(MaterialVariantSpec const &spec, bool canCreate = false) { for(MaterialAnimator const *animator : animators) { if(animator->variantSpec().compare(spec)) { return const_cast(animator); // This will do fine. } } if(!canCreate) return nullptr; animators.append(new MaterialAnimator(self, spec)); return animators.back(); } #endif // __CLIENT__ DENG2_PIMPL_AUDIENCE(Deletion) DENG2_PIMPL_AUDIENCE(DimensionsChange) }; DENG2_AUDIENCE_METHOD(Material, Deletion) DENG2_AUDIENCE_METHOD(Material, DimensionsChange) Material::Material(MaterialManifest &manifest) : MapElement(DMU_MATERIAL) , d(new Instance(this)) { d->manifest = &manifest; } Material::~Material() { d->maybeCancelTextureDimensionsChangeNotification(); DENG2_FOR_AUDIENCE2(Deletion, i) i->materialBeingDeleted(*this); } MaterialManifest &Material::manifest() const { DENG2_ASSERT(d->manifest); return *d->manifest; } Vector2i const &Material::dimensions() const { return d->dimensions; } void Material::setDimensions(Vector2i const &newDimensions) { Vector2i const newDimensionsClamped = newDimensions.max(Vector2i(0, 0)); if(d->dimensions != newDimensionsClamped) { d->dimensions = newDimensionsClamped; d->maybeCancelTextureDimensionsChangeNotification(); // Notify interested parties. DENG2_FOR_AUDIENCE2(DimensionsChange, i) i->materialDimensionsChanged(*this); } } void Material::setHeight(int newHeight) { setDimensions(Vector2i(width(), newHeight)); } void Material::setWidth(int newWidth) { setDimensions(Vector2i(newWidth, height())); } bool Material::isDrawable() const { return d->flags.testFlag(DontDraw) == false; } bool Material::isSkyMasked() const { return d->flags.testFlag(SkyMasked); } bool Material::isValid() const { return d->flags.testFlag(Valid); } void Material::markDontDraw(bool yes) { if(yes) d->flags |= DontDraw; else d->flags &= ~DontDraw; } void Material::markSkyMasked(bool yes) { if(yes) d->flags |= SkyMasked; else d->flags &= ~SkyMasked; } void Material::markValid(bool yes) { if(yes) d->flags |= Valid; else d->flags &= ~Valid; } AudioEnvironmentId Material::audioEnvironment() const { return (isDrawable()? d->audioEnvironment : AE_NONE); } void Material::setAudioEnvironment(AudioEnvironmentId newEnvironment) { d->audioEnvironment = newEnvironment; } void Material::clearAllLayers() { d->maybeCancelTextureDimensionsChangeNotification(); qDeleteAll(d->layers); d->layers.clear(); } int Material::layerCount() const { return d->layers.count(); } void Material::addLayerAt(Layer *layer, int position) { if(!layer) return; if(d->layers.contains(layer)) return; position = de::clamp(0, position, layerCount()); d->maybeCancelTextureDimensionsChangeNotification(); d->layers.insert(position, layer); if(!d->haveValidDimensions()) { if(Texture *tex = d->inheritDimensionsTexture()) { tex->audienceForDeletion += d; tex->audienceForDimensionsChange += d; } } } Material::Layer &Material::layer(int index) const { if(index >= 0 && index < layerCount()) return *d->layers[index]; /// @throw Material::MissingLayerError Invalid layer reference. throw MissingLayerError("Material::layer", "Unknown layer #" + String::number(index)); } Material::Layer *Material::layerPtr(int index) const { if(index >= 0 && index < layerCount()) return d->layers[index]; return nullptr; } #ifdef __CLIENT__ int Material::decorationCount() const { return d->decorations.count(); } LoopResult Material::forAllDecorations(std::function func) const { for(Decoration *decor : d->decorations) { if(auto result = func(*decor)) return result; } return LoopContinue; } /// @todo Update client side MaterialAnimators? void Material::addDecoration(Decoration *decor) { if(!decor || d->decorations.contains(decor)) return; decor->setMaterial(this); d->decorations.append(decor); } void Material::clearAllDecorations() { qDeleteAll(d->decorations); d->decorations.clear(); } int Material::animatorCount() const { return d->animators.count(); } bool Material::hasAnimator(MaterialVariantSpec const &spec) { return d->findAnimator(spec) != nullptr; } MaterialAnimator &Material::getAnimator(MaterialVariantSpec const &spec) { return *d->findAnimator(spec, true/*create*/); } LoopResult Material::forAllAnimators(std::function func) const { for(MaterialAnimator *animator : d->animators) { if(auto result = func(*animator)) return result; } return LoopContinue; } void Material::clearAllAnimators() { qDeleteAll(d->animators); d->animators.clear(); } #endif // __CLIENT__ String Material::describe() const { return "Material \"" + manifest().composeUri().asText() + "\""; } String Material::description() const { String str = String(_E(l) "Dimensions: ") + _E(.) + (d->haveValidDimensions()? dimensions().asText() : "unknown (not yet prepared)") + _E(l) + " Source: " + _E(.) + manifest().sourceDescription() #ifdef __CLIENT__ + _E(b) + " x" + String::number(animatorCount()) + _E(.) #endif + _E(l) + "\nDrawable: " + _E(.) + DENG2_BOOL_YESNO(isDrawable()) #ifdef __CLIENT__ + _E(l) + " EnvClass: \"" + _E(.) + (audioEnvironment() == AE_NONE? "N/A" : S_AudioEnvironmentName(audioEnvironment())) + "\"" #endif + _E(l) + " SkyMasked: " + _E(.) + DENG2_BOOL_YESNO(isSkyMasked()); // Add the layer config: for(Layer const *layer : d->layers) { str += "\n" + layer->description(); } #ifdef __CLIENT__ // Add the decoration config: for(Decoration const *decor : d->decorations) { str += "\n" + decor->description(); } #endif return str; } int Material::property(DmuArgs &args) const { switch(args.prop) { case DMU_FLAGS: { short f = d->flags; args.setValue(DMT_MATERIAL_FLAGS, &f, 0); break; } case DMU_HEIGHT: { int h = d->dimensions.y; args.setValue(DMT_MATERIAL_HEIGHT, &h, 0); break; } case DMU_WIDTH: { int w = d->dimensions.x; args.setValue(DMT_MATERIAL_WIDTH, &w, 0); break; } default: return MapElement::property(args); } return false; // Continue iteration. } D_CMD(InspectMaterial) { DENG2_UNUSED(src); ResourceSystem &resSys = App_ResourceSystem(); de::Uri search = de::Uri::fromUserInput(&argv[1], argc - 1); if(!search.scheme().isEmpty() && !resSys.knownMaterialScheme(search.scheme())) { LOG_SCR_WARNING("Unknown scheme \"%s\"") << search.scheme(); return false; } try { MaterialManifest &manifest = resSys.materialManifest(search); if(Material *material = manifest.materialPtr()) { LOG_SCR_MSG(_E(D)_E(b) "%s\n" _E(.)_E(.)) << material->describe() << material->description(); } else { LOG_SCR_MSG(manifest.description()); } return true; } catch(ResourceSystem::MissingManifestError const &er) { LOG_SCR_WARNING("%s") << er.asText(); } return false; } void Material::consoleRegister() // static { C_CMD("inspectmaterial", "ss", InspectMaterial) C_CMD("inspectmaterial", "s", InspectMaterial) } doomsday-stable-1.15.7/doomsday/client/src/resource/api_resource.cpp0000664000175000017500000002711212641367670025076 0ustar jaakkojaakko/** @file api_resource.cpp Public API of the resource subsystem. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #define DENG_NO_API_MACROS_RESOURCE #include "de_base.h" #include "api_resource.h" #include "resource/resourcesystem.h" #include "gl/gl_tex.h" // averagealpha_analysis_t, etc... #ifdef __CLIENT__ # include "render/r_draw.h" // Rend_PatchTextureSpec() # include "render/r_main.h" // texGammaLut #endif using namespace de; #undef Textures_UniqueId2 DENG_EXTERN_C int Textures_UniqueId2(uri_s const *_uri, dd_bool quiet) { LOG_AS("Textures_UniqueId"); if(!_uri) return -1; de::Uri const &uri = reinterpret_cast(*_uri); try { return App_ResourceSystem().textureManifest(uri).uniqueId(); } catch(ResourceSystem::MissingManifestError const &) { // Log but otherwise ignore this error. if(!quiet) { LOG_RES_WARNING("Unknown texture %s") << uri; } } return -1; } #undef Textures_UniqueId DENG_EXTERN_C int Textures_UniqueId(uri_s const *uri) { return Textures_UniqueId2(uri, false); } #undef R_CreateAnimGroup DENG_EXTERN_C int R_CreateAnimGroup(int flags) { return App_ResourceSystem().newAnimGroup(flags & ~AGF_PRECACHE).id(); } #undef R_AddAnimGroupFrame DENG_EXTERN_C void R_AddAnimGroupFrame(int groupId, uri_s const *textureUri_, int tics, int randomTics) { LOG_AS("R_AddAnimGroupFrame"); if(!textureUri_) return; de::Uri const &textureUri = reinterpret_cast(*textureUri_); try { if(AnimGroup *group = App_ResourceSystem().animGroup(groupId)) { group->newFrame(App_ResourceSystem().textureManifest(textureUri), tics, randomTics); } else { LOG_DEBUG("Unknown anim group #%i, ignoring.") << groupId; } } catch(ResourceSystem::MissingManifestError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ". Failed adding texture \"%s\" to group #%i, ignoring.") << textureUri << groupId; } } #undef R_CreateColorPalette DENG_EXTERN_C colorpaletteid_t R_CreateColorPalette(char const *colorFormatDescriptor, char const *nameCStr, uint8_t const *colorData, int colorCount) { DENG2_ASSERT(nameCStr != 0 && colorFormatDescriptor != 0 && colorData != 0); LOG_AS("R_CreateColorPalette"); String name(nameCStr); if(name.isEmpty()) { LOG_RES_WARNING("Invalid/zero-length name specified, ignoring."); return 0; } try { QVector colors = ColorTableReader::read(colorFormatDescriptor, colorCount, colorData); // Replacing an existing palette? if(App_ResourceSystem().hasColorPalette(name)) { ColorPalette &palette = App_ResourceSystem().colorPalette(name); palette.replaceColorTable(colors); return palette.id(); } // A new palette. ColorPalette *palette = new ColorPalette(colors); App_ResourceSystem().addColorPalette(*palette, name); return palette->id(); } catch(ColorTableReader::FormatError const &er) { LOG_RES_WARNING("Error creating/replacing color palette '%s':\n") << name << er.asText(); } return 0; } #undef R_CreateColorPaletteTranslation DENG_EXTERN_C void R_CreateColorPaletteTranslation(colorpaletteid_t paletteId, ddstring_s const *translationId, uint8_t const *mappings_) { DENG2_ASSERT(mappings_ != 0); LOG_AS("R_CreateColorPaletteTranslation"); try { ColorPalette &palette = App_ResourceSystem().colorPalette(paletteId); // Convert the mapping table. int const colorCount = palette.colorCount(); ColorPaletteTranslation mappings(colorCount); for(int i = 0; i < colorCount; ++i) { int const palIdx = mappings_[i]; DENG2_ASSERT(palIdx >= 0 && palIdx < colorCount); mappings[i] = palIdx; } // Create/update this translation. palette.newTranslation(Str_Text(translationId), mappings); } catch(ResourceSystem::MissingResourceError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING("Error creating/replacing color palette '%u' translation '%s':\n") << paletteId << Str_Text(translationId) << er.asText(); } catch(ColorPalette::InvalidTranslationIdError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING("Error creating/replacing color palette '%u' translation '%s':\n") << paletteId << Str_Text(translationId) << er.asText(); } } #undef R_GetColorPaletteNumForName DENG_EXTERN_C colorpaletteid_t R_GetColorPaletteNumForName(char const *name) { LOG_AS("R_GetColorPaletteNumForName"); try { return App_ResourceSystem().colorPalette(name).id(); } catch(ResourceSystem::MissingResourceError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ", ignoring."); } return 0; // Not found. } #undef R_GetColorPaletteNameForNum DENG_EXTERN_C char const *R_GetColorPaletteNameForNum(colorpaletteid_t id) { LOG_AS("R_GetColorPaletteNameForNum"); try { ColorPalette &palette = App_ResourceSystem().colorPalette(id); return App_ResourceSystem().colorPaletteName(palette).toUtf8().constData(); } catch(ResourceSystem::MissingResourceError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ", ignoring."); } return 0; // Not found. } #undef R_GetColorPaletteRGBubv DENG_EXTERN_C void R_GetColorPaletteRGBubv(colorpaletteid_t paletteId, int colorIdx, uint8_t rgb[3], dd_bool applyTexGamma) { LOG_AS("R_GetColorPaletteRGBubv"); if(!rgb) return; // Always interpret a negative color index as black. if(colorIdx < 0) { rgb[0] = rgb[1] = rgb[2] = 0; return; } try { Vector3ub palColor = App_ResourceSystem().colorPalette(paletteId)[colorIdx]; rgb[0] = palColor.x; rgb[1] = palColor.y; rgb[2] = palColor.z; if(applyTexGamma) { rgb[0] = texGammaLut[rgb[0]]; rgb[1] = texGammaLut[rgb[1]]; rgb[2] = texGammaLut[rgb[2]]; } } catch(ResourceSystem::MissingResourceError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ", ignoring."); } } #undef R_GetColorPaletteRGBf DENG_EXTERN_C void R_GetColorPaletteRGBf(colorpaletteid_t paletteId, int colorIdx, float rgb[3], dd_bool applyTexGamma) { LOG_AS("R_GetColorPaletteRGBf"); if(!rgb) return; // Always interpret a negative color index as black. if(colorIdx < 0) { rgb[0] = rgb[1] = rgb[2] = 0; return; } try { ColorPalette &palette = App_ResourceSystem().colorPalette(paletteId); if(applyTexGamma) { Vector3ub palColor = palette[colorIdx]; rgb[0] = texGammaLut[palColor.x] * reciprocal255; rgb[1] = texGammaLut[palColor.y] * reciprocal255; rgb[2] = texGammaLut[palColor.z] * reciprocal255; } else { Vector3f palColor = palette.colorf(colorIdx); rgb[0] = palColor.x; rgb[1] = palColor.y; rgb[2] = palColor.z; } } catch(ResourceSystem::MissingResourceError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ", ignoring."); } } #undef R_ComposePatchPath DENG_EXTERN_C AutoStr *R_ComposePatchPath(patchid_t id) { LOG_AS("R_ComposePatchPath"); try { TextureManifest &manifest = App_ResourceSystem().textureScheme("Patches").findByUniqueId(id); return AutoStr_FromTextStd(manifest.path().toUtf8().constData()); } catch(TextureScheme::NotFoundError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ", ignoring."); } return AutoStr_NewStd(); } #undef R_ComposePatchUri DENG_EXTERN_C uri_s *R_ComposePatchUri(patchid_t id) { try { TextureManifest &manifest = App_ResourceSystem().textureScheme("Patches").findByUniqueId(id); return reinterpret_cast(new de::Uri(manifest.composeUri())); } catch(TextureScheme::NotFoundError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ", ignoring."); } return reinterpret_cast(new de::Uri()); } #undef R_DeclarePatch DENG_EXTERN_C patchid_t R_DeclarePatch(char const *encodedName) { return App_ResourceSystem().declarePatch(encodedName); } #undef R_GetPatchInfo DENG_EXTERN_C dd_bool R_GetPatchInfo(patchid_t id, patchinfo_t *info) { DENG2_ASSERT(info); LOG_AS("R_GetPatchInfo"); de::zapPtr(info); if(!id) return false; try { Texture &tex = App_ResourceSystem().textureScheme("Patches").findByUniqueId(id).texture(); #ifdef __CLIENT__ // Ensure we have up to date information about this patch. TextureVariantSpec const &texSpec = Rend_PatchTextureSpec(0 | (tex.isFlagged(Texture::Monochrome) ? TSF_MONOCHROME : 0) | (tex.isFlagged(Texture::UpscaleAndSharpen) ? TSF_UPSCALE_AND_SHARPEN : 0)); tex.prepareVariant(texSpec); #endif info->id = id; info->flags.isCustom = tex.isFlagged(Texture::Custom); averagealpha_analysis_t *aa = reinterpret_cast(tex.analysisDataPointer(Texture::AverageAlphaAnalysis)); info->flags.isEmpty = aa && FEQUAL(aa->alpha, 0); info->geometry.size.width = tex.width(); info->geometry.size.height = tex.height(); info->geometry.origin.x = tex.origin().x; info->geometry.origin.y = tex.origin().y; /// @todo fixme: kludge: info->extraOffset[0] = info->extraOffset[1] = (tex.isFlagged(Texture::UpscaleAndSharpen)? -1 : 0); // Kludge end. return true; } catch(TextureManifest::MissingTextureError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ", ignoring."); } catch(TextureScheme::NotFoundError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING(er.asText() + ", ignoring."); } return false; } DENG_DECLARE_API(R) = { { DE_API_RESOURCE }, R_DeclarePatch, R_GetPatchInfo, R_ComposePatchUri, R_ComposePatchPath, R_CreateAnimGroup, R_AddAnimGroupFrame, R_CreateColorPalette, R_CreateColorPaletteTranslation, R_GetColorPaletteNumForName, R_GetColorPaletteNameForNum, R_GetColorPaletteRGBf, R_GetColorPaletteRGBubv, Textures_UniqueId, Textures_UniqueId2, }; doomsday-stable-1.15.7/doomsday/client/src/resource/sprite.cpp0000664000175000017500000001217712641367670023731 0ustar jaakkojaakko/** @file sprite.cpp 3D-Sprite resource. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * @authors Copyright © 1993-1996 by id Software, Inc. * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "resource/sprite.h" #ifdef __CLIENT__ # include "TextureManifest" # include "gl/gl_tex.h" // pointlight_analysis_t # include "Lumobj" # include "render/billboard.h" // Rend_SpriteMaterialSpec #endif #include using namespace de; /** * @todo optimize: allocate only those ViewAngles that are in use. */ DENG2_PIMPL_NOREF(Sprite) { bool haveRotations; ///< @c true= use all view angles and not just rotation 0. ViewAngles viewAngles; Instance() : haveRotations(false) , viewAngles(max_angles) {} Instance(Instance const &other) : de::IPrivate() , haveRotations(other.haveRotations) , viewAngles(other.viewAngles) {} }; Sprite::Sprite() : d(new Instance) {} Sprite::Sprite(Sprite const &other) : d(new Instance(*other.d)) {} Sprite &Sprite::operator = (Sprite const &other) { d.reset(new Instance(*other.d)); return *this; } bool Sprite::hasViewAngle(int rotation) const { if(rotation >= 0 && rotation < d->viewAngles.count()) { return d->viewAngles[rotation].material != 0; } return false; } void Sprite::newViewAngle(Material *material, int rotation, bool mirrorX) { DENG2_ASSERT(rotation <= max_angles); if(rotation <= 0) { // Use only one view angle for all rotations. d->haveRotations = false; for(int i = 0; i < d->viewAngles.count(); ++i) { ViewAngle &vAngle = d->viewAngles[i]; vAngle.material = material; vAngle.mirrorX = mirrorX; } return; } rotation--; // Make 0 based. d->haveRotations = true; ViewAngle &vAngle = d->viewAngles[rotation]; vAngle.material = material; vAngle.mirrorX = mirrorX; } Sprite::ViewAngle const &Sprite::viewAngle(int rotation) const { LOG_AS("Sprite::viewAngle"); if(rotation >= 0 && rotation < d->viewAngles.count()) { return d->viewAngles[rotation]; } /// @throw MissingViewAngle Specified an invalid rotation. throw MissingViewAngleError("Sprite::viewAngle", "Invalid rotation " + String::number(rotation)); } Sprite::ViewAngle const &Sprite::closestViewAngle(angle_t mobjAngle, angle_t angleToEye, bool noRotation) const { int rotation = 0; // Use single rotation for all viewing angles (default). if(!noRotation && d->haveRotations) { // Rotation is determined by the relative angle to the viewer. rotation = ((angleToEye - mobjAngle + (unsigned) (ANG45 / 2) * 9) - (unsigned) (ANGLE_180 / 16)) >> 28; } return viewAngle(rotation); } Sprite::ViewAngles const &Sprite::viewAngles() const { return d->viewAngles; } #ifdef __CLIENT__ double Sprite::visualRadius() const { if(hasViewAngle(0)) { MaterialAnimator &matAnimator = viewAngle(0).material->getAnimator(Rend_SpriteMaterialSpec()); // Ensure we've up to date info about the material. matAnimator.prepare(); return matAnimator.dimensions().x / 2; } return 0; } Lumobj *Sprite::generateLumobj() const { LOG_AS("Sprite::generateLumobj"); // Always use rotation zero. /// @todo We could do better here... if(!hasViewAngle(0)) return nullptr; MaterialAnimator &matAnimator = viewAngle(0).material->getAnimator(Rend_SpriteMaterialSpec()); // Ensure we have up-to-date information about the material. matAnimator.prepare(); TextureVariant *texture = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; if(!texture) return nullptr; // Unloadable texture? auto const *pl = (pointlight_analysis_t const *)texture->base().analysisDataPointer(Texture::BrightPointAnalysis); if(!pl) { LOGDEV_RES_WARNING("Texture \"%s\" has no BrightPointAnalysis") << texture->base().manifest().composeUri(); return nullptr; } // Apply the auto-calculated color. return &(new Lumobj(Vector3d(), pl->brightMul, pl->color.rgb)) ->setZOffset(-texture->base().origin().y - pl->originY * matAnimator.dimensions().y); } #endif // __CLIENT__ doomsday-stable-1.15.7/doomsday/client/src/resource/patch.cpp0000664000175000017500000002477412641367670023530 0ustar jaakkojaakko/** @file patch.cpp Patch Image Format. * * @authors Copyright © 1999-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include #include "resource/patch.h" #include namespace de { namespace internal { /** * Serialized format header. */ struct Header : public IReadable { /// Logical dimensions of the patch in pixels. dint16 dimensions[2]; /// Origin offset (top left) in world coordinate space units. dint16 origin[2]; /// Implements IReadable. void operator << (Reader &from) { from >> dimensions[0] >> dimensions[1] >> origin[0] >> origin[1]; } }; /** * A @em Post is a run of one or more non-masked pixels. */ struct Post : public IReadable { /// Y-Offset to the start of the run in texture space (0-base). dbyte topOffset; /// Length of the run in pixels (inclusive). dbyte length; /// Offset to the first pixel palette index in the source data. size_t firstPixel; /// Implements IReadable. void operator << (Reader &from) { from >> topOffset >> length; firstPixel = from.offset() + 1 /*unused junk*/; } }; /// A @em Column is a list of 0 or more posts. typedef QList Posts; typedef Posts Column; typedef QList Columns; /// Offsets to columns from the start of the source data. typedef QList ColumnOffsets; /** * Attempt to read another @a post from the @a reader. * @return @c true if another post was read; otherwise @c false. */ static bool readNextPost(Post &post, Reader &reader) { static int const END_OF_POSTS = 0xff; // Any more? reader.mark(); dbyte nextByte; reader >> nextByte; reader.rewind(); if(nextByte == END_OF_POSTS) return false; // Another post begins. reader >> post; return true; } /** * Visit each of @a offsets producing a column => post map. */ static Columns readPosts(ColumnOffsets const &offsets, Reader &reader) { Columns columns; #ifdef DENG2_QT_4_7_OR_NEWER columns.reserve(offsets.size()); #endif Post post; DENG2_FOR_EACH_CONST(ColumnOffsets, i, offsets) { reader.setOffset(*i); // A new column begins. columns.push_back(Column()); Column &column = columns.back(); // Read all posts. while(readNextPost(post, reader)) { column.push_back(post); // Skip to the next post. reader.seek(1 + post.length + 1); // A byte of unsed junk either side of the pixel palette indices. } } return columns; } /** * Read @a width column offsets from the @a reader. */ static ColumnOffsets readColumnOffsets(int width, Reader &reader) { ColumnOffsets offsets; #ifdef DENG2_QT_4_7_OR_NEWER offsets.reserve(width); #endif for(int col = 0; col < width; ++col) { dint32 offset; reader >> offset; offsets.push_back(offset); } return offsets; } static inline Columns readColumns(int width, Reader &reader) { return readPosts(readColumnOffsets(width, reader), reader); } /** * Process @a columns to calculate the "real" pixel height of the image. */ static int calcRealHeight(Columns const &columns) { QRect geom(QPoint(0, 0), QSize(1, 0)); DENG2_FOR_EACH_CONST(Columns, colIt, columns) { int tallTop = -1; // Keep track of pos (clipping). DENG2_FOR_EACH_CONST(Posts, postIt, *colIt) { Post const &post = *postIt; // Does this post extend the previous one? (a so-called "tall-patch"). if(post.topOffset <= tallTop) tallTop += post.topOffset; else tallTop = post.topOffset; // Skip invalid posts. if(post.length <= 0) continue; // Unite the geometry of the post. geom |= QRect(QPoint(0, tallTop), QSize(1, post.length)); } } return geom.height(); } static Block compositeImage(Reader &reader, ColorPaletteTranslation const *xlatTable, Columns const &columns, Patch::Metadata const &meta, Patch::Flags flags) { bool const maskZero = flags.testFlag(Patch::MaskZero); bool const clipToLogicalDimensions = flags.testFlag(Patch::ClipToLogicalDimensions); #ifdef DENG_DEBUG // Is the "logical" height of the image equal to the actual height of the // composited pixel posts? if(meta.logicalDimensions.y != meta.dimensions.y) { int postCount = 0; DENG2_FOR_EACH_CONST(Columns, i, columns) { postCount += i->count(); } LOGDEV_RES_NOTE("Inequal heights, logical: %i != actual: %i (%i %s)") << meta.logicalDimensions.y << meta.dimensions.y << postCount << (postCount == 1? "post" : "posts"); } #endif // Determine the dimensions of the output buffer. Vector2i const &dimensions = clipToLogicalDimensions? meta.logicalDimensions : meta.dimensions; int const w = dimensions.x; int const h = dimensions.y; size_t const pels = w * h; // Create the output buffer and fill with default color (black) and alpha (transparent). Block output = QByteArray(2 * pels, 0); // Map the pixel color channels in the output buffer. dbyte *top = output.data(); dbyte *topAlpha = output.data() + pels; // Composite the patch into the output buffer. DENG2_FOR_EACH_CONST(Columns, colIt, columns) { int tallTop = -1; // Keep track of pos (clipping). DENG2_FOR_EACH_CONST(Posts, postIt, *colIt) { Post const &post = *postIt; // Does this post extend the previous one? (a so-called "tall-patch"). if(post.topOffset <= tallTop) tallTop += post.topOffset; else tallTop = post.topOffset; // Skip invalid posts. if(post.length <= 0) continue; // Determine the destination height range. int y = tallTop; int length = post.length; // Clamp height range within bounds. if(y + length > h) length = h - y; int offset = 0; if(y < 0) { offset = de::min(-y, length); y = 0; } length = de::max(0, length - offset); // Skip empty ranges. if(!length) continue; // Find the start of the pixel data for the post. reader.setOffset(post.firstPixel + offset); dbyte *out = top + tallTop * w; dbyte *outAlpha = topAlpha + tallTop * w; // Composite pixels from the post to the output buffer. while(length--) { // Read the next palette index. dbyte palIdx; reader >> palIdx; // Is palette index translation in effect? if(xlatTable) { palIdx = dbyte(xlatTable->at(palIdx)); } if(!maskZero || palIdx) { *out = palIdx; } if(maskZero) *outAlpha = (palIdx? 0xff : 0); else *outAlpha = 0xff; // Move one row down. out += w; outAlpha += w; } } // Move one column right. top++; topAlpha++; } return output; } } // internal static Patch::Metadata prepareMetadata(internal::Header const &hdr, int realHeight) { Patch::Metadata meta; meta.dimensions = Vector2i(hdr.dimensions[0], realHeight); meta.logicalDimensions = Vector2i(hdr.dimensions[0], hdr.dimensions[1]); meta.origin = Vector2i(hdr.origin[0], hdr.origin[1]); return meta; } Patch::Metadata Patch::loadMetadata(IByteArray const &data) { LOG_AS("Patch::loadMetadata"); Reader reader = Reader(data); internal::Header hdr; reader >> hdr; internal::Columns columns = internal::readColumns(hdr.dimensions[0], reader); return prepareMetadata(hdr, internal::calcRealHeight(columns)); } Block Patch::load(IByteArray const &data, ColorPaletteTranslation const &xlatTable, Patch::Flags flags) { LOG_AS("Patch::load"); Reader reader = Reader(data); internal::Header hdr; reader >> hdr; internal::Columns columns = internal::readColumns(hdr.dimensions[0], reader); Metadata meta = prepareMetadata(hdr, internal::calcRealHeight(columns)); return internal::compositeImage(reader, &xlatTable, columns, meta, flags); } Block Patch::load(IByteArray const &data, Patch::Flags flags) { LOG_AS("Patch::load"); Reader reader = Reader(data); internal::Header hdr; reader >> hdr; internal::Columns columns = internal::readColumns(hdr.dimensions[0], reader); Metadata meta = prepareMetadata(hdr, internal::calcRealHeight(columns)); return internal::compositeImage(reader, 0/* no translation */, columns, meta, flags); } bool Patch::recognize(IByteArray const &data) { try { // The format has no identification markings. // Instead we must rely on heuristic analyses of the column => post map. Reader from = Reader(data); internal::Header hdr; from >> hdr; if(!hdr.dimensions[0] || !hdr.dimensions[1]) return false; for(int col = 0; col < hdr.dimensions[0]; ++col) { dint32 offset; from >> offset; if(offset < 0 || (unsigned) offset >= from.source()->size()) return false; } // Validated. return true; } catch(IByteArray::OffsetError const &) {} // Ignore this error. return false; } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/resource/texture.cpp0000664000175000017500000002541512641367670024122 0ustar jaakkojaakko/** @file texture.cpp Logical texture resource. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "resource/texture.h" #include "dd_main.h" // App_ResourceSystem() #include "resource/resourcesystem.h" #include "resource/compositetexture.h" #include "TextureManifest" #include #include #include #include #include using namespace de; typedef QMap Analyses; DENG2_PIMPL(Texture) { TextureManifest &manifest; Flags flags; #ifdef __CLIENT__ /// Set of (render-) context variants. Variants variants; #endif /// User data associated with this texture. void *userData; /// World dimensions in map coordinate space units. Vector2i dimensions; /// World origin offset in map coordinate space units. Vector2i origin; /// Image analysis data, used for various purposes according to context. Analyses analyses; Instance(Public *i, TextureManifest &_manifest) : Base(i) , manifest(_manifest) , userData(0) {} ~Instance() { self.clearAnalyses(); #ifdef __CLIENT__ self.clearVariants(); #endif } /// Notify iterested parties of a change in world dimensions. void notifyDimensionsChanged() { DENG2_FOR_PUBLIC_AUDIENCE(DimensionsChange, i) i->textureDimensionsChanged(self); } }; Texture::Texture(TextureManifest &manifest) : d(new Instance(this, manifest)) { setFlags(manifest.flags()); setDimensions(manifest.logicalDimensions()); setOrigin(manifest.origin()); } Texture::~Texture() { DENG2_FOR_AUDIENCE(Deletion, i) i->textureBeingDeleted(*this); if(!manifest().schemeName().compareWithoutCase("Textures")) { CompositeTexture *pcTex = reinterpret_cast(userDataPointer()); if(pcTex) delete pcTex; } delete d; } TextureManifest &Texture::manifest() const { return d->manifest; } void Texture::setUserDataPointer(void *newUserData) { if(d->userData && newUserData) { LOG_AS("Texture::setUserDataPointer"); LOGDEV_RES_MSG("User data already present for \"%s\" %p, will be replaced") << d->manifest.composeUri() << this; } d->userData = newUserData; } void *Texture::userDataPointer() const { return d->userData; } Vector2i const &Texture::dimensions() const { return d->dimensions; } void Texture::setDimensions(Vector2i const &_newDimensions) { Vector2i newDimensions = _newDimensions.max(Vector2i(0, 0)); if(d->dimensions != newDimensions) { d->dimensions = newDimensions; d->notifyDimensionsChanged(); } } void Texture::setWidth(int newWidth) { if(d->dimensions.x != newWidth) { d->dimensions.x = newWidth; d->notifyDimensionsChanged(); } } void Texture::setHeight(int newHeight) { if(d->dimensions.y != newHeight) { d->dimensions.y = newHeight; d->notifyDimensionsChanged(); } } Vector2i const &Texture::origin() const { return d->origin; } void Texture::setOrigin(Vector2i const &newOrigin) { if(d->origin != newOrigin) { d->origin = newOrigin; } } #ifdef __CLIENT__ uint Texture::variantCount() const { return uint(d->variants.count()); } Texture::Variant *Texture::chooseVariant(ChooseVariantMethod method, TextureVariantSpec const &spec, bool canCreate) { foreach(Variant *variant, d->variants) { TextureVariantSpec const &cand = variant->spec(); switch(method) { case MatchSpec: if(&cand == &spec) { // This is the one we're looking for. return variant; } break; case FuzzyMatchSpec: if(cand == spec) { // This will do fine. return variant; } break; } } #ifdef DENG_DEBUG // 07/04/2011 dj: The "fuzzy selection" features are yet to be implemented. // As such, the following should NOT return a valid variant iff the rest of // this subsystem has been implemented correctly. // // Presently this is used as a sanity check. if(method == MatchSpec) { DENG_ASSERT(!chooseVariant(FuzzyMatchSpec, spec)); } #endif if(!canCreate) return 0; d->variants.push_back(new Variant(*this, spec)); return d->variants.back(); } Texture::Variant *Texture::prepareVariant(TextureVariantSpec const &spec) { Variant *variant = chooseVariant(MatchSpec, spec, true /*can create*/); DENG2_ASSERT(variant); variant->prepare(); return variant; } Texture::Variants const &Texture::variants() const { return d->variants; } void Texture::clearVariants() { while(!d->variants.isEmpty()) { Texture::Variant *variant = d->variants.takeFirst(); #ifdef DENG_DEBUG if(variant->glName()) { String textualVariantSpec = variant->spec().asText(); LOG_AS("Texture::clearVariants") LOGDEV_RES_WARNING("GLName (%i) still set for a variant of \"%s\" %p. Perhaps it wasn't released?") << variant->glName() << d->manifest.composeUri() << this << textualVariantSpec; } #endif delete variant; } } void Texture::releaseGLTextures(TextureVariantSpec *spec) { foreach(TextureVariant *variant, variants()) { if(!spec || spec == &variant->spec()) { variant->release(); if(spec) { break; } } } } #endif // __CLIENT__ void Texture::clearAnalyses() { foreach(void *data, d->analyses) { M_Free(data); } d->analyses.clear(); } void *Texture::analysisDataPointer(AnalysisId analysisId) const { if(!d->analyses.contains(analysisId)) return 0; return d->analyses.value(analysisId); } void Texture::setAnalysisDataPointer(AnalysisId analysisId, void *newData) { LOG_AS("Texture::attachAnalysis"); void *existingData = analysisDataPointer(analysisId); if(existingData) { #if _DEBUG if(newData) { LOGDEV_RES_VERBOSE("Image analysis (id:%i) already present for \"%s\", will be replaed") << int(analysisId) << d->manifest.composeUri(); } #endif M_Free(existingData); } d->analyses.insert(analysisId, newData); } Texture::Flags Texture::flags() const { return d->flags; } void Texture::setFlags(Texture::Flags flagsToChange, FlagOp operation) { applyFlagOperation(d->flags, flagsToChange, operation); } String Texture::description() const { String str = String("Texture \"%1\"").arg(manifest().composeUri().asText()); #ifdef DENG_DEBUG str += String(" [%2]").arg(de::dintptr(this)); #endif str += " Dimensions:" + (width() == 0 && height() == 0? String("unknown (not yet prepared)") : dimensions().asText()) + " Source:" + manifest().sourceDescription(); #ifdef __CLIENT__ str += String(" x%1").arg(variantCount()); #endif return str; } D_CMD(InspectTexture) { DENG2_UNUSED(src); de::Uri search = de::Uri::fromUserInput(&argv[1], argc - 1); if(!search.scheme().isEmpty() && !App_ResourceSystem().knownTextureScheme(search.scheme())) { LOG_RES_WARNING("Unknown scheme %s") << search.scheme(); return false; } try { TextureManifest &manifest = App_ResourceSystem().textureManifest(search); if(manifest.hasTexture()) { Texture &texture = manifest.texture(); String variantCountText; #ifdef __CLIENT__ variantCountText = de::String(" (x%1)").arg(texture.variantCount()); #endif LOG_RES_MSG("Texture " _E(b) "%s" _E(.) "%s" "\n" _E(l) "Dimensions: " _E(.) _E(i) "%s" _E(.) " " _E(l) "Source: " _E(.) _E(i) "%s") << manifest.composeUri() << variantCountText << (texture.width() == 0 && texture.height() == 0? String("unknown (not yet prepared)") : texture.dimensions().asText()) << manifest.sourceDescription(); #if defined(__CLIENT__) && defined(DENG_DEBUG) if(texture.variantCount()) { // Print variant specs. LOG_RES_MSG(_E(R)); int variantIdx = 0; foreach(TextureVariant *variant, texture.variants()) { Vector2f coords; variant->glCoords(&coords.x, &coords.y); String textualVariantSpec = variant->spec().asText(); LOG_RES_MSG(_E(D) "Variant #%i:" _E(.) " " _E(l) "Source: " _E(.) _E(i) "%s" _E(.) " " _E(l) "Masked: " _E(.) _E(i) "%s" _E(.) " " _E(l) "GLName: " _E(.) _E(i) "%i" _E(.) " " _E(l) "Coords: " _E(.) _E(i) "%s" _E(.) _E(R) "\n" _E(b) "Specification:" _E(.) "%s") << variantIdx << variant->sourceDescription() << (variant->isMasked()? "yes":"no") << variant->glName() << coords.asText() << textualVariantSpec; ++variantIdx; } } #endif // __CLIENT__ && DENG_DEBUG } else { LOG_RES_MSG("%s") << manifest.description(); } return true; } catch(ResourceSystem::MissingManifestError const &er) { LOG_RES_WARNING("%s.") << er.asText(); } return false; } void Texture::consoleRegister() // static { C_CMD("inspecttexture", "ss", InspectTexture) C_CMD("inspecttexture", "s", InspectTexture) } doomsday-stable-1.15.7/doomsday/client/src/resource/bitmapfont.cpp0000664000175000017500000002365112641367670024565 0ustar jaakkojaakko/** @file bitmapfont.cpp Bitmap font resource. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "resource/bitmapfont.h" #include "dd_main.h" // isDedicated #include "gl/gl_main.h" // GL_NewTextureWithParams #include "sys_system.h" // novideo #include "FontManifest" #include #include #include // M_CeilPow2() #include #include using namespace de; static byte inByte(FileHandle *file) { byte b; file->read((uint8_t *)&b, sizeof(b)); return b; } static ushort inShort(FileHandle *file) { ushort s; file->read((uint8_t *)&s, sizeof(s)); return USHORT(s); } struct Glyph { Rectanglei posCoords; Rectanglei texCoords; }; DENG2_PIMPL(BitmapFont) { String filePath; ///< The "archived" version of this font (if any). GLuint texGLName; ///< GL-texture name. Vector2ui texMargin; ///< Margin in pixels. Vector2i texDimensions; ///< Texture dimensions in pixels. bool needGLInit; /// Font metrics. int leading; int ascent; int descent; Glyph glyphs[MAX_CHARS]; Glyph missingGlyph; Instance(Public *i) : Base(i) , texGLName(0) , needGLInit(true) , leading(0) , ascent(0) , descent(0) {} ~Instance() { self.glDeinit(); } /** * Lookup the glyph for the specified character @a ch. If no glyph is defined * for this character then the special "missing glyph" is returned instead. */ Glyph &glyph(uchar ch) { //if(ch >= MAX_CHARS) return missingGlyph; return glyphs[ch]; } /** * Determine the dimensions of the "missing glyph" by averaging the dimensions * of the available/defined glyphs. * * @todo Could be smarter. Presently this treats @em all glyphs equally. */ Vector2ui findMissingGlyphSize() { Vector2ui accumSize; int glyphCount = 0; for(int i = 0; i < MAX_CHARS; ++i) { Glyph &glyph = glyphs[i]; if(glyph.posCoords.isNull()) continue; accumSize += glyph.posCoords.size(); glyphCount += 1; } return accumSize / glyphCount; } uint8_t *readFormat0(FileHandle *file) { DENG2_ASSERT(file != 0); self._flags |= AbstractFont::Colorize; self._flags &= ~AbstractFont::Shadowed; texMargin = Vector2ui(0, 0); // Load in the data. texDimensions.x = inShort(file); texDimensions.y = inShort(file); int glyphCount = inShort(file); for(int i = 0; i < glyphCount; ++i) { Glyph *ch = &glyphs[i < MAX_CHARS ? i : MAX_CHARS - 1]; ushort x = inShort(file); ushort y = inShort(file); ushort w = inByte(file); ushort h = inByte(file); ch->posCoords = Rectanglei::fromSize(Vector2i(0, 0), Vector2ui(w, h)); ch->texCoords = Rectanglei::fromSize(Vector2i(x, y), Vector2ui(w, h)); } missingGlyph.posCoords.setSize(findMissingGlyphSize()); int bitmapFormat = inByte(file); if(bitmapFormat > 0) { de::Uri uri = self.manifest().composeUri(); throw Error("BitmapFont::readFormat0", QString("Font \"%1\" uses unknown format '%2'").arg(uri).arg(bitmapFormat)); } // Read the glyph atlas texture. int const numPels = texDimensions.x * texDimensions.y; uint32_t *image = (uint32_t *) M_Calloc(numPels * 4); int c, i; for(c = i = 0; i < (numPels + 7) / 8; ++i) { int bit, mask = inByte(file); for(bit = 7; bit >= 0; bit--, ++c) { if(c >= numPels) break; if(mask & (1 << bit)) { image[c] = ~0; } } } return (uint8_t *)image; } uint8_t *readFormat2(FileHandle *file) { DENG2_ASSERT(file != 0); self._flags |= AbstractFont::Colorize | AbstractFont::Shadowed; int bitmapFormat = inByte(file); if(bitmapFormat != 1 && bitmapFormat != 0) // Luminance + Alpha. { de::Uri uri = self.manifest().composeUri(); throw Error("BitmapFont::readFormat2", QString("Font \"%1\" uses unknown format '%2'").arg(uri).arg(bitmapFormat)); } // Load in the data. texDimensions.x = inShort(file); texDimensions.y = M_CeilPow2(inShort(file)); int glyphCount = inShort(file); texMargin.x = texMargin.y = inShort(file); leading = inShort(file); /*glyphHeight =*/ inShort(file); // Unused. ascent = inShort(file); descent = inShort(file); for(int i = 0; i < glyphCount; ++i) { ushort code = inShort(file); DENG2_ASSERT(code < MAX_CHARS); Glyph *ch = &glyphs[code]; ushort x = inShort(file); ushort y = inShort(file); ushort w = inShort(file); ushort h = inShort(file); ch->posCoords = Rectanglei::fromSize(Vector2i(0, 0), Vector2ui(w, h) - texMargin * 2); ch->texCoords = Rectanglei::fromSize(Vector2i(x, y), Vector2ui(w, h)); } missingGlyph.posCoords.setSize(findMissingGlyphSize()); // Read the glyph atlas texture. int const numPels = texDimensions.x * texDimensions.y; uint32_t *image = (uint32_t *) M_Calloc(numPels * 4); if(bitmapFormat == 0) { uint32_t *ptr = image; for(int i = 0; i < numPels; ++i) { byte red = inByte(file); byte green = inByte(file); byte blue = inByte(file); byte alpha = inByte(file); *ptr++ = ULONG(red | (green << 8) | (blue << 16) | (alpha << 24)); } } else if(bitmapFormat == 1) { uint32_t *ptr = image; for(int i = 0; i < numPels; ++i) { byte luminance = inByte(file); byte alpha = inByte(file); *ptr++ = ULONG(luminance | (luminance << 8) | (luminance << 16) | (alpha << 24)); } } return (uint8_t *)image; } }; BitmapFont::BitmapFont(FontManifest &manifest) : AbstractFont(manifest), d(new Instance(this)) {} BitmapFont *BitmapFont::fromFile(FontManifest &manifest, String resourcePath) // static { BitmapFont *font = new BitmapFont(manifest); font->setFilePath(resourcePath); // Lets try and prepare it right away. font->glInit(); return font; } int BitmapFont::ascent() { glInit(); return d->ascent; } int BitmapFont::descent() { glInit(); return d->descent; } int BitmapFont::lineSpacing() { glInit(); return d->leading; } Rectanglei const &BitmapFont::glyphPosCoords(uchar ch) { glInit(); return d->glyph(ch).posCoords; } Rectanglei const &BitmapFont::glyphTexCoords(uchar ch) { glInit(); return d->glyph(ch).texCoords; } void BitmapFont::glInit() { LOG_AS("BitmapFont"); if(!d->needGLInit) return; if(novideo || isDedicated || BusyMode_Active()) return; glDeinit(); try { // Relative paths are relative to the native working directory. String path = (NativePath::workPath() / NativePath(d->filePath).expand()).withSeparators('/'); FileHandle *hndl = &App_FileSystem().openFile(path, "rb"); int format = inByte(hndl); uint8_t *pixels = 0; switch(format) { // Original format. case 0: pixels = d->readFormat0(hndl); break; // Enhanced format. case 2: pixels = d->readFormat2(hndl); break; default: DENG2_ASSERT(!"BitmapFont: Format not implemented"); } if(!pixels) return; // Upload the texture. if(!novideo && !isDedicated) { LOG_GL_XVERBOSE("Uploading atlas texture for \"%s\"...") << manifest().composeUri(); d->texGLName = GL_NewTextureWithParams(DGL_RGBA, d->texDimensions.x, d->texDimensions.y, pixels, 0, 0, GL_LINEAR, GL_NEAREST, 0 /* no AF */, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE); } M_Free(pixels); App_FileSystem().releaseFile(hndl->file()); delete hndl; } catch(FS1::NotFoundError const&) {} // Ignore error. d->needGLInit = false; } void BitmapFont::glDeinit() { LOG_AS("BitmapFont"); if(novideo || isDedicated) return; d->needGLInit = true; if(BusyMode_Active()) return; if(d->texGLName) { glDeleteTextures(1, (GLuint const *) &d->texGLName); } d->texGLName = 0; } void BitmapFont::setFilePath(String newFilePath) { if(d->filePath != newFilePath) { d->filePath = newFilePath; d->needGLInit = true; } } uint BitmapFont::textureGLName() const { return d->texGLName; } Vector2i const &BitmapFont::textureDimensions() const { return d->texDimensions; } Vector2ui const &BitmapFont::textureMargin() const { return d->texMargin; } doomsday-stable-1.15.7/doomsday/client/src/resource/colorpalette.cpp0000664000175000017500000002302112641367670025106 0ustar jaakkojaakko/** @file colorpalette.cpp Color palette resource. * * @authors Copyright © 1999-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "resource/colorpalette.h" #include "dd_share.h" // reciprocal255 #include "m_misc.h" // M_ReadBits #include #include using namespace de; namespace internal { /** * Example: "R8G8B8" */ static void parseColorFormat(QString const &fmt, Vector3ui &compOrder, Vector3ui &compBits) { compBits = Vector3ui(); int const end = fmt.length(); int readComponents = 0; int pos = 0; while(pos < end) { QChar ch = fmt[pos]; pos++; int comp = -1; if (ch == 'R' || ch == 'r') comp = 0; else if(ch == 'G' || ch == 'g') comp = 1; else if(ch == 'B' || ch == 'b') comp = 2; if(comp != -1 && compBits[comp] == 0) { compOrder[comp] = readComponents++; // Read the number of bits. int start = pos; ch = fmt[pos]; while(ch.isDigit() && ++pos < end) { ch = fmt[pos]; } int numDigits = pos - start; if(numDigits) { compBits[comp] = fmt.mid(start, numDigits).toInt(); // Are we done? if(readComponents == 3) break; continue; } } /// @throw ColorTableReader::FormatError throw ColorTableReader::FormatError("parseColorFormat", QString("Unexpected character '%1' at position %2").arg(ch).arg(pos)); } if(readComponents != 3) { /// @throw ColorTableReader::FormatError throw ColorTableReader::FormatError("parseColorFormat", "Incomplete format specification"); } } } // namespace internal using namespace ::internal; typedef QVector ColorTable; ColorTable ColorTableReader::read(String format, int colorCount, dbyte const *colorData) // static { Vector3ui order; Vector3ui bits; parseColorFormat(format, order, bits); ColorTable colors(colorCount); // Already in the format we want? if(8 == bits.x && 8 == bits.y && 8 == bits.z) { // Great! Just copy it as-is. dbyte const *src = colorData; for(int i = 0; i < colorCount; ++i, src += 3) { colors[i] = Vector3ub(src[order.x], src[order.y], src[order.z]); } } else { // Conversion is necessary. dbyte const *src = colorData; dbyte cb = 0; for(int i = 0; i < colorCount; ++i) { Vector3ub &dst = colors[i]; Vector3i tmp; M_ReadBits(bits[order.x], &src, &cb, (dbyte *) &(tmp[order.x])); M_ReadBits(bits[order.y], &src, &cb, (dbyte *) &(tmp[order.y])); M_ReadBits(bits[order.z], &src, &cb, (dbyte *) &(tmp[order.z])); // Need to do any scaling? if(8 != bits.x) { if(bits.x < 8) tmp.x <<= 8 - bits.x; else tmp.x >>= bits.x - 8; } if(8 != bits.y) { if(bits.y < 8) tmp.y <<= 8 - bits.y; else tmp.y >>= bits.y - 8; } if(8 != bits.z) { if(bits.z < 8) tmp.z <<= 8 - bits.z; else tmp.z >>= bits.z - 8; } // Store the final color. dst = Vector3ub(de::clamp(0, tmp.x, 255), de::clamp(0, tmp.y, 255), de::clamp(0, tmp.z, 255)); } } return colors; } #define RGB18(r, g, b) ((r)+((g)<<6)+((b)<<12)) DENG2_PIMPL(ColorPalette) { typedef Vector3ub Color; typedef QVector ColorTable; ColorTable colors; typedef QMap Translations; Translations translations; /// 18-bit to 8-bit, nearest color translation table. typedef QVector XLat18To8; QScopedPointer xlat18To8; bool need18To8Update; Id id; Instance(Public *i) : Base(i) , need18To8Update(false) // No color table yet. { LOG_RES_VERBOSE("New color palette %s") << id; } Translation *translation(String id) { Translations::iterator found = translations.find(id); if(found != translations.end()) { return &found.value(); } return 0; } void notifyColorTableChanged() { DENG2_FOR_PUBLIC_AUDIENCE(ColorTableChange, i) { i->colorPaletteColorTableChanged(self); } } /// @note A time-consuming operation. void prepareNearestLUT() { #define COLORS18BIT 262144 need18To8Update = false; if(xlat18To8.isNull()) { xlat18To8.reset(new XLat18To8(COLORS18BIT)); } for(int r = 0; r < 64; ++r) for(int g = 0; g < 64; ++g) for(int b = 0; b < 64; ++b) { int nearest = 0; int smallestDiff = DDMAXINT; for(int i = 0; i < colors.count(); ++i) { Color const &color = colors[i]; int diff = (color.x - (r << 2)) * (color.x - (r << 2)) + (color.y - (g << 2)) * (color.y - (g << 2)) + (color.z - (b << 2)) * (color.z - (b << 2)); if(diff < smallestDiff) { smallestDiff = diff; nearest = i; } } (*xlat18To8)[RGB18(r, g, b)] = nearest; } #undef COLORS18BIT } }; ColorPalette::ColorPalette() : d(new Instance(this)) {} ColorPalette::ColorPalette(ColorTable const &colors) : d(new Instance(this)) { replaceColorTable(colors); } Id ColorPalette::id() const { return d->id; } int ColorPalette::colorCount() const { return d->colors.count(); } ColorPalette &ColorPalette::replaceColorTable(ColorTable const &colorTable) { LOG_AS("ColorPalette"); int const colorCountBefore = colorCount(); // We may need a new 18 => 8 bit xlat table. d->need18To8Update = true; // Replace the whole color table. d->colors = colorTable; // Notify interested parties. d->notifyColorTableChanged(); // When the color count changes all existing translations are destroyed as // they will no longer be valid. if(colorCountBefore != colorCount()) { clearTranslations(); } return *this; } Vector3ub ColorPalette::color(int colorIndex) const { LOG_AS("ColorPalette"); if(colorIndex < 0 || colorIndex >= d->colors.count()) { LOG_DEBUG("Index %i out of range %s in palette %s, will clamp.") << colorIndex << Rangeui(0, d->colors.count()).asText() << d->id; } if(!d->colors.isEmpty()) { return d->colors[de::clamp(0, colorIndex, d->colors.count() - 1)]; } return Vector3ub(); } Vector3f ColorPalette::colorf(int colorIdx) const { return color(colorIdx).toVector3f() * reciprocal255; } int ColorPalette::nearestIndex(Vector3ub const &rgb) const { LOG_AS("ColorPalette"); if(d->colors.isEmpty()) return -1; // Ensure we've prepared the 18 to 8 table. if(d->need18To8Update || d->xlat18To8.isNull()) { d->prepareNearestLUT(); } return (*d->xlat18To8)[RGB18(rgb.x >> 2, rgb.y >> 2, rgb.z >> 2)]; } void ColorPalette::clearTranslations() { LOG_AS("ColorPalette"); d->translations.clear(); } ColorPalette::Translation const *ColorPalette::translation(String id) const { LOG_AS("ColorPalette"); return d->translation(id); } void ColorPalette::newTranslation(String id, Translation const &mappings) { LOG_AS("ColorPalette"); if(!colorCount()) { //qDebug() << "Cannot define a translation for an empty palette!"; return; } DENG2_ASSERT(mappings.count() == colorCount()); // sanity check if(!id.isEmpty()) { Translation *xlat = d->translation(id); if(!xlat) { // An entirely new translation. xlat = &d->translations.insert(id, Translation()).value(); } // Replace the whole mapping table. *xlat = mappings; // Ensure the mappings are within valid range. for(int i = 0; i < colorCount(); ++i) { int palIdx = (*xlat)[i]; if(palIdx < 0 || palIdx >= colorCount()) { (*xlat)[i] = i; } } return; } /// @throw InvalidTranslationIdError . throw InvalidTranslationIdError("ColorPalette::newTranslation", "A zero-length id was specified"); } doomsday-stable-1.15.7/doomsday/client/src/resource/tga.cpp0000664000175000017500000002765112641367670023201 0ustar jaakkojaakko/** @file tga.cpp Truevision TGA (a.k.a Targa) image reader/writer * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "dd_share.h" // endianness conversion macros #include "resource/tga.h" #include #include #include #include using namespace de; enum { TGA_FALSE, TGA_TRUE, TGA_TARGA24, // rgb888 TGA_TARGA32 // rgba8888 }; #undef SHORT #ifdef __BIG_ENDIAN__ #define SHORT(x) shortSwap(x) # else // Little-endian. #define SHORT(x) (x) #endif #pragma pack(1) typedef struct { uint8_t idLength; // Identification field size in bytes. uint8_t colorMapType; // Type of the color map. uint8_t imageType; // Image type code. } tga_header_t; // Color map specification. typedef struct { int16_t index; // Index of first color map entry. int16_t length; // Number of color map entries. uint8_t entrySize; // Number of bits in a color map entry (16/24/32). } tga_colormapspec_t; // Image specification flags: #define ISF_SCREEN_ORIGIN_UPPER 0x1 // Upper left-hand corner screen origin. // Data interleaving: #define ISF_INTERLEAVE_TWOWAY 0x2 // Two-way (even/odd) interleaving. #define ISF_INTERLEAVE_FOURWAY 0x4 // Four-way interleaving. // Image specification. typedef struct { uint8_t flags; int16_t xOrigin; // X coordinate of lower left corner. int16_t yOrigin; // Y coordinate of lower left corner. int16_t width; // Width of the image in pixels. int16_t height; // Height of the image in pixels. uint8_t pixelDepth; // Number of bits in a pixel (16/24/32). uint8_t attributeBits; } tga_imagespec_t; #pragma pack() static char *lastErrorMsg = 0; /// @todo potentially never free'd #ifdef __BIG_ENDIAN__ static int16_t shortSwap(int16_t n) { return ((n & 0xff) << 8) | ((n & 0xff00) >> 8); } #endif static void setLastError(char const *msg) { size_t len; if(0 == msg || 0 == (len = strlen(msg))) { if(lastErrorMsg != 0) { M_Free(lastErrorMsg); } lastErrorMsg = 0; return; } lastErrorMsg = (char *) M_Realloc(lastErrorMsg, len+1); strcpy(lastErrorMsg, msg); } static void writeByte(FILE *f, uint8_t b) { fwrite(&b, 1, 1, f); } static void writeShort(FILE *f, int16_t s) { int16_t v = SHORT(s); fwrite(&v, sizeof(v), 1, f); } static uint8_t readByte(FileHandle &f) { uint8_t v; f.read(&v, 1); return v; } static int16_t readShort(FileHandle &f) { int16_t v; f.read((uint8_t *)&v, sizeof(v)); return SHORT(v); } /** * @param idLength Identification field size in bytes (max 255). * @c 0 indicates no identification field. * @param colorMapType Type of the color map, @c 0 or @c 1: * @c 0 = color map data is not present. * @c 1 = color map data IS present. * @param imageType Image data type code, one of: * @c 0 = no image data is present. * @c 1 = uncompressed, color mapped image. * @c 2 = uncompressed, true-color image. * @c 3 = uncompressed, grayscale image. * @c 9 = run-length encoded, color mapped image. * @c 10 = run-length encoded, true-color image. * @c 11 = run-length encoded, grayscale image. * @param file Handle to the file to be written to. */ static void writeHeader(uint8_t idLength, uint8_t colorMapType, uint8_t imageType, FILE *file) { writeByte(file, idLength); writeByte(file, colorMapType? 1 : 0); writeByte(file, imageType); } static void readHeader(tga_header_t *dst, FileHandle &file) { dst->idLength = readByte(file); dst->colorMapType = readByte(file); dst->imageType = readByte(file); } /** * @param index Index of first color map entry. * @param length Total number of color map entries. * @param entrySize Number of bits in a color map entry; 15/16/24/32. * @param file Handle to the file to be written to. */ static void writeColorMapSpec(int16_t index, int16_t length, uint8_t entrySize, FILE *file) { writeShort(file, index); writeShort(file, length); writeByte(file, entrySize); } static void readColorMapSpec(tga_colormapspec_t *dst, FileHandle &file) { dst->index = readShort(file); dst->length = readShort(file); dst->entrySize = readByte(file); } /** * @param xOrigin X coordinate of lower left corner. * @param yOrigin Y coordinate of lower left corner. * @param width Width of the image in pixels. * @param height Height of the image in pixels. * @param pixDepth Number of bits per pixel, one of; 16/24/32. * @param file Handle to the file to be written to. */ static void writeImageSpec(int16_t xOrigin, int16_t yOrigin, int16_t width, int16_t height, uint8_t pixDepth, FILE *file) { writeShort(file, xOrigin); writeShort(file, yOrigin); writeShort(file, width); writeShort(file, height); writeByte(file, pixDepth); /* * attributeBits:4; // Attribute bits associated with each pixel. * reserved:1; // A reserved bit; must be 0. * screenOrigin:1; // Location of screen origin; must be 0. * dataInterleave:2; // TGA_INTERLEAVE_* */ writeByte(file, 0); } static void readImageSpec(tga_imagespec_t *dst, FileHandle &file) { dst->xOrigin = readShort(file); dst->yOrigin = readShort(file); dst->width = readShort(file); dst->height = readShort(file); dst->pixelDepth = readByte(file); uint8_t bits = readByte(file); /* * attributeBits:4; // Attribute bits associated with each pixel. * reserved:1; // A reserved bit; must be 0. * screenOrigin:1; // Location of screen origin; must be 0. * dataInterleave:2; // TGA_INTERLEAVE_* */ dst->flags = 0 | ((bits & 6)? ISF_SCREEN_ORIGIN_UPPER : 0) | ((bits & 7)? ISF_INTERLEAVE_TWOWAY : (bits & 8)? ISF_INTERLEAVE_FOURWAY : 0); dst->attributeBits = (bits & 0xf); } int TGA_Save24_rgb565(FILE *file, int w, int h, const uint16_t *buf) { int i, k; uint8_t *outBuf; size_t outBufStart; if(NULL == file || !buf || !(w > 0 && h > 0)) return 0; // No identification field, no color map, Targa type 2 (unmapped RGB). writeHeader(0, 0, 2, file); writeColorMapSpec(0, 0, 0, file); writeImageSpec(0, 0, w, h, 24, file); /** * Convert the buffer: * From RRRRRGGGGGGBBBBB > ARRRRRGGGGGBBBBB * * \note Alpha will always be @c 0. */ outBuf = (uint8_t *) M_Malloc(w * h * 3); outBufStart = w * h - 1; // From the end. for(k = 0; k < h; ++k) for(i = 0; i < w; ++i) { int16_t r, g, b; const int16_t src = buf[k * w + i]; uint8_t* dst = &outBuf[outBufStart - (((k + 1) * w - 1 - i)) * 3]; r = (src >> 11) & 0x1f; // The last 5 bits. g = (src >> 5) & 0x3f; // The middle 6 bits (one bit'll be lost). b = src & 0x1f; // The first 5 bits. dst[2] = b << 3; dst[0] = g << 2; dst[1] = r << 3; } // Write the converted buffer (bytes may go the wrong way around...!). fwrite(outBuf, w * h * 3, 1, file); free(outBuf); return 1; // Success. } const char* TGA_LastError(void) { if(lastErrorMsg) return lastErrorMsg; return 0; } int TGA_Save24_rgb888(FILE* file, int w, int h, const uint8_t* buf) { uint8_t* outBuf; int i; if(NULL == file || !buf || !(w > 0 && h > 0)) return 0; // No identification field, no color map, Targa type 2 (unmapped RGB). writeHeader(0, 0, 2, file); writeColorMapSpec(0, 0, 0, file); writeImageSpec(0, 0, w, h, 24, file); // The save format is BGR. outBuf = (uint8_t *) M_Malloc(w * h * 3); for(i = 0; i < w * h; ++i) { const uint8_t* src = &buf[i * 3]; uint8_t* dst = &outBuf[i * 3]; dst[0] = src[2]; dst[1] = src[1]; dst[2] = src[0]; } fwrite(outBuf, w * h * 3, 1, file); free(outBuf); return 1; // Success. } int TGA_Save24_rgba8888(FILE* file, int w, int h, const uint8_t* buf) { uint8_t* outBuf; int i; if(NULL == file || !buf || !(w > 0 && h > 0)) return 0; // No identification field, no color map, Targa type 2 (unmapped RGB). writeHeader(0, 0, 2, file); writeColorMapSpec(0, 0, 0, file); writeImageSpec(0, 0, w, h, 24, file); // The save format is BGR. outBuf = (uint8_t *) M_Malloc(w * h * 3); for(i = 0; i < w * h; ++i) { const uint8_t* src = &buf[i * 4]; uint8_t* dst = &outBuf[i * 3]; dst[0] = src[2]; dst[1] = src[1]; dst[2] = src[0]; } fwrite(outBuf, w * h * 3, 1, file); free(outBuf); return 1; // Success. } int TGA_Save16_rgb888(FILE* file, int w, int h, const uint8_t* buf) { int16_t* outBuf; int i; if(NULL == file || !buf || !(w > 0 && h > 0)) return 0; // No identification field, no color map, Targa type 2 (unmapped RGB). writeHeader(0, 0, 2, file); writeColorMapSpec(0, 0, 0, file); writeImageSpec(0, 0, w, h, 16, file); // The destination format is _RRRRRGG GGGBBBBB. outBuf = (int16_t *) M_Malloc(w * h * 2); for(i = 0; i < w * h; ++i) { const uint8_t* src = &buf[i * 3]; int16_t* dst = &outBuf[i]; *dst = (src[2] >> 3) + ((src[1] & 0xf8) << 2) + ((src[0] & 0xf8) << 7); } fwrite(outBuf, w * h * 2, 1, file); free(outBuf); return 1; // Success. } uint8_t *TGA_Load(FileHandle &file, Vector2ui &outSize, int &pixelSize) { uint8_t *dstBuf = 0; size_t const initPos = file.tell(); tga_header_t header; readHeader(&header, file); tga_colormapspec_t colorMapSpec; readColorMapSpec(&colorMapSpec, file); tga_imagespec_t imageSpec; readImageSpec(&imageSpec, file); outSize = Vector2ui(imageSpec.width, imageSpec.height); if(header.imageType != 2 || (imageSpec.pixelDepth != 32 && imageSpec.pixelDepth != 24) || (imageSpec.attributeBits != 8 && imageSpec.attributeBits != 0) || (imageSpec.flags & ISF_SCREEN_ORIGIN_UPPER)) { setLastError("Unsupported format."); file.seek(initPos, SeekSet); return 0; } // Determine format. //int const format = (imageSpec.pixelDepth == 24? TGA_TARGA24 : TGA_TARGA32); pixelSize = (imageSpec.pixelDepth == 24? 3 : 4); // Read the pixel data. int const numPels = outSize.x * outSize.y; uint8_t *srcBuf = (uint8_t *) M_Malloc(numPels * pixelSize); file.read(srcBuf, numPels * pixelSize); // "Unpack" the pixels (origin in the lower left corner). // TGA pixels are in BGRA format. dstBuf = (uint8_t *) M_Malloc(4 * numPels); uint8_t const *src = srcBuf; for(int y = outSize.y - 1; y >= 0; y--) for(int x = 0; x < (signed) outSize.x; x++) { uint8_t *dst = &dstBuf[(y * outSize.x + x) * pixelSize]; dst[2] = *src++; dst[1] = *src++; dst[0] = *src++; if(pixelSize == 4) dst[3] = *src++; } M_Free(srcBuf); setLastError(0); // Success. file.seek(initPos, SeekSet); return dstBuf; } doomsday-stable-1.15.7/doomsday/client/src/resource/texturescheme.cpp0000664000175000017500000002311312641367670025300 0ustar jaakkojaakko/** @file texturescheme.cpp Texture system subspace scheme. * * @authors Copyright © 2010-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "resource/texturescheme.h" #include "dd_main.h" // App_ResourceSystem() #include "TextureManifest" using namespace de; DENG2_PIMPL(TextureScheme), DENG2_OBSERVES(TextureManifest, UniqueIdChange), DENG2_OBSERVES(TextureManifest, Deletion) { /// Symbolic name of the scheme. String name; /// Mappings from paths to manifests. TextureScheme::Index index; /// LUT which translates scheme-unique-ids to their associated manifest (if any). /// Index with uniqueId - uniqueIdBase. QList uniqueIdLut; bool uniqueIdLutDirty; int uniqueIdBase; Instance(Public *i, String symbolicName) : Base(i), name(symbolicName), uniqueIdLut(), uniqueIdLutDirty(false), uniqueIdBase(0) {} ~Instance() { self.clear(); DENG_ASSERT(index.isEmpty()); } bool inline uniqueIdInLutRange(int uniqueId) const { return (uniqueId - uniqueIdBase >= 0 && (uniqueId - uniqueIdBase) < uniqueIdLut.size()); } void findUniqueIdRange(int *minId, int *maxId) { if(!minId && !maxId) return; if(minId) *minId = DDMAXINT; if(maxId) *maxId = DDMININT; PathTreeIterator iter(index.leafNodes()); while(iter.hasNext()) { TextureManifest &manifest = iter.next(); int const uniqueId = manifest.uniqueId(); if(minId && uniqueId < *minId) *minId = uniqueId; if(maxId && uniqueId > *maxId) *maxId = uniqueId; } } void deindex(TextureManifest &manifest) { /// @todo Only destroy the texture if this is the last remaining reference. manifest.clearTexture(); unlinkInUniqueIdLut(manifest); } /// @pre uniqueIdLut is large enough if initialized! void unlinkInUniqueIdLut(TextureManifest &manifest) { // If the lut is already considered 'dirty' do not unlink. if(!uniqueIdLutDirty) { int uniqueId = manifest.uniqueId(); DENG_ASSERT(uniqueIdInLutRange(uniqueId)); uniqueIdLut[uniqueId - uniqueIdBase] = 0; } } /// @pre uniqueIdLut has been initialized and is large enough! void linkInUniqueIdLut(TextureManifest &manifest) { int uniqueId = manifest.uniqueId(); DENG_ASSERT(uniqueIdInLutRange(uniqueId)); uniqueIdLut[uniqueId - uniqueIdBase] = &manifest; } void rebuildUniqueIdLut() { // Is a rebuild necessary? if(!uniqueIdLutDirty) return; // Determine the size of the LUT. int minId, maxId; findUniqueIdRange(&minId, &maxId); int lutSize = 0; if(minId > maxId) // None found? { uniqueIdBase = 0; } else { uniqueIdBase = minId; lutSize = maxId - minId + 1; } // Fill the LUT with initial values. #ifdef DENG2_QT_4_7_OR_NEWER uniqueIdLut.reserve(lutSize); #endif int i = 0; for(; i < uniqueIdLut.size(); ++i) { uniqueIdLut[i] = 0; } for(; i < lutSize; ++i) { uniqueIdLut.push_back(0); } if(lutSize) { // Populate the LUT. PathTreeIterator iter(index.leafNodes()); while(iter.hasNext()) { linkInUniqueIdLut(iter.next()); } } uniqueIdLutDirty = false; } // Observes TextureManifest UniqueIdChange void textureManifestUniqueIdChanged(Manifest & /*manifest*/) { // We'll need to rebuild the id map. uniqueIdLutDirty = true; } // Observes TextureManifest Deletion. void textureManifestBeingDeleted(TextureManifest const &manifest) { deindex(const_cast(manifest)); } }; TextureScheme::TextureScheme(String symbolicName) : d(new Instance(this, symbolicName)) {} TextureScheme::~TextureScheme() { clear(); } void TextureScheme::clear() { /*PathTreeIterator iter(d->index.leafNodes()); while(iter.hasNext()) { d->deindex(iter.next()); }*/ d->index.clear(); d->uniqueIdLutDirty = true; } String const &TextureScheme::name() const { return d->name; } TextureManifest &TextureScheme::declare(Path const &path, Texture::Flags flags, Vector2i const &dimensions, Vector2i const &origin, int uniqueId, de::Uri const *resourceUri) { LOG_AS("TextureScheme::declare"); if(path.isEmpty()) { /// @throw InvalidPathError An empty path was specified. throw InvalidPathError("TextureScheme::declare", "Missing/zero-length path was supplied"); } int const sizeBefore = d->index.size(); Manifest *newManifest = &d->index.insert(path); DENG2_ASSERT(newManifest); if(d->index.size() != sizeBefore) { // We'll need to rebuild the unique id LUT after this (deferred for perf). d->uniqueIdLutDirty = true; // We want notification if/when the manifest's uniqueId changes. newManifest->audienceForUniqueIdChange += d; // We want notification when the manifest is about to be deleted. newManifest->audienceForDeletion += d; // Notify interested parties that a new manifest was defined in the scheme. DENG2_FOR_AUDIENCE(ManifestDefined, i) i->textureSchemeManifestDefined(*this, *newManifest); } /* * (Re)configure the manifest. */ bool mustRelease = false; newManifest->setFlags(flags); newManifest->setOrigin(origin); if(newManifest->setLogicalDimensions(dimensions)) { mustRelease = true; } // We don't care whether these identfiers are truely unique. Our only // responsibility is to release textures when they change. if(newManifest->setUniqueId(uniqueId)) { mustRelease = true; } if(resourceUri && newManifest->setResourceUri(*resourceUri)) { // The mapped resource is being replaced, so release any existing Texture. /// @todo Only release if this Texture is bound to only this binding. mustRelease = true; } if(mustRelease && newManifest->hasTexture()) { #ifdef __CLIENT__ /// @todo Update any Materials (and thus Surfaces) which reference this. newManifest->texture().releaseGLTextures(); #endif } return *newManifest; } bool TextureScheme::has(Path const &path) const { return d->index.has(path, Index::NoBranch | Index::MatchFull); } TextureManifest const &TextureScheme::find(Path const &path) const { if(has(path)) { return d->index.find(path, Index::NoBranch | Index::MatchFull); } /// @throw NotFoundError Failed to locate a matching manifest. throw NotFoundError("TextureScheme::find", "Failed to locate a manifest matching \"" + path.asText() + "\""); } TextureManifest &TextureScheme::find(Path const &path) { TextureManifest const &found = const_cast(this)->find(path); return const_cast(found); } TextureManifest const &TextureScheme::findByResourceUri(de::Uri const &uri) const { if(!uri.isEmpty()) { PathTreeIterator iter(d->index.leafNodes()); while(iter.hasNext()) { TextureManifest &manifest = iter.next(); if(manifest.hasResourceUri()) { if(manifest.resourceUri() == uri) { return manifest; } } } } /// @throw NotFoundError No manifest was found with a matching resource URI. throw NotFoundError("TextureScheme::findByResourceUri", "No manifest found with a resource URI matching \"" + uri.asText() + "\""); } TextureManifest &TextureScheme::findByResourceUri(de::Uri const &uri) { TextureManifest const &found = const_cast(this)->findByResourceUri(uri); return const_cast(found); } TextureManifest const &TextureScheme::findByUniqueId(int uniqueId) const { d->rebuildUniqueIdLut(); if(d->uniqueIdInLutRange(uniqueId)) { TextureManifest *manifest = d->uniqueIdLut[uniqueId - d->uniqueIdBase]; if(manifest) return *manifest; } /// @throw NotFoundError No manifest was found with a matching resource URI. throw NotFoundError("TextureScheme::findByUniqueId", "No manifest found with a unique ID matching \"" + QString("%1").arg(uniqueId) + "\""); } TextureManifest &TextureScheme::findByUniqueId(int uniqueId) { TextureManifest const &found = const_cast(this)->findByUniqueId(uniqueId); return const_cast(found); } TextureScheme::Index const &TextureScheme::index() const { return d->index; } doomsday-stable-1.15.7/doomsday/client/src/resource/model.cpp0000664000175000017500000006040212641367670023515 0ustar jaakkojaakko/** @file model.cpp 3D model resource. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "resource/model.h" #include "Texture" #include "TextureManifest" #include #include #include #include using namespace de; bool Model::DetailLevel::hasVertex(int number) const { return model.lodVertexUsage().testBit(number * model.lodCount() + level); } void Model::Frame::bounds(Vector3f &retMin, Vector3f &retMax) const { retMin = min; retMax = max; } float Model::Frame::horizontalRange(float *top, float *bottom) const { *top = max.y; *bottom = min.y; return max.y - min.y; } // #define MD2_MAGIC 0x32504449 #pragma pack(1) struct md2_header_t { int magic; int version; int skinWidth; int skinHeight; int frameSize; int numSkins; int numVertices; int numTexCoords; int numTriangles; int numGlCommands; int numFrames; int offsetSkins; int offsetTexCoords; int offsetTriangles; int offsetFrames; int offsetGlCommands; int offsetEnd; }; #pragma pack() static bool readMd2Header(FileHandle &file, md2_header_t &hdr) { size_t readBytes = file.read((uint8_t *)&hdr, sizeof(md2_header_t)); if(readBytes < sizeof(md2_header_t)) return false; hdr.magic = littleEndianByteOrder.toNative(hdr.magic); hdr.version = littleEndianByteOrder.toNative(hdr.version); hdr.skinWidth = littleEndianByteOrder.toNative(hdr.skinWidth); hdr.skinHeight = littleEndianByteOrder.toNative(hdr.skinHeight); hdr.frameSize = littleEndianByteOrder.toNative(hdr.frameSize); hdr.numSkins = littleEndianByteOrder.toNative(hdr.numSkins); hdr.numVertices = littleEndianByteOrder.toNative(hdr.numVertices); hdr.numTexCoords = littleEndianByteOrder.toNative(hdr.numTexCoords); hdr.numTriangles = littleEndianByteOrder.toNative(hdr.numTriangles); hdr.numGlCommands = littleEndianByteOrder.toNative(hdr.numGlCommands); hdr.numFrames = littleEndianByteOrder.toNative(hdr.numFrames); hdr.offsetSkins = littleEndianByteOrder.toNative(hdr.offsetSkins); hdr.offsetTexCoords = littleEndianByteOrder.toNative(hdr.offsetTexCoords); hdr.offsetTriangles = littleEndianByteOrder.toNative(hdr.offsetTriangles); hdr.offsetFrames = littleEndianByteOrder.toNative(hdr.offsetFrames); hdr.offsetGlCommands = littleEndianByteOrder.toNative(hdr.offsetGlCommands); hdr.offsetEnd = littleEndianByteOrder.toNative(hdr.offsetEnd); return true; } // #define DMD_MAGIC 0x4D444D44 ///< "DMDM" = Doomsday/Detailed MoDel Magic #pragma pack(1) struct dmd_header_t { int magic; int version; int flags; }; #pragma pack() static bool readHeaderDmd(FileHandle &file, dmd_header_t &hdr) { size_t readBytes = file.read((uint8_t *)&hdr, sizeof(dmd_header_t)); if(readBytes < sizeof(dmd_header_t)) return false; hdr.magic = littleEndianByteOrder.toNative(hdr.magic); hdr.version = littleEndianByteOrder.toNative(hdr.version); hdr.flags = littleEndianByteOrder.toNative(hdr.flags); return true; } static void *allocAndLoad(FileHandle &file, int offset, int len) { uint8_t *ptr = (uint8_t *) M_Malloc(len); file.seek(offset, SeekSet); file.read(ptr, len); return ptr; } // Precalculated normal LUT for use when loading MD2/DMD format models. #define NUMVERTEXNORMALS 162 static float avertexnormals[NUMVERTEXNORMALS][3] = { #include "tab_anorms.h" }; /** * @todo reimplement file loading using de::Reader. */ DENG2_PIMPL(Model) { Flags flags; Skins skins; Frames frames; int numVertices; DetailLevels lods; QBitArray lodVertexUsage; uint modelId; ///< In the repository. Instance(Public *i) : Base(i) , numVertices(0) , modelId(0) {} ~Instance() { self.clearAllFrames(); } #pragma pack(1) struct md2_triangleVertex_t { byte vertex[3]; byte normalIndex; }; struct md2_packedFrame_t { float scale[3]; float translate[3]; char name[16]; md2_triangleVertex_t vertices[1]; }; struct md2_commandElement_t { float s, t; int index; }; #pragma pack() /** * Note vertex Z/Y are swapped here (ordered XYZ in the serialized data). */ static Model *loadMd2(FileHandle &file, float aspectScale) { // Determine whether this appears to be a MD2 model. md2_header_t hdr; if(!readMd2Header(file, hdr)) return 0; if(hdr.magic != MD2_MAGIC) return 0; Model *mdl = new Model; mdl->d->numVertices = hdr.numVertices; // Load and convert to DMD. uint8_t *frameData = (uint8_t *) allocAndLoad(file, hdr.offsetFrames, hdr.frameSize * hdr.numFrames); for(int i = 0; i < hdr.numFrames; ++i) { md2_packedFrame_t const *pfr = (md2_packedFrame_t const *) (frameData + hdr.frameSize * i); Vector3f const scale(FLOAT(pfr->scale[0]), FLOAT(pfr->scale[2]), FLOAT(pfr->scale[1])); Vector3f const translation(FLOAT(pfr->translate[0]), FLOAT(pfr->translate[2]), FLOAT(pfr->translate[1])); String const frameName = pfr->name; ModelFrame *frame = new ModelFrame(*mdl, frameName); frame->vertices.reserve(hdr.numVertices); // Scale and translate each vertex. md2_triangleVertex_t const *pVtx = pfr->vertices; for(int k = 0; k < hdr.numVertices; ++k, pVtx++) { frame->vertices.append(ModelFrame::Vertex()); ModelFrame::Vertex &vtx = frame->vertices.last(); vtx.pos = Vector3f(pVtx->vertex[0], pVtx->vertex[2], pVtx->vertex[1]) * scale + translation; vtx.pos.y *= aspectScale; // Aspect undoing. vtx.norm = Vector3f(avertexnormals[pVtx->normalIndex]); if(!k) { frame->min = frame->max = vtx.pos; } else { frame->min = vtx.pos.min(frame->min); frame->max = vtx.pos.max(frame->max); } } mdl->d->frames.append(frame); } M_Free(frameData); mdl->d->lods.append(new ModelDetailLevel(*mdl, 0)); ModelDetailLevel &lod0 = *mdl->d->lods.last(); uint8_t *commandData = (uint8_t *) allocAndLoad(file, hdr.offsetGlCommands, 4 * hdr.numGlCommands); for(uint8_t const *pos = commandData; *pos;) { int count = LONG( *(int *) pos ); pos += 4; lod0.primitives.append(Model::Primitive()); Model::Primitive &prim = lod0.primitives.last(); // The type of primitive depends on the sign. prim.triFan = (count < 0); if(count < 0) { count = -count; } while(count--) { md2_commandElement_t const *v = (md2_commandElement_t *) pos; pos += 12; prim.elements.append(Model::Primitive::Element()); Model::Primitive::Element &elem = prim.elements.last(); elem.texCoord = Vector2f(FLOAT(v->s), FLOAT(v->t)); elem.index = LONG(v->index); } } M_Free(commandData); // Load skins. (Note: numSkins may be zero.) file.seek(hdr.offsetSkins, SeekSet); for(int i = 0; i < hdr.numSkins; ++i) { char name[64]; file.read((uint8_t *)name, 64); mdl->newSkin(name); } return mdl; } // DMD chunk types. enum { DMC_END, /// Must be the last chunk. DMC_INFO /// Required; will be expected to exist. }; #pragma pack(1) struct dmd_chunk_t { int type; int length; /// Next chunk follows... }; struct dmd_info_t { int skinWidth; int skinHeight; int frameSize; int numSkins; int numVertices; int numTexCoords; int numFrames; int numLODs; int offsetSkins; int offsetTexCoords; int offsetFrames; int offsetLODs; int offsetEnd; }; struct dmd_levelOfDetail_t { int numTriangles; int numGlCommands; int offsetTriangles; int offsetGlCommands; }; struct dmd_packedVertex_t { byte vertex[3]; unsigned short normal; /// Yaw and pitch. }; struct dmd_packedFrame_t { float scale[3]; float translate[3]; char name[16]; dmd_packedVertex_t vertices[1]; // dmd_info_t::numVertices size }; struct dmd_triangle_t { short vertexIndices[3]; short textureIndices[3]; }; #pragma pack() /** * Packed: pppppppy yyyyyyyy. Yaw is on the XY plane. */ static Vector3f unpackVector(ushort packed) { float const yaw = (packed & 511) / 512.0f * 2 * PI; float const pitch = ((packed >> 9) / 127.0f - 0.5f) * PI; float const cosp = float(cos(pitch)); return Vector3f(cos(yaw) * cosp, sin(yaw) * cosp, sin(pitch)); } /** * Note vertex Z/Y are swapped here (ordered XYZ in the serialized data). */ static Model *loadDmd(FileHandle &file, float aspectScale) { // Determine whether this appears to be a DMD model. dmd_header_t hdr; if(!readHeaderDmd(file, hdr)) return 0; if(hdr.magic != DMD_MAGIC) return 0; // Read the chunks. dmd_chunk_t chunk; file.read((uint8_t *)&chunk, sizeof(chunk)); dmd_info_t info; zap(info); while(LONG(chunk.type) != DMC_END) { switch(LONG(chunk.type)) { case DMC_INFO: // Standard DMD information chunk. file.read((uint8_t *)&info, LONG(chunk.length)); info.skinWidth = LONG(info.skinWidth); info.skinHeight = LONG(info.skinHeight); info.frameSize = LONG(info.frameSize); info.numSkins = LONG(info.numSkins); info.numVertices = LONG(info.numVertices); info.numTexCoords = LONG(info.numTexCoords); info.numFrames = LONG(info.numFrames); info.numLODs = LONG(info.numLODs); info.offsetSkins = LONG(info.offsetSkins); info.offsetTexCoords = LONG(info.offsetTexCoords); info.offsetFrames = LONG(info.offsetFrames); info.offsetLODs = LONG(info.offsetLODs); info.offsetEnd = LONG(info.offsetEnd); break; default: // Skip unknown chunks. file.seek(LONG(chunk.length), SeekCur); break; } // Read the next chunk header. file.read((uint8_t *)&chunk, sizeof(chunk)); } Model *mdl = new Model; mdl->d->numVertices = info.numVertices; // Allocate and load in the data. (Note: numSkins may be zero.) file.seek(info.offsetSkins, SeekSet); for(int i = 0; i < info.numSkins; ++i) { char name[64]; file.read((uint8_t *)name, 64); mdl->newSkin(name); } uint8_t *frameData = (uint8_t *) allocAndLoad(file, info.offsetFrames, info.frameSize * info.numFrames); for(int i = 0; i < info.numFrames; ++i) { dmd_packedFrame_t const *pfr = (dmd_packedFrame_t *) (frameData + info.frameSize * i); Vector3f const scale(FLOAT(pfr->scale[0]), FLOAT(pfr->scale[2]), FLOAT(pfr->scale[1])); Vector3f const translation(FLOAT(pfr->translate[0]), FLOAT(pfr->translate[2]), FLOAT(pfr->translate[1])); String const frameName = pfr->name; Frame *frame = new Frame(*mdl, frameName); frame->vertices.reserve(info.numVertices); // Scale and translate each vertex. dmd_packedVertex_t const *pVtx = pfr->vertices; for(int k = 0; k < info.numVertices; ++k, ++pVtx) { frame->vertices.append(Frame::Vertex()); Frame::Vertex &vtx = frame->vertices.last(); vtx.pos = Vector3f(pVtx->vertex[0], pVtx->vertex[2], pVtx->vertex[1]) * scale + translation; vtx.pos.y *= aspectScale; // Aspect undo. vtx.norm = unpackVector(USHORT(pVtx->normal)); if(!k) { frame->min = frame->max = vtx.pos; } else { frame->min = vtx.pos.min(frame->min); frame->max = vtx.pos.max(frame->max); } } mdl->d->frames.append(frame); } M_Free(frameData); file.seek(info.offsetLODs, SeekSet); dmd_levelOfDetail_t *lodInfo = new dmd_levelOfDetail_t[info.numLODs]; for(int i = 0; i < info.numLODs; ++i) { file.read((uint8_t *)&lodInfo[i], sizeof(dmd_levelOfDetail_t)); lodInfo[i].numTriangles = LONG(lodInfo[i].numTriangles); lodInfo[i].numGlCommands = LONG(lodInfo[i].numGlCommands); lodInfo[i].offsetTriangles = LONG(lodInfo[i].offsetTriangles); lodInfo[i].offsetGlCommands = LONG(lodInfo[i].offsetGlCommands); } dmd_triangle_t **triangles = new dmd_triangle_t*[info.numLODs]; for(int i = 0; i < info.numLODs; ++i) { mdl->d->lods.append(new DetailLevel(*mdl, i)); DetailLevel &lod = *mdl->d->lods.last(); triangles[i] = (dmd_triangle_t *) allocAndLoad(file, lodInfo[i].offsetTriangles, sizeof(dmd_triangle_t) * lodInfo[i].numTriangles); uint8_t *commandData = (uint8_t *) allocAndLoad(file, lodInfo[i].offsetGlCommands, 4 * lodInfo[i].numGlCommands); for(uint8_t const *pos = commandData; *pos;) { int count = LONG( *(int *) pos ); pos += 4; lod.primitives.append(Primitive()); Primitive &prim = lod.primitives.last(); // The type of primitive depends on the sign of the element count. prim.triFan = (count < 0); if(count < 0) { count = -count; } while(count--) { md2_commandElement_t const *v = (md2_commandElement_t *) pos; pos += 12; prim.elements.append(Primitive::Element()); Primitive::Element &elem = prim.elements.last(); elem.texCoord = Vector2f(FLOAT(v->s), FLOAT(v->t)); elem.index = LONG(v->index); } } M_Free(commandData); } // Determine vertex usage at each LOD level. mdl->d->lodVertexUsage.resize(info.numVertices * info.numLODs); mdl->d->lodVertexUsage.fill(false); for(int i = 0; i < info.numLODs; ++i) for(int k = 0; k < lodInfo[i].numTriangles; ++k) for(int m = 0; m < 3; ++m) { int vertexIndex = SHORT(triangles[i][k].vertexIndices[m]); mdl->d->lodVertexUsage.setBit(vertexIndex * info.numLODs + i); } delete [] lodInfo; for(int i = 0; i < info.numLODs; ++i) { M_Free(triangles[i]); } delete [] triangles; return mdl; } #if 0 /** * Calculate vertex normals. Only with -renorm. */ static void rebuildNormals(model_t &mdl) { // Renormalizing? if(!CommandLine_Check("-renorm")) return; int const tris = mdl.lodInfo[0].numTriangles; int const verts = mdl.info.numVertices; vector_t* normals = (vector_t*) Z_Malloc(sizeof(vector_t) * tris, PU_APPSTATIC, 0); vector_t norm; int cnt; // Calculate the normal for each vertex. for(int i = 0; i < mdl.info.numFrames; ++i) { model_vertex_t* list = mdl.frames[i].vertices; for(int k = 0; k < tris; ++k) { dmd_triangle_t const& tri = mdl.lods[0].triangles[k]; // First calculate surface normals, combine them to vertex ones. V3f_PointCrossProduct(normals[k].pos, list[tri.vertexIndices[0]].vertex, list[tri.vertexIndices[2]].vertex, list[tri.vertexIndices[1]].vertex); V3f_Normalize(normals[k].pos); } for(int k = 0; k < verts; ++k) { memset(&norm, 0, sizeof(norm)); cnt = 0; for(int j = 0; j < tris; ++j) { dmd_triangle_t const& tri = mdl.lods[0].triangles[j]; for(int n = 0; n < 3; ++n) { if(tri.vertexIndices[n] == k) { cnt++; for(int n = 0; n < 3; ++n) { norm.pos[n] += normals[j].pos[n]; } break; } } } if(!cnt) continue; // Impossible... // Calculate the average. for(int n = 0; n < 3; ++n) { norm.pos[n] /= cnt; } // Normalize it. V3f_Normalize(norm.pos); memcpy(list[k].normal, norm.pos, sizeof(norm.pos)); } } Z_Free(normals); } #endif }; Model::Model(Flags flags) : d(new Instance(this)) { setFlags(flags, de::ReplaceFlags); } static bool recogniseDmd(FileHandle &file) { dmd_header_t hdr; size_t initPos = file.tell(); // Seek to the start of the header. file.seek(0, SeekSet); bool result = (readHeaderDmd(file, hdr) && LONG(hdr.magic) == DMD_MAGIC); // Return the stream to its original position. file.seek(initPos, SeekSet); return result; } static bool recogniseMd2(FileHandle &file) { md2_header_t hdr; size_t initPos = file.tell(); // Seek to the start of the header. file.seek(0, SeekSet); bool result = (readMd2Header(file, hdr) && LONG(hdr.magic) == MD2_MAGIC); // Return the stream to its original position. file.seek(initPos, SeekSet); return result; } bool Model::recognise(FileHandle &hndl) //static { LOG_AS("Model"); if(recogniseDmd(hndl)) return true; if(recogniseMd2(hndl)) return true; return false; } struct ModelFileType { String name; ///< Symbolic name of the resource type. String ext; ///< Known file extension. Model *(*loadFunc)(FileHandle &hndl, float aspectScale); }; Model *Model::loadFromFile(FileHandle &hndl, float aspectScale) //static { LOG_AS("Model"); // Recognized file types. static ModelFileType modelTypes[] = { { "DMD", ".dmd", Instance::loadDmd }, { "MD2", ".md2", Instance::loadMd2 }, { "", "", 0 } // Terminate. }; // Firstly, attempt to guess the resource type from the file extension. ModelFileType *rtypeGuess = 0; String filePath = hndl.file().composePath(); String ext = filePath.fileNameExtension(); if(!ext.isEmpty()) { for(int i = 0; !modelTypes[i].name.isEmpty(); ++i) { ModelFileType &rtype = modelTypes[i]; if(!rtype.ext.compareWithoutCase(ext)) { rtypeGuess = &rtype; if(Model *mdl = rtype.loadFunc(hndl, aspectScale)) { LOG_RES_VERBOSE("Interpreted \"" + NativePath(filePath).pretty() + "\" as a " + rtype.name + " model"); return mdl; } break; } } } // Not yet interpreted - try each known format in order. for(int i = 0; !modelTypes[i].name.isEmpty(); ++i) { ModelFileType const &rtype = modelTypes[i]; // Already tried this? if(&rtype == rtypeGuess) continue; if(Model *mdl = rtype.loadFunc(hndl, aspectScale)) { LOG_RES_VERBOSE("Interpreted \"" + NativePath(filePath).pretty() + "\" as a " + rtype.name + " model"); return mdl; } } return 0; } uint Model::modelId() const { return d->modelId; } void Model::setModelId(uint newModelId) { d->modelId = newModelId; } Model::Flags Model::flags() const { return d->flags; } void Model::setFlags(Model::Flags flagsToChange, FlagOp operation) { LOG_AS("Model"); applyFlagOperation(d->flags, flagsToChange, operation); } int Model::frameNumber(String name) const { if(!name.isEmpty()) { for(int i = 0; i < d->frames.count(); ++i) { Frame *frame = d->frames.at(i); if(!frame->name.compareWithoutCase(name)) return i; } } return -1; // Not found. } Model::Frame &Model::frame(int number) const { LOG_AS("Model"); if(hasFrame(number)) { return *d->frames.at(number); } throw MissingFrameError("Model::frame", "Invalid frame number " + String::number(number) + ", valid range is " + Rangei(0, d->frames.count()).asText()); } Model::Frames const &Model::frames() const { return d->frames; } void Model::clearAllFrames() { LOG_AS("Model"); qDeleteAll(d->frames); d->frames.clear(); } int Model::skinNumber(String name) const { if(!name.isEmpty()) { // Reverse iteration so that later skins override earlier ones. for(int i = d->skins.count(); i--> 0; ) { Skin const &skin = d->skins.at(i); if(!skin.name.compareWithoutCase(name)) return i; } } return -1; // Not found. } Model::Skin &Model::skin(int number) const { LOG_AS("Model"); if(hasSkin(number)) { return const_cast(d->skins.at(number)); } throw MissingSkinError("Model::skin", "Invalid skin number " + String::number(number) + ", valid range is " + Rangei(0, d->skins.count()).asText()); } Model::Skin &Model::newSkin(String name) { LOG_AS("Model"); // Allow duplicates so that skin indices remain unchanged for selection by index. d->skins.append(ModelSkin(name)); return d->skins.last(); } Model::Skins const &Model::skins() const { return d->skins; } void Model::clearAllSkins() { LOG_AS("Model"); d->skins.clear(); } Model::DetailLevel &Model::lod(int level) const { LOG_AS("Model"); if(hasLod(level)) { return *d->lods.at(level); } throw MissingDetailLevelError("Model::lod", "Invalid detail level " + String::number(level) + ", valid range is " + Rangei(0, d->lods.count()).asText()); } Model::DetailLevels const &Model::lods() const { return d->lods; } Model::Primitives const &Model::primitives() const { LOG_AS("Model"); return lod(0).primitives; } int Model::vertexCount() const { return d->numVertices; } QBitArray const &Model::lodVertexUsage() const { return d->lodVertexUsage; } doomsday-stable-1.15.7/doomsday/client/src/resource/texturemanifest.cpp0000664000175000017500000001474612641367670025656 0ustar jaakkojaakko/** @file texturemanifest.cpp Texture Manifest. * * @authors Copyright © 2010-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "resource/texturemanifest.h" #include "dd_main.h" // App_ResourceSystem() using namespace de; DENG2_PIMPL(TextureManifest), DENG2_OBSERVES(Texture, Deletion) { int uniqueId; ///< Scheme-unique identifier (user defined). Uri resourceUri; ///< Image resource path, to be loaded. Vector2i logicalDimensions; ///< Dimensions in map space. Vector2i origin; ///< Origin offset in map space. Texture::Flags flags; ///< Classification flags. QScopedPointer texture; ///< Associated resource (if any). Instance(Public *i) : Base(i) , uniqueId(0) {} ~Instance() { DENG2_FOR_PUBLIC_AUDIENCE(Deletion, i) i->textureManifestBeingDeleted(self); } // Observes Texture Deletion. void textureBeingDeleted(Texture const & /*texture*/) { texture.reset(); } }; TextureManifest::TextureManifest(PathTree::NodeArgs const &args) : Node(args), d(new Instance(this)) {} Texture *TextureManifest::derive() { LOG_AS("TextureManifest::derive"); if(!hasTexture()) { // Instantiate and associate the new texture with this. setTexture(new Texture(*this)); // Notify interested parties that a new texture was derived from the manifest. DENG2_FOR_AUDIENCE(TextureDerived, i) i->textureManifestTextureDerived(*this, texture()); } else { Texture *tex = &texture(); /// @todo Materials and Surfaces should be notified of this! tex->setFlags(d->flags); tex->setDimensions(d->logicalDimensions); tex->setOrigin(d->origin); } return &texture(); } TextureScheme &TextureManifest::scheme() const { LOG_AS("TextureManifest::scheme"); /// @todo Optimize: TextureManifest should contain a link to the owning TextureScheme. foreach(TextureScheme *scheme, App_ResourceSystem().allTextureSchemes()) { if(&scheme->index() == &tree()) { return *scheme; } } /// @throw Error Failed to determine the scheme of the manifest (should never happen...). throw Error("TextureManifest::scheme", String("Failed to determine scheme for manifest [%1]").arg(de::dintptr(this))); } String const &TextureManifest::schemeName() const { return scheme().name(); } String TextureManifest::description(de::Uri::ComposeAsTextFlags uriCompositionFlags) const { String info = String("%1 %2") .arg(composeUri().compose(uriCompositionFlags | Uri::DecodePath), ( uriCompositionFlags.testFlag(Uri::OmitScheme)? -14 : -22 ) ) .arg(sourceDescription(), -7); #ifdef __CLIENT__ info += String("x%1").arg(!hasTexture()? 0 : texture().variantCount()); #endif info += " " + (!hasResourceUri()? "N/A" : resourceUri().asText()); return info; } String TextureManifest::sourceDescription() const { if(!hasTexture()) return "unknown"; if(texture().isFlagged(Texture::Custom)) return "add-on"; return "game"; } bool TextureManifest::hasResourceUri() const { return !d->resourceUri.isEmpty(); } de::Uri TextureManifest::resourceUri() const { if(hasResourceUri()) { return d->resourceUri; } /// @throw MissingResourceUriError There is no resource URI defined. throw MissingResourceUriError("TextureManifest::scheme", "No resource URI is defined"); } bool TextureManifest::setResourceUri(de::Uri const &newUri) { // Avoid resolving; compare as text. if(d->resourceUri.asText() != newUri.asText()) { d->resourceUri = newUri; return true; } return false; } int TextureManifest::uniqueId() const { return d->uniqueId; } bool TextureManifest::setUniqueId(int newUniqueId) { if(d->uniqueId == newUniqueId) return false; d->uniqueId = newUniqueId; // Notify interested parties that the uniqueId has changed. DENG2_FOR_AUDIENCE(UniqueIdChange, i) i->textureManifestUniqueIdChanged(*this); return true; } Texture::Flags TextureManifest::flags() const { return d->flags; } void TextureManifest::setFlags(Texture::Flags flagsToChange, FlagOp operation) { applyFlagOperation(d->flags, flagsToChange, operation); } Vector2i const &TextureManifest::logicalDimensions() const { return d->logicalDimensions; } bool TextureManifest::setLogicalDimensions(Vector2i const &newDimensions) { if(d->logicalDimensions == newDimensions) return false; d->logicalDimensions = newDimensions; return true; } Vector2i const &TextureManifest::origin() const { return d->origin; } void TextureManifest::setOrigin(Vector2i const &newOrigin) { if(d->origin != newOrigin) { d->origin = newOrigin; } } bool TextureManifest::hasTexture() const { return !d->texture.isNull(); } Texture &TextureManifest::texture() const { if(hasTexture()) { return *d->texture; } /// @throw MissingTextureError There is no texture associated with the manifest. throw MissingTextureError("TextureManifest::texture", "No texture is associated"); } void TextureManifest::setTexture(Texture *newTexture) { if(d->texture.data() != newTexture) { if(Texture *curTexture = d->texture.data()) { // Cancel notifications about the existing texture. curTexture->audienceForDeletion -= d; } d->texture.reset(newTexture); if(Texture *curTexture = d->texture.data()) { // We want notification when the new texture is about to be deleted. curTexture->audienceForDeletion += d; } } } doomsday-stable-1.15.7/doomsday/client/src/resource/materialarchive.cpp0000664000175000017500000002647012641367670025564 0ustar jaakkojaakko/** @file materialarchive.cpp Material Archive. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "de_console.h" #include #include #include "resource/materialarchive.h" /// For identifying the archived format version. Written to disk. #define MATERIALARCHIVE_VERSION 4 #define ASEG_MATERIAL_ARCHIVE 112 // Used to denote unknown Material references in records. Written to disk. #define UNKNOWN_MATERIALNAME "DD_BADTX" namespace de { static String readArchivedPath(reader_s &reader) { char _path[9]; Reader_Read(&reader, _path, 8); _path[8] = 0; return QString(QByteArray(_path, qstrlen(_path)).toPercentEncoding()); } static void readArchivedUri(Uri &uri, int version, reader_s &reader) { if(version >= 4) { // A serialized, percent encoded URI. Uri_Read(reinterpret_cast(&uri), &reader); } else if(version == 3) { // A percent encoded textual URI. ddstring_t *_uri = Str_NewFromReader(&reader); uri.setUri(Str_Text(_uri), RC_NULL); Str_Delete(_uri); } else if(version == 2) { // An unencoded textual URI. ddstring_t *_uri = Str_NewFromReader(&reader); uri.setUri(QString(QByteArray(Str_Text(_uri), Str_Length(_uri)).toPercentEncoding()), RC_NULL); Str_Delete(_uri); } else // ver 1 { // A short textual path (unencoded). uri.setPath(readArchivedPath(reader)); // Plus a legacy scheme id. int oldSchemeId = Reader_ReadByte(&reader); switch(oldSchemeId) { case 0: uri.setScheme("Textures"); break; case 1: uri.setScheme("Flats"); break; case 2: uri.setScheme("Sprites"); break; case 3: uri.setScheme("System"); break; default: throw Error("readArchiveUri", QString("Unknown old-scheme id #%1, expected [0..3)").arg(oldSchemeId)); } } } /** * Mappings between URI and Material. * The pointer user value holds a pointer to the resolved Material (if found). * The integer user value tracks whether a material has yet been looked up. */ typedef StringPool Records; typedef StringPool::Id SerialId; static Material *findRecordMaterial(Records &records, SerialId id) { // Time to lookup the material for the record's URI? if(!records.userValue(id)) { Material *material = 0; try { material = &App_ResourceSystem().material(Uri(records.stringRef(id), RC_NULL)); } catch(ResourceSystem::MissingManifestError const &) {} // Ignore this error. records.setUserPointer(id, material); records.setUserValue(id, true); return material; } return (Material *) records.userPointer(id); } DENG2_PIMPL(MaterialArchive) { int version = MATERIALARCHIVE_VERSION; bool useSegments = false; ///< Segment id assertion (Hexen saves). Records records; ///< Mappings between URI and Material. int numFlats = 0; ///< Used with older versions. Instance(Public *i) : Base(i) {} inline SerialId insertRecord(Uri const &uri) { return records.intern(uri.compose()); } void beginSegment(int seg, writer_s &writer) { if(!useSegments) return; Writer_WriteUInt32(&writer, seg); } void assertSegment(int seg, reader_s &reader) { if(!useSegments) return; int i = Reader_ReadUInt32(&reader); if(i != seg) { throw MaterialArchive::ReadError("MaterialArchive::assertSegment", QString("Expected ASEG_MATERIAL_ARCHIVE (%1), but got %2") .arg(ASEG_MATERIAL_ARCHIVE).arg(i)); } } void writeHeader(writer_s &writer) { beginSegment(ASEG_MATERIAL_ARCHIVE, writer); Writer_WriteByte(&writer, version); } void readHeader(reader_s &reader) { assertSegment(ASEG_MATERIAL_ARCHIVE, reader); version = Reader_ReadByte(&reader); } void readGroup(reader_s &reader) { DENG_ASSERT(version >= 1); // Read the group header. int num = Reader_ReadUInt16(&reader); // Read the group records. Uri uri; for(int i = 0; i < num; ++i) { readArchivedUri(uri, version, reader); insertRecord(uri); } } void writeGroup(writer_s &writer) { // Write the group header. Writer_WriteUInt16(&writer, records.size()); // Write the group records. Uri uri; for(int i = 1; i < int(records.size()) + 1; ++i) { uri.setUri(records.stringRef(i), RC_NULL); Uri_Write(reinterpret_cast(&uri), &writer); } } }; MaterialArchive::MaterialArchive(int useSegments, bool recordSymbolicMaterials) : d(new Instance(this)) { d->useSegments = useSegments; if(recordSymbolicMaterials) { // The first material is the special "unknown material". d->insertRecord(de::Uri(UNKNOWN_MATERIALNAME, RC_NULL)); } } struct findUniqueSerialIdWorker_params { Records *records; Material *material; }; static int findUniqueSerialIdWorker(SerialId id, void *parameters) { findUniqueSerialIdWorker_params *parm = (findUniqueSerialIdWorker_params*) parameters; // Is this the material we are looking for? if(findRecordMaterial(*parm->records, id) == parm->material) return id; return 0; // Continue iteration. } materialarchive_serialid_t MaterialArchive::findUniqueSerialId(Material *material) const { if(!material) return 0; // Invalid. findUniqueSerialIdWorker_params parm; parm.records = &d->records; parm.material = material; SerialId found = d->records.iterate(findUniqueSerialIdWorker, &parm); if(found) return found; // Yes. Return existing serial. return d->records.size() + 1; } Material *MaterialArchive::find(materialarchive_serialid_t serialId, int group) const { if(serialId <= 0 || serialId > d->records.size() + 1) return 0; // Invalid. // A group offset? if(d->version < 1 && group == 1) { // Group 1 = walls (skip over the flats). serialId += d->numFlats; } if(d->version <= 1) { // The special case "unknown" material? Uri uri(d->records.stringRef(serialId), RC_NULL); if(!uri.path().toStringRef().compareWithoutCase(UNKNOWN_MATERIALNAME)) return 0; } return findRecordMaterial(d->records, serialId); } materialarchive_serialid_t MaterialArchive::addRecord(Material const &material) { SerialId id = d->insertRecord(material.manifest().composeUri()); d->records.setUserPointer(id, const_cast(&material)); d->records.setUserValue(id, true); return materialarchive_serialid_t(id); } int MaterialArchive::count() const { return d->records.size(); } void MaterialArchive::write(writer_s &writer) const { d->writeHeader(writer); d->writeGroup(writer); } void MaterialArchive::read(reader_s &reader, int forcedVersion) { d->records.clear(); d->readHeader(reader); // Are we interpreting a specific version? if(forcedVersion >= 0) { d->version = forcedVersion; } if(d->version >= 1) { d->readGroup(reader); return; } // The old format saved materials used on floors and walls into seperate // groups. At this time only Flats could be used on floors and Textures // on walls. { // Group 0 (floors) Uri uri("Flats", ""); d->numFlats = Reader_ReadUInt16(&reader); for(int i = 0; i < d->numFlats; ++i) { uri.setPath(readArchivedPath(reader)); d->insertRecord(uri); } } { // Group 1 (walls) Uri uri("Textures", ""); int num = Reader_ReadUInt16(&reader); for(int i = 0; i < num; ++i) { uri.setPath(readArchivedPath(reader)); d->insertRecord(uri); } } } } // namespace de /* * C Wrapper API: */ #define DENG_NO_API_MACROS_MATERIAL_ARCHIVE #include "api_materialarchive.h" #define TOINTERNAL(inst) \ reinterpret_cast(inst) #define TOINTERNAL_CONST(inst) \ reinterpret_cast(inst) #define SELF(inst) \ DENG2_ASSERT(inst); \ de::MaterialArchive *self = TOINTERNAL(inst) #define SELF_CONST(inst) \ DENG2_ASSERT(inst); \ de::MaterialArchive const *self = TOINTERNAL_CONST(inst) #undef MaterialArchive_New MaterialArchive *MaterialArchive_New(int useSegments) { auto *archive = new de::MaterialArchive(useSegments); // Populate the archive using the application's global/main Material collection. App_ResourceSystem().forAllMaterials([&archive] (Material &material) { archive->addRecord(material); return de::LoopContinue; }); return reinterpret_cast(archive); } #undef MaterialArchive_NewEmpty MaterialArchive *MaterialArchive_NewEmpty(int useSegments) { return reinterpret_cast(new de::MaterialArchive(useSegments, false /*don't populate*/)); } #undef MaterialArchive_Delete void MaterialArchive_Delete(MaterialArchive *arc) { if(arc) { SELF(arc); delete self; } } #undef MaterialArchive_FindUniqueSerialId materialarchive_serialid_t MaterialArchive_FindUniqueSerialId(MaterialArchive const *arc, Material *mat) { SELF_CONST(arc); return self->findUniqueSerialId(mat); } #undef MaterialArchive_Find Material *MaterialArchive_Find(MaterialArchive const *arc, materialarchive_serialid_t serialId, int group) { SELF_CONST(arc); return self->find(serialId, group); } #undef MaterialArchive_Count int MaterialArchive_Count(MaterialArchive const *arc) { SELF_CONST(arc); return self->count(); } #undef MaterialArchive_Write void MaterialArchive_Write(MaterialArchive const *arc, Writer *writer) { SELF_CONST(arc); self->write(*writer); } #undef MaterialArchive_Read void MaterialArchive_Read(MaterialArchive *arc, Reader *reader, int forcedVersion) { SELF(arc); self->read(*reader, forcedVersion); } DENG_DECLARE_API(MaterialArchive) = { { DE_API_MATERIAL_ARCHIVE }, MaterialArchive_New, MaterialArchive_NewEmpty, MaterialArchive_Delete, MaterialArchive_FindUniqueSerialId, MaterialArchive_Find, MaterialArchive_Count, MaterialArchive_Write, MaterialArchive_Read }; doomsday-stable-1.15.7/doomsday/client/src/resource/fonts.cpp0000664000175000017500000000172712641367670023553 0ustar jaakkojaakko/** @file fonts.cpp Font resource collection. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ doomsday-stable-1.15.7/doomsday/client/src/gl/0000775000175000017500000000000012641367670020462 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/gl/dgl_common.cpp0000664000175000017500000007571712641367670023325 0ustar jaakkojaakko/** @file dgl_common.cpp Misc Drawing Routines * @ingroup gl * * @authors Copyright © 2004-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #define DENG_NO_API_MACROS_GL #include #include #include "de_base.h" #include "de_console.h" #include "de_graphics.h" #include "de_misc.h" #include "de_filesys.h" #include "de_resource.h" #include "render/r_draw.h" #include "gl/sys_opengl.h" #include "api_gl.h" #include "gl/gl_texmanager.h" #include #include #include using namespace de; /** * Requires a texture environment mode that can add and multiply. * Nvidia's and ATI's appropriate extensions are supported, other cards will * not be able to utilize multitextured lights. */ static void envAddColoredAlpha(int activate, GLenum addFactor) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); if(activate) { glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GLInfo::extensions().NV_texture_env_combine4? GL_COMBINE4_NV : GL_COMBINE); glTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE, 1); // Combine: texAlpha * constRGB + 1 * prevRGB. if(GLInfo::extensions().NV_texture_env_combine4) { glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_ADD); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, addFactor); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_CONSTANT); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE2_RGB, GL_ZERO); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_RGB, GL_ONE_MINUS_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE3_RGB_NV, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND3_RGB_NV, GL_SRC_COLOR); } else if(GLInfo::extensions().ATI_texture_env_combine3) { // MODULATE_ADD_ATI: Arg0 * Arg2 + Arg1. glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE_ADD_ATI); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, addFactor); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE2_RGB, GL_CONSTANT); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_RGB, GL_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); } else { // This doesn't look right. glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_ADD); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, addFactor); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_CONSTANT); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); } } else { glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); } } /** * Setup the texture environment for single-pass multiplicative lighting. * The last texture unit is always used for the texture modulation. * TUs 1...n-1 are used for dynamic lights. */ static void envModMultiTex(int activate) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // Setup TU 2: The modulated texture. glActiveTexture(GL_TEXTURE1); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); // Setup TU 1: The dynamic light. glActiveTexture(GL_TEXTURE0); envAddColoredAlpha(activate, GL_SRC_ALPHA); // This is a single-pass mode. The alpha should remain unmodified // during the light stage. if(activate) { // Replace: primAlpha. glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); } } void GL_ModulateTexture(int mode) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); switch(mode) { case 0: // No modulation: just replace with texture. glActiveTexture(GL_TEXTURE0); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); break; case 1: // Normal texture modulation with primary color. glActiveTexture(GL_TEXTURE0); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); break; case 12: // Normal texture modulation on both stages. TU 1 modulates with // primary color, TU 2 with TU 1. glActiveTexture(GL_TEXTURE1); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); glActiveTexture(GL_TEXTURE0); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); break; case 2: case 3: // Texture modulation and interpolation. glActiveTexture(GL_TEXTURE1); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); glTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE, 1); if(mode == 2) { // Used with surfaces that have a color. // TU 2: Modulate previous with primary color. glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_PRIMARY_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); } else { // Mode 3: Used with surfaces with no primary color. // TU 2: Pass through. glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_REPLACE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR); } glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); // TU 1: Interpolate between texture 1 and 2, using the constant // alpha as the factor. glActiveTexture(GL_TEXTURE0); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_INTERPOLATE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE1); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_TEXTURE0); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE2_RGB, GL_CONSTANT); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_RGB, GL_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE, 1); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); break; case 4: // Apply sector light, dynamic light and texture. envModMultiTex(true); break; case 5: case 10: // Sector light * texture + dynamic light. glActiveTexture(GL_TEXTURE1); envAddColoredAlpha(true, mode == 5 ? GL_SRC_ALPHA : GL_SRC_COLOR); // Alpha remains unchanged. if(GLInfo::extensions().NV_texture_env_combine4) { glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_ADD); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_ZERO); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE2_ALPHA, GL_ZERO); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_ALPHA, GL_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE3_ALPHA_NV, GL_ZERO); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND3_ALPHA_NV, GL_SRC_ALPHA); } else { glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); } glActiveTexture(GL_TEXTURE0); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); break; case 6: // Simple dynlight addition (add to primary color). glActiveTexture(GL_TEXTURE0); envAddColoredAlpha(true, GL_SRC_ALPHA); break; case 7: // Dynlight addition without primary color. glActiveTexture(GL_TEXTURE0); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_CONSTANT); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE, 1); break; case 8: case 9: // Texture and Detail. glActiveTexture(GL_TEXTURE1); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE, 2); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); glActiveTexture(GL_TEXTURE0); if(mode == 8) { glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); } else { // Mode 9: Ignore primary color. glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); } break; case 11: // Normal modulation, alpha of 2nd stage. // Tex0: texture // Tex1: shiny texture glActiveTexture(GL_TEXTURE1); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); glTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE, 1); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_REPLACE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); glActiveTexture(GL_TEXTURE0); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); glTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE, 1); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_TEXTURE1); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, GL_TEXTURE0); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA); break; default: break; } } /*void GL_BlendOp(int op) { if(!GL_state.features.blendSubtract) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glBlendEquationEXT(op); }*/ void GL_SetVSync(dd_bool on) { // Outside the main thread we'll need to defer the call. if(!Sys_InMainThread()) { GL_DeferSetVSync(on); return; } if(!GL_state.features.vsync) return; #ifdef WIN32 wglSwapIntervalEXT(on? 1 : 0); #elif defined(MACOSX) { // Tell CGL to wait for vertical refresh. CGLContextObj context = CGLGetCurrentContext(); assert(context != 0); if(context) { GLint params[1] = { on? 1 : 0 }; CGLSetParameter(context, kCGLCPSwapInterval, params); } } #elif defined(Q_WS_X11) setXSwapInterval(on? 1 : 0); #endif } void GL_SetMultisample(dd_bool on) { if(!GL_state.features.multisample) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); #if defined(WIN32) if(on) glEnable(GL_MULTISAMPLE_ARB); else glDisable(GL_MULTISAMPLE_ARB); #else if(on) glEnable(GL_MULTISAMPLE); else glDisable(GL_MULTISAMPLE); #endif } #undef DGL_SetScissor DENG_EXTERN_C void DGL_SetScissor(RectRaw const *rect) { if(!rect) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); GameWidget &game = ClientWindow::main().game(); GLState::current().setNormalizedScissor( game.normalizedRect( Rectanglei(rect->origin.x, rect->origin.y, rect->size.width, rect->size.height), game.rule().recti())).apply(); } #undef DGL_SetScissor2 DENG_EXTERN_C void DGL_SetScissor2(int x, int y, int width, int height) { RectRaw rect; rect.origin.x = x; rect.origin.y = y; rect.size.width = width; rect.size.height = height; DGL_SetScissor(&rect); } #undef DGL_GetIntegerv dd_bool DGL_GetIntegerv(int name, int *v) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); float color[4]; switch(name) { case DGL_MODULATE_ADD_COMBINE: *v = GLInfo::extensions().NV_texture_env_combine4 || GLInfo::extensions().ATI_texture_env_combine3; break; case DGL_SCISSOR_TEST: *(GLint *) v = GLState::current().scissor(); break; case DGL_FOG: *v = GL_state.currentUseFog; break; case DGL_CURRENT_COLOR_R: glGetFloatv(GL_CURRENT_COLOR, color); *v = int(color[0] * 255); break; case DGL_CURRENT_COLOR_G: glGetFloatv(GL_CURRENT_COLOR, color); *v = int(color[1] * 255); break; case DGL_CURRENT_COLOR_B: glGetFloatv(GL_CURRENT_COLOR, color); *v = int(color[2] * 255); break; case DGL_CURRENT_COLOR_A: glGetFloatv(GL_CURRENT_COLOR, color); *v = int(color[3] * 255); break; case DGL_CURRENT_COLOR_RGBA: glGetFloatv(GL_CURRENT_COLOR, color); for(int i = 0; i < 4; ++i) { v[i] = int(color[i] * 255); } break; default: return false; } return true; } #undef DGL_GetInteger int DGL_GetInteger(int name) { int values[10] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; DGL_GetIntegerv(name, values); return values[0]; } #undef DGL_SetInteger dd_bool DGL_SetInteger(int name, int value) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); switch(name) { case DGL_ACTIVE_TEXTURE: glActiveTexture(GL_TEXTURE0 + byte(value)); break; case DGL_MODULATE_TEXTURE: GL_ModulateTexture(value); break; default: return false; } return true; } #undef DGL_GetFloatv dd_bool DGL_GetFloatv(int name, float *v) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); float color[4]; switch(name) { case DGL_CURRENT_COLOR_R: glGetFloatv(GL_CURRENT_COLOR, color); *v = color[0]; break; case DGL_CURRENT_COLOR_G: glGetFloatv(GL_CURRENT_COLOR, color); *v = color[1]; break; case DGL_CURRENT_COLOR_B: glGetFloatv(GL_CURRENT_COLOR, color); *v = color[2]; break; case DGL_CURRENT_COLOR_A: glGetFloatv(GL_CURRENT_COLOR, color); *v = color[3]; break; case DGL_CURRENT_COLOR_RGBA: glGetFloatv(GL_CURRENT_COLOR, color); for(int i = 0; i < 4; ++i) { v[i] = color[i]; } break; default: return false; } return true; } #undef DGL_GetFloat float DGL_GetFloat(int name) { switch(name) { case DGL_LINE_WIDTH: return GL_state.currentLineWidth; case DGL_POINT_SIZE: return GL_state.currentPointSize; default: return 0; } } #undef DGL_SetFloat dd_bool DGL_SetFloat(int name, float value) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); switch(name) { case DGL_LINE_WIDTH: GL_state.currentLineWidth = value; glLineWidth(value); break; case DGL_POINT_SIZE: GL_state.currentPointSize = value; glPointSize(value); break; default: return false; } return true; } #undef DGL_PushState void DGL_PushState(void) { GLState::push(); } #undef DGL_PopState void DGL_PopState(void) { GLState::pop(); // Make sure the restored state is immediately in effect. GLState::current().apply(); } #undef DGL_Enable int DGL_Enable(int cap) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); switch(cap) { case DGL_TEXTURE_2D: #ifndef DRMESA glEnable(GL_TEXTURE_2D); #endif break; case DGL_FOG: glEnable(GL_FOG); GL_state.currentUseFog = true; break; case DGL_SCISSOR_TEST: //glEnable(GL_SCISSOR_TEST); break; case DGL_LINE_SMOOTH: glEnable(GL_LINE_SMOOTH); break; case DGL_POINT_SMOOTH: glEnable(GL_POINT_SMOOTH); break; default: return 0; } return 1; } #undef DGL_Disable void DGL_Disable(int cap) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); switch(cap) { case DGL_TEXTURE_2D: glDisable(GL_TEXTURE_2D); break; case DGL_FOG: glDisable(GL_FOG); GL_state.currentUseFog = false; break; case DGL_SCISSOR_TEST: //glDisable(GL_SCISSOR_TEST); GLState::current().clearScissor().apply(); break; case DGL_LINE_SMOOTH: glDisable(GL_LINE_SMOOTH); break; case DGL_POINT_SMOOTH: glDisable(GL_POINT_SMOOTH); break; default: break; } } #undef DGL_BlendOp void DGL_BlendOp(int op) { GLState::current().setBlendOp(op == DGL_SUBTRACT ? gl::Subtract : op == DGL_REVERSE_SUBTRACT ? gl::ReverseSubtract : gl::Add) .apply(); } #undef DGL_BlendFunc void DGL_BlendFunc(int param1, int param2) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); GLState::current().setBlendFunc(param1 == DGL_ZERO ? gl::Zero : param1 == DGL_ONE ? gl::One : param1 == DGL_DST_COLOR ? gl::DestColor : param1 == DGL_ONE_MINUS_DST_COLOR ? gl::OneMinusDestColor : param1 == DGL_SRC_ALPHA ? gl::SrcAlpha : param1 == DGL_ONE_MINUS_SRC_ALPHA ? gl::OneMinusSrcAlpha : param1 == DGL_DST_ALPHA ? gl::DestAlpha : param1 == DGL_ONE_MINUS_DST_ALPHA ? gl::OneMinusDestAlpha : gl::Zero , param2 == DGL_ZERO ? gl::Zero : param2 == DGL_ONE ? gl::One : param2 == DGL_SRC_COLOR ? gl::SrcColor : param2 == DGL_ONE_MINUS_SRC_COLOR ? gl::OneMinusSrcColor : param2 == DGL_SRC_ALPHA ? gl::SrcAlpha : param2 == DGL_ONE_MINUS_SRC_ALPHA ? gl::OneMinusSrcAlpha : param2 == DGL_DST_ALPHA ? gl::DestAlpha : param2 == DGL_ONE_MINUS_DST_ALPHA ? gl::OneMinusDestAlpha : gl::Zero) .apply(); } #undef DGL_BlendMode void DGL_BlendMode(blendmode_t mode) { GL_BlendMode(mode); } #undef DGL_MatrixMode void DGL_MatrixMode(int mode) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glMatrixMode(mode == DGL_PROJECTION ? GL_PROJECTION : mode == DGL_TEXTURE ? GL_TEXTURE : GL_MODELVIEW); } #undef DGL_PushMatrix void DGL_PushMatrix(void) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glPushMatrix(); #if _DEBUG if(glGetError() == GL_STACK_OVERFLOW) App_Error("DG_PushMatrix: Stack overflow.\n"); #endif } #undef DGL_SetNoMaterial void DGL_SetNoMaterial(void) { GL_SetNoTexture(); } static gl::Wrapping DGL_ToGLWrapCap(DGLint cap) { switch(cap) { case DGL_CLAMP: case DGL_CLAMP_TO_EDGE: return gl::ClampToEdge; case DGL_REPEAT: return gl::Repeat; default: App_Error("DGL_ToGLWrapCap: Unknown cap value %i.", (int)cap); exit(1); // Unreachable. } } #undef DGL_SetMaterialUI void DGL_SetMaterialUI(Material *mat, DGLint wrapS, DGLint wrapT) { GL_SetMaterialUI2(mat, DGL_ToGLWrapCap(wrapS), DGL_ToGLWrapCap(wrapT)); } #undef DGL_SetPatch void DGL_SetPatch(patchid_t id, DGLint wrapS, DGLint wrapT) { try { TextureManifest &manifest = App_ResourceSystem().textureScheme("Patches").findByUniqueId(id); if(!manifest.hasTexture()) return; Texture &tex = manifest.texture(); TextureVariantSpec const &texSpec = Rend_PatchTextureSpec(0 | (tex.isFlagged(Texture::Monochrome) ? TSF_MONOCHROME : 0) | (tex.isFlagged(Texture::UpscaleAndSharpen) ? TSF_UPSCALE_AND_SHARPEN : 0), DGL_ToGLWrapCap(wrapS), DGL_ToGLWrapCap(wrapT)); GL_BindTexture(tex.prepareVariant(texSpec)); } catch(TextureScheme::NotFoundError const &er) { // Log but otherwise ignore this error. LOG_RES_WARNING("Cannot use patch ID %i: %s") << id << er.asText(); } } #undef DGL_SetPSprite void DGL_SetPSprite(Material *mat) { GL_SetPSprite(mat, 0, 0); } #undef DGL_SetPSprite2 void DGL_SetPSprite2(Material *mat, int tclass, int tmap) { GL_SetPSprite(mat, tclass, tmap); } #undef DGL_SetRawImage void DGL_SetRawImage(lumpnum_t lumpNum, DGLint wrapS, DGLint wrapT) { GL_SetRawImage(lumpNum, DGL_ToGLWrapCap(wrapS), DGL_ToGLWrapCap(wrapT)); } #undef DGL_PopMatrix void DGL_PopMatrix(void) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glPopMatrix(); #if _DEBUG if(glGetError() == GL_STACK_UNDERFLOW) App_Error("DG_PopMatrix: Stack underflow.\n"); #endif } #undef DGL_LoadIdentity void DGL_LoadIdentity(void) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glLoadIdentity(); } #undef DGL_Translatef void DGL_Translatef(float x, float y, float z) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glTranslatef(x, y, z); } #undef DGL_Rotatef void DGL_Rotatef(float angle, float x, float y, float z) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glRotatef(angle, x, y, z); } #undef DGL_Scalef void DGL_Scalef(float x, float y, float z) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glScalef(x, y, z); } #undef DGL_Ortho void DGL_Ortho(float left, float top, float right, float bottom, float znear, float zfar) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glOrtho(left, right, bottom, top, znear, zfar); } #undef DGL_DeleteTextures void DGL_DeleteTextures(int num, DGLuint const *names) { if(!num || !names) return; glDeleteTextures(num, (GLuint const *) names); } #undef DGL_Bind int DGL_Bind(DGLuint texture) { GL_BindTextureUnmanaged(texture); DENG_ASSERT(!Sys_GLCheckError()); return 0; } #undef DGL_NewTextureWithParams DGLuint DGL_NewTextureWithParams(dgltexformat_t format, int width, int height, uint8_t const *pixels, int flags, int minFilter, int magFilter, int anisoFilter, int wrapS, int wrapT) { return GL_NewTextureWithParams(format, width, height, (uint8_t *)pixels, flags, 0, (minFilter == DGL_LINEAR ? GL_LINEAR : minFilter == DGL_NEAREST ? GL_NEAREST : minFilter == DGL_NEAREST_MIPMAP_NEAREST ? GL_NEAREST_MIPMAP_NEAREST : minFilter == DGL_LINEAR_MIPMAP_NEAREST ? GL_LINEAR_MIPMAP_NEAREST : minFilter == DGL_NEAREST_MIPMAP_LINEAR ? GL_NEAREST_MIPMAP_LINEAR : GL_LINEAR_MIPMAP_LINEAR), (magFilter == DGL_LINEAR ? GL_LINEAR : GL_NEAREST), anisoFilter, (wrapS == DGL_CLAMP ? GL_CLAMP : wrapS == DGL_CLAMP_TO_EDGE ? GL_CLAMP_TO_EDGE : GL_REPEAT), (wrapT == DGL_CLAMP ? GL_CLAMP : wrapT == DGL_CLAMP_TO_EDGE ? GL_CLAMP_TO_EDGE : GL_REPEAT)); } // dgl_draw.cpp DENG_EXTERN_C void DGL_Begin(dglprimtype_t mode); DENG_EXTERN_C void DGL_End(void); DENG_EXTERN_C dd_bool DGL_NewList(DGLuint list, int mode); DENG_EXTERN_C DGLuint DGL_EndList(void); DENG_EXTERN_C void DGL_CallList(DGLuint list); DENG_EXTERN_C void DGL_DeleteLists(DGLuint list, int range); DENG_EXTERN_C void DGL_Color3ub(DGLubyte r, DGLubyte g, DGLubyte b); DENG_EXTERN_C void DGL_Color3ubv(const DGLubyte* vec); DENG_EXTERN_C void DGL_Color4ub(DGLubyte r, DGLubyte g, DGLubyte b, DGLubyte a); DENG_EXTERN_C void DGL_Color4ubv(const DGLubyte* vec); DENG_EXTERN_C void DGL_Color3f(float r, float g, float b); DENG_EXTERN_C void DGL_Color3fv(const float* vec); DENG_EXTERN_C void DGL_Color4f(float r, float g, float b, float a); DENG_EXTERN_C void DGL_Color4fv(const float* vec); DENG_EXTERN_C void DGL_TexCoord2f(byte target, float s, float t); DENG_EXTERN_C void DGL_TexCoord2fv(byte target, float* vec); DENG_EXTERN_C void DGL_Vertex2f(float x, float y); DENG_EXTERN_C void DGL_Vertex2fv(const float* vec); DENG_EXTERN_C void DGL_Vertex3f(float x, float y, float z); DENG_EXTERN_C void DGL_Vertex3fv(const float* vec); DENG_EXTERN_C void DGL_Vertices2ftv(int num, const dgl_ft2vertex_t* vec); DENG_EXTERN_C void DGL_Vertices3ftv(int num, const dgl_ft3vertex_t* vec); DENG_EXTERN_C void DGL_Vertices3fctv(int num, const dgl_fct3vertex_t* vec); DENG_EXTERN_C void DGL_DrawLine(float x1, float y1, float x2, float y2, float r, float g, float b, float a); DENG_EXTERN_C void DGL_DrawRect(const RectRaw* rect); DENG_EXTERN_C void DGL_DrawRect2(int x, int y, int w, int h); DENG_EXTERN_C void DGL_DrawRectf(const RectRawf* rect); DENG_EXTERN_C void DGL_DrawRectf2(double x, double y, double w, double h); DENG_EXTERN_C void DGL_DrawRectf2Color(double x, double y, double w, double h, float r, float g, float b, float a); DENG_EXTERN_C void DGL_DrawRectf2Tiled(double x, double y, double w, double h, int tw, int th); DENG_EXTERN_C void DGL_DrawCutRectfTiled(const RectRawf* rect, int tw, int th, int txoff, int tyoff, const RectRawf* cutRect); DENG_EXTERN_C void DGL_DrawCutRectf2Tiled(double x, double y, double w, double h, int tw, int th, int txoff, int tyoff, double cx, double cy, double cw, double ch); DENG_EXTERN_C void DGL_DrawQuadOutline(const Point2Raw* tl, const Point2Raw* tr, const Point2Raw* br, const Point2Raw* bl, const float color[4]); DENG_EXTERN_C void DGL_DrawQuad2Outline(int tlX, int tlY, int trX, int trY, int brX, int brY, int blX, int blY, const float color[4]); // gl_draw.cpp DENG_EXTERN_C void GL_UseFog(int yes); DENG_EXTERN_C void GL_SetFilter(dd_bool enable); DENG_EXTERN_C void GL_SetFilterColor(float r, float g, float b, float a); DENG_EXTERN_C void GL_ConfigureBorderedProjection2(dgl_borderedprojectionstate_t* bp, int flags, int width, int height, int availWidth, int availHeight, scalemode_t overrideMode, float stretchEpsilon); DENG_EXTERN_C void GL_ConfigureBorderedProjection(dgl_borderedprojectionstate_t* bp, int flags, int width, int height, int availWidth, int availHeight, scalemode_t overrideMode); DENG_EXTERN_C void GL_BeginBorderedProjection(dgl_borderedprojectionstate_t* bp); DENG_EXTERN_C void GL_EndBorderedProjection(dgl_borderedprojectionstate_t* bp); DENG_EXTERN_C void GL_ResetViewEffects(); DENG_DECLARE_API(GL) = { { DE_API_GL }, DGL_Enable, DGL_Disable, DGL_PushState, DGL_PopState, DGL_GetIntegerv, DGL_GetInteger, DGL_SetInteger, DGL_GetFloatv, DGL_GetFloat, DGL_SetFloat, DGL_Ortho, DGL_SetScissor, DGL_SetScissor2, DGL_MatrixMode, DGL_PushMatrix, DGL_PopMatrix, DGL_LoadIdentity, DGL_Translatef, DGL_Rotatef, DGL_Scalef, DGL_Begin, DGL_End, DGL_NewList, DGL_EndList, DGL_CallList, DGL_DeleteLists, DGL_SetNoMaterial, DGL_SetMaterialUI, DGL_SetPatch, DGL_SetPSprite, DGL_SetPSprite2, DGL_SetRawImage, DGL_BlendOp, DGL_BlendFunc, DGL_BlendMode, DGL_Color3ub, DGL_Color3ubv, DGL_Color4ub, DGL_Color4ubv, DGL_Color3f, DGL_Color3fv, DGL_Color4f, DGL_Color4fv, DGL_TexCoord2f, DGL_TexCoord2fv, DGL_Vertex2f, DGL_Vertex2fv, DGL_Vertex3f, DGL_Vertex3fv, DGL_Vertices2ftv, DGL_Vertices3ftv, DGL_Vertices3fctv, DGL_DrawLine, DGL_DrawRect, DGL_DrawRect2, DGL_DrawRectf, DGL_DrawRectf2, DGL_DrawRectf2Color, DGL_DrawRectf2Tiled, DGL_DrawCutRectfTiled, DGL_DrawCutRectf2Tiled, DGL_DrawQuadOutline, DGL_DrawQuad2Outline, DGL_NewTextureWithParams, DGL_Bind, DGL_DeleteTextures, GL_UseFog, GL_SetFilter, GL_SetFilterColor, GL_ConfigureBorderedProjection2, GL_ConfigureBorderedProjection, GL_BeginBorderedProjection, GL_EndBorderedProjection, GL_ResetViewEffects }; doomsday-stable-1.15.7/doomsday/client/src/gl/gl_tex.cpp0000664000175000017500000012157012641367670022456 0ustar jaakkojaakko/**\file gl_tex.cpp * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "gl/gl_tex.h" #include "dd_main.h" #include "color.h" #include "render/r_main.h" #include "resource/resourcesystem.h" #include "resource/colorpalette.h" #include "gl/sys_opengl.h" #include #include #include #include #include #include static uint8_t *scratchBuffer; static size_t scratchBufferSize; /** * Provides a persistent scratch buffer for use by texture manipulation * routines e.g. scaleLine(). */ static uint8_t *GetScratchBuffer(size_t size) { // Need to enlarge? if(size > scratchBufferSize) { scratchBuffer = (uint8_t *) Z_Realloc(scratchBuffer, size, PU_APPSTATIC); scratchBufferSize = size; } return scratchBuffer; } /** * Len is measured in out units. Comps is the number of components per * pixel, or rather the number of bytes per pixel (3 or 4). The strides must * be byte-aligned anyway, though; not in pixels. * * @todo Probably could be optimized. */ static void scaleLine(const uint8_t* in, int inStride, uint8_t* out, int outStride, int outLen, int inLen, int comps) { float inToOutScale = outLen / (float) inLen; int i, c; if(inToOutScale > 1) { // Magnification is done using linear interpolation. fixed_t inPosDelta = (FRACUNIT * (inLen - 1)) / (outLen - 1); fixed_t inPos = inPosDelta; const uint8_t* col1, *col2; int weight, invWeight; // The first pixel. memcpy(out, in, comps); out += outStride; // Step at each out pixel between the first and last ones. for(i = 1; i < outLen - 1; ++i, out += outStride, inPos += inPosDelta) { col1 = in + (inPos >> FRACBITS) * inStride; col2 = col1 + inStride; weight = inPos & 0xffff; invWeight = 0x10000 - weight; for(c = 0; c < comps; ++c) out[c] = (uint8_t)((col1[c] * invWeight + col2[c] * weight) >> 16); } // The last pixel. memcpy(out, in + (inLen - 1) * inStride, comps); return; } if(inToOutScale < 1) { // Minification needs to calculate the average of each of // the pixels contained by the out pixel. uint cumul[4] = { 0, 0, 0, 0 }, count = 0; int outpos = 0; for(i = 0; i < inLen; ++i, in += inStride) { if((int) (i * inToOutScale) != outpos) { outpos = (int) (i * inToOutScale); for(c = 0; c < comps; ++c) { out[c] = (uint8_t)(cumul[c] / count); cumul[c] = 0; } count = 0; out += outStride; } for(c = 0; c < comps; ++c) cumul[c] += in[c]; count++; } // Fill in the last pixel, too. if(count) for(c = 0; c < comps; ++c) out[c] = (uint8_t)(cumul[c] / count); return; } // No need for scaling. for(i = outLen; i > 0; i--, out += outStride, in += inStride) { for(c = 0; c < comps; ++c) out[c] = in[c]; } } /// \todo Avoid use of a secondary buffer by scaling directly to output. uint8_t* GL_ScaleBuffer(const uint8_t* in, int width, int height, int comps, int outWidth, int outHeight) { assert(in); { uint inOffsetSize, outOffsetSize; uint8_t* outOff, *buffer; const uint8_t* inOff; uint8_t* out; int stride; if(width <= 0 || height <= 0) return (uint8_t*)in; buffer = GetScratchBuffer(comps * outWidth * height); if(0 == (out = (uint8_t*) malloc(comps * outWidth * outHeight))) App_Error("GL_ScaleBuffer: Failed on allocation of %lu bytes for " "output buffer.", (unsigned long) (comps * outWidth * outHeight)); // First scale horizontally, to outWidth, into the temporary buffer. inOff = in; outOff = buffer; inOffsetSize = width * comps; outOffsetSize = outWidth * comps; { int i; for(i = 0; i < height; ++i, inOff += inOffsetSize, outOff += outOffsetSize) { scaleLine(inOff, comps, outOff, comps, outWidth, width, comps); }} // Then scale vertically, to outHeight, into the out buffer. inOff = buffer; outOff = out; stride = outWidth * comps; inOffsetSize = comps; outOffsetSize = comps; { int i; for(i = 0; i < outWidth; ++i, inOff += inOffsetSize, outOff += outOffsetSize) { scaleLine(inOff, stride, outOff, stride, outHeight, height, comps); }} return out; } } static void* packImage(int components, const float* tempOut, GLint typeOut, int widthOut, int heightOut, int sizeOut, int bpp, int packRowLength, int packAlignment, int packSkipRows, int packSkipPixels) { int rowStride, rowLen; void* dataOut; if(NULL == (dataOut = malloc(bpp * widthOut * heightOut))) App_Error("scaleImage: Failed on allocation of %lu bytes for output " "buffer.", (unsigned long) (bpp * widthOut * heightOut)); if(packRowLength > 0) { rowLen = packRowLength; } else { rowLen = widthOut; } if(sizeOut >= packAlignment) { rowStride = components * rowLen; } else { rowStride = packAlignment / sizeOut * CEILING(components * rowLen * sizeOut, packAlignment); } switch(typeOut) { case GL_UNSIGNED_BYTE: { int i, j, k = 0; for(i = 0; i < heightOut; ++i) { GLubyte* ubptr = (GLubyte*) dataOut + i * rowStride + packSkipRows * rowStride + packSkipPixels * components; for(j = 0; j < widthOut * components; ++j) { *ubptr++ = (GLubyte) tempOut[k++]; } } break; } case GL_BYTE: { int i, j, k = 0; for(i = 0; i < heightOut; i++) { GLbyte* bptr = (GLbyte*) dataOut + i * rowStride + packSkipRows * rowStride + packSkipPixels * components; for(j = 0; j < widthOut * components; ++j) { *bptr++ = (GLbyte) tempOut[k++]; } } break; } case GL_UNSIGNED_SHORT: { int i, j, k = 0; for(i = 0; i < heightOut; ++i) { GLushort* usptr = (GLushort*) dataOut + i * rowStride + packSkipRows * rowStride + packSkipPixels * components; for(j = 0; j < widthOut * components; ++j) { *usptr++ = (GLushort) tempOut[k++]; } } break; } case GL_SHORT: { int i, j, k = 0; for(i = 0; i < heightOut; ++i) { GLshort* sptr = (GLshort*) dataOut + i * rowStride + packSkipRows * rowStride + packSkipPixels * components; for(j = 0; j < widthOut * components; ++j) { *sptr++ = (GLshort) tempOut[k++]; } } break; } case GL_UNSIGNED_INT: { int i, j, k = 0; for(i = 0; i < heightOut; ++i) { GLuint* uiptr = (GLuint*) dataOut + i * rowStride + packSkipRows * rowStride + packSkipPixels * components; for(j = 0; j < widthOut * components; ++j) { *uiptr++ = (GLuint) tempOut[k++]; } } break; } case GL_INT: { int i, j, k = 0; for(i = 0; i < heightOut; ++i) { GLint* iptr = (GLint*) dataOut + i * rowStride + packSkipRows * rowStride + packSkipPixels * components; for(j = 0; j < widthOut * components; ++j) { *iptr++ = (GLint) tempOut[k++]; } } break; } case GL_FLOAT: { int i, j, k = 0; for(i = 0; i < heightOut; ++i) { GLfloat* fptr = (GLfloat*) dataOut + i * rowStride + packSkipRows * rowStride + packSkipPixels * components; for (j = 0; j < widthOut * components; ++j) { *fptr++ = tempOut[k++]; } } break; } default: DENG_ASSERT(!"packImage: Unknown output type"); return 0; } return dataOut; } /** * Originally from the Mesa 3-D graphics library version 3.4 * @note License: GNU Library General Public License (or later) * Copyright (C) 1995-2000 Brian Paul. */ void* GL_ScaleBufferEx(const void* dataIn, int widthIn, int heightIn, int bpp, /*GLint typeIn,*/ int unpackRowLength, int unpackAlignment, int unpackSkipRows, int unpackSkipPixels, int widthOut, int heightOut, /*GLint typeOut, */ int packRowLength, int packAlignment, int packSkipRows, int packSkipPixels) { const GLint typeIn = GL_UNSIGNED_BYTE, typeOut = GL_UNSIGNED_BYTE; int i, j, k, sizeIn, sizeOut, rowStride, rowLen; float* tempIn, *tempOut; float sx, sy; void* dataOut; // Determine bytes per input datum. switch(typeIn) { case GL_UNSIGNED_BYTE: sizeIn = sizeof(GLubyte); break; case GL_BYTE: sizeIn = sizeof(GLbyte); break; case GL_UNSIGNED_SHORT: sizeIn = sizeof(GLushort); break; case GL_SHORT: sizeIn = sizeof(GLshort); break; case GL_UNSIGNED_INT: sizeIn = sizeof(GLuint); break; case GL_INT: sizeIn = sizeof(GLint); break; case GL_FLOAT: sizeIn = sizeof(GLfloat); break; case GL_BITMAP: // Not implemented yet. default: return NULL; } // Determine bytes per output datum. switch(typeOut) { case GL_UNSIGNED_BYTE: sizeOut = sizeof(GLubyte); break; case GL_BYTE: sizeOut = sizeof(GLbyte); break; case GL_UNSIGNED_SHORT: sizeOut = sizeof(GLushort); break; case GL_SHORT: sizeOut = sizeof(GLshort); break; case GL_UNSIGNED_INT: sizeOut = sizeof(GLuint); break; case GL_INT: sizeOut = sizeof(GLint); break; case GL_FLOAT: sizeOut = sizeof(GLfloat); break; case GL_BITMAP: // Not implemented yet. default: return NULL; } // Allocate storage for intermediate images. if(NULL == (tempIn = (float*) malloc(widthIn * heightIn * bpp * sizeof(float)))) App_Error("scaleImage: Failed on allocation of %lu bytes for in buffer.", (unsigned long) (widthIn * heightIn * bpp * sizeof(float))); if(NULL == (tempOut = (float*) malloc(widthOut * heightOut * bpp * sizeof(float)))) { free(tempIn); App_Error("scaleImage: Failed on allocation of %lu bytes for out buffer.", (unsigned long) (widthOut * heightOut * bpp * sizeof(float))); } /** * Unpack the pixel data and convert to floating point */ if(unpackRowLength > 0) { rowLen = unpackRowLength; } else { rowLen = widthIn; } if(sizeIn >= unpackAlignment) { rowStride = bpp * rowLen; } else { rowStride = unpackAlignment / sizeIn * CEILING(bpp * rowLen * sizeIn, unpackAlignment); } switch(typeIn) { case GL_UNSIGNED_BYTE: k = 0; for(i = 0; i < heightIn; ++i) { GLubyte* ubptr = (GLubyte*) dataIn + i * rowStride + unpackSkipRows * rowStride + unpackSkipPixels * bpp; for(j = 0; j < widthIn * bpp; ++j) { tempIn[k++] = (float) *ubptr++; } } break; case GL_BYTE: k = 0; for(i = 0; i < heightIn; ++i) { GLbyte* bptr = (GLbyte*) dataIn + i * rowStride + unpackSkipRows * rowStride + unpackSkipPixels * bpp; for(j = 0; j < widthIn * bpp; ++j) { tempIn[k++] = (float) *bptr++; } } break; case GL_UNSIGNED_SHORT: k = 0; for(i = 0; i < heightIn; ++i) { GLushort* usptr = (GLushort*) dataIn + i * rowStride + unpackSkipRows * rowStride + unpackSkipPixels * bpp; for(j = 0; j < widthIn * bpp; ++j) { tempIn[k++] = (float) *usptr++; } } break; case GL_SHORT: k = 0; for(i = 0; i < heightIn; ++i) { GLshort* sptr = (GLshort*) dataIn + i * rowStride + unpackSkipRows * rowStride + unpackSkipPixels * bpp; for(j = 0; j < widthIn * bpp; ++j) { tempIn[k++] = (float) *sptr++; } } break; case GL_UNSIGNED_INT: k = 0; for(i = 0; i < heightIn; ++i) { GLuint* uiptr = (GLuint*) dataIn + i * rowStride + unpackSkipRows * rowStride + unpackSkipPixels * bpp; for(j = 0; j < widthIn * bpp; ++j) { tempIn[k++] = (float) *uiptr++; } } break; case GL_INT: k = 0; for(i = 0; i < heightIn; ++i) { GLint* iptr = (GLint*) dataIn + i * rowStride + unpackSkipRows * rowStride + unpackSkipPixels * bpp; for(j = 0; j < widthIn * bpp; ++j) { tempIn[k++] = (float) *iptr++; } } break; case GL_FLOAT: k = 0; for(i = 0; i < heightIn; ++i) { GLfloat* fptr = (GLfloat*) dataIn + i * rowStride + unpackSkipRows * rowStride + unpackSkipPixels * bpp; for(j = 0; j < widthIn * bpp; ++j) { tempIn[k++] = *fptr++; } } break; default: return 0; } /** * Scale the image! */ if(widthOut > 1) sx = (float) (widthIn - 1) / (float) (widthOut - 1); else sx = (float) (widthIn - 1); if(heightOut > 1) sy = (float) (heightIn - 1) / (float) (heightOut - 1); else sy = (float) (heightIn - 1); if(sx < 1.0 && sy < 1.0) { // Magnify both width and height: use weighted sample of 4 pixels. int i0, i1, j0, j1; float alpha, beta; float* src00, *src01, *src10, *src11; float s1, s2; float* dst; for(i = 0; i < heightOut; ++i) { i0 = i * sy; i1 = i0 + 1; if(i1 >= heightIn) i1 = heightIn - 1; alpha = i * sy - i0; for(j = 0; j < widthOut; ++j) { j0 = j * sx; j1 = j0 + 1; if(j1 >= widthIn) j1 = widthIn - 1; beta = j * sx - j0; // Compute weighted average of pixels in rect (i0,j0)-(i1,j1) src00 = tempIn + (i0 * widthIn + j0) * bpp; src01 = tempIn + (i0 * widthIn + j1) * bpp; src10 = tempIn + (i1 * widthIn + j0) * bpp; src11 = tempIn + (i1 * widthIn + j1) * bpp; dst = tempOut + (i * widthOut + j) * bpp; for (k = 0; k < bpp; ++k) { s1 = *src00++ * (1.0 - beta) + *src01++ * beta; s2 = *src10++ * (1.0 - beta) + *src11++ * beta; *dst++ = s1 * (1.0 - alpha) + s2 * alpha; } } } } else { // Shrink width and/or height: use an unweighted box filter. int i0, i1; int j0, j1; int ii, jj; float sum, *dst; for(i = 0; i < heightOut; ++i) { i0 = i * sy; i1 = i0 + 1; if(i1 >= heightIn) i1 = heightIn - 1; for(j = 0; j < widthOut; ++j) { j0 = j * sx; j1 = j0 + 1; if(j1 >= widthIn) j1 = widthIn - 1; dst = tempOut + (i * widthOut + j) * bpp; // Compute average of pixels in the rectangle (i0,j0)-(i1,j1) for(k = 0; k < bpp; ++k) { sum = 0.0; for(ii = i0; ii <= i1; ++ii) { for(jj = j0; jj <= j1; ++jj) { sum += *(tempIn + (ii * widthIn + jj) * bpp + k); } } sum /= (j1 - j0 + 1) * (i1 - i0 + 1); *dst++ = sum; } } } } // Free temporary image storage. free(tempIn); /** * Return output image. */ dataOut = packImage(bpp, tempOut, typeOut, widthOut, heightOut, sizeOut, bpp, packRowLength, packAlignment, packSkipRows, packSkipPixels); // Free temporary image storage. free(tempOut); return dataOut; } uint8_t* GL_ScaleBufferNearest(const uint8_t* in, int width, int height, int comps, int outWidth, int outHeight) { assert(in); { int ratioX, ratioY, shearY; uint8_t* out, *outP; if(width <= 0 || height <= 0) return (uint8_t*)in; ratioX = (int)(width << 16) / outWidth + 1; ratioY = (int)(height << 16) / outHeight + 1; if(NULL == (out = (uint8_t *) M_Malloc(comps * outWidth * outHeight))) App_Error("GL_ScaleBufferNearest: Failed on allocation of %lu bytes for " "output buffer.", (unsigned long) (comps * outWidth * outHeight)); outP = out; shearY = 0; { int i; for(i = 0; i < outHeight; ++i, shearY += ratioY) { int shearX = 0; int shearY2 = (shearY >> 16) * width; { int j; for(j = 0; j < outWidth; ++j, outP += comps, shearX += ratioX) { int c, n = (shearY2 + (shearX >> 16)) * comps; for(c = 0; c < comps; ++c, n++) outP[c] = in[n]; }} }} return out; } } void GL_DownMipmap32(uint8_t* in, int width, int height, int comps) { assert(in); { int x, y, c, outW = width >> 1, outH = height >> 1; uint8_t* out; if(width <= 0 || height <= 0 || comps <= 0) return; if(width == 1 && height == 1) { #if _DEBUG App_Error("GL_DownMipmap32: Can't be called for a 1x1 image.\n"); #endif return; } // Limited, 1x2|2x1 -> 1x1 reduction? if(!outW || !outH) { int outDim = (width > 1 ? outW : outH); out = in; for(x = 0; x < outDim; ++x, in += comps * 2) for(c = 0; c < comps; ++c, out++) *out = (uint8_t)((in[c] + in[comps + c]) >> 1); return; } // Unconstrained, 2x2 -> 1x1 reduction? out = in; for(y = 0; y < outH; ++y, in += width * comps) for(x = 0; x < outW; ++x, in += comps * 2) for(c = 0; c < comps; ++c, out++) *out = (uint8_t)((in[c] + in[comps + c] + in[comps * width + c] + in[comps * (width + 1) + c]) >> 2); } } void GL_DownMipmap8(uint8_t* in, uint8_t* fadedOut, int width, int height, float fade) { int x, y, outW = width / 2, outH = height / 2; float invFade; byte* out = in; if(fade > 1) fade = 1; invFade = 1 - fade; if(width == 1 && height == 1) { #if _DEBUG App_Error("GL_DownMipmap8: Can't be called for a 1x1 image.\n"); #endif return; } if(!outW || !outH) { // Limited, 1x2|2x1 -> 1x1 reduction? int outDim = (width > 1 ? outW : outH); for(x = 0; x < outDim; x++, in += 2) { *out = (in[0] + in[1]) / 2; *fadedOut++ = (byte) (*out * invFade + 0x80 * fade); out++; } } else { // Unconstrained, 2x2 -> 1x1 reduction? for(y = 0; y < outH; y++, in += width) for(x = 0; x < outW; x++, in += 2) { *out = (in[0] + in[1] + in[width] + in[width + 1]) / 4; *fadedOut++ = (byte) (*out * invFade + 0x80 * fade); out++; } } } dd_bool GL_PalettizeImage(uint8_t *out, int outformat, ColorPalette const *palette, dd_bool applyTexGamma, uint8_t const *in, int informat, int width, int height) { DENG2_ASSERT(in && out && palette); if(width <= 0 || height <= 0) return false; if(informat <= 2 && outformat >= 3) { long const numPels = width * height; int const inSize = (informat == 2 ? 1 : informat); int const outSize = (outformat == 2 ? 1 : outformat); for(long i = 0; i < numPels; ++i) { de::Vector3ub palColor = palette->color(*in); out[0] = palColor.x; out[1] = palColor.y; out[2] = palColor.z; if(applyTexGamma) { out[0] = texGammaLut[out[0]]; out[1] = texGammaLut[out[1]]; out[2] = texGammaLut[out[2]]; } if(outformat == 4) { if(informat == 2) out[3] = in[numPels * inSize]; else out[3] = 0; } in += inSize; out += outSize; } return true; } return false; } dd_bool GL_QuantizeImageToPalette(uint8_t *out, int outformat, ColorPalette const *palette, uint8_t const *in, int informat, int width, int height) { DENG2_ASSERT(out != 0 && in != 0 && palette != 0); if(informat >= 3 && outformat <= 2 && width > 0 && height > 0) { int inSize = (informat == 2 ? 1 : informat); int outSize = (outformat == 2 ? 1 : outformat); int i, numPixels = width * height; for(i = 0; i < numPixels; ++i, in += inSize, out += outSize) { // Convert the color value. *out = palette->nearestIndex(de::Vector3ub(in)); // Alpha channel? if(outformat == 2) { if(informat == 4) out[numPixels * outSize] = in[3]; else out[numPixels * outSize] = 0; } } return true; } return false; } void GL_DeSaturatePalettedImage(uint8_t *pixels, ColorPalette const &palette, int width, int height) { DENG2_ASSERT(pixels != 0); if(!width || !height) return; long const numPels = width * height; // What is the maximum color value? int max = 0; for(long i = 0; i < numPels; ++i) { de::Vector3ub palColor = palette[pixels[i]]; if(palColor.x == palColor.y && palColor.x == palColor.z) { if(palColor.x > max) { max = palColor.x; } continue; } int temp = (2 * int( palColor.x ) + 4 * int( palColor.y ) + 3 * int( palColor.z )) / 9; if(temp > max) max = temp; } for(long i = 0; i < numPels; ++i) { de::Vector3ub palColor = palette[pixels[i]]; if(palColor.x == palColor.y && palColor.x == palColor.z) { continue; } // Calculate a weighted average. int temp = (2 * int( palColor.x ) + 4 * int( palColor.y ) + 3 * int( palColor.z )) / 9; if(max) temp *= 255.f / max; pixels[i] = palette.nearestIndex(de::Vector3ub(temp, temp, temp)); } } void FindAverageLineColorIdx(uint8_t const *data, int w, int h, int line, ColorPalette const &palette, dd_bool hasAlpha, ColorRawf *color) { DENG2_ASSERT(data != 0 && color != 0); long i, count, numpels, avg[3] = { 0, 0, 0 }; uint8_t const *start, *alphaStart; if(w <= 0 || h <= 0) { V3f_Set(color->rgb, 0, 0, 0); return; } if(line >= h) { #if _DEBUG App_Error("FindAverageLineColorIdx: Attempted to average outside valid area (height=%i line=%i).", h, line); #endif V3f_Set(color->rgb, 0, 0, 0); return; } numpels = w * h; start = data + w * line; alphaStart = data + numpels + w * line; count = 0; for(i = 0; i < w; ++i) { if(!hasAlpha || alphaStart[i]) { de::Vector3ub palColor = palette[start[i]]; avg[0] += palColor.x; avg[1] += palColor.y; avg[2] += palColor.z; ++count; } } // All transparent? Sorry... if(!count) return; V3f_Set(color->rgb, avg[0] / count * reciprocal255, avg[1] / count * reciprocal255, avg[2] / count * reciprocal255); } void FindAverageLineColor(const uint8_t* pixels, int width, int height, int pixelSize, int line, ColorRawf* color) { long avg[3] = { 0, 0, 0 }; const uint8_t* src; int i; assert(pixels && color); if(width <= 0 || height <= 0) { V3f_Set(color->rgb, 0, 0, 0); return; } if(line >= height) { #if _DEBUG App_Error("FindAverageLineColor: Attempted to average outside valid area (height=%i line=%i).", height, line); #endif V3f_Set(color->rgb, 0, 0, 0); return; } src = pixels + pixelSize * width * line; for(i = 0; i < width; ++i, src += pixelSize) { avg[0] += src[0]; avg[1] += src[1]; avg[2] += src[2]; } V3f_Set(color->rgb, avg[0] / width * reciprocal255, avg[1] / width * reciprocal255, avg[2] / width * reciprocal255); } void FindAverageColor(const uint8_t* pixels, int width, int height, int pixelSize, ColorRawf* color) { long i, numpels, avg[3] = { 0, 0, 0 }; const uint8_t* src; assert(pixels && color); if(width <= 0 || height <= 0) { V3f_Set(color->rgb, 0, 0, 0); return; } if(pixelSize != 3 && pixelSize != 4) { #if _DEBUG App_Error("FindAverageColor: Attempted on non-rgb(a) image (pixelSize=%i).", pixelSize); #endif V3f_Set(color->rgb, 0, 0, 0); return; } numpels = width * height; src = pixels; for(i = 0; i < numpels; ++i, src += pixelSize) { avg[0] += src[0]; avg[1] += src[1]; avg[2] += src[2]; } V3f_Set(color->rgb, avg[0] / numpels * reciprocal255, avg[1] / numpels * reciprocal255, avg[2] / numpels * reciprocal255); } void FindAverageColorIdx(uint8_t const *data, int w, int h, ColorPalette const &palette, dd_bool hasAlpha, ColorRawf *color) { DENG2_ASSERT(data != 0 && color != 0); long i, numpels, count, avg[3] = { 0, 0, 0 }; uint8_t const *alphaStart; if(w <= 0 || h <= 0) { V3f_Set(color->rgb, 0, 0, 0); return; } numpels = w * h; alphaStart = data + numpels; count = 0; for(i = 0; i < numpels; ++i) { if(!hasAlpha || alphaStart[i]) { de::Vector3ub palColor = palette[data[i]]; avg[0] += palColor.x; avg[1] += palColor.y; avg[2] += palColor.z; ++count; } } // All transparent? Sorry... if(0 == count) return; V3f_Set(color->rgb, avg[0] / count * reciprocal255, avg[1] / count * reciprocal255, avg[2] / count * reciprocal255); } void FindAverageAlpha(const uint8_t* pixels, int width, int height, int pixelSize, float* alpha, float* coverage) { long i, numPels, avg = 0, alphaCount = 0; const uint8_t* src; if(!pixels || !alpha) return; if(width <= 0 || height <= 0) { // Transparent. *alpha = 0; if(coverage) *coverage = 1; return; } if(pixelSize != 3 && pixelSize != 4) { #if _DEBUG App_Error("FindAverageAlpha: Attempted on non-rgb(a) image (pixelSize=%i).", pixelSize); #endif // Assume opaque. *alpha = 1; if(coverage) *coverage = 0; return; } if(pixelSize == 3) { // Opaque. Well that was easy... *alpha = 1; if(coverage) *coverage = 0; return; } numPels = width * height; src = pixels; for(i = 0; i < numPels; ++i, src += 4) { const uint8_t val = src[3]; avg += val; if(val < 255) alphaCount++; } *alpha = avg / numPels * reciprocal255; // Calculate coverage? if(coverage) *coverage = (float)alphaCount / numPels; } void FindAverageAlphaIdx(uint8_t const *pixels, int w, int h, float *alpha, float *coverage) { long i, numPels, avg = 0, alphaCount = 0; uint8_t const *alphaStart; if(!pixels || !alpha) return; if(w <= 0 || h <= 0) { // Transparent. *alpha = 0; if(coverage) *coverage = 1; return; } numPels = w * h; alphaStart = pixels + numPels; for(i = 0; i < numPels; ++i) { uint8_t const val = alphaStart[i]; avg += val; if(val < 255) { alphaCount++; } } *alpha = avg / numPels * reciprocal255; // Calculate coverage? if(coverage) *coverage = (float)alphaCount / numPels; } void FindClipRegionNonAlpha(const uint8_t* buffer, int width, int height, int pixelsize, int retRegion[4]) { assert(buffer && retRegion); { const uint8_t* src, *alphasrc; int region[4]; if(width <= 0 || height <= 0) { #if _DEBUG App_Error("FindClipRegionNonAlpha: Attempt to find region on zero-sized image."); #endif retRegion[0] = retRegion[1] = retRegion[2] = retRegion[3] = 0; return; } region[0] = width; region[1] = 0; region[2] = height; region[3] = 0; src = buffer; // For paletted images the alpha channel follows the actual image. if(pixelsize == 1) alphasrc = buffer + width * height; else alphasrc = NULL; // \todo This is not very efficent. Better to use an algorithm which works // on full rows and full columns. { int k, i; for(k = 0; k < height; ++k) for(i = 0; i < width; ++i, src += pixelsize, alphasrc++) { // Alpha pixels don't count. if(pixelsize == 1) { if(*alphasrc < 255) continue; } else if(pixelsize == 4) { if(src[3] < 255) continue; } if(i < region[0]) region[0] = i; if(i > region[1]) region[1] = i; if(k < region[2]) region[2] = k; if(k > region[3]) region[3] = k; } } retRegion[0] = region[0]; retRegion[1] = region[1]; retRegion[2] = region[2]; retRegion[3] = region[3]; } } #if 0 void BlackOutlines(uint8_t* pixels, int width, int height) { assert(pixels); { uint16_t* dark; int x, y, a, b; uint8_t* pix; long numpels; if(width <= 0 || height <= 0) return; numpels = width * height; dark = calloc(1, 2 * numpels); for(y = 1; y < height - 1; ++y) { for(x = 1; x < width - 1; ++x) { pix = pixels + (x + y * width) * 4; if(pix[3] > 128) // Not transparent. { // Apply darkness to surrounding transparent pixels. for(a = -1; a <= 1; ++a) { for(b = -1; b <= 1; ++b) { uint8_t* other = pix + (a + b * width) * 4; if(other[3] < 128) // Transparent. { dark[(x + a) + (y + b) * width] += 40; } } } } } } // Apply the darkness. { long i; for(i = 0, pix = pixels; i < numpels; ++i, pix += 4) { pix[3] = MIN_OF(255, (int)(pix[3] + dark[a])); }} // Release temporary storage. free(dark); } } #endif void ColorOutlinesIdx(uint8_t* buffer, int width, int height) { DENG_ASSERT(buffer); const int numpels = width * height; uint8_t* w[5]; int x, y; if(width <= 0 || height <= 0) return; // +----+ // | w0 | // +----+----+----+ // | w1 | w2 | w3 | // +----+----+----+ // | w4 | // +----+ /// @todo Not a very efficient algorithm... for(y = 0; y < height; ++y) { for(x = 0; x < width; ++x) { // Only solid pixels spread. if(!buffer[numpels + x + y * width]) continue; w[2] = buffer + x + y * width; w[0] = buffer + x + (y + (y == 0? 0 : -1)) * width; w[4] = buffer + x + (y + (y == height-1? 0 : 1)) * width; w[1] = buffer + x + (x == 0? 0 : -1) + (y) * width; w[3] = buffer + x + (x == width-1? 0 : 1) + (y) * width; if(w[0] != w[2] && !(*(w[0]+numpels))) *(w[0]) = *(w[2]); if(w[4] != w[2] && !(*(w[4]+numpels))) *(w[4]) = *(w[2]); if(w[1] != w[2] && !(*(w[1]+numpels))) *(w[1]) = *(w[2]); if(w[3] != w[2] && !(*(w[3]+numpels))) *(w[3]) = *(w[2]); } } } void EqualizeLuma(uint8_t* pixels, int width, int height, float* rBaMul, float* rHiMul, float* rLoMul) { assert(pixels); { float hiMul, loMul, baMul; long wideAvg, numpels; uint8_t min, max, avg; uint8_t* pix; if(width <= 0 || height <= 0) return; numpels = width * height; min = 255; max = 0; wideAvg = 0; { long i; for(i = 0, pix = pixels; i < numpels; ++i, pix += 1) { if(*pix < min) min = *pix; if(*pix > max) max = *pix; wideAvg += *pix; }} if(max <= min || max == 0 || min == 255) { if(rBaMul) *rBaMul = -1; if(rHiMul) *rHiMul = -1; if(rLoMul) *rLoMul = -1; return; // Nothing we can do. } avg = MIN_OF(255, wideAvg / numpels); // Allow a small margin of variance with the balance multiplier. baMul = (!INRANGE_OF(avg, 127, 4)? (float)127/avg : 1); if(baMul != 1) { if(max < 255) max = (uint8_t) MINMAX_OF(1, (float)max - (255-max) * baMul, 255); if(min > 0) min = (uint8_t) MINMAX_OF(0, (float)min + min * baMul, 255); } hiMul = (max < 255? (float)255/max : 1); loMul = (min > 0 ? 1-((float)min/255) : 1); if(!(baMul == 1 && hiMul == 1 && loMul == 1)) { long i; for(i = 0, pix = pixels; i < numpels; ++i, pix += 1) { // First balance. float val = baMul * (*pix); // Now amplify. if(val > 127) val *= hiMul; else val *= loMul; *pix = (uint8_t) MINMAX_OF(0, val, 255); } } if(rBaMul) *rBaMul = baMul; if(rHiMul) *rHiMul = hiMul; if(rLoMul) *rLoMul = loMul; } } void Desaturate(uint8_t* pixels, int width, int height, int comps) { assert(pixels); { uint8_t* pix; long i, numpels; if(width <= 0 || height <= 0) return; numpels = width * height; for(i = 0, pix = pixels; i < numpels; ++i, pix += comps) { int min = MIN_OF(pix[0], MIN_OF(pix[1], pix[2])); int max = MAX_OF(pix[0], MAX_OF(pix[1], pix[2])); pix[0] = pix[1] = pix[2] = (min + max) / 2; } } } void AmplifyLuma(uint8_t* pixels, int width, int height, dd_bool hasAlpha) { assert(pixels); { long numPels; uint8_t max = 0; if(width <= 0 || height <= 0) return; numPels = width * height; if(hasAlpha) { uint8_t* pix = pixels; uint8_t* apix = pixels + numPels; long i; for(i = 0; i < numPels; ++i, pix++, apix++) { // Only non-masked pixels count. if(!(*apix > 0)) continue; if(*pix > max) max = *pix; } } else { uint8_t* pix = pixels; long i; for(i = 0; i < numPels; ++i, pix++) { if(*pix > max) max = *pix; } } if(0 == max || 255 == max) return; { uint8_t* pix = pixels; long i; for(i = 0; i < numPels; ++i, pix++) { *pix = (uint8_t) MINMAX_OF(0, (float)*pix / max * 255, 255); }} } } void EnhanceContrast(uint8_t* pixels, int width, int height, int comps) { assert(pixels); { uint8_t* pix; long i, numpels; if(width <= 0 || height <= 0) return; if(comps != 3 && comps != 4) { #if _DEBUG App_Error("EnhanceContrast: Attempted on non-rgb(a) image (comps=%i).", comps); #endif return; } pix = pixels; numpels = width * height; for(i = 0; i < numpels; ++i, pix += comps) { int c; for(c = 0; c < 3; ++c) { uint8_t out; if(pix[c] < 60) // Darken dark parts. out = (uint8_t) MINMAX_OF(0, ((float)pix[c] - 70) * 1.0125f + 70, 255); else if(pix[c] > 185) // Lighten light parts. out = (uint8_t) MINMAX_OF(0, ((float)pix[c] - 185) * 1.0125f + 185, 255); else out = pix[c]; pix[c] = out; } } } } void SharpenPixels(uint8_t* pixels, int width, int height, int comps) { assert(pixels); { const float strength = .05f; uint8_t* result; float A, B, C; int x, y; if(width <= 0 || height <= 0) return; if(comps != 3 && comps != 4) { #if _DEBUG App_Error("EnhanceContrast: Attempted on non-rgb(a) image (comps=%i).", comps); #endif return; } result = (uint8_t *) M_Calloc(comps * width * height); A = strength; B = .70710678 * strength; // 1/sqrt(2) C = 1 + 4*A + 4*B; for(y = 1; y < height - 1; ++y) for(x = 1; x < width -1; ++x) { const uint8_t* pix = pixels + (x + y*width) * comps; uint8_t* out = result + (x + y*width) * comps; int c; for(c = 0; c < 3; ++c) { int r = (C*pix[c] - A*pix[c - width] - A*pix[c + comps] - A*pix[c - comps] - A*pix[c + width] - B*pix[c + comps - width] - B*pix[c + comps + width] - B*pix[c - comps - width] - B*pix[c - comps + width]); out[c] = MINMAX_OF(0, r, 255); } if(comps == 4) out[3] = pix[3]; } memcpy(pixels, result, comps * width * height); free(result); } } /** * @return @c true, if the given color is either (0,255,255) or (255,0,255). */ static inline bool isKeyedColor(uint8_t *color) { DENG2_ASSERT(color); return color[2] == 0xff && ((color[0] == 0xff && color[1] == 0) || (color[0] == 0 && color[1] == 0xff)); } /** * Buffer must be RGBA. Doesn't touch the non-keyed pixels. */ static void doColorKeying(uint8_t *rgbaBuf, int width) { DENG2_ASSERT(rgbaBuf); for(int i = 0; i < width; ++i, rgbaBuf += 4) { if(!isKeyedColor(rgbaBuf)) continue; rgbaBuf[3] = rgbaBuf[2] = rgbaBuf[1] = rgbaBuf[0] = 0; } } uint8_t *ApplyColorKeying(uint8_t *buf, int width, int height, int pixelSize) { DENG2_ASSERT(buf); if(width <= 0 || height <= 0) return buf; // We must allocate a new buffer if the loaded image has less than the // required number of color components. if(pixelSize < 4) { long const numpels = width * height; uint8_t *ckdest = (uint8_t *) M_Malloc(4 * numpels); uint8_t *in, *out; long i; for(in = buf, out = ckdest, i = 0; i < numpels; ++i, in += pixelSize, out += 4) { if(isKeyedColor(in)) { std::memset(out, 0, 4); // Totally black. continue; } std::memcpy(out, in, 3); // The color itself. out[3] = 255; // Opaque. } return ckdest; } // We can do the keying in-buffer. // This preserves the alpha values of non-keyed pixels. for(int i = 0; i < height; ++i) { doColorKeying(buf + 4 * i * width, height); } return buf; } doomsday-stable-1.15.7/doomsday/client/src/gl/sys_opengl.cpp0000664000175000017500000003014712641367670023355 0ustar jaakkojaakko/**\file sys_opengl.cpp * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "de_base.h" #include "de_console.h" #include "de_graphics.h" #include "de_misc.h" #include "sys_system.h" #include "gl/sys_opengl.h" #include #include #include #include #include #include #ifdef WIN32 # define GETPROC(Type, x) x = de::function_cast(wglGetProcAddress(#x)) #elif defined(UNIX) && !defined(MACOSX) # include # define GETPROC(Type, x) x = de::function_cast(glXGetProcAddress((GLubyte const *)#x)) #endif gl_state_t GL_state; #ifdef WIN32 PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT = NULL; PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB = NULL; #endif #ifdef LIBGUI_USE_GLENTRYPOINTS PFNGLBLENDEQUATIONEXTPROC glBlendEquationEXT = NULL; PFNGLLOCKARRAYSEXTPROC glLockArraysEXT = NULL; PFNGLUNLOCKARRAYSEXTPROC glUnlockArraysEXT = NULL; #endif static dd_bool doneEarlyInit = false; static dd_bool inited = false; static dd_bool firstTimeInit = true; static void initialize(void) { de::GLInfo::Extensions const &ext = de::GLInfo::extensions(); if(CommandLine_Exists("-noanifilter")) { GL_state.features.texFilterAniso = false; } if(!ext.ARB_texture_non_power_of_two || CommandLine_Exists("-notexnonpow2") || CommandLine_Exists("-notexnonpowtwo")) { GL_state.features.texNonPowTwo = false; } if(ext.EXT_blend_subtract) { #ifdef LIBGUI_USE_GLENTRYPOINTS GETPROC(PFNGLBLENDEQUATIONEXTPROC, glBlendEquationEXT); if(!glBlendEquationEXT) GL_state.features.blendSubtract = false; #endif } else { GL_state.features.blendSubtract = false; } #ifdef USE_TEXTURE_COMPRESSION_S3 // Enabled by default if available. if(ext.EXT_texture_compression_s3tc) { GLint iVal; glGetIntegerv(GL_NUM_COMPRESSED_TEXTURE_FORMATS, &iVal); if(iVal == 0 || glGetError() != GL_NO_ERROR) GL_state.features.texCompression = false; } #else GL_state.features.texCompression = false; #endif if(!CommandLine_Exists("-texcomp") || CommandLine_Exists("-notexcomp")) { GL_state.features.texCompression = false; } // Automatic mipmap generation. if(!ext.SGIS_generate_mipmap || CommandLine_Exists("-nosgm")) { GL_state.features.genMipmap = false; } #ifdef WIN32 if(ext.Windows_EXT_swap_control) { GETPROC(PFNWGLSWAPINTERVALEXTPROC, wglSwapIntervalEXT); } if(!ext.Windows_EXT_swap_control || !wglSwapIntervalEXT) GL_state.features.vsync = false; #else GL_state.features.vsync = true; #endif } #define TABBED(A, B) _E(Ta) " " _E(l) A _E(.) " " _E(Tb) << B << "\n" de::String Sys_GLDescription() { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); de::String str; QTextStream os(&str); os << _E(b) "OpenGL information:\n" << _E(.); os << TABBED("Version:", (char const *) glGetString(GL_VERSION)); os << TABBED("Renderer:", (char const *) glGetString(GL_RENDERER)); os << TABBED("Vendor:", (char const *) glGetString(GL_VENDOR)); os << _E(T`) "Capabilities:\n"; GLint iVal; #ifdef USE_TEXTURE_COMPRESSION_S3 if(de::GLInfo::extensions().EXT_texture_compression_s3tc) { glGetIntegerv(GL_NUM_COMPRESSED_TEXTURE_FORMATS, &iVal); os << TABBED("Compressed texture formats:", iVal); } #endif os << TABBED("Use texture compression:", (GL_state.features.texCompression? "yes" : "no")); glGetIntegerv(GL_MAX_TEXTURE_UNITS, &iVal); os << TABBED("Available texture units:", iVal); if(de::GLInfo::extensions().EXT_texture_filter_anisotropic) { glGetIntegerv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &iVal); os << TABBED("Maximum texture anisotropy:", iVal); } else { os << _E(Ta) " Variable texture anisotropy unavailable."; } glGetIntegerv(GL_MAX_TEXTURE_SIZE, &iVal); os << TABBED("Maximum texture size:", iVal); GLfloat fVals[2]; glGetFloatv(GL_LINE_WIDTH_GRANULARITY, fVals); os << TABBED("Line width granularity:", fVals[0]); glGetFloatv(GL_LINE_WIDTH_RANGE, fVals); os << TABBED("Line width range:", fVals[0] << "..." << fVals[1]); return str.rightStrip(); #undef TABBED } static void printGLUInfo(void) { LOG_GL_MSG("%s") << Sys_GLDescription(); Sys_GLPrintExtensions(); } dd_bool Sys_GLPreInit(void) { if(novideo) return true; if(doneEarlyInit) return true; // Already been here?? // Init assuming ideal configuration. GL_state.multisampleFormat = 0; // No valid default can be assumed at this time. GL_state.features.blendSubtract = true; GL_state.features.genMipmap = true; GL_state.features.multisample = false; // We'll test for availability... GL_state.features.texCompression = true; GL_state.features.texFilterAniso = true; GL_state.features.texNonPowTwo = true; GL_state.features.vsync = true; GL_state.currentLineWidth = 1.5f; GL_state.currentPointSize = 1.5f; GL_state.currentUseFog = false; doneEarlyInit = true; return true; } dd_bool Sys_GLInitialize(void) { if(novideo) return true; LOG_AS("Sys_GLInitialize"); assert(doneEarlyInit); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); assert(!Sys_GLCheckError()); if(firstTimeInit) { const GLubyte* versionStr = glGetString(GL_VERSION); double version = (versionStr? strtod((const char*) versionStr, NULL) : 0); if(version == 0) { LOG_GL_WARNING("Failed to determine OpenGL version; driver reports: %s") << glGetString(GL_VERSION); } else if(version < 2.0) { if(!CommandLine_Exists("-noglcheck")) { Sys_CriticalMessagef("Your OpenGL is too old!\n" " Driver version: %s\n" " The minimum supported version is 2.0", glGetString(GL_VERSION)); return false; } else { LOG_GL_WARNING("OpenGL may be too old (2.0+ required, " "but driver reports %s)") << glGetString(GL_VERSION); } } initialize(); printGLUInfo(); firstTimeInit = false; } // GL system is now fully initialized. inited = true; /** * We can now (re)configure GL state that is dependent upon extensions * which may or may not be present on the host system. */ // Use nice quality for mipmaps please. if(GL_state.features.genMipmap && de::GLInfo::extensions().SGIS_generate_mipmap) glHint(GL_GENERATE_MIPMAP_HINT_SGIS, GL_NICEST); assert(!Sys_GLCheckError()); return true; } void Sys_GLShutdown(void) { if(!inited) return; // No cleanup. inited = false; } void Sys_GLConfigureDefaultState(void) { GLfloat fogcol[4] = { .54f, .54f, .54f, 1 }; /** * @note Only core OpenGL features can be configured at this time * because we have not yet queried for available extensions, * or configured our prefered feature default state. * * This means that GL_state.extensions and GL_state.features * cannot be accessed here during initial startup. */ assert(doneEarlyInit); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glFrontFace(GL_CW); glDisable(GL_CULL_FACE); glCullFace(GL_BACK); glDisable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glDisable(GL_TEXTURE_1D); glDisable(GL_TEXTURE_2D); glDisable(GL_TEXTURE_CUBE_MAP); // The projection matrix. glMatrixMode(GL_PROJECTION); glLoadIdentity(); // Initialize the modelview matrix. glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // Also clear the texture matrix. glMatrixMode(GL_TEXTURE); glLoadIdentity(); #if DRMESA glDisable(GL_DITHER); glDisable(GL_LIGHTING); glDisable(GL_LINE_SMOOTH); glDisable(GL_POINT_SMOOTH); glDisable(GL_POLYGON_SMOOTH); glShadeModel(GL_FLAT); #else // Setup for antialiased lines/points. glEnable(GL_LINE_SMOOTH); glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); glLineWidth(GL_state.currentLineWidth); glEnable(GL_POINT_SMOOTH); glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); glPointSize(GL_state.currentPointSize); glShadeModel(GL_SMOOTH); #endif // Alpha blending is a go! glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_ALPHA_TEST); glAlphaFunc(GL_GREATER, 0); // Default state for the white fog is off. glDisable(GL_FOG); glFogi(GL_FOG_MODE, GL_LINEAR); glFogi(GL_FOG_END, 2100); // This should be tweaked a bit. glFogfv(GL_FOG_COLOR, fogcol); #if DRMESA glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST); #else glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); #endif // Prefer good quality in texture compression. glHint(GL_TEXTURE_COMPRESSION_HINT, GL_NICEST); // Configure the default GLState (bottom of the stack). de::GLState::current().setBlendFunc(de::gl::SrcAlpha, de::gl::OneMinusSrcAlpha); } static de::String omitGLPrefix(de::String str) { if(str.startsWith("GL_")) return str.substr(3); return str; } static void printExtensions(QStringList extensions) { qSort(extensions); // Find all the prefixes. QSet prefixes; foreach(QString ext, extensions) { ext = omitGLPrefix(ext); int pos = ext.indexOf("_"); if(pos > 0) { prefixes.insert(ext.left(pos)); } } QStringList sortedPrefixes = prefixes.toList(); qSort(sortedPrefixes); foreach(QString prefix, sortedPrefixes) { de::String str; QTextStream os(&str); os << " " << prefix << " extensions:\n " _E(>) _E(2); bool first = true; foreach(QString ext, extensions) { ext = omitGLPrefix(ext); if(ext.startsWith(prefix + "_")) { ext.remove(0, prefix.size() + 1); if(!first) os << ", "; os << ext; first = false; } } LOG_GL_MSG("%s") << str; } } void Sys_GLPrintExtensions(void) { LOG_GL_MSG(_E(b) "OpenGL Extensions:"); printExtensions(QString((char const *) glGetString(GL_EXTENSIONS)).split(" ", QString::SkipEmptyParts)); #if WIN32 // List the WGL extensions too. if(wglGetExtensionsStringARB) { LOG_GL_MSG(" Extensions (WGL):"); printExtensions(QString((char const *) ((GLubyte const *(__stdcall *)(HDC))wglGetExtensionsStringARB)(wglGetCurrentDC())).split(" ", QString::SkipEmptyParts)); } #endif #ifdef Q_WS_X11 // List GLX extensions. LOG_GL_MSG(" Extensions (GLX):"); printExtensions(QString(getGLXExtensionsString()).split(" ", QString::SkipEmptyParts)); #endif } dd_bool Sys_GLCheckError() { #ifdef DENG_DEBUG if(!novideo) { GLenum error = glGetError(); if(error != GL_NO_ERROR) { LOGDEV_GL_ERROR("OpenGL error: 0x%x") << error; } } #endif return false; } doomsday-stable-1.15.7/doomsday/client/src/gl/gl_deferredapi.cpp0000664000175000017500000000373112641367670024126 0ustar jaakkojaakko/** @file gl_deferredapi.cpp GL API deferring. * @ingroup gl * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #define LIBDENG_DISABLE_DEFERRED_GL_API // using regular GL API calls #include "de_platform.h" #include #include "gl/gl_defer.h" static dd_bool __inline mustDefer(void) { return !Sys_InMainThread(); } #define GL_CALL1(form, func, x) \ if(mustDefer()) GL_Defer_##form(func, x); else func(x); #define GL_CALL2(form, func, x, y) \ if(mustDefer()) GL_Defer_##form(func, x, y); else func(x, y); DENG_EXTERN_C void Deferred_glEnable(GLenum e) { GL_CALL1(e, glEnable, e); } DENG_EXTERN_C void Deferred_glDisable(GLenum e) { GL_CALL1(e, glDisable, e); } DENG_EXTERN_C void Deferred_glDeleteTextures(GLsizei num, const GLuint* names) { GL_CALL2(uintArray, glDeleteTextures, num, names); } DENG_EXTERN_C void Deferred_glFogi(GLenum p, GLint v) { GL_CALL2(i, glFogi, p, v); } DENG_EXTERN_C void Deferred_glFogf(GLenum p, GLfloat v) { GL_CALL2(f, glFogf, p, v); } DENG_EXTERN_C void Deferred_glFogfv(GLenum p, const GLfloat* v) { GL_CALL2(fv4, glFogfv, p, v); } doomsday-stable-1.15.7/doomsday/client/src/gl/svg.cpp0000664000175000017500000001773612641367670022003 0ustar jaakkojaakko/** @file svg.cpp Scalable Vector Graphic (SVG) implementation. * @ingroup gl * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include "de_base.h" #include "de_console.h" #include "de_render.h" #include "de_system.h" #include "gl/sys_opengl.h" #include "gl/gl_main.h" #include "gl/svg.h" typedef struct svglinepoint_s { /// Next and previous points on this line. struct svglinepoint_s* next, *prev; /// Coordinates for this line in the normalized coordinate space of the owning SVG. Point2Rawf coords; } SvgLinePoint; struct svgline_s { /// Total number of points for this line. uint numPoints; /// Head of the list of points for this line. SvgLinePoint* head; }; struct svg_s { /// Unique identifier for this graphic. svgid_t id; /// GL display list containing all commands for drawing all primitives (no state changes). DGLuint dlist; /// Set of lines for this graphic. uint lineCount; SvgLine* lines; /// Set of points for this graphic. uint numPoints; SvgLinePoint* points; }; dd_bool SvgLine_IsLoop(const SvgLine* line) { assert(line); return line->head && line->head->prev != NULL; } void Svg_Delete(Svg* svg) { assert(svg); Svg_Unload(svg); free(svg->lines); free(svg->points); free(svg); } svgid_t Svg_UniqueId(Svg* svg) { assert(svg); return svg->id; } static void draw(const Svg* svg) { GLenum nextPrimType, primType = GL_LINE_STRIP; const SvgLine* lIt; uint i; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); lIt = svg->lines; for(i = 0; i < svg->lineCount; ++i, lIt++) { if(lIt->numPoints != 2) { nextPrimType = SvgLine_IsLoop(lIt)? GL_LINE_LOOP : GL_LINE_STRIP; // Do we need to end the current primitive? if(primType == GL_LINES) { glEnd(); // 2-vertex set ends. } // A new n-vertex primitive begins. glBegin(nextPrimType); } else { // Do we need to start a new 2-vertex primitive set? if(primType != GL_LINES) { primType = GL_LINES; glBegin(GL_LINES); } } // Write the vertex data. if(lIt->head) { const SvgLinePoint* pIt = lIt->head; do { /// @todo Use TexGen? glTexCoord2dv((const GLdouble*)pIt->coords.xy); glVertex2dv((const GLdouble*)pIt->coords.xy); } while(NULL != (pIt = pIt->next) && pIt != lIt->head); } if(lIt->numPoints != 2) { glEnd(); // N-vertex primitive ends. } } if(primType == GL_LINES) { // Close any remaining open 2-vertex set. glEnd(); } } static DGLuint constructDisplayList(DGLuint name, const Svg* svg) { if(GL_NewList(name, DGL_COMPILE)) { draw(svg); return GL_EndList(); } return 0; } void Svg_Draw(Svg* svg) { assert(svg); if(novideo || isDedicated) { assert(0); // Should not have been called! return; } // Have we uploaded our draw-optimized representation yet? if(svg->dlist) { // Draw! GL_CallList(svg->dlist); return; } // Draw manually in so-called 'immediate' mode. draw(svg); } dd_bool Svg_Prepare(Svg* svg) { assert(svg); if(!novideo && !isDedicated) { if(!svg->dlist) { svg->dlist = constructDisplayList(0, svg); } } return !!svg->dlist; } void Svg_Unload(Svg* svg) { assert(svg); if(novideo || isDedicated) return; if(svg->dlist) { GL_DeleteLists(svg->dlist, 1); svg->dlist = 0; } } Svg* Svg_FromDef(svgid_t uniqueId, const def_svgline_t* lines, uint lineCount) { uint finalLineCount, finalPointCount; const def_svgline_t* slIt; const Point2Rawf* spIt; dd_bool lineIsLoop; SvgLinePoint* dpIt, *prev; SvgLine* dlIt; uint i, j; Svg* svg; if(!lines || lineCount == 0) return NULL; svg = (Svg*)malloc(sizeof(*svg)); if(!svg) App_Error("Svg::FromDef: Failed on allocation of %lu bytes for new Svg.", (unsigned long) sizeof(*svg)); svg->id = uniqueId; svg->dlist = 0; // Count how many lines and points we actually need. finalLineCount = 0; finalPointCount = 0; slIt = lines; for(i = 0; i < lineCount; ++i, slIt++) { // Skip lines with missing vertices... if(slIt->numPoints < 2) continue; ++finalLineCount; finalPointCount += slIt->numPoints; if(slIt->numPoints > 2) { // If the end point is equal to the start point, we'll ommit it and // set this line up as a loop. if(FEQUAL(slIt->points[slIt->numPoints-1].x, slIt->points[0].x) && FEQUAL(slIt->points[slIt->numPoints-1].y, slIt->points[0].y)) { finalPointCount -= 1; } } } // Allocate the final point set. svg->numPoints = finalPointCount; svg->points = (SvgLinePoint*)malloc(sizeof(*svg->points) * svg->numPoints); if(!svg->points) App_Error("Svg::FromDef: Failed on allocation of %lu bytes for new SvgLinePoint set.", (unsigned long) (sizeof(*svg->points) * finalPointCount)); // Allocate the final line set. svg->lineCount = finalLineCount; svg->lines = (SvgLine*)malloc(sizeof(*svg->lines) * finalLineCount); if(!svg->lines) App_Error("Svg::FromDef: Failed on allocation of %lu bytes for new SvgLine set.", (unsigned long) (sizeof(*svg->lines) * finalLineCount)); // Setup the lines. slIt = lines; dlIt = svg->lines; dpIt = svg->points; for(i = 0; i < lineCount; ++i, slIt++) { // Skip lines with missing vertices... if(slIt->numPoints < 2) continue; // Determine how many points we'll need. dlIt->numPoints = slIt->numPoints; lineIsLoop = false; if(slIt->numPoints > 2) { // If the end point is equal to the start point, we'll ommit it and // set this line up as a loop. if(FEQUAL(slIt->points[slIt->numPoints-1].x, slIt->points[0].x) && FEQUAL(slIt->points[slIt->numPoints-1].y, slIt->points[0].y)) { dlIt->numPoints -= 1; lineIsLoop = true; } } // Copy points. spIt = slIt->points; dlIt->head = dpIt; prev = NULL; for(j = 0; j < dlIt->numPoints; ++j, spIt++) { SvgLinePoint* next = (j < dlIt->numPoints-1)? dpIt + 1 : NULL; dpIt->coords.x = spIt->x; dpIt->coords.y = spIt->y; // Link in list. dpIt->next = next; dpIt->prev = prev; // On to the next point! prev = dpIt; dpIt++; } // Link circularly? prev->next = lineIsLoop? dlIt->head : NULL; dlIt->head->prev = lineIsLoop? prev : NULL; // On to the next line! dlIt++; } return svg; } doomsday-stable-1.15.7/doomsday/client/src/gl/gl_draw.cpp0000664000175000017500000003645212641367670022617 0ustar jaakkojaakko/** @file gl_draw.cpp Basic (Generic) Drawing Routines. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "de_console.h" #include "de_graphics.h" #include "de_render.h" #include "de_misc.h" #include "de_play.h" #include "gl/sys_opengl.h" #include "gl/gl_draw.h" #include "api_render.h" #include #include #include #include #include #include using namespace de; static bool drawFilter = false; static Vector4f filterColor; void GL_DrawRectWithCoords(Rectanglei const &rect, Vector2i const coords[4]) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glBegin(GL_QUADS); // Top left. if(coords) glTexCoord2i(coords[0].x, coords[0].y); glVertex2f(rect.topLeft.x, rect.topLeft.y); // Top Right. if(coords) glTexCoord2i(coords[1].x, coords[1].y); glVertex2f(rect.topRight().x, rect.topRight().y); // Bottom right. if(coords) glTexCoord2i(coords[2].x, coords[2].y); glVertex2f(rect.bottomRight.x, rect.bottomRight.y); // Bottom left. if(coords) glTexCoord2i(coords[3].x, coords[3].y); glVertex2f(rect.bottomLeft().x, rect.bottomLeft().y); glEnd(); } void GL_DrawRect(Rectanglei const &rect) { Vector2i coords[4] = { Vector2i(0, 0), Vector2i(1, 0), Vector2i(1, 1), Vector2i(0, 1) }; GL_DrawRectWithCoords(rect, coords); } void GL_DrawRect2(int x, int y, int w, int h) { GL_DrawRect(Rectanglei::fromSize(Vector2i(x, y), Vector2ui(w, h))); } void GL_DrawRectfWithCoords(const RectRawf* rect, Point2Rawf coords[4]) { if(!rect) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glBegin(GL_QUADS); // Upper left. if(coords) glTexCoord2dv((GLdouble*)coords[0].xy); glVertex2d(rect->origin.x, rect->origin.y); // Upper Right. if(coords) glTexCoord2dv((GLdouble*)coords[1].xy); glVertex2d(rect->origin.x + rect->size.width, rect->origin.y); // Lower right. if(coords) glTexCoord2dv((GLdouble*)coords[2].xy); glVertex2d(rect->origin.x + rect->size.width, rect->origin.y + rect->size.height); // Lower left. if(coords) glTexCoord2dv((GLdouble*)coords[3].xy); glVertex2d(rect->origin.x, rect->origin.y + rect->size.height); glEnd(); } void GL_DrawRectf(const RectRawf* rect) { Point2Rawf coords[4]; coords[0].x = 0; coords[0].y = 0; coords[1].x = 1; coords[1].y = 0; coords[2].x = 1; coords[2].y = 1; coords[3].x = 0; coords[3].y = 1; GL_DrawRectfWithCoords(rect, coords); } void GL_DrawRectf2(double x, double y, double w, double h) { RectRawf rect; rect.origin.x = x; rect.origin.y = y; rect.size.width = w; rect.size.height = h; GL_DrawRectf(&rect); } void GL_DrawRectf2Color(double x, double y, double w, double h, float r, float g, float b, float a) { glColor4f(r, g, b, a); GL_DrawRectf2(x, y, w, h); } void GL_DrawRectf2TextureColor(double x, double y, double width, double height, int texW, int texH, const float topColor[3], float topAlpha, const float bottomColor[3], float bottomAlpha) { if(topAlpha <= 0 && bottomAlpha <= 0) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glBegin(GL_QUADS); // Top color. glColor4f(topColor[0], topColor[1], topColor[2], topAlpha); glTexCoord2f(0, 0); glVertex2f(x, y); glTexCoord2f(width / (float) texW, 0); glVertex2f(x + width, y); // Bottom color. glColor4f(bottomColor[0], bottomColor[1], bottomColor[2], bottomAlpha); glTexCoord2f(width / (float) texW, height / (float) texH); glVertex2f(x + width, y + height); glTexCoord2f(0, height / (float) texH); glVertex2f(x, y + height); glEnd(); } void GL_DrawRectf2Tiled(double x, double y, double w, double h, int tw, int th) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glBegin(GL_QUADS); glTexCoord2f(0, 0); glVertex2d(x, y); glTexCoord2f(w / (float) tw, 0); glVertex2d(x + w, y); glTexCoord2f(w / (float) tw, h / (float) th); glVertex2d(x + w, y + h); glTexCoord2f(0, h / (float) th); glVertex2d(x, y + h); glEnd(); } void GL_DrawCutRectfTiled(const RectRawf* rect, int tw, int th, int txoff, int tyoff, const RectRawf* cutRect) { float ftw = tw, fth = th; float txo = (1.0f / (float)tw) * (float)txoff; float tyo = (1.0f / (float)th) * (float)tyoff; // We'll draw at max four rectangles. float toph = cutRect->origin.y - rect->origin.y; float bottomh = rect->origin.y + rect->size.height - (cutRect->origin.y + cutRect->size.height); float sideh = rect->size.height - toph - bottomh; float lefth = cutRect->origin.x - rect->origin.x; float righth = rect->origin.x + rect->size.width - (cutRect->origin.x + cutRect->size.width); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glBegin(GL_QUADS); if(toph > 0) { // The top rectangle. glTexCoord2f(txo, tyo); glVertex2f(rect->origin.x, rect->origin.y); glTexCoord2f(txo + (rect->size.width / ftw), tyo); glVertex2f(rect->origin.x + rect->size.width, rect->origin.y); glTexCoord2f(txo + (rect->size.width / ftw), tyo + (toph / fth)); glVertex2f(rect->origin.x + rect->size.width, rect->origin.y + toph); glTexCoord2f(txo, tyo + (toph / fth)); glVertex2f(rect->origin.x, rect->origin.y + toph); } if(lefth > 0 && sideh > 0) { float yoff = toph / fth; // The left rectangle. glTexCoord2f(txo, yoff + tyo); glVertex2f(rect->origin.x, rect->origin.y + toph); glTexCoord2f(txo + (lefth / ftw), yoff + tyo); glVertex2f(rect->origin.x + lefth, rect->origin.y + toph); glTexCoord2f(txo + (lefth / ftw), yoff + tyo + sideh / fth); glVertex2f(rect->origin.x + lefth, rect->origin.y + toph + sideh); glTexCoord2f(txo, yoff + tyo + sideh / fth); glVertex2f(rect->origin.x, rect->origin.y + toph + sideh); } if(righth > 0 && sideh > 0) { float ox = rect->origin.x + lefth + cutRect->size.width; float xoff = (lefth + cutRect->size.width) / ftw; float yoff = toph / fth; // The left rectangle. glTexCoord2f(xoff + txo, yoff + tyo); glVertex2f(ox, rect->origin.y + toph); glTexCoord2f(xoff + txo + righth / ftw, yoff + tyo); glVertex2f(ox + righth, rect->origin.y + toph); glTexCoord2f(xoff + txo + righth / ftw, yoff + tyo + sideh / fth); glVertex2f(ox + righth, rect->origin.y + toph + sideh); glTexCoord2f(xoff + txo, yoff + tyo + sideh / fth); glVertex2f(ox, rect->origin.y + toph + sideh); } if(bottomh > 0) { float oy = rect->origin.y + toph + sideh; float yoff = (toph + sideh) / fth; glTexCoord2f(txo, yoff + tyo); glVertex2f(rect->origin.x, oy); glTexCoord2f(txo + rect->size.width / ftw, yoff + tyo); glVertex2f(rect->origin.x + rect->size.width, oy); glTexCoord2f(txo + rect->size.width / ftw, yoff + tyo + bottomh / fth); glVertex2f(rect->origin.x + rect->size.width, oy + bottomh); glTexCoord2f(txo, yoff + tyo + bottomh / fth); glVertex2f(rect->origin.x, oy + bottomh); } glEnd(); } void GL_DrawCutRectf2Tiled(double x, double y, double w, double h, int tw, int th, int txoff, int tyoff, double cx, double cy, double cw, double ch) { RectRawf rect, cutRect; rect.origin.x = x; rect.origin.y = y; rect.size.width = w; rect.size.height = h; cutRect.origin.x = cx; cutRect.origin.y = cy; cutRect.size.width = cw; cutRect.size.height = ch; GL_DrawCutRectfTiled(&rect, tw, th, txoff, tyoff, &cutRect); } /** * Totally inefficient for a large number of lines. */ void GL_DrawLine(float x1, float y1, float x2, float y2, float r, float g, float b, float a) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glColor4f(r, g, b, a); glBegin(GL_LINES); glVertex2f(x1, y1); glVertex2f(x2, y2); glEnd(); } dd_bool GL_FilterIsVisible() { return (drawFilter && filterColor.w > 0); } #undef GL_SetFilter DENG_EXTERN_C void GL_SetFilter(dd_bool enabled) { drawFilter = CPP_BOOL(enabled); } #undef GL_ResetViewEffects DENG_EXTERN_C void GL_ResetViewEffects() { GL_SetFilter(false); Con_Executef(CMDS_DDAY, true, "postfx %i none", consolePlayer); DD_SetInteger(DD_FULLBRIGHT, false); } #undef GL_SetFilterColor DENG_EXTERN_C void GL_SetFilterColor(float r, float g, float b, float a) { Vector4f newColorClamped(de::clamp(0.f, r, 1.f), de::clamp(0.f, g, 1.f), de::clamp(0.f, b, 1.f), de::clamp(0.f, a, 1.f)); if(filterColor != newColorClamped) { filterColor = newColorClamped; LOG_AS("GL_SetFilterColor"); LOGDEV_GL_XVERBOSE("%s") << filterColor.asText(); } } /** * @return Non-zero if the filter was drawn. */ void GL_DrawFilter(void) { const viewdata_t* vd = R_ViewData(displayPlayer); assert(NULL != vd); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glColor4f(filterColor.x, filterColor.y, filterColor.z, filterColor.w); glBegin(GL_QUADS); glVertex2f(vd->window.topLeft.x, vd->window.topLeft.y); glVertex2f(vd->window.topRight().x, vd->window.topRight().y); glVertex2f(vd->window.bottomRight.x, vd->window.bottomRight.y); glVertex2f(vd->window.bottomLeft().x, vd->window.bottomLeft().y); glEnd(); } #undef GL_ConfigureBorderedProjection2 DENG_EXTERN_C void GL_ConfigureBorderedProjection2(dgl_borderedprojectionstate_t* bp, int flags, int width, int height, int availWidth, int availHeight, scalemode_t overrideMode, float stretchEpsilon) { if(!bp) App_Error("GL_ConfigureBorderedProjection2: Invalid 'bp' argument."); bp->flags = flags; bp->width = width; bp->height = height; bp->availWidth = availWidth; bp->availHeight = availHeight; bp->scaleMode = R_ChooseScaleMode2(bp->width, bp->height, bp->availWidth, bp->availHeight, overrideMode, stretchEpsilon); bp->alignHorizontal = R_ChooseAlignModeAndScaleFactor(&bp->scaleFactor, bp->width, bp->height, bp->availWidth, bp->availHeight, bp->scaleMode); } #undef GL_ConfigureBorderedProjection DENG_EXTERN_C void GL_ConfigureBorderedProjection(dgl_borderedprojectionstate_t* bp, int flags, int width, int height, int availWidth, int availHeight, scalemode_t overrideMode) { GL_ConfigureBorderedProjection2(bp, flags, width, height, availWidth, availHeight, overrideMode, DEFAULT_SCALEMODE_STRETCH_EPSILON); } #undef GL_BeginBorderedProjection DENG_EXTERN_C void GL_BeginBorderedProjection(dgl_borderedprojectionstate_t* bp) { DENG_ASSERT(bp != 0); if(!bp) return; if(SCALEMODE_STRETCH == bp->scaleMode) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); /** * Use an orthographic projection in screenspace, translating and * scaling the coordinate space using the modelview matrix producing * an aspect-corrected space of availWidth x availHeight and centered * on the larger of the horizontal and vertical axes. */ glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); DGL_Ortho(0, 0, bp->availWidth, bp->availHeight, -1, 1); glMatrixMode(GL_MODELVIEW); glPushMatrix(); GLState::push(); if(bp->alignHorizontal) { // "Pillarbox": if(bp->flags & BPF_OVERDRAW_CLIP) { int w = .5f + (bp->availWidth - bp->width * bp->scaleFactor) / 2; //bp->scissorState = DGL_GetInteger(DGL_SCISSOR_TEST); //DGL_Scissor(&bp->scissorRegion); DGL_SetScissor2(DENG_GAMEVIEW_X + w, DENG_GAMEVIEW_Y, bp->width * bp->scaleFactor, bp->availHeight); //DGL_Enable(DGL_SCISSOR_TEST); } glTranslatef((float)bp->availWidth/2, 0, 0); //glScalef(1/1.2f, 1, 1); // Aspect correction. glScalef(bp->scaleFactor, bp->scaleFactor, 1); glTranslatef(-bp->width/2, 0, 0); } else { // "Letterbox": if(bp->flags & BPF_OVERDRAW_CLIP) { int h = .5f + (bp->availHeight - bp->height * bp->scaleFactor) / 2; //bp->scissorState = DGL_GetInteger(DGL_SCISSOR_TEST); //DGL_Scissor(&bp->scissorRegion); DGL_SetScissor2(DENG_GAMEVIEW_X, DENG_GAMEVIEW_Y + h, bp->availWidth, bp->height * bp->scaleFactor); //DGL_Enable(DGL_SCISSOR_TEST); } glTranslatef(0, (float)bp->availHeight/2, 0); //glScalef(1, 1.2f, 1); // Aspect correction. glScalef(bp->scaleFactor, bp->scaleFactor, 1); glTranslatef(0, -bp->height/2, 0); } } #undef GL_EndBorderedProjection DENG_EXTERN_C void GL_EndBorderedProjection(dgl_borderedprojectionstate_t* bp) { DENG_ASSERT(bp != 0); if(!bp) return; if(SCALEMODE_STRETCH == bp->scaleMode) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); GLState::pop().apply(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); if(bp->flags & BPF_OVERDRAW_CLIP) { //if(!bp->scissorState) // DGL_Disable(DGL_SCISSOR_TEST); //DGL_SetScissor(&bp->scissorRegion); } if(bp->flags & BPF_OVERDRAW_MASK) { // It shouldn't be necessary to bind the "not-texture" but the game // may have left whatever GL texture state it was using on. As this // isn't cleaned up until drawing control returns to the engine we // must explicitly disable it here. GL_SetNoTexture(); glColor4f(0, 0, 0, 1); if(bp->alignHorizontal) { // "Pillarbox": int w = .5f + (bp->availWidth - bp->width * bp->scaleFactor) / 2; GL_DrawRectf2(0, 0, w, bp->availHeight); GL_DrawRectf2(bp->availWidth - w, 0, w, bp->availHeight); } else { // "Letterbox": int h = .5f + (bp->availHeight - bp->height * bp->scaleFactor) / 2; GL_DrawRectf2(0, 0, bp->availWidth, h); GL_DrawRectf2(0, bp->availHeight - h, bp->availWidth, h); } } glMatrixMode(GL_PROJECTION); glPopMatrix(); } doomsday-stable-1.15.7/doomsday/client/src/gl/gl_defer.cpp0000664000175000017500000002616212641367670022744 0ustar jaakkojaakko/** @file gl_defer.cpp Implementation of deferred GL tasks. * @ingroup gl * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #define LIBDENG_DISABLE_DEFERRED_GL_API // using regular GL API calls #ifdef UNIX # include "de_platform.h" #endif #include "de_base.h" #include "de_console.h" #include "de_system.h" #include "de_graphics.h" #include "m_misc.h" #include "gl/gl_texmanager.h" #include "gl/texturecontent.h" using namespace de; #define NUM_RESERVED_TEXTURENAMES 512 typedef enum { DEFERREDTASK_TYPES_FIRST = 0, // Higher-level or non-OpenGL operations: DTT_UPLOAD_TEXTURECONTENT = DEFERREDTASK_TYPES_FIRST, DTT_SET_VSYNC, // OpenGL API calls: DTT_FUNC_PTR_BEGIN, DTT_FUNC_PTR_E = DTT_FUNC_PTR_BEGIN, DTT_FUNC_PTR_EI, DTT_FUNC_PTR_EF, DTT_FUNC_PTR_EFV4, DTT_FUNC_PTR_UINT_ARRAY, DTT_FUNC_PTR_END, DEFERREDTASK_TYPES_LAST } deferredtask_type_t; #define VALID_DEFERREDTASK_TYPE(t) ((t) >= DEFERREDTASK_TYPES_FIRST || (t) < DEFERREDTASK_TYPES_LAST) typedef struct deferredtask_s { struct deferredtask_s* next; deferredtask_type_t type; void* data; } deferredtask_t; typedef struct apifunc_s { union { void (GL_CALL *ptr_e)(GLenum); void (GL_CALL *ptr_ei)(GLenum, GLint i); void (GL_CALL *ptr_ef)(GLenum, GLfloat f); void (GL_CALL *ptr_efv4)(GLenum, const GLfloat* fv4); void (GL_CALL *ptr_uintArray)(GLsizei, const GLuint*); } func; union { GLenum e; struct { GLenum e; GLint i; } ei; struct { GLenum e; GLfloat f; } ef; struct { GLenum e; GLfloat fv4[4]; } efv4; struct { GLsizei count; GLuint* values; } uintArray; } param; } apifunc_t; static dd_bool inited = false; static mutex_t deferredMutex; static DGLuint reservedTextureNames[NUM_RESERVED_TEXTURENAMES]; static volatile int reservedCount = 0; static volatile deferredtask_t* deferredTaskFirst = NULL; static volatile deferredtask_t* deferredTaskLast = NULL; static deferredtask_t* allocTask(deferredtask_type_t type, void* data) { deferredtask_t* dt = 0; assert(inited); dt = (deferredtask_t*) malloc(sizeof(*dt)); if(!dt) { App_Error("allocDeferredTask: Failed on allocation of %lu bytes.", (unsigned long) sizeof(*dt)); return 0; // Unreachable. } dt->type = type; dt->data = data; dt->next = 0; return dt; } static void enqueueTask(deferredtask_type_t type, void* data) { deferredtask_t* d; if(!inited) App_Error("enqueueTask: Deferred GL task system not initialized."); d = allocTask(type, data); Sys_Lock(deferredMutex); if(deferredTaskLast) { deferredTaskLast->next = d; } if(!deferredTaskFirst) { deferredTaskFirst = d; } deferredTaskLast = d; Sys_Unlock(deferredMutex); } static deferredtask_t* nextTask(void) { assert(inited); { deferredtask_t* d = NULL; if(NULL != (d = (deferredtask_t*) deferredTaskFirst)) { deferredTaskFirst = d->next; } if(!deferredTaskFirst) deferredTaskLast = NULL; return d; } } LIBDENG_GL_DEFER1(e, GLenum e) { apifunc_t* api = (apifunc_t *) M_Malloc(sizeof(apifunc_t)); api->func.ptr_e = ptr; api->param.e = e; enqueueTask(DTT_FUNC_PTR_E, api); } LIBDENG_GL_DEFER2(i, GLenum e, GLint i) { apifunc_t* api = (apifunc_t *) M_Malloc(sizeof(apifunc_t)); api->func.ptr_ei = ptr; api->param.ei.e = e; api->param.ei.i = i; enqueueTask(DTT_FUNC_PTR_EI, api); } LIBDENG_GL_DEFER2(f, GLenum e, GLfloat f) { apifunc_t* api = (apifunc_t *) M_Malloc(sizeof(apifunc_t)); api->func.ptr_ef = ptr; api->param.ef.e = e; api->param.ef.f = f; enqueueTask(DTT_FUNC_PTR_EF, api); } LIBDENG_GL_DEFER2(fv4, GLenum e, const GLfloat* floatArrayWithFourValues) { apifunc_t* api = (apifunc_t *) M_Malloc(sizeof(apifunc_t)); api->func.ptr_efv4 = ptr; api->param.efv4.e = e; memcpy(api->param.efv4.fv4, floatArrayWithFourValues, sizeof(GLfloat) * 4); enqueueTask(DTT_FUNC_PTR_EFV4, api); } LIBDENG_GL_DEFER2(uintArray, GLsizei s, const GLuint* v) { apifunc_t* api = (apifunc_t *) M_Malloc(sizeof(apifunc_t)); api->func.ptr_uintArray = ptr; api->param.uintArray.count = s; api->param.uintArray.values = (GLuint *) M_MemDup(v, sizeof(GLuint) * s); enqueueTask(DTT_FUNC_PTR_UINT_ARRAY, api); } static void processTask(deferredtask_t *task) { apifunc_t *api = (apifunc_t *) task->data; switch(task->type) { case DTT_UPLOAD_TEXTURECONTENT: DENG2_ASSERT(task->data); GL_UploadTextureContent(*reinterpret_cast(task->data), gl::Immediate); break; case DTT_SET_VSYNC: GL_SetVSync(*(dd_bool*)task->data); break; case DTT_FUNC_PTR_E: api->func.ptr_e(api->param.e); break; case DTT_FUNC_PTR_EI: api->func.ptr_ei(api->param.ei.e, api->param.ei.i); break; case DTT_FUNC_PTR_EF: api->func.ptr_ef(api->param.ef.e, api->param.ef.f); break; case DTT_FUNC_PTR_EFV4: api->func.ptr_efv4(api->param.efv4.e, api->param.efv4.fv4); break; case DTT_FUNC_PTR_UINT_ARRAY: api->func.ptr_uintArray(api->param.uintArray.count, api->param.uintArray.values); break; default: App_Error("Unknown deferred GL task type %i.", (int) task->type); break; } } static void destroyTaskData(deferredtask_t* d) { apifunc_t* api = (apifunc_t*) d->data; assert(VALID_DEFERREDTASK_TYPE(d->type)); // Free data allocated for the task. switch(d->type) { case DTT_UPLOAD_TEXTURECONTENT: GL_DestroyTextureContent((texturecontent_s *) d->data); break; case DTT_SET_VSYNC: M_Free(d->data); break; case DTT_FUNC_PTR_UINT_ARRAY: M_Free(api->param.uintArray.values); break; default: break; } if(d->type >= DTT_FUNC_PTR_BEGIN && d->type < DTT_FUNC_PTR_END) { // Free the apifunc_t. free(d->data); } } static void destroyTask(deferredtask_t* d) { assert(inited && d); destroyTaskData(d); free(d); } void GL_InitDeferredTask(void) { if(inited) return; // Been here already... inited = true; deferredMutex = Sys_CreateMutex("DGLDeferredMutex"); GL_ReserveNames(); } void GL_ShutdownDeferredTask(void) { if(!inited) return; GL_ReleaseReservedNames(); GL_PurgeDeferredTasks(); Sys_DestroyMutex(deferredMutex); deferredMutex = 0; inited = false; } int GL_DeferredTaskCount(void) { deferredtask_t* i = 0; int count = 0; if(!inited) return 0; Sys_Lock(deferredMutex); for(i = (deferredtask_t*) deferredTaskFirst; i; i = i->next, ++count) {} Sys_Unlock(deferredMutex); return count; } void GL_ReserveNames(void) { if(!inited) return; // Just ignore. Sys_Lock(deferredMutex); if(reservedCount < NUM_RESERVED_TEXTURENAMES) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glGenTextures(NUM_RESERVED_TEXTURENAMES - reservedCount, (GLuint*) &reservedTextureNames[reservedCount]); reservedCount = NUM_RESERVED_TEXTURENAMES; } Sys_Unlock(deferredMutex); } void GL_ReleaseReservedNames(void) { if(!inited) return; // Just ignore. DENG_ASSERT_IN_MAIN_THREAD(); // not deferring here DENG_ASSERT_GL_CONTEXT_ACTIVE(); Sys_Lock(deferredMutex); glDeleteTextures(reservedCount, (const GLuint*) reservedTextureNames); memset(reservedTextureNames, 0, sizeof(reservedTextureNames)); reservedCount = 0; Sys_Unlock(deferredMutex); } DGLuint GL_GetReservedTextureName(void) { DGLuint name; LOG_AS("GL_GetReservedTextureName"); DENG_ASSERT(inited); Sys_Lock(deferredMutex); if(!reservedCount) { Sys_Unlock(deferredMutex); while(reservedCount == 0) { // Wait for someone to refill the names buffer. LOGDEV_GL_MSG("Sleeping until new names available"); Sys_Sleep(5); } Sys_Lock(deferredMutex); } name = reservedTextureNames[0]; memmove(reservedTextureNames, reservedTextureNames + 1, (NUM_RESERVED_TEXTURENAMES - 1) * sizeof(DGLuint)); reservedCount--; Sys_Unlock(deferredMutex); return name; } void GL_PurgeDeferredTasks(void) { if(!inited) return; Sys_Lock(deferredMutex); { deferredtask_t* d; while(NULL != (d = nextTask())) destroyTask(d); } Sys_Unlock(deferredMutex); } static deferredtask_t* GL_NextDeferredTask(void) { deferredtask_t* d = NULL; if(!inited) { return NULL; } Sys_Lock(deferredMutex); d = nextTask(); Sys_Unlock(deferredMutex); return d; } void GL_ProcessDeferredTasks(uint timeOutMilliSeconds) { deferredtask_t* d; uint startTime; if(novideo || !inited) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); startTime = Timer_RealMilliseconds(); // We'll reserve names multiple times, because the worker thread may be // needing new texture names while we are uploading. GL_ReserveNames(); while((!timeOutMilliSeconds || Timer_RealMilliseconds() - startTime < timeOutMilliSeconds) && (d = GL_NextDeferredTask()) != NULL) { processTask(d); destroyTask(d); GL_ReserveNames(); } GL_ReserveNames(); } gl::UploadMethod GL_ChooseUploadMethod(struct texturecontent_s const *content) { DENG2_ASSERT(content != 0); // Must the operation be carried out immediately? if((content->flags & TXCF_NEVER_DEFER) || !BusyMode_Active()) { return gl::Immediate; } // We can defer. return gl::Deferred; } void GL_DeferTextureUpload(struct texturecontent_s const *content) { if(novideo) return; // Defer this operation. Need to make a copy. enqueueTask(DTT_UPLOAD_TEXTURECONTENT, GL_ConstructTextureContentCopy(content)); } void GL_DeferSetVSync(dd_bool enableVSync) { enqueueTask(DTT_SET_VSYNC, M_MemDup(&enableVSync, sizeof(enableVSync))); } doomsday-stable-1.15.7/doomsday/client/src/gl/gl_main.cpp0000664000175000017500000013306212641367670022601 0ustar jaakkojaakko/** @file gl_main.cpp GL-Graphics Subsystem * @ingroup gl * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifdef UNIX #ifdef HAVE_CONFIG_H # include "config.h" #endif #endif #include "de_base.h" #include "gl/gl_main.h" #include #include #include #include #include #include #include #include #include #include "clientapp.h" #include "sys_system.h" // novideo #include "world/map.h" #include "world/p_object.h" #include "gl/gl_tex.h" #include "gl/gl_texmanager.h" #include "gl/texturecontent.h" #include "resource/hq2x.h" #include "MaterialAnimator" #include "MaterialVariantSpec" #include "Texture" #include "api_render.h" #include "render/rend_main.h" #include "render/r_main.h" #include "render/cameralensfx.h" #include "render/rend_font.h" #include "render/rend_model.h" #include "render/rend_particle.h" #include "render/vr.h" #include "r_util.h" #include "ui/ui_main.h" #include "ui/clientwindowsystem.h" using namespace de; extern dint maxnumnodes; extern dd_bool fillOutlines; dint numTexUnits = 1; dd_bool envModAdd; ///< TexEnv: modulate and add is available. dint test3dfx; dint r_detail = true; ///< Draw detail textures (if available). dfloat vid_gamma = 1.0f, vid_bright, vid_contrast = 1.0f; dfloat glNearClip, glFarClip; static dd_bool initGLOk; static dd_bool initFullGLOk; static dd_bool gamma_support; static dfloat oldgamma, oldcontrast, oldbright; static dint fogModeDefault; static viewport_t currentView; static inline ResourceSystem &resSys() { return App_ResourceSystem(); } dd_bool GL_IsInited() { return initGLOk; } dd_bool GL_IsFullyInited() { return initFullGLOk; } #if defined(WIN32) || defined(MACOSX) void GL_AssertContextActive() { #ifdef WIN32 DENG2_ASSERT(wglGetCurrentContext() != 0); #else DENG2_ASSERT(CGLGetCurrentContext() != 0); #endif } #endif void GL_GetGammaRamp(DisplayColorTransfer *ramp) { if(!gamma_support) return; DisplayMode_GetColorTransfer(ramp); } void GL_SetGammaRamp(DisplayColorTransfer const *ramp) { if(!gamma_support) return; DisplayMode_SetColorTransfer(ramp); } /** * Calculate a gamma ramp and write the result to the location pointed to. * * @todo Allow for finer control of the curves (separate red, green, blue). * * @param ramp Ptr to the ramp table to write to. Must point to a ushort[768] area of memory. * @param gamma Non-linear factor (curvature; >1.0 multiplies). * @param contrast Steepness. * @param bright Brightness, uniform offset. */ void GL_MakeGammaRamp(ushort *ramp, dfloat gamma, dfloat contrast, dfloat bright) { DENG2_ASSERT(ramp); ddouble ideal[256]; // After processing clamped to unsigned short. // Don't allow stupid values. if(contrast < 0.1f) contrast = 0.1f; if(bright > 0.8f) bright = 0.8f; if(bright < -0.8f) bright = -0.8f; // Init the ramp as a line with the steepness defined by contrast. for(dint i = 0; i < 256; ++i) { ideal[i] = i * contrast - (contrast - 1) * 127; } // Apply the gamma curve. if(gamma != 1) { if(gamma <= 0.1f) gamma = 0.1f; ddouble norm = pow(255, 1 / ddouble( gamma ) - 1); // Normalizing factor. for(dint i = 0; i < 256; ++i) { ideal[i] = pow(ideal[i], 1 / ddouble( gamma )) / norm; } } // The last step is to add the brightness offset. for(dint i = 0; i < 256; ++i) { ideal[i] += bright * 128; } // Clamp it and write the ramp table. for(dint i = 0; i < 256; ++i) { ideal[i] *= 0x100; // Byte => word if(ideal[i] < 0) ideal[i] = 0; if(ideal[i] > 0xffff) ideal[i] = 0xffff; ramp[i] = ramp[i + 256] = ramp[i + 512] = (dushort) ideal[i]; } } /** * Updates the gamma ramp based on vid_gamma, vid_contrast and vid_bright. */ void GL_SetGamma() { DisplayColorTransfer myramp; oldgamma = vid_gamma; oldcontrast = vid_contrast; oldbright = vid_bright; GL_MakeGammaRamp(myramp.table, vid_gamma, vid_contrast, vid_bright); GL_SetGammaRamp(&myramp); } void GL_DoUpdate() { if(ClientApp::vr().mode() == VRConfig::OculusRift) return; // Check for color adjustment changes. if(oldgamma != vid_gamma || oldcontrast != vid_contrast || oldbright != vid_bright) { GL_SetGamma(); } DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // Wait until the right time to show the frame so that the realized // frame rate is exactly right. glFlush(); DD_WaitForOptimalUpdateTime(); // Blit screen to video. ClientWindow::main().swapBuffers(); } static void printConfiguration() { LOG_GL_VERBOSE(_E(b) "Render configuration:"); LOG_GL_VERBOSE(" Multisampling: %b") << GL_state.features.multisample; if(GL_state.features.multisample) { LOG_GL_VERBOSE(" Multisampling format: %i") << GL_state.multisampleFormat; } LOG_GL_VERBOSE(" Multitexturing: %s") << (numTexUnits > 1? (envModAdd? "full" : "partial") : "not available"); LOG_GL_VERBOSE(" Texture Anisotropy: %s") << (GL_state.features.texFilterAniso? "variable" : "fixed"); LOG_GL_VERBOSE(" Texture Compression: %b") << GL_state.features.texCompression; LOG_GL_VERBOSE(" Texture NPOT: %b") << GL_state.features.texNonPowTwo; } void GL_EarlyInit() { if(novideo) return; if(initGLOk) return; // Already initialized. LOG_GL_VERBOSE("Initializing Render subsystem..."); ClientApp::renderSystem().glInit(); gamma_support = !CommandLine_Check("-noramp"); // We are simple people; two texture units is enough. numTexUnits = de::min(GLInfo::limits().maxTexUnits, MAX_TEX_UNITS); envModAdd = (GLInfo::extensions().NV_texture_env_combine4 || GLInfo::extensions().ATI_texture_env_combine3); GL_InitDeferredTask(); // Model renderer must be initialized early as it may need to configure // gl-element arrays. Rend_ModelInit(); // Check the maximum texture size. if(GLInfo::limits().maxTexSize == 256) { LOG_GL_WARNING("Using restricted texture w/h ratio (1:8)"); ratioLimit = 8; } if(CommandLine_Check("-outlines")) { fillOutlines = false; LOG_GL_NOTE("Textures have outlines"); } renderTextures = !CommandLine_Exists("-notex"); printConfiguration(); // Initialize the renderer into a 2D state. GL_Init2DState(); GL_SetVSync(App::config().getb("window.main.vsync")); initGLOk = true; } void GL_Init() { if(novideo) return; if(!initGLOk) { App_Error("GL_Init: GL_EarlyInit has not been done yet.\n"); } // Set the gamma in accordance with vid-gamma, vid-bright and vid-contrast. GL_SetGamma(); // Initialize one viewport. R_SetupDefaultViewWindow(0); R_SetViewGrid(1, 1); } void GL_InitRefresh() { if(novideo) return; GL_InitTextureManager(); initFullGLOk = true; } void GL_ShutdownRefresh() { initFullGLOk = false; } void GL_Shutdown() { if(!initGLOk || !ClientWindow::mainExists()) return; // Not yet initialized fully. DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // We won't be drawing anything further but we don't want to shutdown // with the previous frame still visible as this can lead to unwanted // artefacts during video context switches on some displays. // // Render a few black frames before we continue. if(!novideo) { dint i = 0; do { glClear(GL_COLOR_BUFFER_BIT); GL_DoUpdate(); } while(++i < 3); } ClientApp::renderSystem().glDeinit(); GL_ShutdownDeferredTask(); FR_Shutdown(); Rend_ModelShutdown(); LensFx_Shutdown(); Rend_Reset(); GL_ShutdownRefresh(); // Shutdown OpenGL. Sys_GLShutdown(); initGLOk = false; } void GL_Init2DState() { // The variables. glNearClip = 5; glFarClip = 16500; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // Here we configure the OpenGL state and set the projection matrix. glDisable(GL_CULL_FACE); glDisable(GL_DEPTH_TEST); glDisable(GL_TEXTURE_1D); glDisable(GL_TEXTURE_2D); glDisable(GL_TEXTURE_CUBE_MAP); // Default, full area viewport. //glViewport(0, 0, DENG_WINDOW->width(), DENG_WINDOW->height()); // The projection matrix. glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0, 320, 200, 0, -1, 1); // Default state for the white fog is off. usingFog = false; glDisable(GL_FOG); glFogi(GL_FOG_MODE, (fogModeDefault == 0 ? GL_LINEAR : fogModeDefault == 1 ? GL_EXP : GL_EXP2)); glFogf(GL_FOG_START, DEFAULT_FOG_START); glFogf(GL_FOG_END, DEFAULT_FOG_END); glFogf(GL_FOG_DENSITY, DEFAULT_FOG_DENSITY); fogColor[0] = DEFAULT_FOG_COLOR_RED; fogColor[1] = DEFAULT_FOG_COLOR_GREEN; fogColor[2] = DEFAULT_FOG_COLOR_BLUE; fogColor[3] = 1; glFogfv(GL_FOG_COLOR, fogColor); } void GL_SwitchTo3DState(dd_bool push_state, viewport_t const *port, viewdata_t const *viewData) { DENG2_ASSERT(port && viewData); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); if(push_state) { // Push the 2D matrices on the stack. glMatrixMode(GL_PROJECTION); glPushMatrix(); glMatrixMode(GL_MODELVIEW); glPushMatrix(); } glEnable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); std::memcpy(¤tView, port, sizeof(currentView)); viewpx = port->geometry.topLeft.x + viewData->window.topLeft.x; viewpy = port->geometry.topLeft.y + viewData->window.topLeft.y; viewpw = de::min(port->geometry.width(), viewData->window.width()); viewph = de::min(port->geometry.height(), viewData->window.height()); ClientWindow::main().game().glApplyViewport(Rectanglei::fromSize(Vector2i(viewpx, viewpy), Vector2ui(viewpw, viewph))); // The 3D projection matrix. GL_ProjectionMatrix(); } void GL_Restore2DState(dint step, viewport_t const *port, viewdata_t const *viewData) { DENG2_ASSERT(port && viewData); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); switch(step) { case 1: { // After Restore Step 1 normal player sprites are rendered. dint height = dfloat( port->geometry.width() * viewData->window.height() / viewData->window.width() ) / port->geometry.height() * SCREENHEIGHT; scalemode_t sm = R_ChooseScaleMode(SCREENWIDTH, SCREENHEIGHT, port->geometry.width(), port->geometry.height(), scalemode_t(weaponScaleMode)); glMatrixMode(GL_PROJECTION); glLoadIdentity(); if(SCALEMODE_STRETCH == sm) { glOrtho(0, SCREENWIDTH, height, 0, -1, 1); } else { // Use an orthographic projection in native screenspace. Then // translate and scale the projection to produce an aspect // corrected coordinate space at 4:3, aligned vertically to // the bottom and centered horizontally in the window. glOrtho(0, port->geometry.width(), port->geometry.height(), 0, -1, 1); glTranslatef(port->geometry.width()/2, port->geometry.height(), 0); if(port->geometry.width() >= port->geometry.height()) glScalef(dfloat( port->geometry.height() ) / SCREENHEIGHT, dfloat( port->geometry.height() ) / SCREENHEIGHT, 1); else glScalef(dfloat( port->geometry.width() ) / SCREENWIDTH, dfloat( port->geometry.width() ) / SCREENWIDTH, 1); // Special case: viewport height is greater than width. // Apply an additional scaling factor to prevent player sprites // looking too small. if(port->geometry.height() > port->geometry.width()) { dfloat extraScale = (dfloat(port->geometry.height() * 2) / port->geometry.width()) / 2; glScalef(extraScale, extraScale, 1); } glTranslatef(-(SCREENWIDTH / 2), -SCREENHEIGHT, 0); glScalef(1, dfloat( SCREENHEIGHT ) / height, 1); } glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // Depth testing must be disabled so that psprite 1 will be drawn // on top of psprite 0 (Doom plasma rifle fire). glDisable(GL_DEPTH_TEST); break; } case 2: // After Restore Step 2 we're back in 2D rendering mode. ClientWindow::main().game().glApplyViewport(currentView.geometry); glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); glDisable(GL_CULL_FACE); glDisable(GL_DEPTH_TEST); break; default: App_Error("GL_Restore2DState: Invalid value, step = %i.", step); break; } } Matrix4f GL_GetProjectionMatrix() { dfloat const fov = Rend_FieldOfView(); Vector2f const size(viewpw, viewph); yfov = vrCfg().verticalFieldOfView(fov, size); return vrCfg().projectionMatrix(Rend_FieldOfView(), size, glNearClip, glFarClip) * Matrix4f::scale(Vector3f(1, 1, -1)); } void GL_ProjectionMatrix() { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // Actually shift the player viewpoint // We'd like to have a left-handed coordinate system. glMatrixMode(GL_PROJECTION); glLoadMatrixf(GL_GetProjectionMatrix().values()); } void GL_SetupFogFromMapInfo(Record const *mapInfo) { if(!mapInfo || !(mapInfo->geti("flags") & MIF_FOG)) { R_SetupFogDefaults(); } else { dfloat fogColor[3]; Vector3f(mapInfo->get("fogColor")).decompose(fogColor); R_SetupFog(mapInfo->getf("fogStart"), mapInfo->getf("fogEnd"), mapInfo->getf("fogDensity"), fogColor); } String fadeTable = (mapInfo? mapInfo->gets("fadeTable") : ""); if(!fadeTable.isEmpty()) { LumpIndex const &lumps = App_FileSystem().nameIndex(); dint lumpNum = lumps.findLast(fadeTable + ".lmp"); if(lumpNum == lumps.findLast("COLORMAP.lmp")) { // We don't want fog in this case. GL_UseFog(false); } // Probably fog ... don't use fullbright sprites. else if(lumpNum == lumps.findLast("FOGMAP.lmp")) { GL_UseFog(true); } } } #undef GL_UseFog DENG_EXTERN_C void GL_UseFog(dint yes) { usingFog = yes; } void GL_SelectTexUnits(dint count) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); for(dint i = numTexUnits - 1; i >= count; i--) { glActiveTexture(GL_TEXTURE0 + i); glDisable(GL_TEXTURE_2D); } // Enable the selected units. for(dint i = count - 1; i >= 0; i--) { if(i >= numTexUnits) continue; glActiveTexture(GL_TEXTURE0 + i); glEnable(GL_TEXTURE_2D); } } void GL_TotalReset() { if(isDedicated) return; // Update the secondary title and the game status. //Rend_ConsoleUpdateTitle(); // Release all texture memory. resSys().releaseAllGLTextures(); resSys().pruneUnusedTextureSpecs(); GL_LoadLightingSystemTextures(); GL_LoadFlareTextures(); Rend_ParticleLoadSystemTextures(); GL_ReleaseReservedNames(); #if _DEBUG Z_CheckHeap(); #endif } void GL_TotalRestore() { if(isDedicated) return; // Getting back up and running. GL_ReserveNames(); GL_Init2DState(); // Choose fonts again. UI_LoadFonts(); //Con_Resize(); // Restore the fog settings. Map const *map = App_WorldSystem().mapPtr(); GL_SetupFogFromMapInfo(map? &map->mapInfo() : nullptr); #if _DEBUG Z_CheckHeap(); #endif } void GL_BlendMode(blendmode_t mode) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); switch(mode) { case BM_ZEROALPHA: GLState::current().setBlendOp(gl::Add) .setBlendFunc(gl::One, gl::Zero) .apply(); break; case BM_ADD: GLState::current().setBlendOp(gl::Add) .setBlendFunc(gl::SrcAlpha, gl::One) .apply(); break; case BM_DARK: GLState::current().setBlendOp(gl::Add) .setBlendFunc(gl::DestColor, gl::OneMinusSrcAlpha) .apply(); break; case BM_SUBTRACT: GLState::current().setBlendOp(gl::Subtract) .setBlendFunc(gl::One, gl::SrcAlpha) .apply(); break; case BM_ALPHA_SUBTRACT: GLState::current().setBlendOp(gl::Subtract) .setBlendFunc(gl::SrcAlpha, gl::One) .apply(); break; case BM_REVERSE_SUBTRACT: GLState::current().setBlendOp(gl::ReverseSubtract) .setBlendFunc(gl::SrcAlpha, gl::One) .apply(); break; case BM_MUL: GLState::current().setBlendOp(gl::Add) .setBlendFunc(gl::Zero, gl::SrcColor) .apply(); break; case BM_INVERSE: GLState::current().setBlendOp(gl::Add) .setBlendFunc(gl::OneMinusDestColor, gl::OneMinusSrcColor) .apply(); break; case BM_INVERSE_MUL: GLState::current().setBlendOp(gl::Add) .setBlendFunc(gl::Zero, gl::OneMinusSrcColor) .apply(); break; default: GLState::current().setBlendOp(gl::Add) .setBlendFunc(gl::SrcAlpha, gl::OneMinusSrcAlpha) .apply(); break; } } GLenum GL_Filter(gl::Filter f) { switch(f) { case gl::Nearest: return GL_NEAREST; case gl::Linear: return GL_LINEAR; } return GL_REPEAT; } GLenum GL_Wrap(gl::Wrapping w) { switch(w) { case gl::Repeat: return GL_REPEAT; case gl::RepeatMirrored: return GL_MIRRORED_REPEAT; case gl::ClampToEdge: return GL_CLAMP_TO_EDGE; } return GL_REPEAT; } dint GL_NumMipmapLevels(dint width, dint height) { dint numLevels = 0; while(width > 1 || height > 1) { width /= 2; height /= 2; ++numLevels; } return numLevels; } dd_bool GL_OptimalTextureSize(dint width, dint height, dd_bool noStretch, dd_bool isMipMapped, dint *optWidth, dint *optHeight) { DENG2_ASSERT(optWidth && optHeight); if(GL_state.features.texNonPowTwo && !isMipMapped) { *optWidth = width; *optHeight = height; } else if(noStretch) { *optWidth = M_CeilPow2(width); *optHeight = M_CeilPow2(height); } else { // Determine the most favorable size for the texture. if(texQuality == TEXQ_BEST) { // At the best texture quality *opt, all textures are // sized *upwards*, so no details are lost. This takes // more memory, but naturally looks better. *optWidth = M_CeilPow2(width); *optHeight = M_CeilPow2(height); } else if(texQuality == 0) { // At the lowest quality, all textures are sized down to the // nearest power of 2. *optWidth = M_FloorPow2(width); *optHeight = M_FloorPow2(height); } else { // At the other quality *opts, a weighted rounding is used. *optWidth = M_WeightPow2(width, 1 - texQuality / dfloat( TEXQ_BEST )); *optHeight = M_WeightPow2(height, 1 - texQuality / dfloat( TEXQ_BEST )); } } // Hardware limitations may force us to modify the preferred size. if(*optWidth > GLInfo::limits().maxTexSize) { *optWidth = GLInfo::limits().maxTexSize; noStretch = false; } if(*optHeight > GLInfo::limits().maxTexSize) { *optHeight = GLInfo::limits().maxTexSize; noStretch = false; } // Some GL drivers seem to have problems with VERY small textures. if(*optWidth < MINTEXWIDTH) *optWidth = MINTEXWIDTH; if(*optHeight < MINTEXHEIGHT) *optHeight = MINTEXHEIGHT; if(ratioLimit) { if(*optWidth > *optHeight) // Wide texture. { if(*optHeight < *optWidth / ratioLimit) *optHeight = *optWidth / ratioLimit; } else // Tall texture. { if(*optWidth < *optHeight / ratioLimit) *optWidth = *optHeight / ratioLimit; } } return noStretch; } dint GL_GetTexAnisoMul(dint level) { // Should anisotropic filtering be used? if(!GL_state.features.texFilterAniso) return 1; if(level < 0) { // Go with the maximum! return GLInfo::limits().maxTexFilterAniso; } // Convert from a DGL aniso-level to a multiplier. dint mul; switch(level) { default: mul = 1; break; case 1: mul = 2; break; case 2: mul = 4; break; case 3: mul = 8; break; case 4: mul = 16; break; } // Clamp. return de::min(mul, GLInfo::limits().maxTexFilterAniso); } static void uploadContentUnmanaged(texturecontent_t const &content) { LOG_AS("uploadContentUnmanaged"); if(novideo) return; gl::UploadMethod uploadMethod = GL_ChooseUploadMethod(&content); if(uploadMethod == gl::Immediate) { LOGDEV_GL_XVERBOSE("Uploading texture (%i:%ix%i) while not busy! " "Should have been precached in busy mode?") << content.name << content.width << content.height; } GL_UploadTextureContent(content, uploadMethod); } GLuint GL_NewTextureWithParams(dgltexformat_t format, dint width, dint height, duint8 const *pixels, dint flags) { texturecontent_t c; GL_InitTextureContent(&c); c.name = GL_GetReservedTextureName(); c.format = format; c.width = width; c.height = height; c.pixels = pixels; c.flags = flags; uploadContentUnmanaged(c); return c.name; } GLuint GL_NewTextureWithParams(dgltexformat_t format, dint width, dint height, uint8_t const *pixels, dint flags, dint grayMipmap, dint minFilter, dint magFilter, dint anisoFilter, dint wrapS, dint wrapT) { texturecontent_t c; GL_InitTextureContent(&c); c.name = GL_GetReservedTextureName(); c.format = format; c.width = width; c.height = height; c.pixels = pixels; c.flags = flags; c.grayMipmap = grayMipmap; c.minFilter = minFilter; c.magFilter = magFilter; c.anisoFilter = anisoFilter; c.wrap[0] = wrapS; c.wrap[1] = wrapT; uploadContentUnmanaged(c); return c.name; } static inline MaterialVariantSpec const &uiMaterialSpec(gl::Wrapping wrapS, gl::Wrapping wrapT) { return resSys().materialSpec(UiContext, 0, 1, 0, 0, GL_Wrap(wrapS), GL_Wrap(wrapT), 0, 1, 0, false, false, false, false); } static inline MaterialVariantSpec const &pspriteMaterialSpec(dint tClass, dint tMap) { return resSys().materialSpec(PSpriteContext, 0, 1, tClass, tMap, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 0, 1, 0, false, true, true, false); } void GL_SetMaterialUI2(Material *material, gl::Wrapping wrapS, gl::Wrapping wrapT) { if(!material) return; // @todo we need a "NULL material". MaterialAnimator &matAnimator = material->getAnimator(uiMaterialSpec(wrapS, wrapT)); // Ensure we have up to date info about the material. matAnimator.prepare(); GL_BindTexture(matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture); } void GL_SetMaterialUI(Material *mat) { GL_SetMaterialUI2(mat, gl::ClampToEdge, gl::ClampToEdge); } void GL_SetPSprite(Material *material, dint tClass, dint tMap) { if(!material) return; MaterialAnimator &matAnimator = material->getAnimator(pspriteMaterialSpec(tClass, tMap)); // Ensure we have up to date info about the material. matAnimator.prepare(); GL_BindTexture(matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture); } void GL_SetRawImage(lumpnum_t lumpNum, gl::Wrapping wrapS, gl::Wrapping wrapT) { if(rawtex_t *rawTex = resSys().declareRawTexture(lumpNum)) { GL_BindTextureUnmanaged(GL_PrepareRawTexture(*rawTex), wrapS, wrapT, (filterUI ? gl::Linear : gl::Nearest)); } } void GL_BindTexture(TextureVariant *vtexture) { if(BusyMode_InWorkerThread()) return; // Ensure we have a prepared texture. duint glTexName = vtexture? vtexture->prepare() : 0; if(glTexName == 0) { GL_SetNoTexture(); return; } DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glBindTexture(GL_TEXTURE_2D, glTexName); Sys_GLCheckError(); // Apply dynamic adjustments to the GL texture state according to our spec. TextureVariantSpec const &spec = vtexture->spec(); if(spec.type == TST_GENERAL) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, spec.variant.wrapS); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, spec.variant.wrapT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, spec.variant.glMagFilter()); if(GL_state.features.texFilterAniso) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, GL_GetTexAnisoMul(spec.variant.logicalAnisoLevel())); } } } void GL_BindTextureUnmanaged(GLuint glName, gl::Wrapping wrapS, gl::Wrapping wrapT, gl::Filter filter) { if(BusyMode_InWorkerThread()) return; if(glName == 0) { GL_SetNoTexture(); return; } DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glBindTexture(GL_TEXTURE_2D, glName); Sys_GLCheckError(); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_Wrap(wrapS)); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_Wrap(wrapT)); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_Filter(filter)); if(GL_state.features.texFilterAniso) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, GL_GetTexAnisoMul(texAniso)); } } void GL_Bind(GLTextureUnit const &glTU) { if(!glTU.hasTexture()) return; if(!renderTextures) { GL_SetNoTexture(); return; } if(glTU.texture) { GL_BindTexture(glTU.texture); } else { GL_BindTextureUnmanaged(glTU.unmanaged.glName, glTU.unmanaged.wrapS, glTU.unmanaged.wrapT, glTU.unmanaged.filter); } } void GL_BindTo(GLTextureUnit const &glTU, dint unit) { if(!glTU.hasTexture()) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glActiveTexture(GL_TEXTURE0 + dbyte(unit)); GL_Bind(glTU); } void GL_SetNoTexture() { if(BusyMode_InWorkerThread()) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); /// @todo Don't actually change the current binding. Instead we should disable /// all currently enabled texture types. glBindTexture(GL_TEXTURE_2D, 0); } dint GL_ChooseSmartFilter(dint width, dint height, dint /*flags*/) { if(width >= MINTEXWIDTH && height >= MINTEXHEIGHT) return 2; // hq2x return 1; // nearest neighbor. } duint8 *GL_SmartFilter(dint method, duint8 const *src, dint width, dint height, dint flags, dint *outWidth, dint *outHeight) { dint newWidth, newHeight; duint8 *out = nullptr; switch(method) { default: // linear interpolation. newWidth = width * 2; newHeight = height * 2; out = GL_ScaleBuffer(src, width, height, 4, newWidth, newHeight); break; case 1: // nearest neighbor. newWidth = width * 2; newHeight = height * 2; out = GL_ScaleBufferNearest(src, width, height, 4, newWidth, newHeight); break; case 2: // hq2x newWidth = width * 2; newHeight = height * 2; out = GL_SmartFilterHQ2x(src, width, height, flags); break; }; if(!out) { // Unchanged, return the source image. if(outWidth) *outWidth = width; if(outHeight) *outHeight = height; return const_cast(src); } if(outWidth) *outWidth = newWidth; if(outHeight) *outHeight = newHeight; return out; } duint8 *GL_ConvertBuffer(duint8 const *in, dint width, dint height, dint informat, colorpaletteid_t paletteId, dint outformat) { DENG2_ASSERT(in); if(informat == outformat) { // No conversion necessary. return const_cast(in); } if(width <= 0 || height <= 0) { App_Error("GL_ConvertBuffer: Attempt to convert zero-sized image."); exit(1); // Unreachable. } ColorPalette *palette = (informat <= 2? &resSys().colorPalette(paletteId) : nullptr); auto *out = (duint8 *) M_Malloc(outformat * width * height); // Conversion from pal8(a) to RGB(A). if(informat <= 2 && outformat >= 3) { GL_PalettizeImage(out, outformat, palette, false, in, informat, width, height); return out; } // Conversion from RGB(A) to pal8(a), using pal18To8. if(informat >= 3 && outformat <= 2) { GL_QuantizeImageToPalette(out, outformat, palette, in, informat, width, height); return out; } if(informat == 3 && outformat == 4) { long const numPels = width * height; duint8 const *src = in; duint8 *dst = out; for(long i = 0; i < numPels; ++i) { dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; dst[3] = 255; // Opaque. src += informat; dst += outformat; } } return out; } void GL_CalcLuminance(duint8 const *buffer, dint width, dint height, dint pixelSize, colorpaletteid_t paletteId, dfloat *retBrightX, dfloat *retBrightY, ColorRawf *retColor, dfloat *retLumSize) { DENG2_ASSERT(buffer && retBrightX && retBrightY && retColor && retLumSize); static duint8 const sizeLimit = 192, brightLimit = 224, colLimit = 192; ColorPalette *palette = (pixelSize == 1? &resSys().colorPalette(paletteId) : nullptr); // Apply the defaults. // Default to the center of the texture. *retBrightX = *retBrightY = .5f; // Default to black (i.e., no light). for(dint c = 0; c < 3; ++c) { retColor->rgb[c] = 0; } retColor->alpha = 1; // Default to a zero-size light. *retLumSize = 0; dint region[4]; FindClipRegionNonAlpha(buffer, width, height, pixelSize, region); dd_bool zeroAreaRegion = (region[0] > region[1] || region[2] > region[3]); if(zeroAreaRegion) return; // // Image contains at least one non-transparent pixel. // long bright[2]; for(dint i = 0; i < 2; ++i) { bright[i] = 0; } long average[3], lowAvg[3]; for(dint i = 0; i < 3; ++i) { average[i] = 0; lowAvg[i] = 0; } duint8 const *src = buffer; // In paletted mode, the alpha channel follows the actual image. duint8 const *alphaSrc = &buffer[width * height]; // Skip to the start of the first column. if(region[2] > 0) { src += pixelSize * width * region[2]; alphaSrc += width * region[2]; } duint8 rgb[3]; dint avgCnt = 0, lowCnt = 0; dint cnt = 0, posCnt = 0; for(dint y = region[2]; y <= region[3]; ++y) { // Skip to the beginning of the row. if(region[0] > 0) { src += pixelSize * region[0]; alphaSrc += region[0]; } for(dint x = region[0]; x <= region[1]; ++x, src += pixelSize, alphaSrc++) { // Alpha pixels don't count. Why? -ds dd_bool const pixelIsTransparent = (pixelSize == 1? *alphaSrc < 255 : pixelSize == 4? src[3] < 255 : false); if(pixelIsTransparent) continue; if(pixelSize == 1) { Vector3ub palColor = palette->color(*src); rgb[0] = palColor.x; rgb[1] = palColor.y; rgb[2] = palColor.z; } else if(pixelSize >= 3) { std::memcpy(rgb, src, 3); } // Bright enough? if(rgb[0] > brightLimit || rgb[1] > brightLimit || rgb[2] > brightLimit) { // This pixel will participate in calculating the average center point. posCnt++; bright[0] += x; bright[1] += y; } // Bright enough to affect size? if(rgb[0] > sizeLimit || rgb[1] > sizeLimit || rgb[2] > sizeLimit) cnt++; // How about the color of the light? if(rgb[0] > colLimit || rgb[1] > colLimit || rgb[2] > colLimit) { avgCnt++; for(dint c = 0; c < 3; ++c) { average[c] += rgb[c]; } } else { lowCnt++; for(dint c = 0; c < 3; ++c) { lowAvg[c] += rgb[c]; } } } // Skip to the end of this row. if(region[1] < width - 1) { src += pixelSize * (width - 1 - region[1]); alphaSrc += (width - 1 - region[1]); } } if(posCnt) { // Calculate the average of the bright pixels. *retBrightX = (long double) bright[0] / posCnt; *retBrightY = (long double) bright[1] / posCnt; } else { // No bright pixels - Place the origin at the center of the non-alpha region. *retBrightX = region[0] + (region[1] - region[0]) / 2.0f; *retBrightY = region[2] + (region[3] - region[2]) / 2.0f; } // Determine rounding (to the nearest pixel center). dint roundXDir = dint( *retBrightX + .5f ) == dint( *retBrightX )? 1 : -1; dint roundYDir = dint( *retBrightY + .5f ) == dint( *retBrightY )? 1 : -1; // Apply all rounding and output as decimal. *retBrightX = (ROUND(*retBrightX) + .5f * roundXDir) / dfloat( width ); *retBrightY = (ROUND(*retBrightY) + .5f * roundYDir) / dfloat( height ); if(avgCnt || lowCnt) { // The color. if(!avgCnt) { // Low-intensity color average. for(dint c = 0; c < 3; ++c) { retColor->rgb[c] = lowAvg[c] / lowCnt / 255.f; } } else { // High-intensity color average. for(dint c = 0; c < 3; ++c) { retColor->rgb[c] = average[c] / avgCnt / 255.f; } } Vector3f color(retColor->rgb); R_AmplifyColor(color); for(dint i = 0; i < 3; ++i) { retColor->rgb[i] = color[i]; } // How about the size of the light source? /// @todo These factors should be cvars. *retLumSize = MIN_OF(((2 * cnt + avgCnt) / 3.0f / 70.0f), 1); } /* DEBUG_Message(("GL_CalcLuminance: width %dpx, height %dpx, bits %d\n" " cell region X[%d, %d] Y[%d, %d]\n" " flare X= %g Y=%g %s\n" " flare RGB[%g, %g, %g] %s\n", width, height, pixelSize, region[0], region[1], region[2], region[3], *retBrightX, *retBrightY, (posCnt? "(average)" : "(center)"), retColor->red, retColor->green, retColor->blue, (avgCnt? "(hi-intensity avg)" : lowCnt? "(low-intensity avg)" : "(white light)"))); */ } D_CMD(SetRes) { DENG2_UNUSED3(src, argc, argv); ClientWindow *win = ClientWindowSystem::mainPtr(); if(!win) return false; bool isFull = win->isFullScreen(); dint attribs[] = { isFull? ClientWindow::FullscreenWidth : ClientWindow::Width, String(argv[1]).toInt(), isFull? ClientWindow::FullscreenHeight : ClientWindow::Height, String(argv[2]).toInt(), ClientWindow::End }; return win->changeAttributes(attribs); } D_CMD(SetFullRes) { DENG2_UNUSED2(src, argc); ClientWindow *win = ClientWindowSystem::mainPtr(); if(!win) return false; dint attribs[] = { ClientWindow::FullscreenWidth, String(argv[1]).toInt(), ClientWindow::FullscreenHeight, String(argv[2]).toInt(), ClientWindow::Fullscreen, true, ClientWindow::End }; return win->changeAttributes(attribs); } D_CMD(SetWinRes) { DENG2_UNUSED2(src, argc); ClientWindow *win = ClientWindowSystem::mainPtr(); if(!win) return false; dint attribs[] = { ClientWindow::Width, String(argv[1]).toInt(), ClientWindow::Height, String(argv[2]).toInt(), ClientWindow::Fullscreen, false, ClientWindow::Maximized, false, ClientWindow::End }; return win->changeAttributes(attribs); } D_CMD(ToggleFullscreen) { DENG2_UNUSED3(src, argc, argv); ClientWindow *win = ClientWindowSystem::mainPtr(); if(!win) return false; dint attribs[] = { ClientWindow::Fullscreen, !win->isFullScreen(), ClientWindow::End }; return win->changeAttributes(attribs); } D_CMD(ToggleMaximized) { DENG2_UNUSED3(src, argc, argv); ClientWindow *win = ClientWindowSystem::mainPtr(); if(!win) return false; dint attribs[] = { ClientWindow::Maximized, !win->isMaximized(), ClientWindow::End }; return win->changeAttributes(attribs); } D_CMD(ToggleCentered) { DENG2_UNUSED3(src, argc, argv); ClientWindow *win = ClientWindowSystem::mainPtr(); if(!win) return false; dint attribs[] = { ClientWindow::Centered, !win->isCentered(), ClientWindow::End }; return win->changeAttributes(attribs); } D_CMD(CenterWindow) { DENG2_UNUSED3(src, argc, argv); ClientWindow *win = ClientWindowSystem::mainPtr(); if(!win) return false; dint attribs[] = { ClientWindow::Centered, true, ClientWindow::End }; return win->changeAttributes(attribs); } D_CMD(SetBPP) { DENG2_UNUSED2(src, argc); ClientWindow *win = ClientWindowSystem::mainPtr(); if(!win) return false; dint attribs[] = { ClientWindow::ColorDepthBits, String(argv[1]).toInt(), ClientWindow::End }; return win->changeAttributes(attribs); } D_CMD(DisplayModeInfo) { DENG2_UNUSED3(src, argc, argv); ClientWindow *win = ClientWindowSystem::mainPtr(); if(!win) return false; DisplayMode const *mode = DisplayMode_Current(); String str = String("Current display mode:%1 depth:%2 (%3:%4") .arg(Vector2i(mode->width, mode->height).asText()) .arg(mode->depth) .arg(mode->ratioX) .arg(mode->ratioY); if(mode->refreshRate > 0) { str += String(", refresh: %1 Hz").arg(mode->refreshRate, 0, 'f', 1); } str += String(")\nMain window:\n current origin:%1 size:%2" "\n windowed origin:%3 size:%4" "\n fullscreen size:%5") .arg(win->pos().asText()) .arg(win->size().asText()) .arg(win->windowRect().topLeft.asText()) .arg(win->windowRect().size().asText()) .arg(win->fullscreenSize().asText()); str += String("\n fullscreen:%1 centered:%2 maximized:%3") .arg(DENG2_BOOL_YESNO( win->isFullScreen() )) .arg(DENG2_BOOL_YESNO( win->isCentered() )) .arg(DENG2_BOOL_YESNO( win->isMaximized() )); LOG_GL_MSG("%s") << str; return true; } D_CMD(ListDisplayModes) { DENG2_UNUSED3(src, argc, argv); LOG_GL_MSG("There are %i display modes available:") << DisplayMode_Count(); for(dint i = 0; i < DisplayMode_Count(); ++i) { DisplayMode const *mode = DisplayMode_ByIndex(i); if(mode->refreshRate > 0) { LOG_GL_MSG(" %i x %i x %i " _E(>) "(%i:%i, refresh: %.1f Hz)") << mode->width << mode->height << mode->depth << mode->ratioX << mode->ratioY << mode->refreshRate; } else { LOG_GL_MSG(" %i x %i x %i (%i:%i)") << mode->width << mode->height << mode->depth << mode->ratioX << mode->ratioY; } } return true; } D_CMD(UpdateGammaRamp) { DENG2_UNUSED3(src, argc, argv); GL_SetGamma(); LOG_GL_VERBOSE("Gamma ramp set"); return true; } D_CMD(Fog) { DENG2_UNUSED(src); if(argc == 1) { LOG_SCR_NOTE("Usage: %s (cmd) (args)") << argv[0]; LOG_SCR_MSG("Commands: on, off, mode, color, start, end, density"); LOG_SCR_MSG("Modes: linear, exp, exp2"); LOG_SCR_MSG("Color is given as RGB (0-255)"); LOG_SCR_MSG("Start and end are for linear fog, density for exponential fog."); return true; } if(!stricmp(argv[1], "on")) { GL_UseFog(true); LOG_GL_VERBOSE("Fog is now active"); return true; } if(!stricmp(argv[1], "off")) { GL_UseFog(false); LOG_GL_VERBOSE("Fog is now disabled"); return true; } if(!stricmp(argv[1], "color") && argc == 5) { for(dint i = 0; i < 3; ++i) { fogColor[i] = strtol(argv[2 + i], nullptr, 0) / 255.0f; } fogColor[3] = 1; glFogfv(GL_FOG_COLOR, fogColor); LOG_GL_VERBOSE("Fog color set"); return true; } if(!stricmp(argv[1], "start") && argc == 3) { glFogf(GL_FOG_START, (GLfloat) strtod(argv[2], nullptr)); LOG_GL_VERBOSE("Fog start distance set"); return true; } if(!stricmp(argv[1], "end") && argc == 3) { glFogf(GL_FOG_END, (GLfloat) strtod(argv[2], nullptr)); LOG_GL_VERBOSE("Fog end distance set"); return true; } if(!stricmp(argv[1], "density") && argc == 3) { glFogf(GL_FOG_DENSITY, (GLfloat) strtod(argv[2], nullptr)); LOG_GL_VERBOSE("Fog density set"); return true; } if(!stricmp(argv[1], "mode") && argc == 3) { if(!stricmp(argv[2], "linear")) { glFogi(GL_FOG_MODE, GL_LINEAR); LOG_GL_VERBOSE("Fog mode set to linear"); return true; } if(!stricmp(argv[2], "exp")) { glFogi(GL_FOG_MODE, GL_EXP); LOG_GL_VERBOSE("Fog mode set to exp"); return true; } if(!stricmp(argv[2], "exp2")) { glFogi(GL_FOG_MODE, GL_EXP2); LOG_GL_VERBOSE("Fog mode set to exp2"); return true; } } return false; } void GL_Register() { // Cvars C_VAR_INT ("rend-dev-wireframe", &renderWireframe, CVF_NO_ARCHIVE, 0, 2); C_VAR_INT ("rend-fog-default", &fogModeDefault, 0, 0, 2); // * Render-HUD C_VAR_FLOAT("rend-hud-offset-scale", &weaponOffsetScale,CVF_NO_MAX, 0, 0); C_VAR_FLOAT("rend-hud-fov-shift", &weaponFOVShift, CVF_NO_MAX, 0, 1); C_VAR_BYTE ("rend-hud-stretch", &weaponScaleMode, 0, SCALEMODE_FIRST, SCALEMODE_LAST); // * Render-Mobj C_VAR_INT ("rend-mobj-smooth-move", &useSRVO, 0, 0, 2); C_VAR_INT ("rend-mobj-smooth-turn", &useSRVOAngle, 0, 0, 1); // * video C_VAR_FLOAT("vid-gamma", &vid_gamma, 0, 0.1f, 4); C_VAR_FLOAT("vid-contrast", &vid_contrast, 0, 0, 2.5f); C_VAR_FLOAT("vid-bright", &vid_bright, 0, -1, 1); Con_AddMappedConfigVariable("vid-vsync", "i", "window.main.vsync"); Con_AddMappedConfigVariable("vid-fsaa", "i", "window.main.fsaa"); Con_AddMappedConfigVariable("vid-fps", "i", "window.main.showFps"); // Ccmds C_CMD_FLAGS("fog", nullptr, Fog, CMDF_NO_NULLGAME|CMDF_NO_DEDICATED); C_CMD ("displaymode", "", DisplayModeInfo); C_CMD ("listdisplaymodes", "", ListDisplayModes); C_CMD ("setcolordepth", "i", SetBPP); C_CMD ("setbpp", "i", SetBPP); C_CMD ("setres", "ii", SetRes); C_CMD ("setfullres", "ii", SetFullRes); C_CMD ("setwinres", "ii", SetWinRes); C_CMD ("setvidramp", "", UpdateGammaRamp); C_CMD ("togglefullscreen", "", ToggleFullscreen); C_CMD ("togglemaximized", "", ToggleMaximized); C_CMD ("togglecentered", "", ToggleCentered); C_CMD ("centerwindow", "", CenterWindow); } doomsday-stable-1.15.7/doomsday/client/src/gl/texturecontent.cpp0000664000175000017500000007625612641367670024301 0ustar jaakkojaakko/** @file texturecontent.cpp GL-texture content. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "gl/texturecontent.h" #include "de_console.h" #include "dd_def.h" // texGamma #include "dd_main.h" // App_ResourceSystem() #include "sys_system.h" #include "gl/gl_main.h" #include "gl/gl_tex.h" #include "render/rend_main.h" // misc global vars awaiting new home #include #include #include #include using namespace de; static int BytesPerPixelFmt(dgltexformat_t format) { switch(format) { case DGL_LUMINANCE: case DGL_COLOR_INDEX_8: return 1; case DGL_LUMINANCE_PLUS_A8: case DGL_COLOR_INDEX_8_PLUS_A8: return 2; case DGL_RGB: return 3; case DGL_RGBA: return 4; default: App_Error("BytesPerPixelFmt: Unknown format %i, don't know pixel size.\n", format); return 0; // Unreachable. } } /** * Given a pixel format return the number of bytes to store one pixel. * @pre Input data is of GL_UNSIGNED_BYTE type. */ static int BytesPerPixel(GLint format) { switch(format) { case GL_COLOR_INDEX: case GL_STENCIL_INDEX: case GL_DEPTH_COMPONENT: case GL_RED: case GL_GREEN: case GL_BLUE: case GL_ALPHA: case GL_LUMINANCE: return 1; case GL_LUMINANCE_ALPHA: return 2; case GL_RGB: case GL_RGB8: case GL_BGR: return 3; case GL_RGBA: case GL_RGBA8: case GL_BGRA: return 4; default: App_Error("BytesPerPixel: Unknown format %i.", (int) format); return 0; // Unreachable. } } void GL_InitTextureContent(texturecontent_t *content) { DENG_ASSERT(content); content->format = dgltexformat_t(0); content->name = 0; content->pixels = 0; content->paletteId = 0; content->width = 0; content->height = 0; content->minFilter = GL_LINEAR; content->magFilter = GL_LINEAR; content->anisoFilter = -1; // Best. content->wrap[0] = GL_CLAMP_TO_EDGE; content->wrap[1] = GL_CLAMP_TO_EDGE; content->grayMipmap = 0; content->flags = 0; } texturecontent_t *GL_ConstructTextureContentCopy(texturecontent_t const *other) { DENG_ASSERT(other); texturecontent_t *c = (texturecontent_t*) M_Malloc(sizeof(*c)); std::memcpy(c, other, sizeof(*c)); // Duplicate the image buffer. int bytesPerPixel = BytesPerPixelFmt(other->format); size_t bufferSize = bytesPerPixel * other->width * other->height; uint8_t *pixels = (uint8_t*) M_Malloc(bufferSize); std::memcpy(pixels, other->pixels, bufferSize); c->pixels = pixels; return c; } void GL_DestroyTextureContent(texturecontent_t *content) { DENG_ASSERT(content); if(content->pixels) M_Free((uint8_t *)content->pixels); M_Free(content); } /** * Prepares the image for use as a GL texture in accordance with the given * specification. * * @param image The image to prepare (in place). * @param spec Specification describing any transformations which should * be applied to the image. * * @return The DGL texture format determined for the image. */ static dgltexformat_t prepareImageAsTexture(image_t &image, variantspecification_t const &spec) { DENG_ASSERT(image.pixels); bool const monochrome = (spec.flags & TSF_MONOCHROME) != 0; bool const scaleSharp = (spec.flags & TSF_UPSCALE_AND_SHARPEN) != 0; if(spec.toAlpha) { if(0 != image.paletteId) { // Paletted. uint8_t *newPixels = GL_ConvertBuffer(image.pixels, image.size.x, image.size.y, ((image.flags & IMGF_IS_MASKED)? 2 : 1), image.paletteId, 3); M_Free(image.pixels); image.pixels = newPixels; image.pixelSize = 3; image.paletteId = 0; image.flags &= ~IMGF_IS_MASKED; } Image_ConvertToLuminance(image, false /*discard alpha*/); long total = image.size.x * image.size.y; for(long i = 0; i < total; ++i) { image.pixels[total + i] = image.pixels[i]; image.pixels[i] = 255; } image.pixelSize = 2; } else if(0 != image.paletteId) { if(fillOutlines && (image.flags & IMGF_IS_MASKED)) { ColorOutlinesIdx(image.pixels, image.size.x, image.size.y); } if(monochrome && !scaleSharp) { GL_DeSaturatePalettedImage(image.pixels, App_ResourceSystem().colorPalette(image.paletteId), image.size.x, image.size.y); } if(scaleSharp) { int scaleMethod = GL_ChooseSmartFilter(image.size.x, image.size.y, 0); bool origMasked = (image.flags & IMGF_IS_MASKED) != 0; colorpaletteid_t origPaletteId = image.paletteId; uint8_t *newPixels = GL_ConvertBuffer(image.pixels, image.size.x, image.size.y, ((image.flags & IMGF_IS_MASKED)? 2 : 1), image.paletteId, 4); if(newPixels != image.pixels) { M_Free(image.pixels); image.pixels = newPixels; image.pixelSize = 4; image.paletteId = 0; image.flags &= ~IMGF_IS_MASKED; } if(monochrome) { Desaturate(image.pixels, image.size.x, image.size.y, image.pixelSize); } int newWidth = 0, newHeight = 0; newPixels = GL_SmartFilter(scaleMethod, image.pixels, image.size.x, image.size.y, 0, &newWidth, &newHeight); image.size = Vector2ui(newWidth, newHeight); if(newPixels != image.pixels) { M_Free(image.pixels); image.pixels = newPixels; } EnhanceContrast(image.pixels, image.size.x, image.size.y, image.pixelSize); //SharpenPixels(image.pixels, image.size.x, image.size.y, image.pixelSize); //BlackOutlines(image.pixels, image.size.x, image.size.y, image.pixelSize); // Back to paletted+alpha? if(monochrome) { // No. We'll convert from RGB(+A) to Luminance(+A) and upload as is. // Replace the old buffer. Image_ConvertToLuminance(image); AmplifyLuma(image.pixels, image.size.x, image.size.y, image.pixelSize == 2); } else { // Yes. Quantize down from RGA(+A) to Paletted(+A), replacing the old image. newPixels = GL_ConvertBuffer(image.pixels, image.size.x, image.size.y, (origMasked? 2 : 1), origPaletteId, 4); if(newPixels != image.pixels) { M_Free(image.pixels); image.pixels = newPixels; image.pixelSize = (origMasked? 2 : 1); image.paletteId = origPaletteId; if(origMasked) { image.flags |= IMGF_IS_MASKED; } } } } } else if(image.pixelSize > 2) { if(monochrome) { Image_ConvertToLuminance(image); AmplifyLuma(image.pixels, image.size.x, image.size.y, image.pixelSize == 2); } } /* * Choose the final GL texture format. */ if(monochrome) { return image.pixelSize == 2? DGL_LUMINANCE_PLUS_A8 : DGL_LUMINANCE; } if(image.paletteId) { return (image.flags & IMGF_IS_MASKED)? DGL_COLOR_INDEX_8_PLUS_A8 : DGL_COLOR_INDEX_8; } return image.pixelSize == 2 ? DGL_LUMINANCE_PLUS_A8 : image.pixelSize == 3 ? DGL_RGB : image.pixelSize == 4 ? DGL_RGBA : DGL_LUMINANCE; } /** * Prepares the image for use as a detail GL texture in accordance with the * given specification. * * @param image The image to prepare (in place). * @param spec Specification describing any transformations which should * be applied to the image. * * Return values: * @param baMul Luminance equalization balance factor is written here. * @param hiMul Luminance equalization white-shift factor is written here. * @param loMul Luminance equalization black-shift factor is written here. * * @return The DGL texture format determined for the image. */ static dgltexformat_t prepareImageAsDetailTexture(image_t &image, detailvariantspecification_t const &spec, float *baMul, float *hiMul, float *loMul) { DENG_UNUSED(spec); // We want a luminance map. if(image.pixelSize > 2) { Image_ConvertToLuminance(image, false /*discard alpha*/); } // Try to normalize the luminance data so it works expectedly as a detail texture. EqualizeLuma(image.pixels, image.size.x, image.size.y, baMul, hiMul, loMul); return DGL_LUMINANCE; } void GL_PrepareTextureContent(texturecontent_t &c, GLuint glTexName, image_t &image, TextureVariantSpec const &spec, TextureManifest const &textureManifest) { DENG_ASSERT(glTexName != 0); DENG_ASSERT(image.pixels != 0); // Initialize and assign a GL name to the content. GL_InitTextureContent(&c); c.name = glTexName; switch(spec.type) { case TST_GENERAL: { variantspecification_t const &vspec = spec.variant; bool const noCompression = (vspec.flags & TSF_NO_COMPRESSION) != 0; // If the Upscale And Sharpen filter is enabled, scaling is applied // implicitly by prepareImageAsTexture(), so don't do it again. bool const noSmartFilter = (vspec.flags & TSF_UPSCALE_AND_SHARPEN) != 0; // Prepare the image for upload. dgltexformat_t dglFormat = prepareImageAsTexture(image, vspec); // Configure the texture content. c.format = dglFormat; c.width = image.size.x; c.height = image.size.y; c.pixels = image.pixels; c.paletteId = image.paletteId; if(noCompression || (image.size.x < 128 || image.size.y < 128)) c.flags |= TXCF_NO_COMPRESSION; if(vspec.gammaCorrection) c.flags |= TXCF_APPLY_GAMMACORRECTION; if(vspec.noStretch) c.flags |= TXCF_UPLOAD_ARG_NOSTRETCH; if(vspec.mipmapped) c.flags |= TXCF_MIPMAP; if(noSmartFilter) c.flags |= TXCF_UPLOAD_ARG_NOSMARTFILTER; c.magFilter = vspec.glMagFilter(); c.minFilter = vspec.glMinFilter(); c.anisoFilter = vspec.logicalAnisoLevel(); c.wrap[0] = vspec.wrapS; c.wrap[1] = vspec.wrapT; break; } case TST_DETAIL: { detailvariantspecification_t const &dspec = spec.detailVariant; // Prepare the image for upload. float baMul, hiMul, loMul; dgltexformat_t dglFormat = prepareImageAsDetailTexture(image, dspec, &baMul, &hiMul, &loMul); // Determine the gray mipmap factor. int grayMipmapFactor = dspec.contrast; if(baMul != 1 || hiMul != 1 || loMul != 1) { // Integrate the normalization factor with contrast. float const hiContrast = 1 - 1. / hiMul; float const loContrast = 1 - loMul; float const shift = ((hiContrast + loContrast) / 2); grayMipmapFactor = int(255 * de::clamp(0.f, dspec.contrast / 255.f - shift, 1.f)); // Announce the normalization. de::Uri uri = textureManifest.composeUri(); LOG_GL_VERBOSE("Normalized detail texture \"%s\" (balance: %f, high amp: %f, low amp: %f)") << uri << baMul << hiMul << loMul; } // Configure the texture content. c.format = dglFormat; c.flags = TXCF_GRAY_MIPMAP | TXCF_UPLOAD_ARG_NOSMARTFILTER; // Disable compression? if(image.size.x < 128 || image.size.y < 128) c.flags |= TXCF_NO_COMPRESSION; c.grayMipmap = grayMipmapFactor; c.width = image.size.x; c.height = image.size.y; c.pixels = image.pixels; c.anisoFilter = texAniso; c.magFilter = glmode[texMagMode]; c.minFilter = GL_LINEAR_MIPMAP_LINEAR; c.wrap[0] = GL_REPEAT; c.wrap[1] = GL_REPEAT; break; } default: // Invalid spec type. DENG_ASSERT(false); } } /** * Choose an internal texture format. * * @param format DGL texture format identifier. * @param allowCompression @c true == use compression if available. * @return The chosen texture format. */ static GLint ChooseTextureFormat(dgltexformat_t format, dd_bool allowCompression) { dd_bool compress = (allowCompression && GL_state.features.texCompression); switch(format) { case DGL_RGB: case DGL_COLOR_INDEX_8: if(!compress) return GL_RGB8; #if USE_TEXTURE_COMPRESSION_S3 if(GLInfo::extensions().EXT_texture_compression_s3tc) return GL_COMPRESSED_RGB_S3TC_DXT1_EXT; #endif return GL_COMPRESSED_RGB; case DGL_RGBA: case DGL_COLOR_INDEX_8_PLUS_A8: if(!compress) return GL_RGBA8; #if USE_TEXTURE_COMPRESSION_S3 if(GLInfo::extensions().EXT_texture_compression_s3tc) return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; #endif return GL_COMPRESSED_RGBA; case DGL_LUMINANCE: return !compress ? GL_LUMINANCE : GL_COMPRESSED_LUMINANCE; case DGL_LUMINANCE_PLUS_A8: return !compress ? GL_LUMINANCE_ALPHA : GL_COMPRESSED_LUMINANCE_ALPHA; default: App_Error("ChooseTextureFormat: Invalid source format %i.", (int) format); return 0; // Unreachable. } } /** * @param glFormat Identifier of the desired GL texture format. * @param loadFormat Identifier of the GL texture format used during upload. * @param pixels Texture pixel data to be uploaded. * @param width Width of the texture in pixels. * @param height Height of the texture in pixels. * @param genMipmaps If negative sets a specific mipmap level, e.g.: * @c -1, means mipmap level 1. * * @return @c true iff successful. */ static dd_bool uploadTexture(int glFormat, int loadFormat, const uint8_t* pixels, int width, int height, int genMipmaps) { const int packRowLength = 0, packAlignment = 1, packSkipRows = 0, packSkipPixels = 0; const int unpackRowLength = 0, unpackAlignment = 1, unpackSkipRows = 0, unpackSkipPixels = 0; int mipLevel = 0; DENG_ASSERT(pixels); if(!(GL_LUMINANCE_ALPHA == loadFormat || GL_LUMINANCE == loadFormat || GL_RGB == loadFormat || GL_RGBA == loadFormat)) { throw Error("texturecontent_t::uploadTexture", "Unsupported load format " + String::number(loadFormat)); } // Can't operate on null texture. if(width < 1 || height < 1) return false; // Check that the texture dimensions are valid. if(width > GLInfo::limits().maxTexSize || height > GLInfo::limits().maxTexSize) return false; if(!GL_state.features.texNonPowTwo && (width != M_CeilPow2(width) || height != M_CeilPow2(height))) return false; // Negative indices signify a specific mipmap level is being uploaded. if(genMipmaps < 0) { mipLevel = -genMipmaps; genMipmaps = 0; } DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // Automatic mipmap generation? if(GLInfo::extensions().SGIS_generate_mipmap && genMipmaps) glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP_SGIS, GL_TRUE); glPushClientAttrib(GL_CLIENT_PIXEL_STORE_BIT); glPixelStorei(GL_PACK_ROW_LENGTH, (GLint)packRowLength); glPixelStorei(GL_PACK_ALIGNMENT, (GLint)packAlignment); glPixelStorei(GL_PACK_SKIP_ROWS, (GLint)packSkipRows); glPixelStorei(GL_PACK_SKIP_PIXELS, (GLint)packSkipPixels); glPixelStorei(GL_UNPACK_ROW_LENGTH, (GLint)unpackRowLength); glPixelStorei(GL_UNPACK_ALIGNMENT, (GLint)unpackAlignment); glPixelStorei(GL_UNPACK_SKIP_ROWS, (GLint)unpackSkipRows); glPixelStorei(GL_UNPACK_SKIP_PIXELS, (GLint)unpackSkipPixels); if(genMipmaps && !GLInfo::extensions().SGIS_generate_mipmap) { // Build all mipmap levels. int neww, newh, bpp, w, h; void* image, *newimage; bpp = BytesPerPixel(loadFormat); if(bpp == 0) throw Error("texturecontent_t::uploadTexture", "Unknown GL format " + String::number(loadFormat)); GL_OptimalTextureSize(width, height, false, true, &w, &h); if(w != width || h != height) { // Must rescale image to get "top" mipmap texture image. image = GL_ScaleBufferEx(pixels, width, height, bpp, /*GL_UNSIGNED_BYTE,*/ unpackRowLength, unpackAlignment, unpackSkipRows, unpackSkipPixels, w, h, /*GL_UNSIGNED_BYTE,*/ packRowLength, packAlignment, packSkipRows, packSkipPixels); if(!image) throw Error("texturecontent_t::uploadTexture", "Unknown error resizing mipmap level #0"); } else { image = (void*) pixels; } for(;;) { glTexImage2D(GL_TEXTURE_2D, mipLevel, (GLint)glFormat, w, h, 0, (GLint)loadFormat, GL_UNSIGNED_BYTE, image); if(w == 1 && h == 1) break; ++mipLevel; neww = (w < 2) ? 1 : w / 2; newh = (h < 2) ? 1 : h / 2; newimage = GL_ScaleBufferEx(image, w, h, bpp, /*GL_UNSIGNED_BYTE,*/ unpackRowLength, unpackAlignment, unpackSkipRows, unpackSkipPixels, neww, newh, /*GL_UNSIGNED_BYTE,*/ packRowLength, packAlignment, packSkipRows, packSkipPixels); if(!newimage) throw Error("texturecontent_t::uploadTexture", "Unknown error resizing mipmap level #" + String::number(mipLevel)); if(image != pixels) M_Free(image); image = newimage; w = neww; h = newh; } if(image != pixels) M_Free(image); } else { glTexImage2D(GL_TEXTURE_2D, mipLevel, (GLint)glFormat, (GLsizei)width, (GLsizei)height, 0, (GLint)loadFormat, GL_UNSIGNED_BYTE, pixels); } glPopClientAttrib(); DENG_ASSERT(!Sys_GLCheckError()); return true; } /** * @param glFormat Identifier of the desired GL texture format. * @param loadFormat Identifier of the GL texture format used during upload. * @param pixels Texture pixel data to be uploaded. * @param width Width of the texture in pixels. * @param height Height of the texture in pixels. * @param grayFactor Strength of the blend where @c 0:none @c 1:full. * * @return @c true iff successful. */ static dd_bool uploadTextureGrayMipmap(int glFormat, int loadFormat, const uint8_t* pixels, int width, int height, float grayFactor) { int i, w, h, numpels = width * height, numLevels, pixelSize; uint8_t* image, *faded, *out; const uint8_t* in; float invFactor; DENG_ASSERT(pixels); if(!(GL_RGB == loadFormat || GL_LUMINANCE == loadFormat)) { throw Error("texturecontent_t::uploadTextureGrayMipmap", "Unsupported load format " + String::number(loadFormat)); } pixelSize = (loadFormat == GL_LUMINANCE? 1 : 3); // Can't operate on null texture. if(width < 1 || height < 1) return false; // Check that the texture dimensions are valid. if(!GL_state.features.texNonPowTwo && (width != M_CeilPow2(width) || height != M_CeilPow2(height))) return false; if(width > GLInfo::limits().maxTexSize || height > GLInfo::limits().maxTexSize) return false; numLevels = GL_NumMipmapLevels(width, height); grayFactor = MINMAX_OF(0, grayFactor, 1); invFactor = 1 - grayFactor; // Buffer used for the faded texture. faded = (uint8_t*) M_Malloc(numpels / 4); image = (uint8_t*) M_Malloc(numpels); // Initial fading. in = pixels; out = image; for(i = 0; i < numpels; ++i) { *out++ = (uint8_t) MINMAX_OF(0, (*in * grayFactor + 127 * invFactor), 255); in += pixelSize; } // Upload the first level right away. glTexImage2D(GL_TEXTURE_2D, 0, glFormat, width, height, 0, (GLint)loadFormat, GL_UNSIGNED_BYTE, image); // Generate all mipmaps levels. w = width; h = height; for(i = 0; i < numLevels; ++i) { GL_DownMipmap8(image, faded, w, h, (i * 1.75f) / numLevels); // Go down one level. if(w > 1) w /= 2; if(h > 1) h /= 2; glTexImage2D(GL_TEXTURE_2D, i + 1, glFormat, w, h, 0, (GLint)loadFormat, GL_UNSIGNED_BYTE, faded); } // Do we need to free the temp buffer? M_Free(faded); M_Free(image); DENG_ASSERT(!Sys_GLCheckError()); return true; } /// @note Texture parameters will NOT be set here! void GL_UploadTextureContent(texturecontent_t const &content, gl::UploadMethod method) { if(method == gl::Deferred) { GL_DeferTextureUpload(&content); return; } if(novideo) return; // Do this right away. No need to take a copy. bool generateMipmaps = (content.flags & (TXCF_MIPMAP|TXCF_GRAY_MIPMAP)) != 0; bool applyTexGamma = (content.flags & TXCF_APPLY_GAMMACORRECTION) != 0; bool noCompression = (content.flags & TXCF_NO_COMPRESSION) != 0; bool noSmartFilter = (content.flags & TXCF_UPLOAD_ARG_NOSMARTFILTER) != 0; bool noStretch = (content.flags & TXCF_UPLOAD_ARG_NOSTRETCH) != 0; int loadWidth = content.width, loadHeight = content.height; uint8_t const *loadPixels = content.pixels; dgltexformat_t dglFormat = content.format; if(DGL_COLOR_INDEX_8 == dglFormat || DGL_COLOR_INDEX_8_PLUS_A8 == dglFormat) { // Convert a paletted source image to truecolor. uint8_t *newPixels = GL_ConvertBuffer(loadPixels, loadWidth, loadHeight, DGL_COLOR_INDEX_8_PLUS_A8 == dglFormat ? 2 : 1, content.paletteId, DGL_COLOR_INDEX_8_PLUS_A8 == dglFormat ? 4 : 3); if(loadPixels != content.pixels) { M_Free(const_cast(loadPixels)); } loadPixels = newPixels; dglFormat = DGL_COLOR_INDEX_8_PLUS_A8 == dglFormat ? DGL_RGBA : DGL_RGB; } if(DGL_RGBA == dglFormat || DGL_RGB == dglFormat) { int comps = (DGL_RGBA == dglFormat ? 4 : 3); if(applyTexGamma && texGamma > .0001f) { uint8_t* dst, *localBuffer = 0; long const numPels = loadWidth * loadHeight; uint8_t const *src = loadPixels; if(loadPixels == content.pixels) { localBuffer = (uint8_t *) M_Malloc(comps * numPels); dst = localBuffer; } else { dst = const_cast(loadPixels); } for(long i = 0; i < numPels; ++i) { dst[CR] = texGammaLut[src[CR]]; dst[CG] = texGammaLut[src[CG]]; dst[CB] = texGammaLut[src[CB]]; if(comps == 4) dst[CA] = src[CA]; dst += comps; src += comps; } if(localBuffer) { if(loadPixels != content.pixels) { M_Free(const_cast(loadPixels)); } loadPixels = localBuffer; } } if(useSmartFilter && !noSmartFilter) { if(comps == 3) { // Need to add an alpha channel. uint8_t *newPixels = GL_ConvertBuffer(loadPixels, loadWidth, loadHeight, 3, 0, 4); if(loadPixels != content.pixels) { M_Free(const_cast(loadPixels)); } loadPixels = newPixels; dglFormat = DGL_RGBA; } uint8_t *filtered = GL_SmartFilter(GL_ChooseSmartFilter(loadWidth, loadHeight, 0), loadPixels, loadWidth, loadHeight, ICF_UPSCALE_SAMPLE_WRAP, &loadWidth, &loadHeight); if(filtered != loadPixels) { if(loadPixels != content.pixels) { M_Free(const_cast(loadPixels)); } loadPixels = filtered; } } } if(DGL_LUMINANCE_PLUS_A8 == dglFormat) { // Needs converting. This adds some overhead. long const numPixels = content.width * content.height; uint8_t *localBuffer = (uint8_t *) M_Malloc(2 * numPixels); uint8_t *pixel = localBuffer; for(long i = 0; i < numPixels; ++i) { pixel[0] = loadPixels[i]; pixel[1] = loadPixels[numPixels + i]; pixel += 2; } if(loadPixels != content.pixels) { M_Free(const_cast(loadPixels)); } loadPixels = localBuffer; } if(DGL_LUMINANCE == dglFormat && (content.flags & TXCF_CONVERT_8BIT_TO_ALPHA)) { // Needs converting. This adds some overhead. long const numPixels = content.width * content.height; uint8_t *localBuffer = (uint8_t *) M_Malloc(2 * numPixels); // Move the average color to the alpha channel, make the actual color white. uint8_t *pixel = localBuffer; for(long i = 0; i < numPixels; ++i) { pixel[0] = 255; pixel[1] = loadPixels[i]; pixel += 2; } if(loadPixels != content.pixels) { M_Free(const_cast(loadPixels)); } loadPixels = localBuffer; dglFormat = DGL_LUMINANCE_PLUS_A8; } // Calculate the final dimensions for the texture, as required by // the graphics hardware and/or engine configuration. int width = loadWidth, height = loadHeight; noStretch = GL_OptimalTextureSize(width, height, noStretch, generateMipmaps, &loadWidth, &loadHeight); // Do we need to resize? if(width != loadWidth || height != loadHeight) { int comps = BytesPerPixelFmt(dglFormat); if(noStretch) { // Copy the texture into a power-of-two canvas. uint8_t *localBuffer = (uint8_t *) M_Calloc(comps * loadWidth * loadHeight); // Copy line by line. for(int i = 0; i < height; ++i) { std::memcpy(localBuffer + loadWidth * comps * i, loadPixels + width * comps * i, comps * width); } if(loadPixels != content.pixels) { M_Free(const_cast(loadPixels)); } loadPixels = localBuffer; } else { // Stretch into a new power-of-two texture. uint8_t *newPixels = GL_ScaleBuffer(loadPixels, width, height, comps, loadWidth, loadHeight); if(loadPixels != content.pixels) { M_Free(const_cast(loadPixels)); } loadPixels = newPixels; } } DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glBindTexture(GL_TEXTURE_2D, content.name); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, content.minFilter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, content.magFilter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, content.wrap[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, content.wrap[1]); if(GL_state.features.texFilterAniso) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, GL_GetTexAnisoMul(content.anisoFilter)); if(!(content.flags & TXCF_GRAY_MIPMAP)) { GLint loadFormat; switch(dglFormat) { case DGL_LUMINANCE_PLUS_A8: loadFormat = GL_LUMINANCE_ALPHA; break; case DGL_LUMINANCE: loadFormat = GL_LUMINANCE; break; case DGL_RGB: loadFormat = GL_RGB; break; case DGL_RGBA: loadFormat = GL_RGBA; break; default: throw Error("GL_UploadTextureContent", QString("Unknown format %1").arg(int(dglFormat))); } GLint glFormat = ChooseTextureFormat(dglFormat, !noCompression); if(!uploadTexture(glFormat, loadFormat, loadPixels, loadWidth, loadHeight, generateMipmaps ? true : false)) { throw Error("GL_UploadTextureContent", QString("TexImage failed (%1:%2 fmt%3)") .arg(content.name) .arg(Vector2i(loadWidth, loadHeight).asText()) .arg(int(dglFormat))); } } else { // Special fade-to-gray luminance texture (used for details). GLint glFormat, loadFormat; switch(dglFormat) { case DGL_LUMINANCE: loadFormat = GL_LUMINANCE; break; case DGL_RGB: loadFormat = GL_RGB; break; default: throw Error("GL_UploadTextureContent", QString("Unknown format %1").arg(int(dglFormat))); } glFormat = ChooseTextureFormat(DGL_LUMINANCE, !noCompression); if(!uploadTextureGrayMipmap(glFormat, loadFormat, loadPixels, loadWidth, loadHeight, content.grayMipmap * reciprocal255)) { throw Error("GL_UploadTextureContent", QString("TexImageGrayMipmap failed (%1:%2 fmt%3)") .arg(content.name) .arg(Vector2i(loadWidth, loadHeight).asText()) .arg(int(dglFormat))); } } if(loadPixels != content.pixels) { M_Free(const_cast(loadPixels)); } } doomsday-stable-1.15.7/doomsday/client/src/gl/gl_drawvectorgraphic.cpp0000664000175000017500000001122512641367670025367 0ustar jaakkojaakko/**\file gl_drawvectorgraphic.cpp * * @authors Copyright © 2008-2010 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #define DENG_NO_API_MACROS_SVG #include "de_platform.h" #include "dd_share.h" #include "gl/svg.h" #include #include #include #include #define DEFAULT_SCALE (1) #define DEFAULT_ANGLE (0) static dd_bool inited = false; static uint svgCount; static Svg** svgs; static Svg* svgForId(svgid_t id) { if(id != 0) { uint i; for(i = 0; i < svgCount; ++i) { Svg* svg = svgs[i]; if(Svg_UniqueId(svg) == id) return svg; } } return NULL; } /// @return 1-based index for @a svg if found, else zero. static uint indexForSvg(Svg* svg) { if(svg) { uint i; for(i = 0; i < svgCount; ++i) { if(svgs[i] == svg) return i+1; } } return 0; } /** * Link @a svg into the global collection. * @pre Not presently linked. */ static Svg* insertSvg(Svg* svg) { if(svg) { svgs = (Svg**) M_Realloc(svgs, sizeof(*svgs) * ++svgCount); svgs[svgCount-1] = svg; } return svg; } /** * Unlink @a svg if present in the global collection. */ static Svg* removeSvg(Svg* svg) { uint index = indexForSvg(svg); if(index) { index--; // 1-based index. // Unlink from the collection. if(index != svgCount-1) memmove(svgs + index, svgs + index + 1, sizeof(*svgs) * (svgCount - index - 1)); svgs = (Svg**) M_Realloc(svgs, sizeof(*svgs) * --svgCount); } return svg; } static void deleteSvg(Svg* svg) { if(!svg) return; removeSvg(svg); Svg_Delete(svg); } static void clearSvgs(void) { while(svgCount) { deleteSvg(*svgs); } } void R_InitSvgs(void) { if(inited) { // Allow re-init. clearSvgs(); } else { // First init. svgs = NULL; svgCount = 0; } inited = true; } void R_ShutdownSvgs(void) { if(!inited) return; clearSvgs(); inited = false; } void R_UnloadSvgs(void) { uint i; if(!inited) return; if(DD_GetInteger(DD_NOVIDEO) || DD_GetInteger(DD_DEDICATED)) return; // Nothing to do. for(i = 0; i < svgCount; ++i) { Svg_Unload(svgs[i]); } } DENG_EXTERN_C void GL_DrawSvg3(svgid_t id, const Point2Rawf* origin, float scale, float angle) { Svg* svg = svgForId(id); DENG_ASSERT(origin != 0); DENG_ASSERT(svg != 0); if(!Svg_Prepare(svg)) { LOGDEV_GL_ERROR("Cannot draw SVG #%i: failed to prepare") << id; return; } DGL_MatrixMode(DGL_MODELVIEW); DGL_Translatef(origin->x, origin->y, 0); if(angle != 0 || scale != 1) { DGL_PushMatrix(); DGL_Rotatef(angle, 0, 0, 1); DGL_Scalef(scale, scale, 1); } Svg_Draw(svg); DGL_MatrixMode(DGL_MODELVIEW); if(angle != 0 || scale != 1) DGL_PopMatrix(); DGL_Translatef(-origin->x, -origin->y, 0); } DENG_EXTERN_C void GL_DrawSvg2(svgid_t id, const Point2Rawf* origin, float scale) { GL_DrawSvg3(id, origin, scale, DEFAULT_ANGLE); } DENG_EXTERN_C void GL_DrawSvg(svgid_t id, const Point2Rawf* origin) { GL_DrawSvg2(id, origin, DEFAULT_SCALE); } DENG_EXTERN_C void R_NewSvg(svgid_t id, const def_svgline_t* lines, uint numLines) { Svg* svg, *oldSvg; // Valid id? DENG_ASSERT(id != 0); // A new vector graphic. svg = Svg_FromDef(id, lines, numLines); if(!svg) { LOGDEV_GL_ERROR("Failed to construct SVG #%i") << id; return; } // Already a vector graphic with this id? oldSvg = svgForId(id); if(oldSvg) { // We are replacing an existing vector graphic. deleteSvg(oldSvg); } // Add the new SVG to the global collection. insertSvg(svg); } DENG_DECLARE_API(Svg) = { { DE_API_SVG }, R_NewSvg, GL_DrawSvg, GL_DrawSvg2, GL_DrawSvg3 }; doomsday-stable-1.15.7/doomsday/client/src/gl/dgl_draw.cpp0000664000175000017500000002374412641367670022763 0ustar jaakkojaakko/** @file dgl_draw.cpp Drawing Operations and Vertex Arrays. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #define DENG_NO_API_MACROS_GL #include "de_base.h" #include "de_console.h" #include "de_graphics.h" #include "de_misc.h" #include "sys_system.h" #include "gl/sys_opengl.h" #include #include #include using namespace de; static int primLevel = 0; static DGLuint inList = 0; #ifdef _DEBUG static dd_bool inPrim = false; #endif dd_bool GL_NewList(DGLuint list, int mode) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // We enter a New/End list section. #ifdef _DEBUG if(inList) App_Error("GL_NewList: Already in list"); Sys_GLCheckError(); #endif if(list) { // A specific list id was requested. Is it free? if(glIsList(list)) { #if _DEBUG App_Error("GL_NewList: List %u already in use.", (unsigned int) list); #endif return false; } } else { // Just get a new list id, it doesn't matter. list = glGenLists(1); } glNewList(list, mode == DGL_COMPILE? GL_COMPILE : GL_COMPILE_AND_EXECUTE); inList = list; return true; } DGLuint GL_EndList(void) { DGLuint currentList = inList; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glEndList(); #ifdef _DEBUG inList = 0; Sys_GLCheckError(); #endif return currentList; } void GL_CallList(DGLuint list) { if(!list) return; // We do not consider zero a valid list id. DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glCallList(list); } void GL_DeleteLists(DGLuint list, int range) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glDeleteLists(list, range); } #undef DGL_Color3ub DENG_EXTERN_C void DGL_Color3ub(DGLubyte r, DGLubyte g, DGLubyte b) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glColor3ub(r, g, b); } #undef DGL_Color3ubv DENG_EXTERN_C void DGL_Color3ubv(const DGLubyte* vec) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glColor3ubv(vec); } #undef DGL_Color4ub DENG_EXTERN_C void DGL_Color4ub(DGLubyte r, DGLubyte g, DGLubyte b, DGLubyte a) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glColor4ub(r, g, b, a); } #undef DGL_Color4ubv DENG_EXTERN_C void DGL_Color4ubv(const DGLubyte* vec) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glColor4ubv(vec); } #undef DGL_Color3f DENG_EXTERN_C void DGL_Color3f(float r, float g, float b) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glColor3f(r, g, b); } #undef DGL_Color3fv DENG_EXTERN_C void DGL_Color3fv(const float* vec) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glColor3fv(vec); } #undef DGL_Color4f DENG_EXTERN_C void DGL_Color4f(float r, float g, float b, float a) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glColor4f(r, g, b, a); } #undef DGL_Color4fv DENG_EXTERN_C void DGL_Color4fv(const float* vec) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glColor4fv(vec); } #undef DGL_TexCoord2f DENG_EXTERN_C void DGL_TexCoord2f(byte target, float s, float t) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glMultiTexCoord2f(GL_TEXTURE0 + target, s, t); } #undef DGL_TexCoord2fv DENG_EXTERN_C void DGL_TexCoord2fv(byte target, float* vec) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glMultiTexCoord2fv(GL_TEXTURE0 + target, vec); } #undef DGL_Vertex2f DENG_EXTERN_C void DGL_Vertex2f(float x, float y) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glVertex2f(x, y); } #undef DGL_Vertex2fv DENG_EXTERN_C void DGL_Vertex2fv(const float* vec) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glVertex2fv(vec); } #undef DGL_Vertex3f DENG_EXTERN_C void DGL_Vertex3f(float x, float y, float z) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glVertex3f(x, y, z); } #undef DGL_Vertex3fv DENG_EXTERN_C void DGL_Vertex3fv(const float* vec) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glVertex3fv(vec); } #undef DGL_Vertices2ftv DENG_EXTERN_C void DGL_Vertices2ftv(int num, const dgl_ft2vertex_t* vec) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); for(; num > 0; num--, vec++) { glTexCoord2fv(vec->tex); glVertex2fv(vec->pos); } } #undef DGL_Vertices3ftv DENG_EXTERN_C void DGL_Vertices3ftv(int num, const dgl_ft3vertex_t* vec) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); for(; num > 0; num--, vec++) { glTexCoord2fv(vec->tex); glVertex3fv(vec->pos); } } #undef DGL_Vertices3fctv DENG_EXTERN_C void DGL_Vertices3fctv(int num, const dgl_fct3vertex_t* vec) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); for(; num > 0; num--, vec++) { glColor4fv(vec->color); glTexCoord2fv(vec->tex); glVertex3fv(vec->pos); } } #undef DGL_Begin DENG_EXTERN_C void DGL_Begin(dglprimtype_t mode) { if(novideo) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); // We enter a Begin/End section. primLevel++; #ifdef _DEBUG if(inPrim) App_Error("OpenGL: already inPrim"); inPrim = true; Sys_GLCheckError(); #endif glBegin(mode == DGL_POINTS ? GL_POINTS : mode == DGL_LINES ? GL_LINES : mode == DGL_TRIANGLES ? GL_TRIANGLES : mode == DGL_TRIANGLE_FAN ? GL_TRIANGLE_FAN : mode == DGL_TRIANGLE_STRIP ? GL_TRIANGLE_STRIP : mode == DGL_QUAD_STRIP ? GL_QUAD_STRIP : GL_QUADS); } void DGL_AssertNotInPrimitive(void) { DENG_ASSERT(!inPrim); } #undef DGL_End DENG_EXTERN_C void DGL_End(void) { if(novideo) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); if(primLevel > 0) { primLevel--; glEnd(); } #ifdef _DEBUG inPrim = false; Sys_GLCheckError(); #endif } #undef DGL_NewList DENG_EXTERN_C dd_bool DGL_NewList(DGLuint list, int mode) { return GL_NewList(list, mode); } #undef DGL_EndList DENG_EXTERN_C DGLuint DGL_EndList(void) { return GL_EndList(); } #undef DGL_CallList DENG_EXTERN_C void DGL_CallList(DGLuint list) { GL_CallList(list); } #undef DGL_DeleteLists DENG_EXTERN_C void DGL_DeleteLists(DGLuint list, int range) { GL_DeleteLists(list, range); } #undef DGL_DrawLine DENG_EXTERN_C void DGL_DrawLine(float x1, float y1, float x2, float y2, float r, float g, float b, float a) { GL_DrawLine(x1, y1, x2, y2, r, g, b, a); } #undef DGL_DrawRect DENG_EXTERN_C void DGL_DrawRect(RectRaw const *rect) { if(!rect) return; GL_DrawRect(Rectanglei::fromSize(Vector2i(rect->origin.xy), Vector2ui(rect->size.width, rect->size.height))); } #undef DGL_DrawRect2 DENG_EXTERN_C void DGL_DrawRect2(int x, int y, int w, int h) { GL_DrawRect2(x, y, w, h); } #undef DGL_DrawRectf DENG_EXTERN_C void DGL_DrawRectf(RectRawf const *rect) { GL_DrawRectf(rect); } #undef DGL_DrawRectf2 DENG_EXTERN_C void DGL_DrawRectf2(double x, double y, double w, double h) { GL_DrawRectf2(x, y, w, h); } #undef DGL_DrawRectf2Color DENG_EXTERN_C void DGL_DrawRectf2Color(double x, double y, double w, double h, float r, float g, float b, float a) { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glColor4f(r, g, b, a); GL_DrawRectf2(x, y, w, h); } #undef DGL_DrawRectf2Tiled DENG_EXTERN_C void DGL_DrawRectf2Tiled(double x, double y, double w, double h, int tw, int th) { GL_DrawRectf2Tiled(x, y, w, h, tw, th); } #undef DGL_DrawCutRectfTiled DENG_EXTERN_C void DGL_DrawCutRectfTiled(RectRawf const *rect, int tw, int th, int txoff, int tyoff, RectRawf const *cutRect) { GL_DrawCutRectfTiled(rect, tw, th, txoff, tyoff, cutRect); } #undef DGL_DrawCutRectf2Tiled DENG_EXTERN_C void DGL_DrawCutRectf2Tiled(double x, double y, double w, double h, int tw, int th, int txoff, int tyoff, double cx, double cy, double cw, double ch) { GL_DrawCutRectf2Tiled(x, y, w, h, tw, th, txoff, tyoff, cx, cy, cw, ch); } #undef DGL_DrawQuadOutline DENG_EXTERN_C void DGL_DrawQuadOutline(Point2Raw const *tl, Point2Raw const *tr, Point2Raw const *br, Point2Raw const *bl, float const color[4]) { if(!tl || !tr || !br || !bl || (color && !(color[CA] > 0))) return; DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); if(color) DGL_Color4fv(color); glBegin(GL_LINE_LOOP); glVertex2iv((const GLint*)tl->xy); glVertex2iv((const GLint*)tr->xy); glVertex2iv((const GLint*)br->xy); glVertex2iv((const GLint*)bl->xy); glEnd(); } #undef DGL_DrawQuad2Outline DENG_EXTERN_C void DGL_DrawQuad2Outline(int tlX, int tlY, int trX, int trY, int brX, int brY, int blX, int blY, const float color[4]) { Point2Raw tl, tr, bl, br; tl.x = tlX; tl.y = tlY; tr.x = trX; tr.y = trY; br.x = brX; br.y = brY; bl.x = blX; bl.y = blY; DGL_DrawQuadOutline(&tl, &tr, &br, &bl, color); } doomsday-stable-1.15.7/doomsday/client/src/gl/gl_texmanager.cpp0000664000175000017500000002732012641367670024007 0ustar jaakkojaakko/** @file gl_texmanager.cpp GL-Texture management. * * @todo This file needs to be split into smaller portions. * * @authors Copyright © 1999-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * @authors Copyright © 2002 Graham Jackson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "gl/gl_texmanager.h" #include "de_filesys.h" #include "de_resource.h" #include "clientapp.h" #include "dd_main.h" // App_ResourceSystem() #include "def_main.h" #include "sys_system.h" #include "ui/progress.h" #include "gl/gl_main.h" // DENG_ASSERT_GL_CONTEXT_ACTIVE #include "gl/texturecontent.h" #include "render/r_main.h" // R_BuildTexGammaLut #include "render/rend_halo.h" // haloRealistic #include "render/rend_main.h" // misc global vars #include "render/rend_particle.h" // Rend_ParticleLoadSystemTextures() #include "resource/hq2x.h" #include #include #include #include #include #include using namespace de; static bool initedOk = false; // Init done. // Names of the dynamic light textures. static DGLuint lightingTextures[NUM_LIGHTING_TEXTURES]; // Names of the flare textures (halos). static DGLuint sysFlareTextures[NUM_SYSFLARE_TEXTURES]; void GL_InitTextureManager() { if(initedOk) { GL_LoadLightingSystemTextures(); GL_LoadFlareTextures(); Rend_ParticleLoadSystemTextures(); return; // Already been here. } // Disable the use of 'high resolution' textures and/or patches? noHighResTex = CommandLine_Exists("-nohightex"); noHighResPatches = CommandLine_Exists("-nohighpat"); // Should we allow using external resources with PWAD textures? highResWithPWAD = CommandLine_Exists("-pwadtex"); // System textures. zap(sysFlareTextures); zap(lightingTextures); GL_InitSmartFilterHQ2x(); // Initialization done. initedOk = true; } static int reloadTextures(void *context) { bool const usingBusyMode = *static_cast(context); /// @todo re-upload ALL textures currently in use. GL_LoadLightingSystemTextures(); GL_LoadFlareTextures(); Rend_ParticleLoadSystemTextures(); Rend_ParticleLoadExtraTextures(); if(usingBusyMode) { Con_SetProgress(200); BusyMode_WorkerEnd(); } return 0; } void GL_TexReset() { if(!initedOk) return; App_ResourceSystem().releaseAllGLTextures(); LOG_GL_VERBOSE("Released all GL textures"); bool useBusyMode = !BusyMode_Active(); if(useBusyMode) { BusyMode_FreezeGameForBusyMode(); Con_InitProgress(200); BusyMode_RunNewTaskWithName(BUSYF_ACTIVITY | (verbose? BUSYF_CONSOLE_OUTPUT : 0), reloadTextures, &useBusyMode, "Reseting textures..."); } else { reloadTextures(&useBusyMode); } } void GL_LoadLightingSystemTextures() { if(novideo || !initedOk) return; // Preload lighting system textures. GL_PrepareLSTexture(LST_DYNAMIC); GL_PrepareLSTexture(LST_GRADIENT); GL_PrepareLSTexture(LST_CAMERA_VIGNETTE); } void GL_ReleaseAllLightingSystemTextures() { if(novideo || !initedOk) return; glDeleteTextures(NUM_LIGHTING_TEXTURES, (GLuint const *) lightingTextures); zap(lightingTextures); } GLuint GL_PrepareLSTexture(lightingtexid_t which) { if(novideo) return 0; if(which < 0 || which >= NUM_LIGHTING_TEXTURES) return 0; static const struct TexDef { char const *name; gfxmode_t mode; } texDefs[NUM_LIGHTING_TEXTURES] = { /* LST_DYNAMIC */ { "dlight", LGM_WHITE_ALPHA }, /* LST_GRADIENT */ { "wallglow", LGM_WHITE_ALPHA }, /* LST_RADIO_CO */ { "radioco", LGM_WHITE_ALPHA }, /* LST_RADIO_CC */ { "radiocc", LGM_WHITE_ALPHA }, /* LST_RADIO_OO */ { "radiooo", LGM_WHITE_ALPHA }, /* LST_RADIO_OE */ { "radiooe", LGM_WHITE_ALPHA }, /* LST_CAMERA_VIGNETTE */ { "vignette", LGM_NORMAL } }; struct TexDef const &def = texDefs[which]; if(!lightingTextures[which]) { image_t image; if(GL_LoadExtImage(image, def.name, def.mode)) { // Loaded successfully and converted accordingly. // Upload the image to GL. DGLuint glName = GL_NewTextureWithParams( ( image.pixelSize == 2 ? DGL_LUMINANCE_PLUS_A8 : image.pixelSize == 3 ? DGL_RGB : image.pixelSize == 4 ? DGL_RGBA : DGL_LUMINANCE ), image.size.x, image.size.y, image.pixels, TXCF_NO_COMPRESSION, 0, GL_LINEAR, GL_LINEAR, -1 /*best anisotropy*/, ( which == LST_GRADIENT? GL_REPEAT : GL_CLAMP_TO_EDGE ), GL_CLAMP_TO_EDGE); lightingTextures[which] = glName; } Image_ClearPixelData(image); } DENG2_ASSERT(lightingTextures[which] != 0); return lightingTextures[which]; } void GL_LoadFlareTextures() { if(novideo || !initedOk) return; GL_PrepareSysFlaremap(FXT_ROUND); GL_PrepareSysFlaremap(FXT_FLARE); if(!haloRealistic) { GL_PrepareSysFlaremap(FXT_BRFLARE); GL_PrepareSysFlaremap(FXT_BIGFLARE); } } void GL_ReleaseAllFlareTextures() { if(novideo || !initedOk) return; glDeleteTextures(NUM_SYSFLARE_TEXTURES, (GLuint const *) sysFlareTextures); zap(sysFlareTextures); } GLuint GL_PrepareSysFlaremap(flaretexid_t which) { if(novideo) return 0; if(which < 0 || which >= NUM_SYSFLARE_TEXTURES) return 0; static const struct TexDef { char const *name; } texDefs[NUM_SYSFLARE_TEXTURES] = { /* FXT_ROUND */ { "dlight" }, /* FXT_FLARE */ { "flare" }, /* FXT_BRFLARE */ { "brflare" }, /* FXT_BIGFLARE */ { "bigflare" } }; struct TexDef const &def = texDefs[which]; if(!sysFlareTextures[which]) { image_t image; if(GL_LoadExtImage(image, def.name, LGM_WHITE_ALPHA)) { // Loaded successfully and converted accordingly. // Upload the image to GL. DGLuint glName = GL_NewTextureWithParams( ( image.pixelSize == 2 ? DGL_LUMINANCE_PLUS_A8 : image.pixelSize == 3 ? DGL_RGB : image.pixelSize == 4 ? DGL_RGBA : DGL_LUMINANCE ), image.size.x, image.size.y, image.pixels, TXCF_NO_COMPRESSION, 0, GL_LINEAR, GL_LINEAR, 0 /*no anisotropy*/, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE); sysFlareTextures[which] = glName; } Image_ClearPixelData(image); } DENG2_ASSERT(sysFlareTextures[which] != 0); return sysFlareTextures[which]; } GLuint GL_PrepareFlaremap(de::Uri const &resourceUri) { if(resourceUri.path().length() == 1) { // Select a system flare by numeric identifier? int number = resourceUri.path().toStringRef().first().digitValue(); if(number == 0) return 0; // automatic if(number >= 1 && number <= 4) { return GL_PrepareSysFlaremap(flaretexid_t(number - 1)); } } if(Texture *tex = App_ResourceSystem().texture("Flaremaps", resourceUri)) { if(TextureVariant const *variant = tex->prepareVariant(Rend_HaloTextureSpec())) { return variant->glName(); } // Dang... } return 0; } static res::Source loadRaw(image_t &image, rawtex_t const &raw) { de::FS1 &fileSys = App_FileSystem(); // First try an external resource. try { String foundPath = fileSys.findPath(de::Uri("Patches", Path(raw.name)), RLF_DEFAULT, App_ResourceClass(RC_GRAPHIC)); // Ensure the found path is absolute. foundPath = App_BasePath() / foundPath; return GL_LoadImage(image, foundPath)? res::External : res::None; } catch(FS1::NotFoundError const&) {} // Ignore this error. try { FileHandle &file = fileSys.openLump(fileSys.lump(raw.lumpNum)); if(Image_LoadFromFile(image, file)) { fileSys.releaseFile(file.file()); delete &file; return res::Original; } // It must be an old-fashioned "raw" image. #define RAW_WIDTH 320 #define RAW_HEIGHT 200 size_t const fileLength = file.length(); Image_Init(image); image.size = Vector2ui(RAW_WIDTH, fileLength / RAW_WIDTH); image.pixelSize = 1; // Load the raw image data. size_t const numPels = RAW_WIDTH * RAW_HEIGHT; image.pixels = (uint8_t *) M_Malloc(3 * numPels); if(fileLength < 3 * numPels) { std::memset(image.pixels, 0, 3 * numPels); } file.read(image.pixels, fileLength); fileSys.releaseFile(file.file()); delete &file; return res::Original; #undef RAW_HEIGHT #undef RAW_WIDTH } catch(LumpIndex::NotFoundError const &) {} // Ignore error. return res::None; } GLuint GL_PrepareRawTexture(rawtex_t &raw) { if(raw.lumpNum < 0 || raw.lumpNum >= App_FileSystem().lumpCount()) return 0; if(!raw.tex) { image_t image; Image_Init(image); if(loadRaw(image, raw) == res::External) { // Loaded an external raw texture. raw.tex = GL_NewTextureWithParams(image.pixelSize == 4? DGL_RGBA : DGL_RGB, image.size.x, image.size.y, image.pixels, 0, 0, GL_NEAREST, (filterUI ? GL_LINEAR : GL_NEAREST), 0 /*no anisotropy*/, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE); } else { raw.tex = GL_NewTextureWithParams( (image.flags & IMGF_IS_MASKED)? DGL_COLOR_INDEX_8_PLUS_A8 : image.pixelSize == 4? DGL_RGBA : image.pixelSize == 3? DGL_RGB : DGL_COLOR_INDEX_8, image.size.x, image.size.y, image.pixels, 0, 0, GL_NEAREST, (filterUI? GL_LINEAR:GL_NEAREST), 0 /*no anisotropy*/, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE); } raw.width = image.size.x; raw.height = image.size.y; Image_ClearPixelData(image); } return raw.tex; } void GL_SetRawTexturesMinFilter(int newMinFilter) { foreach(rawtex_t *raw, App_ResourceSystem().collectRawTextures()) { if(raw->tex) // Is the texture loaded? { DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); glBindTexture(GL_TEXTURE_2D, raw->tex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, newMinFilter); } } } void GL_ReleaseTexturesForRawImages() { foreach(rawtex_t *raw, App_ResourceSystem().collectRawTextures()) { if(raw->tex) { glDeleteTextures(1, (GLuint const *) &raw->tex); raw->tex = 0; } } LOG_GL_VERBOSE("Released all GL textures for raw images"); } doomsday-stable-1.15.7/doomsday/client/src/m_nodepile.cpp0000664000175000017500000001331212641367670022677 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * m_nodepile.c: Specialized Node Allocation * * The 'piles' are allocated as PU_MAP. */ // HEADER FILES ------------------------------------------------------------ #include "de_platform.h" #include "de_misc.h" #include "dd_main.h" #include // MACROS ------------------------------------------------------------------ #define NP_MAX_NODES 65535 // Indices are shorts. // TYPES ------------------------------------------------------------------- // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- // PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- // EXTERNAL DATA DECLARATIONS ---------------------------------------------- // PUBLIC DATA DEFINITIONS ------------------------------------------------- // PRIVATE DATA DEFINITIONS ------------------------------------------------ // CODE -------------------------------------------------------------------- /** * Initialize (alloc) the nodepile with n nodes. * * @param pile Ptr to the pile to be initialized. * @param initial Number of nodes to allocate. */ void NP_Init(nodepile_t *pile, int initial) { size_t size; // Allocate room for at least two nodes. // Node zero is never used. if(initial < 2) initial = 2; size = sizeof(*pile->nodes) * initial; pile->nodes = (linknode_t *) Z_Calloc(size, PU_MAP, 0); pile->count = initial; // Index #1 is the first. pile->pos = 1; } /** * Adds a new node to a node pile. * Pos always has the index of the next node to check when allocating * a new node. Pos shouldn't be accessed outside this routine because its * value may prove to be outside the valid range. * * @param pile Ptr to nodepile to add the node to. * @param ptr Data to attach to the new node. */ nodeindex_t NP_New(nodepile_t *pile, void *ptr) { linknode_t *node; linknode_t *end = pile->nodes + pile->count; int i, newcount; linknode_t *newlist; dd_bool found; pile->pos %= pile->count; node = pile->nodes + pile->pos++; // Scan for an unused node, starting from current pos. i = 0; found = false; while(i < pile->count - 1 && !found) { if(node == end) node = pile->nodes + 1; // Wrap back to #1. if(!node->ptr) { // This is the one! found = true; } else { i++; node++; pile->pos++; } } if(!found) { // Damned, we ran out of nodes. Let's allocate more. if(pile->count == NP_MAX_NODES) { // This happens *theoretically* only in freakishly complex // maps with lots and lots of mobjs. App_Error("NP_New: Out of linknodes! Contact the developer.\n"); } // Double the number of nodes, but add at most 1024. if(pile->count >= 1024) newcount = pile->count + 1024; else newcount = pile->count * 2; if(newcount > NP_MAX_NODES) newcount = NP_MAX_NODES; newlist = (linknode_t *) Z_Malloc(sizeof(*newlist) * newcount, PU_MAP, 0); memcpy(newlist, pile->nodes, sizeof(*pile->nodes) * pile->count); memset(newlist + pile->count, 0, (newcount - pile->count) * sizeof(*newlist)); // Get rid of the old list and start using the new one. Z_Free(pile->nodes); pile->nodes = newlist; pile->pos = pile->count + 1; node = pile->nodes + pile->count; pile->count = newcount; } node->ptr = ptr; // Make it point to itself by default (a root, basically). node->next = node->prev = node - pile->nodes; return node->next; // Well, node's index, really. } /** * Links the node to the beginning of the ring. * * @param pile Nodepile ring to work with. * @param node Node to be linked. * @param root The root node to link the node to. */ void NP_Link(nodepile_t *pile, nodeindex_t node, nodeindex_t root) { linknode_t *nd = pile->nodes; nd[node].prev = root; nd[node].next = nd[root].next; nd[root].next = nd[nd[node].next].prev = node; } /** * Unlink a node from the ring (make it a root node) * * @param pile Nodepile ring to work with. * @param node Node to be unlinked. */ void NP_Unlink(nodepile_t *pile, nodeindex_t node) { linknode_t *nd = pile->nodes; // Try deciphering that! :-) (d->n->p = d->p, d->p->n = d->n) nd[nd[nd[node].next].prev = nd[node].prev].next = nd[node].next; // Make it link to itself (a root, basically). nd[node].next = nd[node].prev = node; } #if 0 // This is now a macro. /** * Caller must unlink first. */ void NP_Dismiss(nodepile_t *pile, nodeindex_t node) { pile->nodes[node].ptr = 0; } #endif doomsday-stable-1.15.7/doomsday/client/src/sys_system.cpp0000664000175000017500000001656512641367670023023 0ustar jaakkojaakko/** @file sys_system.cpp Abstract interfaces for platform specific services. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * @authors Copyright © 2006 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #ifdef WIN32 # define WIN32_LEAN_AND_MEAN # include # include #endif #include #ifdef MACOSX # include #endif #ifdef WIN32 # include #endif #include #include #include "de_console.h" #include "de_system.h" #include "de_graphics.h" #include "de_misc.h" #ifdef __CLIENT__ # include "clientapp.h" #endif #include "dd_main.h" #include "dd_loop.h" #ifdef __CLIENT__ # include "gl/gl_main.h" #endif #include "ui/nativeui.h" #include "network/net_main.h" #include "network/net_buf.h" #include "audio/s_main.h" #if defined(WIN32) && !defined(_DEBUG) # define DENG_CATCH_SIGNALS #endif int novideo; // if true, stay in text mode for debugging static dd_bool appShutdown = false; ///< Set to true when we should exit (normally). #ifdef DENG_CATCH_SIGNALS /** * Borrowed from Lee Killough. */ static void C_DECL handler(int s) { signal(s, SIG_IGN); // Ignore future instances of this signal. App_Error(s==SIGSEGV ? "Segmentation Violation\n" : s==SIGINT ? "Interrupted by User\n" : s==SIGILL ? "Illegal Instruction\n" : s==SIGFPE ? "Floating Point Exception\n" : s==SIGTERM ? "Killed\n" : "Terminated by signal\n"); } #endif /** * Initialize platform level services. * * \note This must be called from the main thread due to issues with the devices * we use via the WINAPI, MCI (cdaudio, mixer etc) on the WIN32 platform. */ void Sys_Init() { de::Time begunAt; LOG_VERBOSE("Setting up platform state..."); LOG_AUDIO_VERBOSE("Initializing Audio subsystem..."); S_Init(); #ifdef DENG_CATCH_SIGNALS // Register handler for abnormal situations (in release build). signal(SIGSEGV, handler); signal(SIGTERM, handler); signal(SIGILL, handler); signal(SIGFPE, handler); signal(SIGILL, handler); signal(SIGABRT, handler); #endif #ifndef WIN32 // We are not worried about broken pipes. When a TCP connection closes, // we prefer to receive an error code instead of a signal. signal(SIGPIPE, SIG_IGN); #endif LOG_NET_VERBOSE("Initializing Network subsystem..."); N_Init(); LOGDEV_VERBOSE("Sys_Init completed in %.2f seconds") << begunAt.since(); } bool Sys_IsShuttingDown() { return CPP_BOOL(appShutdown); } /** * Return to default system state. */ void Sys_Shutdown() { // We are now shutting down. appShutdown = true; // Time to unload *everything*. if(App_GameLoaded()) Con_Execute(CMDS_DDAY, "unload", true, false); Net_Shutdown(); // Let's shut down sound first, so Windows' HD-hogging doesn't jam // the MUS player (would produce horrible bursts of notes). S_Shutdown(); #ifdef __CLIENT__ GL_Shutdown(); ClientApp::inputSystem().clearEvents(); #endif App_ClearGames(); } static int showCriticalMessage(const char* msg) { // This is going to be the end, I'm afraid. de::Loop::get().stop(); Sys_MessageBox(MBT_WARNING, DOOMSDAY_NICENAME, msg, 0); return 0; #if 0 #ifdef WIN32 #ifdef UNICODE wchar_t buf[256]; #else char buf[256]; #endif int ret; Window *wnd = Window::main(); DENG_ASSERT(wnd != 0); HWND hWnd = (HWND) wnd->nativeHandle(); if(!hWnd) { DD_Win32_SuspendMessagePump(true); MessageBox(HWND_DESKTOP, TEXT("Sys_CriticalMessage: Main window not available."), NULL, MB_ICONERROR | MB_OK); DD_Win32_SuspendMessagePump(false); return false; } ShowCursor(TRUE); ShowCursor(TRUE); DD_Win32_SuspendMessagePump(true); GetWindowText(hWnd, buf, 255); ret = (MessageBox(hWnd, WIN_STRING(msg), buf, MB_OK | MB_ICONEXCLAMATION) == IDYES); DD_Win32_SuspendMessagePump(false); ShowCursor(FALSE); ShowCursor(FALSE); return ret; #else fprintf(stderr, "--- %s\n", msg); return 0; #endif #endif } int Sys_CriticalMessage(const char* msg) { return showCriticalMessage(msg); } int Sys_CriticalMessagef(const char* format, ...) { static const char* unknownMsg = "Unknown critical issue occured."; const size_t BUF_SIZE = 655365; const char* msg; char* buf = 0; va_list args; int result; if(format && format[0]) { va_start(args, format); buf = (char*) calloc(1, BUF_SIZE); dd_vsnprintf(buf, BUF_SIZE, format, args); msg = buf; va_end(args); } else { msg = unknownMsg; } result = showCriticalMessage(msg); if(buf) free(buf); return result; } void Sys_Sleep(int millisecs) { /* #ifdef WIN32 Sleep(millisecs); #endif */ Thread_Sleep(millisecs); } void Sys_BlockUntilRealTime(uint realTimeMs) { uint remaining = realTimeMs - Timer_RealMilliseconds(); if(remaining > 50) { // Target time is in the past; or the caller is attempting to wait for // too long a time. return; } while(Timer_RealMilliseconds() < realTimeMs) { // Do nothing; don't yield execution. We want to exit here at the // precise right moment. } } void Sys_HideMouseCursor() { #ifdef WIN32 if(novideo) return; ShowCursor(FALSE); #else // The cursor is controlled using Qt in Canvas. #endif } de::NativePath Sys_SteamBasePath() { #ifdef WIN32 // The path to Steam can be queried from the registry. { QSettings st("HKEY_CURRENT_USER\\Software\\Valve\\Steam\\", QSettings::NativeFormat); de::String path = st.value("SteamPath").toString(); if(!path.isEmpty()) return path; } { QSettings st("HKEY_LOCAL_MACHINE\\Software\\Valve\\Steam\\", QSettings::NativeFormat); de::String path = st.value("InstallPath").toString(); if(!path.isEmpty()) return path; } #elif MACOSX return de::NativePath(QDir::homePath()) / "Library/Application Support/Steam/"; #endif /// @todo Where are steam apps located on Ubuntu? return ""; } /** * Called when Doomsday should quit (will be deferred until convenient). */ DENG_EXTERN_C void Sys_Quit(void) { if(BusyMode_Active()) { // The busy worker is running; we cannot just stop it abruptly. Sys_MessageBox2(MBT_WARNING, DOOMSDAY_NICENAME, "Cannot quit while in busy mode.", "Try again later after the current operation has finished.", 0); return; } appShutdown = true; // It's time to stop the main loop. DENG2_APP->stopLoop(DD_GameLoopExitCode()); } doomsday-stable-1.15.7/doomsday/client/src/windows/0000775000175000017500000000000012641367670021552 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/windows/joystick_win32.cpp0000664000175000017500000001656712641367670025156 0ustar jaakkojaakko/** @file joystick_win32.cpp Joystick input for Windows. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "directinput.h" #ifndef __CLIENT__ # error "joystick_win32.cpp is only the client" #endif #include "de_base.h" #include "de_console.h" #include "de_system.h" #include "de_misc.h" #include "dd_winit.h" extern int novideo; int joyDevice = 0; ///< cvar Joystick index to use. byte useJoystick = false; ///< cvar Joystick input enabled? static LPDIRECTINPUTDEVICE8 didJoy; static DIDEVICEINSTANCE firstJoystick; static int counter; void Joystick_Register(void) { C_VAR_INT("input-joy-device", &joyDevice, CVF_NO_MAX | CVF_PROTECTED, 0, 0); C_VAR_BYTE("input-joy", &useJoystick, 0, 0, 1); } static BOOL CALLBACK enumJoysticks(LPCDIDEVICEINSTANCE lpddi, void* ref) { // The first joystick is used by default. if(!firstJoystick.dwSize) memcpy(&firstJoystick, lpddi, sizeof(*lpddi)); if(counter == joyDevice) { // We'll use this one. memcpy(ref, lpddi, sizeof(*lpddi)); return DIENUM_STOP; } counter++; return DIENUM_CONTINUE; } dd_bool Joystick_Init() { int joyProp[] = { DIJOFS_X, DIJOFS_Y, DIJOFS_Z, DIJOFS_RX, DIJOFS_RY, DIJOFS_RZ, DIJOFS_SLIDER(0), DIJOFS_SLIDER(1) }; char const *axisName[] = { "X", "Y", "Z", "RX", "RY", "RZ", "Slider 1", "Slider 2" }; LOG_AS("Joystick_Init"); if(CommandLine_Check("-nojoy")) { LOG_INPUT_NOTE("Joystick disabled with the '-nojoy' option"); return false; } HWND hWnd = (HWND) ClientWindow::main().nativeHandle(); if(!hWnd) { LOGDEV_INPUT_ERROR("Main window not available"); return false; } LPDIRECTINPUT8 dInput = DirectInput_IVersion8(); if(!dInput) { LOG_INPUT_ERROR("DirectInput version 8 interface not available"); return false; } // ddi will contain info for the joystick device. DIDEVICEINSTANCE ddi; memset(&firstJoystick, 0, sizeof(firstJoystick)); memset(&ddi, 0, sizeof(ddi)); counter = 0; // Find the joystick we want by doing an enumeration. dInput->EnumDevices(DI8DEVCLASS_GAMECTRL, enumJoysticks, &ddi, DIEDFL_ALLDEVICES); // Was the joystick we want found? if(!ddi.dwSize) { // Use the default joystick. if(!firstJoystick.dwSize) return false; // Not found. LOGDEV_INPUT_WARNING("joydevice = %i, out of range") << joyDevice; // Use the first joystick that was found. memcpy(&ddi, &firstJoystick, sizeof(ddi)); } // Show some info. LOG_INPUT_MSG("Joystick name: %s") << ddi.tszProductName; /// @todo unicode? -jk // Create the joystick device. HRESULT hr = dInput->CreateDevice(ddi.guidInstance, &didJoy, 0); if(FAILED(hr)) { LOGDEV_INPUT_ERROR("Failed to create device (0x%x)") << hr; return false; } // Set data format. hr = didJoy->SetDataFormat(&c_dfDIJoystick); if(FAILED(hr)) { LOGDEV_INPUT_ERROR("Failed to set data format (0x%x)") << hr; goto kill_joy; } // Set behavior. hr = didJoy->SetCooperativeLevel(hWnd, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND); if(FAILED(hr)) { LOGDEV_INPUT_ERROR("Failed to set co-op level (0x%x: %s)") << hr << DirectInput_ErrorMsg(hr); goto kill_joy; } // Set properties. for(uint i = 0; i < sizeof(joyProp) / sizeof(joyProp[0]); ++i) { hr = didJoy->SetProperty(DIPROP_RANGE, DIPropRange(DIPH_BYOFFSET, joyProp[i], IJOY_AXISMIN, IJOY_AXISMAX)); if(FAILED(hr)) { if(hr != DIERR_NOTFOUND) { LOGDEV_INPUT_VERBOSE("Failed to set axis '%s' range (0x%x: %s)") << axisName[i] << hr << DirectInput_ErrorMsg(hr); } } } // Set no dead zone. We'll handle this ourselves thanks... hr = didJoy->SetProperty(DIPROP_DEADZONE, DIPropDWord(DIPH_DEVICE)); if(FAILED(hr)) { if(hr != DIERR_NOTFOUND) { LOGDEV_INPUT_WARNING("Failed to set dead zone (0x%x: %s)") << hr << DirectInput_ErrorMsg(hr); } } // Set absolute mode. hr = didJoy->SetProperty(DIPROP_AXISMODE, DIPropDWord(DIPH_DEVICE, 0, DIPROPAXISMODE_ABS)); if(FAILED(hr)) { if(hr != DIERR_NOTFOUND) { LOGDEV_INPUT_WARNING("Failed to set absolute axis mode (0x%x: %s)") << hr << DirectInput_ErrorMsg(hr); } } // Acquire it. didJoy->Acquire(); // Initialization was successful. return true; kill_joy: I_SAFE_RELEASE(didJoy); return false; } void Joystick_Shutdown(void) { DirectInput_KillDevice(&didJoy); } dd_bool Joystick_IsPresent(void) { return (didJoy != 0); } void Joystick_GetState(joystate_t* state) { static BOOL oldButtons[IJOY_MAXBUTTONS]; // Thats a lot of buttons. memset(state, 0, sizeof(*state)); // Initialization has been done? if(!didJoy || !useJoystick) return; // Some joysticks must be polled. didJoy->Poll(); DIJOYSTATE dijoy; DWORD tries = 1; BOOL acquired = FALSE; while(!acquired && tries > 0) { HRESULT hr = didJoy->GetDeviceState(sizeof(dijoy), &dijoy); if(SUCCEEDED(hr)) { acquired = TRUE; } else if(tries > 0) { // Try to reacquire. didJoy->Acquire(); tries--; } } if(!acquired) return; // The operation is a failure. state->numAxes = 8; state->axis[0] = (int) dijoy.lX; state->axis[1] = (int) dijoy.lY; state->axis[2] = (int) dijoy.lZ; state->axis[3] = (int) dijoy.lRx; state->axis[4] = (int) dijoy.lRy; state->axis[5] = (int) dijoy.lRz; state->axis[6] = (int) dijoy.rglSlider[0]; state->axis[7] = (int) dijoy.rglSlider[1]; state->numButtons = 32; for(DWORD i = 0; i < IJOY_MAXBUTTONS; ++i) { BOOL isDown = (dijoy.rgbButtons[i] & 0x80? TRUE : FALSE); state->buttonDowns[i] = state->buttonUps[i] = 0; if(isDown && !oldButtons[i]) state->buttonDowns[i] = 1; else if(!isDown && oldButtons[i]) state->buttonUps[i] = 1; oldButtons[i] = isDown; } state->numHats = 4; for(DWORD i = 0; i < IJOY_MAXHATS; ++i) { DWORD pov = dijoy.rgdwPOV[i]; if((pov & 0xffff) == 0xffff) state->hatAngle[i] = IJOY_POV_CENTER; else state->hatAngle[i] = pov / 100.0f; } } doomsday-stable-1.15.7/doomsday/client/src/windows/mouse_win32.cpp0000664000175000017500000001375312641367670024441 0ustar jaakkojaakko/** @file mouse_win32.cpp * * Mouse driver that gets mouse input from DirectInput on Windows. * @ingroup input * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "directinput.h" #include "ui/sys_input.h" #include "ui/clientwindow.h" #include "sys_system.h" #include static LPDIRECTINPUTDEVICE8 didMouse; static dd_bool mouseTrapped; static DIMOUSESTATE2 mstate; ///< Polled state. static int Mouse_Win32_Init() { if(CommandLine_Check("-nomouse") || novideo) return false; // We'll need a window handle for this. HWND hWnd = (HWND) ClientWindow::main().nativeHandle(); HRESULT hr = -1; // Prefer the newer version 8 interface if available. if(LPDIRECTINPUT8 dInput = DirectInput_IVersion8()) { hr = dInput->CreateDevice(GUID_SysMouse, &didMouse, 0); } else if(LPDIRECTINPUT dInput = DirectInput_IVersion3()) { hr = dInput->CreateDevice(GUID_SysMouse, (LPDIRECTINPUTDEVICE*) &didMouse, 0); } if(FAILED(hr)) { LOGDEV_INPUT_ERROR("Failed to create device (0x%x: %s)") << hr << DirectInput_ErrorMsg(hr); return false; } // Set data format. hr = didMouse->SetDataFormat(&c_dfDIMouse2); if(FAILED(hr)) { LOGDEV_INPUT_ERROR("Failed to set data format (0x%x: %s)") << hr << DirectInput_ErrorMsg(hr); goto kill_mouse; } // Set behavior. hr = didMouse->SetCooperativeLevel(hWnd, DISCL_EXCLUSIVE | DISCL_FOREGROUND); if(FAILED(hr)) { LOGDEV_INPUT_ERROR("Failed to set co-op level (0x%x: %s)") << hr << DirectInput_ErrorMsg(hr); goto kill_mouse; } // Acquire the device. //didMouse->Acquire(); //mouseTrapped = true; // We will be told when to trap the mouse. mouseTrapped = false; // Init was successful. return true; kill_mouse: I_SAFE_RELEASE(didMouse); return false; } static void Mouse_Win32_Shutdown(void) { DirectInput_KillDevice(&didMouse); } static void Mouse_Win32_Poll(void) { if(!mouseTrapped) { // We are not supposed to be reading the mouse right now. return; } // Try to get the mouse state. int tries = 1; BOOL acquired = false; while(!acquired && tries > 0) { HRESULT hr = didMouse->GetDeviceState(sizeof(mstate), &mstate); if(SUCCEEDED(hr)) { acquired = true; } else if(tries > 0) { // Try to reacquire. didMouse->Acquire(); tries--; } } if(!acquired) { // The operation is a failure. memset(&mstate, 0, sizeof(mstate)); } } static void Mouse_Win32_GetState(mousestate_t* state) { /** * We need to map the mouse buttons as follows: * DX : Deng * (left) 0 > 0 * (right) 1 > 2 * (center) 2 > 1 * (b4) 3 > 5 * (b5) 4 > 6 * (b6) 5 > 7 * (b7) 6 > 8 * (b8) 7 > 9 */ static const int buttonMap[] = { 0, 2, 1, 5, 6, 7, 8, 9 }; static BOOL oldButtons[8]; static int oldZ; memset(state, 0, sizeof(*state)); if(!mouseTrapped) { // We are not supposed to be reading the mouse right now. return; } // Fill in the state structure. state->axis[IMA_POINTER].x = (int) mstate.lX; state->axis[IMA_POINTER].y = (int) mstate.lY; // If this is called again before re-polling, we don't want to return // these deltas again. mstate.lX = 0; mstate.lY = 0; for(DWORD i = 0; i < 8; ++i) { BOOL isDown = (mstate.rgbButtons[i] & 0x80? TRUE : FALSE); int id; id = buttonMap[i]; state->buttonDowns[id] = state->buttonUps[id] = 0; if(isDown && !oldButtons[i]) state->buttonDowns[id] = 1; else if(!isDown && oldButtons[i]) state->buttonUps[id] = 1; oldButtons[i] = isDown; } // Handle mouse wheel (convert to buttons). if(mstate.lZ == 0) { state->buttonDowns[3] = state->buttonDowns[4] = 0; if(oldZ > 0) state->buttonUps[3] = 1; else if(oldZ < 0) state->buttonUps[4] = 1; } else { if(mstate.lZ > 0 && !(oldZ > 0)) { state->buttonDowns[3] = 1; if(state->buttonDowns[4]) state->buttonUps[3] = 1; } else if(mstate.lZ < 0 && !(oldZ < 0)) { state->buttonDowns[4] = 1; if(state->buttonDowns[3]) state->buttonUps[4] = 1; } } oldZ = (int) mstate.lZ; } static void Mouse_Win32_Trap(dd_bool enabled) { LOG_AS("Mouse_Win32"); DENG_ASSERT(didMouse); mouseTrapped = (enabled != 0); if(enabled) { LOG_INPUT_VERBOSE("Acquiring the mouse"); didMouse->Acquire(); } else { LOG_INPUT_VERBOSE("Unacquiring the mouse"); didMouse->Unacquire(); } } // The global interface. extern "C" { mouseinterface_t win32Mouse = { Mouse_Win32_Init, Mouse_Win32_Shutdown, Mouse_Win32_Poll, Mouse_Win32_GetState, Mouse_Win32_Trap }; } doomsday-stable-1.15.7/doomsday/client/src/windows/dd_winit.cpp0000664000175000017500000001141312641367670024057 0ustar jaakkojaakko/** @file dd_winit.cpp Engine Initialization (Windows). * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #define WIN32_LEAN_AND_MEAN #define _WIN32_DCOM //#define STRICT #ifdef __CLIENT__ # include #endif #include #include #include #include #include #include #include #include "de_base.h" #include "de_system.h" #include "dd_winit.h" #include #include #include using namespace de; application_t app; /** * @note GetLastError() should only be called when we *know* an error was thrown. * The result of calling this any other time is undefined. * * @return Ptr to a string containing a textual representation of the last * error thrown in the current thread else @c NULL. */ char const *DD_Win32_GetLastErrorMessage() { static char *buffer = 0; /// @todo Never free'd! static size_t currentBufferSize = 0; LPVOID lpMsgBuf; DWORD dw = GetLastError(), lpMsgBufLen; lpMsgBufLen = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 0, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpMsgBuf, 0, 0); if(!lpMsgBuf || lpMsgBufLen == 0) return ""; if(!buffer || (size_t)(lpMsgBufLen+1+8) > currentBufferSize) { currentBufferSize = (size_t)(lpMsgBufLen+1+8); buffer = (char *) M_Realloc(buffer, currentBufferSize); } dd_snprintf(buffer, currentBufferSize, "#%-5d: ", (int)dw); // Continue splitting as long as there are parts. char *part, *cursor = (char *)lpMsgBuf; while(*(part = M_StrTok(&cursor, "\n"))) { strcat(buffer, part); } // We're done with the system-allocated message. LocalFree(lpMsgBuf); return buffer; } dd_bool DD_Win32_Init() { zap(app); app.hInstance = GetModuleHandle(NULL); // Initialize COM. CoInitialize(NULL); // Prepare the command line arguments. DD_InitCommandLine(); Library_Init(); // Change to a custom working directory? if(CommandLine_CheckWith("-userdir", 1)) { if(NativePath::setWorkPath(CommandLine_NextAsPath())) { LOG_VERBOSE("Changed current directory to \"%s\"") << NativePath::workPath(); app.usingUserDir = true; } } // The runtime directory is the current working directory. DD_SetRuntimePath((NativePath::workPath().withSeparators('/') + '/').toUtf8().constData()); // Use a custom base directory? if(CommandLine_CheckWith("-basedir", 1)) { DD_SetBasePath(CommandLine_Next()); } else { // The default base directory is one level up from the bin dir. String binDir = App::executablePath().fileNamePath().withSeparators('/'); String baseDir = String(QDir::cleanPath(binDir / String(".."))) + '/'; DD_SetBasePath(baseDir.toUtf8().constData()); } // Perform early initialization of subsystems that require it. BOOL failed = TRUE; if(!DD_EarlyInit()) { Sys_MessageBox(MBT_ERROR, DOOMSDAY_NICENAME, "Error during early init.", 0); } #ifdef __CLIENT__ else if(!Sys_GLPreInit()) { Sys_MessageBox(MBT_ERROR, DOOMSDAY_NICENAME, "Error initializing GL.", 0); } #endif else { // All initialization complete. failed = FALSE; } #ifdef __CLIENT__ // No Windows system keys? if(CommandLine_Check("-nowsk")) { // Disable Alt-Tab, Alt-Esc, Ctrl-Alt-Del. A bit of a hack... SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, TRUE, 0, 0); LOG_INPUT_NOTE("Windows system keys disabled"); } #endif return !failed; } /** * Shuts down the engine. */ void DD_Shutdown() { DD_ShutdownAll(); // Stop all engine subsystems. Plug_UnloadAll(); Library_Shutdown(); // No more use of COM beyond, this point. CoUninitialize(); #ifdef __CLIENT__ DisplayMode_Shutdown(); #endif } doomsday-stable-1.15.7/doomsday/client/src/windows/directinput.cpp0000664000175000017500000000614312641367670024614 0ustar jaakkojaakko/** @file directinput.cpp DirectInput for Windows. * @ingroup input * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2005-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifdef __CLIENT__ #include "directinput.h" #include "dd_winit.h" #include static LPDIRECTINPUT8 dInput; static LPDIRECTINPUT dInput3; const char* DirectInput_ErrorMsg(HRESULT hr) { return hr == DI_OK ? "OK" : hr == DIERR_GENERIC ? "Generic error" : hr == DI_PROPNOEFFECT ? "Property has no effect" : hr == DIERR_INVALIDPARAM ? "Invalid parameter" : hr == DIERR_NOTINITIALIZED ? "Not initialized" : hr == DIERR_UNSUPPORTED ? "Unsupported" : hr == DIERR_NOTFOUND ? "Not found" : "?"; } int DirectInput_Init(void) { HRESULT hr; if(dInput || dInput3) return true; // Create the DirectInput interface instance. Try version 8 first. if(FAILED(hr = CoCreateInstance(CLSID_DirectInput8, NULL, CLSCTX_INPROC_SERVER, IID_IDirectInput8, (LPVOID*)&dInput)) || FAILED(hr = dInput->Initialize(app.hInstance, DIRECTINPUT_VERSION))) { LOGDEV_INPUT_ERROR("DirectInput 8 init failed (0x%x)") << hr; // Try the older version 3 interface instead. // I'm not sure if this works correctly. if(FAILED(hr = CoCreateInstance(CLSID_DirectInput, NULL, CLSCTX_INPROC_SERVER, IID_IDirectInput2W, (LPVOID*)&dInput3)) || FAILED(hr = dInput3->Initialize(app.hInstance, 0x0300))) { LOGDEV_INPUT_ERROR("Failed to create DirectInput 3 object (0x%x)") << hr; return false; } LOG_INPUT_NOTE("Using DirectInput 3 as fallback"); } if(!dInput && !dInput3) { LOG_INPUT_ERROR("DirectInput init failed"); return false; } return true; } void DirectInput_Shutdown(void) { if(dInput) { IDirectInput_Release(dInput); dInput = 0; } if(dInput3) { IDirectInput_Release(dInput3); dInput3 = 0; } } LPDIRECTINPUT8 DirectInput_IVersion8() { return dInput; } LPDIRECTINPUT DirectInput_IVersion3() { return dInput3; } void DirectInput_KillDevice(LPDIRECTINPUTDEVICE8* dev) { if(*dev) (*dev)->Unacquire(); I_SAFE_RELEASE(*dev); } #endif // __CLIENT__ doomsday-stable-1.15.7/doomsday/client/src/busymode.cpp0000664000175000017500000003237112641367670022421 0ustar jaakkojaakko/** @file busymode.cpp Busy Mode * @ingroup base * * @authors Copyright © 2007-2013 Jaakko Keränen * @authors Copyright © 2007-2013 Daniel Swanson * @authors Copyright © 2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #define DENG_NO_API_MACROS_BUSY #include "de_base.h" #include "de_platform.h" #include "de_console.h" #include "de_graphics.h" #include "de_network.h" #include "audio/s_main.h" #include "ui/busyvisual.h" #include #include #include #include #include #ifdef __CLIENT__ #include "sys_system.h" #include "clientapp.h" #include "ui/clientwindowsystem.h" #include "ui/widgets/busywidget.h" static void BusyMode_Exit(void); static QEventLoop* eventLoop; static volatile dd_bool busyDoneCopy; static timespan_t busyTime; static dd_bool busyWillAnimateTransition; static dd_bool busyWasIgnoringInput; #endif // __CLIENT__ static dd_bool busyModeAllowed = true; ///< Can we enter busy mode? static dd_bool busyInited; static volatile dd_bool busyDone; static mutex_t busy_Mutex; // To prevent Data races in the busy thread. static BusyTask* busyTask; // Current task. static thread_t busyThread; static timespan_t accumulatedBusyTime; // Never cleared. static dd_bool busyTaskEndedWithError; static char busyError[256]; #ifdef __CLIENT__ static dd_bool animatedTransitionActive(int busyMode) { return (!novideo && !isDedicated && !netGame && !(busyMode & BUSYF_STARTUP) && rTransitionTics > 0 && (busyMode & BUSYF_TRANSITION)); } #endif dd_bool BusyMode_Active(void) { return busyInited; } timespan_t BusyMode_ElapsedTime(void) { if(!BusyMode_Active()) return 0; return accumulatedBusyTime; } dd_bool BusyMode_IsWorkerThread(uint threadId) { dd_bool result; if(!BusyMode_Active() || !busyThread) return false; /// @todo Is locking necessary? Sys_Lock(busy_Mutex); result = (Sys_ThreadId(busyThread) == threadId); Sys_Unlock(busy_Mutex); return result; } dd_bool BusyMode_InWorkerThread(void) { return BusyMode_IsWorkerThread(Sys_CurrentThreadId()); } BusyTask* BusyMode_CurrentTask(void) { if(!BusyMode_Active()) return NULL; return busyTask; } #ifdef __CLIENT__ /** * Callback that is called from the busy worker thread when it exists. * @param status Exit status. */ static void busyWorkerTerminated(systhreadexitstatus_t status) { DENG_ASSERT(BusyMode_Active()); if(status == DENG_THREAD_STOPPED_WITH_EXCEPTION) { _api_Busy.WorkerError("Uncaught exception from busy thread."); } } /** * Sets up module state for running a busy task. After this the busy mode event * loop is started. The loop will run until the worker thread exits. */ static void beginTask(BusyTask* task) { DENG_ASSERT(task); if(!busyInited) { busy_Mutex = Sys_CreateMutex("BUSY_MUTEX"); } if(busyInited) { App_Error("Con_Busy: Already busy.\n"); } BusyVisual_PrepareResources(); Sys_Lock(busy_Mutex); busyDone = false; busyTaskEndedWithError = false; // This is now the current task. busyTask = task; Sys_Unlock(busy_Mutex); busyInited = true; de::ProgressWidget &prog = ClientWindow::main().busy().progress(); prog.show(); prog.setText(task->name); prog.setMode(task->mode & BUSYF_ACTIVITY? de::ProgressWidget::Indefinite : de::ProgressWidget::Ranged); // Start the busy worker thread, which will process the task in the // background while we keep the user occupied with nice animations. busyThread = Sys_StartThread(busyTask->worker, busyTask->workerData); Thread_SetCallback(busyThread, busyWorkerTerminated); busyTask->_startTime = Timer_RealSeconds(); } /** * Called after the busy mode worker thread and the event (sub-)loop has been stopped. */ static void endTask(BusyTask* task) { DENG_ASSERT(task); DENG_ASSERT_IN_MAIN_THREAD(); LOG_VERBOSE("Busy mode lasted %.2f seconds") << busyTime; if(busyTaskEndedWithError) { App_AbnormalShutdown(busyError); } if(busyWillAnimateTransition) { Con_TransitionBegin(); } // Make sure that any remaining deferred content gets uploaded. if(!(task->mode & BUSYF_NO_UPLOADS)) { GL_ProcessDeferredTasks(0); } Sys_DestroyMutex(busy_Mutex); busyInited = false; } #endif // __CLIENT__ /** * Runs the busy mode event loop. Execution blocks here until the worker thread exits. */ static int runTask(BusyTask* task) { DENG_ASSERT(task); #ifdef __CLIENT__ if(busyModeAllowed) { // Let's get busy! beginTask(task); DENG_ASSERT(eventLoop == 0); // Run a local event loop since the primary event loop is blocked while // we're busy. This event loop is able to handle window and input events // just like the primary loop. eventLoop = new QEventLoop; int result = eventLoop->exec(); delete eventLoop; eventLoop = 0; // Teardown. endTask(task); return result; } else #endif { // Don't bother to start a thread -- non-GUI mode. return task->worker(task->workerData); } } #ifdef __CLIENT__ dd_bool BusyMode_IsTransitionAnimated(void) { return busyWillAnimateTransition; } #endif void BusyMode_SetAllowed(dd_bool allow) { busyModeAllowed = allow; } void BusyMode_FreezeGameForBusyMode(void) { #ifdef __CLIENT__ // This is only possible from the main thread. if(ClientWindow::mainExists() && busyModeAllowed && de::App::inMainThread()) { ClientWindow::main().busy().renderTransitionFrame(); } #endif } static void preBusySetup(int initialMode) { #ifdef __CLIENT__ // Are we doing a transition effect? busyWillAnimateTransition = animatedTransitionActive(initialMode); if(busyWillAnimateTransition) { Con_TransitionConfigure(); } busyWasIgnoringInput = ClientApp::inputSystem().ignoreEvents(); // Limit frame rate to 60, no point pushing it any faster while busy. ClientApp::app().loop().setRate(60); // Switch the window to busy mode UI. ClientWindowSystem::main().setMode(ClientWindow::Busy); #else DENG_UNUSED(initialMode); #endif } static void postBusyCleanup() { #ifdef __CLIENT__ // Discard input events so that any and all accumulated input events are ignored. ClientApp::inputSystem().ignoreEvents(busyWasIgnoringInput); DD_ResetTimer(); // Back to unlimited frame rate. ClientApp::app().loop().setRate(0); // Switch the window to normal UI. ClientWindowSystem::main().setMode(ClientWindow::Normal); #endif } /** * @param mode Busy mode flags @ref busyModeFlags * @param worker Worker thread that does processing while in busy mode. * @param workerData Data context for the worker thread. * @param taskName Optional task name (drawn with the progress bar). */ static BusyTask* newTask(int mode, busyworkerfunc_t worker, void* workerData, const char* taskName) { // Initialize the task. BusyTask* task = (BusyTask*) calloc(1, sizeof(*task)); task->mode = mode; task->worker = worker; task->workerData = workerData; // Take a copy of the task name. if(taskName && taskName[0]) { task->name = strdup(taskName); } return task; } static void deleteTask(BusyTask* task) { DENG_ASSERT(task); if(task->name) free((void*)task->name); free(task); } int BusyMode_RunTasks(BusyTask* tasks, int numTasks) { const char* currentTaskName = NULL; BusyTask* task; int i, mode; int result = 0; if(BusyMode_Active()) { App_Error("BusyMode: Internal error, already busy..."); exit(1); // Unreachable. } if(!tasks || numTasks <= 0) return result; // Hmm, no work? // Pick the first task. task = tasks; int initialMode = task->mode; preBusySetup(initialMode); // Process tasks. for(i = 0; i < numTasks; ++i, task++) { // If no new task name is specified, continue using the name of the previous task. if(task->name) { if(task->name[0]) currentTaskName = task->name; else // Clear the name. currentTaskName = NULL; } mode = task->mode; /// @todo Kludge: Force BUSYF_STARTUP here so that the animation of one task /// is not drawn on top of the last frame of the previous. if(numTasks > 1) { mode |= BUSYF_STARTUP; } // kludge end // Null tasks are not processed (implicit success). if(!task->worker) continue; /** * Process the work. */ #ifdef __CLIENT__ // Is the worker updating its progress? if(task->maxProgress > 0) Con_InitProgress2(task->maxProgress, task->progressStart, task->progressEnd); #endif // Invoke the worker in a new thread. /// @todo Kludge: Presently a temporary local task is needed so that we can modify /// the task name and mode flags. { BusyTask* tmp = newTask(mode, task->worker, task->workerData, currentTaskName); result = runTask(tmp); // We are now done with this task. deleteTask(tmp); if(result) break; } // kludge end. } postBusyCleanup(); return result; } int BusyMode_RunTask(BusyTask* task) { return BusyMode_RunTasks(task, 1); } int BusyMode_RunNewTaskWithName(int mode, busyworkerfunc_t worker, void* workerData, const char* taskName) { BusyTask* task = newTask(mode, worker, workerData, taskName); int result = BusyMode_RunTask(task); deleteTask(task); return result; } int BusyMode_RunNewTask(int mode, busyworkerfunc_t worker, void* workerData) { return BusyMode_RunNewTaskWithName(mode, worker, workerData, NULL/*no task name*/); } #ifdef __CLIENT__ /** * Ends the busy event loop and sets its return value. The loop callback, which * during busy mode points to the busy loop callback, is reset to NULL. */ static void stopEventLoopWithValue(int result) { DENG_ASSERT(eventLoop != 0); eventLoop->exit(result); } /** * Exits the busy mode event loop. Called in the main thread, does not return * until the worker thread is stopped. */ static void BusyMode_Exit(void) { int result; systhreadexitstatus_t status; DENG_ASSERT_IN_MAIN_THREAD(); busyDone = true; // Make sure the worker finishes before we continue. result = Sys_WaitThread(busyThread, busyTaskEndedWithError? 100 : 5000, &status); busyThread = NULL; busyTask = NULL; stopEventLoopWithValue(result); } /** * The busy loop callback function. Called periodically in the main (UI) thread * while the busy worker is running. */ void BusyMode_Loop(void) { if(!busyTask || !BusyMode_Active()) return; dd_bool canUpload = !(busyTask->mode & BUSYF_NO_UPLOADS); timespan_t oldTime; // Post and discard all input events. ClientApp::inputSystem().processEvents(0); ClientApp::inputSystem().processSharpEvents(0); if(canUpload) { ClientWindowSystem::main().glActivate(); // Any deferred content needs to get uploaded. GL_ProcessDeferredTasks(15); } // We accumulate time in the busy loop so that the animation of a task // sequence doesn't jump around but remains continuous. oldTime = busyTime; busyTime = Timer_RealSeconds() - busyTask->_startTime; if(busyTime > oldTime) { accumulatedBusyTime += busyTime - oldTime; } Sys_Lock(busy_Mutex); busyDoneCopy = busyDone; Sys_Unlock(busy_Mutex); if(!busyDoneCopy || (canUpload && GL_DeferredTaskCount() > 0) || !Con_IsProgressAnimationCompleted()) { // Let's keep running the busy loop. //ClientWindowSystem::main().draw(); return; } // Stop the loop. BusyMode_Exit(); } #endif // __CLIENT__ void BusyMode_WorkerError(const char* message) { busyTaskEndedWithError = true; strncpy(busyError, message, sizeof(busyError) - 1); _api_Busy.WorkerEnd(); } void BusyMode_WorkerEnd(void) { if(!busyInited) return; Sys_Lock(busy_Mutex); busyDone = true; Sys_Unlock(busy_Mutex); } DENG_DECLARE_API(Busy) = { { DE_API_BUSY }, BusyMode_Active, BusyMode_ElapsedTime, BusyMode_FreezeGameForBusyMode, BusyMode_RunTask, BusyMode_RunTasks, BusyMode_RunNewTask, BusyMode_RunNewTaskWithName, BusyMode_WorkerEnd, BusyMode_WorkerError }; doomsday-stable-1.15.7/doomsday/client/src/dd_main.cpp0000664000175000017500000025513712641367670022174 0ustar jaakkojaakko/** @file dd_main.cpp Engine core. * * @todo Much of this should be refactored and merged into the App classes. * @todo The rest should be split into smaller, perhaps domain-specific files. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2005-2015 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #define DENG_NO_API_MACROS_BASE // functions defined here #include "de_platform.h" #include "dd_main.h" #ifdef WIN32 # define _WIN32_DCOM # include #endif #include #ifdef UNIX # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __CLIENT__ # include #endif #include #include #include #include #include #include #include #include #include #include #ifdef __CLIENT__ # include "clientapp.h" #endif #ifdef __SERVER__ # include "serverapp.h" #endif #include "dd_loop.h" #include "busymode.h" #include "con_config.h" #include "library.h" #include "sys_system.h" #include "edit_bias.h" #include "gl/svg.h" #include "audio/s_main.h" #include "resource/manifest.h" #include "world/worldsystem.h" #include "world/entitydef.h" #include "world/map.h" #include "world/p_players.h" #include "ui/infine/infinesystem.h" #include "ui/nativeui.h" #include "ui/progress.h" #ifdef __CLIENT__ # include "client/cl_def.h" # include "client/cl_infine.h" # include "gl/gl_main.h" # include "gl/gl_defer.h" # include "gl/gl_texmanager.h" # include "network/net_demo.h" # include "render/rend_main.h" # include "render/cameralensfx.h" # include "render/r_draw.h" // R_InitViewWindow # include "render/r_main.h" // pspOffset # include "render/rend_font.h" # include "render/rend_particle.h" // Rend_ParticleLoadSystemTextures # include "render/vr.h" # include "Contact" # include "MaterialAnimator" # include "Sector" # include "ui/ui_main.h" # include "ui/busyvisual.h" # include "ui/sys_input.h" # include "ui/widgets/taskbarwidget.h" # include "updater.h" # include "updater/downloaddialog.h" #endif #ifdef __SERVER__ # include "network/net_main.h" # include "server/sv_def.h" #endif using namespace de; class ZipFileType : public de::NativeFileType { public: ZipFileType() : NativeFileType("FT_ZIP", RC_PACKAGE) { addKnownExtension(".pk3"); addKnownExtension(".zip"); } File1 *interpret(FileHandle &hndl, String path, FileInfo const &info) const { if(Zip::recognise(hndl)) { LOG_AS("ZipFileType"); LOG_RES_VERBOSE("Interpreted \"" + NativePath(path).pretty() + "\""); return new Zip(hndl, path, info); } return nullptr; } }; class WadFileType : public de::NativeFileType { public: WadFileType() : NativeFileType("FT_WAD", RC_PACKAGE) { addKnownExtension(".wad"); } File1 *interpret(FileHandle &hndl, String path, FileInfo const &info) const { if(Wad::recognise(hndl)) { LOG_AS("WadFileType"); LOG_RES_VERBOSE("Interpreted \"" + NativePath(path).pretty() + "\""); return new Wad(hndl, path, info); } return nullptr; } }; static void consoleRegister(); static void initPathMappings(); static dint DD_StartupWorker(void *context); static dint DD_DummyWorker(void *context); static void DD_AutoLoad(); #ifndef WIN32 extern GETGAMEAPI GetGameAPI; #endif dint isDedicated; dint verbose; ///< For debug messages (-verbose). dint gameDataFormat; ///< Game-specific data format identifier/selector. #ifdef __CLIENT__ dint symbolicEchoMode = false; ///< @note Mutable via public API. #endif static char *startupFiles = (char *) ""; ///< List of file names, whitespace seperating (written to .cfg). static void registerResourceFileTypes() { FileType *ftype; // // Packages types: // ResourceClass &packageClass = App_ResourceClass("RC_PACKAGE"); ftype = new ZipFileType(); packageClass.addFileType(ftype); DD_AddFileType(*ftype); ftype = new WadFileType(); packageClass.addFileType(ftype); DD_AddFileType(*ftype); ftype = new FileType("FT_LMP", RC_PACKAGE); ///< Treat lumps as packages so they are mapped to $App.DataPath. ftype->addKnownExtension(".lmp"); DD_AddFileType(*ftype); /// @todo ftype leaks. -jk // // Definition fileTypes: // ftype = new FileType("FT_DED", RC_DEFINITION); ftype->addKnownExtension(".ded"); App_ResourceClass("RC_DEFINITION").addFileType(ftype); DD_AddFileType(*ftype); // // Graphic fileTypes: // ResourceClass &graphicClass = App_ResourceClass("RC_GRAPHIC"); ftype = new FileType("FT_PNG", RC_GRAPHIC); ftype->addKnownExtension(".png"); graphicClass.addFileType(ftype); DD_AddFileType(*ftype); ftype = new FileType("FT_TGA", RC_GRAPHIC); ftype->addKnownExtension(".tga"); graphicClass.addFileType(ftype); DD_AddFileType(*ftype); ftype = new FileType("FT_JPG", RC_GRAPHIC); ftype->addKnownExtension(".jpg"); graphicClass.addFileType(ftype); DD_AddFileType(*ftype); ftype = new FileType("FT_PCX", RC_GRAPHIC); ftype->addKnownExtension(".pcx"); graphicClass.addFileType(ftype); DD_AddFileType(*ftype); // // Model fileTypes: // ResourceClass &modelClass = App_ResourceClass("RC_MODEL"); ftype = new FileType("FT_DMD", RC_MODEL); ftype->addKnownExtension(".dmd"); modelClass.addFileType(ftype); DD_AddFileType(*ftype); ftype = new FileType("FT_MD2", RC_MODEL); ftype->addKnownExtension(".md2"); modelClass.addFileType(ftype); DD_AddFileType(*ftype); // // Sound fileTypes: // ftype = new FileType("FT_WAV", RC_SOUND); ftype->addKnownExtension(".wav"); App_ResourceClass("RC_SOUND").addFileType(ftype); DD_AddFileType(*ftype); // // Music fileTypes: // ResourceClass &musicClass = App_ResourceClass("RC_MUSIC"); ftype = new FileType("FT_OGG", RC_MUSIC); ftype->addKnownExtension(".ogg"); musicClass.addFileType(ftype); DD_AddFileType(*ftype); ftype = new FileType("FT_MP3", RC_MUSIC); ftype->addKnownExtension(".mp3"); musicClass.addFileType(ftype); DD_AddFileType(*ftype); ftype = new FileType("FT_MOD", RC_MUSIC); ftype->addKnownExtension(".mod"); musicClass.addFileType(ftype); DD_AddFileType(*ftype); ftype = new FileType("FT_MID", RC_MUSIC); ftype->addKnownExtension(".mid"); musicClass.addFileType(ftype); DD_AddFileType(*ftype); // // Font fileTypes: // ftype = new FileType("FT_DFN", RC_FONT); ftype->addKnownExtension(".dfn"); App_ResourceClass("RC_FONT").addFileType(ftype); DD_AddFileType(*ftype); // // Misc fileTypes: // ftype = new FileType("FT_DEH", RC_PACKAGE); ///< Treat DeHackEd patches as packages so they are mapped to $App.DataPath. ftype->addKnownExtension(".deh"); DD_AddFileType(*ftype); /// @todo ftype leaks. -jk } static void createPackagesScheme() { FS1::Scheme &scheme = App_FileSystem().createScheme("Packages"); // // Add default search paths. // // Note that the order here defines the order in which these paths are searched // thus paths must be added in priority order (newer paths have priority). // #ifdef UNIX // There may be an iwaddir specified in a system-level config file. filename_t fn; if(UnixInfo_GetConfigValue("paths", "iwaddir", fn, FILENAME_T_MAXLEN)) { NativePath path = de::App::commandLine().startupPath() / fn; scheme.addSearchPath(SearchPath(de::Uri::fromNativeDirPath(path), SearchPath::NoDescend)); LOG_RES_NOTE("Using paths.iwaddir: %s") << path.pretty(); } #endif // Add paths to games bought with/using Steam. if(!CommandLine_Check("-nosteamapps")) { NativePath steamBase = Sys_SteamBasePath(); if(!steamBase.isEmpty()) { NativePath steamPath = steamBase / "SteamApps/common/"; LOG_RES_NOTE("Using SteamApps path: %s") << steamPath.pretty(); static String const appDirs[] = { "doom 2/base", "final doom/base", "heretic shadow of the serpent riders/base", "hexen/base", "hexen deathkings of the dark citadel/base", "ultimate doom/base", "DOOM 3 BFG Edition/base/wads", "" }; for(dint i = 0; !appDirs[i].isEmpty(); ++i) { scheme.addSearchPath(SearchPath(de::Uri::fromNativeDirPath(steamPath / appDirs[i]), SearchPath::NoDescend)); } } } // Add the path from the DOOMWADDIR environment variable. if(!CommandLine_Check("-nodoomwaddir") && getenv("DOOMWADDIR")) { NativePath path = App::commandLine().startupPath() / getenv("DOOMWADDIR"); scheme.addSearchPath(SearchPath(de::Uri::fromNativeDirPath(path), SearchPath::NoDescend)); LOG_RES_NOTE("Using DOOMWADDIR: %s") << path.pretty(); } // Add any paths from the DOOMWADPATH environment variable. if(!CommandLine_Check("-nodoomwadpath") && getenv("DOOMWADPATH")) { #if WIN32 # define SEP_CHAR ';' #else # define SEP_CHAR ':' #endif QStringList allPaths = String(getenv("DOOMWADPATH")).split(SEP_CHAR, String::SkipEmptyParts); for(dint i = allPaths.count(); i--> 0; ) { NativePath path = App::commandLine().startupPath() / allPaths[i]; scheme.addSearchPath(SearchPath(de::Uri::fromNativeDirPath(path), SearchPath::NoDescend)); LOG_RES_NOTE("Using DOOMWADPATH: %s") << path.pretty(); } #undef SEP_CHAR } scheme.addSearchPath(SearchPath(de::Uri("$(App.DataPath)/", RC_NULL), SearchPath::NoDescend)); scheme.addSearchPath(SearchPath(de::Uri("$(App.DataPath)/$(GamePlugin.Name)/", RC_NULL), SearchPath::NoDescend)); } void DD_CreateFileSystemSchemes() { dint const schemedef_max_searchpaths = 5; struct schemedef_s { char const *name; char const *optOverridePath; char const *optFallbackPath; FS1::Scheme::Flags flags; SearchPath::Flags searchPathFlags; /// Priority is right to left. char const *searchPaths[schemedef_max_searchpaths]; } defs[] = { { "Defs", nullptr, nullptr, FS1::Scheme::Flag(0), 0, { "$(App.DefsPath)/", "$(App.DefsPath)/$(GamePlugin.Name)/", "$(App.DefsPath)/$(GamePlugin.Name)/$(Game.IdentityKey)/" } }, { "Graphics", "-gfxdir2", "-gfxdir", FS1::Scheme::Flag(0), 0, { "$(App.DataPath)/graphics/" } }, { "Models", "-modeldir2", "-modeldir", FS1::Scheme::MappedInPackages, 0, { "$(App.DataPath)/$(GamePlugin.Name)/models/", "$(App.DataPath)/$(GamePlugin.Name)/models/$(Game.IdentityKey)/" } }, { "Sfx", "-sfxdir2", "-sfxdir", FS1::Scheme::MappedInPackages, SearchPath::NoDescend, { "$(App.DataPath)/$(GamePlugin.Name)/sfx/", "$(App.DataPath)/$(GamePlugin.Name)/sfx/$(Game.IdentityKey)/" } }, { "Music", "-musdir2", "-musdir", FS1::Scheme::MappedInPackages, SearchPath::NoDescend, { "$(App.DataPath)/$(GamePlugin.Name)/music/", "$(App.DataPath)/$(GamePlugin.Name)/music/$(Game.IdentityKey)/" } }, { "Textures", "-texdir2", "-texdir", FS1::Scheme::MappedInPackages, SearchPath::NoDescend, { "$(App.DataPath)/$(GamePlugin.Name)/textures/", "$(App.DataPath)/$(GamePlugin.Name)/textures/$(Game.IdentityKey)/" } }, { "Flats", "-flatdir2", "-flatdir", FS1::Scheme::MappedInPackages, SearchPath::NoDescend, { "$(App.DataPath)/$(GamePlugin.Name)/flats/", "$(App.DataPath)/$(GamePlugin.Name)/flats/$(Game.IdentityKey)/" } }, { "Patches", "-patdir2", "-patdir", FS1::Scheme::MappedInPackages, SearchPath::NoDescend, { "$(App.DataPath)/$(GamePlugin.Name)/patches/", "$(App.DataPath)/$(GamePlugin.Name)/patches/$(Game.IdentityKey)/" } }, { "LightMaps", "-lmdir2", "-lmdir", FS1::Scheme::MappedInPackages, 0, { "$(App.DataPath)/$(GamePlugin.Name)/lightmaps/" } }, { "Fonts", "-fontdir2", "-fontdir", FS1::Scheme::MappedInPackages, SearchPath::NoDescend, { "$(App.DataPath)/fonts/", "$(App.DataPath)/$(GamePlugin.Name)/fonts/", "$(App.DataPath)/$(GamePlugin.Name)/fonts/$(Game.IdentityKey)/" } } }; createPackagesScheme(); // Setup the rest... for(schemedef_s const &def : defs) { FS1::Scheme &scheme = App_FileSystem().createScheme(def.name, def.flags); dint searchPathCount = 0; while(def.searchPaths[searchPathCount] && ++searchPathCount < schemedef_max_searchpaths) {} for(dint i = 0; i < searchPathCount; ++i) { scheme.addSearchPath(SearchPath(de::Uri(def.searchPaths[i], RC_NULL), def.searchPathFlags)); } if(def.optOverridePath && CommandLine_CheckWith(def.optOverridePath, 1)) { NativePath path = NativePath(CommandLine_NextAsPath()); scheme.addSearchPath(SearchPath(de::Uri::fromNativeDirPath(path), def.searchPathFlags), FS1::OverridePaths); path = path / "$(Game.IdentityKey)"; scheme.addSearchPath(SearchPath(de::Uri::fromNativeDirPath(path), def.searchPathFlags), FS1::OverridePaths); } if(def.optFallbackPath && CommandLine_CheckWith(def.optFallbackPath, 1)) { NativePath path = NativePath(CommandLine_NextAsPath()); scheme.addSearchPath(SearchPath(de::Uri::fromNativeDirPath(path), def.searchPathFlags), FS1::FallbackPaths); } } } void App_Error(char const *error, ...) { static bool errorInProgress = false; LogBuffer_Flush(); char buff[2048], err[256]; va_list argptr; #ifdef __CLIENT__ ClientWindow::main().canvas().trapMouse(false); #endif // Already in an error? if(errorInProgress) { #ifdef __CLIENT__ DisplayMode_Shutdown(); #endif va_start(argptr, error); dd_vsnprintf(buff, sizeof(buff), error, argptr); va_end(argptr); if(!BusyMode_InWorkerThread()) { Sys_MessageBox(MBT_ERROR, DOOMSDAY_NICENAME, buff, 0); } // Exit immediately, lest we go into an infinite loop. exit(1); } // We've experienced a fatal error; program will be shut down. errorInProgress = true; // Get back to the directory we started from. Dir_SetCurrent(DD_RuntimePath()); va_start(argptr, error); dd_vsnprintf(err, sizeof(err), error, argptr); va_end(argptr); LOG_CRITICAL("") << err; LogBuffer_Flush(); strcpy(buff, ""); strcat(buff, "\n"); strcat(buff, err); if(BusyMode_Active()) { BusyMode_WorkerError(buff); if(BusyMode_InWorkerThread()) { // We should not continue to execute the worker any more. forever Thread_Sleep(10000); } } else { App_AbnormalShutdown(buff); } } void App_AbnormalShutdown(char const *message) { // This is a crash landing, better be safe than sorry. BusyMode_SetAllowed(false); Sys_Shutdown(); #ifdef __CLIENT__ DisplayMode_Shutdown(); DENG2_GUI_APP->loop().pause(); // This is an abnormal shutdown, we cannot continue drawing any of the // windows. (Alternatively could hide/disable drawing of the windows.) Note // that the app's event loop is running normally while we show the native // message box below -- if the app windows are not hidden/closed, they might // receive draw events. ClientApp::windowSystem().closeAll(); #endif if(message) // Only show if a message given. { // Make sure all the buffered stuff goes into the file. LogBuffer_Flush(); /// @todo Get the actual output filename (might be a custom one). Sys_MessageBoxWithDetailsFromFile(MBT_ERROR, DOOMSDAY_NICENAME, message, "See Details for complete message log contents.", LogBuffer::get().outputFile().toUtf8()); } //Sys_Shutdown(); DD_Shutdown(); // Get outta here. exit(1); } InFineSystem &App_InFineSystem() { if(App::appExists()) { #ifdef __CLIENT__ return ClientApp::infineSystem(); #endif #ifdef __SERVER__ return ServerApp::infineSystem(); #endif } throw Error("App_InFineSystem", "App not yet initialized"); } ResourceSystem &App_ResourceSystem() { if(App::appExists()) { #ifdef __CLIENT__ return ClientApp::resourceSystem(); #endif #ifdef __SERVER__ return ServerApp::resourceSystem(); #endif } throw Error("App_ResourceSystem", "App not yet initialized"); } ResourceClass &App_ResourceClass(String className) { return App_ResourceSystem().resClass(className); } ResourceClass &App_ResourceClass(resourceclassid_t classId) { return App_ResourceSystem().resClass(classId); } WorldSystem &App_WorldSystem() { if(App::appExists()) { #ifdef __CLIENT__ return ClientApp::worldSystem(); #endif #ifdef __SERVER__ return ServerApp::worldSystem(); #endif } throw Error("App_WorldSystem", "App not yet initialized"); } static File1 *tryLoadFile(de::Uri const &path, size_t baseOffset = 0); static void parseStartupFilePathsAndAddFiles(char const *pathString) { static char const *ATWSEPS = ",; \t"; if(!pathString || !pathString[0]) return; size_t len = strlen(pathString); char *buffer = (char *) M_Malloc(len + 1); strcpy(buffer, pathString); char *token = strtok(buffer, ATWSEPS); while(token) { tryLoadFile(de::Uri(token, RC_NULL)); token = strtok(nullptr, ATWSEPS); } M_Free(buffer); } #undef Con_Open void Con_Open(dint yes) { #ifdef __CLIENT__ if(yes) { ClientWindow &win = ClientWindow::main(); win.taskBar().open(); win.root().setFocus(&win.console().commandLine()); } else { ClientWindow::main().console().closeLog(); } #endif #ifdef __SERVER__ DENG2_UNUSED(yes); #endif } #ifdef __CLIENT__ /** * Console command to open/close the console prompt. */ D_CMD(OpenClose) { DENG2_UNUSED2(src, argc); if(!stricmp(argv[0], "conopen")) { Con_Open(true); } else if(!stricmp(argv[0], "conclose")) { Con_Open(false); } else { Con_Open(!ClientWindow::main().console().isLogOpen()); } return true; } D_CMD(TaskBar) { DENG2_UNUSED3(src, argc, argv); ClientWindow &win = ClientWindow::main(); if(!win.taskBar().isOpen() || !win.console().commandLine().hasFocus()) { win.taskBar().open(); win.console().focusOnCommandLine(); } else { win.taskBar().close(); } return true; } D_CMD(Tutorial) { DENG2_UNUSED3(src, argc, argv); ClientWindow::main().taskBar().showTutorial(); return true; } #endif // __CLIENT__ /** * Find all game data file paths in the auto directory with the extensions * wad, lmp, pk3, zip and deh. * * @param found List of paths to be populated. * * @return Number of paths added to @a found. */ static dint findAllGameDataPaths(FS1::PathList &found) { static String const extensions[] = { "wad", "lmp", "pk3", "zip", "deh" #ifdef UNIX "WAD", "LMP", "PK3", "ZIP", "DEH" // upper case alternatives #endif }; dint const numFoundSoFar = found.count(); for(String const &ext : extensions) { DENG2_ASSERT(!ext.isEmpty()); String const searchPath = de::Uri(Path("$(App.DataPath)/$(GamePlugin.Name)/auto/*." + ext)).resolved(); App_FileSystem().findAllPaths(searchPath, 0, found); } return found.count() - numFoundSoFar; } /** * Find and try to load all game data file paths in auto directory. * * @return Number of new files that were loaded. */ static dint loadFilesFromDataGameAuto() { FS1::PathList found; findAllGameDataPaths(found); dint numLoaded = 0; DENG2_FOR_EACH_CONST(FS1::PathList, i, found) { // Ignore directories. if(i->attrib & A_SUBDIR) continue; if(tryLoadFile(de::Uri(i->path, RC_NULL))) { numLoaded += 1; } } return numLoaded; } bool DD_ExchangeGamePluginEntryPoints(pluginid_t pluginId) { if(pluginId != 0) { // Do the API transfer. GETGAMEAPI fptAdr; if(!(fptAdr = (GETGAMEAPI) DD_FindEntryPoint(pluginId, "GetGameAPI"))) { return false; } app.GetGameAPI = fptAdr; DD_InitAPI(); Def_GetGameClasses(); } else { app.GetGameAPI = 0; DD_InitAPI(); Def_GetGameClasses(); } return true; } static void loadResource(ResourceManifest &manifest) { DENG2_ASSERT(manifest.resourceClass() == RC_PACKAGE); de::Uri path(manifest.resolvedPath(false/*do not locate resource*/), RC_NULL); if(path.isEmpty()) return; if(File1 *file = tryLoadFile(path)) { // Mark this as an original game resource. file->setCustom(false); // Print the 'CRC' number of IWADs, so they can be identified. if(Wad *wad = file->maybeAs()) { LOG_RES_MSG("IWAD identification: %08x") << wad->calculateCRC(); } } } struct ddgamechange_params_t { /// @c true iff caller (i.e., App_ChangeGame) initiated busy mode. dd_bool initiatedBusyMode; }; static dint DD_BeginGameChangeWorker(void *context) { ddgamechange_params_t &parms = *static_cast(context); Map::initDummies(); P_InitMapEntityDefs(); if(parms.initiatedBusyMode) { Con_SetProgress(200); BusyMode_WorkerEnd(); } return 0; } static dint DD_LoadGameStartupResourcesWorker(void *context) { ddgamechange_params_t &parms = *static_cast(context); // Reset file Ids so previously seen files can be processed again. App_FileSystem().resetFileIds(); initPathMappings(); App_FileSystem().resetAllSchemes(); if(parms.initiatedBusyMode) { Con_SetProgress(50); } if(App_GameLoaded()) { // Create default Auto mappings in the runtime directory. // Data class resources. App_FileSystem().addPathMapping("auto/", de::Uri("$(App.DataPath)/$(GamePlugin.Name)/auto/", RC_NULL).resolved()); // Definition class resources. App_FileSystem().addPathMapping("auto/", de::Uri("$(App.DefsPath)/$(GamePlugin.Name)/auto/", RC_NULL).resolved()); } /** * Open all the files, load headers, count lumps, etc, etc... * @note Duplicate processing of the same file is automatically guarded * against by the virtual file system layer. */ GameManifests const &gameManifests = App_CurrentGame().manifests(); dint const numPackages = gameManifests.count(RC_PACKAGE); if(numPackages) { LOG_RES_MSG("Loading game resources") << (verbose >= 1? ":" : "..."); dint packageIdx = 0; for(GameManifests::const_iterator i = gameManifests.find(RC_PACKAGE); i != gameManifests.end() && i.key() == RC_PACKAGE; ++i, ++packageIdx) { loadResource(**i); // Update our progress. if(parms.initiatedBusyMode) { Con_SetProgress((packageIdx + 1) * (200 - 50) / numPackages - 1); } } } if(parms.initiatedBusyMode) { Con_SetProgress(200); BusyMode_WorkerEnd(); } return 0; } static dint addListFiles(QStringList const &list, FileType const &ftype) { dint numAdded = 0; foreach(QString const &path, list) { if(&ftype != &DD_GuessFileTypeFromFileName(path)) { continue; } if(tryLoadFile(de::Uri(path, RC_NULL))) { numAdded += 1; } } return numAdded; } /** * (Re-)Initialize the VFS path mappings. */ static void initPathMappings() { App_FileSystem().clearPathMappings(); if(DD_IsShuttingDown()) return; // Create virtual directory mappings by processing all -vdmap options. dint argC = CommandLine_Count(); for(dint i = 0; i < argC; ++i) { if(strnicmp("-vdmap", CommandLine_At(i), 6)) { continue; } if(i < argC - 1 && !CommandLine_IsOption(i + 1) && !CommandLine_IsOption(i + 2)) { String source = NativePath(CommandLine_PathAt(i + 1)).expand().withSeparators('/'); String destination = NativePath(CommandLine_PathAt(i + 2)).expand().withSeparators('/'); App_FileSystem().addPathMapping(source, destination); i += 2; } } } /// Skip all whitespace except newlines. static inline char const *skipSpace(char const *ptr) { DENG2_ASSERT(ptr != 0); while(*ptr && *ptr != '\n' && isspace(*ptr)) { ptr++; } return ptr; } static bool parsePathLumpMapping(char lumpName[9/*LUMPNAME_T_MAXLEN*/], ddstring_t *path, char const *buffer) { DENG2_ASSERT(lumpName != 0 && path != 0); // Find the start of the lump name. char const *ptr = skipSpace(buffer); // Just whitespace? if(!*ptr || *ptr == '\n') return false; // Find the end of the lump name. char const *end = (char const *)M_FindWhite((char *)ptr); if(!*end || *end == '\n') return false; size_t len = end - ptr; // Invalid lump name? if(len > 8) return false; memset(lumpName, 0, 9/*LUMPNAME_T_MAXLEN*/); strncpy(lumpName, ptr, len); strupr(lumpName); // Find the start of the file path. ptr = skipSpace(end); if(!*ptr || *ptr == '\n') return false; // Missing file path. // We're at the file path. Str_Set(path, ptr); // Get rid of any extra whitespace on the end. Str_StripRight(path); F_FixSlashes(path, path); return true; } /** *
 LUMPNAM0 \\Path\\In\\The\\Base.ext
 * LUMPNAM1 Path\\In\\The\\RuntimeDir.ext
 *  :
*/ static bool parsePathLumpMappings(char const *buffer) { DENG2_ASSERT(buffer != 0); bool successful = false; ddstring_t path; Str_Init(&path); ddstring_t line; Str_Init(&line); char const *ch = buffer; char lumpName[9/*LUMPNAME_T_MAXLEN*/]; do { ch = Str_GetLine(&line, ch); if(!parsePathLumpMapping(lumpName, &path, Str_Text(&line))) { // Failure parsing the mapping. // Ignore errors in individual mappings and continue parsing. //goto parseEnded; } else { String destination = NativePath(Str_Text(&path)).expand().withSeparators('/'); App_FileSystem().addPathLumpMapping(lumpName, destination); } } while(*ch); // Success. successful = true; //parseEnded: Str_Free(&line); Str_Free(&path); return successful; } /** * (Re-)Initialize the path => lump mappings. * @note Should be called after WADs have been processed. */ static void initPathLumpMappings() { // Free old paths, if any. App_FileSystem().clearPathLumpMappings(); if(DD_IsShuttingDown()) return; size_t bufSize = 0; uint8_t *buf = 0; // Add the contents of all DD_DIREC lumps. /// @todo fixme: Enforce scope to the containing package! LumpIndex const &lumpIndex = App_FileSystem().nameIndex(); LumpIndex::FoundIndices foundDirecs; lumpIndex.findAll("DD_DIREC.lmp", foundDirecs); DENG2_FOR_EACH_CONST(LumpIndex::FoundIndices, i, foundDirecs) // in load order { File1 &lump = lumpIndex[*i]; FileInfo const &lumpInfo = lump.info(); // Make a copy of it so we can ensure it ends in a null. if(bufSize < lumpInfo.size + 1) { bufSize = lumpInfo.size + 1; buf = (uint8_t *) M_Realloc(buf, bufSize); } lump.read(buf, 0, lumpInfo.size); buf[lumpInfo.size] = 0; parsePathLumpMappings(reinterpret_cast(buf)); } M_Free(buf); } static dint DD_LoadAddonResourcesWorker(void *context) { ddgamechange_params_t &parms = *static_cast(context); /** * Add additional game-startup files. * @note These must take precedence over Auto but not game-resource files. */ if(startupFiles && startupFiles[0]) { parseStartupFilePathsAndAddFiles(startupFiles); } if(parms.initiatedBusyMode) { Con_SetProgress(50); } if(App_GameLoaded()) { /** * Phase 3: Add real files from the Auto directory. */ game::Session::Profile &prof = game::Session::profile(); FS1::PathList found; findAllGameDataPaths(found); DENG2_FOR_EACH_CONST(FS1::PathList, i, found) { // Ignore directories. if(i->attrib & A_SUBDIR) continue; /// @todo Is expansion of symbolics still necessary here? prof.resourceFiles << NativePath(i->path).expand().withSeparators('/'); } if(!prof.resourceFiles.isEmpty()) { // First ZIPs then WADs (they may contain WAD files). addListFiles(prof.resourceFiles, DD_FileTypeByName("FT_ZIP")); addListFiles(prof.resourceFiles, DD_FileTypeByName("FT_WAD")); } // Final autoload round. DD_AutoLoad(); } if(parms.initiatedBusyMode) { Con_SetProgress(180); } initPathLumpMappings(); // Re-initialize the resource locator as there are now new resources to be found // on existing search paths (probably that is). App_FileSystem().resetAllSchemes(); if(parms.initiatedBusyMode) { Con_SetProgress(200); BusyMode_WorkerEnd(); } return 0; } static dint DD_ActivateGameWorker(void *context) { ddgamechange_params_t &parms = *static_cast(context); ResourceSystem &resSys = App_ResourceSystem(); // Some resources types are located prior to initializing the game. resSys.initTextures(); resSys.textureScheme("Lightmaps").clear(); resSys.textureScheme("Flaremaps").clear(); resSys.initMapDefs(); if(parms.initiatedBusyMode) { Con_SetProgress(50); } // Now that resources have been located we can begin to initialize the game. if(App_GameLoaded()) { // Any game initialization hooks? DD_CallHooks(HOOK_GAME_INIT, 0, 0); if(gx.PreInit) { DENG2_ASSERT(App_CurrentGame().pluginId() != 0); DD_SetActivePluginId(App_CurrentGame().pluginId()); gx.PreInit(App_Games().id(App_CurrentGame())); DD_SetActivePluginId(0); } } if(parms.initiatedBusyMode) { Con_SetProgress(100); } if(App_GameLoaded()) { // Parse the game's main config file. // If a custom top-level config is specified; let it override. Path configFile; if(CommandLine_CheckWith("-config", 1)) { configFile = NativePath(CommandLine_NextAsPath()).withSeparators('/'); } else { configFile = App_CurrentGame().mainConfig(); } LOG_SCR_MSG("Parsing primary config \"%s\"...") << NativePath(configFile).pretty(); Con_ParseCommands(configFile, CPCF_SET_DEFAULT | CPCF_ALLOW_SAVE_STATE); #ifdef __CLIENT__ // Apply default control bindings for this game. ClientApp::inputSystem().bindGameDefaults(); // Read bindings for this game and merge with the working set. Con_ParseCommands(App_CurrentGame().bindingConfig(), CPCF_ALLOW_SAVE_BINDINGS); #endif } if(parms.initiatedBusyMode) { Con_SetProgress(120); } Def_Read(); if(parms.initiatedBusyMode) { Con_SetProgress(130); } resSys.initSprites(); // Fully initialize sprites. #ifdef __CLIENT__ resSys.initModels(); #endif Def_PostInit(); DD_ReadGameHelp(); // Reset the tictimer so than any fractional accumulation is not added to // the tic/game timer of the newly-loaded game. gameTime = 0; DD_ResetTimer(); #ifdef __CLIENT__ // Make sure that the next frame does not use a filtered viewer. R_ResetViewer(); #endif // Invalidate old cmds and init player values. for(dint i = 0; i < DDMAXPLAYERS; ++i) { player_t *plr = &ddPlayers[i]; plr->extraLight = plr->targetExtraLight = 0; plr->extraLightCounter = 0; } if(gx.PostInit) { DD_SetActivePluginId(App_CurrentGame().pluginId()); gx.PostInit(); DD_SetActivePluginId(0); } if(parms.initiatedBusyMode) { Con_SetProgress(200); BusyMode_WorkerEnd(); } return 0; } de::Games &App_Games() { if(App::appExists()) { #ifdef __CLIENT__ return ClientApp::games(); #endif #ifdef __SERVER__ return ServerApp::games(); #endif } throw Error("App_Games", "App not yet initialized"); } dd_bool App_GameLoaded() { if(!App::appExists()) return false; return !App_CurrentGame().isNull(); } void App_ClearGames() { App_Games().clear(); App::app().setGame(App_Games().nullGame()); } static void populateGameInfo(GameInfo &info, de::Game &game) { info.identityKey = AutoStr_FromTextStd(game.identityKey().toUtf8().constData()); info.title = AutoStr_FromTextStd(game.title().toUtf8().constData()); info.author = AutoStr_FromTextStd(game.author().toUtf8().constData()); } /// @note Part of the Doomsday public API. #undef DD_GameInfo dd_bool DD_GameInfo(GameInfo *info) { LOG_AS("DD_GameInfo"); if(!info) return false; zapPtr(info); if(App_GameLoaded()) { populateGameInfo(*info, App_CurrentGame()); return true; } LOGDEV_WARNING("No game currently loaded"); return false; } #undef DD_AddGameResource void DD_AddGameResource(gameid_t gameId, resourceclassid_t classId, dint rflags, char const *names, void *params) { if(!VALID_RESOURCECLASSID(classId)) App_Error("DD_AddGameResource: Unknown resource class %i.", (dint)classId); if(!names || !names[0]) App_Error("DD_AddGameResource: Invalid name argument."); // Construct and attach the new resource record. Game &game = App_Games().byId(gameId); ResourceManifest *manifest = new ResourceManifest(classId, rflags); game.addManifest(*manifest); // Add the name list to the resource record. QStringList nameList = String(names).split(";", QString::SkipEmptyParts); foreach(QString const &nameRef, nameList) { manifest->addName(nameRef); } if(params && classId == RC_PACKAGE) { // Add the identityKey list to the resource record. QStringList idKeys = String((char const *) params).split(";", QString::SkipEmptyParts); foreach(QString const &idKeyRef, idKeys) { manifest->addIdentityKey(idKeyRef); } } } #undef DD_DefineGame gameid_t DD_DefineGame(GameDef const *def) { LOG_AS("DD_DefineGame"); if(!def) return 0; // Invalid id. // Game mode identity keys must be unique. Ensure that is the case. try { /*Game &game =*/ App_Games().byIdentityKey(def->identityKey); LOGDEV_WARNING("Ignored new game \"%s\", identity key '%s' already in use") << def->defaultTitle << def->identityKey; return 0; // Invalid id. } catch(Games::NotFoundError const &) {} // Ignore the error. Game *game = Game::fromDef(*def); if(!game) return 0; // Invalid def. // Add this game to our records. game->setPluginId(DD_ActivePluginId()); App_Games().add(*game); return App_Games().id(*game); } #undef DD_GameIdForKey gameid_t DD_GameIdForKey(char const *identityKey) { try { return App_Games().id(App_Games().byIdentityKey(identityKey)); } catch(Games::NotFoundError const &) { LOG_AS("DD_GameIdForKey"); LOGDEV_WARNING("Game \"%s\" is not defined, returning 0.") << identityKey; } return 0; // Invalid id. } de::Game &App_CurrentGame() { return App::game().as(); } bool App_ChangeGame(Game &game, bool allowReload) { #ifdef __CLIENT__ DENG_ASSERT(ClientWindow::mainExists()); #endif //LOG_AS("App_ChangeGame"); bool isReload = false; // Ignore attempts to re-load the current game? if(&App_CurrentGame() == &game) { if(!allowReload) { if(App_GameLoaded()) { LOG_NOTE("%s (%s) is already loaded") << game.title() << game.identityKey(); } return true; } // We are re-loading. isReload = true; } // The current game will be gone very soon. DENG2_FOR_EACH_OBSERVER(App::GameUnloadAudience, i, App::app().audienceForGameUnload()) { i->aboutToUnloadGame(App::game()); } // Quit netGame if one is in progress. #ifdef __SERVER__ if(netGame && isServer) { N_ServerClose(); } #else if(netGame) { Con_Execute(CMDS_DDAY, "net disconnect", true, false); } #endif S_Reset(); #ifdef __CLIENT__ Demo_StopPlayback(); GL_PurgeDeferredTasks(); //if(!Sys_IsShuttingDown()) { App_ResourceSystem().releaseAllGLTextures(); App_ResourceSystem().pruneUnusedTextureSpecs(); GL_LoadLightingSystemTextures(); GL_LoadFlareTextures(); Rend_ParticleLoadSystemTextures(); } GL_ResetViewEffects(); if(!game.isNull()) { ClientWindow &mainWin = ClientWindow::main(); mainWin.taskBar().close(); // Trap the mouse automatically when loading a game in fullscreen. if(mainWin.isFullScreen()) { mainWin.canvas().trapMouse(); } } #endif // If a game is presently loaded; unload it. if(App_GameLoaded()) { if(gx.Shutdown) { gx.Shutdown(); } Con_SaveDefaults(); #ifdef __CLIENT__ R_ClearViewData(); R_DestroyContactLists(); P_ClearPlayerImpulses(); Con_Execute(CMDS_DDAY, "clearbindings", true, false); ClientApp::inputSystem().bindDefaults(); ClientApp::inputSystem().initialContextActivations(); #endif // Reset the world back to it's initial state (unload the map, reset players, etc...). App_WorldSystem().reset(); Z_FreeTags(PU_GAMESTATIC, PU_PURGELEVEL - 1); P_ShutdownMapEntityDefs(); R_ShutdownSvgs(); App_ResourceSystem().clearAllRuntimeResources(); App_ResourceSystem().clearAllAnimGroups(); App_ResourceSystem().clearAllColorPalettes(); Sfx_InitLogical(); Con_ClearDatabases(); { // Tell the plugin it is being unloaded. void *unloader = DD_FindEntryPoint(App_CurrentGame().pluginId(), "DP_Unload"); LOGDEV_MSG("Calling DP_Unload %p") << unloader; DD_SetActivePluginId(App_CurrentGame().pluginId()); if(unloader) ((pluginfunc_t)unloader)(); DD_SetActivePluginId(0); } // We do not want to load session resources specified on the command line again. game::Session::profile().resourceFiles.clear(); // The current game is now the special "null-game". App::app().setGame(App_Games().nullGame()); Con_InitDatabases(); consoleRegister(); R_InitSvgs(); #ifdef __CLIENT__ ClientApp::inputSystem().initAllDevices(); #endif #ifdef __CLIENT__ R_InitViewWindow(); #endif App_FileSystem().unloadAllNonStartupFiles(); // Reset file IDs so previously seen files can be processed again. /// @todo this releases the IDs of startup files too but given the /// only startup file is doomsday.pk3 which we never attempt to load /// again post engine startup, this isn't an immediate problem. App_FileSystem().resetFileIds(); // Update the dir/WAD translations. initPathLumpMappings(); initPathMappings(); App_FileSystem().resetAllSchemes(); } App_InFineSystem().reset(); #ifdef __CLIENT__ App_InFineSystem().deinitBindingContext(); #endif /// @todo The entire material collection should not be destroyed during a reload. App_ResourceSystem().clearAllMaterialSchemes(); if(!game.isNull()) { LOG_MSG("Selecting game '%s'...") << game.id(); } else if(!isReload) { LOG_MSG("Unloaded game"); } Library_ReleaseGames(); #ifdef __CLIENT__ ClientWindow::main().setWindowTitle(DD_ComposeMainWindowTitle()); #endif if(!DD_IsShuttingDown()) { // Re-initialize subsystems needed even when in ringzero. if(!DD_ExchangeGamePluginEntryPoints(game.pluginId())) { LOG_WARNING("Game plugin for '%s' is invalid") << game.id(); LOGDEV_WARNING("Failed exchanging entrypoints with plugin %i") << dint(game.pluginId()); return false; } } // This is now the current game. App::app().setGame(game); game::Session::profile().gameId = game.id(); #ifdef __CLIENT__ ClientWindow::main().setWindowTitle(DD_ComposeMainWindowTitle()); #endif /* * If we aren't shutting down then we are either loading a game or switching * to ringzero (the current game will have already been unloaded). */ if(!DD_IsShuttingDown()) { #ifdef __CLIENT__ App_InFineSystem().initBindingContext(); #endif /* * The bulk of this we can do in busy mode unless we are already busy * (which can happen if a fatal error occurs during game load and we must * shutdown immediately; Sys_Shutdown will call back to load the special * "null-game" game). */ dint const busyMode = BUSYF_PROGRESS_BAR | (verbose? BUSYF_CONSOLE_OUTPUT : 0); ddgamechange_params_t p; BusyTask gameChangeTasks[] = { // Phase 1: Initialization. { DD_BeginGameChangeWorker, &p, busyMode, "Loading game...", 200, 0.0f, 0.1f, 0 }, // Phase 2: Loading "startup" resources. { DD_LoadGameStartupResourcesWorker, &p, busyMode, nullptr, 200, 0.1f, 0.3f, 0 }, // Phase 3: Loading "add-on" resources. { DD_LoadAddonResourcesWorker, &p, busyMode, "Loading add-ons...", 200, 0.3f, 0.7f, 0 }, // Phase 4: Game activation. { DD_ActivateGameWorker, &p, busyMode, "Starting game...", 200, 0.7f, 1.0f, 0 } }; p.initiatedBusyMode = !BusyMode_Active(); if(App_GameLoaded()) { // Tell the plugin it is being loaded. /// @todo Must this be done in the main thread? void *loader = DD_FindEntryPoint(App_CurrentGame().pluginId(), "DP_Load"); LOGDEV_MSG("Calling DP_Load %p") << loader; DD_SetActivePluginId(App_CurrentGame().pluginId()); if(loader) ((pluginfunc_t)loader)(); DD_SetActivePluginId(0); } /// @todo Kludge: Use more appropriate task names when unloading a game. if(game.isNull()) { gameChangeTasks[0].name = "Unloading game..."; gameChangeTasks[3].name = "Switching to ringzero..."; } // kludge end BusyMode_RunTasks(gameChangeTasks, sizeof(gameChangeTasks)/sizeof(gameChangeTasks[0])); #ifdef __CLIENT__ // Process any GL-related tasks we couldn't while Busy. Rend_ParticleLoadExtraTextures(); #endif if(App_GameLoaded()) { Game::printBanner(App_CurrentGame()); } /*else { // Lets play a nice title animation. DD_StartTitle(); }*/ } DENG_ASSERT(DD_ActivePluginId() == 0); #ifdef __CLIENT__ if(!Sys_IsShuttingDown()) { /** * Clear any input events we may have accumulated during this process. * @note Only necessary here because we might not have been able to use * busy mode (which would normally do this for us on end). */ ClientApp::inputSystem().clearEvents(); if(!App_GameLoaded()) { ClientWindow::main().taskBar().open(); } else { ClientWindow::main().console().zeroLogHeight(); } } #endif // Game change is complete. DENG2_FOR_EACH_OBSERVER(App::GameChangeAudience, i, App::app().audienceForGameChange()) { i->currentGameChanged(App::game()); } return true; } bool DD_IsShuttingDown() { return Sys_IsShuttingDown(); } /** * Looks for new files to autoload from the auto-load data directory. */ static void DD_AutoLoad() { /** * Keep loading files if any are found because virtual files may now * exist in the auto-load directory. */ dint numNewFiles; while((numNewFiles = loadFilesFromDataGameAuto()) > 0) { LOG_RES_VERBOSE("Autoload round completed with %i new files") << numNewFiles; } } /** * Attempt to determine which game is to be played. * * @todo Logic here could be much more elaborate but is it necessary? */ Game *DD_AutoselectGame() { if(CommandLine_CheckWith("-game", 1)) { char const *identityKey = CommandLine_Next(); try { Game &game = App_Games().byIdentityKey(identityKey); if(game.allStartupFilesFound()) { return &game; } } catch(Games::NotFoundError const &) {} // Ignore the error. } // If but one lonely game; select it. if(App_Games().numPlayable() == 1) { return App_Games().firstPlayable(); } // We don't know what to do. return 0; } dint DD_EarlyInit() { // Determine the requested degree of verbosity. ::verbose = CommandLine_Exists("-verbose"); #ifdef __SERVER__ ::isDedicated = true; #else ::isDedicated = false; #endif // Bring the console online as soon as we can. DD_ConsoleInit(); Con_InitDatabases(); // Register the engine's console commands and variables. consoleRegister(); return true; } // Perform basic runtime type size checks. #ifdef DENG2_DEBUG static void assertTypeSizes() { void *ptr = 0; int32_t int32 = 0; int16_t int16 = 0; dfloat float32 = 0; DENG2_UNUSED(ptr); DENG2_UNUSED(int32); DENG2_UNUSED(int16); DENG2_UNUSED(float32); ASSERT_32BIT(int32); ASSERT_16BIT(int16); ASSERT_32BIT(float32); #ifdef __64BIT__ ASSERT_64BIT(ptr); ASSERT_64BIT(int64_t); #else ASSERT_NOT_64BIT(ptr); #endif } #endif /** * Engine initialization. Once completed the game loop is ready to be started. * Called from the app entrypoint function. */ static void initialize() { DENG2_DEBUG_ONLY( assertTypeSizes(); ) static char const *AUTOEXEC_NAME = "autoexec.cfg"; #ifdef __CLIENT__ GL_EarlyInit(); #endif // Initialize the subsystems needed prior to entering busy mode for the first time. Sys_Init(); ResourceClass::setResourceClassCallback(App_ResourceClass); registerResourceFileTypes(); F_Init(); DD_CreateFileSystemSchemes(); #ifdef __CLIENT__ FR_Init(); // Enter busy mode until startup complete. Con_InitProgress2(200, 0, .25f); // First half. #endif BusyMode_RunNewTaskWithName(BUSYF_NO_UPLOADS | BUSYF_STARTUP | BUSYF_PROGRESS_BAR | (verbose? BUSYF_CONSOLE_OUTPUT : 0), DD_StartupWorker, 0, "Starting up..."); // Engine initialization is complete. Now finish up with the GL. #ifdef __CLIENT__ GL_Init(); GL_InitRefresh(); App_ResourceSystem().clearAllTextureSpecs(); App_ResourceSystem().initSystemTextures(); LensFx_Init(); #endif #ifdef __CLIENT__ // Do deferred uploads. Con_InitProgress2(200, .25f, .25f); // Stop here for a while. #endif BusyMode_RunNewTaskWithName(BUSYF_STARTUP | BUSYF_PROGRESS_BAR | (verbose? BUSYF_CONSOLE_OUTPUT : 0), DD_DummyWorker, 0, "Buffering..."); // Add resource paths specified using -iwad on the command line. FS1::Scheme &scheme = App_FileSystem().scheme(App_ResourceClass("RC_PACKAGE").defaultScheme()); for(dint p = 0; p < CommandLine_Count(); ++p) { if(!CommandLine_IsMatchingAlias("-iwad", CommandLine_At(p))) { continue; } while(++p != CommandLine_Count() && !CommandLine_IsOption(p)) { /// @todo Do not add these as search paths, publish them directly to /// the "Packages" scheme. // CommandLine_PathAt() always returns an absolute path. directory_t *dir = Dir_FromText(CommandLine_PathAt(p)); de::Uri uri = de::Uri::fromNativeDirPath(Dir_Path(dir), RC_PACKAGE); LOG_RES_NOTE("User-supplied IWAD path: \"%s\"") << Dir_Path(dir); scheme.addSearchPath(SearchPath(uri, SearchPath::NoDescend)); Dir_Delete(dir); } p--;/* For ArgIsOption(p) necessary, for p==Argc() harmless */ } App_ResourceSystem().updateOverrideIWADPathFromConfig(); // // Try to locate all required data files for all registered games. // #ifdef __CLIENT__ Con_InitProgress2(200, .25f, 1); // Second half. #endif App_Games().locateAllResources(); // Attempt automatic game selection. if(!CommandLine_Exists("-noautoselect") || isDedicated) { if(de::Game *game = DD_AutoselectGame()) { // An implicit game session profile has been defined. // Add all resources specified using -file options on the command line // to the list for the session. game::Session::Profile &prof = game::Session::profile(); for(dint p = 0; p < CommandLine_Count(); ++p) { if(!CommandLine_IsMatchingAlias("-file", CommandLine_At(p))) { continue; } while(++p != CommandLine_Count() && !CommandLine_IsOption(p)) { prof.resourceFiles << NativePath(CommandLine_PathAt(p)).expand().withSeparators('/'); } p--;/* For ArgIsOption(p) necessary, for p==Argc() harmless */ } // Begin the game session. App_ChangeGame(*game); } #ifdef __SERVER__ else { // A server is presently useless without a game, as shell // connections can only be made after a game is loaded and the // server mode started. /// @todo Allow shell connections in ringzero mode, too. App_Error("No playable games available."); } #endif } initPathLumpMappings(); // Re-initialize the filesystem subspace schemes as there are now new // resources to be found on existing search paths (probably that is). App_FileSystem().resetAllSchemes(); // // One-time execution of various command line features available during startup. // if(CommandLine_CheckWith("-dumplump", 1)) { String name = CommandLine_Next(); lumpnum_t lumpNum = App_FileSystem().lumpNumForName(name); if(lumpNum >= 0) { F_DumpFile(App_FileSystem().lump(lumpNum), 0); } else { LOG_RES_WARNING("Cannot dump unknown lump \"%s\"") << name; } } if(CommandLine_Check("-dumpwaddir")) { Con_Executef(CMDS_CMDLINE, false, "listlumps"); } // Try to load the autoexec file. This is done here to make sure everything is // initialized: the user can do here anything that s/he'd be able to do in-game // provided a game was loaded during startup. if(F_FileExists(AUTOEXEC_NAME)) { Con_ParseCommands(AUTOEXEC_NAME); } // Read additional config files that should be processed post engine init. if(CommandLine_CheckWith("-parse", 1)) { LOG_AS("-parse"); Time begunAt; forever { char const *arg = CommandLine_Next(); if(!arg || arg[0] == '-') break; LOG_MSG("Additional (pre-init) config file \"%s\"") << NativePath(arg).pretty(); Con_ParseCommands(arg); } LOGDEV_SCR_VERBOSE("Completed in %.2f seconds") << begunAt.since(); } // A console command on the command line? for(dint p = 1; p < CommandLine_Count() - 1; p++) { if(stricmp(CommandLine_At(p), "-command") && stricmp(CommandLine_At(p), "-cmd")) { continue; } for(++p; p < CommandLine_Count(); p++) { char const *arg = CommandLine_At(p); if(arg[0] == '-') { p--; break; } Con_Execute(CMDS_CMDLINE, arg, false, false); } } // // One-time execution of network commands on the command line. // Commands are only executed if we have loaded a game during startup. // if(App_GameLoaded()) { // Client connection command. if(CommandLine_CheckWith("-connect", 1)) { Con_Executef(CMDS_CMDLINE, false, "connect %s", CommandLine_Next()); } // Incoming TCP port. if(CommandLine_CheckWith("-port", 1)) { Con_Executef(CMDS_CMDLINE, false, "net-ip-port %s", CommandLine_Next()); } #ifdef __SERVER__ // Automatically start the server. N_ServerOpen(); #endif } else { // No game loaded. // Lets get most of everything else initialized. // Reset file IDs so previously seen files can be processed again. App_FileSystem().resetFileIds(); initPathLumpMappings(); initPathMappings(); App_FileSystem().resetAllSchemes(); App_ResourceSystem().initTextures(); App_ResourceSystem().textureScheme("Lightmaps").clear(); App_ResourceSystem().textureScheme("Flaremaps").clear(); App_ResourceSystem().initMapDefs(); Def_Read(); App_ResourceSystem().initSprites(); #ifdef __CLIENT__ App_ResourceSystem().initModels(); #endif Def_PostInit(); if(!CommandLine_Exists("-noautoselect")) { LOG_NOTE("Game could not be selected automatically"); } } } /** * This gets called when the main window is ready for GL init. The application * event loop is already running. */ void DD_FinishInitializationAfterWindowReady() { LOGDEV_MSG("Window is ready, finishing initialization"); #ifdef __CLIENT__ # ifdef WIN32 // Now we can get the color transfer table as the window is available. DisplayMode_SaveOriginalColorTransfer(); # endif if(!Sys_GLInitialize()) { App_Error("Error initializing OpenGL.\n"); } else { ClientWindow::main().setWindowTitle(DD_ComposeMainWindowTitle()); } #endif // Initialize engine subsystems and initial state. try { initialize(); /// @todo This notification should be done from the app. DENG2_FOR_EACH_OBSERVER(App::StartupCompleteAudience, i, App::app().audienceForStartupComplete()) { i->appStartupCompleted(); } return; } catch(Error const &er) { Sys_CriticalMessage((er.asText() + ".").toUtf8().constData()); } catch(...) {} exit(2); // Cannot continue... } static dint DD_StartupWorker(void * /*context*/) { #ifdef WIN32 // Initialize COM for this thread (needed for DirectInput). CoInitialize(nullptr); #endif Con_SetProgress(10); // Any startup hooks? DD_CallHooks(HOOK_STARTUP, 0, 0); Con_SetProgress(20); // Was the change to userdir OK? if(CommandLine_CheckWith("-userdir", 1) && !app.usingUserDir) { LOG_WARNING("User directory not found (check -userdir)"); } initPathMappings(); App_FileSystem().resetAllSchemes(); Con_SetProgress(40); Net_Init(); Sys_HideMouseCursor(); // Read config files that should be read BEFORE engine init. if(CommandLine_CheckWith("-cparse", 1)) { Time begunAt; LOG_AS("-cparse") forever { char const *arg = CommandLine_NextAsPath(); if(!arg || arg[0] == '-') break; LOG_MSG("Additional (pre-init) config file \"%s\"") << NativePath(arg).pretty(); Con_ParseCommands(arg); } LOGDEV_SCR_VERBOSE("Completed in %.2f seconds") << begunAt.since(); } // // Add required engine resource files. // String foundPath = App_FileSystem().findPath(de::Uri("doomsday.pk3", RC_PACKAGE), RLF_DEFAULT, App_ResourceClass(RC_PACKAGE)); foundPath = App_BasePath() / foundPath; // Ensure the path is absolute. File1 *loadedFile = tryLoadFile(de::Uri(foundPath, RC_NULL)); DENG2_ASSERT(loadedFile); DENG2_UNUSED(loadedFile); // No more files or packages will be loaded in "startup mode" after this point. App_FileSystem().endStartup(); // Load engine help resources. DD_InitHelp(); Con_SetProgress(60); // Execute the startup script (Startup.cfg). char const *startupConfig = "startup.cfg"; if(F_FileExists(startupConfig)) { Con_ParseCommands(startupConfig); } Con_SetProgress(90); R_BuildTexGammaLut(); #ifdef __CLIENT__ UI_LoadFonts(); #endif R_InitSvgs(); #ifdef __CLIENT__ R_InitViewWindow(); R_ResetFrameCount(); #endif Con_SetProgress(165); Net_InitGame(); #ifdef __CLIENT__ Demo_Init(); #endif Con_SetProgress(190); // In dedicated mode the console must be opened, so all input events // will be handled by it. if(isDedicated) { Con_Open(true); } Con_SetProgress(199); DD_CallHooks(HOOK_INIT, 0, 0); // Any initialization hooks? Con_SetProgress(200); #ifdef WIN32 // This thread has finished using COM. CoUninitialize(); #endif BusyMode_WorkerEnd(); return 0; } /** * This only exists so we have something to call while the deferred uploads of the * startup are processed. */ static dint DD_DummyWorker(void * /*context*/) { Con_SetProgress(200); BusyMode_WorkerEnd(); return 0; } void DD_CheckTimeDemo() { static bool checked = false; if(!checked) { checked = true; if(CommandLine_CheckWith("-timedemo", 1) || // Timedemo mode. CommandLine_CheckWith("-playdemo", 1)) // Play-once mode. { Block cmd = String("playdemo %1").arg(CommandLine_Next()).toUtf8(); Con_Execute(CMDS_CMDLINE, cmd.constData(), false, false); } } } static dint DD_UpdateEngineStateWorker(void *context) { DENG2_ASSERT(context); auto const initiatedBusyMode = *static_cast(context); #ifdef __CLIENT__ if(!novideo) { GL_InitRefresh(); App_ResourceSystem().clearAllTextureSpecs(); App_ResourceSystem().initSystemTextures(); } #endif if(initiatedBusyMode) { Con_SetProgress(50); } // Allow previously seen files to be processed again. App_FileSystem().resetFileIds(); // Re-read definitions. Def_Read(); // // Rebuild resource data models (defs might've changed). // App_ResourceSystem().clearAllRawTextures(); App_ResourceSystem().initSprites(); #ifdef __CLIENT__ App_ResourceSystem().initModels(); #endif Def_PostInit(); // // Update misc subsystems. // App_WorldSystem().update(); #ifdef __CLIENT__ // Recalculate the light range mod matrix. Rend_UpdateLightModMatrix(); // The rendering lists have persistent data that has changed during the // re-initialization. ClientApp::renderSystem().clearDrawLists(); #endif /// @todo fixme: Update the game title and the status. #ifdef DENG2_DEBUG Z_CheckHeap(); #endif if(initiatedBusyMode) { Con_SetProgress(200); BusyMode_WorkerEnd(); } return 0; } void DD_UpdateEngineState() { LOG_MSG("Updating engine state..."); BusyMode_FreezeGameForBusyMode(); // Stop playing sounds and music. S_Reset(); #ifdef __CLIENT__ GL_SetFilter(false); Demo_StopPlayback(); #endif //App_FileSystem().resetFileIds(); // Update the dir/WAD translations. initPathLumpMappings(); initPathMappings(); // Re-build the filesystem subspace schemes as there may be new resources to be found. App_FileSystem().resetAllSchemes(); App_ResourceSystem().initTextures(); App_ResourceSystem().initMapDefs(); if(App_GameLoaded() && gx.UpdateState) { gx.UpdateState(DD_PRE); } #ifdef __CLIENT__ dd_bool hadFog = usingFog; GL_TotalReset(); GL_TotalRestore(); // Bring GL back online. // Make sure the fog is enabled, if necessary. if(hadFog) { GL_UseFog(true); } #endif // The bulk of this we can do in busy mode unless we are already busy // (which can happen during a runtime game change). bool initiatedBusyMode = !BusyMode_Active(); if(initiatedBusyMode) { #ifdef __CLIENT__ Con_InitProgress(200); #endif BusyMode_RunNewTaskWithName(BUSYF_ACTIVITY | BUSYF_PROGRESS_BAR | (verbose? BUSYF_CONSOLE_OUTPUT : 0), DD_UpdateEngineStateWorker, &initiatedBusyMode, "Updating engine state..."); } else { /// @todo Update the current task name and push progress. DD_UpdateEngineStateWorker(&initiatedBusyMode); } if(App_GameLoaded() && gx.UpdateState) { gx.UpdateState(DD_POST); } #ifdef __CLIENT__ App_ResourceSystem().forAllMaterials([] (Material &material) { return material.forAllAnimators([] (MaterialAnimator &animator) { animator.rewind(); return LoopContinue; }); }); #endif } struct ddvalue_t { dint *readPtr; dint *writePtr; }; ddvalue_t ddValues[DD_LAST_VALUE - DD_FIRST_VALUE - 1] = { {&netGame, 0}, {&isServer, 0}, // An *open* server? {&isClient, 0}, #ifdef __SERVER__ {&allowFrames, &allowFrames}, #else {0, 0}, #endif {&consolePlayer, &consolePlayer}, {&displayPlayer, 0 /*&displayPlayer*/}, // use R_SetViewPortPlayer() instead #ifdef __CLIENT__ {&mipmapping, 0}, {&filterUI, 0}, {0, 0}, // defResX {0, 0}, // defResY #else {0, 0}, {0, 0}, {0, 0}, {0, 0}, #endif {0, 0}, {0, 0}, //{&mouseInverseY, &mouseInverseY}, #ifdef __CLIENT__ {&levelFullBright, &levelFullBright}, #else {0, 0}, #endif {0, 0}, // &CmdReturnValue #ifdef __CLIENT__ {&gameReady, &gameReady}, #else {0, 0}, #endif {&isDedicated, 0}, {&novideo, 0}, {&defs.mobjs.count.num, 0}, {&gotFrame, 0}, #ifdef __CLIENT__ {&playback, 0}, #else {0, 0}, #endif {&defs.sounds.count.num, 0}, {0, 0}, {0, 0}, #ifdef __CLIENT__ {&clientPaused, &clientPaused}, {&weaponOffsetScaleY, &weaponOffsetScaleY}, #else {0, 0}, {0, 0}, #endif {&gameDataFormat, &gameDataFormat}, #ifdef __CLIENT__ {&gameDrawHUD, 0}, {&symbolicEchoMode, &symbolicEchoMode}, {&numTexUnits, 0}, {&rendLightAttenuateFixedColormap, &rendLightAttenuateFixedColormap} #else {0, 0}, {0, 0}, {0, 0}, {0, 0} #endif }; #undef DD_GetInteger /** * Get a 32-bit signed integer value. */ dint DD_GetInteger(dint ddvalue) { switch(ddvalue) { #ifdef __CLIENT__ case DD_SHIFT_DOWN: return dint( ClientApp::inputSystem().shiftDown() ); case DD_WINDOW_WIDTH: return DENG_GAMEVIEW_WIDTH; case DD_WINDOW_HEIGHT: return DENG_GAMEVIEW_HEIGHT; case DD_CURRENT_CLIENT_FINALE_ID: return Cl_CurrentFinale(); case DD_DYNLIGHT_TEXTURE: return dint( GL_PrepareLSTexture(LST_DYNAMIC) ); case DD_USING_HEAD_TRACKING: return vrCfg().mode() == VRConfig::OculusRift && vrCfg().oculusRift().isReady(); #endif case DD_MAP_MUSIC: if(App_WorldSystem().hasMap()) { return Def_GetMusicNum(App_WorldSystem().map().mapInfo().gets("music").toUtf8().constData()); } return -1; default: break; } if(ddvalue >= DD_LAST_VALUE || ddvalue <= DD_FIRST_VALUE) { return 0; } if(ddValues[ddvalue].readPtr == 0) { return 0; } return *ddValues[ddvalue].readPtr; } #undef DD_SetInteger /** * Set a 32-bit signed integer value. */ void DD_SetInteger(dint ddvalue, dint parm) { if(ddvalue <= DD_FIRST_VALUE || ddvalue >= DD_LAST_VALUE) { return; } if(ddValues[ddvalue].writePtr) { *ddValues[ddvalue].writePtr = parm; } } #undef DD_GetVariable /** * Get a pointer to the value of a variable. Not all variables support * this. Added for 64-bit support. */ void *DD_GetVariable(dint ddvalue) { static dint value; static ddouble valueD; static timespan_t valueT; switch(ddvalue) { case DD_GAME_EXPORTS: return &gx; case DD_POLYOBJ_COUNT: value = App_WorldSystem().hasMap()? App_WorldSystem().map().polyobjCount() : 0; return &value; case DD_MAP_MIN_X: valueD = App_WorldSystem().hasMap()? App_WorldSystem().map().bounds().minX : 0; return &valueD; case DD_MAP_MIN_Y: valueD = App_WorldSystem().hasMap()? App_WorldSystem().map().bounds().minY : 0; return &valueD; case DD_MAP_MAX_X: valueD = App_WorldSystem().hasMap()? App_WorldSystem().map().bounds().maxX : 0; return &valueD; case DD_MAP_MAX_Y: valueD = App_WorldSystem().hasMap()? App_WorldSystem().map().bounds().maxY : 0; return &valueD; /*case DD_CPLAYER_THRUST_MUL: return &cplrThrustMul;*/ case DD_GRAVITY: valueD = App_WorldSystem().hasMap()? App_WorldSystem().map().gravity() : 0; return &valueD; #ifdef __CLIENT__ case DD_PSPRITE_OFFSET_X: return &pspOffset[0]; case DD_PSPRITE_OFFSET_Y: return &pspOffset[1]; case DD_PSPRITE_LIGHTLEVEL_MULTIPLIER: return &pspLightLevelMultiplier; case DD_TORCH_RED: return &torchColor.x; case DD_TORCH_GREEN: return &torchColor.y; case DD_TORCH_BLUE: return &torchColor.z; case DD_TORCH_ADDITIVE: return &torchAdditive; # ifdef WIN32 case DD_WINDOW_HANDLE: return ClientWindow::main().nativeHandle(); # endif #endif // We have to separately calculate the 35 Hz ticks. case DD_GAMETIC: valueT = gameTime * TICSPERSEC; return &valueT; case DD_DEFS: return &defs; default: break; } if(ddvalue >= DD_LAST_VALUE || ddvalue <= DD_FIRST_VALUE) { return 0; } // Other values not supported. return ddValues[ddvalue].writePtr; } /** * Set the value of a variable. The pointer can point to any data, its * interpretation depends on the variable. Added for 64-bit support. */ #undef DD_SetVariable void DD_SetVariable(dint ddvalue, void *parm) { if(ddvalue <= DD_FIRST_VALUE || ddvalue >= DD_LAST_VALUE) { switch(ddvalue) { /*case DD_CPLAYER_THRUST_MUL: cplrThrustMul = *(dfloat*) parm; return;*/ case DD_GRAVITY: if(App_WorldSystem().hasMap()) App_WorldSystem().map().setGravity(*(coord_t*) parm); return; #ifdef __CLIENT__ case DD_PSPRITE_OFFSET_X: pspOffset[0] = *(dfloat *) parm; return; case DD_PSPRITE_OFFSET_Y: pspOffset[1] = *(dfloat *) parm; return; case DD_PSPRITE_LIGHTLEVEL_MULTIPLIER: pspLightLevelMultiplier = *(dfloat *) parm; return; case DD_TORCH_RED: torchColor.x = de::clamp(0.f, *((dfloat*) parm), 1.f); return; case DD_TORCH_GREEN: torchColor.y = de::clamp(0.f, *((dfloat*) parm), 1.f); return; case DD_TORCH_BLUE: torchColor.z = de::clamp(0.f, *((dfloat*) parm), 1.f); return; case DD_TORCH_ADDITIVE: torchAdditive = (*(dint*) parm)? true : false; break; #endif default: break; } } } void DD_ReadGameHelp() { LOG_AS("DD_ReadGameHelp"); try { if(App_GameLoaded()) { de::Uri uri(Path("$(App.DataPath)/$(GamePlugin.Name)/conhelp.txt")); Help_ReadStrings(App::fileSystem().find(uri.resolved())); } } catch(Error const &er) { LOG_RES_WARNING("") << er.asText(); } } /// @note Part of the Doomsday public API. fontschemeid_t DD_ParseFontSchemeName(char const *str) { #ifdef __CLIENT__ try { FontScheme &scheme = App_ResourceSystem().fontScheme(str); if(!scheme.name().compareWithoutCase("System")) { return FS_SYSTEM; } if(!scheme.name().compareWithoutCase("Game")) { return FS_GAME; } } catch(ResourceSystem::UnknownSchemeError const &) {} #endif qDebug() << "Unknown font scheme:" << String(str) << ", returning 'FS_INVALID'"; return FS_INVALID; } String DD_MaterialSchemeNameForTextureScheme(String textureSchemeName) { if(!textureSchemeName.compareWithoutCase("Textures")) { return "Textures"; } if(!textureSchemeName.compareWithoutCase("Flats")) { return "Flats"; } if(!textureSchemeName.compareWithoutCase("Sprites")) { return "Sprites"; } if(!textureSchemeName.compareWithoutCase("System")) { return "System"; } return ""; } AutoStr *DD_MaterialSchemeNameForTextureScheme(ddstring_t const *textureSchemeName) { if(!textureSchemeName) { return AutoStr_FromTextStd(""); } else { QByteArray schemeNameUtf8 = DD_MaterialSchemeNameForTextureScheme(String(Str_Text(textureSchemeName))).toUtf8(); return AutoStr_FromTextStd(schemeNameUtf8.constData()); } } D_CMD(Load) { DENG2_UNUSED(src); bool didLoadGame = false, didLoadResource = false; dint arg = 1; AutoStr *searchPath = AutoStr_NewStd(); Str_Set(searchPath, argv[arg]); Str_Strip(searchPath); if(Str_IsEmpty(searchPath)) return false; F_FixSlashes(searchPath, searchPath); // Ignore attempts to load directories. if(Str_RAt(searchPath, 0) == '/') { LOG_WARNING("Directories cannot be \"loaded\" (only files and/or known games)."); return true; } // Are we loading a game? try { Game &game = App_Games().byIdentityKey(Str_Text(searchPath)); if(!game.allStartupFilesFound()) { LOG_WARNING("Failed to locate all required startup resources:"); Game::printFiles(game, FF_STARTUP); LOG_MSG("%s (%s) cannot be loaded.") << game.title() << game.identityKey(); return true; } BusyMode_FreezeGameForBusyMode(); if(!App_ChangeGame(game)) { return false; } didLoadGame = true; ++arg; } catch(Games::NotFoundError const &) {} // Ignore the error. // Try the resource locator. for(; arg < argc; ++arg) { try { String foundPath = App_FileSystem().findPath(de::Uri::fromNativePath(argv[arg], RC_PACKAGE), RLF_MATCH_EXTENSION, App_ResourceClass(RC_PACKAGE)); foundPath = App_BasePath() / foundPath; // Ensure the path is absolute. if(tryLoadFile(de::Uri(foundPath, RC_NULL))) { didLoadResource = true; } } catch(FS1::NotFoundError const &) {} // Ignore this error. } if(didLoadResource) { DD_UpdateEngineState(); } return didLoadGame || didLoadResource; } /** * Attempt to load the (logical) resource indicated by the @a search term. * * @param path Path to the resource to be loaded. Either a "real" file in * the local file system, or a "virtual" file. * @param baseOffset Offset from the start of the file in bytes to begin. * * @return @c true if the referenced resource was loaded. */ static File1 *tryLoadFile(de::Uri const &search, size_t baseOffset) { try { FileHandle &hndl = App_FileSystem().openFile(search.path(), "rb", baseOffset, false /* no duplicates */); de::Uri foundFileUri = hndl.file().composeUri(); LOG_VERBOSE("Loading \"%s\"...") << NativePath(foundFileUri.asText()).pretty().toUtf8().constData(); App_FileSystem().index(hndl.file()); return &hndl.file(); } catch(FS1::NotFoundError const&) { if(App_FileSystem().accessFile(search)) { // Must already be loaded. LOG_RES_XVERBOSE("\"%s\" already loaded") << NativePath(search.asText()).pretty(); } } return nullptr; } /** * Attempt to unload the (logical) resource indicated by the @a search term. * * @return @c true if the referenced resource was loaded and successfully unloaded. */ static bool tryUnloadFile(de::Uri const &search) { try { File1 &file = App_FileSystem().find(search); de::Uri foundFileUri = file.composeUri(); NativePath nativePath(foundFileUri.asText()); // Do not attempt to unload a resource required by the current game. if(App_CurrentGame().isRequiredFile(file)) { LOG_RES_NOTE("\"%s\" is required by the current game." " Required game files cannot be unloaded in isolation.") << nativePath.pretty(); return false; } LOG_RES_VERBOSE("Unloading \"%s\"...") << nativePath.pretty(); App_FileSystem().deindex(file); delete &file; return true; } catch(FS1::NotFoundError const&) {} // Ignore. return false; } D_CMD(Unload) { DENG2_UNUSED(src); BusyMode_FreezeGameForBusyMode(); // No arguments; unload the current game if loaded. if(argc == 1) { if(!App_GameLoaded()) { LOG_MSG("No game is currently loaded."); return true; } return App_ChangeGame(App_Games().nullGame()); } AutoStr *searchPath = AutoStr_NewStd(); Str_Set(searchPath, argv[1]); Str_Strip(searchPath); if(Str_IsEmpty(searchPath)) return false; F_FixSlashes(searchPath, searchPath); // Ignore attempts to unload directories. if(Str_RAt(searchPath, 0) == '/') { LOG_MSG("Directories cannot be \"unloaded\" (only files and/or known games)."); return true; } // Unload the current game if specified. if(argc == 2) { try { Game &game = App_Games().byIdentityKey(Str_Text(searchPath)); if(App_GameLoaded()) { return App_ChangeGame(App_Games().nullGame()); } LOG_MSG("%s is not currently loaded.") << game.identityKey(); return true; } catch(Games::NotFoundError const &) {} // Ignore the error. } // Try the resource locator. bool didUnloadFiles = false; for(dint i = 1; i < argc; ++i) { try { String foundPath = App_FileSystem().findPath(de::Uri::fromNativePath(argv[1], RC_PACKAGE), RLF_MATCH_EXTENSION, App_ResourceClass(RC_PACKAGE)); foundPath = App_BasePath() / foundPath; // Ensure the path is absolute. if(tryUnloadFile(de::Uri(foundPath, RC_NULL))) { didUnloadFiles = true; } } catch(FS1::NotFoundError const&) {} // Ignore this error. } if(didUnloadFiles) { // A changed file list may alter the main lump directory. DD_UpdateEngineState(); } return didUnloadFiles; } D_CMD(Reset) { DENG2_UNUSED3(src, argc, argv); DD_UpdateEngineState(); return true; } D_CMD(ReloadGame) { DENG2_UNUSED3(src, argc, argv); if(!App_GameLoaded()) { LOG_MSG("No game is presently loaded."); return true; } App_ChangeGame(App_CurrentGame(), true/* allow reload */); return true; } #ifdef __CLIENT__ D_CMD(CheckForUpdates) { DENG2_UNUSED3(src, argc, argv); LOG_MSG("Checking for available updates..."); ClientApp::updater().checkNow(Updater::OnlyShowResultIfUpdateAvailable); return true; } D_CMD(CheckForUpdatesAndNotify) { DENG2_UNUSED3(src, argc, argv); LOG_MSG("Checking for available updates..."); ClientApp::updater().checkNow(Updater::AlwaysShowResult); return true; } D_CMD(LastUpdated) { DENG2_UNUSED3(src, argc, argv); ClientApp::updater().printLastUpdated(); return true; } D_CMD(ShowUpdateSettings) { DENG2_UNUSED3(src, argc, argv); ClientApp::updater().showSettings(); return true; } #endif // __CLIENT__ D_CMD(Version) { DENG2_UNUSED3(src, argc, argv); LOG_NOTE(_E(D) DOOMSDAY_NICENAME " " DOOMSDAY_VERSION_FULLTEXT); LOG_MSG(_E(l) "Homepage: " _E(.) _E(i) DOOMSDAY_HOMEURL _E(.) "\n" _E(l) "Project: " _E(.) _E(i) DENGPROJECT_HOMEURL); // Print the version info of the current game if loaded. if(App_GameLoaded()) { LOG_MSG(_E(l) "Game: " _E(.) "%s") << (char const *) gx.GetVariable(DD_PLUGIN_VERSION_LONG); } return true; } D_CMD(Quit) { DENG2_UNUSED2(src, argc); #ifdef __CLIENT__ if(DownloadDialog::isDownloadInProgress()) { LOG_WARNING("Cannot quit while downloading an update"); ClientWindow::main().taskBar().openAndPauseGame(); DownloadDialog::currentDownload().open(); return false; } #endif if(argv[0][4] == '!' || isDedicated || !App_GameLoaded() || gx.TryShutdown == 0) { // No questions asked. Sys_Quit(); return true; // Never reached. } #ifdef __CLIENT__ // Dismiss the taskbar if it happens to be open, we are expecting // the game to handle this from now on. ClientWindow::main().taskBar().close(); #endif // Defer this decision to the loaded game. return gx.TryShutdown(); } #ifdef _DEBUG D_CMD(DebugError) { DENG2_UNUSED3(src, argv, argc); App_Error("Fatal error!\n"); return true; } #endif D_CMD(Help) { DENG2_UNUSED3(src, argc, argv); /* #ifdef __CLIENT__ char actKeyName[40]; strcpy(actKeyName, B_ShortNameForKey(consoleActiveKey)); actKeyName[0] = toupper(actKeyName[0]); #endif */ LOG_SCR_NOTE(_E(b) DOOMSDAY_NICENAME " " DOOMSDAY_VERSION_TEXT " Console"); #define TABBED(A, B) "\n" _E(Ta) _E(b) " " << A << " " _E(.) _E(Tb) << B #ifdef __CLIENT__ LOG_SCR_MSG(_E(D) "Keys:" _E(.)) << TABBED(DENG2_CHAR_SHIFT_KEY "Esc", "Open the taskbar and console") << TABBED("Tab", "Autocomplete the word at the cursor") << TABBED(DENG2_CHAR_UP_DOWN_ARROW, "Move backwards/forwards through the input command history, or up/down one line inside a multi-line command") << TABBED("PgUp/Dn", "Scroll up/down in the history, or expand the history to full height") << TABBED(DENG2_CHAR_SHIFT_KEY "PgUp/Dn", "Jump to the top/bottom of the history") << TABBED("Home", "Move the cursor to the start of the command line") << TABBED("End", "Move the cursor to the end of the command line") << TABBED(DENG2_CHAR_CONTROL_KEY "K", "Clear everything on the line right of the cursor position") << TABBED("F5", "Clear the console message history"); #endif LOG_SCR_MSG(_E(D) "Getting started:"); LOG_SCR_MSG(" " _E(>) "Enter " _E(b) "help (what)" _E(.) " for information about " _E(l) "(what)"); LOG_SCR_MSG(" " _E(>) "Enter " _E(b) "listcmds" _E(.) " to list available commands"); LOG_SCR_MSG(" " _E(>) "Enter " _E(b) "listgames" _E(.) " to list installed games and their status"); LOG_SCR_MSG(" " _E(>) "Enter " _E(b) "listvars" _E(.) " to list available variables"); #undef TABBED return true; } static void printHelpAbout(char const *query) { // Try the console commands first. if(ccmd_t *ccmd = Con_FindCommand(query)) { LOG_SCR_MSG(_E(b) "%s" _E(.) " (Command)") << ccmd->name; HelpId help = DH_Find(ccmd->name); if(char const *description = DH_GetString(help, HST_DESCRIPTION)) { LOG_SCR_MSG("") << description; } Con_PrintCommandUsage(ccmd); // For all overloaded variants. // Any extra info? if(char const *info = DH_GetString(help, HST_INFO)) { LOG_SCR_MSG(" " _E(>) _E(l)) << info; } return; } if(cvar_t *var = Con_FindVariable(query)) { AutoStr *path = CVar_ComposePath(var); LOG_SCR_MSG(_E(b) "%s" _E(.) " (Variable)") << Str_Text(path); HelpId help = DH_Find(Str_Text(path)); if(char const *description = DH_GetString(help, HST_DESCRIPTION)) { LOG_SCR_MSG("") << description; } return; } if(calias_t *calias = Con_FindAlias(query)) { LOG_SCR_MSG(_E(b) "%s" _E(.) " alias of:\n") << calias->name << calias->command; return; } // Perhaps a game? try { Game &game = App_Games().byIdentityKey(query); LOG_SCR_MSG(_E(b) "%s" _E(.) " (IdentityKey)") << game.identityKey(); LOG_SCR_MSG("Unique identifier of the " _E(b) "%s" _E(.) " game mode.") << game.title(); LOG_SCR_MSG("An 'IdentityKey' is used when referencing a game unambiguously from the console and on the command line."); LOG_SCR_MSG(_E(D) "Related commands:"); LOG_SCR_MSG(" " _E(>) "Enter " _E(b) "inspectgame %s" _E(.) " for information and status of this game") << game.identityKey(); LOG_SCR_MSG(" " _E(>) "Enter " _E(b) "listgames" _E(.) " to list all installed games and their status"); LOG_SCR_MSG(" " _E(>) "Enter " _E(b) "load %s" _E(.) " to load the " _E(l) "%s" _E(.) " game mode") << game.identityKey() << game.title(); return; } catch(Games::NotFoundError const &) {} // Ignore this error. LOG_SCR_NOTE("There is no help about '%s'") << query; } D_CMD(HelpWhat) { DENG2_UNUSED2(argc, src); if(!String(argv[1]).compareWithoutCase("(what)")) { LOG_SCR_MSG("You've got to be kidding!"); return true; } printHelpAbout(argv[1]); return true; } #ifdef __CLIENT__ D_CMD(Clear) { DENG2_UNUSED3(src, argc, argv); ClientWindow::main().console().clearLog(); return true; } #endif static void consoleRegister() { C_VAR_CHARPTR("file-startup", &startupFiles, 0, 0, 0); C_CMD("help", "", Help); C_CMD("help", "s", HelpWhat); C_CMD("version", "", Version); C_CMD("quit", "", Quit); C_CMD("quit!", "", Quit); C_CMD("load", "s*", Load); C_CMD("reset", "", Reset); C_CMD("reload", "", ReloadGame); C_CMD("unload", "*", Unload); C_CMD("listmobjtypes", "", ListMobjs); C_CMD("write", "s", WriteConsole); #ifdef DENG2_DEBUG C_CMD("fatalerror", nullptr, DebugError); #endif DD_RegisterLoop(); FS1::consoleRegister(); Con_Register(); Games::consoleRegister(); DH_Register(); S_Register(); #ifdef __CLIENT__ C_CMD("clear", "", Clear); C_CMD("update", "", CheckForUpdates); C_CMD("updateandnotify", "", CheckForUpdatesAndNotify); C_CMD("updatesettings", "", ShowUpdateSettings); C_CMD("lastupdated", "", LastUpdated); C_CMD_FLAGS("conclose", "", OpenClose, CMDF_NO_DEDICATED); C_CMD_FLAGS("conopen", "", OpenClose, CMDF_NO_DEDICATED); C_CMD_FLAGS("contoggle", "", OpenClose, CMDF_NO_DEDICATED); C_CMD ("taskbar", "", TaskBar); C_CMD ("tutorial", "", Tutorial); /// @todo Move to UI module. Con_TransitionRegister(); InputSystem::consoleRegister(); SBE_Register(); // for bias editor RenderSystem::consoleRegister(); GL_Register(); UI_Register(); Demo_Register(); P_ConsoleRegister(); I_Register(); #endif ResourceSystem::consoleRegister(); Net_Register(); WorldSystem::consoleRegister(); InFineSystem::consoleRegister(); } // dd_loop.c DENG_EXTERN_C dd_bool DD_IsSharpTick(void); // net_main.c DENG_EXTERN_C void Net_SendPacket(dint to_player, dint type, const void* data, size_t length); #undef R_SetupMap DENG_EXTERN_C void R_SetupMap(dint mode, dint flags) { DENG2_UNUSED2(mode, flags); if(!App_WorldSystem().hasMap()) return; // Huh? // Perform map setup again. Its possible that after loading we now // have more HOMs to fix, etc.. Map &map = App_WorldSystem().map(); #ifdef __CLIENT__ map.initSkyFix(); #endif #ifdef __CLIENT__ // Update all sectors. /// @todo Refactor away. map.forAllSectors([] (Sector §or) { sector.forAllSides([] (LineSide &side) { side.fixMissingMaterials(); return LoopContinue; }); return LoopContinue; }); #endif // Re-initialize polyobjs. /// @todo Still necessary? map.initPolyobjs(); // Reset the timer so that it will appear that no time has passed. DD_ResetTimer(); } // sys_system.c DENG_EXTERN_C void Sys_Quit(); DENG_DECLARE_API(Base) = { { DE_API_BASE }, Sys_Quit, DD_GetInteger, DD_SetInteger, DD_GetVariable, DD_SetVariable, DD_DefineGame, DD_GameIdForKey, DD_AddGameResource, DD_GameInfo, DD_IsSharpTick, Net_SendPacket, R_SetupMap }; doomsday-stable-1.15.7/doomsday/client/src/m_misc.cpp0000664000175000017500000003360412641367670022041 0ustar jaakkojaakko/** @file m_misc.cpp Miscellanous utility routines. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #define DENG_NO_API_MACROS_FILESYS #include "de_platform.h" #include "de_console.h" #include #include #include #if defined(WIN32) # include # include # include # define open _open #endif #if defined(UNIX) # include # include #endif #ifndef O_BINARY # define O_BINARY 0 #endif #include "de_base.h" #include "de_system.h" #include "de_filesys.h" #include "de_graphics.h" #include "de_misc.h" #include "de_play.h" #include "lzss.h" #include #include #include #include #undef M_WriteFile #undef M_ReadFile #define SLOPERANGE 2048 #define SLOPEBITS 11 #define DBITS (FRACBITS-SLOPEBITS) using namespace de; static size_t FileReader(char const* name, char** buffer); extern int tantoangle[SLOPERANGE + 1]; // get from tables.c int M_BoxOnLineSide(const AABoxd* box, double const linePoint[], double const lineDirection[]) { int a, b; switch(M_SlopeType(lineDirection)) { default: // Shut up compiler. case ST_HORIZONTAL: a = box->maxY > linePoint[VY]? -1 : 1; b = box->minY > linePoint[VY]? -1 : 1; if(lineDirection[VX] < 0) { a = -a; b = -b; } break; case ST_VERTICAL: a = box->maxX < linePoint[VX]? -1 : 1; b = box->minX < linePoint[VX]? -1 : 1; if(lineDirection[VY] < 0) { a = -a; b = -b; } break; case ST_POSITIVE: { double topLeft[2] = { box->minX, box->maxY }; double bottomRight[2] = { box->maxX, box->minY }; a = V2d_PointOnLineSide(topLeft, linePoint, lineDirection) < 0 ? -1 : 1; b = V2d_PointOnLineSide(bottomRight, linePoint, lineDirection) < 0 ? -1 : 1; break; } case ST_NEGATIVE: a = V2d_PointOnLineSide(box->max, linePoint, lineDirection) < 0 ? -1 : 1; b = V2d_PointOnLineSide(box->min, linePoint, lineDirection) < 0 ? -1 : 1; break; } if(a == b) return a; return 0; } int M_BoxOnLineSide_FixedPrecision(const fixed_t box[], const fixed_t linePoint[], const fixed_t lineDirection[]) { int a = 0, b = 0; switch(M_SlopeTypeXY_FixedPrecision(lineDirection[0], lineDirection[1])) { case ST_HORIZONTAL: a = box[BOXTOP] > linePoint[VY]? -1 : 1; b = box[BOXBOTTOM] > linePoint[VY]? -1 : 1; if(lineDirection[VX] < 0) { a = -a; b = -b; } break; case ST_VERTICAL: a = box[BOXRIGHT] < linePoint[VX]? -1 : 1; b = box[BOXLEFT] < linePoint[VX]? -1 : 1; if(lineDirection[VY] < 0) { a = -a; b = -b; } break; case ST_POSITIVE: { fixed_t topLeft[2] = { box[BOXLEFT], box[BOXTOP] }; fixed_t bottomRight[2] = { box[BOXRIGHT], box[BOXBOTTOM] }; a = V2x_PointOnLineSide(topLeft, linePoint, lineDirection)? -1 : 1; b = V2x_PointOnLineSide(bottomRight, linePoint, lineDirection)? -1 : 1; break; } case ST_NEGATIVE: { fixed_t boxMax[2] = { box[BOXRIGHT], box[BOXTOP] }; fixed_t boxMin[2] = { box[BOXLEFT], box[BOXBOTTOM] }; a = V2x_PointOnLineSide(boxMax, linePoint, lineDirection)? -1 : 1; b = V2x_PointOnLineSide(boxMin, linePoint, lineDirection)? -1 : 1; break; } } if(a == b) return a; return 0; // on the line } int M_BoxOnLineSide2(const AABoxd* box, double const linePoint[], double const lineDirection[], double linePerp, double lineLength, double epsilon) { #define NORMALIZE(v) ((v) < 0? -1 : (v) > 0? 1 : 0) double delta; int a, b; switch(M_SlopeType(lineDirection)) { default: // Shut up compiler. case ST_HORIZONTAL: a = box->maxY > linePoint[VY]? -1 : 1; b = box->minY > linePoint[VY]? -1 : 1; if(lineDirection[VX] < 0) { a = -a; b = -b; } break; case ST_VERTICAL: a = box->maxX < linePoint[VX]? -1 : 1; b = box->minX < linePoint[VX]? -1 : 1; if(lineDirection[VY] < 0) { a = -a; b = -b; } break; case ST_POSITIVE: { double topLeft[2] = { box->minX, box->maxY }; double bottomRight[2] = { box->maxX, box->minY }; delta = V2d_PointOnLineSide2(topLeft, lineDirection, linePerp, lineLength, epsilon); a = NORMALIZE(delta); delta = V2d_PointOnLineSide2(bottomRight, lineDirection, linePerp, lineLength, epsilon); b = NORMALIZE(delta); break; } case ST_NEGATIVE: delta = V2d_PointOnLineSide2(box->max, lineDirection, linePerp, lineLength, epsilon); a = NORMALIZE(delta); delta = V2d_PointOnLineSide2(box->min, lineDirection, linePerp, lineLength, epsilon); b = NORMALIZE(delta); break; } if(a == b) return a; return 0; #undef NORMALIZE } /** * Read a file into a buffer allocated using M_Malloc(). */ DENG_EXTERN_C size_t M_ReadFile(const char* name, char** buffer) { return FileReader(name, buffer); } DENG_EXTERN_C AutoStr *M_ReadFileIntoString(ddstring_t const *path, dd_bool *isCustom) { if(isCustom) *isCustom = false; if(Str_StartsWith(path, "LumpIndex:")) { bool isNumber; lumpnum_t const lumpNum = String(Str_Text(path) + 10).toInt(&isNumber); LumpIndex const &lumpIndex = App_FileSystem().nameIndex(); if(isNumber && lumpIndex.hasLump(lumpNum)) { File1 &lump = lumpIndex.lump(lumpNum); if(isCustom) { /// @todo Custom status for contained files is not inherited from the container? *isCustom = (lump.isContained()? lump.container().hasCustom() : lump.hasCustom()); } // Ignore zero-length lumps. if(!lump.size()) return 0; // Ensure the resulting string is terminated. AutoStr *string = Str_PartAppend(AutoStr_NewStd(), (char const *)lump.cache(), 0, lump.size()); lump.unlock(); if(Str_IsEmpty(string)) return 0; return string; } return 0; } if(Str_StartsWith(path, "Lumps:")) { char const *lumpName = Str_Text(path) + 6; LumpIndex const &lumpIndex = App_FileSystem().nameIndex(); if(!lumpIndex.contains(String(lumpName) + ".lmp")) return 0; File1 &lump = lumpIndex[lumpIndex.findLast(String(lumpName) + ".lmp")]; if(isCustom) { /// @todo Custom status for contained files is not inherited from the container? *isCustom = (lump.isContained()? lump.container().hasCustom() : lump.hasCustom()); } // Ignore zero-length lumps. if(!lump.size()) return 0; // Ensure the resulting string is terminated. AutoStr *string = Str_PartAppend(AutoStr_NewStd(), (char const *)lump.cache(), 0, lump.size()); lump.unlock(); if(Str_IsEmpty(string)) return 0; return string; } // Try the virtual file system. try { QScopedPointer hndl(&App_FileSystem().openFile(Str_Text(path), "rb")); if(isCustom) { /// @todo Custom status for contained files is not inherited from the container? File1 &file = hndl->file(); *isCustom = (file.isContained()? file.container().hasCustom() : file.hasCustom()); } // Ignore zero-length lumps. AutoStr *string = nullptr; if(size_t lumpLength = hndl->length()) { // Read in the whole thing and ensure the resulting string is terminated. Block buffer; buffer.resize(lumpLength); hndl->read((uint8_t *)buffer.data(), lumpLength); string = Str_PartAppend(AutoStr_NewStd(), buffer.constData(), 0, lumpLength); } App_FileSystem().releaseFile(hndl->file()); if(!string || Str_IsEmpty(string)) return 0; return string; } catch(FS1::NotFoundError const &) {} // Ignore this error. // Perhaps a local file known to the native file system? char *readBuf = 0; if(size_t bytesRead = M_ReadFile(Str_Text(path), &readBuf)) { // Ensure the resulting string is terminated. AutoStr *string = Str_PartAppend(AutoStr_New(), readBuf, 0, int(bytesRead)); Z_Free(readBuf); // Ignore zero-length files. if(Str_IsEmpty(string)) return 0; return string; } return 0; } #if defined(WIN32) #define close _close #define read _read #define write _write #endif static size_t FileReader(const char* name, char** buffer) { struct stat fileinfo; char* buf = NULL; size_t length = 0; int handle; LOG_AS("FileReader"); // First try with LZSS. LZFILE *file = lzOpen((char*) name, "rp"); if(NULL != file) { #define BSIZE 1024 char readBuf[BSIZE]; // Read 1kb pieces until file ends. while(!lzEOF(file)) { size_t bytesRead = lzRead(readBuf, BSIZE, file); char* newBuf; // Allocate more memory. newBuf = (char*) Z_Malloc(length + bytesRead, PU_APPSTATIC, 0); if(buf != NULL) { memcpy(newBuf, buf, length); Z_Free(buf); } buf = newBuf; // Copy new data to buffer. memcpy(buf + length, readBuf, bytesRead); length += bytesRead; } lzClose(file); *buffer = (char*)buf; return length; #undef BSIZE } handle = open(name, O_RDONLY | O_BINARY, 0666); if(handle == -1) { LOG_RES_WARNING("Failed opening \"%s\" for reading") << name; return length; } if(-1 == fstat(handle, &fileinfo)) { LOG_RES_ERROR("Couldn't read file \"%s\"") << name; return 0; } length = fileinfo.st_size; if(!length) { *buffer = 0; return 0; } buf = (char *) Z_Malloc(length, PU_APPSTATIC, 0); DENG_ASSERT(buf != 0); size_t bytesRead = read(handle, buf, length); close(handle); if(bytesRead < length) { LOG_RES_ERROR("Couldn't read file \"%s\"") << name; } *buffer = buf; return length; } DENG_EXTERN_C dd_bool M_WriteFile(const char* name, const char* source, size_t length) { int handle = open(name, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666); size_t count; if(handle == -1) return false; count = write(handle, source, length); close(handle); return (count >= length); } void M_WriteCommented(FILE *file, const char* text) { char *buff = (char *) M_Malloc(strlen(text) + 1), *line; strcpy(buff, text); line = strtok(buff, "\n"); while(line) { fprintf(file, "# %s\n", line); line = strtok(NULL, "\n"); } M_Free(buff); } /** * The caller must provide the opening and closing quotes. */ void M_WriteTextEsc(FILE* file, const char* text) { DENG_ASSERT(file && text); size_t i; for(i = 0; i < strlen(text) && text[i]; ++i) { if(text[i] == '"' || text[i] == '\\') fprintf(file, "\\"); fprintf(file, "%c", text[i]); } } DENG_EXTERN_C int M_ScreenShot(char const *name, int bits) { #ifdef __CLIENT__ DENG2_UNUSED(bits); de::String fullName(name); if(fullName.fileNameExtension().isEmpty()) { fullName += ".png"; // Default format. } return ClientWindow::main().grabToFile(fullName)? 1 : 0; #else DENG2_UNUSED2(name, bits); return false; #endif } void M_ReadBits(uint numBits, const uint8_t** src, uint8_t* cb, uint8_t* out) { assert(src && cb && out); { int offset = 0, unread = numBits; // Read full bytes. if(unread >= 8) { do { out[offset++] = **src, (*src)++; } while((unread -= 8) >= 8); } if(unread != 0) { // Read remaining bits. uint8_t fb = 8 - unread; if((*cb) == 0) (*cb) = 8; do { (*cb)--; out[offset] <<= 1; out[offset] |= ((**src >> (*cb)) & 0x01); } while(--unread > 0); out[offset] <<= fb; if((*cb) == 0) (*src)++; } } } dd_bool M_RunTrigger(trigger_t *trigger, timespan_t advanceTime) { // Either use the trigger's duration, or fall back to the default. timespan_t duration = (trigger->duration? trigger->duration : 1.0f/35); trigger->accum += advanceTime; if(trigger->accum >= duration) { trigger->accum -= duration; return true; } // It wasn't triggered. return false; } dd_bool M_CheckTrigger(const trigger_t *trigger, timespan_t advanceTime) { // Either use the trigger's duration, or fall back to the default. timespan_t duration = (trigger->duration? trigger->duration : 1.0f/35); return (trigger->accum + advanceTime>= duration); } doomsday-stable-1.15.7/doomsday/client/src/api_uri.cpp0000664000175000017500000001642012641367670022217 0ustar jaakkojaakko/** @file api_uri.cpp Universal Resource Identifier (public C wrapper). * @ingroup base * * @authors Copyright © 2010-2013 Daniel Swanson * @authors Copyright © 2010-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #define DENG_NO_API_MACROS_URI // functions defined here #include "api_uri.h" #include #include #include #include #define TOINTERNAL(inst) reinterpret_cast(inst) #define TOINTERNAL_CONST(inst) reinterpret_cast(inst) #define SELF(inst) \ DENG2_ASSERT(inst); \ de::Uri* self = TOINTERNAL(inst) #define SELF_CONST(inst) \ DENG2_ASSERT(inst); \ de::Uri const* self = TOINTERNAL_CONST(inst) static void writeUri(const Uri* uri, Writer* writer, int omitComponents = 0) { SELF_CONST(uri); if(omitComponents & UCF_SCHEME) { ddstring_t emptyString; Str_InitStatic(&emptyString, ""); Str_Write(&emptyString, writer); } else { Str_Write(DualString(self->scheme()).toStrUtf8(), writer); } Str_Write(DualString(self->path()).toStrUtf8(), writer); } #undef Uri_Clear Uri* Uri_Clear(Uri* uri) { SELF(uri); return reinterpret_cast(&self->clear()); } #undef Uri_SetScheme Uri* Uri_SetScheme(Uri* uri, char const* scheme) { SELF(uri); return reinterpret_cast(&self->setScheme(scheme)); } #undef Uri_SetPath Uri* Uri_SetPath(Uri* uri, char const* path) { SELF(uri); return reinterpret_cast(&self->setPath(path)); } static void readUri(Uri* uri, Reader* reader, de::String defaultScheme = "") { Uri_Clear(uri); ddstring_t scheme; Str_InitStd(&scheme); Str_Read(&scheme, reader); ddstring_t path; Str_InitStd(&path); Str_Read(&path, reader); if(Str_IsEmpty(&scheme) && !defaultScheme.isEmpty()) { Str_Set(&scheme, defaultScheme.toUtf8().constData()); } Uri_SetScheme(uri, Str_Text(&scheme)); Uri_SetPath (uri, Str_Text(&path )); } #undef Uri_NewWithPath3 Uri* Uri_NewWithPath3(char const *defaultScheme, char const *path) { de::Uri *uri = new de::Uri(defaultScheme); uri->setUri(path, RC_NULL); return reinterpret_cast(uri); } #undef Uri_NewWithPath2 Uri* Uri_NewWithPath2(char const* path, resourceclassid_t defaultResourceClass) { return reinterpret_cast( new de::Uri(path, defaultResourceClass) ); } #undef Uri_NewWithPath Uri* Uri_NewWithPath(char const* path) { return reinterpret_cast( new de::Uri(path) ); } #undef Uri_New Uri* Uri_New(void) { return reinterpret_cast( new de::Uri() ); } #undef Uri_Dup Uri* Uri_Dup(Uri const* other) { DENG_ASSERT(other); return reinterpret_cast( new de::Uri(*(TOINTERNAL_CONST(other))) ); } #undef Uri_FromReader Uri* Uri_FromReader(Reader* reader) { DENG_ASSERT(reader); de::Uri* self = new de::Uri; Uri* uri = reinterpret_cast(self); readUri(uri, reader); return uri; } #undef Uri_Delete void Uri_Delete(Uri* uri) { if(uri) { SELF(uri); delete self; } } #undef Uri_Copy Uri* Uri_Copy(Uri* uri, Uri const* other) { SELF(uri); DENG_ASSERT(other); *self = *(TOINTERNAL_CONST(other)); return reinterpret_cast(self); } #undef Uri_Equality dd_bool Uri_Equality(Uri const* uri, Uri const* other) { SELF_CONST(uri); DENG_ASSERT(other); return *self == (*(TOINTERNAL_CONST(other))); } #undef Uri_IsEmpty dd_bool Uri_IsEmpty(Uri const* uri) { SELF_CONST(uri); return self->isEmpty(); } #undef Uri_Resolved AutoStr* Uri_Resolved(Uri const* uri) { SELF_CONST(uri); try { return AutoStr_FromTextStd(self->resolved().toUtf8().constData()); } catch(de::Uri::ResolveError const& er) { LOG_RES_WARNING(er.asText()); } return AutoStr_NewStd(); } #undef Uri_Scheme const Str* Uri_Scheme(Uri const* uri) { SELF_CONST(uri); return self->schemeStr(); } #undef Uri_Path const Str* Uri_Path(Uri const* uri) { SELF_CONST(uri); return self->pathStr(); } #undef Uri_SetUri2 Uri* Uri_SetUri2(Uri* uri, char const* path, resourceclassid_t defaultResourceClass) { SELF(uri); return reinterpret_cast(&self->setUri(path, defaultResourceClass)); } #undef Uri_SetUri Uri* Uri_SetUri(Uri* uri, char const* path) { SELF(uri); return reinterpret_cast(&self->setUri(path)); } #undef Uri_SetUriStr Uri* Uri_SetUriStr(Uri* uri, ddstring_t const* path) { SELF(uri); return reinterpret_cast(&self->setUri(Str_Text(path))); } static de::Uri::ComposeAsTextFlags translateFlags(int flags) { de::Uri::ComposeAsTextFlags catf; if(flags & UCTF_OMITSCHEME) catf |= de::Uri::OmitScheme; if(flags & UCTF_OMITPATH) catf |= de::Uri::OmitPath; if(flags & UCTF_DECODEPATH) catf |= de::Uri::DecodePath; return catf; } #undef Uri_Compose2 AutoStr* Uri_Compose2(Uri const* uri, int flags) { SELF_CONST(uri); return AutoStr_FromTextStd(self->compose(translateFlags(flags)).toUtf8().constData()); } #undef Uri_Compose AutoStr* Uri_Compose(Uri const* uri) { SELF_CONST(uri); return AutoStr_FromTextStd(self->compose().toUtf8().constData()); } #undef Uri_ToString AutoStr* Uri_ToString(Uri const* uri) { SELF_CONST(uri); return AutoStr_FromTextStd(self->asText().toUtf8().constData()); } #undef Uri_Write2 void Uri_Write2(Uri const* uri, struct writer_s* writer, int omitComponents) { DENG_ASSERT(uri); DENG_ASSERT(writer); writeUri(uri, writer, omitComponents); } #undef Uri_Write void Uri_Write(Uri const* uri, struct writer_s* writer) { DENG_ASSERT(uri); DENG_ASSERT(writer); writeUri(uri, writer); } #undef Uri_Read Uri* Uri_Read(Uri* uri, struct reader_s* reader) { DENG_ASSERT(uri); DENG_ASSERT(reader); readUri(uri, reader); return uri; } #undef Uri_ReadWithDefaultScheme void Uri_ReadWithDefaultScheme(Uri* uri, struct reader_s* reader, char const* defaultScheme) { DENG_ASSERT(uri); DENG_ASSERT(reader); readUri(uri, reader, defaultScheme); } DENG_DECLARE_API(Uri) = { { DE_API_URI }, Uri_New, Uri_NewWithPath3, Uri_NewWithPath2, Uri_NewWithPath, Uri_Dup, Uri_FromReader, Uri_Delete, Uri_IsEmpty, Uri_Clear, Uri_Copy, Uri_Resolved, Uri_Scheme, Uri_Path, Uri_SetScheme, Uri_SetPath, Uri_SetUri2, Uri_SetUri, Uri_SetUriStr, Uri_Compose2, Uri_Compose, Uri_ToString, Uri_Equality, Uri_Write2, Uri_Write, Uri_Read, Uri_ReadWithDefaultScheme }; doomsday-stable-1.15.7/doomsday/client/src/r_util.cpp0000664000175000017500000002102612641367670022063 0ustar jaakkojaakko/**\file r_util.cpp * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * Refresh Utility Routines. */ #include #include "de_base.h" #include "de_console.h" #include "de_play.h" #include "de_misc.h" #ifdef __CLIENT__ # include "render/viewports.h" #endif #include "r_util.h" using namespace de; float R_MovementYaw(float const mom[]) { // Multiply by 100 to get some artificial accuracy in bamsAtan2. return BANG2DEG(bamsAtan2(-100 * mom[MY], 100 * mom[MX])); } float R_MovementXYYaw(float momx, float momy) { float mom[2] = { momx, momy }; return R_MovementYaw(mom); } float R_MovementPitch(float const mom[]) { return BANG2DEG(bamsAtan2 (100 * mom[MZ], 100 * V2f_Length(mom))); } float R_MovementXYZPitch(float momx, float momy, float momz) { float mom[3] = { momx, momy, momz }; return R_MovementPitch(mom); } #ifdef __CLIENT__ angle_t R_ViewPointToAngle(Vector2d point) { viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); point -= Vector2d(viewData->current.origin); return M_PointXYToAngle(point.x, point.y); } coord_t R_ViewPointDistance(coord_t x, coord_t y) { Vector3d const &viewOrigin = R_ViewData(viewPlayer - ddPlayers)->current.origin; coord_t viewOriginv1[2] = { viewOrigin.x, viewOrigin.y }; coord_t pointv1[2] = { x, y }; return M_PointDistance(viewOriginv1, pointv1); } #endif // __CLIENT__ Vector3d R_ClosestPointOnPlane(Vector3f const &planeNormal_, Vector3d const &planePoint_, Vector3d const &origin_) { vec3f_t planeNormal; V3f_Set(planeNormal, planeNormal_.x, planeNormal_.y, planeNormal_.z); vec3d_t planePoint; V3d_Set(planePoint, planePoint_.x, planePoint_.y, planePoint_.z); vec3d_t origin; V3d_Set(origin, origin_.x, origin_.y, origin_.z); vec3d_t point; V3d_ClosestPointOnPlanef(point, planeNormal, planePoint, origin); return point; } #ifdef __CLIENT__ void R_ProjectViewRelativeLine2D(coord_t const center[2], dd_bool alignToViewPlane, coord_t width, coord_t offset, coord_t start[2], coord_t end[2]) { viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); float sinrv, cosrv; if(alignToViewPlane) { // Should be fully aligned to view plane. sinrv = -viewData->viewCos; cosrv = viewData->viewSin; } else { // Transform the origin point. coord_t trX = center[VX] - viewData->current.origin.x; coord_t trY = center[VY] - viewData->current.origin.y; float thangle = BANG2RAD(bamsAtan2(trY * 10, trX * 10)) - float(de::PI) / 2; sinrv = sin(thangle); cosrv = cos(thangle); } start[VX] = center[VX]; start[VY] = center[VY]; start[VX] -= cosrv * ((width / 2) + offset); start[VY] -= sinrv * ((width / 2) + offset); end[VX] = start[VX] + cosrv * width; end[VY] = start[VY] + sinrv * width; } void R_ProjectViewRelativeLine2D(Vector2d const center, bool alignToViewPlane, coord_t width, coord_t offset, Vector2d &start, Vector2d &end) { viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); float sinrv, cosrv; if(alignToViewPlane) { // Should be fully aligned to view plane. sinrv = -viewData->viewCos; cosrv = viewData->viewSin; } else { // Transform the origin point. coord_t trX = center[VX] - viewData->current.origin.x; coord_t trY = center[VY] - viewData->current.origin.y; float thangle = BANG2RAD(bamsAtan2(trY * 10, trX * 10)) - float(de::PI) / 2; sinrv = sin(thangle); cosrv = cos(thangle); } start = center - Vector2d(cosrv * ((width / 2) + offset), sinrv * ((width / 2) + offset)); end = start + Vector2d(cosrv * width, sinrv * width); } void R_AmplifyColor(de::Vector3f &rgb) { float max = 0; for(int i = 0; i < 3; ++i) { if(rgb[i] > max) max = rgb[i]; } if(!max || max == 1) return; for(int i = 0; i < 3; ++i) { rgb[i] = rgb[i] / max; } } #endif // __CLIENT__ void R_ScaleAmbientRGB(float *out, float const *in, float mul) { mul = de::clamp(0.f, mul, 1.f); for(int i = 0; i < 3; ++i) { float val = in[i] * mul; if(out[i] < val) out[i] = val; } } bool R_GenerateTexCoords(Vector2f &s, Vector2f &t, Vector3d const &point, float xScale, float yScale, Vector3d const &v1, Vector3d const &v2, Matrix3f const &tangentMatrix) { Vector3d const v1ToPoint = v1 - point; s[0] = v1ToPoint.dot(tangentMatrix.column(0)/*tangent*/) * xScale + .5f; t[0] = v1ToPoint.dot(tangentMatrix.column(1)/*bitangent*/) * yScale + .5f; // Is the origin point visible? if(s[0] >= 1 || t[0] >= 1) return false; // Right on the X axis or below on the Y axis. Vector3d const v2ToPoint = v2 - point; s[1] = v2ToPoint.dot(tangentMatrix.column(0)) * xScale + .5f; t[1] = v2ToPoint.dot(tangentMatrix.column(1)) * yScale + .5f; // Is the end point visible? if(s[1] <= 0 || t[1] <= 0) return false; // Left on the X axis or above on the Y axis. return true; } char const *R_NameForBlendMode(blendmode_t mode) { static char const *names[1 + NUM_BLENDMODES] = { /* invalid */ "(invalid)", /* BM_ZEROALPHA */ "zero_alpha", /* BM_NORMAL */ "normal", /* BM_ADD */ "add", /* BM_DARK */ "dark", /* BM_SUBTRACT */ "subtract", /* BM_REVERSE_SUBTRACT */ "reverse_subtract", /* BM_MUL */ "mul", /* BM_INVERSE */ "inverse", /* BM_INVERSE_MUL */ "inverse_mul", /* BM_ALPHA_SUBTRACT */ "alpha_subtract" }; if(!VALID_BLENDMODE(mode)) return names[0]; return names[2 + int(mode)]; } #undef R_ChooseAlignModeAndScaleFactor DENG_EXTERN_C dd_bool R_ChooseAlignModeAndScaleFactor(float *scale, int width, int height, int availWidth, int availHeight, scalemode_t scaleMode) { if(SCALEMODE_STRETCH == scaleMode) { if(NULL != scale) *scale = 1; return true; } else { float const availRatio = (float)availWidth / availHeight; float const origRatio = (float)width / height; float sWidth, sHeight; // Scaled dimensions. if(availWidth >= availHeight) { sWidth = availWidth; sHeight = sWidth / availRatio; } else { sHeight = availHeight; sWidth = sHeight * availRatio; } if(origRatio > availRatio) { if(NULL != scale) *scale = sWidth / width; return false; } else { if(NULL != scale) *scale = sHeight / height; return true; } } } #undef R_ChooseScaleMode2 DENG_EXTERN_C scalemode_t R_ChooseScaleMode2(int width, int height, int availWidth, int availHeight, scalemode_t overrideMode, float stretchEpsilon) { float const availRatio = (float)availWidth / availHeight; float const origRatio = (float)width / height; // Considered identical? if(INRANGE_OF(availRatio, origRatio, .001f)) return SCALEMODE_STRETCH; if(SCALEMODE_STRETCH == overrideMode || SCALEMODE_NO_STRETCH == overrideMode) return overrideMode; // Within tolerable stretch range? return INRANGE_OF(availRatio, origRatio, stretchEpsilon)? SCALEMODE_STRETCH : SCALEMODE_NO_STRETCH; } #undef R_ChooseScaleMode DENG_EXTERN_C scalemode_t R_ChooseScaleMode(int width, int height, int availWidth, int availHeight, scalemode_t overrideMode) { return R_ChooseScaleMode2(availWidth, availHeight, width, height, overrideMode, DEFAULT_SCALEMODE_STRETCH_EPSILON); } doomsday-stable-1.15.7/doomsday/client/src/m_decomp64.cpp0000664000175000017500000001471312641367670022527 0ustar jaakkojaakko/** @file * * @authors Copyright © 2009-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * m_decomp64.c: Decompression algorithm. * * Used with various lumps of DOOM64 data. */ // HEADER FILES ------------------------------------------------------------ #include "de_platform.h" // MACROS ------------------------------------------------------------------ // TYPES ------------------------------------------------------------------- // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- // PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- // EXTERNAL DATA DECLARATIONS ---------------------------------------------- // PUBLIC DATA DEFINITIONS ------------------------------------------------- // PRIVATE DATA DEFINITIONS ------------------------------------------------ static short mapA[629], mapB[629]; static short tableA[1258], tableB[1258]; static const byte* srcPos; // CODE -------------------------------------------------------------------- static void cycleTable(int a, int b) { short n; for(;;) { if(a == mapA[tableA[b]]) n = mapB[tableA[b]]; else n = mapA[tableA[b]]; tableB[tableA[a]] = tableB[n] + tableB[a]; if(tableA[a] == 1) break; a = b = tableA[a]; } } static short rotateMap(int a, int b, int c) { if(a == mapA[tableA[a]]) { mapB[tableA[a]] = c; } else { mapA[tableA[a]] = c; } if(c == mapA[a]) { mapA[a] = b; return mapB[a]; } else { mapB[a] = b; return mapA[a]; } } /** * DOOM64 data decompression algorithm. * * \todo Needs further analysis and documentation. * Get rid of the fixed-size working buffer used with byte sequences. * Clean up * * @param dst Output buffer. Must be large enough! * @param src Src buffer (the compressed data). */ void M_Decompress64(byte* dst, const byte* src) { #define BUFF_SIZE (21903) // ith = ((2 / 3) * (4 ^ n - 1)) * 8 static const int strides[] = {0, 16, 80, 336, 1360, 5456}; int i, val, lastOut, curByte; byte buff[BUFF_SIZE], curBit; byte* dstPos; // Initialize LUTs, todo: precalculate where possible. for(i = 0; i < 1258; ++i) { tableB[i] = 1; } for(i = 0; i < 1258; ++i) { tableA[i] = ((i + 2) / 2) - 1; } mapB[0] = 0; for(i = 0; i < 628; ++i) { mapB[1 + i] = (i * 2) + 3; } for(i = 0; i < 629; ++i) { mapA[i] = i * 2; } srcPos = src; dstPos = dst; curByte = 0; curBit = 0; // Begin decompression. lastOut = 0; do { int index = 1; while(index < 629) { if(curBit == 0) curByte = *srcPos++; if(curByte & 0x80) index = mapB[index]; else index = mapA[index]; curBit = (curBit == 0? 7 : curBit - 1); curByte <<= 1; } tableB[index]++; if(tableA[index] != 1) { cycleTable(index, index); if(tableB[1] == 2000) { int i; for(i = 0; i < 1258; ++i) { tableB[i] >>= 1; } } } { int c; c = index; while(tableA[c] != 1) { int b, a; a = tableA[c]; if(a == mapA[tableA[a]]) b = mapB[tableA[a]]; else b = mapA[tableA[a]]; if(tableB[b] < tableB[c]) { int result; result = rotateMap(a, b, c); tableA[b] = a; tableA[c] = tableA[a]; cycleTable(b, result); c = tableA[b]; } else { c = tableA[c]; } } } val = index - 629; if(val != 256) { if(val < 256) { byte out = (val & 0xff); *dstPos++ = out; buff[lastOut++] = out; } else { int i, div, to, from, num, result; div = (val - 257) / 62; result = 0; { int shift = 1; for(i = 0; i < div * 2 + 4; ++i) { if(curBit == 0) curByte = *srcPos++; if((curByte) & 0x80) result |= shift; curBit = (curBit == 0? 7 : curBit - 1); curByte <<= 1; shift <<= 1; } } num = val - 254 - (div * 62); from = lastOut - result - strides[div] - num; if(from < 0) from += BUFF_SIZE; to = lastOut; for(i = 0; i < num; ++i) { byte out = buff[from]; *dstPos++ = out; buff[to] = out; if(++from == BUFF_SIZE) from = 0; if(++to == BUFF_SIZE) to = 0; } lastOut += num; } if(lastOut >= BUFF_SIZE) lastOut -= BUFF_SIZE; } } while(val != 256); #undef BUFF_SIZE } doomsday-stable-1.15.7/doomsday/client/src/updater/0000775000175000017500000000000012641367670021524 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/updater/updateavailabledialog.cpp0000664000175000017500000001656512641367670026550 0ustar jaakkojaakko/** @file updateavailabledialog.cpp Dialog for notifying the user about available updates. * @ingroup updater * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "updater/updateavailabledialog.h" #include "updater/updatersettings.h" #include "updater/updatersettingsdialog.h" #include "clientapp.h" #include "versioninfo.h" #include #include #include #include #include #include #include using namespace de; static TimeDelta const SHOW_ANIM_SPAN = 0.3; DENG_GUI_PIMPL(UpdateAvailableDialog), DENG2_OBSERVES(ToggleWidget, Toggle) { ProgressWidget *checking; ToggleWidget *autoCheck; //ButtonWidget *settings; VersionInfo latestVersion; String changeLog; Instance(Public *d) : Base(*d) { initForChecking(); } Instance(Public *d, VersionInfo const &latest) : Base(*d) { initForResult(latest); } void initForChecking(void) { init(); showProgress(true, 0); } void initForResult(VersionInfo const &latest) { init(); updateResult(latest, 0); } void showProgress(bool show, TimeDelta span) { checking->setOpacity(show? 1 : 0, span); self.area().setOpacity(show? 0 : 1, span); if(show) { // Set up a cancel button. self.buttons().clear() << new DialogButtonItem(DialogWidget::Reject); } } void init() { checking = new ProgressWidget; checking->setText(tr("Checking for Updates...")); // The checking indicator is overlaid on the normal content. checking->rule().setRect(self.rule()); self.add(checking); autoCheck = new ToggleWidget; self.area().add(autoCheck); autoCheck->setAlignment(ui::AlignLeft); autoCheck->setText(tr("Check for updates automatically")); autoCheck->audienceForToggle() += this; // Include the toggle in the layout. self.updateLayout(); } void updateResult(VersionInfo const &latest, TimeDelta showSpan) { showProgress(false, showSpan); latestVersion = latest; VersionInfo currentVersion; String channel = (UpdaterSettings().channel() == UpdaterSettings::Stable? "stable" : "unstable"); String builtInType = (String(DOOMSDAY_RELEASE_TYPE) == "Stable"? "stable" : "unstable"); bool askUpgrade = false; bool askDowngrade = false; if(latestVersion > currentVersion) { askUpgrade = true; self.title().setText(tr("Update Available")); self.message().setText(tr("There is an update available. The latest %1 release is %2, while you are running %3.") .arg(channel) .arg(_E(b) + latestVersion.asText() + _E(.)) .arg(currentVersion.asText())); } else if(channel == builtInType) // same release type { self.title().setText(tr("Up to Date")); self.message().setText(tr("The installed %1 is the latest available %2 build.") .arg(currentVersion.asText()) .arg(_E(b) + channel + _E(.))); } else if(latestVersion < currentVersion) { askDowngrade = true; self.title().setText(tr("Up to Date")); self.message().setText(tr("The installed %1 is newer than the latest available %2 build.") .arg(currentVersion.asText()) .arg(_E(b) + channel + _E(.))); } autoCheck->setInactive(UpdaterSettings().onlyCheckManually()); self.buttons().clear(); if(askDowngrade) { self.buttons() << new DialogButtonItem(DialogWidget::Accept, tr("Downgrade to Older")) << new DialogButtonItem(DialogWidget::Reject | DialogWidget::Default, tr("Close")); } else if(askUpgrade) { self.buttons() << new DialogButtonItem(DialogWidget::Accept | DialogWidget::Default, tr("Upgrade")) << new DialogButtonItem(DialogWidget::Reject, tr("Not Now")); } else { self.buttons() << new DialogButtonItem(DialogWidget::Accept, tr("Reinstall")) << new DialogButtonItem(DialogWidget::Reject | DialogWidget::Default, tr("Close")); } self.buttons() << new DialogButtonItem(DialogWidget::Action | DialogWidget::Id1, style().images().image("gear"), new SignalAction(thisPublic, SLOT(editSettings()))); if(askUpgrade) { self.buttons() << new DialogButtonItem(DialogWidget::Action, tr("What's New?"), new SignalAction(thisPublic, SLOT(showWhatsNew()))); } } void toggleStateChanged(ToggleWidget &) { bool set = autoCheck->isInactive(); UpdaterSettings().setOnlyCheckManually(set); LOG_DEBUG("Never check for updates: %b") << set; } }; UpdateAvailableDialog::UpdateAvailableDialog() : MessageDialog("updateavailable"), d(new Instance(this)) {} UpdateAvailableDialog::UpdateAvailableDialog(VersionInfo const &latestVersion, String changeLogUri) : MessageDialog("updateavailable"), d(new Instance(this, latestVersion)) { d->changeLog = changeLogUri; } void UpdateAvailableDialog::showResult(VersionInfo const &latestVersion, String changeLogUri) { d->changeLog = changeLogUri; d->updateResult(latestVersion, SHOW_ANIM_SPAN); } void UpdateAvailableDialog::showWhatsNew() { ClientApp::app().openInBrowser(QUrl(d->changeLog)); } void UpdateAvailableDialog::editSettings() { UpdaterSettingsDialog *st = new UpdaterSettingsDialog; st->setAnchorAndOpeningDirection(buttonWidget(DialogWidget::Id1)->rule(), ui::Up); st->setDeleteAfterDismissed(true); if(st->exec(root())) { // The Gear button will soon be deleted, so we'll need to detach from it. st->detachAnchor(); if(st->settingsHaveChanged()) { d->autoCheck->setInactive(UpdaterSettings().onlyCheckManually()); d->showProgress(true, SHOW_ANIM_SPAN); emit checkAgain(); } } } doomsday-stable-1.15.7/doomsday/client/src/updater/processcheckdialog.cpp0000664000175000017500000000475412641367670026076 0ustar jaakkojaakko/** @file processcheckdialog.cpp Dialog for checking running processes on Windows. * @ingroup updater * * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "updater/processcheckdialog.h" #include "ui/clientwindow.h" #include #include #ifdef WIN32 using namespace de; static bool isProcessRunning(char const *name) { QProcess wmic; wmic.start("wmic.exe", QStringList() << "PROCESS" << "get" << "Caption"); if(!wmic.waitForStarted()) return false; if(!wmic.waitForFinished()) return false; QByteArray result = wmic.readAll(); foreach(QString p, QString(result).split("\n", QString::SkipEmptyParts)) { if(!p.trimmed().compare(QLatin1String(name), Qt::CaseInsensitive)) return true; } return false; } dd_bool Updater_AskToStopProcess(char const *processName, char const *message) { while(isProcessRunning(processName)) { MessageDialog *msg = new MessageDialog; msg->setDeleteAfterDismissed(true); msg->title().setText(QObject::tr("Files In Use")); msg->message().setText(QString(message) + "\n\n" _E(2) + QObject::tr("There is a running process called %1.") .arg(_E(b) + QString(processName) + _E(.))); msg->buttons() << new DialogButtonItem(DialogWidget::Accept | DialogWidget::Default, QObject::tr("Retry")) << new DialogButtonItem(DialogWidget::Reject, QObject::tr("Ignore")); // Show a notification dialog. if(!msg->exec(ClientWindow::main().root())) { return !isProcessRunning(processName); } } return true; } #endif // WIN32 doomsday-stable-1.15.7/doomsday/client/src/updater/updatersettings.cpp0000664000175000017500000001332512641367670025461 0ustar jaakkojaakko/** @file updatersettings.cpp Persistent settings for automatic updates. * @ingroup updater * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "updater/updatersettings.h" #include "versioninfo.h" #include #include #include #include #include #include #include #include using namespace de; #define VAR_FREQUENCY "frequency" #define VAR_CHANNEL "channel" #define VAR_LAST_CHECKED "lastChecked" #define VAR_ONLY_MANUAL "onlyManually" #define VAR_DELETE "delete" #define VAR_DOWNLOAD_PATH "downloadPath" #define VAR_DELETE_PATH "deleteAtStartup" #define VAR_AUTO_DOWNLOAD "autoDownload" #define SUBREC_NAME "updater" #define CONF(name) SUBREC_NAME "." name #define SYMBOL_DEFAULT_DOWNLOAD "${DEFAULT}" UpdaterSettings::UpdaterSettings() {} UpdaterSettings::Frequency UpdaterSettings::frequency() const { return Frequency(App::config().geti(CONF(VAR_FREQUENCY))); } UpdaterSettings::Channel UpdaterSettings::channel() const { return Channel(App::config().geti(CONF(VAR_CHANNEL))); } de::Time UpdaterSettings::lastCheckTime() const { // Note that the variable has only AllowTime as the mode. return App::config().getAs(CONF(VAR_LAST_CHECKED)).time(); } bool UpdaterSettings::onlyCheckManually() const { return App::config().getb(CONF(VAR_ONLY_MANUAL)); } bool UpdaterSettings::autoDownload() const { return App::config().getb(CONF(VAR_AUTO_DOWNLOAD)); } bool UpdaterSettings::deleteAfterUpdate() const { return App::config().getb(CONF(VAR_DELETE)); } de::NativePath UpdaterSettings::pathToDeleteAtStartup() const { de::NativePath p = App::config().gets(CONF(VAR_DELETE_PATH)); de::String ext = p.toString().fileNameExtension(); if(p.fileName().startsWith("doomsday") && (ext == ".exe" || ext == ".deb" || ext == ".dmg")) { return p; } // Doesn't look valid. return ""; } bool UpdaterSettings::isDefaultDownloadPath() const { return downloadPath() == defaultDownloadPath(); } de::NativePath UpdaterSettings::downloadPath() const { de::NativePath dir = App::config().gets(CONF(VAR_DOWNLOAD_PATH)); if(dir.toString() == SYMBOL_DEFAULT_DOWNLOAD) { dir = defaultDownloadPath(); } return dir; } void UpdaterSettings::setDownloadPath(de::NativePath downloadPath) { if(downloadPath == defaultDownloadPath()) { downloadPath = SYMBOL_DEFAULT_DOWNLOAD; } App::config().set(CONF(VAR_DOWNLOAD_PATH), downloadPath.toString()); } void UpdaterSettings::setFrequency(UpdaterSettings::Frequency freq) { App::config().set(CONF(VAR_FREQUENCY), dint(freq)); } void UpdaterSettings::setChannel(UpdaterSettings::Channel channel) { App::config().set(CONF(VAR_CHANNEL), dint(channel)); } void UpdaterSettings::setLastCheckTime(de::Time const &time) { App::config()[CONF(VAR_LAST_CHECKED)] = new TimeValue(time); } void UpdaterSettings::setOnlyCheckManually(bool onlyManually) { App::config().set(CONF(VAR_ONLY_MANUAL), onlyManually); } void UpdaterSettings::setAutoDownload(bool autoDl) { App::config().set(CONF(VAR_AUTO_DOWNLOAD), autoDl); } void UpdaterSettings::setDeleteAfterUpdate(bool deleteAfter) { App::config().set(CONF(VAR_DELETE), deleteAfter); } void UpdaterSettings::useDefaultDownloadPath() { setDownloadPath(defaultDownloadPath()); } void UpdaterSettings::setPathToDeleteAtStartup(de::NativePath deletePath) { App::config().set(CONF(VAR_DELETE_PATH), deletePath.toString()); } de::NativePath UpdaterSettings::defaultDownloadPath() { #ifdef DENG2_QT_5_0_OR_NEWER return QStandardPaths::writableLocation(QStandardPaths::CacheLocation); #else return QDesktopServices::storageLocation(QDesktopServices::CacheLocation); #endif } de::String UpdaterSettings::lastCheckAgo() const { de::Time when = lastCheckTime(); if(!when.isValid()) return ""; // Never checked. de::TimeDelta delta = when.since(); if(delta < 0.0) return ""; int t; if(delta < 60.0) { t = delta.asMilliSeconds() / 1000; return de::String(QObject::tr("%1 %2 ago")).arg(t). arg(t != 1? QObject::tr("seconds") : QObject::tr("second")); } t = delta.asMinutes(); if(t <= 60) { return de::String(QObject::tr("%1 %2 ago")).arg(t). arg(t != 1? QObject::tr("minutes") : QObject::tr("minute")); } t = delta.asHours(); if(t <= 24) { return de::String(QObject::tr("%1 %2 ago")).arg(t). arg(t != 1? QObject::tr("hours") : QObject::tr("hour")); } t = delta.asDays(); if(t <= 7) { return de::String(QObject::tr("%1 %2 ago")).arg(t). arg(t != 1? QObject::tr("days") : QObject::tr("day")); } return de::String("on " + when.asText(de::Time::FriendlyFormat)); } doomsday-stable-1.15.7/doomsday/client/src/updater/updater.cpp0000664000175000017500000005026112641367670023700 0ustar jaakkojaakko/** @file updater.cpp Automatic updater that works with dengine.net. * @ingroup updater * * When one of the updater dialogs is shown, the main window is automatically * switched to windowed mode. This is because the dialogs would be hidden * behind the main window or incorrectly located when the main window is in * fullscreen mode. It is also possible that the screen resolution is too low * to fit the shown dialogs. In the long term, the native dialogs should be * replaced with the engine's own (scriptable) UI widgets (once they are * available). * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include #include #include #include "de_platform.h" #ifdef WIN32 # undef open #endif #include #include "sys_system.h" #include "dd_version.h" #include "dd_def.h" #include "dd_types.h" #include "dd_main.h" #include "clientapp.h" #include "ui/nativeui.h" #include "ui/clientwindowsystem.h" #include "ui/clientwindow.h" #include "ui/widgets/taskbarwidget.h" #include "updater.h" #include "updater/downloaddialog.h" #include "updater/processcheckdialog.h" #include "updater/updateavailabledialog.h" #include "updater/updatersettings.h" #include "updater/updatersettingsdialog.h" #include "versioninfo.h" #include #include #include #include #include #include #include #include using namespace de; #ifdef MACOSX # define INSTALL_SCRIPT_NAME "deng-upgrade.scpt" #endif /// @todo The platform ID should come from the Builder. #if defined(WIN32) # define PLATFORM_ID "win-x86" #elif defined(MACOSX) # if defined(MACOS_10_7) || defined(MACOSX_NATIVESDK) # define PLATFORM_ID "mac10_8-x86_64" # elif defined(__64BIT__) # define PLATFORM_ID "mac10_6-x86-x86_64" # else # define PLATFORM_ID "mac10_4-x86-ppc" # endif #else # if defined(__64BIT__) # define PLATFORM_ID "linux-x86_64" # else # define PLATFORM_ID "linux-x86" # endif #endif static CommandLine* installerCommand; /** * Callback for atexit(). Create the installerCommand before calling this. */ static void runInstallerCommand(void) { DENG_ASSERT(installerCommand != 0); installerCommand->execute(); delete installerCommand; installerCommand = 0; } /** * Notification widget about the status of the Updater. */ class UpdaterStatusWidget : public ProgressWidget { public: UpdaterStatusWidget() { useMiniStyle(); setColor("text"); setShadowColor(""); // no shadow, please setSizePolicy(ui::Expand, ui::Expand); _icon = new LabelWidget; _icon->setImage(ClientApp::windowSystem().style().images().image("updater")); _icon->setOverrideImageSize(overrideImageSize()); _icon->rule().setRect(rule()); add(_icon); hideIcon(); // The notification has a hidden button that can be clicked. _clickable = new ButtonWidget; _clickable->setOpacity(0); // not drawn _clickable->rule().setRect(rule()); _clickable->setAction(new SignalAction(&ClientApp::updater(), SLOT(showCurrentDownload()))); add(_clickable); } void showIcon(DotPath const &path) { _icon->setImageColor(ClientApp::windowSystem().style().colors().colorf(path)); } void hideIcon() { _icon->setImageColor(Vector4f()); } private: LabelWidget *_icon; ButtonWidget *_clickable; }; DENG2_PIMPL(Updater) , DENG2_OBSERVES(App, StartupComplete) { QNetworkAccessManager *network = nullptr; DownloadDialog *download = nullptr; // not owned (in the widget tree, if exists) UniqueWidgetPtr status; UpdateAvailableDialog *availableDlg = nullptr; ///< If currently open (not owned). bool alwaysShowNotification; bool savingSuggested = false; VersionInfo latestVersion; QString latestPackageUri; QString latestPackageUri2; // fallback location QString latestLogUri; Instance(Public *i) : Base(i) { network = new QNetworkAccessManager(thisPublic); // Delete a package installed earlier? UpdaterSettings st; if(st.deleteAfterUpdate()) { de::String p = st.pathToDeleteAtStartup(); if(!p.isEmpty()) { QFile file(p); if(file.exists()) { LOG_NOTE("Deleting previously installed package: %s") << p; file.remove(); } } } st.setPathToDeleteAtStartup(""); } void setupUI() { status.reset(new UpdaterStatusWidget); } QString composeCheckUri() { UpdaterSettings st; QString uri = QString(DOOMSDAY_HOMEURL) + "/latestbuild?"; uri += QString("platform=") + PLATFORM_ID; uri += (st.channel() == UpdaterSettings::Stable? "&stable" : "&unstable"); uri += "&graph"; LOG_XVERBOSE("Check URI: ") << uri; return uri; } bool shouldCheckForUpdate() const { UpdaterSettings st; if(st.onlyCheckManually()) return false; float dayInterval = 30; switch(st.frequency()) { case UpdaterSettings::AtStartup: dayInterval = 0; break; case UpdaterSettings::Daily: dayInterval = 1; break; case UpdaterSettings::Biweekly: dayInterval = 5; break; case UpdaterSettings::Weekly: dayInterval = 7; break; default: break; } de::Time now; // Check always when the day interval has passed. Note that this // doesn't check the actual time interval since the last check, but the // difference in "calendar" days. if(st.lastCheckTime().asDate().daysTo(de::Date()) >= dayInterval) return true; if(st.frequency() == UpdaterSettings::Biweekly) { // Check on Tuesday and Saturday, as the builds are usually on // Monday and Friday. int weekday = now.asDateTime().date().dayOfWeek(); if(weekday == 2 || weekday == 6) return true; } // No need to check right now. return false; } void appStartupCompleted() { LOG_AS("Updater") LOG_DEBUG("App startup was completed"); if(shouldCheckForUpdate()) { queryLatestVersion(false); } } void showNotification(bool show) { ClientWindow::main().notifications().showOrHide(*status, show); } void showCheckingNotification() { status->setRange(Rangei(0, 1)); status->setProgress(0, 0); status->showIcon("text"); showNotification(true); } void showUpdateAvailableNotification() { showCheckingNotification(); status->showIcon("accent"); } void showDownloadNotification() { status->setMode(ProgressWidget::Indefinite); status->hideIcon(); showNotification(true); } void queryLatestVersion(bool notifyAlways) { showCheckingNotification(); UpdaterSettings().setLastCheckTime(de::Time()); alwaysShowNotification = notifyAlways; network->get(QNetworkRequest(composeCheckUri())); } void handleReply(QNetworkReply *reply) { reply->deleteLater(); // make sure it gets deleted DENG2_ASSERT_IN_MAIN_THREAD(); showNotification(false); if(reply->error() != QNetworkReply::NoError) { LOG_WARNING("Network request failed: %s") << reply->url().toString(); return; } QVariant result = de::parseJSON(QString::fromUtf8(reply->readAll())); if(!result.isValid()) return; QVariantMap map = result.toMap(); latestPackageUri = map["direct_download_uri"].toString(); latestLogUri = map["release_changeloguri"].toString(); // Check if a fallback location is specified for the download. if(map.contains("direct_download_fallback_uri")) { latestPackageUri2 = map["direct_download_fallback_uri"].toString(); } else { latestPackageUri2 = ""; } latestVersion = VersionInfo(map["version"].toString(), map["build_uniqueid"].toInt()); VersionInfo currentVersion; LOG_MSG(_E(b) "Received version information:\n" _E(.) " - installed version: " _E(>) "%s ") << currentVersion.asText(); LOG_MSG(" - latest version: " _E(>) "%s") << latestVersion.asText(); LOG_MSG(" - package: " _E(>) _E(i) "%s") << latestPackageUri; LOG_MSG(" - change log: " _E(>) _E(i) "%s") << latestLogUri; if(availableDlg) { // This was a recheck. availableDlg->showResult(latestVersion, latestLogUri); return; } bool const gotUpdate = latestVersion > currentVersion; // Is this newer than what we're running? if(gotUpdate) { LOG_NOTE("Found an update: " _E(b)) << latestVersion.asText(); if(!alwaysShowNotification) { if(UpdaterSettings().autoDownload()) { startDownload(); return; } // Show the notification so the user knows an update is // available. showUpdateAvailableNotification(); } } else { LOG_NOTE("You are running the latest available " _E(b) "%s" _E(.) " release") << (UpdaterSettings().channel() == UpdaterSettings::Stable? "stable" : "unstable"); } if(alwaysShowNotification) { showAvailableDialogAndPause(); } } void showAvailableDialogAndPause() { if(availableDlg) return; // Just one at a time. // Modal dialogs will interrupt gameplay. ClientWindow::main().taskBar().openAndPauseGame(); availableDlg = new UpdateAvailableDialog(latestVersion, latestLogUri); execAvailableDialog(); } void execAvailableDialog() { DENG2_ASSERT(availableDlg != 0); availableDlg->setDeleteAfterDismissed(true); QObject::connect(availableDlg, SIGNAL(checkAgain()), thisPublic, SLOT(recheck())); if(availableDlg->exec(ClientWindow::main().root())) { startDownload(); download->open(); } availableDlg = 0; } void startDownload() { DENG2_ASSERT(!download); // The notification provides access to the download dialog. showDownloadNotification(); LOG_MSG("Download and install update"); download = new DownloadDialog(latestPackageUri, latestPackageUri2); download->setAnchorAndOpeningDirection(status->rule(), ui::Down); QObject::connect(download, SIGNAL(closed()), thisPublic, SLOT(downloadDialogClosed())); QObject::connect(download, SIGNAL(downloadProgress(int)),thisPublic, SLOT(downloadProgressed(int))); QObject::connect(download, SIGNAL(downloadFailed(QString)), thisPublic, SLOT(downloadFailed(QString))); QObject::connect(download, SIGNAL(accepted(int)), thisPublic, SLOT(downloadCompleted(int))); ClientWindow::main().root().addOnTop(download); } /** * Starts the installation process using the provided distribution package. * The engine is first shut down gracefully (game has already been autosaved). * * @param distribPackagePath File path of the distribution package. */ void startInstall(de::String distribPackagePath) { #ifdef MACOSX de::String volName = "Doomsday Engine " + latestVersion.base(); #ifdef DENG2_QT_5_0_OR_NEWER QString scriptPath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); #else QString scriptPath = QDesktopServices::storageLocation(QDesktopServices::CacheLocation); #endif QDir::current().mkpath(scriptPath); // may not exist scriptPath = QDir(scriptPath).filePath(INSTALL_SCRIPT_NAME); QFile file(scriptPath); if(file.open(QFile::WriteOnly | QFile::Truncate)) { QTextStream out(&file); out << "tell application \"System Events\" to set visible of process \"Finder\" to false\n" "tell application \"Finder\"\n" " open POSIX file \"" << distribPackagePath << "\"\n" " -- Wait for it to get mounted\n" " repeat until name of every disk contains \"" << volName << "\"\n" " delay 1\n" " end repeat\n" " -- Start the installer\n" " open file \"" << volName << ":Doomsday.pkg\"\n" " -- Activate the Installer\n" " repeat until name of every process contains \"Installer\"\n" " delay 2\n" " end repeat\n" "end tell\n" "delay 1\n" "tell application \"Installer\" to activate\n" "tell application \"Finder\"\n" " -- Wait for it to finish\n" " repeat until name of every process does not contain \"Installer\"\n" " delay 1\n" " end repeat\n" " -- Unmount\n" " eject disk \"" << volName << "\"\n" "end tell\n"; file.close(); } else { qWarning() << "Could not write" << scriptPath; } // Register a shutdown action to execute the script and quit. installerCommand = new de::CommandLine; installerCommand->append("osascript"); installerCommand->append(scriptPath); atexit(runInstallerCommand); #elif defined(WIN32) /** * @todo It would be slightly neater to check all these processes at * the same time. */ Updater_AskToStopProcess("snowberry.exe", "Please quit the Doomsday Engine Frontend " "before starting the update. Windows cannot update " "files that are currently in use."); Updater_AskToStopProcess("doomsday-shell.exe", "Please quit all Doomsday Shell instances " "before starting the update. Windows cannot update " "files that are currently in use."); Updater_AskToStopProcess("doomsday-server.exe", "Please stop all Doomsday servers " "before starting the update. Windows cannot update " "files that are currently in use."); // The distribution package is an installer executable, we can just run it. installerCommand = new de::CommandLine; installerCommand->append(distribPackagePath); atexit(runInstallerCommand); #else // Open the package with the default handler. installerCommand = new de::CommandLine; installerCommand->append("xdg-open"); installerCommand->append(distribPackagePath); atexit(runInstallerCommand); #endif // If requested, delete the downloaded package afterwards. Currently // this occurs the next time when the engine is launched; on some // platforms it could be incorporated into the reinstall procedure. // (This will work better when there is no more separate frontend, as // the engine is restarted after the install.) UpdaterSettings st; if(st.deleteAfterUpdate()) { st.setPathToDeleteAtStartup(distribPackagePath); } Sys_Quit(); } }; Updater::Updater() : d(new Instance(this)) { connect(d->network, SIGNAL(finished(QNetworkReply *)), this, SLOT(gotReply(QNetworkReply *))); // Do a silent auto-update check when starting. App::app().audienceForStartupComplete() += d; } void Updater::setupUI() { d->setupUI(); } ProgressWidget &Updater::progress() { return *d->status; } void Updater::gotReply(QNetworkReply *reply) { d->handleReply(reply); } void Updater::downloadProgressed(int percentage) { d->status->setRange(Rangei(0, 100)); d->status->setProgress(percentage); } void Updater::downloadCompleted(int) { // Autosave the game. // Well, we can't do that yet so just remind the user about saving. if(App_GameLoaded() && !d->savingSuggested && gx.GetInteger(DD_GAME_RECOMMENDS_SAVING)) { d->savingSuggested = true; MessageDialog *msg = new MessageDialog; msg->setDeleteAfterDismissed(true); msg->title().setText(tr("Save Game?")); msg->message().setText(tr(_E(b) "Installing the update will discard unsaved progress in the game.\n\n" _E(.) "Doomsday will be shut down before the installation can start. " "The game is not saved automatically, so you will have to " "save the game before installing the update.")); msg->buttons() << new DialogButtonItem(DialogWidget::Accept | DialogWidget::Default, tr("I'll Save First")) << new DialogButtonItem(DialogWidget::Reject, tr("Discard Progress & Install")); if(msg->exec(ClientWindow::main().root())) { Con_Execute(CMDS_DDAY, "savegame", false, false); return; } } /// @todo Check the signature of the downloaded file. // Everything is ready to begin the installation! d->startInstall(d->download->downloadedFilePath()); // The download dialog can be dismissed now. d->download->guiDeleteLater(); d->download = 0; d->savingSuggested = false; } void Updater::downloadFailed(QString message) { LOG_NOTE("Update cancelled: ") << message; } void Updater::recheck() { d->queryLatestVersion(d->alwaysShowNotification); } void Updater::showSettings() { //d->showSettingsNonModal(); ClientWindow::main().taskBar().showUpdaterSettings(); } void Updater::showCurrentDownload() { if(d->download) { d->download->open(); } else { d->showNotification(false); d->showAvailableDialogAndPause(); } } void Updater::checkNow(CheckMode mode) { // Not if there is an ongoing download. if(d->download) { d->download->open(); return; } d->queryLatestVersion(mode == AlwaysShowResult); } void Updater::checkNowShowingProgress() { // Not if there is an ongoing download. if(d->download) return; d->availableDlg = new UpdateAvailableDialog; d->queryLatestVersion(true); d->execAvailableDialog(); } void Updater::printLastUpdated(void) { String ago = UpdaterSettings().lastCheckAgo(); if(ago.isEmpty()) { LOG_MSG("Never checked for updates"); } else { LOG_MSG("Latest update check was made %s") << ago; } } void Updater::downloadDialogClosed() { if(!d->download || d->download->isFailed()) { if(d->download) { d->download->setDeleteAfterDismissed(true); d->download = 0; } d->showNotification(false); } } doomsday-stable-1.15.7/doomsday/client/src/updater/updatersettingsdialog.cpp0000664000175000017500000002121412641367670026635 0ustar jaakkojaakko/** @file updatersettingsdialog.cpp Dialog for configuring automatic updates. * @ingroup updater * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "updater/updatersettingsdialog.h" #include "updater/updatersettings.h" #include "clientapp.h" #include #include #include #include #include #include #include using namespace de; static QString defaultLocationName() { #ifdef DENG2_QT_5_0_OR_NEWER QString name = QStandardPaths::displayName(QStandardPaths::CacheLocation); #else QString name = QDesktopServices::displayName(QDesktopServices::CacheLocation); #endif if(name.isEmpty()) { name = "Temporary Files"; } return name; } DENG2_PIMPL(UpdaterSettingsDialog), DENG2_OBSERVES(ToggleWidget, Toggle) { ToggleWidget *autoCheck; ChoiceWidget *freqs; LabelWidget *lastChecked; ChoiceWidget *channels; ChoiceWidget *paths; ToggleWidget *autoDown; ToggleWidget *deleteAfter; bool didApply = false; Instance(Public *i, Mode mode) : Base(i) { ScrollAreaWidget &area = self.area(); // Create the widgets. area.add(autoCheck = new ToggleWidget); area.add(freqs = new ChoiceWidget); area.add(lastChecked = new LabelWidget); area.add(channels = new ChoiceWidget); area.add(autoDown = new ToggleWidget); area.add(paths = new ChoiceWidget); area.add(deleteAfter = new ToggleWidget); // The updater Config is changed when the widget state is modified. QObject::connect(autoCheck, SIGNAL(stateChangedByUser(ToggleWidget::ToggleState)), thisPublic, SLOT(apply())); QObject::connect(freqs, SIGNAL(selectionChangedByUser(uint)), thisPublic, SLOT(apply())); QObject::connect(channels, SIGNAL(selectionChangedByUser(uint)), thisPublic, SLOT(apply())); QObject::connect(autoDown, SIGNAL(stateChangedByUser(ToggleWidget::ToggleState)), thisPublic, SLOT(apply())); QObject::connect(paths, SIGNAL(selectionChangedByUser(uint)), thisPublic, SLOT(apply())); QObject::connect(deleteAfter, SIGNAL(stateChangedByUser(ToggleWidget::ToggleState)), thisPublic, SLOT(apply())); LabelWidget *releaseLabel = new LabelWidget; area.add(releaseLabel); LabelWidget *pathLabel = new LabelWidget; area.add(pathLabel); autoCheck->setText(tr("Check for updates:")); freqs->items() << new ChoiceItem(tr("At startup"), UpdaterSettings::AtStartup) << new ChoiceItem(tr("Daily"), UpdaterSettings::Daily) << new ChoiceItem(tr("Biweekly"), UpdaterSettings::Biweekly) << new ChoiceItem(tr("Weekly"), UpdaterSettings::Weekly) << new ChoiceItem(tr("Monthly"), UpdaterSettings::Monthly); lastChecked->margins().setTop(""); lastChecked->setFont("separator.annotation"); lastChecked->setTextColor("altaccent"); releaseLabel->setText("Release type:"); channels->items() << new ChoiceItem(tr("Stable"), UpdaterSettings::Stable) << new ChoiceItem(tr("Unstable/Candidate"), UpdaterSettings::Unstable); pathLabel->setText(tr("Download location:")); paths->items() << new ChoiceItem(defaultLocationName(), UpdaterSettings::defaultDownloadPath().toString()); autoDown->setText(tr("Download automatically")); deleteAfter->setText(tr("Delete file after install")); fetch(); autoCheck->audienceForToggle() += this; // Place the widgets into a grid. GridLayout layout(area.contentRule().left(), area.contentRule().top()); layout.setGridSize(2, 0); layout.setColumnAlignment(0, ui::AlignRight); // Labels aligned to the right. layout << *autoCheck << *freqs << Const(0) << *lastChecked << *releaseLabel << *channels << Const(0) << *autoDown << Const(0) << *deleteAfter << *pathLabel << *paths; area.setContentSize(layout.width(), layout.height()); self.buttons() << new DialogButtonItem(DialogWidget::Default | DialogWidget::Accept, tr("Close")); if(mode == WithApplyAndCheckButton) { self.buttons() << new DialogButtonItem(DialogWidget::Action, tr("Check Now"), new SignalAction(thisPublic, SLOT(applyAndCheckNow()))); } } void fetch() { UpdaterSettings st; String ago = st.lastCheckAgo(); if(!ago.isEmpty()) { lastChecked->setText(tr("Last checked %1.").arg(st.lastCheckAgo())); } else { lastChecked->setText(tr("Never checked.")); } autoCheck->setActive(!st.onlyCheckManually()); freqs->enable(!st.onlyCheckManually()); freqs->setSelected(freqs->items().findData(st.frequency())); channels->setSelected(channels->items().findData(st.channel())); setDownloadPath(st.downloadPath()); autoDown->setActive(st.autoDownload()); deleteAfter->setActive(st.deleteAfterUpdate()); } void toggleStateChanged(ToggleWidget &toggle) { if(&toggle == autoCheck) { freqs->enable(autoCheck->isActive()); } } void apply() { UpdaterSettings st; st.setOnlyCheckManually(autoCheck->isInactive()); ui::Data::Pos sel = freqs->selected(); if(sel != ui::Data::InvalidPos) { st.setFrequency(UpdaterSettings::Frequency(freqs->items().at(sel).data().toInt())); } sel = channels->selected(); if(sel != ui::Data::InvalidPos) { st.setChannel(UpdaterSettings::Channel(channels->items().at(sel).data().toInt())); } //st.setDownloadPath(pathList->itemData(pathList->currentIndex()).toString()); st.setAutoDownload(autoDown->isActive()); st.setDeleteAfterUpdate(deleteAfter->isActive()); } void setDownloadPath(QString const &/*dir*/) { paths->setSelected(0); /* if(dir != UpdaterSettings::defaultDownloadPath()) { // Remove extra items. while(pathList->count() > 2) { pathList->removeItem(0); } pathList->insertItem(0, QDir(dir).dirName(), dir); pathList->setCurrentIndex(0); } else { pathList->setCurrentIndex(pathList->findData(dir)); }*/ } }; UpdaterSettingsDialog::UpdaterSettingsDialog(Mode mode, String const &name) : DialogWidget(name, WithHeading), d(new Instance(this, mode)) { heading().setText(tr("Updater Settings")); } bool UpdaterSettingsDialog::settingsHaveChanged() const { return d->didApply; } void UpdaterSettingsDialog::apply() { d->apply(); d->didApply = true; } void UpdaterSettingsDialog::applyAndCheckNow() { accept(); ClientApp::updater().checkNow(); } void UpdaterSettingsDialog::finish(int result) { if(result) { d->apply(); } DialogWidget::finish(result); } /* void UpdaterSettingsDialog::pathActivated(int index) { QString path = d->pathList->itemData(index).toString(); if(path.isEmpty()) { QString dir = QFileDialog::getExistingDirectory(this, tr("Download Folder"), QDir::homePath()); if(!dir.isEmpty()) { d->setDownloadPath(dir); } else { d->setDownloadPath(UpdaterSettings::defaultDownloadPath()); } } } */ doomsday-stable-1.15.7/doomsday/client/src/updater/downloaddialog.cpp0000664000175000017500000002401612641367670025222 0ustar jaakkojaakko/** @file downloaddialog.cpp Dialog that downloads a distribution package. * @ingroup updater * * @authors Copyright © 2012-2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include "de_platform.h" #include "updater/downloaddialog.h" #include "updater/updatersettings.h" #include "ui/widgets/taskbarwidget.h" #include "ui/clientwindow.h" #include "dd_version.h" #include "network/net_main.h" #include #include #include #include #include #include #include #ifdef WIN32 # undef open #endif using namespace de; static DownloadDialog *downloadInProgress; DENG2_PIMPL(DownloadDialog) { enum State { Connecting, MaybeRedirected, Downloading, Finished, Error }; State state; QNetworkAccessManager* network; ProgressWidget *progress; QUrl uri; QUrl uri2; NativePath savedFilePath; QNetworkReply *reply; String redirected; dint64 receivedBytes; dint64 totalBytes; String location; String errorMessage; Instance(Public *d, String downloadUri, String fallbackUri) : Base(d), state(Connecting), uri(downloadUri), uri2(fallbackUri), reply(0), receivedBytes(0), totalBytes(0) { ScrollAreaWidget &area = self.area(); progress = new ProgressWidget; area.add(progress); progress->setImageScale(toDevicePixels(.4f)); progress->setAlignment(ui::AlignLeft); progress->setSizePolicy(ui::Fixed, ui::Expand); progress->setRange(Rangei(0, 100)); progress->rule() .setLeftTop(area.contentRule().left(), area.contentRule().top()) .setInput(Rule::Width, self.style().rules().rule("dialog.download.width")); area.setContentSize(progress->rule().width(), progress->rule().height()); self.buttons() << new DialogButtonItem(DialogWidget::Reject, tr("Cancel Download"), new SignalAction(thisPublic, SLOT(cancel()))); updateLocation(uri); updateProgress(); network = new QNetworkAccessManager(thisPublic); QObject::connect(network, SIGNAL(finished(QNetworkReply *)), thisPublic, SLOT(finished(QNetworkReply *))); startDownload(); } void updateLocation(QUrl const &url) { location = url.host(); updateProgress(); } void startDownload() { state = Connecting; redirected.clear(); String path = uri.path(); QDir::current().mkpath(UpdaterSettings().downloadPath()); // may not exist savedFilePath = UpdaterSettings().downloadPath() / path.fileName(); QNetworkRequest request(uri); request.setRawHeader("User-Agent", Net_UserAgent().toLatin1()); reply = network->get(request); QObject::connect(reply, SIGNAL(metaDataChanged()), thisPublic, SLOT(replyMetaDataChanged())); QObject::connect(reply, SIGNAL(downloadProgress(qint64,qint64)), thisPublic, SLOT(progress(qint64,qint64))); LOG_NOTE("Downloading %s, saving as: %s") << uri.toString() << savedFilePath; // Global state "flag". downloadInProgress = thisPublic; } void updateProgress() { String fn = savedFilePath.fileName(); String msg; const double MB = 1.0e6; // MiB would be 2^20 switch(state) { default: msg = String(tr("Connecting to %1")).arg(_E(b) + location + _E(.)); break; case Downloading: msg = String(tr("Downloading %1 (%2 MB) from %3")) .arg(_E(b) + fn + _E(.)).arg(totalBytes / MB, 0, 'f', 1).arg(location); break; case Finished: msg = String(tr("Ready to install\n%1")).arg(_E(b) + fn + _E(.)); break; case Error: msg = String(tr("Failed to download:\n%1")).arg(_E(b) + errorMessage); break; } progress->setText(msg); } }; DownloadDialog::DownloadDialog(String downloadUri, String fallbackUri) : DialogWidget("download"), d(new Instance(this, downloadUri, fallbackUri)) {} DownloadDialog::~DownloadDialog() { downloadInProgress = 0; } String DownloadDialog::downloadedFilePath() const { if(!isReadyToInstall()) return ""; return d->savedFilePath; } bool DownloadDialog::isReadyToInstall() const { return d->state == Instance::Finished; } bool DownloadDialog::isFailed() const { return d->state == Instance::Error; } void DownloadDialog::finished(QNetworkReply *reply) { LOG_AS("Download"); reply->deleteLater(); d->reply = 0; if(reply->error() != QNetworkReply::NoError) { LOG_WARNING("Failed: ") << reply->errorString(); d->state = Instance::Error; d->errorMessage = reply->errorString(); d->updateProgress(); downloadInProgress = 0; return; } /// @todo If/when we include WebKit, this can be done more intelligently using QWebPage. -jk if(!d->redirected.isEmpty()) { LOG_NOTE("Redirected to: %s") << d->redirected; d->uri = QUrl(d->redirected); d->redirected.clear(); d->startDownload(); return; } if(d->state == Instance::MaybeRedirected) { // This does not look like a binary file... Let's see if we can parse the page. QString html = QString::fromUtf8(reply->readAll()); /// @todo Use a regular expression for parsing the redirection. int start = html.indexOf("uri2.isEmpty() && d->uri2 != d->uri) { d->uri = d->uri2; d->updateLocation(d->uri); d->startDownload(); return; } emit downloadFailed(d->uri.toString()); return; } start = html.indexOf("url=\"", start, Qt::CaseInsensitive); if(start < 0) { emit downloadFailed(d->uri.toString()); return; } start += 5; // skip: url=" QString equivRefresh = html.mid(start, html.indexOf("\"", start) - start); equivRefresh.replace("&", "&"); // This is what we should actually be downloading. d->uri = QUrl::fromEncoded(equivRefresh.toLatin1()); LOG_NOTE("Redirected to: %s") << d->uri.toString(); d->startDownload(); return; } // Save the received data. QFile file(d->savedFilePath); if(file.open(QFile::WriteOnly | QFile::Truncate)) { file.write(reply->readAll()); } else { LOG_WARNING("Failed to write to: %s") << d->savedFilePath; emit downloadFailed(d->uri.toString()); } buttons().clear() << new DialogButtonItem(DialogWidget::Reject, tr("Delete"), new SignalAction(this, SLOT(cancel()))) << new DialogButtonItem(DialogWidget::Accept | DialogWidget::Default, tr("Install Update")); d->state = Instance::Finished; d->progress->setRotationSpeed(0); d->updateProgress(); // Make sure the finished download is noticed by the user. showCompletedDownload(); LOG_DEBUG("Request finished"); } void DownloadDialog::cancel() { LOG_NOTE("Download cancelled due to user request"); d->state = Instance::Error; d->progress->setRotationSpeed(0); if(d->reply) { d->reply->abort(); buttons().clear() << new DialogButtonItem(DialogWidget::Reject, tr("Close")); } else { reject(); } } void DownloadDialog::progress(qint64 received, qint64 total) { LOG_AS("Download"); if(d->state == Instance::Downloading && total > 0) { d->totalBytes = total; d->receivedBytes = received; d->updateProgress(); dint64 percent = received * 100 / total; d->progress->setProgress(percent); emit downloadProgress(percent); } } void DownloadDialog::replyMetaDataChanged() { LOG_AS("Download"); String contentType = d->reply->header(QNetworkRequest::ContentTypeHeader).toString(); String redirection = d->reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString(); if(!redirection.isEmpty()) { d->redirected = redirection; } else if(contentType.startsWith("text/html")) { // Looks like a redirection page. d->state = Instance::MaybeRedirected; } else { LOG_DEBUG("Receiving content of type '%s'") << contentType; d->state = Instance::Downloading; } } bool DownloadDialog::isDownloadInProgress() { return downloadInProgress != 0; } DownloadDialog &DownloadDialog::currentDownload() { DENG2_ASSERT(isDownloadInProgress()); return *downloadInProgress; } void DownloadDialog::showCompletedDownload() { if(downloadInProgress && downloadInProgress->isReadyToInstall()) { ClientWindow::main().taskBar().openAndPauseGame(); downloadInProgress->open(); } } doomsday-stable-1.15.7/doomsday/client/src/tab_tables.c0000664000175000017500000036171512641367670022341 0ustar jaakkojaakko/** @file tab_tables.c Precalculated trigonometric functions. * * Tables sourced from tables.c in the Doom GPL release. * * @authors Copyright © 2006 Jamie Jones * @authors Copyright © 1993-1996 by id Software, Inc. * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include // FINEANGLES int finetangent[4096] = { -170910304,-56965752,-34178904,-24413316,-18988036,-15535599,-13145455,-11392683, -10052327,-8994149,-8137527,-7429880,-6835455,-6329090,-5892567,-5512368, -5178251,-4882318,-4618375,-4381502,-4167737,-3973855,-3797206,-3635590, -3487165,-3350381,-3223918,-3106651,-2997613,-2895966,-2800983,-2712030, -2628549,-2550052,-2476104,-2406322,-2340362,-2277919,-2218719,-2162516, -2109087,-2058233,-2009771,-1963536,-1919378,-1877161,-1836758,-1798063, -1760956,-1725348,-1691149,-1658278,-1626658,-1596220,-1566898,-1538632, -1511367,-1485049,-1459630,-1435065,-1411312,-1388330,-1366084,-1344537, -1323658,-1303416,-1283783,-1264730,-1246234,-1228269,-1210813,-1193846, -1177345,-1161294,-1145673,-1130465,-1115654,-1101225,-1087164,-1073455, -1060087,-1047046,-1034322,-1021901,-1009774,-997931,-986361,-975054, -964003,-953199,-942633,-932298,-922186,-912289,-902602,-893117, -883829,-874730,-865817,-857081,-848520,-840127,-831898,-823827, -815910,-808143,-800521,-793041,-785699,-778490,-771411,-764460, -757631,-750922,-744331,-737853,-731486,-725227,-719074,-713023, -707072,-701219,-695462,-689797,-684223,-678737,-673338,-668024, -662792,-657640,-652568,-647572,-642651,-637803,-633028,-628323, -623686,-619117,-614613,-610174,-605798,-601483,-597229,-593033, -588896,-584815,-580789,-576818,-572901,-569035,-565221,-561456, -557741,-554074,-550455,-546881,-543354,-539870,-536431,-533034, -529680,-526366,-523094,-519861,-516667,-513512,-510394,-507313, -504269,-501261,-498287,-495348,-492443,-489571,-486732,-483925, -481150,-478406,-475692,-473009,-470355,-467730,-465133,-462565, -460024,-457511,-455024,-452564,-450129,-447720,-445337,-442978, -440643,-438332,-436045,-433781,-431540,-429321,-427125,-424951, -422798,-420666,-418555,-416465,-414395,-412344,-410314,-408303, -406311,-404338,-402384,-400448,-398530,-396630,-394747,-392882, -391034,-389202,-387387,-385589,-383807,-382040,-380290,-378555, -376835,-375130,-373440,-371765,-370105,-368459,-366826,-365208, -363604,-362013,-360436,-358872,-357321,-355783,-354257,-352744, -351244,-349756,-348280,-346816,-345364,-343924,-342495,-341078, -339671,-338276,-336892,-335519,-334157,-332805,-331464,-330133, -328812,-327502,-326201,-324910,-323629,-322358,-321097,-319844, -318601,-317368,-316143,-314928,-313721,-312524,-311335,-310154, -308983,-307819,-306664,-305517,-304379,-303248,-302126,-301011, -299904,-298805,-297714,-296630,-295554,-294485,-293423,-292369, -291322,-290282,-289249,-288223,-287204,-286192,-285186,-284188, -283195,-282210,-281231,-280258,-279292,-278332,-277378,-276430, -275489,-274553,-273624,-272700,-271782,-270871,-269965,-269064, -268169,-267280,-266397,-265519,-264646,-263779,-262917,-262060, -261209,-260363,-259522,-258686,-257855,-257029,-256208,-255392, -254581,-253774,-252973,-252176,-251384,-250596,-249813,-249035, -248261,-247492,-246727,-245966,-245210,-244458,-243711,-242967, -242228,-241493,-240763,-240036,-239314,-238595,-237881,-237170, -236463,-235761,-235062,-234367,-233676,-232988,-232304,-231624, -230948,-230275,-229606,-228941,-228279,-227621,-226966,-226314, -225666,-225022,-224381,-223743,-223108,-222477,-221849,-221225, -220603,-219985,-219370,-218758,-218149,-217544,-216941,-216341, -215745,-215151,-214561,-213973,-213389,-212807,-212228,-211652, -211079,-210509,-209941,-209376,-208815,-208255,-207699,-207145, -206594,-206045,-205500,-204956,-204416,-203878,-203342,-202809, -202279,-201751,-201226,-200703,-200182,-199664,-199149,-198636, -198125,-197616,-197110,-196606,-196105,-195606,-195109,-194614, -194122,-193631,-193143,-192658,-192174,-191693,-191213,-190736, -190261,-189789,-189318,-188849,-188382,-187918,-187455,-186995, -186536,-186080,-185625,-185173,-184722,-184274,-183827,-183382, -182939,-182498,-182059,-181622,-181186,-180753,-180321,-179891, -179463,-179037,-178612,-178190,-177769,-177349,-176932,-176516, -176102,-175690,-175279,-174870,-174463,-174057,-173653,-173251, -172850,-172451,-172053,-171657,-171263,-170870,-170479,-170089, -169701,-169315,-168930,-168546,-168164,-167784,-167405,-167027, -166651,-166277,-165904,-165532,-165162,-164793,-164426,-164060, -163695,-163332,-162970,-162610,-162251,-161893,-161537,-161182, -160828,-160476,-160125,-159775,-159427,-159079,-158734,-158389, -158046,-157704,-157363,-157024,-156686,-156349,-156013,-155678, -155345,-155013,-154682,-154352,-154024,-153697,-153370,-153045, -152722,-152399,-152077,-151757,-151438,-151120,-150803,-150487, -150172,-149859,-149546,-149235,-148924,-148615,-148307,-148000, -147693,-147388,-147084,-146782,-146480,-146179,-145879,-145580, -145282,-144986,-144690,-144395,-144101,-143808,-143517,-143226, -142936,-142647,-142359,-142072,-141786,-141501,-141217,-140934, -140651,-140370,-140090,-139810,-139532,-139254,-138977,-138701, -138426,-138152,-137879,-137607,-137335,-137065,-136795,-136526, -136258,-135991,-135725,-135459,-135195,-134931,-134668,-134406, -134145,-133884,-133625,-133366,-133108,-132851,-132594,-132339, -132084,-131830,-131576,-131324,-131072,-130821,-130571,-130322, -130073,-129825,-129578,-129332,-129086,-128841,-128597,-128353, -128111,-127869,-127627,-127387,-127147,-126908,-126669,-126432, -126195,-125959,-125723,-125488,-125254,-125020,-124787,-124555, -124324,-124093,-123863,-123633,-123404,-123176,-122949,-122722, -122496,-122270,-122045,-121821,-121597,-121374,-121152,-120930, -120709,-120489,-120269,-120050,-119831,-119613,-119396,-119179, -118963,-118747,-118532,-118318,-118104,-117891,-117678,-117466, -117254,-117044,-116833,-116623,-116414,-116206,-115998,-115790, -115583,-115377,-115171,-114966,-114761,-114557,-114354,-114151, -113948,-113746,-113545,-113344,-113143,-112944,-112744,-112546, -112347,-112150,-111952,-111756,-111560,-111364,-111169,-110974, -110780,-110586,-110393,-110200,-110008,-109817,-109626,-109435, -109245,-109055,-108866,-108677,-108489,-108301,-108114,-107927, -107741,-107555,-107369,-107184,-107000,-106816,-106632,-106449, -106266,-106084,-105902,-105721,-105540,-105360,-105180,-105000, -104821,-104643,-104465,-104287,-104109,-103933,-103756,-103580, -103404,-103229,-103054,-102880,-102706,-102533,-102360,-102187, -102015,-101843,-101671,-101500,-101330,-101159,-100990,-100820, -100651,-100482,-100314,-100146,-99979,-99812,-99645,-99479, -99313,-99148,-98982,-98818,-98653,-98489,-98326,-98163, -98000,-97837,-97675,-97513,-97352,-97191,-97030,-96870, -96710,-96551,-96391,-96233,-96074,-95916,-95758,-95601, -95444,-95287,-95131,-94975,-94819,-94664,-94509,-94354, -94200,-94046,-93892,-93739,-93586,-93434,-93281,-93129, -92978,-92826,-92675,-92525,-92375,-92225,-92075,-91926, -91777,-91628,-91480,-91332,-91184,-91036,-90889,-90742, -90596,-90450,-90304,-90158,-90013,-89868,-89724,-89579, -89435,-89292,-89148,-89005,-88862,-88720,-88577,-88435, -88294,-88152,-88011,-87871,-87730,-87590,-87450,-87310, -87171,-87032,-86893,-86755,-86616,-86479,-86341,-86204, -86066,-85930,-85793,-85657,-85521,-85385,-85250,-85114, -84980,-84845,-84710,-84576,-84443,-84309,-84176,-84043, -83910,-83777,-83645,-83513,-83381,-83250,-83118,-82987, -82857,-82726,-82596,-82466,-82336,-82207,-82078,-81949, -81820,-81691,-81563,-81435,-81307,-81180,-81053,-80925, -80799,-80672,-80546,-80420,-80294,-80168,-80043,-79918, -79793,-79668,-79544,-79420,-79296,-79172,-79048,-78925, -78802,-78679,-78557,-78434,-78312,-78190,-78068,-77947, -77826,-77705,-77584,-77463,-77343,-77223,-77103,-76983, -76864,-76744,-76625,-76506,-76388,-76269,-76151,-76033, -75915,-75797,-75680,-75563,-75446,-75329,-75213,-75096, -74980,-74864,-74748,-74633,-74517,-74402,-74287,-74172, -74058,-73944,-73829,-73715,-73602,-73488,-73375,-73262, -73149,-73036,-72923,-72811,-72699,-72587,-72475,-72363, -72252,-72140,-72029,-71918,-71808,-71697,-71587,-71477, -71367,-71257,-71147,-71038,-70929,-70820,-70711,-70602, -70494,-70385,-70277,-70169,-70061,-69954,-69846,-69739, -69632,-69525,-69418,-69312,-69205,-69099,-68993,-68887, -68781,-68676,-68570,-68465,-68360,-68255,-68151,-68046, -67942,-67837,-67733,-67629,-67526,-67422,-67319,-67216, -67113,-67010,-66907,-66804,-66702,-66600,-66498,-66396, -66294,-66192,-66091,-65989,-65888,-65787,-65686,-65586, -65485,-65385,-65285,-65185,-65085,-64985,-64885,-64786, -64687,-64587,-64488,-64389,-64291,-64192,-64094,-63996, -63897,-63799,-63702,-63604,-63506,-63409,-63312,-63215, -63118,-63021,-62924,-62828,-62731,-62635,-62539,-62443, -62347,-62251,-62156,-62060,-61965,-61870,-61775,-61680, -61585,-61491,-61396,-61302,-61208,-61114,-61020,-60926, -60833,-60739,-60646,-60552,-60459,-60366,-60273,-60181, -60088,-59996,-59903,-59811,-59719,-59627,-59535,-59444, -59352,-59261,-59169,-59078,-58987,-58896,-58805,-58715, -58624,-58534,-58443,-58353,-58263,-58173,-58083,-57994, -57904,-57815,-57725,-57636,-57547,-57458,-57369,-57281, -57192,-57104,-57015,-56927,-56839,-56751,-56663,-56575, -56487,-56400,-56312,-56225,-56138,-56051,-55964,-55877, -55790,-55704,-55617,-55531,-55444,-55358,-55272,-55186, -55100,-55015,-54929,-54843,-54758,-54673,-54587,-54502, -54417,-54333,-54248,-54163,-54079,-53994,-53910,-53826, -53741,-53657,-53574,-53490,-53406,-53322,-53239,-53156, -53072,-52989,-52906,-52823,-52740,-52657,-52575,-52492, -52410,-52327,-52245,-52163,-52081,-51999,-51917,-51835, -51754,-51672,-51591,-51509,-51428,-51347,-51266,-51185, -51104,-51023,-50942,-50862,-50781,-50701,-50621,-50540, -50460,-50380,-50300,-50221,-50141,-50061,-49982,-49902, -49823,-49744,-49664,-49585,-49506,-49427,-49349,-49270, -49191,-49113,-49034,-48956,-48878,-48799,-48721,-48643, -48565,-48488,-48410,-48332,-48255,-48177,-48100,-48022, -47945,-47868,-47791,-47714,-47637,-47560,-47484,-47407, -47331,-47254,-47178,-47102,-47025,-46949,-46873,-46797, -46721,-46646,-46570,-46494,-46419,-46343,-46268,-46193, -46118,-46042,-45967,-45892,-45818,-45743,-45668,-45593, -45519,-45444,-45370,-45296,-45221,-45147,-45073,-44999, -44925,-44851,-44778,-44704,-44630,-44557,-44483,-44410, -44337,-44263,-44190,-44117,-44044,-43971,-43898,-43826, -43753,-43680,-43608,-43535,-43463,-43390,-43318,-43246, -43174,-43102,-43030,-42958,-42886,-42814,-42743,-42671, -42600,-42528,-42457,-42385,-42314,-42243,-42172,-42101, -42030,-41959,-41888,-41817,-41747,-41676,-41605,-41535, -41465,-41394,-41324,-41254,-41184,-41113,-41043,-40973, -40904,-40834,-40764,-40694,-40625,-40555,-40486,-40416, -40347,-40278,-40208,-40139,-40070,-40001,-39932,-39863, -39794,-39726,-39657,-39588,-39520,-39451,-39383,-39314, -39246,-39178,-39110,-39042,-38973,-38905,-38837,-38770, -38702,-38634,-38566,-38499,-38431,-38364,-38296,-38229, -38161,-38094,-38027,-37960,-37893,-37826,-37759,-37692, -37625,-37558,-37491,-37425,-37358,-37291,-37225,-37158, -37092,-37026,-36959,-36893,-36827,-36761,-36695,-36629, -36563,-36497,-36431,-36365,-36300,-36234,-36168,-36103, -36037,-35972,-35907,-35841,-35776,-35711,-35646,-35580, -35515,-35450,-35385,-35321,-35256,-35191,-35126,-35062, -34997,-34932,-34868,-34803,-34739,-34675,-34610,-34546, -34482,-34418,-34354,-34289,-34225,-34162,-34098,-34034, -33970,-33906,-33843,-33779,-33715,-33652,-33588,-33525, -33461,-33398,-33335,-33272,-33208,-33145,-33082,-33019, -32956,-32893,-32830,-32767,-32705,-32642,-32579,-32516, -32454,-32391,-32329,-32266,-32204,-32141,-32079,-32017, -31955,-31892,-31830,-31768,-31706,-31644,-31582,-31520, -31458,-31396,-31335,-31273,-31211,-31150,-31088,-31026, -30965,-30904,-30842,-30781,-30719,-30658,-30597,-30536, -30474,-30413,-30352,-30291,-30230,-30169,-30108,-30048, -29987,-29926,-29865,-29805,-29744,-29683,-29623,-29562, -29502,-29441,-29381,-29321,-29260,-29200,-29140,-29080, -29020,-28959,-28899,-28839,-28779,-28719,-28660,-28600, -28540,-28480,-28420,-28361,-28301,-28241,-28182,-28122, -28063,-28003,-27944,-27884,-27825,-27766,-27707,-27647, -27588,-27529,-27470,-27411,-27352,-27293,-27234,-27175, -27116,-27057,-26998,-26940,-26881,-26822,-26763,-26705, -26646,-26588,-26529,-26471,-26412,-26354,-26295,-26237, -26179,-26120,-26062,-26004,-25946,-25888,-25830,-25772, -25714,-25656,-25598,-25540,-25482,-25424,-25366,-25308, -25251,-25193,-25135,-25078,-25020,-24962,-24905,-24847, -24790,-24732,-24675,-24618,-24560,-24503,-24446,-24389, -24331,-24274,-24217,-24160,-24103,-24046,-23989,-23932, -23875,-23818,-23761,-23704,-23647,-23591,-23534,-23477, -23420,-23364,-23307,-23250,-23194,-23137,-23081,-23024, -22968,-22911,-22855,-22799,-22742,-22686,-22630,-22573, -22517,-22461,-22405,-22349,-22293,-22237,-22181,-22125, -22069,-22013,-21957,-21901,-21845,-21789,-21733,-21678, -21622,-21566,-21510,-21455,-21399,-21343,-21288,-21232, -21177,-21121,-21066,-21010,-20955,-20900,-20844,-20789, -20734,-20678,-20623,-20568,-20513,-20457,-20402,-20347, -20292,-20237,-20182,-20127,-20072,-20017,-19962,-19907, -19852,-19797,-19742,-19688,-19633,-19578,-19523,-19469, -19414,-19359,-19305,-19250,-19195,-19141,-19086,-19032, -18977,-18923,-18868,-18814,-18760,-18705,-18651,-18597, -18542,-18488,-18434,-18380,-18325,-18271,-18217,-18163, -18109,-18055,-18001,-17946,-17892,-17838,-17784,-17731, -17677,-17623,-17569,-17515,-17461,-17407,-17353,-17300, -17246,-17192,-17138,-17085,-17031,-16977,-16924,-16870, -16817,-16763,-16710,-16656,-16603,-16549,-16496,-16442, -16389,-16335,-16282,-16229,-16175,-16122,-16069,-16015, -15962,-15909,-15856,-15802,-15749,-15696,-15643,-15590, -15537,-15484,-15431,-15378,-15325,-15272,-15219,-15166, -15113,-15060,-15007,-14954,-14901,-14848,-14795,-14743, -14690,-14637,-14584,-14531,-14479,-14426,-14373,-14321, -14268,-14215,-14163,-14110,-14057,-14005,-13952,-13900, -13847,-13795,-13742,-13690,-13637,-13585,-13533,-13480, -13428,-13375,-13323,-13271,-13218,-13166,-13114,-13062, -13009,-12957,-12905,-12853,-12800,-12748,-12696,-12644, -12592,-12540,-12488,-12436,-12383,-12331,-12279,-12227, -12175,-12123,-12071,-12019,-11967,-11916,-11864,-11812, -11760,-11708,-11656,-11604,-11552,-11501,-11449,-11397, -11345,-11293,-11242,-11190,-11138,-11086,-11035,-10983, -10931,-10880,-10828,-10777,-10725,-10673,-10622,-10570, -10519,-10467,-10415,-10364,-10312,-10261,-10209,-10158, -10106,-10055,-10004,-9952,-9901,-9849,-9798,-9747, -9695,-9644,-9592,-9541,-9490,-9438,-9387,-9336, -9285,-9233,-9182,-9131,-9080,-9028,-8977,-8926, -8875,-8824,-8772,-8721,-8670,-8619,-8568,-8517, -8466,-8414,-8363,-8312,-8261,-8210,-8159,-8108, -8057,-8006,-7955,-7904,-7853,-7802,-7751,-7700, -7649,-7598,-7547,-7496,-7445,-7395,-7344,-7293, -7242,-7191,-7140,-7089,-7038,-6988,-6937,-6886, -6835,-6784,-6733,-6683,-6632,-6581,-6530,-6480, -6429,-6378,-6327,-6277,-6226,-6175,-6124,-6074, -6023,-5972,-5922,-5871,-5820,-5770,-5719,-5668, -5618,-5567,-5517,-5466,-5415,-5365,-5314,-5264, -5213,-5162,-5112,-5061,-5011,-4960,-4910,-4859, -4808,-4758,-4707,-4657,-4606,-4556,-4505,-4455, -4404,-4354,-4303,-4253,-4202,-4152,-4101,-4051, -4001,-3950,-3900,-3849,-3799,-3748,-3698,-3648, -3597,-3547,-3496,-3446,-3395,-3345,-3295,-3244, -3194,-3144,-3093,-3043,-2992,-2942,-2892,-2841, -2791,-2741,-2690,-2640,-2590,-2539,-2489,-2439, -2388,-2338,-2288,-2237,-2187,-2137,-2086,-2036, -1986,-1935,-1885,-1835,-1784,-1734,-1684,-1633, -1583,-1533,-1483,-1432,-1382,-1332,-1281,-1231, -1181,-1131,-1080,-1030,-980,-929,-879,-829, -779,-728,-678,-628,-578,-527,-477,-427, -376,-326,-276,-226,-175,-125,-75,-25, 25,75,125,175,226,276,326,376, 427,477,527,578,628,678,728,779, 829,879,929,980,1030,1080,1131,1181, 1231,1281,1332,1382,1432,1483,1533,1583, 1633,1684,1734,1784,1835,1885,1935,1986, 2036,2086,2137,2187,2237,2288,2338,2388, 2439,2489,2539,2590,2640,2690,2741,2791, 2841,2892,2942,2992,3043,3093,3144,3194, 3244,3295,3345,3395,3446,3496,3547,3597, 3648,3698,3748,3799,3849,3900,3950,4001, 4051,4101,4152,4202,4253,4303,4354,4404, 4455,4505,4556,4606,4657,4707,4758,4808, 4859,4910,4960,5011,5061,5112,5162,5213, 5264,5314,5365,5415,5466,5517,5567,5618, 5668,5719,5770,5820,5871,5922,5972,6023, 6074,6124,6175,6226,6277,6327,6378,6429, 6480,6530,6581,6632,6683,6733,6784,6835, 6886,6937,6988,7038,7089,7140,7191,7242, 7293,7344,7395,7445,7496,7547,7598,7649, 7700,7751,7802,7853,7904,7955,8006,8057, 8108,8159,8210,8261,8312,8363,8414,8466, 8517,8568,8619,8670,8721,8772,8824,8875, 8926,8977,9028,9080,9131,9182,9233,9285, 9336,9387,9438,9490,9541,9592,9644,9695, 9747,9798,9849,9901,9952,10004,10055,10106, 10158,10209,10261,10312,10364,10415,10467,10519, 10570,10622,10673,10725,10777,10828,10880,10931, 10983,11035,11086,11138,11190,11242,11293,11345, 11397,11449,11501,11552,11604,11656,11708,11760, 11812,11864,11916,11967,12019,12071,12123,12175, 12227,12279,12331,12383,12436,12488,12540,12592, 12644,12696,12748,12800,12853,12905,12957,13009, 13062,13114,13166,13218,13271,13323,13375,13428, 13480,13533,13585,13637,13690,13742,13795,13847, 13900,13952,14005,14057,14110,14163,14215,14268, 14321,14373,14426,14479,14531,14584,14637,14690, 14743,14795,14848,14901,14954,15007,15060,15113, 15166,15219,15272,15325,15378,15431,15484,15537, 15590,15643,15696,15749,15802,15856,15909,15962, 16015,16069,16122,16175,16229,16282,16335,16389, 16442,16496,16549,16603,16656,16710,16763,16817, 16870,16924,16977,17031,17085,17138,17192,17246, 17300,17353,17407,17461,17515,17569,17623,17677, 17731,17784,17838,17892,17946,18001,18055,18109, 18163,18217,18271,18325,18380,18434,18488,18542, 18597,18651,18705,18760,18814,18868,18923,18977, 19032,19086,19141,19195,19250,19305,19359,19414, 19469,19523,19578,19633,19688,19742,19797,19852, 19907,19962,20017,20072,20127,20182,20237,20292, 20347,20402,20457,20513,20568,20623,20678,20734, 20789,20844,20900,20955,21010,21066,21121,21177, 21232,21288,21343,21399,21455,21510,21566,21622, 21678,21733,21789,21845,21901,21957,22013,22069, 22125,22181,22237,22293,22349,22405,22461,22517, 22573,22630,22686,22742,22799,22855,22911,22968, 23024,23081,23137,23194,23250,23307,23364,23420, 23477,23534,23591,23647,23704,23761,23818,23875, 23932,23989,24046,24103,24160,24217,24274,24331, 24389,24446,24503,24560,24618,24675,24732,24790, 24847,24905,24962,25020,25078,25135,25193,25251, 25308,25366,25424,25482,25540,25598,25656,25714, 25772,25830,25888,25946,26004,26062,26120,26179, 26237,26295,26354,26412,26471,26529,26588,26646, 26705,26763,26822,26881,26940,26998,27057,27116, 27175,27234,27293,27352,27411,27470,27529,27588, 27647,27707,27766,27825,27884,27944,28003,28063, 28122,28182,28241,28301,28361,28420,28480,28540, 28600,28660,28719,28779,28839,28899,28959,29020, 29080,29140,29200,29260,29321,29381,29441,29502, 29562,29623,29683,29744,29805,29865,29926,29987, 30048,30108,30169,30230,30291,30352,30413,30474, 30536,30597,30658,30719,30781,30842,30904,30965, 31026,31088,31150,31211,31273,31335,31396,31458, 31520,31582,31644,31706,31768,31830,31892,31955, 32017,32079,32141,32204,32266,32329,32391,32454, 32516,32579,32642,32705,32767,32830,32893,32956, 33019,33082,33145,33208,33272,33335,33398,33461, 33525,33588,33652,33715,33779,33843,33906,33970, 34034,34098,34162,34225,34289,34354,34418,34482, 34546,34610,34675,34739,34803,34868,34932,34997, 35062,35126,35191,35256,35321,35385,35450,35515, 35580,35646,35711,35776,35841,35907,35972,36037, 36103,36168,36234,36300,36365,36431,36497,36563, 36629,36695,36761,36827,36893,36959,37026,37092, 37158,37225,37291,37358,37425,37491,37558,37625, 37692,37759,37826,37893,37960,38027,38094,38161, 38229,38296,38364,38431,38499,38566,38634,38702, 38770,38837,38905,38973,39042,39110,39178,39246, 39314,39383,39451,39520,39588,39657,39726,39794, 39863,39932,40001,40070,40139,40208,40278,40347, 40416,40486,40555,40625,40694,40764,40834,40904, 40973,41043,41113,41184,41254,41324,41394,41465, 41535,41605,41676,41747,41817,41888,41959,42030, 42101,42172,42243,42314,42385,42457,42528,42600, 42671,42743,42814,42886,42958,43030,43102,43174, 43246,43318,43390,43463,43535,43608,43680,43753, 43826,43898,43971,44044,44117,44190,44263,44337, 44410,44483,44557,44630,44704,44778,44851,44925, 44999,45073,45147,45221,45296,45370,45444,45519, 45593,45668,45743,45818,45892,45967,46042,46118, 46193,46268,46343,46419,46494,46570,46646,46721, 46797,46873,46949,47025,47102,47178,47254,47331, 47407,47484,47560,47637,47714,47791,47868,47945, 48022,48100,48177,48255,48332,48410,48488,48565, 48643,48721,48799,48878,48956,49034,49113,49191, 49270,49349,49427,49506,49585,49664,49744,49823, 49902,49982,50061,50141,50221,50300,50380,50460, 50540,50621,50701,50781,50862,50942,51023,51104, 51185,51266,51347,51428,51509,51591,51672,51754, 51835,51917,51999,52081,52163,52245,52327,52410, 52492,52575,52657,52740,52823,52906,52989,53072, 53156,53239,53322,53406,53490,53574,53657,53741, 53826,53910,53994,54079,54163,54248,54333,54417, 54502,54587,54673,54758,54843,54929,55015,55100, 55186,55272,55358,55444,55531,55617,55704,55790, 55877,55964,56051,56138,56225,56312,56400,56487, 56575,56663,56751,56839,56927,57015,57104,57192, 57281,57369,57458,57547,57636,57725,57815,57904, 57994,58083,58173,58263,58353,58443,58534,58624, 58715,58805,58896,58987,59078,59169,59261,59352, 59444,59535,59627,59719,59811,59903,59996,60088, 60181,60273,60366,60459,60552,60646,60739,60833, 60926,61020,61114,61208,61302,61396,61491,61585, 61680,61775,61870,61965,62060,62156,62251,62347, 62443,62539,62635,62731,62828,62924,63021,63118, 63215,63312,63409,63506,63604,63702,63799,63897, 63996,64094,64192,64291,64389,64488,64587,64687, 64786,64885,64985,65085,65185,65285,65385,65485, 65586,65686,65787,65888,65989,66091,66192,66294, 66396,66498,66600,66702,66804,66907,67010,67113, 67216,67319,67422,67526,67629,67733,67837,67942, 68046,68151,68255,68360,68465,68570,68676,68781, 68887,68993,69099,69205,69312,69418,69525,69632, 69739,69846,69954,70061,70169,70277,70385,70494, 70602,70711,70820,70929,71038,71147,71257,71367, 71477,71587,71697,71808,71918,72029,72140,72252, 72363,72475,72587,72699,72811,72923,73036,73149, 73262,73375,73488,73602,73715,73829,73944,74058, 74172,74287,74402,74517,74633,74748,74864,74980, 75096,75213,75329,75446,75563,75680,75797,75915, 76033,76151,76269,76388,76506,76625,76744,76864, 76983,77103,77223,77343,77463,77584,77705,77826, 77947,78068,78190,78312,78434,78557,78679,78802, 78925,79048,79172,79296,79420,79544,79668,79793, 79918,80043,80168,80294,80420,80546,80672,80799, 80925,81053,81180,81307,81435,81563,81691,81820, 81949,82078,82207,82336,82466,82596,82726,82857, 82987,83118,83250,83381,83513,83645,83777,83910, 84043,84176,84309,84443,84576,84710,84845,84980, 85114,85250,85385,85521,85657,85793,85930,86066, 86204,86341,86479,86616,86755,86893,87032,87171, 87310,87450,87590,87730,87871,88011,88152,88294, 88435,88577,88720,88862,89005,89148,89292,89435, 89579,89724,89868,90013,90158,90304,90450,90596, 90742,90889,91036,91184,91332,91480,91628,91777, 91926,92075,92225,92375,92525,92675,92826,92978, 93129,93281,93434,93586,93739,93892,94046,94200, 94354,94509,94664,94819,94975,95131,95287,95444, 95601,95758,95916,96074,96233,96391,96551,96710, 96870,97030,97191,97352,97513,97675,97837,98000, 98163,98326,98489,98653,98818,98982,99148,99313, 99479,99645,99812,99979,100146,100314,100482,100651, 100820,100990,101159,101330,101500,101671,101843,102015, 102187,102360,102533,102706,102880,103054,103229,103404, 103580,103756,103933,104109,104287,104465,104643,104821, 105000,105180,105360,105540,105721,105902,106084,106266, 106449,106632,106816,107000,107184,107369,107555,107741, 107927,108114,108301,108489,108677,108866,109055,109245, 109435,109626,109817,110008,110200,110393,110586,110780, 110974,111169,111364,111560,111756,111952,112150,112347, 112546,112744,112944,113143,113344,113545,113746,113948, 114151,114354,114557,114761,114966,115171,115377,115583, 115790,115998,116206,116414,116623,116833,117044,117254, 117466,117678,117891,118104,118318,118532,118747,118963, 119179,119396,119613,119831,120050,120269,120489,120709, 120930,121152,121374,121597,121821,122045,122270,122496, 122722,122949,123176,123404,123633,123863,124093,124324, 124555,124787,125020,125254,125488,125723,125959,126195, 126432,126669,126908,127147,127387,127627,127869,128111, 128353,128597,128841,129086,129332,129578,129825,130073, 130322,130571,130821,131072,131324,131576,131830,132084, 132339,132594,132851,133108,133366,133625,133884,134145, 134406,134668,134931,135195,135459,135725,135991,136258, 136526,136795,137065,137335,137607,137879,138152,138426, 138701,138977,139254,139532,139810,140090,140370,140651, 140934,141217,141501,141786,142072,142359,142647,142936, 143226,143517,143808,144101,144395,144690,144986,145282, 145580,145879,146179,146480,146782,147084,147388,147693, 148000,148307,148615,148924,149235,149546,149859,150172, 150487,150803,151120,151438,151757,152077,152399,152722, 153045,153370,153697,154024,154352,154682,155013,155345, 155678,156013,156349,156686,157024,157363,157704,158046, 158389,158734,159079,159427,159775,160125,160476,160828, 161182,161537,161893,162251,162610,162970,163332,163695, 164060,164426,164793,165162,165532,165904,166277,166651, 167027,167405,167784,168164,168546,168930,169315,169701, 170089,170479,170870,171263,171657,172053,172451,172850, 173251,173653,174057,174463,174870,175279,175690,176102, 176516,176932,177349,177769,178190,178612,179037,179463, 179891,180321,180753,181186,181622,182059,182498,182939, 183382,183827,184274,184722,185173,185625,186080,186536, 186995,187455,187918,188382,188849,189318,189789,190261, 190736,191213,191693,192174,192658,193143,193631,194122, 194614,195109,195606,196105,196606,197110,197616,198125, 198636,199149,199664,200182,200703,201226,201751,202279, 202809,203342,203878,204416,204956,205500,206045,206594, 207145,207699,208255,208815,209376,209941,210509,211079, 211652,212228,212807,213389,213973,214561,215151,215745, 216341,216941,217544,218149,218758,219370,219985,220603, 221225,221849,222477,223108,223743,224381,225022,225666, 226314,226966,227621,228279,228941,229606,230275,230948, 231624,232304,232988,233676,234367,235062,235761,236463, 237170,237881,238595,239314,240036,240763,241493,242228, 242967,243711,244458,245210,245966,246727,247492,248261, 249035,249813,250596,251384,252176,252973,253774,254581, 255392,256208,257029,257855,258686,259522,260363,261209, 262060,262917,263779,264646,265519,266397,267280,268169, 269064,269965,270871,271782,272700,273624,274553,275489, 276430,277378,278332,279292,280258,281231,282210,283195, 284188,285186,286192,287204,288223,289249,290282,291322, 292369,293423,294485,295554,296630,297714,298805,299904, 301011,302126,303248,304379,305517,306664,307819,308983, 310154,311335,312524,313721,314928,316143,317368,318601, 319844,321097,322358,323629,324910,326201,327502,328812, 330133,331464,332805,334157,335519,336892,338276,339671, 341078,342495,343924,345364,346816,348280,349756,351244, 352744,354257,355783,357321,358872,360436,362013,363604, 365208,366826,368459,370105,371765,373440,375130,376835, 378555,380290,382040,383807,385589,387387,389202,391034, 392882,394747,396630,398530,400448,402384,404338,406311, 408303,410314,412344,414395,416465,418555,420666,422798, 424951,427125,429321,431540,433781,436045,438332,440643, 442978,445337,447720,450129,452564,455024,457511,460024, 462565,465133,467730,470355,473009,475692,478406,481150, 483925,486732,489571,492443,495348,498287,501261,504269, 507313,510394,513512,516667,519861,523094,526366,529680, 533034,536431,539870,543354,546881,550455,554074,557741, 561456,565221,569035,572901,576818,580789,584815,588896, 593033,597229,601483,605798,610174,614613,619117,623686, 628323,633028,637803,642651,647572,652568,657640,662792, 668024,673338,678737,684223,689797,695462,701219,707072, 713023,719074,725227,731486,737853,744331,750922,757631, 764460,771411,778490,785699,793041,800521,808143,815910, 823827,831898,840127,848520,857081,865817,874730,883829, 893117,902602,912289,922186,932298,942633,953199,964003, 975054,986361,997931,1009774,1021901,1034322,1047046,1060087, 1073455,1087164,1101225,1115654,1130465,1145673,1161294,1177345, 1193846,1210813,1228269,1246234,1264730,1283783,1303416,1323658, 1344537,1366084,1388330,1411312,1435065,1459630,1485049,1511367, 1538632,1566898,1596220,1626658,1658278,1691149,1725348,1760956, 1798063,1836758,1877161,1919378,1963536,2009771,2058233,2109087, 2162516,2218719,2277919,2340362,2406322,2476104,2550052,2628549, 2712030,2800983,2895966,2997613,3106651,3223918,3350381,3487165, 3635590,3797206,3973855,4167737,4381502,4618375,4882318,5178251, 5512368,5892567,6329090,6835455,7429880,8137527,8994149,10052327, 11392683,13145455,15535599,18988036,24413316,34178904,56965752,170910304 }; int finesine[10240] = { 25,75,125,175,226,276,326,376, 427,477,527,578,628,678,728,779, 829,879,929,980,1030,1080,1130,1181, 1231,1281,1331,1382,1432,1482,1532,1583, 1633,1683,1733,1784,1834,1884,1934,1985, 2035,2085,2135,2186,2236,2286,2336,2387, 2437,2487,2537,2587,2638,2688,2738,2788, 2839,2889,2939,2989,3039,3090,3140,3190, 3240,3291,3341,3391,3441,3491,3541,3592, 3642,3692,3742,3792,3843,3893,3943,3993, 4043,4093,4144,4194,4244,4294,4344,4394, 4445,4495,4545,4595,4645,4695,4745,4796, 4846,4896,4946,4996,5046,5096,5146,5197, 5247,5297,5347,5397,5447,5497,5547,5597, 5647,5697,5748,5798,5848,5898,5948,5998, 6048,6098,6148,6198,6248,6298,6348,6398, 6448,6498,6548,6598,6648,6698,6748,6798, 6848,6898,6948,6998,7048,7098,7148,7198, 7248,7298,7348,7398,7448,7498,7548,7598, 7648,7697,7747,7797,7847,7897,7947,7997, 8047,8097,8147,8196,8246,8296,8346,8396, 8446,8496,8545,8595,8645,8695,8745,8794, 8844,8894,8944,8994,9043,9093,9143,9193, 9243,9292,9342,9392,9442,9491,9541,9591, 9640,9690,9740,9790,9839,9889,9939,9988, 10038,10088,10137,10187,10237,10286,10336,10386, 10435,10485,10534,10584,10634,10683,10733,10782, 10832,10882,10931,10981,11030,11080,11129,11179, 11228,11278,11327,11377,11426,11476,11525,11575, 11624,11674,11723,11773,11822,11872,11921,11970, 12020,12069,12119,12168,12218,12267,12316,12366, 12415,12464,12514,12563,12612,12662,12711,12760, 12810,12859,12908,12957,13007,13056,13105,13154, 13204,13253,13302,13351,13401,13450,13499,13548, 13597,13647,13696,13745,13794,13843,13892,13941, 13990,14040,14089,14138,14187,14236,14285,14334, 14383,14432,14481,14530,14579,14628,14677,14726, 14775,14824,14873,14922,14971,15020,15069,15118, 15167,15215,15264,15313,15362,15411,15460,15509, 15557,15606,15655,15704,15753,15802,15850,15899, 15948,15997,16045,16094,16143,16191,16240,16289, 16338,16386,16435,16484,16532,16581,16629,16678, 16727,16775,16824,16872,16921,16970,17018,17067, 17115,17164,17212,17261,17309,17358,17406,17455, 17503,17551,17600,17648,17697,17745,17793,17842, 17890,17939,17987,18035,18084,18132,18180,18228, 18277,18325,18373,18421,18470,18518,18566,18614, 18663,18711,18759,18807,18855,18903,18951,19000, 19048,19096,19144,19192,19240,19288,19336,19384, 19432,19480,19528,19576,19624,19672,19720,19768, 19816,19864,19912,19959,20007,20055,20103,20151, 20199,20246,20294,20342,20390,20438,20485,20533, 20581,20629,20676,20724,20772,20819,20867,20915, 20962,21010,21057,21105,21153,21200,21248,21295, 21343,21390,21438,21485,21533,21580,21628,21675, 21723,21770,21817,21865,21912,21960,22007,22054, 22102,22149,22196,22243,22291,22338,22385,22433, 22480,22527,22574,22621,22668,22716,22763,22810, 22857,22904,22951,22998,23045,23092,23139,23186, 23233,23280,23327,23374,23421,23468,23515,23562, 23609,23656,23703,23750,23796,23843,23890,23937, 23984,24030,24077,24124,24171,24217,24264,24311, 24357,24404,24451,24497,24544,24591,24637,24684, 24730,24777,24823,24870,24916,24963,25009,25056, 25102,25149,25195,25241,25288,25334,25381,25427, 25473,25520,25566,25612,25658,25705,25751,25797, 25843,25889,25936,25982,26028,26074,26120,26166, 26212,26258,26304,26350,26396,26442,26488,26534, 26580,26626,26672,26718,26764,26810,26856,26902, 26947,26993,27039,27085,27131,27176,27222,27268, 27313,27359,27405,27450,27496,27542,27587,27633, 27678,27724,27770,27815,27861,27906,27952,27997, 28042,28088,28133,28179,28224,28269,28315,28360, 28405,28451,28496,28541,28586,28632,28677,28722, 28767,28812,28858,28903,28948,28993,29038,29083, 29128,29173,29218,29263,29308,29353,29398,29443, 29488,29533,29577,29622,29667,29712,29757,29801, 29846,29891,29936,29980,30025,30070,30114,30159, 30204,30248,30293,30337,30382,30426,30471,30515, 30560,30604,30649,30693,30738,30782,30826,30871, 30915,30959,31004,31048,31092,31136,31181,31225, 31269,31313,31357,31402,31446,31490,31534,31578, 31622,31666,31710,31754,31798,31842,31886,31930, 31974,32017,32061,32105,32149,32193,32236,32280, 32324,32368,32411,32455,32499,32542,32586,32630, 32673,32717,32760,32804,32847,32891,32934,32978, 33021,33065,33108,33151,33195,33238,33281,33325, 33368,33411,33454,33498,33541,33584,33627,33670, 33713,33756,33799,33843,33886,33929,33972,34015, 34057,34100,34143,34186,34229,34272,34315,34358, 34400,34443,34486,34529,34571,34614,34657,34699, 34742,34785,34827,34870,34912,34955,34997,35040, 35082,35125,35167,35210,35252,35294,35337,35379, 35421,35464,35506,35548,35590,35633,35675,35717, 35759,35801,35843,35885,35927,35969,36011,36053, 36095,36137,36179,36221,36263,36305,36347,36388, 36430,36472,36514,36555,36597,36639,36681,36722, 36764,36805,36847,36889,36930,36972,37013,37055, 37096,37137,37179,37220,37262,37303,37344,37386, 37427,37468,37509,37551,37592,37633,37674,37715, 37756,37797,37838,37879,37920,37961,38002,38043, 38084,38125,38166,38207,38248,38288,38329,38370, 38411,38451,38492,38533,38573,38614,38655,38695, 38736,38776,38817,38857,38898,38938,38979,39019, 39059,39100,39140,39180,39221,39261,39301,39341, 39382,39422,39462,39502,39542,39582,39622,39662, 39702,39742,39782,39822,39862,39902,39942,39982, 40021,40061,40101,40141,40180,40220,40260,40300, 40339,40379,40418,40458,40497,40537,40576,40616, 40655,40695,40734,40773,40813,40852,40891,40931, 40970,41009,41048,41087,41127,41166,41205,41244, 41283,41322,41361,41400,41439,41478,41517,41556, 41595,41633,41672,41711,41750,41788,41827,41866, 41904,41943,41982,42020,42059,42097,42136,42174, 42213,42251,42290,42328,42366,42405,42443,42481, 42520,42558,42596,42634,42672,42711,42749,42787, 42825,42863,42901,42939,42977,43015,43053,43091, 43128,43166,43204,43242,43280,43317,43355,43393, 43430,43468,43506,43543,43581,43618,43656,43693, 43731,43768,43806,43843,43880,43918,43955,43992, 44029,44067,44104,44141,44178,44215,44252,44289, 44326,44363,44400,44437,44474,44511,44548,44585, 44622,44659,44695,44732,44769,44806,44842,44879, 44915,44952,44989,45025,45062,45098,45135,45171, 45207,45244,45280,45316,45353,45389,45425,45462, 45498,45534,45570,45606,45642,45678,45714,45750, 45786,45822,45858,45894,45930,45966,46002,46037, 46073,46109,46145,46180,46216,46252,46287,46323, 46358,46394,46429,46465,46500,46536,46571,46606, 46642,46677,46712,46747,46783,46818,46853,46888, 46923,46958,46993,47028,47063,47098,47133,47168, 47203,47238,47273,47308,47342,47377,47412,47446, 47481,47516,47550,47585,47619,47654,47688,47723, 47757,47792,47826,47860,47895,47929,47963,47998, 48032,48066,48100,48134,48168,48202,48237,48271, 48305,48338,48372,48406,48440,48474,48508,48542, 48575,48609,48643,48676,48710,48744,48777,48811, 48844,48878,48911,48945,48978,49012,49045,49078, 49112,49145,49178,49211,49244,49278,49311,49344, 49377,49410,49443,49476,49509,49542,49575,49608, 49640,49673,49706,49739,49771,49804,49837,49869, 49902,49935,49967,50000,50032,50065,50097,50129, 50162,50194,50226,50259,50291,50323,50355,50387, 50420,50452,50484,50516,50548,50580,50612,50644, 50675,50707,50739,50771,50803,50834,50866,50898, 50929,50961,50993,51024,51056,51087,51119,51150, 51182,51213,51244,51276,51307,51338,51369,51401, 51432,51463,51494,51525,51556,51587,51618,51649, 51680,51711,51742,51773,51803,51834,51865,51896, 51926,51957,51988,52018,52049,52079,52110,52140, 52171,52201,52231,52262,52292,52322,52353,52383, 52413,52443,52473,52503,52534,52564,52594,52624, 52653,52683,52713,52743,52773,52803,52832,52862, 52892,52922,52951,52981,53010,53040,53069,53099, 53128,53158,53187,53216,53246,53275,53304,53334, 53363,53392,53421,53450,53479,53508,53537,53566, 53595,53624,53653,53682,53711,53739,53768,53797, 53826,53854,53883,53911,53940,53969,53997,54026, 54054,54082,54111,54139,54167,54196,54224,54252, 54280,54308,54337,54365,54393,54421,54449,54477, 54505,54533,54560,54588,54616,54644,54672,54699, 54727,54755,54782,54810,54837,54865,54892,54920, 54947,54974,55002,55029,55056,55084,55111,55138, 55165,55192,55219,55246,55274,55300,55327,55354, 55381,55408,55435,55462,55489,55515,55542,55569, 55595,55622,55648,55675,55701,55728,55754,55781, 55807,55833,55860,55886,55912,55938,55965,55991, 56017,56043,56069,56095,56121,56147,56173,56199, 56225,56250,56276,56302,56328,56353,56379,56404, 56430,56456,56481,56507,56532,56557,56583,56608, 56633,56659,56684,56709,56734,56760,56785,56810, 56835,56860,56885,56910,56935,56959,56984,57009, 57034,57059,57083,57108,57133,57157,57182,57206, 57231,57255,57280,57304,57329,57353,57377,57402, 57426,57450,57474,57498,57522,57546,57570,57594, 57618,57642,57666,57690,57714,57738,57762,57785, 57809,57833,57856,57880,57903,57927,57950,57974, 57997,58021,58044,58067,58091,58114,58137,58160, 58183,58207,58230,58253,58276,58299,58322,58345, 58367,58390,58413,58436,58459,58481,58504,58527, 58549,58572,58594,58617,58639,58662,58684,58706, 58729,58751,58773,58795,58818,58840,58862,58884, 58906,58928,58950,58972,58994,59016,59038,59059, 59081,59103,59125,59146,59168,59190,59211,59233, 59254,59276,59297,59318,59340,59361,59382,59404, 59425,59446,59467,59488,59509,59530,59551,59572, 59593,59614,59635,59656,59677,59697,59718,59739, 59759,59780,59801,59821,59842,59862,59883,59903, 59923,59944,59964,59984,60004,60025,60045,60065, 60085,60105,60125,60145,60165,60185,60205,60225, 60244,60264,60284,60304,60323,60343,60363,60382, 60402,60421,60441,60460,60479,60499,60518,60537, 60556,60576,60595,60614,60633,60652,60671,60690, 60709,60728,60747,60766,60785,60803,60822,60841, 60859,60878,60897,60915,60934,60952,60971,60989, 61007,61026,61044,61062,61081,61099,61117,61135, 61153,61171,61189,61207,61225,61243,61261,61279, 61297,61314,61332,61350,61367,61385,61403,61420, 61438,61455,61473,61490,61507,61525,61542,61559, 61577,61594,61611,61628,61645,61662,61679,61696, 61713,61730,61747,61764,61780,61797,61814,61831, 61847,61864,61880,61897,61913,61930,61946,61963, 61979,61995,62012,62028,62044,62060,62076,62092, 62108,62125,62141,62156,62172,62188,62204,62220, 62236,62251,62267,62283,62298,62314,62329,62345, 62360,62376,62391,62407,62422,62437,62453,62468, 62483,62498,62513,62528,62543,62558,62573,62588, 62603,62618,62633,62648,62662,62677,62692,62706, 62721,62735,62750,62764,62779,62793,62808,62822, 62836,62850,62865,62879,62893,62907,62921,62935, 62949,62963,62977,62991,63005,63019,63032,63046, 63060,63074,63087,63101,63114,63128,63141,63155, 63168,63182,63195,63208,63221,63235,63248,63261, 63274,63287,63300,63313,63326,63339,63352,63365, 63378,63390,63403,63416,63429,63441,63454,63466, 63479,63491,63504,63516,63528,63541,63553,63565, 63578,63590,63602,63614,63626,63638,63650,63662, 63674,63686,63698,63709,63721,63733,63745,63756, 63768,63779,63791,63803,63814,63825,63837,63848, 63859,63871,63882,63893,63904,63915,63927,63938, 63949,63960,63971,63981,63992,64003,64014,64025, 64035,64046,64057,64067,64078,64088,64099,64109, 64120,64130,64140,64151,64161,64171,64181,64192, 64202,64212,64222,64232,64242,64252,64261,64271, 64281,64291,64301,64310,64320,64330,64339,64349, 64358,64368,64377,64387,64396,64405,64414,64424, 64433,64442,64451,64460,64469,64478,64487,64496, 64505,64514,64523,64532,64540,64549,64558,64566, 64575,64584,64592,64601,64609,64617,64626,64634, 64642,64651,64659,64667,64675,64683,64691,64699, 64707,64715,64723,64731,64739,64747,64754,64762, 64770,64777,64785,64793,64800,64808,64815,64822, 64830,64837,64844,64852,64859,64866,64873,64880, 64887,64895,64902,64908,64915,64922,64929,64936, 64943,64949,64956,64963,64969,64976,64982,64989, 64995,65002,65008,65015,65021,65027,65033,65040, 65046,65052,65058,65064,65070,65076,65082,65088, 65094,65099,65105,65111,65117,65122,65128,65133, 65139,65144,65150,65155,65161,65166,65171,65177, 65182,65187,65192,65197,65202,65207,65212,65217, 65222,65227,65232,65237,65242,65246,65251,65256, 65260,65265,65270,65274,65279,65283,65287,65292, 65296,65300,65305,65309,65313,65317,65321,65325, 65329,65333,65337,65341,65345,65349,65352,65356, 65360,65363,65367,65371,65374,65378,65381,65385, 65388,65391,65395,65398,65401,65404,65408,65411, 65414,65417,65420,65423,65426,65429,65431,65434, 65437,65440,65442,65445,65448,65450,65453,65455, 65458,65460,65463,65465,65467,65470,65472,65474, 65476,65478,65480,65482,65484,65486,65488,65490, 65492,65494,65496,65497,65499,65501,65502,65504, 65505,65507,65508,65510,65511,65513,65514,65515, 65516,65518,65519,65520,65521,65522,65523,65524, 65525,65526,65527,65527,65528,65529,65530,65530, 65531,65531,65532,65532,65533,65533,65534,65534, 65534,65535,65535,65535,65535,65535,65535,65535, 65535,65535,65535,65535,65535,65535,65535,65534, 65534,65534,65533,65533,65532,65532,65531,65531, 65530,65530,65529,65528,65527,65527,65526,65525, 65524,65523,65522,65521,65520,65519,65518,65516, 65515,65514,65513,65511,65510,65508,65507,65505, 65504,65502,65501,65499,65497,65496,65494,65492, 65490,65488,65486,65484,65482,65480,65478,65476, 65474,65472,65470,65467,65465,65463,65460,65458, 65455,65453,65450,65448,65445,65442,65440,65437, 65434,65431,65429,65426,65423,65420,65417,65414, 65411,65408,65404,65401,65398,65395,65391,65388, 65385,65381,65378,65374,65371,65367,65363,65360, 65356,65352,65349,65345,65341,65337,65333,65329, 65325,65321,65317,65313,65309,65305,65300,65296, 65292,65287,65283,65279,65274,65270,65265,65260, 65256,65251,65246,65242,65237,65232,65227,65222, 65217,65212,65207,65202,65197,65192,65187,65182, 65177,65171,65166,65161,65155,65150,65144,65139, 65133,65128,65122,65117,65111,65105,65099,65094, 65088,65082,65076,65070,65064,65058,65052,65046, 65040,65033,65027,65021,65015,65008,65002,64995, 64989,64982,64976,64969,64963,64956,64949,64943, 64936,64929,64922,64915,64908,64902,64895,64887, 64880,64873,64866,64859,64852,64844,64837,64830, 64822,64815,64808,64800,64793,64785,64777,64770, 64762,64754,64747,64739,64731,64723,64715,64707, 64699,64691,64683,64675,64667,64659,64651,64642, 64634,64626,64617,64609,64600,64592,64584,64575, 64566,64558,64549,64540,64532,64523,64514,64505, 64496,64487,64478,64469,64460,64451,64442,64433, 64424,64414,64405,64396,64387,64377,64368,64358, 64349,64339,64330,64320,64310,64301,64291,64281, 64271,64261,64252,64242,64232,64222,64212,64202, 64192,64181,64171,64161,64151,64140,64130,64120, 64109,64099,64088,64078,64067,64057,64046,64035, 64025,64014,64003,63992,63981,63971,63960,63949, 63938,63927,63915,63904,63893,63882,63871,63859, 63848,63837,63825,63814,63803,63791,63779,63768, 63756,63745,63733,63721,63709,63698,63686,63674, 63662,63650,63638,63626,63614,63602,63590,63578, 63565,63553,63541,63528,63516,63504,63491,63479, 63466,63454,63441,63429,63416,63403,63390,63378, 63365,63352,63339,63326,63313,63300,63287,63274, 63261,63248,63235,63221,63208,63195,63182,63168, 63155,63141,63128,63114,63101,63087,63074,63060, 63046,63032,63019,63005,62991,62977,62963,62949, 62935,62921,62907,62893,62879,62865,62850,62836, 62822,62808,62793,62779,62764,62750,62735,62721, 62706,62692,62677,62662,62648,62633,62618,62603, 62588,62573,62558,62543,62528,62513,62498,62483, 62468,62453,62437,62422,62407,62391,62376,62360, 62345,62329,62314,62298,62283,62267,62251,62236, 62220,62204,62188,62172,62156,62141,62125,62108, 62092,62076,62060,62044,62028,62012,61995,61979, 61963,61946,61930,61913,61897,61880,61864,61847, 61831,61814,61797,61780,61764,61747,61730,61713, 61696,61679,61662,61645,61628,61611,61594,61577, 61559,61542,61525,61507,61490,61473,61455,61438, 61420,61403,61385,61367,61350,61332,61314,61297, 61279,61261,61243,61225,61207,61189,61171,61153, 61135,61117,61099,61081,61062,61044,61026,61007, 60989,60971,60952,60934,60915,60897,60878,60859, 60841,60822,60803,60785,60766,60747,60728,60709, 60690,60671,60652,60633,60614,60595,60576,60556, 60537,60518,60499,60479,60460,60441,60421,60402, 60382,60363,60343,60323,60304,60284,60264,60244, 60225,60205,60185,60165,60145,60125,60105,60085, 60065,60045,60025,60004,59984,59964,59944,59923, 59903,59883,59862,59842,59821,59801,59780,59759, 59739,59718,59697,59677,59656,59635,59614,59593, 59572,59551,59530,59509,59488,59467,59446,59425, 59404,59382,59361,59340,59318,59297,59276,59254, 59233,59211,59190,59168,59146,59125,59103,59081, 59059,59038,59016,58994,58972,58950,58928,58906, 58884,58862,58840,58818,58795,58773,58751,58729, 58706,58684,58662,58639,58617,58594,58572,58549, 58527,58504,58481,58459,58436,58413,58390,58367, 58345,58322,58299,58276,58253,58230,58207,58183, 58160,58137,58114,58091,58067,58044,58021,57997, 57974,57950,57927,57903,57880,57856,57833,57809, 57785,57762,57738,57714,57690,57666,57642,57618, 57594,57570,57546,57522,57498,57474,57450,57426, 57402,57377,57353,57329,57304,57280,57255,57231, 57206,57182,57157,57133,57108,57083,57059,57034, 57009,56984,56959,56935,56910,56885,56860,56835, 56810,56785,56760,56734,56709,56684,56659,56633, 56608,56583,56557,56532,56507,56481,56456,56430, 56404,56379,56353,56328,56302,56276,56250,56225, 56199,56173,56147,56121,56095,56069,56043,56017, 55991,55965,55938,55912,55886,55860,55833,55807, 55781,55754,55728,55701,55675,55648,55622,55595, 55569,55542,55515,55489,55462,55435,55408,55381, 55354,55327,55300,55274,55246,55219,55192,55165, 55138,55111,55084,55056,55029,55002,54974,54947, 54920,54892,54865,54837,54810,54782,54755,54727, 54699,54672,54644,54616,54588,54560,54533,54505, 54477,54449,54421,54393,54365,54337,54308,54280, 54252,54224,54196,54167,54139,54111,54082,54054, 54026,53997,53969,53940,53911,53883,53854,53826, 53797,53768,53739,53711,53682,53653,53624,53595, 53566,53537,53508,53479,53450,53421,53392,53363, 53334,53304,53275,53246,53216,53187,53158,53128, 53099,53069,53040,53010,52981,52951,52922,52892, 52862,52832,52803,52773,52743,52713,52683,52653, 52624,52594,52564,52534,52503,52473,52443,52413, 52383,52353,52322,52292,52262,52231,52201,52171, 52140,52110,52079,52049,52018,51988,51957,51926, 51896,51865,51834,51803,51773,51742,51711,51680, 51649,51618,51587,51556,51525,51494,51463,51432, 51401,51369,51338,51307,51276,51244,51213,51182, 51150,51119,51087,51056,51024,50993,50961,50929, 50898,50866,50834,50803,50771,50739,50707,50675, 50644,50612,50580,50548,50516,50484,50452,50420, 50387,50355,50323,50291,50259,50226,50194,50162, 50129,50097,50065,50032,50000,49967,49935,49902, 49869,49837,49804,49771,49739,49706,49673,49640, 49608,49575,49542,49509,49476,49443,49410,49377, 49344,49311,49278,49244,49211,49178,49145,49112, 49078,49045,49012,48978,48945,48911,48878,48844, 48811,48777,48744,48710,48676,48643,48609,48575, 48542,48508,48474,48440,48406,48372,48338,48304, 48271,48237,48202,48168,48134,48100,48066,48032, 47998,47963,47929,47895,47860,47826,47792,47757, 47723,47688,47654,47619,47585,47550,47516,47481, 47446,47412,47377,47342,47308,47273,47238,47203, 47168,47133,47098,47063,47028,46993,46958,46923, 46888,46853,46818,46783,46747,46712,46677,46642, 46606,46571,46536,46500,46465,46429,46394,46358, 46323,46287,46252,46216,46180,46145,46109,46073, 46037,46002,45966,45930,45894,45858,45822,45786, 45750,45714,45678,45642,45606,45570,45534,45498, 45462,45425,45389,45353,45316,45280,45244,45207, 45171,45135,45098,45062,45025,44989,44952,44915, 44879,44842,44806,44769,44732,44695,44659,44622, 44585,44548,44511,44474,44437,44400,44363,44326, 44289,44252,44215,44178,44141,44104,44067,44029, 43992,43955,43918,43880,43843,43806,43768,43731, 43693,43656,43618,43581,43543,43506,43468,43430, 43393,43355,43317,43280,43242,43204,43166,43128, 43091,43053,43015,42977,42939,42901,42863,42825, 42787,42749,42711,42672,42634,42596,42558,42520, 42481,42443,42405,42366,42328,42290,42251,42213, 42174,42136,42097,42059,42020,41982,41943,41904, 41866,41827,41788,41750,41711,41672,41633,41595, 41556,41517,41478,41439,41400,41361,41322,41283, 41244,41205,41166,41127,41088,41048,41009,40970, 40931,40891,40852,40813,40773,40734,40695,40655, 40616,40576,40537,40497,40458,40418,40379,40339, 40300,40260,40220,40180,40141,40101,40061,40021, 39982,39942,39902,39862,39822,39782,39742,39702, 39662,39622,39582,39542,39502,39462,39422,39382, 39341,39301,39261,39221,39180,39140,39100,39059, 39019,38979,38938,38898,38857,38817,38776,38736, 38695,38655,38614,38573,38533,38492,38451,38411, 38370,38329,38288,38248,38207,38166,38125,38084, 38043,38002,37961,37920,37879,37838,37797,37756, 37715,37674,37633,37592,37551,37509,37468,37427, 37386,37344,37303,37262,37220,37179,37137,37096, 37055,37013,36972,36930,36889,36847,36805,36764, 36722,36681,36639,36597,36556,36514,36472,36430, 36388,36347,36305,36263,36221,36179,36137,36095, 36053,36011,35969,35927,35885,35843,35801,35759, 35717,35675,35633,35590,35548,35506,35464,35421, 35379,35337,35294,35252,35210,35167,35125,35082, 35040,34997,34955,34912,34870,34827,34785,34742, 34699,34657,34614,34571,34529,34486,34443,34400, 34358,34315,34272,34229,34186,34143,34100,34057, 34015,33972,33929,33886,33843,33799,33756,33713, 33670,33627,33584,33541,33498,33454,33411,33368, 33325,33281,33238,33195,33151,33108,33065,33021, 32978,32934,32891,32847,32804,32760,32717,32673, 32630,32586,32542,32499,32455,32411,32368,32324, 32280,32236,32193,32149,32105,32061,32017,31974, 31930,31886,31842,31798,31754,31710,31666,31622, 31578,31534,31490,31446,31402,31357,31313,31269, 31225,31181,31136,31092,31048,31004,30959,30915, 30871,30826,30782,30738,30693,30649,30604,30560, 30515,30471,30426,30382,30337,30293,30248,30204, 30159,30114,30070,30025,29980,29936,29891,29846, 29801,29757,29712,29667,29622,29577,29533,29488, 29443,29398,29353,29308,29263,29218,29173,29128, 29083,29038,28993,28948,28903,28858,28812,28767, 28722,28677,28632,28586,28541,28496,28451,28405, 28360,28315,28269,28224,28179,28133,28088,28042, 27997,27952,27906,27861,27815,27770,27724,27678, 27633,27587,27542,27496,27450,27405,27359,27313, 27268,27222,27176,27131,27085,27039,26993,26947, 26902,26856,26810,26764,26718,26672,26626,26580, 26534,26488,26442,26396,26350,26304,26258,26212, 26166,26120,26074,26028,25982,25936,25889,25843, 25797,25751,25705,25658,25612,25566,25520,25473, 25427,25381,25334,25288,25241,25195,25149,25102, 25056,25009,24963,24916,24870,24823,24777,24730, 24684,24637,24591,24544,24497,24451,24404,24357, 24311,24264,24217,24171,24124,24077,24030,23984, 23937,23890,23843,23796,23750,23703,23656,23609, 23562,23515,23468,23421,23374,23327,23280,23233, 23186,23139,23092,23045,22998,22951,22904,22857, 22810,22763,22716,22668,22621,22574,22527,22480, 22433,22385,22338,22291,22243,22196,22149,22102, 22054,22007,21960,21912,21865,21817,21770,21723, 21675,21628,21580,21533,21485,21438,21390,21343, 21295,21248,21200,21153,21105,21057,21010,20962, 20915,20867,20819,20772,20724,20676,20629,20581, 20533,20485,20438,20390,20342,20294,20246,20199, 20151,20103,20055,20007,19959,19912,19864,19816, 19768,19720,19672,19624,19576,19528,19480,19432, 19384,19336,19288,19240,19192,19144,19096,19048, 19000,18951,18903,18855,18807,18759,18711,18663, 18614,18566,18518,18470,18421,18373,18325,18277, 18228,18180,18132,18084,18035,17987,17939,17890, 17842,17793,17745,17697,17648,17600,17551,17503, 17455,17406,17358,17309,17261,17212,17164,17115, 17067,17018,16970,16921,16872,16824,16775,16727, 16678,16629,16581,16532,16484,16435,16386,16338, 16289,16240,16191,16143,16094,16045,15997,15948, 15899,15850,15802,15753,15704,15655,15606,15557, 15509,15460,15411,15362,15313,15264,15215,15167, 15118,15069,15020,14971,14922,14873,14824,14775, 14726,14677,14628,14579,14530,14481,14432,14383, 14334,14285,14236,14187,14138,14089,14040,13990, 13941,13892,13843,13794,13745,13696,13646,13597, 13548,13499,13450,13401,13351,13302,13253,13204, 13154,13105,13056,13007,12957,12908,12859,12810, 12760,12711,12662,12612,12563,12514,12464,12415, 12366,12316,12267,12218,12168,12119,12069,12020, 11970,11921,11872,11822,11773,11723,11674,11624, 11575,11525,11476,11426,11377,11327,11278,11228, 11179,11129,11080,11030,10981,10931,10882,10832, 10782,10733,10683,10634,10584,10534,10485,10435, 10386,10336,10286,10237,10187,10137,10088,10038, 9988,9939,9889,9839,9790,9740,9690,9640, 9591,9541,9491,9442,9392,9342,9292,9243, 9193,9143,9093,9043,8994,8944,8894,8844, 8794,8745,8695,8645,8595,8545,8496,8446, 8396,8346,8296,8246,8196,8147,8097,8047, 7997,7947,7897,7847,7797,7747,7697,7648, 7598,7548,7498,7448,7398,7348,7298,7248, 7198,7148,7098,7048,6998,6948,6898,6848, 6798,6748,6698,6648,6598,6548,6498,6448, 6398,6348,6298,6248,6198,6148,6098,6048, 5998,5948,5898,5848,5798,5748,5697,5647, 5597,5547,5497,5447,5397,5347,5297,5247, 5197,5146,5096,5046,4996,4946,4896,4846, 4796,4745,4695,4645,4595,4545,4495,4445, 4394,4344,4294,4244,4194,4144,4093,4043, 3993,3943,3893,3843,3792,3742,3692,3642, 3592,3541,3491,3441,3391,3341,3291,3240, 3190,3140,3090,3039,2989,2939,2889,2839, 2788,2738,2688,2638,2587,2537,2487,2437, 2387,2336,2286,2236,2186,2135,2085,2035, 1985,1934,1884,1834,1784,1733,1683,1633, 1583,1532,1482,1432,1382,1331,1281,1231, 1181,1130,1080,1030,980,929,879,829, 779,728,678,628,578,527,477,427, 376,326,276,226,175,125,75,25, -25,-75,-125,-175,-226,-276,-326,-376, -427,-477,-527,-578,-628,-678,-728,-779, -829,-879,-929,-980,-1030,-1080,-1130,-1181, -1231,-1281,-1331,-1382,-1432,-1482,-1532,-1583, -1633,-1683,-1733,-1784,-1834,-1884,-1934,-1985, -2035,-2085,-2135,-2186,-2236,-2286,-2336,-2387, -2437,-2487,-2537,-2588,-2638,-2688,-2738,-2788, -2839,-2889,-2939,-2989,-3039,-3090,-3140,-3190, -3240,-3291,-3341,-3391,-3441,-3491,-3541,-3592, -3642,-3692,-3742,-3792,-3843,-3893,-3943,-3993, -4043,-4093,-4144,-4194,-4244,-4294,-4344,-4394, -4445,-4495,-4545,-4595,-4645,-4695,-4745,-4796, -4846,-4896,-4946,-4996,-5046,-5096,-5146,-5197, -5247,-5297,-5347,-5397,-5447,-5497,-5547,-5597, -5647,-5697,-5748,-5798,-5848,-5898,-5948,-5998, -6048,-6098,-6148,-6198,-6248,-6298,-6348,-6398, -6448,-6498,-6548,-6598,-6648,-6698,-6748,-6798, -6848,-6898,-6948,-6998,-7048,-7098,-7148,-7198, -7248,-7298,-7348,-7398,-7448,-7498,-7548,-7598, -7648,-7697,-7747,-7797,-7847,-7897,-7947,-7997, -8047,-8097,-8147,-8196,-8246,-8296,-8346,-8396, -8446,-8496,-8545,-8595,-8645,-8695,-8745,-8794, -8844,-8894,-8944,-8994,-9043,-9093,-9143,-9193, -9243,-9292,-9342,-9392,-9442,-9491,-9541,-9591, -9640,-9690,-9740,-9790,-9839,-9889,-9939,-9988, -10038,-10088,-10137,-10187,-10237,-10286,-10336,-10386, -10435,-10485,-10534,-10584,-10634,-10683,-10733,-10782, -10832,-10882,-10931,-10981,-11030,-11080,-11129,-11179, -11228,-11278,-11327,-11377,-11426,-11476,-11525,-11575, -11624,-11674,-11723,-11773,-11822,-11872,-11921,-11970, -12020,-12069,-12119,-12168,-12218,-12267,-12316,-12366, -12415,-12464,-12514,-12563,-12612,-12662,-12711,-12760, -12810,-12859,-12908,-12957,-13007,-13056,-13105,-13154, -13204,-13253,-13302,-13351,-13401,-13450,-13499,-13548, -13597,-13647,-13696,-13745,-13794,-13843,-13892,-13941, -13990,-14040,-14089,-14138,-14187,-14236,-14285,-14334, -14383,-14432,-14481,-14530,-14579,-14628,-14677,-14726, -14775,-14824,-14873,-14922,-14971,-15020,-15069,-15118, -15167,-15215,-15264,-15313,-15362,-15411,-15460,-15509, -15557,-15606,-15655,-15704,-15753,-15802,-15850,-15899, -15948,-15997,-16045,-16094,-16143,-16191,-16240,-16289, -16338,-16386,-16435,-16484,-16532,-16581,-16629,-16678, -16727,-16775,-16824,-16872,-16921,-16970,-17018,-17067, -17115,-17164,-17212,-17261,-17309,-17358,-17406,-17455, -17503,-17551,-17600,-17648,-17697,-17745,-17793,-17842, -17890,-17939,-17987,-18035,-18084,-18132,-18180,-18228, -18277,-18325,-18373,-18421,-18470,-18518,-18566,-18614, -18663,-18711,-18759,-18807,-18855,-18903,-18951,-19000, -19048,-19096,-19144,-19192,-19240,-19288,-19336,-19384, -19432,-19480,-19528,-19576,-19624,-19672,-19720,-19768, -19816,-19864,-19912,-19959,-20007,-20055,-20103,-20151, -20199,-20246,-20294,-20342,-20390,-20438,-20485,-20533, -20581,-20629,-20676,-20724,-20772,-20819,-20867,-20915, -20962,-21010,-21057,-21105,-21153,-21200,-21248,-21295, -21343,-21390,-21438,-21485,-21533,-21580,-21628,-21675, -21723,-21770,-21817,-21865,-21912,-21960,-22007,-22054, -22102,-22149,-22196,-22243,-22291,-22338,-22385,-22433, -22480,-22527,-22574,-22621,-22668,-22716,-22763,-22810, -22857,-22904,-22951,-22998,-23045,-23092,-23139,-23186, -23233,-23280,-23327,-23374,-23421,-23468,-23515,-23562, -23609,-23656,-23703,-23750,-23796,-23843,-23890,-23937, -23984,-24030,-24077,-24124,-24171,-24217,-24264,-24311, -24357,-24404,-24451,-24497,-24544,-24591,-24637,-24684, -24730,-24777,-24823,-24870,-24916,-24963,-25009,-25056, -25102,-25149,-25195,-25241,-25288,-25334,-25381,-25427, -25473,-25520,-25566,-25612,-25658,-25705,-25751,-25797, -25843,-25889,-25936,-25982,-26028,-26074,-26120,-26166, -26212,-26258,-26304,-26350,-26396,-26442,-26488,-26534, -26580,-26626,-26672,-26718,-26764,-26810,-26856,-26902, -26947,-26993,-27039,-27085,-27131,-27176,-27222,-27268, -27313,-27359,-27405,-27450,-27496,-27542,-27587,-27633, -27678,-27724,-27770,-27815,-27861,-27906,-27952,-27997, -28042,-28088,-28133,-28179,-28224,-28269,-28315,-28360, -28405,-28451,-28496,-28541,-28586,-28632,-28677,-28722, -28767,-28812,-28858,-28903,-28948,-28993,-29038,-29083, -29128,-29173,-29218,-29263,-29308,-29353,-29398,-29443, -29488,-29533,-29577,-29622,-29667,-29712,-29757,-29801, -29846,-29891,-29936,-29980,-30025,-30070,-30114,-30159, -30204,-30248,-30293,-30337,-30382,-30426,-30471,-30515, -30560,-30604,-30649,-30693,-30738,-30782,-30826,-30871, -30915,-30959,-31004,-31048,-31092,-31136,-31181,-31225, -31269,-31313,-31357,-31402,-31446,-31490,-31534,-31578, -31622,-31666,-31710,-31754,-31798,-31842,-31886,-31930, -31974,-32017,-32061,-32105,-32149,-32193,-32236,-32280, -32324,-32368,-32411,-32455,-32499,-32542,-32586,-32630, -32673,-32717,-32760,-32804,-32847,-32891,-32934,-32978, -33021,-33065,-33108,-33151,-33195,-33238,-33281,-33325, -33368,-33411,-33454,-33498,-33541,-33584,-33627,-33670, -33713,-33756,-33799,-33843,-33886,-33929,-33972,-34015, -34057,-34100,-34143,-34186,-34229,-34272,-34315,-34358, -34400,-34443,-34486,-34529,-34571,-34614,-34657,-34699, -34742,-34785,-34827,-34870,-34912,-34955,-34997,-35040, -35082,-35125,-35167,-35210,-35252,-35294,-35337,-35379, -35421,-35464,-35506,-35548,-35590,-35633,-35675,-35717, -35759,-35801,-35843,-35885,-35927,-35969,-36011,-36053, -36095,-36137,-36179,-36221,-36263,-36305,-36347,-36388, -36430,-36472,-36514,-36555,-36597,-36639,-36681,-36722, -36764,-36805,-36847,-36889,-36930,-36972,-37013,-37055, -37096,-37137,-37179,-37220,-37262,-37303,-37344,-37386, -37427,-37468,-37509,-37551,-37592,-37633,-37674,-37715, -37756,-37797,-37838,-37879,-37920,-37961,-38002,-38043, -38084,-38125,-38166,-38207,-38248,-38288,-38329,-38370, -38411,-38451,-38492,-38533,-38573,-38614,-38655,-38695, -38736,-38776,-38817,-38857,-38898,-38938,-38979,-39019, -39059,-39100,-39140,-39180,-39221,-39261,-39301,-39341, -39382,-39422,-39462,-39502,-39542,-39582,-39622,-39662, -39702,-39742,-39782,-39822,-39862,-39902,-39942,-39982, -40021,-40061,-40101,-40141,-40180,-40220,-40260,-40299, -40339,-40379,-40418,-40458,-40497,-40537,-40576,-40616, -40655,-40695,-40734,-40773,-40813,-40852,-40891,-40931, -40970,-41009,-41048,-41087,-41127,-41166,-41205,-41244, -41283,-41322,-41361,-41400,-41439,-41478,-41517,-41556, -41595,-41633,-41672,-41711,-41750,-41788,-41827,-41866, -41904,-41943,-41982,-42020,-42059,-42097,-42136,-42174, -42213,-42251,-42290,-42328,-42366,-42405,-42443,-42481, -42520,-42558,-42596,-42634,-42672,-42711,-42749,-42787, -42825,-42863,-42901,-42939,-42977,-43015,-43053,-43091, -43128,-43166,-43204,-43242,-43280,-43317,-43355,-43393, -43430,-43468,-43506,-43543,-43581,-43618,-43656,-43693, -43731,-43768,-43806,-43843,-43880,-43918,-43955,-43992, -44029,-44067,-44104,-44141,-44178,-44215,-44252,-44289, -44326,-44363,-44400,-44437,-44474,-44511,-44548,-44585, -44622,-44659,-44695,-44732,-44769,-44806,-44842,-44879, -44915,-44952,-44989,-45025,-45062,-45098,-45135,-45171, -45207,-45244,-45280,-45316,-45353,-45389,-45425,-45462, -45498,-45534,-45570,-45606,-45642,-45678,-45714,-45750, -45786,-45822,-45858,-45894,-45930,-45966,-46002,-46037, -46073,-46109,-46145,-46180,-46216,-46252,-46287,-46323, -46358,-46394,-46429,-46465,-46500,-46536,-46571,-46606, -46642,-46677,-46712,-46747,-46783,-46818,-46853,-46888, -46923,-46958,-46993,-47028,-47063,-47098,-47133,-47168, -47203,-47238,-47273,-47308,-47342,-47377,-47412,-47446, -47481,-47516,-47550,-47585,-47619,-47654,-47688,-47723, -47757,-47792,-47826,-47860,-47895,-47929,-47963,-47998, -48032,-48066,-48100,-48134,-48168,-48202,-48236,-48271, -48304,-48338,-48372,-48406,-48440,-48474,-48508,-48542, -48575,-48609,-48643,-48676,-48710,-48744,-48777,-48811, -48844,-48878,-48911,-48945,-48978,-49012,-49045,-49078, -49112,-49145,-49178,-49211,-49244,-49278,-49311,-49344, -49377,-49410,-49443,-49476,-49509,-49542,-49575,-49608, -49640,-49673,-49706,-49739,-49771,-49804,-49837,-49869, -49902,-49935,-49967,-50000,-50032,-50065,-50097,-50129, -50162,-50194,-50226,-50259,-50291,-50323,-50355,-50387, -50420,-50452,-50484,-50516,-50548,-50580,-50612,-50644, -50675,-50707,-50739,-50771,-50803,-50834,-50866,-50898, -50929,-50961,-50993,-51024,-51056,-51087,-51119,-51150, -51182,-51213,-51244,-51276,-51307,-51338,-51369,-51401, -51432,-51463,-51494,-51525,-51556,-51587,-51618,-51649, -51680,-51711,-51742,-51773,-51803,-51834,-51865,-51896, -51926,-51957,-51988,-52018,-52049,-52079,-52110,-52140, -52171,-52201,-52231,-52262,-52292,-52322,-52353,-52383, -52413,-52443,-52473,-52503,-52534,-52564,-52594,-52624, -52653,-52683,-52713,-52743,-52773,-52803,-52832,-52862, -52892,-52922,-52951,-52981,-53010,-53040,-53069,-53099, -53128,-53158,-53187,-53216,-53246,-53275,-53304,-53334, -53363,-53392,-53421,-53450,-53479,-53508,-53537,-53566, -53595,-53624,-53653,-53682,-53711,-53739,-53768,-53797, -53826,-53854,-53883,-53911,-53940,-53969,-53997,-54026, -54054,-54082,-54111,-54139,-54167,-54196,-54224,-54252, -54280,-54308,-54337,-54365,-54393,-54421,-54449,-54477, -54505,-54533,-54560,-54588,-54616,-54644,-54672,-54699, -54727,-54755,-54782,-54810,-54837,-54865,-54892,-54920, -54947,-54974,-55002,-55029,-55056,-55084,-55111,-55138, -55165,-55192,-55219,-55246,-55274,-55300,-55327,-55354, -55381,-55408,-55435,-55462,-55489,-55515,-55542,-55569, -55595,-55622,-55648,-55675,-55701,-55728,-55754,-55781, -55807,-55833,-55860,-55886,-55912,-55938,-55965,-55991, -56017,-56043,-56069,-56095,-56121,-56147,-56173,-56199, -56225,-56250,-56276,-56302,-56328,-56353,-56379,-56404, -56430,-56456,-56481,-56507,-56532,-56557,-56583,-56608, -56633,-56659,-56684,-56709,-56734,-56760,-56785,-56810, -56835,-56860,-56885,-56910,-56935,-56959,-56984,-57009, -57034,-57059,-57083,-57108,-57133,-57157,-57182,-57206, -57231,-57255,-57280,-57304,-57329,-57353,-57377,-57402, -57426,-57450,-57474,-57498,-57522,-57546,-57570,-57594, -57618,-57642,-57666,-57690,-57714,-57738,-57762,-57785, -57809,-57833,-57856,-57880,-57903,-57927,-57950,-57974, -57997,-58021,-58044,-58067,-58091,-58114,-58137,-58160, -58183,-58207,-58230,-58253,-58276,-58299,-58322,-58345, -58367,-58390,-58413,-58436,-58459,-58481,-58504,-58527, -58549,-58572,-58594,-58617,-58639,-58662,-58684,-58706, -58729,-58751,-58773,-58795,-58818,-58840,-58862,-58884, -58906,-58928,-58950,-58972,-58994,-59016,-59038,-59059, -59081,-59103,-59125,-59146,-59168,-59190,-59211,-59233, -59254,-59276,-59297,-59318,-59340,-59361,-59382,-59404, -59425,-59446,-59467,-59488,-59509,-59530,-59551,-59572, -59593,-59614,-59635,-59656,-59677,-59697,-59718,-59739, -59759,-59780,-59801,-59821,-59842,-59862,-59883,-59903, -59923,-59944,-59964,-59984,-60004,-60025,-60045,-60065, -60085,-60105,-60125,-60145,-60165,-60185,-60205,-60225, -60244,-60264,-60284,-60304,-60323,-60343,-60363,-60382, -60402,-60421,-60441,-60460,-60479,-60499,-60518,-60537, -60556,-60576,-60595,-60614,-60633,-60652,-60671,-60690, -60709,-60728,-60747,-60766,-60785,-60803,-60822,-60841, -60859,-60878,-60897,-60915,-60934,-60952,-60971,-60989, -61007,-61026,-61044,-61062,-61081,-61099,-61117,-61135, -61153,-61171,-61189,-61207,-61225,-61243,-61261,-61279, -61297,-61314,-61332,-61350,-61367,-61385,-61403,-61420, -61438,-61455,-61473,-61490,-61507,-61525,-61542,-61559, -61577,-61594,-61611,-61628,-61645,-61662,-61679,-61696, -61713,-61730,-61747,-61764,-61780,-61797,-61814,-61831, -61847,-61864,-61880,-61897,-61913,-61930,-61946,-61963, -61979,-61995,-62012,-62028,-62044,-62060,-62076,-62092, -62108,-62125,-62141,-62156,-62172,-62188,-62204,-62220, -62236,-62251,-62267,-62283,-62298,-62314,-62329,-62345, -62360,-62376,-62391,-62407,-62422,-62437,-62453,-62468, -62483,-62498,-62513,-62528,-62543,-62558,-62573,-62588, -62603,-62618,-62633,-62648,-62662,-62677,-62692,-62706, -62721,-62735,-62750,-62764,-62779,-62793,-62808,-62822, -62836,-62850,-62865,-62879,-62893,-62907,-62921,-62935, -62949,-62963,-62977,-62991,-63005,-63019,-63032,-63046, -63060,-63074,-63087,-63101,-63114,-63128,-63141,-63155, -63168,-63182,-63195,-63208,-63221,-63235,-63248,-63261, -63274,-63287,-63300,-63313,-63326,-63339,-63352,-63365, -63378,-63390,-63403,-63416,-63429,-63441,-63454,-63466, -63479,-63491,-63504,-63516,-63528,-63541,-63553,-63565, -63578,-63590,-63602,-63614,-63626,-63638,-63650,-63662, -63674,-63686,-63698,-63709,-63721,-63733,-63745,-63756, -63768,-63779,-63791,-63803,-63814,-63825,-63837,-63848, -63859,-63871,-63882,-63893,-63904,-63915,-63927,-63938, -63949,-63960,-63971,-63981,-63992,-64003,-64014,-64025, -64035,-64046,-64057,-64067,-64078,-64088,-64099,-64109, -64120,-64130,-64140,-64151,-64161,-64171,-64181,-64192, -64202,-64212,-64222,-64232,-64242,-64252,-64261,-64271, -64281,-64291,-64301,-64310,-64320,-64330,-64339,-64349, -64358,-64368,-64377,-64387,-64396,-64405,-64414,-64424, -64433,-64442,-64451,-64460,-64469,-64478,-64487,-64496, -64505,-64514,-64523,-64532,-64540,-64549,-64558,-64566, -64575,-64584,-64592,-64601,-64609,-64617,-64626,-64634, -64642,-64651,-64659,-64667,-64675,-64683,-64691,-64699, -64707,-64715,-64723,-64731,-64739,-64747,-64754,-64762, -64770,-64777,-64785,-64793,-64800,-64808,-64815,-64822, -64830,-64837,-64844,-64852,-64859,-64866,-64873,-64880, -64887,-64895,-64902,-64908,-64915,-64922,-64929,-64936, -64943,-64949,-64956,-64963,-64969,-64976,-64982,-64989, -64995,-65002,-65008,-65015,-65021,-65027,-65033,-65040, -65046,-65052,-65058,-65064,-65070,-65076,-65082,-65088, -65094,-65099,-65105,-65111,-65117,-65122,-65128,-65133, -65139,-65144,-65150,-65155,-65161,-65166,-65171,-65177, -65182,-65187,-65192,-65197,-65202,-65207,-65212,-65217, -65222,-65227,-65232,-65237,-65242,-65246,-65251,-65256, -65260,-65265,-65270,-65274,-65279,-65283,-65287,-65292, -65296,-65300,-65305,-65309,-65313,-65317,-65321,-65325, -65329,-65333,-65337,-65341,-65345,-65349,-65352,-65356, -65360,-65363,-65367,-65371,-65374,-65378,-65381,-65385, -65388,-65391,-65395,-65398,-65401,-65404,-65408,-65411, -65414,-65417,-65420,-65423,-65426,-65429,-65431,-65434, -65437,-65440,-65442,-65445,-65448,-65450,-65453,-65455, -65458,-65460,-65463,-65465,-65467,-65470,-65472,-65474, -65476,-65478,-65480,-65482,-65484,-65486,-65488,-65490, -65492,-65494,-65496,-65497,-65499,-65501,-65502,-65504, -65505,-65507,-65508,-65510,-65511,-65513,-65514,-65515, -65516,-65518,-65519,-65520,-65521,-65522,-65523,-65524, -65525,-65526,-65527,-65527,-65528,-65529,-65530,-65530, -65531,-65531,-65532,-65532,-65533,-65533,-65534,-65534, -65534,-65535,-65535,-65535,-65535,-65535,-65535,-65535, -65535,-65535,-65535,-65535,-65535,-65535,-65535,-65534, -65534,-65534,-65533,-65533,-65532,-65532,-65531,-65531, -65530,-65530,-65529,-65528,-65527,-65527,-65526,-65525, -65524,-65523,-65522,-65521,-65520,-65519,-65518,-65516, -65515,-65514,-65513,-65511,-65510,-65508,-65507,-65505, -65504,-65502,-65501,-65499,-65497,-65496,-65494,-65492, -65490,-65488,-65486,-65484,-65482,-65480,-65478,-65476, -65474,-65472,-65470,-65467,-65465,-65463,-65460,-65458, -65455,-65453,-65450,-65448,-65445,-65442,-65440,-65437, -65434,-65431,-65429,-65426,-65423,-65420,-65417,-65414, -65411,-65408,-65404,-65401,-65398,-65395,-65391,-65388, -65385,-65381,-65378,-65374,-65371,-65367,-65363,-65360, -65356,-65352,-65349,-65345,-65341,-65337,-65333,-65329, -65325,-65321,-65317,-65313,-65309,-65305,-65300,-65296, -65292,-65287,-65283,-65279,-65274,-65270,-65265,-65260, -65256,-65251,-65246,-65242,-65237,-65232,-65227,-65222, -65217,-65212,-65207,-65202,-65197,-65192,-65187,-65182, -65177,-65171,-65166,-65161,-65155,-65150,-65144,-65139, -65133,-65128,-65122,-65117,-65111,-65105,-65099,-65094, -65088,-65082,-65076,-65070,-65064,-65058,-65052,-65046, -65040,-65033,-65027,-65021,-65015,-65008,-65002,-64995, -64989,-64982,-64976,-64969,-64963,-64956,-64949,-64943, -64936,-64929,-64922,-64915,-64908,-64902,-64895,-64887, -64880,-64873,-64866,-64859,-64852,-64844,-64837,-64830, -64822,-64815,-64808,-64800,-64793,-64785,-64777,-64770, -64762,-64754,-64747,-64739,-64731,-64723,-64715,-64707, -64699,-64691,-64683,-64675,-64667,-64659,-64651,-64642, -64634,-64626,-64617,-64609,-64601,-64592,-64584,-64575, -64566,-64558,-64549,-64540,-64532,-64523,-64514,-64505, -64496,-64487,-64478,-64469,-64460,-64451,-64442,-64433, -64424,-64414,-64405,-64396,-64387,-64377,-64368,-64358, -64349,-64339,-64330,-64320,-64310,-64301,-64291,-64281, -64271,-64261,-64252,-64242,-64232,-64222,-64212,-64202, -64192,-64181,-64171,-64161,-64151,-64140,-64130,-64120, -64109,-64099,-64088,-64078,-64067,-64057,-64046,-64035, -64025,-64014,-64003,-63992,-63981,-63971,-63960,-63949, -63938,-63927,-63915,-63904,-63893,-63882,-63871,-63859, -63848,-63837,-63825,-63814,-63803,-63791,-63779,-63768, -63756,-63745,-63733,-63721,-63709,-63698,-63686,-63674, -63662,-63650,-63638,-63626,-63614,-63602,-63590,-63578, -63565,-63553,-63541,-63528,-63516,-63504,-63491,-63479, -63466,-63454,-63441,-63429,-63416,-63403,-63390,-63378, -63365,-63352,-63339,-63326,-63313,-63300,-63287,-63274, -63261,-63248,-63235,-63221,-63208,-63195,-63182,-63168, -63155,-63141,-63128,-63114,-63101,-63087,-63074,-63060, -63046,-63032,-63019,-63005,-62991,-62977,-62963,-62949, -62935,-62921,-62907,-62893,-62879,-62865,-62850,-62836, -62822,-62808,-62793,-62779,-62764,-62750,-62735,-62721, -62706,-62692,-62677,-62662,-62648,-62633,-62618,-62603, -62588,-62573,-62558,-62543,-62528,-62513,-62498,-62483, -62468,-62453,-62437,-62422,-62407,-62391,-62376,-62360, -62345,-62329,-62314,-62298,-62283,-62267,-62251,-62236, -62220,-62204,-62188,-62172,-62156,-62141,-62125,-62108, -62092,-62076,-62060,-62044,-62028,-62012,-61995,-61979, -61963,-61946,-61930,-61913,-61897,-61880,-61864,-61847, -61831,-61814,-61797,-61780,-61764,-61747,-61730,-61713, -61696,-61679,-61662,-61645,-61628,-61611,-61594,-61577, -61559,-61542,-61525,-61507,-61490,-61473,-61455,-61438, -61420,-61403,-61385,-61367,-61350,-61332,-61314,-61297, -61279,-61261,-61243,-61225,-61207,-61189,-61171,-61153, -61135,-61117,-61099,-61081,-61062,-61044,-61026,-61007, -60989,-60971,-60952,-60934,-60915,-60897,-60878,-60859, -60841,-60822,-60803,-60785,-60766,-60747,-60728,-60709, -60690,-60671,-60652,-60633,-60614,-60595,-60576,-60556, -60537,-60518,-60499,-60479,-60460,-60441,-60421,-60402, -60382,-60363,-60343,-60323,-60304,-60284,-60264,-60244, -60225,-60205,-60185,-60165,-60145,-60125,-60105,-60085, -60065,-60045,-60025,-60004,-59984,-59964,-59944,-59923, -59903,-59883,-59862,-59842,-59821,-59801,-59780,-59759, -59739,-59718,-59697,-59677,-59656,-59635,-59614,-59593, -59572,-59551,-59530,-59509,-59488,-59467,-59446,-59425, -59404,-59382,-59361,-59340,-59318,-59297,-59276,-59254, -59233,-59211,-59189,-59168,-59146,-59125,-59103,-59081, -59059,-59038,-59016,-58994,-58972,-58950,-58928,-58906, -58884,-58862,-58840,-58818,-58795,-58773,-58751,-58729, -58706,-58684,-58662,-58639,-58617,-58594,-58572,-58549, -58527,-58504,-58481,-58459,-58436,-58413,-58390,-58367, -58345,-58322,-58299,-58276,-58253,-58230,-58207,-58183, -58160,-58137,-58114,-58091,-58067,-58044,-58021,-57997, -57974,-57950,-57927,-57903,-57880,-57856,-57833,-57809, -57785,-57762,-57738,-57714,-57690,-57666,-57642,-57618, -57594,-57570,-57546,-57522,-57498,-57474,-57450,-57426, -57402,-57377,-57353,-57329,-57304,-57280,-57255,-57231, -57206,-57182,-57157,-57133,-57108,-57083,-57059,-57034, -57009,-56984,-56959,-56935,-56910,-56885,-56860,-56835, -56810,-56785,-56760,-56734,-56709,-56684,-56659,-56633, -56608,-56583,-56557,-56532,-56507,-56481,-56456,-56430, -56404,-56379,-56353,-56328,-56302,-56276,-56250,-56225, -56199,-56173,-56147,-56121,-56095,-56069,-56043,-56017, -55991,-55965,-55938,-55912,-55886,-55860,-55833,-55807, -55781,-55754,-55728,-55701,-55675,-55648,-55622,-55595, -55569,-55542,-55515,-55489,-55462,-55435,-55408,-55381, -55354,-55327,-55300,-55274,-55246,-55219,-55192,-55165, -55138,-55111,-55084,-55056,-55029,-55002,-54974,-54947, -54920,-54892,-54865,-54837,-54810,-54782,-54755,-54727, -54699,-54672,-54644,-54616,-54588,-54560,-54533,-54505, -54477,-54449,-54421,-54393,-54365,-54337,-54308,-54280, -54252,-54224,-54196,-54167,-54139,-54111,-54082,-54054, -54026,-53997,-53969,-53940,-53911,-53883,-53854,-53826, -53797,-53768,-53739,-53711,-53682,-53653,-53624,-53595, -53566,-53537,-53508,-53479,-53450,-53421,-53392,-53363, -53334,-53304,-53275,-53246,-53216,-53187,-53158,-53128, -53099,-53069,-53040,-53010,-52981,-52951,-52922,-52892, -52862,-52832,-52803,-52773,-52743,-52713,-52683,-52653, -52624,-52594,-52564,-52534,-52503,-52473,-52443,-52413, -52383,-52353,-52322,-52292,-52262,-52231,-52201,-52171, -52140,-52110,-52079,-52049,-52018,-51988,-51957,-51926, -51896,-51865,-51834,-51803,-51773,-51742,-51711,-51680, -51649,-51618,-51587,-51556,-51525,-51494,-51463,-51432, -51401,-51369,-51338,-51307,-51276,-51244,-51213,-51182, -51150,-51119,-51087,-51056,-51024,-50993,-50961,-50929, -50898,-50866,-50834,-50803,-50771,-50739,-50707,-50675, -50644,-50612,-50580,-50548,-50516,-50484,-50452,-50420, -50387,-50355,-50323,-50291,-50259,-50226,-50194,-50162, -50129,-50097,-50065,-50032,-50000,-49967,-49935,-49902, -49869,-49837,-49804,-49771,-49739,-49706,-49673,-49640, -49608,-49575,-49542,-49509,-49476,-49443,-49410,-49377, -49344,-49311,-49278,-49244,-49211,-49178,-49145,-49112, -49078,-49045,-49012,-48978,-48945,-48911,-48878,-48844, -48811,-48777,-48744,-48710,-48676,-48643,-48609,-48575, -48542,-48508,-48474,-48440,-48406,-48372,-48338,-48305, -48271,-48237,-48202,-48168,-48134,-48100,-48066,-48032, -47998,-47963,-47929,-47895,-47860,-47826,-47792,-47757, -47723,-47688,-47654,-47619,-47585,-47550,-47516,-47481, -47446,-47412,-47377,-47342,-47307,-47273,-47238,-47203, -47168,-47133,-47098,-47063,-47028,-46993,-46958,-46923, -46888,-46853,-46818,-46783,-46747,-46712,-46677,-46642, -46606,-46571,-46536,-46500,-46465,-46429,-46394,-46358, -46323,-46287,-46251,-46216,-46180,-46145,-46109,-46073, -46037,-46002,-45966,-45930,-45894,-45858,-45822,-45786, -45750,-45714,-45678,-45642,-45606,-45570,-45534,-45498, -45462,-45425,-45389,-45353,-45316,-45280,-45244,-45207, -45171,-45135,-45098,-45062,-45025,-44989,-44952,-44915, -44879,-44842,-44806,-44769,-44732,-44695,-44659,-44622, -44585,-44548,-44511,-44474,-44437,-44400,-44363,-44326, -44289,-44252,-44215,-44178,-44141,-44104,-44067,-44029, -43992,-43955,-43918,-43880,-43843,-43806,-43768,-43731, -43693,-43656,-43618,-43581,-43543,-43506,-43468,-43430, -43393,-43355,-43317,-43280,-43242,-43204,-43166,-43128, -43091,-43053,-43015,-42977,-42939,-42901,-42863,-42825, -42787,-42749,-42711,-42672,-42634,-42596,-42558,-42520, -42481,-42443,-42405,-42366,-42328,-42290,-42251,-42213, -42174,-42136,-42097,-42059,-42020,-41982,-41943,-41904, -41866,-41827,-41788,-41750,-41711,-41672,-41633,-41595, -41556,-41517,-41478,-41439,-41400,-41361,-41322,-41283, -41244,-41205,-41166,-41127,-41087,-41048,-41009,-40970, -40931,-40891,-40852,-40813,-40773,-40734,-40695,-40655, -40616,-40576,-40537,-40497,-40458,-40418,-40379,-40339, -40299,-40260,-40220,-40180,-40141,-40101,-40061,-40021, -39982,-39942,-39902,-39862,-39822,-39782,-39742,-39702, -39662,-39622,-39582,-39542,-39502,-39462,-39422,-39382, -39341,-39301,-39261,-39221,-39180,-39140,-39100,-39059, -39019,-38979,-38938,-38898,-38857,-38817,-38776,-38736, -38695,-38655,-38614,-38573,-38533,-38492,-38451,-38411, -38370,-38329,-38288,-38248,-38207,-38166,-38125,-38084, -38043,-38002,-37961,-37920,-37879,-37838,-37797,-37756, -37715,-37674,-37633,-37592,-37550,-37509,-37468,-37427, -37386,-37344,-37303,-37262,-37220,-37179,-37137,-37096, -37055,-37013,-36972,-36930,-36889,-36847,-36805,-36764, -36722,-36681,-36639,-36597,-36556,-36514,-36472,-36430, -36388,-36347,-36305,-36263,-36221,-36179,-36137,-36095, -36053,-36011,-35969,-35927,-35885,-35843,-35801,-35759, -35717,-35675,-35633,-35590,-35548,-35506,-35464,-35421, -35379,-35337,-35294,-35252,-35210,-35167,-35125,-35082, -35040,-34997,-34955,-34912,-34870,-34827,-34785,-34742, -34699,-34657,-34614,-34571,-34529,-34486,-34443,-34400, -34358,-34315,-34272,-34229,-34186,-34143,-34100,-34057, -34015,-33972,-33929,-33886,-33843,-33799,-33756,-33713, -33670,-33627,-33584,-33541,-33498,-33454,-33411,-33368, -33325,-33281,-33238,-33195,-33151,-33108,-33065,-33021, -32978,-32934,-32891,-32847,-32804,-32760,-32717,-32673, -32630,-32586,-32542,-32499,-32455,-32411,-32368,-32324, -32280,-32236,-32193,-32149,-32105,-32061,-32017,-31974, -31930,-31886,-31842,-31798,-31754,-31710,-31666,-31622, -31578,-31534,-31490,-31446,-31402,-31357,-31313,-31269, -31225,-31181,-31136,-31092,-31048,-31004,-30959,-30915, -30871,-30826,-30782,-30738,-30693,-30649,-30604,-30560, -30515,-30471,-30426,-30382,-30337,-30293,-30248,-30204, -30159,-30114,-30070,-30025,-29980,-29936,-29891,-29846, -29801,-29757,-29712,-29667,-29622,-29577,-29533,-29488, -29443,-29398,-29353,-29308,-29263,-29218,-29173,-29128, -29083,-29038,-28993,-28948,-28903,-28858,-28812,-28767, -28722,-28677,-28632,-28586,-28541,-28496,-28451,-28405, -28360,-28315,-28269,-28224,-28179,-28133,-28088,-28042, -27997,-27952,-27906,-27861,-27815,-27770,-27724,-27678, -27633,-27587,-27542,-27496,-27450,-27405,-27359,-27313, -27268,-27222,-27176,-27131,-27085,-27039,-26993,-26947, -26902,-26856,-26810,-26764,-26718,-26672,-26626,-26580, -26534,-26488,-26442,-26396,-26350,-26304,-26258,-26212, -26166,-26120,-26074,-26028,-25982,-25936,-25889,-25843, -25797,-25751,-25705,-25658,-25612,-25566,-25520,-25473, -25427,-25381,-25334,-25288,-25241,-25195,-25149,-25102, -25056,-25009,-24963,-24916,-24870,-24823,-24777,-24730, -24684,-24637,-24591,-24544,-24497,-24451,-24404,-24357, -24311,-24264,-24217,-24171,-24124,-24077,-24030,-23984, -23937,-23890,-23843,-23796,-23750,-23703,-23656,-23609, -23562,-23515,-23468,-23421,-23374,-23327,-23280,-23233, -23186,-23139,-23092,-23045,-22998,-22951,-22904,-22857, -22810,-22763,-22716,-22668,-22621,-22574,-22527,-22480, -22432,-22385,-22338,-22291,-22243,-22196,-22149,-22102, -22054,-22007,-21960,-21912,-21865,-21817,-21770,-21723, -21675,-21628,-21580,-21533,-21485,-21438,-21390,-21343, -21295,-21248,-21200,-21153,-21105,-21057,-21010,-20962, -20915,-20867,-20819,-20772,-20724,-20676,-20629,-20581, -20533,-20485,-20438,-20390,-20342,-20294,-20246,-20199, -20151,-20103,-20055,-20007,-19959,-19912,-19864,-19816, -19768,-19720,-19672,-19624,-19576,-19528,-19480,-19432, -19384,-19336,-19288,-19240,-19192,-19144,-19096,-19048, -19000,-18951,-18903,-18855,-18807,-18759,-18711,-18663, -18614,-18566,-18518,-18470,-18421,-18373,-18325,-18277, -18228,-18180,-18132,-18084,-18035,-17987,-17939,-17890, -17842,-17793,-17745,-17697,-17648,-17600,-17551,-17503, -17455,-17406,-17358,-17309,-17261,-17212,-17164,-17115, -17067,-17018,-16970,-16921,-16872,-16824,-16775,-16727, -16678,-16629,-16581,-16532,-16484,-16435,-16386,-16338, -16289,-16240,-16191,-16143,-16094,-16045,-15997,-15948, -15899,-15850,-15802,-15753,-15704,-15655,-15606,-15557, -15509,-15460,-15411,-15362,-15313,-15264,-15215,-15167, -15118,-15069,-15020,-14971,-14922,-14873,-14824,-14775, -14726,-14677,-14628,-14579,-14530,-14481,-14432,-14383, -14334,-14285,-14236,-14187,-14138,-14089,-14040,-13990, -13941,-13892,-13843,-13794,-13745,-13696,-13647,-13597, -13548,-13499,-13450,-13401,-13351,-13302,-13253,-13204, -13154,-13105,-13056,-13007,-12957,-12908,-12859,-12810, -12760,-12711,-12662,-12612,-12563,-12514,-12464,-12415, -12366,-12316,-12267,-12217,-12168,-12119,-12069,-12020, -11970,-11921,-11872,-11822,-11773,-11723,-11674,-11624, -11575,-11525,-11476,-11426,-11377,-11327,-11278,-11228, -11179,-11129,-11080,-11030,-10981,-10931,-10882,-10832, -10782,-10733,-10683,-10634,-10584,-10534,-10485,-10435, -10386,-10336,-10286,-10237,-10187,-10137,-10088,-10038, -9988,-9939,-9889,-9839,-9790,-9740,-9690,-9640, -9591,-9541,-9491,-9442,-9392,-9342,-9292,-9243, -9193,-9143,-9093,-9043,-8994,-8944,-8894,-8844, -8794,-8745,-8695,-8645,-8595,-8545,-8496,-8446, -8396,-8346,-8296,-8246,-8196,-8147,-8097,-8047, -7997,-7947,-7897,-7847,-7797,-7747,-7697,-7648, -7598,-7548,-7498,-7448,-7398,-7348,-7298,-7248, -7198,-7148,-7098,-7048,-6998,-6948,-6898,-6848, -6798,-6748,-6698,-6648,-6598,-6548,-6498,-6448, -6398,-6348,-6298,-6248,-6198,-6148,-6098,-6048, -5998,-5948,-5898,-5848,-5798,-5747,-5697,-5647, -5597,-5547,-5497,-5447,-5397,-5347,-5297,-5247, -5197,-5146,-5096,-5046,-4996,-4946,-4896,-4846, -4796,-4745,-4695,-4645,-4595,-4545,-4495,-4445, -4394,-4344,-4294,-4244,-4194,-4144,-4093,-4043, -3993,-3943,-3893,-3843,-3792,-3742,-3692,-3642, -3592,-3541,-3491,-3441,-3391,-3341,-3291,-3240, -3190,-3140,-3090,-3039,-2989,-2939,-2889,-2839, -2788,-2738,-2688,-2638,-2588,-2537,-2487,-2437, -2387,-2336,-2286,-2236,-2186,-2135,-2085,-2035, -1985,-1934,-1884,-1834,-1784,-1733,-1683,-1633, -1583,-1532,-1482,-1432,-1382,-1331,-1281,-1231, -1181,-1130,-1080,-1030,-980,-929,-879,-829, -779,-728,-678,-628,-578,-527,-477,-427, -376,-326,-276,-226,-175,-125,-75,-25, 25,75,125,175,226,276,326,376, 427,477,527,578,628,678,728,779, 829,879,929,980,1030,1080,1130,1181, 1231,1281,1331,1382,1432,1482,1532,1583, 1633,1683,1733,1784,1834,1884,1934,1985, 2035,2085,2135,2186,2236,2286,2336,2387, 2437,2487,2537,2587,2638,2688,2738,2788, 2839,2889,2939,2989,3039,3090,3140,3190, 3240,3291,3341,3391,3441,3491,3542,3592, 3642,3692,3742,3792,3843,3893,3943,3993, 4043,4093,4144,4194,4244,4294,4344,4394, 4445,4495,4545,4595,4645,4695,4745,4796, 4846,4896,4946,4996,5046,5096,5146,5197, 5247,5297,5347,5397,5447,5497,5547,5597, 5647,5697,5747,5798,5848,5898,5948,5998, 6048,6098,6148,6198,6248,6298,6348,6398, 6448,6498,6548,6598,6648,6698,6748,6798, 6848,6898,6948,6998,7048,7098,7148,7198, 7248,7298,7348,7398,7448,7498,7548,7598, 7648,7697,7747,7797,7847,7897,7947,7997, 8047,8097,8147,8196,8246,8296,8346,8396, 8446,8496,8545,8595,8645,8695,8745,8794, 8844,8894,8944,8994,9043,9093,9143,9193, 9243,9292,9342,9392,9442,9491,9541,9591, 9640,9690,9740,9790,9839,9889,9939,9988, 10038,10088,10137,10187,10237,10286,10336,10386, 10435,10485,10534,10584,10634,10683,10733,10782, 10832,10882,10931,10981,11030,11080,11129,11179, 11228,11278,11327,11377,11426,11476,11525,11575, 11624,11674,11723,11773,11822,11872,11921,11970, 12020,12069,12119,12168,12218,12267,12316,12366, 12415,12464,12514,12563,12612,12662,12711,12760, 12810,12859,12908,12957,13007,13056,13105,13154, 13204,13253,13302,13351,13401,13450,13499,13548, 13597,13647,13696,13745,13794,13843,13892,13941, 13990,14040,14089,14138,14187,14236,14285,14334, 14383,14432,14481,14530,14579,14628,14677,14726, 14775,14824,14873,14922,14971,15020,15069,15118, 15167,15215,15264,15313,15362,15411,15460,15509, 15557,15606,15655,15704,15753,15802,15850,15899, 15948,15997,16045,16094,16143,16191,16240,16289, 16338,16386,16435,16484,16532,16581,16629,16678, 16727,16775,16824,16872,16921,16970,17018,17067, 17115,17164,17212,17261,17309,17358,17406,17455, 17503,17551,17600,17648,17697,17745,17793,17842, 17890,17939,17987,18035,18084,18132,18180,18228, 18277,18325,18373,18421,18470,18518,18566,18614, 18663,18711,18759,18807,18855,18903,18951,19000, 19048,19096,19144,19192,19240,19288,19336,19384, 19432,19480,19528,19576,19624,19672,19720,19768, 19816,19864,19912,19959,20007,20055,20103,20151, 20199,20246,20294,20342,20390,20438,20485,20533, 20581,20629,20676,20724,20772,20819,20867,20915, 20962,21010,21057,21105,21153,21200,21248,21295, 21343,21390,21438,21485,21533,21580,21628,21675, 21723,21770,21817,21865,21912,21960,22007,22054, 22102,22149,22196,22243,22291,22338,22385,22432, 22480,22527,22574,22621,22668,22716,22763,22810, 22857,22904,22951,22998,23045,23092,23139,23186, 23233,23280,23327,23374,23421,23468,23515,23562, 23609,23656,23703,23750,23796,23843,23890,23937, 23984,24030,24077,24124,24171,24217,24264,24311, 24357,24404,24451,24497,24544,24591,24637,24684, 24730,24777,24823,24870,24916,24963,25009,25056, 25102,25149,25195,25241,25288,25334,25381,25427, 25473,25520,25566,25612,25658,25705,25751,25797, 25843,25889,25936,25982,26028,26074,26120,26166, 26212,26258,26304,26350,26396,26442,26488,26534, 26580,26626,26672,26718,26764,26810,26856,26902, 26947,26993,27039,27085,27131,27176,27222,27268, 27313,27359,27405,27450,27496,27542,27587,27633, 27678,27724,27770,27815,27861,27906,27952,27997, 28042,28088,28133,28179,28224,28269,28315,28360, 28405,28451,28496,28541,28586,28632,28677,28722, 28767,28812,28858,28903,28948,28993,29038,29083, 29128,29173,29218,29263,29308,29353,29398,29443, 29488,29533,29577,29622,29667,29712,29757,29801, 29846,29891,29936,29980,30025,30070,30114,30159, 30204,30248,30293,30337,30382,30427,30471,30516, 30560,30604,30649,30693,30738,30782,30826,30871, 30915,30959,31004,31048,31092,31136,31181,31225, 31269,31313,31357,31402,31446,31490,31534,31578, 31622,31666,31710,31754,31798,31842,31886,31930, 31974,32017,32061,32105,32149,32193,32236,32280, 32324,32368,32411,32455,32499,32542,32586,32630, 32673,32717,32760,32804,32847,32891,32934,32978, 33021,33065,33108,33151,33195,33238,33281,33325, 33368,33411,33454,33498,33541,33584,33627,33670, 33713,33756,33799,33843,33886,33929,33972,34015, 34057,34100,34143,34186,34229,34272,34315,34358, 34400,34443,34486,34529,34571,34614,34657,34699, 34742,34785,34827,34870,34912,34955,34997,35040, 35082,35125,35167,35210,35252,35294,35337,35379, 35421,35464,35506,35548,35590,35633,35675,35717, 35759,35801,35843,35885,35927,35969,36011,36053, 36095,36137,36179,36221,36263,36305,36347,36388, 36430,36472,36514,36556,36597,36639,36681,36722, 36764,36805,36847,36889,36930,36972,37013,37055, 37096,37137,37179,37220,37262,37303,37344,37386, 37427,37468,37509,37551,37592,37633,37674,37715, 37756,37797,37838,37879,37920,37961,38002,38043, 38084,38125,38166,38207,38248,38288,38329,38370, 38411,38451,38492,38533,38573,38614,38655,38695, 38736,38776,38817,38857,38898,38938,38979,39019, 39059,39100,39140,39180,39221,39261,39301,39341, 39382,39422,39462,39502,39542,39582,39622,39662, 39702,39742,39782,39822,39862,39902,39942,39982, 40021,40061,40101,40141,40180,40220,40260,40299, 40339,40379,40418,40458,40497,40537,40576,40616, 40655,40695,40734,40773,40813,40852,40891,40931, 40970,41009,41048,41087,41127,41166,41205,41244, 41283,41322,41361,41400,41439,41478,41517,41556, 41595,41633,41672,41711,41750,41788,41827,41866, 41904,41943,41982,42020,42059,42097,42136,42174, 42213,42251,42290,42328,42366,42405,42443,42481, 42520,42558,42596,42634,42672,42711,42749,42787, 42825,42863,42901,42939,42977,43015,43053,43091, 43128,43166,43204,43242,43280,43317,43355,43393, 43430,43468,43506,43543,43581,43618,43656,43693, 43731,43768,43806,43843,43880,43918,43955,43992, 44029,44067,44104,44141,44178,44215,44252,44289, 44326,44363,44400,44437,44474,44511,44548,44585, 44622,44659,44695,44732,44769,44806,44842,44879, 44915,44952,44989,45025,45062,45098,45135,45171, 45207,45244,45280,45316,45353,45389,45425,45462, 45498,45534,45570,45606,45642,45678,45714,45750, 45786,45822,45858,45894,45930,45966,46002,46037, 46073,46109,46145,46180,46216,46252,46287,46323, 46358,46394,46429,46465,46500,46536,46571,46606, 46642,46677,46712,46747,46783,46818,46853,46888, 46923,46958,46993,47028,47063,47098,47133,47168, 47203,47238,47273,47308,47342,47377,47412,47446, 47481,47516,47550,47585,47619,47654,47688,47723, 47757,47792,47826,47861,47895,47929,47963,47998, 48032,48066,48100,48134,48168,48202,48237,48271, 48305,48338,48372,48406,48440,48474,48508,48542, 48575,48609,48643,48676,48710,48744,48777,48811, 48844,48878,48911,48945,48978,49012,49045,49078, 49112,49145,49178,49211,49244,49278,49311,49344, 49377,49410,49443,49476,49509,49542,49575,49608, 49640,49673,49706,49739,49771,49804,49837,49869, 49902,49935,49967,50000,50032,50064,50097,50129, 50162,50194,50226,50259,50291,50323,50355,50387, 50420,50452,50484,50516,50548,50580,50612,50644, 50675,50707,50739,50771,50803,50834,50866,50898, 50929,50961,50993,51024,51056,51087,51119,51150, 51182,51213,51244,51276,51307,51338,51369,51401, 51432,51463,51494,51525,51556,51587,51618,51649, 51680,51711,51742,51773,51803,51834,51865,51896, 51926,51957,51988,52018,52049,52079,52110,52140, 52171,52201,52231,52262,52292,52322,52353,52383, 52413,52443,52473,52503,52534,52564,52594,52624, 52653,52683,52713,52743,52773,52803,52832,52862, 52892,52922,52951,52981,53010,53040,53069,53099, 53128,53158,53187,53216,53246,53275,53304,53334, 53363,53392,53421,53450,53479,53508,53537,53566, 53595,53624,53653,53682,53711,53739,53768,53797, 53826,53854,53883,53912,53940,53969,53997,54026, 54054,54082,54111,54139,54167,54196,54224,54252, 54280,54309,54337,54365,54393,54421,54449,54477, 54505,54533,54560,54588,54616,54644,54672,54699, 54727,54755,54782,54810,54837,54865,54892,54920, 54947,54974,55002,55029,55056,55084,55111,55138, 55165,55192,55219,55246,55274,55300,55327,55354, 55381,55408,55435,55462,55489,55515,55542,55569, 55595,55622,55648,55675,55701,55728,55754,55781, 55807,55833,55860,55886,55912,55938,55965,55991, 56017,56043,56069,56095,56121,56147,56173,56199, 56225,56250,56276,56302,56328,56353,56379,56404, 56430,56456,56481,56507,56532,56557,56583,56608, 56633,56659,56684,56709,56734,56760,56785,56810, 56835,56860,56885,56910,56935,56959,56984,57009, 57034,57059,57083,57108,57133,57157,57182,57206, 57231,57255,57280,57304,57329,57353,57377,57402, 57426,57450,57474,57498,57522,57546,57570,57594, 57618,57642,57666,57690,57714,57738,57762,57785, 57809,57833,57856,57880,57903,57927,57950,57974, 57997,58021,58044,58067,58091,58114,58137,58160, 58183,58207,58230,58253,58276,58299,58322,58345, 58367,58390,58413,58436,58459,58481,58504,58527, 58549,58572,58594,58617,58639,58662,58684,58706, 58729,58751,58773,58795,58818,58840,58862,58884, 58906,58928,58950,58972,58994,59016,59038,59059, 59081,59103,59125,59146,59168,59190,59211,59233, 59254,59276,59297,59318,59340,59361,59382,59404, 59425,59446,59467,59488,59509,59530,59551,59572, 59593,59614,59635,59656,59677,59697,59718,59739, 59759,59780,59801,59821,59842,59862,59883,59903, 59923,59944,59964,59984,60004,60025,60045,60065, 60085,60105,60125,60145,60165,60185,60205,60225, 60244,60264,60284,60304,60323,60343,60363,60382, 60402,60421,60441,60460,60479,60499,60518,60537, 60556,60576,60595,60614,60633,60652,60671,60690, 60709,60728,60747,60766,60785,60803,60822,60841, 60859,60878,60897,60915,60934,60952,60971,60989, 61007,61026,61044,61062,61081,61099,61117,61135, 61153,61171,61189,61207,61225,61243,61261,61279, 61297,61314,61332,61350,61367,61385,61403,61420, 61438,61455,61473,61490,61507,61525,61542,61559, 61577,61594,61611,61628,61645,61662,61679,61696, 61713,61730,61747,61764,61780,61797,61814,61831, 61847,61864,61880,61897,61913,61930,61946,61963, 61979,61995,62012,62028,62044,62060,62076,62092, 62108,62125,62141,62156,62172,62188,62204,62220, 62236,62251,62267,62283,62298,62314,62329,62345, 62360,62376,62391,62407,62422,62437,62453,62468, 62483,62498,62513,62528,62543,62558,62573,62588, 62603,62618,62633,62648,62662,62677,62692,62706, 62721,62735,62750,62764,62779,62793,62808,62822, 62836,62850,62865,62879,62893,62907,62921,62935, 62949,62963,62977,62991,63005,63019,63032,63046, 63060,63074,63087,63101,63114,63128,63141,63155, 63168,63182,63195,63208,63221,63235,63248,63261, 63274,63287,63300,63313,63326,63339,63352,63365, 63378,63390,63403,63416,63429,63441,63454,63466, 63479,63491,63504,63516,63528,63541,63553,63565, 63578,63590,63602,63614,63626,63638,63650,63662, 63674,63686,63698,63709,63721,63733,63745,63756, 63768,63779,63791,63803,63814,63825,63837,63848, 63859,63871,63882,63893,63904,63915,63927,63938, 63949,63960,63971,63981,63992,64003,64014,64025, 64035,64046,64057,64067,64078,64088,64099,64109, 64120,64130,64140,64151,64161,64171,64181,64192, 64202,64212,64222,64232,64242,64252,64261,64271, 64281,64291,64301,64310,64320,64330,64339,64349, 64358,64368,64377,64387,64396,64405,64414,64424, 64433,64442,64451,64460,64469,64478,64487,64496, 64505,64514,64523,64532,64540,64549,64558,64566, 64575,64584,64592,64600,64609,64617,64626,64634, 64642,64651,64659,64667,64675,64683,64691,64699, 64707,64715,64723,64731,64739,64747,64754,64762, 64770,64777,64785,64793,64800,64808,64815,64822, 64830,64837,64844,64852,64859,64866,64873,64880, 64887,64895,64902,64908,64915,64922,64929,64936, 64943,64949,64956,64963,64969,64976,64982,64989, 64995,65002,65008,65015,65021,65027,65033,65040, 65046,65052,65058,65064,65070,65076,65082,65088, 65094,65099,65105,65111,65117,65122,65128,65133, 65139,65144,65150,65155,65161,65166,65171,65177, 65182,65187,65192,65197,65202,65207,65212,65217, 65222,65227,65232,65237,65242,65246,65251,65256, 65260,65265,65270,65274,65279,65283,65287,65292, 65296,65300,65305,65309,65313,65317,65321,65325, 65329,65333,65337,65341,65345,65349,65352,65356, 65360,65363,65367,65371,65374,65378,65381,65385, 65388,65391,65395,65398,65401,65404,65408,65411, 65414,65417,65420,65423,65426,65429,65431,65434, 65437,65440,65442,65445,65448,65450,65453,65455, 65458,65460,65463,65465,65467,65470,65472,65474, 65476,65478,65480,65482,65484,65486,65488,65490, 65492,65494,65496,65497,65499,65501,65502,65504, 65505,65507,65508,65510,65511,65513,65514,65515, 65516,65518,65519,65520,65521,65522,65523,65524, 65525,65526,65527,65527,65528,65529,65530,65530, 65531,65531,65532,65532,65533,65533,65534,65534, 65534,65535,65535,65535,65535,65535,65535,65535 }; fixed_t *fineCosine = &finesine[FINEANGLES / 4]; int tantoangle[2049] = { 0,333772,667544,1001315,1335086,1668857,2002626,2336395, 2670163,3003929,3337694,3671457,4005219,4338979,4672736,5006492, 5340245,5673995,6007743,6341488,6675230,7008968,7342704,7676435, 8010164,8343888,8677609,9011325,9345037,9678744,10012447,10346145, 10679838,11013526,11347209,11680887,12014558,12348225,12681885,13015539, 13349187,13682829,14016464,14350092,14683714,15017328,15350936,15684536, 16018129,16351714,16685291,17018860,17352422,17685974,18019518,18353054, 18686582,19020100,19353610,19687110,20020600,20354080,20687552,21021014, 21354466,21687906,22021338,22354758,22688168,23021568,23354956,23688332, 24021698,24355052,24688396,25021726,25355046,25688352,26021648,26354930, 26688200,27021456,27354702,27687932,28021150,28354356,28687548,29020724, 29353888,29687038,30020174,30353296,30686404,31019496,31352574,31685636, 32018684,32351718,32684734,33017736,33350722,33683692,34016648,34349584, 34682508,35015412,35348300,35681172,36014028,36346868,36679688,37012492, 37345276,37678044,38010792,38343524,38676240,39008936,39341612,39674272, 40006912,40339532,40672132,41004716,41337276,41669820,42002344,42334848, 42667332,42999796,43332236,43664660,43997060,44329444,44661800,44994140, 45326456,45658752,45991028,46323280,46655512,46987720,47319908,47652072, 47984212,48316332,48648428,48980500,49312548,49644576,49976580,50308556, 50640512,50972444,51304352,51636236,51968096,52299928,52631740,52963524, 53295284,53627020,53958728,54290412,54622068,54953704,55285308,55616888, 55948444,56279972,56611472,56942948,57274396,57605816,57937212,58268576, 58599916,58931228,59262512,59593768,59924992,60256192,60587364,60918508, 61249620,61580704,61911760,62242788,62573788,62904756,63235692,63566604, 63897480,64228332,64559148,64889940,65220696,65551424,65882120,66212788, 66543420,66874024,67204600,67535136,67865648,68196120,68526568,68856984, 69187360,69517712,69848024,70178304,70508560,70838776,71168960,71499112, 71829224,72159312,72489360,72819376,73149360,73479304,73809216,74139096, 74468936,74798744,75128520,75458264,75787968,76117632,76447264,76776864, 77106424,77435952,77765440,78094888,78424304,78753688,79083032,79412336, 79741608,80070840,80400032,80729192,81058312,81387392,81716432,82045440, 82374408,82703336,83032224,83361080,83689896,84018664,84347400,84676096, 85004760,85333376,85661952,85990488,86318984,86647448,86975864,87304240, 87632576,87960872,88289128,88617344,88945520,89273648,89601736,89929792, 90257792,90585760,90913688,91241568,91569408,91897200,92224960,92552672, 92880336,93207968,93535552,93863088,94190584,94518040,94845448,95172816, 95500136,95827416,96154648,96481832,96808976,97136080,97463136,97790144, 98117112,98444032,98770904,99097736,99424520,99751256,100077944,100404592, 100731192,101057744,101384248,101710712,102037128,102363488,102689808,103016080, 103342312,103668488,103994616,104320696,104646736,104972720,105298656,105624552, 105950392,106276184,106601928,106927624,107253272,107578872,107904416,108229920, 108555368,108880768,109206120,109531416,109856664,110181872,110507016,110832120, 111157168,111482168,111807112,112132008,112456856,112781648,113106392,113431080, 113755720,114080312,114404848,114729328,115053760,115378136,115702464,116026744, 116350960,116675128,116999248,117323312,117647320,117971272,118295176,118619024, 118942816,119266560,119590248,119913880,120237456,120560984,120884456,121207864, 121531224,121854528,122177784,122500976,122824112,123147200,123470224,123793200, 124116120,124438976,124761784,125084528,125407224,125729856,126052432,126374960, 126697424,127019832,127342184,127664472,127986712,128308888,128631008,128953072, 129275080,129597024,129918912,130240744,130562520,130884232,131205888,131527480, 131849016,132170496,132491912,132813272,133134576,133455816,133776992,134098120, 134419184,134740176,135061120,135382000,135702816,136023584,136344272,136664912, 136985488,137306016,137626464,137946864,138267184,138587456,138907664,139227808, 139547904,139867920,140187888,140507776,140827616,141147392,141467104,141786752, 142106336,142425856,142745312,143064720,143384048,143703312,144022512,144341664, 144660736,144979744,145298704,145617584,145936400,146255168,146573856,146892480, 147211040,147529536,147847968,148166336,148484640,148802880,149121056,149439152, 149757200,150075168,150393072,150710912,151028688,151346400,151664048,151981616, 152299136,152616576,152933952,153251264,153568496,153885680,154202784,154519824, 154836784,155153696,155470528,155787296,156104000,156420624,156737200,157053696, 157370112,157686480,158002768,158318976,158635136,158951216,159267232,159583168, 159899040,160214848,160530592,160846256,161161840,161477376,161792832,162108208, 162423520,162738768,163053952,163369040,163684080,163999040,164313936,164628752, 164943504,165258176,165572784,165887312,166201776,166516160,166830480,167144736, 167458912,167773008,168087040,168400992,168714880,169028688,169342432,169656096, 169969696,170283216,170596672,170910032,171223344,171536576,171849728,172162800, 172475808,172788736,173101600,173414384,173727104,174039728,174352288,174664784, 174977200,175289536,175601792,175913984,176226096,176538144,176850096,177161984, 177473792,177785536,178097200,178408784,178720288,179031728,179343088,179654368, 179965568,180276704,180587744,180898720,181209616,181520448,181831184,182141856, 182452448,182762960,183073408,183383760,183694048,184004240,184314368,184624416, 184934400,185244288,185554096,185863840,186173504,186483072,186792576,187102000, 187411344,187720608,188029808,188338912,188647936,188956896,189265760,189574560, 189883264,190191904,190500448,190808928,191117312,191425632,191733872,192042016, 192350096,192658096,192966000,193273840,193581584,193889264,194196848,194504352, 194811792,195119136,195426400,195733584,196040688,196347712,196654656,196961520, 197268304,197574992,197881616,198188144,198494592,198800960,199107248,199413456, 199719584,200025616,200331584,200637456,200943248,201248960,201554576,201860128, 202165584,202470960,202776256,203081456,203386592,203691632,203996592,204301472, 204606256,204910976,205215600,205520144,205824592,206128960,206433248,206737456, 207041584,207345616,207649568,207953424,208257216,208560912,208864512,209168048, 209471488,209774832,210078112,210381296,210684384,210987408,211290336,211593184, 211895936,212198608,212501184,212803680,213106096,213408432,213710672,214012816, 214314880,214616864,214918768,215220576,215522288,215823920,216125472,216426928, 216728304,217029584,217330784,217631904,217932928,218233856,218534704,218835472, 219136144,219436720,219737216,220037632,220337952,220638192,220938336,221238384, 221538352,221838240,222138032,222437728,222737344,223036880,223336304,223635664, 223934912,224234096,224533168,224832160,225131072,225429872,225728608,226027232, 226325776,226624240,226922608,227220880,227519056,227817152,228115168,228413088, 228710912,229008640,229306288,229603840,229901312,230198688,230495968,230793152, 231090256,231387280,231684192,231981024,232277760,232574416,232870960,233167440, 233463808,233760096,234056288,234352384,234648384,234944304,235240128,235535872, 235831504,236127056,236422512,236717888,237013152,237308336,237603424,237898416, 238193328,238488144,238782864,239077488,239372016,239666464,239960816,240255072, 240549232,240843312,241137280,241431168,241724960,242018656,242312256,242605776, 242899200,243192512,243485744,243778896,244071936,244364880,244657744,244950496, 245243168,245535744,245828224,246120608,246412912,246705104,246997216,247289216, 247581136,247872960,248164688,248456320,248747856,249039296,249330640,249621904, 249913056,250204128,250495088,250785968,251076736,251367424,251658016,251948512, 252238912,252529200,252819408,253109520,253399536,253689456,253979280,254269008, 254558640,254848176,255137632,255426976,255716224,256005376,256294432,256583392, 256872256,257161024,257449696,257738272,258026752,258315136,258603424,258891600, 259179696,259467696,259755600,260043392,260331104,260618704,260906224,261193632, 261480960,261768176,262055296,262342320,262629248,262916080,263202816,263489456, 263776000,264062432,264348784,264635024,264921168,265207216,265493168,265779024, 266064784,266350448,266636000,266921472,267206832,267492096,267777264,268062336, 268347312,268632192,268916960,269201632,269486208,269770688,270055072,270339360, 270623552,270907616,271191616,271475488,271759296,272042976,272326560,272610048, 272893440,273176736,273459936,273743040,274026048,274308928,274591744,274874432, 275157024,275439520,275721920,276004224,276286432,276568512,276850528,277132416, 277414240,277695936,277977536,278259040,278540448,278821728,279102944,279384032, 279665056,279945952,280226752,280507456,280788064,281068544,281348960,281629248, 281909472,282189568,282469568,282749440,283029248,283308960,283588544,283868032, 284147424,284426720,284705920,284985024,285264000,285542912,285821696,286100384, 286378976,286657440,286935840,287214112,287492320,287770400,288048384,288326240, 288604032,288881696,289159264,289436768,289714112,289991392,290268576,290545632, 290822592,291099456,291376224,291652896,291929440,292205888,292482272,292758528, 293034656,293310720,293586656,293862496,294138240,294413888,294689440,294964864, 295240192,295515424,295790560,296065600,296340512,296615360,296890080,297164704, 297439200,297713632,297987936,298262144,298536256,298810240,299084160,299357952, 299631648,299905248,300178720,300452128,300725408,300998592,301271680,301544640, 301817536,302090304,302362976,302635520,302908000,303180352,303452608,303724768, 303996800,304268768,304540608,304812320,305083968,305355520,305626944,305898272, 306169472,306440608,306711616,306982528,307253344,307524064,307794656,308065152, 308335552,308605856,308876032,309146112,309416096,309685984,309955744,310225408, 310494976,310764448,311033824,311303072,311572224,311841280,312110208,312379040, 312647776,312916416,313184960,313453376,313721696,313989920,314258016,314526016, 314793920,315061728,315329408,315597024,315864512,316131872,316399168,316666336, 316933408,317200384,317467232,317733984,318000640,318267200,318533632,318799968, 319066208,319332352,319598368,319864288,320130112,320395808,320661408,320926912, 321192320,321457632,321722816,321987904,322252864,322517760,322782528,323047200, 323311744,323576192,323840544,324104800,324368928,324632992,324896928,325160736, 325424448,325688096,325951584,326215008,326478304,326741504,327004608,327267584, 327530464,327793248,328055904,328318496,328580960,328843296,329105568,329367712, 329629760,329891680,330153536,330415264,330676864,330938400,331199808,331461120, 331722304,331983392,332244384,332505280,332766048,333026752,333287296,333547776, 333808128,334068384,334328544,334588576,334848512,335108352,335368064,335627712, 335887200,336146624,336405920,336665120,336924224,337183200,337442112,337700864, 337959552,338218112,338476576,338734944,338993184,339251328,339509376,339767296, 340025120,340282848,340540480,340797984,341055392,341312704,341569888,341826976, 342083968,342340832,342597600,342854272,343110848,343367296,343623648,343879904, 344136032,344392064,344648000,344903808,345159520,345415136,345670656,345926048, 346181344,346436512,346691616,346946592,347201440,347456224,347710880,347965440, 348219872,348474208,348728448,348982592,349236608,349490528,349744320,349998048, 350251648,350505152,350758528,351011808,351264992,351518048,351771040,352023872, 352276640,352529280,352781824,353034272,353286592,353538816,353790944,354042944, 354294880,354546656,354798368,355049952,355301440,355552800,355804096,356055264, 356306304,356557280,356808128,357058848,357309504,357560032,357810464,358060768, 358311008,358561088,358811104,359060992,359310784,359560480,359810048,360059520, 360308896,360558144,360807296,361056352,361305312,361554144,361802880,362051488, 362300032,362548448,362796736,363044960,363293056,363541024,363788928,364036704, 364284384,364531936,364779392,365026752,365274016,365521152,365768192,366015136, 366261952,366508672,366755296,367001792,367248192,367494496,367740704,367986784, 368232768,368478656,368724416,368970080,369215648,369461088,369706432,369951680, 370196800,370441824,370686752,370931584,371176288,371420896,371665408,371909792, 372154080,372398272,372642336,372886304,373130176,373373952,373617600,373861152, 374104608,374347936,374591168,374834304,375077312,375320224,375563040,375805760, 376048352,376290848,376533248,376775520,377017696,377259776,377501728,377743584, 377985344,378227008,378468544,378709984,378951328,379192544,379433664,379674688, 379915584,380156416,380397088,380637696,380878176,381118560,381358848,381599040, 381839104,382079072,382318912,382558656,382798304,383037856,383277280,383516640, 383755840,383994976,384233984,384472896,384711712,384950400,385188992,385427488, 385665888,385904160,386142336,386380384,386618368,386856224,387093984,387331616, 387569152,387806592,388043936,388281152,388518272,388755296,388992224,389229024, 389465728,389702336,389938816,390175200,390411488,390647680,390883744,391119712, 391355584,391591328,391826976,392062528,392297984,392533312,392768544,393003680, 393238720,393473632,393708448,393943168,394177760,394412256,394646656,394880960, 395115136,395349216,395583200,395817088,396050848,396284512,396518080,396751520, 396984864,397218112,397451264,397684288,397917248,398150080,398382784,398615424, 398847936,399080320,399312640,399544832,399776928,400008928,400240832,400472608, 400704288,400935872,401167328,401398720,401629984,401861120,402092192,402323136, 402553984,402784736,403015360,403245888,403476320,403706656,403936896,404167008, 404397024,404626944,404856736,405086432,405316032,405545536,405774912,406004224, 406233408,406462464,406691456,406920320,407149088,407377760,407606336,407834784, 408063136,408291392,408519520,408747584,408975520,409203360,409431072,409658720, 409886240,410113664,410340992,410568192,410795296,411022304,411249216,411476032, 411702720,411929312,412155808,412382176,412608480,412834656,413060736,413286720, 413512576,413738336,413964000,414189568,414415040,414640384,414865632,415090784, 415315840,415540800,415765632,415990368,416215008,416439552,416663968,416888288, 417112512,417336640,417560672,417784576,418008384,418232096,418455712,418679200, 418902624,419125920,419349120,419572192,419795200,420018080,420240864,420463552, 420686144,420908608,421130976,421353280,421575424,421797504,422019488,422241344, 422463104,422684768,422906336,423127776,423349120,423570400,423791520,424012576, 424233536,424454368,424675104,424895744,425116288,425336736,425557056,425777280, 425997408,426217440,426437376,426657184,426876928,427096544,427316064,427535488, 427754784,427974016,428193120,428412128,428631040,428849856,429068544,429287168, 429505664,429724064,429942368,430160576,430378656,430596672,430814560,431032352, 431250048,431467616,431685120,431902496,432119808,432336992,432554080,432771040, 432987936,433204736,433421408,433637984,433854464,434070848,434287104,434503296, 434719360,434935360,435151232,435367008,435582656,435798240,436013696,436229088, 436444352,436659520,436874592,437089568,437304416,437519200,437733856,437948416, 438162880,438377248,438591520,438805696,439019744,439233728,439447584,439661344, 439875008,440088576,440302048,440515392,440728672,440941824,441154880,441367872, 441580736,441793472,442006144,442218720,442431168,442643552,442855808,443067968, 443280032,443492000,443703872,443915648,444127296,444338880,444550336,444761696, 444972992,445184160,445395232,445606176,445817056,446027840,446238496,446449088, 446659552,446869920,447080192,447290400,447500448,447710432,447920320,448130112, 448339776,448549376,448758848,448968224,449177536,449386720,449595808,449804800, 450013664,450222464,450431168,450639776,450848256,451056640,451264960,451473152, 451681248,451889248,452097152,452304960,452512672,452720288,452927808,453135232, 453342528,453549760,453756864,453963904,454170816,454377632,454584384,454791008, 454997536,455203968,455410304,455616544,455822688,456028704,456234656,456440512, 456646240,456851904,457057472,457262912,457468256,457673536,457878688,458083744, 458288736,458493600,458698368,458903040,459107616,459312096,459516480,459720768, 459924960,460129056,460333056,460536960,460740736,460944448,461148064,461351584, 461554976,461758304,461961536,462164640,462367680,462570592,462773440,462976160, 463178816,463381344,463583776,463786144,463988384,464190560,464392608,464594560, 464796448,464998208,465199872,465401472,465602944,465804320,466005600,466206816, 466407904,466608896,466809824,467010624,467211328,467411936,467612480,467812896, 468013216,468213440,468413600,468613632,468813568,469013440,469213184,469412832, 469612416,469811872,470011232,470210528,470409696,470608800,470807776,471006688, 471205472,471404192,471602784,471801312,471999712,472198048,472396288,472594400, 472792448,472990400,473188256,473385984,473583648,473781216,473978688,474176064, 474373344,474570528,474767616,474964608,475161504,475358336,475555040,475751648, 475948192,476144608,476340928,476537184,476733312,476929376,477125344,477321184, 477516960,477712640,477908224,478103712,478299104,478494400,478689600,478884704, 479079744,479274656,479469504,479664224,479858880,480053408,480247872,480442240, 480636512,480830656,481024736,481218752,481412640,481606432,481800128,481993760, 482187264,482380704,482574016,482767264,482960416,483153472,483346432,483539296, 483732064,483924768,484117344,484309856,484502240,484694560,484886784,485078912, 485270944,485462880,485654720,485846464,486038144,486229696,486421184,486612576, 486803840,486995040,487186176,487377184,487568096,487758912,487949664,488140320, 488330880,488521312,488711712,488901984,489092160,489282240,489472256,489662176, 489851968,490041696,490231328,490420896,490610336,490799712,490988960,491178144, 491367232,491556224,491745120,491933920,492122656,492311264,492499808,492688256, 492876608,493064864,493253056,493441120,493629120,493817024,494004832,494192544, 494380160,494567712,494755136,494942496,495129760,495316928,495504000,495691008, 495877888,496064704,496251424,496438048,496624608,496811040,496997408,497183680, 497369856,497555936,497741920,497927840,498113632,498299360,498484992,498670560, 498856000,499041376,499226656,499411840,499596928,499781920,499966848,500151680, 500336416,500521056,500705600,500890080,501074464,501258752,501442944,501627040, 501811072,501995008,502178848,502362592,502546240,502729824,502913312,503096704, 503280000,503463232,503646368,503829408,504012352,504195200,504377984,504560672, 504743264,504925760,505108192,505290496,505472736,505654912,505836960,506018944, 506200832,506382624,506564320,506745952,506927488,507108928,507290272,507471552, 507652736,507833824,508014816,508195744,508376576,508557312,508737952,508918528, 509099008,509279392,509459680,509639904,509820032,510000064,510180000,510359872, 510539648,510719328,510898944,511078432,511257856,511437216,511616448,511795616, 511974688,512153664,512332576,512511392,512690112,512868768,513047296,513225792, 513404160,513582432,513760640,513938784,514116800,514294752,514472608,514650368, 514828064,515005664,515183168,515360608,515537952,515715200,515892352,516069440, 516246432,516423328,516600160,516776896,516953536,517130112,517306592,517482976, 517659264,517835488,518011616,518187680,518363648,518539520,518715296,518891008, 519066624,519242144,519417600,519592960,519768256,519943424,520118528,520293568, 520468480,520643328,520818112,520992800,521167392,521341888,521516320,521690656, 521864896,522039072,522213152,522387168,522561056,522734912,522908640,523082304, 523255872,523429376,523602784,523776096,523949312,524122464,524295552,524468512, 524641440,524814240,524986976,525159616,525332192,525504640,525677056,525849344, 526021568,526193728,526365792,526537760,526709632,526881440,527053152,527224800, 527396352,527567840,527739200,527910528,528081728,528252864,528423936,528594880, 528765760,528936576,529107296,529277920,529448480,529618944,529789344,529959648, 530129856,530300000,530470048,530640000,530809888,530979712,531149440,531319072, 531488608,531658080,531827488,531996800,532166016,532335168,532504224,532673184, 532842080,533010912,533179616,533348288,533516832,533685312,533853728,534022048, 534190272,534358432,534526496,534694496,534862400,535030240,535197984,535365632, 535533216,535700704,535868128,536035456,536202720,536369888,536536992,536704000, 536870912 }; doomsday-stable-1.15.7/doomsday/client/src/api_console.cpp0000664000175000017500000001015412641367670023060 0ustar jaakkojaakko/** @file api_console.cpp Public Console API. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #define DENG_NO_API_MACROS_CONSOLE #include #include #include #include "api_console.h" #include "dd_main.h" #undef Con_SetUri2 void Con_SetUri2(char const *path, uri_s const *uri, int svFlags) { cvar_t* var = Con_FindVariable(path); if(!var) return; CVar_SetUri2(var, *reinterpret_cast(uri), svFlags); } #undef Con_SetUri void Con_SetUri(char const *path, uri_s const *uri) { Con_SetUri2(path, uri, 0); } #undef Con_SetString2 void Con_SetString2(char const *path, char const *text, int svFlags) { cvar_t *var = Con_FindVariable(path); if(!var) return; CVar_SetString2(var, text, svFlags); } #undef Con_SetString void Con_SetString(char const *path, char const *text) { Con_SetString2(path, text, 0); } #undef Con_SetInteger2 void Con_SetInteger2(char const *path, int value, int svFlags) { cvar_t *var = Con_FindVariable(path); if(!var) return; CVar_SetInteger2(var, value, svFlags); } #undef Con_SetInteger void Con_SetInteger(char const *path, int value) { Con_SetInteger2(path, value, 0); } #undef Con_SetFloat2 void Con_SetFloat2(char const *path, float value, int svFlags) { cvar_t *var = Con_FindVariable(path); if(!var) return; CVar_SetFloat2(var, value, svFlags); } #undef Con_SetFloat void Con_SetFloat(char const *path, float value) { Con_SetFloat2(path, value, 0); } #undef Con_GetInteger int Con_GetInteger(char const *path) { cvar_t *var = Con_FindVariable(path); if(!var) return 0; return CVar_Integer(var); } #undef Con_GetFloat float Con_GetFloat(char const *path) { cvar_t *var = Con_FindVariable(path); if(!var) return 0; return CVar_Float(var); } #undef Con_GetByte byte Con_GetByte(char const *path) { cvar_t *var = Con_FindVariable(path); if(!var) return 0; return CVar_Byte(var); } #undef Con_GetString char const *Con_GetString(char const *path) { cvar_t *var = Con_FindVariable(path); if(!var) return ""; return CVar_String(var); } #undef Con_GetUri uri_s const *Con_GetUri(char const *path) { return reinterpret_cast(&CVar_Uri(Con_FindVariable(path))); } cvartype_t Con_GetVariableType(char const *path) { cvar_t *var = Con_FindVariable(path); if(!var) return CVT_NULL; return var->type; } /** * Wrapper for Con_Execute * Public method for plugins to execute console commands. */ int DD_Execute(int silent, const char* command) { return Con_Execute(CMDS_GAME, command, silent, false); } /** * Exported version of Con_Executef */ int DD_Executef(int silent, const char *command, ...) { va_list argptr; char buffer[4096]; va_start(argptr, command); vsprintf(buffer, command, argptr); va_end(argptr); return Con_Execute(CMDS_GAME, buffer, silent, false); } DENG_DECLARE_API(Con) = { { DE_API_CONSOLE }, Con_Open, Con_AddCommand, Con_AddVariable, Con_AddCommandList, Con_AddVariableList, Con_GetVariableType, Con_GetByte, Con_GetInteger, Con_GetFloat, Con_GetString, Con_GetUri, Con_SetInteger2, Con_SetInteger, Con_SetFloat2, Con_SetFloat, Con_SetString2, Con_SetString, Con_SetUri2, Con_SetUri, App_Error, DD_Execute, DD_Executef }; doomsday-stable-1.15.7/doomsday/client/src/macx/0000775000175000017500000000000012641367670021010 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/macx/MusicPlayer.mm0000664000175000017500000001017512641367670023604 0ustar jaakkojaakko/** @file *\section License * License: GPL * Online License Link: http://www.gnu.org/licenses/gpl.html * *\author Copyright © 2005-2013 Jaakko Keränen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301 USA */ #import "MusicPlayer.h" /** * QuickTime music player for Mac OS X 10.4 or newer. Uses the Cocoa QTKit. */ static MusicPlayer* musicPlayer; @implementation MusicPlayer - (id)init { currentSong = 0; songVolume = 1.f; return self; } - (void)shutdown { if(currentSong) { [currentSong stop]; [currentSong release]; currentSong = 0; } } - (int)playFile:(const char*)filename looping:(int)loop { // Get rid of the old song? if(currentSong) { [currentSong stop]; [currentSong release]; currentSong = 0; } NSString* path = [[[NSString alloc] initWithCString:filename encoding:[NSString defaultCStringEncoding]] autorelease]; currentSong = [[QTMovie alloc] initWithFile:path error:0]; if(!currentSong) return false; [currentSong setVolume:songVolume]; // Set the looping attribute. [currentSong setAttribute:[NSNumber numberWithBool:(loop? YES : NO)] forKey:QTMovieLoopsAttribute]; // Start playing. [currentSong play]; return true; } - (void)setVolume:(float)volume { songVolume = volume; if(currentSong) { [currentSong setVolume:songVolume]; } } - (void)play { if(currentSong) { [currentSong play]; } } - (void)stop { if(currentSong) { [currentSong stop]; } } - (void)rewind { if(currentSong) { [currentSong gotoBeginning]; } } @end // -------------------------------------------------------------------------- #include "api_audiod.h" #include "api_audiod_mus.h" static int DM_Ext_Init(void) { // Already initialized? if(musicPlayer) return true; // Create a new QTKit player. musicPlayer = [[MusicPlayer alloc] init]; return true; } static void DM_Ext_Shutdown(void) { if(!musicPlayer) return; // Release the player. [musicPlayer shutdown]; [musicPlayer release]; musicPlayer = 0; } static void DM_Ext_Update(void) { if(!musicPlayer) return; // Gets called on every frame. } static void DM_Ext_Set(int property, float value) { if(!musicPlayer) return; switch (property) { case MUSIP_VOLUME: if(value > 1) value = 1; if(value < 0) value = 0; [musicPlayer setVolume:value]; break; } } static int DM_Ext_Get(int property, void *value) { if(!musicPlayer) return false; switch (property) { case MUSIP_ID: strcpy(reinterpret_cast(value), "QuickTime(Cocoa)/Ext"); break; default: return false; } return true; } static void DM_Ext_Pause(int pause) { if(!musicPlayer) return; if(pause) { [musicPlayer stop]; } else { [musicPlayer play]; } } static void DM_Ext_Stop(void) { if(!musicPlayer) return; [musicPlayer stop]; [musicPlayer rewind]; } static int DM_Ext_PlayFile(const char *filename, int looped) { if(!musicPlayer) return 0; return [musicPlayer playFile:filename looping:looped]; } // The audio driver struct. audiointerface_music_t audiodQuickTimeMusic = { { DM_Ext_Init, DM_Ext_Shutdown, DM_Ext_Update, DM_Ext_Set, DM_Ext_Get, DM_Ext_Pause, DM_Ext_Stop }, NULL, NULL, DM_Ext_PlayFile, }; doomsday-stable-1.15.7/doomsday/client/src/macx/cursor_macx.mm0000664000175000017500000000200312641367670023663 0ustar jaakkojaakko/** @file cursor_macx.mm Native OS X mouse cursor functions. * @ingroup ui * * @authors Copyright © 2015 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include void Cursor_Show(bool show) { if(show) { [NSCursor unhide]; } else { [NSCursor hide]; } } doomsday-stable-1.15.7/doomsday/client/src/face.cpp0000664000175000017500000000567012641367670021472 0ustar jaakkojaakko/** @file face.cpp Mesh, face geometry. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "face.h" #include #include /// @todo remove me #include #include "HEdge" namespace de { DENG2_PIMPL_NOREF(Face) { HEdge *hedge = nullptr; ///< First half-edge in the face geometry. AABoxd aaBox; ///< Vertex bounding box. Vector2d center; ///< Center of vertices. }; Face::Face(Mesh &mesh) : MeshElement(mesh) , _hedgeCount(0) , d(new Instance()) {} int Face::hedgeCount() const { return _hedgeCount; } HEdge *Face::hedge() const { return d->hedge; } void Face::setHEdge(HEdge const *newHEdge) { d->hedge = const_cast(newHEdge); } AABoxd const &Face::aaBox() const { return d->aaBox; } void Face::updateAABox() { d->aaBox.clear(); if(!d->hedge) return; // Very odd... HEdge const *hedge = d->hedge; V2d_InitBoxXY(d->aaBox.arvec2, hedge->origin().x, hedge->origin().y); while((hedge = &hedge->next()) != d->hedge) { V2d_AddToBoxXY(d->aaBox.arvec2, hedge->origin().x, hedge->origin().y); } } Vector2d const &Face::center() const { return d->center; } void Face::updateCenter() { // The center is the middle of our AABox. d->center = Vector2d(d->aaBox.min) + (Vector2d(d->aaBox.max) - Vector2d(d->aaBox.min)) / 2; } bool Face::isConvex() const { /// @todo Implement full conformance checking. return _hedgeCount > 2; } String Face::description() const { String text = String("Face [0x%1] comprises %2 half-edges") .arg(de::dintptr(this), 0, 16).arg(_hedgeCount); if(HEdge const *hedge = d->hedge) { do { Vector2d direction = hedge->origin() - d->center; coord_t angle = M_DirectionToAngleXY(direction.x, direction.y); text += String("\n [0x%1]: Angle %2.6f %3 -> %4") .arg(de::dintptr(hedge), 0, 16) .arg(angle) .arg(hedge->origin().asText()) .arg(hedge->twin().origin().asText()); } while((hedge = &hedge->next()) != d->hedge); } return text; } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/client/0000775000175000017500000000000012641367670021336 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/client/cl_mobj.cpp0000664000175000017500000004552212641367670023457 0ustar jaakkojaakko/** @file cl_mobj.cpp Client map objects. * @ingroup client * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #define DENG_NO_API_MACROS_CLIENT #include "de_base.h" #include "client/cl_mobj.h" #include "api_client.h" #include "client/cl_player.h" #include "client/cl_world.h" #include "network/net_main.h" #include "network/protocol.h" #include "world/map.h" #include "world/p_players.h" #include "api_sound.h" #include #include #include using namespace de; /// Convert 8.8/10.6 fixed point to 16.16. #define UNFIXED8_8(x) (((x) << 16) / 256) #define UNFIXED10_6(x) (((x) << 16) / 64) #if 0 ClMobjInfo::ClMobjInfo() : startMagic(CLM_MAGIC1) , next (0) , prev (0) , flags (0) , time (Timer_RealMilliseconds()) , sound (0) , volume (0) , endMagic (CLM_MAGIC2) {} mobj_t *ClMobjInfo::mobj() { DENG2_ASSERT(startMagic == CLM_MAGIC1); DENG2_ASSERT(endMagic == CLM_MAGIC2); return reinterpret_cast((char *)this + sizeof(ClMobjInfo)); } #endif void ClMobj_Link(mobj_t *mo) { ClientMobjThinkerData::RemoteSync *info = ClMobj_GetInfo(mo); CL_ASSERT_CLMOBJ(mo); if((info->flags & (CLMF_HIDDEN | CLMF_UNPREDICTABLE)) || mo->dPlayer) { // We do not yet have all the details about Hidden mobjs. // The server hasn't sent us a Create Mobj delta for them. // Client mobjs that belong to players remain unlinked. return; } LOG_MAP_XVERBOSE("ClMobj_Link: id %i, x%f Y%f, solid:%b") << mo->thinker.id << mo->origin[VX] << mo->origin[VY] << (mo->ddFlags & DDMF_SOLID); Mobj_Link(mo, (mo->ddFlags & DDMF_DONTDRAW ? 0 : MLF_SECTOR) | (mo->ddFlags & DDMF_SOLID ? MLF_BLOCKMAP : 0)); } #undef ClMobj_EnableLocalActions void ClMobj_EnableLocalActions(mobj_t *mo, dd_bool enable) { LOG_AS("ClMobj_EnableLocalActions"); ClientMobjThinkerData::RemoteSync *info = ClMobj_GetInfo(mo); if(!isClient || !info) return; if(enable) { LOG_NET_VERBOSE("Enabled for clmobj %i") << mo->thinker.id; info->flags |= CLMF_LOCAL_ACTIONS; } else { LOG_NET_VERBOSE("Disabled for clmobj %i") << mo->thinker.id; info->flags &= ~CLMF_LOCAL_ACTIONS; } } #undef ClMobj_LocalActionsEnabled dd_bool ClMobj_LocalActionsEnabled(mobj_t *mo) { ClientMobjThinkerData::RemoteSync *info = ClMobj_GetInfo(mo); if(!isClient || !info) return true; return (info->flags & CLMF_LOCAL_ACTIONS) != 0; } void ClMobj_SetState(mobj_t *mo, int stnum) { if(stnum < 0) return; do { Mobj_SetState(mo, stnum); stnum = runtimeDefs.states[stnum].nextState; } while(!mo->tics && stnum > 0); } void Cl_UpdateRealPlayerMobj(mobj_t *localMobj, mobj_t *remoteClientMobj, int flags, dd_bool onFloor) { if(!localMobj || !remoteClientMobj) { LOGDEV_MAP_VERBOSE("Cl_UpdateRealPlayerMobj: mo=%p clmo=%p") << localMobj << remoteClientMobj; return; } localMobj->radius = Mobj_Radius(*remoteClientMobj); if(flags & MDF_MOM_X) localMobj->mom[MX] = remoteClientMobj->mom[MX]; if(flags & MDF_MOM_Y) localMobj->mom[MY] = remoteClientMobj->mom[MY]; if(flags & MDF_MOM_Z) localMobj->mom[MZ] = remoteClientMobj->mom[MZ]; if(flags & MDF_ANGLE) { localMobj->angle = remoteClientMobj->angle; LOGDEV_MAP_XVERBOSE_DEBUGONLY("Cl_UpdateRealPlayerMobj: localMobj=%p angle=%x", localMobj << localMobj->angle); } localMobj->sprite = remoteClientMobj->sprite; localMobj->frame = remoteClientMobj->frame; //localMobj->nextframe = clmo->nextframe; localMobj->tics = remoteClientMobj->tics; localMobj->state = remoteClientMobj->state; //localMobj->nexttime = clmo->nexttime; #define DDMF_KEEP_MASK (DDMF_REMOTE | DDMF_SOLID) localMobj->ddFlags = (localMobj->ddFlags & DDMF_KEEP_MASK) | (remoteClientMobj->ddFlags & ~DDMF_KEEP_MASK); if(flags & MDF_FLAGS) { localMobj->flags = (localMobj->flags & ~0x1c000000) | (remoteClientMobj->flags & 0x1c000000); // color translation flags (MF_TRANSLATION) //DEBUG_Message(("UpdateRealPlayerMobj: translation=%i\n", (localMobj->flags >> 26) & 7)); } localMobj->height = remoteClientMobj->height; localMobj->selector &= ~DDMOBJ_SELECTOR_MASK; localMobj->selector |= remoteClientMobj->selector & DDMOBJ_SELECTOR_MASK; localMobj->visAngle = remoteClientMobj->angle >> 16; if(flags & (MDF_ORIGIN_X | MDF_ORIGIN_Y)) { /* // We have to unlink the real mobj before we move it. P_MobjUnlink(localMobj); localMobj->pos[VX] = remoteClientMobj->pos[VX]; localMobj->pos[VY] = remoteClientMobj->pos[VY]; P_MobjLink(localMobj, MLF_SECTOR | MLF_BLOCKMAP); */ // This'll update the contacted floor and ceiling heights as well. if(gx.MobjTryMoveXYZ) { if(gx.MobjTryMoveXYZ(localMobj, remoteClientMobj->origin[VX], remoteClientMobj->origin[VY], (flags & MDF_ORIGIN_Z)? remoteClientMobj->origin[VZ] : localMobj->origin[VZ])) { if((flags & MDF_ORIGIN_Z) && onFloor) { localMobj->origin[VZ] = remoteClientMobj->origin[VZ] = localMobj->floorZ; } } } } if(flags & MDF_ORIGIN_Z) { if(!onFloor) { localMobj->floorZ = remoteClientMobj->floorZ; } localMobj->ceilingZ = remoteClientMobj->ceilingZ; localMobj->origin[VZ] = remoteClientMobj->origin[VZ]; // Don't go below the floor level. if(localMobj->origin[VZ] < localMobj->floorZ) localMobj->origin[VZ] = localMobj->floorZ; } } dd_bool Cl_IsClientMobj(mobj_t const *mo) { if(ClientMobjThinkerData *data = THINKER_DATA_MAYBE(mo->thinker, ClientMobjThinkerData)) { return data->hasRemoteSync(); } return false; } #undef ClMobj_IsValid dd_bool ClMobj_IsValid(mobj_t *mo) { if(!Cl_IsClientMobj(mo)) return true; ClientMobjThinkerData::RemoteSync *info = ClMobj_GetInfo(mo); if(info->flags & (CLMF_HIDDEN | CLMF_UNPREDICTABLE)) { // Should not use this for playsim. return false; } if(!mo->info) { // We haven't yet received info about the mobj's type? return false; } return true; } ClientMobjThinkerData::RemoteSync *ClMobj_GetInfo(mobj_t *mo) { if(!mo) return 0; if(ClientMobjThinkerData *data = THINKER_DATA_MAYBE(mo->thinker, ClientMobjThinkerData)) { if(!data->hasRemoteSync()) return 0; return &data->remoteSync(); } return 0; } dd_bool ClMobj_Reveal(mobj_t *mo) { LOG_AS("ClMobj_Reveal"); ClientMobjThinkerData::RemoteSync *info = ClMobj_GetInfo(mo); CL_ASSERT_CLMOBJ(mo); // Check that we know enough about the clmobj. if(mo->dPlayer != &ddPlayers[consolePlayer].shared && (!(info->flags & CLMF_KNOWN_X) || !(info->flags & CLMF_KNOWN_Y) || //!(info->flags & CLMF_KNOWN_Z) || !(info->flags & CLMF_KNOWN_STATE))) { // Don't reveal just yet. We lack a vital piece of information. return false; } LOG_MAP_XVERBOSE("clmobj %i 'Hidden' status lifted (z=%f)") << mo->thinker.id << mo->origin[VZ]; info->flags &= ~CLMF_HIDDEN; // Start a sound that has been queued for playing at the time // of unhiding. Sounds are queued if a sound delta arrives for an // object ID we don't know (yet). if(info->flags & CLMF_SOUND) { info->flags &= ~CLMF_SOUND; S_StartSoundAtVolume(info->sound, mo, info->volume); } LOGDEV_MAP_XVERBOSE("Revealing id %i, state %p (%i)") << mo->thinker.id << mo->state << runtimeDefs.states.indexOf(mo->state); return true; } /** * Determines whether @a mo happens to reside inside one of the local players. * In normal gameplay solid mobjs cannot enter inside each other. * * @param mo Client mobj (must be solid). */ static dd_bool ClMobj_IsStuckInsideLocalPlayer(mobj_t *mo) { if(!(mo->ddFlags & DDMF_SOLID) || mo->dPlayer) return false; for(int i = 0; i < DDMAXPLAYERS; ++i) { if(!ddPlayers[i].shared.inGame) continue; if(P_ConsoleToLocal(i) < 0) continue; // Not a local player. mobj_t *plmo = ddPlayers[i].shared.mo; if(!plmo) continue; float blockRadius = Mobj_Radius(*mo) + Mobj_Radius(*plmo); if(fabs(mo->origin[VX] - plmo->origin[VX]) >= blockRadius || fabs(mo->origin[VY] - plmo->origin[VY]) >= blockRadius) continue; // Too far. if(mo->origin[VZ] > plmo->origin[VZ] + plmo->height) continue; // Above. if(plmo->origin[VZ] > mo->origin[VZ] + mo->height) continue; // Under. // Seems to be blocking the player... return true; } return false; // Not stuck. } void ClMobj_ReadDelta() { /// @todo Do not assume the CURRENT map. Map &map = App_WorldSystem().map(); thid_t const id = Reader_ReadUInt16(msgReader); // Read the ID. int const df = Reader_ReadUInt16(msgReader); // Flags. // More flags? byte moreFlags = 0, fastMom = false; if(df & MDF_MORE_FLAGS) { moreFlags = Reader_ReadByte(msgReader); // Fast momentum uses 10.6 fixed point instead of the normal 8.8. if(moreFlags & MDFE_FAST_MOM) fastMom = true; } LOG_NET_XVERBOSE("Reading mobj delta for %i (df:0x%x edf:0x%x)") << id << df << moreFlags; // Get the client mobj for this. mobj_t *mo = map.clMobjFor(id); ClientMobjThinkerData::RemoteSync *info = ClMobj_GetInfo(mo); bool needsLinking = false, justCreated = false; if(!mo) { LOG_NET_XVERBOSE("Creating new clmobj %i (hidden)") << id; // This is a new ID, allocate a new mobj. mo = map.clMobjFor(id, true /* create it now */); info = ClMobj_GetInfo(mo); justCreated = true; needsLinking = true; // Always create new mobjs as hidden. They will be revealed when // we know enough about them. info->flags |= CLMF_HIDDEN; } if(!(info->flags & CLMF_NULLED)) { // Now that we've received a delta, the mobj's Predictable again. info->flags &= ~CLMF_UNPREDICTABLE; // This clmobj is evidently alive. info->time = Timer_RealMilliseconds(); } mobj_t *d = mo; /*if(d->dPlayer && d->dPlayer == &ddPlayers[consolePlayer]) { // Mark the local player known. cmo->flags |= CLMF_KNOWN; }*/ // Need to unlink? (Flags because DDMF_SOLID determines block-linking.) if(df & (MDF_ORIGIN_X | MDF_ORIGIN_Y | MDF_ORIGIN_Z | MDF_FLAGS) && !justCreated && !d->dPlayer) { needsLinking = true; Mobj_Unlink(mo); } // Remember where the mobj used to be in case we need to cancel a move. //mobj_t oldState; zap(oldState); MobjThinker oldState(*mo); bool onFloor = false; // Coordinates with three bytes. if(df & MDF_ORIGIN_X) { d->origin[VX] = FIX2FLT((Reader_ReadInt16(msgReader) << FRACBITS) | (Reader_ReadByte(msgReader) << 8)); if(info) info->flags |= CLMF_KNOWN_X; } if(df & MDF_ORIGIN_Y) { d->origin[VY] = FIX2FLT((Reader_ReadInt16(msgReader) << FRACBITS) | (Reader_ReadByte(msgReader) << 8)); if(info) info->flags |= CLMF_KNOWN_Y; } if(df & MDF_ORIGIN_Z) { if(!(moreFlags & MDFE_Z_FLOOR)) { d->origin[VZ] = FIX2FLT((Reader_ReadInt16(msgReader) << FRACBITS) | (Reader_ReadByte(msgReader) << 8)); if(info) { info->flags |= CLMF_KNOWN_Z; // The mobj won't stick if an explicit coordinate is supplied. info->flags &= ~(CLMF_STICK_FLOOR | CLMF_STICK_CEILING); } d->floorZ = Reader_ReadFloat(msgReader); } else { onFloor = true; // Ignore these. Reader_ReadInt16(msgReader); Reader_ReadByte(msgReader); Reader_ReadFloat(msgReader); info->flags |= CLMF_KNOWN_Z; //d->pos[VZ] = d->floorZ; } d->ceilingZ = Reader_ReadFloat(msgReader); } // Momentum using 8.8 fixed point. if(df & MDF_MOM_X) { short mom = Reader_ReadInt16(msgReader); d->mom[MX] = FIX2FLT(fastMom? UNFIXED10_6(mom) : UNFIXED8_8(mom)); } if(df & MDF_MOM_Y) { short mom = Reader_ReadInt16(msgReader); d->mom[MY] = FIX2FLT(fastMom ? UNFIXED10_6(mom) : UNFIXED8_8(mom)); } if(df & MDF_MOM_Z) { short mom = Reader_ReadInt16(msgReader); d->mom[MZ] = FIX2FLT(fastMom ? UNFIXED10_6(mom) : UNFIXED8_8(mom)); } // Angles with 16-bit accuracy. if(df & MDF_ANGLE) d->angle = Reader_ReadInt16(msgReader) << 16; // MDF_SELSPEC is never used without MDF_SELECTOR. if(df & MDF_SELECTOR) d->selector = Reader_ReadPackedUInt16(msgReader); if(df & MDF_SELSPEC) d->selector |= Reader_ReadByte(msgReader) << 24; if(df & MDF_STATE) { int stateIdx = Reader_ReadPackedUInt16(msgReader); // Translate. stateIdx = Cl_LocalMobjState(stateIdx); // When local actions are allowed, the assumption is that // the client will be doing the state changes. if(!(info->flags & CLMF_LOCAL_ACTIONS)) { ClMobj_SetState(d, stateIdx); info->flags |= CLMF_KNOWN_STATE; } } if(df & MDF_FLAGS) { // Only the flags in the pack mask are affected. d->ddFlags &= ~DDMF_PACK_MASK; d->ddFlags |= DDMF_REMOTE | (Reader_ReadUInt32(msgReader) & DDMF_PACK_MASK); d->flags = Reader_ReadUInt32(msgReader); d->flags2 = Reader_ReadUInt32(msgReader); d->flags3 = Reader_ReadUInt32(msgReader); } if(df & MDF_HEALTH) d->health = Reader_ReadInt32(msgReader); if(df & MDF_RADIUS) d->radius = Reader_ReadFloat(msgReader); if(df & MDF_HEIGHT) d->height = Reader_ReadFloat(msgReader); if(df & MDF_FLOORCLIP) d->floorClip = Reader_ReadFloat(msgReader); if(moreFlags & MDFE_TRANSLUCENCY) d->translucency = Reader_ReadByte(msgReader); if(moreFlags & MDFE_FADETARGET) d->visTarget = ((short)Reader_ReadByte(msgReader)) - 1; if(moreFlags & MDFE_TYPE) { d->type = Cl_LocalMobjType(Reader_ReadInt32(msgReader)); d->info = &runtimeDefs.mobjInfo[d->type]; } // Is it time to remove the Hidden status? if(info->flags & CLMF_HIDDEN) { // Now it can be displayed (potentially). if(ClMobj_Reveal(d)) { // Now it can be linked to the world. needsLinking = true; } } // Non-player mobjs: update the Z position to be on the local floor, which may be // different than the server-side floor. if(!d->dPlayer && onFloor && gx.MobjCheckPositionXYZ) { if(coord_t *floorZ = (coord_t *) gx.GetVariable(DD_TM_FLOOR_Z)) { gx.MobjCheckPositionXYZ(d, d->origin[VX], d->origin[VY], DDMAXFLOAT); d->origin[VZ] = d->floorZ = *floorZ; } } // If the clmobj is Hidden (or Nulled), it will not be linked back to // the world until it's officially Created. (Otherwise, partially updated // mobjs may be visible for a while.) if(!(info->flags & (CLMF_HIDDEN | CLMF_NULLED))) { // Link again. if(needsLinking && !d->dPlayer) { ClMobj_Link(mo); if(ClMobj_IsStuckInsideLocalPlayer(mo)) { // Oopsie, on second thought we shouldn't do this move. Mobj_Unlink(mo); V3d_Copy(mo->origin, oldState->origin); mo->floorZ = oldState->floorZ; mo->ceilingZ = oldState->ceilingZ; ClMobj_Link(mo); } } // Update players. if(d->dPlayer) { LOG_NET_XVERBOSE("Updating player %i local mobj with new clmobj state {%f, %f, %f}") << P_GetDDPlayerIdx(d->dPlayer) << d->origin[VX] << d->origin[VY] << d->origin[VZ]; // Players have real mobjs. The client mobj is hidden (unlinked). Cl_UpdateRealPlayerMobj(d->dPlayer->mo, d, df, onFloor); } } } void ClMobj_ReadNullDelta() { LOG_AS("ClMobj_ReadNullDelta"); /// @todo Do not assume the CURRENT map. Map &map = App_WorldSystem().map(); // The delta only contains an ID. thid_t id = Reader_ReadUInt16(msgReader); LOGDEV_NET_XVERBOSE("Null %i") << id; mobj_t *mo = map.clMobjFor(id); if(!mo) { // Wasted bandwidth... LOGDEV_NET_MSG("Request to remove id %i that doesn't exist here") << id; return; } ClientMobjThinkerData::RemoteSync *info = ClMobj_GetInfo(mo); // Get rid of this mobj. if(!mo->dPlayer) { Mobj_Unlink(mo); } else { LOGDEV_NET_MSG("clmobj of player %i deleted") << P_GetDDPlayerIdx(mo->dPlayer); // The clmobjs of players aren't linked. ClPlayer_State(P_GetDDPlayerIdx(mo->dPlayer))->clMobjId = 0; } // This'll allow playing sounds from the mobj for a little while. // The mobj will soon time out and be permanently removed. info->time = Timer_RealMilliseconds(); info->flags |= CLMF_UNPREDICTABLE | CLMF_NULLED; } #undef ClMobj_Find mobj_t *ClMobj_Find(thid_t id) { if(!App_WorldSystem().hasMap()) return nullptr; /// @todo Do not assume the CURRENT map. return App_WorldSystem().map().clMobjFor(id); } // cl_player.c DENG_EXTERN_C mobj_t *ClPlayer_ClMobj(int plrNum); DENG_DECLARE_API(Client) = { { DE_API_CLIENT }, ClMobj_Find, ClMobj_EnableLocalActions, ClMobj_LocalActionsEnabled, ClMobj_IsValid, ClPlayer_ClMobj }; doomsday-stable-1.15.7/doomsday/client/src/client/clpolymover.cpp0000664000175000017500000000707212641367670024423 0ustar jaakkojaakko/** @file clpolymover.cpp Clientside polyobj mover (thinker). * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "client/clpolymover.h" #include "client/cl_def.h" #include "client/cl_player.h" #include "world/map.h" #include "world/p_players.h" #include "world/thinkers.h" #include "world/polyobjdata.h" using namespace de; thinker_s *ClPolyMover::newThinker(Polyobj &polyobj, bool moving, bool rotating) // static { // If there is an existing mover, modify it. if(ClPolyMover *mover = polyobj.data().mover()) { mover->_move = moving; mover->_rotate = rotating; return &mover->thinker(); } Thinker th(Thinker::AllocateMemoryZone); th.setData(new ClPolyMover(polyobj, moving, rotating)); thinker_s *ptr = th.take(); polyobj.map().thinkers().add(*ptr, false /*not public*/); LOGDEV_MAP_XVERBOSE("New polymover %p for polyobj #%i.") << ptr << polyobj.indexInMap(); return ptr; } ClPolyMover::ClPolyMover(Polyobj &pobj, bool moving, bool rotating) : _polyobj (&pobj ) , _move ( moving ) , _rotate ( rotating) { _polyobj->data().addMover(*this); } ClPolyMover::~ClPolyMover() { _polyobj->data().removeMover(*this); } void ClPolyMover::think() { LOG_AS("ClPolyMover::think"); Polyobj *po = _polyobj; if(_move) { // How much to go? Vector2d delta = Vector2d(po->dest) - Vector2d(po->origin); ddouble dist = M_ApproxDistance(delta.x, delta.y); if(dist <= po->speed || de::fequal(po->speed, 0)) { // We'll arrive at the destination. _move = false; } else { // Adjust deltas to fit speed. delta = (delta / dist) * po->speed; } // Do the move. po->move(delta); } if(_rotate) { // How much to go? int dist = po->destAngle - po->angle; int speed = po->angleSpeed; //dist = FIX2FLT(po->destAngle - po->angle); //if(!po->angleSpeed || dist > 0 /*(abs(FLT2FIX(dist) >> 4) <= abs(((signed) po->angleSpeed) >> 4)*/ // /* && po->destAngle != -1*/) || !po->angleSpeed) if(!po->angleSpeed || ABS(dist >> 2) <= ABS(speed >> 2)) { LOGDEV_MAP_XVERBOSE("Mover %p reached end of turn, destAngle=%i") << &thinker() << po->destAngle; // We'll arrive at the destination. _rotate = false; } else { // Adjust to speed. dist = /*FIX2FLT((int)*/ po->angleSpeed; } po->rotate(dist); } // Can we get rid of this mover? if(!_move && !_rotate) { _polyobj->map().thinkers().remove(thinker()); // we get deleted } } doomsday-stable-1.15.7/doomsday/client/src/client/cl_player.cpp0000664000175000017500000003500112641367670024013 0ustar jaakkojaakko/** @file cl_player.cpp Clientside player management. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "client/cl_player.h" #include "api_client.h" #include "network/net_main.h" #include "network/protocol.h" #include "world/map.h" #include "world/p_players.h" #include "BspLeaf" #include "Sector" #include using namespace de; #define TOP_PSPY (32) #define BOTTOM_PSPY (128) float pspMoveSpeed = 6; float cplrThrustMul = 1; clplayerstate_t clPlayerStates[DDMAXPLAYERS]; //static int fixSpeed = 15; //static float fixPos[3]; //static int fixTics; static float pspY; // Console player demo momentum (used to smooth out abrupt momentum changes). static float cpMom[3][LOCALCAM_WRITE_TICS]; void Cl_InitPlayers() { //fixTics = 0; pspY = 0; de::zap(clPlayerStates); //de::zap(fixPos); de::zap(cpMom); } clplayerstate_t *ClPlayer_State(int plrNum) { DENG2_ASSERT(plrNum >= 0 && plrNum < DDMAXPLAYERS); return &clPlayerStates[plrNum]; } #undef ClPlayer_ClMobj DENG_EXTERN_C struct mobj_s *ClPlayer_ClMobj(int plrNum) { DENG2_ASSERT(plrNum >= 0 && plrNum < DDMAXPLAYERS); return ClMobj_Find(clPlayerStates[plrNum].clMobjId); } void ClPlayer_UpdateOrigin(int plrNum) { DENG2_ASSERT(plrNum >= 0 && plrNum < DDMAXPLAYERS); player_t *plr = &ddPlayers[plrNum]; clplayerstate_t *s = ClPlayer_State(plrNum); if(!s->clMobjId || !plr->shared.mo) return; // Must have a mobj! mobj_t *remoteClientMobj = ClMobj_Find(s->clMobjId); mobj_t *localMobj = plr->shared.mo; // The client mobj is never solid. remoteClientMobj->ddFlags &= ~DDMF_SOLID; remoteClientMobj->angle = localMobj->angle; // The player's client mobj is not linked to any lists, so position // can be updated without any hassles. std::memcpy(remoteClientMobj->origin, localMobj->origin, sizeof(localMobj->origin)); Mobj_Link(remoteClientMobj, 0); // Update bspLeaf pointer. remoteClientMobj->floorZ = localMobj->floorZ; remoteClientMobj->ceilingZ = localMobj->ceilingZ; remoteClientMobj->mom[MX] = localMobj->mom[MX]; remoteClientMobj->mom[MY] = localMobj->mom[MY]; remoteClientMobj->mom[MZ] = localMobj->mom[MZ]; } void ClPlayer_ApplyPendingFixes(int plrNum) { LOG_AS("ClPlayer_ApplyPendingFixes"); clplayerstate_t *state = ClPlayer_State(plrNum); player_t *plr = &ddPlayers[plrNum]; mobj_t *clmo = ClPlayer_ClMobj(plrNum); ddplayer_t *ddpl = &plr->shared; mobj_t *mo = ddpl->mo; bool sendAck = false; // If either mobj is missing, the fix cannot be applied yet. if(!mo || !clmo) return; if(clmo->thinker.id != state->pendingFixTargetClMobjId) return; DENG_ASSERT(clmo->thinker.id == state->clMobjId); if(state->pendingFixes & DDPF_FIXANGLES) { state->pendingFixes &= ~DDPF_FIXANGLES; ddpl->fixAcked.angles = ddpl->fixCounter.angles; sendAck = true; LOGDEV_NET_MSG("Applying angle %x to mobj %p and clmo %i") << state->pendingAngleFix << mo << clmo->thinker.id; clmo->angle = mo->angle = state->pendingAngleFix; ddpl->lookDir = state->pendingLookDirFix; } if(state->pendingFixes & DDPF_FIXORIGIN) { state->pendingFixes &= ~DDPF_FIXORIGIN; ddpl->fixAcked.origin = ddpl->fixCounter.origin; sendAck = true; LOGDEV_NET_MSG("Applying pos %s to mobj %p and clmo %i") << Vector3d(state->pendingOriginFix).asText() << mo << clmo->thinker.id; Mobj_SetOrigin(mo, state->pendingOriginFix[VX], state->pendingOriginFix[VY], state->pendingOriginFix[VZ]); mo->reactionTime = 18; // The position is now known. ddpl->flags &= ~DDPF_UNDEFINED_ORIGIN; Smoother_Clear(clients[plrNum].smoother); ClPlayer_UpdateOrigin(plrNum); } if(state->pendingFixes & DDPF_FIXMOM) { state->pendingFixes &= ~DDPF_FIXMOM; ddpl->fixAcked.mom = ddpl->fixCounter.mom; sendAck = true; LOGDEV_NET_MSG("Applying mom %s to mobj %p and clmo %i") << Vector3d(state->pendingMomFix).asText() << mo << clmo->thinker.id; mo->mom[MX] = clmo->mom[VX] = state->pendingMomFix[VX]; mo->mom[MY] = clmo->mom[VY] = state->pendingMomFix[VY]; mo->mom[MZ] = clmo->mom[VZ] = state->pendingMomFix[VZ]; } // We'll only need to ack fixes targeted to the consoleplayer. if(sendAck && plrNum == consolePlayer) { // Send an acknowledgement. Msg_Begin(PCL_ACK_PLAYER_FIX); Writer_WriteInt32(msgWriter, ddpl->fixAcked.angles); Writer_WriteInt32(msgWriter, ddpl->fixAcked.origin); Writer_WriteInt32(msgWriter, ddpl->fixAcked.mom); Msg_End(); Net_SendBuffer(0, 0); } } void ClPlayer_HandleFix() { LOG_AS("Cl_HandlePlayerFix"); // Target player. int plrNum = Reader_ReadByte(msgReader); player_t *plr = &ddPlayers[plrNum]; ddplayer_t *ddpl = &plr->shared; clplayerstate_t *state = ClPlayer_State(plrNum); // What to fix? int fixes = Reader_ReadUInt32(msgReader); state->pendingFixTargetClMobjId = Reader_ReadUInt16(msgReader); LOGDEV_NET_MSG("Fixing player %i") << plrNum; if(fixes & 1) // fix angles? { ddpl->fixCounter.angles = Reader_ReadInt32(msgReader); state->pendingAngleFix = Reader_ReadUInt32(msgReader); state->pendingLookDirFix = Reader_ReadFloat(msgReader); state->pendingFixes |= DDPF_FIXANGLES; LOGDEV_NET_VERBOSE("Pending fix angles %i: angle=%x, lookdir=%f") << ddpl->fixAcked.angles << state->pendingAngleFix << state->pendingLookDirFix; } if(fixes & 2) // fix pos? { ddpl->fixCounter.origin = Reader_ReadInt32(msgReader); state->pendingOriginFix[VX] = Reader_ReadFloat(msgReader); state->pendingOriginFix[VY] = Reader_ReadFloat(msgReader); state->pendingOriginFix[VZ] = Reader_ReadFloat(msgReader); state->pendingFixes |= DDPF_FIXORIGIN; LOGDEV_NET_VERBOSE("Pending fix pos %i: %s") << ddpl->fixAcked.origin << Vector3d(state->pendingOriginFix).asText(); } if(fixes & 4) // fix momentum? { ddpl->fixCounter.mom = Reader_ReadInt32(msgReader); state->pendingMomFix[VX] = Reader_ReadFloat(msgReader); state->pendingMomFix[VY] = Reader_ReadFloat(msgReader); state->pendingMomFix[VZ] = Reader_ReadFloat(msgReader); state->pendingFixes |= DDPF_FIXMOM; LOGDEV_NET_VERBOSE("Pending fix momentum %i: %s") << ddpl->fixAcked.mom << Vector3d(state->pendingMomFix).asText(); } ClPlayer_ApplyPendingFixes(plrNum); } void ClPlayer_MoveLocal(coord_t dx, coord_t dy, coord_t z, bool onground) { player_t *plr = &ddPlayers[consolePlayer]; ddplayer_t *ddpl = &plr->shared; mobj_t *mo = ddpl->mo; if(!mo) return; // Place the new momentum in the appropriate place. cpMom[MX][SECONDS_TO_TICKS(gameTime) % LOCALCAM_WRITE_TICS] = dx; cpMom[MY][SECONDS_TO_TICKS(gameTime) % LOCALCAM_WRITE_TICS] = dy; // Calculate an average. Vector2d mom; for(int i = 0; i < LOCALCAM_WRITE_TICS; ++i) { mom += Vector2d(cpMom[MX][i], cpMom[MY][i]); } mom /= LOCALCAM_WRITE_TICS; mo->mom[MX] = mom.x; mo->mom[MY] = mom.y; if(dx != 0 || dy != 0) { Mobj_Unlink(mo); mo->origin[VX] += dx; mo->origin[VY] += dy; Mobj_Link(mo, MLF_SECTOR | MLF_BLOCKMAP); } mo->_bspLeaf = &Mobj_Map(*mo).bspLeafAt_FixedPrecision(Mobj_Origin(*mo)); mo->floorZ = Mobj_Sector(mo)->floor().height(); mo->ceilingZ = Mobj_Sector(mo)->ceiling().height(); if(onground) { mo->origin[VZ] = z - 1; } else { mo->origin[VZ] = z; } ClPlayer_UpdateOrigin(consolePlayer); } void ClPlayer_ReadDelta() { LOG_AS("ClPlayer_ReadDelta2"); /// @todo Do not assume the CURRENT map. Map &map = App_WorldSystem().map(); int df = 0; ushort num; // The first byte consists of a player number and some flags. num = Reader_ReadByte(msgReader); df = (num & 0xf0) << 8; df |= Reader_ReadByte(msgReader); // Second byte is just flags. num &= 0xf; // Clear the upper bits of the number. clplayerstate_t *s = &clPlayerStates[num]; ddplayer_t *ddpl = &ddPlayers[num].shared; if(df & PDF_MOBJ) { mobj_t *old = map.clMobjFor(s->clMobjId); ushort newId = Reader_ReadUInt16(msgReader); // Make sure the 'new' mobj is different than the old one; // there will be linking problems otherwise. if(newId != s->clMobjId) { // We are now changing the player's mobj. mobj_t *clmo = 0; //clmoinfo_t* info = 0; bool justCreated = false; s->clMobjId = newId; // Find the new mobj. clmo = map.clMobjFor(s->clMobjId); //info = ClMobj_GetInfo(clmo); if(!clmo) { LOGDEV_NET_NOTE("Player %i's new clmobj is %i, but we haven't received it yet") << num << newId; // This mobj hasn't yet been sent to us. // We should be receiving the rest of the info very shortly. clmo = map.clMobjFor(s->clMobjId, true/*create*/); //info = ClMobj_GetInfo(clmo); /* if(num == consolePlayer) { // Mark everything known about our local player. //info->flags |= CLMF_KNOWN; }*/ justCreated = true; } else { // The client mobj is already known to us. // Unlink it (not interactive or visible). Mobj_Unlink(clmo); } clmo->dPlayer = ddpl; // Make the old clmobj a non-player one (if any). if(old) { old->dPlayer = NULL; ClMobj_Link(old); } // If it was just created, the coordinates are not yet correct. // The update will be made when the mobj data is received. if(!justCreated) // && num != consolePlayer) { LOGDEV_NET_XVERBOSE("Copying clmo %i state to real player %i mobj %p") << newId << num << ddpl->mo; Cl_UpdateRealPlayerMobj(ddpl->mo, clmo, 0xffffffff, true); } LOGDEV_NET_VERBOSE("Player %i: mobj=%i old=%p x=%.1f y=%.1f z=%.1f Fz=%.1f Cz=%.1f") << num << s->clMobjId << old << clmo->origin[VX] << clmo->origin[VY] << clmo->origin[VZ] << clmo->floorZ << clmo->ceilingZ; LOGDEV_NET_VERBOSE("Player %i using mobj id %i") << num << s->clMobjId; } } if(df & PDF_FORWARDMOVE) { s->forwardMove = (char) Reader_ReadByte(msgReader) * 2048; } if(df & PDF_SIDEMOVE) { s->sideMove = (char) Reader_ReadByte(msgReader) * 2048; } if(df & PDF_ANGLE) { //s->angle = Reader_ReadByte(msgReader) << 24; DENG_UNUSED(Reader_ReadByte(msgReader)); } if(df & PDF_TURNDELTA) { s->turnDelta = ((char) Reader_ReadByte(msgReader) << 24) / 16; } if(df & PDF_FRICTION) { s->friction = Reader_ReadByte(msgReader) << 8; } if(df & PDF_EXTRALIGHT) { int val = Reader_ReadByte(msgReader); ddpl->fixedColorMap = val & 7; ddpl->extraLight = val & 0xf8; } if(df & PDF_FILTER) { uint filter = Reader_ReadUInt32(msgReader); ddpl->filterColor[CR] = (filter & 0xff) / 255.f; ddpl->filterColor[CG] = ((filter >> 8) & 0xff) / 255.f; ddpl->filterColor[CB] = ((filter >> 16) & 0xff) / 255.f; ddpl->filterColor[CA] = ((filter >> 24) & 0xff) / 255.f; if(ddpl->filterColor[CA] > 0) { ddpl->flags |= DDPF_REMOTE_VIEW_FILTER; } else { ddpl->flags &= ~DDPF_REMOTE_VIEW_FILTER; } LOG_NET_XVERBOSE("View filter color set remotely to %s") << Vector4f(ddpl->filterColor).asText(); } if(df & PDF_PSPRITES) { for(int i = 0; i < 2; ++i) { // First the flags. int psdf = Reader_ReadByte(msgReader); ddpsprite_t *psp = ddpl->pSprites + i; if(psdf & PSDF_STATEPTR) { int idx = Reader_ReadPackedUInt16(msgReader); if(!idx) { psp->statePtr = 0; } else if(idx < runtimeDefs.states.size()) { psp->statePtr = &runtimeDefs.states[idx - 1]; psp->tics = psp->statePtr->tics; } } /*if(psdf & PSDF_LIGHT) { psp->light = Reader_ReadByte(msgReader) / 255.0f; }*/ if(psdf & PSDF_ALPHA) { psp->alpha = Reader_ReadByte(msgReader) / 255.0f; } if(psdf & PSDF_STATE) { psp->state = Reader_ReadByte(msgReader); } if(psdf & PSDF_OFFSET) { psp->offset[VX] = (char) Reader_ReadByte(msgReader) * 2; psp->offset[VY] = (char) Reader_ReadByte(msgReader) * 2; } } } } mobj_t *ClPlayer_LocalGameMobj(int plrNum) { return ddPlayers[plrNum].shared.mo; } bool ClPlayer_IsFreeToMove(int plrNum) { mobj_t *mo = ClPlayer_LocalGameMobj(plrNum); if(!mo) return false; return (mo->origin[VZ] >= mo->floorZ && mo->origin[VZ] + mo->height <= mo->ceilingZ); } doomsday-stable-1.15.7/doomsday/client/src/client/cl_infine.cpp0000664000175000017500000000527112641367670023775 0ustar jaakkojaakko/** @file cl_infine.cpp Clientside InFine. * * @authors Copyright © 2003-2010 Jaakko Keränen * @authors Copyright © 2006-2010 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "client/cl_infine.h" #include "de_console.h" #include "network/net_main.h" #include "network/net_msg.h" #include "ui/infine/finaleinterpreter.h" #include static finaleid_t currentFinale; static finaleid_t remoteFinale; finaleid_t Cl_CurrentFinale() { return currentFinale; } void Cl_Finale(Reader *msg) { LOG_AS("Cl_Finale"); byte *script = 0; int const flags = Reader_ReadByte(msg); finaleid_t const finaleId = Reader_ReadUInt32(msg); if(flags & FINF_SCRIPT) { // Read the script into map-scope memory. It will be freed // when the next map is loaded. int len = Reader_ReadUInt32(msg); script = (byte *) M_Malloc(len + 1); Reader_Read(msg, script, len); script[len] = 0; } if((flags & FINF_SCRIPT) && (flags & FINF_BEGIN)) { // Start the script. currentFinale = FI_Execute((const char*)script, FF_LOCAL); remoteFinale = finaleId; LOGDEV_NET_MSG("Started finale %i (remote id %i)") << currentFinale << remoteFinale; } /// @todo Wouldn't hurt to make sure that the server is talking about the /// same finale as before... (check remoteFinale) if((flags & FINF_END) && currentFinale) { FI_ScriptTerminate(currentFinale); currentFinale = 0; remoteFinale = 0; } if((flags & FINF_SKIP) && currentFinale) { FI_ScriptRequestSkip(currentFinale); } if(script) M_Free(script); } void Cl_RequestFinaleSkip() { // First the flags. Msg_Begin(PCL_FINALE_REQUEST); Writer_WriteUInt32(msgWriter, remoteFinale); Writer_WriteUInt16(msgWriter, 1); // skip Msg_End(); LOGDEV_NET_MSG("Requesting skip on finale %i") << remoteFinale; Net_SendBuffer(0, 0); } doomsday-stable-1.15.7/doomsday/client/src/client/cl_main.cpp0000664000175000017500000003276712641367670023463 0ustar jaakkojaakko/** @file cl_main.cpp Network client. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "client/cl_def.h" #include "api_client.h" #include "client/cl_frame.h" #include "client/cl_infine.h" #include "client/cl_player.h" #include "client/cl_sound.h" #include "client/cl_world.h" #include "audio/s_main.h" #include "network/net_main.h" #include "network/net_buf.h" #include "network/net_demo.h" #include "world/map.h" #include "world/p_players.h" #include #include #include #include #include #include using namespace de; ident_t clientID; bool handshakeReceived; int gameReady; int serverTime; bool netLoggedIn; // Logged in to the server. int clientPaused; // Set by the server. void Cl_InitID() { if(int i = CommandLine_CheckWith("-id", 1)) { clientID = strtoul(CommandLine_At(i + 1), 0, 0); LOG_NET_NOTE("Using custom client ID: 0x%08x") << clientID; return; } // Read the client ID number file. srand(time(NULL)); if(FILE *file = fopen("client.id", "rb")) { if(fread(&clientID, sizeof(clientID), 1, file)) { clientID = ULONG(clientID); fclose(file); return; } fclose(file); } // Ah-ha, we need to generate a new ID. clientID = (ident_t) ULONG(Timer_RealMilliseconds() * rand() + (rand() & 0xfff) + ((rand() & 0xfff) << 12) + ((rand() & 0xff) << 24)); // Write it to the file. if(FILE *file = fopen("client.id", "wb")) { fwrite(&clientID, sizeof(clientID), 1, file); fclose(file); } } int Cl_GameReady() { return (handshakeReceived && gameReady); } void Cl_CleanUp() { LOG_NET_MSG("Cleaning up client state"); clientPaused = false; handshakeReceived = false; //S_Reset(); S_MapChange(); // Reset the local world state. App_WorldSystem().reset(); // Discard the translation tables for the server we've just left. Cl_ResetTransTables(); // Reset any view effects. GL_ResetViewEffects(); // Forget all packets we've received but haven't yet handled. N_ClearMessages(); } void Cl_SendHello() { LOG_AS("Cl_SendHello"); Msg_Begin(PCL_HELLO2); Writer_WriteUInt32(msgWriter, clientID); // The game mode is included in the hello packet. char buf[256]; zap(buf); strncpy(buf, App_CurrentGame().identityKey().toUtf8().constData(), sizeof(buf) - 1); LOGDEV_NET_VERBOSE("game mode = %s") << buf; Writer_Write(msgWriter, buf, 16); Msg_End(); Net_SendBuffer(0, 0); } void Cl_AnswerHandshake() { LOG_AS("Cl_AnswerHandshake"); byte remoteVersion = Reader_ReadByte(msgReader); byte myConsole = Reader_ReadByte(msgReader); uint playersInGame = Reader_ReadUInt32(msgReader); float remoteGameTime = Reader_ReadFloat(msgReader); // Immediately send an acknowledgement. This lets the server evaluate // an approximate ping time. Msg_Begin(PCL_ACK_SHAKE); Msg_End(); Net_SendBuffer(0, 0); // Check the version number. if(remoteVersion != SV_VERSION) { LOG_NET_ERROR("Version conflict! (you:%i, server:%i)") << SV_VERSION << remoteVersion; Con_Execute(CMDS_DDAY, "net disconnect", false, false); Demo_StopPlayback(); Con_Open(true); return; } // Update time and player ingame status. gameTime = remoteGameTime; for(int i = 0; i < DDMAXPLAYERS; ++i) { /// @todo With multiple local players, must clear only the appropriate flags. ddPlayers[i].shared.flags &= ~DDPF_LOCAL; ddPlayers[i].shared.inGame = (playersInGame & (1 << i)) != 0; } consolePlayer = displayPlayer = myConsole; clients[consolePlayer].viewConsole = consolePlayer; // Mark us as the only local player. ddPlayers[consolePlayer].shared.flags |= DDPF_LOCAL; Smoother_Clear(clients[consolePlayer].smoother); ddPlayers[consolePlayer].shared.flags &= ~DDPF_USE_VIEW_FILTER; isClient = true; isServer = false; netLoggedIn = false; clientPaused = false; if(handshakeReceived) return; // This prevents redundant re-initialization. handshakeReceived = true; // Soon after this packet will follow the game's handshake. gameReady = false; Cl_InitFrame(); LOGDEV_NET_MSG("Answering handshake: myConsole:%i, remoteGameTime:%.2f") << myConsole << remoteGameTime; /* * Tell the game that we have arrived. The map will be changed when the * game's handshake arrives (handled in the game). */ gx.NetPlayerEvent(consolePlayer, DDPE_ARRIVAL, 0); // Prepare the client-side data. Cl_InitPlayers(); Cl_InitTransTables(); // Get ready for ticking. DD_ResetTimer(); Con_Executef(CMDS_DDAY, true, "setcon %i", consolePlayer); } void Cl_HandlePlayerInfo() { byte console = Reader_ReadByte(msgReader); size_t len = Reader_ReadUInt16(msgReader); len = MIN_OF(PLAYERNAMELEN - 1, len); char name[PLAYERNAMELEN]; zap(name); Reader_Read(msgReader, name, len); LOG_NET_VERBOSE("Player %i named \"%s\"") << console << name; // Is the console number valid? if(console >= DDMAXPLAYERS) return; player_t *plr = &ddPlayers[console]; bool present = plr->shared.inGame; plr->shared.inGame = true; strcpy(clients[console].name, name); if(!present) { // This is a new player! Let the game know about this. gx.NetPlayerEvent(console, DDPE_ARRIVAL, 0); Smoother_Clear(clients[console].smoother); } } void Cl_PlayerLeaves(int plrNum) { LOG_NET_NOTE("Player %i has left the game") << plrNum; ddPlayers[plrNum].shared.inGame = false; gx.NetPlayerEvent(plrNum, DDPE_EXIT, 0); } void Cl_GetPackets() { // All messages come from the server. while(Net_GetPacket()) { Msg_BeginRead(); // First check for packets that are only valid when // a game is in progress. if(Cl_GameReady()) { switch(netBuffer.msg.type) { case PSV_FIRST_FRAME2: case PSV_FRAME2: Cl_Frame2Received(netBuffer.msg.type); Msg_EndRead(); continue; // Get the next packet. case PSV_SOUND: Cl_Sound(); Msg_EndRead(); continue; // Get the next packet. default: break; } } // How about the rest? switch(netBuffer.msg.type) { case PSV_PLAYER_FIX: ClPlayer_HandleFix(); break; case PKT_DEMOCAM: case PKT_DEMOCAM_RESUME: Demo_ReadLocalCamera(); break; case PKT_PING: Net_PingResponse(); break; case PSV_SYNC: // The server updates our time. Latency has been taken into // account, so... gameTime = Reader_ReadFloat(msgReader); LOGDEV_NET_VERBOSE("PSV_SYNC: gameTime=%.3f") << gameTime; DD_ResetTimer(); break; case PSV_HANDSHAKE: Cl_AnswerHandshake(); break; case PSV_MATERIAL_ARCHIVE: Cl_ReadServerMaterials(); break; case PSV_MOBJ_TYPE_ID_LIST: Cl_ReadServerMobjTypeIDs(); break; case PSV_MOBJ_STATE_ID_LIST: Cl_ReadServerMobjStateIDs(); break; case PKT_PLAYER_INFO: Cl_HandlePlayerInfo(); break; case PSV_PLAYER_EXIT: Cl_PlayerLeaves(Reader_ReadByte(msgReader)); break; case PKT_CHAT: { int msgfrom = Reader_ReadByte(msgReader); int mask = Reader_ReadUInt32(msgReader); DENG2_UNUSED(mask); size_t len = Reader_ReadUInt16(msgReader); char *msg = (char *) M_Malloc(len + 1); Reader_Read(msgReader, msg, len); msg[len] = 0; Net_ShowChatMessage(msgfrom, msg); gx.NetPlayerEvent(msgfrom, DDPE_CHAT_MESSAGE, msg); M_Free(msg); break; } case PSV_SERVER_CLOSE: // We should quit? netLoggedIn = false; Con_Execute(CMDS_DDAY, "net disconnect", true, false); break; case PSV_CONSOLE_TEXT: { uint32_t conFlags = Reader_ReadUInt32(msgReader); uint16_t textLen = Reader_ReadUInt16(msgReader); char *text = (char *) M_Malloc(textLen + 1); Reader_Read(msgReader, text, textLen); text[textLen] = 0; DENG_UNUSED(conFlags); LOG_NOTE("%s") << text; M_Free(text); break; } case PKT_LOGIN: // Server responds to our login request. Let's see if we were successful. netLoggedIn = CPP_BOOL(Reader_ReadByte(msgReader)); break; case PSV_FINALE: Cl_Finale(msgReader); break; case PSV_FRAME2: case PSV_FIRST_FRAME2: case PSV_SOUND: LOGDEV_NET_WARNING("Packet type %i was discarded (client not ready)") << netBuffer.msg.type; break; default: if(netBuffer.msg.type >= PKT_GAME_MARKER) { gx.HandlePacket(netBuffer.player, netBuffer.msg.type, netBuffer.msg.data, netBuffer.length); } else { LOG_NET_WARNING("Packet was discarded (unknown type %i)") << netBuffer.msg.type; } } Msg_EndRead(); } } #ifdef DENG_DEBUG /** * Check the state of the client on engineside. A debugging utility. */ static void assertPlayerIsValid(int plrNum) { LOG_AS("Client.assertPlayerIsValid"); if(!isClient || !Cl_GameReady() || clientPaused) return; if(plrNum < 0 || plrNum >= DDMAXPLAYERS) return; player_t *plr = &ddPlayers[plrNum]; clplayerstate_t *s = ClPlayer_State(plrNum); // Must have a mobj! if(!s->clMobjId || !plr->shared.mo) return; mobj_t *clmo = ClMobj_Find(s->clMobjId); if(!clmo) { LOGDEV_NET_NOTE("Player %i does not have a clmobj yet [%i]") << plrNum << s->clMobjId; return; } mobj_t *mo = plr->shared.mo; /* ("Assert: client %i, clmo %i (flags 0x%x)", plrNum, clmo->thinker.id, clmo->ddFlags); */ // Make sure the flags are correctly set for a client. if(mo->ddFlags & DDMF_REMOTE) { LOGDEV_NET_NOTE("Player %i's mobj should not be remote") << plrNum; } if(clmo->ddFlags & DDMF_SOLID) { LOGDEV_NET_NOTE("Player %i's clmobj should not be solid (when player is alive)") << plrNum; } } #endif // DENG_DEBUG void Cl_Ticker(timespan_t ticLength) { if(!isClient || !Cl_GameReady() || clientPaused) return; // On clientside, players are represented by two mobjs: the real mobj, // created by the Game, is the one that is visible and modified by game // logic. We'll need to sync the hidden client mobj (that receives all // the changes from the server) to match the changes. The game ticker // has already been run when Cl_Ticker() is called, so let's update the // player's clmobj to its updated state. for(int i = 0; i < DDMAXPLAYERS; ++i) { if(!ddPlayers[i].shared.inGame) continue; if(i != consolePlayer) { if(ddPlayers[i].shared.mo) { Smoother_AddPos(clients[i].smoother, Cl_FrameGameTime(), ddPlayers[i].shared.mo->origin[VX], ddPlayers[i].shared.mo->origin[VY], ddPlayers[i].shared.mo->origin[VZ], false); } // Update the smoother. Smoother_Advance(clients[i].smoother, ticLength); } ClPlayer_ApplyPendingFixes(i); ClPlayer_UpdateOrigin(i); #ifdef DENG_DEBUG assertPlayerIsValid(i); #endif } if(App_WorldSystem().hasMap()) { App_WorldSystem().map().expireClMobjs(); } } /** * Clients use this to establish a remote connection to the server. */ D_CMD(Login) { DENG2_UNUSED(src); // Only clients can log in. if(!isClient) return false; Msg_Begin(PKT_LOGIN); // Write the password. if(argc == 1) { Writer_WriteByte(msgWriter, 0); // No password given! } else if(strlen(argv[1]) <= 255) { Writer_WriteByte(msgWriter, strlen(argv[1])); Writer_Write(msgWriter, argv[1], strlen(argv[1])); } else { Msg_End(); return false; } Msg_End(); Net_SendBuffer(0, 0); return true; } doomsday-stable-1.15.7/doomsday/client/src/client/clplanemover.cpp0000664000175000017500000001017712641367670024537 0ustar jaakkojaakko/** @file clplanemover.cpp Clientside plane mover (thinker). * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "client/clplanemover.h" #include "client/cl_def.h" #include "client/cl_player.h" #include "world/map.h" #include "world/thinkers.h" #include "world/p_players.h" #include "Sector" using namespace de; thinker_s *ClPlaneMover::newThinker(Plane &plane, coord_t dest, float speed) // static { Thinker th(Thinker::AllocateMemoryZone); th.setData(new ClPlaneMover(plane, dest, speed)); // Add to the map. thinker_s *ptr = th.take(); plane.map().thinkers().add(*ptr, false /* not public */); LOGDEV_MAP_XVERBOSE("New mover %p") << ptr; // Immediate move? if(fequal(speed, 0)) { // This will remove the thinker immediately if the move is ok. THINKER_DATA(*ptr, ClPlaneMover).think(); } return ptr; } ClPlaneMover::ClPlaneMover(Plane &plane, coord_t dest, float speed) : _plane (&plane) , _destination ( dest ) , _speed ( speed) { // Set the right sign for speed. if(_destination < P_GetDoublep(_plane, DMU_HEIGHT)) { _speed = -_speed; } // Update speed and target height. P_SetDoublep(_plane, DMU_TARGET_HEIGHT, _destination); P_SetFloatp(_plane, DMU_SPEED, _speed); _plane->addMover(*this); } ClPlaneMover::~ClPlaneMover() { _plane->removeMover(*this); } void ClPlaneMover::think() { LOG_AS("ClPlaneMover::think"); // Can we think yet? if(!Cl_GameReady()) return; DENG2_ASSERT(_plane != 0); // The move is cancelled if the consolePlayer becomes obstructed. bool const freeMove = ClPlayer_IsFreeToMove(consolePlayer); // How's the gap? bool remove = false; coord_t const original = P_GetDoublep(_plane, DMU_HEIGHT); if(de::abs(_speed) > 0 && de::abs(_destination - original) > de::abs(_speed)) { // Do the move. P_SetDoublep(_plane, DMU_HEIGHT, original + _speed); } else { // We have reached the destination. P_SetDoublep(_plane, DMU_HEIGHT, _destination); // This thinker can now be removed. remove = true; } LOGDEV_MAP_XVERBOSE_DEBUGONLY("plane height %f in sector #%i", P_GetDoublep(_plane, DMU_HEIGHT) << _plane->sector().indexInMap()); // Let the game know of this. if(gx.SectorHeightChangeNotification) { gx.SectorHeightChangeNotification(_plane->sector().indexInMap()); } // Make sure the client didn't get stuck as a result of this move. if(freeMove != ClPlayer_IsFreeToMove(consolePlayer)) { LOG_MAP_VERBOSE("move blocked in sector #%i, undoing move") << _plane->sector().indexInMap(); // Something was blocking the way! Go back to original height. P_SetDoublep(_plane, DMU_HEIGHT, original); if(gx.SectorHeightChangeNotification) { gx.SectorHeightChangeNotification(_plane->sector().indexInMap()); } } else { // Can we remove this thinker? if(remove) { LOG_MAP_VERBOSE("finished in sector #%i") << _plane->sector().indexInMap(); // It stops. P_SetDoublep(_plane, DMU_SPEED, 0); _plane->map().thinkers().remove(thinker()); // we get deleted } } } doomsday-stable-1.15.7/doomsday/client/src/client/cl_world.cpp0000664000175000017500000002730312641367670023654 0ustar jaakkojaakko/** @file cl_world.cpp Clientside world management. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "client/cl_world.h" #include "client/cl_player.h" #include "api_map.h" #include "api_materialarchive.h" #include "network/net_msg.h" #include "network/protocol.h" #include "world/map.h" #include "Sector" #include "Surface" #include using namespace de; static MaterialArchive *serverMaterials; typedef QVector IndexTransTable; static IndexTransTable xlatMobjType; static IndexTransTable xlatMobjState; void Cl_InitTransTables() { serverMaterials = 0; xlatMobjType.clear(); xlatMobjState.clear(); } void Cl_ResetTransTables() { if(serverMaterials) { MaterialArchive_Delete(serverMaterials); serverMaterials = 0; } xlatMobjType.clear(); xlatMobjState.clear(); } void Cl_ReadServerMaterials() { LOG_AS("Cl_ReadServerMaterials"); if(!serverMaterials) { serverMaterials = MaterialArchive_NewEmpty(false /*no segment check*/); } MaterialArchive_Read(serverMaterials, msgReader, -1 /*no forced version*/); LOGDEV_NET_VERBOSE("Received %i materials") << MaterialArchive_Count(serverMaterials); } void Cl_ReadServerMobjTypeIDs() { LOG_AS("Cl_ReadServerMobjTypeIDs"); StringArray *ar = StringArray_New(); StringArray_Read(ar, msgReader); LOGDEV_NET_VERBOSE("Received %i mobj type IDs") << StringArray_Size(ar); xlatMobjType.resize(StringArray_Size(ar)); // Translate the type IDs to local. for(int i = 0; i < StringArray_Size(ar); ++i) { xlatMobjType[i] = Def_GetMobjNum(StringArray_At(ar, i)); if(xlatMobjType[i] < 0) { LOG_NET_WARNING("Could not find '%s' in local thing definitions") << StringArray_At(ar, i); } } StringArray_Delete(ar); } void Cl_ReadServerMobjStateIDs() { LOG_AS("Cl_ReadServerMobjStateIDs"); StringArray *ar = StringArray_New(); StringArray_Read(ar, msgReader); LOGDEV_NET_VERBOSE("Received %i mobj state IDs") << StringArray_Size(ar); xlatMobjState.resize(StringArray_Size(ar)); // Translate the type IDs to local. for(int i = 0; i < StringArray_Size(ar); ++i) { xlatMobjState[i] = Def_GetStateNum(StringArray_At(ar, i)); if(xlatMobjState[i] < 0) { LOG_NET_WARNING("Could not find '%s' in local state definitions") << StringArray_At(ar, i); } } StringArray_Delete(ar); } Material *Cl_LocalMaterial(materialarchive_serialid_t archId) { if(!serverMaterials) { // Can't do it. LOGDEV_NET_WARNING("Cannot translate serial id %i, server has not sent its materials!") << archId; return 0; } return (Material *) MaterialArchive_Find(serverMaterials, archId, 0); } int Cl_LocalMobjType(int serverMobjType) { if(serverMobjType < 0 || serverMobjType >= xlatMobjType.size()) return 0; // Invalid type. return xlatMobjType[serverMobjType]; } int Cl_LocalMobjState(int serverMobjState) { if(serverMobjState < 0 || serverMobjState >= xlatMobjState.size()) return 0; // Invalid state. return xlatMobjState[serverMobjState]; } void Cl_ReadSectorDelta(int /*deltaType*/) { /// @todo Do not assume the CURRENT map. Map &map = App_WorldSystem().map(); #define PLN_FLOOR 0 #define PLN_CEILING 1 float height[2] = { 0, 0 }; float target[2] = { 0, 0 }; float speed[2] = { 0, 0 }; // Sector index number. Sector *sec = map.sectorPtr(Reader_ReadUInt16(msgReader)); DENG2_ASSERT(sec); // Flags. int df = Reader_ReadPackedUInt32(msgReader); if(df & SDF_FLOOR_MATERIAL) { P_SetPtrp(sec, DMU_FLOOR_OF_SECTOR | DMU_MATERIAL, Cl_LocalMaterial(Reader_ReadPackedUInt16(msgReader))); } if(df & SDF_CEILING_MATERIAL) { P_SetPtrp(sec, DMU_CEILING_OF_SECTOR | DMU_MATERIAL, Cl_LocalMaterial(Reader_ReadPackedUInt16(msgReader))); } if(df & SDF_LIGHT) P_SetFloatp(sec, DMU_LIGHT_LEVEL, Reader_ReadByte(msgReader) / 255.0f); if(df & SDF_FLOOR_HEIGHT) height[PLN_FLOOR] = FIX2FLT(Reader_ReadInt16(msgReader) << 16); if(df & SDF_CEILING_HEIGHT) height[PLN_CEILING] = FIX2FLT(Reader_ReadInt16(msgReader) << 16); if(df & SDF_FLOOR_TARGET) target[PLN_FLOOR] = FIX2FLT(Reader_ReadInt16(msgReader) << 16); if(df & SDF_FLOOR_SPEED) speed[PLN_FLOOR] = FIX2FLT(Reader_ReadByte(msgReader) << (df & SDF_FLOOR_SPEED_44 ? 12 : 15)); if(df & SDF_CEILING_TARGET) target[PLN_CEILING] = FIX2FLT(Reader_ReadInt16(msgReader) << 16); if(df & SDF_CEILING_SPEED) speed[PLN_CEILING] = FIX2FLT(Reader_ReadByte(msgReader) << (df & SDF_CEILING_SPEED_44 ? 12 : 15)); if(df & (SDF_COLOR_RED | SDF_COLOR_GREEN | SDF_COLOR_BLUE)) { Vector3f newColor = sec->lightColor(); if(df & SDF_COLOR_RED) newColor.x = Reader_ReadByte(msgReader) / 255.f; if(df & SDF_COLOR_GREEN) newColor.y = Reader_ReadByte(msgReader) / 255.f; if(df & SDF_COLOR_BLUE) newColor.z = Reader_ReadByte(msgReader) / 255.f; sec->setLightColor(newColor); } if(df & (SDF_FLOOR_COLOR_RED | SDF_FLOOR_COLOR_GREEN | SDF_FLOOR_COLOR_BLUE)) { Vector3f newColor = sec->floorSurface().tintColor(); if(df & SDF_FLOOR_COLOR_RED) newColor.x = Reader_ReadByte(msgReader) / 255.f; if(df & SDF_FLOOR_COLOR_GREEN) newColor.y = Reader_ReadByte(msgReader) / 255.f; if(df & SDF_FLOOR_COLOR_BLUE) newColor.z = Reader_ReadByte(msgReader) / 255.f; sec->floorSurface().setTintColor(newColor); } if(df & (SDF_CEIL_COLOR_RED | SDF_CEIL_COLOR_GREEN | SDF_CEIL_COLOR_BLUE)) { Vector3f newColor = sec->ceilingSurface().tintColor(); if(df & SDF_CEIL_COLOR_RED) newColor.x = Reader_ReadByte(msgReader) / 255.f; if(df & SDF_CEIL_COLOR_GREEN) newColor.y = Reader_ReadByte(msgReader) / 255.f; if(df & SDF_CEIL_COLOR_BLUE) newColor.z = Reader_ReadByte(msgReader) / 255.f; sec->ceilingSurface().setTintColor(newColor); } // The whole delta has now been read. // Do we need to start any moving planes? if(df & SDF_FLOOR_HEIGHT) { ClPlaneMover::newThinker(sec->floor(), height[PLN_FLOOR], 0); } else if(df & (SDF_FLOOR_TARGET | SDF_FLOOR_SPEED)) { ClPlaneMover::newThinker(sec->floor(), target[PLN_FLOOR], speed[PLN_FLOOR]); } if(df & SDF_CEILING_HEIGHT) { ClPlaneMover::newThinker(sec->ceiling(), height[PLN_CEILING], 0); } else if(df & (SDF_CEILING_TARGET | SDF_CEILING_SPEED)) { ClPlaneMover::newThinker(sec->ceiling(), target[PLN_CEILING], speed[PLN_CEILING]); } #undef PLN_CEILING #undef PLN_FLOOR } void Cl_ReadSideDelta(int /*deltaType*/) { /// @todo Do not assume the CURRENT map. Map &map = App_WorldSystem().map(); int const index = Reader_ReadUInt16(msgReader); int const df = Reader_ReadPackedUInt32(msgReader); // Flags. LineSide *side = map.sidePtr(index); DENG2_ASSERT(side != 0); if(df & SIDF_TOP_MATERIAL) { int matIndex = Reader_ReadPackedUInt16(msgReader); side->top().setMaterial(Cl_LocalMaterial(matIndex)); } if(df & SIDF_MID_MATERIAL) { int matIndex = Reader_ReadPackedUInt16(msgReader); side->middle().setMaterial(Cl_LocalMaterial(matIndex)); } if(df & SIDF_BOTTOM_MATERIAL) { int matIndex = Reader_ReadPackedUInt16(msgReader); side->bottom().setMaterial(Cl_LocalMaterial(matIndex)); } if(df & SIDF_LINE_FLAGS) { // The delta includes the entire lowest byte. int lineFlags = Reader_ReadByte(msgReader); Line &line = side->line(); line.setFlags((line.flags() & ~0xff) | lineFlags, de::ReplaceFlags); } if(df & (SIDF_TOP_COLOR_RED | SIDF_TOP_COLOR_GREEN | SIDF_TOP_COLOR_BLUE)) { Vector3f newColor = side->top().tintColor(); if(df & SIDF_TOP_COLOR_RED) newColor.x = Reader_ReadByte(msgReader) / 255.f; if(df & SIDF_TOP_COLOR_GREEN) newColor.y = Reader_ReadByte(msgReader) / 255.f; if(df & SIDF_TOP_COLOR_BLUE) newColor.z = Reader_ReadByte(msgReader) / 255.f; side->top().setTintColor(newColor); } if(df & (SIDF_MID_COLOR_RED | SIDF_MID_COLOR_GREEN | SIDF_MID_COLOR_BLUE)) { Vector3f newColor = side->middle().tintColor(); if(df & SIDF_MID_COLOR_RED) newColor.x = Reader_ReadByte(msgReader) / 255.f; if(df & SIDF_MID_COLOR_GREEN) newColor.y = Reader_ReadByte(msgReader) / 255.f; if(df & SIDF_MID_COLOR_BLUE) newColor.z = Reader_ReadByte(msgReader) / 255.f; side->middle().setTintColor(newColor); } if(df & SIDF_MID_COLOR_ALPHA) { side->middle().setOpacity(Reader_ReadByte(msgReader) / 255.f); } if(df & (SIDF_BOTTOM_COLOR_RED | SIDF_BOTTOM_COLOR_GREEN | SIDF_BOTTOM_COLOR_BLUE)) { Vector3f newColor = side->bottom().tintColor(); if(df & SIDF_BOTTOM_COLOR_RED) newColor.x = Reader_ReadByte(msgReader) / 255.f; if(df & SIDF_BOTTOM_COLOR_GREEN) newColor.y = Reader_ReadByte(msgReader) / 255.f; if(df & SIDF_BOTTOM_COLOR_BLUE) newColor.z = Reader_ReadByte(msgReader) / 255.f; side->bottom().setTintColor(newColor); } if(df & SIDF_MID_BLENDMODE) { side->middle().setBlendMode(blendmode_t(Reader_ReadInt32(msgReader))); } if(df & SIDF_FLAGS) { // The delta includes the entire lowest byte. int sideFlags = Reader_ReadByte(msgReader); side->setFlags((side->flags() & ~0xff) | sideFlags, de::ReplaceFlags); } } void Cl_ReadPolyDelta() { /// @todo Do not assume the CURRENT map. Map &map = App_WorldSystem().map(); Polyobj &pob = map.polyobj(Reader_ReadPackedUInt16(msgReader)); int const df = Reader_ReadByte(msgReader); // Flags. if(df & PODF_DEST_X) { pob.dest[VX] = Reader_ReadFloat(msgReader); } if(df & PODF_DEST_Y) { pob.dest[VY] = Reader_ReadFloat(msgReader); } if(df & PODF_SPEED) { pob.speed = Reader_ReadFloat(msgReader); } if(df & PODF_DEST_ANGLE) { pob.destAngle = ((angle_t)Reader_ReadInt16(msgReader)) << 16; } if(df & PODF_ANGSPEED) { pob.angleSpeed = ((angle_t)Reader_ReadInt16(msgReader)) << 16; } if(df & PODF_PERPETUAL_ROTATE) { pob.destAngle = -1; } // Update/create the polymover thinker. ClPolyMover::newThinker(pob, /* move: */ CPP_BOOL(df & (PODF_DEST_X | PODF_DEST_Y | PODF_SPEED)), /* rotate: */ CPP_BOOL(df & (PODF_DEST_ANGLE | PODF_ANGSPEED | PODF_PERPETUAL_ROTATE))); } doomsday-stable-1.15.7/doomsday/client/src/client/cl_sound.cpp0000664000175000017500000002017012641367670023650 0ustar jaakkojaakko/** @file cl_sound.cpp Clientside sounds. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "client/cl_sound.h" #include "de_console.h" #include "de_network.h" #include "de_play.h" #include "de_audio.h" using namespace de; void Cl_ReadSoundDelta(deltatype_t type) { LOG_AS("Cl_ReadSoundDelta"); /// @todo Do not assume the CURRENT map. Map &map = App_WorldSystem().map(); int sound = 0, soundFlags = 0; mobj_t *cmo = 0; thid_t mobjId = 0; Sector *sector = 0; Polyobj *poly = 0; LineSide *side = 0; mobj_t *emitter = 0; uint16_t const deltaId = Reader_ReadUInt16(msgReader); byte const flags = Reader_ReadByte(msgReader); bool skip = false; if(type == DT_SOUND) { // Delta ID is the sound ID. sound = deltaId; } else if(type == DT_MOBJ_SOUND) // Mobj as emitter { mobjId = deltaId; if((cmo = ClMobj_Find(mobjId)) != NULL) { ClientMobjThinkerData::RemoteSync* info = ClMobj_GetInfo(cmo); if(info->flags & CLMF_HIDDEN) { // We can't play sounds from hidden mobjs, because we // aren't sure exactly where they are located. cmo = NULL; LOG_NET_VERBOSE("Can't find sound emitter ") << mobjId; } else { emitter = cmo; } } } else if(type == DT_SECTOR_SOUND) // Plane as emitter { int index = deltaId; if(!(sector = map.sectorPtr(index))) { LOG_NET_WARNING("Received sound delta has invalid sector index %i") << index; skip = true; } } else if(type == DT_SIDE_SOUND) // Side section as emitter { int index = deltaId; if(!(side = map.sidePtr(index))) { LOG_NET_WARNING("Received sound delta has invalid side index %i") << index; skip = true; } } else // DT_POLY_SOUND { int index = deltaId; LOG_NET_XVERBOSE("DT_POLY_SOUND: poly=%d") << index; if(!(emitter = (mobj_t *) (poly = map.polyobjPtr(index)))) { LOG_NET_WARNING("Received sound delta has invalid polyobj index %i") << index; skip = true; } } if(type != DT_SOUND) { // The sound ID. sound = Reader_ReadUInt16(msgReader); } if(type == DT_SECTOR_SOUND) { // Select the emitter for the sound. if(flags & SNDDF_PLANE_FLOOR) { emitter = (mobj_t *) §or->floor().soundEmitter(); } else if(flags & SNDDF_PLANE_CEILING) { emitter = (mobj_t *) §or->ceiling().soundEmitter(); } else { // Must be the sector's sound emitter, then. emitter = (mobj_t *) §or->soundEmitter(); } } if(type == DT_SIDE_SOUND) { if(flags & SNDDF_SIDE_MIDDLE) emitter = (mobj_t *) &side->middleSoundEmitter(); else if(flags & SNDDF_SIDE_TOP) emitter = (mobj_t *) &side->topSoundEmitter(); else if(flags & SNDDF_SIDE_BOTTOM) emitter = (mobj_t *) &side->bottomSoundEmitter(); } float volume = 1; if(flags & SNDDF_VOLUME) { byte b = Reader_ReadByte(msgReader); if(b == 255) { // Full volume, no attenuation. soundFlags |= DDSF_NO_ATTENUATION; } else { volume = b / 127.0f; } } if(flags & SNDDF_REPEAT) { soundFlags |= DDSF_REPEAT; } // The delta has been read. Are we skipping? if(skip) return; // Now the entire delta has been read. // Should we start or stop a sound? if(volume > 0 && sound > 0) { // Do we need to queue this sound? if(type == DT_MOBJ_SOUND && !cmo) { ClientMobjThinkerData::RemoteSync *info = 0; // Create a new Hidden clmobj. cmo = map.clMobjFor(mobjId, true/*create*/); info = ClMobj_GetInfo(cmo); info->flags |= CLMF_HIDDEN | CLMF_SOUND; info->sound = sound; info->volume = volume; // The sound will be started when the clmobj is unhidden. return; } // We will start a sound. if(type != DT_SOUND && !emitter) { // Not enough information. LOG_NET_VERBOSE("Cl_ReadSoundDelta2(%i): Insufficient data, snd=%i") << type << sound; return; } // Sounds originating from the viewmobj should really originate // from the real player mobj. if(cmo && cmo->thinker.id == ClPlayer_State(consolePlayer)->clMobjId) { emitter = ddPlayers[consolePlayer].shared.mo; } // First stop any sounds originating from the same emitter. // We allow only one sound per emitter. if(type != DT_SOUND && emitter) { S_StopSound(0, emitter); } S_LocalSoundAtVolume(sound | soundFlags, emitter, volume); } else if(sound >= 0) { // We must stop a sound. We'll only stop sounds from // specific sources. if(emitter) { S_StopSound(sound, emitter); } } } void Cl_Sound() { LOG_AS("Cl_Sound"); /// @todo Do not assume the CURRENT map. Map &map = App_WorldSystem().map(); byte const flags = Reader_ReadByte(msgReader); // Sound ID. int sound; if(flags & SNDF_SHORT_SOUND_ID) { sound = Reader_ReadUInt16(msgReader); } else { sound = Reader_ReadByte(msgReader); } // Is the ID valid? if(sound < 1 || sound >= defs.sounds.size()) { LOGDEV_NET_WARNING("Invalid sound ID %i") << sound; return; } LOGDEV_NET_XVERBOSE("id %i") << sound; int volume = 127; if(flags & SNDF_VOLUME) { volume = Reader_ReadByte(msgReader); if(volume > 127) { volume = 127; sound |= DDSF_NO_ATTENUATION; } } if(flags & SNDF_ID) { thid_t sourceId = Reader_ReadUInt16(msgReader); mobj_t *cmo = ClMobj_Find(sourceId); if(cmo) { S_LocalSoundAtVolume(sound, cmo, volume / 127.0f); } } else if(flags & SNDF_SECTOR) { int num = int(Reader_ReadPackedUInt16(msgReader)); if(num >= map.sectorCount()) { LOG_NET_WARNING("Invalid sector number %i") << num; return; } mobj_t *mo = (mobj_t *) &map.sector(num).soundEmitter(); //S_StopSound(0, mo); S_LocalSoundAtVolume(sound, mo, volume / 127.0f); } else if(flags & SNDF_ORIGIN) { coord_t pos[3]; pos[VX] = Reader_ReadInt16(msgReader); pos[VY] = Reader_ReadInt16(msgReader); pos[VZ] = Reader_ReadInt16(msgReader); S_LocalSoundAtVolumeFrom(sound, NULL, pos, volume / 127.0f); } else if(flags & SNDF_PLAYER) { S_LocalSoundAtVolume(sound, ddPlayers[(flags & 0xf0) >> 4].shared.mo, volume / 127.0f); } else { // Play it from "somewhere". LOGDEV_NET_VERBOSE("Unspecified origin for sound %i") << sound; S_LocalSoundAtVolume(sound, NULL, volume / 127.0f); } } doomsday-stable-1.15.7/doomsday/client/src/client/cl_frame.cpp0000664000175000017500000001343212641367670023615 0ustar jaakkojaakko/** @file * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ /** * cl_frame.c: Frame Reception */ // HEADER FILES ------------------------------------------------------------ #include "de_base.h" #include "de_console.h" #include "client/cl_frame.h" #include "client/cl_mobj.h" #include "client/cl_player.h" #include "client/cl_sound.h" #include "client/cl_world.h" #include "network/net_main.h" #include "network/net_buf.h" #include "network/net_msg.h" // MACROS ------------------------------------------------------------------ #if 0 #define SET_HISTORY_SIZE 50 #define RESEND_HISTORY_SIZE 50 #endif // TYPES ------------------------------------------------------------------- // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- // PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- // EXTERNAL DATA DECLARATIONS ---------------------------------------------- extern int gotFrame; // PUBLIC DATA DEFINITIONS ------------------------------------------------- // Set to true when the PSV_FIRST_FRAME2 packet is received. // Until then, all PSV_FRAME2 packets are ignored (they must be // from the wrong map). dd_bool gotFirstFrame; //int predicted_tics; // PRIVATE DATA DEFINITIONS ------------------------------------------------ // gameTime of the current frame. static float frameGameTime = 0; #if 0 // Ordinal of the latest set received by the client. Used for detecting deltas // that arrive out of order. The ordinal is the logical equivalent of the set // identifier (which is 0...255). static uint latestSetOrdinal; static byte latestSet; // The ordinal base is added to the set number to convert it from a set // identifier to an ordinal. Every time the set numbers wrap around from 255 // to zero, the ordinal is incremented by 256. static uint setOrdinalBase; // The set history keeps track of received sets and is used to detect // duplicate frames. static short setHistory[SET_HISTORY_SIZE]; static int historyIdx; // The resend ID history keeps track of received resend deltas. Used // to detect duplicate resends. static byte resendHistory[RESEND_HISTORY_SIZE]; static int resendHistoryIdx; #endif // CODE -------------------------------------------------------------------- /** * Clear the history of received set numbers. */ void Cl_InitFrame(void) { Cl_ResetFrame(); #if 0 // -1 denotes an invalid entry. memset(setHistory, -1, sizeof(setHistory)); historyIdx = 0; // Clear the resend ID history. memset(resendHistory, 0, sizeof(resendHistory)); resendHistoryIdx = 0; // Reset ordinal counters. latestSet = 0; latestSetOrdinal = 0; setOrdinalBase = 256; #endif } /** * Called when the map changes. */ void Cl_ResetFrame() { gotFrame = false; // All frames received before the PSV_FIRST_FRAME2 are ignored. // They must be from the wrong map. gotFirstFrame = false; } float Cl_FrameGameTime() { return frameGameTime; } void Cl_Frame2Received(int packetType) { // The first thing in the frame is the gameTime. frameGameTime = Reader_ReadFloat(msgReader); // All frames that arrive before the first frame are ignored. // They are most likely from the wrong map. if(packetType == PSV_FIRST_FRAME2) { gotFirstFrame = true; } else if(!gotFirstFrame) { // Just ignore. If this was a legitimate frame, the server will // send it again when it notices no ack is coming. return; } // Read and process the message. while(!Reader_AtEnd(msgReader)) { byte const deltaType = Reader_ReadByte(msgReader); switch(deltaType) { case DT_CREATE_MOBJ: // The mobj will be created/shown. ClMobj_ReadDelta(); break; case DT_MOBJ: // The mobj will be hidden if it's not yet Created. ClMobj_ReadDelta(); break; case DT_NULL_MOBJ: // The mobj will be removed. ClMobj_ReadNullDelta(); break; case DT_PLAYER: ClPlayer_ReadDelta(); break; case DT_SECTOR: Cl_ReadSectorDelta(deltaType); break; //case DT_SIDE_R6: // Old format. case DT_SIDE: Cl_ReadSideDelta(deltaType); break; case DT_POLY: Cl_ReadPolyDelta(); break; case DT_SOUND: case DT_MOBJ_SOUND: case DT_SECTOR_SOUND: case DT_SIDE_SOUND: case DT_POLY_SOUND: Cl_ReadSoundDelta((deltatype_t) deltaType); break; default: LOG_NET_ERROR("Received unknown delta type %i (message size: %i bytes)") << deltaType << netBuffer.length; return; } } if(!gotFrame) { LOGDEV_NET_NOTE("First frame received"); } // We have now received a frame. gotFrame = true; } doomsday-stable-1.15.7/doomsday/client/src/games.cpp0000664000175000017500000002244112641367670021663 0ustar jaakkojaakko/** @file games.cpp Specialized collection for a set of logical Games. * * @authors Copyright © 2012-2013 Daniel Swanson * @authors Copyright © 2012-2013 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "games.h" #include "dd_main.h" #include #include "ui/progress.h" #include //#include #include "resource/manifest.h" #include #include #include #include #include #include namespace de { /// @todo Belongs in App DENG2_PIMPL(Games) { /// The actual collection. All games; /// Special "null-game" object for this collection. NullGame *nullGame; /** * Delegates game addition notifications to scripts. */ class GameAdditionScriptAudience : DENG2_OBSERVES(Games, Addition) { public: void gameAdded(Game &game) { ArrayValue args; args << DictionaryValue() << TextValue(game.id()); App::scriptSystem().nativeModule("App")["audienceForGameAddition"] .value().callElements(args); } }; GameAdditionScriptAudience scriptAudienceForGameAddition; Instance(Public *i) : Base(i), games(), nullGame(0) { /* * One-time creation and initialization of the special "null-game" * object (activated once created). */ nullGame = new NullGame; // Extend the native App module with a script audience for observing game addition. App::scriptSystem().nativeModule("App").addArray("audienceForGameAddition"); audienceForAddition += scriptAudienceForGameAddition; } ~Instance() { clear(); delete nullGame; } void clear() { DENG2_ASSERT(nullGame != 0); qDeleteAll(games); games.clear(); } DENG2_PIMPL_AUDIENCE(Addition) DENG2_PIMPL_AUDIENCE(Readiness) }; DENG2_AUDIENCE_METHOD(Games, Addition) DENG2_AUDIENCE_METHOD(Games, Readiness) Games::Games() : d(new Instance(this)) {} Game &Games::nullGame() const { return *d->nullGame; } int Games::numPlayable() const { int count = 0; foreach(Game *game, d->games) { if(game->allStartupFilesFound()) { count += 1; } } return count; } Game *Games::firstPlayable() const { foreach(Game *game, d->games) { if(game->allStartupFilesFound()) return game; } return NULL; } gameid_t Games::id(Game const &game) const { if(&game == d->nullGame) return 0; // Invalid id. int idx = d->games.indexOf(const_cast(&game)); if(idx < 0) { /// @throw NotFoundError The specified @a game is not a member of the collection. throw NotFoundError("Games::id", QString("Game %p is not a member of the collection").arg(de::dintptr(&game))); } return gameid_t(idx+1); } Game &Games::byId(gameid_t gameId) const { if(gameId <= 0 || gameId > d->games.count()) { /// @throw NotFoundError The specified @a gameId is out of range. throw NotFoundError("Games::byId", QString("There is no Game with id %i").arg(gameId)); } return *d->games[gameId-1]; } Game &Games::byIdentityKey(String identityKey) const { if(!identityKey.isEmpty()) { foreach(Game *game, d->games) { if(!game->identityKey().compareWithoutCase(identityKey)) return *game; } } /// @throw NotFoundError The specified @a identityKey string is not associated with a game in the collection. throw NotFoundError("Games::byIdentityKey", "No game exists with identity key '" + identityKey + "'"); } Game &Games::byIndex(int idx) const { if(idx < 0 || idx > d->games.count()) { /// @throw NotFoundError No game is associated with index @a idx. throw NotFoundError("Games::byIndex", QString("There is no Game at index %i").arg(idx)); } return *d->games[idx]; } void Games::clear() { d->clear(); } Games::All const &Games::all() const { return d->games; } int Games::collectAll(GameList &collected) { int numFoundSoFar = collected.count(); foreach(Game *game, d->games) { collected.push_back(GameListItem(game)); } return collected.count() - numFoundSoFar; } void Games::add(Game &game) { // Already a member of the collection? if(d->games.indexOf(&game) >= 0) return; d->games.push_back(&game); DENG2_FOR_AUDIENCE2(Addition, i) { i->gameAdded(game); } } void Games::locateStartupResources(Game &game) { Game *oldCurrentGame = &App_CurrentGame(); if(oldCurrentGame != &game) { /// @attention Kludge: Temporarily switch Game. App::app().setGame(game); DD_ExchangeGamePluginEntryPoints(game.pluginId()); // Re-init the filesystem subspace schemes using the search paths of this Game. App_FileSystem().resetAllSchemes(); } foreach(ResourceManifest *manifest, game.manifests()) { // We are only interested in startup resources at this time. if(manifest->fileFlags() & FF_STARTUP) { manifest->locateFile(); } } if(oldCurrentGame != &game) { // Kludge end - Restore the old Game. App::app().setGame(*oldCurrentGame); DD_ExchangeGamePluginEntryPoints(oldCurrentGame->pluginId()); // Re-init the filesystem subspace schemes using the search paths of this Game. App_FileSystem().resetAllSchemes(); } } static int locateAllResourcesWorker(void *context) { Games *games = (Games *) context; int n = 0; foreach(Game *game, games->all()) { LOG_RES_MSG("Locating " _E(b) "\"%s\"" _E(.) "...") << game->title(); games->locateStartupResources(*game); Con_SetProgress((n + 1) * 200 / games->count() - 1); LOG_RES_VERBOSE(_E(l) " Game: " _E(.)_E(>) "%s - %s") << game->title() << game->author(); LOG_RES_VERBOSE(_E(l) " IdentityKey: " _E(.)_E(>)) << game->identityKey(); Game::printFiles(*game, FF_STARTUP); LOG_RES_MSG(" " DENG2_CHAR_RIGHT_DOUBLEARROW " ") << game->statusAsText(); ++n; } BusyMode_WorkerEnd(); return 0; } void Games::locateAllResources() { BusyMode_RunNewTaskWithName(BUSYF_STARTUP | BUSYF_PROGRESS_BAR | (verbose? BUSYF_CONSOLE_OUTPUT : 0), locateAllResourcesWorker, (void *)this, "Locating game resources..."); DENG2_FOR_AUDIENCE2(Readiness, i) { i->gameReadinessUpdated(); } } void Games::forgetAllResources() { foreach(Game *game, d->games) { foreach(ResourceManifest *manifest, game->manifests()) { if(manifest->fileFlags() & FF_STARTUP) { manifest->forgetFile(); } } } } D_CMD(ListGames) { DENG2_UNUSED3(src, argc, argv); Games &games = App_Games(); if(!games.count()) { LOG_MSG("No game is currently registered."); return true; } LOG_MSG(_E(b) "Registered Games:"); LOG_VERBOSE("Key: %s'!'=Incomplete/Not playable %s'*'=Loaded") << _E(>) _E(D) << _E(B); LOG_MSG(_E(R) "\n"); Games::GameList found; games.collectAll(found); // Sort so we get a nice alphabetical list. qSort(found.begin(), found.end()); String list; int numCompleteGames = 0; DENG2_FOR_EACH_CONST(Games::GameList, i, found) { Game *game = i->game; bool isCurrent = (&App_CurrentGame() == game); if(!list.isEmpty()) list += "\n"; list += String(_E(0) _E(Ta) "%1%2 " _E(Tb) "%3 " _E(Tc) _E(2) "%4 " _E(i) "(%5)") .arg(isCurrent? _E(B) _E(b) : !game->allStartupFilesFound()? _E(D) : "") .arg(isCurrent? "*" : !game->allStartupFilesFound()? "!" : " ") .arg(game->identityKey()) .arg(game->title()) .arg(game->author()); if(game->allStartupFilesFound()) { numCompleteGames++; } } LOG_MSG("%s") << list; LOG_MSG(_E(R) "\n"); LOG_MSG("%i of %i games are playable") << numCompleteGames << games.count(); LOG_SCR_MSG("Use the " _E(b) "load" _E(.) " command to load a game, for example: \"load gamename\""); return true; } void Games::consoleRegister() //static { C_CMD("listgames", "", ListGames); Game::consoleRegister(); } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/world/0000775000175000017500000000000012641367670021207 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/world/plane.cpp0000664000175000017500000002510012641367670023010 0ustar jaakkojaakko/** @file plane.h World map plane. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/plane.h" #include #include "dd_loop.h" // frameTimePos #include "world/map.h" #include "world/thinkers.h" #include "world/worldsystem.h" /// ddMapSetup #include "Surface" #include "Sector" using namespace de; DENG2_PIMPL(Plane) { Surface surface; ThinkerT soundEmitter; dint indexInSector = -1; ///< Index in the owning sector. coord_t height = 0; ///< Current @em sharp height. coord_t targetHeight = 0; ///< Target @em sharp height. coord_t speed = 0; ///< Movement speed (map space units per tic). #ifdef __CLIENT__ coord_t oldHeight[2]; ///< @em sharp height change tracking buffer (for smoothing). coord_t heightSmoothed = 0; ///< @ref height (smoothed). coord_t heightSmoothedDelta = 0; ///< Delta between the current @em sharp height and the visual height. ClPlaneMover *mover = nullptr; ///< The current mover. #endif Instance(Public *i) : Base(i), surface(dynamic_cast(*i)) { #ifdef __CLIENT__ de::zap(oldHeight); #endif } ~Instance() { DENG2_FOR_PUBLIC_AUDIENCE2(Deletion, i) i->planeBeingDeleted(self); #ifdef __CLIENT__ // Stop movement tracking of this plane. map().trackedPlanes().remove(&self); #endif } inline Map &map() const { return self.map(); } void setHeight(coord_t newHeight) { height = targetHeight = newHeight; #ifdef __CLIENT__ heightSmoothed = newHeight; oldHeight[0] = oldHeight[1] = newHeight; #endif } void applySharpHeightChange(coord_t newHeight) { // No change? if(de::fequal(newHeight, height)) return; height = newHeight; if(!ddMapSetup) { // Update the sound emitter origin for the plane. self.updateSoundEmitterOrigin(); #ifdef __CLIENT__ // We need the decorations updated. /// @todo optimize: Translation on the world up axis would be a /// trivial operation to perform, which, would not require plotting /// decorations again. This frequent case should be designed for. surface.markForDecorationUpdate(); #endif } notifyHeightChanged(); #ifdef __CLIENT__ if(!ddMapSetup) { // Add ourself to tracked plane list (for movement interpolation). map().trackedPlanes().insert(&self); } #endif } #ifdef __CLIENT__ /// @todo Cache this result. Generator *tryFindGenerator() { Generator *found = nullptr; map().forAllGenerators([this, &found] (Generator &gen) { if(gen.plane == thisPublic) { found = &gen; return LoopAbort; // Found it. } return LoopContinue; }); return found; } #endif void notifyHeightChanged() { DENG2_FOR_PUBLIC_AUDIENCE2(HeightChange, i) i->planeHeightChanged(self); } #ifdef __CLIENT__ void notifySmoothedHeightChanged() { DENG2_FOR_PUBLIC_AUDIENCE2(HeightSmoothedChange, i) i->planeHeightSmoothedChanged(self); } #endif DENG2_PIMPL_AUDIENCE(Deletion) DENG2_PIMPL_AUDIENCE(HeightChange) #ifdef __CLIENT__ DENG2_PIMPL_AUDIENCE(HeightSmoothedChange) #endif }; DENG2_AUDIENCE_METHOD(Plane, Deletion) DENG2_AUDIENCE_METHOD(Plane, HeightChange) #ifdef __CLIENT__ DENG2_AUDIENCE_METHOD(Plane, HeightSmoothedChange) #endif Plane::Plane(Sector §or, Vector3f const &normal, coord_t height) : MapElement(DMU_PLANE, §or) , d(new Instance(this)) { d->setHeight(height); setNormal(normal); } Sector &Plane::sector() { return parent().as(); } Sector const &Plane::sector() const { return parent().as(); } dint Plane::indexInSector() const { return d->indexInSector; } void Plane::setIndexInSector(dint newIndex) { d->indexInSector = newIndex; } bool Plane::isSectorFloor() const { return this == §or().floor(); } bool Plane::isSectorCeiling() const { return this == §or().ceiling(); } Surface &Plane::surface() { return d->surface; } Surface const &Plane::surface() const { return d->surface; } void Plane::setNormal(Vector3f const &newNormal) { d->surface.setNormal(newNormal); // will normalize } SoundEmitter &Plane::soundEmitter() { return d->soundEmitter; } SoundEmitter const &Plane::soundEmitter() const { return d->soundEmitter; } void Plane::updateSoundEmitterOrigin() { LOG_AS("Plane::updateSoundEmitterOrigin"); d->soundEmitter->origin[0] = sector().soundEmitter().origin[0]; d->soundEmitter->origin[1] = sector().soundEmitter().origin[1]; d->soundEmitter->origin[2] = d->height; } coord_t Plane::height() const { return d->height; } coord_t Plane::targetHeight() const { return d->targetHeight; } coord_t Plane::speed() const { return d->speed; } #ifdef __CLIENT__ coord_t Plane::heightSmoothed() const { return d->heightSmoothed; } coord_t Plane::heightSmoothedDelta() const { return d->heightSmoothedDelta; } void Plane::lerpSmoothedHeight() { // Interpolate. d->heightSmoothedDelta = d->oldHeight[0] * (1 - frameTimePos) + d->height * frameTimePos - d->height; coord_t newHeightSmoothed = d->height + d->heightSmoothedDelta; if(!de::fequal(d->heightSmoothed, newHeightSmoothed)) { d->heightSmoothed = newHeightSmoothed; d->notifySmoothedHeightChanged(); d->surface.markForDecorationUpdate(); } } void Plane::resetSmoothedHeight() { // Reset interpolation. d->heightSmoothedDelta = 0; coord_t newHeightSmoothed = d->oldHeight[0] = d->oldHeight[1] = d->height; if(!de::fequal(d->heightSmoothed, newHeightSmoothed)) { d->heightSmoothed = newHeightSmoothed; d->notifySmoothedHeightChanged(); d->surface.markForDecorationUpdate(); } } void Plane::updateHeightTracking() { d->oldHeight[0] = d->oldHeight[1]; d->oldHeight[1] = d->height; if(!de::fequal(d->oldHeight[0], d->oldHeight[1])) { if(de::abs(d->oldHeight[0] - d->oldHeight[1]) >= MAX_SMOOTH_MOVE) { // Too fast: make an instantaneous jump. d->oldHeight[0] = d->oldHeight[1]; } d->surface.markForDecorationUpdate(); } } bool Plane::hasGenerator() const { return d->tryFindGenerator() != nullptr; } Generator &Plane::generator() const { if(Generator *gen = d->tryFindGenerator()) return *gen; /// @throw MissingGeneratorError No generator is attached. throw MissingGeneratorError("Plane::generator", "No generator is attached"); } void Plane::spawnParticleGen(ded_ptcgen_t const *def) { //if(!useParticles) return; if(!def) return; // Plane we spawn relative to may not be this one. dint relPlane = indexInSector(); if(def->flags & Generator::SpawnCeiling) relPlane = Sector::Ceiling; if(def->flags & Generator::SpawnFloor) relPlane = Sector::Floor; if(relPlane != indexInSector()) { sector().plane(relPlane).spawnParticleGen(def); return; } // Only planes in sectors with volume on the world X/Y axis can support generators. if(!sector().sideCount()) return; // Only one generator per plane. if(hasGenerator()) return; // Are we out of generators? Generator *gen = map().newGenerator(); if(!gen) return; gen->count = def->particles; // Size of source sector might determine count. if(def->flags & Generator::Density) { gen->spawnRateMultiplier = sector().roughArea() / (128 * 128); } else { gen->spawnRateMultiplier = 1; } // Initialize the particle generator. gen->configureFromDef(def); gen->plane = this; // Is there a need to pre-simulate? gen->presimulate(def->preSim); } void Plane::addMover(ClPlaneMover &mover) { // Forcibly remove the existing mover for this plane. if(d->mover) { LOG_MAP_XVERBOSE("Removing existing mover %p in sector #%i, plane %i") << &d->mover->thinker() << sector().indexInMap() << indexInSector(); map().thinkers().remove(d->mover->thinker()); DENG2_ASSERT(!d->mover); } d->mover = &mover; } void Plane::removeMover(ClPlaneMover &mover) { if(d->mover == &mover) { d->mover = nullptr; } } #endif // __CLIENT__ dint Plane::property(DmuArgs &args) const { switch(args.prop) { case DMU_EMITTER: args.setValue(DMT_PLANE_EMITTER, d->soundEmitter, 0); break; case DMU_SECTOR: { Sector const *secPtr = §or(); args.setValue(DMT_PLANE_SECTOR, &secPtr, 0); break; } case DMU_HEIGHT: args.setValue(DMT_PLANE_HEIGHT, &d->height, 0); break; case DMU_TARGET_HEIGHT: args.setValue(DMT_PLANE_TARGET, &d->targetHeight, 0); break; case DMU_SPEED: args.setValue(DMT_PLANE_SPEED, &d->speed, 0); break; default: return MapElement::property(args); } return false; // Continue iteration. } dint Plane::setProperty(DmuArgs const &args) { switch(args.prop) { case DMU_HEIGHT: { coord_t newHeight = d->height; args.value(DMT_PLANE_HEIGHT, &newHeight, 0); d->applySharpHeightChange(newHeight); break; } case DMU_TARGET_HEIGHT: args.value(DMT_PLANE_TARGET, &d->targetHeight, 0); break; case DMU_SPEED: args.value(DMT_PLANE_SPEED, &d->speed, 0); break; default: return MapElement::setProperty(args); } return false; // Continue iteration. } doomsday-stable-1.15.7/doomsday/client/src/world/mapelement.cpp0000664000175000017500000001053312641367670024044 0ustar jaakkojaakko/** @file mapelement.cpp Base class for all map elements. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "world/mapelement.h" #include "world/map.h" using namespace de; DENG2_PIMPL_NOREF(MapElement) { int type; MapElement *parent; Map *map; int indexInMap; int indexInArchive; Instance(int type, MapElement *parent) : type(type) , parent(parent) , map(0) , indexInMap(NoIndex) , indexInArchive(NoIndex) {} }; MapElement::MapElement(int type, MapElement *parent) : d(new Instance(type, parent)) {} MapElement::~MapElement() {} int MapElement::type() const { return d->type; } bool MapElement::hasParent() const { return d->parent != 0; } MapElement &MapElement::parent() { return const_cast(const_cast(this)->parent()); } MapElement const &MapElement::parent() const { if(d->parent) { return *d->parent; } /// @throw MissingParentError Attempted with no parent element is attributed. throw MissingParentError("MapElement::parent", "No parent map element is attributed"); } void MapElement::setParent(MapElement *newParent) { if(d->parent != newParent) { d->parent = newParent; return; } /// @throw InvalidParentError Attempted to attribute *this* element as parent of itself. throw InvalidParentError("MapElement::setParent", "Cannot attribute 'this' map element as a parent of itself"); } bool MapElement::hasMap() const { // If a parent is configured this property is delegated to the parent. if(d->parent) { return d->parent->hasMap(); } return d->map != 0; } Map &MapElement::map() const { // If a parent is configured this property is delegated to the parent. if(d->parent) { return d->parent->map(); } if(d->map) { return *d->map; } /// @throw MissingMapError Attempted with no map attributed. throw MissingMapError("MapElement::map", "No map is attributed"); } void MapElement::setMap(Map *newMap) { // If a parent is configured this property is delegated to the parent. if(!d->parent) { d->map = newMap; return; } /// @throw WritePropertyError Attempted to change a delegated property. throw WritePropertyError("MapElement::setMap", "The 'map' property has been delegated"); } int MapElement::indexInMap() const { return d->indexInMap; } void MapElement::setIndexInMap(int newIndex) { d->indexInMap = newIndex; } int MapElement::indexInArchive() const { return d->indexInArchive; } void MapElement::setIndexInArchive(int newIndex) { d->indexInArchive = newIndex; } int MapElement::property(DmuArgs &args) const { switch(args.prop) { case DMU_ARCHIVE_INDEX: args.setValue(DMT_ARCHIVE_INDEX, &d->indexInArchive, 0); break; default: /// @throw UnknownPropertyError The requested property is not readable. throw UnknownPropertyError(QString("%1::property").arg(DMU_Str(d->type)), QString("'%1' is unknown/not readable").arg(DMU_Str(args.prop))); } return false; // Continue iteration. } int MapElement::setProperty(DmuArgs const &args) { /// @throw WritePropertyError The requested property is not writable. throw WritePropertyError(QString("%1::setProperty").arg(DMU_Str(d->type)), QString("'%1' is unknown/not writable").arg(DMU_Str(args.prop))); } doomsday-stable-1.15.7/doomsday/client/src/world/blockmap.cpp0000664000175000017500000005201212641367670023503 0ustar jaakkojaakko/** @file blockmap.cpp World map element blockmap. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 1993-1996 by id Software, Inc. * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/blockmap.h" #include #include #include #include #include "de_base.h" #include "de_console.h" #include "de_graphics.h" // For debug visual. #include "de_render.h" // For debug visual. namespace de { struct RingNode { void *elem; RingNode *prev; RingNode *next; }; struct CellData { RingNode *ringNodes; int elemCount; ///< Total number of linked elements. bool unlink(void *elem) { if(RingNode *node = findNode(elem)) { clearElement(*node); return true; } return false; } void unlinkAll() { for(RingNode *node = ringNodes; node; node = node->next) { clearElement(*node); } } bool link(void *elem) { addElement(newNode(), elem); return true; } private: RingNode &newNode() { RingNode *node = 0; if(!ringNodes) { // Create a new root node. node = (RingNode *) Z_Malloc(sizeof(*node), PU_MAP, 0); node->next = 0; node->prev = 0; node->elem = 0; ringNodes = node; return *node; } // Is there an available node in the ring we can reuse? for(node = ringNodes; node->next && node->elem; node = node->next) {} if(!node->elem) { // This will do nicely. return *node; } // Add a new node to the ring. node->next = (RingNode *) Z_Malloc(sizeof(*node), PU_MAP, 0); node->next->next = 0; node->next->prev = node; node->next->elem = 0; return *node->next; } RingNode *findNode(void *elem) { if(!elem) return 0; for(RingNode *found = ringNodes; found; found = found->next) { if(found->elem == elem) return found; } return 0; } void clearElement(RingNode &node) { if(!node.elem) return; node.elem = 0; elemCount--; } void addElement(RingNode &node, void *elem) { DENG2_ASSERT(node.elem == 0); node.elem = elem; elemCount++; } }; DENG2_PIMPL(Blockmap) { /** * Quadtree node. */ struct Node { /// Logical child quadrant identifiers. enum Quadrant { TopLeft, TopRight, BottomLeft, BottomRight }; Cell cell; ///< Cell coordinates for this node. uint size; ///< Size of the cell at this node (width=height). union { Node *children[4]; ///< One per quadrant (if any, not owned). CellData *leafData; ///< Data associated with the leaf cell. }; /** * Construct a new node. * * @param cell Cell coordinates for the node. * @param size Size of the cell. */ Node(Cell const &cell, uint size) : cell(cell), size(size) { zap(children); } ~Node() { if(isLeaf()) { Z_Free(leafData); } } /** * Returns @c true iff the cell is a leaf (i.e., equal to a unit in the * gridmap coordinate space). */ inline bool isLeaf() const { return size == 1; } /** * In which child quadrant is the given @a point? */ Quadrant quadrant(Cell const &point) const { uint const subSize = size >> 1; if(point.x < cell.x + subSize) { return (point.y < cell.y + subSize)? TopLeft : BottomLeft; } else { return (point.y < cell.y + subSize)? TopRight : BottomRight; } } }; typedef QList Nodes; AABoxd bounds; ///< Map space units. uint cellSize; ///< Map space units. Cell dimensions; ///< Dimensions of the indexed space, in cells. Nodes nodes; ///< Quadtree nodes. The first being the root. Instance(Public *i, AABoxd const &bounds, uint cellSize) : Base(i), bounds(bounds), cellSize(cellSize), dimensions(Vector2ui(de::ceil((bounds.maxX - bounds.minX) / cellSize), de::ceil((bounds.maxY - bounds.minY) / cellSize))) { // Quadtree must subdivide the space equally into 1x1 unit cells. newNode(Cell(0, 0), ceilPow2(de::max(dimensions.x, dimensions.y))); } inline int toCellIndex(uint cellX, uint cellY) { return int(cellY * dimensions.x + cellX); } /** * Given map space X coordinate @a x, return the corresponding cell coordinate. * If @a x is outside the blockmap it will be clamped to the nearest edge on * the X axis. * * @param x Map space X coordinate to be translated. * @param didClip Set to @c true iff clamping was necessary. * * @return Translated blockmap cell X coordinate. */ uint toCellX(coord_t x, bool &didClip) { didClip = false; if(x < bounds.minX) { x = bounds.minX; didClip = true; } else if(x >= bounds.maxX) { x = bounds.maxX - 1; didClip = true; } return uint((x - bounds.minX) / cellSize); } /** * Given map space Y coordinate @a y, return the corresponding cell coordinate. * If @a y is outside the blockmap it will be clamped to the nearest edge on * the Y axis. * * @param y Map space Y coordinate to be translated. * @param didClip Set to @c true iff clamping was necessary. * * @return Translated blockmap cell Y coordinate. */ uint toCellY(coord_t y, bool &didClip) { didClip = false; if(y < bounds.minY) { y = bounds.minY; didClip = true; } else if(y >= bounds.maxY) { y = bounds.maxY - 1; didClip = true; } return uint((y - bounds.minY) / cellSize); } /// @return @c true, if clipped; otherwise @c false. bool clipCell(Cell &cell) const { bool didClip = false; if(cell.x > dimensions.x) { cell.x = dimensions.x; didClip = true; } if(cell.y > dimensions.y) { cell.y = dimensions.y; didClip = true; } return didClip; } /** * Clip the cell coordinates in @a block vs the dimensions of this gridmap * so that they are inside the boundary this defines. * * @param block Block coordinates to be clipped. * * @return @c true iff the block coordinates were changed. */ bool clipBlock(CellBlock &block) { bool const didClipMin = clipCell(block.min); bool const didClipMax = clipCell(block.max); return didClipMin | didClipMax; } Node *newNode(Cell const &at, uint size) { nodes.append(Node(at, size)); return &nodes.last(); } Node *findLeaf(Node *node, Cell const &at, bool canSubdivide) { if(node->isLeaf()) return node; // Into which quadrant do we need to descend? Node::Quadrant q = node->quadrant(at); // Has this quadrant been initialized yet? Node **childAdr = &node->children[q]; if(!*childAdr) { if(!canSubdivide) return 0; // Subdivide the space. uint const subSize = node->size >> 1; switch(q) { case Node::TopLeft: *childAdr = newNode(node->cell, subSize); break; case Node::TopRight: *childAdr = newNode(Cell(node->cell.x + subSize, node->cell.y), subSize); break; case Node::BottomLeft: *childAdr = newNode(Cell(node->cell.x, node->cell.y + subSize), subSize); break; case Node::BottomRight: *childAdr = newNode(Cell(node->cell.x + subSize, node->cell.y + subSize), subSize); break; } } return findLeaf(*childAdr, at, canSubdivide); } inline Node *findLeaf(Cell const &at, bool canCreate = false) { return findLeaf(&nodes.first(), at, canCreate); } /** * Retrieve the user data associated with the identified cell. * * @param cell Cell coordinates to retrieve user data for. * @param canCreate @c true= allocate new data if not already present * for the referenced cell. * * @return User data for the identified cell else @c 0if an invalid * reference or no there is no data present (and not allocating). */ CellData *cellData(Cell const &cell, bool canCreate = false) { // Outside our boundary? if(cell.x >= dimensions.x || cell.y >= dimensions.y) { return 0; } // Try to locate this leaf (may fail if not present and we are // not allocating user data (there will be no corresponding cell)). if(Node *node = findLeaf(cell, canCreate)) { // Exisiting user data for this cell? if(!node->leafData) { // Can we allocate new user data? if(canCreate) { node->leafData = (CellData *)Z_Calloc(sizeof(CellData), PU_MAPSTATIC, 0); } } return node->leafData; } return 0; } }; Blockmap::Blockmap(AABoxd const &bounds, uint cellSize) : d(new Instance(this, bounds, cellSize)) {} Blockmap::~Blockmap() {} Vector2d Blockmap::origin() const { return Vector2d(d->bounds.min); } AABoxd const &Blockmap::bounds() const { return d->bounds; } BlockmapCell const &Blockmap::dimensions() const { return d->dimensions; } uint Blockmap::cellSize() const { return d->cellSize; } int Blockmap::toCellIndex(uint cellX, uint cellY) const { return d->toCellIndex(cellX, cellY); } BlockmapCell Blockmap::toCell(Vector2d const &point, bool *retDidClip) const { bool didClipX, didClipY; Cell cell(d->toCellX(point.x, didClipX), d->toCellY(point.y, didClipY)); if(retDidClip) *retDidClip = didClipX | didClipY; return cell; } BlockmapCellBlock Blockmap::toCellBlock(AABoxd const &box, bool *retDidClip) const { bool didClipMin, didClipMax; CellBlock block(toCell(box.min, &didClipMin), toCell(box.max, &didClipMax)); block.max += Vector2ui(1, 1); // CellBlock is inclusive-exclusive. if(retDidClip) *retDidClip = didClipMin | didClipMax; return block; } bool Blockmap::link(Cell const &cell, void *elem) { if(!elem) return false; // Huh? if(auto *cellData = d->cellData(cell, true /*can create*/)) { return cellData->link(elem); } return false; // Outside the blockmap? } bool Blockmap::link(AABoxd const ®ion, void *elem) { if(!elem) return false; // Huh? bool didLink = false; CellBlock cellBlock = toCellBlock(region); d->clipBlock(cellBlock); Cell cell; for(cell.y = cellBlock.min.y; cell.y < cellBlock.max.y; ++cell.y) for(cell.x = cellBlock.min.x; cell.x < cellBlock.max.x; ++cell.x) { if(auto *cellData = d->cellData(cell, true)) { if(cellData->link(elem)) { didLink = true; } } } return didLink; } bool Blockmap::unlink(Cell const &cell, void *elem) { if(!elem) return false; // Huh? if(auto *cellData = d->cellData(cell)) { return cellData->unlink(elem); } return false; } bool Blockmap::unlink(AABoxd const ®ion, void *elem) { if(!elem) return false; // Huh? bool didUnlink = false; CellBlock cellBlock = toCellBlock(region); d->clipBlock(cellBlock); Cell cell; for(cell.y = cellBlock.min.y; cell.y < cellBlock.max.y; ++cell.y) for(cell.x = cellBlock.min.x; cell.x < cellBlock.max.x; ++cell.x) { if(auto *cellData = d->cellData(cell)) { if(cellData->unlink(elem)) { didUnlink = true; } } } return didUnlink; } void Blockmap::unlinkAll() { for(Instance::Node const &node : d->nodes) { // Only leafs with user data. if(!node.isLeaf()) continue; if(auto *cellData = node.leafData) { cellData->unlinkAll(); } } } int Blockmap::cellElementCount(Cell const &cell) const { if(auto *cellData = d->cellData(cell)) { return cellData->elemCount; } return 0; } LoopResult Blockmap::forAllInCell(Cell const &cell, std::function func) const { if(auto *cellData = d->cellData(cell)) { RingNode *node = cellData->ringNodes; while(node) { RingNode *next = node->next; if(node->elem) { if(auto result = func(node->elem)) return result; } node = next; } } return LoopContinue; } LoopResult Blockmap::forAllInBox(AABoxd const &box, std::function func) const { CellBlock cellBlock = toCellBlock(box); d->clipBlock(cellBlock); Cell cell; for(cell.y = cellBlock.min.y; cell.y < cellBlock.max.y; ++cell.y) for(cell.x = cellBlock.min.x; cell.x < cellBlock.max.x; ++cell.x) { if(auto result = forAllInCell(cell, func)) return result; } return LoopContinue; } LoopResult Blockmap::forAllInPath(Vector2d const &from_, Vector2d const &to_, std::function func) const { // We may need to clip and/or adjust these points. Vector2d from = from_; Vector2d to = to_; // Abort if the trace originates from outside the blockmap. if(!(from.x >= d->bounds.minX && from.x <= d->bounds.maxX && from.y >= d->bounds.minY && from.y <= d->bounds.maxY)) { return LoopContinue; } // Check the easy case of a trace line completely outside the blockmap. if((from.x < d->bounds.minX && to.x < d->bounds.minX) || (from.x > d->bounds.maxX && to.x > d->bounds.maxX) || (from.y < d->bounds.minY && to.y < d->bounds.minY) || (from.y > d->bounds.maxY && to.y > d->bounds.maxY)) { return LoopContinue; } /* * Trace lines should not be perfectly parallel to a blockmap axis. This is * because lines in the line blockmap are only linked in the blocks on one * side of the axis parallel line. Note that the same logic is applied to all * of the blockmaps and not just the line blockmap. */ coord_t const epsilon = FIX2FLT(FRACUNIT); coord_t const offset = FIX2FLT(FRACUNIT); Vector2d const delta = (to - origin()) / d->cellSize; if(de::fequal(delta.x, 0, epsilon)) to.x += offset; if(de::fequal(delta.y, 0, epsilon)) to.y += offset; /* * It is possible that one or both points are outside the blockmap. Clip the * trace so that is wholly within the AABB of the blockmap (note we would have * already abandoned if the origin of the trace is outside). */ if(!(to.x >= d->bounds.minX && to.x <= d->bounds.maxX && to.y >= d->bounds.minY && to.y <= d->bounds.maxY)) { // 'to' is outside the blockmap. vec2d_t fromV1, toV1, bmapBounds[4], point; coord_t ab; V2d_Set(fromV1, from.x, from.y); V2d_Set(toV1, to.x, to.y); V2d_Set(bmapBounds[0], d->bounds.minX, d->bounds.minY); V2d_Set(bmapBounds[1], d->bounds.minX, d->bounds.maxY); V2d_Set(bmapBounds[2], d->bounds.maxX, d->bounds.maxY); V2d_Set(bmapBounds[3], d->bounds.maxX, d->bounds.minY); ab = V2d_Intercept(fromV1, toV1, bmapBounds[0], bmapBounds[1], point); if(ab >= 0 && ab <= 1) V2d_Copy(toV1, point); ab = V2d_Intercept(fromV1, toV1, bmapBounds[1], bmapBounds[2], point); if(ab >= 0 && ab <= 1) V2d_Copy(toV1, point); ab = V2d_Intercept(fromV1, toV1, bmapBounds[2], bmapBounds[3], point); if(ab >= 0 && ab <= 1) V2d_Copy(toV1, point); ab = V2d_Intercept(fromV1, toV1, bmapBounds[3], bmapBounds[0], point); if(ab >= 0 && ab <= 1) V2d_Copy(toV1, point); from = Vector2d(fromV1); to = Vector2d(toV1); } BlockmapCell const originCell = toCell(from); BlockmapCell const destCell = toCell(to); // Determine the starting point in blockmap space (preserving the fractional part). Vector2d intercept = (from - origin()) / d->cellSize; // Determine the step deltas. Vector2i cellStep; Vector2d interceptStep; Vector2d frac; if(destCell.x == originCell.x) { cellStep.x = 0; interceptStep.y = 256; frac.y = 1; } else if(destCell.x > originCell.x) { cellStep.x = 1; interceptStep.y = (to.y - from.y) / de::abs(to.x - from.x); frac.y = 1 - (intercept.x - int( intercept.x )); } else // toCell.x < fromCell.x { cellStep.x = -1; interceptStep.y = (to.y - from.y) / de::abs(to.x - from.x); frac.y = intercept.x - int( intercept.x ); } if(destCell.y == originCell.y) { cellStep.y = 0; interceptStep.x = 256; frac.x = 1; } else if(destCell.y > originCell.y) { cellStep.y = 1; interceptStep.x = (to.x - from.x) / de::abs(to.y - from.y); frac.x = 1 - (intercept.y - int( intercept.y )); } else // toCell.y < fromCell.y { cellStep.y = -1; interceptStep.x = (to.x - from.x) / de::abs(to.y - from.y); frac.x = intercept.y - int( intercept.y ); } intercept += frac * interceptStep; // Walk the cells of the blockmap. BlockmapCell cell = originCell; for(int pass = 0; pass < 64; ++pass) // Prevent a round off error leading us into // an infinite loop... { if(auto result = forAllInCell(cell, func)) return result; if(cell == destCell) break; if(cell.y == uint( intercept.y )) { cell.x += cellStep.x; intercept.y += interceptStep.y; } else if(cell.x == uint( intercept.x )) { cell.y += cellStep.y; intercept.x += interceptStep.x; } } return LoopContinue; } // Debug visual ---------------------------------------------------------------- #ifdef __CLIENT__ void Blockmap::drawDebugVisual() const { #define UNIT_SIZE 1 // We'll be changing the color, so query the current and restore later. GLfloat oldColor[4]; glGetFloatv(GL_CURRENT_COLOR, oldColor); /* * Draw the Quadtree. */ glColor4f(1.f, 1.f, 1.f, 1.f / d->nodes.first().size); foreach(Instance::Node const &node, d->nodes) { // Only leafs with user data. if(!node.isLeaf()) continue; if(!node.leafData) continue; Vector2f const topLeft = node.cell * UNIT_SIZE; Vector2f const bottomRight = topLeft + Vector2f(UNIT_SIZE, UNIT_SIZE); glBegin(GL_LINE_LOOP); glVertex2f(topLeft.x, topLeft.y); glVertex2f(bottomRight.x, topLeft.y); glVertex2f(bottomRight.x, bottomRight.y); glVertex2f(topLeft.x, bottomRight.y); glEnd(); } /* * Draw the bounds. */ Vector2f start; Vector2f end = start + d->dimensions * UNIT_SIZE; glColor3f(1, .5f, .5f); glBegin(GL_LINES); glVertex2f(start.x, start.y); glVertex2f( end.x, start.y); glVertex2f( end.x, start.y); glVertex2f( end.x, end.y); glVertex2f( end.x, end.y); glVertex2f(start.x, end.y); glVertex2f(start.x, end.y); glVertex2f(start.x, start.y); glEnd(); // Restore GL state. glColor4fv(oldColor); #undef UNIT_SIZE } #endif // __CLIENT__ } // namespace de doomsday-stable-1.15.7/doomsday/client/src/world/vertex.cpp0000664000175000017500000000572712641367670023243 0ustar jaakkojaakko/** @file vertex.cpp World map vertex. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "world/vertex.h" #include "Line" #include "world/lineowner.h" /// @todo remove me #include "Sector" #include using namespace de; DENG2_PIMPL_NOREF(Vertex) { Vector2d origin; ///< In map space. Instance(Vector2d const &origin) : origin(origin) {} }; Vertex::Vertex(Mesh &mesh, Vector2d const &origin) : MapElement(DMU_VERTEX) , MeshElement(mesh) , _lineOwners(0) , _numLineOwners(0) , _onesOwnerCount(0) , _twosOwnerCount(0) , d(new Instance(origin)) {} Vector2d const &Vertex::origin() const { return d->origin; } void Vertex::setOrigin(Vector2d const &newOrigin) { if(d->origin != newOrigin) { d->origin = newOrigin; DENG2_FOR_AUDIENCE(OriginChange, i) { i->vertexOriginChanged(*this); } } } int Vertex::property(DmuArgs &args) const { switch(args.prop) { case DMU_X: args.setValue(DMT_VERTEX_ORIGIN, &d->origin.x, 0); break; case DMU_Y: args.setValue(DMT_VERTEX_ORIGIN, &d->origin.y, 0); break; case DMU_XY: args.setValue(DMT_VERTEX_ORIGIN, &d->origin.x, 0); args.setValue(DMT_VERTEX_ORIGIN, &d->origin.y, 1); break; default: return MapElement::property(args); } return false; // Continue iteration. } // --------------------------------------------------------------------------- uint Vertex::lineOwnerCount() const { return _numLineOwners; } void Vertex::countLineOwners() { _onesOwnerCount = _twosOwnerCount = 0; if(LineOwner const *firstOwn = firstLineOwner()) { LineOwner const *own = firstOwn; do { if(!own->line().hasFrontSector() || !own->line().hasBackSector()) { _onesOwnerCount += 1; } else { _twosOwnerCount += 1; } } while((own = &own->next()) != firstOwn); } } LineOwner *Vertex::firstLineOwner() const { return _lineOwners; } doomsday-stable-1.15.7/doomsday/client/src/world/lineblockmap.cpp0000664000175000017500000000512712641367670024360 0ustar jaakkojaakko/** @file lineblockmap.cpp Specialized world map line blockmap. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/lineblockmap.h" using namespace de; LineBlockmap::LineBlockmap(AABoxd const &bounds, uint cellSize) : Blockmap(bounds, cellSize) {} void LineBlockmap::link(Line &line) { // Polyobj lines are excluded (presently...). if(line.definesPolyobj()) return; // Determine the block of cells we'll be working within. BlockmapCellBlock const cellBlock = toCellBlock(line.aaBox()); BlockmapCell cell; for(cell.y = cellBlock.min.y; cell.y < cellBlock.max.y; ++cell.y) for(cell.x = cellBlock.min.x; cell.x < cellBlock.max.x; ++cell.x) { if(line.slopeType() == ST_VERTICAL || line.slopeType() == ST_HORIZONTAL) { Blockmap::link(cell, &line); continue; } Vector2d const point(origin() + cellDimensions() * cell); // Choose a cell diagonal to test. Vector2d from, to; if(line.slopeType() == ST_POSITIVE) { // Line slope / vs \ cell diagonal. from = Vector2d(point.x, point.y + cellDimensions().y); to = Vector2d(point.x + cellDimensions().x, point.y); } else { // Line slope \ vs / cell diagonal. from = Vector2d(point.x + cellDimensions().x, point.y + cellDimensions().y); to = Vector2d(point.x, point.y); } // Would Line intersect this? if((line.pointOnSide(from) < 0) != (line.pointOnSide(to) < 0)) { Blockmap::link(cell, &line); } } } void LineBlockmap::link(QList const &lines) { foreach(Line *line, lines) { link(*line); } } doomsday-stable-1.15.7/doomsday/client/src/world/api_map.cpp0000664000175000017500000015400312641367670023324 0ustar jaakkojaakko/** @file api_map.cpp Doomsday Map Update API. * * @todo Throw a game-terminating exception if an illegal value is given * to a public API function. * * @authors Copyright © 2006-2014 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #define DENG_NO_API_MACROS_MAP #include #include #include #include "de_base.h" #include "de_play.h" #include "de_audio.h" #include "api_map.h" #include "network/net_main.h" #include "Face" #include "world/blockmap.h" #include "world/dmuargs.h" #include "world/entitydatabase.h" #include "world/maputil.h" #include "world/worldsystem.h" #include "BspLeaf" #include "ConvexSubspace" #include "Interceptor" #ifdef __CLIENT__ # include "render/lightgrid.h" #endif using namespace de; // Converting a public void* pointer to an internal de::MapElement. #define IN_ELEM(p) reinterpret_cast(p) #define IN_ELEM_CONST(p) reinterpret_cast(p) /** * Additional data for all dummy elements. */ struct DummyData { void *extraData; /// Pointer to user data. DummyData() : extraData(0) {} virtual ~DummyData() {} // polymorphic }; class DummySector : public Sector, public DummyData {}; class DummyLine : public Line, public DummyData { public: DummyLine(Vertex &v1, Vertex &v2) : Line(v1, v2) {} }; typedef QSet Dummies; static Dummies dummies; static Mesh dummyMesh; #undef DMU_Str char const *DMU_Str(uint prop) { static char propStr[40]; struct prop_s { uint prop; char const *str; } props[] = { { DMU_NONE, "(invalid)" }, { DMU_VERTEX, "DMU_VERTEX" }, { DMU_SEGMENT, "DMU_SEGMENT" }, { DMU_LINE, "DMU_LINE" }, { DMU_SIDE, "DMU_SIDE" }, { DMU_SUBSPACE, "DMU_SUBSPACE" }, { DMU_SECTOR, "DMU_SECTOR" }, { DMU_PLANE, "DMU_PLANE" }, { DMU_SURFACE, "DMU_SURFACE" }, { DMU_MATERIAL, "DMU_MATERIAL" }, { DMU_SKY, "DMU_SKY" }, { DMU_LINE_BY_TAG, "DMU_LINE_BY_TAG" }, { DMU_SECTOR_BY_TAG, "DMU_SECTOR_BY_TAG" }, { DMU_LINE_BY_ACT_TAG, "DMU_LINE_BY_ACT_TAG" }, { DMU_SECTOR_BY_ACT_TAG, "DMU_SECTOR_BY_ACT_TAG" }, { DMU_ARCHIVE_INDEX, "DMU_ARCHIVE_INDEX" }, { DMU_X, "DMU_X" }, { DMU_Y, "DMU_Y" }, { DMU_XY, "DMU_XY" }, { DMU_TANGENT_X, "DMU_TANGENT_X" }, { DMU_TANGENT_Y, "DMU_TANGENT_Y" }, { DMU_TANGENT_Z, "DMU_TANGENT_Z" }, { DMU_TANGENT_XYZ, "DMU_TANGENT_XYZ" }, { DMU_BITANGENT_X, "DMU_BITANGENT_X" }, { DMU_BITANGENT_Y, "DMU_BITANGENT_Y" }, { DMU_BITANGENT_Z, "DMU_BITANGENT_Z" }, { DMU_BITANGENT_XYZ, "DMU_BITANGENT_XYZ" }, { DMU_NORMAL_X, "DMU_NORMAL_X" }, { DMU_NORMAL_Y, "DMU_NORMAL_Y" }, { DMU_NORMAL_Z, "DMU_NORMAL_Z" }, { DMU_NORMAL_XYZ, "DMU_NORMAL_XYZ" }, { DMU_VERTEX0, "DMU_VERTEX0" }, { DMU_VERTEX1, "DMU_VERTEX1" }, { DMU_FRONT, "DMU_FRONT" }, { DMU_BACK, "DMU_BACK" }, { DMU_FLAGS, "DMU_FLAGS" }, { DMU_DX, "DMU_DX" }, { DMU_DY, "DMU_DY" }, { DMU_DXY, "DMU_DXY" }, { DMU_LENGTH, "DMU_LENGTH" }, { DMU_SLOPETYPE, "DMU_SLOPETYPE" }, { DMU_ANGLE, "DMU_ANGLE" }, { DMU_OFFSET, "DMU_OFFSET" }, { DMU_OFFSET_X, "DMU_OFFSET_X" }, { DMU_OFFSET_Y, "DMU_OFFSET_Y" }, { DMU_OFFSET_XY, "DMU_OFFSET_XY" }, { DMU_BLENDMODE, "DMU_BLENDMODE" }, { DMU_VALID_COUNT, "DMU_VALID_COUNT" }, { DMU_COLOR, "DMU_COLOR" }, { DMU_COLOR_RED, "DMU_COLOR_RED" }, { DMU_COLOR_GREEN, "DMU_COLOR_GREEN" }, { DMU_COLOR_BLUE, "DMU_COLOR_BLUE" }, { DMU_ALPHA, "DMU_ALPHA" }, { DMU_LIGHT_LEVEL, "DMU_LIGHT_LEVEL" }, { DMT_MOBJS, "DMT_MOBJS" }, { DMU_BOUNDING_BOX, "DMU_BOUNDING_BOX" }, { DMU_EMITTER, "DMU_EMITTER" }, { DMU_WIDTH, "DMU_WIDTH" }, { DMU_HEIGHT, "DMU_HEIGHT" }, { DMU_TARGET_HEIGHT, "DMU_TARGET_HEIGHT" }, { DMU_SPEED, "DMU_SPEED" }, { DMU_FLOOR_PLANE, "DMU_FLOOR_PLANE" }, { DMU_CEILING_PLANE, "DMU_CEILING_PLANE" }, { 0, NULL } }; for(uint i = 0; props[i].str; ++i) { if(props[i].prop == prop) return props[i].str; } dd_snprintf(propStr, 40, "(unnamed %i)", prop); return propStr; } #undef DMU_GetType int DMU_GetType(void const *ptr) { if(!ptr) return DMU_NONE; MapElement const *elem = IN_ELEM_CONST(ptr); // Make sure it's valid. switch(elem->type()) { case DMU_VERTEX: case DMU_SEGMENT: case DMU_LINE: case DMU_SIDE: case DMU_SECTOR: case DMU_SUBSPACE: case DMU_PLANE: case DMU_SURFACE: case DMU_MATERIAL: case DMU_SKY: return elem->type(); default: break; // Unknown. } return DMU_NONE; } void Map::initDummies() // static { // TODO: free existing/old dummies here? dummies.clear(); dummyMesh.clear(); } /** * Determines the type of a dummy object. For extra safety (in a debug build) * it would be possible to look through the dummy arrays and make sure the * pointer refers to a real dummy. */ static int dummyType(void const *dummy) { MapElement const *elem = IN_ELEM_CONST(dummy); if(!dynamic_cast(elem)) { // Not a dummy. return DMU_NONE; } DENG2_ASSERT(dummies.contains(const_cast(elem))); return elem->type(); } #undef P_AllocDummy void *P_AllocDummy(int type, void *extraData) { switch(type) { case DMU_LINE: { // Time to allocate the dummy vertex? if(dummyMesh.vertexsIsEmpty()) dummyMesh.newVertex(); Vertex &dummyVertex = *dummyMesh.vertexs().first(); DummyLine *dl = new DummyLine(dummyVertex, dummyVertex); dummies.insert(dl); dl->extraData = extraData; return dl; } case DMU_SECTOR: { DummySector *ds = new DummySector; dummies.insert(ds); ds->extraData = extraData; return ds; } default: { /// @throw Throw exception. QByteArray msg = String("P_AllocDummy: Dummies of type %1 not supported.").arg(DMU_Str(type)).toUtf8(); App_FatalError(msg.constData()); break; } } return 0; // Unreachable. } #undef P_IsDummy dd_bool P_IsDummy(void const *dummy) { return dummyType(dummy) != DMU_NONE; } #undef P_FreeDummy void P_FreeDummy(void *dummy) { MapElement *elem = IN_ELEM(dummy); int type = dummyType(dummy); if(type == DMU_NONE) { /// @todo Throw exception. App_FatalError("P_FreeDummy: Dummy is of unknown type."); } DENG2_ASSERT(dummies.contains(elem)); dummies.remove(elem); delete elem; } #undef P_DummyExtraData void *P_DummyExtraData(void *dummy) { if(P_IsDummy(dummy)) { MapElement *elem = IN_ELEM(dummy); return elem->maybeAs()->extraData; } return 0; } #undef P_ToIndex int P_ToIndex(void const *ptr) { if(!ptr) return -1; if(P_IsDummy(ptr)) return -1; MapElement const *elem = IN_ELEM_CONST(ptr); switch(elem->type()) { case DMU_VERTEX: case DMU_LINE: case DMU_SIDE: case DMU_SECTOR: case DMU_SUBSPACE: case DMU_SKY: return elem->indexInMap(); case DMU_PLANE: return elem->as().indexInSector(); case DMU_MATERIAL: return elem->as().manifest().id(); // 1-based default: /// @todo Throw exception. DENG2_ASSERT(false); // Unknown/non-indexable DMU type. return -1; } } #undef P_ToPtr void *P_ToPtr(int type, int index) { switch(type) { case DMU_VERTEX: return App_WorldSystem().map().vertexPtr(index); case DMU_LINE: return App_WorldSystem().map().linePtr(index); case DMU_SIDE: return App_WorldSystem().map().sidePtr(index); case DMU_SECTOR: return App_WorldSystem().map().sectorPtr(index); case DMU_PLANE: { /// @todo Throw exception. QByteArray msg = String("P_ToPtr: Cannot convert %1 to a ptr (sector is unknown).").arg(DMU_Str(type)).toUtf8(); App_FatalError(msg.constData()); return 0; /* Unreachable. */ } case DMU_SUBSPACE: return App_WorldSystem().map().subspacePtr(index); case DMU_SKY: if(index != 0) return 0; // Only one sky per map, presently. return &App_WorldSystem().map().sky(); case DMU_MATERIAL: /// @note @a index is 1-based. if(index > 0) return &App_ResourceSystem().toMaterialManifest(index).material(); return 0; default: { /// @todo Throw exception. QByteArray msg = String("P_ToPtr: unknown type %1.").arg(DMU_Str(type)).toUtf8(); App_FatalError(msg.constData()); return 0; /* Unreachable. */ } } } #undef P_Count int P_Count(int type) { switch(type) { case DMU_VERTEX: return App_WorldSystem().hasMap()? App_WorldSystem().map().vertexCount() : 0; case DMU_LINE: return App_WorldSystem().hasMap()? App_WorldSystem().map().lineCount() : 0; case DMU_SIDE: return App_WorldSystem().hasMap()? App_WorldSystem().map().sideCount() : 0; case DMU_SECTOR: return App_WorldSystem().hasMap()? App_WorldSystem().map().sectorCount() : 0; case DMU_SUBSPACE: return App_WorldSystem().hasMap()? App_WorldSystem().map().subspaceCount() : 0; case DMU_SKY: return 1; // Only one sky per map presently. case DMU_MATERIAL: return App_ResourceSystem().materialCount(); default: /// @throw Invalid/unknown DMU element type. throw Error("P_Count", String("Unknown type %1").arg(DMU_Str(type))); } } #undef P_Iteratep int P_Iteratep(void *elPtr, uint prop, int (*callback) (void *p, void *ctx), void *context) { MapElement *elem = IN_ELEM(elPtr); switch(elem->type()) { case DMU_SECTOR: { Sector §or = elem->as(); switch(prop) { case DMU_LINE: return sector.forAllSides([&callback, &context] (LineSide &side) { return callback(&side.line(), context); }); case DMU_PLANE: return sector.forAllPlanes([&callback, &context] (Plane &plane) { return callback(&plane, context); }); default: throw Error("P_Iteratep", QString("Property %1 unknown/not vector").arg(DMU_Str(prop))); }} case DMU_SUBSPACE: /// Note: this iteration method is only needed by the games' automap. switch(prop) { case DMU_LINE: { ConvexSubspace &subspace = elem->as(); HEdge *base = subspace.poly().hedge(); HEdge *hedge = base; do { if(hedge->hasMapElement()) { if(int result = callback(&hedge->mapElement().as().line(), context)) return result; } } while((hedge = &hedge->next()) != base); LoopResult result = subspace.forAllExtraMeshes([&callback, &context] (Mesh &mesh) { for(HEdge *hedge : mesh.hedges()) { // Is this on the back of a one-sided line? if(!hedge->hasMapElement()) continue; if(int result = callback(&hedge->mapElement().as().line(), context)) return LoopResult( result ); } return LoopResult(); // continue }); return result; } default: throw Error("P_Iteratep", QString("Property %1 unknown/not vector").arg(DMU_Str(prop))); } default: throw Error("P_Iteratep", QString("Type %1 unknown").arg(DMU_Str(elem->type()))); } return false; } #undef P_Callback int P_Callback(int type, int index, int (*callback)(void *p, void *ctx), void *context) { switch(type) { case DMU_VERTEX: if(Vertex *vtx = App_WorldSystem().map().vertexPtr(index)) { return callback(vtx, context); } break; case DMU_LINE: if(Line *li = App_WorldSystem().map().linePtr(index)) { return callback(li, context); } break; case DMU_SIDE: if(LineSide *si = App_WorldSystem().map().sidePtr(index)) { return callback(si, context); } break; case DMU_SUBSPACE: if(ConvexSubspace *sub = App_WorldSystem().map().subspacePtr(index)) { return callback(sub, context); } break; case DMU_SECTOR: if(Sector *sec = App_WorldSystem().map().sectorPtr(index)) { return callback(sec, context); } break; case DMU_PLANE: { /// @todo Throw exception. QByteArray msg = String("P_Callback: %1 cannot be referenced by id alone (sector is unknown).").arg(DMU_Str(type)).toUtf8(); App_FatalError(msg.constData()); return 0; /* Unreachable */ } case DMU_SKY: { if(index == 0) // Only one sky per map presently. { return callback(&App_WorldSystem().map().sky(), context); } break; } case DMU_MATERIAL: if(index > 0) return callback(&App_ResourceSystem().toMaterialManifest(materialid_t(index)).material(), context); break; case DMU_LINE_BY_TAG: case DMU_SECTOR_BY_TAG: case DMU_LINE_BY_ACT_TAG: case DMU_SECTOR_BY_ACT_TAG: { /// @todo Throw exception. QByteArray msg = String("P_Callback: Type %1 not implemented yet.").arg(DMU_Str(type)).toUtf8(); App_FatalError(msg.constData()); return 0; /* Unreachable */ } default: { /// @todo Throw exception. QByteArray msg = String("P_Callback: Type %1 unknown (index %2).").arg(DMU_Str(type)).arg(index).toUtf8(); App_FatalError(msg.constData()); return 0; /* Unreachable */ } } return false; // Continue iteration. } #undef P_Callbackp int P_Callbackp(int type, void *elPtr, int (*callback)(void *p, void *ctx), void *context) { MapElement *elem = IN_ELEM(elPtr); LOG_AS("P_Callbackp"); switch(type) { case DMU_VERTEX: case DMU_LINE: case DMU_SIDE: case DMU_SECTOR: case DMU_SUBSPACE: case DMU_PLANE: case DMU_MATERIAL: case DMU_SKY: // Only do the callback if the type is the same as the object's. if(type == elem->type()) { return callback(elem, context); } #ifdef DENG_DEBUG else { LOG_DEBUG("Type mismatch %s != %s\n") << DMU_Str(type) << DMU_Str(elem->type()); DENG2_ASSERT(false); } #endif break; default: { /// @todo Throw exception. QByteArray msg = String("P_Callbackp: Type %1 unknown.").arg(DMU_Str(elem->type())).toUtf8(); App_FatalError(msg.constData()); return 0; /* Unreachable */ } } return false; // Continue iteration. } /** * Only those properties that are writable by outside parties (such as games) * are included here. Attempting to set a non-writable property causes a * fatal error. * * When a property changes, the relevant subsystems are notified of the change * so that they can update their state accordingly. */ static void setProperty(MapElement *elem, DmuArgs &args) { DENG_ASSERT(elem != 0); /** * @par Algorithm * When setting a property, reference resolution is done hierarchically so * that we can update all owner's of the objects being manipulated should * the DMU object's Set routine suggest that a change occured (which other * DMU objects may wish/need to respond to). *
    *
  1. Collect references to all current owners of the object. *
  2. Pass the change delta on to the object. *
  3. Object responds: @c true = update owners, ELSE @c false. *
  4. If num collected references > 0: recurse, Object = owners[n] *
*/ // Dereference where necessary. Note the order, these cascade. if(args.type == DMU_SECTOR) { if(args.modifiers & DMU_FLOOR_OF_SECTOR) { elem = &elem->as().floor(); args.type = elem->type(); } else if(args.modifiers & DMU_CEILING_OF_SECTOR) { elem = &elem->as().ceiling(); args.type = elem->type(); } } if(args.type == DMU_LINE) { if(args.modifiers & DMU_FRONT_OF_LINE) { elem = &elem->as().front(); args.type = elem->type(); } else if(args.modifiers & DMU_BACK_OF_LINE) { elem = &elem->as().back(); args.type = elem->type(); } } if(args.type == DMU_SIDE) { if(args.modifiers & DMU_TOP_OF_SIDE) { elem = &elem->as().top(); args.type = elem->type(); } else if(args.modifiers & DMU_MIDDLE_OF_SIDE) { elem = &elem->as().middle(); args.type = elem->type(); } else if(args.modifiers & DMU_BOTTOM_OF_SIDE) { elem = &elem->as().bottom(); args.type = elem->type(); } } if(args.type == DMU_PLANE) { switch(args.prop) { case DMU_MATERIAL: case DMU_OFFSET_X: case DMU_OFFSET_Y: case DMU_OFFSET_XY: case DMU_TANGENT_X: case DMU_TANGENT_Y: case DMU_TANGENT_Z: case DMU_TANGENT_XYZ: case DMU_BITANGENT_X: case DMU_BITANGENT_Y: case DMU_BITANGENT_Z: case DMU_BITANGENT_XYZ: case DMU_NORMAL_X: case DMU_NORMAL_Y: case DMU_NORMAL_Z: case DMU_NORMAL_XYZ: case DMU_COLOR: case DMU_COLOR_RED: case DMU_COLOR_GREEN: case DMU_COLOR_BLUE: case DMU_ALPHA: case DMU_BLENDMODE: case DMU_FLAGS: elem = &elem->as().surface(); args.type = elem->type(); break; default: break; } } // Write the property value(s). /// @throws MapElement::WritePropertyError If the requested property is not writable. elem->setProperty(args); } static void getProperty(MapElement const *elem, DmuArgs &args) { DENG_ASSERT(elem != 0); // Dereference where necessary. Note the order, these cascade. if(args.type == DMU_SECTOR) { if(args.modifiers & DMU_FLOOR_OF_SECTOR) { elem = &elem->as().floor(); args.type = elem->type(); } else if(args.modifiers & DMU_CEILING_OF_SECTOR) { elem = &elem->as().ceiling(); args.type = elem->type(); } } if(args.type == DMU_LINE) { if(args.modifiers & DMU_FRONT_OF_LINE) { elem = &elem->as().front(); args.type = elem->type(); } else if(args.modifiers & DMU_BACK_OF_LINE) { elem = &elem->as().back(); args.type = elem->type(); } } if(args.type == DMU_SIDE) { if(args.modifiers & DMU_TOP_OF_SIDE) { elem = &elem->as().top(); args.type = elem->type(); } else if(args.modifiers & DMU_MIDDLE_OF_SIDE) { elem = &elem->as().middle(); args.type = elem->type(); } else if(args.modifiers & DMU_BOTTOM_OF_SIDE) { elem = &elem->as().bottom(); args.type = elem->type(); } } if(args.type == DMU_PLANE) { switch(args.prop) { case DMU_MATERIAL: case DMU_OFFSET_X: case DMU_OFFSET_Y: case DMU_OFFSET_XY: case DMU_TANGENT_X: case DMU_TANGENT_Y: case DMU_TANGENT_Z: case DMU_TANGENT_XYZ: case DMU_BITANGENT_X: case DMU_BITANGENT_Y: case DMU_BITANGENT_Z: case DMU_BITANGENT_XYZ: case DMU_NORMAL_X: case DMU_NORMAL_Y: case DMU_NORMAL_Z: case DMU_NORMAL_XYZ: case DMU_COLOR: case DMU_COLOR_RED: case DMU_COLOR_GREEN: case DMU_COLOR_BLUE: case DMU_ALPHA: case DMU_BLENDMODE: case DMU_FLAGS: elem = &elem->as().surface(); args.type = elem->type(); break; default: break; } } // Read the property value(s). /// @throws MapElement::UnknownPropertyError If the requested property is not readable. elem->property(args); // Currently no aggregate values are collected. } static int setPropertyWorker(void *elPtr, void *context) { setProperty(IN_ELEM(elPtr), *reinterpret_cast(context)); return false; // Continue iteration. } #undef P_SetBool void P_SetBool(int type, int index, uint prop, dd_bool param) { DmuArgs args(type, prop); args.valueType = DDVT_BOOL; // Make sure invalid values are not allowed. param = (param? true : false); args.booleanValues = ¶m; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetByte void P_SetByte(int type, int index, uint prop, byte param) { DmuArgs args(type, prop); args.valueType = DDVT_BYTE; args.byteValues = ¶m; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetInt void P_SetInt(int type, int index, uint prop, int param) { DmuArgs args(type, prop); args.valueType = DDVT_INT; args.intValues = ¶m; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetFixed void P_SetFixed(int type, int index, uint prop, fixed_t param) { DmuArgs args(type, prop); args.valueType = DDVT_FIXED; args.fixedValues = ¶m; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetAngle void P_SetAngle(int type, int index, uint prop, angle_t param) { DmuArgs args(type, prop); args.valueType = DDVT_ANGLE; args.angleValues = ¶m; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetFloat void P_SetFloat(int type, int index, uint prop, float param) { DmuArgs args(type, prop); args.valueType = DDVT_FLOAT; args.floatValues = ¶m; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetDouble void P_SetDouble(int type, int index, uint prop, double param) { DmuArgs args(type, prop); args.valueType = DDVT_DOUBLE; args.doubleValues = ¶m; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetPtr void P_SetPtr(int type, int index, uint prop, void *param) { DmuArgs args(type, prop); args.valueType = DDVT_PTR; args.ptrValues = ¶m; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetBoolv void P_SetBoolv(int type, int index, uint prop, dd_bool *params) { DmuArgs args(type, prop); args.valueType = DDVT_BOOL; args.booleanValues = params; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetBytev void P_SetBytev(int type, int index, uint prop, byte *params) { DmuArgs args(type, prop); args.valueType = DDVT_BYTE; args.byteValues = params; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetIntv void P_SetIntv(int type, int index, uint prop, int *params) { DmuArgs args(type, prop); args.valueType = DDVT_INT; args.intValues = params; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetFixedv void P_SetFixedv(int type, int index, uint prop, fixed_t *params) { DmuArgs args(type, prop); args.valueType = DDVT_FIXED; args.fixedValues = params; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetAnglev void P_SetAnglev(int type, int index, uint prop, angle_t *params) { DmuArgs args(type, prop); args.valueType = DDVT_ANGLE; args.angleValues = params; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetFloatv void P_SetFloatv(int type, int index, uint prop, float *params) { DmuArgs args(type, prop); args.valueType = DDVT_FLOAT; args.floatValues = params; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetDoublev void P_SetDoublev(int type, int index, uint prop, double *params) { DmuArgs args(type, prop); args.valueType = DDVT_DOUBLE; args.doubleValues = params; P_Callback(type, index, setPropertyWorker, &args); } #undef P_SetPtrv void P_SetPtrv(int type, int index, uint prop, void *params) { DmuArgs args(type, prop); args.valueType = DDVT_PTR; args.ptrValues = (void **)params; P_Callback(type, index, setPropertyWorker, &args); } /* pointer-based write functions */ #undef P_SetBoolp void P_SetBoolp(void *ptr, uint prop, dd_bool param) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_BOOL; // Make sure invalid values are not allowed. param = (param? true : false); args.booleanValues = ¶m; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetBytep void P_SetBytep(void *ptr, uint prop, byte param) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_BYTE; args.byteValues = ¶m; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetIntp void P_SetIntp(void *ptr, uint prop, int param) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_INT; args.intValues = ¶m; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetFixedp void P_SetFixedp(void *ptr, uint prop, fixed_t param) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_FIXED; args.fixedValues = ¶m; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetAnglep void P_SetAnglep(void *ptr, uint prop, angle_t param) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_ANGLE; args.angleValues = ¶m; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetFloatp void P_SetFloatp(void *ptr, uint prop, float param) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_FLOAT; args.floatValues = ¶m; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetDoublep void P_SetDoublep(void *ptr, uint prop, double param) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_DOUBLE; args.doubleValues = ¶m; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetPtrp void P_SetPtrp(void *ptr, uint prop, void *param) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_PTR; args.ptrValues = ¶m; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetBoolpv void P_SetBoolpv(void *ptr, uint prop, dd_bool *params) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_BOOL; args.booleanValues = params; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetBytepv void P_SetBytepv(void *ptr, uint prop, byte *params) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_BYTE; args.byteValues = params; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetIntpv void P_SetIntpv(void *ptr, uint prop, int *params) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_INT; args.intValues = params; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetFixedpv void P_SetFixedpv(void *ptr, uint prop, fixed_t *params) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_FIXED; args.fixedValues = params; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetAnglepv void P_SetAnglepv(void *ptr, uint prop, angle_t *params) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_ANGLE; args.angleValues = params; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetFloatpv void P_SetFloatpv(void *ptr, uint prop, float *params) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_FLOAT; args.floatValues = params; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetDoublepv void P_SetDoublepv(void *ptr, uint prop, double *params) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_DOUBLE; args.doubleValues = params; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } #undef P_SetPtrpv void P_SetPtrpv(void *ptr, uint prop, void *params) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_PTR; args.ptrValues = (void **)params; P_Callbackp(args.type, ptr, setPropertyWorker, &args); } static int getPropertyWorker(void *elPtr, void *context) { getProperty(IN_ELEM_CONST(elPtr), *reinterpret_cast(context)); return false; // Continue iteration. } /* index-based read functions */ #undef P_GetBool dd_bool P_GetBool(int type, int index, uint prop) { dd_bool returnValue = false; DmuArgs args(type, prop); args.valueType = DDVT_BOOL; args.booleanValues = &returnValue; P_Callback(type, index, getPropertyWorker, &args); return returnValue; } #undef P_GetByte byte P_GetByte(int type, int index, uint prop) { byte returnValue = 0; DmuArgs args(type, prop); args.valueType = DDVT_BYTE; args.byteValues = &returnValue; P_Callback(type, index, getPropertyWorker, &args); return returnValue; } #undef P_GetInt int P_GetInt(int type, int index, uint prop) { int returnValue = 0; DmuArgs args(type, prop); args.valueType = DDVT_INT; args.intValues = &returnValue; P_Callback(type, index, getPropertyWorker, &args); return returnValue; } #undef P_GetFixed fixed_t P_GetFixed(int type, int index, uint prop) { fixed_t returnValue = 0; DmuArgs args(type, prop); args.valueType = DDVT_FIXED; args.fixedValues = &returnValue; P_Callback(type, index, getPropertyWorker, &args); return returnValue; } #undef P_GetAngle angle_t P_GetAngle(int type, int index, uint prop) { angle_t returnValue = 0; DmuArgs args(type, prop); args.valueType = DDVT_ANGLE; args.angleValues = &returnValue; P_Callback(type, index, getPropertyWorker, &args); return returnValue; } #undef P_GetFloat float P_GetFloat(int type, int index, uint prop) { float returnValue = 0; DmuArgs args(type, prop); args.valueType = DDVT_FLOAT; args.floatValues = &returnValue; P_Callback(type, index, getPropertyWorker, &args); return returnValue; } #undef P_GetDouble double P_GetDouble(int type, int index, uint prop) { double returnValue = 0; DmuArgs args(type, prop); args.valueType = DDVT_DOUBLE; args.doubleValues = &returnValue; P_Callback(type, index, getPropertyWorker, &args); return returnValue; } #undef P_GetPtr void *P_GetPtr(int type, int index, uint prop) { void *returnValue = 0; DmuArgs args(type, prop); args.valueType = DDVT_PTR; args.ptrValues = &returnValue; P_Callback(type, index, getPropertyWorker, &args); return returnValue; } #undef P_GetBoolv void P_GetBoolv(int type, int index, uint prop, dd_bool *params) { DmuArgs args(type, prop); args.valueType = DDVT_BOOL; args.booleanValues = params; P_Callback(type, index, getPropertyWorker, &args); } #undef P_GetBytev void P_GetBytev(int type, int index, uint prop, byte *params) { DmuArgs args(type, prop); args.valueType = DDVT_BYTE; args.byteValues = params; P_Callback(type, index, getPropertyWorker, &args); } #undef P_GetIntv void P_GetIntv(int type, int index, uint prop, int *params) { DmuArgs args(type, prop); args.valueType = DDVT_INT; args.intValues = params; P_Callback(type, index, getPropertyWorker, &args); } #undef P_GetFixedv void P_GetFixedv(int type, int index, uint prop, fixed_t *params) { DmuArgs args(type, prop); args.valueType = DDVT_FIXED; args.fixedValues = params; P_Callback(type, index, getPropertyWorker, &args); } #undef P_GetAnglev void P_GetAnglev(int type, int index, uint prop, angle_t *params) { DmuArgs args(type, prop); args.valueType = DDVT_ANGLE; args.angleValues = params; P_Callback(type, index, getPropertyWorker, &args); } #undef P_GetFloatv void P_GetFloatv(int type, int index, uint prop, float *params) { DmuArgs args(type, prop); args.valueType = DDVT_FLOAT; args.floatValues = params; P_Callback(type, index, getPropertyWorker, &args); } #undef P_GetDoublev void P_GetDoublev(int type, int index, uint prop, double *params) { DmuArgs args(type, prop); args.valueType = DDVT_DOUBLE; args.doubleValues = params; P_Callback(type, index, getPropertyWorker, &args); } #undef P_GetPtrv void P_GetPtrv(int type, int index, uint prop, void *params) { DmuArgs args(type, prop); args.valueType = DDVT_PTR; args.ptrValues = (void **)params; P_Callback(type, index, getPropertyWorker, &args); } /* pointer-based read functions */ #undef P_GetBoolp dd_bool P_GetBoolp(void *ptr, uint prop) { dd_bool returnValue = false; if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_BOOL; args.booleanValues = &returnValue; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } return returnValue; } #undef P_GetBytep byte P_GetBytep(void *ptr, uint prop) { byte returnValue = 0; if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_BYTE; args.byteValues = &returnValue; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } return returnValue; } #undef P_GetIntp int P_GetIntp(void *ptr, uint prop) { int returnValue = 0; if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_INT; args.intValues = &returnValue; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } return returnValue; } #undef P_GetFixedp fixed_t P_GetFixedp(void *ptr, uint prop) { fixed_t returnValue = 0; if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_FIXED; args.fixedValues = &returnValue; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } return returnValue; } #undef P_GetAnglep angle_t P_GetAnglep(void *ptr, uint prop) { angle_t returnValue = 0; if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_ANGLE; args.angleValues = &returnValue; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } return returnValue; } #undef P_GetFloatp float P_GetFloatp(void *ptr, uint prop) { float returnValue = 0; if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_FLOAT; args.floatValues = &returnValue; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } return returnValue; } #undef P_GetDoublep double P_GetDoublep(void *ptr, uint prop) { double returnValue = 0; if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_DOUBLE; args.doubleValues = &returnValue; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } return returnValue; } #undef P_GetPtrp void *P_GetPtrp(void *ptr, uint prop) { void *returnValue = 0; if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_PTR; args.ptrValues = &returnValue; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } return returnValue; } #undef P_GetBoolpv void P_GetBoolpv(void *ptr, uint prop, dd_bool *params) { if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_BOOL; args.booleanValues = params; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } } #undef P_GetBytepv void P_GetBytepv(void *ptr, uint prop, byte *params) { if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_BYTE; args.byteValues = params; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } } #undef P_GetIntpv void P_GetIntpv(void *ptr, uint prop, int *params) { if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_INT; args.intValues = params; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } } #undef P_GetFixedpv void P_GetFixedpv(void *ptr, uint prop, fixed_t *params) { if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_FIXED; args.fixedValues = params; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } } #undef P_GetAnglepv void P_GetAnglepv(void *ptr, uint prop, angle_t *params) { if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_ANGLE; args.angleValues = params; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } } #undef P_GetFloatpv void P_GetFloatpv(void *ptr, uint prop, float *params) { if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_FLOAT; args.floatValues = params; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } } #undef P_GetDoublepv void P_GetDoublepv(void *ptr, uint prop, double *params) { if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_DOUBLE; args.doubleValues = params; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } } #undef P_GetPtrpv void P_GetPtrpv(void *ptr, uint prop, void *params) { if(ptr) { DmuArgs args(DMU_GetType(ptr), prop); args.valueType = DDVT_PTR; args.ptrValues = (void **)params; P_Callbackp(args.type, ptr, getPropertyWorker, &args); } } #undef P_MapExists DENG_EXTERN_C dd_bool P_MapExists(char const *uriCString) { if(!uriCString || !uriCString[0]) return false; return App_ResourceSystem().mapDef(de::Uri(uriCString, RC_NULL)) != 0; } #undef P_MapIsCustom DENG_EXTERN_C dd_bool P_MapIsCustom(char const *uriCString) { if(!uriCString || !uriCString[0]) return false; if(MapDef const *mapDef = App_ResourceSystem().mapDef(de::Uri(uriCString, RC_NULL))) { return mapDef->sourceFile()->hasCustom(); } return false; } #undef P_MapSourceFile DENG_EXTERN_C AutoStr *P_MapSourceFile(char const *uriCString) { if(!uriCString || !uriCString[0]) return 0; if(MapDef const *mapDef = App_ResourceSystem().mapDef(de::Uri(uriCString, RC_NULL))) { return AutoStr_FromTextStd(mapDef->sourceFile()->composePath().toUtf8().constData()); } return AutoStr_NewStd(); } #undef P_MapChange DENG_EXTERN_C dd_bool P_MapChange(char const *uriCString) { if(!uriCString || !uriCString[0]) { App_FatalError("P_MapChange: Invalid Uri argument."); } #ifdef __CLIENT__ App_ResourceSystem().purgeCacheQueue(); #endif if(isServer) { // Whenever the map changes, remote players must tell us when they're // ready to begin receiving frames. for(uint i = 0; i < DDMAXPLAYERS; ++i) { //player_t *plr = &ddPlayers[i]; if(/*!(plr->shared.flags & DDPF_LOCAL) &&*/ clients[i].connected) { LOG_DEBUG("Client %i marked as 'not ready' to receive frames.") << i; clients[i].ready = false; } } } return (dd_bool) App_WorldSystem().changeMap(de::Uri(uriCString, RC_NULL)); } #undef P_CountMapObjs DENG_EXTERN_C uint P_CountMapObjs(int entityId) { if(!App_WorldSystem().hasMap()) return 0; EntityDatabase &entities = App_WorldSystem().map().entityDatabase(); return entities.entityCount(P_MapEntityDef(entityId)); } // entitydef.cpp DENG_EXTERN_C byte P_GetGMOByte(int entityId, int elementIndex, int propertyId); DENG_EXTERN_C short P_GetGMOShort(int entityId, int elementIndex, int propertyId); DENG_EXTERN_C int P_GetGMOInt(int entityId, int elementIndex, int propertyId); DENG_EXTERN_C fixed_t P_GetGMOFixed(int entityId, int elementIndex, int propertyId); DENG_EXTERN_C angle_t P_GetGMOAngle(int entityId, int elementIndex, int propertyId); DENG_EXTERN_C float P_GetGMOFloat(int entityId, int elementIndex, int propertyId); #undef Mobj_Link DENG_EXTERN_C void Mobj_Link(mobj_t *mobj, int flags) { if(!mobj || !App_WorldSystem().hasMap()) return; // Huh? App_WorldSystem().map().link(*mobj, flags); } #undef Mobj_Unlink DENG_EXTERN_C void Mobj_Unlink(mobj_t *mobj) { if(!mobj || !Mobj_IsLinked(*mobj)) return; Mobj_Map(*mobj).unlink(*mobj); } #undef Mobj_TouchedLinesIterator DENG_EXTERN_C int Mobj_TouchedLinesIterator(mobj_t *mob, int (*callback) (Line *, void *), void *context) { DENG2_ASSERT(mob && callback); LoopResult result = Mobj_Map(*mob).forAllLinesTouchingMobj(*mob, [&callback, &context] (Line &line) { return LoopResult( callback(&line, context) ); }); return result; } #undef Mobj_TouchedSectorsIterator DENG_EXTERN_C int Mobj_TouchedSectorsIterator(mobj_t *mob, int (*callback) (Sector *, void *), void *context) { DENG2_ASSERT(mob && callback); LoopResult result = Mobj_Map(*mob).forAllSectorsTouchingMobj(*mob, [&callback, &context] (Sector §or) { return LoopResult( callback(§or, context) ); }); return result; } #undef Line_TouchingMobjsIterator DENG_EXTERN_C int Line_TouchingMobjsIterator(Line *line, int (*callback) (mobj_t *, void *), void *context) { DENG2_ASSERT(line && callback); LoopResult result = line->map().forAllMobjsTouchingLine(*line, [&callback, &context] (mobj_t &mob) { return LoopResult( callback(&mob, context) ); }); return result; } #undef Sector_TouchingMobjsIterator DENG_EXTERN_C int Sector_TouchingMobjsIterator(Sector *sector, int (*callback) (mobj_t *, void *), void *context) { DENG2_ASSERT(sector && callback); LoopResult result = sector->map().forAllMobjsTouchingSector(*sector, [&callback, &context] (mobj_t &mob) { return LoopResult( callback(&mob, context) ); }); return result; } #undef Sector_AtPoint_FixedPrecision DENG_EXTERN_C Sector *Sector_AtPoint_FixedPrecision(const_pvec2d_t point) { if(!App_WorldSystem().hasMap()) return 0; return App_WorldSystem().map().bspLeafAt_FixedPrecision(point).sectorPtr(); } #undef Mobj_BoxIterator DENG_EXTERN_C int Mobj_BoxIterator(AABoxd const *box, int (*callback) (mobj_t *, void *), void *context) { DENG2_ASSERT(box && callback); LoopResult result = LoopContinue; if(App_WorldSystem().hasMap()) { Map const &map = App_WorldSystem().map(); int const localValidCount = validCount; result = map.mobjBlockmap().forAllInBox(*box, [&callback, &context, &localValidCount] (void *object) { mobj_t &mob = *(mobj_t *)object; if(mob.validCount != localValidCount) // not yet processed { mob.validCount = localValidCount; return LoopResult( callback(&mob, context) ); } return LoopResult(); // continue }); } return result; } #undef Polyobj_BoxIterator DENG_EXTERN_C int Polyobj_BoxIterator(AABoxd const *box, int (*callback) (struct polyobj_s *, void *), void *context) { DENG2_ASSERT(box && callback); LoopResult result = LoopContinue; if(App_WorldSystem().hasMap()) { Map const &map = App_WorldSystem().map(); int const localValidCount = validCount; result = map.polyobjBlockmap().forAllInBox(*box, [&callback, &context, &localValidCount] (void *object) { Polyobj &pob = *(Polyobj *)object; if(pob.validCount != localValidCount) // not yet processed { pob.validCount = localValidCount; return LoopResult( callback(&pob, context) ); } return LoopResult(); // continue }); } return result; } #undef Line_BoxIterator DENG_EXTERN_C int Line_BoxIterator(AABoxd const *box, int flags, int (*callback) (Line *, void *), void *context) { DENG2_ASSERT(box && callback); LoopResult result = LoopContinue; if(App_WorldSystem().hasMap()) { Map const &map = App_WorldSystem().map(); result = map.forAllLinesInBox(*box, flags, [&callback, &context] (Line &line) { return LoopResult( callback(&line, context) ); }); } return result; } #undef Subspace_BoxIterator DENG_EXTERN_C int Subspace_BoxIterator(AABoxd const *box, int (*callback) (ConvexSubspace *, void *), void *context) { DENG2_ASSERT(box && callback); LoopResult result = LoopContinue; if(App_WorldSystem().hasMap()) { Map const &map = App_WorldSystem().map(); int const localValidCount = validCount; result = map.subspaceBlockmap().forAllInBox(*box, [&box, &callback, &context, &localValidCount] (void *object) { ConvexSubspace &sub = *(ConvexSubspace *)object; if(sub.validCount() != localValidCount) // not yet processed { sub.setValidCount(localValidCount); // Check the bounds. AABoxd const &polyBox = sub.poly().aaBox(); if(!(polyBox.maxX < box->minX || polyBox.minX > box->maxX || polyBox.minY > box->maxY || polyBox.maxY < box->minY)) { return LoopResult( callback(&sub, context) ); } } return LoopResult(); // continue }); } return result; } #undef P_PathTraverse2 DENG_EXTERN_C int P_PathTraverse2(const_pvec2d_t from, const_pvec2d_t to, int flags, traverser_t callback, void *context) { if(App_WorldSystem().hasMap()) { Map &map = App_WorldSystem().map(); return Interceptor(callback, from, to, flags, context).trace(map); } return false; // Continue iteration. } #undef P_PathTraverse DENG_EXTERN_C int P_PathTraverse(const_pvec2d_t from, const_pvec2d_t to, traverser_t callback, void *context) { if(App_WorldSystem().hasMap()) { Map &map = App_WorldSystem().map(); return Interceptor(callback, from, to, PTF_ALL, context).trace(map); } return false; // Continue iteration. } #undef P_CheckLineSight DENG_EXTERN_C dd_bool P_CheckLineSight(const_pvec3d_t from, const_pvec3d_t to, coord_t bottomSlope, coord_t topSlope, int flags) { if(App_WorldSystem().hasMap()) { Map &map = App_WorldSystem().map(); return LineSightTest(from, to, bottomSlope, topSlope, flags).trace(map.bspTree()); } return false; // Continue iteration. } #undef Interceptor_Origin DENG_EXTERN_C coord_t const *Interceptor_Origin(Interceptor const *trace) { if(!trace) return 0; return trace->origin(); } #undef Interceptor_Direction DENG_EXTERN_C coord_t const *(Interceptor_Direction)(Interceptor const *trace) { if(!trace) return 0; return trace->direction(); } #undef Interceptor_Opening DENG_EXTERN_C LineOpening const *Interceptor_Opening(Interceptor const *trace) { if(!trace) return 0; return &trace->opening(); } #undef Interceptor_AdjustOpening DENG_EXTERN_C dd_bool Interceptor_AdjustOpening(Interceptor *trace, Line *line) { if(!trace) return false; return trace->adjustOpening(line); } #undef Mobj_CreateXYZ DENG_EXTERN_C mobj_t *Mobj_CreateXYZ(thinkfunc_t function, coord_t x, coord_t y, coord_t z, angle_t angle, coord_t radius, coord_t height, int ddflags) { return P_MobjCreate(function, Vector3d(x, y, z), angle, radius, height, ddflags); } // p_mobj.c DENG_EXTERN_C void Mobj_Destroy(mobj_t *mobj); DENG_EXTERN_C void Mobj_SetState(mobj_t *mobj, int statenum); DENG_EXTERN_C angle_t Mobj_AngleSmoothed(mobj_t *mobj); DENG_EXTERN_C void Mobj_OriginSmoothed(mobj_t *mobj, coord_t origin[3]); DENG_EXTERN_C Sector *Mobj_Sector(mobj_t const *mobj); DENG_EXTERN_C void Mobj_SpawnDamageParticleGen(mobj_t *mobj, mobj_t *inflictor, int amount); // p_think.c DENG_EXTERN_C struct mobj_s* Mobj_ById(int id); #undef Polyobj_SetCallback DENG_EXTERN_C void Polyobj_SetCallback(void (*func) (struct mobj_s *, void *, void *)) { Polyobj::setCollisionCallback(func); } #undef Polyobj_Unlink DENG_EXTERN_C void Polyobj_Unlink(Polyobj *po) { if(!po) return; po->unlink(); } #undef Polyobj_Link DENG_EXTERN_C void Polyobj_Link(Polyobj *po) { if(!po) return; po->link(); } #undef Polyobj_ById DENG_EXTERN_C Polyobj *Polyobj_ById(int index) { if(!App_WorldSystem().hasMap()) return nullptr; return App_WorldSystem().map().polyobjPtr(index); } #undef Polyobj_ByTag DENG_EXTERN_C Polyobj *Polyobj_ByTag(int tag) { Polyobj *found = nullptr; // not found. if(App_WorldSystem().hasMap()) { App_WorldSystem().map().forAllPolyobjs([&tag, &found] (Polyobj &pob) { if(pob.tag == tag) { found = &pob; return LoopAbort; } return LoopContinue; }); } return found; } #undef Polyobj_Move DENG_EXTERN_C dd_bool Polyobj_Move(Polyobj *po, const_pvec3d_t xy) { if(!po) return false; return po->move(xy); } #undef Polyobj_MoveXY DENG_EXTERN_C dd_bool Polyobj_MoveXY(Polyobj *po, coord_t x, coord_t y) { if(!po) return false; return po->move(x, y); } #undef Polyobj_Rotate DENG_EXTERN_C dd_bool Polyobj_Rotate(Polyobj *po, angle_t angle) { if(!po) return false; return po->rotate(angle); } #undef Polyobj_FirstLine DENG_EXTERN_C Line *Polyobj_FirstLine(Polyobj *po) { if(!po) return 0; return po->lines()[0]; } #undef Line_PointDistance DENG_EXTERN_C coord_t Line_PointDistance(Line *line, coord_t const point[2], coord_t *offset) { DENG_ASSERT(line); return line->pointDistance(point, offset); } #undef Line_PointOnSide DENG_EXTERN_C coord_t Line_PointOnSide(Line const *line, coord_t const point[2]) { DENG_ASSERT(line); if(!point) { LOG_AS("Line_PointOnSide"); LOG_DEBUG("Invalid arguments, returning >0."); return 1; } return line->pointOnSide(point); } #undef Line_BoxOnSide DENG_EXTERN_C int Line_BoxOnSide(Line *line, AABoxd const *box) { DENG_ASSERT(line && box); return line->boxOnSide(*box); } #undef Line_BoxOnSide_FixedPrecision DENG_EXTERN_C int Line_BoxOnSide_FixedPrecision(Line *line, AABoxd const *box) { DENG_ASSERT(line && box); return line->boxOnSide_FixedPrecision(*box); } #undef Line_Opening DENG_EXTERN_C void Line_Opening(Line *line, LineOpening *opening) { DENG2_ASSERT(line && opening); *opening = LineOpening(*line); } DENG_DECLARE_API(Map) = { { DE_API_MAP }, P_MapExists, P_MapIsCustom, P_MapSourceFile, P_MapChange, Line_BoxIterator, Line_BoxOnSide, Line_BoxOnSide_FixedPrecision, Line_PointDistance, Line_PointOnSide, Line_TouchingMobjsIterator, Line_Opening, Sector_TouchingMobjsIterator, Sector_AtPoint_FixedPrecision, Mobj_CreateXYZ, Mobj_Destroy, Mobj_ById, Mobj_BoxIterator, Mobj_SetState, Mobj_Link, Mobj_Unlink, Mobj_SpawnDamageParticleGen, Mobj_TouchedLinesIterator, Mobj_TouchedSectorsIterator, Mobj_OriginSmoothed, Mobj_AngleSmoothed, Mobj_Sector, Polyobj_MoveXY, Polyobj_Rotate, Polyobj_Link, Polyobj_Unlink, Polyobj_FirstLine, Polyobj_ById, Polyobj_ByTag, Polyobj_BoxIterator, Polyobj_SetCallback, Subspace_BoxIterator, P_PathTraverse, P_PathTraverse2, P_CheckLineSight, Interceptor_Origin, Interceptor_Direction, Interceptor_Opening, Interceptor_AdjustOpening, DMU_Str, DMU_GetType, P_ToIndex, P_ToPtr, P_Count, P_Callback, P_Callbackp, P_Iteratep, P_AllocDummy, P_FreeDummy, P_IsDummy, P_DummyExtraData, P_CountMapObjs, P_GetGMOByte, P_GetGMOShort, P_GetGMOInt, P_GetGMOFixed, P_GetGMOAngle, P_GetGMOFloat, P_SetBool, P_SetByte, P_SetInt, P_SetFixed, P_SetAngle, P_SetFloat, P_SetDouble, P_SetPtr, P_SetBoolv, P_SetBytev, P_SetIntv, P_SetFixedv, P_SetAnglev, P_SetFloatv, P_SetDoublev, P_SetPtrv, P_SetBoolp, P_SetBytep, P_SetIntp, P_SetFixedp, P_SetAnglep, P_SetFloatp, P_SetDoublep, P_SetPtrp, P_SetBoolpv, P_SetBytepv, P_SetIntpv, P_SetFixedpv, P_SetAnglepv, P_SetFloatpv, P_SetDoublepv, P_SetPtrpv, P_GetBool, P_GetByte, P_GetInt, P_GetFixed, P_GetAngle, P_GetFloat, P_GetDouble, P_GetPtr, P_GetBoolv, P_GetBytev, P_GetIntv, P_GetFixedv, P_GetAnglev, P_GetFloatv, P_GetDoublev, P_GetPtrv, P_GetBoolp, P_GetBytep, P_GetIntp, P_GetFixedp, P_GetAnglep, P_GetFloatp, P_GetDoublep, P_GetPtrp, P_GetBoolpv, P_GetBytepv, P_GetIntpv, P_GetFixedpv, P_GetAnglepv, P_GetFloatpv, P_GetDoublepv, P_GetPtrpv }; doomsday-stable-1.15.7/doomsday/client/src/world/grabbable.cpp0000664000175000017500000000352012641367670023614 0ustar jaakkojaakko/** @file grabbable.cpp Grabbable. * * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/grabbable.h" using namespace de; Grabbable::Grabbable(): _grabs(0), _locked(true) {} Grabbable::~Grabbable() { DENG2_FOR_AUDIENCE(Deletion, i) i->grabbableBeingDeleted(*this); } void Grabbable::grab() { addGrab(); // Succeeded. } void Grabbable::ungrab() { decGrab(); // Succeeded. } bool Grabbable::isGrabbed() const { return _grabs != 0; } bool Grabbable::isLocked() const { return _locked; } void Grabbable::move(Vector3d const &newOrigin) { if(!_locked) { setOrigin(newOrigin); return; } //qDebug() << "Grabbable" << de::dintptr(this) << "is locked, move denied."; } void Grabbable::addGrab() { ++_grabs; DENG2_ASSERT(_grabs >= 0); } void Grabbable::decGrab() { --_grabs; DENG2_ASSERT(_grabs >= 0); } void Grabbable::setLock(bool enable) { if(_locked != enable) { _locked = enable; DENG2_FOR_AUDIENCE(LockChange, i) i->grabbableLockChanged(*this); } } doomsday-stable-1.15.7/doomsday/client/src/world/sectorcluster.cpp0000664000175000017500000012455612641367670024631 0ustar jaakkojaakko/** @file sectorcluster.cpp World map sector cluster. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "world/sectorcluster.h" #include "Face" #include "BspLeaf" #include "ConvexSubspace" #include "Line" #include "Plane" #include "Surface" #ifdef __CLIENT__ # include "world/blockmap.h" #endif #include "world/map.h" #include "world/p_object.h" #include "world/p_players.h" #ifdef __CLIENT__ # include "render/rend_main.h" // useBias # include "BiasIllum" # include "BiasTracker" # include "Shard" #endif #include #include #include #include #include #include #include #include namespace de { namespace internal { /// Classification flags: enum ClusterFlag { NeverMapped = 0x01, AllMissingBottom = 0x02, AllMissingTop = 0x04, AllSelfRef = 0x08, PartSelfRef = 0x10 }; Q_DECLARE_FLAGS(ClusterFlags, ClusterFlag) Q_DECLARE_OPERATORS_FOR_FLAGS(ClusterFlags) static QRectF qrectFromAABox(AABoxd const &aaBox) { return QRectF(QPointF(aaBox.minX, aaBox.maxY), QPointF(aaBox.maxX, aaBox.minY)); } } } using namespace de; using namespace de::internal; DENG2_PIMPL(SectorCluster) , DENG2_OBSERVES(SectorCluster, Deletion) , DENG2_OBSERVES(Plane, Deletion) , DENG2_OBSERVES(Plane, HeightChange) #ifdef __CLIENT__ , DENG2_OBSERVES(Plane, HeightSmoothedChange) , DENG2_OBSERVES(Sector, LightColorChange) , DENG2_OBSERVES(Sector, LightLevelChange) #endif { bool needClassify; ///< @c true= (Re)classification is necessary. ClusterFlags flags; Subspaces subspaces; QScopedPointer aaBox; SectorCluster *mappedVisFloor; SectorCluster *mappedVisCeiling; struct BoundaryData { /// Lists of unique exterior clusters which share a boundary edge with /// "this" cluster (i.e., one edge per cluster). QList uniqueInnerEdges; /// not owned. QList uniqueOuterEdges; /// not owned. }; QScopedPointer boundaryData; #ifdef __CLIENT__ struct GeometryData { MapElement *mapElement; int geomId; QScopedPointer shard; GeometryData(MapElement *mapElement, int geomId) : mapElement(mapElement), geomId(geomId) {} }; /// @todo Avoid two-stage lookup. typedef QMap Shards; typedef QMap GeometryGroups; GeometryGroups geomGroups; /// Reverse lookup hash from Shard => GeometryData. typedef QHash ShardGeometryMap; ShardGeometryMap shardGeomMap; /// Subspaces in the neighborhood effecting environmental audio characteristics. typedef QSet ReverbSubspaces; ReverbSubspaces reverbSubspaces; /// Final environmental audio characteristics. AudioEnvironmentFactors reverb; bool needReverbUpdate; #endif Instance(Public *i) : Base (i) , needClassify (true) , flags (0) , mappedVisFloor (0) , mappedVisCeiling(0) #ifdef __CLIENT__ , needReverbUpdate(true) #endif { #ifdef __CLIENT__ de::zap(reverb); #endif } ~Instance() { observePlane(§or().floor(), false); observePlane(§or().ceiling(), false); #ifdef __CLIENT__ sector().audienceForLightLevelChange -= this; sector().audienceForLightColorChange -= this; DENG2_FOR_EACH(GeometryGroups, geomGroup, geomGroups) { Shards &shards = *geomGroup; qDeleteAll(shards); } #endif clearMapping(Sector::Floor); clearMapping(Sector::Ceiling); DENG2_FOR_PUBLIC_AUDIENCE(Deletion, i) i->sectorClusterBeingDeleted(self); } inline Sector §or() { DENG2_ASSERT(!subspaces.isEmpty()); return *subspaces.first()->bspLeaf().sectorPtr(); } inline bool floorIsMapped() { return mappedVisFloor != 0 && mappedVisFloor != thisPublic; } inline bool ceilingIsMapped() { return mappedVisCeiling != 0 && mappedVisCeiling != thisPublic; } inline bool needRemapVisPlanes() { return mappedVisFloor == 0 || mappedVisCeiling == 0; } SectorCluster **mappedClusterAdr(int planeIdx) { if(planeIdx == Sector::Floor) return &mappedVisFloor; if(planeIdx == Sector::Ceiling) return &mappedVisCeiling; return nullptr; } inline Plane *mappedPlane(int planeIdx) { SectorCluster **clusterAdr = mappedClusterAdr(planeIdx); if(clusterAdr && *clusterAdr) { return &(*clusterAdr)->plane(planeIdx); } return nullptr; } void observeCluster(SectorCluster *cluster, bool yes = true) { if(!cluster || cluster == thisPublic) return; if(yes) cluster->audienceForDeletion += this; else cluster->audienceForDeletion -= this; } void observePlane(Plane *plane, bool yes = true, bool observeHeight = true) { if(!plane) return; if(yes) { plane->audienceForDeletion() += this; if(observeHeight) { plane->audienceForHeightChange() += this; #ifdef __CLIENT__ plane->audienceForHeightSmoothedChange() += this; #endif } } else { plane->audienceForDeletion() -= this; plane->audienceForHeightChange() -= this; #ifdef __CLIENT__ plane->audienceForHeightSmoothedChange() -= this; #endif } } void map(int planeIdx, SectorCluster *newCluster, bool permanent = false) { SectorCluster **clusterAdr = mappedClusterAdr(planeIdx); if(!clusterAdr || *clusterAdr == newCluster) return; if(*clusterAdr != thisPublic) { observePlane(mappedPlane(planeIdx), false); } observeCluster(*clusterAdr, false); *clusterAdr = newCluster; observeCluster(*clusterAdr); if(*clusterAdr != thisPublic) { observePlane(mappedPlane(planeIdx), true, !permanent); } } void clearMapping(int planeIdx) { map(planeIdx , 0); } /** * To be called when a plane moves to possibly invalidate mapped planes so * that they will be re-evaluated later. */ void maybeInvalidateMapping(int planeIdx) { if(classification() & NeverMapped) return; SectorCluster **clusterAdr = mappedClusterAdr(planeIdx); if(!clusterAdr || *clusterAdr == thisPublic) return; clearMapping(planeIdx); if(classification() & (AllMissingBottom|AllMissingTop)) { // Reclassify incase material visibility has changed. needClassify = true; } } /** * Returns a copy of the classification flags for the cluster, performing * classification of the cluster if necessary. */ ClusterFlags classification() { if(needClassify) { needClassify = false; flags &= ~(NeverMapped|PartSelfRef); flags |= AllSelfRef|AllMissingBottom|AllMissingTop; foreach(ConvexSubspace const *subspace, subspaces) { HEdge const *base = subspace->poly().hedge(); HEdge const *hedge = base; do { if(!hedge->hasMapElement()) continue; // This edge defines a section of a map line. // If a back geometry is missing then never map planes. if(!hedge->twin().hasFace()) { flags |= NeverMapped; flags &= ~(PartSelfRef|AllSelfRef|AllMissingBottom|AllMissingTop); return flags; } if(!hedge->twin().face().hasMapElement()) continue; ConvexSubspace const &backSubspace = hedge->twin().face().mapElementAs(); // Cluster internal edges are not considered. if(&backSubspace.cluster() == thisPublic) continue; LineSide const &frontSide = hedge->mapElementAs().lineSide(); LineSide const &backSide = hedge->twin().mapElementAs().lineSide(); // Similarly if no sections are defined for either side then // never map planes. This can happen due to mapping errors // where a group of one-sided lines facing outward in the // void partly form a convex subspace. if(!frontSide.hasSections() || !backSide.hasSections()) { flags |= NeverMapped; flags &= ~(PartSelfRef|AllSelfRef|AllMissingBottom|AllMissingTop); return flags; } if(frontSide.line().isSelfReferencing()) { flags |= PartSelfRef; continue; } flags &= ~AllSelfRef; if(frontSide.bottom().hasDrawableNonFixMaterial()) { flags &= ~AllMissingBottom; } if(frontSide.top().hasDrawableNonFixMaterial()) { flags &= ~AllMissingTop; } SectorCluster const &backCluster = backSubspace.cluster(); if(backCluster.floor().height() < sector().floor().height() && backSide.bottom().hasDrawableNonFixMaterial()) { flags &= ~AllMissingBottom; } if(backCluster.ceiling().height() > sector().ceiling().height() && backSide.top().hasDrawableNonFixMaterial()) { flags &= ~AllMissingTop; } } while((hedge = &hedge->next()) != base); } } return flags; } void initBoundaryDataIfNeeded() { if(!boundaryData.isNull()) return; QMap extClusterMap; foreach(ConvexSubspace *subspace, subspaces) { HEdge *base = subspace->poly().hedge(); HEdge *hedge = base; do { if(!hedge->hasMapElement()) continue; if(!hedge->twin().hasFace() || !hedge->twin().face().hasMapElement()) continue; SectorCluster &backCluster = hedge->twin().face().mapElementAs().cluster(); if(&backCluster == thisPublic) continue; extClusterMap.insert(&backCluster, hedge); } while((hedge = &hedge->next()) != base); } boundaryData.reset(new BoundaryData); if(extClusterMap.isEmpty()) return; QRectF boundingRect = qrectFromAABox(self.aaBox()); // First try to quickly decide by comparing cluster bounding boxes. QMutableMapIterator iter(extClusterMap); while(iter.hasNext()) { iter.next(); SectorCluster &extCluster = iter.value()->twin().face().mapElementAs().cluster(); if(!boundingRect.contains(qrectFromAABox(extCluster.aaBox()))) { boundaryData->uniqueOuterEdges.append(iter.value()); iter.remove(); } } if(extClusterMap.isEmpty()) return; // More extensive tests are necessary. At this point we know that all // clusters which remain in the map are inside according to the bounding // box of "this" cluster. QList const boundaryEdges = extClusterMap.values(); QList boundaries; foreach(HEdge *base, boundaryEdges) { QRectF bounds; SectorClusterCirculator it(base); do { bounds |= QRectF(QPointF(it->origin().x, it->origin().y), QPointF(it->twin().origin().x, it->twin().origin().y)) .normalized(); } while(&it.next() != base); boundaries.append(bounds); } QRectF const *largest = 0; foreach(QRectF const &boundary, boundaries) { if(!largest || boundary.contains(*largest)) largest = &boundary; } for(int i = 0; i < boundaryEdges.count(); ++i) { HEdge *hedge = boundaryEdges[i]; QRectF const &boundary = boundaries[i]; if(&boundary == largest || boundary == *largest) { boundaryData->uniqueOuterEdges.append(hedge); } else { boundaryData->uniqueInnerEdges.append(hedge); } } } void remapVisPlanes() { // By default both planes are mapped to the parent sector. if(!floorIsMapped()) map(Sector::Floor, thisPublic); if(!ceilingIsMapped()) map(Sector::Ceiling, thisPublic); if(classification() & NeverMapped) return; if(classification() & (AllSelfRef|PartSelfRef)) { // Should we permanently map planes to another cluster? initBoundaryDataIfNeeded(); foreach(HEdge *hedge, boundaryData->uniqueOuterEdges) { SectorCluster &extCluster = hedge->twin().face().mapElementAs().cluster(); if(!hedge->mapElementAs().line().isSelfReferencing()) continue; if(!(classification() & AllSelfRef) && (extCluster.d->classification() & AllSelfRef)) continue; if(extCluster.d->mappedVisFloor == thisPublic) continue; // Setup the mapping and we're done. map(Sector::Floor, &extCluster, true /*permanently*/); map(Sector::Ceiling, &extCluster, true /*permanently*/); break; } if(floorIsMapped()) { // Remove the mapping from all inner clusters to this, forcing // their re-evaluation (however next time a different cluster // will be selected from the boundary). foreach(HEdge *hedge, boundaryData->uniqueInnerEdges) { SectorCluster &extCluster = hedge->twin().face().mapElementAs().cluster(); if(!hedge->mapElementAs().line().isSelfReferencing()) continue; if(!(classification() & AllSelfRef) && (extCluster.d->classification() & AllSelfRef)) continue; if(extCluster.d->mappedVisFloor == thisPublic) { extCluster.d->clearMapping(Sector::Floor); } if(extCluster.d->mappedVisCeiling == thisPublic) { extCluster.d->clearMapping(Sector::Ceiling); } } // Permanent mappings won't be remapped. return; } } if(classification() & AllSelfRef) return; // // Dynamic mapping may be needed for one or more planes. // // The sector must have open space. if(sector().ceiling().height() <= sector().floor().height()) return; bool doFloor = !floorIsMapped() && classification().testFlag(AllMissingBottom); bool doCeiling = !ceilingIsMapped() && classification().testFlag(AllMissingTop); if(!doFloor && !doCeiling) return; initBoundaryDataIfNeeded(); // Map "this" cluster to the first outer cluster found. foreach(HEdge *hedge, boundaryData->uniqueOuterEdges) { SectorCluster &extCluster = hedge->twin().face().mapElementAs().cluster(); if(doFloor && !floorIsMapped()) { Plane &extVisPlane = extCluster.visFloor(); if(!extVisPlane.surface().hasSkyMaskedMaterial() && extVisPlane.height() > sector().floor().height()) { map(Sector::Floor, &extCluster); if(!doCeiling) break; } } if(doCeiling && !ceilingIsMapped()) { Plane &extVisPlane = extCluster.visCeiling(); if(!extVisPlane.surface().hasSkyMaskedMaterial() && extCluster.visCeiling().height() < sector().ceiling().height()) { map(Sector::Ceiling, &extCluster); if(!doFloor) break; } } } if(!floorIsMapped() && !ceilingIsMapped()) return; // Clear mappings for all inner clusters to force re-evaluation (which // may in turn lead to their inner clusters being re-evaluated, producing // a "ripple effect" that will remap any deeply nested dependents). foreach(HEdge *hedge, boundaryData->uniqueInnerEdges) { SectorCluster &extCluster = hedge->twin().face().mapElementAs().cluster(); if(extCluster.d->classification() & NeverMapped) continue; if(doFloor && floorIsMapped() && extCluster.visFloor().height() >= sector().floor().height()) { extCluster.d->clearMapping(Sector::Floor); } if(doCeiling && ceilingIsMapped() && extCluster.visCeiling().height() <= sector().ceiling().height()) { extCluster.d->clearMapping(Sector::Ceiling); } } } #ifdef __CLIENT__ void markAllSurfacesForDecorationUpdate(Line &line) { LineSide &front = line.front(); DENG2_ASSERT(front.hasSections()); { front.middle().markForDecorationUpdate(); front.bottom().markForDecorationUpdate(); front. top().markForDecorationUpdate(); } LineSide &back = line.back(); if(back.hasSections()) { back.middle().markForDecorationUpdate(); back.bottom().markForDecorationUpdate(); back .top().markForDecorationUpdate(); } } /** * To be called when the height changes to update the plotted decoration * origins for surfaces whose material offset is dependant upon this. */ void markDependantSurfacesForDecorationUpdate() { if(ddMapSetup) return; initBoundaryDataIfNeeded(); // Mark surfaces of the outer edge loop. /// @todo What about the special case of a cluster with no outer neighbors? -ds if(!boundaryData->uniqueOuterEdges.isEmpty()) { HEdge *base = boundaryData->uniqueOuterEdges.first(); SectorClusterCirculator it(base); do { if(it->hasMapElement()) // BSP errors may fool the circulator wrt interior edges -ds { markAllSurfacesForDecorationUpdate(it->mapElementAs().line()); } } while(&it.next() != base); } // Mark surfaces of the inner edge loop(s). foreach(HEdge *base, boundaryData->uniqueInnerEdges) { SectorClusterCirculator it(base); do { if(it->hasMapElement()) // BSP errors may fool the circulator wrt interior edges -ds { markAllSurfacesForDecorationUpdate(it->mapElementAs().line()); } } while(&it.next() != base); } } #endif // __CLIENT__ /// Observes SectorCluster Deletion. void sectorClusterBeingDeleted(SectorCluster const &cluster) { if( mappedVisFloor == &cluster) clearMapping(Sector::Floor); if(mappedVisCeiling == &cluster) clearMapping(Sector::Ceiling); } /// Observes Plane Deletion. void planeBeingDeleted(Plane const &plane) { clearMapping(plane.indexInSector()); } #ifdef __CLIENT__ void updateBiasForWallSectionsAfterGeometryMove(HEdge *hedge) { if(!hedge) return; if(!hedge->hasMapElement()) return; MapElement *mapElement = &hedge->mapElement(); if(Shard *shard = self.findShard(*mapElement, LineSide::Middle)) { shard->updateBiasAfterMove(); } if(Shard *shard = self.findShard(*mapElement, LineSide::Bottom)) { shard->updateBiasAfterMove(); } if(Shard *shard = self.findShard(*mapElement, LineSide::Top)) { shard->updateBiasAfterMove(); } } #endif /// Observes Plane HeightChange. void planeHeightChanged(Plane &plane) { if(&plane == mappedPlane(plane.indexInSector())) { // Check if there are any camera players in this sector. If their height // is now above the ceiling/below the floor they are now in the void. for(int i = 0; i < DDMAXPLAYERS; ++i) { player_t *plr = &ddPlayers[i]; ddplayer_t *ddpl = &plr->shared; if(!ddpl->inGame || !ddpl->mo) continue; if(Mobj_ClusterPtr(*ddpl->mo) != thisPublic) continue; if((ddpl->flags & DDPF_CAMERA) && (ddpl->mo->origin[VZ] > self.visCeiling().height() - 4 || ddpl->mo->origin[VZ] < self.visFloor().height())) { ddpl->inVoid = true; } } #ifdef __CLIENT__ // We'll need to recalculate environmental audio characteristics. needReverbUpdate = true; if(!ddMapSetup && useBias) { // Inform bias surfaces of changed geometry. for(ConvexSubspace *subspace : subspaces) { if(Shard *shard = self.findShard(*subspace, plane.indexInSector())) { shard->updateBiasAfterMove(); } HEdge *base = subspace->poly().hedge(); HEdge *hedge = base; do { updateBiasForWallSectionsAfterGeometryMove(hedge); } while((hedge = &hedge->next()) != base); subspace->forAllExtraMeshes([this] (Mesh &mesh) { for(HEdge *hedge : mesh.hedges()) { updateBiasForWallSectionsAfterGeometryMove(hedge); } return LoopContinue; }); } } markDependantSurfacesForDecorationUpdate(); #endif // __CLIENT__ } // We may need to update one or both mapped planes. maybeInvalidateMapping(plane.indexInSector()); } #ifdef __CLIENT__ /** * Find the GeometryData for a MapElement by the element-unique @a group * identifier. * * @param geomId Geometry identifier. * @param canAlloc @c true= to allocate if no data exists. Note that the * number of vertices in the fan geometry must be known * at this time. */ GeometryData *geomData(MapElement &mapElement, int geomId, bool canAlloc = false) { GeometryGroups::iterator foundGroup = geomGroups.find(&mapElement); if(foundGroup != geomGroups.end()) { Shards &shards = *foundGroup; Shards::iterator found = shards.find(geomId); if(found != shards.end()) { return *found; } } if(!canAlloc) return nullptr; if(foundGroup == geomGroups.end()) { foundGroup = geomGroups.insert(&mapElement, Shards()); } return *foundGroup->insert(geomId, new GeometryData(&mapElement, geomId)); } /** * Find the GeometryData for the given @a shard. */ GeometryData *geomDataForShard(Shard *shard) { if(shard && shard->cluster() == thisPublic) { ShardGeometryMap::const_iterator found = shardGeomMap.find(shard); if(found != shardGeomMap.end()) return *found; } return nullptr; } void addReverbSubspace(ConvexSubspace *subspace) { if(!subspace) return; reverbSubspaces.insert(subspace); } /** * Perform environmental audio (reverb) initialization. * * Determines the subspaces which contribute to the environmental audio * characteristics. Given that subspaces do not change shape (on the XY plane, * that is), they do not move and are not created/destroyed once the map has * been loaded; this step can be pre-processed. * * @pre The Map's BSP leaf blockmap must be ready for use. */ void findReverbSubspaces() { Map const &map = sector().map(); AABoxd box = self.aaBox(); box.minX -= 128; box.minY -= 128; box.maxX += 128; box.maxY += 128; // Link all convex subspaces whose axis-aligned bounding box intersects // with the affection bounds to the reverb set. int const localValidCount = ++validCount; map.subspaceBlockmap().forAllInBox(box, [this, &box, &localValidCount] (void *object) { ConvexSubspace &sub = *(ConvexSubspace *)object; if(sub.validCount() != localValidCount) // not yet processed { sub.setValidCount(localValidCount); // Check the bounds. AABoxd const &polyBox = sub.poly().aaBox(); if(!(polyBox.maxX < box.minX || polyBox.minX > box.maxX || polyBox.minY > box.maxY || polyBox.maxY < box.minY)) { addReverbSubspace(&sub); } } return LoopContinue; }); } /** * Recalculate environmental audio (reverb) for the sector. */ void updateReverb() { // Need to initialize? if(reverbSubspaces.isEmpty()) { findReverbSubspaces(); } needReverbUpdate = false; uint spaceVolume = int((self.visCeiling().height() - self.visFloor().height()) * self.roughArea()); reverb[SRD_SPACE] = reverb[SRD_VOLUME] = reverb[SRD_DECAY] = reverb[SRD_DAMPING] = 0; for(ConvexSubspace *subspace : reverbSubspaces) { if(subspace->updateAudioEnvironment()) { auto const &subReverb = subspace->audioEnvironmentData().reverb; reverb[SRD_SPACE] += subReverb[SRD_SPACE]; reverb[SRD_VOLUME] += subReverb[SRD_VOLUME] / 255.0f * subReverb[SRD_SPACE]; reverb[SRD_DECAY] += subReverb[SRD_DECAY] / 255.0f * subReverb[SRD_SPACE]; reverb[SRD_DAMPING] += subReverb[SRD_DAMPING] / 255.0f * subReverb[SRD_SPACE]; } } float spaceScatter; if(reverb[SRD_SPACE]) { spaceScatter = spaceVolume / reverb[SRD_SPACE]; // These three are weighted by the space. reverb[SRD_VOLUME] /= reverb[SRD_SPACE]; reverb[SRD_DECAY] /= reverb[SRD_SPACE]; reverb[SRD_DAMPING] /= reverb[SRD_SPACE]; } else { spaceScatter = 0; reverb[SRD_VOLUME] = .2f; reverb[SRD_DECAY] = .4f; reverb[SRD_DAMPING] = 1; } // If the space is scattered, the reverb effect lessens. reverb[SRD_SPACE] /= (spaceScatter > .8 ? 10 : spaceScatter > .6 ? 4 : 1); // Normalize the reverb space [0..1] // 0= very small // .99= very large // 1.0= only for open areas (special case). reverb[SRD_SPACE] /= 120e6; if(reverb[SRD_SPACE] > .99) reverb[SRD_SPACE] = .99f; if(self.visCeiling().surface().hasSkyMaskedMaterial() || self.visFloor().surface().hasSkyMaskedMaterial()) { // An "open" sector. // It can still be small, in which case; reverb is diminished a bit. if(reverb[SRD_SPACE] > .5) reverb[SRD_VOLUME] = 1; // Full volume. else reverb[SRD_VOLUME] = .5f; // Small, but still open. reverb[SRD_SPACE] = 1; } else { // A "closed" sector. // Large spaces have automatically a bit more audible reverb. reverb[SRD_VOLUME] += reverb[SRD_SPACE] / 4; } if(reverb[SRD_VOLUME] > 1) reverb[SRD_VOLUME] = 1; } /// Observes Plane HeightSmoothedChange. void planeHeightSmoothedChanged(Plane &plane) { markDependantSurfacesForDecorationUpdate(); // We may need to update one or both mapped planes. maybeInvalidateMapping(plane.indexInSector()); } /// Observes Sector LightLevelChange. void sectorLightLevelChanged(Sector &changed) { DENG2_ASSERT(&changed == §or()); DENG2_UNUSED(changed); if(sector().map().hasLightGrid()) { sector().map().lightGrid().blockLightSourceChanged(thisPublic); } } /// Observes Sector LightColorChange. void sectorLightColorChanged(Sector &changed) { DENG2_ASSERT(&changed == §or()); DENG2_UNUSED(changed); if(sector().map().hasLightGrid()) { sector().map().lightGrid().blockLightSourceChanged(thisPublic); } } #endif // __CLIENT__ }; SectorCluster::SectorCluster(Subspaces const &subspaces) : d(new Instance(this)) { d->subspaces.append(subspaces); foreach(ConvexSubspace *subspace, subspaces) { // Attribute the subspace to the cluster. subspace->setCluster(this); } // Observe changes to plane heights in "this" sector. d->observePlane(§or().floor()); d->observePlane(§or().ceiling()); #ifdef __CLIENT__ // Observe changes to sector lighting properties. sector().audienceForLightLevelChange += d; sector().audienceForLightColorChange += d; #endif } SectorCluster::~SectorCluster() {} bool SectorCluster::isInternalEdge(HEdge *hedge) // static { if(!hedge) return false; if(!hedge->hasFace() || !hedge->twin().hasFace()) return false; if(!hedge->face().hasMapElement() || hedge->face().mapElement().type() != DMU_SUBSPACE) return false; if(!hedge->twin().face().hasMapElement() || hedge->twin().face().mapElement().type() != DMU_SUBSPACE) return false; SectorCluster *frontCluster = hedge->face().mapElementAs().clusterPtr(); if(!frontCluster) return false; return frontCluster == hedge->twin().face().mapElementAs().clusterPtr(); } Sector &SectorCluster::sector() { return d->sector(); } Sector const &SectorCluster::sector() const { return const_cast(this)->sector(); } Plane const &SectorCluster::plane(int planeIndex) const { // Physical planes are never mapped. return sector().plane(planeIndex); } Plane &SectorCluster::plane(int planeIndex) { // Physical planes are never mapped. return sector().plane(planeIndex); } Plane &SectorCluster::visPlane(int planeIndex) { return const_cast(const_cast(this)->visPlane(planeIndex)); } Plane const &SectorCluster::visPlane(int planeIndex) const { if(planeIndex >= Sector::Floor && planeIndex <= Sector::Ceiling) { // Time to remap the planes? if(d->needRemapVisPlanes()) { d->remapVisPlanes(); } /// @todo Cache this result. SectorCluster const *mappedCluster = (planeIndex == Sector::Ceiling? d->mappedVisCeiling : d->mappedVisFloor); if(mappedCluster && mappedCluster != this) { return mappedCluster->visPlane(planeIndex); } } // Not mapped. return sector().plane(planeIndex); } AABoxd const &SectorCluster::aaBox() const { // If the cluster is comprised of a single subspace we can use the bounding // box of the subspace geometry directly. if(d->subspaces.count() == 1) { return d->subspaces.first()->poly().aaBox(); } // Time to determine bounds? if(d->aaBox.isNull()) { // Unite the geometry bounding boxes of all subspaces in the cluster. foreach(ConvexSubspace const *subspace, d->subspaces) { AABoxd const &leafAABox = subspace->poly().aaBox(); if(!d->aaBox.isNull()) { V2d_UniteBox((*d->aaBox).arvec2, leafAABox.arvec2); } else { d->aaBox.reset(new AABoxd(leafAABox)); } } } return *d->aaBox; } SectorCluster::Subspaces const &SectorCluster::subspaces() const { return d->subspaces; } #ifdef __CLIENT__ bool SectorCluster::hasWorldVolume(bool useSmoothedHeights) const { if(useSmoothedHeights) { return visCeiling().heightSmoothed() - visFloor().heightSmoothed() > 0; } else { return ceiling().height() - floor().height() > 0; } } coord_t SectorCluster::roughArea() const { AABoxd const &bounds = aaBox(); return (bounds.maxX - bounds.minX) * (bounds.maxY - bounds.minY); } void SectorCluster::markReverbDirty(bool yes) { d->needReverbUpdate = yes; } AudioEnvironmentFactors const &SectorCluster::reverb() const { // Perform any scheduled update now. if(d->needReverbUpdate) { d->updateReverb(); } return d->reverb; } void SectorCluster::markVisPlanesDirty() { d->maybeInvalidateMapping(Sector::Floor); d->maybeInvalidateMapping(Sector::Ceiling); } bool SectorCluster::hasSkyMaskedPlane() const { for(int i = 0; i < sector().planeCount(); ++i) { if(visPlane(i).surface().hasSkyMaskedMaterial()) return true; } return false; } SectorCluster::LightId SectorCluster::lightSourceId() const { /// @todo Need unique cluster ids. return LightId(sector().indexInMap()); } Vector3f SectorCluster::lightSourceColorf() const { if(Rend_SkyLightIsEnabled() && hasSkyMaskedPlane()) { return Rend_SkyLightColor(); } // A non-skylight sector (i.e., everything else!) // Return the sector's ambient light color. return sector().lightColor(); } float SectorCluster::lightSourceIntensity(Vector3d const &/*viewPoint*/) const { return sector().lightLevel(); } int SectorCluster::blockLightSourceZBias() { int const height = int(visCeiling().height() - visFloor().height()); bool hasSkyFloor = visFloor().surface().hasSkyMaskedMaterial(); bool hasSkyCeil = visCeiling().surface().hasSkyMaskedMaterial(); if(hasSkyFloor && !hasSkyCeil) { return -height / 6; } if(!hasSkyFloor && hasSkyCeil) { return height / 6; } if(height > 100) { return (height - 100) / 2; } return 0; } void SectorCluster::applyBiasDigest(BiasDigest &allChanges) { Instance::ShardGeometryMap::const_iterator it = d->shardGeomMap.constBegin(); while(it != d->shardGeomMap.constEnd()) { it.key()->biasTracker().applyChanges(allChanges); ++it; } } // Determine the number of bias illumination points needed for this geometry. // Presently we define a 1:1 mapping to geometry vertices. static int countIlluminationPoints(MapElement &mapElement, int group) { DENG2_UNUSED(group); // just assert switch(mapElement.type()) { case DMU_SUBSPACE: { ConvexSubspace &subspace = mapElement.as(); DENG2_ASSERT(group >= 0 && group < subspace.sector().planeCount()); // sanity check return subspace.fanVertexCount(); } case DMU_SEGMENT: DENG2_ASSERT(group >= 0 && group <= LineSide::Top); // sanity check return 4; default: throw Error("SectorCluster::countIlluminationPoints", "Invalid MapElement type"); } return 0; } Shard &SectorCluster::shard(MapElement &mapElement, int geomId) { Instance::GeometryData *gdata = d->geomData(mapElement, geomId, true /*create*/); if(gdata->shard.isNull()) { gdata->shard.reset(new Shard(countIlluminationPoints(mapElement, geomId), this)); } return *gdata->shard; } Shard *SectorCluster::findShard(MapElement &mapElement, int geomId) { if(Instance::GeometryData *gdata = d->geomData(mapElement, geomId)) { return gdata->shard.data(); } return nullptr; } /** * @todo This could be enhanced so that only the lights on the right side of the * surface are taken into consideration. */ bool SectorCluster::updateBiasContributors(Shard *shard) { if(Instance::GeometryData *gdata = d->geomDataForShard(shard)) { Map const &map = sector().map(); BiasTracker &tracker = shard->biasTracker(); tracker.clearContributors(); switch(gdata->mapElement->type()) { case DMU_SUBSPACE: { ConvexSubspace &subspace = gdata->mapElement->as(); Plane const &plane = visPlane(gdata->geomId); Surface const &surface = plane.surface(); Vector3d const surfacePoint(subspace.poly().center(), plane.heightSmoothed()); map.forAllBiasSources([&tracker, &subspace, &surface, &surfacePoint] (BiasSource &source) { // If the source is too weak we will ignore it completely. if(source.intensity() <= 0) return LoopContinue; Vector3d sourceToSurface = (source.origin() - surfacePoint).normalize(); coord_t distance = 0; // Calculate minimum 2D distance to the subspace. /// @todo This is probably too accurate an estimate. HEdge *baseNode = subspace.poly().hedge(); HEdge *node = baseNode; do { coord_t len = (Vector2d(source.origin()) - node->origin()).length(); if(node == baseNode || len < distance) distance = len; } while((node = &node->next()) != baseNode); if(sourceToSurface.dot(surface.normal()) < 0) return LoopContinue; tracker.addContributor(&source, source.evaluateIntensity() / de::max(distance, 1.0)); return LoopContinue; }); break; } case DMU_SEGMENT: { LineSideSegment &seg = gdata->mapElement->as(); Surface const &surface = seg.lineSide().middle(); Vector2d const &from = seg.hedge().origin(); Vector2d const &to = seg.hedge().twin().origin(); Vector2d const center = (from + to) / 2; map.forAllBiasSources([&tracker, &surface, &from, &to, ¢er] (BiasSource &source) { // If the source is too weak we will ignore it completely. if(source.intensity() <= 0) return LoopContinue; Vector3d sourceToSurface = (source.origin() - center).normalize(); // Calculate minimum 2D distance to the segment. coord_t distance = 0; for(int k = 0; k < 2; ++k) { coord_t len = (Vector2d(source.origin()) - (!k? from : to)).length(); if(k == 0 || len < distance) distance = len; } if(sourceToSurface.dot(surface.normal()) < 0) return LoopContinue; tracker.addContributor(&source, source.evaluateIntensity() / de::max(distance, 1.0)); return LoopContinue; }); break; } default: throw Error("SectorCluster::updateBiasContributors", "Invalid MapElement type"); } return true; } return false; } uint SectorCluster::biasLastChangeOnFrame() const { return sector().map().biasLastChangeOnFrame(); } #endif // __CLIENT__ // SectorClusterCirculator ----------------------------------------------------- SectorCluster *SectorClusterCirculator::getCluster(HEdge const &hedge) // static { if(!hedge.hasFace()) return nullptr; if(!hedge.face().hasMapElement()) return nullptr; if(hedge.face().mapElement().type() != DMU_SUBSPACE) return nullptr; return hedge.face().mapElementAs().clusterPtr(); } HEdge &SectorClusterCirculator::getNeighbor(HEdge const &hedge, ClockDirection direction, SectorCluster const *cluster) // static { HEdge *neighbor = &hedge.neighbor(direction); // Skip over interior edges. if(cluster) { while(neighbor->hasTwin() && cluster == getCluster(neighbor->twin())) { neighbor = &neighbor->twin().neighbor(direction); } } return *neighbor; } doomsday-stable-1.15.7/doomsday/client/src/world/huecircle.cpp0000664000175000017500000000576312641367670023671 0ustar jaakkojaakko/** @file huecircle.cpp HueCircle manipulator, for runtime map editing. * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "world/huecircle.h" #include "m_misc.h" // M_HSVToRGB(), remove me (use QColor) #include using namespace de; DENG2_PIMPL_NOREF(HueCircle) { Vector3f frontVec, sideVec, upVec; Instance() {} }; HueCircle::HueCircle() : d(new Instance()) {} Vector3d HueCircle::origin(Vector3d const &viewOrigin, double distance) const { return viewOrigin + d->frontVec * distance; } void HueCircle::setOrientation(Vector3f const &frontVec, Vector3f const &sideVec, Vector3f const &upVec) { d->frontVec = frontVec; d->sideVec = sideVec; d->upVec = upVec; } Vector3f HueCircle::colorAt(Vector3f const &viewFrontVec, float *angle, float *sat) const { float const minAngle = 0.1f; float const range = 0.19f; float viewDot = viewFrontVec.dot(d->frontVec); float saturation = (acos(viewDot) - minAngle) / range; saturation = de::clamp(0.f, saturation, 1.f); if(sat) *sat = saturation; if(saturation == 0 || viewDot > .999f) { if(angle) *angle = 0; if(sat) *sat = 0; float colorV1[3]; M_HSVToRGB(colorV1, 0, 0, 1); return Vector3f(colorV1); } // Calculate the angle to the viewer by projecting the current viewfront to the // hue circle plane. Project onto the normal, subtract and then normalize. float scale = viewFrontVec.dot(d->frontVec) / d->frontVec.dot(d->frontVec); Vector3f proj = (viewFrontVec - (d->frontVec * scale)).normalize(); float hue = acos(proj.dot(d->upVec)); if(proj.dot(d->sideVec) > 0) hue = float(2 * de::PI) - hue; hue /= float(2 * de::PI); hue += 0.25; if(angle) *angle = hue; float colorV1[3]; M_HSVToRGB(colorV1, hue, saturation, 1); return Vector3f(colorV1); } Vector3f HueCircle::offset(double angle) const { return Vector3f(cos(angle) * d->sideVec.x + sin(angle) * d->upVec.x, sin(angle) * d->upVec.y, cos(angle) * d->sideVec.z + sin(angle) * d->upVec.z); } doomsday-stable-1.15.7/doomsday/client/src/world/p_mobj.cpp0000664000175000017500000007023312641367670023166 0ustar jaakkojaakko/** @file p_mobj.cpp World map objects. * * Various routines for moving mobjs, collision and Z checking. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 1993-1996 by id Software, Inc. * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "world/p_object.h" #include "de_console.h" #include "de_system.h" #include "de_network.h" #include "de_play.h" #include "de_resource.h" #include "de_misc.h" #include "de_audio.h" #include "def_main.h" #include "world/worldsystem.h" // validCount #include "world/thinkers.h" #include "BspLeaf" #include "ConvexSubspace" #include "SectorCluster" #ifdef __CLIENT__ # include "Lumobj" # include "render/viewports.h" # include "render/rend_main.h" # include "render/rend_model.h" # include "render/rend_halo.h" # include "render/billboard.h" # include "gl/gl_tex.h" #endif #include #include #include using namespace de; static mobj_t *unusedMobjs; /* * Console variables: */ int useSRVO = 2; ///< @c 1= models only, @c 2= sprites + models int useSRVOAngle = 1; #ifdef __CLIENT__ static byte mobjAutoLights = true; #endif /** * Called during map loading. */ void P_InitUnusedMobjList() { // Any zone memory allocated for the mobjs will have already been purged. unusedMobjs = 0; } /** * All mobjs must be allocated through this routine. Part of the public API. */ mobj_t *P_MobjCreate(thinkfunc_t function, Vector3d const &origin, angle_t angle, coord_t radius, coord_t height, int ddflags) { if(!function) App_Error("P_MobjCreate: Think function invalid, cannot create mobj."); #ifdef _DEBUG if(isClient) { LOG_VERBOSE("P_MobjCreate: Client creating mobj at %s") << origin.asText(); } #endif // Do we have any unused mobjs we can reuse? mobj_t *mo; if(unusedMobjs) { mo = unusedMobjs; unusedMobjs = unusedMobjs->sNext; } else { // No, we need to allocate another. mo = MobjThinker(Thinker::AllocateMemoryZone).take(); } V3d_Set(mo->origin, origin.x, origin.y, origin.z); mo->angle = angle; mo->visAngle = mo->angle >> 16; // "angle-servo"; smooth actor turning. mo->radius = radius; mo->height = height; mo->ddFlags = ddflags; mo->lumIdx = -1; mo->thinker.function = function; Mobj_Map(*mo).thinkers().add(mo->thinker); return mo; } /** * All mobjs must be destroyed through this routine. Part of the public API. * * @note Does not actually destroy the mobj. Instead, mobj is marked as * awaiting removal (which occurs when its turn for thinking comes around). */ #undef Mobj_Destroy DENG_EXTERN_C void Mobj_Destroy(mobj_t *mo) { #ifdef _DEBUG if(mo->ddFlags & DDMF_MISSILE) { LOG_AS("Mobj_Destroy"); LOG_MAP_XVERBOSE("Destroying missile %i") << mo->thinker.id; } #endif // Unlink from sector and block lists. Mobj_Unlink(mo); S_StopSound(0, mo); Mobj_Map(*mo).thinkers().remove(reinterpret_cast(*mo)); } /** * Called when a mobj is actually removed (when it's thinking turn comes around). * The mobj is moved to the unused list to be reused later. */ void P_MobjRecycle(mobj_t* mo) { // Release the private data. MobjThinker::zap(*mo); // The sector next link is used as the unused mobj list links. mo->sNext = unusedMobjs; unusedMobjs = mo; } dd_bool Mobj_IsSectorLinked(mobj_t *mo) { return mo != 0 && mo->_bspLeaf != 0 && mo->sPrev != 0; } #undef Mobj_SetState DENG_EXTERN_C void Mobj_SetState(mobj_t *mobj, int statenum) { if(!mobj) return; state_t const *oldState = mobj->state; DENG_ASSERT(statenum >= 0 && statenum < defs.states.size()); mobj->state = &runtimeDefs.states[statenum]; mobj->tics = mobj->state->tics; mobj->sprite = mobj->state->sprite; mobj->frame = mobj->state->frame; if(!(mobj->ddFlags & DDMF_REMOTE)) { if(defs.states[statenum].execute) Con_Execute(CMDS_SCRIPT, defs.states[statenum].execute, true, false); } // Notify private data about the changed state. if(mobj->thinker.d == nullptr) Thinker_InitPrivateData(&mobj->thinker); if(MobjThinkerData *data = THINKER_DATA_MAYBE(mobj->thinker, MobjThinkerData)) { data->stateChanged(oldState); } } Vector3d Mobj_Origin(mobj_t const &mobj) { return Vector3d(mobj.origin); } Vector3d Mobj_Center(mobj_t &mobj) { return Vector3d(mobj.origin[0], mobj.origin[1], mobj.origin[2] + mobj.height / 2); } dd_bool Mobj_SetOrigin(struct mobj_s *mo, coord_t x, coord_t y, coord_t z) { if(!gx.MobjTryMoveXYZ) { return false; } return gx.MobjTryMoveXYZ(mo, x, y, z); } #undef Mobj_OriginSmoothed DENG_EXTERN_C void Mobj_OriginSmoothed(mobj_t *mo, coord_t origin[3]) { if(!origin) return; V3d_Set(origin, 0, 0, 0); if(!mo) return; V3d_Copy(origin, mo->origin); // Apply a Short Range Visual Offset? if(useSRVO && mo->state && mo->tics >= 0) { double const mul = mo->tics / float( mo->state->tics ); vec3d_t srvo; V3d_Copy(srvo, mo->srvo); V3d_Scale(srvo, mul); V3d_Sum(origin, origin, srvo); } #ifdef __CLIENT__ if(mo->dPlayer) { /// @todo What about splitscreen? We have smoothed origins for all local players. if(P_GetDDPlayerIdx(mo->dPlayer) == consolePlayer && // $voodoodolls: Must be a real player to use the smoothed origin. mo->dPlayer->mo == mo) { viewdata_t const *vd = R_ViewData(consolePlayer); V3d_Set(origin, vd->current.origin.x, vd->current.origin.y, vd->current.origin.z); } // The client may have a Smoother for this object. else if(isClient) { Smoother_Evaluate(clients[P_GetDDPlayerIdx(mo->dPlayer)].smoother, origin); } } #endif } de::Map &Mobj_Map(mobj_t const &mobj) { return Thinker_Map(mobj.thinker); } bool Mobj_IsLinked(mobj_t const &mobj) { return mobj._bspLeaf != 0; } BspLeaf &Mobj_BspLeafAtOrigin(mobj_t const &mobj) { if(Mobj_IsLinked(mobj)) { return *mobj._bspLeaf; } throw Error("Mobj_BspLeafAtOrigin", "Mobj is not yet linked"); } bool Mobj_HasSubspace(mobj_t const &mobj) { if(!Mobj_IsLinked(mobj)) return false; return Mobj_BspLeafAtOrigin(mobj).hasSubspace(); } SectorCluster &Mobj_Cluster(mobj_t const &mobj) { return Mobj_BspLeafAtOrigin(mobj).subspace().cluster(); } SectorCluster *Mobj_ClusterPtr(mobj_t const &mobj) { return Mobj_HasSubspace(mobj)? &Mobj_Cluster(mobj) : 0; } #undef Mobj_Sector DENG_EXTERN_C Sector *Mobj_Sector(mobj_t const *mob) { if(!mob || !Mobj_IsLinked(*mob)) return nullptr; return Mobj_BspLeafAtOrigin(*mob).sectorPtr(); } void Mobj_SpawnParticleGen(mobj_t *source, ded_ptcgen_t const *def) { #ifdef __CLIENT__ DENG2_ASSERT(def != 0 && source != 0); //if(!useParticles) return; Generator *gen = Mobj_Map(*source).newGenerator(); if(!gen) return; /*LOG_INFO("SpawnPtcGen: %s/%i (src:%s typ:%s mo:%p)") << def->state << (def - defs.ptcgens) << defs.states[source->state-states].id << defs.mobjs[source->type].id << source;*/ // Initialize the particle generator. gen->count = def->particles; // Size of source sector might determine count. if(def->flags & Generator::ScaledRate) { gen->spawnRateMultiplier = Mobj_BspLeafAtOrigin(*source).sectorPtr()->roughArea() / (128 * 128); } else { gen->spawnRateMultiplier = 1; } gen->configureFromDef(def); gen->source = source; gen->srcid = source->thinker.id; // Is there a need to pre-simulate? gen->presimulate(def->preSim); #else DENG2_UNUSED2(source, def); #endif } #undef Mobj_SpawnDamageParticleGen DENG_EXTERN_C void Mobj_SpawnDamageParticleGen(mobj_t *mo, mobj_t *inflictor, int amount) { #ifdef __CLIENT__ if(!mo || !inflictor || amount <= 0) return; // Are particles allowed? //if(!useParticles) return; ded_ptcgen_t const *def = Def_GetDamageGenerator(mo->type); if(def) { Generator *gen = Mobj_Map(*mo).newGenerator(); if(!gen) return; // No more generators. gen->count = def->particles; gen->configureFromDef(def); gen->setUntriggered(); gen->spawnRateMultiplier = de::max(amount, 1); // Calculate appropriate center coordinates. gen->originAtSpawn[VX] += FLT2FIX(mo->origin[VX]); gen->originAtSpawn[VY] += FLT2FIX(mo->origin[VY]); gen->originAtSpawn[VZ] += FLT2FIX(mo->origin[VZ] + mo->height / 2); // Calculate launch vector. vec3f_t vecDelta; V3f_Set(vecDelta, inflictor->origin[VX] - mo->origin[VX], inflictor->origin[VY] - mo->origin[VY], (inflictor->origin[VZ] - inflictor->height / 2) - (mo->origin[VZ] + mo->height / 2)); vec3f_t vector; V3f_SetFixed(vector, gen->vector[VX], gen->vector[VY], gen->vector[VZ]); V3f_Sum(vector, vector, vecDelta); V3f_Normalize(vector); gen->vector[VX] = FLT2FIX(vector[VX]); gen->vector[VY] = FLT2FIX(vector[VY]); gen->vector[VZ] = FLT2FIX(vector[VZ]); // Is there a need to pre-simulate? gen->presimulate(def->preSim); } #else DENG2_UNUSED3(mo, inflictor, amount); #endif } #ifdef __CLIENT__ dd_bool Mobj_OriginBehindVisPlane(mobj_t *mo) { if(!mo || !Mobj_HasSubspace(*mo)) return false; SectorCluster &cluster = Mobj_Cluster(*mo); if(&cluster.floor() != &cluster.visFloor() && mo->origin[VZ] < cluster.visFloor().heightSmoothed()) return true; if(&cluster.ceiling() != &cluster.visCeiling() && mo->origin[VZ] > cluster.visCeiling().heightSmoothed()) return true; return false; } void Mobj_UnlinkLumobjs(mobj_t *mo) { if(!mo) return; mo->lumIdx = Lumobj::NoIndex; } static ded_light_t *lightDefByMobjState(state_t const *state) { if(state) { return runtimeDefs.stateInfo[runtimeDefs.states.indexOf(state)].light; } return 0; } static inline Texture *lightmap(de::Uri const *textureUri) { if(!textureUri) return nullptr; return App_ResourceSystem().texture("Lightmaps", *textureUri); } void Mobj_GenerateLumobjs(mobj_t *mo) { if(!mo) return; Mobj_UnlinkLumobjs(mo); if(!Mobj_HasSubspace(*mo)) return; SectorCluster &cluster = Mobj_Cluster(*mo); if(!(((mo->state && (mo->state->flags & STF_FULLBRIGHT)) && !(mo->ddFlags & DDMF_DONTDRAW)) || (mo->ddFlags & DDMF_ALWAYSLIT))) { return; } // Are the automatically calculated light values for fullbright sprite frames in use? if(mo->state && (!mobjAutoLights || (mo->state->flags & STF_NOAUTOLIGHT)) && !runtimeDefs.stateInfo[runtimeDefs.states.indexOf(mo->state)].light) { return; } // If the mobj's origin is outside the BSP leaf it is linked within, then // this means it is outside the playable map (and no light should be emitted). /// @todo Optimize: Mobj_Link() should do this and flag the mobj accordingly. if(!Mobj_BspLeafAtOrigin(*mo).subspace().contains(mo->origin)) { return; } Sprite *sprite = Mobj_Sprite(*mo); if(!sprite) return; // Always use the front rotation when determining light properties. if(!sprite->hasViewAngle(0)) return; SpriteViewAngle const &sprViewAngle = sprite->viewAngle(0); DENG2_ASSERT(sprViewAngle.material); MaterialAnimator &matAnimator = sprViewAngle.material->getAnimator(Rend_SpriteMaterialSpec()); // Ensure we've up to date info about the material. matAnimator.prepare(); TextureVariant *tex = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; if(!tex) return; // Unloadable texture? Vector2i const &texOrigin = tex->base().origin(); // Will the visual be allowed to go inside the floor? /// @todo Handle this as occlusion so that the halo fades smoothly. coord_t impacted = mo->origin[VZ] + -texOrigin.y - matAnimator.dimensions().y - cluster.visFloor().heightSmoothed(); // If the floor is a visual plane then no light should be emitted. if(impacted < 0 && &cluster.visFloor() != &cluster.floor()) return; // Attempt to generate luminous object from the sprite. QScopedPointer lum(sprite->generateLumobj()); if(lum.isNull()) return; // A light definition may override the (auto-calculated) defaults. if(ded_light_t *def = lightDefByMobjState(mo->state)) { if(!de::fequal(def->size, 0)) { lum->setRadius(de::max(def->size, 32.f / (40 * lum->radiusFactor()))); } if(!de::fequal(def->offset[1], 0)) { lum->setZOffset(-texOrigin.y - def->offset[1]); } if(Vector3f(def->color) != Vector3f(0, 0, 0)) { lum->setColor(def->color); } lum->setLightmap(Lumobj::Side, lightmap(def->sides)) .setLightmap(Lumobj::Down, lightmap(def->down)) .setLightmap(Lumobj::Up, lightmap(def->up)); } // Translate to the mobj's origin in map space. lum->move(mo->origin); // Does the mobj need a Z origin offset? coord_t zOffset = -mo->floorClip - Mobj_BobOffset(*mo); if(!(mo->ddFlags & DDMF_NOFITBOTTOM) && impacted < 0) { // Raise the light out of the impacted surface. zOffset -= impacted; } lum->setZOffset(lum->zOffset() + zOffset); // Insert a copy of the temporary lumobj in the map and remember it's unique // index in the mobj (this'll allow a halo to be rendered). mo->lumIdx = cluster.sector().map().addLumobj(*lum).indexInMap(); } void Mobj_AnimateHaloOcclussion(mobj_t &mob) { for(dint i = 0; i < DDMAXPLAYERS; ++i) { dbyte *haloFactor = &mob.haloFactors[i]; // Set the high bit of halofactor if the light is clipped. This will // make P_Ticker diminish the factor to zero. Take the first step here // and now, though. if(mob.lumIdx == Lumobj::NoIndex || R_ViewerLumobjIsClipped(mob.lumIdx)) { if(*haloFactor & 0x80) { dint f = (*haloFactor & 0x7f); // - haloOccludeSpeed; if(f < 0) f = 0; *haloFactor = f; } } else { if(!(*haloFactor & 0x80)) { dint f = (*haloFactor & 0x7f); // + haloOccludeSpeed; if(f > 127) f = 127; *haloFactor = 0x80 | f; } } // Handle halofactor. dint f = *haloFactor & 0x7f; if(*haloFactor & 0x80) { // Going up. f += ::haloOccludeSpeed; if(f > 127) f = 127; } else { // Going down. f -= ::haloOccludeSpeed; if(f < 0) f = 0; } *haloFactor &= ~0x7f; *haloFactor |= f; } } dfloat Mobj_ShadowStrength(mobj_t const &mob) { dfloat const minSpriteAlphaLimit = .1f; dfloat ambientLightLevel, strength = .65f; ///< Default strength factor. // Is this mobj in a valid state for shadow casting? if(!mob.state) return 0; if(!Mobj_HasSubspace(mob)) return 0; // Should this mobj even have a shadow? if((mob.state->flags & STF_FULLBRIGHT) || (mob.ddFlags & DDMF_DONTDRAW) || (mob.ddFlags & DDMF_ALWAYSLIT)) return 0; SectorCluster &cluster = Mobj_Cluster(mob); // Sample the ambient light level at the mobj's position. Map &map = cluster.sector().map(); if(useBias && map.hasLightGrid()) { // Evaluate in the light grid. ambientLightLevel = map.lightGrid().evaluateIntensity(mob.origin); } else { ambientLightLevel = cluster.lightSourceIntensity(); } Rend_ApplyLightAdaptation(ambientLightLevel); // Sprites have their own shadow strength factor. if(!useModels || !Mobj_ModelDef(mob)) { if(Sprite *sprite = Mobj_Sprite(mob)) { if(sprite->hasViewAngle(0)) { SpriteViewAngle const &sprViewAngle = sprite->viewAngle(0); DENG2_ASSERT(sprViewAngle.material); MaterialAnimator &matAnimator = sprViewAngle.material->getAnimator(Rend_SpriteMaterialSpec()); // Ensure we've up to date info about the material. matAnimator.prepare(); TextureVariant const *texture = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; DENG2_ASSERT(texture); if(texture) { auto const *aa = (averagealpha_analysis_t const *)texture->base().analysisDataPointer(Texture::AverageAlphaAnalysis); DENG2_ASSERT(aa); // We use an average which factors in the coverage ratio // of alpha:non-alpha pixels. /// @todo Constant weights could stand some tweaking... float weightedSpriteAlpha = aa->alpha * (0.4f + (1 - aa->coverage) * 0.6f); // Almost entirely translucent sprite? => no shadow. if(weightedSpriteAlpha < minSpriteAlphaLimit) return 0; // Apply this factor. strength *= de::min(1.f, .2f + weightedSpriteAlpha); } } } } // Factor in Mobj alpha. strength *= Mobj_Alpha(mob); /// @note This equation is the same as that used for fakeradio. return (0.6f - ambientLightLevel * 0.4f) * strength; } Sprite *Mobj_Sprite(mobj_t const &mo) { return App_ResourceSystem().spritePtr(mo.sprite, mo.frame); } ModelDef *Mobj_ModelDef(mobj_t const &mo, ModelDef **retNextModef, float *retInter) { ResourceSystem &resSys = App_ResourceSystem(); // By default there are no models. if(retNextModef) *retNextModef = 0; if(retInter) *retInter = -1; // On the client it is possible that we don't know the mobj's state. if(!mo.state) return 0; state_t &st = *mo.state; ModelDef *modef = resSys.modelDefForState(runtimeDefs.states.indexOf(&st), mo.selector); if(!modef) return 0; // No model available. float interp = -1; // World time animation? bool worldTime = false; if(modef->flags & MFF_WORLD_TIME_ANIM) { float duration = modef->interRange[0]; float offset = modef->interRange[1]; // Validate/modify the values. if(duration == 0) duration = 1; if(offset == -1) { offset = M_CycleIntoRange(MOBJ_TO_ID(&mo), duration); } interp = M_CycleIntoRange(App_WorldSystem().time() / duration + offset, 1); worldTime = true; } else { // Calculate the currently applicable intermark. interp = 1.0f - (mo.tics - frameTimePos) / float( st.tics ); } /*#if _DEBUG if(mo.dPlayer) { qDebug() << "itp:" << interp << " mot:" << mo.tics << " stt:" << st.tics; } #endif*/ // First find the modef for the interpoint. Intermark is 'stronger' than interrange. // Scan interlinks. while(modef->interNext && modef->interNext->interMark <= interp) { modef = modef->interNext; } if(!worldTime) { // Scale to the modeldef's interpolation range. interp = modef->interRange[0] + interp * (modef->interRange[1] - modef->interRange[0]); } // What would be the next model? Check interlinks first. if(retNextModef) { if(modef->interNext) { *retNextModef = modef->interNext; } else if(worldTime) { *retNextModef = resSys.modelDefForState(runtimeDefs.states.indexOf(&st), mo.selector); } else if(st.nextState > 0) // Check next state. { // Find the appropriate state based on interrange. state_t *it = &runtimeDefs.states[st.nextState]; bool foundNext = false; if(modef->interRange[1] < 1) { // Current modef doesn't interpolate to the end, find the proper destination // modef (it isn't just the next one). Scan the states that follow (and // interlinks of each). bool stopScan = false; int max = 20; // Let's not be here forever... while(!stopScan) { if(!((!resSys.modelDefForState(runtimeDefs.states.indexOf(it)) || resSys.modelDefForState(runtimeDefs.states.indexOf(it), mo.selector)->interRange[0] > 0) && it->nextState > 0)) { stopScan = true; } else { // Scan interlinks, then go to the next state. ModelDef *mdit = resSys.modelDefForState(runtimeDefs.states.indexOf(it), mo.selector); if(mdit && mdit->interNext) { forever { mdit = mdit->interNext; if(mdit) { if(mdit->interRange[0] <= 0) // A new beginning? { *retNextModef = mdit; foundNext = true; } } if(!mdit || foundNext) { break; } } } if(foundNext) { stopScan = true; } else { it = &runtimeDefs.states[it->nextState]; } } if(max-- <= 0) stopScan = true; } // @todo What about max == -1? What should 'it' be then? } if(!foundNext) { *retNextModef = resSys.modelDefForState(runtimeDefs.states.indexOf(it), mo.selector); } } } if(retInter) *retInter = interp; return modef; } #endif // __CLIENT__ #undef Mobj_AngleSmoothed DENG_EXTERN_C angle_t Mobj_AngleSmoothed(mobj_t* mo) { if(!mo) return 0; #ifdef __CLIENT__ if(mo->dPlayer) { /// @todo What about splitscreen? We have smoothed angles for all local players. if(P_GetDDPlayerIdx(mo->dPlayer) == consolePlayer && // $voodoodolls: Must be a real player to use the smoothed angle. mo->dPlayer->mo == mo) { const viewdata_t* vd = R_ViewData(consolePlayer); return vd->current.angle(); } } // Apply a Short Range Visual Offset? if(useSRVOAngle && !netGame && !playback) { return mo->visAngle << 16; } #endif return mo->angle; } coord_t Mobj_ApproxPointDistance(mobj_t* mo, coord_t const* point) { if(!mo || !point) return 0; return M_ApproxDistance(point[VZ] - mo->origin[VZ], M_ApproxDistance(point[VX] - mo->origin[VX], point[VY] - mo->origin[VY])); } coord_t Mobj_BobOffset(mobj_t const &mob) { if(mob.ddFlags & DDMF_BOB) { return (sin(MOBJ_TO_ID(&mob) + App_WorldSystem().time() / 1.8286 * 2 * PI) * 8); } return 0; } dfloat Mobj_Alpha(mobj_t const &mob) { dfloat alpha = (mob.ddFlags & DDMF_BRIGHTSHADOW)? .80f : (mob.ddFlags & DDMF_SHADOW )? .33f : (mob.ddFlags & DDMF_ALTSHADOW )? .66f : 1; // The three highest bits of the selector are used for alpha. // 0 = opaque (alpha -1) // 1 = 1/8 transparent // 4 = 1/2 transparent // 7 = 7/8 transparent dint selAlpha = mob.selector >> DDMOBJ_SELECTOR_SHIFT; if(selAlpha & 0xe0) { alpha *= 1 - ((selAlpha & 0xe0) >> 5) / 8.0f; } else if(mob.translucency) { alpha *= 1 - mob.translucency * reciprocal255; } return alpha; } coord_t Mobj_Radius(mobj_t const &mobj) { return mobj.radius; } #ifdef __CLIENT__ coord_t Mobj_ShadowRadius(mobj_t const &mobj) { if(useModels) { if(ModelDef *modef = Mobj_ModelDef(mobj)) { if(modef->shadowRadius > 0) { return modef->shadowRadius; } } } // Fall back to the visual radius. return Mobj_VisualRadius(mobj); } #endif coord_t Mobj_VisualRadius(mobj_t const &mobj) { #ifdef __CLIENT__ // Is a model in effect? if(useModels) { if(ModelDef *modef = Mobj_ModelDef(mobj)) { return modef->visualRadius; } } // Is a sprite in effect? if(Sprite *sprite = Mobj_Sprite(mobj)) { return sprite->visualRadius(); } #endif // Use the physical radius. return Mobj_Radius(mobj); } AABoxd Mobj_AABox(mobj_t const &mobj) { Vector2d const origin = Mobj_Origin(mobj); ddouble const radius = Mobj_Radius(mobj); return AABoxd(origin.x - radius, origin.y - radius, origin.x + radius, origin.y + radius); } D_CMD(InspectMobj) { DENG2_UNUSED(src); if(argc != 2) { LOG_SCR_NOTE("Usage: %s (mobj-id)") << argv[0]; return true; } // Get the ID. thid_t id = strtol(argv[1], NULL, 10); // Find the mobj. mobj_t *mo = App_WorldSystem().map().thinkers().mobjById(id); if(!mo) { LOG_MAP_ERROR("Mobj with id %i not found") << id; return false; } char const *moType = "Mobj"; #ifdef __CLIENT__ ClientMobjThinkerData::RemoteSync *info = ClMobj_GetInfo(mo); if(info) moType = "CLMOBJ"; #endif LOG_MAP_MSG("%s %i [%p] State:%s (%i)") << moType << id << mo << Def_GetStateName(mo->state) << runtimeDefs.states.indexOf(mo->state); LOG_MAP_MSG("Type:%s (%i) Info:[%p] %s") << Def_GetMobjName(mo->type) << mo->type << mo->info << (mo->info? QString(" (%1)").arg(runtimeDefs.mobjInfo.indexOf(mo->info)) : ""); LOG_MAP_MSG("Tics:%i ddFlags:%08x") << mo->tics << mo->ddFlags; #ifdef __CLIENT__ if(info) { LOG_MAP_MSG("Cltime:%i (now:%i) Flags:%04x") << info->time << Timer_RealMilliseconds() << info->flags; } #endif LOG_MAP_MSG("Flags:%08x Flags2:%08x Flags3:%08x") << mo->flags << mo->flags2 << mo->flags3; LOG_MAP_MSG("Height:%f Radius:%f") << mo->height << mo->radius; LOG_MAP_MSG("Angle:%x Pos:%s Mom:%s") << mo->angle << Vector3d(mo->origin).asText() << Vector3d(mo->mom).asText(); LOG_MAP_MSG("FloorZ:%f CeilingZ:%f") << mo->floorZ << mo->ceilingZ; if(SectorCluster *cluster = Mobj_ClusterPtr(*mo)) { LOG_MAP_MSG("Sector:%i (FloorZ:%f CeilingZ:%f)") << cluster->sector().indexInMap() << cluster->floor().height() << cluster->ceiling().height(); } if(mo->onMobj) { LOG_MAP_MSG("onMobj:%i") << mo->onMobj->thinker.id; } return true; } void Mobj_ConsoleRegister() { C_CMD("inspectmobj", "i", InspectMobj); #ifdef __CLIENT__ C_VAR_BYTE("rend-mobj-light-auto", &mobjAutoLights, 0, 0, 1); #endif } doomsday-stable-1.15.7/doomsday/client/src/world/clientmobjthinkerdata.cpp0000664000175000017500000001362412641367670026266 0ustar jaakkojaakko/** @file clientmobjthinkerdata.cpp Private client-side data for mobjs. * * @authors Copyright (c) 2014 Jaakko Keränen * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "world/clientmobjthinkerdata.h" #include "render/modelrenderer.h" #include "render/mobjanimator.h" #include "world/generator.h" #include "clientapp.h" #include "dd_loop.h" #include "def_main.h" #include using namespace de; namespace internal { enum Flag { Initialized = 0x1 ///< Thinker data has been initialized. }; Q_DECLARE_FLAGS(Flags, Flag) Q_DECLARE_OPERATORS_FOR_FLAGS(Flags) } using namespace ::internal; DENG2_PIMPL(ClientMobjThinkerData) { Flags flags; std::unique_ptr sync; std::unique_ptr animator; Matrix4f modelMatrix; Instance(Public *i) : Base(i) {} Instance(Public *i, Instance const &other) : Base(i) { if(other.sync) { sync.reset(new RemoteSync(*other.sync)); } } String thingName() const { return Def_GetMobjName(self.mobj()->type); } String stateName() const { return Def_GetStateName(self.mobj()->state); } String modelId() const { return String("model.thing.%1").arg(thingName().toLower()); } static ModelBank &modelBank() { return ClientApp::renderSystem().modelRenderer().bank(); } void deinitModel() { animator.reset(); } /** * Initializes the client-specific mobj data. This is performed once, during the * first time the object thinks. */ void initOnce() { // Initialization is only done once. if(flags & Initialized) return; flags |= Initialized; // Check for an available model asset. if(modelBank().has(modelId())) { // Prepare the animation state of the model. ModelBank::ModelWithData loaded = modelBank().modelAndData(modelId()); ModelDrawable &model = *loaded.first; animator.reset(new MobjAnimator(modelId(), model)); // The basic transformation of the model. modelMatrix = loaded.second->as().transformation; Vector3f dims = modelMatrix * model.dimensions(); // Scale to thing height. // TODO: This should be optional (but the default behavior). modelMatrix = Matrix4f::scale(self.mobj()->height / dims.y) * modelMatrix; } } /** * Checks if there are any animations defined to start in the current state. All * animation sequences associated with the state are checked. A sequence may specify * a less than 1.0 probability for starting. The sequence may be identified either by * name ("walk") or index (for example, "#3"). */ void triggerStateAnimations() { if(animator) { animator->triggerByState(stateName()); } } /** * Checks the motion of the object and triggers suitable animations (for standing, * walking, or running). These animations are defined separately from the state * based animations. */ void triggerMovementAnimations() { if(!animator) return; } void advanceAnimations(TimeDelta const &delta) { if(animator) { animator->advanceTime(delta); } } void triggerParticleGenerators(bool justSpawned) { // Check for a ptcgen trigger. for(ded_ptcgen_t *pg = runtimeDefs.stateInfo[self.stateIndex()].ptcGens; pg; pg = pg->stateNext) { if(!(pg->flags & Generator::SpawnOnly) || justSpawned) { // We are allowed to spawn the generator. Mobj_SpawnParticleGen(self.mobj(), pg); } } } }; ClientMobjThinkerData::ClientMobjThinkerData() : d(new Instance(this)) {} ClientMobjThinkerData::ClientMobjThinkerData(ClientMobjThinkerData const &other) : MobjThinkerData(other) , d(new Instance(this, *other.d)) {} void ClientMobjThinkerData::think() { d->initOnce(); d->triggerMovementAnimations(); d->advanceAnimations(SECONDSPERTIC); // mobjs think only on sharp ticks } Thinker::IData *ClientMobjThinkerData::duplicate() const { return new ClientMobjThinkerData(*this); } int ClientMobjThinkerData::stateIndex() const { return runtimeDefs.states.indexOf(mobj()->state); } bool ClientMobjThinkerData::hasRemoteSync() const { return bool(d->sync); } ClientMobjThinkerData::RemoteSync &ClientMobjThinkerData::remoteSync() { if(!hasRemoteSync()) { d->sync.reset(new RemoteSync); } return *d->sync; } ModelDrawable::Animator *ClientMobjThinkerData::animator() { return d->animator.get(); } ModelDrawable::Animator const *ClientMobjThinkerData::animator() const { return d->animator.get(); } Matrix4f const &ClientMobjThinkerData::modelTransformation() const { return d->modelMatrix; } void ClientMobjThinkerData::stateChanged(state_t const *previousState) { MobjThinkerData::stateChanged(previousState); bool const justSpawned = !previousState; d->initOnce(); d->triggerStateAnimations(); d->triggerParticleGenerators(justSpawned); } doomsday-stable-1.15.7/doomsday/client/src/world/convexsubspace.cpp0000664000175000017500000003271112641367670024747 0ustar jaakkojaakko/** @file convexsubspace.cpp World map convex subspace. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/convexsubspace.h" #include #include #include #include "BspLeaf" #include "Face" #include "Polyobj" #include "SectorCluster" #include "Surface" using namespace de; #ifdef __CLIENT__ /// Compute the area of a triangle defined by three 2D point vectors. ddouble triangleArea(Vector2d const &v1, Vector2d const &v2, Vector2d const &v3) { Vector2d a = v2 - v1; Vector2d b = v3 - v1; return (a.x * b.y - b.x * a.y) / 2; } #endif // __CLIENT__ DENG2_PIMPL(ConvexSubspace) { Face *poly = nullptr; ///< Convex polygon geometry (not owned). typedef QSet Meshes; Meshes extraMeshes; ///< Additional meshes (owned). typedef QSet Polyobjs; Polyobjs polyobjs; ///< Linked polyobjs (not owned). SectorCluster *cluster = nullptr; ///< Attributed cluster (if any, not owned). BspLeaf *bspLeaf = nullptr; ///< Attributed BSP leaf (if any, not owned). #ifdef __CLIENT__ Vector2d worldGridOffset; ///< For aligning the materials to the map space grid. typedef QSet Lumobjs; Lumobjs lumobjs; ///< Linked lumobjs (not owned). typedef QSet ShadowLines; ShadowLines shadowLines; ///< Linked map lines for fake radio shadowing. HEdge *fanBase = nullptr; ///< Trifan base Half-edge (otherwise the center point is used). bool needUpdateFanBase = true; ///< @c true= need to rechoose a fan base half-edge. AudioEnvironmentData aenv; ///< Cached audio environment characteristics. int lastSpriteProjectFrame = 0; ///< Frame number of last R_AddSprites. #endif int validCount = 0; ///< Used to prevent repeated processing. Instance(Public *i) : Base(i) {} ~Instance() { qDeleteAll(extraMeshes); } #ifdef __CLIENT__ /** * Determine the half-edge whose vertex is suitable for use as the center point * of a trifan primitive. * * Note that we do not want any overlapping or zero-area (degenerate) triangles. * * @par Algorithm *
For each vertex
     *    For each triangle
     *        if area is not greater than minimum bound, move to next vertex
     *    Vertex is suitable
     * 
* * If a vertex exists which results in no zero-area triangles it is suitable for * use as the center of our trifan. If a suitable vertex is not found then the * center of BSP leaf should be selected instead (it will always be valid as * BSP leafs are convex). */ void chooseFanBase() { #define MIN_TRIANGLE_EPSILON (0.1) ///< Area HEdge *firstNode = self.poly().hedge(); fanBase = firstNode; if(self.poly().hedgeCount() > 3) { // Splines with higher vertex counts demand checking. Vertex const *base, *a, *b; // Search for a good base. do { HEdge *other = firstNode; base = &fanBase->vertex(); do { // Test this triangle? if(!(fanBase != firstNode && (other == fanBase || other == &fanBase->prev()))) { a = &other->vertex(); b = &other->next().vertex(); if(de::abs(triangleArea(base->origin(), a->origin(), b->origin())) <= MIN_TRIANGLE_EPSILON) { // No good. We'll move on to the next vertex. base = 0; } } // On to the next triangle. } while(base && (other = &other->next()) != firstNode); if(!base) { // No good. Select the next vertex and start over. fanBase = &fanBase->next(); } } while(!base && fanBase != firstNode); // Did we find something suitable? if(!base) // No. { fanBase = 0; } } //else Implicitly suitable (or completely degenerate...). needUpdateFanBase = false; #undef MIN_TRIANGLE_EPSILON } #endif // __CLIENT__ }; ConvexSubspace::ConvexSubspace(Face &convexPolygon, BspLeaf *bspLeaf) : MapElement(DMU_SUBSPACE) , d(new Instance(this)) { d->poly = &convexPolygon; #ifdef __CLIENT__ // Determine the world grid offset. d->worldGridOffset = Vector2d(fmod(poly().aaBox().minX, 64), fmod(poly().aaBox().maxY, 64)); #endif poly().setMapElement(this); setBspLeaf(bspLeaf); } ConvexSubspace *ConvexSubspace::newFromConvexPoly(de::Face &poly, BspLeaf *bspLeaf) // static { if(!poly.isConvex()) { /// @throw InvalidPolyError Attempted to attribute a non-convex polygon. throw InvalidPolyError("ConvexSubspace::newFromConvexPoly", "Source is non-convex"); } return new ConvexSubspace(poly, bspLeaf); } BspLeaf &ConvexSubspace::bspLeaf() const { if(d->bspLeaf) return *d->bspLeaf; /// @throw MissingBspLeafError Attempted with no BspLeaf attributed. throw MissingBspLeafError("ConvexSubspace::bspLeaf", "No BSP leaf is attributed"); } void ConvexSubspace::setBspLeaf(BspLeaf *newBspLeaf) { d->bspLeaf = newBspLeaf; } Face &ConvexSubspace::poly() const { DENG2_ASSERT(d->poly); return *d->poly; } bool ConvexSubspace::contains(Vector2d const &point) const { HEdge const *hedge = poly().hedge(); do { Vertex const &va = hedge->vertex(); Vertex const &vb = hedge->next().vertex(); if(((va.origin().y - point.y) * (vb.origin().x - va.origin().x) - (va.origin().x - point.x) * (vb.origin().y - va.origin().y)) < 0) { // Outside the BSP leaf's edges. return false; } } while((hedge = &hedge->next()) != poly().hedge()); return true; } void ConvexSubspace::assignExtraMesh(Mesh &newMesh) { LOG_AS("ConvexSubspace"); int const sizeBefore = d->extraMeshes.size(); d->extraMeshes.insert(&newMesh); if(d->extraMeshes.size() != sizeBefore) { LOG_DEBUG("Assigned extra mesh to subspace %p") << this; // Attribute all faces to "this" subspace. for(Face *face : newMesh.faces()) { face->setMapElement(this); } } } LoopResult ConvexSubspace::forAllExtraMeshes(std::function func) const { for(Mesh *mesh : d->extraMeshes) { if(auto result = func(*mesh)) return result; } return LoopContinue; } int ConvexSubspace::polyobjCount() const { return d->polyobjs.count(); } LoopResult ConvexSubspace::forAllPolyobjs(std::function func) const { for(Polyobj *pob : d->polyobjs) { if(auto result = func(*pob)) return result; } return LoopContinue; } void ConvexSubspace::link(Polyobj const &polyobj) { d->polyobjs.insert(const_cast(&polyobj)); } bool ConvexSubspace::unlink(Polyobj const &polyobj) { int sizeBefore = d->polyobjs.size(); d->polyobjs.remove(const_cast(&polyobj)); return d->polyobjs.size() != sizeBefore; } bool ConvexSubspace::hasCluster() const { return d->cluster != 0; } SectorCluster &ConvexSubspace::cluster() const { if(d->cluster) return *d->cluster; /// @throw MissingClusterError Attempted with no sector cluster attributed. throw MissingClusterError("ConvexSubspace::cluster", "No sector cluster is attributed"); } SectorCluster *ConvexSubspace::clusterPtr() const { return hasCluster()? &cluster() : nullptr; } void ConvexSubspace::setCluster(SectorCluster *newCluster) { d->cluster = newCluster; } int ConvexSubspace::validCount() const { return d->validCount; } void ConvexSubspace::setValidCount(int newValidCount) { d->validCount = newValidCount; } #ifdef __CLIENT__ Vector2d const &ConvexSubspace::worldGridOffset() const { return d->worldGridOffset; } int ConvexSubspace::shadowLineCount() const { return d->shadowLines.count(); } void ConvexSubspace::clearShadowLines() { d->shadowLines.clear(); } void ConvexSubspace::addShadowLine(LineSide &side) { d->shadowLines.insert(&side); } LoopResult ConvexSubspace::forAllShadowLines(std::function func) const { for(LineSide *side : d->shadowLines) { if(auto result = func(*side)) return result; } return LoopContinue; } int ConvexSubspace::lumobjCount() const { return d->lumobjs.count(); } LoopResult ConvexSubspace::forAllLumobjs(std::function func) const { for(Lumobj *lob : d->lumobjs) { if(auto result = func(*lob)) return result; } return LoopContinue; } void ConvexSubspace::unlinkAllLumobjs() { d->lumobjs.clear(); } void ConvexSubspace::unlink(Lumobj &lumobj) { d->lumobjs.remove(&lumobj); } void ConvexSubspace::link(Lumobj &lumobj) { d->lumobjs.insert(&lumobj); } int ConvexSubspace::lastSpriteProjectFrame() const { return d->lastSpriteProjectFrame; } void ConvexSubspace::setLastSpriteProjectFrame(int newFrameNumber) { d->lastSpriteProjectFrame = newFrameNumber; } HEdge *ConvexSubspace::fanBase() const { if(d->needUpdateFanBase) { d->chooseFanBase(); } return d->fanBase; } int ConvexSubspace::fanVertexCount() const { // Are we to use one of the half-edge vertexes as the fan base? return poly().hedgeCount() + (fanBase()? 0 : 2); } static void accumReverbForWallSections(HEdge const *hedge, float envSpaceAccum[NUM_AUDIO_ENVIRONMENTS], float &total) { // Edges with no map line segment implicitly have no surfaces. if(!hedge || !hedge->hasMapElement()) return; LineSideSegment const &seg = hedge->mapElementAs(); if(!seg.lineSide().hasSections() || !seg.lineSide().middle().hasMaterial()) return; Material &material = seg.lineSide().middle().material(); AudioEnvironmentId env = material.audioEnvironment(); if(!(env >= 0 && env < NUM_AUDIO_ENVIRONMENTS)) { env = AE_WOOD; // Assume it's wood if unknown. } total += seg.length(); envSpaceAccum[env] += seg.length(); } bool ConvexSubspace::updateAudioEnvironment() { if(!hasCluster()) { d->aenv.reverb[SRD_SPACE] = d->aenv.reverb[SRD_VOLUME] = d->aenv.reverb[SRD_DECAY] = d->aenv.reverb[SRD_DAMPING] = 0; return false; } float envSpaceAccum[NUM_AUDIO_ENVIRONMENTS]; de::zap(envSpaceAccum); // Space is the rough volume of the BSP leaf (bounding box). AABoxd const &aaBox = poly().aaBox(); d->aenv.reverb[SRD_SPACE] = int(cluster().ceiling().height() - cluster().floor().height()) * (aaBox.maxX - aaBox.minX) * (aaBox.maxY - aaBox.minY); // The other reverb properties can be found out by taking a look at the // materials of all surfaces in the BSP leaf. float total = 0; HEdge *base = poly().hedge(); HEdge *hedge = base; do { accumReverbForWallSections(hedge, envSpaceAccum, total); } while((hedge = &hedge->next()) != base); for(Mesh *mesh : d->extraMeshes) for(HEdge *hedge : mesh->hedges()) { accumReverbForWallSections(hedge, envSpaceAccum, total); } if(!total) { // Huh? d->aenv.reverb[SRD_VOLUME] = d->aenv.reverb[SRD_DECAY] = d->aenv.reverb[SRD_DAMPING] = 0; return false; } // Average the results. for(int i = AE_FIRST; i < NUM_AUDIO_ENVIRONMENTS; ++i) { envSpaceAccum[i] /= total; } // Accumulate and clamp the final characteristics int accum[NUM_REVERB_DATA]; zap(accum); for(int i = AE_FIRST; i < NUM_AUDIO_ENVIRONMENTS; ++i) { AudioEnvironment const &envInfo = S_AudioEnvironment(AudioEnvironmentId(i)); // Volume. accum[SRD_VOLUME] += envSpaceAccum[i] * envInfo.volumeMul; // Decay time. accum[SRD_DECAY] += envSpaceAccum[i] * envInfo.decayMul; // High frequency damping. accum[SRD_DAMPING] += envSpaceAccum[i] * envInfo.dampingMul; } d->aenv.reverb[SRD_VOLUME] = de::min(accum[SRD_VOLUME], 255); d->aenv.reverb[SRD_DECAY] = de::min(accum[SRD_DECAY], 255); d->aenv.reverb[SRD_DAMPING] = de::min(accum[SRD_DAMPING], 255); return true; } ConvexSubspace::AudioEnvironmentData const &ConvexSubspace::audioEnvironmentData() const { return d->aenv; } #endif // __CLIENT__ doomsday-stable-1.15.7/doomsday/client/src/world/propertyvalue.cpp0000664000175000017500000000333312641367670024636 0ustar jaakkojaakko/** @file propertyvalue.cpp Data types for representing property values. * @ingroup data * * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include "world/propertyvalue.h" using namespace de; PropertyValue *BuildPropertyValue(valuetype_t type, void *valueAdr) { DENG2_ASSERT(valueAdr != 0); switch(type) { case DDVT_BYTE: return new PropertyByteValue (*( (byte *) valueAdr)); case DDVT_SHORT: return new PropertyInt16Value(*( (short *) valueAdr)); case DDVT_INT: return new PropertyInt32Value(*( (int *) valueAdr)); case DDVT_FIXED: return new PropertyFixedValue(*((fixed_t *) valueAdr)); case DDVT_ANGLE: return new PropertyAngleValue(*((angle_t *) valueAdr)); case DDVT_FLOAT: return new PropertyFloatValue(*( (float *) valueAdr)); default: throw Error("BuildPropertyValue", QString("Unknown/not-supported value type %1").arg(type)); } } doomsday-stable-1.15.7/doomsday/client/src/world/entitydatabase.cpp0000664000175000017500000001157312641367670024723 0ustar jaakkojaakko/** @file entitydatabase.cpp World map entity property value database. * @ingroup world * * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "world/entitydatabase.h" #include #include // An entity is a set of one or more properties. // Key is the unique identifier of said property in the // MapEntityPropertyDef it is derived from. typedef std::map Entity; // Entities are stored in a set, each associated with a unique map element index. typedef std::map Entities; // Entities are grouped in sets by their unique identifier. typedef std::map EntitySet; namespace de { DENG2_PIMPL(EntityDatabase) { EntitySet entitySets; Instance(Public *i) : Base(i) {} ~Instance() { DENG2_FOR_EACH(EntitySet, setIt, entitySets) DENG2_FOR_EACH(Entities, entityIt, setIt->second) DENG2_FOR_EACH(Entity, propIt, entityIt->second) { delete propIt->second; } } /** * Lookup the set in which entities with the unique identifier * @a entityId are stored. */ Entities *entities(int entityId) { std::pair result; result = entitySets.insert(std::pair(entityId, Entities())); return &result.first->second; } /** * Lookup an entity in @a set by its unique @a elementIndex. */ Entity *entityByElementIndex(Entities& set, int elementIndex, bool canCreate) { // Do we already have a record for this entity? Entities::iterator found = set.find(elementIndex); if(found != set.end()) return &found->second; if(!canCreate) return 0; // A new entity. std::pair result; result = set.insert(std::pair(elementIndex, Entity())); return &result.first->second; } }; EntityDatabase::EntityDatabase() : d(new Instance(this)) {} uint EntityDatabase::entityCount(MapEntityDef const *entityDef) { DENG2_ASSERT(entityDef); Entities *set = d->entities(entityDef->id); return set->size(); } bool EntityDatabase::hasEntity(MapEntityDef const *entityDef, int elementIndex) { DENG2_ASSERT(entityDef); Entities *set = d->entities(entityDef->id); return d->entityByElementIndex(*set, elementIndex, false /*do not create*/) != 0; } PropertyValue const &EntityDatabase::property(MapEntityPropertyDef const *def, int elementIndex) { DENG2_ASSERT(def); Entities *set = d->entities(def->entity->id); Entity *entity = d->entityByElementIndex(*set, elementIndex, false /*do not create*/); if(!entity) throw Error("EntityDatabase::property", QString("There is no element %1 of type %2") .arg(elementIndex) .arg(Str_Text(P_NameForMapEntityDef(def->entity)))); Entity::const_iterator found = entity->find(def->id); if(found == entity->end()) throw Error("EntityDatabase::property", QString("Element %1 of type %2 has no value for property %3") .arg(elementIndex) .arg(Str_Text(P_NameForMapEntityDef(def->entity))) .arg(def->id)); return *found->second; } void EntityDatabase::setProperty(MapEntityPropertyDef const *def, int elementIndex, PropertyValue *value) { DENG2_ASSERT(def); Entities *set = d->entities(def->entity->id); Entity *entity = d->entityByElementIndex(*set, elementIndex, true); if(!entity) throw Error("EntityDatabase::setProperty", "Failed adding new entity element record"); // Do we already have a record for this? Entity::iterator found = entity->find(def->id); if(found != entity->end()) { PropertyValue **adr = &found->second; delete *adr; *adr = value; return; } // Add a new record. entity->insert(std::pair(def->id, value)); } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/world/bspnode.cpp0000664000175000017500000000364112641367670023351 0ustar jaakkojaakko/** @file bspnode.cpp World map BSP node. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "world/bspnode.h" #include /// @todo Remove me using namespace de; DENG2_PIMPL_NOREF(BspNode) { Partition partition; ///< Half-plane space partition. AABoxd rightBounds; ///< Right half-space bounds. AABoxd leftBounds; ///< Left half-space bounds. inline AABoxd &bounds(int left) { return left? leftBounds : rightBounds; } }; BspNode::BspNode(Partition const &partition, AABoxd const &rightBounds, AABoxd const &leftBounds) : d(new Instance) { d->partition = partition; d->rightBounds = rightBounds; d->leftBounds = leftBounds; } Partition const &BspNode::partition() const { return d->partition; } AABoxd const &BspNode::childAABox(int which) const { return d->bounds(which); } void BspNode::setChildAABox(int which, AABoxd const *newBounds) { if(newBounds) { d->bounds(which) = *newBounds; } else { d->bounds(which).clear(); } } doomsday-stable-1.15.7/doomsday/client/src/world/generator.cpp0000664000175000017500000010501212641367670023700 0ustar jaakkojaakko/** @file generator.cpp World map (particle) generator. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "world/generator.h" #include "clientapp.h" #include "dd_def.h" #include "Face" #include "world/worldsystem.h" // validCount #include "world/thinkers.h" #include "client/cl_mobj.h" #include "BspLeaf" #include "ConvexSubspace" #include "SectorCluster" #include "Surface" #include "render/rend_model.h" #include "render/rend_particle.h" #include "api_sound.h" #include #include #include #include #include #include #include #include using namespace de; #define DOT2F(a,b) ( FIX2FLT(a[VX]) * FIX2FLT(b[VX]) + FIX2FLT(a[VY]) * FIX2FLT(b[VY]) ) #define VECMUL(a,scalar) ( a[VX] = FixedMul(a[VX], scalar), a[VY] = FixedMul(a[VY], scalar) ) #define VECADD(a,b) ( a[VX] += b[VX], a[VY] += b[VY] ) #define VECMULADD(a,scal,b) ( a[VX] += FixedMul(scal, b[VX]), a[VY] += FixedMul(scal, b[VY]) ) #define VECSUB(a,b) ( a[VX] -= b[VX], a[VY] -= b[VY] ) #define VECCPY(a,b) ( a[VX] = b[VX], a[VY] = b[VY] ) static float particleSpawnRate = 1; // Unmodified (cvar). /** * The offset is spherical and random. * Low and High should be positive. */ static void uncertainPosition(fixed_t *pos, fixed_t low, fixed_t high) { if(!low) { // The simple, cubic algorithm. for(int i = 0; i < 3; ++i) { pos[i] += (high * (RNG_RandByte() - RNG_RandByte())) * reciprocal255; } } else { // The more complicated, spherical algorithm. fixed_t off = ((high - low) * (RNG_RandByte() - RNG_RandByte())) * reciprocal255; off += off < 0 ? -low : low; fixed_t theta = RNG_RandByte() << (24 - ANGLETOFINESHIFT); fixed_t phi = acos(2 * (RNG_RandByte() * reciprocal255) - 1) / PI * (ANGLE_180 >> ANGLETOFINESHIFT); fixed_t vec[3]; vec[VX] = FixedMul(fineCosine[theta], finesine[phi]); vec[VY] = FixedMul(finesine[theta], finesine[phi]); vec[VZ] = FixedMul(fineCosine[phi], FLT2FIX(0.8333f)); for(int i = 0; i < 3; ++i) { pos[i] += FixedMul(vec[i], off); } } } Map &Generator::map() const { return Thinker_Map(thinker); } Generator::Id Generator::id() const { return _id; } void Generator::setId(Id newId) { DENG2_ASSERT(newId >= 1 && newId <= Map::MAX_GENERATORS); // 1-based _id = newId; } int Generator::age() const { return _age; } Vector3d Generator::origin() const { if(source) { Vector3d origin(source->origin); origin.z += -source->floorClip + FIX2FLT(originAtSpawn[VZ]); return origin; } return Vector3d(FIX2FLT(originAtSpawn[VX]), FIX2FLT(originAtSpawn[VY]), FIX2FLT(originAtSpawn[VZ])); } void Generator::clearParticles() { Z_Free(_pinfo); _pinfo = 0; } void Generator::configureFromDef(ded_ptcgen_t const *newDef) { DENG2_ASSERT(newDef != 0); if(count <= 0) count = 1; // Make sure no generator is type-triggered by default. type = type2 = -1; def = newDef; _flags = Flags(def->flags); _pinfo = (ParticleInfo *) Z_Calloc(sizeof(ParticleInfo) * count, PU_MAP, 0); stages = (ParticleStage *) Z_Calloc(sizeof(ParticleStage) * def->stages.size(), PU_MAP, 0); for(int i = 0; i < def->stages.size(); ++i) { ded_ptcstage_t const *sdef = &def->stages[i]; ParticleStage *s = &stages[i]; s->bounce = FLT2FIX(sdef->bounce); s->resistance = FLT2FIX(1 - sdef->resistance); s->radius = FLT2FIX(sdef->radius); s->gravity = FLT2FIX(sdef->gravity); s->type = sdef->type; s->flags = ParticleStage::Flags(sdef->flags); } // Init some data. for(int i = 0; i < 3; ++i) { originAtSpawn[i] = FLT2FIX(def->center[i]); vector[i] = FLT2FIX(def->vector[i]); } // Apply a random component to the spawn vector. if(def->initVectorVariance > 0) { uncertainPosition(vector, 0, FLT2FIX(def->initVectorVariance)); } // Mark unused. for(int i = 0; i < count; ++i) { ParticleInfo* pinfo = &_pinfo[i]; pinfo->stage = -1; } } void Generator::presimulate(int tics) { for(; tics > 0; tics--) { runTick(); } // Reset age so presim doesn't affect it. _age = 0; } bool Generator::isStatic() const { return _flags.testFlag(Static); } bool Generator::isUntriggered() const { return _untriggered; } void Generator::setUntriggered(bool yes) { _untriggered = yes; } blendmode_t Generator::blendmode() const { /// @todo Translate these old flags once, during definition parsing -ds if(_flags.testFlag(BlendAdditive)) return BM_ADD; if(_flags.testFlag(BlendSubtract)) return BM_SUBTRACT; if(_flags.testFlag(BlendReverseSubtract)) return BM_REVERSE_SUBTRACT; if(_flags.testFlag(BlendMultiply)) return BM_MUL; if(_flags.testFlag(BlendInverseMultiply)) return BM_INVERSE_MUL; return BM_NORMAL; } int Generator::activeParticleCount() const { int numActive = 0; for(int i = 0; i < count; ++i) { if(_pinfo[i].stage >= 0) { numActive += 1; } } return numActive; } ParticleInfo const *Generator::particleInfo() const { return _pinfo; } static void setParticleAngles(ParticleInfo *pinfo, int flags) { DENG2_ASSERT(pinfo != 0); if(flags & Generator::ParticleStage::ZeroYaw) pinfo->yaw = 0; if(flags & Generator::ParticleStage::ZeroPitch) pinfo->pitch = 0; if(flags & Generator::ParticleStage::RandomYaw) pinfo->yaw = RNG_RandFloat() * 65536; if(flags & Generator::ParticleStage::RandomPitch) pinfo->pitch = RNG_RandFloat() * 65536; } static void particleSound(fixed_t pos[3], ded_embsound_t *sound) { DENG2_ASSERT(pos != 0 && sound != 0); // Is there any sound to play? if(!sound->id || sound->volume <= 0) return; coord_t orig[3]; for(int i = 0; i < 3; ++i) { orig[i] = FIX2FLT(pos[i]); } S_LocalSoundAtVolumeFrom(sound->id, NULL, orig, sound->volume); } int Generator::newParticle() { #ifdef __CLIENT__ // Check for model-only generators. float inter = -1; ModelDef *mf = 0, *nextmf = 0; if(source) { mf = Mobj_ModelDef(*source, &nextmf, &inter); if(((!mf || !useModels) && def->flags & ModelOnly) || (mf && useModels && mf->flags & MFF_NO_PARTICLES)) return -1; } // Keep the spawn cursor in the valid range. if(++_spawnCP >= count) { _spawnCP -= count; } int const newParticleIdx = _spawnCP; // Set the particle's data. ParticleInfo *pinfo = &_pinfo[_spawnCP]; pinfo->stage = 0; if(RNG_RandFloat() < def->altStartVariance) { pinfo->stage = def->altStart; } pinfo->tics = def->stages[pinfo->stage].tics * (1 - def->stages[pinfo->stage].variance * RNG_RandFloat()); // Launch vector. pinfo->mov[VX] = vector[VX]; pinfo->mov[VY] = vector[VY]; pinfo->mov[VZ] = vector[VZ]; // Apply some random variance. pinfo->mov[VX] += FLT2FIX(def->vectorVariance * (RNG_RandFloat() - RNG_RandFloat())); pinfo->mov[VY] += FLT2FIX(def->vectorVariance * (RNG_RandFloat() - RNG_RandFloat())); pinfo->mov[VZ] += FLT2FIX(def->vectorVariance * (RNG_RandFloat() - RNG_RandFloat())); // Apply some aspect ratio scaling to the momentum vector. // This counters the 200/240 difference nearly completely. pinfo->mov[VX] = FixedMul(pinfo->mov[VX], FLT2FIX(1.1f)); pinfo->mov[VY] = FixedMul(pinfo->mov[VY], FLT2FIX(0.95f)); pinfo->mov[VZ] = FixedMul(pinfo->mov[VZ], FLT2FIX(1.1f)); // Set proper speed. fixed_t uncertain = FLT2FIX(def->speed * (1 - def->speedVariance * RNG_RandFloat())); fixed_t len = FLT2FIX(M_ApproxDistancef( M_ApproxDistancef(FIX2FLT(pinfo->mov[VX]), FIX2FLT(pinfo->mov[VY])), FIX2FLT(pinfo->mov[VZ]))); if(!len) len = FRACUNIT; len = FixedDiv(uncertain, len); pinfo->mov[VX] = FixedMul(pinfo->mov[VX], len); pinfo->mov[VY] = FixedMul(pinfo->mov[VY], len); pinfo->mov[VZ] = FixedMul(pinfo->mov[VZ], len); // The source is a mobj? if(source) { if(_flags & RelativeVector) { // Rotate the vector using the source angle. float temp[3]; temp[VX] = FIX2FLT(pinfo->mov[VX]); temp[VY] = FIX2FLT(pinfo->mov[VY]); temp[VZ] = 0; // Player visangles have some problems, let's not use them. M_RotateVector(temp, source->angle / (float) ANG180 * -180 + 90, 0); pinfo->mov[VX] = FLT2FIX(temp[VX]); pinfo->mov[VY] = FLT2FIX(temp[VY]); } if(_flags & RelativeVelocity) { pinfo->mov[VX] += FLT2FIX(source->mom[MX]); pinfo->mov[VY] += FLT2FIX(source->mom[MY]); pinfo->mov[VZ] += FLT2FIX(source->mom[MZ]); } // Origin. pinfo->origin[VX] = FLT2FIX(source->origin[VX]); pinfo->origin[VY] = FLT2FIX(source->origin[VY]); pinfo->origin[VZ] = FLT2FIX(source->origin[VZ] - source->floorClip); uncertainPosition(pinfo->origin, FLT2FIX(def->spawnRadiusMin), FLT2FIX(def->spawnRadius)); // Offset to the real center. pinfo->origin[VZ] += originAtSpawn[VZ]; // Include bobbing in the spawn height. pinfo->origin[VZ] -= FLT2FIX(Mobj_BobOffset(*source)); // Calculate XY center with mobj angle. angle_t const angle = Mobj_AngleSmoothed(source) + (fixed_t) (FIX2FLT(originAtSpawn[VY]) / 180.0f * ANG180); uint const an = angle >> ANGLETOFINESHIFT; uint const an2 = (angle + ANG90) >> ANGLETOFINESHIFT; pinfo->origin[VX] += FixedMul(fineCosine[an], originAtSpawn[VX]); pinfo->origin[VY] += FixedMul(finesine[an], originAtSpawn[VX]); // There might be an offset from the model of the mobj. if(mf && (mf->testSubFlag(0, MFF_PARTICLE_SUB1) || def->subModel >= 0)) { float off[3] = { 0, 0, 0 }; int subidx; // Select the right submodel to use as the origin. if(def->subModel >= 0) subidx = def->subModel; else subidx = 1; // Default to submodel #1. // Interpolate the offset. if(inter > 0 && nextmf) { off[VX] = nextmf->particleOffset(subidx)[VX] - mf->particleOffset(subidx)[VX]; off[VY] = nextmf->particleOffset(subidx)[VY] - mf->particleOffset(subidx)[VY]; off[VZ] = nextmf->particleOffset(subidx)[VZ] - mf->particleOffset(subidx)[VZ]; off[VX] *= inter; off[VY] *= inter; off[VZ] *= inter; } off[VX] += mf->particleOffset(subidx)[VX]; off[VY] += mf->particleOffset(subidx)[VY]; off[VZ] += mf->particleOffset(subidx)[VZ]; // Apply it to the particle coords. pinfo->origin[VX] += FixedMul(fineCosine[an], FLT2FIX(off[VX])); pinfo->origin[VX] += FixedMul(fineCosine[an2], FLT2FIX(off[VZ])); pinfo->origin[VY] += FixedMul(finesine[an], FLT2FIX(off[VX])); pinfo->origin[VY] += FixedMul(finesine[an2], FLT2FIX(off[VZ])); pinfo->origin[VZ] += FLT2FIX(off[VY]); } } else if(plane) { /// @todo fixme: ignorant of mapped sector planes. fixed_t radius = stages[pinfo->stage].radius; Sector const *sector = &plane->sector(); // Choose a random spot inside the sector, on the spawn plane. if(_flags & SpawnSpace) { pinfo->origin[VZ] = FLT2FIX(sector->floor().height()) + radius + FixedMul(RNG_RandByte() << 8, FLT2FIX(sector->ceiling().height() - sector->floor().height()) - 2 * radius); } else if((_flags & SpawnFloor) || (!(_flags & (SpawnFloor | SpawnCeiling)) && plane->isSectorFloor())) { // Spawn on the floor. pinfo->origin[VZ] = FLT2FIX(plane->height()) + radius; } else { // Spawn on the ceiling. pinfo->origin[VZ] = FLT2FIX(plane->height()) - radius; } /** * Choosing the XY spot is a bit more difficult. * But we must be fast and only sufficiently accurate. * * @todo Nothing prevents spawning on the wrong side (or inside) of one-sided * walls (large diagonal subspaces!). */ ConvexSubspace *subspace = 0; for(int i = 0; i < 5; ++i) // Try a couple of times (max). { float x = sector->aaBox().minX + RNG_RandFloat() * (sector->aaBox().maxX - sector->aaBox().minX); float y = sector->aaBox().minY + RNG_RandFloat() * (sector->aaBox().maxY - sector->aaBox().minY); subspace = map().bspLeafAt(Vector2d(x, y)).subspacePtr(); if(subspace && sector == &subspace->sector()) break; subspace = 0; } if(!subspace) { pinfo->stage = -1; return -1; } AABoxd const &subAABox = subspace->poly().aaBox(); // Try a couple of times to get a good random spot. int tries; for(tries = 0; tries < 10; ++tries) // Max this many tries before giving up. { float x = subAABox.minX + RNG_RandFloat() * (subAABox.maxX - subAABox.minX); float y = subAABox.minY + RNG_RandFloat() * (subAABox.maxY - subAABox.minY); pinfo->origin[VX] = FLT2FIX(x); pinfo->origin[VY] = FLT2FIX(y); if(subspace == map().bspLeafAt(Vector2d(x, y)).subspacePtr()) break; // This is a good place. } if(tries == 10) // No good place found? { pinfo->stage = -1; // Damn. return -1; } } else if(isUntriggered()) { // The center position is the spawn origin. pinfo->origin[VX] = originAtSpawn[VX]; pinfo->origin[VY] = originAtSpawn[VY]; pinfo->origin[VZ] = originAtSpawn[VZ]; uncertainPosition(pinfo->origin, FLT2FIX(def->spawnRadiusMin), FLT2FIX(def->spawnRadius)); } // Initial angles for the particle. setParticleAngles(pinfo, def->stages[pinfo->stage].flags); // The other place where this gets updated is after moving over // a two-sided line. /*if(plane) { pinfo->sector = &plane->sector(); } else*/ { Vector2d ptOrigin(FIX2FLT(pinfo->origin[VX]), FIX2FLT(pinfo->origin[VY])); pinfo->bspLeaf = &map().bspLeafAt(ptOrigin); // A BSP leaf with no geometry is not a suitable place for a particle. if(!pinfo->bspLeaf->hasSubspace()) { pinfo->stage = -1; return -1; } } // Play a stage sound? particleSound(pinfo->origin, &def->stages[pinfo->stage].sound); return newParticleIdx; #else // !__CLIENT__ return -1; #endif } #ifdef __CLIENT__ /** * Callback for the client mobj iterator, called from P_PtcGenThinker. */ static int newGeneratorParticlesWorker(mobj_t *cmo, void *context) { Generator *gen = (Generator *) context; ClientMobjThinkerData::RemoteSync *info = ClMobj_GetInfo(cmo); // If the clmobj is not valid at the moment, don't do anything. if(info->flags & (CLMF_UNPREDICTABLE | CLMF_HIDDEN)) { return false; } if(cmo->type != gen->type && cmo->type != gen->type2) { // Type mismatch. return false; } gen->source = cmo; gen->newParticle(); return false; } #endif /** * Particle touches something solid. Returns false iff the particle dies. */ static int touchParticle(ParticleInfo *pinfo, Generator::ParticleStage *stage, ded_ptcstage_t *stageDef, bool touchWall) { // Play a hit sound. particleSound(pinfo->origin, &stageDef->hitSound); if(stage->flags.testFlag(Generator::ParticleStage::DieTouch)) { // Particle dies from touch. pinfo->stage = -1; return false; } if(stage->flags.testFlag(Generator::ParticleStage::StageTouch) || (touchWall && stage->flags.testFlag(Generator::ParticleStage::StageWallTouch)) || (!touchWall && stage->flags.testFlag(Generator::ParticleStage::StageFlatTouch))) { // Particle advances to the next stage. pinfo->tics = 0; } // Particle survives the touch. return true; } float Generator::particleZ(ParticleInfo const &pinfo) const { SectorCluster &cluster = pinfo.bspLeaf->subspace().cluster(); if(pinfo.origin[VZ] == DDMAXINT) { return cluster.visCeiling().heightSmoothed() - 2; } if(pinfo.origin[VZ] == DDMININT) { return (cluster.visFloor().heightSmoothed() + 2); } return FIX2FLT(pinfo.origin[VZ]); } Vector3f Generator::particleOrigin(ParticleInfo const &pt) const { return Vector3f(FIX2FLT(pt.origin[VX]), FIX2FLT(pt.origin[VY]), particleZ(pt)); } Vector3f Generator::particleMomentum(ParticleInfo const &pt) const { return Vector3f(FIX2FLT(pt.mov[VX]), FIX2FLT(pt.mov[VY]), FIX2FLT(pt.mov[VZ])); } void Generator::spinParticle(ParticleInfo &pinfo) { static int const yawSigns[4] = { 1, 1, -1, -1 }; static int const pitchSigns[4] = { 1, -1, 1, -1 }; ded_ptcstage_t const *stDef = &def->stages[pinfo.stage]; uint const spinIndex = uint(&pinfo - &_pinfo[id() / 8]) % 4; DENG_ASSERT(spinIndex < 4); int const yawSign = yawSigns[spinIndex]; int const pitchSign = pitchSigns[spinIndex]; if(stDef->spin[0] != 0) { pinfo.yaw += 65536 * yawSign * stDef->spin[0] / (360 * TICSPERSEC); } if(stDef->spin[1] != 0) { pinfo.pitch += 65536 * pitchSign * stDef->spin[1] / (360 * TICSPERSEC); } pinfo.yaw *= 1 - stDef->spinResistance[0]; pinfo.pitch *= 1 - stDef->spinResistance[1]; } void Generator::moveParticle(int index) { DENG2_ASSERT(index >= 0 && index < count); ParticleInfo *pinfo = &_pinfo[index]; ParticleStage *st = &stages[pinfo->stage]; ded_ptcstage_t *stDef = &def->stages[pinfo->stage]; // Particle rotates according to spin speed. spinParticle(*pinfo); // Changes to momentum. /// @todo Do not assume generator is from the CURRENT map. pinfo->mov[VZ] -= FixedMul(FLT2FIX(map().gravity()), st->gravity); // Vector force. if(stDef->vectorForce[VX] != 0 || stDef->vectorForce[VY] != 0 || stDef->vectorForce[VZ] != 0) { for(int i = 0; i < 3; ++i) { pinfo->mov[i] += FLT2FIX(stDef->vectorForce[i]); } } // Sphere force pull and turn. // Only applicable to sourced or untriggered generators. For other // types it's difficult to define the center coordinates. if(st->flags.testFlag(ParticleStage::SphereForce) && (source || isUntriggered())) { float delta[3]; if(source) { delta[VX] = FIX2FLT(pinfo->origin[VX]) - source->origin[VX]; delta[VY] = FIX2FLT(pinfo->origin[VY]) - source->origin[VY]; delta[VZ] = particleZ(*pinfo) - (source->origin[VZ] + FIX2FLT(originAtSpawn[VZ])); } else { for(int i = 0; i < 3; ++i) { delta[i] = FIX2FLT(pinfo->origin[i] - originAtSpawn[i]); } } // Apply the offset (to source coords). for(int i = 0; i < 3; ++i) { delta[i] -= def->forceOrigin[i]; } // Counter the aspect ratio of old times. delta[VZ] *= 1.2f; float dist = M_ApproxDistancef(M_ApproxDistancef(delta[VX], delta[VY]), delta[VZ]); if(dist != 0) { // Radial force pushes the particles on the surface of a sphere. if(def->force) { // Normalize delta vector, multiply with (dist - forceRadius), // multiply with radial force strength. for(int i = 0; i < 3; ++i) { pinfo->mov[i] -= FLT2FIX( ((delta[i] / dist) * (dist - def->forceRadius)) * def->force); } } // Rotate! if(def->forceAxis[VX] || def->forceAxis[VY] || def->forceAxis[VZ]) { float cross[3]; V3f_CrossProduct(cross, def->forceAxis, delta); for(int i = 0; i < 3; ++i) { pinfo->mov[i] += FLT2FIX(cross[i]) >> 8; } } } } if(st->resistance != FRACUNIT) { for(int i = 0; i < 3; ++i) { pinfo->mov[i] = FixedMul(pinfo->mov[i], st->resistance); } } // The particle is 'soft': half of radius is ignored. // The exception is plane flat particles, which are rendered flat // against planes. They are almost entirely soft when it comes to plane // collisions. fixed_t hardRadius = st->radius / 2; if((st->type == PTC_POINT || (st->type >= PTC_TEXTURE && st->type < PTC_TEXTURE + MAX_PTC_TEXTURES)) && st->flags.testFlag(ParticleStage::PlaneFlat)) { hardRadius = FRACUNIT; } // Check the new Z position only if not stuck to a plane. fixed_t z = pinfo->origin[VZ] + pinfo->mov[VZ]; bool zBounce = false, hitFloor = false; if(pinfo->origin[VZ] != DDMININT && pinfo->origin[VZ] != DDMAXINT && pinfo->bspLeaf) { SectorCluster &cluster = pinfo->bspLeaf->subspace().cluster(); if(z > FLT2FIX(cluster.visCeiling().heightSmoothed()) - hardRadius) { // The Z is through the roof! if(cluster.visCeiling().surface().hasSkyMaskedMaterial()) { // Special case: particle gets lost in the sky. pinfo->stage = -1; return; } if(!touchParticle(pinfo, st, stDef, false)) return; z = FLT2FIX(cluster.visCeiling().heightSmoothed()) - hardRadius; zBounce = true; hitFloor = false; } // Also check the floor. if(z < FLT2FIX(cluster.visFloor().heightSmoothed()) + hardRadius) { if(cluster.visFloor().surface().hasSkyMaskedMaterial()) { pinfo->stage = -1; return; } if(!touchParticle(pinfo, st, stDef, false)) return; z = FLT2FIX(cluster.visFloor().heightSmoothed()) + hardRadius; zBounce = true; hitFloor = true; } if(zBounce) { pinfo->mov[VZ] = FixedMul(-pinfo->mov[VZ], st->bounce); if(!pinfo->mov[VZ]) { // The particle has stopped moving. This means its Z-movement // has ceased because of the collision with a plane. Plane-flat // particles will stick to the plane. if((st->type == PTC_POINT || (st->type >= PTC_TEXTURE && st->type < PTC_TEXTURE + MAX_PTC_TEXTURES)) && st->flags.testFlag(ParticleStage::PlaneFlat)) { z = hitFloor ? DDMININT : DDMAXINT; } } } // Move to the new Z coordinate. pinfo->origin[VZ] = z; } // Now check the XY direction. // - Check if the movement crosses any solid lines. // - If it does, quit when first one contacted and apply appropriate // bounce (result depends on the angle of the contacted wall). fixed_t x = pinfo->origin[VX] + pinfo->mov[VX]; fixed_t y = pinfo->origin[VY] + pinfo->mov[VY]; struct checklineworker_params_t { AABoxd box; fixed_t tmpz, tmprad, tmpx1, tmpx2, tmpy1, tmpy2; bool tmcross; Line *ptcHitLine; }; checklineworker_params_t clParm; zap(clParm); clParm.tmcross = false; // Has crossed potential sector boundary? // XY movement can be skipped if the particle is not moving on the // XY plane. if(!pinfo->mov[VX] && !pinfo->mov[VY]) { // If the particle is contacting a line, there is a chance that the // particle should be killed (if it's moving slowly at max). if(pinfo->contact) { Sector *front = pinfo->contact->frontSectorPtr(); Sector *back = pinfo->contact->backSectorPtr(); if(front && back && abs(pinfo->mov[VZ]) < FRACUNIT / 2) { coord_t const pz = particleZ(*pinfo); coord_t fz; if(front->floor().height() > back->floor().height()) { fz = front->floor().height(); } else { fz = back->floor().height(); } coord_t cz; if(front->ceiling().height() < back->ceiling().height()) { cz = front->ceiling().height(); } else { cz = back->ceiling().height(); } // If the particle is in the opening of a 2-sided line, it's // quite likely that it shouldn't be here... if(pz > fz && pz < cz) { // Kill the particle. pinfo->stage = -1; return; } } } // Still not moving on the XY plane... goto quit_iteration; } // We're moving in XY, so if we don't hit anything there can't be any line contact. pinfo->contact = 0; // Bounding box of the movement line. clParm.tmpz = z; clParm.tmprad = hardRadius; clParm.tmpx1 = pinfo->origin[VX]; clParm.tmpx2 = x; clParm.tmpy1 = pinfo->origin[VY]; clParm.tmpy2 = y; vec2d_t point; V2d_Set(point, FIX2FLT(MIN_OF(x, pinfo->origin[VX]) - st->radius), FIX2FLT(MIN_OF(y, pinfo->origin[VY]) - st->radius)); V2d_InitBox(clParm.box.arvec2, point); V2d_Set(point, FIX2FLT(MAX_OF(x, pinfo->origin[VX]) + st->radius), FIX2FLT(MAX_OF(y, pinfo->origin[VY]) + st->radius)); V2d_AddToBox(clParm.box.arvec2, point); // Iterate the lines in the contacted blocks. validCount++; DENG2_ASSERT(!clParm.ptcHitLine); map().forAllLinesInBox(clParm.box, [&clParm] (Line &line) { // Does the bounding box miss the line completely? if(clParm.box.maxX <= line.aaBox().minX || clParm.box.minX >= line.aaBox().maxX || clParm.box.maxY <= line.aaBox().minY || clParm.box.minY >= line.aaBox().maxY) { return LoopContinue; } // Movement must cross the line. if((line.pointOnSide(Vector2d(FIX2FLT(clParm.tmpx1), FIX2FLT(clParm.tmpy1))) < 0) == (line.pointOnSide(Vector2d(FIX2FLT(clParm.tmpx2), FIX2FLT(clParm.tmpy2))) < 0)) { return LoopContinue; } /* * We are possibly hitting something here. */ // Bounce if we hit a solid wall. /// @todo fixme: What about "one-way" window lines? clParm.ptcHitLine = &line; if(!line.hasBackSector()) { return LoopAbort; // Boing! } Sector *front = line.frontSectorPtr(); Sector *back = line.backSectorPtr(); // Determine the opening we have here. /// @todo Use R_OpenRange() fixed_t ceil; if(front->ceiling().height() < back->ceiling().height()) { ceil = FLT2FIX(front->ceiling().height()); } else { ceil = FLT2FIX(back->ceiling().height()); } fixed_t floor; if(front->floor().height() > back->floor().height()) { floor = FLT2FIX(front->floor().height()); } else { floor = FLT2FIX(back->floor().height()); } // There is a backsector. We possibly might hit something. if(clParm.tmpz - clParm.tmprad < floor || clParm.tmpz + clParm.tmprad > ceil) { return LoopAbort; // Boing! } // False alarm, continue checking. clParm.ptcHitLine = nullptr; // There is a possibility that the new position is in a new sector. clParm.tmcross = true; // Afterwards, update the sector pointer. return LoopContinue; }); if(clParm.ptcHitLine) { fixed_t normal[2], dotp; // Must survive the touch. if(!touchParticle(pinfo, st, stDef, true)) return; // There was a hit! Calculate bounce vector. // - Project movement vector on the normal of hitline. // - Calculate the difference to the point on the normal. // - Add the difference to movement vector, negate movement. // - Multiply with bounce. // Calculate the normal. normal[VX] = -FLT2FIX(clParm.ptcHitLine->direction().x); normal[VY] = -FLT2FIX(clParm.ptcHitLine->direction().y); if(!normal[VX] && !normal[VY]) goto quit_iteration; // Calculate as floating point so we don't overflow. dotp = FRACUNIT * (DOT2F(pinfo->mov, normal) / DOT2F(normal, normal)); VECMUL(normal, dotp); VECSUB(normal, pinfo->mov); VECMULADD(pinfo->mov, 2 * FRACUNIT, normal); VECMUL(pinfo->mov, st->bounce); // Continue from the old position. x = pinfo->origin[VX]; y = pinfo->origin[VY]; clParm.tmcross = false; // Sector can't change if XY doesn't. // This line is the latest contacted line. pinfo->contact = clParm.ptcHitLine; goto quit_iteration; } quit_iteration: // The move is now OK. pinfo->origin[VX] = x; pinfo->origin[VY] = y; // Should we update the sector pointer? if(clParm.tmcross) { pinfo->bspLeaf = &map().bspLeafAt(Vector2d(FIX2FLT(x), FIX2FLT(y))); // A BSP leaf with no geometry is not a suitable place for a particle. if(!pinfo->bspLeaf->hasSubspace()) { // Kill the particle. pinfo->stage = -1; } } } void Generator::runTick() { // Source has been destroyed? if(!isUntriggered() && !map().thinkers().isUsedMobjId(srcid)) { // Blasted... Spawning new particles becomes impossible. source = nullptr; } // Time to die? DENG2_ASSERT(def); if(++_age > def->maxAge && def->maxAge >= 0) { Generator_Delete(this); return; } // Spawn new particles? dfloat newParts = 0; if((_age <= def->spawnAge || def->spawnAge < 0) && (source || plane || type >= 0 || type == DED_PTCGEN_ANY_MOBJ_TYPE || isUntriggered())) { newParts = def->spawnRate * spawnRateMultiplier; newParts *= particleSpawnRate * (1 - def->spawnRateVariance * RNG_RandFloat()); _spawnCount += newParts; while(_spawnCount >= 1) { // Spawn a new particle. if(type == DED_PTCGEN_ANY_MOBJ_TYPE || type >= 0) // Type-triggered? { #ifdef __CLIENT__ // Client's should also check the client mobjs. if(isClient) { map().clMobjIterator(newGeneratorParticlesWorker, this); } #endif // Spawn new particles using all applicable sources. map().thinkers().forAll(reinterpret_cast(gx.MobjThinker), 0x1 /*public*/, [this] (thinker_t *th) { // Type match? auto &mob = *reinterpret_cast(th); if(type == DED_PTCGEN_ANY_MOBJ_TYPE || mob.type == type || mob.type == type2) { // Someone might think this is a slight hack... source = &mob; newParticle(); } return LoopContinue; }); // The generator has no real source. source = nullptr; } else { newParticle(); } _spawnCount--; } } // Move particles. ParticleInfo *pinfo = _pinfo; for(dint i = 0; i < count; ++i, pinfo++) { if(pinfo->stage < 0) continue; // Not in use. if(pinfo->tics-- <= 0) { // Advance to next stage. if(++pinfo->stage == def->stages.size() || stages[pinfo->stage].type == PTC_NONE) { // Kill the particle. pinfo->stage = -1; continue; } pinfo->tics = def->stages[pinfo->stage].tics * (1 - def->stages[pinfo->stage].variance * RNG_RandFloat()); // Change in particle angles? setParticleAngles(pinfo, def->stages[pinfo->stage].flags); // Play a sound? particleSound(pinfo->origin, &def->stages[pinfo->stage].sound); } // Try to move. moveParticle(i); } } void Generator::consoleRegister() //static { C_VAR_FLOAT("rend-particle-rate", &particleSpawnRate, 0, 0, 5); } void Generator_Delete(Generator *gen) { if(!gen) return; gen->map().unlink(*gen); gen->map().thinkers().remove(gen->thinker); gen->clearParticles(); Z_Free(gen->stages); gen->stages = nullptr; // The generator itself is free'd when it's next turn for thinking comes. } void Generator_Thinker(Generator *gen) { DENG2_ASSERT(gen != 0); gen->runTick(); } doomsday-stable-1.15.7/doomsday/client/src/world/contactspreader.cpp0000664000175000017500000002273212641367670025102 0ustar jaakkojaakko/** @file contactspreader.cpp World object => BSP leaf "contact" spreader. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include #include #include "world/contactspreader.h" #include "Face" #include "HEdge" #include "BspLeaf" #include "Contact" #include "ConvexSubspace" #include "Sector" #include "SectorCluster" #include "Surface" #include "world/worldsystem.h" // validCount #include "render/rend_main.h" // Rend_mapSurfaceMaterialSpec #include "MaterialAnimator" #include "WallEdge" namespace de { namespace internal { /** * On which side of the half-edge does the specified @a point lie? * * @param hedge Half-edge to test. * @param point Point to test in the map coordinate space. * * @return @c <0 Point is to the left/back of the segment. * @c =0 Point lies directly on the segment. * @c >0 Point is to the right/front of the segment. */ static coord_t pointOnHEdgeSide(HEdge const &hedge, Vector2d const &point) { Vector2d const direction = hedge.twin().origin() - hedge.origin(); ddouble pointV1[2] = { point.x, point.y }; ddouble fromOriginV1[2] = { hedge.origin().x, hedge.origin().y }; ddouble directionV1[2] = { direction.x, direction.y }; return V2d_PointOnLineSide(pointV1, fromOriginV1, directionV1); } struct ContactSpreader { Blockmap const &_blockmap; QBitArray *_spreadBlocks; struct SpreadState { Contact *contact; AABoxd contactAABox; SpreadState() : contact(0) {} }; SpreadState _spread; ContactSpreader(Blockmap const &blockmap, QBitArray *spreadBlocks = 0) : _blockmap(blockmap), _spreadBlocks(spreadBlocks) {} /** * Spread contacts in the blockmap to any touched neighbors. * * @param box Map space region in which to perform spreading. */ void spread(AABoxd const &box) { BlockmapCellBlock const cellBlock = _blockmap.toCellBlock(box); BlockmapCell cell; for(cell.y = cellBlock.min.y; cell.y < cellBlock.max.y; ++cell.y) for(cell.x = cellBlock.min.x; cell.x < cellBlock.max.x; ++cell.x) { if(_spreadBlocks) { // Should we skip this cell? int cellIndex = _blockmap.toCellIndex(cell.x, cell.y); if(_spreadBlocks->testBit(cellIndex)) continue; // Mark the cell as processed. _spreadBlocks->setBit(cellIndex); } _blockmap.forAllInCell(cell, [this] (void *element) { spreadContact(*static_cast(element)); return LoopContinue; }); } } private: /** * Link the contact in all non-degenerate subspaces which touch the linked * object (tests are done with subspace bounding boxes and the spread test). * * @param contact Contact to be spread. */ void spreadContact(Contact &contact) { ConvexSubspace &subspace = contact.objectBspLeafAtOrigin().subspace(); R_ContactList(subspace, contact.type()).link(&contact); // Spread to neighboring BSP leafs. subspace.setValidCount(++validCount); _spread.contact = &contact; _spread.contactAABox = contact.objectAABox(); spreadInSubspace(subspace); } void maybeSpreadOverEdge(HEdge *hedge) { DENG2_ASSERT(_spread.contact != 0); if(!hedge) return; ConvexSubspace &subspace = hedge->face().mapElementAs(); SectorCluster &cluster = subspace.cluster(); // There must be a back BSP leaf to spread to. if(!hedge->hasTwin() || !hedge->twin().hasFace() || !hedge->twin().face().hasMapElement()) return; ConvexSubspace &backSubspace = hedge->twin().face().mapElementAs(); SectorCluster &backCluster = backSubspace.cluster(); // Which way does the spread go? if(!(subspace.validCount() == validCount && backSubspace.validCount() != validCount)) { return; // Not eligible for spreading. } // Is the leaf on the back side outside the origin's AABB? AABoxd const &aaBox = backSubspace.poly().aaBox(); if(aaBox.maxX <= _spread.contactAABox.minX || aaBox.minX >= _spread.contactAABox.maxX || aaBox.maxY <= _spread.contactAABox.minY || aaBox.minY >= _spread.contactAABox.maxY) return; // Too far from the edge? coord_t const length = (hedge->twin().origin() - hedge->origin()).length(); coord_t const distance = pointOnHEdgeSide(*hedge, _spread.contact->objectOrigin()) / length; if(abs(distance) >= _spread.contact->objectRadius()) return; // Do not spread if the sector on the back side is closed with no height. if(!backCluster.hasWorldVolume()) return; if(backCluster.visCeiling().heightSmoothed() <= cluster.visFloor().heightSmoothed() || backCluster.visFloor().heightSmoothed() >= cluster.visCeiling().heightSmoothed()) return; // Are there line side surfaces which should prevent spreading? if(hedge->hasMapElement()) { LineSideSegment const &seg = hedge->mapElementAs(); // On which side of the line are we? (distance is from segment to origin). LineSide const &facingLineSide = seg.line().side(seg.lineSide().sideId() ^ (distance < 0)); // One-way window? if(!facingLineSide.back().hasSections()) return; SectorCluster const &fromCluster = facingLineSide.isFront()? cluster : backCluster; SectorCluster const &toCluster = facingLineSide.isFront()? backCluster : cluster; // Might a material cover the opening? if(facingLineSide.hasSections() && facingLineSide.middle().hasMaterial()) { // Stretched middles always cover the opening. if(facingLineSide.isFlagged(SDF_MIDDLE_STRETCH)) return; // Determine the opening between the visual sector planes at this edge. coord_t openBottom; if(toCluster.visFloor().heightSmoothed() > fromCluster.visFloor().heightSmoothed()) { openBottom = toCluster.visFloor().heightSmoothed(); } else { openBottom = fromCluster.visFloor().heightSmoothed(); } coord_t openTop; if(toCluster.visCeiling().heightSmoothed() < fromCluster.visCeiling().heightSmoothed()) { openTop = toCluster.visCeiling().heightSmoothed(); } else { openTop = fromCluster.visCeiling().heightSmoothed(); } MaterialAnimator &matAnimator = facingLineSide.middle().material().getAnimator(Rend_MapSurfaceMaterialSpec()); // Ensure we have up to date info about the material. matAnimator.prepare(); if(matAnimator.isOpaque() && matAnimator.dimensions().y >= openTop - openBottom) { // Possibly; check the placement. WallEdge edge(WallSpec::fromMapSide(facingLineSide, LineSide::Middle), *facingLineSide.leftHEdge(), Line::From); if(edge.isValid() && edge.top().z() > edge.bottom().z() && edge.top().z() >= openTop && edge.bottom().z() <= openBottom) return; } } } // During the next step this contact will spread from the back leaf. backSubspace.setValidCount(validCount); R_ContactList(backSubspace, _spread.contact->type()).link(_spread.contact); spreadInSubspace(backSubspace); } /** * Attempt to spread the obj from the given contact from the source subspace * and into the (relative) back subsapce. * * @param subspace Convex subspace to attempt to spread over to. * * @return Always @c true. (This function is also used as an iterator.) */ void spreadInSubspace(ConvexSubspace &subspace) { HEdge *base = subspace.poly().hedge(); HEdge *hedge = base; do { maybeSpreadOverEdge(hedge); } while((hedge = &hedge->next()) != base); } }; } // internal void spreadContacts(Blockmap const &blockmap, AABoxd const ®ion, QBitArray *spreadBlocks) { internal::ContactSpreader(blockmap, spreadBlocks).spread(region); } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/world/api_mapedit.cpp0000664000175000017500000003265612641367670024203 0ustar jaakkojaakko/** @file api_mapedit.cpp Internal runtime map editing interface. * * @authors Copyright © 2007-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #define DENG_NO_API_MACROS_MAP_EDIT #include #include #include #include "de_platform.h" #include "de_console.h" #include "dd_main.h" #include "world/entitydatabase.h" #include "world/map.h" #include "world/polyobjdata.h" #include "Plane" #include "Sector" #include "Surface" #include "edit_map.h" #include "api_mapedit.h" using namespace de; #define ERROR_IF_NOT_INITIALIZED() { \ if(!editMapInited) \ throw Error(QString("%s").arg(__FUNCTION__), "Not active, did you forget to call MPE_Begin()?"); \ } static Map *editMap; static bool editMapInited; /** * Material name references specified during map conversion are recorded in * this dictionary. A dictionary is used to avoid repeatedly resolving the same * URIs and to facilitate a log of missing materials encountered during the * process. * * The pointer user value holds a pointer to the resolved Material (if found). * The integer user value tracks the number of times a reference occurs. */ static StringPool *materialDict; /** * Either print or count-the-number-of unresolved references in the * material dictionary. * * @param internId Unique id associated with the reference. * @param context If a uint pointer operate in "count" mode (total written * here). Else operate in "print" mode. * @return Always @c 0 (for use as an iterator). */ static int printMissingMaterialWorker(StringPool::Id internId, void *context) { int *count = (int *)context; // A valid id? if(materialDict->string(internId)) { // Have we resolved this reference yet? if(!materialDict->userPointer(internId)) { // An unresolved reference. if(count) { // Count mode. *count += 1; } else { // Print mode. int const refCount = materialDict->userValue(internId); String const &materialUri = materialDict->string(internId); LOG_RES_WARNING("Found %4i x unknown material \"%s\"") << refCount << materialUri; } } } return 0; // Continue iteration. } /** * Destroy the missing material dictionary. */ static void clearMaterialDict() { // Initialized? if(!materialDict) return; materialDict->clear(); delete materialDict; materialDict = 0; } /** * Print any "missing" materials in the dictionary to the log. */ static void printMissingMaterialsInDict() { if(materialDict) { materialDict->iterate(printMissingMaterialWorker); } } /** * Attempt to locate a material by its URI. A dictionary of previously * searched-for URIs is maintained to avoid repeated searching and to record * "missing" materials. * * @param materialUriStr URI of the material to search for. * * @return Pointer to the found material; otherwise @c 0. */ static Material *findMaterialInDict(String const &materialUriStr) { if(materialUriStr.isEmpty()) return 0; // Time to create the dictionary? if(!materialDict) { materialDict = new StringPool; } de::Uri materialUri(materialUriStr, RC_NULL); // Intern this reference. StringPool::Id internId = materialDict->intern(materialUri.compose()); Material *material = 0; // Have we previously encountered this?. uint refCount = materialDict->userValue(internId); if(refCount) { // Yes, if resolved the user pointer holds the found material. material = (Material *) materialDict->userPointer(internId); } else { // No, attempt to resolve this URI and update the dictionary. // First try the preferred scheme, then any. try { material = &App_ResourceSystem().material(materialUri); } catch(ResourceSystem::MissingManifestError const &) { // Try any scheme. try { materialUri.setScheme(""); material = &App_ResourceSystem().material(materialUri); } catch(ResourceSystem::MissingManifestError const &) {} } // Insert the possibly resolved material into the dictionary. materialDict->setUserPointer(internId, material); } // There is now one more reference. refCount++; materialDict->setUserValue(internId, refCount); return material; } static inline Material *findMaterialInDict(ddstring_t const *materialUriStr) { if(!materialUriStr) return 0; return findMaterialInDict(Str_Text(materialUriStr)); } Map *MPE_Map() { return editMapInited? editMap : 0; } Map *MPE_TakeMap() { editMapInited = false; Map *retMap = editMap; editMap = 0; return retMap; } #undef MPE_Begin dd_bool MPE_Begin(uri_s const * /*mapUri*/) { if(!editMapInited) { delete editMap; editMap = new Map; editMapInited = true; } return true; } #undef MPE_End dd_bool MPE_End() { if(!editMapInited) return false; /* * Log warnings about any issues we encountered during conversion of * the basic map data elements. */ printMissingMaterialsInDict(); clearMaterialDict(); // Note the map is left in an editable state in case the caller decides // they aren't finished after all... return true; } #undef MPE_VertexCreate int MPE_VertexCreate(coord_t x, coord_t y, int archiveIndex) { ERROR_IF_NOT_INITIALIZED(); return editMap->createVertex(Vector2d(x, y), archiveIndex)->indexInMap(); } #undef MPE_VertexCreatev dd_bool MPE_VertexCreatev(int num, coord_t const *values, int *archiveIndices, int *retIndices) { ERROR_IF_NOT_INITIALIZED(); if(num <= 0 || !values) return false; // Create many vertexes. for(int n = 0; n < num; ++n) { Vertex *vertex = editMap->createVertex(Vector2d(values[n * 2], values[n * 2 + 1]), archiveIndices[n]); if(retIndices) { retIndices[n] = vertex->indexInMap(); } } return true; } #undef MPE_LineCreate int MPE_LineCreate(int v1, int v2, int frontSectorIdx, int backSectorIdx, int flags, int archiveIndex) { ERROR_IF_NOT_INITIALIZED(); if(frontSectorIdx >= editMap->editableSectorCount()) return -1; if(backSectorIdx >= editMap->editableSectorCount()) return -1; if(v1 < 0 || v1 >= editMap->vertexCount()) return -1; if(v2 < 0 || v2 >= editMap->vertexCount()) return -1; if(v1 == v2) return -1; // Next, check the length is not zero. /// @todo fixme: We need to allow these... -ds Vertex &vtx1 = editMap->vertex(v1); Vertex &vtx2 = editMap->vertex(v2); if(de::abs(Vector2d(vtx1.origin() - vtx2.origin()).length()) <= 0.0001) return -1; Sector *frontSector = (frontSectorIdx >= 0? editMap->editableSectors().at(frontSectorIdx) : 0); Sector *backSector = (backSectorIdx >= 0? editMap->editableSectors().at(backSectorIdx) : 0); return editMap->createLine(vtx1, vtx2, flags, frontSector, backSector, archiveIndex) ->indexInMap(); } #undef MPE_LineAddSide void MPE_LineAddSide(int lineIdx, int sideId, short flags, ddstring_t const *topMaterialUri, float topOffsetX, float topOffsetY, float topRed, float topGreen, float topBlue, ddstring_t const *middleMaterialUri, float middleOffsetX, float middleOffsetY, float middleRed, float middleGreen, float middleBlue, float middleOpacity, ddstring_t const *bottomMaterialUri, float bottomOffsetX, float bottomOffsetY, float bottomRed, float bottomGreen, float bottomBlue, int archiveIndex) { ERROR_IF_NOT_INITIALIZED(); if(lineIdx < 0 || lineIdx >= editMap->editableLineCount()) return; Line *line = editMap->editableLines().at(lineIdx); LineSide &side = line->side(sideId); side.setFlags(flags); side.setIndexInArchive(archiveIndex); // Ensure sections are defined if they aren't already. side.addSections(); // Assign the resolved material if found. side.top() .setMaterial(findMaterialInDict(topMaterialUri)) .setMaterialOrigin(Vector2f(topOffsetX, topOffsetY)) .setTintColor(Vector3f(topRed, topGreen, topBlue)); side.middle() .setMaterial(findMaterialInDict(middleMaterialUri)) .setMaterialOrigin(Vector2f(middleOffsetX, middleOffsetY)) .setTintColor(Vector3f(middleRed, middleGreen, middleBlue)) .setOpacity(middleOpacity); side.bottom() .setMaterial(findMaterialInDict(bottomMaterialUri)) .setMaterialOrigin(Vector2f(bottomOffsetX, bottomOffsetY)) .setTintColor(Vector3f(bottomRed, bottomGreen, bottomBlue)); } #undef MPE_PlaneCreate int MPE_PlaneCreate(int sectorIdx, coord_t height, ddstring_t const *materialUri, float matOffsetX, float matOffsetY, float tintRed, float tintGreen, float tintBlue, float opacity, float normalX, float normalY, float normalZ, int archiveIndex) { ERROR_IF_NOT_INITIALIZED(); if(sectorIdx < 0 || sectorIdx >= editMap->editableSectorCount()) return -1; Sector *sector = editMap->editableSectors().at(sectorIdx); Plane *plane = sector->addPlane(Vector3f(normalX, normalY, normalZ), height); plane->setIndexInArchive(archiveIndex); plane->surface() .setMaterial(findMaterialInDict(materialUri)) .setTintColor(Vector3f(tintRed, tintGreen, tintBlue)) .setMaterialOrigin(Vector2f(matOffsetX, matOffsetY)); if(!plane->isSectorFloor() && !plane->isSectorCeiling()) { plane->surface().setOpacity(opacity); } return plane->indexInSector(); } #undef MPE_SectorCreate int MPE_SectorCreate(float lightlevel, float red, float green, float blue, int archiveIndex) { ERROR_IF_NOT_INITIALIZED(); return editMap->createSector(lightlevel, Vector3f(red, green, blue), archiveIndex)->indexInMap(); } #undef MPE_PolyobjCreate int MPE_PolyobjCreate(int const *lines, int lineCount, int tag, int sequenceType, coord_t originX, coord_t originY, int archiveIndex) { DENG_UNUSED(archiveIndex); /// @todo Use this! ERROR_IF_NOT_INITIALIZED(); if(lineCount <= 0 || !lines) return -1; // First check that all the line indices are valid and that they arn't // already part of another polyobj. for(int i = 0; i < lineCount; ++i) { if(lines[i] < 0 || lines[i] >= editMap->editableLineCount()) return -1; Line *line = editMap->editableLines().at(lines[i]); if(line->definesPolyobj()) return -1; } Polyobj *po = editMap->createPolyobj(Vector2d(originX, originY)); po->setSequenceType(sequenceType); po->setTag(tag); for(int i = 0; i < lineCount; ++i) { Line *line = editMap->editableLines().at(lines[i]); // This line belongs to a polyobj. line->setPolyobj(po); po->data().lines.append(line); } return po->indexInMap(); } #undef MPE_GameObjProperty dd_bool MPE_GameObjProperty(char const *entityName, int elementIndex, char const *propertyName, valuetype_t valueType, void *valueAdr) { LOG_AS("MPE_GameObjProperty"); ERROR_IF_NOT_INITIALIZED(); if(!entityName || !propertyName || !valueAdr) return false; // Hmm... // Is this a known entity? MapEntityDef *entityDef = P_MapEntityDefByName(entityName); if(!entityDef) { LOG_WARNING("Unknown entity name:\"%s\", ignoring.") << entityName; return false; } // Is this a known property? MapEntityPropertyDef *propertyDef; if(MapEntityDef_PropertyByName(entityDef, propertyName, &propertyDef) < 0) { LOG_WARNING("Entity \"%s\" has no \"%s\" property, ignoring.") << entityName << propertyName; return false; } try { EntityDatabase &entities = editMap->entityDatabase(); entities.setProperty(propertyDef, elementIndex, valueType, valueAdr); return true; } catch(Error const &er) { LOG_WARNING("%s. Ignoring.") << er.asText(); } return false; } // p_data.cpp #undef P_RegisterMapObj DENG_EXTERN_C dd_bool P_RegisterMapObj(int identifier, char const *name); #undef P_RegisterMapObjProperty DENG_EXTERN_C dd_bool P_RegisterMapObjProperty(int entityId, int propertyId, char const *propertyName, valuetype_t type); DENG_DECLARE_API(MPE) = { { DE_API_MAP_EDIT }, P_RegisterMapObj, P_RegisterMapObjProperty, MPE_Begin, MPE_End, MPE_VertexCreate, MPE_VertexCreatev, MPE_LineCreate, MPE_LineAddSide, MPE_SectorCreate, MPE_PlaneCreate, MPE_PolyobjCreate, MPE_GameObjProperty }; doomsday-stable-1.15.7/doomsday/client/src/world/p_players.cpp0000664000175000017500000003142412641367670023715 0ustar jaakkojaakko/** @file p_players.cpp World player entities. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #define DENG_NO_API_MACROS_PLAYER #include "world/p_players.h" #include #include #include #include #include #ifdef __CLIENT__ # include "clientapp.h" #endif #include "world/impulseaccumulator.h" #include "world/map.h" #include "world/p_object.h" #include "SectorCluster" #include "Surface" #ifdef __CLIENT__ # include "BindContext" # include "ui/b_util.h" # include "ui/inputdevice.h" #endif using namespace de; player_t *viewPlayer; player_t ddPlayers[DDMAXPLAYERS]; int consolePlayer; int displayPlayer; typedef QMap Impulses; // ID lookup. static Impulses impulses; typedef QMap ImpulseNameMap; // Name lookup static ImpulseNameMap impulsesByName; typedef QMap ImpulseAccumulators; // ImpulseId lookup. static ImpulseAccumulators accumulators[DDMAXPLAYERS]; static PlayerImpulse *addImpulse(int id, impulsetype_t type, String name, String bindContextName) { auto *imp = new PlayerImpulse; imp->id = id; imp->type = type; imp->name = name; imp->bindContextName = bindContextName; impulses.insert(imp->id, imp); impulsesByName.insert(imp->name.toLower(), imp); // Generate impulse accumulators for each player. for(int i = 0; i < DDMAXPLAYERS; ++i) { ImpulseAccumulator::AccumulatorType accumType = (type == IT_BINARY? ImpulseAccumulator::Binary : ImpulseAccumulator::Analog); auto *accum = new ImpulseAccumulator(imp->id, accumType, type != IT_ANALOG); accum->setPlayerNum(i); accumulators[i].insert(accum->impulseId(), accum); } return imp; } static ImpulseAccumulator *accumulator(int impulseId, int playerNum) { if(playerNum < 0 || playerNum >= DDMAXPLAYERS) return nullptr; if(!accumulators[playerNum].contains(impulseId)) return nullptr; return accumulators[playerNum][impulseId]; } int P_LocalToConsole(int localPlayer) { int count = 0; for(int i = 0; i < DDMAXPLAYERS; ++i) { int console = (i + consolePlayer) % DDMAXPLAYERS; player_t *plr = &ddPlayers[console]; ddplayer_t *ddpl = &plr->shared; if(ddpl->flags & DDPF_LOCAL) { if(count++ == localPlayer) return console; } } // No match! return -1; } int P_ConsoleToLocal(int playerNum) { int i, count = 0; player_t *plr = &ddPlayers[playerNum]; if(playerNum < 0 || playerNum >= DDMAXPLAYERS) { // Invalid. return -1; } if(playerNum == consolePlayer) { return 0; } if(!(plr->shared.flags & DDPF_LOCAL)) return -1; // Not local at all. for(i = 0; i < DDMAXPLAYERS; ++i) { int console = (i + consolePlayer) % DDMAXPLAYERS; player_t *plr = &ddPlayers[console]; if(console == playerNum) { return count; } if(plr->shared.flags & DDPF_LOCAL) count++; } return -1; } int P_GetDDPlayerIdx(ddplayer_t *ddpl) { if(ddpl) for(uint i = 0; i < DDMAXPLAYERS; ++i) { if(&ddPlayers[i].shared == ddpl) return i; } return -1; } bool P_IsInVoid(player_t *player) { if(!player) return false; ddplayer_t *ddpl = &player->shared; // Cameras are allowed to move completely freely (so check z height // above/below ceiling/floor). if(ddpl->flags & DDPF_CAMERA) { if(ddpl->inVoid || !ddpl->mo) return true; mobj_t *mo = ddpl->mo; if(!Mobj_HasSubspace(*mo)) return true; SectorCluster &cluster = Mobj_Cluster(*mo); #ifdef __CLIENT__ if(cluster.visCeiling().surface().hasSkyMaskedMaterial()) { coord_t const skyCeil = cluster.sector().map().skyFixCeiling(); if(skyCeil < DDMAXFLOAT && mo->origin[VZ] > skyCeil - 4) return true; } else if(mo->origin[VZ] > cluster.visCeiling().heightSmoothed() - 4) #else if(mo->origin[VZ] > cluster.ceiling().height() - 4) #endif { return true; } #ifdef __CLIENT__ if(cluster.visFloor().surface().hasSkyMaskedMaterial()) { coord_t const skyFloor = cluster.sector().map().skyFixFloor(); if(skyFloor > DDMINFLOAT && mo->origin[VZ] < skyFloor + 4) return true; } else if(mo->origin[VZ] < cluster.visFloor().heightSmoothed() + 4) #else if(mo->origin[VZ] < cluster.floor().height() + 4) #endif { return true; } } return false; } short P_LookDirToShort(float lookDir) { int dir = int( lookDir/110.f * DDMAXSHORT ); if(dir < DDMINSHORT) return DDMINSHORT; if(dir > DDMAXSHORT) return DDMAXSHORT; return (short) dir; } float P_ShortToLookDir(short s) { return s / float( DDMAXSHORT ) * 110.f; } void P_ClearPlayerImpulses() { for(int i = 0; i < DDMAXPLAYERS; ++i) { qDeleteAll(accumulators[i]); accumulators[i].clear(); } qDeleteAll(impulses); impulses.clear(); impulsesByName.clear(); } PlayerImpulse *P_PlayerImpulsePtr(int id) { auto found = impulses.find(id); if(found != impulses.end()) return *found; return nullptr; } PlayerImpulse *P_PlayerImpulseByName(String const &name) { if(!name.isEmpty()) { auto found = impulsesByName.find(name.toLower()); if(found != impulsesByName.end()) return *found; } return nullptr; } D_CMD(ListImpulses) { DENG2_UNUSED3(argv, argc, src); // Group the defined impulses by binding context. typedef QList ImpulseList; QMap contextGroups; for(PlayerImpulse *imp : impulsesByName) { if(!contextGroups.contains(imp->bindContextName)) { contextGroups.insert(imp->bindContextName, ImpulseList()); } contextGroups[imp->bindContextName].append(imp); } LOG_MSG(_E(b) "Player impulses"); LOG_MSG("There are " _E(b) "%i" _E(.) " impulses, in " _E(b) "%i" _E(.) " contexts") << impulses.count() << contextGroups.count(); for(auto const &group : contextGroups) { if(group.isEmpty()) continue; LOG_MSG(_E(D)_E(b) "%s" _E(.) " context: " _E(l) "(%i)") << group.first()->bindContextName << group.count(); for(PlayerImpulse const *imp : group) { LOG_MSG(" [%4i] " _E(>) _E(b) "%s " _E(.) _E(2) "%s%s") << imp->id << imp->name << (imp->type == IT_BINARY? "binary" : "analog") << (IMPULSETYPE_IS_TRIGGERABLE(imp->type)? ", triggerable" : ""); } } return true; } D_CMD(Impulse) { DENG2_UNUSED(src); if(argc < 2 || argc > 3) { LOG_SCR_NOTE("Usage:\n %s (impulse-name)\n %s (impulse-name) (player-ordinal)") << argv[0] << argv[0]; return true; } if(PlayerImpulse *imp = P_PlayerImpulseByName(argv[1])) { int const playerNum = (argc == 3? P_LocalToConsole(String(argv[2]).toInt()) : consolePlayer); if(ImpulseAccumulator *accum = accumulator(imp->id, playerNum)) { accum->receiveBinary(); } } return true; } #ifdef __CLIENT__ D_CMD(ClearImpulseAccumulation) { DENG2_UNUSED3(argv, argc, src); ClientApp::inputSystem().forAllDevices([] (InputDevice &device) { device.reset(); return LoopContinue; }); // For all players. for(int i = 0; i < DDMAXPLAYERS; ++i) for(ImpulseAccumulator *accum : accumulators[i]) { accum->clearAll(); } return true; } #endif void P_ConsoleRegister() { C_CMD("listcontrols", "", ListImpulses); C_CMD("impulse", nullptr, Impulse); #ifdef __CLIENT__ C_CMD("resetctlaccum", "", ClearImpulseAccumulation); ImpulseAccumulator::consoleRegister(); #endif } DENG_EXTERN_C ddplayer_t *DD_GetPlayer(int number) { return &ddPlayers[number].shared; } // net_main.c DENG_EXTERN_C char const *Net_GetPlayerName(int player); DENG_EXTERN_C ident_t Net_GetPlayerID(int player); DENG_EXTERN_C Smoother *Net_PlayerSmoother(int player); DENG_EXTERN_C void P_NewPlayerControl(int id, impulsetype_t type, char const *name, char const *bindContextName) { DENG2_ASSERT(name && bindContextName); LOG_AS("P_NewPlayerControl"); // Ensure the given id is unique. if(PlayerImpulse const *existing = P_PlayerImpulsePtr(id)) { LOG_INPUT_WARNING("Id: %i is already in use by impulse '%s' - Won't replace") << id << existing->name; return; } // Ensure the given name is unique. if(PlayerImpulse const *existing = P_PlayerImpulseByName(name)) { LOG_INPUT_WARNING("Name: '%s' is already in use by impulse Id: %i - Won't replace") << name << existing->id; return; } addImpulse(id, type, name, bindContextName); } DENG_EXTERN_C int P_IsControlBound(int playerNum, int impulseId) { #ifdef __CLIENT__ LOG_AS("P_IsControlBound"); // Impulse bindings are associated with local player numbers rather than // the player console number - translate. int const localPlayer = P_ConsoleToLocal(playerNum); if(localPlayer < 0) return false; if(PlayerImpulse const *imp = P_PlayerImpulsePtr(impulseId)) { InputSystem &isys = ClientApp::inputSystem(); BindContext *bindContext = isys.contextPtr(imp->bindContextName); if(!bindContext) { LOGDEV_INPUT_WARNING("Unknown binding context '%s'") << imp->bindContextName; return false; } int found = bindContext->forAllImpulseBindings(localPlayer, [&isys, &impulseId] (Record &rec) { ImpulseBinding bind(rec); // Wrong impulse? if(bind.geti("impulseId") != impulseId) return LoopContinue; if(InputDevice const *device = isys.devicePtr(bind.geti("deviceId"))) { if(device->isActive()) { return LoopAbort; // found a binding. } } return LoopContinue; }); return (found? 1 : 0); } return false; #else DENG2_UNUSED2(playerNum, impulseId); return 0; #endif } DENG_EXTERN_C void P_GetControlState(int playerNum, int impulseId, float *pos, float *relativeOffset) { #ifdef __CLIENT__ // Ignore NULLs. float tmp; if(!pos) pos = &tmp; if(!relativeOffset) relativeOffset = &tmp; *pos = 0; *relativeOffset = 0; if(ImpulseAccumulator *accum = accumulator(impulseId, playerNum)) { accum->takeAnalog(pos, relativeOffset); } #else DENG2_UNUSED4(playerNum, impulseId, pos, relativeOffset); #endif } DENG_EXTERN_C int P_GetImpulseControlState(int playerNum, int impulseId) { LOG_AS("P_GetImpulseControlState"); ImpulseAccumulator *accum = accumulator(impulseId, playerNum); if(!accum) return 0; // Ensure this is really a binary accumulator. if(accum->type() != ImpulseAccumulator::Binary) { LOG_INPUT_WARNING("ImpulseAccumulator '%s' is not binary") << impulses[impulseId]->name; return 0; } return accum->takeBinary(); } DENG_EXTERN_C void P_Impulse(int playerNum, int impulseId) { LOG_AS("P_Impulse"); ImpulseAccumulator *accum = accumulator(impulseId, playerNum); if(!accum) return; // Ensure this is really a binary accumulator. if(accum->type() != ImpulseAccumulator::Binary) { LOG_INPUT_WARNING("ImpulseAccumulator '%s' is not binary") << impulses[impulseId]->name; return; } accum->receiveBinary(); } DENG_DECLARE_API(Player) = { { DE_API_PLAYER }, Net_GetPlayerName, Net_GetPlayerID, Net_PlayerSmoother, DD_GetPlayer, P_NewPlayerControl, P_IsControlBound, P_GetControlState, P_GetImpulseControlState, P_Impulse }; doomsday-stable-1.15.7/doomsday/client/src/world/worldsystem.cpp0000664000175000017500000006764012641367670024324 0ustar jaakkojaakko/** @file worldsystem.cpp World subsystem. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/worldsystem.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "de_defs.h" #include "de_play.h" #include "de_filesys.h" #include "dd_main.h" #include "dd_def.h" #include "dd_loop.h" #include "audio/s_main.h" #include "network/net_main.h" #include "edit_map.h" #include "Plane" #include "Sector" #include "SectorCluster" #include "world/p_ticker.h" #include "world/sky.h" #include "world/thinkers.h" #include "ui/progress.h" #ifdef __CLIENT__ # include "clientapp.h" # include "client/cl_def.h" # include "client/cl_frame.h" # include "client/cl_player.h" # include "edit_bias.h" # include "Hand" # include "HueCircle" # include "gl/gl_main.h" # include "Lumobj" # include "MaterialAnimator" # include "render/viewports.h" // R_ResetViewer # include "render/rend_fakeradio.h" # include "render/rend_main.h" # include "render/skydrawable.h" #endif #ifdef __SERVER__ # include "server/sv_pool.h" #endif using namespace de; int validCount = 1; // Increment every time a check is made. #ifdef __CLIENT__ static float handDistance = 300; //cvar #endif static inline ResourceSystem &resSys() { return App_ResourceSystem(); } #ifdef __CLIENT__ static inline RenderSystem &rendSys() { return ClientApp::renderSystem(); } #endif /** * Observes the progress of a map conversion and records any issues/problems that * are encountered in the process. When asked, compiles a human-readable report * intended to assist mod authors in debugging their maps. * * @todo Consolidate with the missing material reporting done elsewhere -ds */ class MapConversionReporter : DENG2_OBSERVES(Map, UnclosedSectorFound) , DENG2_OBSERVES(Map, OneWayWindowFound) , DENG2_OBSERVES(Map, Deletion) { /// Record "unclosed sectors". /// Sector index => world point relatively near to the problem area. typedef std::map UnclosedSectorMap; /// Record "one-way window lines". /// Line index => Sector index the back side faces. typedef std::map OneWayWindowMap; /// Maximum number of warnings to output (of each type) about any problems /// encountered during the build process. static int const maxWarningsPerType; public: /** * Construct a new conversion reporter. * @param map */ MapConversionReporter(Map *map = 0) : _map(0) { setMap(map); } ~MapConversionReporter() { observeMap(false); } /** * Change the map to be reported on. Note that any existing report data is * retained until explicitly cleared. */ void setMap(Map *newMap) { if(_map != newMap) { observeMap(false); _map = newMap; observeMap(true); } } /// @see setMap(), clearReport() inline void setMapAndClearReport(Map *newMap) { setMap(newMap); clearReport(); } /// Same as @code setMap(0); @endcode inline void clearMap() { setMap(0); } /** * Clear any existing conversion report data. */ void clearReport() { _unclosedSectors.clear(); _oneWayWindows.clear(); } /** * Compile and output any existing report data to the message log. */ void writeLog() { if(int numToLog = maxWarnings(unclosedSectorCount())) { String str; UnclosedSectorMap::const_iterator it = _unclosedSectors.begin(); for(int i = 0; i < numToLog; ++i, ++it) { if(i != 0) str += "\n"; str += String("Sector #%1 is unclosed near %2") .arg(it->first).arg(it->second.asText()); } if(numToLog < unclosedSectorCount()) str += String("\n(%1 more like this)").arg(unclosedSectorCount() - numToLog); LOG_MAP_WARNING("%s") << str; } if(int numToLog = maxWarnings(oneWayWindowCount())) { String str; OneWayWindowMap::const_iterator it = _oneWayWindows.begin(); for(int i = 0; i < numToLog; ++i, ++it) { if(i != 0) str += "\n"; str += String("Line #%1 seems to be a One-Way Window (back faces sector #%2).") .arg(it->first).arg(it->second); } if(numToLog < oneWayWindowCount()) str += String("\n(%1 more like this)").arg(oneWayWindowCount() - numToLog); LOG_MAP_VERBOSE("%s") << str; } } protected: /// Observes Map UnclosedSectorFound. void unclosedSectorFound(Sector §or, Vector2d const &nearPoint) { _unclosedSectors.insert(std::make_pair(sector.indexInArchive(), nearPoint.toVector2i())); } /// Observes Map OneWayWindowFound. void oneWayWindowFound(Line &line, Sector &backFacingSector) { _oneWayWindows.insert(std::make_pair(line.indexInArchive(), backFacingSector.indexInArchive())); } /// Observes Map Deletion. void mapBeingDeleted(Map const &map) { DENG2_ASSERT(&map == _map); // sanity check. DENG2_UNUSED(map); _map = 0; } private: inline int unclosedSectorCount() const { return int( _unclosedSectors.size() ); } inline int oneWayWindowCount() const { return int( _oneWayWindows.size() ); } static inline int maxWarnings(int issueCount) { #ifdef DENG_DEBUG return issueCount; // No limit. #else return de::min(issueCount, maxWarningsPerType); #endif } void observeMap(bool yes) { if(!_map) return; if(yes) { _map->audienceForDeletion += this; _map->audienceForOneWayWindowFound += this; _map->audienceForUnclosedSectorFound += this; } else { _map->audienceForDeletion -= this; _map->audienceForOneWayWindowFound -= this; _map->audienceForUnclosedSectorFound -= this; } } Map *_map = nullptr; ///< Map currently being reported on, if any (not owned). UnclosedSectorMap _unclosedSectors; OneWayWindowMap _oneWayWindows; }; int const MapConversionReporter::maxWarningsPerType = 10; dd_bool ddMapSetup; // Should we be caching successfully loaded maps? //static byte mapCache = true; // cvar static char const *mapCacheDir = "mapcache/"; /// Determine the identity key for maps loaded from the specified @a sourcePath. static String cacheIdForMap(String const &sourcePath) { DENG2_ASSERT(!sourcePath.isEmpty()); ushort id = 0; for(int i = 0; i < sourcePath.size(); ++i) { id ^= sourcePath.at(i).unicode() << ((i * 3) % 11); } return String("%1").arg(id, 4, 16); } namespace de { DENG2_PIMPL(WorldSystem) { Map *map = nullptr; ///< Current map. Record fallbackMapInfo; ///< Used when no effective MapInfo definition. timespan_t time = 0; ///< World-wide time. #ifdef __CLIENT__ std::unique_ptr hand; ///< For map editing/manipulation. SkyDrawable::Animator skyAnimator; #endif Instance(Public *i) : Base(i) { // One time init of the fallback MapInfo definition. defn::MapInfo(fallbackMapInfo).resetToDefaults(); } /** * Compose the relative path (relative to the runtime directory) to the * directory of the cache where maps from this source (e.g., the add-on * which contains the map) will reside. * * @param sourcePath Path to the primary resource file (the source) for * the original map data. * * @return The composed path. */ static Path cachePath(String sourcePath) { if(sourcePath.isEmpty()) return String(); // Compose the final path. return mapCacheDir + App_CurrentGame().identityKey() / sourcePath.fileNameWithoutExtension() + '-' + cacheIdForMap(sourcePath); } /** * Attempt JIT conversion of the map data with the help of a plugin. Note that * the map is left in an editable state in case the caller wishes to perform * any further changes. * * @param reporter Reporter which will observe the conversion process. * * @return The newly converted map (if any). */ Map *convertMap(MapDef const &mapDef, MapConversionReporter *reporter = 0) { // We require a map converter for this. if(!Plug_CheckForHook(HOOK_MAP_CONVERT)) return 0; LOG_DEBUG("Attempting \"%s\"...") << mapDef.composeUri().path(); if(!mapDef.sourceFile()) return 0; // Initiate the conversion process. MPE_Begin(0/*dummy*/); Map *newMap = MPE_Map(); // Associate the map with its corresponding definition. newMap->setDef(&const_cast(mapDef)); if(reporter) { // Instruct the reporter to begin observing the conversion. reporter->setMap(newMap); } // Ask each converter in turn whether the map format is recognizable // and if so to interpret and transfer it to us via the runtime map // editing interface. if(!DD_CallHooks(HOOK_MAP_CONVERT, 0, const_cast(&mapDef.recognizer()))) return 0; // A converter signalled success. // End the conversion process (if not already). MPE_End(); // Take ownership of the map. return MPE_TakeMap(); } #if 0 /** * Returns @c true iff data for the map is available in the cache. */ bool haveCachedMap(MapDef &mapDef) { // Disabled? if(!mapCache) return false; return DAM_MapIsValid(mapDef.cachePath, mapDef.id()); } /** * Attempt to load data for the map from the cache. * * @see isCachedDataAvailable() * * @return @c true if loading completed successfully. */ Map *loadMapFromCache(MapDef &mapDef) { Uri const mapUri = mapDef.composeUri(); Map *map = DAM_MapRead(mapDef.cachePath); if(!map) /// Failed to load the map specified from the data cache. throw Error("loadMapFromCache", "Failed loading map \"" + mapUri.asText() + "\" from cache"); map->_uri = mapUri; return map; } #endif /** * Attempt to load the associated map data. * * @return The loaded map if successful. Ownership given to the caller. */ Map *loadMap(MapDef &mapDef, MapConversionReporter *reporter = 0) { LOG_AS("WorldSystem::loadMap"); /*if(mapDef.lastLoadAttemptFailed && !forceRetry) return 0; // Load from cache? if(haveCachedMap(mapDef)) { return loadMapFromCache(mapDef); }*/ // Try a JIT conversion with the help of a plugin. Map *map = convertMap(mapDef, reporter); if(!map) { LOG_WARNING("Failed conversion of \"%s\".") << mapDef.composeUri().path(); //mapDef.lastLoadAttemptFailed = true; } return map; } /** * Replace the current map with @a map. */ void makeCurrent(Map *newMap) { // This is now the current map (if any). map = newMap; if(!map) return; // We cannot make an editable map current. DENG2_ASSERT(!map->isEditable()); // Should we cache this map? /*if(mapCache && !haveCachedMap(&map->def())) { // Ensure the destination directory exists. F_MakePath(map->def().cachePath.toUtf8().constData()); // Cache the map! DAM_MapWrite(map); }*/ #ifdef __CLIENT__ // Connect the map to world audiences: /// @todo The map should instead be notified when it is made current /// so that it may perform the connection itself. Such notification /// would also afford the map the opportunity to prepare various data /// which is only needed when made current (e.g., caches for render). self.audienceForFrameBegin() += map; #endif // Print summary information about this map. LOG_MAP_NOTE(_E(b) "Current map elements:"); LOG_MAP_NOTE("%s") << map->elementSummaryAsStyledText(); // See what MapInfo says about this map. Record const &mapInfo = map->mapInfo(); map->_ambientLightLevel = mapInfo.getf("ambient") * 255; map->_globalGravity = mapInfo.getf("gravity"); map->_effectiveGravity = map->_globalGravity; #ifdef __CLIENT__ // Reconfigure the sky. defn::Sky skyDef; if(Record const *def = defs.skies.tryFind("id", mapInfo.gets("skyId"))) { skyDef = *def; } else { skyDef = mapInfo.subrecord("sky"); } map->sky().configure(&skyDef); // Set up the SkyDrawable to get its config from the map's Sky. skyAnimator.setSky(&rendSys().sky().configure(&map->sky())); #endif // Init the thinker lists (public and private). map->thinkers().initLists(0x1 | 0x2); // Must be called before we go any further. P_InitUnusedMobjList(); // Must be called before any mobjs are spawned. map->initNodePiles(); #ifdef __CLIENT__ // Prepare the client-side data. Cl_ResetFrame(); Cl_InitPlayers(); // Player data, too. /// @todo Defer initial generator spawn until after finalization. map->initGenerators(); #endif // The game may need to perform it's own finalization now that the // "current" map has changed. de::Uri const mapUri = (map->def()? map->def()->composeUri() : de::Uri("Maps:", RC_NULL)); if(gx.FinalizeMapChange) { gx.FinalizeMapChange(reinterpret_cast(&mapUri)); } if(gameTime > 20000000 / TICSPERSEC) { // In very long-running games, gameTime will become so large that // it cannot be accurately converted to 35 Hz integer tics. Thus it // needs to be reset back to zero. gameTime = 0; } // Init player values. for(uint i = 0; i < DDMAXPLAYERS; ++i) { player_t *plr = &ddPlayers[i]; ddplayer_t &ddpl = ddPlayers[i].shared; plr->extraLight = plr->targetExtraLight = 0; plr->extraLightCounter = 0; // Determine the "invoid" status. ddpl.inVoid = true; if(mobj_t *mo = ddpl.mo) { if(SectorCluster *cluster = Mobj_ClusterPtr(*mo)) { #ifdef __CLIENT__ if(mo->origin[VZ] >= cluster->visFloor().heightSmoothed() && mo->origin[VZ] < cluster->visCeiling().heightSmoothed() - 4) #else if(mo->origin[VZ] >= cluster->floor().height() && mo->origin[VZ] < cluster->ceiling().height() - 4) #endif { ddpl.inVoid = false; } } } } #ifdef __CLIENT__ /// @todo Refactor away: map->forAllSectors([] (Sector §or) { sector.forAllSides([] (LineSide &side) { side.fixMissingMaterials(); return LoopContinue; }); return LoopContinue; }); #endif map->initPolyobjs(); S_SetupForChangedMap(); #ifdef __SERVER__ if(isServer) { // Init server data. Sv_InitPools(); } #endif #ifdef __CLIENT__ GL_SetupFogFromMapInfo(mapInfo.accessedRecordPtr()); map->initLightGrid(); map->initSkyFix(); map->buildMaterialLists(); map->spawnPlaneParticleGens(); // Precaching from 100 to 200. Con_SetProgress(100); Time begunPrecacheAt; // Sky models usually have big skins. rendSys().sky().cacheAssets(); resSys().cacheForCurrentMap(); resSys().processCacheQueue(); LOG_RES_VERBOSE("Precaching completed in %.2f seconds") << begunPrecacheAt.since(); rendSys().clearDrawLists(); R_InitRendPolyPools(); Rend_UpdateLightModMatrix(); Rend_RadioInitForMap(*map); map->initContactBlockmaps(); R_InitContactLists(*map); rendSys().worldSystemMapChanged(*map); map->initBias(); // Shadow bias sources and surfaces. // Rewind/restart material animators. /// @todo Only rewind animators responsible for map-surface contexts. resSys().forAllMaterials([] (Material &material) { return material.forAllAnimators([] (MaterialAnimator &animator) { animator.rewind(); return LoopContinue; }); }); #endif /* * Post-change map setup has now been fully completed. */ // Run any commands specified in MapInfo. String execute = mapInfo.gets("execute"); if(!execute.isEmpty()) { Con_Execute(CMDS_SCRIPT, execute.toUtf8().constData(), true, false); } // Run the special map setup command, which the user may alias to do // something useful. if(!mapUri.isEmpty()) { String cmd = String("init-") + mapUri.path(); if(Con_IsValidCommand(cmd.toUtf8().constData())) { Con_Executef(CMDS_SCRIPT, false, "%s", cmd.toUtf8().constData()); } } // Reset world time. time = 0; // Now that the setup is done, let's reset the timer so that it will // appear that no time has passed during the setup. DD_ResetTimer(); #ifdef __CLIENT__ // Make sure that the next frame doesn't use a filtered viewer. R_ResetViewer(); // Clear any input events that might have accumulated during setup. ClientApp::inputSystem().clearEvents(); // Inform the timing system to suspend the starting of the clock. firstFrameAfterLoad = true; #endif Z_PrintStatus(); // Inform interested parties that the "current" map has changed. notifyMapChange(); } /// @todo Split this into subtasks (load, make current, cache assets). bool changeMap(MapDef *mapDef = 0) { #ifdef __CLIENT__ if(map) { // Remove the current map from our audiences. /// @todo Map should handle this. self.audienceForFrameBegin() -= map; } #endif // As the memory zone does not provide the mechanisms to prepare another // map in parallel we must free the current map first. /// @todo The memory zone would still be useful if the purge and tagging /// mechanisms allowed more fine grained control. It is no longer useful /// for allocating memory used elsewhere so it should be repurposed for /// this usage specifically. #ifdef __CLIENT__ R_DestroyContactLists(); #endif delete map; map = 0; Z_FreeTags(PU_MAP, PU_PURGELEVEL - 1); // Are we just unloading the current map? if(!mapDef) return true; LOG_MSG("Loading map \"%s\"...") << mapDef->composeUri().path(); // A new map is about to be set up. ddMapSetup = true; // Attempt to load in the new map. MapConversionReporter reporter; Map *newMap = loadMap(*mapDef, &reporter); if(newMap) { // The map may still be in an editable state -- switch to playable. bool mapIsPlayable = newMap->endEditing(); // Cancel further reports about the map. reporter.setMap(0); if(!mapIsPlayable) { // Darn. Discard the useless data. delete newMap; newMap = 0; } } // This becomes the new current map. makeCurrent(newMap); // We've finished setting up the map. ddMapSetup = false; // Output a human-readable report of any issues encountered during conversion. reporter.writeLog(); return map != 0; } struct changemapworker_params_t { Instance *inst; MapDef *mapDef; }; static int changeMapWorker(void *context) { changemapworker_params_t &p = *static_cast(context); int result = p.inst->changeMap(p.mapDef); BusyMode_WorkerEnd(); return result; } #ifdef __CLIENT__ void updateHandOrigin() { DENG2_ASSERT(hand != 0 && map != 0); viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); hand->setOrigin(viewData->current.origin + viewData->frontVec.xzy() * handDistance); } #endif void notifyMapChange() { DENG2_FOR_PUBLIC_AUDIENCE2(MapChange, i) i->worldSystemMapChanged(); } DENG2_PIMPL_AUDIENCE(MapChange) #ifdef __CLIENT__ DENG2_PIMPL_AUDIENCE(FrameBegin) DENG2_PIMPL_AUDIENCE(FrameEnd) #endif }; DENG2_AUDIENCE_METHOD(WorldSystem, MapChange) #ifdef __CLIENT__ DENG2_AUDIENCE_METHOD(WorldSystem, FrameBegin) DENG2_AUDIENCE_METHOD(WorldSystem, FrameEnd) #endif WorldSystem::WorldSystem() : d(new Instance(this)) {} void WorldSystem::timeChanged(Clock const &) { // Nothing to do. } bool WorldSystem::hasMap() const { return d->map != 0; } Map &WorldSystem::map() const { if(d->map) { return *d->map; } /// @throw MapError Attempted with no map loaded. throw MapError("WorldSystem::map", "No map is currently loaded"); } bool WorldSystem::changeMap(de::Uri const &mapUri) { MapDef *mapDef = 0; if(!mapUri.path().isEmpty()) { mapDef = resSys().mapDef(mapUri); } // Switch to busy mode (if we haven't already) except when simply unloading. if(!mapUri.path().isEmpty() && !BusyMode_Active()) { Instance::changemapworker_params_t parm; parm.inst = d; parm.mapDef = mapDef; BusyTask task; zap(task); /// @todo Use progress bar mode and update progress during the setup. task.mode = BUSYF_ACTIVITY | /*BUSYF_PROGRESS_BAR |*/ BUSYF_TRANSITION | (verbose? BUSYF_CONSOLE_OUTPUT : 0); task.name = "Loading map..."; task.worker = Instance::changeMapWorker; task.workerData = &parm; return CPP_BOOL(BusyMode_RunTask(&task)); } else { return d->changeMap(mapDef); } } void WorldSystem::reset() { for(int i = 0; i < DDMAXPLAYERS; ++i) { player_t *plr = &ddPlayers[i]; ddplayer_t *ddpl = &plr->shared; // Mobjs go down with the map. ddpl->mo = 0; // States have changed, the state pointers are unknown. ddpl->pSprites[0].statePtr = ddpl->pSprites[1].statePtr = 0; //ddpl->inGame = false; ddpl->flags &= ~DDPF_CAMERA; ddpl->fixedColorMap = 0; ddpl->extraLight = 0; } #ifdef __CLIENT__ if(isClient) { Cl_ResetFrame(); Cl_InitPlayers(); } #endif // If a map is currently loaded -- unload it. unloadMap(); } void WorldSystem::update() { for(int i = 0; i < DDMAXPLAYERS; ++i) { player_t *plr = &ddPlayers[i]; ddplayer_t *ddpl = &plr->shared; // States have changed, the state pointers are unknown. ddpl->pSprites[0].statePtr = ddpl->pSprites[1].statePtr = 0; } // Update the current map too. if(d->map) { d->map->update(); } } Record const &WorldSystem::mapInfoForMapUri(de::Uri const &mapUri) const { // Is there a MapInfo definition for the given URI? if(Record const *def = defs.mapInfos.tryFind("id", mapUri.compose())) { return *def; } // Is there is a default definition (for all maps)? if(Record const *def = defs.mapInfos.tryFind("id", de::Uri("Maps", Path("*")).compose())) { return *def; } // Use the fallback. return d->fallbackMapInfo; } void WorldSystem::advanceTime(timespan_t delta) { #ifdef __CLIENT__ if(clientPaused) return; #endif d->time += delta; } timespan_t WorldSystem::time() const { return d->time; } void WorldSystem::tick(timespan_t elapsed) { #ifdef __CLIENT__ d->skyAnimator.advanceTime(elapsed); if(DD_IsSharpTick() && d->map) { d->map->thinkers().forAll(reinterpret_cast(gx.MobjThinker), 0x1, [] (thinker_t *th) { Mobj_AnimateHaloOcclussion(*reinterpret_cast(th)); return LoopContinue; }); } #else DENG2_UNUSED(elapsed); #endif } #ifdef __CLIENT__ SkyDrawable::Animator &WorldSystem::skyAnimator() const { return d->skyAnimator; } Hand &WorldSystem::hand(coord_t *distance) const { // Time to create the hand? if(!d->hand) { d->hand.reset(new Hand()); audienceForFrameEnd() += *d->hand; if(d->map) { d->updateHandOrigin(); } } if(distance) { *distance = handDistance; } return *d->hand; } void WorldSystem::beginFrame(bool resetNextViewer) { // Notify interested parties that a new frame has begun. DENG2_FOR_AUDIENCE2(FrameBegin, i) i->worldSystemFrameBegins(resetNextViewer); } void WorldSystem::endFrame() { if(d->map && d->hand) { d->updateHandOrigin(); // If the HueCircle is active update the current edit color. if(HueCircle *hueCircle = SBE_HueCircle()) { viewdata_t const *viewData = R_ViewData(viewPlayer - ddPlayers); d->hand->setEditColor(hueCircle->colorAt(viewData->frontVec)); } } // Notify interested parties that the current frame has ended. DENG2_FOR_AUDIENCE2(FrameEnd, i) i->worldSystemFrameEnds(); } bool WorldSystem::isPointInVoid(Vector3d const &pos) const { // Everything is void if there is no map. if(!hasMap()) return true; SectorCluster const *cluster = map().clusterAt(pos); if(!cluster) return true; // Check the planes of the cluster. if(cluster->visCeiling().surface().hasSkyMaskedMaterial()) { coord_t const skyCeil = cluster->sector().map().skyFixCeiling(); if(skyCeil < DDMAXFLOAT && pos.z > skyCeil) return true; } else if(pos.z > cluster->visCeiling().heightSmoothed()) { return true; } if(cluster->visFloor().surface().hasSkyMaskedMaterial()) { coord_t const skyFloor = cluster->sector().map().skyFixFloor(); if(skyFloor > DDMINFLOAT && pos.z < skyFloor) return true; } else if(pos.z < cluster->visFloor().heightSmoothed()) { return true; } return false; // Not in the void. } #endif // __CLIENT__ void WorldSystem::consoleRegister() // static { //C_VAR_BYTE ("map-cache", &mapCache, 0, 0, 1); #ifdef __CLIENT__ C_VAR_FLOAT("edit-bias-grab-distance", &handDistance, 0, 10, 1000); #endif Map::consoleRegister(); } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/world/impulseaccumulator.cpp0000664000175000017500000001731312641367670025636 0ustar jaakkojaakko/** @file impulseaccumulator.cpp Player impulse accumulation. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "world/impulseaccumulator.h" #include #include #ifdef __CLIENT__ # include "clientapp.h" #endif #include "world/p_players.h" #ifdef __CLIENT__ # include "BindContext" # include "ui/b_util.h" #endif using namespace de; #ifdef __CLIENT__ static inline InputSystem &inputSys() { return ClientApp::inputSystem(); } #endif #ifdef __CLIENT__ static int pimpDoubleClickThreshold = 300; ///< Milliseconds, cvar #endif DENG2_PIMPL_NOREF(ImpulseAccumulator) { int impulseId = 0; int playerNum = 0; AccumulatorType type = Analog; bool expireBeforeSharpTick = false; short binaryAccum = 0; inline PlayerImpulse &getImpulse() const { auto *impulse = P_PlayerImpulsePtr(impulseId); DENG2_ASSERT(impulse); return *impulse; } #ifdef __CLIENT__ /** * Double-"clicks" actually mean double activations that occur within the * double-click threshold. This is to allow double-clicks also from the * analog impulses. */ struct DoubleClick { enum State { None, Positive, Negative }; bool triggered = false; //< True if double-click has been detected. uint previousClickTime = 0; //< Previous time an activation occurred. State lastState = None; //< State at the previous time the check was made. State previousClickState = None; /** Previous click state. When duplicated, triggers the double click. */ } db; /** * Track the double-click state of the impulse and generate a bindable * symbolic event if the trigger conditions are met. * * @param pos State of the impulse. */ void maintainDoubleClick(float pos) { if(pimpDoubleClickThreshold <= 0) { // Let's not waste time here. db.triggered = false; db.previousClickTime = 0; db.previousClickState = DoubleClick::None; return; } DoubleClick::State newState = DoubleClick::None; if(pos > .5) { newState = DoubleClick::Positive; } else if(pos < -.5) { newState = DoubleClick::Negative; } else { db.lastState = newState; // Release. return; } // But has it actually changed? if(newState == db.lastState) { return; } // We have an potential activation! uint const threshold = uint( de::max(0, pimpDoubleClickThreshold) ); uint const nowTime = Timer_RealMilliseconds(); if(newState == db.previousClickState && nowTime - db.previousClickTime < threshold) { db.triggered = true; PlayerImpulse const &impulse = getImpulse(); // Compose the name of the symbolic event. String symbolicName; switch(newState) { case DoubleClick::Positive: symbolicName += "control-doubleclick-positive-"; break; case DoubleClick::Negative: symbolicName += "control-doubleclick-negative-"; break; default: break; } symbolicName += impulse.name; int const localPlayer = P_ConsoleToLocal(playerNum); DENG2_ASSERT(localPlayer >= 0); LOG_INPUT_XVERBOSE("Triggered " _E(b) "'%s'" _E(.) " for player%i state: %i threshold: %i\n %s") << impulse.name << (localPlayer + 1) << newState << (nowTime - db.previousClickTime) << symbolicName; Block symbolicNameUtf8 = symbolicName.toUtf8(); ddevent_t ev; de::zap(ev); ev.device = uint(-1); ev.type = E_SYMBOLIC; ev.symbolic.id = playerNum; ev.symbolic.name = symbolicNameUtf8.constData(); inputSys().postEvent(&ev); // makes a copy. } db.previousClickTime = nowTime; db.previousClickState = newState; db.lastState = newState; } void clearDoubleClick() { db.triggered = false; } #endif }; ImpulseAccumulator::ImpulseAccumulator(int impulseId, AccumulatorType type, bool expireBeforeSharpTick) : d(new Instance) { d->impulseId = impulseId; d->type = type; d->expireBeforeSharpTick = expireBeforeSharpTick; } void ImpulseAccumulator::setPlayerNum(int newPlayerNum) { d->playerNum = newPlayerNum; } int ImpulseAccumulator::impulseId() const { return d->impulseId; } ImpulseAccumulator::AccumulatorType ImpulseAccumulator::type() const { return d->type; } bool ImpulseAccumulator::expireBeforeSharpTick() const { return d->expireBeforeSharpTick; } void ImpulseAccumulator::receiveBinary() { // Ensure this is really a binary accumulator. DENG2_ASSERT(d->type == Binary); LOG_AS("ImpulseAccumulator"); d->binaryAccum++; #ifdef __CLIENT__ // Mark for double click. d->maintainDoubleClick(1); d->maintainDoubleClick(0); #endif } int ImpulseAccumulator::takeBinary() { // Ensure this is really a binary accumulator. DENG2_ASSERT(d->type == Binary); LOG_AS("ImpulseAccumulator"); short *counter = &d->binaryAccum; int count = *counter; *counter = 0; return count; } #ifdef __CLIENT__ void ImpulseAccumulator::takeAnalog(float *pos, float *relOffset) { // Ensure this is really an analog accumulator. DENG2_ASSERT(d->type == Analog); LOG_AS("ImpulseAccumulator"); if(pos) *pos = 0; if(relOffset) *relOffset = 0; if(BindContext *bindContext = inputSys().contextPtr(d->getImpulse().bindContextName)) { // Impulse bindings are associated with local player numbers rather than // the player console number - translate. float position, relative; B_EvaluateImpulseBindings(bindContext, P_ConsoleToLocal(d->playerNum), d->impulseId, &position, &relative, d->expireBeforeSharpTick); // Mark for double-clicks. d->maintainDoubleClick(position); if(pos) *pos = position; if(relOffset) *relOffset = relative; } } void ImpulseAccumulator::clearAll() { LOG_AS("ImpulseAccumulator"); switch(d->type) { case Analog: if(!d->expireBeforeSharpTick) { takeAnalog(); } break; case Binary: takeBinary(); break; default: DENG2_ASSERT(!"ImpulseAccumulator::clearAll: Unknown type"); } // Also clear the double click state. d->clearDoubleClick(); } void ImpulseAccumulator::consoleRegister() // static { LOG_AS("ImpulseAccumulator"); C_VAR_INT("input-doubleclick-threshold", &pimpDoubleClickThreshold, 0, 0, 2000); } #endif // __CLIENT__ doomsday-stable-1.15.7/doomsday/client/src/world/sky.cpp0000664000175000017500000003240012641367670022520 0ustar jaakkojaakko/** @file sky.cpp Sky behavior logic for the world system. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/sky.h" #include #include #include #include "dd_main.h" #ifdef __CLIENT__ # include "gl/gl_main.h" # include "gl/gl_tex.h" # include "render/rend_main.h" // rendSkyLightAuto # include "render/skydrawable.h" // SkyDrawable::layerMaterialSpec # include "MaterialAnimator" # include "Texture" # include "TextureManifest" #endif #define NUM_LAYERS 2 using namespace de; DENG2_PIMPL_NOREF(Sky::Layer) { bool active = false; bool masked = false; Material *material = nullptr; float offset = 0; float fadeOutLimit = 0; Sky &sky; Instance(Sky &sky) : sky(sky) {} DENG2_PIMPL_AUDIENCE(ActiveChange) DENG2_PIMPL_AUDIENCE(MaskedChange) DENG2_PIMPL_AUDIENCE(MaterialChange) }; DENG2_AUDIENCE_METHOD(Sky::Layer, ActiveChange) DENG2_AUDIENCE_METHOD(Sky::Layer, MaskedChange) DENG2_AUDIENCE_METHOD(Sky::Layer, MaterialChange) Sky::Layer::Layer(Sky &sky, Material *material) : d(new Instance(sky)) { setMaterial(material); } Sky &Sky::Layer::sky() const { return d->sky; } bool Sky::Layer::isActive() const { return d->active; } void Sky::Layer::setActive(bool yes) { if(d->active != yes) { d->active = yes; DENG2_FOR_AUDIENCE2(ActiveChange, i) i->skyLayerActiveChanged(*this); } } bool Sky::Layer::isMasked() const { return d->masked; } void Sky::Layer::setMasked(bool yes) { if(d->masked != yes) { d->masked = yes; DENG2_FOR_AUDIENCE2(MaskedChange, i) i->skyLayerMaskedChanged(*this); } } Material *Sky::Layer::material() const { return d->material; } void Sky::Layer::setMaterial(Material *newMaterial) { if(d->material != newMaterial) { d->material = newMaterial; DENG2_FOR_AUDIENCE2(MaterialChange, i) i->skyLayerMaterialChanged(*this); } } float Sky::Layer::offset() const { return d->offset; } void Sky::Layer::setOffset(float newOffset) { d->offset = newOffset; } float Sky::Layer::fadeOutLimit() const { return d->fadeOutLimit; } void Sky::Layer::setFadeoutLimit(float newLimit) { d->fadeOutLimit = newLimit; } #ifdef __CLIENT__ static Vector3f const AmbientLightColorDefault(1, 1, 1); // Pure white. #endif DENG2_PIMPL(Sky) #ifdef __CLIENT__ , DENG2_OBSERVES(Layer, ActiveChange) , DENG2_OBSERVES(Layer, MaterialChange) , DENG2_OBSERVES(Layer, MaskedChange) #endif { Layers layers; Record const *def = nullptr; ///< Sky definition. float height = 1; float horizonOffset = 0; Instance(Public *i) : Base(i) { for(int i = 0; i < NUM_LAYERS; ++i) { layers.append(new Layer(self)); #ifdef __CLIENT__ Layer *layer = layers.last(); layer->audienceForActiveChange() += this; layer->audienceForMaskedChange() += this; layer->audienceForMaterialChange() += this; #endif } } ~Instance() { DENG2_FOR_PUBLIC_AUDIENCE2(Deletion, i) i->skyBeingDeleted(self); qDeleteAll(layers); } #ifdef __CLIENT__ /** * Ambient lighting characteristics. */ struct AmbientLight { bool custom = false; /// @c true= defined in a MapInfo def. bool needUpdate = true; /// @c true= update if not custom. Vector3f color; void setColor(Vector3f const &newColor, bool isCustom = true) { color = newColor.min(Vector3f(1, 1, 1)).max(Vector3f(0, 0, 0)); custom = isCustom; } void reset() { custom = false; color = AmbientLightColorDefault; needUpdate = true; } } ambientLight; /** * @todo Move to SkyDrawable and have it simply update this component once the * ambient color has been calculated. * * @todo Re-implement me by rendering the sky to a low-quality cubemap and use * that to obtain the lighting characteristics. */ void updateAmbientLightIfNeeded() { // Never update if a custom color is defined. if(ambientLight.custom) return; // Is it time to update the color? if(!ambientLight.needUpdate) return; ambientLight.needUpdate = false; ambientLight.color = AmbientLightColorDefault; // Determine the first active layer. int firstActiveLayer = -1; // -1 denotes 'no active layers'. for(int i = 0; i < layers.count(); ++i) { if(layers[i]->isActive()) { firstActiveLayer = i; break; } } // A sky with no active layer uses the default color. if(firstActiveLayer < 0) return; Vector3f avgLayerColor; Vector3f bottomCapColor; Vector3f topCapColor; int avgCount = 0; for(int i = firstActiveLayer; i < layers.count(); ++i) { Layer &layer = *layers[i]; // Inactive layers won't be drawn. if(!layer.isActive()) continue; // A material is required for drawing. Material *mat = layer.material(); if(!mat) continue; MaterialAnimator &matAnimator = mat->getAnimator(SkyDrawable::layerMaterialSpec(layer.isMasked())); // Ensure we've up to date info about the material. matAnimator.prepare(); if(TextureVariant *tex = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture) { averagecolor_analysis_t const *avgColor = reinterpret_cast(tex->base().analysisDataPointer(Texture::AverageColorAnalysis)); if(!avgColor) throw Error("calculateSkyAmbientColor", "Texture \"" + tex->base().manifest().composeUri().asText() + "\" has no AverageColorAnalysis"); if(i == firstActiveLayer) { averagecolor_analysis_t const *avgLineColor = reinterpret_cast(tex->base().analysisDataPointer(Texture::AverageTopColorAnalysis)); if(!avgLineColor) throw Error("calculateSkyAmbientColor", "Texture \"" + tex->base().manifest().composeUri().asText() + "\" has no AverageTopColorAnalysis"); topCapColor = Vector3f(avgLineColor->color.rgb); avgLineColor = reinterpret_cast(tex->base().analysisDataPointer(Texture::AverageBottomColorAnalysis)); if(!avgLineColor) throw Error("calculateSkyAmbientColor", "Texture \"" + tex->base().manifest().composeUri().asText() + "\" has no AverageBottomColorAnalysis"); bottomCapColor = Vector3f(avgLineColor->color.rgb); } avgLayerColor += Vector3f(avgColor->color.rgb); ++avgCount; } } // The caps cover a large amount of the sky sphere, so factor it in too. // Each cap is another unit. ambientLight.setColor((avgLayerColor + topCapColor + bottomCapColor) / (avgCount + 2), false /*not a custom color*/); } /// Observes Layer ActiveChange void skyLayerActiveChanged(Layer &) { ambientLight.needUpdate = true; } /// Observes Layer MaterialChange void skyLayerMaterialChanged(Layer &layer) { // We may need to recalculate the ambient color of the sky. if(!layer.isActive()) return; //if(ambientLight.custom) return; ambientLight.needUpdate = true; } /// Observes Layer MaskedChange void skyLayerMaskedChanged(Layer &layer) { // We may need to recalculate the ambient color of the sky. if(!layer.isActive()) return; //if(ambientLight.custom) return; ambientLight.needUpdate = true; } #endif // __CLIENT__ DENG2_PIMPL_AUDIENCE(Deletion) DENG2_PIMPL_AUDIENCE(HeightChange) DENG2_PIMPL_AUDIENCE(HorizonOffsetChange) }; DENG2_AUDIENCE_METHOD(Sky, Deletion) DENG2_AUDIENCE_METHOD(Sky, HeightChange) DENG2_AUDIENCE_METHOD(Sky, HorizonOffsetChange) Sky::Sky(defn::Sky const *definition) : MapElement(DMU_SKY), d(new Instance(this)) { configure(definition); } void Sky::configure(defn::Sky const *def) { LOG_AS("Sky"); // Remember the definition for this configuration (if any). d->def = def? def->accessedRecordPtr() : 0; setHeight(def? def->getf("height") : DEFAULT_SKY_HEIGHT); setHorizonOffset(def? def->getf("horizonOffset") : DEFAULT_SKY_HORIZON_OFFSET); for(int i = 0; i < d->layers.count(); ++i) { Record const *lyrDef = def? &def->layer(i) : 0; Layer &lyr = *d->layers[i]; lyr.setMasked(lyrDef? (lyrDef->geti("flags") & SLF_MASK) : false); lyr.setOffset(lyrDef? lyrDef->getf("offset") : DEFAULT_SKY_SPHERE_XOFFSET); lyr.setFadeoutLimit(lyrDef? lyrDef->getf("colorLimit") : DEFAULT_SKY_SPHERE_FADEOUT_LIMIT); de::Uri const matUri = lyrDef? de::Uri(lyrDef->gets("material")) : de::Uri(DEFAULT_SKY_SPHERE_MATERIAL, RC_NULL); Material *mat = 0; try { mat = App_ResourceSystem().materialPtr(matUri); } catch(MaterialManifest::MissingMaterialError const &er) { // Log if a material is specified but otherwise ignore this error. if(lyrDef) { LOG_RES_WARNING(er.asText() + ". Unknown material \"%s\" in definition layer %i, using default") << matUri << i; } } lyr.setMaterial(mat); lyr.setActive(lyrDef? (lyrDef->geti("flags") & SLF_ENABLE) : i == 0); } #ifdef __CLIENT__ if(def) { Vector3f color(def->get("color")); if(color != Vector3f(0, 0, 0)) { d->ambientLight.setColor(color); } } else { d->ambientLight.reset(); } #endif } Record const *Sky::def() const { return d->def; } Sky::Layers const &Sky::layers() const { return d->layers; } float Sky::height() const { return d->height; } void Sky::setHeight(float newHeight) { newHeight = de::clamp(0.f, newHeight, 1.f); if(!de::fequal(d->height, newHeight)) { d->height = newHeight; DENG2_FOR_AUDIENCE2(HeightChange, i) i->skyHeightChanged(*this); } } float Sky::horizonOffset() const { return d->horizonOffset; } void Sky::setHorizonOffset(float newOffset) { if(!de::fequal(d->horizonOffset, newOffset)) { d->horizonOffset = newOffset; DENG2_FOR_AUDIENCE2(HorizonOffsetChange, i) i->skyHorizonOffsetChanged(*this); } } int Sky::property(DmuArgs &args) const { LOG_AS("Sky"); switch(args.prop) { case DMU_FLAGS: { int flags = 0; if(layer(0)->isActive()) flags |= SKYF_LAYER0_ENABLED; if(layer(1)->isActive()) flags |= SKYF_LAYER1_ENABLED; args.setValue(DDVT_INT, &flags, 0); break; } case DMU_HEIGHT: args.setValue(DDVT_FLOAT, &d->height, 0); break; /*case DMU_HORIZONOFFSET: args.setValue(DDVT_FLOAT, &d->horizonOffset, 0); break;*/ default: return MapElement::property(args); } return false; // Continue iteration. } int Sky::setProperty(DmuArgs const &args) { LOG_AS("Sky"); switch(args.prop) { case DMU_FLAGS: { int flags = 0; if(layer(0)->isActive()) flags |= SKYF_LAYER0_ENABLED; if(layer(1)->isActive()) flags |= SKYF_LAYER1_ENABLED; args.value(DDVT_INT, &flags, 0); layer(0)->setActive(flags & SKYF_LAYER0_ENABLED); layer(1)->setActive(flags & SKYF_LAYER1_ENABLED); break; } case DMU_HEIGHT: { float newHeight = d->height; args.value(DDVT_FLOAT, &newHeight, 0); setHeight(newHeight); break; } /*case DMU_HORIZONOFFSET: { float newOffset = d->horizonOffset; args.value(DDVT_FLOAT, &d->horizonOffset, 0); setHorizonOffset(newOffset); break; }*/ default: return MapElement::setProperty(args); } return false; // Continue iteration. } #ifdef __CLIENT__ Vector3f const &Sky::ambientColor() const { if(d->ambientLight.custom || rendSkyLightAuto) { d->updateAmbientLightIfNeeded(); return d->ambientLight.color; } return AmbientLightColorDefault; } void Sky::setAmbientColor(Vector3f const &newColor) { d->ambientLight.setColor(newColor); } #endif // __CLIENT__ doomsday-stable-1.15.7/doomsday/client/src/world/bspleaf.cpp0000664000175000017500000000366012641367670023334 0ustar jaakkojaakko/** @file bspleaf.cpp World map BSP leaf half-space. * * @authors Copyright © 2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "world/bspleaf.h" #ifdef __CLIENT__ # include "world/map.h" #endif #include "ConvexSubspace" #include "Sector" using namespace de; BspLeaf::BspLeaf(Sector *sector) : _sector (sector) , _subspace(nullptr) {} bool BspLeaf::hasSubspace() const { return _subspace != nullptr; } ConvexSubspace &BspLeaf::subspace() const { if(hasSubspace()) { return *_subspace; } /// @throw MissingSubspaceError Attempted with no subspace attributed. throw MissingSubspaceError("BspLeaf::subspace", "No subspace is attributed"); } void BspLeaf::setSubspace(ConvexSubspace *newSubspace) { if(_subspace == newSubspace) return; if(hasSubspace()) { _subspace->setBspLeaf(nullptr); } _subspace = newSubspace; if(hasSubspace()) { _subspace->setBspLeaf(this); } } Sector *BspLeaf::sectorPtr() { return _sector; } Sector const *BspLeaf::sectorPtr() const { return _sector; } void BspLeaf::setSector(Sector *newSector) { _sector = newSector; } doomsday-stable-1.15.7/doomsday/client/src/world/polyobj.cpp0000664000175000017500000003142012641367670023371 0ustar jaakkojaakko/** @file polyobj.h World map polyobj. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "world/polyobj.h" #include "world/polyobjdata.h" #include "world/worldsystem.h" // validCount #include "world/map.h" #include "world/p_object.h" #include "BspLeaf" #include "ConvexSubspace" #ifdef __CLIENT__ # include "SectorCluster" # include "Shard" #endif #ifdef __CLIENT__ # include "render/rend_main.h" // useBias #endif #include #include #include using namespace de; // Function to be called when the polyobj collides with some map element. static void (*collisionCallback) (mobj_t *mobj, void *line, void *polyobj); static void notifyGeometryChanged(Polyobj &po) { #ifdef __CLIENT__ if(!ddMapSetup && useBias) { // Shadow bias must be informed when surfaces move/deform. foreach(HEdge *hedge, po.mesh().hedges()) { // Is this on the back of a one-sided line? if(!hedge->hasMapElement()) continue; /// @note If polyobjs are allowed to move between sector clusters /// then we'll need to revise the bias illumination storage specially. if(Shard *shard = po.bspLeaf().subspace().cluster().findShard(hedge->mapElement(), LineSide::Middle)) { shard->updateBiasAfterMove(); } } } #else // !__CLIENT__ DENG2_UNUSED(po); #endif } /** * @param po Polyobj instance. * @param mobj Mobj that @a line of the polyobj is in collision with. * @param line Polyobj line that @a mobj is in collision with. */ static void notifyCollision(Polyobj &po, mobj_t *mobj, Line *line) { if(collisionCallback) { collisionCallback(mobj, line, &po); } } polyobj_s::polyobj_s(de::Vector2d const &origin_) : thinker(thinker_s::InitializeToZero) { origin[VX] = origin_.x; origin[VY] = origin_.y; tag = 0; validCount = 0; dest[0] = dest[1] = 0; angle = destAngle = 0; angleSpeed = 0; speed = 0; crush = false; seqType = 0; _bspLeaf = 0; // Allocate private data. thinker.d = new PolyobjData; THINKER_DATA(thinker, PolyobjData).setThinker(&thinker); } polyobj_s::~polyobj_s() { delete THINKER_DATA_MAYBE(thinker, PolyobjData); } PolyobjData &polyobj_s::data() { return THINKER_DATA(thinker, PolyobjData); } PolyobjData const &polyobj_s::data() const { return THINKER_DATA(thinker, PolyobjData); } void Polyobj::setCollisionCallback(void (*func) (mobj_t *mobj, void *line, void *polyobj)) // static { collisionCallback = func; } Map &Polyobj::map() const { /// @todo Do not assume the CURRENT map. return App_WorldSystem().map(); } Mesh &Polyobj::mesh() const { return *data().mesh; } bool Polyobj::isLinked() { return _bspLeaf != 0; } void Polyobj::unlink() { if(_bspLeaf) { if(_bspLeaf->hasSubspace()) { _bspLeaf->subspace().unlink(*this); } _bspLeaf = 0; map().unlink(*this); } } void Polyobj::link() { if(!_bspLeaf) { map().link(*this); // Find the center point of the polyobj. Vector2d avg; foreach(Line *line, lines()) { avg += line->fromOrigin(); } avg /= lineCount(); // Given the center point determine in which BSP leaf the polyobj resides. _bspLeaf = &map().bspLeafAt(avg); if(_bspLeaf->hasSubspace()) { _bspLeaf->subspace().link(*this); } } } bool Polyobj::hasBspLeaf() const { return _bspLeaf != 0; } BspLeaf &Polyobj::bspLeaf() const { if(_bspLeaf) { return *_bspLeaf; } /// @throw Polyobj::NotLinkedError Attempted while the polyobj is not linked to the BSP. throw Polyobj::NotLinkedError("Polyobj::bspLeaf", "Polyobj is not presently linked in the BSP"); } bool Polyobj::hasSector() const { return hasBspLeaf() && bspLeaf().hasSubspace(); } Sector &Polyobj::sector() const { return *bspLeaf().sectorPtr(); } Sector *Polyobj::sectorPtr() const { return hasBspLeaf()? bspLeaf().sectorPtr() : 0; } SoundEmitter &Polyobj::soundEmitter() { return *reinterpret_cast(this); } SoundEmitter const &Polyobj::soundEmitter() const { return const_cast(const_cast(*this).soundEmitter()); } Polyobj::Lines const &Polyobj::lines() const { return data().lines; } Polyobj::Vertexes const &Polyobj::uniqueVertexes() const { return data().uniqueVertexes; } void Polyobj::buildUniqueVertexes() { QSet vertexSet; foreach(Line *line, lines()) { vertexSet.insert(&line->from()); vertexSet.insert(&line->to()); } Vertexes &uniqueVertexes = data().uniqueVertexes; uniqueVertexes = vertexSet.toList(); // Resize the coordinate vectors as they are implicitly linked to the unique vertexes. data().originalPts.resize(uniqueVertexes.count()); data().prevPts.resize(uniqueVertexes.count()); } void Polyobj::updateOriginalVertexCoords() { PolyobjData::VertexCoords::iterator origCoordsIt = data().originalPts.begin(); foreach(Vertex *vertex, uniqueVertexes()) { // The original coordinates are relative to the polyobj origin. (*origCoordsIt) = vertex->origin() - Vector2d(origin); origCoordsIt++; } } void Polyobj::updateAABox() { aaBox.clear(); if(!lineCount()) return; QListIterator lineIt(lines()); Line *line = lineIt.next(); V2d_CopyBox(aaBox.arvec2, line->aaBox().arvec2); while(lineIt.hasNext()) { line = lineIt.next(); V2d_UniteBox(aaBox.arvec2, line->aaBox().arvec2); } } void Polyobj::updateSurfaceTangents() { foreach(Line *line, lines()) { line->front().updateSurfaceNormals(); line->back().updateSurfaceNormals(); } } struct ptrmobjblockingparams_t { Polyobj *polyobj; Line *line; // Line of the polyobj which suffered collision. bool isBlocked; }; static inline bool mobjCanBlockMovement(mobj_t *mo) { DENG2_ASSERT(mo != 0); return (mo->ddFlags & DDMF_SOLID) || (mo->dPlayer && !(mo->dPlayer->flags & DDPF_CAMERA)); } static int PTR_CheckMobjBlocking(mobj_t *mo, void *context) { ptrmobjblockingparams_t *parms = (ptrmobjblockingparams_t *) context; if(!mobjCanBlockMovement(mo)) return false; // Out of range? AABoxd moAABox = Mobj_AABox(*mo); if(moAABox.maxX <= parms->line->aaBox().minX || moAABox.minX >= parms->line->aaBox().maxX || moAABox.maxY <= parms->line->aaBox().minY || moAABox.minY >= parms->line->aaBox().maxY) return false; if(parms->line->boxOnSide(moAABox)) return false; // This mobj blocks our path! notifyCollision(*parms->polyobj, mo, parms->line); parms->isBlocked = true; // Process all blocking mobjs... return false; } static bool checkMobjBlocking(Polyobj &po, Line &line) { ptrmobjblockingparams_t parms; parms.polyobj = &po; parms.line = &line; parms.isBlocked = false; AABoxd interceptRange(line.aaBox().minX - DDMOBJ_RADIUS_MAX, line.aaBox().minY - DDMOBJ_RADIUS_MAX, line.aaBox().maxX + DDMOBJ_RADIUS_MAX, line.aaBox().maxY + DDMOBJ_RADIUS_MAX); validCount++; Mobj_BoxIterator(&interceptRange, PTR_CheckMobjBlocking, &parms); return parms.isBlocked; } static bool mobjIsBlockingPolyobj(Polyobj &po) { foreach(Line *line, po.lines()) { if(checkMobjBlocking(po, *line)) return true; } return false; // All clear. } bool Polyobj::move(Vector2d const &delta) { LOG_AS("Polyobj::move"); //LOG_DEBUG("Applying delta %s to [%p]") << delta.asText() << this; unlink(); { PolyobjData::VertexCoords::iterator prevCoordsIt = data().prevPts.begin(); foreach(Vertex *vertex, uniqueVertexes()) { // Remember the previous coords in case we need to undo. (*prevCoordsIt) = vertex->origin(); // Apply translation. vertex->setOrigin(vertex->origin() + delta); prevCoordsIt++; } foreach(Line *line, lines()) { line->updateAABox(); } Vector2d newOrigin = Vector2d(origin) + delta; V2d_Set(origin, newOrigin.x, newOrigin.y); updateAABox(); } link(); // With translation applied now determine if we collided with anything. if(mobjIsBlockingPolyobj(*this)) { //LOG_DEBUG("Blocked by mobj, undoing..."); unlink(); { PolyobjData::VertexCoords::const_iterator prevCoordsIt = data().prevPts.constBegin(); foreach(Vertex *vertex, uniqueVertexes()) { vertex->setOrigin(*prevCoordsIt); prevCoordsIt++; } foreach(Line *line, lines()) { line->updateAABox(); } Vector2d newOrigin = Vector2d(origin) - delta; V2d_Set(origin, newOrigin.x, newOrigin.y); updateAABox(); } link(); return false; } // Various parties may be interested in this change; signal it. notifyGeometryChanged(*this); return true; } /** * @param point Point to be rotated (in-place). * @param about Origin to rotate @a point relative to. * @param fineAngle Angle to rotate (theta). */ static void rotatePoint2d(Vector2d &point, Vector2d const &about, uint fineAngle) { coord_t const c = FIX2DBL(fineCosine[fineAngle]); coord_t const s = FIX2DBL(finesine[fineAngle]); Vector2d orig = point; point.x = orig.x * c - orig.y * s + about.x; point.y = orig.y * c + orig.x * s + about.y; } bool Polyobj::rotate(angle_t delta) { LOG_AS("Polyobj::rotate"); //LOG_DEBUG("Applying delta %u (%f) to [%p]") // << delta << (delta / float( ANGLE_MAX ) * 360) << this; unlink(); { uint fineAngle = (angle + delta) >> ANGLETOFINESHIFT; PolyobjData::VertexCoords::const_iterator origCoordsIt = data().originalPts.constBegin(); PolyobjData::VertexCoords::iterator prevCoordsIt = data().prevPts.begin(); foreach(Vertex *vertex, uniqueVertexes()) { // Remember the previous coords in case we need to undo. (*prevCoordsIt) = vertex->origin(); // Apply rotation relative to the "original" coords. Vector2d newCoords = (*origCoordsIt); rotatePoint2d(newCoords, origin, fineAngle); vertex->setOrigin(newCoords); origCoordsIt++; prevCoordsIt++; } foreach(Line *line, lines()) { line->updateAABox(); line->updateSlopeType(); } updateAABox(); angle += delta; } link(); // With rotation applied now determine if we collided with anything. if(mobjIsBlockingPolyobj(*this)) { //LOG_DEBUG("Blocked by mobj, undoing..."); unlink(); { PolyobjData::VertexCoords::const_iterator prevCoordsIt = data().prevPts.constBegin(); foreach(Vertex *vertex, uniqueVertexes()) { vertex->setOrigin(*prevCoordsIt); prevCoordsIt++; } foreach(Line *line, lines()) { line->updateAABox(); line->updateSlopeType(); } updateAABox(); angle -= delta; } link(); return false; } updateSurfaceTangents(); // Various parties may be interested in this change; signal it. notifyGeometryChanged(*this); return true; } void Polyobj::setTag(int newTag) { tag = newTag; } void Polyobj::setSequenceType(int newType) { seqType = newType; } int Polyobj::indexInMap() const { return data().indexInMap; } void Polyobj::setIndexInMap(int newIndex) { data().indexInMap = newIndex; } doomsday-stable-1.15.7/doomsday/client/src/world/linesighttest.cpp0000664000175000017500000002553412641367670024612 0ustar jaakkojaakko/** @file linesighttest.cpp World map line of sight testing. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "world/linesighttest.h" #include "Face" #include "world/worldsystem.h" /// For validCount, @todo Remove me. #include "BspLeaf" #include "BspNode" #include "ConvexSubspace" #include "Line" #include "Polyobj" #include "Sector" #include #include #include /// @todo remove me #include using namespace de; DENG2_PIMPL(LineSightTest) { dint flags; ///< LS_* flags @ref lineSightFlags Vector3d from; ///< Ray origin. Vector3d to; ///< Ray target. dfloat bottomSlope; ///< Slope to bottom of target. dfloat topSlope; ///< Slope to top of target. /// The ray to be traced. struct Ray { fixed_t origin[2]; fixed_t direction[2]; AABoxd aabox; Ray(Vector3d const &from, Vector3d const &to) { origin[VX] = DBL2FIX(from.x); origin[VY] = DBL2FIX(from.y); direction[VX] = DBL2FIX(to.x - from.x); direction[VY] = DBL2FIX(to.y - from.y); ddouble v1From[2] = { from.x, from.y }; V2d_InitBox(aabox.arvec2, v1From); ddouble v1To[2] = { to.x, to.y }; V2d_AddToBox(aabox.arvec2, v1To); } } ray; Instance(Public *i, Vector3d const &from, Vector3d const to, dfloat bottomSlope, dfloat topSlope, dint flags) : Base(i) , flags(flags) , from(from) , to(to) , bottomSlope(bottomSlope) , topSlope(topSlope) , ray(from, to) {} /** * @return @c true if the ray passes the line @a side; otherwise @c false. * * @todo cleanup: Much unnecessary representation flipping... * @todo cleanup: Remove front-side assumption. */ bool crossLine(LineSide &side) { #define RTOP 0x1 ///< Top range. #define RBOTTOM 0x2 ///< Bottom range. Line &line = side.line(); if(line.validCount() == validCount) return true; // Ignore line.setValidCount(validCount); // Does the ray intercept the line on the X/Y plane? // Try a quick bounding-box rejection. if(line.aaBox().minX > ray.aabox.maxX || line.aaBox().maxX < ray.aabox.minX || line.aaBox().minY > ray.aabox.maxY || line.aaBox().maxY < ray.aabox.minY) return true; fixed_t lineV1OriginX[2] = { DBL2FIX(line.fromOrigin().x), DBL2FIX(line.fromOrigin().y) }; fixed_t lineV2OriginX[2] = { DBL2FIX(line.toOrigin().x), DBL2FIX(line.toOrigin().y) }; if(V2x_PointOnLineSide(lineV1OriginX, ray.origin, ray.direction) == V2x_PointOnLineSide(lineV2OriginX, ray.origin, ray.direction)) return true; fixed_t lineDirectionX[2] = { DBL2FIX(line.direction().x), DBL2FIX(line.direction().y) }; fixed_t fromPointX[2] = { DBL2FIX(from.x), DBL2FIX(from.y) }; fixed_t toPointX[2] = { DBL2FIX(to.x), DBL2FIX(to.y) }; if(V2x_PointOnLineSide(fromPointX, lineV1OriginX, lineDirectionX) == V2x_PointOnLineSide(toPointX, lineV1OriginX, lineDirectionX)) return true; // Is this the passable side of a one-way BSP window? if(!side.hasSections()) return true; if(!side.hasSector()) return false; Sector const *frontSec = side.sectorPtr(); Sector const *backSec = side.back().sectorPtr(); bool noBack = side.considerOneSided(); if(!noBack && !(flags & LS_PASSLEFT)) { noBack = (!( backSec->floor().height() < frontSec->ceiling().height()) || !(frontSec->floor().height() < backSec->ceiling().height())); } if(noBack) { // Does the ray pass from left to right? if(flags & LS_PASSLEFT) // Allowed. { if(line.pointOnSide(Vector2d(from.x, from.y)) < 0) return true; } // No back side is present so if the ray is not allowed to pass over/under // the line then end it right here. if(!(flags & (LS_PASSOVER | LS_PASSUNDER))) return false; } // Handle the case of a zero height back side in the top range. dbyte ranges = 0; if(noBack) { ranges |= RTOP; } else { if(backSec->floor().height() != frontSec->floor().height()) ranges |= RBOTTOM; if(backSec->ceiling().height() != frontSec->ceiling().height()) ranges |= RTOP; } // No partially closed ranges which require testing? if(!ranges) return true; dfloat frac = FIX2FLT(V2x_Intersection(lineV1OriginX, lineDirectionX, ray.origin, ray.direction)); // Does the ray pass over the top range? if(flags & LS_PASSOVER) // Allowed. { if(bottomSlope > (frontSec->ceiling().height() - from.z) / frac) return true; } // Does the ray pass under the bottom range? if(flags & LS_PASSUNDER) // Allowed. { if(topSlope < ( frontSec->floor().height() - from.z) / frac) return true; } // Test a partially closed top range? if(ranges & RTOP) { dfloat const top = noBack ? frontSec->ceiling().height() : frontSec->ceiling().height() < backSec->ceiling().height()? frontSec->ceiling().height() : backSec->ceiling().height(); dfloat const slope = (top - from.z) / frac; if((slope < topSlope) ^ (noBack && !(flags & LS_PASSOVER)) || (noBack && topSlope > (frontSec->floor().height() - from.z) / frac)) topSlope = slope; if((slope < bottomSlope) ^ (noBack && !(flags & LS_PASSUNDER)) || (noBack && bottomSlope > (frontSec->floor().height() - from.z) / frac)) bottomSlope = slope; } // Test a partially closed bottom range? if(ranges & RBOTTOM) { dfloat const bottom = noBack? frontSec->floor().height() : frontSec->floor().height() > backSec->floor().height()? frontSec->floor().height() : backSec->floor().height(); dfloat const slope = (bottom - from.z) / frac; if(slope > bottomSlope) bottomSlope = slope; if(slope > topSlope) topSlope = slope; } return topSlope <= bottomSlope? false : true; #undef RTOP #undef RBOTTOM } /** * @return @c true if the ray passes @a bspLeaf; otherwise @c false. */ bool crossBspLeaf(BspLeaf const &bspLeaf) { if(!bspLeaf.hasSubspace()) return false; ConvexSubspace const &subspace = bspLeaf.subspace(); // Check polyobj lines. LoopResult blocked = subspace.forAllPolyobjs([this] (Polyobj &pob) { for(Line *line : pob.lines()) { if(!crossLine(line->front())) return LoopAbort; } return LoopContinue; }); if(blocked) return false; // Check lines for the edges of the subspace geometry. HEdge *base = subspace.poly().hedge(); HEdge *hedge = base; do { if(hedge->hasMapElement()) { if(!crossLine(hedge->mapElementAs().lineSide())) return false; } } while((hedge = &hedge->next()) != base); // Check lines for the extra meshes. blocked = subspace.forAllExtraMeshes([this] (Mesh &mesh) { for(HEdge *hedge : mesh.hedges()) { // Is this on the back of a one-sided line? if(!hedge->hasMapElement()) continue; if(!crossLine(hedge->mapElementAs().lineSide())) return LoopAbort; } return LoopContinue; }); return !blocked; } /** * @return @c true if the ray passes @a bspTree; otherwise @c false. */ bool crossBspNode(BspTree const *bspTree) { DENG2_ASSERT(bspTree != 0); while(!bspTree->isLeaf()) { BspNode const &bspNode = bspTree->userData()->as(); // Does the ray intersect the partition? /// @todo Optionally use the fixed precision version -ds dint const fromSide = bspNode.partition().pointOnSide(Vector2d(from.x, from.y)) < 0; dint const toSide = bspNode.partition().pointOnSide(Vector2d(to.x, to.y)) < 0; if(fromSide != toSide) { // Yes. if(!crossBspNode(bspTree->childPtr(BspTree::ChildId(fromSide)))) return false; // Cross the From side. bspTree = bspTree->childPtr(BspTree::ChildId(fromSide ^ 1)); // Cross the To side. } else { // No - descend! bspTree = bspTree->childPtr(BspTree::ChildId(fromSide)); } } // We've arrived at a leaf. return crossBspLeaf(bspTree->userData()->as()); } }; LineSightTest::LineSightTest(Vector3d const &from, Vector3d const &to, dfloat bottomSlope, dfloat topSlope, dint flags) : d(new Instance(this, from, to, bottomSlope, topSlope, flags)) {} bool LineSightTest::trace(BspTree const &bspRoot) { validCount++; d->topSlope = d->to.z + d->topSlope - d->from.z; d->bottomSlope = d->to.z + d->bottomSlope - d->from.z; return d->crossBspNode(&bspRoot); } doomsday-stable-1.15.7/doomsday/client/src/world/interceptor.cpp0000664000175000017500000002706412641367670024262 0ustar jaakkojaakko/** @file interceptor.cpp World map element/object ray trace interceptor. * * @authors Copyright © 1999-2013 Jaakko Keränen * @authors Copyright © 2005-2014 Daniel Swanson * @authors Copyright © 1993-1996 by id Software, Inc. * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/interceptor.h" #include #include #include "world/blockmap.h" #include "world/lineblockmap.h" #include "world/p_object.h" #include "world/worldsystem.h" // validCount using namespace de; struct ListNode { ListNode *next; ListNode *prev; intercepttype_t type; void *object; float distance; template ObjectType &objectAs() const { DENG2_ASSERT(object); return *static_cast(object); } }; // Blockset from which intercepts are allocated. static zblockset_t *interceptNodeSet; // Head of the used intercept list. static ListNode *interceptFirst; // Trace nodes. static ListNode head; static ListNode tail; static ListNode *mru; DENG2_PIMPL_NOREF(Interceptor) { traverser_t callback; void *context; Vector2d from; Vector2d to; int flags; ///< @ref pathTraverseFlags Map *map = nullptr; LineOpening opening; // Array representation for ray geometry (used with legacy code). vec2d_t fromV1; vec2d_t directionV1; Instance(traverser_t callback, Vector2d const &from, Vector2d const &to, int flags, void *context) : callback(callback) , context (context) , from (from) , to (to) , flags (flags) { V2d_Set(fromV1, from.x, from.y); V2d_Set(directionV1, to.x - from.x, to.y - from.y); } inline bool isSentinel(ListNode const &node) { return &node == &tail || &node == &head; } /** * Empties the intercepts array and makes sure it has been allocated. */ void clearIntercepts() { #define MININTERCEPTS 128 if(!interceptNodeSet) { interceptNodeSet = ZBlockSet_New(sizeof(ListNode), MININTERCEPTS, PU_APPSTATIC); // Configure the static head and tail. head.distance = 0.0f; head.next = &tail; head.prev = nullptr; tail.distance = 1.0f; tail.prev = &head; tail.next = nullptr; } // Start reusing intercepts (may point to a sentinel but that is Ok). if(!interceptFirst) { interceptFirst = head.next; } else if(head.next != &tail) { ListNode *existing = interceptFirst; interceptFirst = head.next; tail.prev->next = existing; } // Reset the trace. head.next = &tail; tail.prev = &head; mru = nullptr; #undef MININTERCEPTS } /** * You must clear intercepts before the first time this is called. * The intercepts array grows if necessary. * * @param type Type of interception. * @param distance Distance along the trace vector that the interception occured [0...1]. * @param object Object being intercepted. */ void addIntercept(intercepttype_t type, float distance, void *object) { DENG2_ASSERT(object); // First reject vs our sentinels if(distance < head.distance) return; if(distance > tail.distance) return; // Find the new intercept's ordered place along the trace. ListNode *before; if(mru && mru->distance <= distance) { before = mru->next; } else { before = head.next; } while(before->next && distance >= before->distance) { before = before->next; } ListNode *icpt; // Can we reuse an existing intercept? if(!isSentinel(*interceptFirst)) { icpt = interceptFirst; interceptFirst = icpt->next; } else { icpt = (ListNode *) ZBlockSet_Allocate(interceptNodeSet); } icpt->type = type; icpt->object = object; icpt->distance = distance; // Link it in. icpt->next = before; icpt->prev = before->prev; icpt->prev->next = icpt; icpt->next->prev = icpt; mru = icpt; } void intercept(Line &line) { fixed_t origin[2] = { DBL2FIX(from.x), DBL2FIX(from.y) }; fixed_t direction[2] = { DBL2FIX(to.x - from.x), DBL2FIX(to.y - from.y) }; fixed_t lineFromX[2] = { DBL2FIX(line.fromOrigin().x), DBL2FIX(line.fromOrigin().y) }; fixed_t lineToX[2] = { DBL2FIX( line.toOrigin().x), DBL2FIX( line.toOrigin().y) }; // Is this line crossed? // Avoid precision problems with two routines. int s1, s2; if(direction[VX] > FRACUNIT * 16 || direction[VY] > FRACUNIT * 16 || direction[VX] < -FRACUNIT * 16 || direction[VY] < -FRACUNIT * 16) { s1 = V2x_PointOnLineSide(lineFromX, origin, direction); s2 = V2x_PointOnLineSide(lineToX, origin, direction); } else { s1 = line.pointOnSide(Vector2d(FIX2FLT(origin[VX]), FIX2FLT(origin[VY]))) < 0; s2 = line.pointOnSide(Vector2d(FIX2FLT(origin[VX] + direction[VX]), FIX2FLT(origin[VY] + direction[VY]))) < 0; } // Is this line crossed? if(s1 == s2) return; // Calculate interception point. fixed_t lineDirectionX[2] = { DBL2FIX(line.direction().x), DBL2FIX(line.direction().y) }; float distance = FIX2FLT(V2x_Intersection(lineFromX, lineDirectionX, origin, direction)); // On the correct side of the trace origin? if(distance >= 0) { addIntercept(ICPT_LINE, distance, &line); } } void intercept(mobj_t &mobj) { // Ignore cameras. if(mobj.dPlayer && (mobj.dPlayer->flags & DDPF_CAMERA)) return; fixed_t origin[2] = { DBL2FIX(from.x), DBL2FIX(from.y) }; fixed_t direction[2] = { DBL2FIX(to.x - from.x), DBL2FIX(to.y - from.y) }; // Check a corner to corner crossection for hit. AABoxd const aaBox = Mobj_AABox(mobj); fixed_t icptFrom[2], icptTo[2]; if((direction[VX] ^ direction[VY]) > 0) { // \ Slope V2x_Set(icptFrom, DBL2FIX(aaBox.minX), DBL2FIX(aaBox.maxY)); V2x_Set(icptTo, DBL2FIX(aaBox.maxX), DBL2FIX(aaBox.minY)); } else { // / Slope V2x_Set(icptFrom, DBL2FIX(aaBox.minX), DBL2FIX(aaBox.minY)); V2x_Set(icptTo, DBL2FIX(aaBox.maxX), DBL2FIX(aaBox.maxY)); } // Is this line crossed? if(V2x_PointOnLineSide(icptFrom, origin, direction) == V2x_PointOnLineSide(icptTo, origin, direction)) return; // Calculate interception point. fixed_t icptDirection[2] = { icptTo[VX] - icptFrom[VX], icptTo[VY] - icptFrom[VY] }; float distance = FIX2FLT(V2x_Intersection(icptFrom, icptDirection, origin, direction)); // On the correct side of the trace origin? if(distance >= 0) { addIntercept(ICPT_MOBJ, distance, &mobj); } } void runTrace() { clearIntercepts(); int const localValidCount = ++validCount; if(flags & PTF_LINE) { // Process polyobj lines. if(map->polyobjCount()) { map->polyobjBlockmap().forAllInPath(from, to, [this, &localValidCount] (void *object) { Polyobj &pob = *(Polyobj *)object; if(pob.validCount != localValidCount) // not yet processed { pob.validCount = localValidCount; for(Line *line : pob.lines()) { if(line->validCount() != localValidCount) // not yet processed { line->setValidCount(localValidCount); intercept(*line); } } } return LoopContinue; }); } // Process sector lines. map->lineBlockmap().forAllInPath(from, to, [this, &localValidCount] (void *object) { Line &line = *(Line *)object; if(line.validCount() != localValidCount) // not yet processed { line.setValidCount(localValidCount); intercept(line); } return LoopContinue; }); } if(flags & PTF_MOBJ) { // Process map objects. map->mobjBlockmap().forAllInPath(from, to, [this, &localValidCount] (void *object) { mobj_t &mob = *(mobj_t *)object; if(mob.validCount != localValidCount) // not yet processed { mob.validCount = localValidCount; intercept(mob); } return LoopContinue; }); } } }; Interceptor::Interceptor(traverser_t callback, Vector2d const &from, Vector2d const &to, int flags, void *context) : d(new Instance(callback, from, to, flags, context)) {} coord_t const *Interceptor::origin() const { return d->fromV1; } coord_t const *Interceptor::direction() const { return d->directionV1; } LineOpening const &Interceptor::opening() const { return d->opening; } bool Interceptor::adjustOpening(Line const *line) { DENG2_ASSERT(d->map != 0); if(line) { if(d->map == &line->map()) { d->opening = LineOpening(*line); } else { qDebug() << "Ignoring alien line" << de::dintptr(line) << "in Interceptor::adjustOpening"; } } return d->opening.range > 0; } int Interceptor::trace(Map const &map) { // Step #1: Collect and sort intercepts. d->map = const_cast(&map); d->runTrace(); // Step #2: Process intercepts. for(ListNode *node = head.next; !d->isSentinel(*node); node = node->next) { // Prepare the intercept info. Intercept icpt; icpt.trace = this; icpt.distance = node->distance; icpt.type = node->type; switch(node->type) { case ICPT_MOBJ: icpt.mobj = &node->objectAs(); break; case ICPT_LINE: icpt.line = &node->objectAs(); break; } // Make the callback. if(int result = d->callback(&icpt, d->context)) return result; } return false; // Intercept traversal completed wholly. } doomsday-stable-1.15.7/doomsday/client/src/world/dmuargs.cpp0000664000175000017500000004130612641367670023361 0ustar jaakkojaakko/** @file dmuargs.cpp Doomsday Map Update (DMU) API arguments. * * @authors Copyright © 2006-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/dmuargs.h" #include #include "dd_share.h" using namespace de; /** * Convert propertyType enum constant into a string for error/debug messages. */ static char const *value_Str(dint val) { static char valStr[40]; struct val_s { dint val; char const *str; } valuetypes[] = { { DDVT_BOOL, "DDVT_BOOL" }, { DDVT_BYTE, "DDVT_BYTE" }, { DDVT_SHORT, "DDVT_SHORT" }, { DDVT_INT, "DDVT_INT" }, { DDVT_UINT, "DDVT_UINT" }, { DDVT_FIXED, "DDVT_FIXED" }, { DDVT_ANGLE, "DDVT_ANGLE" }, { DDVT_FLOAT, "DDVT_FLOAT" }, { DDVT_DOUBLE, "DDVT_DOUBLE" }, { DDVT_LONG, "DDVT_LONG" }, { DDVT_ULONG, "DDVT_ULONG" }, { DDVT_PTR, "DDVT_PTR" }, { DDVT_BLENDMODE, "DDVT_BLENDMODE" }, { 0, nullptr } }; for(duint i = 0; valuetypes[i].str; ++i) { if(valuetypes[i].val == val) return valuetypes[i].str; } sprintf(valStr, "(unnamed %i)", val); return valStr; } DmuArgs::DmuArgs(int type, uint prop) : type (type), prop (prop & ~DMU_FLAG_MASK), modifiers(prop & DMU_FLAG_MASK), valueType(DDVT_NONE), booleanValues(0), byteValues(0), intValues(0), fixedValues(0), floatValues(0), doubleValues(0), angleValues(0), ptrValues(0) { DENG_ASSERT(VALID_DMU_ELEMENT_TYPE_ID(type)); } void DmuArgs::value(valuetype_t dstValueType, void *dst, uint index) const { if(dstValueType == DDVT_FIXED) { fixed_t *d = (fixed_t *)dst; switch(valueType) { case DDVT_BYTE: *d = (byteValues[index] << FRACBITS); break; case DDVT_INT: *d = (intValues[index] << FRACBITS); break; case DDVT_FIXED: *d = fixedValues[index]; break; case DDVT_FLOAT: *d = FLT2FIX(floatValues[index]); break; case DDVT_DOUBLE: *d = FLT2FIX(doubleValues[index]); break; default: { /// @todo Throw exception. QByteArray msg = String("DmuArgs::value: DDVT_FIXED incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(dstValueType == DDVT_FLOAT) { float *d = (float *)dst; switch(valueType) { case DDVT_BYTE: *d = byteValues[index]; break; case DDVT_INT: *d = intValues[index]; break; case DDVT_FIXED: *d = FIX2FLT(fixedValues[index]); break; case DDVT_FLOAT: *d = floatValues[index]; break; case DDVT_DOUBLE: *d = (float)doubleValues[index]; break; default: { /// @todo Throw exception. QByteArray msg = String("DmuArgs::value: DDVT_FLOAT incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(dstValueType == DDVT_DOUBLE) { double *d = (double *)dst; switch(valueType) { case DDVT_BYTE: *d = byteValues[index]; break; case DDVT_INT: *d = intValues[index]; break; case DDVT_FIXED: *d = FIX2FLT(fixedValues[index]); break; case DDVT_FLOAT: *d = floatValues[index]; break; case DDVT_DOUBLE: *d = doubleValues[index]; break; default: { /// @todo Throw exception. QByteArray msg = String("DmuArgs::value: DDVT_DOUBLE incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(dstValueType == DDVT_BOOL) { dd_bool *d = (dd_bool *)dst; switch(valueType) { case DDVT_BOOL: *d = booleanValues[index]; break; default: { /// @todo Throw exception. QByteArray msg = String("DmuArgs::value: DDVT_BOOL incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(dstValueType == DDVT_BYTE) { byte *d = (byte *)dst; switch(valueType) { case DDVT_BOOL: *d = booleanValues[index]; break; case DDVT_BYTE: *d = byteValues[index]; break; case DDVT_INT: *d = intValues[index]; break; case DDVT_FLOAT: *d = (byte) floatValues[index]; break; case DDVT_DOUBLE: *d = (byte) doubleValues[index]; break; default: { /// @todo Throw exception. QByteArray msg = String("DmuArgs::value: DDVT_BYTE incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(dstValueType == DDVT_INT) { int *d = (int *)dst; switch(valueType) { case DDVT_BOOL: *d = booleanValues[index]; break; case DDVT_BYTE: *d = byteValues[index]; break; case DDVT_INT: *d = intValues[index]; break; case DDVT_FLOAT: *d = floatValues[index]; break; case DDVT_DOUBLE: *d = doubleValues[index]; break; case DDVT_FIXED: *d = (fixedValues[index] >> FRACBITS); break; default: { /// @todo Throw exception. QByteArray msg = String("DmuArgs::value: DDVT_INT incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(dstValueType == DDVT_SHORT) { short *d = (short *)dst; switch(valueType) { case DDVT_BOOL: *d = booleanValues[index]; break; case DDVT_BYTE: *d = byteValues[index]; break; case DDVT_INT: *d = intValues[index]; break; case DDVT_FLOAT: *d = floatValues[index]; break; case DDVT_DOUBLE: *d = doubleValues[index]; break; case DDVT_FIXED: *d = (fixedValues[index] >> FRACBITS); break; default: { /// @todo Throw exception. QByteArray msg = String("DmuArgs::value: DDVT_SHORT incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(dstValueType == DDVT_ANGLE) { angle_t *d = (angle_t *)dst; switch(valueType) { case DDVT_ANGLE: *d = angleValues[index]; break; default: { /// @todo Throw exception. QByteArray msg = String("DmuArgs::value: DDVT_ANGLE incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(dstValueType == DDVT_BLENDMODE) { blendmode_t *d = (blendmode_t *)dst; switch(valueType) { case DDVT_INT: if(intValues[index] > DDNUM_BLENDMODES || intValues[index] < 0) { QByteArray msg = String("DmuArgs::value: %1 is not a valid value for DDVT_BLENDMODE.").arg(intValues[index]).toUtf8(); App_FatalError(msg.constData()); } *d = blendmode_t(intValues[index]); break; default: { /// @todo Throw exception. QByteArray msg = String("DmuArgs::value: DDVT_BLENDMODE incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(dstValueType == DDVT_PTR) { void **d = (void **)dst; switch(valueType) { case DDVT_PTR: *d = ptrValues[index]; break; default: { /// @todo Throw exception. QByteArray msg = String("DmuArgs::value: DDVT_PTR incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else { /// @todo Throw exception. QByteArray msg = String("DmuArgs::value: unknown value type %1.").arg(dstValueType).toUtf8(); App_FatalError(msg.constData()); } } void DmuArgs::setValue(valuetype_t srcValueType, void const *src, uint index) { if(srcValueType == DDVT_FIXED) { fixed_t const *s = (fixed_t const *)src; switch(valueType) { case DDVT_BYTE: byteValues[index] = (*s >> FRACBITS); break; case DDVT_INT: intValues[index] = (*s >> FRACBITS); break; case DDVT_FIXED: fixedValues[index] = *s; break; case DDVT_FLOAT: floatValues[index] = FIX2FLT(*s); break; case DDVT_DOUBLE: doubleValues[index] = FIX2FLT(*s); break; default: { QByteArray msg = String("DmuArgs::setValue: DDVT_FIXED incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(srcValueType == DDVT_FLOAT) { float const *s = (float const *)src; switch(valueType) { case DDVT_BYTE: byteValues[index] = *s; break; case DDVT_INT: intValues[index] = (int) *s; break; case DDVT_FIXED: fixedValues[index] = FLT2FIX(*s); break; case DDVT_FLOAT: floatValues[index] = *s; break; case DDVT_DOUBLE: doubleValues[index] = (double)*s; break; default: { QByteArray msg = String("DmuArgs::setValue: DDVT_FLOAT incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(srcValueType == DDVT_DOUBLE) { double const *s = (double const *)src; switch(valueType) { case DDVT_BYTE: byteValues[index] = (byte)*s; break; case DDVT_INT: intValues[index] = (int) *s; break; case DDVT_FIXED: fixedValues[index] = FLT2FIX(*s); break; case DDVT_FLOAT: floatValues[index] = (float)*s; break; case DDVT_DOUBLE: doubleValues[index] = *s; break; default: { QByteArray msg = String("DmuArgs::setValue: DDVT_DOUBLE incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(srcValueType == DDVT_BOOL) { dd_bool const *s = (dd_bool const *)src; switch(valueType) { case DDVT_BOOL: booleanValues[index] = *s; break; default: { QByteArray msg = String("DmuArgs::setValue: DDVT_BOOL incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(srcValueType == DDVT_BYTE) { byte const *s = (byte const *)src; switch(valueType) { case DDVT_BOOL: booleanValues[index] = *s; break; case DDVT_BYTE: byteValues[index] = *s; break; case DDVT_INT: intValues[index] = *s; break; case DDVT_FLOAT: floatValues[index] = *s; break; case DDVT_DOUBLE: doubleValues[index] = *s; break; default: { QByteArray msg = String("DmuArgs::setValue: DDVT_BYTE incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(srcValueType == DDVT_INT) { int const *s = (int const *)src; switch(valueType) { case DDVT_BOOL: booleanValues[index] = *s; break; case DDVT_BYTE: byteValues[index] = *s; break; case DDVT_INT: intValues[index] = *s; break; case DDVT_FLOAT: floatValues[index] = *s; break; case DDVT_DOUBLE: doubleValues[index] = *s; break; case DDVT_FIXED: fixedValues[index] = (*s << FRACBITS); break; default: { QByteArray msg = String("DmuArgs::setValue: DDVT_INT incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(srcValueType == DDVT_SHORT) { short const *s = (short const *)src; switch(valueType) { case DDVT_BOOL: booleanValues[index] = *s; break; case DDVT_BYTE: byteValues[index] = *s; break; case DDVT_INT: intValues[index] = *s; break; case DDVT_FLOAT: floatValues[index] = *s; break; case DDVT_DOUBLE: doubleValues[index] = *s; break; case DDVT_FIXED: fixedValues[index] = (*s << FRACBITS); break; default: { QByteArray msg = String("DmuArgs::setValue: DDVT_SHORT incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(srcValueType == DDVT_ANGLE) { angle_t const *s = (angle_t const *)src; switch(valueType) { case DDVT_ANGLE: angleValues[index] = *s; break; default: { QByteArray msg = String("DmuArgs::setValue: DDVT_ANGLE incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(srcValueType == DDVT_BLENDMODE) { blendmode_t const *s = (blendmode_t const *)src; switch(valueType) { case DDVT_INT: intValues[index] = *s; break; default: { QByteArray msg = String("DmuArgs::setValue: DDVT_BLENDMODE incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else if(srcValueType == DDVT_PTR) { void const *const *s = (void const *const *)src; switch(valueType) { case DDVT_INT: // Attempt automatic conversion using P_ToIndex(). Naturally only // works with map elements. Failure leads into a fatal error. intValues[index] = P_ToIndex(*s); break; case DDVT_PTR: ptrValues[index] = (void *) *s; break; default: { QByteArray msg = String("DmuArgs::setValue: DDVT_PTR incompatible with value type %1.").arg(value_Str(valueType)).toUtf8(); App_FatalError(msg.constData()); } } } else { QByteArray msg = String("DmuArgs::setValue: unknown value type %1.").arg(srcValueType).toUtf8(); App_FatalError(msg.constData()); } } doomsday-stable-1.15.7/doomsday/client/src/world/surface.cpp0000664000175000017500000004430612641367670023352 0ustar jaakkojaakko/** @file surface.cpp World map surface. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/surface.h" #ifdef __CLIENT__ # include #endif #include #include #include #include "de_defs.h" // Def_GetGenerator #include "dd_loop.h" // frameTimePos #include "MaterialManifest" #include "TextureManifest" #include "world/map.h" #include "Plane" #include "world/worldsystem.h" // ddMapSetup #ifdef __CLIENT__ # include "gl/gl_tex.h" # include "Decoration" # include "render/rend_main.h" #endif using namespace de; DENG2_PIMPL(Surface) { int flags = 0; ///< @ref sufFlags Matrix3f tangentMatrix { Matrix3f::Zero }; ///< Tangent space vectors. bool needUpdateTangentMatrix = false; ///< @c true= marked for update. Material *material = nullptr; ///< Currently bound material. bool materialIsMissingFix = false; ///< @c true= @ref material is a "missing fix". Vector2f materialOrigin; ///< @em sharp surface space material origin. Vector3f tintColor; float opacity = 0; blendmode_t blendMode { BM_NORMAL }; #ifdef __CLIENT__ typedef QList Decorations; Decorations decorations; ///< Surface (light) decorations (owned). bool needDecorationUpdate = true; ///< @c true= An update is needed. Vector2f oldMaterialOrigin[2]; ///< Old @em sharp surface space material origins, for smoothing. Vector2f materialOriginSmoothed; ///< @em smoothed surface space material origin. Vector2f materialOriginSmoothedDelta; ///< Delta between @em sharp and @em smoothed. #endif Instance(Public *i) : Base(i) {} ~Instance() { #ifdef __CLIENT__ // Stop scroll interpolation for this surface. map().scrollingSurfaces().remove(&self); // Stop material redecoration for this surface. map().unlinkInMaterialLists(&self); qDeleteAll(decorations); #endif } inline Map &map() const { return self.map(); } inline MapElement &parent() const { return self.parent(); } #ifdef DENG_DEBUG inline bool isSideMiddle() const { return parent().type() == DMU_SIDE && &self == &parent().as().middle(); } inline bool isSectorExtraPlane() const { if(parent().type() != DMU_PLANE) return false; auto const &plane = parent().as(); return !(plane.isSectorFloor() || plane.isSectorCeiling()); } #endif void updateTangentMatrix() { needUpdateTangentMatrix = false; dfloat values[9]; Vector3f normal = tangentMatrix.column(2); V3f_Set(values + 6, normal.x, normal.y, normal.z); V3f_BuildTangents(values, values + 3, values + 6); tangentMatrix = Matrix3f(values); } void notifyMaterialOriginChanged() { DENG2_FOR_PUBLIC_AUDIENCE2(MaterialOriginChange, i) i->surfaceMaterialOriginChanged(self); #ifdef __CLIENT__ if(!ddMapSetup) { needDecorationUpdate = true; map().scrollingSurfaces().insert(&self); } #endif } void notifyNormalChanged() { DENG2_FOR_PUBLIC_AUDIENCE2(NormalChange, i) i->surfaceNormalChanged(self); } void notifyOpacityChanged() { DENG2_FOR_PUBLIC_AUDIENCE2(OpacityChange, i) i->surfaceOpacityChanged(self); } void notifyTintColorChanged() { DENG2_FOR_PUBLIC_AUDIENCE2(TintColorChange, i) i->surfaceTintColorChanged(self); } DENG2_PIMPL_AUDIENCE(MaterialOriginChange) DENG2_PIMPL_AUDIENCE(NormalChange) DENG2_PIMPL_AUDIENCE(OpacityChange) DENG2_PIMPL_AUDIENCE(TintColorChange) }; DENG2_AUDIENCE_METHOD(Surface, MaterialOriginChange) DENG2_AUDIENCE_METHOD(Surface, NormalChange) DENG2_AUDIENCE_METHOD(Surface, OpacityChange) DENG2_AUDIENCE_METHOD(Surface, TintColorChange) Surface::Surface(MapElement &owner, float opacity, Vector3f const &tintColor) : MapElement(DMU_SURFACE, &owner) , d(new Instance(this)) { d->opacity = opacity; d->tintColor = tintColor; } Matrix3f const &Surface::tangentMatrix() const { // Perform any scheduled update now. if(d->needUpdateTangentMatrix) { d->updateTangentMatrix(); } return d->tangentMatrix; } Surface &Surface::setNormal(Vector3f const &newNormal) { Vector3f const oldNormal = normal(); Vector3f const newNormalNormalized = newNormal.normalize(); if(oldNormal != newNormalNormalized) { for(int i = 0; i < 3; ++i) { d->tangentMatrix.at(i, 2) = newNormalNormalized[i]; } // We'll need to recalculate the tangents when next referenced. d->needUpdateTangentMatrix = true; d->notifyNormalChanged(); } return *this; } bool Surface::hasMaterial() const { return d->material != nullptr; } bool Surface::hasFixMaterial() const { return hasMaterial() && d->materialIsMissingFix; } Material &Surface::material() const { if(d->material) return *d->material; /// @throw MissingMaterialError Attempted with no material bound. throw MissingMaterialError("Surface::material", "No material is bound"); } Material *Surface::materialPtr() const { return hasMaterial()? &material() : nullptr; } Surface &Surface::setMaterial(Material *newMaterial, bool isMissingFix) { if(d->material == newMaterial) return *this; // Update the missing-material-fix state. if(!d->material) { if(newMaterial && isMissingFix) { d->materialIsMissingFix = true; // Sides of selfreferencing map lines should never receive fix materials. DENG2_ASSERT(!(parent().type() == DMU_SIDE && parent().as().line().isSelfReferencing())); } } else if(newMaterial && d->materialIsMissingFix) { d->materialIsMissingFix = false; } d->material = newMaterial; #ifdef __CLIENT__ // When the material changes any existing decorations are cleared. clearDecorations(); d->needDecorationUpdate = true; if(!ddMapSetup) { map().unlinkInMaterialLists(this); if(d->material) { map().linkInMaterialLists(this); if(parent().type() == DMU_PLANE) { de::Uri uri = d->material->manifest().composeUri(); ded_ptcgen_t const *def = Def_GetGenerator(reinterpret_cast(&uri)); parent().as().spawnParticleGen(def); } } } #endif // __CLIENT__ return *this; } Vector2f const &Surface::materialOrigin() const { return d->materialOrigin; } Surface &Surface::setMaterialOrigin(Vector2f const &newOrigin) { if(d->materialOrigin != newOrigin) { d->materialOrigin = newOrigin; #ifdef __CLIENT__ // During map setup we'll apply this immediately to the visual origin also. if(ddMapSetup) { d->materialOriginSmoothed = d->materialOrigin; d->materialOriginSmoothedDelta = Vector2f(); d->oldMaterialOrigin[0] = d->oldMaterialOrigin[1] = d->materialOrigin; } #endif d->notifyMaterialOriginChanged(); } return *this; } bool Surface::materialMirrorX() const { return (d->flags & DDSUF_MATERIAL_FLIPH) != 0; } bool Surface::materialMirrorY() const { return (d->flags & DDSUF_MATERIAL_FLIPV) != 0; } Vector2f Surface::materialScale() const { return Vector2f(materialMirrorX()? -1 : 1, materialMirrorY()? -1 : 1); } de::Uri Surface::composeMaterialUri() const { if(!hasMaterial()) return de::Uri(); return material().manifest().composeUri(); } #ifdef __CLIENT__ Vector2f const &Surface::materialOriginSmoothed() const { return d->materialOriginSmoothed; } Vector2f const &Surface::materialOriginSmoothedAsDelta() const { return d->materialOriginSmoothedDelta; } void Surface::lerpSmoothedMaterialOrigin() { // $smoothmaterialorigin d->materialOriginSmoothedDelta = d->oldMaterialOrigin[0] * (1 - frameTimePos) + d->materialOrigin * frameTimePos - d->materialOrigin; // Visible material origin. d->materialOriginSmoothed = d->materialOrigin + d->materialOriginSmoothedDelta; #ifdef __CLIENT__ markForDecorationUpdate(); #endif } void Surface::resetSmoothedMaterialOrigin() { // $smoothmaterialorigin d->materialOriginSmoothed = d->oldMaterialOrigin[0] = d->oldMaterialOrigin[1] = d->materialOrigin; d->materialOriginSmoothedDelta = Vector2f(); #ifdef __CLIENT__ markForDecorationUpdate(); #endif } void Surface::updateMaterialOriginTracking() { // $smoothmaterialorigin d->oldMaterialOrigin[0] = d->oldMaterialOrigin[1]; d->oldMaterialOrigin[1] = d->materialOrigin; if(d->oldMaterialOrigin[0] != d->oldMaterialOrigin[1]) { float moveDistance = de::abs(Vector2f(d->oldMaterialOrigin[1] - d->oldMaterialOrigin[0]).length()); if(moveDistance >= MAX_SMOOTH_MATERIAL_MOVE) { // Too fast: make an instantaneous jump. d->oldMaterialOrigin[0] = d->oldMaterialOrigin[1]; } } } #endif // __CLIENT__ float Surface::opacity() const { return d->opacity; } Surface &Surface::setOpacity(float newOpacity) { DENG2_ASSERT(d->isSideMiddle() || d->isSectorExtraPlane()); // sanity check newOpacity = de::clamp(0.f, newOpacity, 1.f); if(!de::fequal(d->opacity, newOpacity)) { d->opacity = newOpacity; d->notifyOpacityChanged(); } return *this; } Vector3f const &Surface::tintColor() const { return d->tintColor; } Surface &Surface::setTintColor(Vector3f const &newTintColor) { Vector3f const newColorClamped(de::clamp(0.f, newTintColor.x, 1.f), de::clamp(0.f, newTintColor.y, 1.f), de::clamp(0.f, newTintColor.z, 1.f)); if(d->tintColor != newColorClamped) { d->tintColor = newColorClamped; d->notifyTintColorChanged(); } return *this; } blendmode_t Surface::blendMode() const { return d->blendMode; } Surface &Surface::setBlendMode(blendmode_t newBlendMode) { d->blendMode = newBlendMode; return *this; } #ifdef __CLIENT__ float Surface::glow(Vector3f &color) const { if(!d->material || d->material->isSkyMasked()) { color = Vector3f(); return 0; } MaterialAnimator &matAnimator = d->material->getAnimator(Rend_MapSurfaceMaterialSpec()); // Ensure we've up to date info about the material. matAnimator.prepare(); TextureVariant *texture = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; auto const *avgColorAmplified = reinterpret_cast(texture->base().analysisDataPointer(Texture::AverageColorAmplifiedAnalysis)); if(!avgColorAmplified) throw Error("Surface::glow", "Texture \"" + texture->base().manifest().composeUri().asText() + "\" has no AverageColorAmplifiedAnalysis"); color = Vector3f(avgColorAmplified->color.rgb); return matAnimator.glowStrength() * glowFactor; // Global scale factor. } void Surface::addDecoration(Decoration *decoration) { if(!decoration) return; d->decorations.append(decoration); decoration->setSurface(this); if(hasMap()) decoration->setMap(&map()); } void Surface::clearDecorations() { qDeleteAll(d->decorations); d->decorations.clear(); } LoopResult Surface::forAllDecorations(std::function func) const { for(Decoration *decor : d->decorations) { if(auto result = func(*decor)) return result; } return LoopContinue; } int Surface::decorationCount() const { return d->decorations.count(); } void Surface::markForDecorationUpdate(bool yes) { if(ddMapSetup) return; d->needDecorationUpdate = yes; } bool Surface::needsDecorationUpdate() const { return d->needDecorationUpdate; } #endif // __CLIENT__ int Surface::property(DmuArgs &args) const { switch(args.prop) { case DMU_MATERIAL: { Material *mat = (d->materialIsMissingFix? nullptr : d->material); args.setValue(DMT_SURFACE_MATERIAL, &mat, 0); break; } case DMU_OFFSET_X: args.setValue(DMT_SURFACE_OFFSET, &d->materialOrigin.x, 0); break; case DMU_OFFSET_Y: args.setValue(DMT_SURFACE_OFFSET, &d->materialOrigin.y, 0); break; case DMU_OFFSET_XY: args.setValue(DMT_SURFACE_OFFSET, &d->materialOrigin.x, 0); args.setValue(DMT_SURFACE_OFFSET, &d->materialOrigin.y, 1); break; case DMU_TANGENT_X: args.setValue(DMT_SURFACE_TANGENT, &d->tangentMatrix.at(0, 0), 0); break; case DMU_TANGENT_Y: args.setValue(DMT_SURFACE_TANGENT, &d->tangentMatrix.at(1, 0), 0); break; case DMU_TANGENT_Z: args.setValue(DMT_SURFACE_TANGENT, &d->tangentMatrix.at(2, 0), 0); break; case DMU_TANGENT_XYZ: args.setValue(DMT_SURFACE_TANGENT, &d->tangentMatrix.at(0, 0), 0); args.setValue(DMT_SURFACE_TANGENT, &d->tangentMatrix.at(1, 0), 1); args.setValue(DMT_SURFACE_TANGENT, &d->tangentMatrix.at(2, 0), 2); break; case DMU_BITANGENT_X: args.setValue(DMT_SURFACE_BITANGENT, &d->tangentMatrix.at(0, 1), 0); break; case DMU_BITANGENT_Y: args.setValue(DMT_SURFACE_BITANGENT, &d->tangentMatrix.at(1, 1), 0); break; case DMU_BITANGENT_Z: args.setValue(DMT_SURFACE_BITANGENT, &d->tangentMatrix.at(2, 1), 0); break; case DMU_BITANGENT_XYZ: args.setValue(DMT_SURFACE_BITANGENT, &d->tangentMatrix.at(0, 1), 0); args.setValue(DMT_SURFACE_BITANGENT, &d->tangentMatrix.at(1, 1), 1); args.setValue(DMT_SURFACE_BITANGENT, &d->tangentMatrix.at(2, 1), 2); break; case DMU_NORMAL_X: args.setValue(DMT_SURFACE_NORMAL, &d->tangentMatrix.at(0, 2), 0); break; case DMU_NORMAL_Y: args.setValue(DMT_SURFACE_NORMAL, &d->tangentMatrix.at(1, 2), 0); break; case DMU_NORMAL_Z: args.setValue(DMT_SURFACE_NORMAL, &d->tangentMatrix.at(2, 2), 0); break; case DMU_NORMAL_XYZ: args.setValue(DMT_SURFACE_NORMAL, &d->tangentMatrix.at(0, 2), 0); args.setValue(DMT_SURFACE_NORMAL, &d->tangentMatrix.at(1, 2), 1); args.setValue(DMT_SURFACE_NORMAL, &d->tangentMatrix.at(2, 2), 2); break; case DMU_COLOR: args.setValue(DMT_SURFACE_RGBA, &d->tintColor.x, 0); args.setValue(DMT_SURFACE_RGBA, &d->tintColor.y, 1); args.setValue(DMT_SURFACE_RGBA, &d->tintColor.z, 2); args.setValue(DMT_SURFACE_RGBA, &d->opacity, 2); break; case DMU_COLOR_RED: args.setValue(DMT_SURFACE_RGBA, &d->tintColor.x, 0); break; case DMU_COLOR_GREEN: args.setValue(DMT_SURFACE_RGBA, &d->tintColor.y, 0); break; case DMU_COLOR_BLUE: args.setValue(DMT_SURFACE_RGBA, &d->tintColor.z, 0); break; case DMU_ALPHA: args.setValue(DMT_SURFACE_RGBA, &d->opacity, 0); break; case DMU_BLENDMODE: args.setValue(DMT_SURFACE_BLENDMODE, &d->blendMode, 0); break; case DMU_FLAGS: args.setValue(DMT_SURFACE_FLAGS, &d->flags, 0); break; default: return MapElement::property(args); } return false; // Continue iteration. } int Surface::setProperty(DmuArgs const &args) { switch(args.prop) { case DMU_BLENDMODE: { blendmode_t newBlendMode; args.value(DMT_SURFACE_BLENDMODE, &newBlendMode, 0); setBlendMode(newBlendMode); break; } case DMU_FLAGS: args.value(DMT_SURFACE_FLAGS, &d->flags, 0); break; case DMU_COLOR: { Vector3f newColor = d->tintColor; args.value(DMT_SURFACE_RGBA, &newColor.x, 0); args.value(DMT_SURFACE_RGBA, &newColor.y, 1); args.value(DMT_SURFACE_RGBA, &newColor.z, 2); setTintColor(newColor); break; } case DMU_COLOR_RED: { Vector3f newColor = d->tintColor; args.value(DMT_SURFACE_RGBA, &newColor.x, 0); setTintColor(newColor); break; } case DMU_COLOR_GREEN: { Vector3f newColor = d->tintColor; args.value(DMT_SURFACE_RGBA, &newColor.y, 0); setTintColor(newColor); break; } case DMU_COLOR_BLUE: { Vector3f newColor = d->tintColor; args.value(DMT_SURFACE_RGBA, &newColor.z, 0); setTintColor(newColor); break; } case DMU_ALPHA: { float newOpacity; args.value(DMT_SURFACE_RGBA, &newOpacity, 0); setOpacity(newOpacity); break; } case DMU_MATERIAL: { Material *newMaterial; args.value(DMT_SURFACE_MATERIAL, &newMaterial, 0); setMaterial(newMaterial); break; } case DMU_OFFSET_X: { Vector2f newOrigin = d->materialOrigin; args.value(DMT_SURFACE_OFFSET, &newOrigin.x, 0); setMaterialOrigin(newOrigin); break; } case DMU_OFFSET_Y: { Vector2f newOrigin = d->materialOrigin; args.value(DMT_SURFACE_OFFSET, &newOrigin.y, 0); setMaterialOrigin(newOrigin); break; } case DMU_OFFSET_XY: { Vector2f newOrigin = d->materialOrigin; args.value(DMT_SURFACE_OFFSET, &newOrigin.x, 0); args.value(DMT_SURFACE_OFFSET, &newOrigin.y, 1); setMaterialOrigin(newOrigin); break; } default: return MapElement::setProperty(args); } return false; // Continue iteration. } doomsday-stable-1.15.7/doomsday/client/src/world/line.cpp0000664000175000017500000007261012641367670022650 0ustar jaakkojaakko/** @file line.cpp World map line. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/line.h" #include #include #include #include "dd_main.h" // App_Materials(), verbose #include "m_misc.h" #include "Face" #include "HEdge" #include "ConvexSubspace" #include "Sector" #include "SectorCluster" #include "Surface" #include "Vertex" #include "world/maputil.h" #ifdef __CLIENT__ # include "world/map.h" # include "resource/materialdetaillayer.h" # include "resource/materialshinelayer.h" #endif #ifdef WIN32 # undef max # undef min #endif using namespace de; DENG2_PIMPL_NOREF(Line::Side::Segment) { HEdge *hedge = nullptr; ///< Half-edge attributed to the line segment (not owned). #ifdef __CLIENT__ coord_t length = 0; ///< Accurate length of the segment. coord_t lineSideOffset = 0; ///< Distance along the attributed map line at which the half-edge vertex occurs. bool frontFacing = false; #endif }; Line::Side::Segment::Segment(Line::Side &lineSide, HEdge &hedge) : MapElement(DMU_SEGMENT, &lineSide) , d(new Instance) { d->hedge = &hedge; } HEdge &Line::Side::Segment::hedge() const { DENG2_ASSERT(d->hedge); return *d->hedge; } #ifdef __CLIENT__ coord_t Line::Side::Segment::lineSideOffset() const { return d->lineSideOffset; } void Line::Side::Segment::setLineSideOffset(coord_t newOffset) { d->lineSideOffset = newOffset; } coord_t Line::Side::Segment::length() const { return d->length; } void Line::Side::Segment::setLength(coord_t newLength) { d->length = newLength; } bool Line::Side::Segment::isFrontFacing() const { return d->frontFacing; } void Line::Side::Segment::setFrontFacing(bool yes) { d->frontFacing = yes; } #endif // __CLIENT__ DENG2_PIMPL_NOREF(Line::Side) #ifdef __CLIENT__ , DENG2_OBSERVES(Line, FlagsChange) #endif { int flags = 0; ///< @ref sdefFlags Sector *sector = nullptr; ///< Attributed sector (not owned). typedef QList Segments; Segments segments; ///< On "this" side, sorted. bool needSortSegments = false; ///< set to @c true when the list needs sorting. int shadowVisCount = 0; ///< Framecount of last time shadows were drawn. /** * Line side section of which there are three (middle, bottom and top). */ struct Section { Surface surface; ThinkerT soundEmitter; Section(Line::Side &side) : surface(side) {} }; struct Sections { Section middle; Section bottom; Section top; Sections(Side &side) : middle(side), bottom(side), top(side) {} }; std::unique_ptr sections; ~Instance() { qDeleteAll(segments); } /** * Retrieve the Section associated with @a sectionId. */ Section §ionById(int sectionId) { if(sections) { switch(sectionId) { case Middle: return sections->middle; case Bottom: return sections->bottom; case Top: return sections->top; } } /// @throw Line::InvalidSectionIdError The given section identifier is not valid. throw Line::InvalidSectionIdError("Line::Side::section", QString("Invalid section id %1").arg(sectionId)); } void sortSegments(Vector2d lineSideOrigin) { needSortSegments = false; if(segments.count() < 2) return; // We'll use a QMap for sorting the segments. QMap sortedSegs; for(Segment *seg : segments) { sortedSegs.insert((seg->hedge().origin() - lineSideOrigin).length(), seg); } segments = sortedSegs.values(); } #ifdef __CLIENT__ /// Observes Line FlagsChange void lineFlagsChanged(Line &line, int oldFlags) { if(sections) { if((line.flags() & DDLF_DONTPEGTOP) != (oldFlags & DDLF_DONTPEGTOP)) { sections->top.surface.markForDecorationUpdate(); } if((line.flags() & DDLF_DONTPEGBOTTOM) != (oldFlags & DDLF_DONTPEGBOTTOM)) { sections->bottom.surface.markForDecorationUpdate(); } } } #endif }; Line::Side::Side(Line &line, Sector *sector) : MapElement(DMU_SIDE, &line) , d(new Instance) { d->sector = sector; #ifdef __CLIENT__ line.audienceForFlagsChange += d; #endif } int Line::Side::sideId() const { return &line().front() == this? Line::Front : Line::Back; } bool Line::Side::considerOneSided() const { // Are we suppressing the back sector? if(d->flags & SDF_SUPPRESS_BACK_SECTOR) return true; if(!back().hasSector()) return true; // Front side of a "one-way window"? if(!back().hasSections()) return true; if(!line().definesPolyobj()) { // If no segment is linked then the convex subspace on "this" side must // have been degenerate (thus no geometry). HEdge *hedge = leftHEdge(); if(!hedge || !hedge->twin().hasFace()) return true; if(!hedge->twin().face().mapElementAs().hasCluster()) return true; } return false; } bool Line::Side::hasSector() const { return d->sector != nullptr; } Sector &Line::Side::sector() const { if(d->sector) { return *d->sector; } /// @throw Line::MissingSectorError Attempted with no sector attributed. throw Line::MissingSectorError("Line::Side::sector", "No sector is attributed"); } bool Line::Side::hasSections() const { return bool(d->sections); } void Line::Side::addSections() { // Already defined? if(hasSections()) return; d->sections.reset(new Instance::Sections(*this)); } Surface &Line::Side::surface(int sectionId) { return d->sectionById(sectionId).surface; } Surface const &Line::Side::surface(int sectionId) const { return const_cast(this)->surface(sectionId); } SoundEmitter &Line::Side::soundEmitter(int sectionId) { return d->sectionById(sectionId).soundEmitter; } SoundEmitter const &Line::Side::soundEmitter(int sectionId) const { return const_cast(this)->soundEmitter(sectionId); } void Line::Side::clearSegments() { d->segments.clear(); d->needSortSegments = false; // An empty list is sorted. } Line::Side::Segment *Line::Side::addSegment(de::HEdge &hedge) { // Have we an exiting segment for this half-edge? for(Segment *seg : d->segments) { if(&seg->hedge() == &hedge) return seg; } // No, insert a new one. Segment *newSeg = new Segment(*this, hedge); d->segments.append(newSeg); d->needSortSegments = true; // We'll need to (re)sort. // Attribute the segment to half-edge. hedge.setMapElement(newSeg); return newSeg; } HEdge *Line::Side::leftHEdge() const { if(d->segments.isEmpty()) return nullptr; if(d->needSortSegments) { d->sortSegments(from().origin()); } return &d->segments.first()->hedge(); } HEdge *Line::Side::rightHEdge() const { if(d->segments.isEmpty()) return nullptr; if(d->needSortSegments) { d->sortSegments(from().origin()); } return &d->segments.last()->hedge(); } void Line::Side::updateSoundEmitterOrigin(int sectionId) { LOG_AS("Line::Side::updateSoundEmitterOrigin"); if(!hasSections()) return; SoundEmitter &emitter = d->sectionById(sectionId).soundEmitter; Vector2d lineCenter = line().center(); emitter.origin[VX] = lineCenter.x; emitter.origin[VY] = lineCenter.y; DENG2_ASSERT(d->sector); coord_t const ffloor = d->sector->floor().height(); coord_t const fceil = d->sector->ceiling().height(); /// @todo fixme what if considered one-sided? switch(sectionId) { case Middle: if(!back().hasSections() || line().isSelfReferencing()) { emitter.origin[VZ] = (ffloor + fceil) / 2; } else { emitter.origin[VZ] = (de::max(ffloor, back().sector().floor().height()) + de::min(fceil, back().sector().ceiling().height())) / 2; } break; case Bottom: if(!back().hasSections() || line().isSelfReferencing() || back().sector().floor().height() <= ffloor) { emitter.origin[VZ] = ffloor; } else { emitter.origin[VZ] = (de::min(back().sector().floor().height(), fceil) + ffloor) / 2; } break; case Top: if(!back().hasSections() || line().isSelfReferencing() || back().sector().ceiling().height() >= fceil) { emitter.origin[VZ] = fceil; } else { emitter.origin[VZ] = (de::max(back().sector().ceiling().height(), ffloor) + fceil) / 2; } break; } } void Line::Side::updateAllSoundEmitterOrigins() { if(!hasSections()) return; updateMiddleSoundEmitterOrigin(); updateBottomSoundEmitterOrigin(); updateTopSoundEmitterOrigin(); } void Line::Side::updateSurfaceNormals() { if(!hasSections()) return; Vector3f normal(( to().origin().y - from().origin().y) / line().length(), (from().origin().x - to().origin().x) / line().length(), 0); // All line side surfaces have the same normals. middle().setNormal(normal); // will normalize bottom().setNormal(normal); top ().setNormal(normal); } int Line::Side::flags() const { return d->flags; } void Line::Side::setFlags(int flagsToChange, FlagOp operation) { applyFlagOperation(d->flags, flagsToChange, operation); } void Line::Side::chooseSurfaceTintColors(int sectionId, Vector3f const **topColor, Vector3f const **bottomColor) const { if(hasSections()) { switch(sectionId) { case Middle: if(isFlagged(SDF_BLENDMIDTOTOP)) { *topColor = &top ().tintColor(); *bottomColor = &middle().tintColor(); } else if(isFlagged(SDF_BLENDMIDTOBOTTOM)) { *topColor = &middle().tintColor(); *bottomColor = &bottom().tintColor(); } else { *topColor = &middle().tintColor(); *bottomColor = 0; } return; case Top: if(isFlagged(SDF_BLENDTOPTOMID)) { *topColor = &top ().tintColor(); *bottomColor = &middle().tintColor(); } else { *topColor = &top ().tintColor(); *bottomColor = 0; } return; case Bottom: if(isFlagged(SDF_BLENDBOTTOMTOMID)) { *topColor = &middle().tintColor(); *bottomColor = &bottom().tintColor(); } else { *topColor = &bottom().tintColor(); *bottomColor = 0; } return; } } /// @throw Line::InvalidSectionIdError The given section identifier is not valid. throw Line::InvalidSectionIdError("Line::Side::chooseSurfaceTintColors", QString("Invalid section id %1").arg(sectionId)); } int Line::Side::shadowVisCount() const { return d->shadowVisCount; } void Line::Side::setShadowVisCount(int newCount) { d->shadowVisCount = newCount; } #ifdef __CLIENT__ static bool materialHasAnimatedTextureLayers(Material const &mat) { for(int i = 0; i < mat.layerCount(); ++i) { MaterialLayer const &layer = mat.layer(i); if(!layer.is() && !layer.is()) { if(layer.isAnimated()) return true; } } return false; } /** * Given a side section, look at the neighbouring surfaces and pick the * best choice of material used on those surfaces to be applied to "this" * surface. * * Material on back neighbour plane has priority. * Non-animated materials are preferred. * Sky materials are ignored. */ static Material *chooseFixMaterial(LineSide &side, int section) { Material *choice1 = nullptr, *choice2 = nullptr; Sector *frontSec = side.sectorPtr(); Sector *backSec = side.back().sectorPtr(); if(backSec) { // Our first choice is a material in the other sector. if(section == LineSide::Bottom) { if(frontSec->floor().height() < backSec->floor().height()) { choice1 = backSec->floorSurface().materialPtr(); } } else if(section == LineSide::Top) { if(frontSec->ceiling().height() > backSec->ceiling().height()) { choice1 = backSec->ceilingSurface().materialPtr(); } } // In the special case of sky mask on the back plane, our best // choice is always this material. if(choice1 && choice1->isSkyMasked()) { return choice1; } } else { // Our first choice is a material on an adjacent wall section. // Try the left neighbor first. Line *other = R_FindLineNeighbor(frontSec, &side.line(), side.line().vertexOwner(side.sideId()), false /*next clockwise*/); if(!other) { // Try the right neighbor. other = R_FindLineNeighbor(frontSec, &side.line(), side.line().vertexOwner(side.sideId()^1), true /*next anti-clockwise*/); } if(other) { if(!other->hasBackSector()) { // Our choice is clear - the middle material. choice1 = other->front().middle().materialPtr(); } else { // Compare the relative heights to decide. LineSide &otherSide = other->side(&other->frontSector() == frontSec? Line::Front : Line::Back); Sector &otherSec = other->side(&other->frontSector() == frontSec? Line::Back : Line::Front).sector(); if(otherSec.ceiling().height() <= frontSec->floor().height()) choice1 = otherSide.top().materialPtr(); else if(otherSec.floor().height() >= frontSec->ceiling().height()) choice1 = otherSide.bottom().materialPtr(); else if(otherSec.ceiling().height() < frontSec->ceiling().height()) choice1 = otherSide.top().materialPtr(); else if(otherSec.floor().height() > frontSec->floor().height()) choice1 = otherSide.bottom().materialPtr(); // else we'll settle for a plane material. } } } // Our second choice is a material from this sector. choice2 = frontSec->planeSurface(section == LineSide::Bottom? Sector::Floor : Sector::Ceiling).materialPtr(); // Prefer a non-animated, non-masked material. if(choice1 && !materialHasAnimatedTextureLayers(*choice1) && !choice1->isSkyMasked()) return choice1; if(choice2 && !materialHasAnimatedTextureLayers(*choice2) && !choice2->isSkyMasked()) return choice2; // Prefer a non-masked material. if(choice1 && !choice1->isSkyMasked()) return choice1; if(choice2 && !choice2->isSkyMasked()) return choice2; // At this point we'll accept anything if it means avoiding HOM. if(choice1) return choice1; if(choice2) return choice2; // We'll assign the special "missing" material... return &App_ResourceSystem().material(de::Uri("System", Path("missing"))); } static void addMissingMaterial(LineSide &side, int section) { // Sides without sections need no fixing. if(!side.hasSections()) return; // ...nor those of self-referencing lines. if(side.line().isSelfReferencing()) return; // ...nor those of "one-way window" lines. if(!side.back().hasSections() && side.back().hasSector()) return; // A material must actually be missing to qualify for fixing. Surface &surface = side.surface(section); if(surface.hasMaterial() && !surface.hasFixMaterial()) return; Material *oldMaterial = surface.materialPtr(); // Look for and apply a suitable replacement (if found). surface.setMaterial(chooseFixMaterial(side, section), true/* is missing fix */); if(oldMaterial == surface.materialPtr()) return; // We'll need to recalculate reverb. if(HEdge *hedge = side.leftHEdge()) { if(hedge->hasFace() && hedge->face().hasMapElement()) { SectorCluster &cluster = hedge->face().mapElementAs().cluster(); cluster.markReverbDirty(); cluster.markVisPlanesDirty(); } } // During map setup we log missing materials. if(ddMapSetup && verbose) { String path = surface.hasMaterial()? surface.material().manifest().composeUri().asText() : ""; LOG_WARNING("%s of Line #%d is missing a material for the %s section.\n" " %s was chosen to complete the definition.") << (side.isBack()? "Back" : "Front") << side.line().indexInMap() << (section == LineSide::Middle? "middle" : section == LineSide::Top? "top" : "bottom") << path; } } void Line::Side::fixMissingMaterials() { if(hasSector() && back().hasSector()) { Sector const &frontSec = sector(); Sector const &backSec = back().sector(); // A potential bottom section fix? if(!(frontSec.floorSurface().hasSkyMaskedMaterial() && backSec.floorSurface().hasSkyMaskedMaterial())) { if(frontSec.floor().height() < backSec.floor().height()) { addMissingMaterial(*this, LineSide::Bottom); } else if(bottom().hasFixMaterial()) { bottom().setMaterial(0); } } // A potential top section fix? if(!(frontSec.ceilingSurface().hasSkyMaskedMaterial() && backSec.ceilingSurface().hasSkyMaskedMaterial())) { if(backSec.ceiling().height() < frontSec.ceiling().height()) { addMissingMaterial(*this, LineSide::Top); } else if(top().hasFixMaterial()) { top().setMaterial(0); } } } else { // A potential middle section fix. addMissingMaterial(*this, LineSide::Middle); } } #endif // __CLIENT__ int Line::Side::property(DmuArgs &args) const { switch(args.prop) { case DMU_SECTOR: args.setValue(DMT_SIDE_SECTOR, &d->sector, 0); break; case DMU_LINE: { Line const *lineAdr = &line(); args.setValue(DMT_SIDE_LINE, &lineAdr, 0); break; } case DMU_FLAGS: args.setValue(DMT_SIDE_FLAGS, &d->flags, 0); break; default: return MapElement::property(args); } return false; // Continue iteration. } int Line::Side::setProperty(DmuArgs const &args) { switch(args.prop) { case DMU_SECTOR: { if(P_IsDummy(&line())) { args.value(DMT_SIDE_SECTOR, &d->sector, 0); } else { /// @throw WritePropertyError Sector is not writable for non-dummy sides. throw WritePropertyError("Line::Side::setProperty", QString("Property %1 is only writable for dummy Line::Sides").arg(DMU_Str(args.prop))); } break; } case DMU_FLAGS: { int newFlags; args.value(DMT_SIDE_FLAGS, &newFlags, 0); setFlags(newFlags, de::ReplaceFlags); break; } default: return MapElement::setProperty(args); } return false; // Continue iteration. } DENG2_PIMPL(Line) { int flags; ///< Public DDLF_* flags. Vertex *from; ///< Start vertex (not owned). Vertex *to; ///< End vertex (not owned). Vector2d direction; ///< From start to end vertex. binangle_t angle; ///< Calculated from the direction vector. slopetype_t slopeType; ///< Logical line slope (i.e., world angle) classification. coord_t length; ///< Accurate length. ///< Map space bounding box encompassing both vertexes. AABoxd aaBox; /// Logical sides: Side front; Side back; Polyobj *polyobj = nullptr; ///< Polyobj "this" line defines a section of, if any. int validCount = 0; ///< Used by legacy algorithms to prevent repeated processing. /// Whether the line has been mapped by each player yet. bool mapped[DDMAXPLAYERS]; Instance(Public *i, Vertex &from, Vertex &to, int flags, Sector *frontSector, Sector *backSector) : Base(i) , flags (flags) , from (&from) , to (&to) , direction(to.origin() - from.origin()) , angle (bamsAtan2(int( direction.y ), int( direction.x ))) , slopeType(M_SlopeTypeXY(direction.x, direction.y)) , length (direction.length()) , front (*i, frontSector) , back (*i, backSector) { de::zap(mapped); } void notifyFlagsChanged(int oldFlags) { DENG2_FOR_PUBLIC_AUDIENCE(FlagsChange, i) i->lineFlagsChanged(self, oldFlags); } }; Line::Line(Vertex &from, Vertex &to, int flags, Sector *frontSector, Sector *backSector) : MapElement(DMU_LINE) , d(new Instance(this, from, to, flags, frontSector, backSector)) { updateAABox(); } int Line::flags() const { return d->flags; } void Line::setFlags(int flagsToChange, FlagOp operation) { int newFlags = d->flags; applyFlagOperation(newFlags, flagsToChange, operation); if(d->flags != newFlags) { int oldFlags = d->flags; d->flags = newFlags; // Notify interested parties of the change. d->notifyFlagsChanged(oldFlags); } } bool Line::isBspWindow() const { return _bspWindowSector != nullptr; } bool Line::definesPolyobj() const { return d->polyobj != nullptr; } Polyobj &Line::polyobj() const { if(d->polyobj) { return *d->polyobj; } /// @throw Line::MissingPolyobjError Attempted with no polyobj attributed. throw Line::MissingPolyobjError("Line::polyobj", "No polyobj is attributed"); } void Line::setPolyobj(Polyobj *newPolyobj) { d->polyobj = newPolyobj; } Line::Side &Line::side(int back) { return (back? d->back : d->front); } Line::Side const &Line::side(int back) const { return (back? d->back : d->front); } Vertex &Line::vertex(int to) const { DENG2_ASSERT((to? d->to : d->from) != nullptr); return (to? *d->to : *d->from); } void Line::replaceVertex(int to, Vertex &newVertex) { if(to) d->to = &newVertex; else d->from = &newVertex; } AABoxd const &Line::aaBox() const { return d->aaBox; } void Line::updateAABox() { V2d_InitBoxXY(d->aaBox.arvec2, fromOrigin().x, fromOrigin().y); V2d_AddToBoxXY(d->aaBox.arvec2, toOrigin().x, toOrigin().y); } coord_t Line::length() const { return d->length; } Vector2d const &Line::direction() const { return d->direction; } slopetype_t Line::slopeType() const { return d->slopeType; } void Line::updateSlopeType() { d->direction = d->to->origin() - d->from->origin(); d->angle = bamsAtan2(int( d->direction.y ), int( d->direction.x )); d->slopeType = M_SlopeTypeXY(d->direction.x, d->direction.y); } binangle_t Line::angle() const { return d->angle; } int Line::boxOnSide(AABoxd const &box) const { coord_t fromOriginV1[2] = { fromOrigin().x, fromOrigin().y }; coord_t directionV1[2] = { direction().x, direction().y }; return M_BoxOnLineSide(&box, fromOriginV1, directionV1); } int Line::boxOnSide_FixedPrecision(AABoxd const &box) const { /* * Apply an offset to both the box and the line to bring everything into * the 16.16 fixed-point range. We'll use the midpoint of the line as the * origin, as typically this test is called when a bounding box is * somewhere in the vicinity of the line. The offset is floored to integers * so we won't change the discretization of the fractional part into 16-bit * precision. */ coord_t offset[2] = { std::floor(d->from->origin().x + d->direction.x/2.0), std::floor(d->from->origin().y + d->direction.y/2.0) }; fixed_t boxx[4]; boxx[BOXLEFT] = DBL2FIX(box.minX - offset[VX]); boxx[BOXRIGHT] = DBL2FIX(box.maxX - offset[VX]); boxx[BOXBOTTOM] = DBL2FIX(box.minY - offset[VY]); boxx[BOXTOP] = DBL2FIX(box.maxY - offset[VY]); fixed_t pos[2] = { DBL2FIX(d->from->origin().x - offset[VX]), DBL2FIX(d->from->origin().y - offset[VY]) }; fixed_t delta[2] = { DBL2FIX(d->direction.x), DBL2FIX(d->direction.y) }; return M_BoxOnLineSide_FixedPrecision(boxx, pos, delta); } coord_t Line::pointDistance(Vector2d const &point, coord_t *offset) const { Vector2d lineVec = d->direction - fromOrigin(); coord_t len = lineVec.length(); if(len == 0) { if(offset) *offset = 0; return 0; } Vector2d delta = fromOrigin() - point; if(offset) { *offset = ( delta.y * (fromOrigin().y - d->direction.y) - delta.x * (d->direction.x - fromOrigin().x) ) / len; } return (delta.y * lineVec.x - delta.x * lineVec.y) / len; } coord_t Line::pointOnSide(Vector2d const &point) const { Vector2d delta = fromOrigin() - point; return delta.y * d->direction.x - delta.x * d->direction.y; } bool Line::isMappedByPlayer(int playerNum) const { DENG2_ASSERT(playerNum >= 0 && playerNum < DDMAXPLAYERS); return d->mapped[playerNum]; } void Line::markMappedByPlayer(int playerNum, bool yes) { DENG2_ASSERT(playerNum >= 0 && playerNum < DDMAXPLAYERS); d->mapped[playerNum] = yes; } int Line::validCount() const { return d->validCount; } void Line::setValidCount(int newValidCount) { d->validCount = newValidCount; } int Line::property(DmuArgs &args) const { switch(args.prop) { case DMU_VERTEX0: args.setValue(DMT_LINE_V, &d->from, 0); break; case DMU_VERTEX1: args.setValue(DMT_LINE_V, &d->to, 0); break; case DMU_DX: args.setValue(DMT_LINE_DX, &d->direction.x, 0); break; case DMU_DY: args.setValue(DMT_LINE_DY, &d->direction.y, 0); break; case DMU_DXY: args.setValue(DMT_LINE_DX, &d->direction.x, 0); args.setValue(DMT_LINE_DY, &d->direction.y, 1); break; case DMU_LENGTH: args.setValue(DMT_LINE_LENGTH, &d->length, 0); break; case DMU_ANGLE: { angle_t lineAngle = BANG_TO_ANGLE(d->angle); args.setValue(DDVT_ANGLE, &lineAngle, 0); break; } case DMU_SLOPETYPE: args.setValue(DMT_LINE_SLOPETYPE, &d->slopeType, 0); break; case DMU_FLAGS: args.setValue(DMT_LINE_FLAGS, &d->flags, 0); break; case DMU_FRONT: { /// @todo Update the games so that sides without sections can be returned. Line::Side const *frontAdr = hasFrontSections()? &d->front : 0; args.setValue(DDVT_PTR, &frontAdr, 0); break; } case DMU_BACK: { /// @todo Update the games so that sides without sections can be returned. Line::Side const *backAdr = hasBackSections()? &d->back : 0; args.setValue(DDVT_PTR, &backAdr, 0); break; } case DMU_BOUNDING_BOX: if(args.valueType == DDVT_PTR) { AABoxd const *aaBoxAdr = &d->aaBox; args.setValue(DDVT_PTR, &aaBoxAdr, 0); } else { args.setValue(DMT_LINE_AABOX, &d->aaBox.minX, 0); args.setValue(DMT_LINE_AABOX, &d->aaBox.maxX, 1); args.setValue(DMT_LINE_AABOX, &d->aaBox.minY, 2); args.setValue(DMT_LINE_AABOX, &d->aaBox.maxY, 3); } break; case DMU_VALID_COUNT: args.setValue(DMT_LINE_VALIDCOUNT, &d->validCount, 0); break; default: return MapElement::property(args); } return false; // Continue iteration. } int Line::setProperty(DmuArgs const &args) { switch(args.prop) { case DMU_VALID_COUNT: args.value(DMT_LINE_VALIDCOUNT, &d->validCount, 0); break; case DMU_FLAGS: { int newFlags; args.value(DMT_LINE_FLAGS, &newFlags, 0); setFlags(newFlags, de::ReplaceFlags); break; } default: return MapElement::setProperty(args); } return false; // Continue iteration. } LineOwner *Line::vertexOwner(int to) const { DENG2_ASSERT((to? _vo2 : _vo1) != nullptr); return (to? _vo2 : _vo1); } doomsday-stable-1.15.7/doomsday/client/src/world/thinkers.cpp0000664000175000017500000003100312641367670023537 0ustar jaakkojaakko/** @file thinkers.cpp World map thinker management. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #define DENG_NO_API_MACROS_THINKER #include "de_base.h" #include "world/thinkers.h" #ifdef __CLIENT__ # include "client/cl_mobj.h" # include "world/clientmobjthinkerdata.h" #endif #ifdef __SERVER__ # include "def_main.h" # include "server/sv_pool.h" # include #endif #include "world/map.h" #include "world/p_object.h" #include #include #include dd_bool Thinker_IsMobjFunc(thinkfunc_t func) { return (func && func == reinterpret_cast(gx.MobjThinker)); } de::Map &Thinker_Map(thinker_t const & /*th*/) { /// @todo Do not assume the current map. return App_WorldSystem().map(); } namespace de { struct ThinkerList { bool isPublic; ///< All thinkers in this list are visible publically. Thinker sentinel; ThinkerList(thinkfunc_t func, bool isPublic) : isPublic(isPublic) { sentinel.function = func; sentinel.disable(); // Safety measure. sentinel.prev = sentinel.next = sentinel; } void reinit() { sentinel.prev = sentinel.next = sentinel; } thinkfunc_t function() const { return sentinel.function; } // Link the thinker to the list. void link(thinker_t &th) { sentinel.prev->next = &th; th.next = sentinel; th.prev = sentinel.prev; sentinel.prev = &th; } dint count(dint *numInStasis) const { dint num = 0; thinker_t *th = sentinel.next; while(th != &sentinel.base() && th) { #ifdef LIBDENG_FAKE_MEMORY_ZONE DENG2_ASSERT(th->next); DENG2_ASSERT(th->prev); #endif num += 1; if(numInStasis && Thinker_InStasis(th)) { (*numInStasis) += 1; } th = th->next; } return num; } void releaseAll() { for(thinker_t *th = sentinel.next; th != &sentinel.base() && th; th = th->next) { Thinker::release(*th); } } }; typedef QHash MobjHash; DENG2_PIMPL(Thinkers) { dint idtable[2048]; ///< 65536 bits telling which IDs are in use. dushort iddealer = 0; QList lists; MobjHash mobjIdLookup; ///< public only bool inited = false; Instance(Public *i) : Base(i) { clearMobjIds(); } ~Instance() { // Make sure the private instances of thinkers are released. releaseAllThinkers(); // Note that most thinkers are allocated from the memory zone // so there is no memory leak here as this memory will be purged // automatically when the map is "unloaded". qDeleteAll(lists); } void releaseAllThinkers() { for(ThinkerList *list : lists) { list->releaseAll(); } } void clearMobjIds() { de::zap(idtable); idtable[0] |= 1; // ID zero is always "used" (it's not a valid ID). mobjIdLookup.clear(); } thid_t newMobjId() { // Increment the ID dealer until a free ID is found. /// @todo fixme: What if all IDs are in use? 65535 thinkers!? while(self.isUsedMobjId(++iddealer)) {} // Mark this ID as used. self.setMobjId(iddealer); return iddealer; } ThinkerList *listForThinkFunc(thinkfunc_t func, bool makePublic = true, bool canCreate = false) { for(dint i = 0; i < lists.count(); ++i) { ThinkerList *list = lists[i]; if(list->function() == func && list->isPublic == makePublic) return list; } if(!canCreate) return nullptr; // A new thinker type. lists.append(new ThinkerList(func, makePublic)); return lists.last(); } }; Thinkers::Thinkers() : d(new Instance(this)) {} bool Thinkers::isUsedMobjId(thid_t id) { return d->idtable[id >> 5] & (1 << (id & 31) /*(id % 32) */ ); } void Thinkers::setMobjId(thid_t id, bool inUse) { dint c = id >> 5, bit = 1 << (id & 31); //(id % 32); if(inUse) d->idtable[c] |= bit; else d->idtable[c] &= ~bit; } struct mobj_s *Thinkers::mobjById(dint id) { MobjHash::const_iterator found = d->mobjIdLookup.constFind(id); if(found != d->mobjIdLookup.constEnd()) { return found.value(); } return nullptr; } void Thinkers::add(thinker_t &th, bool makePublic) { if(!th.function) throw Error("Thinkers::add", "Invalid thinker function"); // Will it need an ID? if(Thinker_IsMobjFunc(th.function)) { // It is a mobj, give it an ID (not for client mobjs, though, they // already have an id). #ifdef __CLIENT__ if(!Cl_IsClientMobj(reinterpret_cast(&th))) #endif { th.id = d->newMobjId(); } if(makePublic && th.id) { d->mobjIdLookup.insert(th.id, reinterpret_cast(&th)); } } else { th.id = 0; // Zero is not a valid ID. } // Link the thinker to the thinker list. ThinkerList *list = d->listForThinkFunc(th.function, makePublic, true /*can create*/); list->link(th); } void Thinkers::remove(thinker_t &th) { // Has got an ID? if(th.id) { // Flag the identifier as free. setMobjId(th.id, false); d->mobjIdLookup.remove(th.id); #ifdef __SERVER__ // Then it must be a mobj. auto *mob = reinterpret_cast(&th); // If the state of the mobj is the NULL state, this is a // predictable mobj removal (result of animation reaching its // end) and shouldn't be included in netGame deltas. if(!mob->state || !runtimeDefs.states.indexOf(mob->state)) { Sv_MobjRemoved(th.id); } #endif } th.function = (thinkfunc_t) -1; Thinker::release(th); } void Thinkers::initLists(dbyte flags) { if(!d->inited) { d->lists.clear(); } else { for(dint i = 0; i < d->lists.count(); ++i) { ThinkerList *list = d->lists[i]; if(list->isPublic && !(flags & 0x1)) continue; if(!list->isPublic && !(flags & 0x2)) continue; list->reinit(); } } d->clearMobjIds(); d->inited = true; } bool Thinkers::isInited() const { return d->inited; } LoopResult Thinkers::forAll(dbyte flags, std::function func) const { if(!d->inited) return LoopContinue; for(dint i = 0; i < d->lists.count(); ++i) { ThinkerList *list = d->lists[i]; if( list->isPublic && !(flags & 0x1)) continue; if(!list->isPublic && !(flags & 0x2)) continue; thinker_t *th = list->sentinel.next; while(th != &list->sentinel.base() && th) { #ifdef LIBDENG_FAKE_MEMORY_ZONE DENG2_ASSERT(th->next); DENG2_ASSERT(th->prev); #endif thinker_t *next = th->next; if(auto result = func(th)) return result; th = next; } } return LoopContinue; } LoopResult Thinkers::forAll(thinkfunc_t thinkFunc, dbyte flags, std::function func) const { if(!d->inited) return LoopContinue; if(!thinkFunc) { return forAll(flags, func); } if(flags & 0x1 /*public*/) { if(ThinkerList *list = d->listForThinkFunc(thinkFunc)) { thinker_t *th = list->sentinel.next; while(th != &list->sentinel.base() && th) { #ifdef LIBDENG_FAKE_MEMORY_ZONE DENG2_ASSERT(th->next); DENG2_ASSERT(th->prev); #endif thinker_t *next = th->next; if(auto result = func(th)) return result; th = next; } } } if(flags & 0x2 /*private*/) { if(ThinkerList *list = d->listForThinkFunc(thinkFunc, false /*private*/)) { thinker_t *th = list->sentinel.next; while(th != &list->sentinel.base() && th) { #ifdef LIBDENG_FAKE_MEMORY_ZONE DENG2_ASSERT(th->next); DENG2_ASSERT(th->prev); #endif thinker_t *next = th->next; if(auto result = func(th)) return result; th = next; } } } return LoopContinue; } dint Thinkers::count(dint *numInStasis) const { dint total = 0; if(isInited()) { for(dint i = 0; i < d->lists.count(); ++i) { ThinkerList *list = d->lists[i]; total += list->count(numInStasis); } } return total; } static void unlinkThinkerFromList(thinker_t *th) { th->next->prev = th->prev; th->prev->next = th->next; } } // namespace de using namespace de; void Thinker_InitPrivateData(thinker_t *th) { DENG2_ASSERT(th->d == nullptr); /// @todo The game should be asked to create its own private data. -jk if(Thinker_IsMobjFunc(th->function)) { #ifdef __CLIENT__ th->d = new ClientMobjThinkerData; #else th->d = new MobjThinkerData; #endif } else { // Generic thinker data (Doomsday Script namespace, etc.). th->d = new ThinkerData; } if(th->d) THINKER_DATA(*th, ThinkerData).setThinker(th); } /** * Locates a mobj by it's unique identifier in the CURRENT map. */ #undef Mobj_ById DENG_EXTERN_C struct mobj_s *Mobj_ById(dint id) { /// @todo fixme: Do not assume the current map. if(!App_WorldSystem().hasMap()) return nullptr; return App_WorldSystem().map().thinkers().mobjById(id); } #undef Thinker_Init void Thinker_Init() { /// @todo fixme: Do not assume the current map. if(!App_WorldSystem().hasMap()) return; App_WorldSystem().map().thinkers().initLists(0x1); // Init the public thinker lists. } #undef Thinker_Run void Thinker_Run() { /// @todo fixme: Do not assume the current map. if(!App_WorldSystem().hasMap()) return; App_WorldSystem().map().thinkers().forAll(0x1 | 0x2, [] (thinker_t *th) { if(Thinker_InStasis(th)) return LoopContinue; // Skip. // Time to remove it? if(th->function == (thinkfunc_t) -1) { unlinkThinkerFromList(th); if(th->id) { // Recycle for reduced allocation overhead. P_MobjRecycle((mobj_t *) th); } else { // Non-mobjs are just deleted right away. Thinker::destroy(th); } } else if(th->function) { // Create a private data instance of appropriate type. if(th->d == nullptr) Thinker_InitPrivateData(th); // Public thinker callback. th->function(th); // Private thinking. if(th->d) THINKER_DATA(*th, Thinker::IData).think(); } return LoopContinue; }); } #undef Thinker_Add void Thinker_Add(thinker_t *th) { if(!th) return; Thinker_Map(*th).thinkers().add(*th); } #undef Thinker_Remove void Thinker_Remove(thinker_t *th) { if(!th) return; Thinker_Map(*th).thinkers().remove(*th); } #undef Thinker_Iterate dint Thinker_Iterate(thinkfunc_t func, dint (*callback) (thinker_t *, void *), void *context) { if(!App_WorldSystem().hasMap()) return false; // Continue iteration. return App_WorldSystem().map().thinkers().forAll(func, 0x1, [&callback, &context] (thinker_t *th) { return callback(th, context); }); } DENG_DECLARE_API(Thinker) = { { DE_API_THINKER }, Thinker_Init, Thinker_Run, Thinker_Add, Thinker_Remove, Thinker_Iterate }; doomsday-stable-1.15.7/doomsday/client/src/world/contact.cpp0000664000175000017500000001467212641367670023360 0ustar jaakkojaakko/** @file contact.cpp World object => BSP leaf "contact" and contact lists. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_platform.h" #include "world/contact.h" #include #include #include "world/map.h" #include "world/p_object.h" #include "BspLeaf" #include "ConvexSubspace" using namespace de; ContactType Contact::type() const { return _type; } void *Contact::objectPtr() const { return _object; } Vector3d Contact::objectOrigin() const { switch(_type) { case ContactLumobj: return objectAs().origin(); case ContactMobj: return Mobj_Origin(objectAs()); default: break; } DENG2_ASSERT(false); return Vector3d(); } ddouble Contact::objectRadius() const { switch(_type) { case ContactLumobj: return objectAs().radius(); case ContactMobj: return Mobj_VisualRadius(objectAs()); default: break; } DENG2_ASSERT(false); return 0; } AABoxd Contact::objectAABox() const { switch(_type) { case ContactLumobj: return objectAs().aaBox(); case ContactMobj: return Mobj_AABox(objectAs()); default: break; } DENG2_ASSERT(false); return AABoxd(); } BspLeaf &Contact::objectBspLeafAtOrigin() const { switch(_type) { case ContactLumobj: return objectAs().bspLeafAtOrigin(); case ContactMobj: return Mobj_BspLeafAtOrigin(objectAs()); default: throw Error("Contact::objectBspLeafAtOrigin", "Invalid type"); } } struct ContactList::Node { Node *next; ///< Next in the BSP leaf. Node *nextUsed; ///< Next used contact. void *obj; }; static ContactList::Node *firstNode; ///< First unused list node. static ContactList::Node *cursor; ///< Current list node. void ContactList::reset() // static { cursor = firstNode; } void ContactList::link(Contact *contact) { if(!contact) return; Node *node = newNode(contact->objectPtr()); node->next = _head; _head = node; } ContactList::Node *ContactList::begin() const { return _head; } ContactList::Node *ContactList::newNode(void *object) // static { DENG2_ASSERT(object); Node *node; if(!cursor) { node = (Node *) Z_Malloc(sizeof(*node), PU_APPSTATIC, nullptr); // Link in the global list of used nodes. node->nextUsed = firstNode; firstNode = node; } else { node = cursor; cursor = cursor->nextUsed; } node->obj = object; node->next = nullptr; return node; } // Separate contact lists for each BSP leaf and contact type. static ContactList *subspaceContactLists; ContactList &R_ContactList(ConvexSubspace &subspace, ContactType type) { return subspaceContactLists[subspace.indexInMap() * ContactTypeCount + dint( type )]; } static Contact *contacts; static Contact *contactFirst, *contactCursor; static Contact *newContact(void *object, ContactType type) { DENG2_ASSERT(object); Contact *contact; if(!contactCursor) { contact = (Contact *) Z_Malloc(sizeof *contact, PU_APPSTATIC, nullptr); // Link in the global list of used contacts. contact->nextUsed = contactFirst; contactFirst = contact; } else { contact = contactCursor; contactCursor = contactCursor->nextUsed; } // Link in the list of in-use contacts. contact->next = contacts; contacts = contact; contact->_object = object; contact->_type = type; return contact; } void R_InitContactLists(Map &map) { // Initialize object => BspLeaf contact lists. subspaceContactLists = (ContactList *) Z_Calloc(map.subspaceCount() * ContactTypeCount * sizeof(*subspaceContactLists), PU_MAPSTATIC, nullptr); } void R_DestroyContactLists() { Z_Free(subspaceContactLists); subspaceContactLists = nullptr; } void R_ClearContactLists(Map &map) { // Start reusing contacts. contactCursor = contactFirst; contacts = nullptr; // Start reusing nodes from the first one in the list. ContactList::reset(); if(subspaceContactLists) { std::memset(subspaceContactLists, 0, map.subspaceCount() * ContactTypeCount * sizeof(*subspaceContactLists)); } } void R_AddContact(mobj_t &mobj) { // BspLeafs with no geometry cannot be contacted (zero world volume). if(Mobj_BspLeafAtOrigin(mobj).hasSubspace()) { newContact(&mobj, ContactMobj); } } void R_AddContact(Lumobj &lum) { // BspLeafs with no geometry cannot be contacted (zero world volume). if(lum.bspLeafAtOrigin().hasSubspace()) { newContact(&lum, ContactLumobj); } } LoopResult R_ForAllContacts(std::function func) { for(Contact *contact = contacts; contact; contact = contact->next) { if(auto result = func(*contact)) return result; } return LoopContinue; } LoopResult R_ForAllSubspaceMobContacts(ConvexSubspace &subspace, std::function func) { ContactList &list = R_ContactList(subspace, ContactMobj); for(ContactList::Node *node = list.begin(); node; node = node->next) { if(auto result = func(*static_cast(node->obj))) return result; } return LoopContinue; } LoopResult R_ForAllSubspaceLumContacts(ConvexSubspace &subspace, std::function func) { ContactList &list = R_ContactList(subspace, ContactLumobj); for(ContactList::Node *node = list.begin(); node; node = node->next) { if(auto result = func(*static_cast(node->obj))) return result; } return LoopContinue; } doomsday-stable-1.15.7/doomsday/client/src/world/p_ticker.cpp0000664000175000017500000000315512641367670023517 0ustar jaakkojaakko/** @file p_ticker.cpp Timed world events. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "de_base.h" #include "world/p_ticker.h" #ifdef __CLIENT__ # include "MaterialAnimator" #endif using namespace de; void P_Ticker(timespan_t elapsed) { #ifdef __CLIENT__ // Animate materials. /// @todo Each context animator should be driven by a more relevant ticker, rather /// than using the playsim's ticker for all contexts. (e.g., animators for the UI /// context should be driven separately). App_ResourceSystem().forAllMaterials([&elapsed] (Material &material) { return material.forAllAnimators([&elapsed] (MaterialAnimator &animator) { animator.animate(elapsed); return LoopContinue; }); }); #endif App_WorldSystem().tick(elapsed); } doomsday-stable-1.15.7/doomsday/client/src/world/map.cpp0000664000175000017500000034757212641367670022512 0ustar jaakkojaakko/** @file map.cpp World map. * * @todo This file has grown far too large. It should be split up through the * introduction of new abstractions / collections. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "world/map.h" #include "de_console.h" // Con_GetInteger #include "de_defs.h" #ifdef __CLIENT__ # include "clientapp.h" #endif #include "m_nodepile.h" #include "Face" #include "BspLeaf" #include "BspNode" #include "ConvexSubspace" #include "Line" #include "Polyobj" #include "Sector" #include "SectorCluster" #include "Surface" #include "Vertex" #include "world/bsp/partitioner.h" #include "world/worldsystem.h" // ddMapSetup, validCount #include "world/blockmap.h" #include "world/lineblockmap.h" #include "world/entitydatabase.h" #include "world/lineowner.h" #include "world/p_object.h" #include "world/polyobjdata.h" #include "world/sky.h" #include "world/thinkers.h" #ifdef __CLIENT__ # include "Contact" # include "ContactSpreader" # include "client/cl_mobj.h" #endif #ifdef __CLIENT__ # include "api_sound.h" #endif #ifdef __CLIENT__ # include "BiasDigest" # include "LightDecoration" # include "Lumobj" # include "SurfaceDecorator" # include "WallEdge" # include "render/viewports.h" # include "render/rend_main.h" # include "render/rend_particle.h" # include "render/skydrawable.h" #endif #include #include #include #include #include #include #include #include #include #include static int bspSplitFactor = 7; ///< cvar #ifdef __CLIENT__ static int lgMXSample = 1; ///< 5 samples per block. Cvar. /// Milliseconds it takes for Unpredictable and Hidden mobjs to be /// removed from the hash. Under normal circumstances, the special /// status should be removed fairly quickly. #define CLMOBJ_TIMEOUT 4000 #endif namespace internal { static inline de::WorldSystem &worldSys() { return App_WorldSystem(); } } // namespace internal using namespace internal; namespace de { struct EditableElements { QList lines; QList sectors; QList polyobjs; ~EditableElements() { clearAll(); } void clearAll() { qDeleteAll(lines); lines.clear(); qDeleteAll(sectors); sectors.clear(); for(Polyobj *pob : polyobjs) { pob->~Polyobj(); M_Free(pob); } polyobjs.clear(); } }; DENG2_PIMPL(Map) , DENG2_OBSERVES(bsp::Partitioner, UnclosedSectorFound) #ifdef __CLIENT__ , DENG2_OBSERVES(ThinkerData, Deletion) #endif { bool editingEnabled = true; EditableElements editable; MapDef *def = nullptr; ///< Definition for the map (not owned, may be @c NULL). AABoxd bounds; ///< Boundary points which encompass the entire map Mesh mesh; ///< All map geometries. QList sectors; QList lines; QList polyobjs; struct Bsp { BspTree *tree = nullptr; ///< Owns the BspElements. ~Bsp() { clear(); } void clear() { if(!tree) return; tree->traversePostOrder(clearUserDataWorker); delete tree; tree = nullptr; } private: static dint clearUserDataWorker(BspTree &subtree, void *) { delete subtree.userData(); return 0; } } bsp; QList subspaces; QMultiMap clusters; // // Map entities and element properties (things, line specials, etc...). // std::unique_ptr thinkers; Sky sky; EntityDatabase entityDatabase; std::unique_ptr mobjBlockmap; std::unique_ptr polyobjBlockmap; std::unique_ptr lineBlockmap; std::unique_ptr subspaceBlockmap; #ifdef __CLIENT__ struct ContactBlockmap : public Blockmap { QBitArray spreadBlocks; ///< Used to prevent repeat processing. /** * Construct a new contact blockmap. * * @param bounds Map space boundary. * @param cellSize Width and height of a cell in map space units. */ ContactBlockmap(AABoxd const &bounds, duint cellSize = 128) : Blockmap(bounds, cellSize) , spreadBlocks(width() * height()) {} void clear() { spreadBlocks.fill(false); unlinkAll(); } /** * @param contact Contact to be linked. Note that if the object's origin * lies outside the blockmap it will not be linked! */ void link(Contact &contact) { bool outside; BlockmapCell cell = toCell(contact.objectOrigin(), &outside); if(!outside) { Blockmap::link(cell, &contact); } } void spread(AABoxd const ®ion) { spreadContacts(*this, region, &spreadBlocks); } }; std::unique_ptr mobjContactBlockmap; /// @todo Redundant? std::unique_ptr lumobjContactBlockmap; #endif nodepile_t mobjNodes; nodepile_t lineNodes; nodeindex_t *lineLinks = nullptr; ///< Indices to roots. #ifdef __CLIENT__ PlaneSet trackedPlanes; SurfaceSet scrollingSurfaces; /** * All (particle) generators. */ struct Generators { struct ListNode { ListNode *next; Generator *gen; }; std::array activeGens; // We can link 64 generators each into four lists each before running out of links. static dint const LINKSTORE_SIZE = 4 * MAX_GENERATORS; ListNode *linkStore = nullptr; duint linkStoreCursor = 0; duint listsSize = 0; // Array of list heads containing links from linkStore to generators in activeGens. ListNode **lists = nullptr; ~Generators() { Z_Free(lists); Z_Free(linkStore); } /** * Resize the collection. * * @param listCount Number of lists the collection must support. */ void resize(duint listCount) { if(!linkStore) { linkStore = (ListNode *) Z_Malloc(sizeof(*linkStore) * LINKSTORE_SIZE, PU_MAP, 0); linkStoreCursor = 0; activeGens.fill(nullptr); } listsSize = listCount; lists = (ListNode **) Z_Realloc(lists, sizeof(ListNode *) * listsSize, PU_MAP); } /** * Returns an unused link from the linkStore. */ ListNode *newLink() { if(linkStoreCursor < (unsigned)LINKSTORE_SIZE) { return &linkStore[linkStoreCursor++]; } LOG_MAP_WARNING("Exhausted generator link storage"); return nullptr; } }; std::unique_ptr generators; std::unique_ptr lightGrid; /// Shadow Bias data. struct Bias { duint currentTime = 0; ///< The "current" frame in milliseconds. duint lastChangeOnFrame = 0; QList sources; ///< All bias light sources (owned). } bias; QList lumobjs; ///< All lumobjs (owned). std::unique_ptr decorator; coord_t skyFloorHeight = DDMAXFLOAT; coord_t skyCeilingHeight = DDMINFLOAT; ClMobjHash clMobjHash; #endif Instance(Public *i) : Base(i) { sky.setMap(thisPublic); sky.setIndexInMap(0); } ~Instance() { DENG2_FOR_PUBLIC_AUDIENCE(Deletion, i) i->mapBeingDeleted(self); #ifdef __CLIENT__ self.removeAllLumobjs(); self.removeAllBiasSources(); #endif // Delete thinkers before the map elements, because thinkers may reference them // in their private data destructors. thinkers.reset(); qDeleteAll(clusters); qDeleteAll(subspaces); qDeleteAll(sectors); for(Polyobj *polyobj : polyobjs) { polyobj->~Polyobj(); M_Free(polyobj); } qDeleteAll(lines); #ifdef __CLIENT__ // Stop observing client mobjs. for(mobj_t *mo : clMobjHash) { THINKER_DATA(mo->thinker, ThinkerData).audienceForDeletion() -= this; } #endif /// @todo fixme: Free all memory we have ownership of. // mobjNodes/lineNodes/lineLinks } /** * @pre Axis-aligned bounding boxes of all Sectors must be initialized. */ void updateBounds() { bool haveGeometry = false; for(Line *line : lines) { // Polyobj lines don't count. if(line->definesPolyobj()) continue; if(haveGeometry) { // Expand the bounding box. V2d_UniteBox(bounds.arvec2, line->aaBox().arvec2); } else { // The first line's bounds are used as is. V2d_CopyBox(bounds.arvec2, line->aaBox().arvec2); haveGeometry = true; } } } // Observes bsp::Partitioner UnclosedSectorFound. void unclosedSectorFound(Sector §or, Vector2d const &nearPoint) { // Notify interested parties that an unclosed sector was found. DENG2_FOR_PUBLIC_AUDIENCE(UnclosedSectorFound, i) i->unclosedSectorFound(sector, nearPoint); } /** * Notify interested parties of a "one-way window" in the map. * * @param line The window line. * @param backFacingSector Sector that the back of the line is facing. */ void notifyOneWayWindowFound(Line &line, Sector &backFacingSector) { DENG2_FOR_PUBLIC_AUDIENCE(OneWayWindowFound, i) i->oneWayWindowFound(line, backFacingSector); } struct testForWindowEffectParams { ddouble frontDist = 0; ddouble backDist = 0; Sector *frontOpen = nullptr; Sector *backOpen = nullptr; Line *frontLine = nullptr; Line *backLine = nullptr; Line *testLine = nullptr; bool castHorizontal = false; Vector2d testLineCenter; }; static void testForWindowEffect2(Line &line, testForWindowEffectParams &p) { if(&line == p.testLine) return; if(line.isSelfReferencing()) return; if(line.hasZeroLength()) return; ddouble dist = 0; Sector *hitSector = nullptr; bool isFront = false; if(p.castHorizontal) { if(de::abs(line.direction().y) < bsp::DIST_EPSILON) return; if((line.aaBox().maxY < p.testLineCenter.y - bsp::DIST_EPSILON) || (line.aaBox().minY > p.testLineCenter.y + bsp::DIST_EPSILON)) return; dist = (line.fromOrigin().x + (p.testLineCenter.y - line.fromOrigin().y) * line.direction().x / line.direction().y) - p.testLineCenter.x; isFront = ((p.testLine->direction().y > 0) != (dist > 0)); dist = de::abs(dist); // Too close? (overlapping lines?) if(dist < bsp::DIST_EPSILON) return; bool dir = (p.testLine->direction().y > 0) ^ (line.direction().y > 0); hitSector = line.side(dir ^ !isFront).sectorPtr(); } else // Cast vertically. { if(de::abs(line.direction().x) < bsp::DIST_EPSILON) return; if((line.aaBox().maxX < p.testLineCenter.x - bsp::DIST_EPSILON) || (line.aaBox().minX > p.testLineCenter.x + bsp::DIST_EPSILON)) return; dist = (line.fromOrigin().y + (p.testLineCenter.x - line.fromOrigin().x) * line.direction().y / line.direction().x) - p.testLineCenter.y; isFront = ((p.testLine->direction().x > 0) == (dist > 0)); dist = de::abs(dist); bool dir = (p.testLine->direction().x > 0) ^ (line.direction().x > 0); hitSector = line.side(dir ^ !isFront).sectorPtr(); } // Too close? (overlapping lines?) if(dist < bsp::DIST_EPSILON) return; if(isFront) { if(dist < p.frontDist) { p.frontDist = dist; p.frontOpen = hitSector; p.frontLine = &line; } } else { if(dist < p.backDist) { p.backDist = dist; p.backOpen = hitSector; p.backLine = &line; } } } bool lineMightHaveWindowEffect(Line const &line) { if(line.definesPolyobj()) return false; if(line.hasFrontSector() && line.hasBackSector()) return false; if(!line.hasFrontSector()) return false; if(line.hasZeroLength()) return false; // Look for window effects by checking for an odd number of one-sided // line owners for a single vertex. Idea courtesy of Graham Jackson. if((line.from()._onesOwnerCount % 2) == 1 && (line.from()._onesOwnerCount + line.from()._twosOwnerCount) > 1) return true; if((line.to()._onesOwnerCount % 2) == 1 && (line.to()._onesOwnerCount + line.to()._twosOwnerCount) > 1) return true; return false; } void findOneWayWindows() { for(Vertex *vertex : mesh.vertexs()) { // Count the total number of one and two-sided line owners for each // vertex. (Used in the process of locating window effect lines.) vertex->countLineOwners(); } // Search for "one-way window" effects. for(Line *line : lines) { if(!lineMightHaveWindowEffect(*line)) continue; testForWindowEffectParams p; p.frontDist = p.backDist = DDMAXFLOAT; p.testLine = line; p.testLineCenter = line->center(); p.castHorizontal = (de::abs(line->direction().x) < de::abs(line->direction().y)? true : false); AABoxd scanRegion = bounds; if(p.castHorizontal) { scanRegion.minY = line->aaBox().minY - bsp::DIST_EPSILON; scanRegion.maxY = line->aaBox().maxY + bsp::DIST_EPSILON; } else { scanRegion.minX = line->aaBox().minX - bsp::DIST_EPSILON; scanRegion.maxX = line->aaBox().maxX + bsp::DIST_EPSILON; } validCount++; self.forAllLinesInBox(scanRegion, LIF_SECTOR, [&p] (Line &line) { testForWindowEffect2(line, p); return LoopContinue; }); if(p.backOpen && p.frontOpen && line->frontSectorPtr() == p.backOpen) { notifyOneWayWindowFound(*line, *p.frontOpen); line->_bspWindowSector = p.frontOpen; /// @todo Refactor away. } } } /** * Build a new BSP tree. * * @pre Map line bounds have been determined and a line blockmap constructed. */ bool buildBspTree() { DENG2_ASSERT(bsp.tree == nullptr); DENG2_ASSERT(subspaces.isEmpty()); // It begins... Time begunAt; LOGDEV_MAP_XVERBOSE("Building BSP for \"%s\" with split cost factor %d...") << (def? def->composeUri() : "(unknown map)") << bspSplitFactor; // First we'll scan for so-called "one-way window" constructs and mark // them so that the space partitioner can treat them specially. findOneWayWindows(); // Remember the current next vertex ordinal as we'll need to index any // new vertexes produced during the build process. dint nextVertexOrd = mesh.vertexCount(); // Determine the set of lines for which we will build a BSP. auto linesToBuildFor = QSet::fromList(lines); // Polyobj lines should be excluded. for(Polyobj *pob : polyobjs) for(Line *line : pob->lines()) { linesToBuildFor.remove(line); } try { // Configure a space partitioner. bsp::Partitioner partitioner(bspSplitFactor); partitioner.audienceForUnclosedSectorFound += this; // Build a new BSP tree. bsp.tree = partitioner.makeBspTree(linesToBuildFor, mesh); DENG2_ASSERT(bsp.tree); LOG_MAP_VERBOSE("BSP built: %s. With %d Segments and %d Vertexes.") << bsp.tree->summary() << partitioner.segmentCount() << partitioner.vertexCount(); // Attribute an index to any new vertexes. for(dint i = nextVertexOrd; i < mesh.vertexCount(); ++i) { Vertex *vtx = mesh.vertexs().at(i); vtx->setMap(thisPublic); vtx->setIndexInMap(i); } #ifdef DENG2_QT_4_7_OR_NEWER /// @todo Determine the actual number of subspaces needed. subspaces.reserve(bsp.tree->leafCount()); #endif // Iterative pre-order traversal of the map element tree. BspTree const *cur = bsp.tree; BspTree const *prev = nullptr; while(cur) { while(cur) { if(cur->userData()) { if(cur->isLeaf()) { auto &leaf = cur->userData()->as(); if(!leaf.sectorPtr()) { LOG_MAP_WARNING("BSP leaf %p has degenerate geometry (%d half-edges).") << &leaf << (leaf.hasSubspace()? leaf.subspace().poly().hedgeCount() : 0); } if(leaf.hasSubspace()) { // Add this subspace to the LUT. ConvexSubspace &subspace = leaf.subspace(); subspace.setIndexInMap(subspaces.count()); subspaces.append(&subspace); #ifdef DENG_DEBUG // See if we received a partial geometry... dint discontinuities = 0; HEdge *hedge = subspace.poly().hedge(); do { if(hedge->next().origin() != hedge->twin().origin()) { discontinuities++; } } while((hedge = &hedge->next()) != subspace.poly().hedge()); if(discontinuities) { LOG_MAP_WARNING("Face geometry for BSP leaf [%p] at %s in sector %i " "is not contiguous (%i gaps/overlaps).\n%s") << &leaf << subspace.poly().center().asText() << (leaf.sectorPtr()? leaf.sectorPtr()->indexInArchive() : -1) << discontinuities << subspace.poly().description(); } #endif } } } if(prev == cur->parentPtr()) { // Descending - right first, then left. prev = cur; if(cur->hasRight()) cur = cur->rightPtr(); else cur = cur->leftPtr(); } else if(prev == cur->rightPtr()) { // Last moved up the right branch - descend the left. prev = cur; cur = cur->leftPtr(); } else if(prev == cur->leftPtr()) { // Last moved up the left branch - continue upward. prev = cur; cur = cur->parentPtr(); } } if(prev) { // No left child - back up. cur = prev->parentPtr(); } } } catch(Error const &er) { LOG_MAP_WARNING("%s.") << er.asText(); } // How much time did we spend? LOGDEV_MAP_VERBOSE("BSP built in %.2f seconds") << begunAt.since(); return bsp.tree != nullptr; } /** * (Re)Build subspace clusters for the sector. */ void buildClusters(Sector §or) { for(auto it = clusters.find(§or); it != clusters.end() && it.key() == §or; ) { delete *it; } clusters.remove(§or); typedef QList Subspaces; typedef QList SubspaceSets; SubspaceSets subspaceSets; // Separate the subspaces into edge-adjacency clusters. We'll do this by // starting with a set per subspace and then keep merging these sets until // no more shared edges are found. for(ConvexSubspace *subspace : subspaces) { if(subspace->bspLeaf().sectorPtr() == §or) { subspaceSets.append(Subspaces()); subspaceSets.last().append(subspace); } } if(subspaceSets.isEmpty()) return; // Merge sets whose subspaces share a common edge. while(subspaceSets.count() > 1) { bool didMerge = false; for(dint i = 0; i < subspaceSets.count(); ++i) for(dint k = 0; k < subspaceSets.count(); ++k) { if(i == k) continue; for(ConvexSubspace *subspace : subspaceSets[i]) { HEdge *baseHEdge = subspace->poly().hedge(); HEdge *hedge = baseHEdge; do { if(hedge->twin().hasFace() && hedge->twin().face().hasMapElement()) { auto &otherSubspace = hedge->twin().face().mapElementAs(); if(otherSubspace.bspLeaf().sectorPtr() == §or && subspaceSets[k].contains(&otherSubspace)) { // Merge k into i. subspaceSets[i].append(subspaceSets[k]); subspaceSets.removeAt(k); // Compare the next pair. if(i >= k) i -= 1; k -= 1; // We'll need to repeat in any case. didMerge = true; break; } } } while((hedge = &hedge->next()) != baseHEdge); if(didMerge) break; } } if(!didMerge) break; } // Clustering complete. // Build clusters. for(Subspaces const &subspaceSet : subspaceSets) { // Subspace ownership is not given to the cluster. clusters.insert(§or, new SectorCluster(subspaceSet)); } } /// @return @c true= @a mobj was unlinked successfully. bool unlinkMobjFromSectors(mobj_t &mobj) { if(Mobj_IsSectorLinked(&mobj)) { Mobj_Sector(&mobj)->unlink(&mobj); return true; } return false; } /** * Construct an initial (empty) line blockmap. * * @pre Coordinate space bounds have already been determined. */ void initLineBlockmap(ddouble margin = 8) { // Setup the blockmap area to enclose the whole map, plus a margin // (margin is needed for a map that fits entirely inside one blockmap cell). lineBlockmap.reset( new LineBlockmap(AABoxd(bounds.minX - margin, bounds.minY - margin, bounds.maxX + margin, bounds.maxY + margin))); LOG_MAP_VERBOSE("Line blockmap dimensions:") << lineBlockmap->dimensions().asText(); // Populate the blockmap. lineBlockmap->link(lines); } /** * Construct an initial (empty) mobj blockmap. * * @pre Coordinate space bounds have already been determined. */ void initMobjBlockmap(ddouble margin = 8) { // Setup the blockmap area to enclose the whole map, plus a margin // (margin is needed for a map that fits entirely inside one blockmap cell). mobjBlockmap.reset( new Blockmap(AABoxd(bounds.minX - margin, bounds.minY - margin, bounds.maxX + margin, bounds.maxY + margin))); LOG_MAP_VERBOSE("Mobj blockmap dimensions:") << mobjBlockmap->dimensions().asText(); } /** * Unlinks the mobj from all the lines it's been linked to. Can be called * without checking that the list does indeed contain lines. */ bool unlinkMobjFromLines(mobj_t &mo) { // Try unlinking from lines. if(!mo.lineRoot) return false; // A zero index means it's not linked. // Unlink from each line. linknode_t *tn = mobjNodes.nodes; for(nodeindex_t nix = tn[mo.lineRoot].next; nix != mo.lineRoot; nix = tn[nix].next) { // Data is the linenode index that corresponds this mobj. NP_Unlink((&lineNodes), tn[nix].data); // We don't need these nodes any more, mark them as unused. // Dismissing is a macro. NP_Dismiss((&lineNodes), tn[nix].data); NP_Dismiss((&mobjNodes), nix); } // The mobj no longer has a line ring. NP_Dismiss((&mobjNodes), mo.lineRoot); mo.lineRoot = 0; return true; } /** * @note Caller must ensure a mobj is linked only once to any given line. * * @param mob Map-object to be linked. * @param line Line to link the mobj to. */ void linkMobjToLine(mobj_t *mob, Line *line) { if(!mob || !line) return; // Lines with only one sector will not be linked to because a mobj can't // legally cross one. if(!line->hasFrontSector()) return; if(!line->hasBackSector()) return; // Add a node to the mobj's ring. nodeindex_t nodeIndex = NP_New(&mobjNodes, line); NP_Link(&mobjNodes, nodeIndex, mob->lineRoot); // Add a node to the line's ring. Also store the linenode's index // into the mobjring's node, so unlinking is easy. nodeIndex = mobjNodes.nodes[nodeIndex].data = NP_New(&lineNodes, mob); NP_Link(&lineNodes, nodeIndex, lineLinks[line->indexInMap()]); } /** * @note Caller must ensure that the map-object @a mob is @em not linked. */ void linkMobjToLines(mobj_t &mob) { AABoxd const box = Mobj_AABox(mob); // Get a new root node. mob.lineRoot = NP_New(&mobjNodes, NP_ROOT_NODE); validCount++; self.forAllLinesInBox(box, [this, &mob, &box] (Line &line) { // Do the bounding boxes intercept? if(!(box.minX >= line.aaBox().maxX || box.minY >= line.aaBox().maxY || box.maxX <= line.aaBox().minX || box.maxY <= line.aaBox().minY)) { // Line crosses the mobj's bounding box? if(!line.boxOnSide(box)) { this->linkMobjToLine(&mob, &line); } } return LoopContinue; }); } /** * Construct an initial (empty) polyobj blockmap. * * @pre Coordinate space bounds have already been determined. */ void initPolyobjBlockmap(ddouble margin = 8) { // Setup the blockmap area to enclose the whole map, plus a margin // (margin is needed for a map that fits entirely inside one blockmap cell). polyobjBlockmap.reset( new Blockmap(AABoxd(bounds.minX - margin, bounds.minY - margin, bounds.maxX + margin, bounds.maxY + margin))); LOG_MAP_VERBOSE("Polyobj blockmap dimensions:") << polyobjBlockmap->dimensions().asText(); } /** * Construct an initial (empty) convex subspace blockmap. * * @pre Coordinate space bounds have already been determined. */ void initSubspaceBlockmap(ddouble margin = 8) { // Setup the blockmap area to enclose the whole map, plus a margin // (margin is needed for a map that fits entirely inside one blockmap cell). subspaceBlockmap.reset( new Blockmap(AABoxd(bounds.minX - margin, bounds.minY - margin, bounds.maxX + margin, bounds.maxY + margin))); LOG_MAP_VERBOSE("Convex subspace blockmap dimensions:") << subspaceBlockmap->dimensions().asText(); // Populate the blockmap. for(ConvexSubspace *subspace : subspaces) { subspaceBlockmap->link(subspace->poly().aaBox(), subspace); } } #ifdef __CLIENT__ void initContactBlockmaps(ddouble margin = 8) { // Setup the blockmap area to enclose the whole map, plus a margin // (margin is needed for a map that fits entirely inside one blockmap cell). AABoxd expandedBounds(bounds.minX - margin, bounds.minY - margin, bounds.maxX + margin, bounds.maxY + margin); mobjContactBlockmap.reset(new ContactBlockmap(expandedBounds)); lumobjContactBlockmap.reset(new ContactBlockmap(expandedBounds)); } /** * Returns the appropriate contact blockmap for the specified contact @a type. */ ContactBlockmap &contactBlockmap(ContactType type) { switch(type) { case ContactMobj: return *mobjContactBlockmap; case ContactLumobj: return *lumobjContactBlockmap; default: throw Error("Map::contactBlockmap", "Invalid contact type"); } } /** * To be called to link all contacts into the contact blockmaps. * * @todo Why don't we link contacts immediately? -ds */ void linkAllContacts() { R_ForAllContacts([this] (Contact const &contact) { contactBlockmap(contact.type()).link(const_cast(contact)); return LoopContinue; }); } // Clear the "contact" blockmaps (BSP leaf => object). void removeAllContacts() { mobjContactBlockmap->clear(); lumobjContactBlockmap->clear(); R_ClearContactLists(*thisPublic); } #endif /** * Locate a polyobj by sound emitter. * * @param soundEmitter SoundEmitter to search for. * * @return Pointer to the referenced Polyobj instance; otherwise @c nullptr. */ Polyobj *polyobjBySoundEmitter(SoundEmitter const &soundEmitter) const { for(Polyobj *polyobj : polyobjs) { if(&soundEmitter == &polyobj->soundEmitter()) return polyobj; } return nullptr; // Not found. } /** * Locate a sector by sound emitter. * * @param soundEmitter SoundEmitter to search for. * * @return Pointer to the referenced Sector instance; otherwise @c nullptr. */ Sector *sectorBySoundEmitter(SoundEmitter const &soundEmitter) const { for(Sector *sector : sectors) { if(&soundEmitter == §or->soundEmitter()) return sector; } return nullptr; // Not found. } /** * Locate a sector plane by sound emitter. * * @param soundEmitter SoundEmitter to search for. * * @return Pointer to the referenced Plane instance; otherwise @c nullptr. */ Plane *planeBySoundEmitter(SoundEmitter const &soundEmitter) const { Plane *found = nullptr; // Not found. for(Sector *sector : sectors) { LoopResult located = sector->forAllPlanes([&soundEmitter, &found] (Plane &plane) { if(&soundEmitter == &plane.soundEmitter()) { found = &plane; return LoopAbort; } return LoopContinue; }); if(located) break; } return found; } /** * Locate a surface by sound emitter. * * @param soundEmitter SoundEmitter to search for. * * @return Pointer to the referenced Surface instance; otherwise @c nullptr. */ Surface *surfaceBySoundEmitter(SoundEmitter const &soundEmitter) const { // Perhaps a wall surface? for(Line *line : lines) for(dint i = 0; i < 2; ++i) { LineSide &side = line->side(i); if(!side.hasSections()) continue; if(&soundEmitter == &side.middleSoundEmitter()) { return &side.middle(); } if(&soundEmitter == &side.bottomSoundEmitter()) { return &side.bottom(); } if(&soundEmitter == &side.topSoundEmitter()) { return &side.top(); } } return nullptr; // Not found. } #ifdef __CLIENT__ SurfaceDecorator &surfaceDecorator() { if(!decorator) { decorator.reset(new SurfaceDecorator); } return *decorator; } /** * Interpolate the smoothed height of planes. */ void lerpTrackedPlanes(bool resetNextViewer) { if(resetNextViewer) { // Reset the plane height trackers. for(Plane *plane : trackedPlanes) { plane->resetSmoothedHeight(); } // Tracked movement is now all done. trackedPlanes.clear(); } // While the game is paused there is no need to smooth. else //if(!clientPaused) { QMutableSetIterator iter(trackedPlanes); while(iter.hasNext()) { Plane *plane = iter.next(); plane->lerpSmoothedHeight(); // Has this plane reached its destination? if(de::fequal(plane->heightSmoothed(), plane->height())) { iter.remove(); } } } } /** * Interpolate the smoothed material origin of surfaces. */ void lerpScrollingSurfaces(bool resetNextViewer) { if(resetNextViewer) { // Reset the surface material origin trackers. for(Surface *surface : scrollingSurfaces) { surface->resetSmoothedMaterialOrigin(); } // Tracked movement is now all done. scrollingSurfaces.clear(); } // While the game is paused there is no need to smooth. else //if(!clientPaused) { QMutableSetIterator iter(scrollingSurfaces); while(iter.hasNext()) { Surface *surface = iter.next(); surface->lerpSmoothedMaterialOrigin(); // Has this material reached its destination? if(surface->materialOriginSmoothed() == surface->materialOrigin()) { iter.remove(); } } } } /** * Perform preprocessing which must be done before rendering a frame. */ void biasBeginFrame() { if(!useBias) return; // The time that applies on this frame. bias.currentTime = Timer_RealMilliseconds(); // Check which sources have changed and update the trackers for any // affected surfaces. BiasDigest allChanges; bool needUpdateSurfaces = false; for(dint i = 0; i < bias.sources.count(); ++i) { BiasSource *bsrc = bias.sources.at(i); if(bsrc->trackChanges(allChanges, i, bias.currentTime)) { // We'll need to redetermine source => surface affection. needUpdateSurfaces = true; } } if(!needUpdateSurfaces) return; // Apply changes to all surfaces: bias.lastChangeOnFrame = R_FrameCount(); for(SectorCluster *cluster : clusters) { cluster->applyBiasDigest(allChanges); } } /** * Create new mobj => BSP leaf contacts. */ void generateMobjContacts() { for(Sector *sector : sectors) for(mobj_t *iter = sector->firstMobj(); iter; iter = iter->sNext) { R_AddContact(*iter); } } void generateLumobjs(Surface const &surface) { surface.forAllDecorations([this] (Decoration &decor) { if(auto const *decorLight = decor.maybeAs()) { std::unique_ptr lum(decorLight->generateLumobj()); if(lum) { self.addLumobj(*lum); // a copy is made. } } return LoopContinue; }); } /** * Perform lazy initialization of the generator collection. */ Generators &getGenerators() { // Time to initialize a new collection? if(!generators) { generators.reset(new Generators); generators->resize(sectors.count()); } return *generators; } /** * Lookup the next available generator id. * * @todo Optimize: Cache this result. * @todo Optimize: We could maintain an age-sorted list of generators. * * @return The next available id else @c 0 iff there are no unused ids. */ Generator::Id findIdForNewGenerator() { Generators &gens = getGenerators(); // Prefer allocating a new generator if we've a spare id. duint unused = 0; for(; unused < gens.activeGens.size(); ++unused) { if(!gens.activeGens[unused]) break; } if(unused < gens.activeGens.size()) return Generator::Id(unused + 1); // See if there is an active, non-static generator we can supplant. Generator *oldest = nullptr; for(Generator *gen : gens.activeGens) { if(!gen || gen->isStatic()) continue; if(!oldest || gen->age() > oldest->age()) { oldest = gen; } } return (oldest? oldest->id() : 0); } void spawnMapParticleGens() { if(!def) return; for(dint i = 0; i < defs.ptcGens.size(); ++i) { ded_ptcgen_t *genDef = &defs.ptcGens[i]; if(!genDef->map) continue; if(*genDef->map != def->composeUri()) continue; // Are we still spawning using this generator? if(genDef->spawnAge > 0 && worldSys().time() > genDef->spawnAge) continue; Generator *gen = self.newGenerator(); if(!gen) return; // No more generators. // Initialize the particle generator. gen->count = genDef->particles; gen->spawnRateMultiplier = 1; gen->configureFromDef(genDef); gen->setUntriggered(); // Is there a need to pre-simulate? gen->presimulate(genDef->preSim); } } /** * Spawns all type-triggered particle generators, regardless of whether * the type of mobj exists in the map or not (mobjs might be dynamically * created). */ void spawnTypeParticleGens() { for(dint i = 0; i < defs.ptcGens.size(); ++i) { ded_ptcgen_t *def = &defs.ptcGens[i]; if(def->typeNum != DED_PTCGEN_ANY_MOBJ_TYPE && def->typeNum < 0) continue; Generator *gen = self.newGenerator(); if(!gen) return; // No more generators. // Initialize the particle generator. gen->count = def->particles; gen->spawnRateMultiplier = 1; gen->configureFromDef(def); gen->type = def->typeNum; gen->type2 = def->type2Num; // Is there a need to pre-simulate? gen->presimulate(def->preSim); } } dint findDefForGenerator(Generator *gen) { DENG2_ASSERT(gen); // Search for a suitable definition. for(dint i = 0; i < defs.ptcGens.size(); ++i) { ded_ptcgen_t *def = &defs.ptcGens[i]; // A type generator? if(def->typeNum == DED_PTCGEN_ANY_MOBJ_TYPE && gen->type == DED_PTCGEN_ANY_MOBJ_TYPE) { return i + 1; // Stop iteration. } if(def->typeNum >= 0 && (gen->type == def->typeNum || gen->type2 == def->type2Num)) { return i + 1; // Stop iteration. } // A damage generator? if(gen->source && gen->source->type == def->damageNum) { return i + 1; // Stop iteration. } // A flat generator? if(gen->plane && def->material) { try { Material *defMat = &App_ResourceSystem().material(*def->material); Material *mat = gen->plane->surface().materialPtr(); if(def->flags & Generator::SpawnFloor) mat = gen->plane->sector().floorSurface().materialPtr(); if(def->flags & Generator::SpawnCeiling) mat = gen->plane->sector().ceilingSurface().materialPtr(); // Is this suitable? if(mat == defMat) { return i + 1; // 1-based index. } #if 0 /// @todo $revise-texture-animation if(def->flags & PGF_GROUP) { // Generator triggered by all materials in the animation. if(Material_IsGroupAnimated(defMat) && Material_IsGroupAnimated(mat) && &Material_AnimGroup(defMat) == &Material_AnimGroup(mat)) { // Both are in this animation! This def will do. return i + 1; // 1-based index. } } #endif } catch(MaterialManifest::MissingMaterialError const &) {} // Ignore this error. catch(ResourceSystem::MissingManifestError const &) {} // Ignore this error. } // A state generator? if(gen->source && def->state[0] && runtimeDefs.states.indexOf(gen->source->state) == Def_GetStateNum(def->state)) { return i + 1; // 1-based index. } } return 0; // Not found. } /** * Update existing generators in the map following an engine reset. */ void updateParticleGens() { for(Generator *gen : getGenerators().activeGens) { // Only consider active generators. if(!gen) continue; // Map generators cannot be updated (we have no means to reliably // identify them), so destroy them. if(gen->isUntriggered()) { Generator_Delete(gen); continue; // Continue iteration. } if(dint defIndex = findDefForGenerator(gen)) { // Update the generator using the new definition. gen->def = &defs.ptcGens[defIndex - 1]; } else { // Nothing else we can do, destroy it. Generator_Delete(gen); } } // Re-spawn map generators. spawnMapParticleGens(); } /** * Link all generated particles into the map so that they will be drawn. * * @todo Overkill? */ void linkAllParticles() { //LOG_AS("Map::linkAllParticles"); Generators &gens = getGenerators(); // Empty all generator lists. std::memset(gens.lists, 0, sizeof(*gens.lists) * gens.listsSize); gens.linkStoreCursor = 0; if(useParticles) { for(Generator *gen : gens.activeGens) { if(!gen) continue; ParticleInfo const *pInfo = gen->particleInfo(); for(dint i = 0; i < gen->count; ++i, pInfo++) { if(pInfo->stage < 0 || !pInfo->bspLeaf) continue; dint listIndex = pInfo->bspLeaf->sectorPtr()->indexInMap(); DENG2_ASSERT((unsigned)listIndex < gens.listsSize); // Must check that it isn't already there... bool found = false; for(Generators::ListNode *it = gens.lists[listIndex]; it; it = it->next) { if(it->gen == gen) { // Warning message disabled as these are occuring so thick and fast // that logging is pointless (and negatively affecting performance). //LOGDEV_MAP_VERBOSE("Attempted repeat link of generator %p to list %u.") // << gen << listIndex; found = true; // No, no... } } if(found) continue; // We need a new link. if(Generators::ListNode *link = gens.newLink()) { link->gen = gen; link->next = gens.lists[listIndex]; gens.lists[listIndex] = link; } } } } } void thinkerBeingDeleted(thinker_s &th) { clMobjHash.remove(th.id); } #endif // __CLIENT__ }; Map::Map(MapDef *mapDefinition) : d(new Instance(this)) { setDef(mapDefinition); } MapDef *Map::def() const { return d->def; } void Map::setDef(MapDef *newMapDefinition) { d->def = newMapDefinition; } Record const &Map::mapInfo() const { return worldSys().mapInfoForMapUri(def()? def()->composeUri() : de::Uri("Maps:", RC_NULL)); } Mesh const &Map::mesh() const { return d->mesh; } bool Map::hasBspTree() const { return d->bsp.tree != nullptr; } Map::BspTree const &Map::bspTree() const { if(d->bsp.tree) { return *d->bsp.tree; } /// @throw MissingBspTreeError Attempted with no BSP tree available. throw MissingBspTreeError("Map::bspTree", "No BSP tree is available"); } #ifdef __CLIENT__ bool Map::hasLightGrid() { return bool(d->lightGrid); } LightGrid &Map::lightGrid() { if(bool(d->lightGrid)) { return *d->lightGrid; } /// @throw MissingLightGrid Attempted with no LightGrid initialized. throw MissingLightGridError("Map::lightGrid", "No light grid is initialized"); } void Map::initLightGrid() { // Disabled? if(!Con_GetInteger("rend-bias-grid")) return; // Diagonal in maze arrangement of natural numbers. // Up to 65 samples per-block(!) static dint const MSFACTORS = 7; static dint multisample[] = { 1, 5, 9, 17, 25, 37, 49, 65 }; // Time to initialize the LightGrid? if(bool(d->lightGrid)) { d->lightGrid->updateIfNeeded(); return; } Time begunAt; d->lightGrid.reset(new LightGrid(origin(), dimensions())); LightGrid &lg = *d->lightGrid; // Determine how many sector cluster samples we'll make per block and // allocate the tempoary storage. dint const numSamples = multisample[de::clamp(0, lgMXSample, MSFACTORS)]; QVector samplePoints(numSamples); QVector sampleHits(numSamples); /// It would be possible to only allocate memory for the unique /// sample results. And then select the appropriate sample in the loop /// for initializing the grid instead of copying the previous results in /// the loop for acquiring the sample points. /// /// Calculate with the equation (number of unique sample points): /// /// ((1 + lgBlockHeight * lgMXSample) * (1 + lgBlockWidth * lgMXSample)) + /// (size % 2 == 0? numBlocks : 0) /// OR /// /// We don't actually need to store the ENTIRE ssample array. It would be /// sufficent to only store the results from the start of the previous row /// to current col index. This would save a bit of memory. /// /// However until lightgrid init is finalized it would be rather silly to /// optimize this much further. // Allocate memory for all the sample results. QVector ssamples((lg.dimensions().x * lg.dimensions().y) * numSamples); // Determine the size^2 of the samplePoint array plus its center. dint size = 0, center = 0; if(numSamples > 1) { dfloat f = sqrt(dfloat( numSamples )); if(std::ceil(f) != std::floor(f)) { size = sqrt(dfloat( numSamples - 1 )); center = 0; } else { size = dint( f ); center = size + 1; } } // Construct the sample point offset array. // This way we can use addition only during calculation of: // (dimensions.y * dimensions.x) * numSamples if(center == 0) { // Zero is the center so do that first. samplePoints[0] = Vector2d(lg.blockSize() / 2, lg.blockSize() / 2); } if(numSamples > 1) { ddouble bSize = ddouble(lg.blockSize()) / (size - 1); // Is there an offset? dint idx = (center == 0? 1 : 0); for(dint y = 0; y < size; ++y) for(dint x = 0; x < size; ++x, ++idx) { samplePoints[idx] = Vector2d(de::round(x * bSize), de::round(y * bSize)); } } // Acquire the sector clusters at ALL the sample points. for(dint y = 0; y < lg.dimensions().y; ++y) for(dint x = 0; x < lg.dimensions().x; ++x) { LightGrid::Index const blk = lg.toIndex(x, y); Vector2d const off(x * lg.blockSize(), y * lg.blockSize()); dint sampleOffset = 0; if(center == 0) { // Center point is not considered with the term 'size'. // Sample this point and place at index 0 (at the start of the // samples for this block). ssamples[blk * numSamples] = clusterAt(lg.origin() + off + samplePoints[0]); sampleOffset++; } dint count = blk * size; for(dint b = 0; b < size; ++b) { dint i = (b + count) * size; for(dint a = 0; a < size; ++a, ++sampleOffset) { dint idx = a + i + (center == 0? blk + 1 : 0); if(numSamples > 1 && ((x > 0 && a == 0) || (y > 0 && b == 0))) { // We have already sampled this point. // Get the previous result. LightGrid::Ref prev(x, y); LightGrid::Ref prevB(a, b); dint prevIdx; if(x > 0 && a == 0) { prevB.x = size -1; prev.x--; } if(y > 0 && b == 0) { prevB.y = size -1; prev.y--; } prevIdx = prevB.x + (prevB.y + lg.toIndex(prev) * size) * size; if(center == 0) prevIdx += lg.toIndex(prev) + 1; ssamples[idx] = ssamples[prevIdx]; } else { // We haven't sampled this point yet. ssamples[idx] = clusterAt(lg.origin() + off + samplePoints[sampleOffset]); } } } } // Allocate memory used for the collection of the sample results. QVector blkSampleClusters(numSamples); for(dint y = 0; y < lg.dimensions().y; ++y) for(dint x = 0; x < lg.dimensions().x; ++x) { /// Pick the sector cluster at each of the sample points. /// /// @todo We don't actually need the blkSampleClusters array anymore. /// Now that ssamples stores the results consecutively a simple index /// into ssamples would suffice. However if the optimization to save /// memory is implemented as described in the comments above we WOULD /// still require it. /// /// For now we'll make use of it to clarify the code. dint const sampleOffset = lg.toIndex(x, y) * numSamples; for(dint i = 0; i < numSamples; ++i) { blkSampleClusters[i] = ssamples[i + sampleOffset]; } SectorCluster *cluster = nullptr; if(numSamples == 1) { cluster = blkSampleClusters[center]; } else { // Pick the sector which had the most hits. dint best = -1; sampleHits.fill(0); for(dint i = 0; i < numSamples; ++i) { if(!blkSampleClusters[i]) continue; for(dint k = 0; k < numSamples; ++k) { if(blkSampleClusters[k] == blkSampleClusters[i] && blkSampleClusters[k]) { sampleHits[k]++; if(sampleHits[k] > best) { best = i; } } } } if(best != -1) { // Favour the center sample if its a draw. if(sampleHits[best] == sampleHits[center] && blkSampleClusters[center]) { cluster = blkSampleClusters[center]; } else { cluster = blkSampleClusters[best]; } } } if(cluster) { lg.setPrimarySource(lg.toIndex(x, y), cluster); } } LOGDEV_GL_MSG("%i light blocks (%u bytes)") << lg.numBlocks() << lg.blockStorageSize(); // How much time did we spend? LOGDEV_GL_MSG("LightGrid init completed in %.2f seconds") << begunAt.since(); } void Map::initBias() { Time begunAt; LOG_AS("Map::initBias"); // Start with no sources whatsoever. d->bias.sources.clear(); if(d->def) { String const oldUniqueId = d->def->composeUniqueId(App_CurrentGame()); // Load light sources from Light definitions. for(dint i = 0; i < defs.lights.size(); ++i) { ded_light_t *lightDef = &defs.lights[i]; if(lightDef->state[0]) continue; if(oldUniqueId.compareWithoutCase(lightDef->uniqueMapID)) continue; // Already at maximum capacity? if(biasSourceCount() == MAX_BIAS_SOURCES) break; addBiasSource(BiasSource::fromDef(*lightDef)); } } LOGDEV_MAP_VERBOSE("Completed in %.2f seconds") << begunAt.since(); } void Map::unlinkInMaterialLists(Surface *surface) { if(!surface) return; if(!d->decorator) return; d->surfaceDecorator().remove(surface); } void Map::linkInMaterialLists(Surface *surface) { if(!surface) return; // Only surfaces with a material will be linked. if(!surface->hasMaterial()) return; // Ignore surfaces not currently attributed to the map. if(&surface->map() != this) { qDebug() << "Ignoring alien surface" << de::dintptr(surface) << "in Map::unlinkInMaterialLists"; return; } d->surfaceDecorator().add(surface); } void Map::buildMaterialLists() { d->surfaceDecorator().reset(); for(ConvexSubspace const *subspace : d->subspaces) { HEdge *base = subspace->poly().hedge(); HEdge *hedge = base; do { if(hedge->hasMapElement()) { LineSide &side = hedge->mapElementAs().lineSide(); if(side.hasSections()) { linkInMaterialLists(&side.middle()); linkInMaterialLists(&side.top()); linkInMaterialLists(&side.bottom()); } if(side.back().hasSections()) { linkInMaterialLists(&side.back().middle()); linkInMaterialLists(&side.back().top()); linkInMaterialLists(&side.back().bottom()); } } } while((hedge = &hedge->next()) != base); subspace->forAllExtraMeshes([this] (Mesh &mesh) { for(HEdge *hedge : mesh.hedges()) { // Is this on the back of a one-sided line? if(!hedge->hasMapElement()) continue; LineSide &side = hedge->mapElementAs().lineSide(); if(side.hasSections()) { linkInMaterialLists(&side.middle()); linkInMaterialLists(&side.top()); linkInMaterialLists(&side.bottom()); } if(side.back().hasSections()) { linkInMaterialLists(&side.back().middle()); linkInMaterialLists(&side.back().top()); linkInMaterialLists(&side.back().bottom()); } } return LoopContinue; }); subspace->sector().forAllPlanes([this] (Plane &plane) { linkInMaterialLists(&plane.surface()); return LoopContinue; }); } } void Map::initContactBlockmaps() { d->initContactBlockmaps(); } void Map::spreadAllContacts(AABoxd const ®ion) { // Expand the region according by the maxium radius of each contact type. d->mobjContactBlockmap-> spread(AABoxd(region.minX - DDMOBJ_RADIUS_MAX, region.minY - DDMOBJ_RADIUS_MAX, region.maxX + DDMOBJ_RADIUS_MAX, region.maxY + DDMOBJ_RADIUS_MAX)); d->lumobjContactBlockmap-> spread(AABoxd(region.minX - Lumobj::radiusMax(), region.minY - Lumobj::radiusMax(), region.maxX + Lumobj::radiusMax(), region.maxY + Lumobj::radiusMax())); } void Map::initGenerators() { LOG_AS("Map::initGenerators"); Time begunAt; d->spawnTypeParticleGens(); d->spawnMapParticleGens(); LOGDEV_MAP_VERBOSE("Completed in %.2f seconds") << begunAt.since(); } void Map::spawnPlaneParticleGens() { //if(!useParticles) return; for(Sector *sector : d->sectors) { Plane &floor = sector->floor(); floor.spawnParticleGen(Def_GetGenerator(floor.surface().composeMaterialUri())); Plane &ceiling = sector->ceiling(); ceiling.spawnParticleGen(Def_GetGenerator(ceiling.surface().composeMaterialUri())); } } void Map::clearClMobjs() { d->clMobjHash.clear(); } mobj_t *Map::clMobjFor(thid_t id, bool canCreate) const { LOG_AS("Map::clMobjFor"); ClMobjHash::const_iterator found = d->clMobjHash.constFind(id); if(found != d->clMobjHash.constEnd()) { return found.value(); } if(!canCreate) return nullptr; // Create a new client mobj. This is a regular mobj that has network state // associated with it. MobjThinker mo(Thinker::AllocateMemoryZone); mo.id = id; mo.function = reinterpret_cast(gx.MobjThinker); auto *data = new ClientMobjThinkerData; data->remoteSync().flags = DDMF_REMOTE; mo.setData(data); d->clMobjHash.insert(id, mo); data->audienceForDeletion() += d; // for removing from the hash d->thinkers->setMobjId(id); // Mark this ID as used. // Client mobjs are full-fludged game mobjs as well. d->thinkers->add(*(thinker_t *)mo); return mo.take(); } dint Map::clMobjIterator(dint (*callback)(mobj_t *, void *), void *context) { ClMobjHash::const_iterator next; for(ClMobjHash::const_iterator i = d->clMobjHash.constBegin(); i != d->clMobjHash.constEnd(); i = next) { next = i; next++; DENG2_ASSERT(THINKER_DATA(i.value()->thinker, ClientMobjThinkerData).hasRemoteSync()); // Callback returns zero to continue. if(dint result = callback(i.value(), context)) return result; } return 0; } Map::ClMobjHash const &Map::clMobjHash() const { return d->clMobjHash; } #endif // __CLIENT__ AABoxd const &Map::bounds() const { return d->bounds; } coord_t Map::gravity() const { return _effectiveGravity; } void Map::setGravity(coord_t newGravity) { if(!de::fequal(_effectiveGravity, newGravity)) { _effectiveGravity = newGravity; LOG_MAP_VERBOSE("Effective gravity for %s now %.1f") << (d->def? d->def->gets("id") : "(unknown map)") << _effectiveGravity; } } Thinkers &Map::thinkers() const { if(bool(d->thinkers)) { return *d->thinkers; } /// @throw MissingThinkersError The thinker lists are not yet initialized. throw MissingThinkersError("Map::thinkers", "Thinkers not initialized"); } Sky &Map::sky() const { return d->sky; } dint Map::vertexCount() const { return d->mesh.vertexCount(); } Vertex &Map::vertex(dint index) const { if(Vertex *vtx = vertexPtr(index)) return *vtx; /// @throw MissingElementError Invalid Vertex reference specified. throw MissingElementError("Map::vertex", "Unknown Vertex index:" + String::number(index)); } Vertex *Map::vertexPtr(dint index) const { if(index >= 0 && index < d->mesh.vertexCount()) { return d->mesh.vertexs().at(index); } return nullptr; } LoopResult Map::forAllVertexs(std::function func) const { for(Vertex *vtx : d->mesh.vertexs()) { if(auto result = func(*vtx)) return result; } return LoopContinue; } dint Map::lineCount() const { return d->lines.count(); } Line &Map::line(dint index) const { if(Line *li = linePtr(index)) return *li; /// @throw MissingElementError Invalid Line reference specified. throw MissingElementError("Map::line", "Unknown Line index:" + String::number(index)); } Line *Map::linePtr(dint index) const { if(index >= 0 && index < d->lines.count()) { return d->lines.at(index); } return nullptr; } LoopResult Map::forAllLines(std::function func) const { for(Line *li : d->lines) { if(auto result = func(*li)) return result; } return LoopContinue; } dint Map::sectorCount() const { return d->sectors.count(); } Sector &Map::sector(dint index) const { if(Sector *sec = sectorPtr(index)) return *sec; /// @throw MissingElementError Invalid Sector reference specified. throw MissingElementError("Map::sector", "Unknown Sector index:" + String::number(index)); } Sector *Map::sectorPtr(dint index) const { if(index >= 0 && index < d->sectors.count()) { return d->sectors.at(index); } return nullptr; } LoopResult Map::forAllSectors(std::function func) const { for(Sector *sec : d->sectors) { if(auto result = func(*sec)) return result; } return LoopContinue; } dint Map::subspaceCount() const { return d->subspaces.count(); } ConvexSubspace &Map::subspace(dint index) const { if(ConvexSubspace *sub = subspacePtr(index)) return *sub; /// @throw MissingElementError Invalid ConvexSubspace reference specified. throw MissingElementError("Map::subspace", "Unknown subspace index:" + String::number(index)); } ConvexSubspace *Map::subspacePtr(dint index) const { if(index >= 0 && index < d->subspaces.count()) { return d->subspaces.at(index); } return nullptr; } LoopResult Map::forAllSubspaces(std::function func) const { for(ConvexSubspace *sub : d->subspaces) { if(auto result = func(*sub)) return result; } return LoopContinue; } dint Map::clusterCount() const { return d->clusters.count(); } LoopResult Map::forAllClusters(Sector *sector, std::function func) { if(sector) { for(auto it = d->clusters.constFind(sector); it != d->clusters.end() && it.key() == sector; ++it) { if(auto result = func(**it)) return result; } return LoopContinue; } for(SectorCluster *cluster : d->clusters) { if(auto result = func(*cluster)) return result; } return LoopContinue; } dint Map::polyobjCount() const { return d->polyobjs.count(); } Polyobj &Map::polyobj(dint index) const { if(Polyobj *pob = polyobjPtr(index)) return *pob; /// @throw MissingObjectError Invalid ConvexSubspace reference specified. throw MissingObjectError("Map::subspace", "Unknown Polyobj index:" + String::number(index)); } Polyobj *Map::polyobjPtr(dint index) const { if(index >= 0 && index < d->polyobjs.count()) { return d->polyobjs.at(index); } return nullptr; } LoopResult Map::forAllPolyobjs(std::function func) const { for(Polyobj *pob : d->polyobjs) { if(auto result = func(*pob)) return result; } return LoopContinue; } void Map::initPolyobjs() { LOG_AS("Map::initPolyobjs"); for(Polyobj *po : d->polyobjs) { /// @todo Is this still necessary? -ds /// (This data is updated automatically when moving/rotating). po->updateAABox(); po->updateSurfaceTangents(); po->unlink(); po->link(); } } dint Map::ambientLightLevel() const { return _ambientLightLevel; } LineSide &Map::side(dint index) const { if(LineSide *side = sidePtr(index)) return *side; /// @throw MissingElementError Invalid LineSide reference specified. throw MissingElementError("Map::side", "Unknown LineSide index:" + String::number(index)); } LineSide *Map::sidePtr(dint index) const { if(index < 0) return nullptr; return &d->lines.at(index / 2)->side(index % 2); } dint Map::toSideIndex(dint lineIndex, dint backSide) // static { DENG_ASSERT(lineIndex >= 0); return lineIndex * 2 + (backSide? 1 : 0); } bool Map::identifySoundEmitter(SoundEmitter const &emitter, Sector **sector, Polyobj **poly, Plane **plane, Surface **surface) const { *sector = nullptr; *poly = nullptr; *plane = nullptr; *surface = nullptr; /// @todo Optimize: All sound emitters in a sector are linked together forming /// a chain. Make use of the chains instead. *poly = d->polyobjBySoundEmitter(emitter); if(!*poly) { // Not a polyobj. Try the sectors next. *sector = d->sectorBySoundEmitter(emitter); if(!*sector) { // Not a sector. Try the planes next. *plane = d->planeBySoundEmitter(emitter); if(!*plane) { // Not a plane. Try the surfaces next. *surface = d->surfaceBySoundEmitter(emitter); } } } return (*sector != 0 || *poly != 0|| *plane != 0|| *surface != 0); } EntityDatabase &Map::entityDatabase() const { return d->entityDatabase; } void Map::initNodePiles() { LOG_AS("Map"); Time begunAt; // Initialize node piles and line rings. NP_Init(&d->mobjNodes, 256); // Allocate a small pile. NP_Init(&d->lineNodes, lineCount() + 1000); // Allocate the rings. DENG_ASSERT(d->lineLinks == nullptr); d->lineLinks = (nodeindex_t *) Z_Malloc(sizeof(*d->lineLinks) * lineCount(), PU_MAPSTATIC, 0); for(dint i = 0; i < lineCount(); ++i) { d->lineLinks[i] = NP_New(&d->lineNodes, NP_ROOT_NODE); } // How much time did we spend? LOGDEV_MAP_MSG("Initialized node piles in %.2f seconds") << begunAt.since(); } Blockmap const &Map::mobjBlockmap() const { if(bool(d->mobjBlockmap)) { return *d->mobjBlockmap; } /// @throw MissingBlockmapError The mobj blockmap is not yet initialized. throw MissingBlockmapError("Map::mobjBlockmap", "Mobj blockmap is not initialized"); } Blockmap const &Map::polyobjBlockmap() const { if(bool(d->polyobjBlockmap)) { return *d->polyobjBlockmap; } /// @throw MissingBlockmapError The polyobj blockmap is not yet initialized. throw MissingBlockmapError("Map::polyobjBlockmap", "Polyobj blockmap is not initialized"); } LineBlockmap const &Map::lineBlockmap() const { if(bool(d->lineBlockmap)) { return *d->lineBlockmap; } /// @throw MissingBlockmapError The line blockmap is not yet initialized. throw MissingBlockmapError("Map::lineBlockmap", "Line blockmap is not initialized"); } Blockmap const &Map::subspaceBlockmap() const { if(bool(d->subspaceBlockmap)) { return *d->subspaceBlockmap; } /// @throw MissingBlockmapError The subspace blockmap is not yet initialized. throw MissingBlockmapError("Map::subspaceBlockmap", "Convex subspace blockmap is not initialized"); } LoopResult Map::forAllLinesTouchingMobj(mobj_t &mob, std::function func) const { /// @todo Optimize: It should not be necessary to collate the objects first in /// in order to perform the iteration. This kind of "belt and braces" safety /// measure would not be necessary at this level if the caller(s) instead took /// responsibility for managing relationship changes during the iteration. -ds if(&Mobj_Map(mob) == this && Mobj_IsLinked(mob) && mob.lineRoot) { QVarLengthArray linkStore; linknode_t *tn = d->mobjNodes.nodes; for(nodeindex_t nix = tn[mob.lineRoot].next; nix != mob.lineRoot; nix = tn[nix].next) { linkStore.append((Line *)(tn[nix].ptr)); } for(dint i = 0; i < linkStore.count(); ++i) { if(auto result = func(*linkStore[i])) return result; } } return LoopContinue; } LoopResult Map::forAllSectorsTouchingMobj(mobj_t &mob, std::function func) const { /// @todo Optimize: It should not be necessary to collate the objects first in /// in order to perform the iteration. This kind of "belt and braces" safety /// measure would not be necessary at this level if the caller(s) instead took /// responsibility for managing relationship changes during the iteration. -ds if(&Mobj_Map(mob) == this && Mobj_IsLinked(mob)) { QVarLengthArray linkStore; // Always process the mobj's own sector first. Sector &ownSec = *Mobj_BspLeafAtOrigin(mob).sectorPtr(); ownSec.setValidCount(validCount); linkStore.append(&ownSec); // Any good lines around here? if(mob.lineRoot) { linknode_t *tn = d->mobjNodes.nodes; for(nodeindex_t nix = tn[mob.lineRoot].next; nix != mob.lineRoot; nix = tn[nix].next) { auto *ld = (Line *)(tn[nix].ptr); // All these lines have sectors on both sides. // First, try the front. Sector &frontSec = ld->frontSector(); if(frontSec.validCount() != validCount) { frontSec.setValidCount(validCount); linkStore.append(&frontSec); } // And then the back. /// @todo Above comment suggest always twosided, which is it? -ds if(ld->hasBackSector()) { Sector &backSec = ld->backSector(); if(backSec.validCount() != validCount) { backSec.setValidCount(validCount); linkStore.append(&backSec); } } } } for(dint i = 0; i < linkStore.count(); ++i) { if(auto result = func(*linkStore[i])) return result; } } return LoopContinue; } LoopResult Map::forAllMobjsTouchingLine(Line &line, std::function func) const { /// @todo Optimize: It should not be necessary to collate the objects first in /// in order to perform the iteration. This kind of "belt and braces" safety /// measure would not be necessary at this level if the caller(s) instead took /// responsibility for managing relationship changes during the iteration. -ds if(&line.map() == this) { QVarLengthArray linkStore; // Collate mobjs touching the given line in case these relationships change. linknode_t *ln = d->lineNodes.nodes; nodeindex_t root = d->lineLinks[line.indexInMap()]; for(nodeindex_t nix = ln[root].next; nix != root; nix = ln[nix].next) { linkStore.append((mobj_t *)(ln[nix].ptr)); } for(dint i = 0; i < linkStore.count(); ++i) { if(auto result = func(*linkStore[i])) return result; } } return LoopContinue; } LoopResult Map::forAllMobjsTouchingSector(Sector §or, std::function func) const { /// @todo Optimize: It should not be necessary to collate the objects first in /// in order to perform the iteration. This kind of "belt and braces" safety /// measure would not be necessary at this level if the caller(s) instead took /// responsibility for managing relationship changes during the iteration. -ds if(§or.map() == this) { QVarLengthArray linkStore; // Collate mobjs that obviously are in the sector. for(mobj_t *mob = sector.firstMobj(); mob; mob = mob->sNext) { if(mob->validCount != validCount) { mob->validCount = validCount; linkStore.append(mob); } } // Collate mobjs linked to the sector's lines. linknode_t const *ln = d->lineNodes.nodes; sector.forAllSides([this, &linkStore, &ln] (LineSide &side) { nodeindex_t root = d->lineLinks[side.line().indexInMap()]; for(nodeindex_t nix = ln[root].next; nix != root; nix = ln[nix].next) { auto *mob = (mobj_t *)(ln[nix].ptr); if(mob->validCount != validCount) { mob->validCount = validCount; linkStore.append(mob); } } return LoopContinue; }); // Process all collected mobjs. for(dint i = 0; i < linkStore.count(); ++i) { if(auto result = func(*linkStore[i])) return result; } } return LoopContinue; } dint Map::unlink(mobj_t &mob) { dint links = 0; if(d->unlinkMobjFromSectors(mob)) links |= MLF_SECTOR; BlockmapCell cell = d->mobjBlockmap->toCell(Mobj_Origin(mob)); if(d->mobjBlockmap->unlink(cell, &mob)) links |= MLF_BLOCKMAP; if(!d->unlinkMobjFromLines(mob)) links |= MLF_NOLINE; return links; } void Map::link(mobj_t &mob, dint flags) { BspLeaf &bspLeafAtOrigin = bspLeafAt_FixedPrecision(Mobj_Origin(mob)); // Link into the sector? if(flags & MLF_SECTOR) { d->unlinkMobjFromSectors(mob); bspLeafAtOrigin.sectorPtr()->link(&mob); } mob._bspLeaf = &bspLeafAtOrigin; // Link into blockmap? if(flags & MLF_BLOCKMAP) { BlockmapCell cell = d->mobjBlockmap->toCell(Mobj_Origin(mob)); d->mobjBlockmap->link(cell, &mob); } // Link into lines? if(!(flags & MLF_NOLINE)) { d->unlinkMobjFromLines(mob); d->linkMobjToLines(mob); } // If this is a player - perform additional tests to see if they have // entered or exited the void. if(mob.dPlayer && mob.dPlayer->mo) { mob.dPlayer->inVoid = true; if(SectorCluster *cluster = Mobj_ClusterPtr(mob)) { if(Mobj_BspLeafAtOrigin(mob).subspace().contains(Mobj_Origin(mob))) { #ifdef __CLIENT__ if(mob.origin[2] < cluster->visCeiling().heightSmoothed() + 4 && mob.origin[2] >= cluster-> visFloor().heightSmoothed()) #else if(mob.origin[2] < cluster->ceiling().height() + 4 && mob.origin[2] >= cluster-> floor().height()) #endif { mob.dPlayer->inVoid = false; } } } } } void Map::unlink(Polyobj &polyobj) { d->polyobjBlockmap->unlink(polyobj.aaBox, &polyobj); } void Map::link(Polyobj &polyobj) { d->polyobjBlockmap->link(polyobj.aaBox, &polyobj); } LoopResult Map::forAllLinesInBox(AABoxd const &box, dint flags, std::function func) const { LoopResult result = LoopContinue; // Process polyobj lines? if((flags & LIF_POLYOBJ) && polyobjCount()) { dint const localValidCount = validCount; result = polyobjBlockmap().forAllInBox(box, [&func, &localValidCount] (void *object) { auto &pob = *(Polyobj *)object; if(pob.validCount != localValidCount) // not yet processed { pob.validCount = localValidCount; for(Line *line : pob.lines()) { if(line->validCount() != localValidCount) // not yet processed { line->setValidCount(localValidCount); if(auto result = func(*line)) return result; } } } return LoopResult(); // continue }); } // Process sector lines? if(!result && (flags & LIF_SECTOR)) { dint const localValidCount = validCount; result = lineBlockmap().forAllInBox(box, [&func, &localValidCount] (void *object) { auto &line = *(Line *)object; if(line.validCount() != localValidCount) // not yet processed { line.setValidCount(localValidCount); return func(line); } return LoopResult(); // continue }); } return result; } BspLeaf &Map::bspLeafAt(Vector2d const &point) const { if(!d->bsp.tree) /// @throw MissingBspTreeError No BSP data is available. throw MissingBspTreeError("Map::bspLeafAt", "No BSP data available"); BspTree const *bspTree = d->bsp.tree; while(!bspTree->isLeaf()) { auto &bspNode = bspTree->userData()->as(); dint side = bspNode.partition().pointOnSide(point) < 0; // Descend to the child subspace on "this" side. bspTree = bspTree->childPtr(BspTree::ChildId(side)); } // We've arrived at a leaf. return bspTree->userData()->as(); } BspLeaf &Map::bspLeafAt_FixedPrecision(Vector2d const &point) const { if(!d->bsp.tree) /// @throw MissingBspTreeError No BSP data is available. throw MissingBspTreeError("Map::bspLeafAt_FixedPrecision", "No BSP data available"); fixed_t pointX[2] = { DBL2FIX(point.x), DBL2FIX(point.y) }; BspTree const *bspTree = d->bsp.tree; while(!bspTree->isLeaf()) { auto const &bspNode = bspTree->userData()->as(); Partition const &partition = bspNode.partition(); fixed_t lineOriginX[2] = { DBL2FIX(partition.origin.x), DBL2FIX(partition.origin.y) }; fixed_t lineDirectionX[2] = { DBL2FIX(partition.direction.x), DBL2FIX(partition.direction.y) }; dint side = V2x_PointOnLineSide(pointX, lineOriginX, lineDirectionX); // Decend to the child subspace on "this" side. bspTree = bspTree->childPtr(BspTree::ChildId(side)); } // We've arrived at a leaf. return bspTree->userData()->as(); } SectorCluster *Map::clusterAt(Vector2d const &point) const { BspLeaf &bspLeaf = bspLeafAt(point); if(bspLeaf.hasSubspace() && bspLeaf.subspace().contains(point)) { return bspLeaf.subspace().clusterPtr(); } return nullptr; } #ifdef __CLIENT__ void Map::updateScrollingSurfaces() { for(Surface *surface : d->scrollingSurfaces) { surface->updateMaterialOriginTracking(); } } Map::SurfaceSet &Map::scrollingSurfaces() { return d->scrollingSurfaces; } void Map::updateTrackedPlanes() { for(Plane *plane : d->trackedPlanes) { plane->updateHeightTracking(); } } Map::PlaneSet &Map::trackedPlanes() { return d->trackedPlanes; } void Map::initSkyFix() { Time begunAt; LOG_AS("Map::initSkyFix"); d->skyFloorHeight = DDMAXFLOAT; d->skyCeilingHeight = DDMINFLOAT; // Update for sector plane heights and mobjs which intersect the ceiling. /// @todo Can't we defer this? for(Sector *sector : d->sectors) { if(!sector->sideCount()) continue; bool const skyFloor = sector->floorSurface().hasSkyMaskedMaterial(); bool const skyCeil = sector->ceilingSurface().hasSkyMaskedMaterial(); if(!skyFloor && !skyCeil) continue; if(skyCeil) { // Adjust for the plane height. if(sector->ceiling().heightSmoothed() > d->skyCeilingHeight) { // Must raise the skyfix ceiling. d->skyCeilingHeight = sector->ceiling().heightSmoothed(); } // Check that all the mobjs in the sector fit in. for(mobj_t *mob = sector->firstMobj(); mob; mob = mob->sNext) { coord_t extent = mob->origin[2] + mob->height; if(extent > d->skyCeilingHeight) { // Must raise the skyfix ceiling. d->skyCeilingHeight = extent; } } } if(skyFloor) { // Adjust for the plane height. if(sector->floor().heightSmoothed() < d->skyFloorHeight) { // Must lower the skyfix floor. d->skyFloorHeight = sector->floor().heightSmoothed(); } } // Update for middle materials on lines which intersect the // floor and/or ceiling on the front (i.e., sector) side. sector->forAllSides([this, &skyCeil, &skyFloor] (LineSide &side) { if(!side.hasSections()) return LoopContinue; if(!side.middle().hasMaterial()) return LoopContinue; // There must be a sector on both sides. if(!side.hasSector() || !side.back().hasSector()) return LoopContinue; // Possibility of degenerate BSP leaf. if(!side.leftHEdge()) return LoopContinue; WallEdge edge(WallSpec::fromMapSide(side, LineSide::Middle), *side.leftHEdge(), Line::From); if(edge.isValid() && edge.top().z() > edge.bottom().z()) { if(skyCeil && edge.top().z() + edge.materialOrigin().y > d->skyCeilingHeight) { // Must raise the skyfix ceiling. d->skyCeilingHeight = edge.top().z() + edge.materialOrigin().y; } if(skyFloor && edge.bottom().z() + edge.materialOrigin().y < d->skyFloorHeight) { // Must lower the skyfix floor. d->skyFloorHeight = edge.bottom().z() + edge.materialOrigin().y; } } return LoopContinue; }); } LOGDEV_MAP_VERBOSE("Completed in %.2f seconds") << begunAt.since(); } coord_t Map::skyFix(bool ceiling) const { return ceiling? d->skyCeilingHeight : d->skyFloorHeight; } void Map::setSkyFix(bool ceiling, coord_t newHeight) { if(ceiling) d->skyCeilingHeight = newHeight; else d->skyFloorHeight = newHeight; } Generator *Map::newGenerator() { Generator::Id id = d->findIdForNewGenerator(); // 1-based if(!id) return nullptr; // Failed; too many generators? Instance::Generators &gens = d->getGenerators(); // If there is already a generator with that id - remove it. if(id > 0 && (unsigned)id <= gens.activeGens.size()) { Generator_Delete(gens.activeGens[id - 1]); } /// @todo Linear allocation when in-game is not good... auto *gen = (Generator *) Z_Calloc(sizeof(Generator), PU_MAP, 0); gen->setId(id); // Link the thinker to the list of (private) thinkers. gen->thinker.function = (thinkfunc_t) Generator_Thinker; d->thinkers->add(gen->thinker, false /*not public*/); // Link the generator into the collection. gens.activeGens[id - 1] = gen; return gen; } dint Map::generatorCount() const { if(!d->generators) return 0; dint count = 0; for(Generator *gen : d->getGenerators().activeGens) { if(gen) count += 1; } return count; } void Map::unlink(Generator &generator) { Instance::Generators &gens = d->getGenerators(); for(duint i = 0; i < gens.activeGens.size(); ++i) { if(gens.activeGens[i] == &generator) { gens.activeGens[i] = nullptr; break; } } } LoopResult Map::forAllGenerators(std::function func) const { for(Generator *gen : d->getGenerators().activeGens) { if(!gen) continue; if(auto result = func(*gen)) return result; } return LoopContinue; } LoopResult Map::forAllGeneratorsInSector(Sector const §or, std::function func) const { if(sector.mapPtr() == this) // Ignore 'alien' sectors. { duint const listIndex = sector.indexInMap(); Instance::Generators &gens = d->getGenerators(); for(Instance::Generators::ListNode *it = gens.lists[listIndex]; it; it = it->next) { if(auto result = func(*it->gen)) return result; } } return LoopContinue; } dint Map::lumobjCount() const { return d->lumobjs.count(); } Lumobj &Map::addLumobj(Lumobj const &lumobj) { d->lumobjs.append(new Lumobj(lumobj)); Lumobj &lum = *d->lumobjs.last(); lum.setMap(this); lum.setIndexInMap(d->lumobjs.count() - 1); DENG2_ASSERT(lum.bspLeafAtOrigin().hasSubspace()); lum.bspLeafAtOrigin().subspace().link(lum); R_AddContact(lum); // For spreading purposes. return lum; } void Map::removeLumobj(dint which) { if(which >= 0 && which < lumobjCount()) { delete d->lumobjs.takeAt(which); } } void Map::removeAllLumobjs() { for(ConvexSubspace *subspace : d->subspaces) { subspace->unlinkAllLumobjs(); } qDeleteAll(d->lumobjs); d->lumobjs.clear(); } Lumobj &Map::lumobj(dint index) const { if(Lumobj *lum = lumobjPtr(index)) return *lum; /// @throw MissingObjectError Invalid Lumobj reference specified. throw MissingObjectError("Map::lumobj", "Unknown Lumobj index:" + String::number(index)); } Lumobj *Map::lumobjPtr(dint index) const { if(index >= 0 && index < d->lumobjs.count()) { return d->lumobjs.at(index); } return nullptr; } LoopResult Map::forAllLumobjs(std::function func) const { for(Lumobj *lob : d->lumobjs) { if(auto result = func(*lob)) return result; } return LoopContinue; } dint Map::biasSourceCount() const { return d->bias.sources.count(); } BiasSource &Map::addBiasSource(BiasSource const &biasSource) { if(biasSourceCount() < MAX_BIAS_SOURCES) { d->bias.sources.append(new BiasSource(biasSource)); return *d->bias.sources.last(); } /// @throw FullError Attempt to add a new bias source when already at capcity. throw FullError("Map::addBiasSource", "Already at full capacity:" + String::number(MAX_BIAS_SOURCES)); } void Map::removeBiasSource(dint which) { if(which >= 0 && which < biasSourceCount()) { delete d->bias.sources.takeAt(which); } } void Map::removeAllBiasSources() { qDeleteAll(d->bias.sources); d->bias.sources.clear(); } BiasSource &Map::biasSource(dint index) const { if(BiasSource *bsrc = biasSourcePtr(index)) return *bsrc; /// @throw MissingObjectError Invalid BiasSource reference specified. throw MissingObjectError("Map::biasSource", "Unknown BiasSource index:" + String::number(index)); } BiasSource *Map::biasSourcePtr(dint index) const { if(index >= 0 && index < d->bias.sources.count()) { return d->bias.sources.at(index); } return nullptr; } /** * @todo Implement a blockmap for these? * @todo Cache this result (MRU?). */ BiasSource *Map::biasSourceNear(Vector3d const &point) const { BiasSource *nearest = nullptr; coord_t minDist = 0; for(BiasSource *src : d->bias.sources) { coord_t dist = (src->origin() - point).length(); if(!nearest || dist < minDist) { minDist = dist; nearest = src; } } return nearest; } LoopResult Map::forAllBiasSources(std::function func) const { for(BiasSource *bsrc : d->bias.sources) { if(auto result = func(*bsrc)) return result; } return LoopContinue; } dint Map::indexOf(BiasSource const &bsrc) const { return d->bias.sources.indexOf(const_cast(&bsrc)); } duint Map::biasCurrentTime() const { return d->bias.currentTime; } duint Map::biasLastChangeOnFrame() const { return d->bias.lastChangeOnFrame; } #endif // __CLIENT__ void Map::update() { #ifdef __CLIENT__ d->updateParticleGens(); // Defs might've changed. // Update all surfaces. for(Sector *sector : d->sectors) { sector->forAllPlanes([] (Plane &plane) { plane.surface().markForDecorationUpdate(); return LoopContinue; }); } for(Line *line : d->lines) for(dint i = 0; i < 2; ++i) { LineSide &side = line->side(i); if(!side.hasSections()) continue; side.top ().markForDecorationUpdate(); side.middle().markForDecorationUpdate(); side.bottom().markForDecorationUpdate(); } /// @todo Is this even necessary? for(Polyobj *polyobj : d->polyobjs) for(Line *line : polyobj->lines()) { line->front().middle().markForDecorationUpdate(); } // Rebuild the surface material lists. buildMaterialLists(); #endif // __CLIENT__ // Reapply values defined in MapInfo (they may have changed). Record const &inf = mapInfo(); _ambientLightLevel = inf.getf("ambient") * 255; _globalGravity = inf.getf("gravity"); _effectiveGravity = _globalGravity; #ifdef __CLIENT__ // Reconfigure the sky. /// @todo Sky needs breaking up into multiple components. There should be /// a representation on server side and a logical entity which the renderer /// visualizes. We also need multiple concurrent skies for BOOM support. defn::Sky skyDef; if(Record const *def = defs.skies.tryFind("id", inf.gets("skyId"))) { skyDef = *def; } else { skyDef = inf.subrecord("sky"); } sky().configure(&skyDef); #endif } #ifdef __CLIENT__ void Map::worldSystemFrameBegins(bool resetNextViewer) { DENG2_ASSERT(&worldSys().map() == this); // Sanity check. // Interpolate the map ready for drawing view(s) of it. d->lerpTrackedPlanes(resetNextViewer); d->lerpScrollingSurfaces(resetNextViewer); if(!freezeRLs) { // Initialize and/or update the LightGrid. initLightGrid(); d->biasBeginFrame(); removeAllLumobjs(); d->removeAllContacts(); // Generate surface decorations for the frame. if(useLightDecorations) { // Perform scheduled redecoration. d->surfaceDecorator().redecorate(); // Generate lumobjs for all decorations who want them. for(Line *line : d->lines) for(dint i = 0; i < 2; ++i) { LineSide &side = line->side(i); if(!side.hasSections()) continue; d->generateLumobjs(side.middle()); d->generateLumobjs(side.bottom()); d->generateLumobjs(side.top()); } for(Sector *sector : d->sectors) { sector->forAllPlanes([this] (Plane &plane) { d->generateLumobjs(plane.surface()); return LoopContinue; }); } } // Spawn omnilights for mobjs? if(useDynLights) { for(Sector *sector : d->sectors) for(mobj_t *iter = sector->firstMobj(); iter; iter = iter->sNext) { Mobj_GenerateLumobjs(iter); } } d->generateMobjContacts(); d->linkAllParticles(); d->linkAllContacts(); } } /// @return @c false= Continue iteration. static dint expireClMobjsWorker(mobj_t *mo, void *context) { duint const nowTime = *static_cast(context); // Already deleted? if(mo->thinker.function == (thinkfunc_t)-1) return 0; // Don't expire player mobjs. if(mo->dPlayer) return 0; ClientMobjThinkerData::RemoteSync *info = ClMobj_GetInfo(mo); DENG2_ASSERT(info); if((info->flags & (CLMF_UNPREDICTABLE | CLMF_HIDDEN | CLMF_NULLED)) || !mo->info) { // Has this mobj timed out? if(nowTime - info->time > CLMOBJ_TIMEOUT) { LOGDEV_MAP_MSG("Mobj %i has expired (%i << %i), in state %s [%c%c%c]") << mo->thinker.id << info->time << nowTime << Def_GetStateName(mo->state) << (info->flags & CLMF_UNPREDICTABLE? 'U' : '_') << (info->flags & CLMF_HIDDEN? 'H' : '_') << (info->flags & CLMF_NULLED? '0' : '_'); // Too long. The server will probably never send anything // for this mobj, so get rid of it. (Both unpredictable // and hidden mobjs are not visible or bl/seclinked.) Mobj_Destroy(mo); } } return 0; } void Map::expireClMobjs() { duint nowTime = Timer_RealMilliseconds(); clMobjIterator(expireClMobjsWorker, &nowTime); } #endif // __CLIENT__ String Map::elementSummaryAsStyledText() const { #define TABBED(count, label) String(_E(Ta) " %1 " _E(Tb) "%2\n").arg(count).arg(label) String str; QTextStream os(&str); if(lineCount()) os << TABBED(lineCount(), "Lines"); //if(sideCount()) os << TABBED(sideCount(), "Sides"); if(sectorCount()) os << TABBED(sectorCount(), "Sectors"); if(vertexCount()) os << TABBED(vertexCount(), "Vertexes"); if(polyobjCount()) os << TABBED(polyobjCount(), "Polyobjs"); return str.rightStrip(); #undef TABBED } String Map::objectSummaryAsStyledText() const { #define TABBED(count, label) String(_E(Ta) " %1 " _E(Tb) "%2\n").arg(count).arg(label) dint thCountInStasis = 0; dint thCount = thinkers().count(&thCountInStasis); String str; QTextStream os(&str); if(thCount) os << TABBED(thCount, String("Thinkers (%1 in stasis)").arg(thCountInStasis)); #ifdef __CLIENT__ if(biasSourceCount()) os << TABBED(biasSourceCount(), "Bias Sources"); if(generatorCount()) os << TABBED(generatorCount(), "Generators"); if(lumobjCount()) os << TABBED(lumobjCount(), "Lumobjs"); #endif return str.rightStrip(); #undef TABBED } D_CMD(InspectMap) { DENG2_UNUSED3(src, argc, argv); LOG_AS("inspectmap (Cmd)"); if(!App_WorldSystem().hasMap()) { LOG_SCR_WARNING("No map is currently loaded"); return false; } Map &map = App_WorldSystem().map(); LOG_SCR_NOTE(_E(b) "%s - %s") << Con_GetString("map-name") << Con_GetString("map-author"); LOG_SCR_MSG("\n"); LOG_SCR_MSG( _E(l) "Uri: " _E(.) _E(i) "%s" _E(.) /*" " _E(l) " OldUid: " _E(.) _E(i) "%s" _E(.)*/ _E(l) " Music: " _E(.) _E(i) "%i") << (map.def()? map.def()->composeUri().asText() : "(unknown map)") /*<< map.oldUniqueId()*/ << Con_GetInteger("map-music"); if(map.def() && map.def()->sourceFile()->hasCustom()) { LOG_SCR_MSG(_E(l) "Source: " _E(.) _E(i) "\"%s\"") << NativePath(map.def()->sourceFile()->composePath()).pretty(); } LOG_SCR_MSG("\n"); if(map.isEditable()) { LOG_MSG(_E(D) "Editing " _E(b) "Enabled"); } LOG_SCR_MSG(_E(D) "Elements:"); LOG_SCR_MSG("%s") << map.elementSummaryAsStyledText(); if(map.thinkers().isInited()) { LOG_SCR_MSG(_E(D) "Objects:"); LOG_SCR_MSG("%s") << map.objectSummaryAsStyledText(); } LOG_SCR_MSG(_E(R) "\n"); Vector2d geometryDimensions = Vector2d(map.bounds().max) - Vector2d(map.bounds().min); LOG_SCR_MSG(_E(l) "Geometry dimensions: " _E(.) _E(i)) << geometryDimensions.asText(); if(map.hasBspTree()) { LOG_SCR_MSG(_E(l) "BSP: " _E(.) _E(i)) << map.bspTree().summary(); } if(!map.subspaceBlockmap().isNull()) { LOG_SCR_MSG(_E(l) "Subspace blockmap: " _E(.) _E(i)) << map.subspaceBlockmap().dimensions().asText(); } if(!map.lineBlockmap().isNull()) { LOG_SCR_MSG(_E(l) "Line blockmap: " _E(.) _E(i)) << map.lineBlockmap().dimensions().asText(); } if(!map.mobjBlockmap().isNull()) { LOG_SCR_MSG(_E(l) "Mobj blockmap: " _E(.) _E(i)) << map.mobjBlockmap().dimensions().asText(); } if(!map.polyobjBlockmap().isNull()) { LOG_SCR_MSG(_E(l) "Polyobj blockmap: " _E(.) _E(i)) << map.polyobjBlockmap().dimensions().asText(); } #ifdef __CLIENT__ if(map.hasLightGrid()) { LOG_SCR_MSG(_E(l) "LightGrid: " _E(.) _E(i)) << map.lightGrid().dimensions().asText(); } #endif return true; #undef TABBED } void Map::consoleRegister() // static { Mobj_ConsoleRegister(); C_VAR_INT("bsp-factor", &bspSplitFactor, CVF_NO_MAX, 0, 0); #ifdef __CLIENT__ C_VAR_INT("rend-bias-grid-multisample", &lgMXSample, 0, 0, 7); #endif C_CMD("inspectmap", "", InspectMap); } /// Runtime map editing ----------------------------------------------------- /// Used when sorting vertex line owners. static Vertex *rootVtx; /** * Compares the angles of two lines that share a common vertex. * * pre: rootVtx must point to the vertex common between a and b * which are (lineowner_t*) ptrs. */ static dint lineAngleSorter(void const *a, void const *b) { binangle_t angles[2]; LineOwner *own[2] = { (LineOwner *)a, (LineOwner *)b }; for(duint i = 0; i < 2; ++i) { if(own[i]->_link[Anticlockwise]) // We have a cached result. { angles[i] = own[i]->angle(); } else { Line *line = &own[i]->line(); Vertex const &otherVtx = line->vertex(&line->from() == rootVtx? 1:0); fixed_t dx = otherVtx.origin().x - rootVtx->origin().x; fixed_t dy = otherVtx.origin().y - rootVtx->origin().y; own[i]->_angle = angles[i] = bamsAtan2(-100 *dx, 100 * dy); // Mark as having a cached angle. own[i]->_link[Anticlockwise] = (LineOwner *) 1; } } return (angles[1] - angles[0]); } /** * Merge left and right line owner lists into a new list. * * @return The newly merged list. */ static LineOwner *mergeLineOwners(LineOwner *left, LineOwner *right, dint (*compare) (void const *a, void const *b)) { LineOwner tmp; LineOwner *np = &tmp; tmp._link[Clockwise] = np; while(left && right) { if(compare(left, right) <= 0) { np->_link[Clockwise] = left; np = left; left = left->nextPtr(); } else { np->_link[Clockwise] = right; np = right; right = right->nextPtr(); } } // At least one of these lists is now empty. if(left) { np->_link[Clockwise] = left; } if(right) { np->_link[Clockwise] = right; } // Is the list empty? if(!tmp.hasNext()) return nullptr; return tmp.nextPtr(); } static LineOwner *splitLineOwners(LineOwner *list) { if(!list) return nullptr; LineOwner *lista = list; LineOwner *listb = list; LineOwner *listc = list; do { listc = listb; listb = listb->nextPtr(); lista = lista->nextPtr(); if(lista) { lista = lista->nextPtr(); } } while(lista); listc->_link[Clockwise] = nullptr; return listb; } /** * This routine uses a recursive mergesort algorithm; O(NlogN) */ static LineOwner *sortLineOwners(LineOwner *list, dint (*compare) (void const *a, void const *b)) { if(list && list->nextPtr()) { LineOwner *p = splitLineOwners(list); // Sort both halves and merge them back. list = mergeLineOwners(sortLineOwners(list, compare), sortLineOwners(p, compare), compare); } return list; } static void setVertexLineOwner(Vertex *vtx, Line *lineptr, LineOwner **storage) { if(!lineptr) return; // Has this line already been registered with this vertex? LineOwner const *own = vtx->firstLineOwner(); while(own) { if(&own->line() == lineptr) return; // Yes, we can exit. own = &own->next(); } // Add a new owner. vtx->_numLineOwners++; LineOwner *newOwner = (*storage)++; newOwner->_line = lineptr; newOwner->_link[Anticlockwise] = nullptr; // Link it in. // NOTE: We don't bother linking everything at this stage since we'll // be sorting the lists anyway. After which we'll finish the job by // setting the prev and circular links. // So, for now this is only linked singlely, forward. newOwner->_link[Clockwise] = vtx->_lineOwners; vtx->_lineOwners = newOwner; // Link the line to its respective owner node. if(vtx == &lineptr->from()) lineptr->_vo1 = newOwner; else lineptr->_vo2 = newOwner; } #ifdef DENG2_DEBUG /** * Determines whether the specified vertex @a v has a correctly formed line * owner ring. */ static bool vertexHasValidLineOwnerRing(Vertex &v) { LineOwner const *base = v.firstLineOwner(); LineOwner const *cur = base; do { if(&cur->prev().next() != cur) return false; if(&cur->next().prev() != cur) return false; } while((cur = &cur->next()) != base); return true; } #endif /** * Generates the line owner rings for each vertex. Each ring includes all * the lines which the vertex belongs to sorted by angle, (the rings are * arranged in clockwise order, east = 0). */ void buildVertexLineOwnerRings(QList const &vertexs, QList &editableLines) { LOG_AS("buildVertexLineOwnerRings"); // // Step 1: Find and link up all line owners. // // We know how many vertex line owners we need (numLines * 2). auto *lineOwners = (LineOwner *) Z_Malloc(sizeof(LineOwner) * editableLines.count() * 2, PU_MAPSTATIC, 0); LineOwner *allocator = lineOwners; for(Line *line : editableLines) for(dint p = 0; p < 2; ++p) { setVertexLineOwner(&line->vertex(p), line, &allocator); } // // Step 2: Sort line owners of each vertex and finalize the rings. // for(Vertex *v : vertexs) { if(!v->_numLineOwners) continue; // Sort them; ordered clockwise by angle. rootVtx = v; v->_lineOwners = sortLineOwners(v->_lineOwners, lineAngleSorter); // Finish the linking job and convert to relative angles. // They are only singly linked atm, we need them to be doubly // and circularly linked. binangle_t firstAngle = v->_lineOwners->angle(); LineOwner *last = v->_lineOwners; LineOwner *p = last->nextPtr(); while(p) { p->_link[Anticlockwise] = last; // Convert to a relative angle between last and this. last->_angle = last->angle() - p->angle(); last = p; p = p->nextPtr(); } last->_link[Clockwise] = v->_lineOwners; v->_lineOwners->_link[Anticlockwise] = last; // Set the angle of the last owner. last->_angle = last->angle() - firstAngle; /*#ifdef DENG2_DEBUG LOG_MAP_VERBOSE("Vertex #%i: line owners #%i") << editmap.vertexes.indexOf(v) << v->lineOwnerCount(); LineOwner const *base = v->firstLineOwner(); LineOwner const *cur = base; duint idx = 0; do { LOG_MAP_VERBOSE(" %i: p= #%05i this= #%05i n= #%05i, dANG= %-3.f") << idx << cur->prev().line().indexInMap() << cur->line().indexInMap() << cur->next().line().indexInMap() << BANG2DEG(cur->angle()); idx++; } while((cur = &cur->next()) != base); #endif*/ // Sanity check. DENG2_ASSERT(vertexHasValidLineOwnerRing(*v)); } } bool Map::isEditable() const { return d->editingEnabled; } struct VertexInfo { Vertex *vertex = nullptr; ///< Vertex for this info. Vertex *equiv = nullptr; ///< Determined equivalent vertex. duint refCount = 0; ///< Line -> Vertex reference count. /// @todo Math here is not correct (rounding directionality). -ds dint compareVertexOrigins(VertexInfo const &other) const { DENG2_ASSERT(vertex && other.vertex); if(this == &other) return 0; if(vertex == other.vertex) return 0; // Order is firstly X axis major. if(dint(vertex->origin().x) != dint(other.vertex->origin().x)) { return dint(vertex->origin().x) - dint(other.vertex->origin().x); } // Order is secondly Y axis major. return dint(vertex->origin().y) - dint(other.vertex->origin().y); } bool operator < (VertexInfo const &other) const { return compareVertexOrigins(other) < 0; } }; void pruneVertexes(Mesh &mesh, Map::Lines const &lines) { // // Step 1 - Find equivalent vertexes: // // Populate the vertex info. QVector vertexInfo(mesh.vertexCount()); dint ord = 0; for(Vertex *vertex : mesh.vertexs()) { vertexInfo[ord++].vertex = vertex; } { // Sort a copy to place near vertexes adjacently. QVector sortedInfo(vertexInfo); qSort(sortedInfo.begin(), sortedInfo.end()); // Locate equivalent vertexes in the sorted info. for(dint i = 0; i < sortedInfo.count() - 1; ++i) { VertexInfo &a = sortedInfo[i]; VertexInfo &b = sortedInfo[i + 1]; // Are these equivalent? /// @todo fixme: What about polyobjs? They need unique vertexes! -ds if(a.compareVertexOrigins(b) == 0) { b.equiv = (a.equiv? a.equiv : a.vertex); } } } // // Step 2 - Replace line references to equivalent vertexes: // // Count line -> vertex references. for(Line *line : lines) { vertexInfo[line->from().indexInMap()].refCount++; vertexInfo[ line->to().indexInMap()].refCount++; } // Perform the replacement. for(Line *line : lines) { while(vertexInfo[line->from().indexInMap()].equiv) { VertexInfo &info = vertexInfo[line->from().indexInMap()]; info.refCount--; line->replaceFrom(*info.equiv); vertexInfo[line->from().indexInMap()].refCount++; } while(vertexInfo[line->to().indexInMap()].equiv) { VertexInfo &info = vertexInfo[line->to().indexInMap()]; info.refCount--; line->replaceTo(*info.equiv); vertexInfo[line->to().indexInMap()].refCount++; } } // // Step 3 - Prune vertexes: // dint prunedCount = 0, numUnused = 0; for(VertexInfo const &info : vertexInfo) { Vertex *vertex = info.vertex; if(info.refCount) continue; mesh.removeVertex(*vertex); prunedCount += 1; if(!info.equiv) numUnused += 1; } if(prunedCount) { // Re-index with a contiguous range of indices. dint ord = 0; for(Vertex *vertex : mesh.vertexs()) { vertex->setIndexInMap(ord++); } /// Update lines. @todo Line should handle this itself. for(Line *line : lines) { line->updateSlopeType(); line->updateAABox(); } LOGDEV_MAP_NOTE("Pruned %d vertexes (%d equivalents, %d unused)") << prunedCount << (prunedCount - numUnused) << numUnused; } } bool Map::endEditing() { if(!d->editingEnabled) return true; // Huh? d->editingEnabled = false; LOG_AS("Map"); LOG_MAP_VERBOSE("Editing ended"); LOGDEV_MAP_VERBOSE("New elements: %d Vertexes, %d Lines, %d Polyobjs and %d Sectors") << d->mesh.vertexCount() << d->editable.lines.count() << d->editable.polyobjs.count() << d->editable.sectors.count(); // // Perform cleanup on the new map elements. // pruneVertexes(d->mesh, d->editable.lines); // Ensure lines with only one sector are flagged as blocking. for(Line *line : d->editable.lines) { if(!line->hasFrontSector() || !line->hasBackSector()) line->setFlags(DDLF_BLOCKING); } buildVertexLineOwnerRings(d->mesh.vertexs(), d->editable.lines); // // Move the editable elements to the "static" element lists. // // Collate sectors: DENG2_ASSERT(d->sectors.isEmpty()); #ifdef DENG2_QT_4_7_OR_NEWER d->sectors.reserve(d->editable.sectors.count()); #endif d->sectors.append(d->editable.sectors); d->editable.sectors.clear(); // Collate lines: DENG2_ASSERT(d->lines.isEmpty()); #ifdef DENG2_QT_4_7_OR_NEWER d->lines.reserve(d->editable.lines.count()); #endif d->lines.append(d->editable.lines); d->editable.lines.clear(); // Collate polyobjs: DENG2_ASSERT(d->polyobjs.isEmpty()); #ifdef DENG2_QT_4_7_OR_NEWER d->polyobjs.reserve(d->editable.polyobjs.count()); #endif while(!d->editable.polyobjs.isEmpty()) { d->polyobjs.append(d->editable.polyobjs.takeFirst()); Polyobj *polyobj = d->polyobjs.back(); // Create half-edge geometry and line segments for each line. for(Line *line : polyobj->lines()) { HEdge *hedge = polyobj->mesh().newHEdge(line->from()); hedge->setTwin(polyobj->mesh().newHEdge(line->to())); hedge->twin().setTwin(hedge); LineSideSegment *seg = line->front().addSegment(*hedge); #ifdef __CLIENT__ seg->setLength(line->length()); #else DENG2_UNUSED(seg); #endif } polyobj->buildUniqueVertexes(); polyobj->updateOriginalVertexCoords(); } // Determine the map bounds. d->updateBounds(); LOG_MAP_VERBOSE("Geometry bounds:") << Rectangled(d->bounds.min, d->bounds.max).asText(); // Build a line blockmap. d->initLineBlockmap(); // Build a new BspTree. if(!d->buildBspTree()) return false; // The mobj and polyobj blockmaps are maintained dynamically. d->initMobjBlockmap(); d->initPolyobjBlockmap(); // Finish lines. for(Line *line : d->lines) for(dint i = 0; i < 2; ++i) { line->side(i).updateSurfaceNormals(); line->side(i).updateAllSoundEmitterOrigins(); } // Finish sectors. for(Sector *sector : d->sectors) { d->buildClusters(*sector); sector->buildSides(); sector->chainSoundEmitters(); } // Finish planes. for(Sector *sector : d->sectors) { sector->forAllPlanes([] (Plane &plane) { plane.updateSoundEmitterOrigin(); return LoopContinue; }); } // We can now initialize the convex subspace blockmap. d->initSubspaceBlockmap(); // Prepare the thinker lists. d->thinkers.reset(new Thinkers); return true; } Vertex *Map::createVertex(Vector2d const &origin, dint archiveIndex) { if(!d->editingEnabled) /// @throw EditError Attempted when not editing. throw EditError("Map::createVertex", "Editing is not enabled"); Vertex *vtx = d->mesh.newVertex(origin); vtx->setMap(this); vtx->setIndexInArchive(archiveIndex); /// @todo Don't do this here. vtx->setIndexInMap(d->mesh.vertexCount() - 1); return vtx; } Line *Map::createLine(Vertex &v1, Vertex &v2, int flags, Sector *frontSector, Sector *backSector, int archiveIndex) { if(!d->editingEnabled) /// @throw EditError Attempted when not editing. throw EditError("Map::createLine", "Editing is not enabled"); auto *line = new Line(v1, v2, flags, frontSector, backSector); d->editable.lines.append(line); line->setMap(this); line->setIndexInArchive(archiveIndex); /// @todo Don't do this here. line->setIndexInMap(d->editable.lines.count() - 1); line->front().setIndexInMap(Map::toSideIndex(line->indexInMap(), Line::Front)); line->back ().setIndexInMap(Map::toSideIndex(line->indexInMap(), Line::Back)); return line; } Sector *Map::createSector(dfloat lightLevel, Vector3f const &lightColor, dint archiveIndex) { if(!d->editingEnabled) /// @throw EditError Attempted when not editing. throw EditError("Map::createSector", "Editing is not enabled"); auto *sector = new Sector(lightLevel, lightColor); d->editable.sectors.append(sector); sector->setMap(this); sector->setIndexInArchive(archiveIndex); /// @todo Don't do this here. sector->setIndexInMap(d->editable.sectors.count() - 1); return sector; } Polyobj *Map::createPolyobj(Vector2d const &origin) { if(!d->editingEnabled) /// @throw EditError Attempted when not editing. throw EditError("Map::createPolyobj", "Editing is not enabled"); void *region = M_Calloc(POLYOBJ_SIZE); auto *pob = new (region) Polyobj(origin); d->editable.polyobjs.append(pob); /// @todo Don't do this here. pob->setIndexInMap(d->editable.polyobjs.count() - 1); return pob; } Map::Lines const &Map::editableLines() const { if(!d->editingEnabled) /// @throw EditError Attempted when not editing. throw EditError("Map::editableLines", "Editing is not enabled"); return d->editable.lines; } Map::Sectors const &Map::editableSectors() const { if(!d->editingEnabled) /// @throw EditError Attempted when not editing. throw EditError("Map::editableSectors", "Editing is not enabled"); return d->editable.sectors; } Map::Polyobjs const &Map::editablePolyobjs() const { if(!d->editingEnabled) /// @throw EditError Attempted when not editing. throw EditError("Map::editablePolyobjs", "Editing is not enabled"); return d->editable.polyobjs; } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/world/sector.cpp0000664000175000017500000003446112641367670023222 0ustar jaakkojaakko/** @file sector.h World map sector. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/sector.h" #include #include #include #include #include "world/map.h" #include "world/p_object.h" #include "Line" #include "Plane" #include "Surface" #include "SectorCluster" using namespace de; DENG2_PIMPL(Sector) , DENG2_OBSERVES(Plane, HeightChange) { AABoxd aaBox; ///< Bounding box for the whole sector (all clusters). bool needAABoxUpdate = true; ThinkerT emitter; ///< Head of the sound emitter chain. typedef QList Planes; Planes planes; ///< All owned planes. typedef QList Sides; Sides sides; ///< All referencing line sides (not owned). mobj_t *mobjList = nullptr; ///< All mobjs "in" the sector (not owned). float lightLevel = 0; ///< Ambient light level. Vector3f lightColor; ///< Ambient light color. int validCount = 0; #ifdef __CLIENT__ coord_t roughArea = 0; ///< Approximated. @c <0 means an update is needed. bool needRoughAreaUpdate = true; #endif Instance(Public *i) : Base(i) {} ~Instance() { qDeleteAll(planes); } void notifyLightLevelChanged() { DENG2_FOR_PUBLIC_AUDIENCE(LightLevelChange, i) i->sectorLightLevelChanged(self); } void notifyLightColorChanged() { DENG2_FOR_PUBLIC_AUDIENCE(LightColorChange, i) i->sectorLightColorChanged(self); } /** * Update the axis-aligned bounding box in the map coordinate space to * encompass the geometry of all BSP leaf clusters of the sector. */ void updateAABoxIfNeeded() { if(!needAABoxUpdate) return; needAABoxUpdate = false; aaBox.clear(); bool haveGeometry = false; self.map().forAllClusters(thisPublic, [this, &haveGeometry] (SectorCluster &cluster) { if(haveGeometry) { V2d_UniteBox(aaBox.arvec2, cluster.aaBox().arvec2); } else { aaBox = cluster.aaBox(); haveGeometry = true; } return LoopContinue; }); // The XY origin of our sound emitter can now be updated as the center // point of the sector geometry is now known. if(haveGeometry) { emitter->origin[0] = (aaBox.minX + aaBox.maxX) / 2; emitter->origin[1] = (aaBox.minY + aaBox.maxY) / 2; } else { emitter->origin[0] = emitter->origin[1] = 0; } } #ifdef __CLIENT__ void updateRoughAreaIfNeeded() { if(!needRoughAreaUpdate) return; needRoughAreaUpdate = false; roughArea = 0; self.map().forAllClusters(thisPublic, [&] (SectorCluster &cluster) { roughArea += cluster.roughArea(); return LoopContinue; }); } #endif // __CLIENT__ /** * To be called to update sound emitter origins for all dependent surfaces. */ void updateDependentSurfaceSoundEmitterOrigins() { for(LineSide *side : sides) { side->updateAllSoundEmitterOrigins(); side->back().updateAllSoundEmitterOrigins(); } } // Observes Plane HeightChange. void planeHeightChanged(Plane & /*plane*/) { // Update the z-height origin of our sound emitter right away. emitter->origin[2] = (self.floor().height() + self.ceiling().height()) / 2; #ifdef __CLIENT__ // A plane move means we must re-apply missing material fixes. for(LineSide *side : sides) { side->fixMissingMaterials(); side->back().fixMissingMaterials(); } #endif updateDependentSurfaceSoundEmitterOrigins(); } }; Sector::Sector(float lightLevel, Vector3f const &lightColor) : MapElement(DMU_SECTOR) , d(new Instance(this)) { d->lightLevel = de::clamp(0.f, lightLevel, 1.f); d->lightColor = lightColor.min(Vector3f(1, 1, 1)).max(Vector3f(0, 0, 0)); } float Sector::lightLevel() const { return d->lightLevel; } void Sector::setLightLevel(float newLightLevel) { newLightLevel = de::clamp(0.f, newLightLevel, 1.f); if(!de::fequal(d->lightLevel, newLightLevel)) { d->lightLevel = newLightLevel; d->notifyLightLevelChanged(); } } Vector3f const &Sector::lightColor() const { return d->lightColor; } void Sector::setLightColor(Vector3f const &newLightColor) { Vector3f newColorClamped(de::clamp(0.f, newLightColor.x, 1.f), de::clamp(0.f, newLightColor.y, 1.f), de::clamp(0.f, newLightColor.z, 1.f)); if(d->lightColor != newColorClamped) { d->lightColor = newColorClamped; d->notifyLightColorChanged(); } } struct mobj_s *Sector::firstMobj() const { return d->mobjList; } /** * Two links to update: * 1) The link to the mobj from the previous node (sprev, always set) will * be modified to point to the node following it. * 2) If there is a node following the mobj, set its sprev pointer to point * to the pointer that points back to it (the mobj's sprev, just modified). */ void Sector::unlink(mobj_t *mobj) { if(!mobj || !Mobj_IsSectorLinked(mobj)) return; if((*mobj->sPrev = mobj->sNext)) mobj->sNext->sPrev = mobj->sPrev; // Not linked any more. mobj->sNext = nullptr; mobj->sPrev = nullptr; // Ensure this has been completely unlinked. #ifdef DENG2_DEBUG for(mobj_t *iter = d->mobjList; iter; iter = iter->sNext) { DENG2_ASSERT(iter != mobj); } #endif } void Sector::link(mobj_t *mobj) { if(!mobj) return; // Ensure this isn't already linked. #ifdef DENG2_DEBUG for(mobj_t *iter = d->mobjList; iter; iter = iter->sNext) { DENG2_ASSERT(iter != mobj); } #endif // Prev pointers point to the pointer that points back to us. // (Which practically disallows traversing the list backwards.) if((mobj->sNext = d->mobjList)) mobj->sNext->sPrev = &mobj->sNext; *(mobj->sPrev = &d->mobjList) = mobj; } SoundEmitter &Sector::soundEmitter() { // Emitter origin depends on the axis-aligned bounding box. d->updateAABoxIfNeeded(); return d->emitter; } SoundEmitter const &Sector::soundEmitter() const { return const_cast(const_cast(*this).soundEmitter()); } int Sector::validCount() const { return d->validCount; } void Sector::setValidCount(int newValidCount) { d->validCount = newValidCount; } bool Sector::hasSkyMaskedPlane() const { for(Plane *plane : d->planes) { if(plane->surface().hasSkyMaskedMaterial()) return true; } return false; } int Sector::planeCount() const { return d->planes.count(); } Plane &Sector::plane(int planeIndex) { if(planeIndex >= 0 && planeIndex < d->planes.count()) { return *d->planes.at(planeIndex); } /// @throw MissingPlaneError The referenced plane does not exist. throw MissingPlaneError("Sector::plane", QString("Missing plane %1").arg(planeIndex)); } Plane const &Sector::plane(int planeIndex) const { return const_cast(this)->plane(planeIndex); } Plane *Sector::addPlane(Vector3f const &normal, coord_t height) { Plane *plane = new Plane(*this, normal, height); plane->setIndexInSector(d->planes.count()); d->planes.append(plane); if(plane->isSectorFloor() || plane->isSectorCeiling()) { // We want notification of height changes so that we can update sound // emitter origins of dependent surfaces. plane->audienceForHeightChange() += d; } // Once both floor and ceiling are known we can determine the z-height origin // of our sound emitter. /// @todo fixme: Assume planes are defined in order. if(planeCount() == 2) { d->emitter->origin[2] = (floor().height() + ceiling().height()) / 2; } return plane; } LoopResult Sector::forAllPlanes(std::function func) const { for(Plane *plane : d->planes) { if(auto result = func(*plane)) return result; } return LoopContinue; } int Sector::sideCount() const { return d->sides.count(); } LoopResult Sector::forAllSides(std::function func) const { for(LineSide *side : d->sides) { if(auto result = func(*side)) return result; } return LoopContinue; } void Sector::buildSides() { d->sides.clear(); #ifdef DENG2_QT_4_7_OR_NEWER int count = 0; map().forAllLines([this, &count] (Line &line) { if(line.frontSectorPtr() == this || line.backSectorPtr() == this) { ++count; } return LoopContinue; }); if(!count) return; d->sides.reserve(count); #endif map().forAllLines([this] (Line &line) { if(line.frontSectorPtr() == this) { // Ownership of the side is not given to the sector. d->sides.append(&line.front()); } else if(line.backSectorPtr() == this) { // Ownership of the side is not given to the sector. d->sides.append(&line.back()); } return LoopContinue; }); } static void linkSoundEmitter(SoundEmitter &root, SoundEmitter &newEmitter) { // The sector's base is always root of the chain, so link the other after it. newEmitter.thinker.prev = &root.thinker; newEmitter.thinker.next = root.thinker.next; if(newEmitter.thinker.next) newEmitter.thinker.next->prev = &newEmitter.thinker; root.thinker.next = &newEmitter.thinker; } void Sector::chainSoundEmitters() { SoundEmitter &root = d->emitter; // Clear the root of the emitter chain. root.thinker.next = root.thinker.prev = nullptr; // Link plane surface emitters: for(Plane *plane : d->planes) { linkSoundEmitter(root, plane->soundEmitter()); } // Link wall surface emitters: for(LineSide *side : d->sides) { if(side->hasSections()) { linkSoundEmitter(root, side->middleSoundEmitter()); linkSoundEmitter(root, side->bottomSoundEmitter()); linkSoundEmitter(root, side->topSoundEmitter()); } if(side->line().isSelfReferencing() && side->back().hasSections()) { LineSide &back = side->back(); linkSoundEmitter(root, back.middleSoundEmitter()); linkSoundEmitter(root, back.bottomSoundEmitter()); linkSoundEmitter(root, back.topSoundEmitter()); } } } #ifdef __CLIENT__ AABoxd const &Sector::aaBox() const { d->updateAABoxIfNeeded(); return d->aaBox; } coord_t Sector::roughArea() const { d->updateRoughAreaIfNeeded(); return d->roughArea; } #endif // __CLIENT__ int Sector::property(DmuArgs &args) const { switch(args.prop) { case DMU_LIGHT_LEVEL: args.setValue(DMT_SECTOR_LIGHTLEVEL, &d->lightLevel, 0); break; case DMU_COLOR: args.setValue(DMT_SECTOR_RGB, &d->lightColor.x, 0); args.setValue(DMT_SECTOR_RGB, &d->lightColor.y, 1); args.setValue(DMT_SECTOR_RGB, &d->lightColor.z, 2); break; case DMU_COLOR_RED: args.setValue(DMT_SECTOR_RGB, &d->lightColor.x, 0); break; case DMU_COLOR_GREEN: args.setValue(DMT_SECTOR_RGB, &d->lightColor.y, 0); break; case DMU_COLOR_BLUE: args.setValue(DMT_SECTOR_RGB, &d->lightColor.z, 0); break; case DMU_EMITTER: { SoundEmitter const *emitterAdr = d->emitter; args.setValue(DMT_SECTOR_EMITTER, &emitterAdr, 0); break; } case DMT_MOBJS: args.setValue(DMT_SECTOR_MOBJLIST, &d->mobjList, 0); break; case DMU_VALID_COUNT: args.setValue(DMT_SECTOR_VALIDCOUNT, &d->validCount, 0); break; case DMU_FLOOR_PLANE: { Plane *pln = d->planes.at(Floor); args.setValue(DMT_SECTOR_FLOORPLANE, &pln, 0); break; } case DMU_CEILING_PLANE: { Plane *pln = d->planes.at(Ceiling); args.setValue(DMT_SECTOR_CEILINGPLANE, &pln, 0); break; } default: return MapElement::property(args); } return false; // Continue iteration. } int Sector::setProperty(DmuArgs const &args) { switch(args.prop) { case DMU_COLOR: { Vector3f newColor = d->lightColor; args.value(DMT_SECTOR_RGB, &newColor.x, 0); args.value(DMT_SECTOR_RGB, &newColor.y, 1); args.value(DMT_SECTOR_RGB, &newColor.z, 2); setLightColor(newColor); break; } case DMU_COLOR_RED: { Vector3f newColor = d->lightColor; args.value(DMT_SECTOR_RGB, &newColor.x, 0); setLightColor(newColor); break; } case DMU_COLOR_GREEN: { Vector3f newColor = d->lightColor; args.value(DMT_SECTOR_RGB, &newColor.y, 0); setLightColor(newColor); break; } case DMU_COLOR_BLUE: { Vector3f newColor = d->lightColor; args.value(DMT_SECTOR_RGB, &newColor.z, 0); setLightColor(newColor); break; } case DMU_LIGHT_LEVEL: { float newLightLevel; args.value(DMT_SECTOR_LIGHTLEVEL, &newLightLevel, 0); setLightLevel(newLightLevel); break; } case DMU_VALID_COUNT: args.value(DMT_SECTOR_VALIDCOUNT, &d->validCount, 0); break; default: return MapElement::setProperty(args); } return false; // Continue iteration. } doomsday-stable-1.15.7/doomsday/client/src/world/maputil.cpp0000664000175000017500000002334512641367670023375 0ustar jaakkojaakko/** @file maputil.cpp World map utilities. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/maputil.h" #include "Line" #include "Plane" #include "Sector" #ifdef __CLIENT__ # include "Surface" # include "world/lineowner.h" # include "MaterialVariantSpec" # include "render/rend_main.h" // Rend_MapSurfacematerialSpec # include "MaterialAnimator" # include "WallEdge" #endif using namespace de; lineopening_s::lineopening_s(Line const &line) { if(!line.hasBackSector()) { top = bottom = range = lowFloor = 0; return; } Sector const *frontSector = line.front().sectorPtr(); Sector const *backSector = line.back().sectorPtr(); DENG_ASSERT(frontSector != 0); if(backSector && backSector->ceiling().height() < frontSector->ceiling().height()) { top = backSector->ceiling().height(); } else { top = frontSector->ceiling().height(); } if(backSector && backSector->floor().height() > frontSector->floor().height()) { bottom = backSector->floor().height(); } else { bottom = frontSector->floor().height(); } range = top - bottom; // Determine the "low floor". if(backSector && frontSector->floor().height() > backSector->floor().height()) { lowFloor = float( backSector->floor().height() ); } else { lowFloor = float( frontSector->floor().height() ); } } lineopening_s &lineopening_s::operator = (lineopening_s const &other) { top = other.top; bottom = other.bottom; range = other.range; lowFloor = other.lowFloor; return *this; } #ifdef __CLIENT__ /** * Same as @ref R_OpenRange() but works with the "visual" (i.e., smoothed) plane * height coordinates rather than the "sharp" coordinates. * * @param side Line side to find the open range for. * * Return values: * @param bottom Bottom Z height is written here. Can be @c 0. * @param top Top Z height is written here. Can be @c 0. * * @return Height of the open range. * * @todo fixme: Should use the visual plane heights of sector clusters. */ static coord_t visOpenRange(LineSide const &side, coord_t *retBottom = 0, coord_t *retTop = 0) { Sector const *frontSec = side.sectorPtr(); Sector const *backSec = side.back().sectorPtr(); coord_t bottom; if(backSec && backSec->floor().heightSmoothed() > frontSec->floor().heightSmoothed()) { bottom = backSec->floor().heightSmoothed(); } else { bottom = frontSec->floor().heightSmoothed(); } coord_t top; if(backSec && backSec->ceiling().heightSmoothed() < frontSec->ceiling().heightSmoothed()) { top = backSec->ceiling().heightSmoothed(); } else { top = frontSec->ceiling().heightSmoothed(); } if(retBottom) *retBottom = bottom; if(retTop) *retTop = top; return top - bottom; } /// @todo fixme: Should use the visual plane heights of sector clusters. bool R_SideBackClosed(LineSide const &side, bool ignoreOpacity) { if(!side.hasSections()) return false; if(!side.hasSector()) return false; if(side.line().isSelfReferencing()) return false; // Never. if(side.considerOneSided()) return true; Sector const &frontSec = side.sector(); Sector const &backSec = side.back().sector(); if(backSec.floor().heightSmoothed() >= backSec.ceiling().heightSmoothed()) return true; if(backSec.ceiling().heightSmoothed() <= frontSec.floor().heightSmoothed()) return true; if(backSec.floor().heightSmoothed() >= frontSec.ceiling().heightSmoothed()) return true; // Perhaps a middle material completely covers the opening? if(side.middle().hasMaterial()) { MaterialAnimator &matAnimator = side.middle().material().getAnimator(Rend_MapSurfaceMaterialSpec()); // Ensure we have up to date info about the material. matAnimator.prepare(); if(ignoreOpacity || (matAnimator.isOpaque() && !side.middle().blendMode() && side.middle().opacity() >= 1)) { // Stretched middles always cover the opening. if(side.isFlagged(SDF_MIDDLE_STRETCH)) return true; if(side.leftHEdge()) // possibility of degenerate BSP leaf { coord_t openRange, openBottom, openTop; openRange = visOpenRange(side, &openBottom, &openTop); if(matAnimator.dimensions().y >= openRange) { // Possibly; check the placement. WallEdge edge(WallSpec::fromMapSide(side, LineSide::Middle), *side.leftHEdge(), Line::From); return (edge.isValid() && edge.top().z() > edge.bottom().z() && edge.top().z() >= openTop && edge.bottom().z() <= openBottom); } } } } return false; } Line *R_FindLineNeighbor(Sector const *sector, Line const *line, LineOwner const *own, bool antiClockwise, binangle_t *diff) { LineOwner const *cown = antiClockwise? &own->prev() : &own->next(); Line *other = &cown->line(); if(other == line) return 0; if(diff) *diff += (antiClockwise? cown->angle() : own->angle()); if(!other->hasBackSector() || !other->isSelfReferencing()) { if(sector) // Must one of the sectors match? { if(other->frontSectorPtr() == sector || (other->hasBackSector() && other->backSectorPtr() == sector)) return other; } else { return other; } } // Not suitable, try the next. return R_FindLineNeighbor(sector, line, cown, antiClockwise, diff); } /** * @param side Line side for which to determine covered opening status. * * @return @c true iff there is a "middle" material on @a side which * completely covers the open range. * * @todo fixme: Should use the visual plane heights of sector clusters. */ static bool middleMaterialCoversOpening(LineSide const &side) { if(!side.hasSector()) return false; // Never. if(!side.hasSections()) return false; if(!side.middle().hasMaterial()) return false; // Stretched middles always cover the opening. if(side.isFlagged(SDF_MIDDLE_STRETCH)) return true; MaterialAnimator &matAnimator = side.middle().material().getAnimator(Rend_MapSurfaceMaterialSpec()); // Ensure we have up to date info about the material. matAnimator.prepare(); if(matAnimator.isOpaque() && !side.middle().blendMode() && side.middle().opacity() >= 1) { if(side.leftHEdge()) // possibility of degenerate BSP leaf { coord_t openRange, openBottom, openTop; openRange = visOpenRange(side, &openBottom, &openTop); if(matAnimator.dimensions().y >= openRange) { // Possibly; check the placement. WallEdge edge(WallSpec::fromMapSide(side, LineSide::Middle), *side.leftHEdge(), Line::From); return (edge.isValid() && edge.top().z() > edge.bottom().z() && edge.top().z() >= openTop && edge.bottom().z() <= openBottom); } } } return false; } /// @todo fixme: Should use the visual plane heights of sector clusters. Line *R_FindSolidLineNeighbor(Sector const *sector, Line const *line, LineOwner const *own, bool antiClockwise, binangle_t *diff) { DENG_ASSERT(sector); LineOwner const *cown = antiClockwise? &own->prev() : &own->next(); Line *other = &cown->line(); if(other == line) return 0; if(diff) *diff += (antiClockwise? cown->angle() : own->angle()); if(!((other->isBspWindow()) && other->frontSectorPtr() != sector) && !other->isSelfReferencing()) { if(!other->hasFrontSector()) return other; if(!other->hasBackSector()) return other; if(other->frontSector().floor().heightSmoothed() >= sector->ceiling().heightSmoothed() || other->frontSector().ceiling().heightSmoothed() <= sector->floor().heightSmoothed() || other->backSector().floor().heightSmoothed() >= sector->ceiling().heightSmoothed() || other->backSector().ceiling().heightSmoothed() <= sector->floor().heightSmoothed() || other->backSector().ceiling().heightSmoothed() <= other->backSector().floor().heightSmoothed()) return other; // Both front and back MUST be open by this point. // Perhaps a middle material completely covers the opening? // We should not give away the location of false walls (secrets). LineSide &otherSide = other->side(other->frontSectorPtr() == sector? Line::Front : Line::Back); if(middleMaterialCoversOpening(otherSide)) return other; } // Not suitable, try the next. return R_FindSolidLineNeighbor(sector, line, cown, antiClockwise, diff); } #endif // __CLIENT__ doomsday-stable-1.15.7/doomsday/client/src/world/hand.cpp0000664000175000017500000001666212641367670022640 0ustar jaakkojaakko/** @file hand.cpp Hand (metaphor) for the manipulation of "grabbables". * * @authors Copyright © 2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "world/hand.h" #include "BiasSource" // remove me using namespace de; enum PropertyFlag { Color = 0x1, Intensity = 0x2 }; Q_DECLARE_FLAGS(PropertyFlags, PropertyFlag) Q_DECLARE_OPERATORS_FOR_FLAGS(PropertyFlags) DENG2_PIMPL(Hand) , DENG2_OBSERVES(Grabbable, Deletion) , DENG2_OBSERVES(Grabbable, OriginChange) { /// Origin of the hand in the map coordinate space. Vector3d origin; Vector3d oldOrigin; // For tracking changes. /// All currently held grabbables if any (not owned). Grab grab; /// Averaged origin of eveything currently grabbed. Vector3d grabOrigin; bool needUpdateGrabOrigin; /// Edit properties (applied to the grabbables). Vector3f editColor; float editIntensity; PropertyFlags applyProps; Instance(Public *i, Vector3d const &origin) : Base(i) , origin(origin) , oldOrigin(origin) , needUpdateGrabOrigin(false) , editIntensity(0) , applyProps(0) {} void notifyGrabbed(Grabbable &grabbed) { DENG2_FOR_PUBLIC_AUDIENCE(Grabbed, i) { i->handGrabbed(self, grabbed); } } void notifyUngrabbed(Grabbable &ungrabbed) { DENG2_FOR_PUBLIC_AUDIENCE(Ungrabbed, i) { i->handUngrabbed(self, ungrabbed); } } void grabOne(Grabbable &grabbable) { // Ignore attempts to re-grab. if(grab.contains(&grabbable)) return; grabbable.grab(); // Ensure the grabbable is locked. grabbable.lock(); grab.append(&grabbable); grabbable.audienceForDeletion += this; grabbable.audienceForOriginChange += this; needUpdateGrabOrigin = true; notifyGrabbed(grabbable); } void ungrabOne(Grabbable &grabbable) { // Ignore attempts to ungrab what isn't grabbed. if(!grab.contains(&grabbable)) return; grabbable.ungrab(); // Ensure the grabbable is unlocked. grabbable.unlock(); grabbable.audienceForDeletion -= this; grabbable.audienceForOriginChange -= this; grab.removeOne(&grabbable); needUpdateGrabOrigin = true; notifyUngrabbed(grabbable); } // Observes Grabbable Deletion. void grabbableBeingDeleted(Grabbable &grabbable) { DENG2_ASSERT(grab.contains(&grabbable)); //sanity check. grab.removeOne(&grabbable); needUpdateGrabOrigin = true; notifyUngrabbed(grabbable); } // Observes Grabbable OriginChange. void grabbableOriginChanged(Grabbable &grabbable) { DENG_UNUSED(grabbable); DENG2_ASSERT(grab.contains(&grabbable)); //sanity check. needUpdateGrabOrigin = true; } void updateGrabOrigin() { needUpdateGrabOrigin = false; grabOrigin = Vector3d(); foreach(Grabbable *grabbable, grab) grabOrigin += grabbable->origin(); if(grab.count() > 1) grabOrigin /= grab.count(); //qDebug() << "Hand new grab origin" << grabOrigin.asText(); } }; Hand::Hand(Vector3d const &origin) : d(new Instance(this, origin)) {} Vector3d const &Hand::origin() const { return d->origin; } void Hand::setOrigin(Vector3d const &newOrigin) { if(d->origin != newOrigin) { //qDebug() << "Hand new origin" << newOrigin.asText(); d->origin = newOrigin; } } bool Hand::isEmpty() const { return grabbed().isEmpty(); } bool Hand::hasGrabbed(Grabbable const &grabbable) const { return grabbed().contains(const_cast(&grabbable)); } void Hand::grab(Grabbable &grabbable) { if(hasGrabbed(grabbable)) return; ungrab(); d->grabOne(grabbable); } void Hand::grabMulti(Grabbable &grabbable) { d->grabOne(grabbable); } void Hand::ungrab(Grabbable &grabbable) { d->ungrabOne(grabbable); } void Hand::ungrab() { //qDebug() << "Hand ungrabbing all"; while(!isEmpty()) { ungrab(*grabbed().last()); } } Hand::Grab const &Hand::grabbed() const { return d->grab; } de::Vector3d const &Hand::grabbedOrigin() const { if(d->needUpdateGrabOrigin) { d->updateGrabOrigin(); } return d->grabOrigin; } float Hand::editIntensity() const { return d->editIntensity; } void Hand::setEditIntensity(float newIntensity) { d->editIntensity = newIntensity; d->applyProps |= Intensity; } void Hand::setEditColor(Vector3f const &newColor) { d->editColor = newColor; d->applyProps |= Color; } Vector3f const &Hand::editColor() const { return d->editColor; } void Hand::worldSystemFrameEnds() { if(grabbedCount()) { // Do we need to move the grab? bool moveGrab = (d->origin != d->oldOrigin); if(moveGrab && d->needUpdateGrabOrigin) { // Determine the center of the grab. d->updateGrabOrigin(); } foreach(Grabbable *grabbable, grabbed()) { if(moveGrab) { // The move will be denied if the grabbable is locked. if(grabbedCount() == 1) { grabbable->move(d->origin); } else { grabbable->move(d->origin + (grabbable->origin() - d->grabOrigin)); } } if(!!d->applyProps) { /// @todo There should be a generic mechanism for applying the user's /// edits to the grabbables. The editable values are properties of the /// hand (an extension of the user's will) however the hand should not /// be responsible for their application as this requires knowledge of /// their meaning. Instead the hand should provide the values and let /// the grabbable(s) update themselves. if(!internal::cannotCastGrabbableTo(grabbable)) { BiasSource &biasSource = grabbable->as(); if(d->applyProps & Color) { biasSource.setColor(d->editColor); } if(d->applyProps & Intensity) { biasSource.setIntensity(d->editIntensity); } } } } } // Any property changes will have now been applied to the grabbed elements. d->applyProps = 0; // Update the hand origin tracking buffer. d->oldOrigin = d->origin; } doomsday-stable-1.15.7/doomsday/client/src/world/mapobject.cpp0000664000175000017500000000505512641367670023664 0ustar jaakkojaakko/** @file mapobject.cpp Base class for all map objects. * * @authors Copyright © 2013 Jaakko Keränen * @authors Copyright © 2013-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "world/mapobject.h" #include "world/map.h" using namespace de; DENG2_PIMPL_NOREF(MapObject) { Map *map = nullptr; dint indexInMap = NoIndex; Vector3d origin; ///< Position in map space. BspLeaf *bspLeaf = nullptr; ///< BSP leaf at @ref origin in the map (not owned). }; MapObject::MapObject(Vector3d const &origin) : d(new Instance) { d->origin = origin; } MapObject::~MapObject() {} Vector3d const &MapObject::origin() const { return d->origin; } void MapObject::setOrigin(Vector3d const &newOrigin) { if(d->origin != newOrigin) { // When moving on the XY plane; invalidate the BSP leaf. if(!de::fequal(d->origin.x, newOrigin.x) || !de::fequal(d->origin.y, newOrigin.y)) { d->bspLeaf = nullptr; } d->origin = newOrigin; } } void MapObject::move(Vector3d const &delta) { setOrigin(d->origin + delta); } BspLeaf &MapObject::bspLeafAtOrigin() const { if(!d->bspLeaf) { // Determine this now. d->bspLeaf = &map().bspLeafAt(origin()); } return *d->bspLeaf; } bool MapObject::hasMap() const { return d->map != nullptr; } Map &MapObject::map() const { if(d->map) return *d->map; /// @throw MissingMapError Attempted with no map attributed. throw MissingMapError("MapObject::map", "No map is attributed"); } void MapObject::setMap(Map *newMap) { d->map = newMap; } dint MapObject::indexInMap() const { return d->indexInMap; } void MapObject::setIndexInMap(dint newIndex) { d->indexInMap = newIndex; } doomsday-stable-1.15.7/doomsday/client/src/world/entitydef.cpp0000664000175000017500000003276512641367670023723 0ustar jaakkojaakko/** @file entitydef.cpp World playsim data structures. * * @authors Copyright © 2003-2013 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is M_Free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include #include #include #include #include "de_base.h" #include "de_play.h" #include "EntityDatabase" #include "world/map.h" #include "world/propertyvalue.h" using namespace de; // Map entity definitions. static StringPool *entityDefs; typedef std::map EntityDefIdMap; static EntityDefIdMap entityDefIdMap; static int clearEntityDefsWorker(StringPool::Id id, void * /*parameters*/) { MapEntityDef *def = static_cast( entityDefs->userPointer(id) ); DENG2_ASSERT(def); for(uint i = 0; i < def->numProps; ++i) { MapEntityPropertyDef *prop = def->props + i; M_Free(prop->name); } M_Free(def->props); delete def; return false; // Continue iteration. } static void clearEntityDefs(void) { if(!entityDefs) return; entityDefs->iterate(clearEntityDefsWorker, 0/*no parameters*/); delete entityDefs; entityDefs = 0; entityDefIdMap.clear(); } MapEntityDef *P_MapEntityDef(int id) { EntityDefIdMap::iterator i = entityDefIdMap.find(id); if(i != entityDefIdMap.end()) { StringPool::Id id = i->second; return static_cast( entityDefs->userPointer(id) ); } return 0; // Not found. } MapEntityDef *P_MapEntityDefByName(char const *name) { if(name && entityDefs) { StringPool::Id id = entityDefs->isInterned(String(name)); return static_cast( entityDefs->userPointer(id) ); } return 0; // Not found. } static int P_NameForMapEntityDefWorker(StringPool::Id id, void *parameters) { MapEntityDef *def = static_cast( parameters ); if(entityDefs->userPointer(id) == def) return id; return 0; // Continue iteration. } AutoStr *P_NameForMapEntityDef(MapEntityDef *def) { if(def) { StringPool::Id id = entityDefs->iterate(P_NameForMapEntityDefWorker, def); String const& name = entityDefs->string(id); QByteArray nameUtf8 = name.toUtf8(); return AutoStr_FromText(nameUtf8.constData()); } return AutoStr_NewStd(); } int MapEntityDef_Property(MapEntityDef *def, int propertyId, MapEntityPropertyDef **retDef = 0) { DENG2_ASSERT(def); MapEntityPropertyDef *found = 0; for(uint i = 0; i < def->numProps; ++i) { MapEntityPropertyDef *prop = def->props + i; if(prop->id == propertyId) { found = prop; break; } } if(retDef) *retDef = found; return found? found - def->props : -1/* not found */; } int MapEntityDef_PropertyByName(MapEntityDef *def, char const *propertyName, MapEntityPropertyDef **retDef) { DENG2_ASSERT(def); MapEntityPropertyDef *found = 0; if(propertyName && propertyName[0]) { for(uint i = 0; i < def->numProps; ++i) { MapEntityPropertyDef *prop = def->props + i; if(!stricmp(prop->name, propertyName)) { found = prop; break; } } } if(retDef) *retDef = found; return found? found - def->props : -1/* not found */; } void MapEntityDef_AddProperty(MapEntityDef* def, int propertyId, const char* propertyName, valuetype_t type) { DENG2_ASSERT(def); if(propertyId == 0) // Not a valid identifier. throw Error("MapEntityDef_AddProperty", "0 is not a valid propertyId"); if(!propertyName || !propertyName[0]) // Must have a name. throw Error("MapEntityDef_AddProperty", "Invalid propertyName (zero-length string)"); // A supported value type? switch(type) { case DDVT_BYTE: case DDVT_SHORT: case DDVT_INT: case DDVT_FIXED: case DDVT_ANGLE: case DDVT_FLOAT: break; default: throw Error("MapEntityDef_AddProperty", QString("Unknown/not supported value type %1").arg(type)); } // Ensure both the identifer and the name for the new property are unique. if(MapEntityDef_Property(def, propertyId) >= 0) throw Error("MapEntityDef_AddProperty", QString("propertyId %1 not unique for %2") .arg(propertyId).arg(Str_Text(P_NameForMapEntityDef(def)))); if(MapEntityDef_PropertyByName(def, propertyName) >= 0) throw Error("MapEntityDef_AddProperty", QString("propertyName \"%1\" not unique for %2") .arg(propertyName).arg(Str_Text(P_NameForMapEntityDef(def)))); // Looks good! Add it to the list of properties. def->props = (MapEntityPropertyDef*) M_Realloc(def->props, ++def->numProps * sizeof(*def->props)); if(!def->props) throw Error("MapEntityDef_AddProperty", QString("Failed on (re)allocation of %1 bytes for new MapEntityPropertyDef array") .arg((unsigned long) sizeof(*def->props))); MapEntityPropertyDef* prop = &def->props[def->numProps - 1]; prop->id = propertyId; int len = (int)strlen(propertyName); prop->name = (char *) M_Malloc(sizeof(*prop->name) * (len + 1)); if(!prop->name) throw Error("MapEntityDef_AddProperty", QString("Failed on allocation of %1 bytes for property name") .arg((unsigned long) (sizeof(*prop->name) * (len + 1)))); strncpy(prop->name, propertyName, len); prop->name[len] = '\0'; prop->type = type; prop->entity = def; } /** * Look up a mapobj definition. * * @param identifer If objName is @c NULL, compare using this unique identifier. * @param entityName If not @c NULL, compare using this unique name. * @param canCreate @c true= create a new definition if not found. */ static MapEntityDef *findMapEntityDef(int identifier, char const *entityName, bool canCreate) { if(identifier == 0 && (!entityName || !entityName[0])) return 0; // Is this an already known entity? if(entityName && entityName[0]) { MapEntityDef *found = P_MapEntityDefByName(entityName); if(found) return found; } else { MapEntityDef *found = P_MapEntityDef(identifier); if(found) return found; } // An unknown entity. Are we creating? if(!canCreate) return 0; // Ensure the name is unique. if(P_MapEntityDefByName(entityName)) return 0; // Ensure the identifier is unique. if(P_MapEntityDef(identifier)) return 0; // Have we yet to initialize the map entity definition dataset? if(!entityDefs) { entityDefs = new StringPool; } StringPool::Id id = entityDefs->intern(String(entityName)); MapEntityDef *def = new MapEntityDef(identifier); entityDefs->setUserPointer(id, def); entityDefIdMap.insert(std::pair(identifier, id)); return def; } #undef P_RegisterMapObj DENG_EXTERN_C dd_bool P_RegisterMapObj(int identifier, char const *name) { return findMapEntityDef(identifier, name, true /*do create*/) != 0; } #undef P_RegisterMapObjProperty DENG_EXTERN_C dd_bool P_RegisterMapObjProperty(int entityId, int propertyId, char const *propertyName, valuetype_t type) { try { MapEntityDef *def = findMapEntityDef(entityId, 0, false /*do not create*/); if(!def) throw Error("P_RegisterMapObjProperty", QString("Unknown entityId %1").arg(entityId)); MapEntityDef_AddProperty(def, propertyId, propertyName, type); return true; // Success! } catch(Error const &er) { LOG_WARNING("%s. Ignoring.") << er.asText(); } return false; } void P_InitMapEntityDefs() { // Allow re-init. clearEntityDefs(); } void P_ShutdownMapEntityDefs() { clearEntityDefs(); } static MapEntityPropertyDef *entityPropertyDef(int entityId, int propertyId) { // Is this a known entity? MapEntityDef *entity = P_MapEntityDef(entityId); if(!entity) throw Error("entityPropertyDef", QString("Unknown entity definition id %1").arg(entityId)); // Is this a known property? MapEntityPropertyDef *property; if(MapEntityDef_Property(entity, propertyId, &property) < 0) throw Error("entityPropertyDef", QString("Entity definition %1 has no property with id %2") .arg(Str_Text(P_NameForMapEntityDef(entity))) .arg(propertyId)); return property; // Found it. } static void setValue(void *dst, valuetype_t dstType, PropertyValue const &pvalue) { switch(dstType) { case DDVT_FIXED: *((fixed_t *) dst) = pvalue.asFixed(); break; case DDVT_FLOAT: *( (float *) dst) = pvalue.asFloat(); break; case DDVT_BYTE: *( (byte *) dst) = pvalue.asByte(); break; case DDVT_INT: *( (int *) dst) = pvalue.asInt32(); break; case DDVT_SHORT: *( (short *) dst) = pvalue.asInt16(); break; case DDVT_ANGLE: *((angle_t *) dst) = pvalue.asAngle(); break; default: throw Error("setValue", QString("Unknown value type %d").arg(dstType)); } } #undef P_GetGMOByte DENG_EXTERN_C byte P_GetGMOByte(int entityId, int elementIndex, int propertyId) { byte returnVal = 0; if(App_WorldSystem().hasMap()) { try { EntityDatabase &db = App_WorldSystem().map().entityDatabase(); MapEntityPropertyDef *propDef = entityPropertyDef(entityId, propertyId); setValue(&returnVal, DDVT_BYTE, db.property(propDef, elementIndex)); } catch(Error const &er) { LOG_WARNING("%s. Returning 0.") << er.asText(); } } return returnVal; } #undef P_GetGMOShort DENG_EXTERN_C short P_GetGMOShort(int entityId, int elementIndex, int propertyId) { short returnVal = 0; if(App_WorldSystem().hasMap()) { try { EntityDatabase &db = App_WorldSystem().map().entityDatabase(); MapEntityPropertyDef *propDef = entityPropertyDef(entityId, propertyId); setValue(&returnVal, DDVT_SHORT, db.property(propDef, elementIndex)); } catch(Error const &er) { LOG_WARNING("%s. Returning 0.") << er.asText(); } } return returnVal; } #undef P_GetGMOInt DENG_EXTERN_C int P_GetGMOInt(int entityId, int elementIndex, int propertyId) { int returnVal = 0; if(App_WorldSystem().hasMap()) { try { EntityDatabase &db = App_WorldSystem().map().entityDatabase(); MapEntityPropertyDef *propDef = entityPropertyDef(entityId, propertyId); setValue(&returnVal, DDVT_INT, db.property(propDef, elementIndex)); } catch(Error const &er) { LOG_WARNING("%s. Returning 0.") << er.asText(); } } return returnVal; } #undef P_GetGMOFixed DENG_EXTERN_C fixed_t P_GetGMOFixed(int entityId, int elementIndex, int propertyId) { fixed_t returnVal = 0; if(App_WorldSystem().hasMap()) { try { EntityDatabase &db = App_WorldSystem().map().entityDatabase(); MapEntityPropertyDef *propDef = entityPropertyDef(entityId, propertyId); setValue(&returnVal, DDVT_FIXED, db.property(propDef, elementIndex)); } catch(Error const &er) { LOG_WARNING("%s. Returning 0.") << er.asText(); } } return returnVal; } #undef P_GetGMOAngle DENG_EXTERN_C angle_t P_GetGMOAngle(int entityId, int elementIndex, int propertyId) { angle_t returnVal = 0; if(App_WorldSystem().hasMap()) { try { EntityDatabase &db = App_WorldSystem().map().entityDatabase(); MapEntityPropertyDef *propDef = entityPropertyDef(entityId, propertyId); setValue(&returnVal, DDVT_ANGLE, db.property(propDef, elementIndex)); } catch(Error const &er) { LOG_WARNING("%s. Returning 0.") << er.asText(); } } return returnVal; } #undef P_GetGMOFloat DENG_EXTERN_C float P_GetGMOFloat(int entityId, int elementIndex, int propertyId) { float returnVal = 0; if(App_WorldSystem().hasMap()) { try { EntityDatabase &db = App_WorldSystem().map().entityDatabase(); MapEntityPropertyDef *propDef = entityPropertyDef(entityId, propertyId); setValue(&returnVal, DDVT_FLOAT, db.property(propDef, elementIndex)); } catch(Error const &er) { LOG_WARNING("%s. Returning 0.") << er.asText(); } } return returnVal; } doomsday-stable-1.15.7/doomsday/client/src/world/polyobjdata.cpp0000664000175000017500000000356312641367670024232 0ustar jaakkojaakko/** @file polyobjdata.cpp Private data for polyobj. * * @authors Copyright © 2003-2014 Jaakko Keränen * @authors Copyright © 2006-2013 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, see: * http://www.gnu.org/licenses */ #include "world/polyobjdata.h" #include "world/thinkers.h" using namespace de; PolyobjData::PolyobjData() : origIndex(MapElement::NoIndex) , _polyobj(0) #ifdef __CLIENT__ , _mover(0) #endif { mesh = new Mesh; indexInMap = MapElement::NoIndex; } PolyobjData::~PolyobjData() { delete mesh; } void PolyobjData::setThinker(thinker_s *thinker) { _polyobj = (Polyobj *) thinker; } void PolyobjData::think() { // nothing to do; public thinker does all } Thinker::IData *PolyobjData::duplicate() const { return new PolyobjData(*this); } #ifdef __CLIENT__ void PolyobjData::addMover(ClPolyMover &mover) { if(_mover) { Thinker_Map(_mover->thinker()).thinkers().remove(_mover->thinker()); DENG2_ASSERT(!_mover); } _mover = &mover; } void PolyobjData::removeMover(ClPolyMover &mover) { if(_mover == &mover) { _mover = 0; } } ClPolyMover *PolyobjData::mover() const { return _mover; } #endif doomsday-stable-1.15.7/doomsday/client/src/world/reject.cpp0000664000175000017500000000724512641367670023177 0ustar jaakkojaakko/** @file reject.h World map sector LOS reject LUT building. * * @authors Copyright © 2007-2013 Daniel Swanson * @authors Copyright © 2000-2007 Andrew Apted * @authors Copyright © 1998-2000 Colin Reed * @authors Copyright © 1998-2000 Lee Killough * * Based on glBSP 2.24 (in turn, based on BSP 2.3). * @see http://sourceforge.net/projects/glbsp/ * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #if 0 // Needs updating. #include #include "de_base.h" #include "world/map.h" using namespace de; void BuildRejectForMap(Map const &map) { int *secGroups = M_Malloc(sizeof(int) * map.sectorCount()); int group = 0; for(uint i = 0; i < map.sectorCount(); ++i) { Sector §or = map.sectors[i]; secGroups[i] = group++; sector.rejNext = sector.rejPrev = §or; } for(uint i = 0; i < map.lineCount(); ++i) { Line *line = &map.lines[i]; if(!line->hasFrontSector() || !line->hasBackSector()) continue; Sector *sec1 = line->frontSectorPtr(); Sector *sec2 = line->backSectorPtr(); if(!sec1 || !sec2 || sec1 == sec2) continue; // Already in the same group? if(secGroups[sec1->index] == secGroups[sec2->index]) continue; // Swap sectors so that the smallest group is added to the biggest // group. This is based on the assumption that sector numbers in // wads will generally increase over the set of lines, and so // (by swapping) we'll tend to add small groups into larger // groups, thereby minimising the updates to 'rej_group' fields // that is required when merging. Sector *p = 0; if(secGroups[sec1->index] > secGroups[sec2->index]) { p = sec1; sec1 = sec2; sec2 = p; } // Update the group numbers in the second group secGroups[sec2->index] = secGroups[sec1->index]; for(p = sec2->rejNext; p != sec2; p = p->rejNext) { secGroups[p->index] = secGroups[sec1->index]; } // Merge 'em baby... sec1->rejNext->rejPrev = sec2; sec2->rejNext->rejPrev = sec1; p = sec1->rejNext; sec1->rejNext = sec2->rejNext; sec2->rejNext = p; } size_t rejectSize = (map.sectorCount() * map.sectorCount() + 7) / 8; byte *matrix = Z_Calloc(rejectSize, PU_MAPSTATIC, 0); for(int view = 0; view < map.sectorCount(); ++view) for(int target = 0; target < view; ++target) { if(secGroups[view] == secGroups[target]) continue; // For symmetry, do two bits at a time. int p1 = view * map.sectorCount() + target; int p2 = target * map.sectorCount() + view; matrix[p1 >> 3] |= (1 << (p1 & 7)); matrix[p2 >> 3] |= (1 << (p2 & 7)); } M_Free(secGroups); return matrix; } #endif doomsday-stable-1.15.7/doomsday/client/src/world/bsp/0000775000175000017500000000000012641367670021773 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/src/world/bsp/linesegment.cpp0000664000175000017500000002677612641367670025033 0ustar jaakkojaakko/** @file linesegment.cpp BSP Builder Line Segment. * * Originally based on glBSP 2.24 (in turn, based on BSP 2.3) * @see http://sourceforge.net/projects/glbsp/ * * @authors Copyright © 2007-2014 Daniel Swanson * @authors Copyright © 2000-2007 Andrew Apted * @authors Copyright © 1998-2000 Colin Reed * @authors Copyright © 1998-2000 Lee Killough * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/bsp/linesegment.h" #include /// @todo remove me #include #include "world/bsp/convexsubspaceproxy.h" #include "world/bsp/superblockmap.h" #include "m_misc.h" // M_BoxOnLineSide2 namespace de { namespace bsp { DENG2_PIMPL_NOREF(LineSegment::Side) { /// Owning line segment. LineSegment *line = nullptr; /// Direction vector from -> to. Vector2d direction; /// Map Line side that "this" segment initially comes from or @c 0 signifying /// a partition line segment (not owned). LineSide *mapSide = nullptr; /// Map Line of the partition line which resulted in this segment due to /// splitting (not owned). Line *partitionMapLine = nullptr; /// Neighbor line segments relative to "this" segment along the source /// line (both partition and map lines). LineSegment::Side *rightNeighbor = nullptr; LineSegment::Side *leftNeighbor = nullptr; /// The superblock that contains "this" segment, or @c nullptr if the segment is /// no longer in any superblock (e.g., attributed to a convex subspace). LineSegmentBlockTreeNode *blockTreeNode = nullptr; /// Convex subspace which "this" segment is attributed (if any, not owned). ConvexSubspaceProxy *convexSubspace = nullptr; /// Map sector attributed to the line segment. @c nullptr= partition line. Sector *sector = nullptr; // Precomputed data for faster calculations. coord_t pLength = 0; coord_t pAngle = 0; coord_t pPara = 0; coord_t pPerp = 0; slopetype_t pSlopeType = ST_VERTICAL; /// Half-edge produced from this map line segment (if any, not owned). HEdge *hedge = nullptr; inline LineSegment::Side **neighborAdr(int edge) { return edge? &rightNeighbor : &leftNeighbor; } }; LineSegment::Side::Side(LineSegment &line) : d(new Instance) { d->line = &line; } void LineSegment::Side::updateCache() { d->direction = to().origin() - from().origin(); d->pLength = d->direction.length(); DENG2_ASSERT(d->pLength > 0); d->pAngle = M_DirectionToAngleXY(d->direction.x, d->direction.y); d->pSlopeType = M_SlopeTypeXY(d->direction.x, d->direction.y); d->pPerp = from().origin().y * d->direction.x - from().origin().x * d->direction.y; d->pPara = -from().origin().x * d->direction.x - from().origin().y * d->direction.y; } LineSegment &LineSegment::Side::line() const { return *d->line; } int LineSegment::Side::lineSideId() const { return &d->line->front() == this? LineSegment::Front : LineSegment::Back; } Vector2d const &LineSegment::Side::direction() const { return d->direction; } slopetype_t LineSegment::Side::slopeType() const { return d->pSlopeType; } coord_t LineSegment::Side::angle() const { return d->pAngle; } bool LineSegment::Side::hasHEdge() const { return d->hedge != nullptr; } HEdge &LineSegment::Side::hedge() const { if(d->hedge) { return *d->hedge; } /// @throw MissingHEdgeError Attempted with no half-edge associated. throw MissingHEdgeError("LineSegment::Side::hedge", "No half-edge is associated"); } void LineSegment::Side::setHEdge(HEdge *newHEdge) { d->hedge = newHEdge; } ConvexSubspaceProxy *LineSegment::Side::convexSubspace() const { return d->convexSubspace; } void LineSegment::Side::setConvexSubspace(ConvexSubspaceProxy *newConvexSubspace) { d->convexSubspace = newConvexSubspace; } bool LineSegment::Side::hasMapSide() const { return d->mapSide != nullptr; } LineSide &LineSegment::Side::mapSide() const { if(d->mapSide) { return *d->mapSide; } /// @throw MissingMapSideError Attempted with no map line side attributed. throw MissingMapSideError("LineSegment::Side::mapSide", "No map line side is attributed"); } void LineSegment::Side::setMapSide(LineSide *newMapSide) { d->mapSide = newMapSide; } Line *LineSegment::Side::partitionMapLine() const { return d->partitionMapLine; } void LineSegment::Side::setPartitionMapLine(Line *newMapLine) { d->partitionMapLine = newMapLine; } bool LineSegment::Side::hasNeighbor(int edge) const { return (*d->neighborAdr(edge)) != nullptr; } LineSegment::Side &LineSegment::Side::neighbor(int edge) const { LineSegment::Side **neighborAdr = d->neighborAdr(edge); if(*neighborAdr) { return **neighborAdr; } /// @throw MissingNeighborError Attempted with no relevant neighbor attributed. throw MissingNeighborError("LineSegment::Side::neighbor", QString("No %1 neighbor is attributed").arg(edge? "Right" : "Left")); } void LineSegment::Side::setNeighbor(int edge, LineSegment::Side *newNeighbor) { *d->neighborAdr(edge) = newNeighbor; } /*LineSegmentBlockTreeNode*/ void *LineSegment::Side::blockTreeNodePtr() const { return d->blockTreeNode; } void LineSegment::Side::setBlockTreeNode(/*LineSegmentBlockTreeNode*/ void *newNode) { d->blockTreeNode = (LineSegmentBlockTreeNode *)newNode; } bool LineSegment::Side::hasSector() const { return d->sector != nullptr; } Sector &LineSegment::Side::sector() const { if(d->sector) { return *d->sector; } /// @throw LineSegment::MissingSectorError Attempted with no sector attributed. throw LineSegment::MissingSectorError("LineSegment::Side::sector", "No sector is attributed"); } void LineSegment::Side::setSector(Sector *newSector) { d->sector = newSector; } coord_t LineSegment::Side::length() const { return d->pLength; } coord_t LineSegment::Side::distance(Vector2d point) const { coord_t const pointV1[2] = { point.x, point.y }; coord_t const directionV1[2] = { d->direction.x, d->direction.y }; return V2d_PointLineParaDistance(pointV1, directionV1, d->pPara, d->pLength); } void LineSegment::Side::distance(LineSegment::Side const &other, coord_t *fromDist, coord_t *toDist) const { // Any work to do? if(!fromDist && !toDist) return; /// @attention Ensure line segments produced from the partition's source /// line are always treated as collinear. This special case is only /// necessary due to precision inaccuracies when a line is split into /// multiple segments. if(d->partitionMapLine && d->partitionMapLine == other.partitionMapLine()) { if(fromDist) *fromDist = 0; if(toDist) *toDist = 0; return; } coord_t toSegDirectionV1[2] = { other.direction().x, other.direction().y } ; if(fromDist) { coord_t fromV1[2] = { from().origin().x, from().origin().y }; *fromDist = V2d_PointLinePerpDistance(fromV1, toSegDirectionV1, other.d->pPerp, other.d->pLength); } if(toDist) { coord_t toV1[2] = { to().origin().x, to().origin().y }; *toDist = V2d_PointLinePerpDistance(toV1, toSegDirectionV1, other.d->pPerp, other.d->pLength); } } LineRelationship lineRelationship(coord_t fromDist, coord_t toDist) { static coord_t const distEpsilon = LINESEGMENT_INCIDENT_DISTANCE_EPSILON; // Collinear with "this" line? if(de::abs(fromDist) <= distEpsilon && de::abs(toDist) <= distEpsilon) { return Collinear; } // To the right of "this" line?. if(fromDist > -distEpsilon && toDist > -distEpsilon) { // Close enough to intercept? if(fromDist < distEpsilon || toDist < distEpsilon) return RightIntercept; return Right; } // To the left of "this" line? if(fromDist < distEpsilon && toDist < distEpsilon) { // Close enough to intercept? if(fromDist > -distEpsilon || toDist > -distEpsilon) return LeftIntercept; return Left; } return Intersects; } LineRelationship LineSegment::Side::relationship(LineSegment::Side const &other, coord_t *retFromDist, coord_t *retToDist) const { coord_t fromDist, toDist; distance(other, &fromDist, &toDist); LineRelationship rel = lineRelationship(fromDist, toDist); if(retFromDist) *retFromDist = fromDist; if(retToDist) *retToDist = toDist; return rel; } int LineSegment::Side::boxOnSide(AABoxd const &box) const { coord_t const fromV1[2] = { from().origin().x, from().origin().y }; coord_t const directionV1[2] = { d->direction.x, d->direction.y } ; return M_BoxOnLineSide2(&box, fromV1, directionV1, d->pPerp, d->pLength, LINESEGMENT_INCIDENT_DISTANCE_EPSILON); } DENG2_PIMPL(LineSegment) , DENG2_OBSERVES(Vertex, OriginChange) { /// Vertexes of the line segment (not owned). Vertex *from; Vertex *to; /// Sides of the line segment (owned). LineSegment::Side front; LineSegment::Side back; Instance(Public *i, Vertex &from_, Vertex &to_) : Base (i) , from (&from_) , to (&to_) , front(*i) , back (*i) { from->audienceForOriginChange += this; to->audienceForOriginChange += this; } ~Instance() { from->audienceForOriginChange -= this; to->audienceForOriginChange -= this; } inline Vertex **vertexAdr(int edge) { return edge? &to : &from; } void replaceVertex(int edge, Vertex &newVertex) { Vertex **adr = vertexAdr(edge); if(*adr && *adr == &newVertex) return; if(*adr) (*adr)->audienceForOriginChange -= this; *adr = &newVertex; if(*adr) (*adr)->audienceForOriginChange += this; front.updateCache(); back.updateCache(); } void vertexOriginChanged(Vertex & /*vertex*/) { front.updateCache(); back.updateCache(); } }; LineSegment::LineSegment(Vertex &from, Vertex &to) : d(new Instance(this, from, to)) { d->front.updateCache(); d->back.updateCache(); } LineSegment::Side &LineSegment::side(int back) { return back? d->back : d->front; } LineSegment::Side const &LineSegment::side(int back) const { return back? d->back : d->front; } Vertex &LineSegment::vertex(int to) const { DENG2_ASSERT((to? d->to : d->from) != nullptr); return to? *d->to : *d->from; } AABoxd LineSegment::aaBox() const { AABoxd bounds; Vector2d min = d->from->origin().min(d->to->origin()); Vector2d max = d->from->origin().max(d->to->origin()); V2d_Set(bounds.min, min.x, min.y); V2d_Set(bounds.max, max.x, max.y); return bounds; } void LineSegment::replaceVertex(int to, Vertex &newVertex) { d->replaceVertex(to, newVertex); } } // namespace bsp } // namespace de doomsday-stable-1.15.7/doomsday/client/src/world/bsp/hplane.cpp0000664000175000017500000003017212641367670023751 0ustar jaakkojaakko/** @file hplane.cpp World map BSP builder half-plane. * * Originally based on glBSP 2.24 (in turn, based on BSP 2.3) * @see http://sourceforge.net/projects/glbsp/ * * @authors Copyright © 2007-2014 Daniel Swanson * @authors Copyright © 2000-2007 Andrew Apted * @authors Copyright © 1998-2000 Colin Reed * @authors Copyright © 1998-2000 Lee Killough * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/bsp/hplane.h" #include #include #include // M_InverseAngle #include // remove me #include #include #include "Line" #include "Sector" #include "Vertex" #include "world/bsp/edgetip.h" #include "world/bsp/linesegment.h" #include "world/bsp/partitioner.h" namespace de { namespace bsp { HPlane::Intercept::Intercept(ddouble distance, LineSegmentSide &lineSeg, int edge) : _before (nullptr) , _after (nullptr) , _distance(distance) , _lineSeg (&lineSeg) , _edge (edge) {} LineSegmentSide &HPlane::Intercept::lineSegment() const { DENG2_ASSERT(_lineSeg); return *_lineSeg; } int HPlane::Intercept::lineSegmentEdge() const { return _edge; } Sector *HPlane::Intercept::before() const { return _before? _before->sectorPtr() : nullptr; } Sector *HPlane::Intercept::after() const { return _after? _after->sectorPtr() : nullptr; } LineSegmentSide *HPlane::Intercept::beforeLineSegment() const { return _before; } LineSegmentSide *HPlane::Intercept::afterLineSegment() const { return _after; } #ifdef DENG2_DEBUG void HPlane::Intercept::debugPrint() const { LOGDEV_MAP_MSG("Vertex #%i %s beforeSector: #%d afterSector: #%d %s") << vertex().indexInMap() << vertex().origin().asText() << (_before && _before->hasSector()? _before->sector().indexInArchive() : -1) << (_after && _after->hasSector() ? _after-> sector().indexInArchive() : -1); } #endif DENG2_PIMPL(HPlane) { Partition partition; ///< The partition line. coord_t length; ///< Direction vector length. coord_t angle; ///< Cartesian world angle. slopetype_t slopeType; ///< Logical world angle classification. coord_t perp; ///< Perpendicular scale factor. coord_t para; ///< Parallel scale factor. LineSegmentSide *lineSegment; ///< Source of the partition (if any, not owned). Intercepts intercepts; ///< Points along the half-plane. bool needSortIntercepts; ///< @c true= @var intercepts requires sorting. Instance(Public *i, Partition const &partition) : Base(i) , partition (partition) , length (partition.direction.length()) , angle (M_DirectionToAngleXY(partition.direction.x, partition.direction.y)) , slopeType (M_SlopeTypeXY(partition.direction.x, partition.direction.y)) , perp ( partition.origin.y * partition.direction.x - partition.origin.x * partition.direction.y) , para (-partition.origin.x * partition.direction.x - partition.origin.y * partition.direction.y) , lineSegment(nullptr) , needSortIntercepts(false) {} /** * Find an intercept by @a vertex. */ Intercept *interceptByVertex(Vertex const &vertex) { for(Intercept const &icpt : intercepts) { if(&icpt.vertex() == &vertex) return const_cast(&icpt); } return 0; } /** * Merges @a next into @a cur. */ static void mergeIntercepts(HPlane::Intercept &cur, HPlane::Intercept const &next) { /* LOG_AS("HPlane::mergeIntercepts"); cur.debugPrint(); next.debugPrint(); */ if(&cur.lineSegment().line() == &next.lineSegment().line()) return; if(cur.lineSegmentIsSelfReferencing() && !next.lineSegmentIsSelfReferencing()) { if(cur.before() && next.before()) cur._before = next._before; if(cur.after() && next.after()) cur._after = next._after; } if(!cur.before() && next.before()) cur._before = next._before; if(!cur.after() && next.after()) cur._after = next._after; /* LOG_TRACE("Result:"); cur.debugPrint(); */ } }; HPlane::HPlane(Partition const &partition) : d(new Instance(this, partition)) {} void HPlane::clearIntercepts() { d->intercepts.clear(); // An empty intercept list is logically sorted. d->needSortIntercepts = false; } void HPlane::configure(LineSegmentSide const &newBaseSeg) { // Only map line segments are suitable. DENG2_ASSERT(newBaseSeg.hasMapSide()); LOG_AS("HPlane::configure"); // Clear the list of intersection points. clearIntercepts(); // Reconfigure the partition line. LineSide &mapSide = newBaseSeg.mapSide(); d->partition.direction = mapSide.to().origin() - mapSide.from().origin(); d->partition.origin = mapSide.from().origin(); d->lineSegment = const_cast(&newBaseSeg); d->length = d->partition.direction.length(); d->angle = M_DirectionToAngleXY(d->partition.direction.x, d->partition.direction.y); d->slopeType = M_SlopeTypeXY(d->partition.direction.x, d->partition.direction.y); d->perp = d->partition.origin.y * d->partition.direction.x - d->partition.origin.x * d->partition.direction.y; d->para = -d->partition.origin.x * d->partition.direction.x - d->partition.origin.y * d->partition.direction.y; //LOG_DEBUG("line segment %p %s") // << &newBaseSeg << d->partition.asText(); } /** * Determines whether a conceptual line oriented at @a vtx and "pointing" * at the specified world @a angle enters an "open" sector (which is to say * that said line does not enter void space and does not intercept with any * existing map or partition line segment in the plane, thus "closed"). * * @return The "open" sector at this angle; otherwise @c 0 (closed). */ static LineSegmentSide *lineSegAtAngle(EdgeTips const &tips, coord_t angle) { // Is there a tip exactly at this angle? if(tips.at(angle)) return nullptr; // Closed. // Find the first tip after (larger) than this angle. If present the side // we're interested in is the front. if(EdgeTip const *tip = tips.after(angle)) { return tip->hasFront()? &tip->front() : nullptr; } // The open sector must therefore be on the back of the tip with the largest // angle (if present). if(EdgeTip const *tip = tips.largest()) { return tip->hasBack()? &tip->back() : nullptr; } return nullptr; // No edge tips. } double HPlane::intersect(LineSegmentSide const &lineSeg, int edge) { Vertex &vertex = lineSeg.vertex(edge); coord_t pointV1[2] = { vertex.origin().x, vertex.origin().y }; coord_t directionV1[2] = { d->partition.direction.x, d->partition.direction.y }; return V2d_PointLineParaDistance(pointV1, directionV1, d->para, d->length); } HPlane::Intercept *HPlane::intercept(LineSegmentSide const &lineSeg, int edge, EdgeTips const &edgeTips) { bool const selfRef = (lineSeg.hasMapSide() && lineSeg.mapLine().isSelfReferencing()); // Already present for this vertex? Intercept *icpt; if((icpt = d->interceptByVertex(lineSeg.vertex(edge)))) { // If the new intercept line is not self-referencing we'll replace it. if(!(icpt->lineSegmentIsSelfReferencing() && !selfRef)) { return icpt; } } else { d->intercepts.append( Intercept(intersect(lineSeg, edge), const_cast(lineSeg), edge)); icpt = &d->intercepts.last(); // The addition of a new intercept means we'll need to resort. d->needSortIntercepts = true; } icpt->_lineSeg = const_cast(&lineSeg); icpt->_edge = edge; icpt->_before = lineSegAtAngle(edgeTips, inverseAngle()); icpt->_after = lineSegAtAngle(edgeTips, angle()); return icpt; } void HPlane::sortAndMergeIntercepts() { // Any work to do? if(!d->needSortIntercepts) return; qSort(d->intercepts.begin(), d->intercepts.end()); for(int i = 0; i < d->intercepts.count() - 1; ++i) { Intercept &cur = d->intercepts[i]; Intercept &next = d->intercepts[i+1]; // Sanity check. ddouble distance = next.distance() - cur.distance(); if(distance < -0.1) { throw Error("HPlane::sortAndMergeIntercepts", String("Invalid intercept order - %1 > %2") .arg(cur.distance(), 0, 'f', 3) .arg(next.distance(), 0, 'f', 3)); } // Are we merging this pair? if(distance <= HPLANE_INTERCEPT_MERGE_DISTANCE_EPSILON) { // Yes - merge the "next" intercept into "cur". d->mergeIntercepts(cur, next); // Destroy the "next" intercept. d->intercepts.removeAt(i + 1); // Process the new "cur" and "next" pairing. i -= 1; } } d->needSortIntercepts = false; } Partition const &HPlane::partition() const { return d->partition; } coord_t HPlane::angle() const { return d->angle; } coord_t HPlane::inverseAngle() const { return M_InverseAngle(angle()); } slopetype_t HPlane::slopeType() const { return d->slopeType; } LineSegmentSide *HPlane::lineSegment() const { return d->lineSegment; } void HPlane::distance(LineSegmentSide const &lineSeg, coord_t *fromDist, coord_t *toDist) const { // Any work to do? if(!fromDist && !toDist) return; /// @attention Ensure line segments produced from the partition's source /// line are always treated as collinear. This special case is only /// necessary due to precision inaccuracies when a line is split into /// multiple segments. if(d->lineSegment && &d->lineSegment->mapSide().line() == lineSeg.partitionMapLine()) { if(fromDist) *fromDist = 0; if(toDist) *toDist = 0; return; } coord_t toSegDirectionV1[2] = { d->partition.direction.x, d->partition.direction.y } ; if(fromDist) { coord_t fromV1[2] = { lineSeg.from().origin().x, lineSeg.from().origin().y }; *fromDist = V2d_PointLinePerpDistance(fromV1, toSegDirectionV1, d->perp, d->length); } if(toDist) { coord_t toV1[2] = { lineSeg.to().origin().x, lineSeg.to().origin().y }; *toDist = V2d_PointLinePerpDistance(toV1, toSegDirectionV1, d->perp, d->length); } } LineRelationship HPlane::relationship(LineSegmentSide const &lineSeg, coord_t *retFromDist, coord_t *retToDist) const { coord_t fromDist, toDist; distance(lineSeg, &fromDist, &toDist); LineRelationship rel = lineRelationship(fromDist, toDist); if(retFromDist) *retFromDist = fromDist; if(retToDist) *retToDist = toDist; return rel; } HPlane::Intercepts const &HPlane::intercepts() const { return d->intercepts; } #ifdef DENG2_DEBUG void HPlane::printIntercepts() const { uint index = 0; for(Intercept const &icpt : d->intercepts) { LOG_DEBUG(" %u: >%1.2f") << (index++) << icpt.distance(); icpt.debugPrint(); } } #endif } // namespace bsp } // namespace de doomsday-stable-1.15.7/doomsday/client/src/world/bsp/superblockmap.cpp0000664000175000017500000000524212641367670025351 0ustar jaakkojaakko/** @file superblockmap.cpp BSP line segment blockmap block. * * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * @authors Copyright © 2000-2007 Andrew Apted * @authors Copyright © 1998-2000 Colin Reed * @authors Copyright © 1998-2000 Lee Killough * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/bsp/superblockmap.h" #include using namespace de; using namespace de::bsp; DENG2_PIMPL_NOREF(LineSegmentBlock) { AABox bounds; ///< Block bounds at the node. All segments; ///< Line segments contained by the node (not owned). int mapCount = 0; ///< Running total of map-line segments at/under this node. int partCount = 0; ///< Running total of partition-line segments at/under this node. }; LineSegmentBlock::LineSegmentBlock(AABox const &bounds) : d(new Instance) { d->bounds = bounds; } AABox const &LineSegmentBlock::bounds() const { return d->bounds; } void LineSegmentBlock::link(LineSegmentSide &seg) { d->segments.prepend(&seg); } void LineSegmentBlock::addRef(LineSegmentSide const &seg) { if(seg.hasMapSide()) d->mapCount++; else d->partCount++; } void LineSegmentBlock::decRef(LineSegmentSide const &seg) { if(seg.hasMapSide()) d->mapCount--; else d->partCount--; } LineSegmentSide *LineSegmentBlock::pop() { if(!d->segments.isEmpty()) { LineSegmentSide *seg = d->segments.takeFirst(); decRef(*seg); return seg; } return 0; } int LineSegmentBlock::mapCount() const { return d->mapCount; } int LineSegmentBlock::partCount() const { return d->partCount; } int LineSegmentBlock::totalCount() const { return d->mapCount + d->partCount; } LineSegmentBlock::All const &LineSegmentBlock::all() const { return d->segments; } doomsday-stable-1.15.7/doomsday/client/src/world/bsp/partitioner.cpp0000664000175000017500000012405112641367670025042 0ustar jaakkojaakko/** @file partitioner.cpp World map, binary space partitioner. * * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * @authors Copyright © 2000-2007 Andrew Apted * @authors Copyright © 1998-2000 Colin Reed * @authors Copyright © 1998-2000 Lee Killough * @authors Copyright © 1997-1998 Raphael.Quinet * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/bsp/partitioner.h" #include #include #include #include #include #include #include "BspLeaf" #include "BspNode" #include "Line" #include "Sector" #include "Vertex" #include "world/bsp/convexsubspaceproxy.h" #include "world/bsp/edgetip.h" #include "world/bsp/hplane.h" #include "world/bsp/linesegment.h" #include "world/bsp/partitionevaluator.h" #include "world/bsp/superblockmap.h" namespace de { using namespace bsp; typedef QList Lines; typedef QList LineSegments; typedef QList LineSegmentSides; typedef QList SubspaceProxys; typedef QHash EdgeTipSetMap; DENG2_PIMPL(Partitioner) { int splitCostFactor = 7; ///< Cost of splitting a line segment. Lines lines; ///< Set of map lines to build from (in index order, not owned). Mesh *mesh = nullptr; ///< Provider of map geometries (cf. Factory). int segmentCount = 0; ///< Running total of segments built. int vertexCount = 0; ///< Running total of vertexes built. LineSegments lineSegments; ///< Line segments in the plane. SubspaceProxys subspaces; ///< Proxy subspaces in the plane. EdgeTipSetMap edgeTipSets; ///< One set for each vertex. BspTree *bspRoot = nullptr; ///< The BSP tree under construction. HPlane hplane; ///< Current space half-plane (partitioner state). struct LineSegmentBlockTree { LineSegmentBlockTreeNode *rootNode; /// @param bounds Map space bounding box for the root block. LineSegmentBlockTree(AABox const &bounds) : rootNode(new LineSegmentBlockTreeNode(new LineSegmentBlock(bounds))) {} ~LineSegmentBlockTree() { clear(); } /// Implicit conversion from LineSegmentBlockTree to root tree node. inline operator LineSegmentBlockTreeNode /*const*/ &() { return *rootNode; } private: static int clearUserDataWorker(LineSegmentBlockTreeNode &subtree, void *) { delete subtree.userData(); subtree.setUserData(nullptr); return 0; // Continue iteration. } void clear() { if(!rootNode) return; rootNode->traversePostOrder(clearUserDataWorker); delete rootNode; rootNode = nullptr; } }; Instance(Public *i) : Base(i) {} ~Instance() { clear(); } static int clearBspElementWorker(BspTree &subtree, void *) { delete subtree.userData(); subtree.setUserData(nullptr); return 0; // Continue iteration. } void clearBspTree() { if(!bspRoot) return; bspRoot->traversePostOrder(clearBspElementWorker); delete bspRoot; bspRoot = nullptr; } void clear() { //clearBspTree(); lines.clear(); mesh = nullptr; qDeleteAll(lineSegments); lineSegments.clear(); subspaces.clear(); edgeTipSets.clear(); hplane.clearIntercepts(); segmentCount = vertexCount = 0; } /** * Returns a newly allocated Vertex at the given map space @a origin from the * map geometry mesh (ownership is @em not given to the caller). */ Vertex *makeVertex(Vector2d const &origin) { Vertex *vtx = mesh->newVertex(origin); vertexCount += 1; // We built another one. return vtx; } /** * @return The new line segment (front is from @a start to @a end). */ LineSegment *makeLineSegment(Vertex &start, Vertex &end, Sector *frontSec, Sector *backSec, LineSide *frontSide, Line *partitionLine = nullptr) { LineSegment *newSeg = new LineSegment(start, end); lineSegments << newSeg; LineSegmentSide &front = newSeg->front(); front.setMapSide(frontSide); front.setPartitionMapLine(partitionLine); front.setSector(frontSec); LineSegmentSide &back = newSeg->back(); back.setMapSide(frontSide? &frontSide->back() : nullptr); back.setPartitionMapLine(partitionLine); back.setSector(backSec); return newSeg; } /** * Link @a seg into the line segment block tree. * * Performs k-d tree subdivision of the 2D coordinate space, splitting the node * tree as necessary, however new nodes are created only when they need to be * populated (i.e., a split does not generate two nodes at the same time). */ void linkLineSegmentInBlockTree(LineSegmentBlockTreeNode &node_, LineSegmentSide &seg) { // Traverse the node tree beginning at "this" node. for(LineSegmentBlockTreeNode *node = &node_; ;) { LineSegmentBlock &block = *node->userData(); AABox const &bounds = block.bounds(); // The segment "touches" this node; increment the ref counters. block.addRef(seg); // Determine whether further subdivision is necessary/possible. Vector2i dimensions(Vector2i(bounds.max) - Vector2i(bounds.min)); if(dimensions.x <= 256 && dimensions.y <= 256) { // Thats as small as we go; link it in and return. block.link(seg); seg.setBlockTreeNode(node); return; } // Determine whether the node should be split and on which axis. int const splitAxis = (dimensions.x < dimensions.y); // x=0, y=1 int const midOnAxis = (bounds.min[splitAxis] + bounds.max[splitAxis]) / 2; LineSegmentBlockTreeNode::ChildId fromSide = LineSegmentBlockTreeNode::ChildId(seg.from().origin()[splitAxis] >= midOnAxis); LineSegmentBlockTreeNode::ChildId toSide = LineSegmentBlockTreeNode::ChildId(seg.to ().origin()[splitAxis] >= midOnAxis); // Does the segment lie entirely within one half of this node? if(fromSide != toSide) { // No, the segment crosses @var midOnAxis; link it in and return. block.link(seg); seg.setBlockTreeNode(node); return; } // Do we need to create the child node? if(!node->hasChild(fromSide)) { bool const toLeft = (fromSide == LineSegmentBlockTreeNode::Left); AABox childBounds; if(splitAxis) { // Vertical split. int division = bounds.minY + 0.5 + (bounds.maxY - bounds.minY) / 2; childBounds.minX = bounds.minX; childBounds.minY = (toLeft? division : bounds.minY); childBounds.maxX = bounds.maxX; childBounds.maxY = (toLeft? bounds.maxY : division); } else { // Horizontal split. int division = bounds.minX + 0.5 + (bounds.maxX - bounds.minX) / 2; childBounds.minX = (toLeft? division : bounds.minX); childBounds.minY = bounds.minY; childBounds.maxX = (toLeft? bounds.maxX : division); childBounds.maxY = bounds.maxY; } // Add a new child node and link it to its parent. LineSegmentBlock *childBlock = new LineSegmentBlock(childBounds); node->setChild(fromSide, new LineSegmentBlockTreeNode(childBlock, node)); } // Descend to the child node. node = node->childPtr(fromSide); } } /** * Returns the EdgeTips set associated with @a vertex. */ EdgeTips &edgeTipSet(Vertex const &vertex) { EdgeTipSetMap::iterator found = edgeTipSets.find(const_cast(&vertex)); if(found == edgeTipSets.end()) { // Time to construct a new set. found = edgeTipSets.insert(const_cast(&vertex), EdgeTips()); } return found.value(); } /** * Create all initial line segments. We can be certain there are no zero-length * lines as these are screened earlier. */ void createInitialLineSegments(LineSegmentBlockTreeNode &rootNode) { for(Line *line : lines) { Sector *frontSec = line->frontSectorPtr(); Sector *backSec = line->backSectorPtr(); // Handle the "one-way window" effect. if(!backSec && line->_bspWindowSector) { backSec = line->_bspWindowSector; } LineSegment *seg = makeLineSegment(line->from(), line->to(), frontSec, backSec, &line->front()); if(seg->front().hasSector()) { linkLineSegmentInBlockTree(rootNode, seg->front()); } if(seg->back().hasSector()) { linkLineSegmentInBlockTree(rootNode, seg->back()); } edgeTipSet(line->from()) << EdgeTip(seg->front()); edgeTipSet(line->to()) << EdgeTip(seg->back()); } } /** * Splits the given line segment at the point (x,y). The new line segment * is returned. The old line segment is shortened (the original start vertex * is unchanged), the new line segment becomes the cut-off tail (keeping * the original end vertex). * * @note If the line segment has a twin it is also split. */ LineSegmentSide &splitLineSegment(LineSegmentSide &frontLeft, Vector2d const &point, bool updateEdgeTips = true) { DENG2_ASSERT(point != frontLeft.from().origin() && point != frontLeft.to().origin()); //LOG_DEBUG("Splitting line segment %p at %s") // << &frontLeft << point.asText(); Vertex *newVert = makeVertex(point); LineSegment &oldSeg = frontLeft.line(); LineSegment &newSeg = *makeLineSegment(oldSeg.from(), oldSeg.to(), oldSeg.front().sectorPtr(), oldSeg.back().sectorPtr(), oldSeg.front().mapSidePtr(), oldSeg.front().partitionMapLine()); // Perform the split, updating vertex and relative segment links. LineSegmentSide &frontRight = newSeg.side(frontLeft.lineSideId()); oldSeg.replaceVertex(frontLeft.lineSideId() ^ LineSegment::To, *newVert); newSeg.replaceVertex(frontLeft.lineSideId(), *newVert); LineSegmentSide &backRight = frontLeft.back(); LineSegmentSide &backLeft = frontRight.back(); if(ConvexSubspaceProxy *convexSet = frontLeft.convexSubspace()) { *convexSet << frontRight; frontRight.setConvexSubspace(convexSet); } frontLeft.setRight(&frontRight); frontRight.setLeft(&frontLeft); // Handle the twin. if(ConvexSubspaceProxy *convexSet = backRight.convexSubspace()) { *convexSet << backLeft; backLeft.setConvexSubspace(convexSet); } backLeft.setRight(&backRight); backRight.setLeft(&backLeft); if(updateEdgeTips) { /// @todo Optimize: Avoid clearing tips by implementing update logic. edgeTipSet(oldSeg.from()).clearByLineSegment(oldSeg); edgeTipSet(oldSeg.to() ).clearByLineSegment(oldSeg); edgeTipSet(newSeg.from()).clearByLineSegment(newSeg); edgeTipSet(newSeg.to() ).clearByLineSegment(newSeg); edgeTipSet(oldSeg.from()) << EdgeTip(oldSeg.front()); edgeTipSet(oldSeg.to()) << EdgeTip(oldSeg.back()); edgeTipSet(newSeg.from()) << EdgeTip(newSeg.front()); edgeTipSet(newSeg.to()) << EdgeTip(newSeg.back()); } return frontRight; } /** * Find the intersection point between a line segment and the current * partition plane. Takes advantage of some common situations like * horizontal and vertical lines to choose a 'nicer' intersection point. */ Vector2d intersectPartition(LineSegmentSide const &seg, coord_t fromDist, coord_t toDist) const { // Horizontal partition vs vertical line segment. if(hplane.slopeType() == ST_HORIZONTAL && seg.slopeType() == ST_VERTICAL) { return Vector2d(seg.from().origin().x, hplane.partition().origin.y); } // Vertical partition vs horizontal line segment. if(hplane.slopeType() == ST_VERTICAL && seg.slopeType() == ST_HORIZONTAL) { return Vector2d(hplane.partition().origin.x, seg.from().origin().y); } // 0 = start, 1 = end. coord_t ds = fromDist / (fromDist - toDist); Vector2d point = seg.from().origin(); if(seg.slopeType() != ST_VERTICAL) point.x += seg.direction().x * ds; if(seg.slopeType() != ST_HORIZONTAL) point.y += seg.direction().y * ds; return point; } /// @todo refactor away inline void interceptPartition(LineSegmentSide &seg, int edge) { hplane.intercept(seg, edge, edgeTipSet(seg.vertex(edge))); } /** * Take the given line segment @a lineSeg, compare it with the partition * plane and determine into which of the two sets it should be. If the * line segment is found to intersect the partition, the intercept point * is determined and the line segment then split in two at this point. * Each piece of the line segment is then added to the relevant set. * * If the line segment is collinear with, or intersects the partition then * a new intercept is added to the partitioning half-plane. * * @note Any existing @em twin of @a lineSeg is so too handled uniformly. * * @param seg Line segment to be "partitioned". * @param rights Set of line segments on the right side of the partition. * @param lefts Set of line segments on the left side of the partition. */ void divideOneSegment(LineSegmentSide &seg, LineSegmentBlockTreeNode &rights, LineSegmentBlockTreeNode &lefts) { coord_t fromDist, toDist; LineRelationship rel = hplane.relationship(seg, &fromDist, &toDist); switch(rel) { case Collinear: { interceptPartition(seg, LineSegment::From); interceptPartition(seg, LineSegment::To); // Direction (vs that of the partition plane) determines in which // subset this line segment belongs. if(seg.direction().dot(hplane.partition().direction) < 0) { linkLineSegmentInBlockTree(lefts, seg); } else { linkLineSegmentInBlockTree(rights, seg); } break; } case Right: case RightIntercept: if(rel == RightIntercept) { // Direction determines which edge of the line segment interfaces // with the new half-plane intercept. interceptPartition(seg, (fromDist < DIST_EPSILON? LineSegment::From : LineSegment::To)); } linkLineSegmentInBlockTree(rights, seg); break; case Left: case LeftIntercept: if(rel == LeftIntercept) { interceptPartition(seg, (fromDist > -DIST_EPSILON? LineSegment::From : LineSegment::To)); } linkLineSegmentInBlockTree(lefts, seg); break; case Intersects: { // Calculate the intersection point and split this line segment. Vector2d point = intersectPartition(seg, fromDist, toDist); LineSegmentSide &newFrontRight = splitLineSegment(seg, point); // Ensure the new back left segment is inserted into the same block as // the old back right segment. if(LineSegmentBlockTreeNode *backLeftBlock = (LineSegmentBlockTreeNode *)seg.back().blockTreeNodePtr()) { linkLineSegmentInBlockTree(*backLeftBlock, newFrontRight.back()); } interceptPartition(seg, LineSegment::To); // Direction determines which subset the line segments are added to. if(fromDist < 0) { linkLineSegmentInBlockTree(rights, newFrontRight); linkLineSegmentInBlockTree(lefts, seg); } else { linkLineSegmentInBlockTree(rights, seg); linkLineSegmentInBlockTree(lefts, newFrontRight); } break; } } } /** * Remove all the line segments from the list, partitioning them into the * left or right sets according to their position relative to partition line. * Adds any intersections onto the intersection list as it goes. * * @param node Block tree node containing the line segments to be partitioned. * @param rights Set of line segments on the right side of the partition. * @param lefts Set of line segments on the left side of the partition. */ void divideSegments(LineSegmentBlockTreeNode &node, LineSegmentBlockTreeNode &rights, LineSegmentBlockTreeNode &lefts) { /** * @todo Revise this algorithm so that @var segments is not modified * during the partitioning process. */ int const totalSegs = node.userData()->totalCount(); DENG2_ASSERT(totalSegs != 0); DENG2_UNUSED(totalSegs); // Iterative pre-order traversal of SuperBlock. LineSegmentBlockTreeNode *cur = &node; LineSegmentBlockTreeNode *prev = nullptr; while(cur) { while(cur) { LineSegmentBlock &segs = *cur->userData(); LineSegmentSide *seg; while((seg = segs.pop())) { // Disassociate the line segment from the block tree. seg->setBlockTreeNode(nullptr); divideOneSegment(*seg, rights, lefts); } if(prev == cur->parentPtr()) { // Descending - right first, then left. prev = cur; if(cur->hasRight()) cur = cur->rightPtr(); else cur = cur->leftPtr(); } else if(prev == cur->rightPtr()) { // Last moved up the right branch - descend the left. prev = cur; cur = cur->leftPtr(); } else if(prev == cur->leftPtr()) { // Last moved up the left branch - continue upward. prev = cur; cur = cur->parentPtr(); } } if(prev) { // No left child - back up. cur = prev->parentPtr(); } } // Sanity checks... DENG2_ASSERT(rights.userData()->totalCount()); DENG2_ASSERT(lefts.userData ()->totalCount()); DENG2_ASSERT(( rights.userData()->totalCount() + lefts.userData ()->totalCount()) >= totalSegs); } /** * Analyze the half-plane intercepts, building new line segments to cap * any gaps (new segments are added onto the end of the appropriate list * (rights to @a rightList and lefts to @a leftList)). * * @param rights Set of line segments on the right of the partition. * @param lefts Set of line segments on the left of the partition. */ void addPartitionLineSegments(LineSegmentBlockTreeNode &rights, LineSegmentBlockTreeNode &lefts) { LOG_TRACE("Building line segments along partition %s") << hplane.partition().asText(); // First, fix any near-distance issues with the intercepts. hplane.sortAndMergeIntercepts(); //hplane.printIntercepts(); // We must not create new line segments on top of the source partition // line segment (as this will result in duplicate edges finding their // way into the BSP leaf geometries). LineSegmentSide *partSeg = hplane.lineSegment(); double nearDist = 0, farDist = 0; if(partSeg) { nearDist = hplane.intersect(*partSeg, LineSegment::From); farDist = hplane.intersect(*partSeg, LineSegment::To); } // Create new line segments. for(int i = 0; i < hplane.interceptCount() - 1; ++i) { HPlaneIntercept const &cur = hplane.intercepts()[i]; HPlaneIntercept const &next = hplane.intercepts()[i+1]; // Does this range overlap the partition line segment? if(partSeg && cur.distance() >= nearDist && next.distance() <= farDist) continue; // Void space or an existing segment between the two intercepts? if(!cur.after() && !next.before()) continue; // Check for some nasty open/closed or close/open cases. if(cur.after() && !next.before()) { if(!cur.lineSegmentIsSelfReferencing()) { Vector2d nearPoint = (cur.vertex().origin() + next.vertex().origin()) / 2; notifyUnclosedSectorFound(*cur.after(), nearPoint); } continue; } if(!cur.after() && next.before()) { if(!next.lineSegmentIsSelfReferencing()) { Vector2d nearPoint = (cur.vertex().origin() + next.vertex().origin()) / 2; notifyUnclosedSectorFound(*next.before(), nearPoint); } continue; } /* * This is definitely open space. */ Vertex &fromVertex = cur.vertex(); Vertex &toVertex = next.vertex(); Sector *sector = cur.after(); if(!cur.before() && next.before() == next.after()) { sector = next.before(); } // Choose the non-self-referencing sector when we can. else if(cur.after() != next.before()) { if(!cur.lineSegmentIsSelfReferencing() && !next.lineSegmentIsSelfReferencing()) { LOG_DEBUG("Sector mismatch #%d %s != #%d %s") << cur.after()->indexInMap() << cur.vertex().origin().asText() << next.before()->indexInMap() << next.vertex().origin().asText(); } LineSegmentSide *afterSeg = cur.afterLineSegment(); if(afterSeg->hasMapLine() && afterSeg->mapLine().isSelfReferencing()) { LineSegmentSide *beforeSeg = next.beforeLineSegment(); if(beforeSeg->hasMapLine() && !beforeSeg->mapLine().isSelfReferencing()) { sector = next.before(); } } } DENG2_ASSERT(sector); LineSegment &newSeg = *makeLineSegment(fromVertex, toVertex, sector, sector, nullptr /*no map line*/, partSeg? &partSeg->mapLine() : nullptr); edgeTipSet(newSeg.from()) << EdgeTip(newSeg.front()); edgeTipSet(newSeg.to()) << EdgeTip(newSeg.back()); // Add each new line segment to the appropriate set. linkLineSegmentInBlockTree(rights, newSeg.front()); linkLineSegmentInBlockTree(lefts, newSeg.back()); /* LOG_DEBUG("Built line segment from %s to %s (sector #%i)") << fromVertex.origin().asText() << toVertex.origin().asText() << sector->indexInArchive(); */ } } /** * Collate (unlink) all line segments at or beneath @a node to a new list. */ static LineSegmentSides collectAllSegments(LineSegmentBlockTreeNode &node) { LineSegmentSides allSegs; #ifdef DENG2_QT_4_7_OR_NEWER allSegs.reserve(node.userData()->totalCount()); #endif // Iterative pre-order traversal. LineSegmentBlockTreeNode *cur = &node; LineSegmentBlockTreeNode *prev = nullptr; while(cur) { while(cur) { LineSegmentBlock &segs = *cur->userData(); LineSegmentSide *seg; while((seg = segs.pop())) { allSegs << seg; } if(prev == cur->parentPtr()) { // Descending - right first, then left. prev = cur; if(cur->hasRight()) cur = cur->rightPtr(); else cur = cur->leftPtr(); } else if(prev == cur->rightPtr()) { // Last moved up the right branch - descend the left. prev = cur; cur = cur->leftPtr(); } else if(prev == cur->leftPtr()) { // Last moved up the left branch - continue upward. prev = cur; cur = cur->parentPtr(); } } if(prev) { // No left child - back up. cur = prev->parentPtr(); } } return allSegs; } /** * Determine the axis-aligned bounding box containing the vertex coordinates * from all segments. */ static AABoxd segmentBounds(LineSegmentSides const &allSegments) { bool initialized = false; AABoxd bounds; for(LineSegmentSide *seg : allSegments) { if(initialized) { V2d_UniteBox(bounds.arvec2, seg->aaBox().arvec2); } else { V2d_CopyBox(bounds.arvec2, seg->aaBox().arvec2); initialized = true; } } return bounds; } /** * Determine the axis-aligned bounding box containing the vertices of all * line segments at or beneath @a node in the block tree. * * @return Determined AABox (might be empty (i.e., min > max) if no segments). */ static AABoxd segmentBounds(LineSegmentBlockTreeNode const &node) { bool initialized = false; AABoxd bounds; // Iterative pre-order traversal. LineSegmentBlockTreeNode const *cur = &node; LineSegmentBlockTreeNode const *prev = nullptr; while(cur) { while(cur) { LineSegmentBlock const &block = *cur->userData(); if(block.totalCount()) { AABoxd boundsAtNode = segmentBounds(block.all()); if(initialized) { V2d_AddToBox(bounds.arvec2, boundsAtNode.min); } else { V2d_InitBox(bounds.arvec2, boundsAtNode.min); initialized = true; } V2d_AddToBox(bounds.arvec2, boundsAtNode.max); } if(prev == cur->parentPtr()) { // Descending - right first, then left. prev = cur; if(cur->hasRight()) cur = cur->rightPtr(); else cur = cur->leftPtr(); } else if(prev == cur->rightPtr()) { // Last moved up the right branch - descend the left. prev = cur; cur = cur->leftPtr(); } else if(prev == cur->leftPtr()) { // Last moved up the left branch - continue upward. prev = cur; cur = cur->parentPtr(); } } if(prev) { // No left child - back up. cur = prev->parentPtr(); } } return bounds; } LineSegmentSide *choosePartition(LineSegmentBlockTreeNode &candidateSet) { return PartitionEvaluator(splitCostFactor).choose(candidateSet); } /** * Takes the line segment list and determines if it is convex, possibly * converting it into a BSP leaf. Otherwise, the list is divided into two * halves and recursion will continue on the new sub list. * * This is done by scanning all of the line segments and finding the one * that does the least splitting and has the least difference in numbers * of line segments on either side (why is this valued? -ds). * * If the line segments on the left side are convex create another leaf * else put the line segments into the left list. * * If the line segments on the right side are convex create another leaf * else put the line segments into the right list. * * @param node Tree node for the block containing the line segments to * be partitioned. * * @return Newly created BSP subtree; otherwise @c nullptr (degenerate). */ BspTree *partitionSpace(LineSegmentBlockTreeNode &node) { LOG_AS("Partitioner::partitionSpace"); BspElement *bspElement = nullptr; ///< Built BSP map element at this node. BspTree *rightBspTree = nullptr; BspTree *leftBspTree = nullptr; // Pick a line segment to use as the next partition plane. if(LineSegmentSide *partSeg = choosePartition(node)) { // Reconfigure the half-plane for the next round of partitioning. hplane.configure(*partSeg); /* LOG_TRACE("%s, segment side %p %i (segment #%i) %s %s") << hplane.partition().asText() << partSeg << partSeg->lineSideId() << lineSegments.indexOf(&partSeg->line()) << partSeg->from().origin().asText() << partSeg->to().origin().asText(); */ // Take a copy of the current partition - we'll need this for any // BspNode we produce later. Partition partition(hplane.partition()); // Create left and right block trees. /// @todo There should be no need to use additional independent /// structures to contain these subsets. LineSegmentBlockTree rightTree(node.userData()->bounds()); LineSegmentBlockTree leftTree(node.userData()->bounds()); // Partition the line segements into two subsets according to their // spacial relationship with the half-plane (splitting any which // intersect). divideSegments(node, rightTree, leftTree); node.clear(); addPartitionLineSegments(rightTree, leftTree); // Take a copy of the geometry bounds for each child/sub space // - we'll need this for any BspNode we produce later. AABoxd rightBounds = segmentBounds(rightTree); AABoxd leftBounds = segmentBounds(leftTree); // Recurse on each suspace, first the right space then left. rightBspTree = partitionSpace(rightTree); leftBspTree = partitionSpace(leftTree); // Collapse degenerates upward. if(!rightBspTree || !leftBspTree) return rightBspTree? rightBspTree : leftBspTree; // Make a new BSP node. bspElement = new BspNode(partition, rightBounds, leftBounds); } else { // No partition required/possible -- already convex (or degenerate). LineSegmentSides segments = collectAllSegments(node); node.clear(); subspaces.append(ConvexSubspaceProxy()); ConvexSubspaceProxy &convexSet = subspaces.last(); convexSet.addSegments(segments); for(LineSegmentSide *seg : segments) { // Attribute the segment to the convex subspace. seg->setConvexSubspace(&convexSet); // Disassociate the segment from the block tree. seg->setBlockTreeNode(nullptr); } // Make a new BSP leaf. /// @todo Defer until necessary. BspLeaf *leaf = new BspLeaf; // Attribute the leaf to the convex subspace. convexSet.setBspLeaf(leaf); bspElement = leaf; } // Make a new BSP subtree and link up the children. BspTree *subtree = new BspTree(bspElement, nullptr/*no parent*/, rightBspTree, leftBspTree); if(rightBspTree) rightBspTree->setParent(subtree); if(leftBspTree) leftBspTree->setParent(subtree); return subtree; } /** * Split any overlapping line segments in the convex subspaces, creating new * line segments (and vertices) as required. A subspace may well include such * overlapping segments as if they do not break the convexity rule they won't * have been split during the partitioning process. * * @todo Perform the split in divideSpace() */ void splitOverlappingSegments() { for(ConvexSubspaceProxy const &subspace : subspaces) { /* * The subspace provides a specially ordered list of the segments to * simplify this task. The primary clockwise ordering (decreasing angle * relative to the center of the subspace) places overlapping segments * adjacently. The secondary anticlockwise ordering sorts the overlapping * segments enabling the use of single pass algorithm here. */ OrderedSegments convexSet = subspace.segments(); int const numSegments = convexSet.count(); for(int i = 0; i < numSegments - 1; ++i) { // Determine the indice range of the partially overlapping segments. int k = i; while(de::fequal(convexSet[k + 1].fromAngle, convexSet[i].fromAngle) && ++k < numSegments - 1) {} // Split each overlapping segment at the point defined by the end // vertex of each of the other overlapping segments. for(int l = i; l < k; ++l) { OrderedSegment &a = convexSet[l]; for(int m = l + 1; m <= k; ++m) { OrderedSegment &b = convexSet[m]; // Segments of the same length will not be split. if(de::fequal(b.segment->length(), a.segment->length())) continue; // Do not attempt to split at an existing vertex. /// @todo fixme: For this to happen we *must* be dealing with /// an invalid mapping construct such as a two-sided line in /// the void. These cannot be dealt with here as they require /// a detection algorithm ran prior to splitting overlaps (so /// that we can skip them here). Presently it is sufficient to /// simply not split if the would-be split point is equal to /// either of the segment's existing vertexes. Vector2d const &point = b.segment->to().origin(); if(point == a.segment->from().origin() || point == a.segment->to().origin()) continue; splitLineSegment(*a.segment, point, false /*don't update edge tips*/); } } i = k; } } } void buildSubspaceGeometries() { for(ConvexSubspaceProxy const &subspace : subspaces) { /// @todo Move BSP leaf construction here? BspLeaf &bspLeaf = *subspace.bspLeaf(); subspace.buildGeometry(bspLeaf, *mesh); // Account the new segments. /// @todo Refactor away. for(OrderedSegment const &oseg : subspace.segments()) { if(oseg.segment->hasHEdge()) { // There is now one more line segment. segmentCount += 1; } } } /* * Finalize the built geometry by adding a twin half-edge for any * which don't yet have one. */ for(ConvexSubspaceProxy const &convexSet : subspaces) for(OrderedSegment const &oseg : convexSet.segments()) { LineSegmentSide *seg = oseg.segment; if(seg->hasHEdge() && !seg->back().hasHEdge()) { HEdge *hedge = &seg->hedge(); DENG2_ASSERT(!hedge->hasTwin()); // Allocate the twin from the same mesh. hedge->setTwin(hedge->mesh().newHEdge(seg->back().from())); hedge->twin().setTwin(hedge); } } } /** * Notify interested parties of an unclosed sector in the map. * * @param sector The problem sector. * @param nearPoint Coordinates near to where the problem was found. */ void notifyUnclosedSectorFound(Sector §or, Vector2d const &nearPoint) { DENG2_FOR_PUBLIC_AUDIENCE(UnclosedSectorFound, i) { i->unclosedSectorFound(sector, nearPoint); } } #ifdef DENG2_DEBUG void printSegments(LineSegmentSides const &allSegs) { for(LineSegmentSide const *seg : allSegs) { LOG_DEBUG("Build: %s line segment %p sector: %d %s -> %s") << (seg->hasMapSide()? "map" : "part") << seg << (seg->hasSector()? seg->sector().indexInMap() : -1) << seg->from().origin().asText() << seg->to().origin().asText(); } } #endif }; Partitioner::Partitioner(int splitCostFactor) : d(new Instance(this)) { setSplitCostFactor(splitCostFactor); } void Partitioner::setSplitCostFactor(int newFactor) { d->splitCostFactor = newFactor; } static AABox blockmapBounds(AABoxd const &mapBounds) { AABox mapBoundsi; mapBoundsi.minX = int( de::floor(mapBounds.minX) ); mapBoundsi.minY = int( de::floor(mapBounds.minY) ); mapBoundsi.maxX = int( de::ceil(mapBounds.maxX) ); mapBoundsi.maxY = int( de::ceil(mapBounds.maxY) ); AABox blockBounds; blockBounds.minX = mapBoundsi.minX - (mapBoundsi.minX & 0x7); blockBounds.minY = mapBoundsi.minY - (mapBoundsi.minY & 0x7); int bw = ((mapBoundsi.maxX - blockBounds.minX) / 128) + 1; int bh = ((mapBoundsi.maxY - blockBounds.minY) / 128) + 1; blockBounds.maxX = blockBounds.minX + 128 * M_CeilPow2(bw); blockBounds.maxY = blockBounds.minY + 128 * M_CeilPow2(bh); return blockBounds; } static bool lineIndexLessThan(Line const *a, Line const *b) { return a->indexInMap() < b->indexInMap(); } Partitioner::BspTree *Partitioner::makeBspTree(LineSet const &lines, Mesh &mesh) { d->clear(); // Copy the set of lines and sort by index to ensure deterministically // predictable output. d->lines = lines.toList(); qSort(d->lines.begin(), d->lines.end(), lineIndexLessThan); d->mesh = &mesh; // Initialize vertex info for the initial set of vertexes. d->edgeTipSets.reserve(d->lines.count() * 2); // Determine the bounds of the line geometry. AABoxd bounds; bool isFirst = true; for(Line *line : d->lines) { if(isFirst) { // The first line's bounds are used as is. V2d_CopyBox(bounds.arvec2, line->aaBox().arvec2); isFirst = false; } else { // Expand the bounding box. V2d_UniteBox(bounds.arvec2, line->aaBox().arvec2); } } Instance::LineSegmentBlockTree blockTree(blockmapBounds(bounds)); d->createInitialLineSegments(blockTree); d->bspRoot = d->partitionSpace(blockTree); // At this point we know that *something* useful was built. d->splitOverlappingSegments(); d->buildSubspaceGeometries(); return d->bspRoot; } int Partitioner::segmentCount() { return d->segmentCount; } int Partitioner::vertexCount() { return d->vertexCount; } } // namespace de doomsday-stable-1.15.7/doomsday/client/src/world/bsp/convexsubspaceproxy.cpp0000664000175000017500000004500412641367670026634 0ustar jaakkojaakko/** @file convexsubspaceproxy.cpp BSP builder convex subspace proxy. * * @authors Copyright © 2013-2014 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_platform.h" #include "world/bsp/convexsubspaceproxy.h" #include "Face" #include "HEdge" #include "Mesh" #include "BspLeaf" #include "ConvexSubspace" #include "Line" #include "Sector" #include "world/bsp/linesegment.h" #include "world/worldsystem.h" /// validCount @todo Remove me #include #include #include #include #include namespace de { namespace bsp { typedef QList SegmentList; /** * Represents a clockwise ordering of a subset of the line segments and implements logic * for partitioning the subset into @em contiguous ranges, for geometry construction. */ struct Continuity { typedef QList OrderedSegmentList; Sector *sector = nullptr; ///< Front sector uniformly referenced by all line segments. double coverage = 0; ///< Coverage metric. int discordSegments = 0; ///< Number of discordant (i.e., non-contiguous) line segments. /// Number of referencing line segments of each type: int norm = 0; int part = 0; int self = 0; OrderedSegmentList orderedSegs; ///< Ordered line segments (not owned). OrderedSegmentList discordSegs; ///< The discordant line segment subset (not owned). Continuity(Sector *frontSector) { sector = frontSector; } /** * Perform heuristic comparison between two continuities to determine a * preference order for BSP sector attribution. The algorithm used weights * the two choices according to the number and "type" of the referencing * line segments and the "coverage" metric. * * @return @c true if "this" choice is rated better than @a other. * * @todo Remove when heuristic sector selection is no longer necessary. */ bool operator < (Continuity const &other) const { if(norm == other.norm) { return !(coverage < other.coverage); } return norm > other.norm; } /** * Assumes that segments are added in clockwise order. */ void addOneSegment(OrderedSegment const &oseg) { DENG2_ASSERT(oseg.segment->sectorPtr() == sector); // Separate the discordant duplicates. OrderedSegmentList *list = &orderedSegs; for(OrderedSegment const *other : orderedSegs) { if(oseg == *other) { list = &discordSegs; break; } } list->append(const_cast(&oseg)); // Account for the new line segment. LineSegmentSide const &seg = *oseg.segment; if(!seg.hasMapSide()) { part += 1; } else if(seg.mapSide().line().isSelfReferencing()) { self += 1; } else { norm += 1; } // Update the 'coverage' metric. if(oseg.fromAngle > oseg.toAngle) coverage += oseg.fromAngle - oseg.toAngle; else coverage += oseg.fromAngle + (360.0 - oseg.toAngle); } void evaluate() { // Account remaining discontiguous segments. discordSegments = 0; for(int i = 0; i < orderedSegs.count() - 1; ++i) { LineSegmentSide const &segA = *orderedSegs[i ]->segment; LineSegmentSide const &segB = *orderedSegs[i+1]->segment; if(segB.from().origin() != segA.to().origin()) discordSegments += 1; } if(orderedSegs.count() > 1) { LineSegmentSide const &segB = *orderedSegs.last()->segment; LineSegmentSide const &segA = *orderedSegs.first()->segment; if(segB.to().origin() != segA.from().origin()) discordSegments += 1; } } #ifdef DENG2_DEBUG void debugPrint() const { LOGDEV_MAP_MSG("Continuity %p (sector:%i, coverage:%f, discord:%i)") << this << (sector? sector->indexInArchive() : -1) << coverage << discordSegments; for(OrderedSegment const *oseg : orderedSegs) { oseg->debugPrint(); } for(OrderedSegment const *oseg : discordSegs) { oseg->debugPrint(); } } #endif }; DENG2_PIMPL_NOREF(ConvexSubspaceProxy) { typedef QSet Segments; Segments segments; ///< All line segments. OrderedSegments orderedSegments; ///< All line segments in clockwise order, with angle info. bool needRebuildOrderedSegments; ///< @c true= the ordered segment list needs to be rebuilt. BspLeaf *bspLeaf; ///< BSP leaf attributed to the subspace (if any). Instance() : needRebuildOrderedSegments(false) , bspLeaf (nullptr) {} Instance(Instance const &other) : de::IPrivate() , segments (other.segments) , orderedSegments (other.orderedSegments) , needRebuildOrderedSegments(other.needRebuildOrderedSegments) , bspLeaf (other.bspLeaf) {} /** * Returns @c true iff at least one line segment in the set is derived * from a map line. */ bool haveMapLineSegment() const { for(LineSegmentSide const *seg : segments) { if(seg->hasMapSide()) return true; } return false; } Vector2d findCenter() const { Vector2d center; int numPoints = 0; for(LineSegmentSide const *seg : segments) { center += seg->from().origin(); center += seg->to().origin(); numPoints += 2; } if(numPoints) { center /= numPoints; } return center; } /** * Builds the ordered list of line segments, which, is sorted firstly in * a clockwise order (i.e., descending angles) according to the origin of * their 'from' vertex relative to @a point. A secondary ordering is also * applied such that line segments with the same origin coordinates are * sorted by descending 'to' angle. */ void buildOrderedSegments(Vector2d const &point) { needRebuildOrderedSegments = false; orderedSegments.clear(); for(LineSegmentSide *seg : segments) { Vector2d fromDist = seg->from().origin() - point; Vector2d toDist = seg->to().origin() - point; OrderedSegment oseg; oseg.segment = seg; oseg.fromAngle = M_DirectionToAngleXY(fromDist.x, fromDist.y); oseg.toAngle = M_DirectionToAngleXY(toDist.x, toDist.y); orderedSegments.append(oseg); } // Sort algorithm: "double bubble". // Order by descending 'from' angle. int const numSegments = orderedSegments.count(); for(int pass = 0; pass < numSegments - 1; ++pass) { bool swappedAny = false; for(int i = 0; i < numSegments - 1; ++i) { OrderedSegment const &a = orderedSegments.at(i); OrderedSegment const &b = orderedSegments.at(i+1); if(a.fromAngle < b.fromAngle) { orderedSegments.swap(i, i + 1); swappedAny = true; } } if(!swappedAny) break; } for(int pass = 0; pass < numSegments - 1; ++pass) { bool swappedAny = false; for(int i = 0; i < numSegments - 1; ++i) { OrderedSegment const &a = orderedSegments.at(i); OrderedSegment const &b = orderedSegments.at(i+1); if(a.fromAngle == b.fromAngle) { if(b.segment->length() > a.segment->length()) { orderedSegments.swap(i, i + 1); swappedAny = true; } } } if(!swappedAny) break; } // LOG_DEBUG("Ordered segments around %s") << point.asText(); } private: Instance &operator = (Instance const &); // no assignment }; ConvexSubspaceProxy::ConvexSubspaceProxy() : d(new Instance) {} ConvexSubspaceProxy::ConvexSubspaceProxy(QList const &segments) : d(new Instance) { addSegments(segments); } ConvexSubspaceProxy::ConvexSubspaceProxy(ConvexSubspaceProxy const &other) : d(new Instance(*other.d)) {} ConvexSubspaceProxy &ConvexSubspaceProxy::operator = (ConvexSubspaceProxy const &other) { d.reset(new Instance(*other.d)); return *this; } void ConvexSubspaceProxy::addSegments(QList const &newSegments) { int sizeBefore = d->segments.size(); d->segments.unite(QSet::fromList(newSegments)); if(d->segments.size() != sizeBefore) { // We'll need to rebuild the ordered segment list. d->needRebuildOrderedSegments = true; } #ifdef DENG2_DEBUG int numSegmentsAdded = d->segments.size() - sizeBefore; if(numSegmentsAdded < newSegments.size()) { LOG_DEBUG("ConvexSubspaceProxy pruned %i duplicate segments") << (newSegments.size() - numSegmentsAdded); } #endif } void ConvexSubspaceProxy::addOneSegment(LineSegmentSide const &newSegment) { int sizeBefore = d->segments.size(); d->segments.insert(const_cast(&newSegment)); if(d->segments.size() != sizeBefore) { // We'll need to rebuild the ordered segment list. d->needRebuildOrderedSegments = true; } else { LOG_DEBUG("ConvexSubspaceProxy pruned one duplicate segment"); } } void ConvexSubspaceProxy::buildGeometry(BspLeaf &leaf, Mesh &mesh) const { LOG_AS("ConvexSubspaceProxy::buildGeometry"); // Sanity check. if(segmentCount() >= 3 && !d->haveMapLineSegment()) throw Error("ConvexSubspaceProxy::buildGeometry", "No map line segment"); if(d->needRebuildOrderedSegments) { d->buildOrderedSegments(d->findCenter()); } /* * Build the line segment -> sector continuity map. */ typedef QList Continuities; Continuities continuities; typedef QHash SectorContinuityMap; SectorContinuityMap scMap; for(OrderedSegment const &oseg : d->orderedSegments) { Sector *frontSector = oseg.segment->sectorPtr(); SectorContinuityMap::iterator found = scMap.find(frontSector); if(found == scMap.end()) { continuities.append(Continuity(frontSector)); found = scMap.insert(frontSector, &continuities.last()); } Continuity *conty = found.value(); conty->addOneSegment(oseg); } QVarLengthArray extraMeshes; int extraMeshSegments = 0; for(int i = 0; i < continuities.count(); ++i) { Continuity &conty = continuities[i]; conty.evaluate(); if(!conty.discordSegs.isEmpty()) { Mesh *extraMesh = nullptr; Face *face = nullptr; for(OrderedSegment const *oseg : conty.discordSegs) { LineSegmentSide *lineSeg = oseg->segment; LineSide *mapSide = lineSeg->mapSidePtr(); if(!mapSide) continue; if(!extraMesh) { // Construct a new mesh and set of half-edges. extraMesh = new Mesh; face = extraMesh->newFace(); } HEdge *hedge = extraMesh->newHEdge(lineSeg->from()); LineSideSegment *seg = mapSide->addSegment(*hedge); extraMeshSegments += 1; #ifdef __CLIENT__ /// @todo LineSide::newSegment() should encapsulate: seg->setLineSideOffset(Vector2d(mapSide->from().origin() - lineSeg->from().origin()).length()); seg->setLength(Vector2d(lineSeg->to().origin() - lineSeg->from().origin()).length()); #else DENG2_UNUSED(seg); #endif // Link the new half-edge for this line segment to the head of // the list in the new face geometry. hedge->setNext(face->hedge()); face->setHEdge(hedge); // Is there a half-edge on the back side we need to twin with? if(lineSeg->back().hasHEdge()) { lineSeg->back().hedge().setTwin(hedge); hedge->setTwin(lineSeg->back().hedgePtr()); } // Link the new half-edge with the line segment. lineSeg->setHEdge(hedge); } if(extraMesh) { // Link the half-edges anticlockwise and close the ring. HEdge *hedge = face->hedge(); forever { // There is now one more half-edge in this face. /// @todo Face should encapsulate. face->_hedgeCount += 1; // Attribute the half-edge to the Face. hedge->setFace(face); if(hedge->hasNext()) { // Link anticlockwise. hedge->next().setPrev(hedge); hedge = &hedge->next(); } else { // Circular link. hedge->setNext(face->hedge()); hedge->next().setPrev(hedge); break; } } /// @todo Face should encapsulate. face->updateAABox(); face->updateCenter(); extraMeshes.append(extraMesh); } } } // Determine which sector to attribute the BSP leaf to. qSort(continuities.begin(), continuities.end()); leaf.setSector(continuities.first().sector); /*#ifdef DENG_DEBUG LOG_INFO("ConvexSubspace %s BSP sector:%i (%i continuities)") << d->findCenter().asText() << (leaf.sectorPtr()? leaf.sectorPtr()->indexInArchive() : -1) << continuities.count(); for(Continuity const &conty : continuities) { conty.debugPrint(); } #endif*/ if(segmentCount() - extraMeshSegments >= 3) { // Construct a new face and a ring of half-edges. Face *face = mesh.newFace(); // Iterate backwards so that the half-edges can be linked clockwise. for(int i = d->orderedSegments.size(); i-- > 0; ) { LineSegmentSide *lineSeg = d->orderedSegments[i].segment; // Already added this to an extra mesh? if(lineSeg->hasHEdge()) continue; HEdge *hedge = mesh.newHEdge(lineSeg->from()); if(LineSide *mapSide = lineSeg->mapSidePtr()) { LineSideSegment *seg = mapSide->addSegment(*hedge); #ifdef __CLIENT__ /// @todo LineSide::newSegment() should encapsulate: seg->setLineSideOffset(Vector2d(mapSide->from().origin() - lineSeg->from().origin()).length()); seg->setLength(Vector2d(lineSeg->to().origin() - lineSeg->from().origin()).length()); #else DENG2_UNUSED(seg); #endif } // Link the new half-edge for this line segment to the head of // the list in the new Face geometry. hedge->setNext(face->hedge()); face->setHEdge(hedge); // Is there a half-edge on the back side we need to twin with? if(lineSeg->back().hasHEdge()) { lineSeg->back().hedge().setTwin(hedge); hedge->setTwin(lineSeg->back().hedgePtr()); } // Link the new half-edge with the line segment. lineSeg->setHEdge(hedge); } // Link the half-edges anticlockwise and close the ring. HEdge *hedge = face->hedge(); forever { // There is now one more half-edge in this face. /// @todo Face should encapsulate. face->_hedgeCount += 1; // Attribute the half-edge to the Face. hedge->setFace(face); if(hedge->hasNext()) { // Link anticlockwise. hedge->next().setPrev(hedge); hedge = &hedge->next(); } else { // Circular link. hedge->setNext(face->hedge()); hedge->next().setPrev(hedge); break; } } /// @todo Face should encapsulate. face->updateAABox(); face->updateCenter(); // Assign a new convex subspace to the BSP leaf (takes ownership). leaf.setSubspace(ConvexSubspace::newFromConvexPoly(*face)); // Assign any extra meshes to the subspace (takes ownership). for(int i = 0; i < extraMeshes.count(); ++i) { leaf.subspace().assignExtraMesh(*extraMeshes.at(i)); } } /*else { // Dump the unneeded extra meshes. qDeleteAll(extraMeshes); }*/ } int ConvexSubspaceProxy::segmentCount() const { return d->segments.count(); } OrderedSegments const &ConvexSubspaceProxy::segments() const { if(d->needRebuildOrderedSegments) { d->buildOrderedSegments(d->findCenter()); } return d->orderedSegments; } BspLeaf *ConvexSubspaceProxy::bspLeaf() const { return d->bspLeaf; } void ConvexSubspaceProxy::setBspLeaf(BspLeaf *newBspLeaf) { d->bspLeaf = newBspLeaf; } } // namespace bsp } // namespace de doomsday-stable-1.15.7/doomsday/client/src/world/bsp/partitionevaluator.cpp0000664000175000017500000004153712641367670026445 0ustar jaakkojaakko/** @file partitionevaluator.cpp Evaluator for a would-be BSP. * * @authors Copyright © 2006-2014 Daniel Swanson * @authors Copyright © 2006-2007 Jamie Jones * @authors Copyright © 2000-2007 Andrew Apted * @authors Copyright © 1998-2000 Colin Reed * @authors Copyright © 1998-2000 Lee Killough * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. 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 along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "world/bsp/partitionevaluator.h" #include #include #include #include #include "world/bsp/partitioner.h" #include "world/worldsystem.h" // validCount namespace de { namespace bsp { namespace internal { struct PartitionCost { int total = 0; int splits = 0; int iffy = 0; int nearMiss = 0; int mapRight = 0; int mapLeft = 0; int partRight = 0; int partLeft = 0; inline PartitionCost &addSegmentRight(LineSegmentSide const &seg) { if(seg.hasMapSide()) mapRight += 1; else partRight += 1; return *this; } inline PartitionCost &addSegmentLeft(LineSegmentSide const &seg) { if(seg.hasMapSide()) mapLeft += 1; else partLeft += 1; return *this; } PartitionCost &operator += (PartitionCost const &other) { total += other.total; splits += other.splits; iffy += other.iffy; nearMiss += other.nearMiss; mapLeft += other.mapLeft; mapRight += other.mapRight; partLeft += other.partLeft; partRight += other.partRight; return *this; } PartitionCost &operator = (PartitionCost const &other) { total = other.total; splits = other.splits; iffy = other.iffy; nearMiss = other.nearMiss; mapLeft = other.mapLeft; mapRight = other.mapRight; partLeft = other.partLeft; partRight = other.partRight; return *this; } bool operator < (PartitionCost const &rhs) const { return total < rhs.total; } String asText() const { return String("PartitionCost(Total= %1.%2; splits:%3, iffy:%4, near:%5, left:%6+%7, right:%8+%9)") .arg(total / 100).arg(total % 100, 2, QChar('0')) .arg(splits).arg(iffy).arg(nearMiss) .arg(mapLeft).arg(partLeft) .arg(mapRight).arg(partRight); } }; /** * "Near miss" predicate. */ static bool nearMiss(LineRelationship rel, coord_t fromDist, coord_t toDist, coord_t *distance) { if(rel == Right && !((fromDist >= SHORT_HEDGE_EPSILON && toDist >= SHORT_HEDGE_EPSILON) || (fromDist <= DIST_EPSILON && toDist >= SHORT_HEDGE_EPSILON) || (toDist <= DIST_EPSILON && fromDist >= SHORT_HEDGE_EPSILON))) { // Need to know how close? if(distance) { if(fromDist <= DIST_EPSILON || toDist <= DIST_EPSILON) { *distance = SHORT_HEDGE_EPSILON / de::max(fromDist, toDist); } else { *distance = SHORT_HEDGE_EPSILON / de::min(fromDist, toDist); } } return true; } if(rel == Left && !((fromDist <= -SHORT_HEDGE_EPSILON && toDist <= -SHORT_HEDGE_EPSILON) || (fromDist >= -DIST_EPSILON && toDist <= -SHORT_HEDGE_EPSILON) || (toDist >= -DIST_EPSILON && fromDist <= -SHORT_HEDGE_EPSILON))) { // Need to know how close? if(distance) { if(fromDist >= -DIST_EPSILON || toDist >= -DIST_EPSILON) { *distance = SHORT_HEDGE_EPSILON / -de::min(fromDist, toDist); } else { *distance = SHORT_HEDGE_EPSILON / -de::max(fromDist, toDist); } } return true; } return false; } /** * "Near edge" predicate. Assumes intersecting line segment relationship. */ static bool nearEdge(coord_t fromDist, coord_t toDist, coord_t *distance) { if(de::abs(fromDist) < SHORT_HEDGE_EPSILON || de::abs(toDist) < SHORT_HEDGE_EPSILON) { // Need to know how close? if(distance) { *distance = SHORT_HEDGE_EPSILON / de::min(de::abs(fromDist), de::abs(toDist)); } return true; } return false; } } using namespace internal; DENG2_PIMPL_NOREF(PartitionEvaluator) { int splitCostFactor = 7; LineSegmentBlockTreeNode *rootNode = nullptr; ///< Current block tree root node. struct PartitionCandidate { LineSegmentSide *line; ///< Candidate partition line. PartitionCost cost; ///< Running cost metric total. PartitionCandidate(LineSegmentSide &partition) : line(&partition) {} }; typedef QList Candidates; Candidates candidates; PartitionCandidate *nextCandidate() { DENG2_ASSERT(costTaskPool.isDone()); if(candidates.isEmpty()) return nullptr; return candidates.takeFirst(); } class CostTask : public Task { public: Instance &evaluator; PartitionCandidate &candidate; CostTask(Instance &evaluator, PartitionCandidate &candidate) : evaluator(evaluator), candidate(candidate) {} /** * Evaluate the cost of the partition candidate. * * If the candidate is not suitable (or a better choice has already been * determined) then @var partition is zeroed. Otherwise the candidate is * suitable and @var cost contains valid costing metrics. */ void runTask() { LineSegmentSide **partition = &candidate.line; PartitionCost &cost = candidate.cost; costForBlock(*evaluator.rootNode); // Make sure there is at least one map line segment on each side. if(!cost.mapLeft || !cost.mapRight) { //LOG_DEBUG("evaluate: No map line segments on %s%sside") // << (cost.mapLeft ? "" : "left ") // << (cost.mapRight? "" : "right "); *partition = nullptr; return; } // This is suitable for use as a partition. // Increase cost by the difference between left and right. cost.total += 100 * de::abs(cost.mapLeft - cost.mapRight); // Allow partition segment counts to affect the outcome. cost.total += 50 * de::abs(cost.partLeft - cost.partRight); // Another little twist, here we show a slight preference for partition // lines that lie either purely horizontally or purely vertically. if((*partition)->slopeType() != ST_HORIZONTAL && (*partition)->slopeType() != ST_VERTICAL) { cost.total += 25; } } private: void costForSegment(LineSegmentSide const &seg) { LineSegmentSide **partition = &candidate.line; PartitionCost &cost = candidate.cost; int const splitCostFactor = evaluator.splitCostFactor; /// Determine the relationship between @a seg and the partition plane. coord_t fromDist, toDist; LineRelationship rel = seg.relationship(**partition, &fromDist, &toDist); switch(rel) { case Collinear: { // This line segment runs along the same line as the partition. // Check whether it goes in the same direction or the opposite. if(seg.direction().dot((*partition)->direction()) < 0) { cost.addSegmentLeft(seg); } else { cost.addSegmentRight(seg); } break; } case Right: case RightIntercept: { cost.addSegmentRight(seg); /* * Near misses are bad, as they have the potential to result in * really short line segments being produced later on. * * The closer the near miss, the higher the cost. */ coord_t nearDist; if(nearMiss(rel, fromDist, toDist, &nearDist)) { cost.nearMiss += 1; cost.total += int( 100 * splitCostFactor * (nearDist * nearDist - 1.0) ); } break; } case Left: case LeftIntercept: { cost.addSegmentLeft(seg); // Near miss? coord_t nearDist; if(nearMiss(rel, fromDist, toDist, &nearDist)) { /// @todo Why the cost multiplier imbalance between the left /// and right edge near misses? cost.nearMiss += 1; cost.total += int( 70 * splitCostFactor * (nearDist * nearDist - 1.0) ); } break; } case Intersects: { cost.splits += 1; cost.total += 100 * splitCostFactor; /* * If the split point is very close to one end, which is quite an * undesirable situation (producing really short edges), thus a * rather hefty surcharge. * * The closer to the edge, the higher the cost. */ coord_t nearDist; if(nearEdge(fromDist, toDist, &nearDist)) { cost.iffy += 1; cost.total += int( 140 * splitCostFactor * (nearDist * nearDist - 1.0) ); } break; } } } /** * Test the whole block against the partition line to quickly handle all the * line segments within it at once. Only when the partition line intercepts * the block do we need to go deeper into it. */ void costForBlock(LineSegmentBlockTreeNode const &node) { LineSegmentBlock const &block = *node.userData(); LineSegmentSide const *partition = candidate.line; PartitionCost &cost = candidate.cost; /// @todo Why are we extending the bounding box for this test? Also, /// there is no need to convert from integer to floating-point each /// time this is tested. (If we intend to do this with floating-point /// then we should return that representation in SuperBlock::bounds() ). AABoxd bounds(coord_t( block.bounds().minX ) - SHORT_HEDGE_EPSILON * 1.5, coord_t( block.bounds().minY ) - SHORT_HEDGE_EPSILON * 1.5, coord_t( block.bounds().maxX ) + SHORT_HEDGE_EPSILON * 1.5, coord_t( block.bounds().maxY ) + SHORT_HEDGE_EPSILON * 1.5); int side = partition->boxOnSide(bounds); if(side > 0) { // Right. cost.mapRight += block.mapCount(); cost.partRight += block.partCount(); return; } if(side < 0) { // Left. cost.mapLeft += block.mapCount(); cost.partLeft += block.partCount(); return; } for(LineSegmentSide *otherSeg : block.all()) { costForSegment(*otherSeg); } if(node.hasRight()) { costForBlock(node.right()); } if(node.hasLeft()) { costForBlock(node.left()); } } }; TaskPool costTaskPool; /** * @param line Partition line to evaluate. */ void beginPartitionCosting(LineSegmentSide *line) { DENG2_ASSERT(line && line->hasMapSide()); // Run a new partition cost task. PartitionCandidate *newCandidate = new PartitionCandidate(*line); candidates << newCandidate; costTaskPool.start(new CostTask(*this, *newCandidate)); } }; PartitionEvaluator::PartitionEvaluator(int splitCostFactor) : d(new Instance) { d->splitCostFactor = splitCostFactor; } LineSegmentSide *PartitionEvaluator::choose(LineSegmentBlockTreeNode &node) { LOG_AS("PartitionEvaluator"); d->rootNode = &node; // Increment valid count so we can avoid testing the line segments // produced from a single line more than once per round of partition // selection. validCount++; // Iterative pre-order traversal. LineSegmentBlockTreeNode const *cur = d->rootNode; LineSegmentBlockTreeNode const *prev = nullptr; while(cur) { while(cur) { LineSegmentBlock const &segs = *cur->userData(); // Test each line segment as a potential partition candidate. for(LineSegmentSide *candidate : segs.all()) { //LOG_DEBUG("%sline segment %p sector:%d %s -> %s") // << (candidate->hasMapLineSide()? "" : "mini-") << candidate // << (candidate->sector? candidate->sector->indexInMap() : -1) // << candidate->fromOrigin().asText() // << candidate->toOrigin().asText(); // Only map line segments are suitable candidates. if(!candidate->hasMapSide()) continue; // Optimization: Only the first line segment produced from a // given line is tested per round of partition costing because // they are all collinear. if(candidate->mapLine().validCount() == validCount) continue; // Skip this. // Don't consider further segments of the candidate. candidate->mapLine().setValidCount(validCount); // Determine candidate suitability and cost. d->beginPartitionCosting(candidate); } if(prev == cur->parentPtr()) { // Descending - right first, then left. prev = cur; if(cur->hasRight()) cur = cur->rightPtr(); else cur = cur->leftPtr(); } else if(prev == cur->rightPtr()) { // Last moved up the right branch - descend the left. prev = cur; cur = cur->leftPtr(); } else if(prev == cur->leftPtr()) { // Last moved up the left branch - continue upward. prev = cur; cur = cur->parentPtr(); } } if(prev) { // No left child - back up. cur = prev->parentPtr(); } } LineSegmentSide *best = nullptr; if(!d->candidates.isEmpty()) { d->costTaskPool.waitForDone(); PartitionCost bestCost; while(Instance::PartitionCandidate *candidate = d->nextCandidate()) { //LOG_DEBUG("%p: %s") << candidate->line << candidate->cost.asText(); if(candidate->line && (!best || candidate->cost < bestCost)) { // We have a new better choice. best = candidate->line; bestCost = candidate->cost; } delete candidate; } //LOG_DEBUG("best %p score: %d.%02d") // << best << bestCost.total / 100 << bestCost.total % 100; } return best; } } // namespace bsp } // namespace de doomsday-stable-1.15.7/doomsday/client/client.pro0000664000175000017500000007006412641367670021300 0ustar jaakkojaakko# The Doomsday Engine Project # Copyright (c) 2011-2014 Jaakko Keränen # Copyright (c) 2011-2014 Daniel Swanson TEMPLATE = app win32|macx: TARGET = Doomsday else: TARGET = doomsday # Build Configuration ---------------------------------------------------------- include(../config.pri) VERSION = $$DENG_VERSION echo(Doomsday Client $${DENG_VERSION}.) # Some messy old code here: *-g++*|*-gcc*|*-clang* { QMAKE_CXXFLAGS_WARN_ON += \ -Wno-missing-field-initializers \ -Wno-unused-parameter \ -Wno-missing-braces } # External Dependencies -------------------------------------------------------- CONFIG += deng_qtgui deng_qtopengl include(../dep_sdl2.pri) include(../dep_opengl.pri) include(../dep_zlib.pri) include(../dep_lzss.pri) win32 { include(../dep_directx.pri) } include(../dep_core.pri) include(../dep_legacy.pri) include(../dep_shell.pri) include(../dep_gui.pri) include(../dep_appfw.pri) include(../dep_doomsday.pri) # Definitions ------------------------------------------------------------------ DEFINES += __DOOMSDAY__ __CLIENT__ !isEmpty(DENG_BUILD) { !win32: echo(Build number: $$DENG_BUILD) DEFINES += DOOMSDAY_BUILD_TEXT=\\\"$$DENG_BUILD\\\" } else { !win32: echo(DENG_BUILD is not defined.) } # Linking ---------------------------------------------------------------------- win32 { RC_FILE = res/windows/doomsday.rc OTHER_FILES += $$RC_FILE deng_msvc: QMAKE_LFLAGS += /NODEFAULTLIB:libcmt LIBS += -lkernel32 -lgdi32 -lole32 -luser32 -lwsock32 -lopengl32 } else:macx { useFramework(Cocoa) useFramework(QTKit) } else { # Generic Unix. !freebsd-*: LIBS += -ldl } # Source Files ----------------------------------------------------------------- !deng_mingw: PRECOMPILED_HEADER = include/precompiled.h DENG_API_HEADERS = \ $$DENG_API_DIR/apis.h \ $$DENG_API_DIR/api_audiod.h \ $$DENG_API_DIR/api_audiod_mus.h \ $$DENG_API_DIR/api_audiod_sfx.h \ $$DENG_API_DIR/api_base.h \ $$DENG_API_DIR/api_busy.h \ $$DENG_API_DIR/api_client.h \ $$DENG_API_DIR/api_console.h \ $$DENG_API_DIR/api_def.h \ $$DENG_API_DIR/api_event.h \ $$DENG_API_DIR/api_infine.h \ $$DENG_API_DIR/api_internaldata.h \ $$DENG_API_DIR/api_filesys.h \ $$DENG_API_DIR/api_fontrender.h \ $$DENG_API_DIR/api_gameexport.h \ $$DENG_API_DIR/api_gl.h \ $$DENG_API_DIR/api_material.h \ $$DENG_API_DIR/api_materialarchive.h \ $$DENG_API_DIR/api_map.h \ $$DENG_API_DIR/api_mapedit.h \ $$DENG_API_DIR/api_player.h \ $$DENG_API_DIR/api_plugin.h \ $$DENG_API_DIR/api_render.h \ $$DENG_API_DIR/api_resource.h \ $$DENG_API_DIR/api_resourceclass.h \ $$DENG_API_DIR/api_sound.h \ $$DENG_API_DIR/api_svg.h \ $$DENG_API_DIR/api_thinker.h \ $$DENG_API_DIR/api_uri.h \ $$DENG_API_DIR/dd_share.h \ $$DENG_API_DIR/dd_types.h \ $$DENG_API_DIR/dd_version.h \ $$DENG_API_DIR/def_share.h \ $$DENG_API_DIR/dengproject.h \ $$DENG_API_DIR/doomsday.h \ $$DENG_API_DIR/xgclass.h # Convenience headers. DENG_CONVENIENCE_HEADERS += \ include/AbstractFont \ include/BiasDigest \ include/BiasIllum \ include/BiasSource \ include/BiasTracker \ include/BindContext \ include/Binding \ include/BitmapFont \ include/BspLeaf \ include/BspNode \ include/CommandAction \ include/CommandBinding \ include/CompositeBitmapFont \ include/Contact \ include/ContactSpreader \ include/ConvexSubspace \ include/Decoration \ include/DrawList \ include/DrawLists \ include/EntityDatabase \ include/Face \ include/FontManifest \ include/FontScheme \ include/Game \ include/Games \ include/Generator \ include/Grabbable \ include/Hand \ include/HEdge \ include/HueCircle \ include/HueCircleVisual \ include/IHPlane \ include/ImpulseBinding \ include/Interceptor \ include/LightDecoration \ include/Line \ include/Lumobj \ include/MapDef \ include/MapElement \ include/MapObject \ include/Material \ include/MaterialAnimator \ include/MaterialArchive \ include/MaterialContext \ include/MaterialManifest \ include/MaterialScheme \ include/MaterialVariantSpec \ include/Mesh \ include/Model \ include/ModelDef \ include/Plane \ include/Polyobj \ include/Sector \ include/SectorCluster \ include/SettingsRegister \ include/Shard \ include/SkyFixEdge \ include/Sprite \ include/Surface \ include/SurfaceDecorator \ include/Texture \ include/TextureManifest \ include/TextureScheme \ include/TextureVariantSpec \ include/TriangleStripBuilder \ include/Vertex \ include/WallEdge \ include/WallSpec # Private headers. DENG_HEADERS += \ $$DENG_CONVENIENCE_HEADERS \ include/alertmask.h \ include/audio/audiodriver.h \ include/audio/audiodriver_music.h \ include/audio/m_mus2midi.h \ include/audio/s_cache.h \ include/audio/s_environ.h \ include/audio/s_main.h \ include/audio/s_mus.h \ include/audio/s_sfx.h \ include/audio/sys_audio.h \ include/audio/sys_audiod_dummy.h \ include/busymode.h \ include/client/cl_def.h \ include/client/cl_frame.h \ include/client/cl_infine.h \ include/client/cl_mobj.h \ include/client/cl_player.h \ include/client/cl_sound.h \ include/client/cl_world.h \ include/client/clplanemover.h \ include/client/clpolymover.h \ include/clientapp.h \ include/color.h \ include/con_config.h \ include/dd_def.h \ include/dd_loop.h \ include/dd_main.h \ include/dd_pinit.h \ include/de_audio.h \ include/de_base.h \ include/de_console.h \ include/de_defs.h \ include/de_edit.h \ include/de_filesys.h \ include/de_graphics.h \ include/de_infine.h \ include/de_misc.h \ include/de_network.h \ include/de_platform.h \ include/de_play.h \ include/de_render.h \ include/de_resource.h \ include/de_system.h \ include/de_ui.h \ include/def_main.h \ include/edit_bias.h \ include/edit_map.h \ include/face.h \ include/game.h \ include/games.h \ include/gl/gl_defer.h \ include/gl/gl_deferredapi.h \ include/gl/gl_draw.h \ include/gl/gl_main.h \ include/gl/gl_tex.h \ include/gl/gl_texmanager.h \ include/gl/gltextureunit.h \ include/gl/svg.h \ include/gl/sys_opengl.h \ include/gl/texturecontent.h \ include/hedge.h \ include/ihplane.h \ include/library.h \ include/m_decomp64.h \ include/m_misc.h \ include/m_nodepile.h \ include/m_profiler.h \ include/mesh.h \ include/network/masterserver.h \ include/network/monitor.h \ include/network/net_buf.h \ include/network/net_demo.h \ include/network/net_event.h \ include/network/net_main.h \ include/network/net_msg.h \ include/network/protocol.h \ include/network/serverlink.h \ include/network/sys_network.h \ include/partition.h \ include/r_util.h \ include/render/angleclipper.h \ include/render/biasdigest.h \ include/render/biasillum.h \ include/render/biassource.h \ include/render/biastracker.h \ include/render/billboard.h \ include/render/blockmapvisual.h \ include/render/cameralensfx.h \ include/render/consoleeffect.h \ include/render/decoration.h \ include/render/drawlist.h \ include/render/drawlists.h \ include/render/fx/bloom.h \ include/render/fx/colorfilter.h \ include/render/fx/lensflares.h \ include/render/fx/postprocessing.h \ include/render/fx/resize.h \ include/render/fx/vignette.h \ include/render/huecirclevisual.h \ include/render/ilightsource.h \ include/render/lightdecoration.h \ include/render/lightgrid.h \ include/render/lumobj.h \ include/render/materialcontext.h \ include/render/mobjanimator.h \ include/render/modelrenderer.h \ include/render/projectedtexturedata.h \ include/render/r_draw.h \ include/render/r_main.h \ include/render/r_things.h \ include/render/rend_fakeradio.h \ include/render/rend_font.h \ include/render/rend_halo.h \ include/render/rend_main.h \ include/render/rend_model.h \ include/render/rend_particle.h \ include/render/rendpoly.h \ include/render/rendersystem.h \ include/render/shadowedge.h \ include/render/shard.h \ include/render/skydrawable.h \ include/render/skyfixedge.h \ include/render/surfacedecorator.h \ include/render/trianglestripbuilder.h \ include/render/vectorlightdata.h \ include/render/viewports.h \ include/render/vissprite.h \ include/render/vr.h \ include/render/walledge.h \ include/render/wallspec.h \ include/resource/abstractfont.h \ include/resource/animgroup.h \ include/resource/bitmapfont.h \ include/resource/colorpalette.h \ include/resource/compositebitmapfont.h \ include/resource/compositetexture.h \ include/resource/fontmanifest.h \ include/resource/fontscheme.h \ include/resource/hq2x.h \ include/resource/image.h \ include/resource/manifest.h \ include/resource/mapdef.h \ include/resource/material.h \ include/resource/materialanimator.h \ include/resource/materialarchive.h \ include/resource/materialdetaillayer.h \ include/resource/materiallightdecoration.h \ include/resource/materialmanifest.h \ include/resource/materialscheme.h \ include/resource/materialshinelayer.h \ include/resource/materialtexturelayer.h \ include/resource/materialvariantspec.h \ include/resource/model.h \ include/resource/modeldef.h \ include/resource/patch.h \ include/resource/patchname.h \ include/resource/pcx.h \ include/resource/rawtexture.h \ include/resource/resourcesystem.h \ include/resource/sprite.h \ include/resource/texture.h \ include/resource/texturemanifest.h \ include/resource/texturescheme.h \ include/resource/texturevariantspec.h \ include/resource/tga.h \ include/settingsregister.h \ include/sys_system.h \ include/tab_anorms.h \ include/ui/b_main.h \ include/ui/b_util.h \ include/ui/bindcontext.h \ include/ui/binding.h \ include/ui/busyvisual.h \ include/ui/clientrootwidget.h \ include/ui/clientwindow.h \ include/ui/clientwindowsystem.h \ include/ui/commandaction.h \ include/ui/commandbinding.h \ include/ui/ddevent.h \ include/ui/dialogs/aboutdialog.h \ include/ui/dialogs/alertdialog.h \ include/ui/dialogs/audiosettingsdialog.h \ include/ui/dialogs/coloradjustmentdialog.h \ include/ui/dialogs/gamesdialog.h \ include/ui/dialogs/inputsettingsdialog.h \ include/ui/dialogs/logsettingsdialog.h \ include/ui/dialogs/manualconnectiondialog.h \ include/ui/dialogs/networksettingsdialog.h \ include/ui/dialogs/renderersettingsdialog.h \ include/ui/dialogs/videosettingsdialog.h \ include/ui/dialogs/vrsettingsdialog.h \ include/ui/editors/rendererappearanceeditor.h \ include/ui/impulsebinding.h \ include/ui/infine/finale.h \ include/ui/infine/finaleanimwidget.h \ include/ui/infine/finaleinterpreter.h \ include/ui/infine/finalepagewidget.h \ include/ui/infine/finaletextwidget.h \ include/ui/infine/finalewidget.h \ include/ui/infine/infinesystem.h \ include/ui/inputdevice.h \ include/ui/inputdeviceaxiscontrol.h \ include/ui/inputdevicebuttoncontrol.h \ include/ui/inputdevicehatcontrol.h \ include/ui/progress.h \ include/ui/widgets/busywidget.h \ include/ui/widgets/consolecommandwidget.h \ include/ui/widgets/consolewidget.h \ include/ui/widgets/cvarchoicewidget.h \ include/ui/widgets/cvarlineeditwidget.h \ include/ui/widgets/cvarnativepathwidget.h \ include/ui/widgets/cvarsliderwidget.h \ include/ui/widgets/cvartogglewidget.h \ include/ui/widgets/gamefilterwidget.h \ include/ui/widgets/gameselectionwidget.h \ include/ui/widgets/gamesessionwidget.h \ include/ui/widgets/gameuiwidget.h \ include/ui/widgets/gamewidget.h \ include/ui/widgets/icvarwidget.h \ include/ui/widgets/inputbindingwidget.h \ include/ui/widgets/keygrabberwidget.h \ include/ui/widgets/mpsessionmenuwidget.h \ include/ui/widgets/multiplayermenuwidget.h \ include/ui/widgets/profilepickerwidget.h \ include/ui/widgets/savedsessionmenuwidget.h \ include/ui/widgets/sessionmenuwidget.h \ include/ui/widgets/singleplayersessionmenuwidget.h \ include/ui/widgets/taskbarwidget.h \ include/ui/widgets/tutorialwidget.h \ include/ui/inputsystem.h \ include/ui/joystick.h \ include/ui/mouse_qt.h \ include/ui/nativeui.h \ include/ui/styledlogsinkformatter.h \ include/ui/sys_input.h \ include/ui/ui_main.h \ include/ui/zonedebug.h \ include/updater.h \ include/updater/downloaddialog.h \ include/updater/processcheckdialog.h \ include/updater/updateavailabledialog.h \ include/updater/updatersettings.h \ include/updater/updatersettingsdialog.h \ include/versioninfo.h \ include/world/blockmap.h \ include/world/bsp/convexsubspaceproxy.h \ include/world/bsp/edgetip.h \ include/world/bsp/hplane.h \ include/world/bsp/linesegment.h \ include/world/bsp/partitioner.h \ include/world/bsp/partitionevaluator.h \ include/world/bsp/superblockmap.h \ include/world/bspleaf.h \ include/world/bspnode.h \ include/world/clientmobjthinkerdata.h \ include/world/contact.h \ include/world/contactspreader.h \ include/world/convexsubspace.h \ include/world/dmuargs.h \ include/world/entitydatabase.h \ include/world/entitydef.h \ include/world/generator.h \ include/world/grabbable.h \ include/world/hand.h \ include/world/huecircle.h \ include/world/impulseaccumulator.h \ include/world/interceptor.h \ include/world/line.h \ include/world/lineblockmap.h \ include/world/lineowner.h \ include/world/linesighttest.h \ include/world/map.h \ include/world/mapelement.h \ include/world/mapobject.h \ include/world/maputil.h \ include/world/p_object.h \ include/world/p_players.h \ include/world/p_ticker.h \ include/world/plane.h \ include/world/polyobj.h \ include/world/polyobjdata.h \ include/world/propertyvalue.h \ include/world/reject.h \ include/world/sector.h \ include/world/sectorcluster.h \ include/world/sky.h \ include/world/surface.h \ include/world/thinkers.h \ include/world/vertex.h \ include/world/worldsystem.h INCLUDEPATH += \ $$DENG_INCLUDE_DIR \ $$DENG_API_DIR HEADERS += \ include/precompiled.h \ $$DENG_API_HEADERS \ $$DENG_HEADERS # Platform-specific sources. win32 { # Windows. HEADERS += \ $$DENG_WIN_INCLUDE_DIR/dd_winit.h \ $$DENG_WIN_INCLUDE_DIR/directinput.h \ $$DENG_WIN_INCLUDE_DIR/mouse_win32.h \ $$DENG_WIN_INCLUDE_DIR/resource.h INCLUDEPATH += $$DENG_WIN_INCLUDE_DIR SOURCES += \ src/windows/dd_winit.cpp \ src/windows/directinput.cpp \ src/windows/joystick_win32.cpp \ src/windows/mouse_win32.cpp } else:unix { # Common Unix (including Mac OS X). HEADERS += \ $$DENG_UNIX_INCLUDE_DIR/dd_uinit.h INCLUDEPATH += $$DENG_UNIX_INCLUDE_DIR SOURCES += \ src/unix/dd_uinit.cpp \ src/unix/joystick.cpp } macx { # Mac OS X only. HEADERS += \ $$DENG_MAC_INCLUDE_DIR/MusicPlayer.h \ $$DENG_MAC_INCLUDE_DIR/cursor_macx.h OBJECTIVE_SOURCES += \ src/macx/MusicPlayer.mm \ src/macx/cursor_macx.mm INCLUDEPATH += $$DENG_MAC_INCLUDE_DIR } else:unix { # Unix (non-Mac) only. } # Platform-independent sources. SOURCES += \ src/alertmask.cpp \ src/api_console.cpp \ src/api_filesys.cpp \ src/api_uri.cpp \ src/audio/audiodriver.cpp \ src/audio/audiodriver_music.cpp \ src/audio/m_mus2midi.cpp \ src/audio/s_cache.cpp \ src/audio/s_environ.cpp \ src/audio/s_main.cpp \ src/audio/s_mus.cpp \ src/audio/s_sfx.cpp \ src/audio/sys_audiod_dummy.cpp \ src/busymode.cpp \ src/client/cl_frame.cpp \ src/client/cl_infine.cpp \ src/client/cl_main.cpp \ src/client/cl_mobj.cpp \ src/client/cl_player.cpp \ src/client/cl_sound.cpp \ src/client/cl_world.cpp \ src/client/clplanemover.cpp \ src/client/clpolymover.cpp \ src/clientapp.cpp \ src/color.cpp \ src/con_config.cpp \ src/dd_loop.cpp \ src/dd_main.cpp \ src/dd_pinit.cpp \ src/dd_plugin.cpp \ src/def_main.cpp \ src/edit_bias.cpp \ src/face.cpp \ src/game.cpp \ src/games.cpp \ src/gl/dgl_common.cpp \ src/gl/dgl_draw.cpp \ src/gl/gl_defer.cpp \ src/gl/gl_deferredapi.cpp \ src/gl/gl_draw.cpp \ src/gl/gl_drawvectorgraphic.cpp \ src/gl/gl_main.cpp \ src/gl/gl_tex.cpp \ src/gl/gl_texmanager.cpp \ src/gl/texturecontent.cpp \ src/gl/svg.cpp \ src/gl/sys_opengl.cpp \ src/hedge.cpp \ src/library.cpp \ src/m_decomp64.cpp \ src/m_misc.cpp \ src/m_nodepile.cpp \ src/main_client.cpp \ src/mesh.cpp \ src/network/masterserver.cpp \ src/network/monitor.cpp \ src/network/net_buf.cpp \ src/network/net_demo.cpp \ src/network/net_event.cpp \ src/network/net_main.cpp \ src/network/net_msg.cpp \ src/network/net_ping.cpp \ src/network/serverlink.cpp \ src/network/sys_network.cpp \ src/r_util.cpp \ src/render/angleclipper.cpp \ src/render/api_render.cpp \ src/render/biasdigest.cpp \ src/render/biasillum.cpp \ src/render/biassource.cpp \ src/render/biastracker.cpp \ src/render/billboard.cpp \ src/render/blockmapvisual.cpp \ src/render/cameralensfx.cpp \ src/render/consoleeffect.cpp \ src/render/decoration.cpp \ src/render/drawlist.cpp \ src/render/drawlists.cpp \ src/render/fx/bloom.cpp \ src/render/fx/colorfilter.cpp \ src/render/fx/lensflares.cpp \ src/render/fx/postprocessing.cpp \ src/render/fx/resize.cpp \ src/render/fx/vignette.cpp \ src/render/huecirclevisual.cpp \ src/render/lightdecoration.cpp \ src/render/lightgrid.cpp \ src/render/lumobj.cpp \ src/render/mobjanimator.cpp \ src/render/modelrenderer.cpp \ src/render/r_draw.cpp \ src/render/r_fakeradio.cpp \ src/render/r_main.cpp \ src/render/r_things.cpp \ src/render/rend_fakeradio.cpp \ src/render/rend_font.cpp \ src/render/rend_halo.cpp \ src/render/rend_main.cpp \ src/render/rend_model.cpp \ src/render/rend_particle.cpp \ src/render/rendpoly.cpp \ src/render/rendersystem.cpp \ src/render/shadowedge.cpp \ src/render/shard.cpp \ src/render/skydrawable.cpp \ src/render/skyfixedge.cpp \ src/render/surfacedecorator.cpp \ src/render/trianglestripbuilder.cpp \ src/render/viewports.cpp \ src/render/vissprite.cpp \ src/render/vr.cpp \ src/render/walledge.cpp \ src/render/wallspec.cpp \ src/resource/abstractfont.cpp \ src/resource/animgroup.cpp \ src/resource/api_material.cpp \ src/resource/api_resource.cpp \ src/resource/bitmapfont.cpp \ src/resource/colorpalette.cpp \ src/resource/compositebitmapfont.cpp \ src/resource/compositetexture.cpp \ src/resource/fontmanifest.cpp \ src/resource/fontscheme.cpp \ src/resource/hq2x.cpp \ src/resource/image.cpp \ src/resource/manifest.cpp \ src/resource/mapdef.cpp \ src/resource/material.cpp \ src/resource/materialanimator.cpp \ src/resource/materialarchive.cpp \ src/resource/materialdetaillayer.cpp \ src/resource/materiallightdecoration.cpp \ src/resource/materialmanifest.cpp \ src/resource/materialscheme.cpp \ src/resource/materialshinelayer.cpp \ src/resource/materialtexturelayer.cpp \ src/resource/model.cpp \ src/resource/patch.cpp \ src/resource/patchname.cpp \ src/resource/pcx.cpp \ src/resource/resourcesystem.cpp \ src/resource/sprite.cpp \ src/resource/texture.cpp \ src/resource/texturemanifest.cpp \ src/resource/texturescheme.cpp \ src/resource/texturevariant.cpp \ src/resource/tga.cpp \ src/settingsregister.cpp \ src/sys_system.cpp \ src/tab_tables.c \ src/ui/b_main.cpp \ src/ui/b_util.cpp \ src/ui/bindcontext.cpp \ src/ui/binding.cpp \ src/ui/busyvisual.cpp \ src/ui/clientrootwidget.cpp \ src/ui/clientwindow.cpp \ src/ui/clientwindowsystem.cpp \ src/ui/commandaction.cpp \ src/ui/commandbinding.cpp \ src/ui/impulsebinding.cpp \ src/ui/dialogs/aboutdialog.cpp \ src/ui/dialogs/alertdialog.cpp \ src/ui/dialogs/audiosettingsdialog.cpp \ src/ui/dialogs/coloradjustmentdialog.cpp \ src/ui/dialogs/gamesdialog.cpp \ src/ui/dialogs/inputsettingsdialog.cpp \ src/ui/dialogs/logsettingsdialog.cpp \ src/ui/dialogs/manualconnectiondialog.cpp \ src/ui/dialogs/networksettingsdialog.cpp \ src/ui/dialogs/videosettingsdialog.cpp \ src/ui/dialogs/vrsettingsdialog.cpp \ src/ui/dialogs/renderersettingsdialog.cpp \ src/ui/editors/rendererappearanceeditor.cpp \ src/ui/infine/finale.cpp \ src/ui/infine/finaleanimwidget.cpp \ src/ui/infine/finaleinterpreter.cpp \ src/ui/infine/finalepagewidget.cpp \ src/ui/infine/finaletextwidget.cpp \ src/ui/infine/finalewidget.cpp \ src/ui/infine/infinesystem.cpp \ src/ui/inputdebug.cpp \ src/ui/inputdevice.cpp \ src/ui/inputdeviceaxiscontrol.cpp \ src/ui/inputdevicebuttoncontrol.cpp \ src/ui/inputdevicehatcontrol.cpp \ src/ui/inputsystem.cpp \ src/ui/mouse_qt.cpp \ src/ui/nativeui.cpp \ src/ui/progress.cpp \ src/ui/styledlogsinkformatter.cpp \ src/ui/sys_input.cpp \ src/ui/ui_main.cpp \ src/ui/widgets/busywidget.cpp \ src/ui/widgets/consolecommandwidget.cpp \ src/ui/widgets/consolewidget.cpp \ src/ui/widgets/cvarchoicewidget.cpp \ src/ui/widgets/cvarlineeditwidget.cpp \ src/ui/widgets/cvarnativepathwidget.cpp \ src/ui/widgets/cvarsliderwidget.cpp \ src/ui/widgets/cvartogglewidget.cpp \ src/ui/widgets/gamefilterwidget.cpp \ src/ui/widgets/gameselectionwidget.cpp \ src/ui/widgets/gamesessionwidget.cpp \ src/ui/widgets/gamewidget.cpp \ src/ui/widgets/gameuiwidget.cpp \ src/ui/widgets/inputbindingwidget.cpp \ src/ui/widgets/keygrabberwidget.cpp \ src/ui/widgets/multiplayermenuwidget.cpp \ src/ui/widgets/mpsessionmenuwidget.cpp \ src/ui/widgets/profilepickerwidget.cpp \ src/ui/widgets/savedsessionmenuwidget.cpp \ src/ui/widgets/sessionmenuwidget.cpp \ src/ui/widgets/singleplayersessionmenuwidget.cpp \ src/ui/widgets/taskbarwidget.cpp \ src/ui/widgets/tutorialwidget.cpp \ src/ui/zonedebug.cpp \ src/updater/downloaddialog.cpp \ src/updater/processcheckdialog.cpp \ src/updater/updateavailabledialog.cpp \ src/updater/updater.cpp \ src/updater/updatersettings.cpp \ src/updater/updatersettingsdialog.cpp \ src/world/api_map.cpp \ src/world/api_mapedit.cpp \ src/world/blockmap.cpp \ src/world/bsp/convexsubspaceproxy.cpp \ src/world/bsp/hplane.cpp \ src/world/bsp/linesegment.cpp \ src/world/bsp/partitioner.cpp \ src/world/bsp/partitionevaluator.cpp \ src/world/bsp/superblockmap.cpp \ src/world/bspleaf.cpp \ src/world/bspnode.cpp \ src/world/clientmobjthinkerdata.cpp \ src/world/contact.cpp \ src/world/contactspreader.cpp \ src/world/convexsubspace.cpp \ src/world/dmuargs.cpp \ src/world/entitydatabase.cpp \ src/world/entitydef.cpp \ src/world/generator.cpp \ src/world/grabbable.cpp \ src/world/hand.cpp \ src/world/huecircle.cpp \ src/world/impulseaccumulator.cpp \ src/world/interceptor.cpp \ src/world/line.cpp \ src/world/lineblockmap.cpp \ src/world/linesighttest.cpp \ src/world/map.cpp \ src/world/mapelement.cpp \ src/world/mapobject.cpp \ src/world/maputil.cpp \ src/world/p_mobj.cpp \ src/world/p_players.cpp \ src/world/p_ticker.cpp \ src/world/plane.cpp \ src/world/polyobj.cpp \ src/world/polyobjdata.cpp \ src/world/propertyvalue.cpp \ src/world/reject.cpp \ src/world/sector.cpp \ src/world/sectorcluster.cpp \ src/world/sky.cpp \ src/world/surface.cpp \ src/world/thinkers.cpp \ src/world/vertex.cpp \ src/world/worldsystem.cpp !deng_nosdlmixer:!deng_nosdl { HEADERS += include/audio/sys_audiod_sdlmixer.h SOURCES += src/audio/sys_audiod_sdlmixer.cpp } OTHER_FILES += \ include/template.h.template \ src/template.c.template \ client-mac.doxy \ client-win32.doxy # Resources ------------------------------------------------------------------ buildPackage(net.dengine.client, $$OUT_PWD/..) data.files = $$OUT_PWD/../doomsday.pk3 # These fonts may be needed during the initial startup busy mode. startupfonts.files = \ data/fonts/console11.dfn \ data/fonts/console14.dfn \ data/fonts/console18.dfn \ data/fonts/normal12.dfn \ data/fonts/normal18.dfn \ data/fonts/normal24.dfn \ data/fonts/normalbold12.dfn \ data/fonts/normalbold18.dfn \ data/fonts/normalbold24.dfn \ data/fonts/normallight12.dfn \ data/fonts/normallight18.dfn \ data/fonts/normallight24.dfn macx { xcodeFinalizeAppBuild() xcodeDeployDengLibs(shell.1 gui.1 appfw.1 doomsday.1 legacy.1) xcodeDeployDengPlugins(audio_fluidsynth audio_fmod \ idtech1converter savegameconverter dehread \ doom heretic hexen doom64) macx-xcode { QMAKE_BUNDLE_DATA += exectools exectools.files = $$DESTDIR/savegametool $$DESTDIR/doomsday-server exectools.path = Contents/Resources } res.path = Contents/Resources res.files = \ res/macx/English.lproj \ res/macx/deng.icns data.path = $${res.path} startupfonts.path = $${res.path}/data/fonts QMAKE_BUNDLE_DATA += res data startupfonts QMAKE_INFO_PLIST = res/macx/Info.plist # Since qmake is unable to copy directories as bundle data, let's copy # the frameworks manually. FW_DIR = Doomsday.app/Contents/Frameworks/ doPostLink("rm -rf $$FW_DIR") doPostLink("mkdir $$FW_DIR") !deng_nosdl:!isEmpty(SDL2_FRAMEWORK_DIR) { doPostLink("cp -fRp $${SDL2_FRAMEWORK_DIR}/SDL2.framework $$FW_DIR") !deng_nosdlmixer: doPostLink("cp -fRp $${SDL2_FRAMEWORK_DIR}/SDL2_mixer.framework $$FW_DIR") } deng_fmod { # Bundle the FMOD shared library under Frameworks. doPostLink("cp -f \"$$FMOD_DIR/api/lib/libfmodex.dylib\" $$FW_DIR") doPostLink("install_name_tool -id @rpath/libfmodex.dylib $${FW_DIR}libfmodex.dylib") } # Fix the dynamic linker paths so they point to ../Frameworks/ inside the bundle. fixInstallName(Doomsday.app/Contents/MacOS/Doomsday, libdeng_core.2.dylib, ..) fixInstallName(Doomsday.app/Contents/MacOS/Doomsday, libdeng_legacy.1.dylib, ..) linkBinaryToBundledLibshell(Doomsday.app/Contents/MacOS/Doomsday) # Clean up previous deployment. doPostLink("rm -rf Doomsday.app/Contents/PlugIns/") doPostLink("rm -f Doomsday.app/Contents/Resources/qt.conf") doPostLink("macdeployqt Doomsday.app") } # i18n ----------------------------------------------------------------------- #TRANSLATIONS = client_en.ts # Installation --------------------------------------------------------------- DENG_PACKAGES += net.dengine.client.pack deployPackages($$DENG_PACKAGES, $$OUT_PWD/..) deployTarget() !macx { # Common (non-Mac) parts of the installation. INSTALLS += data startupfonts data.path = $$DENG_DATA_DIR startupfonts.path = $$DENG_DATA_DIR/fonts win32 { # Windows-specific installation. INSTALLS += license icon license.files = doc/LICENSE license.path = $$DENG_DOCS_DIR icon.files = res/windows/doomsday.ico icon.path = $$DENG_DATA_DIR/graphics } else { # Generic Unix installation. INSTALLS += readme readme.files = ../doc/output/doomsday.6 readme.path = $$PREFIX/share/man/man6 } } doomsday-stable-1.15.7/doomsday/dep_core_cwrapper.pri0000664000175000017500000000073012641367670022212 0ustar jaakkojaakko# Build configuration for using the libcore C wrapper. INCLUDEPATH += $$PWD/libcore/include # Use the appropriate library path. !useLibDir($$OUT_PWD/../libcore) { !useLibDir($$OUT_PWD/../../libcore) { !useLibDir($$OUT_PWD/../../../libcore) { useLibDir($$OUT_PWD/../../builddir/libcore) } } } LIBS += -ldeng_core macx { defineTest(linkToBundledLibcore) { fixInstallName($${1}.bundle/$$1, libdeng_core.2.dylib, ..) } } doomsday-stable-1.15.7/doomsday/gpl-3.0.txt0000664000175000017500000010451312641367670017640 0ustar jaakkojaakko GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 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 along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . doomsday-stable-1.15.7/README.md0000664000175000017500000000541612641367670015461 0ustar jaakkojaakko# Doomsday Engine This is the source code for Doomsday Engine: a portable, enhanced source port of id Software's Doom I/II and Raven Software's Heretic and Hexen. The sources are under the GNU General Public license (see doomsday/gpl-3.0.txt), with the exception of the Doomsday 2 libraries that are under the GNU Lesser General Public License (see doomsday/lgpl-3.0.txt). For [compilation instructions](http://dengine.net/dew/index.php?title=Compilation) and other details, see the documentation wiki: http://dengine.net/dew/ ## Libraries **libcore** is the core of Doomsday 2. It is a C++ class framework containing functionality such as the file system, plugin loading, Doomsday Script, network communications, and generic data structures. Almost everything relies or will rely on this core library. **liblegacy** is a collection of C language routines extracted from the old Doomsday 1 code base. Its purpose is to (eventually) act as a C wrapper for libcore. (Game plugins are mostly in C.) **libgui** builds on libcore to add low-level GUI capabilities such as OpenGL graphics, fonts, images, and input devices. **libappfw** contains the Doomsday UI framework: widgets, generic dialogs, abstract data models. libappfw is built on libgui and libcore. **libshell** has functionality related to connecting to and controlling Doomsday servers remotely. ## External Dependencies ### Qt Using Qt 5 is recommended. The minimum required version of Qt is 4.8. See [Supported platforms](http://dengine.net/dew/index.php?title=Supported_platforms) in the wiki for details about which version is being used on which platform. ### Open Asset Import Library libgui requires the [Open Asset Import Library](http://assimp.sourceforge.net/lib_html/index.html) for reading 3D model and animation files. 1. Clone https://github.com/skyjake/assimp. 2. Check out the "deng-patches" branch. 3. Run [cmake](http://cmake.org) to generate appropriate project files (e.g., Visual Studio on Windows). 4. Compile the generated project. 5. Add `ASSIMP_DIR` to your *config_user.pri*. ### SDL 2 [SDL 2](http://libsdl.org) is needed for game controller input (e.g., joysticks and gamepads). Additionally, SDL2_mixer can be used for audio output (not required). ### FMOD Ex The optional FMOD audio plugin requires the [FMOD Ex Programmer's API](http://fmod.org/). ## Branches The following branches are currently active in the repository. - **master**: Main code base. This is where releases are made from on a biweekly basis. Bug fixing is done in this branch, while larger development efforts occur in separate work branches. - **stable**: Latest stable release. Patch releases can be made from this branch when necessary. - **stable-x.y.z**: Stable release x.y.z. - **legacy**: Old stable code base. Currently at the 1.8.6 release. doomsday-stable-1.15.7/.gitignore0000664000175000017500000000056112641367670016166 0ustar jaakkojaakko*~ .directory .DS_Store macx_release_build /doomsday/build/win32/license.txt *.user.* *-build-* *-build build-* *.dsc *.tar.gz *.deb *.changes server-runtime client-runtime Zcustom.conf .web.manifest doxyissues-*.txt doomsday/apidoc/api2 /distrib/win32/vcredist_x86.exe Makefile.Debug Makefile.Release tools/*/debug tools/*/release vc100.pdb *.pyc *~ precompiled.h.cpp

Je2  ڪ$}t NOCY|.T0OO9ؑ `:2;>!@ybxT4d3P1&K{uQED`PȽ3z6_6Nњg ~ )>_a~?d+9vcC8<ޏլ|@9!Y ֛J" aqGvDxbx%8 WQNEh6P׹2gJ_W Kb F5& s*@±LJz>wh a6p xڤ0W-280Nd&Sѯ c aSA@Hl;LwB.sE#/(_0ͤɓ]z)XO 2}&Gkd~=&.ذ}<<9ihH0;MRSw2rƦk5 6z>ٸͻDqY`:3| g7FïW9r.dJa>>o޿l?W>U?b?}X?D˿GU8Hr J"*Pi|U6̜~4! >N,#X'psL9vv opI8lf)shs"h)oY l(exw$#b2v3p~Bs$sI9"`u+ 5k,t ̨a-#W $f#vZDe …02MJ_v(PKcNAf6I)Q~5pW'\yE73ҥX ч *5v&8s&P,\L)2r0?iZ1#3T32_ΊUZdUv{,WՐۗǗǗG2=L`y-h.ۀYdUգ[?#u\Vٻ{I&=Ski,Xi-\;/m<+pFOy| VW9eH ,-Y3% [p5Ȏ){igmysڃhGGܦA<Ƒ폛#pBlXpX+:;T1O}C=;6PEZ^tw̦U$a~|" m/d6FSbn~8u{=S\qCf9zZbԓ\1cwPWӔ.G eh\»c0r=` 3) 4#t6Ξszf\[Aܑ,?'@( ~WfĆ"ťxINף n):zsK~9k;` f4t-6]];Nu aw]GDuTc^ӟEВJi/FyR(oT1&zy>>/ 2 ![-:]%7ow_/5U7{.^Ea2R?ԌXmBs*w8nmx>YLBbz)uzFW sp.g$/jaBx3ؓsdR')vwjV:RxbS ٗC0jL\ Jxx4'(ln+?4[CK0߬eAd%O"XD B6 5̶ ? 8rE3~j17L_ ǔ8go_|&ţ fqP=ɔ],tU$XgY =;14MRQ'Lg@'Am&35+9)|7Ů?|zzx|txw ڕilۇ]M;f-lɴcjFqء=j&m~c??pFܙz]w۷?7o1u2_a`XXoe=;x=^H)$zMmD'a`Z舮$DBv^5+eqUs9W )C _ysQ*r`Hq[M0ivFeI+`5}3崙?XM0 g#C %̐Zʩ ^r xApKijEf8RDl3{6@/r}@PY =TYY~Ăz"k3 -Qz!~JpS'7_ b/^@#Aa T8HNML 0nih63I]bӸ{:_eu/쮗K}~j~WvM8yy,oW}~_w,LeTþ\QȄdg!ct8qYg<5H;JTXShJ]Ds&p/rW {%7I|h*.7,Y!._vi|& q0ĖL͚?+YGU #I#]׌]jӀ^T=xbgg%kUtMi*_w@T $W[~%_mp,U6("Yo'}*#@)՞C᧖|ȁ{a(N5xp|ww&N} `m[#Ke:UST]C=M2,y /Wjw&xshY~e?tZw56vQޭf7~,޼YU˦y`Gu1Qj<qg[+ɷ;i 9;H.-zzݦ˧mXu/ pj?|6SmYZb~X>,7CRz]#a`}DՎz`+Ȏm;{x1O} rG J[Pcaȷ*Ai_ ؗC8A i9CI۫tRS^pavg7819f%֤XKy&vF}]i-Ϯ$#ȳ1p!|op,ad8!꫸?JQ]qXjtyW噵 jSq|Z.% 3' +#_r;L#kndY#Y VvJBp|V2;1XoDx<딠E |+ep\4Eu'8 &LR-p g堹;/0t ,/.º EXϣ2]Gj`+TunH؀a)㛻b]"/ַ߬{;!c36㮭Uv]{, nCTgu+wh~i-d.ULv#''dG}T2c9Ԍ&ky} uN{xz\)[ U`\&rCBZߞvAj ϬBްM|:J-P⤋Wh5΅9fQs KA$EQ‡$$7^2q FJzSPN[LH&D.Z^|>G5OoaKm\QCĈ$=AECybÄ~jONOLDP7_i( x{(09sߦ*3F"Q\E-y 9(aYX@YX/2z"?1?§i3ؾv+<_7oͻ Ўnf욭 뇦S}{IX&ſX>c*C=Lp+ 2`75P$'N@fG` ֡lJ9-WO1*KéKDn9=嗮Ը́sgwEciL_ȴ0AZ͎pG~dGCO\}}I ,bb\48a9Rpm08FE8 ƠWs:?/Kd^.2/I_H+M8ZZ!oaN";zs2p՟>~zT{1)DF_!O!hà %#9rn"pc~7CxhOuǚU|<~KSJu Ⱦq}Wo~yۛļrУB #ǮU@9Ӵ#Ң-𘢇Gb8Ӎ+iEULv3JVK L S*#1ǘ80W-&9x#*l6\|r8Sڲѕ2Ŗ%8N𒻀 `g][6b#f#;[3NOdb{ܣ>;Yq܏HtM¢CK?S`%QkN)Y3KCuօ ɣ}=|: ?WnDվ"];g~ܾdxG͒v-kqhq|ُD-M`R4`s`QBtvYʛ[FF=SŠc5`R$#AG09`AZ4)&J/(9%:T.pG zMs'#Nt*)ef*5 1*x{akF7oƵ}zƯ] mj .i:Nch6EJTteUʂ-WZICB`xhݫA_sSx΅5ݠ :]=zO9^f0/iOn9"2cpNWvܳP(nK`},(iuy3Su ց.b?^V*5ǡ>vuն­AZDEX^h~kNe"{܄=kXPeӧ6G4 %h&E`)5-&@/|EI*Nt=}SW5oHg/8@&H:p9@a‹Ͱ,wʘ}XmmB6D [CoaKoo+L\VY&Lfc,Xz\J<2rM⭂.@ӭhhh4+ ʣǚr_ axfICL|/3/~ß| )ʙ)<5:poO3M㰎* i{,C،s"}lS3쟎#NĀ"Z-"[go?8XOf/cѱj=XWG7GY+v㞱Ս[ʱi' yUH;7e1^l@6g DQq ^Q)MhT&w'dw ~ڡORz~_m)px| d㗜^`Hits{ O"qCA$ER[KQ=4:}>oWx(R4(ŚSvA QQ:9#X>}iC-Ald3k60$tJPB$ $$ȓqHf_/s.*x"OmCn@/fS;؋z8<$al?hd_}|G!{E^F2w7MXSvXlimm"^Q3ӈ;VS9aJ`?gald0K}5!*F.6isuâ\p>` /ڇi% ^=Jhm* ft +i@0(NA ǡjl4=!p:r${#v nLo|p)$p*|Q /!:!ebʲA!~r+dBƐ .(>Ұí"yfpKH6}M4Ma!ꇸ;jSm Yg@(`F^wծnD)YnކMݍrLT15*.k2J!! Vd\6d?H 曻:"ȎAP4W=A<5/Nwbosd<Ġk ~w! *'?x߃b.99z2R>~~ُ/4z;SSbɗ QEcF.hPl̍~ƴiAamr54sF $͉Z)Nvϳ<yLaSw1|)Lw$ș򝝘55|a0eQmִ<ͦcQ<ʗC7a#jg8nJ6IʇæP!1,&/,麤K8"헙C].~^*rirӆ%KrWkĐ4Ӥ@ZR*.!ȓE4QHzyst|=01Dll j| vpH [#E X@IDAT]M? n!d{ׄ3Yѱ(gôٱA PO;9/ڦmZj~#_..^GX|=>b}U2V! ~}p$'y#mgFb! oݳBGNBB 8t iVAiTw&t%U5>ջ8K8wAӄ|z4]Le !0nBcT^=Cx1*ȫ_X-øC?8K.%zdl_v0WQuUp\&,-״8&` ߲72{L۬oӁmз,iJC͉i/dpME&Ti*f#7ŋ8IKЩ$1?P;Þ>]2!S(i} "1dvCvkoHBn~p c*殐Y$:zH1̈JN#r:S/և"NK`Glq7̋yjˏ^[+mvgZ;FxX{ߢlw`?ͺh2BDxZuHoa~B#BMHC@cEÜkWذDGf7R !M QԦs70[qٶzM%8\%8瘪P #X{kȃ3wbO˗Up+B-6B֗c b9.x|_>w1I36fI1Oߍ񰌎YRhvy5aU~9ԾMBM !ñ6NcudL!3I)a E.2x76oizS;o&7|o_0?O ." ws˂j?33&W&ExSfUxt0Hg`;QefYhqy̱ 6l0,uY#VuZGVf1nM<_If!* tf4sWdNEmƪXdzq6o *638C5>Nd`]ӪZtIKƏ'OizwY(}qx#S>ϙ ^`p|(.a:@ q՚DUp;]G0v:AW E7Y%gIlxyoXw-EK~5~&ٰEZdR-4-lwͨ a-@m aRSI&# Op CL)ӡӢN9E;lEzu,jzZ"\{MiȎ: »28Bt5>"?5nJQ2{*FȎ*&Jb֬e)ڤcc>џP]Pj 6cnk,F>.w0?# {ە43 > J{|/[mÝÁ4'Db6SF 2EA`3M,#Nu mBvG>爴{:zi?Nb‚osN19S&hn 9S2uLwixwY)VmF4}vC%U]6_*cV*iUhl| K=آga4u밞ѫ7n[Yܭu|9lwiD@!d64| 5#4[m ˿؃V`#/$fd̅)#4~MSp$s{6XCZ>^pڅ^XP!~³K,0gy6+XG,@vUP)(1FÇSUL&oFjv˂2ƃsml6gCi1*ﶈɧ NHZ/ts?  '@'A.t-F=<ib%UHZk]&/i1?7 l{:}Ɉ}t^,a@vta[m?6V:',qg#f⦼CIm X~M /G RLڍF>r^ʊ Z r}r!Ց6\7Ti|k9{NjF xmO5 'x͒Oycb/+npWDBh_]sKLP tm|'w r͛ =g\dz,_6619E$_-ʩ3nY,Yggi6竀Gw3A^3Kuhٴ `x'iEQeUUsC)z}y_'bm`< Cl~D/wv1Ex|b}X.7rYJ̖)"$vQN{V1)cx`F!sc)*N{ :GdvY֐J4BxOfONk]T"#Q͜ƞɝޤVNV IN9$&8I *~1Ln6b*%#Kgo367r?"#$&ҽbga Ca雮C(k;y5e<]ZYf(kl= xʣ"~-iWuM'&SH?/Ch|'|{d7)Bc#@jz%}ϠA`pv ݺ0%/rIB|h>U|};{͜mb3 gBv±M zEs@Q<ȡRckӲOf7Bv6pL2 >=&Uϡf%sVJf;MÌD0>kUGv)(NNKooz/+<j5Skcz 3zu$m);M'j;wxRD; @rD#a[e.QDҽf}y-[K˾- w Nif̃q/:+p4اnoYn&{w=˙fΩW+G*3W$u%FMnPF0 ȱ?5 Lෝ '&u ܡklЉüoLQc c )Qc7EvN7c (cf^ngڄo;>YgaFdPŀֈ *=b;{%=;Z,ltv-F {GHD^nж:)tCO`>|gǾt&MFh"Epx A(>C {!*Phw6@좦#;N43(DmB6wB9D29.N'~pu:{wI>3d&QGӚ5[[=ud^ќLz& D Hpg=lhn>r:Y7}KC*n<%V k|y, 4"0lT6uF`wbSY=&: o#!CfkY?ܰ?)9퀄ͱ4d鏙g7fR_z`t=Q{IcP}Xi{q*a`RMs8T?hbtFni"=(Zk ]!:EH%"mseEXؐX\vwWc_?aٌhʢ] ٟ8;A\Q1t-:%p_mI2WpzA/ a K'h>3>\K\)!vl8maƇ`@to1O٤G77k;TVa(ljΝw(A^;h& Ѫ@ڏ;oǝUs܁MdN@Mߑv%<0Y;ޜ" ߪε'TS?'Cn= 78ӿnr#xԂ ZS }~"{~{X|XZʧ(r|1Аtt/t$3Yl[b]~-~qu9QѮRh%w$2\ W}D6>$3򏯁7o}Y tڷOZ\z1QcG6G' W(ۼVCe3Ã3@#&a}ϒzuwL9v ~xleP(L)`R $L;\LugRxIL l/;0MXērevzb( Ll%dL g{Xx=XLSwD8dx*:?h@c;sn@WMIڳW2ð4 uDwh Zoݘ?dz'g Ⱦa,ƔaޞP #J\ӆrj5ޮ: :%=N<-#1՚Q[q}9\湑%yU!]NO_Gs^~gad*4@XͩSHf5|6nr[g^F=ž ⭯͛RfMcv ,MxnkS6>g"[b%hu%dVLa=ݮ]Jq9&]}H'.\G?ltRuT i̋@С(U㚴*z9Ia4?tǐ}L"^FvJ7}f59L3+? rx \ %cvz_Ow;vsw~<>] sw. [ԙ>*]!ГmD*C+D\wYwOUH+oN?&?/jmw[pxnAm*A\:Gw,\Bz(Gpb*Qg 6'ONb,H*cO.|b+,NF+2N~.m!Dȋ%z}ۧ;VizpE6upms4ކr+ۘF$_h<[؂8tL Khd/]5ŮNqv Us-FpG'^J}KŅ [ C,g|6z5D1|emվ9 Hb6b/N>L>읕Z'89KK ]g" 0z^n)axyuzxAjM>쏏O}-ˤwLdzŘӿ9֗٘O#fA|ݮ6X!;Q5<{qxl^*wF=Z@g>]pu?;?#j`pu՝Ruٺc#֥#}8ޜ+ox@5/9;A!ݞ>hT/I'NJ--%ه5Żcta~٭Fn!=7“pJYI<)E{z 8'4Ks@*ߧ󏉣dsH;TnH(ɻq۞&)O-zwxyix/y G)aG{fvD]2 ;u)χA]mڴXYZ+x ٟVϏ+™,}[!~x?==busEІ[q2;6W,~|q&\o dQ:Gפ1^<;pWL]Xɠ)LR?NGhI'L-ewObá*SѪY S%X"<,X06#a#~/(kwaǴ6AzwgKI,=Z ƎR喇J/e@|#񤯺H {EÑs_mʿ)>8UMG:rGxA 2A`Bx/q XG%QWapY=/fcʔ( rDM|c,a[_?RnV`)?%dZgB4L>;=MgW;3"u)y3g{̊m.5 ¹e_lLD3 ;h6qSDi>a)Yr@7 Ȉ v2WN\bgzn_ tMv%gx.҉?\zW7tÿV2 "KA%D@wІ! 0 Xz=|gR@FWpY_;R l}EVъٻ96JfzD4xvt;^zg`=܃;!m7c*͸A¼笓K7h|8TCD}U?hWٷ57d۰F^˪dGOcy\5τijaa/񽞓=h\nT9qٮϾ - -r8:~r)uzʭh9VjIf퇛o}{wvTXw̨$dGRFÐ.Ѧt%cET37HuCZ)яD=uJzv{M]Νޘ҉U/ԛ*c5J;]⚬+Vt~kGk05Oqd)7m.Kzs1Å^>!w9!{&WFNP9D !Lp>|7O(}!r azI;V'Y/<>#w{ DSl# }c:ѫnahXPnν ^ 6^:x . C/CGꮫRPnB( ;Arm`,=ޕՆ_VeƔ@})G+z>Lwm IsdXxް~wFO6#33Gώ>?\\?^;:&{>\Q{gvdg`tqёgLr\>.Yώ<" =sH}|ш/m6nE:!4\.Q#-uc[8H 0֥m lC GT6:@.Uv7HJgHo̿ط9h!/pWM݌uXrKD1p\;!wx{C:D\Ā)ˡ]zOd,$I6M6=akՠg-)<=H>Gc&s*=w(#=3Ұގ砃LpP_V?+(b@9.>_ݎ>먯Lx"oC ?P4B]_V t _deҭRvG sd{lccugK|cZK>\<D6RÚǨOwN-yeiwG9^t&C~|썾^U+~aaɱ*#mWoG^R8O5ht߭mxy )_9nt7R$>H^YECf3є}z$bp2)pO2bt ^b ~ w{G1ۢ.M3%b5,|0X60qv ɨ2e@s;۝-K/#:0!Ѧ*Wy~˟K=F<r]63 w /V-LUusEoD/X-!o5CĔ,< %Y춽0<;#`jmdpa/' Ng!AӣCr02SOoA=(Q-˒lDOtW*_Gkє #͐n$Ϯ8kq`IZR?$Jwt>&G^ow kSOWTzts?"WkѲu\Wt{ #+|cI~j%(|w9GŢ6߳BV# wD4͎̄ʎżhdwv$C\vDs!Z&&Un=u/&\)vX|ؓߩ@K_\KT Ӕ) L(!-QTF˯,)_/AMWq:}wV0쐝`zi: ug^X4bӊIX &5 4o9sWW~Sֳ4w~z:{~dr}xst,$`[ASΛ6MTp /_>}p<>Mh5G]dF .tw?o*|jDe(&]bmsuR'U)33n204,] ,ϦR_ӇfQ[=8y0c?m- "j--hV둵K~m:ADot?ŘNC0mx>=DHpsСW} )!;U_icmԩ5 oCw3oImZ3h[N1}[š1 ˨G /ONgfdTkWw;[{o_/..4:tovdd(dyntn]gA-P@e!nhӞ %J4BC _@<3ފt.GZTEȗJ![٭.s~ˡ?Qd ?D)C_MyT>:vZPnF$:ε ^y#^s/yjV6xl]cq+yDeŤe,d}>G\%B؂C7= ۧ%`ĉl>>}&`! K7&Flww#jNTlHswN$#/gdZvct@|XԓSr%bvEH|܇HC|wڢ#?Y,G*!6P 7 lWq% p-D1U&qDRYQMU۔Y"; ;>pg~ru|qٟ!;=?O_~xqi!P;ߏ8as{y.S~~>+g9ܯUtgDX$d!R2锐4xˀ˫Jܢv"`֛_,DPLњS4df-9۲}Ų#qYCn~8cv?8J`pQ }M~ cie#N+c ʟor4:?:ͪ+`#ډG1@ 2RX4|O7Ν zr>' R0K4otu淋/WOwќC*Ўu{n¸&Ҙj%_77}X?<Nc^MrZ'$2fLO8P&_KjIGHLD:Oywvͨ);#_C6~ysH]r1 )&DnI|>dIm`p>(#ɷG숫8:BD27ԙh723`{f˰^nm%j ( ӌ;T"*mwcG7+]2k J-/EjMu۞oQ$HcA@qK]R=9+侔:t)";*E~)sGJG'#fi!N]);T\KRAFni6C7ddzZJM Zr&qqd8 /B]]vqo(R SIgSO6OQ2+dwj9<c$0]ta: uIYcƎ'De}VYWLJoz `uL(OR{[ Mzk{5#oi>ZJ~ ^ܑGx$o/xg/;.3óåC#"K靹dĪ]!YDCpTjׂEa 0 hRB6[)˾;:[Ӑ=`lnh 1HZ,h?Xx_+{^TmS2vڒ`Ʊ֫'#AlhnJ` ktfb- .HZDA8 H)sL,&ia~ 1n4*p/:AỦrG7F]f Bnt g>kOU, &Aùޥ[Cǁ8#SRsfRVejѻՌ`|rtp-j7[n9,NqzQ|JmO7A/O{wpuW}4/d,onH!7eU77W6W_67N#t%Mk!g㔨,{Yy^s4Vb)3I.GY LU'+m(\rݪoΠX:hͥ \/=Gfw w:}ozK蔮!o;@}ٯ~ߣ2 咵fIf2{Es{!l8R&5y=%~\v)l,.X{ت#㓸`w.PP|F {i{aXS4܂ԫ5BYfuGnrGJ7"Ċ@,$GȾs^)pw^і썫&[ kUС `UwS}ktsg9Tp-Ňxۓ7^+5/^[x &]z`C&gG_NxÏ[- 4>&p7Avq2 f7PZ$)L&7;:G2CXw /(H]|_[_Y'8e in:W^loKMWgrzDwnnM)N>$vm֐O;<8 ;tW7w vzgno4pKP3.n %w!P kPp^fVslW-uhNF@6"f+:(0cv|g)5N,nf3 > 1LLLjGi쳯%[_߱l|:Y\3N=ͳs<+NR8tu\<֯hv &m>F'@IDATK^ei7E&:%huTwk JouNoӇ)\}{yQ^ ~4#yש ,oyig z}}}^i 'J9Eч2mW=^)« 4 ^,RQ1"sd|clL/J|z<"Ao~t2T/IjX*ŜRy4nNQ0ܪ,З q}3P[}ʐ~|ùCg#QN~i.o %(JxY>@&Z ˍFtes$bWqiN?`\^ 12'T/o!^\@HW n=n&ma愉yN7Dtø]p61~*BCh?|OW_;d|;xtٟ߳̅'4\]_?D>B)Yv{fvP\GJ}v\}#6Th2_ֆb؉n@%MgG^B {Q3:$ ;&ɑ%{t=:"bC"/uxU65F_6P4\͗gI\GHoCw !u:OzIեDS,vlHDťEzy[4LXz0~\vքbt?Bv8Ufvo ow7ja|^h6/ .fy*+M1hV77O|j}miY-&T'1ŊQ.ј-ov&mg⤓CsnW9Avug^<?H""xL1-,P_/XnmeƺjհnDV-W ^ l\,ӭV*>[.CIl%vϯO܅Hl#Rܷ:%eB]U{SقvG^/i յ4kzT#;xHj.äc7tzr 6[ç&&fO8"xCx,YE7g DNg8ny΁+r;KAqU4ŜS|aK-%{E"v qí!^i/:sUutM~ù3t'?\FSʘL;qhƙճOf㏇_ܨW5 31FO<670/O_?~rŷ'<{VL c̦`ƹ>+3O 98>-ss[ -//ONùۼzPj=3c9V3{Viᗲ7݄夒y݇~^ HEb9e \dndW9-2Ja=3@SXdwvKN+!ƌԈól2K˪zyRqA~Ks >.G<|8얟܇HrOjtnpBl "Ph> q5{`l4Sy%.{$Ŝ|w a[ ]1ҋ,~ʔ/[̨x{#d4nҌp< jjRb%MxŋNgb4&RyVbĉ-q峷D;<;ϊÌE_@ zt|߫VံiFauP?D["stAWj{QCh^ QllKn]g~,pjdt<|ݰ &CoAcȾ/-8@<f%1 Ycnb`nv/xcv?-oLjjd3741t|uɻo'v2 V*9wX<5+iHQPAv*R6IȭبaRm*DY3 _j{559}8TNK3v9>&ҽR,]l;seLJ 8EN٥osمt9ōj"}A_V*MAtݣRMDbgpǼGC/&:;J8K;6'2)3X/%_لFV7-9VPGwOIFLc:fe ߳wԽlj-2AdFy])#{*eP^wZ69O^pZ,~%J )\.&:ABvh"HD$}!w՟t!3Es?g ]d RBۇ\=nA^dGF7.Ҵ 78o;L\2:d?>?z/fy,?Av/]g~#|Ex>m`p|1?<ө0~z |NqM)auD,ʎEQk:™G|>\& xd@M r.ّT ;}qΒ$گAͿG;7c+L^[g߉8)%םJSlI*Lwh-'^߻决}tOWt]}+ Yk6֙qk :5Q.>~݇G³/[5˧:dG㿜L~=Ŧq+ˀo_<=;јpo1AIzyh꩷01fUʧ,] ؉_L9fk#nz ˔fNxO^ͤ4}^d7ˎӑEWg(A<Θ#~) ͯm{ Cböc؁;My}r$ P Դ [O5UM *s;#*)|SD/"8:swĹܯl(w!rM|x{ym.,GP?-O5VJPՂLKݧIϏ$:`8'kQXL 26+aT:\쟿{cj<g)Zk0x} W`_4dx>NK_i7Z!!Q0Ve̜Rz*|)kd/؄ 7lؠZj:v{8ٱԝ2$1T&OS3`:)>^"g*4&];{װ:xנćsE'd3!tJp~;_#U f˹R%`B ̐#b4 .6*lpRgו|v}[}\}dǑo>8 mí<>+t00^?n>_}ͬɂu,О$]\Ӹon.@2ttwyG6ꙬIݞUQi d(~͘'*dG G{6i{h{-Y . Q׌Io ڹ+2{PxǾlNսݼ\j@a٥tA6eAn|9;dl2<2$z|Ey 58TB{JNqɭs6;e6ar2)ƴ/o]rͼۘyⲵJL~;Y %aG#~#>G~zo?j0xۃZy`Y0.~fRZ||"jMPK)!Ic<  bwJ^1NmݣS-N-|W%V3[&;wL ^P^' ʼ$wdxw o6pH>P yH頱ޕmhPޭ)&>,<Ϲ7̇ՆUtH4Nx|~LCxK:7_zL%z}aԶXu/+UI٣p˪C{@ϛV3jGcҾ'K{} '7_쟾Ze{F,##0ϷYL |R}>>Ёw?.au1:x?%eЛZbU* aW]IC։ d'֗G ciWdn֘.=9ENS+܉]l?jj BŊ$v!҇/̙H U31PܫT(tJK& p nMIflg{Of4D!gxtr][Yvڱ^[LyP 0_@ʰ!+ AN˹N# ;=?U񊰉<Ϡ(Ҟ.:ͨDŽI/Ut֕܉c,$E^ze1>sJ=ٱ@>"-_P :Yi[ck@p)_y(n ^pOgd ⣍Ov<"դy5%ؼy+7wmw[Ȯ"A#?[OwV?] /]վnn'M5\vk%sa?gpod8<}գ%~6\$tRÊsO(e `{< xc3(X?n Ks/=O>(bwNW^'Ndz\RrXL5büG8먄-ѐ.gp{;.t7|kp.A<ֳ#.әYs*,bY!y-Q"~w;Kc"n1 [҄M' ЫLBA*AK}GAR( 7ƮW:?i;~8 #e΅t؄g4AD113;>殕Hib|;XOtvttzo'U!ǃ}Kպ{maw=fn~pNSs_oH\C?2F,uj __Drg r}{'P=:`}.mίGwz/w"6?F؉ -҃|үE摭b"X_qFՠ;+&C" E$՜;&7/_$B;RoJ'B@? 9nߚ. _<sBvrhb0QӔ=n1d:z7=«x}sGۦBA ꆍHGmӢmIW|{~qt:?:Of1Z/_[4|aiyyv豙P ܽ_p`OFGcqSp*S+QɋJ@'?48_GU&xL?%FUͶ9zŌiunꑹhVFs;MZANMe]ҡre:9;s;]-_!REeĀF2 $ujS]jSmTvY^™hT-Rʹ7ʩG{qi!;a}=3?Mw"!X`2K}O,Oh)w ۔4<> f!@>3a}?MRMA?>UIss}]FHS zBnqǗV:wY&&+-Uj G~Ou+Fq$o#C\k9tAW4T{Vt@4P3]AקׇowG\h1G;dqsy3x5tnx^iu @KLW-Ngty|dž3SYLm7db{/7 xd ~ntG:X6Y0>oU=eAψ}! Ǒƴh>c`¨r e5jchxu|8@6HmI6";j`%k ۸_6'lUJ'nyWwK1)&;2%ӨR"Z{D@yZmnd'Ha;c+f{6)!.~gnGgkT@ alcOwG6 o߬QkMVP(ףga6=ao{\o%i`ÿ%gMH=Ws!s@.(|ÿW \u#{$4zt{?1TqW}PC(7ːzڠ0A|I2(GZs!aj=ܹ~;gG]'rJט} w~^.?6׻nag[ׄ=,?LX>WL23 ]fvt?',*n@ÃχOgL-s!l}sS w~.'zr:n6y?',n}anz}G7jo!x0YGY;F4RW*/;R KY ռcatzY N^y \j\Pޭp"!?1=Wߴ(p9WGR{W@nc:^a۝OOӢc!5YlQ"4D`}tAbwOL0Axw'xӃ{C".߬)VgBȣ}k^?X%jl!`嫏Fؒp<{^{dza^WK1v{uf5.`O'O%}Ć0aKI#/y7|rtn}p-llP.4fs^пp0WyE 8eikN= maI62w ߎȮ4iehINS%WrFCsJZ(!w&Bh4gx )Aw-LdV57TKj-lq3aە#fNʷҸ4'[/1%ܔ4ӶgM1N|u6>S6-K;E!DnJBhp>G|z=h)wz5v*zA<=='Bb?`F1&=bzļ۝dĔ-|B!:xc["xQҘ63Պh$OEoCKyCkz_%.uGQ;τ*#ÒТ|716df7X6dcMt} [K`s#Qu{FQ1)cPrb_>Xݭ#8YAleti(MZD2蝶 r:oyyD,&z9d2w0. [2Scك'gggF{tS.[9ءk>ןt\-S/O}֍5ؠTLb %jgs23(Si-R `q_My_֏}UފYzqwt+l5sVTW#@~ݟ/ûM -\nnW 'tu|2Y=?inbyozy?/Fϳ/לlKlLwigM\D=k3E\yii( qܱTUDiA}6Ț8wH;r ֐apWCK8=u@y}3g_zzdGΦxEqgfڭWټZg5l>xzV fhv ެﳒlh#ya-钐V;"͸bj'xK6A-?Y[=}Cjd|<0;!;D7=~})~C“*b(f;U94QěS Ėk@iAM Ԑ/$~-X_͚-@P!XNlXfg y uɍB8.j98D#5~"$UP @U")qNXnȊ2b'äw-hBb֫quz^!80FפS N60w3g>o'GHbv#ug4cH}k3Odrqemv}|v0?LI],>ݤf)l}}7 i̾w{&;nv ;+_nt5Y|d%1ިcc7ԥcg%f%x䨃kh\| ݗHr;MƵMqpKk>JECƍjZ>BDxchsC0T, dfj1b*JtŒk9]3x"-&>~Ɂ { ͟Y]4˼xτfwg),[ϝVXw=ُ`O6Kx%8/q:Ff[9)gJʩeRNNqsöc g*l.PRa>P&'MԂfE|Źö]#ьyFR!Ã} B<( A})s ) (%Oa+e׻@yO8#;#Hip *BỪSI$WPQ8潆$&T)*)o&EOMa12VڙtZjm-g-rxna/h/'\߳"`,d-mf5&,_DR~oU֗x':g??١~8 uG8.;3xHJ/zNi #X)]xE0iڨdD ,ٳ^9&\L_ŀu0=|}|\\G|z%ɂq̔~ ZiX_=\2^mJxo $e+.^2V`d߬構wwzy3V`fɢ~v>9?R8l1}Xv6M&s;낟L^hd$!A.yq̛>rF'ďdNR*~J1*Ku nXrk( !b Th7랪 gte ߃-G^)}s2^Ck8s(zbax]E}#QL2l*C%_\ܟ#YJGbsvm޵-G˓d=٘ݝt)wt-R-x\8x'޾^MogN&r1<=oW(h>_xOitݟ°|:)Xg.5-=©. 3M<-G=f M3~q_Sp/h7 {#3EsU= x'^vNwnRS?OZ#,6(۸:tڻy&J` PtqʃuAoӣ0FmL{# woq⠼Fѓ֊dw}%WRJ!j@=A]˪Hfl 6*|ڧ0Gz *;Ml[NMnG񊯮,'[Ey#1"Rd+0noYTMW$w_/o'G{N'~=.&V\4{ti91Bƞ'<_}nfq}>7W'#ix,&o2,ڲAt T7Ikds%yIθ@YHAw6t>vuN&x&:=6F˰9p`=B}f.pk@kJ-o6\5װ"` aZ8ޜ{d" !} ;|5&t/3B{l(p̧Mª x_^SBZ#!V0&RjT()QOK%bq;Xw4@olb~'я-[)*MWO S[WtIHY {{ĨazNpy{w}w?{?.e6yZZžMu#+ۛxMG>N }08WZEѐX _Ǩo:݂v t:5&uIPgj^9wբYT.n 7ȟ6D=;1?+k=$j yK1A%m! C.z@րM6|t!+KӉSn71xv-[m?ط-ł _3nUnXhB]BHOO:xк,|~sf%+Ƙ6a1!=g.MlB [Rmw^{gwL/&V\;k/gaeرr{d>^ܸYvc4!;c@y !;`pvj0ۧ5TW+T탭nawf@Ͷk<:ݰGPI> W`(G+e 8.[$12Rs?" m(95ꌖl6%߳l?a,H :~Owͷkly0=}w>[=f{=J$ij;=*~}G7Z|9G-w{3JmH9kyֵXƗޕX;L@,OfPRIo͒}uI э*X =ޞC!KVpWCuBh9"q)܁h3*Yd2%E>*3sH /nl!;wkWjf[섛_N2*LvOtcAvh\'Ǔr6g EC;P9z}:]G7&nd֖xS?~ƬP&JſCgvmeu|rPO;N'ώ@v`Gbޣ_oi4F4}ƀTSH&Ȯ}:~e3cKC寧t Q@ G`ww s$q%zWt4)?&J$gyԀC.%$ى|Uf|i'l g솶tTs@iX/\~7_粹.}0~Q`֏?xڬMOi@s,Aps8CKȶd&x^JVT5֪} 7N_xl9tL NvUٖXUwS?SuA>lS36,2US)^@*VӵA!t=;Sb QãCѕYU ?l\^y1z -jhK,c:@wAns kT w %]ؐ'xXRy{>@H`XPa3wkȕƬy9C/;}s4 sLfAۍh=܏ifuP{ipE>!** @V6`olI߻ր;ʀ^0EmD! } [R@<'Wa׺7rݦF`QZj⟈lKɫG|]%-;*P"KxVThdl33py>*tD*/ qg/; *dxZSv \Ma:6*e,cfr8<O͕,z,ҔU$Ҏ2@IDAT4Iw\Hzއo p:ʥi6m:{Y[.Yn09tڜ 2;:@*+w7rP s*h&~2SR c/sՀ&} "ᒌԈ>^3')]2y+2vwmϫ}͌;ѣCag4%r\({<=|N0ߣXkr*;**H'poHL0w_܊DS+@V#'-&M ZDb 7A5ޝAFʔIysnclZ_c$r[7);eB# b:RZC&`J|^//+=6{.5>Nʓyuw~~Z1T|8Y+y%tJFcZP?bf_^H *XHJ (EMהxkuN^;_Vg P$-hGӻ"@S;j!k+C> ꎮpWE282ֹn(:ɺqEW%vjJaÌi 0/  zKthp72podW8#(D1P2R,w Q  8_%uKyGd"dL:_L)u <.$f@*AِEXK{^][pE5*~8'eDR^Ckfore9=y-M6gD\xBvg*ݣ:+s< bp,̄CCb39&ECvùYGD]|rA!g'X1cD wϫ}\.(3LwB`7Gy8O{L:a/#Bw_N;lE8u܉CegZ!Mxs؟ELy/ KU ;?2=/@S(iu)KgݘmoG2pכI>p>[764~kU<} &5 m+5]p4"R麬LLQmg亏m܋.~  L5^;c}=B E"GRd?ސdca'% bw@(gǬtzhbHLJv\[;{[bت:s9/g?Ͼ|R]PswH>~<^+~7~8e"VpDP*b&t9}I{ķ/pJ2o'<$r2v8e2,iMK[Rss G)f#_j\ٽ8n(ka%9nuyJ 'o"酁aOhMD* W1|#`k[6@͉d]\"HGgk;H'?\:\1w)To0b: gxeqgt?"qs'B!;rhya"G;@B]}OQ0L8 ߸~^>O.ߏ?͌in&WXL'l倱i{սU^E|h,ḘA!c, ;2iTT4o[5opo4urr]-lS!> 9*4^E/Dqs2([ԛ슚ƀJ3X<#ul!0/M}]йxY1zO͆ΉuT#a137'WNh{N5cdAvV@RfVM H?4P~߀;E`q\ 5@zcq!1R0,Q y}ӑqiU+w$De}ڌx7M 2CWUjm^8V-sZ@sY|uA[Iˆ0V{r2 ׵{B[2H#P'K4aDb%t ~/8iP0@k|zzP3جHy#]lח{É*$oU/&ZȠC= !W+*6H Z.CR-ULy:j=м4unzbD1?'HcvI8AqPzr \\ d LU<}hu,l>]֓pݢǐ1Qi_y/SG5 HlW"D[8xb&I>˹GCޅ])9?F}sFحz$8lg:y%%zE$*Gτ*6Q+Iq~4AgPEA~F]cWu:ҩ(r%?C >,Rܞ=~p[djL{F_uߧ_l5Su%D0wltD$~ !&?M|??W,MD5?n`h&trz0G>c?`!` Ύ6 >q|;<^hc7f| y9dO8 x^,^ Fۦ?i'UawR]p< l0v(MC Iw&v5Ō3?S֫PfG x??C1l+x7_)$ Θ #z1Mi^yq&8 [ҁC *\1^v[!,&G  ς5=.ⳏcE)Iѵ1"G:'P#S^,{V1uFlW-U؉B4X> ZoPC!R5 ўΩSVĺv==n~7OpaZs[v>α(+]_.F>Tsuݢ P:3bbJ?VyNQ~3dSG#6`-.yXN td\^Dur_Zg2OVz8F>}v87!=f&<EdnV D{eț'ӻ) ]B%%卽\=NE.{NGfOBR$fw9 ZJmH9zbYmy9cz2!M3I t.vr2]E2-y9,?%կ ərEz!&4yqcx`{5k;6N+K\kjb959Nͩϟwb631D37_bK#!ĭ\3 W%f(i!]!&Xb;|&)Aܚ{#87&sd}٩_T)PzWjZ%Z*rWl(lmq;o|.wEYI~[|uTm(4ʐ] 5Ό)̰T0=[@%0Bi&3Mi*0Ύ>r( ]^ְ_G9} lurGiD*:/ $30_L>[J9h=h;t1ܗ&zMyQQo/8Ô3xiFM\$91L e6"8W/8[x[Z: }D'd|UN٣=]XVj!4`djY56|ߵ kp=<2y6̧GN "8dىTӰܧd1gXx(xo>+f/><]dN\=}SM<\ _^s&@rT]y!qE03;:PTm>gtJ㩑![d[ՓlOm=v4ܐO/~Av(d rfvA4BػB) -4M/@Sp 3*O vM-Lwe9L aW: -mZc@Hqjh/ln/^bj:@@ 3> y6`$(+$>3FUg^h{)HșMB4 {vaӡy=M qqo2զ4`T=#<4kw {ʛ=\\O+Qw,nW(.% mwdw@Y,'\W^f(Ĝ+;_ oQ8.l99 ,FxB^yƓ] ќӀ;}fK 꾫f/N?(GNj|/ m]SB7s;qvX> sdd:lL 3zٽh3=7Q3y4O!UzQE`܊epa2[6ޫ0wo Pwo&%c>9/OƗQ$>l3l.p%M57ڦ8{zS*I찜l{thѥ(Ȼ%PzndL-;׵-e%RcyWVDLp3ETu8 AyN:n,.O_Xdx|xcʼ -~Zp#9-m>N aSZg {G_ڑke mCam'tbcbdt]rd@ +XA~x^n) .w lE3*3-Tf?WOos؉  p1 /GFŜcݡE8Jf|9aLp?l9`}F;I7h.N)BbL0]Xz92%{t4Pޱv01;q6VjΓNUzA}.p \3ÔdaŗaPRG8fvXHQy:tTJǀZ"LX|!PCCqafiMQ/SBvV$ ((*nqlXK>w1vˉVYJQP' QfD, O㣩̑1|X-upVIicx(`vuؼ7rO) LS^tuϤf{JwZ sd4R\_/_v\qt3wH ǖixE&d$/=^4ݔ&[]º޶۞ި]G~ƭ|_8ޥGQ촶@{Űlb&r~ߢl{(a(XbMD$s ߉س:/67h<4sAx|3OݍΘK(0W2Z6Ʒ1-`nXZ?~w'{xF" z?M8|oBdxfAb3Fw̆-͐5ZPf*sv/LR82*RIp$$Ixx.1NZ Sl!qX'c`kh]ɂ؉t-]yII>2nkvk8PΓIͽ8>*<By-8x¡Un 6w  FGMIm蘨C|??ǹKO^<؊'' 2h" .(?D8Z.ʻNAy}6^WKT6FWp+ JO }z˙fwKaf "_ELjl.W1rQ8%HXMl8;N?LbM>V,5=X^2=<8˄1KxSRLBQE(-G˻8AlOLyc:]0#f~6f_ӻJ$R@#29@0]l!Kd@O|3xqd$叐,Ý3zWÀBj)0t7,g{" 9L)XOKQ. >5*5#9CWn&U#h \kOD5=YvКG{n25~t $p yY7h1:H@NCU$Oؘ@yY:QVCOgNRJɹIDyb?c{5os{]HI{I֠钢)Z(-Q+Á2!lF}&My91tTEQ(5T׸Բo5kW:ZUow׿9pٲbʐ3Av/ 2K۶^ЯԂ'U X@gER\w7 Ul/DŽE~T!n |p=$._ftC^h;4an¼L=1%S 2Ğ%hwBF"d-^in] q0י< nl&?L)/Due}9KJFΓBs(@a9N0"B.1¨i9C2ڏQyC,jkA'Y" ަQV m$v| <&g΂礐 0qE'Bqx3B4cLr!;2Η3:eZC3iGqﮉ$5 %|FYf5dN#ﱝF ~_^&ui9E1sv>GC2:u#`ufkfQa6D7Pgv+:E3⛡( 6jJE_΃gi|0SA/uOxs G>wE=+pJtHAAQ\R'832e!{u|L%yUBbH0Bt]ɨ :iE;zp1jܔx"U>^4/ V> GZΐT8.<|[` #Fh{\E;߿r:Q̀I 7d$_)f04^wR g0k zZrOK ȥI!2g"4mN<9O \`]3- yEtqd ?~ʸ'wnXD%BY]}zxYX:qs1Z_] Pk/ cG噹g_J) 44χvȻNl'Ў(="q | yb6t8m Jm'||*Uɠ@X7.[wQ}}(>7%9W|ܰE35*Pk"ّUD4w6 /_G I' DEgm9d Vsyޭۗ##xB`m@0&voU[I{zZPgC!`*ƽ .b aqDBYbAf$&Azt1 \O$nh2n:C-l}Ai;O€{;}pxt3r4 8 iq̞L{꒐54A`&8tW|@& bĦY7h-Ƀ Ǯ 'P4:UP 1_3(|H<i] qT3>^ـ.SyjoJ6I#<?_W k`..g@i=.e0f 4zhYV5Ga;S;GtNZ;rҟԈ֤|~;{S>VoKVbQh]zבp? \ؔh,s/dm tb4~\P$X'݅ǣ県YЏPcEipxXqbuz66X?_9t|*^Toԙ&៉ײ4"=!+Z܍Bcmc7s+&*%"yU ޡoLk3Ά>pfSs!Pszb寂;ZHw3 ]F7*70Spwed`FF̵2 ltW28 =ߑTL5, $ -A֑ גt;s5`tŻ&3yQ~t9n3t|_Vᖥky*BqsOCsrj?RGf([q5̯LڮS*4)k7OpAtƱCYڏ8SC zC{)v*4W|\ 5wS}z^Ԫwvmj;_k)w/d(+Gl4}PΜ>UUz߆5$Liڢ_hKiL:SV3F*fM= A:ݎA12În ‘g Ӿl' 3n/7/i-l9_9 _X{Ί?.,O Q&8 E1w1 O9"HQ1@ ڞFY\1wBV-R4 e ծcud0N}q{kH`;v N/OЕlr4zEvq @)pI"cfA'Ys3B(pQt4uH1 7wC&i{D: "Npؕg_ͻjm/x>l+ (};})^JO+EH7$)zy)=2iuzYcÐ᳾ MsCl<+#UyHM0L@l;zT$$A@_Iy.#D_Mx{GYfL"Mu{J5:~CIz;{vˁMDP25I8]򺨽?4QC x׊F2Ͻv<]v_{&vK y4xj{ⳄT!N;ϩJ}kv}|ޥ'1VVzEvWd9SfnmCu"$̰@G7;H:'"x',}CW~|.p3/@9rsn(P\)dͷnu8xcLȎ"o68f)`Ϡ؍+ Y,%pfa=pϰvQWrq)$#+&U=2&fKEc3lE"O}RA59Ǡ=cD Dx!XeLтloeIÓԑˇq81Čg Άtπ6aN.ܽvX n +J&J. S}4Q0Пp__Zw5Mk {1uK3XbH뗵2#Wl$<< kz= z?j 7H,)ō=o"}`'BWGB)0w]en⌯}Ks篥YJ[۱ e҅NSc8" ^a.ӱ$&~#s#dҙ?U . ֙ ;ԖK{d=h'OUp1F /K )!x?nl&y :4hcP!@IDATFb<ɀՎUx+|քԵH ~xϴz%aܼE Sh;pr,̝&#KbFjςڼN,0DCVh Y&{grٯe teȪ8*aH#=]<,[K9%vH?þߙdN,A<"۳dЪP ^tl6@mYJL͝Eʻ "-fzYDE yO|LYyRkJRF]1RI2=/`%&w/|LYJO:RZ"ߴU;W쾒W[wz,'w:ض);}rE, }/9d+.G- g//{UgFFq"{U!uCODqZ&] 9?yuʮ6LFrB5"}́)Jqe%b0s6!ڧy3cD_XL+!w˜%H95D>K :##`[(S1j1DMkuΞױHtL'׮2,UoVjC}W%E*%n(O{;ԧ9ʾ꣘*GoWǹnZXR{] 3oq ӡ"aW!^U˯)VWWȞJkJHv.(<#JZI]`3Ô pE7bM AsGo)OHv}}%ΗB]Ja$|4֥oh}91>AC ΀I$S,gbmMd+Vpq ڋ,+M=a`' {B(Gb܄'I3~#L]uՌ¥cn<3N9,S,]e sdl޻6Gy&W.uBtl{RI!HV֡| | =o#z\})7;նWצQ;v߿MJQbz[N @ݬOke_Csj:Xm^9~N竊}SYR o_gmk 19o[ Hw#| xn{LI?crW/kdg8}8=w6adlfD,_wy:')0[T3#v L yDs|ކS`7|Cv}K)$V~3 %TyNM dqy:%EaH-]A{~δ~W;묏տ$Zg)>}F>-v %U w_o{*@ bFT9{U{dXU" ;czv×pZc 0a[(( Os s7|v%a0H;kI݋H5.ʚýjRK;$k1㙷B4JKż=+wQ@flWu,>RgL 4$eڠ ܳw@oAD O)pK\ M0p8(0)F3|_dv;Ζ Jt1$.{e?}#iǁS'q+0ɀx*-( Y L7G:XMhL&) R"Fs*~&0qOd$@E!u*- PDuN֡)dO6*CՈ8W}PRk3U 'Tɺ~3ַ7dR{ݧyrvr3}U@=JvD=,Җ8 mw>kUB8U<os?q.cldj*F^mv݌hh{NQ*aI++Xஇ+Oң)Ltp7-ޘmsnk\8tfĈ*I={::)!7Ņ CGpƬՃ.-=|XM;4CC ̐eGMzHp <ƁyHtٍ: wMs- 2g\'sC{d쉿?-GΘvj$ X/xW.Q·2Ȼ81 \^`;Kfݝ3=g4 2ѻ`j9Kz']õ.!0w Vl̑O&z4)a*9ybQӍ߅^:7NB|SonOݲzoqۺ7uuϿ3>ߚ` ")>׽vbr_-E?*>*ǹeJ؄kZ{IwJe=$%&F6$9NoDUQΌ@p*Op3 E 5jx:zP(=DH5b\QOQuaUj؏[Qۤ04"fg-AXSityA ʇ4SAC7͝vٽ+= i{c@K9M2A4hU=*͏Р/j18̘4i`aQ7ca5(jKCs\y;(V܉祤YI;q٘@v0t@r$fUlfޭJ?iSy7+iJ3a}xНJ IwydqvW8)~o¥*uI/v-q?o3>o&ϟT[}mdw=Jgc[C֊j* Zon>5od^.o|!֘=*a aD?n0l9o>]8Oth6EwuS Sjf"cfCRqk4_2w%)3'ߝQ0D`F C{zyRMrj 叮BaXLwI[=Pσ[44Y&+ٵ:bd.&~1{4VOQ3|Mg\5Q䗧°zx0_^;xNۋS(v.dwL@ 8LkBVFox3:1tVX/͙QA{JPXgH>*Oa^I(<mW~w>\n#EJV}]³Ga8:]s}lSIV\%RI0[=i} E=t]Hta}[ {0 XyTnrݠYUM % w%-ݼ}k ^[dQ ]~%V. Mnnbd2P9}:r]HuЈyķM=Cx;s,I$CQG޵'8;W w=PQ_bÐ=q2~'YRɒE DYuƃS(M"NrkxgN<sQlTC=|:m4*:Sʚ39̀N9S--Gp-`$nL<*l%ݼ~Y_op804 :{z ^5?ʪ9ꪐJ1;l DR4.QJm=[\ >V)iO1DJj﹕.~p>eC@X,;hԿ bx& x pn WC%ȸC{MXrqOBk7 k<2͕sxy`x 7hzKHNH y8m^e Oz@&;I둿358-CD|wN!>>h=OP{0`SZ'R-k?y-ŐeՂ֛*KTq~ htMFy_ Ε&ZI!{5c%)Z;.ڳlҺAa Ru̧Q*.w g_ ߅K3f- ӎDIxT*3_9}(;_-qik_c[jgRKjHTMoݿ}k uZ3( !* Vx…6265cYQ7V_\,* &JX -@8K hdg>r(VL3wOQ`x;]zXmĄL,3WwQ"󎐚TF.7>`kqu8BwQ1C{y:B6 2K5'psJ[z):jDy\&qWய \~۹O O-on7l[{s[@׏Z9~$__}lRcW%RyUT^ՄJYz.1RaO4woSnMw3.vX<4_O#uU QピzSTYMKTRc#r̸JdTuMӃ(3C*œqKG1C%===ܴıF4Ύ@ƙR+dEC/Hva@C0kaqODWn]Aߢi;4 wLy&n.y41΃ydl y4LJ+4&U _19XWUw(; k_'gZ=^R0e˘[Y۷v`9}_gW~|/z+6n۷Cu[vQ1qWkPh/]TwQcHmO\`Z 2Tꆘ` `΀opV0"w8?+@knni$Wފᡸ ؝G-!M?^#23#\L-H,aݏn^1m@g!.$V٘C{8x;-]dxx@#~"/5Nɍ'k$TbxGJg=i? wB!ǘ{xk]oo5s{^6164yЦr<$AX9gN!w`]ٜ,G|k7%i;l(dn=F'ㄣl(n67=H'8dj/T9R}kGB |B"bڅo|ֽq;z9}aʞP8C*-~'W{:^qwƐ> -iڋZSYo}wL7ݷ]NPɸRkά%g{n'U_o?c?O;*ܯQN꯽ר>.t%!*IW!kh; ^%\,bNB)|q0!N2W? CAM9lHc _7onSanPl{?`y'%͢!Xa~6xeNMcTHGG޹ӀiT FC12x gLLFW#Ԕ]7L6qZ/9"VeUK~QC\Y}k  ;h;q҂mXG/A23MS5ЀK5:ܓ@_ڞȖ75+ +={%=W)pO:B]WƝYyت;$nxHc!*~dM{O'w-Ø0w7 1SlL~0 ĥt5<#4/`=ϙ4R!$!][Ϳ\ ,uiރ{+Wr&aj~7k; eG uJbd;o _}YZe |~*bݿm79vZT!oOsR!ߪJZn b\پL㾡qHJ|6g&BBVch$LjTHʹ5A,B-7ĢZ) : 31RX,HȃufoOj&ZɢaK<> T,M4JZ1K|Vv9;&lC z7CaY##y-Wzʹ%ORmF%:yt]C<8VAp'׍3^&Lw; iDT2>'p4&Q- 'r?8,"K? ߁IQ%m>Wp@%W啹av F[2h{ 4,5窹s@[.H{d|%yc'##+5$ɻdARՖJʙ$T8$r(o7TMs8!mCY=o]R_۵r;u/>Jykq >]goNYrvZ|U6@^ۓꭝ_w1R/q;\U. pTBN훇ps qQ2%3{%+s8GYS|GI6XȻE^F8V9 m{S>?cFiizѱȉ~ûCcsg 8ҔU2El5A_k/<[/S6հ)?,d{oUZv erZtQ{y*83; Zk+Uryz͆s, 'n{-?]S~VԽW{{Ncqgꏍ;<6h*x ۙ4m"7ćdI 07L 9(w?hӓSi L hBO{z}g{{<4w0Ii.73֛Ra(7 m_{Z݀u5o{#YQj ޓYU3I#Kؙev1_6I񒁾nٗfFNh_VƬX /*V1xqg˸};m'B+1ߗB7QXm \p o%,ҾX:V,*ڹTņSL'+l~'<.xA7u`ttC| s"yGQa߻BapbétC`LZ6^LigD '.Ï9vUf )axL=4w]EL! i|{V[87k2p%ZzUKS~VrތiA4s=FepBp @sRݽ\4i{\ܢw+}ͩ;v5lU$h yfƇa|[6=Fg,`w/|R9x@PDkp>^໋~{Hqdei}:Ԛ w7ZZ&ћ诡Y1ybԸGNe4O(#ڔodwZX-گE·ESgN0 AdM,0}W\ po',[gB_zdd}8 Fp}e@|d3ob7Qkk^027nNmnf6" =O9}tJ{7kgc ~Qb3A@~su֦I4D(;Na[ _n">}b?uNG ~Ev;94u( u5D6v3ھd+:i?q9{2p{y9oVAX=9G!"@޶c6-.[,H?Iռudh!)g6uN{I2ؽ:kmuYA: E&c;@0q9KCx.߈_trcä#Uw NT}fpe;n6fVŲNx*%̱vkws|!0wD<?'bwIrT6M)hDq҈Ӓ*.eVE*ޖ(֚s-h;Qd~!~};w' r8N2?H>CC1`AXܕYyxfAp gwm`qc֦|3Gc6TxsA_lƚ%ʞI dpgwİX,$ v{h2Ljxcg^c"}_,,ɕ`v?e-3DY]9ě3(\Hpܺ 8Ws5 vmPv)0"tJBvta,]S3`CCLgw)aңs1P̹X8+nİ[ļaqm}b$} ivqN?_mxF؁FCv mMY3=ۇ#aOhEf !u9>DvBO91SktR3X$=V t%IbXxsu:q3b+two-bܲҊ3'2tŘ]ˌзTV1#pp [ |DKs؋`:%Ї`˰"p^W:iHěR~-*=eGk5 â}v[pṌ[əL!?UŔLN' V@csUmʵ̏(T ?R? `6Di _\VCr!hg[s%5 WY' b="=Njj:̧>NMlRJ7铙xB ئ qL=G_r4LY! VB Jk"SF4DޒƏY0g3>JUNO+_Yš\~>ѤTi:u}s. f/ G SyT ìk|fC=:8{Oi҄Z!mdhk)9Uς_7*h[ʼn<lZ}. f/e>vDB.`SE3Cժ7P1!ҏyοݠ2GC 7 i"3'A!y>~2̢!+hk,Qm9x6>y72p%D#hB"X IaZy>LUxS(PO-~Q }||fGk*dWc\L ΈEET$L'[E{-7_$c ;lzrN>)Q:*~#ؤ{L 2jua+jΏ0O ,h5~\IщbNJ&Kby:|UL2_ߜc*Obٷr.9/8NNHf(#~ѩ!.2c*yr'RФ{F**NS(ijiuծsޒf\ 1 )B<!>xTP4G]#ҥF`h:Hu8 fq*_{< \}m7Dn'3|Q@dhR1k9txB3emzڣN0A9 a<KZQݛ?6X3{mVW2x r_E/ q} xD~CnjڣD_ &m]ϜN.fO?BC |Vdߏp@vm 7~{x3w2{޵OOQs7777777777777777777777777777777777777777777777777777777777777777777777777777777777777777?e`1BIENDB`././@LongLink0000644000000000000000000000016300000000000011603 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/graphics/exponent.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/graphic0000664000175000017500000024026312641367670033026 0ustar jaakkojaakkoPNG  IHDRߊ AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  $iTXtXML:com.adobe.xmp 1 5 72 1 72 500 1 500 2013-11-23T17:11:35 Pixelmator 3.0 f@IDATx 8%XY9=zvљABF$$VDX}oKTSO EO < =,cx*0Ӹ'F?уx*гzpn)u?$ɍ|wOvOOC%{qTľGz*Tex?Զ?tc>/;n䳋x/!Q7Gݱd=OC%7ƠOw}xz_g+*@okwoiwW < Ɠ~طpEBB'K ZؓQ_A?VF1~|gVi?Yݤ_ۛ(otOswn < oѓiX}2w < *Miܩ{ FCz/Ϭ}YNGom[G'xPy׼S(VokOs_uxj0*M7}/%Wٱ~߉m_4N=YOCeuS>?T!_2??>VϨ]~y~g+~C?dd=ylOC=kvF7wD>?`;*4wTOAOMTOxN_(_D?V龳:cWVTl8c;;ȗ-CtG]|WnuzJ_/+7\#-ܺOSx < }r߻⼊wH%67ׅW5W86xϿo;bM]qv Ԛ+#oE͇*4o#_˚앞WHyĿkz& N{{|?OC-Y!n/^-yeE|yױgVi?4VW4o9>/م!*Lxw7ʻt5_%GyY_%;B~hwAylƯl#􄽡t#xCŽxasm61%=o[μޟƮOCßۚ]w ;>+ՍFzgӼ#}_}-{]<<[W%+]x +?F3mYplགྷ"~'S v Avʊ* {|g;8l~]/W5WQ;8޵_o]ow4oᰏmE~w}1u]lh2s;8Fپl[+x.F󞁕|=G 7+#tiYݭŮ^=|F|wwJ G 4{Kz[8V ~y/4d]~+qV*}#NKt.{[M̤ñ곂_b+f|Sӥ_g,V`.024UJvwpD܏~OC߯ݽw4ş4NS^◻_qkNsRxGd*v[; gէ,ꗜU|1 >vdc*@G#ו7|P䳐/m&Nt _ic|`X2i^i;M>Yzfyuw|:,.~1||t< Z /z/4J7FsXj4Ⰿ~q\6a5+Maky,uZ>ɖyi^Зu/x;+>p4%Z{/vr=qܥz6^ki>^F;d,w`;>S? < z S&v6x/Es!K=yjsĴf8*NSZYgi l>\p}w\|WE|ލ6\}mpb77y}i ]i𫾫h/']g%*@GJv}/5zd}nFw~fcA_Ogf_dEOC| .&2qކ &l}~XdGrO*}\yJ\Gމ4ff0 &o\߳.xƓp^bg {K`q@Cx4fLP%.{J/' yh{bbK;6L7Y^8bpH 9O C6ƈOޯXiw̷pd 812(zG=;)ebfMwj yrf<3dsx-~4'aMefea31J6>H܋5RZK;)0I׵:&u_eOvk5΃G)iDSq9wt1N,_i}Yt67pId'{߉p]h7}P"|F)׋|4ĭ4&;k扃~aHCZĻ0/ykO!zD٣`Ҽ۠e%X #ay45չ,YӞzl OC+owٱ}x?;,H|jG-p> kCy B&N NΠaafF}d;;<ؿcf1$,Nzy~4>,>kތ-Lt [2o7'iGeS9W|2;0Ƌz#U3Y\A+=]= =ۇي/_qq |q7Qjyܒ K,MXշiꁝgpw`f?Ng ^W|5oqbo`_-U321F [ձ3.پ |;341`5N1"l=W2wafqF 7a}A7j);}'\//ľ? D2iLdq"#|wW%C90ZrL,6TZ&ey˞/K?p,l%_~ۇ 3qdِwdĽIHNė{W8~g90W7+JLڳ@_7f{0?b4zInOQ7:'ط}Gl92of&f7gcxQG'g7qΞImQ#b\w{Ѥ7610sͭ&S;9߈cV ȯ̑G7$4uĵoWߔ9|qQ#MٌF?i9n_yF{X5(5l)^Ѐgyc',[;#_ק2ş/b3]ٵd#NkǸ'gRg(U)y?EAT3rՆ}yf"p{ AXtUuc8K#v79QF~Ɇ`?loPOhp~/2w ,:óHW-+WU,~{ wq?_ո_ u6y Grd객4FDH/_a]$ޅeꞩ`dS0~*O/fPNե]F΁'c0G?6zGh6C8f^C!Fm}9gv;k;! 5╫%`9yz8g3!}VogggwDDz!_H:G66f>jx}K}/H)>Q /NaG1<"Nؑ̌+X9qwHӃ{o;\|G?]mQ (7|F~$8)eDH?|;c=r/FcvYfM,3!w(gw|<z713G~o ބ<"= ی?r}$Dy&72CA݇~7wNa.qgﲍߺw{1u],41{pCfqv|~ZٵQMebYo~`G `d8ÿ;Ǭ;0sf9?كyGylق&s 0o6?Cne/~vkyd .Dzidl}F>V#-3پž~#!8x9p9Q-҃ܵ j9˸!>A| 0;y53|¨50#ݶ;v|Pu_gpt}wPUr%Ý5#[3z|39gw|0g16xlFݜa:\GwjvmQØSF3EĠ/%Ҩ :CğǞqFH;|"/NJVQ!E#{C[o_ʆz`/ 3=ݥCxOX3 N3ڛ1rCD \1:Pm6 c=2T>tK&p`9IsN )wg1W_З74l(j=.F 8ށk1Z1?Av|È'zd4ņuޏy/(,W.Q"U|m_:E_nV6\f~WwZuF|+f;DH#[q;Eub/qR|J-Zi+螉ݛ37~OCɨvwlOߓzÎF_ B{yLMz5O<1̖g1#4u8'Qy| [`k~SZ ][w*^2=Go&mi0Nz _2c|_w~3nCIdt>ݤMDcG6n 8_.>Cwa|ϋ { {5#>~C/j&:ُЁ"~ [6̲ǑJ~Gst\E9Jh|G72nrsAC`_>o&e| oEXe/vO ƖX=yę N3NV6psFz<{c{9ϸ`u_z /b)י]Yeyp#-¯b{cyZt'sPSp`Gy9V(Ό3kI?̆9=#{d3yzOq`8xvKoDpcq8#2,2}ف-/[r7G6O鐫2Xt{9|pɾ@91g2h83c|1{X6v| ǖ?3n[ķOt+Xc㌚sOeLd Fϫx.y`Gӡ^߂_OĈqŃ#m 7Fxobǡ}o{5۱wȽ^[[|y<=q#}kP}WyNr?يixѠ}dZLCQf>#-Cqx:_o iXWbz~&BWL)@g`}ZߢT^t,%nstF߫Jz/LͣXӺн0,\ @9 }(ȶYžwwjz¼ 9UGEb9|7;Etfܳ 7G>ڟkZ n;9Xcx{c}1`~$J9Sv3=Ezȶb(/_7̻x4*lܑS#_GasaK{hV-{ЈO-a"U5q^Xwlj8b443 }۷Ϳgq"=zܺF{Y?e0:/(H?eY p?7g\s3ٲzP'D+1U:{t.k,&S^]ͫxn}[gwd{g.no'.U>"^$-I|9,w7- 7GSi' ux//|ɹS/ \F3ؾ~xG\w=52> )?z0r>oy<_"$$>'̆b0;6ׇNi)p: dv0;>w8l^} P_Α &(_(i:YyOvcZ۳0}k1ϾaD3[ 8}a\2 3G6OP&kzEhtD/1nL/HWpia_'-;l-J8#=D;>ݩ~פO (;i Ӏay5hd\#XFvrpe\F|kj^]f{z:aEhx>;C@s~b̮.㷃>v5ƹ7']STۑ~pGg5#s۩ZdsM|ܤmܻr򿢳y {/NF[3d^ŋ;k談YaV5]]osޛkcx;o^n.Pd킛}Qh5Í'8`kFuO y,m'{K\=;ax/F\O#qj4Q,{zsr|ЯpxXkdW[Ŝ=єqu xGTSvjKa`vmEaή]5Ѳ>ey~ad#H?yq1 Nf^5|Bz{g䳦?7382 &˟:cr+rml hU淉o}y?[{ޟ@QRc׳aE~f؁iéA]F=y;uˮW٫+'n'ߒqcV\cb5 b FE}ӶOɿ{9߅=kgs&eN1pnuqD6 s~:;ļ\l+zku52ccpli,&;H?eS-EC_=˹)*ۋ`]S۰Wm̞,h,iđ VnW kl+zٟ6X4y =qCɣ }*㋓к UW6tBy6OzcWg7 n- s7suOٴr-^g04Z<wq-|f5s2ۘ4C>f/\xZ-vGچٵYEU80FIŻgV =<2#{d[_zݫ0/n:,׊nԠml1k;Q:/2juZeW|~'r0ѐ[p#я|$U|sU7\{XO5 Og}7:>WW|ߝ+~vm=qEaǰV]o|WSqvPN~o۾ݹbf0/SE_m^aT/6QUShCbf3<B.郃e|v0(.6gkO8F;lDJi݅fnGdN3jMfV"gK$3۵v|}lmwK xa"]rFcxq؝W:ͥV\x,Cե,n0=B_/3[hOb{~KClC R3;0cimj5æ9"[&:"׷ȃdG9Y[fa"0{lEbP~Ecc]S|dX\FKƕ-+){ǵwe@`>g'Qx0ۖ8Z*F-D~ajmXןpw2\Zj1m̆ 2z*۷UWijd';4tE捦nh2Bi 6gy4Msl)>f,rG9ָlg_緤Sͣhc'+-W/n9ڄwi?ϮtzW~ O2 Pɵ}#y#ƿRn^G!ƕ+8]0<4'l/ \|7.ļxbjcU߾~iCׇg&!ie.΋<_kmufF;lgqz7ݒv4%O39ki}|cB:yk:R)}y˶cXcV8g3Ũ\R9;`]<=U4tv5G6Oj]bF e60_$؂9Z?/vSF6qŅk)O6t:3KNlVii#w1;j%;wWeG-h ^G81[\fX,y.˫XufXӢշ/kdWtXaGev0G2KF묭Ùm+ {Nc= U|:O`He-ܷ}AulZeM|'ĒQ>>I:fC F·lEgy@z'&FpM [Sczwͮ-֮A`hmZmh]y׏B3>el ޮE'->#U;Y =WNV̺iBEuZ]X+]YuM4]}躸vGgd7g1vp#泸udu6_aܥ/l1EChCSso3=& gWJqtcקCx-z-ɯv%?wrZf]7k>4Hixsy==gsfOl|%q~芊g4qgeuW7׍ЮofVtz }rsxX|i{䴟mt0F;0 kv0g6-MYѲn`]Mo*+)zuD'dnGCLT>YDZ /ZJ7(1CKJ5OߪN:3ٹwKW\Sdɗq3?121q. t]#.Oz.q,s+>fٍ;6!/k@=aeD 8SN[_)a#zNIS49Q}-[iaeӕGT[2=NM0sb6Y{i]VֱCATw^c:>Ra析56Fl&V_9#m\P<5OWf6\xHMXkC='c(3a_vm.'R`PIZ>CXΨ ڃ,{zk}֭7o|tt6qeOS.'fW{Wtv}Xr&&km07d$Jeyz j+.zƢn8yLcMN5wF<5No9(Gb0q,u«q1׶o^5Wmw}.gIv/@GZywªZYc?rIxvPiV]'IaI>6^46j6bF, 6t`]iו`yQ#qIWx[grLڛۻ G̈́hPrl*HV?Xת-ۡ2`@IDAT*<:|R‰Ze8" 7檆I 3r~e gRFDj_׿l4_yЖ!6",dEFu\;[ZyH:h5>"&s#ՙ+DYWk4ac4,FF2l8 wٌdԖB(E%~VI!xĖzD#,t帊bAÜK}cI͔ذעxBӁc ^^@v sO4F=YnئPjvYb_#*7n*hybZ#"1=] $Yk$mM,U?c18,J no|L[ AZߘ/~vő)٠C<ی 8=cя+[>6mLp?;|sEr<"?&[eiBH"PO^]%/~35f= = _S|}ؑѝ,K j]]k.>pBa95, _dK+8'̼x-sdOL:lmI[L:<د ~:^ۡGQ^5j\( t4!\2xiœ$'|0KCQ3S/+Lڂ~Ů:م^kY8YۯȚʚQQӲ>W]ܺ95\?/LW50sl冺 ܔW3,f6٧G26C= f Ss0q<ŕt#P/[q?j^mw׬&AbeݐgQ[ڀҌxҟD)ȼ~.)V($5NƖ]`e榄gwL4f8W<Ǯ{=|$gcer$/kWȈ''%j*CDwᯥja< HҺ(n^n|3VXKϸNr#_mkMzAWY=@rU|mx>ʬKčK9"#:ć,k.|bD}N P. ۴PoXb_dQl(:Jq~m4W<J RMx]taB[s/55s:m2sh~-kqEε=qklFİ 2nW {3d9k%tNW5WֳE:#pbCy)L^_J_RqХBeq|e&g^pYյryyQHD_2^mi&~>i_v4{>,vRe~B14i~*֎̍⋭5y͞~+s5NhĠ&K#Vpp̚{-jH5p4Li"^ƬuZE2dNrJ&GfQCqum=NIӑPoVPO6_]jszI6Fؑi?Gۯ#vbkƇrq>gtӋnk͢xS õ뙌-FL٩6xXR:]A ©щ͙u^d6$m $vw& 7.rG͌7@MA[{A`D8f_W^'2b1gqj]9"{[RbۙkeyoubZeh+dcgkf;azʲswv0Z7)7P0#%Wch ktb Zj/Vg6vς#K#ykSg#,׊tܓOnw8i~E8dEJAdcqZcEcɇ BYϘpV}5'汾1҉ H6. ƍ͜`u/S͜أnn\T,N5s"@0֚>q']@Yc2iޑ}~"{d,]};6=򫛴vP>bt`\zu\+ܐ5ϊb3Q,{.GN?*3nsl5X_38eş1jdgBYqhn8I\53>tXC8m?D|6o-8)i.=[׺ZnԍuQ`_r`l󅀡E@81@ki Y`Y7t"y̛td(j*j:jhUyTuBmn/!)XORCnVS9$o hW(#ע"_#RKQ3vx(?RK^#V1:8%oNy0K'3?G7Nn56uw7n4H_q_4*愴Ǝ}lѰ3 FfaF~p <~]p1Wů_pl[tձW۵LNXOW;E~XCfR[+k^CgR0-YyK):|͸1_:p^6 wtuIDVA]gwt.@ bp%*Spl6J h|*װoO}>y7b,yEO~{&]kՎoo4`Fr Hةצ 4lp8͢ 4`)QK`Ui0'[SSҥVHyGS4m-a`}2!3v)69r=xD'8J.9itC2|?r?b[:qm7:;<;MBX =Z۬j'{:3UOr:׺NMffņYxYc`v励6wMl*WU1ħ̼+~uԇc HbY[C7?Щ8P'z]enzhK[xizd CWD]彨5^O)EggrFmS+|yMȝ ) M)37kjHʗkPkۛjRǺ%ײrqZ_2kCGʿ޻ "dFSBN:Ѿ;[Cs{[vL몃0kcךPgN;,8o-:_祧o>5Fxub4.cdHSjS0f-u8ѫYHBe[7C,p+5< n4&Mڠ)n`эj{"MVȦ|gwG;Ƣ=vXm水[Cj_V^;׏b :;#3kkP(US)7Jˊ6L#./_ɽqHh(˥>dJ= G orvVg.eԩjOtkW}U'x02=ӆsTj6=d b'!̱.6o:X˟-M'6~|<ۍPB1wxF`yAQXtJ>7pֺX19CNMiհ +`f wVA3X2mu_ ~C<9`|_MɥTGƾxD?]gnCtGuv+͗v>Z_%mYff}7\`G~\k_44УlP-Owˇ`%(dBtݼ|]:-xf>U>Xv%+~7lA϶c].J\mm[l;Ƈ9֧Y\(RB]`W>} -cld-2Uy3aߡf}ĭ⥡#W9MՆ#cI->m,| _q|ժ7?<'.i9Q7[/^j.fӐZ ]͖tG/uuM[-4tTgܬF52mxCOe5Vi'~nr=& G go:2v-k=kš+r㪹6 7 %uYc:[3ȿaNMAiHЉ\xLAq.b;zo@0-ثx#w6dot4N?aklPfo{[Mv>TR#J9Cn q.ZŁP>y^ާP2tCGWnps}-~^:H|9j=2!f‹ Xk w%q^7jo}V1S9ƀ& 60 1MM#95yɜkqu⺋!6AB*_x]N28~v9$Rj_Zwpbk \?--Gq4,vVq=>lt&>+%+;M|<ڜ\(ζO-xIIjo}S]Qt9+ E͝uڮ7d=Wt2sl_G쟚lR9Wc]Yg\M.娡:s q1ܔȮQ㨉+GaR Lr1㾟7KB240KR_dbRK9ŃÙBݺMJ|-|< eԝ1Be{#&BGMÑjH:;V#+(0: 7ѐ7]=nvֱ,jy 1N)oأڰaj(p~+;tЋ}i@rk_wv2l~Ћ$ ؿg1WWJMbǽgUi4Z!5e׍tFԁQTws#('pkCjnM #{nUt/ E>y h} ]2D4~Z̬f; LnkCWYc~!K%!._}[{YK>; 24:O `O3^{5bYc*U,Xob ndLv+Q|xC!QDYi]NKr\bgWt'&`ع抓5"#Z/2ۄAL8I/ɭG7\ur:KMHtEf#w.b] 2Ynڴ LڴJ! 3Vmr8kԈ4kiof6p@gXY7\DDh'v6i$H'x.6g ~zx8ٵF~,g3c7t>9P;P;Hru8s(O4 cKfiQ_l[[($?L g`Mڤ7'aKᰟбnexi@U' Xi4eFEo /2Ȭ?Sqn̅FL@^8͈#`3 ֨y #?x{e3p>WMnyJ&|biCՁ"#;@;UC kd Vyi:/[G6+kC Ig/bmbĊy7~&}ڏs |"cn"ZzӜzqS,v6^5?/mc.~.?n7oz_MUx.*ֻ^Xcơ=bj͓ AՏaV2w063.#m5PICp8TN<֙b5ny:(pZ&egzW4 Fka Љ_87~e^fakĶ<+7jڤoO~ ƥ³2ZeB]S`$ Ua,6fB"U5hYpjSйi#QMZsssy"] QRbYbExYV~rd 'G$2F\fڙM=kGMG_2g1 -O~RA +*QV?ݝ#QP#sƒ&q#~>N󈣫f;R78N`/Ew$ILçNP2׸O[/s|<=&7ʕ~:@o5BB] ;]464̂ckV{s-v _s ^,8ߟsSǏ<3o$F-FhK5ozrf-׍Pu-v΋l%R&0pv&~gV'Q߳kjcUڵzJ,[u5tq|4Ý)%qod[ 5wnUsGÑEwZsc,͛ފIuܩ6?j͸ ^7 x {,M/?ͥ9۠0ahۊTeMC2`׶ 5VlTtӹn_tυň{d2t^Å#?=r29c _!n'QL3UUNWCOw&GfSt aGhQ8$@ir̕aUaf? pÀ]KzѡH"ּ+^ko =l켹-))׼;vKMޝW:4иS 3ohjeW6pf8o^aӢ2=:5GCF-Ld?QJ?jG8r B VtG5qQ=?N/#3(tv 5?uc6v:-(obb ֡nU!5Rd;c}d}plz\%_YfdfٵbZ+cjb5t79pM3e;f{#P8$Q,4WX)4闊 YVxGtOd3T,hQeA-Zch:8K1R v۸Y(o4vGoƭvI R\?C.ւ\Q G̺n,haXGlǚ3Q s)dY94bU2lK1KTLku6^'H*v>Y↎8B`y#P"8y\4؎"`E\kuPK뚬ȵ3diȺk/mtu=r 8S`ab c*,e9Zn>LY&&z4Nƺ6y!s#':y+o:4o-ucMZ~̎-2o|䍜R}×bW2l\Z7tjRI,Bؿ4 ?ʣe ;dYXLb;^UPIѳSyS2=}܈'>' |NHIڡ4(iϡaCoF/;;(YڔI\3 `a n.:8jYzY_ giZDA,5 mK׍5pjC#9dմZ#/ Υ-h`J1W eXG6=:ҷY4h=]pfFeƮک6LMu~huĠ@ˤuv4Ҷ@{`"fƞCWx%N|074f$7\N6spB*/ $굖#Q[JxÆTy|tO7(#[5mUOÈEv`>uN%؄fLO1.kĦġNd.M1۹j|h};;PQWk FE?#oߢ8fį+[򂇨f;UtYfٝG<*F_mNжoTxIwVx0Nvqqxh(ͧ.h?`؞Nx-֥om3s5_Α1Hɢk3ױ FzH# E'6({sHȧ7mvLo"=5s[ho@NgG^61Kc]ڑ\1'ti%8s聖x]xWfp==M0ysT(a ӳQ%GnCOߐ>BPeĮ#5El\qO:z¦튮4%c|iՁPLl}[[Fe&4uFm*'krKae>Mfr5ndl2omw/8kw?.E/\,;V0>o9&eYCVuփ5~LLSWiaW2g~VYFe FI;oCn]wqgSW"x,Ef?Hc6,&ėǕ Ds|VvUCՉw[,c˄`-~ {orVߕ"hd\i`[SwհwszEɘMnl)bmy(mv bִygNQns~Q:a?nkE7lAfPﶍ>wa³fzLV R!ypw-F NVS8.F;==La>f~B.?0\~7jy}CIdq66tt3 7c$sSN9KjtUǕ'Ԍ];8.W9hw~׌lq;Nf^ y9'st#][:+=z-a6z.p)~:!x #ro93jkՔ$o?fiawٸh֫x+]2[X3NM4b!g5A6h~k,I7PGqɲhv:(#d`#& HZ?ʶjgW,tWzƉj暺w|]z1.cCϋB/DogEÅʸc'ϚUJgϲx+|T?9ޒpLK>Gǯ7Ĩ;֚ s4M~h~Goi ݷ|q:[p=>Dcwrv%zl6r;%^,?'rWQa7v\Fz 4krC9R#B"!P^I~*_iH?ǙuⲚ1#KJ/j~ն=UYAW|ϱ^;w|xwmָXB1gt,_}X \K^h5|Ec{yUnm;>IǦM`4nVCv\Y3uo16k:f>~p幤MOnɿ0X]ݪι+z e4"X][*3N /ԟ2"^׻Ň1Nfw.n[Doqv'7t:9?\14:r~JşPՍ_91mpae4F4+Oxb3pcKSv\yk,GS ;x6;ʎ靈W l3W|mrGJ#;vyH@DN;6qSqNa{U5ae.5(f#n\okOSg}c4AmUȏ&ݛ|˴KGtlQ ` J ԁ % |6v:d<ǫG1#z銭r=V:xlpnv6 m]wp+t)<»*3Wlyxlm1kfGIotMeFN_4f[wwoܬM)_ҢY@IDAT^W9V 5ixq7r0cyCOT&wD#uXlӝh[, ԁU#wЩ͢ixZ4~LԺsU?q5b]sfeRq,.ڳ^Uǵe+&/7;+&.*tU<0yi{4J_lr5J+yǺWg=&i5Mtyc!I>}h4:5qo.wޠMomCY e)bפfmfP]X͠ξnl>DEnS>4w? У;ua4ּMAG.9r@t4nJO#r:!YWHD3;NU>1aX>@I`g6zb˶Яڝ>y;-ʻݖq+wǑ_污Z}';.*J5o!5HV}"Uo/Kk6-w4pbc⢩垃ewZ _FYw|*mɿOʌ \x5o2-YS1hr5<k~=TQ]9&-)6r5<J?㿜D?)?)[4OSϜpAxߧdw׌m:,7ÙoY'ę@o|~qD#6]6h5pa{O.2OͻޝƗc,z9otg>5k썜&;*26s|3zdwoT!)$V:/f.?&y5'v2y&ұ ϐ!l,ƞ Q GcGK+;ye76k׀K+s?H.dՋ3~zk\^Æ[]4chݢY;!}yω˽>G J WFjF_~sfm[ ިaSޤ 3Ѡ||k}\kS\Xhdzβ֮nGF*#x0?:}s&-~/q=p U_!~BKv66}ҍ8.# u6! kk}Şѽ:;zE߭[vj_XAH3mloܙg3@7ڣy[η@5mEϚx]{#@)[@{:kWh~N9>l4xwZ 4ay6WFethgDƗwy>`ů=T|N^?*}8q0ގGl tQuLKvg/Nr^:r?&07I;gj[ n~:ZH3fM8ol47Ӆ!o#OƗȝM\MVI9+Wg,Q CLW)xfs5\詏d1\=S[]<\gU&@\VF E&Yv+xǙnoup Xζwvkcc~}&7V4͠ra:&wqt*x4o wռc]lK`;f5,hNq6;2&?3n[5~7&JH9nm[9/"u׃.k90MŃOEc>!ﰃTx4 2>߀ڈLa"z*;wq"66ɚ:kގr%:=YGlx+<' y-6ClDŽA%Hɰ-2[nWzs]qlhϦ;zk7uK!iF7ϭ?EKυ69񈯞Hsn|["+xViT7E¸3nU7`G̛mʫgz 9:'nu1gRf^=ï䚑:m*ǵs';?gioswEBäNvfv~B7$̞lȉ]" ]DFo3ێ ߥ,#}DN( ZBogk#ak`~Z6 1IͳZ# sx3Зyy 7GV 4w$v$| z:x('t׸CY BKaf teGu؄-渇~b 6qcŬFh5q~'w79oGr<5'>jf-Vo=xKeNdBnV[PG} k`/e/|&x+]Dxg"=N? כn:oɯIn%ٸռcCCCf9 /vܙ|/85hM=,ZG10JKp_ G.ZgwCv9L2]K~GWa)/´G ꋄ >t\q=5|}/_ⴷ`HAm=wuɋ)ykmn\m:AfW}JLOjXI$<:ayzhrY+5|ޑzw_;􈧸}V1[L34Cߕ[ۦ4i-wxd˵SL_O>?MuC N}ۚkCmz5s5ObFsk$qaУD" z9%z[7O ]W[U5;$; omr!Z%gymU  ;k39"1|HC/~?i 2݅ۜ~JO <#/dћ8v4q5 +sȍf>"rFF>J/-MGllzV2x1y:xKǦz aUiE2͒3W46X%g5It(?9,m×ܤۀ.-~IM66)ՠG4;Mʼn~<DSHh;u~לM&]]{Ԕ#OldY)8 ޡXC 'k߫mc$cf#軃!SUlzu~O8+Lt_~%gȟfƯwVk)|8,F:5l9d 8E_]uGwٝq46s^r1m1I\Gtnc=:@2WWRm "q|XDzo }1N|Mm زQͻbAsy']YRs 6쿼Nm2YNo' ׀k߄E&ZH8v Xs?dm.%#6Mh? 汧 m špC2M>;ɸ3N< VYV?CW8Ϡ^Ъ2,쯷#w!Wnyk. %gpJJ~CQ#y1qnseVSF5< :ƨ.iت"ư=66RVědrᝀs$v;?u |[ov |Xfo?]3> ZT]pW}\q ج7u8~!gm&?PTv8t V'lLlEk' tL>cqVsKLe$-0Gt}`ŇNz/s/?CC D+\IjdכIN2Uc٨[N9ar9-^n-f?A ܭ{~_aG\n#^DuNI_^}M i?_й@>2v;XrMiNؙvI鞧J5zv? Ƈ9a=TDh3yI$|~^cP;y"LX積l?8Ϳ(γi5QuL1'v'=\]mҭBS'ܛ6E5nBOoӄIf7ojk) Yu.w;.5ñ%Z1͑iwQC b!71 Zt8VknI5ϏWRR1u%ٙM׽N"܇Y(eE\W|L:*@vڢj#jӛ:ŤauX"2WtD"3mn];̤F06uwߎr_W+}u/iJXg#⓻q'/sӁ27b>!]&Xx̫XUn%Z,_~Mүdۼ_CVqyZ uCBļM7!vYPIݥ Px8+6T3\>lr-f*_+d_u}%is7>%нf*k+ Ssg1gS#皵t7{?^Fgjڈ>rX ns6ơ#mtfތ bA? ؄t-=Çxl`ŋh3fl[#a6eՍV9K?oi+~4s[׽?n诇9׮z??k:ݹ YGLQs?!=utE A/qˁk wx{v-kx_$UK[sK~m |U>_=EjO_ݟ_ָ׫ha}ksiuqp U&vab/=+|w!vʵ}zC?_*U|~*2KV7]Z篼?bx%;;D ᚥz)lG39hz6֧ID®;F"3Sg)Y ZZOo&G؝oY¿t4p}{v2_n܊NrRiƲѨ8Wb1[d8 H輺o B65k|o ?<5\=cņ':kC99x;њUv6dDH=*rr0rW|6U.6b@;~;=jiϟ|&#"hb.UË!-ÐN>|!Gs&H7t4K ѲJ8q&i D̂~(ݶ>`Y6؈6A'w#D,ɺ{񽋰KhW| <7'n)L X^#FDIKV׃~:{FsJ;n򺟍?wj;26^3{y;⹅|mstqS 2a\| Q|p|7*!7#m+i1jA4r`tuɘ|;ti ۂsqL 5| 6(ƷK;刌-p\:6yE Bf]77tf>L雗AR+Rشo$^Y<0yOo]WPfpRw~Лn>`|d?A)<>4Ħ =^sA'9>Ɨ/?Y`׻%qKd>'?_S>!l") ['cW3p$6#Vle? 1OXO/4tN6;xk1<&t'܈Fϩ *bӴ͗K1u+˸ɫ߾nuw:籎ǥ߰272?ߢ)sBCj3~)y43YXFk2_EV.ha{wɐMsl걽??n-oA,-6,?~g!,#?ҿ 琐v\xӗKݤ8\yN|j3f>C9،Gh L\} ~kN;[yqSYOE6lBG`|98mxa2 n}΁]ߥLYO4k'gytWӊ=_q ݟ{ys%\(w`ҙMK`r3\wl*;i,tk_71m,,W=O%كmxĖyrfy;d8" o#xqѕ~0 0МkkxNʂό1=aPD&KpJö`L~xۛqċ!J6 \D65^J#,R;((j;k&ϓK|]-`Eӗmf>l,ssGFm+vmwɳHpa]e6wo[.3U^2G-r;pu߆N>ddy'"(]~e1;wYKn yȃ\\1Y9w\e:4sL%TfM.#SNLJ7^wϧV;nNӅyR(aՠ>r]i[kWv+6BTZfM6ٹ1w&G?y~Ï/fm,|ސ-IoߢYK2#Mϟ ϯm׷qot\_8<6WG i0\m♽ɦY9AVòΚy呇l+Xg䲛qf6a;B7& %J;; %XU8xϓ-"?I6)/ʶٿ1/OZ=k+Lk]઻yrA:Mn:x3;0MWHb-CsO`t?d B>:hlKLdfw4hr̫ՆiaCuDRqf|ޭ[[s+?f&q}<HO{;Qo<}Ӱ9gFN}T~?5t 2?Kwȝ^|cիl׈OtYN4 l LHRt'2dgdX/ +D͚Fš o0MowzxY#soTq٪Oi,KA\h1#Lk4D]}ɧů׏`n7dl !AZ-LwAFO+2sh٥vmk^k MM2s֞ٹdM"<.|ԋUlU̱ުfs4%+j-.<#a M+ǧ9͑FNx]w:V{o::s rKCք\4s4~ݚЋ>}7̛Yr=̎f58i$ѹ .Cs0a;G{ #XOMz8Ou*Akyt,/ yԍ|_vߎ~%FԞ<@vd ιt5o|2U M@yd;sk'?vI8ܱN?b &?'k_ ħGS7?v7=6zj&QSՇUI> ̵8X7mwEwнGcdyj¡vY09ʖ3Tp.:>8ĒT͵/T#,_rp>s_ߍϻ3y/Xl7 ؂_}l%z3^ׯ}YDjWѯˤ^/:Jv;;w0=m!v5sq SDwow~MEVe|ݩ9L3D3k@ixMLp/4s纑ٕ;t}3ܿg:=Z'-c;ȑQ瞷;s仦O7p3snyrً4EX9ؼR:c< ư/&iM5 6Z1&qc*lE\m6ϰJbvV^1{.N~/K ڤ I:oup7d\4gnƞW~V]Zd_ϡdʑayg?B3w%fPш&|w=e+͝ we<,meُrT~Gnj-O3 :(]tUh"jP 5kьkCsigNw!?\59+Y"3‚^\p3]_tU.*> {F_N|0/5t.۾iWlϿ@r>Ub&fiqĆT\t:ԺhV3"aɪ[-fۋO^_ tZQ':/9|6wqwu1k 㮜єwH7v^CWMyg6أ1 &o Ab#ZrWڃJ Yf6w®oz@ 癮9I͐oߚ θth… *}aه-FLkGLRgggIOw`{Zy=x~X ֯-1žٸy^'X#I{|9e|!.)` ⼩BuQ癆5uwì5mkIcK8~PsEcE.ku<6_)HqڣMG,gn"cUu=0y ^3c~ چ6\CWM3ȓIMt'|sY#l#Ʌ3_oښHP6.Mw8,tewuǾau3r^c\Gٌg߂=j /ܭצNC;x6c hr A㣾nmvŶΟ;yiu̐m·h晖Y[2v:tͼ4#OQg5Cqnqsܱh/ :3"pըSq_?^ \LԮ9Ԋ b.T1o:yׯ m(pm3όB K4XG+ b'fi8$ٌgB纨 k쩲y%K fmw_+&oצrkT?pF'E*֘kŃrW|4oyw47tDβXǎEz/??xǘtE&3) ѯ̝o~ hGЀLOZ75h[ˆ2jm|t7^k@˃*mc]x4}YIkfv wȩ,i4qf~WNc/.8q-`-e:EOL@.16!v<4<|=>SA`;tl H:ͬ;U/ɨn5;AyY)[zɵ{%O}7ntȅ[oF6e]^]ѨL%r]ʑy%#I[zM|WPLfFb\g`u >C碦*mXY6`MNY-?E?l_?੍ Mi.a-cZEu]N;i>;R/yƥOS>`erW]m& SgRu~>US,S_wڧ ^n~Yc 38wꆊMWwg3y)3Ns8Ue5l}m!ﺦiD]kVEr'ܬYgw[N?~F3C?]#esiWeQWjhZ% hG;ɽagVflGf0:gΛd\t.TH>'F`,?@ +,2UVdG+ fnyKsg[z53X_# =Nq^͙$]z5`JΕzke/Ygd̕nfMJU_Ҍt-?n?Թf?fnɓ 9mrbaw͛v9r7qߪ/:-a'y)0ʦca_a2k=(VNF%c=DlcPMwJ_UۚcAo V~榿wÎ x#/דCbA{sXs=F4bm^le%V9b ƃxu:L~^;`!2;Si_eȪ~G{z{iHKiJˌi&W'w٦m}G\\`ŊtuڑA_ђ1mA+}|^KbF^#fQ1,XKu٬ 2\ϴcdcvE?䃳PnR÷Qb$ɪ}sT7|Q5Gĩz%4z\`FhU/Y+?IY5dz̜}}1|AN]9nպ1D^u6oKpa?ߍqN W[O^y:$-tO$EV.0e&v1>*2J[riNd~'vTS7'@oe}CNܺwI 3ҫEd栉kvE_e;ծa@c6nX Q Ȅg2e o}?^_l檹{m\4F} us qSs %#-MGkE&.0>Guv!\άl],};]fBZcGF圮M⨮oV?0L̚γ]6tN Ƈq.8ڴ>?oi0L7Yb9Kmq~#. LZadb$f]Z8faOv??qwh3͇.$GgG:=]dүfdt}lkO\e|w%^1ko2IvWw]^rnW6Tc=ؔ. ĕP)Ga`q_9<^%{eqfCXd7R=~fRYa;wɟ겡? q/ %SȺU{5=ɥzNl(fzR{F "9ʏaVM;E,3M[5qҐԸcf]F{.jŬ8!Q%R *hf^f5cu+ZudvtůhؼC[ͺ3>g~"ICչK0c^v0WqWIJ#S2 ͜6|jjCmVgKz0uizы^|ôStZx+?5lݦ䣸m| ٦\:QLz8Qʫ=&l F7ɪ`f4x[ɚiO*Qeem 3畮s} 2(5%nne•PpCW֜0xWBn^pmwRsLpZN;M4}HcIvDXEweo&|]ܩ*eqa~ٸ~W3?KҴ%. “F <,9 \h̼'+zVK9|X ,><>7pcϸֻ)t|f{muK9sY /~djW󌩼7|?A">S7y8 պsv=qqgz=~8+Lg"{^Ȼi6{fAQ8poiJ:=G)__w̧q}l: pk?) {9Ǭ||=^Zt+6<wyY7hPe.od ~*];M^i6h>V;wi4f׭h yܑCyGK@{Ni: 57Qp?wW fXzaCko|ݰ´9MU b sigڕUx/\O?۬tK s9:j޳>kwl6 bmhR/,Vw"ݬߔaw9a>L?,18__#E_ugəW}9o=7M9؞5:9<UsMȢK&Q!Y?j0;cvjtބai&FywUY?|\^ àcMi:l,4x45DUA>iYʼ)Jkߌ5ƖT,ά;KyMlxJ5OrN]Ps2}S B-vugb\ S\a-"^Z&+iQ'^qszqPX<ʋ<5ibxnoظ.Soc38~Or\_"pN%F%+叭AVuY@Λ.p eSd\i/mE}W󰤼 24ʢbtTaS)'`9 BLs~R.u T3B]U-5L{bR+5aM;܉Tq}- :Gp"g|}-ֲ-eJ(I8V~2nEèƟ!]<.axC 6lxZwo*[>+oѳz8kzԕ_,?鱜P@9Ŋj;xsm ~[YsguؼnkHWdo#MPE&I*N"]u <ſc>J1jorI,WT rw{[}ޡ8W|),m\,h+UF}BX#cKwa4+^i uٷ;r5e_56ƯͷyGmxOŀaLz?h 'i.-*4|M\k9 ;m|4IR:}i=76pTcBk1:phxǔ0β푚VvYɩbF3#j6G ͌|g?ş!6t6  ˉ.m<'j\^}Վltnk^Eae!;n&' ckIY>|W'Iԉ!=XxݡǏwyW$_Ϭ| ?+^qhύ?ƣ׈vkEm><)ŵ9`0E~瘿 Wk]Cx!$^;kp yXWH5^+25]Sr J1Nb~3%yϕ_W΢5z5z͵6뵘e3Z,njn>ˈe^Lj@\d;)g)06Ŕ5'Ѯs  ܬf~[ّ]]6WɉM{6qLw/=,xE_6nn@#L<oO gxB/̲/We^}^H M{ Jy[sՠ4&?_WPqO]c-FYL34.cp8E4^zfa5t tœ gĿ:hƱVj𪛯 y5QVZ)_X4QQk#z5KF ]\5_-- ˚/ʖ,K^znjtFrYyz1l:a/s@]W]l.~){e6-՛U#f:2F;rOG.Au]NSib\gyPnX!+lHVB(Y5";yw4s֫F`6Cvk:#zܕ5 4Wɽ k"6\kl8Kl}$`KΙ*+%n?=rlw\lZ\5gſg"9?/Nm ]qlS=}FF}Nӗ28lHb _3 g5yLZcZ0g`pKVsCfCֻ*ݙW=c="h`BlM}6HiI>az _cׅg* m&~Iib2x S ^IW},e5h60]f<%_|7 K7y[9͛Y3NDޑ͘Ol+vGSOeW@ZT6UT&yҨ5ESN;wx䚡W Eߥ͈gWu*t _y1hz#nft~Ft:m:'A/3?w]*]VyVl]I`+? >rS=!}7zBеUyVzEl>A!\%Wi;>;PXhbJm_{e'Ή?J}1ӔFHhA'=xD֎t^\l5P_pRKrO2+t8mc4#otϳ|.KYGv~6nzޙ_k{y,4E; ݁gL6[L{1 MS59^nB?|gNIw:VC_9~M]1~eX֍-u|WMZB_CF.sj.p 9ZT~EKL~|?ўzxI d#jv3J M򅌴{I_u3x-yx)>7]Yq-.:Wd~YYz/0WWN*ܭ:EU+<,{**f׵̱^(T܊^"α {lZ,j_md,aT؀iefk࡙{s2pmF\x顽v 9Û:m{;{Q/D麸x)>]~L,swaW[?xӈ4`];tO i rΫ8eUm6Ii JL0W]6*'!pІVs$T/&>+-@ tB:W\CϺ7`2g~/3vʵUTHōV0m2U?33߃{ޟ geʔܬsL _?6Jc˚s:dF2?T$Ǚ-YA*̨~E䵉u͓ktݓQ */[R9io=fW#%X5d~b:W 35s>d㚚-J0zsb *ϧm7V۲t9WLɒ{:Mg~lpwqG }}?"ԥ/H6ka+nEdO} _}ݑ| w1Ԇ7J#|3QCB5~}͸~MMל*ыsj÷eqf{Q#}wNVφbVSu.m}u6(.-Nug!.~m4+ tͮÙ|hGYeoSgEd|IV%g_LoO W'؝ϐN1EqNW^V̕bWlϓtnlN's1ЋF}yEU\$g+93;13A杪ׇZA0HܘZS?޽WfqZ /qziku5sҞtc HαYJݙ\먼64<3<}1ο|YƗrwQ~PMrȄ׌?IK7ω xM.!( ~Hx=ɬvGJS*\f'ĆNZr):ৌiV?g_ڝ.yIJYR~x<0\岒=aqn0n2\_*ϲOO{lOvocS0`MI]ր)Nc\ΥVnzב7ꑵe_~7nw/A:[;kvy:Z̒o?&=:^#o\Wq9<7#QcMw8 ;~!3,bIq3OUm(d%\99'~F6;#}Ʃnѱ5'g| rc`<>||qC["d|5rFH^3+ӘgtL5 mo ^wdkĎR'2uf.D&3өt+]snvWgzb1;mD+7oDkq?~x3fONyt<Oԋ^9L|X=D&v}8xc@&KΫXF fEGdl$;3oYʒp@)켷82ܧX:/c4PY%DW.5> h =$xNڤOndKӜ0~_f_6b+3=l|:1`jNŽY|c<ܞQeEPۢ "_ m C#}w*@P!]8Wˊr(p#(v>>zZ ( >EM m%][bɷ7o6m 9LoPo? v`_r]` bxZ.}UvDۅ:$cFpɫlNc"OkqPtQ&<8>pwZMvLy3ؑqAer4oӜQ} ! xyHUy K]s_=JJPG#9s_SE6 Eo-FQ煈޸q@FC@|rjH䦫I=a[Wm37CŨb!mm}5*l:,~UnQ^f14jqF1΃1gto16†? L%9ͼa-Ni. ٜ-,No9[0y:P$Λgy;~ilqhlbelz«="T؈E_V`Кʔ9I#Ph!X~_Wb>dNj6XƄ= lr} .8[Rw,՜*+{SZŮ:8.~'pq蘖u#yHH\ٲJ8.\@d|vV2\БH,0qť xn4gVgc߸lCcWbF}OrqdS<\^\֋ZTѷ8]_2cbU#?-g$5Fg1Ifa3\1 ?[^,nӘN1X5i .F=IH9y þ.irDZ|L'{c ;c0F&:êf vooN\NcpdWO^#R X&mI7KI1G#=\==5xndFd#rؠ=iM:^u(h!xT ώ=~O1)}P>츌èK{@r/Ŵ׽5>;ɶ CL=7܏RvF3ڶk"(i?3lx݃Ks{ni-zU V_msY IŸ^_1cG6º=x&xr=1hcƚqیNbGoi̊9 5r_rMtK$s|><=>Mᒘ9Cri:z>l5Ș[؋~\S_>UfEKͳk'[_HmQ8޳FBK$(~¡x4k+|:ۃHY~g9k!jRJ)HzK5Vtg9:bmhZr^uatds1]Os ~["pݾe -in}SrW!cZcܖccK~͇d|ۇ]Vm'dplg٦ z֙G#'׸ 3 5:$=9㘩q!y<8S_QDT-pߪ$# ѵNm7: D ~B/tZj9"5Ii5zݱR ;3d~a6ĩ:8oK`S)(aHiQ8](N?2>>0uEu3NF*0]";>C >WZ|i ܵt<@Rip\@3V[ڊ_c^!gk<^Pr ԁrL[.☑Wۅp9ƞNbG#N z zz~ G!! ¦fi(~xJK(Hq6qX& Aq县pz`3r=2}Z7_sz, imcT%ֻ  ŴmmE:O1 {]Dh~6;v:ʿ&,l1ʜOS|^!NB\m&slv9sq%q ͹#cKKL !cgEv6cL#|~C"Oߵ% '2^%?>2ٔ3@fks9]d,hQ˾(:S1ބ߸?}n'(x$t-Զ(}/3UƵ8!=L¶6&*O\ƻIeAT.#L[H].)rA\F6 /w=Š$gh@k EWV d <1+܏9#:aqgÎ7 ٟ -=_!rMZ#Ee| |~븬V!<6OF:5;M>UonQz9b!QA#4]݉s;+o |rʈ#M73ܒ M4kwgmW >nl =B(8 I{%_|0E]m(.cIqS[GSulH-|i:!k@[Hj`}(.um 嘣]> E! #d>⧺-{D2ꡨ|0/ 7M’MGhN0IX'0myQ@EWJ3vy6-'9ɀ7Y?[ч9\h:X.wεeD 1{t\55}rX^0u >{vQ1^X 6='8Kzܣd'{x~ l׶M`N>H9v #=/r8{Cpv1V`MIxm8o-\/,װ ycO=疸sC84LFfa!G5# =.ˌqŒF+CrD66ޜXu|q2r/L a5'c^vuuZ\W\V!>I&[s?<22~ Y$g|dI4MYCPoK[6F1(,2X#LFe x`Jl>1#c6l4ԻX=v0qZ>4&˹Vtx.6Еz>csT76JƕI<ͩ9%N+L'5-# \3XyJ1 c0r(7dqiDk$|p}dF<<\FWv)(ɍ9ZF,{dLMڮhœ]hAGpӂ+Cέ$N5;9nm&0M":Pcq\}򵓾+0qJ1/X)j#>+.ii1eo.ˎ)˹<瓸yFDe2^ء76|sg :|:X4JYk#mhN=-ћtm묱̫,PSLfDmeo_=A\+d鸌aSŖ"O\NP<J@|nr/ј(g ϊVN{2OgER.eK jyC+Yt*0䅘]7qp"}H>͟&;ka]vlsW&\jqGa9 8N[c !.#?l9.9Ns#VL.9,\íI.9GOGI'xV8,'U Vr=OaڒǬSNz8y,=dS!8!GjvG.3#~K_3[b29׸$gaWNlO<;kMEAF< Dqms1>~>o٥hCKJ7:^Qh\~%@]Ƶ]Ʀq5mR5RxFxxr,gF7-!.*<g)b83ee|S‹9Τ $OoT9+o6$?|H;9a$x#ck&Ѱ8Ux<jٍ #3^HATj'yNsIlEN,.gn1 OKyFl|MRWtΛ*m݊ xKm߼YxY:W;ŗ"63vmFfa!`mθdMWSxYz C[:UΊŤV"\|3sB9"cd,l/)ʘ1vvqqi,b9I+PG@]1E2 ќmǙVJ5&#\\ǖIlo-FCfqB&coJS :֢qY])q[hg[ k{FV\2vNb#s c Mu3z74ɧRd7wyiL=ϭ1IDATJtORF9Dz|򱇝Y>W-SX2K [4 4gK|qܪcg[r- ]7Z$i^v?BtrX:|r]}{_%K \ 5{R.xq58i+5R\j<<=|!8Zf}O_-ԷLdAGCQk20e_Î"vrZPeN=w8?{>S5 W}ےGz+h:#RYQ܎,9`Ʊ|y_{<_gI8%b{U4Q}EDzGVt.bzxS/ De(oȾ%y6RgrS9!gψ-%T;8eqg?Q/[$9Lj՛{( hӔv82z:v%j\Genf`coN.Яx d?,G5amC\e_&)舷wp5irMfūaE%9Z2 kK)ƒS[4uwxig igȣt=[y]+Wd ́dh֒[X 95ϖ0 :7~-;oaV3vT&9 mK`綫Ɵ-XE-=[Jd1-n[[29Hc0MI]zt|T&[Eߪo? yo.zJ̒k֒[)u 8>Z k=g}D6yxs~bЊk)nq@ZN$GCu8/j\ 7ӾmNs͏9&^)8=dޮ?Z.XMcɓK;ty"9`Mf,z-\=^bMbPeFְcˎH>;JeVk2>|Hz%޼l|xQ(2fd}V4OW~QrO?^б!_đT]gŐb5{)͵HFȹJalAZC8#=yO!Iʟk>5D|L՛G _#hiAkjF>0 dG1dhҧ%K˕6OGTjC> uj[)Ӿ8⪹ڝml^rH\+d]Nj%\݇]zY6<"__lC]bsd>WLF (NObk޴XÓt=0iǿ[vPS5&~\dmYa-$%erMbg#E[Qjŏz4MImgloփ̌T-O`pt{Lr9bZ pž昏:4*_ ;ְO)VmX18؝s?ׁuHZF=/y=eb]",#dm$^εX%FC6iнI90,S-*7Сi~"Ow"87[v|Tv+~]#1r%KYo|Z90LIaTZ$ R hZMӮ==bK;UбB[tPn<áaL[$ɩE^0b^[㓘iq =]CXG93[\Jc)R&0^%Z Lڌb4;MAi~y`%,=N=UA­JJoarG0c&Ӱ.$Wdn5?(O5"H,'t2oYmfmlBW4U$Ҋr,;kb>vUkOCFl-?Y6=]5 ~F㑲qϧ#(.b5iכq>l\E(cU˩iy"t95%'L#7A.rY܋òY!8>!rڋo{WCTN/۶n3$Z>QyzKg~/5JV"#S^Y߷6Z1q g+nZ7_]Б)[Ka٬K[ o;c@CZ'"xC p9br _D# &;c*B)^үkv#2!Bŧ C[l+c~oZ6%{l-ϲL4sbȴPe,HRRTN+O Ukn Q1\ں$AyKgɅ'+ɽj2/d'Vl7P mqNLPi(J6=Nk :=)l-/FFȬf8GmS%~j~-ߞAbjZַPJ1QPmti;= ˍ5jw˴SΑ'dw8=}ʌKiy? oLV"- -mS֣1iB/~' :^{6~Dg٬kʕ ˴7]! P_)xV4Ln,lٿ-dO7 Q.8Cܤ.#װ֡cυk[@6gfVV=.F6-'M)HZ lQ-*oJ\f >5^Caj9Uv&Q-n͟. =?0D0 pVlY1zk͋E := 6 moDghrMf'M^:%[晃/Oz|irMjrB%0fL@kT Fa*m=HߓMU;+o G} :~ɂ [͌޲4MfqXo)I20I%-%8,9قY-=__Uј:irMAk2C_1#[ELeqr}mo|i5!N{9#/(Vȵů5Ūk+T57[{koZ5M>9HGlǧ?]БIx'u/MAkUfxG thַ!M{\<hʲN2rEmz8OYG>iGe2bk"z`dI7wަ䗣|ӆ׊4>MV6gόU2Z =1qt]MC10#:f|b..L*Oq{5-rtxf﬽?4\{=gk/(^4аF_XIo룘6ֹQ89QQp1N{spb>!__{zOGs>+Ht{eJdGX_d4m}يS`∷4C.qm t|iCb䚬QoY't^ СYjv`f{ooֆDfFofX_&|dy,#6c|N5C{E8V~!%*LB1B;`H\8-%+v[v5Tδ~ Y¿mu7ӍҬx7gMd=}e7rK_т^!9h{eFX,c[6܋.]zj^܄Q; Y+=eS˜Qp4MC»~H1whcoFȈM3lro׈ŕl|p;4+m,𾾇s+}x:mu{T-.fKo`K}邎M>-]Vދ3W~mK!&4Kg=8K >޼Fpd%~u^nėe[?F-.OgXUͳAO0KExd_ ON{[[6#>Π֪mףgO3XA?j9uٯi84nfS#P/g[-OH,^(^6+zp3z֒c-yH1m>ƒ{{`8qbC<n4zz}`?`x( S՟x]Nzxճŗ]s_ļ;8Ƞ{}Z/~X&ekZrlޔ=8`WTzf;b,ب7bcqXr<;|_ {0FֳO蝯{,=`x:>l-%rAVО2f}-y mQdoKv_lAmu8z:q?69?3G*E|xA[g[[w)+0:x5Wyʣ y=v/LOWw~۲-'|[@]FzmGcsm @Й鑆fFz?>Tɽxk[quװ({1.e{%sǁtijtQ癇FPQ #r 3MZJ].KpAyzOَy=ݬss-[/cwG֌]=]$~=[OY}`OH` yQB1~z>{xF)_SߋfFَz1{=۞]pq[oAR5zgzX_9;Ejm;1Mv|G-}Lo}ӔvȨ%^`m/4KC_sa#Ţǁ{g;ˋzqxƽ=ۿOem\~G]ܞ_OXAԺP.ۄ3k6zz$K?+3ܱT.nU[Ύ>?{S?CNE7&n}/̉KV~ƹ {a{ܽ\x{nт!T"෠+8z(g"X鰤OYJ zjuԓA~kJ35o.} =zE7s-F>a횻0mwxg{%^==tYڰa /إ7'{J.=W7{X<-7 6_olavrIXbmtggܨmӗ2zǝ8(7=힑:TGx26 LMN5-=vn @!:-=9=\O%0[4me/H'ay?j9b݉~:&q͒iv9#k`zkZoނߙC9jXFs #o졵FҺykھTtd}_?(%x)!{xG\_@ s`x<]϶;_??h4~-9}\E_giKV/•QG o=Ӕ/YpJ7rd~AAߋpZ?cu[;}QpvHilw֪42ļrǬ8(WoAOAa{z,k[8Nu͛sg(~%vsO`"~6:c{{yYG Oa~"؟,mqZO+UD&Q?*x h`گE"䪇koB-wcjAb=GÞIqg)a-6m},u}g%4v3s>gzUl@+bG-n/G߾)7?bW#E|fGHwg-z^x>`~ Y(h4/w'_ ~ Z(oإ07#qBC7eįG4kq/ك?cŮE"}5.Ù[4vn3iϳ5E-<܆ƙqv{$Yp>{~ނgH;{gO>oX?lf~žm3/֚=[3>1e ~"v~ނb-H_aoE"$9bÇlHۿ7_-WZ^mk9ƃ6| q.oAk=sc2_5G8#Z]ؖ5r3d~P8ro]CgݳLѵbW-%GS!$,쨟'eA1- }ҙM恅|dmA6^+703d:ƴy 6.eE4[|6gކWXEQb |~$e-s=G36w`3H\Ub)^uDS82ڴ8f(k>TGk3Le t ~P,U<3Z5r3Fpʟ/Eh?R`rMFq4o1t/ؾ#|KyvW/ZT<IPKʸ o8[`\]%-+82-:] <[HY/; _#*Gԣo^8OYo)Xݱ8~{2r;ļ0d,ŏTK\HS u *ѺlQ `_l+[4}󦠳3 dͦ^qIM_X͗67l| 9Yê7l|x f_<m槤>T iMR1 bvxem#Y7ٯ⑼|Mނ&8G [x$96f4doq}E>ZOɷj)\YX˪(Eox n i>ʕK.:6iv2U<7;?rKVިsX-HgCA{3{󻄽ǩ=9Õ-`Y<6b ;k |]񦴌oFem=x/_6Bqx 98Z4\_#6XlAf\3^_iE|?h|h +{&oA&˼. lnSul\=LqqsQۑ8b<}+L^۳PXH#6Xm}^~Üh8}WDErn\OqEޙߙݛW3ovĆR)[X8Ͽ9%i|Ù4j;V;saEk3Kw s_yuAC">e{?GYӲ㋈VMK{k#QQ#_j3;{2oA27ZPG Ҩ݌jpnwQ[}q3vN_]C[~?y!Sw3s?Hys+렣\׊7UMr[пnB5coĵzw}A=2Pܢ_|*p xh}Kox wϽA"WryP~l͏ Gn''}㿛ݽ}}?mķAhu}-bxVLx Y~}lϢxgXYZd~H^[ޞ؊>_y¾@7o-(ٷo *{iTp;~3 }7VLO*xS?x Xx W%K~z{p̼_ˌo[пug޸ p'-oˌ_[a |jȎ|M-Z߯ӵ_[mx o%ehZ\EH;<~3s>_Zݼߜx G:$o$-߸+oLwe-we ;eaT 7zU*y R: 6 (Ӂ _y|ނox|?G }O^_ֿ}}Wu{)6~[}"雁 }8u+o%h. d VXBނ6]Ώe-?aoJZ7]g#yx3g-~~^훁7o  ODf7o @Ao* x3fo+oߛ7o x3p^!ںIENDB`././@LongLink0000644000000000000000000000016100000000000011601 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/graphics/circle.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/graphic0000664000175000017500000007170412641367670033030 0ustar jaakkojaakkoPNG  IHDR/ AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  $iTXtXML:com.adobe.xmp 1 5 72 1 72 254 1 254 2013-11-23T17:11:94 Pixelmator 3.0 R:@IDATx}]q-HsDRiQl%/reb˒@$Ukwu8ޙA^wTU]gsܹpἝ ݥ=c>Dy#_0Ϋr_sO>sO#~i 5A%TJm/{qso}9!v< I/]3lldh`:~Sv}I}ׯ_r|YsCJ}< I?0YTaj<*3fU2ϓ~5۾ׯ{K:0Gw?>teeOu'^X'+I|g?0>3x̓?CؙH@qWlٔkOv'ߩ oiLxuիփ_N+κI22T{Pcg^2%lOc+JN5Ӏ Ug<{>o?qmbꊐ .&tWj IaIIYdv{B}P5g?xШ'W}YP'_{1BR+5 j@#阉&w야)tĿvcD+>U(F_=NT}*dc'C]$aAqZ.3e\:;Uc;Wx@{9^Czr3-X'hCdٳ:e\h2@?>‹C]azS{0 ~+nqVNlQeHf/:fdgTqIf# G?h^N}\F~2uɸ.kH=Ǚ!$^{wG(<՛%ҋ#[k3ΘU";|6fO'|~!>%+x H/k?9K8+#@*P s9vP0XєJBdHHK1{?;>^Ϩ^x7!KrN]dY]"b+!$WajY~^_c߷/tː_|ſi$,bk]vVeFvg.c|+%|;XL-4H|V:MyX'ZjpF*jm;KK356X I#%}4V ۀ#zcۏOdg{xf߾C׋΅8oB$v>ؒi [[xve$clѵ{=h$,G^f3m&=U{c He : HC4]x (xF=oț8=L9Jۦ޽Ƃqi{ hԌ\Er Ib#cA7[i{>j #ұuu%k{; C?ߣ=/`&ə;o6ȝK(faE\Pk+-@Kq a'BZ$+X>W^yws{~|]4noڻU8.>M%ˎT&aNE@ۗ0Y8ۙ%ߴdGNe)(mQY]nrP>y4H >sw{n6G:/x\UzcwQJ Kg.|˟/ӟ <]=Ŝ_Ѕ gd]S#wc8(f:팂[9wӥIn;U}ͷ*۾Eϻwz"p_ n^ %=X!)-ٻ*`N WHZ F.nR_wN}}Me,٧*qZ )S3՝o p7;e!kx՟Է5_1{רcy=fHf yHV1d P)!{ޛW!yϴfђǂW_ŋ?i+;}y )R :?"`325Ȫ`{?_;NS|(M9ދ{,f6 /`'\ b!(;^NmRϕ.+a9Xuax{?m2>vG󁽿qqRVdM'$ ~d蕱W 5 J:P%̿<1ҷ,1X*D0(SQnv*k|μu稏gVTAoX"d N;SS-'R6-ЊqPeO5IhLH9>GA +p #a9@dX05f 0g|x&_n~MU'S6-oy_ǴyقIɣ^Z tpgIϐ `Pe/ph6#m_j{0mDH2sͲW! $j x筷⿉)ŮQ>i۽V^!yÒh|c`d'{{X." %u"(MFw5` ӏr(}3Y*(\43!7|@w֟ &_0wL-˰du*H Z5B'^[2:*RuvZR06s]1'&jvi.De(G\w8xL̩[lsqH;Tc$,ޫAQ//ԉkb1s2K!;SrtX/^{Y0Ì~\m7oۮTYx)n?zE|}%UF St7҂yڞHNؚ@q f$1-1șZ^pbb=M-eMIkA+`?gvNg(2?IW2IթLn1D%A`Jh+LLM:e! ~-qFYQˀy[_=Y^H~psY<=+FŞ# LM8e\:;ՆX+MDQXr3Qme;y1YRHV+u:?Sy=x W_}MrҙP?J ۑw%5Ne2Pk,hE /2˝mNlNN2HaTG ^6[@;g4׾(;-;/-?ͤ _f׺,p'y1Y:V!If!hEA؆Pl cTp)F&]ABT&, >όڍ iAxM,|oRf9t%``JRC`ڋ1Nah%`B'+ICzLf2.&v%XY.w%}-<ˀŞE8'O{?{Y Zh ϷWRf%l5`*5/bӓ4Z76Snʼ*FB~&ˠ#u&4 s Z`MS˝2Ӥ~Zp^DQƩE&y'J*9`"CNF8P |_ƟD_'B`Ʒm4( A4ZhM?_]~}4<Ƿ萔Uh\:L* qY0s5 VL}"TSv6Z{ܺ h)$;c;Z% +]dOl5YaLO-t [ 8a;]:yscэקY!XA\<0.XZ m?&}c X1xNH>W=g8X3YH eZn̫P; vSYx* %E98$҆F=v=_|q۞SطHY'5S ;Ǥ;O̒RsU #-J(&;cՓDW\B<z97IRY^$Vjtp{4N'n{@y#W/9tnx;qÃi+ڳiSbwXr:``UpfLebcj6v1̪ i,[~_0C9}ܮl8`/Zx[ ?o@moP*Q ~T'?JPz$j8[X'B6[L0xsjI ( r$TX~t m<[MS>x/E ՃZ)SX%B2<߰F,YX Fy@FTb $X& M3ZO C,N Gf2܌dUZ󸡳%s T=ÜN\'+^I'O=|:օP̒K%ŗl]A`SzFW=\'BA .(d>b 8P;9Ȁ䵐SKnR>~G :?鮏_ ׿u^DĬ-\CX"r4h`"pm)v;H4 2KEk1ufyl¨xE(Tf)R Ī7km~;lLh׿z*y=쳗\V'Sy ,BBr myuilM0ʑ}bG.El8 3x,cd1:R55K)tGv ik׮>߻35Ϳݲ YG(No+nL-"M*F p:^l\KQ*e< Y*I1de;WM{kFqQgNP>ӏ>P‘}6{[Ld!v`:W2[nTo¬ \pF^YuʂY H$bb_?*c I'W?اQ58~bM>%25&6%_˻G8xmnMGu=h\  Cfhsyd a0w{Cۤ NQ2PI<~$"&9-RB؊pX5RrE!yQxa~xOFn`p6*eh@lqb4ޠix"$^<6QpuN-MXi)hLL16Er٠JXOZg]@HQ<Ҏ:o# +<> ^811>&?G| lZ@Jq5d3q%,eGhAy [c ](qS"Rf`ٻb`E~2BE0{$LUg= xFFs!(^BAh UYzrecFi<1 ( TLl?|;Cp+xx#{~䉅!lhsecg)jU`ZW1\BiQeX圬x={d W$Ms;S$:SUI6h?y9q$/Y Cc++. c`kj1#aYR ByzN}Dz WI'0F5~|cny&׈-xXo8 `3ʕ4N,QΰыUʖvxg3Z{c!ЌHh `{ xRlAnǓޔP;,Kì LC;r]瞳KmgT/H=fNv인ڴ̓aH]0y4NzbPz^V6²98䅣g-+{Y{v~(,ẁ?[:^I潻>zgl*qxfc֌g7G1oIw*ˈ PLcO(b|'(vw{\3,9àzhºC?eUat~[{w}!kvt7ب9g}_S%[$GyUC:yow* U z#CQBu;1 ۔0v|-`z9,*Œ$,SSpulwub]vM& 3nS(?}lVr`*lgL#{g ,7lJcj9i,+Kѝ\GalIJq:#ˣI`NCEUve߾ᱻh~`|҅2WgjeYN[ឪR` 8k4¢"g#I7 '0 Lx%8Z{y-yο}4̺3(H"7f)Q¹&f 0](e +.G;A% HDtTF']Zh'ixQ(Y4KSOc/m a%Qw>^P! @!TNz9. .6ZxB+lGcC6ݍ (=kض^Cr1)=.NhAɍ_'iXgf* MUQ9L*ګneQ_ĵٯC9vz+>i, 7boF + WjʪY&*>D kH6V:M[~~KC8f:Zd9T)Ig%+![ryR=ZB/ enz3.z{H޺Qܹٗ0Q.vm}l,)Ҳ; (td'jVY%PJ>P6>ŹH} )Q+&Qd9!ȞPai/ hBl|ղb7; T;ʐ|P"YQuH- b<{~5\ʕ@~fu-AIJ1Q[МٷQgn'DycPr=|Fo6VR%ExoXm^Ov7e˖&\86FBTISaz['URRQ_;9cA1n3 b)#r(yE{V6ݍUi׹e2;*е4Mg5j$;e+-lr1\2Os qtqyKQeb830ؙ^kl#kT)v7VK]~:K$gS!lܲ ,+$VاnC*Qv5D*@"5A]Xdw}> P}5SΉ 'ʳ+ ρvcqaj)TFҳ$Xl;%H ѭP-6÷p?4ˌŢ5!R^TՖ.~v FtU+&>R$XGCV5ʩ"cxN"N<‚\].9y&2P(ydùY:Aԓ;_BG;s4(D;FV$tY8PUgY- Q9G,Ip"yIڪ{6 YRJ> s.3MnsXxgS[?hxX{xKqvOh?rrYKnSJզpVT5l`' +ÍU()mp> 'Sf5 w'TK v^\<|j{fU00={r!49B$cA2#k1tym8[uj,~͵V)*5>c O)o3➳bl{` ރsɽx$FoqjtbOwpD PF-kvpj|O_c"\=k3TB$zd_c]w7WRefs 54hnn|PYPRyx$C:y|%#{JZޚ^am8Է ˹ֱ2poI/462;~Љlgķ`BPl]jX#)2yA.A2䷖.hfL \ri)f ]cw$kb}" = ={'+$2ΆeH#^:CD10Jd+',ZJytX%fU iሜ CGG*5Мp+PYka8ݰQ07(B3SC{J>yQίG:F.ubuUff-J 'xw|[+6|dV_0p1: Ҏ;A k(X^Z^łz@@{&,`Z!! N_dP۠JI 'va<ڎ5)^b׫!4m%Q`oLdv]8(YCU I#ήVpWDNUNrɶikQF>һVroC:ս3[xkCKOAM[t/gIuon 6 yxv- y jAlh9K8q-Zx9gY2Xk-M2_"_T2$iĀLJr5ouymVΪ:@}Gh2d*5YEqXą'ղo8BBfvB:wA:cauuoѭw0 l-S*\^ycdx \gU3>V% `C=|ʋw:F$7j:LbB2c!:M$_Ej"\+8 4-sO},>huB:kUܭy9кm$GHN0 Y dԾzgǺTY8MOes Qe(*l#܄c]H ;)Gkµ*hWQչѷ p¯$5JϢ؋ڴ;S9Vo,s $~\ ^/[6Ps&P.23;Gf0F 0x30[4+Lbd2 ڭ1D-Y ru} ԇM/N'1bNp ?!vR_y% @Ეi#2XQvR_"paSF~T쟪"2̛LZd |fa@Ni7A 5^5„+u 7\ ȋ[O}{w├м$f&,Nrb>p ?Sɨƾo{e? WGJ F7:*n2Q:(}NvHpqZ.b8nj0Bl5?Q<ʐ7R^E* !6Ca+ooDpzu12F63\YN.VŨ'm,R߅f$_L1GGE1[{źoj5d ٔ`_U_%sO} ncO>A WǠء7'$"7^ 7BݺnڅxZ˃.k Kf:^.·V0"K 4}Hk=>ƷX>pM>K%J'WUiG"Y 穏j$,j}!kUEZ["Be0_J*;Yt fY}Ԍ 2T )m(|@<h)e~`$K~hI>PZ-d}* ӈwר5X_ʘ?o.4QD %V*X4z댷U52TKW 9M# S[u@z(+{_+}xo!Ej[%Կ}?ZK)~X`D!@y W${"=srC34_LĚ us|%p]$g8hc'd'RbRdaiMk/IbhC"C'(g@ qמЬWSI>䜳A-Ͼ$f~< ֶ͆>_}|e I0U78u[=29I7.5eלEy6Rຓ@ n1$E&3[Zas[*D[Cv}e)Aef?f̡XZ?\F̉*#y8k+Y({;Lk[g-V:[ kiU(KИ[9a 4(;ERv޹zu@f3F~n!nPzx҂.XdpO}dP*>iπy2Y5!@ueVf׎Qjihc;#Nc?J\YFć\j.$bdRScqOƒ$g7qn>>@}w^%6k@)t=3BPsmXmV "#1~L`rk5꧂-Qقepw5LiK}auVUeP*RNSo޼ _Q,~BwpFŖ6.;-gE=qXddKrp *zp35 TءCI+ 462;sh&|Fy4gy}31o)@/D%dt.>*)H2|`v䁝+8/(Z;>z96mFd Z2Կ Xsɮ{}ОJMݣ"~XcS׶ZoT&rU+C~p nTJObRrb3YvM##IH{Z9Xq# !%ǏAh;{'ԇۘo>+..d ou[.u.qc3Vump:yrNv\SX\^tySZʬzUtB^,m4 _h1A͛|tsEdӷQm&`̊yЦZ'RՁ"P3\b?_1k R7;-!vJ.!C$Ur%u~p'5!BWyY Կ|`8 qض!vAICdDe ^~_.$`֎U uY׍.Kvl|ugKL;Ǩc-2! .ԟ8CXBbZ@<wZXw 갇R)PSG>v~;Οn\R^&4j i"$IDATYΖmÍX +yPpT#(*p2@!y\eI)fٵ n-}71rE,tLvo jH\!vt~FY6'<:Rv^C;]ӷJ o@ЁB q{jiuloeE]51L+8}n863,p3 O%յIXI\-SIl,8cwbKc3dgOJgzLBu-E >bBtq.,W<[0QacǢݭ@ rpf/[b_ Aؘ;cPkV 4zh`笧UVUԅ0em`_|?\ޚeCkC7W.:Vl (ˎQO{' {r⃲0xvuQ:sXFu7 .,Ox6 GtwMSeWDvu w*d#eEZYBADP$L<3 CXJ/'q9| `Piw:ei46NmI1n쥞Rꑀ%sj~3t.ތ煜-_g~m>+S4?`:lZhGܧ;Eo/ GڛDۿLa)w(zNJjeXU8\vXgB *z<ѵz*\NQ0t.!{w}vmv]*^xFީk4vNeK370GqܑGPmyvhN-KF4fOD;҇P]xOv}oKs}ʶ{, X+iXY2l#Z8ZH0U26"G;c{f8](=Pl H$U,0QF + ,=x@lǻ{?W+bT`Sh,YmdQf($)S(G~\y% !P7hZԥD,Yթeɸb+nZ~o;_<`GUxOxgalӢ ;:NcF¢ e5 dzgo!%{"@/)8늘,DUWIbeeS{vӠK!9$ȇQp ަؙ^8:g]A]Hɕ6~fzu z>[ n嵍%-j61'B7}Q& r&!gk!b@›h1޲=l ,GeXF-nx F}^@UqjO]0N*,ch dQy52”'~(1Vn/szj˄ة# [?+ ?x`ڵO^EUs[~soV,BN㊥| 0`FҸa <PWe-OSDZX-A$ˀ̎K&"$n7kAHA&`U9xG6lOa <7~:hqrŧȩk4v^Me$a1l#TpugT|:G- {)#>zrqdVcGgA)xy0s禿62#"v祌]_hd>!0ٛ]7ӗHqWcCh78Iy1T4aAPWl#5!%9 v..05WA-'u6L4_Pܕw@ |ߞ#ztG^\v=o|Ftx"fĒ${kNP Ws#aU#\f5I%DȥQ`/ H<+9Ksڨ,t#;~h۷Ȑ)P_4 "Jyw^58ەVƉ8 JRL' *4bjX 8l~#Ԍ۰t(IQzɬWlj2_g|)w;'7ȏ-5]9VM:A`{5%-B\SAx1ܷ'zfw1)%'B`9mF :a[AHxP;Mc` cmOi6-kG[2'dlXKAȢip8,tB`4ߏ 97fG="zADIJִJ@rakH4sȖ'qO|*픩 }G7pVβfT^Ww=٦CcIC#XzU q>T"r!ZxY8&Gl'o>'i뫦>Å8Xtw¾|^Kn@WFM8m*H"F usѱUj}FX9JfdS zHVoPR|NGqx[=[o9l95KBgZ/zCZ:5*wu:<zWj䊅1*-x8*O QG- ?Yi\E՗(YWʓ3MQjɋC6;Y Ńt}$rM! -r9JC3ՁF5?qގx INQ%.S{8edE.\WWE`K8WY$Z, 5݌.WB=DQH<%ْ+!K!O5]jg4W%X g>3x&>\ 33Z$=y 0sOyPMx_㌰:nJhddX2dgz5(H1x9Zoxj ؗ&7'֫%gc$ҏa}{wI.U@t"'< *cSGԉP[ 58ڵn] t:k;֡JHk$#<~]HjgN}T?8aJ%+:"oz( UH` D*{V-{^ C}. x@>1v+otN t10bN~;6NeqT\^[3nLz HQjbeJ .3D}T?b,lQeak{ޅ:{Z9!:?g1kR\P*EP%o j=XLSh8+26Qh>Ou>K;ib|f%W~XE, 256!%b3^-%CLt7M+aKZ|w듦+V@ё}/}QMc7cz|v$[87F) y\tFjy`BD_$K˰ZQy_^ty wcϦ`M5v~̵͇$tJfs^ji$Cf3Kq:Op_QtGDitJir@9vc__]kǔ*؛_i̯c8 bcE%XZS թEWA{]y{v7q;X/ֺ)tMD٣Qkr4 ޣU{Z;V.uspsPj :WΗ8r* ^'lT(n\ShQyB6z&}$L]"/ nfKV%/ JH@.Y SW\M6("C1 &> [^ȝ9.|"zp~~:Vqˑ;B kVRSՏOpǰ|Gyw8R)F{(l+G|aS۷>EO,(%TUb\)\$,5B L/w.s{c͡mxdLxd7δO+25cB/Wœ"1`Z,&c O}&4d˔Q!HLLlx־;x}jss=ϞgHWZWņ]eٺC&RǰLeEU)+|g/i\1yOo6u"x_?f!ZKFNsӥLU16Lj^Qa8H` \q]+睘su^4x}U}F}&]w? m=@gG0c93ʖUUn71{d{U?;1?sAރ._r@y|ɣ ˒1 Dvl2p>}j kWsy;epO>أ_eZ’edoYmdqzdf˛AzMOiZW}Z ӾtW}`j+,.!ׅQeHf/fdgTqIf#ds O?S> >|?{U_ڗWHeE\PՔP=؁;{VKWƿя:ь`n6'nw^'^@tEȰNiJjK|]]l*˸$tvxSyoKO8xG'`4/LB{E󭘢/8[Nє7{z[phSA>'?W._֓` S?c,zm|U\./90K|" iNW- \\Fz/8=s2Nԏ2 nZj?⫯cv\2;, u0[>FZU423.)G!{Wd©N"w&qWlٔ{h ={{/ 5][)^A{=c %fJNթ6Uy:KV`< /\2qQT!>O! ?z ~Dytt.qdu*x9 =F|_+_Iᡣ>O|>Hx X$ YUahHwi1q%TO#1Bg$/ɃlɟOGO'.Kt gM}z&&(ƍB}^{?"@߱ >(o_ݽd}%UxzoAX<V9wym߯MOޣckxCGѣֱʥadެ /”bA{{g8gV9>٦B|5 CӓT{țrW~#5Ԧ;s|րs̃F}o`>//۞8ƞoGp @`N:5grhᱛ9@!>K4|3זϤ+p+p+p+p+p+p+p |QRonIENDB`././@LongLink0000644000000000000000000000015700000000000011606 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/graphics/ring.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/graphic0000664000175000017500000010117612641367670033025 0ustar jaakkojaakkoPNG  IHDR/ AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  $iTXtXML:com.adobe.xmp 1 5 72 1 72 254 1 254 2013-11-23T17:11:41 Pixelmator 3.0  <@IDATx{6mKܒ/Sܻ-r{ Z%x$ u@o[`l[`l[`l[`lvw u?p@ʼ->~ -,t4?|X[^^~=^^&ď[YZo^8|=>\8 ;^8rG q߼y;߽#ȧ9wb!7_o{-;r(c}>zA z `Ǡvsޞ̝}uD@OZ8u ğ;wSgϚS  )l型ؿ_> ح ?JE1كo򗋋Rϑ,1,zQi_4h}"Ӏ~Ϝ9QSx0qZنa M[PὈIC_rBȣ |-i* &/--.J?gψjXe׋>V f>}|v~cF/(>Mwk$[*$@L!wo1?A" Lr[CD<̙sϣ 6m<^ aUקȋ:+EHBwF"X<0GdY"l_oT ŋ\ΟGX5.mO!ؐh^1KN_=!pM$aXxm( ` >+㇏=~$8 4z=it3._tˠ?sS5z=YcЏqe!hPЗdFD8 [aa<{܃Gvسkz<˗K@=ctj-AKbK@P^WzvSOjz ˯Wr(0o*BE@pO<`@ÇO=c˾{Ӭexr Moe_?=Y9tu%p"3Qلyý|o_4voK D@tLNNr Ir/̅cX<|}{'O*NK=>wd˗]{Y8A4=PJLsp_pw)$P}mf!{F&ӧ<}[n߽ b){zUnʵkׯ^wW\t"[6=l5NӅ}bOz()dE|wa}u #AK)@n+`TKbse`A@#lbG޽;wܾ}ν{DINrhp:*W~}/_֍ӧ1oJσ>s~Rfo)o&'O>et[5S3+ȿ֧Ӈin#=y3" {M8pTu3>0H p zE$# xVpXĚܵYáoΝ=d+Al6 xRi'CȰ,׈'< 3d(#@vfk@Cƽ%#m<C>Ϸ2̜.;ẁ?[!v%p<]YO"rXP@ohJqOJ6Etظ@f4@['̉Lq??:6M{I(pq; 3>pg1E9wDo/VЃaNg$zwߡcpÊM?f;gK:.j] zM(JZ@wX\J|\y}ed0-CiRxeF:ǎe(>ݺÇqGnBWP}, Y~z9e-:|ýhΗq7^ :eBgLȡFѢΝH`.g E$R#%i2GG38 ;8z`o=fG b gש݁>K4kX({z|!;ևhG|nȤ#hq|C?825 B+z4pOڝD%$yX,Y:E}F8)mX wnbt|?X˹]+F{™bLA}Lf_׀B 1ub(,- MZS9f8WJHtc}q<@: @qxSwʹ[6tYU}Eo3.\nBE6yv! =Lގ0" Cj2PpmHty?L'u$a('X?݃G?,@,#|&`a vsuewExYS{ǏɾEg,_׮a٣ikιN1fz}A?V.o@(s؉[AG[N偞VzYm2%6v0%GyCb#~6$VA.yH!ϗxk HkDySOr"zc!?ă8fawj?rx&`<`_ۙn'~iuEvӨZbXXӚŲb#ݔ@Qb_ZDj<9]K<% cei+Xa (WGj9 V-@@G<ZO:3bp!*/өy',Z7󾮻w؍;}d&?Op&}Vz#gL>26!C vHT5߇2용C7-LNh+;NOp4}E< *gp6R eX5<S(#}CH0~<ut۷w鿳gwX8ᇫ|$`9Yj7 'EWIUW$d瑊I[b?2Q|c˶ʁiA)dp?+@Y_6(TZ @C.^˅s <ϕe6,7߰eSFoB{,{ y<$B|߽KJ!`=aL @24?fL z9>Դ-Mj‰f4ʝCEz$ӟp=92o~'˼,єcD dK$ԿM~1ON6޷\]p*3܎@aÆU??E +p?b7{ehnį}AKJWW)/1HgA * ljpfԚX{d#+ΡJGjq@y5Ίik+(9!$_xC'Ljv ͏,|sJ^0ecx`tlLd/RNo : A @ŽS~%^݉J%> ?$-D HCȇ7hV/s=π2 8Z~ָ2r.?$ZFayt@M dh:%qoq!@JRc@<[pW_ =ge_,˪F*Aķ$U+ʼnIC.#LQq2鍓ze#=z\d]|֭/YѧX~~Ivŋ^ 85⋲Cj贺B{+1w$_d[bȫ_mCǣ?b>rB_,*nY8|1#o|' jgw;n߾MۚYgV|g!t*o2jȁe9wRZ+x'4}郫mkl* A8TM¬"99 Po#>&G㫾X@B藤y-ɯ~C};NNh1k>s~Gtp ]iy;] ,܋hg~6mTM Z u}*lxWX+pF wN&8MxD U:NU" hhmI trW[Xe/C#V tϥkM `*L!džYz g~Ͼ_<4\oxzTRVhDlWd YIzė\Ӂ vUiXh0-TI,v$%xd!`PX-xIA3Yfv+~Lowێ>|c_e>O[b@hf7+3*8ͽn z~.3ZiuX02xa5=Qkw\$_ś7|5}-90hJ۾@op:oܡ\)Z~}|%&^9M<ճO?t<<=/@>Cl/W_}o% UpHmKOeX  EωM"+S|왐J݅{(+?| @28Xzt?CseyJ#?ڇ߽nE̟~~c5[0{43-tNqoMNOJM.ʞb4R:ӳ_ aSm@e#.moau4G#)\x5_{Ŀ4$~|?"]!׿ݽ@-7svx9oaQԷaZI‡ - c5+s=p'G޽SOeeYRq|)ؘCVQy_Qt#Ue8?~R-^(%_}|v{x?oӯ2Rٳ9lZMsc0@eX!XTo2()"x$l=)v@/z6}3/NťpwCt8/R9a'C+:+:[o@ .=Z'Hmrۂ>oϿ?!o'Ԗ:z~TԱ~:g-_+n 81qvDo2lS˲@O@ dS7W xi)҉7)65鶠?o{Lmy= K:TjP7XC)qV1Q¬[| O4F2gy8ݑ.E41QG.-@Gu.o?s* n f\ Dk aEyB&_7ڦzy4=<\tXt=Z8ά2 B:[q=}8NJ${ 89 E[&6㮾,hP69d  k'"- 0DGl й:PԿ* 4FHQ$n+h$ncf[tx=2OjN5RϠ& 'olvŬ=re 4FN$Wb(*DD׆fjU"g[GMNcpܭɬe@_!q&}ȹnN-oUi !sKR=-3ΖW7Z=r,tr|DCYWhzzjk, W#e=C 6HY{/\jkwAkB%+f_L Ci @.$pyt#/ƹl!_J#]I?y_urgwXFv찊ojc2nuE9V/AmmN@qkX :͗ʹu ;znIQi#>˘qJWL;%q~2+ }(gMG-]A :YaL"V-C{Ȗ~!uئo̤5U2C*G[MsKWMf-:s#=j:}IGɁy1FV)ݡ@84s!M N+G" {d=[ڳ5:}}ُ^WO.]b@['g Z΄TiӎГpNgռVq {ukkEuXu'Ή7`) =|̧яn[~7Nm#W٫?!u~ر~5"" ڊ_Xg1UӋy,ׄɰ~V1_~aVNүffH *ߡ\)\*[= !߅ WX=//.jn ?O7?{Xrmnb졜Ox^ԮuDoʇk. v4͸TL\CӴ(xxV# 0'O &%oDneZ۪̓=> 8X8ܽKatm?rϹVz"[| Tyh#\]W.}kgK滟>sY*<-s(/G>ŽW3Eq3c5/7dTTGa 73֭N~߭f@~M><ydgqɕ͓;\,_O'Z3-[ʪQcM]׌cBr٨$ \3)>ڑO`C?1|Y-\(~v8ZΈȳ)˵Cãm2e"OsGeynNnZռ6sV|[ ^G0O͜-39,|u@P‰r(h&'Fx; e _̓.-ЉL9Gnv_~KlmGbC-)EWaU*I)x- "kʘ=3F6MdA'giz*T[޾]bigRM#W9ܻP^.٭=jt48/@k*93%@r$R:υ~*(uIfUtr7'j^y̷,]p; }+~`K3]?{6D+y WȿpajgUJ H^L(n[ļ!_Y}KwC<%-|lF%vp\|.G;3i)¼CD,>u˷h0 /=Go^ogb䰰cݙ̂6XT>zcH6~mJ8 4N*݉ x:s(9yX2h!UWECR+Ċa(o O ,v_WBnS]T?5fпr劬|~Tv|ն.a:^R'\mdz0neàcYEC|Xk|pԤ3<A_.o:s,F/sq盫ԆU -9o 3αɫ*%/Sޛ4 `v5rFVŇlU4vL b\nErkV2aQRoF@<, 2zYϲr첥!eᗌxKdy Ln2|w$9f^֌FyՔ7Ky7aGQP X`4[#4MTrxyQH|l1>6t-%3\ 'aoC}-ml+PW"Z<@#ْ{9 4N{G3[۷UG.؛x:Mq|O#~#|9E*6M"~poYįo ;&T~ڨ|ߴݻ)Ž]ULߙR_`,Fq=FRfv 0x+ө6ʗ篊daptƋㆇTf 2z$MToFQd0P$ e<vZ Fs<vo֫yym< .kK>C}]S FST|-QKj//P}_ ,Mdcl_)E*lc3nOOG2V^1mA[0KpKMy&ErEfMl@s ̘s'F?lb`V{*] }F=HP?{9i}K?57ʵ_d7_e|=€ 5ɘsw;^GEk ^A<2ʯ-B'O3gri{9ׅ>7dQ]w gS@&c̝ન.* 1똠M[MXN\ctXG-cը9=]Gz۠e~0 3Uy{G]\Q^Ӥvv;4 $R*S*LpG~v^NOc8N~*}`T4xZl$]C։;r>Q YX[cpˀ)4o9y8K=kT\v)c\(_i*l\\/ĜMӭQX:ŧ' !^|oG%'J&ssm~gmk,N1Z;2#ҚV^duߖC>IbxT;獕5oxEPA:Qg8$d_6䊮uLnBlS4Yʵ3gVfttI❓ඬQTEqT쿝>=n=$Y UlOb?{ȵU?wm'2rl._ ?-Gs<F%Pajyv~ CQ_ =*֎"))h,5,܉c8%- ߄i6zfH!I<-۪ [h5Y rTTv;Y\~CʟZ&y}N&i8Y(" J{FPMRXT>uCgR5j51 9[;ܗ[rHĝ,<*SZ5$S5|[@c0ѽ #J3+Q&|9Њkj-".Ƽe? WUv'T*U_ D;b_c]׮)~bB^OppT%ݙ!KQ>D0(_GK9DtsKB=Oe#è]A|wݮN/HLRW8kc5`i>K9/HO_A"CUrRb팆z=76OZA?S*_PG6Q'ΰ!9jtDQʼng.||y&B~QBA@[TR%).^]3JJ̖oLK-P@ $V~h $ٴɋ٪̓I9|J+rqL*T>BA3_ͅF/70Hw|erʟArS-頬.pm*54ddʯq/S9 ]J)"yޤB'b$)z  *Ge HhpÅ?d 8lDؑ5df+1[p0xv Ȕ|SOJiH2q8"G.e*S6~pwgP }gx`Di%S$$&)͞(~u* 9EuHޝrO8Yw̼˨bVS*?,U*IsP B@됣g'tѩ̓G%SyZt<$am6rH嬅P)[.m?J+@4c`Ϸ:bAR3DF@ )^9= 9~T3e]c9:CZ`i+ayrWK9 x@/orc/`th7:[$-u"elm;0x`1BA6*K戇 )M?J*qJ~zsϽRhF:xQPI()5T+bgs֋B3Y SAȉw#}^mXH!y̒F$2m5]Ń߈Cv!7:e'윒z S2o'T[ﭧv-> #N@t 2:olgkg<}Ϝ~6PVs9-u=alzkiыʫP"ѪEssLtW+671a9#(|~VN-_d4N*tw|q a (\!& ~ND)$Jہ.U-o}4X`4j(ׇIYBhj`OmSNu+9=WvV݄r=Z|z+ڢUzޕvKd`2}1[@NtI… (rO}BNU0k;9}mD{E?}Ue6DȜ+I{:ZEZhU-O Ġ&_ai栯¢@D`~qqbQdT`t_g XO)kTt^ 6%Rz2s&8;wEÜ:1u}g~TLPJ-2Lg/g@M9d})l'ЩSŚv˘Uukaϯ*aJPT"Hp-oA˓4zڔRՕ s0Қj&Zw5V٢譵Iik9JZmc l껹qsY+kA?-0} :^nnw _^M!Bu_i=ܪ0̭V/>h &Y ֮z|wy>SMD{*E#_C tįB8MLwv$xf{&7\h7h_>9Sѩ=+"R1A"@ktb0X+J{ap 6}T}feL{-1*7ܷ<-39^Wy>SZCJ{fwuYKy  ]=}1[@oʗk`t^s?O@aU^^?uڝw3 n7ϧ%aU8-V:AZ?PSJX@ Tr6RNPtzEC7o5ԉ:?RZnݡÅ(蔵2SK~B0ac (\'<PCz +.-+^Wr"UDl]cX*P?s|Y]9eeT t2Z=Es;\zQ߾pN;y$/,=-}~V%ͳ87 9\diz֯-U} fBX!G*᳸jJW2e}ⳝ;槛%7۫N m] (Kos֤ϟ÷qQ+W TM$(Q]ခ!|vg>ZSi;. 绠n\W>\lru @Hf Il ꔖa ,aol]M^mWyMBF)uR#P=Urs|W^<ik=H@i MS8%Fx--)k+[ܔM,Awu·z2ETdWE@^nu@Ћ-ŐBztDYTxbx?]5uYIo {Ǚ%ÜZ*E)(;ERJ9DB t:QҎ|W$KFwE.PN\@uUt!b@,N+.Ɔ03IDAT3j H[UeYbD(9%,D}_eh+BSckpt&4}V9.3ݺܥq)%yJO2Rzk*Ao_:vu ߷.Y.V TɂKR]k? b9jO[t⟲OjL*i5܃JpUK" 4tt{l w½N槝 @&j ] kph_|R7Ő*Uօ\Zӕ z`ct@|LTfBIAFnjkr?+}듫7}%ЫW`ц2ӵt| BU%J4%9}݇T4_0 UV6~*[\u[xO~T'ڟ@"5tITe E/.l熮>6j˗Zh".N|\!GgۍVV@֎(H61UF;YZZdm5j5(`<[w'p@_Fv"2lhc0U^WTzZJX }̗~ g9.箆>>ZK ^q[X8 WD2J!-_߅lVd:T7Ho$a(su[iH'1 fje'+ϟ?_|,<~fk{R b?j8~jmf'~u5{m7e;O 5ūPprxjPX8(gO<Zנ$i Yߔ%NW \ՍoF6t*V NXO<;J:ns эBmvIhGE rYd{2vӶ2kEv?}uedSC; ʘcz9IqCz49-@WƎ6yJwafED9u":sL7lY}qOϰy^uyĩ1Gַrf25_RvT5 f㋷RÇc~ џPq!h2 0謝kG* })617|6\ǡ(y;t 蟝BG[HZۏY1ۗju$8<[gwYApЋ:0.|Z;hd3LrYߵ>4yxkWmvu GAS+|HDQ*:7b͡b󌊿n 1÷Grupex! A23ϳvYf"-Zkυ^hw!~GqX 횔U5,U1!h2)ua̶[Ù}Y PQ}!^hD>F$@F+*Y;KKocfsyRާj1ˆ#KiLvGſI9Q|Dj1rA'D_s֥Nvl"kd9Dl#G4Y?Qt[5Z`ʗOs|xǃ-hf Gؘ1Z;uEd3?pռafD5pvڎdY?y|Y'#< .:.2M<L_C\12FPu!cd^gsvH iq_0:]m" cyY;fd㧏rC-.>"IŋN:rNDœGU& z}yOeX>|lE)~a7+;nCU3\=ض3,'_ C5zTRY;P(gOΣYz~ͣAg;z ,+q}nᕙԉn-^G'ҕ lÃ#*PQI 9i + `ǭ܍cZJž>}ŋOaPV'0TLg]gM'%*-Hh}YK_qюTK^LOt\|YGG<XhvػSFg=zFgC'<=z b'NPI".{n,`xl<~u"MޝGD߹S?N7Ev4{YCF+G.cc<2*eisE!)%Tt vկ @Q6sJ9{ #G0|ಫ-sHzkON-̤Miܗ/#7QՍϺ)u/ONvBVFbַ7DMC|vxx _mhv1hNO3ht]/(p׏83Dw Es="1!kW+ ,`:(s\h:"炾7ZٴLr{,b7;*=ybn20#'{9M8rJROjl MpEDט>1g.,*Y:+egL H<ܗʖ@JGiv>*@u+i:MDcܢwsI^d&߿sO?xI§W>J2{ *dV{/@\HBZ\3 9_4Om+{j+!K? ;6`VA_E2,`wi|T?hA jYn0ϻwnO!bȣn w@~/m0OEqm21!ЇCW}5Epy9$)!jkkE¬rF[ހ۷o;6>CP-@ЉC[[ۃcze}:IQ -0G鏯U },hLԆx|0ٴ>ڧHd9n}{?YмqnKOQ4q||4a? ;q˹ Lo`@ga,Q[Pf 6m/hƀ-}5ejk@?M.j4hMz_Ia{u4qGf_-@au>tq|$^ c8C/*]XKT2650~Trwn\XWN7;ONL<Q  yb1ud$:lS{ wQx*2AK1x<ٕ空3ډm 49H"@O:^;c%Rι%|W裼tVn6]UӅ3Mjlp1u^u{X7nvۺrЇ3,u"]+ q1{B?X-xN3l.hw70G!^SdW:th]@G1rm('+L ʑVuU'٥g&][>_kFY~滘s^Lie F\ѐ=w`QoPL .Wѯ/Ӧprb5Wo[XAOW4Cys3}>F_G#l4~i2Mݻݸޮn- ?z vt:N*4,'% 60֙i"MiU޾`EtE"M>w\Rcw`%`k({}vat4t(bP#^:tH<pj&*޾ϭ[wn~yRZ"c8?z e@A h@L٩-? 4yCVy?ؙ|XsWe3޶OAb3Fuj4oEl02l* R8 ΡtQ]^0Xt\ucu{HF|//&~ K =rG{{W 46( xvfmm1xRbeiY,Y4w7>ZaZX6l۪y_$?9 JK|U N H k"VG(ZA6(j1?5:|EJOH5y f޹{7oLksmDPwh[ѣy3F?ͲJ,0YHqD2/Cz'(aѓ\'y 2z p 4F|ž6htJxtq&`a4h;&D߻_0opHh{ѧ$RЏݏ-.ٱT@йsM1>k"PulPC#1pfܡy $k.K˃<B;_Pݺk>} z(mZşmGĘV3WS;GiL h܇U|s,')HP|qSP0R&< Jl*A go{NXSeOPF9@0,*C F=S[,L^;ajlg}olOHA-3*ŏ$xV Izl-ϑ#ӗ7Ӏ;+ @6=|^a5Kzy iږ72& vhjEQ2@aIB)Q±HW!] VԖ O;y2vnOm{*j}ܻP~^hPJМΰxL(2`۝^!^9EQ_o9Wey||FS3x./.wN@j| % &Jrq#ab5DҠ٥6{L7xԃmk;O|_An ׂ* i.ErHF00@N~@P"QK> ߮fTkά&S?= w$Ao7o'=AI`C,ޡs Xzo kq{sO}VanvY$~"+߾|$l"i@Y5_}#>>p/O~Œ9,:_/+#Cٳ#2 [$3-#{J㚽D;*@'4=A33D:jkNi.ppvb9p?>3eݻ_Ğ\mA*VwI؎),=GO|2M_rWM_TkIa9Tne. 2,D1PH".>y[&dv26spS|R/Eٓgȉc٤jmݻgasbO%?M7IȬ~)+&Klja;T*)|X@]?!!l-u+ZZ' ̰,qP):&DgNU{oP 1蓻 -_j:n ;oݽ˼7mE@z[.`C3VǏ<"oK1_I 'a髚}u17x}C_; &p,{0E5 (QU]y(pS9 ?sqf= pГI#)b6([rbF]Rgݰycfޭ{6$= K/>]Z\d4LP ,=s&^5 Iv>?鿦݉r/z;Q"!3>8@Oz=Žė뢍_*{L: (gy$z~_SЯ;:= 38>Ye+x9D꘬&"{39X7o2_bScvIAF.gΞ'neJnDŕu&Du\yɆ! `iICgU߷@@DƽdٗEUl,Od@?rT5M*똎%/Ǟ!@4=ǫF^/;rX'6`HÈ1qˋXg3_9_VӐ/>_8xE۷<9s|:i_PQq!nF=pze0t~m $_~m񎑆 $5N+78Alpx/GA) rO91~}cuDJ&Wɗ+ţ4D߳+}O`V7nd=ν{ȃF@֭SN/5i;e(.3M|V/}_ofa KǏ [0C wzjU=kV2PO40twPqO:q;)l.՞iew9x>Z~{sSsOݰݻ_j=֚r_} Ċ[xTG_; "ϟgܔ__LN@8*P&!;) e {nT1E_d`t/i0{6~O Yt&0F&áWNb h #n:MZe?U刷ܗP/@zܦQQy\(w@@A'ê:n$h[ :аuGϮ|M a==/gߧ@v )%;=s}I9uO:%*xX2V)3!2Sзh(l"ԡ%`}?A*O<\w 'ףxEKg6Nry}\fgRb [?ygޗFvc[Dz=1'K"įv ajπ5?-l -lgi2@ ';aR2P'OJv[V-408p ?lx*έ[q;Lezo7\$pd@R.E{!l;^>Gc;Q\*K\h{#_͛7qaPv"/׿ɍ@}{Yļ[Dmd':/-@ .z4>-LĦ曭?:4 fuWS$>LJOy=Bs}] Cpsߺ{7xi_:csǺ>Żnǃ_ZZ9PK6=LpL1?ů;v1H5B<;sC<)E" 75_ųI ц;e~2T@_wi|'г7YQܿϫer)n碟vd tm\xs=r=uΡe#)1J YXfm|g^AX`. :1sI~EOstZBpڄ5={r0=Cd-/DCqWM|ѧibGKK_pSxOLR4 Z:tmveң"`L 32vˌ@ {1+?{N_(݁u1F<mL_==GKc/>t~#w]F]>A0{,={ ^sOo4+ So{A@\[x z۲A*qY'y4T# w qh|%& ^Aa?z_wcR{Yd59LӚ,Grx?gϮ_~Ν>}Zm?ҏ w$ -QxLw#|KGBm=W~,_Vh`=GqC:1\kpYQȷ4=SaJ%e܀g ϚY r }ZEV9˗/^t7 W= fWlqR@@O!_F^`JwBCV#'K' #5\X[Jhfs"LSOY8,`?|ksoa5iNٷ([e[|_xʕo^׮_`>} ~ψgՉ?2o=mGt< wĀY5@4&mPE|9qu^|#Diwpp )Ç|'VX<dիX>^vK~ΈWY^p'('DF|9ZqOgK8w۾wBTOrtңNpq'8;W&^=o[..>}~޽Gxւwso]r\9y(n˯0p zaH?;-dk@_e@G5;S;6@nws) 'º:(пYU1Ytm/#$!mޥ]~]._}+׮]tKgasͣts7~@C{qdBd}_;յjj,߷y% f=K^Y +7A?q)o2Uf(Kx@A˯.FP.ƽ#Z eOpwWOu'hƽv|(^ASpiz=!͂Z?X\d^ܹG^r{ t +΢'>B_̀Ń$.pk~_!ǭsϹa @uߕqC9Ęr\֋q/pc@ςf={O}A?f,|^@ Ο?y#q f +P`(ah6εxd&-LEGyN)Q%eEĨy^gEbVlqXyNLRE?f 8g៵O g`.رcyt%b@l*J|s, pÝ&tg; Q { =f-7 nik? `͇O9t ' , ^rՖWgI%y{ʊ h@YrBD|lm3FѣҟGO vVM :LsYA.;w@#N :y1+\7!oʞC}ޘ/HOosqWnPC<Գ wxqgyTv+B??Z?wD,?1<⪩ȰZUDy;)"pxOOP Rx=y{8>e 4Lyh__7!5[%gHH 8Fp0\JʶGO 797A$V# vG@{ "ĔTTP >.2c&ђ.FZpzc_uusxjx| F< m q_qmmџӬOĀ8np@Q6.WD@Oխzu9;C64="}řxizC,}G@osbX& ) ;g=`~_ A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  #iTXtXML:com.adobe.xmp 1 5 72 1 72 510 1 32 2013-11-27T17:11:00 Pixelmator 3.0 x2IDATx[v8K{wiJrnۙڽu(i%λ88v؁c8v؁c8vgϾ>q^^=]>q[R}.=]>wCف︙Kacێ6|>;k4ýJRGo|]3י_dS~`fF=#!>l&5ǂ6 5Am D8S3<ڕv_fҥJQŘ8Co_B]BMLӳ?gc,c^pc̘'.j;;z{*?w+Ąۑ9 o`-e?K-4[!{.9Hij;g:Kq'2β{Gt }6,Ld.Zj$u`? pZc]`5J+64RXN3ڳZk,HY.L6lG%>g2S/G]qF4G}g/o/ܰ:h&pO*՚z<*rYg qAGR{FURK 'ϋGABό=U6!sr[T Z߽ }aFꕙCj,A[|Mo,9uRA*N<ZYh0J4}[a|x/n%*1$4_*ᱬnV&gs{S)eK -&GلG,_ာKNܢC.-Vxm.qN.d0%%Ҷ `d's[|$q\AR ٲ$_ᕶRΫL._' ^\pʜ JB $qX'lg2DK: $b |/)}q}k-Y1)@P|QYH x1WRw/W＀j>F7DZ _oW@JDcg z1$Y ?^@.6%A4;gQ(DqVZєRVg۔\ְ.vW hM0iSmU.BO)9RG( >_ե"chΰLZcF) -.V6ENN˗cW^fbcBZJ%gZU`rwZ@m.Bfq˫?J,R嘄~NWjlO{.x5rWɬoߧەMENO.^"j|`F(6wǿ 0W4CI !쩃3(؞ ґz!#/L:D"qoo݃Qh̴exzM3oF#%:zK&Zc˰.s~dA@yc M^tyޫу1Ki3I,͘rR|0ǡك ReBSejHH嬖Bswf"CL hF8hƗɇ-\a 5ZR5ƫG5XvJO_8S݌~}O;~K8]*R=pE zx =)؆FqjI Y?L;S*j$q`iqT 7dX\o配ieg{BeR.YY!%܀w7ƳϺ0׋\})y ;1Wi*G "Yhs>8ED?^;ӂN_s(d.Fǣf {_9 M1R)–e\! Wpax 9](g!R,ϒ89T.ᄡonwyar93dG eV,/oFBa7Ujzz/p _maйs5"%C<}_`4oX~ٷxzz{yQoÀo__(u[?u/g0(x6l-fb4:[[?Lپc+d0_y>_fwP70zP'`FDWPw3=E~Sv M ]dseAB%hCG6||VKN 9tHEOD(Ye4XB|wQvDZ,ޖj;y}TY/+7^roJkGz(§-WثL9y#v\_1~_rNMoW'~W#e?4(؁-b'_oϿ@ O6 zZw)xP|X|IS߭|~ $pp@ g6 f7":j$7)U`h$JDQ,$6p9f(Wm{D X1%jevZxoyA>CnٯOʺ@gX2eT;,d*P);BZ.}:rKUAcJCȩVUr Oiθ5c:kӖ] vULܝ `Z =-x6AZH{O4ƱMoL٭#I͕<`eA-@ta{|zek=XT$PoS/'-ٴ0ߧ Rl#Sd×ezDc&dE*v95OsZ "uL 3 V,3дI(.n< >H5 ip0j3U gx[ jKb\ gx\LVK)izVn)̪ر'@2a"OɆΏ c. )uO#|Vdz?.uVeܧb*U->[k9?dCވ`[W'~+ߦ7~tOt A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  $iTXtXML:com.adobe.xmp 1 5 72 1 72 500 1 500 2013-11-28T08:11:24 Pixelmator 3.0 (Oc@IDATx콋b893g?[_ !Y%sҶHP",I_cm_+?_8zUUUUUUU_п1\ z | JaU`U`U`U`U`5XXXXXX ĕªj5***** *78+UUUUUk`U`U`U`U`UT`5opW 7j VVVVVVC_UUUUUoPпA\) ^ߠRXXXXXX }VVVVVAVCq*****z | JaU`U`U`U`U`5XXXXXX ĕªj5***** *78+UUUUUk`U`U`U`U`UT`5opW 7j VVVVVVC_UUUUUoPпA\) ^ߠRXXXXXX }VVVVVAVCq*****z | JaU`U`U`U`U`5XXXXXX ĕªj5***** *78+UUUUUk`U`U`U`U`UT`5opW 7j VVVVVVC_UUUUUoPпA\) ^ߠRXZUU/[l+UY1O$[-HGpqVV>y8 oU*7 77wܭ <\?5[Shd~ߘ|Q[^WޯzyZؾ1Ơn뫯uGFVj诫߲^x lỸ|蠮MC@0j܈j_޻nB|t}f~oU8٩}U/TΟ ;W@g|o[!_ [w>Vq`5yvo]7"[x yz/oot^`/pU~ŭ%>l^Oy>xqR | ?!Z lfW?l{o߼q}b^^VUFY]_Eas3OwOxk1jjLk|Wīח`U+P$%h*âW}o5s|ON|ϻ_UBlY_Y)!^|4jV[+*Fkq '^_ߠLo5gp/ߠ_bKj?H<+po;_Z ve3Og}%yUg>?xlg_𠭐`vT3S+~~ޓ\9º ~V}gi? C˿o]W7um1b̯~7(-]ַojrmt+W>>;g5ڣkt-NYk[x +w-r~"o-x]][_ܺ?G}M48x5݅u6szST`aOqVoV5ML^ep*:PȼxA$ob&%u~N Wk+_TZ%FżO3y}6[ޮC99S{2ރt_]~k7T(-/넜D+/Xot|ĐGžnG{kxm>=GvX>nP4gG'R,u>އE9k8{W\nc?pv<)E+>x_'j4U7z,/͝wz%5#= n4f ppz*6DXNuOph=Zc+[xgԧfoCq~Gwb~_w}QkkwiA޹4):7_-gfW3K5;9iѰ5_ {o Quѯ*~m\<^_r OߠCۿQv͓=i؛'ǽn㬞`}B;Ww,OψZ%~F%q]-e)w#SؽͧkK;OSp5E?>Wa \k(2qEy|aB].uc܏^Hҏb۵8aycXZ~> c_g^Z_M13D߾:7&Y#r'@__DdWG_r'X8LPGDPV\z! TjqyӤ_b>h( {X|~y$ySEyイu{^;y=i \T`5,qܱz#[}kc|jm$FuRѾLÿn~&"8\ȸgsxyPq[-bx˵RIƂw-it07]<5+mᦺ7{p^nnin!T޹~6N8o/}m:h_ x8J8J_ n}W1`v>7bkշ|q:W`5xY9[;X|COR 6[6&~f{{}x|/?ޗV`})Ok3."M_;NV{WJL^i(BUqm5j/l ަ Ih# l.dmWshϼ|(H<Xz_E_Eon'Cnmo+߾)hLӒskܩ*3ֈ .Iwu6t$CIf+g- 8'hl\&]u&U1w |Z6I^)}F:.WF|"a0ߣcq]dh Owt\/T3Z8>kbz޾8k E$ҭXiX:ұr<[?? ئժ{tVoV/2Wgqog|fg{{UYX*y`鉩?dz}4 qki6[Dф4Z3sM,-ĕqBk$nAXGc=WX/[0<,OZ?50c}-K*pG8_w/dzS>lG>rFU K_44Fx,c*~3/p$M8&sɻHi#x*wNE\؃z,ܗKnk;4[trMe5>耿\az?ĝ#Yٹj.?2:\S{ z: '\P1zlU;)ξA.s *n8fsdG>3͘*%(YV"bueݒv=ھouf c5Bk rGGvG\)xm(mŷ5X 7L`o٩Ὴqvł<ݳyki8Ce?;;Df=qԋ)15pMl_>.jtgnG6ڢtPa7L슣lcxl@}SVylZh֫pk0 IH1:DGYxU믳=Ƿ`jT`Я{~P,Zn<4Rx@voӤ51\Mpt{i+k)8#W{`5}5CVK_[6w6}[%l @05~6>n,mV) a!;mܞmEE:W acۦp3_m܍]5 gmxbB0C(ŞXCx1nů\UmG(#0#L+갽B.݈*I!c߹mSo‰c+_{Gc$媈k~7v6*xũՠJH61ٮ$g5> 9H]/g*ε0X_$^ToWYD?7#9 zފl\ɻ_Bo*5M(9,h2c#GQ3j&~r0:Hߎ 9Q85<M6O?W|j:suAS?Vvv}߫ˏolʇEǂ cv};zW5㏗^^wú ? Kl_^K_s~e̷0/ W=3@9cgvHZ_hOs^ڗ1@"Xz*骊m֛88ÕX{uGiycQQ,b!gNQF-w/arvLK/K>f_!`ocb߃/wqCsyӀ7$|ߒ "%|#<_&?&Iᆛ QFU*۟/;f|oȜH!_QRf#+GVez@U f/Rj,']9 _EjGt&tLr\MGqLf}ZV>>Kנ"6gmrn&$"ԟS]-}5tscc3wog=g+=_ c1 sesW&\0[(d{ax~i id}JH9톏:[f.·X:Y֗/M7Mf5>tOߤQÚ^z<#T3@6ut:si;ت.f`/IeyOqO9ҏ[vam|x|Z_>~td}Y> /M78kے596G +叽HޠT~0sKk} x"5Nm-#?G߈:s퇩rOPc<6W&{+P8"J/=2xikMNS{m#b 7#U<IM,M }afÞu:?X 裊2`o0G!b(Z< EVKr^꾷MbAD f[tV8oo=NvQAqM 2%8c81)9br'U(]{s2<~pV$V:kG_dnaB{/ {_nP\gżUu*؃ߑOjA޿*6/o pjbrPqa1]S.G \7Ӂlk[Ȯ6D@sZ$^pfyd$*9h>l )1|\QXDY}vk\ / yHw+#>CD/Ow ¢‹7֬y~&kF\3䛈/G$&p08݊lc s0?.@~M#1ȾХ_Gb)\ U|4Nl?Z@^$}A "_mϒf_={ !<\6OS=y8k4GQt1xEimY\`rM|Z$0ԑW;N rݣxӔGӫ0\Ta15I0[ hˆecGҐ1R#Mc+1ѳʭJ#)Lw&GcDRSm7Gf#'j3&B&XSݨ 3N+ZPjmZ?jY:;Fva9 `fT X$X Ӽ9mvC[Pp "햶aX$t( VqDΘ& &l`!d3*fHAs41,ʈ+c/|ol1&xM_W2ul|o8wW{g~rVK L/OnJ8wԦ|lG]Q(h}LzQ WeGIqS)Y2ryA[ɦX"Lcn9$6Ȝ7@|#\L2s*768H0D`$`͞eO=i&OMpe,#a1G܏F,3v۳HiQ3b||sM?"k$xt@ݪT^˅l㉢os-kW>p*L5!>xb,&H3P'v,tÐޘP*бlO}ۈKV#:v9SlC4;qEy[wvs.IQyYP99;WhT{c^ؒ1&Cs6Fwε׽bȸX_1q/[PT!œYO[=M ©,q-'AFs7w濯&ցHqAox +i)?~TmeD"Nb۰ے,&4}L/O}6Dh05s7J"n>Z Ĺ9yz-b}1;煶\u{ȗ?neGW֟RwӤ*d>O} 4.?:,)DdP܏+Әt+y ~:ݩyavu&2ȹ$aL5|;ǵ-A%(쏽~4 Pe vxR;B fOS9aqr~ο޴15yup SX2Tȹ6͝ _[{~7&G"ZUnɹ EmVU{K4ךآ>0"V"D@#Jn~ 0qلT)vL>97L/FM"?æjHizв!6%X|G i"TOboOjaz`69 Jg!BV'ȍ4HF'rY~pg{}ߐs}d~4#G#^/ciDF+ m,^Xŕ F[ q,ZG w9S;~f8r.8_C@Z|K f/r.ٴQsdԫ~yaGXZ \ J +|\ >%}N(y޻($q?1ϱGLUtܻͽxp~ )BIArqVd_N3㌵3V /|9F ޏ0՘M+ˈ/}i-?aPѮZ\%TQ "u Q*\Ekc0<#Ϗ A`6z`y`?%i꡸lI?s|[rכxuv -N}/VwFV ihXvGL\ Ɱ\T7HMjQ .Ѳ.*=k@Gd).,/$ysUSrL4{vE췼Fc>7~7·!W@x3?y Mδ-AGۗq 6{UjuY<)[j+.ǐh:iZ<mdXVMZHSGBjbǔGBKvu?S C8M Y*G~uꕅ{b1uo 77Q B~"897+G\7VYK*Zˏ PtL:ny63ej넱a-7_3cE\Ñs 69:inפl#` W΁XrO-j=i<}aoda 99 CzcGC:P!Q;o/^Wjq)_͍,#4>k99nJ7cyټlC9ȹvptU7r{֠b3{9͍zp'6XԸv=әmqÆiԛAMoQ-|NцO0SQ| n;kDpO>|EVC\Ye\H:bdʫ S,\cG' >q8ϻD{ dvg2c㪐qLj94yگʁ|3<)gZ׊p!ژK cq:Ŏ-c/Ol8DN`д$#&pm` BK<>_r(7q "S-u ͛\ѕAr.W:=k5~E?򨍓n. 767{syc.TҾ햑d3 ZPVLs&"㱌u9T6K[٭Sa E9@G' 4onKgBLD Wag9Ҏu quL5|̝ I !``CjVœ#ljm mD\Fxs|]ۀعF&Y$3̙GMpehlpwM5ZUC3Q,T ?q~>rF a^4߻-}n}cYx; `lS1a~%qF{ub>| 55dh _8  H?}X&D<,JX:/p9f"qಮ5 wt V b˼Gdw.cwlF螱[q7H =5X}( xxzgc*-O ?c8K]:~iMWu}Mn|\M`k@M|l`k4i ;I\tb S_Is a&"=693pDa?)1a& ܌RbS)CBx re?)]| &;k syeeRÉ<~5fDAP1 x3]-A#W|<QS*B$ϝW8WB}n_}r geJ\%}+n9qEWm^YKoٌ/"ºnS3iks&FqX)3gxZh 82D@ܙ ,@mIˣjϻyK9&Hump16VS1O4ȋP MM3!Ɒ7r/0@?teq>y.dLmJ?(k5Orf M44v%1B'ڏt:2r-^Dܸ 9om҉vX&irs,F5qQ4|R7ΛXm$Gd5)@u[;kcm7ؾiRudc|l5o:˛:d81бm}LB!`-jwf N~PVPj}vt{,s9bi񆵶 JyWۧJ4DBǒ"O4GB)dLgy'26>B4؍͗/ &vrt+\*m,Q7c5SR.% B*i.bX jΦ6kYGhxĴM%;R#=|oo3 ˼C+/t5 [WC4~$"cwn xxn&NmuxS=,(gΝ,z}l>1"9cte>9(|<086uu;Dg@/Vי/ 0)mR5vrdle~VM ?=+\<&&nev[38|o PWMQ/ڇδ2W)v.8؇bCr/Ǝg+U}fE9}o7f1sTպPȆLf18QH"fпgoG/~`PyB{бx]z@ٮ,\$sC8.eDaUhX_4+F5 bZD` q č?L'|.)+NJQ\6OyY,m/c r=f-G>Kl#.h,270'#a]C$0 S̑tk'WumHɚ[ѷ麬#d?qELN^@y'fj $.`{"+{W5<@^[NZz.4N:F)FI,! 㼶lg=5fw,'^ X AG* ݜq`Kzeھ 8nWuñc Mjqx.ш,r t c 3ˏMЩ_Nc 踋7B% 6V+EPHb#k9kXv[mi&3_HR8$t5z ӠZ_ `_Іbpy|,,ef ~1)\.le~߮k!7'S;rurO bS1F˄w~c$9j1 ya,?.c?qyOM__E~.br\Cf5$K}cqr55Ia^7x؄xL\;Q`;1S\k ПVlj.h4͉}tZMمG^o]-7]ޙ7oܗe }m"4(n˽5{Z SOh$%]Ϝ`: ]tss 3 ǔqt&_ cMlo&M ~PFCWie8<gCJtfȌ = 0=ULSyD^ٿ\? ]?MEH;$0v\Z|#6+U=VКZ9 4e~ea!@AW\ npNt?+'lX{\,y*{ae}BK!'c-v,ӾyxhybJδtj|'RSlbz%KARX;F/Lgym|o^i0D ] ̓x׮;2͹MolW G4G |Rfp^&2EGT8漉 pz9_r/jYl(xR_sbxlrPA$X 4i_k)PG'#bgl9Ehe?7l^[*+L6R56$PA:/tv??>|nπX 8`h!Z|), nk!*ݞ~3'P6\ k^U2*bhn%x+z>uGc"ni:^{PUI2@iV[@ԦF-|t%NCgvep+=ӆm/E%769JqZ!ϱA Qs: >7<V#oa/NȘU|GKYP-eVCɩ$S*b 8-ZH8aĭ:GXdsJ /T ersOI\yaI.:r nȸU]@/ [ScbԦjl1MOyd\fz!,ZZ1!_ka~\xA'b\!9ʵ̻Ƃ7b Mjla1ۇT|3tۈM#zAdǀ2!Q2.G5Hl -񕯈'DplQ;7QX DH!}{P >rTɗ;&AF9 ##g2Į'ʓ-O4#I|2mKU l9u2ɲ>~ǮOKҽ,4Gm gx89\nj+@)O5Lϫ֙},3>9ӦLDt^Z{=,"5@ȶ zV6oxU0/';[ceS2f9ys#NPaBU%"P1M@||C69J;&L.uv*ֽG썧b-[ywX L0OrIX`ћ7m]²`S/W }/R{_Y,#"Or|-Fbueel{ Xy 1)ٲkI*Rd^#BP^#+!ZX;ŗKcÕZs?4# "ޔ?}6KO{=Ap% {2Dt`Fd&"6?"~~t~ _Rƞ1|@z|ZĨQ>WaQjyqqC&Z͝0^HQ.6;y!>&|#d r%q.3 ۵ pp#eHVAn#a]n/%au&G)_qXBjt5OI L5ϡuT'e͏PNN'Cƹ-h-LRyAOHǖ9_; k-O[׈W|^Kxv`-=usfQm\c:Գd?]q6"eO z z4/cl`bv-[-Yb=ڰ&l?3ꬑwEJMjݼleG(C{i52܅*cq7Lx擳#⸄7{8ܷU 7E,rF2k``riܰMmc^6:7{r]ؚMtafQLq@nf?t+0;#[:OJ On/+2qvZϊVZB-[kkOȵM9x1bbLd3Q[,Zȵָ !o>{X4&FG0$cwć"VYC(w gرI)O ;.iQ5Z8(xTf2&78B>m'D~#uo$ `oİ r}6bQp(flTWMEL{]ZζQ{%vE;]wјʑ~fVCxQ,Xs$EuŢZk.LrʙTl7c`KO8*e Cw21"eK}ЋW(AkeZ\=,czmxӜ3DW4 wB)Şj\Đϳ7ǾWdF(5oXpv{8Xl C}5qS~~DW؎RW-9M8bз*9$ v'0tgn!BaHra$K5|cgcT2 0S,&FbyA!>|ҏ(M|jcN=W5`\8>M|M`:= ZT ׈w\gyƉڇeẮdō/="YX* u ]B_X*$L *rr^`cƪEI'~+O3VĘz wG(9WPU Nb[ }9 P`zql,^e[nBA3RF>d?p@6yfx,94G\a`q9ĞJcl7cny;yccg)ثV'`XrLя0؎K tgh|Bc"IFE6A>GOm\p\B|y? sw22-[N槟l0wYAK=ދ~8XnǺ(X\Bc˻`,KE3o-Dړ*X9ɋcMاm'cK81bEHt;"7HNlpLG-n(fWB!bDzbtјEMN .5/!O&I#A"͹~W=$>'!Ka"[N!Ǝ[bĿ~–n'eno>ț g8|nRZt L^ߘAnqz!.qݲݾط( U>Ƭ/.'.0J[(BX&P4@ܓaȌ윟BYyyJ?[jC#hlq:bm?6ДoDŽ:#;aZ۠׆8;GX8Q>>`ggv#λst ۪x?8c%PβՔ9Gsh)~;HQ{~PBv!> lم>Q\rV,xX-1Ϟ{YR†19 ~`b1jܖ2& d]AJwr{&3zrlz^CׄimWCo&!/goxwP ['3xcr%"%J"hiS& #d\9G$bb5+?k-xb Wֶl%Ґk#: WO2;pU ?sz0jO5<:PcO[>4S~AӔ6+>cj,@b] B6B ;bB/99B2-7gԮ1Y'x=Bhuc9aq6k"wz R6c.{Ѓ5A-!CMc36ļo0pv$ MWCgŘ@P7'tE91ONѰM4S"01rZ8OFD c.y׃ m6N 5&fB]i@qcBZJܶp..]]SL+-G,QtKLXN82S{[Kq)?@!G#뱕[T60G3kP ,{?jݼK{d/*52^\Q d-\ ?̒eSiZcbro÷68eDlUE;#m0E45BS &J C!ka480Wqn[-ڻ!ʯ̧[pĠy4=wi~9garꙦB ";>n &= pH+q}+~CoA*4oIqn6,ei?E2z ;DaCi8vτ'!VlA,G "sWc$ eŹ\KYqp HlNX(ܶhs)G<[5K3<+hi*$S/ 9ymxǣlnD;YFĉZ &kh 4#7k<{Nca8Xp3})g3Y^}%I?Kw]VF؊3CKb-ժ7i Hl\# 'So _9˗$.q1'b׃:y*NlRmJ+v/xsw"V8h³ZqG@X$B#aA,1  67 6DQm9$-] Vr~-'N[暗2Z/ vγQʡsv nc:Ɂ" wNOӵ +t"NYgn(o LX' 8`3W0ԉNEѷɾ oSrG,W8lǷM_B_?Iѵ4[ ,nqb?;p,tqڗƿZN?-knl\ј6d§nq}oJf:2SlGҨm:N\$^#77q<¸AW0lGLᦚqk|Mop \uqOO :9 =m%_Ѿ+ f"ksekg.Dli q'zwy^ndL:TU-Pܙw ;u#=20919eq|?<~<2xAoY3F.!ci7uc]*Y ᤬s;esP C]z37d6%ٚIm^9 Udbç+b@QpC+ &|`o}itN…7 =aȢH{1qǜ\~}Ū.5\kBH`iҘ 7Sp$?jˏ axx1#|!L"*[0CRBV kA u.cs^\G#KV3f?pω'_/++d<]Bxv*Թ\[4_8b,p}kAVB}J{EVZliXDϮx!d,침c/ٯ`7j5)"݌5U "ڱ}9Ke5u<Na~Ħdkcb-qef\ç~}5,#58ȧލﭧOdSgax88&Rb*Sg ="|-0r-ad0-f,Rp0,i`I<r?N%/\ xrTѥ?Ş-D-=WoBZWG*ˎb'6'v"2cu4T£ a& kr&E\JKF]|rT0X>EXu i?YD ׿Fp_Qd,<":/]b$2 uHA\Y[I )j*mL6A-cAuD7Yؙo2\4aQc ~=wd;:Nx+ל湅OgvǛz ^J2K~w2ܙ2cAO54yڒLгY w:mM{@_qJB{+/#8|ʼYdIwRVUͷC7ƠNnϡN\-ga9U'u[9!)9#)z"%Ը"R!ǚlqFM.ܚ;\!>_ Z9A*K;6_Uyݱf2\ I>8/ClIhSw|E|qW`:tzs!] ov(KM~vlV~k#kn~MHLl3M"RAqwyMxQ/kf,,)m9y.0,gF'kFNA |S^>ݐvr|'_%%&~c4 YHpkK.r4x)|^"W݈',~}+v9A 5W@ǹ<4ڌMRx95Hx(삪\X|h0edR)#ֳPDa |떫c9Wvqqu-b8fRRTo,Gm_ĺ#kCp#g9Q&>ŕG.Z4 ʒKG_?$S&uwibFɍ@5+ҬXaBǒ1r,x%k. ]>|ƕ.)6Wb1 75),oKoFv9\l$|O?n4 0€%\xM \XįIk[꫱|F7/)$|+}k4N'TA;pĠZ18ŜZMG5T}̱-0:7(\0&_88 OC(XK -)1m ȯ-u $._N|VQ΋PE_ouGP&ÆG~'!@3k:Nŭ؇0[ :—b3n{"[bAyLXbf X~JNhٳJՄ?U3!Es&, TB*6?? }Ý|_Jxv5?>|n$F#{N.j; LNتC &!NM3fhޣ0U܍>hh#Grnp 7LofbDoN38\7#8VU 7@ #z A6.B(@I& bӱ"|15=vá|g|Kr$= ԫ㍌@fd{ӺFGquM/ /}OO7)/; ='cC(gf톜et XđS\ N-9I7HN֒W4G#~1[ǎ 2~>F3K/|lKq\D\M6(?\GMj.YOntw;ld^dKL,_~kX8H֨-a-U+>Oǒ tv~< nWSN oh\_x; uxQx Qޠ5ut*! S%nӇnĈt%#t~ߕv}pB}41>^ l3F'BAÆczS`e7Ɓ-EG=uRuk_8G[;V$b>qɎ*mq"Q?g,N?.wmM3oyf[gKJ#aOhFY6yM85E8=N8 U^haOEMJl:*G30ť{!*_|ȝį ijEklֻ _,a4nW_uZ?B"w:`oT٠Mn=RcKp-zMZ:FP.ǭp=/;%M@ { N>^ >oq2Ý0qkXgmR[N>3>\nA_抽)uxq@b6;Ї*+ DsʮbqhEL *BƼڣ`L 'Bl_a~+,|@je'OWOXX"y1kl['9{ y߼(KesJbrl`,RȪFh iPhc4.w"V,$oo<_WXifi;r.C3V~  Q%j49MPl<#cЀU:f"M)} 2Xy\p ߩ^1cS@?S>Ho&-|zƦ 6?bBzXQ[No Gzug[yWP-y4 sI(8f2QkH 8ϻV :hhU^KJ=k9 [N1vғ:2}~vY9ȑV# PGYw=mڮAZ7yO98ft*O-k E$J~s{d;X ld̗ 9O"д [zojiUjۃ3GnCrLjS4,AEBAXXl3"9&~")# ) f%z hA3LYj, L|Bdo^]62˩yE :Ta3Kq1zi.id .STxy˜ 4 $*ԍ1=JCs~G6~r <|kɻ+K/p),5P@Y+Bvb ӅSQ2!%L=vO8~| gsS)Cѻl< :HYR+~ȃOuTPG=#a/&ET?89'\py;Gܮ0 um:zmG:?Z0A-7UVN3H/r+y$F$ӊ,VؗC! C~(ۿET|}QA?i];W-,MMLCZ5ݺ&oRBWO\%ʏҨi*ezzme۟oa/C#vixxu[/R1КWˢ(Hx7h[4\ Z|X-#9Yҿ>Gz̟}܉jj;wi#j d")W]4Dea¼NzҀuNznUKb @т%a2BaOL.71'o XM#)EJ"?:CtQw>=|LJ۰<D@IDAT ^^iu4w^N2GKtB%.ceԟϡCO,(<5Rp:G& i0)? ؑzQ*C=}k5έ)0N4Dtuԃ/5dcd0Ǩ KCGq:zWWȏ?R|x٩"]tyJ"ZN+.^sx0Cnc#]j,sټ&_ ^&į@zuHfݲ.♃iVxQo Wҭ+vl+# N \a?bW @-'ZDWabQǠGMP@N /FR UR>tƷW -Ÿ=%iY~o'*+1=Ӵ46tnO~S=-756xW5,H5{a4GӘKaVg(ץ4^6չx Fym,奁 h3>{ հӘOdz׫"/zxݕ_ q@[λ}ouڶA0>Edɫ/&/yc`ː吺=,4D6 F4gn7 cjX+#OR`nI)@Oe!R:d见Ð{h&kZ9cE1 ?.;ï8Al6 B[8]v{С9P& gMk\/FTy\zpѨ'C,J`Zq&5x2C5SF΅m#6`66+:e9l_ 얮4H;s5>Y͎s]5H S'WM_`@ 9HwLjGʔb8yЀWmݏ1 ,#*^?JJ@9? Z je1 P on|/~&h8Їs@ C<1rE}r sf, $8UYY&,nXR̳O~8 Uͣ!hk=UP46WN_E,U &T';s|:HwG:|~ު_S@}yڎ11P|iM=ڎpp4k)䥑xWZun\/!c6M[8u׋X@ @7aʻkθheԋ_ bh=4jeG">)CКZ;F=lc1Υ`ɕHO!G[420zğۣ6tLD2e?lPPuAv0da[vP^?[FDeHhr]_kPw%he':䬗⏁Qﲩ+U0Z4y6H]:)[ и87|λGmy%j\}CuÙ+R߻c"zŠvlz1ĸ܇ ~VX Ecrrq{FшW' 2n:bYcuù'9XxF(SKW:_^c4AE"ݞe,{g}x#;0C p/ r/ޑMq̀wQ,Kr*F#,}4]D޷Ы2uANVGha8ԫ38(pQ=y1* cBg], 7ȝeC䷕eȠ/iF>g`.Ɖ8c9-94dAv}P3{F]qKF ,:珔43 v< 98X:Zsѐϙ`%);0hY-kpԃw Ѫ{^#/pxZy ^L0e?UʬBoYnmi`k2oJo*g_ݔVx=R4}3^iXsf}_[[:4wKژFA9 h !$ܹ?%ayװ+!/ a8% raøq+_#!W^RGV0͕؀F "?"5nTswu54ka͂uQs;OG❇׳/3Ŋs鹪c>cUChϤB KLg=mIN1¦p~\5J{0eyӛqd;sN[ᄫQNK7, D\YP'$0I9)IeDgq{ 蟡bNTu'GhgYR.BN[,Y .I%嫨,P*:) W[Uu9D IZ Uc8u5n8 8o&8Al%N]K+ԯJ:~\qXǟC z#9 bF4"Tqu[1sGp34}@,QP-$'!HTDA e 1OhqV]-X5k#;0VsJS5 8a 62d 1/)*xgxC␷i?=t# C\_`c8عaj\Xve'b8]S Qd_gZg ?ဖf4˾8O߀&!y'!t_gK:Vmk K`zf7Az:m[[7z#}AYC/7z0<tjh28p'iqhXހkF:AZIl:΢L :,%4ilĸ5cem8ARFO=/>Y%+ L!h@ZAf=ss7#pFmXG7|_ex0vp([] ,:SHtqg4fG?" |ԅSIA,tyR&u{1:-饠T7I N'%@qd 5|M^L[+U0GټCa0"#or;F^黤}?"'TV!|)/J$>y,WעP1|Ap$xjG8W *_jE**th n<VF\y՘'<7mFOVxsGjV_;W_3E ӊU_Y_eeuͿ ;<}v ӼQ5덞s;rcJH'su]vI'>С|<_37Œo\lkvA8l0ke=|gX+1Z LQch)*3sF ߲N4y<;F^*|;pZ>0vW CZ]^㌨| ܹH[\c ^Zu\\= s\erȝ x]u#9֫Z C 9sB׼1HS1|^]]oC.d%aƈC#yJKڐ?4Y&~, 1>L26iY ?s s S._>5 8Rp@ w uNS,dć$5(Іn62 ɺi(HLxH(xd˚(Fk cuB><"؂$8u2oޚϦݭ +OJDŽ2Q>v0}hW/N9[4Y.:#xBul:_sBz뽻Sw\ J ;pN:>piW.<ϟ}d\EnF{{+Yr'R޸E̗ۋ@!J^aلD`e:ɥ^70_@hQw׸|6#лsv ]}rJَiYh>aj=Uz ƶ^cۻ>QfFbOYw̓s1|w3 ? gP=yܼW3nϗ턯jw]18) }UO9tX0;:qiG{}0뙋Xng$m~[^M\ç$]i_~VG?_zO˥tZRJ`ã)KRCՠG 9y"u‘k&V"4$9qX[ Vp^]ghO>nȗj7VyRbY|>9aqjR?E9N~/jk 0pA(.KT2 wɐN_uچ_~\cH=+1b9F&H, mkux_];Qc9aI(9zYkY1A|WC8j' X ᵲ'pzUyjx<`L_sp #- 0zIT #ֲ `zS=Ϥ*и ۟F߆.7xǛ05.!*] c̽wZlu HW\Pv$=\.:7ʎO{sh[C(OBCϛ:~]M.RӊMJa~}!护7M0"̫#n@R? bIJb&ㅱ_ VxW$I^G~O>RQ' ~ wqQßaGyA5<#}yL0.]600ܼgFqM 4H4=/ecitCL:[!6k#-{̉4x0IZQǀ)x{I.F}4.OjŻfQKR[kZ1CN~K?AGw~r<|;ko4z:c.3rzӜFT5 gchv9<2d :ǜ|=k\|W^XKYnKF,<<Y:MYknh1 ʎ<8$Hkc,Om^Ui~% e{D'B c)Ǭ3.YGb<2ՑZhj SZ|3/_UʰV٫1H = )v)+<Ѵ7Ö #nx9mǓV  *Q#s|K Ƿiu|,tU￾x  M+[Y&S"j,T~`4b\MՍZP3,[!pW=jBGo\i 4Kc; ye~Li5h#vhtp@p Lʼ8-unwǰYv|u?}U_F:dlƅkm.yO3;]ka:3ѱř&3Uͣ (hwmm/hdw~y]#Ͼ)eM}^Q@jw!|W9}ʌNFnԧXg ~^#BG8\ip-@ݑ oF6!9~M3c;d1CQWY]?x곡Ek"'Ӱb6@a=NKiYx؇r9M ڕ,rh}G$Yh wq&tд+MZ8U:fNtLJqh׷J3詶[ub/[j.钕v_CHJxPPy3WlýӨvSA+W L$`Jakkihc4PsKNHL ~r_']ՀpbcƢ['5ϫmvI޶{!L5YƲr9pxo;dKTW_]3;떧e{]Bp5RCH^w(“ '5 SxlHo'O%;\_K'KDz:c8 +/' +zHXwY!|3%ӫx^lPVv'@gtV'̩ʷp=g9tec:oe콟ǁ"]*cqj5ya }NO.d9Cl|sEOE LHN':>WHP&,:gEׁ㓱b摠ģ4A5eְɎ@Zw\CK&`F~<[m}jf ACj~PlE?C3hF€⳰oǬJqɆZ,U` ?x`|M/uey{ſu(j{W C2tV1.sդ}c_2 Ͳ9F߬^g 9{?c{FX)`@wY9*l@auld Uk;ԏPcs̾;N--]]Fs|ӹq3\)Kly=2pư .pj'@:;赂ݲ]\/Iyo x9 y#UB? +5Rseuubrx'b0}3,k: VhPedVt`ɓb/iy^e Fb9s @K4Ws8PEΈh+ »*wQײ%0.?? gtB&ny辖cO{v5F4SBbtz6:џsWy IE^4myŲ>nBԥ)EE`gWoy`0uh>i\k5u]>VC9Q@u[g5Pygk4dPd;xe?>EW.1/,^C9f3#-?~pIwU: ȋg0 1Lb8tIT3|)o3CW:JҸHjt1#) syW'm:i3RuZ?X/B+_sJR/^bT:(q 9හ%ϯ.Pz&jDf^R( y$D9n4]lw{/iȨ[oh+.x`nCQ r+W}}'WqGc^U-M)HpZ;&lz\r}m0ElƆ1} W>8c'4շ;bw U^#]%+1l4W0fc)C׋2U;~ǂ?ٺwwrtq #Y>jh? oCc~S~vNqv\v89uGwaHqtVXNtyr+Zo-r 6ᱷվdx!=tKmj9kz+TlWi(nXb1҈shc2rH#*@ӛP<b QUï4%Z3_hiQr uc0YôG8ǮKwןcx?BP:Fqy_^#J -!Y2Rg-\3 8`3}+h &QZ&>i<>Ľ7Bi(f. Jr=?^{ΰwɕ6޹6PÏ׈aUv40uIp8}R9=FИV=@8<B!@#p4GD&XjZq§+SI>~8Гq7G芧G%H9gh v9_0ׄ9dWQXVZ m ~y7Cax!mU4A!~kjpF' {`1.|؞s ]T+L>݊uoCx=iWL#.NcN˭Ep?ى<94@9>g_W.p0 _2?`o~X1X4y(*y_Ë~;Qkۚ&.mcF04sVэmu *#xj\Z5 l_ SMk> ǸBę=w4ĽH„ɽ;G?*-JTpq1XI[ócl)[Ϯ;_0ԼRWXpW0΄Cn[q UǠA+ Cg; DNq9FgO0[>{xO>ÏlJG=wte=) -skMk3:ܞt<ˠ=S뀨1%b[QY@9 8u20\5w(h^p\T. HlPKĖ kAK 7xhk)y9þ5z+ݟGrv Ax2+,t3E [E;V*&BfnAV~G3xJOL]C4~ 54114(K#LCԘ{8}AՃ·G0AOyv ]=  v [?+ϻ]1Gn ?cd8`_ ]DGt8üл-+)K~G2fA/ޕ5 UwX(w@s+US 5CnVs> 1s,>XsF ^.ogրN # 䞠t1$Op[0خx#7y 5:@'ùgt;?t+Xc3HFao'R)kWg+8W%sNs:r}hUfZ+m9[UH*CTxt*¹ɨTuX=fQ&4; 1 ɥ5 6^#6џTRXk}އ8il+_B嬋|}h#TZ "RaKV 3M tИrB/F< Kʨ48LlcHݎ4o<av֯i$ ;_wn;'~2C~E𹫮]w;~m팎W_oqvS;7N`>>|]`9x^iS-3}|ef 9sqn-ؤF7{#/=PWx~LITh]G8r|'_tl s:?jHeܜw'YOtvѲ`!y_/(9rXS{=˓L/ryq:e\ 8b޼W˧T 2VÇ*!(*Ř0}>@nSLᤲS@IDAT8IkPKȱwוfn6kF8&kN+to-vYc=W0%cGWF>܃FM-LCt Mgx$G5֫AKzkmX7Cz[ fCKo]=H!?c@朹$Jt}GalCgF_zՎ>F=O1F~|б=}ި;Jף{c|3~pyE j,1zz/w`HS}ͷyguʙ+23>U F4O>Ϗ|.ٳj_㖏`ܹͤߢ_g5#1`z[}"|<Ӑ3B`;DݻptNj]}ks I|t*okaZu:K69N9"VfձKW`9(\FH4S5X) NT$`;Pk_RHVLޕ3}̚gǞS{rw |; 'v2O1O>?_S.̇ɦs 3#ϔXwW}%ΏS<|E'tp\p}̨#Xo=hB; ytx cM7HrDxa0*N ZoN_^kK'Qes@54d+tyb>鋁>}0,ʯoOLVPX#|{ָx\0_Z/@"@/EqV׏kRp@J}=\x3|5ɔ# Ŕ6*xEڼO:yM|a,EąVtFw5_swUw>{ZUIP\#y7nC|vX=@'íL4gKF~ρz & Y"N519P'g۟FQ3>cXH3x_0g<22pQ~??晼 :nֱ4x=&Dg>]^' ~yЎ(P^/E`>wd uBƲoG2) }/b "(Elf ;szY7P.[d/s ~8Kj M,pHAkZ!Y^K`ѩ ro `sPFt=W5NHLN%lZ=htquPf?1t_;g!sҳJ5.6p:r4: v龪Ak'kx&?zC|Ɛ{3gEUvp;?0{ IKO^}Ng9rpK\P9fa-FU?2\a3CRϟ1t$x\uv\\K8o}ጞ36L;,hQl+q; |=Yt~^.sKs h*z/J*yn?RMC>a&6^Oh+%MXx&3}qI%`lIk>x߰q-Ti['2' MR"딯<^G2oq"q5Ўoa IY &={{+K Mo| 6-LƩ|»u' 8|uГޯq r! Ith'_.U~ z*a1z~%t==>vUpŸ_ hvِ…czA acU~,Jcih*zp,9x[w^/2c(#Cz}+^;s:Kn _Q`ff\9De->i|L92ag%wnϛg3j4sN`akvjp<=|и?[k'vЙkp?e5;q_uvjJ3 GħL[vX;];١haDUg 2o'/uZ¬1l\1(9B0pf }h F.0BE/x]T:< j¦ylp -0:¿6>4aǠmWhqe0 O9i\=p+Nu> J49\%\"uj[h!77a;Mx\1S@hN=w}uhsw6->w;|q GMr(]#̿lE;整l n;O~=_%v-3?8kW7WRAvy\uc9^6ؾZwxP>:74 )Or`4?3\ZvʌFX?^;j윋w; .'Wܜ扷}7 t> rԠf ;t:xcy؄oO)!⟟8{q5Q ; zIj10.Jy#'G`-: W +6:u$niF"B*tǫYaWߒ5Ǡʏn86(G(m5:VיOJ =S6Rr*р#[y[-%I~kx}UR[o\'i9M=}*ՠM[=t42-^ :.k|guܥ|A.8~o)2o౨]~1z*ޯ]3wKAd^2/w'^{̙1?\xCm*wQR8FUi=kr| ,q*vªvi].9ߘ{|`]6u?tw=K1,^E[J_h plNJ1ұ ] "Y pn^$[ٍ9|(뀰tVHPcx$RȤ4J[QtIn4p"N}篯g .D[1I+$&`2:rb%FFWx\A3WǍXApa\eOnyg]OJϜ^3>;?Kv_Ue_ #G j^Ngsȿ}?*G<0lԢt;NvG2=ˋi̯nb/cCt}9xl`?zr̬s,sw8 8$D)⧗̂CakSsPj. {tt|0 gfOxOr㔈Zc#ʓμ/3ZOg }AГ[r'd,nUOln Yuĵ qÿ)a y(`CG^s5?I1$&j ^;).ŋs=>˰AY0 sPFV"/ fe: Iǚ sq7t301VBXq%=URVC9M~zv"+\ cqq ^=gd|.6\q a4tҌeЇ+cXXĦ/cE'Ȓ[+ :Far1\3oc0ڟc4$$`s{Afn۳_z#;Vo ݌ޒr0o:nN%{wf4Zh=w # )!qFB@}e{̹[kl\\c'2~ͪs<žcs F#tǶ/?oz%؟$`!-E: zt2H1F7cA+c}/\,x/Y紓g1z0`? pI <}f{yf># =Cg )ClKwO~c}T_L4 ڞ qzz߾*xE^ i(O\l׎2񎥩:!wPGn@۞`W4͟E%\Qќ)n +GĒƦJP<*07A,tF=2+ᰑs޺ egzN^ 9˻$ĿQMK5=4 Cg" ,xk#Ȩ _DT(ǟ~&54ZqmjC#`5l#S(_)bȣ:MMB;* 2D+mغZ![@ 3W=ՕꦍbxX$rS0>O,f^ίaxuwNj"W`͗sW]=1 |BmOW> mޞtrAo߲N;XW-7۱̹q{8Dɶ3f.Ym:tC?yL?.zl{ǀC},S%ߑw3^7=?RzG (|- c5_K{gu+V!ۃ8:sf_밺u7sn(QSJƖ86+̓3)$VñJ}&so1G&CHTiݓKz&*ϴ?sʓ/ƬFp"!è( 9zgΐ*Zgy1/␯^Ckxʅ[o]՗$%_XCMG9ܱq#l)`=szؾW EK2Fɯ?%^ڑeh*{_]xB6<XM[{GHK  "hO2]_c@ϰ=J^٧wkZg3O;_astiteXG]9"8|8 }YZGc)s-!w-D)\8p4#DO͞B:.[3nL CSwEj-kou:m Dq!J![bXqz7ҽ|;XECa$nIo]zg ׵i+:xJׄ+En"84!n _JCNHs;cJ̲y~-c/ #)x!9~/C9V8 %JyKgzC922>:3>h4l39\Gx̝~W.0Ш;>B<2T:umD:f9|f)gq{aW\8Go6wo}IsUL^V{Ovk~GvП.pn:/ݤkZBaAst7VC\qZOIgjU|•0}C^3H|ϙ py}s: Ųm+{XyTTY{|fs8ʠHذ׃/4GN/D Lޛ(q#m$Ed˝}Ib[JRy nR-/3$i[l4P FP--=:,v? +<넉4H?Ja0Uk@m6oYfcBlSw:嬁G01`2ndsw`ᑢ,BS <%f%OܦޓZ9zt?:Jc%ǟ΁5ng/\5"`ȫ@^>-WΫw+)S97sI^"ԗlA17絪0In y78ʡYvr"we|`zʃ e#%RO:8V39]&qoÚ:RBΩ?M=wә,_#9ӽ8m8 ah|]6咫ш0v{ZmpтAi'N8  J ^ Sv~r8)TߓL5_\U`KΞ-LgT )=A$?"(bW>:cÆT>$Mk-C0JGT BW ?WԆ;^HSOBzAF#,dnr {m$Yb; x`vy+1y ?v8g7d_/(ƼkG)L]͞y$7`iZށ#;J̼,rG|7lsȂ!y8+ލj 4,P,Wv|?U8X- 0B@FV4Swbv`y ?zOZ>cxRKc3ėd p? 8a>16ktSBUw4ҝk,af. ~[Z\`vO(QSWv`FWJ(V3hD m2=ʐ,T@{6֎_|ɓz(I,o(Pn]3ff1G \53u?5%N_K4@D|`yyI-xo Z3|jޔ/Nވ.aV;Gm>路yuaSSb։L $"q@Yj$;iۯ<淯l1.{3?Q3ـis57;9ogK!ԍjTښk1H~NaӀAwZɌqNY +#7PG9Ȁ0yR@7[j0' Uz8`5+lCc5!K^S !7_i"r aA*8D=a L τH%1,94iL E9;|W״~s|_x}J`·ſ`Jm%·OOyAP?,yh{<-=D\[\)«u$S,n-tҙ};2ڬaL_.b{ߩd2hԀ:zn@Wr5wQ[ x3p9>vwJ<;Ux_b M[]x/nᅰ,c+r.j_$swӧNr]3V#ӵ׸+w) AJwoF,aJr, A;= a0CnW Y '?{sjܒu'ZiΘ lMK\XPjaZK/v@163cia0A‡p^>G?6F`\M)4Nm!;,Ȥ׾$%9 c{7G0uմȯ B?=_ܓb:5Ͻ%ސ'9 _T++}R!1/C {ğߝ@Ѧ1~ `qzG5ggv&l(r#ԍ8f(p;hg;^a^\kFv:K8qlsaZNs+wօ#W̧+2w΄ʼB+bK;8"JMA,L1mp|:#~h]aM 2$>!p8yA5? HzP&( 25B=tu -<[8ȭ0;|떱E[6Gx#8(hh+ᗨ.V.0G/ =+͜,sS`fR!x@ ;qTX$~rd"00 Z:G 2n7?`yn&9:9U:/- AxMo8'ݶuCݚK|?pkϹkqxq(kӁs&MW0H+"9ar m*?m.(:tcQȵ,[|6C2LWF#֫EGw78jηYM(?7Gx/Y~6z[_"ɪN=/;Q{6pz~sKzgs:?NԵgM4xwjSes"ۂ yꆨ4J Su~l5s,;a㔀?S5p yiHj_~4А)vS 0= t'VW[8p#^;ARaL,|z#zEzoSl_rM?:%3 @c`lxiK4sd5r>^05⎜ɇjDCZhZuJ0e g7Ztբ/fCP=q h^%粕h:Cש涮|MVhkhww^ zW˴LTAj)f~.ç ,3CL:DIZq{GGE?씉ufϡ/!)]\n4 \5|X} b a|r`ug8W 0@)44*(!u ؖ]b&TX(F\Eb֊B89?$jن7:Fhȭ}dwgvg?uiZ4&9EϨeR.㽴lD:`Xcހ?<J/4`*A~A%:3Ta/Rfւhl%Z B٨% 0ԍ۽%w/u⿕܇]KU器I0?'k7(ֱq;>GZcK%j`={\ŤH#£ׇ/@bN;m l^*?=]A*]aB8ʧrW۞:{hE=yn KeX1E9+o(̀0S={#L 7vӑ2 B3K#!`:swn<4nj+ &ȃP1B/P1>G;g,zRwT}ܣ Vspf|a@Fz{s]GD\8yPV>Ek1Ue81`:8}k7X*8"}vں݀c50Y0eb?yx^08Dzh 3߳Wy V;uet A uIA_YOȇr"1`ǻ+3;p@,\拧@ łg7.y~[qϥD*Z>38nR24†?+?وn73ܥc]ɅJZ4/[KP! yh'c &z6G1>ͫƵz9q  sy~UUq1+ӻbvȶO8n(Oz~Sw@jHس1:-!KP%Np«< g:eR1J#? b`%j?ݩ&!^h843訙#g*r {uji[z|j՘kFn~dB7zy] |F{w5RsO;a.3Џ=:sKXҥiF!z9~AN&a:v-X}mrgDχ z¼/(s+6kq zc|94|#ϥs>~ow:{x-0k8ܫ;^Gl@5~@^LUьq XG'Irie5ť:>.g&|?13 5o6cm&LEsZq 6 v)`),DVzhUƂ\љ0*}Bt5C|Jϕֵ\5 =;4/=7AFtڇ)va=H[I":DkϧU_vI.DLjVv)-Htv~=#Kb]vW}'`6-3%/r.pN]>%אYAs@o,7bdΖ\A=$,_c))λsՑd1GKlwz`\`9GȫYX><Mu!?y1Zlso8O]@دqy$[Ѷ-\,ۛ#\wG \&8iEn>^vcGRvQgq;@HS{߃@>k*Cg&xL+y$B.ᥲJMJ{'84#Ǧ]ΔI<թC#mf{e'X-8i?>%QO]aYHHFqa l$bZކg"{86mNs$7%˴Q ( Bx#5iQpeD&wooPO~W=iS{dogKCO"QPY/Oz6>?4r"ft@IDATr5'D5zR&L N(~Dr% RnCz&?[y2|*`I .-[tq'tĵ ֬]DפVzp7$NIEtRC\-=;ESȵKCS9c' #L8!uvzh8@s͜5 '}zPfg8ҕ*{D9܎Nͬǒa0Χ{+(L S<Ą<>\vct0^mרqfC9kx ~g07?\vn%nr^-gwǤ`Po?u{) Xn|9{0Ǧa;Vz`2_0br79d:ЇDm_+soxM񐇦Top/n ԟ?krje Տ%+y0\q`VM7uwp ^^fGC0Mx?N>o R<' ؓI0SF遲G@賿7}Io!wzs߁Ceaß6nQǏ\uan?*ła+ZzwԹx*Qs֩-uY˜fDŽ} C{N6PvLakb?#qf[upM9N1c / <[fW36|FQ S^j'L#\/mܵUSwh̵ >w#/A^cB _!\8Bp=Nrww)^BYMvψN';{wãj n綰aDYS{y3`=Xj8 v ni!ٗ l{&D.mNow#RṖoy l|?"'%'Vm8[vc g cO1|gHp솏l$]2a2!>fi9Fv!]h胠9"pm1{}0a'oɬz|5pFMtzdYnht[F(v`~ce+[H:fc'mm;ZvqP 푤'hGhK:Wsr~Y<^ \%#!KݱBM1¸(YŠR"< 77 Kb8Nt+ls6aY6{2O8#osv\Y`G1GDe9IG/>܊U9}m2oBy`7jK ea9kO9 ^wͣe8iBݳ#h, 7x_ܰܭq{u,t4-:{;eq0/ϔͺ5vS \Mg96(*juk}wmwQ"z \'C3ғ8Ipb9` oA[y 0?הR)Zp V~-k{"!10E긐:f HR(V7N˶ܧ?Wn6ۯ7')) ԭG4w`vӃ/&Meߎ(*¿lrMx >/ѥ_ d< z- |'~/*Xjc\AU>F;w1v>lor$d1O$lkO֢-Nֲ|=>i{}[q|Em+y\O ')`E c::T k>ɧe"Y( `/b^k»o9wjvqkK][~B_&Z1Mo X+YK [͟kО]c%k[#5g519ISO_|vF`Hǁ1rE{=|8e9޼|˼2w9s yf8X}9x_KOc6]ST7@ЕS9 >C ZpN%wlFx+DU<#\g-ځͥkDpvu6+Dz6:]$oà-?4_G 3`Nɛ/kP1 h[S*gn< BeFoZ9) o>wHg-ϴɤI`ƕ[s"Pخ+GB! UocZbOFqEQ~$ Ih \~ [@ݼ轵h$[~4=?=j%u(tTiߥ/>܄i-BauZ~|&_*"HAXk4I_Nw:ϿҲ`hLjw?5]c3_}|~6;svnE[@i +7h:ȝlOp| ?$Mad̛_Y rU\קoa[# Z/_߱'̏8H29\2As{]AC ] -ʼbP;%t7 6qp*@^ fS쯯kǙXܶ|7w~=Tu(? -aPga8qP$ x*ҹ*Ƕ2`mp/so7-4 F&'ӡ-7M@oGՖMV{6A%\418 4}wԒx"CcѢM;JF^ۉ ن NRC}2iGS˴Ï<'$ڣEMG[?3~ueijx46@NwiM(*C{77W ';Y] {B;i=ӝh_eYAd\NxjC5YӸ%A4cmNs:77Ǭ->h-'|Es9g}#h:{?@xvq3q Gw̜oHm={pyK]cjǍ]|_M:;!糣s{!(OXCyCyBj|0ekᄷԇmyRkh+]#̡sOYՠ9^.hXAt$T ^~4ڽ;G-_ }:mYCl꿚xbhҾiPm&vV8Պ3_VOb^s 4KM𷞥6 =z@6n g< c*&xG6# I"ۂR46q.P/nؑ~sJo.pʰq) # ` ~C$ͯH#IKL'_( ݏL}ot-s]hʼni`F/jhA~0mጀR ̳v|6hd;BT w <駳:x-= 65nsX$o>~^? D~s3vTCt3}uz17ơ<#zSnhug BViнk?D#@֫}>lU;Uщ2~ͼ;wO&~U-kWhWiZך{\mh7hLR|+7o  GAȒArƁxײM&@A M2+m5p:4sOvvm by[Oش7 lS@UqE< '0/q*jF{t'4<5B( ?}'tӴ]~_ _ fJl#<Jc>zqSi@6ָ))nWndkH*\7"c޺8̛/y%*=u 8ȡɕӜ8H>hUPjp N8,ߥkq\kDD+ r]sv:1E2cg{oBk9Dp!jP86xokG[alBp=o_Ŝ&]_%i~z#By8AƒwNA;$O'|*rt^QcbW@(k8;g?uȐw 1l9eοo_1]`}kn/4a5x]l 8KY l.df_ywɴ6Ҳq۫%7C4#,pũnA37]>~90ӮO[PwVB[<`yї;} f5KJe\ h[$8XG"BS2FrMavw ^C?.&f=V!zzνsަ5`&#*qu[)_0`>z|+1m|HSo42v#8Mi{4M30m+ܠ;+"oCXYƻQ g/yĘhj WRmas.SЛ-Zq 3xAzv!GyqYKÎO&]ϟ"sKB2eMWB!IJ3BahzK9Eн<8dc$;?^<2_||jvsq+1Z8sw<ϲ4Ufg1Z;"[m*'8걡s2;#ZxLrI+o5)վߡc q?ň|G)sc޿:!p5+ԌkNpg+PRm|t1t\]pV `5qEP<̫1aspˆ-A{4tۉB*_B SQ+DCfI܉1+#S@ sDy[ G<ErFq790:>1ls|̯T|iN?}/}7nOO)qlƍBOmZ#` 4 FxIOW"!C:&tuZY4"xנBܚVj&XgHH|9j=9܀$8fSi&Wh~wh5_[0.eqחhgg˫wb-Oz|u돯1+u/xEjKLg5ǣ"4nfs̕kFp_1W \ݠNqD̾`)=#M,{C?`@b͘\թ&2L%|C>Xp.ф53q-k.? woPgS~=xuטUFzhK,30 )u{HsnW+Pڬ`(߇n!gi7T1mA^pG<qs@@h綠U -J˰^ݾAȴXR8 C\{Z%a[ 3 Lh|+(Z;-$~))p߄nz{~ߊIBbɯr_#{N&I;'mOu{MK"Nx7vBWr5Kљf|㔶i 61l\柎TAa6xGPF<VZ"4uG݃q |sny2 s;od };;Ne/_Ig ;ehAPhV~5|foHt ;701ǻ_aI-+և9z''_g~"5' aX_! W/vav~yCrlz}JAjw8h(ЩS8tl͠y5 L ӵI*юt.0#x>]Me0`o7R~LuaXo\^p2t`Hp[*hDP%[5x5kxAZBPOӵq l)2x?F ߁ߏgWيۊ={ߞo+8M|;=1ʵ\Ph~.k.ݐ@l,nէOsqwUEK ;#iY;xU1]+‰0I[-Gv2T<]~6~Ij 1љ'Y9S0뉍g֥˓:9dI4E\N/I{@Hcf- ӵs݊P{?{9'Lگ^$ 34amc,ٕg;߳apro,_pmyeWNg8+?A99k`p ƬϜ%K0 paz{N~5<5+0&fu<޿]ul%wkכ3|z4?5=Ԫ\:@Xt,H5nߧݻB_ND&ߙS'UwKӳ N! /[h]i+i/ v}m?W+HJm 73R)1m5^#VIm잯~*6H"?< Sa/T7B^'BvBS܂nE$O~nԶx WGG8Cîҏ?|5OnSaz]v#˾Ӣf1ɋBW* &/pSwC 6Lq3"͜;o5Wh˻:1iG#_"VpVcw^89 s=4# Kⸯvvcnߓ 43pwuh{μ^Gq jƞNv.sat[ӝ`u9|| ?Rz| [6oNx_Z dPq&}ؒۼxI?ۂ=ۦ~Jvcޡc=Y~ XqQ`+4=N&.,VLl1WM>P={L'g{gO?@x/x]`|)}\zAf~[-21%  &8yiv{ ҉&2JGx^tEMdG9zgzxVu/v:90WsO'KǯF =78=B#a'0JIeX֖ěiʚ_if r\win:\gNe??;|nmhwg' ?@X(ߒc=׆32si(zSnwdਫ1y?&/TV8ٱ=͢"3MGm2 ?!e.pbnXI0p[[Lxmձ1IuH'}gJ"Mz K[7̡yb8SsO:w:͋ EFLq XsY pu6 ^ӚߏѬ=EkAkʯhw|OߒYZ|;:o99wMk% 58_. ̌UWl塓ݶ=\{t0˯ڃm6ͳ5O$nW'ҧiIM"d!e | Si# 1C?@zw>/`?x;FEz4Ôz3wLim t vg Na_8G3 Ҫ2v̘Rv:T૓|5kVWR\̞vYUX 쌽h.1>sUм@_ᩳEGy)gv)4f ˳_x3|:=}=y?0玔pvse'̭#iѴy:sZ%eRp {vzÒ0g}upgqoɫߠj+a!4::]a]~L})ςuvz2Q7}ӆsl38s ֱd}ɴյZO6mƶB a" 4|m[$c+U$vkZ8in!X]n\)ݴޓme^!r}Zx?RBr $ 2MX ɍT'gF37B:ˣnNλ\)+]sf ';KoPnh߇l)_39/8ۑ?ŭka9;O^9?n;p0\Bӽ+纅enKw aAbI]K(6 ؀N WpyLF'ǖ*wҲNnh~L\X7Y)m {v}7t n}8KMM?O`B9}twvq>ia)4>j 8)KţҦ(ߟWثk'+cP8WA};z~'Z]{6Sij N:-I|Ѳ#0n,UhW 0AqZgջ?d^:!z'dZi1]߭V~|kuH-9`]vv6ihmR~f0t7x m}fĤws 8!s71GSr#) ۸ X!5->E9~uyέeG pXF$wg!]cx0-]nxX2_"@hS]dAwa~i.&x-F Wjqݚ{=iCCp K(ߵ)^sq埰g#{pg&,KښAHu%p)/!Fs6:P'$SY) |"0Ə D) 9h⪚oq Nq/ϸ^ ^ϸuOwq?7DvvyNӃ=!0M+@LwM{S"pt *=ߓ(S>m> rcCgegɟBwlB.pv֨/? sU&C;s)`۴:лф1\>Fu/_ '1 Yn?E`k|p0^^Hx2/e/'# UfqN_;4uGxSM h{Y(n縝5 `nntPÔѮJ<,! `uayܖN4o`A0``xbu{\׺[uiNͽށWԭ>egA>-&p$BleC[$ΗswҤ5.?~ AyN Z2d<3nzX&{jq>|duz1'Q ,?.П Gu}ℇ?|.CgzsGJ b+?;yc~:]o#p.f"]פ#{tͩ)īDž\38zʏX")-4 ͰMqNwݏ\~ɻr?A+H8+d_vƜ&w`)y7jB]>^ޱ&Nsn0rNh c/p~7Z'-ׯq+{XxSd0vQ#0w #hgaXqGKu-x/㬇Gýx T9}`. ^<m]3pP{, j hrw強G5B|ȝȎo6^蔆X?{g.^ֺ-KZEw*ɘI=O*qi6oHG̾F"~^N?a;0R>WTiCzz E@@/]s/i5>;k[vƻg*{Ie׭S[7=q$Bme蕮Y[;<_g >Pt:]28xlɿFWo^2ͺo?_,9A6{Ns 쮣GފcTr֏}r6FH_T 3x Z•A Y_$%mZ4k )C|wwӋP:n.=;;Zَuy}#Nȹr$ʳG2R DZXm]$?5E_%' [o.?@RKm^<^6펠:~w]i4;klpx=O}`$H&OeWa'V%;Wz6!)ԭi=4`?ēmvɚKstŰsvx 1+{>hܲE ;i7:-8ǹnG-k4`''iE{%sg+Ʌdk s8B^9]~)D}nO& rMf]5excT㲵w,;d4fsti,"}w S3}`y"p4;%UDZ P#c?ceEm{6<Y&;`JW m/Mζiݤc`+PZD {bjU/iiyLr|ldi],}Puv;@xϣlڟ-#}D@IDAT} xn|]EhD_ Mj+v;xC6Y^ CFK:[.n+c>}B}O1A%Qq~P'H=9Bԥ1$=yp/l]rt? ?Qïq{p^fnm.KMm>OOUcs6ؐ5>S/9=r;dw뜹kw~e =Bu7XvC8nw#+,$<zvp]ay@Vg0סM[@ Xc_! hj\ m#yAsW/]r:Z3KQ A6v#o-f!;S?"sAQWKNh LJo;!*~;qq0 :0.?FϤI6lm{څ)\\4|Y?Ip?:x T"鳅IG7ICp߭AԎ"'jѬg*WNDswD ?!pߺnjDgow1O ,,r NmUZj s/?c09Z7GMYޜeeZhcsW 'dZg9;7F"nMtdzCNuNw` gScxꚲrw-cwp;ͱf/(Iܑg+]t=^ L!l*]3 {j)9SNzjGLaPg5)n3pNtSPm,L9OBvԁ;U:6ԧ~u(vA-APxF| !V;i@`,LTJ7zɯ)o/AȪrTY؍~ׁ|UM h7~e/ W%KYZpTϤӅSKw9 Lqc7pUģDXC"m+4楣 UPNI,6DgKZ.!ḭG/Mtπao1b5B|vn(hkִpTTϦ'>"&cwJ;f t3p'[zT+/lƿzAִl@ ehGes71  Jeܧ W]#]!2wm8x~2xLC.lV\p; 9xPk?`Û{4{Nm8JUG7+?тa%!dR;n|l?OphL4nxhR2lJB|16fT}3^tL NRMivkL +Т1O-KG$?@ .8= pB<8;6rrA4"ŋV`Kb^pŃLQЭDf.5slWhҕwF~3K^qv?w=Oo,NN8rޥ6ǹ"_+Z.7gAbS,K|]VyYΜ{nxGz/\!(djCM\;77zk| = `Hٛ+WxOPh&}{dHԱιmk`X1`8tKAnP6erz#XGLO2;{#!O{~z=wP='W7>nGw4PcM{#4S #?`O軽8>EI|jQ#z%6Xlf5}~:{u 0/d4wR7}[4ok63_X,Ϫ"'PZ*ܖ`k t.d޼9jr[;Ʃ{—_eE0@#nώK^#DW?{g&7$U=>e>ҵ qd*R$? #oZR_tآ#7DAKѿkV+o|o>7c,A8/4Ϧ?GQff TşY-)q~[pnU\1d>ó|۝u84k uU7{Y pgG9W-%'⹟ <ٶDmw9_1ȋh}:l& R^\@*er&vh{jyeĪң 3k>hyְ r53p/;$eqN~@̔FHJͳnid n"3mK۹ɿ55pKypFNڵ[r۵ţ9Kv(\L}=7ڸP@Ka|v.^ulM(*U_;wsCwmqw|ב;/|lgv~wY43`gaHiR__C 3E4 {|'-Ͽ)0{@"/y(tIiJ 8 AӉXu?\ hmi鮸Oz%L&IՃԝvU + 7?Ug8Р~YOpo3g|x#_5~~xF{Z oڌ:j@8̸<^>lWH<ȯ}Y9aE({Ԏ44/B;jUV0x߂`-GJjSڛ#` r5`eO! u'n;h)qTa]* $aœ;C+xcݡ= l|mI#E؉>xc~K [rY|Y/'2{\ʾ^EeC {k@ })L Z,j>We?̜t)~}Oܿ?lMt(^>itt?yf ':=3y;~=BU&#]O?1'ߙUYez~\;?`(W>2OIMe5A_]e k<,ǧis2^;'%nϏ~Vd_hSl 43p~K ?$43W[o4p\4J/W-!*DdLV"yFٍdF;]ʦM>z_kOZيR-n{Mjf/2Te{\9YΤ<}%#sQ1-{k:QƏo;YgZ ; -Exb_px]y\@Ē5K^5~ȅeylG xܪf'>Ľt^s: x;߿hv0<٩F?rv;K .#!zݯz|BKA; ^c SY?} _λ37560 ZcsфҺ3ms:|.Bh؃#͖u8<'KwW/hEO?'I>'OrMkK |:%Jk0+8'C|J׸TlJ%8E7\Z^gnG*nҽ{/?\r(s+Xĺ7*z-U6A"gZ]BCRQDtEy \0J3e ݑr@ٵ.fm$F1]O/Y1X~'[?fm;W z?7_VaⓎOj_U-  X}`࠯g#Xlo-Y9!U!,{Uϲ“i'},LkL|X=ADd,N{yPMtWSf8s߾x7v6$^ O'k.99C-.t{LC9 6 k]:HzEٶ۝b|?:l2{w1R]]4e&=Z?hGߝf܃%x;nl+.󻗕mfFog洬H}T>=׿նYiA0~e0zN;2@|q=&?W ϯ1XxPpٸ~TG\>?5Iu1R/~6n ?r|VR^NSXUQ0?_cfVC{ZE?/Oj[g- mhԎCnP X^6 d=¥MA;$f~5SV`iO* yR6?T/*N^p|O$7O;{&@zѻ<fcZҋEkJ-sKw'J?0u?_MKa?cBp~Ux6>BC]hϏj_i6 jOY>an~:_c&)6ǃ'yY9ap@mx D*#CpzyG{=:7 icfDf 5E> }΋5h$W`D8x :s-ǖ6QM,%O_ m@7n9BR n ~p8øN/SYJsu59B.WZ*ׅ2#Xr`fUY-oPLǙw ҅GvDbvƽsf$E9<D/zK^ .K|ٍsfjP@މN'?29g289U3}:IG↯2<a_:Ȳ@(TTfG=?rB ' "yWz][j{5Ty[<7S^4+~i\&|%PuhΉbveՁ2I73;uRt 쁖iVIxey/uE~;8ujTt̑^]%Og]fSTA ҭ"%86: L6>nKW |e; 7^PП`s-|I#t(),峕G׮*آœl 0#wmd[tAtu i.O ,R1d%\TKdVN8A' eN[ wg-ҸN>1wԨҿ t 2~Շ\|ֲH_l#_t^ z V l]4 ޅgu樂aI[>dY:mޫ޾j d0G`Ԯtއ?k%Cva@E+~8Q`{ m.{*f5kwkRVzѫ6o, 9W: ДXx- h7K0ul? l"1G2aF|zt EŒ"xLlS01:9ԑ4՛:m61x]XO]M_lz֔_\G>i[Rž(,Edpݩ+ˇQAuL# ce/݈]9hG*Iiꠙ,KL||,SQtxmfV+t)1Q_nyϔy'u%0~r7}x?jGh~U˒?,BeHqXo"  @.r߹@ăp8H0eŀD_y0Cz)XR}|tS9T ,J*|9T<&9#LV>jսW!zSIS'ڣ(t2)VS@HZ'оj>B xZw)- 8.zcP 8N^ ⫝̸P_d\m{r6}C8%'-Xͳ(v~5J>4:"1R46鸢3d=&kI`GXp*; 4Ce^r:~ٙ Nu"Z{ KlZ8j^! WTx GM'M%@+b~d['?B@B3 f*+}ڇN}M4V⓵jɰna7aOh+\f̎OJ]O3{',[iN%2|P@W||g@D`@peyGOsoC;NtR]GDo]L4)$&LKb6aL]J:[:+iEa,da5rV,ML:L!2Ss;xt=`iWϣR@,_xj'Hڛd2~ i#Uo , Zen1RGHF0NOutfΫ{?`-d Wb64O(LWkg  $ު?P< }+#wE8s ϴTA[B J 4'32'KaK]?Au}t1z.5')8_G`M&>x+y;ǎ[V]ǃ@Jb^<`iu.+1so/ ҵWӸnR|^yq8Cy/1ܬU( JѺ1?lm#TQLʚl;V[vý #>_Et-'-ډ9]NKz:?paȚ*of ^JtGB4U:R4mrxhD>}d30tz~@L3@Usx訙F'4jev M@"y&@"_iXf;gf{]3SH/oWTlV{ M\/zZ>폯_BД9}2;q+ /̞5;fΪ??"$}nffsаGv0VsTyy@ȪOjVܞsk /lwrz`_ܜVÀep/q;E84s )n7dɅWׂ6q̧Óy!K'loO)x΢u=rT~s`ț>П+DZp7^Ve90Drr^.ThFoM}/VV {t}۝H:lXD#7ʻS6riՌp^E=R t!9 &_#P F`DK L{Y;&\I~S Vsjf̘'’=W00_]¦}y3˪%n^*8H~C? yMVK"1 , WTӄ?Pf̪{a=`A#(K ʴo+o9F 3XyAɠa:k%,K &Ϡ!nmʱΌW t*&89I ͙ڇ3z Y0ri82KVVɍ HP ?}ӎ%;U^dO~QPG|yp%!}8qM:xR΁,(yBepzfNK$w@erd#"ߚ?ØOpA$o=YfVQG J>4BlA>3}$qd1%:QlUi[ձjGj >A>S W`$о(|N3`8KN:hG~&;RgGՙµۂQ'bhx߄ GL994Nr `^8Q]:eT4ඉ8 $13} _jxx HwIYdWdCB$:;ic+m?ZrtS*nh!.HhҜeU`! ]dWy4_N2thM}2o/RԳb\]8ƷK*UFH} FeY.Q"DOCP^Up'oIgq4f^z/}1Opc {Ӫ=O}H̓i[|/Žv*Dޙ tk-Sdrz\pkW<!zZ{ sb8K|nzμV5x](Xxﶵ=8:/VD]eVRvuG5+WjztS[i܉GN#l%ug5k,|mM`)XgϬ;(;|N{k~}`KcF5O㡈ZPGۑG:K Yw 1Ǹ|Kݹ$] W $S܉fڴ|uGC`@/mG*t. >i,/pԦ LJm^rturWDX!OKLJڙ&~\j0Oݡf^cAXTAspu5>uuX*֐#[m(8i nfd5˞7nHg+q[io6W9P_; UY /$oovsʼn vu 6ekdw*rc2՝'Kƪ|a/˯v;ID `yABsP`^|/{Ώ8`hiOwkv 6ʢwdߥ"Cc-sZ9kGoM>JEt fhLX`_g, m>gkqe9g'k^_zߥ]y[Ю gk;[%-pz/mqEp{ @N-F[3A:>'0).A߹Iv"9`2"z|xȳ y6Hy2||4PQ*a$KMhCO{cRuͱmP+e}5qq cSui;MyFU {1 _ލ]'nzkleKsEڝɑŶ%j \SMMذHi1xAygsIU]l:j`$lAhsj\Tc3s=&2{-Beo V&IVf0/)]x̐O+N注ԃpY~t i#XSX':^DCUu6Dl5İ@Z8Ќ)y0U󠝔. ?}H;2Tu<~@Vy-eO~4q`U6Yq}hWoy%UE_ +I/B5r~hOK8 lr>Ny`BcӞޘd&ć3}B'l"?V&.У^flD2&}ɝzІq/<Z/cr{yLpN2a;깸0ONKzzJii@Hg{As!!SIGK4mTA@Kʲo>2Ť7>TK8`hFk%nf0_:O7ͫUnnT_kKҘNu$ %P= g:hce~x0r,3PPw^WQ<可't9FDv=&u԰8zkuӨ&܃ ~IǸ7>R GE*]*R1v+{_)dk{;ڏů#6 z li}E-pK+t?8IPtaU_g,D dcNΦӑkh +=XHr 0K}J:6LpEKm6x{MSqɼ?s@qQ<<]e^S`'.MɾysgZhj\m@ꩲ ^wLUrER ^cvڗǬ>y0ٞBe\mXFx ]Ȃ %Y^owq˽>p[3Y-y%D7 﫰䯩ŀdA%v)ͼb\A8U:V}ldM5stn+hj;/|q |9W6f#/_] c)cD]X_^Py`c -]R!<Y4 *Dڧ҅Ke){/ؚH+y`k޼iV߂Z~]z_|1}_lR{ekmtRn=ԇ;xq}) CO+Nm/1Tߺ'f#l:@q鰪c+;}⺹ʰO*=vK` M8-A&BpLjӡZ([8<'7w&(2O5n uƑ3NpkMD|C+i[M]'hjƶ Rpph9#; *_ jBM cdYw̉kB'ZRCE^N;'(|.?1l:TU 4kHAFT8'gJ9rAT:d_ر- ~8zi]1f1M{;A.n꤀@0M{U~^&+G9Xn,6<3Uy*ae b`~8?bOS@w*B-  `c˲k 1(Qi{ٶlcHuN#,+y^D龖-Ts`8kJCgz`QaYƎ@>Wl,W[i3JܒWֲژJ]M)&xe*ݰy q'WwWN4|"PfygplԇFKW H:$30{с3$]D }t[n.*k=|T=>زqU\BڧۛncSӁ olR_4ZRtY&PcRޯiO! i ,1䙽SKͼoY*v}~u3cLF+tG?mW>$eM5SD[̪4 #)@+mo "8(hmV,.ِg֬Y?м]^0٪b3ò##>{E3 QhkJ,+q'!Du24SEgnvJ WD"oS|+* b7H:au}ŏ5m}Y9g+}kylͬ1RRّȓ8j~$h_tw86aQ|]誏} 7%e'(%pTG$l,n`LygԩtUup2  JԦ[g甝@DP@uTmRl$.6fF);vCrnshm3#2b}#u#,]eDb4z nSgAmQOE^DPr~eSPH"t8'Z.$,/c~n/H/%{tn\]WثKr =wJ)j{3+}2®kf)N$ %7=(βk 8J+۔blFoQIhw]#)}>Ӗ~v=زX {@Rܞ'aq?)hlX WPڠ ?zj87j>C2|^w=t^?+|@tԽ Tf0**a9]PS=XH(# _]pQu1h@6AWXx`kl>j`1HjXA`pbms:cE&pBv"w63IR'L:FJTh;YiEtٍks"KQnG%NyY|\dNwdLΪLw0خjDXۥM{:iWk.^WS3 sVFÆCT﫷3t6yy%wVr rhγ'5)0{h#ʯ!`!h]MѠSxo$8FުBdA8;r/%'_$GQ*0y8/m(BXl>:`[8} >πs^?olW68y\ ^$FL[:_߆!aҒm 8шhNTico`Z)ڃg!T̴7s{wc uE/+bvgpQSrjt 5'8V/[`BdGo7R‚;jKg6gSo=/}]B$ % d2Ɍ`g.Ff-A ppEr]zI,/x9Fein1RA>  ba-lc[]>M*:mٸH]gd-,',3~8O:$o5@@ j" OzQmO[mRls l˧'O;&4Ơđ]Yd7*;Af"(JeGmp;̰[g!UVSD\J5{=?9ҾP}~_Ǯp1vt$Q kv' ˝L["Uc؎]NO}옠wU,uwlֶ&EZ ό.n04tF;tʪ* Q=;HSo3zg?ײ`eoۗ68?XY6񅠸")dP" : LprR^INiD$I6O} $ps\ag9ǒ|x1?fؒ߾U ?Rk*+{ p?}MX>8nT®CqA .ƷRDۢHC M&&AodUWb`. ݇dҦ5ntAx"-b {Bxm7B_L+uMY\2.]KҰ.ɔb ?KhGP)1啓3PUx ,wj8͉Y-c$)zw`ċMӯJ_ѾR-EtKSp&pb'hIƼ#4i's Y0e33~+[^5lJhWm(P1wV5Ű;q3UI*&V\e*ntY=ε%Ueus?w[}dЏZF37_7u;CK|Y.ch ޤ3 s)څ$(֎eC7oJ{"nUrekI]B=Kdα$XZi_MЙ*y[N̾E$p pZLjEz=esK@*[8?U2 FK3AO]Ҧ}˫n>qȸ:h}ɣX"5>X 'h[8Æ48]U-C.CK]1$ &^scaƍaȣ nۜH5ܚ1/1@qzlnjmK9OY/8nF|T8e>yw6m-#L:4ӫlsJPIݍt֔R%atmS1 4lJx9Ƿi5 ҼoͬOo^yHnleww`n}ÿ>s[^G&6هn!+92.ؿ&3̚%Jz@cXsAX~x_yԳ'ɒdBP?r]IQ&Ohey0MqݟJR7`l>>JM2 vlr/֗<>tCضHd><y&x yIHG{K'!?ۖ ``mW$B.e; *6]EvHVfG6 Q䷍FζyHڥI=jKc4yiNquqU_dF:ew)1ul;z7zIg HsNIh-# _]˦ d|UZ>/O1 nF`g^Wvyt]_5OGNm"3S;w/ ?cvŲi)a|B ,ûӨlvL}Kϰ"0f8&A֟bRo1x߯Sh|){#=V=-~騹M /kHaGu!7 '/gr< qג JeЗV^`ۧUK`˃dΣ-B_yҿ>\O0Z:2JGr%sGWӯwET6D)4NI`pIJþUjC6R'zk/yuh+У}/ ^`nH9]Y]2w;mey5Sldt52MH`h$ɔo`'&{peQ>ͯ φ9v*=0-6ln*_c\;N4fV*  W#aVC]HD G2Uwj*t=#%k8TIMڮrH̸]c [~X6نxR;a?am' 'u/>4C;U{/^BP: ɴo_Oy2s2]>BxV[hM t FVpZzɕ[^؝ \kv" Ö2,<'SSW K QӡE XLz^3\>CI QۨF ž|zϙ+3K/@չ+%~ _eeՉ$;G=ghxhj as1aE˗VEZ゘6)F &fcd 3]>OVKiATڛTyOګqӟ}r?>=3!W5%.)xGgw'"*湋tiHҹāVq.HdvJUeYYOO|qm'gxkٴI1?aJ;6Tz?ht[xT>iԟ[ Ŵ 0dZ~_+2< Wag Д܇d̗H_3\.lh!aHP_6沗\l-cڟ&m&y:zQ7ڐe2m1xV:9zz6~{z=cǾ?Թ?qD^/S o#˅|6b/"uj/f]ȷڵM𡃌 \\1}g[0X#OmoR u=Az/7F؇{`:ӌ_N"<+mPS!jF"HϫM0MQy8.y$ !l{HJ~BDZsܯQ|lk)b"CߎMG:#u٘lշJ Yib+p\ snvR&]M.vl$,fQHQ݆PNF 3QgڣK?Rcae EIaL*Z¶XlnUG/ijI#xF֦VY*#:J@ +Yi4i{Ѧ"R 6, #G])գX )1Sgw kF²H\9pmVK#Bݸ2N~%-9=+a(z ޙl3BwSxL|>EU.NLTQy\9tX'|*iv̲6-VW;n~-AuCDh=.=}o|07ty?M49~-uӀA`]RooO.RP3Bp[@sT,c ۱ 6r?3˜G}:g/1Q[NYk^Q/dMfQb7AR9["r6c{e*6WB?mmd-fkcwJoC 3Ż/Dk/CI5}}Ws|>`v}D֗ZprmR+E"il^Ɇ٬BV'L Q'm9|pz~Vl ['- ac<¡4~N.~,qaw}s? ],#nh [2ƵCj8@m(h.ʕg砎PǑxR]|68lf&(,3O; S>Q?{oD[?#dL5(66jMQٟ㸘suU{t{v5{X҃L}oT"' eK}3b2m:uGFd&;y".֢LI>#("%®2k Z 8c̪K"mBP 03FIK/~ٜ ,?%6т.W3`Uc|afQɶI=ሎZ6^'74A'jcrhjn;ɀ,d[^lՀh)?b/Z9M%:"Crf|(0Csq- M۬Nۨ Vc-%Ե}mo5;:}irKfw: }? ݩ[Zo&mns6eժ݆U m(Í:EpndfF IrʰLL&nHHنr|@UfTj G[ ].=خ6n{!k0Ib6ֱTȔC03k-6A&rPlz%A2";*L(8/!c]2QC dH1Q,bv}m>y9{@cv2IսOMsz7.8CkyD)hdg.zqxc 1{r'DV>:Z+c:08P4}`I|l[6!d!q6?|6`1:o>x"\0Pgibً%=W{)RuaTH qR|3Vh͠Z{|Mˑz RjF|4̣_=?a3 /W߶#.sZG2 -h:s;ִ@캎=R[}G)2dYM/fDT#"3`oX(!dFV})mՁU|Hى %2w_(gȓ"p%qS|b|be2+p\.`/1-}ǸaoYϣ:#o+u,}f5RRR죔(ֵ5'K2Gx9Q=7v+بv]6[ 'EiH & `Nx랞.a E_CGaU**䕾vaBͮ4Q+VeՅp'#O'$:Þҗٖ)sϻ>Rt:e;Jtҍ=eJ)nS+HsGTWu`aY3m_}v)Su01#fwi%+jD d6ߥ?f-l&l6*ͨ6b7 {i>me?U8Kxr!b=ZЏZ.#iY˅NLQZwPݹJ9t;ۿYolxqeDz'tOmPk~Z#TAcm y<;m)+h9$Rڤ`)LTNە".<%e<&-$aTܓ~s~ ֈ1f)hthG Vݜ*~Rʫi]d7bGL)KRs*mۭyҴ4xzp ;Yk-TMcNQ햄*wY~^hO^za`XU۟7%q&G_54)]Op{:X(YV.:>~  uVy BY mkixearuaNmk/=<|YXd׃.L|mm2lfW#ŘC< .MY m`,5^2-sn?pfچq6Ovt\}~>$g}ists8;& mq2.dgMSESIjp IoلzG>$հ6*m-rmR~``byp[x?fBn1TNc5*z? RiG#۠s}"$ zX:% -':Sp)#8;hC .N*<ɇ}Γ%Ys_}Yw.y>ȝI]`w>\8CrNeWcP=lf2N>)WW`9_lYH*,K鴗ɜCmGjU/q s^>ADk:f'` 哝2/ǽcg}c]>/uʣpW G /ŞdSi 9+%ݡG=Xq!PBk$!Cx3r@bDiX66a۬])mY0ܶŠYۀKץG-pGiyj (-َE'cډnW 9ZiYYJv:H1 r!Vt$cI5;F! zg1 |Yj6D\XF|F{ѾxL+AK?2g];)]ǁx@5}UAcٸgIpNCh<\Cj:}v[B9f)y3rb>m:x:}q)A$[]C#+T{-EuaLk~Rbk<=?q!n׹~?VA¦Cc܀l˃בY:<:XӺdH"XKn2Ԣ<yճ}+~|.e,smx,|Rþ6@!8b?3 37t;HEӠYG{rcLƷбr-JVᗱcfLC_ISW1X  ^{@+J9MsW e/.xЋvv̰HBp ([{vQO]/a]'Pm4ȫB69ŏr"M7f&"q,ڗZ!z_@JؽK>1Umb@3K&fJؕ6KIRn^HM3p}fIT ds-)y6+Bzt"LS{f6tlǗ k]@:Tf):8ՑmH9@O/UgS)>D:-.f1+K|3 }qUvٌ|f;^EJZ*^vkzX6ζn H[Y}l{4(W\^xY:Bi^!/7 ig`HI`1ޟsݖee#g +Fi +ԦZ|DvQ8A F]nU10+bRfq?:muc1Zh$me!U۳z9WW7 r:| H_ p8Ew-1Յҕf)r)a@LmfҊ]r&^{P./W@omM )6%%WHiei ۧ쁁V:2C%tmZJy-c ui˽''Ռ'ܻMh}PK)' {+~i}0<wt ~Iؔt0Z !BS"p62xG*Рٍ$ۭdZeM7mO}JiwҶ9ky8ްb݇%@{HJ'8+[a9>&n8u'/2UY,}LRk}}b{/νLzvV-p7G_@EgobtzFHXGtA=mZK9Dś䶸?ɼDpV|! 0l4n'KT{\EW 3cÚn' >vrv)9_h Ƕ/0)%b:;Ռ۫kvH[bI-Jߺn!քUufP/߃&ˬǵOVzk&߼:#{xw/n vfm.fukzE~BjDYN#;P^tK;Y*:wyf ]RʺOCwF%7&jO{M@(w=tƷihΙܞucOܾʦ &L[@SŴ]Mu] mfhK#2Cʧ=ʧ\(ܜ4U[zQ aeehܸ868_hi-K]E>V y *_3gj0׋iޢEȹQsM?iN+.ͣ8YT Mxȗ{OkУ&4 S/Rvb܊qNm"`},GSπu,|;_@U,ޅ7aڋ&4dWwqع3BtET](kaV}Z"$I?haxˡm5`g :rqփ3d̝i ӻQ}mEqg@`#v+3EHAQ sÛ9f!,*OŐR,1_O4|_1U <^s> g.( %]q]^,C,ɉ]r8l`}d^fG]2/Ǟ2vYiYs-NC`5g`#8>CD|9ra뎱^T/fDŽ胞O;3'O~'jĦd;E"B{eQ33t/Q;ڸ>{ 3瑼Oʂqfe G^ʳ&DqLzl8q(U:pLkQ#߲ct&'`1sU">HD @8|){&IDAT1X0Q]>579uR B|PA}3Im `_|gRl.ˉg,t%ʴ@Wr#c_/.Xy k/V< ]L SK:DMRj`~}jbyui(Ict~6.S.Ƌd[c"[)E"a=amS.=ƞO},* w#"n1r|eqj(8L<8DePڝ(Fn/alRpcYS¥Qu}Γ00@dz;ͳnhYi,E3yq;5oR0b(;[j9n\t X<0<gY7u;b81yx \/D⪳~1"VBN0kFcѷ[%uTa!-)q #-s b W2 9-D}1-Q0B _%eO|Ԓ݃5{-QLi_JP7zū*j3[>ٶl1WR8/1duSLKp"zlpNrh;!x"<7"+q5ht!⶗9v ' W O?TM3ePs959BNe~ٶ%8<=31ϫql{\eZJ- [)9PL0vW>S^bjuU%E ^`eckS`J& ̈́ `q1`,fDܬxݑK46F[ =Cl@.ܯHBc׾Q#Pxɑ_*( \:ض\CiLu<<cx8YastdW1IRa *+q`珪)s?d".G50n:͒ pQQbPgՒ/>PC896cs&X~qv%GYb6p;m:_܅ J!1OɗGm)|X!N$nLY0h5ղ Ll6|߇st[#I~r x Qȶ':E6\<~J<(Lˤ`JbrcGܖk9FN&*O前~Դ-炎!tt4$2Gv&UqGE;ڃ2v?3d0㛠Pbf8i_'pqsuc`Oeݣm10\`NN"DZ^;Piye?BS7a@W!fn:ʫ5^W6Fs1ZQ萛 9.ds)H>tj1g,xcq @J&-DÁo51D%1,22p{UY.[sG Ϗӳc ~9&vEbm=2ϖs,;(Nj68in'֖Ys6z ry:7X&9-1ȏ:нP.\=Y޳3Glgcd}G8fk0.%yә 5nN<(LD0ǃk- #91 t ΠLϞgJʖqk^xzCO3?[E!x W ?"PQ1r`Kj3d-ֆt{$N[:RWCғŒyTdBՔXHCǍA_΂*1gbn,:wӐG0kM[]mR_+1#Hj?!lP0#2/sr 7f2 㯬lT¹q2ΉU ]vէf?ӛ,P.5 !?>2 *fo3l%kc(sM%4#Y A0c#v1 ~DZ,9prcA!SSn_kΐ,\yˊ:. *Q5pr+Wę`{PC/nꩌRT3j0^'nN Ɠ"*:ѩ.z+Sx4_mWd#ƐC&nrtm3YǪF 9".|O1>wn +e|k\.뫙NTnA*:4Е΀nf+Ost u8 N} k*@,-n\qƗ}Ƙт-fdwPא`7k%DrM;VwWZ[UgmUiZ0;M=!}: o`XcPhqu&,e}0Ÿຆ̑{ڸ8slU(`GHf↑u 4V+nCZUsnH: tw>\^@5-<+p}▲Ph7NO3)뼏&]΃cq޹aLW\ㄳ.E9tۊm`<-3h[ad=/ -N9/DŽ T΂ۆ惄}n05M4MtƦY Z?xu꣢|b_fS'U8hi[3NWES\{ D_ɼpn9vc,shxqf簍Ǹ39'y 1lngX`@ }A7fxlť_rfѼӿIͭƵM9\a9n?Gw&">VYzQsw C (3b/Q3<!҄\sNZor^ָ?<]EY/r§eN3<3L8ټϾޏFd$EɈgnSpײF3.oo?W>e11ívKmX`E57Ҍ5\֏;kְPD|$0+ *tд2f힀 Q\x>@4l+٣~xNK!nf gwܕ0s1k̼EeS"Մ\Gy*e:VXs^ ;8{L+X箛pX_3t!>>EjRcҴ=֎O9 #rRNy愨䭹ˋ1QI|4-x6{Շpb3<-}Om4ܛ5{cPr<[ҼN9*h,2mG\'kޛ 383I4ɖEuo["?32VGPC?$wC8+WY6dhaǓjtiG`v[K/R|=U˘]^@BGD&NkN6ckg[D>";IaDDAc:ؓgo .oI,!Ɯ3u:yd~^o/Iw[o5/l9 (ϑ(O\>iS=sò1b1|Ǽ1{o31ˏYH! c*&j4A>7C3e;?zw/˻ rNoՉNBkI3/c\6ۮ (v}EsNأI vNƂ>#zq/]&lĈ8y#-B֘FyY Xbf3l:0nM=yeٺat|#Kؼ {وFq%FK{zn2z_W'5c2>1÷9M_-:/=ހ،r\Ā!`gA1cD!+ }6~]| UZm<{#gyįJG|6}hM}z)ӾՐ=]!0`pq#m-Q9&e 9EivWѿrpzuLM{H 2Lv ; L,^MS@{)^-#½"?ڞ9rY_}ڬ4&@%c\qx2ر(r}cJxԭIGAܼ n Rޜ})c+X!RY%к[#/iV.>ڷ1j򑻀y hlV)gi\f($&Gb#N W5c)O:/q:&6>=̼s\l± sJ\ПaΊ5{G{E;DsWj쏯ܺ7}f۰s[)S}04R0:9$;y}oclM}39 !Z7lse!gS-fK<Z)>B{h!汸+,_ :o~E&Rӌf!FV>l̝y |7JnQ j\_w~^'$wy^[:tsFT֋q:l]C\1憵tB/ltX #n[ſymߺ'oRMȇ2,?gp= l~AnN`D{$U}l$<`J1|93\h7,c}㷭C4ANOFKgDa&EcދU11#.ۄׁ{M`a&{4!SWE.,㿴ߺ oh x%kgh=yZT W˭n9Uq^fܣl_JߌMwMej-O3:ߞޛЅ`|(/oVXMo~}~~fwoNxw (ڤMh8Wqmؗ~,P0~ۗvց/]!\4w,{(qG8ߠ捏gw8IE^0(]ݟal01Ke\N}0ܾ5ܶI죦XAEV^YSkL k;%6ͻmz& /ٰYY` "jqןY 3v64B k/աƘDZ/3Y!Ǵ,fct~20e#}!,;Xty}1o)v =n#O_Wi(vu =QmV[ɫc\0mNG䧜w|LX>LVVߺ,R{glfm9y`FcO|0{bfiu Wo]p-lhp٨8pzA`ڈCYQ#Ge8.^gx.e@WwZ+:V6^xG]ڇ|'Q7p͚hr4B8G3lLήy.2~I, .q9֞>61p^ Anu#6Ćpa;U5tlWJ100>k\_ kySsh̳pE5z޾Q9>hfg>3af)F=Z oЭsjp`_Q#=jՂOOa>YV10c@_"ً@=Gfg_yʼngog3sWkY.}}PYD5X ]p|zJ)~WC&W-ػ%&0qzf/Wd1:[#4d@Xd+2仧@h`az{5+8dpZ73[BHz?o]s {}#1IBpX ܛb{ x g6v~5xi4O,xǦzӞϞL#3?jpSD %QĊ2P.;/\|NὺѼjr1#s#=tWOP:˩C'|~u2[{8ݐZxS:7%^5ù̵߃g~\ZϏ13[@`C@aVqn;飼~+66 0;ywЗ^u(n/`;)?|12Um܅؟wb1/=[+f^IN&M^}'H>пoτW_pvog"}k zI[;~Yx_m'"KŻ?j#! |iW0ظB+_>vwAa Kq?@R=_FsQ:~i;@S7co2+ZhW>*WKh8 M<>4?w&x )#7N <]$Z W^@( t|;YyK'֨R5WXb=`kT3_ne@{#db<ڀL|,s Kq'o1pBëk9<1.ݐ@f49qnaTJ' wv|Kf!|<ū<jZ_q~ wV#3ϤWj`̀>Ce~tZd@}ժ)>T_#iH Ci!E2O܂ :R r@ g 7M6zXb`ˀs-'?ˀ7v۹? 0_@B@nmen\h$ȀwEMPS02b D+PCk@ k 9_KbS SL+E \iWlEZ8j\ `T~OҴ 1Fϧ1B1*7ɀub@ 1pKtX 1 Mg7b@ 1p5+lV 1 MPCb@ + _aKb@ 1 nʀM7FĀb@ \a@ [b@ 1pSo1%Āb jWؒb@ 2~Ӎ,1 ĀWPC–lŀb@ ܔ5n`1 Ā€d+Āb tcK 1 Я%[1 Ā7e@ #Xb@ 1 0~-ي1 Ā)j7b@ 1p5+lV 1 MPCb@ + _aKb@ 1 nʀM7FĀb@ \a@ [b@ 1pSo1%Āb jWؒb@ 2~Ӎ,1 ĀWPC–lŀb@ ܔ5n`1 Ā€d+Āb tcK 1 Я%[1 Ā7e@ #Xb@ 1 0~-ي1 Ā)j7b@ 1p5+lV 1 MPCb@ + _aKb@ 1 nʀM7FĀb@ \a@ [b@ 1pSo1%Āb jWؒb@ 2&}%IENDB`././@LongLink0000644000000000000000000000014600000000000011604 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/shaders/doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/shaders0000775000175000017500000000000012641367670033031 5ustar jaakkojaakko././@LongLink0000644000000000000000000000016400000000000011604 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/shaders/lensflares.vshdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/shaders0000664000175000017500000000341212641367670033033 0ustar jaakkojaakkouniform highp mat4 uMvpMatrix; uniform highp vec4 uActiveRect; // (x-scale, y-scale, x-offset, y-offset) uniform highp vec2 uViewUnit; uniform highp vec2 uPixelAsUv; uniform sampler2D uDepthBuf; attribute highp vec4 aVertex; attribute highp vec4 aColor; attribute highp vec2 aUV; attribute highp vec2 aUV2; attribute highp vec2 aUV3; varying highp vec4 vColor; varying highp vec2 vUV; bool isOccluded(highp vec2 uv) { // Apply a possible viewport transformation. uv = uv * uActiveRect.xy + uActiveRect.zw; float depth = texture2D(uDepthBuf, uv).r; return (depth < (gl_Position.z/gl_Position.w + 1.0) / 2.0); } float occlusionLevel() { highp vec2 depthUv = gl_Position.xy / gl_Position.w / 2.0 + vec2(0.5, 0.5); float occ = 0.0; if(!isOccluded(depthUv)) occ += 0.2; if(!isOccluded(depthUv + vec2(uPixelAsUv.x * 4.0, uPixelAsUv.y))) occ += 0.2; if(!isOccluded(depthUv - vec2(uPixelAsUv.x * 4.0, uPixelAsUv.y))) occ += 0.2; if(!isOccluded(depthUv + vec2(uPixelAsUv.x, uPixelAsUv.y * 4.0))) occ += 0.2; if(!isOccluded(depthUv - vec2(uPixelAsUv.x, uPixelAsUv.y * 4.0))) occ += 0.2; return occ; } void main(void) { vUV = aUV; vColor = aColor; gl_Position = uMvpMatrix * aVertex; // Is the origin occluded in the depth buffer? float ocl = occlusionLevel(); if(ocl <= 0.0) { // Occluded! Make it invisibile and leave the quad zero-sized. vUV = aUV; vColor = vec4(0.0, 0.0, 0.0, 0.0); return; } else { vColor.a *= ocl; } // Position on the axis that passes through the center of the view. highp float axisPos = aUV3.s; gl_Position.xy *= axisPos; // Position the quad corners. gl_Position.xy += aUV2 * uViewUnit * vec2(gl_Position.w); } ././@LongLink0000644000000000000000000000015100000000000011600 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/shaders.deidoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/shaders0000664000175000017500000000111712641367670033033 0ustar jaakkojaakko# GL Shaders for Lens Flares shader fx.lensflares { # The vertex shader is rather lengthy, so it is in its separate file. path.vertex = "shaders/lensflares.vsh" fragment = " uniform sampler2D uTex; uniform sampler2D uDepthBuf; varying highp vec4 vColor; varying highp vec2 vUV; void main(void) { highp vec4 tex = texture2D(uTex, vUV); gl_FragColor = tex * vColor; // Discard fragments without alpha. if(gl_FragColor.a <= 0.0) { discard; } }" } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/Info0000664000175000017500000000015512641367670032276 0ustar jaakkojaakkotitle: Doomsday Default Lens Flares version: 1.15.7 license: GPL 3+ tags: core fx alias: feature.lensflares ././@LongLink0000644000000000000000000000015000000000000011577 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/images.deidoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/lensflares.pack/images.0000664000175000017500000000056112641367670032727 0ustar jaakkojaakko# Lens Flare Images group fx.lensflares { image burst { path = "graphics/h-burst.png" } image circle { path = "graphics/circle.png" } image exponent { path = "graphics/exponent.png" } image halo { path = "graphics/halo.png" } image ring { path = "graphics/ring.png" } image star { path = "graphics/star.png" } } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/renderer.pack/Info0000664000175000017500000000015212641367670027220 0ustar jaakkojaakko# General resources for the renderer title: Doomsday Renderer version: 1.15.7 license: GPL 3+ tags: core doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/Info.dei0000664000175000017500000000017512641367670025242 0ustar jaakkojaakko# Client's primary resources title: Doomsday Client version: 1.15.7 license: GPL 3+ tags: core client importPath doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/0000775000175000017500000000000012641367670027303 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/rules.dei0000664000175000017500000000347312641367670031127 0ustar jaakkojaakko# Length rules for the default UI style script { import DisplayMode UNIT = 4.0 * DisplayMode.DPI_FACTOR } rule unit { constant $= UNIT } rule halfunit { constant $= UNIT / 2 } rule gap { constant $= UNIT * 3 } rule glow { constant $= UNIT * 25 } group label { rule gap { constant $= gap.constant / 2 } } rule scrollarea.bar { constant $= UNIT } group popup { group menu { rule margin { constant $= UNIT } rule paddedmargin { constant $= UNIT * 8 } } } group document { rule progress { constant $= UNIT * 30 } rule popup.width { constant $= UNIT * 120 } } group editor { rule width { constant $= UNIT * 60 } rule completion.height { constant $= UNIT * 100 } } group progress { rule textgap { constant $= gap.constant } } group slider { rule width { constant $= UNIT * 55 } rule label { constant $= UNIT * 9 } rule editor { constant $= UNIT * 20 } } group dialog { rule gap { constant $= UNIT * 2 } rule about.width { constant $= UNIT * 80 } rule message.width { constant $= UNIT * 115 } rule download.width { constant $= UNIT * 115 } rule multiplayer.width { constant $= UNIT * 80 } } group alerts { rule width { constant $= UNIT * 100 } } group sidebar { rule width { constant $= UNIT * 80 } } group console { rule prompt.width { constant $= UNIT * 13 } rule width { constant $= UNIT * 125 } group commandline { rule width.min { constant $= UNIT * 25 } rule width.max { constant $= UNIT * 75 } } } group gameselection { rule max.width { constant $= UNIT * 270 } rule max.height { constant $= UNIT * 215 } } group coloradjustment { rule slider { constant $= slider.width.constant * 1.36 } } group rendererappearance { rule width { constant $= UNIT * 100 } } doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/0000775000175000017500000000000012641367670031103 5ustar jaakkojaakkodoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/close.png0000664000175000017500000000631612641367670032724 0ustar jaakkojaakkoPNG  IHDR@@iqgAMA a pHYsnu>piTXtXML:com.adobe.xmp 2 5 200 1 200 2 64 1 64 2014:11:26 22:11:70 Pixelmator 3.3 ?d`IDATxYEv!dT@,&@4>("Ac`DCX|`Fd4l2* `F(#E@"[޾L=G-}ΩSuVRG:_WWׅptPq8{aEE%= A^<6Ͱ@|=1r[`.hƤ*̀ D74}74JH{@/ 'P0#t۴ 3`'_*~>YZ3T6?E*{4e%Aptkp;`uT? ]NR/7 s1>8V4yL*8m ) Ϯϴ16 JemYpUIh$mjPNLci`;6Z9mka=N&/xi'{Lǒ'ql:/8 H5yIb )>zG=pr=f`LL 5['n 'Jub9 6P5`colشqV"_g'R-.C-pTZ,[Cx}G{xVEW^2zP`#d9Fo3"][u~98Ԩ< gD:;6qد["cZ(yQ+: GCD@y(:Ҏ*JvVޡpY >!hP=oھ:1OVB.q t Hut-:tKtVb mQ` #-~awƏJ)|װ(e^6~GѯwI =J/eCy+D: |L!V3 r m)ϒTBPX.\ |GX V>py Mb?u(@ni]`aӼe|tuڊE׽ǥ݂:`hqWG_ŧAߊy}DI,LQWשu;>p<8}0|媴 c r´aV_\i Ι|,ɟ~gZ+=*1?AcLjdLGce}px1s,Q+J1Lͩ u937)eZ٠+} Fo"y-4{_0ݔx j/ `;J<ô7B-q uU`%XgCn7\)/v$[aEilIp0r8xD@ Au o&'[ɴf٠:oOt5 D]IN}b(}! pPW.@t5A;uoi7z}(طdOWbgZ#wFvG, TS_p,h!tL1\P`Lz=7p\ 2v;жfSLgi`&AКP MtP[0a'?~=i4լ6£jm نɟlp/hp+00k`!ubHK@ghE~,۶e aUb舭ҼBADXt4+ ;F]A0 d#[˯TTt6 DB@z0!>Rà7^]˾7*%CNG|@}́AO"0JRf/*NW}l9 vL$3`mS5T׭)}ۜQY#Pd:r.tIENDB`././@LongLink0000644000000000000000000000015400000000000011603 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/deng-logo-128.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/deng-logo-0000664000175000017500000005450412641367670032766 0ustar jaakkojaakkoPNG  IHDRtM=: pHYs  *iTXtXML:com.adobe.xmp 2013-05-30T09:05:31 Adobe ImageReady 1 72 5 1 72 128 65535 116 #@IDATxwdG~kzz{rݙͻ$\HNt;l l$G pa8ll ad@6$+`+8d[w'cڜwr>7\I}ݯ߫WU/֯jڶsG;sGw;߮oKvET]kC v&'VΟ=9A 9?><τns-lOxH{];&yϭ}{ oqϭou0KL`;LL s=|yO&v9_󜋿r#>yO`gBd¶s5x2DL|KR:(e@Snr޹Y" =>5 pgB{Eb8~'(OR۳ފ@9Ô?@8ERGG8t(Mۛ:T.# f/-sm3ut /wKP4VfZK3noM@sH_W(J !7(u}nw  dn,[ G)PRR7>yHM=.C wW\/ F3Imko |^Րq +T,-l&Ex PtŴ<;gs<^g/P.S$AЪ]ė[235μIlﬤNR7Dі*]4 z?ПS tmn`؀a&h&#=մ0֑ <gjkkgɠ3!Aq;Qζ>s8`?S{Oã#8+uۻh󫩻+U])ç^g|%"~q[RߐlJ7o@S\:jzZozj/48+iᶎDx?(׼!#%c x=g)rx$|?+ ݮQgIH|v|b`l%~[Z\Nn2& Z]XN%R|bz-AH!jj@;>sP.TzzcO<F0zR .:<>]ݩ/m8QVRlm?|nokC7z_;w ݸqI0JHBKFZC̦uεBr0,2{OOW^JZśߔE2C4({7=L`3KLx?W)I~쩧 AJOy$9r4G!xwbᗑTs%\V?ה|yfb}F60iJ_̧vTLsqӕ.z1M9۶!|Z˸z!E#$^nFst/Z0!!Kq<,s)=i ^?̓O't{흝uNOQ8R_7AAh|^^LkHٕzU~ka pxggg A#wP;GQ6ᛴ^Ҡ>~_Eb .@PAPr|6$/uTGQ?4RDuU8¹b[ 2OeHB Bk)>W10z}u!e5@ul4;;R  H~YAJmCt.mh)PBRonեZ۴N/`4~ _HkK (Y$x\/(`x@^ѣ>cS_O/3Ķ3b' u;J|r^2Cc#7V , Ϩghii @`AxC+ xID`N\ND:q3i unCx`U * ,Kի{Lɒ  o+B|ž( tSKSǎNg2}>OJ}ZJTRC (AT.}> z_eF$įqbc+^XF1X],+ܻY !` !F 6E@ Ԃ p bxs Ӥarg7.ޡ4 }Lׯ\IB_S~A 2:>h!q#ƁJR˵}m ᶣyltl@!xUwAåbж[kuC }z>;:5~\KsXX5P0+~:^Y^2zeޣ A@F"ϸk"6Tm =uJ; 4 tGw#󬃣ʒ~U^9[zK_î|Kf{;@`3[E{(OﬦISߘN COZE+&W!-o vo3lyOPީ'ΖMv:>utO~czV /T pJz/)W;c JZjj[~nPN Ѯ9xHvj:zo1TAoh'~LQtPC= ( nT'0 aV7%ё3Os(&ű[:̷tߺrķSO<1uNoңǎ^·uUkj=`< @7W4[XDdRw{30e`~szӇ֛oaW/CLgl{Zjꦝ]9P>:p Vu5<2/\i(BNG#=xF |=+c=7RD P:5Ӻ?8^0ηӔ:~艓UuPzDuy&lh9ܷ]Q/,,j+p\$"TgFOa5<?FS:numW"ÿ8al#OlKTچW/N mt6rtI vP-u\||x~-}úl SxȉGN3[L3s6=?Ȥ:ՐnvŸgEaAqNS8.HRV]BkN70DKKuA=u᳇Q) !lq\WݬJ 7 n_A`),B{s ۶:A"%LNDx(0j1z@iptt[>eS p@ 6/O>3iQhju`o!lcDCKG柧@A>:|Ԓ/1\q:'>N Q/YK6uga#19>k@iyCy2KA)V[o=:+N&r.*s"\=CB 'xԐ\H !۰+"~5T!bvA-í=L ;&<܃Hтw^ /.+YNE5 m"psDjs;6O9b"Gw RȠ@ɸ }ow~JQO} ЍN`§JtV8u*'r1[KN_ tkz9ݞ ǵjC,s󥥹w5r"~t .98֋x6oF:֭ -M" Ej@W1pBBYmul 62֔:` jW(J@@pmeXkߏčvg1ǧ{H҇wQa|_&Tmaf|e(u4;,$aܿ\k7|}mm턕fE4źzL昮 (a7^^Y˕FDUF3.m4T0R/z5%VE& ))輱==T-a|rrG ζ0^¤t}TZ)~IN h7-{XD6&bB+;/04vYS{1oŊ}3{UKN,.urP7/`FU#y~SʕXJAX{@];uD$/=ت:OD6 thKǕ KUR}}`V ?IIcږ8$uV6יX/qt*td9r-Y8+@1ԗ4 5B"loCCizu+ͱtttu<x\Gf0K8#xId :].7oP4(TwՌDΜ+o{`U\9:M5*n~MNM2WT(!PH<s`r2Oӕ@BjX,<{?5O (Z@kM8+E><6M1@FȤEwQg탢TZC3KX!V d/~'!}=M}!2Ė8&(3X]s"dShsm#~Ep ;~b?VO2 C7ˌ'U(#m#@PFGgf>@$E)p#7!}bl@hGE$9at+i(Jts)b\OO?4Eۑ!t%8m X❆W2g%iK XIbȺAQ=1xWHWBx3>]S y<ȡ)|3WM&`HVuc_ة!W*lߏS퇳$LcNwN}ZFE'q*p?!Ej#U,<$p̀Gc}%|iB7u""xUVPl\zG]p i^P:nޜNNJΝOϽ>te!ʳ BMHet~],}A8؀@$lG'0rifZNPuhWU0 Uȍ1=!Li]5 o)Ãdklq˔̔fb,}.Ǜ@&/}ZN^)VUF!pp ڸ\`}Tpƀ@T^'XuF5Ճ6*]ԍAgSz=:ߴ[ P&! v!Hຮ&ϭVx KXL _8yz{XGB!mڐj& =FիFubG왹qY*E ]Z{?@Xbbs" uBu*p9 }U`B m__J/j^1&F}F itMS&q$G}p)G?MFsC D1PlNy B\|1v qhur&"AtɢWײemC;t¾v4.-h@k mMQ@p贜sw|bQYs'm1YY>&p[gΈ_q#O|Gr L&2߃Ϸm:6KQŰ`>vnl &ơqP\|]굫mh*&N+IZYcI Õ ^>r5H=TyF0ݸF(n8< :_twOa5:KnrͽkAK'k"Ɔ A}H;־A\1Opz@+{x0(EG} p_Zİ}sV?cߒ>jH栭$Bow`\1k42ɕIu´페T58 FqGb͐ZmpҕtLH9ZwrowoTd>ˏVVqCR2<=7ߜ=,4 ߠu^vPN02#VZCm ܉$ -w-/}݉ "X!ֿ"ϫ..U "vx( DC?[lqv:*BUANC=,yyF3ޅ7C5@CE[Y# Sn*SUR9n ͘_AmBUUS=«8H'hP~P\&}퐠9v^eL?[4\9@{:U'}@1];ȽEXynʰ%\*V;;%w "v {`88yN!#@ Dsg3WI;Mn-a#ⶈbSx+"{J8\}v9|7Mf:vNt t~)6C#p3A. P=AXwf~IZmD=9NSYdF pc? W9Wl;qrr&2gZm6h;u ]?!`<塎sju&YDTv=GrWb"VD k|fWϐE>OzOZJUZF #Xs}=H@IF/PUцev xԪW 黷YA{0aj(1iuE3,L%P@`c 4YJfqO<+g/9`:8aZW{ck` pŢ;!,]R:+ya% n~)/]:1.(tЂ­ᴔXUKzxs,BAV• fQt6*B0<ŠVetFR$j=%2}@u2T\_+q9jO;|`Q)nf+qx>LsUc?ga"ѬAX ZZBGB @"Zl߱x"/rV j C` 48ĢqU/H@ݿѝpýߌ1Qݼq Cq{[s pR6;~-OYGz ˤF%}Fg0 tn\E n5~5K|K?j,j9gIUJHm1DN3׶ LVvg̊=b>aJ>:JCU8z}\&bt 8N.u;Q,YZKq0ӯ7 r 9Sva0ԜDST # t!W$f|6QmXBF[ ![t%O`H.lPnTRɸNKF);ZQge NCI2&#X<}LLx< \'E1:%΢+1x 9j/TV2hb2_O Bs׋(1?;0qt@ϨDְ1E ;ѥX׆pCpHfA 0G52MRC?y= g2Rо+B'J^ A:nVMȏ(m2H/20u}Qd|LQFwq=yV+sg :( P@ Q t^:JdLwڢS6:be#uu] K+&p+.3{4"h MTwȭZ 뫊l:*u3o"EX}]+#ҐnUiDry٥֌JWJ3p6Gvrqr-Mn[90.[^w son\Th2iD}W5uH*wE3f&A:&e*è~3r! 9"Y# lZWG^uW0"F9HI%JD]MI#ۭ` Xmwgr%Hl+Ae*Pi2I,MC|cҏ؏2K@k`_ς ^J٩*5'Ҕ€g)"f=<81bx\2D/= "w J:rxt4Z:ygU%:b]?%)PΝ bP 3'k4H G[ E"F.=Ԅ4YCj F$} P [D5z<\ZNC۝'SP=ma_HG?H?ӟͅ3퍬`88nVUYI1g0P!P6\<1 #g` ׸X|)wk\ hcJYFHvjH* 8pfF*]&fgU? n&=Np[Oy:$8.;C;s"n_AG6ff˰?!~$7>d6F6_U 1WՄF: Csbn:nu2)&:nLߺfKc,}}c SACDk^ݶS7mLj <Ic͠ ITu_I ^q6~Ÿ {W韙nȪ.A'Dr{x!Q#}}vH]骦b s\Fx W,l(,u${;ފk&oF˸K(P nֺ\ꡔѮ0Ad .±X.ZCM@bXW[jhI[&Ss\0N: aNſSD6z2tĉ6{PEI?:{ |-3M H'HmsjO"DЏ0Et2J7']X2ĭQj!W#׋qL/+iNivf{}c?#ɴ^a5%#gXIYU{b!c!Y# \U^EuC92rV3x=l & @]iIcҾjJ̌7cn"i97h 苶I[x)݆T6H4=PƐ8sl+ z槻@ke/Z'EhwPS+vu\t"D]TljEf;1_\ld5 šdMuqqvs ݠArms,$eK} r6Go0m1-dZ 6kիߠbVLh jz8w~7vK( FՂLf=l8\ PK,5:50ǙW"]c"N/6p]c'1 Vm-`D!Ve l $MLLY]#S/Am~Q7b"r@_N[:t@ƼztGWC#TRL_A u oƨ n!ssֺ}>Ұ5KA@3N?""\o2B/9 &RaKiJ|b[˿@(WnWǏ( LXcSi!OwCDgF@[K\m9#&/^mz1^f•G,=&U(|Y&E=ؗ];tD@ { ^gk?V[hFY;('Dz:j/\!P"׹G&p P$SiD/nhdO8 ]kL]jQqϾ  ::>g3uN#{.%sC,p>6Jwu65ʭ-2,9/QZAp_s$wzߑ+e"A @N!)];\O]-:Hԅ4*Pr^E4󯝏 ruiiqw'[a j<:B\$yqi ϻn*ץh "$‰$uTdz 8on&\+GьmeM̲18ޑ̠wi=c/=T\8&i5]{Jr%V|j|fw~1 /bv~pсnfʗH!urw_kdB"U'T2fVe5Bz[J'qXxйqMU v*$~*4J0rRFQM> C9<DL2^_.l(Acx5^Y&L|~ /R}k>|X4JQ#gEQv@@zO?&MЁvuSPz-F@R Mr`  焍xDٸ׽<49bj,FI$1-x4Ȭ 4xt-T4G{IW$d Ii8٤1=fOk00J'U-FKd&w)$JfHX/37Y,ɦ5.r Dt ؅۲:0por($r`00jq`YZsU/0~ 9vͺsLjnES /K.1L;=\.3o^c1I{s-<CaNȊ0#C!,X;H*3:1 xJH"%1[>c`-_ȠāA-ǵ=JΕf ˔/#>zI`rap0<&bL?V :w\D5B_[mBܠA4-3J#e}̾9A;wˮz! #̙=4zTp@0u'$@l%h:k^Ǘǭb-b/SlWp\T*w $թqa5$u.>PKaV(9Zg2D}-}'AThT#MLuKI{}Ajl7j=72݇q{3VQrfK;rч *Ք 8_k,5Y>q_ſT5mܑR NBpt#VǶ^p3 C.ي}_"G*+%,m|63Qμ8*s@6C2 1-t *6c[YW ,K$q÷ͤOvz1XH`CnPkkKiӹ軒KPa$ N9,&+6LLD?D FQFs+-2dLioV qʢ^p;y5N 5 Ȉ7{7,1KjkգZC׫U:e^ltlLST`$*t2$\*STYF67J>@[^ȫr*J%h6U+]g}wRIz+D`Ķ:߯'0W@)SěPJ|dniG8" ' xO 3Du(;Q]v[NhӿCK`8n;Mr/ &I1 fԏk vC ;- cw *F0q\"& 4Cz7~֒"?pBb˸T ۦ]MTb ȘYO;SbRHKJ7ra\1ܱQ8#ުR2X({H$Yv@0hߋu}b`(v@j 9Vq~֭t{iVDt)#5N,I37|wn!w8]x vuhp8vE[T~4{9&e׺44%8>$O;Dsҭm. DQl/zk:Ø()523"hiSvz-a4ޅ߆U 9G.K@K-h6'k( 3 >F/5ӮBirUnPZȖ687E<J>zHA1*#&8M#G* Z1 ]NMqo8]A4#uz TH6rpa㼅mY5 bUEK[Ŀ^ ~P֣g&2Wg%SE0XdP|{_Y`_$PW_>=t]Ul6 1">1˅" u_M%7 $.mT;sK,@kx$3)v\ױo V(^ĕsR\.n:rk+x!rݤH,\>Vw|*M5?(U[K[/j7 V!Gb@!(!H~u~#&̓"(E3pO@ĩnUncNC'-N57 T D1{csHe-1$`WΝ ?GHLJfK;ޔ SlgA1)/_qnEs441 #X9a$ߊrCxCЭKygΎ:ŤvEs TN P+m2/76\IL949-e8}=vj*-TT\=gPfg |9]alR՛kH'P#W_x_9;rE \Rm@6BX4 J r: u ^x5]9ZFx$HK@S%`y`"T,Ro*ܩ,_ﭡ6 9/jl?tYܨQkEYvZru dw5KCQwmX#Z]U<,u]tq*Ҁű/ 2;d}8^Y-Tڟk/~pY(ALwvf>gqD #%mЫ}a$5\N,vݨ@r%:'tT31@/z *TB})Ql GC둠Z">TBʎ@zwj qlfJg_~u){cPѧ3uMl,qe=nkZF?0L,A\$d*߽*Vt;hc0 H% "1̀z#y@6ՈsD[а '= Dէşrl40>::]'2' TۧޏV_p$f% v@b6;}RUM.ԮpNЯb]oؼh"ǻ\aٿKk"g[!IY[FQrJ6@A/%Eug9HR@U Po 4iW#c廘k`V42q T K}\J(3obasAg ̓l%+Q.ŘvtpCE27u! }$Bhgdepȍ}^s,fqjd:5mPPhTk·a(ۋyHO0P$t+ *dkxq?=(2T@P*Iw}^8G~p! 5$HoOV;! *G<͈}R#r]]:r;dg໛gxK6@~!)w@ g EJٯ{PpX9;$V=.7I!T 2RB1ִn}yWAWҍFȣ)[u Eq_c>@bKd|*@v?{4J}NE&]QvgP(T/=b)rw#~oHk YdP@`^_9u2B -{%`Ju~$>a[hKXw q{ m0Ɵ3>D$2\9$=rx"[. [F߹פU /Hu9Χ r?{+Q? D?H;TRv</yat`X1fD7 õ\2[ PXY( ZApM%xÈB@cB a-' 1IQy3d"?&Hx$e"\+H:(7_7 A7MZx ewL P_zo^. ʿ\&j@~Dj; CՁE #ǟIϜ! ~kT,*a8wzF dFˆ{p1.&U܅WEǬg$"0q{5*\\W䘳W{E?fwp^n St 9>L|]=9?gAY6m=` R%A R (g?Ma# "P tn*ĀvELc0VmpKt}^99[0ӕ$>!'(zV`Gu5 Kv3ve\*EJ\JL]Sae *A"8ȩJ)2?b>ſ>5k!tN$<^r4, ©g}.9.*{p_VgpyYpҁ.MLgLgϝ&Be@E,%ħcG _ @I ,\2 (J2h\*A B@tF`|Zi{ׂCܭ[A5 9|aqXT!gm~,RnQLx=y@@J}HvcoA~)Z:85Eg ==Bb(\ zl+W"}7hi xľDL_ EJV-^ ,=㡩m1RJ? x@884GwI-sW 5K!iYAx?0FB57N7nbH\K]+H_ ,q3ܞLDN7i×$@d K@Vl; WIP0C\A@#ClElH%DVt&:]>|u̱E o} /Q3Y"?0=qA0ȍ@ZZ Zu3 4c(;S7Y(P P@iGD*z޸;7 $Zx~˼8~{%蒫3[ϙ=K\|Fؔ ;Ax;%JV @|wYua=d8cܧWa=rHf2*i^~~;GгW:nljO{Y [ \Z 3Csg-  Cm"^6섐@&*eusoK L r rX2s 8 coYExnE@9[$GgI8ϙ{>gN&gse H~0\|uzOyg= r Lgl.n%ֳ5=6w s&#ϭϙ`g?U2[{m}7"s5zϙ^ẇ5Z;]"JHs&|tKo+Nu 8εȷL IENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/audio.png0000664000175000017500000000407112641367670032714 0ustar jaakkojaakkoPNG  IHDR@@iqbKGD pHYs  7ttIME -1IDATxk]UiK* BiUBF!i$~(h!1B PmՀ 4@j) b[f2yuvsϝv&42ɽw^{uku%J(QDp 3TҙfJ,d8$II8'fka;p3pHNW&`#DZ `gIڃ'ǀ 8bu!ι?DJI?4qXR.I%Jj'A[7LI'e2:jms'y2[xrNv2 OXok8˷E?)3|x Op0f*1E"grm~?qee}笽#97vx ]& @F{c4*u{){sZ1Ow`!C ȚCjfN |m;0-tAHsgXfSLe=CM9 x89:gxn=OB-[{x!,)h}?/ayWI3~j)ǭ%]/p$U,/#L&K0QI[k>'iqI@'s*aNRUt 7L$] Jƒ,إ,m9~ cf^qεr5Ctmz%!j3D%-t6Ii{]`9tdRt_Ω7OڂOik?;.9)^$^gDhk8G^|}]@r8Zs~ -TI/=:c8Hz׳>WFCڃ9>fP}b, 6jx,K&xcJ*}E*ǒSkNL@?2ϑvDSK񲭕9f;Ge<z?EJ@HYx_qӊai:DS2?I@cYS%|<='xz߇I:@k9?jf`yLι0#x`5ҭI;9Hj,`s_USbIgz> /IZ[žs\`~UeozzZ$3jX.±@+YUι5Ƅ d0P@s}06E6l jx]aNkUu7]_a˼]mPIp.`CN#@ q,J*3ñނ5HȦPp=gE w qEla *?1j`l`4D>S Fڄwp 8ô+ǟҎ_f|3"OzDu9Lx>0XSf #wa]9fuOFG"~~"@ّ5c'ݒI#2SYtxcxqڕapjNW aa<:Clf0e毁5ez1Vz[˂ZIx}zV.`Z'Ύ ٧Fz%mp=SKpεF~ |-2B ж7eFc E- ZOX"i$=moM.hk9kuۢ{z9[%J(QD%Jh]_y6IENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/alert.png0000664000175000017500000000634212641367670032725 0ustar jaakkojaakkoPNG  IHDR% S /iCCPICC ProfileHǝwTTϽwz0z.0. Qf Ml@DEHb!(`HPb0dFJ|yyǽgs{.$O./ 'z8WGбx0Y驾A@$/7z HeOOҬT_lN:K"N3"$F/JPrb[䥟}Qd[Sl1x{#bG\NoX3I[ql2$ 8xtrp/8 pCfq.Knjm͠{r28?.)ɩL^6g,qm"[Z[Z~Q7%" 3R`̊j[~: w!$E}kyhyRm333: }=#vʉe tqX)I)B>== <8Xȉ9yP:8p΍Lg kk Ѐ$t!0V87`ɀ2A. @JPA#h'@8 .: ``a!2D!UH 2 dA>P ECqB**Z:]B=h~L2  5pN:|ó@ QC !H,G6 H9R ]H/r Aw( Q(OTJCm@*QGQ-(j MF+ 6h/*t:].G7Зw7 Xa<1:L1s3bXyeb~19 vGĩp+5qy^ oó|= ?'Htv`Ba3BDxHxE$Չ"XAP44077&9$An0;T2421t.54ld+s;# V]=iY9FgM֚k&=%Ō:nc1gcbcfX.}lGv{c)LŖN퉛w/p+/<j$.$%&㒣OdxTԂԑ4i3|o~C:&S@L u[Uo3C3OfIgwdO|;W-wsz 17jl8c͉̈́3+{%lKWr[ $ llGmnacOkE&EEY׾2⫅;K,KhtiN=e²{^-_V^Oo§s]?TWީrjVQ=w}`嚢zԶiו8>k׍ E  [ly邟~_Y53rW򯎼^{7so}x>|쇊z>yz7)bKGD pHYs  tIME s4IDATX͘AMQL4BcfX! SH1baQS,,Ԕ-Ya$eE`e$Bj" Q35?3uusޛνu|t/0SuvB=e>N js RU-%qkc<zLÎV5HtԳ8lB+OÏfUUYsYv/usQvc%ѓ:9r. PT pAGB]R*/ZpŻ}6^RCatۮX0QSX 'G+;I4I7cKV+Z@:%n+@ڝGRM(v7Qn`MT`3kZD6<11RxQR veժT4 H_,{ncȐ> 2B_Tܺ% ='aA}EZ7fdYuJE]h@i+!*Pg5u~SiQSAXFY^ /ɛmp՘B/fXoIENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/input.png0000664000175000017500000000153512641367670032754 0ustar jaakkojaakkoPNG  IHDR((mbKGD pHYs  7ttIME  8K`6IDATX͘KUAƟji~Зb "E AD+h2h۪EhAQ`(J3BK4ͥWzsP;ΜxHyqW$UFjs9+ PһK*BE&m7IcĔIz̖AR*%O;$KΗnt>-%yL<1%&Wo1giT]ܯϛ0h"igM_t:pC7"[F}#T_sIzpѝιl `XیY nI/$]/{uIfmWvh/Gӡ@O={좲^y]TVQ=ޟX#)vE>pR1zM`џS|c:&G n9)]8, Ħ+3I?-S $v 2013-05-28T16:05:45 Adobe ImageReady 1 72 5 1 72 256 65535 232 |@IDATxyeYu_^YWwWLXcki[bhB`!$ƀE0Ba?䱰x3tttWuK^{=q^FEfג u\V*y߃߇eA)|$3G@1}af `?跏[13}#tm,?t^y|k;3x 6wyG|?pqݛiqyyo}O^e!d<7@?׽_{3u_~u_uuw?躿?Slg4C=g9 zGP5zmw~z?/?{=v' c7|@W=houw_}[ͷ syAǻ?;m4G>g{CwW`k5hk굽^ {Y^z/W_>yg^3P C5׽7W׽ z}{>|\ {~gg4O#99g4O>+(+cV{ >'P6<11p4#i}|Kn\]7QH琼zsHcRS^&M2e|½JBR[^cV A{s{_ng5>x-v }%~@W`V}o%XU/K+Ijnh3LiPicjxoj0zݦh:vGnEҦ?I6p߽ ߋ!k޳W2ڮulL>&w@!Tu!Jll:ϐ&)t337L}`p6Vo+/=gxGwg~( ޷i:RԬ,/5xZ~*w3LeulV V 3:!/k_ <9GI{nJCBp`C 6_[z=.VmA{ZPݦRC(1HG$ 7ua j zxNs֭֍MZCI9~6Hj ntNZ,z{칔g>۾ '_bēq݋&TTMЛ~mIppsfjzU~@}W+]Crm j808`.DGݎ$ļ'tt$yt3[,_ܸz\o6(<)orfV ěǞپh ~@u_@%\yҷH_'M޷8r9BCʏOG}"y?(Cأ d-zj3#0 2M@.0(ڛݍ\@w Ȩ <@@=~5#DpLCh*WŻ͵wmnopm!Kzd[y|}~vAdEb_8/Hl5U{^ݿ%:ٕOΐ޷9y9zx3=; m$ICHa,8#@s/72:S@ 6o!7;[L"m`6n{r/3fo3N@uݘ [MgӴzj[M!̦ e2 OKf#A-ʢ"cQ;tBs~= \:I8K,he5UWPMcn?Xb{tǻZ?/qdӤ_&"itvٳ8f ޣH=<0wpR$!'Lj0ލ~[ ZGKtx·WV5z9p]s^z24 ˕@`<QWfnŋ͍˗S]r[5`W&P_;YCߝ; |aIP~{\_ 9ҿH16O6A:GPJgm&{d &&e Mf?ehO@+`ӣ4>?x4+޿t@Iڪo*&v8(J^7q#B4 <-dX0:dlֵŷnV0vm:tD~~&P}?3 ~srs |SG7I@{^su/?A$SSi?=7 B8zx!)֫Ͽ~LL^?8g>EIS+0ƭ |MWQN2u$NƥK we! E!-e[`|&:Ia(L ,;4VwxzBHU#TY{qMy> `Kbnu_Re%S$mO7'NiQZWDcc#1i=qy뵷@DX[ZnJݧ UDư#*2=lP2"G00w\~ZǝM? $Mnc>Ww__[ݗϙ6e?u/9$}F[ű'haZLS1?6/|~_{6iFxi9 OM4kK0̋.w,D2͕02ɽj0]v->_ng ;ٴlؕ&nsVc+K$v9vhEz#bEDOB]yjb4072$RN@;E\S&X.d*HQuĄm!egź]$7uF*5[d&O># 8GFiqR~9ӎ oABw1+ ޺̟8,4ʳviP.LR>Q~M*\c n#I32L{xu/'3U3\R; ~ۣOuҟ!~Kͩ3OD tʏ&"!s0A00{lx$0a a?-O\.% KJܞF8Ǭ/nCJŅ.8^t6ylx5ڨshL&Hf?57p\Qy(O=sd$Ƣfo_є "˴LSB#JǕ! vx-YR?lUCQP*=Ifo4R~3Th;nN  U\(2/h6;u8wUIS 0gmuEE W׶G RU4ʍAQU|HxL 嫔sfszo}MߝWx5{)hQ75(4 ʐ"Gh ~:}$I#F ؽf&8̱lU&g{kv0^df8A8S803`W՟lLlqؽBU|Z |47?1;ė1w)_~</Ĺĥt Mj<@nԫߡ\HgHsgi;mܵUqZ.ΖψBM?an~aD#:q4sV=xB)157`~{&3u'CըC0X1-oNWs"#05jjVNG rɊ2nF`S.grx7;c5 t+j~!e;{gk[k0QQO]bPocIM~/mVxfL;gM2]ை{q~7=*4vd5+_|*1N[6܁1&(PDZcljK8/L@Gf28 }τ])gT' ԷcPSO`kHu%Zxp^^ZSV9޿+Wb[?# <#fȢ!(\Z\mݻ,7D$.Trn3εV͏߃vCDS2.A&I544 4/"%(Zw˨-!ÿh*殌 ys> C/W۹_"e/sSH'B)?|Hs{H}Lم;_^| <_4*Gs?u(Z _d-ʱ때MsWy@X0 `9.sW @5 S۳:0\,3_~G+C-7H^-uM@`k.64,`79ad@&s0,=QɦU }s)xĚ{eJza*=iA/O SGG'mN0?ȡCC 'qh헀B*ݽn:}qzo~ByuB>[- u<ß8 XLAJfEC̜~b 4)n2̷&EwL>:(P!@x _C|f B Yeᨴs!GC|tҩpa$ K{9EB|hN=bdPJgHP>a& S2 К2Z8!iBHyiv@+#3ӜS} @ϑ=Rf-9y9 4$+!Eb.!/7f\ ݫK.ۦfA|]zҶ6qopNq Eo5x;~9o-|=H6@BɼH[9{zܧO"zܽ{YbD pF P:k858/%6b3OD"׉7 ȏ9ޜKG䅖G}q|2]å2-~zn cjQ ^ɬ@(wjɂW jGmL@Hc͇ B,׹ڂ5AA&:EhftR&' `d2:4c($xzш͊.FC$I TF@zn'{η/"O%m ;B׽&%noz' A9Nԙ橳Cf଻%lW~a+K~"%@ Вn>;2f@23su,0W)2\JCAfخo% WCF1~ѹh5>*7LC9b%u^Cc Cz7u*uj68 3 )3}bei[Aߍd:6Dd,f虴3+,"+!P_M2|9j{W.^n^cT/T~7ؽ[ݗ+)> Eį{$%x'uH:wLgÌ;j1J2h^hrxdn77jZ-g}l 0 26R|e"zkxoq=*:qSN =jRW#wnNuy.a@K~v6k?f +>-`LEM3th^g2.2} Vj SMcZH~!xˊ9h3N41Mb:̐bQh/}&rHldn|(*T#3-Stq{c~ĶO%H7"|<4 D)jv[y 7g=ќls|n!>BWjڒ;R_ ";j窶v[_ש6Ͽ5BޯoSۮ!:H%Zn60!@-A<|.Hxe̔`5 Fw`6<"zJ4AD+Bf|EdR2b NJsj= gdI#e8kUFF|_"- \&O߿BR#pۋ Tb*cAߟ+X<_ f_Fzk;Nb>{yJ/7.\iLǦN?R ./CBW b -!暄I2_7T]gH%akU{Q>,zbuڭc@10" scc 6n L}}Jekj5zԭ`tX6 He‡-)@0 K^Q5Դ43qھyTF-3G;8 ESRVjvX:e$ɅK~H27w3ߟl~m -̵N;\ۧ)k| {<4D:sQh/m>q3gU(4,纑mg{}ζݷRǦr_3q+z]KxЌ~^s-4 **Ku eJgNwuJ$XTQtB2%!؅@dd*(jwq O+].CLr{|NBqFj`r㯑ohFi8jpBh+Z?R}8F)O" mABs]̣>&{\wI6(=vo<6Q{e$4n_/ү~m~o;t4F3w| 7F=+einb@~WE/J9N;Z<셯HL5N_g|#r)W}NH/LRǝw6=u'6 ٨5$ u# Q"#e.CV'ЖYl$v o` g]\$thaofH` VQ1+!0,Ihy%LJL3_roҟjtZ'YO|CZ0u}\Ͳڸ{%Ơ24^xE*i'&gGO6g|9q@32T_^ TKzeyCܣ}G/A{VP0)yrWƽmʐU 5ǍcHy|yN~t{qwq`]e"_0nD#=%h^[(!O}bxM H[Z_q_x|آH=%p _:$K?D?Xw'qrړԙF űۮd]}_ޏ#)3TP4[8 u?xҧ7IҜӼ}?KJ$Im4;ǞKn7;f(]o|l8أ6hM6DžKV[v&bo g΄@8.:/[|:/ETiڱU[C*"gF% ͕&i(fXc반=xR.\NT-Lc/\S_x\ܔrqf啐ȧN\GGcBo1GÉҵ#cTy mVb|-XFVȻqo"er3J#?R]/DFa띮k~r}-G3ye"[0E5,N&ad/vۯ.~$-VPښ89Y_ok?MrJ~kBX@?tR 2G񓧚㧘ҋMs DW$b\ABL~[zt7{,j+-(f=ʣ֚o݌m9\ ?!Pw+WnP?` qiC;) ޘ/]~= m:;3~+dYU_`r Tԥu`m__-DL td21?l::5C\H8@[)caN2 9VeM||lgwuۤˤFݶK}= ~ܿ l{g2-;N5gu%\;ǀI$rq0SЖWr Cu>)1T'[O%dE0)@*Z{edUFm#ķˬ5u7N4'qrj"ܺ ah.Ϳ9bp^l;wE6-Y{Uo%u?ʢ}ẮC-R>،wHT3^ SWQK{aq=Cf>Jy24h8l$`}C #qihAYt0ħt"Xε_Q S¨KrQgP`vUyV =yk Hy Ez؃ITH3̗So"OF96ޫ#6R oYNoSoE"~,w.^h8RY ]dF㣥΂lT&a(on,AQc4*b,Pk`4 2:T\Sľʈ ije._B):TH[`=;ig&Da6,apo%~!A| ; |Lϙf!37o*!PUzO6^'gG=vG-T@yձ'Ih_ʳ3H>5wj~Ԕ`OkCP:.سN0h|}{G4("|Q5I E&1#8s\r& e^@=,aGBύk7I-3׾mbnxuFf |dM0Ab h_YDix_ZKo;5@I\D"3<7^ * fqbXTi|wmU_Bg|" Z+ԞhYu9`Di.V Gξ3$+СE]=ŁW4 _d>Iup˯ńy02|woG[:HACXs /BdlZ~*fc0` AtFaރbY5SKr2&4civY6m,oT$)duM+ca{pZٺ_9feB*/@!&p3G;' NQ/iV~;]3RTe } ?or77{(!ܤw>@X7qpqU|!Ux* `F[v^&Q $A=6#C$ɧ80qիW2*p͛FsQ,I-2CCl}&C0eQ wk16hZa!|drI#Ѵb vgQDx`LF+5wjn'PgB-;/eH2un pubR= /  QoJQ˪G=|S5?v~Dg Y..8ίR@IDATX@jQʥ^֭/upʿJ:x <|BI:Fd?o Oʏߟsh^2T{$qNyD[kuo$˕?VcNC yUPңGbquwki"Q$DkYCaU'1L<Zu rOYGڃo1W mj,ә2ޮ7PjYp\' FLy*WF]RS (瞶X&Dzٯ3Syw;j,_Ct5GAٜs`~BKmߘtPD߃9dA31[pMYO:q~&_ G$:aknirh>rp/حIW 9}tĝt]ztSNspl9%P^l$Z;02XIO E!O@ݶ/)[$ugǙ:AԳN&Z̀&kl][L@]>@*zLr szR9?@ټ4-г4#VV_E{v2;JM+ qG/`4жYZ4Iޓܧy0?2]S2نC:~)h/ "QDzٶ. Ҭ?@'~Dy4y^~*}a,?#0\( 7SLHx?FHAuҶP¤J%^Ɉ#gTn&uo%= P{A4ԋ/KP,Cc:oL@y.uH+F疳N8\J*RGM2zH&h"%Qf6 CGB9GTOTE!\( n+|d|(@Vsd\%X1ga5 S6mmŵkMͅ;ii_5 Ã629 X 7Ɉ3pdï5yݲJ 20wڊV7&ϊ&n4%e瞀e2M"$Yɘk |_~e#g=As׹gixeAI%d7Mb/9<{H}OVƨ{"4 _`(oGh3~|:cEj3!J#B/2#s[%l;Y)PnJ \ J&,. 0lWZ?Cu<0⨡J[K/-vU@W}7F_F{Z/3ΰg'( `9P2᧩Hzm~4%M* Yqz5 SƸWڃo] L\7Տ (h '@k8!ߠUeNXPKHg Ck0Caa97O^Dj=h쭛)f9[Q֖#${SH2P>9F0u?N߼$Me*PS/=L] tc"nu{=v_ b1_ON# v\9*,3OЇRcpShcHl;X"ܼK\xJtIqv h O@gkz;6c|y I{jsrf ](LJP$TUl+h)t"*TkWz)}1u%bDH Du)v! H%R0 CMgjCg g\JHG]Pf* S pc(4 dX N&tƈ8ui1>2MD4( U;d~knk ʠ_|6``(::tx9'˲QvLJ^=vحCkA&ڕV}mnNx9.)era|[$B 0Lk,hBgՂJ28{%5 5_VQ%{70 hY~ e"hׯal ]k U}+o A3smYAp s+B6͖.|Ÿ^dD Sv*Plزġy0}d77IbNNb%+J#˔߳ўU xd p{+걩VvJ6W?Uzms !A1DroqE+9=D/U1o2.6&v gJ@;R7 Sԯm"PF!XxC\WK0<_ MuWX$k RqE$}H*3_U{df=l ns!cqd,Z4a{_QyL,`sxyL[ƢjFl>\8iHKȻ2cԄe&`}ef9;yF!L!vLlAc]. ab iE;^ !h0Bߜ: b3KLgFД$7;9|x`B wMV%N>y<}<ܪ[B(,F5\Ȧۄ܋Ç渋J+ DMM%2)ܳB+T<-B,"V 1lU{Cu?U7U }#w 1OPyJxi_ǜTD򜪲10C@i0dPߕSp6O"V": Ҙ~4^]a 2PgTT?í^bVÊ] :pli->WF~ds:CgJFT^cyy$ YVT9{+t_v+%@ 3ckK0Cp.`SB 6!L_TbջwpBOUpp4PwiH/pQC * yߕ7 Dя55<[[m}P-k!d4PY 2_wbP?eRvSe~RJ# %LE%v\'CЩh \a+WCaX@j  Ӥ2:@|t 2g(y57Ү5:oqoF3?G!ctRJMvt0ВL@R.XKo. ̀D])g?aԉKhW,P:84 La.^<@_H+#]3U&`[iJܽwHӑᗸ;GVoq Qar-s 3w^䣙pMr1Vpi@)h<7A>8yw!E'^֢ x:j{~C\eQ;K O[;J 87A8;C~cدKhm_h͛,lNTb%Lvpԛ7n(5z-ER1_rls(pSrjZ2P`Ϛԏ4w?}rFPf4>-bYg;n0 (E19{_I 3Oүt}C߃}Bg!pز_ڔS,ڡd Tlk;9)dt׹2g?`0g\׽yL=?2Bo}{rLg:PKP/F\&XXkm:ZyjuV5,ǵq*؎oz4p?E`nG3lXg u}}L=>]UXݹam\F6"q|'Oaxxo>Eui2HqJ dgf&P8䜦T3$61H0ensKC~S9v}+07f'^f]Z`q cՏsF.Lk!2Tޣԇ@#ji32Gj0j2Uiy\XYXg2A5b2P?a0f(hO}&z0oPVσk\- #Ua+(=p1C44wm|i0"7Ȅ$r5 /q3$C-I"5T=?kr݈=TW +arǵR{+'t 5*?f>3 3 8Wih%  hp M"q6Q%'ՈĻ"w""7Zm^yر p?5yo*p9f1V]³v c)30LGOͰ Z΢M5d] *5-!Ë4ܖz!v[hF: 606XfQ΋ s@싦Atʣiel x3PLm{ǘ9d_ئ+um2]J$ITԅ, Q-u2߹N$$@GED {ikFpqgZls EHMÎ֣EJ&-lm@}m(m'hFd3yiBpzI.g}gD7˗.773gy~H W}d %f[o7O=.쯮 K^O oW+m(~T!G &*^F2 +nNHh]P駔W%g()83}Q6nQ܈>Y׌(wJ̐P~'6 0-܅D$O~(|nϸw~?k D&%ksר}νfhe<4P ľҽ{+RRoۑB &@FmXt4m&`#y7]暑tK*6P׉L'"Ug -WuZN6!IڬrջڇoA@2zjFD(:9 )e$ZcBuQImSO7'O@@{4?#͜3tW"eXgaZpj/.Ƿb`-?8i!ҎeMV'\q--'W \#F)Qg)\gz}!)Y6Uc&X/< ap}a+SafU|B楣>=C[ŷ> 3Wf(ز s̫S4ʢa0~i87L&dn;~ŠXXSX[oē6n  (۠IFPis#/v4kЇ~Fɮ8%+w،zyg")3Dt7= h?ڳ]rP TseL^BR)$ʈDͪTS7o{]ǔB5Y"L\ 0(}fK|䝃]Fnf eEQkgYXw)56ڒtZön}%tqE c"ap\3v; 0?/ |!{ϑ#>퓉 ,A$2n.f0,h#QLhX0~YgѠ'2lqM zE㛝{x[d3ٮM!!h7LwۿPı`vW-Re Թns8WqG#VJ i A UV{`$.osv>7h24W#M-)9v8s 1F|J?b͵)ѺRڪ޽4ַ{̳L #:Ʀ&(F- PSw}NҮUJ 8F0hFwJƴN'3lr0Uڡh\9ߊ rC =G9~ihM]GH@|a$v }؞d"> ]`V9P+Wbt[#۾afɘɂrmrBQxb?Z.Wz x?1̀HǦ+#*}0N[Wn%j3Wak./jz pd{ Y%"fѷBN"uԹҽ՜,=</ |f]F\.5@L c?ދ60 ub6C5@$3Q Eü hs2yb̀cr+2'Yz aָ1>?qY80\yy p*dLK' qZp)"H9L_o>RLAӜ8ľ84gzhg8GM#HJ]X{ !8@\!91 xf/*2f0qKFAHC5+s67`uY: \LH70d !}Y :UY# S}F:OUJaP&'v ~~f p}t?Cak,dfWکړ^o(UD<ŹZ=r\?*\CL)싑qJ5D$rzb jVbV%buVURbKTxA੢z맳$DxR5=+RG mݥt s|kuV%a3>J}՜dB>{,߰g22Iں Ϟ??s9(~rޥņ`V\HQ-f~S> ܺ!->((YLUe2[a,>d8eG=C.lS-[?F̔pbjxvdSS%;UgU^^G =Gf)mO1"|10GI3vW{lVk\9ۣ`d^$:j5J/`h7|[7O=qsqA-AXЦշZ23n#Pq RiI861aFõmz|q6Tš7 N]i͙Id*3|K2"b(222Y &nאd.=&j8g q␛= Md((@ hj~_!&P,:~MW],ϟu<+ wX&i2?{M2LW4kY`V1׾i>6~ݩVI_]F5%E(C'tĢ=ViY)e Z/g)7t$P]mᗎT` K2EL>lu{OCCz;K}boE pnԗkiO)r4xL=ҢuV\L& pA7_{)yHO7:U5t:Cb8?d?f0n k 餫RY T!S6MJ۠v` iey7Q -zyTfvN'( Q`d?)\Q[4 P'q"M& rY#[FD!ÎI>_S$WeŒޗ/_l~~!o޾HHCDʹ&XdtPub۞n>ʐx} Nޡ <~7KjOyq I-oqie\Xⵕ4C5(8 %}C ,˘d򥋗yÄ.Ynr#LĽ2Z> ~@Y@\0-NK? |![Y~O{p`_g ܯd@MXĄqڧNˬLcK%]JZ-" tjHhq&PYJ_[+pmٯL>I-fe>yMXfp"_qm ƴJKJ!rߍNyͲOsUslJMbdr3 6$\"_%H sa#Hd#ӎ-۲HJ&{*:i|lPʩz k^kCQ.;} qBnI%[U$]̣_Vy IbD9jxB< C^!dy)=u5Nv:B'0J-*`H4{wHhTћnE>^,ҥ`5gv̀E|Ә1&I+2xk5Zho=$\52q: wQ@[;2L:ŏgth@ֳZhHVԉ[aDVT'B f/9D 0MRMHصDR1y&`a*R޳7^+͓{xU| `O#~^N={Ú{1,y K`H-'Xr""@_ܾ~cs$@ G`_o O77_6~q̄J*RXHiPD!pv6i=`1ILT]3 ǟ%iL$CYXx|wu~`1p-j"1c>{븦ln4` )M[$ny1kH1;j;?F ̈5;!پ`a?S춻$C!14F& Ux\8gf;=8^|Y1p8loQݗ_:uqGyvߓMZUv;͛6bHSSKVSe#ruΞ^^٥t@4ƐYi.m:zp]BR 2gB ԈZ޹ygΛooL*_?ٜ8tէ|ܸ{csw ~a̷'HNP G{CYiǀ"\OQi\&3x?Sش2Ṟ$`T%Xo2lMo<9ze1WZ{riCt`D8ϻ?vycQIP%>)LJ@kbq[bm< i|$D2LAm֜Ptav`̭JsL UM@F;;l.{ kŮퟗs_Dl񇮝\`@s%=b 'i&RjĴ /|l<  Xޘ={fp㆛s`d܇w޵|S>?J/nOgj3_xmo>;;^u #@2#=23G n"8u$6fl3E2I3p/A^Z?δm"oXzM8o3PRKx<s2eeK'^#?[TG @W\|6?6gˆ;#` }~vT^N9&wpI)$,IJߒރ9N?׿|y?Q-:!bTTbF=Ӯq#SmȤ+¤Ty]7\f94 fnmS%v{&u^i>BhRx9v ;iSͨhAᜥT|~Y۱7BO;-b49gsaʘG1cFĝ$H*p21߶t8$0~9V$,~ F0z^~/t(aNpK9ܧ4`-E;0_i}Wm_y՘ùHy t6 π?アyW?g/_t>`i]1/´Ld5&7v4y#<持71367M!2WS<<&"cS&0Zґ`6afCԧ^J#cj>sƞ>[4DaXNFգ3յD5ӴpaLY\v߮G(s 9i ÄXWiehhr={zgNb]O^DF_\͇XxuݲҋY~n=7< 뱾_;ʡlz(7l{!X^VAzZ xyq 0kt:5ً)`"bi,HL"-jg.jaq[{6sY.C[ kvE%6gt01?gc{U&N1FNkoN{w )g|Ggi>^r!15x0,5|ͼ6gO7d^}”7l-dP lPYlפrgp?&D NBGo>̓y~ƌ/rhǍG9Q#|}l} {&l]%Lϡqf_Q8?6HX.ªŠ\\7[> ,kS!ўh Dȶk ۪s8$ݾqC.@N|pM|򬏵MrelN*i}`L3Gςn[OEKT؁ݏ-BY"% ^z0u;6ǎT/qY8$d1!7zՅ\*0>hч;<ϰ&7էTѐ[x<08^ٌ"AqHĞH!ehҊb⟿;b_t8WYvc&ֺ qkWDbRqT\Wp3nQ*$-A~o^e#C_ 79Eͳrf*W63Dv瘄ڱw1cK#yH:|5:LERky[%l]r^ ،9!؟2ڞU4ǜ7g@jH ?y^w^/||F"^\_sGTƚ+2?9F;4h ԉCp0lDHyTi]_ VR!KUWҍoES|b9xMWXl] Hت J1cY9Xb xF8õeiy!|1;G:&@3_{ZI>ϳ0vvh=*^ ! W+Bkշg z'N|tR!CϥAۤĐ{$C[7CbOmw|Y2X 8fƠlDHO 5\u|0ǭlJ^),ѹL!Kdz~YBqz' S8ekd{X+yB,`5 ڲgQg;4$// B8p)V꺺86Dr˾p|T[Lg *~nJ_#vxR\eZ9~ȝHk=#!zQti.u*yq"q6je)@(Wpj=4\y Ц0FC2] +x5j3x7$$RsT>yHo \z.sIx9 ¿1!cֱl+I~6ں<:iYXtv_[f0׃]-}7B?^wI_ f8{`pbi !`8a^]UujIlk^ϑhsmN88B F0߉T`b<)CPyI S* QI)Gs^`ݢdNB?x7skb>g D cL]ԛXDH^-;DЊC;_Ouɏ0hL$%uFоZ,DO Np$?9=5x<1zi0``>ƧR*"eq ]$ f:R>0%nTUtA=Ec'=^o>(iHFilJiyi d7F^1c秮/1@rE!7aq;KER-zJGt/~<+vl{O|X[ў% Fl#t.ZG 3z/N[9c[$8z2 /CʁP^t]^8#4Ԥ6L ZYtlTm2$MM&55MS|}edܻabGOkWӅK1 eR!q0+\ H[7!%fO&X;sP "q4B4/) *1V/NBTF J rPzI*` Vҧl#x>Ώ&I۷'si_(b gBȕ&ǿKé$kZpE8aW0w1Y 49/ݥ&6c)dɄ*$#|Uw~!V(g X0˭?gz,ڱ;=Ζ %UpB3 e!*!D #Tw\ܶ_B>0 ]lb<:lZlB0b"6%O%z1`&ƒJ/6oK/|/[ e.\j(t{jcKܺg `jgyq q!Rb牓$Syџ/X& J)GD/*ŗ_¢ ,>Q ϳKɋ|[ʄ%E._]81k]N l3'#f͑G b*+~x.H0LX2a #భKAHtp!5mm5!|q$1W^~qxb ~kp)m`8ǧ4 SP̈́ǞL&KnC$9t{NKϚY;vi/҅ &K0^G2Iy4CN?Jq\up&4;Uf498ilc>Cv Dg>LCޑJ1y"TZAW_D oATCIHPl,1`XDc"߸HRH(8Y&Ѓ G$[PŗFcL:|N,@x;SI.x7]2_J?):v L_@vs=n \(B׻~" ]_xlӾ7i`W^ L1wr5w7ovLXهYq*{b}ƂQ &hibĕ70õAp;Fyz8.&j=zgbmw\grIKSk'׳A7)[ǡE@YLbLh Aװw'غ?{pO*BJxT._(y>T57AL^m*Sb ]v^\/q%X}6ؽi5>ǣӗ/^ *h;C9>3KPhTjW|s!s=`ـ*Hr?XA5Udl~~sX;vtvy z#c a-rE"zFPX0G`'ɟ6y҆ȤP+z~|^MXHMl7&bJ/ N0|+owooݩ t2/hup |vNq [qŗcv#bѢuNI̼8Q'FXxZ{gz^>}ς|׎Ұ7 DMr[ !:O$9Rxz\]0DJSy G r!HD#1)ߓ$3ܻ7$m!rdM6;AX,i>a(Kǣ Ph$5ٵ0D|J6aBrN!&^ͷ"?}Sm}},$he$쉹( ɟ ug].RjI|("Rݣ_6+"E%ThG*<1 lDq?ɿ/AӲG`?eEfEs@1Y' "cz +/͌D^̹{D9<2҄֯o p>lQK~. h*-σ9S O/r*.re|,%+fgqTW^cP|z^>_@\ugǶw`m=om5jy1 ~<ęx& ȯry7C hn}i&V` o[fh1nft&0e&q`QO%Ԙ6c?fy j .>pnJ3=m1=֚_{K9u/-E ;h$0"Kνs-&OE3 <SѤmy ?h,/~kŦ=jsgl~/0_UXØ@DH!@M.e Chhf?NɓO."\ƼQC [M`iߝt~^;.G/g3_lkwzOҡV*k_!i2F*m y^<0x[W8P*ŏ!M5lEd:Ni+X\rھ$f@N:XŞgv~V[@?F ֺ{-.n Nj4Jm ؛HYh"@ZvuxWfBåPh<NEWJD"LbeC/cf#1!yj=T[qQ8vrPI/-؆̦kImfYz4I˞O=gmGy=Ԧړi(0&Bv#|vOOpϼ/OտnWEĀ4S]IgEZz9l91+3R'_ǃG65z*jf co{N#Y4z?[:|Y;1 M@: *iw N@Ĺ:'u4xwԙ)w%CD8Յk){}s0@7AQU}Sm%ysH^ֻd*﹓/{m!I+o}L=>/& T/dg2Hб WρhPw!bXhu|$ݝ0Tk'%pp2eX@Շoa  Of(Dc+r7k!zr >ܰ'By8GC!_zi_߼;T)CA u̲NۘB11at)֍D׾2"Ǔ^[^'A4 fiG&6iuuaF(G-MH/eXX4E0Iv\]بv|d!,FlÈCݿ>L!i7YN`Vlme1 BsDL.}ps=a&?b $ZfS !{~M 6> #, t <iU9O!K׍T Ybѣ6"AF8l5ҵT[K~<2gʫQ'$4,+ _??U~ƈ_x`ʑ:R7f4vV1ZZ0lp0PUqYITN[񵯽ymʿL䇟h5K<-Lǫ/73pJm( m<;S68 dyzm+ h&/l1ڼ q/M} => a&He_3$#eMd9sӜ-Y7ET'jgA X0]hØl4 ɁJB[Iyո8[0zoу}O=8D!>D?{֝RĨ[ $|x@"fƆ{XB(ީY=HbEImel pD0@,GpME@J@0WHl0 H=hr˞@3LV>{9)U9HV`B: >ؘki<`vB/^cw2tvohφ $(&%0/`bPOsM0] `l|_Gۍ?s Qٗ 4{O#iåiL[:ǼOx ,:)?M|8 L/4kí!DM\gmM" MB@z4BIBPϤqѽn Nߚ]zII7D1@ΑPV"gNF:@z?] f#cv #0*b|ن8B }{㹾HQLqYXE2%l6#@1kN(~Qhhqxңr UCSC Bq<>yKl1LRma|5&E}dѶ&ֵi13| Cfǣb^k_7(ni_i)I#R #h^s!YĿHtEREɑ\[,9WA1 fi5}[7P&jßpc=//OBݼ @9}1y9z&d@.5rBiߙ$jwa8m^!{c& &GN=iX!x:clw}-3ViWy)Q‹/3j4] Y)v}!$)l/_g$||jl8̃:~M #3#a\@̤9DEP]qJh!8Q98SH-q$ͭZ]0 EZT1s5 if~OZy8*Q46+A9ss,Ѕq`G?zcϢB… YEhb?k|aB[}Wη|g inji`tx}ǞOu@M1{Ƙ4 4rzzg:iPډ"2.-v,Ɂ ֲ`9Le慤6 L u8|ʄ eLTvTX[ xt{6iz(_»O.m隽6|G5$4^<|0NGImt~Y,G(6+x߸[2%Ipe$#Ĵ;X'aTןt .5٭n j90'&(4Ḋ I6Ќ؞ &j9lÉ-ࡽQ<,upZ3ʾ6nU":F߀[ jd)7fӄkhi ^=)cG(|E hN`[Q9{2>*iitit50єW ?>X˧m綏 1Om 5q޿=\WRt]8,0 #Sn3&5'߯ȥ~RI G1N,}.&q/XudVnzfWv0,$c |z :7/CD\ 1M$>Q]㹇qVBfa MoCHPb.FӏU|I8̡"a,=sL:DHW pkS͟8ծI'v/U2 "SssRkT\ҝ~JSi*lYD)Gfȼx2u璜 ?B:v(ɾ*؂H`q,yu"˜w(v|T6R^9%'aaq{z,]x[`WIUeDӸB865eȓ \g`39[.`cS[˳\|qP64j 㣑4^<0.y\v 5CW>> `o|1-&iMZ1Mm[nVx[k]9>kJ0҄&L.'}֣F>ۙisPh !% m/rq0RD7DTKN23}VrT:odrF`Bqo` و«STfzMt y:b_q;Ǩb eMϤEs[2˭Rmiaxr][&ryY1 ϥc(LCS@IXEKW*>x& s ,Lk_Yfo!1%_\$avI lSx66χ$").0 >M@cg{ZGKG AC>隝|g40V;BD@hm '6`䛊8DWA D)I:RkCcܫZDը$e&2P?pRv&p<Nz D~|! ?E1 5RV4f/hng.q4d%T/+$ 1<#ޔ^N!N߿98X8g8T+v%4^dӎ&1%A,WFV.Z}_)#>/u2#kXUǢ9JG ~"'+ rN:BH0o!of|? z.LfN\ q\i5'♘*8m  d3 zĕ^='z,r^_#-4#Q:LV?pky]:SLHW!\&g6 s/Nhg-Rϻ6vO `k}^[V64 f>wb=ŒP[Vo;Пn/t~ @;_y K80}PT౟l_!lB[F/ΛŮ_Mʼnٖ&:<1,B1#RdQ8ؓ7*u-֍4”!Tv&B=Yu[ A:ZCD '*_gϵRWӷa' QRvc!1E 7IoxLeå*~Ydc+Hٓ savFFd an.yg1tLPh} 0 98{ٜ}Lӳ!NĻ1EL|jt>Lrȑ"R؏휈86k!& \k$<mYtg?~/$)QK"ҋmbJY3&`i˙'Ug0&AaI/ q͇٫>zsςxX;fP A{^]0\ע d $rBNK4 r?;|BmQSž مryj%;SsA۳U!nؚ& n50 K=Z0ONc1(E`:V& ɤCD;bJP"$/FıC>GGjEo4R}$fKCmpqx'G={dpD%Q1R~\p8CQ$1tV!r:D'`VF`HLu:K8hs_87‡8gŗ^YRVRC&p~`;x{/˷m3xڙO:˽ݽuQ 7*tL0f2lP" q08k' }VVS%\\M][v$&8wqB1 x@x!x!ԏ9H6 1WU}ڒ.ۛ:=rN5.$9lp[S'=IC=gW"}0{=S'@$$648O7mċ3I ݓut[Wܽp|s"ػli/f& ߈ѲRڞ3>*SFJmL'87s7/c3c km6,~܇m#hϕDzI뗈ơLU\ii=)̑o7E!H/l}I~Ǘe:?ano 8\ql9ٶp2i}d*;Dťk`^Jv!='ƅ]dqr`*É5ڴG3pAUh OZ-ٚ1Ć%|w8&b]+bba ax5^kR?m$K׶˼W> R{N`9XHP3}`{mկf1.Dh>itx|Gw}k~)/?:&0c&Ae̓(LfN}BTqiz_ѕY2Y+sw2˅ $+Ţy.Mh.靷߭ͽo|󛛳1jvH1h.O  ?1>޻JWo?~~gvvbs>O%u& !8zoY*6.^<ɸzsD4qJc""i6L0Ʀ$uMҸvm$Q P٣~iȉ8,:u&zg]Q^Cmaj>\峳1?|P82U~BwٝT,î]:`c^1%vIę$1x-h{$C,UmK^R#h,7;Ms8*楗_xE39U7oo€ĴnÒ #n۞׿-mb3V)( @05;c|L̟ BwrdubpSmSWqs0L7H$u=3qeLB<:l2LW~[sς |~םGv\b_IpS{:ت]?*:v8A5_w1Y " BC̈ciD|NX 9%%.WO^#w6Rϻjs8oPH6!H:]6!PÐCOۘiqpug{MԌrY0RX1˅.:K۠J11 2#U9QAbƘ vxx;"iwǎUs' &1HQ>×I.C { 6zY_~=|^^gևZ:k6z]uqt `#IMZSp6Tg 2~KrlI3AI֬8`~MmF$ DW(IVVc2laE4Pr#!X׫[`PTR'ˏ߇aa Z TFS w& g-K3BRW5{x6Si%7!]|O;&gfw4KO 9!f[w!b&s+FiKrH}{Gri/A>&6X}ƒ: ySƋoH.}ty54xr~ D_SN f[_׹oibEo:Vz~6e?{/"KwLέv^saRNj{ ]ڊ4Z̆ǿr!&9n?YL,!!TNrOR~%d1Q)PվO:)lkǦ>i9҄9x9t} r=JԀ(1[WaOJL̓b$ƣPZi, N鷍?L4֡h#u-` oz^"|4YO*՞#ɇFE{"9,Lk=gGL"ʉ%̉F~*(iLqEU0ܽ-j~2>bB֣ϖN3qfϘ:X/F߼ o>? {?;[ q}Up#esy&[\!B.QyR噴ZL9ַ`G;Iw_  靯ZY{Zq|a)q,zAT d 2I@&Ф='"~efAKܹsum!2&A" L+&" vl#]-o@IDAT MŦr3{nDsK3_C]CsZvydƴ`HN㩯qIӬTM[ }Nn}Sm-0ɚ _8qMMC&LH/80Y&k$1?>xS6V_eٵ>;\gt(ϱvn{ww|L"Y!/;&x6V{G*!Jow I>d;}(zIH5d3%¹bg0n̥Ns$BX"DKA$GQ+]⤯c>pD޻R!mN6o6DH`C;x$qڳŽ?s*y"b#8Ӗ0=N^>2欦I >/k|l#&w&1X'bB<~5"ߖ㗐HvZa/Ȏ&"LZWv 2.k許q{UW=O||p>ПIY['#h໚HN0) WvH$pH^[xS?/ivg w/n^$d![H#3K9߂XxĂx3yedAjx%XDŽ87ݷUYB&wLbI6dx!1PIl=ߜW5|D zO⤝PhIrvVa^?d naqj(Ϥ~s&k!*ԝ7/(GڹSKzg[ce 7~P߀gan}taPo޹1јǼ:MDQ698D+8jk'\# wh`cG;6 |c(0(K_XHGkrF Y:{ v@R&s ymn۾@D=;;}`3$9/ wfA75nN!"'@.` F oKC`0ҒSgRM6Ubs mRM0dP̤޳jл!&wA ?dqD ^TTshABBO*s>p ms*!3n01Ƶ7ZQ/=Ƀ̓{;,{n xB ! o h^$}RLDv2HT1 ",Ux1*OisS!.B[%xO'iyØQד0B9CcU1 CFͫ,mM 5S>u8"5:$"ӣ; (׾yg^߼K3?| d.DV#FJgN4e;;s`(z￿W^nu[R4%w 4쨑MxCZ GFē}bBcTG1]$h 6TKЄ_IDhݿjܽFW{| ZO 6/4wC bR|) ){"Bv+fic,WDD0#'ڠ`9Ū9 I?*/+QݒX"5CPX$Ю( @^ 7LjϤ.l' aR$5퉶5Q u&DPExwS:Kg#&5b 7 SV5~ 1R.gP3dK%R}>ܝLP?G{rNRa q,Leq2eK|,­_wF IWںuNJzɝ z]ͫ|$EE& ф&4 I$JS0 Hotͅ;fSf*?=.Fa(No΁W d$jÛ B@,%#q( AkWÙH!θ?HKR]@ xr ԕֱ+"2lT#CE;8pYI#j lIϕNQ:݂yg0&đM'M<8to !]0jE8s| ޢs1~̟gt le b"_)ii(" WH(@k s֙b X!6/ҟ·=kdz=v$[̑P>/n/)ٺ9lU8:bEICt!ux&='YlY !1 I460fk l?nB,yE{֟>!{Ru??@H0kUi~k>'8No BǶͅk浆r,%dH0&@LJx&W?X8L;Heή弒KZ*a SYF݈^ B"*J2U }Mu"$eh@p]lyhUQNLJ~cW%^mD?Tjs0!xi{{=xy`RLDh=RΨ+ |d`?[pU0(p m߆iGᚇ99G>mp+dfTKJS/uj̹W릻%"lᤆI9p0M0}h#d_:6|0qoO*`BH{$c9B:BDYu-:оT/V@zFHᆈbňw_UA _Rj\K[ ^G՝Vc=u$:ۢw)5R~a2*`bZ`O ?4! 8&uxʵ+hパ3;̩ƨ͡8OjMNDNp`p`Q?bW]NshMi3у=wfG̲mÆI?iaͣ4OGo<`p냛wvҊ@}n~B8wv{}o$뾻R )J 3q e ԢB%z 3jM>Wfܫ9f (IZ \/½0IA֣{,D^pDwR3t?Ʀјa$l P`//pA Ө1.kN4Y~^rhKʏ͵ݏx̃5&s"*~UDx=OE4 k<ca9cs|O8Z]1eTLjy0ltr/LcYGXupYK89P,:~B o9l'ã!G&CAV)@ ί&z/8˧/w/ȧܮkZvMF粤ZK:؄Te&_?D"]Iߙ𮅘@!b҈߯TKUSٲ0nׯ\@7FDHn5= Yb&Soz$ 8q$=G; 8ز|B.8D㐪K?;$I-3OSK;^s:)L ko42S˅ļ`ӑS?P<ʰ76ۏH\`|rC>x؜%4a~eN0Q+|_ y`vňBE7s\wŪ] R32~>0aޘ[iEOIg*|lovέp7s;-xJ/:wz631vtEG |a6:p17[88"۷k"HNG;CžnzpCdhZ<܇Tu7ш17 b&QC!Te@a#qvl|S&eޏSkh0-d '-H5OH1+0Rޞ~,}GoYMmgZd| /aA(C}gO9:$ZTOXHhs #Ld"U(,"3kL簡(_},V 'HjI@wc_l}섌`e;`))>\x='? i|/|^0 ;臵yz6{ 睐*xTHlS iF:!Nl ]y!QٖoN&6ĉՌK}T!pBp,@u2aLzfe5 JXIc>$tb [Ӓd38pKN>]-`˳%#X1N̾1񙐾%ʃ =sHn Ώd' 9^[Sϧ#KQ,>>yf q ?P9>;io@PFÇ>4 ^8V& L[^h(Pv ha0S?QyUVuBAi'G:} e֠CvӂrO=.8kL:~v1-3>lRChwI~98Ӻ/%cB3h,4.[I~F1.v\}7B7kSGI3@=o*+ ., ^jo{yDkq`ŠИ:n1vOZF: 21S@"bO*XkB V9gү:0hJ5eU7i${Y0i2Tld;yj9@h t2kL|g- Ɛ)0E|W5uwHa <`_y?ƣmF9wu BPVNct(韺cIVڈBRaiӔ;M)~ocHopejK@TrFp>O>Yq4 *)υ{:3UؐЮUL% J6#-o.{(4v>P# 3#=<b}:uLt_ϖPDW3r\/sXUNhlcF`&"R"Y|@ ?:jɑy5H.HG\Ʊԏ,oLgk#cLnj2w xb7Kk'9 Ys_~'^~mbO8^kg>AX'2ݧU Y,ܝ!O|OCy"gSĵ QMyV=`Do;Zv[kU0: XIt_ٛzhk_rރa8Ֆ2CVGΘh ٔVߴnI?'^Z6oN)Nԧa;we+@Ӽok9:;tו(KC TETh1#>F! HoFU?Ld$:#ve[ȉTZ%H7'nzhw1<}t:qۮ@zRt,&*J*/}>Zŀfc=sC%ICZꄙaM "6Z]0"Ĩ@[5URw}é%@Fhj s&HHfFk 8Ѣ.\1s2PvJHpљ| 31('2bHE>{._7cؚ/LK7Yõ3lb1'51c*zh9>e̿RfP6oC^xOwYsd<J[` :c3vX]0.32K&1m[lqH^F%\ZQfs!sY; :Wb`ǛjeCy}`Bu ضk@VX5)m{Kks6ey,Ɨx!S $wnX@'[ gH&} 1b-&D}mM !>g [yׄTdۖ|ᔑb/drԜ-YK;v0@_N6kIf^g[ᨎ!~q6"U!F)*mRssʥvT.i6^Z 0~®3V?VkJ6FllȯEdDtP)m@?'1:CBj3VP{-FM)#Zx]i N|~9&R-h|rJ_}>k0~##؄25&鼅OWo?l7ͅ+ӕTa`5-C~>Q>}Cx:/iCV_ t07?RkgIuܻYQ!isޞ齐^r RsWSWɗi=!mO0&r_|LIx=W./T-Ö洑RQs̫oٖ 8VFJwH(ShJ }"'xGy>or_ǘbT}S_Cϻ^ͺ$oi-VFz A&iN{էk1NF gYTC1+MvO;k LB[Ic {kdʥNݘv6Æ`aq*+ R휲%i6m&c(K Cw U~ǠM L>#9&`S?Q/;ͻ*&>~OY( uE; AXozsKoθqpQU'X~ =)߲?_V6'hu%P5c?, %>E-lXZJ!2DkB@ KrFsd2<<'9o 4+CX v1Fc$`>QvFShr?䭁 Dzl ڹzL PWbdz@ M c ;]@rL@c %jA݈6LgMiH\=hmZ`(G$` tMOG~ h+gR.,]\$n3#B7l@8-B=MN~x< 7+\כf a+򓸊a]js4wäC|_]/~1MGRp~'p!iAbZ!B1OJ n H_~*ޥs)ZkH3`)%E@欝jA "&+M:``b ˱ |My9v C8)DK`76frqq[Rn`U(G"Yj0 \ S۝m5$u-b"e\;>pss)6 us"I޸RQr_6F)!P,miG̦+`OʨϊI6%tYdObʐv$l? $-Xj|(y -"y ;Rl֑ zaowؘ0]w3 hvCa퍡SO>UU<`IYBG q @ej1FiXa?C m ZC) +δŸvqގ,+ujm_^It5RI`cPt!2a3rfװ;R*/P_1gmr:/*slT^B8%, {d>30 *)s u{Hcj;W0jZ LYǼ>ۃ3̚Ħivd1cӹ,} ^4e5 CHbfVK=V^;8Y?'$ BjCS/RL/WZGeyu(h'3;ڏģy/*ū& zůa < )ʦ Z@_ܷf{H _./#1TPuu} o}._nBws1:nRZO'"͗w^Z#-3-KtHƿ LOD[_$yV!b3fQIbSy ð Z|P}Uܛ|$kQG{:9{s\zЂ JC+qϾFVCߢVLV:\f;X8XƸRLf*F}(C[;1(i#tlbY Z`ie`;)3McFe0gz 4Zy6~OU伟YN]&}.z<$+ijEi߬0LP}+_^5?(:^LqиH2*. nUOSLU^#`FsZ ~%ڀ *׎<'?]e|RzeI ҉6϶`v B5D~xAƂm@ 2MR7qV#JZc픎Ɂ|VIT,4OxI` ʍ[ߕI4柵S1[u4 ?#F3ȦnRoX煛b_HK+OSC$2;K{} Ùh:{WHiZH0ǏIȱaLj˳8b FL#͖ Jݗ鸀VTe2slI1ocMpx  ])QFc`y_ 3s_=4&0SǯWxCR!w®; hָ/iC"Rn\xFlv3v`-EϤoҙd5LI0B 6EBfH3On9sKw}+K);hE 3 O@?]61!LԌ2ZVZXsi=*1tH RBaeٴ: εHfnO"z` Ð`^'i_icAV~޽%: x5g܉,%М ȻII_H^qCl {!ЀL=c?T~3V/7mwi~k¥o^Fb-1n<ǔһm QU7^>J"\$dڈ /)Ae E^vi8-Yr&4\K؀SR#)͠Kd(|H3J!`-wS 'f!f3,\fd4$^: C@ի ^@21^ˀA϶i{-Ea̟[âֳ?FF>p`Ij2DUKrX{h,ܩǎgxp˻I?=ga<^vl0hU_=u!G=hrơ]IH 6y)ϣ)+?W?Oި3>ZYa60 F1(/1XWT/QE5hF9P>k}.8}"ؒ,݄7%e/ՔE9:z1E4F`cQh`j6ٷh_b(WU80ޕwW#MۓyW$C6JleqwtͮZV}M*+~K(oc!pyh60ZYϳ* Ek;z}d M9m,{/0d9xiNcD0oj練}qpT,v_ڟק%8ZOTj7UWy~=/'Dž>}5urG q7,\e+!k a Ham~5 2#kC`%pMYхs-AVmʘ#Gc4s;D?0M8mc1 dADRY_ ECc9ތa~ )bf4ċJHbBYsMNPZB+s02C@0 0Z·CHpMퟲ h/ ~ժG#+SqL%ɵߩҤ3^JZ#mJNpqf1 P_0I fam -l:ud*3&4 Mo"Wт=Z6˔Cl34C}:c)]l;ɭ*,NdyVb1eNP. CHQ>rtoƿ^3po{>I s Y#pG?[eߋlVbQ;?x5Rƭ1]̪?8[e3%[V|!Dzvg)=۲ {QcG+U<JNR@7R41Ѻک%%@=@jrdY5}7&@B:$^_Ko8gVВ,׽$,:0qysK 1Zk"|1>]fgvPvp.yP=[LkfJ0qU)6ߔx:SmG~v;^~Tx':%ܘ~O^4<㛋Yy. &0c9'S.x%~B-ƅw{}7{Z- IDATTTX\uhI+Y~-S=^YC,Χk^Vn ` m2*dvᅲ:7ϸ}wjW${}|1aH_km@rhy<2ujac_hDE B5/{1M@֯d*OkS&v_06׀ME:?ihj6xkGyޚ?d Ua=w|u&{SfbO @rcCgO.~o3#>qVxgl4<ҁګ4dSC #vB{ҭj'pNL4}k" 2#dMsf_Z*-[/)1k Yۉ+׾,Ûq/Sjj}g*zbBc+MC(! qʦ9C[ rM6À2B ޴/#5z8MD\L;a{}ONv}S'H8ќSXKŗU!yT04ܣJAg݂b'+xZPq!L5:#f`P{CMgj [ 4` |!wN~//|wIlE@ 5NS`8ATIeTӀMa$,VvE5]f!2dʽr9SfRȼ}OI2Z9lU ,;@ߞ%njvV 2y@5)+ɞJf6W`D򲵙m`W'a"à8O~+!ܘcwFzRhM5ToBwN[a-cu{:huY$/Rw)SI}Z&ESʼwL:W⑯|%O-& =&~_f@>\&&{P/<KeY1fF |0!ܧhYZWi5x ( ЊiDU`wN6ԌA0ֽA!"/elY C8B,EN8?pfȁ.|PS,QUboz0ư3¯B4C} sPiξB AII3:#}c?PKUSiȓ݂04 FJpGx|_aq]ByZPbvK]iyԓ5+?m-W㹔$o?O3s}4/4w y-<"Be 4C??_&`30P[()F50<ȓE@~3ftsƜHc,.%/߭;3 X3N~9[/Sf!J'*+YÅ- Pߖ%ƀ ,= bLƐ|GC CL`H L󞣲ƐlIJ^ ɐ2&EҀL th_CflͦV>{LuBO m$ݻ`$j)r1/{F cK_ # 0ʋ@[=ǣoTHE32]{Bol$U)iwCf}5&`8/|f`6Pk|O#EJk@BRG 8IM9zwÑǞ?VD ("` )la|;5YhKr/zmsLJh^IǀNQOIfG=K: r-coL#!]~]Q7vl2ʻk$1DquZHW`63`l" gr¨0+&- .+c{gY"y3}48DU_վpCz)ufh<auvb1q";apH ˹@@?K?7<g$T%*zS @ Ynf!@#' /'?vw>OK a[:< xeO^\ql'6?Z[b2< FqiXaA|h=o[|8yVbYonGLSTUvUN i1cgň7`d3<%-W&x5QW{qLe1n=ds!ߴflYΤ a<(а`Gi%=m%J/ F-MqJoJ^DS秌ꧤskO~QO{:?ހC6<+/>QUUnEv(}iLgFYÀ`|>e:@ݐ! #,uv|$>&(!w ؔ>E@%8-!m<ܟ5%^ެib@1?إl,2*/> w&L Ҹ& P}ģ6>mdȐF%Q[1Lֻs?ջll6+d&3'O` Zo8zWc 1?ac: {\Mf!J>[뉛۞g ]h#?3FSK~}3z54CI\٩N2=慀wut=ٱ`k\4@IeoP66PbЋc Qk;&9 ι;~ׯhM#-hH~.eY# ouCo |\ s?ݜPLh@3 ~mx0q?_no~9Z Om.=y%C4pr!5Kg X;F!K“;~s-՞93*8M:CmG1!0Ma -4+v,#2uI-'%./?=H:Q݃<4mAP^TYmעԍѵ0"22l0^c-hJ3& U=&/S@<Py(O1WL*!sR_AXt=迉:^`fBoE0\C[Tٔo `f>fzL H1 v+R"SN%# Mk3041c 8ʓkXMZ+}y8I\=ɟv@ 7(S17ҳh^PKrI˘+ElT~co5ß2\D#CHf(Ps3є0>H6 ([HѶ jAPTs~i$`pB1̉S{7.r?OXpo3*O@̋7;eM@Z3g P"_8ʗdLwt![IEZXF¤Y@ƗMfyGe Ub(a#ƷK:"h@\# ~"fMR~ϕMWbݷ>g 1;4KTo&Ya ,5~0(OG׹~cu~:T>~ך/㨣GGYWUf~}ط]H;rhZ<)~Y_~||h:|0@aML@R[v&0Cޏ(r]nK$g>Xk8&@u!@#dSL E$ 0 u0 XW[ mg*Ç\6;A($=P5vLl`b7+@7Vjtmj1 ?rK*= khBBec)*WBIq+rܷ4dOKPo;LTmhc%,,Чp@{4FROɽ]LR';}dˠow}<+5r a?霁we&Y(4 `fζB<#4g?[6Kӕ;r>0GX%P96U|$5!T 1>y.1u 33l1Sp+Tsߜ2N)e1D aoW;c^HWD){@e)LWǒ_ q5C@R3$&;g Kgx=S0K Fk0;))WkT"yU~# ч|? |zb| |jTa^?[Rvx L1@3f3Z/oCsEENWƫ3i: ֹ[HY`d2l5XX{CYg#]# ea{K҇)PH i&axyZ`r.RTg -S/Eg$Q3S<hݮ@'>ώ帰t)/NbI=^JvL`f=Gs'=Q+2w) P4ˮGJL@k6CCH?F@n[O}*{ [fިheLƀ_[h_j8wkkH!mB]hɬ_ N b:ed>i )m2 >?c|5҆(4R1ZB,?p2cTNV7/EN\L dN~i0myXC:x|0^qlsi#>JV 16uƎ~8[.h'DB!d֠ހ﴾J,.lhŅzs3@x 7h-C sL@c}!Fg֟\>q4IN13`t"H<z3sac+مFEH5uW ƒc&eZx#Go LQF0!wI՞_}Ҙs'r&D}aG|Wkkh-h62j >1X}-i^Rp!hOeCCod:4 z! _?o෴P~:̷[dzHЌgL;T#'op?`ߗ2*u M9!1 < (CX䞡rc㐏11Cf9mnck۲gZK2F TeH~VQ ע$IĹz>q&ӈ OO;v14@Q )y+RR*{JHyr-Ʃ跟}M2۹42 ߿ |~C: Ծ@ ,3׳&zS(ѬEC H'}ψ2(Eu舁pAB;(iVZ-G2MECu{j%^N2= bTsқ@|ش&~j؅HB3 YaHYza$9ea/$c0oQv1= j+ HB|e7e%wiÐ/dAc)H=oRϮ7b˿nߠP!+s^[ bUpu\8f8dסܒUob!-'ڡYhq*xTnq_cJ ^ +X] j/@,9k_ !YGB ՚s!4  ޤ_96Hjܟx$n^k'Q$!TuNk%X1*g߶[y3oZ.Šjѣѯ _b͠u_؀ﰙ@JX \C-u@hx 9\Az>`/")C1GrbAI r-%NHp I [B#Jz4VϽ V)]3)N-i&ud+) ȞɿO"Z@ w3 }HM0&}L OUXBh'Cȋ0ƻ'Yp>w>/z ~2*@%H\ߵxeFL@3B HT M;` vb#HGsHg 4GP2F6à, ZAQ3E$:%' H1u{EgW7Z i/4> ǒٗ7Y]/қ={3_{47oI y@?/liqxD+.,6ۻҽgu!b5#Br;A?"@v}AJ`:!xO=h%p5NI}2k=VFfj>;ze$u;{Ff0=;."L<-` o_ϡx_o*{?~4rmKW4 6E/"5;l! A.69mU\>ksƹ?31r}W{V垷e[1c[Hh,.: e$La-'Zr{S0;*/ڒHdXt 2W{<@w\uŅ OrӸkW<PI.jЏi=A@(m@zgw&y߮;HƇďG~, 9«s I`ၱ<&%텽{>GO8aLTovi{9o@P:p\*a L~v˒U@z߀_uO癬;|/tHDI?`p fLpXD9.LJ " ҖA>_{.&{/JEy_1_*ٟ8`fF} o̜yv.Op,A>ݑzNxo0;v٣Em 4-kryG pb۶W3y׉dB+ ~9*}.e@Π[_ Vyo}auDžr8Rvníy1k` 封ـ||opv/ܲ7 f$0 #}aYeF[M5l }W~iysg#-9ӹ`>3Yyz ܾp3: Ҏ s\^ם%[/~~}4U@ |p gwC/_w>k!!b{ki]GZo 9\bӅsFx|o)}wVN *fWaMUH7X6kwiv`\jmP 4_U^N[_2qsu8$-.G `{λCծӅ\1f t\}oם6`9˰vyP] [b CF` wWϿu\4!םahu6 wߪLzv9m.Wv\JT/`=r,H7LF?-S8Ai}}o=UiE;MÌpu@9sg۪~績rH]{6+chmFN׫~[uOu8?Ua^(gg\&mrܽsZ_/:>_s}N_phE6X4ŕ#d 㝶*}-qyʳӺ ~V׀r?q&o}UaoxV-m6b[`Yc_/ U^|{^V7AUNR8}Wzv{q{-$oJ$מA%Pڵv|Ϝw͠@up~[^U7FЙ-|i%g;.{FʵA ܜ*>] x^ގ/_4[fo~[d U ؎mWȾ ntsgvrrg}^.wo?~]kdi^)|<:mZzpyIooiǫrfe }a?|v}OVegosr^}o~=r\&zWa2X|t|OlUZ΀:t[sڪ;m)nh6{m U@ Q} U~ִE~hnjs_ˮ)Jo^wxy`z k.52U]fpU`zmj['6u⺔;`o{9y4IENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/gear.png0000664000175000017500000001264612641367670032540 0ustar jaakkojaakkoPNG  IHDR@@iq AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  iTXtXML:com.adobe.xmp 1 5 72 1 72 64 1 64 2013-08-22T10:08:26 Pixelmator 2.2 A#IDATx]UUqL0 K">ߤz C")R z "z( Ɉ|%̲2h,4etlLs/=}^{{ܚ9{_묻syY׵DP;'x DCFl9Q5 G5% , ;&]':J"].j,ARg.]֙+#""¶+nybpSfZu+ XKDfAN6>Q{;Y2N"π ./R1 p Ċڨ8ĕ|ثl#Z['{P.0R'.qxD?:`6:@sпʛoK19Fa;dkIohK2N/hMbnq͒!~ eIҞQ>vmZpaN:B^dx\A*Ѱ8EвUT@f=elA8bw. OCU`E qZN bs5@,w^Q2Л dZwHcn,AC^vBA1*ֲ--'Fz(I/nxxB7Fj,TG9ਫʷ.7X[Bѳ=T>Ɍb '4r+]vi2F%s2g=R R~ -C=xNcDN0ޡ(B/-\6QYXSdqo/Rnd7SGpQ^YLIhG֎*BB6#tfoi6E`[iI}J q'Z3E GUzC .ƴ iV2ed+(Nqhv*Mf2l>X9;p<r{U x}u ]qdЫVhc Slv-V>pGdIruY b*'_lꔀ!K0q(DԇP\L@4PiD1|rlRerC@Co11)`y M)zyJ/azvh|*ߣ4fNQTVÎ;Z=K]1qAxueBs?kc)`=_|{a}6;ui5= 0  ul5{էB^!;X531csqf ehiʼn^S[ګ|vu&@SUbetՙ-s8vVPYgtp -rԛ`9cF&*7)+SMRQCkI>:TAIENDB`././@LongLink0000644000000000000000000000015100000000000011600 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/background.jpgdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/background0000664000175000017500000025736412641367670033166 0ustar jaakkojaakkoJFIFHH XICC_PROFILE HLinomntrRGB XYZ  1acspMSFTIEC sRGB-HP cprtP3desclwtptbkptrXYZgXYZ,bXYZ@dmndTpdmddvuedLview$lumimeas $tech0 rTRC< gTRC< bTRC< textCopyright (c) 1998 Hewlett-Packard CompanydescsRGB IEC61966-2.1sRGB IEC61966-2.1XYZ QXYZ XYZ o8XYZ bXYZ $descIEC http://www.iec.chIEC http://www.iec.chdesc.IEC 61966-2.1 Default RGB colour space - sRGB.IEC 61966-2.1 Default RGB colour space - sRGBdesc,Reference Viewing Condition in IEC61966-2.1,Reference Viewing Condition in IEC61966-2.1view_. \XYZ L VPWmeassig CRT curv #(-27;@EJOTY^chmrw| %+28>ELRY`gnu| &/8AKT]gqz !-8COZfr~ -;HUcq~ +:IXgw'7HYj{+=Oat 2FZn  % : O d y  ' = T j " 9 Q i  * C \ u & @ Z t .Id %A^z &Ca~1Om&Ed#Cc'Ij4Vx&IlAe@e Ek*Qw;c*R{Gp@j>i  A l !!H!u!!!"'"U"""# #8#f###$$M$|$$% %8%h%%%&'&W&&&''I'z''( (?(q(())8)k))**5*h**++6+i++,,9,n,,- -A-v--..L.../$/Z///050l0011J1112*2c223 3F3334+4e4455M555676r667$7`7788P8899B999:6:t::;-;k;;<' >`>>?!?a??@#@d@@A)AjAAB0BrBBC:C}CDDGDDEEUEEF"FgFFG5G{GHHKHHIIcIIJ7J}JK KSKKL*LrLMMJMMN%NnNOOIOOP'PqPQQPQQR1R|RSS_SSTBTTU(UuUVV\VVWDWWX/X}XYYiYZZVZZ[E[[\5\\]']x]^^l^__a_``W``aOaabIbbcCccd@dde=eef=ffg=ggh?hhiCiijHjjkOkklWlmm`mnnknooxop+ppq:qqrKrss]sttptu(uuv>vvwVwxxnxy*yyzFz{{c{|!||}A}~~b~#G k͂0WGrׇ;iΉ3dʋ0cʍ1fΏ6n֑?zM _ɖ4 uL$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)KmExifMM*bj(1r2iHHPixelmator 3.12014:03:10 20:03:02http://ns.adobe.com/xap/1.0/ C    C" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?Kl"nUDQE\sN}iIm.%ۃH<au-^Ffsd~{PeuRf+ȼc=W ?qX6hqFX2IR: ߞϵ*'[z?$r g Χ_Ÿh ߻==NXҨp׾R Fz67Q"0R%;8>5tR#?f@$uWORMm^ԖWG9͜u띳{&%`IX8ϗ׀O])Ki̱K$Q;K eg mOZѵOw+g=쬢 VI .$*A$@MĤ}*LmדӐ8ۓLm?"oj͑鞕k?c#) ~q:cm1Z)7f-8W}q)GqkX[S+;۩9Vl%k4pH 88jx2T "r}q:zI%7)ܲBIscO;ȷqyqF|,N9~U]JLe*aO'$6s Ls^k2Py KH @5{#4kP7Tӏֺ?J1Bxdap=h8o{pL*2ɭ'RG/rC{hd@6!+=G\Sҭ\ <́1njg 5uM2`0qɚ $ ,CH ۧ>^ȚU`ުlV4y ~ր:bp+1*@=2?o Ha%0#SԜUx!0˼๏ivnHhډUUkHǐR(7QĨ+Vu&t9!{ʺm!\pIӊO=#^c%ڀ2 .?+T1q1$:'n/UneFJ68'#?UWk[9T?s֒#)E.gc_}+;W { ⻑c?F$AVb8BF0Ùq>WPxm^O{~]&Э$LOPlz;g4bSO)<%#!%I'?t\ZI [j}{usvnVқ>Ra*IAֺ(tВ-<ՐH0rz掙iȪcJiS1u@Z1or_k ^|jy8#^ïMѦv!ewh4u+XfdN0_.~MZwf;}FY-R1$V韼q P fq_r?S?*ԂE+%No9'_PL7A 𓓏ҵu Zr"#x~-aNi(^YADPG$vd\YeܲG 8@'uy" )О>-%%'{t#oZHdlqFq~01"@#84D2"ל+8/l ȴ5 1oÒzZm4s3m8qq=-r|g5akf.U$Q'#\[j>SL,b #@'МqMUkB7Goo @g 4귺ĶAo@#8nsɬ[uV\v-:5H,>'+]ONy$޽Z&e`&OC`=QZW| ku.5 8٥FMeWv%+&‰׾IP[٬@Ihb9%;ZWMgx[K"سϙx =y<YV Poj2[+!nF[>  8G`K'b>è«q;FΉ, rR8iek^EF |޸[b+HV,sӧsս6h~q:{gjX IҪ `Bg9'TJm*Aݝ5`k%@vu,D'\onϠR40^2ݝgt=AQ`G1=~(Hm\>QX,y=k%,̗p?D,{g?5Oq~$r`Ɯd5{8kGKiվTdFHNv:b}@+˹p=9:}kmPl='s`Khrˏ!"cߚӆUeN@ib +u',xǷrwrKS 2hv,JЖ,8O_Zdܴ*:#Ngڲ.+Pӭs m[w fŸp$c#L5m1{܌z`~jդ{CE>ȉv:c>sGsegmEy{c$pmQ7Ԯ0w@gQvCc9$۵-NZo+iUw -D̊f|SԁS R\eVe -眎 [ۉE&\ o<~%NS\-̛lh;/OҶ#  ;O_s p?%ٜ䐾j :bV]qzץ>HVch'QOt7w1 +Ic89tRYqOEwm}"l6^_ߗi0=Ny+[-CX"C3NvL~ 7V}?ϙ&<zҍa}oc/Vcވ2 >VH]7OsӓA&:lo0}6DX `\c$p o)(.e8r19$8> OZ@G$EmomMflG21@+X¿9?SǵV¯!PxZM#Dz[ 0Fy=)0tk?LGeĀZ#?7NIeI>W-}#G9!BF?}:VmJ خ1,{1q0o 3LGTEn۹ DZ$HRG# ӰE7\߼%FGc)\r}@ƛ"85 bpԞ=3ګ^''{2a'c.v!:+K'=rY(J~w=07+}in&63NIj p9(ͪ]lZ 9 vW]B8ϕnnq8$"(E|8$eǽsL@1CNzPGΌ&)@av\jxi`ӌsqyyuX]@͸qO8v:ߵ!??Z鯯R[*Gt$DӞ}CV8P x'Fe8S4"-CR_b[ul=qܖPy6=kx<O8DKXDb7&'${VP40 ۪w`qϧSnY9D4!$ /OivOO6(ܫ7&Ou=N٠n庚r[r2gyqZZVm{+Oӵ) |1಑9JFV#E p2 cgucl2F`PYqϧ[/\qokYH>nx/l&RYK y ;[ő} ʁ|n+ԧT++]2D C^xp&xegӢ .)3U98횆Vw,,#q9<97D*Gs-ᄐɂ"to-짖ic3OGOJ|7ao&H<]@q[ڔ`bۚYvm9ۚ6w)ki,ۈϐ1g:+N 6O S|ے$$14&e| GsKqڛbyٝ$8^N1qj$8qNz5DjW/{sL@_9=8iR4 |_Xs_8;:wqN洣Z,ѺIV[9^ܲnz'΀4էxZ)F?lHHf:8}jg68ɑU }J.%\̳DFyq}ElX6p1y?礗ķ"cR$cONEXa yɘzoc\SPm8<{&+i"y!l1>Ց oLCżQ$Vms$mߜn[4|iy tP,nbfwc|ħkm ~ຂ9?kdwty{0lv*(7ݴj7,$PWXd-ߪFn- =3%.El)etr ,Ok[M_)aؘؙ'}%&OLjM:g.7l[wQj6Y/ `c}8Qwp AH]uj:uճ, ]7E-cD#w'd'ҹdD}.O_gqwvW{,sW܈ 6׏$ڲ&ZOG\~U-_j 1 qd+JtEHW&s:sր!kC%}Fcu'yO~B8Ӟ? 2oi[[ IA$⢓Q)SnD;&7$\ $Ze&͌ B O?jόIJ `d'%پvaON8shIW>] y [%nS a"02Ԟ}jC߲"ʌ={f4Ied@Isٙ1 zt5F\Xt$VIr`Q|@w냌}V ;` i L@\IǮ˫H-m.HW=y$h$c9>Po C)+&-7 q;OZZmķ2\_-VQG+@HTP Nq#d ys *";O9=ֱo///XA,.$8OZ \dv}MHl/c4 tJVf٦kdynNzw8)$vp1wB&VH%*Ű8$J3E\IeG:.7DjN +paIS;", _i2.Ma'fNZ}[kXGo=eJRE21嶵7r]Tg >wr{qן=3-9\'9`29>e+KYY'b#nONWnI1?Ͽ>JgFF80A#@שfE㓜g_]2ϲx$XNB?{g\1D^G=;P=U%bHO$|%̒N dGH$~m"qis -$|l ?m :L(TU$m=s4ɵfhnBp@x|WlTE]KM.&qCUcG}fw"=1~dk(mv?}Ugkt%p!Eԣ6=A1`O]I)Y XqXz5ɷYn%h{LNyS#[^9#o42OҥVP7!i|g\oooky1edBz,N)didUn*A^ɦYCۻ-H) !uUp9  y ӧ{"!!~r#r$M$A6x#βntѼY# #>m.-ݤXb̑ezO|ǴKQ%}8  mWG[?}S9?88j2ȾaG?gL@A342Jmog-h,Ht9 {36(O|c:c}Pyn[( *7HL3VUüyO ǡ=`\03/( ;ְQ1@ն&bH  g'l71:dw9 8#OjK^\肓պwiX, 7mooϐ@%v _ji|~Byjº Zotn0G%pF۷8% 8(*qN+Ku}q/1 Ǯ8ǽz%'٢Ƞ =n9۷qmY*!epE%[{olOXٚ&.݈vѺrpHZ$e[A95}7m;^N ,Q{-"`3jO+;+#P.'D6aXqҀ3Npσ*X}{eGNWY,NQ0x֕'dYU>A9g='ء}fy>`}ޤKh=7H__Ymg ϧ:s qTm@,Vݕ Ƌ"Oׯ^(4gnA\uVkk_8gf~Ul:~uϨtSO;W4B&y !xrNkT]ip8KI8duNӏl[[1 % y> gͼK{Hv9m~\uzUIu85O6g;@ڵfʴwGON3=Ic۸AdXpk?,Pg|0 x 8O5YƖmyYK'딚}_>@թ{X-!]n++;``tmn'yej>I}GmxE%-!L$H+9 MBLyqAgC{xI糯Ǐ֩4s܉JۊncֵuxCHxRnz ΚawlK8 Go~h3&2Q_'u뗟@k{6dTQ^ocYr ǰZ2i7*|K# r#AaOX[^#MyZ;=?zXtf,7ٍs7:=>]۸A:0x;c/j0 k%U0W0z䓎=ԃ!Rȟ3q#8⹈#Sf<|`{z[pnlcK'{ vDzm$tsV&BPFH b톱F$Q)+O'#x5K36P^x>jS%Ւy#6 '(k۹[QF"'<:W+X̦;ur#L}}]xli4)w$D? ěIWW[y2@u[XDp/9WK` UkM>yr#MdywgspX`=>iD:0{=:K`mY'y={U-#60%ӔY!u.+nfp;yw&0,>w⢴?jXڴz=.c,k"GIZNo\GortW EN\s^G*nnv_CE>ϥ,Z]^Mu4W'o˳szh:k,8b7 duv/ )H;l8AۚX " 7#prO?/ÉA=iT$[ڡql^k[EER- 7dڀy[kMFG$WdːuOM*]KS&NpDI|sd;i^܏P‡)3.3ZJSO"I'8#+,~qgdY%b(ù$d8a\];t;ϧpq$`f.2?$tz - "mRKKuG X `{VJmJ̩ d NבKiɨ$%6qG뎾jڍFD H3Z8nn 8$[˖BWB|KtA蕋$ M3tzӃE^?=-qO21r~uw%7>L='oUK[ɯQ'9>q3A yo寜;7֝ivRHYL /zKM%9>'e$W7RB@`'3[4rO"2@';yp+sIVHmX\r'߰$*ripJrJj^#;{'ՠLkR߾6z kie^o.ƙ܌sG=̚{*ysfbI}GUitxlnZ9n= 6|u7v:#Is/0>:}&-~`<XQMM?w sۧ>z-4^E@&zy Rcm"x#$Tw㚡5vй s=IY4Wc?Ϡ(VKiLTrH8I^MMx )H~JԋfYdpo6$3&I'ҳh@j78V=9< eo-ͤ7&Ӟ==?$G1@=My2uK;T.׏zۮ<,> z}pdLTHA0dPˤLvvCE̸ݏCRk6$f]B0g}n:p`x vCH~y>"[ඖ]feE?b9,9=h;4FRɱNsb YV_27o'Rzt&,=ذI^@6H>cc:OYO^9!=FgZ]/FR-R[t>0_I '8g$zs}/YGA',y85;|r"? P3K,F( 0 =y&[ψ>Ԟ%{=0 `_s>I?}ëϥ}gJ#HnJn-U2G*SVGzS!A^=+.jqOoay=ΩY<#>@QZ?axn9"m~,@6B|A7r?˶=C_x⶙vЏZXhPڒhq1P=2OiiO[Ț;XoA=1PM>;Bm_ȹCl]#]J@ҍv2DŽL1־[:Q4 ,oͳ1ǁӟ΀=SNzmkj77;:fԖF`1H>[j8vcW$]'6gt6Q,x'37f0c Tƣ k-,8szudqdft{Mm > +R#2쒪@[lӽfq=ʏrDjm 'y5]}Iwl|fodT? @홃?)8,,1W'} >oywn|(S(rw^J>`;a5Y$UeeShÀ2x9Ӭԯ畣d `iMqĻFp9n{(I]Cr}h^;Fdwؓ&#<pzӎ+A6YܓNs]WN1T;c8Aۧ7Vc2,oح{}kAzӐcTIuyyqizҬp98!!w9=KwFy"(ZL&Oȣ^`'&%uXy'5H൐-`~HR[2Fbme19OӚձs #i#LaOW'ݥG'lʨa>bzVv~~=Eo%{m]Ew8+8R9nVY—Nurh94ˋ7Wp"FnNV*AyI6 s 4㳺N;?rF[a95n4]N 2q2?:_IkczU67 2Itژ-٠GPsGSVΤfSI0&1 |cɮOnE&|[c<;$}hnmBI]2XɋۗOG'ԕ^Ee#Qʮ Scsf;`4pZtlKaEG:Ʊ+U@#ć@<-4Ll$hsg3mwkmZVxˠFXԜ pxV^O?d1@*sJunk&}fTQxW|>e#ڀ}Ace iʽ%#<p2[E+|h~pU8ں^%{V'9ty5^wmcDžP9q@673yO@ v~z(O# O>]%'X1Jș%nmrdn{;qnK{2R\nlO|wӶ{b; n1;#5OOAm CԵQvorϺ83#w+9\>$M=BnbHżyxl2G(ByI$v$9s€'?6=e/$2 EB.T:Fs M=zIc4f˵uy3hoH~c!rZ7w7R=kdFb y~!h-YW,#b98'Nѓ!e_m嫧Ӽ9{*/Ahɗ%eiǘ+F?ğ*@m|<>{r;W]m%75oی>f] ASZE°h}{r? 7gǏ@nԫ81'>d{xfaD=~*C&Fc8EDkfZYAy+xc[$+_}m$q+ֹ|=fio[= G([1'OMrW5?d2q7ò WZz9R  _g >ݗЬ,2O Qړn uA{7KHFC;q^$hHҀ9W 6k{vbaO?5DS[6D$nAzVmm3M:pq-,˲WEZ\x橢Yj7wZYrxÈkZk~7ZZmrӚ&I.afzh,w+.u}-sNմ{ku0DR+nnt6sHv۾g>$}Jnyt:\UZ߅t_9[B}Q[̸sӯ}i {#:c$O$q@ Jq uc8wgszX5R{ -d99kPI$7kik^DnO\g5CZuDG' Ҁ:Mrcmn KEnd=bi RK#&cuӚ5Cn^dbWǽ_&8I 1޴C41G$gq@5g=5}ĄcӐxZKŮ"zF9:ϭ5Kh#fyeo,#dNQ7eYd $鎴Zigfer>T7/-5 o ` :;ۥy2~?]\"F[oHx@KkHNA=93Wl-CC}F<#0D|dED1;յX,`Hř>QzMQi^!G̐@#$ϥrȷ-huO>N.06 d$fkvvKaY/.mܻαb&.XoP|d6ADZtN4p!9}>XFeFAT/0tJn"K4hBIw\mZIjv:GpH@ߗ+޵1;&c\Gqv$اz!I=sWf mź,qn\5B&3I$nC+<c܌P ^*"MGQ,J-#?gfy|4C<"0m]TC̗>vp0XZ-ΕmYG)DY\R= `:rHE\EAox1Ѐ;2@Nm5ynV"$Gc1=L7@n;$}+? saYwxch.~S zVMZo&.BmB~PR!ŵ(vt;lI8%j6f@J#p0z~R5IRXa9޴u .5k,=F̍$&ٓ$A_\ α72o-܂*vr3\5q]3QXf0d#? s< ֶ ָH} hwTs}sTB$t) ] C-n"O4eԷ-y4yi؋{%A zz |ݍ ~BkZO;Y.Hniw)Jmǫ>9|^׆N.5i7 ?N\; +tV!>K+@YD0w8=;򮫦.!x/̢+biK; p9'6LZDmRхO==w˸$dB2?0 0?*-w7ySpx2;B}:R561d$g%Hy ;Ki'qbvdH>Nb8)v`)=_umms,-~!px;G֮FUk{F;k0EAA(\ŻbtOɧڄq1<;ZPLg.U߂ܜ }j62])t3̗c"6s܌:4jhVO`69;dɩy>WP4q+ ?'֝Ͷ=[^\6oo9SV} y^f/2c X#bdK*$S6/f"<(X$r nzt-l8+UǦ2B2oz]ZfWFMt2d6p@9h6MKPG=I KgǷ1>}uuwpW,I,zl\{zk/l[i/u?ݫK{t #;5ּit?h1,{(fPMŧt[<3<$ɯS3kL.ܓ5E"T^z4tv6Lq |<2QAu1o׸'/[a{t)w'haֽcJ]!g_Ch6z֐P y^oTOºO i~e܁3V~U-a θԝrlu $ڇ VFf>a1yޱwޞ'yהj6`gy?yֲePϝ;\l=f;**K+ʷݯYĻ9Fs[=޲5}~^G)\.h.wnPӲnkXMiQ%-sGB $6פKkVvQh_zNvwImH( T7n-Zw[t]ֶK'98湋A/aUO/hN9+pC4$Dd|1cm}(8hO2DA jd>^zwУ]\άGjsٮ%'ˉlc'8>߅V T7wc^vKq5r o2ٟϕ+9y:~k6yS<HHUBّ8~ukOɾ{%Fx_5+ȿd~'#~ktZYMh]'n1z9y5Z~2.? \.%1ێ ^|b2q,:ǭaG8`[yBB>Q*|0{UWkݦdt'Ҁ,O̫nB`<;7E Wֳ) 7 c=~ ?nml47m촧q$b|;ӞxH5ܤcgCyy֩Ay|!`6Jy{]a GѪe gojR Z% F8F: "^%I1 ~|$j&c8\TwXH:Gj3EpWoAm o$J0=:WCizFZqlt4Q@Q@Q@Q@1 5 Br;W1yI1;r +5@+-p=_QMviww+uovrsAF82mN=kmG>iݏaTzָ;pOP}yǖ'4w1J7@W}MX:u6Sܷ #qs e+*lM-qkڄo,` ʳ֠VjhK&S˔bkrYw`nV[].\ǘ<8-楦iE(nVl7? LldMq~oo]rvh6-V0>ss]u n|mun@"ygv}kyS屍Ic UrIh@:V 'HeRo^|9qпIkմ r66ߙv\Yk+h/xVi}FaVA+4I2h]fPx ]Vs+\Wy ր;mYЬ\.dMB Q ;p}#YIgh%y' wq8ufdlfD qT@9SW';{BKS͒ ;> [ۅH-[YFc=}qPz؉ZmA`"1ǰbGvumi>}}K.;@3GŶݭv`H-N(SJ#Kb]CrE;䌞m}}ܫ Iɻ}W:uk7u4 , gAzqUv(3lGN@qPK-uiyo$h1nj@<0G#ˆ^K(j"y9N}+Ydew9dY-vXۄ/KuojS*lgug+e|II3ؕvF:P[ͨk:֣Hך9A *DJ _iW? <#F$@Ls :iWǃmyc_%HWJFTp(F#@5PE:ΣwFXqYJ0 94WK|S??[1wY2ua^4W~ŋNW?/]& /|k+eRrXnf28fui\+}yzE{.$VEy&"FN5 ?'4+bp9jKY[]j.ܬQLOG=ƵGf-!ڹ{_eٽWk>% f@zYY2[-RA'-Ҹ- $svJ#Jp\&5+XgԂZL+ϵ)B7L3kKjR({Vhq34Eq:͠Ѥl` Տ>f7! +2W6j ][z!͵3$pP3yVљ--qܞ+^ᤚ;;#?%ikrEo2g~u}Egdt{. ,dz|=y[j6s}8a;ϧoz/O0d2I6]]GjʵWX"uOhw.-5;{K;+9X!Ǚ$g${׍ϡ3YK*IN s@׏~5O޹k/5>c)ٽX%N=MXUXmێ6& iV[Kc_ݷ| ]]*ŸCGn aX)\7i@`ڳ|9\Ab_֖F_ils޸nPHk+ɰws9ϥtU/,W-"\HB1`wM%keZdH ҡ_isʆSr>+{kc]L]_%Hz`p_ܥ2a&9۷r/ismi4n2;8rzNbgSI>=\%q,v7/xo^OC ֌mkgñy3޹+ Ucqg u ܴi- F>t;>GmK SXV Mk G"3u/Ct5Xjt kw0d>𣯿n3M.YW9Fzŷ3{۹,9GPsFBgn3'῰iw${H^~%p?-x`\(~xC_񆵨jbMyq S ( u2މv[Wןm,zDA}هW]E?½yoJuy=޳3q{m~bͧZǓD2{!8 R0Yڸ̑eld`־{gV1hy`XuFJ+eUK@ ߱W,d˨+bxUNpbkmK*~|)`ơz2,w6b =t΅ r4t=aa#>vZ_쥂:xaˀ:kuCn^;jax'MF/Rr?<}sx~ͨG}J>KE~\ZY6m c@g\š[ 9UFOϝG[5sFbs-ǘG3H}{KGlݤw̋ Cqx@WDiH eq漷$-8șF=6wv#G6my!a }k5Öw&n|HOz?,1ALWʮ x|xKB[ƾ^kI%[̍WZ/Q"bT_7zޮ"ܲ ջUU.txՙn8 nր>w!dSHW;_^c%Dž/ƣ:},.&4KϏ?*>rdWݳ1iR^\ڀ?S)__ǥEg#NSĄ3QϽ{3 d[fäI#&ȳ9?|Q:A|jw17lmd ~Tp2:p?uΝ6 [; g2DM@>\=(<q+۽WZFmF88S;:rORq}L=)4B壉̫`' u,(6+~ߔ$vNc|Ɖgq^Xpy(h?Ѝ#8@Ƞ2k xm1 V@>zL$` dgzu=98+ ۜO9}j Bn)iP߀ ou2HAd ,"}X H]ykHdy;+B u'm\yQ'999}qUJ<9ms`mE__^ "qu;p#]1to\FwM a}Ó0[;YsȖ~&ִ}ņy"7QҽC?m,%}c#dpj_2, >:Tuyds<ܐ3]>kohF-cDc$.'y%5#2=h|S/ث6 O\/Z?_mxnUQ#V䯹WW6p a'bOp=ұ5ǶEi""8-eu! _j6lRnJH⼿T,C Fyc~=|S*T;qrkY%9`{k;@2LfTKxyg wk>+Z2D@?{0:u3SXxr|:FB-|ZSNy@d9[]ɴ\nycPy8Ҁ"|O㛻F;0SOujk#F1پIK㏨z|=hndHqz~u+9fu pxOzx?6W¥vx~)4G R i?xgf3NsX1kNt |<z>+t+=l7s ]v#zEgs:E Vi;C;8MDVӵ_0c6W|'꺄izDpp'j;ҽ<5oGh4ao5  8h@k[Z2*0NzWg/&>4 AXZvSo9nhaAזx{Kym8S{a@RITo5$Xd_츀Yw~1xZ2܉Ӵm5KA #cw5FMQGpTJ>>_PjeMespI gPEwwuYe,z?<Áos lRuo38?ƶ|Sku{ut}'Uc^MzEė 3 ?68.kX\Ge+$FFr\`Ͷ4_o+>hcx;mFgD]8fjqmm.wlj7^hNԷ2s sۂN?i\4 r|[𮑖 fPLmѱgFYEfVeVjeܒM U˓$: U6޿gpq&YYYki&X +TJI"}Z@gGy?X9˨y}qU5H-])H实ԑmnyaYSr/u .vWz\;2$΀Oxϵ~Z[E1IMX<?\s^jY.X>_-)^ Q8t9DcYYHc5;ู[yԆs@9|tڀ_n;%@g:bK[= s<`:ds]yInɓ O׷ju9Dylzc8ހ޽ҾwX4Y'}1LQy}GYguf1,Ŀjm~{;I(F2 ?M^9%FU4Pƚۇ1-1ݺ> i bHc)8?x~w$M=6p?: voOÃ;N1z_6ipDdz;z){;rd?xǰ^᫻5Ky$e(8 d$ת[N"_V9dU]sף/sgHW;[4+lV geXZV7I 3}iⅺHm};&Tx>Xl{WoK܌.2Ovp" ~bjƪ/bYw {w,^;$BWM6̺X= JxC?wD>0!<[D==x` $v̜Sյ G/t|~"ƓL_WmRF?r?5h/l 0_k=f4C!2gwuq$K2A=kŵMP"$o?zu+p.d#*;ҹd7 m!=^q#;w5yms [Jl@Cz}F3Q+>pvrHmƇn2>ׇɩPfOAO*6RV}@iMY" BUi Yg["]O~h(H@x! 2XI/xn᥎Խ M#H{J|`5ioKJQ-Nק=kM=qGmcF>ODh3_Ib[vڱ$xi?_4ߊU2[H?E<{˧XoXc~zuzvу"tyuGs#6"|.nWz ,Ⱦ^\Ew _̻U[t33rWvk֪ LJ_<kus)hu}c/}A^Me%ų>F{}h߅zĭ~Iil<~|?ҾGU*/6(Xy%?hOWhw;W~,h"RW(\䁹n /xP/!zg8!$kXzs^9oq$VRFCv_wc Z|S ;[;Ȅ3626@Ch6Ks,lb:wAՒɬHc-"=,ùs^qx3aA듏z'ZM{Qf`@g|"<^-+Hq W񖩤Z %Zd1?g!_"%ÐX>:q-֕t󷛱XbJG }hω; iz\{~wo:R?9ǽ|K=^x#cOFӜ1b|c⫿߮a4ȏI,oO^ků-KVaə@qgs@CxQe,/+`Lz!_$xS𥆟vzrO1_51 ug=x+ }SFƅθҀCy dƼ f3Fծ_j7kfK ^>߭s3[cy$`?<<-[2æk|'ְTy+X_[Jxv܇vnqWgƭs,MY3ұgeoxZ k07$w5N82LnvR99۶kTӍ7^AZ σ2l<$Lcw9Hǘ75{"]ޮk&ӾjFMCΨĥ1t-@wszos_3xݻi:%:쮙Y/naxL2?Q6k0ӧ3ˉ>F`ry2k`1E+)& cuOS߭~e| 5-_H̴DfmȋI3^7ȃVqH€=P,Yؘؗ8>ðf;cG Fw}Ivך˂A y ~'`j);]-d,9<ր0WeIQ ק9޲䕮ga8ك`>`;V,vq R bש*CM)i=p%c`˟ ? +/qj>)f Ƴ*7^$b hԝ$}ISHlJǑq@Vi-ڑQ&QBɕ yq׎6̑!VePk<0e9#)nf.{|;kiڻ@^1=sYδɣAq9sgNQhɏAnd(:<QI]"@uV8ys_O*iu]W='n/FyK6*I})t__N|4 6i.:Ath?JFB "=+bP")샩+~%E+ˬЦбOj:lEPl|t_𰇓*3o ACX\kݜZY,@s0@8+[T[v$`ltha.Π8X[񉵵{/x$5h<̞Zw =k>V,Fi[ \ր> k)12gɯ׼E3J&g'#}+k/,3gh=p+4g !l ־5uV-w`^ۡj3Ck=xU^R q^_r]_\)_ գkMw6/o'hwTDp%oRNDB7FCyI jؾ`=n@*q޼U)ʉO5E.y$ڧ?OQ=Ġ4vOVU1'}t[4I z,-iOkV࢝\u$kDTŶY7yp~@KwP5ׁd7p}{J&H澉b|7R*#sҺXyDlLCэt~ֵ xLwljcQ [ҼW+=kE6! ?5bv^YGY|7p~mQ[@I joP8a7#a$WxmY\&bv`i;̜JaOL(k'cR3'fޣ7FE?hfvysUf|w[$|`tWa6$S@E|/[QIln絑[cJ<H_>w9$U8Ϙ /kM]BגxZnK[{k2yp4] جq^QQq;JitC!$nY$~N1ʀ;U cJ#dNxU&կ>e3GAr:~k#U$zd:L4O {pr3{^W NClg=&d֩q<bl`'O''G]^M>rey=Mx^˩4I.S8> kk1Je.獟|}nGK摅Oךp-`K2Lq:qⳮ:õ(KINbH6^1qqstp7b]ր&6*ǐZ=;v@1nP`|N:ȶw1>6.p_o-䬵֞kY[Ke=^kǞO,P7#p aDɍZimn1+YH,{n_n|+H[kYP~zoeT)ՠ{2n?ʀ1~WZ~dM ~7u:=B[: W:hދ,Q1Bw̿Zm:iK|}{n (#\=OֺpY9l#zr_YAC_'`=ںm.%WP?ʀ9br4 -݃3'n2k'X%\N+.;횷ejJfK&V U=H;tlJ6cH۞ PοXm)zEQaܓ=_5%ԬeR^TWؙ8ߵ};c" x"_ΐy_|ݿ* ʲ 7GѮDQP<1 # D;Y-m.P_%f$ t=h/omXEu9)뗩k$1[*Ʌ_ X$JW=x&i:Iu$l*F[;em:T}&;$uO\m{s52:4aX~#y78zΘtѬhED&EcYNg7ZbyA^k7q6L]]v9oKmN఍8Z?bIs1]l$āt6ȮrOڶm兩}2$C,@jⰚ$YBp]y Z,|IY[@FkI@i}:j7D6Þ2z{V:ZCm1p4fԿt[ٮ^F#Y"1Pߍ{mR!898u!eo+qП@-[-0Rs+4+qql_9.kNVMNu zus-շH=Wƍvw(žqZFY-%F9ڋhZF"}ּQɊI<_ FpYRL?(ʵkgVK`t>Q@mxgKkg{W94-OҀ3c&i_hTu]U h30_{0YI V=nEaחrͭ0~b~hռZi2R2QEQ <ۼ߭?·i< H}xOWEȒFVi'Yw7{]]΅ Y'v#y$r0M͸"_۫1P370D6Or?]Ue1?1ҹ B_aqhM(k6[o0Xv/lZۖaF9皖}F6gn3fH{. :^ڿ<ֳ>?Eh]u orG;^\8ut8s9xѼ5KuT ơ{M{6/ӆcPG^B5H~E O^÷К(-Ǻ7Kpkg4/ۢF‰Oj-<;K{X^O0|wᶋXa7D[Y7J`Y.oG8M[%Wv2hIaYFJ ww &$V2'szf&Uh87pӽa_qGkŁs}}u[Ʊ8܏v䌎j#y5Lq w]Kdhq~brJRbcvr ԡiwQʻogNyO#3|qTTyIc[l"G9u m~R)T=H}˸~G̠} *ndmm^تP\W,)QUv*ksD!+if3*}œi*d,}LRZI,*%Dno+<732? "#}5OG.cF!s/;g:KW?qjH[k٢hvwx)'>ul{9|t'=3vZϛml/ȏ1Lsk2-BK= YP# ڲ-/uW{ͤ}'.8|s;j})v]ƥpI#c@+TI p-Oʞ=3̪dIc ~&m2eIƋbѸc϶9~%\-ƹ0s8&7he?tg6p5؎GP_=?msyo˵uvw szu(nr}y&H.2o56ZܦY$eHv'*G'\X1p>U9iodW&B\j@|+s?hx)瘞G\S<;NH Tq1JHQ(/cH~΃]կ.n֐^I#6jZLqKǮ(c乑 ^jޞQcIfm[ \ѵp#SS-쑋^ !۽OPnfQݰ}մZH !w.?-&y/ʉ/=_B^|Iso >|uF2<'oy&gnƲ9|Q^iyNF>a~\v^ 4Z*5'zecK;H wH1={ho٢XM Akɵ knLͅ1n:+io|ﲣevm+[#Hwmc f@$_ڪAz|i|-4Wp$rH;״XiGzV;z@)ym{rju@i1ɰ+SK;cfa-sמmw,7@8jIqf$퍤ԜVww 1; Eǭ}< ,rMLi ,ɜzN%,gFof6s\]* Rö:sҀ4|?Eo5%ȏp \8 gT5^Qfh*BwҺêyNcOMy޹l!?m;r9<ϥeC&U xhaH%U؊6Msuq|Mp-a aZjw84SZM/ʱÑ6+c}lQ޺ vP˱~y?n,I$8dhd$;o,a5孖s#jMs!-&șq;蚷h.6~ȘbKKh,!Y.v€8=WL(G4J_nߛGSF x߿ jFc6ӰGsmnu^i:[ 揧Ii_gDۛk'#NW{ݶ+Mdaʦg~ +m ጁss@o}kX$[{\."Oϒ Z'þXn'fTL@^3 >F)$rF7_v>mUeYx;ŷ7PnRFؑ'?|˗+nc^t߱xmq4alݷz[9hobOslV-v`#:Ucx0$J{.kDipdE*}ܟOP Դv0{c3t' DLK 0NrAvל_\GK|!cL8sP_ohF68@z\{g}g>tMd)rp_&jd,lRH }88 %k{9&>r}Ww7Wl'nFj:mVg]zφ2p,;b>kɍTڍJ /2F}zǏ/|Ck>ζQO+o(%E'?jO\M{'{גk7sq%:.Pzh.yeMό5wr[۽31_k{IB[s" \fu`j-Q%`ܭ2@{S iQ .>`^{fY]N~{VemriVĖۺV[4FԋU*[֢3Gc?AS+WmĒ qV7H,aYː ?pI{ifĖ7*G.whPM*:}>\uh#ڤ!z㓷EvnS38C]/@G).:Zƀr??HiB9'u}EkC"C2%;pI^}8>xO A+X@{g=+ FJUYQ*;w _)rna@;HrM=ʹ/#ʹMg-+.hL$coL&h28q@(GD7.Q+4Ll"9%߀ s=tMG]E~#qk<1upU`۷`d@mmF-̍yOqW0$Ȁ.?*VFa]ex0]=~]h 77lc sTa[u\$l%{?3usHyHJu %1qͻ$7Gks \x%4EN  ]/$LKFMN,;q@+j]#q?ϥz; %1BC)ۡqrܽO9ZD-C(;:fv?H"iV}[#;I $`cww2HdkEqǜOQۚm,>cbsМPo $clt\mkntq(o-`,i"Nj X|ƍ~eZ.I˺u|Vm-,#fgsˁ=6۝yWjM[ۯ7JiY v:zʀ9WQvhbɽ~눻I.d>Nk Ƙ W$?9%ҹx/?(g&8["[k K]]*˷P[kn\neziI#xd0q\LЭIެ2ܼf:(sJTb{,p2>jIq4Ffp۾_¶-,-hIf د''ۏ_8xA-( -#~fS TL׃iT74i1ޯjlcV[w͏F ۨ*l})/x?MqEBHQ{PCO;O3l̏qcJ4b e5ܷ$;.%9?pVxF;unK:[4ܿa&ISN]wfțNCQew-xZ jM`  Lj7M jDfj2<0@3QUӭVVLM.6>GM.ɘL#:(?/޺#OK0ƫp~|)QGFu=74LR~hbJQO S(@ϧhPw+=`¼uA#A$fnюD(\]a,4ZFDՁ:\Վxz&Bgpt-ހ<z^u)] ui4J<x[s>>ѯ]6dl: ր92{u,۩Wnw:ڊ]ZUcVvyS85bHՅ]0W}h_lK]6۽ylPpIuЭͼsFSMֶS][C5j )K[`AoZu]q23v͙-}tP^g>EسH>=bn¦x<}k#R&w]+p ;+סXfu#=;W+ޙId+oS`8kry5zI%Xg@|WMt.|?=JG:<@=HyhrW'͗![㷕ϵ9D1RiCEY ճ@ ǙYURtkWbh֗OvA,YL{K.挖HPCquwgZ8q8S^ݕ.df:wp/.nTH ֿnaygFv$2;ZwyL %1Y3kى|O4mm=.nK+]L% qM}BYf2ԧL{+OͣRQ[B: 1_> Eɹ{\3}jBH"R ڈ8ݜp z[Y Lqs =I'?|i.&@>s}%n 1ūIj` 6I?w/v-$RcDu(еqM!t뻶M-Ux?;6dq}zFQ",shԱ ,#?z*LӒWX]$P$kD2C7+'+tJHȽսFVdI{rP+W5syI3ŸZϾhw ܚW&)-BcJ/2#e.OA^c{5đH)<B ַ.dXD2Hw`.2ryעiP\J[>fqWJ­ /V8 OaI4V #!2$nր/[4/*]\>՗4p6_``G=z,vsIklAԈר'\fܕ% 46Fb\/#W/Ke23;9 ܻXm#QB;ӷo4A_Gh2,{(-IaĆm}=U Z^ {O]V#\gX$L&gMP<;1j[¡yN?Z.r^G&e|ۚњ(!Tm0[''Oͳh^W^"; 1*-w&{vkldW*K[׏Yq#?u6(дXhcWlӁ@Nq:m yGZ4mI8X#(43lFve\ǵv\RI8PscLn.dF^7Y[76rV5횦H-8\p+^-7S䎭@c"*$obtuInY7%60&,;ve5$[15M5t-BIܼ,Kw;<\W]1RO31g.ܐ0zc9$8&XD{psYw&8c{Q|ݿ|@8oSEl߻kcGcK.(< zd֫l9E 6N䞧`;4zֹ$?.$6^clIy@doiR,,m m|z^u]I4J(uXEzuuE vx.U-!|w'^#v$𿋵B3s?}Ys;fIq\Kg$Ѥsǰ?͑) Π}kjͤ@93~Ziuqff_9W%{UX[}Wf?Uҹ}RK[&ѠVY*ϷנO7oIo(78R2q^KŨȥg?uzNqg^\ XazqPayѳ/21%rXA42_oj֬Ge Y _:igLל\h_:)?wlZj UC:ȻOگIu{F(=z 7Hۤ۰c;K`$`9ƀ64{ucs3,dmwJ%fX~s\F<ۗjZ<,tdA;~SO1g,O+9O|׷W3͒Ȼ@'bᆑiq#[l@ܜHGWVv .``#@i\ZX­B^<_Rk_C's^mr$[$m?,kԾjgԤXyl,zoBYjk=ЗtD695mGOUh,e, 's\5\ڿ b0rx''Pm>y#)k˽F#<=ObXbR!ؾ߭gɨu Fmr[2He{Rk`m@|{g}nt\^Nרקր=|iao۴~wS5;H$`͚r(6@ z Չ;(mt鰳ff3g޹ք0flv*@;V)$Q׮-Ӵ˄V| G]#Y$CMZ ܎R_6k鿱( "7lmm>cu }23ր44+_YK$׋yNlҿ-9s{q5EYs?UI$6lZGEF#Ydr@UPze}?ȶs$kg(ZhY`~9ǧ:vҵr"Q܀zǽ}i߇4[9Q|ж͈R}9=?q׺jٯ 79^mt52-$|o;=|9<&{Yq,d~@;M)$yI>d,7c;zs^su #QdʾJ7pѬ]B'$42fN$cצ蚙Dʙ6|Ǐ%4Eq# |?4XMxy{ %T1bxjNPA<{\4Lgp,7ǭa؂WV]Z5;2rs{8dUiF^~l?P6M>"4g>_$7Q"‘;OUuEq8LkI-Y6#Þ3L*}9diH"L>k/,v]Fvs83Ҁ: km5"YdCR>՝4i&hwٽ9+VomGz|+/Y .%AmĆrě>Pxdw'VDN_?3z U6wiai ɱ] qzivLڀ$.Ld2awg}iY-,Y~?yCLY[{nOLQK{[Eu@U5יo4 #>u vB-Kew,r=:.fў[8UP`qӿZHԤkkVOEq|=73@~Ln ,cvX6P(u"e63[F٣]K.MrCp֦FtDy.k~w#W|bAʏ\ ַO x/̱ۿyi]Wq_"ya7gk yHvKWHݩv}W6(V!P?v'8oYL2鶌p̯oƽw\&Vmțq1+w-aF݆.r:>MoK$vgF*rp0zUtZ E3eyY#1VR7s֝[Hd6`:ܚ/yn.K Z0G';; ,6{`Xlⅅ+3nqkNJkW$@y>sQ&"<[`:#|ǩ?yEn%˖=*6&A5\^ZeQ<9k!<-$T2M*1zOZoi1k:uΝ[OBFNrI;w⿰Mi-fmvzOxŶ% 3#ν?PP}K46e^Ē~C.P֭Jjqhq(km_-]~qZ?Xi,"9m VqЩc=_Ko+m[2Ix~o}5,ɸ}ۏ^2F||^xtXIfUC&og75k">wFݠ 庞%1=tG Hw3s^2Ys7L.m}fVWn>_hKQ__;]"4on[n+ʹRX&῜-i^j4"F[v63=^,bZMyI6:[x[0(`z}v.7sI~P~5&_G{(BFy7*ۧM"Wl|ۇG_dx#y>S_J:F=w#B説0hjMe,gUN~oq$H{3\]]ZGo믙܌]U] sA;smUί$xn`{S4Yei#Wdj"n[yL~Mt'L񎸓éjEg1ぶƭg4f_>`~|j[]^M2O.Z-6U"}74Rx eܶ9)fyc 7ۺz/Q<1dD8aXI02@r/&~῅GUX#GkmR.`д͙uǟS^]9;P{&יM_V9"&Gwf}k.-`EpA?_^8"iev'8XHk h#tϖ(6^=GPaDOK*AmNkS\?^[)|N]<̠']S4l~,H'#oL_>ۏk% ȽsƱV6mٵm:;$-*a^I1SKH_/eFgeAOJ&)%w%~Whř~ N=z>+G1ʈɽf;H(LH[j;4hУvsPЗ=pn~wy #}ysSe}2ȻOt`ɏM'ތ\u'p={俉4H*Mm7d9;J=bXҙe%Ɍ8{nҭYkM*Á}3[֦I6<ms);>eLy߬0F#]:Id*̱Iqk)5f1mc@;8OY@*؛/nMtkC!sמ=`[gէ]oio׳pp2}}5xgu3\_56e_g|L>u7-㙶bBGzg5[j>/FC+}o. ۮ!y'0O¾Ҵec#/\z fE5gmPPH@zӥz湣l?໘ld^z`|.#xQ~=*ٴD^A?yWI$+rXJ F62Ǒ\3O澆6w70 ]!x\}3\fȷ_ ꉷ8Z :4Ke~Q,.vut+T_'%6×UXrlp([{Wr7]{E,$Sw}lu.y,G[.vZ-sHQeIcm%@?f[o$y'BI_eۚMվ\Z|1p|y[LCK* #[o74Qd9HpooҴ/\[xI$do} }$_V8ye!0Âq#|(ŷLcˁvcw+}O4ko鷖,-9c1W6zܗV{gqӡO×2i0ړqΎѝ /9M"Ym/ ݿ+Gb:cw>e¶gON#{A|[ af#8qp*Շf3#DNݜc־>K'|1|BDy5K ׂ:7žl$4l{ҾOv֚LGN?>$wUΓle!D 2+Gn- ѰVnM)[0kUi~}s}"I |1mƳ!^o}2"o ֵl|+-WC:n}k; IQFܷ[hqw3'#_ }N,/o.nI$ßW(8[  ;}ב2kc3{t h^j^"5]?f".OLz}(W@ncʖ@bz :|y"y&Uġ荸q^i ^%ékmny[6 ;5zI{ty@eJv 3lR(W5xn]=fVl!^ewJ翛;z9Vu="7^4*R1Gw4 rK=~ k\[ ߖIJ!\RPF-8-njcq<33}@%^M{X+)*E':B ៬ݾ[Hުڗh$1꺺۪$@N7p9#ԑ]8v|MrWܬL?zguqkn>gϫguv},G(CNɜ%݆i`ܴsO}#L`7I X|UgXo]Aſa?uhvw7]i0,j2zOz}Wܯgfx%q5.2jkRhm_*yiyB"!C!WuVֵ0go `~6v7ieiN Ԧ=?O]+OIUVAO7ZywG^k n!c-ﳭl[R$RBǵxE)8zNm <|˞VYZ>f~]Úºt5Fڊ^Lfb?߽m[:$ƣQD٧Tߟ»kXo[ɚ(Cb^@ !l㐯\̠k[r >1 |[=&d?ǖ)Yg}(/n{6 +닗Q >oNM.KM:b$P"hCdw*[qp]@SXVGzצ[IE msM<Ō 5x{W՜Vwpq z»=mh%(az,Zf}itNI9^?6OiAg>F ?=-*+#2}m>\j 5hd~uS[+t1;`GQS^a$VG݂S'_'#c[6.ܾ*b=qz鲈٣&ygwO{{q@VVu%F ( 88=s՝RS5$VI4r`+"@\j:6r!mSrF`AYΐD Hѿg@25_ qs5cbo$B'A@px yƖCcptyz~wZug$6#u~QHIدg4xhG=\GvoA@Zuu{/E&fdx?ݯ8߆!#Դ7mַ v_YVLyһCV__o\wOk[q׾H-؊;{$y,qWLJQsEH>p~,jY?#lz{<9mWtn8 ]*eηnGzFw+>d?[3ڜ(N/ILyov޽oRӚk_o{ nНOݒr7\? N/}]]>n|tEiYt~i~JBds^lUisV=늪0}·Xr`">洲Lli('*.!Xn1V)DݵGLswR["E; 2y6dҫO.yW2AݑjKՊq:)m%-,Q ͏m&S3!n8'wi%cos]q%iw*[y0ah#O-mUIUrxOj4j74V0t'8POq-"{8<ǜ;}qO^h.Lmq "8;HpڎOq f6 7`ֽxd+f$XTrH=}ORwmA}?߻VI@B8K5ty9[vZ&ۇ2S$`cHo"{=1 ZG1$㯽gj:Uh{đSߒOր!SiDZAW*O I1eklK?P/ckL&[kP;w's呏wҶ÷mQbm|Ƿ24k5{%z/8Qt[镐|cߐ q[WrjXYnB/{V1D-%/!:^MҠӠc[۠5p]Rq-.-xŽG'czf*Z\i6\'$cֺ;7e\`cg67v$5XLyɍk{xԠ:H@W\PfR_lPLxhL GSu6TH-D u$V&q=\3c6g]} ]pMytj_W2zOcm0_^̶]ozgOKKkmJ5P2h­Jݿj8mmRFv7UfEUf"ᡖY$w^k\XK#,IEtM=ź>l$voHr?3{e6{?5|T=4ѳmSPr2C_Z ˩f h#˽۪ef~b[5lmMu~ڂ?XNߴL:iRCm=\CDƹbw]kW0i0Ceigq(_rx>7k)1`l?dMZ/̇B0W~xϩ46\BZ-(SssVo{>6(9c89kRS)OnuQ-qNHܢ+y5"{[_>Aϕ*m-x%ǝDSB{vc̠0#]M*\M$P=jt Tޝ Cd%چEp :PPɧ\JU'Y='oP.Q+`8P瞜Tg-]I%Tg~5봗wݴZlG'I'&x Y}^w.F1ع `unIk-Km0HN0VhD $D`H0]3+lRȑ!< gac٭J0?N}ξ'־!е6HeQy01[t $%_0!>3`ux'Z)eż1bGPAy >"|/ҥѭ"lN۞H}s9]jݭQ׎O!5hMoh̷ >~A G^#^ 7xKY>5ڦ,$hLcks*a[Uok+T҇.։0!:5ͥOHw2ww~+Yoky"6ȝ7zW{*p<ί]+SLlV4fΑMIDNOV"Ⱥm3=Ϸ~~xC↧M_Nۑھ>.Ʒf77ր=nި>d}-Jr]ǭgZjktnD}Y=oETr??€xIbgwfBoq4۞H4^]lN#CLU>ekgk Gcm"j/a c;{o1H Gc3qUMPqq3"`9YZ~nm[48PiU̺Dd-=r}뚏SiG|GǕS X\&!I>M0m}B %qɺICgx#=.%xaZXY,{<}$=O^&XM+ !>GhKhݱQ;r:߶l;E98s귗r}291*y[o3cN{Q.$I!,y3w4L4eyL+wڥ[[\mD0y%oLo ejnrTHAMyNj%;99cgʬgJҒ+7ځT=43;+$g|c 3kgv@^+Kxo=6T^Rг+ +^wu+ɧg&V-poǪ,Z ۉ$"/u+[褊XF]f5h-I0Od)jo o *vy;+;;4Xs!8a޻tb!Ip2OMsZ͝-WP @R;9.$|?& UMVu 4/vybq~jM>,_b;նwC|q@zgcon'iGp8y?i7đ$ʋ2i NqCñA4!Ϙ3͖~;opڅsHھr}Jx@V=ulErd\y֝Yh=9 ZeL 8p;TC˾dFs;`AZEFR=S`""qz{P}myu rr=ȬnR Al烎8%zPDm"WЮo1yr,dHe_03F1ǸҀ:t+Ri{OZ1}ʒd0o޷^GSU&i"̒L2H |sԱI{}= H"E^t۽K7sy*ǖ,{z{WA&Z[}y#taj 'zgoJ{.kŸv,ϻ1?pF;b/],.ٷo;|" ۷?={Pđ21=y&@rzjĿ[diK`]>ս ԁi yh .uuT1)616` =pp1@axwPJqn?6E$fFI8\XMDŽ9F?:ӴZ->mN.<ԱR98-<6GCߊn=BA.58d{x%I⍙n|`{w!I-գl::+Đ:T%gK%D|r}{W_~4S%|MwKćXPcimy^ڼ5U?x2~-J "M3Skkw*xU7l~wEYw5 5e?+^Gq7$Gn~mi԰}4$8KPBfY<+>u');dp>_J>۝KP83G4`= T_g '(Fx)xU3WudtyM/7k9s~FTbӮ? }bʒvO6(ȑ >_?%HMW/v8$gvU6sjGG?wN1lc?ZGψN-Rxŗ<YKE- (LvoZ;HܶwL ltϨs}e[ISO9{я 'RA@+ewg8E Pܰyc+DۋYYYLY{5̰!\ENshlJ ''ڱ5QqolI1˟`1^|RG\myDv Zun ZB-s@Ko-hʵlY+͝j|RHA\?1G5İq3oysǯ W+ѤK9#\m[̲I&s~ʭjzIp6q~s\VoogzOM!ݸۨ[h! <4Bh5H^n mD>8meh=ܒo2!qVy _dH͂?:zgnYm)FFHZ?@͌3mYJ~pC\F n;Y]֮K\n遄²SQ8(<D=z{9-!j7U~_OiнNVm$˸OZ\-~d*ovzIwj&[ 7C};XXAeuk;\35+,}Gs~&.b5IJdWϩ,-/g%f5/qJ [9:vYtoK70K~+DZPѣ[B$Ff{Nn|qZOy$5lֽ.j -%t5}̃<%G/uÁzW^-m.2[x?zV>3?wI.I `+ÝV 7 ]_WN ]J63nOr~uS?VmoU6zq\;+^ZXx_ u6!\HqJe"G~kʿo$|\/^JfVixfuVP|G\,:1#_jl>5M,~|W rRcӵQr3mB_&-GJIuGMc!c|X9V(x!!1k_Bi-.aW0!V_n5;#cG9۽%MٺiLq¾ivwjmwGs+gCqYNlA11`(#vwn,'[}V2[=z_ZBeכL#)fb80{6DΓ01~zZڊ,A$jpBsxÿWQu'Cr83@sGAyadzAH?Ru-ϖ ^Ue g*kk 5ԗOp޼o=dX=WnJ#WA{˲bp0#k)?iXX#`svO3>9dހ>5."Fwz`>+/So! H TN{־W{ c|X2o/GtկvڕKm%kn A;PkZzMstc GV>U>"էMF[]䌌t+SwOPr+-xKyX_2h"MY泍rOⅅ7BFN8>tyq3/+6WBc\ӊ⁚% 'wC\U׏u./Lxbq^5wᘹC̾J^Sm+`cܒ?1hI$hMGgq5v𖜚\wwW";rdGdǽgC\:>2Mzt=EͤU{ۄ򣘓^4點I?;IF\ӮpGj3RJү>X##3Ek(.k;Mg(q0=}jz"jեM:dHrrz+RY+I\qx9]Z,hшsZ {a(9 xr;ۍ" KI"6FqORzAMV3U)Ms9e[{ e =hZ]aoz5bBcOrG֯kowJ%=p8N2I4np$; 5Z@βw',^_2`u'<;g4KIKU]?PϹ0=Lk+c^ tMē901Wwx-BH# @sHz%;f0<-IIO.S7H{ ㊥\$ 瀛ć ں7BJyQY8u ~tY]IrXl8I#LU_,dE/$W(nGs$'@?P4f{ى }9几vN1L]OJy xi["[~|3!Q!n21 :Z^,V܆û+:kDBGd6"z_Xj7MI'ϒ,0dEc$zpkcR5`Xʎ)$RTOcVrX`@y$aKg>]Z{+xl<)X;XҀ>Y׬4祿X>j4y2qA^xkƦY/Y%kxA,tl{w=pŢçI墓+1@~,/<R:8vU_r`U<$Hs_گ-eK}L:n,?+c5EtCr\Uz}"*,' Z39 uo*VFFf?b: K񕍾_fF!RZ+dZʰY ϽI̶ŮB+fUrXd2 T[\+otvs*ȳms;{m^Ѧ̭ۨ+!ooޭqeniƯqݮNkk!phmۛhӬq"\[۳;o`2B~Ь۪2w 7?_Ҽv.ym^Mnk['ۚK{,lN[=cQHaC7eR+ ̫wgU|O ̒BYJ*WV[^de:S5إe ukv)YC>0n&{/1e_vYX*۶7n{kH޹ CT3ʷk@YC 4r2styS P ҭ]]Z\@D%_W_j$8۹ֵ֥8q<2&n }HnM5 jx.tUYrTloֹ#e[yTesp6)URESĞTk)]{j؏WiNrͷҹ:s-l #Yep(*PfEBR],vnkGXQT$;*G@O.n܎=y|>MgOnYOF[ +m@Or3b_P^Djy!Eg_UجV(ܯ4zH˻[vL$osv\ l|E{|3d뽮JDծⅿ ;=2ռ=CMxC3WxCG,ˉ.[D;gһ 7vU]dFOր44[XKF1LIK~d="QK{[h[A&BN;5iw$Zmq,xD9p x}W/.u(ѝ8?=w}6<=]gf\]3G0F3s]lYcB .1'8^1tvH. {Uv ĬHNW/le̷qBPaH.0:P.i_Y5Ēry3$ȼX{idKLF|ā̄X`u$^04<|J$:HOYU?{p=h [Elq<_zMnb@4"x}sW 5}2)c&nQTs?#U׷﨣mk'RZ)˓q@KW@cL9յHAdG1~qsՓdIQK|AG^MCVhdDG A@4{TѦ!{6I,8;=)`PH4dX!HV UM.iCm(=A _¹V EYHGz'9'h k,n8'F3׭tWcoTy|"!}2?L+u+\2:p'`Q^¡:6Kn$GesKGR3@eb[Nh)s#'C;| + [g =2ZL8gq?^*kWv[[Z[th ˍ,6ucg=-aʇ^I'}h܎`2>`q 'Uy#+TɤmmdܦgWs x|E577|it)Pqy<'4iqif/Ϟ_럯kytTdcp?vqjqhV\sa' 9֧εo6XXtB= J6U"r0xy5LJᶳ7Y-Tú(bxݏ_5=&E.q;Pqϧ@Mr姘! v?SkfxK¼+9r&<5,}ƃ51m~`{㺟_fhSl2$ǜ?㷵rRSIu$Pȣ9x (o]q? -ZlYȊϹOMۡVi {6th-D~ZǓ>|oc[/^܏Dmdyj HOJI-8Z8XbuR P:|P6c%:tW#xC"o4KR~>aP'M |[j5KeǩcEt%aFGF5[;I/1SoFqs`%V|g,yr2+4-S^ M7jGQJ z8<u=B5I9'ן]߇5Ir>H<Y_ %Vѭs;'ʝ@d5oٴ.7kkEE8xГ̃4㭧YE[ZA$vU|s ?j,%Kŷ)s=+Ӭ \5>vG=7tj3kosEh#+V.~l簠HSQ4IWF[q{m\lYjtwsiQI>̏(swkVChX}&B@۲^^qwy{X-Cm@$;j7Wi4#}Qk/km٦ϳ?]OPݍs 3wIq Mqkz =!lc~P/ד@VjvIX\m uўI](8M积0+V]I4~aKqp ǡz\.m$]FC-~ ݟ@]@-R0#2Ax?vV:Nj4O0 d]rx?µƔM r8d=8=Gs%^I-sB0=y8~u),ⶕXqht5 ͥlM`~G sZ. yr΀$cH!=hv:[Zɑ:ڿr ,*ÈS;9GQx;'me y1y$rx?ZKi/EFmcn9?Jh).b け۹ɬ˦,p kC`9qҀ*]-J "`Q>ǎKSP6ƾXs: }ymͺWW 'չt ZXfu?syWVr[E./qgpF%S#@+:+K#.tK[:̇.:?'lq@R7QШ8 FsVΛh;'Ҍj\2L`ӒMqw?fC]]/@2g?xtf/|Q4Mm4"8$Uc@r>Qr1׎y+Ko]˥Ii G_]_'K=ԩS鶳n)kDK;mb(K#Swޯ[9f͚+d9n014?g`a˙ce#/ek1ZLcXO?UbgC#Kw_Zt-k+|6jv=U?{úȶ M>^ktxZ$_m2)¦mG6I zrhu]"@gawrrZ0VΛD>vMtcKֱe#Xqsjm's;JOQPߧVPo~%khV:M _|d`&󒾂-;=ΥDߗ ܜwCQ[vZ<|3Ev6DGmHٜ쓯 ݿ¿ 8l%coNwl :c8>WX!Ao3O\ҺAw"[~\+*m3R 3O6ww+>p(@li_ڛcIY|8u?Vo^Z%IV,F+4]\:pzt?Q¯j춀=Fe"L ҲaåXưYn.7. .pG zEpp4$a9hryݱߵwjrIiv+qQ`t$c\\OEcsFHOXEK&isr:4Inʺ=u~·zT'i ;F;0rACwZw*! FvqYw:~Kˤʐ%gczTF{eɲ s튫\L 희\H`qz뚎]P˩ߴFLp*k @I,p1KIボ ᝴TW73*!r#Y9V $} @ Ig-Ӵ7[$?$T8qօ娎5w|I;uKڵ/m`4D|wu$ ,KE vq?pzE &7S]!{k܌ұ89r#mKh5? {9PϦ[j[$)=y4ۼ$GHNI`cWQZǥq&n vѤ0h;Y&ȒAty=Gۘ`ٲsu?Jt6=*){BR$I=zY}(l#u/oC\DƩs%rorp:t],vM6 !żH p9=@LػSv `cCbΖY̦3QPw'սsRӑ 5 uY.'ڳc6M Oڼ;`}>UC$љ/#|Czd=,s̲F[q;FOJVWS[6OQII+=> h/]N(:Dqy3~`n8^1h~9WfFF*9?1VoGeKK{nZvp{t=}pq30Ⱥ?F |v޼ QQQ֨tU3r]t8?fɢZp(`N}hXۮ9=+Դ`i A\y?)}E]4ws[q'|xQϷB(Ҽ! ;g"v;&^_+2\(ӗ7Wriuml$3 qj:n9Xp;bm61F  |U! /M*``}G_֫EiY %7¤@ubH[2}2I-)b1G$wӷJ㇇!TKXl|w$ִ-E_,y&'wuvۢǗsW;q$<5![M&  .:Ilrp:)Ǧ)MuV}˗s,=OQ3ƺ(a;m>kqY@g%h"m~|d(8,1Ks\Xun 3A:M9L/LS)m qڇuVE$Pk{A~4z~UvI[o1L9u "lBYoۧJnt)ʣ͔&vBqקָ["c%D v޽YO弖HgByqXӭ/fHn5QO5лJ [Ue:~9KDy>g h;"'Rp=ȭktu:tR#Mǻסb !}9@Iv[  u$k˙EG߁<y8CLծRZG" #{omӤ+$-Eډ&z!l9ǐo.`[dK O${ K+Q.o@wF@Fzhn{cPm9\`@ ԓR˩-h==qz#֭,* h W9c\`v:u6v{{KĶ`|3G2^8Ʀsi#+L@';3եrmA B>20=A*$_<Рzzڀ5yLi $3ކGd>`LC-"0o=+Z+7[-P\1(<I6@-'=O n[M-uN>dTϧ\iK@F@}*Ks0Ue[[)Gcv^GA]̷iv+g`мXU+Œrk^|Ybl,0Ns[+ȒWxV@>CF-BH( S~IH8nOTdh)cüA<2@ހ7 n/xlFV^{5jfM4KE6#niNF>Z3CnL&qւk#yހ$ᯝb_0cm9+n8@Y%@.XlN9瓎kn$Xe Iqr:n;b5\FW|3q ͑9ԲKnӘԙ{csN1@Gmx_[?1x!2ytuc3@ NyKF:]I[݌^jɸjⶒ+RA<^ssq4^Tr;LkgK+wǩvP˸+xA:rGLq2EK(Kg8:f3%ґof{TR 7sϵ3Q{yhg21۷ ckRN=? Q[C{`Kpzz5Sm5QJD9ߎS]}p@bv&Yei?^fHm丹 yb"ncm_N@1{kb$.]vyl:t#m4QD I!]"d??Ӂ{?r.VX%1<Ztɪqoma {yeG8SV_BVb rry="]6+oF~0*1@$q+ϴqemr!P98AuZ["̲ݳn Ęzd9>Ʒ+Xٮd||v ~͆Ō1\@y.$ '/zԝ47͹խyK2z` =8 Kyk#/ z|juS^K;4%Xdi.G@_n'۟ʭKo*'m=P1!`zw4B'kyq8,Mce SInUYp&ywe+i@ENpO@C@mb>e5uCi FQeNp8UYd)=ݮ} n /9 Ǹ2K(lURkdAurvd+[Úy1XiVp9\XHچ2A",t@9N&'K󧱰P"I3t(?)8r|?j-#C#5a=p99#=K;f.C@%i.B' Oך49ԧi]Āc; ׂ{B/|Wm`(=x=Bckmq8#9kM5]JeTQlAݵy`ڲ>z` rO~`'ɊV3m$(w,kn-F=Nm?F6+' ֱ-f%(䐄21ޣێiJf-ODOdm$KoYMn0Lx8u=*6VK-nH$?7O_I}h4d,#|gO5w='5[O69Yůq1{9- ot9 ԥyW71tz]i-Ʒbw0SL\\ߠ= ^]dDA,'n% @7A9#5uSIsw6pdyw` {uGvfd-{WCu70q><}jxb!TG=g:\]abr20988▣C,BI>dN|3Ϩgm:K(dUٖ]GpK4M>#s3 c洵ťW~k,{(;K+in!!I'V~0z>3Hj"RxnjgVvi.#El\Q[9K4p_}if{H0.XSN{~b2N %"|<9?Ru+eӊNVHkqOZky%$n7x/yDf(Ҍ Oր;6m&Kh]Hţ\GxBΐ(i.{s| k,h{=H'1]z'R~\wS\f);Γ L] I 턎v3gA^;a,@$QT9sjb==䳉9q֬*>wdy[\ۧ]5%ga+S9GjI:M41b˄ӏz]2 ٤du?Oʵm=ڪE3)<:R"H.ހ yu&d'##{/o1XGHpw+nK/hV(wc*G銒8X٭ >p?c$[xbr|GkerN~Yq}kO mmiHJn+KJ9m` 62Y>@y#>PNm[r7^z d4CPq@Lv9=k_a"U8NOX.%-FfKJ aatj@C _v-{tS"Fs ~SOgP:|7%B9~'~H_hh"x;90GMKf$s\"4ɏק֡I|O cQcP'%בSCkaYvy銶b܈X^~{PQڙFg9K5c|nG2Hq,K;Hb>Ҁ2RӤ{#-~@Tm9mmVss/G1WB$ʞx`N98K?-eEv0x81@/s.p<=SԵj)pnnfĄc1 IP#ogo^E8K|Ì wܰ<6Cߡ;q};PmM1 I7-8B6 ZY`NHn"#2`ιL:^I/ 2߾}GZuC&h$1'<}}hti~ic;ۓLd{ť0llOlwч>cHppڄ!i|vOW4.6&""y3yGo}$2Kq1cm)'N;ki:'X|3 5."]R"Fc}^3jǏL/&U1ȷnn{kwVy8PX+cqsޝss5@!y>֥6#Yʂsp=zzPBdi#Oni-塸dh0l'~OBGA} qOOj &*%1=^#2%OIL=ϥ2M{ IOmv998 V_ ,kmdJݥ$kQ!'s_ojko|KIUÒ:dwjG\]uCn#߷YđF}̬]${vVI&'e<͏c,ŃpF_ZY//˖7 cάi(7ya1f9D.e*6FKOdZ9 H/ox M5廵d`(#Zϸ O5 Ã~.o,@CA1U?dΒrIY#894{ 4#p1g?Ua[]+.A\${d~uz[Ŭ>T0'qGlTgq%i!,ؐr} kKd!xߐϿ#үEݎecܲIp?zgp*[uQgcH~[3}ghDI@Txs?))-E"$I*8$;'5^@[e9BUArH_nc%FBn98Z׻b3̞Tg\t;N{P-H# r0qx0+i凙nH xU+&I<}2r{*̖nʍQ"pGJ'KrN~AH!I鎕C Μ#m.rR31Ǯ:$j{yDvG$~}hoN;3HbmZZTWA'ɴz Eԧ2kvd?:غZ5M 9@9Gt ŷukd3IL i9#קx]7ՕkɎ;< dp94eMO4.F||du\<SO7ےh^'۱'sviol Læ[-s]F Y~Q8oocI?IMOԒ`gIFݻ$1$rC%,_)W<pHX,p(@eqDlR&ؓ*HoAy6K,uk\~ A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs\|iTXtXML:com.adobe.xmp 1 5 1/2 1 1/2 56 1 32 2013-08-23T13:08:09 Pixelmator 2.2 7 JIDATX Y;N@ _%g;p$Eo5Gd%=΄qVt+Yòpo}[tof;858wu8|Y,$ѵ*!V&*3%Jm 7=DLl(st8(~0gMm %b TR[~h-\F=8NZ8rbbmBB25\@fUØSu<.3N`ܨVt3H,Q6u6yLW'PX΄K]KjAa54D,&Kn#ߠJM]G[?)*Gkrڐ&=@jlr Z$ :qZ@}Q_ʸ9S7u|ށA_à+Ԡd73u6Ivc-IENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/log.png0000664000175000017500000000521012641367670032370 0ustar jaakkojaakkoPNG  IHDR@@iqgAMA a cHRMz&u0`:pQ< pHYsnu>riTXtXML:com.adobe.xmp 1 5 200 1 200 2 64 1 64 2014:10:29 19:10:17 Pixelmator 3.2.1 ?]IDATx=lEǽ;َ-[Ɩ *2HE|` * Q@ R B@N68XD"ۈ}{hn?ݻ;FzݙootyqnZ 9DHIsLeS8רK G4xe4^w!b붏߉c^6?/B¦/ sR_3bʸN/E^.CuQ8_.?o/W*E{aL&S^=loA䋺>"MWJ(t?:hxUs詅=./QªWspF8+ HҖs;+++DR af̟;69= NW)|qq1;55u*roY3qIL;f嶙RQs@kDOE Je5Er@)a@z1 /@@.3f?{u8~^]1[ ِAI"OTamիg{N2}G׺;݋w  0'PՖ8?3H,V߳[-}25Yk9 l}u7a+zPu2Aa1u,W~cu=NJ֧B|;붫P ĨOVDEJmBKP,`o_`cH(1(V!Jk}/{O3 ofفBh0*{`p8|7-0-Gi` 0)`p~8#i~ۓ,V> |lX{!i+`j%`x-` h <`_Ikؾr0XS,TrRS!h4J~*G QJ {7'lp6th3#ۣܛi"7D^'qOܗP\ ؠ$] Jzk ]cR͂$}A0vn+2>ru+X~%}=j^[79͍"`d6#o;Izkm"wE'%N >Ԁb!z/W6IENDB`././@LongLink0000644000000000000000000000015300000000000011602 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/toggle-onoff.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/toggle-ono0000664000175000017500000000763012641367670033106 0ustar jaakkojaakkoPNG  IHDR AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  "iTXtXML:com.adobe.xmp 2013-08-10T16:08:99 Pixelmator 2.2 1 72 5 1 72 28 1 16 `*IDAT8핁 0EMq; 5@clNbtPDsB9CDsKHQ\/أB|2)T2ČfkzD >4c&6hq~LC )WN*e[6"[GF-zZ$=Epzm˖-\_[k_v)}̽IENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/window.png0000664000175000017500000001017512641367670033124 0ustar jaakkojaakkoPNG  IHDR@@iq AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  iTXtXML:com.adobe.xmp 1 5 72 1 72 64 1 64 2013-09-11T14:09:00 Pixelmator 2.2 FIDATx횱 0edz:h:t|^Ϳw@ 9{=0@R*kCKr)%`Q68u[ooop,""% #XDEK@$FˋXH` ."./b]"1E$\^ĺDbHu py,""% #Xۗಷ 0-Eo|ZЯƊIENDB`././@LongLink0000644000000000000000000000015400000000000011603 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/progress-mini.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/progress-m0000664000175000017500000001167712641367670033140 0ustar jaakkojaakkoPNG  IHDR((m AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  7t"iTXtXML:com.adobe.xmp 1 5 85 1 85 40 1 40 2014-03-01T13:03:93 Pixelmator 3.1 v]IDATX ͘[UU=cݽfI"L((zn"^}Q PiC]]H2#FS3F5{}nC׿׿.{ʬHZțT*Sy]7J72v!,ҋIχspd$᳤KGz`,AOQ>1milƖcu[A⥭mv՝=9t;)87mp[9EïQX4.rYO2`N/x^pk fZɺ#*j@>]:9s/_Jh{O`UFRGy4q3Ih&oIb%pϕ{Ȏ}i)oc\ށ!Hr^7B| S^k1 ǷpOHPP(Apjra ,mlvۘtW* )X  Zo??8jK0~fV]@X R/Nfw*geOa]`dQ&ѨDG^ D3ҿs(^> ,5ȇxx(h*K  y{^$CShT82&f`9 k>U [`'xeC:[J`343';0~+հ -cA\`eH=ބIh&v-eG1{`-/mIޅ&F6)C j %kT:sƠAg΅hѳ\f%3曉{:ιI'`?Zr4g7xFιI52@E80x <[YeXmF彰|󖁷BgÐ?-}iґZI1թUG:i^IkD(#`;u u2gu6/sS87aFz%IENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/gauge.png0000664000175000017500000000233512641367670032704 0ustar jaakkojaakkoPNG  IHDR szzbKGD pHYs  7ttIME 6]hyjIDATXÕ[VU8m43MEQiD> A/ITԃ" ,z(aB*BYJdd8:̙l>{mNTJJ< ,f 8~6^b^2mru/Fv5/|p# pYt=> 0t@K <lH)u FI ꓙF]w}u7^]nQgC5{VUw+6;ڪ^Ԉ 8Ni9%ed`Y}"cJqc1@SFwQ E~t{khudRvuXIjKIqsy36*)/S(?W3FKI%ah_ [i`mJp)Tz#̖" s+֩~R/pV%lgז4~ސiRp:-%+1P[ "#\ KԹIֆ(WS3iW% PW)ufo+*B7Ua[$3&7 ⲯEa8R:>.]jY_;EjuJH20uCX`KE7ceа&uziR/C( FwJĀ5nG* )3m\,d(P%XO)u3j񘏯э (g_x 8?3IZ|` zqOoc0lu~z?8,h0'\ujԛvEϠ@^.h6E{P??Qg !d7uXwf~\K߀5qU/Hu_l &@B׶Lܾ06{^1iUԞ]Up%[_GՏ3_hovW ǨhPG ϤNQDК.c?nIШT0j%s(`_'4j[ײxU~:*-66:33aՉURLU/zE7Gjmuڙ]*莎@iF7A_fV| lJ)z?ƒ:q\IENDB`././@LongLink0000644000000000000000000000015500000000000011604 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/close-ringless.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/close-ring0000664000175000017500000000370212641367670033072 0ustar jaakkojaakkoPNG  IHDR@@iqgAMA a pHYsnu>piTXtXML:com.adobe.xmp 2 5 200 1 200 2 64 1 64 2014:11:27 18:11:97 Pixelmator 3.3 BIDATxɎ0Afe#ކ q e^bѠJJejIITJvU%N @A  ,]v<`]}CWktN {s MvzI3q {b1M״{#  2/@(.<XQγKxb]V| O6F#> {g,M.p #*uDyimNz ϊm:@y)"&#^WdHo\##:F^kN't9״W6a *&y^@L7iC?iL{msȞ_M ?F~u b[ N?(kU|ZJqisM?ǂNf:G|#,A_*۩/ ; 9o'+i# 76qxu<;Fuk.YAX](NB' /NG'Yz_ *8&OP_{:M{ms|C >ϊx4JWh.Z?rj^y18uh׼ogt>$P-?{qӴOϾ" V>pv{= r' @A )%g_8IENDB`././@LongLink0000644000000000000000000000015400000000000011603 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/deng-logo-512.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/deng-logo-0000664000175000017500000077042312641367670032773 0ustar jaakkojaakkoPNG  IHDR%.bgAMA7 pHYs   IDATx%zy?پSUf̘1cƌ.f̘1cƌo31cƌ3 3f̘-Lf̘1cƌo!f0cƌ3f| 13f̘1[̘1cƌB`ƌ3f"~70cƌ眻6c7n{1u 31LfM5׍(̘03^ް{aƌc&3f^F~r̘q1f0cƆfckmP3)1˘ esٟy,\2_˃k&3͘ \w9M a7Ʀ?h3!mLf|+ y=rӶw/0j3*q8k&Na&3Z\"c3ZpMo^ūhu x0oYU2*Q]wUZUC~r:s9g{31_f0 0a7%Ӯyy]uU3җ1 D`ƛLf|mE,C}ȿ*8i眵2/«Vm_d/k#ME?6ykLfqFÿwɾ e_O^wsO[?Ӷ7=v\(n&h22W8w~0o 4_?ϰozD λuE71g*LdN;g_=q~Xϸf`k_@_ozsNuY^7 _1۔`wgO33ZpMUi翪ѿnp޽wuei20u$WdvyLf f0Zo_Mu㭔an;-Ӳ%h4- R<-0i9iy6-OOsOs_RM翌ˆү8_M񋎝<8?g\3q-!MׯB.58~}]]ιJ)wswTv~%"R8$7pc{9z:}/"꽾O9ȟ&2;m}'qFf"02 W6zW1#:W! `w~ 1!fLO=_mK7}&xsig;3 f0JxM}9x1ƻOZD?!"?q?m? !Qr y{Bp;>N;ySg"0a&3._e}E. Ou]ӥT'[Gzտk5bG))"c=1nO3A>:_+f0c#Ao0I~ھ=cifM>FiFӍY|pѹg;gsEiN9~޾S1/c&3.%EA?:Ӯv]Q瀟gK)?Uݿq8IMb$@۶!Dm!R"x 8l{T"wPEPjTQ "R9Ss!0c30PrYF%[[[4]bbZv-MY UTu0wED΁Z༣r\9< ށ'BRKzX0=zu 9π Q>i7\Gx k6Oc-?0?uڇ~U8Xnmj{j6 UAEnUJPUڶNUU1 ΡUP^w<}@~xGUD ABQ@^" srxp>~OY׃?H3-?F$qۧ_Gd`&3^f0h_d7Y4MljPkJ)6lvdww6-vvwm)<JvΛfOQ=ߊ GPUqfU*2VEs(9x:RDwM 1qIA`EJ?k=~A?!? þ xĀ3)ՓfcLf7m_^, /ƴ,Л:vewo Tb޽q:V*!Edh+J PR N>PU>߃y)EU;zEh/z,x禎h| )u (.f=~ NP0E "x 1c@hdzOxϟ3v`&R\su_9x>]nܼr{o[x{:&b B3QbL(<8hRyռUJG>Hip{^'R4f"MLpi*Mg?Zo?[ |wz*{7n{{7n৶9GKb#>x$+&B$׊o >؂Z>̔)!Z.1,TgE|qU(G6Rdk9e>D T!Rjs C>y0|֌9`OUpL or2ԁrT1B7E-7GrE<ĩQ<}1O&~#ව I:?k}e?ד b&"p2}mLϗR~ _qmV;;wbZ|&1ѼPmNo xR V}/B4bJRRIM"X )Y0h)SAaj>VV'~2\cZ~pt8jt6^ĉcr}_>a6(7k/uܺuݛ7q6GR MJZh S 8!H#IWOj#޿(.kHV]H,+j,<>Ar1%?,dž]t`*BӦkC1a)~j!u;n*` v~) ^ 뾠*S18RBPg+RTpT!#"<(BlTDE)ubbMX(U\ nR4,YpvsL|-G}|Cu `ד F`&`\_GUiw0 ݸ'*bF;,  ><$|hP(Q:hQ ނPEi#OvͦU§ddiv6CKJZT:y U:UzgC16S" vX:fw@0CQD#bB{ZqJ?".L#G3YW_~xov[wp}}!ۃJ$J)#8Lj;JbJ8IN(A{S1xga\Z[pt]eZϢ]$OP\!r4)c}Nc!xr3=Z'8=~ >;{7ܼmבs>.>CuyMKJS>5h-VtlhwJt1F/ND ΋:|_%-0o(f 2^u3KO)ZmMíޝٽyZ$3&oc^_lmXD̰ۤs6Zjt!mV /V/Rݲ;KhqkK 8TU—LZV@H!&yf>h8QXS+TX bԗj9w5o]Qk&ȺTèhɬTٯsYK8db yD0 }?wv>EA`G몈>>z4Ez9 )*%8<DSK O y(,Z&:OJPR|B-G? 8=8WB 36Aw$Ôw&Bb=CƚQkvV4mKj &R2a*S$i4}0):B GkL_0fs:iS9 &Mgxt>ϟ< 5 Duzr`&f㚌u71G %65q޸i{Xoz4EpfBSyJpr`!z5GL-*`kّBJ\R"Mд-E>hۆZ=:i iMowS_"] 8S: HɌUPWqhiZZm=#>T`#*^ E pF"&>M^XMJjr?R2닙Um+ Hj,ۖq?` FU: (}!RPKK`z"*Ldhuw&BG|!}͢U M3:N8c$xM*=_5_&Զ|wxhΦUVc'W!|i*Cix#Dh"&ѻMӰ\u,t%6JK[ZEւa]h}LVT ]T#$ka62嬧ȀVL_d]5)M+Lmtl[ ZnʼntwUkFjEOj%S>+Z L΅">kK*z#CXVEy=;-"8+9ƾ{ PUV#ԩ3Ed-!RzV}`kB7Yb7T&bjb0G9sOxg Sob$pppglzr`&o7fkOo?s·rGTl¬yG!}"ajSmM)JL")QL4-nw]H-Ie\<1ѵ-c]⼣ A>Jh"RUˈkc!Zƒr_udCsjlUyDMགྷaMx)Qxd"!ErY}l#сź:ɇQJbzϟS).jdhqUk8g @gBh)69.S4xD~EHрo)f5](Lq:W`ܹ!߽k 7rY.EkajA#MLT9yb hRO m*ޞyv(GV+B !4 ]QhθbKץ)o9"wPX*!5V+*M&U:ˤE2M+!qL~MJ4T%g?zK.1dEQ1 H=49To`ׄdʀS)(~ϟ㴲{ c5hb0䞝=vV_Pke|M?ܼbgI)#a&º? G?MKm~2i P֣1QֽuITA|)j_ 9@u?lO#EKpYdqџI;x!27ܼ>w>a̕|wؘN ,Zt`yڶ |]@̢YZ߻nܘtrJu'[Tu4)bBls閈X=8ZkUJMT,3Ί5gSC¿Ll;ʘMt &lPfQsp$4AOJ=pSN3;6NvW2B #MPC:LjBJRFe7~q8Ν BVKr#MlRa@:}quȌyje%gٽ_t)QB\)H+0]@kST+N=:xDT^@CT'O'<~?Go9lzMD;u ҿpJTߺcލ])|$@kb4αn28|p1V2.B-mi&$IQCc"UdUjEb"id!Bm:GEӀ# b8ŊRF%j6<N9L?()s zQ N=XKx  񄶥uF|A+Zq>fNqGpTI ωM"u /Exƃ}.^L10#:Sscwݽm4=nH& *(@Lm-| M])ѶKd1\C汧jS%F@  qc 048xOqL.r{wrx\"3K1ƿUJ_0}}yܗ_hk^zǗ9fv`ŸƐia_ϾGOOg&\al"`i}`mm{5C?zcRL̕R2x3Pn-ɽb=PYI\H&%ǣqgc#;ˑ5jIX*|q>. X4 f` )FN%03xKY"3?yލ>⃻2#q9ؒR9gΡ8# Jʹ}Kr.t%۫mt˖ij!kmOS_rk?cžLޤpe@Hx(Mh U=j*YCL-PUqdj"Zm26`v RT1lAe@4c|LTA3ߚmܯwH>WJUrnc9#UaH4Cre,|g 7oy+tχDz!kd>}DW.1הL 1qxxbSk@9W~m`B8 )p(udXE9Tk9&X@jIӉrG酢BEZ$9PJN#؆Ә [` _?k1tXrG[9mMё# ͕rxegwZ'?HJ q},ڔ)Rv-^=e%yaid4b!$D-\lQʷckk}TUt:OMDTO)I޹@KbtUW3Bp6O4?#En1zS8EGG5c5v1CPZr%:?  c>{J-TqhQkC` [o˝>8|pk #냑?dÃCcuDtÌV>CTmmr?ؾuDRxr^u)!yz@5[]IH^4'g1m!6{kN?G`(6菩i>g>~ޟf \l=pr9@}'L-+/?ico7κ~1~K".GB*cJ8l]l(YY:Ţeu(#ECAjk[[KnI$ECpb#!:kCɅhڄ3ݲ&5N2[q`kĠ:a *"C#UV-*NqƼL,U !4vqjYhWMaR+tD]E H8 @H8*ٺPc:G@_J9rxZ/-Rm @`' =y#9Xpkg[wnΣ+2֞gZhЭZr?ƇkOcw5~`LԡL\D4gУ1Ku"Vov4XɵN'2}tj =O0rZrqyHgl_L Ny91жu?pZm|~]?3h<EDZ+z$.-3tl:J JO^gS_tJ8⒧N"D* =G~Z(п )5<-Eqx!H~tޟ=:(|lR#pQ4hsJxӐy}//0q |=޿!{nM#d=a$$#m MCj'u-m@,K.цksOv"Z ]c5Mk=R4Ɋ UVۦʼnBv\c: XVT͟N@J6oOv4)G -(cPFXHAE,=60׊ӊ֣B4@*/+gE{:KA#2RkaFJ"܏y_OÓCȊACKhPjg.)@*}JjVˎaw#߽ۖÁ#Z/ ʢY09 H(jORd(9`%6,iȅmhTOL ,}@?jŝC ,6HF0Xˤt]Oxg|yχ5F[NleR3 x1o ߿Iy-}// O_8fnGq>c!hI6k[X,q ! pγX.PQwv)ų^l^|LrZ+ &C!:O"!E"5,0ЦD&uF"BԈ=ژ(EiW SB8VδCުC#<O-,TY,2=o~J&`Ce&_AE(*< ,b#HUL9韏PJ,>CUFC֑TA'Ec O@pή?l`s%Fl لLa9HT {~O??88Yр,M 纀w3 UgU4w3[g?|㏏+.];mGYt_,V"֊rJi;mבАjYDJ?ZnvѦ`!S9HHJpB hʤ&(CxE(T'"], EO Qnt%Rt͂Li9%u9yK=U+J Z '-JZ4辢Qlݼg!}Ţe7V+\PWС8㒅=\yOxzpjѰ-5afX?g(g쬶Poy SCj8RDP YH1@ &c-m<9g$%ib@ N)N)bÎJM-mJrO?DcWXt-1BAuiR$5 R UlJҤXN5W%u Ǥ 4nַ=ŔnvǃfT,o\"G~IFTg#1M5c8QrǞb44g(m >K%FB{\jhڣߗP3U7ږ'OI)!gF19gA<8'w|w(#Cn w<ILJ! qg:L2 rRr-I͈w B Cflb"^`QKdP1!F=gOm-N#jN `&o!f%4ב? XT~wm84}.tDE DOmG ݽHtݒb ގ vnl :څ)5>pt݂ݔv4Mv0D\-Ħ%p\̖"Ul8?Bתre)y'6/yi"J@\h'!#L~QPgy"0E5QQh*9*DJx1JMЦh%h@xs^9Tr.-MnGjN7LEn׸`)Y {kұTrHrDrav`ox抔i#a=Ʊ}0mqDO?azݽZQS 5x2QLƊU&k`) V*c!Hu_NG ǁ@B8-4]7 )Iu^9ԢIUYb̟h3_u~G8~rdѳm^:׽)tΩU_/5MU\U჏?fk{rSy4ާJmB t݂ZVR lm{7IӦjo BKhK[WPKXPETbSuh9$7Rih#biaqXWk0[&`YҀy(zSs7<>.=@{CL24'fIlHHҺU. 5&Z7z)DJsbrvZnmV$>:nZo}$TTlHS XN>MMgQ/2#/ x}X9"}Q߿R?xgǽDLG"˲S$aOJݐSG$KL9s!M8{Ô9\C) >s4b\)xniAcݎD"o  =uZgO7'z}ȲwR㉴,ɛVO<;2S34vmn']a|t"+e+K nVd NgBj#FJ0{pR#a9mvZ'/3{^ἀO~_W;^|nWX, ߼A>|wNgyXg\_"y!Qe;p}'̣+pZ9ykZ72MOpn7C MbGǨan;*Ӕ<>x Λ6 u0cq;Gdi7QՎ 鐭yqTzgd{&'ʩT&~y8FסF>S ڶBg&$~G?cgxWʳ;~ n>:G7F9A21vÚ3VJ/ \![FR1>;1&h%0LIrZ_+[b_|r4)5/~UC`yMˮs:+]5#}ѢW>Bj? ľt3=4s7f<~)"Cr(D7Yrf|#G{Ji׿Vֶ!n> X=p> N()L }_uyY߀7 ;\71 1e@: Dhf< o Qf(w6Џ+#ڍOT 0l'bĥzfa\'B0 IDATtUvVZuQJiЁ`BgWUƲnl!?f /_v$*ȁDu5*|?Eo.`S;S2<#p ZjDo8\]3LJ!e O1 Aw7ep|o> mePN(*R-dZW#eXUl>:h~Bkneh]ѮƉ;Sˑލl9QNEA]W$VzQ i#L3fJF'reJL34ݎǻgu>rzu_L@Ԉ(bXP1itsúQF4 A;@Rk!@>Q]̼aI/?oHE6W^r^7_! w7^e7m3)rvRBexǏ%qu8)gnw`Z\FwJ'~Pkg2aq)!DA(SefT͉d|{OkN-kÌG|Tw>c8 w<A8"jHs+3gey @)cMTSK7Za* w&؊thKvڬ~m.lfضut| DʶQ٘䈄/@3AM"7j->X7N#ͥ:N;uGH`&n֊bMc"䍬d(wbM9i&fD~K$ڸy|EivtN'dSv"VInΫP ЕRGz)}-4B ÎBڨ]IAPچdPw'Dm )gRNٯ~ͯxz~tn~G/j^l4 K^7_=ߥJgo[n'usxuʄnW{iG^9>e\%rfzkR@ÀsL)ⰶs&DL6WLً6i4b}6Yv3\[Ywn0Q[S ;>V!e0>eJcSv.)d㦬ӺVNhHX\ПThW}f}z(%ՂJVkS0ON__x?66^r^7_T?w^}譏ayA@ faqcvGb0Hs@"S zG2O9bSNQ\7{`LV AsgJiß|bΣ?uB]o6mKF! hu_x&>]pY1BAH<X@192x`Qk&;UH Ne洞/ёz,&zWh06J-VUd6D=S!udj1n )fQ%@7@̮v譱mV25W;1Jt)ww%eD/h=1%b#G登Hhxg}hNdB)+6ZX;k¶D7w=Sn쓞fpv4%h*K噲8gVHjlz D1 /?o'>[H3"Duxv _p<|p_&]wtD5剜31F$DD)τw7K7vrMy/Qu]8}>0|VWo@g|?oE5%CqSo 6R;bn|n2st 6 tb oy V~?g[{~`kښ&XP~ABFPZWʶ/SUh|,ȃ^#_z|/I/E6Ljzu}4 DN5)Eov~DΑݴ0ωy}M@yH!:٩˜YvNk3M L9V}~wD@Xb;;n+83#{εUŝ0ڽXhIpU w7PNU8uckm$"m3%q-qDPVE ln F(`kk'g7k^ٶ#B`lusBtBviQpsZ+JqN@ޫ;=S5fZmN3%HieJȉ($@azw- }! I{]u1T+˲Cr5 G 9G!$B ӊF̳CBp+}R cL^1S'*ʳL=hHU!&`g?~m4E n ﹾ2߫fgġϬ!o70O̼!43&o 3y^ztMPeT%X;,2SHNJeo]IS=F&x,/|vsdx[r>=E xNH\ĸ\yKJQqت6 4d©5w5GZ'IȂmh@mȑfu$u+Z8%y6iOjm&YPн&mϵ56bȞkz!ƙxA⣄޻Dm\|,Pk#QJC{qGG&Cke:ΪzQz28%Ъ gwGoVo.u&o ks#O>eAÞA|JHxVGsSbʴ)a>f[7RN$_2i郪!_Om|re |]|5X_c__V?04?o}{.~341v,L7M4x}yd@[&M1rR\S`!1̜g/J=U ChFE_Ok;`* 75┽'u:}(J|;#"v|:t 7`g u_ p4'In+)z_ !%0'uJR/HJIv{{at,qR >6X ӲÌɋzN#]vS <Ѝi7a~屦eG IrRG >M'/9b)0Pȷyy?nu7;y n_x7>]ﱾD{O>5o.7>7#iqb1%o[#_Ox^On s>pC48=w\C?_~ ռa?_kxw%͓ACt3b΄aSp &1_Fa7_9 Ujwaf29h*\x&) $[1JkTEOy"CÝO[N)Menfa }+>b(h# PJ@0cv\3s^Arԕ yth";7*֔@&݂nV*W y!A(m^ <2>=Q(k <ٰ[qHBQZݜdmL|ஞ{a3aU7*ĉFGR*D3! VѨHg)$34 <~ XXgw7ATsraJ ."DVTSbL͓VH!P[)Dgu}#n+S}b~g1|yZ1u"֍Ҝs ^ !R)-h`ՄWZ7<xc ޏSDJ ꨃ6d)Yvr{pv Ksu-+b#:HA.}$5VQsWF"œ3_/BhiF=_/0y?{g!z ļ"iޱ\<,|$}aj,y!cXw=*1:SztMkN+g'SBßnYoWyrs} ΢w 1\>!2 'H/v~(ߏ5 L7!JFPw1ivA;'<<}x#{sޅ5П7"mݽ 2>]| '7y]`S#d F&@ݩ#*`zATi"~J_g2HG&aVfEYA#q>Diy!Is?VZ+1'zS[@CK6gm%sC;e-y43L`j)BB'z7m]I]ch蹁К)11XE%w @0#;ܓv!W~F- O+:/ng?1BJ6LpEf0HMʃ'"* xf07|3xg?C}_ſ={{]*#MXG_pxxÂW /oyy?7[d$rK"8Mia׏G6TI/s5)fR n1Eiϳ+ D;1Mp1eyХ܃nNOF;wEwI@>Zvox!\b~x{io#F2NUѦJ2 W&F/J$nIY?nnyޑA"~Ywumջ%a]w'H6%`4콵Fm}ȝq޵rtR6L`Ju;[cZaJ>P)S`-S#;)ӑ|JT iޫjg'uyꆙ8Q"JCƥ 8#u#y$#yQ[佟Y/o: *S|H4OX_cM?vp1:.xo9ㄅ8f 7nW77<~reG+$yv$"U;ni.[)OԱӴ|rF—e~?\ӵ0d}\^s}ⰸ=/H SBe)8Pd~F)HTcOmLQ0-^L YWB֙,Ӈ' m+д Dv$e 4s` B)^4jaXi)C*^')6? bFЭQ[!MCVbߛo'*;>VQÍx?aY2}۰6$PY7 J37r\['JbD3eNbwT"o*vbw)E8l4/3X'nc}z<űjEƼ(;4L >8ҥ{9MR9k!i2DŽOٌ!Ëy*64Qۆ-ȑ^L(ե׏oAxϝF |_rU!B$5 h,"mÙ8~Tɻݘߺ?D2ݞ4MpG5Og7<)˫4OYv{sߋ ?p{~Sӟ=^CI0|}݄fEhIsg?BEcdE'iW,$3&ċV8D63nl;rJfON-0I^:w! k%&6da֞a,qs )7Ioə§)q;=oh:iR1W~zE=4:}Hʶm4oy('0Iyzʳ;y& d7zm4ke3^ĕV*fҴlڌ8] r$Ri|5@T| ^ aOXLE Qct4d<%<'y ӑ)z6GL0m!|3!PBWϜ2)J)'Ą w޼9ELXw]^|1\n|F^"b.0?k%.)M w (쑃8KS'?ZCˆB O)JJ)F'IkpRyAAګ6 sĜ>FP%LH$Dh*· 1)r"2a-Z{=O=?On;49RYS_3>Ϭ``gϟ,׆{Z'H9 '}S<scS'㋧OZu:ތ wu#(PVDK齑COvUٺT!LdXq pkut-% :beoaBn1Zac?$Ї/VGosa%}ֱv i(w+ΪEeH9)զyZ_Qr)J3ʆVluD‚e N / U4&+esu--oPS㓃Kdc Gޙ3ZE؏U1AP!JƺR DyvCtuiaV9cёsD>b(X/r )#_ x%2$>"ϛxsߧO +OX=&zIVw{/˲=iQ5ƛ>%x[ODȑ`10ޛLhru}uIwK)]ȅ|?o\&۽t?ܸ~4+[)HMv(;c)IS۳#ݶ#DHV[Gg_:7La-?tCb>p&Cڅ엢n!8* Z TZ^Vj=B&H$(Yg(euo $LDJ"-AhGT6RLnw|NbL!!U7rӂVt<޴71 Z a"ڜ\p@+=rX"?];9H-VGc6 ajNFE(0B|JOyG?~|&e -/AFO'q~iG z4anhwv{<͛or cQh\{, ׻޻Ȗey^{}DDfFedDFf T'B-E1߅|̘`."3nfb]bGr]w3v{?6[w[VlfH9yJ#B7:jq>T}RzYЪ߀#X0m,ySDеSҖFB)<KT%A}"m k9qrH*4:16uYgYkbΛ v4ͤ<QI9~79yό.xF1|3SMC)$u9M&j@#9Oj<戌Tʜ}Oz-Xkww?n j-Hw=-.w5ػ.7ÿn_xwqO>ݞfUO jP牔g6~ :)yKa&,i %*}#}xwk0t#3Y?>r{һnЃASj OJwXLViҌVy4&R;Rh[}ʉWY@[;3LP  W[[ ;/,T.&]IěהZM('1X!Fv\ ZaY{  *|@1FkQPiZ(khn94_c#~dXuAy"D-ZOr,/l ԃ ӄّ7\^yss5)Bf->XOGPL1ܐNVvӎtA=21 5b<#RfΔ2FXVA܌t 9n(v"(= 9]!o -߁I"L7(D\5`CGZaܙ-.,l(z'&J9"EA&~ӟRk}<W{Puxj.ny|/?2< #|Gl{>S6URN46iސ2=ϞqlCTżeۀbK_*D%:vULoe{0I|Rr(1Mlh6~#wsR?yFq}!s^DOg1"vП}qjvx T Z @/ +FەR;>a%k +V/N4*LxIJfB&K+Bܲ~I_b9]p**AN$RHk 2N:Io: .9A={C-\ @sLM03qYcu¢ ywp|!b`zJ_]]0%Z7qσy c}7GsuRQRh^ǺRO+"ƈD4(kV5CJՂz\W{dµGa՜>O9<&^/>?&k=)[||pf?a9DUn" S !E4qݎŖW~(K[?x mk]p BãT ͠#<'*T)kevS8~y e] pbݰHQ_~/(ke=J+R|&NLAC:i?| |Ff)G;74*;?jSD^x>pElQkc-RT68"f>@_f娓1̤u1(4JACDi7(k-wD' [;eg,5|S $Mjkc^jn[~s}#Ñzt8rYYN#B4G:7noX^x:b@S@0 fOl0 ,#I"Q, 1e>?g?amkZx.$]gqPvK҈L=}ƔLhBHy3ӗfl NV|Q1y4J9{=(? 0o?76 }|#6;֧C!,B۝Cޭ4,((SRjǸh_ȲvP 7\Y(o>?ZA6JzSֵkW^嗯5rQgqoy9ܜ"yB<=5zBRJq@U%OVf/`0! H Rk$8s1WOܓ뱨Ҫo;:93Du!%Nˊ4SRRuӝӲz9)4D3L-ZWobע y QȔpӋW4^HHJ/}}7j/$RPKcsyI479kjnqFb&ĩ<)7bx`iesf)L= N!%/4¼O?R޶ {5"M+<kV_T!:o_?4D> O` S9WQ%D&Ҵ!e./.x6o *-S&EwGS|t.:#}C_n}so-W"lx{eÑ[zh-}4 IDAT:vg9;PPŐSEM#=(u=%eN?Uެ0a}b~}?ŢrYNzD/ʋ힐}KQ]N/y=_~UEk7C8Wbs_zCl$({PċV}]uR'xQԬ[Uw'ECél FQ6NݴјQ a([29)Eyz<8ݺ)Dfκ!(iK%rbFI4S7xð52$s(qC1{s`WD͖;]\4n=-C.L9)֠ 7Ĕs?&*Aq~BimL:s.$*AڐO:z%@S΄18TX'#J)Vaheł߇譲 uzNy7)+Tu%idmYa3o}ܡއ1-U߁%᭼ŋWh^ |ߪ>w?oGO~s}W }Q!LΈT"W_Rf`q$e]]8 8[ZTa}6nc;yW-o?@d谐5\uwZt=W)Nb*!}tED«ֱ>[g3zM(U%(5S.sRO|5ս_^\~9-278l_JzcX9rs{Ͼx5/yrugNJ͑[9 E$܄h=ְHVAP[I6tE\g7OM#_os~&H@"{]|xjc _{d?c~?͟!"]\L4`;Ml6BʨDMFŒb|g|3K4mLQbNXԌ_> 6ݽv&1~Fw- cG$hcN`ݻaUtG$)}DpD[ Zs4Bh8XG۰.f {:D$9ї{尬tu[T_RYnN1(eD.K7^cK3/.Sfӎ,/_~?K{׿5r3 I4x9GzAP4ƑDm1^+9:uAĀv$!MuG7D<Λ71E/2cXG['Hmb`NlFuaLц6rN=]; $93]H+iL.UTJoBF IdsCZ鲢-2O:_fvJSbۄNU7m-n,gG4";N MCc'O;#vc]V>яXKW xy 07or|o %Uy̸cֿxOYuĹ%B͆g4[h -ݖ /w2J 7`J@C Ι`DFc-!I~wg4uud 8LP X]}6(kw&6#o7ĵalN7dOJ_m:iaS̡5`Nܮψs+= Y8+Foׄmjs&6Ú* Q3oN7/f1Z_8ݸ-r;T(n(;!*i%z>B) zQ9 ʘߟ61:sX*)*- Vb-1NzJ)֠ͼݸCk+kM s ΉmADGڭ7zYPsMYʃ):SaU/8GˡGsk}J\+<Lj`kqZyᖎ5`Ak#m7 :Y9"Dº`ݔYm(w:9%J mJV1qKiu候8Z>i>9>#ڲ_o;\ow x[)]/QeB}"7x{|HUn.wf&LNiJpsyyżٰx)e^^nx9Ml@NngOC9zmL7_z#G~2+`ػ?oj {AO:s͐`5nx1gߎy@Y1)֨\Am8"c a\LYxlJ[V\ր.m&DH1Ӗe]vRWjbR57@lW.X |I-FN~iAխ^sN\9U5ܡuz/t )N~¹V A)#Qu7r1_6b8<Ց7dS͖-7:;#GsB XiZW,@6,?ёغ;ŘSǯZvTgH_o4L:8%GϚZsps *<2|sts4c5ݸUWpgGVI&h^AnF0DYjӠ8ul nl>&2/o?vNSO< : Oe<.*_9EcRyawR# 8 jgT ܲ!̓u4B 0Hwr|.ЏB9tt0SJYΔvDQ yrTCuwp ZB;҅ݼ!u!ª#N'Hpk$ff9䙴,J/׼%hk_> Y(F-b$N֜I.jX!^) vQZ͟X.x4q w3'u|Ы1}Lbpŋ<sgQ$wӈAj{vz5G1TZq/ynΉ(CB z $JDG݈w3ÉeYqeAL+`9 Ĝ(5r!)TH^htB Kޑ){ =A( )!9 'CB$[ _,OCo/C#nx?|bӟ1o6bwSt&rSv޹_0ovDnb`3ҍ= i9|;2;O󈚵=4:=0bjlwdư_Ju6XmxSk4B9i2 7U.6cuJ7D0 v"pZ S ,<Hcw*) J-kt:9N^ yqKމ7GN.7oK;giJ>^|g_\VI3ϧ@vOAX2ybwӠnŻK&D(<ƨ93ul~UH #qJ"IPTJ~`RA(/T4 ws*PH12 jtm&>ɕr{YøA+aYIc}s(\ _MHb&.K1 spyq*+wBtz'Du54yli9Qz[`$1M[$)pyyn{q9rx󚺮? q%LJH֝4Vx['Q5ґ썝DESD\z*40k!2!DYg=$@Ndofnp51'Gɭw1 e[w˔nDp_@tlӋDtr)-#\5.U!@5/J&G Y]=f:~%)ѣ+\Z{ $LB@Bt sk #V})*7?Skw?LuaC:#Рyځ 4DЉ0O6[\NL1ICp+Hɻ );g6\\7re3oўJ.μ>?Ͻ~ﭷw]=U20䆾/gÚQrgy^l&dǎvR!VzWAmhM/ޜj<{߸MK_Is$ʹV<׍a;q:.NgFLWknof|?88R L!{R8rZҐKwk82Ȳ4);`/Vkm|4J ͙ԁ";Ҫs8b qV6$:PZ;->)'j)_0oA;DϪϺ\.Pџ9=QZ&ڙ{H#ki y !_z]h&ep4+ d0`ú `G>)^X񜽻kĈTGʩc&ͧX>2sD*gcn/> CjDNCa%GRM}N ,}!}2?QeO)Qoʪ:(myߦW]v~шn,2M)D_di`y{pi` q* eIR]LA6"aw"BP[㴜%;bk'4Dܧ4N]^:փ^01鸸a\^^2m'OʋK>q ʼnYW{K4₋+!Nܞ8.lp)r2 `!%x_!Dn}&eyF`r{sz<̼5'#/P/.e`JnZf ]Wks RXJ'8V?fJ3I6sH >" nĤ1ęM";gw# nbwj-h5p:M[^J] ?wyf.'v$4ix(QirZtvobB*׻;NQk>DyW!Ouwy{|~퉏o;?7?RB=yCb"rR0$fxe_œB)HLJ SG*)q1' ]jx䴫d~ 1u=899nߪoo?|>Ņ}yU,y)2O{6vn{AD[D: Ԙ>{n$u=pJts=<~a&'CDѮ?u:puW4=/ɛtn> .4iZ? IDAT1x3AiON=tj)l-r69](@R]HJ7rPǚ>6(,b [wݝQ2M󍵖n73?#( 7q=N̋vl-m]9GF?{ΧϹy}ZVn]u=VPPZ;Ί1׌66fcp ۯ`.3"Bm}N/íB4Ni "^ސ J2e9kCHA+tJDz%mb+<߱ALY'>[ǖy3!%#٨ºTB XWno۱3[~L٣co^r?ĘX֕[y1fTn8.'nGk57W$ (3A͔ kCsOnfwlzvw4J:\w sut^vb(AR 'd(HxNEE,іݍ l1_ܤ(;jd$ iGȔq]a׽4@C E5_Y k5R2抔۰K!&7j3H3Jn4)Shy>g4ΣS?=mw?S/=㣏?vI8Lʙ)49y( _^1kb.䉺v]⌵BJi`o/`0$mj^o6~N} T\kz#. 6|(eat P(U!IP!RVkT2eeCs"V1KD%jmw#ՑM7ĜH1beETx?şrZn)b, {9'ʺ,' ZV^}`/D)87jK 1V^P I.%l &Y%&3N ֵqZ *rwًS- dKΎ6:qY=EHHrSfn=arpdY+ٸ_G4AE1Q;BSbH8y ){b55 LK*Q/DaT.1ıa8^jQ?M4|n״Q,Gn nFin\bN)tGIk#or܂~ׯ^W+? /x_YQwN8:?$}o_,ď?fv#J+2'mc hW&-E6=sR\5:= "n8S*,#̂Б& R,bqvFmknESݽO f|DP))T"R+eYm//Hsʱ`ո>4'gQQ4eM;\3Y'R|+س0iIeŘC[2`A8=V:g's(Mpn0cq>|KE!gQ_q3Ӌsu< "`<τ&?ެG,=s޸1ߌ̄fh!f @ KE !@ RIZ(QdvfZ<}zGDVgKu,ܮR+w)OL[!M<2n ,]xhbX֍CBNb!5FP?ν{>@Dzk<WO*NnJ<,bSyQ[c %MxG r G0;W n_} wwwxOUYπcWQOa&'m?|Z/xuEh1%<4E] e"ĉ"7H"3ff P |8ж4;gkN\}кc7;~f;_]{j_|/{51g AP>_x'2≀ |aAh͓ish'!ԻL=` uت ̋a;ٌNw慦', 1vUmPS F7 1/n9.32q[V2q:{7/ ܟV'"29mebHʺCo#C.[+K.AAaHv i\?KJj|ZPtʦU7I)eE"F'zava7/Ї?֓RR!-y"l/?yI[e^kocJ~:Ϊ1kijϩFmZ*q!QJJ$x#f90ݩ:8XƥVdBOcvہ3u;R$D 'N8SM[wRTQ1Ro~K?O?+SawYQ ?.O6xf{lN]x?GϾW~F]Xer>0tb101-G7G|b{'!Z7b3Uyrz [{zGoB>!:vv E0cl0@| @XA#aN!Es!Q (siEDQqtR1uV"EhZ (B=]/݌vB;u'}7oSkAE Zaڈys"-Ъec !*6([Z#A gԓA3\U6¾(voG|!%9'nVw9! 8B|Έx7D01G׷}g Gd$R >Nٱ)>6ۘ]S*M'⌙o1_]ҁ@띭g@_+iϬCT$Q+ GеnFH}[C5TݏA՘tU93ۆzfuё3c`,%DOŇ J ̻̥w'98Ú0Bo^⋯xS2)}FO% ?.O2ϣ\?Oq?/x 1{^{s\(u#O'#W/^q:kd$"Øi*6r7>ޛzlfg۝}pξ4Q?#w e%+>xi3I+N0 S**)NJu[%UC TH5ARNPIAٺ F9\DՊDq~ 96ֶ%Q34_Y&ۥzkwL]<8qo߾ h>gnL#I]#NI|j\EjF YNꀞ 9*@*mN#?^ݼ/fAӌ8hޙٗ7 a0H "ގonטOvC#zv^z۷@%x#DQRqX= 6y S㊡[neGkB6K 6յ+oի*slmC#}B4q:U$F W8RHsv9jG8%0P[BE:9::$)'jx||oIX=?3 |n?kG=}\}_0-߁xqՅJ|iX`vy>͋hW6XRgL4Z1S=~\|v~|%}"0d[qns&G@5CB8\|֜'q x,=Ŕ<$MJJ@OF]:zjђp[>VYKgmc&$Nz`lڝ&Bg|^B}O!a2ϙ A]haGRMl=}˺\vW_}J(}p /o聊rbĘ-w?Yuz),Իw@5Z]ꆺzx E[EkEוV+k+Z)uAr֓pYgMNuqc.-@&0FCm!M1vw?d})0N#*=rTx>3œZ+۶LZ-ÙцcǤ:.V~oDE$Q'Ԣ'@$gvz1tuΎeC[(0%$@4uk&V{7̚SW$j1YIYhŋ8e(UAU )7<^bw]ৰWύg/剋(N5O.xf{d}v%Rbf DNnCHΨ:h-%rvp2OgDΙwzRâ>tpuE̙6:Fh(ʷh9>A]s쟯nSuZk`"g \#ezB(i)*{^GGQk|ɋF RwǬ„mV Ezi)[[Bڈ< DЇcx_s\Zy< x Kܺc'k'x==wr=n?xy{_x[u͙ڎ8480GRL>=\1 <>̑ 1@Ly0qZuR|o+qW?|={<޽|l'n6fձ?q6GgB6LTr gM=hW{։6gmwބ qr$:TN"]iʂmgW/D䶲b(CNGRؚX0=1yp(]L_Pׂ֬-L9Ѻqwg1`W߽8ߺ}mgx$XpYnҍ;Yo^ܓ^6}2HPG+ #IZ:nh^`9cK빕B`y.8l%Q@B"M~z7CB?rqZOH͘Rg;lUzjg0Fa)cBf>ԐF2ğvϊ= W^IQ9Y{ ڔZZ_u$7BMaJX٘cݡurjÄ6,t D{Aq)^a(@50+CDb_r;-/ϗ\0<? {.[|. o'n(/!ePƅ-.4%xM2ןܰ'7$9F"n< LUxoG<˅S<a,h>w'vxI@|9ȣe{AD@Sc>5Pm  'Bm݈Y{CR"ZGz$9MP+Q%NJZNO0Z }+H޿OsįS مZ ރ*"o!O߾-1>O^}Jom]V>phŽRKvz= 璷6 $?V[m 2y9S׆ ^$q λ1<'As|v{ GI41Ei@ 1N; SHcci!2P)3` ]Å iU 3"jw?.Au{\dK.~12 3%&W77|Q6o;S$ϚӔ1rX -tJ |zrqp; e$ba737>iƖbuLenU&4ij e?XR6wnQbw[V$"T D<hSZ#HNC\ߞH11K슌TQn930Rz+RLl=ӼiAO9m53\=8/e|1; :ywY:g܇7jo߹omof{ƗC?;'X0$ i`o8Z} [hBtz4(-@[  [𪸀ɞSt 3l6@k>sNqӇٱD$@0abݙ!tU9͝ ]'emlֺ[! \_1OPg71iBԸo"qJlw'7D'>j IXU6!a| )&s`?-{Qg2HQe6 :f:F [Cq6,OcǏNOb/W断" [coB+X%+9sujW41-30Lb&خncFlj{ 8?{j{oo_:/nw9X-Yf@T9B} [1MmqgтJNJBӬ.}N}V RHUP항si>ItUtGȁz#z}}+fFĐYcDk!/!ѶƤ *V JB g52nCn=W%GX/Ө8C0O[^ _6cjzު;Eq s  NUԔ@~;秛ߺQEpfчAt}V^w+RJ.ؓ F+c/MqκmDyN n~╹A͏iPޛ2` V*kts&ǯQSO<KSmet*97}ehhp&>.hy~}Y9 \v ݀Q~}]>U?iY8}[]s2gy}yi92 w.İsdIRj9DH<'gy,czSǙ w sٽ3Oο.r{o΀ใ.ahu & 霊P׊J A|UBa[ jJݞ.[ՂTWbmhnorH 6%Zu3JG@{䐩0IkmL)U(u~xt6%W9%R%CS;k&ćN:5:2[-4m#"LLk4akcRn]; 1]EgRB$G't Fh0HקPj#XOҍăRL6U/axdt LhWZ0t Zsޔ'וh5:aq%3R$DzBu`Nб`Aj%Dywb'nkaL֨UI$zQ:x]7gRL4*cF>D ĵLzqkQ8~n$|tI8$90྾?n SMЏ"?} YF=~.)x>/o5 ? ;4<_n!&^^@pnrʙO>E p]ƒ"!14.ṀrN8{*{XH.ևb߳S=*OحzCnT7 n0bj" z)x$j}q `81U:~\, xq<2V(ڰh/ 3e]%L)cb-epRhUvڨ;A:v?Bpϰ\V8H]WV)ci|b$< tz|zwУ8_`u$k+1C;LQ1٘Qx\RdD'yf2fFZ*8QMϣ8tu_9"[%yTk${ J6PSrrdiV0姟woe_Ǔ`}yTp\k_.Oc$@?gqA~?qtnɈiF'JDlokqjxr@ 9;!MjaTT|L,n# ۓ>n{~]>[$hN>kj}oOń~ceҘ2HH6WL]qDsR(ݱ*}3 :>u])ec.TNtv8!]i mđ vj{^oB #aA;᪓w?ǃg5ΰqVJc#dT:˔0:#An(:EPJ.ƨaP0͜.ZJ5љxGKRHk\ 0 &gps#0 ÈTEB< 'i^)`Bv޻)t1*wQvpe6/| ?Ňج"A?fmNK7!uR֕%O!2#fD=1ps}$t{ryrZG}# yp+kP 0%z@+jm}z+ǂqbMZ\nۘz'Ml:emP:b S_aV2SWeׯ1UWJ3#h# ꠞscq[F0NXc9Y<" ju O@,Z|H{}3(| Sn4ew]68LjNZO|t܁xSTSJgf:75/H )̻w:̾zx\.6/t\ZOLj߻ 1P?S_> 7믇~:v7&Ĺ&hg x7"ha +5CiC0R]yG#{kmWD-j9I)$`5}_e|tL鯩Zs.3ڛtkwָ{SXt3R)es8L]Wz=Iͫ$,B JWhZЮlĺޝ %m+9M1B}=`Ũ}5xҪF+LL93-^߿lTMmĶVjq9 B?U׽(ѫJ Aj^EvZ7owjw򋯸a+jُ9<tHJɕ~ 0q]?q7O{ptm$;mSlkvE?3uHbә&8Um5\FKgJ1l7u?i2k(;4hF@ǻnnz 6B$AtbLY :01I`Hɵ)KK#6:vUVu@d1EHtPjC 5qnı_ڝWe 1sNOuxO Q㟟p)/_oXͅ@ƜR#\q}bBdgy!) ")r0UƚRP' L<Y|nyoDuèxv$PG{8*_]־ٰUUvOA^ѩw@zcPN+M:Kb-Y7.QJ Xp.wi)eJ4 =pvc>}vZQss__- gV7uq>~ʠ2#ɳMn3NJ&YYN.~ѺJZ1q>\OuMN<ϼ}b"O }l0J7?B8Ü{ $vnl㧍ΧJ*ix8mo-fo։1s>784|OGWhDzL 5p]i;$։)C"4Zm !x=.,X䴹ZHpMDîkse'i:=4J|嗼o{~_D]O/L˟X~g?zNB+K #ӆDD41{vS"M+|EXH: a!:ތhFh#ŜEgwKQ6ًMàH¸t\k;(`:} X!;L|ԝD_.wulj'JݨP18ZUq,n#[(Vw﨏J-HwG2 Uͅ^Z!T S6k) J uSd %Q". [+*&VVJ(inKsH#@ܗNi@JrtFs5?@zi=k_/]ežw88wv(; AO.\K)*J\mij3!pso>tt[uD`ZaIض{w{86P^?ir8e+(VgO*)|"RkztK.jf^k7> @v+Sr6Ձ5nHS%> ".]PJ$5sXE%t%D@5$w@Z㳯L.C{y1.Z>P_n^o_Oٶm\zxU }kERUD cȨTl ?> ſ  {UwBWnqڠ;)nJ^Fc*kڍ5,풦.i6{ZF7hrYpZZ9 Gc\_ݰ߽yxY4-H*~^H߷8+nяQbF7^[Gzb+Wcּ#e[i{DWXHhPN1/#ϕ] pAsp5ObAj#IûXa]W= IݿuOLsf%5jevw7Rkwcgֻw}Nò3Gqw1ՑiZX׍m+-0LsffB ucġ1]9ubf"NSUSM0lʢTF 0fPuÒÁjtZ.JtFH$g s W1:3SIjd|&7+^zŻOj.,Uy].KJ_ymT)9zi~BOS"jP#gCsٔ_Rf7/_ /_Do 49gVVn։{]}H|dMG7ñp}WW7iMl_=P)JSEYJVbS@7lp 0Є;&\e:"}s9Qje;mgPo0D  Rk H!SJ)y fnkZH IDAT-VƆ67XueJvQ֊9~6bB ZOO [n-d N' aw'd(N^Y_#%b:68ON QO_|R){Z91aN iaO Hۜc">:,x25Dg5W[17D͓]{cŔ@<2Mm]t< JveҪR c䟩5(ך`>tUM$X2vVS&s6"n+p&N l[saꎒN;&B+!Z_L= ||T ?Nb|\ ?6XM;jݧa|d@nn>Qxc/6``s+A\%N:<W$ wg}otuڍ0h1Ԑ$Xd"Q\gN"pȉTt%>#+_o?_3-)UUnH]i.QQ Uy8}kw>)e͆גv:m/vgRȥw<Ϥ 4 Zpݥ/atGjF >Ɯ V;ӔT]uicԜbqs[<5+"p1(BcJ<p|)S_b3~jV:si GHβ,v S 9įQj;+'z`$RW1q t{%Y a x`18iu%Ki#iPv}D`ԲLfl[Sqׯ_徤-< o.xS〟tJ~`U ϲhj C=O:[QٵcdS*e!Oi.&@BNf=y޳i5tUW!i$|!aK4%ٖ??a_ _m@ )EPS7-_/ ;DVW{ϋ["fQ 1`YX[gV4Mf76``W)V\#ruϗQƔD.,?ϊjV&1!{UBh1ޯP{Ȧ: )[1Jq+bI!ee4G#*w3ZMqTYR )F<}cMr`@Ii|R-G0Ǯ60ԝMGKF1 XǫYZ7|wEnL1-=#b9uFB݂xO+ k#HYl`2N`a)P"X֙5ߵ*+) da|owAbQU!D@lDk\E)Fvr}|Խ֍O՜g3N_\^.QXPqbU1ES!<`@dBpi"`GB(>kf{6vr3<^Yv3Ʌ*btʻ|9sŭL 濝8a;@o6Eޖ UA!J :Nn0 dNkSFU\ѡ*\g;6JgwOR-'IjlCQ_FRQ؛j"]G-;~m8'( 1͉T͂<=6[޾'N=qs}ၮF發e{r.ZKCڊyKi:˲px8ryE\8hۊ8s 0xV ca9!kT=7;;]f1*jUbBlM1_[fKó^lg&jxc 8Qp xq@ZU^-6X3ۺZÍN70w֒ ޑ4LsgԶ, Y&!t@ 2![g Lלg㻚?l%<|$8GQeO31Lͱ@Rj"PEIM՜KfAgu NXQ\\pګQWXTj{sqK]n0Qe<"C$av1m:az5@cy.ޚ#\CBGKGuСj 1on#X|ۆفvZYgsBR@&r\tN+6yt HO6*AC[>m`vI1 ќ׃7 jӚ;J)l2OOqiGqN"%66Y+t`V vt D C}ǡGAď!؋fkC:&)'j"ܻ^VLoh/Dx !j ZNWh k KavZn9tbsID.Ȳ(,v[px`]W{NٖvX60Ȣ\WӺm\RZPմJ X 璿۸hE@ Lv NbC[0ʶӞY^"Z`ήcPMڐqrZ/j<޳7zw{i8K"zCP迫?]->*97:j<]1/_}8$ޫG1&S qASje^v 8#81EΕm[ӜXbFܔcmS$hfD >yNf"+?a6.RQPQݸҙ5>\ >s2L} AGϓ,yt_.]E_WfaH[R`+6 ZPimZheR8ڙSU Xq&0}x5O--gwhndQGcblb|8Pew;ʺu^)ee˕=[$(SfY&[l\ٲERXL}&vٛ_"K9'-XLWn #_{޿yk;j!ȄsTN fZtКt:4">H󞦆0۷heLC䊚B[B.PT/Ef<֬u TWF qQRƦBpbXhԺm,BJiPi1ۺ Li #p >@PbD lBkF҉i' )0ZŖc85DT5^LP;Abc𖈚[%"t`FBLK\V*MġMFGG JnUstXW>L#n5SN~jEL*߷ȣ?gț2CqQY{pѨW"Ղ9~'|#+Z;>QJcESH#Wj#b'V=1-Q,_M:;NTc[Y4p=}NZ#wvU%hMsK7 7mxR/b7%/VJFpxߩ쒹6NՊV)u4,&\+P&S|nf8VĚ?{@hęFu8xNu{۷rܼ|G}R/G dΕRN)1邭eR7N^_ MnDv3u~z- qG Z5^;A9}Ӆ`LS|4sFj !ІՏՌnt7p E`χ`vǧ\Z{ly |5qh2Šeo6#α2W`$ңMs:qJɨъ5:W#XR9\:t$AL["InhM^j ȤXZcerHukN{&57Pvh$&,C8PۋλAVuሟg0zL) Q'ijWxW[?[>Ǐ?;'>N?/~YnF 7I>*aP 9R;&ڙv0= ]L`v$uC7;uP;uC]VsA݈J>>t;  "./JA$PP80?@%9d͗|U4X;mwM,PZɵYThRTL#4Vj/!E=kY"VT8J3EcެxpR-lӼa謈`0jmhjZM-Ą[.1[!hJ`>FUf!pEյM>R-@N)  {ya_[y(1.Fd{0AVXj.  ue=p vMփ<7@M⺭8iknȪ{SӍulܮʼLVA%:TЩXZ xgb\Gj)6R\bQm7d.ĻO}FHVT6|, ^fNȋ7}|e]-P*b*b~LMJHiFMZ*A-Qщr.^ WDwddFsv(Pi鿖J!Zqp(tuZ ߻FDm[<"8ݟZ'4~V-sV+(yg?Lp]4lۆby℧/;#%g;c{vl"9mtb 1&=qJiͮS%Ԉq;<{Bi7pq9[k5`SSQsH^@0ukVJ6'sO8)-!bQVnry/j]L]KCdmGR/yF>iC mK[ZR 8]q5?d6✆ߙ>MPY 0bLjú}aY4y(fq@ʜF &7HmS4k3vb `% !R싈ɳYw[JJg>ߜo?CaN7,?G>? ?>?n |i.!aàŮ >HR$D\tۉ)qke|ruMmrN)8rosn\szelZ͔G*I 1--jJf8_E7=]JqCcIf*hui)7m7DloBa*v3L堌 +p>":xM 3l5Zja+BEj=/BC6LЕC7Q]NCXrA;Zj%6t10 ,BǞ h=Z:0ݞZGD4ef:.k~帞XO+᷾ ޳>mBpu#D5,ӠԕZO,<J |Gd(uQz)PZԋt;;F Gs7pՊrH*e+"N$O "?ؑ Ď| [qt7T% qt9gX"MIP+1RMk&D‰@Hz*%XOU4FCҒ /z[nwq3ӭQHћ[g*4]]2(&YИve}D -c=1Ux#G!]וM 0K&dDWZs(/>x5|߿K C\k>{}Dq1c vWWv"vي4`Y4MV{#V)8Fӛee,IxBtī+Z,UK igh1=5\99W6\+*"Pod" x-7NT۸LW\ ⢂NrbmQ]!BF dH.u0{S76&D?^lR9=lH(ک=cl]IrH]b2yÅ!Z3jmƨSs%/='֋ 8#a!%#ĝnPk%o޼=͛~-oҸWd}l3MoSr)nA1ljGnË wW^w۴S֘D@0(f' akOof˫~7$ډ+F2]vβ_QZ'&z6EM3Yfތ輧myfԎ|Bɬ$T (C0&`VőƊ(, k!K;5,@bbKSݍ!Ѻ,М iD^D(GKvK"131}a a:4G'EVm 䁶nݚ <t,t=`~LeYx|+#C<hfx\y?5O[>ns>~'sg/_ P?uQ!2 lݘŒ*߫+ƻ`֢yL<j3eyfC IDAT J`V-UԺjR2) E;M!nrF0WPù`svb'J48 BХŁB͞ u'`fUj847V牭@ `|Hc ݐZp)F[Si6R ;]C6n.VJp<"r Z԰$Ū^ IR|ψx4C 1)h ț^8'yYb]E GdM4'x'?3\\]aLkD>\LКvQbJ6>6M;Qb1ir}4{e\}3OJZD9@lD?@,K^5^]| 6@͸H-؛8y6w2rGzZq8t[^ >{B\l_;ћCpf'0_mO&䓎hG'tX ňubv QZkrKHe^n$Bn45DBEc軭ݞSUsBq$%ּS}mGd8$X$XUvVqj|sAf8\'!86s\?}=l Dž?n n~ w?>c(<{p 3U-@!bL*,bH;Kqqv0B` pBvm}UP5וiIl9Cbm@EDys|[v<<r3eB}o0d;J+"?@ݠuݔ"^YkGj:EzPY|lۺ$zV:d ƿW4h3VLmZvz0B5݊\ lQ.Z[g]W̿ +/!,8 & mͬ[J3g4Tx:q ta} /lRkYડl=#4S4)oG?ا:?nL1 /"$OT1"Y̪6sow#Onn~1gv$Hx<3Yk+1ERwZ_5]+Ϟ=#x746ŰT\+NaJ) 'Xf-y;=9rVP_rwwqc.$Rxsp=oo+6]?՗ P𪔪x鈘G}m+KXeBTQvD ڭr85nE8()F6rVt3wِ D!ٶjPgS)l=dy&L~ߕB$o>+;F`?OnY }*|07!"3:! XLTAjjV]!bɈ˺bf3#} CSG9f$UBC!М*`V/ "*)q2]Em~&|Ue6=! fV/>{_x|T>w,?5Ob 4qvCn]|[`_\pw7օy8xj_'K^|_寘v%{qg/XKԕdp^Qּڿu$*uno 7.8hk/Į gDc-'z O!oՒVZFD9ݟ ZhI'#qZiISF1вwg++! wJ9=*]#Y4x7I)|R 9ޟp[h)4”pj߀P`.ga/;BN=)}4`gΩ`~|Z 8#f't qmQł̖ n /֌ 7ؚjtX9h]g.g iğbSp~|xcg/?xS TU3bHB+\!ʉ`$ fkݬ|eNLsi=M4lJ>||\?O?~O@x@hxK^|~ @k/'̜\ͼ>{ob]'' 1޼~9oܾew#H0>C lfq",%MYB)%M}dQm{.(Ͻ@7w+_ ~NE';-1xdd`=s8m|oʟW{]_,{"D77@k],y'a: )Zޟ1ԭ羷FB(>9Z6AkmFBm58aqFNm>cuF:Gеs^`#YFtX9[9jL @ eЪ5esDxYTֲRm7.MjvW,')+%x 6ȧp z[:`{ؘ&G!⸷ o"F2=Xh@#੪+ZuәUc_oϵ+Nր2޷G?>z'kS >6 o=<6_dt"ެ+MI&_)//n:;ơϹ/Y ifLs{R3O=eʹ^X&i>Т%ťŚ"j{u/o_2C7C\O)t/kdSٟpwZ;޲‹'Oxq4UJPuP \,dJv6%CH0m$S;8O&JS:sL Pf<1tiW8<i+}$J c0 UglPn`IS2tDҰp6i׎^"L1 bl-to %ne`>:D)[cNG4 ~lI1+0 ,׉-NHo"b!g$!C)IOwÑ8r4TTX*Cի8,ڠAlޅ>(D@ژ::iS> Z9u\>x^xaH]/;*wkf7%BjeVVvyrk #ImLTD@5,hWh\xA^WFOk[B8Gpc?}+|'R9Ls{ၰ 1nPER0J`ʱigMX7 Q0G>ES[l\<1F;Z1DkJr j >{ &3IוyZO7gb=k;5WoϞ>1(JوL8(M+J4s^+nZ.{8t8d","?7vؤRJƠPvJԍiMJ4ϷV|O qc~G?>?>_`m T\by*oÅ+v00lQk"$Nq4jlͬNVC+.ݐ 4|w{A2n֪ͅ,ftp)qƍU[ZS/>#vvxis̓'5~1?g[9WX;~>=m!JOR V[QdjtzQtuu#&Oi:6PR.Q>#TK:ܠ5v16G^KBh14}SP焵^ f\PŰm :^:V,HGz:G)\ }ٱ!g`l3@66/߼a~F:O(UJt lEJb[^,M '(kyՉkcӨV 8֍6&u^V^q*n9zZ*#;!5Dڛ^e.:Cg)ҪJU5^ Qydia>ʗ/ƯΒWDybt''" vz@kZRl\pZbheijAE5֫Z*)c(0 IDAT7N؅@F\nS<ڵ`.UpMXU.-w4w%n)AW#D*%)+*xr6hmy]牔 sP0 &ȍ0Mg Bt㺩iY^7EJ.,CWaY1օig]^;}"==u^}W?nWxqxs[*o'rʣk8LwF!Xg3EkPE/.ّKŌL{1zVt:2@a4pR:=f DQ-s"ջ_*95T0݇ H?o1"i[FZ1;W}PDàjjCꈸmqwwV=!Ni'USd2UaD#{TyV Tw}[-:AiMZ?>VcqUj!JFjTxYNAk#0lh6Y *R)XxYc)ɓgϘfM*!zX/FEd:ZҸP *8T.?XO^.^"#"R:-9PZagNZ+ `ض!1^ńXRf9i"LoS])PszkOvx!j[+ghU@TZUaWSՌq R*%cJlb$:.z( 7bQJyϒVj!hpf"p^-7q˲8a""i)8tjI'vӎ\ul)yӫK%Z\ aM,3 w'eY ϟ"āÓsnc(ǍNWqԚ9O e+6<"YC焖{w i #BC/c7 B=t5$+N5=!p'iNཨ4fQE'FACPHtFE4wxH'q6nbQi/^ի3jW4t-Zf7?Օ&!!ЍrSh j$a-SMϑN/.jwCVoiN+(*Du`:4֢X|Wn] <1Swɀop/?oOz}x[%97հkufFli+*=rEȵ ǜm;NQX7GsxH4݉[Mh$oGoj)"5ic /nq", t[:Th׎zxo(Bl$lZG`QaFFDN, ^ YӨ֒(`6xҪR:B9VgjyLDQ%"D1M;0bu <<'3dk݇N[IC[Z!7;8θBKc ab[6E0bʤaa++^,0i!;|it{CF-ALʆзD:)[CA"C1$QlE^)e2%⬁#{\[O].3Z1۝&_" Ugݏ{}@o`WWWm}p@:Zv'b{gn5Wɍy1V{ql)sw4* [J][U~o 'NB5aYϝzMgɉ7%Z]譱n+w[6ׯq>.GLȯWV:ke}؝QGF/3ceaW))Ǻѕ*^qdF[V x8ܯ'U7CG V 0"锱WW:8՞^tSjaыIJ4O5;1:SGo:Vh}GZ([lsclݑ&뽤 ລЉXXi8ڴE㴍6NjYLí⃣vQZ//G,GC;RlyawƺlF nط\Vɛ9\ܲ'uPf9vlb0FɚZM41K[F j ޙ"PЕcь z1'2I4ɓ ].)wxos== gB?ˏ+~ "$O><.jq3#s1/nOs.6Fδ1@`tk#`Tt/iwEvB0| 9iUƀuӺ˜@83F:YnӢELk0Sf[Д;^0OUN,'ti%sšVpqWj{m"b:Y$W='Lck LZ+mF_rRT)N^w _ײRrÝM*aNRxcV HZ->pv0!~[nX=NdzVw;ɱV~?9§D^w--vcN/pVn3__[b{TYiЃ;4|AV}:G8 TVzGuKA!o*EWdAY՚X@~c}$5Vժ2+f֐G׬ь!t,n۾}Jnw򆳏=k<('ّTN 4mZhv3 a&Z-l tNxf:6RGcN:t;aFݹ6*l[y.蹲KMa)PPgAjss{WV%#iK F ))02€:9%q^MQ!h&"XJM8(M:M!ɠ*mL1-OF>ޅ4SwϞ5S/??*oWWa5Fws=i5^}誻y4[cK 9NW_z8GLZ+~^iD(C^zG|ΖVŻ蘱mxc9.)l ?lf]q)iO7^e,Sر 4m ޼>?ٗgLZxĻJ|L\zE9Wys{_;gϘL :׾5~XR%"IˉW/_|4әja>;Gw0ޱltܨNѵdzݴ#=:ztD&3эpwZA;JW_V*tB :L+Oz&1LH H/ VdUjV5{Ӕ'Z5 =-F;iuP!E,Eyba#]Z#NAI9 Pb*Axт:SH[#c%=뵊BO'.b 89;#0f.\rctL˜@*[*FpkFƪuCQ]ZN D|BF!F_.o''C>Rch퍜GZUSw`xpr´v9M w&qAĈ>w./9lºzopG?`?vt϶njNpu ^|)]iE(b 6 XYu[U)wD*Ǫ!R18bP~Eo1G؋A38:81T3`@ iV N]D  SyR8QJmD' &(YTx}~@&{Ŭ RJb>u}AL탛5&V,rYUȂl'ӱa"WgO++_BzOK|m7F(=iM$75r08QFm A)xwI❖&%gcX˪h񖾌8gt_~;"C / GAB**(aH G?fw5t )~̋ݏ޾=Or 笠TcCC"i?'#/s>#_N?y9功wpȶ$Cg-nih$"Q!8R](@U;U{#f  b{A?{mԿ<}[1H~uQGW8LCzH:@KơX>p G`ڈ՚Jhzu\2`?2a'$D-XݚDd4`~IVRKBR 3=.U0vE [JR#XC5:k=V)3'Hw'.%@:XMv>+m :ƨ9kœچBCMDv#oe锔0s /? {IwyYGgwxџ!;[0Up=O5%gכ؝n  ^|Z7, 1حn͉< 7j+da6N=-c:Xr)3a౭|'[OJz:RaqTHc`Dm![.Z`MgO#?+5:ܘBц!5Mv79bFw &ZעIrUFFUa9%9OM }*[N4ER4*3.Zvh`FpY ze44P>a X%Ey KYM8Wpłi7}>zhi{+ncP`zWITVc_T3TB pC3l&R[OM)[ cڍ (E8C tL FXQCЋHt0n߼$ˁTd{C:K Or`lLbĺNw8gQp\<:ެȖpa¸@Dk6F\i+^zl%e*h-|;5XC`yu_:'|ɧhnŸbUZi؈Fp ;5)`qHgQmN! iP 5z7:@ΩH`͸njduөuVtV4RyHW#&NB:^_Qgm~S<,-w0k-1 p{sGoX ~r8^;U 1 `2݌K,ku\5F1F-1B*>JtW@40£@ :M 9 p#ܫÓWpcΒw'[ m?ӗ/DSwc8;WeqXo:U8s B<3Xg a􂌓9K68\DF -kEn<Á7gY+ܻ;Shl` ËDNGXIIIfEHKAF0>WK-(ˌ rԊ dTslۊEt([ΎIUx-}7Xt!ic=N ݵUbHՍI`%-%vsxឪC#&Vy} mz` {َ )Rj1u[Lm)J7Lot#C3Rw)#2'~ IDAT |`;zk|s>S>#j_dEV XyOϥ %V! Um%m -'u Uy.sEEyi9贈n7qjMCH邠"䝒AץB=""lfYF.Yi P݂N,*164F)Ɉ Fѵ fp*Ho4QԺ!o'Q >=ΪͿk)_NgW:xP:+ ŰΌvI>x=8PVeM)2y Ez.vY#M^;Dk(5M6FQEM-ZKZlPKiTtJ8"S}_~ p)(}p6[IÒU [XlXL8Z5flj噣{|<INM'ȓ3Wn?s{wC)[0Bim89GRR$in/#x,urFTlJ]OlIr/3e혾,7vUؖ)MޗoH_XJ0 7 M`Ű)?@z4s فӑ 똸r}?8FtfORB@#iO/u;md&C9 fҦ >ZUSΉDʪl3wy&fjF<Q@ ]Gw~E΅vb2~QC0,4BB)(z# Xj4tӂ,eM'R"Dl,WmI(xD "ي7K3B7Eܰb~}jK nAI1b1 tLɕ'p7;O [;ŇJZU `2ixo0(u'ĥuvy"'j.AEŌok7*ݚϙ6D.һ:sΘu#Ǖn +J&LG|5 _G0mo yy(wd;WAKZm]<20{ /qAwV,ttܦkIݓkk.}'1ۺquuYA-qBdQ'DfsWq,[)D#1X!+M ,MC-*cl{/MO~vRf΁ [K : K˔ AI-.FnƳN[3aҗ7\V޿8g9;;Cd7?=8yrqK w7~f_+a @%=H]Yۖ~wx?Kag/&w_Ѯ4ENɇGjXZ푻%o|ygW\:snﹽ2eF,~š~ +h=@Y3`1P0OwnNKRϋV{C\ȩ`\$%jpBWIzS巊479Ιf/zR w/A8Qڴ[-`*XrG?~7fp+壗rc;*^iz8)ӚUq1R1P T]Z֤ jZ*[*u#J a#];kf%}a6i4C!B^`|W MT(}ù@LiEZxzQ6~u+9z'^kё锬+V'scJqIŰX N1"ԪDP6vM}qf3 pP 6 :E"u痗T*"1g}xݏ>,cֱM}| ZBZq 1[4&ȐَuQ1݁n ]OIV8,C[ b{R)9rɔbAN (xޥ#M$37 uo Nfn7t^^4egG?x}" 7;ܩ0Ӵ'D˛;xB9/痗vB~x9W]^\qZ7[~w}?p| liͫW|+o^ [ǟ|Ÿ|裏'Ϸ_KY';OmHG)sG_^qsygLd%jl:gʽCtq^/ FG@7hu -OBH&&z:1LjuB=6?2ϯ.?-m[_~_K~~HΓ61T*5WҲᭁuP@)XH<Դ".`{8UZ/ GJˈx1>{ozp#f y]<ԡtC2-*01ہaS):zig[-H|fik=Dv3|kY~FZMӑ-op2<ޱ Sh ،l+ 8n7ڸk\_;< 3 ={h)`] DE;.~??Ï>OcM/>,R{g;޳;?ǴΩlۦȬP۲>*Cp0VyePo@¨$x$WJԮRuGnEtfag<eS!^#887qƩYrJdׯ`D9Ӌf-w(vjaHK&m$9Cy?3eW9Is`c˙4&V͗/YoD\]>=woTjin99'Wn0G i].p?2ԭa Yˋ*R;9mVjF'K]+&XZ͸8v]7 >^*qY7z #ϟ]º\31):Ћz{`iZh][N\z6 >⼧sӎӲi3BmzA ŦiTMbi9ӍVM$m_<= G|xmyoo?t|'+w߾2{Wo>|\v;!-UOY"hcX-gg%rօ;-Ug4~7SÚ72DdUN.,j%QJ8D^N邕i9ECQi,lٺ |jjp{:{ ΅DR77 wx1=nG;mgޝvT~GmҶufcY:socٕg>{<Ý""GTZn (2ntڂBmْ\Ri*ْk"̌gأ?T:I` 7#2s^{}^U=iO%9ѤZZm1ZXJi$s;*Պ\ jkRLt(ghRׁ8cf͓ǿeUGZY})~G[>s"1fJ1qbN'(48ecRd^31lv;\afFݜҎR12FM}2.EDSN l \(I^ 5!E^h.b!EtkE)ӢVl7@&1=JqbiC4p9`ǸefxvU4;PT WFfXzY?ѵ`GRw7=?{0lnR؝q<Ү{BN@M lJ9Vfa,S> Wל_\TambêGA[C%g&r8.vJHUqpGM )G:+q7gXHC$i~gy&D:Qi]1"bR.u "t*qB) x/b)E~r9Sn8b1J *]AhNE\K1e4 XfO.y$R%ay| 7O׾yO/^2QwB:tM DI4&ji-YU#aK.@ua%W0]4By֊<-MOLa rbIa5:f6 Vculc1H̲xuNhgH>J^,mU4РWu0NU(ug3k_Qi gk5S.2Q0KEhQU9I@ DY,˝Xa'.;!eBݚ43Gt['tỿ[\l19j-6*CM4b)s Du2& ņY`Jat8,'ժb' h-sP\˦E8ٝȳpFIeP`*帬j*Ggl]ZWR 0#lsḃPf  U6ߓVrh)Qc FWΙG2XqFpA)>o<;g=ɍzduniВX zI9c[G:b$EjEm,q).cچu1L}BMh%c,ah-BL8ѯ C*3j$9V,6)p$O1GaRXIf8is1)|j(=Ӈ0tݖ :z6ZՂkcZ(}[N)IGk3ŶqtzG=V™R=6ՌBSFrT]0f%`(_|/! - AEDBU)rɂgU\U%C͉*%%&c)qypKL/_{\Wc~w[UV65Hau'ѵ0 'wx+x yrR8rmJH^ZKBeKQYR*pz Vpx:m 52 J'9jTPJ$5Tzu)jw Z:673mI~j4?+OpvSz`/(PZG8RTea>0|ߢi!?~N8(-v]֝\Ӛf\ӐBYM s=]IQH)^Dڷ5LY Z3ݖ0GV^F%QH+EJ+s8PE ˆpluw e$*&z'7i0$oۣ>_h?y_fdK!(j|#0Z3 I+.즤LZia[oL;;cNM/'NGQ tjsIf9|7M6P2 P* QXj)T%kY͕2&vڠ)TϠGgo!$D1X7`gfNU Ԑ<fch //ݎT2BR4Y<9BuD5NA*ƣ[g@4ܗs-su 8}TKЋR誘pD\\1# OXy~_?7떿?CLӱ]t݊SHK+BkіTE,k0&(1w&g-1UD$3PF_j%DNkF@mF @K@0)mQ|ئP մ%mº鱈%ѢגCuUIub}Hi=Qc:\ӶB UP6 7.VDaݮXI^WH?J\ 4DgQ-W[LcIaw<:Sk[BkLi(r:xvEm ÉaĿ$_yb"։pU)EJG>~4-qUQaRr̕1 IDATX=U Xq V/9pM#Z0"QB06B.,ճ@b)(#E&u8n6;_Ew|l; ?|)o>@ݓFBAZN/X7lWN9Dھ~O~3>|($jFHu٤P$›VM }G\HN\TcxRu!69fJLΣunijIL׶ӄJrۢš``fkw Z3ĀFlq6$E`Zմ% |} f `C/'VN3!hXaPDRa4}0iBaPU]"VpOgjAF[ TT(\MKȐb.e,CʽOYGUV--7޲|yT7;o{ |:̆* iFf,tlZS*DWJ a߬Ц.@V_KgJ,p@іiq%!sj f_hCȕapNFqTT8S !&$6ރq>Zu8cae{1d13T+VIdsMJ@6P+RBt ]4a>@E)K%@8tFM ᴨӤ0֊]|]h/y#qN͛XjE%ֹdRy-YkOdE`>t2Z Uz㕰~O׵t}KUb[ (κtpy^a|O麆w.Z?/?1W[ư9;UWWW\ѹ'ZZc ŀoZbi8MWlش3|{a*Jzkʹa4k%BVXL+jyoo*,X4%'F3#аZIHP)ά7=f9L2.JF JAR૕RW-* fm$wJi+>9ᓥ)q(tS.zzbc<8?#'>2~ˏ?yq_=z~׾ƃ՚8sޭ؝q<cDo`N-1PU+!ҌQ9Y.0ʈE-fphk<5Ĕypq/I96-Uj3#iQl i->gSVC#78 < x޾QhK7m,F\n^+hizGƧWQ(@I\j2J*izGa 9{KB3Vk5rʑvxB-ѩ,:WyjI|K޸LA@=&vega`ZIw8MLqK;,h$ _ƪTU֫!n L(PZE&(J^\$c DVpt{`?f61 'RJ x%t]2'L@8-r)f#M]6noPS؉,}U4v}Z1 MPlru ?UonNϼxuzѣ oP{JR4JBΰF/^f3|dN(sG4wo1:L/̖4zK;m")!U4N2IANF`1,H-I)*3D*tP*xAjɪ0wj>9ata:0pFMO) 5BEfE[`qM)JiTx/pb1#"CT"ւ)w}uyR8UkRI\4;sFaDKMM6bm6ȣ6-Y˽t:1"01O]jK@ $^S.gv$TRk4%&Jcf[ޕ\W¶rj#4T ] TR6NV>m_z*_o}Yo n'4H򥌴mR]2\uUNR7N[ʐؐuaEU*Z e :AT+H3ڊuE)6R9v%lse鄐l Īф6Z[ڑKm/_][_w.?d-LCP̬ F0lj"3;kDVr?oY_l(UộDY4E\c9Ea8T*R ^8 b\\s#u?Zx_a9Fk~?p9Ndk6˗s~ b{nt]oZq\F֢U(vB. s9O)hD_ N"d]H@U51Xg֨pֳ?Q AXSдsJ%mpY3řW{ sh^ xЍaۡjXkh5)Dm `vJԢ!Z 92j=Y:/%!MOkJe+*)J: *qK̑mIҳvM,Rf"26rGY+91~~RB{bJ|Pa3#)AP܂.xgxR:wঝ]=yhkYY&oIDX}iZIs^G'Z (E )Tj"m{g__^7۾_߸{_dASCªOUj(5ZOc'JmdΘ#Xh:;RWdR8Yuh&*إ@o{-)Pȼh$t2բ+8#lQ9F7X-ɂm2HḰ/zҜ!R=TYU)ʒLjӽ"ZJ~iv i%IhN#HKQcHcyAm28I7fd4_BXD@zڶZ餜{Պ9\?b!3CXZZS"55?t]Ow|ϩnH)3 u֤p \ K-)cmK7x,I+򵧤#>?dOlv;Yxd#T3c8G5EUBJY9-WY(sդDX*qbIY"O$6Yb2xWY ivmW+bӚi4N"kAĶ8czO,:'^lZ}+]&z'2NkJ&G;s=ޑ/9CM00:`[Q¯+zYTG*/]-`4iO8F@=pv-M9G.:6=znӺi 4TUqo(Tηqsәz}RY' 1/oL)QC)7<}BZnnh,DoRMhT!%ޤ9am=y/b mZS"o5iM: S*hBKJ!X !h}G~j&JjœH V@cpFor ыbTFVsBwN. %ɐjz)Jl.o])eX3mt8l"[7jw_  | =>Koِs^fH)JG6{H(H7mQ2['Wlנ G7($ӵK_Ng8 qtH59%Q yPߴ&(ZTnRtq4cs1NU`\8[oP'y\)jj4GI@9'*9 ((J g80 3QE7%9L8 jBt@g,43=yDf#B4Ԝd,6>N7Z_ IDAT};1<~G_ݯI*늦HY1Fw*]0+ 1P$3YkъŪdX2[t1/@ C~t|a/%v}__)%"#RrVԬ%!.$)&֍xĵpH&[+4i`*qNT-)e&kb>id _kr*J{(LbYc"W.ZX.iA.2^[ͅuLMiOJMδ.9~y aL9CJT*!'L, d+ba+ᆮiIJW cq#́?#9±nK&@e:A2"mfBj̽ɏdYӛ̇DEnҟ$p!A#@qAPɚU޻eT(r*tp9;0‖ëTUx~/W7Fk-eWR`?utggQ<4=)FBURc;͈^EV,)%J|XϘg[>?y|?Osq Dq<3??San ⽧Nȇ*U"316kFJQΞ@.Ӳ0#߼yg}ʺ8/ ϒ!! 4ANQq7 n6)JŽH k. e7NQ=4}#nI<'rxs0+Doz|穱9!IfO KƱh2L܌qtC 3~y?lɡFQ[C]y51/ѣHCjQ4ퟞ)AUE*.SM:ašRR.Ei9͵2M>x6v:&̎OI\AKp@j+#J%WY$"d%+@J~դMo%PpF1Ʌnd|vՏJ1 ucwmqoTZZ-$Z ZbHl 0Vљi"Hu|&p{p}ۛjLwL@0lt \5$c)ƨ`-5ENi%Yu9jJ,>aco+r5UI;no)m[ f[ TD-eҮcCj|Z gF-q醞~гHjtH g:Jɸ~]H<1^]5RJaЊq]c[(4-Is˖WH75+rW/Pŷ/Ogh9#;s4bOOB jK̹) t+XǎsX9Oi~3Si>oK&(wg [v7#a3v'E DŽ Vxf3=vjVtM#(4 ~)Et1AW J94]GIczR(_6YUg)Z,NUD@QU>W:yiGn\R h۱5'$0JQXNMr`.iҜZg>ny"7Zg@xoɖZ3u(%{z',񌳃 U_'j%Rkbi k@uerCGNTA Y|kXysihҬu0ZOSke0ǹve1J1l&qHsT/:k !JNi8^=YIcH;d7c( TbLM_QZ /CCrnZ-CYP#[zG>dZ1?):Sp.4c/؏=[YH-S10N^}pZtB Ԙ2!'${Utމ_QD~ŔZC{'|NRtj%' )}i*6H$ nF6SǦj pK2t=d6[|xf s$ۜ7y# "^ M 560(a_зBJx>c*M#ӑnzOiDS2ahmŋ;_;5-&Ki|3F>3^'D熦kI "XV8cakmƵ`#YC*ϿE1Z3hmͲ=W|??'O_pX (Ұ&OXs\1JRx:U3vpTv8%Qؾ-.(r$H.˟s:II! ViY)qja+aN,)CAWRzJiU }t1#J1v"fݡ;KzG (T)E̮ Ykb=ly(%s>*])U Ndu\bd^: wC"SEO_<G)᳌]/bTB 5d6'QqsS#Ur*dSb+x< .rS^8:/(1ChJ(A1\7dP ,V)9Vk%T-8`mHCF~!)!%_|Wm~l]?Ǯ X-QD*`B\e\L5tmף ZŠkPZ{rܾE)O,ƨJ8_[#[AM!eb,Y X bc f;޽/=zPXB2SX-ceßnpֈRi .USf]WIUL"2E;lˀWNAw(3kH߱=Rq$Y6H ހsXOƁr^x^p j6ZY<:Zf5[LL!BMZvͥ0ڑ7ôVe>ؿ>r^|^ w#o޼%ʣwLw|嗘P9 pQ|av/Kkm-lqvt\Ͱ )˔8RZ옕#MRfWN{!ՌmW eU\`4N9z7 Ǒ^;:X,a4'Mk)$*aӑW֦Xl'v]oqβ+Z+a?'e!>?> \erST3HNәFMT N[NxIbnznʷwloHE\RLڐCu~?s3NخjEPe~"Z,JUbB.Cka=XԌɨTc%2%Њs;Gʘ:mĔ!`ܼ*"| ?ׂ5'Qr.R~%Jb֮{ x?xɊO}/w_4bbSji^b"f0]749#!,93k^ظQj*cB#7?Ϙ^ ;r|;1eߝsZy>⣟!x|H k!5i#V)! \<Dž~` <N!I<!k`|Zml$|A[EAW)UkBĒ/%p^ֶ7XBKq8($y=!K| ,X 5?Sm~Z>x_|&ϱ2}4ȧ69բOV\2n9%"x(i< 4|ͨQZeKy%LSId|)L\uM#hbH )lG ,HB%Y',ܼV)ִҥ;٘tM:!cl%W֚S!DurАTP BHI4Y`OBE\hHh9| ~4Sx/BR C/Za()I"s8g K҂pdWz&ӻG>> #lQ4(1F|Z?=sPRK,*.zs;v\wXT+|bfӣ49]Jʑ/I<3/^ޣn_ ps7-Z'n h^JJĘ )5UtF2$;*A#UK'|i ZS$2WԪ9mª&RU"]3/|UEGҲNN8 dXCwXmqڢ".n lk q{{Yg,۔8Taa|ƗHB\O3%48Y(ֈiOc,;x:f+j uVc;ńi6[ZM7x>cIր2I=ӨdbVsK"@ @@Y|jx!kDY"9_5\+H|ō=qJ]ɨ|uj؟4?;77$3 S$Z}h;[]am)"pBҙTZYFubs1B ):WK"󝐄jN @,NH0+ru:%M ]x)J{4#v˝|} d*p\U;NN)UW2k NLuߡʊiUB)<}x:lowx͖w_hYF)UML)\1e!@g$iMABn42XòxTt*QBi!DZ[)TL$u-sl7[:}b3m1cs,Y໛{mO |Ʋ/ UvI2A&PQ曐se6Te';c;GL.ՓU%ֶ/k:M%YsFry+ctMkk2d0“ѺÉ+'"KOYw[ BL!tnC<1Ί-q8'Eiݢ/'j1*7 "@{BYB`VqT^C^볿gcן!7@`_]8N9δE'!'k/9f2!f MIiQ4v|zkxEgVXc@)C*gŕR*&(Zs^&(:vnbL62#ihF!+dӑ oP#O5@j;j_b9@Yd ):;Ck6 q#{9B4Ck,˲i2#Cݖa;]j-m'>Ua+9vN6m}qGuPd5RvŹyY"42`9M΂(1R~drh#MZpgp^gRlw;Yn3lǿ~˿AJt[dDsf{itka BUX}i:yتflAO ]4>'h `4dU)#N|TEJrMF4YUh?L))I3_?1gtF3M=}7bb9DiٌX4U ՠ1'D!qx~&({n6,6ZI&ˆGP;ePa4'rθP1 gM9au !hAjgTơs3]'^p8g]=aYJ)<+1TWQJJWN*Q$O_~ eyH b(H k$J |-u^$ƻ5hAxUjzVAKeCs`EaBWEE)q=" ZnZ Qן[]/w ! ¥sEf"ׄQUA)2K8 (9ejL6=:C,ǣXݖD̚ZL)ӆ_P>/uo6Fm5#9@I2'qrPBcmt,K45}G79N>0*GeH+%u7 f$,Z4J:)*WNIHU2YҺgq7wĘYzw.e}?S'*o_PGA^3-}LĬ5JsEW1;'r>`zμxxiΆƨ YֹL)$Fw+a]%J*zxɲ\()H:5eD?^bj? q(<Lq]__ %RhĵR 'ʧ($AQ*ċ"*}旡n"OGq lk>a"N~@"$$MКPʣbEc@)²r@` ʶ[\Xn8k!VL[Sdz9{ͼQVcY}` .d{hXm"=V+N1QD5ReO^dY$ŵO.Ia~|81EAB0tïB>L%~V TnF8IIȔ `{K)}^RQ+Z?Ji!V_% k1s31Di!vy}ZX䛘_8 41Zr9J~*uL#q"nJ9()IKA? IDAT 皳 vz|jIH]Ck|}LѮ ໮zᾯ| ڶ+sB QK:9k|(哿E:cIHJdous/ZU!g!Qs'LM)1,VІ]?-/[!ħ13Xu"DXXȷ_XwFTUGGO' #uOw< ҄U:*A~eLqE2TW\%K$,ȐB`gL93a/}ȬzCkXqU`@c4!A\a0 =ޯD@ W~7 LjA8%K8Il& ZgpøF`DL+UkY3>J-ݭ*qv$"C,92;v7{~wn;DbjA¼AUJlYs ^rj#^ ="5ΰxyf]O͆ k̲z%0&RTfZu,r4gS# |%ָH)r{wnױ̊U Q\ WbٍiivB7PZ*qUV!Kz\Y)\D"%Er EϸK o[cI҈Ե(!R$2ZV_miM5n)y9,dKT4RRՋ}_RU[a5D{CIj7?IP/$X\eC@j4Jh"XۭNY-Ƨr8BNF`PWa6^#:nnn %yBH; ן8B<u0v9frt2)Rd͑CT kZW]dlD<{'J^(PBdZ"_>}M7H͜źR|b ;RiCZ野;oeRc ۛ/_=Ps4ϔn4]+v^~ nK:HV<*S]R"UİFA*LŇ]+er3V3L=78X#\黎?yg_q7JQ-[H?Zibwy /ARSe+%(a143g8ga׊ub#6*i&t#U׎7۝<EЖ%9x`qXK ̲xQN ĒPP(r { Rc}Y3۩ӽwt*!{3_pwsˇ8׉H+ p>"m;^B@ V p5 a0ƾϨPջ$ł*mhCcஅ59%LӈuTpNZ+)y =b|')Ր4s9 IQ䂪mFBBCc2 X#;Y-^')JlH>**ZDt@HT* Dn4(kDVQ+w}O}._Ҹ/ ]A hd%+]2q](E[P]1pRu3YS@R9ʲ@.c;v}O7m(J|dg6#&gGN>jÕZby!KA;SX/2 ,8 SzI[؁N^Q%$EP\ ډ)svvjM&YU Uu| !@)D!Ho;z g=>r>N=FUqK:(,Q1m8$:Gnw!RD4r>"j>%(19,,GMXDT )EFnڲ+4Sf^6 H?kQxK(egRqzXC B^=({NIIlm{-r-2S348QJ}[r!+)FHskҥXRW.Q +`۽__^@,>?SjN>G+I2ZI7HOҘ|hMEn҂G3MҔF5he^R4 a:Yubر7xX2w9)t`Xւ$U:0"ZlVUn"%UB)"4!=)JYHp)%(K\u-s+aV&[=\%q[5r&JnBsA{ }#)v5͛ׄƞWwg| >8kend!grM CZ#߾eYόna^=1U^3^*]7Rf;1jɁx^x>Ъ\oůK-P6g_|zy||8??. 852Tρo޼n\QYf7WT[O7g˿+؎[%C-@ұź[' X"ƥV׭M .zmAFzt%J͔Msfs=)Xzk#y9VjV$BLRb=4!z^ |$#ETW=>zLgY2)a8O~. i4Fi6U!gC{&i B~+E4j23M[n7QQ /5ÉRĘ, 9JoO1,̇4ETBpRc΅xbYA?M"R(ԚFvSZ縼0:w K m"x*pq⠅Xv8rV/K*OG|((_ՊpIM ]=k5^ ĘmV/y_CŸJ Ix05JQO)&d1!!ĐIkl2D$T9I*d@3ֹ3q&XcL!s%zbF+_K7y;ΧYal fJDm2uC=qjN&F YHJg4/lFrɜ+χ=F (+^ƈqR5A)&%MS_dHqEnp8ۓ'%qDRXb"5gE7Ԝ[! rKLTq r|/~VC7x6:*,YeХ. 4CǢ@ͫf$ G?},)Kt]c唚gHA)ȉVYqtv;q~i T,>b=fД^ TՌShg#%޾|>s5_쟞d41{h{l 0 Rjxk% I|-46)?VSۯ*L*\$(I^ 䵑fGI+#ANhuMPB#Ռ=V[JBB)1(=(:kFC5"$֔,gv=2)6,bŭdeSZ]=B^ԙ?c)4^?CZ{?bdeψ>:Q8O5E^re9\hΧ#)L? ˏ ʺˆrae=ϔ3p< EH/!z²`"<.kKlK.UE4ܠм|xS⨗$ fNRT2_2*qU]b|^YdMb}Pk D,b{{5"\u4EؚY_|>}jٟJNRAaJ7Rb38g9aۇ-volgEY%^D3) Z|bU~Csg3Z%^5d(6A;29jq)n`!DS|1ݸLGZWrr"`/6շaQYŠEV_y1p!;WFb yfB}t&:B UXR \B S#Dqxq8A31U&H !aDRhGH 甙ӑj\qA)WiKҞK;`kEvCL@Sp3BMQ|m $+B)E\ۯGXr:cۇ^P*NkE8cQjhM^ a3f_ٲ,v7ޒa`{a=k&7Ȯڍ@Em56ssr̢.e^Y]x|L,ҕH*o=/1Rcjr:M%_ hgC6D,XN)hк5Ň+qI)ipRqz\Y;bHf+7iaw'܌;F\' n4f#!*%nE9gR Ɩ=`j% 1QbbzQƱV Bc+͖Uk$LwP){FeLH /:rt0#B @5b}$-w7Sevxc7Wސue9IB!y *jR]f2k ʡo{T׸DFUJ8,НFsHTPV"JLTV)]Ch*JBM Hw$E]BM ODrxX5Ԅ3T2Yx3n% ZN +T+/75kt\E:7L!XΆ$ .ΐCAoy:bE~eYvv$%^cõUvmi}O\WF\ZbD4\@+iRuK^Ő 6JN̩i::dUjeUr& yV/,@^z1%M9{D~v,ܕ$ +2p+ : $ av>K$e%Re{A\6g%DO 7Tr%,j*P٠':BT"TZD]]dM߰ݲbjc7CP8{ws+$c4|o޾Ep`1ɁuWi ]4:ΧCѹ!Nl6t]NTV5r)E5QrŢ#D펢=mB MF:mgF^+4:l~)rHg0'rH72,? igJIiT "nb]WƛQ N)n^pk\-~.Ո1tM_2.Cs8cG^w_}2{Ҍ;!Gh|C( IT KZ1`Ȫ( k91ʇr,(=: 3F4ۂxF{K4Y^z`8;,3ENn`R?ŬzW&,+%%İ R*;c;Fg\/Wh42(kؖMJ1#vQ/yx|w. I`^CiIcnMDk Yet8Bv -/M4Ų\x۷-t/+(斈s8uA-h2j1 lP2kZТ6DӅfjefiwGO)§hMQfb,| Ru! c-eMNӱ6ȩ@Jy $}Q@xNA9+N[LDQ*֥' @< t&!JiZ~7KWrxqʲ?0JKe&|08P ewFN9):!JeHAؒXUB$a!׭e| 1v;7^DfZzr" $Yc6ZsgYWZkwG Ronߵ}.Q]9}W7W4M>4nl= մ8kUTɁO4JQ`]\9_7v~*Ds#t Rq JudM"۲\JWrYH_r~:[F7t#e-ne$tZx9 ^=()ll-a'|[f@J 5IBl~08@KzyVETiNn1ךl@bI~r]O9eGֱo8~ǔ+LU*8r.H5zu%'7<};vw;^z)6%{N[F:U9QY žv?͠yIF\7<' MW}D!g94XRQNoHɍtG6\ E3 o c/^))&o/$,\.H1ٌR׍˲@E(r)RjxrhBX{c-$5h*z!J 0d%/RgYɬ>ĺbڰ SdlƁa²Ilu"~b58F9((Iq gRU˔>jUrZm\/#j ZF9-6iSDX`<M")b@'h4#!e讐ZSszYXk{+me9szէ2nꝚFXb/0#'ə(; IDAT7o+){ןhnO{E0 msrc_Hv֋OZ}&,r@9- gcԖidTUk8/ O+J[id.Zi3GzFa|YO޼!a6Af0kfEXHdPr[b.ic%&\<>Iו6 /?|`=m~0$tp{o^rAbT'!ʢn MihۭËCMgRc&&(PZ28Z0p}@m헑e[9x?<38$)A/qZ㬴/12BHh-$g,wr")Yphy틛dm]#|։q9ɯ_&}.զMbdx W*VnY9MnO'޾-δ<T)PQZNKƊm*6vHK.'^ӕZo=;(7Y%'5%h48r=rlqC5Y E!wmRDǔXTl!pJ5Ls8 FSJqHr{()9m 佰t"ga+mrF܎fضITrqm~,4Z- 8zAEdB )blƎ3a4Fm )GZ3Ĕ^4-M (+ZKG*EŹ}8 ĉd*aA룞 MH <]XdJ {KMMm(*3;J/}6߷fC/yݬ~wmYelRHp,*f3zI-glBW3"g+>/am*1W-꺷]ee[6{rEx70Ż5*KJOO-⶛ȮJJinv[oK| ]7eH c g(/>mNke}0v}S 1tBQ i5kky-'LӀMآ9$ X*I1p]7BGuc٤Y˜=B&ȹeuݸ%P*}U(1^7=c~o$H3!o6xInJ&'ɩRZ{ j@8 4:FɈ *"bT813 ;a(1JFV=&Jn:n)ey)sDH;E,1"i8MP9%ﭔ ~cmtnBYIʫp'r}˞ms?v~(m/ֹg` Uko_R]eZXrdr6RX9Λ“5O+t"$KUrJIW۱45IR+5R8g[ J a"UpN aV2hN뙰Eqֺᦑl!@T -J6b5qkkkC#+!.WI;,Y0%& qp=:wLuR%k#Snু*"*3aN ;\W䷴$ q͖87Pr Qi nbֽiRj =OJh'BH?Xv[MɡZ''2IVymr8d10y`¬9M)$JRΒ =Jb|_~+ZۛBQhde.Xh5rj2 A#?UN^|!\#n}Q2@Z2Š[ߴҢ5șyхk0ܫHmwPZ49 \`eb<_iYN;OS"uN(~YkJS-.FK-QQFr*q4(mbLKhSd-! p`ٮMfkبJeӄmvȭ Q?=>2L#ӗ-㜄}JS~[gmZ"q@-UE1"[\i)u7=ټs{ԵB,.bCa(T%g#%%>5)ly˸tgbȌ8~s3keYȠ4qSdX 3ϸ&ii4Dbr<#Bȅ[?KB++V[ H1V+%ei»g^xWVQ(=p 4LdN/hg*Ԙ$/d S+F&m q5ӚaF1FQQ::0wb )1 < Ki~ޱ<~EE#c<yw5vQ45FjڨB#@mqy]$Wsg@,bYp4j(*ˈGk)$4ߥE>{\C( }_{Xߛ@/P$bt:-WZ[z̙lU-S3=~[ MF 3t/$*dS~Zqݲv;kg#IE˥д&jpw7cݞh,ZWJѠ!ݼ6j-L#Aђ6eet!GQx)ZQc(i5 8Yl18K@aG,FZ w>"Z:0&edo -$ydwb`DkD^̌ci]T6^Nhs E˲oͿkOG9M87VVؒo-WR<-]TM,} o:0Ɋ1Ωւs7B~]]*n޾ϿP|ǴI g*ME'UQ*JnϪVZ}:x{kayG:QjRHbq3rqΒK&~|:3ИT3.0(痹ԈOPJZ)ږ(1ӌeGmC7rv*4΢s̺o8 3_>c.,El49'6.P n"R(0#~?RRXW[rM8'.#4Yj՗Rt$n1ʙ,VPJ!*݌6^UaoB)/EB =׊e[ċſt:,gJQ rJ68I3D_=zENt?"5VV문 ZKR0(Fnd{-RJV*zP nk )&A^qfXЪДEigq(eGL3jNVS!5Kg`PbG$?uK@ 7Qڰ;;2L\5B&֌BQ1ֳFs=3o~)QVLrFkv;r){ /_f^+ڹ*Hr W+S6h-VT(9QXkOh vN:15XDAUtl;TJ? o–iDPqnO'?/y|<9Vqdw:[V!Duի7g\ A Rz((֖Z%N6yJSRFX=Pӎ-<=^ɵGe458M-gAJ "&ڝc_oy ?E}7KA":HU_/dv%l2ͭp:^=I9A0 WEԴ )\z= H0Rg]79喂 1PBHE 4ZbJԔH 9g93 k-UR `-h1RPhYrs\PR(1jYCmDQS`M J@5DDe ލ*=%}Gy:$WiKjY#@kI~1X#25V𻑒>hਮGXOOiRqM!01#s67An:ճ(5?`w7T8Roa7H;y[4Z{*SZe `SZV1deefmWxl>[BW҅ހ ([ˠ>/_ޑbՒ)ѴP8o0+~ƑuJ.8bM\48V~Ǻl NS4iLnĀ$LUR )n.cQ;x5 eKr9cA!54ٜ:FBiU0DG̙ \ HlR{kP%A=REՂdEB=29oiJPP??.'劳 4()7)Zirh9X[V0rkkRksQV".Oe0[;,Er]boPULJ)}v}?* # 8N`AG{7S},5gyֽ}l>II $5=@v (;OU9 B_D5M\)MM0AsJy7kUDN2:-* ZĚ5mL^7Ք uI z_ aH KjE2HgDlHNRu qJ1%0В CB#*c%nj`QJA݈q%tD3(mk9Hk" ZBc 〶B;m|G p}:/S2k%90+{];M m!] o>b;lNDWΧm yfwC{ͲDzH R;J (Yr\H[JX&t9Uwʹk"?ȩfx|WZwiP7uWP9TѾR+YmޣKŻFi 1jpbA ~*_.&/ 6vDhm8Qc4I;ugi" HIPTZXuDR3x#ɗfƋ>5c.BD|yx9Umt+wᗌƾ9FH%&7њιPo'Cg*PZ?K(MZϣMfh!L-8,$;UZZseSVQ@uBbX#!>_~%E?痿5?=/_"V{T b6JjM`NiJYΏ,; ʐSjZ P#.%00 ;)M+8wNsT>$ n1lh66Ɩ*7?' w1ǟ nɆQu nr }~TGpސLʙm L5rpSrrIhXMQd"W v4_*qڱ^J3woT8c+wSB Þ3JDӅ'oZ`jQTU0Fˢ*n+~PHXR.b r/SRH02',v ʋ7(-}i̻r:c4w{'v̸z|okMG~#ho?. ^I!MI\D kk҅q)OW.,lrf&r)X+EkpwxS8X ym8eư\9SFu[<ʼqx@k'~o+b+ 4?[c[*=YJnmsCC>cFN.RB҃oIRJjPp>dٶn?QJe(% V;`cEy[E 5SX3z6~?q ^,0{5bdMb 4J R `E:K>чBJ hr\a.(aC1,H -'sV(1@)h+6jN,mo)5Lo7UJK82󕇗X%S4w/Zc<-Ss!6ռVկ%'1YM[a܏hg8tSrx=4E1qĞnuW~Iu˼*VlkٵE9(GҒ8.ς%i7MxgTFU-m/? _x0 N(RI ydeawÂ4-ݞ HkF;*'ɭHxŽߖe4A))֊ޑ(X=`0*bSjʵaxNT0 ,nƍ5RR"Xk$@apjùR%bgMIZM4aL5DճkmLZu]5JӎM*b"2V-} Mp0y_2%B>Gy8qhmOݿ~)_D+8J;?wҢgh!AwHWƈg8zHV PSxi9DDtB 2ӄȌ :uAv9SEQ ethK.ϖs[vY~_W}}_rU_ֆپGCk;ģTjZz┐Ж#Y<+IkJX 4 lQUXww/9` ~8!, 8¶RrfK^wW_V2LSMr$_Vދ q>MR45xAzĖS]`J; _k YqoU bZf o:˼߳]\|>sy$͖w(!JJq;9]x<=dpJOZeέM؅SM*hc$7 n[eY/!ĮW_IZu?~9qDXķb9^D)SC&p\c12&/oUԚ´On/ko!Hac9yۓIU<߭5aU ң"ǂA\/2땇B\&-*i{'8;=wwM\aQP*d?JKkYO= -ADuxxy R+Z kYxaO7&H- 9cEF3-[ *}fzHZ~ؓ:l'z^ &ȯ[٧___?>i)C21'tT:[o+YT PU5I:K@ zV%'j<ܿ`|xI%#`sjq[%P8KNEfMN`GyeTag2έIQ(4%h_Fb|u~}OO_]=[v%יvc)`70P{B,ŪB\s6is%ER1 =g̕kRW˩Z\B r 3FzeN;2#͞D?PN ܲs<||dְr<9 s<>>a UO)JJ,ݖ\ 2sz:T<X 8/}I"\\ׅ"!+kXQĔpjF)fIn^ ;BLR$`<@K c-q<`$渽5:Cd~+#-aYD-m:ldt> O? ,9週=B ;noTӁe ,b,oTLS_"^Ma [\B!t:ac9iE,Y2iw"e[BF9NS|:ߍ+k͋/[t`G[OXgbznm5˽JZB{'6Ѝs< ma}OZ @)Ebo%Ų33}oRb-t=b4̙Rʁp6z+4%gޓֈ29HJc 䂳 UMHA!T[j,1Xy>ʨw%$XEkɰv~LJAb;u,6W}i݊s\xb'g l6[!Yx5'Ļw?ᅮdqQ0߃LOK)"ΐfU(()U/XR+0Vz#{l^,Gћ=e>t#Yu%Ȕ1b:#"ZsDY%5.e)bbƉ|/uc{eknFJˎf]))he[ߧVRJ=4e)(]/F:(ZI 'J.L3hZY%38įfMY+ez4漞IQF eb+J6-C5%2KOe^1qIZ25$$Cё5W;ǴFSvpmir(Y6$*(i8Z6e&*1y!y wۑHmzJ*X͖vYN'|/TwX׉Z5/_A÷m cE٭e{a8 TtI0EY\onc]y 8ppxΚ" sy䥲Fb(4˲bQB^ID7߽*I)^nF 6VJ)¼7qEv MCRsm*)`wϧgJi0B2 ))5t~t2,5)9IT: xLr5wwLDzB~z3xSK& kLS%W\I"|/a`Z ҹJi9mwr]t}/En.67Z"c\Rb7k{vX"jTm=UIX+|sXgK_ ]Fvq}q叾g?|BiKE~&^}Χ)'Rʘ #%(1QU:IRP)k877RJQk&gʸې*( E-(Q( 㼸TR i'>1ht(B'o9\nq_SY7D[W>K%B_EieRdVrSJ,xZRKg?9߽tbN--W&#Rs".+eԘJU"TQUt8*~:( A0UU԰HePjpNYJ5)g4)T!i&N$΋`I+Ew]6 ~,H3 r#_ŋוU+jlz?p?FZ*Qja]VR\k`^'*[kXQ).:eX 9NL2znU<~|?}S*qQZqn1Ǘt::GT*9d#MWRUkD.&B,KaӊRk4[ԺxP6w#J8@U3&fjT#\tT/VCAUrn=rW*-ltt?{//oXsITHg.d D|6j YU(cyrT3v8cx/j)*6Ҫ6[EVF,D͕%E*m˼*Mu/jy~~&D׋GahUc*Z% B"UOg]Wafwt*. RXU%gb ^*^ܜ#;g5JdOO0;~?'<=8.g>fBkuP*JIhQro!ߠkA5 9)2F@K:]42DLJ2]CD0cYN}aȜ2J?̡i·7ATq9S^qVlgyy;Xt2o68醎 yra X煠"C-~eLll̺ؕLMRE?l_eXqAkzr͘j(B?KfEXI^yYESCx>֐R!E~U p3=m qnFw(M%6M;^:ya۵nv-ۛ;v9'sjE"-E@sDX+g&akNCT)X۵CYW{?|dxA#Bj3rZwmdwbդj(j>k[`Ou_/f{ߪКǶ$iyeB#9T_-.sm]hn[&P\,YiP|I@5s:1v]0+X97O%f0 R*=NR,L& Vr)sł4JѬqfWEڸPRy:gyǕԵ|8a0rT ݞt sↇw[NOgkdYfz~S5C9M紂^-*$ݻyiZ!t|橉. )JnyÉ*gvCGMvpXJLJt:14:v2:(4+*m~yk-XgnLŴG9D7;²'WvnyRe k"QntW1(bvF bz<|䋗XS&tF)\ )~sƛcJɅu Ale#?|G SxJ)Rt$.NES}gYAjW1DCѹqXs$ dC}ony+P"]mmfQwX8i95kBӵ< dsNkZ-͆ o )g?o߿j%4MQ+}Sg[ŅSJeFjSF,1tiW%50O󵨌JdZ[h`#Xgp$,c?m0$\vbrF4 )֐2ږymʵt_j'x෹˼X7"2m7]Q2o@% aJ+RœowͅbEEP\ZD0#BUFw΢XT l m0f˂.wfDҢp pٳsr)ZSHՁ ( YElZ`MVq72&bu%k׶\_" RueAfJnxE,3Z8܃2 )! ޒcpxFFS2Knkd9| h5RZ$ʹr|:,뙖3)F#\kЦNuf;nnv[G'nɜJ{¼<K^)^u!EPu]s-"qCIAGUcbKn|vO#W煺J*s ƈZBj%8z5K@ +xMߍfCQ8EQ!2́a7PRەrʄ=_79͑9* r ɟT*JULafp=q(g(!5t^@3꥘XHxxp8 ^xE.nP%DL7Yea hD!׶jMOEĆ֢TZ鬇YW?Nss˸8<YtKzA=yAYvdR3q ;߲鶄N"JَR[~cR AnK^<9hE)uP7;Y#rZN\9k]Y$^ uV3_Yzi#NcVK?o?~ _Z/|\ZirMj[M[VyI s5|$zZ$ <x`3)MnZjZQ4&I>YOiȗ#-sb4*sP(bK-2s7nnFi)q@HhPv4~ɂT96S% þLk\))sRtWckaYW. ]YͺFrU8񽈩YE1c6´k,OΑf3˙i:2 ݆$*~`g^4f?x+!9%'j5Q tn@Ӂ wްٌ%^ZI4`*M9 !p~|<%QiW!RHZ[hR"cN t۟KZ1}.7ZYP1UPRQآ@Yt~Cu&r*(cF]j8R*9I,q1M {J$NCID Et0Jg3fpƀX_еPhPnKɅa蹿g6x~5|'T1qRmgnoo)(NGfZ0Ns>e2\2'6k,UUQR͆|zEy:/ "w J~!A!KCMR ƃd3Ƌ H^$Ҝ;U1I:din܊R@&*"޾Dv^Q }T% vm4i#-eu{i}5@a;KմHaiWo$w 0_w9[-*cUA[n{iB2]_.]Q k`ƭPKS^!U%/Y1]WnK4ֆ#qVm&d6 FJRJAhbDy.%:ӁNf1idXz8 ^iJFFSt"tɚk"0zJLJbጴ*B6*\ی1Gc6[aӂFd[EqgPdC(ZZ)\t]r>c<ś`CХÂw/x&uXuDe\r&̷/Pov34S቏>rsw#"t|"@?3? 1]P9|Β8t Æj IF$û>p:qg`|D g[p 27wwxc;N $PU@iMJȩaW%j-E+b v͕0=p2n<կT8 5@YjU̧WT1X0WzFN56El)$J̸F0 hK,* oW]_JKts$eUœr,5g5҈s;B (#ݏ3RNۯRr@kϴ,m9'f4NR2q{#(H،B{j&-r:u:1qwfִ8Ҋ͸nIhqCU=eJ|ȭ7bn{CԚuHz!mܪMT+TTӞu5NVT6`8S4JEjJM ܿu>uyZ(\HMR_9WOU`MUK˫L.MWlT6J*1PJQ +󶢚])Jc3=9Ę5% #:)יX(-.D&5 CY-DR9HhGS ѮJZ[,_UJ5~)bSQu[RΕLē4*W5aø8_\5xܺVҶRX%モAKݢ(BZۀM9 ≓i cle7߅ߍop2vkIw=q|.˼w~D`h%^ڴ@x^[E#PXynܹ͵9KUN9$.lq5s8%F֜CB/Y>)GbUJ)Sf^m|wcQdVr\Z3dy!J}ٗZ1I{<2"2ĒaV^nxk(<>>pssT^|/2=|n%Qۦ͛W?߫a\~+".c|O wtq%_yC aY˰%݈Y5*j(8gM\̹ +OO0鴴Q3CH\XCDsm$E@)RbAkwmO糨m@1ֈ ?BNk P ؑJܿ~5_R}GRWDJ40aBKG*5L+q#aZyݎF7۽D-Yg}5Kzd.E9,Hh=i麞q;ܼz)7#8JĚ)11Mg!9",+ϤZ޲﷬oBZ+y ̮̚hSʰЖ>CIvۺMr0/$鲖f*,"[IU,HX.t%;L*koXVuF;3;Z5=LJ*Ơi?oF>Fqu`ߢZ~뷹E5]UNMŠ cRZ몈Gx4O.Spްg@V`塱:#k8+=jB 8-lϺ|KΕnO*wYPk8vhyZXZZh ZsFWIƈAn 3Ɯѭu,ӕb8,ab.3T鶝,W[y6iQd%,ۭbW}AY:B*G|&Lgz*ZT#ÁAKiZI7uKXÇ58#c+) 0p鉒7P)21-UNmuKgYV'd]UE6џ  )+nO T ks+Ykq=eNgKH)G򞩋86'|c/*IZ]4Xq4BXb9urox.z]td4H1 *RRA$)yˤ>hBxvjQUΦGJUo H׺gs2[敔J3]9HV"n4VUt)KSSYp58#! 5pA(SjXcZ7.H%WHAa!rը Uq9g@6>|@Ӓ y f~K`}t ;23w:R#4Bq0 ht1FiZ;!BJuLq;^!l47wA烜L41-3-Y FRKZfԵnJ-.`Jh3(^dkK`;C uYf b"tPj"츲DxcPHpTD8i( A& wυ[攥k01->LRmfo_Â^ .W,{Nk+uᨫvo7f˱띹~Q|,eFhg[~iJ^W5"Ċ93i= 27FYb![uKۺqs-$*2C?.J:š-6LQ@My!A6&jUy!LJQD>FF~E4mTSo)$Ӥ9^ITWܿxABGȕ(ޱ+}x8O|׼J+Im%Vbά1c`Yp:M ۽X%DbY2d60tQp` DE+qws͝cJ U7;r e WB)cT U:.8r.]BMvؠefYJ9sě 3/r IJNӑT"PVAߖ"#93U•R5P,J "P,0{gw:"!ڼ1R1%O'$!\+Z)TyaڇEt?9bZ%n xI[ܞgRmR+7ƕwRYZn)&qw;:7Ogle󮕛˾o |)Du/qRp?Ha" ]Ҹ{Br\'AU"6r?u (%3˲ཧs=َ֤ k^G[SXZgLRJ&ƻ95Qv{eDZ#Y`>T-Y~X7~U.>^ama*= %I1]եO- -i$Úk6T k,C謤Պ0XEZƊU^|l$j4"BQRs@!0JFduwJ8Ѣ`V8 R0s䈳wQfBZVjTY~v` +)|tYoAki,(Z!4/~2{-U뺰7lu]1LJ!y=瓈S&,hgl^kRNϏv"˺`hc0N DZg9Oߠ!=Qֲ^qK,ҊicsK|k޾˺z'c{rXVDЭBK2fPȘ%!Eޟ~Ʌj$$OB)XE1-:. !*1E|Dj1$ׯ݆5y:i)T%ŊK b9T xFvCrx~ΜtixHWNN$Oj- ZY'J$K-~OgwݵPU+`yeM&sHk=l:FR4˺dSkQG=I v9q59ZpIJ-5hr#Kj%Z 9>.zϴ]UȯJ*C5׿[PVe>Wkc"#!rY-d֋/6(K Tbc]T:?*gJ2` c?`Ki0T &l D$ֺ`z7w[ l%gcX~q֊qd-u1=]ˡmY |-|{\Xkg\&B_ȹҚ%0#4ˢB7' q +o~Mv|[ؚS#vAOsta&@7 $VN5jq QVK(lGR{KS-b[>:۞ A~Z:}^Z+8/9?pk~y/?<[!4 Ҭ=_(czIƓ7Mj·*KU;>_%G( >EtdR1GWFA*2 UC%&IJ5s_jJ}Ai+Y~("Tmp($:+y42TL7 9+DŽҚ!+;)DqB+$J-l䚈X,*H*vF*}U)syP<>C[ACk Ѫ\tҎؗN'ӨZG;Dr3?3`[#EYX3iqD7̯c7"̉aw^>e2o+ce[ke=#!LʢZNAaSaf4ryt8ߺ9JG>r8$ uA8镜 ˼ {ȱx}}AU&@Y]Ug&Y4%f)QNFR^/t`[(L|s2Yz$͠TVm5F]Y}r&+HY|I0OmkɅD"7p8tζ˕76rEn>h+QDwRݣzC$k˶n(%v;|̐*!er 웬*Y!ڜ ou%|Ҵdz63yK)ĢF`mI]Rҽ[#Eu}O@וd\,$bFLFv`~.Z-JRGFY;ԛ&bJbYrJu)vi6sVCrd˃u(:B^2!~n_`Q ZOj7Ru,8:'[-okFOs>%% !:&-#&5_y{im)1Qm'TaQ;̍1M+5:- h!E,Ut"zI)ݽ~D3Z i$ "#GU%be+M91SZawtPVHdZq){t'6e)38M78j)Xyz̻4U{g\QJj#|\e?1Tm*ǁ &R =o^]7RJTEp~z-rH{0Jul6IgE0XkaVb_WT6xd3OO۴HkZ#:N39ea\R\ ².=T5JWSR[JI14շՋ4KDe ^/'ɄM =&4vo" Ԧe(Bų 5?aXC. %xć `r+*Ϗ'^^_;I Z޽ǺXE'-emMɟrrٖ=u:_du)ɪ&θ[s-<b$IYi<ߕK{C;q(5b۶LHT Cךx)Cj}F;H^,i]B;igJwݚm!a> ;= (3`[=ػ%\L7gX?.?s3]PD3[kCwڶsR^AO Z{ɮP*X!Qv޾#푌J.^jekvCJF2ʋ1E))F:kGv1-JqF%;\ ^d/YmP(rU;bTŶcDBW{Ib7b3 ]B" L!XmEiM#ȵS"®:b=W-8aRF1&1,[¶{=c c1TkTXŇ?~+˲Cp|}C7|??ۯw9 D^_)%B`Wx+ႈ _}K){btcO;bP뾉6 sw/pm1tnUp9V@(#BZzr?n?k SSJ i+H-kB96m"3w!ϛwgj pijM ~pgҿoV  ?w)u}G}ƵjNc 1Hk%yQƂTB(Bl3ZyN[[PI1hxiۛQ -le5>$_~"8u֚\!$b|M\W+uBB2uNdfueTS+$uY6J|Q3ASFY'h %TQU]k?oR17PjS8cLϺ,|Ya[, 7!f<?7J*=C<_ERhEbgyxxlD;#9. JcLb*eWغμ2͆ [\U, $p,e4,n2QOCO-퍜 'f4U@/; y22P)\ [Fat`Cr/!3rWʪ%%Q4_fJMٶD 8#_04uVBLpJ.lƾ,*m2I9L')4Jڎ0|2s[keSM{dg^__&8X ӧy?^yo yRdF\ ²Eau퀔m[xNIh9toz-Yu򩭠SD ]HQ\F2:BH DA54`J EUa]Mk%ٜ2dc{HnݴF13N8+LqcpZZ.D7x !~4U_rͮ_Pf}'jɌHW~%&4"un!$-)Tipx:P&HHB~icMD[pXCLg{I֖=q 93BӧO O,K=NEFJP7a#"Ho;bjKTFV@Ԋ:TȨ, F٦o,1K_`CB F[\A UnĭREvyC2L9&DIǝlc C/Ve&t/{Je/)V{1zK&MQ'S{E&):.lԖ_LRAj% "ͥRV(T,nMWҮ/Wo:K_cK/ |]yƠ( QыIYm>k희ecDΧgL* IDAT*]eWi_oہZ+yuLh8mi(]˵ )E%!U „&WIkh8[{S*qtӀ9+RUu} | @ @B>zrXS7Qt-'Ù 9mߛ[q%ц~Po3_u]zE㑯IQR9?>ǧħ.m[ tB`8yoNDPUkD6{N#bM }G^:d2ܮ3Prrkf-'+/7KHQt>e(y+i%:Ud:+p(kOo44` 6":S[oAbS&N]Rbu!niPHAwK"d_/Wr<D4ΉճV]/"}a^.T*w'aEݽa#Ђ6{Aƨm[G*1b~IrșZ찔jdd^Up`M yQVF F|14-ӊ5C'҆WTAR{: (K-JFF^М Չ0j*"ߏESjWX?whEɍ햗BZ5apvdG>|=G@L" eB,< ۏ\ɩpgy]tʵ 6v>0_ҭg4BV B-lJ۞Um̼|"zwm\eT5ӱ^d `uuATNla-j*M%$jMrf95 li/np ^^_FVq980 o?GvnRʜN({S4֪Y2u^uNrHgg`uiDSH#cmu.Ej]K{[QIRʥMIrziڎ*EcF^>n1u$p[~B]s+,,4B财ih8uLcO4Ͼk\us69(dR;tE3ݷ;sl&|pFNa) iJ)ZuN=&ľ@J-$§*]_)5ZyEhY*mq >7(}g_<凢)U!08qŮPh2/Zsz>~ _vy#F?bV.oB1zyfn߾7<+d3s!( =Z(\ =jQb43rϏbm(mi|vMcG \Zbp= iƁ "·j,Bgq:~DRԛFY)P9s\JK9ZRMҔ";ۗ>Λ_k-~o䘚00ՏjB-!sVvO|NZ\qmݟF@7Gg,d%73䘱NFL Hf7l(9WanjvVR 5 ?ZYkeR1J@PbţBq[Α /{l -ֽ*0N$#)hEԺԚMk]HPyeUwO͕qx8B|t{u@N7ǰ"ą5}?\gO3JN4goF[ |2oM^#ix-4ӄ n-/ƈ >ɚ)P qxXW cs|."\/W:cQY~ \/ I'잗,LSV K !D:ۣ@u*|OJ$Gl9URȩtV8l/E´jDDgDkӻgr 0 \D$m u븼, 0"Ī9bfi(^>9p>rֻV{$,}]1t,)eD'J.I+I F r!Վ#7QO)З˅+U I.Ӝ"nltCiưV;_8(v~M=4i#BB#`[0uN4 ~֑f)) bVe[x}_Z0\/?S =6vQv9l2@,Y+N`ybETp Mb3K.[xt}솾U(.E~ RC27m9G^.0M5.ZJ5>x)F1My8 TPN2eD!nGHj&DV EƘeAkWB WMqJUdOk IY&IGɔGU}uR٭CbsS5KR[rkي|8M麑}YH%g(o'F-,weYֶ/l,t兰yv¼_TA)%;Yjo8L }RzO7弌}@݉%E-i qGʉeӧ7I3T1jTc V*Ԍ6^EIqUmuۊH(.0I4=0J+u1NBŁutw.}JmpVާ!U΅P??x/9Vƥ!3J)RHrZyfva$jQYv¯jefGlgBaGCߒ#3vq %//Nu^a`{JL^l)DʔSVF%7A3LN仕Qi*K ްJaC'cȶ/2шg4]7Hg*ֺ[uB, Q/¥TE ,XDe[ 9@:R䐥LI\k"Alje#3Rhi8?&荈X%==>=nJԦ07evѝCadRQw;GuY9Tkg aΓb@K|ٍe[|cXT5ր-; 4iJ1h~e<}xx(J,YRƴ""1p<#1M^^ޤԚ䣈HI}BUA$+Z|tLVr*8.Tj,( t 8\/@vhKY^>}byÌp XWJ Lûj!bl,ES[EJ|\*8Y[(%6 UWXE+&^HLw({[Tmo|}52N'|L8/} JiR,i(Br8McבĔrtElB7twB˼N)tv-hGq$,D Tx~~b;!Ohk[RI$% 03v|6׷WE7Tk*np !W_c`]=1lլ^rMʭI9nyf*qG'ٷQ,>eB喝#;KS44)$ĎݹN!Wfo92ԶBzA?5]&_2&CO)[^KGf sJm|*H׶˪Wm_ *{؅1NiVk@2Z\Ҕ遒<}s,HN֍V;Q%u=YE-7A`\ ȱ0}#hp$쫧fNlARj=#c0/5SI!\6BNJ-L6JEWkj-U]m[Y獔bg{UJ9E0F1o-Tj_ sD <>?)1abÉq g7¶/ ToLiШ&S V=>q ^$#~@DT}c6mk 9'\Q\'E/K>?H:dG}bx:2BӋtڰ;÷]]QjTpCGwԤ$(]-dɩBd.wQ<:x#58g}X\SZq5Esi(^ Dc`]6MP8un7\x|~p0pr]MC/>Hm]=H`E&/S~sV?&߻8_lH~PMxUeiunode_oUFvZpUʐoR%4=IVQ%<7ν2 ϪBn+c5.3o/mݻVJct[~a)+gut-@ =)G않pr>ù=<4nKjbLBUtsAӠTKX2ٖUqt"Rr88NOg툛 ^c]Gʑظ9H5Xz'Id>tPۥVEB+|2c48!mV,Ac 3Ꮼ*^e"~+tÀ!t[Դor'[Da-: *;2;@lmGOnwdѐ]OMՓ0!np)B(/\>P!Icĸfp-nnn˂ߦ1fhR  +|ea%C`R6bALI7QeĀzT7\ tP7w*aLSZFIhOӺ.XkRFxNn׷7$(^>~d@ყ?x||$z/;veʼnM^R"0º8?#[{1ΰl+˔Ђ 75DTk'O. ZfmKgxiihkE7;)b?s΢؟: k%/ύaؽ1 I˶QP 0V^Nj!MQM%u*њTKE/ :i6LTctR{(Y*Zt 7RemݛBYX RTyȣ68 ![K-;g08vY·֍*ض ) 7tyM2Cl{S:#ieM }c/BI_)ewSJ.7q](umc[Wrfd타R`qW^Jr*^[O)J U"˶jgt8#z<a#m\-˼H|a(-`j"/r) ZJO]"JU^fR͉{nS VPJ$kPe*-Fə-zJ}j˼-+΄ҹ1w/W;vB-yyaoY-dֹkwahIZ*~Чuh7ym`eAk">`os-*}xzbY=>r4Vܮ?cS/ uo Q+(F%R+0N6K )aQ䅎Sª@*4ORJ2I+uRHb_slS9q§|GZUiF2g%~C0MLJF+X =DUHwt0<)6nwc QH1ZpU=GJuwt #}ߢk&hZ֊׏/"f I "m_-uV iD1|fYfz IDAT>mۂAcjXe;:׏,o 1x:zI[k2*jVyY GUFne.zȼ̼HAc/+D^Ī>A;旅~w#OB)K7 ,89t_ bhzBg;B(-$P9gTr1t S @g-C׏COJ2M߽7T2)B |zΟ֝w :5T$rh<τfa5F }w7 Bplֿs<<24-\4}@?m5[$m'A<ˢ3۶R}m0t j=#̗GğOp;_fK{onΛ_7k+ɮ>9F}'deE^=T 33푅.3D"c8^[*RWM-nWl.u"+I'Zn[IC~b1w(J.ݭ6EXRU)UXj.}ݮ9 r12N-%^_62+V4M~&QFAk߈0P(0P#mƪPUw0 B̳ 3n\n`)t ~`L\VYH!{TmkeL+reCF|)DPYRB % :$x ux:|p9aY:y0,3uz9I k2CNUѣY%94݉ۈ4")q(haא>+bG-K!I,Y#cZrkŠj^WA~jo68?H{iH A 3;>9+aPRfD]dm?We|믿^3_A V2_Yi |7s?k 192hg~:qPvhwQ7>Өxp>1,3a8?<{em=;RRj/bTU8x&ц IוAZ+(ŚisLg1qzB9>д00KZq`'.|?ZJb*kOˢRZ:%gjd-9[6 S@BO?O?'읟z@?2\{, -_9V+oPkD|Fp c}aG*rhJkdG5};aim5I@aGSsg?kE;q9Z#ykrU*,F23 }3ƸvM.)kaŏ%qMH|@ULCJfi!C#AFLPRlVɵͫGĔJ(J"VPX [w(9gsv]ΏEI.5D 1ZU N>xpd1w{-0+%UahLb 6ְ+ߣ$)rRZ'nm$݄FjN'nI?x5_p8p<%L 66 %2;a)v%Zk@E@Iskp8xau4KOOgv@ʉ31Hxj e-]:UirJ*m[ݞN0 JXJ)|7?3߽}эxuH8Ҩ aE?*!:yҷO7I?}ǟ݁s0sO3Ok,Q% jj9jbGa77/g[JhQ6ompl3H[-PU8/X)A57]R 8?0 ^ְ sr,-2qV3 R <^Ţg /^`>_: {!0eLբ9/s$vj" '7,2&E鐈;פ Ruj{q\+i)p:=o<޼:*ҏ.u.1PR&ħ3#J0Aubϲ{ O41TuV4ђJXV\nZJ+g-f%גޖ˲-Rh- kD\J |ՕR/0Hko9cZGkI%SSA+b ]ax<ŕm^(Pj6ŊqYfXb˂]0%3w=UjmAkKITJmeG+^=I,<{ڝjE98#ns,x?Ъ/(E9y#<5I0:Ѓxy-gyeigzY!=J)7vQ@r. Xm+h/W˿Hk\/gZ FX(!$颔۷ߢF:>"Fc97%[?/4zQ7&Vk`Wͽ᧊M~?kϥb?]}b$EA Vwo!meCI8ZfHEMViO+vԒba7ضth ӊr[ Z>m6{e?e^pFprĶZ%E!^d.YA7=nó=kx0GqD+X8Xe~w@;#:SelcJ4Bu;={n(_c M ]) IQT9S)C:J k=n:`SC,:@W^WYL"Wi=Zo.WgMU3z궉B:.4L41ڲAD^Ff W!\YMۆt?#[gvEU&Z*dZQ[y.ޔ0I5 ۶ SLON,=Y62;;r /m&G}?<$W{bۛQR|è LJ{ꩲ+;Ώ'#>WJȺupQXJk?o?1ZfݑE7 <ۏ1Jwwѽ:CbNDm;W/C0{~':0/E5#?m (=$)T즰Fw(-M=#K,M:917N{ӇW~6?S~lDzp/-V*T0R,_- @_ Zf-M(}*ߛ܀B3W\/W*U*xcc}[7qQf(Z*pmd0M] pΐZEPJNV(իZ&GM; ZEffCFk/WbtǛ/g9\fr!7 BRѭ4EfwTu8N]%n5fģK_\Jrnȉ:Qފ,dif։ʜtzGjH9nawh,m7.眹^A{nu&}"5$%A;a"PeHƂњJ vXݤ݀N{ip֫h@GN'5n xo(Qs;[|kPa.6&q(͔њa$`{.xx ؒt׻f˒+p{RxZmϘMc j-F)bN/n,4iGS *m4qC3aڳ;88v#J+Χ;2WxD#ki9/>J3XY;/$uZ+NX{VHCso݁RM 7d,|kJm} ޿{n/Q41Z*&ӎXI1 ~8\ҰM:5)m?[$]̘_ŋc(W_УaCGR;7p@n8oiPr0׵zk:8tQhu˾6XHx~6{~ԓހ|g!yr=͙sҲZo?l#J⏵F㜴/Jw0Kc P<ڈZ{%1tSP o^q] :/K*jZI~'I;l4{ 9^W_ K I7Va-e2ʶOZt^w{ܽ<S!QlW g8I, 0<^¹Q%a-p0YЭ1_mX+E~&d>*9VUTNwtlߊ߽waKač4~HBa{[Yb<'晴^it}Zo,|EĊTxP#=$ڹ외y9OIYr)p$),aT備%?B.mrt@`p/{/PֱmA bnG+w{܄r>QZ?QPwwi(mFϗ^~~1W/_)YXcN[&~_~3wg.{wֵ6$0 zqlh5DXօ%J=Ogh'@F0a{Yu9xwwo9==,3O/QZ3'xFNE,#_fa7 )ͷsgĸkͅѣwO\N ~@#^Rhh5bE9>.fTˇ{5o,>wrXրg2,2Xo%f3h,L )$׸a6TډOJ|c&L70vJf:2X cn}Wv;9?J3%!(ض0yZ"4JA"ZgT"D+i 'mR6N{JYxz|я\猲@|K,F5,$PG^ų/*IF|VXǾPF3yYVȎ8xG\zc9K  ˾֊Q 0^Z&FF9+qʷQxcI[tzwXc?PFE(w-&k%kdN<=ʺѳѻ3!Z/@ʚ1V=Nf[W5(M/(Nh v;P.ia-ް"*G) E/<|iy1_<\f3^=Hq̋WQ̅ӊ$Ro8v#ݞiءa13??I5RWap9y|zX~5t'_°{IJ,ȈNk*ɡ ض6{*T)UI0i7,[2GyLU0է'Lm2CUk%ZU_3Т)Z݉\gಚaY.44UiE8iY/]:c)IFuOm AoKEkmJk. =c^|J*?r;$H;:TJ,hLc4R,J54P V(CڗDJQ "*VUƈQoh&b,XQuˡr{ZJ8=`@zBhJV >:*0/0hܸcFUZz>34] +1Fn:=:JxtցR x/<_9x7`*9Vܺ[،(ٚww(zPx&[G+ dRKf')PЌJBrrXrnmb1x/A<9fJg(PeԌvָ)]UR4~ocM(cqΑJJeu-)F. T\oR&m,k:C\^he 3i 3 ͗_MJ |Fg+9uN*.qo &?q|aϲ!`v9"r=Y'9UG3R"\QZ\U!E"۳){S{ɧ~}|ns&_p><ßJ? $~PV>Je8et_ofQoC e2Psc@IlHKYh` 2';YpujE K&9I v#a fN-T$U7z>zbet©ߍC89ߞh=" n;y|kYKnʲӄXi0-rD ֌ִ d0F!( Iae^&'҅Xe$Mqw>n<S/Hܾt:q: qSBQ2 8<2 ZԖ:SȌn EP])Em N,+E0U-kD[2bs&ZK YDմh#mh$;HRXۑ<$b&8bD\ =wy+^x)(ˆ&U])tĶw<[є-|w~aUZЈ1a{2Wjk HML"HtjyC`^[`K|Tkț8{20KqՋ/X/V0Pv^#.W7_g),n0.8 9$1(oM,*ŠQF(ik9SM,vg%)0KSHbD ;d*o$LhGLFH1$de>_ɗ4U\)ú- FS'Jat1ҽOi5gŗ<=؁˗ό"Г/&> lZ=%HQNaݨaw! Qr=]ItWR R\}(I/4W_}RiKLI`%Ћ|åHFB"-YJAYQV+_X˺nLmk='x%~!- XQ0Ⱥ(mGc=Vqq<<3Ma# n SHJ yE^+x4 "bXJB2[b3X'y!%KkM&!%lRk9r=;~w3x_n_^?5?"Z#d/m(4Z=Fj%7ײ#jg n?Jm Rī m`7 Ycc#iUbRG (u%n`ZM8JfE7BY:!Zic528xcK"rI=(3hI3lfrGz-ךTΝָ0 Y#zRQp6ixؖ+'o:e=)M(u݆)Ѷx-I7rL;ZSU{sR)Z38?t;hDN 6h9+)H2t**rdkh#Bc=˼. ql[ \bu#sT9+5(|]qޢ_>,BUD%!Mqݻ9}lcs+>>jS/?tDԍOPA7tK"iz4 ASV ' m[Xp~8K ?(#\"ۖ(,+Єd8#۲8ɹby[Ə6$YgqJKX.nXP`6fixޤxk-PAJA- j+p9%E Jk]4%.@CNE j1$޿?q8>7//9x;Χ/^ac& t*x|xdyĤ2[X iūW\+z1,"n2'B ,{0ޒ@ 5Uxw""h޿{kBG6kMp2BW\pM{B+(J(Qd[^]H׀gi3 9ZH[%7-dXPVN|W5b߼E\lyM9GXfɻ0&jm3iS²xz`w`TD"LB`9ZM mpNfup׍c/mŲp8^g- 4ei 1VhMi-s`(MHZQ@-%?$2^%79 TEa-)uV%P 1%3eJmbm喂j(TJQ ! Eb +)uR$BDaH5IЄ!BpާP=PM%2R3;)O(] nGٿB**B69?|w>qX:*Eϡܦ?ؐ"࿂Xc,Jt!Vfֆ nb[#Ңf6QiwPeY'^5J)u-LoY(wn_,2vp &R*u2'@SbbSf6q̩u69ԒI)bU0Ű19o;FSRbYk}V$$) yyDUMϭܗDs^z"DpV״X$bTQ[XKG{ŧNSܦ+~p^zf'_{R{W$wb)>SR1 XE;,NtM;jP"8g;MJкwO=@YzVSس(sapܢ]U8kX[E}< UUBCSXD݁ 58c Һᆑpe$jw*se5ע/h7R̘KaOM4Z!,\YNDJ7F58gQY/ ǡk=G*vSMY<98먺vLM}ӊ11IR@X$,G)̪xuU8R IN5*;H{ӈh΢[4!T&ASRl0R )Еeâ:|R3&|fhY_f[6ɗbY}cLM==^nz( Gk5RoCqKAn-dOm0Z?-Mx zۚC ޴m$(a+b4nmtƀ#Gw*OX?5?Va}EufNx ##R LRJjjS)7Xk-b˖,V1e] Y6Sͳ z؊rkxm)Z6эKg*RV[NS0hJr"1EaD)@΍#znv:Dd 5 (nq7q:_:8a7эX7˩'sш5iI-`؆o NhJ"_hebs j( VeF!0Ck~H !'QVJIࢵ1$6L/?&mߔ,-}՗U!TJWǔ"ٓyo+[i=ߍpQb#@ 1ea $-LH,TUE&Y95{2=q㺇PF#1۶ַ}6]`KQ,2ȱQUdIUa,QN~VH n~/4)UbJ㫠(Md|a: ;|N#hF4=tŲݶГPhM?-Ni6ºV}߭a NưUOXuHR:w8FbwTj\IUl<Ǡk,`%!VG>cU0SL(U,~BY'z:_[mQH!t{.$1rбֈXY!R  QKp|44_N2"1ƂE Pr/ARbaZ+:j?Ԟ~@~uǿk/"?9SYK4@euo#*9&$&|cEtٵ&mB۹K5-]i2ׅ}Z)E(zk"?'[_NOG(h]gxҘ@0A* kJN"8 @HuewVi?jZZhܵc;%T3ZZ\m :J̻d}ڻiJkZ^5 @ C0466aҠ/h VuK/ y뢝&8P9AJMV լRnnUQVJ4xR[0Ͱ<$Ζ}t.|+&Yr%qkryK~F |eD]C(I75mի0tFc؆p JeҚq A<IJZ|RH-"=kMu]w; 1al i87a= P[35З*sO Eiǧ=)abM8=P 53Fywy,QHPQVZb8IBưFjŞk*KwB jU+UFF߾u_bM7?|oEx}>?..wWb+59XQN5 -2L jJTkE,jFI@:ޠ(B34YTe10,_JUd=FJP- ei Lٖyf2G)&פIGXCEl"-X`*SƢޢTOTM kx JfZX'1&(Is0θ8 \YZՌ;*P l8ϴޝmmejhĘs!mEfVˈIR1j$7$|t+wonBGEB-nlWp[{ᢓxiRMF>)]& h-;fno.<=>wub僼TEIHtC`=%rN)|r;@{V7q)i#﬇xZy? 1EwXfm]8NPIl)s.E'r95Te#@0Khl/U1%QLjezKεoB˥.dl U6J+ZUK2p7h-6pkԋ|a:fYBr*IyhXc)S,LNf1]ϼ5?~a${uREO|jXH!x1mtk%^03w?jnP Y;}o|;-/&MS_/__<'-^k,9FX'V*{䳷4J _]Xb/m%g\(-EvM`綜sS}ֳJ ]#3S@z1k%c2RuZqʱ -hڔH@Ԧڲn+%'48J[y3VD*VF"pkWXEf}FɩQhcZʺ2#kVcM9E`m1uٞje6#XFI[(~p` iYQrRn"YoƢP#nTwVrmqpĔs`o0sE;6=GRX)}5h͔m2n韕(aҩF Co =˺s8?r',w=Ȑ,#Lok4NAk;rJI a`ލl[IQhfA{'}"ZШMcO%//oߴ7};} o}S]szbd82 NNoF)g]X;b f7۔6걧ƩH^Z۶t<]i --B$p>T޽'Vq(ǽsFGJBRDׯ(JnҕmI E=-u@i=,yaM=-at+{,a6l*"#-jprF2%+d.: ;)%+Rl%+';u"rhnA;SºxAZ !Xafyz&2۶+ȻEtBU`O]+|5#mbg)^J-j)єWa@FaCkۺ_~<|b?zQm{̷/~oo +_6˷o?]VSxEl1ȧ5N %w 7ZQ*bRK6Z"i{,!ZC#TVxAi{ѱ6]I=ʝ0"!8O)DIAY PƐ ժ,HvBiR%A:8$9YX"^F7aVq&Z UIWYhiȨFU{%E;ǁZJ;UZHra)ѓ'L#&t80ד1u`X `#`PT2d^`yK.(  ?&)x3H+4Tt/_3_~O?Z,<>>aHmKDÛT! J?HCYjZ"aE kY)Qㆀ6FT[ᴖ MHuMR2izC =xPn?= x%PkK-0[HF=R=wq/)ȸ`be/P2ӺUٴq nܭ4PҪ@jQlyj*L!`p\#]9 VVqBRtJj.Za=k{PpXUŖZM"FKD Mֺ|ѳ%Y8Jk7wa# "$*9* _#Aa 8 }nv^&߮k x~z `i_Z Aw>?iE;3m(7z8lV"XR+)mv<+8)Sс4$ DmR[zhxz|⇟PJ#[ !b$7yǧ'i1R'ł W$a+$8tM&7jd|PqZ6bĽiX+M7R-Xev2\a˛kRݘֹ ߟmBkO=˺[(x"~)n2==c7 ҏt"LEk8hRGa QͲƙz$;yNc4{t10 [*\:LVkY㯪C6Oe>~Sr1u]o4$鯁 'vWWb)kJg%GJ.+5jN6ƐRŹ*iwո%I]JqLH帢Z1Z/kn38"ysBf/dAPnqCOp6ĘYq>|SD;IAk'U& BxZOdVsf=l[ANk2jY V*B08-uX4Y5jiZJ1nb= ^AEx0; Q]h;k I.ْ ݯ VZuT,_&+mFIMJx%]ZAKg<q7G7?ɗ y|r)'ӂqVWjgXV*X eZva QQ-]\Fj ~Ć`)6$qa( lˆ3Ca#1w3KץQcҊS)BNtɀ'l0\?IsgQh>RI0ct0MbM/"4sd]uY0&wFi`5R1 Z\ZI(v@)R)LU caF i˥Mِlpm#/:(etP\[lDڶOw-xq|SGw^ | M}[UuX}|A/޿y8O@/$lF[\QRjc)#5-%֊RCMƑM+'*BznnZW~޳, ww7a^)ZW WAUCۖkRJ*x?ȂS-f5Q^^%VeFxN2TZsoo%[D Ƽjm 歈P3(#jm>l 8(Б(,%Gz|_i uk:1MQ RiO)*wmiI@I:g\(g(`lzIwFNۼhуIa{BG]lLw pJ,uc/lgN K>9(vF DVdpΠUbFֽ 1 FUEC<*C; tV|g=vv4vZM3ۛ[rx\hqy+W k>02#uX3jnfVZv: PKa[#Z6Z%4,,I E2ȷwpT K09f9Eߎp$N+y[9-]:+MYU G3,u3-BLAYKNYg :O~3FjsXiM14ˑOhip??b10`|Ia4hjBⅷoޡ4޽(ROeEݕ ,J0c;mL?L\m5B"n ލv;RJ;"vhĂ5#p<IۯnƉErJJfyU"²DtO=rZYCB, *i/ CH".YA~qM4)9H;iGd|9Z+%i ZR.dB 8,.cxZM #E֋[1ҙ 78`9NrڨZNU s, _CDhPwQM[k[ UsrdaI\ |z 7u>~ox߿Mu/?'+r=Ӥm@Jݬ|gF#)p%7$/(FԷ8ōjn מSJ)V&("ݒ=k"&Ú"[ӉyOq}}''qZ%Vc׌y , 6Xv+ilk.jWL\[ È6DrdGɰݎikǟoտ% <4Oe `Sf"ےp:\}IV6)&QΏ?8֜-TKm=777`m`F'?2ߍl?n2vĘBQ7 5nF7Y=e~3w=6?j p<zq^Y(p*ih6e<Frv ,>ƺ ,ad9@Yhnf?#n:HA9Yj%7 `t%EsQHN55GF`9~ƺT4vX%BQ#9JkJs?kJč ђw{y70F? iFK鬈}ǽp HhC̱$S@N1vbzu+n*.֤-B_bK 68B輛җŶjX+-R*>^S IDATj8{vBd]iJE1nLCįvFn^"8y?>r8yzx`Fnoff 7bfZ2Zȵ@hm)$6bhZ38׿Zukrk(N"5ݢY[w*ˆuw5 MwAm>O~yV^CwW_]Zn[C! 4G+CndiF=e:uX[$@s[ٜ09 Udet"U=v<k!Ԫx=1`r+q3Ȧy3zj- 2WWnMW;LG m`-Ԛ}w5ue3~/y 7\μ}{Oz#773}6K?ⵣi =C]s4PZq:9ZwXHg_x% ӎ ~,,#Á㲠Hk21/-{2xKZ Ѩ3Xɧ6XVdôFs}sGk'j+ZCb( ⳎkϏ>Qp0͵\KPZ݄g-Ñy#S3Mru^kXk p@M}Z["nFTЌoivnn_wtȆ1R)0HG[ZSDߡki=Z!̓3#e>~t/{SZ&[<|B> h z+A\R P̾bN; R]@uLڣ4s{ZKT8k E PR&X˿G\]]؛6dm49'vw=eK$Q[X 2?NanfjF9 RC+9VDLit4مQ$m40?^(U9ҺUYq[rXhL-YGkܽ~ͮ*ˁ}3J0^l[{u@x|0"tD\*x5]Ek9/Vy||_;w; kaYWÑP9kU<[LXg#ƍ$ETuO\__. Z̈́a(s,=󸃎쾾&#gF +_ݽ;wy7qJ so Րh1SIx Laᡱ.'C A6ގ 2x!J'+eZ4@U.N^>3BLDHTѶmȶEN#9 CpP,qAk')eY׍!XB̻o~#k~ Vq(J1V X}M@JJJRQYmc/kW7?5~t^׏x~W#{* o?/H)K IYHge  9r^1{k~}?w'>&wѿlSj?f*員7?E|- w[ @*h.\ 9B\9c{sNXafKg(#"h Xk{@gZFȺ}/xP [:]K}VcNXkyR3֊ jX֡ H%u[T'Tpމ61ȥ'zTJэɃx=Pca#aus[d m@%C\"m-hPl+vMWQ2HCIPM{v8xwDЖZ'Ryzzf&ԎmQUCw>fPy>/9͛_rssMʅi?zʚ/.R|.V=MWmk  烢R5&JMUkDkih|'T:h@JI&$2Ʌ2kZD2~jcwuj텄cN( U MYDh [8PJ#n:ZdqV7t=FaJmr}/҂W r.hVpQV$ͮwx?y|j_{} e!p~>e:Xn_̙q;m 1mv6,+KzN{beiڛB u^xxe]ed5?uM—>Uy2ܧzw[8>y}\e9d /y3nﺳ`}t\ Xiw}E/h/"Brw9,pmGs̛zezqHA d {`\1)Tm5@uh~)\6T'ygle} i(RhUn^ץH'452XbNg*AM|MC]܇ŠZ XNq$\ג〈cV<φEӌ-@Χ_ ̲2@`]|x͸g1\+slloۚm|k QnY [0xV-9s=WS6||7ڮ$^-JcOGG^MHdZYO||qQQQ̛#aQ9+߉5L1'Ůyߍj+QbQM =9[SJYLq\+޹!ޚh͜J7SΦvlNa6EU?`OɅˢmeQդcR=>WgZX fɋڌ$I)#Vj-TL'Sp ?W|Kukkuu$g w~w Y%Yʞ1%dcwӀ ֆ +'1F`EMZ 7iE "i0-o m0e [{bgc-2oSI٘ 6੭v͆Ag[fn Qr‰"wͱ*l2-0Rea /ΦQ0*.+It=Fv-Ggx(Et5̂tUmJW14МC[/h5ñnTМ (V/!&FZtak5Us;̗F.hH1tM4 Zh^i24uպ4m S9#mJ)؍i4d̫gJeĜ{3yuwV 8iݫ[fCԔ UpkjETab|2c8Z=cu֒B`ds\p\>Lx'O\^Fk7[cq..eYr*XW[۶ab]] |.I}AͼQBRqgGv'JLeZ(Bwv!tZS: SJA^%w(wo߽({>~)_t^r? |c[|3޿|ǜg.}6O!%yCE%#Y^^ȓđɦbE{k78\svH[YzuHp,G:F#F,d[G-6OS1D!Z Fٜz笪ƈ!Alڻ=`0s_%3w+~̥@^7'cDr 241쓙Ɍl+nQEQgICXkJ#xHSJ!pw>Pfu \5ƨ^ gb0+`e+!$)%6htG9ZAh֡5jՆ#]*%up/r}ܵ5v^lރV3Zvcxkr+x^sy}5#A&i Bݫ;7'0 ܾ8ON3K!3;Р{nx WBq".0&@n}uV+V4q!<no{)n"%InPhƒ m&rqSJ`7g;Cjm(8rG"NW9E^XrZ^y#vZ5gr4j{O~ƕׯJXe+Z?5)ڄa7cĉ# !IΫ+V ۺC"/_JR2޿{g)o1?4ϧ|Kk &os[ li}RS:F`,{'(v)b;:("Ez(J'!Li%jb!AbnE Ҋ2nv@  -ُxo=iH6-eỿzp1\NY/)cfx,'O3CscYb\`p`lwA U郑\NNLlnP"tDhgCϢ%1~ fLMq.+c]{ Nz|xgkxˏTuMܾړS0˗:6z\oo޾{uE9Ո,.n R[OV[j۴yr u׮~[Cưh!39Vuj o:l[o1D !w{a‰cAɕO3cv7w'Bl9$8d?Jmjhj?&%t´7>Aj2[*xu41[XbGi C`؏V5H!ؔb #uvXnZQg{!n: #qٖr3V`ÕmN@w=|t:qg1i7]9.K-3պ!yb Dv3;b|㈊_Uޡ*p[a$@1^I!unJ0SN^<%o?0|;$m@\Gz}~>{ QԦ~}˸hՙqY{EΙ㇏N8 H-Aoj_pFɼ&WyVLؤJ/7o^֍ٿK?S/ܞO<92'Kk{_C޾ep!˭vZq͓ĔG*aޛv̧#kRmg6YW6îH3fGpBf3=>~4ֳsInL=B*fYR`7ޠq>{4<¢f*}J0u?P932QC"xDjZ 7=pb'RqDGEd?%8#VvM%o!ck1"8ǰYGJ)d`YAdx Q$6!JS<Jh+3ƻ`uyZ*!~;EgM^?!+y퇻Khm$(Z|%3q6Zt>!+TAXζ{j5òeq͔XJfW=jO!>+?<؍/i5h3`Pf;}Qn>2Fj͚֚iss-FLUmiP62y3߱M9ZtízOB#Xi%HXV [ko >kbFf5Um=?|ҵ79y3U% *:氣VO VDw7O:TNǕ&m#|jZF,[ȋPh3.ՆkLg:Xp#\vd9'T_֞/}j_x)X uq/ }Gcw%of ccsk&(qpO -S M^6b4ivZh4W1НsT5PwƬ{;!b Kc@ʆ  Зk0d!s:ro@=0Iɋ\gplShxU3[^ُJ6^,{5 O @D_cd/ucƇdή)Fb.\Шxq d?v9VSּ3HV#mu}ݪ]hMV{1@W{ gI:aP}G!)ҟt^t >b߇8\mj@.9֓I~7ӎy!ҀS Ir8>r{{B8/GO|:}s{s4 xqIinO<<|D~<>u/ֲͅS at:]5kZySN'ԚiRt˲Rj5oR-g[@7;6#k"!A.Ȕү#zBvn?xLb(ݠN[jU\݄2> ('K('"Ja+ "̶eg$FV0B\_(g猼XsO@L)e1%SK󣶚{=Z9%jϗԯB~ Ơa΀ _oTe*;nhx^⨮ k߶#֕ׯP_SKf˶\adyqL>-<==#{z^yb9hZptݰ7”vGi 8N>nѶS`^HFhXvѲ\kΨ .x\ny3/ASqHcG# ;ڼe+=Rג,DLq_\䤓6/N \%gpei4 MӀ8=>Mn7Pbp]$k멊8a7Mvj ;vyT&z\o_z>e=O%^51"-e0gfeGSaA&SEמU_aʶ~wwEr6yk n&Og4# lݾD dCn֪CU! ߶*l'XqG,lòjq>5@Lmۻ;b'd lۆQr|>Mbs Zk8U9y+4ѪKGCbZtJ8ͭ08^m[>KwBu<\̢󕰦c?IcU);x .:V,#~f!,Iu7Y]bjh qʖT)9[d(f"lB AiYlɽxGLPrerl C{Ln|১//ǿ_ U aS:^/!! [f],dY3Ñv@sC툣lTM[2M!FB<=>n L~4N/r{ҔJBJȦ뉵omƲDTAun tVe'(4l9A㑼e\i.g臚! M]T#9͏הUҕg!8 \bR[|ZVP/xk"pҙܶO#"5\F 9[GviMB":Ƒ֧xyQya.!^6 UL>TJ/!*9vxkU1[9gmFB L\Q;ʫ' yX0D$x{vI)1'pdfkV p4 1ᣧl֘5wK0^hmhqZ5uf2^sea^e>~f2dMS,*Jbo'P;1uFV񗄵Ȑe}{L5ˏIZy~U@O_[ߗ^$_ߤG~7RÕ!<3i&e&Zl!ۋBt4/kMΠ v{nn9|܍4l֯\_9ND9m#@FF| "{c"3!ތoDE )BU%q0֮,eQkjavg H"5b qF`2ưP9϶bpiTlbC0 Һβ ~\CQ 1Кgs!+Z=D-lTC"3;̺fнnlZ+8x)5C{bAhל7@['pgy9[Z`|'bo >R0,ߨ}-NDn"rwETlל̖ ͰKeTҜeYLYv-`ٺ~Z̷ Dr.OO(Əxk39Z;S2yYH-縹wȺ7<|zZ^thyk.D i{ԭH5)D,yP~m(Vr=S1WZçOFL) f$1 LuK*p(eST8rm+c̀f@I\rG=6cY/TjEO?W]O+?\ N:`Z3X/ڡ=bu}(hM1ҢCL,8̇rH38|7L_}"#Je9yU+~ЊbqVnsx$@fޢ]c\j;;TsASH&qm`fpl{rF;̥U:#[q>SZ첳P"&t$Znqxq}Jnk;t.@5's%A3m+VˆbֹR6jE1kݠBRW4H0yGMJ+ xɂZj39I>tAGZlיo~0=|V 61wFF򝽝Uؖ0\֔-(xzj´6[s8\\eo3u%DS$F3{3("Z{xQcG_"DO=- ~{@ Ӊӑe]:<nnf;%TyRtrׯL+F u)vI{{-T*ҥ U5%u_騃|T1ADGR?+B4bs\2Pvib.R՚R)݃\P_yT̩ %Zڶ 'y=wŢl1.G!Fg׊7ěd߀ O_֒9<} x K$H<_ 7֪]4湟|JT!A/|`^b| >P\ to&t"8F~xqϺ,Tdy%uRvޝD PԊtO+!M+[VR#BFTeeb]3TF72bHaec^:y[֙.bB4tn0!Hd`+FlO-`j bi1k#9v;uj`Iԟ̑9ki1l*`)r8Qpz:ӤSgXT챥g+`)"GCuWa?M{ E5t7qә4$vM(i7"JPr.38Ex:?&l1&O{֚O"/h^h-8aӺ\.dɎ;~!zl q|_Dӊ:ŋgdk9f`Nv#8G(T3`J'\+jX*ӰFt6JWT.~+k!"_˗ZS/?Lo) "/ԾD| ;l__~|GH|MŵBZ)cf[4ø3ŗ*Ў*pZT@io>=2 #>Ak~!l-漡E񮢧ZX]a" Y觾.HRw>d돜+1yb+q׉hsՊ ojSPHQ[e'{5T(DFR }a|9gRIvAՃ8R~kbb+Flj A/ !8sEamA_Qn_[Y?Ze3DB|RR΍2}r8ܲxGnPu˜ȹL .))D^3z6e5BsbT4h4se!%^GU|>QUy (W' i`Ȕt`΍O«~6[z^f^{QHՏtEmm|L|74;_'v>#6اvu FzMCV!;aL3$!+#M٪LL <q93Nc ƽVJgډaͶxɧnfkT͎IkV)rEYK俗5C_֙~UORyf9]ͷ߰m˲duO>nh0_(szt^or)}Rtݭ]c +m5`-͍;y*jqوl8N8>>@^ j{zk~k.1nL篨Cⶣ22;'?uehz#[[-k X|b>ZJd {Dg[a#]ycV-*[z=7f C̚):?_3}UWKhI!\3p7ue~W~˸D?O2+Qh́k6puS `hD>gd6̦ѦۘkfD X؇uɎ;F*O t֌@f΃҉c;/כP-n{J7c$XUI51[`IIUwZ=ӹK +>yX\4yh #Nr9+E׋~Q7 .8j*f֌;R Z U7:=2R,֎JJfL4 0DyEc]WږnvLZ΁'4m(DG&T`H&q5%KmᜡY/snֺAlzXMx՝c1#˼tFeȺ,,ʚW_fƲS#"jBƭiSi^Fra؍Lm$HC"w-#>sL)k@hѶv^-;+G5|"dKwIl!M;2VXe{=[T  )\pBXx~;ׁSHӌԎkV?qRdYI6sEDij\.{x`_+_*~ue^y j4_>x( Љ:jt@hM qNh YVё|dc8FbfyLĚgT!RrfaPJm <44MDfZZxmrX\7#Ʉh|fc697 i̧AVM hJq-\NV+bYEmqJ.ѪE+Qa*CH–Le0YD\2>x{6lh҉q;zZArڣ0Ė k!2*HFp r>9teǏ)ke>}zd<=irCOLڗEN N{c!WևP݌;_Zg!Z Ƕl4%6R[\!Xb۔q aG^2o޾A1D͸#`nl-^޳qҜl?1Du-ԣňKȁ [5^:){Ymj"O)݋E-zA_=Y2Vn*Su|ިuC#s&DP&]7]V Ud[1/ѕ亙  45k,D~iW3bn//E~/ׯm~<ޗ^o ?b g.ܽ~ehe^%\YBqb 伱_#㔠Obl|0 )ЃW!2p:8:Q2{#-ٺuGXA==jsUYx$DtGkwU!\c@m(d~!xqʖ!9\~ǖ/NWD 9W|2ɨZ7QI$9o3`l΢Os\m [Z%Sׂ0 =B׮wU# {jV3.EgG8_TP`3@+Ck9FNThNXKk+-ڿ xږ10 ꬨlet>s9VM5`La1LQ>}hh*Wx|d^m,ˌΥ^V\ H'sKz,+gJ o#Oye7ޘeGҰCӈPmɌd|69`:n+oX甶6kFzwH1|cu=t/CHJzj,q p'gwsOGV d|DҀǓ׵I,Hi9XB=1Aٶq7^׿krG޽y|\N#!K imO O3n$ܺe#(&SW#S纑gs&D4KO&=lWrm\RR/y_FԄ/_+?[?=j i)$9 pyy{3-'+G;C lJ<"+wl^}08)CmhcM+A8xy||`H8c$W#5ySMG2ZCG-5ŗB7)e^ʊWaw`kMU j~i9Sy%m19hr ,`*iz3#?uP[ɵ1 F۶Lf9ZѼeJ.̞aFJ.8ylYT 4^Hʈe-fV2q;[B5RW{5yvk;AQ!DqT0E[jHH~;r.d<>}B~G^2H`ybxeS8Qzsv{&nqF  H".D|4f muӧj{aəØH>87˲PPno$֔aw(UsVT3ê&{:aE!2LpbZAzhg 47wh)4mmg -oD }ppus ޭܛ ;jYK'F ~Gᰣȱy! dzs^3ّ*"GE ;bg@Lka6(R5Q!#٤ʚ1j=A߿y#Yy\㒕 LCs#'/< P| hxh!1hĠKVeF9xX,<"e6G ]{_>E_? [ x?OnO~SR?;=>۟|ɾ7Ouc VH`g0/pF*}r\{f$Lj"/~p5& nBJӮW@5].M I`}pK{+ ?T,0њn?'׹zϻH7J;ZCFRJ1R9CfFuh'DauM5  f֨z)D17BE-XM飑r&8=ZHS 5U9R!sZX# 5A uyVP㈳pk=6V6|pO-k1֑y[C|9&/9SsU2.xo?\g[I 2FAѣY񤎀Wl->|`j3U>|A>-:?)p_Wܿz} P֯9OM&!N3zC&&(y8g(kpo}0]@a<1߯"t ͨL|N7Z55ՂAeӱŞ:Z1+y~9IBGraliYv)XzЏth:dJL|\}ԫ W9G*#JĘ[\tO'e:K\&xѨb ÉЌqK8EêdSګ< wO&?ŗoh|`/ZtqwiLi-dX-˦aXy]dh+C:i*@w D:-7o&¡bZWyز@ڔcadjcH1G%ߊQ2x)>4!zu:JSnRiM+GϼrwQ[ 1Ðj۴uum8.wUƾ'+][|)z:"_?E>/<ķ_[gT x? Q(+>GXIβzOcga7H);7zSn@m բdgP4d|jƃȜsL:u#\6ӷ Yke)Wτp˟ 3u+>m__* > 6cR)%16oG?~rX8]@ buxĊ 7]xmM.ι0/SzeOSPˎ1XXB>'Yc7»wx?FȘG2~0轓F!Wh"0 pEyx[HW&t s<&BuXE5C@f0|Ҵ6it[|E>2LWV7#ghꌭll3lLCVJAP9ghf:HoȘ+rfltt/g^`k;ޑKck-oNV;yQ5Eesު7o[,s?s]Czo$!b <#6JVhB n4a7ys_AjV>`r )^"CJ[7eķ~ETKuٴ^W^p>P.?dqT!̀ѥIEuTDrUtz=j}6O :h|ɂÙu]o=h%r`Gmu/D}^MԘC؏alfȦTgDhyfkNkw3Bw߼O|ӡm`;D , uO yJ+7yL7E=Fun}9;joD;H5w,8 ;08s1]Jj}H]A8iԔ ?ގǵnVX.xE謚 )0CLrk/i1W׼{率Gq8)zEq)J,.Фrz:O"8<ʕA_w+)pəΔיQ7fӅ*g):N :{˅:h#4~Z t,T}nI-эUsӟ QEKEF?J~gbns)rͯ~7on7,XfBߵ]2jdJQF<gM~޴n,޶q֙_Bw әe]02R'#秋8V μ̳veБl&?qRvrj[{@&cL&Hj%k{:]@]p U%zڙJ7i"C&JڽPU~BoOixNpO!#K,NxLyf?]طBklMX& s4ox*x\ R SkqbYg~o fԯo Dm,< DP[ivJ FiEm}uhTD߼p*fa9@ o5[]^ml)g:KCYFPwv~ z}VVfHC8:\. m RDcS 8,_%.=q촦F&5czC~Jg:vJʚf.L@ݛ!Q3'FۀHUk:&&?p&nXhu"_~eK/xQE2~z(@XG) ,]!X .* 0"Ph7<.79_.LEg_Vh{xƼ.T S԰ PF:N?e7C35Fylx4Ijm+oFmx[oRT8<똍ȣ1GM2rGAYjͺF-]Six,j{̴:z7_o!5n!E`㆙4&%NFE)eq IDAT4Ve - xgY/ KS{ 80("|,zJvU:C`ORuGy33.]s,Zmùm(3˺Qf:|8c[)9(B)zwGhQ2yʞ@I5| j,9gRM1ŭU^JLw4]:^sUbНk;8wӚx7~2a s\NZI% 5̆3V8)DYOcN\i)ﻒ{g5$u1H7!%5hR:Ȧ42JJ]ͿB#(;?5}m|1IPx(w, H_~cZVqhA%aibL#QȽ4Ez tiʪ`vZ wξUۘ="ܞT4d3qY%11nXn^x2@3fu3ߒ0?hQ&d2R(&<18ZRKޯ,}nJTPC#3n xQg;KEi7@uzW;^ zBk y%L1`r<,9!~ BT ! ̈Kuj!m3͖%ZIjlf7&}˥Co;"5ӓa'j֎T5u;ZCn :{uǻjĶw7` jaw0FhF?%Y83V.m^[]yp V@upɱmoȴZ)ӭ #RR21N1J X7U7<2`gE46ghjK?7]? B>p2(> hx>~_+(][W`;0IV\6-cTs{wwo Vq. 3s:a0݁Z ۅ9DztUFvY\RfzjlqWBqn>Zd5` uuQMqiҘbua3uecyxx۷ )Ac՜Gn<:0Dn`%sQ2juQnceDΩQsTzr? yzZR~u3*b+9q8nl)ӔDŖX aTn=lrXCR 8 jNUs"2O8r-YkM7[+?Gp:{bYWRzBTnDmOD7cZTON~%BVy/V4T^9!NͧBʷƖ\-k3w> Fq~}+jrW?^ UR޼V cV'}RϙZ/:rCr.Z;?j*^ض'],2D_e9 As/jV]-8yR={A޽{O'#-.g؊OH3R;gU ^GZDd Z^&#B߿dS_{vߧ ?3cP@9'ϟԯ~s^:4N?rҠYĐk]י#@- u#1ҺaKE% 8kNDCzzR-?<:r8.cn@4$O b{T%IDk Lލư.2a U?uV˿ d0V {VX)%s+q8n abޑї9uS(Fs?JY|l4eOF.h[0 Ue/غչzrN-;M ҌjֽQB+wzfOpfi׊#"11\FƼ?Mg< jxwf}\ >FqC'mY!Z~񈵆y HԺr/|cKyѫ2(FX9Z[ÖX-C=mXq!Ste/"IVGJbj>YɁy-NՌ %WΛ rI\.*mNsd4詖LXhFh!>cH] 4ÈmѢmp"N3yaMs4פ ߔNmpT?x;jaLjgH:欥7%+NZ dt {!Ġ\kdCSw>AL"RFkI`D3:]/6nyKkXAGE𞯿:5͆sK_mT<:P+|p 7ϟ/p^޴c4QRpa\! /cj V ~Č墳.fj(MMQ0[@j9JS0P50@m #k0H\5bz#pwXuPr1jպX:tK^Q8gyĞdq@/GSՕ n& ZRqX}S61 ҡB7Gk,bORDp q%ƀu.;[*Y;U?֫sSXCZX!#9 &s@00Sk6:\"Z1w8i][6jTcpfJMYݚG=w}-^V7H1<ћK_} to` Vv9;Pp,~٫ mRzCoiR`\s뮹hR`fY{eܮevVlYt"Je7QUJX(IoUP|X,tSjӈp{?Pߍ6~C0(]P06+`D۱2ڇĵ[ږ0F.S@WAgEzk4"75ԭa'3aOJ'iP#x_|K?o>?(hva j+~T|_ p3B sc9J>v̮ Ч}K(.3vnZ }aAcLZ"^\|d 5xֳInӎ:4SFZa޶Py)e&]60']sQk2-ce'uhjEacvb0OpP-:׶V1WeC+6'ibOD)#^gA݋B+I`W = 3 (W1M M;=﬊ z$nsHWg-H50)4:{N8LJK&GJJtut$4KnJP 1Mu=kB΅8rd5tq;Q0OOt:)5*4@iQq?>6=aY5.!L3G|B/j@ͅKIݝ!r.c,C˚g%׬5x裈W:Pt؋4LWY5 ћ%郃[:֎3+cbAjuU}GP3}=ZdQC!,-EiN3hB7DMgg?CYk]GÏ )79ЁoC,'7GD*իUM3a#K4Nz,A3aGpgr )_XL~3]ulp 45m캀K7nHegҞ(TjjA 5cӵXa'k˘L ~Sh\ת]iU0,-04*v\2aF) Ms}oajv c_M'ն{k0QvI7&N`Ry^߱LJy *21FR\/kiY%_5kc`Zvjgio=#?'Lbq{e80JSp@DY|sQsEn+ϸd5rX(v)f3.J4s"ye5xH:clgZp<Ъ&ʥ4:`Y"֙T8MhZ|4yU<|z_e$HkPz fé TAal7q#ZmԢ "]DEݞMQRc+X-D9tÙw+bĠׁ&45z\ i8k(1PK#xꥰs{wqÏ@w/ݾh@!_=+uUU@&nEgn[iW=.^mQ4M.$Z4kJنPO)q7K&,WpotwP';}iQv19z]_¤@t(F{lPZW 2MR2Kkf0% ro<Zߩʮ* #,M:UyҍF'RP:VX.K#.WSweιS4NʮwKOG'1/%@=_԰[*?>!NQ_y=kf'vP5`W+6P_ A9к 8)]F]elz \.|/ rzĀs}8u!JəmNopptK hFIs1d;uu%۱nxু!c&G𗞿Ơ X~#vL9?a ]sa^&V4[DoJsһ4Zenho]ُsBn[0> w~s>5? q(KKOh!p;9WFB}MSُqV,V$iv?6c5,YA'{+oEq:;{y7ި?8l [MF7è]uX Jbl0eRKW+_~ݫ;}o0,Jk$ `>X ~9+iJ0@\(,_ B JZ35i/7p~0-iq ;[roTS L=3-A ԺS) ^yM :: Rt#`:쩀 ]em_){QToY+<&=o;em\cJ /,f1D;Lo.w.(+pb>o.-e W0r\zL!N XZ^X e`-sˬ,J=zJnd٠kN-Sҍf3aRa/w)״=at^ ?pY=wwSPZ*)'abQkG 0 J!z8ࢺnn*LUt]!ÎbWyb"\1EغfZ n=Jd)j-79t-,jt**$]S"1/ ){m/⥵_|? w?v߅|\¦~ρ{t'B.LkPnLGD~-yHpkWØmid^v%2 Y0^G^Q[hӤd-OWLq䚕($3A|`PG?FNGHzZDض OO;1Z.u\STGFy#%ͦי :R%ѵ8 @/-e-~'͎((`s,Eܐ5%ȥh<6@:dˁ`9qF ζe~; PHZhWZ d,^sq鏢zfRL~5 C'_&R1_W u||7BaN z*/OCygw/|uh^]EȿJu IDAT>q^n.W >U\xQȍUh&: N%.QcVje[Jw [ݺ zM18}D'Uv}:ߺ+iɹ13~y Mug#OzD^_HgK;2}ȽQcoW2 <5y"%BD[ 葪qwo_q+^uj,j;xhcL` |E%{n bD@> )g4|p4TF;a\o]gpJIwg NI8>`ȸ,0E3 %utgqƨ4>|БTp4!4&%^34ᓯJ@uhFͅ@Z=}~jA }059f\fF{ٻG⮃8:z>cr:5ԕSGj-"PoDPƘ"L+bhPΕ9/`nM_utRT=5<ڋZm.j/쵫T,ʇ>y P:43@t*O}-֔_KU;4ׯk{ |w|nt,kGO9;ET׏*)IAۗkJSzyطm0 >ШD2༝X癊x^''Q~u8i&q%I"r.NG٣7=Fhe;m(liZ4EMN/B>3qhqPzYV*6Qz mlegrQi$oIɍ~Z!Oz'FͪJ31SHuqҎ|t$X]EJ.֘[ǹH; 0a!7RnѲ]m`{V?ciۣ5[63K*8X`]WjoA=ޝ#L B;Vy!U`t9#&qЛ]$n>4Sq`p?dD745Ԯ¦J1juMg1hHN552c p=daluJÛ~\;Mkaqi3r(JB{,wޯ{="6 v @h6@0 !W,i!4Fi`TUWfUfUMeeDٽ:,kͰȨR(?"{yQ_#1xgʣsLl'S<nR\! |>6 z쇎4;,f<Ѵ|'>{Wl _ɟQOMoC>>\coc@|6kv%s~!EՍi Ql9྘OĔ_%mmn;\*رYg ^e0(d^pQ>^UhZmW4R9Cl2m8-ghig.ek7"g݌Ja9@ȋpvsGx<挗7^iإ" z@o&WĸH R{kvqxs"/ -%᭫MczGZ3wv#v:;mvUh]ÎyڶxbY 5 Ф_u/]G=i%炟l-(:gi^LV9zb Tݶ/ {a芋R#=MZE)sV =^]I v745z-mbyou^W5?jk2MYӑ5nAM+qyJO+ǧ&`xj So^;ߣ^ ko[\cQ)?O?1 t'#y;҅i7 $Ӷd4Ǎ{FD'cSD+3u1yPWو`:nZqnߊAi"Ɖ\38UkT1@:AJR `!mIe8r1Xjp N]+ÎioS-8E=Ӽ3Vzw@/_EI`2n)iDn!{7{O `Z- |14Ij7C]dvV SNV3@J,3vCwA%Si4kZcxqp}|(''J+ӼhB LZ,B9*{o:K@jUxO\SLWmZhYCs-[Š@I0[3;WT snV`iS6;/ 8Ewz%~[Oc'Zve;meѪ1z5"dٶJtD:-O9KW ??\<<< x)b)6a"nN(1[O\v,{AS3$pt`A/A:϶e'ͩn#:Y}zM4D\4f>LN9,6:^,(7ϺUb͏`wqxv"\ ^X%>D12];ZX{CqsrnFDbbYI[*Z;@pF tzOu~6N1@ǕLf7(%n+>Hta(<\o4K6rdD,'0YsB@{c@W'|dYu8f֫CFm}&*,:Xڶm1|0[]4 5;gil qui`zg+մ83C2eZùnT(ZSc7 [L4V7\jvȣ)@RyN`! %vHa Wغd/_&$n 9 uؕ ڤS6ٟRa'<\DE7DՆ_Q-< ??s?׉Ƿ?u|'$_r|}F lf?3/` W\gdՏn7JC52lXiAuj-̸Ef9n؎+m|ͤDō.bQAR#ovSi?}2[LfGL'd8-c2KDŽ;7=Ӝ,^ (HQJnLky?Z)lbO Umd{% v>X~_[& su:Z2u{enmegRH9A&~:g9+[3 y LِJ:S`wrㆆ}gN{;sJ4U8Mn9d!8; 2K+mt aFƤE%sww:#a뼀zJ/tW jihVJaK:\!+@1r B Jm>^5_يy8ECԼ?XcEb jDlךs;CԍՊ+(7whw u!nν҄J3ϫņ~靠U"b y< 1P6CO[~Ӆmd?<-?|u Ўw ok>$bw77`Jd g#ل ݙDއ}0n:Z۸!5@"Z 4FY2ee[6ۅZ/_x:َ{zw1 rSY@M˥ě"!ű4vYKt:-P ngY v8˜^wvkh*'!]\Jkw ARlAlAt"w7n~@Ce]3[-ЬU1<5hd;md@F{ct)F3^, =3{W5qm׬OMb @@"cZ{FiIBpw>~Nxg2s$"KLˆ̳̎XvZӽī6#q!?&Py^TH @%-:\#MZM"4|z[ӌм# øzeͫl򖻛\ W .Q%nbJ#qB8?u N@Qq폄N/C`7|'oo7Xd|*ߔǽ>~o=P+{pPWX i_kF nvgB'M5B4h\㺻$ 3t}\F[OGvTZMyvQ/_Hg!Xj/ -<CPy:Jr=F;H OeS1FOI]TVXϟ1ɴBqfS{b{jn8^E 4MytV GA/7mMkjΈ4@ӥjjrΦsVJjJڇĭȐu:bkc (tҔ>=q]l/euy~t-7\0qxgh_6< m(Ta{uG3rDiUQ'–k͔΅ck+C<)k-\é?$u|Lwq[%kq1XX&Gr<IDlre7`Z&cc.i?}nڥajbAJ;|)"DhʶYԱ7af'd;5X c.*Tk#̼T .[6 {ny7v\aދ ʫWj4A .W{Գbqj3yt#XN+LEG;ɽ@3IRBH\l#緕zYՁ[ `Y,L=/Ckx-|) ^Rp,&CmdKFphwߕu\!9`qi`K/{uK}y/fo-߶@௄Ƚ?O6{ܯ~KvM%yZ >KM*i=19 yzA'D{q#Ttl?[{'9O3DW%FҪ"  |&/L^h $&VӻBdũDJ3ayOr9W:ԷRg(x1F*<1OV7rdDȭPfK1BtДR*A`əa[2C2gxAΚ,P4QΑ2"s{Q)u4?a'ϲ, MIiNДmԢ^hQ*e3"̪u !UU=իzh%|=|$H n W/뺍)sk!Ŋݶsެ}7Cp8*zO?}"MwO}||?|@ p}Yum ? _Z5P#a"] frc&%ؔqۓV%"b['s]{W^I) [^xwՊ6mt)EE.dm:a2U=33tC4.P4 aҠQ޼+[Dsh,Pdډ鰻Ԛxu5VX݀ƭ!ֺIC|=/mܵ#xyO bBF8A:[Dywd >yݵ#7QF@ɂlh#^XDѝy8u=clCFHvJce4| s/k.X zHpn)>8c7-n8O6V Z=жcJ0*Re4ȴ ,ѳ٤Nh$#g>U? !/y|6@AOM״_ IDAT,x  x .>A w?-oS APpa.B'4$VC1$M7ȭ-k`kN<L"W2/Dq` ^ 0{&.ud^*y$[,wwt:Sqspb15WslSa7G+dLB,TvnL{?;3tmy~։@E)c"I$&meYO1Z+nܪmTpQJkYo3TZ)a&Dnʦ)q4Zk Ҕj`X]^Y<&Tz\ Hk"M4L& ;4qZ2I)4|C(kj5$/$xǩZzdr'sc]*Ϯ D+5gr.t<~~EF(6PC[F]v(w']ʶ{ci~LVxa;mC5T>QԦpIҩ !._~+K,h$#E7l55PVW2I{r7ދ>?կ/ySmm4o>--oEO}_s3>/~O i_(5#pբ+GDAi7LG uқF-7."p W LO =8iB&sҘ+[ɔGb{FF<Ե+) aqڭ4ȁcE N;Xd ҅\>^E-E5k;|p5EqAc7--Rڊkp O<7 7}  M#j}u ebDNifs:HmS Z-ĹHFNr)J=)Dd$Fxv =eJ ۮy4ӷ)'S/l[chM^ s1joOk5;`7J\B8nnyGniBtnA[ [mfd=NM1nzsCw7u5+)xBI1ښ+[t5 :RskJ9hR6! i#:Z6r^X Rl7KAGUw }4aGhom̟5ja)5aH;41X"3iys 29HwڱSJ1M)mDL#.ix PVz757(ʦ0dHMhT؅Drb~s4x!ixiNա:fDs~'`Ú_%?t;t7ePHoD P @\!%o^huP,]I  j.uCei'ܐ%[Ϟ3%`bwHb"Xnي%ٯ*Xg鎫g׈B,4ʩve5]҉g.aJвrKiiXĈ^RHl-lN3)k8јi[&Mfkx#m[ţDM;3h%e2Wky6j+UeU;j 8%IFF0;_u[7wkyFQȊ>'knpH>RnngfBIBytdSD'Mr/ %@׷NUB gjaU-N"s)3d=v*c^ag'’3w9'i4U測sp⿻}=P<!{dL m1зJk EaӂwptF5N,p -*}{[? M.UcMS]?s| ?#Bwp}5/?'xs=v\6 p9:p^֎`ӎ^z#@;etYj9\Lx^~;>zmRdK+Sce7q/3ke3}rZh'?xG'IֱO:W7w0jS`+S&5dAPzse([tAhiy769Z }0x<I y( |A.SD4uh 4*E:e-*X|8c4m4x}=JVJ8ͻ(Ս^t In9"͓1U@E鹒gczT#zf,'RJn6\"n:BQr>|d\kwBir{wBK/*8O5h% XRdR=z{cyBDy}|g!C1r1BJhFGM?mjXZ3nBjnePj 3s?S۟a7}jܿ7UO7o9BchSS Bֿ.Go>}'j*ֿpMwzg;}My*mXYKU`mJ:$ֱbe|.wjG-y"g v mL=#z%vnn/7 vۚ:dآKeL̇΅񂋷*1$j+hU+R;wq8 EU͵:tL`Df=6a.zv.mw;4Qբ6o>"c8^?gZ@,p;4"vefYn3[h)gHe^~ϑd1y6 Dxϼ߃VPZ8T^3ej.oLJ'#%`ڲ={I'E]G,[e`"ǣn? #xk+8'u Eww|voo߶ x_)TMP6uc.';vO?fg[ 2HC2RKb\'dbB17DK%9ωa+SQ=sM,ץD`D cv)Y؎=*Jp `nђ1G-yȧ IWW;ﻀD{(\- =x c>aBtx+g A uMd 2hz2~E4;Ō%Fۇ>r !ʘmLANwCB6h E=p&+M;e]&KcYWTӵ +c#uZ ۲ pzB`\7VX6Zbҿs(ڈΓ{dnاCFȫŴV,ymӑX:leËB:.45@h9QXY(U @jXn4]luDowkln)ޝOr<>v]xoC ާ!!L<<ds=pcB\O~S´Ynx֭\Xݕ$oh+4罅<[o@ZAnd׵ a[;a̯`_~5xŵ5%#Dffs0fs.tN2M;&P+:0 @ /oVIfd6_Wx 0 )JJ_lMM.95F qVk9p6'1:H\I\pd4;Vgl*:unR[wm?'y$Ҕ9`GӰ]8{F.B+)@w7w&%W5Ŋݑ5gRO.i6t7(>"ad,.>l|}O8M}VZ054Z8LvۚrxvpwCiB pS`!s+lvn gDj~F^څB`ٶ)F[8Ja5Hy٬q[%:/8q7w|No_`ĿwAߴ?9z18念{?U['o'?1aoXoy; 9pg[1HKs)Z:Y>>JV|jī;xd8iWd`ܔY1؋'<##Ş)yܭ[@J(LIHЍ%}@"0fS&e̎VUҔ)̸u1VYޙbvjnF&%s n P4wQp*+1h04('*lb`RVF!>21o6q?BՁ 8tx=ZiiZ+0f5ڤ݆%f6Sfm$8\eq U_W;b Щ)қ9A֠al#8Gi-3_JX^vΛMӵ+^@ō7T,Ī9޽zv\(yvƼp#MIV(!qj6 Ds B7J5%"hDb7t k07S2jo§?)F?`~#=5?Ǐ5oS:|P=i&m Z ZZ8L0+Ɵ~lSh Ӡ;$2!i4͂b=F0O3*@w e`7|V琌4sK6)*y-&SFeBe ō7=ong+XGknFfT:VDo犢u>VT5hURl;1S<ج ) ʚ7mu(y%\Tphe iU#B |3EI4jCDndFFZ2R)ŢE̹#RA]7&ҒK͔elF]q]g[6n_ߡ(9.'%xO.,f5btD9g\Otƭ#6oN޲*Tn,Ll[+ƹ22<+ſ/y?\A<|ScV0x7 oFfFp&DžF#i4FV QN<%gR0BݲmV6DLl$j9眄AHhوu=}SR+sO8cfFh^8t\4 juz[y3HDRr{,Xfn׎`葶'?)Ǜ'n>S* _WgmH?cAV?C_{I_={lkf0g #[x.M@fakk29"0`2M#GwT;^a&:Sx 1PzcAIy )ٔƙcrFJftzQ{wyg7i yx:17OVqh4",v;zRbQ4پyVӉR;!$k;F P#$"FDa}i,&!zp  |v"@Oja o h7s2ϑ";VvʈZvZec[NZ]]!e>Œζ7kzAĚڋO ti1=<`1 ґ (AohA:e{38X+y;ћEJV baFyLMg@k2Bs!xz,|>Κ>cyqԚ5'o !2y6#㣧9ג&m_jafj)|O~^g|{߷{s=/=*|Ea3p5cOp}ŏ>S[ ;NT[t/y^ t604xQfA,cOӝ8Ǎ+^WlX LgN~Q }l^g#=#̓妋UD ~[g]fx"D xN aUkW|9RZ1_) ΚWRq]Je;R0KcGk>̖zQ0H4rb+f삠@ q1$ mۨcb0xm0e!u!P9M9`!/#fmR کby1Zz΂oԦm[;R)n+p:9,nfp/{o#iw}%3o^1c$,[#bb4cy`E<^ҷ*3#}xxpG`t )P*U.// 3M{{Uy测'Q HgY*lVԹ U2HNumv#,̲qnA,Urd7 ]?>Կh4#3&TrFzC.25~y tT:4v IDAT\@ZS'3]sPP##/0?Lw5ޟ;ns;9,ߩv^ 7w᷉ c9D{x 2G'hs~0}aTNJ襖^3GnD2؄d}i<<&MkjճKݻv#(lZВ 9k#*DٹVKJXOf05iJpyc^vl6[EFQ*lWĐAK;9^t)ʩA8{}u#$sB ,H\k]c691(Qhb`y@&]ء"js ,t7h4YkFdhpͲ.5CtԹR )|β1N5x Qǹ6Ts+9RJ=dY6g!o7%0DXZUdn}` Ԋ+t8̆xcKaHGE{-Z>_"y(ݥ9g%LZiLIC uWUSw$kVR!LV .޼8RJ>z?+p]MWskxU aAxpLߖǍ`vcq<8Ƙ[k ±'{?VKáw0H1( ^q{ǁ4[ z&IwJ$iVzx7Ȅ3~8^j¹vf +辫MT"4wqhR*9=My4 kSjRJcsf>Vgڌn#:HIRYzœO/ m>#pA}!o>mb al[]p]萁h 2BAѐEcl#<>ۍqLBDIZl6+ݕQn5M*[W8:.FΧ#G-g]? Jx8ލqV!DCDQTk?윥trE5ň(e4>H '^SZe>X;B̧-"H.*M͚D)(gG Ym{c8\!pwi]$Ϙ;܂219ڮ2@y1Sf,ӋBu8N]1wZ|/|B[˦oѾ)xܾ&*v|(U%%<x?~^~G*Kn&FS=B$xcbNXi{a1$D!zz?NHa 4iw63z=3+0y!p#㣋Z';OCQ%}<,.UʠkX9u>Җ>T9)D4w!|g̋|e;_m}=oN6'x! oǧi_5?bYDr&'S\m}clR!ZNs1A!`,!(LbN9ɛV:]*U;Uf=O9IۭWrľcF|6M[O+41pHӴb`ڜS lK):0ׅuZdPq18堤XO+Fw]T2 ֙,8פ3yaJJrQi4e֫Ӟà|gq2Qɡ5232఩wscP" ~f=3)Q"₢ ]BFث%Wf@[9C%g)zU9FKJuw,zYNcdro]9!j@BmZ;>HWHoƍPyr]Ĥ{E _NJ쭛)Q fERV a臅Yf֥`q Ur n УEJf; tbRm}|E[ 54MD6Vyڰp1ĴZ1Jvi.N'jcXV: 2)8eO5{楰[O2 u]/ӔD^d>ix^SVE@'}_# g0'IooSޛ&IPg1p}zUb34'IW4تeM 6|@3N6)ӖWL0ƠIW*lMӘ~1y+i5}<ݞV |w3WFK?#"m6mr٥*;5)*ϔ;Nn,uPS#1&R5Yn䈭TE@5pR061G(G[|jbbk8jzvO^{G2Cpw#oW jC^|ЀxA~țoRäu.{"h:)M3j Oʖֲd,j⌅S .u=VJ/6F?,\]ZS{ 'HDzFf...n{ [Sb7&;uUcL:Z-7|3`*B@Q>Ͱv;1'a_ n~*ä༩hPkaYL :`VVa0k7_#R9u"O9[=_.]s:MЩEC)w}w~wxx !I.2㾵{g?~ ᆱ1稲* SfgR+Stp``#\"gA4 VFZPJkkER 7H5{s6ҨݤubCZ3JC)ڄͳ_>#N2Oct] xڴ#s6Ym(]CDєU$LцcizQ6jsJTQ. y2Pz% 1qbmH1}|}ׯ_f?E?{b-GǷM |]xE{oɻPM_j5s N4mO.!j!J)[N0ru-r>x \nQ!U -IpWti e!Qa,_pq~Nz>'.k͖ѣ.|PhAp .]rL%EST;KzOm)Bef,aw1C1]k)-6qy䰀JqrPc1Bq|!ʢӔ)(G՝ȍ2Wb пߡ)G_o/Ez>g?{%;}d{W3?޻?ʻ?iʔL&]Mt|WoyIKoM)׋^tO:R4π.H,4P*1W`Z'C'$iR߾۱=@5z9Q]J:F$9:4q/DRNׅZwO5xz^ W=niXDl{m,iMLH1Wskjw,3.* &5c2.iIJJIN!2!;Պ17u CGWHg)1?Done7z轃~v!{ck5O?Z 4zptoݼuGSz䩇JNQYiLcu'8q轪QG(6Vs-&Sĵ<>97G5`?l/.QѢC* pG|;ziѣ-el/YQȬ辫pp ¤@Τ3s,@4e4NMтaՍYO,1$ԡkUwIh]QB0D1MY ExVIьP#S@6M,PU*("Jʴ\haGCdQ\Т8HU AC Q m9v()U}bԦaKA֚ ]в>gKv٫wErpQ:R5RP^"=$_MZEb`ˢ)&rPƑL:!ΩzZ}TCO#5  S6'Ec71iR|f ;b(+4DpR1aTqUZM qG#3ϩ%pPˢ;cAiHh]p3ja`6"}TRhEXjAZOy):6 B=JYC`Y9)Pھ0-z}%ghiQtX?,^Y9/lv;%*sM )Y youA|{Lx$ k!*)eEL#z :tgvn-TH4e3>|}I_pO^ x:^|>]̂kvc}G?we ʦQw!P"x3ݕ 5QsA< 4!])FRYϦi2Y!Ѽ eYxZ:>>}8s6 Ϟ^`u~z;z;ꭦA6:sA'ZitaiXV$: gg<)c bf-Mx+*aG uD S J-&Z5.EC%yj0N1bQ!/RYRtd(c\nE,ިЫacŠYV5guIYԄu!DCpQR\(huN)1J+ )b6wgxZloooW(=8_"Cy.G_p)E 3@/ 9Kxv+Dd~]ߏ]  NԪ(C4<PDAVN?>\_1ƿZ'|]; } w_8^5o w~D˘#/~s=7~~Qn7R{#57u\WR|*/~뺃ET\ÜtaR֦㐢8SiB~}ꔕ2i)yHi$="hhcF5aPP;ݥeΎ)qK֚Nm:cjGwԭSF+ a8<lN1Hw &yWs,xX^+Ap Nauo(M18J2qџUi[v8M.c"2S%-ւǩh5m) :Oǐ#iHN5hM1)PƿIHE֚@~ȧ|reԊwZk8^5w_P5߄B !=Niw^{=֛ 55s ֺB'Gb:px ,y|H\XZ*)&QZg+#t)O2Z;1B:gEF!.Yo֌XчMAYRŰ O^{ aVr4\ )&mnrX=KiYh!j3 n v#bSEUKkU&ju.u[5?SJ՟AoB i3Sf,Li D: ]hsC)HLu9лpq0Vn;i߰rUp8QûÞO^ :~3>!uW ]~|FQK)ucĻ?!O^˼(Lu 2l$fE+DjqvG@!O+՜핬t|{IT ?E*rCYasrm6x\L\yv@o$9z IDATe[iIa[f#5}\)ivM!x|5>8r6FQmN'KS ~٬'R4Na^chKnZ/]f^QJHoMԔWapM kgJ´t/ѭgP"h- t?T>?hREm]!k^]WW{2v:Mb}Han5.``j CJy^N_E,zkv;5s1`}MBz{='.jsĉr%B@&k P:c8(HJ5+cjO>?9R.yː7{}swj^p|Ui'$"81?m7Bjӡ+K{ -h@͆0seۍ?'ޚs"zZ!:u*Ja"efp1|uͫ (xdoΛovlL5 ϪQZ*?{܀ʬ7kZm&uK$3J}b6\86 hRO)޲n.EԜ5 .|Si6WmofiՕOn_>dʉںAt>$z pnK Ky7қ@l59yuP;1x P @WDG!w\i-ޫ: Ӕ5UU9xMFה6a腎zxQ,u.:D@ D,5M32bz,NDI2U!H7G])O~/>.w[k[ǭ_lW2/Ao?} 8ϭ'/gunWw& |\_]{ڬTTc)ȕT/٤nO[Ik"ѝMvښNgΝ'.4ѭN\OM* I\G=ݳ&@)W{6gkZS-+q;bkR3ct,mf:e@@P}-8%j.R` GU~3j91ִ:VkSGڔ05Pgi`ШFDA䜑yG/ULލL){ Ϟmn^H`ڳs+҈D^T9z?,{6 pC޲aِ UblGt\qgM#䢭rRy%ϳϞ61ީU x}Ik)Yկ&4YFt,I*SDGÈN. u}v8G+3q5YdTHEv`ζ"6'12x ן?fnB}/bˊ׶U x-)^D |~k52$"u8~ =i?}uj)Fs{ө)"DpҊ*4Ǎ~讽i`PUOri_^{kf;֔qN@k zL]_wڰ +u  ߔ' IхZ4Wiٳ[ |c1D<:RIGg_aj+(TM[8?ו΀[+}1ի_1u9:Ћ: ʨhi3e@m.8Y|?/xaJwkjxQw_8^=^58RM]qlnEo]__/{o.o8g(/EemgǠ2iq/xek%>H&pDST?h´rћc[S|6,DoV%:3T`=X_.pq8y<%ig/fs^l0DH)(J㚰9;jiRJI.zbB#'CrAONAiѽY9%օ>zֳSLȭD:/'.W;s+NLyJYk=Թ#`f{GS}hnR5+ꠓI\q=)AU-'qd(LqGÝ4ޚ<UtzEjY;& QZӦP⁏>O?eou?c~e 5MW-w}m/٫w<^5<&Eo7zZ+/{s_c{.Œ1iH*!g5$iI4^P2tӄ;|'/8]S$3B\ 8?)=3jQ/L!@ņ6+J{'JZy|Gxv2$`1MywkQ i؄+]%)Sj TFHF+(7ܽ:٢/g`LUN^׆KcH"חM^9 :GpAA&uΊ 9NV։YÂٯm ^+tzVӚrGJMWIF|T aFfJ^^+> KI׻+z >hA@.k~e#' e-pH3tWֽsk AvJ;USĠ{Ur8ĵWz1 ?㴦s/쥂O2?#>U ߫|9~/*/c^5/q|Met"K_˴2l hr2ĥ s<RQ弢Fm >ea޲C9'*?I#.쁴V5 5=S^ӻ:M&*gMw/sC}S?M!ބv1 rr ՉєC$`y=v-OMofVs2䔀( h+* p۩+ԯwA51ɀZ79eCNkNR1lC@:hsǠstDWQ]yCW!Z;Z'&=\[:dK%F x*cO_V߿_b~(os//y|Co xPC3l>~y[o!Ѻ c4MԝF߭ lEJK)3Lb9݁:6Ǽ76G|Bk[+F ѓr2?x atViyWyD e'΅H\_pyuO?ljcZPʢqCt=^%`Gu7!LQNRLn#Ph&!FR{T5HӖB2Ft?1[Iv | 3mHV(fK1r0ь +.zcNy;'^c mN<8Z%+R \_r⎑${s}eu8 Qƣuf-SC.2pX9PaAɖS`#Rf' fr*j'}>fR{O)+'/__s/ǫ+6U 9~ۗ=O^7yv#ȣ9%si ZGIgEl ]Ɣ4wՊz{g}R/8Aq|p5,M6u)bKy>S2׽fc||g,X_{u 81e + S^S"ylBwaL j@ C\S$xv'?02F>uԵE$t J@UmDZG~J-,eQHT]w'K`*'% rjc2$kb=7rBb*>z U%G#sjsZ[#8[zc ~"z8 (m&{C 0T!vXIOO+~U/_x|KM뻚݅~볳t8~e{)'fzaT R>:趏Etgzlx7'=fRFP6Wle#}@Q`^1ypl{@P8ܧ>`< SV ζge*]ɚ뜮<ѨyAf0GY=GD}z,x5̳3{;.G!gM('#M^)'K1/E" S3vSNA9; 1OҨjңWx>@Lr&2N&}NaGs tU @8O!QFW&׷p0QӇ6-8I V[$DF@xb* 7hf얇Ő!8 :r?Ux&u״?8&?哟sEx?߷b;_8^W 8ְkn盀_ne y.<vnw6g cru{F+$éCtUmiCPF|YmVV+ss`kZS5 BJ GXEWwò $W;JՊz6meo~”y9bRy#VD4]_٬T&תfj)*s;HN_RVAj,Ǚ9OΙiAFɺ) R%* &~$*]~} 39eäiiiE x4Hg)'_gjs<,xD3!EyD15^;}OҜ'yz!xY֡?UX1ܩ,MQ t=xq PE.rC00!pY#SU<Kp10h#vLU]NM!"$͜3חW}S} ܾ_8^q|CxXMe5!v˥o=xw;oߦ#9Pvrc)73̧xՆNT8 A竟DSw̥Y%'22WBpl] {ibW9D؜1匌2,´ִ10&1cL-bR 9Nݝ&N=9et.N'phvŽvJ/z3MST@Z)y% QW!xJCum){zSzJQ{) bN*ia94*ZĢDȫ^.7&XJNw4fg[~,p8Ԋԭ!dk CGNF=(NJ u&JQE!m8Iө_km?j<vȞ~/  7xU^zj-6wq|2$;Q'OWWW~k_}n[ooPJ= DЩc<;RHAt֚Pv} 8@p-5O 3ϖ=Ɖꮃi-li- t1HlTw6g,a?S`XX)@f+jRЂyi :UѢY.b|#o Ɂ! ~gizK˲E`g|{\?GP8 L"JÙ>DkBڮ,FႦܕVYOڳŜx u? 1 RSg>o!!D1HoՒtU*"d'FE7nᗿ+ZS^BL"ݎp0F0 jq+,ɋfG|tZM~\4xiSD21P1Q4iDTtU0+@jSOox i1}> -r08̚+qh];lȡT=o&W_og\"+~Oy1 " "!oFxs4:pvv篯"goX{M|m4Q{ޗ9]}o@f4>MGktǎ79Llˊl˒F=]];ImPXYnS<{A;7Jr[\p6FiR*64ȲUT,N`Ael57nQJBM@LSlm$.d*-5=Ғ8kQz6`ȼ(ɤ)`0 P,rX-jZe),gOį_ _i`Lf3XW^,H 06P%CO@HhHRJEQ d8:< ߜbq}l$4v 8?Ga gm4On(]rz=fe$R(fTk"%!d779 PD!SKc@:C5VY %‡"IT PIc%<@2d][b D4[c! Biyӟ?[_^__5z-zlQHPGڈ@7eYis\cL4ŗ>'_DDRVS>XT'P:!GZi:!/psl1TrV'}Bz8/> )CR2JӘ4-M`Ch^*k2YKN!@Q>Anijb CJKd;;>@% E/,gV!͞>) IDAT>>ʘ Z:"w|,I&(uo#s,fX.rne 4Em>J s:ꗊz$iIBX.-}?I! 8alP|ˋ(NɃPlYJq5qAB'1dSyN K 9˖IA*1jS^*j=/O (EJaGM<'?_?i~ZϊӢ(>Gsp@u$`GlYYz?^,1|ׅ0G>EOZux҂ B .(C>XHrpU7 Ps U)RpIxn $'u,C&p\гE^Gr 0:A(;V \R7sA9x9-Yk%3fT!vS(2`// f;\PkTkXi({+EHAeԇ!0' j ˍBYKbY@^*= 8W\%>pk,!{a)PC>@) g,()jXCtx e:kOt d /OqX\__͛7格M }u-.2P$`:~e;_v5qx|L)bܻs9X^a21=2Fcks$[HWY "P6$ PRq4FA]H I((wwg0ơ-Mb@j:-Cw2NK$P, YbY#M\o K;wB+O  g ꠨5EAc|x, LYGu={) Q"3AÍJNq}Pr&XPZ;0;aC55  ,חH$Ts0P +9c]<@Y-{bDםc<L꜇D "dU x@k}}||痗\._`so"C}uZO\F@rAHAb-[k{''88:mnXQv R"k؂M SeAȊSڔϭEo -BHSҖ2Z'<!P,#GH5@rB"5arǷSj%R)*Hh^!KjP(39^|9$~cZ:˿G?!M>_Ge<< Xf ԑ:8::ΫWWͺ={/=i*i\ | 9Kps%İAH{T/;p/$5Snq(sD]C!P>y  '!tJ!P ڞ$0. (P0 ȴ$ 7\H0\&M@yNRȲ ;3h)痗"PJ@}PlSvOM4, Ґ1ȦS(/m@QT{("Fx,K@X@ˤ CfiKK0Τ'i?'/By'R$_˗CM燇X,<+5}&|#?A ˆ$ n*WMրA].@~=˲?cH{/9z G=7K cU*#.Ԭ`Ci4@Œ)}G5hh'N c9]{ o-5B o}3 Ƣ)>*I%xxdehϲtu3b:2!?4%`R+x/i:!뿠SX[̦҄ߜ[zn<^r3g>P܁u,B"_;x'(J+HĀ %$ן׬  (JbLXUrL)ZhۜnH /rQsp@ i0ָ֖{k-^x29^"s,u"4@SENV$QP}VT'~q, 5HЉTk9=Rlggg@\]!&>~&<< &@_bP}!_뀾Ó} Gt>V5_P\xGJ*&d,{(E6beF֕-wH4D.;w"J%uT{_%U YCs=" p {;(:~6-I5>* yk! @7xyz7oz+?988|Ƙus57.P}h_@jbHQH>Cb,}IN)I?Bq5N?G;Yb@ 0knBUZi.LT:U%L^&/) vdO&>B±+4E^,x$s9R`jCUiDN{dPel 眃tԙJ/ZHt UXGpc L$ȋ|j-ޒKAdWX9%+ <(r^w`en5] `YK||xp)8/wAu@)UpO $}Y^›__ ,)M}Bfo`'%#xd ]_߾/ J6~7ЀD*,XGdpBqX$&UZR%6U;Gz$l a-84Zx)!2JbIů-kLR_NU@,!5UzR_.uMLCh䦠T#\K:@Ҕ{M:b,55a*GVU1[D)Eu8{ NOa?mhdlW/_xh~uLtC+02 [&uո.@),gNPϟ?·_z[2w8z'';<) *M;WZ$2=-XeJJPgBgd@HOq`TVrTf?B$\o>JZS5Dc ?\LQf3%Q jf`Uq?Gaa e J@XJZƃ!{Geڣq.8(AT ,9AH O=!5n 9ZSu?+Y=Gx-B>@19DЏW_VL 9 g^﯇hKKwszz=6wymORFd X"qJ,uySAH)X,y娗}89Pj; \dE`-"{3fŚg4(s%A!oc:.I$i>f{H~^Oy LCn6U{NS1{.X).dޏ`0 @U !BN4 eN%c Щ,LL|#Nhp"0q`ł\Jk E{NSZSp%yDAAzG)`te h8cpyI#~H5C(cOKrƿK\٫W& &goOOO Hǿ}46XF\F$ noP *h~<{XO\L#9V: .|?} "J\P(MqD3FT=G!x"PQ޾)kUࡄ*?_ח ;PԃX 6}l%(M e'B,s_T8  ,[w~JK>w0!a+1ߟɇTkmy`XыTLsP*ec4) \yׯK)1N?Ng?Xܿ]#?I  }H@q=&'''w///{S].xxbcD['X$(KFW8Zkr)h &*J3DOU0 b:Xqe)dڧ*yR4$5@Py4U3dMXui|' 7zGRrEFࠕ}s*TeE$B 8p$ϟWgekd(Ul޷X)pgg8{C|UI{{{wo޼[7y)4]NP3/+mM]maYHNNN~7ggg ! *X$Mqx|݃0t0RvU",w;d+KHAqU‡Dh=JD]#vuXc3~»!%=!J(DXZ{\_| gg!7o]*]_uۨlH 4j@108"PٳgbN&?:>vL>X= J_NHWRQs,aUbE;X' :Q%7c - R>6~.,(߃-QȐ*>3aVkv`atv\K-AWL :X]c!fRJAO`U I&Ƽ~K\y3 ]Nt)EB~vm!&&F4&6;2H2"ULR+ 枚>|ggy_qd_oz4wXuۓ#5 Gr Y:k@֣|燇ߚ !V$B`{GG?8zcB(+IP)"ND* o-705{1 9\]7WW_ o$MSH)bgg矖?x3 {bo:.Z?0SCE~{@lu u&PK)}'!o󽇰 DIwv;!Ͱple=ρzrcʠT sžPZVt5<w%ϛ\dBri߷\G9{_1Hrs8_wk|"a??ԛɻL}t́iAIDAT7&Gy2wP%P6B5`}ٳg_;<)Blq[iB)JJB9ԁ>"_U(wߣr;bQ"bb>G^X\ߠX.\.)Ek4M$Irr~~ׯ_sS.30vHQ hzF>`]h"}?(_H7A( dLi,ː) Jk2;@@/I- wџEpVCJ}tC(KϹ}a]GFwd$sϲϿ׊bճ۶|袵"MO;?qQ$>_77-Lmcl&G0yd$<5- "mtqONN bQƘ1t>knң.y nnL&d`i-j%uc[ gt:=RƘ~2B@t-vr$t:- Rʥ2I {6ό1gmh} Cmתk Z|nm>adaf:v ן-*Mۖ&io>cu~ub PߖyS_n^0{+#@ր꘨ٯ>D.M|kħzf:6vw﵌r okC$/굩o5uǪ{ /hFd>࿍״9uM۾ZÐ7[-"PݯFVjM@gM1ձM @6Yk`Dn*]D:u㨙o`JF0J)[t m! סqPs!m:]4V7tS*M6޶gf]W@zm!ĥ:눚F0d$ܑ- !6P\]e}n 3.m7:TuP+ذny67]|u cw HFiLf|w_{ssM4\_i:ܺk>uu#2Q: M,f 4<]|\Ui.P7^: 7]uƛ(,# @7Mv},]kێ<>CmU26im~} M }PyǪ㨙oGY2HTcwiM`ӱjo#Mcu]]h}@ h}A5#2Q6'HL!C^u]im mEXGu:Gy4 ("!&:pnn!s>شmM!v5Mq{&=tna9֭GںZ>2Q"LbW{zVc34״:״nnTX_5Wp`DؾUn}y umk]sMHۍ @l~_rmy75GF0ʃʖ@2>zM|uݴiV}@P`r5hXg~xe 2QM=P m`|}kι 2yviinkf=ʖe$6Djz>ZW7_il|t,aB yV_<ޜGy` (OBoBX9<4hئV`ɺmZ5^x!2Ql@!Mcuۛv6黮 b(4iZ5(#6d$6dm;:; #彑"mj𛾦kmiQqܶu^}_4LGyd$r2ܟ]3u]s}Zj]z>/o2Q>yKnn(M5&v/l?(`$|rOBm6Agsؔ t?2QF붑X7AxCgQF)#e!(@k.mm pXF0(=evߦfk7G嶌`Q![&@yxc`?(2QFyy@bPv^}n*#Џ22QFy ᝕Gd$}% #2ۗ vsIENDB`././@LongLink0000644000000000000000000000014600000000000011604 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/updater.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/updater.pn0000664000175000017500000000102012641367670033077 0ustar jaakkojaakkoPNG  IHDR@@iqbKGD pHYs  7ttIME IDATxۿJPkAPA]NN#tsp|_A|]\PQ{젶&笥Mί74d5ɑ;wE,lg({F,/y,?d,#g `:ϭA&Ke75f )}d>B)ː& st3d@ձs̀̚>BnPJ&u?dm3dϾ-86wοU~ )_r"1 @ @ @zb5@W/s68GVs[f6 @?v} G|IENDB`doomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/vr.png0000664000175000017500000001117012641367670032240 0ustar jaakkojaakkoPNG  IHDR@@iq AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  niTXtXML:com.adobe.xmp 1 5 72 1 72 2 64 1 64 2014-01-02T09:01:23 Pixelmator 3.0 VKcIDATx=hAL4 *6ZA% R+ +;l ZH"XZB`( h0&>y{?$==&ƀ1` ƀ1` ƀ1` ƀ1` @#.l,/gh|WK6-e45پT[hK՞^KLmsq?M@gfWUtY 7A^^q9$o&F3Pj;~>|t`ș=oNW5`pJ SQ 3\x_iP]K֛D8JcF݅.(P3:MLR'Nhw`$Q) `sR! n&bs.qXT+d7yIENDB`././@LongLink0000644000000000000000000000015600000000000011605 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/game-libheretic.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/game-libhe0000664000175000017500000001600112641367670033016 0ustar jaakkojaakkoPNG  IHDR22?gAMA|Q AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  iTXtXML:com.adobe.xmp 1 5 72 1 72 50 1 50 2013-07-16T20:07:33 Pixelmator 2.2 M6 nIDAThYklf߻^{mNI ICUh%*T -jԊJRUҖ PiC Q6"x;~ǮN,wdz=yΝ )sv2ĮrpSbvj;5lҘʭ2몍β)e\/]y }9s!nHqM26, Ds$\@V$!W\8+a ٥r9k%Hg 7J,4< !u~U*U+WD*eE ⷯ8GM!=2k~Cu#]YY^r`@jLak\0B+ s07ͺJyr\t1w謑Dj Af瑜)~tͣ!1B,go~Û)( #F87̫([9M-צP ]">gwd%tӹ4N8G8qӅn}wGZ:=4|-QRKd2U\Eܐi.Is / $I|4!' %wb27Fpq8!ߞB* ~| V4ֵcm lyKE 9cq~O m_i\s-Z-pŔ ml!GC0bhnҶ6hgKZ@ 뷾绵 v<8V5alb t| CCǴ3C(&2 u& 2%xJR5?uwkO?^?N=P@KK 4 ԩS0tB6Q?Dd;.6>*R ?3y&?u?рXܺssU߷o DVEpM_@^Lpq~56k՘|@8RO?9G;lZZpȿίBh=JI\Ɖ_a#[V"_»P},\~Ͽ=sس{RY882N60~9geiй~^]uզSkrQT@m|* ڂ/<=JOS~cF$O'94)ͦL 3T˺v;qf,lXl^Fcؓhmo5K] QդYF ҸJBPt=H$t]wh=B[c8gȡnfU\ RahhzFG m7܊(>]NL^@l$fHz~C/7s*>,s]&@j5\\[ˁix#ɝn@4'*lڸ8}p\X{_$ VBSWIYz!" yzB}.8YlJ!FoR=gG- l3\E^/n}әfrMsལnej[[N34jբRK݊T3_̚p::;m;>Ph7z6Gưދ%fϮJ v@V]Sr7omCWF~:"t4FdxSu CkyB(R:4ͣ g劋x)@+`r'30cG?#q];n7>oOC/$rG/|6'" 0XXЁ}w܁5k: Ӂs"v:7!ேQ4`"3>$VVa9 jG>= x'$r/BlГ˃|)RΉ7`jf/<)>_PZ4eDKQEf?^.s2s(K<Թ07FA|uJ,99X'*#+P HAÁ{\?6oڈ'bHtLa4UM?Ѡϔ8hFvy5$ZGd[+"sU\ny՝ٽ ;wl;nC%mBt3saJAzv3>1/ <Âb'+U΅&-4|l -5/biquuxviwM!o| jUr@$!;\-OlGCWɓ0]AޑG OWN7lp#;zӸz۵h4!RW*+a<;-D$+@zīR۷9Y~hӷ޼{v=O2J{2;U(9?9ԋ՚Vtxqs4yћ9?0]ؓyPioO@^~GhZO/ő~dV`"pr3IJR4kWe"s m&LU1}؏"nF@  WGVY ʅ)i^^-n;aM'S:6f pHYs  niTXtXML:com.adobe.xmp ? A IDATx fSǓ$dfuFn\rdtQD{tPr)F%rH\¸۸ s~_3y}߽~߽=okg{z=̜953`diV۵6f@ $l|W  Hp0 3`]u3`6`f afm $̀@ẁۀH;瀞;30v U7vnf a6n@ $l|W  Hp0 3`]u3`6`f afm $̀@ẁۀH;瀞;30v U7vnf a6n@ JMhU mXV˵.뇄_ ?}K 3s̔_׺/:~aQktw?'\'>Kπ@a)YGX/ t_RW l%_ӽW + 8G˄kKEaƨX tv:BLTCJs Hy^7 [ƭ#" T*CM-MA63_أ`:FW C\* XȀW k (N)!N~Tapp`0v&-t}y۽a\T'u=m6sŽgO mZO6vc)=Yx}#SW_xXQ`!INc);z ݧ/%ܷg1.FO̺c3k8v^7=Ο`j zmDY6_<Y=g`mDn-8N@W#jTJ );~5JK X09? D!KUӔv5Sʎ=oK8I[oI;KO`lΉ+2\XS"TQ%9 l/ ͹UKB# -ܣNN(,<] Xeɀ@>x':#'xwu5akanOWk9%XI)^\b3 )aeydS΁C 'OӋBKVŗBL! -A*\n Aԓy^-!l((!w "s-zߤ𭄻."T8GiPu᩼Qa) !n&-}0`Й4:y7ē~ ;'xo^(8:v"B,NR;->NN;띙 kht\$&L㕤= Iˣ"\L,x5aџv1rITD**Ȁ5yZ؈%'vse>'9W3Ҡ3vrWcU/ kKz%vH~+ b*121[p_U ?-_)莑+/IIW/3Khv-R')7"k+ oXSC`7!II1_<N@^M❪ *t } k<XINRvlFcU~K1xb&!,],+չdW}snT@O~:;OLˈ]C}BQaK!d$EBwQ%Y{ ?e[ge,q3bcꐄ>.>iKRLpe<$Wʨw2(P(.2: /Jg2E]P\5#/83UzwX ¸e`aR +2@TX'K6´t[e>84s0_BP /F>`TOBp C#I OV\}Mv䣅J F1AsZ3v-IB?ۏ]_'36D}V?ڿ)wSW8k,],0%/I^Txj|(s1D0sV`Rؙ %},6/4BbSBUA֏\I-Yn1e)c]iTLD~9{M`G`Q^nMt#ŪY~1m᭱SOC"9{\($XvĞdI)ⅲXe+ObB,yn֒ `G]c o&NlXz@TF!+{bWKq CO<\Ѵ `W식cX):d=}r"l3αvQ,eă@2X:_i^ sC^v1dM)Y-AhNm͟Rae=S0g:?$F Đb(&o0Yr"}kë_R +-X Eɟi#T}sTU 9J  CV6l:brufX0vяܩD'u [Ix^ExD` aKpڣ"9,IWY\@oP\ MemIg^ۅS%*;Ki.hyMZv~5u|A=B趷^/6NxW$%~PSQ2V %tϝ&,}] õ:ؼa /(=z Uz=p`aVa ,&,Y Nh/Y"J{d=VM|PdhxbPH n UI1@{dZ0>+RUë6q+E :"=k)Uw2O i2@dV?/ZxEl=VO,Vk'Uv5#0g57B6VPQ/QYō2*XXڇʐ.͊p3ЃKF;ZG.),, Ty\?U"9 `snV ? SǷ,(Ñ\@tpk@Z6RɁ: p+)!! oVJ踗 !S';< 8о_JTF:x ̓ߝ&, ЎBGO7Y,'K )@i ($@ZsUnSj%1)Kf Fb$*X7}ĭ`3@u !x9ms';`[A!=I) ђV Ȋ2Ɋ$H,p6.';Ս/TMNZ3@}ns: ^VR^C` : Nm%Ng]e=4W++djLrRRu; \ yn0;/ySwV|}yί(o{Z TH?@(,(< ᯭB!:oP;Dxkw} 1]]`SW0( bDʒ?fERxUց$: 3@{ozZ׬/GI6y<#VC%|-4O:+bε [U#:]U@rU\?ҭTq= (@v%W)BVV>QëPӥcz"]h?^t^eV:Ⱦ"ȮtXE i?ppP Īno%]7 Mxf`6w<8RDhk_.hq<YL@ *HMK8RexE$jb $*;H`%T>$*qnBJqHAiX;_=VM#e`1Q@ a5wl Exh'yVkODeb$]Vlh'P`JL_mVYL@-=eq 3Љ19UXS`{w*>ZT'zF/uH >bt~9[l秀uPUZ/2MW c`qUVaU.γTV0ۅ_EbI4TV~!E.OH^Έ &F 1lH\*p. P 1bgv@{O~K9X9=\+6n勔biRT2 [ kn>,Vz6Nޛ>ҽbbsN#dK#Ϫ=m@_'𵠷G).**XkA|d#aR>#])xVUD6PpS^̈i u#zv=-ER6(=umo$ycO6K6WdErx#;)epc1woMoQ w0*b ]'Y?%#?f {7 xezȻK=*jZ11)o,"_K 4s>@'T]r=Ycݣ |m:P>S u5lEd=` .T BbU2dU,oH#.KгJȔ_YBca[Rc+ T 5@S!XÎRTjҗVЬeqZ `P$>&Ye%Uюioq b`2`) Ń֙"H~׍3Gt݅ 'tj1_P_24O7X2GpG#&!F>E>e_q-QSwkB <sJNΛ' ]p%/(#;")<)N2O%4؅JǺ I`d9G(wOf Lv:%|/U^u,8˶F7-+Kc`1iƖȁ`a4SXYOxZ`zv_I?^ ,5J+WEg5Şq9':w73a!'AS\XUF}Kh|+"x,ދtWWˇXkt秢)65Ty|D%]~#,QU3|{^9IMc):"\+–b;RQ~9ja'&J%^ඒ˫ɿxQy%xOx߼"#̫Rt7rl=_x됭SLB >P|KRtO ˖+wY<,0rXhTQ42U ' 0:f?_"÷"] Ͱ>o8߄7bd) #&- 2̽ٝ0=s9{246,$r>TLJ*˩" -u@4 wA๙'x,pmf_2Rc)bO^i|^JZ]|I#3Wlz]6ŘFḩ p GXc\9(2t1/6J h0cLS@>-QD}!$b'3 Yp"k _E :Qsq_ J3=1.0Ӆj5ϩOI,I{$}鎤j$]΃y(Kz$GIC5u$8qڀҊ>TE \4ꁫ*$&"&/]KCU4[GpE4gS>%5$ ̬Is %u֨Tk-1ࠇ=WUVfJ'Fv3*}tV=86QVV[" _-?yIU =W{e \\^1'UA3RkTR"~t.O$ 32Gk$\wkX z$,s#fNZu\y*/釤+9YoWQ~+閤ݒ4^I;2h{/+ Nw;QX7}&#9-R|@ fddd5u N;$>}ΫfV L]~7S-5_KNVH-$ԕ=V*DޯuJY?#EIENDB`././@LongLink0000644000000000000000000000015500000000000011604 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/progress-wheel.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/progress-w0000664000175000017500000002474512641367670033152 0ustar jaakkojaakkoPNG  IHDR\rfgAMAaLAiCCPICC ProfilexmOhAƿшV""sM/IVbL[Q,fdmAD,śVzQAAXB-Mvc ;}͛ ,ha@H;aM'S:6f pHYs  niTXtXML:com.adobe.xmp ? A IDATxU % %E @Z(B4 i"E@ P)"#DiB E@$ Z7ݻef;3<)w^SLp:S;^jW3 V`vr/@>s0 罻0}kva}9>- Ǘi1X  y*io Es+2 =, +E@ix2x<9wG-# h X,']%pkI. 2@Ox;BB RR@:BM2jί 6zһuW@rrt[SWOa{`e&[44N_03>݌p`+lЭ ۂ[2 32p;p3Rlִt h@@ n pкx[ 4&[z #+Z*hQww񅛇(#@ L"+u7 BTsU[BѣyȔЌno& h"W?je_g9n۾Z?^ j\ &/sƃ!_C#h&Kw&'X`e?*SӀ tiΎ0-nW6x Em\Nw"A'.ީ._pwXN/t ԩ@l#6kE>UA/=p}~>d>U 0+4'PH,uܟO͈3$zcQ։@hhs@O,pc*09a~ g:, Z/׏"ǀ{$$r:+.TYa@.E @ʯ B,m#4Ȓ:I],M#wX:+~C2̇f4Fh-BxC>r1ΌɉZIr$"X |Γ5#Cz3 ޭ&q|-(n@nVˇmت`:j5Mگ>\"+,a gG$ֿĹ׹]) 4ڰP%z+|8MJbuwNs/ET`:ɰ`?n0l%:q ueW \NFn#& 8rY.R<@%jQ[<չ]+w a!wk``8Mj*&a+Pd4~X 8!0OCA* H-vBղ_=/Q T²% |CjhO?N`e7@#Vj:OP}! NQ:y6 no\eX`u#-ӏao Eq9+P4#6Ѩ0+P(dh`J$4 A Eq ? Y05_GWp ts\ |_`BLlTׯ) n+M=4l}&!N=z@[^ q<+ ܌Pa^ٰQ8+I Aa6*ᙵW=<"ssku+ W@cg1VZ `~qN_~\W # eİfZ@k㞚0V[c$A]B(ـJh2jOn+КoMkh,lHf&迦F0v6%͋wcF\\ՁZ<ԟU,x4W 2(p,ZS`aʸB8+WTԚ0YEhĪz<+4Q1WT;ouGϣ@w=VV;I{ĺ\T3 tVZm&? ՛|@HpPY`1bV;cHOc+P_envx:9!9 +Q T-ԵgJ*[q +#69~} =xÐ+V:5ݬ Xo4#u\ wy>h + )0ވJ{}K[k2kTfqrޯWkU:y S-tW`jU[+iy#W0RI~ʋ*[seW 4@X4׷[, p\n LlR+"Z [ػ$ =x/HU +`UݪtBpj+`U@WTD+Ё Xխr]tzY TX9\)`UuX@V4ս<@F[^,~ L:ip[5@b|V^*A6<+PH,H]ńYe2a6<+P8}e@?ea| _+`Uuܻ  YuhU&ѣSHqweD8u @:gp Xխ-2rګUݪ*/ +o4 sW(+`e\+;WPZjU&-sWH aTK{*@+$L@B=+PGY\{f`"q ]+ЏZ8Mht+^>W %N} ӿݕ}!A=+ T@-kH*^mRrn+`~>(eLۥ >| UA"]P}Lj!PpXЈJuv`, n+`KPMmA-jA+0UFZt>f%[͝ΣT+I+K7/F\Y@a\b*@7sc[i0p+|r Z;``RӾe&K|x|W jTnOqV;׹f'`QtV1jjWj|y4WXhNtyKK2]+VkQxV-fW`J۠/m 8k9MZ jjByus:MJF~{vh*Չ[[ϣuꐅ|QMThDp")46HzE\觃V[!M)J$ܬW+ NM(+KT2̹v/NQ#>a!q>5+8 4mpoEW`~ #BjOlqy@mZy0 RM/dT2-ֻ h 0uJD Zl f sW G '1-.Y\ 7W)e]zqj^O@a+P$P= 4X[q{xDb9 xȃGI~WtZuZlp6И|Vd dT-0og_m:%tx9v#,- EU@o>XC:5^Wt%q%,8"O QIٕ 3)\'KZcd*n!T(kՔIDJ4$w8vq/yU@d0aO<<'t`h17W hoエE(^S @"W4J{iMժQ\ e.]̢ \H7M\*ЗXV~iq*H\EdԌg"5ZՐϩ\4r jVV@ \i.m: Xg\ |?y'œ{$J`tx,wNei\ ,Uuy7uKҪZZįcm{C5Ц6-͵ryJv>wN ? ך|N PpH @O[ZwJۀ> ԱȚ-G\| L:=AUJ%k<`k~ dAӺNN̊dpXpY/?6̞Su(S)CuJUf`:psڨ} éٴ_u+pAB 9] bt*2( 9-6T 5P%BVH1N]R^VV$HupsRR+4i|3{Y\p]1t*Wp3T&@ʯy@$!lr+ wNWBm9P[6J( JP kP8N[:B,1-ʼn| _kgN  (Ípo >F[ʀ7?_$% /inLN!+R*u(2 ꍩwP`FPR}]@< %`o4 hz2)5|^$SQRPMCr6ڧ DQ`ak-Q&1Xq  h7UH򟀉!q+ %8,$ZB|5)Yp2`!;Vp* YWQ09d"Q߳H31 ,t^qOJ @ym$i8JO,SW` Q:~1Fc V영"dq诓vy-p:NM![*@NbYTq'Vy1ոf6 %[G@_]\02UOSvL/+iJR >[ 3܊iau7ӰIDgH,nYT!M'tqN ǂ}$1TjdҲ$<he_3o@Y4i|D(㤥/fT(}q@Z$ 4+OT-:-'" OV"4a`9{L + .dS[ pZ5C@@V62-+?b@59h2 VdhC(`.˘fzt\8+З5=ѱ`ȢM SL ƀhBu -VT!Oi`҅ >M4nnc]>ѾAԯW~e[zǃQuV3<^2 h N-V?Z:pˎ_>-"T#\vֳOHjp%1*Bk`%0+p  Z(+r]ˋ5sn Lnp3 n pPѵ|v ̀0D =.0)H:4Z3.hqYXcxm֞  j\_ u0 fn_oT!Ty- \~fk-64w`꿿 4V yi;4ծ,ܒ 4ŨEk<w)/<Ywm$/ʿ(9q k݇OD*M8ެG,;,}҇ ZxXYJy?`m㻠iwhb'nyP@y jh|A 40WHIzW+5^~*'uAntm p˱ry] L@~>I٭4IENDB`././@LongLink0000644000000000000000000000015100000000000011600 Lustar rootrootdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/borderglow.pngdoomsday-stable-1.15.7/doomsday/client/net.dengine.client.pack/defaultstyle.pack/graphics/borderglow0000664000175000017500000003135712641367670033205 0ustar jaakkojaakkoPNG  IHDR\rfbKGDC pHYs  tIME +/J IDATx}r뺮$H;ԼΜg.oPU8,ۊh`i߹} M{6mo{m=%[0&{>&M{a<8&vBAsž6l_€Lfۄ `g)6k!4`L%0 Ha+MeIg¾i77m 6}_{7`[E9U@[t&M݀6lx ?:W."oMi ewk ,bhc&ˁ?WE}op[N 6LKyf{qHڍ`B }g}Dgm +otms6mkf6"sDݕQ 6lOSִ=ڤ׌FlE6>__;r1PܟA.- P&FBMCQ3^ (R𒃵r\ _kT(|++WQfHϔw6$l0$-*%zk212V2B FR.L"$`䏨2 rxv3nG|4%(I\6v"mx?;\9TxU 0vE$ůJ2K#I2|Gs_ #Dp$ՓV̜/;[~Uī2K? 3 aEk}WޙЊQb-J t!p!U2,)i `췂tid*lo`TdTEDeՀd*$ѡt63tȾ*H ,>@,2'l$0ϭ$bX[n9m"f<;fHQI,`(W4~ [D~dBKdGUQL"(Q&ʼ> ?6 d#~GH#x?k0 0xf#*_LK Ѵ`}Fo*F܊F}~U4'1X+3G]Ѩ?6dL&ΛM^M"<Z2Fj%~#v3z*FQR^ x=S TYM?fx"m$ǏG<#3q^DlJn3ÑXG@g/н&K9*ڣ|$U庲${kJn&_f3HKOYQWpT*gtg)Yj #$o>r omRRLx_,&h |U!#)έH`\ =,DWV9l}}y"Q1-y|36wi4Ri0>ݸ@lAIQ}o8׀nȮ* {~kMMS,Q0wG32%Q{G+ uVrlw'E| $UpKI~+>o uˆv"xF9j%0L@x GHxg6Z6ŐC! D=jZA=F[jR+>O,g$!* Q7ux%GYG`F*cOg 4b5##q4dTy1=,c։@g߈*j H[n1HsS\*F0faoFh9ץM\;1 #d0VP߷!2.Rq2@{C$u:$cK`-͹ ,HZV?:1ј *[Ce2a| {3MSv#ֺߖk`}Q~5F&x2HY떔B* iF0;3X[oEm< k`i$a Sd@ݴMpu2\Qޒ_J"轍/<|W[>6X 8"ޣmaR ؠDk+ H@&x ^E5 -N 2ДKSLCP[(_CjtA!XL  `DusoGϣ.<=4>.{NkhŨ G.UuG k8ze~@}֪ϪZD ^W&Mࢹ.@3=։BSDoH*`gZZy;FPK_*h$`/n3g{ߪ&zpJ} -粐 )/W'80SySRb÷#Ĭa2D,`ȧCs։H%U Tֹ^GxJT CLUQ C{¡*Kxj 5*[m.@l1HѸyЗ#̙1Td2\C@ٝ3DGJ8?O%H <L?7^VD鬆љT0 ^S1<ʠٛ(C$̼F){`M")Bhs. ݻ&5'1@!JR Ht's"@ T:PH ,ix#LBV%5*㤑:HeҔ"}Jp*u3W:+' cZvzԁy- h[YOi.(wUTDUeȦ 0;e}=apaD7'CVo09XHDVP btȪweȡ D,U0A[сlj#T9/"L8-ˈeʊٍ@MǮ,*IM֔ #s{(aQ 9 #IgпA\@@.@+ߑe5(6NR o 9%*j$ ^+}хyM|l9,?Gtiz 3:\_ #8 PaHcJ'*&8bfXD ;kGR)$Fz(`V2 T!x_J +n%̭Ӻh"p)cB94F4YxUOGM3![}~}aFO>r8ߋJ8?{RS[/Do+ =^"*"#U9A`?Q'BpioEpT>O@ўE?*tދVϞ>|&-kt"7} d=D0"?6{?z|!j;z0$p*)5o{99 1xJ cF k!r F @&^)pD)'tDo4[" X=T+%)wb)Z,f!OHX ɨN '"C1#`9ҭW}n<+W4weވѧ:OFhÂ]}$ILJ$LJ|DRN "ߋhv͛X:9(CV(Y)'b[_Ox]W;S'z%R.{?>>x)/'|] S)BzX!uGF=KE y2L eQPA.=é]_ b_PFAu/pHܧ |v< ׿ge)b_cy,A7 9gqSqh~ܸO_?H |x ࣋R(B*2_#*d^S`kAn23¥FٽtFC(o@_DP`2{ 瞚r"aB p0?m(zU4>## @z=@$/ TH$d=xv@(=?@R|*KTJ3*"cLyL]; f֎=3.f(R}%Ce!"Ey! |qI{tK\uy1x!5Ȕ+ Lqyd׺uM@QqkP}9n)1jxKq 'q0@A!\_ntR#f`v?qalZe }f`PX9$|_Ѿ}΁u'"?} ;q][ -G_$LB `Fdn9_&5w~W#x ߃1]" 8pۧO"=@ &A % X!ý;{a` 3H C3upΎ >:"@dT ǿ !D,_'f\yԢ RCg7 B˂\qYjM.\]?={>6+8 9a? + IjA#!h0W yr{Z]+~PE"Ce_p2&)(4r٭+xH4/PgW^TA.?R`{@p]~^T#l>IH\T !!?@'A9 u\ҞM^_ Vh~ = }1 8_pt?&wPf?EBi0ՈG5HOmf@șU5,s7yb0M<*}cT߃̽)ykeLpoA'k HΌ~ | #/Qj?T(*!)?C(.C>C_?Q>pI#9#r^QRo0P{H'܈V"1& 01?رdRP

Alphabetical Index | Tags by Size | Find tag: